mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-17 12:00:26 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a10a413c8 | ||
|
|
5ce39c27a7 | ||
|
|
67d738451c | ||
|
|
91bdb7e67c | ||
|
|
bf778b6d60 | ||
|
|
d8a786f5a1 | ||
|
|
7c294c9c39 | ||
|
|
f90d955527 | ||
|
|
085676d827 | ||
|
|
4301ca88d6 | ||
|
|
9c70f5f5e3 | ||
|
|
d9669f6eef |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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." },
|
||||
|
||||
@@ -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
@@ -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, ¤t);
|
||||
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
|
||||
|
||||
@@ -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, ¤t);
|
||||
|
||||
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, ¤t) <= 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);
|
||||
}
|
||||
|
||||
@@ -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("."):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user