Compare commits

...
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 5ce39c27a7 Roadmap: multiview to look into later; refresh rate steps in main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:57:17 +02:00
Stefan MaierandClaude Opus 5.5 67d738451c Draw the eyes one by one by default; multiview only with debug.multiview
On the Quest 3, in big online games (26-35 players), the picture was
doubled and shifted from the start as the head turned, nauseating; with
48 bots it was not, and with the eyes drawn one by one it was not online
either. Not the frame rate: one by one, online at 72 Hz, the compositor
repeated as many frames and nothing was seen. The Steam Frame had the
same doubling with multiview (2026-10-05) and draws them one by one
since. So both now do by default; debug.multiview (was vr.multiview, and
debug.multiview_steamvr) draws them at once, for finding what the shared
pass draws wrongly. The refresh rate's steps take up the extra cost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:48:27 +02:00
Stefan MaierandClaude Opus 5.5 91bdb7e67c Refresh rate: step down at one late frame in 32, show the rate it runs at
The first try stepped 90 to 80 Hz a few seconds into a 30-player game,
and no doubled picture was seen, but 2-5 stale frames a second were left
at 80: one late frame in 32 (was 16) takes it on to 72. The VR options
show the rate chosen and the one it runs at while lowered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:44:18 +02:00
Stefan MaierandClaude Opus 5.5 bf778b6d60 Step the refresh rate down while frames come late (vr.refresh_rate_auto)
In big online games a client's ticks cost 3 to 5 ms (twice what bots or
smaller games do), and the frames with a tick missed 90 Hz's 11.1 ms:
the headset showed them twice, a doubled picture when turning, with the
GPU at 60-80 %. While more than one frame in sixteen comes late, two
two-second windows running, the rate now steps down to the next the
headset offers (90 to 72 Hz); after ten windows in which hardly any
would be late at the rate above, it steps back up, never above
vr.refresh_rate. A load's long frames count for neither. On by default,
"Lower it when frames are late" on the VR options' MORE page; nothing
where the headset offers one rate (SteamVR).

