mirror of
https://github.com/astromaddie/lambda1-vr.git
synced 2026-10-19 01:00:14 +02:00
Compare commits
47
Commits
steam-frame
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89fb3fc07e | ||
|
|
73d20c21aa | ||
|
|
41a599a871 | ||
|
|
b5f1779386 | ||
|
|
f3773cb04d | ||
|
|
1a6749b072 | ||
|
|
04676eefe1 | ||
|
|
4fb7428b9e | ||
|
|
98bdb4405b | ||
|
|
aacf6e5a0f | ||
|
|
2aee8ee1e1 | ||
|
|
77150eeb57 | ||
|
|
7552d2c312 | ||
|
|
3372d5af6e | ||
|
|
3843dda03e | ||
|
|
a5c1c07503 | ||
|
|
57aa2aea08 | ||
|
|
ee43034963 | ||
|
|
b791b82099 | ||
|
|
ad16b48b1f | ||
|
|
208cb7d1c7 | ||
|
|
b5689385e1 | ||
|
|
bd8d932baf | ||
|
|
231f5513a2 | ||
|
|
e6b3e26003 | ||
|
|
3c5d2cc813 | ||
|
|
5ee2b12181 | ||
|
|
afbc400cfe | ||
|
|
a4f12f84fd | ||
|
|
571fed0ef5 | ||
|
|
c97235b543 | ||
|
|
f32023fff2 | ||
|
|
45c66088fd | ||
|
|
51c2ffd4d5 | ||
|
|
b126446f9c | ||
|
|
0220e23845 | ||
|
|
1eac3db3ce | ||
|
|
02e68f7916 | ||
|
|
6ff4159d3c | ||
|
|
daec3490e7 | ||
|
|
ce8b7f8d6d | ||
|
|
5c2769c0ea | ||
|
|
3e9c44b38b | ||
|
|
247d8be660 | ||
|
|
afe3c6f890 | ||
|
|
9ad98dcf51 | ||
|
|
b3058f76f6 |
No files matched your search
@@ -15,3 +15,5 @@
|
||||
*.PDF diff=astextplain
|
||||
*.rtf diff=astextplain
|
||||
*.RTF diff=astextplain
|
||||
# Preserve the exact archived upstream README, including its original whitespace.
|
||||
docs/README_PRE_PR0.md -text -eol whitespace=-blank-at-eol
|
||||
+12
@@ -69,3 +69,15 @@ build/intermediates/lint-cache/maven.google/master-index.xml
|
||||
build/intermediates/lint-cache/sdk-registry.xml/sdk-registry.xml
|
||||
assets/AoMDC/models/gmgeneral_display.aomdc
|
||||
/assets/background
|
||||
|
||||
# Steam Frame build: the Khronos OpenXR loader gets fetched here
|
||||
Projects/Android/build/
|
||||
|
||||
# Steam Frame build: Python for the menu tile
|
||||
tools/steam_frame/.venv/
|
||||
|
||||
# Frame dev: run output from deploy.sh
|
||||
scratchpad/
|
||||
|
||||
# Local Android SDK configuration
|
||||
/local.properties
|
||||
@@ -0,0 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-10-11 — PR0 preparation
|
||||
|
||||
- Updated the Steam Frame README and build information.
|
||||
- Recorded source revisions, existing verification and the specific publication blockers.
|
||||
- Preserved upstream notices and kept game data separate from the source license.
|
||||
|
||||
## 2026-10-11 — public source preparation
|
||||
|
||||
- Prepared the public Frame source fork, checked the host suite and removed tracked machine-specific SDK paths.
|
||||
@@ -11,6 +11,74 @@ buildscript {
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
// Steam Frame build (./gradlew assembleFrame, see docs/STEAM_FRAME_VR.md).
|
||||
// The Frame's Android container has no OpenXR loader broker, so the Khronos
|
||||
// loader goes into the APK. It comes from Maven Central, gets checked against
|
||||
// this hash and is unpacked into build/ (never committed). Quest builds don't
|
||||
// touch any of this.
|
||||
def khronosLoaderVersion = '1.1.63'
|
||||
def khronosLoaderSha256 = '622419d2f6741c3443a3beb4779af0764318edd01830de967f24c741ebcded73'
|
||||
def khronosLoaderDir = file("build/openxr-loader/${khronosLoaderVersion}")
|
||||
def khronosLoaderSo = new File(khronosLoaderDir, 'libopenxr_loader.so')
|
||||
|
||||
def fetchKhronosLoader = tasks.register('fetchKhronosLoader') {
|
||||
outputs.file khronosLoaderSo
|
||||
doLast {
|
||||
def url = "https://repo1.maven.org/maven2/org/khronos/openxr/openxr_loader_for_android/${khronosLoaderVersion}/openxr_loader_for_android-${khronosLoaderVersion}.aar"
|
||||
def aar = new File(khronosLoaderDir, 'openxr_loader_for_android.aar')
|
||||
khronosLoaderDir.mkdirs()
|
||||
new URL(url).withInputStream { input -> aar.withOutputStream { it << input } }
|
||||
def digest = java.security.MessageDigest.getInstance('SHA-256')
|
||||
aar.eachByte(8192) { buf, len -> digest.update(buf, 0, len) }
|
||||
def sha = digest.digest().collect { String.format('%02x', it) }.join()
|
||||
if (sha != khronosLoaderSha256) {
|
||||
aar.delete()
|
||||
throw new GradleException("OpenXR loader AAR has the wrong SHA-256 (${sha}), expected ${khronosLoaderSha256}")
|
||||
}
|
||||
def zip = new java.util.zip.ZipFile(aar)
|
||||
try {
|
||||
def entry = zip.getEntry('jni/arm64-v8a/libopenxr_loader.so')
|
||||
if (entry == null) throw new GradleException('libopenxr_loader.so (arm64-v8a) is not in the loader AAR')
|
||||
zip.getInputStream(entry).withStream { input -> khronosLoaderSo.withOutputStream { it << input } }
|
||||
} finally {
|
||||
zip.close()
|
||||
}
|
||||
aar.delete()
|
||||
}
|
||||
}
|
||||
|
||||
// Steam Frame build: the menu background tile with the Frame's button mapping on it. The release
|
||||
// tile (800_1_a_loading.tga, Team Beef's art, git-ignored) has the Quest's help baked in, so
|
||||
// tools/steam_frame/make_frame_tile.py makes a Frame one into build/frame-assets, and the frame build
|
||||
// type's assets override the release one with it. Nothing generated is committed, and Quest and Pico
|
||||
// builds don't see this. If the release tile or Python with Pillow and numpy is missing, the Frame
|
||||
// APK still builds with the release tile, and this says so.
|
||||
// Python is -PframePython, $FRAME_PYTHON, tools/steam_frame/.venv, or python3, in that order.
|
||||
def frameAssetsDir = file('build/frame-assets')
|
||||
def makeFrameTile = tasks.register('makeFrameTile') {
|
||||
doLast {
|
||||
delete frameAssetsDir // never ship a stale tile after the source went away
|
||||
def venvPython = file('../../tools/steam_frame/.venv/bin/python')
|
||||
def python = project.findProperty('framePython') ?: System.getenv('FRAME_PYTHON') ?:
|
||||
(venvPython.exists() ? venvPython.absolutePath : 'python3')
|
||||
def status = -1
|
||||
try {
|
||||
status = exec {
|
||||
commandLine python, file('../../tools/steam_frame/make_frame_tile.py').absolutePath,
|
||||
'--src', file('../../assets/background/800_1_a_loading.tga').absolutePath,
|
||||
'--out', new File(frameAssetsDir, 'background/800_1_a_loading.tga').absolutePath
|
||||
ignoreExitValue = true
|
||||
}.exitValue
|
||||
} catch (Exception e) {
|
||||
logger.warn("Could not run ${python}: ${e.message}")
|
||||
}
|
||||
if (status != 0) {
|
||||
logger.warn('WARNING: no Frame menu tile, the Frame APK gets the release one (with the Quest button help). ' +
|
||||
'See "Build" in docs/STEAM_FRAME_VR.md.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
namespace = "com.drbeef.lambda1vr"
|
||||
@@ -40,6 +108,13 @@ android {
|
||||
|
||||
minSdkVersion 32
|
||||
targetSdkVersion 32
|
||||
|
||||
// True only in the frame build type
|
||||
buildConfigField 'boolean', 'STEAM_FRAME', 'false'
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
@@ -103,6 +178,29 @@ android {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Valve Steam Frame (Lepton, Android 11). Same as release, plus a
|
||||
// manifest overlay (src/frame) and L1VR_STEAM_FRAME for the native code.
|
||||
// Signed with the debug key, it's only sideloaded.
|
||||
frame {
|
||||
initWith release
|
||||
matchingFallbacks = ['release']
|
||||
applicationIdSuffix ".frame"
|
||||
versionNameSuffix "-frame"
|
||||
signingConfig signingConfigs.debug
|
||||
buildConfigField 'boolean', 'STEAM_FRAME', 'true'
|
||||
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
arguments "L1VR_STEAM_FRAME=1", "APP_PLATFORM=android-30"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets.frame {
|
||||
// generated by makeFrameTile, wins over the release tile in main
|
||||
assets.srcDir frameAssetsDir
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
@@ -133,6 +231,23 @@ android {
|
||||
ndkVersion '25.1.8937393'
|
||||
}
|
||||
|
||||
// Lepton runs Android 11, the Quest build needs 12L (API 32)
|
||||
androidComponents {
|
||||
beforeVariants(selector().withBuildType('frame')) { variantBuilder ->
|
||||
variantBuilder.minSdk = 30
|
||||
}
|
||||
}
|
||||
|
||||
// The loader has to be there before ndk-build links against it
|
||||
tasks.configureEach { task ->
|
||||
if (task.name in ['preBuild', 'preDebugBuild', 'preReleaseBuild', 'preFrameBuild']) {
|
||||
task.dependsOn fetchKhronosLoader
|
||||
}
|
||||
if (task.name == 'mergeFrameAssets') {
|
||||
task.dependsOn makeFrameTile
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "com.android.support:support-compat:28.0.0"
|
||||
implementation "com.android.support:support-core-utils:28.0.0"
|
||||
@@ -148,4 +263,4 @@ repositories {
|
||||
// not found error
|
||||
if (!tasks.findByName("prepareKotlinBuildScriptModel")) {
|
||||
tasks.register("prepareKotlinBuildScriptModel") {}
|
||||
}
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MSYS* | MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
Vendored
+89
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -13,6 +13,10 @@
|
||||
#include "VrInput.h"
|
||||
#include "VrCvars.h"
|
||||
#include "VrCommon.h"
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
#include "VrEyeMath.h"
|
||||
#include "VrDiag.h"
|
||||
#endif
|
||||
|
||||
#include <common/common.h>
|
||||
#include <common/library.h>
|
||||
@@ -28,6 +32,13 @@
|
||||
//Let's go to the maximum!
|
||||
extern float SS_MULTIPLIER;
|
||||
|
||||
//Where the game data is, the same folder the Java side uses
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
#define L1VR_DATA_DIR "/sdcard/Documents/Lambda1VR"
|
||||
#else
|
||||
#define L1VR_DATA_DIR "/sdcard/xash"
|
||||
#endif
|
||||
|
||||
/* global arg_xxx structs */
|
||||
struct arg_dbl *ss;
|
||||
struct arg_int *cpu;
|
||||
@@ -53,6 +64,12 @@ extern int Host_Main( int argc, const char **argv, const char *progname, int bCh
|
||||
|
||||
bool VR_UseScreenLayer()
|
||||
{
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
if (vr_flat_screen != NULL && vr_flat_screen->integer != 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
return (showingScreenLayer || cls.demoplayback || cls.state == ca_cinematic || cls.key_dest != key_game);
|
||||
}
|
||||
|
||||
@@ -316,6 +333,98 @@ void VR_Get2DOffset(int eye, int width, int height, float *dx, float *dy)
|
||||
*dy = (tanU + tanD) / (2.0f * (tanU - tanD)) * (float)height;
|
||||
}
|
||||
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
// which way the 2D space turns for a positive view roll (it's y-down, so the other way to the world)
|
||||
#define HUD_ROLL_SIGN -1.0f
|
||||
|
||||
/*
|
||||
Where an eye is for the world render, from the runtime's own eye pose in head space (the same
|
||||
poses and predicted time the layer is submitted with). offset is in world units, in the
|
||||
head-aligned engine axes. cantDegrees is the eye's turn relative to the head about cantAxis,
|
||||
0 when there is none. False when the eye isn't to be moved: the mono eyes of a scope, and the
|
||||
flat screen layer, where it's the middle that's wanted.
|
||||
*/
|
||||
bool VR_GetEyeTransform(int eye, float worldScale, vec3_t offset, float* cantDegrees, vec3_t cantAxis)
|
||||
{
|
||||
*cantDegrees = 0.0f;
|
||||
if (eye < 0 || eye >= ovrMaxNumEyes || !gAppState.SessionActive || gAppState.Projections == NULL || VR_UseScreenLayer())
|
||||
return false;
|
||||
|
||||
const XrPosef* pose = &gAppState.Projections[eye].pose;
|
||||
const float position[3] = {pose->position.x, pose->position.y, pose->position.z};
|
||||
const float quat[4] = {pose->orientation.x, pose->orientation.y, pose->orientation.z, pose->orientation.w};
|
||||
float offsetOut[3];
|
||||
VrEye_ToEngineOffset(position, worldScale, offsetOut);
|
||||
VectorSet(offset, offsetOut[0], offsetOut[1], offsetOut[2]);
|
||||
|
||||
float axis[3];
|
||||
float degrees;
|
||||
if (VrEye_CantAxisAngle(quat, axis, °rees))
|
||||
{
|
||||
VectorSet(cantAxis, axis[0], axis[1], axis[2]);
|
||||
*cantDegrees = degrees;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
How far to shift the 2D content (the HUD) in an eye's image, in pixels, positive to the right,
|
||||
so that it sits vr_hud_distance metres ahead of the head. 0 for the mono eyes and the screen layer.
|
||||
*/
|
||||
float VR_GetHudShift(int eye)
|
||||
{
|
||||
if (eye < 0 || eye >= ovrMaxNumEyes || !gAppState.SessionActive || gAppState.Projections == NULL ||
|
||||
VR_UseScreenLayer() || vr_hud_distance == NULL)
|
||||
return 0.0f;
|
||||
|
||||
const XrView* view = &gAppState.Projections[eye];
|
||||
const int width = gAppState.Renderer.FrameBuffer[eye].Width;
|
||||
const float shift = VrEye_HudShiftPixels(view->pose.position.x, view->fov.angleLeft, view->fov.angleRight,
|
||||
width, vr_hud_distance->value);
|
||||
|
||||
static bool logged[ovrMaxNumEyes];
|
||||
if (!logged[eye])
|
||||
{
|
||||
logged[eye] = true;
|
||||
ALOGI("[openxr] hud: eye %d at x %.1f mm, focal length %.0f px, vr_hud_distance %.2f m, shift %.1f px",
|
||||
eye, view->pose.position.x * 1000.0f, VrEye_FocalPixels(view->fov.angleLeft, view->fov.angleRight, width),
|
||||
vr_hud_distance->value, shift);
|
||||
}
|
||||
return shift;
|
||||
}
|
||||
|
||||
/*
|
||||
How far to turn the 2D content (the HUD and the text of scripted sequences) so that it stays level
|
||||
with the world when the head rolls: the roll the world is drawn with, the other way. 0 on the screen
|
||||
layer (a world-locked panel, it never rolls with the head) and in a scope.
|
||||
*/
|
||||
/*
|
||||
Whether the camera-facing sprites (a plain sprite, glows, muzzle flashes) face the view with the roll taken
|
||||
out, so they stay upright in the world when the head tilts, as the HUD does. Not in a scope, where the whole
|
||||
view is the gun's.
|
||||
*/
|
||||
bool VR_BillboardsLevel()
|
||||
{
|
||||
return gAppState.SessionActive && !VR_UseScreenLayer() && !isScopeEngaged() && vr_hud_level != NULL && vr_hud_level->integer != 0;
|
||||
}
|
||||
|
||||
float VR_GetHudRoll(int eye)
|
||||
{
|
||||
if (eye < 0 || eye >= ovrMaxNumEyes || !gAppState.SessionActive || VR_UseScreenLayer() || isScopeEngaged() ||
|
||||
vr_hud_level == NULL || vr_hud_level->integer == 0)
|
||||
return 0.0f;
|
||||
|
||||
const float roll = cl.refdef.viewangles[ROLL];
|
||||
static bool logged = false;
|
||||
if (!logged && fabsf(roll) > 5.0f)
|
||||
{
|
||||
logged = true;
|
||||
ALOGI("[vr] hud kept level: the view rolls %.1f degrees, the HUD is turned the other way", roll);
|
||||
}
|
||||
return HUD_ROLL_SIGN * roll;
|
||||
}
|
||||
#endif
|
||||
|
||||
void R_ChangeDisplaySettings( int width, int height, qboolean fullscreen );
|
||||
|
||||
int VR_SetRefreshRate(int refreshRate)
|
||||
@@ -395,6 +504,13 @@ convar_t *vr_quick_crouchjump;
|
||||
convar_t *vr_stereo_side;
|
||||
convar_t *vr_gesture_triggered_use;
|
||||
convar_t *vr_use_gesture_boundary;
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
convar_t *vr_hud_distance;
|
||||
convar_t *vr_hud_level;
|
||||
convar_t *vr_refresh_rate;
|
||||
convar_t *vr_resolution_scale;
|
||||
convar_t *vr_flat_screen;
|
||||
#endif
|
||||
|
||||
|
||||
void initialize_gl4es();
|
||||
@@ -451,9 +567,34 @@ void VR_Init()
|
||||
vr_highlight_actionables = Cvar_Get( "vr_highlight_actionables", "1", CVAR_ARCHIVE, "Set to 0 to disable highlighting of actionable objects/entities" );
|
||||
vr_headtorch = Cvar_Get( "vr_headtorch", "0", CVAR_ARCHIVE, "Set to 1 to enable head-torch flashlight mode" );
|
||||
vr_reversetorch = Cvar_Get( "vr_reversetorch", "0", CVAR_ARCHIVE, "Set to 1 to enable reverse-direction flashlight mode" );
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
// The Frame has a button for crouch, so double clicking jump isn't also crouch unless asked for.
|
||||
// And use is the grips: this cvar makes the game take use from each hand, within a short reach of it
|
||||
// (the automatic use by gesture is off on the Frame, the grips do it)
|
||||
vr_quick_crouchjump = Cvar_Get( "vr_quick_crouchjump", "0", CVAR_ARCHIVE, "Set to 1 to enable quick crouch-jump mode (double clicking jump button triggers duck)" );
|
||||
vr_gesture_triggered_use = Cvar_Get( "vr_gesture_triggered_use", "1", CVAR_ARCHIVE, "On the Steam Frame: 1 is a short reach from each hand for the grips to use (0 is the old 64 units from the weapon hand, and no use from the other hand)" );
|
||||
#else
|
||||
vr_quick_crouchjump = Cvar_Get( "vr_quick_crouchjump", "1", CVAR_ARCHIVE, "Set to 0 to disable quick crouch-jump mode (double clicking jump button triggers duck)" );
|
||||
vr_gesture_triggered_use = Cvar_Get( "vr_gesture_triggered_use", "1", CVAR_ARCHIVE, "Set to 0 to disable use gesture, 1 to enable" );
|
||||
#endif
|
||||
vr_use_gesture_boundary = Cvar_Get( "vr_use_gesture_boundary", "0.35", CVAR_ARCHIVE, "Use gesture boundary" );
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
{
|
||||
// The Frame's controller map changed some defaults that an earlier run may have saved, so
|
||||
// set them once (this number goes up when the map changes defaults again)
|
||||
convar_t* frameMap = Cvar_Get( "vr_frame_map", "0", CVAR_ARCHIVE, "Version of the Steam Frame controller map the saved settings were made for" );
|
||||
if (frameMap->integer < 1) {
|
||||
Cvar_SetFloat( "vr_quick_crouchjump", 0.0f );
|
||||
Cvar_SetFloat( "vr_gesture_triggered_use", 1.0f );
|
||||
Cvar_SetFloat( "vr_frame_map", 1.0f );
|
||||
}
|
||||
}
|
||||
vr_hud_distance = Cvar_Get( "vr_hud_distance", "1.0", CVAR_ARCHIVE, "How far ahead the HUD sits, in metres" );
|
||||
vr_refresh_rate = Cvar_Get( "vr_refresh_rate", "90", CVAR_ARCHIVE, "The display refresh rate to ask for, the highest the runtime offers that isn't over this" );
|
||||
vr_resolution_scale = Cvar_Get( "vr_resolution_scale", "1.25", CVAR_ARCHIVE, "Eye image size as a multiple of the runtime's recommended one (1.25 is the panel's 2160 on the Steam Frame), 0.5 to 2" );
|
||||
vr_flat_screen = Cvar_Get( "vr_flat_screen", "0", CVAR_ARCHIVE, "1 shows the game on a flat screen in front of you instead of around you. The controllers only point and click then, like in a menu" );
|
||||
vr_hud_level = Cvar_Get( "vr_hud_level", "1", CVAR_ARCHIVE, "1 keeps the HUD, text, and camera-facing sprites level when the head rolls, 0 lets them roll with the head" );
|
||||
#endif
|
||||
|
||||
//Not to be changed by users, as it will be overwritten anyway
|
||||
vr_stereo_side = Cvar_Get( "vr_stereo_side", "0", CVAR_READ_ONLY, "Eye being drawn" );
|
||||
@@ -530,18 +671,29 @@ void * AppThreadFunction( void * parm )
|
||||
|
||||
|
||||
TBXR_InitialiseOpenXR();
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
VrDiag_Init();
|
||||
#endif
|
||||
|
||||
Host_Main(argc, (const char**)argv, "valve", false, NULL);
|
||||
|
||||
VR_Init();
|
||||
|
||||
TBXR_EnterVR();
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
{
|
||||
// the eye image size, from vr_resolution_scale (the engine set up its screen before it was known)
|
||||
extern void R_ChangeDisplaySettingsFast( int width, int height );
|
||||
TBXR_SetEyeScale( vr_resolution_scale->value );
|
||||
R_ChangeDisplaySettingsFast( (int)gAppState.Width, (int)gAppState.Height );
|
||||
}
|
||||
#endif
|
||||
TBXR_InitRenderer();
|
||||
TBXR_InitActions();
|
||||
TBXR_WaitForSessionActive();
|
||||
|
||||
//Always use this folder
|
||||
chdir("/sdcard/xash");
|
||||
chdir(L1VR_DATA_DIR);
|
||||
|
||||
bool destroyed = false;
|
||||
while (!destroyed)
|
||||
@@ -555,6 +707,32 @@ void * AppThreadFunction( void * parm )
|
||||
{
|
||||
TBXR_FrameSetup();
|
||||
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
{
|
||||
//The positional movement scaling goes by vr_refresh. SteamVR picks the rate for
|
||||
//the app, so take it from the frame period rather than from what's on offer.
|
||||
static int lastRate = 0;
|
||||
const XrDuration period = gAppState.FrameState.predictedDisplayPeriod;
|
||||
const int rate = period > 0 ? (int)(1e9 / (double)period + 0.5) : 0;
|
||||
if (rate > 0 && rate != lastRate)
|
||||
{
|
||||
char text[16];
|
||||
Q_snprintf(text, sizeof(text), "%d", rate);
|
||||
Cvar_Set2("vr_refresh", text, true);
|
||||
ALOGI("[openxr] frame period %.2f ms, vr_refresh %s", period / 1e6, text);
|
||||
lastRate = rate;
|
||||
}
|
||||
}
|
||||
VrDiag_FrameBegin();
|
||||
if (!TBXR_ShouldRender())
|
||||
{
|
||||
//Standby: keep the frame loop going, but the game waits too
|
||||
TBXR_submitFrame();
|
||||
VrDiag_FrameEnd();
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
//Call the game drawing code to populate the cylinder layer texture
|
||||
//if we are now shutting down, drop out here
|
||||
if (isHostAlive())
|
||||
@@ -570,7 +748,11 @@ void * AppThreadFunction( void * parm )
|
||||
{
|
||||
TBXR_prepareEyeBuffer(eye);
|
||||
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
if (TBXR_ShouldRender())
|
||||
#else
|
||||
if (gAppState.FrameState.shouldRender)
|
||||
#endif
|
||||
{
|
||||
if (isScopeEngaged())
|
||||
{
|
||||
@@ -676,12 +858,18 @@ void * AppThreadFunction( void * parm )
|
||||
GL( glDisable( GL_SCISSOR_TEST ) );
|
||||
}
|
||||
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
VrDiag_AfterEye(eye);
|
||||
#endif
|
||||
TBXR_finishEyeBuffer(eye);
|
||||
}
|
||||
|
||||
Host_EndFrame();
|
||||
|
||||
TBXR_submitFrame();
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
VrDiag_FrameEnd();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -717,6 +905,8 @@ static jobject jniCallbackObj=0;
|
||||
int JNI_OnLoad(JavaVM* vm, void* reserved)
|
||||
{
|
||||
JNIEnv *env;
|
||||
//jni_shutdown needs this, nothing else sets it
|
||||
jVM = vm;
|
||||
if((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK)
|
||||
{
|
||||
ALOGE("Failed JNI_OnLoad");
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "VrCommon.h"
|
||||
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
#include "VrCvars.h"
|
||||
#include "VrInputFrame.h"
|
||||
#include "VrDiag.h"
|
||||
#endif
|
||||
|
||||
extern ovrApp gAppState;
|
||||
|
||||
XrResult CheckXrResult(XrResult res, const char* originator) {
|
||||
@@ -45,6 +53,14 @@ XrAction BTouchAction = 0;
|
||||
XrAction XTouchAction = 0;
|
||||
XrAction YTouchAction = 0;
|
||||
XrAction aimAction = 0;
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
XrAction viewAction = 0;
|
||||
XrAction bumperAction = 0;
|
||||
XrAction dpadUpAction = 0;
|
||||
XrAction dpadDownAction = 0;
|
||||
XrAction dpadLeftAction = 0;
|
||||
XrAction dpadRightAction = 0;
|
||||
#endif
|
||||
|
||||
XrSpace aimSpace[SIDE_COUNT];
|
||||
XrPath handSubactionPath[SIDE_COUNT];
|
||||
@@ -128,6 +144,122 @@ void CreateAction(
|
||||
OXR(xrCreateAction(actionSet, &aci, action));
|
||||
}
|
||||
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
/*
|
||||
================================================================================
|
||||
|
||||
Steam Frame controllers
|
||||
|
||||
The Frame's controllers are a split gamepad: the left one has the D-pad, View,
|
||||
bumper, trigger, grip and stick, the right one has A B X Y, Menu, bumper, trigger,
|
||||
grip and stick. What each button does is in VrFrameMap.c (and the table in
|
||||
docs/STEAM_FRAME_VR.md); this file reads them, VrInputFrame.c applies the map.
|
||||
|
||||
Every path has to exist on the profile it's suggested for, or the runtime refuses
|
||||
the whole profile (the table check is tools/steam_frame/tests/check_bindings.py
|
||||
in the dev branch).
|
||||
|
||||
================================================================================
|
||||
*/
|
||||
|
||||
#define FRAME_PROFILE "/interaction_profiles/valve/frame_controller_valve"
|
||||
#define INDEX_PROFILE "/interaction_profiles/valve/index_controller"
|
||||
|
||||
typedef struct {
|
||||
XrAction* action;
|
||||
const char* path;
|
||||
} profileBinding_t;
|
||||
|
||||
static const profileBinding_t frameBindings[] = {
|
||||
{ &touchpadAction, "/user/hand/left/input/thumbstick/click" },
|
||||
{ &touchpadAction, "/user/hand/right/input/thumbstick/click" },
|
||||
{ &joystickAction, "/user/hand/left/input/thumbstick" },
|
||||
{ &joystickAction, "/user/hand/right/input/thumbstick" },
|
||||
{ &thumbstickTouchAction, "/user/hand/left/input/thumbstick/touch" },
|
||||
{ &thumbstickTouchAction, "/user/hand/right/input/thumbstick/touch" },
|
||||
{ &triggerAction, "/user/hand/left/input/trigger/value" },
|
||||
{ &triggerAction, "/user/hand/right/input/trigger/value" },
|
||||
{ &GripAction, "/user/hand/left/input/squeeze/value" },
|
||||
{ &GripAction, "/user/hand/right/input/squeeze/value" },
|
||||
{ &poseAction, "/user/hand/left/input/grip/pose" },
|
||||
{ &poseAction, "/user/hand/right/input/grip/pose" },
|
||||
{ &aimAction, "/user/hand/left/input/aim/pose" },
|
||||
{ &aimAction, "/user/hand/right/input/aim/pose" },
|
||||
{ &vibrateAction, "/user/hand/left/output/haptic" },
|
||||
{ &vibrateAction, "/user/hand/right/output/haptic" },
|
||||
{ &AAction, "/user/hand/right/input/a/click" },
|
||||
{ &BAction, "/user/hand/right/input/b/click" },
|
||||
{ &XAction, "/user/hand/right/input/x/click" },
|
||||
{ &YAction, "/user/hand/right/input/y/click" },
|
||||
{ &backAction, "/user/hand/right/input/menu/click" },
|
||||
{ &viewAction, "/user/hand/left/input/view/click" },
|
||||
{ &bumperAction, "/user/hand/left/input/bumper/click" },
|
||||
{ &bumperAction, "/user/hand/right/input/bumper/click" },
|
||||
{ &dpadUpAction, "/user/hand/left/input/dpad_up/click" },
|
||||
{ &dpadDownAction, "/user/hand/left/input/dpad_down/click" },
|
||||
{ &dpadLeftAction, "/user/hand/left/input/dpad_left/click" },
|
||||
{ &dpadRightAction, "/user/hand/left/input/dpad_right/click" },
|
||||
};
|
||||
|
||||
//Index controllers on SteamVR: A and B on both hands, like the Touch ones. They
|
||||
//have no Menu button for apps, so the pause is the left trackpad pressed hard.
|
||||
static const profileBinding_t indexBindings[] = {
|
||||
{ &touchpadAction, "/user/hand/left/input/thumbstick/click" },
|
||||
{ &touchpadAction, "/user/hand/right/input/thumbstick/click" },
|
||||
{ &joystickAction, "/user/hand/left/input/thumbstick" },
|
||||
{ &joystickAction, "/user/hand/right/input/thumbstick" },
|
||||
{ &thumbstickTouchAction, "/user/hand/left/input/thumbstick/touch" },
|
||||
{ &thumbstickTouchAction, "/user/hand/right/input/thumbstick/touch" },
|
||||
{ &triggerAction, "/user/hand/left/input/trigger/value" },
|
||||
{ &triggerAction, "/user/hand/right/input/trigger/value" },
|
||||
{ &TriggerTouchAction, "/user/hand/left/input/trigger/touch" },
|
||||
{ &TriggerTouchAction, "/user/hand/right/input/trigger/touch" },
|
||||
{ &GripAction, "/user/hand/left/input/squeeze/value" },
|
||||
{ &GripAction, "/user/hand/right/input/squeeze/value" },
|
||||
{ &poseAction, "/user/hand/left/input/grip/pose" },
|
||||
{ &poseAction, "/user/hand/right/input/grip/pose" },
|
||||
{ &aimAction, "/user/hand/left/input/aim/pose" },
|
||||
{ &aimAction, "/user/hand/right/input/aim/pose" },
|
||||
{ &vibrateAction, "/user/hand/left/output/haptic" },
|
||||
{ &vibrateAction, "/user/hand/right/output/haptic" },
|
||||
{ &XAction, "/user/hand/left/input/a/click" },
|
||||
{ &YAction, "/user/hand/left/input/b/click" },
|
||||
{ &AAction, "/user/hand/right/input/a/click" },
|
||||
{ &BAction, "/user/hand/right/input/b/click" },
|
||||
{ &XTouchAction, "/user/hand/left/input/a/touch" },
|
||||
{ &YTouchAction, "/user/hand/left/input/b/touch" },
|
||||
{ &ATouchAction, "/user/hand/right/input/a/touch" },
|
||||
{ &BTouchAction, "/user/hand/right/input/b/touch" },
|
||||
{ &backAction, "/user/hand/left/input/trackpad/force" },
|
||||
};
|
||||
|
||||
static XrResult TBXR_SuggestBindings(const char* profile, const profileBinding_t* table, int count)
|
||||
{
|
||||
XrPath profilePath;
|
||||
CHECK_XRCMD(xrStringToPath(gAppState.Instance, profile, &profilePath));
|
||||
|
||||
XrActionSuggestedBinding bindings[128];
|
||||
for (int i = 0; i < count; i++) {
|
||||
XrPath path;
|
||||
CHECK_XRCMD(xrStringToPath(gAppState.Instance, table[i].path, &path));
|
||||
bindings[i] = ActionSuggestedBinding(*table[i].action, path);
|
||||
}
|
||||
|
||||
XrInteractionProfileSuggestedBinding suggestedBindings = {};
|
||||
suggestedBindings.type = XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING;
|
||||
suggestedBindings.interactionProfile = profilePath;
|
||||
suggestedBindings.suggestedBindings = bindings;
|
||||
suggestedBindings.countSuggestedBindings = count;
|
||||
suggestedBindings.next = NULL;
|
||||
XrResult result = xrSuggestInteractionProfileBindings(gAppState.Instance, &suggestedBindings);
|
||||
|
||||
char resultString[XR_MAX_RESULT_STRING_SIZE];
|
||||
xrResultToString(gAppState.Instance, result, resultString);
|
||||
ALOGI("[openxr] suggested %d bindings for %s: %s", count, profile, resultString);
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
void TBXR_InitActions( void )
|
||||
{
|
||||
// Create an action set.
|
||||
@@ -182,6 +314,14 @@ void TBXR_InitActions( void )
|
||||
CreateAction(actionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "btouch", "Btouch", SIDE_COUNT, handSubactionPath, &BTouchAction);
|
||||
CreateAction(actionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "xtouch", "Xtouch", SIDE_COUNT, handSubactionPath, &XTouchAction);
|
||||
CreateAction(actionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "ytouch", "Ytouch", SIDE_COUNT, handSubactionPath, &YTouchAction);
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
CreateAction(actionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "viewkey", "Viewkey", SIDE_COUNT, handSubactionPath, &viewAction);
|
||||
CreateAction(actionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "bumperkey", "Bumperkey", SIDE_COUNT, handSubactionPath, &bumperAction);
|
||||
CreateAction(actionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "dpadup", "Dpadup", SIDE_COUNT, handSubactionPath, &dpadUpAction);
|
||||
CreateAction(actionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "dpaddown", "Dpaddown", SIDE_COUNT, handSubactionPath, &dpadDownAction);
|
||||
CreateAction(actionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "dpadleft", "Dpadleft", SIDE_COUNT, handSubactionPath, &dpadLeftAction);
|
||||
CreateAction(actionSet, XR_ACTION_TYPE_BOOLEAN_INPUT, "dpadright", "Dpadright", SIDE_COUNT, handSubactionPath, &dpadRightAction);
|
||||
#endif
|
||||
}
|
||||
|
||||
XrPath selectPath[SIDE_COUNT];
|
||||
@@ -276,6 +416,17 @@ void TBXR_InitActions( void )
|
||||
|
||||
XrResult result;
|
||||
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
//SteamVR picks whichever profile matches the controllers, so suggest all of
|
||||
//them: the Frame's own first, then Index, and Touch below
|
||||
if (TBXR_ExtensionEnabled(XR_VALVE_FRAME_CONTROLLER_INTERACTION_EXTENSION_NAME)) {
|
||||
TBXR_SuggestBindings(FRAME_PROFILE, frameBindings, sizeof(frameBindings) / sizeof(frameBindings[0]));
|
||||
} else {
|
||||
ALOGI("[openxr] the runtime doesn't offer the Frame controller extension, no suggestion for %s", FRAME_PROFILE);
|
||||
}
|
||||
TBXR_SuggestBindings(INDEX_PROFILE, indexBindings, sizeof(indexBindings) / sizeof(indexBindings[0]));
|
||||
result = XR_ERROR_PATH_UNSUPPORTED;
|
||||
#else
|
||||
//First try Pico Devices
|
||||
{
|
||||
XrPath picoMixedRealityInteractionProfilePath;
|
||||
@@ -334,6 +485,7 @@ void TBXR_InitActions( void )
|
||||
suggestedBindings.next = NULL;
|
||||
result = xrSuggestInteractionProfileBindings(gAppState.Instance, &suggestedBindings);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (result != XR_SUCCESS)
|
||||
{
|
||||
@@ -385,6 +537,13 @@ void TBXR_InitActions( void )
|
||||
suggestedBindings.countSuggestedBindings = currBinding;
|
||||
suggestedBindings.next = NULL;
|
||||
result = xrSuggestInteractionProfileBindings(gAppState.Instance, &suggestedBindings);
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
{
|
||||
char resultString[XR_MAX_RESULT_STRING_SIZE];
|
||||
xrResultToString(gAppState.Instance, result, resultString);
|
||||
ALOGI("[openxr] suggested %d bindings for /interaction_profiles/oculus/touch_controller: %s", currBinding, resultString);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
XrActionSpaceCreateInfo actionSpaceInfo = {};
|
||||
@@ -427,6 +586,81 @@ void TBXR_SyncActions( void )
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
static bool usingFrameProfile = false;
|
||||
|
||||
//Which profile the runtime is using for the controllers right now, and does it
|
||||
//have our Frame layout
|
||||
static void TBXR_UpdateInteractionProfile()
|
||||
{
|
||||
static char lastProfile[SIDE_COUNT][XR_MAX_PATH_LENGTH];
|
||||
|
||||
bool frame = false;
|
||||
for (int hand = 0; hand < SIDE_COUNT; hand++) {
|
||||
XrInteractionProfileState state = {};
|
||||
state.type = XR_TYPE_INTERACTION_PROFILE_STATE;
|
||||
char profile[XR_MAX_PATH_LENGTH] = "";
|
||||
if (XR_SUCCEEDED(xrGetCurrentInteractionProfile(gAppState.Session, handSubactionPath[hand], &state)) &&
|
||||
state.interactionProfile != XR_NULL_PATH) {
|
||||
uint32_t length = 0;
|
||||
xrPathToString(gAppState.Instance, state.interactionProfile, sizeof(profile), &length, profile);
|
||||
}
|
||||
|
||||
if (strcmp(profile, lastProfile[hand]) != 0) {
|
||||
strcpy(lastProfile[hand], profile);
|
||||
ALOGI("[openxr] %s controller profile: %s", hand == SIDE_LEFT ? "left" : "right",
|
||||
profile[0] ? profile : "(none yet)");
|
||||
}
|
||||
frame |= (strcmp(profile, FRAME_PROFILE) == 0);
|
||||
}
|
||||
usingFrameProfile = frame;
|
||||
}
|
||||
|
||||
//What the physical buttons say this frame
|
||||
static void TBXR_ReadFrameInputs(frameRaw_t* raw)
|
||||
{
|
||||
raw->triggerLeft = GetActionStateFloat(triggerAction, SIDE_LEFT).currentState;
|
||||
raw->triggerRight = GetActionStateFloat(triggerAction, SIDE_RIGHT).currentState;
|
||||
raw->gripLeft = GetActionStateFloat(GripAction, SIDE_LEFT).currentState;
|
||||
raw->gripRight = GetActionStateFloat(GripAction, SIDE_RIGHT).currentState;
|
||||
raw->bumperLeft = GetActionStateBoolean(bumperAction, SIDE_LEFT).currentState != 0;
|
||||
raw->bumperRight = GetActionStateBoolean(bumperAction, SIDE_RIGHT).currentState != 0;
|
||||
raw->stickClickLeft = GetActionStateBoolean(touchpadAction, SIDE_LEFT).currentState != 0;
|
||||
raw->stickClickRight = GetActionStateBoolean(touchpadAction, SIDE_RIGHT).currentState != 0;
|
||||
raw->a = GetActionStateBoolean(AAction, SIDE_RIGHT).currentState != 0;
|
||||
raw->b = GetActionStateBoolean(BAction, SIDE_RIGHT).currentState != 0;
|
||||
raw->x = GetActionStateBoolean(XAction, SIDE_RIGHT).currentState != 0;
|
||||
raw->y = GetActionStateBoolean(YAction, SIDE_RIGHT).currentState != 0;
|
||||
raw->menu = GetActionStateBoolean(backAction, SIDE_RIGHT).currentState != 0;
|
||||
raw->view = GetActionStateBoolean(viewAction, SIDE_LEFT).currentState != 0;
|
||||
raw->dpadUp = GetActionStateBoolean(dpadUpAction, SIDE_LEFT).currentState != 0;
|
||||
raw->dpadDown = GetActionStateBoolean(dpadDownAction, SIDE_LEFT).currentState != 0;
|
||||
raw->dpadLeft = GetActionStateBoolean(dpadLeftAction, SIDE_LEFT).currentState != 0;
|
||||
raw->dpadRight = GetActionStateBoolean(dpadRightAction, SIDE_LEFT).currentState != 0;
|
||||
|
||||
const XrActionStateVector2f left = GetActionStateVector2(joystickAction, SIDE_LEFT);
|
||||
const XrActionStateVector2f right = GetActionStateVector2(joystickAction, SIDE_RIGHT);
|
||||
raw->stickLeftX = left.currentState.x;
|
||||
raw->stickLeftY = left.currentState.y;
|
||||
raw->stickRightX = right.currentState.x;
|
||||
raw->stickRightY = right.currentState.y;
|
||||
}
|
||||
|
||||
static void TBXR_UpdateFrameLayout()
|
||||
{
|
||||
TBXR_UpdateInteractionProfile();
|
||||
// (with diag_press the map runs without controllers, to be tested)
|
||||
if (!usingFrameProfile && !VrDiag_Injecting()) {
|
||||
return;
|
||||
}
|
||||
|
||||
frameRaw_t raw;
|
||||
TBXR_ReadFrameInputs(&raw);
|
||||
VrDiag_InjectInputs(&raw);
|
||||
VrInputFrame_Apply(&raw);
|
||||
}
|
||||
#endif
|
||||
|
||||
void TBXR_UpdateControllers( )
|
||||
{
|
||||
TBXR_SyncActions();
|
||||
@@ -446,6 +680,9 @@ void TBXR_UpdateControllers( )
|
||||
gAppState.TrackedController[i].Active = (loc.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) != 0;
|
||||
gAppState.TrackedController[i].Pose = loc.pose;
|
||||
gAppState.TrackedController[i].Velocity = vel;
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
VrDiag_SyntheticHand(i, &gAppState.TrackedController[i]);
|
||||
#endif
|
||||
}
|
||||
|
||||
leftRemoteTracking_new = gAppState.TrackedController[0];
|
||||
@@ -498,6 +735,10 @@ void TBXR_UpdateControllers( )
|
||||
moveJoystickState = GetActionStateVector2(joystickAction, SIDE_RIGHT);
|
||||
rightTrackedRemoteState_new.Joystick.x = moveJoystickState.currentState.x;
|
||||
rightTrackedRemoteState_new.Joystick.y = moveJoystickState.currentState.y;
|
||||
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
TBXR_UpdateFrameLayout();
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -30,13 +30,20 @@
|
||||
|
||||
#define ALOGE(...) __android_log_print( ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__ )
|
||||
|
||||
#define ALOGI(...) __android_log_print( ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__ )
|
||||
|
||||
#if DEBUG
|
||||
#define ALOGV(...) __android_log_print( ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__ )
|
||||
#else
|
||||
#define ALOGV(...)
|
||||
#endif
|
||||
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
// the projection or menu layer, then the menu laser's beam and its dot
|
||||
enum { ovrMaxLayerCount = 3 };
|
||||
#else
|
||||
enum { ovrMaxLayerCount = 1 };
|
||||
#endif
|
||||
enum { ovrMaxNumEyes = 2 };
|
||||
|
||||
typedef enum xrButton_ {
|
||||
@@ -354,4 +361,23 @@ void TBXR_prepareEyeBuffer(int eye );
|
||||
void TBXR_finishEyeBuffer(int eye );
|
||||
void TBXR_submitFrame();
|
||||
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
//Steam Frame only
|
||||
#define XR_VALVE_FRAME_CONTROLLER_INTERACTION_EXTENSION_NAME "XR_VALVE_frame_controller_interaction"
|
||||
|
||||
bool TBXR_ExtensionEnabled(const char* name);
|
||||
bool TBXR_ShouldRender();
|
||||
void TBXR_RecenterToHead(bool resetHeight);
|
||||
|
||||
// The menu laser: where the weapon hand's aim ray meets the flat screen, 0..1 across and down. False
|
||||
// when the screen isn't showing or the hand isn't tracked (the ray can also miss the panel's edge).
|
||||
bool TBXR_ScreenPointer(float* u, float* v);
|
||||
|
||||
// Dev: the pointer layers' poses, one line each, for the dump
|
||||
void TBXR_DescribePointerLayers(FILE* file);
|
||||
|
||||
// The eye image size from the recommended one times a scale, held to the limits. Before the swapchains are made.
|
||||
float TBXR_SetEyeScale(float scale);
|
||||
#endif
|
||||
|
||||
#endif //vrcommon_h
|
||||
@@ -33,4 +33,12 @@ extern convar_t *vr_gesture_triggered_use;
|
||||
extern convar_t *vr_use_gesture_boundary;
|
||||
|
||||
//Used and updated continuously during rendering
|
||||
extern convar_t *vr_stereo_side;
|
||||
extern convar_t *vr_stereo_side;
|
||||
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
extern convar_t *vr_hud_distance;
|
||||
extern convar_t *vr_hud_level;
|
||||
extern convar_t *vr_refresh_rate;
|
||||
extern convar_t *vr_resolution_scale;
|
||||
extern convar_t *vr_flat_screen;
|
||||
#endif
|
||||
@@ -0,0 +1,578 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <math.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <common/common.h>
|
||||
#include <client/client.h>
|
||||
|
||||
#include "VrDiag.h"
|
||||
#include "VrDiagCore.h"
|
||||
#include "VrEyeMath.h"
|
||||
#include "VrFrameMap.h"
|
||||
#include "VrCommon.h"
|
||||
|
||||
extern convar_t *vr_worldscale;
|
||||
extern ovrApp gAppState;
|
||||
extern float snapTurn;
|
||||
void TBXR_exit(int exitCode);
|
||||
|
||||
#define DIAG_DIR "/sdcard/Documents/Lambda1VR"
|
||||
#define DIAG_FILE DIAG_DIR "/vr-diag.ini"
|
||||
#define DUMP_DIR DIAG_DIR "/vr-dumps"
|
||||
|
||||
static vrDiagConfig_t config;
|
||||
static bool enabled = false;
|
||||
|
||||
static long frames = 0; // XR frames since the loop started
|
||||
static long renderedFrames = 0; // the ones the game was drawn for
|
||||
static bool mapStarted = false;
|
||||
static long mapStartFrame = 0;
|
||||
static bool cmdDone = false;
|
||||
static bool dumpDone = false;
|
||||
static bool dumpEyeDone[2];
|
||||
|
||||
// perf window
|
||||
static double windowStart = 0.0;
|
||||
static double lastBegin = 0.0;
|
||||
static double cpuSum = 0.0;
|
||||
static double waitSum = 0.0, acquireSum = 0.0; // ms in xrWaitFrame and xrAcquireSwapchainImage (+ its wait)
|
||||
static double threadStart = 0.0, threadCpuNow = 0.0; // the game thread's CPU time
|
||||
static long windowFrames = 0;
|
||||
static long windowMissed = 0;
|
||||
|
||||
void VrDiag_NoteTiming(int what, double ms)
|
||||
{
|
||||
if (what == VRDIAG_TIME_WAIT_FRAME)
|
||||
waitSum += ms;
|
||||
else
|
||||
acquireSum += ms;
|
||||
}
|
||||
|
||||
static double threadCpuMs(void)
|
||||
{
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now);
|
||||
return now.tv_sec * 1000.0 + now.tv_nsec / 1e6;
|
||||
}
|
||||
|
||||
void VrDiag_Init(void)
|
||||
{
|
||||
VrDiagCore_Defaults(&config);
|
||||
|
||||
FILE* file = fopen(DIAG_FILE, "rb");
|
||||
if (file == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
char text[4096];
|
||||
size_t length = fread(text, 1, sizeof(text) - 1, file);
|
||||
fclose(file);
|
||||
text[length] = '\0';
|
||||
|
||||
if (VrDiagCore_Parse(text, &config) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
enabled = true;
|
||||
ALOGI("[vr-diag] %s: force_render=%d diag_yaw=%.1f autostart=%d map=%s dump_frame=%d exit_after=%d perf=%d probe=%d oneshot=%d cmd=%s diag_roll=%.1f diag_gap=%.2f presses=%d",
|
||||
DIAG_FILE, config.forceRender, config.yaw, config.autostart, config.map[0] ? config.map : "-",
|
||||
config.dumpFrame, config.exitAfter, config.perf, config.probe, config.oneshot, config.cmd[0] ? config.cmd : "-", config.roll, config.gap, config.pressCount);
|
||||
|
||||
if (config.oneshot)
|
||||
{
|
||||
rename(DIAG_FILE, DIAG_FILE ".last");
|
||||
ALOGI("[vr-diag] one-shot: the file is now vr-diag.ini.last");
|
||||
}
|
||||
}
|
||||
|
||||
bool VrDiag_ForceRender(void)
|
||||
{
|
||||
return enabled && config.forceRender;
|
||||
}
|
||||
|
||||
bool VrDiag_Injecting(void)
|
||||
{
|
||||
return enabled && config.pressCount > 0;
|
||||
}
|
||||
|
||||
bool VrDiag_InjectInputs(frameRaw_t* raw)
|
||||
{
|
||||
if (!VrDiag_Injecting() || !mapStarted)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool any = false;
|
||||
for (int i = 0; i < config.pressCount; i++)
|
||||
{
|
||||
const vrDiagPress_t* press = &config.presses[i];
|
||||
const long first = mapStartFrame + press->start;
|
||||
if (renderedFrames < first || renderedFrames >= first + press->length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
any = true;
|
||||
switch (press->input)
|
||||
{
|
||||
case FI_L_TRIGGER: raw->triggerLeft = 1.0f; break;
|
||||
case FI_R_TRIGGER: raw->triggerRight = 1.0f; break;
|
||||
case FI_L_GRIP: raw->gripLeft = 1.0f; break;
|
||||
case FI_R_GRIP: raw->gripRight = 1.0f; break;
|
||||
case FI_L_BUMPER: raw->bumperLeft = true; break;
|
||||
case FI_R_BUMPER: raw->bumperRight = true; break;
|
||||
case FI_L_STICK_CLICK: raw->stickClickLeft = true; break;
|
||||
case FI_R_STICK_CLICK: raw->stickClickRight = true; break;
|
||||
case FI_A: raw->a = true; break;
|
||||
case FI_B: raw->b = true; break;
|
||||
case FI_X: raw->x = true; break;
|
||||
case FI_Y: raw->y = true; break;
|
||||
case FI_MENU_SHORT: case FI_MENU_HOLD: raw->menu = true; break;
|
||||
case FI_VIEW_SHORT: case FI_VIEW_HOLD_1S: case FI_VIEW_HOLD_3S: raw->view = true; break;
|
||||
case FI_DPAD_UP: raw->dpadUp = true; break;
|
||||
case FI_DPAD_DOWN: raw->dpadDown = true; break;
|
||||
case FI_DPAD_LEFT: raw->dpadLeft = true; break;
|
||||
case FI_DPAD_RIGHT: raw->dpadRight = true; break;
|
||||
case FI_L_STICK_UP: raw->stickLeftY = 0.9f; break;
|
||||
case FI_L_STICK_DOWN: raw->stickLeftY = -0.9f; break;
|
||||
case FI_L_STICK_LEFT: raw->stickLeftX = -0.9f; break;
|
||||
case FI_L_STICK_RIGHT: raw->stickLeftX = 0.9f; break;
|
||||
case FI_R_STICK_UP: raw->stickRightY = 0.9f; break;
|
||||
case FI_R_STICK_DOWN: raw->stickRightY = -0.9f; break;
|
||||
case FI_R_STICK_LEFT: raw->stickRightX = -0.9f; break;
|
||||
case FI_R_STICK_RIGHT: raw->stickRightX = 0.9f; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
return any;
|
||||
}
|
||||
|
||||
static XrQuaternionf yawQuat(float degrees)
|
||||
{
|
||||
XrQuaternionf q = {0.0f, sinf(DEG2RAD(degrees) * 0.5f), 0.0f, cosf(DEG2RAD(degrees) * 0.5f)};
|
||||
return q;
|
||||
}
|
||||
|
||||
bool VrDiag_SyntheticHead(XrPosef* pose)
|
||||
{
|
||||
if (!VrDiag_ForceRender())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
memset(pose, 0, sizeof(*pose));
|
||||
// yaw about up, then roll about the view axis (z)
|
||||
const XrQuaternionf y = yawQuat(config.yaw);
|
||||
const float half = DEG2RAD(config.roll) * 0.5f;
|
||||
const XrQuaternionf r = {0.0f, 0.0f, sinf(half), cosf(half)};
|
||||
pose->orientation.x = y.w * r.x + y.x * r.w + y.y * r.z - y.z * r.y;
|
||||
pose->orientation.y = y.w * r.y - y.x * r.z + y.y * r.w + y.z * r.x;
|
||||
pose->orientation.z = y.w * r.z + y.x * r.y - y.y * r.x + y.z * r.w;
|
||||
pose->orientation.w = y.w * r.w - y.x * r.x - y.y * r.y - y.z * r.z;
|
||||
pose->position.y = 1.6f; // standing height above the floor
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VrDiag_SyntheticViews(XrView* views)
|
||||
{
|
||||
if (!VrDiag_ForceRender())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The Frame's usual asymmetric field of view, when the runtime gave none
|
||||
// left eye L -1.028 R 0.885 U 0.864 D -1.048, the right eye is its mirror
|
||||
for (int eye = 0; eye < ovrMaxNumEyes; eye++)
|
||||
{
|
||||
XrFovf* fov = &views[eye].fov;
|
||||
if (fabsf(fov->angleLeft) + fabsf(fov->angleRight) < 0.1f)
|
||||
{
|
||||
fov->angleLeft = (eye == 0) ? -1.028f : -0.885f;
|
||||
fov->angleRight = (eye == 0) ? 0.885f : 1.028f;
|
||||
fov->angleUp = 0.864f;
|
||||
fov->angleDown = -1.048f;
|
||||
}
|
||||
views[eye].pose.orientation.w = 1.0f;
|
||||
views[eye].pose.position.x = (eye == 0) ? -0.032f : 0.032f; // 64 mm
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VrDiag_SyntheticHand(int hand, ovrTrackedController* controller)
|
||||
{
|
||||
if (!VrDiag_ForceRender())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parked in front of the chest, turned with the head
|
||||
const float side = (hand == 0) ? -config.gap * 0.5f : config.gap * 0.5f;
|
||||
const float rad = DEG2RAD(config.yaw);
|
||||
const float x = side;
|
||||
const float z = -0.3f;
|
||||
controller->Active = GL_TRUE;
|
||||
memset(&controller->Pose, 0, sizeof(controller->Pose));
|
||||
{
|
||||
// aimed where the head looks, or turned from it: yaw about up, then pitch about the hand's own x
|
||||
const XrQuaternionf y = yawQuat(config.yaw + config.handYaw);
|
||||
const float half = DEG2RAD(config.handPitch) * 0.5f;
|
||||
const XrQuaternionf p = {sinf(half), 0.0f, 0.0f, cosf(half)};
|
||||
controller->Pose.orientation.x = y.w * p.x + y.x * p.w + y.y * p.z - y.z * p.y;
|
||||
controller->Pose.orientation.y = y.w * p.y - y.x * p.z + y.y * p.w + y.z * p.x;
|
||||
controller->Pose.orientation.z = y.w * p.z + y.x * p.y - y.y * p.x + y.z * p.w;
|
||||
controller->Pose.orientation.w = y.w * p.w - y.x * p.x - y.y * p.y - y.z * p.z;
|
||||
}
|
||||
controller->Pose.position.x = x * cosf(rad) + z * sinf(rad);
|
||||
controller->Pose.position.y = 1.2f;
|
||||
controller->Pose.position.z = -x * sinf(rad) + z * cosf(rad);
|
||||
memset(&controller->Velocity, 0, sizeof(controller->Velocity));
|
||||
controller->Velocity.type = XR_TYPE_SPACE_VELOCITY;
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char* connectionName(void)
|
||||
{
|
||||
switch (cls.state)
|
||||
{
|
||||
case ca_uninitialized: return "uninitialized";
|
||||
case ca_disconnected: return "disconnected";
|
||||
case ca_connecting: return "connecting";
|
||||
case ca_connected: return "connected";
|
||||
case ca_active: return "active";
|
||||
case ca_cinematic: return "cinematic";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// Ground truth for the stereo check: rays from the left eye through a grid of pixels, traced
|
||||
// into the world, so each gets the depth along the view axis and the disparity a correct
|
||||
// stereo pair should show there (the eye separation times the focal length over the depth).
|
||||
static void dumpDepthProbes(FILE* file)
|
||||
{
|
||||
if (gAppState.Projections == NULL || cls.state != ca_active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const XrView* v0 = &gAppState.Projections[0];
|
||||
const XrView* v1 = &gAppState.Projections[1];
|
||||
const int width = gAppState.Renderer.FrameBuffer[0].Width;
|
||||
const int height = gAppState.Renderer.FrameBuffer[0].Height;
|
||||
const float fx = VrEye_FocalPixels(v0->fov.angleLeft, v0->fov.angleRight, width);
|
||||
const float fy = (float)height / (tanf(v0->fov.angleUp) - tanf(v0->fov.angleDown));
|
||||
const float axisX = VrEye_AxisPixel(v0->fov.angleLeft, v0->fov.angleRight, width);
|
||||
const float axisY = tanf(v0->fov.angleUp) / (tanf(v0->fov.angleUp) - tanf(v0->fov.angleDown)) * (float)height;
|
||||
const float ipd = v1->pose.position.x - v0->pose.position.x;
|
||||
|
||||
vec3_t forward, right, up;
|
||||
AngleVectors(cl.refdef.viewangles, forward, right, up);
|
||||
|
||||
// the left eye: the middle of the head, moved by the eye's offset in the head's frame
|
||||
const float position[3] = {v0->pose.position.x, v0->pose.position.y, v0->pose.position.z};
|
||||
float offset[3];
|
||||
VrEye_ToEngineOffset(position, vr_worldscale->value, offset);
|
||||
vec3_t eye;
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
eye[i] = cl.refdef.vieworg[i] + forward[i] * offset[0] - right[i] * offset[1] + up[i] * offset[2];
|
||||
}
|
||||
|
||||
fprintf(file, "probe_setup units_per_m %.2f ipd_m %.5f focal_px %.2f axis_px %.2f %.2f eye0_world %.2f %.2f %.2f view_angles %.2f %.2f %.2f\n",
|
||||
vr_worldscale->value, ipd, fx, axisX, axisY, eye[0], eye[1], eye[2],
|
||||
cl.refdef.viewangles[0], cl.refdef.viewangles[1], cl.refdef.viewangles[2]);
|
||||
|
||||
for (float fv = 0.20f; fv < 0.97f; fv += 0.08f)
|
||||
{
|
||||
for (float fu = 0.15f; fu < 0.86f; fu += 0.1f)
|
||||
{
|
||||
const float u = fu * width;
|
||||
const float v = fv * height;
|
||||
const float left = -(u - axisX) / fx; // to the left of the view axis, per unit forward
|
||||
const float upward = (axisY - v) / fy;
|
||||
|
||||
vec3_t dir, end;
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
dir[i] = forward[i] - right[i] * left + up[i] * upward;
|
||||
end[i] = eye[i] + dir[i] * 8192.0f;
|
||||
}
|
||||
|
||||
pmtrace_t trace = CL_TraceLine(eye, end, PM_WORLD_ONLY);
|
||||
if (trace.fraction >= 1.0f || trace.allsolid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
vec3_t hit;
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
hit[i] = eye[i] + (end[i] - eye[i]) * trace.fraction;
|
||||
}
|
||||
const float depthUnits = (hit[0] - eye[0]) * forward[0] + (hit[1] - eye[1]) * forward[1] + (hit[2] - eye[2]) * forward[2];
|
||||
const float depthM = depthUnits / vr_worldscale->value;
|
||||
if (depthM < 0.25f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
fprintf(file, "probe %.0f %.0f depth_m %.3f disparity_px %.2f\n", u, v, depthM, ipd * fx / depthM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void dumpState(void)
|
||||
{
|
||||
char path[256];
|
||||
snprintf(path, sizeof(path), DUMP_DIR "/vr-state.txt");
|
||||
FILE* file = fopen(path, "w");
|
||||
if (file == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
fprintf(file, "xr_frame %ld\nrendered_frame %ld\nshould_render %d\nforced %d\n", frames, renderedFrames,
|
||||
(int)gAppState.FrameState.shouldRender, VrDiag_ForceRender() ? 1 : 0);
|
||||
fprintf(file, "eye_size %dx%d\n", gAppState.Renderer.FrameBuffer[0].Width, gAppState.Renderer.FrameBuffer[0].Height);
|
||||
fprintf(file, "head_position %.3f %.3f %.3f\n", hmdPosition[0], hmdPosition[1], hmdPosition[2]);
|
||||
fprintf(file, "head_angles pitch %.2f yaw %.2f roll %.2f\n", hmdorientation[PITCH], hmdorientation[YAW], hmdorientation[ROLL]);
|
||||
fprintf(file, "player_yaw %.2f\nsnap_turn %.2f\ndiag_yaw %.2f\ndiag_roll %.2f\nview_roll %.2f\nvr_fov_x %d\n", playerYaw, snapTurn,
|
||||
config.yaw, config.roll, cl.refdef.viewangles[ROLL], vrFOV);
|
||||
if (gAppState.Projections != NULL)
|
||||
{
|
||||
for (int eye = 0; eye < ovrMaxNumEyes; eye++)
|
||||
{
|
||||
const XrFovf* fov = &gAppState.Projections[eye].fov;
|
||||
fprintf(file, "fov_eye%d L %.3f R %.3f U %.3f D %.3f\n", eye, fov->angleLeft, fov->angleRight, fov->angleUp, fov->angleDown);
|
||||
const XrPosef* pose = &gAppState.Projections[eye].pose;
|
||||
fprintf(file, "eye%d_in_head_mm %.2f %.2f %.2f quat %.5f %.5f %.5f %.5f\n", eye, pose->position.x * 1000.0f,
|
||||
pose->position.y * 1000.0f, pose->position.z * 1000.0f, pose->orientation.x, pose->orientation.y,
|
||||
pose->orientation.z, pose->orientation.w);
|
||||
}
|
||||
}
|
||||
dumpDepthProbes(file);
|
||||
{
|
||||
float pu = 0.0f, pv = 0.0f;
|
||||
const bool aimed = TBXR_ScreenPointer(&pu, &pv);
|
||||
fprintf(file, "pointer %s u %.4f v %.4f (the cursor at %.0f, %.0f in the screen image)\n", aimed ? "aimed" : "none", pu, pv,
|
||||
pu * gAppState.Renderer.FrameBuffer[0].Width, pv * gAppState.Renderer.FrameBuffer[0].Height);
|
||||
}
|
||||
TBXR_DescribePointerLayers(file);
|
||||
fprintf(file, "screen_layer %d\nconnection %s\nkey_dest %d\nmaxclients %d\n", VR_UseScreenLayer() ? 1 : 0,
|
||||
connectionName(), (int)cls.key_dest, (int)cl.maxclients);
|
||||
fclose(file);
|
||||
}
|
||||
|
||||
// The driver's GL for the readback: the plain gl* names in this library are gl4es's,
|
||||
// which doesn't know the swapchain's framebuffers
|
||||
typedef void (GL_APIENTRYP PFN_BindFramebuffer)(GLenum target, GLuint framebuffer);
|
||||
typedef void (GL_APIENTRYP PFN_ReadPixels)(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
typedef GLenum (GL_APIENTRYP PFN_GetError)(void);
|
||||
typedef void (GL_APIENTRYP PFN_Finish)(void);
|
||||
typedef void (GL_APIENTRYP PFN_GetIntegerv)(GLenum pname, GLint* data);
|
||||
typedef GLboolean (GL_APIENTRYP PFN_IsEnabled)(GLenum cap);
|
||||
typedef void (GL_APIENTRYP PFN_ClearColor)(GLfloat r, GLfloat g, GLfloat b, GLfloat a);
|
||||
typedef void (GL_APIENTRYP PFN_Clear)(GLbitfield mask);
|
||||
typedef void (GL_APIENTRYP PFN_Disable)(GLenum cap);
|
||||
|
||||
// One synchronous readback on the chosen frame, it's a test dump
|
||||
void VrDiag_AfterEye(int eye)
|
||||
{
|
||||
if (!enabled || config.dumpFrame <= 0 || dumpDone || renderedFrames < config.dumpFrame || eye < 0 || eye > 1 || dumpEyeDone[eye])
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ovrFramebuffer* frameBuffer = &gAppState.Renderer.FrameBuffer[eye];
|
||||
const int width = frameBuffer->Width;
|
||||
const int height = frameBuffer->Height;
|
||||
unsigned char* pixels = (unsigned char*)malloc((size_t)width * (size_t)height * 4);
|
||||
if (pixels == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mkdir(DUMP_DIR, 0777);
|
||||
|
||||
PFN_BindFramebuffer driverBind = (PFN_BindFramebuffer)eglGetProcAddress("glBindFramebuffer");
|
||||
PFN_ReadPixels driverRead = (PFN_ReadPixels)eglGetProcAddress("glReadPixels");
|
||||
PFN_GetError driverError = (PFN_GetError)eglGetProcAddress("glGetError");
|
||||
PFN_Finish driverFinish = (PFN_Finish)eglGetProcAddress("glFinish");
|
||||
if (driverBind == NULL || driverRead == NULL || driverError == NULL || driverFinish == NULL)
|
||||
{
|
||||
free(pixels);
|
||||
dumpDone = true;
|
||||
ALOGE("[vr-dump] the driver's GL functions weren't found");
|
||||
return;
|
||||
}
|
||||
|
||||
glFinish(); // gl4es flushes what it holds back, and finishes
|
||||
const GLenum drawError = driverError(); // whatever the game left behind
|
||||
|
||||
// what the driver thinks the state is after the game drew this eye
|
||||
{
|
||||
PFN_GetIntegerv getInt = (PFN_GetIntegerv)eglGetProcAddress("glGetIntegerv");
|
||||
PFN_IsEnabled isEnabled = (PFN_IsEnabled)eglGetProcAddress("glIsEnabled");
|
||||
if (getInt != NULL && isEnabled != NULL)
|
||||
{
|
||||
GLint drawFb = -1, readFb = -1, viewport[4] = {0}, scissor[4] = {0};
|
||||
getInt(0x8CA6 /*GL_DRAW_FRAMEBUFFER_BINDING*/, &drawFb);
|
||||
getInt(0x8CAA /*GL_READ_FRAMEBUFFER_BINDING*/, &readFb);
|
||||
getInt(GL_VIEWPORT, viewport);
|
||||
getInt(GL_SCISSOR_BOX, scissor);
|
||||
ALOGI("[vr-dump] eye %d state: draw fb %d, read fb %d, eye fb %u, viewport %d %d %d %d, scissor %s %d %d %d %d, gl error 0x%x",
|
||||
eye, drawFb, readFb, frameBuffer->FrameBuffers[frameBuffer->TextureSwapChainIndex],
|
||||
viewport[0], viewport[1], viewport[2], viewport[3], isEnabled(GL_SCISSOR_TEST) ? "on" : "off",
|
||||
scissor[0], scissor[1], scissor[2], scissor[3], (unsigned int)drawError);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.probe)
|
||||
{
|
||||
// not the game's picture: a flat colour, to see if the readback works at all
|
||||
PFN_ClearColor driverClearColor = (PFN_ClearColor)eglGetProcAddress("glClearColor");
|
||||
PFN_Clear driverClear = (PFN_Clear)eglGetProcAddress("glClear");
|
||||
PFN_Disable driverDisable = (PFN_Disable)eglGetProcAddress("glDisable");
|
||||
if (driverClearColor != NULL && driverClear != NULL && driverDisable != NULL)
|
||||
{
|
||||
driverBind(GL_FRAMEBUFFER, frameBuffer->FrameBuffers[frameBuffer->TextureSwapChainIndex]);
|
||||
driverDisable(GL_SCISSOR_TEST);
|
||||
driverClearColor(eye == 0 ? 1.0f : 0.0f, 0.0f, eye == 0 ? 0.0f : 1.0f, 1.0f);
|
||||
driverClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
driverError(); // clear
|
||||
driverBind(GL_READ_FRAMEBUFFER, frameBuffer->FrameBuffers[frameBuffer->TextureSwapChainIndex]);
|
||||
driverRead(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||
const GLenum error = driverError();
|
||||
driverBind(GL_READ_FRAMEBUFFER, 0);
|
||||
driverFinish();
|
||||
|
||||
// how much of the picture isn't black, for the log
|
||||
long lit = 0;
|
||||
for (long px = 0; px < (long)width * height; px++)
|
||||
{
|
||||
if (pixels[px * 4] | pixels[px * 4 + 1] | pixels[px * 4 + 2])
|
||||
{
|
||||
lit++;
|
||||
}
|
||||
}
|
||||
|
||||
char path[256];
|
||||
snprintf(path, sizeof(path), DUMP_DIR "/vr-eye%d.bmp", eye);
|
||||
const bool ok = VrDiagCore_WriteBmp(path, pixels, width, height);
|
||||
free(pixels);
|
||||
|
||||
ALOGI("[vr-dump] eye %d at rendered frame %ld: %dx%d, %s, gl error 0x%x, %.1f%% not black, game %s", eye, renderedFrames,
|
||||
width, height, ok ? path : "write failed", (unsigned int)error, 100.0 * lit / ((double)width * height), connectionName());
|
||||
|
||||
dumpEyeDone[eye] = true;
|
||||
if (dumpEyeDone[0] && dumpEyeDone[1])
|
||||
{
|
||||
dumpState();
|
||||
dumpDone = true;
|
||||
ALOGI("[vr-dump] state written to " DUMP_DIR "/vr-state.txt");
|
||||
}
|
||||
}
|
||||
|
||||
void VrDiag_FrameBegin(void)
|
||||
{
|
||||
if (!enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const double now = TBXR_GetTimeInMilliSeconds();
|
||||
if (threadStart == 0.0)
|
||||
threadStart = threadCpuMs();
|
||||
frames++;
|
||||
if (TBXR_ShouldRender())
|
||||
{
|
||||
renderedFrames++;
|
||||
}
|
||||
|
||||
// perf bookkeeping
|
||||
if (windowStart == 0.0)
|
||||
{
|
||||
windowStart = now;
|
||||
}
|
||||
else if (lastBegin != 0.0)
|
||||
{
|
||||
const double period = gAppState.FrameState.predictedDisplayPeriod / 1e6; // ns to ms
|
||||
if (period > 0.0 && (now - lastBegin) > period * 1.5)
|
||||
{
|
||||
windowMissed++;
|
||||
}
|
||||
}
|
||||
lastBegin = now;
|
||||
windowFrames++;
|
||||
|
||||
if (config.perf && (now - windowStart) >= 10000.0)
|
||||
{
|
||||
const double seconds = (now - windowStart) / 1000.0;
|
||||
// the thread's own CPU time over the window, against the wall time: what's left is waiting
|
||||
const double threadMs = threadCpuMs();
|
||||
const double cpuPerFrame = windowFrames ? (threadMs - threadStart) / windowFrames : 0.0;
|
||||
ALOGI("[vr-perf] fps=%.1f missed=%ld cpu_ms=%.2f thread_cpu_ms=%.2f wait_ms=%.2f acquire_ms=%.2f period_ms=%.2f frames=%ld rendered=%ld size=%d game=%s",
|
||||
windowFrames / seconds, windowMissed, windowFrames ? cpuSum / windowFrames : 0.0, cpuPerFrame,
|
||||
windowFrames ? waitSum / windowFrames : 0.0, windowFrames ? acquireSum / windowFrames : 0.0,
|
||||
gAppState.FrameState.predictedDisplayPeriod / 1e6, frames, renderedFrames,
|
||||
gAppState.Renderer.FrameBuffer[0].Width, connectionName());
|
||||
threadStart = threadMs;
|
||||
waitSum = acquireSum = 0.0;
|
||||
windowStart = now;
|
||||
windowFrames = 0;
|
||||
windowMissed = 0;
|
||||
cpuSum = 0.0;
|
||||
}
|
||||
|
||||
// load a map without the menus, once the engine has been running a moment
|
||||
if (config.autostart && !mapStarted && renderedFrames >= 150)
|
||||
{
|
||||
char command[96];
|
||||
if (config.load[0])
|
||||
snprintf(command, sizeof(command), "load %s\n", config.load);
|
||||
else
|
||||
snprintf(command, sizeof(command), "map %s\n", config.map[0] ? config.map : "c1a0");
|
||||
Cbuf_AddText(command);
|
||||
mapStarted = true;
|
||||
mapStartFrame = renderedFrames;
|
||||
ALOGI("[vr-diag] autostart: %s", command);
|
||||
}
|
||||
|
||||
// console commands once the map is up (cheats, a cvar, a give)
|
||||
if (config.cmd[0] && !cmdDone && mapStarted && renderedFrames >= mapStartFrame + 300)
|
||||
{
|
||||
char command[sizeof(config.cmd) + 2];
|
||||
snprintf(command, sizeof(command), "%s\n", config.cmd);
|
||||
Cbuf_AddText(command);
|
||||
cmdDone = true;
|
||||
ALOGI("[vr-diag] cmd: %s", config.cmd);
|
||||
}
|
||||
|
||||
static double firstFrameMs = 0.0;
|
||||
if (firstFrameMs == 0.0)
|
||||
firstFrameMs = now;
|
||||
if (config.exitSeconds > 0 && (now - firstFrameMs) >= config.exitSeconds * 1000.0)
|
||||
{
|
||||
ALOGI("[vr-diag] exit_seconds %d reached, shutting down", config.exitSeconds);
|
||||
config.exitSeconds = 0;
|
||||
TBXR_exit(0);
|
||||
}
|
||||
|
||||
if (config.exitAfter > 0 && frames >= config.exitAfter)
|
||||
{
|
||||
ALOGI("[vr-diag] exit_after %d reached, shutting down", config.exitAfter);
|
||||
config.exitAfter = 0;
|
||||
TBXR_exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
void VrDiag_FrameEnd(void)
|
||||
{
|
||||
if (enabled && lastBegin != 0.0)
|
||||
{
|
||||
cpuSum += TBXR_GetTimeInMilliSeconds() - lastBegin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Unattended test switches for the Steam Frame build: the headset on a desk, no
|
||||
// one wearing it. They come from vr-diag.ini in the data folder and do nothing
|
||||
// when that file isn't there. See docs/VR_ARCHITECTURE.md.
|
||||
#ifndef VRDIAG_H
|
||||
#define VRDIAG_H
|
||||
|
||||
#include "TBXR_Common.h"
|
||||
#include "VrInputFrame.h"
|
||||
|
||||
void VrDiag_Init(void); // once, reads vr-diag.ini
|
||||
bool VrDiag_ForceRender(void); // draw and run while shouldRender is false
|
||||
void VrDiag_FrameBegin(void); // each XR frame, after xrWaitFrame and the input
|
||||
void VrDiag_FrameEnd(void); // each XR frame, after xrEndFrame
|
||||
void VrDiag_AfterEye(int eye); // the eye is drawn, still bound, before it's released
|
||||
|
||||
// Stand-ins for what the runtime can't give an unworn headset. They return false
|
||||
// (and touch nothing) unless force_render is on.
|
||||
bool VrDiag_SyntheticHead(XrPosef* pose);
|
||||
bool VrDiag_SyntheticViews(XrView* views);
|
||||
bool VrDiag_SyntheticHand(int hand, ovrTrackedController* controller);
|
||||
|
||||
// diag_press: holds buttons down for a while, so the controller map can run with nobody holding
|
||||
// controllers. Returns true when the buttons were changed; Injecting says there are some to come.
|
||||
bool VrDiag_InjectInputs(frameRaw_t* raw);
|
||||
bool VrDiag_Injecting(void);
|
||||
|
||||
// time spent in the blocking XR calls, for the [vr-perf] split between work and waiting
|
||||
enum { VRDIAG_TIME_WAIT_FRAME, VRDIAG_TIME_ACQUIRE };
|
||||
void VrDiag_NoteTiming(int what, double ms);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,267 @@
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "VrDiagCore.h"
|
||||
#include "VrFrameMap.h"
|
||||
|
||||
void VrDiagCore_Defaults(vrDiagConfig_t* config)
|
||||
{
|
||||
memset(config, 0, sizeof(*config));
|
||||
config->perf = 1;
|
||||
config->gap = 0.5f;
|
||||
}
|
||||
|
||||
static const struct { const char* name; int input; } pressNames[] = {
|
||||
{ "LT", FI_L_TRIGGER }, { "RT", FI_R_TRIGGER }, { "LG", FI_L_GRIP }, { "RG", FI_R_GRIP },
|
||||
{ "LB", FI_L_BUMPER }, { "RB", FI_R_BUMPER }, { "LC", FI_L_STICK_CLICK }, { "RC", FI_R_STICK_CLICK },
|
||||
{ "A", FI_A }, { "B", FI_B }, { "X", FI_X }, { "Y", FI_Y },
|
||||
{ "MENU", FI_MENU_SHORT }, { "MENUHOLD", FI_MENU_HOLD }, { "VIEW", FI_VIEW_SHORT },
|
||||
{ "UP", FI_DPAD_UP }, { "DOWN", FI_DPAD_DOWN }, { "LEFT", FI_DPAD_LEFT }, { "RIGHT", FI_DPAD_RIGHT },
|
||||
{ "LSUP", FI_L_STICK_UP }, { "LSDOWN", FI_L_STICK_DOWN }, { "LSLEFT", FI_L_STICK_LEFT }, { "LSRIGHT", FI_L_STICK_RIGHT },
|
||||
{ "RSUP", FI_R_STICK_UP }, { "RSDOWN", FI_R_STICK_DOWN }, { "RSLEFT", FI_R_STICK_LEFT }, { "RSRIGHT", FI_R_STICK_RIGHT },
|
||||
};
|
||||
|
||||
// "A@300+30, LG@400+20": a button, the frame it goes down at (after the map has started) and for how many
|
||||
static void parsePresses(const char* value, vrDiagConfig_t* config)
|
||||
{
|
||||
char copy[256];
|
||||
snprintf(copy, sizeof(copy), "%s", value);
|
||||
config->pressCount = 0;
|
||||
char* save = NULL;
|
||||
for (char* item = strtok_r(copy, ", ", &save); item != NULL && config->pressCount < VRDIAG_MAX_PRESSES; item = strtok_r(NULL, ", ", &save))
|
||||
{
|
||||
char* at = strchr(item, '@');
|
||||
char* plus = at ? strchr(at, '+') : NULL;
|
||||
if (at == NULL || plus == NULL)
|
||||
continue;
|
||||
*at = '\0';
|
||||
*plus = '\0';
|
||||
int input = -1;
|
||||
for (size_t i = 0; i < sizeof(pressNames) / sizeof(pressNames[0]); i++)
|
||||
if (strcasecmp(item, pressNames[i].name) == 0)
|
||||
input = pressNames[i].input;
|
||||
const int start = atoi(at + 1);
|
||||
const int length = atoi(plus + 1);
|
||||
if (input < 0 || start < 0 || length <= 0)
|
||||
continue;
|
||||
vrDiagPress_t* press = &config->presses[config->pressCount++];
|
||||
press->input = input;
|
||||
press->start = start;
|
||||
press->length = length;
|
||||
}
|
||||
}
|
||||
|
||||
static char* trim(char* text)
|
||||
{
|
||||
while (isspace((unsigned char)*text))
|
||||
{
|
||||
text++;
|
||||
}
|
||||
char* end = text + strlen(text);
|
||||
while (end > text && isspace((unsigned char)end[-1]))
|
||||
{
|
||||
*--end = '\0';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
static bool parseInt(const char* value, int* out)
|
||||
{
|
||||
if (strcasecmp(value, "true") == 0 || strcasecmp(value, "on") == 0 || strcasecmp(value, "yes") == 0)
|
||||
{
|
||||
*out = 1;
|
||||
return true;
|
||||
}
|
||||
if (strcasecmp(value, "false") == 0 || strcasecmp(value, "off") == 0 || strcasecmp(value, "no") == 0)
|
||||
{
|
||||
*out = 0;
|
||||
return true;
|
||||
}
|
||||
char* end = NULL;
|
||||
long number = strtol(value, &end, 10);
|
||||
if (end == value || *end != '\0')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
*out = (int)number;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parseFloat(const char* value, float* out)
|
||||
{
|
||||
char* end = NULL;
|
||||
float number = strtof(value, &end);
|
||||
if (end == value || *end != '\0')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
*out = number;
|
||||
return true;
|
||||
}
|
||||
|
||||
int VrDiagCore_Parse(const char* text, vrDiagConfig_t* config)
|
||||
{
|
||||
int found = 0;
|
||||
char* copy = strdup(text);
|
||||
if (copy == NULL)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (char* line = strtok(copy, "\r\n"); line != NULL; line = strtok(NULL, "\r\n"))
|
||||
{
|
||||
line = trim(line);
|
||||
if (*line == '\0' || *line == '#' || *line == ';')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
char* equals = strchr(line, '=');
|
||||
if (equals == NULL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
*equals = '\0';
|
||||
const char* key = trim(line);
|
||||
const char* value = trim(equals + 1);
|
||||
|
||||
int number = 0;
|
||||
float real = 0.0f;
|
||||
if (strcasecmp(key, "force_render") == 0 && parseInt(value, &number))
|
||||
{
|
||||
config->forceRender = number != 0;
|
||||
}
|
||||
else if (strcasecmp(key, "diag_yaw") == 0 && parseFloat(value, &real))
|
||||
{
|
||||
config->yaw = real;
|
||||
}
|
||||
else if (strcasecmp(key, "diag_roll") == 0 && parseFloat(value, &real))
|
||||
{
|
||||
config->roll = real;
|
||||
}
|
||||
else if (strcasecmp(key, "diag_hand_yaw") == 0 && parseFloat(value, &real))
|
||||
{
|
||||
config->handYaw = real;
|
||||
}
|
||||
else if (strcasecmp(key, "diag_hand_pitch") == 0 && parseFloat(value, &real))
|
||||
{
|
||||
config->handPitch = real;
|
||||
}
|
||||
else if (strcasecmp(key, "diag_gap") == 0 && parseFloat(value, &real))
|
||||
{
|
||||
config->gap = real > 0.0f ? real : 0.5f;
|
||||
}
|
||||
else if (strcasecmp(key, "diag_press") == 0 && *value != '\0')
|
||||
{
|
||||
parsePresses(value, config);
|
||||
}
|
||||
else if (strcasecmp(key, "autostart") == 0 && parseInt(value, &number))
|
||||
{
|
||||
config->autostart = number != 0;
|
||||
}
|
||||
else if (strcasecmp(key, "load") == 0 && *value != '\0')
|
||||
{
|
||||
snprintf(config->load, sizeof(config->load), "%s", value);
|
||||
config->autostart = 1;
|
||||
}
|
||||
else if (strcasecmp(key, "exit_seconds") == 0 && parseInt(value, &number))
|
||||
{
|
||||
config->exitSeconds = number > 0 ? number : 0;
|
||||
}
|
||||
else if (strcasecmp(key, "map") == 0 && *value != '\0')
|
||||
{
|
||||
snprintf(config->map, sizeof(config->map), "%s", value);
|
||||
config->autostart = 1;
|
||||
}
|
||||
else if (strcasecmp(key, "cmd") == 0 && *value != '\0')
|
||||
{
|
||||
snprintf(config->cmd, sizeof(config->cmd), "%s", value);
|
||||
}
|
||||
else if (strcasecmp(key, "dump_frame") == 0 && parseInt(value, &number))
|
||||
{
|
||||
config->dumpFrame = number > 0 ? number : 0;
|
||||
}
|
||||
else if (strcasecmp(key, "exit_after") == 0 && parseInt(value, &number))
|
||||
{
|
||||
config->exitAfter = number > 0 ? number : 0;
|
||||
}
|
||||
else if (strcasecmp(key, "probe") == 0 && parseInt(value, &number))
|
||||
{
|
||||
config->probe = number != 0;
|
||||
}
|
||||
else if (strcasecmp(key, "perf") == 0 && parseInt(value, &number))
|
||||
{
|
||||
config->perf = number != 0;
|
||||
}
|
||||
else if (strcasecmp(key, "oneshot") == 0 && parseInt(value, &number))
|
||||
{
|
||||
config->oneshot = number != 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
found++;
|
||||
}
|
||||
|
||||
free(copy);
|
||||
config->keysSeen = found;
|
||||
return found;
|
||||
}
|
||||
|
||||
static void put16(unsigned char* p, unsigned int v) { p[0] = v & 255; p[1] = (v >> 8) & 255; }
|
||||
static void put32(unsigned char* p, unsigned int v) { put16(p, v & 0xffff); put16(p + 2, v >> 16); }
|
||||
|
||||
bool VrDiagCore_WriteBmp(const char* path, const unsigned char* rgba, int width, int height)
|
||||
{
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int stride = (width * 3 + 3) & ~3;
|
||||
const unsigned int imageSize = (unsigned int)stride * (unsigned int)height;
|
||||
|
||||
unsigned char header[54];
|
||||
memset(header, 0, sizeof(header));
|
||||
header[0] = 'B';
|
||||
header[1] = 'M';
|
||||
put32(header + 2, 54 + imageSize);
|
||||
put32(header + 10, 54);
|
||||
put32(header + 14, 40);
|
||||
put32(header + 18, (unsigned int)width);
|
||||
put32(header + 22, (unsigned int)height); // positive: rows are bottom to top, as given
|
||||
put16(header + 26, 1);
|
||||
put16(header + 28, 24);
|
||||
put32(header + 34, imageSize);
|
||||
|
||||
FILE* file = fopen(path, "wb");
|
||||
if (file == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = fwrite(header, 1, sizeof(header), file) == sizeof(header);
|
||||
unsigned char* row = (unsigned char*)calloc(1, (size_t)stride);
|
||||
if (row == NULL)
|
||||
{
|
||||
fclose(file);
|
||||
return false;
|
||||
}
|
||||
for (int y = 0; y < height && ok; y++)
|
||||
{
|
||||
const unsigned char* src = rgba + (size_t)y * (size_t)width * 4;
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
row[x * 3 + 0] = src[x * 4 + 2];
|
||||
row[x * 3 + 1] = src[x * 4 + 1];
|
||||
row[x * 3 + 2] = src[x * 4 + 0];
|
||||
}
|
||||
ok = fwrite(row, 1, (size_t)stride, file) == (size_t)stride;
|
||||
}
|
||||
free(row);
|
||||
return fclose(file) == 0 && ok;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Unattended test switches for the Steam Frame build, the parts that need no
|
||||
// OpenXR or GL so they can be tested on the host (tools/steam_frame/tests).
|
||||
#ifndef VRDIAGCORE_H
|
||||
#define VRDIAGCORE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#define VRDIAG_MAX_PRESSES 24
|
||||
|
||||
typedef struct {
|
||||
int input; // FI_* of VrFrameMap.h
|
||||
int start; // frames after the map has started
|
||||
int length; // frames held
|
||||
} vrDiagPress_t;
|
||||
|
||||
typedef struct {
|
||||
int forceRender; // draw and run the game while the runtime says shouldRender=false
|
||||
float yaw; // synthetic head yaw in degrees (turning left is positive)
|
||||
float handYaw, handPitch; // the synthetic hands' aim, turned from the head's (degrees)
|
||||
float gap; // metres between the synthetic hands (they're parked either side of the chest)
|
||||
vrDiagPress_t presses[VRDIAG_MAX_PRESSES]; // buttons to hold down for a while
|
||||
int pressCount;
|
||||
float roll; // synthetic head roll in degrees (tilting the head to the left is positive)
|
||||
int autostart; // load a map without touching the menus
|
||||
char load[64]; // a save to load instead of starting a map (a load key implies autostart)
|
||||
int exitSeconds; // leave after this many seconds of rendering, 0 = off
|
||||
char map[64]; // which one (a map set here starts it as well)
|
||||
char cmd[128]; // console commands (; between them) to run once, about 4 s after the map starts
|
||||
int dumpFrame; // dump both eyes on this rendered frame, 0 = off
|
||||
int exitAfter; // leave through the normal shutdown after this many frames, 0 = off
|
||||
int perf; // the [vr-perf] line every 10 s
|
||||
int probe; // clear each eye to a colour (red, blue) right before the dump, to test the readback
|
||||
int oneshot; // rename the file once it has been read
|
||||
int keysSeen; // how many known keys were in the file
|
||||
} vrDiagConfig_t;
|
||||
|
||||
void VrDiagCore_Defaults(vrDiagConfig_t* config);
|
||||
|
||||
// "key = value" lines, # or ; for comments, keys in any case. Unknown keys and
|
||||
// values that don't parse are ignored. Returns how many known keys it found.
|
||||
int VrDiagCore_Parse(const char* text, vrDiagConfig_t* config);
|
||||
|
||||
// A 24-bit BMP from RGBA rows as glReadPixels gives them (bottom row first).
|
||||
bool VrDiagCore_WriteBmp(const char* path, const unsigned char* rgba, int width, int height);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,101 @@
|
||||
#include <math.h>
|
||||
|
||||
#include "VrEyeMath.h"
|
||||
|
||||
void VrEye_ToEngineOffset(const float position[3], float worldScale, float out[3])
|
||||
{
|
||||
out[0] = -position[2] * worldScale; // forward
|
||||
out[1] = -position[0] * worldScale; // left
|
||||
out[2] = position[1] * worldScale; // up
|
||||
}
|
||||
|
||||
bool VrEye_CantAxisAngle(const float quat[4], float axis[3], float* degrees)
|
||||
{
|
||||
float x = quat[0], y = quat[1], z = quat[2], w = quat[3];
|
||||
const float length = sqrtf(x * x + y * y + z * z + w * w);
|
||||
if (length < 1e-6f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
x /= length; y /= length; z /= length; w /= length;
|
||||
|
||||
// the same rotation by the shorter way round
|
||||
if (w < 0.0f)
|
||||
{
|
||||
x = -x; y = -y; z = -z; w = -w;
|
||||
}
|
||||
|
||||
const float s = sqrtf(x * x + y * y + z * z);
|
||||
const float angle = 2.0f * atan2f(s, w);
|
||||
if (angle < 1e-4f || s < 1e-6f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// the axis changes basis like a vector does (the axes differ by a proper rotation)
|
||||
axis[0] = -z / s;
|
||||
axis[1] = -x / s;
|
||||
axis[2] = y / s;
|
||||
*degrees = angle * (180.0f / (float)M_PI);
|
||||
return true;
|
||||
}
|
||||
|
||||
float VrEye_AxisPixel(float angleLeft, float angleRight, int widthPx)
|
||||
{
|
||||
const float tanL = tanf(angleLeft);
|
||||
const float tanR = tanf(angleRight);
|
||||
if (tanR - tanL <= 0.0f)
|
||||
{
|
||||
return widthPx * 0.5f;
|
||||
}
|
||||
return -tanL / (tanR - tanL) * (float)widthPx;
|
||||
}
|
||||
|
||||
float VrEye_FocalPixels(float angleLeft, float angleRight, int widthPx)
|
||||
{
|
||||
const float span = tanf(angleRight) - tanf(angleLeft);
|
||||
return span > 0.0f ? (float)widthPx / span : 0.0f;
|
||||
}
|
||||
|
||||
float VrEye_HudShiftPixels(float eyeX, float angleLeft, float angleRight, int widthPx, float distanceM)
|
||||
{
|
||||
if (distanceM <= 0.0f)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
// a point straight ahead of the head lies at -eyeX from this eye's own axis
|
||||
return -eyeX / distanceM * VrEye_FocalPixels(angleLeft, angleRight, widthPx);
|
||||
}
|
||||
|
||||
float VrEye_ProjectX(float eyeX, float pointX, float pointZ, float angleLeft, float angleRight, int widthPx)
|
||||
{
|
||||
const float tanL = tanf(angleLeft);
|
||||
const float tanR = tanf(angleRight);
|
||||
const float slope = (pointX - eyeX) / -pointZ;
|
||||
return (slope - tanL) / (tanR - tanL) * (float)widthPx;
|
||||
}
|
||||
|
||||
void VrEye_ComposePose(const float a[7], const float b[7], float out[7])
|
||||
{
|
||||
const float ax = a[3], ay = a[4], az = a[5], aw = a[6];
|
||||
const float bx = b[3], by = b[4], bz = b[5], bw = b[6];
|
||||
|
||||
// position: a's position plus b's rotated by a's orientation
|
||||
// v' = v + 2w(q x v) + 2(q x (q x v))
|
||||
const float vx = b[0], vy = b[1], vz = b[2];
|
||||
const float cx = ay * vz - az * vy;
|
||||
const float cy = az * vx - ax * vz;
|
||||
const float cz = ax * vy - ay * vx;
|
||||
const float dx = ay * cz - az * cy;
|
||||
const float dy = az * cx - ax * cz;
|
||||
const float dz = ax * cy - ay * cx;
|
||||
out[0] = a[0] + vx + 2.0f * (aw * cx + dx);
|
||||
out[1] = a[1] + vy + 2.0f * (aw * cy + dy);
|
||||
out[2] = a[2] + vz + 2.0f * (aw * cz + dz);
|
||||
|
||||
// orientation: a * b
|
||||
out[3] = aw * bx + ax * bw + ay * bz - az * by;
|
||||
out[4] = aw * by - ax * bz + ay * bw + az * bx;
|
||||
out[5] = aw * bz + ax * by - ay * bx + az * bw;
|
||||
out[6] = aw * bw - ax * bx - ay * by - az * bz;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Steam Frame: where each eye is, from what the runtime says, and the 2D shift that puts
|
||||
// the HUD at a distance. Plain C with no OpenXR or engine types, so the host tests can use it.
|
||||
// OpenXR axes: x right, y up, -z forward. Engine (Quake) axes: x forward, y left, z up.
|
||||
#ifndef VREYEMATH_H
|
||||
#define VREYEMATH_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
// An eye's position in head space (OpenXR axes, metres) as an offset in the engine's
|
||||
// head-aligned axes, scaled to world units.
|
||||
void VrEye_ToEngineOffset(const float position[3], float worldScale, float out[3]);
|
||||
|
||||
// The eye's rotation relative to the head (OpenXR quaternion x y z w) as an axis in the
|
||||
// engine's axes and an angle in degrees. Returns false when there's no rotation to speak of.
|
||||
bool VrEye_CantAxisAngle(const float quat[4], float axis[3], float* degrees);
|
||||
|
||||
// Pixels from the left of an eye image to where straight ahead lands, for an asymmetric
|
||||
// frustum (angles in radians as OpenXR gives them: angleLeft is negative).
|
||||
float VrEye_AxisPixel(float angleLeft, float angleRight, int widthPx);
|
||||
|
||||
// Focal length in pixels along x.
|
||||
float VrEye_FocalPixels(float angleLeft, float angleRight, int widthPx);
|
||||
|
||||
// How far to shift 2D content in an eye's image (positive: right) so that it sits at
|
||||
// distanceM straight ahead of the head, given that eye's x position in head space.
|
||||
float VrEye_HudShiftPixels(float eyeX, float angleLeft, float angleRight, int widthPx, float distanceM);
|
||||
|
||||
// Pixels from the left of an eye image where a point at (x, z) in head space lands
|
||||
// (z negative is ahead), the eye being at eyeX. For tests and measuring.
|
||||
float VrEye_ProjectX(float eyeX, float pointX, float pointZ, float angleLeft, float angleRight, int widthPx);
|
||||
|
||||
// pose = a pose b: b (an eye in head space) placed by a (the head in the stage). A pose is
|
||||
// position x y z then quaternion x y z w.
|
||||
void VrEye_ComposePose(const float a[7], const float b[7], float out[7]);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,127 @@
|
||||
#include "VrFrameMap.h"
|
||||
|
||||
// The map. One physical input does one thing in a context: the rows of an input must not overlap
|
||||
// (the host test checks that, and that every action can be reached). The same action on two
|
||||
// inputs is an alias.
|
||||
static const frameMapRow_t rows[] = {
|
||||
// ---- playing
|
||||
{ FI_R_TRIGGER, ROLE_TRIGGER_WEAPON, 0, CTX_MENU, ACT_FIRE, "weapon hand trigger" },
|
||||
{ FI_L_TRIGGER, ROLE_TRIGGER_OTHER, 0, CTX_MENU, ACT_ALTFIRE, "other hand trigger" },
|
||||
{ FI_A, 0, 0, CTX_MENU, ACT_JUMP, "" },
|
||||
{ FI_B, 0, 0, CTX_MENU, ACT_CROUCH_HOLD, "" },
|
||||
{ FI_X, 0, 0, CTX_MENU, ACT_USE, "" },
|
||||
{ FI_Y, 0, 0, CTX_MENU, ACT_RELOAD, "" },
|
||||
{ FI_L_STICK_CLICK, 0, 0, CTX_MENU, ACT_CROUCH_TOGGLE, "" },
|
||||
{ FI_R_STICK_CLICK, 0, CTX_OFF_ZONE, CTX_MENU, ACT_QUICKSAVE, "off hand behind the head" },
|
||||
{ FI_R_STICK_CLICK, 0, 0, CTX_MENU | CTX_OFF_ZONE, ACT_FLASHLIGHT, "" },
|
||||
{ FI_VIEW_SHORT, 0, CTX_OFF_ZONE, CTX_MENU, ACT_QUICKLOAD, "off hand behind the head" },
|
||||
{ FI_VIEW_SHORT, 0, CTX_MULTI, CTX_MENU | CTX_OFF_ZONE, ACT_SCOREBOARD, "multiplayer" },
|
||||
{ FI_VIEW_SHORT, 0, 0, CTX_MENU | CTX_OFF_ZONE | CTX_MULTI, ACT_FLASHLIGHT, "" },
|
||||
{ FI_L_STICK_UP, 0, 0, CTX_MENU, ACT_MOVE, "move" },
|
||||
{ FI_L_STICK_DOWN, 0, 0, CTX_MENU, ACT_MOVE, "move" },
|
||||
{ FI_L_STICK_LEFT, 0, 0, CTX_MENU, ACT_MOVE, "move" },
|
||||
{ FI_L_STICK_RIGHT, 0, 0, CTX_MENU, ACT_MOVE, "move" },
|
||||
{ FI_R_STICK_LEFT, 0, 0, CTX_MENU, ACT_TURN, "snap or smooth turn" },
|
||||
{ FI_R_STICK_RIGHT, 0, 0, CTX_MENU, ACT_TURN, "snap or smooth turn" },
|
||||
// (right stick up and down: nothing in the game, they scroll in a menu)
|
||||
{ FI_L_BUMPER, 0, 0, CTX_MENU, ACT_PREV_WEAPON, "" },
|
||||
{ FI_R_BUMPER, 0, 0, CTX_MENU, ACT_NEXT_WEAPON, "" },
|
||||
{ FI_DPAD_LEFT, 0, 0, CTX_MENU, ACT_PREV_WEAPON, "alias of the left bumper" },
|
||||
{ FI_DPAD_RIGHT, 0, 0, CTX_MENU, ACT_NEXT_WEAPON, "alias of the right bumper" },
|
||||
{ FI_DPAD_UP, 0, 0, CTX_MENU, ACT_LAST_WEAPON, "" },
|
||||
{ FI_DPAD_DOWN, 0, 0, CTX_MENU, ACT_LASER, "laser sight, steadies a scope" },
|
||||
{ FI_R_GRIP, ROLE_GRIP_WEAPON, CTX_DOM_ZONE, CTX_MENU, ACT_BACKPACK, "the crowbar, from behind the head" },
|
||||
{ FI_R_GRIP, ROLE_GRIP_WEAPON, 0, CTX_MENU | CTX_DOM_ZONE, ACT_USE, "alias of X: grab, push, pull, press" },
|
||||
{ FI_L_GRIP, ROLE_GRIP_OTHER, 0, CTX_MENU | CTX_OFF_GRIP_AWAY, ACT_STEADY, "two-handed hold, scope: near the weapon" },
|
||||
{ FI_L_GRIP, ROLE_GRIP_OTHER, CTX_OFF_GRIP_AWAY, CTX_MENU, ACT_USE_OFF, "grab with this hand: well away from the weapon" },
|
||||
{ FI_MENU_SHORT, 0, 0, CTX_MENU, ACT_PAUSE, "" },
|
||||
{ FI_MENU_HOLD, 0, 0, CTX_MENU, ACT_QUICKSAVE, "held for half a second" },
|
||||
{ FI_VIEW_HOLD_1S, 0, 0, 0, ACT_RECENTRE, "" },
|
||||
{ FI_VIEW_HOLD_3S, 0, 0, 0, ACT_RECENTRE_HEIGHT, "" },
|
||||
|
||||
// ---- in a menu (the laser points from the weapon hand)
|
||||
{ FI_R_TRIGGER, ROLE_TRIGGER_WEAPON, CTX_MENU, 0, ACT_MENU_CLICK, "pointer click" },
|
||||
{ FI_A, 0, CTX_MENU, 0, ACT_MENU_CONFIRM, "" },
|
||||
{ FI_B, 0, CTX_MENU, 0, ACT_MENU_BACK, "" },
|
||||
{ FI_MENU_SHORT, 0, CTX_MENU, 0, ACT_MENU_BACK, "alias of B" },
|
||||
{ FI_VIEW_SHORT, 0, CTX_MENU, 0, ACT_MENU_BACK, "alias of B" },
|
||||
{ FI_DPAD_UP, 0, CTX_MENU, 0, ACT_MENU_UP, "" },
|
||||
{ FI_DPAD_DOWN, 0, CTX_MENU, 0, ACT_MENU_DOWN, "" },
|
||||
{ FI_DPAD_LEFT, 0, CTX_MENU, 0, ACT_MENU_LEFT, "" },
|
||||
{ FI_DPAD_RIGHT, 0, CTX_MENU, 0, ACT_MENU_RIGHT, "" },
|
||||
{ FI_L_STICK_UP, 0, CTX_MENU, 0, ACT_MENU_UP, "alias of the D-pad" },
|
||||
{ FI_L_STICK_DOWN, 0, CTX_MENU, 0, ACT_MENU_DOWN, "alias of the D-pad" },
|
||||
{ FI_L_STICK_LEFT, 0, CTX_MENU, 0, ACT_MENU_LEFT, "alias of the D-pad" },
|
||||
{ FI_L_STICK_RIGHT, 0, CTX_MENU, 0, ACT_MENU_RIGHT, "alias of the D-pad" },
|
||||
{ FI_L_BUMPER, 0, CTX_MENU, 0, ACT_MENU_PAGE_PREV, "previous tab or page" },
|
||||
{ FI_R_BUMPER, 0, CTX_MENU, 0, ACT_MENU_PAGE_NEXT, "next tab or page" },
|
||||
{ FI_R_STICK_UP, 0, CTX_MENU, 0, ACT_MENU_SCROLL_UP, "scroll" },
|
||||
{ FI_R_STICK_DOWN, 0, CTX_MENU, 0, ACT_MENU_SCROLL_DOWN, "scroll" },
|
||||
};
|
||||
|
||||
const frameMapRow_t* VrFrameMap_Rows(int* count)
|
||||
{
|
||||
*count = (int)(sizeof(rows) / sizeof(rows[0]));
|
||||
return rows;
|
||||
}
|
||||
|
||||
int VrFrameMap_RoleInput(int role, bool leftHanded)
|
||||
{
|
||||
switch (role)
|
||||
{
|
||||
case ROLE_TRIGGER_WEAPON: return leftHanded ? FI_L_TRIGGER : FI_R_TRIGGER;
|
||||
case ROLE_TRIGGER_OTHER: return leftHanded ? FI_R_TRIGGER : FI_L_TRIGGER;
|
||||
case ROLE_GRIP_WEAPON: return leftHanded ? FI_L_GRIP : FI_R_GRIP;
|
||||
case ROLE_GRIP_OTHER: return leftHanded ? FI_R_GRIP : FI_L_GRIP;
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int VrFrameMap_Action(int input, int context, bool leftHanded)
|
||||
{
|
||||
const int count = (int)(sizeof(rows) / sizeof(rows[0]));
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
const frameMapRow_t* row = &rows[i];
|
||||
const int rowInput = row->role ? VrFrameMap_RoleInput(row->role, leftHanded) : row->input;
|
||||
if (rowInput != input)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if ((context & row->require) != row->require || (context & row->forbid) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return row->action;
|
||||
}
|
||||
return ACT_NONE;
|
||||
}
|
||||
|
||||
const char* VrFrameMap_InputName(int input)
|
||||
{
|
||||
static const char* const names[FI_COUNT] = {
|
||||
"left trigger", "right trigger", "left grip", "right grip", "left bumper", "right bumper",
|
||||
"left stick click", "right stick click", "A", "B", "X", "Y",
|
||||
"Menu (short)", "Menu (hold 0.5 s)", "View (short)", "View (hold 1 s)", "View (hold 3 s)",
|
||||
"D-pad up", "D-pad down", "D-pad left", "D-pad right",
|
||||
"left stick up", "left stick down", "left stick left", "left stick right",
|
||||
"right stick up", "right stick down", "right stick left", "right stick right"};
|
||||
return (input >= 0 && input < FI_COUNT) ? names[input] : "?";
|
||||
}
|
||||
|
||||
const char* VrFrameMap_ActionName(int action)
|
||||
{
|
||||
static const char* const names[ACT_COUNT] = {
|
||||
"none",
|
||||
"fire", "alt fire", "use", "use (other hand)", "reload", "jump", "crouch (hold)", "crouch (toggle)",
|
||||
"flashlight", "scoreboard", "laser sight / scope steady",
|
||||
"move", "turn",
|
||||
"previous weapon", "next weapon", "last weapon",
|
||||
"backpack weapon", "hold the gun steady",
|
||||
"pause", "quick save", "quick load",
|
||||
"recentre", "recentre and set height",
|
||||
"menu click", "menu confirm", "menu back",
|
||||
"menu up", "menu down", "menu left", "menu right",
|
||||
"menu previous tab", "menu next tab", "menu scroll up", "menu scroll down"};
|
||||
return (action >= 0 && action < ACT_COUNT) ? names[action] : "?";
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Steam Frame: the controller map as one table, so it is easy to change and can be checked on
|
||||
// the host. It follows Half-Life's own Steam Input gamepad layout (the 25th anniversary update's
|
||||
// xbox_controller_config_standard.vdf: sticks move and turn, RT fires, LT alt fires, A jump,
|
||||
// B crouch, X use, Y reload, bumpers and D-pad pick weapons, Start pauses and, held, quick saves,
|
||||
// Select is the flashlight) with the VR parts added: the grips, and the head for looking. The right
|
||||
// stick's up and down do nothing in the game (it turns left and right), and the flat screen view is a
|
||||
// setting (vr_flat_screen), not a button.
|
||||
//
|
||||
// The Frame's controllers are a split gamepad: the left one has the D-pad, View, a bumper, trigger,
|
||||
// grip and stick, the right one has A B X Y, Menu, a bumper, trigger, grip and stick. Handedness
|
||||
// decides which hand is the weapon hand (so which trigger fires and which grip grabs, and where the
|
||||
// laser comes from); the buttons stay where they physically are.
|
||||
#ifndef VRFRAMEMAP_H
|
||||
#define VRFRAMEMAP_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// Physical inputs. The stick directions are the stick pushed over a threshold, View and Menu
|
||||
// come as a short press (on release), and a hold that fires once while it's held.
|
||||
enum {
|
||||
FI_L_TRIGGER, FI_R_TRIGGER, FI_L_GRIP, FI_R_GRIP, FI_L_BUMPER, FI_R_BUMPER,
|
||||
FI_L_STICK_CLICK, FI_R_STICK_CLICK,
|
||||
FI_A, FI_B, FI_X, FI_Y,
|
||||
FI_MENU_SHORT, FI_MENU_HOLD, FI_VIEW_SHORT, FI_VIEW_HOLD_1S, FI_VIEW_HOLD_3S,
|
||||
FI_DPAD_UP, FI_DPAD_DOWN, FI_DPAD_LEFT, FI_DPAD_RIGHT,
|
||||
FI_L_STICK_UP, FI_L_STICK_DOWN, FI_L_STICK_LEFT, FI_L_STICK_RIGHT,
|
||||
FI_R_STICK_UP, FI_R_STICK_DOWN, FI_R_STICK_LEFT, FI_R_STICK_RIGHT,
|
||||
FI_COUNT
|
||||
};
|
||||
|
||||
// What can happen
|
||||
enum {
|
||||
ACT_NONE,
|
||||
ACT_FIRE, ACT_ALTFIRE, ACT_USE, ACT_USE_OFF, ACT_RELOAD, ACT_JUMP, ACT_CROUCH_HOLD, ACT_CROUCH_TOGGLE,
|
||||
ACT_FLASHLIGHT, ACT_SCOREBOARD, ACT_LASER,
|
||||
ACT_MOVE, ACT_TURN,
|
||||
ACT_PREV_WEAPON, ACT_NEXT_WEAPON, ACT_LAST_WEAPON,
|
||||
ACT_BACKPACK, ACT_STEADY,
|
||||
ACT_PAUSE, ACT_QUICKSAVE, ACT_QUICKLOAD,
|
||||
ACT_RECENTRE, ACT_RECENTRE_HEIGHT,
|
||||
ACT_MENU_CLICK, ACT_MENU_CONFIRM, ACT_MENU_BACK,
|
||||
ACT_MENU_UP, ACT_MENU_DOWN, ACT_MENU_LEFT, ACT_MENU_RIGHT,
|
||||
ACT_MENU_PAGE_PREV, ACT_MENU_PAGE_NEXT, ACT_MENU_SCROLL_UP, ACT_MENU_SCROLL_DOWN,
|
||||
ACT_COUNT
|
||||
};
|
||||
|
||||
// What's going on around the button
|
||||
enum {
|
||||
CTX_MENU = 1, // a menu or the console has the keys
|
||||
CTX_DOM_ZONE = 2, // the weapon hand is behind the head (the backpack)
|
||||
CTX_OFF_ZONE = 4, // the other hand is behind the head (quick save and load, as on the Quest)
|
||||
CTX_MULTI = 8, // a multiplayer game
|
||||
CTX_OFF_GRIP_AWAY = 16 // the other hand's grip was pressed with that hand well away from the weapon
|
||||
};
|
||||
|
||||
// What one physical input does in a context, ACT_NONE when nothing
|
||||
int VrFrameMap_Action(int input, int context, bool leftHanded);
|
||||
|
||||
// The map as rows, for the docs and the tests. role is 0 for a plain input; or one of the
|
||||
// weapon-hand / other-hand roles below, which the hand setting turns into a physical input.
|
||||
enum { ROLE_NONE, ROLE_TRIGGER_WEAPON, ROLE_TRIGGER_OTHER, ROLE_GRIP_WEAPON, ROLE_GRIP_OTHER };
|
||||
|
||||
typedef struct {
|
||||
int input; // FI_* (for a role row: one of the right-handed ones)
|
||||
int role;
|
||||
int require; // context bits that must be set
|
||||
int forbid; // context bits that must not be
|
||||
int action;
|
||||
const char* note;
|
||||
} frameMapRow_t;
|
||||
|
||||
const frameMapRow_t* VrFrameMap_Rows(int* count);
|
||||
|
||||
const char* VrFrameMap_InputName(int input);
|
||||
const char* VrFrameMap_ActionName(int action);
|
||||
|
||||
// The physical input behind a role
|
||||
int VrFrameMap_RoleInput(int role, bool leftHanded);
|
||||
|
||||
#endif
|
||||
@@ -52,6 +52,11 @@ void updateScopeAngles(float forwardYaw);
|
||||
|
||||
bool isBackpack(ovrTrackedController* pTracking);
|
||||
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
// true while the Steam Frame's controller map is in charge: use comes from the grips and X, not from a gesture
|
||||
bool VR_FrameHandUse();
|
||||
#endif
|
||||
|
||||
extern char * g_pszBackpackWeapon;
|
||||
|
||||
#endif //VRINPUT_H
|
||||
|
||||
@@ -144,6 +144,10 @@ void interactWithTouchScreen(ovrTrackedController *tracking, ovrInputStateTracke
|
||||
|
||||
float cursorX = -sinf(DEG2RAD(remoteAngles[YAW] - playerYaw)) + 0.5f;
|
||||
float cursorY = (float)(remoteAngles[PITCH] / 90.0) + 0.5f;
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
// the cursor is where the hand's ray meets the screen, the same place the laser ends
|
||||
TBXR_ScreenPointer(&cursorX, &cursorY);
|
||||
#endif
|
||||
touchEventType t = event_motion;
|
||||
if (newRemoteTrigState != prevRemoteTrigState)
|
||||
{
|
||||
|
||||
@@ -208,7 +208,12 @@ void HandleInput_Default( ovrInputStateTrackedRemote *pDominantTrackedRemoteNew,
|
||||
weaponangles[MELEE][PITCH] *= -1.0f;
|
||||
|
||||
// Use (Action gesture)
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
// On the Frame the grips do this: the cvar only sets how close the hand has to be
|
||||
if (vr_gesture_triggered_use->integer && !VR_FrameHandUse()) {
|
||||
#else
|
||||
if (vr_gesture_triggered_use->integer) {
|
||||
#endif
|
||||
bool gestureUseAllowed = !vr_weapon_stabilised->value;
|
||||
// Off-hand gesture
|
||||
float xOffset = hmdPosition[0] - pOffTracking->Pose.position.x;
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
/************************************************************************************
|
||||
|
||||
Filename : VrInputFrame.c
|
||||
Content : The Steam Frame controller map (the table in VrFrameMap.c) applied to the game
|
||||
|
||||
The scheme code (VrInputDefault.c and the others) reads Quest-shaped controller states: a dominant
|
||||
and an off hand, and which bits of their buttons mean what. This turns what the Frame's buttons
|
||||
mean, by the map, into those bits, and into commands for what the scheme code doesn't do.
|
||||
|
||||
*************************************************************************************/
|
||||
|
||||
#include <common/common.h>
|
||||
#include <common/library.h>
|
||||
#include <common/cvardef.h>
|
||||
#include <common/xash3d_types.h>
|
||||
#include <engine/keydefs.h>
|
||||
#include <client/touch.h>
|
||||
#include <client/client.h>
|
||||
|
||||
#include "VrInput.h"
|
||||
#include "VrCvars.h"
|
||||
#include "VrFrameMap.h"
|
||||
#include "VrInputFrame.h"
|
||||
|
||||
// every bit of the Quest-shaped state that the map decides
|
||||
#define MAP_BUTTONS (xrButton_A | xrButton_B | xrButton_X | xrButton_Y | xrButton_Enter | xrButton_Joystick | \
|
||||
xrButton_LThumb | xrButton_RThumb | xrButton_GripTrigger | xrButton_Trigger | \
|
||||
xrButton_LShoulder | xrButton_RShoulder)
|
||||
|
||||
#define MENU_HOLD_MS 500.0
|
||||
#define VIEW_HOLD_1S_MS 1000.0
|
||||
#define VIEW_HOLD_3S_MS 3000.0
|
||||
|
||||
typedef struct {
|
||||
bool wasDown;
|
||||
double downTime;
|
||||
int stage;
|
||||
} holdState_t;
|
||||
|
||||
// A button that does one thing when it's tapped (reported once, on release) and another when it's
|
||||
// held (reported once, when the time is up). Returns the stage reached, 0 for none.
|
||||
static void holdButton(holdState_t* hold, bool down, double firstMs, double secondMs, bool* tapped, bool* first, bool* second)
|
||||
{
|
||||
const double now = TBXR_GetTimeInMilliSeconds();
|
||||
*tapped = *first = *second = false;
|
||||
|
||||
if (down && !hold->wasDown) {
|
||||
hold->downTime = now;
|
||||
hold->stage = 0;
|
||||
}
|
||||
|
||||
if (down) {
|
||||
const double held = now - hold->downTime;
|
||||
if (hold->stage < 1 && held >= firstMs) {
|
||||
*first = true;
|
||||
hold->stage = 1;
|
||||
}
|
||||
if (secondMs > 0.0 && hold->stage < 2 && held >= secondMs) {
|
||||
*second = true;
|
||||
hold->stage = 2;
|
||||
}
|
||||
} else if (hold->wasDown && hold->stage == 0) {
|
||||
*tapped = true;
|
||||
}
|
||||
|
||||
hold->wasDown = down;
|
||||
}
|
||||
|
||||
static bool frameHandUse = false;
|
||||
|
||||
bool VR_FrameHandUse()
|
||||
{
|
||||
return frameHandUse;
|
||||
}
|
||||
|
||||
static float handDistance(const ovrTrackedController* a, const ovrTrackedController* b)
|
||||
{
|
||||
const float dx = a->Pose.position.x - b->Pose.position.x;
|
||||
const float dy = a->Pose.position.y - b->Pose.position.y;
|
||||
const float dz = a->Pose.position.z - b->Pose.position.z;
|
||||
return sqrtf(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
// The other hand's grip steadies the gun when that hand is on or near it and grabs things when it is
|
||||
// well away from it. Decided when the grip goes down, and kept until it comes up. Between the two
|
||||
// distances the last choice stands, so a normal two-handed hold never turns into a grab.
|
||||
#define GRIP_STEADY_BELOW_M 0.35f
|
||||
#define GRIP_GRAB_ABOVE_M 0.55f
|
||||
|
||||
static bool flick(float along, float across, float threshold)
|
||||
{
|
||||
return along > threshold && along > fabsf(across) * 1.5f;
|
||||
}
|
||||
|
||||
void VrInputFrame_Apply(const frameRaw_t* raw)
|
||||
{
|
||||
static uint32_t previousActions[2]; // [0] the bits of actions 0..31, [1] those above
|
||||
static holdState_t menuHold, viewHold;
|
||||
static bool gripWasDown = false;
|
||||
static bool gripZone = false;
|
||||
static bool crouchToggled = false;
|
||||
static bool offGripWasDown = false;
|
||||
static bool offGripAway = false;
|
||||
static bool scoreboardShown = false;
|
||||
|
||||
frameHandUse = true;
|
||||
|
||||
const bool leftHanded = (vr_control_scheme != NULL && vr_control_scheme->integer >= 10);
|
||||
const bool menu = cls.key_dest != key_game;
|
||||
const bool multiplayer = isMultiplayer();
|
||||
if (!multiplayer) {
|
||||
scoreboardShown = false;
|
||||
}
|
||||
|
||||
ovrInputStateTrackedRemote* dom = leftHanded ? &leftTrackedRemoteState_new : &rightTrackedRemoteState_new;
|
||||
ovrInputStateTrackedRemote* off = leftHanded ? &rightTrackedRemoteState_new : &leftTrackedRemoteState_new;
|
||||
ovrTrackedController* domTracking = leftHanded ? &leftRemoteTracking_new : &rightRemoteTracking_new;
|
||||
ovrTrackedController* offTracking = leftHanded ? &rightRemoteTracking_new : &leftRemoteTracking_new;
|
||||
|
||||
// the physical inputs that are down
|
||||
bool menuTap, menuHeld, menuUnused, viewTap, view1s, view3s;
|
||||
holdButton(&menuHold, raw->menu, MENU_HOLD_MS, 0.0, &menuTap, &menuHeld, &menuUnused);
|
||||
holdButton(&viewHold, raw->view, VIEW_HOLD_1S_MS, VIEW_HOLD_3S_MS, &viewTap, &view1s, &view3s);
|
||||
|
||||
bool down[FI_COUNT] = {0};
|
||||
down[FI_L_TRIGGER] = raw->triggerLeft > 0.5f;
|
||||
down[FI_R_TRIGGER] = raw->triggerRight > 0.5f;
|
||||
down[FI_L_GRIP] = raw->gripLeft > 0.5f;
|
||||
down[FI_R_GRIP] = raw->gripRight > 0.5f;
|
||||
down[FI_L_BUMPER] = raw->bumperLeft;
|
||||
down[FI_R_BUMPER] = raw->bumperRight;
|
||||
down[FI_L_STICK_CLICK] = raw->stickClickLeft;
|
||||
down[FI_R_STICK_CLICK] = raw->stickClickRight;
|
||||
down[FI_A] = raw->a;
|
||||
down[FI_B] = raw->b;
|
||||
down[FI_X] = raw->x;
|
||||
down[FI_Y] = raw->y;
|
||||
down[FI_MENU_SHORT] = menuTap;
|
||||
down[FI_MENU_HOLD] = menuHeld;
|
||||
down[FI_VIEW_SHORT] = viewTap;
|
||||
down[FI_VIEW_HOLD_1S] = view1s;
|
||||
down[FI_VIEW_HOLD_3S] = view3s;
|
||||
down[FI_DPAD_UP] = raw->dpadUp;
|
||||
down[FI_DPAD_DOWN] = raw->dpadDown;
|
||||
down[FI_DPAD_LEFT] = raw->dpadLeft;
|
||||
down[FI_DPAD_RIGHT] = raw->dpadRight;
|
||||
down[FI_L_STICK_UP] = flick(raw->stickLeftY, raw->stickLeftX, 0.6f);
|
||||
down[FI_L_STICK_DOWN] = flick(-raw->stickLeftY, raw->stickLeftX, 0.6f);
|
||||
down[FI_L_STICK_LEFT] = flick(-raw->stickLeftX, raw->stickLeftY, 0.6f);
|
||||
down[FI_L_STICK_RIGHT] = flick(raw->stickLeftX, raw->stickLeftY, 0.6f);
|
||||
down[FI_R_STICK_UP] = flick(raw->stickRightY, raw->stickRightX, 0.75f);
|
||||
down[FI_R_STICK_DOWN] = flick(-raw->stickRightY, raw->stickRightX, 0.75f);
|
||||
down[FI_R_STICK_LEFT] = flick(-raw->stickRightX, raw->stickRightY, 0.3f);
|
||||
down[FI_R_STICK_RIGHT] = flick(raw->stickRightX, raw->stickRightY, 0.3f);
|
||||
|
||||
// Where the hands are decides some things. The weapon hand's grip is the backpack when it was
|
||||
// pressed with the hand behind the head, and stays that until it's let go (the crowbar stays out
|
||||
// while you swing it).
|
||||
const int weaponGrip = leftHanded ? FI_L_GRIP : FI_R_GRIP;
|
||||
if (down[weaponGrip] && !gripWasDown) {
|
||||
gripZone = isBackpack(domTracking);
|
||||
}
|
||||
gripWasDown = down[weaponGrip];
|
||||
|
||||
const int offGrip = leftHanded ? FI_R_GRIP : FI_L_GRIP;
|
||||
if (down[offGrip] && !offGripWasDown) {
|
||||
const float apart = handDistance(domTracking, offTracking);
|
||||
if (apart < GRIP_STEADY_BELOW_M) {
|
||||
offGripAway = false;
|
||||
} else if (apart > GRIP_GRAB_ABOVE_M) {
|
||||
offGripAway = true;
|
||||
}
|
||||
ALOGI("[vr] other hand grip: %s (hands %.2f m apart)", offGripAway ? "interact" : "steady the gun", apart);
|
||||
}
|
||||
offGripWasDown = down[offGrip];
|
||||
|
||||
int context = 0;
|
||||
if (offGripAway) context |= CTX_OFF_GRIP_AWAY;
|
||||
if (menu) context |= CTX_MENU;
|
||||
if (down[weaponGrip] ? gripZone : isBackpack(domTracking)) context |= CTX_DOM_ZONE;
|
||||
if (isBackpack(offTracking) && !multiplayer) context |= CTX_OFF_ZONE;
|
||||
if (multiplayer) context |= CTX_MULTI;
|
||||
|
||||
// say what was pressed and what the map made of it, once a press, for the log (not the stick directions)
|
||||
static bool wasDown[FI_COUNT];
|
||||
for (int input = 0; input < FI_COUNT; input++) {
|
||||
const int mapped = VrFrameMap_Action(input, context, leftHanded);
|
||||
if (down[input] && !wasDown[input] && mapped != ACT_NONE && mapped != ACT_MOVE && mapped != ACT_TURN) {
|
||||
ALOGI("[vr] %s -> %s%s%s", VrFrameMap_InputName(input),
|
||||
VrFrameMap_ActionName(mapped),
|
||||
menu ? " (menu)" : "", (context & CTX_OFF_ZONE) ? " (hand behind the head)" : "");
|
||||
}
|
||||
wasDown[input] = down[input];
|
||||
}
|
||||
|
||||
// what they do: a bit for each action
|
||||
uint32_t actions[2] = {0, 0};
|
||||
uint32_t quickSaveFromMenuHold = 0;
|
||||
for (int input = 0; input < FI_COUNT; input++) {
|
||||
if (!down[input]) {
|
||||
continue;
|
||||
}
|
||||
const int action = VrFrameMap_Action(input, context, leftHanded);
|
||||
if (action == ACT_NONE) {
|
||||
continue;
|
||||
}
|
||||
actions[action / 32] |= 1u << (action % 32);
|
||||
if (input == FI_MENU_HOLD && action == ACT_QUICKSAVE) {
|
||||
quickSaveFromMenuHold = 1;
|
||||
}
|
||||
}
|
||||
#define HAS(a) ((actions[(a) / 32] & (1u << ((a) % 32))) != 0)
|
||||
#define PRESSED(a) (HAS(a) && !(previousActions[(a) / 32] & (1u << ((a) % 32))))
|
||||
#define RELEASED(a) (!HAS(a) && (previousActions[(a) / 32] & (1u << ((a) % 32))))
|
||||
|
||||
if (PRESSED(ACT_CROUCH_TOGGLE)) {
|
||||
crouchToggled = !crouchToggled;
|
||||
}
|
||||
|
||||
// The Quest-shaped states the scheme code reads. The dominant hand: trigger fires (or clicks in a
|
||||
// menu), grip (only the backpack, from behind the head), stick turns, use on the stick click
|
||||
// bit, crouch and jump on its two buttons. The off hand: grip steadies the gun, its stick
|
||||
// moves, its two buttons are the torch and (with the hand behind the head) quick save and
|
||||
// load, the stick click bit is the laser sight.
|
||||
const uint32_t domCrouch = leftHanded ? xrButton_X : xrButton_A;
|
||||
const uint32_t domJump = leftHanded ? xrButton_Y : xrButton_B;
|
||||
const uint32_t offTorch = leftHanded ? xrButton_A : xrButton_X;
|
||||
const uint32_t offLoad = leftHanded ? xrButton_B : xrButton_Y;
|
||||
|
||||
leftTrackedRemoteState_new.Buttons &= ~MAP_BUTTONS;
|
||||
rightTrackedRemoteState_new.Buttons &= ~MAP_BUTTONS;
|
||||
|
||||
if (HAS(ACT_FIRE) || HAS(ACT_MENU_CLICK)) dom->Buttons |= xrButton_Trigger;
|
||||
if (HAS(ACT_BACKPACK)) dom->Buttons |= xrButton_GripTrigger;
|
||||
if (HAS(ACT_STEADY)) off->Buttons |= xrButton_GripTrigger;
|
||||
if (HAS(ACT_JUMP)) dom->Buttons |= domJump;
|
||||
if (HAS(ACT_CROUCH_HOLD) || crouchToggled) dom->Buttons |= domCrouch;
|
||||
if (HAS(ACT_USE)) dom->Buttons |= xrButton_Joystick;
|
||||
// what the Quest does with its off-hand buttons, with the hand behind the head
|
||||
if (HAS(ACT_FLASHLIGHT) || (HAS(ACT_QUICKSAVE) && !quickSaveFromMenuHold)) off->Buttons |= offTorch;
|
||||
if (HAS(ACT_QUICKLOAD)) off->Buttons |= offLoad;
|
||||
if (HAS(ACT_LASER)) off->Buttons |= xrButton_Joystick;
|
||||
// one escape for pause and for every way back
|
||||
if (HAS(ACT_PAUSE) || HAS(ACT_MENU_BACK)) leftTrackedRemoteState_new.Buttons |= xrButton_Enter;
|
||||
|
||||
// triggers and sticks: the left stick is always the move stick and the right stick the turn stick
|
||||
// (the weapon hand only decides the aim, the triggers and the grips), and there's no weapon
|
||||
// select on the turn stick, the bumpers and the D-pad do that.
|
||||
dom->IndexTrigger = leftHanded ? raw->triggerLeft : raw->triggerRight;
|
||||
off->IndexTrigger = 0.0f;
|
||||
dom->GripTrigger = HAS(ACT_BACKPACK) ? 1.0f : 0.0f;
|
||||
off->GripTrigger = HAS(ACT_STEADY) ? 1.0f : 0.0f;
|
||||
dom->Joystick.x = raw->stickRightX;
|
||||
dom->Joystick.y = 0.0f;
|
||||
off->Joystick.x = raw->stickLeftX;
|
||||
off->Joystick.y = raw->stickLeftY;
|
||||
|
||||
// commands the scheme code doesn't have
|
||||
if (PRESSED(ACT_RECENTRE)) {
|
||||
TBXR_RecenterToHead(false);
|
||||
TBXR_Vibrate(80, 0, 0.6f);
|
||||
TBXR_Vibrate(80, 1, 0.6f);
|
||||
}
|
||||
if (PRESSED(ACT_RECENTRE_HEIGHT)) {
|
||||
TBXR_RecenterToHead(true);
|
||||
TBXR_Vibrate(80, 0, 0.6f);
|
||||
TBXR_Vibrate(80, 1, 0.6f);
|
||||
}
|
||||
|
||||
// the grips and X are "use", from the hand: the server finds what's in reach of that hand
|
||||
if (PRESSED(ACT_USE_OFF)) sendButtonActionSimple("+use2");
|
||||
if (RELEASED(ACT_USE_OFF)) sendButtonActionSimple("-use2");
|
||||
|
||||
if (!menu) {
|
||||
if (PRESSED(ACT_ALTFIRE)) sendButtonActionSimple("+attack2");
|
||||
if (RELEASED(ACT_ALTFIRE)) sendButtonActionSimple("-attack2");
|
||||
if (PRESSED(ACT_RELOAD)) sendButtonActionSimple("+reload");
|
||||
if (RELEASED(ACT_RELOAD)) sendButtonActionSimple("-reload");
|
||||
if (PRESSED(ACT_PREV_WEAPON)) sendButtonActionSimple("invprev");
|
||||
if (PRESSED(ACT_NEXT_WEAPON)) sendButtonActionSimple("invnext");
|
||||
if (PRESSED(ACT_LAST_WEAPON)) sendButtonActionSimple("lastinv");
|
||||
if (PRESSED(ACT_SCOREBOARD) && multiplayer) {
|
||||
// the scoreboard only. The flat screen view is the vr_flat_screen setting, no button does it
|
||||
scoreboardShown = !scoreboardShown;
|
||||
sendButtonAction("+showscores", scoreboardShown);
|
||||
}
|
||||
if (quickSaveFromMenuHold && PRESSED(ACT_QUICKSAVE) && !multiplayer) {
|
||||
sendButtonActionSimple("savequick");
|
||||
TBXR_Vibrate(80, leftHanded ? 0 : 1, 0.6f);
|
||||
}
|
||||
} else {
|
||||
// let go in a menu of what was held in the game, so nothing stays down
|
||||
if (RELEASED(ACT_ALTFIRE)) sendButtonActionSimple("-attack2");
|
||||
if (RELEASED(ACT_RELOAD)) sendButtonActionSimple("-reload");
|
||||
|
||||
static const struct { int action; int key; } keys[] = {
|
||||
{ ACT_MENU_CONFIRM, K_ENTER }, { ACT_MENU_UP, K_UPARROW }, { ACT_MENU_DOWN, K_DOWNARROW },
|
||||
{ ACT_MENU_LEFT, K_LEFTARROW }, { ACT_MENU_RIGHT, K_RIGHTARROW },
|
||||
{ ACT_MENU_PAGE_PREV, K_PGUP }, { ACT_MENU_PAGE_NEXT, K_PGDN },
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
|
||||
if (PRESSED(keys[i].action)) Key_Event(keys[i].key, true);
|
||||
if (RELEASED(keys[i].action)) Key_Event(keys[i].key, false);
|
||||
}
|
||||
if (PRESSED(ACT_MENU_SCROLL_UP)) { Key_Event(K_MWHEELUP, true); Key_Event(K_MWHEELUP, false); }
|
||||
if (PRESSED(ACT_MENU_SCROLL_DOWN)) { Key_Event(K_MWHEELDOWN, true); Key_Event(K_MWHEELDOWN, false); }
|
||||
}
|
||||
|
||||
previousActions[0] = actions[0];
|
||||
previousActions[1] = actions[1];
|
||||
#undef HAS
|
||||
#undef PRESSED
|
||||
#undef RELEASED
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Steam Frame controllers: what the runtime says about the physical buttons this frame, and
|
||||
// the code that turns it into what the game reads. The map itself is VrFrameMap.c.
|
||||
#ifndef VRINPUTFRAME_H
|
||||
#define VRINPUTFRAME_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef struct {
|
||||
float triggerLeft, triggerRight, gripLeft, gripRight;
|
||||
bool bumperLeft, bumperRight;
|
||||
bool stickClickLeft, stickClickRight;
|
||||
bool a, b, x, y, menu, view;
|
||||
bool dpadUp, dpadDown, dpadLeft, dpadRight;
|
||||
float stickLeftX, stickLeftY, stickRightX, stickRightY;
|
||||
} frameRaw_t;
|
||||
|
||||
// Called once a frame after the Quest-shaped controller states have been read, when the runtime
|
||||
// is using the Frame's controller profile. Rewrites those states and sends the commands.
|
||||
void VrInputFrame_Apply(const frameRaw_t* raw);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,153 @@
|
||||
#include <math.h>
|
||||
|
||||
#include "VrPointerMath.h"
|
||||
|
||||
static void rotate(const float q[4], const float v[3], float out[3])
|
||||
{
|
||||
const float x = q[0], y = q[1], z = q[2], w = q[3];
|
||||
const float cx = y * v[2] - z * v[1];
|
||||
const float cy = z * v[0] - x * v[2];
|
||||
const float cz = x * v[1] - y * v[0];
|
||||
const float dx = y * cz - z * cy;
|
||||
const float dy = z * cx - x * cz;
|
||||
const float dz = x * cy - y * cx;
|
||||
out[0] = v[0] + 2.0f * (w * cx + dx);
|
||||
out[1] = v[1] + 2.0f * (w * cy + dy);
|
||||
out[2] = v[2] + 2.0f * (w * cz + dz);
|
||||
}
|
||||
|
||||
static float dot(const float a[3], const float b[3])
|
||||
{
|
||||
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
||||
}
|
||||
|
||||
static void cross(const float a[3], const float b[3], float out[3])
|
||||
{
|
||||
out[0] = a[1] * b[2] - a[2] * b[1];
|
||||
out[1] = a[2] * b[0] - a[0] * b[2];
|
||||
out[2] = a[0] * b[1] - a[1] * b[0];
|
||||
}
|
||||
|
||||
static bool normalise(float v[3])
|
||||
{
|
||||
const float length = sqrtf(dot(v, v));
|
||||
if (length < 1e-6f)
|
||||
return false;
|
||||
v[0] /= length; v[1] /= length; v[2] /= length;
|
||||
return true;
|
||||
}
|
||||
|
||||
void VrPointer_Forward(const float pose[7], float out[3])
|
||||
{
|
||||
const float forward[3] = {0.0f, 0.0f, -1.0f};
|
||||
rotate(pose + 3, forward, out);
|
||||
}
|
||||
|
||||
bool VrPointer_RayQuad(const float origin[3], const float direction[3], const float quadPose[7],
|
||||
float width, float height, float* u, float* v, float* t)
|
||||
{
|
||||
const float xAxis[3] = {1.0f, 0.0f, 0.0f}, yAxis[3] = {0.0f, 1.0f, 0.0f}, zAxis[3] = {0.0f, 0.0f, 1.0f};
|
||||
float right[3], up[3], normal[3];
|
||||
rotate(quadPose + 3, xAxis, right);
|
||||
rotate(quadPose + 3, yAxis, up);
|
||||
rotate(quadPose + 3, zAxis, normal);
|
||||
|
||||
float d[3] = {direction[0], direction[1], direction[2]};
|
||||
if (!normalise(d))
|
||||
return false;
|
||||
|
||||
const float facing = dot(d, normal);
|
||||
if (facing > -1e-4f) // away from the quad's face, or along it
|
||||
return false;
|
||||
|
||||
const float toQuad[3] = {quadPose[0] - origin[0], quadPose[1] - origin[1], quadPose[2] - origin[2]};
|
||||
const float distance = dot(toQuad, normal) / facing;
|
||||
if (distance <= 0.0f)
|
||||
return false;
|
||||
|
||||
const float hit[3] = {origin[0] + d[0] * distance - quadPose[0], origin[1] + d[1] * distance - quadPose[1],
|
||||
origin[2] + d[2] * distance - quadPose[2]};
|
||||
*u = dot(hit, right) / width + 0.5f;
|
||||
*v = 0.5f - dot(hit, up) / height;
|
||||
*t = distance;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void quaternionFromAxes(const float x[3], const float y[3], const float z[3], float q[4])
|
||||
{
|
||||
// the rotation whose columns are x, y, z
|
||||
const float trace = x[0] + y[1] + z[2];
|
||||
if (trace > 0.0f)
|
||||
{
|
||||
const float s = sqrtf(trace + 1.0f) * 2.0f;
|
||||
q[3] = 0.25f * s;
|
||||
q[0] = (y[2] - z[1]) / s;
|
||||
q[1] = (z[0] - x[2]) / s;
|
||||
q[2] = (x[1] - y[0]) / s;
|
||||
}
|
||||
else if (x[0] > y[1] && x[0] > z[2])
|
||||
{
|
||||
const float s = sqrtf(1.0f + x[0] - y[1] - z[2]) * 2.0f;
|
||||
q[3] = (y[2] - z[1]) / s;
|
||||
q[0] = 0.25f * s;
|
||||
q[1] = (y[0] + x[1]) / s;
|
||||
q[2] = (z[0] + x[2]) / s;
|
||||
}
|
||||
else if (y[1] > z[2])
|
||||
{
|
||||
const float s = sqrtf(1.0f + y[1] - x[0] - z[2]) * 2.0f;
|
||||
q[3] = (z[0] - x[2]) / s;
|
||||
q[0] = (y[0] + x[1]) / s;
|
||||
q[1] = 0.25f * s;
|
||||
q[2] = (z[1] + y[2]) / s;
|
||||
}
|
||||
else
|
||||
{
|
||||
const float s = sqrtf(1.0f + z[2] - x[0] - y[1]) * 2.0f;
|
||||
q[3] = (x[1] - y[0]) / s;
|
||||
q[0] = (z[0] + x[2]) / s;
|
||||
q[1] = (z[1] + y[2]) / s;
|
||||
q[2] = 0.25f * s;
|
||||
}
|
||||
}
|
||||
|
||||
float VrPointer_BeamPose(const float a[3], const float b[3], const float eye[3], float out[7])
|
||||
{
|
||||
float y[3] = {b[0] - a[0], b[1] - a[1], b[2] - a[2]};
|
||||
const float length = sqrtf(dot(y, y));
|
||||
if (!normalise(y))
|
||||
{
|
||||
out[0] = a[0]; out[1] = a[1]; out[2] = a[2];
|
||||
out[3] = out[4] = out[5] = 0.0f; out[6] = 1.0f;
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const float mid[3] = {(a[0] + b[0]) * 0.5f, (a[1] + b[1]) * 0.5f, (a[2] + b[2]) * 0.5f};
|
||||
float z[3] = {eye[0] - mid[0], eye[1] - mid[1], eye[2] - mid[2]};
|
||||
const float along = dot(z, y);
|
||||
z[0] -= y[0] * along; z[1] -= y[1] * along; z[2] -= y[2] * along;
|
||||
if (!normalise(z))
|
||||
{
|
||||
// looking straight down the beam: any way round will do
|
||||
const float other[3] = {fabsf(y[0]) < 0.9f ? 1.0f : 0.0f, fabsf(y[0]) < 0.9f ? 0.0f : 1.0f, 0.0f};
|
||||
cross(y, other, z);
|
||||
normalise(z);
|
||||
}
|
||||
float x[3];
|
||||
cross(y, z, x);
|
||||
|
||||
out[0] = mid[0]; out[1] = mid[1]; out[2] = mid[2];
|
||||
quaternionFromAxes(x, y, z, out + 3);
|
||||
return length;
|
||||
}
|
||||
|
||||
void VrPointer_DotPose(const float p[3], const float quadPose[7], float lift, float out[7])
|
||||
{
|
||||
const float zAxis[3] = {0.0f, 0.0f, 1.0f};
|
||||
float normal[3];
|
||||
rotate(quadPose + 3, zAxis, normal);
|
||||
out[0] = p[0] + normal[0] * lift;
|
||||
out[1] = p[1] + normal[1] * lift;
|
||||
out[2] = p[2] + normal[2] * lift;
|
||||
out[3] = quadPose[3]; out[4] = quadPose[4]; out[5] = quadPose[5]; out[6] = quadPose[6];
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Steam Frame: the menu laser. A ray from the weapon hand's aim pose against the flat screen
|
||||
// quad, and the thin quads (the beam and its dot) that are drawn as layers of their own so nothing
|
||||
// can cover them. Plain C with no OpenXR types, for the host tests. Poses are position x y z and
|
||||
// quaternion x y z w (OpenXR axes: forward is -z, a quad faces +z).
|
||||
#ifndef VRPOINTERMATH_H
|
||||
#define VRPOINTERMATH_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
// Where a ray from origin along direction (any length) meets the quad, from the quad's front. u and v
|
||||
// are 0..1 across and down it (they can be outside that when the ray misses the quad itself), t is how
|
||||
// far along the ray in metres of the (normalised) direction. False when the ray points away from the
|
||||
// quad or along it.
|
||||
bool VrPointer_RayQuad(const float origin[3], const float direction[3], const float quadPose[7],
|
||||
float width, float height, float* u, float* v, float* t);
|
||||
|
||||
// The direction a pose's -z axis points in
|
||||
void VrPointer_Forward(const float pose[7], float out[3]);
|
||||
|
||||
// The pose of a thin quad that lies along the line from a to b (its long side, the height, is
|
||||
// the line's length) and turns about it to face the eye. Returns the length.
|
||||
float VrPointer_BeamPose(const float a[3], const float b[3], const float eye[3], float out[7]);
|
||||
|
||||
// A pose at p turned like the quad (and pushed out of its face by lift, towards the viewer)
|
||||
void VrPointer_DotPose(const float p[3], const float quadPose[7], float lift, float out[7]);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,8 @@
|
||||
LOCAL_PATH := $(call my-dir)
|
||||
|
||||
# libxash links against the pinned Khronos loader in every Android build type.
|
||||
# Gradle fetches this build-only binary before ndk-build runs.
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := openxr_loader
|
||||
LOCAL_SRC_FILES := ../../../build/openxr-loader/1.1.63/libopenxr_loader.so
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
@@ -69,7 +69,11 @@ GNU General Public License for more details.
|
||||
#define MENUDLL "libmenu" POSTFIX "." OS_LIB_EXT
|
||||
#define CLIENTDLL "libclient" POSTFIX "." OS_LIB_EXT
|
||||
#define SERVERDLL "libserver" POSTFIX "." OS_LIB_EXT
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
#define GAMEPATH "/sdcard/Documents/Lambda1VR"
|
||||
#else
|
||||
#define GAMEPATH "/sdcard/xash"
|
||||
#endif
|
||||
#elif defined(__SAILFISH__)
|
||||
#define POSTFIX
|
||||
// don't change these names
|
||||
|
||||
@@ -14,6 +14,12 @@ LOCAL_CFLAGS += -D__MULTITEXTURE_SUPPORT__ -DXASH_GLES -DXASH_GL4ES -DUSE_EVDEV
|
||||
#enable VR compatibility
|
||||
LOCAL_CFLAGS += -DVR
|
||||
|
||||
# Valve Steam Frame build (gradle assembleFrame, docs/STEAM_FRAME_VR.md)
|
||||
ifeq ($(L1VR_STEAM_FRAME),1)
|
||||
# the engine's messages go to logcat (tag Xash), but only with a developer level
|
||||
LOCAL_CFLAGS += -DL1VR_STEAM_FRAME -DDEFAULT_DEV=3
|
||||
endif
|
||||
|
||||
LOCAL_CONLYFLAGS += -std=c99
|
||||
|
||||
LOCAL_C_INCLUDES := \
|
||||
@@ -180,6 +186,29 @@ LOCAL_SRC_FILES := \
|
||||
platform/android/android_nosdl.c \
|
||||
platform/android/dlsym-weak.cpp
|
||||
|
||||
# test switches for the Frame build, off unless vr-diag.ini is in the data folder
|
||||
ifeq ($(L1VR_STEAM_FRAME),1)
|
||||
LAMBDA1VR_SRC_FILES += \
|
||||
$(LOCAL_PATH)/../../../Lambda1VR/VrDiag.c \
|
||||
$(LOCAL_PATH)/../../../Lambda1VR/VrDiagCore.c
|
||||
LOCAL_SRC_FILES += $(LOCAL_PATH)/../../../Lambda1VR/VrDiag.c \
|
||||
$(LOCAL_PATH)/../../../Lambda1VR/VrDiagCore.c
|
||||
endif
|
||||
|
||||
ifeq ($(L1VR_STEAM_FRAME),1)
|
||||
LAMBDA1VR_SRC_FILES += $(LOCAL_PATH)/../../../Lambda1VR/VrEyeMath.c \
|
||||
$(LOCAL_PATH)/../../../Lambda1VR/VrFrameMap.c \
|
||||
$(LOCAL_PATH)/../../../Lambda1VR/VrPointerMath.c \
|
||||
$(LOCAL_PATH)/../../../Lambda1VR/VrInputFrame.c
|
||||
LOCAL_SRC_FILES += $(LOCAL_PATH)/../../../Lambda1VR/VrEyeMath.c \
|
||||
$(LOCAL_PATH)/../../../Lambda1VR/VrFrameMap.c \
|
||||
$(LOCAL_PATH)/../../../Lambda1VR/VrPointerMath.c \
|
||||
$(LOCAL_PATH)/../../../Lambda1VR/VrInputFrame.c
|
||||
endif
|
||||
|
||||
# warnings for the VR layer (argtable3 isn't ours)
|
||||
$(call add-src-files-target-cflags,$(filter-out %/argtable3.c,$(LAMBDA1VR_SRC_FILES)),-Wall)
|
||||
|
||||
LOCAL_LDLIBS := -ldl -llog -landroid -lGLESv3 -lEGL # include default libraries
|
||||
LOCAL_SHARED_LIBRARIES := openxr_loader gl4es
|
||||
|
||||
|
||||
@@ -1148,6 +1148,58 @@ static void CL_DrawPause( void )
|
||||
R_DrawStretchPic( x, y, width, height, 0, 0, 1, 1, cls.pauseIcon );
|
||||
}
|
||||
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
extern convar_t *vr_stereo_side;
|
||||
float VR_GetHudShift( int eye );
|
||||
void R_SetHudShift( float pixels );
|
||||
void R_SetHudRoll( float degrees );
|
||||
float VR_GetHudRoll( int eye );
|
||||
bool isScopeEngaged( void );
|
||||
|
||||
/*
|
||||
===============
|
||||
CL_RedrawHUD
|
||||
|
||||
The client DLL puts its HUD in front of the screen with a fixed sideways shift of width / 36
|
||||
per eye, which it works out from vr_stereo_side. That's about 0.4 m on the Frame's lenses, and
|
||||
tiring. While it draws, the eyes are given as the mono ones (that's 0 for it, and the 2D
|
||||
offset onto the optical axis still works for those), and the whole HUD is shifted here
|
||||
instead, to where vr_hud_distance says. A scope is mono already, so that is left alone.
|
||||
===============
|
||||
*/
|
||||
static void CL_RedrawHUD( qboolean paused )
|
||||
{
|
||||
const int eye = vr_stereo_side->integer;
|
||||
float shift;
|
||||
|
||||
if( eye < VR_EYE_LEFT || eye > VR_EYE_RIGHT || isScopeEngaged( ))
|
||||
{
|
||||
CL_DrawCenterPrint ();
|
||||
clgame.dllFuncs.pfnRedraw( cl.time, cl.refdef.intermission );
|
||||
if( paused ) CL_DrawPause();
|
||||
return;
|
||||
}
|
||||
|
||||
shift = VR_GetHudShift( eye );
|
||||
|
||||
vr_stereo_side->value = eye + 2;
|
||||
vr_stereo_side->integer = eye + 2;
|
||||
R_SetHudShift( shift );
|
||||
R_SetHudRoll( VR_GetHudRoll( eye ));
|
||||
R_Set2DMode( true );
|
||||
|
||||
CL_DrawCenterPrint ();
|
||||
clgame.dllFuncs.pfnRedraw( cl.time, cl.refdef.intermission );
|
||||
if( paused ) CL_DrawPause();
|
||||
|
||||
vr_stereo_side->value = eye;
|
||||
vr_stereo_side->integer = eye;
|
||||
R_SetHudShift( 0.0f );
|
||||
R_SetHudRoll( 0.0f );
|
||||
R_Set2DMode( true );
|
||||
}
|
||||
#endif
|
||||
|
||||
void CL_DrawHUD( int state )
|
||||
{
|
||||
if( state == CL_ACTIVE && !cl.video_prepped )
|
||||
@@ -1161,15 +1213,23 @@ void CL_DrawHUD( int state )
|
||||
case CL_ACTIVE:
|
||||
CL_DrawScreenFade ();
|
||||
CL_DrawCrosshair ();
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
CL_RedrawHUD ( false );
|
||||
#else
|
||||
CL_DrawCenterPrint ();
|
||||
clgame.dllFuncs.pfnRedraw( cl.time, cl.refdef.intermission );
|
||||
#endif
|
||||
break;
|
||||
case CL_PAUSED:
|
||||
CL_DrawScreenFade ();
|
||||
CL_DrawCrosshair ();
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
CL_RedrawHUD ( true );
|
||||
#else
|
||||
CL_DrawCenterPrint ();
|
||||
clgame.dllFuncs.pfnRedraw( cl.time, cl.refdef.intermission );
|
||||
CL_DrawPause();
|
||||
#endif
|
||||
break;
|
||||
case CL_LOADING:
|
||||
CL_DrawLoading( scr_loading->value );
|
||||
|
||||
@@ -43,11 +43,20 @@ typedef struct
|
||||
static dirty_t scr_dirty, scr_old_dirty[2];
|
||||
static qboolean scr_init = false;
|
||||
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
float VR_GetHudShift( int eye );
|
||||
#endif
|
||||
|
||||
int GetStereoDepthOffset()
|
||||
{
|
||||
if (vr_stereo_side->value >= VR_EYE_LEFT_MONO)
|
||||
return 0;
|
||||
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
// the same place the HUD is put
|
||||
return (int)VR_GetHudShift( vr_stereo_side->integer );
|
||||
#endif
|
||||
|
||||
return (int)( ( vr_stereo_side->value * -2.0f ) + 1.0f) * (scr_width->integer / 36.0f);
|
||||
}
|
||||
|
||||
|
||||
@@ -283,6 +283,23 @@ two are not the same point, so 2D content anchored to the buffer centre would no
|
||||
the eyes. Shifting the ortho window here moves every 2D item at once.
|
||||
===============
|
||||
*/
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
// extra shift for the HUD pass (see CL_RedrawHUD), in pixels, positive to the right
|
||||
static float r_hudShift = 0.0f;
|
||||
// and how far to turn it, in degrees, about the optical axis (it stays level when the head rolls)
|
||||
static float r_hudRoll = 0.0f;
|
||||
|
||||
void R_SetHudShift( float pixels )
|
||||
{
|
||||
r_hudShift = pixels;
|
||||
}
|
||||
|
||||
void R_SetHudRoll( float degrees )
|
||||
{
|
||||
r_hudRoll = degrees;
|
||||
}
|
||||
#endif
|
||||
|
||||
void R_Get2DOffset( float *dx, float *dy )
|
||||
{
|
||||
*dx = *dy = 0.0f;
|
||||
@@ -292,6 +309,9 @@ void R_Get2DOffset( float *dx, float *dy )
|
||||
return;
|
||||
|
||||
VR_Get2DOffset( vr_stereo_side->integer, glState.width, glState.height, dx, dy );
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
*dx += r_hudShift;
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -302,6 +322,9 @@ R_Set2DMode
|
||||
void R_Set2DMode( qboolean enable )
|
||||
{
|
||||
static float last_dx = 0.0f, last_dy = 0.0f;
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
static float last_roll = 0.0f;
|
||||
#endif
|
||||
float dx, dy;
|
||||
|
||||
if( enable )
|
||||
@@ -309,8 +332,14 @@ void R_Set2DMode( qboolean enable )
|
||||
R_Get2DOffset( &dx, &dy );
|
||||
|
||||
// the offset flips sign between eyes, so a cached ortho from the other eye is stale
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
if( glState.in2DMode && dx == last_dx && dy == last_dy && r_hudRoll == last_roll )
|
||||
return;
|
||||
last_roll = r_hudRoll;
|
||||
#else
|
||||
if( glState.in2DMode && dx == last_dx && dy == last_dy )
|
||||
return;
|
||||
#endif
|
||||
|
||||
last_dx = dx;
|
||||
last_dy = dy;
|
||||
@@ -320,6 +349,16 @@ void R_Set2DMode( qboolean enable )
|
||||
pglMatrixMode( GL_PROJECTION );
|
||||
pglLoadIdentity();
|
||||
pglOrtho( -dx, glState.width - dx, glState.height - dy, -dy, -99999, 99999 );
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
if( r_hudRoll != 0.0f )
|
||||
{
|
||||
// about the middle of the 2D space, which the offset above has put on the optical axis,
|
||||
// before the HUD distance shift (that one is along the eyes, and so is along the image)
|
||||
pglTranslatef( glState.width * 0.5f, glState.height * 0.5f, 0.0f );
|
||||
pglRotatef( r_hudRoll, 0.0f, 0.0f, 1.0f );
|
||||
pglTranslatef( -glState.width * 0.5f, -glState.height * 0.5f, 0.0f );
|
||||
}
|
||||
#endif
|
||||
pglMatrixMode( GL_MODELVIEW );
|
||||
pglLoadIdentity();
|
||||
|
||||
|
||||
@@ -668,6 +668,9 @@ static void R_SetupProjectionMatrix( const ref_params_t *fd, matrix4x4 m )
|
||||
}
|
||||
|
||||
#ifdef VR
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
bool VR_GetEyeTransform( int eye, float worldScale, vec3_t offset, float *cantDegrees, vec3_t cantAxis );
|
||||
#endif
|
||||
/*
|
||||
=============
|
||||
R_SetupModelviewMatrix
|
||||
@@ -677,10 +680,24 @@ static void R_SetupModelviewMatrix( const ref_params_t *fd, matrix4x4 m )
|
||||
{
|
||||
Matrix4x4_CreateModelview( m );
|
||||
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
// Each eye is where the runtime says it is, in the head's frame, turned the way
|
||||
// it says too, instead of a fixed IPD along the side
|
||||
if (vr_stereo_side->value == VR_EYE_LEFT || vr_stereo_side->value == VR_EYE_RIGHT) {
|
||||
vec3_t eyeOffset, cantAxis;
|
||||
float cantDegrees;
|
||||
if (VR_GetEyeTransform(vr_stereo_side->integer, vr_worldscale->value, eyeOffset, &cantDegrees, cantAxis)) {
|
||||
if (cantDegrees != 0.0f)
|
||||
Matrix4x4_ConcatRotate(m, -cantDegrees, cantAxis[0], cantAxis[1], cantAxis[2]);
|
||||
Matrix4x4_ConcatTranslate(m, -eyeOffset[0], -eyeOffset[1], -eyeOffset[2]);
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (vr_stereo_side->value == VR_EYE_LEFT || vr_stereo_side->value == VR_EYE_RIGHT) {
|
||||
Matrix4x4_ConcatTranslate(m, 0, vr_worldscale->value * (VR_IPD / 2.0f) *
|
||||
((vr_stereo_side->integer - 0.5f) * 2.0f), 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
Matrix4x4_ConcatRotate( m, -fd->viewangles[ROLL], 1, 0, 0 );
|
||||
Matrix4x4_ConcatRotate( m, -fd->viewangles[PITCH], 0, 1, 0 );
|
||||
|
||||
@@ -912,6 +912,11 @@ static qboolean R_SpriteHasLightmap( cl_entity_t *e, int texFormat )
|
||||
R_DrawSpriteModel
|
||||
=================
|
||||
*/
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
#include <stdbool.h>
|
||||
bool VR_BillboardsLevel( void );
|
||||
#endif
|
||||
|
||||
void R_DrawSpriteModel( cl_entity_t *e )
|
||||
{
|
||||
mspriteframe_t *frame, *oldframe;
|
||||
@@ -921,6 +926,7 @@ void R_DrawSpriteModel( cl_entity_t *e )
|
||||
float angle, dot, sr, cr, flAlpha;
|
||||
float lerp = 1.0f, ilerp, scale;
|
||||
vec3_t v_forward, v_right, v_up;
|
||||
vec3_t view_right, view_up;
|
||||
vec3_t origin, color, color2 = {0.0f};
|
||||
|
||||
if( RI.params & RP_ENVVIEW )
|
||||
@@ -1024,6 +1030,21 @@ void R_DrawSpriteModel( cl_entity_t *e )
|
||||
|
||||
type = psprite->type;
|
||||
|
||||
// the way the sprite faces the view: the view's own right and up, or with the head's roll taken out
|
||||
// on the Frame, so a sprite stays upright in the world when the head tilts
|
||||
VectorCopy( RI.vright, view_right );
|
||||
VectorCopy( RI.vup, view_up );
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
if( VR_BillboardsLevel( ))
|
||||
{
|
||||
vec3_t level_angles, level_forward;
|
||||
|
||||
VectorCopy( RI.refdef.viewangles, level_angles );
|
||||
level_angles[ROLL] = 0.0f;
|
||||
AngleVectors( level_angles, level_forward, view_right, view_up );
|
||||
}
|
||||
#endif
|
||||
|
||||
// automatically roll parallel sprites if requested
|
||||
if( e->angles[ROLL] != 0.0f && type == SPR_FWD_PARALLEL )
|
||||
type = SPR_FWD_PARALLEL_ORIENTED;
|
||||
@@ -1053,14 +1074,14 @@ void R_DrawSpriteModel( cl_entity_t *e )
|
||||
SinCos( angle, &sr, &cr );
|
||||
for( i = 0; i < 3; i++ )
|
||||
{
|
||||
v_right[i] = (RI.vright[i] * cr + RI.vup[i] * sr);
|
||||
v_up[i] = RI.vright[i] * -sr + RI.vup[i] * cr;
|
||||
v_right[i] = (view_right[i] * cr + view_up[i] * sr);
|
||||
v_up[i] = view_right[i] * -sr + view_up[i] * cr;
|
||||
}
|
||||
break;
|
||||
case SPR_FWD_PARALLEL: // normal sprite
|
||||
default:
|
||||
VectorCopy( RI.vright, v_right );
|
||||
VectorCopy( RI.vup, v_up );
|
||||
VectorCopy( view_right, v_right );
|
||||
VectorCopy( view_up, v_up );
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ endif
|
||||
LOCAL_MODULE := menu
|
||||
LOCAL_CPPFLAGS := -std=gnu++11 -DMAINUI_USE_STB -DMAINUI_USE_CUSTOM_FONT_RENDER -DNO_STL -fno-rtti -fno-exceptions -DVR
|
||||
|
||||
# Valve Steam Frame build (gradle assembleFrame): the options that only it has
|
||||
ifeq ($(L1VR_STEAM_FRAME),1)
|
||||
LOCAL_CPPFLAGS += -DL1VR_STEAM_FRAME
|
||||
endif
|
||||
|
||||
LOCAL_C_INCLUDES := \
|
||||
$(SDL_PATH)/include \
|
||||
$(LOCAL_PATH)/. \
|
||||
|
||||
@@ -27,6 +27,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#include "Table.h"
|
||||
#include "Action.h"
|
||||
#include "YesNoMessageBox.h"
|
||||
#include "HostGameOptions.h"
|
||||
#include "Table.h"
|
||||
|
||||
#define ART_BANNER "gfx/shell/head_creategame"
|
||||
@@ -82,6 +83,17 @@ private:
|
||||
void _VidInit() override;
|
||||
};
|
||||
|
||||
static void NormalizeMaxClientsField( CMenuField *field, bool preserveEmpty )
|
||||
{
|
||||
const char *value = field->GetBuffer();
|
||||
if( preserveEmpty && !value[0] )
|
||||
return;
|
||||
|
||||
char normalized[4];
|
||||
snprintf( normalized, sizeof( normalized ), "%d", HostGame_NormalizeMaxPlayers( atoi( value ) ));
|
||||
field->SetBuffer( normalized );
|
||||
}
|
||||
|
||||
static CMenuCreateGame uiCreateGame;
|
||||
|
||||
/*
|
||||
@@ -124,7 +136,9 @@ void CMenuCreateGame::Begin( CMenuBaseItem *pSelf, void *pExtra )
|
||||
menu->password.WriteCvar();
|
||||
menu->hostName.WriteCvar();
|
||||
menu->hltv.WriteCvar();
|
||||
NormalizeMaxClientsField( &menu->maxClients, false );
|
||||
menu->maxClients.WriteCvar();
|
||||
const int maxPlayers = HostGame_NormalizeMaxPlayers( atoi( menu->maxClients.GetBuffer() ));
|
||||
|
||||
EngFuncs::PlayBackgroundTrack( NULL, NULL );
|
||||
|
||||
@@ -150,12 +164,12 @@ void CMenuCreateGame::Begin( CMenuBaseItem *pSelf, void *pExtra )
|
||||
EngFuncs::ClientCmd( TRUE, cmd );
|
||||
|
||||
// dirty listenserver config form old xash may rewrite maxplayers
|
||||
EngFuncs::CvarSetValue( "maxplayers", atoi( menu->maxClients.GetBuffer() ));
|
||||
EngFuncs::CvarSetValue( "maxplayers", maxPlayers );
|
||||
|
||||
Com_EscapeCommand( cmd2, mapName, 256 );
|
||||
|
||||
// hack: wait three frames allowing server to completely shutdown, reapply maxplayers and start new map
|
||||
sprintf( cmd, "endgame;menu_connectionprogress localserver;wait;wait;wait;maxplayers %i;latch;map %s\n", atoi( menu->maxClients.GetBuffer() ), cmd2 );
|
||||
sprintf( cmd, "endgame;menu_connectionprogress localserver;wait;wait;wait;maxplayers %i;latch;map %s\n", maxPlayers, cmd2 );
|
||||
EngFuncs::ClientCmd( FALSE, cmd );
|
||||
|
||||
}
|
||||
@@ -248,26 +262,17 @@ void CMenuCreateGame::_Init( void )
|
||||
maxClients.bNumbersOnly = true;
|
||||
maxClients.szName = "Max Players:";
|
||||
maxClients.LinkCvar( "maxplayers" );
|
||||
NormalizeMaxClientsField( &maxClients, false );
|
||||
SET_EVENT_MULTI( maxClients.onCvarChange,
|
||||
{
|
||||
CMenuField *self = (CMenuField*)pSelf;
|
||||
if( !self->GetBuffer()[0] )
|
||||
return;
|
||||
int players = atoi( self->GetBuffer() );
|
||||
if( players <= 1 )
|
||||
self->SetBuffer( "2" );
|
||||
else if( players > 32 )
|
||||
self->SetBuffer( "32" );
|
||||
NormalizeMaxClientsField( self, true );
|
||||
});
|
||||
|
||||
SET_EVENT_MULTI( maxClients.onLostFocus,
|
||||
{
|
||||
CMenuField *self = (CMenuField*)pSelf;
|
||||
int players = atoi( self->GetBuffer() );
|
||||
if( players <= 1 )
|
||||
self->SetBuffer( "2" );
|
||||
else if( players > 32 )
|
||||
self->SetBuffer( "32" );
|
||||
NormalizeMaxClientsField( self, false );
|
||||
});
|
||||
|
||||
password.szName = "Password:";
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#ifndef XASH_HOST_GAME_OPTIONS_H
|
||||
#define XASH_HOST_GAME_OPTIONS_H
|
||||
|
||||
// A listen server needs a slot for its local player and at least one remote player.
|
||||
static inline int HostGame_NormalizeMaxPlayers( int players )
|
||||
{
|
||||
if( players < 2 ) return 2;
|
||||
if( players > 32 ) return 32;
|
||||
return players;
|
||||
}
|
||||
|
||||
#endif // XASH_HOST_GAME_OPTIONS_H
|
||||
@@ -66,6 +66,9 @@ public:
|
||||
CMenuCheckBox vbo;
|
||||
CMenuCheckBox actionables;
|
||||
CMenuCheckBox fps;
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
CMenuCheckBox flatScreen;
|
||||
#endif
|
||||
CMenuSlider height;
|
||||
CMenuSlider vignette;
|
||||
|
||||
@@ -104,6 +107,9 @@ void CMenuVidOptions::SaveAndPopMenu( void )
|
||||
vbo.WriteCvar();
|
||||
actionables.WriteCvar();
|
||||
fps.WriteCvar();
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
flatScreen.WriteCvar();
|
||||
#endif
|
||||
height.WriteCvar();
|
||||
vignette.WriteCvar();
|
||||
// gamma is already written
|
||||
@@ -240,8 +246,19 @@ void CMenuVidOptions::_Init( void )
|
||||
fps.SetCoord( 72, 705 );
|
||||
fps.LinkCvar( "cl_showfps" );
|
||||
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
// Steam Frame build only: the game on a flat screen in front of you, it has no button
|
||||
flatScreen.SetNameAndStatus( "Flat Screen View", "Show the game on a flat screen in front of you. The controllers only point and click then" );
|
||||
flatScreen.SetCoord( 72, 755 );
|
||||
flatScreen.LinkCvar( "vr_flat_screen" );
|
||||
#endif
|
||||
|
||||
done.SetNameAndStatus( "Done", "Go back to the Video Menu" );
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
done.SetCoord( 72, 805 );
|
||||
#else
|
||||
done.SetCoord( 72, 755 );
|
||||
#endif
|
||||
done.SetPicture( PC_DONE );
|
||||
done.onActivated = VoidCb( &CMenuVidOptions::SaveAndPopMenu );
|
||||
|
||||
@@ -253,6 +270,9 @@ void CMenuVidOptions::_Init( void )
|
||||
AddItem( refresh );
|
||||
AddItem( actionables );
|
||||
AddItem( fps );
|
||||
#ifdef L1VR_STEAM_FRAME
|
||||
AddItem( flatScreen );
|
||||
#endif
|
||||
AddItem( vbo );
|
||||
AddItem( height );
|
||||
AddItem( vignette );
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
## This file must *NOT* be checked into Version Control Systems,
|
||||
# as it contains information specific to your local configuration.
|
||||
#
|
||||
# Location of the SDK. This is only used by Gradle.
|
||||
# For customization when using a Version Control System, please read the
|
||||
# header note.
|
||||
#Thu Dec 12 20:21:11 GMT 2019
|
||||
sdk.dir=C\:\\Users\\Simon\\AppData\\Local\\Android\\Sdk
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Merged over the main manifest for the frame build type (Valve Steam Frame).
|
||||
Its Android container (Lepton) doesn't advertise android.hardware.vr.headtracking,
|
||||
so the install would fail with it required. The Quest and Pico shell metadata
|
||||
doesn't do anything there either. -->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature android:name="android.hardware.vr.headtracking"
|
||||
tools:node="remove" />
|
||||
|
||||
<application>
|
||||
<meta-data android:name="com.samsung.android.vr.application.mode" tools:node="remove" />
|
||||
<meta-data android:name="com.oculus.supportedDevices" tools:node="remove" />
|
||||
<meta-data android:name="com.oculus.dualcorecpuset" tools:node="remove" />
|
||||
<meta-data android:name="com.oculus.trade_cpu_for_gpu_amount" tools:node="remove" />
|
||||
<meta-data android:name="com.oculus.vr.focusaware" tools:node="remove" />
|
||||
<meta-data android:name="pvr.app.type" tools:node="remove" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -4,10 +4,17 @@ include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_MODULE := openxr_loader
|
||||
|
||||
# The Steam Frame build passes the Khronos loader it fetched
|
||||
ifneq ($(L1VR_OPENXR_LOADER),)
|
||||
LOCAL_SRC_FILES := $(L1VR_OPENXR_LOADER)
|
||||
LOADER_FILE := $(L1VR_OPENXR_LOADER)
|
||||
else
|
||||
LOCAL_SRC_FILES := lib$(LOCAL_MODULE).so
|
||||
LOADER_FILE := $(LOCAL_PATH)/$(LOCAL_SRC_FILES)
|
||||
endif
|
||||
|
||||
# NOTE: This check is added to prevent the following error when running a "make clean" where
|
||||
# the prebuilt lib may have been deleted: "LOCAL_SRC_FILES points to a missing file"
|
||||
ifneq (,$(wildcard $(LOCAL_PATH)/$(LOCAL_SRC_FILES)))
|
||||
ifneq (,$(wildcard $(LOADER_FILE)))
|
||||
include $(PREBUILT_SHARED_LIBRARY)
|
||||
endif
|
||||
@@ -1,179 +1,38 @@
|
||||

|
||||
# Lambda1VR on Steam Frame
|
||||
|
||||
# Lambda1VR
|
||||
This is my Steam Frame build of [Team Beef's Lambda1VR](https://github.com/Team-Beef-Studios/Lambda1VR), the Xash3D-FWGS and GoldSrc-based Half-Life VR port. It runs in the Frame's Lepton Android container with ARM64, OpenXR and GLES. See the [architecture notes](docs/VR_ARCHITECTURE.md) for the input and rendering details.
|
||||
|
||||
Lambda1VR is a port / mod of the Xash3D-FWGS Half-Life Engine for the Oculus Quest adding 6DoF weapon support and OpenGLES2.0 rendering.
|
||||
**EXPERIMENTAL.** I made this for personal use. This is a source release of my Frame changes to Team Beef's existing port, with the original GPLv3 and component notices kept. See the [build and publication notes](docs/PR0_READINESS.md). The [original README](docs/README_PRE_PR0.md) is preserved.
|
||||
|
||||
```
|
||||
Xash3D is a game engine, aimed to provide compatibility with Half-Life Engine,
|
||||
as well as to give game developers well known workflow and extend it.
|
||||
Read more about Xash3D on ModDB: https://www.moddb.com/engines/xash3d-engine
|
||||
The Frame build keeps tracked head and hands, stereo rendering, Steam Frame controller bindings, a laser pointer for menus, snap or smooth turning, and a flat-screen viewing option. I've played it on my Frame and found it smooth, sharp and clean. The in-game laser sight and several upstream assets, including Barney's vest and helmet and the scope sprite, are known gaps.
|
||||
|
||||
## Half-Life data
|
||||
|
||||
You need the full retail Half-Life game data from your own copy. The app can read Half-Life installed through Steam on the Frame, or a `valve/` folder under `/sdcard/Documents/Lambda1VR/valve` (`~/Documents/Lambda1VR/valve` on SteamOS). Half-Life: Source is not compatible. Saves and settings live in the same Documents folder. The bundled `extras.pak` comes unchanged from Team Beef's public source; it doesn't replace the required retail game files.
|
||||
|
||||
## Build and install
|
||||
|
||||
Build on x86-64 Linux or macOS, or on Windows through WSL2 with an x86-64 Ubuntu distribution. These instructions
|
||||
use Bash. On Apple Silicon, install Rosetta for the pinned NDK's x86-64 host tools; Steam Frame is ARM64 and is not
|
||||
a host for this Android toolchain.
|
||||
|
||||
Clone the public Frame branch and its submodules:
|
||||
|
||||
```sh
|
||||
mkdir -p ~/src
|
||||
cd ~/src
|
||||
git clone --recurse-submodules --branch frame-dev https://github.com/astromaddie/lambda1-vr.git
|
||||
cd lambda1-vr
|
||||
```
|
||||
|
||||
### SideQuest
|
||||
The easiest way to install this on your Quest is using SideQuest, a PC desktop app designed to simplify sideloading apps and games ( even beat saber songs on quest ) on Standalone Android Headsets like Oculus Quest and Oculus Go. It supports drag and drop for installing APK files!
|
||||
Use JDK 17 and the Android SDK packages listed in the [Steam Frame guide](docs/STEAM_FRAME_VR.md). Before the first
|
||||
build, restore the release assets the repository does not track, then build the Frame variant:
|
||||
|
||||
Download the latest version of SideQuest here:
|
||||
https://github.com/the-expanse/SideQuest/releases
|
||||
|
||||
Once you have installed Lambda1VR you will need to copy the game assets from a legally owned half-life install. Details below.
|
||||
|
||||
|
||||
## IMPORTANT NOTE
|
||||
|
||||
This is solely an engine port/mod, no game assets are included whatsoever. YOU *MUST* LEGALLY OWN HALF-LIFE IF YOU WISH TO PLAY IT IN VR USING LAMBDA1VR.
|
||||
|
||||
The original half-life can be purchased on Steam: https://store.steampowered.com/app/70/HalfLife/
|
||||
|
||||
*PLEASE NOTE:* Half-life: Source is *not* compatible.
|
||||
|
||||
|
||||
## Copying the Full Game files to your Oculus Quest
|
||||
|
||||
Before attempting to start Lambda1VR you need to do the following:
|
||||
|
||||
- Install and start the application once until it shows the menu screen. At this point exit and you will find that a "xash" folder has been created on your headset.
|
||||
- Locate the install of half-life on your PC, if installed from Steam then it will be somewhere like C:/Program Files (x86)/Steam/steamApps/common/HalfLife/
|
||||
- Copy the content of the "valve" folder to the newly created "xash/valve" folder on the Quest (This will take a long time)
|
||||
- (optional) Copy the contents of the valve_hd folder into the valve folder now on your Quest (this gives you some nicer models). If you are a Mac user please read this post, otherwise you will get a crash: https://www.reddit.com/r/Lambda1VR/comments/cls57m/mac_running_halflife_with_hd_assets_on_quest/
|
||||
|
||||
|
||||
## Save Games File
|
||||
|
||||
Save game files are stored on the internal memory of your quest in the xash/valve folder. This means that if you uninstall/reinstall Lambda1VR all the saves are retained. If you change headset just make sure you copy {quest}/xash/valve/save to the new Quest.
|
||||
Any update will not affect any of your save game files.
|
||||
|
||||
|
||||
## Controls
|
||||
|
||||
Before you ask... YES!, the crowbar has to be swung in real life to smash stuff! (except in multi-player, you still have to press the trigger I'm afraid).
|
||||
|
||||
|
||||
### Left-handed people
|
||||
|
||||
Add the following cvar to your config.cfg file: vr_control_scheme "10"
|
||||
|
||||
If left handed players wish to mirror the weapons to avoid seeing the gaps in some of the weapon models then set the following cvar in config.cfg: vr_mirror_weapons "1"
|
||||
|
||||
|
||||
### Button / Controller Mappings (Primary configuration)
|
||||
|
||||
#### "BackPack"
|
||||
|
||||
You can get quick access to the crowbar by reaching behind (so the controller is obscured) and then holding down the grip trigger, this will then auto-switch to the crowbar whilst the grip trigger remains held down. As soon as you release the grip you will switch back to the weapon you had selected previously. This is an excellent way to quickly smash boxes/vents etc without the hassle of cycling through the weapon selector for the crowbar.
|
||||
|
||||
#### Mappings
|
||||
|
||||
This is the primary/default button mapping, an alternative configuration that might suit smooth turn locomotion players will be coming soon..
|
||||
|
||||
|Dominant Hand Controller||
|
||||
| --- | --- |
|
||||
|Orientation/Position|Weapon Orientation/Position|
|
||||
|Trigger|Fire Primary / Secondary|
|
||||
|Grip (Hold)|Enable secondary fire|
|
||||
|Grip (Quick Click)|Reload|
|
||||
|Click Thumbstick|Use (action)|
|
||||
|Thumbstick Left/Right|Turn|
|
||||
|Thumbstick Up/Down|Select Weapon (Fire to accept new weapon)|
|
||||
|A / X (depending on dominant hand controller)|Crouch*|
|
||||
|B / Y (depending on dominant hand controller)|Jump|
|
||||
|
||||
\* If you physically crouch in real life, then when the proper duck mechanic is triggered in-game you will see a HUD indicator in the top-right of the view
|
||||
|
||||
|Off-hand Controller||
|
||||
| --- | --- |
|
||||
|Thumbstick|Move/Strafe|
|
||||
|Menu Button|Menu|
|
||||
|X / A (depending on off-hand controller)|Flashlight on/off|
|
||||
|Y / B (depending on off-hand controller)|Toggle screen mode for cut-scenes (Show multi-player score table in multi-player games)|
|
||||
|Orientation/Position|Flashlight Orientation/Position|
|
||||
|Trigger|Hold down to run)|
|
||||
|Grip (hold)|Weapon Stabilise (hold to remain active)*|
|
||||
|Click Thumbstick|Enable/Disable Laser Aim|
|
||||
|
||||
\* Weapon stabilisation has a deadzone when the two controllers are less than 15cm from each other to prevent screwing up two-handed aiming with the Glock etc
|
||||
|
||||
## Command Line Parameters
|
||||
|
||||
It is possible to supply command line parameter to the xash engine (and the Lambda1VR wrapper) using the commandline.txt file. The following command line parameters are supported by Lambda1VR (for a list of command line params supported by the Xash3D engine please see the Xash3D documentation):
|
||||
|
||||
for example you might use:
|
||||
|
||||
```
|
||||
xash3d -log --supersampling 1.5 --msaa 2
|
||||
```sh
|
||||
bash tools/steam_frame/fetch_release_assets.sh
|
||||
cd Projects/Android
|
||||
sh ./gradlew assembleFrame
|
||||
```
|
||||
|
||||
Which will set super sampling to 1.5 and msaa to 2.
|
||||
|
||||
|Parameter|Values|Desription|
|
||||
| --- | --- | --- |
|
||||
|supersampling|0.1 upwards (default: 1.25)|Sets the super (or under) sampling value|
|
||||
|msaa|1, 2, 4 (default: 4)|Sets the MSAA anti-aliasing value|
|
||||
|cpu|1-4 (default: 2)|Sets the CPU scaler|
|
||||
|gpu|1-4 (default: 3)|Sets the GPU scaler|
|
||||
|
||||
|
||||
## Things to note / FAQs
|
||||
|
||||
* Your screen name (for multi-player) can be set in the config.cfg file
|
||||
* FPS counter can be enabled in the Video Options menu
|
||||
* Weapon recoil is disabled by default.. if you want a more authentic experience it can be enabled with the cvar: vr_weapon_recoil 1
|
||||
* If the player height is messed up. Try reseting the view by holding the home button for 2 seconds. After that go into the main menu and back into the game.
|
||||
* After changing files in xash/vale folder, often the device needs to be restarted for the game to use the changed files.
|
||||
|
||||
|
||||
## Known Issues
|
||||
|
||||
* Application crashes when you complete the Hazard Course
|
||||
* No way to enter text
|
||||
* Positional tracking doesn't really work on multi-player servers (unless you run your own server and set the following cvar: sv_accelerate 10000), it sort of works if you hold down the "run" trigger, but it isn't very good
|
||||
* Lambda1VR will crash if you haven't copied the half-life game assets to the right location, no warning, just crash
|
||||
* Complaint when you exit that the app did not shutdown properly (or crashed or something).. since you are quitting this wasn't considered serious enough to hold up the release, but will be fixed in a future update
|
||||
* The edges of some textures are still corrupting when the flashlight shines on them
|
||||
|
||||
## Future To-Dos
|
||||
|
||||
* Text entry / Virtual Keyboard - Frustratingly the Quest virtual keyboard isn't available through the native SDK :(
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
You need the following:
|
||||
|
||||
* Android Developer Studio
|
||||
* Android SDK API level 24
|
||||
* Latest Android Native Development Kit
|
||||
* Oculus Mobile SDK 1.24.0
|
||||
* The Lambda1VR folder should be below VrSamples in the extracted SDK
|
||||
* Create a local.properties file in the root of the extracted Oculus Mobile SDK that contains the ndk.dir and sdk.dir properties for where your SDK/NDK are located (see Gradle documentation regarding this)
|
||||
* To build debug you will need a _android.debug.keystore_ file placed in the following folder:
|
||||
_oculus_sdk_dir_/VrSamples/Lambda1VR/Projects/Android
|
||||
|
||||
Alternatively you can use the docker image created by BrainSlugs83 which can be found here: https://github.com/BrainSlugs83/DockerOvrSdk
|
||||
|
||||
|
||||
## Credits
|
||||
|
||||
I would like to thank the following teams and individual for making this possible:
|
||||
|
||||
* Baggyg - My long-time VR friend whose roles in this have been varied and all helpful, also the creator of excellent websites/artwork/assets for this mod and more recently becoming something of a MilkShape guru
|
||||
* The Xash3D team / Uncle Mike
|
||||
* GLE4ES without which this wouldn't have worked at all: https://github.com/ptitSeb/gl4es
|
||||
* Max Vollmer for their 2017 Half-Life VR mod, of which I used and modified the 6DoF weapons code for this mod, the original code for which can be seen here: https://github.com/maxvollmer/Half-Life-VR
|
||||
* The SideQuest team - For making it easy for people to install this
|
||||
* https://github.com/formicsapien for the weapon models that were used in the first couple of releases
|
||||
* Please go to https://www.lambda1vr.com/credits.html for a full list of credits for weapon models used in the current version
|
||||
|
||||
|
||||
## Notice of Non-Affiliation and Disclaimers
|
||||
|
||||
Lambda1VR is not affiliated, associated, authorized, endorsed by, or in any way officially connected with Valve Corporation, or any of its subsidiaries or its affiliates.
|
||||
Lambda1VR is an unofficial port of the Xash3D-FWGS engine, which was originally written by Uncle Mike as a fully compatible open-source GoldSrc engine alternative, more details on that extraordinary project can be found here: https://www.moddb.com/engines/xash3d-engine
|
||||
However this port (Lambda1VR) is not affiliated, associated, authorized, endorsed by, or in any way officially connected with the Xash3D team.
|
||||
This port was developed using the now deprecated Xash3D port found here: https://github.com/FWGS/xash3d
|
||||
|
||||
|
||||
## Licensing
|
||||
|
||||
Lambda1VR is covered by the GPL 3.0 license which is inherited from the Xash3D-FWGS project.
|
||||
The HLSDK sub-repository and associated works (which include the original 6DoF weapons code by Max Vollmer) are in a separate repository wholly covered with the Valve Corporation Half Life 1 SDK LICENSE (https://github.com/DrBeef/hlsdk-xash3d).
|
||||
The APK is `Projects/Android/build/outputs/apk/frame/lambda1vr-frame.apk`. Building creates the APK; it does not
|
||||
install or launch it. The guide covers the first upload, later updates, and your Half-Life data.
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"milestone": "PR0",
|
||||
"status": "EXPERIMENTAL",
|
||||
"publication_status": "public experimental source; no binary release",
|
||||
"audited_commit": "f3773cb04d004a8d51ae095f0fac8c383d6bbafc",
|
||||
"current_checkout_commit": "b5f1779386801d997cb1afef02bff875d80d61db",
|
||||
"branch": "frame-dev",
|
||||
"tested_commit": "b5f1779386801d997cb1afef02bff875d80d61db",
|
||||
"supported_targets": [
|
||||
"Steam Frame / Lepton Android 11 / ARM64 / SteamVR OpenXR / GLES 3.2"
|
||||
],
|
||||
"validation": {
|
||||
"source_commit": "b5f1779386801d997cb1afef02bff875d80d61db",
|
||||
"commands": [
|
||||
"bash tools/steam_frame/tests/run_tests.sh"
|
||||
],
|
||||
"result": "PASS: three binding profiles and 34 actions, LAN host options, diagnostics/BMP, eye math, pointer math, frame mapping and deploy lock safety. No device install or launch."
|
||||
},
|
||||
"full_application_build": "not run for current audited source",
|
||||
"distribution_blockers": [],
|
||||
"data_boundary": "A user-provided Half-Life installation is required. Half-Life: Source is incompatible.",
|
||||
"changelog": "CHANGELOG.md",
|
||||
"audit_date": "2026-10-11",
|
||||
"audit_details": "docs/PR0_READINESS.md",
|
||||
"distribution_notes": [
|
||||
"Root GPLv3 and original engine/SDK notices retained. Retail Half-Life data is supplied separately by the user.",
|
||||
"assets/extras.pak is unchanged inherited Team Beef material, present in the public v1.7.4 source lineage at 99a0d62. The shared Android debug key is development build material.",
|
||||
"Machine-specific local.properties files are removed from the tracked tip and ignored; historical upstream configuration remains in Git history."
|
||||
],
|
||||
"publication_date": "2026-10-11"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# PR0 — Lambda1VR on Steam Frame
|
||||
|
||||
**EXPERIMENTAL.** Source release of the Steam Frame fork of [Team Beef's Lambda1VR](https://github.com/Team-Beef-Studios/Lambda1VR). See the [README](../README.md) for features, controls, required game data and build instructions.
|
||||
|
||||
Target: Steam Frame / Lepton Android 11 / ARM64 / SteamVR OpenXR / GLES 3.2.
|
||||
|
||||
| Record | Revision |
|
||||
| --- | --- |
|
||||
| Earlier hygiene audit | `f3773cb04d004a8d51ae095f0fac8c383d6bbafc` |
|
||||
| Host-tested source | `b5f1779386801d997cb1afef02bff875d80d61db` |
|
||||
| Public upstream release lineage | Team Beef v1.7.4 at `99a0d62` |
|
||||
|
||||
`bash tools/steam_frame/tests/run_tests.sh` passed: bindings, LAN host options, diagnostics/BMP, eye and pointer math, controller mapping and deploy lock safety. No fresh full APK build was run for this publication update.
|
||||
|
||||
The root GPLv3 and engine/SDK notices are retained. `assets/extras.pak` matches inherited Team Beef source, and the shared Android debug key is development build material. Machine-specific `local.properties` files are removed from the tracked tip and ignored; the old upstream configuration remains in history.
|
||||
|
||||
Supply the retail Half-Life files from your own copy. Half-Life: Source is incompatible. The source license does not grant redistribution rights to the commercial game. No APK or game-data package is published here.
|
||||
|
||||
[Exact metadata](PR0_METADATA.json) · [Changelog](../CHANGELOG.md)
|
||||
@@ -0,0 +1,181 @@
|
||||

|
||||
|
||||
# Lambda1VR
|
||||
|
||||
Lambda1VR is a port / mod of the Xash3D-FWGS Half-Life Engine for the Oculus Quest adding 6DoF weapon support and OpenGLES2.0 rendering.
|
||||
|
||||
```
|
||||
Xash3D is a game engine, aimed to provide compatibility with Half-Life Engine,
|
||||
as well as to give game developers well known workflow and extend it.
|
||||
Read more about Xash3D on ModDB: https://www.moddb.com/engines/xash3d-engine
|
||||
```
|
||||
|
||||
There's a build for Valve's Steam Frame too, see [docs/STEAM_FRAME_VR.md](docs/STEAM_FRAME_VR.md).
|
||||
|
||||
### SideQuest
|
||||
The easiest way to install this on your Quest is using SideQuest, a PC desktop app designed to simplify sideloading apps and games ( even beat saber songs on quest ) on Standalone Android Headsets like Oculus Quest and Oculus Go. It supports drag and drop for installing APK files!
|
||||
|
||||
Download the latest version of SideQuest here:
|
||||
https://github.com/the-expanse/SideQuest/releases
|
||||
|
||||
Once you have installed Lambda1VR you will need to copy the game assets from a legally owned half-life install. Details below.
|
||||
|
||||
|
||||
## IMPORTANT NOTE
|
||||
|
||||
This is solely an engine port/mod, no game assets are included whatsoever. YOU *MUST* LEGALLY OWN HALF-LIFE IF YOU WISH TO PLAY IT IN VR USING LAMBDA1VR.
|
||||
|
||||
The original half-life can be purchased on Steam: https://store.steampowered.com/app/70/HalfLife/
|
||||
|
||||
*PLEASE NOTE:* Half-life: Source is *not* compatible.
|
||||
|
||||
|
||||
## Copying the Full Game files to your Oculus Quest
|
||||
|
||||
Before attempting to start Lambda1VR you need to do the following:
|
||||
|
||||
- Install and start the application once until it shows the menu screen. At this point exit and you will find that a "xash" folder has been created on your headset.
|
||||
- Locate the install of half-life on your PC, if installed from Steam then it will be somewhere like C:/Program Files (x86)/Steam/steamApps/common/HalfLife/
|
||||
- Copy the content of the "valve" folder to the newly created "xash/valve" folder on the Quest (This will take a long time)
|
||||
- (optional) Copy the contents of the valve_hd folder into the valve folder now on your Quest (this gives you some nicer models). If you are a Mac user please read this post, otherwise you will get a crash: https://www.reddit.com/r/Lambda1VR/comments/cls57m/mac_running_halflife_with_hd_assets_on_quest/
|
||||
|
||||
|
||||
## Save Games File
|
||||
|
||||
Save game files are stored on the internal memory of your quest in the xash/valve folder. This means that if you uninstall/reinstall Lambda1VR all the saves are retained. If you change headset just make sure you copy {quest}/xash/valve/save to the new Quest.
|
||||
Any update will not affect any of your save game files.
|
||||
|
||||
|
||||
## Controls
|
||||
|
||||
Before you ask... YES!, the crowbar has to be swung in real life to smash stuff! (except in multi-player, you still have to press the trigger I'm afraid).
|
||||
|
||||
|
||||
### Left-handed people
|
||||
|
||||
Add the following cvar to your config.cfg file: vr_control_scheme "10"
|
||||
|
||||
If left handed players wish to mirror the weapons to avoid seeing the gaps in some of the weapon models then set the following cvar in config.cfg: vr_mirror_weapons "1"
|
||||
|
||||
|
||||
### Button / Controller Mappings (Primary configuration)
|
||||
|
||||
#### "BackPack"
|
||||
|
||||
You can get quick access to the crowbar by reaching behind (so the controller is obscured) and then holding down the grip trigger, this will then auto-switch to the crowbar whilst the grip trigger remains held down. As soon as you release the grip you will switch back to the weapon you had selected previously. This is an excellent way to quickly smash boxes/vents etc without the hassle of cycling through the weapon selector for the crowbar.
|
||||
|
||||
#### Mappings
|
||||
|
||||
This is the primary/default button mapping, an alternative configuration that might suit smooth turn locomotion players will be coming soon..
|
||||
|
||||
|Dominant Hand Controller||
|
||||
| --- | --- |
|
||||
|Orientation/Position|Weapon Orientation/Position|
|
||||
|Trigger|Fire Primary / Secondary|
|
||||
|Grip (Hold)|Enable secondary fire|
|
||||
|Grip (Quick Click)|Reload|
|
||||
|Click Thumbstick|Use (action)|
|
||||
|Thumbstick Left/Right|Turn|
|
||||
|Thumbstick Up/Down|Select Weapon (Fire to accept new weapon)|
|
||||
|A / X (depending on dominant hand controller)|Crouch*|
|
||||
|B / Y (depending on dominant hand controller)|Jump|
|
||||
|
||||
\* If you physically crouch in real life, then when the proper duck mechanic is triggered in-game you will see a HUD indicator in the top-right of the view
|
||||
|
||||
|Off-hand Controller||
|
||||
| --- | --- |
|
||||
|Thumbstick|Move/Strafe|
|
||||
|Menu Button|Menu|
|
||||
|X / A (depending on off-hand controller)|Flashlight on/off|
|
||||
|Y / B (depending on off-hand controller)|Toggle screen mode for cut-scenes (Show multi-player score table in multi-player games)|
|
||||
|Orientation/Position|Flashlight Orientation/Position|
|
||||
|Trigger|Hold down to run)|
|
||||
|Grip (hold)|Weapon Stabilise (hold to remain active)*|
|
||||
|Click Thumbstick|Enable/Disable Laser Aim|
|
||||
|
||||
\* Weapon stabilisation has a deadzone when the two controllers are less than 15cm from each other to prevent screwing up two-handed aiming with the Glock etc
|
||||
|
||||
## Command Line Parameters
|
||||
|
||||
It is possible to supply command line parameter to the xash engine (and the Lambda1VR wrapper) using the commandline.txt file. The following command line parameters are supported by Lambda1VR (for a list of command line params supported by the Xash3D engine please see the Xash3D documentation):
|
||||
|
||||
for example you might use:
|
||||
|
||||
```
|
||||
xash3d -log --supersampling 1.5 --msaa 2
|
||||
```
|
||||
|
||||
Which will set super sampling to 1.5 and msaa to 2.
|
||||
|
||||
|Parameter|Values|Desription|
|
||||
| --- | --- | --- |
|
||||
|supersampling|0.1 upwards (default: 1.25)|Sets the super (or under) sampling value|
|
||||
|msaa|1, 2, 4 (default: 4)|Sets the MSAA anti-aliasing value|
|
||||
|cpu|1-4 (default: 2)|Sets the CPU scaler|
|
||||
|gpu|1-4 (default: 3)|Sets the GPU scaler|
|
||||
|
||||
|
||||
## Things to note / FAQs
|
||||
|
||||
* Your screen name (for multi-player) can be set in the config.cfg file
|
||||
* FPS counter can be enabled in the Video Options menu
|
||||
* Weapon recoil is disabled by default.. if you want a more authentic experience it can be enabled with the cvar: vr_weapon_recoil 1
|
||||
* If the player height is messed up. Try reseting the view by holding the home button for 2 seconds. After that go into the main menu and back into the game.
|
||||
* After changing files in xash/vale folder, often the device needs to be restarted for the game to use the changed files.
|
||||
|
||||
|
||||
## Known Issues
|
||||
|
||||
* Application crashes when you complete the Hazard Course
|
||||
* No way to enter text
|
||||
* Positional tracking doesn't really work on multi-player servers (unless you run your own server and set the following cvar: sv_accelerate 10000), it sort of works if you hold down the "run" trigger, but it isn't very good
|
||||
* Lambda1VR will crash if you haven't copied the half-life game assets to the right location, no warning, just crash
|
||||
* Complaint when you exit that the app did not shutdown properly (or crashed or something).. since you are quitting this wasn't considered serious enough to hold up the release, but will be fixed in a future update
|
||||
* The edges of some textures are still corrupting when the flashlight shines on them
|
||||
|
||||
## Future To-Dos
|
||||
|
||||
* Text entry / Virtual Keyboard - Frustratingly the Quest virtual keyboard isn't available through the native SDK :(
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
You need the following:
|
||||
|
||||
* Android Developer Studio
|
||||
* Android SDK API level 24
|
||||
* Latest Android Native Development Kit
|
||||
* Oculus Mobile SDK 1.24.0
|
||||
* The Lambda1VR folder should be below VrSamples in the extracted SDK
|
||||
* Create a local.properties file in the root of the extracted Oculus Mobile SDK that contains the ndk.dir and sdk.dir properties for where your SDK/NDK are located (see Gradle documentation regarding this)
|
||||
* To build debug you will need a _android.debug.keystore_ file placed in the following folder:
|
||||
_oculus_sdk_dir_/VrSamples/Lambda1VR/Projects/Android
|
||||
|
||||
Alternatively you can use the docker image created by BrainSlugs83 which can be found here: https://github.com/BrainSlugs83/DockerOvrSdk
|
||||
|
||||
|
||||
## Credits
|
||||
|
||||
I would like to thank the following teams and individual for making this possible:
|
||||
|
||||
* Baggyg - My long-time VR friend whose roles in this have been varied and all helpful, also the creator of excellent websites/artwork/assets for this mod and more recently becoming something of a MilkShape guru
|
||||
* The Xash3D team / Uncle Mike
|
||||
* GLE4ES without which this wouldn't have worked at all: https://github.com/ptitSeb/gl4es
|
||||
* Max Vollmer for their 2017 Half-Life VR mod, of which I used and modified the 6DoF weapons code for this mod, the original code for which can be seen here: https://github.com/maxvollmer/Half-Life-VR
|
||||
* The SideQuest team - For making it easy for people to install this
|
||||
* https://github.com/formicsapien for the weapon models that were used in the first couple of releases
|
||||
* Please go to https://www.lambda1vr.com/credits.html for a full list of credits for weapon models used in the current version
|
||||
|
||||
|
||||
## Notice of Non-Affiliation and Disclaimers
|
||||
|
||||
Lambda1VR is not affiliated, associated, authorized, endorsed by, or in any way officially connected with Valve Corporation, or any of its subsidiaries or its affiliates.
|
||||
Lambda1VR is an unofficial port of the Xash3D-FWGS engine, which was originally written by Uncle Mike as a fully compatible open-source GoldSrc engine alternative, more details on that extraordinary project can be found here: https://www.moddb.com/engines/xash3d-engine
|
||||
However this port (Lambda1VR) is not affiliated, associated, authorized, endorsed by, or in any way officially connected with the Xash3D team.
|
||||
This port was developed using the now deprecated Xash3D port found here: https://github.com/FWGS/xash3d
|
||||
|
||||
|
||||
## Licensing
|
||||
|
||||
Lambda1VR is covered by the GPL 3.0 license which is inherited from the Xash3D-FWGS project.
|
||||
The HLSDK sub-repository and associated works (which include the original 6DoF weapons code by Max Vollmer) are in a separate repository wholly covered with the Valve Corporation Half Life 1 SDK LICENSE (https://github.com/DrBeef/hlsdk-xash3d).
|
||||
@@ -0,0 +1,269 @@
|
||||
# Lambda1VR on the Valve Steam Frame
|
||||
|
||||
This is a Steam Frame build of Lambda1VR for the Frame's Lepton Android container. It uses ARM64, OpenXR and GLES, with Steam Frame controller bindings and a laser pointer for menus. The game keeps its head-tracked stereo view and adds a flat-screen viewing option.
|
||||
|
||||
I've played it on my Frame and found it smooth, sharp and clean. The in-game laser sight and several upstream assets remain known gaps. See the [main README](../README.md) for the game data requirements and port features.
|
||||
|
||||
## Build the APK
|
||||
|
||||
Build on x86-64 Linux or macOS, or on Windows through WSL2 with an x86-64 Ubuntu distribution. These steps use Bash.
|
||||
On Apple Silicon, install Rosetta for the pinned NDK's x86-64 host tools. Steam Frame is ARM64 and is not a host for
|
||||
this Android toolchain. Android's [NDK downloads](https://developer.android.com/ndk/downloads) list the available host
|
||||
packages.
|
||||
|
||||
### Windows with WSL2
|
||||
|
||||
Install [WSL 2 and Ubuntu](https://learn.microsoft.com/en-us/windows/wsl/install). From PowerShell run as
|
||||
Administrator, install Ubuntu with:
|
||||
|
||||
```powershell
|
||||
wsl --install -d Ubuntu
|
||||
```
|
||||
|
||||
Restart Windows if prompted, open Ubuntu, and create its Linux user. Do the build in Ubuntu, with the checkout and
|
||||
Android SDK under Linux `~` (for example `~/src`), not under `/mnt/c`. Install the Linux JDK and tools inside Ubuntu;
|
||||
a Windows JDK or Android SDK is not used by the WSL build:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y openjdk-17-jdk git curl unzip zip perl python3-venv openssh-client
|
||||
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
|
||||
export ANDROID_HOME="$HOME/Android/Sdk"
|
||||
export PATH="$JAVA_HOME/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$PATH"
|
||||
mkdir -p "$ANDROID_HOME/cmdline-tools/latest" /tmp/android-cmdline-tools
|
||||
curl -fL -o /tmp/commandlinetools-linux.zip \
|
||||
https://dl.google.com/android/repository/commandlinetools-linux-15859902_latest.zip
|
||||
unzip -q /tmp/commandlinetools-linux.zip -d /tmp/android-cmdline-tools
|
||||
mv /tmp/android-cmdline-tools/cmdline-tools/* "$ANDROID_HOME/cmdline-tools/latest/"
|
||||
```
|
||||
|
||||
The Linux command-line tools archive and `sdkmanager` are documented on Google's [Android Studio downloads](https://developer.android.com/studio)
|
||||
and [`sdkmanager` pages](https://developer.android.com/tools/sdkmanager). Keep this Ubuntu terminal open so the
|
||||
environment variables remain set.
|
||||
|
||||
Clone the public Frame branch and its submodules into the Linux home directory:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/src && cd ~/src
|
||||
git clone --recurse-submodules --branch frame-dev https://github.com/astromaddie/lambda1-vr.git
|
||||
cd lambda1-vr
|
||||
```
|
||||
|
||||
Install JDK 17, Android SDK command-line tools and Git. This branch uses Gradle 8.2.1, Android Gradle Plugin 8.2.1,
|
||||
Android SDK platform 32, build-tools 29.0.3 and NDK 25.1.8937393. On Linux/macOS only, set these variables to the
|
||||
JDK 17 and Android SDK locations for that host (skip this block if you followed the Windows/WSL setup above):
|
||||
|
||||
```bash
|
||||
export JAVA_HOME="/path/to/jdk-17"
|
||||
export ANDROID_HOME="/path/to/android-sdk"
|
||||
export PATH="$JAVA_HOME/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
|
||||
```
|
||||
|
||||
Accept the Android SDK licenses, then install the pinned packages in the same terminal:
|
||||
|
||||
```bash
|
||||
sdkmanager --sdk_root="$ANDROID_HOME" --licenses
|
||||
sdkmanager --sdk_root="$ANDROID_HOME" \
|
||||
"platform-tools" "platforms;android-32" "build-tools;29.0.3" "ndk;25.1.8937393"
|
||||
```
|
||||
|
||||
For Linux and macOS, install the command-line tools for that host from Google's [Android Studio downloads](https://developer.android.com/studio)
|
||||
page and put their `cmdline-tools` contents under `<android-sdk>/cmdline-tools/latest` before running the setup above.
|
||||
|
||||
On Windows through WSL2, clone from Ubuntu into `~/src` and run the build there. A Windows file such as
|
||||
`C:\Users\WindowsUser\Documents\valve` appears to WSL as `/mnt/c/Users/WindowsUser/Documents/valve`; replace
|
||||
`WindowsUser` with the Windows account name. The Frame game reads Half-Life files from `~/Documents/Lambda1VR/valve`
|
||||
on the headset.
|
||||
|
||||
The first build needs the release assets that are not stored in Git. From the repository root, make sure `curl`,
|
||||
`unzip` and `shasum` are available, then run the checked asset-fetch script and build the Frame variant:
|
||||
|
||||
```bash
|
||||
bash tools/steam_frame/fetch_release_assets.sh
|
||||
cd Projects/Android
|
||||
sh ./gradlew assembleFrame
|
||||
```
|
||||
|
||||
The script downloads Team Beef's public v1.7.4 release APK, verifies its SHA-256, and restores missing ignored assets.
|
||||
The build also downloads and verifies the Khronos OpenXR loader. Both downloads need an internet connection. The APK
|
||||
is `Projects/Android/build/outputs/apk/frame/lambda1vr-frame.apk` and uses the debug key.
|
||||
|
||||
Running the checked asset-fetch script before Gradle is required for a source build. The restored files are bundled
|
||||
into the APK; on first launch, the app copies `extras.pak` to its private files directory and copies the bundled
|
||||
replacement models, sounds and menu images into the Frame's `~/Documents/Lambda1VR` data folder. You do not need to
|
||||
copy these release assets to the Frame separately.
|
||||
|
||||
The Frame menu tile is generated when the release background image and Python 3 with Pillow and NumPy are available.
|
||||
To install those optional modules, run `python3 -m venv tools/steam_frame/.venv` and
|
||||
`tools/steam_frame/.venv/bin/pip install pillow numpy` from the repository root. Without the image or modules, the
|
||||
build still completes with the release tile and its Quest button hints.
|
||||
|
||||
## Install and run
|
||||
|
||||
If Steam's Devkit Client is running on Windows, copy the built APK from WSL to a Windows folder so the client can
|
||||
browse it. From the repository root in Ubuntu:
|
||||
|
||||
```bash
|
||||
windows_user="$(cmd.exe /C echo %USERNAME% | tr -d '\r')"
|
||||
mkdir -p "/mnt/c/Users/$windows_user/Downloads/Lambda1VR"
|
||||
cp Projects/Android/build/outputs/apk/frame/lambda1vr-frame.apk \
|
||||
"/mnt/c/Users/$windows_user/Downloads/Lambda1VR/"
|
||||
```
|
||||
|
||||
Use Valve's [SteamOS Devkit Client](https://partner.steamgames.com/doc/steamhardware/steamframe/loadgames) from Steam
|
||||
on your desktop. Pair it once with the Frame: enable Developer Mode, choose **Settings > Developer > Pair new host**,
|
||||
then register the Frame in the client's **Devkits** tab and confirm pairing on the headset.
|
||||
|
||||
For a first install, open **Devkits > Connect to devkit by IP**, choose **Title Upload**, and set:
|
||||
|
||||
- **Name:** `Lambda1VR`
|
||||
- **Local Folder:** The folder containing the APK: the Windows Downloads folder copied above on WSL, or
|
||||
`Projects/Android/build/outputs/apk/frame` on Linux/macOS.
|
||||
- **Start Command:** `lambda1vr-frame.apk`
|
||||
- **Runtime:** `Android`
|
||||
|
||||
Upload and start the title. It then appears in the Frame's Steam library under **Library > Non-Steam > Devkit Game**.
|
||||
For later builds, upload again with the same title name to update that library entry. Steam launches provide the
|
||||
Frame controller input.
|
||||
|
||||
## Game data
|
||||
|
||||
Use the full retail Half-Life game data from your own copy; **Half-Life: Source is not compatible**. The app can read
|
||||
Half-Life installed through Steam on the Frame, or a `valve/` folder under `~/Documents/Lambda1VR/valve` on the Frame.
|
||||
Saves, settings and the files restored from the v1.7.4 release assets live under `~/Documents/Lambda1VR`.
|
||||
|
||||
If you do not have Half-Life installed through Steam on the Frame, copy the retail `valve/` folder from your own
|
||||
Half-Life installation. Enable Developer Mode and set the SteamOS password using Valve's [Steam Frame setup guide](https://partner.steamgames.com/doc/steamhardware/steamframe/setup).
|
||||
Valve's [debugging guide](https://partner.steamgames.com/doc/steamhardware/steamframe/debugging) documents SSH as
|
||||
`steamos@frame`. Set `FRAME_HOST` to `frame` or the Frame's IP address; enter the password you set when SSH asks.
|
||||
These commands use the host's OpenSSH `ssh` and `scp` clients; install `openssh-client` on Ubuntu (included in the
|
||||
WSL setup above).
|
||||
For WSL, a `valve/` folder under Windows Documents appears at `/mnt/c/Users/WindowsUser/Documents/valve`; replace
|
||||
`WindowsUser` with your Windows account name. From the build computer, run:
|
||||
|
||||
```bash
|
||||
export FRAME_HOST=frame # replace with the Frame's IP if its hostname does not resolve
|
||||
VALVE_DIR="/mnt/c/Users/WindowsUser/Documents/valve" # use the local folder path on Linux/macOS
|
||||
ssh "steamos@$FRAME_HOST" 'mkdir -p /home/steamos/Documents/Lambda1VR/valve'
|
||||
scp -r "$VALVE_DIR"/. "steamos@$FRAME_HOST:/home/steamos/Documents/Lambda1VR/valve/"
|
||||
```
|
||||
|
||||
If Half-Life is already installed through Steam on the Frame, no game-data copy is needed.
|
||||
|
||||
## Quest and Frame LAN multiplayer
|
||||
|
||||
Use compatible Lambda1VR builds from the same source revision, with matching Half-Life game data, and put
|
||||
both headsets on the same local network. The LAN browser uses local UDP discovery and direct connections;
|
||||
it does not need an Internet server, NAT bypass, or an IP address typed on-head. A guest Wi-Fi network with
|
||||
client isolation can block discovery and direct play.
|
||||
|
||||
On the host, open **Multiplayer > LAN game > Create game**, choose a map, then select **Ok**. The player
|
||||
count starts at 2 so the listen server has a slot for its local player and one joining player. The headset
|
||||
that starts the map is player 1. On the other headset, open **Multiplayer > LAN game**, select the host in
|
||||
the server list, and choose **Join game**. Use **Refresh** if the host does not appear.
|
||||
|
||||
Keep a Frame host active during play. In the Frame build, headset standby (`shouldRender=false`) currently
|
||||
skips the engine frame, including the LAN server tick. A joining client can remain connected if the host
|
||||
resumes within its 60-second `cl_timeout`; after that the client reports a timeout and must rejoin from the
|
||||
LAN list. The server frees a silent client after its 125-second `sv_timeout` when ticking resumes. The Quest
|
||||
build continues its engine frame while not rendering, so its current standby behavior differs.
|
||||
|
||||
## 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 / down | Nothing in the game |
|
||||
| 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 left / right | Previous / next weapon, same as the bumpers |
|
||||
| D-pad up | Last weapon |
|
||||
| D-pad down | Laser sight, and steadies a scope |
|
||||
| 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 right stick click is quick save and View is quick load, like the
|
||||
two buttons on a Quest controller. Walk (the slow key) is gone, the stick is analog.
|
||||
|
||||
The right stick is only for turning. Pushing it up or down does nothing in the game, so you can't hit the
|
||||
laser sight or anything else by accident while you turn. The crowbar is the weapon hand's grip behind your
|
||||
head, there's no button for it.
|
||||
|
||||
There's no button for the flat screen view either. It's a setting now: Pause, Configuration, Video, Video
|
||||
options, "Flat Screen View" (the `vr_flat_screen` cvar, off by default, and only in the Frame build). It
|
||||
puts the game on a flat screen in front of you instead of around you. The controllers only point and click
|
||||
in that mode, like in a menu, so it's for watching and not for playing. Menu still opens the pause menu, so
|
||||
you can turn it off again from there.
|
||||
|
||||
| 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 |
|
||||
|
||||
|
||||
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.
|
||||
|
||||
## Display settings and performance
|
||||
|
||||
These are the build defaults and the display settings I prefer on the Frame.
|
||||
|
||||
- Refresh is 120 Hz. That comes from SteamVR, from the per-app setting for the game in the headset's
|
||||
SteamVR settings. The game's own `vr_refresh_rate` (default 90) only picks from the rates SteamVR
|
||||
offers, and it offers just the one set for the app, so the app can't push it higher. It only counts when
|
||||
the game is started from the Steam library.
|
||||
- Each eye is 2160 x 2160. That's the 1728 SteamVR recommends times `vr_resolution_scale` (1.25), which is
|
||||
the panel's own size. There's no extra supersampling on top of that.
|
||||
- The eye images are sRGB (`GL_SRGB8_ALPHA8`), one swapchain per eye, three images each. No multiview.
|
||||
- No MSAA. It's one sample, plain GLES 3 framebuffers. The engine's `gl_msaa` is 0 too.
|
||||
- Depth is a 24 bit renderbuffer, no stencil, and it isn't handed to SteamVR as a depth layer.
|
||||
- No foveation.
|
||||
- The game's GL comes from gl4es, using its GLES 2.0 backend on top of a GLES 3 context.
|
||||
- Textures are trilinear (`gl_texturemode GL_LINEAR_MIPMAP_LINEAR`) with 8x anisotropic filtering
|
||||
(`gl_anisotropy 8`), no LOD bias, no picmip.
|
||||
- The shipped `config.cfg` has `gamma 2.0` and `brightness 0.8`. My own copy on the headset had `gamma 2.5`,
|
||||
so that's what I was looking at.
|
||||
|
||||
In six recorded runs on two saves, with the Frame display in standby, the game held 120 fps at 120 Hz and
|
||||
2160x2160 per eye with no missed frames. The game thread used about 3.1 ms of the 8.33 ms frame period. The
|
||||
Adreno ran at 903 MHz and was about 60% busy at 1728 pixels per eye and fully busy at 2160. One run each on a
|
||||
busy save also recorded 120 fps at 2592 and 3024 pixels per eye.
|
||||
@@ -0,0 +1,306 @@
|
||||
# Lambda1VR on the Steam Frame: how it fits together
|
||||
|
||||
This document describes the Frame build (`assembleFrame`, `L1VR_STEAM_FRAME`). The public setup and controls are in [STEAM_FRAME_VR.md](STEAM_FRAME_VR.md); this file maps the rendering, input and Android build changes. Known gameplay and asset gaps are called out where they occur below.
|
||||
|
||||
## 1. Target and probe facts
|
||||
|
||||
| Item | Value |
|
||||
| --- | --- |
|
||||
| Device | Valve Steam Frame, Snapdragon 8 Gen 3, Adreno 750 |
|
||||
| Container | Lepton: Android 11 (API 30), arm64-v8a, GLES 3.2 through Zink on Turnip |
|
||||
| Runtime | SteamVR OpenXR 2.17.10 at `/vendor/etc/openxr/1/active_runtime.json`; no loader broker, so the Khronos loader 1.1.63 is in the APK |
|
||||
| Recommended eye size | 1728x1728, asymmetric FOV (left eye about L -1.028, R 0.885, U 0.864, D -1.048 rad) |
|
||||
| Refresh | the rate set for the app in SteamVR (72 Hz by default; 120 for the Lambda1VR Steam title, `steam.app.4086534518` in steamvr.vrsettings); `XR_FB_display_refresh_rate` offers only 90 and the app can't ask for more. Only a Steam title launch gets the per-app rate |
|
||||
| Controllers | `/interaction_profiles/valve/frame_controller_valve` (`XR_VALVE_frame_controller_interaction`) |
|
||||
| GL | `GL_OVR_multiview(2)` are advertised, not used: one swapchain per eye, sRGB (`GL_SRGB8_ALPHA8`), plain GLES 3 framebuffers |
|
||||
| Game libs | `libxash.so` (engine plus the VR layer), `libgl4es.so` (GL on top of GLES), `libmenu.so`, `libclient*.so`, `libserver*.so` |
|
||||
| Data | `/sdcard/Documents/Lambda1VR` (the headset's `~/Documents/Lambda1VR`) |
|
||||
| Package | `com.drbeef.lambda1vr.frame`, minSdk 30 |
|
||||
|
||||
Measured 2026-10-04 (unattended): SteamVR/OpenXR 2.17.10 offers 48 instance extensions, not `XR_EXT_performance_settings`;
|
||||
swapchain formats 0x8c41, 0x8c43 (sRGB8_ALPHA8, used), depth16/24/32F; recommended 1728x1728; the runtime listed 90 Hz but the
|
||||
frame period is 13.89 ms (72 Hz); Frame, Index and Touch suggestions all `XR_SUCCESS`; gl4es reports "GLES 2.0 backend", Mesa.
|
||||
|
||||
Two things that bit: the plain `gl*` names in `libxash.so` resolve to gl4es (it's first in NEEDED), and gl4es doesn't know
|
||||
framebuffers or textures the driver made. So swapchain attachments are set with `eglGetProcAddress` functions, but the
|
||||
framebuffers are created and bound through gl4es (it refuses to bind ones it didn't make). A readback from the eye
|
||||
framebuffer has to use the driver's `glReadPixels` too. `jVM` was never assigned, which made the quit path fall off the end of
|
||||
`AppThreadFunction` (fixed).
|
||||
|
||||
## 2. Frame protocol (as found in `TBXR_Common.c` and `L1VR_SurfaceView.c`)
|
||||
|
||||
One thread (`AppThreadFunction`) owns the EGL context, the OpenXR session and the engine.
|
||||
|
||||
1. `TBXR_InitialiseOpenXR`: EGL context (pbuffer), `xrInitializeLoaderKHR`, instance (Frame: only the
|
||||
extensions the runtime lists), system, GL requirements, view configuration, eye size.
|
||||
2. `Host_Main` (engine init), `VR_Init` (cvars), `TBXR_EnterVR` (session, head-space), `TBXR_InitRenderer`
|
||||
(reference spaces, swapchains), `TBXR_InitActions` (actions, bindings, attach), `TBXR_WaitForSessionActive`.
|
||||
3. Each XR frame:
|
||||
- `TBXR_FrameSetup`: events and session state, `xrWaitFrame`, `xrBeginFrame`, `xrLocateViews`
|
||||
(`vrFOV`, the per-eye projections), head pose, controllers (`xrSyncActions`), haptics.
|
||||
- Frame build: if `shouldRender` is false the frame goes in with zero layers and nothing else runs.
|
||||
- `Host_BeginFrame`: the whole server frame, input, timing. Once per XR frame.
|
||||
- for each eye: acquire the swapchain image, set `vr_stereo_side` (0/1, or 2/3 for the mono scope eyes),
|
||||
`Host_Frame` (the client frame and the render of that eye), comfort-mask and border clears, release.
|
||||
- `Host_EndFrame`, then `xrEndFrame` with a projection layer (both swapchains) or, when
|
||||
`VR_UseScreenLayer()`, a 5 x 4.5 m quad layer 4.5 m away that shows eye 0 only.
|
||||
4. Frame build: each eye is drawn from the located eye pose in head space (`VR_GetEyeTransform`, position scaled by
|
||||
`vr_worldscale`, plus the eye's rotation if any) and its projection view is submitted with the head pose placed by
|
||||
that eye pose (`VrEye_ComposePose`), both from the same `xrLocateViews` at the predicted time. Mono scope eyes and the
|
||||
screen layer draw from the middle and submit the head pose. (Quest build: fixed 65 mm times `vr_worldscale`, head pose
|
||||
for both views.)
|
||||
5. Exit: `runStatus == 0` leaves the loop: `Host_Shutdown`, `TBXR_LeaveVR`, Java `shutdown()` (`System.exit`).
|
||||
The Frame build also sets it on `EXITING`, `LOSS_PENDING` and instance loss.
|
||||
|
||||
Session state is logged with `[openxr] session state: X`. `FOCUSED` and `VISIBLE` set flags on `ovrApp` that
|
||||
nothing reads. Input while unfocused is empty (the runtime gives nothing); the game isn't paused on focus loss.
|
||||
|
||||
Spaces: `CurrentSpace` is STAGE when the runtime has it (else LOCAL minus 1.675 m). Recentre (Frame build,
|
||||
View held 1 s) moves our offset yaw and floor position, keeps the game's view yaw (`snapTurn` takes the difference)
|
||||
and the world position, so nothing jumps.
|
||||
|
||||
## 3. Camera-relative and screen-space draw inventory
|
||||
|
||||
How the engine draws what isn't plain world geometry, and the rule each one has now. From reading the code,
|
||||
nothing run. Paths are under `Projects/Android/jni/src` (`E/` = `Xash3D/xash3d/engine`).
|
||||
|
||||
Common rules: the eye is `vr_stereo_side`; `R_SetupModelviewMatrix` (`E/client/gl_rmain.c`) shifts by
|
||||
`vr_worldscale * VR_IPD / 2` (a fixed 0.065 IPD; the Frame build uses the runtime's eye poses instead); the projection is the headset's
|
||||
asymmetric per-eye FOV (`VR_GetVRProjection`); 2D content gets `R_Get2DOffset` (`E/client/gl_draw.c`), the pixel
|
||||
shift from the optical axis; `GetStereoDepthOffset` adds +-width/36 per eye for HUD items (two copies, `E/client/cl_scrn.c`
|
||||
and the client DLL's `hud.cpp`).
|
||||
|
||||
| Category | Where | Rule it uses now | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| HUD | `E/client/cl_view.c` `V_PostRender`, `cl_game.c` `CL_DrawHUD` / `CL_RedrawHUD`, client DLL | Quest: 2D ortho with the axis shift, most elements add the DLL's own width/36 per eye (about 0.4 m on the Frame's lenses). Frame build: the DLL is told the eyes are mono while it draws (no shift), and the whole HUD pass (centre print too) is shifted in `R_Get2DOffset` by `VrEye_HudShiftPixels` for `vr_hud_distance` (default 1 m) | hud_scale 1.3; messages, scoreboard and VGUI draw outside that pass and stay at infinity; scope and the screen layer unchanged; the HUD redraws once per eye |
|
||||
| Sprites | `E/client/gl_sprite.c`, `gl_rpart.c`, `gl_beams.c` | oriented with the shared view vectors, positions from the cyclopean `RI.vieworg`; eye shift is in the modelview | fine in stereo; glow sprites test occlusion from the middle eye |
|
||||
| Crosshair | `E/client/cl_game.c` `CL_DrawCrosshair` | only in a scope: centre of the 2D space (the optical axis), rotated by hand and head roll, mono | outside a scope the client DLL forces `crosshair 0` |
|
||||
| Laser sight | server DLL (`weapons.cpp`, `rpg.cpp`) | a beam entity or a glow spot, both world entities | follows the weapon's aim vector, not the controller ray |
|
||||
| Comfort vignette | `cl_game.c` `CL_DrawVignette`, mask in `L1VR_SurfaceView.c` | textured quad and scissor mask, both centred on the optical axis | three separate level statics each step per eye; the mask isn't gated on the screen layer |
|
||||
| Scope overlay | `cl_game.c` `CL_DrawScope` | mono eyes (2/3), centred | three different "scope engaged" thresholds (`vrFOV`, 85, 85) |
|
||||
| Menus and console | `E/client/cl_view.c`, `cl_menu.c`, `mainui/` | 2D mode on eye 0, shown on the screen-layer quad; symmetric projection, no axis shift | the quad is at y = 1.0 in stage space, 4.5 m along `playerYaw` |
|
||||
| Pointer | `E/client/touch.c`, `VrInputCommon.c` | angle based: `-sin(yaw - playerYaw)`, `pitch / 90` from the controller angles | not a ray against the quad; no ray is drawn |
|
||||
| Skybox | `E/client/gl_warp.c` `R_DrawSkyBox` | cube of half-size `farClip / 2` around `RI.cullorigin` (middle eye) | effectively at infinity; fine in stereo |
|
||||
| View weapon, hands, torch | `E/client/gl_studio.c`, `cl_view.c`, client DLL `vr_helper.cpp` | world positions: `vieworg + weapon.org`, controller offset scaled by `vr_worldscale`; depth range squeezed to 0.3 | origin computed on the left eye and reused; hidden in a scope |
|
||||
| Fog | `E/client/gl_rmain.c`, `gl_rsurf.c`, `cl_game.c` | GL fog in eye space | the sky isn't fogged unless custom fog |
|
||||
| Mirrors | `E/client/gl_mirror.c` | reflect the middle-eye view, then the normal eye projection | mirror viewport clamped to 96-1024 x 72-768, so the aspect is off; copies the eye framebuffer |
|
||||
| Culling | `E/client/gl_rmain.c` `R_SetupFrustum`, `gl_cull.c` | symmetric frustum from `vrFOV` (int degrees) about the middle eye | can be narrower than the real per-eye FOV; vertical edge pop-in possible |
|
||||
| Screen fades, cinematics | `cl_game.c`, `cl_video.c` | fades overdraw the shifted ortho; cinematics go on the screen layer | |
|
||||
| Shake, strobe | client DLL `view.cpp`, `gl_rstrobe.c` | shake compiled out under `VR`; strobe off | |
|
||||
|
||||
Frame split inside the engine: `Host_BeginFrame` runs the server and input once per XR frame; `Host_Frame` runs per eye
|
||||
and only does the client frame. Per-frame client state runs on the left eye (`cl.time`, `CL_ReadPackets`, `CL_AddEntities`,
|
||||
`V_SetupRefDef`, `pfnCalcRefdef`) or the right eye (`S_RenderFrame`, `CL_DecayLights`, cinematics, console).
|
||||
|
||||
## 4. Controls
|
||||
|
||||
Source of the layout: Half-Life's own gamepad layout from the 25th anniversary update (`valve/controller_configs/xbox_controller_config_standard.vdf`),
|
||||
with the VR parts added (grips, head look). It replaced the first Frame map (the Quest layout moved around) after the second wear, and replaces
|
||||
`standard.md` sections 1 and 2 where they differ: B is crouch (not back), A jump, X use, Y reload, bumpers and D-pad pick weapons, right stick click is the
|
||||
flashlight. Menu and View kept their system jobs (Menu pause, View recentre) as in the standard.
|
||||
|
||||
Code: `VrFrameMap.c` is the map as one table (`rows[]`: physical input, context, action); `VrInputFrame.c` turns it into the Quest-shaped states that
|
||||
`VrInputDefault.c` reads (dominant hand: trigger, backpack grip, turn stick, use on the stick-click bit, crouch and jump on its two buttons; other hand: steady on
|
||||
the grip, move stick, torch and quick load (hand behind the head) on its two buttons, laser on its stick-click bit) and into commands for what the scheme code doesn't have
|
||||
(alt fire, reload, weapon select, last weapon, scoreboard, quick save, menu keys, recentre). `OpenXrInput.c` reads the physical buttons (new `dpad*` actions) and logs the
|
||||
profile. It only runs when the runtime reports the Frame profile active (or `diag_press` is in use). Contexts: menu (`cls.key_dest != key_game`), weapon hand
|
||||
behind the head, other hand behind the head (not in multiplayer), multiplayer, and the other grip's mode (decided at press time).
|
||||
|
||||
Scheme code kept as it was: aim from the controller, crowbar swing (weapon velocity), flashlight beam from the off hand, two-handed hold and scope (`vr_weapon_stabilised`,
|
||||
`+alt1`), backpack grab, physical ducking (`vr_enable_crouching`), room-scale walking, haptics, usable-object highlight. Server side (`hlsdk-xash3d/dlls/player.cpp`,
|
||||
`PlayerUse`): `+use` finds usable things within `PLAYER_HAND_SEARCH_RADIUS` of the *weapon hand's* position in front of the hand's direction (64 units; 24 and a wide cone
|
||||
when `vr_gesture_triggered_use` is 1), and `+use2` does the same from the off hand but only when that cvar is 1. So hand-origin use already exists and needs no DLL change; the
|
||||
grips need the cvar at 1 and the client's automatic gesture use switched off (`VR_FrameHandUse()`, `VrInputDefault.c`). The reach for `+use` is therefore 24 units (0.6 m), which also
|
||||
applies to X.
|
||||
|
||||
Changed defaults on the Frame (saved settings are updated once, `vr_frame_map`): `vr_quick_crouchjump` 0 (double clicking A would be a second crouch input), `vr_gesture_triggered_use` 1 (see above).
|
||||
Not carried: walk/slow (`+speed`, was the off trigger; the stick is analog), weapon select on the turn stick (it fought the turn).
|
||||
|
||||
Not kept from the standard: dominant-hand setting beyond `vr_control_scheme`.
|
||||
|
||||
Changed after the second wear (the user's call): the right stick's up and down are unbound in the game (they still scroll in a menu), so turning can't toggle anything. D-pad
|
||||
left / right are previous / next weapon (aliases of the bumpers), up is the last weapon, down the laser sight (what right stick down did). The D-pad crowbar is gone, the crowbar is
|
||||
the weapon hand's grip behind the head. The flat screen view has no button any more: it is `vr_flat_screen` (Frame build only), a "Flat Screen View" checkbox on the Video options
|
||||
page (`mainui/menus/VideoOptions.cpp`, compiled with `-DL1VR_STEAM_FRAME` for the Frame only). `VR_UseScreenLayer()` returns true while it is set, which is the same screen layer the
|
||||
Quest's toggle shows: the controllers point and click, the scheme code's gameplay half doesn't run, Menu still pauses. Quick load had been on right stick up with the other hand
|
||||
behind the head; it is View now (right stick click stays quick save), the two buttons a Quest controller has. In multiplayer View toggles the scoreboard only (`+showscores`,
|
||||
sent from `VrInputFrame.c`), where it used to toggle the flat screen view as well.
|
||||
|
||||
The menu tile: `tools/steam_frame/make_frame_tile.py` (Pillow and numpy) reads the release tile `assets/background/800_1_a_loading.tga`, inpaints the Quest help off it and draws the
|
||||
Frame's, into `Projects/Android/build/frame-assets/background/`. `build.gradle`'s `makeFrameTile` runs it before `mergeFrameAssets` and the `frame` source set adds that folder, so the
|
||||
Frame APK's tile overrides the release one. A missing tile or module is a warning and the release tile. Not committed, Quest and Pico builds don't see it.
|
||||
|
||||
The table, generated by `tools/steam_frame/tests/test_frame_map --markdown`:
|
||||
|
||||
| Frame input | Playing | Notes |
|
||||
| --- | --- | --- |
|
||||
| weapon hand trigger | fire | weapon hand trigger |
|
||||
| other hand trigger | alt fire | other hand trigger |
|
||||
| A | jump | |
|
||||
| B | crouch (hold) | |
|
||||
| X | use | |
|
||||
| Y | reload | |
|
||||
| left stick click | crouch (toggle) | |
|
||||
| right stick click | quick save | off hand behind the head |
|
||||
| right stick click | flashlight | |
|
||||
| View (short) | quick load | off hand behind the head |
|
||||
| View (short) | scoreboard | multiplayer |
|
||||
| View (short) | flashlight | |
|
||||
| left stick up | move | move |
|
||||
| left stick down | move | move |
|
||||
| left stick left | move | move |
|
||||
| left stick right | move | move |
|
||||
| right stick left | turn | snap or smooth turn |
|
||||
| right stick right | turn | snap or smooth turn |
|
||||
| left bumper | previous weapon | |
|
||||
| right bumper | next weapon | |
|
||||
| D-pad left | previous weapon | alias of the left bumper |
|
||||
| D-pad right | next weapon | alias of the right bumper |
|
||||
| D-pad up | last weapon | |
|
||||
| D-pad down | laser sight / scope steady | laser sight, steadies a scope |
|
||||
| weapon hand grip | backpack weapon | the crowbar, from behind the head |
|
||||
| weapon hand grip | use | alias of X: grab, push, pull, press |
|
||||
| other hand grip | hold the gun steady | two-handed hold, scope: near the weapon |
|
||||
| other hand grip | use (other hand) | grab with this hand: well away from the weapon |
|
||||
| Menu (short) | pause | |
|
||||
| Menu (hold 0.5 s) | quick save | held for half a second |
|
||||
| View (hold 1 s) | recentre | |
|
||||
| View (hold 3 s) | recentre and set height | |
|
||||
|
||||
| Frame input | In a menu | Notes |
|
||||
| --- | --- | --- |
|
||||
| weapon hand trigger | menu click | pointer click |
|
||||
| A | menu confirm | |
|
||||
| B | menu back | |
|
||||
| Menu (short) | menu back | alias of B |
|
||||
| View (short) | menu back | alias of B |
|
||||
| D-pad up | menu up | |
|
||||
| D-pad down | menu down | |
|
||||
| D-pad left | menu left | |
|
||||
| D-pad right | menu right | |
|
||||
| left stick up | menu up | alias of the D-pad |
|
||||
| left stick down | menu down | alias of the D-pad |
|
||||
| left stick left | menu left | alias of the D-pad |
|
||||
| left stick right | menu right | alias of the D-pad |
|
||||
| left bumper | menu previous tab | previous tab or page |
|
||||
| right bumper | menu next tab | next tab or page |
|
||||
| right stick up | menu scroll up | scroll |
|
||||
| right stick down | menu scroll down | scroll |
|
||||
|
||||
Aliases (one action, two inputs): use on X and the weapon hand's grip; previous / next weapon on the bumpers and D-pad left / right; flashlight on the right stick click and View;
|
||||
quick save on Menu held and (hand behind the head) the right stick click; menu back on B, Menu and View; menu up/down/left/right on the D-pad and left stick. The host
|
||||
test fails on any other action with two inputs, on any input doing two things in one context, and on an action nothing reaches.
|
||||
|
||||
## 5. Diagnostics (Frame build, `vr-diag.ini` in the data folder)
|
||||
|
||||
Set with `tools/steam_frame/deploy.sh --diag KEY=VAL`, removed with `--diag-reset`. No file, no effect.
|
||||
|
||||
| Key | Effect |
|
||||
| --- | --- |
|
||||
| `force_render` | draw and run the game while `shouldRender` is false, synthetic head at 1.6 m, hands parked in front of the chest, FOV and 64 mm eye offsets when the runtime gives none |
|
||||
| `diag_yaw` | synthetic head yaw in degrees (turning left is positive); the hands follow |
|
||||
| `autostart`, `map` | load a map without the menus after 150 rendered frames (`map` implies `autostart`; default `c1a0`) |
|
||||
| `dump_frame` | on rendered frame N: `vr-dumps/vr-eye0.bmp`, `vr-eye1.bmp` (one synchronous readback each, with the driver's GL) and `vr-state.txt` (poses, FOVs, each eye's pose in head space, connection state, and 80 `probe` lines: rays from the left eye through a pixel grid traced into the world, with depth and the disparity a correct pair should show; `tools/steam_frame/analyze_stereo.py` matches the right eye there and compares). The log gets the driver's framebuffer bindings, viewport and scissor and the percentage of pixels that aren't black |
|
||||
| `diag_roll` | synthetic head roll in degrees (tilting left is positive); with `force_render`. The HUD should stay level with the world |
|
||||
| `diag_press` | holds buttons down: `A@300+30, LG@400+20` is A from 300 frames after the map starts for 30 frames, then the left grip (names: LT RT LG RG LB RB LC RC A B X Y MENU MENUHOLD VIEW UP DOWN LEFT RIGHT, LS/RS + UP DOWN LEFT RIGHT). Runs the map with no controllers; each press is logged `[vr] input -> action` |
|
||||
| `diag_gap` | metres between the synthetic hands (default 0.5), for the two-handed hold (under 0.35) against the grab (over 0.55) |
|
||||
| `cmd` | console commands (`;` between them) about 300 rendered frames after the map starts: `sv_cheats 1; impulse 101` for the suit and weapons, `vr_hud_distance 2`, `vr_hand_model 0` |
|
||||
| `probe` | right before the dump, clear each eye to a flat colour (red, blue) instead: tests the readback and the swapchain, not the game |
|
||||
| `diag_hand_yaw`, `diag_hand_pitch` | turn the synthetic hands' aim from the head's (degrees), to point the menu laser. `vr-state.txt` then has `pointer`, `hand_aim_pose`, `beam_origin`, `beam_end`, `layer_order` and each layer's pose; `tools/steam_frame/composite_menu.py DUMP OUT.png` composites them over the menu image |
|
||||
| `load`, `exit_seconds` | `load` = a save name to `load` instead of a map after the start (`save000`); `exit_seconds` = leave through the normal shutdown N seconds after the first frame. `tools/steam_frame/perf_run.sh` uses both |
|
||||
| `exit_after` | after N XR frames, leave through the normal shutdown |
|
||||
| `perf` | `[vr-perf]` every 10 s, on by default when the file exists: fps, missed frames (period over 1.5x the display period), cpu ms from after the wait to after the submit, and the connection state (`game=active` once a map runs) |
|
||||
| `oneshot` | rename the file to `vr-diag.ini.last` when it has been read |
|
||||
|
||||
Log prefixes: `[openxr]` (setup and state), `[vr-diag]` (what was read, autostart, exit), `[vr-dump]`, `[vr-perf]`, and the engine's own
|
||||
messages under the tag `Xash` (developer level 3 by default on this build).
|
||||
|
||||
Eye geometry measured on the Frame (flags 0x3, valid): eye 0 at (-34.5, 0, 0) mm, eye 1 at (+34.5, 0, 0) mm, rotation 0 yaw / 0 pitch / 0 roll for
|
||||
both, so 68.9 mm apart (the headset's IPD setting), fovs L -1.028 R 0.885 U 0.864 D -1.048 and L -0.877 R 1.033 U 0.866 D -1.042 rad. The engine's
|
||||
fixed 65 mm was 6% short, and there is no cant. Ground-truth check on the eye images (after the fix): wall 10.2 m away 4.0 px measured against
|
||||
4.05 predicted (11 probes within 0.2 px), 58.6 m away 0.1 against 0.7, walls at 2.4 to 4 m within 0.6 px, a floor point at 1.06 m 39.5
|
||||
against 38.9 px; floor texture and blurred near floors don't match reliably (spread up to 8 px), which is a limit of the block matching,
|
||||
not the render. HUD (`c1a0` with `impulse 101`): 41.0 px at `vr_hud_distance` 1 and 21.0 px at 2 (41.4 and 20.7 expected); the old fixed shift was
|
||||
2 x 48 = 96 px, about 0.43 m.
|
||||
|
||||
Yaw 0 and 90 dumps of `c1a0` (the first corridor): the eyes differ by about the asymmetric-frustum axis offset, 90 turns the head
|
||||
to the side wall. Those runs predate the release assets below, so they have no hands or weapons.
|
||||
|
||||
The repo git-ignores `*.mdl`, `*.wav` and `assets/background`, so a plain build has no `v_hand`, `v_torch` or replacement
|
||||
weapons (`Mod_ForName ... couldn't load`). Run `tools/steam_frame/fetch_release_assets.sh` once before `assembleFrame`: it
|
||||
takes the missing, ignored files from Team Beef's v1.7.4 release APK (SHA-256 pinned) and leaves tracked files alone. The APK
|
||||
is then about 39 MB, like the release.
|
||||
|
||||
Host tests: `tools/steam_frame/tests/run_tests.sh` (binding tables against the profiles' component lists, unique action names,
|
||||
the ini parser, the BMP writer, the booking check in `deploy.sh`).
|
||||
|
||||
## 6. Status
|
||||
|
||||
| Area | State |
|
||||
| --- | --- |
|
||||
| Gradle `frame` build, APK content, manifest | verified unattended: installs over adb and as the Steam devkit title `Lambda1VR` (shortcut appid 4086534518) |
|
||||
| Steam title launch | verified unattended (2026-10-04): session SYNCHRONIZED, Frame/Index/Touch suggestions `XR_SUCCESS`, main menu at 72 fps, exit_after clean, Steam logs "Game process removed" |
|
||||
| Release assets (hands, replacement weapons, sounds) | verified unattended: `v_hand` loads and the parked hands show in the eye dumps. `barney_vest`/`barney_helmet` and `sprites/scope` are still missing (upstream gaps too) |
|
||||
| Khronos loader finds SteamVR | verified unattended (SteamVR/OpenXR 2.17.10) |
|
||||
| Instance, session, swapchains, frame loop | verified unattended: 72 fps, 1728x1728, session SYNCHRONIZED |
|
||||
| Standby handling | built, not run (force_render hides it); exit_after: verified unattended, clean |
|
||||
| Exit from the SteamVR menu (EXITING) | built, not run |
|
||||
| Data folder in Documents | verified unattended (Steam Half-Life loose files, no pak0.pak, load fine) |
|
||||
| Bindings: Frame, Index, Touch | suggestions `XR_SUCCESS` unattended; host-checked against the component lists; no input tested |
|
||||
| View hold recentre | built, not run |
|
||||
| Diagnostics (force render, autostart, dumps, perf, exit_after, probe) | verified unattended |
|
||||
| Eye dumps at yaw 0 and 90 | verified unattended (yaw 180 not done); stereo parity test not started |
|
||||
| Eyes drawn and submitted from the runtime's eye poses | verified unattended (eye images against traced depths); not worn |
|
||||
| HUD at `vr_hud_distance` | verified unattended at 1 and 2 m (disparity); not worn |
|
||||
| Menu laser along the aim pose, quad layout | not started |
|
||||
| Depth layer, motion vectors, foveation | not started |
|
||||
| Controller map (HL25 gamepad layout, grips as hands, menu navigation) | built, host table test passes; the buttons ran unattended through `diag_press` on 2026-10-04 and logged the intended actions; not worn |
|
||||
| HUD, text and camera-facing sprites kept level when the head rolls (`vr_hud_level`) | HUD verified unattended at roll +-20 (it turned with the world once the sign was fixed: +20.5 / -20.5 deg against +-20); sprites built, not measured; not worn |
|
||||
| Menu laser on top of the screen (aim pose ray, beam and dot as layers after the screen) | verified unattended (2026-10-05) as far as standby allows: the dump's `vr-state.txt` has the hand's aim pose, the cursor (u 0.4765, v 0.5211 = pixel 1029, 1126, where the engine's own cursor is drawn), the layer order (screen, beam, dot) and each layer's pose; `tools/steam_frame/composite_menu.py` composites them over the eye 0 menu image from the left eye: the beam leaves the hand and ends in the dot on the cursor, both over the panel. That models the compositor, it isn't SteamVR's output (standby captures are black). Not worn |
|
||||
| In-game laser sight (`vr_lasersight` 1 and 2) | not seen in four unattended dumps (glock, force_render, hand aimed three ways); the HUD and the weapon draw. Cause not found; needs a look in the headset or a server-side trace |
|
||||
| Half-Life from Steam in place (`XASH3D_RODIR`) | verified unattended (2026-10-05): with 4174 byte-identical files moved out of `~/Documents/Lambda1VR/valve`, the log says `[data] game files: Steam, in place`, `fs_path` lists the Steam `valve/` as `rodir` and its wads (halflife.wad 3116 lumps), c1a0, the save and c2a5 load, and the hands and replacement weapons still come from Documents (no `Mod_ForName` errors for them). Reads are direct: Documents and Steam are ext4 bind mounts, not FUSE; SELinux is off in Lepton; no avc or EACCES. Case is handled (`exec MEDIA/STARTUPVIDS.TXT` finds `StartupVids.txt`) |
|
||||
| `vr_refresh_rate`, `vr_resolution_scale` (default 1.25 = 2160) | `vr_resolution_scale` verified (1728, 2160, 2592, 3024 logged and drawn). `vr_refresh_rate` only picks from what the runtime offers (90 Hz alone), so it asked for 90 while SteamVR ran the title at its per-app 120: the rate comes from SteamVR's per-app setting, and only for the Steam title launch (an adb launch runs at 72) |
|
||||
| 120 Hz and 2160 measurement | done unattended 2026-10-05 from the Steam title (standby, force_render, the user's control-room save): 120 fps, 0 missed at 1728 and 2160, 3 runs each, and in c2a5 at 2160 (3 runs), 2592 and 3024 (1 each); frame period 8.33 ms. Adreno 903 MHz 56-61% at 1728, 100% at 2160 and up. Standby: no display scan-out, so the compositor's cost with the headset worn is not in these numbers |
|
||||
| Comfort set, haptics table, VR settings page | not started |
|
||||
| Performance at the Frame's eye size | unattended numbers above; thread CPU 3.1 ms of 8.33, the rest of `cpu_ms` (7.5) is waits (acquire 2.25 ms, wait 0.65 ms, GPU-fenced calls). Worn (2026-10-05): the user saw a smooth 120 fps with the FPS counter on at 2160 per eye, "very sharp and very clean"; no worn frame-time capture |
|
||||
|
||||
## 7. Reference display settings (the build that was worn, 2026-10-05)
|
||||
|
||||
The user wore the Steam title at its defaults with the FPS counter on: smooth 120 fps, "very sharp and very clean", "feels really good".
|
||||
This is the reference target for the other Frame ports. Everything below is read from the code or the logs; the file and function
|
||||
names are the anchor, line numbers are for this commit.
|
||||
|
||||
| Setting | Value | Where |
|
||||
| --- | --- | --- |
|
||||
| Refresh | 120 Hz, from SteamVR's per-app setting (`~/.config/openvr/config/steamvr.vrsettings`: `steam.app.4086534518` `preferredRefreshRate` 120). `XR_FB_display_refresh_rate` offers only that one rate for an app (the logs said 90 on the first run), so the app's request can't raise it. Only a Steam title launch gets it; an adb launch ran at 72 | `TBXR_ChooseRefreshRate` (`TBXR_Common.c`:1537) |
|
||||
| App's own request | `vr_refresh_rate` 90 (archived), the highest offered rate not over it, else the lowest. The run asked for 90 and got 120 from SteamVR | `L1VR_SurfaceView.c`:586, `TBXR_Common.c`:1537 |
|
||||
| Frame period | 8.33 ms (`predictedDisplayPeriod`); `vr_refresh` (walking scale) is set from it, 120 | `L1VR_SurfaceView.c`:705-714 |
|
||||
| Eye image | 2160 x 2160 = 1728 (recommended) x `vr_resolution_scale` 1.25 (archived, clamped 0.5 to 2, and to the view and swapchain maximum, 8192), rounded down to even. Log: `[openxr] eye image: recommended 1728x1728 ... scale 1.25, using 2160x2160` | `TBXR_SetEyeScale` (`TBXR_Common.c`:1504), `L1VR_SurfaceView.c`:587 |
|
||||
| Supersampling | none: `SS_MULTIPLIER` is 1.0 on the Frame (1.3 on Quest), only the `-s` command line option changes it, and `TBXR_SetEyeScale` multiplies by it | `TBXR_Common.c`:60-65, `L1VR_SurfaceView.c`:964 |
|
||||
| Colour format | `GL_SRGB8_ALPHA8` (0x8c43), the engine draws linear into sRGB images. Offered: 0x8c41, 0x8c43, 0x81a5, 0x81a6, 0x8cac | `ovrRenderer_Create` (`TBXR_Common.c`:820), `TBXR_CheckSwapchainFormats` |
|
||||
| Samples | 1 on the swapchain (`sampleCount = 1`) and `NUM_MULTI_SAMPLES = 1`: no MSAA and no `EXT_multisampled_render_to_texture`. The Frame path attaches the swapchain texture and a depth renderbuffer with the driver's own GLES 3 calls (`GenRenderbuffers`, `FramebufferTexture2D`). The engine's `gl_msaa` is 0 | `TBXR_Common.c`:58, 577, 625-660; `vid_common.c`:901; `opengl.cfg` |
|
||||
| Swapchain | one per eye, 3 images, 2D (array size 1, face count 1, 1 mip). Log: `swapchain: format 0x8c43, 2160x2160, 3 images, multiview off (one swapchain per eye), 1 samples` | `ovrFramebuffer_Create`, `TBXR_Common.c`:699 |
|
||||
| Multiview | not used. `GL_OVR_multiview(2)` is advertised, but `glExtensions.multi_view` is detected and never read, and the engine draws each eye in its own pass | `TBXR_Common.c`:260, 279 |
|
||||
| Depth | `GL_DEPTH_COMPONENT24` renderbuffer per swapchain image, no stencil (the engine's `gl_stencilbits` 8 has no effect on the eye FBO), `glInvalidateFramebuffer` on it after each eye so the tiler doesn't write it back. No `XrCompositionLayerDepthInfoKHR` is submitted | `TBXR_Common.c`:638, 731 |
|
||||
| Foveation | none. The enum has Quest's `FOVEATION_*` entries and nothing sets them; no `XR_FB_foveation` use on the Frame; the loader log shows Valve's `fdm_injection` XR layer loaded, and I haven't checked what it does for this app | `TBXR_Common.c`:98, 117 |
|
||||
| Layers | one projection layer, flags `BLEND_TEXTURE_SOURCE_ALPHA` and `CORRECT_CHROMATIC_ABERRATION`, each view with its own pose and the runtime's asymmetric FOV; quad layers for the menu, laser beam and dot | `TBXR_Common.c`:2757-2758, 2815 (quad layers) |
|
||||
| EGL | ES 3 context (`EGL_CONTEXT_CLIENT_VERSION` 3), RGBA8888, no depth, no stencil, 0 samples | `ovrEgl_CreateContext`, `TBXR_Common.c`:416 |
|
||||
| gl4es | built with `-DDEFAULT_ES=2`, linked against `libGLESv3.so`: log says `Using GLES 2.0 backend`, "Targeting OpenGL 2.0", full NPOT, FBO in core, `GL_EXT_texture_filter_anisotropic` (max 16), `GL_OES_depth24`, `GL_EXT_color_buffer_float`, sRGB surface supported; Mesa (Zink on Turnip). So the game's GL is gl4es on a GLES 2 backend, in a GLES 3 context, and only the swapchain attachment calls are GLES 3 | `gl4es/Android.mk`:80-84 |
|
||||
| Texture filtering | `gl_texturemode GL_LINEAR_MIPMAP_LINEAR` (trilinear), `gl_anisotropy 8` (clamped to the driver's 16), `gl_texture_lodbias 0`, `gl_picmip 0`, `gl_skymip 0`, `gl_round_down 0`, `gl_max_size 512` (legacy, no effect), `gl_compress_textures 0`. Eye framebuffer images are `GL_LINEAR` / clamp to edge | `vid_common.c`:886-901; `TBXR_Common.c`:620-624 |
|
||||
| Other renderer defaults that were in effect | `r_vbo 0`, `r_bump 0`, `r_detailtextures 0` (the device's saved config; the code default is 1), `r_dynamic 1`, `r_lighting_modulate 0.6`, `r_lighting_ambient 0.3`, `gl_overbright 0`, `gl_swapInterval 0`, `gl_finish 0`, `gl_allow_mirrors` forced 0 | the device's `valve/config.cfg`, `vid_android.c`:135 |
|
||||
| Gamma | `assets/config.cfg` ships `gamma 2.0`, `brightness 0.8` (code default gamma 1.0). The data copy on the Frame had `gamma 2.5` and `fps_max 144`, saved 2026-10-04 21:26, and was read at start of the worn session | `assets/config.cfg`, `valve/config.cfg` on the device |
|
||||
| Frame cap | `fps_max 144` in the device's config; the compositor's 120 Hz wait paces the loop (`wait_ms` 0.65, `acquire_ms` 2.25) | `[vr-perf]` |
|
||||
|
||||
Measured unattended at this configuration (Steam title launch, standby with `force_render`, `scratchpad/step4/summary.txt`; 3 runs per row,
|
||||
`[vr-perf]` every 10 s):
|
||||
|
||||
| Eye size | fps (min..avg) | missed | thread CPU | wait | acquire | `cpu_ms` | Adreno at 903 MHz |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 1728 | 120.0..120.0 | 0 | 3.08-3.12 ms | 0.64-0.68 | 2.22-2.26 | 7.43-7.49 | 56-61% |
|
||||
| 2160 (reference) | 120.0..120.0 | 0 | 3.07-3.11 ms | 0.65-0.66 | 2.24-2.27 | 7.46-7.48 | 100% |
|
||||
| 2160, `c2a5` (busier map) | 120.0..120.0 | 0 | 3.14-3.18 ms | 1.60-1.66 | 2.25-2.27 | 6.46-6.53 | 100% |
|
||||
| 2592, 3024 (`c2a5`, 1 run each) | 120.0..120.0 | 0 | 3.17, 3.19 ms | 1.65, 1.82 | 2.24, 2.07 | 6.49, 6.30 | 100% |
|
||||
|
||||
Thread CPU is about 3.1 ms of the 8.33 ms budget at every size, and the Adreno is at 100% from 2160 up, so what limits the frame is the GPU side,
|
||||
not the game thread. The first 10 s window of a run is short of 120 (s125_r1: 107.7 fps, 9 missed, thread CPU 4.06 ms while the map loads) and isn't in the summary. These numbers are from standby: no
|
||||
display scan-out, so the compositor's cost with the headset worn isn't in them; the user's report is the worn check.
|
||||
@@ -16,6 +16,7 @@ import java.util.Locale;
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
@@ -49,19 +50,36 @@ import static android.system.Os.setenv;
|
||||
manufacturer = "meta";
|
||||
}
|
||||
|
||||
try
|
||||
if (BuildConfig.STEAM_FRAME)
|
||||
{
|
||||
//Load manufacturer specific loader
|
||||
System.loadLibrary("openxr_loader_" + manufacturer);
|
||||
setenv("OPENXR_HMD", manufacturer, true);
|
||||
} catch (Exception e)
|
||||
{}
|
||||
//The Frame has no loader broker, so the Khronos loader is in the APK
|
||||
System.loadLibrary("openxr_loader");
|
||||
try
|
||||
{
|
||||
setenv("OPENXR_HMD", "steamframe", true);
|
||||
} catch (Exception e)
|
||||
{}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
//Load manufacturer specific loader
|
||||
System.loadLibrary("openxr_loader_" + manufacturer);
|
||||
setenv("OPENXR_HMD", manufacturer, true);
|
||||
} catch (Exception e)
|
||||
{}
|
||||
}
|
||||
|
||||
System.loadLibrary( "xash" );
|
||||
}
|
||||
|
||||
private static final String TAG = "Lambda1VR";
|
||||
|
||||
//Where the game data lives. On the Frame it's in Documents, which is the
|
||||
//headset's own folder and survives a reset of the Android container.
|
||||
private static final String DATA_DIR = BuildConfig.STEAM_FRAME ? "/sdcard/Documents/Lambda1VR/" : "/sdcard/xash/";
|
||||
|
||||
private static final int REQUEST_MANAGE_ALL_FILES = 2296;
|
||||
|
||||
|
||||
@@ -89,13 +107,32 @@ import static android.system.Os.setenv;
|
||||
}
|
||||
|
||||
/** Initializes the Activity only if the permission has been granted. */
|
||||
private boolean canUseStorage() {
|
||||
if (Environment.isExternalStorageManager()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (BuildConfig.STEAM_FRAME) {
|
||||
//The container on the Frame has no settings page for this, and the folder is
|
||||
//plain files from the host, so all that matters is if we can write to it
|
||||
File dir = new File(DATA_DIR);
|
||||
return (dir.isDirectory() || dir.mkdirs()) && dir.canWrite();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void checkPermissionsAndInitialize() {
|
||||
if (!Environment.isExternalStorageManager()) {
|
||||
if (!canUseStorage()) {
|
||||
//request for the permission
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
|
||||
Uri uri = Uri.fromParts("package", getPackageName(), null);
|
||||
intent.setData(uri);
|
||||
startActivityForResult(intent, REQUEST_MANAGE_ALL_FILES);
|
||||
try {
|
||||
startActivityForResult(intent, REQUEST_MANAGE_ALL_FILES);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.e(TAG, "No settings page for the all files permission, and " + DATA_DIR + " can't be written");
|
||||
}
|
||||
|
||||
finishAffinity(); // Cleanly exit
|
||||
|
||||
@@ -114,21 +151,68 @@ import static android.system.Os.setenv;
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Frame's Android container shows the headset's Steam folders at the same paths, read only. If
|
||||
* Half-Life is installed there, the game's files can be used from where they are instead of a copy.
|
||||
* Looks in the Steam library folders (the main one, and the others libraryfolders.vdf lists, such as
|
||||
* one on an SD card) for steamapps/common/Half-Life/valve/liblist.gam. Returns the Half-Life folder or null.
|
||||
*/
|
||||
private String findSteamHalfLife()
|
||||
{
|
||||
final String steam = "/home/steamos/.local/share/Steam";
|
||||
java.util.ArrayList<String> libraries = new java.util.ArrayList<String>();
|
||||
libraries.add(steam);
|
||||
|
||||
try
|
||||
{
|
||||
BufferedReader br = new BufferedReader(new FileReader(steam + "/steamapps/libraryfolders.vdf"));
|
||||
String line;
|
||||
while ((line = br.readLine()) != null)
|
||||
{
|
||||
// "path" "/some/library"
|
||||
line = line.trim();
|
||||
if (line.startsWith("\"path\""))
|
||||
{
|
||||
String[] parts = line.split("\"");
|
||||
if (parts.length >= 4 && !libraries.contains(parts[3]))
|
||||
{
|
||||
libraries.add(parts[3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
br.close();
|
||||
} catch (IOException e)
|
||||
{
|
||||
Log.i(TAG, "[data] no libraryfolders.vdf: " + e.getMessage());
|
||||
}
|
||||
|
||||
for (String library : libraries)
|
||||
{
|
||||
File liblist = new File(library + "/steamapps/common/Half-Life/valve/liblist.gam");
|
||||
if (liblist.canRead())
|
||||
{
|
||||
return library + "/steamapps/common/Half-Life";
|
||||
}
|
||||
Log.i(TAG, "[data] no Half-Life in the Steam library " + library);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void create()
|
||||
{
|
||||
copy_asset(getFilesDir().getPath(), "extras.pak", false);
|
||||
copy_asset("/sdcard/xash/", "commandline.txt", false); // Copy in case user has deleted their config
|
||||
copy_asset(DATA_DIR, "commandline.txt", false); // Copy in case user has deleted their config
|
||||
|
||||
|
||||
//Read these from a file and pass through
|
||||
commandLineParams = new String("xash3d -dev 3 -log");
|
||||
|
||||
//See if user is trying to use command line params
|
||||
if(new File("/sdcard/xash/commandline.txt").exists()) // should exist!
|
||||
if(new File(DATA_DIR + "commandline.txt").exists()) // should exist!
|
||||
{
|
||||
BufferedReader br;
|
||||
try {
|
||||
br = new BufferedReader(new FileReader("/sdcard/xash/commandline.txt"));
|
||||
br = new BufferedReader(new FileReader(DATA_DIR + "commandline.txt"));
|
||||
String s;
|
||||
StringBuilder sb=new StringBuilder(0);
|
||||
while ((s=br.readLine())!=null)
|
||||
@@ -159,319 +243,335 @@ import static android.system.Os.setenv;
|
||||
}
|
||||
|
||||
//game configuration
|
||||
copy_asset("/sdcard/xash/" + game + "/", "config.cfg", false); // Copy in case user has deleted their config
|
||||
copy_asset(DATA_DIR + game + "/", "config.cfg", false); // Copy in case user has deleted their config
|
||||
|
||||
//special commands
|
||||
copy_asset("/sdcard/xash/" + game + "/", "commands.lst", false); // Copy in case user has deleted their config
|
||||
copy_asset(DATA_DIR + game + "/", "commands.lst", false); // Copy in case user has deleted their config
|
||||
|
||||
//Copy our special stuff
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sprites/s_stealth.spr", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sprites/vignette.tga", true);
|
||||
copy_asset("/sdcard/xash/valve/", "sprites/vignette.tga", true); //seems to need to be here for some people
|
||||
copy_asset(DATA_DIR + game + "/", "sprites/s_stealth.spr", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sprites/vignette.tga", true);
|
||||
copy_asset(DATA_DIR + "valve/", "sprites/vignette.tga", true); //seems to need to be here for some people
|
||||
|
||||
//Menu Arrow
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sprites/pointer.tga", true);
|
||||
copy_asset("/sdcard/xash/valve/", "sprites/pointer.tga", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sprites/pointer.tga", true);
|
||||
copy_asset(DATA_DIR + "valve/", "sprites/pointer.tga", true);
|
||||
|
||||
//Menu Background
|
||||
copy_asset("/sdcard/xash/valve/resource/", "background/800_1_a_loading.tga", true);
|
||||
copy_asset("/sdcard/xash/valve/resource/", "background/800_1_b_loading.tga", true);
|
||||
copy_asset("/sdcard/xash/valve/resource/", "background/800_1_c_loading.tga", true);
|
||||
copy_asset("/sdcard/xash/valve/resource/", "background/800_1_d_loading.tga", true);
|
||||
copy_asset("/sdcard/xash/valve/resource/", "background/800_2_a_loading.tga", true);
|
||||
copy_asset("/sdcard/xash/valve/resource/", "background/800_2_b_loading.tga", true);
|
||||
copy_asset("/sdcard/xash/valve/resource/", "background/800_2_c_loading.tga", true);
|
||||
copy_asset("/sdcard/xash/valve/resource/", "background/800_2_d_loading.tga", true);
|
||||
copy_asset("/sdcard/xash/valve/resource/", "background/800_3_a_loading.tga", true);
|
||||
copy_asset("/sdcard/xash/valve/resource/", "background/800_3_b_loading.tga", true);
|
||||
copy_asset("/sdcard/xash/valve/resource/", "background/800_3_c_loading.tga", true);
|
||||
copy_asset("/sdcard/xash/valve/resource/", "background/800_3_d_loading.tga", true);
|
||||
copy_asset(DATA_DIR + "valve/resource/", "background/800_1_a_loading.tga", true);
|
||||
copy_asset(DATA_DIR + "valve/resource/", "background/800_1_b_loading.tga", true);
|
||||
copy_asset(DATA_DIR + "valve/resource/", "background/800_1_c_loading.tga", true);
|
||||
copy_asset(DATA_DIR + "valve/resource/", "background/800_1_d_loading.tga", true);
|
||||
copy_asset(DATA_DIR + "valve/resource/", "background/800_2_a_loading.tga", true);
|
||||
copy_asset(DATA_DIR + "valve/resource/", "background/800_2_b_loading.tga", true);
|
||||
copy_asset(DATA_DIR + "valve/resource/", "background/800_2_c_loading.tga", true);
|
||||
copy_asset(DATA_DIR + "valve/resource/", "background/800_2_d_loading.tga", true);
|
||||
copy_asset(DATA_DIR + "valve/resource/", "background/800_3_a_loading.tga", true);
|
||||
copy_asset(DATA_DIR + "valve/resource/", "background/800_3_b_loading.tga", true);
|
||||
copy_asset(DATA_DIR + "valve/resource/", "background/800_3_c_loading.tga", true);
|
||||
copy_asset(DATA_DIR + "valve/resource/", "background/800_3_d_loading.tga", true);
|
||||
|
||||
|
||||
//Copy modified weapon models - This is the base set
|
||||
if (!(new File("/sdcard/xash/" + game + "/models/no_copy").exists()) && !(game.equalsIgnoreCase("Hunger")))
|
||||
if (!(new File(DATA_DIR + game + "/models/no_copy").exists()) && !(game.equalsIgnoreCase("Hunger")))
|
||||
{
|
||||
//Colt (A-16)
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/v_9mmar.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/p_9mmar.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/w_9mmar.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/items/clipinsert1.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/items/cliprelease1.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/t_m4_boltpull.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/t_m4_deploy.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/t_m4_m203_in.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/t_m4_m203_out.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/t_m4_m203_shell.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/v_9mmar.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/p_9mmar.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/w_9mmar.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/items/clipinsert1.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/items/cliprelease1.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/t_m4_boltpull.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/t_m4_deploy.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/t_m4_m203_in.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/t_m4_m203_out.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/t_m4_m203_shell.wav", true);
|
||||
|
||||
//Colt Pistol M1911
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/v_9mmhandgun.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/p_9mmhandgun.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/w_9mmhandgun.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/glock_magin.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/glock_magout.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/glock_slideforward.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/v_9mmhandgun.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/p_9mmhandgun.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/w_9mmhandgun.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/glock_magin.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/glock_magout.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/glock_slideforward.wav", true);
|
||||
|
||||
//357 Python
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/v_357.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/p_357.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/w_357.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/357_shot1.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/357_shot2.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/v_357.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/p_357.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/w_357.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/357_shot1.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/357_shot2.wav", true);
|
||||
|
||||
//RGD Grenade
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/v_grenade.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/w_grenade.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/p_grenade.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/grenade_pinpull.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/grenade_throw.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/grenade_draw.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/explode3.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/explode4.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/explode5.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/v_grenade.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/w_grenade.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/p_grenade.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/grenade_pinpull.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/grenade_throw.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/grenade_draw.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/explode3.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/explode4.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/explode5.wav", true);
|
||||
|
||||
//Shotgun Benelli M3
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/v_shotgun.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/p_shotgun.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/w_shotgun.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/m3_insertshell.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/m3_pump.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/v_shotgun.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/p_shotgun.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/w_shotgun.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/m3_insertshell.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/m3_pump.wav", true);
|
||||
|
||||
//Stalker Rocket
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/v_rpg.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/p_rpg.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/w_rpg.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/rpgrocket.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/v_rpgammo.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/rocket1.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/rocketfire1.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/rpg7_reload.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/v_rpg.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/p_rpg.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/w_rpg.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/rpgrocket.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/v_rpgammo.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/rocket1.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/rocketfire1.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/rpg7_reload.wav", true);
|
||||
|
||||
//Satchel
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/v_satchel.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/v_satchel_radio.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/w_satchel.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/p_satchel.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/p_satchel_radio.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/v_satchel.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/v_satchel_radio.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/w_satchel.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/p_satchel.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/p_satchel_radio.mdl", true);
|
||||
|
||||
//Tripmine
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/v_tripmine.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/p_tripmine.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/mine_activate.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/mine_deploy.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/tripmine_ant.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/tripmine_button.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/tripmine_move.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/v_tripmine.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/p_tripmine.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/mine_activate.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/mine_deploy.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/tripmine_ant.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/tripmine_button.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/tripmine_move.wav", true);
|
||||
|
||||
//BMS Crossbow
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/v_crossbow.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/p_crossbow.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/w_crossbow.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/w_crossbow_clip.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/natianul.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/xbow.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/xbow_fire1.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/xbow_reload1.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/v_crossbow.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/p_crossbow.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/w_crossbow.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/w_crossbow_clip.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/natianul.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/xbow.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/xbow_fire1.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/xbow_reload1.wav", true);
|
||||
|
||||
|
||||
//Egon (Overhaul)
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/v_egon.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/p_egon.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/w_egon.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/v_egon.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/p_egon.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/w_egon.mdl", true);
|
||||
|
||||
//Gauss (Overhaul)
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/v_gauss.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/p_gauss.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/w_gauss.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/w_gaussammo.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/v_gauss.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/p_gauss.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/w_gauss.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/w_gaussammo.mdl", true);
|
||||
|
||||
//HGun (Overhaul)
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/v_hgun.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/p_hgun.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/w_hgun.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/v_hgun.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/p_hgun.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/w_hgun.mdl", true);
|
||||
|
||||
//Squeak (Overhaul)
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/v_squeak.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/p_squeak.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/w_squeak.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/w_sqknest.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/v_squeak.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/p_squeak.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/w_squeak.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/w_sqknest.mdl", true);
|
||||
|
||||
//Crowbar
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/v_crowbar.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/w_crowbar.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/cbar_draw.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/cbar_hit1.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/cbar_hit2.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/cbar_hitbod1.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/cbar_hitbod2.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/cbar_hitbod3.wav", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sound/weapons/cbar_miss1.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/v_crowbar.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/w_crowbar.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/cbar_draw.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/cbar_hit1.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/cbar_hit2.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/cbar_hitbod1.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/cbar_hitbod2.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/cbar_hitbod3.wav", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sound/weapons/cbar_miss1.wav", true);
|
||||
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/v_torch.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "models/v_hand.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/v_torch.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "models/v_hand.mdl", true);
|
||||
}
|
||||
|
||||
//Copy Opposing Force specific models
|
||||
if (game.equalsIgnoreCase("gearbox") &&
|
||||
!(new File("/sdcard/xash/" + game + "/models/no_copy").exists()))
|
||||
!(new File(DATA_DIR + game + "/models/no_copy").exists()))
|
||||
{
|
||||
//Scope vignette texture
|
||||
copy_asset("/sdcard/xash/" + game + "/", "sprites/scope.tga", true);
|
||||
copy_asset("/sdcard/xash/valve/", "sprites/scope.tga", true);
|
||||
copy_asset(DATA_DIR + game + "/", "sprites/scope.tga", true);
|
||||
copy_asset(DATA_DIR + "valve/", "sprites/scope.tga", true);
|
||||
|
||||
//Sniper Rifle
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/v_m40a1.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/w_m40a1.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/scout_clipin.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/scout_clipout.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/sniper_fire.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/v_m40a1.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/w_m40a1.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/scout_clipin.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/scout_clipout.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/sniper_fire.wav", true);
|
||||
|
||||
//Pipe Wrench
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/v_pipe_wrench.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/w_pipe_wrench.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/p_pipe_wrench.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/v_pipe_wrench.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/w_pipe_wrench.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/p_pipe_wrench.mdl", true);
|
||||
|
||||
//Penguins
|
||||
new File("/sdcard/xash/gearbox/sound/penguin/").mkdirs(); // Make the penguin directory as it won't exist
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/penguin/penguin_die1.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/penguin/penguin_deploy1.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/penguin/penguin_hunt1.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/penguin/penguin_hunt2.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/penguin/penguin_hunt3.wav", true);
|
||||
new File(DATA_DIR + "gearbox/sound/penguin/").mkdirs(); // Make the penguin directory as it won't exist
|
||||
copy_asset(DATA_DIR, "gearbox/sound/penguin/penguin_die1.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/penguin/penguin_deploy1.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/penguin/penguin_hunt1.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/penguin/penguin_hunt2.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/penguin/penguin_hunt3.wav", true);
|
||||
|
||||
//Knife
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/v_knife.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/w_knife.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/p_knife.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/v_knife.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/w_knife.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/p_knife.mdl", true);
|
||||
|
||||
//Desert Eagle
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/v_desert_eagle.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/w_desert_eagle.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/v_desert_eagle.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/w_desert_eagle.mdl", true);
|
||||
|
||||
//Saw
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/v_saw.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/p_saw.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/w_saw.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/w_saw_clip.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/saw_link.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/saw_shell.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/SAW_bolt.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/saw_fire1.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/saw_fire2.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/saw_fire3.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/saw_reload_new.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/saw_reload2_new.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/v_saw.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/p_saw.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/w_saw.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/w_saw_clip.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/saw_link.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/saw_shell.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/SAW_bolt.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/saw_fire1.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/saw_fire2.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/saw_fire3.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/saw_reload_new.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/saw_reload2_new.wav", true);
|
||||
|
||||
//Barnacle
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/v_bgrap.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/p_bgrap.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/w_bgrap.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/v_bgrap_tonguetip.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/saw_link.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/saw_shell.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/bgrapple_cough.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/bgrapple_fire.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/bgrapple_impact.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/bgrapple_pull.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/bgrapple_release.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/bgrapple_wait.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/v_bgrap.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/p_bgrap.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/w_bgrap.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/v_bgrap_tonguetip.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/saw_link.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/saw_shell.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/bgrapple_cough.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/bgrapple_fire.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/bgrapple_impact.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/bgrapple_pull.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/bgrapple_release.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/bgrapple_wait.wav", true);
|
||||
|
||||
//Shock
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/v_shock.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/v_shock.mdl", true);
|
||||
|
||||
//Pingu
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/v_penguin.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/v_penguin.mdl", true);
|
||||
|
||||
//Spore
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/v_spore_launcher.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/w_spore_launcher.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/spore.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/spore_ammo.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/splauncher_bounce.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/splauncher_impact.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/spore_hit1.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/v_spore_launcher.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/w_spore_launcher.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/spore.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/spore_ammo.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/splauncher_bounce.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/splauncher_impact.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/spore_hit1.wav", true);
|
||||
|
||||
//Displacer
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/v_displacer.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/w_displacer.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/p_displacer.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/displacer_fire.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/displacer_impact.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/displacer_self.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/displacer_spin.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/displacer_spin2.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/displacer_start.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/displacer_teleport.wav", true);
|
||||
copy_asset("/sdcard/xash/", "gearbox/sound/weapons/displacer_teleport_player.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/v_displacer.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/w_displacer.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/p_displacer.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/displacer_fire.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/displacer_impact.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/displacer_self.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/displacer_spin.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/displacer_spin2.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/displacer_start.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/displacer_teleport.wav", true);
|
||||
copy_asset(DATA_DIR, "gearbox/sound/weapons/displacer_teleport_player.wav", true);
|
||||
|
||||
//Hand
|
||||
copy_asset("/sdcard/xash/", "gearbox/models/v_hand.mdl", true);
|
||||
copy_asset(DATA_DIR, "gearbox/models/v_hand.mdl", true);
|
||||
}
|
||||
|
||||
//Copy Blue Shift specific models
|
||||
if (game.equalsIgnoreCase("bshift") &&
|
||||
!(new File("/sdcard/xash/" + game + "/models/no_copy").exists()))
|
||||
!(new File(DATA_DIR + game + "/models/no_copy").exists()))
|
||||
{
|
||||
copy_asset("/sdcard/xash/", "bshift/models/v_hand.mdl", true);
|
||||
copy_asset(DATA_DIR, "bshift/models/v_hand.mdl", true);
|
||||
}
|
||||
|
||||
//Copy They Hunger specific models
|
||||
if (game.equalsIgnoreCase("Hunger") &&
|
||||
!(new File("/sdcard/xash/" + game + "/models/no_copy").exists()))
|
||||
!(new File(DATA_DIR + game + "/models/no_copy").exists()))
|
||||
{
|
||||
//HL1 models
|
||||
copy_asset("/sdcard/xash/" + game + "/", "/models/v_rpg.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "/models/v_357.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "/models/v_satchel.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "/models/v_satchel_radio.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "/models/v_tripmine.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "/models/v_crossbow.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "/models/v_gauss.mdl", true);
|
||||
copy_asset("/sdcard/xash/" + game + "/", "/models/v_squeak.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "/models/v_rpg.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "/models/v_357.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "/models/v_satchel.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "/models/v_satchel_radio.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "/models/v_tripmine.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "/models/v_crossbow.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "/models/v_gauss.mdl", true);
|
||||
copy_asset(DATA_DIR + game + "/", "/models/v_squeak.mdl", true);
|
||||
|
||||
//They Hunger models
|
||||
copy_asset("/sdcard/xash/", "Hunger/models/v_9mmar.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "Hunger/models/v_9mmhandgun.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "Hunger/models/v_ap9.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "Hunger/models/v_crowbar.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "Hunger/models/v_egon.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "Hunger/models/v_hkg36.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "Hunger/models/v_shotgun.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "Hunger/models/v_shovel.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "Hunger/models/v_taurus.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "Hunger/models/v_tfac.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "Hunger/models/v_tfc_medkit.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "Hunger/models/v_tfc_sniper.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "Hunger/models/v_tfc_spanner.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "Hunger/models/v_tnt.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "Hunger/models/v_hand.mdl", true);
|
||||
copy_asset(DATA_DIR, "Hunger/models/v_9mmar.mdl", true);
|
||||
copy_asset(DATA_DIR, "Hunger/models/v_9mmhandgun.mdl", true);
|
||||
copy_asset(DATA_DIR, "Hunger/models/v_ap9.mdl", true);
|
||||
copy_asset(DATA_DIR, "Hunger/models/v_crowbar.mdl", true);
|
||||
copy_asset(DATA_DIR, "Hunger/models/v_egon.mdl", true);
|
||||
copy_asset(DATA_DIR, "Hunger/models/v_hkg36.mdl", true);
|
||||
copy_asset(DATA_DIR, "Hunger/models/v_shotgun.mdl", true);
|
||||
copy_asset(DATA_DIR, "Hunger/models/v_shovel.mdl", true);
|
||||
copy_asset(DATA_DIR, "Hunger/models/v_taurus.mdl", true);
|
||||
copy_asset(DATA_DIR, "Hunger/models/v_tfac.mdl", true);
|
||||
copy_asset(DATA_DIR, "Hunger/models/v_tfc_medkit.mdl", true);
|
||||
copy_asset(DATA_DIR, "Hunger/models/v_tfc_sniper.mdl", true);
|
||||
copy_asset(DATA_DIR, "Hunger/models/v_tfc_spanner.mdl", true);
|
||||
copy_asset(DATA_DIR, "Hunger/models/v_tnt.mdl", true);
|
||||
copy_asset(DATA_DIR, "Hunger/models/v_hand.mdl", true);
|
||||
}
|
||||
|
||||
//Copy Afraid of Monsters Director's Cut specific models
|
||||
if (game.equalsIgnoreCase("AoMDC") &&
|
||||
!(new File("/sdcard/xash/" + game + "/models/no_copy").exists()))
|
||||
!(new File(DATA_DIR + game + "/models/no_copy").exists()))
|
||||
{
|
||||
//Afraid of Monsters Director's Cut models
|
||||
copy_asset("/sdcard/xash/", "AoMDC/models/v_axe.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "AoMDC/models/v_beretta.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "AoMDC/models/v_deagle.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "AoMDC/models/v_glock.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "AoMDC/models/v_hammer.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "AoMDC/models/v_hand.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "AoMDC/models/v_kitchenknife.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "AoMDC/models/v_mp5k.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "AoMDC/models/v_p228.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "AoMDC/models/v_revolver.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "AoMDC/models/v_shotgun.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "AoMDC/models/v_spear.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "AoMDC/models/v_torch.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "AoMDC/models/v_uzi.mdl", true);
|
||||
copy_asset("/sdcard/xash/", "AoMDC/models/gmgeneral_display.aomdc", true);
|
||||
copy_asset(DATA_DIR, "AoMDC/models/v_axe.mdl", true);
|
||||
copy_asset(DATA_DIR, "AoMDC/models/v_beretta.mdl", true);
|
||||
copy_asset(DATA_DIR, "AoMDC/models/v_deagle.mdl", true);
|
||||
copy_asset(DATA_DIR, "AoMDC/models/v_glock.mdl", true);
|
||||
copy_asset(DATA_DIR, "AoMDC/models/v_hammer.mdl", true);
|
||||
copy_asset(DATA_DIR, "AoMDC/models/v_hand.mdl", true);
|
||||
copy_asset(DATA_DIR, "AoMDC/models/v_kitchenknife.mdl", true);
|
||||
copy_asset(DATA_DIR, "AoMDC/models/v_mp5k.mdl", true);
|
||||
copy_asset(DATA_DIR, "AoMDC/models/v_p228.mdl", true);
|
||||
copy_asset(DATA_DIR, "AoMDC/models/v_revolver.mdl", true);
|
||||
copy_asset(DATA_DIR, "AoMDC/models/v_shotgun.mdl", true);
|
||||
copy_asset(DATA_DIR, "AoMDC/models/v_spear.mdl", true);
|
||||
copy_asset(DATA_DIR, "AoMDC/models/v_torch.mdl", true);
|
||||
copy_asset(DATA_DIR, "AoMDC/models/v_uzi.mdl", true);
|
||||
copy_asset(DATA_DIR, "AoMDC/models/gmgeneral_display.aomdc", true);
|
||||
}
|
||||
|
||||
//Set default environment
|
||||
try {
|
||||
ApplicationInfo info = getApplicationInfo();
|
||||
setenv("XASH3D_BASEDIR", "/sdcard/xash/", true);
|
||||
setenv("XASH3D_BASEDIR", DATA_DIR, true);
|
||||
|
||||
if (BuildConfig.STEAM_FRAME)
|
||||
{
|
||||
// Half-Life from Steam, read only, in place. The folder above is searched after it, so what's
|
||||
// in there (the saves, the settings, this app's own models and sprites) wins over Steam's files.
|
||||
String steamHalfLife = findSteamHalfLife();
|
||||
if (steamHalfLife != null)
|
||||
{
|
||||
setenv("XASH3D_RODIR", steamHalfLife, true);
|
||||
Log.i(TAG, "[data] game files: Steam, in place, read only (" + steamHalfLife + "); saves and settings in " + DATA_DIR);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.i(TAG, "[data] game files: no Steam install of Half-Life found, using the copy in " + DATA_DIR);
|
||||
}
|
||||
}
|
||||
setenv("XASH3D_GAMELIBDIR", info.nativeLibraryDir, true);
|
||||
setenv("XASH3D_GAMEDIR", "valve", true);
|
||||
setenv( "XASH3D_EXTRAS_PAK1", getFilesDir().getPath() + "/extras.pak", true );
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
## This file must *NOT* be checked into Version Control Systems,
|
||||
# as it contains information specific to your local configuration.
|
||||
#
|
||||
# Location of the SDK. This is only used by Gradle.
|
||||
# For customization when using a Version Control System, please read the
|
||||
# header note.
|
||||
#Thu Dec 12 20:21:11 GMT 2019
|
||||
ndk.dir=C\:\\Users\\Simon\\AppData\\Local\\Android\\Sdk\\ndk\\21.1.6352462
|
||||
sdk.dir=C\:\\Users\\Simon\\AppData\\Local\\Android\\Sdk
|
||||
Executable
+103
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Does the HUD stay level when the head rolls? Takes dump directories made with different diag_roll values,
|
||||
each as a pair: the picture with the HUD (ON) and the same with `hud_draw 0` (OFF), so the HUD is exactly what
|
||||
differs between them (hands, torch and lamps are in both). It reports, for each roll against the first pair
|
||||
(the reference, normally roll 0), how far the world turned and how far the HUD turned, both about the eye's
|
||||
optical axis. In the eye image the head's roll turns the world, and a stabilised HUD has to turn with it
|
||||
(a HUD that is head-locked does not turn at all).
|
||||
|
||||
analyze_roll.py REF_ON REF_OFF ROLL_ON ROLL_OFF [ROLL_ON ROLL_OFF ...]
|
||||
|
||||
Needs numpy and pillow (a venv: python3 -m venv /tmp/v && /tmp/v/bin/pip install numpy pillow).
|
||||
"""
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def load(d, eye=0):
|
||||
b = Path(d, f"vr-eye{eye}.bmp").read_bytes()
|
||||
w, h = struct.unpack("<ii", b[18:26])
|
||||
off = struct.unpack("<I", b[10:14])[0]
|
||||
stride = (w * 3 + 3) & ~3
|
||||
a = np.frombuffer(b, dtype=np.uint8, count=stride * h, offset=off).reshape(h, stride)[:, : w * 3].reshape(h, w, 3)
|
||||
return Image.fromarray(a[::-1, :, ::-1].copy())
|
||||
|
||||
|
||||
def state(d):
|
||||
t = Path(d, "vr-state.txt").read_text()
|
||||
m = re.search(r"fov_eye0 L (\S+) R (\S+) U (\S+) D (\S+)", t)
|
||||
l, r, u, dn = (float(x) for x in m.groups())
|
||||
import math
|
||||
size = int(re.search(r"eye_size (\d+)x", t).group(1))
|
||||
ax = -math.tan(l) / (math.tan(r) - math.tan(l)) * size
|
||||
ay = math.tan(u) / (math.tan(u) - math.tan(dn)) * size
|
||||
roll = float(re.search(r"diag_roll (\S+)", t).group(1))
|
||||
view = re.search(r"view_roll (\S+)", t)
|
||||
return ax, ay, roll, float(view.group(1)) if view else None, size
|
||||
|
||||
|
||||
def small(img, f):
|
||||
return img.resize((img.width // f, img.height // f), Image.BILINEAR)
|
||||
|
||||
|
||||
def best_angle(ref, other, centre, mask, span=40):
|
||||
"""the rotation of ref (degrees, counter-clockwise on screen) that best matches other where mask is true"""
|
||||
ref_a = np.asarray(ref, dtype=np.float32)
|
||||
oth = np.asarray(other, dtype=np.float32)
|
||||
best = None
|
||||
for tenth in range(-span * 10, span * 10 + 1, 5):
|
||||
a = tenth / 10.0
|
||||
rot = np.asarray(ref.rotate(a, resample=Image.BILINEAR, center=centre), dtype=np.float32)
|
||||
e = float(np.mean(np.abs(rot - oth)[mask]))
|
||||
if best is None or e < best[0]:
|
||||
best = (e, a)
|
||||
return best[1], best[0]
|
||||
|
||||
|
||||
def hud_mask(on, off, h, w):
|
||||
d = np.abs(np.asarray(on, dtype=np.float32) - np.asarray(off, dtype=np.float32)).sum(axis=2)
|
||||
return Image.fromarray(((d > 150) * 255).astype(np.uint8))
|
||||
|
||||
|
||||
def main():
|
||||
dirs = sys.argv[1:]
|
||||
if len(dirs) < 4 or len(dirs) % 2:
|
||||
print(__doc__)
|
||||
return
|
||||
f = 4
|
||||
ax, ay, roll0, view0, size = state(dirs[0])
|
||||
ref = small(load(dirs[0]), f)
|
||||
h, w = np.asarray(ref).shape[:2]
|
||||
ref_mask = hud_mask(ref, small(load(dirs[1]), f), h, w)
|
||||
print(f"reference {dirs[0]}: diag_roll {roll0}, view roll {view0}, {int(np.asarray(ref_mask).astype(bool).sum())} HUD px")
|
||||
centre = (ax / f, ay / f)
|
||||
for i in range(2, len(dirs), 2):
|
||||
d_on, d_off = dirs[i], dirs[i + 1]
|
||||
_, _, roll, view, _ = state(d_on)
|
||||
img = small(load(d_on), f)
|
||||
mask = hud_mask(img, small(load(d_off), f), h, w)
|
||||
|
||||
# the world: the upper middle, away from the HUD, hands and weapon
|
||||
world = np.zeros((h, w), dtype=bool)
|
||||
world[int(h * 0.12): int(h * 0.50), int(w * 0.22): int(w * 0.78)] = True
|
||||
wa, we = best_angle(ref, img, centre, world)
|
||||
|
||||
best = None
|
||||
mimg = np.asarray(mask, dtype=np.float32) / 255.0
|
||||
for tenth in range(-400, 401, 5):
|
||||
a = tenth / 10.0
|
||||
rot = np.asarray(ref_mask.rotate(a, resample=Image.BILINEAR, center=centre), dtype=np.float32) / 255.0
|
||||
score = float((rot * mimg).sum())
|
||||
if best is None or score > best[0]:
|
||||
best = (score, a)
|
||||
print(f"{d_on}: diag_roll {roll}, view roll {view}: the world turned {wa:+.1f} deg (match error {we:.1f}), "
|
||||
f"the HUD turned {best[1]:+.1f} deg ({int(mimg.sum())} HUD px)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+191
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stereo numbers from a dump directory (vr-eye0.bmp, vr-eye1.bmp, vr-state.txt written by dump_frame).
|
||||
|
||||
Block-matches the right eye against the left along rows and reports the disparity between the optical
|
||||
axes, the way a viewer's eyes would see it (positive: nearer than infinity). The asymmetric-frustum
|
||||
offset between the two eye images is taken out first, from the fov in vr-state.txt.
|
||||
|
||||
analyze_stereo.py DIR floor model and a few regions
|
||||
analyze_stereo.py DIR --box x0 y0 x1 y1 disparity of one box (pixels of the left eye image)
|
||||
|
||||
Needs numpy and pillow (a venv: python3 -m venv /tmp/v && /tmp/v/bin/pip install numpy pillow).
|
||||
The floor model: for a flat floor h below the eyes, the disparity at a row y is ipd * (y - yaxis) / h, so a fit
|
||||
over the floor rows gives h (it should be the eye height) whatever the texture is.
|
||||
"""
|
||||
import math
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def load_bmp(path):
|
||||
d = Path(path).read_bytes()
|
||||
w, h = struct.unpack("<ii", d[18:26])
|
||||
off = struct.unpack("<I", d[10:14])[0]
|
||||
stride = (w * 3 + 3) & ~3
|
||||
a = np.frombuffer(d, dtype=np.uint8, count=stride * h, offset=off).reshape(h, stride)[:, : w * 3].reshape(h, w, 3)
|
||||
return a[::-1, :, ::-1].astype(np.float32) # top row first, RGB
|
||||
|
||||
|
||||
def luma(a):
|
||||
return a @ np.array([0.299, 0.587, 0.114], dtype=np.float32)
|
||||
|
||||
|
||||
def parse_state(path):
|
||||
t = Path(path).read_text()
|
||||
st = {}
|
||||
for eye in (0, 1):
|
||||
m = re.search(rf"fov_eye{eye} L (\S+) R (\S+) U (\S+) D (\S+)", t)
|
||||
st[f"fov{eye}"] = tuple(float(x) for x in m.groups())
|
||||
m = re.search(rf"eye{eye}_in_head_mm (\S+) (\S+) (\S+)", t)
|
||||
st[f"pos{eye}"] = tuple(float(x) / 1000.0 for x in m.groups()) if m else None
|
||||
m = re.search(r"eye_size (\d+)x(\d+)", t)
|
||||
st["size"] = (int(m.group(1)), int(m.group(2)))
|
||||
return st
|
||||
|
||||
|
||||
def axis_px(fov, size):
|
||||
l, r, u, d = fov
|
||||
w, h = size
|
||||
return (-math.tan(l) / (math.tan(r) - math.tan(l)) * w, math.tan(u) / (math.tan(u) - math.tan(d)) * h)
|
||||
|
||||
|
||||
def focal(fov, size):
|
||||
l, r, u, d = fov
|
||||
return size[0] / (math.tan(r) - math.tan(l)), size[1] / (math.tan(u) - math.tan(d))
|
||||
|
||||
|
||||
def match_row_blocks(g0, g1, y0, y1, x0, x1, centre=0, search=110, block=33, predict=None):
|
||||
"""For each block in the left image find the shift s (x1 = x0 + s) that best matches the right image."""
|
||||
res = []
|
||||
half = block // 2
|
||||
for y in range(y0 + half, y1 - half, block):
|
||||
for x in range(x0 + half, x1 - half, block):
|
||||
patch = g0[y - half : y + half + 1, x - half : x + half + 1]
|
||||
if patch.std() < 6.0:
|
||||
continue
|
||||
best, bs = None, 0
|
||||
c = centre if predict is None else int(round(centre - predict(y)))
|
||||
for s in range(c - search, c + search + 1):
|
||||
xs = x + s
|
||||
if xs - half < 0 or xs + half + 1 > g1.shape[1]:
|
||||
continue
|
||||
cand = g1[y - half : y + half + 1, xs - half : xs + half + 1]
|
||||
e = float(np.mean((patch - cand) ** 2))
|
||||
if best is None or e < best:
|
||||
best, bs = e, s
|
||||
res.append((x, y, bs, best))
|
||||
return res
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
d = Path(args[0])
|
||||
st = parse_state(d / "vr-state.txt")
|
||||
g0 = luma(load_bmp(d / "vr-eye0.bmp"))
|
||||
g1 = luma(load_bmp(d / "vr-eye1.bmp"))
|
||||
size = st["size"]
|
||||
ax0, ay0 = axis_px(st["fov0"], size)
|
||||
ax1, ay1 = axis_px(st["fov1"], size)
|
||||
f0x, f0y = focal(st["fov0"], size)
|
||||
f1x, f1y = focal(st["fov1"], size)
|
||||
p0, p1 = st["pos0"], st["pos1"]
|
||||
ipd = (p1[0] - p0[0]) if p0 and p1 else 0.063
|
||||
print(f"axis pixel: eye0 ({ax0:.1f}, {ay0:.1f}) eye1 ({ax1:.1f}, {ay1:.1f}); focal x {f0x:.1f} / {f1x:.1f} px, y {f0y:.1f} / {f1y:.1f} px")
|
||||
print(f"eye separation {ipd * 1000:.1f} mm; disparity at 1 m would be {ipd * f0x:.1f} px, at 5 m {ipd * f0x / 5:.1f} px")
|
||||
|
||||
centre = int(round(ax1 - ax0)) # where the infinity shift lies
|
||||
|
||||
def to_disp(x0, s):
|
||||
# a point at x0 in the left image sits at x0 + s in the right: from the axes, d = (x0 - ax0) - (x0 + s - ax1)
|
||||
return (x0 - ax0) - (x0 + s - ax1)
|
||||
|
||||
probes = [(float(m.group(1)), float(m.group(2)), float(m.group(3)), float(m.group(4)))
|
||||
for m in re.finditer(r"probe (\d+) (\d+) depth_m (\S+) disparity_px (\S+)", (d / "vr-state.txt").read_text())]
|
||||
if probes:
|
||||
# ground truth: the engine traced a ray through each of these pixels of the left eye
|
||||
print(f"{len(probes)} traced probes. u v depth_m predicted measured error (px)")
|
||||
rows = []
|
||||
for u, v, depth, pred in probes:
|
||||
half = 12 if depth < 4.0 else 20
|
||||
xu, yv = int(u), int(v)
|
||||
if yv - half < 0 or yv + half + 1 > g0.shape[0] or xu - half < 0 or xu + half + 1 > g0.shape[1]:
|
||||
continue
|
||||
patch = g0[yv - half : yv + half + 1, xu - half : xu + half + 1]
|
||||
if patch.std() < 7.0:
|
||||
continue
|
||||
c = int(round(centre - pred))
|
||||
errs = {}
|
||||
for sh in range(c - 30, c + 31):
|
||||
xs = xu + sh
|
||||
if xs - half < 0 or xs + half + 1 > g1.shape[1]:
|
||||
continue
|
||||
errs[sh] = float(np.mean((patch - g1[yv - half : yv + half + 1, xs - half : xs + half + 1]) ** 2))
|
||||
if len(errs) < 40:
|
||||
continue
|
||||
bs = min(errs, key=errs.get)
|
||||
best = errs[bs]
|
||||
# a match counts only when it's clearly better than any other place (the floor texture repeats)
|
||||
others = [e for sh, e in errs.items() if abs(sh - bs) >= 4]
|
||||
if not others or best > 0.75 * min(others):
|
||||
continue
|
||||
# sub-pixel: a parabola through the match and its two neighbours
|
||||
frac = 0.0
|
||||
if bs - 1 in errs and bs + 1 in errs:
|
||||
a, b, c2 = errs[bs - 1], errs[bs], errs[bs + 1]
|
||||
den = a - 2 * b + c2
|
||||
if den > 0:
|
||||
frac = 0.5 * (a - c2) / den
|
||||
meas = to_disp(xu, bs + frac)
|
||||
rows.append((depth, pred, meas, u, v))
|
||||
rows.sort()
|
||||
for depth, pred, meas, u, v in rows:
|
||||
print(f" {u:5.0f} {v:5.0f} {depth:7.2f} m {pred:6.2f} {meas:6.1f} {meas - pred:+5.1f}")
|
||||
for name, lo, hi in (("far (> 6 m)", 6.0, 1e9), ("mid (2 to 6 m)", 2.0, 6.0), ("near (< 2 m)", 0.0, 2.0)):
|
||||
sel = [(p_, m_) for dep, p_, m_, _, _ in rows if lo <= dep < hi]
|
||||
if sel:
|
||||
err = np.array([m_ - p_ for p_, m_ in sel])
|
||||
print(f"{name}: {len(sel)} probes, predicted {np.mean([p_ for p_, _ in sel]):.1f} px, measured {np.mean([m_ for _, m_ in sel]):.1f} px, "
|
||||
f"mean error {err.mean():+.2f} px, median {np.median(err):+.2f}, sd {err.std():.2f}")
|
||||
return
|
||||
|
||||
if "--box" in args:
|
||||
i = args.index("--box")
|
||||
bx0, by0, bx1, by1 = (int(v) for v in args[i + 1 : i + 5])
|
||||
res = match_row_blocks(g0, g1, by0, by1, bx0, bx1, centre, block=17)
|
||||
ds = [to_disp(x, s) for x, y, s, e in res]
|
||||
if ds:
|
||||
print(f"box {bx0},{by0}-{bx1},{by1}: {len(ds)} blocks, disparity median {np.median(ds):.1f} px (min {min(ds):.1f}, max {max(ds):.1f})"
|
||||
f" -> distance {ipd * f0x / max(np.median(ds), 1e-3):.2f} m")
|
||||
else:
|
||||
print("no textured blocks")
|
||||
return
|
||||
|
||||
# floor model over the lower part of the picture
|
||||
# the floor repeats its texture, so look only near what a floor 1.6 m down would give (and fit h afterwards)
|
||||
res = match_row_blocks(g0, g1, int(size[1] * 0.62), int(size[1] * 0.97), int(size[0] * 0.30), int(size[0] * 0.70), centre,
|
||||
search=18, block=65, predict=lambda y: ipd * (y - ay0) / 1.6)
|
||||
if res:
|
||||
ys = np.array([y for x, y, s, e in res], dtype=np.float32)
|
||||
ds = np.array([to_disp(x, s) for x, y, s, e in res], dtype=np.float32)
|
||||
slope, icpt = np.polyfit(ys - ay0, ds, 1)
|
||||
resid = ds - (slope * (ys - ay0) + icpt)
|
||||
print(f"floor: {len(res)} blocks, fit d = {slope:.4f} * (y - yaxis) + {icpt:.2f}; residual sd {resid.std():.2f} px")
|
||||
print(f" eye height above the floor from the slope: {ipd / slope:.3f} m (expected ~1.6 m); offset at the axis row {icpt:.2f} px (expected 0)")
|
||||
for lo, hi in ((0.62, 0.72), (0.72, 0.85), (0.85, 0.97)):
|
||||
sel = [(y, dd) for y, dd in zip(ys, ds) if lo * size[1] <= y < hi * size[1]]
|
||||
if sel:
|
||||
m = float(np.median([dd for _, dd in sel]))
|
||||
print(f" rows {lo:.2f}-{hi:.2f} of the height: disparity {m:.1f} px, i.e. {ipd * f0x / max(m, 1e-3):.2f} m away")
|
||||
# the far region around the centre, above the floor
|
||||
res = match_row_blocks(g0, g1, int(size[1] * 0.28), int(size[1] * 0.52), int(size[0] * 0.40), int(size[0] * 0.60), centre, block=17)
|
||||
ds = [to_disp(x, s) for x, y, s, e in res]
|
||||
if ds:
|
||||
print(f"centre (far end): {len(ds)} blocks, disparity median {np.median(ds):.1f} px -> {ipd * f0x / max(np.median(ds), 1e-3):.1f} m")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""What the compositor shows for the main menu: the screen quad (the eye 0 dump as its picture), then the
|
||||
menu laser's beam and dot quads over it, in the order the app submits them (layer_order in vr-state.txt),
|
||||
seen from the left eye. The runtime composites for real; this models it from the poses the app logged, so
|
||||
it checks the maths and the order, not SteamVR. Dev tool.
|
||||
|
||||
composite_menu.py DUMP_DIR OUT.png
|
||||
|
||||
Needs numpy and pillow (a venv: python3 -m venv /tmp/v && /tmp/v/bin/pip install numpy pillow).
|
||||
"""
|
||||
import math
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
BEAM_COLOUR = (0.15, 0.75, 1.0) # what TBXR_PaintPointerTexture clears the pointer swapchain to (sRGB)
|
||||
|
||||
|
||||
def load_bmp(d):
|
||||
b = Path(d, "vr-eye0.bmp").read_bytes()
|
||||
w, h = struct.unpack("<ii", b[18:26])
|
||||
off = struct.unpack("<I", b[10:14])[0]
|
||||
stride = (w * 3 + 3) & ~3
|
||||
a = np.frombuffer(b, dtype=np.uint8, count=stride * h, offset=off).reshape(h, stride)[:, : w * 3].reshape(h, w, 3)
|
||||
return a[::-1, :, ::-1].copy()
|
||||
|
||||
|
||||
def rotate(q, v):
|
||||
"""rotate vectors v (..., 3) by quaternion q (x, y, z, w)"""
|
||||
x, y, z, w = q
|
||||
u = np.array([x, y, z])
|
||||
return v + 2.0 * np.cross(u, np.cross(u, v) + w * v)
|
||||
|
||||
|
||||
def conj(q):
|
||||
return (-q[0], -q[1], -q[2], q[3])
|
||||
|
||||
|
||||
def vec(text, key, n):
|
||||
m = re.search(key + r" (.*)", text)
|
||||
return [float(x) for x in re.findall(r"-?\d+\.\d+|-?\d+", m.group(1))][:n]
|
||||
|
||||
|
||||
def layer(text, name):
|
||||
m = re.search(r"layer_%s_pose (\S+) (\S+) (\S+) quat (\S+) (\S+) (\S+) (\S+) size (\S+) (\S+)" % name, text)
|
||||
if not m:
|
||||
return None
|
||||
v = [float(x) for x in m.groups()]
|
||||
return np.array(v[0:3]), tuple(v[3:7]), (v[7], v[8])
|
||||
|
||||
|
||||
def hit_quad(origin, dirs, pos, quat, size):
|
||||
"""ray/quad: returns u (0..1 across, left to right), v (0..1 down), t, inside (all per ray)"""
|
||||
normal = rotate(quat, np.array([0.0, 0.0, 1.0]))
|
||||
denom = dirs @ normal
|
||||
with np.errstate(divide="ignore", invalid="ignore"):
|
||||
t = ((pos - origin) @ normal) / denom
|
||||
p = origin + dirs * t[..., None]
|
||||
local = rotate(conj(quat), p - pos)
|
||||
u = local[..., 0] / size[0] + 0.5
|
||||
v = 0.5 - local[..., 1] / size[1]
|
||||
inside = (t > 0) & (u >= 0) & (u <= 1) & (v >= 0) & (v <= 1)
|
||||
return u, v, t, inside
|
||||
|
||||
|
||||
def main():
|
||||
d, out = sys.argv[1], sys.argv[2]
|
||||
text = Path(d, "vr-state.txt").read_text()
|
||||
menu = load_bmp(d)
|
||||
size = menu.shape[0]
|
||||
head = np.array(vec(text, "head_position", 3))
|
||||
eye = head + np.array(vec(text, "eye0_in_head_mm", 3)) / 1000.0
|
||||
l, r, u_, dn = vec(text, "fov_eye0 L", 4)
|
||||
n = 1080
|
||||
xs = (np.arange(n) + 0.5) / n
|
||||
tx = math.tan(l) + xs * (math.tan(r) - math.tan(l))
|
||||
ty = math.tan(u_) + xs * (math.tan(dn) - math.tan(u_))
|
||||
dirs = np.stack(np.broadcast_arrays(tx[None, :], ty[:, None], -np.ones((n, n))), axis=-1)
|
||||
dirs = dirs / np.linalg.norm(dirs, axis=-1, keepdims=True)
|
||||
img = np.zeros((n, n, 3), np.float32)
|
||||
order = re.search(r"layer_order (.*)", text).group(1).split()
|
||||
colour = np.array(BEAM_COLOUR) * 255.0
|
||||
drawn = {}
|
||||
for name in order:
|
||||
lay = layer(text, name)
|
||||
if lay is None:
|
||||
drawn[name] = "none"
|
||||
continue
|
||||
pos, quat, sz = lay
|
||||
u, v, t, inside = hit_quad(eye, dirs, pos, quat, sz)
|
||||
if name == "screen":
|
||||
px = np.clip((u * (size - 1)).astype(int), 0, size - 1)
|
||||
py = np.clip((v * (size - 1)).astype(int), 0, size - 1)
|
||||
img[inside] = menu[py[inside], px[inside]]
|
||||
else:
|
||||
img[inside] = colour
|
||||
drawn[name] = int(inside.sum())
|
||||
pic = Image.fromarray(np.clip(img, 0, 255).astype(np.uint8))
|
||||
pic.save(out)
|
||||
# a crop around the dot, enlarged, for the eye
|
||||
dot = layer(text, "dot")
|
||||
if dot is not None:
|
||||
rel = dot[0] - eye
|
||||
fx = rel[0] / -rel[2]
|
||||
fy = rel[1] / -rel[2]
|
||||
px = (fx - math.tan(l)) / (math.tan(r) - math.tan(l)) * n
|
||||
py = (fy - math.tan(u_)) / (math.tan(dn) - math.tan(u_)) * n
|
||||
c = 90
|
||||
box = (int(px) - c, int(py) - c, int(px) + c, int(py) + c)
|
||||
pic.crop(box).resize((c * 6, c * 6), Image.NEAREST).save(out.replace(".png", "-dot.png"))
|
||||
print("dot at pixel %.0f, %.0f of %d" % (px, py, n))
|
||||
print("layers in order %s, pixels covered: %s" % (" -> ".join(order), drawn))
|
||||
|
||||
|
||||
main()
|
||||
Executable
+235
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install, launch and feed Lambda1VR on a Valve Steam Frame (Android in the Lepton
|
||||
# container) from a Mac or Linux host. See docs/STEAM_FRAME_VR.md.
|
||||
#
|
||||
# tools/steam_frame/deploy.sh [options]
|
||||
#
|
||||
# --lock-holder NAME your name in the shared Frame booking (frame-lock.sh). The
|
||||
# script looks at the current holder and refuses to install,
|
||||
# launch or register the title unless it is NAME.
|
||||
# --apk FILE the frame APK (default: the assembleFrame output)
|
||||
# --data DIR only needed when Half-Life isn't installed in Steam on the Frame
|
||||
# (the app uses that install in place, read only): rsync a Half-Life
|
||||
# "valve" folder to the headset's ~/Documents/Lambda1VR/valve. A copy
|
||||
# there shadows Steam's files. No lock needed.
|
||||
# --register-title make ~/devkit-game/Lambda1VR a persistent Steam library title
|
||||
# (once). When the title exists every install refreshes its APK.
|
||||
# --no-install skip adb install
|
||||
# --no-launch skip starting the game over adb
|
||||
# --logcat SECONDS after launching, capture SECONDS of the log into
|
||||
# scratchpad/frame-logcat.txt and print the key lines
|
||||
# --diag KEY=VAL set a key in ~/Documents/Lambda1VR/vr-diag.ini (repeatable).
|
||||
# Keys: force_render, autostart, map, diag_yaw, dump_frame,
|
||||
# exit_after, perf, oneshot (see docs/VR_ARCHITECTURE.md)
|
||||
# --diag-oneshot shorthand for --diag oneshot=1: the game renames the file
|
||||
# when it reads it, so the next start is clean
|
||||
# --diag-reset delete vr-diag.ini
|
||||
# --clear-dumps remove ~/Documents/Lambda1VR/vr-dumps before the run
|
||||
# --pull-dumps DIR adb pull vr-dumps into DIR (PNG copies with sips on a Mac)
|
||||
# --no-adb only the SSH steps (--data, --diag, --register-title)
|
||||
#
|
||||
# Environment:
|
||||
# FRAME_HOST ssh target (default steamos@192.168.1.31, the Frame on Wi-Fi)
|
||||
# FRAME_ADB_HOST adb host (default: the host part of FRAME_HOST); Lepton
|
||||
# Development publishes its adbd on <host>:5555
|
||||
# ADB adb binary
|
||||
# FRAME_LOCK path of frame-lock.sh
|
||||
#
|
||||
# adb needs "Lepton Development" running (start it from the Frame's Steam library).
|
||||
# It deletes its apps when it exits, the persistent title and the data in
|
||||
# ~/Documents/Lambda1VR stay. A binary started by adb gets no controllers and no
|
||||
# focus from SteamVR: use it for logs, and launch the Steam title to play.
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
package="com.drbeef.lambda1vr.frame"
|
||||
activity="com.drbeef.lambda1vr.GLES3JNIActivity"
|
||||
title="Lambda1VR" # Steam devkit titles must be alphanumeric
|
||||
title_apk="Lambda1VR.apk" # fixed name: the title's argv stays valid across builds
|
||||
data_dir_remote="Documents/Lambda1VR"
|
||||
|
||||
frame_host="${FRAME_HOST:-steamos@192.168.1.31}"
|
||||
adb_host="${FRAME_ADB_HOST:-${frame_host#*@}}"
|
||||
adb_bin="${ADB:-${ANDROID_HOME:-$HOME/Library/Android/sdk}/platform-tools/adb}"
|
||||
[[ -x "$adb_bin" ]] || adb_bin="$(command -v adb || true)"
|
||||
frame_lock="${FRAME_LOCK:-$HOME/.claude/skills/steam-frame-vr-port/scripts/frame-lock.sh}"
|
||||
apk="$root/Projects/Android/build/outputs/apk/frame/lambda1vr-frame.apk"
|
||||
lock_holder=""
|
||||
data=""
|
||||
register=0
|
||||
install=1
|
||||
launch=1
|
||||
use_adb=1
|
||||
logcat_seconds=0
|
||||
diag=()
|
||||
diag_reset=0
|
||||
clear_dumps=0
|
||||
pull_dumps=""
|
||||
|
||||
die() { echo "deploy.sh: $*" >&2; exit 1; }
|
||||
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--lock-holder) lock_holder="$2"; shift 2 ;;
|
||||
--apk) apk="$2"; shift 2 ;;
|
||||
--data) data="$2"; shift 2 ;;
|
||||
--register-title) register=1; shift ;;
|
||||
--no-install) install=0; shift ;;
|
||||
--no-launch) launch=0; shift ;;
|
||||
--logcat) logcat_seconds="$2"; shift 2 ;;
|
||||
--diag) diag+=("$2"); shift 2 ;;
|
||||
--diag-oneshot) diag+=("oneshot=1"); shift ;;
|
||||
--diag-reset) diag_reset=1; shift ;;
|
||||
--clear-dumps) clear_dumps=1; shift ;;
|
||||
--pull-dumps) pull_dumps="$2"; shift 2 ;;
|
||||
--no-adb) use_adb=0; shift ;;
|
||||
-h|--help) sed -n '2,/^set -euo/p' "$0" | sed '$d'; exit 0 ;;
|
||||
*) die "unknown option $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
ssh_frame() { ssh -o ConnectTimeout=10 -o BatchMode=yes "$frame_host" "$@"; }
|
||||
|
||||
# ---- The Frame is shared: only the booking holder may install or launch ------------
|
||||
needs_lock=0
|
||||
((use_adb && (install || launch))) && needs_lock=1
|
||||
((register)) && needs_lock=1
|
||||
if ((needs_lock)); then
|
||||
[[ -x "$frame_lock" ]] || die "no frame-lock.sh at $frame_lock (set FRAME_LOCK)"
|
||||
status="$("$frame_lock" status | head -n 1)"
|
||||
echo "frame-lock: $status"
|
||||
if [[ -z "$lock_holder" ]]; then
|
||||
die "installing, launching and registering need the Frame booking: claim it with frame-lock.sh and pass --lock-holder <name>"
|
||||
fi
|
||||
if [[ "$status" =~ ^HELD\ by\ ([^ ]+)\ ]]; then
|
||||
holder="${BASH_REMATCH[1]}"
|
||||
[[ "$holder" == "$lock_holder" ]] || die "the Frame is booked by '$holder', not '$lock_holder'"
|
||||
else
|
||||
die "nobody holds the Frame booking ($status); claim it first with frame-lock.sh claim $lock_holder"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- Game data: persistent storage on the Frame host ----------------------------------
|
||||
if [[ -n "$data" ]]; then
|
||||
[[ -d "$data" ]] || die "no such folder: $data"
|
||||
[[ -e "$data/liblist.gam" || -e "$data/pak0.pak" || -e "$data/halflife.wad" ]] \
|
||||
|| echo "warning: $data doesn't look like a Half-Life valve folder (no liblist.gam, pak0.pak or halflife.wad)" >&2
|
||||
ssh_frame "mkdir -p ~/$data_dir_remote/valve && chmod 777 ~/$data_dir_remote ~/$data_dir_remote/valve"
|
||||
rsync -a --info=progress2 "${data%/}/" "$frame_host:$data_dir_remote/valve/"
|
||||
ssh_frame "chmod -R a+rwX ~/$data_dir_remote/valve"
|
||||
echo "game data in place: ~/$data_dir_remote/valve (/sdcard/$data_dir_remote/valve inside Lepton)"
|
||||
fi
|
||||
|
||||
# ---- Unattended-test switches (files in the data folder) ---------------------------
|
||||
if ((diag_reset)); then
|
||||
ssh_frame "rm -f ~/$data_dir_remote/vr-diag.ini ~/$data_dir_remote/vr-diag.ini.last"
|
||||
echo "vr-diag.ini removed"
|
||||
fi
|
||||
if ((${#diag[@]})); then
|
||||
quoted="$(printf '%q ' "${diag[@]}")"
|
||||
ssh_frame "mkdir -p ~/$data_dir_remote && python3 - ~/$data_dir_remote/vr-diag.ini $quoted" <<'PY'
|
||||
import os, sys
|
||||
path, pairs = sys.argv[1], sys.argv[2:]
|
||||
lines = open(path).read().splitlines() if os.path.exists(path) else []
|
||||
for pair in pairs:
|
||||
key, _, val = pair.partition("=")
|
||||
for i, line in enumerate(lines):
|
||||
if line.split("=")[0].strip() == key:
|
||||
lines[i] = "%s = %s" % (key, val)
|
||||
break
|
||||
else:
|
||||
lines.append("%s = %s" % (key, val))
|
||||
open(path, "w").write("\n".join(lines) + "\n")
|
||||
os.chmod(path, 0o666)
|
||||
print("vr-diag.ini:", " ".join(lines))
|
||||
PY
|
||||
fi
|
||||
if ((clear_dumps)); then
|
||||
ssh_frame "rm -rf ~/$data_dir_remote/vr-dumps"
|
||||
fi
|
||||
|
||||
[[ -f "$apk" ]] || { ((install || register)) && die "no APK at $apk (build it: cd Projects/Android && sh ./gradlew assembleFrame)"; }
|
||||
|
||||
# ---- Persistent Steam title ------------------------------------------------------------
|
||||
# An existing title always gets the new APK; --register-title creates it.
|
||||
title_exists=0
|
||||
ssh_frame "test -d ~/devkit-game/$title" 2>/dev/null && title_exists=1
|
||||
if ((register || (title_exists && install && use_adb))); then
|
||||
ssh_frame "mkdir -p ~/devkit-game/$title"
|
||||
scp -q "$apk" "$frame_host:devkit-game/$title/$title_apk"
|
||||
echo "copied the APK into ~/devkit-game/$title/$title_apk"
|
||||
fi
|
||||
if ((register)); then
|
||||
parms="{\"gameid\": \"$title\", \"directory\": \"/home/steamos/devkit-game/$title\", \"force_appid\": \"\", \"argv\": [\"$title_apk\"], \"env\": {}, \"settings\": {\"steam_play\": \"1\", \"compat_tool\": \"lepton\"}, \"lepton_args\": \"\"}"
|
||||
# needs the Steam client running on the Frame; it answers with success or an error
|
||||
ssh_frame "cd ~/devkit-utils && python3 steam-client-create-shortcut --parms '$parms'"
|
||||
fi
|
||||
|
||||
((use_adb)) || exit 0
|
||||
[[ -n "$adb_bin" && -x "$adb_bin" ]] || die "adb not found; set ADB"
|
||||
|
||||
# ---- Lepton Development over network adb -----------------------------------------------
|
||||
"$adb_bin" start-server >/dev/null
|
||||
for attempt in 1 2 3 4 5; do
|
||||
"$adb_bin" connect "$adb_host:5555" >/dev/null 2>&1 || true
|
||||
if "$adb_bin" -s "$adb_host:5555" shell true 2>/dev/null; then break; fi
|
||||
sleep $((attempt * 2))
|
||||
if ((attempt == 5)); then
|
||||
die 'cannot reach Lepton Development at '"$adb_host"':5555. Start "Lepton Development" from the Steam library on the Frame.'
|
||||
fi
|
||||
done
|
||||
device="$adb_host:5555"
|
||||
adb() { "$adb_bin" -s "$device" "$@"; }
|
||||
|
||||
if ((install)); then
|
||||
adb install -r -g "$apk" | grep -i -E 'success|fail|error' || true
|
||||
# Lepton doesn't enforce all-files access for Documents, but grant it if it's asked for
|
||||
adb shell appops set "$package" MANAGE_EXTERNAL_STORAGE allow >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Don't start anything while someone is wearing the headset (an unattended run over their game). SteamVR
|
||||
# logs when the headset goes to standby; with nobody in it that happens a few seconds after an app
|
||||
# closes. If it doesn't within two minutes, someone is in it.
|
||||
headset_not_worn() {
|
||||
[[ -n "${FRAME_SKIP_WORN_CHECK:-}" ]] && return 0
|
||||
|
||||
for ((i = 0; i < 24; i++)); do
|
||||
last="$(ssh_frame 'grep -E "entering standby|leaving standby" ~/.local/share/Steam/logs/vrserver.txt | tail -n 1' 2>/dev/null || true)"
|
||||
[[ -z "$last" || "$last" == *"entering standby"* ]] && return 0
|
||||
sleep 5
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if ((launch)); then
|
||||
headset_not_worn || die "the headset isn't in standby ($last): somebody may be wearing it, not starting the game. Set FRAME_SKIP_WORN_CHECK=1 to override"
|
||||
# A running game of ours is restarted; nothing else is touched.
|
||||
adb shell am force-stop "$package"
|
||||
adb logcat -c
|
||||
adb shell am start -n "$package/$activity" >/dev/null
|
||||
echo "started $package; follow it with: $adb_bin -s $device logcat -s TBXR Lambda1VR"
|
||||
fi
|
||||
|
||||
if ((logcat_seconds > 0)); then
|
||||
out="$root/scratchpad/frame-logcat.txt"
|
||||
mkdir -p "$(dirname "$out")"
|
||||
timeout_bin="$(command -v timeout || command -v gtimeout || true)"
|
||||
if [[ -n "$timeout_bin" ]]; then
|
||||
"$timeout_bin" "$logcat_seconds" "$adb_bin" -s "$device" logcat -v time >"$out" || true
|
||||
else
|
||||
("$adb_bin" -s "$device" logcat -v time >"$out" & pid=$!; sleep "$logcat_seconds"; kill "$pid") 2>/dev/null || true
|
||||
fi
|
||||
echo "log: $out"
|
||||
grep -E "\[openxr\]|\[vr-perf\]|\[vr-dump\]|\[vr-diag\]|Lambda1VR|AndroidRuntime|FATAL" "$out" | head -120 || true
|
||||
fi
|
||||
|
||||
if [[ -n "$pull_dumps" ]]; then
|
||||
mkdir -p "$pull_dumps"
|
||||
"$adb_bin" -s "$device" pull "/sdcard/$data_dir_remote/vr-dumps/." "$pull_dumps" >/dev/null
|
||||
if command -v sips >/dev/null; then
|
||||
for bmp in "$pull_dumps"/*.bmp; do
|
||||
[[ -f "$bmp" ]] && sips -s format png "$bmp" --out "${bmp%.bmp}.png" >/dev/null
|
||||
done
|
||||
fi
|
||||
echo "dumps in $pull_dumps: $(ls "$pull_dumps" | tr '\n' ' ')"
|
||||
fi
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fill in the assets the repo doesn't track (*.mdl, *.wav, assets/background and
|
||||
# friends are git-ignored) from Team Beef's own release APK, so a build from source
|
||||
# gets the same hands, weapons, sounds and loading screens as the Quest release.
|
||||
#
|
||||
# tools/steam_frame/fetch_release_assets.sh
|
||||
#
|
||||
# Only files missing from assets/ are written; tracked files are never touched.
|
||||
# Everything it adds stays git-ignored.
|
||||
set -euo pipefail
|
||||
|
||||
tag="v1.7.4"
|
||||
apk_name="lambda1vr-v1.7.4.apk"
|
||||
apk_sha256="23e0d2ecb230ca49a4e23bead213735c3ac6097cf46aa741fac76b184adece48"
|
||||
url="https://github.com/Team-Beef-Studios/Lambda1VR/releases/download/$tag/$apk_name"
|
||||
|
||||
root="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cache="$root/Projects/Android/build/release-assets"
|
||||
apk="$cache/$apk_name"
|
||||
mkdir -p "$cache"
|
||||
|
||||
if [ ! -f "$apk" ] || ! echo "$apk_sha256 $apk" | shasum -a 256 -c - >/dev/null 2>&1; then
|
||||
echo "downloading $url"
|
||||
curl -fL --retry 3 -o "$apk.part" "$url"
|
||||
echo "$apk_sha256 $apk.part" | shasum -a 256 -c - >/dev/null || { echo "SHA-256 mismatch for $apk_name"; rm -f "$apk.part"; exit 1; }
|
||||
mv "$apk.part" "$apk"
|
||||
fi
|
||||
|
||||
stage="$cache/unpacked"
|
||||
rm -rf "$stage"
|
||||
mkdir -p "$stage"
|
||||
unzip -q "$apk" 'assets/*' -d "$stage"
|
||||
|
||||
added=0
|
||||
while IFS= read -r -d '' f; do
|
||||
rel="${f#"$stage"/}"
|
||||
dest="$root/$rel"
|
||||
[ -e "$dest" ] && continue
|
||||
if ! git -C "$root" check-ignore -q "$rel"; then
|
||||
echo "skipping $rel (not git-ignored, so it should come from the repo)"
|
||||
continue
|
||||
fi
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
cp "$f" "$dest"
|
||||
added=$((added + 1))
|
||||
done < <(find "$stage/assets" -type f -print0)
|
||||
rm -rf "$stage"
|
||||
|
||||
echo "added $added files from $apk_name to assets/"
|
||||
Executable
+159
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Make the Steam Frame's menu tile: the loading screen tile with the Frame's button mapping on it.
|
||||
|
||||
Team Beef's release has the menu background as 12 tiles, assets/background/800_R_C_loading.tga, and
|
||||
the controller help for the Quest is baked into the first one, 800_1_a_loading.tga. This reads that
|
||||
tile from the release (the tiles are git-ignored, they aren't in the repo), takes the Quest text off
|
||||
it and draws the Frame's mapping instead. The result goes into a build folder, never into the repo,
|
||||
and gradle packs it into the Frame APK only (assembleFrame runs this by itself).
|
||||
|
||||
tools/steam_frame/make_frame_tile.py [--src TILE] [--out FILE] [--preview DIR]
|
||||
|
||||
Needs Python 3 with Pillow (10.1 or newer) and numpy:
|
||||
|
||||
python3 -m venv tools/steam_frame/.venv
|
||||
tools/steam_frame/.venv/bin/pip install pillow numpy
|
||||
|
||||
Exit status: 0 made the tile, 2 the source tile isn't there, 3 Pillow or numpy is missing.
|
||||
The font is DIN Alternate Bold when the machine has it (macOS), else DejaVu Sans Bold, else the one
|
||||
that comes with Pillow. Set L1VR_TILE_FONT to a .ttf to pick another.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
DEFAULT_SRC = os.path.join(root, 'assets', 'background', '800_1_a_loading.tga')
|
||||
DEFAULT_OUT = os.path.join(root, 'Projects', 'Android', 'build', 'frame-assets', 'background', '800_1_a_loading.tga')
|
||||
|
||||
# One line per thing, "Action = Button", as the Quest's text does. It's the map in VrFrameMap.c
|
||||
# (docs/STEAM_FRAME_VR.md, Controls). Keep them in step.
|
||||
LINES = [
|
||||
'Button Mapping (Steam Frame):',
|
||||
'Move = Left stick / Turn = Right stick',
|
||||
'Fire = Right trigger / Alt fire = Left trigger',
|
||||
'Jump = A / Use = X / Reload = Y',
|
||||
'Crouch = B (hold) / Click left stick',
|
||||
'Flashlight = Click right stick / View',
|
||||
'Weapons = LB / RB or D-pad left / right',
|
||||
'Last weapon = D-pad up',
|
||||
'Laser sight = D-pad down',
|
||||
'Crowbar = Grip behind your head',
|
||||
'Two handed guns = Left grip',
|
||||
'Right grip = Use (grab, push, pull)',
|
||||
'Pause = Menu / Quick save = Hold Menu',
|
||||
'Recentre = Hold View 1 s (3 s = set height)',
|
||||
]
|
||||
INK = (232, 232, 232)
|
||||
PITCH = 11
|
||||
X, Y, MAX_WIDTH = 18, 3, 232
|
||||
|
||||
FONTS = [
|
||||
'/System/Library/Fonts/Supplemental/DIN Alternate Bold.ttf',
|
||||
'/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',
|
||||
'/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf',
|
||||
'/usr/share/fonts/TTF/DejaVuSans-Bold.ttf',
|
||||
]
|
||||
|
||||
|
||||
def load_font(ImageFont, size):
|
||||
wanted = [os.environ['L1VR_TILE_FONT']] if os.environ.get('L1VR_TILE_FONT') else FONTS
|
||||
for path in wanted:
|
||||
if os.path.exists(path):
|
||||
return ImageFont.truetype(path, size)
|
||||
return ImageFont.load_default(size)
|
||||
|
||||
|
||||
def dilate(mask, steps, np):
|
||||
# 3x3 box dilation, `steps` times
|
||||
for _ in range(steps):
|
||||
p = np.pad(mask, 1)
|
||||
out = np.zeros_like(mask)
|
||||
for dy in range(3):
|
||||
for dx in range(3):
|
||||
out |= p[dy:dy + mask.shape[0], dx:dx + mask.shape[1]]
|
||||
mask = out
|
||||
return mask
|
||||
|
||||
|
||||
def clean_tile(rgb, np):
|
||||
"""Take the baked-in text off: mask the bright glyphs and what's around them, then fill the mask
|
||||
from its edges (each masked pixel becomes the average of its neighbours, over and over)."""
|
||||
i = rgb.astype(np.int32)
|
||||
gray = np.rint((i[..., 0] * 299 + i[..., 1] * 587 + i[..., 2] * 114) / 1000.0).astype(np.int32)
|
||||
seed = gray >= 150
|
||||
seed[170:] = False
|
||||
mask = dilate((gray > 60) & dilate(seed, 2, np), 1, np)
|
||||
out = rgb.astype(np.float32)
|
||||
out[mask] = out[~mask].mean(axis=0)
|
||||
m = mask[..., None]
|
||||
for _ in range(400):
|
||||
p = np.pad(out, ((1, 1), (1, 1), (0, 0)), mode='edge')
|
||||
avg = (p[:-2, 1:-1] + p[2:, 1:-1] + p[1:-1, :-2] + p[1:-1, 2:]) / 4
|
||||
out = np.where(m, avg, out)
|
||||
return np.clip(out + 0.5, 0, 255).astype(np.uint8)
|
||||
|
||||
|
||||
def draw_text(image, ImageDraw, ImageFont):
|
||||
draw = ImageDraw.Draw(image)
|
||||
for size in (10, 9, 8, 7):
|
||||
font = load_font(ImageFont, size)
|
||||
if all(draw.textlength(t, font=font) <= MAX_WIDTH for t in LINES):
|
||||
break
|
||||
for n, text in enumerate(LINES):
|
||||
draw.text((X, Y + n * PITCH), text, font=font, fill=INK)
|
||||
|
||||
|
||||
def write_preview(folder, tile, src_dir, Image):
|
||||
"""The whole 800x600 menu background with the new tile in it, and a zoom of the text."""
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
full = Image.new('RGB', (800, 600))
|
||||
for r, row in enumerate('123'):
|
||||
for c, col in enumerate('abcd'):
|
||||
name = os.path.join(src_dir, '800_%s_%s_loading.tga' % (row, col))
|
||||
t = tile if (r, c) == (0, 0) else Image.open(name).convert('RGB') if os.path.exists(name) else None
|
||||
if t is not None:
|
||||
full.paste(t.convert('RGB'), (c * 256, r * 256))
|
||||
full.save(os.path.join(folder, 'compact_800x600.png'))
|
||||
full.crop((0, 0, 300, 180)).resize((600, 360), Image.LANCZOS).save(os.path.join(folder, 'zoom_header.png'))
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__.split('\n')[0])
|
||||
ap.add_argument('--src', default=DEFAULT_SRC, help='the release tile 800_1_a_loading.tga')
|
||||
ap.add_argument('--out', default=DEFAULT_OUT, help='where the Frame tile is written')
|
||||
ap.add_argument('--preview', metavar='DIR', help='also write a 800x600 preview and a zoom of the text here')
|
||||
args = ap.parse_args()
|
||||
|
||||
try:
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
except ImportError as e:
|
||||
print('make_frame_tile: %s. Needs Pillow and numpy, see the top of this file.' % e, file=sys.stderr)
|
||||
return 3
|
||||
|
||||
if not os.path.isfile(args.src):
|
||||
print('make_frame_tile: %s is not there. It comes from Team Beef\'s release APK '
|
||||
'(assets/background), see docs/STEAM_FRAME_VR.md.' % args.src, file=sys.stderr)
|
||||
return 2
|
||||
|
||||
source = Image.open(args.src)
|
||||
if source.size != (256, 256):
|
||||
print('make_frame_tile: %s is %dx%d, expected 256x256' % (args.src, *source.size), file=sys.stderr)
|
||||
return 2
|
||||
alpha = source.getchannel('A') if 'A' in source.getbands() else None
|
||||
tile = Image.fromarray(clean_tile(np.asarray(source.convert('RGB')), np))
|
||||
draw_text(tile, ImageDraw, ImageFont)
|
||||
if alpha is not None:
|
||||
tile.putalpha(alpha)
|
||||
|
||||
os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
|
||||
tile.save(args.out, format='TGA')
|
||||
print('make_frame_tile: wrote %s' % args.out)
|
||||
if args.preview:
|
||||
write_preview(args.preview, tile, os.path.dirname(os.path.abspath(args.src)), Image)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# One unattended performance run: starts the game with force_render for N seconds with the given
|
||||
# diag keys, then prints the [vr-perf] lines of the run and the Adreno's time at each frequency over it
|
||||
# (devfreq trans_stat, so the share at the top frequency says if the GPU was saturated).
|
||||
#
|
||||
# tools/steam_frame/perf_run.sh --lock-holder NAME [--seconds 70] [--load SAVE | --map MAP] [--diag K=V ...]
|
||||
#
|
||||
# Several runs per setting, and say what the setting was (the first [vr-perf] line has the eye size and the rate):
|
||||
# a single run can be a compositor state, not the setting (lesson 67).
|
||||
set -euo pipefail
|
||||
root="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
frame_host="${FRAME_HOST:-steamos@192.168.1.31}"
|
||||
seconds=70
|
||||
holder=""
|
||||
start=()
|
||||
diag=()
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--lock-holder) holder="$2"; shift 2 ;;
|
||||
--seconds) seconds="$2"; shift 2 ;;
|
||||
--load) start=(--diag "load=$2"); shift 2 ;;
|
||||
--map) start=(--diag "map=$2"); shift 2 ;;
|
||||
--diag) diag+=(--diag "$2"); shift 2 ;;
|
||||
*) echo "unknown option $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
[[ -n "$holder" ]] || { echo "need --lock-holder" >&2; exit 1; }
|
||||
gpu='/sys/class/devfreq/3d00000.gpu/trans_stat'
|
||||
snap() { ssh -o BatchMode=yes "$frame_host" "cat $gpu" 2>/dev/null || true; }
|
||||
|
||||
"$root/tools/steam_frame/deploy.sh" --lock-holder "$holder" --no-install --diag-reset --clear-dumps \
|
||||
--diag force_render=1 --diag perf=1 "${start[@]}" "${diag[@]}" --diag "exit_seconds=$((seconds))" --no-launch >/dev/null 2>&1
|
||||
before="$(snap)"
|
||||
"$root/tools/steam_frame/deploy.sh" --lock-holder "$holder" --no-install --logcat $((seconds + 12)) 2>&1 | grep -E "vr-perf|\[openxr\] (refresh|eye image|frame period)|FORTIFY" || true
|
||||
after="$(snap)"
|
||||
python3 - "$before" "$after" <<'PY'
|
||||
import sys
|
||||
def rows(t):
|
||||
r = {}
|
||||
for line in t.splitlines():
|
||||
line = line.replace("*", " ")
|
||||
if ":" in line and line.strip()[0].isdigit():
|
||||
head, _, rest = line.partition(":")
|
||||
try:
|
||||
r[int(head.strip())] = int(rest.split()[-1])
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
return r
|
||||
b, a = rows(sys.argv[1]), rows(sys.argv[2])
|
||||
if a and b:
|
||||
d = {k: a[k] - b.get(k, 0) for k in a}
|
||||
total = sum(d.values()) or 1
|
||||
top = max(d)
|
||||
print("gpu devfreq time at each frequency over the run (Hz: ms, share):")
|
||||
for k in sorted(d):
|
||||
if d[k]:
|
||||
print(" %d: %d ms, %.1f%%" % (k, d[k], 100.0 * d[k] / total))
|
||||
print("gpu at the top frequency %d: %.1f%%" % (top, 100.0 * d[top] / total))
|
||||
else:
|
||||
print("no devfreq trans_stat")
|
||||
PY
|
||||
Executable
+148
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Binding table sanity for the Steam Frame build (Projects/Android/jni/src/Lambda1VR/OpenXrInput.c).
|
||||
|
||||
A runtime refuses a whole interaction profile when one suggested path isn't a component of it, and it
|
||||
refuses to create an action whose name or localized name is already taken in the action set (both have
|
||||
cost other ports their input). This checks, without a headset:
|
||||
|
||||
- every action and localized name given to CreateAction() is unique (names are lower case);
|
||||
- every path in the Frame and Index tables, and in the Touch block, is a component of that profile
|
||||
(the component lists are the OpenXR spec's and Valve's; the Frame's is what the other Steam Frame
|
||||
ports bind: ABXY, Menu and the right bumper on the right controller, View and the D-pad on the left);
|
||||
- every action in a table was created, and the Frame profile binds what the game needs.
|
||||
|
||||
Run: tools/steam_frame/tests/check_bindings.py [OpenXrInput.c] (exit 1 on a problem)
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SRC = Path(__file__).resolve().parents[3] / "Projects/Android/jni/src/Lambda1VR/OpenXrInput.c"
|
||||
|
||||
POSES = {"grip/pose", "aim/pose"}
|
||||
HAPTIC = {"output/haptic"}
|
||||
STICK = {"thumbstick", "thumbstick/x", "thumbstick/y", "thumbstick/click", "thumbstick/touch"}
|
||||
PAD = {"trackpad", "trackpad/x", "trackpad/y", "trackpad/touch"}
|
||||
|
||||
PROFILES = {
|
||||
"/interaction_profiles/oculus/touch_controller": (
|
||||
{"left", "right"}, {"x/click", "x/touch", "y/click", "y/touch", "menu/click", "a/click", "a/touch", "b/click",
|
||||
"b/touch", "squeeze/value", "trigger/value", "trigger/touch", "thumbrest/touch"}
|
||||
| STICK | POSES | HAPTIC),
|
||||
"/interaction_profiles/valve/index_controller": (
|
||||
{"left", "right"}, {"system/click", "system/touch", "a/click", "a/touch", "b/click", "b/touch",
|
||||
"squeeze/value", "squeeze/force", "trigger/click", "trigger/value", "trigger/touch",
|
||||
"trackpad/force"} | STICK | PAD | POSES | HAPTIC),
|
||||
"/interaction_profiles/valve/frame_controller_valve": (
|
||||
{"left", "right"}, {"a/click", "b/click", "x/click", "y/click", "menu/click", "view/click", "bumper/click",
|
||||
"trigger/value", "trigger/click", "squeeze/value", "dpad_up/click", "dpad_down/click",
|
||||
"dpad_left/click", "dpad_right/click"} | STICK | POSES | HAPTIC),
|
||||
}
|
||||
# The Frame is a split gamepad: which hand has which button
|
||||
FRAME_HAND = {"a/click": "right", "b/click": "right", "x/click": "right", "y/click": "right", "menu/click": "right",
|
||||
"view/click": "left", "dpad_up/click": "left", "dpad_down/click": "left", "dpad_left/click": "left",
|
||||
"dpad_right/click": "left"}
|
||||
# What the game needs from the Frame profile (the layout in TBXR_UpdateFrameLayout)
|
||||
FRAME_NEEDS = ["touchpadAction", "joystickAction", "thumbstickTouchAction", "triggerAction", "GripAction",
|
||||
"poseAction", "aimAction", "vibrateAction", "AAction", "BAction", "XAction", "YAction",
|
||||
"backAction", "viewAction", "bumperAction", "dpadUpAction", "dpadDownAction", "dpadLeftAction",
|
||||
"dpadRightAction"]
|
||||
|
||||
|
||||
def main():
|
||||
text = (Path(sys.argv[1]) if len(sys.argv) > 1 else SRC).read_text()
|
||||
bad = 0
|
||||
|
||||
def problem(msg):
|
||||
nonlocal bad
|
||||
print("BAD " + msg)
|
||||
bad += 1
|
||||
|
||||
# ---- actions: unique names, unique localized names
|
||||
created = {}
|
||||
names, localized = {}, {}
|
||||
for m in re.finditer(r'CreateAction\(actionSet,\s*\w+,\s*"([^"]+)",\s*"([^"]+)",\s*SIDE_COUNT,\s*handSubactionPath,\s*&(\w+)\)', text):
|
||||
name, loc, var = m.groups()
|
||||
created[var] = name
|
||||
if not re.fullmatch(r"[a-z0-9_.-]+", name):
|
||||
problem(f"action name '{name}' has characters other than a-z 0-9 _ . -")
|
||||
if name.lower() in names:
|
||||
problem(f"action name '{name}' is used twice (also by {names[name.lower()]})")
|
||||
if loc.lower() in localized:
|
||||
problem(f"localized name '{loc}' is used twice (also by {localized[loc.lower()]}): XR_ERROR_LOCALIZED_NAME_DUPLICATED")
|
||||
names[name.lower()] = var
|
||||
localized[loc.lower()] = var
|
||||
if not created:
|
||||
problem("no CreateAction calls found")
|
||||
|
||||
# ---- the Frame and Index tables
|
||||
suggested = {}
|
||||
for m in re.finditer(r'TBXR_SuggestBindings\((\w+|"[^"]+"),\s*(\w+),', text):
|
||||
suggested[m.group(2)] = m.group(1)
|
||||
defines = dict(re.findall(r'#define (\w+_PROFILE) "([^"]+)"', text))
|
||||
tables = {}
|
||||
for m in re.finditer(r"static const profileBinding_t (\w+)\[\] = \{(.*?)\n\};", text, re.S):
|
||||
tables[m.group(1)] = m.group(2)
|
||||
|
||||
checked = 0
|
||||
for table, profile_ref in suggested.items():
|
||||
profile = defines.get(profile_ref, profile_ref.strip('"'))
|
||||
if table not in tables:
|
||||
problem(f"table {table} not found")
|
||||
continue
|
||||
hands, comps = PROFILES.get(profile, (None, None))
|
||||
if comps is None:
|
||||
problem(f"{profile}: no component list in this test")
|
||||
continue
|
||||
checked += 1
|
||||
actions = set()
|
||||
for line in tables[table].splitlines():
|
||||
line = line.split("//")[0]
|
||||
e = re.search(r'\{\s*&(\w+),\s*"(/user/hand/(\w+)/(input|output)/([\w/]+))"\s*\}', line)
|
||||
if not e:
|
||||
continue
|
||||
action, path, hand, kind, comp = e.groups()
|
||||
comp = comp if kind == "input" else "output/" + comp
|
||||
actions.add(action)
|
||||
if action not in created:
|
||||
problem(f"{profile}: {action} is never created")
|
||||
if hand not in hands or comp not in comps:
|
||||
problem(f"{profile}: {path} is not a component of that profile")
|
||||
if profile.endswith("frame_controller_valve") and comp in FRAME_HAND and FRAME_HAND[comp] != hand:
|
||||
problem(f"{profile}: {path}: the Frame has {comp} on the {FRAME_HAND[comp]} controller")
|
||||
if profile.endswith("frame_controller_valve"):
|
||||
for need in FRAME_NEEDS:
|
||||
if need not in actions:
|
||||
problem(f"the Frame profile doesn't bind {need}")
|
||||
if not any(p.endswith("frame_controller_valve") for p in (defines.get(r, r.strip('"')) for r in suggested.values())):
|
||||
problem("no Steam Frame profile suggested")
|
||||
|
||||
# ---- the Touch block (paths come from the xrStringToPath lines)
|
||||
var_path = {}
|
||||
for m in re.finditer(r'xrStringToPath\(gAppState\.Instance,\s*"(/user/hand/(?:left|right)/[\w/]+)",\s*&(\w+)\[SIDE_(LEFT|RIGHT)\]', text):
|
||||
var_path[(m.group(2), m.group(3))] = m.group(1)
|
||||
touch = re.search(r'touchControllerInteractionProfilePath\)\);(.*?)xrSuggestInteractionProfileBindings', text, re.S)
|
||||
if touch:
|
||||
hands, comps = PROFILES["/interaction_profiles/oculus/touch_controller"]
|
||||
checked += 1
|
||||
for m in re.finditer(r"ActionSuggestedBinding\((\w+),\s*(\w+)\[SIDE_(LEFT|RIGHT)\]\)", touch.group(1)):
|
||||
action, var, side = m.groups()
|
||||
path = var_path.get((var, side))
|
||||
if path is None:
|
||||
problem(f"Touch: no path for {var}[SIDE_{side}]")
|
||||
continue
|
||||
hand, kind, comp = re.match(r"/user/hand/(\w+)/(input|output)/([\w/]+)", path).groups()
|
||||
comp = comp if kind == "input" else "output/" + comp
|
||||
if comp not in comps:
|
||||
problem(f"Touch: {path} is not a component of that profile")
|
||||
if action not in created:
|
||||
problem(f"Touch: {action} is never created")
|
||||
else:
|
||||
problem("Touch block not found")
|
||||
|
||||
print(f"checked {checked} profiles and {len(created)} actions: {'FAILED' if bad else 'ok'}")
|
||||
return 1 if bad else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# Host tests for the Steam Frame code that needs no headset.
|
||||
# tools/steam_frame/tests/run_tests.sh
|
||||
set -euo pipefail
|
||||
here="$(cd "$(dirname "$0")" && pwd)"
|
||||
root="$(cd "$here/../../.." && pwd)"
|
||||
vr="$root/Projects/Android/jni/src/Lambda1VR"
|
||||
menu="$root/Projects/Android/jni/src/Xash3D/xash3d/mainui/menus"
|
||||
out="$(mktemp -d)"
|
||||
trap 'rm -rf "$out"' EXIT
|
||||
|
||||
# binding tables and action names against the profiles' component lists
|
||||
python3 "$here/check_bindings.py"
|
||||
|
||||
# LAN listen servers must always have room for the host and one remote player.
|
||||
cxx="${CXX:-clang++}"
|
||||
"$cxx" -std=c++11 -Wall -Wextra -Werror -I"$menu" "$here/test_host_game_options.cpp" -o "$out/test_host_game_options"
|
||||
"$out/test_host_game_options"
|
||||
|
||||
# vr-diag.ini parser and the BMP writer
|
||||
cc="${CC:-clang}"
|
||||
"$cc" -std=c11 -Wall -Wextra -Werror -I"$vr" "$here/test_diag_core.c" "$vr/VrDiagCore.c" "$vr/VrFrameMap.c" -lm -o "$out/test_diag_core"
|
||||
"$out/test_diag_core"
|
||||
|
||||
# eye offsets, cant, the HUD shift against a projected point, the layer pose
|
||||
"$cc" -std=c11 -Wall -Wextra -Werror -I"$vr" "$here/test_eye_math.c" "$vr/VrEyeMath.c" -lm -o "$out/test_eye_math"
|
||||
"$out/test_eye_math"
|
||||
|
||||
# the menu laser: the ray against the screen quad, the beam and dot poses
|
||||
"$cc" -std=c11 -Wall -Wextra -Werror -I"$vr" "$here/test_pointer_math.c" "$vr/VrPointerMath.c" -lm -o "$out/test_pointer_math"
|
||||
"$out/test_pointer_math"
|
||||
|
||||
# the controller map: no input doing two things, every action reachable, aliases only where listed
|
||||
"$cc" -std=c11 -Wall -Wextra -Werror -I"$vr" "$here/test_frame_map.c" "$vr/VrFrameMap.c" -o "$out/test_frame_map"
|
||||
"$out/test_frame_map"
|
||||
|
||||
# deploy.sh: syntax, and it must refuse to install or launch without the booking
|
||||
bash -n "$root/tools/steam_frame/deploy.sh"
|
||||
fake="$out/frame-lock.sh"
|
||||
printf '#!/bin/sh\necho "HELD by someone-else for 5m0s more: testing (since 10:00)"\n' > "$fake"
|
||||
chmod +x "$fake"
|
||||
if FRAME_LOCK="$fake" "$root/tools/steam_frame/deploy.sh" --lock-holder me --apk /nonexistent.apk >/dev/null 2>&1; then
|
||||
echo "deploy.sh went on without holding the Frame booking"; exit 1
|
||||
fi
|
||||
if FRAME_LOCK="$fake" "$root/tools/steam_frame/deploy.sh" --apk /nonexistent.apk >/dev/null 2>&1; then
|
||||
echo "deploy.sh went on without --lock-holder"; exit 1
|
||||
fi
|
||||
echo "deploy.sh lock check: ok"
|
||||
@@ -0,0 +1,102 @@
|
||||
// Host test for the vr-diag.ini parser and the BMP writer (VrDiagCore.c).
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "VrDiagCore.h"
|
||||
#include "VrFrameMap.h"
|
||||
|
||||
static int failures = 0;
|
||||
#define CHECK(cond) do { if (!(cond)) { printf("FAIL line %d: %s\n", __LINE__, #cond); failures++; } } while (0)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
vrDiagConfig_t c;
|
||||
|
||||
// nothing in the file: everything off, the perf line on
|
||||
VrDiagCore_Defaults(&c);
|
||||
CHECK(VrDiagCore_Parse("", &c) == 0);
|
||||
CHECK(!c.forceRender && !c.autostart && c.dumpFrame == 0 && c.exitAfter == 0 && c.perf == 1 && !c.oneshot);
|
||||
|
||||
// a normal file, with spaces, other case, comments, CRLF
|
||||
VrDiagCore_Defaults(&c);
|
||||
const char* text =
|
||||
"# an unattended run\r\n"
|
||||
"force_render = 1\r\n"
|
||||
" DIAG_YAW=90.5 \r\n"
|
||||
"; another comment\n"
|
||||
"autostart = on\n"
|
||||
"dump_frame = 600\n"
|
||||
"exit_after=1800\n"
|
||||
"perf = 0\n"
|
||||
"probe = 1\n"
|
||||
"cmd = sv_cheats 1; impulse 101\n"
|
||||
"diag_roll = -20\n"
|
||||
"diag_gap = 0.8\n"
|
||||
"diag_press = A@300+30, lg@400+20 bogus@1+1 B@5\n"
|
||||
"oneshot = true\n";
|
||||
CHECK(VrDiagCore_Parse(text, &c) == 12);
|
||||
CHECK(fabsf(c.gap - 0.8f) < 0.001f);
|
||||
CHECK(c.pressCount == 2);
|
||||
CHECK(c.presses[0].input == FI_A && c.presses[0].start == 300 && c.presses[0].length == 30);
|
||||
CHECK(c.presses[1].input == FI_L_GRIP && c.presses[1].start == 400 && c.presses[1].length == 20);
|
||||
CHECK(fabsf(c.roll + 20.0f) < 0.001f);
|
||||
CHECK(c.probe == 1);
|
||||
CHECK(strcmp(c.cmd, "sv_cheats 1; impulse 101") == 0);
|
||||
CHECK(c.forceRender == 1);
|
||||
CHECK(fabsf(c.yaw - 90.5f) < 0.001f);
|
||||
CHECK(c.autostart == 1 && c.map[0] == '\0');
|
||||
CHECK(c.dumpFrame == 600 && c.exitAfter == 1800);
|
||||
CHECK(c.perf == 0 && c.oneshot == 1);
|
||||
|
||||
// a map starts the game by itself
|
||||
VrDiagCore_Defaults(&c);
|
||||
CHECK(VrDiagCore_Parse("map = c1a0\n", &c) == 1);
|
||||
CHECK(c.autostart == 1 && strcmp(c.map, "c1a0") == 0);
|
||||
|
||||
// junk is ignored, a bad value leaves the default, negatives don't turn things on
|
||||
VrDiagCore_Defaults(&c);
|
||||
CHECK(VrDiagCore_Parse("bogus = 1\nforce_render = maybe\nno equals sign\ndump_frame = -5\nexit_after = abc\n= 3\n", &c) == 1);
|
||||
CHECK(c.forceRender == 0 && c.dumpFrame == 0 && c.exitAfter == 0);
|
||||
|
||||
// a long map name is cut, not overrun
|
||||
VrDiagCore_Defaults(&c);
|
||||
char longName[200];
|
||||
memset(longName, 'a', sizeof(longName));
|
||||
strcpy(longName + 150, "\n");
|
||||
char line[300];
|
||||
snprintf(line, sizeof(line), "map = %s", longName);
|
||||
VrDiagCore_Parse(line, &c);
|
||||
CHECK(strlen(c.map) == sizeof(c.map) - 1);
|
||||
|
||||
// the BMP: 2x2, rows bottom to top, RGBA in, BGR out, rows padded to 4 bytes
|
||||
const unsigned char rgba[2 * 2 * 4] = {
|
||||
10, 20, 30, 255, 40, 50, 60, 255, // bottom row
|
||||
70, 80, 90, 255, 100, 110, 120, 255, // top row
|
||||
};
|
||||
char path[] = "/tmp/vrdiag-test.bmp";
|
||||
CHECK(VrDiagCore_WriteBmp(path, rgba, 2, 2));
|
||||
FILE* f = fopen(path, "rb");
|
||||
CHECK(f != NULL);
|
||||
if (f)
|
||||
{
|
||||
unsigned char b[70];
|
||||
size_t n = fread(b, 1, sizeof(b), f);
|
||||
fclose(f);
|
||||
CHECK(n == 54 + 16);
|
||||
CHECK(b[0] == 'B' && b[1] == 'M');
|
||||
CHECK(b[2] == 70 && b[10] == 54 && b[14] == 40);
|
||||
CHECK(b[18] == 2 && b[22] == 2 && b[26] == 1 && b[28] == 24);
|
||||
CHECK(b[54] == 30 && b[55] == 20 && b[56] == 10); // first pixel, bottom left, as BGR
|
||||
CHECK(b[57] == 60 && b[58] == 50 && b[59] == 40);
|
||||
CHECK(b[60] == 0 && b[61] == 0); // row padding
|
||||
CHECK(b[62] == 90 && b[63] == 80 && b[64] == 70); // next row
|
||||
}
|
||||
remove(path);
|
||||
CHECK(!VrDiagCore_WriteBmp("/tmp/no-such-dir/x.bmp", rgba, 2, 2));
|
||||
CHECK(!VrDiagCore_WriteBmp(path, rgba, 0, 2));
|
||||
|
||||
printf(failures ? "diag core: FAILED (%d)\n" : "diag core: ok\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// Host test for the eye helpers (VrEyeMath.c): eye offsets, the cant, the HUD shift and the
|
||||
// pose the layer is submitted with. The HUD check is the one that matters: the shifted 2D
|
||||
// content has to land exactly where a point at that distance lands when projected through
|
||||
// the same asymmetric frustum from the same eye.
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "VrEyeMath.h"
|
||||
|
||||
static int failures = 0;
|
||||
#define CHECK(cond) do { if (!(cond)) { printf("FAIL line %d: %s\n", __LINE__, #cond); failures++; } } while (0)
|
||||
#define NEAR(a, b, eps) (fabsf((a) - (b)) <= (eps))
|
||||
|
||||
int main(void)
|
||||
{
|
||||
// the Frame's recommended image and a typical pair of asymmetric views
|
||||
const int W = 1728;
|
||||
const float L0 = -1.028f, R0 = 0.885f; // left eye
|
||||
const float L1 = -0.877f, R1 = 1.032f; // right eye
|
||||
const float eyeX0 = -0.0315f, eyeX1 = 0.0315f;
|
||||
|
||||
// ---- offsets: an eye to the left of the head is to the engine's left (+y)
|
||||
{
|
||||
const float left[3] = {-0.0315f, 0.0f, 0.0f};
|
||||
const float right[3] = {0.0315f, 0.0f, 0.0f};
|
||||
float o[3];
|
||||
VrEye_ToEngineOffset(left, 40.0f, o);
|
||||
CHECK(NEAR(o[0], 0.0f, 1e-6f) && NEAR(o[1], 1.26f, 1e-4f) && NEAR(o[2], 0.0f, 1e-6f));
|
||||
VrEye_ToEngineOffset(right, 40.0f, o);
|
||||
CHECK(NEAR(o[1], -1.26f, 1e-4f));
|
||||
// forward (-z) is +x, up is +z
|
||||
const float fwd[3] = {0.0f, 0.01f, -0.02f};
|
||||
VrEye_ToEngineOffset(fwd, 100.0f, o);
|
||||
CHECK(NEAR(o[0], 2.0f, 1e-4f) && NEAR(o[2], 1.0f, 1e-4f));
|
||||
}
|
||||
|
||||
// ---- cant: none, then 5 degrees about the up axis (OpenXR y), then the shorter way round
|
||||
{
|
||||
float axis[3], deg;
|
||||
const float none[4] = {0, 0, 0, 1};
|
||||
CHECK(!VrEye_CantAxisAngle(none, axis, °));
|
||||
const float zero[4] = {0, 0, 0, 0};
|
||||
CHECK(!VrEye_CantAxisAngle(zero, axis, °));
|
||||
|
||||
const float half = 2.5f * (float)M_PI / 180.0f;
|
||||
const float yaw5[4] = {0, sinf(half), 0, cosf(half)};
|
||||
CHECK(VrEye_CantAxisAngle(yaw5, axis, °));
|
||||
CHECK(NEAR(deg, 5.0f, 1e-3f));
|
||||
// up is z in the engine's axes
|
||||
CHECK(NEAR(axis[0], 0.0f, 1e-5f) && NEAR(axis[1], 0.0f, 1e-5f) && NEAR(axis[2], 1.0f, 1e-5f));
|
||||
|
||||
const float yaw5neg[4] = {0, -sinf(half), 0, -cosf(half)}; // the same turn, other sign of q
|
||||
CHECK(VrEye_CantAxisAngle(yaw5neg, axis, °));
|
||||
CHECK(NEAR(deg, 5.0f, 1e-3f) && NEAR(axis[2], 1.0f, 1e-5f));
|
||||
|
||||
// about x (right) is the engine's -y (left): pitch up 3 degrees
|
||||
const float h3 = 1.5f * (float)M_PI / 180.0f;
|
||||
const float pitch3[4] = {sinf(h3), 0, 0, cosf(h3)};
|
||||
CHECK(VrEye_CantAxisAngle(pitch3, axis, °));
|
||||
CHECK(NEAR(deg, 3.0f, 1e-3f) && NEAR(axis[1], -1.0f, 1e-5f));
|
||||
}
|
||||
|
||||
// ---- the axis pixel is where straight ahead lands
|
||||
{
|
||||
const float axis0 = VrEye_AxisPixel(L0, R0, W);
|
||||
CHECK(NEAR(VrEye_ProjectX(0.0f, 0.0f, -10.0f, L0, R0, W), axis0, 1e-2f));
|
||||
// the formula VR_Get2DOffset uses: centre plus dx
|
||||
const float tanL = tanf(L0), tanR = tanf(R0);
|
||||
const float dx = -(tanR + tanL) / (2.0f * (tanR - tanL)) * (float)W;
|
||||
CHECK(NEAR(W / 2.0f + dx, axis0, 1e-2f));
|
||||
}
|
||||
|
||||
// ---- the HUD: shifted 2D content equals a world point at that distance, both eyes
|
||||
{
|
||||
const float distances[] = {0.5f, 1.0f, 2.0f, 5.0f};
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
const float D = distances[i];
|
||||
const float s0 = VrEye_HudShiftPixels(eyeX0, L0, R0, W, D);
|
||||
const float s1 = VrEye_HudShiftPixels(eyeX1, L1, R1, W, D);
|
||||
|
||||
// 2D content anchored at the buffer centre: axis offset, then the shift
|
||||
const float tanL0 = tanf(L0), tanR0 = tanf(R0), tanL1 = tanf(L1), tanR1 = tanf(R1);
|
||||
const float u0 = W / 2.0f - (tanR0 + tanL0) / (2.0f * (tanR0 - tanL0)) * W + s0;
|
||||
const float u1 = W / 2.0f - (tanR1 + tanL1) / (2.0f * (tanR1 - tanL1)) * W + s1;
|
||||
CHECK(NEAR(u0, VrEye_ProjectX(eyeX0, 0.0f, -D, L0, R0, W), 0.05f));
|
||||
CHECK(NEAR(u1, VrEye_ProjectX(eyeX1, 0.0f, -D, L1, R1, W), 0.05f));
|
||||
|
||||
// the left eye's content moves right, the right eye's left
|
||||
CHECK(s0 > 0.0f && s1 < 0.0f);
|
||||
}
|
||||
// and the old fixed shift (width / 36 = 48 px) really was about 0.4 m
|
||||
float D = 0.0f;
|
||||
for (float d = 0.1f; d < 3.0f; d += 0.01f)
|
||||
if (VrEye_HudShiftPixels(eyeX0, L0, R0, W, d) > 48.0f) D = d;
|
||||
CHECK(D > 0.3f && D < 0.5f);
|
||||
// no distance, no shift
|
||||
CHECK(VrEye_HudShiftPixels(eyeX0, L0, R0, W, 0.0f) == 0.0f);
|
||||
}
|
||||
|
||||
// ---- disparity of world points: IPD * f / depth, and nothing at infinity
|
||||
{
|
||||
const float f = VrEye_FocalPixels(L0, R0, W);
|
||||
const float ipd = eyeX1 - eyeX0;
|
||||
const float depths[] = {0.5f, 1.0f, 3.0f, 20.0f, 1000.0f};
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
const float z = -depths[i];
|
||||
const float uL = VrEye_ProjectX(eyeX0, 0.0f, z, L0, R0, W);
|
||||
const float uR = VrEye_ProjectX(eyeX1, 0.0f, z, L0, R0, W);
|
||||
CHECK(NEAR(uL - uR, ipd * f / depths[i], 1e-2f + 1e-5f * ipd * f / depths[i]));
|
||||
}
|
||||
CHECK(NEAR(VrEye_ProjectX(eyeX0, 0.0f, -1e6f, L0, R0, W) - VrEye_ProjectX(eyeX1, 0.0f, -1e6f, L0, R0, W), 0.0f, 0.1f));
|
||||
}
|
||||
|
||||
// ---- the pose a layer view is submitted with: head placed in the stage, eye placed in the head
|
||||
{
|
||||
const float s = sqrtf(0.5f);
|
||||
// head 1.6 m up, 2 m to the right of the origin, turned 90 degrees left (about y)
|
||||
const float head[7] = {2.0f, 1.6f, 0.0f, 0.0f, s, 0.0f, s};
|
||||
// eye 32 mm to the head's left, no cant
|
||||
const float eye[7] = {-0.032f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f};
|
||||
float out[7];
|
||||
VrEye_ComposePose(head, eye, out);
|
||||
// facing left, the head's left is the stage's +z... turned 90 deg left, the head's -x
|
||||
// becomes the stage's +z
|
||||
CHECK(NEAR(out[0], 2.0f, 1e-5f) && NEAR(out[1], 1.6f, 1e-5f) && NEAR(out[2], 0.032f, 1e-5f));
|
||||
CHECK(NEAR(out[3], 0.0f, 1e-6f) && NEAR(out[4], s, 1e-5f) && NEAR(out[6], s, 1e-5f));
|
||||
|
||||
// with the eye's own cant of 5 degrees about y: the orientations add
|
||||
const float half = 2.5f * (float)M_PI / 180.0f;
|
||||
const float eyeCant[7] = {0, 0, 0, 0, sinf(half), 0, cosf(half)};
|
||||
VrEye_ComposePose(head, eyeCant, out);
|
||||
const float total = (90.0f + 5.0f) * 0.5f * (float)M_PI / 180.0f;
|
||||
CHECK(NEAR(out[4], sinf(total), 1e-5f) && NEAR(out[6], cosf(total), 1e-5f));
|
||||
}
|
||||
|
||||
printf(failures ? "eye math: FAILED (%d)\n" : "eye math: ok\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
// Host test for the Steam Frame controller map (VrFrameMap.c). It builds the effective table, physical
|
||||
// input to game action, for both hands and every context (playing, the hands behind the head, a
|
||||
// menu, multiplayer), and fails on:
|
||||
// - one physical input doing two different things in the same context;
|
||||
// - an action with two inputs that isn't a documented alias;
|
||||
// - an action that nothing can reach;
|
||||
// - a grip or trigger doing something that isn't a hand's job;
|
||||
// - the right stick's up or down doing anything in the game (it only turns, and scrolls in a menu);
|
||||
// - a button for the flat screen view (that's the vr_flat_screen setting) or the D-pad crowbar.
|
||||
// With --markdown it prints the table for the docs.
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "VrFrameMap.h"
|
||||
|
||||
static int failures = 0;
|
||||
#define FAIL(...) do { printf("FAIL: " __VA_ARGS__); printf("\n"); failures++; } while (0)
|
||||
|
||||
// actions that may have more than one input in a context, because they are meant to
|
||||
static bool aliasAllowed(int action)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case ACT_USE: // X and the weapon hand's grip
|
||||
case ACT_PREV_WEAPON: // left bumper and D-pad left
|
||||
case ACT_NEXT_WEAPON: // right bumper and D-pad right
|
||||
case ACT_FLASHLIGHT: // right stick click and View
|
||||
case ACT_QUICKSAVE: // hold Menu; with the hand behind the head the right stick click
|
||||
case ACT_MENU_BACK: // B, Menu, View
|
||||
case ACT_MENU_UP: case ACT_MENU_DOWN: case ACT_MENU_LEFT: case ACT_MENU_RIGHT: // D-pad and left stick
|
||||
case ACT_MOVE: case ACT_TURN: // four stick directions
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool isHandInput(int input)
|
||||
{
|
||||
return input == FI_L_GRIP || input == FI_R_GRIP;
|
||||
}
|
||||
|
||||
static bool handAction(int action)
|
||||
{
|
||||
return action == ACT_BACKPACK || action == ACT_USE || action == ACT_USE_OFF || action == ACT_STEADY || action == ACT_NONE;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
int rowCount;
|
||||
const frameMapRow_t* rows = VrFrameMap_Rows(&rowCount);
|
||||
|
||||
bool reached[ACT_COUNT] = {false};
|
||||
|
||||
for (int hand = 0; hand < 2; hand++)
|
||||
{
|
||||
const bool left = hand == 1;
|
||||
for (int context = 0; context < 32; context++)
|
||||
{
|
||||
// a menu has no hands behind the head
|
||||
if ((context & CTX_MENU) && (context & (CTX_DOM_ZONE | CTX_OFF_ZONE)))
|
||||
continue;
|
||||
// quick save and load aren't offered in multiplayer
|
||||
if ((context & CTX_MULTI) && (context & CTX_OFF_ZONE))
|
||||
continue;
|
||||
|
||||
int inputsOfAction[ACT_COUNT] = {0};
|
||||
for (int input = 0; input < FI_COUNT; input++)
|
||||
{
|
||||
// overlapping rows: how many match this input in this context
|
||||
int matches = 0, firstAction = ACT_NONE;
|
||||
for (int r = 0; r < rowCount; r++)
|
||||
{
|
||||
const int rowInput = rows[r].role ? VrFrameMap_RoleInput(rows[r].role, left) : rows[r].input;
|
||||
if (rowInput != input) continue;
|
||||
if ((context & rows[r].require) != rows[r].require || (context & rows[r].forbid) != 0) continue;
|
||||
if (matches++ == 0) firstAction = rows[r].action;
|
||||
else FAIL("%s: %s does %s and %s (context %d, %s)", left ? "left-handed" : "right-handed",
|
||||
VrFrameMap_InputName(input), VrFrameMap_ActionName(firstAction), VrFrameMap_ActionName(rows[r].action),
|
||||
context, "rows overlap");
|
||||
}
|
||||
const int action = VrFrameMap_Action(input, context, left);
|
||||
if (action != firstAction) FAIL("Action() and the rows disagree for %s", VrFrameMap_InputName(input));
|
||||
if (action == ACT_NONE) continue;
|
||||
|
||||
inputsOfAction[action]++;
|
||||
reached[action] = true;
|
||||
if (isHandInput(input) && !handAction(action))
|
||||
FAIL("%s does %s, which isn't a hand's job", VrFrameMap_InputName(input), VrFrameMap_ActionName(action));
|
||||
}
|
||||
for (int action = 1; action < ACT_COUNT; action++)
|
||||
if (inputsOfAction[action] > 1 && !aliasAllowed(action))
|
||||
FAIL("%s: %s has %d inputs in context %d and isn't a listed alias", left ? "left-handed" : "right-handed",
|
||||
VrFrameMap_ActionName(action), inputsOfAction[action], context);
|
||||
}
|
||||
}
|
||||
|
||||
// everything must be reachable somewhere
|
||||
for (int action = 1; action < ACT_COUNT; action++)
|
||||
if (!reached[action]) FAIL("nothing does %s", VrFrameMap_ActionName(action));
|
||||
|
||||
// the hands: face buttons, bumpers, D-pad, Menu, View and sticks don't change with the weapon hand;
|
||||
// the weapon hand's trigger fires and its grip is the interaction / backpack one
|
||||
for (int context = 0; context < 32; context++)
|
||||
{
|
||||
for (int input = 0; input < FI_COUNT; input++)
|
||||
{
|
||||
if (input == FI_L_TRIGGER || input == FI_R_TRIGGER || input == FI_L_GRIP || input == FI_R_GRIP) continue;
|
||||
if (VrFrameMap_Action(input, context, false) != VrFrameMap_Action(input, context, true))
|
||||
FAIL("%s changes with the hand in context %d", VrFrameMap_InputName(input), context);
|
||||
}
|
||||
if (!(context & CTX_MENU))
|
||||
{
|
||||
if (VrFrameMap_Action(FI_R_TRIGGER, context, false) != ACT_FIRE || VrFrameMap_Action(FI_L_TRIGGER, context, false) != ACT_ALTFIRE)
|
||||
FAIL("right-handed triggers");
|
||||
if (VrFrameMap_Action(FI_L_TRIGGER, context, true) != ACT_FIRE || VrFrameMap_Action(FI_R_TRIGGER, context, true) != ACT_ALTFIRE)
|
||||
FAIL("left-handed triggers");
|
||||
}
|
||||
else if (VrFrameMap_Action(FI_R_TRIGGER, context, false) != ACT_MENU_CLICK || VrFrameMap_Action(FI_L_TRIGGER, context, true) != ACT_MENU_CLICK)
|
||||
FAIL("the laser's trigger in a menu");
|
||||
}
|
||||
|
||||
// the things the user asked for by name
|
||||
struct { int input; int context; int action; } expect[] = {
|
||||
{ FI_A, 0, ACT_JUMP }, { FI_B, 0, ACT_CROUCH_HOLD }, { FI_X, 0, ACT_USE }, { FI_Y, 0, ACT_RELOAD },
|
||||
{ FI_L_STICK_CLICK, 0, ACT_CROUCH_TOGGLE }, { FI_R_STICK_CLICK, 0, ACT_FLASHLIGHT },
|
||||
{ FI_L_BUMPER, 0, ACT_PREV_WEAPON }, { FI_R_BUMPER, 0, ACT_NEXT_WEAPON },
|
||||
{ FI_DPAD_LEFT, 0, ACT_PREV_WEAPON }, { FI_DPAD_RIGHT, 0, ACT_NEXT_WEAPON },
|
||||
{ FI_DPAD_UP, 0, ACT_LAST_WEAPON }, { FI_DPAD_DOWN, 0, ACT_LASER },
|
||||
{ FI_VIEW_SHORT, CTX_OFF_ZONE, ACT_QUICKLOAD }, { FI_R_STICK_CLICK, CTX_OFF_ZONE, ACT_QUICKSAVE },
|
||||
{ FI_VIEW_SHORT, CTX_MULTI, ACT_SCOREBOARD },
|
||||
{ FI_MENU_SHORT, 0, ACT_PAUSE }, { FI_MENU_HOLD, 0, ACT_QUICKSAVE },
|
||||
{ FI_VIEW_SHORT, 0, ACT_FLASHLIGHT }, { FI_VIEW_HOLD_1S, 0, ACT_RECENTRE }, { FI_VIEW_HOLD_3S, 0, ACT_RECENTRE_HEIGHT },
|
||||
{ FI_R_GRIP, 0, ACT_USE }, { FI_R_GRIP, CTX_DOM_ZONE, ACT_BACKPACK },
|
||||
{ FI_L_GRIP, 0, ACT_STEADY }, { FI_L_GRIP, CTX_OFF_GRIP_AWAY, ACT_USE_OFF },
|
||||
{ FI_A, CTX_MENU, ACT_MENU_CONFIRM }, { FI_B, CTX_MENU, ACT_MENU_BACK },
|
||||
{ FI_DPAD_UP, CTX_MENU, ACT_MENU_UP }, { FI_L_STICK_UP, CTX_MENU, ACT_MENU_UP },
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(expect) / sizeof(expect[0]); i++)
|
||||
if (VrFrameMap_Action(expect[i].input, expect[i].context, false) != expect[i].action)
|
||||
FAIL("%s should be %s (context %d)", VrFrameMap_InputName(expect[i].input), VrFrameMap_ActionName(expect[i].action), expect[i].context);
|
||||
|
||||
// the right stick's up and down are not bound in the game, in any context outside a menu (and the
|
||||
// flat screen view has no action to reach, so no input can do it)
|
||||
for (int hand = 0; hand < 2; hand++)
|
||||
for (int context = 0; context < 32; context++)
|
||||
{
|
||||
if (context & CTX_MENU) continue;
|
||||
if (VrFrameMap_Action(FI_R_STICK_UP, context, hand == 1) != ACT_NONE)
|
||||
FAIL("right stick up does %s in the game (context %d)", VrFrameMap_ActionName(VrFrameMap_Action(FI_R_STICK_UP, context, hand == 1)), context);
|
||||
if (VrFrameMap_Action(FI_R_STICK_DOWN, context, hand == 1) != ACT_NONE)
|
||||
FAIL("right stick down does %s in the game (context %d)", VrFrameMap_ActionName(VrFrameMap_Action(FI_R_STICK_DOWN, context, hand == 1)), context);
|
||||
}
|
||||
for (int r = 0; r < rowCount; r++)
|
||||
{
|
||||
if (rows[r].require & CTX_MENU) continue;
|
||||
if (rows[r].input == FI_R_STICK_UP || rows[r].input == FI_R_STICK_DOWN)
|
||||
FAIL("a game row for the right stick's %s", VrFrameMap_InputName(rows[r].input));
|
||||
}
|
||||
for (int action = 1; action < ACT_COUNT; action++)
|
||||
if (strstr(VrFrameMap_ActionName(action), "screen view") || strcmp(VrFrameMap_ActionName(action), "crowbar") == 0)
|
||||
FAIL("%s is an action again", VrFrameMap_ActionName(action));
|
||||
// right stick up and down still scroll in a menu
|
||||
if (VrFrameMap_Action(FI_R_STICK_UP, CTX_MENU, false) != ACT_MENU_SCROLL_UP || VrFrameMap_Action(FI_R_STICK_DOWN, CTX_MENU, false) != ACT_MENU_SCROLL_DOWN)
|
||||
FAIL("the right stick should still scroll in a menu");
|
||||
// the crowbar is the grip behind the head, as it was
|
||||
if (VrFrameMap_Action(FI_R_GRIP, CTX_DOM_ZONE, false) != ACT_BACKPACK || VrFrameMap_Action(FI_L_GRIP, CTX_DOM_ZONE, true) != ACT_BACKPACK)
|
||||
FAIL("the grip behind the head should be the backpack weapon");
|
||||
|
||||
if (argc > 1 && strcmp(argv[1], "--markdown") == 0)
|
||||
{
|
||||
printf("| Frame input | Playing | Notes |\n| --- | --- | --- |\n");
|
||||
for (int r = 0; r < rowCount; r++)
|
||||
{
|
||||
if (rows[r].require & CTX_MENU) continue;
|
||||
const char* name = rows[r].role == ROLE_TRIGGER_WEAPON ? "weapon hand trigger" : rows[r].role == ROLE_TRIGGER_OTHER ? "other hand trigger"
|
||||
: rows[r].role == ROLE_GRIP_WEAPON ? "weapon hand grip" : rows[r].role == ROLE_GRIP_OTHER ? "other hand grip" : VrFrameMap_InputName(rows[r].input);
|
||||
printf("| %s | %s | %s |\n", name, VrFrameMap_ActionName(rows[r].action), rows[r].note);
|
||||
}
|
||||
printf("\n| Frame input | In a menu | Notes |\n| --- | --- | --- |\n");
|
||||
for (int r = 0; r < rowCount; r++)
|
||||
{
|
||||
if (!(rows[r].require & CTX_MENU)) continue;
|
||||
const char* name = rows[r].role == ROLE_TRIGGER_WEAPON ? "weapon hand trigger" : VrFrameMap_InputName(rows[r].input);
|
||||
printf("| %s | %s | %s |\n", name, VrFrameMap_ActionName(rows[r].action), rows[r].note);
|
||||
}
|
||||
}
|
||||
|
||||
printf(failures ? "frame map: FAILED (%d)\n" : "frame map: ok\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#include "HostGameOptions.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
int main()
|
||||
{
|
||||
assert( HostGame_NormalizeMaxPlayers( -1 ) == 2 );
|
||||
assert( HostGame_NormalizeMaxPlayers( 0 ) == 2 );
|
||||
assert( HostGame_NormalizeMaxPlayers( 1 ) == 2 );
|
||||
assert( HostGame_NormalizeMaxPlayers( 2 ) == 2 );
|
||||
assert( HostGame_NormalizeMaxPlayers( 12 ) == 12 );
|
||||
assert( HostGame_NormalizeMaxPlayers( 32 ) == 32 );
|
||||
assert( HostGame_NormalizeMaxPlayers( 33 ) == 32 );
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Host test for the menu laser's maths (VrPointerMath.c)
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "VrPointerMath.h"
|
||||
|
||||
static int failures = 0;
|
||||
#define CHECK(cond) do { if (!(cond)) { printf("FAIL line %d: %s\n", __LINE__, #cond); failures++; } } while (0)
|
||||
#define NEAR(a, b, eps) (fabsf((a) - (b)) <= (eps))
|
||||
|
||||
static void rotate(const float q[4], const float v[3], float out[3])
|
||||
{
|
||||
const float x = q[0], y = q[1], z = q[2], w = q[3];
|
||||
const float cx = y * v[2] - z * v[1], cy = z * v[0] - x * v[2], cz = x * v[1] - y * v[0];
|
||||
const float dx = y * cz - z * cy, dy = z * cx - x * cz, dz = x * cy - y * cx;
|
||||
out[0] = v[0] + 2 * (w * cx + dx); out[1] = v[1] + 2 * (w * cy + dy); out[2] = v[2] + 2 * (w * cz + dz);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const float W = 5.0f, H = 4.5f;
|
||||
|
||||
// a quad 4.5 m ahead, 1 m up, facing the viewer, and a hand a little right of and above the middle
|
||||
const float quad[7] = {0.0f, 1.0f, -4.5f, 0, 0, 0, 1};
|
||||
const float hand[3] = {0.3f, 1.2f, -0.3f};
|
||||
const float ahead[3] = {0, 0, -1};
|
||||
float u, v, t;
|
||||
CHECK(VrPointer_RayQuad(hand, ahead, quad, W, H, &u, &v, &t));
|
||||
CHECK(NEAR(u, 0.5f + 0.3f / W, 1e-4f) && NEAR(v, 0.5f - 0.2f / H, 1e-4f) && NEAR(t, 4.2f, 1e-4f));
|
||||
|
||||
// pointing right by 10 degrees moves it right by 4.2 * tan(10)
|
||||
const float right10[3] = {sinf(10.0f * (float)M_PI / 180.0f), 0, -cosf(10.0f * (float)M_PI / 180.0f)};
|
||||
CHECK(VrPointer_RayQuad(hand, right10, quad, W, H, &u, &v, &t));
|
||||
CHECK(NEAR(u, 0.5f + (0.3f + 4.2f * tanf(10.0f * (float)M_PI / 180.0f)) / W, 1e-3f));
|
||||
// the direction needn't be a unit vector
|
||||
const float longAhead[3] = {0, 0, -7};
|
||||
CHECK(VrPointer_RayQuad(hand, longAhead, quad, W, H, &u, &v, &t) && NEAR(t, 4.2f, 1e-4f));
|
||||
|
||||
// pointing off the panel still gives u and v, outside 0..1
|
||||
const float wide[3] = {1.0f, 0, -0.2f};
|
||||
CHECK(VrPointer_RayQuad(hand, wide, quad, W, H, &u, &v, &t) && u > 1.0f);
|
||||
|
||||
// away from it, along it, behind the hand
|
||||
const float back[3] = {0, 0, 1};
|
||||
CHECK(!VrPointer_RayQuad(hand, back, quad, W, H, &u, &v, &t));
|
||||
const float along[3] = {1, 0, 0};
|
||||
CHECK(!VrPointer_RayQuad(hand, along, quad, W, H, &u, &v, &t));
|
||||
const float behindHand[3] = {0.3f, 1.2f, -9.0f};
|
||||
CHECK(!VrPointer_RayQuad(behindHand, ahead, quad, W, H, &u, &v, &t));
|
||||
|
||||
// a quad turned 90 degrees left about y (what the screen does after a snap turn): it sits to the left
|
||||
// of the origin facing +x; a hand at the origin pointing at it
|
||||
const float s = sqrtf(0.5f);
|
||||
const float turned[7] = {-4.5f, 1.0f, 0.0f, 0, s, 0, s};
|
||||
const float toLeft[3] = {-1, 0, 0};
|
||||
const float origin[3] = {0, 1.0f, 0};
|
||||
CHECK(VrPointer_RayQuad(origin, toLeft, turned, W, H, &u, &v, &t));
|
||||
CHECK(NEAR(u, 0.5f, 1e-4f) && NEAR(v, 0.5f, 1e-4f) && NEAR(t, 4.5f, 1e-4f));
|
||||
// aimed a little toward -z: the quad turned left faces +x, so its right is toward -z
|
||||
const float slightly[3] = {-1, 0, -0.1f};
|
||||
CHECK(VrPointer_RayQuad(origin, slightly, turned, W, H, &u, &v, &t));
|
||||
CHECK(u > 0.5f);
|
||||
|
||||
// the aim pose's forward
|
||||
const float aim[7] = {0, 0, 0, 0, s, 0, s}; // turned 90 left: forward is -x
|
||||
float f[3];
|
||||
VrPointer_Forward(aim, f);
|
||||
CHECK(NEAR(f[0], -1.0f, 1e-5f) && NEAR(f[1], 0.0f, 1e-5f) && NEAR(f[2], 0.0f, 1e-5f));
|
||||
|
||||
// the beam: its long axis along the line, its face to the eye, whatever way the line goes
|
||||
const float starts[4][3] = {{0.3f, 1.2f, -0.3f}, {0, 1, 0}, {1, 2, -1}, {-0.4f, 0.9f, -0.2f}};
|
||||
const float ends[4][3] = {{0.5f, 1.0f, -4.4f}, {0, 1, -4}, {1, -3, -1.0f}, {-0.4f, 5.0f, -0.2f}};
|
||||
const float eye[3] = {0.0f, 1.6f, 0.0f};
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
float pose[7], axisY[3] = {0, 1, 0}, axisZ[3] = {0, 0, 1}, axisX[3] = {1, 0, 0}, y[3], z[3], x[3];
|
||||
const float length = VrPointer_BeamPose(starts[i], ends[i], eye, pose);
|
||||
const float dx = ends[i][0] - starts[i][0], dy = ends[i][1] - starts[i][1], dz = ends[i][2] - starts[i][2];
|
||||
const float expected = sqrtf(dx * dx + dy * dy + dz * dz);
|
||||
CHECK(NEAR(length, expected, 1e-4f));
|
||||
rotate(pose + 3, axisY, y);
|
||||
rotate(pose + 3, axisZ, z);
|
||||
rotate(pose + 3, axisX, x);
|
||||
CHECK(NEAR(y[0], dx / expected, 1e-4f) && NEAR(y[1], dy / expected, 1e-4f) && NEAR(y[2], dz / expected, 1e-4f));
|
||||
CHECK(NEAR(x[0] * y[0] + x[1] * y[1] + x[2] * y[2], 0.0f, 1e-4f));
|
||||
const float toEye[3] = {eye[0] - pose[0], eye[1] - pose[1], eye[2] - pose[2]};
|
||||
CHECK(z[0] * toEye[0] + z[1] * toEye[1] + z[2] * toEye[2] > 0.0f);
|
||||
CHECK(NEAR(pose[3] * pose[3] + pose[4] * pose[4] + pose[5] * pose[5] + pose[6] * pose[6], 1.0f, 1e-4f));
|
||||
CHECK(NEAR(pose[0], (starts[i][0] + ends[i][0]) / 2, 1e-5f));
|
||||
}
|
||||
|
||||
// the dot: turned like the panel, a little out of its face
|
||||
const float point[3] = {0.5f, 1.0f, -4.5f};
|
||||
float dot[7];
|
||||
VrPointer_DotPose(point, quad, 0.01f, dot);
|
||||
CHECK(NEAR(dot[2], -4.49f, 1e-5f) && NEAR(dot[6], 1.0f, 1e-6f));
|
||||
VrPointer_DotPose((const float[3]){-4.5f, 1.0f, 0.0f}, turned, 0.01f, dot);
|
||||
CHECK(NEAR(dot[0], -4.49f, 1e-4f)); // the turned quad faces +x, so toward the origin is +x
|
||||
|
||||
printf(failures ? "pointer math: FAILED (%d)\n" : "pointer math: ok\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Reference in new issue
Block a user