mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-17 16:00:26 +02:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06cb70fb58 | ||
|
|
1709698053 |
No files matched your search
@@ -0,0 +1,49 @@
|
||||
# Steam Frame
|
||||
|
||||
The same APK as for the Quest runs on the Steam Frame, in Lepton (SteamOS's
|
||||
Android, a Waydroid fork: Android 11, arm64), with SteamVR's OpenXR runtime.
|
||||
What differs is found at run time, not by a build of its own.
|
||||
|
||||
## The device
|
||||
|
||||
- Steam Settings > System > Enable Developer Mode; then start "Lepton
|
||||
Development" from the library. While the headset sleeps Lepton stops, and
|
||||
adb with it.
|
||||
- adb over the network: `adb connect <frame-ip>:5555`; over USB:
|
||||
`adb forward tcp:5555 tcp:5555` then `adb connect localhost:5555`.
|
||||
Always name the device (`adb -s <frame-ip>:5555 ...`): a Quest may be
|
||||
connected as well.
|
||||
- Install with `adb install -r --no-streaming`. It prints "Lepton post
|
||||
installation failed due to APP_PATH being unset", but installs. Start the
|
||||
game with `am start -n com.halo.decomp/.LauncherActivity`.
|
||||
- Lepton empties `/sdcard/Android/data` whenever it starts again: the game's
|
||||
data must live elsewhere (stage 2). `/sdcard/Documents` and
|
||||
`/sdcard/Download` are the SteamOS user's folders and are kept.
|
||||
|
||||
## What the port does for it
|
||||
|
||||
- SDL3's clipboard handler takes a clipboard service for granted, which
|
||||
Lepton has not: `tools/android_build.py` patches it to do without.
|
||||
- SteamVR lists an implicit OpenXR layer for Vulkan apps whose library an
|
||||
app cannot load: the APK carries a pass-through layer under its name
|
||||
(`port/android/xr_layer/fdm_injection_stub.c`).
|
||||
|
||||
## Stages
|
||||
|
||||
| # | Stage | State |
|
||||
| --- | --- | --- |
|
||||
| 1 | Base: the fixes above on the current main | in progress |
|
||||
| 2 | The game's data where Lepton keeps it | |
|
||||
| 3 | Menus, HUD and text visible (quad layers) | |
|
||||
| 4 | The freeze at the cryo bay's calibration | |
|
||||
| 5 | The Frame's controllers | |
|
||||
| 6 | Frame rate, resolution | |
|
||||
| 7 | Multiplayer, voice, cross-play with the Quest | |
|
||||
| 8 | Installing it, for others | |
|
||||
|
||||
## Findings
|
||||
|
||||
- 2026-10-02: runs flat and in VR (GLES 3.2 by Mesa zink on Turnip, eyes
|
||||
1728x1728, 72 Hz, XR_FB_display_refresh_rate absent). Quad layers
|
||||
(menus, HUD, instructions) are not shown; the projection layer is. Froze
|
||||
at the cryo bay's look calibration; its log was lost to Lepton's wipe.
|
||||
@@ -895,6 +895,12 @@ int host_xr_probe(float resolution_scale, int *eye_width, int *eye_height)
|
||||
/* the loader and the runtime keep the activity */
|
||||
activity = (*environment)->NewGlobalRef(environment, activity);
|
||||
|
||||
/* (The Steam Frame lists an implicit API layer of its own for Vulkan
|
||||
apps, XR_APILAYER_VALVE_fdm_injection, whose library an app cannot load
|
||||
from its namespace, failing xrCreateInstance; the Android loader reads
|
||||
no variable to turn it off, so the app carries a pass-through layer
|
||||
under that library's name: port/android/xr_layer/fdm_injection_stub.c) */
|
||||
|
||||
if (!XR_SUCCEEDED(xrGetInstanceProcAddr(XR_NULL_HANDLE, "xrInitializeLoaderKHR",
|
||||
(PFN_xrVoidFunction *)&initialize_loader)) || !initialize_loader)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
FDM_INJECTION_STUB.C
|
||||
|
||||
An OpenXR API layer that does nothing, named as the Steam Frame's
|
||||
XR_APILAYER_VALVE_fdm_injection names its library
|
||||
(libVkLayer_VALVE_fdm_injection.so). The Frame lists that layer as implicit
|
||||
for every OpenXR app (/vendor/etc/openxr/1/api_layers/implicit.d), but an
|
||||
Android app (in Lepton) cannot load the system's library from its own
|
||||
namespace, and the Khronos loader then fails xrCreateInstance altogether;
|
||||
nor does the Android loader read the variable that would turn the layer
|
||||
off. With this one in the app's libraries the loader finds a layer of that
|
||||
name, which hands every call on to the next. (The real layer adds fragment
|
||||
density maps to Vulkan apps; this game draws with OpenGL ES.) On a Quest no
|
||||
manifest names it and it is never loaded.
|
||||
*/
|
||||
|
||||
#include <openxr/openxr.h>
|
||||
#include <openxr/openxr_loader_negotiation.h>
|
||||
#include <stddef.h>
|
||||
|
||||
static PFN_xrGetInstanceProcAddr next_get_instance_proc_addr;
|
||||
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL layer_get_instance_proc_addr(XrInstance instance, const char *name,
|
||||
PFN_xrVoidFunction *function)
|
||||
{
|
||||
if (!next_get_instance_proc_addr)
|
||||
return XR_ERROR_HANDLE_INVALID;
|
||||
return next_get_instance_proc_addr(instance, name, function);
|
||||
}
|
||||
|
||||
static XRAPI_ATTR XrResult XRAPI_CALL layer_create_api_layer_instance(const XrInstanceCreateInfo *info,
|
||||
const XrApiLayerCreateInfo *layer_info, XrInstance *instance)
|
||||
{
|
||||
XrApiLayerCreateInfo next_layer_info;
|
||||
|
||||
if (!layer_info || !layer_info->nextInfo)
|
||||
return XR_ERROR_INITIALIZATION_FAILED;
|
||||
next_get_instance_proc_addr = layer_info->nextInfo->nextGetInstanceProcAddr;
|
||||
next_layer_info = *layer_info;
|
||||
next_layer_info.nextInfo = layer_info->nextInfo->next;
|
||||
return layer_info->nextInfo->nextCreateApiLayerInstance(info, &next_layer_info, instance);
|
||||
}
|
||||
|
||||
__attribute__((visibility("default")))
|
||||
XRAPI_ATTR XrResult XRAPI_CALL xrNegotiateLoaderApiLayerInterface(const XrNegotiateLoaderInfo *loader_info,
|
||||
const char *layer_name, XrNegotiateApiLayerRequest *request)
|
||||
{
|
||||
(void)layer_name;
|
||||
if (!loader_info || !request || loader_info->structType != XR_LOADER_INTERFACE_STRUCT_LOADER_INFO ||
|
||||
request->structType != XR_LOADER_INTERFACE_STRUCT_API_LAYER_REQUEST ||
|
||||
loader_info->minInterfaceVersion > XR_CURRENT_LOADER_API_LAYER_VERSION ||
|
||||
loader_info->maxInterfaceVersion < XR_CURRENT_LOADER_API_LAYER_VERSION)
|
||||
{
|
||||
return XR_ERROR_INITIALIZATION_FAILED;
|
||||
}
|
||||
request->layerInterfaceVersion = XR_CURRENT_LOADER_API_LAYER_VERSION;
|
||||
request->layerApiVersion = loader_info->maxApiVersion < XR_CURRENT_API_VERSION ? loader_info->maxApiVersion :
|
||||
XR_CURRENT_API_VERSION;
|
||||
request->getInstanceProcAddr = layer_get_instance_proc_addr;
|
||||
request->createApiLayerInstance = layer_create_api_layer_instance;
|
||||
return XR_SUCCESS;
|
||||
}
|
||||
@@ -231,7 +231,6 @@ symbols in this file:
|
||||
#include "units/units.h"
|
||||
#ifdef HALO_ANDROID
|
||||
#include "halo_vr.h"
|
||||
#include "physics/collisions.h"
|
||||
#endif
|
||||
|
||||
/* port/linux/game/pal_tags.c's */
|
||||
@@ -2206,10 +2205,6 @@ static void trigger_create_projectiles(
|
||||
long target_object_index= NONE;
|
||||
long projectile_definition_index;
|
||||
short projectile_count;
|
||||
#ifdef HALO_ANDROID
|
||||
/* VR: fired from the muzzle of the weapon in the hand */
|
||||
boolean vr_from_muzzle= FALSE;
|
||||
#endif
|
||||
|
||||
if (!TEST_FLAG(trigger_definition->flags, _weapon_trigger_projectiles_cannot_be_aimed_bit) &&
|
||||
unit &&
|
||||
@@ -2287,7 +2282,6 @@ static void trigger_create_projectiles(
|
||||
origin.x = vr_origin[0];
|
||||
origin.y = vr_origin[1];
|
||||
origin.z = vr_origin[2];
|
||||
vr_from_muzzle = TRUE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -2389,26 +2383,6 @@ static void trigger_create_projectiles(
|
||||
real_point3d camera_position;
|
||||
|
||||
unit_get_camera_position(owner_object_index, &camera_position);
|
||||
#ifdef HALO_ANDROID
|
||||
if (vr_from_muzzle)
|
||||
{
|
||||
/* VR: from the eyes down to the hand the line runs through
|
||||
the player's own body, which the game's test hits, moving the
|
||||
shot up towards the eyes (it then flies parallel to the aim,
|
||||
above it): only the level stops it, a hand through a wall */
|
||||
struct collision_result collision;
|
||||
struct object_datum *projectile_object= object_get(projectile_object_index);
|
||||
|
||||
if (collision_test_line(FLAG(_collision_test_structure_bit) | FLAG(_collision_test_front_facing_surfaces_bit),
|
||||
&camera_position, &projectile_object->object.position, owner_object_index, &collision) &&
|
||||
collision.location.cluster_index!=NONE)
|
||||
{
|
||||
object_translate(projectile_object_index, &collision.point, &collision.location);
|
||||
object_compute_node_matrices(projectile_object_index);
|
||||
}
|
||||
}
|
||||
else
|
||||
#endif
|
||||
object_force_inside_bsp(projectile_object_index, &camera_position);
|
||||
}
|
||||
if (target_object_index!=NONE)
|
||||
|
||||
+53
-2
@@ -169,6 +169,45 @@ def _find_ndk() -> Optional[Path]:
|
||||
return None
|
||||
|
||||
|
||||
# SDL3's Java takes the clipboard service for granted: Lepton, the Steam
|
||||
# Frame's Android, has none, and the activity died starting. These edits let
|
||||
# it do without one (the clipboard is then empty and keeps nothing).
|
||||
SDL_JAVA_PATCHES = [
|
||||
(" mClipMgr = (ClipboardManager) SDL.getContext().getSystemService(Context.CLIPBOARD_SERVICE);\n"
|
||||
" mClipMgr.addPrimaryClipChangedListener(this);\n",
|
||||
" mClipMgr = (ClipboardManager) SDL.getContext().getSystemService(Context.CLIPBOARD_SERVICE);\n"
|
||||
" if (mClipMgr != null) /* halo-ce-vr: none in Lepton */\n"
|
||||
" mClipMgr.addPrimaryClipChangedListener(this);\n"),
|
||||
(" public boolean clipboardHasText() {\n",
|
||||
" public boolean clipboardHasText() {\n"
|
||||
" if (mClipMgr == null) /* halo-ce-vr */\n"
|
||||
" return false;\n"),
|
||||
(" public String clipboardGetText() {\n",
|
||||
" public String clipboardGetText() {\n"
|
||||
" if (mClipMgr == null) /* halo-ce-vr */\n"
|
||||
" return null;\n"),
|
||||
(" public void clipboardSetText(String string) {\n",
|
||||
" public void clipboardSetText(String string) {\n"
|
||||
" if (mClipMgr == null) /* halo-ce-vr */\n"
|
||||
" return;\n"),
|
||||
]
|
||||
|
||||
|
||||
def patch_sdl_java() -> None:
|
||||
"""SDL_JAVA_PATCHES, once, into the cloned SDL3's Java."""
|
||||
path = SDL_DIR / "android-project" / "app" / "src" / "main" / "java" / "org" / "libsdl" / "app" / "SDLActivity.java"
|
||||
if not path.is_file():
|
||||
return
|
||||
text = path.read_text()
|
||||
if "halo-ce-vr: none in Lepton" in text:
|
||||
return
|
||||
for old, new in SDL_JAVA_PATCHES:
|
||||
if text.count(old) != 1:
|
||||
raise SystemExit(f"cannot patch {path}: SDL3's clipboard handler has changed")
|
||||
text = text.replace(old, new)
|
||||
path.write_text(text)
|
||||
|
||||
|
||||
def fetch_third_party() -> None:
|
||||
"""Download musl and SDL3 (configure time, once)."""
|
||||
THIRD_PARTY.mkdir(parents=True, exist_ok=True)
|
||||
@@ -182,6 +221,7 @@ def fetch_third_party() -> None:
|
||||
print(f"Cloning SDL3 {SDL_TAG}")
|
||||
subprocess.run(["git", "clone", "-q", "--depth", "1", "--branch", SDL_TAG, SDL_URL, str(SDL_DIR)],
|
||||
check=True)
|
||||
patch_sdl_java()
|
||||
if not OPENXR_DIR.is_dir():
|
||||
print(f"Downloading {OPENXR_URL}")
|
||||
import zipfile
|
||||
@@ -571,6 +611,17 @@ def generate_android_build(n: Writer, sln: Any) -> None:
|
||||
)
|
||||
n.build(outputs=libmain, rule="android_host_link", inputs=host_objects, implicit=[libsdl])
|
||||
|
||||
# a do-nothing OpenXR layer under the Steam Frame's implicit layer's
|
||||
# library name (port/android/xr_layer/fdm_injection_stub.c)
|
||||
fdm_stub = jni_dir / "libVkLayer_VALVE_fdm_injection.so"
|
||||
n.rule(
|
||||
name="android_xr_layer",
|
||||
command=(f"$android_host_cc -O2 -fPIC -shared -fvisibility=hidden -I{OPENXR_DIR}/include -o $out $in "
|
||||
"-Wl,-z,max-page-size=16384"),
|
||||
description="ANDROID XR LAYER $out",
|
||||
)
|
||||
n.build(outputs=fdm_stub, rule="android_xr_layer", inputs=PORT_DIR / "xr_layer" / "fdm_injection_stub.c")
|
||||
|
||||
# ---------- staging for Gradle
|
||||
|
||||
staged_sdl = jni_dir / "libSDL3.so"
|
||||
@@ -578,7 +629,7 @@ def generate_android_build(n: Writer, sln: Any) -> None:
|
||||
n.rule(name="android_copy", command="cp $in $out", description="ANDROID STAGE $out")
|
||||
n.build(outputs=staged_sdl, rule="android_copy", inputs=libsdl)
|
||||
n.build(outputs=staged_image, rule="android_copy", inputs=image)
|
||||
n.build(outputs="android", rule="phony", inputs=[libmain, staged_sdl, staged_image])
|
||||
n.build(outputs="android", rule="phony", inputs=[libmain, staged_sdl, staged_image, fdm_stub])
|
||||
|
||||
apk = PORT_DIR / "app" / "build" / "outputs" / "apk" / "debug" / "app-debug.apk"
|
||||
n.rule(
|
||||
@@ -589,6 +640,6 @@ def generate_android_build(n: Writer, sln: Any) -> None:
|
||||
description="ANDROID GRADLE $out",
|
||||
pool="console",
|
||||
)
|
||||
n.build(outputs=apk, rule="android_gradle", inputs=[libmain, staged_sdl, staged_image])
|
||||
n.build(outputs=apk, rule="android_gradle", inputs=[libmain, staged_sdl, staged_image, fdm_stub])
|
||||
n.build(outputs="android_apk", rule="phony", inputs=apk)
|
||||
n.newline()
|
||||
Reference in new issue
Block a user