Also the BUTTONS page's notes below its ten rows (the push to talk row
pushed the last one onto them).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:38:59 +02:00
Stefan MaierandClaude Opus 5.5 d8a786f5a1 Merge merge/upstream-oct16: OpenCE's string conversion fix, voice packets checked, the postgame host screen's menu
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:34:03 +02:00
Stefan MaierandClaude Opus 5.5 7c294c9c39 Merge OpenCE upstream: its fix of the string conversion (taken over ours), voice packets checked, the postgame host screen's menu
Upstream fixed the Android asm conversion's string bug independently
(d9669f6ee, also from a Steam Frame crash); its converter and tests
replace ours (c85cf8ece), which they cover and extend (quoted names
honour escapes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:05:24 +02:00
bnunu f90d955527 Merge pull request #374 from chestahh/opence-multiplayer-menu-fix
Restore the main menu when leaving the postgame host screen
2026-10-16 00:13:42 -04:00
bnunu 085676d827 Merge pull request #346 from startupfoundry/arm64-guest-strings
arm64 and Android guests: string literals kept byte for byte (fixes a voice chat crash)
2026-10-15 23:37:14 -04:00
chestahh 4301ca88d6 Restore the main menu when leaving the postgame host screen
The postgame host map picker opens without menu history. Give it the
existing main_menu_if_no_history flag used by the lobby so leaving the
last screen restores the main menu instead of leaving only the background.
Update the generator and generated XML together. Preserve OpenCE's
existing handlers; no MCC code or regression-test files are included.
This local menu change does not require a network version bump.

Resolved the fork cherry-pick against OpenCE main 8102e711 by adding only
the flag override. Windows portable release build passed. Existing menu
teardown, touch and web checks returned 28 passes and 6 failures: missing
m.lib/pthread.h and Windows path separators in POSIX-oriented tests.
Android and web build targets were unavailable locally. Checked generator
round-trip stability and git diff --check. The two simple wrappers were
regenerated through ce_menus.widget_xml from their exported XML fields;
the original PC tag inputs were unavailable. No upstream runtime game
session or Linux build was run. Earlier scripted runtime verification
was performed on the GulchCE implementation, not this upstream revision.

(cherry picked from commit c811c7f527d70d5476578ed88ea0edcef2269967)
2026-10-16 02:51:54 +01:00
Stefan MaierandClaude Opus 5.5 b5cdd9268c Merge merge/upstream-net29: OpenCE network version 29
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:26:16 +02:00
Paul Hontz 9c70f5f5e3 Voice chat: a packet that is not one 20 ms frame is dropped before decoding
Every build sends one 20 ms frame per packet (voice_audio_encode). A packet
whose table of contents says otherwise, or that Opus cannot parse, is
damaged: voice_audio_play now drops it before the decoder sees it
(opus_packet_get_nb_samples), and the next frame makes up for it as lost.
Cheap defence in depth: Opus's decoder already refuses such packets, and the
crash in decode_pulses came from a valid packet and the guest's garbled
pulse cache, fixed in the assembly conversion.
2026-10-13 22:13:44 -04:00
Paul Hontz d9669f6eef The guest's assembly conversion keeps string literals byte for byte
The arm64 and Android guests are compiled for arm64_32-apple-watchos and
their Darwin assembly converted to ELF (tools/android_asm_convert.py). The
converter took a quoted .ascii/.asciz operand for a quoted symbol name and
dropped a leading "_" from it, as it does from symbols; and its quoted-name
pattern ended at an escaped quote (\"), so the "_" of a word after one was
dropped too. Clang writes byte tables as .ascii strings, so a table holding
95 ("_") lost that byte and everything after it moved down by one.

In Opus that table was CELT's pulse cache (modes.c, cache_bits50): the bits
a band may have are read off it, and shifted by a byte it lets a band have
more pulses than the PVQ tables hold. Decoding a frame from another player
then read past CELT_PVQ_U_ROW in decode_pulses (n 32, k 240) and crashed: a
client on the Steam Frame, in a CTF game on the internet. The guest's own
encoder could crash likewise in encode_pulses. The same build of Opus,
driven with the game's settings and with clean, corrupt, truncated and
random packets and voice_audio_play's loss handling, crashed within a few
thousand frames (and identically at -O0); with the fix it ran 4.8 million
decodes clean, and its encoded bytes and decoded samples are identical to a
native 64-bit build's.

The same loss hit other strings and tables of the guest: musl's iswalpha,
iswpunct and towctrans tables and iconv's charmaps; tomlc17's "_0123..."
digit sets (TOML numbers with "_"); the attract mode's "_de"/"_fr" names,
"_lan_", "_lit" and "_%s"; the progress bar's compressed monk tone and the
DirectSound effects image; and assertion and error texts. All are now
assembled as compiled.

String directives now go through unchanged, and quoted names honour
escapes. tools/test_android_asm_convert.py checks both, and a table of
every byte value through clang, the conversion and the assembler.
2026-10-13 22:13:44 -04:00
13 changed files with 278 additions and 60 deletions

No files matched your search

+1 -1
View File
@@ -99,7 +99,7 @@ jobs:
- name: Test
run: |
python -m pytest -q tools/test_linux_port.py tools/test_host_watch_hash.py tools/test_touch_menu.py tools/harness tools/test_profile.py
python -m pytest -q tools/test_linux_port.py tools/test_android_asm_convert.py tools/test_host_watch_hash.py tools/test_touch_menu.py tools/harness tools/test_profile.py
python tools/test_light_storage.py
- name: Compile cache
+3 -1
View File
@@ -388,7 +388,9 @@ The guest is the game, the platform layer and a small runtime:
AArch64 target of clang. The options `-U__APPLE__` and
`-fno-define-target-os-macros` hide the Darwin environment.
2. `tools/android_asm_convert.py` changes the Mach-O assembly to ELF
assembly.
assembly. It renames symbols, but keeps string literals (`.ascii`,
`.asciz`) byte for byte: they hold tables as well as text
(`tools/test_android_asm_convert.py`).
3. The AArch64 assembler makes the objects.
4. `ld.lld` links the objects with `guest/guest.ld` to a static image,
`build/android/halo_guest.elf`, at a fixed address above the Xbox memory
+32 -1
View File
@@ -17,7 +17,7 @@ named where code is taken over.
| Area | What |
| --- | --- |
| Headsets | Quest 3; Steam Frame (Lepton): its controllers' layout, a Steam library tile that installs, starts, updates and quits the game, data kept in `~/Documents/RingfallMjolnirVR` |
| Rendering | Both eyes in one pass (multiview) on the Quest; one by one on SteamVR, where multiview's eyes did not fuse; lens flares at their depth in stereo; HUD, menus, scope, crosshair and names as layers of their own |
| Rendering | The eyes drawn one by one (both at once, multiview, only with `debug.multiview`: its eyes did not always fuse, see the open points); lens flares at their depth in stereo; HUD, menus, scope, crosshair and names as layers of their own |
| Aiming | The weapon in the hand, its shots from the muzzle, laser and crosshair options |
| Body | Physical crouch (`vr.crouch_limit`) beside the crouch button, leaning (`vr.lean_limit`), melee by swinging, grenades thrown with the left hand, two-handed weapons |
| Vehicles | The Warthog's wheel turned with the hands, right trigger throttle, left brake; first-person seats; turrets with handles |
@@ -42,6 +42,7 @@ named where code is taken over.
| 3c | Weapons held physically, holsters at shoulder and hip (the buttons stay) | |
| 3d | Other VR players shown with their heads, hands and bodies (today their bodies move as flat players' do: only their aim and throws come from their hands) | |
| 4 | Later: disc image import in the launcher; moving by swinging the arms (in place of the stick, never faster than it: online too), and a sprint by swinging them harder (faster than the game allows: offline only) | import **done** (in `main` 2026-10-06): the app's import (tried on the Quest 3: all 24 maps; on the Frame kept in `~/Documents/RingfallMjolnirVR`), a Steam Frame installer from Desktop Mode (`STEAM_FRAME.md`; tried on the Frame: the old folder renamed, the shortcut added by Steam), a release signing key (CI and local builds the same). Arm swinging and its offline sprint still open |
| 5 | Later: multiview again (both eyes in one pass, half the drawing's CPU cost): find why its eyes do not fuse in big online games on the Quest 3 (and now and then on the Steam Frame), then turn it back on | open: off by default since 2026-10-16 (`debug.multiview`); findings in the open points |
| R | Release as **Ringfall: Mjolnir VR** (no "Halo" in the product's name): app name, id `io.github.cesarus85.ringfall`, icon and library pictures (`tools/ringfall_art.py`), Frame folder and installer, release assets, README; the repository renamed | **done** (in `main` 2026-10-06; Quest 3 and Steam Frame good) |
## Step 2, first part (done)
@@ -518,6 +519,34 @@ see that nothing is missing or slower.
## Open points outside the order
- Multiview's doubled pictures (2026-10-16): on the developer's Quest 3,
doubled and shifted pictures from the start of big online games (26-35
players) as the head turned, nauseating; none in a local game with 48
bots; none online either with the eyes drawn one by one. Not the frame
rate: one by one, online at 72 Hz, the compositor repeated as many
frames (about 4 a second) and nothing was seen. Not the facing jumps
(respawns only) nor the camera (it moves every frame). Before: the
Steam Frame's (2026-10-05), each eye alone sharp. So the eyes are drawn
one by one by default now (`debug.multiview` for both at once). To do:
find what the shared pass draws wrongly online, e.g. by saving both
eyes' pictures in a big online game and comparing what is doubled
(sprites and effects projected by the game itself, `mv_depth`; programs
made on the shader worker; the sky rows), then turn it back on: it
halves the drawing's CPU cost.
- Big online games (2026-10-16): doubled pictures on a 32-player server,
none in a local game with 48 bots. Not the GPU (60–80 %): the ticks cost
3.3–4.6 ms as a client there (1.3–1.9 ms with bots or 16 players), so
47–70 of 150 frames with a tick missed 90 Hz's 11.1 ms (the doubling
itself was multiview's, above). In `main` 2026-10-16 (Quest 3 good):
`vr.refresh_rate_auto`
(VR options MORE, "Lower it when frames are late", on by default) steps
the rate down a step (90, 80, 72 Hz) while more than one frame in 32
comes late, and back up once they would keep up; "VR: refresh rate" in
the log. First try: 90 to 80 Hz a few seconds into a 30-player game, no
doubled picture seen; 2-5 stale frames a second left at 80, hence one
in 32 (was 16). Later:
why a client's ticks cost that much per remote player.
- Doubled pictures when turning on the developer's Quest 3 (2026-10-12):
with every build tried, back to 2026-10-06's, at 90 and at 72 Hz, even
in seconds the compositor reported no stale frame. That Quest runs
@@ -526,6 +555,8 @@ see that nothing is missing or slower.
default (smooth there). To do: try this and later builds with "high" on
a Quest 3 outside PTC (the developer's wife's), and set the default
back to "high" or "auto" if it holds.
Resolved 2026-10-16: multiview (above), not the Horizon OS PTC.
- Multiview on the Steam Frame: why the eyes do not fuse there (suspected
Mesa zink/Turnip); tools in branch `frame/doubled-images` (86335828),
not merged.
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- The PC version's menus: ui\shell\main_menu\multiplayer_type_select\connected (tools/ce_menus.py) -->
<menus>
<widget name="main_menu/multiplayer_type_select/connected/connected_map_select_wrapper" width="640" height="480" flags="pass_unhandled_to_focused_child pause_game tag_controller_index force_handle_mouse">
<widget name="main_menu/multiplayer_type_select/connected/connected_map_select_wrapper" width="640" height="480" flags="pass_unhandled_to_focused_child pause_game main_menu_if_no_history tag_controller_index force_handle_mouse">
<child widget="main_menu/multiplayer_type_select/mp_map_select/mp_map_select_screen"/>
</widget>
<widget name="main_menu/multiplayer_type_select/connected/gametype_select_screen_wrapper" width="640" height="480" flags="pass_unhandled_to_focused_child pause_game">
+1 -1
View File
@@ -50,7 +50,7 @@ void halo_vr_eye_camera(int eye, const float game_position[3],
/* copies the back buffer, just drawn for an eye, to that eye's image */
void halo_vr_capture_eye(int eye);
/* Both eyes in one draw (vr.multiview, where GL_OVR_multiview2 is): whether
/* Both eyes in one draw (debug.multiview, where GL_OVR_multiview2 is): whether
this frame draws so; the camera between the eyes whose view takes in both
(as halo_vr_eye_camera), which also starts drawing into both eyes' layers;
and after the draw, the layers taken for the eyes' images. */
+12 -7
View File
@@ -200,6 +200,12 @@ static const struct config_setting config_settings[] =
{ "vr.refresh_rate", _config_real, "90.0", "HALO_VR_REFRESH_RATE", _environment_value, _platform_android,
"The display refresh rate to ask the headset for, in Hz (72, 90, 120);\n"
"it picks the nearest it has at or below. 0 for the highest." },
{ "vr.refresh_rate_auto", _config_boolean, "true", "HALO_VR_REFRESH_RATE_AUTO", _environment_value,
_platform_android,
"While frames come late at that rate (big online games), step down to the\n"
"next the headset has (90 to 72 Hz), and back up once they would keep up\n"
"again; the headset shows a late frame twice, a doubled picture when\n"
"turning. Only where the headset lets the game choose (not SteamVR)." },
{ "vr.graphics", _config_string, "\"medium\"", "HALO_VR_GRAPHICS", _environment_value, _platform_android,
"Which of the costlier effects are drawn (\"medium\" by default): \"auto\" all of them while the\n"
"GPU keeps up, leaving out one after another (the shield's glow, object\n"
@@ -227,9 +233,6 @@ static const struct config_setting config_settings[] =
"away from the player's body (walls are not checked)." },
{ "vr.crouch_limit", _config_real, "0.9", "HALO_VR_CROUCH_LIMIT", _environment_value, _platform_android,
"How far, in metres, the view follows your head down." },
{ "vr.multiview", _config_boolean, "true", "HALO_VR_MULTIVIEW", _environment_value, _platform_android,
"Draw both eyes in one pass (GL_OVR_multiview2), from between them: about\n"
"half the drawing's cost on the processor. false: one eye after the other." },
{ "vr.log_performance", _config_boolean, "false", "HALO_VR_LOG_PERFORMANCE", _environment_value, _platform_android,
"Log the frame rate and where the frame time goes every five seconds\n"
"(adb logcat -s halo)." },
@@ -780,10 +783,12 @@ static const struct config_setting config_settings[] =
"machines of twice it), to test the netcode as over the internet; 0 none." },
{ "debug.network_loss", _config_real, "0.0", "HALO_NETWORK_LOSS", _environment_value, _platform_all,
"Percent of datagrams received that are dropped, for the same; 0 none." },
{ "debug.multiview_steamvr", _config_boolean, "false", "HALO_MULTIVIEW_STEAMVR", _environment_value,
_platform_android,
"On SteamVR (the Steam Frame) the eyes are drawn one by one, whatever\n"
"vr.multiview says: true draws them at once there too, for testing." },
{ "debug.multiview", _config_boolean, "false", "HALO_MULTIVIEW", _environment_value, _platform_android,
"Draw both eyes in one pass (GL_OVR_multiview2), from between them: about\n"
"half the drawing's cost on the processor, but the eyes' pictures did not\n"
"always fuse (doubled as the head turned: on the Steam Frame now and then,\n"
"on the Quest 3 in big online games). Off, one eye after the other; on,\n"
"for finding why." },
{ "debug.gl_thread", _config_integer, "-1", "HALO_GL_THREAD", _environment_value, _platform_android,
"Mesa's GL thread (its drivers: the Steam Frame's): 1 on, 0 off, -1 as the\n"
"driver chooses. Read when the app starts." },
+4
View File
@@ -395,6 +395,10 @@ void voice_audio_play(int speaker, unsigned short sequence, const unsigned char
if (length <= 0 || length > VOICE_MAXIMUM_PACKET)
return;
/* (every build sends a frame of 20 ms: anything else, damaged, is
dropped before the decoder sees it, and made up for as lost) */
if (opus_packet_get_nb_samples(packet, length, VOICE_RATE) != VOICE_FRAME_SAMPLES)
return;
pthread_mutex_lock(&voice_lock);
slot = voice_slot_for(speaker, TRUE);
if (!slot)
+119 -14
View File
@@ -138,7 +138,7 @@ static struct
float seat_frame_yaw;
int seat_frame_valid;
float seat_turn;
int multiview; /* vr.multiview: both eyes in one draw */
int multiview; /* debug.multiview: both eyes in one draw */
int multiview_logged;
int multiview_logged_views;
/* vr.foveation's level last logged (+1; 0: none yet) */
@@ -298,6 +298,15 @@ static struct
vr.resolution_scale as the app started with it */
float refresh_rate_wanted;
float resolution_scale;
/* vr.refresh_rate_auto (refresh_governor): the rate asked for now (the
one wanted, or lower while frames come late), the rates below and above
it, and the window's frames: all, late at this rate, and those that
would be late at the one above */
int refresh_auto;
float refresh_rate_now, refresh_lower, refresh_higher, refresh_period_ms;
unsigned long long refresh_window_us, refresh_changed_us;
unsigned long refresh_frames, refresh_late, refresh_tight;
int refresh_bad_windows, refresh_good_windows;
} vr;
/* d3d8_gl.c */
@@ -585,12 +594,23 @@ static void vr_read_live_settings(void)
/* (the refresh rate changes at once where the runtime lets it) */
float refresh_rate = (float)config_real("vr.refresh_rate");
if (vr.active && refresh_rate != vr.refresh_rate_wanted)
int automatic = config_boolean("vr.refresh_rate_auto");
if (vr.active && (refresh_rate != vr.refresh_rate_wanted ||
(!automatic && vr.refresh_rate_now != refresh_rate)))
{
platform_log("VR: refresh rate %.0f Hz asked for", refresh_rate);
host_xr_set_refresh_rate(refresh_rate);
}
if (refresh_rate != vr.refresh_rate_wanted || automatic != vr.refresh_auto)
{
/* (the governor starts over from the rate chosen) */
vr.refresh_rate_now = refresh_rate;
vr.refresh_window_us = 0;
vr.refresh_bad_windows = vr.refresh_good_windows = 0;
}
vr.refresh_rate_wanted = refresh_rate;
vr.refresh_auto = automatic;
}
}
@@ -625,7 +645,7 @@ void halo_vr_initialize(void)
vr.two_hand_reach = (float)config_real("vr.two_hand_reach");
vr.melee_swing_speed = (float)config_real("vr.melee_swing_speed");
vr.grenade_throw_speed = (float)config_real("vr.grenade_throw_speed");
vr.multiview = config_boolean("vr.multiview");
vr.multiview = config_boolean("debug.multiview");
vr.vehicle_first_person = config_boolean("vr.vehicle_first_person");
vr.vehicle_spawn = config_boolean("debug.vehicle_spawn");
vr.turret_handles = config_boolean("vr.turret_handles");
@@ -658,16 +678,13 @@ void halo_vr_initialize(void)
vr.active = 1;
platform_log("VR: %s, eyes %dx%d at %.0f Hz", vr.info.system_name, vr.info.eye_width, vr.info.eye_height,
vr.info.refresh_rate);
/* SteamVR (the Steam Frame): each eye drawn apart. With both drawn at
once the eyes' pictures did not fuse, now and then: each alone was
sharp, the two together doubled as the head turned (2026-10-05), and
one by one costs it no more (its time goes to the driver either way).
debug.multiview_steamvr draws them at once there again, for finding why */
if (vr.multiview && strstr(vr.info.system_name, "SteamVR") && !config_boolean("debug.multiview_steamvr"))
{
vr.multiview = 0;
platform_log("VR: SteamVR: the eyes drawn one by one (debug.multiview_steamvr)");
}
/* Each eye drawn apart (debug.multiview draws both at once). At once,
the eyes' pictures did not always fuse: on the Steam Frame now and then
(each alone sharp, the two together doubled as the head turned,
2026-10-05; one by one costs it no more there), and on the Quest 3 in
big online games (doubled from the start as the head turned, none with
48 bots; one by one, none online either, 2026-10-16) */
platform_log("VR: the eyes drawn %s (debug.multiview)", vr.multiview ? "at once" : "one by one");
}
int halo_vr_active(void)
@@ -889,6 +906,93 @@ static void performance_phases(unsigned long long now)
vr.frame_ticks = 0;
}
/* vr.refresh_rate_auto: frames late at the headset's rate (a game's big
online games: the ticks cost as a client in step with the players) are
shown twice by the headset, a doubled picture when turning. While more
than one in 32 of them come late, two windows of REFRESH_WINDOW_US
running, the rate steps down to the next the headset offers (the Quest
3's: 90, 80, 72 Hz); while hardly any would be late at the next one up (one in a
hundred), ten windows running, it steps back up, never above
vr.refresh_rate. Frames of a load (REFRESH_LOAD_MS and more) count for
neither; a step waits REFRESH_SETTLE_US (down) or REFRESH_RAISE_US (up)
after the last. */
#define REFRESH_WINDOW_US 2000000ULL
#define REFRESH_SETTLE_US 10000000ULL
#define REFRESH_RAISE_US 30000000ULL
#define REFRESH_LOAD_MS 50.0f
static void refresh_rates_around(void)
{
float rates[16], current = 0.0f;
int count = host_xr_refresh_rates(rates, 16, &current);
float ceiling = vr.refresh_rate_wanted > 1.0f ? vr.refresh_rate_wanted + 0.5f : 1e6f;
int index;
vr.refresh_lower = vr.refresh_higher = 0.0f;
if (current <= 1.0f)
current = vr.refresh_rate_now;
vr.refresh_period_ms = current > 1.0f ? 1000.0f / current : 0.0f;
for (index = 0; index < count; index++)
{
if (rates[index] < current - 0.5f && rates[index] > vr.refresh_lower)
vr.refresh_lower = rates[index];
if (rates[index] > current + 0.5f && rates[index] <= ceiling &&
(!vr.refresh_higher || rates[index] < vr.refresh_higher))
vr.refresh_higher = rates[index];
}
}
static void refresh_step(float rate, unsigned long long now, const char *why)
{
platform_log("VR: refresh rate %.0f Hz (from %.0f): %s (%lu of %lu frames late, %lu too slow for the rate above)",
rate, 1000.0f / vr.refresh_period_ms, why, vr.refresh_late, vr.refresh_frames, vr.refresh_tight);
host_xr_set_refresh_rate(rate);
vr.refresh_rate_now = rate;
vr.refresh_changed_us = now;
vr.refresh_bad_windows = vr.refresh_good_windows = 0;
}
static void refresh_governor(void)
{
unsigned long long now = now_us();
float work_ms = (float)(now - vr.frame_start_us) * 1e-3f;
if (!vr.refresh_auto)
return;
if (!vr.refresh_window_us)
{
refresh_rates_around();
vr.refresh_window_us = now;
vr.refresh_frames = vr.refresh_late = vr.refresh_tight = 0;
return;
}
if (work_ms < REFRESH_LOAD_MS && vr.refresh_period_ms > 0.0f)
{
vr.refresh_frames++;
if (work_ms + 1.0f > vr.refresh_period_ms)
vr.refresh_late++;
if (vr.refresh_higher > 1.0f && work_ms + 1.0f > 1000.0f / vr.refresh_higher)
vr.refresh_tight++;
}
if (now - vr.refresh_window_us < REFRESH_WINDOW_US)
return;
if (vr.refresh_frames)
{
vr.refresh_bad_windows = vr.refresh_late * 32 > vr.refresh_frames ? vr.refresh_bad_windows + 1 : 0;
vr.refresh_good_windows = vr.refresh_tight * 100 <= vr.refresh_frames ? vr.refresh_good_windows + 1 : 0;
if (vr.refresh_lower > 1.0f && vr.refresh_bad_windows >= 2 && now - vr.refresh_changed_us >= REFRESH_SETTLE_US)
refresh_step(vr.refresh_lower, now, "frames come late");
else if (vr.refresh_higher > 1.0f && vr.refresh_good_windows >= 10 &&
now - vr.refresh_changed_us >= REFRESH_RAISE_US)
{
refresh_step(vr.refresh_higher, now, "frames would keep up again");
}
}
refresh_rates_around();
vr.refresh_window_us = now;
vr.refresh_frames = vr.refresh_late = vr.refresh_tight = 0;
}
static void performance_report(int stereo)
{
unsigned long long now = now_us();
@@ -1886,6 +1990,7 @@ void halo_vr_present(unsigned int back_buffer_texture)
vr.last_overlay_mode = vr.overlay_mode;
vr.last_frame_stereo = submit.eyes;
vr.frame_begun = 0;
refresh_governor();
if (vr.log_performance)
performance_report(submit.eyes);
}
@@ -2053,7 +2158,7 @@ float halo_vr_refresh_rate(void)
return vr.active ? vr.info.refresh_rate : 0.0f;
}
/* ---------- both eyes at once (vr.multiview)
/* ---------- both eyes at once (debug.multiview)
The scene is drawn once, from a camera between the eyes whose view takes in
both eyes' (with a margin, for what is near: the eyes stand apart), into two
+26 -7
View File
@@ -7468,6 +7468,7 @@ enum
_vr_option_graphics,
_vr_option_foveation,
_vr_option_refresh_rate,
_vr_option_refresh_auto,
_vr_option_resolution,
_vr_option_controls,
_vr_option_quit,
@@ -7521,8 +7522,8 @@ static short const vr_option_pages[NUMBER_OF_VR_OPTION_PAGES][VR_OPTIONS_PER_PAG
_vr_option_button_first + HALO_VR_ACTION_CROUCH, _vr_option_button_first + HALO_VR_ACTION_ZOOM,
_vr_option_button_first + HALO_VR_ACTION_MELEE, _vr_option_button_first + HALO_VR_ACTION_TALK,
_vr_option_buttons_reset, _vr_option_controls, NONE, NONE },
{ _vr_option_graphics, _vr_option_foveation, _vr_option_refresh_rate, _vr_option_resolution, _vr_option_quit,
NONE, NONE, NONE, NONE, NONE, NONE, NONE },
{ _vr_option_graphics, _vr_option_foveation, _vr_option_refresh_rate, _vr_option_refresh_auto,
_vr_option_resolution, _vr_option_quit, NONE, NONE, NONE, NONE, NONE, NONE },
};
/* the page a row is on, and its place there */
@@ -7942,6 +7943,9 @@ static void ui_vr_options_change(
}
break;
}
case _vr_option_refresh_auto:
config_write_boolean("vr.refresh_rate_auto", !config_boolean("vr.refresh_rate_auto"));
break;
case _vr_option_resolution:
ui_vr_options_step("vr.resolution_scale", vr_resolution_scales, NUMBEROF(vr_resolution_scales), change);
break;
@@ -8651,7 +8655,10 @@ static void ui_vr_options_render(
short count = ui_vr_options_refresh_rates(rates, &chosen, &current);
label = "Refresh rate";
if (count > 0)
/* (lowered while frames come late: vr.refresh_rate_auto) */
if (count > 0 && current > 1.0f && current < rates[chosen] - 0.5f)
snprintf(value, sizeof(value), "%.0f Hz (now %.0f)", rates[chosen], current);
else if (count > 0)
snprintf(value, sizeof(value), "%.0f Hz", rates[chosen]);
else if (current > 1.0f)
snprintf(value, sizeof(value), "%.0f Hz (system)", current);
@@ -8660,6 +8667,17 @@ static void ui_vr_options_render(
unused = count <= 1;
break;
}
case _vr_option_refresh_auto:
{
/* (dim where the headset offers one rate: SteamVR's) */
real rates[16], current;
short chosen;
label = "Lower it when frames are late";
snprintf(value, sizeof(value), "%s", config_boolean("vr.refresh_rate_auto") ? "On" : "Off");
unused = ui_vr_options_refresh_rates(rates, &chosen, &current) <= 1;
break;
}
case _vr_option_resolution:
{
real scale = vr_resolution_scales[ui_vr_options_nearest((real)config_real("vr.resolution_scale"),
@@ -8717,12 +8735,11 @@ static void ui_vr_options_render(
}
if (page == _vr_options_page_buttons)
{
/* (what the buttons' rows do) */
ui_join_code_draw_text(128, 512, 302, "An action moved to a button swaps with the one there;", FALSE, 0,
/* (what the buttons' rows do: below the page's ten rows) */
ui_join_code_draw_text(128, 512, 320, "An action moved to a button swaps with the one there;", FALSE, 0,
&dim_color);
ui_join_code_draw_text(128, 512, 320, "left-handed and mirrored, the buttons are mirrored too.", FALSE, 0,
ui_join_code_draw_text(128, 512, 338, "mirrored, the buttons are too. The menus keep theirs.", FALSE, 0,
&dim_color);
ui_join_code_draw_text(128, 512, 338, "The menus keep their buttons.", FALSE, 0, &dim_color);
}
if (page == _vr_options_page_more)
{
@@ -8739,6 +8756,8 @@ static void ui_vr_options_render(
&dim_color);
ui_join_code_draw_text(128, 512, 320, "Foveation draws the view's edges coarser (Quest only).", FALSE, 0,
&dim_color);
ui_join_code_draw_text(128, 512, 338, "Late frames: the rate steps down (90, 80, 72) while they last.", FALSE, 0,
&dim_color);
}
ui_join_code_draw_text(126, 514, VR_OPTIONS_FOOTER + 8, "%a-button Change %b-button Done", TRUE, 0, &text_color);
}
+8 -5
View File
@@ -15,7 +15,13 @@ Usage: android_asm_convert.py input.s output.s
import re
import sys
IDENT = re.compile(r'"[^"]*"|[A-Za-z_.$][\w.$]*')
# a quoted name (its escapes kept: \" does not end it) or a plain one
IDENT = re.compile(r'"(?:[^"\\]|\\.)*"|[A-Za-z_.$][\w.$]*')
# directives whose operands are string literals, the same in both
# syntaxes: they go through as they are, never renamed as symbols (data
# starting with "_" would lose that byte)
STRING_DIRECTIVES = {".ascii", ".asciz", ".string"}
DROP_DIRECTIVES = {
".subsections_via_symbols", ".build_version", ".watchos_version_min",
@@ -217,10 +223,7 @@ class Converter:
if op == ".align":
self.emit(f"\t.p2align {args}")
return
if op in (".ascii", ".asciz", ".string"):
# string data, kept as it is: no symbol names in it (renamed, a
# "_" at the start of a word was dropped: a string with \" in it
# went wrong, Opus's tables among them)
if op in STRING_DIRECTIVES:
self.emit(f"\t{op} {args}")
return
if op.startswith("."):
+1 -1
View File
@@ -22,7 +22,7 @@ no files beside its guest image.
The data are 32-bit words, not bytes: the Android build passes the guest's
assembly through tools/android_asm_convert.py, which rewrites identifiers in
operands and would garble a long .ascii string of binary data.
operands (it keeps .ascii strings as they are, but once garbled them).
"""
import json
+5
View File
@@ -618,6 +618,11 @@ MAP_KIND_CHOOSER = "main_menu/new_select/list_item_0_map_kind"
# changes to the PC version's widgets (by our names): attributes set, all
# their handlers replaced, children added, game data inputs added
WIDGET_PATCHES = {
# The host returns here after a round with no menu history. Back must
# reopen the main menu when it closes the last screen, as the lobby does.
f"{MT}/connected/connected_map_select_wrapper": {"set": {
"flags": "pass_unhandled_to_focused_child pause_game main_menu_if_no_history tag_controller_index force_handle_mouse",
}},
# (the profile settings' picture: on Gamepad Setup's row, the profile's
# button settings, BITMAP_FRAMES; menu_functions.c's
# profile_gamepad_layout)
+65 -21
View File
@@ -1,32 +1,76 @@
"""Tests of tools/android_asm_convert.py, the Mach-O to ELF assembly
conversion of the Android build's guest code."""
"""The Darwin to ELF assembly conversion of the ILP32 guest
(tools/android_asm_convert.py): string literals keep every byte. It once took
a quoted .ascii operand for a quoted symbol name and dropped its leading "_"
(and, after an escaped quote, the "_" of any word after it), which shifted
tables such as Opus's pulse cache by a byte: voice chat crashed in
decode_pulses on the arm64 and Android builds."""
import shutil
import struct
import subprocess
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import pytest
from android_asm_convert import Converter # noqa: E402
from tools.android_asm_convert import Converter
ROOT = Path(__file__).resolve().parent.parent
def convert(*lines):
return Converter(list(lines)).run().split("\n")
return Converter(list(lines)).run().splitlines()
def test_symbols_lose_their_underscore():
out = convert("_main:", "\tbl\t_foo", "\t.long\t_bar")
assert out[:3] == ["main:", "\tbl foo", "\t.long bar"]
@pytest.mark.parametrize("directive", [
'.ascii\t"_c[}]L{s{"',
'.asciz\t"_transport_error_none"',
'.ascii\t"v\\177\\"O7v\\177_\\037\\374"',
'.ascii\t"=`\'\\\\_\\026"',
'.asciz\t"ltmp0 _x @PAGE ; not a comment"',
])
def test_string_literals_are_kept(directive):
op, args = directive.split("\t", 1)
assert convert("l_.str:", directive) == [".Ll_.str:", f"\t{op} {args}"]
def test_strings_are_kept():
# (a string starting with "_", or one with \" in it, lost a "_": Opus's
# pulse cache was one byte short, and its encoder crashed)
out = convert(
'\t.asciz\t"_lan_"',
'\t.ascii\t"\\"$&\'\\0063_\\206"',
'\t.string\t"l_.str _foo"',
)
assert out[:3] == [
'\t.asciz "_lan_"',
'\t.ascii "\\"$&\'\\0063_\\206"',
'\t.string "l_.str _foo"',
]
def test_symbols_are_still_renamed():
assert convert("\t.globl\t_table", "_table:", "\tadrp\tx8, _table@PAGE",
"\tadd\tx8, x8, _table@PAGEOFF", '\t.long\t"_odd name"') == [
"\t.globl table", "table:", "\tadrp x8, table", "\tadd x8, x8, :lo12:table", '\t.long "odd name"']
def rodata(elf: bytes) -> bytes:
"""the .rodata section of a 64-bit little-endian ELF object"""
shoff, = struct.unpack_from("<Q", elf, 0x28)
shentsize, shnum, shstrndx = struct.unpack_from("<HHH", elf, 0x3a)
sections = [struct.unpack_from("<IIQQQQ", elf, shoff + index * shentsize) for index in range(shnum)]
names = sections[shstrndx][4]
for name, _, _, _, offset, size in sections:
if elf[names + name:elf.index(b"\0", names + name)] == b".rodata":
return elf[offset:offset + size]
raise AssertionError("no .rodata")
def test_tables_survive_the_build(tmp_path):
"""every byte value, and the patterns that were lost, through the
guest's pipeline (clang for arm64_32, the conversion, the assembler)"""
clang = shutil.which("clang")
if not clang:
pytest.skip("no clang")
table = bytes(range(256)) + b'_a"_b\\_c' + bytes(reversed(range(256)))
source = tmp_path / "table.c"
source.write_text(
"const unsigned char table[] = {" + ",".join(str(b) for b in table) + "};\n"
'const char text[] = "_starts with an underscore";\n')
darwin, converted, obj = tmp_path / "table.darwin.s", tmp_path / "table.s", tmp_path / "table.o"
try:
subprocess.run([clang, "--target=arm64_32-apple-watchos", "-O2", "-S", str(source), "-o", str(darwin)],
check=True, capture_output=True)
except subprocess.CalledProcessError:
pytest.skip("clang has no arm64_32 target")
subprocess.run([sys.executable, str(ROOT / "tools" / "android_asm_convert.py"), str(darwin), str(converted)],
check=True)
subprocess.run([clang, "--target=aarch64-linux-gnu", "-c", str(converted), "-o", str(obj)], check=True)
data = rodata(obj.read_bytes())
assert table in data
assert b"_starts with an underscore\0" in data