Compare commits

...
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 59b1fb422b Steam Frame: a library button that starts, updates and quits the game
The launcher gives Lepton what Steam gives an Android title of its own (a
compat data path, which the app's data links to after an install, and the
shader path), keeps a container id of its own, tries a start that ends at
once again (a new APK's first start only installs it), and stops when the
game was quit. Steam's "Exit game" cannot reach the game in Lepton's
container: the VR options get "Quit Halo" (asked twice), which on Android
now ends the process and leaves quit_requested.txt for the launcher. The
library's pictures are in port/android/steam_frame_grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:17:35 +02:00
Stefan MaierandClaude Opus 5.5 5ecd3075a0 Steam Frame: a launcher that starts the game as Steam starts Android titles
Lepton runs the APK in a container of its own, kept between starts, and
installs it anew whenever the APK is newer; the script starts it a second
time after a new APK's first start, which only installs it. With the APK in
~/Documents/HaloCE, updating the game is replacing that file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:17:54 +02:00
Stefan Maier 8e8713681d Merge remote-tracking branch 'origin/fix/multiview-desktop-compile' 2026-10-04 22:59:55 +02:00
Stefan Maier d7099f3e40 Fix desktop link: bot machine check and glClearDepthf were Android-only
network_game_server_is_bot_machine (cea7abfc, Bots as players of the
host) was written into the HALO_ANDROID block of network_server_manager.c
while bots.c calls it on every platform, and so was the array it reads;
both move out of the guard (on the desktop nothing marks a bot machine,
so the check is always false there, the writes stay Android-only).

glClearDepthf (dc21f4b4, Play in VR on OpenXR headsets) is ES-only:
GL_FUNCTIONS declares it for Android's loader and the define at the top
maps glClearDepth to it, but the desktop context links against GL's
glClearDepth. The clear now calls glClearDepth, which the Android define
turns into glClearDepthf as before.
2026-10-04 22:37:17 +02:00
Stefan Maier 4ba36d7305 VR: fix desktop build, targets_multiview was Android-only
6c3f4ce2 (VR: draw both eyes in one pass) changed prepare_draw to pass
targets_multiview to vertex_shader_get, but the variable is declared and
kept inside the HALO_ANDROID guard (L887): Linux and Windows failed to
compile with 'use of undeclared identifier targets_multiview', Android
was fine. On the desktop there are no multiview layers to stand in for
the back buffer, so the desktop call passes FALSE.
2026-10-04 22:25:21 +02:00
Stefan Maier 855f083117 Merge branch 'frame/performance' 2026-10-04 22:04:33 +02:00
Stefan Maier 6f3786de91 Merge branch 'fix/vr-shot-origin' 2026-10-04 22:04:33 +02:00
10 changed files with 176 additions and 20 deletions

No files matched your search

+31 -1
View File
@@ -39,7 +39,37 @@ What differs is found at run time, not by a build of its own.
| 5 | The Frame's controllers | done: their own profile, the Xbox's layout |
| 6 | Frame rate, resolution | done: 72 Hz steady, multiview on |
| 7 | Multiplayer, voice, cross-play with the Quest | |
| 8 | Installing it, for others | |
| 8 | A button in the Steam library | done: `steam_frame_launch.sh` |
## Starting it on the Frame
A button in the Steam library ("Halo VR", a non-Steam game) runs
`~/Documents/HaloCE/halo-vr.sh` (`port/android/steam_frame_launch.sh`),
which starts the game as Steam starts an Android title of its own:
`lepton waitforexitandrun -- ~/Documents/HaloCE/HaloVR.apk`, in a container
of its own (`steamlaunch-4290000001`) that Lepton installs the APK into
anew whenever the file is newer. Updating the game is replacing that APK.
What the script does that Steam would for an Android title:
- a compat data path (`steamapps/compatdata/4290000001/internal`): after an
install Lepton links the app's data to it, a dangling link without it;
and the shader path, which Lepton reads Steam's shader dump path against;
- a container id of its own: with the shortcut's id, SteamVR's client in the
container stalled in xrCreateInstance;
- a few starts: a new APK's first start only installs it, and a start that
ends within 30 s is tried again (up to three times), unless the game was
quit (it writes `quit_requested.txt`).
Steam's "Exit game" does not reach the game (its process is the container's,
not Steam's): quit it with the VR options' "Quit Halo" (Y in the main or
pause menu, twice). A container ended within 30 s of its start makes
Lepton drop the app's install, so the next start installs it again.
The shortcut is in `userdata/<id>/config/shortcuts.vdf` (Steam reads it when
it starts; appid 3151454348 for this name and path), its pictures in
`userdata/<id>/config/grid/` (from `port/android/steam_frame_grid/`). From
the Mac, over USB the Frame's SteamOS is an adb device of its own, `frame`,
beside Lepton's `<frame-ip>:5555`.
## The game's data
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+74
View File
@@ -0,0 +1,74 @@
#!/bin/bash
# Halo VR on the Steam Frame: Lepton runs the APK in a container of its own,
# and installs it anew whenever the APK is newer than the installed one
# (port/android/STEAM_FRAME.md). The game's data: ~/Documents/HaloCE.
# (a container of its own, not the shortcut's id: with that, SteamVR's
# client in the container waited on xrCreateInstance)
export SteamAppId="${HALO_LEPTON_APP_ID:-4290000001}"
# (what Steam gives an Android title of its own, and a non-Steam game not:
# the app's data in the container links to the compat data path's
# internal/, a dangling link without it; and the shader paths: Lepton reads
# the dump path against the other)
export STEAM_COMPAT_DATA_PATH="$HOME/.local/share/Steam/steamapps/compatdata/$SteamAppId"
export STEAM_COMPAT_SHADER_PATH="$HOME/.local/share/Steam/steamapps/shadercache/$SteamAppId"
mkdir -p "$STEAM_COMPAT_DATA_PATH/internal" "$STEAM_COMPAT_SHADER_PATH"
unset STEAM_FOSSILIZE_DUMP_PATH LD_PRELOAD
LEPTON="$HOME/.local/share/Steam/steamapps/common/Lepton/lepton"
APK="$HOME/Documents/HaloCE/HaloVR.apk"
LOG="$HOME/Documents/HaloCE/launcher.log"
# (written by the game when the player quits it: no start again)
QUIT="$HOME/Documents/HaloCE/quit_requested.txt"
rm -f "$QUIT"
CONTEXT="steamlaunch-$SteamAppId"
log() { echo "$(date '+%F %T') $*" >> "$LOG"; }
log "start (pid $$, parent $PPID)"
# Steam's "Exit game" ends the game's processes as Steam knows them (this
# script, under Steam's reaper), not the container, which podman runs: a
# signal here is passed on to Lepton, and a watcher of its own takes the
# container down once this script is gone (killed)
stopping=0
child=0
stop()
{
log "signal $1"
stopping=1
[ "$child" -gt 0 ] && kill -TERM "$child" 2>/dev/null
}
trap 'stop TERM' TERM
trap 'stop INT' INT
trap 'stop HUP' HUP
script=$$
setsid bash -c '
while kill -0 '"$script"' 2>/dev/null; do sleep 1; done
echo "$(date "+%F %T") watcher: the game was ended, so its container" >> "'"$LOG"'"
"'"$LEPTON"'" kill "'"$CONTEXT"'" >/dev/null 2>&1
' </dev/null >/dev/null 2>&1 &
run()
{
"$LEPTON" waitforexitandrun -- "$APK" &
child=$!
# (wait returns early when a signal comes: until Lepton is gone)
while kill -0 "$child" 2>/dev/null; do
wait "$child"
done
child=0
}
# (a new APK is only installed on its first start, not started; and a start
# right after a game that was ended hard can fail in SteamVR, which then
# needs a moment: again, up to three times, while it ends within 30 s)
for attempt in 1 2 3; do
started=$SECONDS
run
if (( stopping || SECONDS - started >= 30 )) || [ -e "$QUIT" ]; then
break
fi
log "ended within $((SECONDS - started)) s: again"
sleep 5
done
rm -f "$QUIT"
log "end"
+12 -2
View File
@@ -2863,7 +2863,15 @@ static struct program_entry *prepare_draw(BOOL immediate)
key.count_samples = device.visibility_test_active && xgpu_capabilities.atomic_counters;
#endif
entry = program_get(vertex_shader_get(program, immediate, targets_multiview), fragment_shader_get(&key));
entry = program_get(vertex_shader_get(program, immediate,
#ifdef HALO_ANDROID
/* the bound targets are the layers (Android's GL_OVR_multiview2);
on the desktop there are no layers to stand in for the back buffer */
targets_multiview
#else
FALSE
#endif
), fragment_shader_get(&key));
if (!entry)
{
stats.skipped_link++;
@@ -4023,7 +4031,9 @@ void d3d8_clear_back_buffer(float red, float green, float blue, float alpha)
glDepthMask(GL_TRUE);
glStencilMask(0xff);
glClearColor(red, green, blue, alpha);
glClearDepthf(1.0f);
/* (glClearDepth: on Android the define above maps it to glClearDepthf,
ES's float entry point) */
glClearDepth(1.0f);
glClearStencil(0);
glClear(GL_COLOR_BUFFER_BIT | (depth_buffer ? GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT : 0));
xgpu_gl_state_invalidate();
+18 -2
View File
@@ -925,8 +925,10 @@ static void platform_show_pending_message(void)
/* ---------- events */
/* quits as closing the window does, when the events are next read (the
menus' Quit: port/linux/game/menu_functions.c); Android's menus have none,
as the system closes its apps */
menus' Quit: port/linux/game/menu_functions.c); on Android, where the
system closes its apps, the VR options' Quit (ui_widget.c) ends the process
at once: on the Steam Frame, Steam cannot end the game, which runs in
Lepton's container (port/android/STEAM_FRAME.md) */
void platform_request_quit(void)
{
#ifndef HALO_ANDROID
@@ -935,6 +937,20 @@ void platform_request_quit(void)
memset(&event, 0, sizeof(event));
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
#else
{
/* (and says so beside the game's data: the Steam Frame's launcher
does not then take it for a start that failed and start it again) */
char path[512];
FILE *file;
snprintf(path, sizeof(path), "%s/quit_requested.txt", platform_data_root());
file = fopen(path, "wb");
if (file)
fclose(file);
}
platform_log("quit: the player asked");
exit(EXIT_SUCCESS);
#endif
}
+25 -3
View File
@@ -6570,10 +6570,11 @@ enum
_vr_option_crosshair,
_vr_option_motion_tracker,
_vr_option_controls,
_vr_option_quit,
NUMBER_OF_VR_OPTIONS,
VR_OPTIONS_TOP = 102,
VR_OPTIONS_ROW_HEIGHT = 28,
VR_OPTIONS_TOP = 98,
VR_OPTIONS_ROW_HEIGHT = 26,
VR_OPTIONS_FOOTER = 362,
_vr_controls_on_foot = 0,
@@ -6588,8 +6589,12 @@ static struct
boolean controls;
short selection;
short page;
/* "Quit Halo" chosen once: a second time quits */
boolean quit_armed;
} ui_vr_options_globals;
void platform_request_quit(void);
static real const vr_snap_angles[] = { 30.0f, 45.0f, 60.0f, 90.0f };
static real const vr_smooth_speeds[] = { 60.0f, 90.0f, 120.0f, 150.0f, 180.0f };
static real const vr_vignettes[] = { 0.0f, 0.35f, 0.7f, 1.0f };
@@ -6661,6 +6666,13 @@ static void ui_vr_options_change(
ui_vr_options_globals.controls = TRUE;
ui_vr_options_globals.page = _vr_controls_on_foot;
break;
case _vr_option_quit:
/* (the Steam Frame's Steam cannot end the game, which runs in
Lepton's container: it ends itself; asked twice) */
if (ui_vr_options_globals.quit_armed)
platform_request_quit();
ui_vr_options_globals.quit_armed = TRUE;
break;
}
ui_play_audio_feedback_sound(_ui_audio_feedback_forward);
}
@@ -6668,6 +6680,7 @@ static void ui_vr_options_change(
static void ui_vr_options_close(
void)
{
ui_vr_options_globals.quit_armed = FALSE;
ui_vr_options_globals.open = FALSE;
ui_vr_options_globals.controls = FALSE;
}
@@ -6754,6 +6767,7 @@ static boolean ui_vr_options_take_event(
else if (move)
{
ui_vr_options_globals.selection = (short)PIN(ui_vr_options_globals.selection + move, 0, NUMBER_OF_VR_OPTIONS - 1);
ui_vr_options_globals.quit_armed = FALSE;
ui_play_audio_feedback_sound(_ui_audio_feedback_cursor);
}
else if (accept)
@@ -6813,7 +6827,11 @@ static boolean ui_vr_options_take_pointer(
if (x >= 118 && x < 522 && y >= VR_OPTIONS_TOP - 4 &&
y < VR_OPTIONS_TOP - 4 + NUMBER_OF_VR_OPTIONS * VR_OPTIONS_ROW_HEIGHT)
{
ui_vr_options_globals.selection = (short)((y - (VR_OPTIONS_TOP - 4)) / VR_OPTIONS_ROW_HEIGHT);
short row = (short)((y - (VR_OPTIONS_TOP - 4)) / VR_OPTIONS_ROW_HEIGHT);
if (row != ui_vr_options_globals.selection)
ui_vr_options_globals.quit_armed = FALSE;
ui_vr_options_globals.selection = row;
if (pointer->left_clicks)
ui_vr_options_change(1);
}
@@ -7066,6 +7084,10 @@ static void ui_vr_options_render(
case _vr_option_controls:
label = "Controller buttons...";
break;
case _vr_option_quit:
label = ui_vr_options_globals.quit_armed && ui_vr_options_globals.selection == _vr_option_quit ?
"Quit Halo? Once more to quit" : "Quit Halo";
break;
}
if (row == ui_vr_options_globals.selection)
ui_join_code_draw_quad(118, (short)(y - 4), 522, (short)(y + VR_OPTIONS_ROW_HEIGHT - 6), 0x803A6EA5);
+16 -12
View File
@@ -703,9 +703,10 @@ typedef char network_game_server_countdown_state_offset_assert[
offsetof(struct network_game_server, countdown_state) ==
8 + HALO_PORT_NETWORK_GAME_SIZE + MAXIMUM_NETWORK_MACHINE_COUNT * 0x10 + 0xC ? 1 : -1];
#ifdef HALO_ANDROID
/* bots' machines (below: the VR port's bots) */
/* bots' machines (the VR port's bots, port/linux/game/bots.c; on the
desktop none is ever marked, the writes live behind the Android guard) */
static boolean network_game_server_bot_machines[MAXIMUM_NETWORK_MACHINE_COUNT];
#ifdef HALO_ANDROID
static short network_game_server_bot_machine_count(struct network_game_server *server);
#endif
@@ -4340,6 +4341,19 @@ static boolean network_game_server_have_all_machines_have_precached(
return all_machines_have_precached;
}
/* whether the machine at this index is one of the host's bots (the VR
port's bots.c asks, on every platform: on the desktop no bot machine is
ever marked, so this is always false there) */
boolean network_game_server_is_bot_machine(
long machine_index)
{
struct network_game_server *server = global_network_game_server_get();
return server && VALID_INDEX(machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) &&
network_game_server_bot_machines[machine_index] &&
server->game.machines[machine_index].machine_index == machine_index;
}
#ifdef HALO_ANDROID
/* ---------- bots (the VR port's: port/linux/game/bots.c)
@@ -4363,16 +4377,6 @@ static void network_game_server_bot_copy_name(
destination[index] = 0;
}
boolean network_game_server_is_bot_machine(
long machine_index)
{
struct network_game_server *server = global_network_game_server_get();
return server && VALID_INDEX(machine_index, MAXIMUM_NETWORK_MACHINE_COUNT) &&
network_game_server_bot_machines[machine_index] &&
server->game.machines[machine_index].machine_index == machine_index;
}
static short network_game_server_bot_machine_count(
struct network_game_server *server)
{