Files

16141 lines
850 KiB
Diff

diff --git a/port/android/guest/runtime/guest_host.h b/port/android/guest/runtime/guest_host.h
index e77403c2d2c98ca312703036483906f7a1071925..e133ed2ceb4c407ff3fa13b38b47608c5683f020 100644
--- a/port/android/guest/runtime/guest_host.h
+++ b/port/android/guest/runtime/guest_host.h
@@ -102,6 +102,16 @@ the GPU to finish the work last fenced for a slot */
void host_gl_fence_frame(unsigned int slot);
void host_gl_wait_frame(unsigned int slot);
+#if defined(HALO_ARM64_GUEST) && !defined(HALO_GLES)
+/* Native desktop experiment: the host owns the mapping, never a guest pointer.
+Creation returns a fresh, mapped buffer or 0, with failed objects deleted. */
+unsigned int host_gl_buffer_create_persistent(unsigned int size);
+void host_gl_buffer_destroy_persistent(unsigned int buffer);
+int host_gl_buffer_write_persistent(unsigned int buffer, unsigned int offset, unsigned int size, const void *data);
+/* Unlike the legacy wait, a timeout preserves the fence and returns 0. */
+int host_gl_wait_frame_checked(unsigned int slot);
+#endif
+
/* ---------- Android */
/* the storage directories the port uses, copied into buffer */
diff --git a/port/android/host/host_gl.c b/port/android/host/host_gl.c
index 16bf3ecb7a798886225409f379db0f891dfe3761..b900917b925cf0e1d6b393cb92a55779ddae338a 100644
--- a/port/android/host/host_gl.c
+++ b/port/android/host/host_gl.c
@@ -103,6 +103,7 @@ waiting to run would otherwise read a later frame's vertices. */
#define FRAME_FENCE_SLOTS 8
static GLsync frame_fences[FRAME_FENCE_SLOTS];
+static int frame_fence_failed[FRAME_FENCE_SLOTS];
void host_gl_fence_frame(uint32_t slot)
{
@@ -111,6 +112,7 @@ void host_gl_fence_frame(uint32_t slot)
if (frame_fences[slot])
glDeleteSync(frame_fences[slot]);
frame_fences[slot] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+ frame_fence_failed[slot] = frame_fences[slot] == NULL;
}
void host_gl_wait_frame(uint32_t slot)
@@ -123,6 +125,124 @@ void host_gl_wait_frame(uint32_t slot)
frame_fences[slot] = NULL;
}
+/* Neither persistent nor unsynchronized mutable uploads may reuse storage
+after a timeout or lost context. The native renderer can orphan mutable
+storage instead of blocking the render thread; retain an unfinished fence. */
+int host_gl_wait_frame_checked(uint32_t slot)
+{
+ GLenum status;
+
+ if (slot >= FRAME_FENCE_SLOTS || frame_fence_failed[slot])
+ return 0;
+ if (!frame_fences[slot])
+ return 1;
+ status = glClientWaitSync(frame_fences[slot], GL_SYNC_FLUSH_COMMANDS_BIT, 1000000ull);
+ if (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED)
+ return 0;
+ glDeleteSync(frame_fences[slot]);
+ frame_fences[slot] = NULL;
+ return 1;
+}
+
+/* Native desktop GL only. The ordinary GLES path retains its current uploads.
+Six ring buffers fit here; records abandoned after a failed fence remain alive
+until context teardown and are never reused, bounding that retirement to six.
+Every allocation is transactional: no failed immutable object reaches the
+renderer, and no GL object name can be confused with a guest memory address. */
+#define PERSISTENT_BUFFER_SLOTS 16
+static struct
+{
+ GLuint buffer;
+ uint32_t size;
+ unsigned char *mapping;
+} persistent_buffers[PERSISTENT_BUFFER_SLOTS];
+
+uint32_t host_gl_buffer_create_persistent(uint32_t size)
+{
+#ifdef HALO_DESKTOP_GL
+ void (*buffer_storage)(GLenum, GLsizeiptr, const void *, GLbitfield);
+ const GLbitfield flags = GL_MAP_WRITE_BIT | 0x0040 /* PERSISTENT */ | 0x0080 /* COHERENT */;
+ GLint major = 0, minor = 0, previous = 0;
+ GLuint buffer = 0;
+ unsigned char *mapping;
+ int slot;
+
+ if (!size)
+ return 0;
+ glGetIntegerv(GL_MAJOR_VERSION, &major);
+ glGetIntegerv(GL_MINOR_VERSION, &minor);
+ if (!(major > 4 || (major == 4 && minor >= 4) || host_gl_has_extension("GL_ARB_buffer_storage")))
+ return 0;
+ buffer_storage = (void (*)(GLenum, GLsizeiptr, const void *, GLbitfield))host_gl_resolve("glBufferStorage");
+ if (!buffer_storage)
+ return 0;
+ for (slot = 0; slot < PERSISTENT_BUFFER_SLOTS && persistent_buffers[slot].buffer; slot++)
+ ;
+ if (slot == PERSISTENT_BUFFER_SLOTS)
+ return 0;
+ glGetIntegerv(GL_COPY_WRITE_BUFFER_BINDING, &previous);
+ glGenBuffers(1, &buffer);
+ if (!buffer)
+ return 0;
+ glBindBuffer(GL_COPY_WRITE_BUFFER, buffer);
+ /* DYNAMIC_STORAGE allows a safe glBufferSubData fallback if a later
+ write cannot find its host record. It never permits glBufferData. */
+ buffer_storage(GL_COPY_WRITE_BUFFER, size, NULL, flags | 0x0100 /* DYNAMIC_STORAGE */);
+ mapping = glMapBufferRange(GL_COPY_WRITE_BUFFER, 0, size, flags);
+ if (!mapping)
+ {
+ /* Storage may already be immutable even when mapping fails. */
+ glDeleteBuffers(1, &buffer);
+ glBindBuffer(GL_COPY_WRITE_BUFFER, (GLuint)previous);
+ return 0;
+ }
+ persistent_buffers[slot].buffer = buffer;
+ persistent_buffers[slot].size = size;
+ persistent_buffers[slot].mapping = mapping;
+ glBindBuffer(GL_COPY_WRITE_BUFFER, (GLuint)previous);
+ return buffer;
+#else
+ (void)size;
+ return 0;
+#endif
+}
+
+/* Used only to roll back initialization, before any draw references the buffer.
+After an uncertain fence, the renderer instead retains the mapped object. */
+void host_gl_buffer_destroy_persistent(uint32_t buffer)
+{
+ int slot;
+
+ for (slot = 0; slot < PERSISTENT_BUFFER_SLOTS; slot++)
+ {
+ if (persistent_buffers[slot].buffer == buffer && buffer)
+ {
+ glDeleteBuffers(1, &persistent_buffers[slot].buffer);
+ memset(&persistent_buffers[slot], 0, sizeof(persistent_buffers[slot]));
+ return;
+ }
+ }
+}
+
+int host_gl_buffer_write_persistent(uint32_t buffer, uint32_t offset, uint32_t size, const void *data)
+{
+ int slot;
+
+ for (slot = 0; slot < PERSISTENT_BUFFER_SLOTS; slot++)
+ {
+ if (persistent_buffers[slot].buffer == buffer && buffer)
+ {
+ if (offset > persistent_buffers[slot].size || size > persistent_buffers[slot].size - offset ||
+ (size && !data))
+ return 0;
+ if (size)
+ memcpy(persistent_buffers[slot].mapping + offset, data, size);
+ return 1;
+ }
+ }
+ return 0;
+}
+
/* writes data into the buffer bound to target. The renderer streams a
range per draw, so a frame makes hundreds of these, and the cost per call
rather than per byte is what a frame is made of.
diff --git a/port/assets/menus/ce/bitmaps.xml b/port/assets/menus/ce/bitmaps.xml
index a7013c7415b1fff3e94f692f8b42773a51204b68..e5ce675c8a3d46b03fb7eafad02f45d90e258d4f 100644
--- a/port/assets/menus/ce/bitmaps.xml
+++ b/port/assets/menus/ce/bitmaps.xml
@@ -328,14 +328,17 @@
<bitmap name="pause/pausebox_left">
<frame png="ce/port/pause/pausebox_left__0.png" width="16" height="256"/>
<frame png="ce/port/pause/pausebox_left__1.png" width="16" height="256"/>
+ <frame png="ce/port/pause/pausebox_left__2.png" width="16" height="512"/>
</bitmap>
<bitmap name="pause/pausebox_center">
<frame png="ce/port/pause/pausebox_center__0.png" width="4" height="256"/>
<frame png="ce/port/pause/pausebox_center__1.png" width="4" height="256"/>
+ <frame png="ce/port/pause/pausebox_center__2.png" width="4" height="512"/>
</bitmap>
<bitmap name="pause/pausebox_right">
<frame png="ce/port/pause/pausebox_right__0.png" width="16" height="256"/>
<frame png="ce/port/pause/pausebox_right__1.png" width="16" height="256"/>
+ <frame png="ce/port/pause/pausebox_right__2.png" width="16" height="512"/>
</bitmap>
<bitmap name="blank">
<frame png="ce/port/blank__0.png" width="4" height="4"/>
diff --git a/port/assets/menus/ce/main_menu.multiplayer_type_select.coop.xml b/port/assets/menus/ce/main_menu.multiplayer_type_select.coop.xml
index 1368453f1fd9f19937f27149eb2c9928becf72e2..8612491537dd482e464f9c583c6e101ae00f2883 100644
--- a/port/assets/menus/ce/main_menu.multiplayer_type_select.coop.xml
+++ b/port/assets/menus/ce/main_menu.multiplayer_type_select.coop.xml
@@ -2,35 +2,54 @@
<!-- The port's multiplayer screens, in the PC version's style (tools/port_settings.py) -->
<menus>
<widget name="main_menu/multiplayer_type_select/multiplayer_type_coop_item" type="text" left="51" width="232" height="32" bitmap="bitmaps/list_item_bkd" string_list="main_menu/multiplayer_type_select/multiplayer_options" string_index="7" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="5">
- <on event="a" run="port coop begin" open="main_menu/multiplayer_type_select/coop/player_2_profile_screen" branch="true"/>
- <on event="start" run="port coop begin" open="main_menu/multiplayer_type_select/coop/player_2_profile_screen" branch="true"/>
+ <on event="a" run="port coop begin" open="main_menu/multiplayer_type_select/coop/network_screen" branch="true"/>
+ <on event="start" run="port coop begin" open="main_menu/multiplayer_type_select/coop/network_screen" branch="true"/>
<on event="left_mouse" run="mouse emit accept event"/>
</widget>
- <widget name="main_menu/multiplayer_type_select/coop/player_2_profile_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
+ <widget name="main_menu/multiplayer_type_select/coop/network_screen" width="640" height="480" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/gradient">
+ <on event="b" run="port coop begin" back="true"/>
+ <on event="back" run="port coop begin" back="true"/>
<child widget="main_menu/new_select/sel_list_desc_bkd"/>
- <child widget="main_menu/multiplayer_type_select/coop/player_2_profile_list"/>
- <child widget="main_menu/multiplayer_type_select/coop/header_player_2"/>
- <child widget="main_menu/multiplayer_type_select/coop/player_2_help" x="20" y="416"/>
- </widget>
- <widget name="main_menu/multiplayer_type_select/coop/header_player_2" controller="1" left="35" top="11" width="363" height="59" bitmap="main_menu/multiplayer_type_select/coop/header_player_2">
- </widget>
- <widget name="main_menu/multiplayer_type_select/coop/player_2_profile_list" type="column_list" width="640" height="480" flags="pass_unhandled_to_focused_child up_down_tabs_children" description="main_menu/profile_manager/player_profile_extended_desc">
- <data input="3wide player profile list update"/>
- <on event="created" run="port coop player 2 list initialize"/>
- <on event="deleted" run="player profile list dispose"/>
- <on event="custom_activation" run="port coop player 2" open="main_menu/solo_level_select/solo_level_select_screen" branch="true"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_0" x="20" y="73"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_1" x="20" y="103"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_2" x="20" y="133"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_3" x="20" y="163"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_4" x="20" y="193"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_5" x="20" y="223"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_6" x="20" y="253"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_7" x="20" y="283"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_8" x="20" y="313"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_9" x="20" y="343"/>
- <child widget="main_menu/multiplayer_type_select/coop/list_item_10" x="20" y="373"/>
- <child widget="main_menu/multiplayer_type_select/coop/player_2_button_bar" y="414"/>
+ <child widget="main_menu/multiplayer_type_select/coop/network_title" x="35" y="25"/>
+ <child widget="main_menu/multiplayer_type_select/coop/network_list"/>
+ <child widget="main_menu/multiplayer_type_select/coop/network_help" x="30" y="340"/>
+ <child widget="common_button_back" x="510" y="415"/>
+ </widget>
+ <widget name="main_menu/multiplayer_type_select/coop/network_title" type="text" controller="1" width="570" height="35" text="CO-OP CAMPAIGN" font="ui\large_ui" color="#FF2896FF" text_flags="no_focus_test">
+ </widget>
+ <widget name="main_menu/multiplayer_type_select/coop/network_list" type="column_list" width="640" height="310" flags="pass_unhandled_to_focused_child up_down_tabs_children">
+ <child widget="main_menu/multiplayer_type_select/coop/coop_host_lan" x="45" y="90"/>
+ <child widget="main_menu/multiplayer_type_select/coop/coop_join_lan" x="45" y="132"/>
+ <child widget="main_menu/multiplayer_type_select/coop/coop_host_online" x="45" y="174"/>
+ <child widget="main_menu/multiplayer_type_select/coop/coop_join_online" x="45" y="216"/>
+ <child widget="main_menu/multiplayer_type_select/coop/coop_join_invite" x="45" y="258"/>
+ </widget>
+ <widget name="main_menu/multiplayer_type_select/coop/coop_host_lan" type="text" width="550" height="35" bitmap="bitmaps/list_item_bkd" text="HOST LOCAL (LAN)" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="5">
+ <on event="a" run="port coop begin"/>
+ <on event="start" run="port coop begin"/>
+ <on event="left_mouse" run="mouse emit accept event"/>
+ </widget>
+ <widget name="main_menu/multiplayer_type_select/coop/coop_join_lan" type="text" width="550" height="35" bitmap="bitmaps/list_item_bkd" text="JOIN LOCAL (LAN)" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="5">
+ <on event="a" run="port coop begin"/>
+ <on event="start" run="port coop begin"/>
+ <on event="left_mouse" run="mouse emit accept event"/>
+ </widget>
+ <widget name="main_menu/multiplayer_type_select/coop/coop_host_online" type="text" width="550" height="35" bitmap="bitmaps/list_item_bkd" text="HOST ONLINE (INTERNET)" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="5">
+ <on event="a" run="port coop begin"/>
+ <on event="start" run="port coop begin"/>
+ <on event="left_mouse" run="mouse emit accept event"/>
+ </widget>
+ <widget name="main_menu/multiplayer_type_select/coop/coop_join_online" type="text" width="550" height="35" bitmap="bitmaps/list_item_bkd" text="JOIN ONLINE (INTERNET)" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="5">
+ <on event="a" run="port coop begin"/>
+ <on event="start" run="port coop begin"/>
+ <on event="left_mouse" run="mouse emit accept event"/>
+ </widget>
+ <widget name="main_menu/multiplayer_type_select/coop/coop_join_invite" type="text" width="550" height="35" bitmap="bitmaps/list_item_bkd" text="JOIN INVITE LINK" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="5">
+ <on event="a" run="port coop begin"/>
+ <on event="start" run="port coop begin"/>
+ <on event="left_mouse" run="mouse emit accept event"/>
+ </widget>
+ <widget name="main_menu/multiplayer_type_select/coop/network_help" type="text" controller="1" width="580" height="65" text="One player per headset or PC. LAN uses your local network.&#10;Online hosts: share your invite, or choose PUBLIC in Server Setup." font="ui\small_ui" color="#FFFFFFFF" text_flags="no_focus_test">
</widget>
<widget name="main_menu/multiplayer_type_select/coop/list_item_0" width="390" height="28" bitmap="bitmaps/sel_list_item_bkd_top" font="ui\large_ui" color="#FF2896FF" align="center" text_y="3">
<on event="a" run="single prev cl item activated"/>
@@ -109,8 +128,6 @@
<child widget="main_menu/new_select/list_item_text" x="25"/>
<child widget="main_menu/new_select/list_item_arrows"/>
</widget>
- <widget name="main_menu/multiplayer_type_select/coop/player_2_help" type="text" width="350" height="24" text="Player 2: choose with your controller." font="ui\small_ui" color="#FF2896FF" text_y="5" text_flags="no_focus_test">
- </widget>
<widget name="main_menu/multiplayer_type_select/coop/player_2_button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
<data input="common button bar update"/>
<child widget="main_menu/profile_manager/profile_manager_button_ok" x="380" y="1"/>
diff --git a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.vr_display.xml b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.vr_display.xml
index 81a54a33c55739790c5a5c383fce954f9a0f6cc8..0580b01566eab33aa3d8fce71896a63174201bb3 100644
--- a/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.vr_display.xml
+++ b/port/assets/menus/ce/main_menu.settings_select.player_setup.player_profile_edit.vr_display.xml
@@ -22,6 +22,7 @@
<child widget="main_menu/settings_select/player_setup/player_profile_edit/vr_display/op_menu_width" x="54" y="241"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/vr_display/op_resolution_scale" x="54" y="269"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/vr_display/op_refresh_rate" x="54" y="297"/>
+ <child widget="main_menu/settings_select/player_setup/player_profile_edit/vr_display/op_persistent_stream_buffers" x="54" y="325"/>
<child widget="main_menu/settings_select/player_setup/player_profile_edit/vr_display/button_bar" y="414"/>
</widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/vr_display/button_bar" type="column_list" width="640" height="28" flags="pass_unhandled_to_focused_child left_right_tabs_items">
@@ -110,6 +111,15 @@
<widget name="main_menu/settings_select/player_setup/player_profile_edit/vr_display/refresh_rate_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="HEADSET'S|72 HZ|80 HZ|90 HZ|120 HZ|144 HZ" setting="vr.refresh_rate" values="0|72|80|90|120|144" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
<on event="created" run="port setting load"/>
</widget>
+ <widget name="main_menu/settings_select/player_setup/player_profile_edit/vr_display/op_persistent_stream_buffers" width="512" height="28" flags="pass_unhandled_to_focused_child" bitmap="bitmaps/option_bkds" color="#FF2896FF">
+ <child widget="main_menu/settings_select/player_setup/player_profile_edit/vr_display/persistent_stream_buffers_label"/>
+ <child widget="main_menu/settings_select/player_setup/player_profile_edit/vr_display/persistent_stream_buffers_spinner" x="320" y="1"/>
+ </widget>
+ <widget name="main_menu/settings_select/player_setup/player_profile_edit/vr_display/persistent_stream_buffers_label" type="text" controller="1" width="300" height="22" string_list="main_menu/settings_select/player_setup/player_profile_edit/vr_display/labels" string_index="9" font="ui\large_ui" color="#FF2896FF" text_x="13" text_y="4">
+ </widget>
+ <widget name="main_menu/settings_select/player_setup/player_profile_edit/vr_display/persistent_stream_buffers_spinner" type="spinner" left="3" top="2" width="147" height="20" flags="pass_unhandled_to_focused_child left_right_tabs_items" strings="ON|OFF" setting="display.persistent_stream_buffers" values="true|false" font="ui\large_ui" color="#FF2896FF" align="center" text_y="1" header_bitmap="bitmaps/arrow_sm_left" footer_bitmap="bitmaps/arrow_sm_right" header_bounds="7 -6 19 0" footer_bounds="7 150 19 156">
+ <on event="created" run="port setting load"/>
+ </widget>
<widget name="main_menu/settings_select/player_setup/player_profile_edit/vr_display/button_defaults" type="text" width="128" height="24" bitmap="bitmaps/text_button_background" string_list="strings/common_button_captions" string_index="3" font="ui\small_ui" color="#FFFFFFFF" align="center" text_y="2">
<on event="a" run="port settings defaults"/>
<on event="start" run="port settings defaults"/>
@@ -130,6 +140,7 @@
<string text="MENU WIDTH:"/>
<string text="RESOLUTION (RESTART):"/>
<string text="REFRESH RATE (RESTART):"/>
+ <string text="BUFFER STREAMING (RESTART):"/>
</strings>
<strings name="main_menu/settings_select/player_setup/player_profile_edit/vr_display/help_strings">
<string text=""/>
@@ -142,5 +153,6 @@
<string text="How wide the menus are."/>
<string text="Of the size the headset recommends: less is\nfaster, more sharper. From the next start."/>
<string text="The headset's rate, if it offers it; the game\ndraws a frame each refresh. From the next start."/>
+ <string text="Experimental upload buffers; default off.\nCompare on/off after restarting the game."/>
</strings>
</menus>
diff --git a/port/assets/menus/ce/port/pause/pausebox_center__2.png b/port/assets/menus/ce/port/pause/pausebox_center__2.png
new file mode 100644
index 0000000000000000000000000000000000000000..19fdee471b1e4b98318b63c64ac41c5dea67e7c4
GIT binary patch
literal 633
zcmeAS@N?(olHy`uVBq!ia0vp^0t^fs3><7gmj0>DbAc3Rfk$L91A~q?2s19VowkyJ
zfl1rb#WAE}&f5!yxh#w#&Ifbt1-u)RS{5c}NDEw#edd`Ld*<`!-}~mjDg02^d~oUS
zl>TNzjb?*{0Jg*eo?{Dm4n2_Qag=O1XeiNa#L=8=z?Le&c07USlmbuRL5V4j5;9=r
z7HrAHYiI{+GyvK{k~WaDh}3Wx>_9S|M~poW62SpMQ7~Y&#{d${AHXy-+h3{X`S-v7
o1xCtuX&x2NNwh;>*8gJGerI~%jr&FiU|M4EboFyt=akR{0C-f>nE(I)
literal 0
HcmV?d00001
diff --git a/port/assets/menus/ce/port/pause/pausebox_left__2.png b/port/assets/menus/ce/port/pause/pausebox_left__2.png
new file mode 100644
index 0000000000000000000000000000000000000000..283a2d03dd027de298987d0b958c445316d1b897
GIT binary patch
literal 4394
zcmeH~dr(tX9><ejw^1uxcUFsv5W3yg-SNd(6%|ORkAhXiHu9DbWRW7sOP(RQ$t;3(
zaX@RAhY<oQB9A5_$WtOFQd9y#AiN<^ARz%vFu6Q#lA8_I8J*EqcW3KA-8*x?=gzsm
zIrrSp?{|LZcg`L@<Y=;V%~A{oW3u0A@7KoG0fSkLS^U27DSdF8*w`!yck(=i!E89N
zaAWiB(+e?}<^B8jetsnG@>IX?jZrwharzXgP5I9QX{BDR9igfoNo-I_Q2w~{MW-U&
zvF?fvw>=kY9gaHXnytL}-Q+PVv;E$;wKAKOB(y17?tGK6d-(AhCx<SNhLD3nrAwSy
z%;KPrdYn560U;~o4MNWW`%^KrIttfb9f!6EWp$;c@7dUS)FzL>IetZe;0UPk)K6K9
z+v#qGT|z~NwK$7V+?&cU?+cKW)<k3~2L0U9LS(>EITnJ7Dq&ALEmK@p4CVGwnLEf`
zAc@X%iH;G2;oe|tV6;Ft5ibT$^nu}e04C9k1ZD!mv>IaixmAKBy{riTpaMVCD}d88
z3|u7>S2~63PPjI+jATFk1u95LyG*E+kwkV3u@7h;+hEb?63UzX)4CW*RG0gvI<&<x
z7S^YguR3Lmp1i|Rs>+bGE_Ln*s?4rN{C#y&=?^$|_qIt!T&?A3ylp+tALbCmDKey9
z0*&{WZIT$O3wp!*QsY{=6Bw-&gxn;=3>;53^pM1=q&>Y~gda60)Q6XIScX*Im<y~W
z+kIxJA;XEbn{nN?hU8dNUo|m7<S(fnwYaMvFT!`n@2ua(AQP*_${E39JTGcYsgoPg
zILKfOy{IQ0x@JM3&-_qHmp*hQW~!;oVGn(0CseNQ%4(j~4f+>DRz}M%OU{7ch=hzb
z2|=hlUOkY;MLxeg3oI+URPl5hqbE-3?ykqQXt9XSrFWywHFgYVn7pD*uDlH#gkzg5
zb=zj`&}>)6kl83kZ^&u&&_x>@H|Ov&p%!*KHn|X`_Fy4o=3`L_lE|}q##`fAS3Vi5
zb!F6>O&VvqrwLVx4Cj1g0FB<mE-ywYo4c_{NvPKh!|+tkrk^+oS|5y7-tvu&HBmC+
zvO!f^ifK6PXh2!TsDsfqH1qD^>e1UZ&EB0#mZTUrPO3|o#@iK$LS<#;eMavW0Ut1C
z7>ZyK7fHO+ii@K>#&UM4l4P9-hW;=m%e_BB7d(NkR%!TQOr`$*S(s2?lH(o49I%1r
znigK@t^h}o7LXP+0EQobjP0Cvq-dvY?j<pR5(J21s#c%pE#l8IHO|($6;8zW1xl>k
zEKkLvAg()f8_%PwmNHDP6VC|LSox9*s&j{_Uv98lmzlG|M6|S^pg}P4o;>M->d|3p
z(1ttPGs$aBxTXaejRGyuX8sqx?cO%;zw;ALx3RAAYwx%9jq<gN+RZ-^*zRlh4iF?H
zwX^aCwGHikV*;|3$)`09PFcbzR^FCN3OE0N?OjvvH-+EwPED7GR9b8rk%!uorD`8y
z?xGdFrZ?;|MQ7$|m)^0&s^_<`l~Q#8LDDR1qVZUwjCq<3d$W2zl;A5>572l*S?xS6
zOH^rbbJ~Uts^_=9V|km&J^PC=Tgpfn0WVbt{l|h}or#nV@EZhs6>?|?SHZv~sQw1N
zRResXXI{r!D{x!&%&&;Ka@@IBO*O!7q7@9(Kn`+Tl%ClJjw9H7d@BrmNz9$VeGfTw
zfq&IAIrzf}*63>u&};hb+Hu@z$U*V0<atp2w!V&kGpRR0Z3P$WnH_K9_X6}cwpNk3
z_~i(;@LkFC(0apKc?on({f`nFK`3i~Ya##lk@$iyM$c>oonDT*7t~%Go)y5KaR@dK
zfBv0`RP9G#zno!SqV~E}bP^Y$YnBOUgtrp-ZC36JUca))nUKRc?%FG>_?LP1VJ`sB
z@jvsyZ!hY3YqrZP8T*%cRsh@4=lGxb;8%qEmPzpcD1Q#;*Q5NOQv7*?`rYk1Y3i+P
z{PkUB<NwO@p&LF9))%X+#cE^eDYqr6@?WVDHCKQTRH0;hOaPif)L12Qsz&A>MjMtr
zD>rK<@?@QQ4DLe<is;@&5f{cHN0j6!W}V|9wvP;-WWI1HLP`#8-o+27w4@$0*1R@C
z#f5#iD&(rKu9J$V6eXBJtNRU-z?g+9_#}8!=`m0>ITe|qH1v~#6PVQmahI9J(9{{;
zG~a0MhRdLT7%~{CI(yr`KKu^E5h?N;*nbe^c=Z87xDY+j6L@cBC7ynq4u+RQg)T5L
z#dP^>U7Ym0i6?PU8$s*J$+WBUcY~kA>uQao)60+sg19HZvFd3eV|vodl42b2!7X4}
zBQ)0*pwwzE2FEjl!LBESVuo+b={klkv5aLS(a-f-P&h6ytI1ULi2HD%r`y;QnyDC1
zPYs{b^i&ScWHG#IfujjkgKdHEoLj23AxHHY9%%|I5o5_}^Pk$3I4=6FZtkp-OdZHJ
zYgR5X_kJ7*r~0`SR8UV>84?1?(3WVWwsQn?N_!k$Pj{0@6n`Yoe-K$fWR*v3k~*$B
z4)d$m4v+1jA#C3&T^Bl3m_2vx(T~+@Qaht%Pwj^dh6l91Ms(OaV}LqdT#ImtkzZL-
zLX8K|>n?Ej#7=&3<Bmw_WKQBb7oX^%JVxzLEySJG@lESoCV6rWSLmD4-x8Q-L*|-W
zmavUq>j_}kKhZGE!#a3{kbkD4FRT|3>iPK2Pt>AhKQ|(kWI41l;tmIY9pN`<0!kx3
z5;DGCMxsOOMe9q(u;hrT;TmeDB366Eh4(j5<&{u?j1k0n1lLUo8zVOwJKQDiCncp+
zcedCu*>k|=Al#ZpJb(tEIr#x${WasD@!<b^yh4tiL(NFp-#4$j`l;Unfc^Up?d9yI
G{NvxJ5zp%Y
literal 0
HcmV?d00001
diff --git a/port/assets/menus/ce/port/pause/pausebox_right__2.png b/port/assets/menus/ce/port/pause/pausebox_right__2.png
new file mode 100644
index 0000000000000000000000000000000000000000..32996d6d40fef19bc8377914fb92d01c98b23786
GIT binary patch
literal 4416
zcmeHLc~n!`8BZo-BdCNC(Wxxq9BXw%zy*nlA&kSKA_k>}DMJ#h8^{tBfv_Y&XOL0^
ziYSX9p+!N&5Eum1kOdGCSrP#W5lGl%i!32zCrMw>KU&UAYiC;iX#382-@E7c-hKCb
zzwh4P{l1svb<BN@;YLFc2(;$#A(s=t)fog@30nCJpp_}u^BTAqTsZW*a1dy_hyDcL
zLuQtMK*n>2UB3JcA#bV=_i*Gbrb*1`IN(um#>2qC>vW5}6`%Co+C8#^=Qn!Cm4lC(
z(1X0}l2i7)a>;cwJUEo_#}NOX=nJvD$rQ$k-b*Lnkoas@n{`_^IwU+w=N46X26=2M
z$7Iod`RNG7@tp2rQ6<fHjxLbIw)6LepB<G598w-2hHtltxFKEMo%iua3oUa-Bjh1n
z`Fu=7gFOwWNzhagdwsG6cOgB`q<tY>SAx1IQ*V4(ar-Tu8IxPic9C1%G>x0pqC3TJ
z!AEsb7CCLAN;8S-UZ)f^nGlTSa7Lv{OkvqxBrKqIa?4O>=N)M^aPR!ZHaFtA+dSy8
zUPv}}{~w}>s;j-F7IN^TaphV6-r``RylCZgEGI_32Ir*aIcZ;qN6nxJ+8LrYDH$am
zmm{VL(=7C?m1TDSZ&5WJP;RDqdunMIY4)m6m$l^-r!z5CV$4L+D{a*!2jXQf8?qU?
z{O~emi+iMCp$C>ad~py;cf%;XiJ&P{9^bcrexN;<rR~~j`D}O7<&~i;!Y=b8+N)}i
zYip3wzMbavX}0~oGk&>{z*my%Elz|PS9t6Iy=gAHJQ#<0<N)PjYuo8s@3c0VX5>YY
z(MhZv_k9g1l4?}o%CXR7COXKR!U-9P<xx{c1vZveLUofI|4u2M!{ta~NW5B7IDory
zYOu;gToOYULD-GXjhd#_JnRQQY9uF?;)#TeGy`&FCX~A#cAcduX>t&IU)VeSpoKRF
z@|%xqYGTe&aur`F#;TC<yX|_3z{_8~g0CBsE{O82s!SC_=x(V@xG^nksCZy^a4aZv
zc(U&q{J1+-y1F7%x`B9uRw;}c_RVs#Y8O2R-mxATB~U4jlMT;GVnV2}fxKXR>}fu_
zuEnmh##C_?-I%;BmvCmH2zazHV$hv6J8}b}=<tKH^+5=11|N{umo|T+NyvW?tiIfT
zCB@OmLi&0Xqn<<pgC~oIa*b?bk>ZA6B4~^}GQA~p;mTz-(EAA;6@8%@92Z7~1(<LQ
zS=HUpa)MbHfP@21T(>S`;hQN<!f!;7uNSuY%lrd+eaPv>DnA*bu9~X-#KKu&a7eeJ
z{*-f!@oLtJ4qDyp0W0KZhMZ43y$5G&oK>f`F6_zHnj6uecQ?4yTOXX*Trqn)Am+Gz
z?Y2DZdf3dWy9l>>$FC-+6|;hXm~i`A$2@^0i~>6Z|I<o}@nLwn0p+tJ@EeAdwI1+{
zRTPt>@SCeC>pbDN)=*53!8460>(THmSnL)1?9bP4I<sqc$hDV&=f6(5#qTRO+uqQ8
za-UyOWkclS>&1yuLJkL}iC^m#Xzdlfg94K%?U&iWM<r*RP_1JvainZcmU3YS#ZIC;
z>4Y1VG{iAm#}busfQJ(0E(+edT#Q&|=N9?+k9)|2jjO<(r~(nxUK3vi=gvc(lAJsL
zH86wZJOMqb0^fu0n1J3SvG2mYRNy6GJc-SPZPmnAf07Q-#B*VXmfQSU@6=bQf_ca_
zy}PS5@g1<w7k!?G96<$)LvMbFfAuG3WQlSI1^v+``X@8Dg!mt1ZYfv*-=X)ofa*Nr
zjIx@CoLFX~r|Ls6e}vNyYl=x_KNrS1wT^Xuq>(~ylbm_5KouB+Dwu#?*2EXVxjzjI
z=+IBL!}@UlS|91`#UL*Mx$h?c*T?Eo0o+&xE?H)?7{`A|=Pt^k00?~ohVf27Gw7e2
zpS_y@KPUAw7W?xgaS4g<6?KUuE?wA*;`v{tlm9khT)HFu_&)PDq>jHA`9N#qeO%Z|
zO?=lfo4?;(en>JuF1tuGL47yX83E0#MJW3GbT${BoAQKFU65>^yjulo*B1!bIg{D8
zDyMjT2K%jt(Lk_1tDM%3)D>E=L$DN#2(nkVDGF6FvU7mOQ-N%m69e8J+EsCc-u*hm
zy#DC%7q5&0c=xS&GEn4#$_8em6q98@IYONR`zxThg>E3V9jZ>CY7e3ttB5jf?C<sE
zg~}|TYfHBb)c!<P?Vd?I5GJdUCIiW>BR+Z|(SxbRkq1XNRI8Gad|oPZq;7@`8$k0U
zAcr!ns-NO!!LOp(`n5pz%D;_5K7B_=wTa+ZrV<$BFX--w(YN#&jO+yfDj%2H3S~z^
zS1=VB7cukVL?(Tnj+%tyA|$JT^qO*AR4de|n-F4l6e02)rh2OCjbR@<$seyyL6Hg7
zGqdTQ=Inf4|3bZwP!AJ1@P1Z>Jf|V|v2x~J9}dr#xdA<1i2cv8<Y>#D9?5VaYMh~9
zBg`VE%mn&wz&*z@rgSjHF(0G4gPKg!dH_AR75-zIb|JDlM<=l($#v2WP8a84BY3fH
zLnuq9@8#;c@2N!=?hN3om=$gQcpY0n#;JZa_8OsU;2{T<x<{su>k1hUDwD=!G)6wZ
z$CveeNiMEuGB<9jiWu339P?NE>LSe%OaeQ0GCPcPa&uVB(`oh)ow!(WJkKvCUwVkp
zcIcErT^zil&y**<8K@k^Ww8Dl0{y%=wQa~Vxltrb&MM1d#L!FJ)olj`80ix(+CeIF
zRJveKV|b8XQzyW2>ieA8ZYk(&p^+U^l=Y5Uc}$@X-nCS5lZGOgMZYoG87fV&VWn;t
zG!m6(-erXP_0K5k__bkAPLmmun|+Fx7VO&mJ8s0FjS-z+sKfAY!L6eafey48bRlzR
zn`n@KpJOAS;A&kNge+MCVjCFvAMihTR8z}z;M?Z6Vtdu3LjCt2c0J}&b}-=TUjcNU
Bex?8b
literal 0
HcmV?d00001
diff --git a/port/assets/menus/menus.json b/port/assets/menus/menus.json
index 169d8443e35f4c7998d315eb53c7fda7c60c935d..582902d664e3c2a28a30f8d2c07f30aa9c12d5c8 100644
--- a/port/assets/menus/menus.json
+++ b/port/assets/menus/menus.json
@@ -63,10 +63,13 @@
"ce/port/main_menu/settings_select/player_setup/player_profile_edit/vr_display/header_vr_display.png",
"ce/port/pause/pausebox_center__0.png",
"ce/port/pause/pausebox_center__1.png",
+ "ce/port/pause/pausebox_center__2.png",
"ce/port/pause/pausebox_left__0.png",
"ce/port/pause/pausebox_left__1.png",
+ "ce/port/pause/pausebox_left__2.png",
"ce/port/pause/pausebox_right__0.png",
"ce/port/pause/pausebox_right__1.png",
+ "ce/port/pause/pausebox_right__2.png",
"ce/shell/bitmaps/alert_bkd__0.png",
"ce/shell/bitmaps/arrow_sm_down__0.png",
"ce/shell/bitmaps/arrow_sm_down__1.png",
diff --git a/port/assets/menus/port_svg/pause/pausebox_center__2.svg b/port/assets/menus/port_svg/pause/pausebox_center__2.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a273d83ab52f01f95ba2c23b57fdafa67704e751
--- /dev/null
+++ b/port/assets/menus/port_svg/pause/pausebox_center__2.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="4" height="512" viewBox="0 0 4 512">
+ <rect x="0" y="2" width="4" height="260" fill="#0a2649" fill-opacity="0.75"/>
+ <rect x="0" y="0" width="4" height="2" fill="#2896ff"/>
+ <rect x="0" y="262" width="4" height="2" fill="#2896ff"/>
+</svg>
diff --git a/port/assets/menus/port_svg/pause/pausebox_left__2.svg b/port/assets/menus/port_svg/pause/pausebox_left__2.svg
new file mode 100644
index 0000000000000000000000000000000000000000..4405b47d7a8215852cf4022f1c12455121d71101
--- /dev/null
+++ b/port/assets/menus/port_svg/pause/pausebox_left__2.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="512" viewBox="0 0 16 512">
+ <path d="M9.5,1 H17 V263 H9.5 A8.5,8.5 0 0 1 1,254.5 V9.5 A8.5,8.5 0 0 1 9.5,1 Z" fill="#0a2649" fill-opacity="0.75" fill-rule="evenodd"/>
+ <path d="M17,1 H9.5 A8.5,8.5 0 0 0 1,9.5 V254.5 A8.5,8.5 0 0 0 9.5,263 H17" fill="none" stroke="#2896ff" stroke-width="2"/>
+</svg>
diff --git a/port/assets/menus/port_svg/pause/pausebox_right__2.svg b/port/assets/menus/port_svg/pause/pausebox_right__2.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f040cc2c1286ae21c9f129c1b184490a2d9c51ec
--- /dev/null
+++ b/port/assets/menus/port_svg/pause/pausebox_right__2.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="512" viewBox="0 0 16 512">
+ <g transform="matrix(-1 0 0 1 16 0)">
+ <path d="M9.5,1 H17 V263 H9.5 A8.5,8.5 0 0 1 1,254.5 V9.5 A8.5,8.5 0 0 1 9.5,1 Z" fill="#0a2649" fill-opacity="0.75" fill-rule="evenodd"/>
+ <path d="M17,1 H9.5 A8.5,8.5 0 0 0 1,9.5 V254.5 A8.5,8.5 0 0 0 9.5,263 H17" fill="none" stroke="#2896ff" stroke-width="2"/>
+ </g>
+</svg>
diff --git a/port/linux/arm64/host_imports.list b/port/linux/arm64/host_imports.list
index 930db2e3e3f6e4ecf546544442b6b5966365ec9b..a82ac5b31b28f3adce5d2387894477ec17bf187e 100644
--- a/port/linux/arm64/host_imports.list
+++ b/port/linux/arm64/host_imports.list
@@ -38,3 +38,7 @@ host_update_launch
host_gl_map_results
host_gl_read_results
host_gl_read_buffer
+host_gl_buffer_create_persistent
+host_gl_buffer_destroy_persistent
+host_gl_buffer_write_persistent
+host_gl_wait_frame_checked
diff --git a/port/linux/arm64/host_vr.c b/port/linux/arm64/host_vr.c
index 4379c9516d90c051e288a68122b522680618906b..cac1ba7b43500f691adb4f5131da9a14905a6aff 100644
--- a/port/linux/arm64/host_vr.c
+++ b/port/linux/arm64/host_vr.c
@@ -875,9 +875,20 @@ int host_vr_locate(struct vr_host_views *views)
XrViewState state = { XR_TYPE_VIEW_STATE };
XrView located[2] = { { XR_TYPE_VIEW }, { XR_TYPE_VIEW } };
XrSpaceLocation location = { XR_TYPE_SPACE_LOCATION };
+ const XrViewStateFlags eye_pose_flags = XR_VIEW_STATE_ORIENTATION_VALID_BIT | XR_VIEW_STATE_POSITION_VALID_BIT;
+ const XrSpaceLocationFlags space_pose_flags = XR_SPACE_LOCATION_ORIENTATION_VALID_BIT | XR_SPACE_LOCATION_POSITION_VALID_BIT;
uint32_t count = 0;
int eye, hand, valid;
+ /* A failed fresh locate must not keep a preceding frame's valid pose. */
+ for (eye = 0; eye < 2; eye++)
+ views->eye[eye].valid = 0;
+ views->head.valid = 0;
+ for (hand = 0; hand < 2; hand++)
+ {
+ views->aim[hand].valid = 0;
+ views->grip[hand].valid = 0;
+ }
if (!vr.frame_begun)
return 0;
locate.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
@@ -885,7 +896,8 @@ int host_vr_locate(struct vr_host_views *views)
locate.space = vr.play_space;
if (xrLocateViews(vr.session, &locate, &state, 2, &count, located) != XR_SUCCESS || count != 2)
return 0;
- valid = (state.viewStateFlags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) != 0;
+ /* The game consumes both rotation and origin from these 6DoF poses. */
+ valid = (state.viewStateFlags & eye_pose_flags) == eye_pose_flags;
for (eye = 0; eye < 2; eye++)
{
pose_from_xr(&located[eye].pose, valid, &views->eye[eye]);
@@ -895,17 +907,17 @@ int host_vr_locate(struct vr_host_views *views)
views->fov[eye][3] = tanf(located[eye].fov.angleDown);
}
if (xrLocateSpace(vr.view_space, vr.play_space, vr.frame.predictedDisplayTime, &location) == XR_SUCCESS)
- pose_from_xr(&location.pose, (location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0, &views->head);
+ pose_from_xr(&location.pose, (location.locationFlags & space_pose_flags) == space_pose_flags, &views->head);
for (hand = 0; hand < 2; hand++)
{
location.type = XR_TYPE_SPACE_LOCATION;
location.next = NULL;
if (xrLocateSpace(vr.aim_spaces[hand], vr.play_space, vr.frame.predictedDisplayTime, &location) == XR_SUCCESS)
- pose_from_xr(&location.pose, (location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0,
+ pose_from_xr(&location.pose, (location.locationFlags & space_pose_flags) == space_pose_flags,
&views->aim[hand]);
location.type = XR_TYPE_SPACE_LOCATION;
if (xrLocateSpace(vr.grip_spaces[hand], vr.play_space, vr.frame.predictedDisplayTime, &location) == XR_SUCCESS)
- pose_from_xr(&location.pose, (location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0,
+ pose_from_xr(&location.pose, (location.locationFlags & space_pose_flags) == space_pose_flags,
&views->grip[hand]);
}
return 1;
diff --git a/port/linux/game/menu_functions.c b/port/linux/game/menu_functions.c
index 7d098faad67b44185095673ab6019668945f5b9f..19a001895aca7bbf2adfca4a20a2dab0185cee78 100644
--- a/port/linux/game/menu_functions.c
+++ b/port/linux/game/menu_functions.c
@@ -53,10 +53,10 @@ the one last used, else the first) and the game's saved game in it:
The Xbox's functions of those names take the Xbox's widgets (a spinner of
levels, the difficulty list itself), so ours run instead (menu_tags.c).
-Co-op (the Xbox's Cooperative Play, Multiplayer's CO-OP CAMPAIGN): "port coop
-begin" makes two players, player 1 on its profile; "port coop player 2"
-gives player 2 the profile chosen and the controller that chose it; then the
-campaign's New Game and difficulty start the game for both.
+Multiplayer's CO-OP CAMPAIGN hosts or joins over LAN or Internet, with one
+local player per machine in flat and VR play. Internet hosts start with
+network.coop_public's listing choice; private games use invite links.
+Competitive multiplayer retains its local-player controls.
The rest are the PC version's, which its menus (port/assets/menus/ce)
name and the Xbox's has not: they do nothing yet, and succeed, so that what
@@ -688,7 +688,7 @@ static void rows_update(struct widget_instance *list, short count, void (*row_te
/* player 1's profile, read again (the active one, else the one last used,
else the first), on the controller (in co-op, on the one that chose co-op:
-coop_begin); FALSE if there is none */
+network campaign); FALSE if there is none */
static boolean campaign_profile(short controller, struct player_profile *profile)
{
long profile_index = player_ui_get_active_player_profile_index(0);
@@ -1637,78 +1637,6 @@ static boolean profile_delete(void)
return TRUE;
}
-/* ---------- Co-op: the campaign for two players on this machine, in split
-screen (the Xbox's Cooperative Play, which the PC version has not):
-Multiplayer's CO-OP CAMPAIGN ("port coop begin"), player 2's profile, chosen
-with player 2's controller ("port coop player 2"), then New Game's levels
-(those either has reached) and difficulty, with either player's controller
-(ui_widget.c, widget_takes_events_of_controller). The main menu and
-Multiplayer go back to one player (main_menu_initialize,
-multiplayer_type_menu_initialize).
-With one gamepad, it is player 2's (pc_menu_split_players) */
-
-/* "port coop begin": two players, player 1 on its profile (campaign_profile)
-and the controller that chose co-op */
-static boolean coop_begin(short controller)
-{
- struct player_profile profile;
-
- player_spawn_count = 1;
- player_ui_reset_single_player_local_player_controllers();
- if (!campaign_profile(controller, &profile))
- return campaign_fail();
- player_spawn_count = 2;
- return TRUE;
-}
-
-/* "port coop player 2 list initialize": on player 2's profile, else the first
-that is not player 1's */
-static boolean coop_player2_list_initialize(struct widget_instance *list)
-{
- long player1 = player_ui_get_active_player_profile_index(0);
- long player2 = player_ui_get_active_player_profile_index(1);
- short index;
-
- profile_list_read(TRUE);
- profile_list.chosen = 0;
- for (index = profile_list.count - 1; index >= 0; index--)
- {
- if (profile_list.indices[index] != player1)
- profile_list.chosen = index;
- }
- for (index = 0; index < profile_list.count && player2 != NONE; index++)
- {
- if (profile_list.indices[index] == player2)
- profile_list.chosen = index;
- }
- profile_list.first = (short)PIN(profile_list.chosen - PROFILE_ROWS / 2, 0,
- MAX(0, profile_list.count + 1 - PROFILE_ROWS));
- focus_row(list, (short)(profile_list.chosen - profile_list.first));
- return TRUE;
-}
-
-/* "port coop player 2": the profile chosen made player 2's, on the controller
-that chose it, which must not be player 1's */
-static boolean coop_player2_choose(short controller)
-{
- struct player_profile profile;
-
- if (player_spawn_count < 2 || profile_list.chosen >= profile_list.count ||
- !player_profile_get(profile_list.indices[profile_list.chosen], &profile))
- {
- return campaign_fail();
- }
- if (controller == player_ui_get_single_player_local_player_controller(0))
- {
- display_error_text_deferred(L"Player 2 chooses their\r\nprofile with their own\r\ncontroller.",
- NONE);
- return campaign_fail();
- }
- player_ui_set_single_player_local_player_controller(1, controller);
- player_ui_set_active_player_profile(1, profile_list.indices[profile_list.chosen], &profile);
- return TRUE;
-}
-
/* ---------- Controls Setup: the keyboard and mouse's controls
(port/linux/include/halo_keyboard.h), shown a group at a time, two
bindings each */
@@ -2038,6 +1966,9 @@ void network_game_server_port_set_settings(wchar_t const *name, long maximum_pla
void network_game_server_port_set_cooperative_friendly_fire(short friendly_fire);
void *global_network_game_client_get(void);
void *global_network_game_server_get(void);
+void dispose_global_network_game_client(void);
+void dispose_global_network_game_server(void);
+void network_game_accept_remote_connections(boolean accept);
boolean network_game_is_splitscreen_local(void);
struct advertised_game *network_game_client_get_available_games(void *client);
boolean network_game_client_advertised_game_is_valid(struct advertised_game *game);
@@ -2098,6 +2029,15 @@ static struct
boolean game_private;
} multiplayer = { 0, 0, 0, { 0 }, 0, { 0 }, 0, 0, 0, 0, { 0 }, NUMBEROF(maximum_players) - 1 };
+/* Scoped to the synchronous screen load below. A cancelled/failed opening
+cannot leave the next ordinary Create Game choosing campaign maps. */
+static boolean cooperative_map_opening;
+/* Campaign intent persists while its lobby receives the game. */
+static boolean cooperative_network_game;
+static boolean lobby_join_reset(void);
+static short lobby_local_player_count(void);
+static short campaign_level_of(char const *map_name);
+
/* ---- a text field (Direct Link's link, the game's name): the keyboard
types into it (Ctrl+V pastes), its row's A (enter) is done, B (escape)
cancels */
@@ -2213,6 +2153,8 @@ static void multiplayer_mode_set(struct widget_instance *widget)
{
char const *name = widget->name;
+ cooperative_network_game = FALSE;
+ cooperative_map_opening = FALSE;
/* (an instance's name is cut to 31 characters:
"multiplayer_type_create_interne") */
multiplayer.mode =
@@ -2234,19 +2176,135 @@ static boolean multiplayer_player(short controller)
/* Create Game's ("join controller to mp game", first): player 1 in, and
the server made: an internet one (an invite, Discord) or a LAN one */
-static boolean multiplayer_host(struct widget_instance *widget, struct event_record *event, short controller,
- boolean *widget_deleted)
+static boolean multiplayer_host_mode(struct widget_instance *widget, struct event_record *event, short controller,
+ short mode, boolean cooperative, boolean *widget_deleted)
{
- multiplayer_mode_set(widget);
+ boolean result;
+
+ cooperative_map_opening = FALSE;
+ cooperative_network_game = cooperative;
if (!multiplayer_player(controller))
return FALSE;
+ multiplayer.mode = mode;
p2p_set_hosting_allowed(multiplayer.mode == _multiplayer_mode_host_internet);
/* (in the server browser, if PUBLIC: Server Setup's LISTING, which a
- new game starts with as network.host_public says) */
- multiplayer.game_private = !config_boolean("network.host_public");
+ new game starts with as network.host_public says; network campaign
+ uses co-op's choice from the start, PRIVATE by default) */
+ multiplayer.game_private = !config_boolean(cooperative ? "network.coop_public" : "network.host_public");
p2p_set_hosting_public(multiplayer.mode == _multiplayer_mode_host_internet && !multiplayer.game_private);
multiplayer.cooperative_maximum_players_set = FALSE;
- return ui_widget_port_host(widget, event, widget_deleted);
+ result = ui_widget_port_host(widget, event, widget_deleted);
+ if (!result)
+ {
+ game_connection_set(0);
+ p2p_set_hosting_allowed(FALSE);
+ p2p_set_hosting_public(FALSE);
+ }
+ return result;
+}
+
+static boolean multiplayer_host(struct widget_instance *widget, struct event_record *event, short controller,
+ boolean *widget_deleted)
+{
+ multiplayer_mode_set(widget);
+ return multiplayer_host_mode(widget, event, controller, multiplayer.mode, FALSE, widget_deleted);
+}
+
+/* Campaign uses one local player. Clear pending competitive additions. */
+static void cooperative_single_player(void)
+{
+ short controller;
+ player_spawn_count = 1;
+ player_ui_reset_single_player_local_player_controllers();
+ lobby_join_reset();
+ for (controller = 0; controller < MAXIMUM_LOCAL_PLAYERS; controller++)
+ player_ui_local_player_left_multiplayer_game(controller);
+}
+
+static void cooperative_session_cleanup(void)
+{
+ cooperative_map_opening = FALSE;
+ cooperative_network_game = FALSE;
+ dispose_global_network_game_client();
+ dispose_global_network_game_server();
+ network_game_accept_remote_connections(FALSE);
+ player_ui_clear_multiplayer_variant();
+ game_connection_set(0);
+ p2p_set_hosting_allowed(FALSE);
+ p2p_set_hosting_public(FALSE);
+ p2p_lobby_browse(FALSE);
+ cooperative_single_player();
+}
+
+static boolean cooperative_network_host(struct widget_instance *widget, struct event_record *event,
+ short controller, short mode, boolean *widget_deleted)
+{
+ boolean opened;
+ cooperative_session_cleanup();
+ if (mode == _multiplayer_mode_host_internet && !config_boolean("network.online"))
+ {
+ display_error_text_deferred(L"Internet play is off. Enable it\r\nin Network Setup, then restart Halo.", NONE);
+ return campaign_fail();
+ }
+ if (!multiplayer_host_mode(widget, event, controller, mode, TRUE, widget_deleted))
+ {
+ cooperative_session_cleanup();
+ return FALSE;
+ }
+ lan_mode = mode == _multiplayer_mode_host_lan;
+ /* Child creation is synchronous; only this opening chooses campaign maps. */
+ cooperative_map_opening = TRUE;
+ opened = ui_widget_port_open(widget,
+ "pc\\main_menu\\multiplayer_type_select\\connected\\connected_map_select_wrapper", widget_deleted);
+ cooperative_map_opening = FALSE;
+ if (!opened)
+ cooperative_session_cleanup();
+ return opened;
+}
+
+static boolean cooperative_network_join(struct widget_instance *widget, short controller,
+ short mode, boolean *widget_deleted)
+{
+ cooperative_session_cleanup();
+ if (mode != _multiplayer_mode_lan && !config_boolean("network.online"))
+ {
+ display_error_text_deferred(L"Internet play is off. Enable it\r\nin Network Setup, then restart Halo.", NONE);
+ return campaign_fail();
+ }
+ cooperative_network_game = TRUE;
+ multiplayer.mode = mode;
+ lan_mode = mode == _multiplayer_mode_lan;
+ if (!multiplayer_player(controller) || !ui_widget_port_open(widget,
+ "pc\\main_menu\\multiplayer_type_select\\join_game\\join_game_screen", widget_deleted))
+ {
+ cooperative_session_cleanup();
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/* Registered for both flat and VR. Named rows select LAN or Internet. */
+static boolean cooperative_begin(struct widget_instance *widget, struct event_record *event,
+ short controller, boolean *widget_deleted)
+{
+ struct player_profile profile;
+ if (!strcmp(widget->name, "coop_host_lan"))
+ return cooperative_network_host(widget, event, controller, _multiplayer_mode_host_lan, widget_deleted);
+ if (!strcmp(widget->name, "coop_host_online"))
+ return cooperative_network_host(widget, event, controller, _multiplayer_mode_host_internet, widget_deleted);
+ if (!strcmp(widget->name, "coop_join_lan"))
+ return cooperative_network_join(widget, controller, _multiplayer_mode_lan, widget_deleted);
+ if (!strcmp(widget->name, "coop_join_online"))
+ return cooperative_network_join(widget, controller, _multiplayer_mode_server_browser, widget_deleted);
+ if (!strcmp(widget->name, "coop_join_invite"))
+ return cooperative_network_join(widget, controller, _multiplayer_mode_direct_link, widget_deleted);
+ cooperative_session_cleanup();
+ if (!strcmp(widget->name, "network_screen"))
+ return TRUE;
+ if (strcmp(widget->name, "multiplayer_type_coop_item") || !campaign_profile(controller, &profile))
+ return campaign_fail();
+ cooperative_network_game = TRUE;
+ return TRUE;
}
/* ---- the map list (the Map screen's): its chooser's SINGLEPLAYER levels
@@ -2266,6 +2324,8 @@ static struct
short kind, step;
/* hosting over the network, which co-op needs */
boolean hosting;
+ /* Dedicated campaign routes cannot choose competitive maps. */
+ boolean campaign_only;
/* the multiplayer maps (ui_widget_port_multiplayer_maps) */
short map_count;
short first, chosen;
@@ -2312,9 +2372,12 @@ static boolean map_list_initialize(struct widget_instance *list)
map_list.hosting = global_network_game_server_get() != NULL && !network_game_is_splitscreen_local();
map_list.map_count = ui_widget_port_multiplayer_maps(&names, &last_used);
- map_list.kind = MAP_KIND_MULTIPLAYER;
+ /* Returning from Server Setup recreates this list; keep campaign intent. */
+ map_list.campaign_only = (cooperative_map_opening || cooperative_network_game) && map_list.hosting;
+ map_list.kind = map_list.campaign_only ? MAP_KIND_SINGLEPLAYER : MAP_KIND_MULTIPLAYER;
+ cooperative_map_opening = FALSE;
map_kind_set(list, map_list.kind);
- map_step_open(list, MAP_STEP_MAPS, last_used);
+ map_step_open(list, MAP_STEP_MAPS, map_list.kind == MAP_KIND_SINGLEPLAYER ? 0 : last_used);
return TRUE;
}
@@ -2323,7 +2386,8 @@ them the level or map chosen: its picture, name and words */
static void map_list_update(struct widget_instance *list)
{
struct widget_instance *description = list->parameters.list.extended_description;
- short kind = map_kind_shown(list, map_list.hosting ? NONE : MAP_KIND_MULTIPLAYER), count, entry;
+ short kind = map_kind_shown(list, map_list.campaign_only ? MAP_KIND_SINGLEPLAYER :
+ map_list.hosting ? NONE : MAP_KIND_MULTIPLAYER), count, entry;
if (kind != map_list.kind)
{
@@ -2358,13 +2422,22 @@ static boolean map_list_choose(struct widget_instance *list, boolean *widget_del
{
short chosen = map_list.chosen;
+ if (map_list.campaign_only)
+ map_list.kind = MAP_KIND_SINGLEPLAYER;
if (chosen >= map_step_count())
return campaign_fail();
if (map_list.step == MAP_STEP_DIFFICULTIES)
{
+ /* Ordinary Create Game cannot create a split campaign either. */
+ if (player_spawn_count >= 2 || lobby_local_player_count() > 1)
+ {
+ display_error_text_deferred(L"Campaign uses one player per machine.\r\nLeave extra local players first.", NONE);
+ return campaign_fail();
+ }
/* the co-op game set up, then Server Setup in the gametypes' place */
if (!ui_widget_port_cooperative_level_choose(main_get_solo_level_name(map_list.level), chosen))
return campaign_fail();
+ cooperative_network_game = TRUE;
return ui_widget_port_open(list, SERVER_SETUP_NAME, widget_deleted);
}
if (map_list.kind == MAP_KIND_MULTIPLAYER)
@@ -2385,6 +2458,11 @@ static boolean map_list_back(struct widget_instance *list, boolean *widget_delet
map_step_open(list, MAP_STEP_MAPS, map_list.level);
return TRUE;
}
+ if (map_list.campaign_only)
+ {
+ cooperative_session_cleanup();
+ map_list.campaign_only = FALSE;
+ }
ui_widget_port_go_back(list);
*widget_deleted = TRUE;
return TRUE;
@@ -2845,7 +2923,8 @@ static void browser_games_read(void)
struct advertised_game *game = &games[index];
if (network_game_client_advertised_game_is_valid(game) && !advertised_in_progress(game) == (pass == 0) &&
- game_from_peer(game) == (multiplayer.mode == _multiplayer_mode_direct_link))
+ game_from_peer(game) == (multiplayer.mode == _multiplayer_mode_direct_link) &&
+ (!cooperative_network_game || (!game->engine_type && campaign_level_of(game->map_name) != NONE)))
{
multiplayer.games[multiplayer.game_count++] = game;
}
@@ -2931,6 +3010,7 @@ static boolean lobby_browser_select(struct widget_instance *widget, short contro
static boolean browser_initialize(struct widget_instance *screen, struct event_record *event,
boolean *widget_deleted)
{
+ boolean result;
char const *title = multiplayer.mode == _multiplayer_mode_lan ? "header_lan" :
multiplayer.mode == _multiplayer_mode_direct_link ? "header_direct_link" : "header_server_browser";
char const *const titles[] = { "header_internet", "header_lan", "header_direct_link", "header_server_browser" };
@@ -2965,7 +3045,10 @@ static boolean browser_initialize(struct widget_instance *screen, struct event_r
one reaches its host, whose game the client then finds as Direct Link's) */
if (multiplayer.mode == _multiplayer_mode_server_browser)
lobby_browser_begin();
- return ui_widget_port_browse(screen, event, widget_deleted);
+ result = ui_widget_port_browse(screen, event, widget_deleted);
+ if (!result && cooperative_network_game)
+ cooperative_session_cleanup();
+ return result;
}
/* a scenario's name without its path */
@@ -3029,6 +3112,24 @@ static void game_map_name(struct advertised_game const *game, wchar_t *text)
map_display_name(game->map_name, text);
}
+/* Network campaign rows are identified by both their engine and map. */
+static boolean advertised_cooperative(struct advertised_game const *game)
+{
+ return game && !game->engine_type && campaign_level_of(game->map_name) != NONE;
+}
+
+/* Joining campaign from any browser also uses one local player. Competitive
+joins keep their existing local-player selection. */
+static boolean multiplayer_game_player(short controller, struct advertised_game const *game)
+{
+ if (advertised_cooperative(game))
+ {
+ cooperative_single_player();
+ cooperative_network_game = TRUE;
+ }
+ return multiplayer_player(controller);
+}
+
/* ---- Join Game > Server Browser: internet play's public games (their
hosts' listings, p2p_lobby.c). Joining one joins its invite (as Direct
Link's PASTE LINK); once its host is reached its game is among the client's,
@@ -3105,6 +3206,8 @@ static short lobby_browser_valid_games(struct p2p_listing *games, short count)
{
wchar_t name[P2P_LISTING_NAME_SIZE + 1];
short index;
+ if (cooperative_network_game && (games[read].engine_type || campaign_level_of(games[read].map) == NONE))
+ continue;
text_to_wide(games[read].name, name, NUMBEROF(name));
if (!player_name_clean(name, NUMBEROF(name)))
@@ -3347,7 +3450,7 @@ static boolean lobby_browser_select(struct widget_instance *widget, short contro
if (advertised_in_progress(found))
{
/* (player 1 joining it: others join them there) */
- if (!multiplayer_player(controller))
+ if (!multiplayer_game_player(controller, found))
return FALSE;
csmemcpy(multiplayer.preview_key_id, found->key_id, sizeof(multiplayer.preview_key_id));
csmemcpy(multiplayer.preview_xnaddr, found->xnaddr, sizeof(multiplayer.preview_xnaddr));
@@ -3355,7 +3458,7 @@ static boolean lobby_browser_select(struct widget_instance *widget, short contro
}
if (!found->open)
return campaign_fail();
- if (!multiplayer_player(controller))
+ if (!multiplayer_game_player(controller, found))
return FALSE;
return ui_widget_port_join(widget, found, LOBBY_NAME, widget_deleted);
}
@@ -3500,7 +3603,7 @@ static boolean browser_join(struct widget_instance *widget, short controller, bo
{
if (multiplayer.game_chosen >= multiplayer.game_count)
return campaign_fail();
- if (!multiplayer_player(controller))
+ if (!multiplayer_game_player(controller, multiplayer.games[multiplayer.game_chosen]))
return FALSE;
return ui_widget_port_join(widget, multiplayer.games[multiplayer.game_chosen], LOBBY_NAME, widget_deleted);
}
@@ -3523,7 +3626,7 @@ static boolean browser_select(struct widget_instance *widget, struct event_recor
advertised_in_progress(multiplayer.games[multiplayer.game_chosen]))
{
/* (player 1 joining it: others join them there) */
- if (!multiplayer_player(controller))
+ if (!multiplayer_game_player(controller, multiplayer.games[multiplayer.game_chosen]))
return FALSE;
csmemcpy(multiplayer.preview_key_id, multiplayer.games[multiplayer.game_chosen]->key_id,
sizeof(multiplayer.preview_key_id));
@@ -3656,10 +3759,27 @@ static boolean lobby_join_reset(void)
return TRUE;
}
+/* Campaign never adds another local controller, including while the game
+is loading its lobby. Other game modes keep competitive split-screen. */
+static boolean lobby_campaign_single_player(void)
+{
+ void *client = global_network_game_client_get();
+ return cooperative_network_game || (client && game_cooperative(network_game_client_get_game(client)));
+}
+
+static boolean campaign_local_player_block(void)
+{
+ lobby_join_reset();
+ display_error_text_deferred(L"One player per machine in campaign.\r\nJoin from another headset or PC.", NONE);
+ return campaign_fail();
+}
+
/* "port lobby add player" (ADD PLAYER): the next START of another controller
joins (one gamepad leaves the keyboard's controller for its own) */
static boolean lobby_add_player(void)
{
+ if (lobby_campaign_single_player())
+ return campaign_local_player_block();
if (lobby_local_player_count() >= MAXIMUM_LOCAL_PLAYERS)
return campaign_fail();
lobby_join.adding = TRUE;
@@ -3680,6 +3800,8 @@ static boolean lobby_join_start(struct widget_instance *widget, short controller
ui_widget_port_dispatch_event(focused_leaf(widget), BUTTON_START, controller, widget_deleted);
return FALSE;
}
+ if (lobby_campaign_single_player())
+ return campaign_local_player_block();
if (!game || network_game_client_get_state(client, &state_data) != _client_state_pregame)
return campaign_fail();
if (lobby_player_count >= game->maximum_players)
@@ -3743,6 +3865,8 @@ static boolean lobby_player_choose(void)
{
short controller = lobby_join.controller;
+ if (lobby_campaign_single_player())
+ return campaign_local_player_block();
if (controller == NONE || profile_list.chosen >= profile_list.count ||
!ui_widget_port_multiplayer_player(controller, profile_list.indices[profile_list.chosen]))
{
@@ -3758,7 +3882,15 @@ static void lobby_join_help(struct widget_instance *list)
wchar_t const *text = L"";
short controller;
- if (lobby_join.adding)
+ visible_set(named(screen_of(list), "lobby_button_add", 0), !lobby_campaign_single_player());
+ visible_set(named(screen_of(list), "preview_button_add", 0), !lobby_campaign_single_player());
+ focus_off_hidden(named(screen_of(list), "lobby_button_bar", 0));
+ focus_off_hidden(named(screen_of(list), "preview_button_bar", 0));
+ if (lobby_campaign_single_player())
+ {
+ text = L"One player per machine. Join from another headset or PC.";
+ }
+ else if (lobby_join.adding)
{
text = L"New player: press START.";
}
@@ -3984,6 +4116,8 @@ static boolean preview_add(struct widget_instance *widget, short controller, boo
ui_widget_port_dispatch_event(focused_leaf(widget), BUTTON_START, controller, widget_deleted);
return FALSE;
}
+ if (lobby_campaign_single_player() || advertised_cooperative(game))
+ return campaign_local_player_block();
if (!game || !game->open || count >= MAXIMUM_LOCAL_PLAYERS)
return campaign_fail();
if (game->player_count + count >= game->maximum_player_count)
@@ -4027,7 +4161,12 @@ static boolean preview_join(struct widget_instance *widget, short controller, bo
if (!game || !game->open)
return campaign_fail();
- if (!lobby_local_player_count() && !multiplayer_player(controller))
+ if (advertised_cooperative(game))
+ {
+ if (!multiplayer_game_player(controller, game))
+ return FALSE;
+ }
+ else if (!lobby_local_player_count() && !multiplayer_player(controller))
return FALSE;
return ui_widget_port_join(widget, game, LOBBY_NAME, widget_deleted);
}
@@ -4965,39 +5104,12 @@ boolean pc_menu_event_function_invoke(
}
else if (!strcmp(name, "port coop begin"))
{
- return coop_begin(controller);
+ return cooperative_begin(widget, event, controller, widget_deleted);
}
- else if (!strcmp(name, "port coop player 2 list initialize"))
+ else if (!strcmp(name, "port coop player 2 list initialize") || !strcmp(name, "port coop player 2"))
{
- return coop_player2_list_initialize(widget);
- }
- else if (!strcmp(name, "port coop player 2"))
- {
- return coop_player2_choose(controller);
- }
- else if (!strcmp(name, "request del player profile"))
- {
- return profile_delete_request();
- }
- else if (!strcmp(name, "final del player profile"))
- {
- return profile_delete();
- }
- else if (!strcmp(name, "color picker menu initialize"))
- {
- return color_list_initialize(widget);
- }
- else if (!strcmp(name, "color picker select color"))
- {
- return color_choose();
- }
- else if (!strcmp(name, "controls binding slot"))
- {
- controls_screen.slot = (short)!controls_screen.slot;
- }
- else if (!strcmp(name, "campaign menu init"))
- {
- return campaign_menu_initialize(controller);
+ /* Reject stale split-campaign handlers. */
+ return campaign_fail();
}
else if (!strcmp(name, "campaign menu continue"))
{
@@ -5038,6 +5150,9 @@ boolean pc_menu_event_function_invoke(
else if (!strcmp(name, "controls back handler") || !strcmp(name, "gamespy back handler") ||
!strcmp(name, "gamespy dismiss error") || !strcmp(name, "gamespy dismiss filters"))
{
+ if (!strcmp(name, "gamespy back handler") && cooperative_network_game &&
+ !global_network_game_server_get())
+ cooperative_session_cleanup();
ui_widget_port_go_back(widget);
*widget_deleted = TRUE;
}
diff --git a/port/linux/game/menu_tags.c b/port/linux/game/menu_tags.c
index 2799c50c00b24fd83e407f2233580ac8d4b3e79f..5e6afd6829a718304c173889c5232248d8957d9c 100644
--- a/port/linux/game/menu_tags.c
+++ b/port/linux/game/menu_tags.c
@@ -49,6 +49,9 @@ int platform_window_sizes(long *widths, long *heights, int maximum);
/* the game's network server (this machine hosts: network_game_globals.c) */
void *global_network_game_server_get(void);
+#ifdef HALO_VR
+int halo_vr_running(void);
+#endif
/* cache_files.c's (port) */
void *cache_files_tag_instances(long *count);
@@ -71,8 +74,9 @@ boolean pc_menu_tag(long tag_index);
/* the multiplayer maps' widget collection, whose pause screens pause_patch
adds to */
#define MULTIPLAYER_COLLECTION "ui\\shell\\multiplayer"
-/* the tags pause_patch adds: two buttons and their texts */
-#define PAUSE_PATCH_TAGS 4
+/* At most three MP buttons (or one campaign button), each with its text. */
+#define PAUSE_PATCH_TAGS 6
+#define SOLO_PAUSE_SCREEN "ui\\shell\\solo_game\\pause_game\\pause_game"
/* what a frame of ui.map's is drawn as in a game map, which has not got it */
#define BLANK_PNG "ce/port/blank__0.png"
#define UI_WIDGET_DEFINITION_TAG 'DeLa'
@@ -409,9 +413,10 @@ static struct
long *text_tags;
long *spinner_tags;
boolean failed;
- /* a multiplayer map's build (the pause menu's SETTINGS): what the map
- has not got is none, not a failure */
+ /* A game map has only some UI resources; absent stock UI tags are NONE.
+ Multiplayer widgets must keep the network simulation running. */
boolean game_map;
+ boolean multiplayer_map;
} build;
/* ---------- private code */
@@ -1001,7 +1006,7 @@ static void *widget_build(long widget_index)
definition->flags = flags_parse(source->flags, widget_flag_names, NUMBEROF(widget_flag_names),
source->file, source->line);
/* (in a network game the clock runs on) */
- if (build.game_map)
+ if (build.multiplayer_map)
definition->flags &= ~FLAG(1); /* pause_game */
definition->milliseconds_to_auto_close = source->auto_close;
definition->auto_close_fade_time = source->auto_close_fade;
@@ -1325,7 +1330,7 @@ static void menu_tags_release(void)
/* ---------- public code */
-/* ---------- the in-game pause menu (a multiplayer map's)
+/* ---------- the in-game pause menu
Its list (RESUME GAME, LEAVE GAME; a custom map's own buttons too) gets
SETTINGS, the menus' settings (as the main menu's SETTINGS opens them), and
@@ -1335,9 +1340,12 @@ They are copies of the map's LEAVE GAME button (its look), put before it.
The map's pause screens move to fit them: on the Xbox's own (its box of
pausebox2 pieces), the box is the port's taller one (pause/pausebox_*:
tools/port_settings.py), centred where the old one was; on a custom map's,
-what is below the list moves down. */
+what is below the list moves down. A running VR session also gets a direct
+VR SETUP button; campaign/native co-op use their exact solo pause root and
+retain its existing actions and pause-time policy. */
#define PAUSE_SETTINGS_SCREEN "main_menu/settings_select/player_setup/player_profile_edit/player_profile_edit_screen"
+#define PAUSE_VR_SCREEN "main_menu/settings_select/player_setup/player_profile_edit/vr_controls/vr_controls_screen"
#define PAUSE_BUTTON_SPACING 35
#define PAUSE_BOX_FIRST_BUTTONS 3
@@ -1401,33 +1409,44 @@ static void *pause_button(struct cache_file_tag_instance *instances, struct ui_w
char const *name, char const *text, long function, long widget_tag)
{
struct ui_widget_definition *button = allocate(sizeof(*button));
+ long handler_count = function == NONE ? 2 : model->event_handlers.count;
struct ui_widget_event_handler_reference *handlers =
- allocate(model->event_handlers.count * sizeof(struct ui_widget_event_handler_reference));
+ allocate(handler_count * sizeof(struct ui_widget_event_handler_reference));
long text_tag = next_tag(), handler;
if (!button || !handlers)
return NULL;
memcpy(button, model, sizeof(*button));
instance_set(instances, UNICODE_STRING_LIST_TAG, text_tag, name, " text", string_list_build(&text, 1));
+ if (build.failed)
+ return NULL;
reference_set(&button->text_label_string_list, UNICODE_STRING_LIST_TAG, text_tag);
button->string_list_index = 0;
button->game_data_inputs.count = 0;
button->game_data_inputs.address = NULL;
/* (the game's memcpy asserts on a NULL source, even for nothing) */
- if (model->event_handlers.count)
+ if (function != NONE && model->event_handlers.count)
memcpy(handlers, model->event_handlers.address, model->event_handlers.count * sizeof(*handlers));
- for (handler = 0; handler < model->event_handlers.count; handler++)
+ for (handler = 0; handler < handler_count; handler++)
{
- handlers[handler].flags = FLAG(_event_handler_run_function_bit);
+ handlers[handler].flags = function == NONE ? 0 : FLAG(_event_handler_run_function_bit);
+ /* A/Start open VR Setup directly; no profile-edit/network function. */
+ if (function == NONE)
+ {
+ handlers[handler].event_type = (short)(handler ? 12 : 0);
+ reference_clear(&handlers[handler].sound_effect, SOUND_TAG);
+ }
handlers[handler].function = (short)function;
handlers[handler].script[0] = 0;
reference_set(&handlers[handler].widget_tag, UI_WIDGET_DEFINITION_TAG, widget_tag);
if (widget_tag != NONE)
{
- handlers[handler].flags |= FLAG(_event_handler_open_widget_bit) |
- FLAG(_event_handler_try_to_branch_on_failure_bit);
+ handlers[handler].flags |= FLAG(_event_handler_open_widget_bit);
+ if (function != NONE)
+ handlers[handler].flags |= FLAG(_event_handler_try_to_branch_on_failure_bit);
}
}
+ button->event_handlers.count = handler_count;
button->event_handlers.address = handlers;
return button;
}
@@ -1435,33 +1454,53 @@ static void *pause_button(struct cache_file_tag_instance *instances, struct ui_w
/* the list's buttons: SETTINGS (and the host's END GAME) put before LEAVE
GAME (quit); returns how many were added */
static long pause_list_patch(struct cache_file_tag_instance *instances, struct ui_widget_definition *list, long quit,
- boolean host)
+ boolean host, long vr_widget, boolean campaign)
{
struct ui_widget_child_reference *children = list->child_widgets.address;
- long count = list->child_widgets.count, added = host ? 2 : 1, child;
+ long count = list->child_widgets.count, ordinary = campaign ? 0 : host ? 2 : 1;
+ long added = ordinary + (vr_widget != NONE), child;
struct ui_widget_child_reference *grown = allocate((count + added) * sizeof(struct ui_widget_child_reference));
struct ui_widget_definition const *model = tag_get(UI_WIDGET_DEFINITION_TAG, children[quit].widget_tag.index);
long settings_widget = widget_named(PAUSE_SETTINGS_SCREEN);
short spacing = count >= 2 ? (short)(children[count - 1].vertical_offset - children[count - 2].vertical_offset) :
PAUSE_BUTTON_SPACING;
- void *buttons[2];
- long tags[2];
+ void *buttons[3];
+ long tags[3];
+ char const *names[3];
- if (!grown || settings_widget == NONE || spacing <= 0)
- return 0;
- tags[0] = next_tag();
- buttons[0] = pause_button(instances, model, "pause/settings_button", "SETTINGS",
- function_index("profile set edit begin", "pause", 0), build.widget_tags[settings_widget]);
- tags[1] = host ? next_tag() : NONE;
- buttons[1] = host ? pause_button(instances, model, "pause/end_game_button", "END GAME",
- function_index("port pause end game", "pause", 0), NONE) : NULL;
- if (!buttons[0] || (host && !buttons[1]))
+ if (!grown || (!campaign && settings_widget == NONE) || spacing <= 0 || !added)
return 0;
+ if (!campaign)
+ {
+ names[0] = "pause/settings_button";
+ tags[0] = next_tag();
+ buttons[0] = pause_button(instances, model, names[0], "SETTINGS",
+ function_index("profile set edit begin", "pause", 0), build.widget_tags[settings_widget]);
+ if (host)
+ {
+ names[1] = "pause/end_game_button";
+ tags[1] = next_tag();
+ buttons[1] = pause_button(instances, model, names[1], "END GAME",
+ function_index("port pause end game", "pause", 0), NONE);
+ }
+ }
+ if (vr_widget != NONE)
+ {
+ names[ordinary] = "pause/vr_setup_button";
+ tags[ordinary] = next_tag();
+ buttons[ordinary] = pause_button(instances, model, names[ordinary], "VR SETUP", NONE,
+ build.widget_tags[vr_widget]);
+ }
for (child = 0; child < added; child++)
{
- instance_set(instances, UI_WIDGET_DEFINITION_TAG, tags[child],
- child ? "pause/end_game_button" : "pause/settings_button", "", buttons[child]);
+ if (!buttons[child])
+ return 0;
+ instance_set(instances, UI_WIDGET_DEFINITION_TAG, tags[child], names[child], "", buttons[child]);
}
+ /* Do not install pointers into owned memory if any tag/text allocation
+ failed: menu_tags_release then restores the original table safely. */
+ if (build.failed)
+ return 0;
if (quit)
memcpy(grown, children, quit * sizeof(*grown));
for (child = 0; child < added; child++)
@@ -1482,14 +1521,18 @@ static long pause_list_patch(struct cache_file_tag_instance *instances, struct u
return added;
}
-/* the Xbox's own box: a widget of the three pausebox2 pieces */
+/* The Xbox's own two/five-button box: three validated stock pieces. */
static boolean pause_box_stock(struct ui_widget_definition const *box)
{
struct ui_widget_child_reference const *pieces = box->child_widgets.address;
- return box->child_widgets.count == 3 && tag_name_ends(pieces[0].widget_tag.index, "\\pausebox2_left") &&
- tag_name_ends(pieces[1].widget_tag.index, "\\pausebox2_left_center") &&
- tag_name_ends(pieces[2].widget_tag.index, "\\pausebox2_right");
+ return box->child_widgets.count == 3 &&
+ ((tag_name_ends(pieces[0].widget_tag.index, "\\pausebox2_left") &&
+ tag_name_ends(pieces[1].widget_tag.index, "\\pausebox2_left_center") &&
+ tag_name_ends(pieces[2].widget_tag.index, "\\pausebox2_right")) ||
+ (tag_name_ends(pieces[0].widget_tag.index, "\\pausebox5_left") &&
+ tag_name_ends(pieces[1].widget_tag.index, "\\pausebox5_left_center") &&
+ tag_name_ends(pieces[2].widget_tag.index, "\\pausebox5_right")));
}
/* the stock box's pieces drawn with the port's taller box: its frame for
@@ -1515,89 +1558,115 @@ static void pause_box_redraw(struct ui_widget_definition const *box, long button
sequence->first_bitmap_index = (short)PIN(buttons - PAUSE_BOX_FIRST_BUTTONS, 0, group->bitmaps.count - 1);
sequence->bitmap_count = 1;
reference_set(&definition->background_bitmap, BITMAP_GROUP_TAG, build.bitmap_tags[bitmap]);
- definition->bounds.y1 = (short)(definition->bounds.y0 + 256);
+ definition->bounds.y1 = (short)(definition->bounds.y0 +
+ ((struct bitmap_data *)group->bitmaps.address)[sequence->first_bitmap_index].height);
}
}
-static void pause_patch(struct cache_file_tag_instance *instances)
+static long pause_vr_widget(void)
{
- long collection = tag_loaded('Soul', MULTIPLAYER_COLLECTION);
- long quit_function = ui_function_named("mp game player quit");
- boolean host = global_network_game_server_get() != NULL;
- struct tag_block const *screens;
- long patched_list = NONE, added = 0, buttons = 0, screen;
- boolean box_redrawn = FALSE;
+#ifdef HALO_VR
+ if (halo_vr_running())
+ return widget_named(PAUSE_VR_SCREEN);
+#endif
+ return NONE;
+}
+
+static void pause_screen_patch(struct cache_file_tag_instance *instances, long screen_tag, long quit_function,
+ boolean host, long vr_widget, boolean campaign, long *patched_list, long *added, long *buttons,
+ boolean *box_redrawn)
+{
+ struct ui_widget_definition *definition;
+ struct ui_widget_child_reference *children;
+ long child, list_child = NONE, box_child = NONE;
+ short grow, list_top;
- if (collection == NONE || quit_function == NONE)
+ if (screen_tag == NONE || quit_function == NONE)
return;
- screens = tag_get('Soul', collection);
- for (screen = 0; screen < screens->count; screen++)
+ definition = tag_get(UI_WIDGET_DEFINITION_TAG, screen_tag);
+ children = definition->child_widgets.address;
+ for (child = 0; child < definition->child_widgets.count && list_child == NONE; child++)
{
- long screen_tag = ((struct tag_reference const *)screens->address)[screen].index;
- struct ui_widget_definition *definition;
- struct ui_widget_child_reference *children;
- long child, list_child = NONE, box_child = NONE;
- short grow, list_top;
+ struct ui_widget_definition *list;
+ long quit;
- if (screen_tag == NONE)
+ if (children[child].widget_tag.index == NONE)
continue;
- definition = tag_get(UI_WIDGET_DEFINITION_TAG, screen_tag);
- children = definition->child_widgets.address;
- for (child = 0; child < definition->child_widgets.count && list_child == NONE; child++)
+ list = tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index);
+ if (list->type != _widget_type_column_list)
+ continue;
+ /* (one list, which the screens share: patched once) */
+ if (children[child].widget_tag.index == *patched_list)
{
- struct ui_widget_definition *list;
- long quit;
-
- if (children[child].widget_tag.index == NONE)
- continue;
- list = tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index);
- if (list->type != _widget_type_column_list)
- continue;
- /* (one list, which the screens share: patched once) */
- if (children[child].widget_tag.index == patched_list)
- {
- list_child = child;
- continue;
- }
- quit = pause_quit_button(list, quit_function);
- if (quit == NONE || patched_list != NONE)
- continue;
- added = pause_list_patch(instances, list, quit, host);
- if (!added)
- return;
- buttons = list->child_widgets.count;
- patched_list = children[child].widget_tag.index;
list_child = child;
- }
- if (list_child == NONE)
continue;
- for (child = 0; child < definition->child_widgets.count; child++)
- {
- if (child != list_child && children[child].widget_tag.index != NONE &&
- pause_box_stock(tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index)))
- {
- box_child = child;
- }
- }
- /* the stock box: taller, centred where it was, the list with it, and
- what is below it moved down; else only what is below the list */
- grow = (short)(added * PAUSE_BUTTON_SPACING);
- list_top = children[list_child].vertical_offset;
- for (child = 0; child < definition->child_widgets.count; child++)
- {
- if (box_child != NONE && (child == box_child || child == list_child))
- children[child].vertical_offset -= grow / 2;
- else if (children[child].vertical_offset > list_top)
- children[child].vertical_offset += box_child != NONE ? grow - grow / 2 : grow;
}
- if (box_child != NONE && !box_redrawn)
+ quit = pause_quit_button(list, quit_function);
+ if (quit == NONE || *patched_list != NONE)
+ continue;
+ *added = pause_list_patch(instances, list, quit, host, vr_widget, campaign);
+ if (!*added)
+ return;
+ *buttons = list->child_widgets.count;
+ *patched_list = children[child].widget_tag.index;
+ list_child = child;
+ }
+ if (list_child == NONE)
+ return;
+ for (child = 0; child < definition->child_widgets.count; child++)
+ {
+ if (child != list_child && children[child].widget_tag.index != NONE &&
+ pause_box_stock(tag_get(UI_WIDGET_DEFINITION_TAG, children[child].widget_tag.index)))
{
- pause_box_redraw(tag_get(UI_WIDGET_DEFINITION_TAG, children[box_child].widget_tag.index), buttons);
- box_redrawn = TRUE;
+ box_child = child;
}
}
- if (patched_list != NONE)
- platform_log("menus: the pause menu has SETTINGS%s", host ? " and END GAME" : "");
+ /* the stock box: taller, centred where it was, the list with it, and
+ what is below it moved down; else only what is below the list */
+ grow = (short)(*added * PAUSE_BUTTON_SPACING);
+ list_top = children[list_child].vertical_offset;
+ for (child = 0; child < definition->child_widgets.count; child++)
+ {
+ if (box_child != NONE && (child == box_child || child == list_child))
+ children[child].vertical_offset -= grow / 2;
+ else if (children[child].vertical_offset > list_top)
+ children[child].vertical_offset += box_child != NONE ? grow - grow / 2 : grow;
+ }
+ if (box_child != NONE && !*box_redrawn)
+ {
+ pause_box_redraw(tag_get(UI_WIDGET_DEFINITION_TAG, children[box_child].widget_tag.index), *buttons);
+ *box_redrawn = TRUE;
+ }
+}
+
+static void pause_patch(struct cache_file_tag_instance *instances)
+{
+ long collection = tag_loaded('Soul', MULTIPLAYER_COLLECTION);
+ long quit_function = ui_function_named("mp game player quit");
+ boolean host = global_network_game_server_get() != NULL;
+ long vr_widget = pause_vr_widget(), patched_list = NONE, added = 0, buttons = 0, screen;
+ boolean box_redrawn = FALSE;
+
+ if (collection != NONE && quit_function != NONE)
+ {
+ struct tag_block const *screens = tag_get('Soul', collection);
+ for (screen = 0; screen < screens->count && !build.failed; screen++)
+ pause_screen_patch(instances, ((struct tag_reference const *)screens->address)[screen].index,
+ quit_function, host, vr_widget, FALSE, &patched_list, &added, &buttons, &box_redrawn);
+ if (patched_list != NONE)
+ platform_log("menus: the pause menu has SETTINGS%s%s", host ? " and END GAME" : "",
+ vr_widget != NONE ? " and VR SETUP" : "");
+ }
+ if (vr_widget != NONE && collection == NONE)
+ {
+ /* Exact campaign root, also used by native co-op. Its actions stay
+ unchanged; only VR Setup is inserted before Return to Main Menu. */
+ patched_list = NONE;
+ box_redrawn = FALSE;
+ pause_screen_patch(instances, tag_loaded(UI_WIDGET_DEFINITION_TAG, SOLO_PAUSE_SCREEN),
+ ui_function_named("pause game return to main menu"), FALSE, vr_widget, TRUE,
+ &patched_list, &added, &buttons, &box_redrawn);
+ }
}
void menu_tags_loaded(
@@ -1608,9 +1677,14 @@ void menu_tags_loaded(
long widget_count, own_lists = 0, total, index;
boolean game_map = strcmp(map_name, "ui") != 0;
-
- /* (ui.map, and a multiplayer map: its pause menu's SETTINGS) */
- if ((game_map && tag_loaded('Soul', MULTIPLAYER_COLLECTION) == NONE) ||
+ boolean multiplayer_map = tag_loaded('Soul', MULTIPLAYER_COLLECTION) != NONE;
+ boolean vr_campaign_map = FALSE;
+#ifdef HALO_VR
+ vr_campaign_map = halo_vr_running() && tag_loaded(UI_WIDGET_DEFINITION_TAG, SOLO_PAUSE_SCREEN) != NONE;
+#endif
+
+ /* ui.map, multiplayer pause menus, or the exact VR campaign pause root. */
+ if ((game_map && !multiplayer_map && !vr_campaign_map) ||
strcmp(config_string("display.menus"), "pc"))
{
return;
@@ -1621,6 +1695,7 @@ void menu_tags_loaded(
memset(&build, 0, sizeof(build));
build.menus = menus;
build.game_map = game_map;
+ build.multiplayer_map = game_map && multiplayer_map;
widget_count = menus->widget_count;
build.widget_tags = malloc((widget_count + 1) * sizeof(long));
build.text_tags = malloc((widget_count + 1) * sizeof(long));
diff --git a/port/linux/game/vr_hands.c b/port/linux/game/vr_hands.c
index 0ee945cc3aa0555c8553f770e83e9f0efd3a9931..c2cd143fbd22e7184c0cec38eacb0807d057616b 100644
--- a/port/linux/game/vr_hands.c
+++ b/port/linux/game/vr_hands.c
@@ -719,13 +719,25 @@ static void shoulders_place(struct vr_hands_frame const *frame, boolean const vi
if (visible[side])
{
- hands.i += hand->position.x - frame->head.x;
- hands.j += hand->position.y - frame->head.y;
+ real_vector3d offset;
+ offset.i = hand->position.x - frame->head.x;
+ offset.j = hand->position.y - frame->head.y;
+ offset.k = hand->position.z - frame->head.z;
+ if (frame->vehicle_basis_valid)
+ {
+ hands.i += frame->vehicle_basis[0][0] * offset.i + frame->vehicle_basis[0][1] * offset.j + frame->vehicle_basis[0][2] * offset.k;
+ hands.j += frame->vehicle_basis[1][0] * offset.i + frame->vehicle_basis[1][1] * offset.j + frame->vehicle_basis[1][2] * offset.k;
+ }
+ else
+ {
+ hands.i += offset.i;
+ hands.j += offset.j;
+ }
count++;
}
}
if (count && (real)sqrt(hands.i * hands.i + hands.j * hands.j) > 0.1f * frame->units * count)
- facing += 0.5f * angle_wrap((real)atan2(hands.j, hands.i) - frame->base_yaw - frame->head_yaw);
+ facing += 0.5f * angle_wrap((real)atan2(hands.j, hands.i) - (frame->vehicle_basis_valid ? 0.0f : frame->base_yaw) - frame->head_yaw);
if (!vr_hands.body_valid)
{
vr_hands.body_yaw = facing;
@@ -745,7 +757,7 @@ static void shoulders_place(struct vr_hands_frame const *frame, boolean const vi
}
if (frame->seconds > 0.0f)
vr_hands.body_yaw += difference * (1.0f - (real)exp(-frame->seconds / BODY_TURN_SECONDS));
- yaw = frame->base_yaw + vr_hands.body_yaw;
+ yaw = (frame->vehicle_basis_valid ? 0.0f : frame->base_yaw) + vr_hands.body_yaw;
forward->i = (real)cos(yaw);
forward->j = (real)sin(yaw);
forward->k = 0.0f;
@@ -754,6 +766,19 @@ static void shoulders_place(struct vr_hands_frame const *frame, boolean const vi
left->k = 0.0f;
up->i = up->j = 0.0f;
up->k = 1.0f;
+ if (frame->vehicle_basis_valid)
+ {
+ real c = forward->i, s = forward->j;
+ forward->i = frame->vehicle_basis[0][0] * c + frame->vehicle_basis[1][0] * s;
+ forward->j = frame->vehicle_basis[0][1] * c + frame->vehicle_basis[1][1] * s;
+ forward->k = frame->vehicle_basis[0][2] * c + frame->vehicle_basis[1][2] * s;
+ left->i = frame->vehicle_basis[1][0] * c - frame->vehicle_basis[0][0] * s;
+ left->j = frame->vehicle_basis[1][1] * c - frame->vehicle_basis[0][1] * s;
+ left->k = frame->vehicle_basis[1][2] * c - frame->vehicle_basis[0][2] * s;
+ up->i = frame->vehicle_basis[2][0];
+ up->j = frame->vehicle_basis[2][1];
+ up->k = frame->vehicle_basis[2][2];
+ }
for (side = 0; side < NUMBER_OF_SIDES; side++)
{
real out = side == _left ? frame->shoulder[0] : -frame->shoulder[0];
diff --git a/port/linux/game/vr_hands.h b/port/linux/game/vr_hands.h
index fc17b6e10f824be4002cfd90408c7c44486426a1..1bc58334806396ca919eb9fc37125d22e6bfd0f9 100644
--- a/port/linux/game/vr_hands.h
+++ b/port/linux/game/vr_hands.h
@@ -34,6 +34,9 @@ struct vr_hands_frame
real_point3d head;
real base_yaw;
real head_yaw;
+ /* The calibrated rigid seat basis, shared with physical head and hands. */
+ boolean vehicle_basis_valid;
+ float vehicle_basis[3][3];
/* vr.shoulders: each shoulder from the eyes' middle, in metres (out to
its side, up, forward), and the world's units a metre */
real shoulder[3];
diff --git a/port/linux/game/vr_render.c b/port/linux/game/vr_render.c
index 6b98a0b7739800bc5c90de07b0a821629e462705..5b1d76ab0ffc1c5d188cf6ed5bd0cc559fa1b23d 100644
--- a/port/linux/game/vr_render.c
+++ b/port/linux/game/vr_render.c
@@ -30,16 +30,22 @@ they belong on the layer before the eyes.
#include "render/render_cameras_internal.h"
#include "cutscene/cinematics.h"
#include "camera/director.h"
+#include "camera/observer.h"
#include "game/players.h"
+#include "items/weapons.h"
+#include "items/weapon_definitions.h"
#include "objects/objects.h"
#include "units/units.h"
+#include "units/unit_definitions.h"
#include "tag_files/tag_files.h"
#include "interface/hud.h"
+#include "interface/ui_widget.h"
#include "physics/collision_usage.h"
#include "physics/collisions.h"
#include "render/render.h"
#include "../src/vr.h"
+#include "../src/vr_vehicle.h"
#include "vr_hands.h"
#include <math.h>
@@ -48,12 +54,9 @@ they belong on the layer before the eyes.
/* the union of the eyes' fields of view, a little wider (the eyes are
apart, and the culling's planes start at the centre) */
#define UNION_MARGIN 1.03f
-/* the crosshairs in the eyes (vr_render_crosshairs): where the aim meets
-the world, no farther than this (world units) nor nearer than this, the
-distance's changes smoothed over this time constant (seconds) */
+/* The crosshairs show the nominal primary projectile's current hit point,
+before spread and ballistics, no farther than this (world units). */
#define CROSSHAIR_FARTHEST 100.0f
-#define CROSSHAIR_NEAREST 0.15f
-#define CROSSHAIR_SMOOTHING 0.06f
static struct
{
@@ -64,6 +67,7 @@ static struct
real_rectangle2d bounds;
/* each eye's frustum, of the eye's own camera */
struct render_frustum eyes[2];
+ struct render_camera eye_cameras[2];
/* the player's eye as the game has it, and the heading the head's and
controllers' poses are turned by */
real_point3d base_position;
@@ -95,10 +99,11 @@ static struct
real_vector3d aim_forward;
real_vector3d aim_up;
/* the crosshairs were drawn in the eyes this frame (not in the HUD's
- layer), and the inverse of their distance, smoothed */
+ layer) */
boolean crosshairs_drawn;
- boolean crosshair_distance_valid;
- real crosshair_inverse_distance;
+ /* Only the explicit world reticle draw may use the legacy HUD glyphs
+ in controller mode; its later head-locked HUD pass must not. */
+ boolean crosshairs_drawing;
} vr_render;
/* where the crosshairs are drawn from the HUD's middle, in its 640x480
@@ -132,6 +137,23 @@ static void view_rotate(real yaw, real pitch, const float local[3], real_vector3
yaw_rotate(yaw, pitched, world);
}
+/* A calibrated seat rotates physical head and hands by one rigid hull
+transform. The stock level/yaw path remains exact outside that seat. */
+static void view_vector(struct halo_vr_view const *view, real yaw, real pitch,
+ const float local[3], real_vector3d *world)
+{
+ if (view->vehicle_basis_valid)
+ {
+ float rotated[3];
+ halo_vr_vehicle_rotate(view->vehicle_basis, local, rotated);
+ world->i = rotated[0];
+ world->j = rotated[1];
+ world->k = rotated[2];
+ }
+ else
+ view_rotate(yaw, pitch, local, world);
+}
+
/* the view's pitch before the head's: the facing's while the gamepad aims
with vr.gamepad_view = "camera", else none (level with the horizon) */
static real view_pitch(void)
@@ -248,22 +270,445 @@ static long seated_unit(void)
return unit_index;
}
+static boolean vr_render_vehicle_camera_position(long unit_index, real_point3d *position);
+
+/* The left axis retains a horizontal heading at vertical flight's pole.
+Both the simulation controls and calibrated seat view use the same check. */
+static boolean vr_render_vehicle_yaw(real_vector3d const *forward, real_vector3d const *up, real *heading)
+{
+ real left_i, left_j;
+ if (!isfinite(forward->i) || !isfinite(forward->j) || !isfinite(forward->k))
+ return FALSE;
+ if (forward->i * forward->i + forward->j * forward->j < 0.0001f)
+ {
+ if (!isfinite(up->i) || !isfinite(up->j) || !isfinite(up->k))
+ return FALSE;
+ left_i = up->j * forward->k - up->k * forward->j;
+ left_j = up->k * forward->i - up->i * forward->k;
+ if (!isfinite(left_i) || !isfinite(left_j) || left_i * left_i + left_j * left_j < 0.0001f)
+ return FALSE;
+ *heading = (real)atan2(-left_i, left_j);
+ }
+ else
+ *heading = (real)atan2(forward->j, forward->i);
+ return TRUE;
+}
+
+/* Driving uses the simulation hull heading, independently of the rendered
+seat/camera heading and physical gaze. Only the stock local driver's seats
+opt in: passengers, gunner-only seats and scripted/recorded vehicles retain
+their ordinary control paths. The action queue and vehicle physics are unchanged. */
+int vr_render_vehicle_driving(long unit_index, float *hull_yaw)
+{
+ long player_index = local_player_get_player_index(0), parent_index;
+ struct unit_datum *unit, *parent;
+ struct unit_definition *definition;
+ struct unit_seat *seat;
+ real_vector3d forward, up;
+ const char *name;
+ short seat_index;
+ int mode;
+ if (!hull_yaw)
+ return HALO_VR_DRIVING_NONE;
+ *hull_yaw = 0.0f;
+ if (player_index == NONE || unit_index == NONE || !player_input_enabled() ||
+ cinematic_in_progress() || (director_camera_scripted && *director_camera_scripted) ||
+ director_get_perspective(0) == _director_perspective_scripted ||
+ director_inhibited_input(0) || director_inhibited_facing(0) ||
+ ui_widgets_active_for_local_player(0) || player_get(player_index)->unit_index != unit_index ||
+ !(unit = unit_try_and_get(unit_index)) || unit->unit.player_index != player_index ||
+ unit->object.type != _object_type_biped || TEST_FLAG(unit->object.damage_flags, _object_dead_bit) ||
+ TEST_FLAG(unit->unit.flags, _unit_possessed_by_recording_bit) ||
+ unit->unit.animation.state == _unit_state_entering_seat ||
+ unit->unit.animation.state == _unit_state_exiting_seat)
+ return HALO_VR_DRIVING_NONE;
+ parent_index = unit->object.parent_object_index;
+ seat_index = unit->unit.parent_seat_index;
+ if (parent_index == NONE || !(parent = unit_try_and_get(parent_index)) ||
+ parent->object.type != _object_type_vehicle || TEST_FLAG(parent->object.damage_flags, _object_dead_bit) ||
+ parent->object.parent_object_index != NONE ||
+ TEST_FLAG(parent->unit.flags, _unit_possessed_by_recording_bit))
+ return HALO_VR_DRIVING_NONE;
+ definition = unit_definition_get(parent->definition_index);
+ if (seat_index < 0 || seat_index >= definition->unit.seats.count)
+ return HALO_VR_DRIVING_NONE;
+ seat = TAG_BLOCK_GET_ELEMENT(&definition->unit.seats, seat_index, struct unit_seat);
+ if (!TEST_FLAG(seat->flags, _unit_seat_driver_bit))
+ return HALO_VR_DRIVING_NONE;
+ name = tag_get_name(parent->definition_index);
+ if (!name)
+ return HALO_VR_DRIVING_NONE;
+ if (!strcmp(name, "vehicles\\warthog\\warthog"))
+ mode = HALO_VR_DRIVING_WARTHOG;
+ else if (!strcmp(name, "vehicles\\ghost\\ghost") || !strcmp(name, "vehicles\\ghost\\ghost_mp"))
+ mode = HALO_VR_DRIVING_GHOST;
+ else if (!strcmp(name, "vehicles\\scorpion\\scorpion") || !strcmp(name, "vehicles\\scorpion\\scorpion_mp"))
+ mode = HALO_VR_DRIVING_SCORPION;
+ else if (!strcmp(name, "vehicles\\banshee\\banshee"))
+ mode = HALO_VR_DRIVING_BANSHEE;
+ else
+ return HALO_VR_DRIVING_NONE;
+ /* object_get_node_matrix is interpolated while drawing; do not use it
+ for authoritative desired facing or feed a rotating turret back into aim. */
+ object_get_orientation(parent_index, &forward, &up);
+ if (!vr_render_vehicle_yaw(&forward, &up, hull_yaw))
+ return HALO_VR_DRIVING_NONE;
+ return mode;
+}
+
+/* Prefer the same rigid hull axes as the camera anchor. If the renderer
+has no valid interpolated root, use its current simulation orientation. */
+static boolean vr_render_vehicle_basis(long parent_index, real_matrix4x3 const *root, real heading, float basis[3][3])
+{
+ float axes[3][3];
+ real_vector3d forward, up, left;
+ if (root)
+ {
+ memcpy(axes[0], &root->forward, sizeof(axes[0]));
+ memcpy(axes[1], &root->left, sizeof(axes[1]));
+ memcpy(axes[2], &root->up, sizeof(axes[2]));
+ if (halo_vr_vehicle_basis(axes, heading, heading, basis))
+ {
+ return TRUE;
+ }
+ }
+ object_get_orientation(parent_index, &forward, &up);
+ cross_product3d(&up, &forward, &left);
+ memcpy(axes[0], &forward, sizeof(axes[0]));
+ memcpy(axes[1], &left, sizeof(axes[1]));
+ memcpy(axes[2], &up, sizeof(axes[2]));
+ return halo_vr_vehicle_basis(axes, heading, heading, basis);
+}
+
+/* Called before simulation aim and fresh render views. Seat facing is
+captured once; pitch, roll and bounce follow the current rigid hull. */
+void vr_render_vehicle_reference(long unit_index)
+{
+ static struct { boolean valid; long unit, parent; short seat; real yaw_offset; } reference;
+ long player_index = local_player_get_player_index(0);
+ struct unit_datum *unit, *parent;
+ struct unit_definition *definition;
+ struct unit_seat *seat;
+ struct object_marker marker;
+ real_matrix4x3 const *root;
+ real_vector3d forward = { 0.0f, 0.0f, 0.0f }, up = { 0.0f, 0.0f, 0.0f };
+ real heading;
+ float hull_axes[3][3];
+ long parent_index;
+ short seat_index;
+ if (player_index == NONE || unit_index == NONE || cinematic_in_progress() ||
+ (director_camera_scripted && *director_camera_scripted) ||
+ director_get_perspective(0) == _director_perspective_scripted ||
+ player_get(player_index)->unit_index != unit_index ||
+ !(unit = unit_try_and_get(unit_index)) || unit->unit.player_index != player_index ||
+ unit->object.type != _object_type_biped || TEST_FLAG(unit->object.damage_flags, _object_dead_bit))
+ goto exit_seat;
+ parent_index = unit->object.parent_object_index;
+ seat_index = unit->unit.parent_seat_index;
+ if (parent_index == NONE || !(parent = unit_try_and_get(parent_index)) ||
+ parent->object.type != _object_type_vehicle || TEST_FLAG(parent->object.damage_flags, _object_dead_bit))
+ goto exit_seat;
+ definition = unit_definition_get(parent->definition_index);
+ if (seat_index < 0 || seat_index >= definition->unit.seats.count)
+ goto exit_seat;
+ seat = TAG_BLOCK_GET_ELEMENT(&definition->unit.seats, seat_index, struct unit_seat);
+ /* Root-node matrices are interpolated for rendering. Capture a seat's
+ facing offset once: a moving turret marker must not feed aim back into
+ the view heading each frame. Subsequent yaw follows only the vehicle. */
+ root = object_get_node_matrix(parent_index, 0);
+ if (root)
+ {
+ forward = root->forward;
+ up = root->up;
+ }
+ if (!root || !isfinite(forward.i) || !isfinite(forward.j) || !isfinite(forward.k) ||
+ forward.i * forward.i + forward.j * forward.j < 0.0001f)
+ object_get_orientation(parent_index, &forward, &up);
+ if (!vr_render_vehicle_yaw(&forward, &up, &heading) ||
+ !vr_render_vehicle_basis(parent_index, root, heading, hull_axes))
+ goto invalid_hull;
+ if (!reference.valid || reference.unit != unit_index || reference.parent != parent_index || reference.seat != seat_index)
+ {
+ reference.yaw_offset = 0.0f;
+ if (seat->marker_name[0] && memchr(seat->marker_name, 0, sizeof(seat->marker_name)) &&
+ object_get_marker_by_name(parent_index, seat->marker_name, &marker, 1))
+ {
+ /* World heading differences are not local seat yaw on a
+ tilted hull. Project both marker axes into that rigid frame. */
+ forward.i = hull_axes[0][0] * marker.matrix.forward.i + hull_axes[0][1] * marker.matrix.forward.j + hull_axes[0][2] * marker.matrix.forward.k;
+ forward.j = hull_axes[1][0] * marker.matrix.forward.i + hull_axes[1][1] * marker.matrix.forward.j + hull_axes[1][2] * marker.matrix.forward.k;
+ forward.k = hull_axes[2][0] * marker.matrix.forward.i + hull_axes[2][1] * marker.matrix.forward.j + hull_axes[2][2] * marker.matrix.forward.k;
+ up.i = hull_axes[0][0] * marker.matrix.up.i + hull_axes[0][1] * marker.matrix.up.j + hull_axes[0][2] * marker.matrix.up.k;
+ up.j = hull_axes[1][0] * marker.matrix.up.i + hull_axes[1][1] * marker.matrix.up.j + hull_axes[1][2] * marker.matrix.up.k;
+ up.k = hull_axes[2][0] * marker.matrix.up.i + hull_axes[2][1] * marker.matrix.up.j + hull_axes[2][2] * marker.matrix.up.k;
+ if (!vr_render_vehicle_yaw(&forward, &up, &reference.yaw_offset))
+ goto exit_seat;
+ }
+ reference.unit = unit_index;
+ reference.parent = parent_index;
+ reference.seat = seat_index;
+ reference.valid = TRUE;
+ }
+ halo_vr_vehicle_reference((int)unit_index, (int)parent_index, seat_index, heading + reference.yaw_offset);
+ halo_vr_vehicle_orientation(hull_axes, heading);
+ return;
+invalid_hull:
+ /* Preserve seat identity on corrupt hull data so tracked controls are
+ neutral and no flat/old controller-aim path is accidentally revived. */
+ halo_vr_vehicle_reference((int)unit_index, (int)parent_index, seat_index, 0.0f);
+ halo_vr_vehicle_orientation(NULL, 0.0f);
+ return;
+exit_seat:
+ reference.valid = FALSE;
+ (void)vr_render_vehicle_camera_position(NONE, NULL);
+ halo_vr_vehicle_reference(-1, -1, -1, 0.0f);
+}
+
+/* Seat acceleration animates the biped's head, which is unsuitable as a VR
+camera anchor. Authored camera markers stay attached to the rendered vehicle.
+The stock Warthog driver omits its tag's camera name even though its model
+contains the intended camera driver marker. Use its position, never rotation. */
+static boolean vr_render_vehicle_camera_position(long unit_index, real_point3d *position)
+{
+ static struct { boolean valid; long unit, parent; short seat; real_point3d offset; } fallback;
+ long player_index = local_player_get_player_index(0);
+ struct unit_datum *unit, *parent;
+ struct unit_definition *definition;
+ struct unit_seat *seat;
+ struct object_marker marker;
+ real_matrix4x3 const *root;
+ real_point3d head, result;
+ const char *name, *vehicle_name;
+ long parent_index;
+ short seat_index, row, column, other;
+ real dot;
+ if (!position || player_index == NONE || unit_index == NONE || cinematic_in_progress() ||
+ (director_camera_scripted && *director_camera_scripted) ||
+ director_get_perspective(0) == _director_perspective_scripted ||
+ player_get(player_index)->unit_index != unit_index ||
+ !(unit = unit_try_and_get(unit_index)) || unit->unit.player_index != player_index ||
+ unit->object.type != _object_type_biped || TEST_FLAG(unit->object.damage_flags, _object_dead_bit))
+ goto invalid_seat;
+ parent_index = unit->object.parent_object_index;
+ seat_index = unit->unit.parent_seat_index;
+ if (parent_index == NONE || !(parent = unit_try_and_get(parent_index)) ||
+ parent->object.type != _object_type_vehicle || TEST_FLAG(parent->object.damage_flags, _object_dead_bit))
+ goto invalid_seat;
+ definition = unit_definition_get(parent->definition_index);
+ if (seat_index < 0 || seat_index >= definition->unit.seats.count)
+ goto invalid_seat;
+ seat = TAG_BLOCK_GET_ELEMENT(&definition->unit.seats, seat_index, struct unit_seat);
+ name = seat->camera.marker_name;
+ if (!memchr(name, 0, sizeof(seat->camera.marker_name)))
+ goto invalid_seat;
+ if (!*name)
+ {
+ vehicle_name = tag_get_name(parent->definition_index);
+ if (TEST_FLAG(seat->flags, _unit_seat_driver_bit) && vehicle_name &&
+ !strcmp(vehicle_name, "vehicles\\warthog\\warthog"))
+ name = "camera driver";
+ }
+ if (*name && object_get_marker_by_name(parent_index, name, &marker, 1))
+ {
+ if (!isfinite(marker.matrix.position.x) || !isfinite(marker.matrix.position.y) ||
+ !isfinite(marker.matrix.position.z))
+ goto invalid_seat;
+ fallback.valid = FALSE;
+ *position = marker.matrix.position;
+ return TRUE;
+ }
+ /* Linking precedes boarding animation/interpolation. Until it finishes,
+ let the caller use the live head rather than freezing its outside pose.
+ Afterward retain one head offset in the hull root, not the lean-animated
+ biped or a rotating turret. Full salted identities prevent datum reuse. */
+ if (unit->unit.animation.state == _unit_state_entering_seat ||
+ unit->unit.animation.state == _unit_state_exiting_seat)
+ goto invalid_seat;
+ root = object_get_node_matrix(parent_index, 0);
+ if (!root || !isfinite(root->scale) || fabs(root->scale) < 0.000001f)
+ goto invalid_seat;
+ for (row = 0; row < 4; row++)
+ for (column = 0; column < 3; column++)
+ if (!isfinite(root->n[row][column]))
+ goto invalid_seat;
+ /* The engine's inverse assumes an orthonormal basis and uniform scale.
+ Finite, unit, perpendicular axes make that inverse well-defined (also
+ for a mirrored root). A singular or distorted matrix keeps the old path. */
+ for (row = 0; row < 3; row++)
+ for (other = row; other < 3; other++)
+ {
+ dot = 0.0f;
+ for (column = 0; column < 3; column++)
+ dot += root->n[row][column] * root->n[other][column];
+ if (!isfinite(dot) || fabs(dot - (row == other ? 1.0f : 0.0f)) > 0.01f)
+ goto invalid_seat;
+ }
+ if (!fallback.valid || fallback.unit != unit_index || fallback.parent != parent_index ||
+ fallback.seat != seat_index)
+ {
+ unit_get_head_position(unit_index, &head);
+ if (!isfinite(head.x) || !isfinite(head.y) || !isfinite(head.z))
+ goto invalid_seat;
+ matrix4x3_inverse_transform_point(root, &head, &result);
+ if (!isfinite(result.x) || !isfinite(result.y) || !isfinite(result.z))
+ goto invalid_seat;
+ fallback.offset = result;
+ fallback.unit = unit_index;
+ fallback.parent = parent_index;
+ fallback.seat = seat_index;
+ fallback.valid = TRUE;
+ }
+ matrix4x3_transform_point(root, &fallback.offset, &result);
+ if (!isfinite(result.x) || !isfinite(result.y) || !isfinite(result.z))
+ goto invalid_seat;
+ *position = result;
+ return TRUE;
+invalid_seat:
+ fallback.valid = FALSE;
+ return FALSE;
+}
+
+/* Clear only the occupied stock Warthog's glass, never its body or another
+object's glass. This changes the local VR draw, not retail tags/physics. */
+boolean vr_render_hides_vehicle_glass(long object_index)
+{
+ struct unit_datum *unit, *parent;
+ const char *name;
+ if (!vr_render.active || vr_render.seated_unit_index == NONE ||
+ !halo_vr_vehicle_clear_windshield() || seated_unit() != vr_render.seated_unit_index ||
+ !(unit = unit_try_and_get(vr_render.seated_unit_index)) ||
+ TEST_FLAG(unit->object.damage_flags, _object_dead_bit) ||
+ object_index != unit->object.parent_object_index ||
+ !(parent = unit_try_and_get(object_index)) || parent->object.type != _object_type_vehicle)
+ return FALSE;
+ name = tag_get_name(parent->definition_index);
+ return name && !strcmp(name, "vehicles\\warthog\\warthog");
+}
+
+/* Scripts run between eye passes too. Build the same HMD basis and centre
+origin as the world camera from a fresh view, rather than using the ended
+render pass or the controller's weapon-facing pose. */
+boolean vr_render_player_gaze(long unit_index, real_point3d *origin, real_vector3d *forward)
+{
+ long player_index = local_player_get_player_index(0);
+ struct unit_datum *unit;
+ struct halo_vr_view view;
+ struct observer_result const *observer;
+ director_perspective perspective;
+ real_point3d position;
+ real_vector3d pointing, offset;
+ real yaw, pitch = 0.0f, length;
+ boolean aiming;
+ if (!origin || !forward || player_index == NONE || unit_index == NONE ||
+ cinematic_in_progress() || (director_camera_scripted && *director_camera_scripted) ||
+ player_get(player_index)->unit_index != unit_index ||
+ !(unit = unit_try_and_get(unit_index)) || unit->unit.player_index != player_index ||
+ unit->object.type != _object_type_biped || TEST_FLAG(unit->object.damage_flags, _object_dead_bit))
+ return FALSE;
+ vr_render_vehicle_reference(unit_index);
+ perspective = director_get_perspective(0);
+ if (perspective == _director_perspective_scripted || perspective == _director_perspective_neutral ||
+ director_inhibited_facing(0) || !halo_vr_view(&view) || !halo_vr_gaze_tracking_valid())
+ return FALSE;
+ observer = render_interpolation_camera(0, observer_get_camera(0));
+ if (!observer || !isfinite(view.body_yaw) || !isfinite(view.world_units_per_metre) ||
+ view.world_units_per_metre <= 0.0f)
+ return FALSE;
+ aiming = halo_vr_aiming();
+ yaw = aiming ? view.body_yaw : (real)atan2(observer->forward.j, observer->forward.i);
+ if (view.gamepad_pitch && aiming)
+ pitch = player_control_get_facing_angles(0)->pitch;
+ if (!isfinite(yaw) || !isfinite(pitch))
+ return FALSE;
+ position = observer->position;
+ if (view.vehicle_first_person && aiming && seated_unit() == unit_index)
+ {
+ if (!vr_render_vehicle_camera_position(unit_index, &position))
+ unit_get_head_position(unit_index, &position);
+ }
+ view_vector(&view, yaw, pitch, view.head_forward, &pointing);
+ view_vector(&view, yaw, pitch, view.head_position, &offset);
+ position.x += offset.i * view.world_units_per_metre;
+ position.y += offset.j * view.world_units_per_metre;
+ position.z += offset.k * view.world_units_per_metre;
+ length = magnitude3d(&pointing);
+ if (!isfinite(position.x) || !isfinite(position.y) || !isfinite(position.z) ||
+ !isfinite(length) || length <= 0.0f)
+ return FALSE;
+ normalize3d(&pointing);
+ *origin = position;
+ *forward = pointing;
+ return TRUE;
+}
+
+/* Render-only heading for the local motion sensor. The gaze uses the same
+current HMD/body basis as the camera; a weapon or two-hand aim never turns
+the radar. Flat, remote, scripted and invalid/near-vertical views keep the
+original tick heading without changing simulation or history data. */
+boolean vr_render_motion_sensor_yaw(short local_player_index, real *yaw)
+{
+ long player_index;
+ real_point3d origin;
+ real_vector3d forward;
+ real horizontal_squared, heading;
+ if (!yaw || local_player_index != 0 || !halo_vr_radar_head_heading() ||
+ (player_index = local_player_get_player_index(0)) == NONE ||
+ !vr_render_player_gaze(player_get(player_index)->unit_index, &origin, &forward))
+ return FALSE;
+ if (!isfinite(forward.i) || !isfinite(forward.j) || !isfinite(forward.k))
+ return FALSE;
+ horizontal_squared = forward.i * forward.i + forward.j * forward.j;
+ if (!isfinite(horizontal_squared) || horizontal_squared < 0.01f)
+ return FALSE;
+ heading = (real)atan2(forward.j, forward.i);
+ if (!isfinite(heading))
+ return FALSE;
+ *yaw = heading;
+ return TRUE;
+}
+
+/* A small high-contrast cursor on the menu texture, in native UI pixels.
+No focus or input is changed while drawing; Xbox navigation remains live. */
+void vr_render_menu_pointer_draw(void)
+{
+ short x, y;
+ rectangle2d bounds;
+ if (!halo_vr_menu_cursor(&x, &y))
+ return;
+ bounds.x0 = (short)MAX(0, x - 3);
+ bounds.y0 = (short)MAX(0, y - 3);
+ bounds.x1 = (short)MIN(640, x + 4);
+ bounds.y1 = (short)MIN(480, y + 4);
+ draw_quad(&bounds, 0xff000000);
+ bounds.x0 = (short)MAX(0, x - 1);
+ bounds.y0 = (short)MAX(0, y - 1);
+ bounds.x1 = (short)MIN(640, x + 2);
+ bounds.y1 = (short)MIN(480, y + 2);
+ draw_quad(&bounds, 0xffffffff);
+}
+
/* turns the window's camera into the VR frame's centre camera (both its
render and rasterizer cameras); FALSE without a VR frame to draw */
+
+
boolean vr_render_camera(struct render_camera *camera, struct render_camera *rasterizer_camera)
{
struct halo_vr_view *view = &vr_render.view;
real base_yaw;
real pitch;
real_vector3d offset;
- real left = 0.0f, right = 0.0f, up = 0.0f, down = 0.0f, tangent;
+ real left, right, up, down, tangent;
+ float union_bounds[4];
real aspect;
short eye;
vr_render.active = FALSE;
+ halo_vr_render_view_commit(0);
vr_render.seated_unit_index = NONE;
vr_render.weapon_camera_set = FALSE;
vr_render.weapon_camera_valid = FALSE;
+ vr_render.crosshairs_drawing = FALSE;
+ vr_render_vehicle_reference(seated_unit());
if (!halo_vr_view(view))
return FALSE;
/* the turn of the stick (and of the aim) while the aim follows the head;
@@ -273,11 +718,11 @@ boolean vr_render_camera(struct render_camera *camera, struct render_camera *ras
if (view->vehicle_first_person && halo_vr_aiming() &&
(vr_render.seated_unit_index = seated_unit()) != NONE)
{
- /* in a vehicle's seat (vr.vehicle_view): the eye is the seated
- player's head, as the frame poses it between ticks, and the view is
- level and turned as on foot; not the game's camera following the
- vehicle, which swings about it with the aim */
- unit_get_head_position(vr_render.seated_unit_index, &camera->position);
+ /* Use the authored stable seat camera before physical HMD/hand
+ offsets. Keep head tracking and full rigid hull rotation, without the
+ biped's acceleration lean or the game's follow-camera sway. */
+ if (!vr_render_vehicle_camera_position(vr_render.seated_unit_index, &camera->position))
+ unit_get_head_position(vr_render.seated_unit_index, &camera->position);
}
vr_render.base_position = camera->position;
vr_render.base_yaw = base_yaw;
@@ -300,27 +745,38 @@ boolean vr_render_camera(struct render_camera *camera, struct render_camera *ras
vr_render.aim_up = camera->up;
}
pitch = view_pitch();
- view_rotate(base_yaw, pitch, view->head_forward, &camera->forward);
- view_rotate(base_yaw, pitch, view->head_up, &camera->up);
- view_rotate(base_yaw, pitch, view->head_position, &offset);
+ view_vector(view, base_yaw, pitch, view->head_forward, &camera->forward);
+ view_vector(view, base_yaw, pitch, view->head_up, &camera->up);
+ view_vector(view, base_yaw, pitch, view->head_position, &offset);
camera->position.x += offset.i * view->world_units_per_metre;
camera->position.y += offset.j * view->world_units_per_metre;
camera->position.z += offset.k * view->world_units_per_metre;
vr_render.head_position = camera->position;
- for (eye = 0; eye < 2; eye++)
- {
- left = eye ? MIN(left, view->fov[eye][0]) : view->fov[eye][0];
- right = eye ? MAX(right, view->fov[eye][1]) : view->fov[eye][1];
- up = eye ? MAX(up, view->fov[eye][2]) : view->fov[eye][2];
- down = eye ? MIN(down, view->fov[eye][3]) : view->fov[eye][3];
- }
- left *= UNION_MARGIN;
- right *= UNION_MARGIN;
- up *= UNION_MARGIN;
- down *= UNION_MARGIN;
+ /* Include both translated eye cones at the actual world near plane;
+ the original 3% angular margin alone misses nearby cockpit geometry.
+ Each eye still receives its original headset projection below. */
+ if (!halo_vr_stereo_union(view->fov, view->eye_offset, view->world_units_per_metre,
+ camera->z_near, UNION_MARGIN, union_bounds) ||
+ camera->viewport_bounds.x1 <= camera->viewport_bounds.x0 ||
+ camera->viewport_bounds.y1 <= camera->viewport_bounds.y0 ||
+ !isfinite(camera->position.x) || !isfinite(camera->position.y) || !isfinite(camera->position.z) ||
+ !isfinite(camera->forward.i) || !isfinite(camera->forward.j) || !isfinite(camera->forward.k) ||
+ !isfinite(camera->up.i) || !isfinite(camera->up.j) || !isfinite(camera->up.k) ||
+ fabs(dot_product3d(&camera->forward, &camera->forward) - 1.0f) > 0.01f ||
+ fabs(dot_product3d(&camera->up, &camera->up) - 1.0f) > 0.01f ||
+ fabs(dot_product3d(&camera->forward, &camera->up)) > 0.01f)
+ return FALSE;
+ left = union_bounds[0];
+ right = union_bounds[1];
+ up = union_bounds[2];
+ down = union_bounds[3];
tangent = MAX(up, -down);
camera->vertical_field_of_view = 2.0f * (real)atan(tangent);
+ if (camera->vertical_field_of_view <= _real_epsilon ||
+ camera->vertical_field_of_view >= _pi - _real_epsilon ||
+ !isfinite(camera->z_far) || camera->z_far <= camera->z_near)
+ return FALSE;
aspect = (real)(camera->viewport_bounds.x1 - camera->viewport_bounds.x0) /
(real)(camera->viewport_bounds.y1 - camera->viewport_bounds.y0);
vr_render.bounds.x0 = left / (aspect * tangent);
@@ -356,6 +812,7 @@ boolean vr_render_camera(struct render_camera *camera, struct render_camera *ras
eye_bounds.y0 = view->fov[eye][3] / tangent;
eye_bounds.y1 = view->fov[eye][2] / tangent;
render_camera_build_frustum(&eye_camera, &eye_bounds, &vr_render.eyes[eye], TRUE);
+ vr_render.eye_cameras[eye] = eye_camera;
render_camera_build_frustum(camera, &eye_bounds, &vr_render.eyes_at_centre[eye], TRUE);
}
{
@@ -372,6 +829,8 @@ boolean vr_render_camera(struct render_camera *camera, struct render_camera *ras
}
vr_render.infinite = FALSE;
vr_render.projection_valid = FALSE;
+ if (!halo_vr_render_view_commit(1))
+ return FALSE;
vr_render.active = TRUE;
return TRUE;
}
@@ -400,12 +859,70 @@ static void hand_point(const float place[3], real_point3d *point)
real_vector3d offset;
real units = vr_render.view.world_units_per_metre;
- yaw_rotate(vr_render.base_yaw, place, &offset);
+ view_vector(&vr_render.view, vr_render.base_yaw, 0.0f, place, &offset);
point->x = vr_render.base_position.x + offset.i * units;
point->y = vr_render.base_position.y + offset.j * units;
point->z = vr_render.base_position.z + offset.k * units;
}
+/* A visible first-person rig may remain in a scripted arrival after gameplay
+aiming is disabled. Its tracked pose is presentation only: never revive aim,
+controls, or a third-person cinematic's hidden rig. */
+static boolean vr_render_controller_pose_valid(void)
+{
+ struct halo_vr_view const *view = &vr_render.view;
+ real_vector3d forward, up, side;
+ real length;
+ short axis;
+ if (!view->controller_aim ||
+ !isfinite(vr_render.base_yaw) || !isfinite(view->world_units_per_metre) || view->world_units_per_metre <= 0.0f ||
+ !isfinite(vr_render.base_position.x) || !isfinite(vr_render.base_position.y) ||
+ !isfinite(vr_render.base_position.z))
+ return FALSE;
+ for (axis = 0; axis < 3; axis++)
+ {
+ if (!isfinite(view->hand_forward[axis]) || !isfinite(view->hand_up[axis]) ||
+ !isfinite(view->hand_position[axis]) || !isfinite(view->right_grip_position[axis]) ||
+ !isfinite(view->weapon_offset[axis]))
+ return FALSE;
+ }
+ view_vector(view, vr_render.base_yaw, 0.0f, view->hand_forward, &forward);
+ view_vector(view, vr_render.base_yaw, 0.0f, view->hand_up, &up);
+ cross_product3d(&forward, &up, &side);
+ length = magnitude3d(&side);
+ return isfinite(length) && length > 0.0001f;
+}
+
+static boolean vr_render_controller_presentation(void)
+{
+ struct halo_vr_view const *view = &vr_render.view;
+ long player_index, unit_index;
+ struct unit_datum *unit;
+ return vr_render.active && render.local_player_index == 0 && !view->gamepad_aim &&
+ halo_vr_presentation_tracking_valid() && vr_render_controller_pose_valid() &&
+ director_get_perspective(0) == _director_perspective_first_person &&
+ (player_index = local_player_get_player_index(0)) != NONE &&
+ (unit_index = player_get(player_index)->unit_index) != NONE &&
+ (unit = unit_try_and_get(unit_index)) && unit->unit.player_index == player_index &&
+ unit->object.type == _object_type_biped && !TEST_FLAG(unit->object.damage_flags, _object_dead_bit);
+}
+
+/* Keep the actual rig visibility in step with the live menu and the
+controller pose. Returning FALSE from a camera/hand override alone leaves
+the stock head-attached rig visible, or reuses its previous frame's nodes. */
+boolean vr_render_first_person_visible(short local_player_index)
+{
+ boolean frame_active = halo_vr_frame_active();
+ if (frame_active && local_player_index >= 0 &&
+ local_player_index < MAXIMUM_NUMBER_OF_LOCAL_PLAYERS &&
+ ui_widgets_active_for_local_player(local_player_index))
+ return FALSE;
+ if (!frame_active || local_player_index != 0 ||
+ !vr_render.view.controller_aim_requested || vr_render.view.gamepad_aim)
+ return TRUE;
+ return vr_render.active && halo_vr_presentation_tracking_valid() && vr_render_controller_pose_valid();
+}
+
/* the camera the first-person weapon is posed from (render.c): with the
controller aiming, one placed so that the weapon is in the right controller,
pointing where it points: with the hands floating (vr.hands), the right
@@ -419,13 +936,21 @@ boolean vr_render_weapon_camera(struct render_camera *camera)
real_point3d hand, centre;
real units = view->world_units_per_metre;
const float *o = view->weapon_offset;
+ boolean aiming = halo_vr_aiming();
+ boolean presentation;
if (!vr_render.active)
return FALSE;
- /* (the head's, kept for the hands when the controller is not tracked) */
+ vr_render.weapon_camera_set = FALSE;
+ vr_render.weapon_camera_valid = FALSE;
+ if (!vr_render_first_person_visible(render.local_player_index))
+ return FALSE;
+ presentation = !aiming && vr_render_controller_presentation();
+ /* Head/gamepad aim deliberately retain the ordinary camera. */
vr_render.weapon_camera = *camera;
- vr_render.weapon_camera_set = halo_vr_aiming();
- if (view->gamepad_aim && halo_vr_aiming())
+ vr_render.weapon_camera_set = aiming || presentation;
+ vr_render.weapon_camera_valid = FALSE;
+ if (view->gamepad_aim && aiming)
{
/* the gamepad aims (vr.aim = "gamepad"): the weapon is posed as on
the flat screen, from the player's eye looking where the player
@@ -437,10 +962,11 @@ boolean vr_render_weapon_camera(struct render_camera *camera)
vr_render.weapon_camera_valid = TRUE;
return TRUE;
}
- if (!view->controller_aim || !halo_vr_aiming())
+ if (!view->controller_aim || (!aiming && !presentation) ||
+ !vr_render_controller_pose_valid())
return FALSE;
- yaw_rotate(vr_render.base_yaw, view->hand_forward, &forward);
- yaw_rotate(vr_render.base_yaw, view->hand_up, &up);
+ view_vector(view, vr_render.base_yaw, 0.0f, view->hand_forward, &forward);
+ view_vector(view, vr_render.base_yaw, 0.0f, view->hand_up, &up);
cross_product3d(&forward, &up, &right);
normalize3d(&right);
camera->forward = forward;
@@ -462,6 +988,14 @@ boolean vr_render_weapon_camera(struct render_camera *camera)
camera->position.y = hand.y - (right.j * o[0] + up.j * o[1] + forward.j * o[2]) * units;
camera->position.z = hand.z - (right.k * o[0] + up.k * o[1] + forward.k * o[2]) * units;
}
+ if (!isfinite(camera->position.x) || !isfinite(camera->position.y) ||
+ !isfinite(camera->position.z))
+ {
+ *camera = vr_render.weapon_camera;
+ view->controller_aim = FALSE;
+ vr_render.weapon_camera_set = FALSE;
+ return FALSE;
+ }
vr_render.weapon_camera = *camera;
vr_render.weapon_camera_valid = TRUE;
return TRUE;
@@ -475,7 +1009,8 @@ boolean vr_render_hands(struct vr_hands_frame *frame)
struct halo_vr_view *view = &vr_render.view;
struct render_camera const *camera = &vr_render.weapon_camera;
- if (!vr_render.active || !vr_render.weapon_camera_set || !view->floating_hands)
+ if (!vr_render.active || !vr_render_first_person_visible(render.local_player_index) ||
+ !vr_render.weapon_camera_set || !view->floating_hands)
return FALSE;
frame->arms = view->arms;
matrix4x3_from_point_and_vectors(&frame->camera, &camera->position, &camera->forward, &camera->up);
@@ -490,21 +1025,30 @@ boolean vr_render_hands(struct vr_hands_frame *frame)
frame->left_valid = view->left_hand_valid;
if (frame->left_valid)
{
- real_vector3d forward, up;
+ real_vector3d forward, up, side;
real_point3d position;
+ real length;
- yaw_rotate(vr_render.base_yaw, view->left_hand_forward, &forward);
- yaw_rotate(vr_render.base_yaw, view->left_hand_up, &up);
+ view_vector(view, vr_render.base_yaw, 0.0f, view->left_hand_forward, &forward);
+ view_vector(view, vr_render.base_yaw, 0.0f, view->left_hand_up, &up);
normalize3d(&forward);
normalize3d(&up);
hand_point(view->left_grip_position, &position);
- matrix4x3_from_point_and_vectors(&frame->left, &position, &forward, &up);
+ cross_product3d(&forward, &up, &side);
+ length = magnitude3d(&side);
+ frame->left_valid = isfinite(position.x) && isfinite(position.y) && isfinite(position.z) &&
+ isfinite(forward.i) && isfinite(forward.j) && isfinite(forward.k) &&
+ isfinite(up.i) && isfinite(up.j) && isfinite(up.k) && isfinite(length) && length > 0.0001f;
+ if (frame->left_valid)
+ matrix4x3_from_point_and_vectors(&frame->left, &position, &forward, &up);
}
frame->two_handed = view->two_handed;
frame->right_trigger = view->right_trigger;
frame->left_grip = view->left_grip;
frame->head = vr_render.head_position;
frame->base_yaw = vr_render.base_yaw;
+ frame->vehicle_basis_valid = view->vehicle_basis_valid;
+ memcpy(frame->vehicle_basis, view->vehicle_basis, sizeof(frame->vehicle_basis));
frame->head_yaw = (real)atan2(view->head_forward[1], view->head_forward[0]);
frame->shoulder[0] = view->shoulder_offset[0];
frame->shoulder[1] = view->shoulder_offset[1];
@@ -542,6 +1086,39 @@ void vr_render_end(void)
vr_render.active = FALSE;
vr_render.seated_unit_index = NONE;
vr_render.infinite = FALSE;
+ vr_render.crosshairs_drawing = FALSE;
+ vr_render_screen_point_end();
+ halo_vr_screen_space(0);
+}
+
+/* The legacy sun glow stamps an occlusion mask into the eye, copies that
+eye's pixels into mono 64x64 scratch textures, blurs and composites them.
+Run it once per eye: its CPU projection, depth test and source texture must
+all refer to the same eye. These screen coordinates are already eye-local. */
+boolean vr_render_sun_glow_begin(short eye, struct render_camera *camera, struct render_frustum *frustum)
+{
+ short k;
+ real depth[4];
+ if (!vr_render.active || eye < 0 || eye >= 2)
+ return FALSE;
+ for (k = 0; k < 4; k++)
+ depth[k] = frustum->projection_matrix[k][2];
+ *camera = vr_render.eye_cameras[eye];
+ *frustum = vr_render.eyes[eye];
+ /* Match the depth range currently used by the scene, as the world
+ geometry's eye transforms do; x/y use the eye's asymmetric projection. */
+ for (k = 0; k < 4; k++)
+ frustum->projection_matrix[k][2] = depth[k];
+ vr_render_screen_point_end();
+ halo_vr_screen_space(1);
+ halo_vr_draw_eye(eye);
+ return TRUE;
+}
+
+void vr_render_sun_glow_end(void)
+{
+ halo_vr_draw_eye(-1);
+ halo_vr_screen_space(0);
vr_render_screen_point_end();
}
@@ -575,44 +1152,209 @@ boolean vr_render_hud_camera(struct render_camera *camera)
{
real pitch = view_pitch();
- view_rotate(base_yaw, pitch, view->head_forward, &camera->forward);
- view_rotate(base_yaw, pitch, view->head_up, &camera->up);
+ view_vector(view, base_yaw, pitch, view->head_forward, &camera->forward);
+ view_vector(view, base_yaw, pitch, view->head_up, &camera->up);
camera->vertical_field_of_view = 2.0f * (real)atan(view->hud_tangent);
- if ((view->controller_aim || view->gamepad_aim) && halo_vr_aiming() && !vr_render_crosshair_offset[0])
+ /* Unsupported projectile previews retain the ordinary HUD centre. */
+ }
+ return TRUE;
+}
+
+/* The nominal ray is drawn every frame, while unit aiming is 30 Hz.
+Only an unrestricted local player on foot previews the current rendered
+pointing pose. The paired camera is the interpolated observer before camera
+shake and physical head/hand offsets; it retains the shot's origin rules. */
+static boolean vr_render_pointing_pose(long player_index, struct unit_datum *unit,
+ struct halo_vr_view const *view, real base_yaw, struct projectile_preview_pose *pose)
+{
+ struct unit_definition *definition;
+ struct observer_result const *observer;
+ real length;
+ if (player_index != local_player_get_player_index(0) ||
+ unit->object.type != _object_type_biped || unit->object.parent_object_index != NONE ||
+ unit->unit.gunner_object_index != NONE ||
+ director_get_perspective(0) != _director_perspective_first_person || director_inhibited_facing(0))
+ return FALSE;
+ definition = unit_definition_get(unit->definition_index);
+ if (!TEST_FLAG(definition->unit.flags, _unit_fires_from_camera_bit) ||
+ definition->unit.aiming_velocity_maximum != 0.0f || definition->unit.aiming_acceleration_maximum != 0.0f)
+ return FALSE;
+ if (view->controller_aim)
+ {
+ real yaw, pitch, horizontal;
+ view_vector(view, base_yaw, 0.0f, view->hand_forward, &pose->forward);
+ length = magnitude3d(&pose->forward);
+ if (!isfinite(length) || length <= 0.0f)
+ return FALSE;
+ normalize3d(&pose->forward);
+ yaw = (real)atan2(pose->forward.j, pose->forward.i);
+ pitch = PIN((real)asin(PIN(pose->forward.k, -1.0f, 1.0f)),
+ -DEGREES_TO_RADIANS(85.0f), DEGREES_TO_RADIANS(85.0f));
+ horizontal = (real)cos(pitch);
+ pose->forward.i = horizontal * (real)cos(yaw);
+ pose->forward.j = horizontal * (real)sin(yaw);
+ pose->forward.k = (real)sin(pitch);
+ }
+ else if (view->gamepad_aim)
+ player_control_get_facing_direction(0, &pose->forward);
+ else
+ return FALSE;
+ length = magnitude3d(&pose->forward);
+ if (!isfinite(length) || length <= 0.0f)
+ return FALSE;
+ normalize3d(&pose->forward);
+ unit_clip_to_aiming_bounds(player_get(player_index)->unit_index, &pose->forward, TRUE);
+ observer = render_interpolation_camera(0, observer_get_camera(0));
+ if (!observer)
+ return FALSE;
+ pose->camera_position = observer->position;
+ return isfinite(pose->camera_position.x) && isfinite(pose->camera_position.y) && isfinite(pose->camera_position.z) &&
+ isfinite(pose->forward.i) && isfinite(pose->forward.j) && isfinite(pose->forward.k);
+}
+
+static boolean vr_render_projectile_pose(long player_index, struct unit_datum *unit,
+ struct projectile_preview_pose *pose)
+{
+ return vr_render_pointing_pose(player_index, unit, &vr_render.view, vr_render.base_yaw, pose);
+}
+
+static boolean vr_render_unarmed_ray(long player_index, long unit_index, struct unit_datum *unit,
+ struct halo_vr_view const *view, real base_yaw, real_point3d *origin, real_vector3d *forward)
+{
+ struct projectile_preview_pose pose;
+ if (unit->unit.player_index != player_index ||
+ TEST_FLAG(unit->object.damage_flags, _object_dead_bit) ||
+ unit_get_weapon_count(unit_index) != 0 || cinematic_in_progress() ||
+ unit_inventory_get_weapon(unit_index, unit->unit.current_weapon_index) != NONE ||
+ !vr_render_pointing_pose(player_index, unit, view, base_yaw, &pose))
+ return FALSE;
+ *origin = pose.camera_position;
+ *forward = pose.forward;
+ return TRUE;
+}
+
+/* A weapon's flashlight marker is posed each render frame. Without a
+weapon, use that same rendered controller camera instead of the biped's
+30 Hz attachment. All outputs are presentation copies, shared by both eyes. */
+boolean vr_render_player_flashlight_pose(long unit_index, real_point3d *position,
+ real_vector3d *forward, real_vector3d *up)
+{
+ long player_index = local_player_get_player_index(0);
+ struct unit_datum *unit;
+ real_point3d ignored_origin, source;
+ real_vector3d direction, vertical;
+ real along, length;
+ if (!position || !forward || !up || !vr_render.active ||
+ !vr_render.weapon_camera_set || !vr_render.weapon_camera_valid ||
+ render.local_player_index != 0 || player_index == NONE || unit_index == NONE ||
+ (director_camera_scripted && *director_camera_scripted) ||
+ player_get(player_index)->unit_index != unit_index || !(unit = unit_try_and_get(unit_index)) ||
+ !halo_vr_aiming() || !halo_vr_gaze_tracking_valid() ||
+ !vr_render_unarmed_ray(player_index, unit_index, unit, &vr_render.view, vr_render.base_yaw,
+ &ignored_origin, &direction))
+ return FALSE;
+ source = vr_render.weapon_camera.position;
+ vertical = vr_render.weapon_camera.up;
+ if (!isfinite(source.x) || !isfinite(source.y) || !isfinite(source.z) ||
+ !isfinite(vertical.i) || !isfinite(vertical.j) || !isfinite(vertical.k))
+ return FALSE;
+ /* Preserve controller roll while accommodating the same pitch limits
+ used by the unarmed reticle; never apply body yaw or HMD pitch twice. */
+ along = dot_product3d(&vertical, &direction);
+ vertical.i -= direction.i * along;
+ vertical.j -= direction.j * along;
+ vertical.k -= direction.k * along;
+ length = magnitude3d(&vertical);
+ if (!isfinite(length) || length <= 0.00001f)
+ return FALSE;
+ normalize3d(&vertical);
+ *position = source;
+ *forward = direction;
+ *up = vertical;
+ return TRUE;
+}
+
+/* The tutorial's unarmed calibration panels use the same presentation ray
+as their crosshair. A script query refreshes tracking without changing the
+renderer cache or the simulation's facing/aiming vectors. */
+boolean vr_render_player_pointing_ray(long unit_index, real_point3d *origin, real_vector3d *forward)
+{
+ long player_index = local_player_get_player_index(0);
+ struct unit_datum *unit;
+ struct halo_vr_view view;
+ if (!origin || !forward || player_index == NONE || unit_index == NONE ||
+ player_get(player_index)->unit_index != unit_index || !(unit = unit_try_and_get(unit_index)) ||
+ !halo_vr_aiming() || !halo_vr_view(&view) || !halo_vr_gaze_tracking_valid())
+ return FALSE;
+ return vr_render_unarmed_ray(player_index, unit_index, unit, &view, view.body_yaw, origin, forward);
+}
+
+/* Query the same primary weapon as the HUD, including a vehicle gunner's
+weapon. An unarmed player on foot has the HUD's default crosshair but no
+projectile: place that crosshair along the current bounded pointing pose.
+Unsupported weapon previews retain the ordinary HUD path. */
+static boolean vr_render_projectile_ray(long player_index, real_point3d *origin,
+ real_vector3d *direction, long *owner_object_index)
+{
+ long unit_index = player_get(player_index)->unit_index;
+ struct unit_datum *unit;
+ long weapon_index;
+ struct projectile_preview_pose pose;
+ if (unit_index == NONE || !(unit = unit_try_and_get(unit_index)))
+ return FALSE;
+ weapon_index = unit_inventory_get_weapon(unit_index, unit->unit.current_weapon_index);
+ if (weapon_index == NONE)
+ {
+ long aiming_unit = unit_get_aiming_unit_index(unit_index);
+ if (aiming_unit != NONE && aiming_unit != unit_index)
{
- /* a point far along the controller (or the player's facing, where
- the gamepad aims), where the HUD shows it */
- struct render_frustum frustum;
- real_vector3d forward;
- real_point3d point;
- real_point2d screen;
-
- render_camera_build_frustum(camera, NULL, &frustum, TRUE);
- if (view->gamepad_aim)
- forward = vr_render.aim_forward;
- else
- yaw_rotate(base_yaw, view->hand_forward, &forward);
- point.x = camera->position.x + forward.i * 100.0f;
- point.y = camera->position.y + forward.j * 100.0f;
- point.z = camera->position.z + forward.k * 100.0f;
- if (render_camera_world_to_screen(camera, &frustum, &point, &screen))
- {
- vr_render_crosshair_offset[0] = (short)(screen.x - (camera->viewport_bounds.x0 + camera->viewport_bounds.x1) * 0.5f);
- vr_render_crosshair_offset[1] = (short)(screen.y - (camera->viewport_bounds.y0 + camera->viewport_bounds.y1) * 0.5f);
- }
- else
- {
- /* (off the HUD: not drawn) */
- vr_render_crosshair_offset[0] = 10000;
- }
+ struct unit_datum *parent = unit_try_and_get(aiming_unit);
+ if (parent && parent->unit.gunner_object_index == unit_index)
+ weapon_index = unit_inventory_get_weapon(aiming_unit, parent->unit.current_weapon_index);
}
}
- return TRUE;
+ if (weapon_index == NONE)
+ {
+ if (!vr_render_unarmed_ray(player_index, unit_index, unit, &vr_render.view, vr_render.base_yaw,
+ origin, direction))
+ return FALSE;
+ *owner_object_index = unit_index;
+ return TRUE;
+ }
+ return weapon_preview_projectile_ray(weapon_index, 0, player_index, origin, direction, owner_object_index,
+ vr_render_projectile_pose(player_index, unit, &pose) ? &pose : NULL);
+}
+
+/* One query per frame, before projecting into both eyes. The UI context
+also leaves room for aim assist and its nested line-of-sight collision user. */
+static boolean vr_render_projectile_point(long player_index, real_point3d *point)
+{
+ real_point3d origin;
+ real_vector3d direction, vector;
+ struct collision_result collision;
+ long owner;
+ if (global_current_collision_user_depth >= MAXIMUM_COLLISION_USER_STACK_DEPTH - 2)
+ return FALSE;
+ global_current_collision_users[global_current_collision_user_depth++] = _collision_user_ui;
+ if (!vr_render_projectile_ray(player_index, &origin, &direction, &owner))
+ {
+ --global_current_collision_user_depth;
+ return FALSE;
+ }
+ vector.i = direction.i * CROSSHAIR_FARTHEST;
+ vector.j = direction.j * CROSSHAIR_FARTHEST;
+ vector.k = direction.k * CROSSHAIR_FARTHEST;
+ point->x = origin.x + vector.i;
+ point->y = origin.y + vector.j;
+ point->z = origin.z + vector.k;
+ if (collision_test_vector(_collision_test_for_projectiles_flags, &origin, &vector, owner, &collision))
+ *point = collision.point;
+ --global_current_collision_user_depth;
+ return isfinite(point->x) && isfinite(point->y) && isfinite(point->z);
}
/* render.c, at the end of the eye pass: the crosshairs, drawn in each eye
-where the aim meets the world (vr.aim = "gamepad": the facing from the
-player's eye; "controller": the right controller's line), as the
+at the nominal primary projectile's current collision point, as the
first-person weapon is, and as large as in the HUD's layer. In that layer,
in front of the head, they were placed for the head's pose as the frame was
drawn, which the compositor moved them with to the pose shown while it
@@ -621,68 +1363,48 @@ moved, and at the layer's distance the eyes saw them doubled against what
they aimed at. Drawn in the screen, about the middle as the game draws
them, then moved to the point in each eye by a scale and offset (no
rounding to the HUD's pixels). */
+/* The menu and its pointing cursor occupy the HUD layer on their own.
+ * Read the current widget state, rather than the previous input poll's
+ * menu flag, so the first paused frame cannot retain a gameplay reticle. */
+boolean vr_render_gameplay_hud_visible(short local_player_index)
+{
+ return !halo_vr_frame_active() || local_player_index < 0 ||
+ local_player_index >= MAXIMUM_NUMBER_OF_LOCAL_PLAYERS ||
+ !ui_widgets_active_for_local_player(local_player_index);
+}
+
+/* Controller mode owns a world-space reticle. If its pose or projection
+is unavailable (including a valid aim behind the head), the ordinary HUD
+centre is not an aim fallback. This remains effective after vr_render_end,
+when the HUD is drawn, while leaving every other HUD element unchanged. */
+boolean vr_render_hud_crosshairs_visible(short local_player_index)
+{
+ if (!halo_vr_frame_active() || local_player_index != 0)
+ return TRUE;
+ if (!vr_render_gameplay_hud_visible(local_player_index))
+ return FALSE;
+ return !vr_render.view.controller_aim_requested || vr_render.view.gamepad_aim ||
+ vr_render.crosshairs_drawing;
+}
+
void vr_render_crosshairs(void)
{
struct halo_vr_view *view = &vr_render.view;
- real_point3d origin, point;
- real_vector3d direction, vector;
- struct collision_result collision;
+ real_point3d point;
long player_index = local_player_get_player_index(0);
- long ignore_index = NONE;
- real distance = CROSSHAIR_FARTHEST, width, height, aspect, centre_x, centre_y;
+ real width, height, aspect, centre_x, centre_y;
float corrections[2][4];
short eye, i;
- if (!vr_render.active || !halo_vr_aiming() || cinematic_in_progress() || view->hud_tangent <= 0.0f ||
- !(view->gamepad_aim || view->controller_aim) || player_index == NONE)
+ if (!vr_render.active || !vr_render_gameplay_hud_visible(0) ||
+ !halo_vr_aiming() || cinematic_in_progress() || view->hud_tangent <= 0.0f ||
+ !(view->gamepad_aim || view->controller_aim) || player_index == NONE ||
+ (view->controller_aim_requested && !view->gamepad_aim && !halo_vr_presentation_tracking_valid()))
{
- vr_render.crosshair_distance_valid = FALSE;
return;
}
- if (view->gamepad_aim)
- {
- origin = vr_render.base_position;
- direction = vr_render.aim_forward;
- }
- else
- {
- hand_point(view->hand_position, &origin);
- yaw_rotate(vr_render.base_yaw, view->hand_forward, &direction);
- }
- if (normalize3d(&direction) <= 0.0f)
+ if (!vr_render_projectile_point(player_index, &point))
return;
- if (player_get(player_index)->unit_index != NONE)
- ignore_index = object_get_ultimate_parent(player_get(player_index)->unit_index);
- vector.i = direction.i * CROSSHAIR_FARTHEST;
- vector.j = direction.j * CROSSHAIR_FARTHEST;
- vector.k = direction.k * CROSSHAIR_FARTHEST;
- if (global_current_collision_user_depth < MAXIMUM_COLLISION_USER_STACK_DEPTH)
- {
- global_current_collision_users[global_current_collision_user_depth++] = _collision_user_ui;
- if (collision_test_vector(_collision_test_for_line_of_sight_flags | FLAG(_collision_test_objects_bipeds_bit),
- &origin, &vector, ignore_index, &collision))
- {
- distance = (real)sqrt(distance_squared3d(&origin, &collision.point));
- }
- --global_current_collision_user_depth;
- }
- if (distance < CROSSHAIR_NEAREST)
- distance = CROSSHAIR_NEAREST;
- if (!vr_render.crosshair_distance_valid)
- {
- vr_render.crosshair_inverse_distance = 1.0f / distance;
- vr_render.crosshair_distance_valid = TRUE;
- }
- else
- {
- real t = view->seconds > 0.0f ? 1.0f - (real)exp(-view->seconds / CROSSHAIR_SMOOTHING) : 1.0f;
-
- vr_render.crosshair_inverse_distance += (1.0f / distance - vr_render.crosshair_inverse_distance) * t;
- }
- distance = 1.0f / vr_render.crosshair_inverse_distance;
- point.x = origin.x + direction.i * distance;
- point.y = origin.y + direction.j * distance;
- point.z = origin.z + direction.k * distance;
/* the game's screen (the viewport, its pixels: x = ((ndc + 1) width -
1) / 2, y from the top), and its middle, where the game draws them */
@@ -721,11 +1443,13 @@ void vr_render_crosshairs(void)
vr_render_crosshair_offset[0] = vr_render_crosshair_offset[1] = 0;
halo_vr_set_screen_corrections(corrections);
/* (each eye in a draw of its own: halo_vr_draw_eye) */
+ vr_render.crosshairs_drawing = TRUE;
for (eye = 0; eye < 2; eye++)
{
halo_vr_draw_eye(eye);
hud_draw_vr_crosshairs();
}
+ vr_render.crosshairs_drawing = FALSE;
halo_vr_draw_eye(-1);
vr_render_screen_point_end();
vr_render.crosshairs_drawn = TRUE;
diff --git a/port/linux/src/d3d8_gl.c b/port/linux/src/d3d8_gl.c
index 304179be60aeebea6a7f1ed3c297fed82700db1f..ae94cd2e4c6af9dff0775b1f9095ba555e197cff 100644
--- a/port/linux/src/d3d8_gl.c
+++ b/port/linux/src/d3d8_gl.c
@@ -23,6 +23,9 @@ Conventions carried over from the Xbox:
#include "xgpu.h"
#include "sdl_platform.h"
+#if defined(HALO_GLES) || defined(HALO_ARM64_GUEST)
+#include <SDL3/SDL_timer.h>
+#endif
#ifdef HALO_VR
#include <SDL3/SDL.h>
#endif
@@ -316,6 +319,9 @@ the unsynchronized write the ring allows (host_gl_buffer_write). */
#define STREAM_BUFFER_SIZE (16 * 1024 * 1024)
#define INDEX_BUFFER_SIZE (2 * 1024 * 1024)
#define STREAM_BUFFER_RING 3
+#if defined(HALO_ARM64_GUEST) && !defined(HALO_GLES)
+#define XGPU_PERSISTENT_STREAMS
+#endif
#else
#define STREAM_BUFFER_SIZE (32 * 1024 * 1024)
#define INDEX_BUFFER_SIZE (8 * 1024 * 1024)
@@ -379,10 +385,22 @@ struct gl_device
GLuint stream_buffers[STREAM_BUFFER_RING];
GLuint index_buffers[STREAM_BUFFER_RING];
unsigned long buffer_ring;
+#ifdef XGPU_PERSISTENT_STREAMS
+ /* Base mappings are reused only after the slot fence. Overflow goes
+ to mutable spills; those mappings are never rewound within a frame. */
+ BOOL persistent_requested;
+ BOOL persistent_ring[STREAM_BUFFER_RING];
+ BOOL stream_persistent, index_persistent;
+ GLuint stream_spills[STREAM_BUFFER_RING];
+ GLuint index_spills[STREAM_BUFFER_RING];
+ BOOL ring_wait_warned[STREAM_BUFFER_RING];
+#endif
#endif
unsigned long stream_offset;
+ unsigned long stream_capacity;
GLuint index_buffer;
unsigned long index_offset;
+ unsigned long index_capacity;
GLuint samplers[D3DTSS_MAXSTAGES];
GLuint queries[VISIBILITY_TEST_SLOTS];
@@ -395,6 +413,11 @@ struct gl_device
#ifdef XGPU_SAMPLE_COUNTERS
/* with atomic counters: one counter per test, used as a ring */
GLuint visibility_counters;
+#ifdef XGPU_PERSISTENT_STREAMS
+ /* A constant source lets the GPU reset a counter without a CPU mapping
+ that could overwrite an earlier queued test when the counter ring wraps. */
+ GLuint visibility_zero_buffer;
+#endif
unsigned long counter_next;
unsigned long counter_active;
/* Reading the counters waits for the draws that counted, which stops
@@ -474,11 +497,14 @@ static struct
present) */
struct render_target_entry eye_target;
BOOL eyes_drawn, hud_drawn;
+ /* No compositor image may be submitted after a failed target bind. */
+ BOOL frame_failed;
/* row-major (vr.h), and the screen's points' (vr.h); the serial counts
their changes */
float eye_transforms[2][16];
float screen_corrections[2][4];
BOOL screen_points;
+ BOOL screen_space;
unsigned long eye_serial;
/* the bound framebuffer draws both eyes (bind_targets), or one eye's
layer alone: in the eye pass, a visibility test is drawn into the left
@@ -543,6 +569,9 @@ static struct
unsigned long target_changes;
/* vertex and index bytes drawn from the mirror, and streamed */
unsigned long mirrored_bytes, streamed_bytes;
+ unsigned long persistent_writes, mutable_writes, persistent_fallback_writes;
+ unsigned long stream_overflows, index_overflows, stream_spills, index_spills, ring_wait_failures;
+ double ring_wait_milliseconds, ring_wait_worst;
} stats;
static D3DDevice *device_pointer(void)
@@ -1026,17 +1055,26 @@ static struct render_target_entry *render_target_get(const D3DSurface *surface)
/* (the entries are never freed) */
screen = halo_screen_width();
+ surface_dimensions(surface, &width, &height, &depth);
+#ifdef HALO_VR
+ if (width == (unsigned long)screen && height == SCREEN_HEIGHT && vr_gl.pass == HALO_VR_PASS_EYES)
+ {
+ if (!depth && surface->Data == device.back_buffer.Data)
+ return &vr_gl.eye_target;
+ layers = 2;
+ }
+#endif
for (slot = 0; slot < RECENT_RENDER_TARGET_COUNT; slot++)
{
if (recent_render_targets[slot].entry && recent_render_targets[slot].data == surface->Data &&
recent_render_targets[slot].format == surface->Format && recent_render_targets[slot].size == surface->Size &&
recent_render_targets[slot].screen_width == screen && recent_render_targets[slot].scale[0] == screen_scale[0] &&
- recent_render_targets[slot].scale[1] == screen_scale[1])
+ recent_render_targets[slot].scale[1] == screen_scale[1] &&
+ recent_render_targets[slot].entry->target.layers == layers)
{
return recent_render_targets[slot].entry;
}
}
- surface_dimensions(surface, &width, &height, &depth);
/* the screen's targets are drawn at the screen's scale */
if (width == (unsigned long)halo_screen_width() && height == SCREEN_HEIGHT)
{
@@ -1121,7 +1159,8 @@ struct xgpu_render_target *xgpu_render_target_find(unsigned long data)
#ifdef HALO_VR
&& (vr_gl.pass == HALO_VR_PASS_NONE || entry->target.width != (unsigned long)halo_screen_width() ||
entry->target.height != SCREEN_HEIGHT ||
- (entry->target.layers > 1) == (vr_gl.pass == HALO_VR_PASS_EYES))
+ ((entry->target.layers > 1) == (vr_gl.pass == HALO_VR_PASS_EYES) &&
+ entry->target.scale[0] == screen_scale[0] && entry->target.scale[1] == screen_scale[1]))
#endif
)
best = entry;
@@ -1183,7 +1222,20 @@ static GLuint framebuffer_get(GLuint color, GLuint depth)
}
glDrawBuffers(1, &draw_buffer);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
+ {
platform_log("framebuffer %u/%u is incomplete", color, depth);
+ /* An incomplete attachment set must never enter the cache or be
+ used as framebuffer zero by a caller. A later frame can retry it. */
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ glDeleteFramebuffers(1, &entry->framebuffer);
+ free(entry);
+#ifdef HALO_VR
+ if (vr_gl.pass != HALO_VR_PASS_NONE)
+ vr_gl.frame_failed = TRUE;
+#endif
+ xgpu_gl_state_invalidate();
+ return 0;
+ }
xgpu_gl_state_invalidate();
entry->next = framebuffers;
framebuffers = entry;
@@ -1231,6 +1283,7 @@ static void vr_frame_resume(void);
static BOOL bind_targets(BOOL *has_depth)
{
struct render_target_entry *color, *depth;
+ GLuint framebuffer;
#ifdef HALO_VR
vr_frame_resume();
@@ -1242,8 +1295,6 @@ static BOOL bind_targets(BOOL *has_depth)
depth = NULL;
if (!color && !depth)
return FALSE;
- if (color)
- color->last_rendered = device.frame + 1;
/* viewports and clears are in the targets' units (render_target_get) */
target_scale[0] = color ? color->target.scale[0] : depth->target.scale[0];
target_scale[1] = color ? color->target.scale[1] : depth->target.scale[1];
@@ -1258,15 +1309,25 @@ static BOOL bind_targets(BOOL *has_depth)
if (vr_gl.right_eye)
framebuffer_views = -2;
vr_gl.multiview = framebuffer_views > 1;
- if (vr_gl.multiview || vr_gl.left_eye || vr_gl.right_eye)
- vr_gl.eyes_drawn = TRUE;
- if (vr_gl.pass == HALO_VR_PASS_HUD && color && color->target.data == device.back_buffer.Data)
- vr_gl.hud_drawn = TRUE;
target_height = color ? color->target.gl_height : depth->target.gl_height;
#endif
- state_framebuffer(framebuffer_get(color ? color->target.texture : 0, depth ? depth->target.texture : 0));
+ framebuffer = framebuffer_get(color ? color->target.texture : 0, depth ? depth->target.texture : 0);
#ifdef HALO_VR
framebuffer_views = 1;
+#endif
+ if (!framebuffer)
+ {
+ *has_depth = FALSE;
+ return FALSE;
+ }
+ state_framebuffer(framebuffer);
+ if (color)
+ color->last_rendered = device.frame + 1;
+#ifdef HALO_VR
+ if (vr_gl.multiview || vr_gl.left_eye || vr_gl.right_eye)
+ vr_gl.eyes_drawn = TRUE;
+ if (vr_gl.pass == HALO_VR_PASS_HUD && color && color->target.data == device.back_buffer.Data)
+ vr_gl.hud_drawn = TRUE;
#endif
*has_depth = depth != NULL;
return TRUE;
@@ -1275,6 +1336,32 @@ static BOOL bind_targets(BOOL *has_depth)
/* ---------- device creation */
#ifdef XGPU_SAMPLE_COUNTERS
+#ifdef XGPU_PERSISTENT_STREAMS
+static void visibility_zero_source_create(void)
+{
+ const GLuint zero = 0;
+ GLuint buffer = 0;
+
+ if (device.visibility_zero_buffer)
+ return;
+ glGenBuffers(1, &buffer);
+ if (!buffer)
+ return;
+ glBindBuffer(GL_COPY_READ_BUFFER, buffer);
+ glBufferData(GL_COPY_READ_BUFFER, sizeof(zero), &zero, GL_STATIC_DRAW);
+ glBindBuffer(GL_COPY_READ_BUFFER, 0);
+ if (glGetError() != GL_NO_ERROR)
+ {
+ glDeleteBuffers(1, &buffer);
+ return;
+ }
+ /* Commit only a successfully allocated source. On allocation failure,
+ BeginVisibilityTest uses ordered BufferSubData instead. The source lives
+ with the device's other buffers until its GL context is destroyed. */
+ device.visibility_zero_buffer = buffer;
+}
+#endif
+
/* the visibility tests' counters and their snapshots (with
SAMPLE_COUNTING) */
static void sample_counters_create(void)
@@ -1294,6 +1381,9 @@ static void sample_counters_create(void)
glBufferData(GL_COPY_WRITE_BUFFER, VISIBILITY_TEST_SLOTS * sizeof(GLuint), NULL, GL_STREAM_READ);
}
glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
+#ifdef XGPU_PERSISTENT_STREAMS
+ visibility_zero_source_create();
+#endif
xgpu_gl_state_invalidate();
}
#endif
@@ -1302,6 +1392,69 @@ static void sample_counters_create(void)
static void vr_gl_initialize(void);
#endif
+#ifdef XGPU_STREAM_RING
+static void stream_buffers_initialize(void)
+{
+ int ring;
+#ifdef XGPU_PERSISTENT_STREAMS
+ BOOL mapped = TRUE;
+
+ device.persistent_requested = config_boolean("display.persistent_stream_buffers");
+ if (device.persistent_requested)
+ {
+ /* Commit all six mappings together, before any draw uses them.
+ Failed immutable allocations never enter the mutable upload path. */
+ for (ring = 0; ring < STREAM_BUFFER_RING; ring++)
+ {
+ device.stream_buffers[ring] = host_gl_buffer_create_persistent(STREAM_BUFFER_SIZE);
+ device.index_buffers[ring] = host_gl_buffer_create_persistent(INDEX_BUFFER_SIZE);
+ if (!device.stream_buffers[ring] || !device.index_buffers[ring])
+ {
+ mapped = FALSE;
+ break;
+ }
+ }
+ if (!mapped)
+ {
+ for (ring = 0; ring < STREAM_BUFFER_RING; ring++)
+ {
+ host_gl_buffer_destroy_persistent(device.stream_buffers[ring]);
+ host_gl_buffer_destroy_persistent(device.index_buffers[ring]);
+ device.stream_buffers[ring] = device.index_buffers[ring] = 0;
+ }
+ }
+ else
+ {
+ for (ring = 0; ring < STREAM_BUFFER_RING; ring++)
+ device.persistent_ring[ring] = TRUE;
+ device.stream_persistent = device.index_persistent = TRUE;
+ }
+ }
+ if (!device.persistent_requested || !mapped)
+#endif
+ {
+ glGenBuffers(STREAM_BUFFER_RING, device.stream_buffers);
+ glGenBuffers(STREAM_BUFFER_RING, device.index_buffers);
+ for (ring = 0; ring < STREAM_BUFFER_RING; ring++)
+ {
+ glBindBuffer(GL_ARRAY_BUFFER, device.stream_buffers[ring]);
+ glBufferData(GL_ARRAY_BUFFER, STREAM_BUFFER_SIZE, NULL, GL_STREAM_DRAW);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, device.index_buffers[ring]);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_BUFFER_SIZE, NULL, GL_STREAM_DRAW);
+ }
+ }
+ device.stream_buffer = device.stream_buffers[0];
+ device.index_buffer = device.index_buffers[0];
+ device.stream_capacity = STREAM_BUFFER_SIZE;
+ device.index_capacity = INDEX_BUFFER_SIZE;
+#ifdef XGPU_PERSISTENT_STREAMS
+ platform_log("stream buffers: persistent %s (%d fenced slots; mutable overflow spills)",
+ device.stream_persistent ? "enabled" : device.persistent_requested ? "unavailable; mutable fallback" : "disabled",
+ STREAM_BUFFER_RING);
+#endif
+}
+#endif
+
static void gl_initialize(void)
{
GLint major = 0, minor = 0;
@@ -1349,21 +1502,7 @@ static void gl_initialize(void)
glGenVertexArrays(1, &device.vertex_array);
glBindVertexArray(device.vertex_array);
#ifdef XGPU_STREAM_RING
- {
- int ring;
-
- glGenBuffers(STREAM_BUFFER_RING, device.stream_buffers);
- glGenBuffers(STREAM_BUFFER_RING, device.index_buffers);
- for (ring = 0; ring < STREAM_BUFFER_RING; ring++)
- {
- glBindBuffer(GL_ARRAY_BUFFER, device.stream_buffers[ring]);
- glBufferData(GL_ARRAY_BUFFER, STREAM_BUFFER_SIZE, NULL, GL_STREAM_DRAW);
- glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, device.index_buffers[ring]);
- glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_BUFFER_SIZE, NULL, GL_STREAM_DRAW);
- }
- device.stream_buffer = device.stream_buffers[0];
- device.index_buffer = device.index_buffers[0];
- }
+ stream_buffers_initialize();
#endif
#ifndef XGPU_STREAM_RING
glGenBuffers(1, &device.stream_buffer);
@@ -1372,6 +1511,8 @@ static void gl_initialize(void)
glGenBuffers(1, &device.index_buffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, device.index_buffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_BUFFER_SIZE, NULL, GL_STREAM_DRAW);
+ device.stream_capacity = STREAM_BUFFER_SIZE;
+ device.index_capacity = INDEX_BUFFER_SIZE;
#endif
glGenSamplers(D3DTSS_MAXSTAGES, device.samplers);
glGenQueries(VISIBILITY_TEST_SLOTS, device.queries);
@@ -1634,6 +1775,8 @@ int halo_ui_pointer_update(int menus_active, struct halo_ui_pointer *pointer)
platform_ui_pointer_set_active(menus_active != 0);
#ifdef HALO_VR
halo_vr_menus(menus_active != 0);
+ if (halo_vr_menu_pointer(menus_active, pointer))
+ return menus_active && device.gl_ready;
#endif
if (!menus_active || !device.gl_ready || !platform_ui_pointer_read(&state))
return 0;
@@ -1828,10 +1971,34 @@ void WINAPI D3DDevice_BeginVisibilityTest(void)
device.counter_next = (device.counter_next + 1) % VISIBILITY_TEST_SLOTS;
device.counter_active = device.counter_next;
+#ifdef XGPU_PERSISTENT_STREAMS
+ /* Shader counter writes must finish before an API update/copy of
+ the same range. The copy is GPU ordered, even when the frame fence
+ timed out; no unsynchronized CPU write reaches a pending counter. */
+ glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
+ if (device.visibility_zero_buffer)
+ {
+ glBindBuffer(GL_COPY_READ_BUFFER, device.visibility_zero_buffer);
+ glBindBuffer(GL_COPY_WRITE_BUFFER, device.visibility_counters);
+ glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0,
+ (GLintptr)(device.counter_active * sizeof(GLuint)), sizeof(zero));
+ glBindBuffer(GL_COPY_READ_BUFFER, 0);
+ glBindBuffer(GL_COPY_WRITE_BUFFER, 0);
+ }
+ else
+ {
+ glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, device.visibility_counters);
+ glBufferSubData(GL_ATOMIC_COUNTER_BUFFER,
+ (GLintptr)(device.counter_active * sizeof(GLuint)), sizeof(zero), &zero);
+ glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
+ }
+ glMemoryBarrier(GL_ATOMIC_COUNTER_BARRIER_BIT);
+#else
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, device.visibility_counters);
host_gl_buffer_write(GL_ATOMIC_COUNTER_BUFFER, (unsigned int)(device.counter_active * sizeof(GLuint)),
sizeof(zero), &zero);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);
+#endif
return;
}
#endif
@@ -2585,10 +2752,14 @@ static GLuint framebuffer_get(GLuint color, GLuint depth);
static void copy_level_by_blit(GLuint source, GLuint destination, GLint level, GLsizei width, GLsizei height)
{
static GLuint draw_framebuffer;
+ GLuint read_framebuffer = framebuffer_get(source, 0);
+
+ if (!read_framebuffer)
+ return;
if (!draw_framebuffer)
glGenFramebuffers(1, &draw_framebuffer);
- glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer_get(source, 0));
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, read_framebuffer);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, draw_framebuffer);
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, destination, level);
glDisable(GL_SCISSOR_TEST);
@@ -3074,6 +3245,9 @@ static struct program_entry *prepare_draw(BOOL immediate)
#ifdef HALO_VR
key.multiview = vr_gl.multiview ? XGPU_MULTIVIEW_EYES : vr_gl.left_eye ? XGPU_MULTIVIEW_LEFT_EYE :
vr_gl.right_eye ? XGPU_MULTIVIEW_RIGHT_EYE : XGPU_MULTIVIEW_NONE;
+ /* Copying an eye into a mono scratch target still samples that eye;
+ the scratch geometry itself remains a normal single-view draw. */
+ key.sample_eye = vr_gl.eye_alone == 1 ? 1 : 0;
entry = program_get(vertex_shader_get(program, immediate, key.multiview), fragment_shader_get(&key));
if (vr_gl.multiview && vr_gl.statistics && !program->projects)
{
@@ -3122,7 +3296,7 @@ static struct program_entry *prepare_draw(BOOL immediate)
if (entry->screen_correction >= 0)
glUniform4fv(entry->screen_correction, 2, &vr_gl.screen_corrections[0][0]);
if (entry->screen_points >= 0)
- glUniform1f(entry->screen_points, vr_gl.screen_points ? 1.0f : 0.0f);
+ glUniform1f(entry->screen_points, vr_gl.screen_points || vr_gl.screen_space ? 1.0f : 0.0f);
entry->eye_serial = vr_gl.eye_serial;
}
#endif
@@ -3650,35 +3824,65 @@ static void index_extent(const WORD *indices, unsigned long count, unsigned long
/* ---------- vertex data */
-/* makes room for size bytes of uploads, orphaning the stream buffer if it
-is full. A draw reserves room for all of its streams at once: orphaning
-between two of them would leave the attributes already pointed at the
-buffer reading its new, empty storage. */
+/* Reserve every stream of a draw together. A full mapped page stays intact
+until its frame fence: switch to a mutable spill, never glFinish or rewind
+the mapping. Only mutable storage can then be orphaned. A single large draw
+gets enough storage rather than writing beyond a fixed-size page. */
static void stream_reserve(unsigned long size)
{
- if (device.stream_offset + size > STREAM_BUFFER_SIZE)
+ if (device.stream_offset > device.stream_capacity || size > device.stream_capacity - device.stream_offset)
{
- /* orphan the buffer and start again */
+ stats.stream_overflows++;
+#ifdef XGPU_PERSISTENT_STREAMS
+ if (device.stream_persistent)
+ {
+ GLuint *spill = &device.stream_spills[device.buffer_ring];
+
+ if (!*spill)
+ glGenBuffers(1, spill);
+ device.stream_buffer = *spill;
+ device.stream_persistent = FALSE;
+ stats.stream_spills++;
+ }
+#endif
state_array_buffer(device.stream_buffer);
- glBufferData(GL_ARRAY_BUFFER, STREAM_BUFFER_SIZE, NULL, GL_STREAM_DRAW);
+ device.stream_capacity = size > STREAM_BUFFER_SIZE ? size : STREAM_BUFFER_SIZE;
+ glBufferData(GL_ARRAY_BUFFER, device.stream_capacity, NULL, GL_STREAM_DRAW);
device.stream_offset = 0;
}
}
static unsigned long stream_upload(const void *data, unsigned long size)
{
- unsigned long offset;
+ unsigned long offset, reserved = (size + 15) & ~15UL;
- size = (size + 15) & ~15UL;
- stream_reserve(size);
+ stream_reserve(reserved);
offset = device.stream_offset;
state_array_buffer(device.stream_buffer);
#ifdef XGPU_STREAM_RING
- host_gl_buffer_write(GL_ARRAY_BUFFER, (unsigned int)offset, (unsigned int)size, data);
+#ifdef XGPU_PERSISTENT_STREAMS
+ if (device.stream_persistent)
+ {
+ if (!host_gl_buffer_write_persistent(device.stream_buffer, (unsigned int)offset, (unsigned int)size, data))
+ {
+ /* Mapped pages also have DYNAMIC_STORAGE, so this is legal;
+ never attempt a second mapping or orphan immutable storage. */
+ glBufferSubData(GL_ARRAY_BUFFER, offset, size, data);
+ stats.persistent_fallback_writes++;
+ }
+ stats.persistent_writes++;
+ }
+ else
+#endif
+ {
+ host_gl_buffer_write(GL_ARRAY_BUFFER, (unsigned int)offset, (unsigned int)size, data);
+ stats.mutable_writes++;
+ }
#else
buffer_upload(GL_ARRAY_BUFFER, offset, size, data);
#endif
- device.stream_offset += size;
+ /* Alignment reserves padding, but does not read past the source. */
+ device.stream_offset += reserved;
return offset;
}
@@ -3724,24 +3928,58 @@ static unsigned long stream_upload_swizzled(const struct vertex_shader_object *d
}
#endif
-static unsigned long index_upload(const void *data, unsigned long size)
+static void index_reserve(unsigned long size)
{
- unsigned long offset;
-
- size = (size + 15) & ~15UL;
- state_element_array_buffer(device.index_buffer);
- if (device.index_offset + size > INDEX_BUFFER_SIZE)
+ if (device.index_offset > device.index_capacity || size > device.index_capacity - device.index_offset)
{
- glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_BUFFER_SIZE, NULL, GL_STREAM_DRAW);
+ stats.index_overflows++;
+#ifdef XGPU_PERSISTENT_STREAMS
+ if (device.index_persistent)
+ {
+ GLuint *spill = &device.index_spills[device.buffer_ring];
+
+ if (!*spill)
+ glGenBuffers(1, spill);
+ device.index_buffer = *spill;
+ device.index_persistent = FALSE;
+ stats.index_spills++;
+ }
+#endif
+ state_element_array_buffer(device.index_buffer);
+ device.index_capacity = size > INDEX_BUFFER_SIZE ? size : INDEX_BUFFER_SIZE;
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, device.index_capacity, NULL, GL_STREAM_DRAW);
device.index_offset = 0;
}
+}
+
+static unsigned long index_upload(const void *data, unsigned long size)
+{
+ unsigned long offset, reserved = (size + 15) & ~15UL;
+
+ index_reserve(reserved);
offset = device.index_offset;
+ state_element_array_buffer(device.index_buffer);
#ifdef XGPU_STREAM_RING
- host_gl_buffer_write(GL_ELEMENT_ARRAY_BUFFER, (unsigned int)offset, (unsigned int)size, data);
+#ifdef XGPU_PERSISTENT_STREAMS
+ if (device.index_persistent)
+ {
+ if (!host_gl_buffer_write_persistent(device.index_buffer, (unsigned int)offset, (unsigned int)size, data))
+ {
+ glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, offset, size, data);
+ stats.persistent_fallback_writes++;
+ }
+ stats.persistent_writes++;
+ }
+ else
+#endif
+ {
+ host_gl_buffer_write(GL_ELEMENT_ARRAY_BUFFER, (unsigned int)offset, (unsigned int)size, data);
+ stats.mutable_writes++;
+ }
#else
buffer_upload(GL_ELEMENT_ARRAY_BUFFER, offset, size, data);
#endif
- device.index_offset += size;
+ device.index_offset += reserved;
return offset;
}
@@ -4237,7 +4475,14 @@ static unsigned char *read_target(GLuint texture, int layer, unsigned long width
if (layer < 0)
{
- glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer_get(texture, 0));
+ GLuint framebuffer = framebuffer_get(texture, 0);
+
+ if (!framebuffer)
+ {
+ memset(pixels, 0, width * height * 4);
+ return pixels;
+ }
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
}
else
{
@@ -4400,6 +4645,7 @@ void halo_vr_pass(int pass)
else if (pass == HALO_VR_PASS_HUD && vr_gl.pass == HALO_VR_PASS_EYES)
vr_timing(2);
vr_gl.pass = pass;
+ halo_vr_screen_space(0);
vr_gl.multiview = FALSE;
vr_gl.left_eye = FALSE;
vr_gl.right_eye = FALSE;
@@ -4413,13 +4659,18 @@ void halo_vr_pass(int pass)
if (hud)
{
- glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_get(hud->target.texture, 0));
- glDisable(GL_SCISSOR_TEST);
- glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
- glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
- glClear(GL_COLOR_BUFFER_BIT);
- hud->last_rendered = device.frame + 1;
- vr_gl.hud_drawn = TRUE;
+ GLuint framebuffer = framebuffer_get(hud->target.texture, 0);
+
+ if (framebuffer)
+ {
+ glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
+ glDisable(GL_SCISSOR_TEST);
+ glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+ glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+ hud->last_rendered = device.frame + 1;
+ vr_gl.hud_drawn = TRUE;
+ }
}
}
xgpu_gl_state_invalidate();
@@ -4458,6 +4709,16 @@ void halo_vr_draw_eye(int eye)
vr_gl.eye_alone = eye;
}
+void halo_vr_screen_space(int active)
+{
+ BOOL screen_space = active != 0;
+ if (vr_gl.screen_space != screen_space)
+ {
+ vr_gl.screen_space = screen_space;
+ vr_gl.eye_serial++;
+ }
+}
+
void halo_vr_set_screen_corrections(const float corrections[2][4])
{
static const float none[2][4] = { { 1.0f, 1.0f, 0.0f, 0.0f }, { 1.0f, 1.0f, 0.0f, 0.0f } };
@@ -4530,6 +4791,7 @@ is then premultiplied, as the compositor's default blending expects).
FALSE if the copy's program cannot be made. */
static BOOL vr_copy_hud(struct render_target_entry *hud, GLuint image)
{
+ GLuint framebuffer;
static GLuint program;
static BOOL failed;
static GLint size_location;
@@ -4575,7 +4837,10 @@ static BOOL vr_copy_hud(struct render_target_entry *hud, GLuint image)
glUniform1i(glGetUniformLocation(program, "hud_texture"), 0);
size_location = glGetUniformLocation(program, "size");
}
- glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_get(image, 0));
+ framebuffer = framebuffer_get(image, 0);
+ if (!framebuffer)
+ return FALSE;
+ glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glViewport(0, 0, vr_gl.hud_width, vr_gl.hud_height);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_BLEND);
@@ -4625,7 +4890,7 @@ static void vr_copy_depth(void)
" gl_FragDepth = texture(depth_texture, vec3(uv, float(gl_ViewID_OVR))).r;\n"
"}\n";
struct render_target_entry *depth;
- GLuint image;
+ GLuint image, framebuffer;
int pass = vr_gl.pass;
float scale[2];
@@ -4664,8 +4929,11 @@ static void vr_copy_depth(void)
if (vr_gl.depth_failed)
return;
framebuffer_views = 2;
- glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_get(0, image));
+ framebuffer = framebuffer_get(0, image);
framebuffer_views = 1;
+ if (!framebuffer)
+ return;
+ glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glViewport(0, 0, vr_gl.eye_width, vr_gl.eye_height);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_BLEND);
@@ -4695,8 +4963,9 @@ static void vr_present(BOOL screenshot)
struct render_target_entry *hud = NULL;
vr_timing(3);
- vr_copy_depth();
- if (vr_gl.hud_drawn)
+ if (!vr_gl.frame_failed)
+ vr_copy_depth();
+ if (vr_gl.hud_drawn && !vr_gl.frame_failed)
{
int pass = vr_gl.pass;
@@ -4706,7 +4975,7 @@ static void vr_present(BOOL screenshot)
hud = render_target_get(&device.back_buffer);
vr_gl.pass = pass;
}
- if (screenshot)
+ if (screenshot && !vr_gl.frame_failed)
vr_write_screenshots(hud);
if (hud)
{
@@ -4738,9 +5007,11 @@ static void vr_present(BOOL screenshot)
vr_gl.gpu_worst = milliseconds;
vr_gl.gpu_frames++;
}
- halo_vr_present(vr_gl.eyes_drawn, hud != NULL);
+ /* Never hand the runtime uninitialized or partially failed eye storage. */
+ halo_vr_present(vr_gl.eyes_drawn && !vr_gl.frame_failed, hud != NULL && !vr_gl.frame_failed);
vr_gl.eyes_drawn = FALSE;
vr_gl.hud_drawn = FALSE;
+ vr_gl.frame_failed = FALSE;
vr_timing(0);
}
@@ -4755,12 +5026,70 @@ static void vr_frame_resume(void)
/* the end of a frame's GL work: the streamed data and visibility results of
the frames the GPU may still be drawing stay as they are */
+#ifdef XGPU_STREAM_RING
+static unsigned long long stream_clock_ns(void)
+{
+#ifdef XGPU_PERSISTENT_STREAMS
+ return SDL_GetTicksNS();
+#else
+ /* The Android guest implements millisecond ticks, not GetTicksNS. */
+ return (unsigned long long)SDL_GetTicks() * 1000000ull;
+#endif
+}
+
+static void stream_ring_reset(int ready)
+{
+ unsigned long ring = device.buffer_ring;
+#ifdef XGPU_PERSISTENT_STREAMS
+ if (!ready)
+ {
+ /* Never overwrite a mapping whose fence is unfinished. Retain the
+ original objects until context teardown (at most six) and make this
+ slot mutable. Orphaning keeps pending draws' storage in the driver. */
+ if (device.persistent_ring[ring])
+ {
+ glGenBuffers(1, &device.stream_buffers[ring]);
+ glGenBuffers(1, &device.index_buffers[ring]);
+ device.persistent_ring[ring] = FALSE;
+ }
+ glBindBuffer(GL_ARRAY_BUFFER, device.stream_buffers[ring]);
+ glBufferData(GL_ARRAY_BUFFER, STREAM_BUFFER_SIZE, NULL, GL_STREAM_DRAW);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, device.index_buffers[ring]);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_BUFFER_SIZE, NULL, GL_STREAM_DRAW);
+ xgpu_gl_state_invalidate();
+ stats.ring_wait_failures++;
+ /* A repeatedly late GPU must not turn recovery into per-frame logging. */
+ if (!device.ring_wait_warned[ring])
+ {
+ platform_log("stream buffers: unfinished slot %lu; fresh mutable storage, visibility snapshot skipped", ring);
+ device.ring_wait_warned[ring] = TRUE;
+ }
+ }
+ device.stream_persistent = device.index_persistent = device.persistent_ring[ring];
+#else
+ (void)ready;
+#endif
+ device.stream_buffer = device.stream_buffers[ring];
+ device.index_buffer = device.index_buffers[ring];
+ device.stream_capacity = STREAM_BUFFER_SIZE;
+ device.index_capacity = INDEX_BUFFER_SIZE;
+ device.stream_offset = device.index_offset = 0;
+}
+#endif
+
static void frame_end_buffers(void)
{
+#ifdef XGPU_STREAM_RING
+ int ring_ready = 1;
+ unsigned long long wait_started;
+#endif
#ifdef XGPU_SAMPLE_COUNTERS
if (SAMPLE_COUNTING)
{
/* this frame's counts, for when the GPU is done with it */
+#ifdef XGPU_PERSISTENT_STREAMS
+ glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
+#endif
glBindBuffer(GL_COPY_READ_BUFFER, device.visibility_counters);
glBindBuffer(GL_COPY_WRITE_BUFFER, device.counter_snapshots[device.buffer_ring]);
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0,
@@ -4772,7 +5101,22 @@ static void frame_end_buffers(void)
#ifdef XGPU_STREAM_RING
host_gl_fence_frame((unsigned int)device.buffer_ring);
device.buffer_ring = (device.buffer_ring + 1) % STREAM_BUFFER_RING;
+ wait_started = debug_settings.statistics ? stream_clock_ns() : 0;
+#ifdef XGPU_PERSISTENT_STREAMS
+ /* Mutable uploads map unsynchronized ranges too: a timeout must not
+ declare a slot reusable simply because persistent mode is disabled. */
+ ring_ready = host_gl_wait_frame_checked((unsigned int)device.buffer_ring);
+#else
host_gl_wait_frame((unsigned int)device.buffer_ring);
+#endif
+ if (wait_started)
+ {
+ double elapsed = (double)(stream_clock_ns() - wait_started) * 1e-6;
+
+ stats.ring_wait_milliseconds += elapsed;
+ if (elapsed > stats.ring_wait_worst)
+ stats.ring_wait_worst = elapsed;
+ }
#endif
#ifdef XGPU_SAMPLE_COUNTERS
if (SAMPLE_COUNTING && device.ring_test_count[device.buffer_ring])
@@ -4782,27 +5126,31 @@ static void frame_end_buffers(void)
/* the GPU has passed that frame's fence: its copy is complete,
and the slot is free for this frame's tests */
- host_gl_read_buffer(device.counter_snapshots[ring], 0, VISIBILITY_TEST_SLOTS * sizeof(GLuint),
- device.counter_values);
- for (test = 0; test < device.ring_test_count[ring]; test++)
+ if (ring_ready)
{
- device.visibility_latest[device.ring_tests[ring][test][0]] =
- device.counter_values[device.ring_tests[ring][test][1]];
+ host_gl_read_buffer(device.counter_snapshots[ring], 0, VISIBILITY_TEST_SLOTS * sizeof(GLuint),
+ device.counter_values);
+ for (test = 0; test < device.ring_test_count[ring]; test++)
+ {
+ device.visibility_latest[device.ring_tests[ring][test][0]] =
+ device.counter_values[device.ring_tests[ring][test][1]];
+ }
}
device.ring_test_count[ring] = 0;
}
#endif
#ifdef XGPU_STREAM_RING
- device.stream_buffer = device.stream_buffers[device.buffer_ring];
- device.index_buffer = device.index_buffers[device.buffer_ring];
- device.stream_offset = 0;
- device.index_offset = 0;
+ stream_ring_reset(ring_ready);
#else
- device.stream_offset = STREAM_BUFFER_SIZE; /* orphan next frame */
- device.index_offset = INDEX_BUFFER_SIZE;
+ device.stream_offset = device.stream_capacity; /* orphan next frame, including a grown buffer */
+ device.index_offset = device.index_capacity;
#endif
#if defined(HALO_ARM64_GUEST) && !defined(HALO_GLES)
- if (device.visibility_results)
+ if (ring_ready &&
+#ifdef XGPU_SAMPLE_COUNTERS
+ !SAMPLE_COUNTING &&
+#endif
+ device.visibility_results)
host_gl_read_results(device.visibility_results_copy, sizeof(device.visibility_results_copy));
#endif
}
@@ -4833,6 +5181,7 @@ void WINAPI D3DDevice_Present(CONST RECT *source_rectangle, CONST RECT *destinat
{
struct render_target_entry *back_buffer = render_target_get(&device.back_buffer);
int window_width, window_height, width, height, x, y;
+ GLuint framebuffer;
if (trace_frame())
platform_log("present back buffer %08lx texture %u", (unsigned long)device.back_buffer.Data,
@@ -4856,10 +5205,12 @@ void WINAPI D3DDevice_Present(CONST RECT *source_rectangle, CONST RECT *destinat
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
- glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer_get(back_buffer->target.texture, 0));
+ framebuffer = framebuffer_get(back_buffer->target.texture, 0);
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
/* row 0 of the render target is the top of the picture */
- glBlitFramebuffer(0, 0, (GLint)back_buffer->target.gl_width, (GLint)back_buffer->target.gl_height,
- x, y + height, x + width, y, GL_COLOR_BUFFER_BIT, GL_LINEAR);
+ if (framebuffer)
+ glBlitFramebuffer(0, 0, (GLint)back_buffer->target.gl_width, (GLint)back_buffer->target.gl_height,
+ x, y + height, x + width, y, GL_COLOR_BUFFER_BIT, GL_LINEAR);
platform_video_swap();
xgpu_gl_state_invalidate();
xgpu_texture_cache_begin_frame();
@@ -4874,6 +5225,13 @@ void WINAPI D3DDevice_Present(CONST RECT *source_rectangle, CONST RECT *destinat
device.frame, stats.draws / stats.presents, stats.immediate_draws / stats.presents, stats.clears / stats.presents,
stats.target_changes / stats.presents, stats.skipped_no_program, stats.skipped_no_target, stats.skipped_link,
stats.mirrored_bytes / stats.presents / 1024, stats.streamed_bytes / stats.presents / 1024);
+#ifdef XGPU_STREAM_RING
+ platform_log("stream: %lu persistent writes, %lu mutable writes, %lu subdata fallbacks; "
+ "%lu/%lu vertex/index overflows, %lu/%lu spills; ring waits %.3f ms/frame, worst %.3f ms, %lu failures",
+ stats.persistent_writes, stats.mutable_writes, stats.persistent_fallback_writes,
+ stats.stream_overflows, stats.index_overflows, stats.stream_spills, stats.index_spills,
+ stats.ring_wait_milliseconds / stats.presents, stats.ring_wait_worst, stats.ring_wait_failures);
+#endif
memset(&stats, 0, sizeof(stats));
}
platform_pump_events();
diff --git a/port/linux/src/gl.h b/port/linux/src/gl.h
index 7ba6633bcaa9601389c3fb0220cf77e0b138683b..32888af17ee28bb9582b23f22c6a2ff26213117e 100644
--- a/port/linux/src/gl.h
+++ b/port/linux/src/gl.h
@@ -114,6 +114,7 @@ this list to generate the guest's entry points */
X(glGetShaderInfoLog) \
X(glDeleteShader) \
X(glCreateProgram) \
+ X(glDeleteProgram) \
X(glAttachShader) \
X(glBindAttribLocation) \
X(glLinkProgram) \
@@ -226,6 +227,7 @@ the Linux arm64 build's desktop renderer (tools/linux_arm64_build.py) */
X(glGetShaderInfoLog) \
X(glDeleteShader) \
X(glCreateProgram) \
+ X(glDeleteProgram) \
X(glAttachShader) \
X(glBindAttribLocation) \
X(glBindFragDataLocation) \
@@ -353,6 +355,7 @@ pointers, sees the declarations without these aliases */
#define glGetShaderInfoLog halo_glGetShaderInfoLog
#define glDeleteShader halo_glDeleteShader
#define glCreateProgram halo_glCreateProgram
+#define glDeleteProgram halo_glDeleteProgram
#define glAttachShader halo_glAttachShader
#define glBindAttribLocation halo_glBindAttribLocation
#define glLinkProgram halo_glLinkProgram
@@ -460,6 +463,7 @@ pointers, sees the declarations without these aliases */
#define glGetShaderInfoLog halo_glGetShaderInfoLog
#define glDeleteShader halo_glDeleteShader
#define glCreateProgram halo_glCreateProgram
+#define glDeleteProgram halo_glDeleteProgram
#define glAttachShader halo_glAttachShader
#define glBindAttribLocation halo_glBindAttribLocation
#define glBindFragDataLocation halo_glBindFragDataLocation
diff --git a/port/linux/src/nv2a_psh.c b/port/linux/src/nv2a_psh.c
index c2e5de729dae16b503b1c5eee07834c8add9e94d..6c3a0814611add89f0d6a2abdfb9d27b01e769c2 100644
--- a/port/linux/src/nv2a_psh.c
+++ b/port/linux/src/nv2a_psh.c
@@ -353,7 +353,8 @@ static void sample(struct xgpu_text *text, const struct nv2a_pixel_shader_key *k
xgpu_text_append(text, "texture(tex%d, vec3((%s).xy * texture_scale[%d].xy * vec2(1.0, -1.0) + vec2(0.0, 1.0), %s)"
SAMPLE_BIAS ")", stage,
coordinates, stage, key->multiview == XGPU_MULTIVIEW_EYES ? "float(gl_ViewID_OVR)" :
- key->multiview == XGPU_MULTIVIEW_RIGHT_EYE ? "1.0" : "0.0"
+ key->multiview == XGPU_MULTIVIEW_RIGHT_EYE ||
+ (key->multiview == XGPU_MULTIVIEW_NONE && key->sample_eye) ? "1.0" : "0.0"
#ifdef HALO_GLES
, stage
#endif
diff --git a/port/linux/src/port_config.c b/port/linux/src/port_config.c
index c23181bd889e417aa70dd1b549765265a254ba5c..14aedd6a3b8df0ca153f2b368ea1246d9aedff1a 100644
--- a/port/linux/src/port_config.c
+++ b/port/linux/src/port_config.c
@@ -102,6 +102,11 @@ static const struct config_setting config_settings[] =
{ "display.interpolation", _config_boolean, "true", "HALO_INTERPOLATION", _environment_value, _platform_all,
"Draw a frame for every display refresh, blending between the game's 30\n"
"ticks a second; false keeps the original 30 frames a second." },
+ { "display.persistent_stream_buffers", _config_boolean, "false", "HALO_PERSISTENT_STREAM_BUFFERS",
+ _environment_value, _platform_desktop,
+ "Experimental native ARM64 vertex/index uploads through persistent GL\n"
+ "mappings. Read at startup; restart to change. Unsupported or failed\n"
+ "allocation keeps mutable uploads. Enable debug.gpu_stats for counters." },
{ "display.direct_camera", _config_boolean, "true", "HALO_DIRECT_CAMERA", _environment_value, _platform_desktop,
"In first person, point the view where the player aims now instead of\n"
"where the last tick left it: the view turns the frame the mouse moves,\n"
@@ -159,12 +164,28 @@ static const struct config_setting config_settings[] =
{ "vr.refresh_rate", _config_real, "90.0", "HALO_VR_REFRESH_RATE", _environment_value, _platform_desktop,
"The headset's refresh rate to ask for, in Hz (the game draws a frame for\n"
"each refresh); 0 leaves the runtime's." },
+ { "vr.sun_glow_strength", _config_real, "0.5", "HALO_VR_SUN_GLOW_STRENGTH", _environment_value, _platform_desktop,
+ "The final stereo sun-glow intensity, from 0.0 to 1.0. Does not change\n"
+ "the sun's occlusion mask or the flat-screen effect." },
+ { "vr.menu_pointer", _config_boolean, "true", "HALO_VR_MENU_POINTER", _environment_value, _platform_desktop,
+ "Point the right controller at the menu and use its trigger to select.\n"
+ "The existing stick and button menu controls remain available." },
+ { "vr.radar_head_heading", _config_boolean, "true", "HALO_VR_RADAR_HEAD_HEADING", _environment_value,
+ _platform_desktop,
+ "Rotate the local VR motion sensor with the head's horizontal heading.\n"
+ "False uses the player's facing direction." },
{ "vr.aim", _config_string, "\"controller\"", "HALO_VR_AIM", _environment_value, _platform_desktop,
"What aims: \"controller\" the right controller, which holds the weapon\n"
"(the reticle shows where it points), \"head\" the head (the reticle in\n"
"the middle of the view, the weapon before the eyes), \"gamepad\" a\n"
"gamepad, as on the flat screen (the stick turns the view, and the head\n"
"looks about from it: vr.gamepad_view)." },
+ { "vr.movement", _config_string, "\"head\"", "HALO_VR_MOVEMENT", _environment_value, _platform_desktop,
+ "On foot with controller aiming, \"head\" walks in the head's horizontal\n"
+ "direction while the controller still aims the weapon. \"aim\" walks\n"
+ "in the weapon's direction, as before. Vehicles and gamepad aiming\n"
+ "keep their controls. Looking nearly straight up or down keeps the last\n"
+ "usable head heading; losing head tracking stops head-relative walking." },
{ "vr.gamepad_view", _config_string, "\"camera\"", "HALO_VR_GAMEPAD_VIEW", _environment_value,
_platform_desktop,
"With vr.aim = \"gamepad\", how the stick moves the view: \"camera\" turns\n"
@@ -186,6 +207,13 @@ static const struct config_setting config_settings[] =
"With vr.aim = \"controller\", a long gun (not a pistol or the needler)\n"
"is aimed with both hands while the left grip holds it by the foregrip:\n"
"along the line from the right hand to the left." },
+ /* Recognize old private-build keys without reactivating removed optics. */
+ { "vr.zoom_stabilization", _config_boolean, "false", "HALO_VR_ZOOM_STABILIZATION", _environment_value,
+ _platform_desktop, "Legacy compatibility key; ignored. Controller aiming uses its direct path." },
+ { "vr.scope", _config_boolean, "false", "HALO_VR_SCOPE", _environment_value, _platform_desktop,
+ "Legacy compatibility key; ignored. The original weapon zoom is used." },
+ { "vr.scope_size", _config_real, "0.06", "HALO_VR_SCOPE_SIZE", _environment_value, _platform_desktop,
+ "Legacy compatibility key; ignored. No gun-mounted optic is rendered." },
{ "vr.hands", _config_string, "\"arms\"", "HALO_VR_HANDS", _environment_value, _platform_desktop,
"With vr.aim = \"controller\", the first-person hands: each where its\n"
"controller is (the left on a long gun's foregrip while it holds it),\n"
@@ -200,6 +228,12 @@ static const struct config_setting config_settings[] =
"The view in a vehicle's seat: \"first_person\" from your head where you\n"
"sit, level with the horizon; \"third_person\" the game's camera following\n"
"the vehicle." },
+ { "vr.vehicle_seat_calibration", _config_boolean, "true", "HALO_VR_VEHICLE_SEAT_CALIBRATION",
+ _environment_value, _platform_desktop,
+ "Calibrate the local VR seat on entry and follow the vehicle's heading." },
+ { "vr.vehicle_clear_windshield", _config_boolean, "true", "HALO_VR_VEHICLE_CLEAR_WINDSHIELD",
+ _environment_value, _platform_desktop,
+ "Hide the occupied local Warthog's glass for a clear view from its seat." },
{ "vr.turn", _config_string, "\"snap\"", "HALO_VR_TURN", _environment_value, _platform_desktop,
"How the right stick turns: \"snap\" in steps (vr.snap_turn_angle),\n"
"\"smooth\" continuously (vr.smooth_turn_speed); with vr.aim = \"gamepad\",\n"
@@ -208,7 +242,7 @@ static const struct config_setting config_settings[] =
"Degrees of each snap turn." },
{ "vr.smooth_turn_speed", _config_real, "150.0", "HALO_VR_SMOOTH_TURN_SPEED", _environment_value, _platform_desktop,
"Degrees a second of smooth turning, the stick pushed all the way." },
- { "vr.height", _config_string, "\"seated\"", "HALO_VR_HEIGHT", _environment_value, _platform_desktop,
+ { "vr.height", _config_string, "\"standing\"", "HALO_VR_HEIGHT", _environment_value, _platform_desktop,
"\"seated\": the head where it is when recentred (both grips, or a\n"
"gamepad's Back, held) is the player's eye. \"standing\": the head\n"
"vr.player_height above the floor is." },
diff --git a/port/linux/src/vr.c b/port/linux/src/vr.c
index 89cfb6f0d81d1d406b6b9cff2e76a66129202b4a..f19aeff061fea719294477852e6b6f68961ca5fb 100644
--- a/port/linux/src/vr.c
+++ b/port/linux/src/vr.c
@@ -31,6 +31,10 @@ it or in steps ("level", "snap"). The controllers are a gamepad's halves.
#include "port_config.h"
#include "vr.h"
#include "vr_host.h"
+#include "vr_locomotion.h"
+#include "vr_vehicle.h"
+#include "vr_driving.h"
+#include "../include/halo_ui_pointer.h"
#include <SDL3/SDL.h>
#include <math.h>
@@ -94,11 +98,20 @@ static struct
int was_focused;
float origin_position[3];
float origin_yaw;
+ struct halo_vr_vehicle_reference vehicle;
/* the turn, and whether the aim follows the head */
float body_yaw;
int aiming;
int aimed;
int aim_synced;
+ /* Head-relative walking has its own heading; it never changes aim. */
+ struct halo_vr_locomotion_heading movement_heading;
+ /* the tutorial's look tests observe head motion independently of the
+ controller's aim, without feeding it into the game's facing */
+ int tutorial_head_valid;
+ float tutorial_head_yaw, tutorial_head_pitch;
+ int tutorial_turn_flags;
+ int turn_control_allowed;
int snap_latched;
float recentre_time;
/* a gamepad's Back held (halo_vr_gamepad_back), and for how long */
@@ -109,6 +122,10 @@ static struct
int acquired[VR_SWAPCHAIN_COUNT];
int force_render;
int menus;
+ /* A menu is placed once when opened, independently of the gameplay
+ origin. It stays in the play space until closed or deliberately recentred. */
+ int menu_open;
+ struct vr_host_pose menu_pose;
/* the eyes' depth this frame, and its range */
int depth_written;
/* the melee gesture: the right hand's last position, and the frames
@@ -127,6 +144,11 @@ static struct
float depth_near, depth_far;
/* the presented frame, ended when the next begins (halo_vr_present) */
struct vr_host_layers pending_layers;
+ /* Exact locate sample used by the completed world camera. Auxiliary
+ gaze/radar locates must not change its compositor projection pose. */
+ int render_pose_valid;
+ struct vr_host_pose render_eye[2];
+ float render_fov[2][4];
int pending;
int attempted;
/* debug.gpu_stats: the frames' timing */
@@ -136,6 +158,16 @@ static struct
double frame_seconds, wait_seconds, worst_seconds;
} vr;
+/* The ray uses the controller's raw OpenXR aim pose, independently of the
+weapon, head-relative walking and the saved gameplay origin. Button history
+survives a menu close so a held selection trigger cannot fire on resuming. */
+static struct
+{
+ int active, armed, down, blocked;
+ int position_valid;
+ short x, y;
+} menu_pointer;
+
/* ---------- settings */
static struct
@@ -151,8 +183,11 @@ static struct
float hud_size;
float menu_distance;
float menu_width;
+ int menu_pointer;
+ int radar_head_heading;
int controller_aim;
int gamepad_aim;
+ int head_movement;
/* vr.gamepad_view: the view's heading in steps ("snap"), and pitched
with the facing ("camera") */
int gamepad_snap;
@@ -160,6 +195,7 @@ static struct
float weapon_offset[3];
int melee_gesture;
int vehicle_first_person;
+ int vehicle_seat_calibration, vehicle_clear_windshield;
float test_turn;
float test_jitter;
/* debug.vr_test_head: the head held still, turned left and right by
@@ -179,6 +215,10 @@ static struct
float test_hands[TEST_HANDS_POSES][16];
} settings = { (unsigned long)-1 };
+static struct halo_vr_driving_state driving;
+static void driving_reset(int release_required);
+static int driving_active(void);
+
/* debug.vr_test_hands: "lx ly lz lyaw lpitch lroll, rx ry rz ryaw rpitch
rroll[, grip[, buttons[, trigger[, head yaw]]]]", poses separated by ';',
each held TEST_HANDS_FRAMES frames */
@@ -218,6 +258,11 @@ static void settings_read(void)
if (settings.read_at == config_changes())
return;
settings.read_at = config_changes();
+ /* Mode/config changes reset the driver's held wheel and neutral latch. */
+ driving_reset(1);
+ driving.accelerator_blocked = 1;
+ driving.accelerator_consumed = 0;
+ driving.brake_consumed = 0;
settings.smooth_turn = !strcmp(config_string("vr.turn"), "smooth");
settings.snap_angle = (float)config_real("vr.snap_turn_angle") * (float)M_PI / 180.0f;
settings.smooth_speed = (float)config_real("vr.smooth_turn_speed") * (float)M_PI / 180.0f;
@@ -234,12 +279,17 @@ static void settings_read(void)
settings.hud_size = 60.0f;
settings.menu_distance = (float)config_real("vr.menu_distance");
settings.menu_width = (float)config_real("vr.menu_width");
+ settings.menu_pointer = config_boolean("vr.menu_pointer");
+ settings.radar_head_heading = config_boolean("vr.radar_head_heading");
settings.gamepad_aim = !strcmp(config_string("vr.aim"), "gamepad");
settings.controller_aim = !settings.gamepad_aim && strcmp(config_string("vr.aim"), "head") != 0;
+ settings.head_movement = strcmp(config_string("vr.movement"), "aim") != 0;
settings.gamepad_snap = !strcmp(config_string("vr.gamepad_view"), "snap");
settings.gamepad_pitch = !settings.gamepad_snap && strcmp(config_string("vr.gamepad_view"), "level") != 0;
settings.melee_gesture = config_boolean("vr.melee_gesture");
settings.vehicle_first_person = strcmp(config_string("vr.vehicle_view"), "third_person") != 0;
+ settings.vehicle_seat_calibration = config_boolean("vr.vehicle_seat_calibration");
+ settings.vehicle_clear_windshield = config_boolean("vr.vehicle_clear_windshield");
settings.test_turn = (float)config_real("debug.vr_test_turn") * (float)M_PI / 180.0f;
settings.test_jitter = (float)config_real("debug.vr_test_jitter") * 0.001f;
settings.test_head_yaw = 0.0f;
@@ -319,6 +369,238 @@ static void game_axes(const float v[3], float yaw, float out[3])
out[2] = v[1];
}
+/* ---------- stock driver controls */
+
+static void driving_reset(int release_required)
+{
+ driving.wheel_valid = 0;
+ driving.wheel_blocked = release_required != 0;
+ driving.wheel_steer = 0.0f;
+ memset(driving.wheel_axis, 0, sizeof(driving.wheel_axis));
+ memset(driving.wheel_line, 0, sizeof(driving.wheel_line));
+}
+
+static int driving_active(void)
+{
+ return driving.mode != HALO_VR_DRIVING_NONE && !settings.gamepad_aim;
+}
+
+static int driving_tracking_valid(void)
+{
+ float orientation[4];
+ int eye, axis;
+ if (!vr.initialized || !vr.frame || !vr.views.should_render || !vr.views.focused || vr.menus ||
+ (vr.vehicle.active && vr.vehicle.orientation_valid < 0) ||
+ !vr.origin_valid || !isfinite(vr.origin_yaw) || !vr.views.head.valid ||
+ !halo_vr_locomotion_orientation(vr.views.head.orientation, orientation))
+ return 0;
+ for (axis = 0; axis < 3; axis++)
+ if (!isfinite(vr.views.head.position[axis]))
+ return 0;
+ for (eye = 0; eye < 2; eye++)
+ {
+ if (!vr.views.eye[eye].valid ||
+ !halo_vr_locomotion_orientation(vr.views.eye[eye].orientation, orientation))
+ return 0;
+ for (axis = 0; axis < 3; axis++)
+ if (!isfinite(vr.views.eye[eye].position[axis]))
+ return 0;
+ }
+ return 1;
+}
+
+void halo_vr_vehicle_controls(int mode, float hull_yaw)
+{
+ settings_read();
+ if (!vr.initialized || mode < HALO_VR_DRIVING_NONE || mode > HALO_VR_DRIVING_BANSHEE || !isfinite(hull_yaw))
+ mode = HALO_VR_DRIVING_NONE;
+ if (mode != driving.mode)
+ {
+ if (!settings.gamepad_aim && (driving.mode != HALO_VR_DRIVING_NONE || mode != HALO_VR_DRIVING_NONE))
+ driving.turn_blocked = 1;
+ driving.mode = mode;
+ driving.align = mode != HALO_VR_DRIVING_NONE;
+ driving_reset(1);
+ driving.accelerator_blocked = 1;
+ if (mode != HALO_VR_DRIVING_NONE && mode != HALO_VR_DRIVING_WARTHOG)
+ {
+ driving.accelerator_consumed = 0;
+ driving.brake_consumed = 0;
+ }
+ if (mode == HALO_VR_DRIVING_WARTHOG && !settings.gamepad_aim)
+ vr.melee_frames = 0;
+ }
+ driving.hull_yaw = isfinite(hull_yaw) ? remainderf(hull_yaw, 2.0f * (float)M_PI) : 0.0f;
+ if (driving_active())
+ {
+ /* A frame may have polled the stick before the game registered entry. */
+ vr.vehicle.turn = 0.0f;
+ vr.tutorial_turn_flags = 0;
+ vr.tutorial_head_valid = 0;
+ }
+}
+
+int halo_vr_vehicle_controls_ready(void)
+{
+ return !vr.initialized || settings.gamepad_aim || (driving_active() && driving_tracking_valid());
+}
+
+int halo_vr_vehicle_brake(void)
+{
+ if (!driving_active() || driving.mode != HALO_VR_DRIVING_WARTHOG ||
+ !driving_tracking_valid() || !vr.input.active[1])
+ return 0;
+ if (!(vr.input.buttons & VR_BUTTON_B))
+ {
+ driving.brake_consumed = 0;
+ return 0;
+ }
+ driving.brake_consumed = 1;
+ return 1;
+}
+
+float halo_vr_vehicle_accelerator(float stick_forward)
+{
+ if (!driving_active() || driving.mode != HALO_VR_DRIVING_WARTHOG)
+ return stick_forward;
+ if (!driving_tracking_valid() || !isfinite(stick_forward))
+ {
+ driving.accelerator_blocked = 1;
+ return 0.0f;
+ }
+ if (!vr.input.active[1])
+ {
+ /* An inactive motion controller cannot accelerate. Keep a separately
+ merged ordinary gamepad/stick's original forward request available. */
+ driving.accelerator_blocked = 1;
+ return stick_forward;
+ }
+ if (!(vr.input.buttons & VR_BUTTON_A))
+ {
+ driving.accelerator_blocked = 0;
+ driving.accelerator_consumed = 0;
+ }
+ else if (!driving.accelerator_blocked)
+ driving.accelerator_consumed = 1;
+ /* Braking wins over both A and reverse, while a released A still rearms
+ above. The game also receives its native brake flag, so this is braking
+ rather than merely removing the gas and coasting. */
+ if (halo_vr_vehicle_brake())
+ return 0.0f;
+ if (!(vr.input.buttons & VR_BUTTON_A) || driving.accelerator_blocked)
+ return stick_forward;
+ /* Keep reverse available even when A is held. The processed stick has
+ already passed the game's dead zone; allow its small centre noise. */
+ return stick_forward < -0.10f ? stick_forward : 1.0f;
+}
+
+static void driving_frame_update(void)
+{
+ static const float forward[3] = { 0.0f, 0.0f, -1.0f };
+ float orientation[4], head_forward[3];
+ int hand;
+ if (!driving_active() || driving.mode != HALO_VR_DRIVING_WARTHOG || !driving_tracking_valid() ||
+ !isfinite(vr.input.left_grip) || !isfinite(vr.input.right_grip) ||
+ vr.input.left_grip < 0.0f || vr.input.left_grip > 1.0f ||
+ vr.input.right_grip < 0.0f || vr.input.right_grip > 1.0f)
+ {
+ driving_reset(1);
+ return;
+ }
+ /* After lost tracking or a menu, both grips must be observed released. */
+ if (vr.input.left_grip <= 0.3f && vr.input.right_grip <= 0.3f)
+ {
+ driving_reset(0);
+ return;
+ }
+ if (driving.wheel_blocked)
+ return;
+ if (vr.input.left_grip <= 0.3f || vr.input.right_grip <= 0.3f)
+ {
+ driving_reset(0);
+ return;
+ }
+ if (!driving.wheel_valid && (vr.input.left_grip < 0.65f || vr.input.right_grip < 0.65f))
+ return;
+ for (hand = 0; hand < 2; hand++)
+ if (!vr.input.active[hand] || !vr.views.grip[hand].valid ||
+ !halo_vr_locomotion_orientation(vr.views.grip[hand].orientation, orientation))
+ {
+ driving_reset(1);
+ return;
+ }
+ /* The plane is captured at engagement and stays fixed while the driver
+ looks around. Only a change in the physical hand line steers it. */
+ (void)halo_vr_locomotion_orientation(vr.views.head.orientation, orientation);
+ quaternion_rotate(orientation, forward, head_forward);
+ if (!halo_vr_driving_wheel(&driving, head_forward,
+ vr.views.grip[0].position, vr.views.grip[1].position))
+ driving_reset(1);
+}
+
+int halo_vr_vehicle_steering(float hull_yaw, float lateral, float seconds, float *yaw, float *pitch)
+{
+ float steer;
+ if (!yaw || !pitch || !driving_active() || driving.mode != HALO_VR_DRIVING_WARTHOG || !isfinite(hull_yaw))
+ return 0;
+ if (!driving_tracking_valid() || !isfinite(lateral) || !isfinite(seconds) || seconds < 0.0f)
+ {
+ driving_reset(1);
+ driving.accelerator_blocked = 1;
+ *yaw = remainderf(hull_yaw, 2.0f * (float)M_PI);
+ *pitch = 0.0f;
+ return 2;
+ }
+ driving_frame_update();
+ steer = driving.wheel_valid ? driving.wheel_steer : halo_vr_driving_curve(lateral);
+ *yaw = remainderf(hull_yaw + steer * ((float)M_PI / 3.0f), 2.0f * (float)M_PI);
+ *pitch = 0.0f;
+ return 1;
+}
+
+/* 0: no override; -1: registered driver, tracking unavailable; 1: entry
+alignment; 2: retain the game's stick facing. Never fall back to tracked aim. */
+static int driving_aim(float *yaw, float *pitch)
+{
+ if (!driving_active())
+ return 0;
+ if (!driving_tracking_valid())
+ {
+ driving_reset(1);
+ driving.accelerator_blocked = 1;
+ return -1;
+ }
+ vr.vehicle.turn = 0.0f;
+ vr.aiming = 1;
+ if (driving.align || !vr.aim_synced)
+ {
+ driving.align = 0;
+ vr.aim_synced = 1;
+ *yaw = driving.hull_yaw;
+ *pitch = 0.0f;
+ return 1;
+ }
+ return 2;
+}
+
+static int driving_turn_blocked(float x)
+{
+ if (driving_active())
+ {
+ driving.turn_blocked = 1;
+ return 1;
+ }
+ if (settings.gamepad_aim || !driving.turn_blocked)
+ return 0;
+ if (isfinite(x) && fabsf(x) < 0.3f)
+ {
+ driving.turn_blocked = 0;
+ vr.snap_latched = 0;
+ return 0;
+ }
+ return 1;
+}
+
/* ---------- the session */
int halo_vr_initialize(void)
@@ -347,7 +629,7 @@ int halo_vr_initialize(void)
return 0;
}
vr.force_render = config_boolean("debug.vr_force_render");
- vr.acquired[0] = vr.acquired[1] = vr.acquired[2] = -1;
+ vr.acquired[0] = vr.acquired[1] = vr.acquired[2] = vr.acquired[3] = -1;
vr.initialized = 1;
vr.statistics = config_boolean("debug.gpu_stats");
platform_log("vr: %s, eyes %dx%d, %.0f Hz", vr.info.system_name, vr.info.eye_width, vr.info.eye_height,
@@ -380,6 +662,88 @@ unsigned int halo_vr_image(int swapchain)
return vr.info.images[swapchain][index];
}
+/* A vehicle captures a neutral seated HMD pose without modifying the
+saved standing/on-foot reference. Seat and aim share this basis. */
+static int vehicle_view_active(void)
+{
+ return settings.vehicle_first_person && settings.vehicle_seat_calibration &&
+ vr.vehicle.active && vr.vehicle.valid;
+}
+
+static void vehicle_capture(void)
+{
+ float normalized[4], position[3];
+ int axis;
+ vr.vehicle.valid = 0;
+ if (!vr.views.focused || !vr.views.head.valid ||
+ !halo_vr_locomotion_orientation(vr.views.head.orientation, normalized))
+ return;
+ for (axis = 0; axis < 3; axis++)
+ position[axis] = vr.views.eye[0].valid && vr.views.eye[1].valid ?
+ (vr.views.eye[0].position[axis] + vr.views.eye[1].position[axis]) * 0.5f : vr.views.head.position[axis];
+ if (halo_vr_vehicle_capture(&vr.vehicle, position, quaternion_yaw(normalized)))
+ vr.aim_synced = 0;
+}
+
+void halo_vr_vehicle_reference(int unit, int vehicle, int seat, float forward_yaw)
+{
+ float normalized[4], position[3] = { 0.0f, 0.0f, 0.0f }, yaw = 0.0f;
+ int tracked, axis, was_valid = vr.vehicle.valid;
+ settings_read();
+ if (!vr.initialized || !settings.vehicle_first_person || !settings.vehicle_seat_calibration)
+ unit = vehicle = -1;
+ tracked = (vr.frame || vr.pending) && vr.views.should_render && vr.views.focused &&
+ vr.views.head.valid && halo_vr_locomotion_orientation(vr.views.head.orientation, normalized);
+ if (tracked)
+ {
+ yaw = quaternion_yaw(normalized);
+ for (axis = 0; axis < 3; axis++)
+ position[axis] = vr.views.eye[0].valid && vr.views.eye[1].valid ?
+ (vr.views.eye[0].position[axis] + vr.views.eye[1].position[axis]) * 0.5f : vr.views.head.position[axis];
+ for (axis = 0; axis < 3; axis++)
+ if (!isfinite(position[axis]) || !isfinite(vr.views.head.position[axis]))
+ tracked = 0;
+ if (vr.views.eye[0].valid && vr.views.eye[1].valid &&
+ (!halo_vr_locomotion_orientation(vr.views.eye[0].orientation, normalized) ||
+ !halo_vr_locomotion_orientation(vr.views.eye[1].orientation, normalized)))
+ tracked = 0;
+ }
+ if (halo_vr_vehicle_update(&vr.vehicle, unit, vehicle, seat, forward_yaw, tracked, position, yaw) ||
+ (!was_valid && vr.vehicle.valid))
+ {
+ driving_reset(1);
+ driving.accelerator_blocked = 1;
+ vr.aim_synced = 0;
+ halo_vr_locomotion_reset(&vr.movement_heading);
+ }
+}
+
+int halo_vr_vehicle_clear_windshield(void)
+{
+ settings_read();
+ return settings.vehicle_first_person && settings.vehicle_clear_windshield;
+}
+
+void halo_vr_vehicle_orientation(const float axes[3][3], float hull_yaw)
+{
+ float basis[3][3];
+ if (!vr.vehicle.active)
+ return;
+ vr.vehicle.orientation_valid = -1;
+ if (!halo_vr_vehicle_basis(axes, hull_yaw, hull_yaw, basis))
+ return;
+ memcpy(vr.vehicle.hull_axes, basis, sizeof(basis));
+ vr.vehicle.hull_yaw = hull_yaw;
+ vr.vehicle.orientation_valid = 1;
+}
+
+static int vehicle_basis(float basis[3][3])
+{
+ return vehicle_view_active() && vr.vehicle.orientation_valid == 1 &&
+ halo_vr_vehicle_basis(vr.vehicle.hull_axes, vr.vehicle.hull_yaw,
+ halo_vr_vehicle_heading(&vr.vehicle), basis);
+}
+
static void recentre(void)
{
const struct vr_host_pose *head = &vr.views.head;
@@ -388,6 +752,15 @@ static void recentre(void)
if (!head->valid)
return;
yaw = quaternion_yaw(head->orientation);
+ driving_reset(1);
+ driving.accelerator_blocked = 1;
+ if (settings.vehicle_first_person && settings.vehicle_seat_calibration && vr.vehicle.active)
+ {
+ vehicle_capture();
+ vr.menu_pose.valid = 0;
+ platform_log("vr: recentred (%s)", "vehicle seat");
+ return;
+ }
/* the aim keeps pointing where it did; the gamepad's is where the head
looks now (halo_vr_aim) */
if (settings.gamepad_aim)
@@ -395,10 +768,12 @@ static void recentre(void)
else if (vr.origin_valid)
vr.body_yaw += yaw - vr.origin_yaw;
vr.origin_yaw = yaw;
+ halo_vr_locomotion_reset(&vr.movement_heading);
vr.origin_position[0] = head->position[0];
vr.origin_position[1] = settings.standing ? settings.player_height : head->position[1];
vr.origin_position[2] = head->position[2];
vr.origin_valid = 1;
+ vr.menu_pose.valid = 0;
platform_log("vr: recentred (%s)", settings.standing ? "standing" : "seated");
}
@@ -484,7 +859,7 @@ static void two_handed_update(float seconds)
/* where the weapon in the right hand points, and its up (OpenXR axes): the
right controller's pointing pose, or with both hands on a long gun, the line
between them, the right hand's roll kept */
-static void weapon_pointing(float forward[3], float up[3])
+static void weapon_pointing_raw(float forward[3], float up[3])
{
static const float backwards[3] = { 0.0f, 0.0f, -1.0f };
static const float upwards[3] = { 0.0f, 1.0f, 0.0f };
@@ -510,7 +885,52 @@ static void weapon_pointing(float forward[3], float up[3])
}
}
+static void weapon_pointing(float forward[3], float up[3])
+{
+ weapon_pointing_raw(forward, up);
+}
+
/* the controllers' turning and recentring, once a frame */
+/* Apply a deliberate stick view turn only while the game permits free-look.
+The tutorial observes the same turn that the headset displays. */
+static void tutorial_turn_input(float x, float seconds)
+{
+ float delta = 0.0f;
+
+ if (driving_turn_blocked(x) || !vr.turn_control_allowed || settings.gamepad_aim ||
+ !halo_vr_gaze_tracking_valid() || !isfinite(x) || !isfinite(seconds) || seconds < 0.0f)
+ {
+ vr.tutorial_turn_flags = 0;
+ vr.tutorial_head_valid = 0;
+ /* A held stick must return to neutral after tracking/control recovers. */
+ vr.snap_latched = !isfinite(x) || fabsf(x) >= 0.3f;
+ return;
+ }
+ if (settings.smooth_turn)
+ {
+ if (fabsf(x) > 0.2f && isfinite(settings.smooth_speed))
+ delta = -(x - (x > 0.0f ? 0.2f : -0.2f)) / 0.8f * settings.smooth_speed * seconds;
+ }
+ else if (!vr.snap_latched && fabsf(x) > 0.7f)
+ {
+ if (isfinite(settings.snap_angle) && settings.snap_angle > 0.0f)
+ delta = x > 0.0f ? -settings.snap_angle : settings.snap_angle;
+ vr.snap_latched = 1;
+ }
+ else if (vr.snap_latched && fabsf(x) < 0.3f)
+ {
+ vr.snap_latched = 0;
+ }
+ if (isfinite(delta) && delta != 0.0f)
+ {
+ if (vehicle_view_active())
+ vr.vehicle.turn = remainderf(vr.vehicle.turn + delta, 2.0f * (float)M_PI);
+ else
+ vr.body_yaw += delta;
+ vr.tutorial_turn_flags |= delta > 0.0f ? HALO_VR_TUTORIAL_TURN_LEFT : HALO_VR_TUTORIAL_TURN_RIGHT;
+ }
+}
+
static void input_update(void)
{
unsigned long now = GetTickCount();
@@ -520,13 +940,17 @@ static void input_update(void)
float x = settings.gamepad_aim ? 0.0f : vr.input.right_stick[0];
vr.last_ticks = now;
+ driving_frame_update();
if (seconds > 0.1f)
seconds = 0.1f;
/* a punch of the right controller melees: its hand moving fast along
where it points, for a few frames (vr.melee_gesture) */
if (vr.melee_frames > 0)
vr.melee_frames--;
+ if (driving_active() && driving.mode == HALO_VR_DRIVING_WARTHOG)
+ vr.melee_frames = 0;
if (settings.melee_gesture && !settings.gamepad_aim && vr.views.grip[1].valid && vr.hand_valid &&
+ !(driving_active() && driving.mode == HALO_VR_DRIVING_WARTHOG) &&
seconds > 0.0f)
{
static const float backwards[3] = { 0.0f, 0.0f, -1.0f };
@@ -548,8 +972,13 @@ static void input_update(void)
memcpy(vr.hand_position, vr.views.grip[1].position, sizeof(vr.hand_position));
/* debug.vr_test_turn: turning without hands, for automated tests (the
gamepad's aim turns the body itself) */
- if (!settings.gamepad_aim)
- vr.body_yaw += settings.test_turn * seconds;
+ if (!settings.gamepad_aim && !driving_active())
+ {
+ if (vehicle_view_active())
+ vr.vehicle.turn = remainderf(vr.vehicle.turn + settings.test_turn * seconds, 2.0f * (float)M_PI);
+ else
+ vr.body_yaw += settings.test_turn * seconds;
+ }
/* a gamepad's Back held recentres (the controllers may be off) */
if (vr.gamepad_back)
{
@@ -564,22 +993,9 @@ static void input_update(void)
{
vr.gamepad_recentre_time = 0.0f;
}
- if (settings.smooth_turn)
- {
- if (fabsf(x) > 0.2f)
- vr.body_yaw -= (x - (x > 0.0f ? 0.2f : -0.2f)) / 0.8f * settings.smooth_speed * seconds;
- }
- else if (!vr.snap_latched && fabsf(x) > 0.7f)
- {
- vr.body_yaw -= x > 0.0f ? settings.snap_angle : -settings.snap_angle;
- vr.snap_latched = 1;
- }
- else if (vr.snap_latched && fabsf(x) < 0.3f)
- {
- vr.snap_latched = 0;
- }
+ tutorial_turn_input(x, seconds);
/* (not while the left hand holds a long gun's foregrip) */
- if (vr.input.left_grip > 0.8f && vr.input.right_grip > 0.8f && !vr.two_handed)
+ if (!driving_active() && vr.input.left_grip > 0.8f && vr.input.right_grip > 0.8f && !vr.two_handed)
{
vr.recentre_time += seconds;
if (vr.recentre_time >= RECENTRE_SECONDS && vr.recentre_time - seconds < RECENTRE_SECONDS)
@@ -811,10 +1227,11 @@ static int frame_begin(void)
return 0;
/* (once a present: until the session runs, each try waits a little) */
vr.attempted = 1;
+ vr.render_pose_valid = 0;
if (vr.pending)
{
host_vr_frame_end(&vr.pending_layers);
- vr.acquired[0] = vr.acquired[1] = vr.acquired[2] = -1;
+ vr.acquired[0] = vr.acquired[1] = vr.acquired[2] = vr.acquired[3] = -1;
vr.pending = 0;
}
settings_read();
@@ -863,6 +1280,108 @@ int halo_vr_frame_active(void)
return vr.frame && vr.views.should_render;
}
+static int render_tracking_valid(void)
+{
+ float normalized[4];
+ int eye, axis;
+ if (!vr.initialized || !vr.frame || !vr.views.should_render || !vr.views.focused || !vr.views.head.valid)
+ return 0;
+ if (!halo_vr_locomotion_orientation(vr.views.head.orientation, normalized))
+ return 0;
+ for (eye = 0; eye < 2; eye++)
+ {
+ float norm = 0.0f;
+ if (!vr.views.eye[eye].valid ||
+ !halo_vr_locomotion_orientation(vr.views.eye[eye].orientation, normalized))
+ return 0;
+ for (axis = 0; axis < 4; axis++)
+ {
+ norm += vr.views.eye[eye].orientation[axis] * vr.views.eye[eye].orientation[axis];
+ if (!isfinite(vr.views.fov[eye][axis]))
+ return 0;
+ }
+ if (!isfinite(norm) || fabsf(norm - 1.0f) > 0.01f ||
+ vr.views.fov[eye][0] >= vr.views.fov[eye][1] || vr.views.fov[eye][3] >= vr.views.fov[eye][2])
+ return 0;
+ for (axis = 0; axis < 3; axis++)
+ if (!isfinite(vr.views.eye[eye].position[axis]) || !isfinite(vr.views.head.position[axis]))
+ return 0;
+ }
+ return 1;
+}
+
+int halo_vr_render_view_commit(int valid)
+{
+ vr.render_pose_valid = 0;
+ if (!valid || !render_tracking_valid())
+ return 0;
+ memcpy(vr.render_eye, vr.views.eye, sizeof(vr.render_eye));
+ memcpy(vr.render_fov, vr.views.fov, sizeof(vr.render_fov));
+ vr.render_pose_valid = 1;
+ return 1;
+}
+
+/* Capture only a fresh, focused pose. Menus can open without a gameplay
+camera (the main menu or postgame), so they do not depend on aiming or the
+recentred gameplay origin. Place the panel along the full forward direction,
+but remove sideways head tilt from its orientation. Keep the captured quad
+fixed while the menu is open. */
+static int menu_tracking_valid(float orientation[4])
+{
+ const struct vr_host_pose *head = &vr.views.head;
+ int axis;
+
+ if (!vr.initialized || !vr.frame || !vr.views.should_render || !vr.views.focused || !head->valid ||
+ !halo_vr_locomotion_orientation(head->orientation, orientation))
+ return 0;
+ for (axis = 0; axis < 3; axis++)
+ if (!isfinite(head->position[axis]))
+ return 0;
+ return 1;
+}
+
+/* A yaw/pitch-only panel has a horizontal right edge and an upright top.
+Keep the head's forward pitch, so a menu above/below still faces the wearer.
+At straight up/down there is no usable yaw; choose a deterministic heading
+instead of letting tiny quaternion rounding or head roll rotate the text. */
+static void menu_level_orientation(const float pointing[3], float orientation[4])
+{
+ float horizontal_squared = pointing[0] * pointing[0] + pointing[2] * pointing[2];
+ float yaw = horizontal_squared > 0.00000001f ? atan2f(-pointing[0], -pointing[2]) : 0.0f;
+ float pitch = atan2f(pointing[1], sqrtf(horizontal_squared));
+ float turn[4] = { 0.0f, sinf(yaw * 0.5f), 0.0f, cosf(yaw * 0.5f) };
+ float tilt[4] = { sinf(pitch * 0.5f), 0.0f, 0.0f, cosf(pitch * 0.5f) };
+ quaternion_multiply(turn, tilt, orientation);
+}
+
+static int menu_place(void)
+{
+ static const float forwards[3] = { 0.0f, 0.0f, -1.0f };
+ const struct vr_host_pose *head = &vr.views.head;
+ struct vr_host_pose pose;
+ float pointing[3], head_orientation[4];
+ int axis;
+
+ if (!menu_tracking_valid(head_orientation) || !isfinite(settings.menu_distance) || settings.menu_distance <= 0.0f)
+ {
+ vr.menu_pose.valid = 0;
+ return 0;
+ }
+ if (vr.menu_pose.valid)
+ return 1;
+ quaternion_rotate(head_orientation, forwards, pointing);
+ menu_level_orientation(pointing, pose.orientation);
+ for (axis = 0; axis < 3; axis++)
+ {
+ pose.position[axis] = head->position[axis] + pointing[axis] * settings.menu_distance;
+ if (!isfinite(pose.position[axis]))
+ return 0;
+ }
+ pose.valid = 1;
+ vr.menu_pose = pose;
+ return 1;
+}
+
/* The frame is ended (xrEndFrame) only when the next begins, as the game
starts drawing it: SteamVR's xrEndFrame waits for the GPU to finish the
frame, and in the meantime the game updates (input, ticks, sounds). */
@@ -876,24 +1395,42 @@ void halo_vr_present(int eyes_drawn, int hud_drawn)
gamepad goes by this frame's) */
vr.aimed = vr.aiming;
vr.aiming = 0;
+ if (vr.menu_open)
+ {
+ float orientation[4];
+
+ /* Even a blank or suppressed frame can lose tracking or focus.
+ The next valid frame places the menu for the current wearer. */
+ if (!menu_tracking_valid(orientation))
+ vr.menu_pose.valid = 0;
+ }
if (!vr.frame)
+ {
+ vr.render_pose_valid = 0;
return;
+ }
memset(pending, 0, sizeof(*pending));
if (vr.views.should_render)
{
- pending->projection = eyes_drawn && vr.acquired[VR_SWAPCHAIN_EYES] >= 0;
- memcpy(pending->eye, vr.views.eye, sizeof(pending->eye));
- memcpy(pending->fov, vr.views.fov, sizeof(pending->fov));
+ pending->projection = eyes_drawn && vr.render_pose_valid && render_tracking_valid() &&
+ vr.acquired[VR_SWAPCHAIN_EYES] >= 0;
+ if (pending->projection)
+ {
+ memcpy(pending->eye, vr.render_eye, sizeof(pending->eye));
+ memcpy(pending->fov, vr.render_fov, sizeof(pending->fov));
+ }
pending->hud = hud_drawn && vr.acquired[VR_SWAPCHAIN_HUD] >= 0;
pending->depth = pending->projection && vr.depth_written && vr.acquired[VR_SWAPCHAIN_DEPTH] >= 0 &&
vr.depth_far > vr.depth_near;
pending->near_z = vr.depth_near;
pending->far_z = vr.depth_far;
- if (pending->projection && !vr.menus)
+ if (eyes_drawn && !vr.menus)
{
/* the HUD before the eyes, where the HUD's camera looks */
float width = 2.0f * settings.hud_distance * tanf(settings.hud_size * 0.5f * (float)M_PI / 180.0f);
+ vr.menu_open = 0;
+ vr.menu_pose.valid = 0;
pending->hud_head_locked = 1;
pending->hud_pose.orientation[3] = 1.0f;
pending->hud_pose.position[2] = -settings.hud_distance;
@@ -902,20 +1439,25 @@ void halo_vr_present(int eyes_drawn, int hud_drawn)
}
else
{
- /* the menus, in front of where the head was at the recentring */
- float s = sinf(vr.origin_yaw * 0.5f), c = cosf(vr.origin_yaw * 0.5f);
-
- pending->hud_pose.orientation[1] = s;
- pending->hud_pose.orientation[3] = c;
- pending->hud_pose.position[0] = vr.origin_position[0] - sinf(vr.origin_yaw) * settings.menu_distance;
- pending->hud_pose.position[1] = settings.standing ? settings.player_height : vr.origin_position[1];
- pending->hud_pose.position[2] = vr.origin_position[2] - cosf(vr.origin_yaw) * settings.menu_distance;
+ /* Capture at the first actually displayed menu, not during a
+ blank loading frame. A missing eye image is still a gameplay HUD. */
+ if (pending->hud)
+ {
+ if (!vr.menu_open)
+ vr.menu_pose.valid = 0;
+ vr.menu_open = 1;
+ if (menu_place())
+ pending->hud_pose = vr.menu_pose;
+ else
+ pending->hud = 0;
+ }
pending->hud_size[0] = settings.menu_width;
pending->hud_size[1] = settings.menu_width * (float)HUD_HEIGHT / (float)HUD_WIDTH;
}
}
vr.pending = 1;
vr.frame = 0;
+ vr.render_pose_valid = 0;
vr.depth_written = 0;
}
@@ -941,13 +1483,24 @@ int halo_vr_view(struct halo_vr_view *view)
{
const struct vr_host_pose *head;
float centre[3], offset[3];
+ float origin_yaw;
+ const float *origin_position;
static const float forward[3] = { 0.0f, 0.0f, -1.0f };
static const float up[3] = { 0.0f, 1.0f, 0.0f };
float v[3];
float inverse[4];
- int eye;
+ float normalized[4];
+ int eye, axis;
+ int frame_active = halo_vr_frame_active();
- if (!halo_vr_frame_active())
+ /* Keep mode ownership separate from pose validity, including failed
+ late locates. The HUD must never reuse a preceding controller pose or
+ fall back to a fabricated head-centred reticle on those frames. */
+ view->controller_aim_requested = settings.controller_aim;
+ view->gamepad_aim = settings.gamepad_aim;
+ view->controller_aim = 0;
+ view->left_hand_valid = 0;
+ if (!frame_active)
return 0;
/* late: the poses the frame is drawn and submitted with */
if (host_vr_locate(&vr.views))
@@ -956,20 +1509,35 @@ int halo_vr_view(struct halo_vr_view *view)
jitter_apply();
test_hands_apply();
}
+ else
+ {
+ vr.render_pose_valid = 0;
+ return 0;
+ }
+ /* Late-located tracking uses the same validation as simulation aim. */
+ if (vr.vehicle.active)
+ halo_vr_vehicle_reference(vr.vehicle.unit, vr.vehicle.vehicle, vr.vehicle.seat, vr.vehicle.forward_yaw);
if (!vr.views.eye[0].valid)
return 0;
+ /* Tracking loss in a calibrated seat must not switch to the saved
+ standing origin. Defer the view until a neutral seat pose is valid. */
+ if (settings.vehicle_first_person && settings.vehicle_seat_calibration &&
+ vr.vehicle.active && (!vr.vehicle.valid || vr.vehicle.orientation_valid < 0))
+ return 0;
head = &vr.views.head;
+ origin_yaw = vehicle_view_active() ? vr.vehicle.origin_yaw : vr.origin_yaw;
+ origin_position = vehicle_view_active() ? vr.vehicle.origin_position : vr.origin_position;
quaternion_rotate(vr.views.eye[0].orientation, forward, v);
- game_axes(v, vr.origin_yaw, view->head_forward);
+ game_axes(v, origin_yaw, view->head_forward);
quaternion_rotate(vr.views.eye[0].orientation, up, v);
- game_axes(v, vr.origin_yaw, view->head_up);
+ game_axes(v, origin_yaw, view->head_up);
/* the head's centre, between the eyes */
for (eye = 0; eye < 3; eye++)
{
centre[eye] = (vr.views.eye[0].position[eye] + vr.views.eye[1].position[eye]) * 0.5f;
- offset[eye] = centre[eye] - vr.origin_position[eye];
+ offset[eye] = centre[eye] - origin_position[eye];
}
- game_axes(offset, vr.origin_yaw, view->head_position);
+ game_axes(offset, origin_yaw, view->head_position);
/* each eye from the centre, in the head's axes */
inverse[0] = -vr.views.eye[0].orientation[0];
inverse[1] = -vr.views.eye[0].orientation[1];
@@ -987,49 +1555,61 @@ int halo_vr_view(struct halo_vr_view *view)
memcpy(view->fov[eye], vr.views.fov[eye], sizeof(view->fov[eye]));
}
(void)head;
- view->body_yaw = vr.body_yaw;
+ view->body_yaw = vehicle_view_active() ? halo_vr_vehicle_heading(&vr.vehicle) : vr.body_yaw;
+ view->vehicle_basis_valid = vehicle_basis(view->vehicle_basis);
view->hud_tangent = tanf(settings.hud_size * 0.5f * (float)M_PI / 180.0f) * (float)HUD_HEIGHT / (float)HUD_WIDTH;
view->world_units_per_metre = settings.world_scale / METRES_PER_WORLD_UNIT;
/* the right controller */
- view->controller_aim = settings.controller_aim && vr.views.aim[1].valid;
+ view->controller_aim = settings.controller_aim && vr.views.aim[1].valid &&
+ halo_vr_locomotion_orientation(vr.views.aim[1].orientation, normalized);
+ for (axis = 0; axis < 3; axis++)
+ if (!isfinite(vr.views.aim[1].position[axis]))
+ view->controller_aim = 0;
if (view->controller_aim)
{
const struct vr_host_pose *hand = &vr.views.aim[1];
float pointing_up[3];
weapon_pointing(v, pointing_up);
- game_axes(v, vr.origin_yaw, view->hand_forward);
- game_axes(pointing_up, vr.origin_yaw, view->hand_up);
+ game_axes(v, origin_yaw, view->hand_forward);
+ game_axes(pointing_up, origin_yaw, view->hand_up);
for (eye = 0; eye < 3; eye++)
- offset[eye] = hand->position[eye] - vr.origin_position[eye];
- game_axes(offset, vr.origin_yaw, view->hand_position);
+ offset[eye] = hand->position[eye] - origin_position[eye];
+ game_axes(offset, origin_yaw, view->hand_position);
}
memcpy(view->weapon_offset, settings.weapon_offset, sizeof(view->weapon_offset));
/* the hands (vr.hands): where the controllers' grips are (else where
they point from), and where the left one points */
view->floating_hands = settings.floating_hands && settings.controller_aim;
view->arms = settings.arms;
- view->left_hand_valid = vr.views.aim[0].valid;
+ view->left_hand_valid = vr.views.aim[0].valid &&
+ halo_vr_locomotion_orientation(vr.views.aim[0].orientation, normalized);
+ for (axis = 0; axis < 3; axis++)
+ if (!isfinite(vr.views.aim[0].position[axis]))
+ view->left_hand_valid = 0;
if (view->floating_hands && view->controller_aim)
{
const struct vr_host_pose *grip = vr.views.grip[1].valid ? &vr.views.grip[1] : &vr.views.aim[1];
for (eye = 0; eye < 3; eye++)
- offset[eye] = grip->position[eye] - vr.origin_position[eye];
- game_axes(offset, vr.origin_yaw, view->right_grip_position);
+ offset[eye] = grip->position[eye] - origin_position[eye];
+ game_axes(offset, origin_yaw, view->right_grip_position);
}
if (view->floating_hands && view->left_hand_valid)
{
const struct vr_host_pose *hand = &vr.views.aim[0];
const struct vr_host_pose *grip = vr.views.grip[0].valid ? &vr.views.grip[0] : hand;
+ for (axis = 0; axis < 3; axis++)
+ if (!isfinite(grip->position[axis]))
+ view->left_hand_valid = 0;
quaternion_rotate(hand->orientation, forward, v);
- game_axes(v, vr.origin_yaw, view->left_hand_forward);
+ game_axes(v, origin_yaw, view->left_hand_forward);
quaternion_rotate(hand->orientation, up, v);
- game_axes(v, vr.origin_yaw, view->left_hand_up);
+ game_axes(v, origin_yaw, view->left_hand_up);
for (eye = 0; eye < 3; eye++)
- offset[eye] = grip->position[eye] - vr.origin_position[eye];
- game_axes(offset, vr.origin_yaw, view->left_grip_position);
+ offset[eye] = grip->position[eye] - origin_position[eye];
+ game_axes(offset, origin_yaw, view->left_grip_position);
}
view->two_handed = vr.two_handed_weight * vr.two_handed_weight * (3.0f - 2.0f * vr.two_handed_weight);
view->right_trigger = vr.input.right_trigger;
@@ -1038,22 +1618,199 @@ int halo_vr_view(struct halo_vr_view *view)
view->seconds = vr.views.display_elapsed > 0.0f ? vr.views.display_elapsed : vr.views.display_period;
view->vehicle_first_person = settings.vehicle_first_person;
view->gamepad_aim = settings.gamepad_aim;
- view->gamepad_pitch = settings.gamepad_aim && settings.gamepad_pitch;
+ view->gamepad_pitch = settings.gamepad_aim && settings.gamepad_pitch && !vehicle_view_active();
return 1;
}
/* ---------- aiming */
+void halo_vr_locomotion(float aim_yaw, float *forward, float *left)
+{
+ static const float forwards[3] = { 0.0f, 0.0f, -1.0f };
+ float orientation[4], pointing[3], facing[3] = { 0.0f, 0.0f, 0.0f };
+ int tracked;
+ settings_read();
+ /* Flat play, head aiming and a gamepad already use the game's correct
+ throttle basis. The optional old aim-relative mode does too. */
+ if (!vr.initialized || !settings.head_movement || !settings.controller_aim || settings.gamepad_aim)
+ {
+ halo_vr_locomotion_reset(&vr.movement_heading);
+ return;
+ }
+ tracked = (vr.frame || vr.pending) && vr.views.should_render && vr.views.focused &&
+ vr.views.head.valid && vr.origin_valid && vr.aim_synced && !vr.menus &&
+ isfinite(vr.origin_yaw) &&
+ halo_vr_locomotion_orientation(vr.views.head.orientation, orientation);
+ if (tracked)
+ {
+ quaternion_rotate(orientation, forwards, pointing);
+ game_axes(pointing, vr.origin_yaw, facing);
+ }
+ halo_vr_locomotion_project(&vr.movement_heading, tracked, facing, vr.body_yaw, aim_yaw, forward, left);
+}
+
+/* Presentation remains tracked during control-disabled first-person scenes.
+ * This deliberately does not grant aiming or tutorial/gameplay input. */
+int halo_vr_presentation_tracking_valid(void)
+{
+ float normalized[4];
+ const float *origin_position = vehicle_view_active() ? vr.vehicle.origin_position : vr.origin_position;
+ int eye, axis;
+ if (!vr.initialized || !(vr.frame || vr.pending) || !vr.views.should_render ||
+ !vr.views.focused || !vr.views.head.valid || vr.menus || !vr.origin_valid ||
+ !isfinite(vr.origin_yaw) || !isfinite(vr.body_yaw) ||
+ !halo_vr_locomotion_orientation(vr.views.head.orientation, normalized) ||
+ (settings.vehicle_first_person && settings.vehicle_seat_calibration && vr.vehicle.active && !vr.vehicle.valid))
+ return 0;
+ for (axis = 0; axis < 3; axis++)
+ if (!isfinite(vr.views.head.position[axis]) || !isfinite(origin_position[axis]))
+ return 0;
+ for (eye = 0; eye < 2; eye++)
+ {
+ if (!vr.views.eye[eye].valid ||
+ !halo_vr_locomotion_orientation(vr.views.eye[eye].orientation, normalized))
+ return 0;
+ for (axis = 0; axis < 3; axis++)
+ if (!isfinite(vr.views.eye[eye].position[axis]))
+ return 0;
+ }
+ return 1;
+}
+
+int halo_vr_gaze_tracking_valid(void)
+{
+ float normalized[4];
+ int eye, axis;
+
+ if (!vr.initialized || !(vr.frame || vr.pending) || !vr.views.should_render ||
+ !vr.views.focused || !vr.views.head.valid || vr.menus || !vr.origin_valid || !vr.aim_synced ||
+ !isfinite(vr.origin_yaw) || !isfinite(vr.body_yaw) ||
+ !halo_vr_locomotion_orientation(vr.views.head.orientation, normalized))
+ return 0;
+ for (eye = 0; eye < 2; eye++)
+ {
+ if (!vr.views.eye[eye].valid ||
+ !halo_vr_locomotion_orientation(vr.views.eye[eye].orientation, normalized))
+ return 0;
+ for (axis = 0; axis < 3; axis++)
+ if (!isfinite(vr.views.eye[eye].position[axis]) || !isfinite(vr.origin_position[axis]))
+ return 0;
+ }
+ return 1;
+}
+
+void halo_vr_turn_control(int allowed)
+{
+ vr.turn_control_allowed = allowed != 0;
+ if (!allowed)
+ {
+ vr.tutorial_turn_flags = 0;
+ vr.tutorial_head_valid = 0;
+ }
+}
+
+int halo_vr_tutorial_turn(void)
+{
+ int flags = vr.tutorial_turn_flags;
+
+ vr.tutorial_turn_flags = 0;
+ return vr.turn_control_allowed && halo_vr_gaze_tracking_valid() ? flags : 0;
+}
+
+int halo_vr_tutorial_look(float *yaw, float *pitch)
+{
+ static const float forwards[3] = { 0.0f, 0.0f, -1.0f };
+ const float threshold = 0.01f; /* about half a degree; ignore tracking jitter */
+ float orientation[4], pointing[3], facing[3], head_yaw, head_pitch;
+
+ *yaw = *pitch = 0.0f;
+ if (!halo_vr_gaze_tracking_valid() || !settings.controller_aim || !vr.turn_control_allowed ||
+ !halo_vr_locomotion_orientation(vr.views.head.orientation, orientation))
+ {
+ vr.tutorial_head_valid = 0;
+ return 0;
+ }
+ /* Use the tracking space, rather than body_yaw or the recentred origin:
+ stick turns and recentring must not count as physical head movement. */
+ quaternion_rotate(orientation, forwards, pointing);
+ game_axes(pointing, 0.0f, facing);
+ head_yaw = atan2f(facing[1], facing[0]);
+ head_pitch = asinf(facing[2] < -1.0f ? -1.0f : facing[2] > 1.0f ? 1.0f : facing[2]);
+ if (!isfinite(head_yaw) || !isfinite(head_pitch))
+ {
+ vr.tutorial_head_valid = 0;
+ return 0;
+ }
+ if (!vr.tutorial_head_valid || !isfinite(vr.tutorial_head_yaw) || !isfinite(vr.tutorial_head_pitch))
+ {
+ vr.tutorial_head_valid = 1;
+ vr.tutorial_head_yaw = head_yaw;
+ vr.tutorial_head_pitch = head_pitch;
+ return 0;
+ }
+ *yaw = remainderf(head_yaw - vr.tutorial_head_yaw, 2.0f * (float)M_PI);
+ *pitch = head_pitch - vr.tutorial_head_pitch;
+ /* Keep each anchor until movement crosses the threshold, so a slow
+ look still counts and sub-threshold jitter does not. */
+ if (fabsf(*yaw) >= threshold)
+ vr.tutorial_head_yaw = head_yaw;
+ else
+ *yaw = 0.0f;
+ if (fabsf(*pitch) >= threshold)
+ vr.tutorial_head_pitch = head_pitch;
+ else
+ *pitch = 0.0f;
+ return *yaw != 0.0f || *pitch != 0.0f;
+}
+
+void halo_vr_tutorial_reset(void)
+{
+ float yaw, pitch;
+
+ vr.tutorial_turn_flags = 0;
+ vr.tutorial_head_valid = 0;
+ (void)halo_vr_tutorial_look(&yaw, &pitch);
+}
+
int halo_vr_aim(float current_yaw, int long_gun, float *yaw, float *pitch)
{
static const float backwards[3] = { 0.0f, 0.0f, -1.0f };
float v[3], f[3], head_yaw;
+ float origin_yaw = vehicle_view_active() ? vr.vehicle.origin_yaw : vr.origin_yaw;
+ float body_yaw = vehicle_view_active() ? halo_vr_vehicle_heading(&vr.vehicle) : vr.body_yaw;
+ int driving_result;
if (!(vr.frame || vr.pending) || !vr.views.should_render || !vr.views.head.valid)
{
vr.aiming = 0;
+ driving_reset(1);
+ driving.accelerator_blocked = 1;
+ return 0;
+ }
+ if (settings.vehicle_first_person && settings.vehicle_seat_calibration &&
+ vr.vehicle.active && (!vr.vehicle.valid || vr.vehicle.orientation_valid < 0))
+ {
+ vr.aiming = 0;
+ driving_reset(1);
+ driving.accelerator_blocked = 1;
return 0;
}
+ driving_result = driving_aim(yaw, pitch);
+ if (driving_result)
+ {
+ if (driving_result < 0)
+ vr.aiming = 0;
+ return driving_result > 0 ? driving_result : 0;
+ }
+ /* A physical gamepad normally keeps the game's facing. On seat entry,
+ align it once as well, so that facing cannot undo the forward capture. */
+ if (settings.gamepad_aim && vehicle_view_active() && !vr.aim_synced)
+ {
+ vr.aim_synced = vr.aiming = 1;
+ *yaw = body_yaw;
+ *pitch = 0.0f;
+ return 1;
+ }
if (settings.gamepad_aim)
{
/* the gamepad aims, as on the flat screen: the facing stays the
@@ -1062,13 +1819,17 @@ int halo_vr_aim(float current_yaw, int long_gun, float *yaw, float *pitch)
of vr.snap_turn_angle each time the aim is more than that from it
("snap"), so the reticle and the weapon move about the view and the
view turns in steps. (The view's pitch: vr_render.c.) */
- float off = remainderf(current_yaw - vr.body_yaw, 2.0f * (float)M_PI);
+ float off = remainderf(current_yaw - body_yaw, 2.0f * (float)M_PI);
float step = settings.snap_angle;
if (!vr.aim_synced || !settings.gamepad_snap || step <= 0.0f || fabsf(off) > 2.0f * step)
- vr.body_yaw = current_yaw;
+ body_yaw = current_yaw;
else if (fabsf(off) > step)
- vr.body_yaw += off > 0.0f ? step : -step;
+ body_yaw += off > 0.0f ? step : -step;
+ if (vehicle_view_active())
+ vr.vehicle.turn = body_yaw - vr.vehicle.forward_yaw;
+ else
+ vr.body_yaw = body_yaw;
vr.aim_synced = 1;
*yaw = current_yaw;
*pitch = 0.0f;
@@ -1076,12 +1837,13 @@ int halo_vr_aim(float current_yaw, int long_gun, float *yaw, float *pitch)
return 2;
}
quaternion_rotate(vr.views.head.orientation, backwards, v);
- game_axes(v, vr.origin_yaw, f);
+ game_axes(v, origin_yaw, f);
head_yaw = atan2f(f[1], f[0]);
if (!vr.aim_synced)
{
/* the turn that keeps the view where the player faced */
- vr.body_yaw = current_yaw - head_yaw;
+ if (!vehicle_view_active())
+ vr.body_yaw = body_yaw = current_yaw - head_yaw;
vr.aim_synced = 1;
}
vr.long_gun = long_gun;
@@ -1092,11 +1854,23 @@ int halo_vr_aim(float current_yaw, int long_gun, float *yaw, float *pitch)
float up[3];
weapon_pointing(v, up);
- game_axes(v, vr.origin_yaw, f);
+ game_axes(v, origin_yaw, f);
head_yaw = atan2f(f[1], f[0]);
}
- *yaw = vr.body_yaw + head_yaw;
- *pitch = asinf(f[2] < -1.0f ? -1.0f : f[2] > 1.0f ? 1.0f : f[2]);
+ {
+ float basis[3][3], world[3];
+ if (vehicle_basis(basis))
+ {
+ halo_vr_vehicle_rotate(basis, f, world);
+ *yaw = atan2f(world[1], world[0]);
+ *pitch = asinf(world[2] < -1.0f ? -1.0f : world[2] > 1.0f ? 1.0f : world[2]);
+ }
+ else
+ {
+ *yaw = body_yaw + head_yaw;
+ *pitch = asinf(f[2] < -1.0f ? -1.0f : f[2] > 1.0f ? 1.0f : f[2]);
+ }
+ }
vr.aiming = 1;
return 1;
}
@@ -1105,11 +1879,191 @@ void halo_vr_aim_release(void)
{
vr.aim_synced = 0;
vr.aiming = 0;
+ halo_vr_locomotion_reset(&vr.movement_heading);
}
void halo_vr_menus(int active)
{
+ active = active != 0;
+ if (active != vr.menus)
+ {
+ vr.menu_open = 0;
+ vr.menu_pose.valid = 0;
+ menu_pointer.active = menu_pointer.armed = menu_pointer.down = 0;
+ menu_pointer.position_valid = 0;
+ }
vr.menus = active;
+ if (active)
+ {
+ driving_reset(1);
+ driving.accelerator_blocked = 1;
+ driving.accelerator_consumed = 0;
+ driving.brake_consumed = 0;
+ vr.tutorial_turn_flags = 0;
+ vr.tutorial_head_valid = 0;
+ }
+}
+
+/* A ray/quad intersection in the fixed menu's play space. OpenXR quads face
+local +Z; accept only a forward ray from that side. The HUD texture is the
+native 640x480 UI at twice its size, so widget coordinates remain unchanged. */
+static int menu_pointer_panel_valid(const struct vr_host_pose *panel, const float size[2])
+{
+ float orientation[4];
+ int axis;
+ if (!panel || !size || !panel->valid || !isfinite(size[0]) || !isfinite(size[1]) ||
+ size[0] <= 0.0001f || size[1] <= 0.0001f ||
+ !halo_vr_locomotion_orientation(panel->orientation, orientation))
+ return 0;
+ for (axis = 0; axis < 3; axis++)
+ if (!isfinite(panel->position[axis]))
+ return 0;
+ return 1;
+}
+
+static int menu_pointer_intersection(const struct vr_host_pose *ray,
+ const struct vr_host_pose *panel, const float size[2], short *x, short *y)
+{
+ static const float forwards[3] = { 0.0f, 0.0f, -1.0f };
+ float ray_orientation[4], panel_orientation[4], inverse[4];
+ float direction[3], offset[3], local_origin[3], local_direction[3];
+ float distance, hit_x, hit_y, pixel_x, pixel_y;
+ int axis;
+ if (!ray || !x || !y || !ray->valid || !menu_pointer_panel_valid(panel, size) ||
+ !halo_vr_locomotion_orientation(ray->orientation, ray_orientation) ||
+ !halo_vr_locomotion_orientation(panel->orientation, panel_orientation))
+ return 0;
+ for (axis = 0; axis < 3; axis++)
+ {
+ if (!isfinite(ray->position[axis]) || !isfinite(panel->position[axis]))
+ return 0;
+ offset[axis] = ray->position[axis] - panel->position[axis];
+ if (!isfinite(offset[axis]))
+ return 0;
+ inverse[axis] = -panel_orientation[axis];
+ }
+ inverse[3] = panel_orientation[3];
+ quaternion_rotate(ray_orientation, forwards, direction);
+ quaternion_rotate(inverse, offset, local_origin);
+ quaternion_rotate(inverse, direction, local_direction);
+ if (!isfinite(local_origin[2]) || !isfinite(local_direction[2]) ||
+ local_origin[2] <= 0.0001f || local_direction[2] >= -0.0001f)
+ return 0;
+ distance = -local_origin[2] / local_direction[2];
+ hit_x = local_origin[0] + distance * local_direction[0];
+ hit_y = local_origin[1] + distance * local_direction[1];
+ if (!isfinite(distance) || distance <= 0.0f || !isfinite(hit_x) || !isfinite(hit_y) ||
+ fabsf(hit_x) > size[0] * 0.5f || fabsf(hit_y) > size[1] * 0.5f)
+ return 0;
+ pixel_x = (hit_x / size[0] + 0.5f) * 640.0f;
+ pixel_y = (0.5f - hit_y / size[1]) * 480.0f;
+ *x = (short)(pixel_x <= 0.0f ? 0 : pixel_x >= 640.0f ? 639 : (int)pixel_x);
+ *y = (short)(pixel_y <= 0.0f ? 0 : pixel_y >= 480.0f ? 479 : (int)pixel_y);
+ return 1;
+}
+
+static int menu_pointer_tracking_valid(void)
+{
+ float orientation[4];
+ int axis;
+ if (!vr.initialized || !(vr.frame || vr.pending) || !vr.views.should_render ||
+ !vr.views.focused || !vr.views.head.valid || !vr.input.active[1] || !vr.views.aim[1].valid ||
+ !isfinite(vr.input.right_trigger) || vr.input.right_trigger < 0.0f || vr.input.right_trigger > 1.0f ||
+ !halo_vr_locomotion_orientation(vr.views.head.orientation, orientation) ||
+ !halo_vr_locomotion_orientation(vr.views.aim[1].orientation, orientation))
+ return 0;
+ for (axis = 0; axis < 3; axis++)
+ if (!isfinite(vr.views.head.position[axis]) || !isfinite(vr.views.aim[1].position[axis]))
+ return 0;
+ return 1;
+}
+
+/* Release is processed even off the panel. Tracking/focus loss disarms the
+next press until a fresh release, rather than inventing a click on recovery. */
+static int menu_pointer_button(int active, int tracked, int on_panel, float trigger)
+{
+ int clicked = 0;
+ if (!active || active != menu_pointer.active || !tracked)
+ {
+ menu_pointer.armed = 0;
+ menu_pointer.down = 0;
+ }
+ menu_pointer.active = active;
+ if (!active || !tracked)
+ return 0;
+ if (trigger <= 0.35f)
+ {
+ menu_pointer.down = 0;
+ menu_pointer.armed = 1;
+ }
+ else if (trigger >= 0.60f && !menu_pointer.down)
+ {
+ clicked = menu_pointer.armed && on_panel;
+ menu_pointer.down = 1;
+ menu_pointer.armed = 0;
+ }
+ return clicked;
+}
+
+/* Return ownership even off-panel: desktop mouse coordinates must not
+replace the VR ray during tracking loss. A stationary ray emits no hover
+move, leaving the existing Xbox A/B/dpad navigation usable. */
+int halo_vr_menu_pointer(int menus_active, struct halo_ui_pointer *pointer)
+{
+ int active = menus_active && vr.initialized && settings.menu_pointer &&
+ (vr.input.active[1] || menu_pointer.active);
+ int tracked = menu_pointer_tracking_valid();
+ int valid = 0, panel_tracked, clicked;
+ short x = -1, y = -1;
+ if (!pointer)
+ return 0;
+ memset(pointer, 0, sizeof(*pointer));
+ pointer->x = pointer->y = pointer->click_x = pointer->click_y = -1;
+ panel_tracked = tracked && vr.menu_open && menu_pointer_panel_valid(&vr.menu_pose, vr.pending_layers.hud_size) &&
+ vr.pending_layers.hud && !vr.pending_layers.hud_head_locked;
+ if (active && panel_tracked)
+ valid = menu_pointer_intersection(&vr.views.aim[1], &vr.menu_pose,
+ vr.pending_layers.hud_size, &x, &y);
+ clicked = menu_pointer_button(active, panel_tracked, valid, vr.input.right_trigger);
+ if (active && tracked && vr.input.right_trigger <= 0.35f)
+ menu_pointer.blocked = 0;
+ else if (active && vr.input.active[1] && isfinite(vr.input.right_trigger) && vr.input.right_trigger > 0.35f)
+ menu_pointer.blocked = 1;
+ pointer->moved = (unsigned char)(valid != menu_pointer.position_valid ||
+ (valid && (x != menu_pointer.x || y != menu_pointer.y)));
+ pointer->x = pointer->click_x = x;
+ pointer->y = pointer->click_y = y;
+ pointer->left_clicks = (unsigned char)clicked;
+ menu_pointer.position_valid = valid;
+ menu_pointer.x = x;
+ menu_pointer.y = y;
+ return active;
+}
+
+int halo_vr_menu_cursor(short *x, short *y)
+{
+ if (!x || !y || !vr.menus || !settings.menu_pointer || !menu_pointer.position_valid ||
+ !vr.menu_open || !menu_pointer_panel_valid(&vr.menu_pose, vr.pending_layers.hud_size) || !menu_pointer_tracking_valid())
+ return 0;
+ *x = menu_pointer.x;
+ *y = menu_pointer.y;
+ return 1;
+}
+
+static int menu_pointer_fire_blocked(void)
+{
+ int controller_menu = vr.menus && settings.menu_pointer &&
+ (vr.input.active[1] || menu_pointer.active);
+ if (controller_menu && vr.input.active[1] && isfinite(vr.input.right_trigger) && vr.input.right_trigger > 0.35f)
+ menu_pointer.blocked = 1;
+ else if (menu_pointer.blocked && menu_pointer_tracking_valid() && vr.input.right_trigger <= 0.35f)
+ menu_pointer.blocked = 0;
+ return controller_menu || menu_pointer.blocked;
+}
+
+int halo_vr_radar_head_heading(void)
+{
+ return vr.initialized && settings.radar_head_heading;
}
int halo_vr_aiming(void)
@@ -1157,20 +2111,50 @@ void halo_vr_gamepad(void *gamepad)
controller, as any gamepad does, their poses aiming nothing) */
if (!vr.initialized || !(vr.frame || vr.pending))
return;
- analog(pad, XINPUT_GAMEPAD_A, buttons & VR_BUTTON_A);
- analog(pad, XINPUT_GAMEPAD_B, (buttons & VR_BUTTON_B) || vr.melee_frames > 0);
+ if (!(buttons & VR_BUTTON_A))
+ driving.accelerator_consumed = 0;
+ /* Steam Input/SDL may have already merged this same controller's A.
+ Consume both copies while driving, and a carried held accelerator on
+ exit, so A cannot apply the stock Warthog brake or an accidental jump.
+ Menus and physical-gamepad aiming retain their usual A behavior. */
+ if (!vr.menus && !settings.gamepad_aim &&
+ ((driving_active() && driving.mode == HALO_VR_DRIVING_WARTHOG) ||
+ (driving.mode == HALO_VR_DRIVING_NONE && driving.accelerator_consumed)))
+ pad->bAnalogButtons[XINPUT_GAMEPAD_A] = 0;
+ else
+ analog(pad, XINPUT_GAMEPAD_A, buttons & VR_BUTTON_A);
+ if (!(buttons & VR_BUTTON_B))
+ driving.brake_consumed = 0;
+ if (!vr.menus && !settings.gamepad_aim &&
+ ((driving_active() && driving.mode == HALO_VR_DRIVING_WARTHOG) ||
+ (driving.mode == HALO_VR_DRIVING_NONE && driving.brake_consumed)))
+ {
+ /* B brakes only through the final vehicle action. Consume duplicate
+ SDL B and physical punches here so neither can leak a melee request. */
+ pad->bAnalogButtons[XINPUT_GAMEPAD_B] = 0;
+ vr.melee_frames = 0;
+ }
+ else
+ analog(pad, XINPUT_GAMEPAD_B, (buttons & VR_BUTTON_B) || vr.melee_frames > 0);
analog(pad, XINPUT_GAMEPAD_X, buttons & VR_BUTTON_X);
analog(pad, XINPUT_GAMEPAD_Y, buttons & VR_BUTTON_Y);
- /* the bumpers where the Xbox's white and black buttons were: the
- flashlight and the grenade switch */
- analog(pad, XINPUT_GAMEPAD_WHITE, buttons & VR_BUTTON_LEFT_BUMPER);
- analog(pad, XINPUT_GAMEPAD_BLACK, buttons & VR_BUTTON_RIGHT_BUMPER);
+ /* Keep the user's Xbox layout: left bumper changes grenade, and the
+ right bumper toggles the flashlight. */
+ analog(pad, XINPUT_GAMEPAD_WHITE, buttons & VR_BUTTON_RIGHT_BUMPER);
+ analog(pad, XINPUT_GAMEPAD_BLACK, buttons & VR_BUTTON_LEFT_BUMPER);
trigger = (BYTE)(vr.input.left_trigger * 255.0f);
if (trigger > pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER])
pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER] = trigger;
- trigger = (BYTE)(vr.input.right_trigger * 255.0f);
- if (trigger > pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER])
- pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER] = trigger;
+ /* SDL may also expose the motion controllers as gamepads. Consume that
+ merged trigger too, through the first tracked release after closing. */
+ if (menu_pointer_fire_blocked())
+ pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER] = 0;
+ else if (isfinite(vr.input.right_trigger) && vr.input.right_trigger >= 0.0f && vr.input.right_trigger <= 1.0f)
+ {
+ trigger = (BYTE)(vr.input.right_trigger * 255.0f);
+ if (trigger > pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER])
+ pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER] = trigger;
+ }
if (buttons & VR_BUTTON_DPAD_UP) pad->wButtons |= XINPUT_GAMEPAD_DPAD_UP;
if (buttons & VR_BUTTON_DPAD_DOWN) pad->wButtons |= XINPUT_GAMEPAD_DPAD_DOWN;
if (buttons & VR_BUTTON_DPAD_LEFT) pad->wButtons |= XINPUT_GAMEPAD_DPAD_LEFT;
@@ -1184,7 +2168,7 @@ void halo_vr_gamepad(void *gamepad)
/* the right stick turns (input_update); while the aim does not follow
the head (menus, cinematics, a vehicle's turret without it), and always
with vr.aim = "gamepad", it looks as a gamepad's would */
- if (!vr.aimed || settings.gamepad_aim)
+ if (!vr.aimed || settings.gamepad_aim || driving_active())
{
stick_merge(&pad->sThumbRX, vr.input.right_stick[0]);
stick_merge(&pad->sThumbRY, vr.input.right_stick[1]);
diff --git a/port/linux/src/vr.h b/port/linux/src/vr.h
index 9f78308d963134ea03d74c3b856263ae430af933..483ec1f8f50ddd601835b9c6ac6a5f934a9dfad3 100644
--- a/port/linux/src/vr.h
+++ b/port/linux/src/vr.h
@@ -39,6 +39,9 @@ struct halo_vr_view
float fov[2][4];
/* the turn of the controllers' stick (radians, the game's yaw) */
float body_yaw;
+ /* Current interpolated seat/hull rotation, shared with authoritative aim. */
+ int vehicle_basis_valid;
+ float vehicle_basis[3][3];
/* the HUD's layer: the tangent of half its vertical field of view, so
that its camera's projection matches (waypoints, the reticle) */
float hud_tangent;
@@ -84,6 +87,9 @@ struct halo_vr_view
/* vr.gamepad_view = "camera": the view is pitched with the facing too
(before the head's turn), as the flat screen's camera is */
int gamepad_pitch;
+ /* Controller mode owns its weapon/reticle even while a fresh pose is
+ unavailable; losing tracking must not turn it into head aiming. */
+ int controller_aim_requested;
};
/* ---------- vr.c */
@@ -106,6 +112,30 @@ stays the game's (vr.aim = "gamepad"); 0 when it does not (no VR frame, a
cinematic). With a long gun, the left hand on its foregrip aims it along
both hands (vr.two_handed). */
int halo_vr_aim(float current_yaw, int long_gun, float *yaw, float *pitch);
+/* On-foot local input only, after aim updates: rotate forward/left throttle
+to the head's horizontal heading without changing weapon aim. */
+void halo_vr_locomotion(float aim_yaw, float *forward, float *left);
+/* Physical head movement for the campaign's tutorial look-direction tests;
+does not change controller aiming or the player's facing. */
+int halo_vr_tutorial_look(float *yaw, float *pitch);
+/* Fresh, finite, focused HMD/eye tracking for game-side world-space gaze. */
+int halo_vr_gaze_tracking_valid(void);
+/* Focused finite display tracking, independent of gameplay aim/control.
+ * For the visible local first-person rig during a scripted arrival. */
+int halo_vr_presentation_tracking_valid(void);
+/* Game free-look gates apply to platform turns too; disabled gameplay input
+alone does not disable free-look (the cryopod tutorial). */
+void halo_vr_turn_control(int allowed);
+enum
+{
+ HALO_VR_TUTORIAL_TURN_LEFT = 1,
+ HALO_VR_TUTORIAL_TURN_RIGHT = 2,
+};
+/* Deliberate view turns since the last call, excluding recentering. */
+int halo_vr_tutorial_turn(void);
+/* The script's action reset clears recorded turns and rebases physical head
+motion at the current pose, rather than carrying a prior prompt's anchor. */
+void halo_vr_tutorial_reset(void);
/* the aim no longer follows the head (a cinematic, a vehicle's camera
taking over): the next halo_vr_aim starts again from the player's facing */
void halo_vr_aim_release(void);
@@ -123,12 +153,48 @@ unsigned int halo_vr_image(int swapchain);
/* the frame is presented with what was drawn (it ends as the next begins,
which halo_vr_frame_active waits for, as vsync would) */
void halo_vr_present(int eyes_drawn, int hud_drawn);
+/* Clear before selecting a world camera; commit its exact raw eye/FOV
+sample only after successful camera validation, before auxiliary locates. */
+int halo_vr_render_view_commit(int valid);
/* the controllers as port 0's gamepad (an XINPUT_GAMEPAD), merged in */
void halo_vr_gamepad(void *gamepad);
void halo_vr_rumble(float left, float right);
/* the menus are up: their layer stays in front of the recentred place,
not of the head */
void halo_vr_menus(int active);
+/* Controller ray against the captured menu quad; native 640x480 UI pixels.
+Return 1 while VR owns the pointer, including off-panel or tracking loss. */
+struct halo_ui_pointer;
+int halo_vr_menu_pointer(int menus_active, struct halo_ui_pointer *pointer);
+int halo_vr_menu_cursor(short *x, short *y);
+int halo_vr_radar_head_heading(void);
+/* Local first-person seat identity and current world heading; -1 exits.
+The neutral seated tracking reference never overwrites the on-foot origin. */
+void halo_vr_vehicle_reference(int unit, int vehicle, int seat, float forward_yaw);
+/* Root rotation only; the seat's captured yaw/calibration remain independent. */
+void halo_vr_vehicle_orientation(const float axes[3][3], float hull_yaw);
+int halo_vr_vehicle_clear_windshield(void);
+enum
+{
+ HALO_VR_DRIVING_NONE = 0,
+ HALO_VR_DRIVING_WARTHOG = 1,
+ HALO_VR_DRIVING_GHOST = 2,
+ HALO_VR_DRIVING_SCORPION = 3,
+ HALO_VR_DRIVING_BANSHEE = 4,
+};
+/* Register only an owned, controllable stock driver; NONE clears it. */
+void halo_vr_vehicle_controls(int mode, float hull_yaw);
+/* Physical gamepads retain their original path; headset drivers require
+fresh focused tracking. The game can neutralize stale controls on loss. */
+int halo_vr_vehicle_controls_ready(void);
+/* Processed lateral is positive left. 0: inactive, 1: steering, 2: handled
+neutral after tracking/focus/menu loss. Reverse is handled by native physics. */
+int halo_vr_vehicle_steering(float hull_yaw, float lateral, float seconds, float *yaw, float *pitch);
+/* Apply Warthog A acceleration after radial stick normalization. Backward
+stick takes priority; menus/tracking loss require a fresh A release. */
+float halo_vr_vehicle_accelerator(float stick_forward);
+/* B applies the stock Warthog brake only for a fresh tracked driver. */
+int halo_vr_vehicle_brake(void);
/* the aim follows the head: the controller's magnetism leaves it alone */
int halo_vr_aiming(void);
/* the aim is the gamepad's (vr.aim = "gamepad"), magnetism and all */
@@ -153,6 +219,9 @@ void halo_vr_set_eye_transforms(const float transforms[2][16]);
from the centre view's (identity: 1, 1, 0, 0), for what the game projects
to the screen itself (vr_render.c, vr_render_screen_point) */
void halo_vr_set_screen_corrections(const float corrections[2][4]);
+/* Already projected for an individual eye: screen coordinates bypass the
+world clip correction even when their scale/offset is identity. */
+void halo_vr_screen_space(int active);
/* the draws from now on go into that eye's layer alone (0 left, 1 right),
or both at once (-1, multiview) */
void halo_vr_draw_eye(int eye);
diff --git a/port/linux/src/vr_driving.h b/port/linux/src/vr_driving.h
new file mode 100644
index 0000000000000000000000000000000000000000..f3066785289536445614b70d626e140751618460
--- /dev/null
+++ b/port/linux/src/vr_driving.h
@@ -0,0 +1,107 @@
+/* Deterministic steering math shared by the live VR path and host tests. */
+#ifndef HALO_VR_DRIVING_H
+#define HALO_VR_DRIVING_H
+#include <math.h>
+#include <string.h>
+
+struct halo_vr_driving_state
+{
+ int mode, align, turn_blocked;
+ /* A is an independent accelerator, armed only after a fresh release. */
+ int accelerator_blocked, accelerator_consumed;
+ int brake_consumed;
+ float hull_yaw;
+ int wheel_valid, wheel_blocked;
+ float wheel_axis[3], wheel_line[3], wheel_steer;
+};
+
+static inline float halo_vr_driving_curve(float lateral)
+{
+ float amount;
+ if (!isfinite(lateral))
+ return 0.0f;
+ amount = fabsf(lateral);
+ if (amount <= 0.10f)
+ return 0.0f;
+ amount = (amount - 0.10f) / 0.90f;
+ if (amount > 1.0f)
+ amount = 1.0f;
+ amount = 0.40f * amount + 0.60f * amount * amount * amount;
+ return lateral < 0.0f ? -amount : amount;
+}
+
+/* Project the left-to-right hand line into the wheel's captured plane.
+The captured axis lets the driver look around without steering the wheel. */
+static inline int halo_vr_driving_line(const float axis[3], const float left[3],
+ const float right[3], float line[3])
+{
+ float distance = 0.0f, along = 0.0f, length = 0.0f;
+ int component;
+ for (component = 0; component < 3; component++)
+ {
+ if (!isfinite(axis[component]) || !isfinite(left[component]) || !isfinite(right[component]))
+ return 0;
+ line[component] = right[component] - left[component];
+ distance += line[component] * line[component];
+ along += line[component] * axis[component];
+ }
+ if (!isfinite(distance) || distance < 0.10f * 0.10f || distance > 1.20f * 1.20f)
+ return 0;
+ for (component = 0; component < 3; component++)
+ {
+ line[component] -= along * axis[component];
+ length += line[component] * line[component];
+ }
+ if (!isfinite(length) || length < 0.05f * 0.05f)
+ return 0;
+ length = sqrtf(length);
+ for (component = 0; component < 3; component++)
+ line[component] /= length;
+ return 1;
+}
+
+static inline int halo_vr_driving_wheel(struct halo_vr_driving_state *state,
+ const float head_forward[3], const float left[3], const float right[3])
+{
+ float axis[3], line[3], cross[3], length = 0.0f, along = 0.0f, across = 0.0f, angle;
+ int component;
+ for (component = 0; component < 3; component++)
+ {
+ axis[component] = state->wheel_valid ? state->wheel_axis[component] : head_forward[component];
+ if (!isfinite(axis[component]))
+ return 0;
+ length += axis[component] * axis[component];
+ }
+ if (!isfinite(length) || length < 0.000001f)
+ return 0;
+ length = sqrtf(length);
+ for (component = 0; component < 3; component++)
+ axis[component] /= length;
+ if (!halo_vr_driving_line(axis, left, right, line))
+ return 0;
+ if (!state->wheel_valid)
+ {
+ memcpy(state->wheel_axis, axis, sizeof(axis));
+ memcpy(state->wheel_line, line, sizeof(line));
+ state->wheel_valid = 1;
+ state->wheel_steer = 0.0f;
+ return 1;
+ }
+ cross[0] = state->wheel_line[1] * line[2] - state->wheel_line[2] * line[1];
+ cross[1] = state->wheel_line[2] * line[0] - state->wheel_line[0] * line[2];
+ cross[2] = state->wheel_line[0] * line[1] - state->wheel_line[1] * line[0];
+ for (component = 0; component < 3; component++)
+ {
+ along += state->wheel_line[component] * line[component];
+ across += axis[component] * cross[component];
+ }
+ angle = atan2f(across, along);
+ if (!isfinite(angle))
+ return 0;
+ /* Clockwise about head-forward means right, hence negative Halo yaw. */
+ state->wheel_steer = -angle / 0.7853981633974483f;
+ if (state->wheel_steer < -1.0f) state->wheel_steer = -1.0f;
+ if (state->wheel_steer > 1.0f) state->wheel_steer = 1.0f;
+ return 1;
+}
+#endif
diff --git a/port/linux/src/vr_host.h b/port/linux/src/vr_host.h
index 1ac24b436c27c6ff30e7d8008ec9dd257c50e7fd..a28cbb242dfd881b0144e75a1d3fc26768a034f6 100644
--- a/port/linux/src/vr_host.h
+++ b/port/linux/src/vr_host.h
@@ -22,6 +22,7 @@ enum
VR_SWAPCHAIN_EYES = 0, /* a 2-layer texture array: layer 0 the left eye */
VR_SWAPCHAIN_HUD, /* the HUD and the menus, a quad layer */
VR_SWAPCHAIN_DEPTH, /* the eyes' depth (XR_KHR_composition_layer_depth), 2 layers */
+ VR_SWAPCHAIN_RESERVED, /* unused former optic slot; never created or submitted */
VR_SWAPCHAIN_COUNT,
VR_SWAPCHAIN_IMAGES = 4,
};
@@ -112,6 +113,10 @@ struct vr_host_layers
int hud_head_locked;
struct vr_host_pose hud_pose;
float hud_size[2];
+ /* Reserved former optic payload; zeroed by the guest and ignored by the host. */
+ int reserved_optic;
+ struct vr_host_pose reserved_optic_pose;
+ float reserved_optic_size[2];
};
/* after the GL context is made and current (on its thread); 0 if there is
diff --git a/port/linux/src/vr_locomotion.h b/port/linux/src/vr_locomotion.h
new file mode 100644
index 0000000000000000000000000000000000000000..9fb2eeb599ae20ed8e74d4e72a703379e8b7d76c
--- /dev/null
+++ b/port/linux/src/vr_locomotion.h
@@ -0,0 +1,86 @@
+/* Independent head-relative walking. Throttle axes are forward and left;
+the engine applies them relative to the player's (weapon) facing. Keep this
+math separate from OpenXR so the production code can be tested on a host. */
+#ifndef HALO_VR_LOCOMOTION_H
+#define HALO_VR_LOCOMOTION_H
+
+#include <math.h>
+
+struct halo_vr_locomotion_heading
+{
+ int valid;
+ float yaw;
+};
+
+static inline void halo_vr_locomotion_reset(struct halo_vr_locomotion_heading *heading)
+{
+ heading->valid = 0;
+ heading->yaw = 0.0f;
+}
+
+/* A tracking quaternion must be finite and reasonably close to unit length.
+Normalize its small drift before projecting the head's forward direction. */
+static inline int halo_vr_locomotion_orientation(const float pose[4], float normalized[4])
+{
+ float length_squared = 0.0f;
+ float inverse_length;
+ int index;
+ for (index = 0; index < 4; index++)
+ {
+ if (!isfinite(pose[index]))
+ return 0;
+ length_squared += pose[index] * pose[index];
+ }
+ if (!isfinite(length_squared) || length_squared < 0.5f || length_squared > 1.5f)
+ return 0;
+ inverse_length = 1.0f / sqrtf(length_squared);
+ for (index = 0; index < 4; index++)
+ normalized[index] = pose[index] * inverse_length;
+ return 1;
+}
+
+/* Head forward is in the recentred game axes. At nearly vertical pitch its
+horizontal projection has no reliable yaw: keep the last usable heading,
+or wait for one. Tracking loss invalidates that history and stops walking. */
+static inline int halo_vr_locomotion_project(struct halo_vr_locomotion_heading *heading,
+ int tracked, const float head_forward[3], float body_yaw, float aim_yaw,
+ float *forward, float *left)
+{
+ const float tau = 6.2831853071795864769f;
+ float horizontal_squared, length_squared, delta, cosine, sine, original_forward, original_left;
+ if (!tracked || !isfinite(body_yaw) || !isfinite(aim_yaw) ||
+ !isfinite(*forward) || !isfinite(*left) || !isfinite(head_forward[0]) ||
+ !isfinite(head_forward[1]) || !isfinite(head_forward[2]))
+ goto invalid;
+ horizontal_squared = head_forward[0] * head_forward[0] + head_forward[1] * head_forward[1];
+ length_squared = horizontal_squared + head_forward[2] * head_forward[2];
+ if (!isfinite(length_squared) || length_squared < 0.5f || length_squared > 1.5f)
+ goto invalid;
+ if (horizontal_squared >= 0.01f)
+ {
+ heading->yaw = atan2f(head_forward[1], head_forward[0]);
+ heading->valid = 1;
+ }
+ if (!heading->valid || !isfinite(heading->yaw))
+ goto invalid;
+ delta = remainderf(body_yaw + heading->yaw - aim_yaw, tau);
+ if (!isfinite(delta))
+ goto invalid;
+ cosine = cosf(delta);
+ sine = sinf(delta);
+ original_forward = *forward;
+ original_left = *left;
+ *forward = cosine * original_forward - sine * original_left;
+ *left = sine * original_forward + cosine * original_left;
+ if (!isfinite(*forward) || !isfinite(*left))
+ goto invalid;
+ return 1;
+
+invalid:
+ halo_vr_locomotion_reset(heading);
+ *forward = 0.0f;
+ *left = 0.0f;
+ return 0;
+}
+
+#endif
diff --git a/port/linux/src/vr_vehicle.h b/port/linux/src/vr_vehicle.h
new file mode 100644
index 0000000000000000000000000000000000000000..997d6b351e359b07b4d565534921fe0910d5349c
--- /dev/null
+++ b/port/linux/src/vr_vehicle.h
@@ -0,0 +1,171 @@
+/* A seat owns a tracking reference separate from the saved walking origin.
+ * Plain C math/state only, shared by production VR and the host regression. */
+#ifndef HALO_VR_VEHICLE_H
+#define HALO_VR_VEHICLE_H
+#include <math.h>
+#include <string.h>
+
+struct halo_vr_vehicle_reference
+{
+ int active, valid;
+ int unit, vehicle, seat;
+ float origin_position[3], origin_yaw;
+ float forward_yaw, turn;
+ /* 0 before a hull hook, -1 for invalid data, 1 for a rigid rotation. */
+ int orientation_valid;
+ float hull_axes[3][3], hull_yaw;
+};
+
+/* Axes are forward, left, up. Reject stretched/singular data, then remove
+ * small interpolation rounding with an orthonormal, proper rotation. */
+static inline int halo_vr_vehicle_basis(const float axes[3][3], float hull_yaw,
+ float seat_yaw, float basis[3][3])
+{
+ float forward[3], left[3], up[3], length, dot, c, s;
+ int row, other, axis;
+ if (!axes || !basis || !isfinite(hull_yaw) || !isfinite(seat_yaw))
+ return 0;
+ for (row = 0; row < 3; row++)
+ for (other = row; other < 3; other++)
+ {
+ dot = 0.0f;
+ for (axis = 0; axis < 3; axis++)
+ {
+ if (!isfinite(axes[row][axis]) || !isfinite(axes[other][axis]))
+ return 0;
+ dot += axes[row][axis] * axes[other][axis];
+ }
+ if (!isfinite(dot) || fabsf(dot - (row == other ? 1.0f : 0.0f)) > 0.01f)
+ return 0;
+ }
+ length = sqrtf(axes[0][0] * axes[0][0] + axes[0][1] * axes[0][1] + axes[0][2] * axes[0][2]);
+ for (axis = 0; axis < 3; axis++)
+ forward[axis] = axes[0][axis] / length;
+ left[0] = axes[2][1] * forward[2] - axes[2][2] * forward[1];
+ left[1] = axes[2][2] * forward[0] - axes[2][0] * forward[2];
+ left[2] = axes[2][0] * forward[1] - axes[2][1] * forward[0];
+ length = sqrtf(left[0] * left[0] + left[1] * left[1] + left[2] * left[2]);
+ if (!isfinite(length) || length < 0.0001f)
+ return 0;
+ for (axis = 0; axis < 3; axis++)
+ left[axis] /= length;
+ up[0] = forward[1] * left[2] - forward[2] * left[1];
+ up[1] = forward[2] * left[0] - forward[0] * left[2];
+ up[2] = forward[0] * left[1] - forward[1] * left[0];
+ dot = remainderf(seat_yaw, 6.28318530718f) - remainderf(hull_yaw, 6.28318530718f);
+ c = cosf(dot);
+ s = sinf(dot);
+ for (axis = 0; axis < 3; axis++)
+ {
+ basis[0][axis] = forward[axis] * c + left[axis] * s;
+ basis[1][axis] = left[axis] * c - forward[axis] * s;
+ basis[2][axis] = up[axis];
+ }
+ return 1;
+}
+
+static inline void halo_vr_vehicle_rotate(const float basis[3][3],
+ const float local[3], float world[3])
+{
+ int axis;
+ for (axis = 0; axis < 3; axis++)
+ world[axis] = basis[0][axis] * local[0] + basis[1][axis] * local[1] + basis[2][axis] * local[2];
+}
+
+/* A centre cone must contain translated eye cones at its near plane too.
+ * Eye x is engine-left, whereas the horizontal FOV tangents are right. */
+static inline int halo_vr_stereo_union(const float fov[2][4], const float offset[2][3],
+ float units, float near_plane, float margin, float bounds[4])
+{
+ int eye, edge;
+ float x, y, value[4];
+ if (!isfinite(units) || units <= 0.0f || !isfinite(near_plane) || near_plane < 0.000001f ||
+ !isfinite(margin) || margin < 1.0f)
+ return 0;
+ for (eye = 0; eye < 2; eye++)
+ {
+ if (!isfinite(offset[eye][0]) || !isfinite(offset[eye][1]) || !isfinite(offset[eye][2]))
+ return 0;
+ for (edge = 0; edge < 4; edge++)
+ if (!isfinite(fov[eye][edge]))
+ return 0;
+ if (fov[eye][0] >= fov[eye][1] || fov[eye][3] >= fov[eye][2])
+ return 0;
+ x = -offset[eye][1] * units / near_plane;
+ y = offset[eye][2] * units / near_plane;
+ value[0] = fov[eye][0] + x;
+ value[1] = fov[eye][1] + x;
+ value[2] = fov[eye][2] + y;
+ value[3] = fov[eye][3] + y;
+ for (edge = 0; edge < 4; edge++)
+ if ((edge == 0 || edge == 3) ? value[edge] > fov[eye][edge] : value[edge] < fov[eye][edge])
+ value[edge] = fov[eye][edge];
+ for (edge = 0; edge < 4; edge++)
+ {
+ if (!isfinite(value[edge]))
+ return 0;
+ if (!eye || ((edge == 0 || edge == 3) ? value[edge] < bounds[edge] : value[edge] > bounds[edge]))
+ bounds[edge] = value[edge];
+ }
+ }
+ /* Expand outward even for asymmetric fields entirely on one side. */
+ for (edge = 0; edge < 4; edge++)
+ {
+ bounds[edge] += ((edge == 0 || edge == 3) ? -1.0f : 1.0f) * fabsf(bounds[edge]) * (margin - 1.0f);
+ if (!isfinite(bounds[edge]))
+ return 0;
+ }
+ return bounds[0] < bounds[1] && bounds[3] < bounds[2] && bounds[2] > 0.0f && bounds[3] < 0.0f;
+}
+
+static inline int halo_vr_vehicle_capture(struct halo_vr_vehicle_reference *state,
+ const float position[3], float yaw)
+{
+ int axis;
+ if (!state->active || !isfinite(yaw))
+ return 0;
+ for (axis = 0; axis < 3; axis++)
+ if (!isfinite(position[axis]))
+ return 0;
+ memcpy(state->origin_position, position, sizeof(state->origin_position));
+ state->origin_yaw = yaw;
+ state->turn = 0.0f;
+ state->valid = 1;
+ return 1;
+}
+
+/* Return a transition so aim synchronisation can be restarted on exit.
+ * Missing tracking defers entry capture; it never captures every frame. */
+static inline int halo_vr_vehicle_update(struct halo_vr_vehicle_reference *state,
+ int unit, int vehicle, int seat, float yaw, int tracked,
+ const float position[3], float head_yaw)
+{
+ int changed;
+ if (unit == -1 || vehicle == -1 || seat < 0 || !isfinite(yaw))
+ {
+ changed = state->active;
+ memset(state, 0, sizeof(*state));
+ return changed;
+ }
+ changed = !state->active || state->unit != unit || state->vehicle != vehicle || state->seat != seat;
+ if (changed)
+ {
+ memset(state, 0, sizeof(*state));
+ state->active = 1;
+ state->unit = unit;
+ state->vehicle = vehicle;
+ state->seat = seat;
+ }
+ state->forward_yaw = yaw;
+ if (!tracked)
+ state->valid = 0;
+ else if (!state->valid)
+ halo_vr_vehicle_capture(state, position, head_yaw);
+ return changed;
+}
+
+static inline float halo_vr_vehicle_heading(const struct halo_vr_vehicle_reference *state)
+{
+ return state->forward_yaw + state->turn;
+}
+#endif
diff --git a/port/linux/src/xgpu.h b/port/linux/src/xgpu.h
index 27a29747ac09406a721d886ef8775912c1f82b71..493b65566f2ff826b8a9dfc2e3d8efc2c9b83de7 100644
--- a/port/linux/src/xgpu.h
+++ b/port/linux/src/xgpu.h
@@ -45,6 +45,10 @@ void host_gl_wait_frame(unsigned int slot);
int host_gl_map_results(unsigned int buffer, unsigned int size);
void host_gl_read_results(void *data, unsigned int size);
void host_gl_read_buffer(unsigned int buffer, unsigned int offset, unsigned int size, void *data);
+unsigned int host_gl_buffer_create_persistent(unsigned int size);
+void host_gl_buffer_destroy_persistent(unsigned int buffer);
+int host_gl_buffer_write_persistent(unsigned int buffer, unsigned int offset, unsigned int size, const void *data);
+int host_gl_wait_frame_checked(unsigned int slot);
#endif
/* ---------- GL state
@@ -135,7 +139,10 @@ struct nv2a_pixel_shader_key
/* drawn into both eyes at once (the VR mode, d3d8_gl.c): layered
render targets are sampled at the view's layer (XGPU_MULTIVIEW_*) */
unsigned char multiview;
- unsigned char reserved[3];
+ /* A mono scratch pass copying a layered source still selects an eye;
+ this is independent of the destination's multiview geometry. */
+ unsigned char sample_eye;
+ unsigned char reserved[2];
};
char *nv2a_pixel_shader_to_glsl(const struct nv2a_pixel_shader_key *key);
diff --git a/source/game/aim_assist.c b/source/game/aim_assist.c
index dd5c80fab626818dc7dede27c44f3b07e7884358..7b94680c99e554921376d467ada09807f8eb5031 100644
--- a/source/game/aim_assist.c
+++ b/source/game/aim_assist.c
@@ -539,15 +539,20 @@ boolean aim_assist(
return FALSE;
}
-long player_aim_projectile(
+static long player_aim_projectile_internal(
long player_index,
real_point3d const *position,
- real_vector3d *direction)
+ real_vector3d *direction,
+ boolean record_target,
+ struct projectile_preview_pose const *pose)
{
long target_object_index= NONE;
struct player_datum *player= player_get(player_index);
long aiming_unit_index= unit_get_aiming_unit_index(player->unit_index);
struct aim_assist_parameters parameters;
+#ifndef HALO_VR
+ (void)pose;
+#endif
match_assert_valid_real_normal3d(
"c:\\halo\\SOURCE\\game\\aim_assist.c",
@@ -567,8 +572,19 @@ long player_aim_projectile(
real_vector3d target_direction;
real_vector3d collision_direction;
real autoaim_level;
+#ifdef HALO_VR
+ real_point3d simulation_camera_position;
+#endif
director_camera_deterministic(player->unit_index, &camera_position, &camera_direction);
+#ifdef HALO_VR
+ simulation_camera_position = camera_position;
+ if (pose)
+ {
+ camera_position = pose->camera_position;
+ camera_direction = pose->forward;
+ }
+#endif
target_direction= *direction;
autoaim_level= 0.f;
@@ -598,6 +614,12 @@ long player_aim_projectile(
real camera_to_unit_distance;
vector_from_points3d(&unit->object.position, &camera_position, &camera_to_unit);
+#ifdef HALO_VR
+ /* Keep the deterministic camera-height displacement. An
+ interpolated eye against a raw unit origin would add motion lag. */
+ if (pose)
+ vector_from_points3d(&unit->object.position, &simulation_camera_position, &camera_to_unit);
+#endif
camera_to_unit_distance= magnitude3d(&camera_to_unit);
camera_displacement= camera_direction;
normalize3d(&camera_displacement);
@@ -631,12 +653,36 @@ long player_aim_projectile(
global_current_collision_user_depth > 1);
--global_current_collision_user_depth;
- player->aim_assist_unit_index= target_object_index;
- player->aim_assist_timestamp= game_time_get();
+ if (record_target)
+ {
+ player->aim_assist_unit_index= target_object_index;
+ player->aim_assist_timestamp= game_time_get();
+ }
return target_object_index;
}
+long player_aim_projectile(
+ long player_index,
+ real_point3d const *position,
+ real_vector3d *direction)
+{
+ return player_aim_projectile_internal(player_index, position, direction, TRUE, NULL);
+}
+
+/* A reticle uses the same nominal direction calculation as a shot, without
+recording a target or advancing the player's targeting timestamp. */
+#ifdef HALO_VR
+long player_preview_projectile_aim(
+ long player_index,
+ real_point3d const *position,
+ real_vector3d *direction,
+ struct projectile_preview_pose const *pose)
+{
+ return player_aim_projectile_internal(player_index, position, direction, FALSE, pose);
+}
+#endif
+
long local_player_aim_assist(
short local_player_index,
real *autoaim_level,
diff --git a/source/game/aim_assist.h b/source/game/aim_assist.h
index f5570893c4325fc94c6fd32e96e1a62d9443a318..6a6a22cbaeb975a928b18d989ac13935fd42f0c5 100644
--- a/source/game/aim_assist.h
+++ b/source/game/aim_assist.h
@@ -19,6 +19,7 @@ header included in hcex build.
/* ---------- structures */
struct aim_assist_target;
+struct projectile_preview_pose;
struct aim_assist_parameters
{
@@ -63,6 +64,15 @@ long player_aim_projectile(
long player_index,
real_point3d const *position,
real_vector3d *direction);
+/* Same direction calculation as player_aim_projectile, without changing
+the player's recorded aim-assist target or timestamp. */
+#ifdef HALO_VR
+long player_preview_projectile_aim(
+ long player_index,
+ real_point3d const *position,
+ real_vector3d *direction,
+ struct projectile_preview_pose const *pose);
+#endif
long local_player_aim_assist(
short local_player_index,
real *autoaim_level,
diff --git a/source/game/player_control.c b/source/game/player_control.c
index f954ef16dd217cefa37f2988d4269f8c4cf6db3d..d95c3443269ea083498e6a96a0d59bf99c914694 100644
--- a/source/game/player_control.c
+++ b/source/game/player_control.c
@@ -211,6 +211,11 @@ symbols in this file:
#include "real_math.h"
#include "network_coop.h" /* port: port/linux/game/network_coop.c */
+#ifdef HALO_VR
+#include "../../port/linux/src/vr.h"
+#include "interface/ui_widget.h"
+#endif
+
/* ---------- constants */
enum control_button
@@ -603,6 +608,42 @@ static void player_action_clear(
return;
}
+#ifdef HALO_VR
+/* Register before the game consumes cached input and prepares physical aim.
+The next platform poll uses this mode; entry alignment makes the first cached
+sample safe. Refresh after unit changes, even when unit-specific aim is skipped. */
+static int player_control_vr_vehicle_controls(long unit_index, float *hull_yaw)
+{
+ static struct { boolean valid; long unit, parent; short seat; } identity;
+ extern int vr_render_vehicle_driving(long unit_index, float *hull_yaw);
+ struct unit_datum *unit;
+ int mode = HALO_VR_DRIVING_NONE;
+ *hull_yaw = 0.0f;
+ if (player_control_camera_control_is_active())
+ mode = vr_render_vehicle_driving(unit_index, hull_yaw);
+ if (mode != HALO_VR_DRIVING_NONE && (unit = unit_try_and_get(unit_index)))
+ {
+ /* A camera option may disable seat calibration. Still reset wheel
+ capture and entry alignment for a new salted unit/parent/seat. */
+ if (identity.valid && (identity.unit != unit_index ||
+ identity.parent != unit->object.parent_object_index || identity.seat != unit->unit.parent_seat_index))
+ halo_vr_vehicle_controls(HALO_VR_DRIVING_NONE, 0.0f);
+ identity.unit = unit_index;
+ identity.parent = unit->object.parent_object_index;
+ identity.seat = unit->unit.parent_seat_index;
+ identity.valid = TRUE;
+ }
+ else
+ {
+ identity.valid = FALSE;
+ mode = HALO_VR_DRIVING_NONE;
+ *hull_yaw = 0.0f;
+ }
+ halo_vr_vehicle_controls(mode, *hull_yaw);
+ return mode;
+}
+#endif
+
static void handle_one_player_input(
short local_player_index,
real time_delta_sec)
@@ -614,11 +655,28 @@ static void handle_one_player_input(
struct game_globals_player_control);
struct input_blob input;
long current_weapon_index;
+#ifdef HALO_VR
+ int vr_driving_mode = HALO_VR_DRIVING_NONE;
+ float vr_hull_yaw = 0.0f;
+#endif
csmemset(
&input,
0xFA,
sizeof(input));
+#ifdef HALO_VR
+ if (local_player_index == 0)
+ {
+ extern void halo_vr_turn_control(int allowed);
+ struct unit_datum *vr_unit = unit_try_and_get(player->unit_index);
+ vr_driving_mode = player_control_vr_vehicle_controls(player->unit_index, &vr_hull_yaw);
+ /* player_enable_input(false) still allows the tutorial's free-look. */
+ halo_vr_turn_control(vr_unit &&
+ !TEST_FLAG(vr_unit->object.damage_flags, _object_dead_bit) &&
+ player_control_camera_control_is_active() && !director_inhibited_facing(local_player_index) &&
+ !cinematic_in_progress());
+ }
+#endif
get_local_player_input_blob(
local_player_index,
time_delta_sec,
@@ -663,6 +721,12 @@ static void handle_one_player_input(
}
}
+ /* Debug unit changes and disappearing units must clear driving mode in
+ this same input update, including when the unit-specific aim block is skipped. */
+#ifdef HALO_VR
+ if (local_player_index == 0)
+ vr_driving_mode = player_control_vr_vehicle_controls(player->unit_index, &vr_hull_yaw);
+#endif
if (player->unit_index != NONE)
{
struct unit_datum *unit = unit_get(player->unit_index);
@@ -747,20 +811,73 @@ static void handle_one_player_input(
and the netcode sends an ordinary player's facing */
extern int halo_vr_aim(float current_yaw, int long_gun, float *yaw, float *pitch);
extern void halo_vr_aim_release(void);
+ extern void halo_vr_locomotion(float aim_yaw, float *forward, float *left);
extern boolean vr_render_long_gun(long weapon_index);
+ extern void vr_render_vehicle_reference(long unit_index);
float vr_yaw, vr_pitch;
int vr_aim = 0;
+ if (local_player_index == 0)
+ {
+ vr_render_vehicle_reference(player->unit_index);
+ }
/* (a long gun held on foot may be aimed with both hands; with
vr.aim = "gamepad" the stick aims as on the flat screen, and the
facing is left as it is: 2) */
- if (local_player_index == 0 && !director_inhibited_facing(local_player_index) &&
+ if (local_player_index == 0 && player_control_camera_control_is_active() &&
+ (unit->object.parent_object_index == NONE || player_input_enabled()) &&
+ !director_inhibited_input(local_player_index) && !director_inhibited_facing(local_player_index) &&
+ !ui_widgets_active_for_local_player(local_player_index) &&
+ !(director_camera_scripted && *director_camera_scripted) &&
+ director_get_perspective(local_player_index) != _director_perspective_scripted &&
!cinematic_in_progress())
{
vr_aim = halo_vr_aim(player->desired_angles.yaw,
unit->object.parent_object_index == NONE && vr_render_long_gun(current_weapon_index),
&vr_yaw, &vr_pitch);
}
+ /* A tracked driver losing focus/tracking sends an ordinary neutral
+ action, rather than retaining a stale heading/throttle. The platform
+ keeps the original physical gamepad branch ready. */
+ if (local_player_index == 0 && vr_driving_mode != HALO_VR_DRIVING_NONE &&
+ !halo_vr_vehicle_controls_ready())
+ {
+ input.throttle.i = input.throttle.j = 0.0f;
+ input.facing_delta.yaw = input.facing_delta.pitch = 0.0f;
+ vr_yaw = vr_hull_yaw;
+ vr_pitch = 0.0f;
+ vr_aim = 1;
+ }
+ /* Warthog lateral input is a bounded wheel angle (+j is left),
+ not a strafe request. The stock physics already reverses steering
+ while backing up. Other vehicles keep their native stick actions. */
+ if (local_player_index == 0 && vr_driving_mode == HALO_VR_DRIVING_WARTHOG)
+ {
+ float steering_yaw, steering_pitch;
+ int steering = halo_vr_vehicle_steering(vr_hull_yaw, input.throttle.j, time_delta_sec,
+ &steering_yaw, &steering_pitch);
+ if (steering > 0 && isfinite(steering_yaw) && isfinite(steering_pitch))
+ {
+ vr_yaw = steering_yaw;
+ vr_pitch = steering_pitch;
+ vr_aim = 1;
+ input.throttle.j = 0.0f;
+ /* Stock A/jump is the Warthog brake. A is an independent
+ accelerator in VR; apply it after radial stick normalization
+ so holding full left/right cannot reduce forward speed. */
+ SET_FLAG(input.unit_control_flags, _unit_control_jump_bit,
+ steering == 1 && halo_vr_vehicle_brake());
+ SET_FLAG(input.unit_control_flags, _unit_control_use_equipment_bit, FALSE);
+ if (steering == 2)
+ input.throttle.i = 0.0f;
+ else
+ input.throttle.i = halo_vr_vehicle_accelerator(input.throttle.i);
+ }
+ }
+ /* Reject a corrupt tracking result before wrapping yaw: infinity
+ would never leave the loop, and NaN must not reach the action queue. */
+ if (vr_aim == 1 && (!isfinite(vr_yaw) || !isfinite(vr_pitch)))
+ vr_aim = 0;
if (vr_aim == 1)
{
while (vr_yaw < 0.f)
@@ -774,6 +891,13 @@ static void handle_one_player_input(
{
halo_vr_aim_release();
}
+ /* Walking follows the head independently of controller/long-gun
+ aim. Vehicle steering, cinematics and other players stay unchanged. */
+ if (local_player_index == 0 && unit->object.parent_object_index == NONE &&
+ !director_inhibited_facing(local_player_index) && !cinematic_in_progress())
+ {
+ halo_vr_locomotion(player->desired_angles.yaw, &input.throttle.i, &input.throttle.j);
+ }
}
#endif
@@ -1599,6 +1723,12 @@ void player_control_action_test_reset(
globals->action_flags = 0;
globals->action_test_flags = 0;
+#ifdef HALO_VR
+ {
+ extern void halo_vr_tutorial_reset(void);
+ halo_vr_tutorial_reset();
+ }
+#endif
return;
}
@@ -1762,6 +1892,31 @@ void player_control_action_test_note(
{
globals->action_flags |= FLAG(_player_control_look_relative_right_bit);
}
+ /* In VR the controller aims the weapon; physical head motion answers
+ the tutorial's look tests independently, without moving that aim. */
+#ifdef HALO_VR
+ {
+ extern int halo_vr_tutorial_look(float *yaw, float *pitch);
+ extern int halo_vr_tutorial_turn(void);
+ float vr_yaw, vr_pitch;
+ int vr_turn = halo_vr_tutorial_turn();
+ if (vr_turn & 1)
+ globals->action_flags |= FLAG(_player_control_look_relative_left_bit);
+ if (vr_turn & 2)
+ globals->action_flags |= FLAG(_player_control_look_relative_right_bit);
+ if (halo_vr_tutorial_look(&vr_yaw, &vr_pitch))
+ {
+ if (vr_pitch > 0.f)
+ globals->action_flags |= FLAG(_player_control_look_relative_up_bit);
+ else if (vr_pitch < 0.f)
+ globals->action_flags |= FLAG(_player_control_look_relative_down_bit);
+ if (vr_yaw > 0.f)
+ globals->action_flags |= FLAG(_player_control_look_relative_left_bit);
+ else if (vr_yaw < 0.f)
+ globals->action_flags |= FLAG(_player_control_look_relative_right_bit);
+ }
+ }
+#endif
if (throttle->i > 0.f)
{
globals->action_flags |= FLAG(_player_control_move_relative_forward_bit);
@@ -1784,6 +1939,10 @@ static void player_control_action_test_check_reset_input_blob(
struct input_blob *input)
{
struct player_control_globals_data *globals;
+#ifdef HALO_VR
+ extern int halo_vr_running(void);
+ boolean vr_controls = halo_vr_running() != 0;
+#endif
if (input->accept && (cinematic_can_be_skipped() || network_coop_skip_offered()))
{
@@ -1807,6 +1966,22 @@ static void player_control_action_test_check_reset_input_blob(
{
globals->action_flags |= FLAG(_player_control_zoom_bit);
}
+#ifdef HALO_VR
+ /* The tutorial tests physical B, while the VR Xbox layout maps B to
+ melee. Consume its mapped actions until that button is released; using
+ the old weapon-switch flag here releases suppression too early. */
+ if (vr_controls && (TEST_FLAG(globals->action_test_flags, _player_control_back_bit) ||
+ TEST_FLAG(globals->suppressed_action_flags, _player_control_back_bit)))
+ {
+ SET_FLAG(globals->suppressed_action_flags, _player_control_back_bit, input->back);
+ SET_FLAG(input->unit_control_flags, _unit_control_use_equipment_bit, FALSE);
+ SET_FLAG(input->unit_control_flags, _unit_control_throw_grenade_bit, FALSE);
+ SET_FLAG(input->unit_control_flags, _unit_control_weapon_secondary_trigger_bit, FALSE);
+ SET_FLAG(input->player_control_flags, _player_control_input_zoom_bit, FALSE);
+ SET_FLAG(input->player_control_flags, _player_control_rotate_weapons_bit, FALSE);
+ SET_FLAG(input->player_control_flags, _player_control_rotate_grenades_bit, FALSE);
+ }
+#endif
if (!TEST_FLAG(globals->action_test_flags, _player_control_action_bit))
{
@@ -1838,6 +2013,10 @@ check_accept:
SET_FLAG(input->unit_control_flags, _unit_control_action_bit, FALSE);
check_back_unswapped:
+#ifdef HALO_VR
+ if (vr_controls)
+ return;
+#endif
if (!TEST_FLAG(globals->action_test_flags, _player_control_back_bit))
{
if (!TEST_FLAG(globals->suppressed_action_flags, _player_control_back_bit))
@@ -1867,6 +2046,10 @@ check_accept:
SET_FLAG(input->unit_control_flags, _unit_control_jump_bit, FALSE);
check_back_swapped:
+#ifdef HALO_VR
+ if (vr_controls)
+ return;
+#endif
if (!TEST_FLAG(globals->action_test_flags, _player_control_back_bit))
{
if (!TEST_FLAG(globals->suppressed_action_flags, _player_control_accept_bit))
diff --git a/source/hs/hs_library_external.c b/source/hs/hs_library_external.c
index 0402e0234b124837d2c1f7434b5e5aa8ef0cda89..a75c12fbc851ba5706dff9f002ea79ee44266679 100644
--- a/source/hs/hs_library_external.c
+++ b/source/hs/hs_library_external.c
@@ -237,6 +237,37 @@ boolean hs_trigger_volume_test_objects(
return result;
}
+#ifdef HALO_VR
+/* The five original a10 calibration targets use a strict five-degree gate.
+Only these named scenery objects may also be pointed at with the unarmed
+hand reticle; technician/AI/other-map gaze keeps its original predicate. */
+static boolean hs_vr_calibration_panel(long object_index, real degrees)
+{
+ static char const *const names[] = {
+ "looking_panel_center", "looking_panel_left", "looking_panel_right",
+ "looking_panel_up", "looking_panel_down",
+ };
+ struct object_datum *object;
+ struct scenario *scenario;
+ struct scenario_object_name *name;
+ short index;
+
+ if (degrees != 5.f || global_scenario_index == NONE ||
+ strcmp(tag_get_name(global_scenario_index), "levels\\a10\\a10"))
+ return FALSE;
+ object = object_get(object_index);
+ scenario = global_scenario_get();
+ if (object->object.type != _object_type_scenery ||
+ !VALID_INDEX(object->object.name_index, scenario->object_names.count))
+ return FALSE;
+ name = TAG_BLOCK_GET_ELEMENT(&scenario->object_names, object->object.name_index, struct scenario_object_name);
+ for (index = 0; index < NUMBEROF(names); index++)
+ if (!strncmp(name->name, names[index], sizeof(name->name)))
+ return TRUE;
+ return FALSE;
+}
+#endif
+
boolean hs_unit_can_see_object(
long unit_index,
long object_index,
@@ -258,6 +289,20 @@ boolean hs_unit_can_see_object(
unit_index,
&target_point,
DEGREES_TO_RADIANS(degrees));
+#ifdef HALO_VR
+ if (!result && hs_vr_calibration_panel(object_index, degrees))
+ {
+ extern boolean vr_render_player_pointing_ray(long unit_index, real_point3d *origin, real_vector3d *forward);
+ real_point3d origin;
+ real_vector3d forward, direction;
+ if (vr_render_player_pointing_ray(unit_index, &origin, &forward))
+ {
+ vector_from_points3d(&origin, &target_point, &direction);
+ if (normalize3d(&direction) > 0.f)
+ result = dot_product3d(&direction, &forward) > cosine(DEGREES_TO_RADIANS(degrees));
+ }
+ }
+#endif
}
return result;
diff --git a/source/interface/first_person_weapons.c b/source/interface/first_person_weapons.c
index c69e49486fafa69835310527a25ace074984aa13..80db42319b211638e49475259231f2be4f03e4f2 100644
--- a/source/interface/first_person_weapons.c
+++ b/source/interface/first_person_weapons.c
@@ -623,6 +623,13 @@ void first_person_weapon_render_update(
boolean visible= director_get_perspective(render.local_player_index)==_director_perspective_first_person &&
player_control_get_zoom_level(render.local_player_index)==NONE;
+#ifdef HALO_VR
+ /* Menus and missing controller poses must hide the gameplay rig,
+ not revive its stock head-attached pose. */
+ extern boolean vr_render_first_person_visible(short local_player_index);
+ visible = visible && vr_render_first_person_visible(render.local_player_index);
+#endif
+
first_person_weapon_set_visibility(render.local_player_index, visible);
if (first_person_weapon->visible)
{
diff --git a/source/interface/hud_weapon.c b/source/interface/hud_weapon.c
index 0be624229fba126da65261e46017220b223ab271..a3c0fd204039de51d7e0de0418f8c2da5bf2d064 100644
--- a/source/interface/hud_weapon.c
+++ b/source/interface/hud_weapon.c
@@ -961,6 +961,14 @@ static void crosshairs_draw(
long stack_buffer[STACK_BUFFER_LENGTH];
boolean firing_active = FALSE;
+#ifdef HALO_VR
+ /* Only the explicit per-eye controller reticle draw owns these glyphs;
+ tracking/projection loss must not create a head-centred HUD fallback. */
+ extern boolean vr_render_hud_crosshairs_visible(short local_player_index);
+ if (!vr_render_hud_crosshairs_visible(player->local_player_index))
+ return;
+#endif
+
csmemset(
stack_buffer,
0x62,
diff --git a/source/interface/motion_sensor.c b/source/interface/motion_sensor.c
index 9607f857d3c02103ff8197de943effb04e5be793..4c7d94640a7f166b7d2971d5dff4b89dff40d1e6 100644
--- a/source/interface/motion_sensor.c
+++ b/source/interface/motion_sensor.c
@@ -639,6 +639,11 @@ static void render_motion_sensor(
real relative_scale;
real_point2d corner;
short history_index;
+#ifdef HALO_VR
+ extern boolean vr_render_motion_sensor_yaw(short local_player_index, real *yaw);
+ real head_yaw;
+ boolean head_heading;
+#endif
csmemset(stack_buffer, 0x62, sizeof(stack_buffer));
perspective = director_get_perspective(local_player_index);
@@ -654,6 +659,9 @@ static void render_motion_sensor(
hud_globals->defaults.motion_sensor_range;
corner.x = (real)reference->x;
corner.y = (real)reference->y;
+#ifdef HALO_VR
+ head_heading = vr_render_motion_sensor_yaw(local_player_index, &head_yaw);
+#endif
blip_begin(&corner, 0, in_multiplayer, local_player_index);
for (history_index = 0;
@@ -666,6 +674,12 @@ static void render_motion_sensor(
MOTION_SENSOR_HISTORY_COUNT);
struct motion_sensor_datum *sensor =
&player->sensor_data[sensor_index];
+#ifdef HALO_VR
+ struct motion_sensor_datum draw_sensor;
+ struct motion_sensor_datum *render_sensor = sensor;
+#else
+ struct motion_sensor_datum *render_sensor = sensor;
+#endif
real weight =
(real)(MOTION_SENSOR_HISTORY_COUNT - history_index) * 0.1f;
real fade = weight * weight;
@@ -673,6 +687,17 @@ static void render_motion_sensor(
power(1.0f - weight, 3.5f) * 7.0 + 1.0);
short blip_index;
+#ifdef HALO_VR
+ if (head_heading)
+ {
+ /* Rotate both ordinary and goal blips for this draw only. The
+ tick's heading/history stays available for flat/invalid views. */
+ draw_sensor = *sensor;
+ draw_sensor.yaw = head_yaw + 1.57079632679489661923f;
+ render_sensor = &draw_sensor;
+ }
+#endif
+
for (blip_index = 0;
blip_index < MAXIMUM_MOTION_SENSOR_BLIPS;
blip_index++)
@@ -685,7 +710,7 @@ static void render_motion_sensor(
tiny_point2d_get(&blip->position, &blip_pos);
render_blip(
- sensor,
+ render_sensor,
blip->type,
blip->size,
&blip_pos,
@@ -711,7 +736,7 @@ static void render_motion_sensor(
tiny_point2d_get(&sensor->custom_blips[blip_index], &blip_pos);
render_blip(
- sensor,
+ render_sensor,
_blip_type_custom,
_hud_blip_type_medium,
&blip_pos,
diff --git a/source/interface/ui_widget.c b/source/interface/ui_widget.c
index ad45e08e8e2c4258ebf69345b005f505102d7e36..1d6c27abee750d1c4753c140d21f13d71267a4ea 100644
--- a/source/interface/ui_widget.c
+++ b/source/interface/ui_widget.c
@@ -6375,6 +6375,14 @@ void render_ui_widgets(
virtual_keyboard_render();
}
+#ifdef HALO_VR
+ if (!virtual_keyboard_active() && local_player_index == 0)
+ {
+ extern void vr_render_menu_pointer_draw(void);
+ vr_render_menu_pointer_draw();
+ }
+#endif
+
return;
}
diff --git a/source/items/weapons.c b/source/items/weapons.c
index b2e1403d795586b3fc1ab5464a6fae3472ebf805..7785206b6c960acaadb475f15bd6fd87b30cd116 100644
--- a/source/items/weapons.c
+++ b/source/items/weapons.c
@@ -220,6 +220,7 @@ symbols in this file:
#include "game/game.h"
#include "game/game_engine.h"
#include "game/players.h"
+#include "game/aim_assist.h"
#include "interface/first_person_weapons.h"
#include "math/periodic_functions.h"
#include "models/model_animation_definitions.h"
@@ -2165,6 +2166,206 @@ static void projectile_distribute(
return;
}
+/* Shared pre-spread ray: both real shots and VR previews use these exact
+origin/aim/trigger-offset calculations. A preview may supply a render pose
+and never records an aim-assist target. */
+static long trigger_adjust_projectile_ray(
+ long owner_object_index,
+ struct unit_datum *unit,
+ struct weapon_trigger_definition const *trigger_definition,
+ real_point3d *origin,
+ real_vector3d *forward,
+ real *velocity,
+ real *error,
+ boolean preview,
+ struct projectile_preview_pose const *pose)
+{
+ long target_object_index = NONE;
+#ifndef HALO_VR
+ (void)preview;
+ (void)pose;
+#endif
+ if (!TEST_FLAG(trigger_definition->flags, _weapon_trigger_projectiles_cannot_be_aimed_bit) &&
+ unit &&
+ !TEST_FLAG(unit->object.damage_flags, _object_dead_bit))
+ {
+ struct unit_definition *unit_definition= unit_definition_get(unit->definition_index);
+ long player_index= unit->unit.player_index;
+ long actor_index= unit->unit.actor_index;
+ boolean adjust_origin;
+ boolean use_aiming_vector;
+
+ if (unit->unit.gunner_object_index!=NONE)
+ {
+ struct unit_datum *gunner= unit_get(unit->unit.gunner_object_index);
+
+ player_index= gunner->unit.player_index;
+ actor_index= gunner->unit.actor_index;
+ }
+
+ adjust_origin= TEST_FLAG(unit_definition->unit.flags, _unit_fires_from_camera_bit);
+ use_aiming_vector= TRUE;
+ if (actor_index!=NONE && actor_firing_blindly(actor_index))
+ {
+ use_aiming_vector= FALSE;
+ }
+ if (unit->unit.gunner_object_index!=NONE)
+ {
+ use_aiming_vector= FALSE;
+ }
+
+#ifdef HALO_VR
+ if (preview && !use_aiming_vector)
+ pose = NULL;
+ if (preview && pose)
+ unit_preview_projectile_ray(owner_object_index, origin, forward, velocity,
+ adjust_origin, use_aiming_vector, &pose->camera_position, &pose->forward);
+ else
+#endif
+ unit_adjust_projectile_ray(owner_object_index, origin, forward, velocity, adjust_origin, use_aiming_vector);
+
+ if (player_index!=NONE)
+ {
+ real_vector3d right;
+ real_vector3d up;
+
+ cross_product3d(global_up3d, forward, &right);
+ if (normalize3d(&right)==0.0f)
+ {
+ right= *global_left3d;
+ }
+ cross_product3d(forward, &right, &up);
+ normalize3d(&up);
+
+ point_from_line3d(
+ origin,
+ forward,
+ trigger_definition->first_person_weapon_offset.x,
+ origin);
+ point_from_line3d(
+ origin,
+ &right,
+ trigger_definition->first_person_weapon_offset.y,
+ origin);
+ point_from_line3d(
+ origin,
+ &up,
+ trigger_definition->first_person_weapon_offset.z,
+ origin);
+
+#ifdef HALO_VR
+ if (preview)
+ {
+ real length = magnitude3d(forward);
+ if (!isfinite(origin->x) || !isfinite(origin->y) || !isfinite(origin->z) ||
+ !isfinite(forward->i) || !isfinite(forward->j) || !isfinite(forward->k) ||
+ !isfinite(length) || length <= 0.0f)
+ {
+ /* Keep invalid preview data out of the shared aim assertions.
+ A zero output also makes the wrapper suppress this preview. */
+ forward->i = forward->j = forward->k = 0.0f;
+ return NONE;
+ }
+ target_object_index = player_preview_projectile_aim(player_index, origin, forward, pose);
+ }
+ else
+#endif
+ target_object_index = player_aim_projectile(player_index, origin, forward);
+ }
+ else if (actor_index!=NONE)
+ {
+ target_object_index= actor_aim_projectile(actor_index, origin, forward, error);
+ }
+ }
+ return target_object_index;
+}
+
+#ifdef HALO_VR
+boolean weapon_preview_projectile_ray(
+ long weapon_index,
+ short trigger_index,
+ long player_index,
+ real_point3d *origin,
+ real_vector3d *forward,
+ long *owner_object_index,
+ struct projectile_preview_pose const *pose)
+{
+ struct weapon_datum *weapon;
+ struct weapon_definition *definition;
+ struct weapon_trigger_definition *trigger;
+ struct unit_datum *unit;
+ struct object_marker marker;
+ struct projectile_preview_pose validated_pose;
+ long owner, firing_player;
+ real velocity = 0.0f, error = 0.0f, length;
+
+ if (weapon_index == NONE || player_index == NONE)
+ return FALSE;
+ weapon = weapon_try_and_get(weapon_index);
+ if (!weapon)
+ return FALSE;
+ definition = weapon_definition_get(weapon->definition_index);
+ if (trigger_index < 0 || trigger_index >= MAXIMUM_NUMBER_OF_TRIGGERS_PER_WEAPON ||
+ trigger_index >= definition->weapon.triggers.count)
+ return FALSE;
+ owner = weapon_get_owner_object_index(weapon_index);
+ unit = unit_try_and_get(owner);
+ if (!unit || TEST_FLAG(unit->object.damage_flags, _object_dead_bit))
+ return FALSE;
+ firing_player = unit->unit.player_index;
+ if (unit->unit.gunner_object_index != NONE)
+ {
+ struct unit_datum *gunner = unit_try_and_get(unit->unit.gunner_object_index);
+ if (!gunner)
+ return FALSE;
+ firing_player = gunner->unit.player_index;
+ }
+ if (firing_player != player_index)
+ return FALSE;
+ trigger = TAG_BLOCK_GET_ELEMENT(&definition->weapon.triggers, trigger_index, struct weapon_trigger_definition);
+ if (pose)
+ {
+ struct unit_definition *unit_definition = unit_definition_get(unit->definition_index);
+ /* Limited turrets, vehicle seats and unaimed projectiles keep the
+ authoritative path; a render pose cannot bypass their firing rules. */
+ if (unit->object.type != _object_type_biped || unit->object.parent_object_index != NONE ||
+ unit->unit.gunner_object_index != NONE ||
+ !TEST_FLAG(unit_definition->unit.flags, _unit_fires_from_camera_bit) ||
+ unit_definition->unit.aiming_velocity_maximum != 0.0f ||
+ unit_definition->unit.aiming_acceleration_maximum != 0.0f ||
+ TEST_FLAG(trigger->flags, _weapon_trigger_projectiles_cannot_be_aimed_bit))
+ pose = NULL;
+ }
+ if (pose)
+ {
+ length = magnitude3d(&pose->forward);
+ if (!isfinite(pose->camera_position.x) || !isfinite(pose->camera_position.y) ||
+ !isfinite(pose->camera_position.z) || !isfinite(length) || length <= 0.0f)
+ return FALSE;
+ validated_pose = *pose;
+ normalize3d(&validated_pose.forward);
+ pose = &validated_pose;
+ }
+
+ /* object_get_marker_by_name supplies node zero when the named marker
+ is missing, the same fallback used by trigger_create_projectiles. */
+ object_get_marker_by_name(weapon_get_effect_object_index(weapon_index),
+ trigger_index == 0 ? "primary trigger" : "secondary trigger", &marker, 1);
+ *origin = marker.matrix.position;
+ *forward = marker.matrix.forward;
+ trigger_adjust_projectile_ray(owner, unit, trigger, origin, forward, &velocity, &error, TRUE, pose);
+ if (TEST_FLAG(trigger->flags, _weapon_trigger_uses_weapon_origin_bit))
+ *origin = marker.matrix.position;
+ length = magnitude3d(forward);
+ if (!isfinite(origin->x) || !isfinite(origin->y) || !isfinite(origin->z) ||
+ !isfinite(forward->i) || !isfinite(forward->j) || !isfinite(forward->k) ||
+ !isfinite(length) || length <= 0.0f || normalize3d(forward) <= 0.0f)
+ return FALSE;
+ *owner_object_index = object_get_ultimate_parent(weapon_get_projectile_owner_object_index(weapon_index));
+ return TRUE;
+}
+#endif
+
static void trigger_create_projectiles(
long weapon_index,
short trigger_index)
@@ -2203,73 +2404,8 @@ static void trigger_create_projectiles(
long projectile_definition_index;
short projectile_count;
- if (!TEST_FLAG(trigger_definition->flags, _weapon_trigger_projectiles_cannot_be_aimed_bit) &&
- unit &&
- !TEST_FLAG(unit->object.damage_flags, _object_dead_bit))
- {
- struct unit_definition *unit_definition= unit_definition_get(unit->definition_index);
- long player_index= unit->unit.player_index;
- long actor_index= unit->unit.actor_index;
- boolean adjust_origin;
- boolean use_aiming_vector;
-
- if (unit->unit.gunner_object_index!=NONE)
- {
- struct unit_datum *gunner= unit_get(unit->unit.gunner_object_index);
-
- player_index= gunner->unit.player_index;
- actor_index= gunner->unit.actor_index;
- }
-
- adjust_origin= TEST_FLAG(unit_definition->unit.flags, _unit_fires_from_camera_bit);
- use_aiming_vector= TRUE;
- if (actor_index!=NONE && actor_firing_blindly(actor_index))
- {
- use_aiming_vector= FALSE;
- }
- if (unit->unit.gunner_object_index!=NONE)
- {
- use_aiming_vector= FALSE;
- }
-
- unit_adjust_projectile_ray(owner_object_index, &origin, &forward, &velocity, adjust_origin, use_aiming_vector);
-
- if (player_index!=NONE)
- {
- real_vector3d right;
- real_vector3d up;
-
- cross_product3d(global_up3d, &forward, &right);
- if (normalize3d(&right)==0.0f)
- {
- right= *global_left3d;
- }
- cross_product3d(&forward, &right, &up);
- normalize3d(&up);
-
- point_from_line3d(
- &origin,
- &forward,
- trigger_definition->first_person_weapon_offset.x,
- &origin);
- point_from_line3d(
- &origin,
- &right,
- trigger_definition->first_person_weapon_offset.y,
- &origin);
- point_from_line3d(
- &origin,
- &up,
- trigger_definition->first_person_weapon_offset.z,
- &origin);
-
- target_object_index= player_aim_projectile(player_index, &origin, &forward);
- }
- else if (actor_index!=NONE)
- {
- target_object_index= actor_aim_projectile(actor_index, &origin, &forward, &error);
- }
- }
+ target_object_index = trigger_adjust_projectile_ray(owner_object_index, unit, trigger_definition,
+ &origin, &forward, &velocity, &error, FALSE, NULL);
if (TEST_FLAG(trigger_definition->flags, _weapon_trigger_uses_weapon_origin_bit))
{
diff --git a/source/items/weapons.h b/source/items/weapons.h
index a9c5c8308e82e1ff2ada96554b92a582fc639606..f451943f30ad6485f8a131faef5afd6b1dea3237 100644
--- a/source/items/weapons.h
+++ b/source/items/weapons.h
@@ -309,6 +309,27 @@ real weapon_estimate_time_to_target(
real distance);
/* ---------- globals */
+/* Nominal ray for the first primary projectile, before random spread and
+ballistics. Read-only: returns FALSE for an unsupported player/weapon state.
+The returned owner is the ultimate parent the shot's collision trace ignores. */
+struct projectile_preview_pose;
+#ifdef HALO_VR
+/* Render-only inputs: unshaken interpolated gameplay camera, and the local
+player's bounded current pointing direction. Never a physical hand origin. */
+struct projectile_preview_pose
+{
+ real_point3d camera_position;
+ real_vector3d forward;
+};
+boolean weapon_preview_projectile_ray(
+ long weapon_index,
+ short trigger_index,
+ long player_index,
+ real_point3d *origin,
+ real_vector3d *forward,
+ long *owner_object_index,
+ struct projectile_preview_pose const *pose);
+#endif
/* ---------- public code */
diff --git a/source/objects/object_lights.c b/source/objects/object_lights.c
index 919df847818bd36658fdf9448404ea67d8eead2a..3b1e447d48831e7d6fe26db60d317d53e3356ec4 100644
--- a/source/objects/object_lights.c
+++ b/source/objects/object_lights.c
@@ -157,6 +157,9 @@ symbols in this file:
#include "cache/texture_cache.h"
#include "game/game.h"
#include "game/game_engine.h"
+#ifdef HALO_VR
+#include "game/players.h"
+#endif
#include "interface/first_person_weapons.h"
#include "math/periodic_functions.h"
#include "memory/data.h"
@@ -450,12 +453,19 @@ static void light_compute_bounding_sphere(
boolean lens_flare_only,
real_point3d *position,
real *radius);
+static void light_compute_render_bounding_sphere(
+ long light_index, boolean maximum, boolean specular, boolean lens_flare_only,
+ real_point3d *position, real *radius);
static void render_debug_light(
long light_index);
static void light_get_bounding_sphere(
long light_index,
real_point3d *position,
real *radius);
+#ifdef HALO_VR
+extern boolean vr_render_player_flashlight_pose(long unit_index, real_point3d *position,
+ real_vector3d *forward, real_vector3d *up);
+#endif
/* ---------- globals */
@@ -815,6 +825,61 @@ long light_new_unattached(
return light_index;
}
+/* Only a living local unarmed VR player's actual flashlight has a
+render-time pose. Ordinary lights, weapon markers and simulation datums
+retain their original paths. */
+static boolean light_vr_unarmed_flashlight_pose(struct light_datum const *light,
+ real_point3d *position, real_vector3d *forward, real_vector3d *up)
+{
+#ifdef HALO_VR
+ if (light->parent_light_index == NONE && light->object_index != NONE &&
+ TEST_FLAG(light->flags, _point_light_dynamic_bit) &&
+ TEST_FLAG(light_definition_get(light->definition_index)->flags,
+ _light_definition_is_first_person_flashlight_bit))
+ return vr_render_player_flashlight_pose(light->object_index, position, forward, up);
+#else
+ (void)light; (void)position; (void)forward; (void)up;
+#endif
+ return FALSE;
+}
+
+/* Simulation cluster membership can lag behind a controller that points
+elsewhere between ticks. Include this one local render light without
+changing the partition. Reserve no extra capacity or unbounded writes. */
+static void lights_vr_include_unarmed_flashlights(void)
+{
+#ifdef HALO_VR
+ long light_index, player_index;
+ real_point3d source;
+ real_vector3d direction, vertical;
+ if (render.local_player_index != 0 || (player_index = local_player_get_player_index(0)) == NONE ||
+ !vr_render_player_flashlight_pose(player_get(player_index)->unit_index, &source, &direction, &vertical))
+ return;
+ for (light_index = data_next_index(light_data, NONE); light_index != NONE;
+ light_index = data_next_index(light_data, light_index))
+ {
+ real_point3d position;
+ real_vector3d forward, up;
+ if (light_unmarked(light_index) && light_vr_unarmed_flashlight_pose(light_get(light_index),
+ &position, &forward, &up))
+ {
+ struct light_datum const *light = light_get(light_index);
+ real intensity;
+ short slot = lights_globals.scene_point_light_count;
+ object_get_function_value(light->object_index, light->function_index, &intensity);
+ if (intensity <= 0.0f)
+ continue;
+ if (slot < MAXIMUM_RENDERED_LIGHTS)
+ lights_globals.scene_point_light_count++;
+ else
+ slot = MAXIMUM_RENDERED_LIGHTS - 1;
+ lights_globals.scene_point_lights[slot] = light_index;
+ light_mark(light_index);
+ }
+ }
+#endif
+}
+
void lights_preprocess_scene(
void)
{
@@ -869,6 +934,7 @@ void lights_preprocess_scene(
light_get_bounding_sphere,
light_unmarked,
light_mark);
+ lights_vr_include_unarmed_flashlights();
light_marker_end();
rasterizer_lights_begin();
for (rendered_cluster_index = 0;
@@ -1053,6 +1119,8 @@ void lights_preprocess_scene(
TRUE);
}
}
+ light_vr_unarmed_flashlight_pose(light, &light_parameters.position,
+ &light_parameters.forward, &light_parameters.up);
light->rasterizer_light_index = rasterizer_light_submit(
&light_parameters);
@@ -1550,15 +1618,14 @@ static long cluster_get_next_light(
reference_index);
}
-static void light_compute_bounding_sphere(
- long light_index,
+static void light_compute_bounding_sphere_from_basis(
+ struct light_datum const *light,
boolean maximum,
boolean specular,
boolean lens_flare_only,
real_point3d *position,
real *radius)
{
- struct light_datum *light = light_get(light_index);
struct point_light_definition *definition = light_definition_get(light->definition_index);
real light_radius = maximum
? definition->radius_modifier_upper_bound * definition->radius
@@ -1602,6 +1669,34 @@ static void light_compute_bounding_sphere(
return;
}
+/* Tick attachment/reconnection continues using the simulation basis. */
+static void light_compute_bounding_sphere(long light_index, boolean maximum,
+ boolean specular, boolean lens_flare_only, real_point3d *position, real *radius)
+{
+ light_compute_bounding_sphere_from_basis(light_get(light_index), maximum, specular,
+ lens_flare_only, position, radius);
+}
+
+/* Visibility and diffuse/specular use the same frame pose as submission.
+The effective light is a stack copy; neither its datum nor clusters move. */
+static void light_compute_render_bounding_sphere(long light_index, boolean maximum,
+ boolean specular, boolean lens_flare_only, real_point3d *position, real *radius)
+{
+ struct light_datum effective = *light_get(light_index);
+ light_vr_unarmed_flashlight_pose(&effective, &effective.position, &effective.forward, &effective.up);
+ light_compute_bounding_sphere_from_basis(&effective, maximum, specular, lens_flare_only, position, radius);
+}
+
+static boolean light_render_skips_clusters(struct light_datum const *light)
+{
+ real_point3d position;
+ real_vector3d forward, up;
+ return (TEST_FLAG(light->flags, _point_light_attached_to_first_person_weapon_bit) &&
+ TEST_FLAG(light_definition_get(light->definition_index)->flags,
+ _light_definition_supersize_in_first_person_bit)) ||
+ light_vr_unarmed_flashlight_pose(light, &position, &forward, &up);
+}
+
static short light_build_cluster_array(
long light_index,
short maximum_count,
@@ -1636,7 +1731,7 @@ static void light_get_bounding_sphere(
real_point3d *position,
real *radius)
{
- light_compute_bounding_sphere(
+ light_compute_render_bounding_sphere(
light_index,
TRUE,
FALSE,
@@ -1666,10 +1761,7 @@ void lights_render_diffuse(
if (TEST_FLAG(light->flags, _point_light_dynamic_bit)
&& light->rasterizer_light_index != NONE)
{
- boolean skip_clusters = TEST_FLAG(light->flags,
- _point_light_attached_to_first_person_weapon_bit)
- && TEST_FLAG(light_definition_get(light->definition_index)->flags,
- _light_definition_supersize_in_first_person_bit);
+ boolean skip_clusters = light_render_skips_clusters(light);
short cluster_count = 0;
real_point3d center;
real radius;
@@ -1681,7 +1773,7 @@ void lights_render_diffuse(
MAXIMUM_CLUSTERS_PER_LIGHT,
cluster_indices);
}
- light_compute_bounding_sphere(
+ light_compute_render_bounding_sphere(
lights_globals.scene_point_lights[light_index],
FALSE,
FALSE,
@@ -1723,10 +1815,7 @@ void lights_render_specular(
&& !TEST_FLAG(light_definition_get(light->definition_index)->flags,
_light_definition_no_specular_bit))
{
- boolean skip_clusters = TEST_FLAG(light->flags,
- _point_light_attached_to_first_person_weapon_bit)
- && TEST_FLAG(light_definition_get(light->definition_index)->flags,
- _light_definition_supersize_in_first_person_bit);
+ boolean skip_clusters = light_render_skips_clusters(light);
short cluster_count = 0;
real_point3d center;
real radius;
@@ -1738,7 +1827,7 @@ void lights_render_specular(
MAXIMUM_CLUSTERS_PER_LIGHT,
cluster_indices);
}
- light_compute_bounding_sphere(
+ light_compute_render_bounding_sphere(
lights_globals.scene_point_lights[light_index],
FALSE,
TRUE,
diff --git a/source/rasterizer/rasterizer_transparent_geometry.c b/source/rasterizer/rasterizer_transparent_geometry.c
index b1b251ee62b5b60782f1c3867e6ce971d8757668..304c8a0e7e3115a78e4712bf056c050607041802 100644
--- a/source/rasterizer/rasterizer_transparent_geometry.c
+++ b/source/rasterizer/rasterizer_transparent_geometry.c
@@ -98,6 +98,7 @@ symbols in this file:
enum
{
_shader_type_transparent_water = 7,
+ _shader_type_transparent_glass = 8,
_rasterizer_geometry_first_person_bit = 7,
_rasterizer_target_render_primary = 0,
};
@@ -569,6 +570,19 @@ void rasterizer_transparent_geometry_draw(
}
}
+#ifdef HALO_VR
+ /* The native first-person Warthog view must see through its glass.
+ Skip before changing stencil/frustum state; keep queue progress. */
+ if (!water && group->shader && group->shader->base.type == _shader_type_transparent_glass)
+ {
+ extern boolean vr_render_hides_vehicle_glass(long object_index);
+ if (vr_render_hides_vehicle_glass(group->object_index))
+ {
+ transparent_geometry_group_index++;
+ continue;
+ }
+ }
+#endif
if (TEST_FLAG(group->geometry_flags, _rasterizer_geometry_first_person_bit))
{
match_assert(
diff --git a/source/rasterizer/xbox/rasterizer_xbox_lights.c b/source/rasterizer/xbox/rasterizer_xbox_lights.c
index 2cfc3f9be838a7c92c6c43484b7fcea5b6557afa..5c8e7b0b4c35e45de68a5a2b1f115584d2d8bdc0 100644
--- a/source/rasterizer/xbox/rasterizer_xbox_lights.c
+++ b/source/rasterizer/xbox/rasterizer_xbox_lights.c
@@ -154,8 +154,9 @@ extern struct pixel_shader_definition pixel_shader;
/* ---------- public code */
-void rasterizer_sun_glow_draw(
- struct rasterizer_lens_flare_submit_parameters const *flare)
+static void rasterizer_sun_glow_draw_internal(
+ struct rasterizer_lens_flare_submit_parameters const *flare,
+ real glow_strength)
{
real vertex_constants[5][4];
real_point3d occlusion_point;
@@ -519,7 +520,7 @@ void rasterizer_sun_glow_draw(
0.0f,
0.0f,
0.0f,
- brightness / (real)(pass + 1)) >= 0 && success)
+ brightness * glow_strength / (real)(pass + 1)) >= 0 && success)
{
success = TRUE;
}
@@ -680,6 +681,55 @@ void rasterizer_sun_glow_draw(
return;
}
+#ifdef HALO_VR
+/* Only the final sun-ray composite is scaled. Keep the visibility mask,
+ * depth test and per-eye blur at their original strength. */
+static real rasterizer_vr_sun_glow_strength(double configured)
+{
+ if (!isfinite(configured))
+ return 0.5f;
+ if (configured < 0.0)
+ return 0.0f;
+ if (configured > 1.0)
+ return 1.0f;
+ return (real)configured;
+}
+#endif
+
+void rasterizer_sun_glow_draw(
+ struct rasterizer_lens_flare_submit_parameters const *flare)
+{
+#ifdef HALO_VR
+ extern boolean vr_render_sun_glow_begin(short eye, struct render_camera *camera, struct render_frustum *frustum);
+ extern void vr_render_sun_glow_end(void);
+ extern double config_real(const char *name);
+ struct render_camera camera = global_window_parameters.camera;
+ struct render_frustum frustum = global_window_parameters.frustum;
+ short eye;
+ if (vr_render_sun_glow_begin(0, &global_window_parameters.camera, &global_window_parameters.frustum))
+ {
+ real glow_strength = rasterizer_vr_sun_glow_strength(config_real("vr.sun_glow_strength"));
+ for (eye = 0; eye < 2; eye++)
+ {
+ if (eye != 0)
+ {
+ /* Start from the saved depth range, rather than the previous
+ eye's temporary camera/frustum. Scratch textures are overwritten. */
+ global_window_parameters.camera = camera;
+ global_window_parameters.frustum = frustum;
+ vr_render_sun_glow_begin(eye, &global_window_parameters.camera, &global_window_parameters.frustum);
+ }
+ rasterizer_sun_glow_draw_internal(flare, glow_strength);
+ }
+ global_window_parameters.camera = camera;
+ global_window_parameters.frustum = frustum;
+ vr_render_sun_glow_end();
+ return;
+ }
+#endif
+ rasterizer_sun_glow_draw_internal(flare, 1.0f);
+}
+
/* ---------- private code */
static boolean rasterizer_project_billboard(
diff --git a/source/render/render.c b/source/render/render.c
index 59f44d71bf11b187bab93d87e4c15c278abb06fb..590c370b06bab8484b02ba68fb54cc1af38771eb 100644
--- a/source/render/render.c
+++ b/source/render/render.c
@@ -162,6 +162,7 @@ void vr_render_end(void);
boolean vr_render_hud_camera(struct render_camera *camera);
boolean vr_render_weapon_camera(struct render_camera *camera);
void vr_render_crosshairs(void);
+boolean vr_render_gameplay_hud_visible(short local_player_index);
static boolean render_vr_eye_pass;
#endif
@@ -654,7 +655,8 @@ static void render_vr_hud(
rasterizer_window_begin(&parameters);
if (!bink_playback_in_progress())
{
- interface_draw_screen();
+ if (vr_render_gameplay_hud_visible(window->local_player_index))
+ interface_draw_screen();
halo_screen_ui_offset(TRUE);
render_ui_widgets(window->local_player_index, &camera.viewport_bounds);
halo_screen_ui_offset(FALSE);
diff --git a/source/units/units.c b/source/units/units.c
index 4857fb4d0fe4f45e18d71834d648230f60be946d..817cfa4d15bbdf2f6dbbeeb63da62d1ffecd67d7 100644
--- a/source/units/units.c
+++ b/source/units/units.c
@@ -2911,12 +2911,25 @@ boolean unit_can_see_point(
struct unit_datum *unit = unit_get(unit_index);
struct object_marker head_marker;
real_vector3d direction;
+ real_vector3d looking = unit->unit.looking_vector;
object_get_marker_by_name(unit_index, "head", &head_marker, 1);
+#ifdef HALO_VR
+ /* Only the living primary player's gaze follows the actual headset
+ view. AI/remote units retain their simulation looking vectors. */
+ if (unit->unit.player_index != NONE &&
+ !TEST_FLAG(unit->object.damage_flags, _object_dead_bit) &&
+ player_get(unit->unit.player_index)->local_player_index == 0 &&
+ player_get(unit->unit.player_index)->unit_index == unit_index)
+ {
+ extern boolean vr_render_player_gaze(long unit_index, real_point3d *origin, real_vector3d *forward);
+ (void)vr_render_player_gaze(unit_index, &head_marker.matrix.position, &looking);
+ }
+#endif
vector_from_points3d(&head_marker.matrix.position, point, &direction);
normalize3d(&direction);
- if (dot_product3d(&direction, &unit->unit.looking_vector)>cosine(field_of_view))
+ if (dot_product3d(&direction, &looking)>cosine(field_of_view))
{
can_see_point = TRUE;
}
@@ -4845,13 +4858,15 @@ void unit_scripting_exit_vehicle(
return;
}
-void unit_adjust_projectile_ray(
+static void unit_adjust_projectile_ray_internal(
long unit_index,
real_point3d *origin,
real_vector3d *direction,
real *velocity,
boolean adjust_origin,
- boolean use_aiming_vector)
+ boolean use_aiming_vector,
+ real_point3d const *preview_camera,
+ real_vector3d const *preview_forward)
{
struct unit_datum *unit;
real projection;
@@ -4860,14 +4875,17 @@ void unit_adjust_projectile_ray(
unit = unit_get(unit_index);
if (use_aiming_vector)
{
- *direction = unit->unit.aiming_vector;
+ *direction = preview_forward ? *preview_forward : unit->unit.aiming_vector;
}
if (adjust_origin)
{
real_point3d camera_position;
- unit_get_camera_position(unit_index, &camera_position);
+ if (preview_camera)
+ camera_position = *preview_camera;
+ else
+ unit_get_camera_position(unit_index, &camera_position);
vector_from_points3d(&camera_position, origin, &relative);
projection =
((relative.i*direction->i + relative.k*direction->k) +
@@ -4885,6 +4903,35 @@ void unit_adjust_projectile_ray(
return;
}
+void unit_adjust_projectile_ray(
+ long unit_index,
+ real_point3d *origin,
+ real_vector3d *direction,
+ real *velocity,
+ boolean adjust_origin,
+ boolean use_aiming_vector)
+{
+ unit_adjust_projectile_ray_internal(unit_index, origin, direction, velocity,
+ adjust_origin, use_aiming_vector, NULL, NULL);
+}
+
+/* A read-only render preview shares the shot's camera/muzzle projection. */
+#ifdef HALO_VR
+void unit_preview_projectile_ray(
+ long unit_index,
+ real_point3d *origin,
+ real_vector3d *direction,
+ real *velocity,
+ boolean adjust_origin,
+ boolean use_aiming_vector,
+ real_point3d const *camera,
+ real_vector3d const *forward)
+{
+ unit_adjust_projectile_ray_internal(unit_index, origin, direction, velocity,
+ adjust_origin, use_aiming_vector, camera, forward);
+}
+#endif
+
void unit_render_debug(
long unit_index)
{
diff --git a/source/units/units.h b/source/units/units.h
index ba4d9c2bf3a8fd98bd2076eb8c40210a49de343a..d2ebc960f4904431fb0dd291506f721bd718b067 100644
--- a/source/units/units.h
+++ b/source/units/units.h
@@ -820,6 +820,13 @@ void unit_adjust_projectile_ray(
real *velocity,
boolean adjust_origin,
boolean use_aiming_vector);
+#ifdef HALO_VR
+void unit_preview_projectile_ray(long unit_index, real_point3d *origin,
+ real_vector3d *direction, real *velocity, boolean adjust_origin,
+ boolean use_aiming_vector, real_point3d const *camera,
+ real_vector3d const *forward);
+#endif
+
void unit_render_debug(
long object_index);
boolean unit_clip_to_aiming_bounds(long unit_index, real_vector3d *vector, boolean use_aiming_screen);
diff --git a/tools/android_build.py b/tools/android_build.py
index 9e5f67bf04a7dfcae08ede2760d823b527d0de1f..26a26cff323c575ca1a428d80f0e438921c1e7b4 100644
--- a/tools/android_build.py
+++ b/tools/android_build.py
@@ -18,6 +18,7 @@ and stages both, with the SDL3 Java sources, for the Gradle project in
port/android/app, which ``ninja android_apk`` then assembles.
"""
+import hashlib
import os
import shutil
import subprocess
@@ -45,7 +46,10 @@ MONOCYPHER_DIR = Path("port/third_party/monocypher")
MUSL_VERSION = "1.2.5"
MUSL_DIR = THIRD_PARTY / f"musl-{MUSL_VERSION}"
MUSL_URL = f"https://musl.libc.org/releases/musl-{MUSL_VERSION}.tar.gz"
+MUSL_SHA256 = "a9a118bbe84d8764da0ea0d28b3ab3fae8477fc7e4085d90102b8596fc7c75e4"
+MUSL_STAMP = ".halo-verified-source-sha256"
SDL_TAG = "release-3.4.16"
+SDL_COMMIT = "fa2c02bb6e21974a89ea9824bc53c9932abe5f9c"
SDL_DIR = THIRD_PARTY / "SDL3"
SDL_URL = "https://github.com/libsdl-org/SDL.git"
ANDROID_API = 28
@@ -166,20 +170,47 @@ def _find_ndk() -> Optional[Path]:
def fetch_third_party(third_party: Path = THIRD_PARTY) -> None:
- """Download musl and SDL3 (configure time, once)."""
+ """Fetch pinned build sources; reject unverified or modified cached inputs."""
musl_dir = third_party / f"musl-{MUSL_VERSION}"
sdl_dir = third_party / "SDL3"
third_party.mkdir(parents=True, exist_ok=True)
- if not musl_dir.is_dir():
+ if musl_dir.is_symlink() or sdl_dir.is_symlink():
+ raise OSError("Build source caches must not be symbolic links. Use a fresh third-party directory.")
+ if musl_dir.is_dir():
+ stamp = musl_dir / MUSL_STAMP
+ if (not stamp.is_file() or stamp.is_symlink() or stamp.stat().st_size != 65
+ or stamp.read_bytes() != (MUSL_SHA256 + "\n").encode("ascii")):
+ raise OSError(f"Unverified musl source cache: {musl_dir}. Remove it and configure again to fetch the pinned archive.")
+ else:
print(f"Downloading {MUSL_URL}")
archive = third_party / f"musl-{MUSL_VERSION}.tar.gz"
- subprocess.run(["curl", "-sSfL", "-o", str(archive), MUSL_URL], check=True)
- subprocess.run(["tar", "xzf", archive.name], cwd=third_party, check=True)
- archive.unlink()
+ if archive.is_symlink():
+ raise OSError("The musl archive cache must not be a symbolic link.")
+ try:
+ subprocess.run(["curl", "-sSfL", "-o", str(archive), MUSL_URL], check=True)
+ digest = hashlib.sha256()
+ with archive.open("rb") as stream:
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
+ digest.update(chunk)
+ if digest.hexdigest() != MUSL_SHA256:
+ raise OSError("The musl archive checksum does not match the pinned source. Extraction refused.")
+ subprocess.run(["tar", "xzf", archive.name], cwd=third_party, check=True)
+ if not musl_dir.is_dir() or musl_dir.is_symlink():
+ raise OSError("The verified musl archive did not produce its expected source directory.")
+ (musl_dir / MUSL_STAMP).write_bytes((MUSL_SHA256 + "\n").encode("ascii"))
+ finally:
+ archive.unlink(missing_ok=True)
if not sdl_dir.is_dir():
- print(f"Cloning SDL3 {SDL_TAG}")
- subprocess.run(["git", "clone", "-q", "--depth", "1", "--branch", SDL_TAG, SDL_URL, str(sdl_dir)],
- check=True)
+ print(f"Fetching SDL3 {SDL_TAG} at {SDL_COMMIT}")
+ subprocess.run(["git", "init", "-q", str(sdl_dir)], check=True)
+ subprocess.run(["git", "-C", str(sdl_dir), "fetch", "-q", "--depth", "1", SDL_URL, SDL_COMMIT], check=True)
+ subprocess.run(["git", "-C", str(sdl_dir), "checkout", "-q", "--detach", SDL_COMMIT], check=True)
+ head = subprocess.check_output(["git", "-C", str(sdl_dir), "rev-parse", "HEAD"], text=True).strip()
+ if head != SDL_COMMIT:
+ raise OSError(f"SDL source cache is not the pinned commit {SDL_COMMIT}. Use a fresh third-party directory.")
+ dirty = subprocess.check_output(["git", "-C", str(sdl_dir), "status", "--porcelain", "--untracked-files=no"], text=True)
+ if dirty.strip():
+ raise OSError("SDL source cache contains tracked local changes. Use a fresh third-party directory.")
def _musl_sources(musl_dir: Path = MUSL_DIR) -> List[Path]:
diff --git a/tools/ce_menus.py b/tools/ce_menus.py
index 9d5153e8d9e3844fc28780dc1932aac0b92754f8..a93c1d9d29196774f06cc51bcbd44e7f1d7de77a 100644
--- a/tools/ce_menus.py
+++ b/tools/ce_menus.py
@@ -621,7 +621,7 @@ def main() -> None:
for piece in ("left", "center", "right"):
lines.append(art.port_picture(f"pause/pausebox_{piece}", [
(port_settings.pause_box_svg(piece, port_settings.pause_box_height(buttons)), 4 if piece == "center" else 16,
- 256) for buttons in port_settings.PAUSE_BOX_BUTTONS]))
+ port_settings.pause_box_texture_height(buttons)) for buttons in port_settings.PAUSE_BOX_BUTTONS]))
# (nothing: what a game map draws for a frame of ui.map's it has not,
# menu_tags.c)
lines.append(art.port_picture("blank", [
diff --git a/tools/port_settings.py b/tools/port_settings.py
index 63166f2ee98e00214ed4edbadda8f57e63a75d87..7544c8511f7628ba7729256eb2d45f7d483b9937 100644
--- a/tools/port_settings.py
+++ b/tools/port_settings.py
@@ -218,6 +218,8 @@ VR_SCREENS = {
[("HEADSET'S", "0"), *((f"{rate} HZ", str(rate)) for rate in (72, 80, 90, 120, 144))],
"The headset's rate, if it offers it; the game\ndraws a frame each refresh. From the next start.",
None),
+ (f"BUFFER STREAMING {RESTART}:", "display.persistent_stream_buffers", ON_OFF,
+ "Experimental upload buffers; default off.\nCompare on/off after restarting the game.", None),
],
},
}
@@ -654,18 +656,25 @@ def title_backdrop(width: float) -> str:
# pausebox5 redraw's rounded box (ui-svg-handmade/shell/bitmaps), drawn to
# hold each number of buttons, one frame each (the Xbox's 2-button box is
# 159 units high, and each button 35 more)
-PAUSE_BOX_BUTTONS = [3, 4]
+PAUSE_BOX_BUTTONS = [3, 4, 5]
def pause_box_height(buttons: int) -> int:
return 159 + (buttons - 2) * 35
+def pause_box_texture_height(buttons: int) -> int:
+ """Power-of-two texture that fits the complete box, including five rows."""
+ return 256 if pause_box_height(buttons) <= 256 else 512
+
+
def pause_box_svg(piece: str, height: int) -> str:
"""a piece of the pause box (left cap, centre strip, right cap; 9-slice:
- the strip tiles across), height units high in its 256-high texture"""
+ the strip tiles across), height units high in a fitting power-of-two texture"""
width = 4 if piece == "center" else 16
- lines = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="256" viewBox="0 0 {width} 256">']
+ texture_height = 256 if height <= 256 else 512
+ lines = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{texture_height}" '
+ f'viewBox="0 0 {width} {texture_height}">']
if piece == "center":
lines += [f' <rect x="0" y="2" width="4" height="{height - 4}" fill="#0a2649" fill-opacity="0.75"/>',
' <rect x="0" y="0" width="4" height="2" fill="#2896ff"/>',
@@ -1085,40 +1094,48 @@ def _lobby() -> list:
def _coop() -> list:
- """Co-op, the campaign for two players on this machine in split screen
- (the Xbox's Cooperative Play): Multiplayer's CO-OP CAMPAIGN, then player
- 2's profile, chosen with player 2's controller (its rows take any
- controller's presses, as the shared rows do only controller 1's), then
- New Game's levels and difficulty, which either player's controller uses
- (ui_widget.c's widget_takes_events_of_controller; menu_functions.c's
- coop_begin)"""
+ """Network campaign: one player here, other players on LAN or Internet.
+ Shared profile rows below remain available to competitive multiplayer.
+ """
base = f"{MT}/coop"
lines = _widget(f"{MT}/multiplayer_type_coop_item",
[("type", "text"), ("left", 51), ("width", 232), ("height", 32), ("bitmap", "bitmaps/list_item_bkd"),
("string_list", f"{MT}/multiplayer_options"), ("string_index", 7), ("font", "ui\\large_ui"),
("color", "#FF2896FF"), ("text_x", 13), ("text_y", 5)],
- [f'<on event="a" run="port coop begin" open="{base}/player_2_profile_screen" branch="true"/>',
- f'<on event="start" run="port coop begin" open="{base}/player_2_profile_screen" branch="true"/>',
+ [f'<on event="a" run="port coop begin" open="{base}/network_screen" branch="true"/>',
+ f'<on event="start" run="port coop begin" open="{base}/network_screen" branch="true"/>',
'<on event="left_mouse" run="mouse emit accept event"/>'])
- lines += _widget(f"{base}/player_2_profile_screen", [("width", 640), ("height", 480),
- ("flags", "pass_unhandled_to_focused_child"),
- ("bitmap", "bitmaps/gradient")],
- ['<child widget="main_menu/new_select/sel_list_desc_bkd"/>',
- f'<child widget="{base}/player_2_profile_list"/>',
- f'<child widget="{base}/header_player_2"/>',
- f'<child widget="{base}/player_2_help" x="20" y="416"/>'])
- lines += _header(f"{base}/header_player_2", f"{base}/header_player_2")
- rows = [f'<child widget="{base}/list_item_{index}" x="20" y="{73 + 30 * index}"/>' for index in range(11)]
- lines += _widget(f"{base}/player_2_profile_list",
- [("type", "column_list"), ("width", 640), ("height", 480),
- ("flags", "pass_unhandled_to_focused_child up_down_tabs_children"),
- ("description", "main_menu/profile_manager/player_profile_extended_desc")],
- ['<data input="3wide player profile list update"/>',
- '<on event="created" run="port coop player 2 list initialize"/>',
- '<on event="deleted" run="player profile list dispose"/>',
- '<on event="custom_activation" run="port coop player 2" '
- 'open="main_menu/solo_level_select/solo_level_select_screen" branch="true"/>',
- *rows, f'<child widget="{base}/player_2_button_bar" y="414"/>'])
+ lines += _widget(f"{base}/network_screen", [("width", 640), ("height", 480),
+ ("flags", "pass_unhandled_to_focused_child"),
+ ("bitmap", "bitmaps/gradient")],
+ ['<on event="b" run="port coop begin" back="true"/>',
+ '<on event="back" run="port coop begin" back="true"/>',
+ '<child widget="main_menu/new_select/sel_list_desc_bkd"/>',
+ f'<child widget="{base}/network_title" x="35" y="25"/>',
+ f'<child widget="{base}/network_list"/>',
+ f'<child widget="{base}/network_help" x="30" y="340"/>',
+ '<child widget="common_button_back" x="510" y="415"/>'])
+ lines += _widget(f"{base}/network_title", [("type", "text"), ("controller", 1), ("width", 570), ("height", 35),
+ ("text", "CO-OP CAMPAIGN"), ("font", "ui\\large_ui"),
+ ("color", "#FF2896FF"), ("text_flags", "no_focus_test")], [])
+ choices = (("coop_host_lan", "HOST LOCAL (LAN)"), ("coop_join_lan", "JOIN LOCAL (LAN)"),
+ ("coop_host_online", "HOST ONLINE (INTERNET)"), ("coop_join_online", "JOIN ONLINE (INTERNET)"),
+ ("coop_join_invite", "JOIN INVITE LINK"))
+ lines += _widget(f"{base}/network_list", [("type", "column_list"), ("width", 640), ("height", 310),
+ ("flags", "pass_unhandled_to_focused_child up_down_tabs_children")],
+ [f'<child widget="{base}/{key}" x="45" y="{90 + 42 * index}"/>'
+ for index, (key, _) in enumerate(choices)])
+ for key, caption in choices:
+ lines += _widget(f"{base}/{key}", [("type", "text"), ("width", 550), ("height", 35),
+ ("bitmap", "bitmaps/list_item_bkd"), ("text", caption),
+ ("font", "ui\\large_ui"), ("color", "#FF2896FF"), ("text_x", 13), ("text_y", 5)],
+ ['<on event="a" run="port coop begin"/>', '<on event="start" run="port coop begin"/>',
+ '<on event="left_mouse" run="mouse emit accept event"/>'])
+ lines += _widget(f"{base}/network_help", [("type", "text"), ("controller", 1), ("width", 580), ("height", 65),
+ ("text", "One player per headset or PC. LAN uses your local network.\nOnline hosts: share your invite, or choose PUBLIC in Server Setup."),
+ ("font", "ui\\small_ui"), ("color", "#FFFFFFFF"),
+ ("text_flags", "no_focus_test")], [])
+ # Competitive lobbies reuse these profile rows and their button bar.
for index in range(11):
lines += _widget(f"{base}/list_item_{index}",
[("width", 390), ("height", 28),
@@ -1129,10 +1146,6 @@ def _coop() -> list:
'<on event="left_mouse" run="mouse emit accept event"/>',
'<child widget="main_menu/new_select/list_item_text" x="25"/>',
'<child widget="main_menu/new_select/list_item_arrows"/>'])
- lines += _widget(f"{base}/player_2_help", [("type", "text"), ("width", 350), ("height", 24),
- ("text", "Player 2: choose with your controller."),
- ("font", "ui\\small_ui"), ("color", "#FF2896FF"), ("text_y", 5),
- ("text_flags", "no_focus_test")], [])
lines += _widget(f"{base}/player_2_button_bar", [("type", "column_list"), ("width", 640), ("height", 28),
("flags", "pass_unhandled_to_focused_child left_right_tabs_items")],
['<data input="common button bar update"/>',
@@ -1176,7 +1189,7 @@ def _item_options_extras() -> list:
def _map_kind() -> list:
"""the map lists' first row (New Game's and the Map screen's), as the
gametype list's chooser: a spinner of SINGLEPLAYER or MULTIPLAYER maps,
- for either controller (split screen co-op's New Game takes both)"""
+ for the player choosing a map"""
lines = _widget(MAP_KIND_CHOOSER, [("width", 256), ("height", 28), ("flags", "pass_unhandled_to_focused_child"),
("bitmap", "bitmaps/option_bkds_sm"), ("font", "ui\\large_ui"),
("color", "#FF2896FF"), ("align", "center"), ("text_y", 3)],
diff --git a/tools/test_build_sources.py b/tools/test_build_sources.py
new file mode 100644
index 0000000000000000000000000000000000000000..cbf5316de2938537be2ea1d4d52c442283279f30
--- /dev/null
+++ b/tools/test_build_sources.py
@@ -0,0 +1,176 @@
+#!/usr/bin/env python3
+"""Offline regressions for the production musl digest and SDL commit fetch.
+
+Network/download/git commands are synthetic stubs. Corrupt-download tests use
+the real SHA256 implementation; the successful extraction test supplies a
+deterministic digest stub for synthetic bytes rather than shipping upstream data.
+"""
+import hashlib
+from pathlib import Path
+import subprocess
+import sys
+import tempfile
+import unittest
+from unittest.mock import patch
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT))
+from tools import android_build as build
+
+
+class SyntheticSources:
+ payload = b"synthetic musl archive; no upstream source bytes"
+
+ def __init__(self):
+ self.commands = []
+ self.queries = []
+ self.head = build.SDL_COMMIT
+ self.dirty = ""
+ self.fail_tar = False
+
+ def run(self, argv, *, check, cwd=None):
+ assert check is True
+ self.commands.append(tuple(str(value) for value in argv))
+ if argv[0] == "curl":
+ assert argv[-1] == build.MUSL_URL
+ Path(argv[argv.index("-o") + 1]).write_bytes(self.payload)
+ elif argv[0] == "tar":
+ assert argv[1:] == ["xzf", f"musl-{build.MUSL_VERSION}.tar.gz"]
+ source = Path(cwd) / f"musl-{build.MUSL_VERSION}"
+ source.mkdir()
+ if self.fail_tar:
+ raise subprocess.CalledProcessError(1, argv)
+ elif argv[:3] == ["git", "init", "-q"]:
+ Path(argv[3]).mkdir()
+ elif argv[0] == "git" and argv[3] == "fetch":
+ assert argv[4:] == ["-q", "--depth", "1", build.SDL_URL, build.SDL_COMMIT]
+ elif argv[0] == "git" and argv[3] == "checkout":
+ assert argv[4:] == ["-q", "--detach", build.SDL_COMMIT]
+ else:
+ raise AssertionError(f"Unexpected command {argv!r}")
+ return subprocess.CompletedProcess(argv, 0)
+
+ def check_output(self, argv, *, text):
+ assert text is True
+ self.queries.append(tuple(argv))
+ if argv[3:] == ["rev-parse", "HEAD"]:
+ return self.head + "\n"
+ if argv[3:] == ["status", "--porcelain", "--untracked-files=no"]:
+ return self.dirty
+ raise AssertionError(f"Unexpected query {argv!r}")
+
+
+class PinnedSourcesTests(unittest.TestCase):
+ def setUp(self):
+ self.temp = tempfile.TemporaryDirectory(prefix="halo-pinned-sources-")
+ self.addCleanup(self.temp.cleanup)
+ self.root = Path(self.temp.name) / "third_party"
+ self.server = SyntheticSources()
+ self.run_patch = patch.object(build.subprocess, "run", self.server.run)
+ self.output_patch = patch.object(build.subprocess, "check_output", self.server.check_output)
+ self.run_patch.start(); self.output_patch.start()
+ self.addCleanup(self.run_patch.stop); self.addCleanup(self.output_patch.stop)
+
+ @property
+ def musl(self):
+ return self.root / f"musl-{build.MUSL_VERSION}"
+
+ def cached_musl(self, stamp=True):
+ self.musl.mkdir(parents=True)
+ if stamp:
+ (self.musl / build.MUSL_STAMP).write_bytes((build.MUSL_SHA256 + "\n").encode("ascii"))
+
+ def synthetic_valid_digest(self):
+ case = self
+ class Digest:
+ def __init__(self):
+ self.bytes = bytearray()
+ def update(self, data):
+ self.bytes.extend(data)
+ def hexdigest(self):
+ case.assertEqual(bytes(self.bytes), case.server.payload)
+ return build.MUSL_SHA256
+ return patch.object(build.hashlib, "sha256", Digest)
+
+ def test_known_upstream_pins_and_guest_abi_are_unchanged(self):
+ self.assertEqual(build.MUSL_VERSION, "1.2.5")
+ self.assertEqual(build.MUSL_SHA256, "a9a118bbe84d8764da0ea0d28b3ab3fae8477fc7e4085d90102b8596fc7c75e4")
+ self.assertEqual(build.SDL_TAG, "release-3.4.16")
+ self.assertEqual(build.SDL_COMMIT, "fa2c02bb6e21974a89ea9824bc53c9932abe5f9c")
+ self.assertIn("--target=arm64_32-apple-watchos", build.GUEST_ABI_FLAGS)
+
+ def test_corrupt_download_is_rejected_before_tar_or_git(self):
+ self.assertNotEqual(hashlib.sha256(self.server.payload).hexdigest(), build.MUSL_SHA256)
+ with self.assertRaisesRegex(OSError, "checksum.*Extraction refused"):
+ build.fetch_third_party(self.root)
+ self.assertEqual([command[0] for command in self.server.commands], ["curl"])
+ self.assertFalse(self.musl.exists())
+ self.assertFalse((self.root / f"musl-{build.MUSL_VERSION}.tar.gz").exists())
+
+ def test_verified_download_and_exact_checkout_then_cached_head_recheck(self):
+ with self.synthetic_valid_digest():
+ build.fetch_third_party(self.root)
+ self.assertEqual((self.musl / build.MUSL_STAMP).read_bytes(), (build.MUSL_SHA256 + "\n").encode("ascii"))
+ fetch = next(command for command in self.server.commands if command[0] == "git" and command[3] == "fetch")
+ self.assertEqual(fetch[-2:], (build.SDL_URL, build.SDL_COMMIT))
+ self.assertFalse(any("--branch" in command or "clone" in command for command in self.server.commands))
+ self.assertEqual(self.server.queries[0][3:], ("rev-parse", "HEAD"))
+ before = len(self.server.commands)
+ build.fetch_third_party(self.root)
+ self.assertEqual(len(self.server.commands), before) # No download or checkout silently replaces a cache.
+ self.assertEqual(len(self.server.queries), 4) # HEAD and tracked status checked on both calls.
+
+ def test_preexisting_unverified_musl_is_never_reused(self):
+ self.cached_musl(stamp=False)
+ with self.assertRaisesRegex(OSError, "Unverified musl source cache"):
+ build.fetch_third_party(self.root)
+ self.assertFalse(self.server.commands)
+
+ def test_wrong_or_malformed_musl_stamp_is_never_reused(self):
+ self.cached_musl()
+ stamp = self.musl / build.MUSL_STAMP
+ for content in (b"0" * 64 + b"\n", b"not a verified archive\n", (build.MUSL_SHA256 + "\r\n").encode("ascii")):
+ with self.subTest(stamp=content):
+ stamp.write_bytes(content)
+ with self.assertRaisesRegex(OSError, "Unverified musl source cache"):
+ build.fetch_third_party(self.root)
+ self.assertFalse(self.server.commands)
+
+ def test_partial_extract_cannot_acquire_a_verification_stamp(self):
+ self.server.fail_tar = True
+ with self.synthetic_valid_digest(), self.assertRaises(subprocess.CalledProcessError):
+ build.fetch_third_party(self.root)
+ self.assertTrue(self.musl.is_dir())
+ self.assertFalse((self.musl / build.MUSL_STAMP).exists())
+ with self.assertRaisesRegex(OSError, "Unverified musl source cache"):
+ build.fetch_third_party(self.root)
+ self.assertFalse(any(command[0] == "git" for command in self.server.commands))
+
+ def test_cached_sdl_wrong_head_rejected_without_reset_or_network(self):
+ self.cached_musl()
+ (self.root / "SDL3").mkdir()
+ self.server.head = "0" * 40
+ with self.assertRaisesRegex(OSError, "SDL source cache is not the pinned commit"):
+ build.fetch_third_party(self.root)
+ self.assertFalse(self.server.commands)
+ self.assertEqual(len(self.server.queries), 1)
+
+ def test_new_sdl_fetch_must_also_verify_its_actual_head(self):
+ self.cached_musl()
+ self.server.head = "0" * 40
+ with self.assertRaisesRegex(OSError, "SDL source cache is not the pinned commit"):
+ build.fetch_third_party(self.root)
+ self.assertEqual([command[3] if command[1] == "-C" else command[1]
+ for command in self.server.commands], ["init", "fetch", "checkout"])
+
+ def test_cached_sdl_with_tracked_changes_is_rejected(self):
+ self.cached_musl()
+ (self.root / "SDL3").mkdir()
+ self.server.dirty = " M CMakeLists.txt\n"
+ with self.assertRaisesRegex(OSError, "tracked local changes"):
+ build.fetch_third_party(self.root)
+ self.assertFalse(self.server.commands)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tools/test_online_coop_menu.py b/tools/test_online_coop_menu.py
new file mode 100644
index 0000000000000000000000000000000000000000..72086bda0960abfdbd70213e48f3e4f868a75ce7
--- /dev/null
+++ b/tools/test_online_coop_menu.py
@@ -0,0 +1,536 @@
+#!/usr/bin/env python3
+"""Execute production LAN/Internet campaign menu routes in flat and VR builds.
+
+Validate actual generated XML, event dispatch, map and browser filters,
+failure/cancel cleanup and one-player-per-machine campaign guards. No game,
+network or headset is used; compile the extracted C with warnings as errors.
+"""
+import argparse
+import importlib.util
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+import xml.etree.ElementTree as ET
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def mask(text):
+ return re.sub(r'/\*.*?\*/|//[^\n]*|"(?:\\.|[^"\\])*"',
+ lambda match: re.sub(r'[^\n]', ' ', match.group()), text, flags=re.S)
+
+
+def block(text, start):
+ hidden = mask(text)
+ opening = hidden.index('{', start)
+ depth, end = 1, opening + 1
+ while depth:
+ depth += (hidden[end] == '{') - (hidden[end] == '}')
+ end += 1
+ return text[start:end]
+
+
+def function(text, name):
+ found = re.search(r'(?:static\s+)?(?:boolean|void|short|int|char const\s*\*)\s*' + re.escape(name) +
+ r'\s*\([^;]*?\)\s*\{', text)
+ if not found:
+ raise AssertionError('Missing production helper: ' + name)
+ return block(text, found.start())
+
+
+def source_integration():
+ menu = (ROOT / 'port/linux/game/menu_functions.c').read_text()
+ events = (ROOT / 'source/interface/ui_widget_event_handler_functions.c').read_text()
+ widget = (ROOT / 'source/interface/ui_widget.c').read_text()
+ asset = ROOT / 'port/assets/menus/ce/main_menu.multiplayer_type_select.coop.xml'
+ spec = importlib.util.spec_from_file_location('coop_settings_generator', ROOT / 'tools/port_settings.py')
+ generator = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(generator)
+ assert asset.read_text() == '\n'.join(generator.multiplayer_files()[asset.name])
+ xml = ET.parse(asset).getroot()
+ nodes = {node.attrib['name'].rsplit('/', 1)[-1]: node for node in xml}
+ for button in ('a', 'start'):
+ handler = next(node for node in nodes['multiplayer_type_coop_item'] if node.attrib.get('event') == button)
+ assert handler.attrib['run'] == 'port coop begin'
+ assert handler.attrib['open'].endswith('coop/network_screen') and handler.attrib['branch'] == 'true'
+ for button in ('b', 'back'):
+ handler = next(node for node in nodes['network_screen'] if node.attrib.get('event') == button)
+ assert handler.attrib['run'] == 'port coop begin' and handler.attrib['back'] == 'true'
+ captions = {'coop_host_lan': 'HOST LOCAL (LAN)', 'coop_join_lan': 'JOIN LOCAL (LAN)',
+ 'coop_host_online': 'HOST ONLINE (INTERNET)', 'coop_join_online': 'JOIN ONLINE (INTERNET)',
+ 'coop_join_invite': 'JOIN INVITE LINK'}
+ for name, caption in captions.items():
+ assert nodes[name].attrib['text'] == caption and len(name) <= 31
+ for button in ('a', 'start'):
+ handler = next(node for node in nodes[name] if node.attrib.get('event') == button)
+ assert handler.attrib == {'event': button, 'run': 'port coop begin'}
+ assert 'player_2_profile_screen' not in nodes and 'player_2_profile_list' not in nodes
+ assert not any(node.attrib.get('run', '').startswith('port coop player 2') for node in xml.iter())
+ assert len([name for name in nodes if name.startswith('list_item_')]) == 11
+ assert 'player_2_button_bar' in nodes
+ lobby = ET.parse(ROOT / 'port/assets/menus/ce/main_menu.multiplayer_type_select.lobby.xml').getroot()
+ prefix = 'main_menu/multiplayer_type_select/coop/'
+ for node in lobby.iter():
+ reference = node.attrib.get('widget', '')
+ if reference.startswith(prefix):
+ assert reference[len(prefix):] in nodes
+ assert '"port coop begin"' in (ROOT / 'port/linux/game/menu_tags.c').read_text()
+ assert 'player_spawn_count = 2;' not in menu and 'vr_cooperative_begin' not in menu
+ dispatcher = function(menu, 'pc_menu_event_function_invoke')
+ coop = block(dispatcher, dispatcher.index('else if (!strcmp(name, "port coop begin"))'))
+ assert '#ifdef' not in coop and 'return cooperative_begin(' in coop
+ stale = block(dispatcher, dispatcher.index('else if (!strcmp(name, "port coop player 2 list initialize")'))
+ assert 'return campaign_fail();' in stale
+ back = block(dispatcher, dispatcher.index('else if (!strcmp(name, "controls back handler")'))
+ assert '!global_network_game_server_get()' in back and 'cooperative_session_cleanup();' in back
+ assert 'if (!result && cooperative_network_game)' in function(menu, 'browser_initialize')
+ assert 'lobby_local_player_count() > 1' in function(menu, 'map_list_choose')
+ assert 'map_list.campaign_only ? MAP_KIND_SINGLEPLAYER' in function(menu, 'map_list_update')
+ assert function(menu, 'lobby_browser_select').count('multiplayer_game_player(controller, found)') == 2
+ native = (ROOT / 'source/networking/network_server_message_handler.c').read_text()
+ response = function(native, 'network_game_server_handle_message_client_broadcast_game_search')
+ assert not any(name in response for name in ('game_private', 'coop_public', 'p2p_lobby'))
+ assert 'advertisement.engine_type = (short)game->variant.game_engine_index;' in response
+ assert 'csmemcpy(&advertisement.map, &game->map, sizeof(game->map));' in response
+ assert 'network_game_accept_remote_connections(TRUE);' in function(events, 'network_game_start_new_server')
+ dispatch = function(widget, 'event_handler_dispatch')
+ fallback = block(dispatch, dispatch.index('if (TEST_FLAG(handler->flags, _event_handler_open_widget_bit)'))
+ assert '!widget_deleted' in fallback
+ assert widget.index('ui_widget_load_children_recursive(widget, definition)') < widget.index(
+ 'if (handler->event_type == _widget_event_created)', widget.index('ui_widget_load_children_recursive(widget, definition)'))
+ helpers = '\n\n'.join(function(menu, name) for name in (
+ 'lobby_join_reset', 'scenario_name', 'campaign_level_of', 'game_cooperative',
+ 'lobby_local_player_count', 'multiplayer_mode_set', 'multiplayer_player', 'multiplayer_host_mode',
+ 'multiplayer_host', 'cooperative_single_player', 'cooperative_session_cleanup',
+ 'cooperative_network_host', 'cooperative_network_join', 'cooperative_begin',
+ 'map_step_count', 'map_step_open', 'map_list_initialize', 'map_list_choose', 'map_list_back',
+ 'advertised_cooperative', 'multiplayer_game_player', 'browser_games_read',
+ 'text_to_wide', 'lobby_browser_valid_games', 'browser_initialize', 'browser_join',
+ 'lobby_campaign_single_player', 'campaign_local_player_block', 'lobby_add_player',
+ 'lobby_join_start', 'lobby_player_choose', 'lobby_join_help', 'preview_add', 'preview_join'))
+ helpers += '\n' + function(events, 'ui_widget_port_open')
+ return helpers, coop, back, fallback
+
+
+HARNESS = r'''
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+#include <wchar.h>
+typedef int boolean;
+#define FALSE 0
+#define TRUE 1
+#define NONE (-1)
+#define MIN(a,b) ((a)<(b)?(a):(b))
+#define MAX(a,b) ((a)>(b)?(a):(b))
+#define PIN(v,a,b) MIN(MAX(v,a),b)
+#define NUMBEROF(a) ((short)(sizeof(a)/sizeof((a)[0])))
+#define MAXIMUM_LOCAL_PLAYERS 4
+#define MAXIMUM_ADVERTISED_GAMES 4
+#define NUMBER_OF_GAME_DIFFICULTY_LEVELS 4
+#define NUMBER_OF_SINGLE_PLAYER_LEVELS 10
+#define P2P_LISTING_NAME_SIZE 31
+#define TEST_FLAG(value,bit) ((value) & (1U << (bit)))
+#define _event_handler_open_widget_bit 0
+#define _ui_audio_feedback_none 0
+#define _ui_audio_feedback_forward 1
+#define _error_silent 0
+#define MAP_KIND_SINGLEPLAYER 0
+#define MAP_KIND_MULTIPLAYER 1
+#define MAP_STEP_MAPS 0
+#define MAP_STEP_DIFFICULTIES 1
+#define SOUND_BACK 3
+#define SOUND_FORWARD 2
+#define BUTTON_START 7
+#define _client_state_pregame 1
+#define SERVER_SETUP_NAME "pc\\main_menu\\multiplayer_type_select\\server_settings\\server_settings_screen"
+#define LOBBY_NAME "lobby"
+enum { _multiplayer_mode_server_browser, _multiplayer_mode_lan, _multiplayer_mode_direct_link,
+ _multiplayer_mode_host_internet, _multiplayer_mode_host_lan };
+struct widget_instance {
+ const char *name; struct widget_instance *parent; int definition_tag_index;
+ struct widget_instance *child, *next; boolean disabled, visible;
+};
+struct event_record { short controller_index; };
+struct player_profile { int marker; };
+struct ui_widget_event_handler_reference { unsigned flags; struct { int index; } widget_tag; };
+struct advertised_game {
+ short engine_type, player_count, maximum_player_count;
+ const char *map_name; boolean valid, in_progress, peer, open;
+};
+struct p2p_listing { short engine_type; char name[32], map[32]; };
+struct network_game { struct { short game_engine_index; } variant; struct { const char *name; } map; short maximum_players; };
+static struct {
+ short mode; boolean game_private, cooperative_maximum_players_set;
+ short game_count, game_chosen; struct advertised_game *games[MAXIMUM_ADVERTISED_GAMES];
+} multiplayer;
+static struct { short kind,step; boolean hosting,campaign_only; short map_count,first,chosen,level; } map_list;
+static struct { boolean adding; short controller; } lobby_join;
+static struct { short chosen,count; long indices[4]; } profile_list;
+static struct network_game client_game;
+static struct advertised_game available[MAXIMUM_ADVERTISED_GAMES], *selected_preview;
+static boolean cooperative_map_opening, cooperative_network_game, lan_mode;
+static int checks, player_spawn_count, online, host_public, coop_public;
+static int profile_ok, player_ok, host_ok, open_ok, browse_ok, initialize_child, server_exists, client_exists, local_split;
+static int host_calls, open_calls, reset_calls, panel_opens, failures, errors, went_back;
+static int allow_hosting, public_hosting, spinner_kind, focused_entry, disposed_client, disposed_server;
+static int accept_remote, variant_cleared, connection, expected_controller, players[4], old_players_cleared, browse_calls;
+static int choose_ok, choose_calls, chosen_level, chosen_difficulty, mp_map_calls, dispatch_calls, join_calls, join_ok;
+static int client_state, lobby_player_count, copy_public_browse, campaign_ui_visible;
+static struct widget_instance maps={"maps",NULL,5,NULL,NULL,FALSE,TRUE};
+static struct widget_instance helper_widget={"help",NULL,5,NULL,NULL,FALSE,TRUE};
+static const char *const map_names[]={"bloodgulch"};
+static const char *const campaign_names[]={"a10","a30","a50","b30","b40","c10","c20","c40","d20","d40"};
+#define CHECK(c) do { checks++; if (!(c)) { fprintf(stderr,"FAIL line %d: %s\n",__LINE__,#c); exit(1); } } while(0)
+static int config_boolean(const char *name) {
+ if (!strcmp(name,"network.online")) return online;
+ if (!strcmp(name,"network.host_public")) return host_public;
+ CHECK(!strcmp(name,"network.coop_public")); return coop_public;
+}
+static boolean campaign_profile(short controller,struct player_profile *profile) {
+ (void)profile; CHECK(controller==expected_controller); return profile_ok;
+}
+static boolean campaign_fail(void) { failures++; return FALSE; }
+static void display_error_text_deferred(const wchar_t *message,short controller) {
+ CHECK(message && controller==NONE); errors++;
+}
+static void player_ui_reset_single_player_local_player_controllers(void) { reset_calls++; }
+static void player_ui_local_player_left_multiplayer_game(short controller) {
+ CHECK(controller>=0 && controller<4); players[controller]=0; old_players_cleared++;
+}
+static boolean lobby_controller_playing(short controller) { return players[controller]; }
+static long player_ui_get_active_player_profile_index(int index) { CHECK(index==0); return 9; }
+static boolean ui_widget_port_multiplayer_player(short controller,long profile) {
+ CHECK(controller==expected_controller && profile==9); if(player_ok) players[controller]=1; return player_ok;
+}
+static void p2p_set_hosting_allowed(int value) { allow_hosting=value; }
+static void p2p_set_hosting_public(int value) { public_hosting=value; }
+static void p2p_lobby_browse(int value) { copy_public_browse=value; }
+static boolean ui_widget_port_host(struct widget_instance *w,struct event_record *e,boolean *deleted) {
+ (void)w;(void)e;(void)deleted; host_calls++; server_exists=client_exists=host_ok;
+ accept_remote=host_ok; connection=host_ok?2:0; return host_ok;
+}
+static void *global_network_game_server_get(void) { return server_exists?&server_exists:NULL; }
+static void *global_network_game_client_get(void) { return client_exists?&client_exists:NULL; }
+static struct network_game *network_game_client_get_game(void *client) { CHECK(client!=NULL); return &client_game; }
+static short network_game_client_get_state(void *client,short *data) { CHECK(client!=NULL);*data=0;return (short)client_state; }
+static boolean network_game_is_splitscreen_local(void) { return local_split; }
+static short ui_widget_port_multiplayer_maps(const char *const **names,short *last) {
+ *names=map_names;*last=7;return 13;
+}
+static const char *main_get_solo_level_name(short level) { CHECK(level>=0 && level<10);return campaign_names[level]; }
+static short main_get_solo_level_from_name(const char *name) {
+ short i;for(i=0;i<10;i++) if(!strcmp(name,campaign_names[i])) return i;return NONE;
+}
+static short main_get_difficulty(void) { return 2; }
+#ifndef _WIN32
+static int _stricmp(const char *a,const char *b) { return strcmp(a,b); }
+#endif
+static boolean ui_widget_port_cooperative_level_choose(const char *name,short difficulty) {
+ choose_calls++;chosen_level=main_get_solo_level_from_name(name);chosen_difficulty=difficulty;
+ CHECK(server_exists && chosen_level!=NONE && difficulty>=0 && difficulty<4);
+ if(choose_ok){client_game.variant.game_engine_index=0;client_game.map.name=name;} return choose_ok;
+}
+static boolean ui_widget_port_multiplayer_map_choose(short chosen) { CHECK(chosen>=0);mp_map_calls++;return TRUE; }
+static void map_kind_set(struct widget_instance *list,short kind) { CHECK(list==&maps);spinner_kind=kind; }
+static short map_kind_first(short chosen,short count) { (void)count;return chosen; }
+static void map_kind_focus(struct widget_instance *list,short first,short chosen) { CHECK(list==&maps && first==chosen);focused_entry=chosen; }
+static void ui_play_audio_feedback_sound(int sound) { CHECK(sound==SOUND_BACK || sound==SOUND_FORWARD); }
+static void ui_widget_port_go_back(struct widget_instance *list) { CHECK(list!=NULL);went_back++; }
+static void dispose_global_network_game_client(void) { disposed_client++;client_exists=0; }
+static void dispose_global_network_game_server(void) { disposed_server++;server_exists=0; }
+static void network_game_accept_remote_connections(boolean accept) { accept_remote=accept; }
+static void player_ui_clear_multiplayer_variant(void) { variant_cleared++; }
+static void game_connection_set(short value) { connection=value; }
+static struct widget_instance *widget_instance_get_topmost_parent(struct widget_instance *w) { return w; }
+static int widget_instance_get_child_index_from_parent(struct widget_instance *w) { (void)w;return 0; }
+static void error(int level,const char *text,...) { (void)level;(void)text;errors++; }
+static boolean map_list_initialize(struct widget_instance *list);
+static boolean browser_initialize(struct widget_instance *screen,struct event_record *event,boolean *deleted);
+static struct widget_instance *ui_widget_load_by_name_or_tag(const char *name,int tag,void *other,int index,
+ int previous,int parent,int child) {
+ struct event_record event={(short)expected_controller};boolean deleted=FALSE;
+ (void)tag;(void)other;(void)index;(void)previous;(void)parent;(void)child;open_calls++;
+ if(strstr(name,"connected_map_select_wrapper")) {
+ CHECK(cooperative_map_opening && player_spawn_count==1);
+ if(initialize_child) CHECK(map_list_initialize(&maps));
+ } else if(strstr(name,"join_game_screen")) {
+ CHECK(cooperative_network_game && !cooperative_map_opening && player_spawn_count==1);
+ if(initialize_child) (void)browser_initialize(&maps,&event,&deleted);
+ } else CHECK(!strcmp(name,SERVER_SETUP_NAME));
+ return open_ok?&maps:NULL;
+}
+static boolean ui_widget_launch_widget(struct widget_instance *widget,int tag) {
+ (void)widget;CHECK(tag==55);panel_opens++;return TRUE;
+}
+static boolean ui_widget_port_open(struct widget_instance *widget,const char *name,boolean *deleted);
+static struct widget_instance *screen_of(struct widget_instance *list) { return list; }
+static struct widget_instance *named(struct widget_instance *screen,const char *name,short index) {
+ (void)screen;(void)index;
+ if(!strcmp(name,"lobby_join_help") || strstr(name,"button_add") || strstr(name,"button_bar")) return &helper_widget;
+ return NULL;
+}
+static void visible_set(struct widget_instance *widget,boolean visible) { if(widget)campaign_ui_visible=visible; }
+static void focus_off_hidden(struct widget_instance *widget) { (void)widget; }
+static void text_set(struct widget_instance *widget,const wchar_t *text) {
+ CHECK(widget && text);if(cooperative_network_game)CHECK(wcsstr(text,L"One player per machine")!=NULL);
+}
+static boolean input_has_gamepad(short controller) { (void)controller;return TRUE; }
+static void server_browser_initialize(struct widget_instance *screen) { (void)screen; }
+static boolean browser_item_usable(struct widget_instance *child) { (void)child;return TRUE; }
+static void browser_focus(struct widget_instance *list) { (void)list; }
+static void lobby_browser_begin(void) { p2p_lobby_browse(TRUE); }
+static boolean ui_widget_port_browse(struct widget_instance *screen,struct event_record *event,boolean *deleted) {
+ (void)screen;(void)event;(void)deleted;browse_calls++;client_exists=browse_ok;return browse_ok;
+}
+static struct advertised_game *network_game_client_get_available_games(void *client) { CHECK(client!=NULL);return available; }
+static boolean network_game_client_advertised_game_is_valid(struct advertised_game *game) { return game->valid; }
+static boolean advertised_in_progress(struct advertised_game *game) { return game->in_progress; }
+static boolean game_from_peer(struct advertised_game *game) { return game->peer; }
+static boolean player_name_clean(wchar_t *name,short size) { (void)size;return *name!=0; }
+static struct widget_instance *focused_leaf(struct widget_instance *widget) { return widget; }
+static void ui_widget_port_dispatch_event(struct widget_instance *widget,int button,short controller,boolean *deleted) {
+ (void)widget;(void)controller;(void)deleted;CHECK(button==BUTTON_START);dispatch_calls++;
+}
+static struct advertised_game *preview_game(void) { return selected_preview; }
+static boolean ui_widget_port_join(struct widget_instance *widget,struct advertised_game *game,const char *name,boolean *deleted) {
+ (void)widget;(void)deleted;CHECK(game && !strcmp(name,LOBBY_NAME));join_calls++;return join_ok;
+}
+@@HELPERS@@
+static boolean dispatch(struct widget_instance *widget,struct event_record *event,boolean *widget_deleted) {
+ short controller=event->controller_index;const char *name="port coop begin";
+ if(0)return FALSE;
+ @@COOP_DISPATCH@@
+ return FALSE;
+}
+static boolean dispatch_back(struct widget_instance *widget,const char *name,boolean *widget_deleted) {
+ if(0)return FALSE;
+ @@BACK_DISPATCH@@
+ return TRUE;
+}
+static void xml_open_fallback(struct widget_instance *widget,boolean *deleted) {
+ struct ui_widget_event_handler_reference reference={1U,{55}};
+ struct ui_widget_event_handler_reference *handler=&reference;
+ boolean widget_deleted=*deleted,success=TRUE;long audio_feedback=_ui_audio_feedback_none;
+ @@XML_FALLBACK@@
+ *deleted=widget_deleted;(void)success;(void)audio_feedback;
+}
+static boolean press_coop(struct widget_instance *widget,struct event_record *event,boolean *deleted) {
+ boolean handled=dispatch(widget,event,deleted);
+ if(handled && !strcmp(widget->name,"multiplayer_type_coop_item"))xml_open_fallback(widget,deleted);
+ return handled;
+}
+static void reset_state(void) {
+ memset(&multiplayer,0,sizeof(multiplayer));memset(&map_list,0,sizeof(map_list));memset(players,0,sizeof(players));
+ memset(available,0,sizeof(available));cooperative_map_opening=cooperative_network_game=lan_mode=FALSE;
+ player_spawn_count=1;online=1;host_public=1;coop_public=0;
+ profile_ok=player_ok=host_ok=open_ok=browse_ok=choose_ok=join_ok=initialize_child=1;
+ server_exists=client_exists=local_split=host_calls=open_calls=reset_calls=panel_opens=failures=errors=went_back=0;
+ allow_hosting=public_hosting=0;spinner_kind=focused_entry=-1;
+ disposed_client=disposed_server=variant_cleared=old_players_cleared=browse_calls=0;
+ accept_remote=1;connection=2;choose_calls=mp_map_calls=dispatch_calls=join_calls=copy_public_browse=0;
+ chosen_level=chosen_difficulty=-1;client_state=_client_state_pregame;lobby_player_count=1;selected_preview=NULL;
+ client_game.variant.game_engine_index=1;client_game.map.name="bloodgulch";client_game.maximum_players=16;
+ profile_list.chosen=0;profile_list.count=1;profile_list.indices[0]=9;
+ lobby_join.adding=FALSE;lobby_join.controller=NONE;campaign_ui_visible=TRUE;
+}
+static void expect_cleanup(void) {
+ CHECK(!cooperative_map_opening && !cooperative_network_game && !server_exists && !client_exists);
+ CHECK(!allow_hosting && !public_hosting && !accept_remote && !connection && !copy_public_browse);
+ CHECK(player_spawn_count==1 && lobby_join.controller==NONE && !lobby_join.adding);
+ CHECK(players[0]+players[1]+players[2]+players[3]==0);
+}
+static void test_routes(short controller) {
+ struct widget_instance item={"multiplayer_type_coop_item",NULL,2,NULL,NULL,FALSE,TRUE};
+ struct event_record event={controller};boolean deleted;int i,public_choice,child;
+ const char *routes[]={"coop_host_lan","coop_host_online","coop_join_lan","coop_join_online","coop_join_invite"};
+ const int modes[]={_multiplayer_mode_host_lan,_multiplayer_mode_host_internet,_multiplayer_mode_lan,
+ _multiplayer_mode_server_browser,_multiplayer_mode_direct_link};
+ expected_controller=controller;
+ reset_state();players[0]=players[1]=players[2]=players[3]=1;player_spawn_count=4;
+ lobby_join.adding=TRUE;lobby_join.controller=2;deleted=FALSE;
+ CHECK(press_coop(&item,&event,&deleted));CHECK(deleted && panel_opens==1 && !host_calls && !open_calls);
+ CHECK(player_spawn_count==1 && cooperative_network_game && old_players_cleared==4);
+ item.name="network_screen";deleted=FALSE;CHECK(press_coop(&item,&event,&deleted));expect_cleanup();
+ for(i=0;i<5;i++) {
+ item.name=routes[i];reset_state();deleted=FALSE;
+ players[0]=players[1]=players[2]=players[3]=1;player_spawn_count=4;lobby_join.adding=TRUE;
+ online=(i==0 || i==2)?0:1;
+ CHECK(press_coop(&item,&event,&deleted));CHECK(deleted && !panel_opens && player_spawn_count==1);
+ CHECK(multiplayer.mode==modes[i] && cooperative_network_game && !cooperative_map_opening);
+ CHECK(players[0]+players[1]+players[2]+players[3]==1 && players[controller]);
+ CHECK(lan_mode==(i==0 || i==2) && !lobby_join.adding && lobby_join.controller==NONE);
+ if(i<2) {
+ CHECK(host_calls==1 && server_exists && client_exists && accept_remote);
+ CHECK(allow_hosting==(i==1) && !public_hosting && multiplayer.game_private);
+ CHECK(map_list.kind==MAP_KIND_SINGLEPLAYER && map_list.campaign_only && spinner_kind==MAP_KIND_SINGLEPLAYER);
+ CHECK(focused_entry==0);
+ /* Campaign route cannot be redirected by a forged spinner state. */
+ map_list.kind=MAP_KIND_MULTIPLAYER;map_list.chosen=3;deleted=FALSE;
+ CHECK(!map_list_choose(&maps,&deleted));CHECK(map_list.step==MAP_STEP_DIFFICULTIES && map_list.level==3);
+ map_list.chosen=1;CHECK(map_list_choose(&maps,&deleted));CHECK(deleted && choose_calls==1);
+ CHECK(chosen_level==3 && chosen_difficulty==1 && !mp_map_calls);
+ deleted=FALSE;CHECK(dispatch_back(&maps,"gamespy back handler",&deleted));
+ CHECK(deleted && server_exists && cooperative_network_game); /* Server Setup -> map keeps host. */
+ CHECK(map_list_initialize(&maps)); /* Back reconstructs the real map list. */
+ CHECK(map_list.campaign_only && map_list.kind==MAP_KIND_SINGLEPLAYER && spinner_kind==MAP_KIND_SINGLEPLAYER);
+ map_list.chosen=3;deleted=FALSE;CHECK(!map_list_choose(&maps,&deleted));
+ CHECK(map_list_back(&maps,&deleted));CHECK(!deleted && map_list.step==MAP_STEP_MAPS);
+ CHECK(map_list_back(&maps,&deleted));CHECK(deleted);expect_cleanup();
+ CHECK(map_list_initialize(&maps));CHECK(map_list.kind==MAP_KIND_MULTIPLAYER && !map_list.campaign_only);
+ } else {
+ CHECK(!host_calls && browse_calls==1 && client_exists && !server_exists);
+ CHECK(copy_public_browse==(i==3));
+ deleted=FALSE;CHECK(dispatch_back(&maps,"gamespy back handler",&deleted));CHECK(deleted);expect_cleanup();
+ }
+ /* Profile rejection and failed screen loads never inherit old campaign intent. */
+ reset_state();deleted=FALSE;profile_ok=0;CHECK(!press_coop(&item,&event,&deleted));expect_cleanup();
+ reset_state();deleted=FALSE;player_ok=0;CHECK(!press_coop(&item,&event,&deleted));expect_cleanup();
+ for(child=0;child<2;child++) {
+ reset_state();deleted=FALSE;open_ok=0;initialize_child=child;
+ CHECK(!press_coop(&item,&event,&deleted));CHECK(!deleted);expect_cleanup();
+ }
+ if(i==1 || i==3 || i==4) {
+ reset_state();deleted=FALSE;online=0;CHECK(!press_coop(&item,&event,&deleted));
+ CHECK(errors==1 && failures==1 && !host_calls && !open_calls);expect_cleanup();
+ }
+ if(i<2) {
+ reset_state();deleted=FALSE;host_ok=0;CHECK(!press_coop(&item,&event,&deleted));expect_cleanup();
+ } else {
+ reset_state();deleted=FALSE;browse_ok=0;CHECK(press_coop(&item,&event,&deleted));
+ /* Creation succeeds even if its created callback fails; connection is still disposed. */
+ expect_cleanup();
+ }
+ }
+ for(public_choice=0;public_choice<2;public_choice++) {
+ reset_state();deleted=FALSE;item.name="coop_host_online";coop_public=public_choice;host_public=!public_choice;
+ CHECK(press_coop(&item,&event,&deleted));CHECK(public_hosting==public_choice && multiplayer.game_private==!public_choice);
+ reset_state();deleted=FALSE;item.name="coop_host_lan";coop_public=public_choice;online=0;
+ CHECK(press_coop(&item,&event,&deleted));CHECK(!public_hosting && !allow_hosting && accept_remote);
+ }
+ /* Competitive hosts retain normal preference and map choice. */
+ reset_state();deleted=FALSE;item.name="multiplayer_type_create_interne";cooperative_network_game=TRUE;cooperative_map_opening=TRUE;
+ CHECK(multiplayer_host(&item,&event,controller,&deleted));CHECK(!cooperative_network_game && !cooperative_map_opening);
+ CHECK(multiplayer.mode==_multiplayer_mode_host_internet && public_hosting==host_public);
+ CHECK(map_list_initialize(&maps));CHECK(map_list.kind==MAP_KIND_MULTIPLAYER && !map_list.campaign_only);
+ reset_state();item.name="multiplayer_type_create_lan_item";CHECK(multiplayer_host(&item,&event,controller,&deleted));
+ CHECK(multiplayer.mode==_multiplayer_mode_host_lan && !allow_hosting && !public_hosting);
+}
+static void test_discovery(void) {
+ struct p2p_listing listings[3]={{0,"campaign","a10"},{1,"competitive","bloodgulch"},{0,"bad campaign","bloodgulch"}};
+ struct widget_instance item={"join",NULL,2,NULL,NULL,FALSE,TRUE};boolean deleted=FALSE;
+ reset_state();expected_controller=0;client_exists=TRUE;cooperative_network_game=TRUE;multiplayer.mode=_multiplayer_mode_lan;online=0;
+ available[0]=(struct advertised_game){0,1,8,"levels\\a10\\a10",TRUE,FALSE,FALSE,TRUE};
+ available[1]=(struct advertised_game){1,1,16,"bloodgulch",TRUE,FALSE,FALSE,TRUE};
+ available[2]=(struct advertised_game){0,1,8,"a30",TRUE,TRUE,FALSE,TRUE};
+ available[3]=(struct advertised_game){0,1,8,"a50",TRUE,FALSE,TRUE,TRUE};
+ browser_games_read();CHECK(multiplayer.game_count==2 && multiplayer.games[0]==&available[0] && multiplayer.games[1]==&available[2]);
+ CHECK(browser_join(&item,0,&deleted));CHECK(join_calls==1 && players[0] && player_spawn_count==1);
+ multiplayer.mode=_multiplayer_mode_direct_link;browser_games_read();CHECK(multiplayer.game_count==1 && multiplayer.games[0]==&available[3]);
+ CHECK(lobby_browser_valid_games(listings,3)==1 && !strcmp(listings[0].map,"a10"));
+ cooperative_network_game=FALSE;multiplayer.mode=_multiplayer_mode_lan;browser_games_read();CHECK(multiplayer.game_count==3);
+ players[0]=players[1]=players[2]=players[3]=1;player_spawn_count=4;
+ CHECK(multiplayer_game_player(0,&available[0]));CHECK(cooperative_network_game && player_spawn_count==1);
+ CHECK(players[0] && !players[1] && !players[2] && !players[3]);
+ reset_state();client_exists=TRUE;players[1]=1;
+ available[1]=(struct advertised_game){1,1,16,"bloodgulch",TRUE,FALSE,FALSE,TRUE};
+ CHECK(multiplayer_game_player(0,&available[1]));CHECK(players[0] && players[1]);CHECK(!cooperative_network_game && !reset_calls);
+}
+static void test_single_player_guards(void) {
+ struct widget_instance item={"lobby",NULL,2,NULL,NULL,FALSE,TRUE};boolean deleted=FALSE;int source;
+ for(source=0;source<3;source++) {
+ reset_state();expected_controller=1;client_exists=TRUE;players[0]=1;
+ cooperative_network_game=source==0;
+ client_game.variant.game_engine_index=source==1?0:1;client_game.map.name=source==1?"a10":"bloodgulch";
+ available[0]=(struct advertised_game){0,1,8,"a10",TRUE,TRUE,FALSE,TRUE};selected_preview=&available[0];
+ if(source<2) {
+ CHECK(!lobby_add_player());CHECK(!lobby_join.adding && lobby_join.controller==NONE);
+ CHECK(!lobby_join_start(&item,1,&deleted));CHECK(lobby_join.controller==NONE);
+ lobby_join.controller=1;CHECK(!lobby_player_choose());CHECK(!players[1] && lobby_join.controller==NONE);
+ lobby_join_help(&item);CHECK(!campaign_ui_visible);
+ }
+ CHECK(!preview_add(&item,1,&deleted));CHECK(lobby_join.controller==NONE && !players[1]);
+ CHECK(!preview_add(&item,0,&deleted));CHECK(dispatch_calls==1);
+ if(source<2){CHECK(!lobby_join_start(&item,0,&deleted));CHECK(dispatch_calls==2);}
+ }
+ reset_state();expected_controller=1;client_exists=TRUE;players[0]=1;
+ CHECK(lobby_add_player());CHECK(lobby_join.adding);
+ CHECK(lobby_join_start(&item,1,&deleted));CHECK(lobby_join.controller==1);
+ CHECK(lobby_player_choose());CHECK(players[0] && players[1]);lobby_join_help(&item);CHECK(campaign_ui_visible);
+ reset_state();server_exists=client_exists=TRUE;map_list.kind=MAP_KIND_SINGLEPLAYER;map_list.step=MAP_STEP_DIFFICULTIES;
+ map_list.level=0;map_list.chosen=1;players[0]=players[1]=1;
+ CHECK(!map_list_choose(&maps,&deleted));CHECK(!choose_calls);
+ players[1]=0;player_spawn_count=2;CHECK(!map_list_choose(&maps,&deleted));CHECK(!choose_calls);
+ reset_state();expected_controller=0;client_exists=TRUE;players[0]=players[1]=1;player_spawn_count=2;
+ available[0]=(struct advertised_game){0,1,8,"a10",TRUE,TRUE,FALSE,TRUE};selected_preview=&available[0];
+ CHECK(preview_join(&item,0,&deleted));CHECK(join_calls==1 && players[0] && !players[1] && player_spawn_count==1);
+ reset_state();client_exists=TRUE;players[0]=players[1]=1;
+ available[0]=(struct advertised_game){1,1,16,"bloodgulch",TRUE,TRUE,FALSE,TRUE};selected_preview=&available[0];
+ CHECK(preview_join(&item,0,&deleted));CHECK(players[0] && players[1] && !reset_calls);
+}
+int main(void) {
+ short controller;for(controller=0;controller<4;controller++)test_routes(controller);
+ test_discovery();test_single_player_guards();
+ printf("PASS: %d production campaign routing / single-player assertions (%s)\n",checks,
+#ifdef HALO_VR
+ "VR"
+#else
+ "flat"
+#endif
+ );return 0;
+}
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc', help='C compiler executable: cl, clang, gcc or cc')
+ parser.add_argument('--vcvarsall', type=Path, help='Visual Studio vcvarsall.bat')
+ args = parser.parse_args()
+ helpers, dispatch, back_dispatch, fallback = source_integration()
+ harness = HARNESS.replace('@@HELPERS@@', helpers).replace('@@COOP_DISPATCH@@', dispatch).replace('@@BACK_DISPATCH@@', back_dispatch).replace('@@XML_FALLBACK@@', fallback)
+ compiler = args.cc or os.environ.get('CC')
+ vcvars = args.vcvarsall
+ if not compiler and os.name == 'nt':
+ installations = Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio'
+ candidates = sorted(installations.glob('*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1])
+ vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('No host C compiler; select one with --cc.')
+ with tempfile.TemporaryDirectory(prefix='halo-online-coop-menu-', ignore_cleanup_errors=True) as directory:
+ temp = Path(directory)
+ source = temp / 'test.c'
+ source.write_text(harness)
+ for vr in (True, False):
+ executable = temp / ('test-vr' if vr else 'test-flat')
+ if os.name == 'nt':
+ executable = executable.with_suffix('.exe')
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX', '/wd4505',
+ '/D_CRT_SECURE_NO_WARNINGS', *(['/DHALO_VR'] if vr else []),
+ '/Fe:' + str(executable), '/Fo:' + str(temp / ('test-vr.obj' if vr else 'test-flat.obj')), str(source)]
+ if vcvars:
+ for path in (vcvars, Path(compiler), temp):
+ if any(character in str(path) for character in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported compiler/temp path shell metacharacter.')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\n'
+ 'if errorlevel 1 exit /b 1\n' + subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-Wno-unused-function',
+ *(['-DHALO_VR'] if vr else []), str(source), '-o', str(executable)]
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(executable)], cwd=temp, check=True)
+ print('PASS: generated five-route XML, LAN discovery, and single-player campaign integration')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_persistent_stream_buffers.py b/tools/test_persistent_stream_buffers.py
new file mode 100644
index 0000000000000000000000000000000000000000..c63374bc3dc751154e6fc7accf13c4723ed90952
--- /dev/null
+++ b/tools/test_persistent_stream_buffers.py
@@ -0,0 +1,469 @@
+#!/usr/bin/env python3
+"""Exercise production native upload/fence functions against fault-injected GL.
+
+Requires a host C11 compiler, no headset or assets. This verifies ownership,
+fallback, bounds and ring ordering; it does not measure driver performance.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def function(text, name):
+ match = re.search(r'^[\w \t*]+\b' + re.escape(name) + r'\s*\([^;]*?\)\s*\{', text, re.M)
+ assert match, name
+ masked = re.sub(r'/\*.*?\*/|//[^\n]*|"(?:\\.|[^"\\])*"',
+ lambda item: re.sub(r'[^\n]', ' ', item.group()), text, flags=re.S)
+ end, depth = masked.index('{', match.start()) + 1, 1
+ while depth:
+ depth += (masked[end] == '{') - (masked[end] == '}')
+ end += 1
+ return text[match.start():end]
+
+
+HARNESS = r'''
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+typedef unsigned int GLuint;
+typedef unsigned int GLenum;
+typedef unsigned int GLbitfield;
+typedef int GLint;
+typedef long long GLsizeiptr;
+typedef long long GLintptr;
+typedef void *GLsync;
+typedef int BOOL;
+#define WINAPI
+#define TRUE 1
+#define FALSE 0
+#define HALO_DESKTOP_GL 1
+#define HALO_ARM64_GUEST 1
+#define XGPU_STREAM_RING 1
+#define XGPU_PERSISTENT_STREAMS 1
+#define XGPU_SAMPLE_COUNTERS 1
+#define STREAM_BUFFER_RING 3
+/* Small pages run the identical production boundary logic. */
+#define STREAM_BUFFER_SIZE 128
+#define INDEX_BUFFER_SIZE 64
+#define VISIBILITY_TEST_SLOTS 8
+#define SAMPLE_COUNTING sample_counting
+#define VISIBILITY_QUERY 99
+#define GL_NO_ERROR 0
+#define GL_BUFFER_UPDATE_BARRIER_BIT 0x200
+#define GL_ATOMIC_COUNTER_BARRIER_BIT 0x1000
+enum { GL_ARRAY_BUFFER=1, GL_ELEMENT_ARRAY_BUFFER, GL_COPY_WRITE_BUFFER, GL_COPY_READ_BUFFER,
+ GL_ATOMIC_COUNTER_BUFFER, GL_COPY_WRITE_BUFFER_BINDING, GL_MAJOR_VERSION, GL_MINOR_VERSION,
+ GL_STREAM_DRAW, GL_STATIC_DRAW,
+ GL_SYNC_GPU_COMMANDS_COMPLETE, GL_ALREADY_SIGNALED, GL_CONDITION_SATISFIED,
+ GL_TIMEOUT_EXPIRED, GL_WAIT_FAILED };
+#define GL_MAP_WRITE_BIT 0x02
+#define GL_MAP_READ_BIT 0x01
+#define GL_MAP_UNSYNCHRONIZED_BIT 0x20
+#define GL_SYNC_FLUSH_COMMANDS_BIT 1
+static unsigned int checks;
+#define CHECK(c) do { checks++; if (!(c)) { fprintf(stderr,"line %d: %s\n",__LINE__,#c); exit(1); } } while (0)
+struct object { unsigned long size; int immutable, mapped, deleted; unsigned char data[1024]; };
+static struct object objects[256];
+static GLuint bound[6], next_object;
+static int mock_major=4, mock_minor=5, extension, resolver=1, storage_calls, map_calls, data_calls;
+static int fail_map, fail_storage, fail_gen, fail_fence, fail_data, deletes, subdata_calls, read_calls, copied;
+static GLenum wait_status=GL_ALREADY_SIGNALED;
+static unsigned int wait_calls, sync_deletes, next_sync;
+static int requested, invalidations;
+static int sample_counting=1, mapped_results_reads, begin_queries, log_calls;
+static GLenum gl_error;
+static GLuint test_counter_buffer;
+static int ordering_active, counter_update_done, pending_shader_write;
+static unsigned long pending_counter_offset;
+static GLuint pending_counter_value;
+static GLbitfield barriers;
+static unsigned int barrier_calls;
+static unsigned long long ticks, wait_duration;
+static void platform_log(const char *format, ...) { (void)format; log_calls++; }
+static int config_boolean(const char *name) { CHECK(!strcmp(name,"display.persistent_stream_buffers")); return requested; }
+static unsigned long long SDL_GetTicksNS(void) { ticks+=1000; return ticks; }
+static void xgpu_gl_state_invalidate(void) { invalidations++; }
+static void glGetIntegerv(GLenum name, GLint *out) {
+ *out = name==GL_MAJOR_VERSION ? mock_major : name==GL_MINOR_VERSION ? mock_minor :
+ name==GL_COPY_WRITE_BUFFER_BINDING ? (GLint)bound[GL_COPY_WRITE_BUFFER] : -1;
+ CHECK(*out >= 0);
+}
+static int host_gl_has_extension(const char *name) { CHECK(!strcmp(name,"GL_ARB_buffer_storage")); return extension; }
+static void glGenBuffers(int count, GLuint *out) {
+ int i; for (i=0;i<count;i++) { if(fail_gen) { out[i]=0; fail_gen=0; } else { out[i]=++next_object; CHECK(next_object<256); } }
+}
+static void glBindBuffer(GLenum target, GLuint object) { CHECK(target>=1 && target<=5); CHECK(object<256); bound[target]=object; }
+static void glDeleteBuffers(int count, const GLuint *names) {
+ int i,j; for(i=0;i<count;i++) if(names[i]) { CHECK(!objects[names[i]].deleted); objects[names[i]].deleted=1; deletes++;
+ for(j=1;j<=5;j++) if(bound[j]==names[i]) bound[j]=0; }
+}
+static void storage(GLenum target, GLsizeiptr size, const void *data, GLbitfield flags) {
+ struct object *o=&objects[bound[target]]; (void)data;
+ CHECK(bound[target] && !o->immutable && !o->deleted); CHECK(flags & 0x0100);
+ storage_calls++; if(storage_calls==fail_storage) return;
+ CHECK(size>0 && size<=(GLsizeiptr)sizeof(o->data)); o->size=(unsigned long)size; o->immutable=1;
+}
+static void *host_gl_resolve(const char *name) { CHECK(!strcmp(name,"glBufferStorage")); return resolver ? (void *)storage : NULL; }
+static void *glMapBufferRange(GLenum target, GLsizeiptr offset, GLsizeiptr size, GLbitfield flags) {
+ struct object *o=&objects[bound[target]]; map_calls++;
+ CHECK(!(ordering_active && target==GL_ATOMIC_COUNTER_BUFFER));
+ if(map_calls==fail_map) return NULL;
+ if(!o->size) return NULL;
+ CHECK(!o->deleted && offset>=0 && size>=0 && (unsigned long)(offset+size)<=o->size);
+ CHECK(!o->mapped); if(flags & 0x0040) CHECK(o->immutable);
+ o->mapped=1; return o->data+offset;
+}
+static int glUnmapBuffer(GLenum target) { objects[bound[target]].mapped=0; return 1; }
+static void glBufferData(GLenum target, GLsizeiptr size, const void *data, GLenum usage) {
+ struct object *o=&objects[bound[target]]; (void)usage;
+ CHECK(bound[target] && !o->immutable && !o->deleted); CHECK(size>=0 && size<=(GLsizeiptr)sizeof(o->data));
+ data_calls++; if(fail_data) { fail_data=0; gl_error=1; return; }
+ o->size=(unsigned long)size; memset(o->data,0,sizeof(o->data));
+ if(data) memcpy(o->data,data,(size_t)size);
+}
+static GLenum glGetError(void) { GLenum error=gl_error; gl_error=0; return error; }
+static void glMemoryBarrier(GLbitfield flags) {
+ CHECK(flags==GL_BUFFER_UPDATE_BARRIER_BIT || flags==GL_ATOMIC_COUNTER_BARRIER_BIT);
+ if(flags==GL_BUFFER_UPDATE_BARRIER_BIT && pending_shader_write) {
+ CHECK(test_counter_buffer);
+ memcpy(objects[test_counter_buffer].data+pending_counter_offset,&pending_counter_value,sizeof(GLuint));
+ pending_shader_write=0;
+ }
+ if(ordering_active && flags==GL_ATOMIC_COUNTER_BARRIER_BIT) CHECK(counter_update_done && !pending_shader_write);
+ barriers=flags; barrier_calls++;
+}
+static void glBufferSubData(GLenum target, GLsizeiptr offset, GLsizeiptr size, const void *data) {
+ struct object *o=&objects[bound[target]]; CHECK(!o->deleted && offset>=0 && size>=0);
+ CHECK((unsigned long)(offset+size)<=o->size); subdata_calls++; memcpy(o->data+offset,data,(size_t)size);
+ if(ordering_active && bound[target]==test_counter_buffer) {
+ CHECK(barriers==GL_BUFFER_UPDATE_BARRIER_BIT && !pending_shader_write);
+ counter_update_done=1; barriers=0;
+ }
+}
+static void glCopyBufferSubData(GLenum source, GLenum dest, GLsizeiptr a, GLsizeiptr b, GLsizeiptr size) {
+ CHECK((unsigned long)(a+size)<=objects[bound[source]].size);
+ CHECK((unsigned long)(b+size)<=objects[bound[dest]].size);
+ if(test_counter_buffer && (bound[source]==test_counter_buffer || bound[dest]==test_counter_buffer)) {
+ CHECK(barriers==GL_BUFFER_UPDATE_BARRIER_BIT && !pending_shader_write);
+ if(ordering_active && bound[dest]==test_counter_buffer) counter_update_done=1;
+ barriers=0;
+ }
+ memcpy(objects[bound[dest]].data+b,objects[bound[source]].data+a,(size_t)size); copied++;
+}
+static GLsync glFenceSync(GLenum condition, GLbitfield flags) {
+ (void)condition; (void)flags; if(fail_fence) return NULL; return (void *)(uintptr_t)++next_sync;
+}
+static GLenum glClientWaitSync(GLsync sync, GLbitfield flags, unsigned long long duration) {
+ CHECK(sync && flags==GL_SYNC_FLUSH_COMMANDS_BIT);
+ wait_duration=duration; wait_calls++; return wait_status;
+}
+static void glDeleteSync(GLsync sync) { CHECK(sync); sync_deletes++; }
+static void state_array_buffer(GLuint buffer) { glBindBuffer(GL_ARRAY_BUFFER,buffer); }
+static void state_element_array_buffer(GLuint buffer) { glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,buffer); }
+@@HOST_STATE@@
+@@HOST_FUNCTIONS@@
+static void host_gl_read_buffer(unsigned int buffer, unsigned int offset, unsigned int size, void *data) {
+ CHECK(wait_status==GL_ALREADY_SIGNALED || wait_status==GL_CONDITION_SATISFIED);
+ CHECK(offset<=objects[buffer].size && size<=objects[buffer].size-offset);
+ read_calls++; memcpy(data,objects[buffer].data+offset,size);
+}
+static void host_gl_read_results(void *data, unsigned int size) {
+ CHECK(!sample_counting && (wait_status==GL_ALREADY_SIGNALED || wait_status==GL_CONDITION_SATISFIED));
+ memset(data,0,size); mapped_results_reads++;
+}
+static void glBeginQuery(GLenum target, GLuint query) { CHECK(target==VISIBILITY_QUERY); (void)query; begin_queries++; }
+static struct {
+ GLuint stream_buffer, index_buffer, stream_buffers[STREAM_BUFFER_RING], index_buffers[STREAM_BUFFER_RING];
+ unsigned long buffer_ring, stream_offset, index_offset, stream_capacity, index_capacity;
+ BOOL persistent_requested, persistent_ring[STREAM_BUFFER_RING], stream_persistent, index_persistent;
+ GLuint stream_spills[STREAM_BUFFER_RING], index_spills[STREAM_BUFFER_RING];
+ BOOL ring_wait_warned[STREAM_BUFFER_RING];
+ GLuint visibility_counters, counter_snapshots[STREAM_BUFFER_RING], counter_values[VISIBILITY_TEST_SLOTS];
+ unsigned short ring_tests[STREAM_BUFFER_RING][VISIBILITY_TEST_SLOTS][2];
+ unsigned long ring_test_count[STREAM_BUFFER_RING];
+ GLuint visibility_latest[VISIBILITY_TEST_SLOTS];
+ GLuint *visibility_results, visibility_results_copy[VISIBILITY_TEST_SLOTS];
+ GLuint visibility_zero_buffer, queries[VISIBILITY_TEST_SLOTS];
+ unsigned long counter_active, counter_next;
+ BOOL gl_ready, visibility_test_active;
+} device;
+static struct {
+ unsigned long persistent_writes,mutable_writes,persistent_fallback_writes;
+ unsigned long stream_overflows,index_overflows,stream_spills,index_spills,ring_wait_failures;
+ double ring_wait_milliseconds,ring_wait_worst;
+} stats;
+static struct { int statistics; } debug_settings;
+@@RENDER_FUNCTIONS@@
+static void reset(void) {
+ memset(objects,0,sizeof(objects)); memset(bound,0,sizeof(bound)); memset(persistent_buffers,0,sizeof(persistent_buffers));
+ memset(frame_fences,0,sizeof(frame_fences)); memset(frame_fence_failed,0,sizeof(frame_fence_failed));
+ memset(&device,0,sizeof(device)); memset(&stats,0,sizeof(stats));
+ memset(&debug_settings,0,sizeof(debug_settings)); next_object=0; storage_calls=map_calls=data_calls=0;
+ fail_map=fail_storage=fail_gen=fail_fence=fail_data=deletes=subdata_calls=read_calls=copied=invalidations=0;
+ mock_major=4; mock_minor=5; extension=0; resolver=1; requested=0; ticks=0;
+ wait_status=GL_ALREADY_SIGNALED; wait_calls=sync_deletes=next_sync=0;
+ wait_duration=0; gl_error=0; log_calls=0; sample_counting=1; mapped_results_reads=begin_queries=0;
+ test_counter_buffer=0; ordering_active=counter_update_done=pending_shader_write=0;
+ pending_counter_offset=0; pending_counter_value=0; barriers=0; barrier_calls=0;
+}
+static int mapped_records(void) { int i,n=0; for(i=0;i<PERSISTENT_BUFFER_SLOTS;i++) n+=persistent_buffers[i].buffer!=0; return n; }
+static void visibility_setup(void) {
+ unsigned int values[VISIBILITY_TEST_SLOTS]={17,29}; int ring;
+ glGenBuffers(1,&device.visibility_counters); glBindBuffer(GL_COPY_WRITE_BUFFER,device.visibility_counters);
+ test_counter_buffer=device.visibility_counters;
+ glBufferData(GL_COPY_WRITE_BUFFER,sizeof(values),NULL,GL_STREAM_DRAW);
+ glBufferSubData(GL_COPY_WRITE_BUFFER,0,sizeof(values),values);
+ glGenBuffers(STREAM_BUFFER_RING,device.counter_snapshots);
+ for(ring=0;ring<STREAM_BUFFER_RING;ring++) {
+ glBindBuffer(GL_COPY_WRITE_BUFFER,device.counter_snapshots[ring]); glBufferData(GL_COPY_WRITE_BUFFER,sizeof(values),NULL,GL_STREAM_DRAW);
+ glBufferSubData(GL_COPY_WRITE_BUFFER,0,sizeof(values),values);
+ }
+ glBindBuffer(GL_COPY_READ_BUFFER,0); glBindBuffer(GL_COPY_WRITE_BUFFER,0);
+ device.ring_test_count[1]=1; device.ring_tests[1][0][0]=3; device.ring_tests[1][0][1]=1;
+}
+static void test_creation_and_rollback(void) {
+ GLuint buffer,previous; int failure;
+ reset(); glGenBuffers(1,&previous); glBindBuffer(GL_COPY_WRITE_BUFFER,previous);
+ buffer=host_gl_buffer_create_persistent(32); CHECK(buffer && bound[GL_COPY_WRITE_BUFFER]==previous); CHECK(mapped_records()==1);
+ host_gl_buffer_destroy_persistent(buffer); CHECK(mapped_records()==0 && objects[buffer].deleted);
+ reset(); fail_gen=1; CHECK(!host_gl_buffer_create_persistent(32)); CHECK(mapped_records()==0);
+ reset(); fail_map=1; CHECK(!host_gl_buffer_create_persistent(32)); CHECK(objects[1].immutable && objects[1].deleted); CHECK(mapped_records()==0);
+ reset(); fail_storage=1; CHECK(!host_gl_buffer_create_persistent(32)); CHECK(objects[1].deleted && mapped_records()==0);
+ reset(); mock_major=3; mock_minor=2; CHECK(!host_gl_buffer_create_persistent(32)); CHECK(!storage_calls && !next_object);
+ reset(); resolver=0; CHECK(!host_gl_buffer_create_persistent(32)); CHECK(!next_object);
+ reset(); CHECK(!host_gl_buffer_create_persistent(0)); CHECK(!next_object);
+ /* Fail every position of the six-buffer transaction, including a
+ success after the failed member. No immutable object enters BufferData. */
+ for(failure=1;failure<=6;failure++) {
+ int i; reset(); requested=1; fail_map=failure; stream_buffers_initialize();
+ CHECK(mapped_records()==0 && !device.stream_persistent && !device.index_persistent);
+ CHECK(data_calls==6); CHECK(deletes==storage_calls);
+ for(i=0;i<STREAM_BUFFER_RING;i++) {
+ CHECK(!device.persistent_ring[i]); CHECK(!objects[device.stream_buffers[i]].immutable);
+ CHECK(!objects[device.index_buffers[i]].immutable);
+ }
+ }
+ reset(); stream_buffers_initialize(); CHECK(!storage_calls && data_calls==6 && !mapped_records());
+ reset(); requested=1; mock_major=3; mock_minor=2; stream_buffers_initialize(); CHECK(!storage_calls && data_calls==6);
+ reset(); requested=1; stream_buffers_initialize(); CHECK(mapped_records()==6 && !data_calls);
+}
+static void test_bounds_and_exact_source(void) {
+ const unsigned char bytes[3]={0x12,0x34,0x56}; GLuint buffer; unsigned char snapshot[32];
+ reset(); buffer=host_gl_buffer_create_persistent(32); memset(objects[buffer].data,0xa5,32);
+ CHECK(host_gl_buffer_write_persistent(buffer,29,3,bytes)); CHECK(!memcmp(objects[buffer].data+29,bytes,3));
+ memcpy(snapshot,objects[buffer].data,32);
+ CHECK(!host_gl_buffer_write_persistent(buffer,30,3,bytes)); CHECK(!host_gl_buffer_write_persistent(buffer,UINT32_MAX,3,bytes));
+ CHECK(!host_gl_buffer_write_persistent(buffer,0,UINT32_MAX,bytes)); CHECK(!host_gl_buffer_write_persistent(buffer,0,3,NULL));
+ CHECK(host_gl_buffer_write_persistent(buffer,32,0,NULL)); CHECK(!host_gl_buffer_write_persistent(0,0,0,NULL));
+ CHECK(!memcmp(snapshot,objects[buffer].data,32));
+ reset(); requested=1; stream_buffers_initialize(); memset(objects[device.stream_buffer].data,0xa5,STREAM_BUFFER_SIZE);
+ CHECK(stream_upload(bytes,3)==0 && device.stream_offset==16); CHECK(stats.persistent_writes==1);
+ CHECK(!memcmp(objects[device.stream_buffer].data,bytes,3)); CHECK(objects[device.stream_buffer].data[3]==0xa5);
+ CHECK(index_upload(bytes,3)==0 && device.index_offset==16);
+ /* Missing mapping record takes legal subdata, never another map. */
+ host_gl_buffer_destroy_persistent(device.stream_buffers[2]);
+ device.stream_buffer=device.stream_buffers[0]; memset(persistent_buffers,0,sizeof(persistent_buffers));
+ CHECK(stream_upload(bytes,3)==16); CHECK(stats.persistent_fallback_writes==1 && subdata_calls==1);
+ reset(); stream_buffers_initialize(); CHECK(stream_upload(bytes,3)==0); CHECK(stats.mutable_writes==1);
+ CHECK(!memcmp(objects[device.stream_buffer].data,bytes,3)); CHECK(objects[device.stream_buffer].data[3]==0);
+}
+static void test_overflow_spills(void) {
+ unsigned char bytes[512]; GLuint base,index_base,spill; memset(bytes,0x6b,sizeof(bytes));
+ reset(); requested=1; stream_buffers_initialize(); base=device.stream_buffer; index_base=device.index_buffer;
+ CHECK(stream_upload(bytes,STREAM_BUFFER_SIZE)==0); CHECK(device.stream_persistent && !data_calls);
+ CHECK(stream_upload(bytes,1)==0); spill=device.stream_buffer; CHECK(spill!=base && !objects[spill].immutable);
+ CHECK(!device.stream_persistent && device.index_persistent && stats.stream_spills==1);
+ CHECK(objects[base].data[0]==0x6b && objects[base].mapped); CHECK(!deletes);
+ CHECK(index_upload(bytes,INDEX_BUFFER_SIZE)==0); CHECK(index_upload(bytes,1)==0);
+ CHECK(device.index_buffer!=index_base && !device.index_persistent && stats.index_spills==1);
+ stream_reserve(2*STREAM_BUFFER_SIZE); CHECK(device.stream_capacity==2*STREAM_BUFFER_SIZE);
+ CHECK(device.stream_buffer==spill && stats.stream_spills==1); CHECK(stream_upload(bytes,2*STREAM_BUFFER_SIZE)==0);
+ index_reserve(2*INDEX_BUFFER_SIZE); CHECK(index_upload(bytes,2*INDEX_BUFFER_SIZE)==0);
+ CHECK(device.index_capacity==2*INDEX_BUFFER_SIZE); CHECK(stats.index_spills==1);
+ /* All streams of a draw reserve together before attribute pointers.
+ Two uploads must stay on the spill selected by that single reservation. */
+ reset(); requested=1; stream_buffers_initialize(); stream_upload(bytes,112);
+ stream_reserve(32); spill=device.stream_buffer; CHECK(!device.stream_persistent);
+ CHECK(stream_upload(bytes,16)==0 && stream_upload(bytes,16)==16); CHECK(device.stream_buffer==spill);
+ stream_ring_reset(1); CHECK(device.stream_buffer==device.stream_buffers[0] && device.stream_persistent);
+ CHECK(device.stream_offset==0 && device.stream_capacity==STREAM_BUFFER_SIZE);
+}
+static void test_fences_and_snapshots(void) {
+ GLuint retired_vertex,retired_index; GLsync retained;
+ reset(); host_gl_fence_frame(1); retained=frame_fences[1]; wait_status=GL_TIMEOUT_EXPIRED;
+ CHECK(!host_gl_wait_frame_checked(1) && frame_fences[1]==retained && !sync_deletes);
+ CHECK(wait_duration==1000000ull && wait_calls==1);
+ wait_status=GL_WAIT_FAILED; CHECK(!host_gl_wait_frame_checked(1) && frame_fences[1]==retained);
+ wait_status=GL_CONDITION_SATISFIED; CHECK(host_gl_wait_frame_checked(1) && !frame_fences[1] && sync_deletes==1);
+ CHECK(host_gl_wait_frame_checked(1)); CHECK(!host_gl_wait_frame_checked(FRAME_FENCE_SLOTS));
+ reset(); fail_fence=1; host_gl_fence_frame(1); CHECK(!frame_fences[1] && !host_gl_wait_frame_checked(1));
+ /* The older GLES entry point keeps its existing behavior/deadline. */
+ reset(); host_gl_fence_frame(1); host_gl_wait_frame(1);
+ CHECK(wait_duration==1000000000ull && sync_deletes==1 && !frame_fences[1]);
+ reset(); requested=1; stream_buffers_initialize(); visibility_setup(); host_gl_fence_frame(1);
+ retired_vertex=device.stream_buffers[1]; retired_index=device.index_buffers[1]; wait_status=GL_TIMEOUT_EXPIRED;
+ frame_end_buffers(); CHECK(device.buffer_ring==1 && stats.ring_wait_failures==1);
+ CHECK(!read_calls && !device.ring_test_count[1] && !device.visibility_latest[3]);
+ CHECK(!device.persistent_ring[1] && !device.stream_persistent && !device.index_persistent);
+ CHECK(device.stream_buffer!=retired_vertex && device.index_buffer!=retired_index);
+ CHECK(!objects[retired_vertex].deleted && !objects[retired_index].deleted && mapped_records()==6);
+ CHECK(objects[retired_vertex].mapped && objects[retired_index].mapped && invalidations==1);
+ CHECK(frame_fences[1]); CHECK(!objects[device.stream_buffer].immutable && !objects[device.index_buffer].immutable);
+ /* A later slot pass keeps mutable storage, with no further mapped retirements. */
+ stream_ring_reset(0); CHECK(mapped_records()==6 && !device.persistent_ring[1]);
+ reset(); requested=1; stream_buffers_initialize(); visibility_setup(); host_gl_fence_frame(1);
+ debug_settings.statistics=1; frame_end_buffers(); CHECK(device.buffer_ring==1 && device.stream_persistent);
+ CHECK(read_calls==1 && device.visibility_latest[3]==29 && !device.ring_test_count[1]);
+ CHECK(stats.ring_wait_milliseconds>0 && copied==1); frame_end_buffers(); frame_end_buffers();
+ CHECK(device.buffer_ring==0 && device.stream_persistent && !stats.ring_wait_failures);
+ reset(); stream_buffers_initialize(); visibility_setup(); debug_settings.statistics=1;
+ frame_end_buffers(); CHECK(!device.persistent_requested && stats.ring_wait_milliseconds>0 && read_calls==1);
+ /* Default mutable pages are mapped UNSYNCHRONIZED too. A failed fence
+ * must orphan both pages and skip incomplete visibility readback. */
+ {
+ int failure;
+ for (failure=0; failure<3; failure++) {
+ int allocations, logs;
+ reset(); stream_buffers_initialize(); visibility_setup();
+ fail_fence=failure==2; host_gl_fence_frame(1);
+ wait_status=failure==0 ? GL_TIMEOUT_EXPIRED : GL_WAIT_FAILED;
+ allocations=data_calls; logs=log_calls;
+ frame_end_buffers();
+ CHECK(!device.persistent_requested && device.buffer_ring==1 && stats.ring_wait_failures==1);
+ CHECK(data_calls==allocations+2 && !read_calls && !device.ring_test_count[1]);
+ CHECK(device.stream_offset==0 && device.index_offset==0 && !device.stream_persistent);
+ CHECK(!mapped_records() && wait_calls==(unsigned)(failure!=2));
+ CHECK(sync_deletes==0); /* An unfinished fence is not declared complete. */
+ CHECK(failure==2 || wait_duration==1000000ull);
+ CHECK(log_calls==logs+1 && device.ring_wait_warned[1]);
+ stream_ring_reset(0);
+ CHECK(log_calls==logs+1 && stats.ring_wait_failures==2);
+ }
+ }
+ /* The coherent query mapping is not read while its fence is unfinished,
+ * and VR atomic-counter mode never makes an unused query-buffer copy. */
+ {
+ int failed, counting;
+ for(failed=0;failed<2;failed++) for(counting=0;counting<2;counting++) {
+ reset(); stream_buffers_initialize(); visibility_setup();
+ sample_counting=counting; device.visibility_results=device.visibility_results_copy;
+ host_gl_fence_frame(1); wait_status=failed ? GL_TIMEOUT_EXPIRED : GL_ALREADY_SIGNALED;
+ frame_end_buffers();
+ CHECK(mapped_results_reads==(!failed && !counting));
+ CHECK(wait_calls==1 && wait_duration==1000000ull);
+ }
+ }
+}
+static void test_ordered_counter_reset(void) {
+ GLuint source; int wrap, fallback;
+ reset(); visibility_zero_source_create(); source=device.visibility_zero_buffer;
+ CHECK(source && objects[source].size==sizeof(GLuint) && !objects[source].mapped);
+ CHECK(!memcmp(objects[source].data,"\0\0\0\0",sizeof(GLuint)) && !bound[GL_COPY_READ_BUFFER]);
+ visibility_zero_source_create(); CHECK(device.visibility_zero_buffer==source && data_calls==1);
+ reset(); fail_gen=1; visibility_zero_source_create(); CHECK(!device.visibility_zero_buffer && !data_calls);
+ reset(); fail_data=1; visibility_zero_source_create();
+ CHECK(!device.visibility_zero_buffer && deletes==1 && !bound[GL_COPY_READ_BUFFER]);
+ CHECK(objects[1].deleted && !gl_error);
+ visibility_zero_source_create(); CHECK(device.visibility_zero_buffer && !objects[device.visibility_zero_buffer].deleted);
+ for(fallback=0;fallback<2;fallback++) for(wrap=0;wrap<2;wrap++) {
+ unsigned int before_maps, before_updates, before_copies; GLuint other;
+ reset(); visibility_setup(); if(!fallback) visibility_zero_source_create();
+ device.gl_ready=TRUE; device.counter_next=wrap ? VISIBILITY_TEST_SLOTS-1 : 0;
+ pending_counter_offset=(wrap ? 0 : 1)*sizeof(GLuint); pending_counter_value=73;
+ pending_shader_write=1; ordering_active=1;
+ before_maps=map_calls; before_updates=subdata_calls; before_copies=copied;
+ D3DDevice_BeginVisibilityTest();
+ CHECK(device.visibility_test_active && device.counter_active==(unsigned)(wrap ? 0 : 1));
+ CHECK(barrier_calls==2 && barriers==GL_ATOMIC_COUNTER_BARRIER_BIT && counter_update_done);
+ CHECK(!pending_shader_write && map_calls==(int)before_maps && !begin_queries);
+ CHECK(subdata_calls==(int)before_updates+fallback && copied==(int)before_copies+!fallback);
+ CHECK(!bound[GL_COPY_READ_BUFFER] && !bound[GL_COPY_WRITE_BUFFER] && !bound[GL_ATOMIC_COUNTER_BUFFER]);
+ memcpy(&other,objects[test_counter_buffer].data+pending_counter_offset,sizeof(other)); CHECK(other==0);
+ memcpy(&other,objects[test_counter_buffer].data+(wrap ? 1 : 0)*sizeof(other),sizeof(other)); CHECK(other==(unsigned)(wrap ? 29 : 17));
+ D3DDevice_BeginVisibilityTest(); CHECK(barrier_calls==2); /* active test is not cleared twice */
+ }
+ reset(); sample_counting=0; device.gl_ready=TRUE; D3DDevice_BeginVisibilityTest();
+ CHECK(begin_queries==1 && !barrier_calls && !map_calls);
+ reset(); D3DDevice_BeginVisibilityTest(); CHECK(!begin_queries && !device.visibility_test_active);
+}
+int main(void) {
+ test_creation_and_rollback(); test_bounds_and_exact_source(); test_overflow_spills(); test_fences_and_snapshots();
+ test_ordered_counter_reset();
+ printf("PASS: %u production persistent-stream assertions (mock GL; no GPU performance claim)\n",checks);
+ return 0;
+}
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ args = parser.parse_args()
+ host = (ROOT / 'port/android/host/host_gl.c').read_text()
+ renderer = (ROOT / 'port/linux/src/d3d8_gl.c').read_text()
+ config = (ROOT / 'port/linux/src/port_config.c').read_text()
+ assert re.search(r'"display.persistent_stream_buffers", _config_boolean, "false", "HALO_PERSISTENT_STREAM_BUFFERS"', config)
+ host_functions = '\n\n'.join(function(host, name) for name in (
+ 'host_gl_fence_frame', 'host_gl_wait_frame', 'host_gl_wait_frame_checked',
+ 'host_gl_buffer_create_persistent', 'host_gl_buffer_destroy_persistent',
+ 'host_gl_buffer_write_persistent', 'host_gl_buffer_write'))
+ render_functions = '\n\n'.join(function(renderer, name) for name in (
+ 'stream_buffers_initialize', 'stream_reserve', 'stream_upload', 'index_reserve', 'index_upload',
+ 'stream_clock_ns', 'stream_ring_reset', 'frame_end_buffers',
+ 'visibility_zero_source_create', 'D3DDevice_BeginVisibilityTest'))
+ assert 'glFinish(' not in render_functions + host_functions
+ host_state = re.search(r'#define FRAME_FENCE_SLOTS[^\n]*\n\s*static GLsync frame_fences\[[^;]*;\nstatic int frame_fence_failed\[[^;]*;', host).group()
+ host_state += '\n' + re.search(r'#define PERSISTENT_BUFFER_SLOTS[^\n]*\nstatic struct\s*\{.*?\} persistent_buffers\[[^;]*;', host, re.S).group()
+ harness = HARNESS.replace('@@HOST_STATE@@', host_state).replace('@@HOST_FUNCTIONS@@', host_functions)
+ harness = harness.replace('@@RENDER_FUNCTIONS@@', render_functions)
+ # Confirm the actual initializer/reset/present remain wired, rather than
+ # testing a helper disconnected from production.
+ assert 'stream_buffers_initialize();' in function(renderer, 'gl_initialize')
+ assert 'visibility_zero_source_create();' in function(renderer, 'sample_counters_create')
+ assert 'frame_end_buffers();' in function(renderer, 'D3DDevice_Present')
+ imports = (ROOT / 'port/linux/arm64/host_imports.list').read_text().splitlines()
+ declarations = (ROOT / 'port/linux/src/xgpu.h').read_text()
+ for name in ('host_gl_buffer_create_persistent', 'host_gl_buffer_destroy_persistent',
+ 'host_gl_buffer_write_persistent', 'host_gl_wait_frame_checked'):
+ assert name in imports and name in declarations, name
+ compiler = args.cc or os.environ.get('CC')
+ vcvars = args.vcvarsall
+ if not compiler and os.name == 'nt':
+ installations = Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio'
+ candidates = sorted(installations.glob('*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1]); vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('No C11 host compiler found; select one with --cc.')
+ with tempfile.TemporaryDirectory(prefix='halo-persistent-stream-check-', ignore_cleanup_errors=True) as directory:
+ temp = Path(directory); source = temp / 'test.c'; executable = temp / ('test.exe' if os.name == 'nt' else 'test')
+ source.write_text(harness)
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX', '/D_CRT_SECURE_NO_WARNINGS',
+ '/Fe:' + str(executable), '/Fo:' + str(temp / 'test.obj'), str(source)]
+ if vcvars:
+ for path in (vcvars, Path(compiler), temp):
+ if any(character in str(path) for character in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported shell metacharacter in compiler/temp path.')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\nif errorlevel 1 exit /b 1\n' +
+ subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', str(source), '-o', str(executable)]
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(executable)], cwd=temp, check=True)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_cinematic_hands.py b/tools/test_vr_cinematic_hands.py
new file mode 100644
index 0000000000000000000000000000000000000000..bdbb46988da8a444e8d89cd8681fa6a35c5b2768
--- /dev/null
+++ b/tools/test_vr_cinematic_hands.py
@@ -0,0 +1,336 @@
+#!/usr/bin/env python3
+"""Exercise real first-person controller camera/hands with gameplay aim disabled.
+
+Extracts production render and presentation-tracking functions, uses the real VR
+view/hands layouts, and compares ordinary/gamepad poses with pinned original
+math. Game services and tracking are deterministic fixtures; no map, GPU,
+headset or network is used. Stock zoom/perspective and control scripts are
+preserved; live menus and unavailable controller poses hide the gameplay rig.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+
+from test_vr_projectile_reticle import function, normalized, without_visibility_guard
+
+ROOT = Path(__file__).resolve().parents[1]
+PIN = '2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4'
+
+HARNESS = r'''
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "vr.h"
+#include "vr_vehicle.h"
+#include "vr_host.h"
+#include "vr_locomotion.h"
+#include "vr_vehicle.h"
+typedef float real;typedef int boolean;
+typedef struct {real x,y,z;} real_point3d;
+typedef struct {real i,j,k;} real_vector3d;
+typedef struct {real scale;real_vector3d forward,left,up;real_point3d position;} real_matrix4x3;
+#include "vr_hands.h"
+#define TRUE 1
+#define FALSE 0
+#define NONE (-1)
+#define TEST_FLAG(v,b) (((v)&(1u<<(b)))!=0)
+enum {_object_type_biped,_object_type_vehicle,_object_dead_bit=2};
+enum {_director_perspective_first_person=1,_director_perspective_scripted,_director_perspective_neutral,_director_perspective_third_person};
+enum {MAXIMUM_NUMBER_OF_LOCAL_PLAYERS=4};
+struct render_camera {real_point3d position;real_vector3d forward,up;real fov;};
+static struct {
+ int initialized,frame,pending,menus,origin_valid,aim_synced,aiming;
+ float origin_yaw,body_yaw,origin_position[3];struct vr_host_views views;
+ struct halo_vr_vehicle_reference vehicle;
+} vr;
+static struct {int vehicle_first_person,vehicle_seat_calibration;} settings;
+static struct {
+ boolean active,weapon_camera_set,weapon_camera_valid;struct halo_vr_view view;
+ struct render_camera weapon_camera;real_point3d base_position,head_position;real base_yaw;real_vector3d aim_forward,aim_up;
+} vr_render;
+static struct {short local_player_index;} render;
+struct unit_datum {struct {int type;unsigned damage_flags;long parent_object_index;real_point3d position;} object;
+ struct {long player_index;real_vector3d aiming_vector;} unit;};
+static struct unit_datum fixture_unit;
+struct player_datum {long unit_index;real yaw,pitch;};
+static struct player_datum fixture_player;
+static int player_exists,unit_exists,perspective,centre_valid,scripted,inhibited,cinematic;
+static int widgets[4],fixture_zoom,node_builds,visibility_calls;
+struct first_person_weapon {long unit_index,weapon_index;boolean visible;};
+static struct first_person_weapon fixture_weapon;
+static real_point3d fixture_centre;
+static unsigned checks;
+#define CHECK(c) do{checks++;if(!(c)){fprintf(stderr,"line %d: %s\n",__LINE__,#c);exit(1);}}while(0)
+static int near(real a,real b){return fabsf(a-b)<0.0001f;}
+static void point_equal(real_point3d a,real_point3d b){CHECK(near(a.x,b.x));CHECK(near(a.y,b.y));CHECK(near(a.z,b.z));}
+static void vector_equal(real_vector3d a,real_vector3d b){CHECK(near(a.i,b.i));CHECK(near(a.j,b.j));CHECK(near(a.k,b.k));}
+static void camera_equal(struct render_camera a,struct render_camera b){point_equal(a.position,b.position);vector_equal(a.forward,b.forward);vector_equal(a.up,b.up);CHECK(a.fov==b.fov);}
+static long local_player_get_player_index(short index){CHECK(index==0);return player_exists?3:NONE;}
+static struct player_datum *player_get(long index){CHECK(index==3);return &fixture_player;}
+static struct unit_datum *unit_try_and_get(long index){return index==7&&unit_exists?&fixture_unit:NULL;}
+static int director_get_perspective(short index){CHECK(index==0);return perspective;}
+int halo_vr_aiming(void){return vr.aiming;}
+int halo_vr_frame_active(void){return vr.frame||vr.pending;}
+static boolean ui_widgets_active_for_local_player(short local){CHECK(local>=0&&local<4);return widgets[local];}
+static struct first_person_weapon *first_person_weapon_get(short local){CHECK(local==0);return &fixture_weapon;}
+static short player_control_get_zoom_level(short local){CHECK(local==0);return (short)fixture_zoom;}
+static void first_person_weapon_set_visibility(short local,boolean visible){CHECK(local==0);visibility_calls++;fixture_weapon.visible=visible;}
+static void first_person_weapon_build_node_matrices(short local){CHECK(local==0&&fixture_weapon.visible);node_builds++;}
+boolean vr_hands_right_centre(real_point3d *centre){if(!centre_valid)return FALSE;*centre=fixture_centre;return TRUE;}
+static void cross_product3d(const real_vector3d *a,const real_vector3d *b,real_vector3d *out){
+ *out=(real_vector3d){a->j*b->k-a->k*b->j,a->k*b->i-a->i*b->k,a->i*b->j-a->j*b->i};}
+static real magnitude3d(const real_vector3d *v){return sqrtf(v->i*v->i+v->j*v->j+v->k*v->k);}
+static real normalize3d(real_vector3d *v){real n=magnitude3d(v);if(n>0){v->i/=n;v->j/=n;v->k/=n;}return n;}
+static void set_real_point3d(real_point3d *p,real x,real y,real z){*p=(real_point3d){x,y,z};}
+@@MATH@@
+@@TRACKING@@
+@@RENDER@@
+@@LEGACY@@
+@@VISIBILITY@@
+static struct render_camera head_camera(void){return (struct render_camera){{100,200,301},{0,1,0},{0,0,1},1.2f};}
+static void reset(void){
+ int eye;memset(&vr,0,sizeof(vr));memset(&vr_render,0,sizeof(vr_render));memset(&settings,0,sizeof(settings));
+ memset(&fixture_unit,0,sizeof(fixture_unit));
+ vr.initialized=vr.frame=vr.origin_valid=1;vr.views.focused=vr.views.should_render=vr.views.head.valid=1;
+ vr.views.head.orientation[3]=1;for(eye=0;eye<2;eye++){vr.views.eye[eye]=vr.views.head;vr.views.eye[eye].position[0]=eye?0.032f:-0.032f;}
+ vr_render.active=1;vr_render.base_position=(real_point3d){10,20,30};vr_render.head_position=(real_point3d){10,20,30.5f};
+ vr_render.base_yaw=0.7f;vr_render.view.world_units_per_metre=0.33f;vr_render.view.controller_aim=1;
+ vr_render.view.controller_aim_requested=1;
+ vr_render.aim_forward=(real_vector3d){1,0,0};vr_render.aim_up=(real_vector3d){0,0,1};
+ vr_render.view.floating_hands=1;vr_render.view.left_hand_valid=1;vr_render.view.arms=1;
+ vr_render.view.hand_forward[0]=vr_render.view.left_hand_forward[0]=1;
+ vr_render.view.hand_up[2]=vr_render.view.left_hand_up[2]=vr_render.view.head_forward[0]=1;
+ vr_render.view.right_grip_position[0]=vr_render.view.hand_position[0]=0.5f;
+ vr_render.view.right_grip_position[1]=vr_render.view.hand_position[1]=-0.2f;
+ vr_render.view.left_grip_position[0]=0.4f;vr_render.view.left_grip_position[1]=0.3f;
+ vr_render.view.weapon_offset[0]=0.1f;vr_render.view.weapon_offset[1]=0.04f;vr_render.view.weapon_offset[2]=0.2f;
+ vr_render.view.shoulder_offset[0]=0.2f;vr_render.view.shoulder_offset[1]=-0.2f;
+ vr_render.view.seconds=0.01f;vr_render.view.right_trigger=0.3f;vr_render.view.left_grip=0.4f;
+ render.local_player_index=0;player_exists=unit_exists=centre_valid=1;perspective=_director_perspective_first_person;
+ fixture_player.unit_index=7;fixture_player.yaw=0.2f;fixture_player.pitch=-0.3f;
+ fixture_unit.unit.player_index=3;fixture_unit.object.parent_object_index=19;
+ fixture_unit.unit.aiming_vector=(real_vector3d){0,1,0};fixture_centre=(real_point3d){0.1f,0.02f,-0.04f};
+ scripted=inhibited=cinematic=1;
+ memset(widgets,0,sizeof(widgets));fixture_weapon=(struct first_person_weapon){7,9,TRUE};fixture_zoom=NONE;node_builds=visibility_calls=0;
+}
+static void tracking_guards(void){
+ int invalid;for(invalid=0;invalid<17;invalid++){reset();CHECK(halo_vr_presentation_tracking_valid());
+ switch(invalid){case 0:vr.initialized=0;break;case 1:vr.frame=vr.pending=0;break;case 2:vr.views.should_render=0;break;
+ case 3:vr.views.focused=0;break;case 4:vr.views.head.valid=0;break;case 5:vr.menus=1;break;case 6:vr.origin_valid=0;break;
+ case 7:vr.origin_yaw=NAN;break;case 8:vr.body_yaw=INFINITY;break;case 9:vr.views.head.orientation[3]=0;break;
+ case 10:vr.views.head.position[1]=NAN;break;case 11:vr.origin_position[2]=INFINITY;break;case 12:vr.views.eye[0].valid=0;break;
+ case 13:vr.views.eye[1].orientation[0]=NAN;break;case 14:vr.views.eye[1].position[0]=INFINITY;break;
+ case 15:settings.vehicle_first_person=settings.vehicle_seat_calibration=vr.vehicle.active=1;break;
+ case 16:settings.vehicle_first_person=settings.vehicle_seat_calibration=vr.vehicle.active=vr.vehicle.valid=1;vr.vehicle.origin_position[0]=NAN;break;}
+ CHECK(!halo_vr_presentation_tracking_valid());}
+ reset();vr.frame=0;vr.pending=1;CHECK(halo_vr_presentation_tracking_valid());CHECK(!vr.aim_synced);
+}
+static void cinematic_pose(void){
+ int frame;struct render_camera camera,old;struct vr_hands_frame hands;struct unit_datum original_unit;
+ reset();camera=head_camera();old=camera;CHECK(!legacy_vr_render_weapon_camera(&camera));camera_equal(camera,old);
+ CHECK(!legacy_vr_render_hands(&hands));
+ for(frame=0;frame<120;frame++){
+ real_point3d expected;real_vector3d offset;real yaw=(real)frame*0.02f;
+ reset();original_unit=fixture_unit;vr_render.view.hand_forward[0]=cosf(yaw);vr_render.view.hand_forward[1]=sinf(yaw);
+ vr_render.view.right_grip_position[2]=(real)frame*0.005f;vr_render.view.left_grip_position[2]=(real)frame*0.003f;
+ camera=head_camera();camera.position.x+=(real)frame;camera.forward=(real_vector3d){sinf(yaw),cosf(yaw),0};
+ CHECK(vr_render_weapon_camera(&camera));CHECK(vr_render.weapon_camera_set&&vr_render.weapon_camera_valid);
+ CHECK(vr_render_hands(&hands));CHECK(hands.right_valid&&hands.left_valid);
+ hand_point(vr_render.view.right_grip_position,&expected);point_equal(hands.right.position,expected);
+ hand_point(vr_render.view.left_grip_position,&expected);point_equal(hands.left.position,expected);
+ yaw_rotate(vr_render.base_yaw,vr_render.view.hand_forward,&offset);vector_equal(camera.forward,offset);
+ expected.x=hands.right.position.x-camera.forward.i*fixture_centre.x-hands.camera.left.i*fixture_centre.y-camera.up.i*fixture_centre.z;
+ expected.y=hands.right.position.y-camera.forward.j*fixture_centre.x-hands.camera.left.j*fixture_centre.y-camera.up.j*fixture_centre.z;
+ expected.z=hands.right.position.z-camera.forward.k*fixture_centre.x-hands.camera.left.k*fixture_centre.y-camera.up.k*fixture_centre.z;
+ point_equal(camera.position,expected);CHECK(camera.fov==1.2f);
+ CHECK(!vr.aiming&&!vr.aim_synced&&scripted&&inhibited&&cinematic);
+ CHECK(memcmp(&original_unit,&fixture_unit,sizeof(original_unit))==0);CHECK(fixture_player.yaw==0.2f&&fixture_player.pitch==-0.3f);
+ }
+ reset();centre_valid=0;camera=head_camera();CHECK(vr_render_weapon_camera(&camera));CHECK(vr_render_hands(&hands));
+ reset();vr_render.view.left_hand_valid=0;camera=head_camera();CHECK(vr_render_weapon_camera(&camera));CHECK(vr_render_hands(&hands));CHECK(!hands.left_valid&&hands.right_valid);
+}
+static void renderer_guards(void){
+ int invalid;for(invalid=0;invalid<23;invalid++){
+ struct render_camera camera,old;struct vr_hands_frame hands,before;reset();
+ switch(invalid){case 0:vr_render.active=0;break;case 1:render.local_player_index=1;break;case 2:player_exists=0;break;
+ case 3:fixture_player.unit_index=NONE;break;case 4:unit_exists=0;break;case 5:fixture_unit.unit.player_index=4;break;
+ case 6:fixture_unit.object.damage_flags=1u<<_object_dead_bit;break;case 7:fixture_unit.object.type=_object_type_vehicle;break;
+ case 8:perspective=_director_perspective_scripted;break;case 9:perspective=_director_perspective_neutral;break;
+ case 10:perspective=_director_perspective_third_person;break;case 11:vr_render.view.gamepad_aim=1;break;
+ case 12:vr_render.view.controller_aim=0;break;case 13:vr.views.focused=0;break;case 14:vr.menus=1;break;
+ case 15:vr_render.base_yaw=NAN;break;case 16:vr_render.base_position.z=INFINITY;break;
+ case 17:vr_render.view.world_units_per_metre=0;break;case 18:vr_render.view.hand_forward[0]=NAN;break;
+ case 19:vr_render.view.hand_up[2]=0;break;case 20:vr_render.view.hand_position[0]=INFINITY;break;
+ case 21:vr_render.view.right_grip_position[1]=NAN;break;case 22:vr_render.view.weapon_offset[2]=INFINITY;break;}
+ camera=head_camera();old=camera;memset(&hands,0x7c,sizeof(hands));before=hands;
+ CHECK(!vr_render_weapon_camera(&camera));camera_equal(camera,old);CHECK(!vr_render_hands(&hands));
+ CHECK(memcmp(&hands,&before,sizeof(hands))==0);CHECK(!vr.aiming&&!vr.aim_synced);
+ }
+}
+static void ordinary_equivalence(void){
+ int mode,frame;for(mode=0;mode<4;mode++)for(frame=0;frame<30;frame++){
+ struct render_camera actual,expected;struct vr_hands_frame current,old;int actual_result,old_result;
+ reset();vr.aiming=vr.aim_synced=1;scripted=inhibited=cinematic=0;centre_valid=mode!=1;
+ vr_render.view.gamepad_aim=mode==2;vr_render.view.controller_aim=mode!=3;vr_render.view.controller_aim_requested=mode!=2&&mode!=3;
+ vr_render.view.right_grip_position[2]=(real)frame*0.01f;actual=expected=head_camera();
+ actual_result=vr_render_weapon_camera(&actual);memset(&current,0,sizeof(current));
+ CHECK(vr_render_hands(&current));old_result=legacy_vr_render_weapon_camera(&expected);memset(&old,0,sizeof(old));
+ CHECK(legacy_vr_render_hands(&old));CHECK(actual_result==old_result);camera_equal(actual,expected);
+ CHECK(memcmp(&current,&old,sizeof(current))==0);CHECK(vr.aiming&&vr.aim_synced);
+ }
+}
+static void pose_transition(void){
+ struct render_camera camera;struct vr_hands_frame hands;reset();camera=head_camera();
+ CHECK(vr_render_weapon_camera(&camera));CHECK(vr_render_hands(&hands));
+ vr_render.view.controller_aim=0;camera=head_camera();CHECK(!vr_render_weapon_camera(&camera));
+ CHECK(!vr_render.weapon_camera_set&&!vr_render.weapon_camera_valid);CHECK(!vr_render_hands(&hands));
+ vr_render.view.controller_aim=1;camera=head_camera();CHECK(vr_render_weapon_camera(&camera));
+ vr.views.focused=0;camera=head_camera();CHECK(!vr_render_weapon_camera(&camera));
+ CHECK(!vr_render.weapon_camera_set&&!vr_render.weapon_camera_valid);CHECK(!vr_render_hands(&hands));
+ vr.views.focused=1;vr.aiming=vr.aim_synced=1;camera=head_camera();
+ CHECK(vr_render_weapon_camera(&camera));CHECK(vr_render_hands(&hands));CHECK(vr.aiming&&vr.aim_synced);
+}
+static void menu_and_tracking_visibility(void){
+ int mode,aiming,invalid;struct render_camera camera,original;struct vr_hands_frame hands;
+ /* The live widget appears before the platform menu poll or aim-release.
+ * Both camera and actual engine visibility must reject that first frame. */
+ for(mode=0;mode<3;mode++)for(aiming=0;aiming<2;aiming++){
+ reset();vr.aiming=aiming;vr_render.view.controller_aim_requested=mode==0;
+ vr_render.view.controller_aim=mode==0;vr_render.view.gamepad_aim=mode==2;
+ widgets[0]=1;CHECK(!vr.menus);camera=original=head_camera();
+ CHECK(!vr_render_first_person_visible(0));CHECK(!vr_render_weapon_camera(&camera));camera_equal(camera,original);
+ CHECK(!vr_render.weapon_camera_set&&!vr_render.weapon_camera_valid&&!vr_render_hands(&hands));
+ first_person_weapon_render_update();CHECK(!fixture_weapon.visible&&node_builds==0&&visibility_calls==1);
+ /* Pinned stock code demonstrates the reported head-attached fallback. */
+ legacy_first_person_weapon_render_update();CHECK(fixture_weapon.visible&&node_builds==1);
+ widgets[0]=0;camera=head_camera();CHECK(vr_render_first_person_visible(0));
+ (void)vr_render_weapon_camera(&camera);first_person_weapon_render_update();
+ CHECK(fixture_weapon.visible&&node_builds==2&&visibility_calls==3);
+ }
+ for(invalid=0;invalid<12;invalid++){
+ reset();vr.aiming=vr.aim_synced=1;camera=original=head_camera();
+ switch(invalid){case 0:vr_render.view.controller_aim=0;break;case 1:vr.views.focused=0;break;
+ case 2:vr_render.view.hand_forward[0]=NAN;break;case 3:vr_render.view.hand_up[2]=NAN;break;
+ case 4:vr_render.view.hand_up[2]=0;break;case 5:vr_render.view.hand_up[2]=0;vr_render.view.hand_up[0]=1;break;
+ case 6:vr_render.view.hand_position[0]=INFINITY;break;case 7:vr_render.view.right_grip_position[1]=NAN;break;
+ case 8:vr_render.view.world_units_per_metre=0;break;case 9:vr_render.base_yaw=INFINITY;break;
+ case 10:vr_render.view.weapon_offset[1]=NAN;break;case 11:fixture_centre.x=NAN;break;}
+ CHECK(!vr_render_weapon_camera(&camera));camera_equal(camera,original);CHECK(!vr_render_hands(&hands));
+ first_person_weapon_render_update();CHECK(!fixture_weapon.visible&&node_builds==0);
+ CHECK(vr.aiming&&vr.aim_synced); /* no aim release/recalibration on pose loss */
+ }
+ reset();vr.aiming=vr.aim_synced=1;camera=head_camera();
+ CHECK(vr_render_weapon_camera(&camera));first_person_weapon_render_update();CHECK(fixture_weapon.visible&&node_builds==1);
+ vr_render.view.controller_aim=0;camera=head_camera();CHECK(!vr_render_weapon_camera(&camera));
+ first_person_weapon_render_update();CHECK(!fixture_weapon.visible&&node_builds==1);
+ vr_render.view.controller_aim=1;vr_render.view.hand_forward[0]=-1;vr_render.view.hand_position[0]=-2;
+ vr_render.view.right_grip_position[0]=-2;camera=original=head_camera();
+ CHECK(vr_render_weapon_camera(&camera));CHECK(vr_render_hands(&hands)&&hands.right_valid);
+ CHECK(!near(camera.position.x,original.position.x));first_person_weapon_render_update();CHECK(fixture_weapon.visible&&node_builds==2);
+ CHECK(vr.aiming&&vr.aim_synced); /* valid out-of-view poses stay controller-attached */
+ /* A bad left pose hides that side only; never inject NaN node matrices. */
+ for(invalid=0;invalid<4;invalid++){
+ reset();vr.aiming=1;camera=head_camera();CHECK(vr_render_weapon_camera(&camera));
+ switch(invalid){case 0:vr_render.view.left_grip_position[1]=NAN;break;case 1:vr_render.view.left_hand_forward[0]=NAN;break;
+ case 2:vr_render.view.left_hand_up[2]=0;break;case 3:vr_render.view.left_hand_up[2]=0;vr_render.view.left_hand_up[0]=1;break;}
+ memset(&hands,0,sizeof(hands));CHECK(vr_render_hands(&hands));CHECK(hands.right_valid&&!hands.left_valid);
+ }
+}
+static void stock_visibility_contract(void){
+ int frame,menu,mode,zoom,view;
+ for(frame=0;frame<2;frame++)for(menu=0;menu<2;menu++)for(mode=0;mode<3;mode++)for(zoom=0;zoom<2;zoom++)for(view=1;view<=4;view++){
+ int expected;reset();vr.frame=frame;widgets[0]=menu;vr.aiming=1;perspective=view;fixture_zoom=zoom?0:NONE;
+ vr_render.view.controller_aim_requested=vr_render.view.controller_aim=mode==0;vr_render.view.gamepad_aim=mode==2;
+ legacy_first_person_weapon_render_update();expected=fixture_weapon.visible;node_builds=visibility_calls=0;
+ first_person_weapon_render_update();CHECK(fixture_weapon.visible==(expected&&!(frame&&menu)));
+ CHECK(node_builds==fixture_weapon.visible&&visibility_calls==1);
+ }
+ reset();render.local_player_index=NONE;first_person_weapon_render_update();CHECK(!visibility_calls&&!node_builds);
+ reset();fixture_weapon.weapon_index=NONE;first_person_weapon_render_update();CHECK(!visibility_calls&&!node_builds);
+ reset();fixture_weapon.unit_index=NONE;first_person_weapon_render_update();CHECK(!visibility_calls&&!node_builds);
+ reset();vr_render.active=FALSE;first_person_weapon_render_update();CHECK(!fixture_weapon.visible&&!node_builds);
+ vr.frame=0;first_person_weapon_render_update();CHECK(fixture_weapon.visible&&node_builds==1); /* unavailable runtime/flat */
+}
+int main(void){tracking_guards();cinematic_pose();renderer_guards();ordinary_equivalence();pose_transition();menu_and_tracking_visibility();stock_visibility_contract();
+ printf("PASS: %u production cinematic/disabled-aim controller camera and hands assertions\n",checks);return 0;}
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ parser.add_argument('--self-test', action='store_true', help='Verify the regression rejects the former menu rig visibility.')
+ args = parser.parse_args()
+ render = (ROOT / 'port/linux/game/vr_render.c').read_text()
+ vr = (ROOT / 'port/linux/src/vr.c').read_text()
+ math = (ROOT / 'source/math/matrix_math.c').read_text()
+ original = subprocess.check_output(['git', 'show', PIN + ':port/linux/game/vr_render.c'], cwd=ROOT, text=True)
+ legacy = '\n\n'.join(function(original, name).replace(name, 'legacy_' + name) for name in (
+ 'vr_render_weapon_camera', 'vr_render_hands'))
+ visibility = (ROOT / 'source/interface/first_person_weapons.c').read_text()
+ original_visibility = subprocess.check_output(['git', 'show', PIN + ':source/interface/first_person_weapons.c'], cwd=ROOT, text=True)
+ assert normalized(without_visibility_guard(function(visibility, 'first_person_weapon_render_update'),
+ 'vr_render_first_person_visible')) == normalized(
+ function(original_visibility, 'first_person_weapon_render_update')), 'Stock first-person zoom visibility changed'
+ hands = (ROOT / 'port/linux/game/vr_hands.c').read_text()
+ assert 'vr_render_hands(&frame)' in function(hands, 'vr_hands_pose')
+ pose = function(render, 'vr_render_weapon_camera')
+ assert 'halo_vr_aim(' not in pose and 'halo_vr_aim_release(' not in pose
+ source = HARNESS.replace('@@MATH@@', '\n\n'.join(function(math, name) for name in (
+ 'matrix4x3_rotation_from_vectors', 'matrix4x3_from_point_and_vectors'))).replace(
+ '@@TRACKING@@', '\n\n'.join(function(vr, name) for name in (
+ 'vehicle_view_active', 'halo_vr_presentation_tracking_valid'))).replace(
+ '@@RENDER@@', '\n\n'.join(function(render, name) for name in (
+ 'yaw_rotate', 'view_rotate', 'view_vector', 'hand_point', 'vr_render_controller_pose_valid',
+ 'vr_render_controller_presentation', 'vr_render_first_person_visible', 'vr_render_weapon_camera', 'vr_render_hands'))).replace(
+ '@@LEGACY@@', legacy).replace('@@VISIBILITY@@', function(visibility, 'first_person_weapon_render_update') + '\n\n' +
+ function(original_visibility, 'first_person_weapon_render_update').replace('first_person_weapon_render_update', 'legacy_first_person_weapon_render_update'))
+ compiler, vcvars = args.cc or os.environ.get('CC'), args.vcvarsall
+ if not compiler and os.name == 'nt':
+ candidates = sorted((Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio').glob(
+ '*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1]); vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('Select a C11 host compiler with --cc.')
+ with tempfile.TemporaryDirectory(prefix='halo-vr-cinematic-hands-', ignore_cleanup_errors=True) as directory:
+ temp = Path(directory); cfile = temp / 'test.c'; executable = temp / ('test.exe' if os.name == 'nt' else 'test')
+ cfile.write_text(source, encoding='utf-8')
+ include = ROOT / 'port/linux/src'; game = ROOT / 'port/linux/game'
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX', '/wd4505', '/DHALO_VR', '/D_CRT_SECURE_NO_WARNINGS',
+ '/I' + str(include), '/I' + str(game), str(cfile), '/Fe:' + str(executable), '/Fo:' + str(temp / 'test.obj')]
+ if vcvars:
+ for path in (vcvars, Path(compiler), temp, ROOT):
+ if any(c in str(path) for c in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported compiler/temp path shell metacharacter.')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\nif errorlevel 1 exit /b 1\n' +
+ subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-Wno-unused-function', '-DHALO_VR',
+ '-I', str(include), '-I', str(game), str(cfile), '-o', str(executable)]
+ if os.name != 'nt':
+ command.append('-lm')
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(executable)], cwd=temp, check=True)
+ if args.self_test:
+ old_visibility = function(original_visibility, 'first_person_weapon_render_update')
+ mutant = source.replace(function(visibility, 'first_person_weapon_render_update'), old_visibility)
+ cfile.write_text(mutant, encoding='utf-8')
+ subprocess.run(command, cwd=temp, check=True, capture_output=True, text=True)
+ result = subprocess.run([str(executable)], cwd=temp, capture_output=True, text=True)
+ assert result.returncode != 0 and '!fixture_weapon.visible&&node_builds==0' in result.stderr, result.stderr
+ print('PASS: regression rejects pinned menu/pose-loss rig visibility')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_driving.py b/tools/test_vr_driving.py
new file mode 100644
index 0000000000000000000000000000000000000000..3e9450433a8a1884c5dc8aebaf4b25e8be2181b8
--- /dev/null
+++ b/tools/test_vr_driving.py
@@ -0,0 +1,446 @@
+#!/usr/bin/env python3
+"""Execute production driver inputs and wheel math with deterministic VR services.
+
+No engine, headset or retail data is executed. The tests extract the actual
+platform functions, include the production math header, and mock only services.
+"""
+import argparse
+from pathlib import Path
+
+from test_vr_pointer_radar import function
+from test_vr_zoom_aim import compile_and_run
+
+ROOT = Path(__file__).resolve().parents[1]
+
+HARNESS = r'''
+#define _USE_MATH_DEFINES
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "vr.h"
+#include "vr_host.h"
+#include "vr_locomotion.h"
+#include "vr_vehicle.h"
+#include "vr_driving.h"
+#ifndef M_PI
+#define M_PI 3.14159265358979323846
+#endif
+#define RECENTRE_SECONDS 0.75f
+#define GAMEPAD_RECENTRE_SECONDS 1.0f
+#define MELEE_SPEED 2.5f
+#define MELEE_INTERVAL_MS 600
+typedef unsigned char BYTE;
+typedef short SHORT;
+enum { XINPUT_GAMEPAD_A,XINPUT_GAMEPAD_B,XINPUT_GAMEPAD_X,XINPUT_GAMEPAD_Y,
+ XINPUT_GAMEPAD_BLACK,XINPUT_GAMEPAD_WHITE,XINPUT_GAMEPAD_LEFT_TRIGGER,XINPUT_GAMEPAD_RIGHT_TRIGGER };
+enum { XINPUT_GAMEPAD_DPAD_UP=1,XINPUT_GAMEPAD_DPAD_DOWN=2,XINPUT_GAMEPAD_DPAD_LEFT=4,
+ XINPUT_GAMEPAD_DPAD_RIGHT=8,XINPUT_GAMEPAD_START=16,XINPUT_GAMEPAD_BACK=32,
+ XINPUT_GAMEPAD_LEFT_THUMB=64,XINPUT_GAMEPAD_RIGHT_THUMB=128 };
+typedef struct { BYTE bAnalogButtons[8]; unsigned short wButtons; SHORT sThumbLX,sThumbLY,sThumbRX,sThumbRY; } XINPUT_GAMEPAD;
+static unsigned checks,logs,two_hand_updates,weapon_calls;
+#define CHECK(c) do { checks++;if(!(c)){fprintf(stderr,"line %d: %s\n",__LINE__,#c);exit(1);} } while(0)
+static int near(float a,float b){return fabsf(a-b)<0.0001f;}
+static struct {
+ int initialized,frame,pending,menus,origin_valid,aim_synced,aiming,aimed,long_gun;
+ int snap_latched,turn_control_allowed,tutorial_turn_flags,tutorial_head_valid;
+ int menu_open;
+ int hand_valid,melee_frames,two_handed,gamepad_back;
+ unsigned long last_ticks,melee_ticks;
+ float origin_position[3],origin_yaw,body_yaw,hand_position[3];
+ float recentre_time,gamepad_recentre_time,two_handed_weight;
+ struct halo_vr_vehicle_reference vehicle;
+ struct halo_vr_locomotion_heading movement_heading;
+ struct vr_host_views views;
+ struct vr_host_input input;
+ struct vr_host_pose menu_pose;
+} vr;
+static struct {
+ int gamepad_aim,controller_aim,gamepad_snap,standing,vehicle_first_person,vehicle_seat_calibration;
+ int smooth_turn,melee_gesture;
+ float snap_angle,smooth_speed,player_height,test_turn;
+} settings;
+static struct halo_vr_driving_state driving;
+static struct {int active,armed,down,blocked,position_valid;short x,y;} menu_pointer;
+static unsigned long ticks;
+static unsigned long GetTickCount(void){ticks+=100;return ticks;}
+static void settings_read(void){}
+static void platform_log(const char *format,const char *mode){CHECK(format&&mode);logs++;}
+static void two_handed_update(float seconds){CHECK(isfinite(seconds));two_hand_updates++;}
+static int menu_pointer_fire_blocked(void){return 0;}
+static void weapon_pointing(float forward[3],float up[3]){
+ weapon_calls++;forward[0]=0;forward[1]=0;forward[2]=-1;up[0]=0;up[1]=1;up[2]=0;
+}
+@@PRODUCTION@@
+static void pose(struct vr_host_pose *p,float yaw,float x,float y,float z){
+ memset(p,0,sizeof(*p));p->valid=1;p->orientation[1]=sinf(yaw/2);p->orientation[3]=cosf(yaw/2);
+ p->position[0]=x;p->position[1]=y;p->position[2]=z;
+}
+static void reset(void){
+ memset(&vr,0,sizeof(vr));memset(&settings,0,sizeof(settings));memset(&driving,0,sizeof(driving));
+ memset(&menu_pointer,0,sizeof(menu_pointer));logs=two_hand_updates=weapon_calls=0;ticks=1000;
+ vr.initialized=vr.frame=vr.origin_valid=vr.aim_synced=vr.turn_control_allowed=1;
+ vr.views.should_render=vr.views.focused=1;settings.controller_aim=1;
+ settings.snap_angle=(float)M_PI/4;settings.smooth_speed=2;settings.player_height=1.7f;
+ settings.vehicle_first_person=settings.vehicle_seat_calibration=1;
+ pose(&vr.views.head,0,0,1.2f,0);pose(&vr.views.eye[0],0,-.032f,1.2f,0);
+ pose(&vr.views.eye[1],0,.032f,1.2f,0);pose(&vr.views.aim[1],0,.2f,1,-.4f);
+ pose(&vr.views.grip[0],0,-.2f,1,-.4f);pose(&vr.views.grip[1],0,.2f,1,-.4f);
+ vr.input.active[0]=vr.input.active[1]=1;vr.body_yaw=.4f;
+}
+static void register_driver(int mode){
+ halo_vr_vehicle_controls(mode,.7f);
+ if(mode){vr.vehicle.active=vr.vehicle.valid=1;vr.vehicle.forward_yaw=.7f;}
+}
+static void wheel(float angle){
+ float x=.2f*cosf(angle),y=-.2f*sinf(angle);
+ pose(&vr.views.grip[0],0,-x,1-y,-.4f);pose(&vr.views.grip[1],0,x,1+y,-.4f);
+}
+static void engage(void){
+ float yaw,pitch;
+ vr.input.left_grip=vr.input.right_grip=0;
+ CHECK(halo_vr_vehicle_steering(.7f,0,.016f,&yaw,&pitch)==1);
+ vr.input.left_grip=vr.input.right_grip=.9f;
+ CHECK(halo_vr_vehicle_steering(.7f,0,.016f,&yaw,&pitch)==1);
+ CHECK(driving.wheel_valid&&near(yaw,.7f)&&near(pitch,0));
+}
+static void driver_aim_and_sticks(void){
+ int mode,head;
+ for(mode=1;mode<=4;mode++)for(head=0;head<2;head++){
+ float yaw=-9,pitch=-8;XINPUT_GAMEPAD pad;
+ reset();settings.controller_aim=!head;register_driver(mode);vr.vehicle.turn=.8f;
+ CHECK(halo_vr_aim(-2,1,&yaw,&pitch)==1);CHECK(near(yaw,.7f)&&near(pitch,0));
+ CHECK(!weapon_calls&&vr.aiming&&vr.aim_synced&&near(vr.vehicle.turn,0));
+ pose(&vr.views.head,1.8f,.5f,1.1f,.6f);pose(&vr.views.aim[1],-2.1f,1,2,3);
+ yaw=1.1f;pitch=.3f;CHECK(halo_vr_aim(yaw,1,&yaw,&pitch)==2);
+ CHECK(near(yaw,1.1f)&&near(pitch,.3f)&&!weapon_calls);
+ vr.aimed=1;vr.input.right_stick[0]=.8f;vr.input.right_stick[1]=-.6f;
+ memset(&pad,0,sizeof(pad));halo_vr_gamepad(&pad);
+ CHECK(pad.sThumbRX>26000&&pad.sThumbRY<-19600);
+ vr.input.buttons=0xffff;vr.input.left_trigger=.4f;vr.input.right_trigger=.8f;
+ memset(&pad,0,sizeof(pad));halo_vr_gamepad(&pad);
+ CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_A]==(mode==HALO_VR_DRIVING_WARTHOG?0:255)&&pad.bAnalogButtons[XINPUT_GAMEPAD_B]==(mode==HALO_VR_DRIVING_WARTHOG?0:255));
+ CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_X]==255&&pad.bAnalogButtons[XINPUT_GAMEPAD_Y]==255);
+ CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_BLACK]==255&&pad.bAnalogButtons[XINPUT_GAMEPAD_WHITE]==255);
+ CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER]==102&&pad.bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER]==204);
+ CHECK(pad.wButtons==255);
+ }
+ reset();settings.gamepad_aim=1;register_driver(HALO_VR_DRIVING_BANSHEE);
+ {
+ float yaw=-9,pitch=-8;CHECK(halo_vr_vehicle_controls_ready());
+ CHECK(halo_vr_aim(1.4f,0,&yaw,&pitch)==2&&near(yaw,1.4f)&&near(pitch,0));
+ CHECK(near(vr.vehicle.turn,.7f)); /* Original gamepad view follows its facing. */
+ CHECK(!halo_vr_vehicle_steering(.7f,.9f,.016f,&yaw,&pitch));
+ }
+ reset();vr.aimed=1;vr.input.right_stick[0]=.8f;
+ {
+ XINPUT_GAMEPAD pad;memset(&pad,0,sizeof(pad));halo_vr_gamepad(&pad);CHECK(pad.sThumbRX==0);
+ settings.gamepad_aim=1;halo_vr_gamepad(&pad);CHECK(pad.sThumbRX>26000);
+ }
+}
+static void bounded_steering(void){
+ int sample;float yaw,pitch,previous=-2;
+ reset();register_driver(HALO_VR_DRIVING_WARTHOG);
+ for(sample=-150;sample<=150;sample++){
+ float lateral=sample*.01f,curve=halo_vr_driving_curve(lateral);
+ CHECK(isfinite(curve)&&curve>=-1&&curve<=1&&curve>=previous);previous=curve;
+ CHECK(near(curve,-halo_vr_driving_curve(-lateral)));
+ CHECK(halo_vr_vehicle_steering(.7f,lateral,.016f,&yaw,&pitch)==1);
+ CHECK(near(yaw,.7f+curve*(float)M_PI/3)&&near(pitch,0));
+ }
+ CHECK(near(halo_vr_driving_curve(.1f),0)&&near(halo_vr_driving_curve(-.1f),0));
+ CHECK(near(halo_vr_driving_curve(1),1)&&near(halo_vr_driving_curve(-1),-1));
+ CHECK(halo_vr_vehicle_steering(8*(float)M_PI,.9f,0,&yaw,&pitch)==1&&fabsf(yaw)<(float)M_PI);
+ CHECK(!halo_vr_vehicle_steering(NAN,0,.016f,&yaw,&pitch));
+ CHECK(!halo_vr_vehicle_steering(0,0,.016f,NULL,&pitch));
+ CHECK(!halo_vr_vehicle_steering(0,0,.016f,&yaw,NULL));
+ for(sample=2;sample<=4;sample++){
+ register_driver(sample);yaw=7;pitch=8;
+ CHECK(!halo_vr_vehicle_steering(.7f,.9f,.016f,&yaw,&pitch)&&yaw==7&&pitch==8);
+ }
+ halo_vr_vehicle_controls(99,0);CHECK(driving.mode==0);
+ halo_vr_vehicle_controls(1,INFINITY);CHECK(driving.mode==0);
+ halo_vr_vehicle_controls(1,1e30f);CHECK(isfinite(driving.hull_yaw)&&fabsf(driving.hull_yaw)<=(float)M_PI);
+}
+static void independent_accelerator(void){
+ int step,mode,failure;float yaw,pitch;XINPUT_GAMEPAD pad;
+ /* A provides full forward with an otherwise purely lateral left stick.
+ The merged XInput A is consumed too; it must never become the stock brake. */
+ for(step=-100;step<=100;step++){
+ reset();register_driver(HALO_VR_DRIVING_WARTHOG);
+ CHECK(halo_vr_vehicle_accelerator(0)==0&&!driving.accelerator_blocked);
+ vr.input.buttons=VR_BUTTON_A;vr.input.left_stick[0]=step/100.f;
+ memset(&pad,0,sizeof(pad));pad.bAnalogButtons[XINPUT_GAMEPAD_A]=255;
+ halo_vr_gamepad(&pad);
+ CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_A]==0&&pad.sThumbLY==0);
+ CHECK(labs(pad.sThumbLX-(long)(vr.input.left_stick[0]*32767))<=1);
+ CHECK(halo_vr_vehicle_accelerator(0)==1&&driving.accelerator_consumed);
+ CHECK(halo_vr_vehicle_steering(.7f,-vr.input.left_stick[0],.016f,&yaw,&pitch)==1);
+ CHECK(near(yaw,.7f+halo_vr_driving_curve(-vr.input.left_stick[0])*(float)M_PI/3));
+ CHECK(halo_vr_vehicle_accelerator(-.75f)==-.75f);
+ CHECK(halo_vr_vehicle_accelerator(.75f)==1);
+ vr.input.buttons=0;CHECK(halo_vr_vehicle_accelerator(0)==0&&!driving.accelerator_consumed);
+ CHECK(halo_vr_vehicle_accelerator(.75f)==.75f);
+ }
+ /* Two hands turn the optional wheel independently of the A accelerator. */
+ reset();register_driver(1);CHECK(halo_vr_vehicle_accelerator(0)==0);engage();wheel(.4f);
+ vr.input.buttons=VR_BUTTON_A;CHECK(halo_vr_vehicle_accelerator(0)==1);
+ CHECK(halo_vr_vehicle_steering(.7f,1,.016f,&yaw,&pitch)==1);
+ CHECK(near(yaw,.7f-.4f/((float)M_PI/4)*(float)M_PI/3));
+ /* A held during boarding, a menu, tracking loss or a seat/recenter change
+ stays blocked until a fresh tracked release. Repeated simulation samples
+ cannot turn the same old held press into a new accelerator. */
+ for(failure=0;failure<9;failure++){
+ reset();register_driver(1);CHECK(halo_vr_vehicle_accelerator(0)==0);
+ vr.input.buttons=VR_BUTTON_A;CHECK(halo_vr_vehicle_accelerator(0)==1);
+ switch(failure){
+ case 0:halo_vr_vehicle_controls(0,0);halo_vr_vehicle_controls(1,.7f);break;
+ case 1:halo_vr_menus(1);memset(&pad,0,sizeof(pad));halo_vr_gamepad(&pad);
+ CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_A]==255);halo_vr_menus(0);break;
+ case 2:vr.views.focused=0;CHECK(halo_vr_vehicle_accelerator(.75f)==0);vr.views.focused=1;break;
+ case 3:vr.frame=0;CHECK(halo_vr_vehicle_accelerator(.75f)==0);vr.frame=1;break;
+ case 4:vr.views.head.valid=0;CHECK(halo_vr_vehicle_accelerator(.75f)==0);vr.views.head.valid=1;break;
+ case 5:vr.views.eye[0].position[0]=NAN;CHECK(halo_vr_vehicle_accelerator(.75f)==0);vr.views.eye[0].position[0]=-.032f;break;
+ case 6:vr.input.active[1]=0;CHECK(halo_vr_vehicle_accelerator(.75f)==.75f);vr.input.active[1]=1;break;
+ case 7:halo_vr_vehicle_reference(7,42,0,.7f);break;
+ case 8:recentre();break;
+ }
+ for(step=0;step<5;step++)CHECK(halo_vr_vehicle_accelerator(0)==0&&driving.accelerator_blocked);
+ vr.input.buttons=0;CHECK(halo_vr_vehicle_accelerator(0)==0);
+ vr.input.buttons=VR_BUTTON_A;CHECK(halo_vr_vehicle_accelerator(0)==1);
+ }
+ reset();vr.input.buttons=VR_BUTTON_A;register_driver(1);
+ CHECK(halo_vr_vehicle_accelerator(0)==0);vr.input.buttons=0;CHECK(halo_vr_vehicle_accelerator(0)==0);
+ vr.input.buttons=VR_BUTTON_A;CHECK(halo_vr_vehicle_accelerator(0)==1);
+ /* A carried out of the car must be released before it can jump. A new
+ on-foot press, all other vehicle types, menus and gamepad aim remain stock. */
+ register_driver(0);memset(&pad,0,sizeof(pad));pad.bAnalogButtons[XINPUT_GAMEPAD_A]=255;
+ halo_vr_gamepad(&pad);CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_A]==0);
+ vr.input.buttons=0;memset(&pad,0,sizeof(pad));halo_vr_gamepad(&pad);CHECK(!driving.accelerator_consumed);
+ vr.input.buttons=VR_BUTTON_A;halo_vr_gamepad(&pad);CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_A]==255);
+ for(mode=2;mode<=4;mode++){
+ reset();register_driver(mode);vr.input.buttons=VR_BUTTON_A;
+ CHECK(halo_vr_vehicle_accelerator(.75f)==.75f);memset(&pad,0,sizeof(pad));halo_vr_gamepad(&pad);
+ CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_A]==255);
+ }
+ reset();settings.gamepad_aim=1;register_driver(1);vr.input.buttons=VR_BUTTON_A;driving.accelerator_consumed=1;
+ CHECK(halo_vr_vehicle_accelerator(.75f)==.75f);memset(&pad,0,sizeof(pad));halo_vr_gamepad(&pad);
+ CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_A]==255);
+ reset();vr.initialized=0;CHECK(halo_vr_vehicle_accelerator(.75f)==.75f);
+ reset();register_driver(1);CHECK(halo_vr_vehicle_accelerator(NAN)==0&&driving.accelerator_blocked);
+}
+static void dedicated_brake(void){
+ int step,mode;float yaw,pitch;XINPUT_GAMEPAD pad;
+ for(step=-100;step<=100;step++){
+ reset();register_driver(1);CHECK(halo_vr_vehicle_accelerator(0)==0);
+ vr.input.buttons=VR_BUTTON_A;CHECK(halo_vr_vehicle_accelerator(0)==1);
+ vr.input.buttons=VR_BUTTON_A|VR_BUTTON_B;
+ CHECK(halo_vr_vehicle_brake()&&driving.brake_consumed);
+ CHECK(halo_vr_vehicle_accelerator(0)==0&&!driving.accelerator_blocked);
+ CHECK(halo_vr_vehicle_accelerator(-.75f)==0);
+ CHECK(halo_vr_vehicle_steering(.7f,step/100.f,.016f,&yaw,&pitch)==1);
+ CHECK(near(yaw,.7f+halo_vr_driving_curve(step/100.f)*(float)M_PI/3));
+ memset(&pad,0,sizeof(pad));pad.bAnalogButtons[XINPUT_GAMEPAD_A]=255;pad.bAnalogButtons[XINPUT_GAMEPAD_B]=255;
+ vr.melee_frames=3;halo_vr_gamepad(&pad);
+ CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_A]==0&&pad.bAnalogButtons[XINPUT_GAMEPAD_B]==0&&vr.melee_frames==0);
+ /* Releasing only B restores continuously held A without a new A edge. */
+ vr.input.buttons=VR_BUTTON_A;CHECK(!halo_vr_vehicle_brake()&&!driving.brake_consumed);
+ CHECK(halo_vr_vehicle_accelerator(0)==1);
+ vr.input.buttons=VR_BUTTON_B;CHECK(halo_vr_vehicle_brake()&&halo_vr_vehicle_accelerator(.75f)==0);
+ CHECK(!driving.accelerator_blocked); /* Released A rearms even while B held. */
+ vr.input.buttons=0;CHECK(!halo_vr_vehicle_brake()&&halo_vr_vehicle_accelerator(-.75f)==-.75f);
+ }
+ /* A physical punch or a duplicate SDL B by itself is never a brake. */
+ reset();vr.melee_frames=3;register_driver(1);CHECK(vr.melee_frames==0);
+ vr.melee_frames=3;memset(&pad,0,sizeof(pad));pad.bAnalogButtons[XINPUT_GAMEPAD_B]=255;
+ halo_vr_gamepad(&pad);CHECK(!halo_vr_vehicle_brake()&&pad.bAnalogButtons[XINPUT_GAMEPAD_B]==0&&vr.melee_frames==0);
+ settings.melee_gesture=1;input_update();
+ vr.views.grip[1].position[2]-=.4f;input_update();CHECK(vr.melee_frames==0&&!halo_vr_vehicle_brake());
+ register_driver(0);vr.views.grip[1].position[2]-=.4f;input_update();CHECK(vr.melee_frames==3);
+ /* B carried out of the driver seat does not become an accidental melee.
+ Menus keep Back; a fresh on-foot B and every other vehicle keep melee. */
+ reset();register_driver(1);vr.input.buttons=VR_BUTTON_B;CHECK(halo_vr_vehicle_brake());
+ register_driver(0);memset(&pad,0,sizeof(pad));pad.bAnalogButtons[XINPUT_GAMEPAD_B]=255;
+ halo_vr_gamepad(&pad);CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_B]==0);
+ vr.input.buttons=0;memset(&pad,0,sizeof(pad));halo_vr_gamepad(&pad);CHECK(!driving.brake_consumed);
+ vr.input.buttons=VR_BUTTON_B;halo_vr_gamepad(&pad);CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_B]==255);
+ reset();register_driver(1);vr.input.buttons=VR_BUTTON_B;halo_vr_menus(1);
+ CHECK(!halo_vr_vehicle_brake());memset(&pad,0,sizeof(pad));halo_vr_gamepad(&pad);
+ CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_B]==255);
+ for(mode=0;mode<=4;mode++)if(mode!=1){
+ reset();register_driver(mode);vr.input.buttons=VR_BUTTON_B;CHECK(!halo_vr_vehicle_brake());
+ memset(&pad,0,sizeof(pad));halo_vr_gamepad(&pad);CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_B]==255);
+ }
+ reset();settings.gamepad_aim=1;register_driver(1);vr.input.buttons=VR_BUTTON_B;
+ CHECK(!halo_vr_vehicle_brake());memset(&pad,0,sizeof(pad));halo_vr_gamepad(&pad);CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_B]==255);
+ reset();register_driver(1);vr.input.buttons=VR_BUTTON_B;vr.views.focused=0;CHECK(!halo_vr_vehicle_brake());
+ vr.views.focused=1;vr.input.active[1]=0;CHECK(!halo_vr_vehicle_brake());
+}
+static void flat_and_physical_gamepad_fallback(void){
+ float yaw=9,pitch=8;XINPUT_GAMEPAD pad,before;
+ reset();vr.initialized=vr.frame=vr.pending=0;vr.views.focused=vr.views.head.valid=0;
+ halo_vr_vehicle_controls(HALO_VR_DRIVING_WARTHOG,.7f);
+ CHECK(driving.mode==HALO_VR_DRIVING_NONE&&halo_vr_vehicle_controls_ready());
+ CHECK(!halo_vr_vehicle_steering(.7f,1,.016f,&yaw,&pitch)&&yaw==9&&pitch==8);
+ CHECK(!halo_vr_aim(.7f,0,&yaw,&pitch)&&yaw==9&&pitch==8);
+ memset(&pad,0,sizeof(pad));pad.sThumbLX=16000;pad.sThumbLY=27000;
+ pad.sThumbRX=-18000;pad.sThumbRY=12000;pad.bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER]=201;
+ before=pad;halo_vr_gamepad(&pad);CHECK(!memcmp(&pad,&before,sizeof(pad)));
+ /* A physical-pad setting preserves stock input even when VR loses focus
+ or tracking. Only the active tracked-driver setting neutralizes actions. */
+ reset();settings.gamepad_aim=1;register_driver(HALO_VR_DRIVING_WARTHOG);
+ vr.frame=vr.pending=0;vr.views.focused=vr.views.head.valid=0;
+ CHECK(halo_vr_vehicle_controls_ready());
+ CHECK(!halo_vr_vehicle_steering(.7f,1,.016f,&yaw,&pitch)&&yaw==9&&pitch==8);
+ CHECK(!halo_vr_aim(.7f,0,&yaw,&pitch)&&yaw==9&&pitch==8);
+ halo_vr_gamepad(&pad);CHECK(!memcmp(&pad,&before,sizeof(pad)));
+ settings.gamepad_aim=0;CHECK(!halo_vr_vehicle_controls_ready());
+ CHECK(halo_vr_vehicle_steering(.7f,1,.016f,&yaw,&pitch)==2&&near(yaw,.7f)&&near(pitch,0));
+}
+static void wheel_geometry_and_head_independence(void){
+ int sample;float yaw,pitch;
+ reset();register_driver(1);engage();
+ for(sample=-100;sample<=100;sample++){
+ float angle=sample*.012f,steer=-angle/((float)M_PI/4);
+ if(steer<-1)steer=-1;if(steer>1)steer=1;
+ wheel(angle);CHECK(halo_vr_vehicle_steering(.7f,1,.016f,&yaw,&pitch)==1);
+ CHECK(near(driving.wheel_steer,steer)&&near(yaw,.7f+steer*(float)M_PI/3)&&near(pitch,0));
+ }
+ wheel(.3f);CHECK(halo_vr_vehicle_steering(.7f,0,.016f,&yaw,&pitch)==1);
+ {
+ float saved=yaw;
+ for(sample=-60;sample<=60;sample++){
+ float angle=sample*.05f;
+ pose(&vr.views.head,angle,sample*.1f,1.2f+sample*.005f,-sample*.02f);
+ CHECK(halo_vr_vehicle_steering(.7f,0,.016f,&yaw,&pitch)==1&&near(yaw,saved));
+ /* Sideways roll and vertical head look also leave the captured plane. */
+ vr.views.head.orientation[0]=sinf(angle/2);vr.views.head.orientation[1]=0;
+ vr.views.head.orientation[2]=0;vr.views.head.orientation[3]=cosf(angle/2);
+ CHECK(halo_vr_vehicle_steering(.7f,0,.016f,&yaw,&pitch)==1&&near(yaw,saved));
+ vr.views.head.orientation[0]=0;vr.views.head.orientation[2]=sinf(angle/2);
+ CHECK(halo_vr_vehicle_steering(.7f,0,.016f,&yaw,&pitch)==1&&near(yaw,saved));
+ }
+ }
+ /* First engagement from a pitched/yawed pose captures that fixed axis. */
+ for(sample=-30;sample<=30;sample++){
+ struct halo_vr_driving_state state={0};float axis[3]={sinf(sample*.05f),0,-cosf(sample*.05f)};
+ float left[3]={-.2f*cosf(sample*.05f),1,-.2f*sinf(sample*.05f)};
+ float right[3]={-left[0],1,-left[2]};
+ CHECK(halo_vr_driving_wheel(&state,axis,left,right)&&near(state.wheel_steer,0));
+ axis[0]=1;axis[1]=axis[2]=0;
+ CHECK(halo_vr_driving_wheel(&state,axis,left,right)&&near(state.wheel_steer,0));
+ }
+ {
+ struct halo_vr_driving_state state={0};float axis[3]={0,0,-1},left[3]={0,0,0},right[3]={0,0,0},line[3];
+ right[0]=.10f;CHECK(halo_vr_driving_line(axis,left,right,line));
+ right[0]=1.20f;CHECK(halo_vr_driving_line(axis,left,right,line));
+ right[0]=.099f;CHECK(!halo_vr_driving_line(axis,left,right,line));
+ right[0]=1.201f;CHECK(!halo_vr_driving_line(axis,left,right,line));
+ right[0]=0;right[2]=.4f;CHECK(!halo_vr_driving_line(axis,left,right,line));
+ right[0]=.4f;right[2]=0;axis[0]=NAN;CHECK(!halo_vr_driving_wheel(&state,axis,left,right));
+ axis[0]=axis[2]=0;CHECK(!halo_vr_driving_wheel(&state,axis,left,right));
+ }
+}
+static void loss_and_rearm(void){
+ int failure;float yaw,pitch;
+ for(failure=0;failure<18;failure++){
+ reset();register_driver(1);engage();wheel(.5f);
+ CHECK(halo_vr_vehicle_steering(.7f,0,.016f,&yaw,&pitch)==1&&!near(yaw,.7f));
+ switch(failure){
+ case 0:vr.frame=0;break;case 1:vr.views.focused=0;break;case 2:vr.views.should_render=0;break;
+ case 3:vr.views.head.valid=0;break;case 4:vr.views.head.orientation[0]=NAN;break;
+ case 5:vr.views.head.position[0]=INFINITY;break;case 6:vr.views.eye[0].valid=0;break;
+ case 7:vr.views.eye[1].orientation[3]=0;break;case 8:vr.views.eye[0].position[2]=NAN;break;
+ case 9:vr.origin_valid=0;break;case 10:vr.origin_yaw=NAN;break;case 11:halo_vr_menus(1);break;
+ case 12:vr.views.grip[0].valid=0;break;case 13:vr.input.active[1]=0;break;
+ case 14:vr.views.grip[1].position[0]=INFINITY;break;
+ case 15:vr.views.grip[0].orientation[0]=NAN;break;
+ case 16:vr.views.grip[1].position[0]=vr.views.grip[0].position[0];
+ vr.views.grip[1].position[1]=vr.views.grip[0].position[1];break;
+ case 17:vr.input.left_grip=NAN;break;
+ }
+ CHECK(halo_vr_vehicle_steering(.7f,0,.016f,&yaw,&pitch)==(failure<12?2:1));
+ CHECK(near(yaw,.7f)&&near(pitch,0)&&!driving.wheel_valid&&driving.wheel_blocked);
+ if(failure<12){float a=9,b=8;CHECK(!halo_vr_vehicle_controls_ready());CHECK(!halo_vr_aim(.2f,0,&a,&b)&&!vr.aiming);}
+ vr.frame=vr.origin_valid=1;vr.views.focused=vr.views.should_render=1;vr.origin_yaw=0;halo_vr_menus(0);
+ pose(&vr.views.head,0,0,1.2f,0);pose(&vr.views.eye[0],0,-.032f,1.2f,0);pose(&vr.views.eye[1],0,.032f,1.2f,0);
+ vr.input.active[0]=vr.input.active[1]=1;vr.input.left_grip=vr.input.right_grip=.9f;wheel(0);
+ CHECK(halo_vr_vehicle_steering(.7f,0,.016f,&yaw,&pitch)==1&&!driving.wheel_valid&&near(yaw,.7f));
+ vr.input.left_grip=0;CHECK(halo_vr_vehicle_steering(.7f,0,.016f,&yaw,&pitch)==1&&!driving.wheel_valid);
+ engage();CHECK(driving.wheel_valid);
+ }
+ reset();register_driver(1);CHECK(halo_vr_vehicle_steering(.7f,NAN,.016f,&yaw,&pitch)==2&&near(yaw,.7f));
+ CHECK(halo_vr_vehicle_steering(.7f,0,INFINITY,&yaw,&pitch)==2);
+ CHECK(halo_vr_vehicle_steering(.7f,0,-1,&yaw,&pitch)==2);
+ reset();register_driver(1);engage();register_driver(2);CHECK(!driving.wheel_valid&&driving.wheel_blocked);
+ register_driver(1);CHECK(!driving.wheel_valid&&driving.wheel_blocked);
+}
+static void seat_identity_and_recentre_alignment(void){
+ float yaw,pitch;
+ reset();register_driver(1);CHECK(halo_vr_aim(2,0,&yaw,&pitch)==1);engage();
+ halo_vr_vehicle_reference(7,42,0,.7f);
+ CHECK(!driving.wheel_valid&&driving.wheel_blocked&&!vr.aim_synced);
+ CHECK(halo_vr_aim(2,0,&yaw,&pitch)==1&&near(yaw,.7f)&&near(pitch,0));
+ CHECK(halo_vr_aim(2,0,&yaw,&pitch)==2);
+ engage();halo_vr_vehicle_controls(1,-.4f);halo_vr_vehicle_reference(7,43,0,-.4f);
+ CHECK(!driving.wheel_valid&&driving.wheel_blocked);
+ CHECK(halo_vr_aim(2,0,&yaw,&pitch)==1&&near(yaw,-.4f));
+ engage();halo_vr_vehicle_reference(7,43,1,-.4f);
+ CHECK(!driving.wheel_valid&&driving.wheel_blocked);
+ CHECK(halo_vr_aim(2,0,&yaw,&pitch)==1&&near(yaw,-.4f));
+ engage();recentre();CHECK(!driving.wheel_valid&&driving.wheel_blocked);
+ CHECK(halo_vr_aim(2,0,&yaw,&pitch)==1&&near(yaw,-.4f));
+}
+static void invalid_hull_is_neutral(void){
+ float yaw=9,pitch=8;reset();register_driver(1);engage();
+ halo_vr_vehicle_orientation(NULL,.7f);
+ CHECK(!halo_vr_vehicle_controls_ready());CHECK(!halo_vr_aim(.7f,0,&yaw,&pitch)&&yaw==9&&pitch==8);
+ CHECK(halo_vr_vehicle_steering(.7f,.8f,.016f,&yaw,&pitch)==2&&near(yaw,.7f)&&near(pitch,0));
+ CHECK(!driving.wheel_valid&&driving.wheel_blocked);
+ settings.gamepad_aim=1;CHECK(halo_vr_vehicle_controls_ready());
+ vr.initialized=0;settings.gamepad_aim=0;CHECK(halo_vr_vehicle_controls_ready());
+}
+static void turns_and_recentre(void){
+ int smooth,step;
+ for(smooth=0;smooth<2;smooth++){
+ reset();settings.smooth_turn=smooth;register_driver(1);
+ for(step=0;step<20;step++){tutorial_turn_input(.9f,.016f);CHECK(near(vr.body_yaw,.4f)&&near(vr.vehicle.turn,0)&&!vr.tutorial_turn_flags);}
+ register_driver(0);vr.vehicle.active=0;
+ for(step=0;step<20;step++){tutorial_turn_input(.9f,.016f);CHECK(near(vr.body_yaw,.4f)&&!vr.tutorial_turn_flags);}
+ tutorial_turn_input(0,.016f);tutorial_turn_input(.9f,.016f);CHECK(vr.body_yaw<.4f&&vr.tutorial_turn_flags);
+ }
+ reset();register_driver(1);engage();vr.input.left_grip=vr.input.right_grip=.9f;
+ for(step=0;step<20;step++)input_update();CHECK(!logs&&vr.recentre_time==0&&driving.wheel_valid);
+ vr.gamepad_back=1;for(step=0;step<12;step++)input_update();CHECK(logs==1&&!driving.wheel_valid&&driving.wheel_blocked);
+ reset();vr.input.left_grip=vr.input.right_grip=.9f;
+ for(step=0;step<12;step++)input_update();CHECK(logs==1); /* Walking grip recenter unchanged. */
+ reset();settings.gamepad_aim=1;register_driver(1);vr.input.left_grip=vr.input.right_grip=.9f;
+ for(step=0;step<12;step++)input_update();CHECK(logs==1); /* Existing physical-pad mode unchanged. */
+}
+int main(void){driver_aim_and_sticks();bounded_steering();independent_accelerator();dedicated_brake();wheel_geometry_and_head_independence();
+ flat_and_physical_gamepad_fallback();loss_and_rearm();seat_identity_and_recentre_alignment();invalid_hull_is_neutral();turns_and_recentre();
+ printf("PASS: %u production driver/wheel/input assertions\n",checks);return 0;}
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ args = parser.parse_args()
+ vr = (ROOT / 'port/linux/src/vr.c').read_text()
+ names = ('quaternion_rotate', 'quaternion_yaw', 'game_axes', 'driving_reset', 'driving_active',
+ 'driving_tracking_valid', 'halo_vr_vehicle_controls', 'halo_vr_vehicle_controls_ready',
+ 'halo_vr_vehicle_brake', 'halo_vr_vehicle_accelerator', 'driving_frame_update', 'halo_vr_vehicle_steering', 'driving_aim', 'driving_turn_blocked',
+ 'vehicle_view_active', 'vehicle_capture', 'halo_vr_vehicle_reference', 'halo_vr_vehicle_orientation', 'vehicle_basis', 'recentre', 'halo_vr_gaze_tracking_valid',
+ 'tutorial_turn_input', 'input_update', 'halo_vr_aim', 'halo_vr_menus', 'analog', 'stick_merge', 'halo_vr_gamepad')
+ production = '\n\n'.join(function(vr, name) for name in names)
+ assert 'driving_frame_update();' in function(vr, 'input_update')
+ assert 'driving_active()' in function(vr, 'halo_vr_gamepad')
+ assert 'driving_reset(1);' in function(vr, 'settings_read')
+ assert 'driving_reset(1);' in function(vr, 'halo_vr_vehicle_reference')
+ compile_and_run(HARNESS.replace('@@PRODUCTION@@', production), args)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_driving_game.py b/tools/test_vr_driving_game.py
new file mode 100644
index 0000000000000000000000000000000000000000..943f5463e3f22a3337f25dd10881927747fc8425
--- /dev/null
+++ b/tools/test_vr_driving_game.py
@@ -0,0 +1,429 @@
+#!/usr/bin/env python3
+"""Execute production driving query, input hooks and ordinary action queue seam.
+
+Compiles the actual C helpers/hooks with deterministic game/platform services.
+Checks ownership/seat/script guards, simulation hull orientation, registration
+before sampling, refresh after unit changes, handled-only lateral consumption,
+ tracking-loss neutral actions and unchanged non-driver/gamepad actions. This
+does not execute retail vehicle physics or validate headset presentation.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+
+from test_vr_projectile_reticle import function, block_end
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def integration(player):
+ dispatch = function(player, 'handle_one_player_input')
+ early_start = dispatch.index('if (local_player_index == 0)')
+ early = dispatch[early_start:block_end(dispatch, dispatch.index('{', early_start))]
+ sample = re.search(r'get_local_player_input_blob\(\s*local_player_index,\s*time_delta_sec,\s*&input\);', dispatch)
+ assert sample and early_start < sample.start()
+ start = dispatch.index('extern int halo_vr_aim(')
+ start = dispatch.rfind('{', 0, start)
+ aim = dispatch[start:block_end(dispatch, start)]
+ refresh_start = dispatch.index('if (local_player_index == 0)', sample.end())
+ refresh = dispatch[refresh_start:dispatch.index(';',refresh_start)+1]
+ assert 'player_control_vr_vehicle_controls(' in refresh
+ assert refresh_start < start
+ assert 'halo_vr_vehicle_controls_ready()' in aim
+ assert 'steering > 0 && isfinite(steering_yaw) && isfinite(steering_pitch)' in aim
+ assert 'if (steering == 2)' in aim
+ tail = dispatch[dispatch.index('player->control_flags = input.unit_control_flags;'):dispatch.rindex('\n\treturn;')]
+ assert tail.count('update_client_queue(&action);') == 1
+ assert dispatch.index('halo_vr_vehicle_steering(') < dispatch.index('player->throttle = input.throttle;')
+ return early, sample.group(), refresh, aim, tail
+
+
+HARNESS = r'''
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "vr.h"
+#include "vr_host.h"
+#include "vr_driving.h"
+#define NONE (-1)
+#define TRUE 1
+#define FALSE 0
+#define TEST_FLAG(v,b) ((v) & (1u<<(b)))
+#define SET_FLAG(v,b,on) ((on)?((v)|=1u<<(b)):((v)&=~(1u<<(b))))
+#define PIN(v,lo,hi) ((v)<(lo)?(lo):(v)>(hi)?(hi):(v))
+#define DEGREES_TO_RADIANS(v) ((v)*0.017453292519943295f)
+#define _pi 3.14159265358979323846f
+typedef int boolean;
+typedef float real;
+typedef struct { float i,j,k; } real_vector3d;
+typedef struct { float i,j; } real_vector2d;
+typedef struct { float yaw,pitch; } real_euler_angles2d;
+enum { _object_type_biped=0, _object_type_vehicle=1, _object_type_other=2 };
+enum { _object_dead_bit=0, _unit_possessed_by_recording_bit=1 };
+enum { _unit_state_idle=0, _unit_state_entering_seat=1, _unit_state_exiting_seat=2 };
+enum { _unit_seat_driver_bit=2, _unit_seat_gunner_bit=3 };
+enum { _unit_control_jump_bit=1, _unit_control_use_equipment_bit=7 };
+enum { _director_perspective_first_person=0, _director_perspective_third_person=1, _director_perspective_scripted=2 };
+struct unit_seat { unsigned flags; };
+struct tag_block { int count; struct unit_seat *address; };
+#define TAG_BLOCK_GET_ELEMENT(block,index,type) ((type*)(block)->address+(index))
+struct unit_definition { struct { struct tag_block seats; } unit; };
+struct unit_datum {
+ int definition_index;
+ struct { int type; long parent_object_index; unsigned damage_flags; } object;
+ struct { long player_index; short parent_seat_index; unsigned flags; struct { int state; } animation; } unit;
+};
+struct player_datum { long unit_index; };
+struct player_control {
+ long unit_index; real_euler_angles2d desired_angles; unsigned control_flags;
+ real_vector2d throttle; float primary_trigger; short zoom_level;
+ long desired_weapon_index,desired_grenade_index;
+};
+struct input_blob { real_vector2d throttle; float primary_trigger; real_euler_angles2d facing_delta; unsigned unit_control_flags; };
+struct player_action { real_euler_angles2d desired_facing; unsigned control_flags;
+ long desired_weapon_index,desired_grenade_index; short desired_zoom_level;
+ real_vector2d throttle; float primary_trigger; };
+static unsigned checks;
+#define CHECK(c) do { checks++; if (!(c)) { fprintf(stderr,"line %d: %s\n",__LINE__,#c); exit(1); } } while(0)
+static int near(float a,float b) { return fabsf(a-b)<0.0001f; }
+static struct unit_datum units[12];
+static struct player_datum players[2];
+static struct player_control controls[2];
+static struct unit_seat seats[3];
+static struct unit_definition definition;
+static real_vector3d hull_forward, hull_up;
+static const char *tag_name;
+static int local_present,cinematic,perspective,inhibit_input,inhibit_facing,menus,input_enabled,camera_enabled;
+static int script_flag, *director_camera_scripted;
+static int orientation_calls,registered_mode,register_calls,reference_calls,sample_calls,queue_calls;
+static int register_events[12],runtime_initialized,reference_enabled;
+static float registered_yaw;
+static int ready,steering_result,steering_calls,aim_result,aim_calls,release_calls;
+static int corrupt_yaw,corrupt_pitch,change_on_sample,disappear_on_sample,change_mode_on_sample;
+static float sample_heading;
+static struct input_blob sample;
+static struct player_action queued;
+static struct { struct vr_host_input input; } vr;
+static struct { int gamepad_aim; } settings;
+static struct halo_vr_driving_state driving;
+static int driving_active(void) { return runtime_initialized && !settings.gamepad_aim && driving.mode!=0; }
+static int driving_tracking_valid(void) { return ready && !menus; }
+@@BRAKE@@
+@@ACCELERATOR@@
+static int local_player_get_player_index(short local) { return local_present && local>=0 && local<2 ? local : NONE; }
+static struct player_datum *player_get(long player) { CHECK(player>=0 && player<2); return &players[player]; }
+static struct unit_datum *unit_try_and_get(long unit) { unsigned index=(unsigned)unit&0xffffu;return unit!=NONE && index<12 ? &units[index] : NULL; }
+static int player_input_enabled(void) { return input_enabled; }
+static int cinematic_in_progress(void) { return cinematic; }
+static int director_get_perspective(short local) { CHECK(local==0); return perspective; }
+static int director_inhibited_input(short local) { (void)local; return inhibit_input; }
+static int director_inhibited_facing(short local) { (void)local; return inhibit_facing; }
+static int ui_widgets_active_for_local_player(short local) { CHECK(local==0); return menus; }
+static struct unit_definition *unit_definition_get(int index) { CHECK(index==9); return &definition; }
+static const char *tag_get_name(int index) { CHECK(index==9); return tag_name; }
+static void object_get_orientation(long unit,real_vector3d *forward,real_vector3d *up) {
+ CHECK(((unsigned)unit&0xffffu)==4 || ((unsigned)unit&0xffffu)==5); orientation_calls++; *forward=hull_forward; *up=hull_up;
+}
+static int player_control_camera_control_is_active(void) { return camera_enabled; }
+void halo_vr_vehicle_controls(int mode,float yaw) {
+ CHECK(register_calls<12);register_events[register_calls++]=mode;registered_mode=runtime_initialized?mode:0;registered_yaw=yaw;
+ if(driving.mode!=registered_mode)driving.accelerator_blocked=1;
+ driving.mode=registered_mode;
+}
+int halo_vr_vehicle_controls_ready(void) { return !runtime_initialized || ready; }
+void halo_vr_turn_control(int allowed) { (void)allowed; }
+static void vr_render_vehicle_reference(long unit) { (void)unit;if(reference_enabled)reference_calls++; }
+static boolean vr_render_long_gun(long weapon) { (void)weapon; return TRUE; }
+int halo_vr_aim(float current,int long_gun,float *yaw,float *pitch) {
+ (void)current;(void)long_gun; aim_calls++; *yaw=1.23f; *pitch=.21f; return aim_result;
+}
+void halo_vr_aim_release(void) { release_calls++; }
+void halo_vr_locomotion(float yaw,float *forward,float *left) { (void)yaw;(void)forward;(void)left; }
+int halo_vr_vehicle_steering(float yaw,float lateral,float seconds,float *result_yaw,float *result_pitch) {
+ if(!runtime_initialized)return 0;
+ CHECK(registered_mode==HALO_VR_DRIVING_WARTHOG); CHECK(near(yaw,registered_yaw)); CHECK(seconds>=0);
+ steering_calls++; *result_yaw=corrupt_yaw?NAN:yaw+(steering_result==2?0:lateral*_pi/3);
+ *result_pitch=corrupt_pitch?INFINITY:0; return steering_result;
+}
+#define match_assert_valid_real(file,line,value) CHECK(isfinite(value))
+static void update_client_queue(struct player_action const *action) { queued=*action; queue_calls++; }
+@@QUERY@@
+@@REGISTER@@
+static float magnitude_squared2d(real_vector2d const *v) { return v->i*v->i+v->j*v->j; }
+static float square_root(float value) { return sqrtf(value); }
+static void normalize_input(struct input_blob *input) { @@NORMALIZE@@ }
+static void get_local_player_input_blob(short local,real seconds,struct input_blob *input) {
+ (void)seconds; sample_calls++; sample_heading=registered_yaw;
+ if (local==0) CHECK(register_calls>=1);
+ *input=sample;
+ normalize_input(input);
+ if (change_on_sample) { units[2]=units[1]; controls[local].unit_index=players[local].unit_index=2; units[2].object.parent_object_index=5; units[5]=units[4]; }
+ if (change_mode_on_sample) tag_name="vehicles\\ghost\\ghost";
+ if (disappear_on_sample) controls[local].unit_index=players[local].unit_index=NONE;
+}
+static void dispatch(short local_player_index,real time_delta_sec) {
+ struct player_control *player=&controls[local_player_index];
+ struct input_blob input;
+ long current_weapon_index=7;
+ int vr_driving_mode=HALO_VR_DRIVING_NONE;
+ float vr_hull_yaw=0;
+ @@EARLY@@
+ @@SAMPLE@@
+ if (director_inhibited_input(local_player_index)) memset(&input,0,sizeof(input));
+ @@REFRESH@@
+ if (player->unit_index!=NONE) {
+ struct unit_datum *unit=unit_try_and_get(player->unit_index);
+ CHECK(unit);
+ if (!director_inhibited_facing(local_player_index)) {
+ player->desired_angles.yaw+=input.facing_delta.yaw;
+ player->desired_angles.pitch+=input.facing_delta.pitch;
+ }
+ @@AIM@@
+ }
+ @@TAIL@@
+}
+static void reset(void) {
+ float ignored;
+ camera_enabled=0;register_calls=0;player_control_vr_vehicle_controls(NONE,&ignored);
+ memset(units,0,sizeof(units));memset(players,0,sizeof(players));memset(controls,0,sizeof(controls));memset(seats,0,sizeof(seats));
+ units[1].object.type=_object_type_biped; units[1].object.parent_object_index=4;
+ units[1].unit.player_index=0; units[1].unit.parent_seat_index=0;
+ units[4].object.type=_object_type_vehicle; units[4].object.parent_object_index=NONE; units[4].definition_index=9;
+ players[0].unit_index=controls[0].unit_index=1;
+ players[1].unit_index=controls[1].unit_index=3; units[3]=units[1];units[3].unit.player_index=1;
+ seats[0].flags=1u<<_unit_seat_driver_bit; seats[1].flags=1u<<_unit_seat_gunner_bit;
+ definition.unit.seats.count=3;definition.unit.seats.address=seats;
+ hull_forward=(real_vector3d){1,0,0};hull_up=(real_vector3d){0,0,1};
+ tag_name="vehicles\\warthog\\warthog";local_present=input_enabled=camera_enabled=1;
+ cinematic=inhibit_input=inhibit_facing=menus=script_flag=0;director_camera_scripted=NULL;perspective=_director_perspective_third_person;
+ orientation_calls=register_calls=reference_calls=sample_calls=queue_calls=steering_calls=aim_calls=release_calls=0;
+ registered_mode=HALO_VR_DRIVING_NONE;registered_yaw=sample_heading=0;ready=1;steering_result=1;aim_result=2;
+ runtime_initialized=reference_enabled=1;memset(register_events,0,sizeof(register_events));
+ memset(&vr,0,sizeof(vr));memset(&settings,0,sizeof(settings));memset(&driving,0,sizeof(driving));
+ vr.input.active[1]=1;
+ corrupt_yaw=corrupt_pitch=change_on_sample=disappear_on_sample=change_mode_on_sample=0;
+ sample=(struct input_blob){{.75f,.5f},.7f,{.1f,-.1f},0x123u};
+ controls[0].desired_angles=(real_euler_angles2d){.8f,.3f};controls[1].desired_angles=controls[0].desired_angles;
+ controls[0].desired_weapon_index=controls[1].desired_weapon_index=7;
+ controls[0].desired_grenade_index=controls[1].desired_grenade_index=2;controls[0].zoom_level=controls[1].zoom_level=NONE;
+}
+static void query_cases(void) {
+ static const char *names[]={"vehicles\\warthog\\warthog","vehicles\\ghost\\ghost","vehicles\\scorpion\\scorpion","vehicles\\banshee\\banshee"};
+ float yaw;
+ int mode,step,gate;
+ for(mode=1;mode<=4;mode++) for(step=-150;step<=150;step++) {
+ float angle=step*.017f;
+ reset();tag_name=names[mode-1];hull_forward=(real_vector3d){cosf(angle),sinf(angle),.2f};
+ CHECK(vr_render_vehicle_driving(1,&yaw)==mode);CHECK(near(sinf(yaw),sinf(angle)));CHECK(near(cosf(yaw),cosf(angle)));CHECK(orientation_calls==1);
+ seats[0].flags|=1u<<_unit_seat_gunner_bit;CHECK(vr_render_vehicle_driving(1,&yaw)==mode);
+ }
+ for(gate=0;gate<27;gate++) {
+ reset();yaw=7;
+ switch(gate) {
+ case 0:local_present=0;break;case 1:input_enabled=0;break;case 2:cinematic=1;break;
+ case 3:script_flag=1;director_camera_scripted=&script_flag;break;case 4:perspective=_director_perspective_scripted;break;
+ case 5:inhibit_input=1;break;case 6:inhibit_facing=1;break;case 7:menus=1;break;
+ case 8:players[0].unit_index=2;break;case 9:units[1].unit.player_index=1;break;
+ case 10:units[1].object.type=_object_type_vehicle;break;case 11:units[1].object.damage_flags=1u<<_object_dead_bit;break;
+ case 12:units[1].unit.flags=1u<<_unit_possessed_by_recording_bit;break;
+ case 13:units[1].unit.animation.state=_unit_state_entering_seat;break;case 14:units[1].unit.animation.state=_unit_state_exiting_seat;break;
+ case 15:units[1].object.parent_object_index=NONE;break;case 16:units[1].object.parent_object_index=50;break;
+ case 17:units[4].object.type=_object_type_biped;break;case 18:units[4].object.damage_flags=1u<<_object_dead_bit;break;
+ case 19:units[4].unit.flags=1u<<_unit_possessed_by_recording_bit;break;
+ case 20:units[1].unit.parent_seat_index=NONE;break;case 21:units[1].unit.parent_seat_index=3;break;
+ case 22:units[1].unit.parent_seat_index=1;break;case 23:tag_name="vehicles\\pelican\\pelican";break;
+ case 24:tag_name=NULL;break;case 25:tag_name="vehicles\\warthog\\custom_warthog";break;
+ case 26:units[4].object.parent_object_index=5;break;
+ }
+ CHECK(vr_render_vehicle_driving(1,&yaw)==HALO_VR_DRIVING_NONE);CHECK(yaw==0);CHECK(orientation_calls==0);
+ }
+ reset();CHECK(vr_render_vehicle_driving(1,NULL)==0);CHECK(vr_render_vehicle_driving(NONE,&yaw)==0);
+ players[0].unit_index=99;CHECK(vr_render_vehicle_driving(99,&yaw)==0);
+ reset();director_camera_scripted=&script_flag;CHECK(vr_render_vehicle_driving(1,&yaw)==1);
+ reset();tag_name="vehicles\\ghost\\ghost_mp";CHECK(vr_render_vehicle_driving(1,&yaw)==HALO_VR_DRIVING_GHOST);
+ reset();tag_name="vehicles\\scorpion\\scorpion_mp";CHECK(vr_render_vehicle_driving(1,&yaw)==HALO_VR_DRIVING_SCORPION);
+ reset();tag_name="vehicles\\banshee\\banshee_cinematic";CHECK(vr_render_vehicle_driving(1,&yaw)==0);
+ reset();tag_name="vehicles\\banshee\\banshee_oldplayback";CHECK(vr_render_vehicle_driving(1,&yaw)==0);
+ reset();tag_name="vehicles\\scorpion\\scorpion_cinematic";CHECK(vr_render_vehicle_driving(1,&yaw)==0);
+ reset();hull_forward=(real_vector3d){NAN,1,0};CHECK(vr_render_vehicle_driving(1,&yaw)==0 && yaw==0);
+ reset();hull_forward=(real_vector3d){1,INFINITY,0};CHECK(vr_render_vehicle_driving(1,&yaw)==0 && yaw==0);
+ reset();hull_forward=(real_vector3d){1,0,NAN};CHECK(vr_render_vehicle_driving(1,&yaw)==0 && yaw==0);
+ reset();hull_forward=(real_vector3d){0,0,1};CHECK(vr_render_vehicle_driving(1,&yaw)==0 && yaw==0);
+ for(step=-100;step<=100;step++) {
+ int pole;
+ float angle=step*.031f;
+ for(pole=-1;pole<=1;pole+=2) {
+ reset();tag_name="vehicles\\banshee\\banshee";
+ hull_forward=(real_vector3d){0,0,(float)pole};hull_up=(real_vector3d){-pole*cosf(angle),-pole*sinf(angle),0};
+ CHECK(vr_render_vehicle_driving(1,&yaw)==HALO_VR_DRIVING_BANSHEE);
+ CHECK(near(sinf(yaw),sinf(angle)) && near(cosf(yaw),cosf(angle)));
+ hull_forward.i=cosf(angle)*.001f;hull_forward.j=sinf(angle)*.001f;
+ CHECK(vr_render_vehicle_driving(1,&yaw)==HALO_VR_DRIVING_BANSHEE);
+ CHECK(near(sinf(yaw),sinf(angle)) && near(cosf(yaw),cosf(angle)));
+ }
+ }
+ reset();hull_forward=(real_vector3d){0,0,1};hull_up=(real_vector3d){NAN,0,0};CHECK(vr_render_vehicle_driving(1,&yaw)==0);
+}
+static void action_cases(void) {
+ static const char *names[]={"vehicles\\warthog\\warthog","vehicles\\ghost\\ghost","vehicles\\scorpion\\scorpion","vehicles\\banshee\\banshee"};
+ int mode,step;
+ for(mode=1;mode<=4;mode++) for(step=-60;step<=60;step++) {
+ float magnitude,forward,lateral;
+ reset();tag_name=names[mode-1];sample.throttle.j=step/60.f;
+ magnitude=fmaxf(1,sqrtf(sample.throttle.i*sample.throttle.i+sample.throttle.j*sample.throttle.j));
+ forward=sample.throttle.i/magnitude;lateral=sample.throttle.j/magnitude;
+ hull_forward=(real_vector3d){cosf(-.4f),sinf(-.4f),0};
+ dispatch(0,1.f/72);
+ CHECK(register_calls==2 && sample_calls==1 && queue_calls==1 && registered_mode==mode);
+ CHECK(near(sample_heading,-.4f));CHECK(queued.control_flags==(mode==1?0x121u:0x123u));CHECK(near(queued.primary_trigger,.7f));
+ CHECK(queued.desired_weapon_index==7 && queued.desired_grenade_index==2 && queued.desired_zoom_level==NONE);
+ CHECK(near(queued.throttle.i,forward));
+ if(mode==1) { CHECK(steering_calls==1);CHECK(queued.throttle.j==0);CHECK(near(sinf(queued.desired_facing.yaw),sinf(-.4f+lateral*_pi/3)));CHECK(queued.desired_facing.pitch==0); }
+ else { CHECK(steering_calls==0);CHECK(near(queued.throttle.j,lateral));CHECK(near(queued.desired_facing.yaw,.9f));CHECK(near(queued.desired_facing.pitch,.2f)); }
+ reset();tag_name=names[mode-1];ready=0;steering_result=2;aim_result=0;dispatch(0,1.f/72);
+ CHECK(queued.throttle.i==0 && queued.throttle.j==0);CHECK(queued.desired_facing.yaw==0 && queued.desired_facing.pitch==0);
+ }
+ reset();steering_result=0;dispatch(0,.01f);CHECK(near(queued.throttle.j,.5f) && near(queued.desired_facing.yaw,.9f));
+ reset();corrupt_yaw=1;dispatch(0,.01f);CHECK(near(queued.throttle.j,.5f) && near(queued.desired_facing.yaw,.9f));
+ reset();corrupt_pitch=1;dispatch(0,.01f);CHECK(near(queued.throttle.j,.5f) && near(queued.desired_facing.pitch,.2f));
+ reset();steering_result=2;dispatch(0,.01f);CHECK(queued.throttle.i==0 && queued.throttle.j==0 && queued.desired_facing.yaw==0);
+ reset();change_on_sample=1;dispatch(0,.01f);CHECK(register_calls==3 && registered_mode==1);CHECK(controls[0].unit_index==2 && orientation_calls==2);CHECK(register_events[0]==1 && register_events[1]==0 && register_events[2]==1);
+ reset();change_mode_on_sample=1;dispatch(0,.01f);CHECK(register_calls==2 && registered_mode==2 && steering_calls==0);CHECK(near(queued.throttle.j,.5f));
+ reset();disappear_on_sample=1;dispatch(0,.01f);CHECK(register_calls==2 && queue_calls==1 && registered_mode==0);
+ /* Invalid or non-driver mode must be cleared before sampling. */
+ reset();camera_enabled=0;registered_mode=3;dispatch(0,.01f);CHECK(registered_mode==0 && steering_calls==0 && aim_calls==0);
+ reset();units[1].unit.parent_seat_index=1;registered_mode=3;dispatch(0,.01f);CHECK(registered_mode==0 && steering_calls==0);
+ reset();tag_name="vehicles\\pelican\\pelican";registered_mode=3;dispatch(0,.01f);CHECK(registered_mode==0 && steering_calls==0);
+ reset();units[1].unit.animation.state=_unit_state_entering_seat;registered_mode=3;dispatch(0,.01f);CHECK(registered_mode==0 && steering_calls==0);
+ reset();menus=1;registered_mode=3;aim_result=1;dispatch(0,.01f);CHECK(registered_mode==0 && steering_calls==0 && aim_calls==0);CHECK(near(queued.desired_facing.yaw,.9f));
+ reset();script_flag=1;director_camera_scripted=&script_flag;registered_mode=3;aim_result=1;dispatch(0,.01f);CHECK(registered_mode==0 && steering_calls==0 && aim_calls==0);
+ reset();inhibit_input=1;aim_result=1;dispatch(0,.01f);CHECK(aim_calls==0 && queued.throttle.i==0 && queued.throttle.j==0);
+ reset();input_enabled=0;aim_result=1;dispatch(0,.01f);CHECK(registered_mode==0 && aim_calls==0);
+ reset();input_enabled=0;units[1].object.parent_object_index=NONE;aim_result=1;dispatch(0,.01f);CHECK(registered_mode==0 && aim_calls==1);CHECK(near(queued.desired_facing.yaw,1.23f));
+ reset();runtime_initialized=0;ready=0;steering_result=0;aim_result=0;dispatch(0,.01f);CHECK(registered_mode==0 && near(queued.throttle.i,.75f) && near(queued.throttle.j,.5f));CHECK(near(queued.desired_facing.yaw,.9f));
+ reset();reference_enabled=0;dispatch(0,.01f);CHECK(reference_calls==0);
+ for(step=0;step<3;step++) {
+ register_calls=0;
+ if(step==0) controls[0].unit_index=players[0].unit_index=0x10001L;
+ if(step==1) units[1].object.parent_object_index=0x10004L;
+ if(step==2) { units[1].unit.parent_seat_index=2;seats[2].flags=1u<<_unit_seat_driver_bit; }
+ dispatch(0,.01f);
+ CHECK(register_calls==3 && register_events[0]==0 && register_events[1]==1 && register_events[2]==1);
+ CHECK(registered_mode==1 && reference_calls==0 && near(queued.throttle.i,.75f));
+ register_calls=0;dispatch(0,.01f);CHECK(register_calls==2 && register_events[0]==1 && register_events[1]==1);
+ }
+ reset();
+ for(step=0;step<4;step++) {
+ register_calls=0;units[1].unit.animation.state=(step==0?_unit_state_entering_seat:step==2?_unit_state_exiting_seat:_unit_state_idle);
+ dispatch(0,.01f);CHECK(registered_mode==(step%2?1:0));CHECK(register_calls==2);
+ }
+ reset();dispatch(1,.01f);CHECK(register_calls==0 && steering_calls==0 && reference_calls==0);CHECK(near(queued.throttle.j,.5f));CHECK(near(queued.desired_facing.yaw,.9f));
+ /* Stock on-foot physical aim remains the original finite guarded path. */
+ reset();units[1].object.parent_object_index=NONE;aim_result=1;dispatch(0,.01f);CHECK(registered_mode==0 && steering_calls==0);CHECK(near(queued.desired_facing.yaw,1.23f));CHECK(near(queued.desired_facing.pitch,.21f));
+}
+static void accelerator_queue_cases(void) {
+ int direction,step;
+ for(direction=-1;direction<=1;direction+=2)for(step=0;step<2;step++) {
+ float lateral;
+ reset();sample.throttle=(real_vector2d){0,0};dispatch(0,.016f);
+ CHECK(!driving.accelerator_blocked);
+ register_calls=0;vr.input.buttons=VR_BUTTON_A;
+ sample.throttle=(real_vector2d){(float)step,(float)direction};
+ lateral=direction/(step?sqrtf(2):1);
+ dispatch(0,.016f);
+ CHECK(queued.throttle.i==1 && queued.throttle.j==0);
+ CHECK(near(sinf(queued.desired_facing.yaw),sinf(lateral*_pi/3)));
+ CHECK(!TEST_FLAG(queued.control_flags,_unit_control_jump_bit));
+ /* Repeat the same held state over simulation samples: no edge/toggle
+ behavior, and the action queue retains full independent acceleration. */
+ register_calls=0;dispatch(0,.016f);CHECK(queued.throttle.i==1);
+ register_calls=0;vr.input.buttons=0;sample.throttle.i=0;
+ dispatch(0,.016f);CHECK(queued.throttle.i==0);
+ }
+ reset();sample.throttle=(real_vector2d){0,0};dispatch(0,.016f);
+ register_calls=0;vr.input.buttons=VR_BUTTON_A;sample.throttle.i=-.75f;sample.throttle.j=0;
+ dispatch(0,.016f);CHECK(queued.throttle.i==-.75f&&!TEST_FLAG(queued.control_flags,_unit_control_jump_bit));
+ register_calls=0;ready=0;steering_result=2;dispatch(0,.016f);CHECK(queued.throttle.i==0);
+ /* An A boarding/menu press cannot auto-accelerate when the final owned
+ seat becomes controllable; normal on-foot and other-vehicle A flags remain. */
+ reset();vr.input.buttons=VR_BUTTON_A;sample.throttle.i=sample.throttle.j=0;
+ dispatch(0,.016f);CHECK(queued.throttle.i==0&&driving.accelerator_blocked);
+ reset();vr.input.buttons=VR_BUTTON_A;units[1].object.parent_object_index=NONE;dispatch(0,.016f);
+ CHECK(TEST_FLAG(queued.control_flags,_unit_control_jump_bit)&&queued.throttle.i==.75f);
+ reset();vr.input.buttons=VR_BUTTON_A;tag_name="vehicles\\ghost\\ghost";dispatch(0,.016f);
+ CHECK(TEST_FLAG(queued.control_flags,_unit_control_jump_bit)&&queued.throttle.i==.75f);
+}
+static void brake_queue_cases(void) {
+ int direction,pressed,mode;
+ static const char *names[]={"vehicles\\warthog\\warthog","vehicles\\ghost\\ghost","vehicles\\scorpion\\scorpion","vehicles\\banshee\\banshee"};
+ for(direction=-1;direction<=1;direction+=2)for(pressed=0;pressed<2;pressed++) {
+ reset();sample.throttle=(real_vector2d){0,0};dispatch(0,.016f);
+ register_calls=0;vr.input.buttons=VR_BUTTON_B|(pressed?VR_BUTTON_A:0);
+ sample.throttle=(real_vector2d){1,(float)direction};sample.unit_control_flags|=1u<<_unit_control_use_equipment_bit;
+ dispatch(0,.016f);
+ CHECK(queued.throttle.i==0&&queued.throttle.j==0);
+ CHECK(TEST_FLAG(queued.control_flags,_unit_control_jump_bit));
+ CHECK(!TEST_FLAG(queued.control_flags,_unit_control_use_equipment_bit));
+ CHECK(near(sinf(queued.desired_facing.yaw),sinf(direction/sqrtf(2)*_pi/3)));
+ CHECK(!driving.accelerator_blocked);
+ register_calls=0;dispatch(0,.016f);CHECK(queued.throttle.i==0&&TEST_FLAG(queued.control_flags,_unit_control_jump_bit));
+ register_calls=0;vr.input.buttons=pressed?VR_BUTTON_A:0;sample.throttle=(real_vector2d){0,(float)direction};
+ dispatch(0,.016f);
+ CHECK(queued.throttle.i==(pressed?1:0)&&!TEST_FLAG(queued.control_flags,_unit_control_jump_bit));
+ }
+ /* B maps only for a handled native VR Warthog driver. Stock gamepad,
+ passenger, other vehicles and focus loss never get synthesized brakes. */
+ for(mode=1;mode<=3;mode++) {
+ reset();tag_name=names[mode];vr.input.buttons=VR_BUTTON_B;
+ sample.unit_control_flags|=1u<<_unit_control_use_equipment_bit;sample.unit_control_flags&=~(1u<<_unit_control_jump_bit);
+ dispatch(0,.016f);
+ CHECK(!TEST_FLAG(queued.control_flags,_unit_control_jump_bit)&&TEST_FLAG(queued.control_flags,_unit_control_use_equipment_bit));
+ CHECK(queued.throttle.i==.75f);
+ }
+ reset();units[1].unit.parent_seat_index=1;vr.input.buttons=VR_BUTTON_B;
+ sample.unit_control_flags|=1u<<_unit_control_use_equipment_bit;sample.unit_control_flags&=~(1u<<_unit_control_jump_bit);
+ dispatch(0,.016f);CHECK(!TEST_FLAG(queued.control_flags,_unit_control_jump_bit)&&TEST_FLAG(queued.control_flags,_unit_control_use_equipment_bit));
+ reset();settings.gamepad_aim=1;steering_result=0;vr.input.buttons=VR_BUTTON_B;
+ sample.unit_control_flags|=1u<<_unit_control_use_equipment_bit;dispatch(0,.016f);
+ CHECK(TEST_FLAG(queued.control_flags,_unit_control_use_equipment_bit)&&queued.throttle.i==.75f);
+ reset();ready=0;steering_result=2;vr.input.buttons=VR_BUTTON_A|VR_BUTTON_B;
+ sample.unit_control_flags|=1u<<_unit_control_use_equipment_bit;dispatch(0,.016f);
+ CHECK(queued.throttle.i==0&&!TEST_FLAG(queued.control_flags,_unit_control_jump_bit)&&!TEST_FLAG(queued.control_flags,_unit_control_use_equipment_bit));
+}
+int main(void) { query_cases();action_cases();accelerator_queue_cases();brake_queue_cases();printf("PASS: %u production driving game assertions\n",checks);return 0; }
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc', default=os.environ.get('CC') or shutil.which('clang') or shutil.which('cc'))
+ args = parser.parse_args()
+ assert args.cc, 'Select a C11 host compiler with --cc'
+ player = (ROOT/'source/game/player_control.c').read_text()
+ platform = (ROOT/'port/linux/src/vr.c').read_text()
+ renderer = (ROOT/'port/linux/game/vr_render.c').read_text()
+ query = function(renderer, 'vr_render_vehicle_driving')
+ assert 'object_get_node_matrix' not in re.sub(r'/\*.*?\*/', '', query, flags=re.S)
+ early, sample, refresh, aim, tail = integration(player)
+ harness = HARNESS.replace('@@QUERY@@',function(renderer,'vr_render_vehicle_yaw')+'\n'+query).replace('@@REGISTER@@',function(player,'player_control_vr_vehicle_controls'))
+ input_function = function(player, 'get_local_player_input_blob')
+ normalize_start = input_function.index('if (magnitude_squared2d(&input->throttle) > 1.f)')
+ normalization = input_function[normalize_start:block_end(input_function,input_function.index('{',normalize_start))]
+ harness = harness.replace('@@NORMALIZE@@',normalization).replace('@@BRAKE@@',function(platform,'halo_vr_vehicle_brake')).replace('@@ACCELERATOR@@',function(platform,'halo_vr_vehicle_accelerator'))
+ for key, code in (('EARLY',early),('SAMPLE',sample),('REFRESH',refresh),('AIM',aim),('TAIL',tail)):
+ harness = harness.replace('@@'+key+'@@',code)
+ with tempfile.TemporaryDirectory(prefix='halo-vr-driving-game-',ignore_cleanup_errors=True) as directory:
+ temp=Path(directory);source=temp/'test.c';executable=temp/('test.exe' if os.name=='nt' else 'test')
+ source.write_text(harness)
+ command=[args.cc,'-std=c11','-Wall','-Wextra','-Werror','-I'+str(ROOT/'port/linux/src'),str(source)]
+ if os.name!='nt': command.append('-lm')
+ subprocess.run([*command,'-o',str(executable)],cwd=temp,check=True)
+ subprocess.run([str(executable)],cwd=temp,check=True)
+
+
+if __name__=='__main__': main()
diff --git a/tools/test_vr_flashlight.py b/tools/test_vr_flashlight.py
new file mode 100644
index 0000000000000000000000000000000000000000..0b0f6b83d8e2a90962c81eac5ae46a8110b61c1f
--- /dev/null
+++ b/tools/test_vr_flashlight.py
@@ -0,0 +1,282 @@
+#!/usr/bin/env python3
+"""Host checks of production unarmed VR flashlight pose and rendering bounds.
+
+Reuses the real projectile/reticle regression harness, the actual VR view
+layout and extracted production light functions. Deterministic engine services
+stand in for maps/GPU/headset; this does not validate physical beam placement
+or dynamic object-light cluster selection on hardware.
+"""
+from pathlib import Path
+import re
+import subprocess
+
+import test_vr_projectile_reticle as reticle
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+LIGHT_FIXTURE = r'''
+#define MAXIMUM_RENDERED_LIGHTS 2
+#define MAXIMUM_CLUSTERS_PER_LIGHT 8
+static const real _pi=3.14159265358979323846f;
+enum { _point_light_dynamic_bit=0, _point_light_attached_to_first_person_weapon_bit=3 };
+enum { _light_definition_no_specular_bit=1, _light_definition_supersize_in_first_person_bit=3,
+ _light_definition_is_first_person_flashlight_bit=4 };
+struct light_datum {
+ unsigned flags; long definition_index,object_index,parent_light_index,rasterizer_light_index; short function_index;
+ real_point3d position; real_vector3d forward,up; real radius;
+};
+struct point_light_definition {
+ unsigned flags; real radius,radius_modifier_upper_bound,cutoff_angle,lens_flare_radius;
+ real runtime_cosine_cutoff_angle,runtime_sine_cutoff_angle,specular_radius_multiplier;
+};
+static struct light_datum fixture_lights[4];
+static struct point_light_definition fixture_light_definition;
+static struct { short scene_point_light_count; long scene_point_lights[MAXIMUM_RENDERED_LIGHTS]; } lights_globals;
+static int light_data=1,marked[4],cluster_reads,diffuse_calls,specular_calls,diffuse_skip,specular_skip,data_next_calls;
+static real_point3d diffuse_center,specular_center;
+static real diffuse_radius,specular_radius;
+static real fixture_light_intensity;
+static boolean object_get_function_value(long object_index,short function_index,real *value) {
+ CHECK(object_index==0 && function_index==0); *value=fixture_light_intensity; return TRUE;
+}
+static struct light_datum *light_get(long index) { CHECK(index>=0 && index<4); return &fixture_lights[index]; }
+static struct point_light_definition *light_definition_get(long index) { CHECK(index==0); return &fixture_light_definition; }
+static long data_next_index(int data,long index) { CHECK(data==light_data); data_next_calls++; return index<3 ? index+1 : NONE; }
+static boolean light_unmarked(long index) { CHECK(index>=0 && index<4); return !marked[index]; }
+static boolean light_mark(long index) { CHECK(index>=0 && index<4); marked[index]=1; return TRUE; }
+static boolean should_render_lights(void) { return TRUE; }
+static short light_build_cluster_array(long index,short maximum,short *clusters) {
+ CHECK(index>=0 && index<4 && maximum==MAXIMUM_CLUSTERS_PER_LIGHT); cluster_reads++; clusters[0]=17; return 1;
+}
+static void rasterizer_environment_diffuse_lights_begin(void) { }
+static void rasterizer_environment_diffuse_lights_end(void) { }
+static void rasterizer_environment_specular_lights_begin(void) { }
+static void rasterizer_environment_specular_lights_end(void) { }
+static void structure_render_diffuse_light(long index,real_point3d const *center,real radius,short count,short const *clusters) {
+ CHECK(index==3); diffuse_calls++; diffuse_center=*center; diffuse_radius=radius;
+ diffuse_skip=clusters==NULL; CHECK(count==(diffuse_skip ? 0 : 1));
+}
+static void structure_render_specular_light(long index,real_point3d const *center,real radius,short count,short const *clusters) {
+ CHECK(index==3); specular_calls++; specular_center=*center; specular_radius=radius;
+ specular_skip=clusters==NULL; CHECK(count==(specular_skip ? 0 : 1));
+}
+@@FLASHLIGHT_FUNCTIONS@@
+static void flashlight_reset(void) {
+ int index;
+ reset(); inventory[0]=NONE; fixture_weapon_count[0]=0;
+ unit_definitions[0].unit.flags=1u<<_unit_fires_from_camera_bit;
+ vr_render.weapon_camera_set=TRUE; vr_render.weapon_camera_valid=TRUE;
+ vr_render.weapon_camera.position=(real_point3d){3,4,0.4f};
+ vr_render.weapon_camera.up=(real_vector3d){0,0,1};
+ render.local_player_index=0;
+ memset(fixture_lights,0,sizeof(fixture_lights)); memset(marked,0,sizeof(marked));
+ memset(&lights_globals,0,sizeof(lights_globals));
+ memset(&fixture_light_definition,0,sizeof(fixture_light_definition));
+ fixture_light_definition.flags=1u<<_light_definition_is_first_person_flashlight_bit;
+ fixture_light_definition.radius=5; fixture_light_definition.radius_modifier_upper_bound=2;
+ fixture_light_definition.cutoff_angle=0.3f;
+ fixture_light_definition.runtime_cosine_cutoff_angle=cosf(0.3f);
+ fixture_light_definition.runtime_sine_cutoff_angle=sinf(0.3f);
+ fixture_light_definition.specular_radius_multiplier=1.2f;
+ for(index=0;index<4;index++) {
+ fixture_lights[index].flags=1u<<_point_light_dynamic_bit;
+ fixture_lights[index].object_index=index ? 1 : 0;
+ fixture_lights[index].parent_light_index=NONE;
+ fixture_lights[index].rasterizer_light_index=3;
+ fixture_lights[index].radius=5;
+ fixture_lights[index].position=(real_point3d){7,8,9};
+ fixture_lights[index].forward=(real_vector3d){1,0,0};
+ fixture_lights[index].up=(real_vector3d){0,0,1};
+ }
+ cluster_reads=diffuse_calls=specular_calls=diffuse_skip=specular_skip=0;
+ data_next_calls=0;
+ fixture_light_intensity=1;
+}
+static void check_flashlight_render_refresh(void) {
+ int frame;
+ real_point3d position,expected_position,ignored_origin;
+ real_vector3d forward,up,expected_forward;
+ struct light_datum tick;
+ flashlight_reset(); tick=fixture_lights[0];
+ /* Ninety rendered poses with a completely unchanged simulation unit/light. */
+ for(frame=0;frame<90;frame++) {
+ real angle=(real)frame*0.025f, pitch=sinf(angle)*0.7f;
+ vr_render.base_yaw=0.5f;
+ vr_render.view.hand_forward[0]=cosf(angle)*cosf(pitch);
+ vr_render.view.hand_forward[1]=sinf(angle)*cosf(pitch);
+ vr_render.view.hand_forward[2]=sinf(pitch);
+ vr_render.weapon_camera.position.z=(real)frame*0.003f;
+ expected_position=vr_render.weapon_camera.position;
+ CHECK(vr_render_unarmed_ray(0,0,&units[0],&vr_render.view,vr_render.base_yaw,&ignored_origin,&expected_forward));
+ CHECK(vr_render_player_flashlight_pose(0,&position,&forward,&up));
+ point_equal(position,expected_position); direction_equal(forward,expected_forward);
+ CHECK(near(dot_product3d(&forward,&up),0)); CHECK(near(magnitude3d(&up),1));
+ CHECK(memcmp(&tick,&fixture_lights[0],sizeof(tick))==0);
+ direction_equal(units[0].unit.aiming_vector,(real_vector3d){1,0,0});
+ /* The second eye queries identical centre-frame presentation, not a new locate. */
+ CHECK(vr_render_player_flashlight_pose(0,&position,&forward,&up));
+ point_equal(position,expected_position); direction_equal(forward,expected_forward);
+ }
+ /* Hand/controller height must not be replaced by the observer/HMD origin. */
+ fixture_observer.position.z=12; vr_render.weapon_camera.position.z=-0.2f;
+ CHECK(vr_render_player_flashlight_pose(0,&position,&forward,&up)); CHECK(near(position.z,-0.2f));
+ vr_render.view.hand_forward[0]=0; vr_render.view.hand_forward[1]=0; vr_render.view.hand_forward[2]=1;
+ CHECK(vr_render_player_flashlight_pose(0,&position,&forward,&up));
+ CHECK(near(forward.k,sinf(DEGREES_TO_RADIANS(85.f)))); CHECK(near(dot_product3d(&forward,&up),0));
+ vr_render.view.hand_forward[2]=-1;
+ CHECK(vr_render_player_flashlight_pose(0,&position,&forward,&up)); CHECK(near(forward.k,-sinf(DEGREES_TO_RADIANS(85.f))));
+ vr_render.view.hand_forward[0]=1; vr_render.view.hand_forward[2]=0; vr_render.base_yaw=0;
+ vr_render.weapon_camera.up=(real_vector3d){0,1,0};
+ CHECK(vr_render_player_flashlight_pose(0,&position,&forward,&up)); direction_equal(up,(real_vector3d){0,1,0});
+}
+static void check_flashlight_rejection(void) {
+ int scenario;
+ for(scenario=0;scenario<20;scenario++) {
+ real_point3d position={11,12,13}; real_vector3d forward={14,15,16},up={17,18,19};
+ flashlight_reset();
+ switch(scenario) {
+ case 0: vr_render.active=FALSE; break;
+ case 1: vr_render.weapon_camera_set=FALSE; break;
+ case 2: vr_render.weapon_camera_valid=FALSE; break;
+ case 3: fixture_vr_aiming=FALSE; break;
+ case 4: fixture_tracking_valid=FALSE; break;
+ case 5: render.local_player_index=1; break;
+ case 6: fixture_weapon_count[0]=1; break;
+ case 7: inventory[0]=0; break;
+ case 8: units[0].object.damage_flags=1u<<_object_dead_bit; break;
+ case 9: units[0].object.parent_object_index=1; break;
+ case 10: units[0].unit.gunner_object_index=1; break;
+ case 11: fixture_cinematic=TRUE; break;
+ case 12: fixture_inhibited=TRUE; break;
+ case 13: fixture_perspective=0; break;
+ case 14: vr_render.weapon_camera.position.z=NAN; break;
+ case 15: vr_render.weapon_camera.up.i=NAN; break;
+ case 16: vr_render.weapon_camera.up=(real_vector3d){0,1,0}; break; /* parallel to default pointing */
+ case 17: vr_render.view.hand_forward[1]=NAN; break;
+ case 18: fixture_local_player=NONE; break;
+ case 19: fixture_scripted_camera=TRUE; break;
+ }
+ CHECK(!vr_render_player_flashlight_pose(0,&position,&forward,&up));
+ point_equal(position,(real_point3d){11,12,13}); direction_equal(forward,(real_vector3d){14,15,16});
+ direction_equal(up,(real_vector3d){17,18,19});
+ lights_vr_include_unarmed_flashlights(); CHECK(data_next_calls==0);
+ }
+ flashlight_reset(); {
+ real_point3d position; real_vector3d forward,up;
+ CHECK(!vr_render_player_flashlight_pose(1,&position,&forward,&up));
+ CHECK(!vr_render_player_flashlight_pose(NONE,&position,&forward,&up));
+ CHECK(!vr_render_player_flashlight_pose(0,NULL,&forward,&up));
+ CHECK(!vr_render_player_flashlight_pose(0,&position,NULL,&up));
+ CHECK(!vr_render_player_flashlight_pose(0,&position,&forward,NULL));
+ }
+}
+static void check_flashlight_bounds_and_clusters(void) {
+ int scenario;
+ flashlight_reset(); {
+ struct light_datum tick=fixture_lights[0];
+ real_point3d center,tick_center,position; real_vector3d forward,up; real radius,tick_radius;
+ /* Tick cone points right; current rendered controller points left. */
+ vr_render.view.hand_forward[0]=-1; vr_render.view.hand_forward[1]=0;
+ light_get_bounding_sphere(0,&center,&radius);
+ light_compute_bounding_sphere(0,TRUE,FALSE,TRUE,&tick_center,&tick_radius);
+ CHECK(center.x<vr_render.weapon_camera.position.x); CHECK(tick_center.x>tick.position.x);
+ CHECK(near(radius,tick_radius)); CHECK(memcmp(&tick,&fixture_lights[0],sizeof(tick))==0);
+ CHECK(light_vr_unarmed_flashlight_pose(&fixture_lights[0],&position,&forward,&up));
+ CHECK(near(center.x,position.x+radius*forward.i));
+ /* Missing from stale clusters: included once without partition/datum edits. */
+ lights_vr_include_unarmed_flashlights(); CHECK(lights_globals.scene_point_light_count==1);
+ CHECK(data_next_calls>0);
+ CHECK(lights_globals.scene_point_lights[0]==0);
+ lights_vr_include_unarmed_flashlights(); CHECK(lights_globals.scene_point_light_count==1);
+ lights_render_diffuse(); lights_render_specular();
+ CHECK(diffuse_calls==1 && specular_calls==1 && diffuse_skip && specular_skip && cluster_reads==0);
+ CHECK(near(diffuse_center.x,position.x+diffuse_radius*forward.i));
+ CHECK(near(specular_center.x,position.x+specular_radius*forward.i));
+ CHECK(memcmp(&tick,&fixture_lights[0],sizeof(tick))==0);
+ }
+ /* Full visibility budget stays bounded and keeps the player's flashlight. */
+ flashlight_reset(); lights_globals.scene_point_light_count=MAXIMUM_RENDERED_LIGHTS;
+ lights_globals.scene_point_lights[0]=2; lights_globals.scene_point_lights[1]=3;
+ marked[2]=marked[3]=1; lights_vr_include_unarmed_flashlights();
+ CHECK(lights_globals.scene_point_light_count==MAXIMUM_RENDERED_LIGHTS);
+ CHECK(lights_globals.scene_point_lights[0]==2 && lights_globals.scene_point_lights[1]==0);
+ flashlight_reset(); fixture_light_intensity=0;
+ lights_globals.scene_point_light_count=MAXIMUM_RENDERED_LIGHTS;
+ lights_globals.scene_point_lights[0]=2; lights_globals.scene_point_lights[1]=3;
+ marked[2]=marked[3]=1; lights_vr_include_unarmed_flashlights();
+ CHECK(lights_globals.scene_point_light_count==MAXIMUM_RENDERED_LIGHTS);
+ CHECK(lights_globals.scene_point_lights[0]==2 && lights_globals.scene_point_lights[1]==3);
+ for(scenario=0;scenario<6;scenario++) {
+ real_point3d rendered,tick; real radius,tick_radius;
+ flashlight_reset();
+ switch(scenario) {
+ case 0: fixture_light_definition.flags=0; break; /* ordinary dynamic light */
+ case 1: fixture_lights[0].parent_light_index=5; break; /* transient effect */
+ case 2: fixture_lights[0].flags=0; break; /* static light */
+ case 3: inventory[0]=0; fixture_weapon_count[0]=1; break; /* armed marker path */
+ case 4: vr_render.active=FALSE; break; /* flat/HUD/ended pass */
+ case 5: fixture_lights[0].object_index=1; break; /* remote owner */
+ }
+ light_get_bounding_sphere(0,&rendered,&radius);
+ light_compute_bounding_sphere(0,TRUE,FALSE,TRUE,&tick,&tick_radius);
+ point_equal(rendered,tick); CHECK(near(radius,tick_radius));
+ CHECK(!light_render_skips_clusters(&fixture_lights[0]));
+ lights_vr_include_unarmed_flashlights(); CHECK(lights_globals.scene_point_light_count==0);
+ if(scenario==3 || scenario==4) CHECK(data_next_calls==0);
+ }
+ /* Armed supersize convention remains exactly as before. */
+ flashlight_reset(); inventory[0]=0; fixture_weapon_count[0]=1;
+ fixture_lights[0].flags|=1u<<_point_light_attached_to_first_person_weapon_bit;
+ fixture_light_definition.flags|=1u<<_light_definition_supersize_in_first_person_bit;
+ CHECK(light_render_skips_clusters(&fixture_lights[0]));
+}
+'''
+
+
+def main():
+ render = (ROOT / 'port/linux/game/vr_render.c').read_text()
+ lights = (ROOT / 'source/objects/object_lights.c').read_text()
+ first_person = (ROOT / 'source/interface/first_person_weapons.c').read_text()
+ old_first_person = subprocess.check_output(['git', 'show', reticle.ORIGINAL_ENGINE_COMMIT +
+ ':source/interface/first_person_weapons.c'], cwd=ROOT, text=True)
+ assert reticle.function(first_person, 'first_person_weapon_center_flashlight') == reticle.function(old_first_person, 'first_person_weapon_center_flashlight'), 'Armed marker behavior changed'
+ old_lights = subprocess.check_output(['git', 'show', reticle.ORIGINAL_ENGINE_COMMIT +
+ ':source/objects/object_lights.c'], cwd=ROOT, text=True)
+ # Preserve recovered cone math and its simulation-only wrapper exactly.
+ old_body = reticle.function(old_lights, 'light_compute_bounding_sphere')
+ old_body = old_body[old_body.index('{'):].replace('struct light_datum *light = light_get(light_index);', '')
+ new_body = reticle.function(lights, 'light_compute_bounding_sphere_from_basis')
+ assert reticle.normalized(old_body) == reticle.normalized(new_body[new_body.index('{'):])
+ reconnect = reticle.function(lights, 'light_reconnect_to_map')
+ assert 'light_compute_render_bounding_sphere' not in reconnect and 'light_compute_bounding_sphere(' in reconnect
+ preprocess = reticle.function(lights, 'lights_preprocess_scene')
+ assert preprocess.index('lights_vr_include_unarmed_flashlights();') < preprocess.index('light_marker_end();')
+ assert preprocess.index('light_vr_unarmed_flashlight_pose(light, &light_parameters.position,') < preprocess.index('rasterizer_light_submit(')
+ helper = reticle.function(render, 'vr_render_player_flashlight_pose')
+ assert 'halo_vr_view(' not in helper and 'weapon_camera.position' in helper
+ names = ['light_vr_unarmed_flashlight_pose', 'lights_vr_include_unarmed_flashlights',
+ 'light_compute_bounding_sphere_from_basis', 'light_compute_bounding_sphere',
+ 'light_compute_render_bounding_sphere', 'light_render_skips_clusters',
+ 'light_get_bounding_sphere', 'lights_render_diffuse', 'lights_render_specular']
+ production = helper + '\n\n' + '\n\n'.join(reticle.function(lights, name) for name in names)
+ extension = LIGHT_FIXTURE.replace('@@FLASHLIGHT_FUNCTIONS@@', production)
+ harness = reticle.HARNESS
+ view_header = (ROOT / 'port/linux/src/vr.h').read_text()
+ view_begin = view_header.index('struct halo_vr_view\n{')
+ view_end = reticle.block_end(view_header, view_header.index('{', view_begin))
+ harness = re.sub(r'struct halo_vr_view \{[^\n]+\};',
+ lambda match: view_header[view_begin:view_end] + ';', harness)
+ harness = harness.replace('boolean active,crosshairs_drawn;',
+ 'boolean active,crosshairs_drawn,weapon_camera_set,weapon_camera_valid; struct render_camera weapon_camera;')
+ harness = harness.replace('static struct { struct render_camera camera; } render;',
+ 'static struct { struct render_camera camera; short local_player_index; } render;')
+ harness = harness.replace('int main(void) {', extension + '\nint main(void) {\n' +
+ ' check_flashlight_render_refresh(); check_flashlight_rejection(); check_flashlight_bounds_and_clusters();')
+ reticle.HARNESS = harness
+ reticle.main()
+ print('PASS: actual render-rate flashlight pose, unchanged armed/tick math and read-only visibility/diffuse/specular bounds')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_framebuffer.py b/tools/test_vr_framebuffer.py
new file mode 100644
index 0000000000000000000000000000000000000000..251854a1c21c39e3e56af83e72fb8c94b1bc6750
--- /dev/null
+++ b/tools/test_vr_framebuffer.py
@@ -0,0 +1,158 @@
+#!/usr/bin/env python3
+"""Execute production framebuffer cache, draw binding and VR submission guards.
+
+GL attachment failures are deterministic. No runtime, game data or GPU is
+used. This checks frame validity and recovery, not visual/performance quality.
+"""
+import argparse
+from pathlib import Path
+from test_persistent_stream_buffers import function
+from test_vr_zoom_aim import compile_and_run
+
+ROOT = Path(__file__).resolve().parents[1]
+
+HARNESS = r'''
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+typedef unsigned int GLuint, GLenum;
+typedef int GLint, GLsizei, BOOL;
+typedef float GLfloat;
+typedef unsigned long long Uint64;
+#define TRUE 1
+#define FALSE 0
+#define GL_TRUE 1
+#define SCREEN_HEIGHT 480
+enum {GL_FRAMEBUFFER=1,GL_FRAMEBUFFER_COMPLETE,GL_COLOR_ATTACHMENT0,GL_DEPTH_STENCIL_ATTACHMENT,
+ GL_NONE,GL_TEXTURE_2D,GL_TEXTURE_2D_ARRAY,GL_VERTEX_SHADER,GL_FRAGMENT_SHADER,GL_LINK_STATUS,
+ GL_SCISSOR_TEST,GL_BLEND,GL_CULL_FACE,GL_STENCIL_TEST,GL_DEPTH_TEST,GL_POLYGON_OFFSET_FILL,
+ GL_TEXTURE0,GL_TEXTURE_MIN_FILTER,GL_TEXTURE_MAG_FILTER,GL_LINEAR,GL_TRIANGLES};
+enum {HALO_VR_PASS_NONE,HALO_VR_PASS_EYES,HALO_VR_PASS_HUD,VR_SWAPCHAIN_HUD=1};
+static unsigned checks;
+#define CHECK(c) do {checks++;if(!(c)){fprintf(stderr,"line %d: %s\n",__LINE__,#c);exit(1);}}while(0)
+struct framebuffer_entry {struct framebuffer_entry *next;GLuint color,depth,framebuffer;int views;};
+static struct framebuffer_entry *framebuffers;
+static int framebuffer_views=1;
+struct surface {unsigned long Data;};
+struct target {GLuint texture;BOOL depth;int layers;unsigned long data,gl_height;float scale[2];};
+struct render_target_entry {struct target target;unsigned long last_rendered;};
+static struct {struct surface back_buffer;struct surface *render_target,*depth_stencil;unsigned long frame;} device;
+static struct {
+ BOOL ready,eyes_drawn,hud_drawn,frame_failed,multiview,left_eye,right_eye,visibility_test;
+ int pass,eye_alone,hud_width,hud_height,gpu_time,gpu_frames;
+ double gpu_milliseconds,gpu_worst;
+} vr_gl;
+static float screen_scale[2],target_scale[2];
+static unsigned long target_height;
+static struct render_target_entry color_fixture,depth_fixture;
+static int fixture_complete=1,created,deleted,invalidated,state_binds,draws,submitted,submitted_eyes,submitted_hud;
+static int depth_copies,screenshots,image_calls;
+static GLuint bound;
+static int complete[512],attachment_views[512];
+static void platform_log(const char *format,...){(void)format;}
+static void xgpu_gl_state_invalidate(void){invalidated++;}
+static void glGenFramebuffers(GLsizei n,GLuint *fbo){CHECK(n==1&&created<510);*fbo=(GLuint)++created;}
+static void glBindFramebuffer(GLenum target,GLuint fbo){CHECK(target==GL_FRAMEBUFFER&&fbo<512);bound=fbo;}
+static void glDeleteFramebuffers(GLsizei n,const GLuint *fbo){CHECK(n==1&&*fbo>0);deleted++;complete[*fbo]=0;}
+static void glFramebufferTextureMultiviewOVR(GLenum target,GLenum attachment,GLuint texture,GLint level,GLint first,GLsizei views){
+ CHECK(target==GL_FRAMEBUFFER&&texture&&level==0&&first==0&&views==2);(void)attachment;attachment_views[bound]=views;}
+static void glFramebufferTextureLayer(GLenum target,GLenum attachment,GLuint texture,GLint level,GLint layer){
+ CHECK(target==GL_FRAMEBUFFER&&texture&&level==0&&(layer==0||layer==1));(void)attachment;attachment_views[bound]=-layer-1;}
+static void glFramebufferTexture2D(GLenum target,GLenum attachment,GLenum kind,GLuint texture,GLint level){
+ CHECK(target==GL_FRAMEBUFFER&&kind==GL_TEXTURE_2D&&texture&&level==0);(void)attachment;attachment_views[bound]=1;}
+static void glDrawBuffers(GLsizei n,const GLenum *value){CHECK(n==1&&(*value==GL_NONE||*value==GL_COLOR_ATTACHMENT0));}
+static GLenum glCheckFramebufferStatus(GLenum target){CHECK(target==GL_FRAMEBUFFER&&bound);complete[bound]=fixture_complete;return fixture_complete?GL_FRAMEBUFFER_COMPLETE:0;}
+static void state_framebuffer(GLuint fbo){CHECK(fbo&&complete[fbo]);state_binds++;bound=fbo;}
+static struct render_target_entry *render_target_get(const struct surface *surface){
+ if(surface==device.render_target||surface==&device.back_buffer)return &color_fixture;
+ if(surface==device.depth_stencil)return &depth_fixture;
+ return NULL;
+}
+static void vr_frame_resume(void){}
+static GLuint compile_shader(GLenum type,const char *source,const char *name){CHECK(source&&name);return type==GL_VERTEX_SHADER?11:12;}
+static GLuint glCreateProgram(void){return 20;}
+static void glAttachShader(GLuint program,GLuint shader){CHECK(program==20&&(shader==11||shader==12));}
+static void glLinkProgram(GLuint program){CHECK(program==20);}
+static void glGetProgramiv(GLuint program,GLenum name,GLint *status){CHECK(program==20&&name==GL_LINK_STATUS);*status=1;}
+static void glUseProgram(GLuint program){CHECK(program==20);}
+static GLint glGetUniformLocation(GLuint program,const char *name){CHECK(program==20&&name);return !strcmp(name,"size")?2:1;}
+static void glUniform1i(GLint location,GLint value){CHECK(location==1&&value==0);}
+static void glUniform2f(GLint location,GLfloat x,GLfloat y){CHECK(location==2&&x>0&&y>0);}
+static void glViewport(GLint x,GLint y,GLsizei width,GLsizei height){CHECK(x==0&&y==0&&width>0&&height>0);}
+static void glDisable(GLenum flag){(void)flag;}
+static void glColorMask(int r,int g,int b,int a){CHECK(r&&g&&b&&a);}
+static void glActiveTexture(GLenum texture){CHECK(texture==GL_TEXTURE0);}
+static void glBindTexture(GLenum kind,GLuint texture){CHECK(kind==GL_TEXTURE_2D&&texture);}
+static void glTexParameteri(GLenum kind,GLenum parameter,GLint value){(void)kind;(void)parameter;(void)value;}
+static void glBindSampler(GLuint unit,GLuint sampler){CHECK(!unit&&!sampler);}
+static void glDrawArrays(GLenum mode,GLint first,GLsizei count){CHECK(mode==GL_TRIANGLES&&first==0&&count==3&&bound&&complete[bound]);draws++;}
+static void vr_timing(int phase){CHECK(phase==0||phase==3);}
+static void vr_copy_depth(void){depth_copies++;}
+static void vr_write_screenshots(struct render_target_entry *hud){(void)hud;screenshots++;}
+static GLuint halo_vr_image(int kind){CHECK(kind==VR_SWAPCHAIN_HUD);image_calls++;return 33;}
+static void glFlush(void){}
+static void glFinish(void){}
+static Uint64 SDL_GetTicksNS(void){return 1;}
+static void halo_vr_present(int eyes,int hud){submitted++;submitted_eyes=eyes;submitted_hud=hud;}
+@@PRODUCTION@@
+static void reset(void){
+ struct framebuffer_entry *entry;
+ while((entry=framebuffers)!=NULL){framebuffers=entry->next;free(entry);}
+ memset(&vr_gl,0,sizeof(vr_gl));memset(&device,0,sizeof(device));
+ memset(&color_fixture,0,sizeof(color_fixture));memset(&depth_fixture,0,sizeof(depth_fixture));
+ memset(complete,0,sizeof(complete));memset(attachment_views,0,sizeof(attachment_views));
+ framebuffer_views=1;fixture_complete=1;created=deleted=invalidated=state_binds=draws=submitted=0;
+ submitted_eyes=submitted_hud=depth_copies=screenshots=image_calls=0;bound=0;
+ device.back_buffer.Data=3;device.render_target=&device.back_buffer;device.depth_stencil=(struct surface*)4;device.frame=41;
+ color_fixture.target=(struct target){11,0,2,3,1200,{2,2}};
+ depth_fixture.target=(struct target){12,1,2,4,1200,{2,2}};
+ vr_gl.ready=1;vr_gl.pass=HALO_VR_PASS_EYES;vr_gl.eye_alone=-1;vr_gl.hud_width=1280;vr_gl.hud_height=960;
+}
+static void check_fbo_cache_recovery(void){
+ int views;static const int modes[]={1,2,-1,-2};
+ for(views=0;views<4;views++){
+ GLuint fbo;reset();framebuffer_views=modes[views];fixture_complete=0;
+ CHECK(!framebuffer_get(11,12));CHECK(!framebuffers&&deleted==1&&bound==0&&vr_gl.frame_failed);
+ CHECK(invalidated==1);fixture_complete=1;vr_gl.frame_failed=0;
+ fbo=framebuffer_get(11,12);CHECK(fbo&&framebuffers&&complete[fbo]&&attachment_views[fbo]==modes[views]);
+ CHECK(framebuffer_get(11,12)==fbo&&created==2&&!vr_gl.frame_failed);CHECK(framebuffers->next==NULL);
+ }
+}
+static void check_failed_draw_not_marked(void){
+ BOOL has_depth=1;reset();fixture_complete=0;
+ CHECK(!bind_targets(&has_depth));CHECK(!has_depth&&!state_binds&&!vr_gl.eyes_drawn&&vr_gl.frame_failed);
+ CHECK(!color_fixture.last_rendered&&framebuffer_views==1);
+ fixture_complete=1;CHECK(bind_targets(&has_depth));CHECK(has_depth&&state_binds==1&&vr_gl.eyes_drawn);
+ CHECK(color_fixture.last_rendered==42);CHECK(vr_gl.frame_failed); /* Failure is latched until present. */
+ vr_present(1);CHECK(submitted==1&&!submitted_eyes&&!submitted_hud&&!depth_copies&&!screenshots);
+ CHECK(!vr_gl.frame_failed&&!vr_gl.eyes_drawn&&!vr_gl.hud_drawn);
+ vr_gl.pass=HALO_VR_PASS_EYES;CHECK(bind_targets(&has_depth));vr_present(0);
+ CHECK(submitted==2&&submitted_eyes&&!submitted_hud&&depth_copies==1);
+}
+static void check_hud_copy_and_submission(void){
+ reset();vr_gl.pass=HALO_VR_PASS_HUD;fixture_complete=0;
+ CHECK(!vr_copy_hud(&color_fixture,33));CHECK(!draws&&vr_gl.frame_failed&&!framebuffers);
+ fixture_complete=1;vr_gl.frame_failed=0;CHECK(vr_copy_hud(&color_fixture,33));CHECK(draws==1);
+ reset();vr_gl.eyes_drawn=vr_gl.hud_drawn=1;vr_present(1);
+ CHECK(submitted_eyes&&submitted_hud&&draws==1&&screenshots==1&&depth_copies==1);
+ reset();vr_gl.eyes_drawn=vr_gl.hud_drawn=1;fixture_complete=0;vr_present(0);
+ CHECK(!submitted_eyes&&!submitted_hud&&!draws&&!vr_gl.frame_failed);
+}
+int main(void){check_fbo_cache_recovery();check_failed_draw_not_marked();check_hud_copy_and_submission();reset();
+ printf("PASS: %u production framebuffer failure, recovery, bind and submission assertions\n",checks);return 0;}
+'''
+
+def main():
+ parser=argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall',type=Path)
+ args=parser.parse_args()
+ source=(ROOT/'port/linux/src/d3d8_gl.c').read_text()
+ production='\n\n'.join(function(source,name) for name in ('framebuffer_get','bind_targets','vr_copy_hud','vr_present'))
+ assert 'framebuffer_get(0, image)' in function(source,'vr_copy_depth')
+ assert 'if (!framebuffer)' in function(source,'vr_copy_depth')
+ assert 'framebuffer = framebuffer_get' in function(source,'halo_vr_pass')
+ assert 'if (framebuffer)' in function(source,'halo_vr_pass')
+ compile_and_run(HARNESS.replace('@@PRODUCTION@@',production),args)
+
+if __name__=='__main__':main()
diff --git a/tools/test_vr_locomotion.py b/tools/test_vr_locomotion.py
new file mode 100644
index 0000000000000000000000000000000000000000..353b04257e8f42c3e05b75c78c03fb53b063791f
--- /dev/null
+++ b/tools/test_vr_locomotion.py
@@ -0,0 +1,475 @@
+#!/usr/bin/env python3
+"""Compile and run the production VR walking math and integration on a host.
+
+No headset, game data, SDL or OpenXR is required. Uses cc/clang/gcc on POSIX
+or Visual Studio's C11 compiler on Windows; --cc and --vcvarsall can override.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+import textwrap
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def masked(text):
+ return re.sub(r'/\*.*?\*/|//[^\n]*|"(?:\\.|[^"\\])*"',
+ lambda match: re.sub(r'[^\n]', ' ', match.group()), text, flags=re.S)
+
+
+def function(text, name):
+ match = re.search(r'(?:static\s+)?(?:void|float|int)\s+' + re.escape(name) +
+ r'\s*\([^;]*?\)\s*\{', text)
+ if not match:
+ raise AssertionError(f"Missing production function: {name}")
+ code = masked(text)
+ depth = 1
+ end = code.index('{', match.start()) + 1
+ while depth:
+ depth += (code[end] == '{') - (code[end] == '}')
+ end += 1
+ return text[match.start():end]
+
+
+def source_checks(vr, player):
+ baseline = subprocess.check_output(['git', 'show', '2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4:port/linux/src/vr.c'],
+ cwd=ROOT, text=True)
+ # Seat calibration changes only the vehicle basis; the C harness below
+ # exercises original on-foot aim and walking, and test_vr_vehicle covers seats.
+ assert 'if (!vehicle_view_active())' in function(vr, 'halo_vr_aim')
+ assert function(vr, 'weapon_pointing_raw').replace('weapon_pointing_raw', 'weapon_pointing') == function(baseline, 'weapon_pointing'), 'Raw long-gun aim changed'
+ old_pad = masked(function(baseline, 'halo_vr_gamepad'))
+ new_pad = masked(function(vr, 'halo_vr_gamepad'))
+ old_pad = old_pad.replace('XINPUT_GAMEPAD_WHITE, buttons & VR_BUTTON_LEFT_BUMPER',
+ 'XINPUT_GAMEPAD_WHITE, buttons & VR_BUTTON_RIGHT_BUMPER')
+ old_pad = old_pad.replace('XINPUT_GAMEPAD_BLACK, buttons & VR_BUTTON_RIGHT_BUMPER',
+ 'XINPUT_GAMEPAD_BLACK, buttons & VR_BUTTON_LEFT_BUMPER')
+ # The Warthog consumes A/B as accelerator/brake; production driving tests
+ # execute that exception. Strip only that explicitly delimited block to
+ # compare the remaining on-foot Xbox mappings against the pinned source.
+ a_start = new_pad.index('if (!(buttons & VR_BUTTON_A))')
+ a_end = new_pad.index('if (!(buttons & VR_BUTTON_B))', a_start)
+ assert 'pad->bAnalogButtons[XINPUT_GAMEPAD_A] = 0;' in new_pad[a_start:a_end]
+ new_pad = new_pad[:a_start] + 'analog(pad, XINPUT_GAMEPAD_A, buttons & VR_BUTTON_A);\n' + new_pad[a_end:]
+ b_start = new_pad.index('if (!(buttons & VR_BUTTON_B))')
+ b_end = new_pad.index('analog(pad, XINPUT_GAMEPAD_X', b_start)
+ assert 'pad->bAnalogButtons[XINPUT_GAMEPAD_B] = 0;' in new_pad[b_start:b_end]
+ new_pad = new_pad[:b_start] + 'analog(pad, XINPUT_GAMEPAD_B, (buttons & VR_BUTTON_B) || vr.melee_frames > 0);\n' + new_pad[b_end:]
+ # The private menu ray consumes only the right trigger until released;
+ # all remaining Xbox mappings, sticks and native A/B navigation stay identical.
+ old_start = old_pad.index('trigger = (BYTE)(vr.input.right_trigger')
+ new_start = new_pad.index('if (menu_pointer_fire_blocked())')
+ old_end = old_pad.index('if (buttons & VR_BUTTON_DPAD_UP)', old_start)
+ new_end = new_pad.index('if (buttons & VR_BUTTON_DPAD_UP)', new_start)
+ assert 'pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER] = 0;' in new_pad[new_start:new_end]
+ old_pad = old_pad[:old_start] + old_pad[old_end:]
+ new_pad = new_pad[:new_start] + new_pad[new_end:]
+ # Driver seats retain native right-stick input; inactive driver behavior
+ # and every original button/axis mapping remain byte-for-byte equivalent.
+ new_pad = new_pad.replace(' || driving_active()', '')
+ assert re.sub(r'\s+', '', old_pad) == re.sub(r'\s+', '', new_pad), 'Xbox controls changed beyond authorized menu trigger/bumper fix'
+ config = (ROOT / 'port/linux/src/port_config.c').read_text()
+ assert re.search(r'"vr\.movement",\s*_config_string,\s*"\\"head\\"",\s*"HALO_VR_MOVEMENT"', config)
+ hook_start = player.index('if (local_player_index == 0 && unit->object.parent_object_index == NONE &&')
+ hook_open = player.index('{', hook_start)
+ hook_end = player.index('}', hook_open) + 1
+ hook = player[hook_start:hook_end]
+ assert '!director_inhibited_facing(local_player_index)' in hook and '!cinematic_in_progress()' in hook
+ assert 'halo_vr_locomotion(player->desired_angles.yaw, &input.throttle.i, &input.throttle.j)' in hook
+ assert player.index('player->desired_angles.yaw = vr_yaw;') < hook_start
+ assert hook_end < player.index('player->throttle = input.throttle;') < player.index('update_client_queue(&action);')
+ guard = re.search(r'if \(vr_aim == 1 && \(!isfinite\(vr_yaw\) \|\| !isfinite\(vr_pitch\)\)\)\s*vr_aim = 0;', player)
+ assert guard and guard.start() < player.index('while (vr_yaw < 0.f)')
+ # Include the production statement's leading indent before dedenting.
+ # The extracted if body must retain its relative indent in generated C.
+ guard_line_start = player.rfind('\n', 0, guard.start()) + 1
+ guard_source = player[guard_line_start:guard.end()].expandtabs(4)
+ return hook, textwrap.indent(textwrap.dedent(guard_source), ' ')
+
+
+HARNESS = r'''
+#define _USE_MATH_DEFINES
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "vr_host.h"
+#include "vr_locomotion.h"
+#include "vr_vehicle.h"
+#include "vr_driving.h"
+#ifndef M_PI
+#define M_PI 3.14159265358979323846
+#endif
+#define NONE (-1)
+#define FLAG(bit) (1UL << (bit))
+#define TEST_FLAG(value, bit) (((value) & FLAG(bit)) != 0)
+#define EPSILON 0.00005f
+static int checks;
+#define CHECK(condition) do { checks++; if (!(condition)) { \
+ fprintf(stderr, "failed line %d: %s\n", __LINE__, #condition); exit(1); } } while (0)
+static int close_float(float first, float second) { return fabsf(first - second) <= EPSILON; }
+static struct {
+ int initialized, frame, pending, origin_valid, aim_synced, menus, aiming, long_gun;
+ struct vr_host_views views;
+ int menu_open;
+ struct vr_host_pose menu_pose;
+ struct halo_vr_vehicle_reference vehicle;
+ float origin_yaw, body_yaw, origin_position[3];
+ struct halo_vr_locomotion_heading movement_heading;
+ int tutorial_head_valid, tutorial_turn_flags, turn_control_allowed;
+ float tutorial_head_yaw, tutorial_head_pitch;
+} vr;
+static struct {
+ int head_movement, controller_aim, gamepad_aim, gamepad_snap, standing;
+ int vehicle_first_person, vehicle_seat_calibration;
+ float snap_angle, player_height;
+} settings;
+static void settings_read(void) {}
+static struct halo_vr_driving_state driving;
+static void driving_reset(int release_required) { (void)release_required; }
+static int vehicle_basis(float basis[3][3]) { (void)basis; return 0; }
+static int driving_aim(float *yaw,float *pitch) { (void)yaw; (void)pitch; return 0; }
+static void platform_log(const char *format, const char *height) { (void)format; (void)height; }
+static int weapon_calls;
+/* The production aiming function is unchanged; the host stub supplies the
+controller pose that the unmodified long-gun/weapon_pointing code computes. */
+static void weapon_pointing(float point[3], float up[3]);
+@@FUNCTIONS@@
+static void weapon_pointing(float point[3], float up[3]) {
+ static const float forward[3] = { 0.0f, 0.0f, -1.0f };
+ static const float upwards[3] = { 0.0f, 1.0f, 0.0f };
+ weapon_calls++;
+ quaternion_rotate(vr.views.aim[1].orientation, forward, point);
+ quaternion_rotate(vr.views.aim[1].orientation, upwards, up);
+}
+typedef struct { float yaw, pitch; } real_euler_angles2d;
+typedef struct { float i, j; } real_vector2d;
+typedef float real;
+enum {
+ _unit_control_action_bit, _unit_control_jump_bit, _unit_control_throw_grenade_bit,
+ _player_control_action_bit, _player_control_jump_bit, _player_control_grenade_trigger_bit,
+ _player_control_primary_trigger_bit, _player_control_look_relative_up_bit,
+ _player_control_look_relative_down_bit, _player_control_look_relative_left_bit,
+ _player_control_look_relative_right_bit, _player_control_move_relative_forward_bit,
+ _player_control_move_relative_backward_bit, _player_control_move_relative_right_bit,
+ _player_control_move_relative_left_bit
+};
+struct player_control_globals_data { unsigned long action_flags; } tutorial;
+static struct player_control_globals_data *player_control_globals = &tutorial;
+@@TUTORIAL@@
+static int local_player_index, inhibited, cinematic, dispatch_calls;
+static struct { struct { long parent_object_index; } object; } unit_storage, *unit = &unit_storage;
+static struct { real_euler_angles2d desired_angles; } player_storage, *player = &player_storage;
+static struct { real_vector2d throttle; } input;
+static int director_inhibited_facing(int index) { (void)index; return inhibited; }
+static int cinematic_in_progress(void) { return cinematic; }
+static void record_locomotion(float yaw, float *forward, float *left) {
+ dispatch_calls++;
+ halo_vr_locomotion(yaw, forward, left);
+}
+#define halo_vr_locomotion record_locomotion
+static void dispatch(void) { @@HOOK@@ }
+#undef halo_vr_locomotion
+static int guard_aim(float vr_yaw, float vr_pitch, int vr_aim) {
+@@AIM_GUARD@@
+ return vr_aim;
+}
+
+static void head_pose(float yaw, float pitch) {
+ float cy = cosf(yaw * 0.5f), sy = sinf(yaw * 0.5f);
+ float cp = cosf(pitch * 0.5f), sp = sinf(pitch * 0.5f);
+ vr.views.head.orientation[0] = cy * sp;
+ vr.views.head.orientation[1] = sy * cp;
+ vr.views.head.orientation[2] = -sy * sp;
+ vr.views.head.orientation[3] = cy * cp;
+}
+static void reset_state(void) {
+ memset(&vr, 0, sizeof(vr)); memset(&settings, 0, sizeof(settings));
+ vr.initialized = vr.frame = vr.origin_valid = vr.aim_synced = 1;
+ vr.views.should_render = vr.views.focused = vr.views.head.valid = 1;
+ vr.turn_control_allowed = 1;
+ vr.views.eye[0].valid = vr.views.eye[1].valid = 1;
+ vr.views.eye[0].orientation[3] = vr.views.eye[1].orientation[3] = 1.0f;
+ settings.head_movement = settings.controller_aim = 1;
+ settings.player_height = 1.7f;
+ head_pose(0.0f, 0.0f);
+ tutorial.action_flags = 0;
+}
+static void expect_walk(float aim, float forward, float left, float expected_forward, float expected_left) {
+ halo_vr_locomotion(aim, &forward, &left);
+ CHECK(close_float(forward, expected_forward)); CHECK(close_float(left, expected_left));
+}
+static void check_directions(void) {
+ const float half_pi = (float)M_PI * 0.5f;
+ float angle, aim, forward, left, expected_x, expected_y, actual_x, actual_y;
+ int head_step, aim_step;
+ reset_state(); head_pose(half_pi, 0.0f);
+ expect_walk(0.0f, 1.0f, 0.0f, 0.0f, 1.0f);
+ expect_walk(0.0f, 0.0f, 1.0f, -1.0f, 0.0f);
+ reset_state(); expect_walk(half_pi, 1.0f, 0.0f, 0.0f, -1.0f);
+ /* Compare actual world-space motion, not a second copy of rotation:
+ engine throttle uses weapon facing. Independently form head forward
+ and head left vectors and compare those with engine-applied throttle. */
+ for (head_step = -24; head_step <= 24; head_step++) {
+ angle = (float)head_step * 0.19f;
+ for (aim_step = -24; aim_step <= 24; aim_step++) {
+ aim = (float)aim_step * 0.27f;
+ reset_state(); vr.body_yaw = 0.43f; vr.origin_yaw = -0.31f;
+ head_pose(angle, 0.75f);
+ forward = 0.6f; left = 0.8f;
+ halo_vr_locomotion(aim, &forward, &left);
+ expected_x = 0.6f * cosf(angle + 0.74f) - 0.8f * sinf(angle + 0.74f);
+ expected_y = 0.6f * sinf(angle + 0.74f) + 0.8f * cosf(angle + 0.74f);
+ actual_x = forward * cosf(aim) - left * sinf(aim);
+ actual_y = forward * sinf(aim) + left * cosf(aim);
+ CHECK(close_float(actual_x, expected_x)); CHECK(close_float(actual_y, expected_y));
+ CHECK(close_float(forward * forward + left * left, 1.0f));
+ }
+ }
+ reset_state(); head_pose(0.7f, 0.0f);
+ expect_walk(12.0f * (float)M_PI + 0.7f, -0.6f, 0.4f, -0.6f, 0.4f);
+ vr.body_yaw = half_pi; head_pose(0.0f, 0.0f);
+ expect_walk(0.0f, 1.0f, 0.0f, 0.0f, 1.0f); /* snap turn */
+ vr.body_yaw = 0.25f;
+ expect_walk(0.0f, 1.0f, 0.0f, cosf(0.25f), sinf(0.25f)); /* smooth turn */
+}
+static void check_pitch_and_recentering(void) {
+ float before_forward = 1.0f, before_left = 0.0f;
+ reset_state(); head_pose(0.9f, 0.0f);
+ halo_vr_locomotion(0.2f, &before_forward, &before_left);
+ head_pose(-2.0f, (float)M_PI * 0.5f);
+ expect_walk(0.2f, 1.0f, 0.0f, before_forward, before_left);
+ /* Body turns still move the cached vertical heading; weapon turns do
+ not redirect walking. Recenter adjusts body/origin by opposite amounts. */
+ vr.body_yaw += 0.3f;
+ expect_walk(0.5f, 1.0f, 0.0f, before_forward, before_left);
+ reset_state(); head_pose(2.0f, -(float)M_PI * 0.5f);
+ expect_walk(0.0f, 1.0f, 0.0f, 0.0f, 0.0f);
+ CHECK(!vr.movement_heading.valid);
+ reset_state(); vr.origin_yaw = -0.4f; vr.body_yaw = 0.2f; head_pose(0.7f, 0.0f);
+ before_forward = 1.0f; before_left = 0.0f;
+ halo_vr_locomotion(0.1f, &before_forward, &before_left);
+ recentre(); CHECK(!vr.movement_heading.valid);
+ expect_walk(0.1f, 1.0f, 0.0f, before_forward, before_left);
+ halo_vr_aim_release(); CHECK(!vr.movement_heading.valid);
+}
+static void check_tracking_guards(void) {
+ float forward, left, normalized[4], pose[4] = { 0.0f, 0.0f, 0.0f, 1.01f };
+ int flag;
+ for (flag = 0; flag < 8; flag++) {
+ reset_state(); head_pose(0.9f, 0.0f);
+ forward = 1.0f; left = 0.0f; halo_vr_locomotion(0.0f, &forward, &left);
+ CHECK(vr.movement_heading.valid);
+ switch (flag) {
+ case 0: vr.views.head.valid = 0; break;
+ case 1: vr.views.focused = 0; break;
+ case 2: vr.views.should_render = 0; break;
+ case 3: vr.frame = vr.pending = 0; break;
+ case 4: vr.origin_valid = 0; break;
+ case 5: vr.aim_synced = 0; break;
+ case 6: vr.menus = 1; break;
+ case 7: vr.origin_yaw = NAN; break;
+ }
+ expect_walk(0.0f, 1.0f, 0.4f, 0.0f, 0.0f); CHECK(!vr.movement_heading.valid);
+ vr.views.head.valid = vr.views.focused = vr.views.should_render = vr.frame = 1;
+ vr.origin_valid = vr.aim_synced = 1; vr.menus = 0; vr.origin_yaw = 0.0f;
+ head_pose(-1.0f, (float)M_PI * 0.5f);
+ expect_walk(0.0f, 1.0f, 0.0f, 0.0f, 0.0f); /* no stale yaw after lost tracking */
+ }
+ reset_state(); vr.frame = 0; vr.pending = 1;
+ expect_walk(0.0f, 0.6f, 0.8f, 0.6f, 0.8f);
+ reset_state(); vr.views.head.orientation[1] = NAN;
+ expect_walk(0.0f, 1.0f, 0.2f, 0.0f, 0.0f);
+ reset_state(); vr.views.head.orientation[3] = INFINITY;
+ expect_walk(0.0f, 1.0f, 0.2f, 0.0f, 0.0f);
+ reset_state(); memset(vr.views.head.orientation, 0, sizeof(vr.views.head.orientation));
+ expect_walk(0.0f, 1.0f, 0.2f, 0.0f, 0.0f);
+ reset_state(); expect_walk(NAN, 1.0f, 0.2f, 0.0f, 0.0f);
+ reset_state(); vr.body_yaw = INFINITY; expect_walk(0.0f, 1.0f, 0.2f, 0.0f, 0.0f);
+ reset_state(); expect_walk(0.0f, NAN, 0.2f, 0.0f, 0.0f);
+ reset_state(); expect_walk(0.0f, 0.2f, INFINITY, 0.0f, 0.0f);
+ CHECK(halo_vr_locomotion_orientation(pose, normalized));
+ CHECK(close_float(normalized[3], 1.0f));
+ pose[3] = 10.0f; CHECK(!halo_vr_locomotion_orientation(pose, normalized));
+ pose[3] = 0.0f; CHECK(!halo_vr_locomotion_orientation(pose, normalized));
+}
+static void check_exclusions_and_aim(void) {
+ float aim, pitch, saved_aim, saved_pitch, forward, left;
+ int exclusion;
+ CHECK(guard_aim(0.3f, -0.4f, 1) == 1);
+ CHECK(guard_aim(INFINITY, 0.0f, 1) == 0);
+ CHECK(guard_aim(-INFINITY, 0.0f, 1) == 0);
+ CHECK(guard_aim(NAN, 0.0f, 1) == 0);
+ CHECK(guard_aim(0.0f, NAN, 1) == 0);
+ CHECK(guard_aim(0.0f, INFINITY, 1) == 0);
+ CHECK(guard_aim(0.3f, -0.4f, 2) == 2);
+ for (exclusion = 0; exclusion < 4; exclusion++) {
+ reset_state(); head_pose(1.2f, 0.0f); vr.views.head.valid = 0;
+ switch (exclusion) {
+ case 0: vr.initialized = 0; break;
+ case 1: settings.head_movement = 0; break;
+ case 2: settings.gamepad_aim = 1; break;
+ case 3: settings.controller_aim = 0; break;
+ }
+ expect_walk(0.0f, 0.4f, -0.7f, 0.4f, -0.7f);
+ }
+ for (exclusion = 0; exclusion < 5; exclusion++) {
+ reset_state(); head_pose((float)M_PI * 0.5f, 0.0f);
+ local_player_index = inhibited = cinematic = dispatch_calls = 0;
+ unit->object.parent_object_index = NONE; player->desired_angles.yaw = 0.0f;
+ input.throttle.i = 1.0f; input.throttle.j = 0.0f;
+ switch (exclusion) {
+ case 1: local_player_index = 1; break;
+ case 2: unit->object.parent_object_index = 42; break;
+ case 3: inhibited = 1; break;
+ case 4: cinematic = 1; break;
+ }
+ dispatch(); CHECK(dispatch_calls == (exclusion == 0));
+ CHECK(close_float(input.throttle.i, exclusion == 0 ? 0.0f : 1.0f));
+ CHECK(close_float(input.throttle.j, exclusion == 0 ? 1.0f : 0.0f));
+ }
+ reset_state(); head_pose(0.8f, 0.1f);
+ memcpy(vr.views.aim[1].orientation, vr.views.head.orientation, sizeof(vr.views.head.orientation));
+ vr.views.aim[1].valid = 1; head_pose(0.0f, 0.0f); weapon_calls = 0;
+ CHECK(halo_vr_aim(0.0f, 1, &aim, &pitch) == 1); CHECK(weapon_calls == 1 && vr.long_gun == 1);
+ saved_aim = aim; saved_pitch = pitch; CHECK(close_float(aim, 0.8f));
+ forward = 1.0f; left = 0.0f; halo_vr_locomotion(aim, &forward, &left);
+ CHECK(aim == saved_aim && pitch == saved_pitch && weapon_calls == 1 && vr.long_gun == 1);
+ CHECK(close_float(forward * cosf(aim) - left * sinf(aim), 1.0f));
+ CHECK(close_float(forward * sinf(aim) + left * cosf(aim), 0.0f));
+}
+static void check_tutorial(void) {
+ const real_euler_angles2d still = { 0.0f, 0.0f };
+ const real_vector2d idle = { 0.0f, 0.0f };
+ const real_vector2d walk = { 1.0f, -1.0f };
+ reset_state();
+ player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(tutorial.action_flags == 0); /* establish physical-head anchor */
+ head_pose(0.005f, 0.0f); player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(tutorial.action_flags == 0); /* jitter rejected */
+ head_pose(0.03f, 0.04f); player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_left_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_up_bit));
+ tutorial.action_flags = 0; head_pose(-0.04f, -0.05f);
+ player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_right_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_down_bit));
+ tutorial.action_flags = 0; vr.views.head.valid = 0;
+ player_control_action_test_note(FLAG(_unit_control_jump_bit), &still, &walk, 1.0f);
+ CHECK(!vr.tutorial_head_valid);
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_jump_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_primary_trigger_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_move_relative_forward_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_move_relative_left_bit));
+}
+static void check_tutorial_tracking_recovery(void) {
+ const real_euler_angles2d still = { 0.0f, 0.0f };
+ const real_vector2d idle = { 0.0f, 0.0f };
+ int corruption, component;
+ for (corruption = 0; corruption < 5; corruption++) {
+ reset_state();
+ if (corruption != 0) {
+ player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(vr.tutorial_head_valid);
+ }
+ switch (corruption) {
+ case 0: case 1: vr.views.head.orientation[1] = NAN; break;
+ case 2: vr.views.head.orientation[3] = INFINITY; break;
+ case 3: memset(vr.views.head.orientation, 0, sizeof(vr.views.head.orientation)); break;
+ case 4: vr.views.head.orientation[3] = 10.0f; break;
+ }
+ /* A pose incorrectly marked valid must neither count movement nor
+ poison the next healthy head-motion anchor. */
+ player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(tutorial.action_flags == 0); CHECK(!vr.tutorial_head_valid);
+ head_pose(0.0f, 0.0f);
+ player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(vr.tutorial_head_valid); CHECK(tutorial.action_flags == 0);
+ head_pose(0.03f, 0.04f);
+ player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_left_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_up_bit));
+ }
+ reset_state(); player_control_action_test_note(0, &still, &idle, 0.0f);
+ head_pose(0.03f, 0.04f);
+ for (component = 0; component < 4; component++) vr.views.head.orientation[component] *= 1.01f;
+ player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_left_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_up_bit));
+ reset_state(); vr.tutorial_head_valid = 1;
+ vr.tutorial_head_yaw = NAN; vr.tutorial_head_pitch = INFINITY;
+ player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(vr.tutorial_head_valid && isfinite(vr.tutorial_head_yaw) && isfinite(vr.tutorial_head_pitch));
+ CHECK(tutorial.action_flags == 0);
+ head_pose(-0.03f, -0.04f);
+ player_control_action_test_note(0, &still, &idle, 0.0f);
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_right_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_down_bit));
+}
+int main(void) {
+ check_directions(); check_pitch_and_recentering(); check_tracking_guards();
+ check_exclusions_and_aim(); check_tutorial(); check_tutorial_tracking_recovery();
+ printf("PASS: %d production VR locomotion / input / tutorial assertions\n", checks);
+ return 0;
+}
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc', help='C compiler executable (cl, clang, gcc or cc)')
+ parser.add_argument('--vcvarsall', type=Path, help='Visual Studio VC/Auxiliary/Build/vcvarsall.bat')
+ args = parser.parse_args()
+ vr = (ROOT / 'port/linux/src/vr.c').read_text()
+ player = (ROOT / 'source/game/player_control.c').read_text()
+ hook, guard = source_checks(vr, player)
+ functions = '\n\n'.join(function(vr, name) for name in (
+ 'quaternion_rotate', 'quaternion_yaw', 'game_axes', 'vehicle_view_active', 'vehicle_capture', 'recentre',
+ 'halo_vr_locomotion', 'halo_vr_gaze_tracking_valid', 'halo_vr_tutorial_turn',
+ 'halo_vr_tutorial_look', 'halo_vr_aim', 'halo_vr_aim_release'))
+ harness = HARNESS.replace('@@FUNCTIONS@@', functions).replace('@@HOOK@@', hook)
+ harness = harness.replace('@@AIM_GUARD@@', guard)
+ harness = harness.replace('@@TUTORIAL@@', function(player, 'player_control_action_test_note'))
+ compiler = args.cc or os.environ.get('CC')
+ vcvars = args.vcvarsall
+ if not compiler and os.name == 'nt':
+ installations = Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio'
+ candidates = sorted(installations.glob('*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1])
+ vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('No host C compiler found. Install/select a compiler with --cc.')
+ with tempfile.TemporaryDirectory(prefix='halo-vr-locomotion-') as directory:
+ temp = Path(directory)
+ source = temp / 'test.c'
+ executable = temp / ('test.exe' if os.name == 'nt' else 'test')
+ source.write_text(harness)
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX',
+ '/D_CRT_SECURE_NO_WARNINGS', '/DHALO_VR', '/I' + str(ROOT / 'port/linux/src'),
+ '/Fe:' + str(executable), '/Fo:' + str(temp / 'test.obj'), str(source)]
+ if vcvars:
+ # Initialize only this child shell; never print or save its environment.
+ for path in (vcvars, Path(compiler), temp):
+ if any(character in str(path) for character in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported shell metacharacter in compiler/temp path.')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\n'
+ 'if errorlevel 1 exit /b 1\n' + subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-DHALO_VR',
+ '-I' + str(ROOT / 'port/linux/src'), str(source), *([] if os.name == 'nt' else ['-lm']), '-o', str(executable)]
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(executable)], cwd=temp, check=True)
+ print('PASS: production source integration, Xbox mapping and unchanged controller/long-gun aim')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_menu.py b/tools/test_vr_menu.py
new file mode 100644
index 0000000000000000000000000000000000000000..ffc4f1859326747a39958f3168ba5d48efc71aa2
--- /dev/null
+++ b/tools/test_vr_menu.py
@@ -0,0 +1,432 @@
+#!/usr/bin/env python3
+"""Run production menu placement and OpenXR quad submission on a C11 host.
+
+No game data, headset, runtime or SDL is required. The checked-in guest/host
+ABI and Khronos headers are used; only runtime calls and unrelated logging
+are stubbed. Menu input remains covered by the existing walking/tutorial tests.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+
+from test_vr_locomotion import function
+
+ROOT = Path(__file__).resolve().parents[1]
+
+HARNESS = r'''
+#define _USE_MATH_DEFINES
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <openxr/openxr.h>
+#include "vr_host.h"
+#include "vr_locomotion.h"
+#include "vr_vehicle.h"
+#include "vr_driving.h"
+#ifndef M_PI
+#define M_PI 3.14159265358979323846
+#endif
+#define HUD_WIDTH 1280
+#define HUD_HEIGHT 960
+static unsigned checks;
+#define CHECK(c) do { checks++; if (!(c)) { fprintf(stderr,"failed line %d: %s\n",__LINE__,#c); exit(1); } } while(0)
+static int close_float(float a, float b) { return fabsf(a-b) < 0.00005f; }
+static struct {
+ int initialized, frame, pending, attempted, origin_valid, aim_synced, aiming, aimed;
+ int menus, menu_open, depth_written, render_pose_valid;
+ struct vr_host_pose render_eye[2];
+ float render_fov[2][4];
+ unsigned long presents;
+ int acquired[VR_SWAPCHAIN_COUNT];
+ struct halo_vr_vehicle_reference vehicle;
+ float origin_yaw, body_yaw, origin_position[3], depth_near, depth_far;
+ struct vr_host_views views;
+ struct vr_host_layers pending_layers;
+ struct vr_host_pose menu_pose;
+ struct halo_vr_locomotion_heading movement_heading;
+ int tutorial_turn_flags, tutorial_head_valid;
+} vr;
+static struct {
+ int active, armed, down, blocked, position_valid;
+ short x, y;
+} menu_pointer;
+static struct {
+ int gamepad_aim, standing, vehicle_first_person, vehicle_seat_calibration;
+ float player_height, menu_distance, menu_width, hud_distance, hud_size;
+} settings;
+static void platform_log(const char *format, const char *mode) { (void)format; (void)mode; }
+static void driving_reset(int release_required) { (void)release_required; }
+static struct halo_vr_driving_state driving;
+@@GUEST@@
+
+/* Keep the host's production global separate from the guest's production
+global; this is its only identifier substitution in the extracted functions. */
+static struct {
+ int frame_begun;
+ int has_depth;
+ XrFrameState frame;
+ XrSession session;
+ XrSpace play_space, view_space;
+ XrSwapchain swapchains[VR_SWAPCHAIN_COUNT];
+ int swapchain_width[VR_SWAPCHAIN_COUNT], swapchain_height[VR_SWAPCHAIN_COUNT];
+} runtime;
+static struct { int enabled; double release, end; } timing;
+static XrCompositionLayerQuad submitted_quad;
+static unsigned submitted_quads, submitted_projections, release_calls;
+static double seconds_now(void) { return 0.0; }
+static void timing_frame(void) { CHECK(timing.enabled > 0); }
+static void check(XrResult result, const char *message) { CHECK(result == XR_SUCCESS && message != NULL); }
+void host_vr_release(int swapchain) { CHECK(swapchain >= 0 && swapchain < VR_SWAPCHAIN_RESERVED); release_calls++; }
+XRAPI_ATTR XrResult XRAPI_CALL xrEndFrame(XrSession session, const XrFrameEndInfo *end) {
+ uint32_t index;
+ CHECK(session == runtime.session && end->type == XR_TYPE_FRAME_END_INFO && end->next == NULL);
+ CHECK(end->displayTime == runtime.frame.predictedDisplayTime);
+ CHECK(end->environmentBlendMode == XR_ENVIRONMENT_BLEND_MODE_OPAQUE && end->layerCount <= 2);
+ submitted_quads = submitted_projections = 0;
+ for (index = 0; index < end->layerCount; index++) {
+ const XrCompositionLayerBaseHeader *layer = end->layers[index];
+ if (layer->type == XR_TYPE_COMPOSITION_LAYER_QUAD) {
+ submitted_quad = *(const XrCompositionLayerQuad *)layer; submitted_quads++;
+ } else {
+ CHECK(layer->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION);
+ submitted_projections++;
+ }
+ }
+ return XR_SUCCESS;
+}
+/* OpenXR's conventional { TYPE } initializes the other members to zero. */
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wmissing-field-initializers"
+#endif
+@@HOST@@
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
+
+/* Execute the exact production swapchain setup fragment. Runtime creation
+ * records explicit kinds; no reserved former-optic slot may be touched. */
+static int created_count, created_kinds[3], creation_failure;
+static int swapchain_create(int kind, int width, int height, int layers, struct vr_host_info *info) {
+ CHECK(kind >= VR_SWAPCHAIN_EYES && kind < VR_SWAPCHAIN_RESERVED);
+ CHECK(width > 0 && height > 0 && layers == (kind == VR_SWAPCHAIN_HUD ? 1 : 2));
+ CHECK(info && created_count < 3);
+ created_kinds[created_count++] = kind;
+ return created_count != creation_failure;
+}
+static int production_swapchains(struct vr_host_info *info) {
+ @@INIT@@
+ return 1;
+}
+static void check_no_optic_swapchain(void) {
+ struct vr_host_info info;
+ int depth, failure;
+ for (depth = 0; depth < 2; depth++) for (failure = 0; failure < 4; failure++) {
+ memset(&info, 0, sizeof(info));
+ info.eye_width = info.eye_height = 1200; info.hud_width = 1280; info.hud_height = 960;
+ runtime.has_depth = depth; creation_failure = failure; created_count = 0;
+ CHECK(production_swapchains(&info) == (failure != 1 && failure != 2));
+ CHECK(created_count == (failure == 1 ? 1 : failure == 2 ? 2 : 2 + depth));
+ CHECK(created_kinds[0] == VR_SWAPCHAIN_EYES);
+ if (created_count >= 2) CHECK(created_kinds[1] == VR_SWAPCHAIN_HUD);
+ if (created_count == 3) CHECK(created_kinds[2] == VR_SWAPCHAIN_DEPTH);
+ CHECK(info.image_count[VR_SWAPCHAIN_RESERVED] == 0);
+ }
+}
+
+static void head_pose(float yaw, float pitch, float roll) {
+ float turn[4] = { 0.f, sinf(yaw*.5f), 0.f, cosf(yaw*.5f) };
+ float tilt[4] = { sinf(pitch*.5f), 0.f, 0.f, cosf(pitch*.5f) };
+ float lean[4] = { 0.f, 0.f, sinf(roll*.5f), cosf(roll*.5f) };
+ float oriented[4];
+ quaternion_multiply(turn, tilt, oriented);
+ quaternion_multiply(oriented, lean, vr.views.head.orientation);
+ vr.views.head.valid = 1;
+}
+static void fixture(void) {
+ int image;
+ memset(&vr, 0, sizeof(vr)); memset(&settings, 0, sizeof(settings));
+ vr.initialized = vr.frame = vr.origin_valid = vr.aim_synced = 1;
+ vr.views.should_render = vr.views.focused = 1;
+ head_pose(0.f, 0.f, 0.f);
+ vr.views.head.position[0] = 1.f; vr.views.head.position[1] = 1.6f; vr.views.head.position[2] = 2.f;
+ vr.origin_position[0] = -500.f; vr.origin_position[1] = 20.f; vr.origin_position[2] = 700.f;
+ vr.origin_yaw = -1.3f; vr.body_yaw = 2.5f;
+ settings.standing = 1; settings.player_height = 1.7f;
+ settings.menu_distance = 1.5f; settings.menu_width = 1.8f;
+ settings.hud_distance = 1.f; settings.hud_size = 60.f;
+ memset(&runtime, 0, sizeof(runtime)); memset(&timing, 0, sizeof(timing));
+ runtime.session = (XrSession)(uintptr_t)1;
+ runtime.play_space = (XrSpace)(uintptr_t)2; runtime.view_space = (XrSpace)(uintptr_t)3;
+ runtime.frame.predictedDisplayTime = 123456;
+ for (image = 0; image < VR_SWAPCHAIN_COUNT; image++) {
+ vr.acquired[image] = 0;
+ runtime.swapchains[image] = (XrSwapchain)(uintptr_t)(image+4);
+ runtime.swapchain_width[image] = HUD_WIDTH; runtime.swapchain_height[image] = HUD_HEIGHT;
+ }
+ release_calls = 0;
+}
+static void commit_world_view(void) {
+ int eye;
+ for(eye=0;eye<2;eye++) {
+ vr.views.eye[eye]=vr.views.head;
+ vr.views.eye[eye].position[0]+=eye ? .032f : -.032f;
+ vr.views.fov[eye][0]=vr.views.fov[eye][3]=-1;
+ vr.views.fov[eye][1]=vr.views.fov[eye][2]=1;
+ }
+ (void)halo_vr_render_view_commit(1);
+}
+static void submit(int eyes, int hud) {
+ unsigned releases_before = release_calls;
+ vr.frame = 1;
+ if(eyes) commit_world_view();
+ halo_vr_present(eyes, hud);
+ runtime.frame_begun = 1;
+ host_vr_frame_end(&vr.pending_layers);
+ CHECK(!runtime.frame_begun && vr.pending && !vr.frame);
+ CHECK(release_calls == releases_before + 3); /* eyes, HUD and depth only */
+ CHECK(submitted_quads == (unsigned)vr.pending_layers.hud);
+ CHECK(submitted_projections == (unsigned)vr.pending_layers.projection);
+ if (submitted_quads) {
+ CHECK(submitted_quad.space == (vr.pending_layers.hud_head_locked ? runtime.view_space : runtime.play_space));
+ CHECK(submitted_quad.eyeVisibility == XR_EYE_VISIBILITY_BOTH);
+ CHECK(submitted_quad.subImage.swapchain == runtime.swapchains[VR_SWAPCHAIN_HUD]);
+ CHECK(close_float(submitted_quad.pose.position.x, vr.pending_layers.hud_pose.position[0]));
+ CHECK(close_float(submitted_quad.pose.position.y, vr.pending_layers.hud_pose.position[1]));
+ CHECK(close_float(submitted_quad.pose.position.z, vr.pending_layers.hud_pose.position[2]));
+ CHECK(close_float(submitted_quad.pose.orientation.x, vr.pending_layers.hud_pose.orientation[0]));
+ CHECK(close_float(submitted_quad.pose.orientation.y, vr.pending_layers.hud_pose.orientation[1]));
+ CHECK(close_float(submitted_quad.pose.orientation.z, vr.pending_layers.hud_pose.orientation[2]));
+ CHECK(close_float(submitted_quad.pose.orientation.w, vr.pending_layers.hud_pose.orientation[3]));
+ CHECK(close_float(submitted_quad.size.width, vr.pending_layers.hud_size[0]));
+ CHECK(close_float(submitted_quad.size.height, vr.pending_layers.hud_size[1]));
+ }
+}
+static void check_front(void) {
+ static const float forwards[3] = { 0.f, 0.f, -1.f };
+ static const float right[3] = { 1.f, 0.f, 0.f }, up[3] = { 0.f, 1.f, 0.f };
+ float orientation[4], pointing[3], panel_forward[3], panel_right[3], panel_up[3], distance_squared = 0.f;
+ int axis;
+ CHECK(halo_vr_locomotion_orientation(vr.views.head.orientation, orientation));
+ quaternion_rotate(orientation, forwards, pointing);
+ CHECK(vr.menu_open && vr.menu_pose.valid && vr.pending_layers.hud && !vr.pending_layers.hud_head_locked);
+ for (axis = 0; axis < 3; axis++) {
+ float offset = vr.pending_layers.hud_pose.position[axis] - vr.views.head.position[axis];
+ CHECK(close_float(offset, pointing[axis] * settings.menu_distance));
+ distance_squared += offset * offset;
+ }
+ CHECK(close_float(distance_squared, settings.menu_distance * settings.menu_distance));
+ CHECK(halo_vr_locomotion_orientation(vr.pending_layers.hud_pose.orientation, orientation));
+ quaternion_rotate(orientation, forwards, panel_forward);
+ quaternion_rotate(orientation, right, panel_right);
+ quaternion_rotate(orientation, up, panel_up);
+ for (axis = 0; axis < 3; axis++) CHECK(fabsf(panel_forward[axis]-pointing[axis]) < 0.00015f);
+ CHECK(close_float(panel_right[1], 0.f)); /* no sideways text tilt */
+ CHECK(panel_up[1] >= -0.00005f); /* never upside down */
+}
+static void check_open_angles_and_gameplay_state(void) {
+ int yaw, pitch, roll;
+ for (yaw = -180; yaw <= 180; yaw += 15) for (pitch = -90; pitch <= 90; pitch += 15)
+ for (roll = -180; roll <= 180; roll += 30) {
+ float origin[3], body;
+ fixture(); head_pose((float)yaw*(float)M_PI/180.f, (float)pitch*(float)M_PI/180.f, (float)roll*(float)M_PI/180.f);
+ memcpy(origin, vr.origin_position, sizeof(origin)); body = vr.body_yaw;
+ halo_vr_menus(1); submit(1, 1); check_front();
+ CHECK(memcmp(origin, vr.origin_position, sizeof(origin)) == 0 && vr.body_yaw == body);
+ CHECK(vr.origin_yaw == -1.3f && vr.aim_synced && vr.pending_layers.projection);
+ }
+ fixture(); settings.gamepad_aim = 1; settings.standing = 0; vr.aim_synced = vr.origin_valid = 0;
+ head_pose(2.8f, 0.4f, -0.2f); halo_vr_menus(1); submit(1, 1); check_front();
+ CHECK(!vr.origin_valid && !vr.aim_synced); /* A menu needs no gameplay camera/aim. */
+ fixture(); vr.views.head.orientation[3] = 1.01f; halo_vr_menus(1); submit(1, 1); check_front();
+}
+static void check_rolled_openings(void) {
+ int roll,pitch;
+ struct vr_host_pose upright;
+ for(pitch=-90;pitch<=90;pitch+=30) {
+ fixture(); head_pose(1.1f,(float)pitch*(float)M_PI/180.f,0.f);
+ halo_vr_menus(1); submit(1,1); check_front(); upright=vr.menu_pose;
+ for(roll=-180;roll<=180;roll+=15) {
+ int axis;
+ halo_vr_menus(0); halo_vr_menus(1);
+ head_pose(1.1f,(float)pitch*(float)M_PI/180.f,(float)roll*(float)M_PI/180.f);
+ submit(1,1); check_front();
+ for(axis=0;axis<3;axis++) CHECK(close_float(vr.menu_pose.position[axis],upright.position[axis]));
+ for(axis=0;axis<4;axis++) CHECK(close_float(vr.menu_pose.orientation[axis],upright.orientation[axis]));
+ }
+ }
+}
+static void check_stable_reopen_recentre(void) {
+ struct vr_host_pose anchor;
+ int frame;
+ fixture(); head_pose(2.4f, 0.3f, 0.1f); halo_vr_menus(1); submit(1, 1); check_front();
+ anchor = vr.menu_pose;
+ for (frame = 0; frame < 20; frame++) {
+ head_pose((float)frame*0.2f, -(float)frame*0.03f, 0.f);
+ vr.views.head.position[0] += 0.1f; vr.views.head.position[1] += 0.01f;
+ vr.views.head.position[2] -= 0.1f;
+ halo_vr_menus(1); submit(1, 1);
+ CHECK(memcmp(&anchor, &vr.menu_pose, sizeof(anchor)) == 0);
+ CHECK(memcmp(&anchor, &vr.pending_layers.hud_pose, sizeof(anchor)) == 0);
+ }
+ halo_vr_menus(0); CHECK(!vr.menu_pose.valid && !vr.menu_open);
+ halo_vr_menus(1); submit(1, 1); check_front();
+ CHECK(memcmp(&anchor, &vr.menu_pose, sizeof(anchor)) != 0);
+ /* Deliberate recenter is one replacement, not continual head locking. */
+ head_pose(-2.3f, -0.2f, 1.1f); recentre(); CHECK(!vr.menu_pose.valid);
+ submit(1, 1); check_front(); anchor = vr.menu_pose;
+ head_pose(0.f, 0.f, 0.f); submit(1, 1); CHECK(memcmp(&anchor, &vr.menu_pose, sizeof(anchor)) == 0);
+ halo_vr_menus(0); submit(1, 1);
+ CHECK(!vr.menu_open && !vr.menu_pose.valid && vr.pending_layers.hud_head_locked);
+ CHECK(close_float(submitted_quad.pose.position.z, -settings.hud_distance));
+ /* Failed eye acquisition must not reclassify the gameplay HUD as a menu. */
+ vr.acquired[VR_SWAPCHAIN_EYES] = -1; submit(1, 1);
+ CHECK(!vr.pending_layers.projection && vr.pending_layers.hud_head_locked && !vr.menu_open);
+}
+static void corrupt_pose(int corruption) {
+ switch (corruption) {
+ case 0: vr.initialized = 0; break;
+ case 1: vr.views.should_render = 0; break;
+ case 2: vr.views.focused = 0; break;
+ case 3: vr.views.head.valid = 0; break;
+ case 4: vr.views.head.position[0] = NAN; break;
+ case 5: vr.views.head.position[1] = INFINITY; break;
+ case 6: vr.views.head.position[2] = -INFINITY; break;
+ case 7: vr.views.head.orientation[0] = NAN; break;
+ case 8: vr.views.head.orientation[1] = INFINITY; break;
+ case 9: memset(vr.views.head.orientation, 0, sizeof(vr.views.head.orientation)); break;
+ case 10: vr.views.head.orientation[3] = 2.f; break;
+ case 11: settings.menu_distance = NAN; break;
+ case 12: settings.menu_distance = 0.f; break;
+ }
+}
+static void check_tracking_loss_and_deferral(void) {
+ int corruption, already_open;
+ for (already_open = 0; already_open < 2; already_open++) for (corruption = 0; corruption < 13; corruption++) {
+ fixture(); halo_vr_menus(1);
+ if (already_open) { submit(1, 1); CHECK(vr.menu_pose.valid); }
+ corrupt_pose(corruption); submit(1, 1);
+ CHECK(!vr.menu_pose.valid && !vr.pending_layers.hud && !submitted_quads);
+ vr.initialized = vr.views.should_render = vr.views.focused = 1;
+ vr.views.head.position[0] = 7.f; vr.views.head.position[1] = 1.25f; vr.views.head.position[2] = -4.f;
+ settings.menu_distance = 1.5f; head_pose(-2.9f, 0.8f, 0.3f);
+ submit(1, 1); check_front(); /* Never retain the previous wearer's pose. */
+ }
+ fixture(); halo_vr_menus(1); submit(1, 1);
+ vr.frame = 0; CHECK(!menu_place() && !vr.menu_pose.valid); /* Pending frame is stale. */
+ submit(1, 1); check_front();
+ vr.views.focused = 0; submit(0, 0); CHECK(!vr.menu_pose.valid);
+ vr.views.focused = 1; head_pose(2.6f, -0.4f, 0.f); submit(1, 1); check_front();
+ fixture(); halo_vr_menus(1); submit(1, 1);
+ vr.frame = 0; halo_vr_present(1, 1); CHECK(!vr.menu_pose.valid);
+ head_pose(-2.f, 0.2f, 0.f); submit(1, 1); check_front();
+}
+static void check_main_postgame_and_blank_frames(void) {
+ struct vr_host_pose anchor;
+ fixture(); vr.origin_valid = vr.aim_synced = 0;
+ head_pose(2.5f, -0.5f, 0.f); submit(0, 0);
+ CHECK(!vr.menu_open && !vr.menu_pose.valid); /* No capture before menu is visible. */
+ head_pose(-2.7f, 0.2f, 0.f); submit(0, 1); check_front();
+ CHECK(!vr.pending_layers.projection && submitted_projections == 0);
+ anchor = vr.menu_pose;
+ head_pose(0.f, 0.f, 0.f); submit(0, 1); CHECK(memcmp(&anchor, &vr.menu_pose, sizeof(anchor)) == 0);
+ submit(1, 1); CHECK(!vr.menu_open && vr.pending_layers.hud_head_locked);
+ /* The endgame path displays no 3D projection even before its menu flag. */
+ head_pose(2.9f, 0.5f, 0.2f); submit(0, 1); check_front();
+ CHECK(!vr.pending_layers.projection && submitted_quad.space == runtime.play_space);
+ /* An explicit close/reopen without an intermediate render is still new. */
+ halo_vr_menus(1); submit(0, 1); anchor = vr.menu_pose;
+ halo_vr_menus(0); halo_vr_menus(1); head_pose(-2.9f, -0.2f, 0.f);
+ submit(0, 1); check_front(); CHECK(memcmp(&anchor, &vr.menu_pose, sizeof(anchor)) != 0);
+ fixture(); vr.tutorial_head_valid = 1; vr.tutorial_turn_flags = 3; halo_vr_menus(7);
+ CHECK(vr.menus == 1 && !vr.tutorial_head_valid && !vr.tutorial_turn_flags);
+}
+static void check_reserved_optic_payload_ignored(void) {
+ int eyes, hud;
+ for (eyes = 0; eyes < 2; eyes++) for (hud = 0; hud < 2; hud++) {
+ fixture(); if(eyes) commit_world_view(); halo_vr_present(eyes, hud);
+ /* Even stale callers cannot cause an extra compositor layer. */
+ vr.pending_layers.reserved_optic = vr.pending_layers.reserved_optic_pose.valid = 1;
+ vr.pending_layers.reserved_optic_pose.orientation[3] = 1.f;
+ vr.pending_layers.reserved_optic_size[0] = vr.pending_layers.reserved_optic_size[1] = .06f;
+ runtime.frame_begun = 1; host_vr_frame_end(&vr.pending_layers);
+ CHECK(release_calls == 3 && !runtime.frame_begun);
+ CHECK(submitted_quads == (unsigned)hud && submitted_projections == (unsigned)eyes);
+ if (hud) CHECK(submitted_quad.subImage.swapchain == runtime.swapchains[VR_SWAPCHAIN_HUD]);
+ }
+}
+int main(void) {
+ check_no_optic_swapchain(); check_reserved_optic_payload_ignored();
+ check_open_angles_and_gameplay_state(); check_stable_reopen_recentre();
+ check_tracking_loss_and_deferral(); check_main_postgame_and_blank_frames(); check_rolled_openings();
+ printf("PASS: %u production VR menu/quad placement assertions\n", checks); return 0;
+}
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ args = parser.parse_args()
+ guest = (ROOT / 'port/linux/src/vr.c').read_text()
+ host = (ROOT / 'port/linux/arm64/host_vr.c').read_text()
+ initialization = function(host, 'host_vr_initialize')
+ begin = initialization.index('if (!swapchain_create(VR_SWAPCHAIN_EYES')
+ finish = initialization.index('info->depth = vr.has_depth && swapchain_create(', begin)
+ finish = initialization.index(';', finish) + 1
+ initialization = initialization[begin:finish]
+ assert re.findall(r'swapchain_create\((VR_SWAPCHAIN_\w+)', initialization) == [
+ 'VR_SWAPCHAIN_EYES', 'VR_SWAPCHAIN_HUD', 'VR_SWAPCHAIN_DEPTH']
+ harness = HARNESS.replace('@@GUEST@@', '\n\n'.join(function(guest, name) for name in (
+ 'quaternion_rotate', 'quaternion_yaw', 'quaternion_multiply', 'vehicle_capture', 'recentre',
+ 'render_tracking_valid', 'halo_vr_render_view_commit', 'menu_tracking_valid', 'menu_level_orientation', 'menu_place', 'halo_vr_present', 'halo_vr_menus')))
+ harness = harness.replace('@@HOST@@', re.sub(r'\bvr\b', 'runtime', '\n\n'.join(
+ function(host, name) for name in ('pose_to_xr', 'host_vr_frame_end'))))
+ harness = harness.replace('@@INIT@@', re.sub(r'\bvr\b', 'runtime', initialization))
+ compiler = args.cc or os.environ.get('CC')
+ vcvars = args.vcvarsall
+ if not compiler and os.name == 'nt':
+ installations = Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio'
+ candidates = sorted(installations.glob('*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1])
+ vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('No C11 host compiler found; select one with --cc.')
+ include_paths = [ROOT / 'port/third_party/openxr', ROOT / 'port/linux/src']
+ with tempfile.TemporaryDirectory(prefix='halo-vr-menu-', ignore_cleanup_errors=True) as directory:
+ temp = Path(directory)
+ source = temp / 'test.c'
+ executable = temp / ('test.exe' if os.name == 'nt' else 'test')
+ source.write_text(harness)
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX', '/D_CRT_SECURE_NO_WARNINGS',
+ '/Fe:' + str(executable), '/Fo:' + str(temp / 'test.obj')]
+ command.extend('/I' + str(path) for path in include_paths)
+ command.append(str(source))
+ if vcvars:
+ for path in (vcvars, Path(compiler), temp):
+ if any(character in str(path) for character in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported compiler/temp shell metacharacter')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\n'
+ 'if errorlevel 1 exit /b 1\n' + subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror']
+ command.extend('-I' + str(path) for path in include_paths)
+ command.extend([str(source), *([] if os.name == 'nt' else ['-lm']), '-o', str(executable)])
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(executable)], cwd=temp, check=True)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_pause_menu.py b/tools/test_vr_pause_menu.py
new file mode 100644
index 0000000000000000000000000000000000000000..f01d7ffa1d738c8e8f9f0fcc81120cb663a49f6e
--- /dev/null
+++ b/tools/test_vr_pause_menu.py
@@ -0,0 +1,453 @@
+#!/usr/bin/env python3
+"""Run production pause-tag construction and existing VR settings events.
+
+No game data, graphics context, headset or network is used. Validate generated
+XML/embedded art, then compile production C helpers with strict host warnings.
+The real guest ABI is checked separately by the native build.
+"""
+import argparse
+import importlib.util
+import json
+import os
+from pathlib import Path
+import re
+import shutil
+import struct
+import subprocess
+import tempfile
+import xml.etree.ElementTree as ET
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def block(text, start):
+ masked = re.sub(r'/\*.*?\*/|//[^\n]*|"(?:\\.|[^"\\])*"',
+ lambda m: re.sub(r'[^\n]', ' ', m.group()), text, flags=re.S)
+ opening = masked.index('{', start)
+ depth, end = 1, opening + 1
+ while depth:
+ depth += (masked[end] == '{') - (masked[end] == '}')
+ end += 1
+ return text[start:end]
+
+
+def function(text, name):
+ match = re.search(r'(?:static\s+)?(?:struct\s+widget_instance\s*\*|boolean|void\s*\*|void|short|int|long)\s*' +
+ re.escape(name) + r'\s*\([^;]*?\)\s*\{', text)
+ if not match:
+ raise AssertionError('Missing production function: ' + name)
+ return block(text, match.start())
+
+
+def inputs():
+ spec = importlib.util.spec_from_file_location('pause_settings_generator', ROOT / 'tools/port_settings.py')
+ generator = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(generator)
+ widgets = {}
+ for page in ('vr_controls', 'vr_display'):
+ name = f'{generator.PE}/{page}'.replace('/', '.') + '.xml'
+ path = ROOT / 'port/assets/menus/ce' / name
+ assert path.read_text() == '\n'.join(generator.settings_files()[name]), name
+ widgets.update({node.attrib['name']: node for node in ET.parse(path).getroot() if node.tag == 'widget'})
+ controls = widgets[f'{generator.PE}/vr_controls/vr_controls_screen']
+ display = widgets[generator.VR_DISPLAY]
+ for screen in (controls, display):
+ assert 'pause_game' in screen.attrib['flags'].split()
+ assert {node.attrib['event'] for node in screen if node.tag == 'on'} == {'b', 'back'}
+ assert all(node.attrib.get('back') == 'true' for node in screen if node.tag == 'on')
+ link = widgets[f'{generator.PE}/vr_controls/op_vr_display_screen']
+ for button in ('a', 'start'):
+ handler = next(node for node in link if node.attrib.get('event') == button)
+ assert handler.attrib['run'] == 'port settings save' and handler.attrib['open'] == generator.VR_DISPLAY
+ for page in ('vr_controls', 'vr_display'):
+ base = f'{generator.PE}/{page}'
+ for button in ('a', 'start'):
+ ok = next(node for node in widgets[base + '/button_ok'] if node.attrib.get('event') == button)
+ defaults = next(node for node in widgets[base + '/button_defaults'] if node.attrib.get('event') == button)
+ assert ok.attrib['run'] == 'port settings save' and ok.attrib['back'] == 'true'
+ assert defaults.attrib['run'] == 'port settings defaults'
+ options = widgets[base + '/options_menu']
+ rows = [node for node in options if node.tag == 'child' and '/op_' in node.attrib['widget']]
+ help_top = int(widgets[base + '/help'].attrib['top'])
+ assert len(rows) == 10
+ for row in rows:
+ top = int(row.attrib['y'])
+ option = widgets[row.attrib['widget']]
+ # The existing 28-pixel row background overlaps the help area by
+ # three pixels; its label/spinner content stays above the help.
+ assert top + int(option.attrib['height']) <= help_top + 3
+ for child in option:
+ if child.tag == 'child':
+ content = widgets[child.attrib['widget']]
+ assert top + int(child.attrib.get('y', 0)) + int(content.attrib['height']) <= help_top
+ buffer = widgets[f'{generator.PE}/vr_display/persistent_stream_buffers_spinner']
+ assert buffer.attrib['setting'] == 'display.persistent_stream_buffers'
+ assert buffer.attrib['values'] == 'true|false' and buffer.attrib['strings'] == 'ON|OFF'
+ assert not any(node.attrib.get('setting', '').startswith('profile.') for node in widgets.values())
+ settings = {node.attrib['setting'] for node in widgets.values() if 'setting' in node.attrib}
+ assert {'vr.aim', 'vr.hands', 'vr.two_handed', 'vr.turn', 'vr.height', 'vr.player_height',
+ 'vr.gamepad_view', 'vr.vehicle_view', 'vr.snap_turn_angle', 'vr.smooth_turn_speed',
+ 'vr.hud_distance', 'vr.hud_size', 'vr.menu_distance', 'vr.menu_width'} <= settings
+ manifest = json.loads((ROOT / 'port/assets/menus/menus.json').read_text())['files']
+ bitmaps = ET.parse(ROOT / 'port/assets/menus/ce/bitmaps.xml').getroot()
+ for piece in ('left', 'center', 'right'):
+ bitmap = next(node for node in bitmaps if node.attrib.get('name') == 'pause/pausebox_' + piece)
+ assert len(bitmap) == 3 and generator.PAUSE_BOX_BUTTONS == [3, 4, 5]
+ for index, buttons in enumerate(generator.PAUSE_BOX_BUTTONS):
+ frame = bitmap[index]
+ assert frame.attrib['png'] in manifest
+ assert int(frame.attrib['height']) == generator.pause_box_texture_height(buttons)
+ data = (ROOT / 'port/assets/menus' / frame.attrib['png']).read_bytes()
+ width, height, depth, colour = struct.unpack('>IIBB', data[16:26])
+ assert (width, height) == (int(frame.attrib['width']) * 4, int(frame.attrib['height']) * 4)
+ assert (depth, colour) == (8, 6)
+ assert generator.pause_box_height(buttons) <= int(frame.attrib['height'])
+ svg = ROOT / 'port/assets/menus/port_svg/pause' / ('pausebox_' + piece + '__2.svg')
+ assert svg.read_text() == generator.pause_box_svg(piece, generator.pause_box_height(5))
+ tags = (ROOT / 'port/linux/game/menu_tags.c').read_text()
+ assert '#define PAUSE_VR_SCREEN "' + controls.attrib['name'] + '"' in tags
+ loaded = function(tags, 'menu_tags_loaded')
+ assert 'halo_vr_running() && tag_loaded(UI_WIDGET_DEFINITION_TAG, SOLO_PAUSE_SCREEN) != NONE' in loaded
+ assert '!multiplayer_map && !vr_campaign_map' in loaded
+ assert 'build.multiplayer_map = game_map && multiplayer_map;' in loaded
+ build = function(tags, 'widget_build')
+ pause_clear = block(build, build.index('if (build.multiplayer_map)')) if '{' in build[build.index('if (build.multiplayer_map)'):][:70] else \
+ re.search(r'if \(build.multiplayer_map\)\s*definition->flags &= ~FLAG\(1\);', build).group()
+ widget = (ROOT / 'source/interface/ui_widget.c').read_text()
+ initialize = function(widget, 'widget_instance_initialize')
+ pause_set = re.search(r'widget->pause_game_time = TEST_FLAG\(definition->flags, _widget_pause_game_time_bit\) &&\s*!network_coop_active\(\);', initialize).group()
+ dispatch = function(widget, 'event_handler_dispatch')
+ open_event = block(dispatch, dispatch.index('if (TEST_FLAG(handler->flags, _event_handler_open_widget_bit)'))
+ menu = (ROOT / 'port/linux/game/menu_functions.c').read_text()
+ helpers = '\n\n'.join(function(tags, name) for name in (
+ 'reference_clear', 'reference_set', 'next_tag', 'instance_set', 'ui_function_named',
+ 'tag_name_ends', 'pause_quit_button', 'pause_button', 'pause_list_patch', 'pause_box_stock',
+ 'pause_box_redraw', 'pause_vr_widget', 'pause_screen_patch', 'pause_patch',
+ 'menu_tags_release', 'menu_tags_unloaded'))
+ helpers += '\n\n' + '\n\n'.join(function(menu, name) for name in (
+ 'settings_each', 'screen_of', 'setting_changed_save'))
+ structures = tags[tags.index('struct cache_file_tag_instance\n'):tags.index('/* a spinner bound')]
+ structures = re.sub(r'typedef char verify_.*?;', '', structures, flags=re.S)
+ return helpers, structures, pause_clear, pause_set, open_event
+
+
+HARNESS = r'''
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+typedef int boolean;typedef unsigned char byte;typedef unsigned short word;
+typedef struct {short y0,x0,y1,x1;} rectangle2d;
+typedef struct {float a,r,g,b;} real_argb_color;
+struct tag_reference {long group_tag;char *name;long name_length,index;};
+struct tag_block {long count;void *address;};
+#define TRUE 1
+#define FALSE 0
+#define NONE (-1)
+#define FLAG(n) (1L<<(n))
+#define TEST_FLAG(f,n) (((f)&FLAG(n))!=0)
+#define NUMBEROF(a) (sizeof(a)/sizeof((a)[0]))
+#define DATUM_INDEX_TO_ABSOLUTE_INDEX(i) ((i)&0xFFFF)
+#define PIN(x,a,b) ((x)<(a)?(a):(x)>(b)?(b):(x))
+#define UI_WIDGET_DEFINITION_TAG 0x44654C61
+#define UNICODE_STRING_LIST_TAG 0x75737472
+#define BITMAP_GROUP_TAG 0x6269746D
+#define SOUND_TAG 0x736E6421
+#define PC_MENU_TAG_PREFIX "pc\\"
+#define MULTIPLAYER_COLLECTION "ui\\shell\\multiplayer"
+#define SOLO_PAUSE_SCREEN "ui\\shell\\solo_game\\pause_game\\pause_game"
+#define PAUSE_SETTINGS_SCREEN "main_menu/settings_select/player_setup/player_profile_edit/player_profile_edit_screen"
+#define PAUSE_VR_SCREEN "main_menu/settings_select/player_setup/player_profile_edit/vr_controls/vr_controls_screen"
+#define PAUSE_BUTTON_SPACING 35
+#define PAUSE_BOX_FIRST_BUTTONS 3
+enum {_event_handler_open_widget_bit=3,_event_handler_run_function_bit=7,
+ _event_handler_try_to_branch_on_failure_bit=11};
+enum {_widget_type_column_list=3,_widget_pause_game_time_bit=1,_ui_audio_feedback_none=0,_ui_audio_feedback_forward=1};
+@@STRUCTURES@@
+struct bitmap_data {short height;void *hardware_format;};
+struct bitmap_group_sequence {short first_bitmap_index,bitmap_count;};
+struct bitmap_group {struct tag_block sequences,bitmaps;};
+struct named_data {char const *name;};
+struct halo_menus {long widget_count,bitmap_count;struct named_data *widgets,*bitmaps;};
+static struct {struct halo_menus const *menus;long first_salt,first_index,next;long *widget_tags,*bitmap_tags;
+ boolean failed,game_map,multiplayer_map;} build;
+static struct {boolean loaded;long block_count,bitmap_count,original_count;void **blocks;
+ struct bitmap_data **bitmaps;void *settings,*original_instances;} menu_tags;
+static struct cache_file_tag_instance instances[256];static struct ui_widget_definition definitions[40];
+static struct ui_widget_child_reference mp_children[2],solo_children[4],screen_children[2][3],second_screen_children[3],pieces[3];
+static struct ui_widget_event_handler_reference quit_handlers[2];
+static struct tag_reference collection_refs[2];static struct tag_block collection;
+static struct bitmap_group groups[3];static struct bitmap_group_sequence sequences[3];
+static struct bitmap_data bitmap_data[3][3];
+static struct named_data generated_widgets[2]={{PAUSE_SETTINGS_SCREEN},{PAUSE_VR_SCREEN}};
+static struct named_data generated_bitmaps[3]={{"pause/pausebox_left"},{"pause/pausebox_center"},{"pause/pausebox_right"}};
+static struct halo_menus menus={2,3,generated_widgets,generated_bitmaps};
+static long widget_tags[2]={4,5},bitmap_tags[3]={20,21,22};
+static int checks,vr_running,mp_map,solo_map,hosting,fail_at,allocations,table_restores,coop;
+static void *current_instances;
+#define CHECK(c) do {checks++;if(!(c)){fprintf(stderr,"FAIL line %d: %s\n",__LINE__,#c);exit(1);}}while(0)
+static void platform_log(char const *format,...) {(void)format;}
+static int halo_vr_running(void) {return vr_running;}
+static void *global_network_game_server_get(void) {return hosting?&hosting:NULL;}
+static long tag_loaded(long group,char const *name) {
+ if(group==0x536F756C&&!strcmp(name,MULTIPLAYER_COLLECTION))return mp_map?30:NONE;
+ if(group==UI_WIDGET_DEFINITION_TAG&&!strcmp(name,SOLO_PAUSE_SCREEN))return solo_map?10:NONE;return NONE;
+}
+static void *tag_get(long group,long index) {
+ CHECK(index!=NONE&&DATUM_INDEX_TO_ABSOLUTE_INDEX(index)<256);if(group==0x536F756C)return &collection;
+ CHECK(group==UI_WIDGET_DEFINITION_TAG);return instances[DATUM_INDEX_TO_ABSOLUTE_INDEX(index)].base_address;
+}
+static char *tag_get_name(long index) {return instances[DATUM_INDEX_TO_ABSOLUTE_INDEX(index)].name;}
+static char const *ui_widget_event_handler_function_name(long index) {
+ static char const *const names[]={"mp game player quit","pause game return to main menu"};
+ return index>=0&&index<2?names[index]:NULL;
+}
+static long widget_named(char const *name) {long n;for(n=0;n<2;n++)if(!strcmp(name,generated_widgets[n].name))return n;return NONE;}
+static char const *bitmap_name(long n) {return generated_bitmaps[n].name;}
+static long find(char const *name,long count,char const *(*name_of)(long)) {long n;for(n=0;n<count;n++)if(!strcmp(name,name_of(n)))return n;return NONE;}
+static long function_index(char const *name,char const *file,long line) {(void)file;(void)line;return !strcmp(name,"profile set edit begin")?500:501;}
+static void *allocate(long size) {
+ void *p;if(fail_at>=0&&allocations++==fail_at){build.failed=TRUE;return NULL;}
+ p=calloc(1,(size_t)(size>0?size:1));CHECK(p!=NULL);
+ menu_tags.blocks=realloc(menu_tags.blocks,(size_t)(menu_tags.block_count+1)*sizeof(void*));CHECK(menu_tags.blocks!=NULL);
+ menu_tags.blocks[menu_tags.block_count++]=p;return p;
+}
+static void *string_list_build(char const *const *texts,long count) {
+ char *p;CHECK(count==1);p=allocate((long)strlen(texts[0])+1);if(p)memcpy(p,texts[0],strlen(texts[0])+1);return p;
+}
+static struct bitmap_group *bitmap_group_get(long index) {CHECK(index>=20&&index<=22);return &groups[index-20];}
+static void rasterizer_bitmap_delete(struct bitmap_data *data) {(void)data;CHECK(0);}
+static void halo_menus_art_forget(void) {}
+static void cache_files_set_tag_instances(void *table,long count) {CHECK(count==32);current_instances=table;table_restores++;}
+struct widget_instance {struct widget_instance *child,*next,*parent;long type,definition_tag_index;
+ struct {struct {short selected_index;} list;} parameters;boolean pause_game_time;};
+struct pc_menu_setting {char const *setting;long value_count;char const *values[2];short loaded_index;};
+static struct pc_menu_setting saved_setting;
+static int writes;static char saved_key[100],saved_value[40];
+static boolean spinner_item(struct widget_instance *widget) {(void)widget;return FALSE;}
+static struct pc_menu_setting *pc_menu_setting_get(long tag) {return tag==7?&saved_setting:NULL;}
+static boolean setting_write(char const *name,char const *value) {
+ writes++;snprintf(saved_key,sizeof(saved_key),"%s",name);snprintf(saved_value,sizeof(saved_value),"%s",value);return TRUE;
+}
+static boolean network_coop_active(void) {return coop;}
+static int launch_calls;static long launched;
+static boolean ui_widget_launch_widget(struct widget_instance *widget,long tag) {(void)widget;launch_calls++;launched=tag;return TRUE;}
+static void error(int kind,char const *message) {(void)kind;(void)message;CHECK(0);}
+#define _error_silent 0
+@@HELPERS@@
+static void open_event(struct widget_instance *widget,struct ui_widget_event_handler_reference *handler) {
+ boolean widget_deleted=FALSE,success=TRUE;long audio_feedback=_ui_audio_feedback_none;
+ @@OPEN_EVENT@@
+ CHECK(success);CHECK(widget_deleted);CHECK(audio_feedback==_ui_audio_feedback_forward);
+}
+static void pause_flags(struct ui_widget_definition *definition,struct widget_instance *widget) {
+ @@PAUSE_CLEAR@@
+ @@PAUSE_SET@@
+}
+static void reset_state(int mp,int solo,int host,int running) {
+ long n;menu_tags_unloaded();memset(instances,0,sizeof(instances));memset(definitions,0,sizeof(definitions));
+ memset(mp_children,0,sizeof(mp_children));memset(solo_children,0,sizeof(solo_children));memset(screen_children,0,sizeof(screen_children));
+ memset(&build,0,sizeof(build));memset(&menu_tags,0,sizeof(menu_tags));
+ for(n=0;n<40;n++){instances[n].base_address=&definitions[n];instances[n].name="stock";}
+ mp_map=mp;solo_map=solo;hosting=host;vr_running=running;fail_at=-1;allocations=0;table_restores=0;coop=0;
+ build.menus=&menus;build.widget_tags=widget_tags;build.bitmap_tags=bitmap_tags;build.first_index=64;build.first_salt=1;
+ menu_tags.original_instances=definitions;menu_tags.original_count=32;current_instances=instances;
+ quit_handlers[0]=(struct ui_widget_event_handler_reference){0};quit_handlers[1]=quit_handlers[0];
+ quit_handlers[0].flags=quit_handlers[1].flags=FLAG(_event_handler_run_function_bit);
+ quit_handlers[0].function=0;quit_handlers[1].function=1;
+ for(n=0;n<2;n++){definitions[11+n].type=_widget_type_column_list;definitions[11+n].bounds.y1=200;}
+ definitions[11].child_widgets=(struct tag_block){2,mp_children};definitions[12].child_widgets=(struct tag_block){4,solo_children};
+ for(n=0;n<2;n++){mp_children[n].widget_tag.index=2+n;mp_children[n].vertical_offset=(short)(n*35);}
+ for(n=0;n<4;n++){solo_children[n].widget_tag.index=13+n;solo_children[n].vertical_offset=(short)(n*28);}
+ definitions[3].event_handlers=(struct tag_block){1,&quit_handlers[0]};definitions[16].event_handlers=(struct tag_block){1,&quit_handlers[1]};
+ for(n=0;n<2;n++){screen_children[n][0].widget_tag.index=11+n;screen_children[n][0].vertical_offset=100;
+ screen_children[n][1].widget_tag.index=19;screen_children[n][1].vertical_offset=100;
+ screen_children[n][2].widget_tag.index=18;screen_children[n][2].vertical_offset=300;}
+ definitions[9].child_widgets=(struct tag_block){3,screen_children[0]};definitions[10].child_widgets=(struct tag_block){3,screen_children[1]};
+ memcpy(second_screen_children,screen_children[0],sizeof(second_screen_children));
+ definitions[17].child_widgets=(struct tag_block){3,second_screen_children};
+ collection_refs[0].index=9;collection_refs[1].index=17;collection=(struct tag_block){2,collection_refs};
+ definitions[19].child_widgets=(struct tag_block){3,pieces};
+ for(n=0;n<3;n++){pieces[n].widget_tag.index=23+n;groups[n].sequences=(struct tag_block){1,&sequences[n]};
+ groups[n].bitmaps=(struct tag_block){3,bitmap_data[n]};bitmap_data[n][0].height=256;bitmap_data[n][1].height=256;bitmap_data[n][2].height=512;}
+ instances[23].name="ui\\pausebox2_left";instances[24].name="ui\\pausebox2_left_center";instances[25].name="ui\\pausebox2_right";
+}
+static struct ui_widget_definition *added_button(long list_tag,long at) {
+ struct ui_widget_child_reference *children=definitions[list_tag].child_widgets.address;
+ return tag_get(UI_WIDGET_DEFINITION_TAG,children[at].widget_tag.index);
+}
+static void verify_vr_button(struct ui_widget_definition *button) {
+ struct ui_widget_event_handler_reference *handlers=button->event_handlers.address;
+ struct widget_instance widget={0};long n;CHECK(button->event_handlers.count==2);CHECK(button->game_data_inputs.count==0);
+ for(n=0;n<2;n++){CHECK(handlers[n].event_type==(n?12:0));CHECK(handlers[n].function==NONE);
+ CHECK(handlers[n].flags==FLAG(_event_handler_open_widget_bit));CHECK(handlers[n].widget_tag.index==5);
+ CHECK(handlers[n].sound_effect.index==NONE);launch_calls=0;open_event(&widget,&handlers[n]);CHECK(launch_calls==1&&launched==5);}
+}
+static void test_routes(void) {
+ int mp,host,running;for(mp=0;mp<2;mp++)for(host=0;host<2;host++)for(running=0;running<2;running++){
+ int active=0;reset_state(mp,!mp,host,running);
+#ifdef HALO_VR
+ active=running;
+#endif
+ pause_patch(instances);CHECK(!build.failed);
+ CHECK(build.next<=6);
+ if(mp){CHECK(definitions[11].child_widgets.count==2+(host?2:1)+active);
+ CHECK(((struct ui_widget_event_handler_reference*)added_button(11,1)->event_handlers.address)[0].function==500);
+ if(host)CHECK(((struct ui_widget_event_handler_reference*)added_button(11,2)->event_handlers.address)[0].function==501);
+ if(active)verify_vr_button(added_button(11,host?3:2));
+ CHECK(sequences[0].first_bitmap_index==definitions[11].child_widgets.count-3);
+ CHECK(definitions[23].bounds.y1==(host&&active?512:256));
+ CHECK(second_screen_children[0].vertical_offset==screen_children[0][0].vertical_offset);
+ }else{CHECK(definitions[12].child_widgets.count==4+active);if(active)verify_vr_button(added_button(12,3));
+ CHECK(definitions[16].event_handlers.address==&quit_handlers[1]);CHECK(quit_handlers[1].function==1);}
+ }
+ reset_state(TRUE,TRUE,TRUE,TRUE);pause_patch(instances);CHECK(definitions[12].child_widgets.count==4);
+#ifdef HALO_VR
+ reset_state(FALSE,TRUE,FALSE,TRUE);
+ instances[23].name="ui\\pausebox5_left";instances[24].name="ui\\pausebox5_left_center";instances[25].name="ui\\pausebox5_right";
+ pause_patch(instances);CHECK(definitions[12].child_widgets.count==5);CHECK(sequences[0].first_bitmap_index==2);
+ CHECK(definitions[23].bounds.y1==512);verify_vr_button(added_button(12,3));
+#endif
+}
+static void test_allocation_failure(void) {
+#ifdef HALO_VR
+ int mode,at;for(mode=0;mode<2;mode++)for(at=0;at<21;at++){
+ long original_count;void *original;reset_state(mode,!mode,TRUE,TRUE);original=definitions[mode?11:12].child_widgets.address;
+ original_count=definitions[mode?11:12].child_widgets.count;fail_at=at;pause_patch(instances);
+ CHECK(build.next<=6);
+ if(build.failed){CHECK(definitions[mode?11:12].child_widgets.address==original);
+ CHECK(definitions[mode?11:12].child_widgets.count==original_count);}
+ if(build.failed)menu_tags_release();else{menu_tags.loaded=TRUE;menu_tags_unloaded();}
+ CHECK(current_instances==definitions);CHECK(table_restores==1);CHECK(menu_tags.block_count==0);
+ }
+#endif
+}
+static void test_pause_and_saving(void) {
+ struct ui_widget_definition definition={0};struct widget_instance screen={0},button={0},row={0},spinner={0};int mp,network;
+ for(mp=0;mp<2;mp++)for(network=0;network<2;network++){
+ build.multiplayer_map=mp;coop=network;definition.flags=FLAG(_widget_pause_game_time_bit);pause_flags(&definition,&screen);
+ CHECK(screen.pause_game_time==(!mp&&!network));}
+ screen.child=&row;row.parent=&screen;row.child=&spinner;spinner.parent=&row;spinner.type=2;spinner.definition_tag_index=7;
+ button.parent=&screen;spinner.parameters.list.selected_index=1;
+ saved_setting=(struct pc_menu_setting){"vr.turn",2,{"snap","smooth"},0};writes=0;
+ settings_each(screen_of(&button),setting_changed_save);CHECK(writes==1);CHECK(!strcmp(saved_key,"vr.turn"));CHECK(!strcmp(saved_value,"smooth"));
+ settings_each(screen_of(&button),setting_changed_save);CHECK(writes==1);
+ saved_setting=(struct pc_menu_setting){"display.persistent_stream_buffers",2,{"true","false"},1};spinner.parameters.list.selected_index=0;
+ settings_each(screen_of(&button),setting_changed_save);CHECK(writes==2);CHECK(!strcmp(saved_key,"display.persistent_stream_buffers"));CHECK(!strcmp(saved_value,"true"));
+ spinner.parameters.list.selected_index=9;settings_each(screen_of(&button),setting_changed_save);CHECK(writes==2);
+}
+@@VRHUD@@
+int main(void){test_routes();test_allocation_failure();test_pause_and_saving();menu_tags_unloaded();
+#ifdef HALO_VR
+ test_menu_hud_visibility();
+#endif
+ printf("PASS: %d production pause-tag, event, allocation/release, save and co-op-clock assertions (%s)\n",checks,
+#ifdef HALO_VR
+ "VR"
+#else
+ "flat"
+#endif
+ );return 0;}
+'''
+
+VRHUD_HARNESS = r'''
+#ifdef HALO_VR
+enum {MAXIMUM_NUMBER_OF_LOCAL_PLAYERS=4,_render_target_primary=0};
+struct render_camera {int identity;rectangle2d viewport_bounds;};
+struct render_window {struct render_camera rasterizer_camera;short local_player_index;};
+struct rasterizer_window_begin_parameters {struct render_camera camera;int frustum,rasterizer_target;boolean suppress_clear;int window_index;};
+static struct {struct render_camera camera;int frustum,window_index;short local_player_index;} render;
+static boolean hud_frame_active,hud_widgets[4],hud_video_active;
+static int hud_profile_begin,hud_profile_end,hud_begin,hud_end,hud_gameplay,hud_ui,hud_cursor,hud_offset_begin,hud_offset_end;
+static boolean halo_vr_frame_active(void){return hud_frame_active;}
+static boolean ui_widgets_active_for_local_player(short local){CHECK(local>=0&&local<4);return hud_widgets[local];}
+static void profile_render_window_start(boolean mirror){CHECK(!mirror);hud_profile_begin++;}
+static void profile_render_window_end(void){hud_profile_end++;}
+static boolean vr_render_hud_camera(struct render_camera *camera){CHECK(camera);return TRUE;}
+static void render_camera_build_frustum(struct render_camera const *camera,void const *bounds,int *frustum,boolean clip){
+ CHECK(camera&&bounds==NULL&&clip);*frustum=camera->identity+10;}
+static void rasterizer_window_begin(struct rasterizer_window_begin_parameters const *parameters){
+ CHECK(parameters->suppress_clear&&parameters->rasterizer_target==_render_target_primary);
+ CHECK(parameters->window_index==6&&parameters->frustum==parameters->camera.identity+10);hud_begin++;}
+static void rasterizer_window_end(void){hud_end++;}
+static boolean bink_playback_in_progress(void){return hud_video_active;}
+/* The production interface call contains both stock zoom masks/effects and
+ * gameplay HUD. The actual UI call also carries the controller cursor. */
+static void interface_draw_screen(void){hud_gameplay++;}
+static void halo_screen_ui_offset(boolean active){if(active)hud_offset_begin++;else hud_offset_end++;}
+static void render_ui_widgets(short local,rectangle2d const *bounds){CHECK(local==render.local_player_index);CHECK(bounds);hud_ui++;hud_cursor++;}
+@@HUDVISIBLE@@
+@@HUDRENDER@@
+static void test_menu_hud_visibility(void){
+ int frame,menu,video,local;
+ for(frame=0;frame<2;frame++)for(menu=0;menu<2;menu++)for(video=0;video<2;video++)for(local=0;local<4;local++){
+ struct render_window window={{77,{0,0,480,640}},(short)local};
+ memset(hud_widgets,0,sizeof(hud_widgets));hud_widgets[local]=menu;
+ hud_frame_active=frame;hud_video_active=video;render.window_index=6;
+ hud_profile_begin=hud_profile_end=hud_begin=hud_end=hud_gameplay=hud_ui=hud_cursor=hud_offset_begin=hud_offset_end=0;
+ render_vr_hud(&window);
+ CHECK(hud_profile_begin==1&&hud_profile_end==1&&hud_begin==1&&hud_end==1);
+ CHECK(render.camera.identity==77&&render.local_player_index==local);
+ CHECK(hud_gameplay==(!video&&!(frame&&menu)));
+ CHECK(hud_ui==!video&&hud_cursor==!video);
+ CHECK(hud_offset_begin==!video&&hud_offset_end==!video);
+ }
+}
+#endif
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ args = parser.parse_args()
+ helpers, structures, pause_clear, pause_set, open_event = inputs()
+ # Fixture globals must not shadow the names in extracted production code.
+ fixture = re.sub(r'\b(instances|pieces|collection)\b', lambda m: 'fixture_' + m.group(), HARNESS)
+ source = fixture.replace('@@HELPERS@@', helpers).replace('@@STRUCTURES@@', structures).replace(
+ '@@PAUSE_CLEAR@@', pause_clear).replace('@@PAUSE_SET@@', pause_set).replace('@@OPEN_EVENT@@', open_event)
+ vr_render = (ROOT / 'port/linux/game/vr_render.c').read_text()
+ render = (ROOT / 'source/render/render.c').read_text()
+ hud = VRHUD_HARNESS.replace('@@HUDVISIBLE@@', function(vr_render, 'vr_render_gameplay_hud_visible')).replace(
+ '@@HUDRENDER@@', function(render, 'render_vr_hud'))
+ source = source.replace('@@VRHUD@@', hud)
+ compiler, vcvars = args.cc or os.environ.get('CC'), args.vcvarsall
+ if not compiler and os.name == 'nt':
+ candidates = sorted((Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio').glob(
+ '*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1])
+ vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('No host C compiler; select --cc.')
+ with tempfile.TemporaryDirectory(prefix='halo-vr-pause-', ignore_cleanup_errors=True) as directory:
+ temp = Path(directory)
+ cfile = temp / 'test.c'
+ cfile.write_text(source, encoding='utf-8')
+ for vr in (True, False):
+ name = 'test-vr' if vr else 'test-flat'
+ executable = temp / (name + ('.exe' if os.name == 'nt' else ''))
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX', '/wd4505', '/D_CRT_SECURE_NO_WARNINGS',
+ *(['/DHALO_VR'] if vr else []), str(cfile), '/Fe:' + str(executable), '/Fo:' + str(temp / (name + '.obj'))]
+ if vcvars:
+ for path in (vcvars, Path(compiler), temp):
+ if any(c in str(path) for c in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported compiler/temp path shell metacharacter.')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\nif errorlevel 1 exit /b 1\n' +
+ subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-Wno-unused-function',
+ '-Wno-multichar', '-D_CRT_SECURE_NO_WARNINGS', *(['-DHALO_VR'] if vr else []),
+ str(cfile), '-o', str(executable)]
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(executable)], cwd=temp, check=True)
+ print('PASS: existing two-page VR Setup XML, bounded embedding art and direct pause access')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_pointer_radar.py b/tools/test_vr_pointer_radar.py
new file mode 100644
index 0000000000000000000000000000000000000000..0ec619313900e4ec1801c9ba987b021f159f5827
--- /dev/null
+++ b/tools/test_vr_pointer_radar.py
@@ -0,0 +1,464 @@
+#!/usr/bin/env python3
+"""Run the production menu ray/button and local HMD radar code on a C11 host.
+
+No game data, SDL, headset or OpenXR runtime is required. Runtime/game services
+are mocked; geometry, pose validation, input merge, current gaze, radar render
+copy and cursor drawing are extracted unchanged from the production sources.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+
+from test_vr_locomotion import masked
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def function(text, name):
+ match = re.search(r'(?:static\s+)?(?:void|float|int|boolean)\s+' + re.escape(name) +
+ r'\s*\([^;]*?\)\s*\{', text)
+ if not match:
+ raise AssertionError('Missing production function: ' + name)
+ code = masked(text)
+ depth = 1
+ end = code.index('{', match.start()) + 1
+ while depth:
+ depth += (code[end] == '{') - (code[end] == '}')
+ end += 1
+ return text[match.start():end]
+
+
+HARNESS = r'''
+#define _USE_MATH_DEFINES
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <float.h>
+#include "vr.h"
+#include "vr_vehicle.h"
+#include "vr_host.h"
+#include "vr_locomotion.h"
+#include "halo_ui_pointer.h"
+#ifndef M_PI
+#define M_PI 3.14159265358979323846
+#endif
+#define NONE (-1)
+#define TRUE 1
+#define FALSE 0
+#define MAX(a,b) ((a) > (b) ? (a) : (b))
+#define MIN(a,b) ((a) < (b) ? (a) : (b))
+#define TEST_FLAG(value,bit) ((value) & (1u << (bit)))
+typedef int boolean;
+typedef float real;
+typedef unsigned char BYTE;
+typedef short SHORT;
+typedef struct { real x,y,z; } real_point3d;
+typedef struct { real i,j,k; } real_vector3d;
+typedef struct { real pitch; } real_euler_angles2d;
+typedef struct { short x0,y0,x1,y1; } rectangle2d;
+typedef struct { short x,y; } point2d;
+typedef struct { real x,y; } real_point2d;
+enum { XINPUT_GAMEPAD_A, XINPUT_GAMEPAD_B, XINPUT_GAMEPAD_X, XINPUT_GAMEPAD_Y,
+ XINPUT_GAMEPAD_BLACK, XINPUT_GAMEPAD_WHITE, XINPUT_GAMEPAD_LEFT_TRIGGER, XINPUT_GAMEPAD_RIGHT_TRIGGER };
+enum { XINPUT_GAMEPAD_DPAD_UP=1, XINPUT_GAMEPAD_DPAD_DOWN=2, XINPUT_GAMEPAD_DPAD_LEFT=4,
+ XINPUT_GAMEPAD_DPAD_RIGHT=8, XINPUT_GAMEPAD_START=16, XINPUT_GAMEPAD_BACK=32,
+ XINPUT_GAMEPAD_LEFT_THUMB=64, XINPUT_GAMEPAD_RIGHT_THUMB=128 };
+typedef struct { BYTE bAnalogButtons[8]; unsigned short wButtons; SHORT sThumbLX,sThumbLY,sThumbRX,sThumbRY; } XINPUT_GAMEPAD;
+static unsigned checks;
+#define CHECK(c) do { checks++; if (!(c)) { fprintf(stderr,"failed line %d: %s\n",__LINE__,#c); exit(1); } } while(0)
+static int near_float(float a,float b) { return fabsf(a-b) < 0.0001f; }
+static struct {
+ int initialized,frame,pending,menus,menu_open,tutorial_turn_flags,tutorial_head_valid;
+ int origin_valid,aim_synced,aiming,aimed,melee_frames;
+ float origin_yaw,body_yaw,origin_position[3];
+ struct vr_host_views views;
+ struct vr_host_input input;
+ struct vr_host_pose menu_pose;
+ struct vr_host_layers pending_layers;
+} vr;
+static struct { int menu_pointer,radar_head_heading,gamepad_aim; float menu_distance; } settings;
+static struct { int active,armed,down,blocked,position_valid; short x,y; } menu_pointer;
+static void driving_reset(int release_required) { (void)release_required; }
+static struct { int mode, accelerator_blocked, accelerator_consumed, brake_consumed; } driving;
+static int driving_active(void) { return 0; }
+@@GUEST@@
+
+/* Only unrelated game/runtime services are mocked for the production gaze. */
+enum { _object_type_biped, _object_dead_bit=1 };
+typedef enum { _director_perspective_first_person, _director_perspective_scripted,
+ _director_perspective_neutral } director_perspective;
+struct unit_datum { struct { int type; unsigned damage_flags; } object; struct { long player_index; } unit; } unit;
+struct player_datum { long unit_index; } player;
+struct observer_result { real_point3d position; real_vector3d forward; } observer;
+static struct halo_vr_view current_view;
+static int cinematic,scripted,view_available,observer_available,inhibited;
+static int *director_camera_scripted=&scripted;
+static long local_player_index=2;
+static director_perspective perspective;
+static real_euler_angles2d facing;
+static unsigned view_reads;
+static long local_player_get_player_index(short index) { CHECK(index==0); return local_player_index; }
+static struct player_datum *player_get(long index) { CHECK(index==local_player_index); return &player; }
+static struct unit_datum *unit_try_and_get(long index) { return index==7 ? &unit : NULL; }
+static int cinematic_in_progress(void) { return cinematic; }
+static director_perspective director_get_perspective(short index) { CHECK(index>=0 && index<4); return perspective; }
+static int director_inhibited_facing(short index) { CHECK(index==0); return inhibited; }
+int halo_vr_view(struct halo_vr_view *view) { view_reads++; *view=current_view; return view_available; }
+static struct observer_result *observer_get_camera(short index) { CHECK(index==0); return &observer; }
+static struct observer_result *render_interpolation_camera(short index,struct observer_result *camera) {
+ CHECK(index==0 && camera==&observer); return observer_available ? camera : NULL;
+}
+static real_euler_angles2d *player_control_get_facing_angles(short index) { CHECK(index==0); return &facing; }
+static long seated_unit(void) { return NONE; }
+static void vr_render_vehicle_reference(long unit_index) { CHECK(unit_index==7); }
+static boolean vr_render_vehicle_camera_position(long unit_index,real_point3d *position) {
+ CHECK(unit_index==7 && position); return FALSE;
+}
+static void unit_get_head_position(long index,real_point3d *position) { CHECK(index==7); *position=observer.position; }
+static real magnitude3d(const real_vector3d *v) { return sqrtf(v->i*v->i+v->j*v->j+v->k*v->k); }
+static void normalize3d(real_vector3d *v) { real n=magnitude3d(v); v->i/=n; v->j/=n; v->k/=n; }
+static unsigned cursor_draws;
+static void draw_quad(rectangle2d *bounds,uint32_t color) {
+ CHECK(bounds->x0>=0 && bounds->x1<=640 && bounds->x0<bounds->x1);
+ CHECK(bounds->y0>=0 && bounds->y1<=480 && bounds->y0<bounds->y1);
+ CHECK(color==0xff000000u || color==0xffffffffu); cursor_draws++;
+}
+@@RENDER@@
+
+/* Exercise the actual history render loop: ordinary and goal blips must
+receive the same current heading without touching serialized tick history. */
+#define MOTION_SENSOR_HISTORY_COUNT 10
+#define MAXIMUM_MOTION_SENSOR_BLIPS 1
+#define MAX_CUSTOM_BLIPS 1
+enum { _blip_type_none, _blip_type_custom, _hud_blip_type_medium=1 };
+struct motion_sensor_blip { char type,size; real_point2d position; };
+struct motion_sensor_datum {
+ real yaw; struct motion_sensor_blip blips[1];
+ short custom_blip_count,custom_blip_goal_indices[1]; real_point2d custom_blips[1];
+};
+struct motion_sensor_player { struct motion_sensor_datum sensor_data[10]; } sensor_player;
+static struct { short active_sensor_index; } sensor_globals;
+static void *motion_sensor_globals_unused;
+#define motion_sensor_globals (&sensor_globals)
+static struct { struct { real motion_sensor_scale,motion_sensor_range; } defaults; } hud;
+#define hud_globals (&hud)
+static real_point2d center_point;
+static real sweep_theta;
+static char temporary[64];
+static unsigned blip_draws;
+static real blip_yaws[20];
+static long get_return_eip(void) { return 123; }
+static void csmemset(void *target,int value,size_t size) {
+ size_t i; long *words=target;
+ memset(target,value,size);
+ /* The game stack fence is a 32-bit long, including on a 64-bit host. */
+ for(i=0;i<size/sizeof(long);i++) words[i]=0x62626262;
+}
+#define match_assert(file,line,condition) CHECK(condition)
+#define match_vassert(file,line,condition,message) do { CHECK(condition); (void)(message); } while(0)
+#define csprintf sprintf
+#define power pow
+static struct motion_sensor_player *get_motion_sensor_data(short index) { CHECK(index>=0 && index<4); return &sensor_player; }
+static void blip_begin(const real_point2d *corner,int zero,int multiplayer,short index) {
+ CHECK(corner && zero==0 && multiplayer==1 && index>=0); blip_draws=0;
+}
+static void tiny_point2d_get(const real_point2d *source,real_point2d *target) { *target=*source; }
+static int game_engine_get_goal_in_use(short goal) { return goal==1; }
+static void render_blip(struct motion_sensor_datum *sensor,char type,char size,const real_point2d *point,
+ real scale,real fade,real radius) {
+ CHECK(blip_draws<20 && sensor && type!=0 && size==1 && point && scale>0 && fade>0 && radius>0);
+ blip_yaws[blip_draws++]=sensor->yaw;
+}
+static void rasterizer_hud_motion_sensor_blip_end(const real_point2d *center,real theta) { CHECK(center==&center_point && theta==sweep_theta); }
+@@SENSOR@@
+
+static void fixture(void) {
+ int eye,history;
+ memset(&vr,0,sizeof(vr)); memset(&settings,0,sizeof(settings)); memset(&menu_pointer,0,sizeof(menu_pointer));
+ vr.initialized=vr.frame=vr.origin_valid=vr.aim_synced=vr.aiming=vr.aimed=1;
+ vr.views.focused=vr.views.should_render=vr.views.head.valid=vr.views.aim[1].valid=1;
+ vr.views.head.orientation[3]=vr.views.aim[1].orientation[3]=1.f;
+ for(eye=0;eye<2;eye++) { vr.views.eye[eye].valid=1; vr.views.eye[eye].orientation[3]=1.f; }
+ vr.input.active[1]=1;
+ settings.menu_pointer=settings.radar_head_heading=1;
+ settings.menu_distance=2.f;
+ vr.menus=vr.menu_open=vr.menu_pose.valid=1; vr.menu_pose.orientation[3]=1.f; vr.menu_pose.position[2]=-2.f;
+ vr.pending_layers.hud=1; vr.pending_layers.hud_size[0]=2.f; vr.pending_layers.hud_size[1]=1.5f;
+ memset(&current_view,0,sizeof(current_view)); current_view.head_forward[0]=1.f;
+ current_view.head_up[2]=1.f; current_view.world_units_per_metre=1.f/3.048f;
+ memset(&unit,0,sizeof(unit)); unit.object.type=_object_type_biped; unit.unit.player_index=2;
+ local_player_index=2; player.unit_index=7;
+ memset(&observer,0,sizeof(observer)); observer.forward.i=1.f;
+ cinematic=scripted=inhibited=0; view_available=observer_available=1;
+ perspective=_director_perspective_first_person; facing.pitch=0.f; view_reads=cursor_draws=0;
+ memset(&sensor_player,0,sizeof(sensor_player));
+ for(history=0;history<10;history++) {
+ struct motion_sensor_datum *sensor=&sensor_player.sensor_data[history];
+ sensor->yaw=(float)history*.2f; sensor->blips[0].type=2; sensor->blips[0].size=1;
+ sensor->custom_blip_count=1; sensor->custom_blip_goal_indices[0]=1;
+ }
+ sensor_globals.active_sensor_index=9; hud.defaults.motion_sensor_scale=1.f; hud.defaults.motion_sensor_range=1.f;
+ (void)motion_sensor_globals_unused;
+}
+static struct halo_ui_pointer sample(float trigger) {
+ struct halo_ui_pointer pointer; vr.input.right_trigger=trigger;
+ CHECK(halo_vr_menu_pointer(1,&pointer)); return pointer;
+}
+static void check_leveled_menu_pointer(void) {
+ int roll,pitch,axis;
+ static const float right[3]={1.f,0.f,0.f}, up[3]={0.f,1.f,0.f};
+ for(pitch=-90;pitch<=90;pitch+=30) for(roll=-180;roll<=180;roll+=30) {
+ struct vr_host_pose anchor;
+ struct halo_ui_pointer p;
+ float turn[4]={0.f,sinf(.45f),0.f,cosf(.45f)};
+ float tilt[4]={sinf((float)pitch*(float)M_PI/360.f),0.f,0.f,cosf((float)pitch*(float)M_PI/360.f)};
+ float lean[4]={0.f,0.f,sinf((float)roll*(float)M_PI/360.f),cosf((float)roll*(float)M_PI/360.f)};
+ float oriented[4], panel_right[3], panel_up[3];
+ fixture(); quaternion_multiply(turn,tilt,oriented);
+ quaternion_multiply(oriented,lean,vr.views.head.orientation);
+ vr.views.aim[1]=vr.views.head; vr.menu_pose.valid=0;
+ CHECK(menu_place()); anchor=vr.menu_pose;
+ p=sample(1.f); CHECK(abs(p.x-320)<=1 && abs(p.y-240)<=1 && !p.left_clicks);
+ p=sample(0.f); p=sample(1.f); CHECK(p.left_clicks==1);
+ quaternion_rotate(vr.menu_pose.orientation,right,panel_right);
+ quaternion_rotate(vr.menu_pose.orientation,up,panel_up);
+ CHECK(near_float(panel_right[1],0.f)); CHECK(panel_up[1]>=-0.0001f);
+ /* Hit the same widget pixels in the leveled panel's axes. */
+ for(axis=0;axis<3;axis++) vr.views.aim[1].position[axis]+=.5f*panel_right[axis]+.3f*panel_up[axis];
+ p=sample(0.f); CHECK(abs(p.x-480)<=1 && abs(p.y-144)<=1 && p.moved);
+ /* New head tilt leaves both captured quad and controller target fixed. */
+ memcpy(vr.views.head.orientation,oriented,sizeof(oriented));
+ CHECK(menu_place()); p=sample(0.f); CHECK(!p.moved);
+ CHECK(memcmp(&anchor,&vr.menu_pose,sizeof(anchor))==0);
+ p=sample(1.f); CHECK(p.left_clicks==1 && abs(p.click_x-480)<=1 && abs(p.click_y-144)<=1);
+ }
+}
+static void check_geometry(void) {
+ struct vr_host_pose panel,ray; float size[2]={2.f,1.5f}; short x=-9,y=-9; int yaw,pitch,roll,axis;
+ static const float away[3]={0.f,0.f,2.f};
+ for(yaw=-180;yaw<=180;yaw+=30) for(pitch=-90;pitch<=90;pitch+=30) for(roll=-60;roll<=60;roll+=30) {
+ float qyaw[4]={0.f,sinf((float)yaw*(float)M_PI/360.f),0.f,cosf((float)yaw*(float)M_PI/360.f)};
+ float qpitch[4]={sinf((float)pitch*(float)M_PI/360.f),0.f,0.f,cosf((float)pitch*(float)M_PI/360.f)};
+ float qroll[4]={0.f,0.f,sinf((float)roll*(float)M_PI/360.f),cosf((float)roll*(float)M_PI/360.f)};
+ float intermediate[4],offset[3];
+ memset(&panel,0,sizeof(panel)); panel.valid=1; panel.position[0]=3.f; panel.position[1]=-2.f; panel.position[2]=5.f;
+ quaternion_multiply(qyaw,qpitch,intermediate); quaternion_multiply(intermediate,qroll,panel.orientation);
+ ray=panel; quaternion_rotate(panel.orientation,away,offset);
+ for(axis=0;axis<3;axis++) ray.position[axis]+=offset[axis];
+ CHECK(menu_pointer_intersection(&ray,&panel,size,&x,&y)); CHECK(abs(x-320)<=1 && abs(y-240)<=1);
+ }
+ fixture(); ray=vr.views.aim[1]; panel=vr.menu_pose;
+ ray.position[0]=-.999f; ray.position[1]=.749f;
+ CHECK(menu_pointer_intersection(&ray,&panel,size,&x,&y)); CHECK(x==0 && y==0);
+ ray.position[0]=1.f; ray.position[1]=-.75f;
+ CHECK(menu_pointer_intersection(&ray,&panel,size,&x,&y)); CHECK(x==639 && y==479);
+ ray.position[0]=1.01f; CHECK(!menu_pointer_intersection(&ray,&panel,size,&x,&y));
+ ray=vr.views.aim[1]; ray.position[2]=-3.f; CHECK(!menu_pointer_intersection(&ray,&panel,size,&x,&y));
+ ray=vr.views.aim[1]; ray.orientation[1]=1.f; ray.orientation[3]=0.f;
+ CHECK(!menu_pointer_intersection(&ray,&panel,size,&x,&y));
+ ray.orientation[1]=ray.orientation[3]=sqrtf(.5f); CHECK(!menu_pointer_intersection(&ray,&panel,size,&x,&y));
+ for(axis=0;axis<3;axis++) { ray=vr.views.aim[1]; ray.position[axis]=NAN; CHECK(!menu_pointer_intersection(&ray,&panel,size,&x,&y)); }
+ ray=vr.views.aim[1]; ray.orientation[3]=10.f; CHECK(!menu_pointer_intersection(&ray,&panel,size,&x,&y));
+ ray=vr.views.aim[1]; panel.orientation[0]=INFINITY; CHECK(!menu_pointer_intersection(&ray,&panel,size,&x,&y));
+ panel=vr.menu_pose; size[0]=INFINITY; CHECK(!menu_pointer_intersection(&ray,&panel,size,&x,&y));
+ size[0]=0.f; CHECK(!menu_pointer_intersection(&ray,&panel,size,&x,&y));
+ size[0]=2.f; ray.position[0]=FLT_MAX; panel.position[0]=-FLT_MAX;
+ CHECK(!menu_pointer_intersection(&ray,&panel,size,&x,&y));
+}
+static void check_buttons_and_tracking(void) {
+ struct halo_ui_pointer p; struct vr_host_pose anchor; int corruption;
+ fixture(); p=sample(1.f); CHECK(!p.left_clicks && p.moved); p=sample(1.f); CHECK(!p.left_clicks && !p.moved);
+ p=sample(0.f); CHECK(!p.left_clicks); p=sample(.7f); CHECK(p.left_clicks==1);
+ p=sample(1.f); CHECK(!p.left_clicks); p=sample(.4f); CHECK(!p.left_clicks); p=sample(.7f); CHECK(!p.left_clicks);
+ /* Release off panel, then press there: returning while held cannot select. */
+ vr.views.aim[1].position[0]=3.f; p=sample(0.f); CHECK(p.x==-1 && p.moved && !p.left_clicks);
+ p=sample(1.f); CHECK(!p.left_clicks); vr.views.aim[1].position[0]=0.f;
+ p=sample(1.f); CHECK(p.x==320 && p.moved && !p.left_clicks); p=sample(0.f); p=sample(1.f); CHECK(p.left_clicks==1);
+ /* Head movement cannot recapture or move the fixed pointer target. */
+ anchor=vr.menu_pose; vr.views.head.position[0]=20.f; p=sample(0.f);
+ CHECK(p.x==320 && memcmp(&anchor,&vr.menu_pose,sizeof(anchor))==0);
+ for(corruption=0;corruption<15;corruption++) {
+ fixture(); p=sample(0.f); p=sample(1.f); CHECK(p.left_clicks);
+ switch(corruption) {
+ case 0: vr.views.focused=0; break; case 1: vr.views.should_render=0; break;
+ case 2: vr.views.head.valid=0; break; case 3: vr.input.active[1]=0; break;
+ case 4: vr.views.aim[1].valid=0; break; case 5: vr.views.aim[1].orientation[0]=NAN; break;
+ case 6: vr.views.head.position[0]=INFINITY; break; case 7: vr.views.head.orientation[3]=10.f; break;
+ case 8: vr.frame=vr.pending=0; break; case 9: vr.menu_pose.valid=0; break;
+ case 10: vr.pending_layers.hud=0; break;
+ case 11: vr.menu_pose.orientation[0]=NAN; break;
+ case 12: vr.menu_pose.position[0]=INFINITY; break;
+ case 13: vr.pending_layers.hud_size[0]=NAN; break;
+ case 14: vr.pending_layers.hud_size[1]=0.f; break;
+ }
+ p=sample(1.f); CHECK(p.x==-1 && !p.left_clicks && !menu_pointer.armed);
+ /* Restore tracking without restoring button history. */
+ vr.views.focused=vr.views.should_render=vr.views.head.valid=vr.views.aim[1].valid=vr.input.active[1]=vr.frame=1;
+ memset(vr.views.aim[1].orientation,0,sizeof(vr.views.aim[1].orientation)); vr.views.aim[1].orientation[3]=1.f;
+ memset(vr.views.head.orientation,0,sizeof(vr.views.head.orientation)); vr.views.head.orientation[3]=1.f;
+ vr.views.head.position[0]=0.f; vr.menu_pose.valid=vr.pending_layers.hud=1;
+ memset(vr.menu_pose.orientation,0,sizeof(vr.menu_pose.orientation)); vr.menu_pose.orientation[3]=1.f;
+ vr.menu_pose.position[0]=0.f; vr.pending_layers.hud_size[0]=2.f; vr.pending_layers.hud_size[1]=1.5f;
+ p=sample(1.f); CHECK(!p.left_clicks); p=sample(0.f); p=sample(1.f); CHECK(p.left_clicks==1);
+ }
+ fixture(); p=sample(NAN); CHECK(p.x==-1 && !p.left_clicks); p=sample(1.f); CHECK(!p.left_clicks);
+ p=sample(0.f); p=sample(1.f); CHECK(p.left_clicks);
+ halo_vr_menus(0); halo_vr_menus(1); vr.menu_open=vr.menu_pose.valid=1;
+ p=sample(1.f); CHECK(!p.left_clicks); p=sample(0.f); p=sample(1.f); CHECK(p.left_clicks);
+ fixture(); settings.menu_pointer=0; CHECK(!halo_vr_menu_pointer(1,&p)); CHECK(!halo_vr_menu_cursor(&p.x,&p.y));
+ fixture(); vr.initialized=0; CHECK(!halo_vr_menu_pointer(1,&p));
+ fixture(); p=sample(0.f); vr_render_menu_pointer_draw(); CHECK(cursor_draws==2);
+ vr.views.focused=0; vr_render_menu_pointer_draw(); CHECK(cursor_draws==2);
+}
+static void check_no_menu_fire_and_xbox_mapping(void) {
+ XINPUT_GAMEPAD pad; struct halo_ui_pointer p;
+ fixture(); memset(&pad,0,sizeof(pad)); pad.bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER]=220;
+ vr.input.buttons=VR_BUTTON_A|VR_BUTTON_B|VR_BUTTON_DPAD_DOWN|VR_BUTTON_RIGHT_BUMPER|VR_BUTTON_LEFT_BUMPER;
+ vr.input.left_trigger=.5f; vr.input.right_trigger=1.f; halo_vr_gamepad(&pad);
+ CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER]==0);
+ CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_A]==255 && pad.bAnalogButtons[XINPUT_GAMEPAD_B]==255);
+ CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_BLACK]==255 && pad.bAnalogButtons[XINPUT_GAMEPAD_WHITE]==255);
+ CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER]==127 && (pad.wButtons&XINPUT_GAMEPAD_DPAD_DOWN));
+ halo_vr_menus(0); CHECK(!halo_vr_menu_pointer(0,&p));
+ pad.bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER]=220; halo_vr_gamepad(&pad); CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER]==0);
+ vr.views.focused=0; vr.input.right_trigger=0.f; halo_vr_gamepad(&pad); CHECK(menu_pointer.blocked);
+ vr.views.focused=1; halo_vr_gamepad(&pad); CHECK(!menu_pointer.blocked);
+ vr.input.right_trigger=1.f; halo_vr_gamepad(&pad); CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER]==255);
+ fixture(); settings.menu_pointer=0; memset(&pad,0,sizeof(pad)); vr.input.right_trigger=.5f;
+ halo_vr_gamepad(&pad); CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER]==127);
+ /* An Xbox-only menu must neither own the pointer nor latch its trigger. */
+ fixture(); settings.gamepad_aim=1; vr.input.active[1]=0; vr.input.right_trigger=0.f;
+ memset(&pad,0,sizeof(pad)); pad.bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER]=220;
+ CHECK(!halo_vr_menu_pointer(1,&p)); halo_vr_gamepad(&pad);
+ CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER]==220 && !menu_pointer.blocked);
+ halo_vr_menus(0); halo_vr_gamepad(&pad); CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER]==220);
+ /* Tracking loss after a known released selection is safe for Xbox play. */
+ fixture(); p=sample(0.f); CHECK(!menu_pointer.blocked); vr.input.active[1]=0;
+ CHECK(halo_vr_menu_pointer(1,&p)); halo_vr_menus(0);
+ memset(&pad,0,sizeof(pad)); pad.bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER]=220;
+ halo_vr_gamepad(&pad); CHECK(pad.bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER]==220);
+}
+static void check_radar_heading_and_history(void) {
+ real yaw=99.f; point2d reference={0,0}; int history,mode;
+ struct motion_sensor_player saved;
+ fixture(); vr.menus=0; current_view.body_yaw=1.2f;
+ current_view.head_forward[0]=cosf(.6f); current_view.head_forward[1]=sinf(.6f);
+ current_view.controller_aim=1; current_view.hand_forward[0]=-1.f;
+ CHECK(vr_render_motion_sensor_yaw(0,&yaw) && near_float(yaw,1.8f)); CHECK(view_reads==1);
+ current_view.hand_forward[0]=0.f; current_view.hand_forward[1]=1.f; current_view.two_handed=1.f;
+ CHECK(vr_render_motion_sensor_yaw(0,&yaw) && near_float(yaw,1.8f));
+ yaw=99.f; CHECK(!vr_render_motion_sensor_yaw(1,&yaw) && yaw==99.f);
+ CHECK(!vr_render_motion_sensor_yaw(0,NULL));
+ saved=sensor_player; render_motion_sensor(0,1,&reference); CHECK(blip_draws==20);
+ for(history=0;history<20;history++) CHECK(near_float(blip_yaws[history],1.8f+(float)M_PI*.5f));
+ CHECK(memcmp(&saved,&sensor_player,sizeof(saved))==0);
+ settings.radar_head_heading=0; render_motion_sensor(0,1,&reference); CHECK(blip_draws==20);
+ for(history=0;history<10;history++) CHECK(near_float(blip_yaws[history*2],(float)(9-history)*.2f));
+ CHECK(memcmp(&saved,&sensor_player,sizeof(saved))==0);
+ for(mode=0;mode<12;mode++) {
+ fixture(); vr.menus=0;
+ switch(mode) {
+ case 0: settings.radar_head_heading=0; break; case 1: vr.initialized=0; break;
+ case 2: vr.views.focused=0; break; case 3: vr.views.head.valid=0; break;
+ case 4: vr.views.eye[1].orientation[0]=NAN; break; case 5: local_player_index=NONE; break;
+ case 6: cinematic=1; break; case 7: scripted=1; break; case 8: inhibited=1; break;
+ case 9: current_view.head_forward[0]=0.f; current_view.head_forward[2]=1.f; break;
+ case 10: current_view.head_forward[0]=NAN; break; case 11: observer_available=0; break;
+ }
+ yaw=99.f; CHECK(!vr_render_motion_sensor_yaw(0,&yaw) && yaw==99.f);
+ }
+ fixture(); vr.menus=0; vr.aiming=0; observer.forward.i=0.f; observer.forward.j=1.f;
+ CHECK(vr_render_motion_sensor_yaw(0,&yaw) && near_float(yaw,(float)M_PI*.5f));
+}
+int main(void) {
+ check_geometry(); check_leveled_menu_pointer(); check_buttons_and_tracking();
+ check_no_menu_fire_and_xbox_mapping(); check_radar_heading_and_history();
+ printf("PASS: %u production menu pointer / controller / HMD radar assertions\n",checks); return 0;
+}
+'''
+
+# Fixture names must not shadow locals in the untouched production functions.
+for fixture_name in ('unit', 'player', 'observer', 'perspective', 'local_player_index'):
+ HARNESS = re.sub(r'\b' + fixture_name + r'\b', 'mock_' + fixture_name, HARNESS)
+HARNESS = HARNESS.replace('} mock_unit; } mock_unit;', '} unit; } mock_unit;').replace('.mock_unit.', '.unit.')
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ args = parser.parse_args()
+ guest = (ROOT / 'port/linux/src/vr.c').read_text()
+ render = (ROOT / 'port/linux/game/vr_render.c').read_text()
+ sensor = (ROOT / 'source/interface/motion_sensor.c').read_text()
+ ui = (ROOT / 'source/interface/ui_widget.c').read_text()
+ renderer = (ROOT / 'port/linux/src/d3d8_gl.c').read_text()
+ config = (ROOT / 'port/linux/src/port_config.c').read_text()
+ desktop_pointer = function(renderer[renderer.rfind('int halo_ui_pointer_update'):], 'halo_ui_pointer_update')
+ assert 'halo_vr_menu_pointer(menus_active, pointer)' in desktop_pointer
+ assert 'vr_render_menu_pointer_draw();' in function(ui, 'render_ui_widgets')
+ assert '&vr.menu_pose' in function(guest, 'halo_vr_menu_pointer')
+ assert 'menu_place(' not in function(guest, 'halo_vr_menu_pointer')
+ sensor_draw = function(sensor, 'render_motion_sensor')
+ assert sensor_draw.count('render_sensor,') == 2 and 'draw_sensor = *sensor;' in sensor_draw
+ assert 'sensor->yaw =' not in sensor_draw
+ for key in ('menu_pointer', 'radar_head_heading'):
+ assert re.search(r'"vr\.' + key + r'",\s*_config_boolean,\s*"true"', config)
+ harness = HARNESS.replace('@@GUEST@@', '\n\n'.join(function(guest, name) for name in (
+ 'quaternion_rotate', 'quaternion_multiply', 'halo_vr_menus',
+ 'menu_tracking_valid', 'menu_level_orientation', 'menu_place',
+ 'menu_pointer_panel_valid', 'menu_pointer_intersection', 'menu_pointer_tracking_valid', 'menu_pointer_button',
+ 'halo_vr_menu_pointer', 'halo_vr_menu_cursor', 'menu_pointer_fire_blocked',
+ 'halo_vr_radar_head_heading', 'halo_vr_gaze_tracking_valid', 'halo_vr_aiming',
+ 'analog', 'stick_merge', 'halo_vr_gamepad')))
+ harness = harness.replace('@@RENDER@@', '\n\n'.join(function(render, name) for name in (
+ 'yaw_rotate', 'view_rotate', 'view_vector', 'vr_render_player_gaze', 'vr_render_motion_sensor_yaw', 'vr_render_menu_pointer_draw')))
+ harness = harness.replace('@@SENSOR@@', sensor_draw)
+ compiler = args.cc or os.environ.get('CC')
+ vcvars = args.vcvarsall
+ if not compiler and os.name == 'nt':
+ installations = Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio'
+ candidates = sorted(installations.glob('*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1])
+ vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('No C11 host compiler found; select one with --cc.')
+ includes = [ROOT / 'port/linux/src']
+ harness = harness.replace('"halo_ui_pointer.h"', '"' + (ROOT / 'port/linux/include/halo_ui_pointer.h').as_posix() + '"')
+ with tempfile.TemporaryDirectory(prefix='halo-vr-pointer-', ignore_cleanup_errors=True) as directory:
+ temp = Path(directory)
+ source = temp / 'test.c'
+ executable = temp / ('test.exe' if os.name == 'nt' else 'test')
+ source.write_text(harness)
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX', '/D_CRT_SECURE_NO_WARNINGS',
+ '/DHALO_VR', '/Fe:' + str(executable), '/Fo:' + str(temp / 'test.obj')]
+ command.extend('/I' + str(path) for path in includes)
+ command.append(str(source))
+ if vcvars:
+ for path in (vcvars, Path(compiler), temp):
+ if any(character in str(path) for character in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported compiler/temp shell metacharacter')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\n'
+ 'if errorlevel 1 exit /b 1\n' + subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-DHALO_VR']
+ command.extend('-I' + str(path) for path in includes)
+ command.extend([str(source), *([] if os.name == 'nt' else ['-lm']), '-o', str(executable)])
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(executable)], cwd=temp, check=True)
+ print('PASS: production UI glue, fixed menu quad, toggle defaults and render-only radar history')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_projectile_reticle.py b/tools/test_vr_projectile_reticle.py
new file mode 100644
index 0000000000000000000000000000000000000000..8e353b0dc418172ad2aa221d221d30abbe212ebf
--- /dev/null
+++ b/tools/test_vr_projectile_reticle.py
@@ -0,0 +1,902 @@
+#!/usr/bin/env python3
+"""Compile actual nominal firing/preview/reticle functions with a C11 host.
+
+Checks source equivalence to the pinned firing path, camera/trigger offsets,
+read-only aim assist, vehicle collision ignores, unarmed pointing, current hit
+depth and stereo projection. Engine services are deterministic stubs; no headset or maps needed.
+This does not validate random spread, ballistics or GPU/headset presentation.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+
+ROOT = Path(__file__).resolve().parents[1]
+ORIGINAL_ENGINE_COMMIT = '2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4'
+
+
+def function(text, name):
+ match = re.search(r'(?:static\s+)?(?:void|float|real|int|boolean|long|short)\s+' +
+ re.escape(name) + r'\s*\([^;]*?\)\s*\{', text)
+ assert match, name
+ start = text.index('{', match.start())
+ return text[match.start():block_end(text, start)]
+
+
+def block_end(text, start):
+ masked = re.sub(r'/\*.*?\*/|//[^\n]*|"(?:\\.|[^"\\])*"',
+ lambda item: re.sub(r'[^\n]', ' ', item.group()), text, flags=re.S)
+ end, depth = start + 1, 1
+ while depth:
+ depth += (masked[end] == '{') - (masked[end] == '}')
+ end += 1
+ return end
+
+
+def normalized(text):
+ return re.sub(r'\s+', '', text)
+
+
+def without_visibility_guard(text, name):
+ """Remove only the small VR gate when comparing the rest with stock."""
+ pattern = r'\n#ifdef HALO_VR\n(?:(?!#endif).)*?extern boolean ' + re.escape(name) + r'\([^;]+;(?:(?!#endif).)*?\n#endif\n'
+ matches = list(re.finditer(pattern, text, re.S))
+ assert len(matches) == 1, (name, len(matches))
+ match = matches[0]
+ return text[:match.start()] + text[match.end():]
+
+
+def verify_refactors(weapons, aim, render, units):
+ old_weapons = subprocess.check_output(['git', 'show', ORIGINAL_ENGINE_COMMIT + ':source/items/weapons.c'], cwd=ROOT, text=True)
+ old_aim = subprocess.check_output(['git', 'show', ORIGINAL_ENGINE_COMMIT + ':source/game/aim_assist.c'], cwd=ROOT, text=True)
+ old_fire = function(old_weapons, 'trigger_create_projectiles')
+ start = old_fire.index('if (!TEST_FLAG(trigger_definition->flags, _weapon_trigger_projectiles_cannot_be_aimed_bit)')
+ end = block_end(old_fire, old_fire.index('{', start))
+ preparation = old_fire[start:end]
+ # Reinsert the shared call into the full real-shot function: everything
+ # after nominal preparation (spread, placement, ammo and RNG) must match.
+ new_fire = function(weapons, 'trigger_create_projectiles')
+ call = 'target_object_index = trigger_adjust_projectile_ray(owner_object_index, unit, trigger_definition,\n\t\t\t&origin, &forward, &velocity, &error, FALSE, NULL);'
+ assert new_fire.count(call) == 1
+ assert normalized(new_fire.replace(call, preparation)) == normalized(old_fire), 'Real firing behavior changed'
+ shared = function(weapons, 'trigger_adjust_projectile_ray')
+ shared_start = shared.index('if (!TEST_FLAG(trigger_definition->flags, _weapon_trigger_projectiles_cannot_be_aimed_bit)')
+ shared_block = shared[shared_start:block_end(shared, shared.index('{', shared_start))]
+ assert 'player_preview_projectile_aim' in shared_block and 'length <= 0.0f' in shared_block
+ shared_block = re.sub(r'#ifdef HALO_VR\n.*?#endif\n', '', shared_block, flags=re.S)
+ expected = preparation
+ for local in ('origin', 'forward', 'velocity', 'error'):
+ expected = re.sub(r'&' + local + r'\b', local, expected)
+ assert normalized(shared_block) == normalized(expected), 'Shared trigger preparation changed math'
+ old_body = function(old_aim, 'player_aim_projectile')
+ old_body = old_body[old_body.index('{'):]
+ new_body = function(aim, 'player_aim_projectile_internal')
+ new_body = new_body[new_body.index('{'):]
+ new_body = re.sub(r'#ifn?def HALO_VR\n.*?#endif\n', '', new_body, flags=re.S)
+ recording = re.search(r'if \(record_target\)\s*\{([^{}]*)\}', new_body)
+ assert recording and 'aim_assist_unit_index' in recording.group(1) and 'aim_assist_timestamp' in recording.group(1)
+ assert normalized(new_body[:recording.start()] + recording.group(1) + new_body[recording.end():]) == normalized(old_body), 'Shared aim math changed'
+ assert 'TRUE' in function(aim, 'player_aim_projectile') and 'FALSE' in function(aim, 'player_preview_projectile_aim')
+ old_units = subprocess.check_output(['git', 'show', ORIGINAL_ENGINE_COMMIT + ':source/units/units.c'], cwd=ROOT, text=True)
+ original_unit = function(old_units, 'unit_adjust_projectile_ray')
+ new_unit = function(units, 'unit_adjust_projectile_ray_internal')
+ new_unit = new_unit[new_unit.index('{'):]
+ new_unit = new_unit.replace('preview_forward ? *preview_forward : unit->unit.aiming_vector', 'unit->unit.aiming_vector')
+ new_unit = new_unit.replace('if (preview_camera)\n\t\t\tcamera_position = *preview_camera;\n\t\telse\n\t\t\tunit_get_camera_position(unit_index, &camera_position);',
+ 'unit_get_camera_position(unit_index, &camera_position);')
+ assert normalized(new_unit) == normalized(original_unit[original_unit.index('{'):]), 'Unit NULL firing math changed'
+ assert 'NULL, NULL' in function(units, 'unit_adjust_projectile_ray')
+ assert function(units, 'unit_clip_to_aiming_bounds') == function(old_units, 'unit_clip_to_aiming_bounds')
+ pose_builder = function(render, 'vr_render_pointing_pose')
+ assert 'render_interpolation_camera(0, observer_get_camera(0))' in pose_builder
+ assert 'hand_position' not in pose_builder and 'base_position' not in pose_builder and 'player_effect_get_camera_effect_matrix' not in pose_builder
+ assert 'DEGREES_TO_RADIANS(85.0f)' in pose_builder and 'unit_clip_to_aiming_bounds' in pose_builder
+ assert 'vr_render_pointing_pose(player_index, unit, &vr_render.view, vr_render.base_yaw, pose)' in function(render, 'vr_render_projectile_pose')
+ preview = function(weapons, 'weapon_preview_projectile_ray')
+ assert 'object_get_ultimate_parent(weapon_get_projectile_owner_object_index(weapon_index))' in preview
+ assert 'MAXIMUM_NUMBER_OF_TRIGGERS_PER_WEAPON' in preview
+ hud = function(render, 'vr_render_hud_camera')
+ assert 'hand_forward' not in hud and 'render_camera_world_to_screen' not in hud and '100.0f' not in hud, 'Unsupported preview retains fake hand trajectory'
+ point = function(render, 'vr_render_projectile_point')
+ assert point.index('MAXIMUM_COLLISION_USER_STACK_DEPTH - 2') < point.index('vr_render_projectile_ray(')
+ assert '_collision_test_for_projectiles_flags' in point and '*point = collision.point;' in point
+ crosshairs = function(render, 'vr_render_crosshairs')
+ assert crosshairs.count('vr_render_projectile_point(') == 1
+ assert 'CROSSHAIR_SMOOTHING' not in render and 'crosshair_inverse_distance' not in render
+ print('PASS: pinned unit/firing/aim NULL math identical; preview/HUD/collision contracts')
+
+
+HARNESS = r'''
+#include <math.h>
+#include <float.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+typedef float real;
+typedef int boolean;
+typedef struct { real x,y,z; } real_point3d;
+typedef struct { real i,j,k; } real_vector3d;
+typedef struct { short x0,y0,x1,y1; } rectangle2d;
+#define TRUE 1
+#define FALSE 0
+#define NONE -1
+#define PIN(v,l,h) ((v)<(l)?(l):(v)>(h)?(h):(v))
+#define DEGREES_TO_RADIANS(v) ((v)*0.017453292519943295f)
+#define TEST_FLAG(value, bit) (((value) & (1u << (bit))) != 0)
+#define MAXIMUM_NUMBER_OF_TRIGGERS_PER_WEAPON 2
+#define MAXIMUM_COLLISION_USER_STACK_DEPTH 8
+#define CROSSHAIR_FARTHEST 100.0f
+enum { _object_dead_bit, _object_invisible_bit, _unit_fires_from_camera_bit,
+ _weapon_trigger_projectiles_cannot_be_aimed_bit, _weapon_trigger_uses_weapon_origin_bit };
+enum { _object_type_biped=0, _object_type_vehicle=1, _director_perspective_first_person=1 };
+enum { _object_mask_unit=3, _director_perspective_scripted=2, _director_perspective_neutral=3 };
+typedef int director_perspective;
+enum { _collision_user_ui, _collision_user_aim_assist };
+enum { _collision_test_for_projectiles_flags = 31, _collision_test_for_line_of_sight_flags = 7, _collision_result_object = 1 };
+static int checks;
+#define CHECK(c) do { checks++; if (!(c)) { fprintf(stderr,"failed line %d: %s\n",__LINE__,#c); exit(1); } } while (0)
+#define match_assert(file,line,c) CHECK(c)
+#define match_assert_valid_real_normal3d(file,line,v) CHECK(isfinite((v)->i) && isfinite((v)->j) && isfinite((v)->k))
+static int near(real a, real b) { return fabsf(a-b)<0.002f; }
+static void point_equal(real_point3d a, real_point3d b) { CHECK(near(a.x,b.x)); CHECK(near(a.y,b.y)); CHECK(near(a.z,b.z)); }
+static void direction_equal(real_vector3d a, real_vector3d b) { CHECK(near(a.i,b.i)); CHECK(near(a.j,b.j)); CHECK(near(a.k,b.k)); }
+struct object_data { int type; unsigned flags,damage_flags; long parent_object_index; real_point3d position; };
+struct object_datum { struct object_data object; };
+struct unit_datum { long definition_index; struct object_data object; struct { long player_index,actor_index,gunner_object_index; short current_weapon_index; real_vector3d aiming_vector; } unit; };
+struct weapon_datum { long definition_index; struct object_data object; };
+struct player_datum { long unit_index,aim_assist_unit_index,aim_assist_timestamp; short team_index,local_player_index; };
+struct weapon_trigger_definition { unsigned flags; real_point3d first_person_weapon_offset; };
+struct trigger_block { short count; struct weapon_trigger_definition data[2]; };
+struct weapon_definition { struct { struct trigger_block triggers; } weapon; };
+struct unit_definition { struct { unsigned flags; real aiming_velocity_maximum,aiming_acceleration_maximum; } unit; };
+struct projectile_preview_pose { real_point3d camera_position; real_vector3d forward; };
+struct observer_result { real_point3d position; real_vector3d forward; };
+typedef struct { real yaw,pitch; } real_euler_angles2d;
+struct object_marker { struct { real_point3d position; real_vector3d forward; } matrix; };
+struct aim_assist_parameters { real deviation_angle; };
+struct aim_assist_target { long object_index; real_point3d position; real autoaim_level; };
+struct collision_result { real_point3d point; int type; long object_index; };
+struct render_frustum { struct { real n[4][3]; } world_to_view; real projection_matrix[4][4]; };
+struct render_camera { real_point3d position; real_vector3d forward,up; real vertical_field_of_view; rectangle2d viewport_bounds,window_bounds; };
+struct halo_vr_view { int vehicle_basis_valid; float vehicle_basis[3][3]; real body_yaw,hud_tangent,world_units_per_metre; boolean controller_aim,gamepad_aim,gamepad_pitch,vehicle_first_person,controller_aim_requested; real head_forward[3],head_up[3],head_position[3],hand_forward[3]; float fov[2][4]; };
+static struct { real base_yaw; boolean active,crosshairs_drawn; boolean crosshairs_drawing; struct halo_vr_view view; struct render_frustum eyes[2]; } vr_render;
+static struct { struct render_camera camera; } render;
+static short vr_render_crosshair_offset[2];
+static struct unit_datum units[4];
+static struct object_datum fixture_objects[4];
+static struct weapon_datum weapons[2];
+static struct player_datum players[1];
+static struct weapon_definition definitions[1];
+static struct unit_definition unit_definitions[1];
+static struct object_marker fixture_marker;
+static real_point3d unit_camera, deterministic_camera, assist_target, fixture_assist_camera;
+static real_vector3d fixture_assist_forward;
+static real_point3d fixture_aim_trace_origin;
+static real_vector3d deterministic_direction, fixture_velocity;
+static real fixture_autoaim_level, deviation_angle, wall_x;
+static int unit_valid[4], weapon_valid[2], inventory[4], fixture_weapon_count[4], fixture_aiming_unit[4];
+static int params_enabled, assist_enabled, actor_calls, time_calls, marker_count, marker_object;
+static int collision_calls, aim_calls, los_calls, max_depth, last_flags;
+static long last_ignore;
+static int global_current_collision_user_depth, global_current_collision_users[MAXIMUM_COLLISION_USER_STACK_DEPTH];
+static real_vector3d up_vector={0,0,1}, left_vector={0,1,0};
+static real_vector3d const *global_up3d=&up_vector,*global_left3d=&left_vector;
+#define TAG_BLOCK_GET_ELEMENT(block,index,type) (&(block)->data[(index)])
+static struct unit_datum *unit_try_and_get(long index) { return index>=0 && index<4 && unit_valid[index] ? &units[index] : NULL; }
+static struct unit_datum *unit_get(long index) { CHECK(unit_try_and_get(index)!=NULL); return &units[index]; }
+static struct object_datum *object_get(long index) { CHECK(unit_try_and_get(index)!=NULL); fixture_objects[index].object=units[index].object; return &fixture_objects[index]; }
+static struct weapon_datum *weapon_try_and_get(long index) { return index>=0 && index<2 && weapon_valid[index] ? &weapons[index] : NULL; }
+static struct weapon_datum *weapon_get(long index) { CHECK(weapon_try_and_get(index)!=NULL); return &weapons[index]; }
+static struct player_datum *player_get(long index) { CHECK(index==0); return &players[0]; }
+static struct weapon_definition *weapon_definition_get(long index) { CHECK(index==0); return &definitions[0]; }
+static struct unit_definition *unit_definition_get(long index) { CHECK(index==0); return &unit_definitions[0]; }
+static long unit_inventory_get_weapon(long index,short slot) { (void)slot; CHECK(index>=0 && index<4); return inventory[index]; }
+static short unit_get_weapon_count(long index) { CHECK(index>=0 && index<4); return (short)fixture_weapon_count[index]; }
+static long unit_get_aiming_unit_index(long index) { CHECK(index>=0 && index<4); return fixture_aiming_unit[index]; }
+static short unit_get_zoom_level(long index) { (void)index; return 0; }
+static int actor_firing_blindly(long index) { (void)index; return FALSE; }
+static long actor_aim_projectile(long actor,real_point3d *origin,real_vector3d *forward,real *error) { (void)actor; (void)origin; (void)forward; (void)error; actor_calls++; return NONE; }
+static long game_time_get(void) { time_calls++; return 1234; }
+static long object_get_ultimate_parent(long index) { int steps=0; while (index>=0 && index<4 && units[index].object.parent_object_index!=NONE) { CHECK(++steps<4); index=units[index].object.parent_object_index; } return index; }
+static short object_get_marker_by_name(long object,char const *name,struct object_marker *result,short maximum) { CHECK(maximum==1); CHECK(strcmp(name,"primary trigger")==0 || strcmp(name,"secondary trigger")==0); marker_object=(int)object; *result=fixture_marker; return (short)marker_count; }
+static void unit_get_camera_position(long index,real_point3d *result) { (void)index; *result=unit_camera; }
+static void object_get_velocities(long index,real_vector3d *velocity,void *unused) { (void)index; (void)unused; *velocity=fixture_velocity; }
+static real dot_product3d(real_vector3d const *a,real_vector3d const *b) { return a->i*b->i+a->j*b->j+a->k*b->k; }
+static void vector_from_points3d(real_point3d const *a,real_point3d const *b,real_vector3d *r) { r->i=b->x-a->x; r->j=b->y-a->y; r->k=b->z-a->z; }
+static real magnitude3d(real_vector3d const *v) { return sqrtf(dot_product3d(v,v)); }
+static real normalize3d(real_vector3d *v) { real length=magnitude3d(v); if(length>0) { v->i/=length; v->j/=length; v->k/=length; } return length; }
+static void fast_normalize3d(real_vector3d *v) { normalize3d(v); }
+static void scale_vector3d(real_vector3d const *v,real amount,real_vector3d *r) { r->i=v->i*amount; r->j=v->j*amount; r->k=v->k*amount; }
+static void point_from_line3d(real_point3d const *o,real_vector3d const *v,real t,real_point3d *r) { r->x=o->x+v->i*t; r->y=o->y+v->j*t; r->z=o->z+v->k*t; }
+static void set_real_point3d(real_point3d *r,real x,real y,real z) { r->x=x; r->y=y; r->z=z; }
+static void cross_product3d(real_vector3d const *a,real_vector3d const *b,real_vector3d *r) { real_vector3d v={a->j*b->k-a->k*b->j,a->k*b->i-a->i*b->k,a->i*b->j-a->j*b->i}; *r=v; }
+static void perpendicular3d(real_vector3d const *v,real_vector3d *r) { real_vector3d basis=fabsf(v->i)<0.8f ? (real_vector3d){1,0,0} : (real_vector3d){0,1,0}; cross_product3d(v,&basis,r); }
+static void rotate_vector_about_axis(real_vector3d *v,real_vector3d const *axis,real sine,real cosine) { real_vector3d cross; real dot=dot_product3d(axis,v); cross_product3d(axis,v,&cross); v->i=v->i*cosine+cross.i*sine+axis->i*dot*(1-cosine); v->j=v->j*cosine+cross.j*sine+axis->j*dot*(1-cosine); v->k=v->k*cosine+cross.k*sine+axis->k*dot*(1-cosine); }
+static real sine(real angle) { return sinf(angle); }
+static real cosine(real angle) { return cosf(angle); }
+static boolean unit_get_aim_assist_parameters(long unit,short zoom,struct aim_assist_parameters *p) { (void)unit; (void)zoom; p->deviation_angle=deviation_angle; return params_enabled; }
+static void director_camera_deterministic(long unit,real_point3d *position,real_vector3d *direction) { (void)unit; *position=deterministic_camera; *direction=deterministic_direction; }
+static boolean collision_test_vector(int flags,real_point3d const *origin,real_vector3d const *vector,long ignore,struct collision_result *collision) {
+ real t;
+ CHECK(global_current_collision_user_depth>0 && global_current_collision_user_depth<=MAXIMUM_COLLISION_USER_STACK_DEPTH);
+ if(global_current_collision_user_depth>max_depth) max_depth=global_current_collision_user_depth;
+ collision_calls++; last_flags=flags; last_ignore=ignore;
+ if(flags==_collision_test_for_projectiles_flags && global_current_collision_users[global_current_collision_user_depth-1]==_collision_user_aim_assist)
+ fixture_aim_trace_origin=*origin;
+ point_from_line3d(origin,vector,1,&collision->point); collision->type=0; collision->object_index=NONE;
+ t=vector->i!=0 ? (wall_x-origin->x)/vector->i : -1;
+ if(t>=0 && t<=1) { point_from_line3d(origin,vector,t,&collision->point); return TRUE; }
+ return FALSE;
+}
+@@LOS@@
+static boolean aim_assist(struct aim_assist_parameters const *parameters,real_point3d const *position,real_vector3d const *direction,long ignore,short team,struct aim_assist_target *target) {
+ (void)parameters; (void)team; aim_calls++; fixture_assist_camera=*position; fixture_assist_forward=*direction;
+ if(!assist_enabled) return FALSE;
+ /* Exercise the production nested LOS bracket used by actual autoaim. */
+ los_calls++; (void)aim_assist_clear_line_of_sight(position,&assist_target,ignore,3);
+ target->position=assist_target; target->object_index=3; target->autoaim_level=fixture_autoaim_level; return TRUE;
+}
+@@MATH@@
+@@AIM@@
+@@UNIT@@
+@@WEAPON_HELPERS@@
+@@PREVIEW@@
+static struct observer_result fixture_observer;
+static real_point3d fixture_head_position;
+static real_euler_angles2d fixture_facing_angles;
+static boolean fixture_scripted_camera;
+static boolean *director_camera_scripted=&fixture_scripted_camera;
+static int fixture_perspective,fixture_inhibited,fixture_observer_valid,fixture_clip_calls;
+static real fixture_clip_pitch;
+static real_vector3d fixture_facing;
+static int director_get_perspective(short local) { (void)local; return fixture_perspective; }
+static int director_inhibited_facing(short local) { (void)local; return fixture_inhibited; }
+static struct observer_result const *observer_get_camera(short local) { (void)local; return fixture_observer_valid ? &fixture_observer : NULL; }
+static struct observer_result const *render_interpolation_camera(short local,struct observer_result const *observer) { (void)local; return observer; }
+static real_vector3d *player_control_get_facing_direction(short local,real_vector3d *direction) { (void)local; *direction=fixture_facing; return direction; }
+static real_euler_angles2d const *player_control_get_facing_angles(short local) { (void)local; return &fixture_facing_angles; }
+static void unit_get_head_position(long index,real_point3d *position) { CHECK(index==0); *position=fixture_head_position; }
+static boolean unit_clip_to_aiming_bounds(long index,real_vector3d *direction,boolean aiming) {
+ real yaw,pitch,horizontal;
+ CHECK(index==0 && aiming); fixture_clip_calls++;
+ if(fixture_clip_pitch<=0) return FALSE;
+ yaw=atan2f(direction->j,direction->i); pitch=asinf(PIN(direction->k,-1,1));
+ pitch=PIN(pitch,-fixture_clip_pitch,fixture_clip_pitch); horizontal=cosf(pitch);
+ *direction=(real_vector3d){horizontal*cosf(yaw),horizontal*sinf(yaw),sinf(pitch)}; return TRUE;
+}
+static int drawn_eye,draw_count;
+static int hud_glyph_calls;
+static float draw_corrections[2][4],drawn_points[2][2];
+static boolean fixture_frame_active;
+static boolean halo_vr_frame_active(void) { return fixture_frame_active; }
+enum { MAXIMUM_NUMBER_OF_LOCAL_PLAYERS = 4 };
+static boolean fixture_widgets[4];
+static boolean ui_widgets_active_for_local_player(short local) {
+ CHECK(local>=0 && local<MAXIMUM_NUMBER_OF_LOCAL_PLAYERS); return fixture_widgets[local];
+}
+static int fixture_vr_aiming,fixture_cinematic;
+static long fixture_local_player;
+static struct halo_vr_view fixture_live_view;
+static int fixture_live_view_valid,fixture_tracking_valid;
+static boolean halo_vr_aiming(void) { return fixture_vr_aiming; }
+static boolean cinematic_in_progress(void) { return fixture_cinematic; }
+static long local_player_get_player_index(short local) { (void)local; return fixture_local_player; }
+static int halo_vr_view(struct halo_vr_view *view) { if(!fixture_live_view_valid) return FALSE; *view=fixture_live_view; return TRUE; }
+static int halo_vr_gaze_tracking_valid(void) { return fixture_tracking_valid; }
+static int halo_vr_presentation_tracking_valid(void) { return fixture_tracking_valid; }
+/* Seat reference integration has its own production vehicle harness. This
+fixture supplies the already-selected view basis for reticle/gaze tests. */
+static void vr_render_vehicle_reference(long unit_index) { (void)unit_index; }
+static boolean vr_render_vehicle_camera_position(long unit_index,real_point3d *position) {
+ (void)unit_index; (void)position; return FALSE;
+}
+static real view_pitch(void) { return 0; }
+static void halo_vr_set_screen_corrections(float value[2][4]) { memcpy(draw_corrections,value,sizeof(draw_corrections)); }
+static void halo_vr_draw_eye(int eye) { drawn_eye=eye; }
+boolean vr_render_hud_crosshairs_visible(short local_player_index);
+static void test_hud_crosshair_dispatch(struct player_datum *player);
+static void hud_draw_vr_crosshairs(void) { int previous=hud_glyph_calls;CHECK(drawn_eye==0 || drawn_eye==1); CHECK(vr_render_hud_crosshairs_visible(0)); test_hud_crosshair_dispatch(&players[0]);CHECK(hud_glyph_calls==previous+1);drawn_points[drawn_eye][0]=draw_corrections[drawn_eye][0]*320+draw_corrections[drawn_eye][2]; drawn_points[drawn_eye][1]=draw_corrections[drawn_eye][1]*240+draw_corrections[drawn_eye][3]; draw_count++; }
+static void vr_render_screen_point_end(void) { }
+@@RENDER@@
+@@HUDGATE@@
+static void reset(void) {
+ int i;
+ memset(units,0,sizeof(units)); memset(weapons,0,sizeof(weapons)); memset(definitions,0,sizeof(definitions));
+ memset(players,0,sizeof(players)); memset(unit_definitions,0,sizeof(unit_definitions)); memset(&vr_render,0,sizeof(vr_render));
+ memset(&render,0,sizeof(render));
+ for(i=0;i<4;i++) { unit_valid[i]=1; units[i].object.parent_object_index=NONE; units[i].unit.player_index=NONE; units[i].unit.actor_index=NONE; units[i].unit.gunner_object_index=NONE; inventory[i]=NONE; fixture_weapon_count[i]=0; fixture_aiming_unit[i]=i; }
+ for(i=0;i<2;i++) { weapon_valid[i]=1; weapons[i].object.parent_object_index=NONE; }
+ units[0].unit.player_index=0; units[0].unit.aiming_vector=(real_vector3d){1,0,0}; inventory[0]=0; fixture_weapon_count[0]=1;
+ players[0].unit_index=0; players[0].aim_assist_unit_index=41; players[0].aim_assist_timestamp=42;
+ weapons[0].object.parent_object_index=0; definitions[0].weapon.triggers.count=1;
+ fixture_marker.matrix.position=(real_point3d){2,0.7f,0.3f}; fixture_marker.matrix.forward=(real_vector3d){0.97f,0.12f,-0.08f}; normalize3d(&fixture_marker.matrix.forward);
+ unit_camera=(real_point3d){0,0,0.5f}; deterministic_camera=unit_camera; deterministic_direction=(real_vector3d){1,0,0};
+ fixture_velocity=(real_vector3d){0.3f,0.4f,0.2f}; assist_target=(real_point3d){20,0.1f,0.6f}; fixture_autoaim_level=0.65f; deviation_angle=1;
+ params_enabled=assist_enabled=actor_calls=time_calls=collision_calls=aim_calls=los_calls=max_depth=0;
+ marker_count=1; marker_object=NONE; wall_x=20; last_ignore=NONE; last_flags=0;
+ global_current_collision_user_depth=1; memset(global_current_collision_users,0,sizeof(global_current_collision_users));
+ vr_render.active=TRUE; vr_render.view.controller_aim=TRUE; vr_render.view.hud_tangent=1;
+ vr_render.view.head_forward[0]=1; vr_render.view.head_up[2]=1;
+ vr_render.view.hand_forward[1]=1; /* Deliberately very different from actual unit firing. */
+ render.camera.forward=(real_vector3d){1,0,0}; render.camera.viewport_bounds=(rectangle2d){0,0,640,480}; render.camera.window_bounds=render.camera.viewport_bounds;
+ for(i=0;i<2;i++) {
+ struct render_frustum *eye=&vr_render.eyes[i];
+ eye->world_to_view.n[0][2]=1; eye->world_to_view.n[1][0]=-1; eye->world_to_view.n[2][1]=1;
+ eye->world_to_view.n[3][0]=i==0 ? -0.035f : 0.035f;
+ eye->projection_matrix[0][0]=1; eye->projection_matrix[1][1]=1; eye->projection_matrix[2][2]=1; eye->projection_matrix[2][3]=1;
+ vr_render.view.fov[i][0]=-1; vr_render.view.fov[i][1]=1; vr_render.view.fov[i][2]=1; vr_render.view.fov[i][3]=-1;
+ }
+ fixture_observer.position=unit_camera; fixture_facing=units[0].unit.aiming_vector;
+ fixture_observer.forward=(real_vector3d){1,0,0}; fixture_head_position=(real_point3d){3,4,5};
+ fixture_facing_angles=(real_euler_angles2d){0,0}; fixture_scripted_camera=FALSE;
+ vr_render.view.world_units_per_metre=2;
+ fixture_perspective=_director_perspective_first_person; fixture_inhibited=0; fixture_observer_valid=1;
+ fixture_clip_pitch=0; fixture_clip_calls=0;
+ fixture_vr_aiming=TRUE; fixture_cinematic=FALSE; fixture_local_player=0;
+ fixture_frame_active=TRUE;
+ fixture_live_view=vr_render.view; fixture_live_view_valid=TRUE; fixture_tracking_valid=TRUE;
+ draw_count=0; drawn_eye=-1; vr_render_crosshair_offset[0]=vr_render_crosshair_offset[1]=0;
+ hud_glyph_calls=0;
+ memset(fixture_widgets,0,sizeof(fixture_widgets));
+}
+static void check_ray_equivalence(void) {
+ int camera,offset,assist,origin_flag,orientation;
+ real_vector3d orientations[]={{1,0,0},{0.9f,0.2f,0.35f},{0,0,1},{0,0,-1}};
+ for(camera=0;camera<2;camera++) for(offset=0;offset<2;offset++) for(assist=0;assist<3;assist++)
+ for(origin_flag=0;origin_flag<2;origin_flag++) for(orientation=0;orientation<4;orientation++) {
+ real_point3d preview_origin,fire_origin; real_vector3d preview_direction,fire_direction;
+ real velocity=0,error=0; long ignore,target; struct weapon_trigger_definition *trigger;
+ reset(); units[0].unit.aiming_vector=orientations[orientation]; normalize3d(&units[0].unit.aiming_vector);
+ deterministic_direction=units[0].unit.aiming_vector; unit_definitions[0].unit.flags=camera ? 1u<<_unit_fires_from_camera_bit : 0;
+ params_enabled=assist>0; assist_enabled=assist>1;
+ trigger=&definitions[0].weapon.triggers.data[0]; trigger->first_person_weapon_offset=offset ? (real_point3d){0.2f,-0.15f,0.1f} : (real_point3d){0,0,0};
+ trigger->flags=origin_flag ? 1u<<_weapon_trigger_uses_weapon_origin_bit : 0;
+ CHECK(weapon_preview_projectile_ray(0,0,0,&preview_origin,&preview_direction,&ignore,NULL));
+ CHECK(ignore==0); CHECK(players[0].aim_assist_unit_index==41 && players[0].aim_assist_timestamp==42);
+ CHECK(time_calls==0 && actor_calls==0 && global_current_collision_user_depth==1);
+ fire_origin=fixture_marker.matrix.position; fire_direction=fixture_marker.matrix.forward;
+ target=trigger_adjust_projectile_ray(0,&units[0],trigger,&fire_origin,&fire_direction,&velocity,&error,FALSE,NULL);
+ if(origin_flag) fire_origin=fixture_marker.matrix.position;
+ normalize3d(&fire_direction);
+ point_equal(preview_origin,fire_origin); direction_equal(preview_direction,fire_direction);
+ CHECK(players[0].aim_assist_unit_index==target); CHECK(time_calls==1); CHECK(global_current_collision_user_depth==1);
+ if(assist>1) CHECK(max_depth==3); /* base1 + aim + LOS */
+ }
+ reset(); marker_count=0; CHECK(weapon_preview_projectile_ray(0,0,0,&unit_camera,&deterministic_direction,&last_ignore,NULL)); CHECK(marker_object==0);
+ reset(); weapons[0].object.flags=1u<<_object_invisible_bit; CHECK(weapon_preview_projectile_ray(0,0,0,&unit_camera,&deterministic_direction,&last_ignore,NULL)); CHECK(marker_object==0);
+}
+static void check_vehicles_and_guards(void) {
+ real_point3d point,origin; real_vector3d direction; long ignore;
+ reset(); units[0].object.parent_object_index=1; CHECK(vr_render_projectile_point(0,&point)); CHECK(last_ignore==1);
+ CHECK(weapon_preview_projectile_ray(0,0,0,&origin,&direction,&ignore,NULL)); CHECK(ignore==1);
+ reset(); inventory[0]=NONE; inventory[1]=0; fixture_aiming_unit[0]=1; weapons[0].object.parent_object_index=1; units[1].unit.gunner_object_index=0; units[0].object.parent_object_index=1;
+ CHECK(vr_render_projectile_point(0,&point)); CHECK(last_ignore==1);
+ direction_equal(fixture_marker.matrix.forward,(real_vector3d){(point.x-fixture_marker.matrix.position.x)/magnitude3d(&(real_vector3d){point.x-fixture_marker.matrix.position.x,point.y-fixture_marker.matrix.position.y,point.z-fixture_marker.matrix.position.z}),
+ (point.y-fixture_marker.matrix.position.y)/magnitude3d(&(real_vector3d){point.x-fixture_marker.matrix.position.x,point.y-fixture_marker.matrix.position.y,point.z-fixture_marker.matrix.position.z}),
+ (point.z-fixture_marker.matrix.position.z)/magnitude3d(&(real_vector3d){point.x-fixture_marker.matrix.position.x,point.y-fixture_marker.matrix.position.y,point.z-fixture_marker.matrix.position.z})});
+ CHECK(actor_calls==0 && time_calls==0);
+ reset(); CHECK(!weapon_preview_projectile_ray(NONE,0,0,&origin,&direction,&ignore,NULL));
+ CHECK(!weapon_preview_projectile_ray(0,-1,0,&origin,&direction,&ignore,NULL)); CHECK(!weapon_preview_projectile_ray(0,2,0,&origin,&direction,&ignore,NULL));
+ definitions[0].weapon.triggers.count=0; CHECK(!weapon_preview_projectile_ray(0,0,0,&origin,&direction,&ignore,NULL));
+ reset(); unit_valid[0]=0; CHECK(!vr_render_projectile_point(0,&point)); CHECK(global_current_collision_user_depth==1);
+ reset(); units[0].object.damage_flags=1u<<_object_dead_bit; CHECK(!vr_render_projectile_point(0,&point));
+ reset(); units[0].unit.player_index=1; CHECK(!vr_render_projectile_point(0,&point)); CHECK(actor_calls==0 && collision_calls==0);
+ reset(); fixture_marker.matrix.position.y=NAN; CHECK(!vr_render_projectile_point(0,&point)); CHECK(global_current_collision_user_depth==1);
+ reset(); fixture_marker.matrix.forward=(real_vector3d){0,0,0}; definitions[0].weapon.triggers.data[0].flags=1u<<_weapon_trigger_projectiles_cannot_be_aimed_bit; CHECK(!vr_render_projectile_point(0,&point));
+ reset(); params_enabled=assist_enabled=TRUE; fixture_marker.matrix.position.x=NAN;
+ CHECK(!vr_render_projectile_point(0,&point)); CHECK(aim_calls==0 && time_calls==0 && global_current_collision_user_depth==1);
+ CHECK(players[0].aim_assist_unit_index==41 && players[0].aim_assist_timestamp==42);
+ reset(); params_enabled=assist_enabled=TRUE; units[0].unit.aiming_vector.i=NAN;
+ CHECK(!vr_render_projectile_point(0,&point)); CHECK(aim_calls==0 && time_calls==0 && global_current_collision_user_depth==1);
+ reset(); params_enabled=assist_enabled=TRUE; units[0].unit.aiming_vector=(real_vector3d){FLT_MAX,FLT_MAX,0};
+ CHECK(!vr_render_projectile_point(0,&point)); CHECK(aim_calls==0 && time_calls==0 && global_current_collision_user_depth==1);
+ reset(); params_enabled=assist_enabled=TRUE; definitions[0].weapon.triggers.data[0].flags=1u<<_weapon_trigger_uses_weapon_origin_bit;
+ definitions[0].weapon.triggers.data[0].first_person_weapon_offset.x=NAN;
+ CHECK(!vr_render_projectile_point(0,&point)); CHECK(aim_calls==0 && time_calls==0 && global_current_collision_user_depth==1);
+ reset(); params_enabled=assist_enabled=TRUE; definitions[0].weapon.triggers.data[0].flags=1u<<_weapon_trigger_projectiles_cannot_be_aimed_bit;
+ fixture_marker.matrix.forward=(real_vector3d){FLT_MAX,FLT_MAX,0};
+ CHECK(!vr_render_projectile_point(0,&point)); CHECK(aim_calls==0 && time_calls==0 && global_current_collision_user_depth==1);
+
+ reset(); units[0].unit.gunner_object_index=3; unit_valid[3]=0; CHECK(!vr_render_projectile_point(0,&point));
+ reset(); weapons[0].object.parent_object_index=NONE; CHECK(!vr_render_projectile_point(0,&point));
+ reset(); inventory[0]=NONE; CHECK(!vr_render_projectile_point(0,&point)); vr_render_crosshairs(); CHECK(draw_count==0 && !vr_render.crosshairs_drawn);
+ CHECK(vr_render_hud_camera(&render.camera)); CHECK(vr_render_crosshair_offset[0]==0 && vr_render_crosshair_offset[1]==0);
+ reset(); params_enabled=assist_enabled=TRUE; global_current_collision_user_depth=MAXIMUM_COLLISION_USER_STACK_DEPTH-3;
+ CHECK(vr_render_projectile_point(0,&point)); CHECK(max_depth==MAXIMUM_COLLISION_USER_STACK_DEPTH); CHECK(global_current_collision_user_depth==MAXIMUM_COLLISION_USER_STACK_DEPTH-3);
+ reset(); params_enabled=assist_enabled=TRUE; global_current_collision_user_depth=MAXIMUM_COLLISION_USER_STACK_DEPTH-2;
+ CHECK(!vr_render_projectile_point(0,&point)); CHECK(aim_calls==0 && collision_calls==0 && time_calls==0); CHECK(global_current_collision_user_depth==MAXIMUM_COLLISION_USER_STACK_DEPTH-2);
+}
+static void check_collision_and_projection(void) {
+ real_point3d point; real walls[]={3,50,4,80}; int wall,eye;
+ reset(); params_enabled=assist_enabled=TRUE;
+ for(wall=0;wall<4;wall++) {
+ int previous_aim=aim_calls;
+ wall_x=walls[wall]; CHECK(vr_render_projectile_point(0,&point)); CHECK(near(point.x,wall_x));
+ CHECK(last_flags==_collision_test_for_projectiles_flags && last_ignore==0); CHECK(global_current_collision_user_depth==1);
+ draw_count=0; vr_render_crosshairs(); CHECK(draw_count==2 && drawn_eye==-1); CHECK(aim_calls-previous_aim==2); /* one explicit point + one draw query */
+ for(eye=0;eye<2;eye++) {
+ real eye_y=eye==0 ? -0.035f : 0.035f;
+ CHECK(near(drawn_points[eye][0], ((-(point.y-eye_y)/point.x+1)*640-1)*0.5f));
+ CHECK(near(drawn_points[eye][1], ((1-point.z/point.x)*480-1)*0.5f));
+ }
+ CHECK(vr_render.crosshairs_drawn); CHECK(vr_render_hud_camera(&render.camera)); CHECK(vr_render_crosshair_offset[0]==10000 && !vr_render.crosshairs_drawn);
+ CHECK(players[0].aim_assist_unit_index==41 && players[0].aim_assist_timestamp==42 && time_calls==0);
+ }
+ /* A moved/asymmetric eye camera changes only where the same hit projects. */
+ reset(); CHECK(vr_render_projectile_point(0,&point)); vr_render.eyes[0].world_to_view.n[3][0]=-0.2f;
+ vr_render.eyes[0].projection_matrix[2][0]=0.12f; vr_render_crosshairs();
+ CHECK(near(drawn_points[0][0], ((-(point.y+0.2f)/point.x+0.12f+1)*640-1)*0.5f));
+ CHECK(near(point.x,wall_x));
+ reset(); wall_x=1000; CHECK(vr_render_projectile_point(0,&point)); CHECK(near(point.x,fixture_marker.matrix.position.x+100));
+}
+static void check_render_pose_anchors(void) {
+ int assist,weapon_origin;
+ for(assist=0;assist<3;assist++) for(weapon_origin=0;weapon_origin<2;weapon_origin++) {
+ struct projectile_preview_pose pose;
+ real_point3d old_origin,new_origin; real_vector3d old_direction,new_direction; long old_ignore,new_ignore;
+ reset(); unit_definitions[0].unit.flags=1u<<_unit_fires_from_camera_bit;
+ params_enabled=assist>0; assist_enabled=assist>1;
+ definitions[0].weapon.triggers.data[0].first_person_weapon_offset=(real_point3d){0.2f,-0.15f,0.1f};
+ definitions[0].weapon.triggers.data[0].flags=weapon_origin ? 1u<<_weapon_trigger_uses_weapon_origin_bit : 0;
+ pose.camera_position=unit_camera; pose.forward=units[0].unit.aiming_vector;
+ CHECK(weapon_preview_projectile_ray(0,0,0,&old_origin,&old_direction,&old_ignore,NULL));
+ CHECK(weapon_preview_projectile_ray(0,0,0,&new_origin,&new_direction,&new_ignore,&pose));
+ point_equal(old_origin,new_origin); direction_equal(old_direction,new_direction); CHECK(old_ignore==new_ignore);
+ CHECK(time_calls==0 && actor_calls==0 && global_current_collision_user_depth==1);
+ CHECK(players[0].aim_assist_unit_index==41 && players[0].aim_assist_timestamp==42);
+ }
+}
+static void check_render_frame_refresh(void) {
+ struct unit_datum simulation;
+ struct projectile_preview_pose pose;
+ real_point3d point;
+ real last_y=-1000;
+ int frame;
+ reset(); unit_definitions[0].unit.flags=1u<<_unit_fires_from_camera_bit; params_enabled=TRUE;
+ simulation=units[0]; render.camera.position=(real_point3d){9,9,9}; /* Presentation shake must not become a shot origin. */
+ for(frame=0;frame<12;frame++) {
+ real yaw=-0.25f+0.04f*(real)frame;
+ int old_aim=aim_calls;
+ /* A fixed 30 Hz simulation sample, twelve distinct displayed poses. */
+ vr_render.view.hand_forward[0]=cosf(yaw); vr_render.view.hand_forward[1]=sinf(yaw); vr_render.view.hand_forward[2]=0;
+ fixture_observer.position=(real_point3d){0.01f*(real)frame,0.02f*(real)frame,0.5f};
+ CHECK(vr_render_projectile_pose(0,&units[0],&pose));
+ direction_equal(pose.forward,(real_vector3d){cosf(yaw),sinf(yaw),0});
+ point_equal(pose.camera_position,fixture_observer.position);
+ CHECK(vr_render_projectile_point(0,&point)); CHECK(aim_calls-old_aim==1);
+ CHECK(near(point.x,wall_x)); CHECK(near(point.y,fixture_observer.position.y+(wall_x-fixture_observer.position.x)*tanf(yaw)));
+ CHECK(point.y>last_y); last_y=point.y;
+ point_equal(fixture_assist_camera,fixture_observer.position); direction_equal(fixture_assist_forward,pose.forward);
+ /* The paired render camera moves, but the original deterministic
+ camera-height displacement must not acquire a horizontal lag. */
+ point_equal(fixture_aim_trace_origin,(real_point3d){fixture_observer.position.x+pose.forward.i*0.5f,
+ fixture_observer.position.y+pose.forward.j*0.5f,fixture_observer.position.z+pose.forward.k*0.5f});
+ CHECK(memcmp(&units[0],&simulation,sizeof(simulation))==0);
+ CHECK(time_calls==0 && actor_calls==0 && global_current_collision_user_depth==1);
+ CHECK(players[0].aim_assist_unit_index==41 && players[0].aim_assist_timestamp==42);
+ old_aim=aim_calls; draw_count=0; vr_render_crosshairs(); CHECK(aim_calls-old_aim==1 && draw_count==2);
+ CHECK(near(drawn_points[0][0], ((-(point.y+0.035f)/point.x+1)*640-1)*0.5f));
+ CHECK(near(drawn_points[1][0], ((-(point.y-0.035f)/point.x+1)*640-1)*0.5f));
+ }
+ /* Gamepad facing also changes every rendered frame without changing unit aim. */
+ reset(); unit_definitions[0].unit.flags=1u<<_unit_fires_from_camera_bit;
+ vr_render.view.controller_aim=FALSE; vr_render.view.gamepad_aim=TRUE;
+ for(frame=0;frame<6;frame++) {
+ real yaw=0.1f*(real)frame;
+ fixture_facing=(real_vector3d){cosf(yaw),sinf(yaw),0};
+ CHECK(vr_render_projectile_pose(0,&units[0],&pose)); direction_equal(pose.forward,fixture_facing);
+ CHECK(vr_render_projectile_point(0,&point)); CHECK(near(point.y,wall_x*tanf(yaw)));
+ direction_equal(units[0].unit.aiming_vector,(real_vector3d){1,0,0});
+ }
+}
+static void unarmed(void) {
+ reset(); inventory[0]=NONE; fixture_weapon_count[0]=0;
+ unit_definitions[0].unit.flags=1u<<_unit_fires_from_camera_bit;
+ vr_render.view.hand_forward[0]=1; vr_render.view.hand_forward[1]=0;
+ fixture_live_view=vr_render.view;
+}
+static void check_unarmed_refresh_and_transition(void) {
+ struct unit_datum simulation;
+ struct player_datum player_state;
+ real_point3d origin,point,armed_origin; real_vector3d direction,armed_direction;
+ struct projectile_preview_pose pose;
+ long ignore,armed_ignore; int frame;
+ unarmed(); simulation=units[0]; player_state=players[0];
+ /* No weapon, trigger or projectile metadata is available. */
+ weapon_valid[0]=FALSE; definitions[0].weapon.triggers.count=0;
+ params_enabled=assist_enabled=TRUE;
+ render.camera.position=(real_point3d){9,9,9};
+ for(frame=0;frame<12;frame++) {
+ real yaw=-0.25f+0.04f*(real)frame;
+ int previous_collision;
+ vr_render.view.hand_forward[0]=cosf(yaw); vr_render.view.hand_forward[1]=sinf(yaw);
+ fixture_observer.position=(real_point3d){0.01f*(real)frame,0.02f*(real)frame,0.5f};
+ CHECK(vr_render_projectile_ray(0,&origin,&direction,&ignore));
+ point_equal(origin,fixture_observer.position);
+ direction_equal(direction,(real_vector3d){cosf(yaw),sinf(yaw),0}); CHECK(ignore==0);
+ wall_x=frame%2 ? 40.0f : 4.0f;
+ CHECK(vr_render_projectile_point(0,&point)); CHECK(near(point.x,wall_x));
+ CHECK(near(point.y,origin.y+(wall_x-origin.x)*tanf(yaw)));
+ CHECK(last_ignore==0 && last_flags==_collision_test_for_projectiles_flags);
+ previous_collision=collision_calls; draw_count=0; vr_render_crosshairs();
+ CHECK(collision_calls==previous_collision+1 && draw_count==2 && vr_render.crosshairs_drawn);
+ CHECK(near(drawn_points[0][0], ((-(point.y+0.035f)/point.x+1)*640-1)*0.5f));
+ CHECK(near(drawn_points[1][0], ((-(point.y-0.035f)/point.x+1)*640-1)*0.5f));
+ CHECK(vr_render_hud_camera(&render.camera));
+ CHECK(vr_render_crosshair_offset[0]==10000 && !vr_render.crosshairs_drawn);
+ CHECK(aim_calls==0 && marker_object==NONE && actor_calls==0 && time_calls==0);
+ CHECK(global_current_collision_user_depth==1);
+ CHECK(memcmp(&units[0],&simulation,sizeof(simulation))==0);
+ CHECK(memcmp(&players[0],&player_state,sizeof(player_state))==0);
+ }
+ /* Acquiring a weapon immediately restores its nominal origin/aim rules. */
+ inventory[0]=0; fixture_weapon_count[0]=1; weapon_valid[0]=TRUE;
+ definitions[0].weapon.triggers.count=1;
+ definitions[0].weapon.triggers.data[0].first_person_weapon_offset=(real_point3d){0.4f,0.2f,0.1f};
+ CHECK(vr_render_projectile_pose(0,&units[0],&pose));
+ CHECK(weapon_preview_projectile_ray(0,0,0,&armed_origin,&armed_direction,&armed_ignore,&pose));
+ CHECK(vr_render_projectile_ray(0,&origin,&direction,&ignore));
+ point_equal(origin,armed_origin); direction_equal(direction,armed_direction); CHECK(ignore==armed_ignore);
+ CHECK(aim_calls==2 && marker_object==0 && time_calls==0);
+ /* Gamepad unarmed pointing also refreshes without changing simulation aim. */
+ unarmed(); vr_render.view.controller_aim=FALSE; vr_render.view.gamepad_aim=TRUE;
+ for(frame=0;frame<6;frame++) {
+ real yaw=0.1f*(real)frame;
+ fixture_facing=(real_vector3d){cosf(yaw),sinf(yaw),0};
+ CHECK(vr_render_projectile_point(0,&point)); CHECK(near(point.y,wall_x*tanf(yaw)));
+ direction_equal(units[0].unit.aiming_vector,(real_vector3d){1,0,0});
+ CHECK(aim_calls==0 && time_calls==0 && global_current_collision_user_depth==1);
+ }
+ unarmed(); wall_x=1000; fixture_observer.position=(real_point3d){0.2f,0.3f,0.5f};
+ CHECK(vr_render_projectile_point(0,&point));
+ point_equal(point,(real_point3d){100.2f,0.3f,0.5f}); CHECK(collision_calls==1 && aim_calls==0);
+}
+static void check_unarmed_guards(void) {
+ real_point3d point,origin; real_vector3d direction; long ignore;
+ unarmed(); fixture_weapon_count[0]=1; CHECK(!vr_render_projectile_point(0,&point));
+ vr_render_crosshairs(); CHECK(draw_count==0 && !vr_render.crosshairs_drawn);
+ CHECK(vr_render_hud_camera(&render.camera)); CHECK(vr_render_crosshair_offset[0]==0);
+ unarmed(); units[0].unit.player_index=NONE; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); units[0].unit.player_index=1; CHECK(!vr_render_projectile_point(0,&point));
+ origin=(real_point3d){7,8,9}; direction=(real_vector3d){3,4,5}; ignore=123;
+ CHECK(!vr_render_projectile_ray(0,&origin,&direction,&ignore));
+ point_equal(origin,(real_point3d){7,8,9}); direction_equal(direction,(real_vector3d){3,4,5}); CHECK(ignore==123);
+ unarmed(); players[0].unit_index=NONE; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); unit_valid[0]=FALSE; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); units[0].object.damage_flags=1u<<_object_dead_bit; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); units[0].object.parent_object_index=1; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); units[0].unit.gunner_object_index=2; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); units[0].object.type=_object_type_vehicle; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); fixture_local_player=NONE; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); fixture_inhibited=TRUE; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); fixture_perspective=0; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); fixture_cinematic=TRUE; CHECK(!vr_render_projectile_point(0,&point));
+ vr_render_crosshairs(); CHECK(draw_count==0 && !vr_render.crosshairs_drawn);
+ unarmed(); fixture_vr_aiming=FALSE; vr_render_crosshairs(); CHECK(draw_count==0);
+ unarmed(); vr_render.active=FALSE; vr_render_crosshairs(); CHECK(draw_count==0);
+ unarmed(); fixture_observer_valid=FALSE; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); fixture_observer.position.x=NAN; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); vr_render.view.controller_aim=FALSE; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); vr_render.view.hand_forward[0]=NAN; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); vr_render.view.hand_forward[0]=INFINITY; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); vr_render.view.hand_forward[0]=0; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); unit_definitions[0].unit.flags=0; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); unit_definitions[0].unit.aiming_velocity_maximum=1; CHECK(!vr_render_projectile_point(0,&point));
+ unarmed(); unit_definitions[0].unit.aiming_acceleration_maximum=1; CHECK(!vr_render_projectile_point(0,&point));
+ /* Existing but unsupported weapons must never become an unarmed ray. */
+ unarmed(); inventory[0]=0; definitions[0].weapon.triggers.count=0;
+ CHECK(!vr_render_projectile_point(0,&point)); CHECK(aim_calls==0 && collision_calls==0);
+ unarmed(); global_current_collision_user_depth=MAXIMUM_COLLISION_USER_STACK_DEPTH-2;
+ CHECK(!vr_render_projectile_point(0,&point)); CHECK(collision_calls==0);
+ CHECK(global_current_collision_user_depth==MAXIMUM_COLLISION_USER_STACK_DEPTH-2);
+ /* Bounds remain shared with render-rate armed pointing. */
+ unarmed(); vr_render.view.hand_forward[0]=0; vr_render.view.hand_forward[2]=1;
+ CHECK(vr_render_projectile_ray(0,&origin,&direction,&ignore));
+ CHECK(near(direction.k,sinf(DEGREES_TO_RADIANS(85.0f))));
+ fixture_clip_pitch=0.25f; CHECK(vr_render_projectile_ray(0,&origin,&direction,&ignore));
+ CHECK(near(direction.k,sinf(0.25f)) && fixture_clip_calls==2);
+}
+static void check_script_pointing_ray(void) {
+ struct unit_datum simulation;
+ struct halo_vr_view cached_view;
+ real_point3d origin,render_origin; real_vector3d forward,render_forward; long owner;
+ unarmed(); simulation=units[0]; cached_view=vr_render.view;
+ vr_render.active=FALSE; /* Scripts run after the eye pass has ended. */
+ fixture_live_view.body_yaw=0.3f;
+ fixture_live_view.hand_forward[0]=cosf(0.8f); fixture_live_view.hand_forward[1]=sinf(0.8f);
+ fixture_observer.position=(real_point3d){0.2f,0.3f,0.5f};
+ CHECK(vr_render_player_pointing_ray(0,&origin,&forward));
+ point_equal(origin,fixture_observer.position);
+ direction_equal(forward,(real_vector3d){cosf(1.1f),sinf(1.1f),0});
+ CHECK(memcmp(&vr_render.view,&cached_view,sizeof(cached_view))==0 && !vr_render.active);
+ CHECK(memcmp(&units[0],&simulation,sizeof(simulation))==0);
+ CHECK(collision_calls==0 && aim_calls==0 && marker_object==NONE && time_calls==0);
+ CHECK(players[0].aim_assist_unit_index==41 && players[0].aim_assist_timestamp==42);
+ /* The next rendered view uses precisely the same unarmed ray. */
+ vr_render.view=fixture_live_view; vr_render.base_yaw=fixture_live_view.body_yaw;
+ CHECK(vr_render_projectile_ray(0,&render_origin,&render_forward,&owner));
+ point_equal(origin,render_origin); direction_equal(forward,render_forward); CHECK(owner==0);
+ fixture_tracking_valid=FALSE; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ fixture_tracking_valid=TRUE; fixture_live_view_valid=FALSE; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ fixture_live_view_valid=TRUE; fixture_vr_aiming=FALSE; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); CHECK(!vr_render_player_pointing_ray(1,&origin,&forward));
+ CHECK(!vr_render_player_pointing_ray(NONE,&origin,&forward));
+ CHECK(!vr_render_player_pointing_ray(0,NULL,&forward)); CHECK(!vr_render_player_pointing_ray(0,&origin,NULL));
+ unarmed(); fixture_local_player=NONE; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); fixture_weapon_count[0]=1; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); inventory[0]=0; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); units[0].unit.player_index=1; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); units[0].object.damage_flags=1u<<_object_dead_bit; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); units[0].object.parent_object_index=1; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); fixture_cinematic=TRUE; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); fixture_inhibited=TRUE; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); fixture_perspective=0; CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ unarmed(); fixture_live_view.hand_forward[0]=NAN;
+ origin=(real_point3d){7,8,9}; forward=(real_vector3d){3,4,5};
+ CHECK(!vr_render_player_pointing_ray(0,&origin,&forward));
+ point_equal(origin,(real_point3d){7,8,9}); direction_equal(forward,(real_vector3d){3,4,5});
+}
+static void check_script_hmd_gaze(void) {
+ struct unit_datum simulation;
+ struct halo_vr_view cached_view;
+ real_point3d origin,expected_position; real_vector3d forward,expected_forward;
+ int frame;
+ reset(); simulation=units[0]; cached_view=vr_render.view; vr_render.active=FALSE;
+ for(frame=0;frame<12;frame++) {
+ real yaw=0.1f*(real)frame,head_yaw=-0.3f+0.02f*(real)frame;
+ real pitch=frame>=6 ? 0.15f : 0;
+ real px,py,pz;
+ fixture_live_view.body_yaw=yaw;
+ fixture_live_view.head_forward[0]=cosf(head_yaw); fixture_live_view.head_forward[1]=sinf(head_yaw);
+ fixture_live_view.head_forward[2]=0;
+ fixture_live_view.head_position[0]=0.1f; fixture_live_view.head_position[1]=0.2f; fixture_live_view.head_position[2]=0.3f;
+ fixture_live_view.gamepad_pitch=frame>=6; fixture_facing_angles.pitch=pitch;
+ fixture_observer.position=(real_point3d){0.01f*(real)frame,0.02f*(real)frame,0.5f};
+ CHECK(vr_render_player_gaze(0,&origin,&forward));
+ px=cosf(head_yaw)*cosf(pitch); py=sinf(head_yaw); pz=cosf(head_yaw)*sinf(pitch);
+ expected_forward=(real_vector3d){cosf(yaw)*px-sinf(yaw)*py,sinf(yaw)*px+cosf(yaw)*py,pz};
+ direction_equal(forward,expected_forward);
+ px=0.1f*cosf(pitch)-0.3f*sinf(pitch); py=0.2f; pz=0.1f*sinf(pitch)+0.3f*cosf(pitch);
+ expected_position=(real_point3d){fixture_observer.position.x+2*(cosf(yaw)*px-sinf(yaw)*py),
+ fixture_observer.position.y+2*(sinf(yaw)*px+cosf(yaw)*py),fixture_observer.position.z+2*pz};
+ point_equal(origin,expected_position);
+ CHECK(memcmp(&units[0],&simulation,sizeof(simulation))==0);
+ CHECK(memcmp(&vr_render.view,&cached_view,sizeof(cached_view))==0 && !vr_render.active);
+ CHECK(aim_calls==0 && collision_calls==0 && marker_object==NONE && time_calls==0);
+ }
+ /* Without current VR aiming, the rendered camera supplies the base yaw. */
+ reset(); fixture_vr_aiming=FALSE; fixture_live_view.body_yaw=-2;
+ fixture_observer.forward=(real_vector3d){0,1,0};
+ CHECK(vr_render_player_gaze(0,&origin,&forward)); direction_equal(forward,(real_vector3d){0,1,0});
+ /* Vehicle first-person uses the same seated head origin as rendering. */
+ reset(); units[0].object.parent_object_index=1; units[1].object.type=_object_type_vehicle;
+ fixture_live_view.vehicle_first_person=TRUE; fixture_live_view.head_position[0]=0.2f;
+ CHECK(vr_render_player_gaze(0,&origin,&forward));
+ point_equal(origin,(real_point3d){3.4f,4,5});
+ fixture_live_view.vehicle_first_person=FALSE; CHECK(vr_render_player_gaze(0,&origin,&forward));
+ point_equal(origin,(real_point3d){0.4f,0,0.5f});
+ reset(); CHECK(!vr_render_player_gaze(NONE,&origin,&forward));
+ CHECK(!vr_render_player_gaze(1,&origin,&forward));
+ CHECK(!vr_render_player_gaze(0,NULL,&forward)); CHECK(!vr_render_player_gaze(0,&origin,NULL));
+ reset(); fixture_local_player=NONE; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); units[0].unit.player_index=1; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); unit_valid[0]=FALSE; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); units[0].object.damage_flags=1u<<_object_dead_bit; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); units[0].object.type=_object_type_vehicle; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_cinematic=TRUE; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_scripted_camera=TRUE; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_perspective=_director_perspective_scripted; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_perspective=_director_perspective_neutral; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_inhibited=TRUE; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_tracking_valid=FALSE; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_live_view_valid=FALSE; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_observer_valid=FALSE; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_observer.position.x=NAN; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_live_view.body_yaw=NAN; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_live_view.world_units_per_metre=0; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_live_view.head_position[0]=INFINITY; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_live_view.head_forward[0]=0; CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ reset(); fixture_live_view.gamepad_pitch=TRUE; fixture_facing_angles.pitch=NAN;
+ origin=(real_point3d){7,8,9}; forward=(real_vector3d){3,4,5};
+ CHECK(!vr_render_player_gaze(0,&origin,&forward));
+ point_equal(origin,(real_point3d){7,8,9}); direction_equal(forward,(real_vector3d){3,4,5});
+}
+static void check_rigid_vehicle_gaze(void) {
+ struct halo_vr_view saved;
+ real_point3d origin; real_vector3d forward; float offset[3];
+ float axes[3][3]={{.70710678f,0,.70710678f},{-.35355339f,.86602540f,.35355339f},{-.61237243f,-.5f,.61237243f}};
+ reset(); saved=vr_render.view; vr_render.active=FALSE;
+ units[0].object.parent_object_index=1; units[1].object.type=_object_type_vehicle;
+ fixture_live_view.vehicle_first_person=TRUE; fixture_live_view.vehicle_basis_valid=TRUE;
+ CHECK(halo_vr_vehicle_basis(axes,0,0,fixture_live_view.vehicle_basis));
+ fixture_live_view.head_position[0]=.1f; fixture_live_view.head_position[1]=.2f; fixture_live_view.head_position[2]=.3f;
+ CHECK(vr_render_player_gaze(0,&origin,&forward));
+ direction_equal(forward,(real_vector3d){axes[0][0],axes[0][1],axes[0][2]});
+ halo_vr_vehicle_rotate(fixture_live_view.vehicle_basis,fixture_live_view.head_position,offset);
+ point_equal(origin,(real_point3d){3+offset[0]*2,4+offset[1]*2,5+offset[2]*2});
+ CHECK(!memcmp(&saved,&vr_render.view,sizeof(saved))&&!vr_render.active);
+ CHECK(!aim_calls&&!collision_calls);
+}
+static void check_pose_limits_and_fallbacks(void) {
+ struct projectile_preview_pose pose;
+ real_point3d origin,baseline_origin; real_vector3d direction,baseline_direction; long ignore;
+ reset(); unit_definitions[0].unit.flags=1u<<_unit_fires_from_camera_bit;
+ vr_render.view.hand_forward[0]=0; vr_render.view.hand_forward[1]=0; vr_render.view.hand_forward[2]=1;
+ CHECK(vr_render_projectile_pose(0,&units[0],&pose)); CHECK(near(pose.forward.k,sinf(DEGREES_TO_RADIANS(85.0f))));
+ CHECK(near(pose.forward.i,cosf(DEGREES_TO_RADIANS(85.0f))));
+ vr_render.view.hand_forward[2]=-1; CHECK(vr_render_projectile_pose(0,&units[0],&pose)); CHECK(near(pose.forward.k,-sinf(DEGREES_TO_RADIANS(85.0f))));
+ fixture_clip_pitch=0.25f; CHECK(vr_render_projectile_pose(0,&units[0],&pose)); CHECK(near(pose.forward.k,-sinf(0.25f)));
+ CHECK(fixture_clip_calls==3);
+ fixture_clip_pitch=0; vr_render.view.hand_forward[0]=1; vr_render.view.hand_forward[2]=0; vr_render.base_yaw=0.75f;
+ CHECK(vr_render_projectile_pose(0,&units[0],&pose)); direction_equal(pose.forward,(real_vector3d){cosf(0.75f),sinf(0.75f),0});
+ fixture_inhibited=1; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); fixture_inhibited=0;
+ fixture_perspective=0; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); fixture_perspective=_director_perspective_first_person;
+ fixture_observer_valid=0; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); fixture_observer_valid=1;
+ fixture_observer.position.x=NAN; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); fixture_observer.position=unit_camera;
+ vr_render.view.hand_forward[0]=NAN; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); vr_render.view.hand_forward[0]=1;
+ unit_definitions[0].unit.aiming_velocity_maximum=1; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); unit_definitions[0].unit.aiming_velocity_maximum=0;
+ unit_definitions[0].unit.aiming_acceleration_maximum=1; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); unit_definitions[0].unit.aiming_acceleration_maximum=0;
+ units[0].object.parent_object_index=1; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); units[0].object.parent_object_index=NONE;
+ units[0].object.type=_object_type_vehicle; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); units[0].object.type=_object_type_biped;
+ units[0].unit.gunner_object_index=2; CHECK(!vr_render_projectile_pose(0,&units[0],&pose)); units[0].unit.gunner_object_index=NONE;
+ pose.camera_position=(real_point3d){0,0,0.5f}; pose.forward=(real_vector3d){0,1,0};
+ CHECK(weapon_preview_projectile_ray(0,0,0,&baseline_origin,&baseline_direction,&ignore,NULL));
+ unit_definitions[0].unit.aiming_velocity_maximum=1;
+ CHECK(weapon_preview_projectile_ray(0,0,0,&origin,&direction,&ignore,&pose)); point_equal(origin,baseline_origin); direction_equal(direction,baseline_direction);
+ unit_definitions[0].unit.aiming_velocity_maximum=0; pose.forward=(real_vector3d){NAN,0,0};
+ CHECK(!weapon_preview_projectile_ray(0,0,0,&origin,&direction,&ignore,&pose)); CHECK(time_calls==0 && actor_calls==0 && global_current_collision_user_depth==1);
+ pose.forward=(real_vector3d){0,0,0}; CHECK(!weapon_preview_projectile_ray(0,0,0,&origin,&direction,&ignore,&pose));
+ pose.forward=(real_vector3d){0,2,0}; CHECK(weapon_preview_projectile_ray(0,0,0,&origin,&direction,&ignore,&pose)); CHECK(near(magnitude3d(&direction),1));
+ pose.camera_position.x=NAN; CHECK(!weapon_preview_projectile_ray(0,0,0,&origin,&direction,&ignore,&pose));
+}
+static void check_menu_reticle_visibility(void) {
+ int mode,local;
+ for(mode=0;mode<2;mode++) {
+ reset(); vr_render.view.controller_aim=mode==0; vr_render.view.gamepad_aim=mode==1;
+ fixture_widgets[0]=TRUE;
+ CHECK(!vr_render_gameplay_hud_visible(0));
+ vr_render_crosshairs();
+ CHECK(draw_count==0 && collision_calls==0 && aim_calls==0 && !vr_render.crosshairs_drawn);
+ /* Current widgets suppress the first menu frame even if aim is still
+ * active from simulation. Closing resumes the usual nominal reticle. */
+ CHECK(fixture_vr_aiming);
+ fixture_widgets[0]=FALSE; CHECK(vr_render_gameplay_hud_visible(0));
+ vr_render_crosshairs(); CHECK(draw_count==2 && vr_render.crosshairs_drawn);
+ }
+ for(local=0;local<MAXIMUM_NUMBER_OF_LOCAL_PLAYERS;local++) {
+ reset(); fixture_widgets[local]=TRUE;
+ CHECK(!vr_render_gameplay_hud_visible((short)local));
+ CHECK(vr_render_gameplay_hud_visible((short)((local+1)%MAXIMUM_NUMBER_OF_LOCAL_PLAYERS)));
+ fixture_frame_active=FALSE; CHECK(vr_render_gameplay_hud_visible((short)local));
+ }
+ reset(); CHECK(vr_render_gameplay_hud_visible(NONE));
+ CHECK(vr_render_gameplay_hud_visible(MAXIMUM_NUMBER_OF_LOCAL_PLAYERS));
+}
+static void check_controller_reticle_ownership(void){
+ int mode,invalid;real_point3d point;struct render_camera camera;
+ for(invalid=0;invalid<5;invalid++){
+ reset();vr_render.view.controller_aim_requested=TRUE;unit_definitions[0].unit.flags=1u<<_unit_fires_from_camera_bit;
+ switch(invalid){case 0:vr_render.view.controller_aim=FALSE;break;case 1:fixture_tracking_valid=FALSE;break;
+ case 2:vr_render.view.hand_forward[0]=-1;wall_x=-200;break; /* valid aim behind headset */
+ case 3:definitions[0].weapon.triggers.count=0;break; /* unsupported preview */
+ case 4:vr_render.active=FALSE;break; /* failed/ended world view, VR frame still active */}
+ vr_render_crosshairs();CHECK(draw_count==0&&!vr_render.crosshairs_drawing);
+ if(invalid==2)CHECK(vr_render.view.controller_aim&&vr_render_projectile_point(0,&point)&&point.x<0);
+ CHECK(vr_render_gameplay_hud_visible(0)); /* keep ammo/shields/zoom overlays */
+ vr_render.active=FALSE;camera=render.camera;CHECK(halo_vr_frame_active());
+ CHECK(vr_render_hud_camera(&camera));CHECK(!vr_render_hud_crosshairs_visible(0));
+ test_hud_crosshair_dispatch(&players[0]);CHECK(hud_glyph_calls==0);
+ }
+ /* A valid controller ray outside the camera frustum remains world-space
+ * and may clip naturally; it never relocates to the HUD centre. */
+ reset();vr_render.view.controller_aim_requested=TRUE;unit_definitions[0].unit.flags=1u<<_unit_fires_from_camera_bit;
+ vr_render.view.hand_forward[0]=.1f;vr_render.view.hand_forward[1]=.9949874f;wall_x=200;
+ vr_render_crosshairs();CHECK(draw_count==2&&vr_render.crosshairs_drawn&&!vr_render.crosshairs_drawing);
+ CHECK(fabsf(drawn_points[0][0]-320)>320&&fabsf(drawn_points[1][0]-320)>320);
+ vr_render.active=FALSE;test_hud_crosshair_dispatch(&players[0]);CHECK(hud_glyph_calls==2);
+ vr_render.active=TRUE;vr_render.view.controller_aim=FALSE;draw_count=0;hud_glyph_calls=0;
+ vr_render_crosshairs();CHECK(draw_count==0);test_hud_crosshair_dispatch(&players[0]);CHECK(hud_glyph_calls==0);
+ vr_render.view.controller_aim=TRUE;vr_render.view.hand_forward[0]=1;vr_render.view.hand_forward[1]=0;
+ vr_render_crosshairs();CHECK(draw_count==2&&hud_glyph_calls==2); /* fresh recovery */
+ /* The actual common glyph-dispatch gate preserves intentional head,
+ * gamepad, nonlocal and flat behavior after the eye pass has ended. */
+ for(mode=0;mode<3;mode++){
+ reset();vr_render.active=FALSE;vr_render.view.controller_aim_requested=mode==0;
+ vr_render.view.gamepad_aim=mode==2;
+ test_hud_crosshair_dispatch(&players[0]);CHECK(hud_glyph_calls==(mode!=0));
+ fixture_widgets[0]=TRUE;hud_glyph_calls=0;test_hud_crosshair_dispatch(&players[0]);CHECK(!hud_glyph_calls);
+ fixture_widgets[0]=FALSE;fixture_frame_active=FALSE;test_hud_crosshair_dispatch(&players[0]);CHECK(hud_glyph_calls==1);
+ }
+ reset();vr_render.view.controller_aim_requested=TRUE;players[0].local_player_index=1;
+ test_hud_crosshair_dispatch(&players[0]);CHECK(hud_glyph_calls==1);
+}
+int main(void) {
+ check_menu_reticle_visibility();
+ check_controller_reticle_ownership();
+ check_render_pose_anchors(); check_render_frame_refresh(); check_pose_limits_and_fallbacks();
+ check_unarmed_refresh_and_transition(); check_unarmed_guards();
+ check_script_pointing_ray();
+ check_script_hmd_gaze(); check_rigid_vehicle_gaze();
+ check_ray_equivalence(); check_vehicles_and_guards(); check_collision_and_projection();
+ printf("PASS: %d production projectile/reticle assertions\n",checks); return 0;
+}
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ parser.add_argument('--self-test', action='store_true', help='Verify the regression rejects the old ungated HUD fallback.')
+ args = parser.parse_args()
+ weapons = (ROOT / 'source/items/weapons.c').read_text()
+ aim = (ROOT / 'source/game/aim_assist.c').read_text()
+ render = (ROOT / 'port/linux/game/vr_render.c').read_text()
+ hud_weapon = (ROOT / 'source/interface/hud_weapon.c').read_text()
+ units = (ROOT / 'source/units/units.c').read_text()
+ math = (ROOT / 'source/math/real_math.c').read_text()
+ verify_refactors(weapons, aim, render, units)
+ harness = ('#include \"' + (ROOT / 'port/linux/src/vr_vehicle.h').as_posix() + '\"\n' + HARNESS).replace('@@LOS@@', function(aim, 'aim_assist_clear_line_of_sight'))
+ glyphs = function(hud_weapon, 'crosshairs_draw')
+ guard_start = glyphs.index('#ifdef HALO_VR')
+ guard_end = glyphs.index('#endif', guard_start) + len('#endif')
+ # Execute the actual common dispatch gate, with the glyph renderer below
+ # it as the seam. All unmodified stock glyph logic is checked byte-for-byte
+ # against the pinned source by test_vr_zoom_aim.py.
+ gate = 'static void test_hud_crosshair_dispatch(struct player_datum *player){\n' + glyphs[guard_start:guard_end] + '\n hud_glyph_calls++;\n}'
+ harness = harness.replace('@@HUDGATE@@', gate)
+ groups = {
+ '@@MATH@@': (math, ['fast_normals_interpolate', 'pin_normal_to_cone3d']),
+ '@@AIM@@': (aim, ['player_aim_projectile_internal', 'player_aim_projectile', 'player_preview_projectile_aim']),
+ '@@UNIT@@': (units, ['unit_adjust_projectile_ray_internal', 'unit_adjust_projectile_ray', 'unit_preview_projectile_ray']),
+ '@@WEAPON_HELPERS@@': (weapons, ['weapon_get_effect_object_index', 'weapon_get_owner_object_index', 'weapon_get_projectile_owner_object_index', 'trigger_adjust_projectile_ray']),
+ '@@PREVIEW@@': (weapons, ['weapon_preview_projectile_ray']),
+ '@@RENDER@@': (render, ['frustum_clip_transform', 'yaw_rotate', 'view_rotate', 'view_vector', 'seated_unit', 'vr_render_player_gaze', 'vr_render_hud_camera', 'vr_render_pointing_pose', 'vr_render_projectile_pose', 'vr_render_unarmed_ray', 'vr_render_player_pointing_ray', 'vr_render_projectile_ray', 'vr_render_projectile_point', 'vr_render_gameplay_hud_visible', 'vr_render_hud_crosshairs_visible', 'vr_render_crosshairs']),
+ }
+ for placeholder, (text, names) in groups.items():
+ harness = harness.replace(placeholder, '\n\n'.join(function(text, name) for name in names))
+ compiler = args.cc or os.environ.get('CC')
+ vcvars = args.vcvarsall
+ if not compiler and os.name == 'nt':
+ installations = Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio'
+ candidates = sorted(installations.glob('*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1]); vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('No C11 host compiler found; select one with --cc.')
+ with tempfile.TemporaryDirectory(prefix='halo-vr-projectile-check-', ignore_cleanup_errors=True) as directory:
+ temp = Path(directory); source = temp / 'test.c'; executable = temp / ('test.exe' if os.name == 'nt' else 'test')
+ source.write_text(harness)
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX', '/DHALO_VR', '/D_CRT_SECURE_NO_WARNINGS',
+ '/Fe:' + str(executable), '/Fo:' + str(temp / 'test.obj'), str(source)]
+ if vcvars:
+ for path in (vcvars, Path(compiler), temp):
+ if any(character in str(path) for character in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported shell metacharacter in compiler/temp path.')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\nif errorlevel 1 exit /b 1\n' +
+ subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-DHALO_VR', str(source)]
+ if os.name != 'nt':
+ command.append('-lm')
+ command.extend(['-o', str(executable)])
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(executable)], cwd=temp, check=True)
+ if args.self_test:
+ old_dispatch = 'static void test_hud_crosshair_dispatch(struct player_datum *player){\n(void)player;hud_glyph_calls++;\n}'
+ source.write_text(harness.replace(gate, old_dispatch))
+ subprocess.run(command, cwd=temp, check=True, capture_output=True, text=True)
+ result = subprocess.run([str(executable)], cwd=temp, capture_output=True, text=True)
+ assert result.returncode != 0 and 'hud_glyph_calls==0' in result.stderr, result.stderr
+ print('PASS: regression rejects the former unguarded centred-HUD fallback')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_renderer_link.py b/tools/test_vr_renderer_link.py
new file mode 100644
index 0000000000000000000000000000000000000000..eabb851287bd8848a60af74d08090d817290e085
--- /dev/null
+++ b/tools/test_vr_renderer_link.py
@@ -0,0 +1,211 @@
+#!/usr/bin/env python3
+"""Audit production renderer GL imports with real ARM guest objects.
+
+Runs from a source tarball without Git, configuration, musl, or SDL libraries.
+Uses installed Khronos declarations (or already-local SDL SDK headers on
+Windows), the production guest ABI, GL generators, pointer loader and import
+thunks. This is a relocatable partial link, not a full game or hardware test.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import struct
+import subprocess
+import sys
+import tempfile
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT))
+from tools import android_build
+from tools.test_vr_projectile_reticle import function
+
+
+def symbols(path):
+ data = path.read_bytes()
+ assert data[:7] == b'\x7fELF\x02\x01\x01'
+ assert struct.unpack_from('<HH', data, 16) == (1, 183), path
+ offset = struct.unpack_from('<Q', data, 40)[0]
+ size, count = struct.unpack_from('<HH', data, 58)
+ assert size == 64 and 0 < count < 65536 and offset <= len(data)-size*count
+ sections = [struct.unpack_from('<IIQQQQIIQQ', data, offset+i*size) for i in range(count)]
+ result = {}
+ for section in sections:
+ if section[1] != 2:
+ continue
+ start, length, link, stride = section[4], section[5], section[6], section[9]
+ assert stride == 24 and length % stride == 0 and start <= len(data)-length and link < count
+ strings = sections[link]
+ assert strings[1] == 3 and strings[4] <= len(data)-strings[5]
+ names = data[strings[4]:strings[4]+strings[5]]
+ for p in range(start, start+length, stride):
+ name, info, _, index, _, _ = struct.unpack_from('<IBBHQQ', data, p)
+ if not name or info >> 4 not in (1, 2):
+ continue
+ assert name < len(names)
+ end = names.find(b'\0', name)
+ assert end >= 0
+ result[names[name:end].decode('ascii')] = index != 0
+ assert result, path
+ return result
+
+
+def run(argv):
+ result = subprocess.run([str(x) for x in argv], cwd=ROOT, text=True, capture_output=True)
+ if result.returncode:
+ raise AssertionError(' '.join(str(x) for x in argv) + '\n' + result.stdout + result.stderr)
+
+
+def strip_comments(text):
+ return re.sub(r'/\*.*?\*/|//[^\n]*', '', text, flags=re.S)
+
+
+def has_provider(text, name, allow_static=False):
+ # A prototype or another translation unit's static function cannot supply
+ # an external symbol (the missing owner API is such a static function).
+ guard = '' if allow_static else r'(?!static\b)'
+ return bool(re.search(r'^[ \t]*'+guard+r'(?!#)[A-Za-z_][A-Za-z_ \t\n*]*?\b'+
+ re.escape(name)+r'\s*\([^;{}]*\)\s*\{', text, re.M) or
+ re.search(r'^\s*#define\s+'+re.escape(name)+r'\b', text, re.M))
+
+
+def resolve_tool(command, description):
+ """Accept a PATH command name or an explicit absolute/relative tool path."""
+ candidate = shutil.which(str(command)) or str(command)
+ try:
+ path = Path(candidate).resolve(strict=True)
+ except (OSError, ValueError):
+ raise ValueError(description+' was not found: '+str(command)+'. Install it or provide its executable path.') from None
+ if not path.is_file() or (os.name != 'nt' and not os.access(path, os.X_OK)):
+ raise ValueError(description+' is not an executable file: '+str(command))
+ return path
+
+
+def source_audit():
+ header = (ROOT/'port/linux/src/gl.h').read_text()
+ renderer = strip_comments((ROOT/'port/linux/src/d3d8_gl.c').read_text())
+ calls = set(re.findall(r'\b(gl[A-Z]\w*)\s*\(', renderer))
+ aliases = set(re.findall(r'^#define\s+(gl\w+)\s+halo_gl\w+', header, re.M))
+ for marker in ('DESKTOP_GL_FUNCTIONS', 'ANDROID_GL_FUNCTIONS'):
+ start, end = header.index('/* '+marker+'_BEGIN */'), header.index('/* '+marker+'_END */')
+ listed = set(re.findall(r'X\((\w+)\)', header[start:end]))
+ assert 'glDeleteProgram' in listed, marker+' is missing glDeleteProgram'
+ if marker == 'DESKTOP_GL_FUNCTIONS':
+ assert not calls-listed, 'Renderer GL calls absent from production desktop list: '+str(sorted(calls-listed))
+ assert not calls-aliases, 'Renderer GL calls missing guest pointer aliases: '+str(sorted(calls-aliases))
+ # The failed mounted-optic owner API was private to weapons.c. Its removed
+ # renderer code must never return as an external guest call.
+ source = strip_comments((ROOT/'port/linux/game/vr_render.c').read_text())
+ owner = 'weapon_get_owner_object_index'
+ assert owner not in source, 'Renderer calls an unavailable static weapon helper'
+ assert not has_provider('long '+owner+'(long weapon_index);', owner), 'Prototype-only negative was accepted'
+ return header, calls
+
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc', default=os.environ.get('CC') or 'clang')
+ parser.add_argument('--ld', help='GNU LLD or lld-link (GNU flavor supported)')
+ parser.add_argument('--sdk', type=Path, default=Path('/usr/include'), help='Installed Khronos include directory')
+ parser.add_argument('--sdl', type=Path, help='Already-local SDL source for SDK declarations on Windows')
+ args = parser.parse_args()
+ header, calls = source_audit()
+ try:
+ cc = resolve_tool(args.cc, 'C compiler')
+ except ValueError as error:
+ parser.error(str(error))
+ linker = args.ld or next((str(p) for p in (cc.with_name('ld.lld'), cc.with_name('ld.lld.exe'),
+ cc.with_name('lld-link.exe')) if p.is_file()), None) or shutil.which('ld.lld')
+ if not linker:
+ parser.error('GNU LLD is required for the actual ARM import link. Install LLVM LLD or pass --ld with its command name or executable path.')
+ try:
+ linker = resolve_tool(linker, 'GNU LLD linker')
+ except ValueError as error:
+ parser.error(str(error))
+ # Ubuntu's ld.lld is a symlink to the generic lld driver. Resolving it
+ # removes the argv[0] flavor, so select GNU explicitly on every host.
+ link_command = [linker, '-flavor', 'gnu', '-m', 'aarch64linux', '-r']
+ resource = Path(subprocess.check_output([str(cc), '-print-resource-dir'], text=True).strip())
+ abi = [x for x in android_build.GUEST_ABI_FLAGS if x not in ('-DHALO_GLES=1', '-DHALO_ANDROID=1')]
+ with tempfile.TemporaryDirectory(prefix='halo-renderer-link-', ignore_cleanup_errors=True) as directory:
+ temp = Path(directory)
+ sdk = temp/'include'; (sdk/'GL').mkdir(parents=True); (sdk/'SDL3').mkdir()
+ glcore = sdk/'GL/glcorearb.h'
+ sdk_includes = []
+ installed = args.sdk.resolve()/'GL/glcorearb.h'
+ if installed.is_file():
+ shutil.copyfile(installed, glcore)
+ sdk_includes += ['-isystem', installed.parent.parent]
+ else:
+ if not args.sdl:
+ parser.error('Khronos GL headers were not found at '+str(installed)+'. Install them, pass --sdk with their include directory, or pass --sdl with an existing local SDL source.')
+ try:
+ sdl_include = args.sdl.resolve(strict=True)/'include'
+ declarations = (sdl_include/'SDL3/SDL_opengl.h').read_text()+'\n'+(sdl_include/'SDL3/SDL_opengl_glext.h').read_text()
+ except (OSError, ValueError) as error:
+ parser.error('SDL GL declarations are unavailable at '+str(args.sdl)+'. Pass --sdl with an existing complete SDL source: '+str(error))
+ glcore.write_text(re.sub(r'\bGLAPIENTRY\b', 'APIENTRY', declarations))
+ sdk_includes += ['-isystem', sdl_include]
+ # Declaration-only boundaries: no fake GL/game provider implementations.
+ (sdk/'string.h').write_text('int strcmp(const char *, const char *);\n')
+ (sdk/'SDL3/SDL_opengl.h').write_text('#include <GL/glcorearb.h>\n')
+ (sdk/'SDL3/SDL.h').write_text('typedef void (*SDL_FunctionPointer)(void);\nSDL_FunctionPointer SDL_GL_GetProcAddress(const char *);\n')
+ (sdk/'platform.h').write_text('#define TRUE 1\n#define FALSE 0\nvoid platform_log(const char *, ...);\n')
+ (sdk/'gl.h').write_text(header)
+ flags = [*abi, *android_build.GUEST_CODE_FLAGS, '-std=gnu11', '-ffreestanding', '-w',
+ '-I'+str(sdk), '-isystem', resource/'include', *sdk_includes]
+ serial = 0
+ def compile_source(path, extra=()):
+ nonlocal serial
+ serial += 1
+ output = temp/('object-'+str(serial))
+ run([cc, *extra, *flags, '-S', path, '-o', str(output)+'.darwin.s'])
+ run([sys.executable, 'tools/android_asm_convert.py', str(output)+'.darwin.s', str(output)+'.s'])
+ run([cc, '--target=aarch64-linux-gnu', '-c', str(output)+'.s', '-o', str(output)+'.o'])
+ return Path(str(output)+'.o')
+ def link(objects, name):
+ output = temp/(name+'.o')
+ run([*link_command, '-o', output, *objects])
+ return symbols(output)
+ guest_gl, gl_imports, imports = temp/'guest_gl.c', temp/'gl_imports.list', temp/'imports.s'
+ run([sys.executable, 'tools/android_gl_stubs.py', '--desktop', 'port/linux/src/gl.h', glcore, guest_gl, gl_imports])
+ assert 'hostgl_glDeleteProgram' in gl_imports.read_text().splitlines()
+ assert 'guest_glDeleteProgram' in guest_gl.read_text()
+ run([sys.executable, 'tools/android_imports.py', imports, 'port/android/host_imports.list',
+ 'port/linux/arm64/host_imports.list', 'port/linux/arm64/vr_imports.list', gl_imports])
+ imports_obj = temp/'imports.o'
+ run([cc, '--target=aarch64-linux-gnu', '-c', imports, '-o', imports_obj])
+ generated_obj = compile_source(guest_gl)
+ loader = temp/'gl_functions.c'
+ loader.write_text((ROOT/'port/linux/src/gl_functions.c').read_text())
+ pointers_obj = compile_source(loader)
+ caller_text = '#include "gl.h"\n'+''.join(
+ 'void *renderer_gl_reference_'+name+'(void) { return (void *)&'+name+'; }\n' for name in sorted(calls))
+ caller_text += 'void renderer_delete_program(GLuint program) { glDeleteProgram(program); }\n'
+ caller = temp/'caller.c'; caller.write_text(caller_text)
+ caller_obj = compile_source(caller)
+ merged = link([caller_obj, pointers_obj, generated_obj, imports_obj], 'renderer-current')
+ missing = [n for n, defined in merged.items() if not defined and
+ (n.startswith(('halo_gl', 'hostgl_')) or re.match(r'^gl[A-Z]', n))]
+ assert not missing, 'Unresolved production GL guest references: '+str(sorted(missing))
+ assert merged.get('hostgl_glDeleteProgram') and merged.get('halo_glDeleteProgram')
+ # Exact pre-repair header: compiles successfully but leaves a raw GL
+ # reference that the real guest imports cannot provide.
+ negative_dir = temp/'negative'; negative_dir.mkdir()
+ broken_header = re.sub(r'^\s*X\(glDeleteProgram\) \\\n', '', header, flags=re.M)
+ broken_header = re.sub(r'^#define glDeleteProgram halo_glDeleteProgram\n', '', broken_header, flags=re.M)
+ (negative_dir/'gl.h').write_text(broken_header)
+ broken_caller = negative_dir/'caller.c'; broken_caller.write_text(caller_text)
+ broken_obj = compile_source(broken_caller)
+ negative = link([broken_obj, pointers_obj, generated_obj, imports_obj], 'broken-gl')
+ assert negative.get('glDeleteProgram') is False, 'Broken GL import negative did not reproduce the failed link'
+ print('PASS: '+str(len(calls))+' renderer GL calls, real generated ARM guest imports/pointer loader, broken GL negative')
+ print('PASS: removed unavailable weapon owner API and prototype-only negative')
+ print('Relocatable partial link only; full game link and headset execution require separate checks.')
+ return 0
+
+
+if __name__ == '__main__':
+ raise SystemExit(main())
diff --git a/tools/test_vr_stock_zoom_hud.py b/tools/test_vr_stock_zoom_hud.py
new file mode 100644
index 0000000000000000000000000000000000000000..9767c64a6dde15858cd27a6fba8ea5901f91e0ad
--- /dev/null
+++ b/tools/test_vr_stock_zoom_hud.py
@@ -0,0 +1,224 @@
+#!/usr/bin/env python3
+"""Execute stock zoom HUD element loops and screen effects after optic removal.
+
+Production classifiers, all three draw loops and interface_draw_screen execute
+with deterministic services. Test VR/flat, remote HUD, zoom-only graphics,
+and preserved normal flashlight/effect/HUD dispatch.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+from test_vr_projectile_reticle import function, block_end
+
+ROOT = Path(__file__).resolve().parents[1]
+
+HARNESS = r'''
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+typedef int boolean;typedef float real;typedef unsigned char byte;typedef unsigned short word;
+typedef struct{short y0,x0,y1,x1;}rectangle2d;
+typedef struct{float alpha,red,green,blue;}real_argb_color;
+#define TRUE 1
+#define FALSE 0
+#define NONE (-1)
+#define FLAG(n) (1u<<(n))
+#define TEST_FLAG(f,n) (((f)&FLAG(n))!=0)
+#define PIN(x,a,b) ((x)<(a)?(a):(x)>(b)?(b):(x))
+#define TAG_BLOCK_GET_ELEMENT(b,i,t) (((t*)((b)->address))+(i))
+static unsigned checks;
+#define CHECK(c) do{checks++;if(!(c)){fprintf(stderr,"line %d: %s\n",__LINE__,#c);exit(1);}}while(0)
+struct tag_block{long count;void *address;};struct tag_reference{long index;};
+enum{_hud_element_runtime_invalid_bit=0,_weapon_number_divide_by_clip_size_bit=0,
+ _hud_number_show_only_when_zoomed_bit=1,_hud_multitexture_overlay_effector_source_zoom_level=7};
+enum{_hud_screen_effect_mask_only_when_zoomed_bit=0,_hud_screen_effect_convolution_only_when_zoomed_bit=0,
+ _hud_screen_effect_light_enhancement_only_when_zoomed_bit=0,_hud_screen_effect_light_enhancement_connect_to_flashlight_bit=1,
+ _hud_screen_effect_light_enhancement_uses_convolution_mask_bit=2,_hud_screen_effect_desaturation_only_when_zoomed_bit=0,
+ _hud_screen_effect_desaturation_connect_to_flashlight_bit=1,_hud_screen_effect_desaturation_is_additive_bit=2,
+ _hud_screen_effect_desaturation_uses_convolution_mask_bit=3,_rasterizer_screen_effect_convolution_type_warp=2};
+struct multitexture_overlay_hud_element_effector_definition{int source;};
+struct multitexture_overlay_hud_element_definition{struct tag_block functions;};
+struct hud_absolute_placement_definition{int id;};
+struct static_hud_element_definition{struct tag_block multitexture_overlays;int id;};
+struct meter_hud_element_definition{struct tag_block multitexture_overlays;int id;};
+struct number_hud_element_definition{unsigned number_flags;short fractional_digits;int id;};
+struct weapon_hud_element_header{short state_type,runtime_flags,use_on_map_type;};
+struct weapon_hud_static_element{struct weapon_hud_element_header header;struct static_hud_element_definition static_element;};
+struct weapon_hud_meter_element{struct weapon_hud_element_header header;struct meter_hud_element_definition meter_element;};
+struct weapon_hud_number_element{struct weapon_hud_element_header header;struct number_hud_element_definition number_element;word weapon_flags;};
+struct weapon_magazine_definition{short rounds_loaded_maximum;};
+struct weapon_definition{struct{struct tag_block magazines;}weapon;};
+struct hud_screen_effect_definition{unsigned mask_flags,convolution_flags,light_enhancement_flags,desaturation_flags;
+ struct tag_reference mask_fullscreen,mask_splitscreen;real convolution_radius_in_bounds[2],convolution_radius_out_bounds[2];
+ real light_enhancement_intensity,desaturation_intensity;int light_enhancement_script_source,desaturation_script_source;real_argb_color desaturation_tint;};
+struct weapon_hud_interface_definition{struct hud_absolute_placement_definition absolute_placement;struct tag_block statics,meters,numbers,screen_effects;};
+struct bitmap_data{int id;};struct bitmap_group{struct tag_block bitmaps;};
+struct rasterizer_cinematic_screen_effect_parameters{struct bitmap_data *convolution_mask;
+ boolean filter_light_enhancement_uses_convolution_mask,filter_desaturation_uses_convolution_mask;
+ real convolution_radius;int convolution_type;real filter_light_enhancement_intensity,filter_desaturation_intensity;
+ boolean filter_desaturation_is_additive;real_argb_color filter_desaturation_tint;};
+static struct{struct{rectangle2d window_bounds;real vertical_field_of_view;}camera;short local_player_index;}render;
+struct hud_state_fixture{long last_weapon_flash_time[8];};
+static struct hud_state_fixture fixture_hud_state;
+static short fixture_zoom;static int fixture_windows=1;
+static long fixture_hud_index=1;static struct weapon_hud_interface_definition fixture_definition;
+static struct hud_screen_effect_definition fixture_effect;static struct bitmap_data fixture_bitmap={8};
+static struct bitmap_group fixture_group;static struct rasterizer_cinematic_screen_effect_parameters drawn_effect;
+static int static_draw[4],meter_draw[4],number_draw[4],layout_begin,layout_end,effect_calls,hud_calls,engine_calls;
+static boolean effect_has_parameters;
+static long interface_get_weapon_hud_index(real *flashlight){*flashlight=.4f;return fixture_hud_index;}
+static struct weapon_hud_interface_definition *weapon_hud_interface_definition_get(long index){CHECK(index==1);return &fixture_definition;}
+static struct bitmap_group *bitmap_group_get(long index){CHECK(index==5||index==6);return &fixture_group;}
+static short player_control_get_zoom_level(short local){CHECK(local==0||local==1);return fixture_zoom;}
+static int main_get_window_count(void){return fixture_windows;}
+static void csmemset(void *memory,int value,size_t bytes){memset(memory,value,bytes);}
+static void scalars_interpolate(real a,real b,real t,real *out){*out=a+(b-a)*t;}
+static real rasterizer_script_screen_effect_get_value(int source){CHECK(source==3);return .5f;}
+static void rasterizer_screen_effect(struct rasterizer_cinematic_screen_effect_parameters const *parameters){
+ effect_calls++;effect_has_parameters=parameters!=NULL;if(parameters)drawn_effect=*parameters;}
+static void hud_draw_screen(void){hud_calls++;}
+static void game_engine_post_rasterize(void){engine_calls++;}
+static void hud_zoomed_layout_begin(rectangle2d *saved){*saved=render.camera.window_bounds;render.camera.window_bounds.x0+=13;layout_begin++;}
+static void hud_zoomed_layout_end(rectangle2d const *saved){render.camera.window_bounds=*saved;layout_end++;}
+static void hud_draw_static_element(short local,struct hud_absolute_placement_definition const *place,struct static_hud_element_definition const *element,short flags,long flash){
+ (void)local;(void)place;(void)flags;(void)flash;CHECK(element->id>=0&&element->id<4);static_draw[element->id]++;}
+static void hud_draw_meter(short local,struct hud_absolute_placement_definition const *place,struct meter_hud_element_definition const *element,byte a,byte b,short flags,real flash,real seconds){
+ (void)local;(void)place;(void)a;(void)b;(void)flags;(void)flash;(void)seconds;CHECK(element->id>=0&&element->id<4);meter_draw[element->id]++;}
+static void hud_draw_numbers(short local,struct hud_absolute_placement_definition const *place,struct number_hud_element_definition const *element,short value,short decimal,short flags,long flash,real seconds){
+ (void)local;(void)place;(void)value;(void)decimal;(void)flags;(void)flash;(void)seconds;CHECK(element->id>=0&&element->id<4);number_draw[element->id]++;}
+static real power(real a,real b){return powf(a,b);}
+static long fast_ftol_C(real a){return (long)a;}
+@@CLASSIFY@@
+@@INTERFACE@@
+static void production_elements(struct weapon_hud_interface_definition const *definition,short local_player_index,struct weapon_definition const *weapon_definition){
+ long element_index;short state_index;unsigned map_type_flags=3;
+ short state_flags[8]={0},number_values[8]={10,20};real numbers_real[8]={1,2};
+ void *unused=NULL; (void)unused;
+ struct hud_state_fixture *hud_state=&fixture_hud_state;
+ @@ELEMENTS@@
+}
+static void reset(void){
+ memset(&drawn_effect,0,sizeof(drawn_effect));memset(&fixture_definition,0,sizeof(fixture_definition));memset(&fixture_effect,0,sizeof(fixture_effect));
+ memset(static_draw,0,sizeof(static_draw));memset(meter_draw,0,sizeof(meter_draw));memset(number_draw,0,sizeof(number_draw));
+ fixture_hud_index=1;fixture_windows=1;layout_begin=layout_end=effect_calls=hud_calls=engine_calls=0;effect_has_parameters=FALSE;
+ render.local_player_index=0;render.camera.vertical_field_of_view=1;render.camera.window_bounds=(rectangle2d){0,0,480,640};
+ fixture_group.bitmaps=(struct tag_block){1,&fixture_bitmap};fixture_definition.screen_effects=(struct tag_block){1,&fixture_effect};
+ fixture_effect.mask_fullscreen.index=5;fixture_effect.mask_splitscreen.index=6;
+ fixture_effect.convolution_radius_in_bounds[0]=fixture_effect.convolution_radius_in_bounds[1]=1;
+ fixture_effect.convolution_radius_out_bounds[0]=fixture_effect.convolution_radius_out_bounds[1]=2;
+ fixture_effect.light_enhancement_intensity=.8f;fixture_effect.desaturation_intensity=.6f;
+ fixture_effect.light_enhancement_script_source=fixture_effect.desaturation_script_source=3;
+ fixture_effect.desaturation_tint=(real_argb_color){1,.2f,.3f,.4f};
+}
+static void check_effects(void){
+ int local,zoom,mask,windows;
+ for(local=0;local<2;local++)for(zoom=0;zoom<2;zoom++)for(mask=0;mask<16;mask++)for(windows=1;windows<=2;windows++){
+ int effective;reset();render.local_player_index=(short)local;fixture_zoom=zoom?0:NONE;fixture_windows=windows;
+ fixture_effect.mask_flags=mask&1;fixture_effect.convolution_flags=(mask>>1)&1;
+ fixture_effect.light_enhancement_flags=((mask>>2)&1)|FLAG(_hud_screen_effect_light_enhancement_connect_to_flashlight_bit);
+ fixture_effect.desaturation_flags=((mask>>3)&1)|FLAG(_hud_screen_effect_desaturation_connect_to_flashlight_bit);
+ effective=zoom;interface_draw_screen();
+ CHECK(effect_calls==1&&effect_has_parameters&&hud_calls==1&&engine_calls==1);
+ CHECK((drawn_effect.convolution_mask!=NULL)==(effective||!(mask&1)));
+ CHECK(drawn_effect.convolution_radius==((windows==1&&(effective||!(mask&2)))?2:0));
+ CHECK(fabsf(drawn_effect.filter_light_enhancement_intensity-((effective||!(mask&4))?.16f:0))<.00001f);
+ CHECK(fabsf(drawn_effect.filter_desaturation_intensity-((effective||!(mask&8))?.12f:0))<.00001f);
+ CHECK(fixture_zoom==(zoom?0:NONE));
+ }
+ reset();fixture_hud_index=NONE;interface_draw_screen();CHECK(effect_calls==1&&!effect_has_parameters&&hud_calls==1&&engine_calls==1);
+ reset();fixture_definition.screen_effects.count=0;interface_draw_screen();CHECK(effect_calls==1&&!effect_has_parameters&&hud_calls==1&&engine_calls==1);
+ reset();render.local_player_index=NONE;interface_draw_screen();CHECK(effect_calls==0&&hud_calls==0&&engine_calls==0);
+}
+static void check_elements(void){
+ int local,i;struct weapon_hud_static_element statics[4];struct weapon_hud_meter_element meters[4];struct weapon_hud_number_element numbers[4];
+ struct multitexture_overlay_hud_element_effector_definition effector={7};
+ struct multitexture_overlay_hud_element_definition overlay={{1,&effector}};
+ struct weapon_definition weapon={0};rectangle2d original;
+ for(local=0;local<2;local++){
+ int special;reset();memset(statics,0,sizeof(statics));memset(meters,0,sizeof(meters));memset(numbers,0,sizeof(numbers));
+ for(i=0;i<4;i++){
+ statics[i].static_element.id=meters[i].meter_element.id=numbers[i].number_element.id=i;
+ statics[i].header.use_on_map_type=meters[i].header.use_on_map_type=numbers[i].header.use_on_map_type=1;
+ }
+ statics[1].static_element.multitexture_overlays=(struct tag_block){1,&overlay};
+ meters[1].meter_element.multitexture_overlays=(struct tag_block){1,&overlay};numbers[1].number_element.number_flags=FLAG(_hud_number_show_only_when_zoomed_bit);
+ statics[2].header.runtime_flags=meters[2].header.runtime_flags=numbers[2].header.runtime_flags=FLAG(_hud_element_runtime_invalid_bit);
+ statics[3].header.use_on_map_type=meters[3].header.use_on_map_type=numbers[3].header.use_on_map_type=2;
+ fixture_definition.statics=(struct tag_block){4,statics};fixture_definition.meters=(struct tag_block){4,meters};fixture_definition.numbers=(struct tag_block){4,numbers};
+ original=render.camera.window_bounds;production_elements(&fixture_definition,(short)local,&weapon);special=1;
+ CHECK(static_draw[0]==1&&meter_draw[0]==1&&number_draw[0]==1);
+ CHECK(static_draw[1]==special&&meter_draw[1]==special&&number_draw[1]==special);
+ CHECK(static_draw[2]==0&&meter_draw[2]==0&&number_draw[2]==0&&static_draw[3]==0&&meter_draw[3]==0&&number_draw[3]==0);
+ CHECK(layout_begin==3*special&&layout_end==layout_begin&&memcmp(&original,&render.camera.window_bounds,sizeof(original))==0);
+ }
+}
+int main(void){check_effects();check_elements();printf("PASS: %u production stock zoom/HUD element and screen-effect assertions (%s)\n",checks,
+#ifdef HALO_VR
+ "VR"
+#else
+ "flat"
+#endif
+ );return 0;}
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ args = parser.parse_args()
+ weapon = (ROOT / 'source/interface/hud_weapon.c').read_text()
+ draw = (ROOT / 'source/interface/hud_draw.c').read_text()
+ interface = (ROOT / 'source/interface/interface.c').read_text()
+ body = function(weapon, 'render_weapon_hud')
+ loops = []
+ for kind in ('statics', 'meters', 'numbers'):
+ match = re.search(r'for \(element_index = 0;\s*element_index < definition->' + kind + r'\.count;\s*element_index\+\+\)', body)
+ assert match, kind
+ loops.append(body[match.start():block_end(body, body.index('{', match.end()))])
+ source = HARNESS.replace('@@CLASSIFY@@', '\n\n'.join(function(draw, name) for name in (
+ 'hud_multitexture_overlays_follow_zoom', 'hud_number_shows_only_when_zoomed'))).replace(
+ '@@INTERFACE@@', function(interface, 'interface_draw_screen')).replace('@@ELEMENTS@@', '\n'.join(loops))
+ compiler, vcvars = args.cc or os.environ.get('CC'), args.vcvarsall
+ if not compiler and os.name == 'nt':
+ candidates = sorted((Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio').glob(
+ '*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1]); vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('Select a C11 host compiler with --cc.')
+ with tempfile.TemporaryDirectory(prefix='halo-vr-stock-zoom-hud-', ignore_cleanup_errors=True) as directory:
+ temp = Path(directory); cfile = temp / 'test.c'; executable = temp / ('test.exe' if os.name == 'nt' else 'test')
+ cfile.write_text(source, encoding='utf-8')
+ for vr in (True, False):
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX', '/D_CRT_SECURE_NO_WARNINGS',
+ str(cfile), '/Fe:' + str(executable), '/Fo:' + str(temp / 'test.obj')]
+ if vr:
+ command.append('/DHALO_VR')
+ if vcvars:
+ for path in (vcvars, Path(compiler), temp):
+ if any(c in str(path) for c in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported compiler/temp path shell metacharacter.')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\nif errorlevel 1 exit /b 1\n' +
+ subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', str(cfile), '-o', str(executable)]
+ if vr:
+ command.append('-DHALO_VR')
+ if os.name != 'nt':
+ command.append('-lm')
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(executable)], cwd=temp, check=True)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_sun_glow.py b/tools/test_vr_sun_glow.py
new file mode 100644
index 0000000000000000000000000000000000000000..b18699fc7fa151904b0aadad56a4a31ca6826ed8
--- /dev/null
+++ b/tools/test_vr_sun_glow.py
@@ -0,0 +1,335 @@
+#!/usr/bin/env python3
+"""Run production stereo sun projection, dispatch and shader-sampling checks.
+
+Host C11 compiler only; no headset, game data, SDL or OpenXR is required.
+This validates CPU/matrix and shader source contracts, not GPU visual output.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def function(text, name):
+ match = re.search(r'(?:static\s+)?(?:void|float|real|int|boolean)\s+' + re.escape(name) +
+ r'\s*\([^;]*?\)\s*\{', text)
+ assert match, name
+ masked = re.sub(r'/\*.*?\*/|//[^\n]*|"(?:\\.|[^"\\])*"',
+ lambda item: re.sub(r'[^\n]', ' ', item.group()), text, flags=re.S)
+ end, depth = masked.index('{', match.start()) + 1, 1
+ while depth:
+ depth += (masked[end] == '{') - (masked[end] == '}')
+ end += 1
+ return text[match.start():end]
+
+
+HARNESS = r'''
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+typedef float real;
+typedef int boolean;
+typedef int BOOL;
+typedef unsigned int DWORD;
+#define TRUE 1
+#define FALSE 0
+#define MIN(a,b) ((a) < (b) ? (a) : (b))
+#define D3DRS_PS_MAX 128
+#define SAMPLE_BIAS ""
+static int checks;
+#define CHECK(condition) do { checks++; if (!(condition)) { \
+ fprintf(stderr, "failed line %d: %s\n", __LINE__, #condition); exit(1); } } while (0)
+static int near_float(float a, float b) { return fabsf(a - b) < 0.002f; }
+typedef struct { real x, y, z; } real_point3d;
+typedef struct { real i, j; } real_vector2d;
+typedef struct { short x0, y0, x1, y1; } rectangle2d;
+typedef struct { real n[4][3]; } real_matrix4x3;
+struct render_camera { real_point3d position; rectangle2d viewport_bounds; real marker; };
+struct render_frustum { real_matrix4x3 world_to_view; real projection_matrix[4][4]; real marker; };
+struct rasterizer_lens_flare_submit_parameters { real_point3d position; };
+static struct { struct render_camera camera; struct render_frustum frustum; } global_window_parameters;
+static struct { boolean active; struct render_camera eye_cameras[2]; struct render_frustum eyes[2]; } vr_render;
+static struct { int screen_space, screen_points, eye_alone; unsigned long eye_serial; } vr_gl;
+static int corrections_calls;
+static void halo_vr_set_screen_corrections(const float corrections[2][4]) {
+ static const float identity[2][4] = { { 1.0f, 1.0f, 0.0f, 0.0f }, { 1.0f, 1.0f, 0.0f, 0.0f } };
+ CHECK(memcmp(corrections, identity, sizeof(identity)) == 0);
+ vr_gl.screen_points = 0; corrections_calls++;
+}
+static void matrix4x3_transform_point(const real_matrix4x3 *matrix, const real_point3d *point, real_point3d *out) {
+ out->x = matrix->n[0][0]*point->x + matrix->n[1][0]*point->y + matrix->n[2][0]*point->z + matrix->n[3][0];
+ out->y = matrix->n[0][1]*point->x + matrix->n[1][1]*point->y + matrix->n[2][1]*point->z + matrix->n[3][1];
+ out->z = matrix->n[0][2]*point->x + matrix->n[1][2]*point->y + matrix->n[2][2]*point->z + matrix->n[3][2];
+}
+@@FUNCTIONS@@
+enum { XGPU_MULTIVIEW_NONE, XGPU_MULTIVIEW_EYES, XGPU_MULTIVIEW_LEFT_EYE, XGPU_MULTIVIEW_RIGHT_EYE };
+enum { _xgpu_sampler_none, _xgpu_sampler_2d, _xgpu_sampler_3d, _xgpu_sampler_cube, _xgpu_sampler_2d_layered };
+@@KEY@@
+@@OLD_KEY@@
+struct xgpu_text { char buffer[2048]; unsigned long length; };
+static void xgpu_text_append(struct xgpu_text *text, const char *format, ...) {
+ va_list arguments;
+ int count;
+ va_start(arguments, format);
+ count = vsnprintf(text->buffer + text->length, sizeof(text->buffer) - text->length, format, arguments);
+ va_end(arguments);
+ CHECK(count > 0 && (unsigned long)count < sizeof(text->buffer) - text->length);
+ text->length += (unsigned long)count;
+}
+@@SAMPLE@@
+static void choose_source_eye(struct nv2a_pixel_shader_key *output) {
+ struct nv2a_pixel_shader_key key = *output;
+ @@SAMPLE_BINDING@@
+ *output = key;
+}
+static int draws, draw_eye[2], visible[2];
+static float draw_x[2], draw_y[2], depth[2], wall_depth[2];
+static double configured_glow = 0.5;
+static float composite_strength[2];
+static double config_real(const char *name) {
+ CHECK(strcmp(name, "vr.sun_glow_strength") == 0);
+ return configured_glow;
+}
+@@STRENGTH@@
+static void rasterizer_sun_glow_draw_internal(struct rasterizer_lens_flare_submit_parameters const *flare, real glow_strength) {
+ real_point3d center;
+ real_vector2d axes;
+ struct nv2a_pixel_shader_key key;
+ struct xgpu_text text;
+ int slot = draws++;
+ CHECK(slot < 2); composite_strength[slot] = glow_strength;
+ if (!vr_render.active) return;
+ CHECK(vr_gl.screen_space && !vr_gl.screen_points);
+ CHECK(vr_gl.eye_alone == slot); CHECK(global_window_parameters.camera.marker == (float)(slot + 1));
+ CHECK(rasterizer_project_billboard(&flare->position, 0.5f, &center, &axes));
+ draw_eye[slot] = vr_gl.eye_alone; draw_x[slot] = center.x; draw_y[slot] = center.y; depth[slot] = center.z;
+ /* The unchanged legacy mask uses ZENABLE + LESSEQUAL against this
+ eye's depth, then this eye alone is copied into the shared scratch. */
+ visible[slot] = center.z <= wall_depth[slot];
+ memset(&key, 0, sizeof(key)); memset(&text, 0, sizeof(text));
+ key.sampler_type[0] = _xgpu_sampler_2d_layered; key.multiview = XGPU_MULTIVIEW_NONE;
+ choose_source_eye(&key); sample(&text, &key, 0, "point");
+ CHECK(key.sample_eye == slot);
+ CHECK(strstr(text.buffer, slot ? ", 1.0)" : ", 0.0)") != NULL);
+}
+@@WRAPPER@@
+static void frustum(struct render_frustum *target, float eye, const float fov[4], float near_z, float far_z) {
+ memset(target, 0, sizeof(*target));
+ target->world_to_view.n[0][0] = target->world_to_view.n[1][1] = target->world_to_view.n[2][2] = 1.0f;
+ target->world_to_view.n[3][0] = -eye;
+ target->projection_matrix[0][0] = 2.0f / (fov[1] - fov[0]);
+ target->projection_matrix[1][1] = 2.0f / (fov[2] - fov[3]);
+ target->projection_matrix[2][0] = -(fov[1] + fov[0]) / (fov[1] - fov[0]);
+ target->projection_matrix[2][1] = -(fov[2] + fov[3]) / (fov[2] - fov[3]);
+ target->projection_matrix[2][2] = far_z / (far_z - near_z);
+ target->projection_matrix[3][2] = -near_z * far_z / (far_z - near_z);
+ target->projection_matrix[2][3] = 1.0f;
+}
+static const float fovs[2][4] = { { -1.12f, 0.83f, 1.02f, -0.94f }, { -0.79f, 1.17f, 0.96f, -1.03f } };
+static void reset_state(void) {
+ int eye;
+ memset(&vr_render, 0, sizeof(vr_render)); memset(&vr_gl, 0, sizeof(vr_gl));
+ memset(&global_window_parameters, 0, sizeof(global_window_parameters));
+ vr_render.active = 1; vr_gl.eye_alone = -1;
+ draws = corrections_calls = 0;
+ for (eye = 0; eye < 2; eye++) {
+ struct render_camera *camera = &vr_render.eye_cameras[eye];
+ camera->position.x = eye ? 0.03f : -0.03f;
+ camera->viewport_bounds.x1 = 640; camera->viewport_bounds.y1 = 480; camera->marker = (float)(eye + 1);
+ frustum(&vr_render.eyes[eye], camera->position.x, fovs[eye], 0.0625f, 1024.0f);
+ }
+ global_window_parameters.camera = vr_render.eye_cameras[0];
+ global_window_parameters.camera.marker = 9.0f;
+ frustum(&global_window_parameters.frustum, 0.0f, fovs[0], 0.125f, 4096.0f);
+ global_window_parameters.frustum.marker = 9.0f;
+ wall_depth[0] = 1.0f; wall_depth[1] = 0.0f;
+}
+static void check_projection(void) {
+ int eye, lateral, vertical, distance_step, k;
+ const float distances[4] = { 0.2f, 2.0f, 30.0f, 974.0f };
+ reset_state();
+ for (eye = 0; eye < 2; eye++) {
+ struct render_camera camera = global_window_parameters.camera;
+ struct render_frustum selected = global_window_parameters.frustum;
+ float hardware_depth[4];
+ for (k = 0; k < 4; k++) hardware_depth[k] = selected.projection_matrix[k][2];
+ CHECK(vr_render_sun_glow_begin((short)eye, &camera, &selected));
+ for (k = 0; k < 4; k++) CHECK(hardware_depth[k] == selected.projection_matrix[k][2]);
+ global_window_parameters.camera = camera; global_window_parameters.frustum = selected;
+ for (distance_step = 0; distance_step < 4; distance_step++) {
+ float z = distances[distance_step];
+ for (lateral = -8; lateral <= 8; lateral++) {
+ for (vertical = -5; vertical <= 5; vertical++) {
+ float tangent_x = (float)lateral * 0.11f, tangent_y = (float)vertical * 0.13f;
+ real_point3d point = { tangent_x*z, tangent_y*z, z }, center;
+ real_vector2d axes;
+ const float *fov = fovs[eye];
+ float x = tangent_x - camera.position.x / z;
+ float expected_x = ((2.0f*x - fov[1] - fov[0]) / (fov[1] - fov[0]) + 1.0f) * 320.0f - 0.5f;
+ float expected_y = (1.0f - (2.0f*tangent_y - fov[2] - fov[3]) / (fov[2] - fov[3])) * 240.0f - 0.5f;
+ float expected_depth = hardware_depth[2] + hardware_depth[3] / z;
+ CHECK(rasterizer_project_billboard(&point, 0.1f, &center, &axes));
+ CHECK(near_float(center.x, expected_x)); CHECK(near_float(center.y, expected_y));
+ CHECK(near_float(center.z, expected_depth));
+ CHECK(near_float(axes.i, 64.0f / ((fov[1] - fov[0]) * z)));
+ CHECK(near_float(axes.j, 48.0f / ((fov[2] - fov[3]) * z)));
+ }
+ }
+ }
+ vr_render_sun_glow_end();
+ }
+}
+static void check_dispatch_and_occlusion(void) {
+ struct rasterizer_lens_flare_submit_parameters flare = { { 2.0f, 0.3f, 5.0f } };
+ struct render_camera saved_camera;
+ struct render_frustum saved_frustum;
+ unsigned long serial;
+ reset_state(); saved_camera = global_window_parameters.camera; saved_frustum = global_window_parameters.frustum;
+ rasterizer_sun_glow_draw(&flare);
+ CHECK(draws == 2 && draw_eye[0] == 0 && draw_eye[1] == 1);
+ CHECK(composite_strength[0] == 0.5f && composite_strength[1] == 0.5f);
+ CHECK(visible[0] && !visible[1]); /* independent eye masks; right wall cannot reuse left visibility */
+ CHECK(fabsf(draw_x[0] - draw_x[1]) > 20.0f && fabsf(draw_y[0] - draw_y[1]) > 5.0f);
+ CHECK(near_float(depth[0], depth[1]));
+ CHECK(memcmp(&saved_camera, &global_window_parameters.camera, sizeof(saved_camera)) == 0);
+ CHECK(memcmp(&saved_frustum, &global_window_parameters.frustum, sizeof(saved_frustum)) == 0);
+ CHECK(vr_gl.eye_alone == -1 && !vr_gl.screen_space && !vr_gl.screen_points);
+ CHECK(vr_gl.eye_serial == 2);
+ serial = vr_gl.eye_serial; halo_vr_screen_space(0); CHECK(vr_gl.eye_serial == serial);
+ halo_vr_screen_space(1); CHECK(vr_gl.eye_serial == serial + 1);
+ halo_vr_screen_space(1); CHECK(vr_gl.eye_serial == serial + 1);
+ halo_vr_screen_space(0); CHECK(vr_gl.eye_serial == serial + 2);
+ CHECK(!vr_render_sun_glow_begin(-1, &saved_camera, &saved_frustum));
+ CHECK(!vr_render_sun_glow_begin(2, &saved_camera, &saved_frustum));
+ reset_state(); vr_render.active = 0; saved_camera = global_window_parameters.camera; saved_frustum = global_window_parameters.frustum;
+ rasterizer_sun_glow_draw(&flare);
+ CHECK(draws == 1 && corrections_calls == 0);
+ CHECK(composite_strength[0] == 1.0f);
+ CHECK(!vr_gl.screen_space && vr_gl.eye_alone == -1);
+ CHECK(memcmp(&saved_camera, &global_window_parameters.camera, sizeof(saved_camera)) == 0);
+ CHECK(memcmp(&saved_frustum, &global_window_parameters.frustum, sizeof(saved_frustum)) == 0);
+}
+static void check_glow_strength(void) {
+ const double inputs[] = { -1.0, 0.0, 0.2, 0.5, 1.0, 1.5, NAN, INFINITY, -INFINITY };
+ const float expected[] = { 0.0f, 0.0f, 0.2f, 0.5f, 1.0f, 1.0f, 0.5f, 0.5f, 0.5f };
+ struct rasterizer_lens_flare_submit_parameters flare = { { 2.0f, 0.3f, 5.0f } };
+ unsigned int setting;
+ for (setting = 0; setting < sizeof(inputs) / sizeof(inputs[0]); setting++) {
+ reset_state(); configured_glow = inputs[setting];
+ rasterizer_sun_glow_draw(&flare);
+ CHECK(draws == 2);
+ CHECK(near_float(composite_strength[0], expected[setting]));
+ CHECK(near_float(composite_strength[1], expected[setting]));
+ CHECK(visible[0] && !visible[1]);
+ CHECK(vr_gl.eye_alone == -1 && !vr_gl.screen_space);
+ }
+ configured_glow = 0.5;
+}
+static void check_shader_sampling(void) {
+ struct nv2a_pixel_shader_key key;
+ struct xgpu_text text;
+ int view, source_eye;
+ size_t key_size = sizeof(struct nv2a_pixel_shader_key), old_size = sizeof(struct original_pixel_shader_key);
+ CHECK(key_size == old_size);
+ for (view = XGPU_MULTIVIEW_NONE; view <= XGPU_MULTIVIEW_RIGHT_EYE; view++) {
+ for (source_eye = 0; source_eye < 2; source_eye++) {
+ const char *expected = view == XGPU_MULTIVIEW_EYES ? "float(gl_ViewID_OVR)" :
+ view == XGPU_MULTIVIEW_RIGHT_EYE || (view == XGPU_MULTIVIEW_NONE && source_eye) ? ", 1.0)" : ", 0.0)";
+ memset(&key, 0, sizeof(key)); memset(&text, 0, sizeof(text));
+ key.sampler_type[0] = _xgpu_sampler_2d_layered; key.multiview = (unsigned char)view;
+ key.sample_eye = (unsigned char)source_eye; sample(&text, &key, 0, "point");
+ CHECK(strstr(text.buffer, expected) != NULL);
+ }
+ }
+ memset(&key, 0, sizeof(key)); memset(&text, 0, sizeof(text));
+ key.sampler_type[0] = _xgpu_sampler_2d; key.sample_eye = 1;
+ sample(&text, &key, 0, "point"); CHECK(strstr(text.buffer, "vec3") == NULL);
+}
+int main(void) {
+ check_projection(); check_dispatch_and_occlusion(); check_glow_strength(); check_shader_sampling();
+ printf("PASS: %d production stereo sun-glow assertions\n", checks);
+ return 0;
+}
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ args = parser.parse_args()
+ lights_path = 'source/rasterizer/xbox/rasterizer_xbox_lights.c'
+ lights = (ROOT / lights_path).read_text()
+ render = (ROOT / 'port/linux/game/vr_render.c').read_text()
+ gl = (ROOT / 'port/linux/src/d3d8_gl.c').read_text()
+ psh = (ROOT / 'port/linux/src/nv2a_psh.c').read_text()
+ xgpu = (ROOT / 'port/linux/src/xgpu.h').read_text()
+ # Compare the original routine with the actual upstream pin, including
+ # when later private/release commits have already committed its wrapper.
+ baseline = '2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4:'
+ old_lights = subprocess.check_output(['git', 'show', baseline + lights_path], cwd=ROOT, text=True)
+ old_xgpu = subprocess.check_output(['git', 'show', baseline + 'port/linux/src/xgpu.h'], cwd=ROOT, text=True)
+ old_draw = function(old_lights, 'rasterizer_sun_glow_draw')
+ new_draw = function(lights, 'rasterizer_sun_glow_draw_internal')
+ # The sole change inside the legacy routine is the final composite alpha.
+ original_alpha = 'brightness / (real)(pass + 1)) >= 0 && success)'
+ scaled_alpha = 'brightness * glow_strength / (real)(pass + 1)) >= 0 && success)'
+ assert new_draw.count(scaled_alpha) == 1 and old_draw.count(original_alpha) == 1
+ assert new_draw[new_draw.index('{'):].replace(scaled_alpha, original_alpha) == old_draw[old_draw.index('{'):], 'Legacy mask/blur/depth path changed'
+ assert function(lights, 'rasterizer_project_billboard') == function(old_lights, 'rasterizer_project_billboard')
+ assert 'D3DRS_ZFUNC,\n\t\t\t\tD3DCMP_LESSEQUAL' in new_draw and 'D3DRS_ZENABLE,\n\t\t\t\tTRUE' in new_draw
+ binding = re.search(r'key\.sample_eye = vr_gl\.eye_alone == 1 \? 1 : 0;', gl)
+ assert binding and binding.start() < gl.index('entry = program_get(vertex_shader_get(program, immediate, key.multiview)')
+ assert 'vr_gl.screen_points || vr_gl.screen_space ? 1.0f : 0.0f' in gl
+ assert 'halo_vr_screen_space(0);' in function(render, 'vr_render_end')
+ assert 'vr_render.eye_cameras[eye] = eye_camera;' in render
+ key = re.search(r'struct nv2a_pixel_shader_key\s*\{.*?\};', xgpu, re.S).group()
+ old_key = re.search(r'struct nv2a_pixel_shader_key\s*\{.*?\};', old_xgpu, re.S).group()
+ functions = '\n\n'.join([function(gl, 'halo_vr_screen_space'), function(gl, 'halo_vr_draw_eye'),
+ function(render, 'vr_render_screen_point_end'), function(render, 'vr_render_sun_glow_begin'),
+ function(render, 'vr_render_sun_glow_end'), function(lights, 'rasterizer_project_billboard')])
+ harness = HARNESS.replace('@@FUNCTIONS@@', functions).replace('@@WRAPPER@@', function(lights, 'rasterizer_sun_glow_draw'))
+ harness = harness.replace('@@STRENGTH@@', function(lights, 'rasterizer_vr_sun_glow_strength'))
+ harness = harness.replace('@@KEY@@', key).replace('@@OLD_KEY@@', old_key.replace('nv2a_pixel_shader_key', 'original_pixel_shader_key'))
+ harness = harness.replace('@@SAMPLE@@', function(psh, 'sample')).replace('@@SAMPLE_BINDING@@', binding.group())
+ compiler = args.cc or os.environ.get('CC')
+ vcvars = args.vcvarsall
+ if not compiler and os.name == 'nt':
+ installations = Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio'
+ candidates = sorted(installations.glob('*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1]); vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('No C11 host compiler found; select one with --cc.')
+ with tempfile.TemporaryDirectory(prefix='halo-vr-sun-check-', ignore_cleanup_errors=True) as directory:
+ temp = Path(directory); source = temp / 'test.c'; executable = temp / ('test.exe' if os.name == 'nt' else 'test')
+ source.write_text(harness)
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX', '/DHALO_VR', '/D_CRT_SECURE_NO_WARNINGS',
+ '/Fe:' + str(executable), '/Fo:' + str(temp / 'test.obj'), str(source)]
+ if vcvars:
+ for path in (vcvars, Path(compiler), temp):
+ if any(character in str(path) for character in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported shell metacharacter in compiler/temp path.')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\nif errorlevel 1 exit /b 1\n' +
+ subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-DHALO_VR', str(source),
+ *([] if os.name == 'nt' else ['-lm']), '-o', str(executable)]
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(executable)], cwd=temp, check=True)
+ print('PASS: original glow/depth pipeline preserved; stereo source and screen-space binding contracts')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_tracking.py b/tools/test_vr_tracking.py
new file mode 100644
index 0000000000000000000000000000000000000000..ff5c227a1bc9fe89e517753befbc12759ded8011
--- /dev/null
+++ b/tools/test_vr_tracking.py
@@ -0,0 +1,220 @@
+#!/usr/bin/env python3
+"""Compile the production OpenXR locate functions against deterministic failures.
+
+Uses the checked-in Khronos and guest/host ABI headers. No runtime or headset
+is loaded. Verifies fresh-query validity after prior successful tracking.
+"""
+import argparse
+import os
+from pathlib import Path
+import shutil
+import subprocess
+import sys
+import tempfile
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT))
+from tools.test_vr_projectile_reticle import function
+
+
+HARNESS = r'''
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <openxr/openxr.h>
+#include "vr_host.h"
+static unsigned checks;
+#define CHECK(c) do { checks++; if(!(c)) { fprintf(stderr,"failed line %d: %s\n",__LINE__,#c); exit(1); } } while(0)
+static struct {
+ int frame_begun;
+ XrFrameState frame;
+ XrSession session;
+ XrSpace play_space,view_space,aim_spaces[2],grip_spaces[2];
+} vr;
+static XrResult views_result,space_result[5];
+static uint32_t view_count;
+static XrViewStateFlags view_flags;
+static XrSpaceLocationFlags space_flags[5];
+static int view_calls,space_calls,generation;
+XRAPI_ATTR XrResult XRAPI_CALL xrLocateViews(XrSession session,
+ const XrViewLocateInfo *info,XrViewState *state,uint32_t capacity,
+ uint32_t *count,XrView *views) {
+ int eye;
+ CHECK(session==vr.session && capacity==2);
+ CHECK(info->type==XR_TYPE_VIEW_LOCATE_INFO && info->next==NULL);
+ CHECK(info->displayTime==vr.frame.predictedDisplayTime && info->space==vr.play_space);
+ CHECK(state->type==XR_TYPE_VIEW_STATE && state->next==NULL);
+ view_calls++;
+ if(views_result!=XR_SUCCESS) return views_result;
+ *count=view_count; state->viewStateFlags=view_flags;
+ for(eye=0;eye<2;eye++) {
+ CHECK(views[eye].type==XR_TYPE_VIEW && views[eye].next==NULL);
+ views[eye].pose.position=(XrVector3f){(float)(10*generation+eye),2,3};
+ views[eye].pose.orientation=(XrQuaternionf){0,0,0,1};
+ views[eye].fov=(XrFovf){-0.5f,0.5f,0.5f,-0.5f};
+ }
+ return XR_SUCCESS;
+}
+XRAPI_ATTR XrResult XRAPI_CALL xrLocateSpace(XrSpace space,XrSpace base,
+ XrTime time,XrSpaceLocation *location) {
+ int slot=(int)(uintptr_t)space-2;
+ CHECK(slot>=0 && slot<5 && base==vr.play_space && time==vr.frame.predictedDisplayTime);
+ CHECK(location->type==XR_TYPE_SPACE_LOCATION && location->next==NULL);
+ space_calls++;
+ if(space_result[slot]!=XR_SUCCESS) return space_result[slot];
+ location->locationFlags=space_flags[slot];
+ location->pose.position=(XrVector3f){(float)(100*generation+slot),4,5};
+ location->pose.orientation=(XrQuaternionf){0,0,0,1};
+ return XR_SUCCESS;
+}
+/* OpenXR's standard { TYPE } initializer intentionally zeroes every other
+ member. Suppress only Clang's warning about this established API idiom. */
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wmissing-field-initializers"
+#endif
+@@PRODUCTION@@
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
+static void fixture(void) {
+ int slot;
+ memset(&vr,0,sizeof(vr)); vr.frame_begun=1;
+ vr.frame.predictedDisplayTime=123456;
+ vr.session=(XrSession)(uintptr_t)1; vr.play_space=(XrSpace)(uintptr_t)1;
+ vr.view_space=(XrSpace)(uintptr_t)2;
+ vr.aim_spaces[0]=(XrSpace)(uintptr_t)3; vr.grip_spaces[0]=(XrSpace)(uintptr_t)4;
+ vr.aim_spaces[1]=(XrSpace)(uintptr_t)5; vr.grip_spaces[1]=(XrSpace)(uintptr_t)6;
+ views_result=XR_SUCCESS; view_count=2;
+ view_flags=XR_VIEW_STATE_ORIENTATION_VALID_BIT|XR_VIEW_STATE_POSITION_VALID_BIT;
+ for(slot=0;slot<5;slot++) {
+ space_result[slot]=XR_SUCCESS;
+ space_flags[slot]=XR_SPACE_LOCATION_ORIENTATION_VALID_BIT|XR_SPACE_LOCATION_POSITION_VALID_BIT;
+ }
+ view_calls=space_calls=0; generation=1;
+}
+static struct vr_host_pose *space_pose(struct vr_host_views *views,int slot) {
+ switch(slot) {
+ case 0: return &views->head;
+ case 1: return &views->aim[0];
+ case 2: return &views->grip[0];
+ case 3: return &views->aim[1];
+ default: return &views->grip[1];
+ }
+}
+static void all_valid(struct vr_host_views *views,int expected) {
+ int eye,slot;
+ for(eye=0;eye<2;eye++) CHECK(views->eye[eye].valid==expected);
+ for(slot=0;slot<5;slot++) CHECK(space_pose(views,slot)->valid==expected);
+ CHECK(views->should_render==1 && views->focused==1 && views->display_elapsed==0.01f);
+}
+static void success(struct vr_host_views *views) {
+ int eye,slot;
+ CHECK(host_vr_locate(views)); all_valid(views,1);
+ for(eye=0;eye<2;eye++) CHECK(views->eye[eye].position[0]==(float)(10*generation+eye));
+ for(slot=0;slot<5;slot++) CHECK(space_pose(views,slot)->position[0]==(float)(100*generation+slot));
+}
+int main(void) {
+ struct vr_host_views views;
+ unsigned mask; int slot,eye;
+ memset(&views,0,sizeof(views)); views.should_render=1; views.focused=1; views.display_elapsed=0.01f;
+ fixture(); success(&views);
+ views_result=XR_ERROR_RUNTIME_FAILURE; generation=2;
+ CHECK(!host_vr_locate(&views)); all_valid(&views,0);
+ CHECK(space_calls==5); /* A view failure must not query individual spaces. */
+ views_result=XR_SUCCESS; success(&views);
+ view_count=1; CHECK(!host_vr_locate(&views)); all_valid(&views,0);
+ view_count=2; success(&views);
+ vr.frame_begun=0; view_calls=space_calls=0;
+ CHECK(!host_vr_locate(&views)); all_valid(&views,0); CHECK(view_calls==0 && space_calls==0);
+ vr.frame_begun=1; success(&views);
+ for(mask=1;mask<32;mask++) {
+ fixture(); success(&views); generation=2;
+ for(slot=0;slot<5;slot++) if(mask&(1u<<slot)) space_result[slot]=XR_ERROR_RUNTIME_FAILURE;
+ CHECK(host_vr_locate(&views));
+ for(eye=0;eye<2;eye++) CHECK(views.eye[eye].valid==1 && views.eye[eye].position[0]==(float)(20+eye));
+ for(slot=0;slot<5;slot++) {
+ struct vr_host_pose *pose=space_pose(&views,slot);
+ CHECK(pose->valid==((mask&(1u<<slot)) ? 0 : 1));
+ CHECK(pose->position[0]==(float)(((mask&(1u<<slot)) ? 100 : 200)+slot));
+ }
+ for(slot=0;slot<5;slot++) space_result[slot]=XR_SUCCESS;
+ generation=3; success(&views);
+ }
+ fixture(); success(&views); view_flags=XR_VIEW_STATE_POSITION_VALID_BIT;
+ for(slot=0;slot<5;slot++) space_flags[slot]=XR_SPACE_LOCATION_POSITION_VALID_BIT;
+ CHECK(host_vr_locate(&views)); all_valid(&views,0);
+ view_flags=XR_VIEW_STATE_ORIENTATION_VALID_BIT;
+ for(slot=0;slot<5;slot++) space_flags[slot]=XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
+ CHECK(host_vr_locate(&views)); all_valid(&views,0);
+ view_flags|=XR_VIEW_STATE_ORIENTATION_VALID_BIT;
+ view_flags|=XR_VIEW_STATE_POSITION_VALID_BIT;
+ for(slot=0;slot<5;slot++) space_flags[slot]|=XR_SPACE_LOCATION_POSITION_VALID_BIT;
+ generation=2; success(&views);
+ for(slot=0;slot<5;slot++) {
+ int other;
+ space_flags[slot]=XR_SPACE_LOCATION_ORIENTATION_VALID_BIT;
+ CHECK(host_vr_locate(&views));
+ CHECK(views.eye[0].valid && views.eye[1].valid);
+ for(other=0;other<5;other++) CHECK(space_pose(&views,other)->valid==(other!=slot));
+ space_flags[slot]|=XR_SPACE_LOCATION_POSITION_VALID_BIT;
+ success(&views);
+ }
+ printf("PASS: %u production OpenXR tracking assertions\n",checks); return 0;
+}
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ args = parser.parse_args()
+ source_text = (ROOT / 'port/linux/arm64/host_vr.c').read_text()
+ harness = HARNESS.replace('@@PRODUCTION@@', '\n\n'.join(
+ function(source_text, name) for name in ('pose_from_xr', 'host_vr_locate')))
+ compiler = args.cc or os.environ.get('CC')
+ vcvars = args.vcvarsall
+ if not compiler and os.name == 'nt':
+ installations = Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio'
+ candidates = sorted(installations.glob('*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1])
+ vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('No C11 host compiler found; select one with --cc.')
+ include_paths = [ROOT / 'port/third_party/openxr', ROOT / 'port/linux/src']
+ with tempfile.TemporaryDirectory(prefix='halo-vr-tracking-check-', ignore_cleanup_errors=True) as directory:
+ temp = Path(directory)
+ source = temp / 'test.c'
+ executable = temp / ('test.exe' if os.name == 'nt' else 'test')
+ source.write_text(harness)
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX', '/D_CRT_SECURE_NO_WARNINGS',
+ '/Fe:' + str(executable), '/Fo:' + str(temp / 'test.obj')]
+ command.extend('/I' + str(path) for path in include_paths)
+ command.append(str(source))
+ if vcvars:
+ for path in (vcvars, Path(compiler), temp):
+ if any(character in str(path) for character in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported shell metacharacter in compiler/temp path.')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\nif errorlevel 1 exit /b 1\n' +
+ subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror']
+ command.extend('-I' + str(path) for path in include_paths)
+ command.append(str(source))
+ if os.name != 'nt':
+ command.append('-lm')
+ command.extend(['-o', str(executable)])
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(executable)], cwd=temp, check=True)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_tutorial.py b/tools/test_vr_tutorial.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ea8ac6575e068d015f5c39c9ff4179960e67856
--- /dev/null
+++ b/tools/test_vr_tutorial.py
@@ -0,0 +1,546 @@
+#!/usr/bin/env python3
+"""Host regressions for production VR tutorial gates; no headset or map required.
+
+An optional --xbox-a10 reads an authorized local Xbox map passively to verify
+the actual tutorial predicates. Its bytes and scripts are never saved or copied.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import struct
+import subprocess
+import tempfile
+import zlib
+
+import test_vr_locomotion as walking
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def function(source, name):
+ match = re.search(r'(?:static\s+)?(?:void|float|int|boolean)\s+' + re.escape(name) +
+ r'\s*\([^;]*?\)\s*\{', source)
+ assert match, f'Missing production function {name}'
+ code = walking.masked(source)
+ end = code.index('{', match.start()) + 1
+ depth = 1
+ while depth:
+ depth += (code[end] == '{') - (code[end] == '}')
+ end += 1
+ return source[match.start():end]
+
+
+TURN_CHECKS = r'''
+static int camera_enabled = 1, paused;
+enum { _object_dead_bit = 0 };
+static struct unit_datum { struct { unsigned long damage_flags; } object; } living;
+static struct unit_datum *unit_try_and_get(long index) { return index == NONE ? NULL : &living; }
+static int player_control_vr_vehicle_controls(long unit_index,float *hull_yaw) {
+ (void)unit_index; *hull_yaw=0; return 0;
+}
+static int player_control_camera_control_is_active(void) { return camera_enabled && !paused; }
+static void turn_control_dispatch(void) {
+ int vr_driving_mode=0; float vr_hull_yaw=0;
+ @@TURN_CONTROL@@
+ CHECK(vr_driving_mode==0 && vr_hull_yaw==0);
+}
+
+static void note_idle(void) {
+ const real_euler_angles2d still = { 0.f, 0.f };
+ const real_vector2d idle = { 0.f, 0.f };
+ player_control_action_test_note(0, &still, &idle, 0.f);
+}
+static void tutorial_reset(void) {
+ reset_state(); settings.snap_angle = (float)M_PI / 4.f;
+ vr.snap_latched = 0; camera_enabled = 1; paused = inhibited = cinematic = 0;
+ living.object.damage_flags = 0; player->unit_index = 7; local_player_index = 0;
+ player_control_action_test_reset();
+}
+static void check_snap_and_reset(void) {
+ float yaw, pitch, before;
+ tutorial_reset();
+ /* Reproduce input-disabled cryopod: the script freezes game-facing,
+ but free look and the displayed body-yaw turn remain permitted. */
+ turn_control_dispatch(); CHECK(vr.turn_control_allowed);
+ tutorial_turn_input(0.9f, 0.016f); CHECK(close_float(vr.body_yaw, -(float)M_PI / 4.f));
+ note_idle(); CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_right_bit));
+ CHECK(!TEST_FLAG(tutorial.action_flags, _player_control_look_relative_left_bit));
+ player_control_action_test_reset(); before = vr.body_yaw;
+ tutorial_turn_input(0.9f, 0.016f); note_idle();
+ CHECK(close_float(vr.body_yaw, before)); CHECK(tutorial.action_flags == 0); /* held stick */
+ tutorial_turn_input(0.f, 0.016f); tutorial_turn_input(-0.9f, 0.016f); note_idle();
+ CHECK(close_float(vr.body_yaw, 0.f));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_left_bit));
+ /* Multiple render frames between game ticks must retain both directions. */
+ player_control_action_test_reset(); tutorial_turn_input(0.f, 0.016f);
+ tutorial_turn_input(0.9f, 0.016f); tutorial_turn_input(0.f, 0.016f);
+ tutorial_turn_input(-0.9f, 0.016f); note_idle();
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_left_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_right_bit));
+ head_pose(0.f, 0.04f); note_idle(); head_pose(0.f, -0.04f); note_idle();
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_up_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_down_bit));
+ /* The actual a10 gate requires all four accumulated flags. */
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_left_bit) &&
+ TEST_FLAG(tutorial.action_flags, _player_control_look_relative_right_bit) &&
+ TEST_FLAG(tutorial.action_flags, _player_control_look_relative_up_bit) &&
+ TEST_FLAG(tutorial.action_flags, _player_control_look_relative_down_bit));
+ tutorial_reset(); head_pose(0.8f, 0.4f); vr.tutorial_turn_flags = 3;
+ tutorial.action_flags = tutorial.action_test_flags = ~0UL;
+ player_control_action_test_reset(); note_idle();
+ CHECK(tutorial.action_flags == 0 && tutorial.action_test_flags == 0);
+ CHECK(vr.tutorial_head_valid && vr.tutorial_turn_flags == 0);
+ CHECK(close_float(vr.tutorial_head_yaw, 0.8f) && close_float(vr.tutorial_head_pitch, 0.4f));
+ head_pose(0.805f, 0.405f); note_idle(); CHECK(tutorial.action_flags == 0);
+ head_pose(0.83f, 0.44f); note_idle();
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_left_bit));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_up_bit));
+ tutorial_reset(); head_pose(0.6f, 0.2f); player_control_action_test_reset(); recentre(); note_idle();
+ CHECK(tutorial.action_flags == 0); /* rebasing origin is not deliberate looking */
+ tutorial_reset(); halo_vr_turn_control(0); head_pose(0.9f, 0.4f);
+ CHECK(!halo_vr_tutorial_look(&yaw, &pitch) && yaw == 0.f && pitch == 0.f);
+}
+static void check_smooth_and_controls(void) {
+ int exclusion;
+ float before;
+ tutorial_reset(); settings.smooth_turn = 1; settings.smooth_speed = 2.f;
+ tutorial_turn_input(0.6f, 0.25f); CHECK(close_float(vr.body_yaw, -0.25f));
+ note_idle(); CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_right_bit));
+ player_control_action_test_reset(); tutorial_turn_input(-0.6f, 0.25f); note_idle();
+ CHECK(close_float(vr.body_yaw, 0.f));
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_left_bit));
+ for (exclusion = 0; exclusion < 6; exclusion++) {
+ tutorial_reset();
+ switch (exclusion) {
+ case 0: camera_enabled = 0; break;
+ case 1: paused = 1; break;
+ case 2: inhibited = 1; break;
+ case 3: cinematic = 1; break;
+ case 4: player->unit_index = NONE; break;
+ case 5: living.object.damage_flags = FLAG(_object_dead_bit); break;
+ }
+ turn_control_dispatch(); CHECK(!vr.turn_control_allowed);
+ before = vr.body_yaw; tutorial_turn_input(0.9f, 0.016f);
+ head_pose(0.9f, 0.4f); note_idle();
+ CHECK(vr.body_yaw == before && tutorial.action_flags == 0 && !vr.tutorial_head_valid);
+ }
+ tutorial_reset(); settings.gamepad_aim = 1;
+ tutorial_turn_input(0.9f, 0.016f); note_idle();
+ CHECK(vr.body_yaw == 0.f && tutorial.action_flags == 0); /* gamepad owns ordinary turn */
+ tutorial_reset(); tutorial_turn_input(NAN, 0.016f); tutorial_turn_input(0.9f, INFINITY);
+ tutorial_turn_input(0.9f, -0.01f); note_idle(); CHECK(vr.body_yaw == 0.f && tutorial.action_flags == 0);
+}
+static void check_pose_masks(void) {
+ int corruption;
+ for (corruption = 0; corruption < 16; corruption++) {
+ tutorial_reset();
+ switch (corruption) {
+ case 0: vr.initialized = 0; break;
+ case 1: vr.frame = vr.pending = 0; break;
+ case 2: vr.views.should_render = 0; break;
+ case 3: vr.views.focused = 0; break;
+ case 4: vr.views.head.valid = 0; break;
+ case 5: vr.menus = 1; break;
+ case 6: vr.origin_valid = 0; break;
+ case 7: vr.aim_synced = 0; break;
+ case 8: vr.origin_yaw = NAN; break;
+ case 9: vr.body_yaw = INFINITY; break;
+ case 10: vr.views.head.orientation[0] = NAN; break;
+ case 11: vr.views.eye[0].valid = 0; break;
+ case 12: vr.views.eye[1].orientation[3] = 0.f; break;
+ case 13: vr.views.eye[0].position[2] = INFINITY; break;
+ case 14: vr.origin_position[1] = NAN; break;
+ case 15: vr.views.eye[1].orientation[0] = NAN; break;
+ }
+ CHECK(!halo_vr_gaze_tracking_valid());
+ tutorial_turn_input(0.9f, 0.016f); note_idle();
+ CHECK(tutorial.action_flags == 0 && !vr.tutorial_head_valid && vr.tutorial_turn_flags == 0);
+ }
+ tutorial_reset(); vr.frame = 0; vr.pending = 1; CHECK(halo_vr_gaze_tracking_valid());
+ vr.views.focused = 0; tutorial_turn_input(0.9f, 0.016f);
+ vr.views.focused = 1; tutorial_turn_input(0.9f, 0.016f); note_idle();
+ CHECK(vr.body_yaw == 0.f && tutorial.action_flags == 0); /* recovery requires release */
+ tutorial_turn_input(0.f, 0.016f); tutorial_turn_input(0.9f, 0.016f); note_idle();
+ CHECK(TEST_FLAG(tutorial.action_flags, _player_control_look_relative_right_bit));
+ tutorial_reset(); tutorial_turn_input(0.9f, 0.016f); vr.views.focused = 0; note_idle();
+ CHECK(tutorial.action_flags == 0 && vr.tutorial_turn_flags == 0); /* queued event invalidated */
+ tutorial_reset(); vr.views.focused = 0; tutorial_turn_input(0.f, 0.016f);
+ head_pose(0.9f, 0.4f); vr.views.focused = 1; note_idle();
+ CHECK(tutorial.action_flags == 0 && vr.tutorial_head_valid); /* no game tick during loss */
+ tutorial_reset(); tutorial_turn_input(0.9f, 0.016f); halo_vr_menus(1);
+ CHECK(vr.tutorial_turn_flags == 0 && !vr.tutorial_head_valid);
+ head_pose(0.9f, 0.4f); halo_vr_menus(0); note_idle(); CHECK(tutorial.action_flags == 0);
+}
+'''
+
+
+GAZE_HARNESS = r'''
+#define _USE_MATH_DEFINES
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#define NONE (-1)
+#define TRUE 1
+#define FALSE 0
+#define FLAG(bit) (1UL << (bit))
+#define TEST_FLAG(value, bit) (((value) & FLAG(bit)) != 0)
+#define VALID_INDEX(index, count) ((index) >= 0 && (index) < (count))
+#define NUMBEROF(array) ((short)(sizeof(array) / sizeof((array)[0])))
+#define DEGREES_TO_RADIANS(value) ((value) * 0.01745329251994329577f)
+#define cosine cosf
+#define TAG_BLOCK_GET_ELEMENT(block, index, type) (&((type *)(block)->address)[index])
+typedef int boolean;
+typedef float real;
+typedef struct { float x, y, z; } real_point3d;
+typedef struct { float i, j, k; } real_vector3d;
+enum { _object_dead_bit = 0, _object_type_scenery = 6 };
+struct object_fields { unsigned long damage_flags; short type, name_index; real_point3d bounding_sphere_center; };
+struct unit_datum { struct object_fields object; struct { long player_index; real_vector3d looking_vector; } unit; };
+struct object_datum { struct object_fields object; };
+struct player_fixture { long unit_index; short local_player_index; };
+struct object_marker { struct { real_point3d position; } matrix; };
+struct scenario_object_name { char name[32]; short runtime_object_type, runtime_scenario_datum_index; };
+struct scenario { struct { long count; void *address; } object_names; };
+static struct unit_datum units[3];
+static struct object_datum target;
+static struct player_fixture players[2];
+static struct scenario_object_name fixture_names[6];
+static struct scenario scenario_fixture;
+static long global_scenario_index;
+static const char *scenario_path;
+static real_point3d sim_origin, head_origin, hand_origin;
+static real_vector3d head_forward, hand_forward;
+static int gaze_valid, pointing_valid, gaze_calls, pointing_calls, checks;
+#define CHECK(condition) do { checks++; if (!(condition)) { \
+ fprintf(stderr, "failed gaze line %d: %s\n", __LINE__, #condition); exit(1); } } while (0)
+static struct unit_datum *unit_get(long index) { CHECK(index >= 0 && index < 3); return &units[index]; }
+static struct unit_datum *unit_try_and_get(long index) { return index >= 0 && index < 3 ? &units[index] : NULL; }
+static struct object_datum *object_get(long index) { CHECK(index == 5); return &target; }
+static struct player_fixture *player_get(long index) { CHECK(index >= 0 && index < 2); return &players[index]; }
+static struct scenario *global_scenario_get(void) { return &scenario_fixture; }
+static const char *tag_get_name(long index) { CHECK(index == 9); return scenario_path; }
+static void object_get_marker_by_name(long index, const char *name, struct object_marker *marker, int count) {
+ (void)index; CHECK(!strcmp(name, "head") && count == 1); marker->matrix.position = sim_origin;
+}
+static void unit_get_head_position(long index, real_point3d *point) { (void)index; *point = target.object.bounding_sphere_center; }
+static void vector_from_points3d(const real_point3d *from, const real_point3d *to, real_vector3d *vector) {
+ vector->i = to->x - from->x; vector->j = to->y - from->y; vector->k = to->z - from->z;
+}
+static float normalize3d(real_vector3d *v) {
+ float length = sqrtf(v->i*v->i + v->j*v->j + v->k*v->k);
+ if (length > 0.f) { v->i /= length; v->j /= length; v->k /= length; }
+ return length;
+}
+static float dot_product3d(const real_vector3d *a, const real_vector3d *b) {
+ return a->i*b->i + a->j*b->j + a->k*b->k;
+}
+/* Presentation math/freshness is independently tested in the production
+ reticle harness. These boundary stubs preserve its unchanged-output and
+ living-owned-unarmed eligibility contract while testing the real callers. */
+boolean vr_render_player_gaze(long index, real_point3d *origin, real_vector3d *forward) {
+ gaze_calls++;
+ if (!gaze_valid) return FALSE;
+ CHECK(index == 0); *origin = head_origin; *forward = head_forward; return TRUE;
+}
+boolean vr_render_player_pointing_ray(long index, real_point3d *origin, real_vector3d *forward) {
+ pointing_calls++;
+ if (!pointing_valid || index != 0 || units[index].unit.player_index != 0 ||
+ players[0].local_player_index != 0 || players[0].unit_index != index ||
+ TEST_FLAG(units[index].object.damage_flags, _object_dead_bit)) return FALSE;
+ *origin = hand_origin; *forward = hand_forward; return TRUE;
+}
+@@FUNCTIONS@@
+static void reset_gaze(void) {
+ int index;
+ memset(units, 0, sizeof(units)); memset(players, 0, sizeof(players));
+ memset(&target, 0, sizeof(target)); memset(fixture_names, 0, sizeof(fixture_names));
+ for (index = 0; index < 3; index++) {
+ units[index].unit.player_index = NONE; units[index].unit.looking_vector.i = 1.f;
+ }
+ units[0].unit.player_index = 0; players[0].unit_index = 0; players[0].local_player_index = 0;
+ units[1].unit.player_index = 1; players[1].unit_index = 1; players[1].local_player_index = NONE;
+ sim_origin = (real_point3d){0.f, 0.f, 0.f}; head_origin = (real_point3d){0.4f, -0.3f, 1.7f};
+ hand_origin = (real_point3d){0.6f, -0.5f, 1.1f};
+ head_forward = (real_vector3d){0.f, 1.f, 0.f}; hand_forward = (real_vector3d){1.f, 0.f, 0.f};
+ target.object.type = _object_type_scenery; target.object.name_index = 0;
+ target.object.bounding_sphere_center = (real_point3d){5.6f, -0.5f, 1.1f};
+ snprintf(fixture_names[0].name, sizeof(fixture_names[0].name), "%s", "looking_panel_center"); scenario_fixture.object_names.count = 6;
+ scenario_fixture.object_names.address = fixture_names; global_scenario_index = 9; scenario_path = "levels\\a10\\a10";
+ gaze_valid = pointing_valid = 1; gaze_calls = pointing_calls = 0;
+}
+static void check_technician_gaze(void) {
+ real_point3d point;
+ int exclusion, angle;
+ reset_gaze(); point = (real_point3d){0.4f, 4.7f, 1.7f};
+ CHECK(unit_can_see_point(0, &point, DEGREES_TO_RADIANS(15.f))); /* HMD vs frozen sim */
+ CHECK(!unit_can_see_point(1, &point, DEGREES_TO_RADIANS(15.f))); /* remote */
+ CHECK(!unit_can_see_point(2, &point, DEGREES_TO_RADIANS(15.f))); /* AI */
+ CHECK(!unit_can_see_point(NONE, &point, DEGREES_TO_RADIANS(15.f)));
+ for (angle = 0; angle < 2; angle++) {
+ float gate = angle ? 20.f : 15.f;
+ point = (real_point3d){head_origin.x + 5.f*sinf(DEGREES_TO_RADIANS(gate - 0.1f)),
+ head_origin.y + 5.f*cosf(DEGREES_TO_RADIANS(gate - 0.1f)), head_origin.z};
+ CHECK(unit_can_see_point(0, &point, DEGREES_TO_RADIANS(gate)));
+ point.x = head_origin.x + 5.f*sinf(DEGREES_TO_RADIANS(gate + 0.1f));
+ point.y = head_origin.y + 5.f*cosf(DEGREES_TO_RADIANS(gate + 0.1f));
+ CHECK(!unit_can_see_point(0, &point, DEGREES_TO_RADIANS(gate)));
+ }
+ for (exclusion = 0; exclusion < 5; exclusion++) {
+ reset_gaze(); point = (real_point3d){0.4f, 4.7f, 1.7f};
+ switch (exclusion) {
+ case 0: units[0].unit.player_index = NONE; break;
+ case 1: units[0].object.damage_flags = FLAG(_object_dead_bit); break;
+ case 2: players[0].local_player_index = 1; break;
+ case 3: players[0].unit_index = 2; break;
+ case 4: gaze_valid = 0; break;
+ }
+ CHECK(!unit_can_see_point(0, &point, DEGREES_TO_RADIANS(15.f)));
+ CHECK(gaze_calls == (exclusion == 4));
+ point = (real_point3d){5.f, 0.f, 0.f};
+ CHECK(unit_can_see_point(0, &point, DEGREES_TO_RADIANS(15.f))); /* baseline preserved */
+ }
+}
+static void check_panel_eligibility(void) {
+ int exclusion, index;
+ static const char *const active[] = {"looking_panel_center", "looking_panel_left", "looking_panel_right",
+ "looking_panel_up", "looking_panel_down"};
+ for (index = 0; index < 5; index++) {
+ reset_gaze(); snprintf(fixture_names[0].name, sizeof(fixture_names[0].name), "%s", active[index]);
+ CHECK(hs_unit_can_see_object(0, 5, 5.f)); CHECK(pointing_calls == 1);
+ }
+ for (exclusion = 0; exclusion < 13; exclusion++) {
+ reset_gaze();
+ switch (exclusion) {
+ case 0: scenario_path = "levels\\a30\\a30"; break;
+ case 1: global_scenario_index = NONE; break;
+ case 2: target.object.type = 7; break;
+ case 3: target.object.name_index = NONE; break;
+ case 4: target.object.name_index = 6; break;
+ case 5: snprintf(fixture_names[0].name, sizeof(fixture_names[0].name), "%s", "looking_panel_center_success"); break;
+ case 6: snprintf(fixture_names[0].name, sizeof(fixture_names[0].name), "%s", "looking_panel_center_extra"); break;
+ case 7: memset(fixture_names[0].name, 'x', sizeof(fixture_names[0].name)); break;
+ case 8: pointing_valid = 0; break; /* armed, vehicle, cinematic, stale pose */
+ case 9: units[0].object.damage_flags = FLAG(_object_dead_bit); break;
+ case 10: players[0].local_player_index = 1; break;
+ case 11: players[0].unit_index = 2; break;
+ case 12: units[0].unit.player_index = NONE; break;
+ }
+ CHECK(!hs_unit_can_see_object(0, 5, 5.f));
+ }
+ reset_gaze(); CHECK(!hs_unit_can_see_object(0, 5, 6.f)); CHECK(pointing_calls == 0);
+ CHECK(!hs_unit_can_see_object(1, 5, 5.f)); /* remote retains simulation result */
+ CHECK(!hs_unit_can_see_object(NONE, NONE, 5.f));
+ reset_gaze(); head_forward = hand_forward; head_origin = hand_origin;
+ pointing_valid = 0; CHECK(hs_unit_can_see_object(0, 5, 5.f)); CHECK(pointing_calls == 0); /* HMD also accepted */
+}
+static void check_panel_cone_origin_height(void) {
+ int height, distance, outside, axis;
+ for (height = -2; height <= 4; height++) for (distance = 1; distance <= 12; distance++) {
+ for (outside = 0; outside < 2; outside++) for (axis = 0; axis < 2; axis++) {
+ float angle = DEGREES_TO_RADIANS(outside ? 5.1f : 4.9f);
+ reset_gaze(); hand_origin.z = (float)height * 0.6f;
+ target.object.bounding_sphere_center.x = hand_origin.x + distance * cosf(angle);
+ target.object.bounding_sphere_center.y = hand_origin.y + (axis ? 0.f : distance * sinf(angle));
+ target.object.bounding_sphere_center.z = hand_origin.z + (axis ? distance * sinf(angle) : 0.f);
+ CHECK(hs_unit_can_see_object(0, 5, 5.f) == !outside);
+ }
+ }
+ reset_gaze(); target.object.bounding_sphere_center = hand_origin;
+ CHECK(!hs_unit_can_see_object(0, 5, 5.f)); /* degenerate direction */
+}
+int main(void) {
+ check_technician_gaze(); check_panel_eligibility(); check_panel_cone_origin_height();
+ printf("PASS: %d production tutorial HMD / named-panel ray assertions\n", checks);
+ return 0;
+}
+'''
+
+
+def compile_run(harness, compiler, vcvars):
+ with tempfile.TemporaryDirectory(prefix='halo-vr-tutorial-', ignore_cleanup_errors=True) as directory:
+ temp = Path(directory)
+ source = temp / 'test.c'
+ exe = temp / ('test.exe' if os.name == 'nt' else 'test')
+ source.write_text(harness)
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX', '/D_CRT_SECURE_NO_WARNINGS',
+ '/DHALO_VR', '/I' + str(ROOT / 'port/linux/src'), '/Fe:' + str(exe),
+ '/Fo:' + str(temp / 'test.obj'), str(source)]
+ if vcvars:
+ for path in (vcvars, Path(compiler), temp):
+ if any(character in str(path) for character in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported compiler/temp shell metacharacter')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\n'
+ 'if errorlevel 1 exit /b 1\n' + subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-DHALO_VR',
+ '-I' + str(ROOT / 'port/linux/src'), str(source),
+ *([] if os.name == 'nt' else ['-lm']), '-o', str(exe)]
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(exe)], cwd=temp, check=True)
+
+
+def verify_xbox_a10(path):
+ """Bounded passive inspection; return facts, never distribute retail scripts."""
+ raw = path.read_bytes()
+ assert 2048 < len(raw) < 600_000_000 and raw[:4] == b'daeh'
+ declared = struct.unpack_from('<I', raw, 8)[0]
+ assert 2048 < declared < 600_000_000
+ stream = zlib.decompressobj()
+ expanded = stream.decompress(raw[2048:], declared - 2048 + 1)
+ assert stream.eof and not stream.unconsumed_tail and len(expanded) == declared - 2048
+ data = raw[:2048] + expanded
+ assert struct.unpack_from('<I', data, 4)[0] == 5
+ def u32(offset):
+ assert 0 <= offset <= len(data) - 4
+ return struct.unpack_from('<I', data, offset)[0]
+ index = u32(16)
+ table = index + 36
+ magic = u32(index) - table
+ tag_count = u32(index + 12)
+ assert 0 < tag_count < 65536 and table + tag_count * 32 <= len(data)
+ scenario_handle = u32(index + 4)
+ scenario = u32(table + (scenario_handle & 65535)*32 + 20) - magic
+ assert 0 <= scenario < len(data) - 1188
+ syntax, strings = u32(scenario + 1152) - magic, u32(scenario + 1172) - magic
+ strings_size, syntax_size = u32(scenario + 1160), u32(scenario + 1140)
+ assert 0 <= strings <= len(data) - strings_size and 0 <= syntax <= len(data) - syntax_size
+ count = struct.unpack_from('<H', data, syntax + 46)[0]
+ assert 56 + count * 20 <= syntax_size
+ nodes = [struct.unpack_from('<4H3I', data, syntax + 56 + n*20) for n in range(count)]
+ def string(offset):
+ assert 0 <= offset < strings_size
+ end = data.index(b'\0', strings + offset, strings + strings_size)
+ return data[strings + offset:end].decode('ascii')
+ def expression(handle, ancestors=frozenset()):
+ n = handle & 65535
+ assert n < count and n not in ancestors
+ node = nodes[n]
+ if node[3] & 1:
+ if node[3] & 4:
+ return string(node[5]) if node[5] < strings_size else node[6]
+ if node[2] == 6:
+ return struct.unpack('<f', struct.pack('<I', node[6]))[0]
+ if node[2] == 5:
+ return bool(node[6] & 1)
+ return string(node[5]) if node[5] < strings_size else node[6]
+ children = []
+ child = node[6]
+ seen = set()
+ while child != 0xffffffff:
+ slot = child & 65535
+ assert slot < count and slot not in seen
+ seen.add(slot)
+ children.append(expression(child, ancestors | {n}))
+ child = nodes[slot][4]
+ return children
+ script_count, script_table = u32(scenario + 1180), u32(scenario + 1184) - magic
+ assert 0 < script_count < 4096 and 0 <= script_table <= len(data) - script_count*92
+ scripts = {}
+ for n in range(script_count):
+ offset = script_table + n*92
+ name = data[offset:offset+32].split(b'\0', 1)[0].decode('ascii')
+ if name in ('tutorial_looking', 'tutorial_setup', 'test_looking_tech', 'mission_a10') or name.startswith('test_looking_cycle_'):
+ scripts[name] = expression(u32(offset + 36))
+ def calls(tree, name):
+ found = []
+ if isinstance(tree, list):
+ if tree and tree[0] == name:
+ found.append(tree)
+ for child in tree:
+ found.extend(calls(child, name))
+ return found
+ look = scripts['tutorial_looking']
+ assert calls(look, 'player_action_test_reset')
+ for direction in ('left', 'right', 'up', 'down'):
+ assert calls(look, 'player_action_test_look_relative_' + direction)
+ assert any(len(call) == 2 and call[1] is False for call in calls(scripts['tutorial_setup'], 'player_enable_input'))
+ for name in ('center', 'left', 'right', 'up', 'down'):
+ gate = calls(scripts['test_looking_cycle_' + name], 'objects_can_see_object')
+ assert any('looking_panel_' + name in call and call[-1] == 5.0 for call in gate)
+ assert any(call[-1] == 15.0 for call in calls(scripts['test_looking_tech'], 'objects_can_see_object'))
+ assert any(call[-1] == 20.0 for call in calls(scripts['test_looking_tech'], 'objects_can_see_object'))
+ # The five actual named targets are scenery; success variants are different names.
+ name_count, name_table = u32(scenario + 516), u32(scenario + 520) - magic
+ assert 5 <= name_count < 65536 and 0 <= name_table <= len(data) - name_count*36
+ for n, suffix in enumerate(('center', 'right', 'left', 'up', 'down')):
+ offset = name_table + n*36
+ assert data[offset:offset+32].split(b'\0', 1)[0].decode('ascii') == 'looking_panel_' + suffix
+ assert struct.unpack_from('<h', data, offset+32)[0] == 6
+ # Check tutorial routing remains authored by the map: co-op and difficulty.
+ mission = scripts['mission_a10']
+ assert calls(mission, 'game_is_cooperative') and calls(mission, 'game_difficulty_get')
+ print('PASS: passive original Xbox a10 predicates: input-disabled look, all four direction flags, '
+ 'five named scenery targets at 5 degrees, technician 15/20 degrees, authored difficulty/co-op routing')
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ parser.add_argument('--xbox-a10', type=Path, help='Authorized original Xbox a10.map, passive read only')
+ args = parser.parse_args()
+ vr = (ROOT / 'port/linux/src/vr.c').read_text()
+ player = (ROOT / 'source/game/player_control.c').read_text()
+ hook, aim_guard = walking.source_checks(vr, player)
+ dispatch_function = function(player, 'handle_one_player_input')
+ start = dispatch_function.index('if (local_player_index == 0)')
+ opening = dispatch_function.index('{', start)
+ # This integration block is deliberately simple and has one nested-free body.
+ gate = dispatch_function[start:dispatch_function.index('}', opening) + 1]
+ assert 'halo_vr_turn_control' in gate and 'player_control_camera_control_is_active()' in gate
+ assert 'player_input_is_enabled' not in walking.masked(gate)
+ assert player.index('halo_vr_turn_control(') < player.index('get_local_player_input_blob(', player.index('static void handle_one_player_input('))
+ harness = walking.HARNESS
+ harness = harness.replace('@@FUNCTIONS@@',
+ 'static struct { int active,armed,down,blocked,position_valid; short x,y; } menu_pointer;\n@@FUNCTIONS@@')
+ harness = harness.replace('int tutorial_head_valid, tutorial_turn_flags, turn_control_allowed;',
+ 'int tutorial_head_valid, tutorial_turn_flags, turn_control_allowed, snap_latched;')
+ harness = harness.replace('int head_movement, controller_aim, gamepad_aim, gamepad_snap, standing;',
+ 'int head_movement, controller_aim, gamepad_aim, gamepad_snap, standing, smooth_turn;')
+ harness = harness.replace('float snap_angle, player_height;', 'float snap_angle, player_height, smooth_speed;')
+ harness = harness.replace('real_euler_angles2d desired_angles;', 'long unit_index; real_euler_angles2d desired_angles;')
+ harness = harness.replace('unsigned long action_flags;', 'unsigned long action_flags, action_test_flags;')
+ functions = '\n\n'.join(function(vr, name) for name in (
+ 'quaternion_rotate', 'quaternion_yaw', 'game_axes', 'vehicle_view_active', 'vehicle_capture', 'recentre', 'halo_vr_locomotion',
+ 'halo_vr_gaze_tracking_valid', 'halo_vr_turn_control', 'halo_vr_tutorial_turn',
+ 'tutorial_turn_input', 'halo_vr_tutorial_look', 'halo_vr_tutorial_reset',
+ 'halo_vr_aim', 'halo_vr_aim_release', 'halo_vr_menus'))
+ functions = ('#define HALO_VR_TUTORIAL_TURN_LEFT 1\n#define HALO_VR_TUTORIAL_TURN_RIGHT 2\n'
+ 'static int driving_turn_blocked(float x) {(void)x;return 0;}\n' + functions)
+ harness = harness.replace('@@FUNCTIONS@@', functions).replace('@@HOOK@@', hook).replace('@@AIM_GUARD@@', aim_guard)
+ harness = harness.replace('@@TUTORIAL@@', function(player, 'player_control_action_test_note') + '\n\n' +
+ function(player, 'player_control_action_test_reset'))
+ harness = harness.replace('int main(void) {', TURN_CHECKS.replace('@@TURN_CONTROL@@', gate) + '\nint main(void) {\n'
+ ' check_snap_and_reset(); check_smooth_and_controls(); check_pose_masks();')
+ gaze = GAZE_HARNESS.replace('@@FUNCTIONS@@', '\n\n'.join((
+ function((ROOT / 'source/units/units.c').read_text(), 'unit_can_see_point'),
+ function((ROOT / 'source/hs/hs_library_external.c').read_text(), 'hs_vr_calibration_panel'),
+ function((ROOT / 'source/hs/hs_library_external.c').read_text(), 'hs_unit_can_see_object'))))
+ compiler = args.cc or os.environ.get('CC')
+ vcvars = args.vcvarsall
+ if not compiler and os.name == 'nt':
+ base = Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio'
+ candidates = sorted(base.glob('*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1])
+ vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('No C compiler; select --cc')
+ compile_run(harness, compiler, vcvars)
+ compile_run(gaze, compiler, vcvars)
+ if args.xbox_a10:
+ verify_xbox_a10(args.xbox_a10)
+ else:
+ print('SKIP: optional original Xbox a10 data verification (provide --xbox-a10)')
+ print('PASS: unchanged armed aim / Xbox controls; no tutorial script, difficulty or skip changes')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_tutorial_buttons.py b/tools/test_vr_tutorial_buttons.py
new file mode 100644
index 0000000000000000000000000000000000000000..29f780a9132add7a896623679d28377b306220b3
--- /dev/null
+++ b/tools/test_vr_tutorial_buttons.py
@@ -0,0 +1,288 @@
+#!/usr/bin/env python3
+"""Run production tutorial button consumption and inversion persistence logic.
+
+The same extracted input function is compiled with and without its VR branch,
+so the legacy melee leak and unchanged flat behavior are tested directly. The
+profile setter is also extracted; its disk/HUD boundaries are controlled stubs,
+not a claim that the reported headset crash has been reproduced. VR activity
+links a separate translation unit with the real vr.h declaration and extracted
+vr.c definition; no invented activity API is provided by this harness.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def function(text, name):
+ hidden = re.sub(r'/\*.*?\*/|//[^\n]*|"(?:\\.|[^"\\])*"',
+ lambda match: re.sub(r'[^\n]', ' ', match.group()), text, flags=re.S)
+ found = re.search(r'(?:static\s+)?(?:boolean|void|int)\s+' + re.escape(name) +
+ r'\s*\([^;]*?\)\s*\{', hidden)
+ if not found:
+ raise AssertionError('Missing production function: ' + name)
+ start = found.start()
+ opening = hidden.index('{', start)
+ depth, end = 1, opening + 1
+ while depth:
+ depth += (hidden[end] == '{') - (hidden[end] == '}')
+ end += 1
+ return text[start:end]
+
+
+HARNESS = r'''
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <wchar.h>
+@@VR_HEADER@@
+typedef int boolean;
+#define FALSE 0
+#define TRUE 1
+#define NONE (-1)
+#define FLAG(bit) (1u << (bit))
+#define TEST_FLAG(value,bit) ((value) & FLAG(bit))
+#define SET_FLAG(value,bit,on) ((value) = ((value) & ~FLAG(bit)) | ((on) ? FLAG(bit) : 0u))
+#define UNICODE_STRING_LIST_TAG 123
+enum {
+ _player_control_action_bit, _player_control_accept_bit, _player_control_back_bit,
+ _player_control_zoom_bit, _player_control_input_zoom_bit,
+ _player_control_rotate_weapons_bit, _player_control_rotate_grenades_bit
+};
+enum {
+ _unit_control_action_bit, _unit_control_jump_bit, _unit_control_use_equipment_bit,
+ _unit_control_throw_grenade_bit, _unit_control_weapon_secondary_trigger_bit,
+ _unit_control_weapon_primary_trigger_bit
+};
+struct facing { float yaw,pitch; };
+struct throttle { float i,j; };
+struct input_blob {
+ boolean accept,back; unsigned unit_control_flags,player_control_flags;
+ struct facing facing_delta; struct throttle throttle; float primary_trigger;
+};
+struct player_control_globals_data {
+ unsigned action_flags,action_test_flags,suppressed_action_flags;
+};
+static struct player_control_globals_data fixture_globals;
+static struct player_control_globals_data *player_control_globals = &fixture_globals;
+static boolean controls_swapped;
+static int cinematic_skip, coop_skip, voted, skipped, votes, notes, checks;
+void fixture_set_vr_running(int initialized);
+static int cinematic_can_be_skipped(void) { return cinematic_skip; }
+static int network_coop_skip_offered(void) { return coop_skip; }
+static int network_coop_vote_skip(void) { ++votes; return voted; }
+static void main_skip_cinematic(void) { ++skipped; }
+static void player_control_action_test_note(unsigned flags,const struct facing *f,
+ const struct throttle *t,float trigger) {
+ (void)flags; (void)f; (void)t; (void)trigger; ++notes;
+}
+struct player_profile { struct { boolean invert_look; } controller_settings; };
+static struct {
+ struct { struct player_profile profile; long active_profile_index; } local_players[4];
+} player_ui_globals;
+static int saved, messaged, applied, saved_inversion, applied_inversion;
+static long string_tag = 22;
+static const wchar_t message_text[] = L"Profile saved";
+static long tag_loaded(int group,const char *path) {
+ if (group != UNICODE_STRING_LIST_TAG || strcmp(path,"ui\\shell\\strings\\temp_strings")) abort();
+ return string_tag;
+}
+static const wchar_t *unicode_string_list_get_string(long tag,long index) {
+ if(tag != string_tag || index != 1) abort(); return message_text;
+}
+static void hud_message_to_all(const wchar_t *message) { if(!message) abort(); ++messaged; }
+static void player_profile_save(long index,const struct player_profile *profile) {
+ if(index != 7 || !profile) abort(); ++saved; saved_inversion=profile->controller_settings.invert_look;
+}
+static void set_local_player_controls_from_player_profile(short local) {
+ if(local != 0) abort(); ++applied;
+ applied_inversion=player_ui_globals.local_players[0].profile.controller_settings.invert_look;
+}
+#define CHECK(condition) do { ++checks; if(!(condition)) { \
+ fprintf(stderr,"FAIL line %d: %s\n",__LINE__,#condition); exit(1); } } while(0)
+
+/* Compile the actual legacy branch alongside the VR-enabled production code. */
+#undef HALO_VR
+@@LEGACY@@
+#define HALO_VR 1
+@@INPUT@@
+@@BACK@@
+@@ACCEPT@@
+@@INVERT@@
+
+static struct input_blob button_input(boolean down) {
+ struct input_blob input={0};
+ input.back=down;
+ input.unit_control_flags=FLAG(_unit_control_use_equipment_bit) |
+ FLAG(_unit_control_throw_grenade_bit) | FLAG(_unit_control_weapon_secondary_trigger_bit) |
+ FLAG(_unit_control_weapon_primary_trigger_bit);
+ input.player_control_flags=FLAG(_player_control_rotate_weapons_bit) |
+ FLAG(_player_control_rotate_grenades_bit) | FLAG(_player_control_input_zoom_bit);
+ return input;
+}
+static void consumed(const struct input_blob *input) {
+ CHECK(!TEST_FLAG(input->unit_control_flags,_unit_control_use_equipment_bit));
+ CHECK(!TEST_FLAG(input->unit_control_flags,_unit_control_throw_grenade_bit));
+ CHECK(!TEST_FLAG(input->unit_control_flags,_unit_control_weapon_secondary_trigger_bit));
+ CHECK(!TEST_FLAG(input->player_control_flags,_player_control_rotate_weapons_bit));
+ CHECK(!TEST_FLAG(input->player_control_flags,_player_control_rotate_grenades_bit));
+ CHECK(!TEST_FLAG(input->player_control_flags,_player_control_input_zoom_bit));
+ CHECK(TEST_FLAG(input->unit_control_flags,_unit_control_weapon_primary_trigger_bit));
+}
+static void legacy_leak_and_release(void) {
+ int swapped;
+ for(swapped=0;swapped<2;++swapped) {
+ struct input_blob input;
+ controls_swapped=swapped; fixture_set_vr_running(1); memset(&fixture_globals,0,sizeof(fixture_globals));
+ CHECK(halo_vr_running());
+ CHECK(!player_control_action_test_back()); input=button_input(TRUE);
+ original_action_test_input(&input);
+ CHECK(TEST_FLAG(input.unit_control_flags,_unit_control_use_equipment_bit));
+ memset(&fixture_globals,0,sizeof(fixture_globals)); CHECK(!player_control_action_test_back());
+ input=button_input(TRUE); player_control_action_test_check_reset_input_blob(&input);
+ consumed(&input); CHECK(player_control_action_test_back());
+ CHECK(TEST_FLAG(fixture_globals.suppressed_action_flags,_player_control_back_bit));
+ /* Script reset while B remains held must retain suppression. */
+ fixture_globals.action_test_flags=0; fixture_globals.action_flags=0;
+ input=button_input(TRUE); player_control_action_test_check_reset_input_blob(&input);
+ consumed(&input); CHECK(TEST_FLAG(fixture_globals.suppressed_action_flags,_player_control_back_bit));
+ input=button_input(FALSE); player_control_action_test_check_reset_input_blob(&input);
+ consumed(&input); CHECK(!TEST_FLAG(fixture_globals.suppressed_action_flags,_player_control_back_bit));
+ input=button_input(TRUE); player_control_action_test_check_reset_input_blob(&input);
+ CHECK(TEST_FLAG(input.unit_control_flags,_unit_control_use_equipment_bit));
+ CHECK(TEST_FLAG(input.unit_control_flags,_unit_control_throw_grenade_bit));
+ CHECK(TEST_FLAG(input.player_control_flags,_player_control_rotate_weapons_bit));
+ CHECK(TEST_FLAG(input.player_control_flags,_player_control_rotate_grenades_bit));
+ }
+}
+static void flat_equivalence(void) {
+ unsigned requested,suppressed; int swapped,accept,back;
+ fixture_set_vr_running(0);
+ CHECK(!halo_vr_running());
+ for(requested=0;requested<8;++requested) for(suppressed=0;suppressed<8;++suppressed)
+ for(swapped=0;swapped<2;++swapped) for(accept=0;accept<2;++accept) for(back=0;back<2;++back) {
+ struct player_control_globals_data initial={0}, expected;
+ struct input_blob before=button_input(back), original, current;
+ before.accept=accept; before.unit_control_flags|=FLAG(_unit_control_action_bit)|FLAG(_unit_control_jump_bit);
+ initial.action_test_flags=requested;
+ initial.suppressed_action_flags=suppressed;
+ controls_swapped=swapped; fixture_globals=initial; original=before;
+ original_action_test_input(&original); expected=fixture_globals;
+ fixture_globals=initial; current=before; player_control_action_test_check_reset_input_blob(&current);
+ CHECK(!memcmp(&original,&current,sizeof(current)));
+ CHECK(!memcmp(&expected,&fixture_globals,sizeof(fixture_globals)));
+ }
+}
+static void inversion_and_accept(void) {
+ struct input_blob input={0};
+ fixture_set_vr_running(1); controls_swapped=0; memset(&fixture_globals,0,sizeof(fixture_globals));
+ memset(&player_ui_globals,0,sizeof(player_ui_globals));
+ player_ui_globals.local_players[0].active_profile_index=7;
+ player0_look_invert_pitch(TRUE);
+ CHECK(saved==1 && applied==1 && messaged==1 && saved_inversion && applied_inversion);
+ CHECK(!player_control_action_test_accept()); CHECK(!player_control_action_test_back());
+ input=button_input(TRUE); player_control_action_test_check_reset_input_blob(&input);
+ CHECK(!player_control_action_test_accept() && player_control_action_test_back());
+ consumed(&input);
+ /* This is the tutorial's Back branch; persistence remains the real setter. */
+ player0_look_invert_pitch(FALSE);
+ CHECK(saved==2 && applied==2 && messaged==2 && !saved_inversion && !applied_inversion);
+ CHECK(!player_ui_globals.local_players[0].profile.controller_settings.invert_look);
+ player0_look_invert_pitch(TRUE);
+ CHECK(saved==3 && applied==3 && saved_inversion && applied_inversion);
+ player_ui_globals.local_players[0].active_profile_index=NONE;
+ player0_look_invert_pitch(FALSE);
+ CHECK(saved==3 && messaged==3 && applied==4 && !applied_inversion);
+ player_ui_globals.local_players[0].active_profile_index=7; string_tag=NONE;
+ player0_look_invert_pitch(TRUE); CHECK(saved==4 && messaged==4 && applied==5);
+ memset(&fixture_globals,0,sizeof(fixture_globals)); input=(struct input_blob){0}; input.accept=TRUE;
+ input.unit_control_flags=FLAG(_unit_control_jump_bit)|FLAG(_unit_control_action_bit);
+ CHECK(!player_control_action_test_accept());
+ player_control_action_test_check_reset_input_blob(&input);
+ CHECK(player_control_action_test_accept() && !player_control_action_test_back());
+ CHECK(TEST_FLAG(input.unit_control_flags,_unit_control_jump_bit));
+ cinematic_skip=1; coop_skip=1; voted=0;
+ player_control_action_test_check_reset_input_blob(&input); CHECK(votes==1 && skipped==1);
+ voted=1; player_control_action_test_check_reset_input_blob(&input); CHECK(votes==2 && skipped==1);
+}
+int main(void) {
+ legacy_leak_and_release(); flat_equivalence(); inversion_and_accept();
+ CHECK(notes>0);
+ printf("PASS: %d production tutorial-button/inversion assertions\n",checks);
+ return 0;
+}
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ args = parser.parse_args()
+ controls = (ROOT / 'source/game/player_control.c').read_text()
+ profile = (ROOT / 'source/interface/player_ui.c').read_text()
+ vr = (ROOT / 'port/linux/src/vr.c').read_text()
+ vr_header = ROOT / 'port/linux/src/vr.h'
+ header = vr_header.read_text()
+ assert 'input->back = gamepad->buttons[_gamepad_analog_button_b];' in controls
+ assert 'player_profile_save(' in function(profile, 'player0_look_invert_pitch')
+ actual = function(controls, 'player_control_action_test_check_reset_input_blob')
+ assert 'halo_vr_active' not in actual, 'Do not mask a nonexistent VR activity API.'
+ assert 'halo_vr_running() != 0' in actual
+ assert re.search(r'\bint\s+halo_vr_running\s*\(\s*void\s*\)\s*;', header)
+ production_activity = function(vr, 'halo_vr_running')
+ include = '#include "' + vr_header.as_posix() + '"'
+ activity_source = (include + '\nstatic struct { int initialized; } vr;\n' +
+ production_activity + '\nvoid fixture_set_vr_running(int initialized) {\n' +
+ ' vr.initialized = initialized;\n}\n')
+ harness = HARNESS.replace('@@INPUT@@', actual).replace('@@LEGACY@@', actual.replace(
+ 'player_control_action_test_check_reset_input_blob', 'original_action_test_input'))
+ harness = harness.replace('@@VR_HEADER@@', include)
+ for placeholder,name in (('@@BACK@@','player_control_action_test_back'),
+ ('@@ACCEPT@@','player_control_action_test_accept')):
+ harness = harness.replace(placeholder,function(controls,name))
+ harness = harness.replace('@@INVERT@@',function(profile,'player0_look_invert_pitch'))
+ compiler = args.cc or os.environ.get('CC')
+ vcvars = args.vcvarsall
+ if not compiler and os.name == 'nt':
+ installations = Path(os.environ.get('ProgramFiles','C:/Program Files')) / 'Microsoft Visual Studio'
+ candidates = sorted(installations.glob('*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1]); vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('No C11 host compiler found; select one with --cc.')
+ compiler = str(Path(shutil.which(compiler) or compiler).resolve())
+ with tempfile.TemporaryDirectory(prefix='halo-vr-buttons-',ignore_cleanup_errors=True) as directory:
+ temp=Path(directory); source=temp/'test.c'; executable=temp/('test.exe' if os.name=='nt' else 'test')
+ activity=temp/'vr_activity.c'; activity_object=temp/('vr_activity.obj' if os.name=='nt' else 'vr_activity.o')
+ source.write_text(harness)
+ activity.write_text(activity_source)
+ if Path(compiler).name.lower() in ('cl','cl.exe'):
+ activity_command=[compiler,'/nologo','/std:c11','/W4','/WX','/c',
+ '/Fo:'+str(activity_object),str(activity)]
+ command=[compiler,'/nologo','/std:c11','/W4','/WX','/D_CRT_SECURE_NO_WARNINGS',
+ '/Fe:'+str(executable),'/Fo:'+str(temp/'test.obj'),str(source),str(activity_object)]
+ else:
+ activity_command=[compiler,'-std=c11','-Wall','-Wextra','-Werror','-c',str(activity),'-o',str(activity_object)]
+ command=[compiler,'-std=c11','-Wall','-Wextra','-Werror',str(source),str(activity_object),'-o',str(executable)]
+ for index,step in enumerate((activity_command,command)):
+ if vcvars:
+ for path in (vcvars,Path(compiler),temp):
+ if any(character in str(path) for character in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported shell metacharacter in compiler/temp path.')
+ batch=temp/('compile-'+str(index)+'.cmd')
+ batch.write_text('@echo off\ncall "'+str(vcvars)+'" x64 >nul\nif errorlevel 1 exit /b 1\n'+
+ subprocess.list2cmdline(step)+'\n')
+ step=['cmd.exe','/d','/c',str(batch)]
+ subprocess.run(step,cwd=temp,check=True)
+ subprocess.run([str(executable)],cwd=temp,check=True)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_vehicle.py b/tools/test_vr_vehicle.py
new file mode 100644
index 0000000000000000000000000000000000000000..66bb251a2df47d6b7c5758d4433a70eeeb467168
--- /dev/null
+++ b/tools/test_vr_vehicle.py
@@ -0,0 +1,580 @@
+#!/usr/bin/env python3
+"""Run production seat/aim/view and occupied-glass code without retail data.
+
+Tracking and game services are mocked; the seat state header, platform functions,
+local-seat guards, glass predicate and transparent draw loop are production code.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+
+from test_vr_pointer_radar import function
+from test_vr_projectile_reticle import block_end
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def matrix_function(text, name):
+ match = re.search(r'real_point3d\s*\*\s*' + re.escape(name) + r'\s*\([^;]*?\)\s*\{', text)
+ assert match, name
+ return text[match.start():block_end(text, text.index('{', match.start()))]
+
+
+HARNESS = r'''
+#define _USE_MATH_DEFINES
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "vr.h"
+#include "vr_host.h"
+#include "vr_locomotion.h"
+#include "vr_vehicle.h"
+#include "vr_driving.h"
+#ifndef M_PI
+#define M_PI 3.14159265358979323846
+#endif
+#define HUD_WIDTH 1280
+#define HUD_HEIGHT 960
+#define METRES_PER_WORLD_UNIT 3.048f
+#define NONE (-1)
+#define TRUE 1
+#define FALSE 0
+#define TEST_FLAG(v,b) ((v) & (1u<<(b)))
+typedef int boolean;
+typedef float real;
+typedef struct { float i,j,k; } real_vector3d;
+typedef struct { float x,y,z; } real_point3d;
+typedef struct {
+ real scale;
+ union { real n[4][3]; struct { real_vector3d forward,left,up; real_point3d position; }; };
+} real_matrix4x3;
+static unsigned checks;
+#define CHECK(c) do { checks++; if (!(c)) { fprintf(stderr,"line %d: %s\n",__LINE__,#c); exit(1); } } while(0)
+static int near(float a,float b) { return fabsf(a-b)<0.0001f; }
+static struct {
+ int initialized,frame,pending,aim_synced,aiming,origin_valid,long_gun,render_pose_valid;
+ struct vr_host_views views;
+ struct vr_host_input input;
+ struct vr_host_pose menu_pose;
+ struct halo_vr_locomotion_heading movement_heading;
+ struct halo_vr_vehicle_reference vehicle;
+ float origin_position[3],origin_yaw,body_yaw,two_handed_weight;
+} vr;
+static struct {
+ int vehicle_first_person,vehicle_seat_calibration,vehicle_clear_windshield;
+ int standing,gamepad_aim,gamepad_snap,gamepad_pitch,controller_aim,floating_hands,arms;
+ float player_height,snap_angle,hud_size,world_scale,weapon_offset[3],shoulder_offset[3];
+} settings;
+static void settings_read(void) {}
+static struct halo_vr_driving_state driving;
+static void driving_reset(int release_required) { (void)release_required; }
+static int driving_aim(float *yaw,float *pitch) { (void)yaw; (void)pitch; return 0; }
+static void platform_log(const char *format,const char *mode) { (void)format; (void)mode; }
+int halo_vr_frame_active(void) { return vr.frame; }
+int host_vr_locate(struct vr_host_views *views) { (void)views; return 1; }
+static void test_head_apply(void) {}
+static void jitter_apply(void) {}
+static void test_hands_apply(void) {}
+static void weapon_pointing(float forward[3],float up[3]);
+@@PLATFORM@@
+static void weapon_pointing(float forward[3],float up[3]) {
+ static const float f[3]={0,0,-1}, u[3]={0,1,0};
+ quaternion_rotate(vr.views.aim[1].orientation,f,forward);
+ quaternion_rotate(vr.views.aim[1].orientation,u,up);
+}
+static void pose(float yaw,float x,float y,float z) {
+ int eye;
+ vr.views.head.valid=1;
+ vr.views.head.orientation[0]=vr.views.head.orientation[2]=0;
+ vr.views.head.orientation[1]=sinf(yaw/2); vr.views.head.orientation[3]=cosf(yaw/2);
+ vr.views.head.position[0]=x; vr.views.head.position[1]=y; vr.views.head.position[2]=z;
+ for(eye=0;eye<2;eye++) {
+ vr.views.eye[eye]=vr.views.head;
+ vr.views.eye[eye].position[0]+=eye ? 0.032f : -0.032f;
+ }
+ vr.views.aim[1]=vr.views.head;
+ vr.views.grip[1]=vr.views.head;
+}
+static void reset(void) {
+ memset(&vr,0,sizeof(vr)); memset(&settings,0,sizeof(settings));
+ vr.initialized=vr.frame=vr.origin_valid=vr.aim_synced=1;
+ vr.views.should_render=vr.views.focused=1;
+ settings.vehicle_first_person=settings.vehicle_seat_calibration=settings.vehicle_clear_windshield=1;
+ settings.controller_aim=settings.standing=1;
+ settings.player_height=1.7f; settings.world_scale=1; settings.hud_size=70;
+ vr.origin_position[0]=7; vr.origin_position[1]=1.7f; vr.origin_position[2]=-9;
+ vr.origin_yaw=-0.4f; vr.body_yaw=0.8f;
+ pose(1.2f,3,1,-4);
+}
+static void check_seat_view_and_aim(void) {
+ struct halo_vr_view view;
+ float yaw,pitch,saved_origin[3], saved_body;
+ int axis,step;
+ reset(); memcpy(saved_origin,vr.origin_position,sizeof(saved_origin)); saved_body=vr.body_yaw;
+ halo_vr_vehicle_reference(7,42,0,2.1f);
+ CHECK(vr.vehicle.active && vr.vehicle.valid && !vr.aim_synced);
+ CHECK(halo_vr_view(&view)); CHECK(near(view.body_yaw,2.1f));
+ CHECK(near(view.head_forward[0],1) && near(view.head_forward[1],0));
+ for(axis=0;axis<3;axis++) CHECK(near(view.head_position[axis],0));
+ CHECK(halo_vr_aim(-2,0,&yaw,&pitch)==1); CHECK(near(yaw,2.1f));
+ CHECK(near(vr.body_yaw,saved_body)); CHECK(!memcmp(saved_origin,vr.origin_position,sizeof(saved_origin)));
+ /* Lean and look remain relative to the entry pose while the vehicle turns.
+ The controller and HMD share a basis, but aiming can differ from gaze. */
+ for(step=-15;step<=15;step++) {
+ float vehicle_yaw=step*0.19f, head_yaw=1.2f+step*0.031f;
+ float controller_yaw=head_yaw+0.23f;
+ pose(head_yaw,3.15f,1.05f,-4.1f);
+ vr.views.aim[1].orientation[1]=sinf(controller_yaw/2);
+ vr.views.aim[1].orientation[3]=cosf(controller_yaw/2);
+ halo_vr_vehicle_reference(7,42,0,vehicle_yaw);
+ CHECK(halo_vr_view(&view)); CHECK(near(view.body_yaw,vehicle_yaw));
+ CHECK(near(view.head_position[2],0.05f));
+ CHECK(near(atan2f(view.head_forward[1],view.head_forward[0]),head_yaw-1.2f));
+ CHECK(halo_vr_aim(0,0,&yaw,&pitch)==1);
+ CHECK(near(yaw,view.body_yaw+atan2f(view.hand_forward[1],view.hand_forward[0])));
+ CHECK(near(yaw,vehicle_yaw+controller_yaw-1.2f));
+ CHECK(!memcmp(saved_origin,vr.origin_position,sizeof(saved_origin)));
+ }
+ vr.vehicle.turn=0.7f; CHECK(halo_vr_view(&view));
+ CHECK(near(view.body_yaw,vr.vehicle.forward_yaw+0.7f));
+ recentre(); CHECK(near(vr.vehicle.turn,0)); CHECK(halo_vr_view(&view));
+ for(axis=0;axis<3;axis++) CHECK(near(view.head_position[axis],0));
+ CHECK(near(view.head_forward[0],1));
+ CHECK(!memcmp(saved_origin,vr.origin_position,sizeof(saved_origin)));
+ halo_vr_vehicle_reference(-1,-1,-1,0);
+ CHECK(!vr.vehicle.active && !vr.aim_synced);
+ CHECK(halo_vr_aim(0.4f,0,&yaw,&pitch)==1);
+ CHECK(halo_vr_view(&view)); CHECK(!memcmp(saved_origin,vr.origin_position,sizeof(saved_origin)));
+ CHECK(!near(view.head_position[2],0)); /* walking standing-height basis restored */
+}
+static void check_seat_transitions_and_tracking(void) {
+ struct halo_vr_view view;
+ float saved[3],yaw,pitch;
+ reset(); memcpy(saved,vr.origin_position,sizeof(saved));
+ vr.views.focused=0; halo_vr_vehicle_reference(7,42,0,0.2f);
+ CHECK(vr.vehicle.active && !vr.vehicle.valid);
+ CHECK(!halo_vr_view(&view)); CHECK(!halo_vr_aim(2,0,&yaw,&pitch));
+ vr.views.focused=1; pose(-0.6f,2,0.85f,1); halo_vr_vehicle_reference(7,42,0,0.3f);
+ CHECK(vr.vehicle.valid && near(vr.vehicle.origin_yaw,-0.6f));
+ pose(0.7f,4,1.3f,6); halo_vr_vehicle_reference(7,42,1,-0.4f);
+ CHECK(vr.vehicle.seat==1 && near(vr.vehicle.origin_position[1],1.3f));
+ CHECK(halo_vr_view(&view)); CHECK(near(view.head_position[2],0));
+ halo_vr_vehicle_reference(7,43,1,0.8f); CHECK(vr.vehicle.vehicle==43);
+ vr.views.head.orientation[3]=NAN; halo_vr_vehicle_reference(7,43,1,0.8f); CHECK(!vr.vehicle.valid);
+ pose(0.2f,9,0.7f,-3); halo_vr_vehicle_reference(7,43,1,0.8f); CHECK(vr.vehicle.valid);
+ CHECK(near(vr.vehicle.origin_position[1],0.7f)); CHECK(!memcmp(saved,vr.origin_position,sizeof(saved)));
+ vr.views.eye[0].position[0]=NAN; halo_vr_vehicle_reference(7,43,1,0.8f);
+ CHECK(!vr.vehicle.valid && !halo_vr_aim(2,0,&yaw,&pitch)); CHECK(!halo_vr_view(&view));
+ pose(0.2f,9,0.7f,-3); halo_vr_vehicle_reference(7,43,1,0.8f); CHECK(vr.vehicle.valid);
+ vr.views.eye[1].position[1]=INFINITY; CHECK(!halo_vr_view(&view)); CHECK(!vr.vehicle.valid);
+ pose(0.2f,9,0.7f,-3); halo_vr_vehicle_reference(7,43,1,0.8f);
+ halo_vr_vehicle_reference(7,43,1,INFINITY); CHECK(!vr.vehicle.active);
+ settings.vehicle_seat_calibration=0; halo_vr_vehicle_reference(7,42,0,0); CHECK(!vr.vehicle.active);
+ settings.vehicle_seat_calibration=1; settings.vehicle_first_person=0;
+ halo_vr_vehicle_reference(7,42,0,0); CHECK(!vr.vehicle.active);
+ settings.vehicle_first_person=1; halo_vr_vehicle_reference(7,42,0,0); CHECK(vr.vehicle.valid);
+ settings.gamepad_aim=settings.gamepad_pitch=1; CHECK(halo_vr_view(&view)); CHECK(!view.gamepad_pitch);
+ vr.aim_synced=0; CHECK(halo_vr_aim(2.5f,0,&yaw,&pitch)==1); CHECK(near(yaw,0));
+ CHECK(halo_vr_view(&view) && near(view.body_yaw,0));
+ CHECK(halo_vr_aim(yaw,0,&yaw,&pitch)==2); CHECK(near(yaw,0));
+ vr.vehicle.turn=0.8f; vr.aim_synced=1; recentre(); CHECK(!vr.aim_synced);
+ CHECK(halo_vr_aim(0.8f,0,&yaw,&pitch)==1); CHECK(near(yaw,0));
+ settings.vehicle_clear_windshield=0; CHECK(!halo_vr_vehicle_clear_windshield());
+}
+
+/* Actual local-seat selection and glass scope, with game services stubbed. */
+enum { _object_type_biped, _object_type_vehicle, _object_dead_bit=2 };
+enum { _unit_seat_driver_bit=2 };
+enum { _unit_state_idle, _unit_state_entering_seat, _unit_state_exiting_seat };
+enum { _director_perspective_first_person, _director_perspective_scripted };
+struct unit_datum { int definition_index; struct { int type,damage_flags,parent_object_index; } object;
+ struct { int player_index; short parent_seat_index; struct { int state; } animation; } unit; } occupant,fixture_parent_datum;
+struct player_datum { long unit_index; } player;
+struct unit_seat { unsigned flags; char marker_name[32]; struct { char marker_name[32]; } camera; } seats[2];
+struct unit_definition { struct { struct { int count; } seats; } unit; } fixture_definition;
+struct object_marker { struct { real_vector3d forward,up; real_point3d position; } matrix; } fixture_marker,camera_marker;
+static struct { int active; long seated_unit_index; } vr_render;
+static int cinematic, scripted, perspective, marker_found, reference_calls;
+static int camera_found,head_reads;
+static int *director_camera_scripted=&scripted;
+static long local_player=2, fixture_seated=7;
+static long fixture_unit=7, fixture_parent=42;
+static real_vector3d fixture_forward={1,0,0},fixture_up={0,0,1};
+static int separate_root,missing_root;
+static real_matrix4x3 parent_matrix;
+static real_point3d live_head;
+static const char *fixture_vehicle_name="vehicles\\warthog\\warthog";
+static long local_player_get_player_index(int index) { CHECK(index==0); return local_player; }
+static struct player_datum *player_get(long index) { CHECK(index==local_player); return &player; }
+static struct unit_datum *unit_try_and_get(long index) { return index==fixture_unit ? &occupant : index==fixture_parent ? &fixture_parent_datum : NULL; }
+static int cinematic_in_progress(void) { return cinematic; }
+static int director_get_perspective(int index) { CHECK(index==0); return perspective; }
+static struct unit_definition *unit_definition_get(int index) { CHECK(index==9); return &fixture_definition; }
+#define TAG_BLOCK_GET_ELEMENT(block,index,type) ((void)(block), &seats[(index)])
+static void object_get_orientation(long index,real_vector3d *f,real_vector3d *u) {
+ CHECK(index==fixture_parent); *f=fixture_forward; *u=fixture_up;
+}
+/* Real object node getter supplies the current interpolated fixture_parent_datum basis. */
+#define object_get_node_matrix(index,node) (CHECK_MATRIX(index,node), missing_root ? NULL : &parent_matrix)
+static void CHECK_MATRIX(long index,int node) {
+ CHECK(index==fixture_parent && node==0); if(!separate_root) parent_matrix.forward=fixture_forward;
+}
+static int object_get_marker_by_name(long index,const char *name,struct object_marker *out,int count) {
+ CHECK(index==fixture_parent && name && count==1);
+ if (!strcmp(name,"camera driver") || !strcmp(name,"camera rider")) {
+ *out=camera_marker; return camera_found;
+ }
+ *out=fixture_marker; return marker_found;
+}
+static long seated_unit(void) { return fixture_seated; }
+static void unit_get_head_position(long index,real_point3d *position) {
+ CHECK(index==fixture_unit); head_reads++; *position=live_head;
+}
+static const char *tag_get_name(int index) { CHECK(index==9); return fixture_vehicle_name; }
+static void record_reference(int unit,int vehicle,int seat,float yaw) {
+ reference_calls++; halo_vr_vehicle_reference(unit,vehicle,seat,yaw);
+}
+#define halo_vr_vehicle_reference record_reference
+@@MATH@@
+static void cross_product3d(const real_vector3d *a,const real_vector3d *b,real_vector3d *c) {
+ c->i=a->j*b->k-a->k*b->j;c->j=a->k*b->i-a->i*b->k;c->k=a->i*b->j-a->j*b->i;
+}
+@@GAME@@
+#undef halo_vr_vehicle_reference
+static void reset_game(void) {
+ reset(); memset(&occupant,0,sizeof(occupant)); memset(&fixture_parent_datum,0,sizeof(fixture_parent_datum));
+ memset(seats,0,sizeof(seats));
+ vr_render_vehicle_reference(NONE);
+ occupant.object.type=_object_type_biped; occupant.object.parent_object_index=42;
+ occupant.unit.player_index=2; fixture_parent_datum.object.type=_object_type_vehicle; fixture_parent_datum.definition_index=9;
+ player.unit_index=fixture_unit=7; fixture_parent=42; local_player=2; fixture_definition.unit.seats.count=2;
+ memcpy(seats[0].marker_name,"driver",7); memcpy(seats[1].marker_name,"passenger",10);
+ seats[0].flags=1u<<_unit_seat_driver_bit;
+ cinematic=scripted=perspective=marker_found=reference_calls=0;
+ vr_render.active=1; vr_render.seated_unit_index=fixture_seated=7;
+ fixture_forward.i=1; fixture_forward.j=fixture_forward.k=0;
+ fixture_up.i=fixture_up.j=0; fixture_up.k=1; separate_root=missing_root=0;
+ fixture_marker.matrix.up=fixture_up;
+ memset(&parent_matrix,0,sizeof(parent_matrix)); parent_matrix.scale=1;
+ parent_matrix.forward=fixture_forward; parent_matrix.left.j=parent_matrix.up.k=1;
+ live_head.x=0.3f; live_head.y=0.2f; live_head.z=0.7f; head_reads=0;
+ fixture_vehicle_name="vehicles\\warthog\\warthog";
+ camera_found=1; camera_marker.matrix.position.x=0.3f;
+ camera_marker.matrix.position.y=0.2f; camera_marker.matrix.position.z=0.7f;
+}
+static void check_authored_seat_camera(void) {
+ real_point3d position={-9,-8,-7};
+ int step;
+ reset_game(); CHECK(vr_render_vehicle_camera_position(7,&position));
+ CHECK(near(position.x,0.3f) && near(position.y,0.2f) && near(position.z,0.7f));
+ /* The lookup follows the vehicle's interpolated camera fixture_marker; it never
+ reads the biped skeleton or accelerations. Exercise translated/rotated
+ world fixture_marker values and verify its exact position survives unmodified. */
+ for(step=-20;step<=20;step++) {
+ float yaw=step*0.14f;
+ camera_marker.matrix.position.x=10+0.3f*cosf(yaw)-0.2f*sinf(yaw);
+ camera_marker.matrix.position.y=-5+0.3f*sinf(yaw)+0.2f*cosf(yaw);
+ camera_marker.matrix.position.z=0.7f+step*0.01f;
+ CHECK(vr_render_vehicle_camera_position(7,&position));
+ CHECK(!memcmp(&position,&camera_marker.matrix.position,sizeof(position)));
+ }
+ occupant.unit.parent_seat_index=1;
+ memcpy(seats[1].camera.marker_name,"camera rider",13);
+ CHECK(vr_render_vehicle_camera_position(7,&position));
+ camera_found=0; position.x=55; CHECK(vr_render_vehicle_camera_position(7,&position)); CHECK(near(position.x,live_head.x));
+ camera_found=1; camera_marker.matrix.position.x=NAN;
+ position.x=55; CHECK(!vr_render_vehicle_camera_position(7,&position)); CHECK(position.x==55);
+ reset_game(); fixture_vehicle_name="vehicles\\ghost\\ghost";
+ CHECK(vr_render_vehicle_camera_position(7,&position)); CHECK(head_reads==1);
+ reset_game(); seats[0].flags=0; CHECK(vr_render_vehicle_camera_position(7,&position)); CHECK(head_reads==1);
+ reset_game(); memset(seats[0].camera.marker_name,'x',sizeof(seats[0].camera.marker_name));
+ CHECK(!vr_render_vehicle_camera_position(7,&position));
+ reset_game(); cinematic=1; CHECK(!vr_render_vehicle_camera_position(7,&position));
+ reset_game(); scripted=1; CHECK(!vr_render_vehicle_camera_position(7,&position));
+ reset_game(); occupant.object.damage_flags=1u<<_object_dead_bit;
+ CHECK(!vr_render_vehicle_camera_position(7,&position));
+ reset_game(); occupant.object.parent_object_index=NONE; CHECK(!vr_render_vehicle_camera_position(7,&position));
+ reset_game(); occupant.unit.player_index=8; CHECK(!vr_render_vehicle_camera_position(7,&position));
+ reset_game(); CHECK(!vr_render_vehicle_camera_position(8,&position));
+ CHECK(!vr_render_vehicle_camera_position(7,NULL));
+}
+static void fallback_seat(void) {
+ reset_game(); fixture_vehicle_name="vehicles\\ghost\\ghost";
+ memcpy(seats[0].camera.marker_name,"driver camera",14); camera_found=marker_found=0;
+}
+static void check_stable_seat_fallback(void) {
+ real_point3d position={55,56,57},offset,expected;
+ int step,row,column;
+ fallback_seat(); occupant.unit.animation.state=_unit_state_entering_seat;
+ live_head.x=-20; CHECK(!vr_render_vehicle_camera_position(7,&position)); CHECK(position.x==55 && head_reads==0);
+ live_head.x=-10; CHECK(!vr_render_vehicle_camera_position(7,&position)); CHECK(position.x==55 && head_reads==0);
+ occupant.unit.animation.state=_unit_state_idle; live_head.x=0.3f;
+ CHECK(vr_render_vehicle_camera_position(7,&position)); CHECK(head_reads==1 && near(position.x,0.3f));
+ offset=live_head;
+ /* Leaned head values and sharp vehicle turns cannot change the captured
+ local seat point. Actual production matrix math carries it with the root. */
+ for(step=-30;step<=30;step++) {
+ float yaw=step*0.19f;
+ fixture_forward.i=cosf(yaw); fixture_forward.j=sinf(yaw); fixture_forward.k=0;
+ parent_matrix.forward=fixture_forward;
+ parent_matrix.left.i=-sinf(yaw); parent_matrix.left.j=cosf(yaw); parent_matrix.left.k=0;
+ parent_matrix.position.x=step*1.5f; parent_matrix.position.y=step*-0.8f; parent_matrix.position.z=step*0.25f;
+ parent_matrix.scale=step&1 ? 2.0f : 0.5f;
+ expected.x=parent_matrix.position.x+parent_matrix.scale*(offset.x*cosf(yaw)-offset.y*sinf(yaw));
+ expected.y=parent_matrix.position.y+parent_matrix.scale*(offset.x*sinf(yaw)+offset.y*cosf(yaw));
+ expected.z=parent_matrix.position.z+parent_matrix.scale*offset.z;
+ live_head.x=100.0f+(float)step; live_head.y=-100.0f-(float)step; live_head.z=50;
+ CHECK(vr_render_vehicle_camera_position(7,&position)); CHECK(head_reads==1);
+ CHECK(near(position.x,expected.x) && near(position.y,expected.y) && near(position.z,expected.z));
+ }
+ /* Full pitch/roll, mirrored roots, and scale are transformed once. */
+ parent_matrix.scale=1; parent_matrix.position.x=3; parent_matrix.position.y=4; parent_matrix.position.z=5;
+ fixture_forward.i=0; fixture_forward.j=0; fixture_forward.k=1;
+ parent_matrix.left.i=1; parent_matrix.left.j=parent_matrix.left.k=0;
+ parent_matrix.up.i=parent_matrix.up.k=0; parent_matrix.up.j=1;
+ CHECK(vr_render_vehicle_camera_position(7,&position));
+ CHECK(near(position.x,3+offset.y) && near(position.y,4+offset.z) && near(position.z,5+offset.x));
+ parent_matrix.left.i=-1; CHECK(vr_render_vehicle_camera_position(7,&position));
+ CHECK(near(position.x,3-offset.y) && head_reads==1);
+ /* Exit must reset even when only the reference helper runs on foot. */
+ occupant.object.parent_object_index=NONE; vr_render_vehicle_reference(7);
+ occupant.object.parent_object_index=42; live_head.x=4; live_head.y=5; live_head.z=6;
+ CHECK(vr_render_vehicle_camera_position(7,&position)); CHECK(head_reads==2);
+ CHECK(near(position.x,4) && near(position.y,5) && near(position.z,6));
+ /* Enter/exit animations invalidate an established cache. */
+ occupant.unit.animation.state=_unit_state_exiting_seat;
+ position.x=55; CHECK(!vr_render_vehicle_camera_position(7,&position)); CHECK(position.x==55);
+ occupant.unit.animation.state=_unit_state_entering_seat;
+ CHECK(!vr_render_vehicle_camera_position(7,&position)); CHECK(head_reads==2);
+ occupant.unit.animation.state=_unit_state_idle; live_head.x=7;
+ CHECK(vr_render_vehicle_camera_position(7,&position)); CHECK(head_reads==3 && near(position.x,7));
+ /* Full salted identities, not absolute datum indices, key the cache. */
+ fixture_unit=player.unit_index=7+0x10000; live_head.x=8;
+ CHECK(vr_render_vehicle_camera_position(fixture_unit,&position)); CHECK(head_reads==4 && near(position.x,8));
+ fixture_parent=occupant.object.parent_object_index=42+0x10000; live_head.x=9;
+ CHECK(vr_render_vehicle_camera_position(fixture_unit,&position)); CHECK(head_reads==5 && near(position.x,9));
+ occupant.unit.parent_seat_index=1; live_head.x=10;
+ CHECK(vr_render_vehicle_camera_position(fixture_unit,&position)); CHECK(head_reads==6 && near(position.x,10));
+ /* Authored cameras take over, and their later disappearance recaptures. */
+ memcpy(seats[1].camera.marker_name,"camera rider",13); camera_found=1;
+ CHECK(vr_render_vehicle_camera_position(fixture_unit,&position)); CHECK(head_reads==6 && near(position.x,0.3f));
+ camera_found=0; live_head.x=11;
+ CHECK(vr_render_vehicle_camera_position(fixture_unit,&position)); CHECK(head_reads==7 && near(position.x,11));
+ /* Invalid roots return without touching the caller and invalidate caches.
+ Recovery captures anew, never retaining a NaN or an inverse of zero. */
+ fallback_seat(); CHECK(vr_render_vehicle_camera_position(7,&position)); CHECK(head_reads==1);
+ parent_matrix.scale=0; position.x=55;
+ CHECK(!vr_render_vehicle_camera_position(7,&position)); CHECK(position.x==55);
+ parent_matrix.scale=1; live_head.x=2;
+ CHECK(vr_render_vehicle_camera_position(7,&position)); CHECK(head_reads==2 && near(position.x,2));
+ parent_matrix.scale=0.00000001f; CHECK(!vr_render_vehicle_camera_position(7,&position));
+ parent_matrix.scale=NAN; CHECK(!vr_render_vehicle_camera_position(7,&position));
+ parent_matrix.scale=1; parent_matrix.left=fixture_forward;
+ CHECK(!vr_render_vehicle_camera_position(7,&position));
+ parent_matrix.left.i=0; parent_matrix.left.j=1; parent_matrix.left.k=0;
+ for(row=0;row<4;row++) for(column=0;column<3;column++) {
+ float saved=parent_matrix.n[row][column];
+ parent_matrix.n[row][column]=INFINITY;
+ if(row==0) fixture_forward=parent_matrix.forward;
+ CHECK(!vr_render_vehicle_camera_position(7,&position));
+ parent_matrix.n[row][column]=saved;
+ if(row==0) fixture_forward=parent_matrix.forward;
+ }
+ live_head.x=NAN; CHECK(!vr_render_vehicle_camera_position(7,&position));
+ live_head.x=4; CHECK(vr_render_vehicle_camera_position(7,&position)); CHECK(near(position.x,4));
+ /* Death and scripting invalidate even between render queries. */
+ scripted=1; vr_render_vehicle_reference(7); scripted=0; live_head.x=5;
+ CHECK(vr_render_vehicle_camera_position(7,&position)); CHECK(near(position.x,5));
+ occupant.object.damage_flags=1u<<_object_dead_bit; vr_render_vehicle_reference(7);
+ occupant.object.damage_flags=0; live_head.x=6;
+ CHECK(vr_render_vehicle_camera_position(7,&position)); CHECK(near(position.x,6));
+ cinematic=1; vr_render_vehicle_reference(7); cinematic=0; live_head.x=7;
+ CHECK(vr_render_vehicle_camera_position(7,&position)); CHECK(near(position.x,7));
+}
+static void check_local_game_guards(void) {
+ reset_game(); vr_render_vehicle_reference(7); CHECK(reference_calls==1 && vr.vehicle.valid);
+ reset_game(); marker_found=1; fixture_marker.matrix.forward.i=0; fixture_marker.matrix.forward.j=1; fixture_marker.matrix.forward.k=0;
+ vr_render_vehicle_reference(7); CHECK(near(vr.vehicle.forward_yaw,(float)M_PI/2));
+ fixture_marker.matrix.forward.i=1; fixture_marker.matrix.forward.j=0;
+ vr_render_vehicle_reference(7); CHECK(near(vr.vehicle.forward_yaw,(float)M_PI/2));
+ fixture_forward.i=0; fixture_forward.j=1;
+ vr_render_vehicle_reference(7); CHECK(near(vr.vehicle.forward_yaw,(float)M_PI));
+ occupant.unit.parent_seat_index=1; vr_render_vehicle_reference(7); CHECK(vr.vehicle.seat==1);
+ occupant.unit.parent_seat_index=2; vr_render_vehicle_reference(7); CHECK(!vr.vehicle.active);
+ reset_game(); cinematic=1; vr_render_vehicle_reference(7); CHECK(!vr.vehicle.active);
+ reset_game(); scripted=1; vr_render_vehicle_reference(7); CHECK(!vr.vehicle.active);
+ reset_game(); occupant.object.damage_flags=1u<<_object_dead_bit; vr_render_vehicle_reference(7); CHECK(!vr.vehicle.active);
+ reset_game(); occupant.unit.player_index=8; vr_render_vehicle_reference(7); CHECK(!vr.vehicle.active);
+ reset_game(); fixture_parent_datum.object.type=_object_type_biped; vr_render_vehicle_reference(7); CHECK(!vr.vehicle.active);
+ reset_game(); fixture_forward.i=NAN; vr_render_vehicle_reference(7);
+ CHECK(vr.vehicle.active && vr.vehicle.orientation_valid < 0);
+ { struct halo_vr_view invalid_view; float yaw=9,pitch=8; CHECK(!halo_vr_view(&invalid_view)); CHECK(!halo_vr_aim(0,0,&yaw,&pitch) && yaw==9 && pitch==8); }
+ reset_game(); CHECK(vr_render_hides_vehicle_glass(42)); CHECK(!vr_render_hides_vehicle_glass(43));
+ vr_render.active=0; CHECK(!vr_render_hides_vehicle_glass(42)); vr_render.active=1;
+ fixture_seated=8; CHECK(!vr_render_hides_vehicle_glass(42)); fixture_seated=7;
+ fixture_vehicle_name="vehicles\\ghost\\ghost"; CHECK(!vr_render_hides_vehicle_glass(42));
+ fixture_vehicle_name="vehicles\\warthog\\warthog"; settings.vehicle_first_person=0;
+ CHECK(!vr_render_hides_vehicle_glass(42));
+}
+
+static void flight_heading(float heading,int sign,float horizontal) {
+ float vertical=sqrtf(1-horizontal*horizontal);
+ fixture_forward.i=horizontal*cosf(heading); fixture_forward.j=horizontal*sinf(heading);
+ fixture_forward.k=sign*vertical;
+ fixture_up.i=-sign*vertical*cosf(heading); fixture_up.j=-sign*vertical*sinf(heading);
+ fixture_up.k=horizontal;
+}
+static void check_vertical_flight_reference(void) {
+ int sign,step,near_pole;struct halo_vr_view view;float anchor[3],walking[3];
+ for(sign=-1;sign<=1;sign+=2) {
+ reset_game(); fixture_vehicle_name="vehicles\\banshee\\banshee";
+ vr_render_vehicle_reference(7); CHECK(vr.vehicle.active&&vr.vehicle.valid);
+ memcpy(anchor,vr.vehicle.origin_position,sizeof(anchor));
+ memcpy(walking,vr.origin_position,sizeof(walking)); vr.aim_synced=1;
+ for(near_pole=0;near_pole<2;near_pole++) for(step=-27;step<=27;step++) {
+ float heading=step*.1f;
+ flight_heading(heading,sign,near_pole ? .005f : 0);
+ /* Head movement and pole crossing must not recapture the seat. */
+ pose(-heading,step*.03f,1+step*.01f,-step*.04f);
+ vr_render_vehicle_reference(7);
+ CHECK(vr.vehicle.active&&vr.vehicle.valid&&near(vr.vehicle.forward_yaw,heading));
+ CHECK(!memcmp(anchor,vr.vehicle.origin_position,sizeof(anchor))&&vr.aim_synced);
+ CHECK(!memcmp(walking,vr.origin_position,sizeof(walking)));
+ CHECK(halo_vr_view(&view)&&near(view.body_yaw,heading));
+ }
+ }
+ /* Exact upward/downward flight retains yaw zero using the left axis. */
+ for(sign=-1;sign<=1;sign+=2) {
+ reset_game();flight_heading(0,sign,0);vr_render_vehicle_reference(7);
+ CHECK(vr.vehicle.valid&&near(vr.vehicle.forward_yaw,0));
+ }
+ /* Invalid interpolated roots and unavailable nodes use a finite simulation
+ basis; an ordinary finite interpolated yaw still remains authoritative. */
+ reset_game();separate_root=1;fixture_forward.i=cosf(.8f);fixture_forward.j=sinf(.8f);
+ parent_matrix.forward.i=NAN;vr_render_vehicle_reference(7);
+ CHECK(vr.vehicle.valid&&near(vr.vehicle.forward_yaw,.8f));
+ missing_root=1;vr_render_vehicle_reference(7);CHECK(vr.vehicle.valid&&near(vr.vehicle.forward_yaw,.8f));
+ missing_root=0;parent_matrix.forward.i=cosf(-.4f);parent_matrix.forward.j=sinf(-.4f);
+ parent_matrix.forward.k=0;vr_render_vehicle_reference(7);
+ CHECK(vr.vehicle.valid&&near(vr.vehicle.forward_yaw,-.4f));
+ /* A captured seat offset survives vertical flight and a moving fixture_marker. */
+ reset_game();marker_found=1;fixture_marker.matrix.forward.i=0;fixture_marker.matrix.forward.j=1;fixture_marker.matrix.forward.k=0;
+ vr_render_vehicle_reference(7);CHECK(near(vr.vehicle.forward_yaw,(float)M_PI/2));
+ fixture_marker.matrix.forward.i=1;fixture_marker.matrix.forward.j=0;
+ for(sign=-1;sign<=1;sign+=2) for(step=-20;step<=20;step++) {
+ float heading=step*.1f;flight_heading(heading,sign,0);vr_render_vehicle_reference(7);
+ CHECK(vr.vehicle.valid&&near(vr.vehicle.forward_yaw,heading+(float)M_PI/2));
+ }
+ /* The same finite pole check also defines a vertical entry fixture_marker. */
+ reset_game();fixture_forward.i=cosf(.6f);fixture_forward.j=sinf(.6f);
+ marker_found=1;fixture_marker.matrix.forward.i=fixture_marker.matrix.forward.j=0;fixture_marker.matrix.forward.k=1;
+ fixture_marker.matrix.up.i=-cosf(1.2f);fixture_marker.matrix.up.j=-sinf(1.2f);fixture_marker.matrix.up.k=0;
+ vr_render_vehicle_reference(7);CHECK(vr.vehicle.valid&&near(vr.vehicle.forward_yaw,1.2f));
+ fixture_marker.matrix.up.i=0;fixture_marker.matrix.up.j=1;
+ flight_heading(1.3f,1,0);vr_render_vehicle_reference(7);
+ CHECK(vr.vehicle.valid&&near(vr.vehicle.forward_yaw,1.9f));
+ /* Corrupt hull data retains seat identity but rejects view and tracked aim. */
+ for(step=0;step<5;step++) {
+ reset_game();flight_heading(.8f,1,0);vr_render_vehicle_reference(7);CHECK(vr.vehicle.valid);
+ switch(step) {
+ case 0:fixture_up.i=NAN;break;case 1:fixture_up.k=INFINITY;break;
+ case 2:fixture_up=fixture_forward;break;
+ case 3:fixture_forward.i=NAN;break;
+ case 4:memset(&fixture_forward,0,sizeof(fixture_forward));break;
+ }
+ vr_render_vehicle_reference(7);CHECK(vr.vehicle.active&&vr.vehicle.orientation_valid<0);
+ { struct halo_vr_view invalid_view; float yaw=9,pitch=8; CHECK(!halo_vr_view(&invalid_view)); CHECK(!halo_vr_aim(0,0,&yaw,&pitch)&&yaw==9&&pitch==8); }
+ }
+}
+
+enum { _rasterizer_profile_water, _rasterizer_profile_queued_transparents,
+ _rasterizer_geometry_first_person_bit, _rasterizer_target_render_primary };
+enum { _shader_type_transparent_water=7, _shader_type_transparent_glass=8 };
+struct shader { struct { int type; } base; } glass={{8}},model={{4}},water_shader={{7}};
+struct transparent_geometry_group { unsigned geometry_flags; long object_index,source_object_index; struct shader *shader; } groups[3];
+static struct transparent_geometry_group *transparent_geometry_groups=groups;
+static int sorted[3]={0,1,2}, *transparent_geometry_group_sorted_indices=sorted;
+static int transparent_geometry_group_index,transparent_geometry_group_count;
+static struct { int window_index,rasterizer_target; } global_window_parameters;
+static struct { int transparent_pixel_counter_active; } rasterizer_debug_options;
+static struct { float first_person_weapon_near_clip_distance,first_person_weapon_far_clip_distance; } rasterizer_globals;
+static int draw_count,stencil_count,frustum_count;
+static void rasterizer_profile_begin(short profile) { (void)profile; }
+static void rasterizer_profile_end(short profile) { (void)profile; }
+static void rasterizer_sort_external(void) {}
+static void rasterizer_transparent_geometry_groups_begin(void) {}
+static void rasterizer_transparent_geometry_groups_end(void) {}
+static int shader_is_water_decal(struct shader *shader) { (void)shader; return 0; }
+static void match_assert(const char *file,int line,int condition) { (void)file; (void)line; CHECK(condition); }
+static void rasterizer_set_stencil_mode(int mode) { CHECK(mode==0); stencil_count++; }
+static void rasterizer_set_frustum_z(float near_clip,float far_clip) { (void)near_clip; (void)far_clip; frustum_count++; }
+static void rasterizer_transparent_geometry_group_draw(struct transparent_geometry_group *group,int unused) {
+ CHECK(group && !unused); draw_count++;
+}
+@@DRAW@@
+static void check_draw_queue(void) {
+ reset_game(); memset(groups,0,sizeof(groups)); draw_count=stencil_count=frustum_count=0;
+ global_window_parameters.window_index=NONE; transparent_geometry_group_index=0; transparent_geometry_group_count=3;
+ groups[0].shader=&glass; groups[0].object_index=42;
+ groups[1].shader=&model; groups[1].object_index=42;
+ groups[2].shader=&glass; groups[2].object_index=43;
+ rasterizer_transparent_geometry_draw(FALSE);
+ CHECK(draw_count==2 && transparent_geometry_group_index==3 && !stencil_count && !frustum_count);
+ settings.vehicle_clear_windshield=0; draw_count=transparent_geometry_group_index=0;
+ rasterizer_transparent_geometry_draw(FALSE); CHECK(draw_count==3);
+ settings.vehicle_clear_windshield=1; draw_count=transparent_geometry_group_index=0;
+ groups[0].shader=&water_shader; rasterizer_transparent_geometry_draw(TRUE);
+ CHECK(draw_count==1 && transparent_geometry_group_index==1);
+ rasterizer_transparent_geometry_draw(FALSE); CHECK(draw_count==3 && transparent_geometry_group_index==3);
+}
+int main(void) {
+ check_seat_view_and_aim(); check_seat_transitions_and_tracking(); check_local_game_guards(); check_draw_queue();
+ check_authored_seat_camera();
+ check_stable_seat_fallback();
+ check_vertical_flight_reference();
+ printf("PASS: %u production seated vehicle/view/aim/glass assertions\n",checks); return 0;
+}
+'''
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ args = parser.parse_args()
+ vr = (ROOT / 'port/linux/src/vr.c').read_text()
+ render = (ROOT / 'port/linux/game/vr_render.c').read_text()
+ raster = (ROOT / 'source/rasterizer/rasterizer_transparent_geometry.c').read_text()
+ player = (ROOT / 'source/game/player_control.c').read_text()
+ matrix = (ROOT / 'source/math/matrix_math.c').read_text()
+ assert player.index('vr_render_vehicle_reference(player->unit_index)') < player.index('vr_aim = halo_vr_aim(')
+ camera = function(render, 'vr_render_camera')
+ assert camera.index('vr_render_vehicle_reference(') < camera.index('halo_vr_view(')
+ assert camera.index('vr_render_vehicle_camera_position(') < camera.index('vr_render.base_position = camera->position')
+ assert 'vr_render_vehicle_camera_position(' in function(render,'vr_render_player_gaze')
+ harness = HARNESS.replace('@@PLATFORM@@', '\n\n'.join(function(vr, name) for name in (
+ 'quaternion_rotate','quaternion_yaw','game_axes','vehicle_view_active','vehicle_capture',
+ 'halo_vr_vehicle_reference','halo_vr_vehicle_orientation','vehicle_basis','halo_vr_vehicle_clear_windshield','recentre','halo_vr_view','halo_vr_aim')))
+ harness = harness.replace('@@MATH@@', '\n\n'.join(matrix_function(matrix,name) for name in (
+ 'matrix4x3_transform_point','matrix4x3_inverse_transform_point')))
+ harness = harness.replace('@@GAME@@', '\n\n'.join(function(render,name) for name in (
+ 'vr_render_vehicle_camera_position','vr_render_vehicle_yaw','vr_render_vehicle_basis','vr_render_vehicle_reference','vr_render_hides_vehicle_glass')))
+ harness = harness.replace('@@DRAW@@', function(raster,'rasterizer_transparent_geometry_draw'))
+ compiler = args.cc or os.environ.get('CC') or shutil.which('clang') or shutil.which('cc') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('Select a C11 host compiler with --cc.')
+ with tempfile.TemporaryDirectory(prefix='halo-seat-',ignore_cleanup_errors=True) as directory:
+ temp = Path(directory)
+ source = temp / 'test.c'
+ exe = temp / ('test.exe' if os.name == 'nt' else 'test')
+ source.write_text(harness)
+ subprocess.run([compiler,'-std=c11','-Wall','-Wextra','-Werror','-DHALO_VR=1',
+ '-I'+str(ROOT/'port/linux/src'),str(source),
+ *([] if os.name=='nt' else ['-lm']),'-o',str(exe)],cwd=temp,check=True)
+ subprocess.run([str(exe)],cwd=temp,check=True)
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_vehicle_pose.py b/tools/test_vr_vehicle_pose.py
new file mode 100644
index 0000000000000000000000000000000000000000..81c658a0b7a2f20ae0e11bdf3986ab311078955f
--- /dev/null
+++ b/tools/test_vr_vehicle_pose.py
@@ -0,0 +1,275 @@
+#!/usr/bin/env python3
+"""Execute production rigid seat, camera/hand and near stereo culling math.
+
+Only tracking/game services are mocked. The platform aim/view/reference and
+renderer camera/hand/shoulder functions are extracted from the shipped source.
+"""
+import argparse
+from pathlib import Path
+import re
+
+from test_vr_projectile_reticle import function
+from test_vr_vehicle import HARNESS as SEAT_HARNESS, matrix_function
+from test_vr_zoom_aim import compile_and_run
+
+ROOT = Path(__file__).resolve().parents[1]
+
+HULL = r'''
+static void hull(float yaw,float pitch,float roll,float axes[3][3]) {
+ float c=cosf(yaw),s=sinf(yaw),p=cosf(pitch),q=sinf(pitch),r=cosf(roll),t=sinf(roll);
+ axes[0][0]=c*p;axes[0][1]=s*p;axes[0][2]=q;
+ axes[1][0]=-s*r-c*q*t;axes[1][1]=c*r-s*q*t;axes[1][2]=p*t;
+ axes[2][0]=s*t-c*q*r;axes[2][1]=-c*t-s*q*r;axes[2][2]=p*r;
+}
+'''
+
+SEAT_CASES = HULL + r'''
+static void full_rigid_seat(void) {
+ int step,axis;float axes[3][3],world[3],yaw,pitch;struct halo_vr_view view;
+ /* Opening the seat on an already tilted hull must recover LOCAL fixture_marker
+ yaw. The old world-yaw subtraction gives 63.4deg for this 45deg seat. */
+ reset_game();hull(0,1.04719755f,0,axes);separate_root=1;
+ memcpy(&parent_matrix.forward,axes[0],12);memcpy(&parent_matrix.left,axes[1],12);memcpy(&parent_matrix.up,axes[2],12);
+ fixture_forward=parent_matrix.forward;fixture_up=parent_matrix.up;marker_found=1;
+ {float local[3]={.70710678f,.70710678f,0};halo_vr_vehicle_rotate(axes,local,world);memcpy(&fixture_marker.matrix.forward,world,12);fixture_marker.matrix.up=parent_matrix.up;}
+ vr_render_vehicle_reference(7);CHECK(near(vr.vehicle.forward_yaw,.78539816f));
+ CHECK(halo_vr_view(&view)&&view.vehicle_basis_valid);
+ CHECK(near(view.vehicle_basis[0][0],.35355339f)&&near(view.vehicle_basis[0][1],.70710678f)&&near(view.vehicle_basis[0][2],.61237243f));
+ for(step=-30;step<=30;step++) {
+ float heading=step*.075f;hull(heading,step*.027f,step*.039f,axes);
+ memcpy(&parent_matrix.forward,axes[0],12);memcpy(&parent_matrix.left,axes[1],12);memcpy(&parent_matrix.up,axes[2],12);
+ fixture_forward=parent_matrix.forward;fixture_up=parent_matrix.up;
+ fixture_marker.matrix.forward.i=1;fixture_marker.matrix.forward.j=fixture_marker.matrix.forward.k=0; /* animated fixture_marker no longer changes seat */
+ vr_render_vehicle_reference(7);CHECK(near(vr.vehicle.forward_yaw,heading+.78539816f));
+ CHECK(halo_vr_view(&view)&&view.vehicle_basis_valid);
+ CHECK(halo_vr_aim(0,0,&yaw,&pitch)==1);
+ halo_vr_vehicle_rotate(view.vehicle_basis,view.hand_forward,world);
+ CHECK(near(cosf(pitch)*cosf(yaw),world[0])&&near(cosf(pitch)*sinf(yaw),world[1])&&near(sinf(pitch),world[2]));
+ for(axis=0;axis<3;axis++)CHECK(near(view.vehicle_basis[2][axis],axes[2][axis]));
+ }
+ vr.vehicle.turn=.2f;CHECK(halo_vr_view(&view));CHECK(near(view.body_yaw,vr.vehicle.forward_yaw+.2f));
+ recentre();CHECK(halo_vr_view(&view)&&view.vehicle_basis_valid);CHECK(near(vr.vehicle.turn,0));
+ for(axis=0;axis<3;axis++)CHECK(near(view.head_position[axis],0));
+ /* Direct invalid API data cannot contaminate gaze or authoritative aim. */
+ for(step=0;step<5;step++) {
+ hull(.3f,.4f,.5f,axes);switch(step){case 0:axes[0][0]=NAN;break;case 1:axes[2][2]=INFINITY;break;case 2:axes[0][0]*=2;break;case 3:memcpy(axes[2],axes[0],12);break;case 4:memset(axes,0,sizeof(axes));break;}
+ halo_vr_vehicle_orientation(axes,.3f);CHECK(vr.vehicle.orientation_valid<0);CHECK(!halo_vr_view(&view));yaw=9;pitch=8;CHECK(!halo_vr_aim(0,0,&yaw,&pitch)&&yaw==9&&pitch==8);
+ }
+ halo_vr_vehicle_reference(-1,-1,-1,0);CHECK(halo_vr_view(&view)&&!view.vehicle_basis_valid);
+}
+'''
+
+CAMERA = r'''
+#define _USE_MATH_DEFINES
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "vr.h"
+#include "vr_vehicle.h"
+typedef int boolean;typedef float real;
+typedef struct {real i,j,k;} real_vector3d;
+typedef struct {real x,y,z;} real_point3d;
+typedef struct {real scale;real_vector3d forward,left,up;real_point3d position;} real_matrix4x3;
+typedef struct {real x0,x1,y0,y1;} real_rectangle2d;
+typedef struct {short x0,x1,y0,y1;} rectangle2d;
+struct render_camera {real_point3d position;real_vector3d forward,up;real vertical_field_of_view,z_near,z_far;rectangle2d viewport_bounds;boolean mirrored;};
+struct render_frustum {int unused;};
+#include "vr_hands.h"
+#define TRUE 1
+#define FALSE 0
+#define NONE (-1)
+#define MIN(a,b) ((a)<(b)?(a):(b))
+#define MAX(a,b) ((a)>(b)?(a):(b))
+#define UNION_MARGIN 1.03f
+#define _pi 3.14159265358979323846f
+#define _real_epsilon .0001f
+#define BODY_TURN_FREE .70f
+#define BODY_TURN_SECONDS 1.5f
+enum {_left=0,_right,NUMBER_OF_SIDES};
+static unsigned checks;
+#define CHECK(c) do{checks++;if(!(c)){fprintf(stderr,"line %d: %s\n",__LINE__,#c);exit(1);}}while(0)
+static int near(float a,float b){return fabsf(a-b)<.0001f;}
+static struct {boolean body_valid;real body_yaw;} vr_hands;
+@@STATE@@
+static struct halo_vr_view fresh;
+static real_vector3d global_up={0,0,1},*global_up3d=&global_up;
+static real_point3d anchor;
+static int builds;
+static int cinematic_in_progress(void){return 0;}
+int halo_vr_aiming(void){return 1;}
+int halo_vr_view(struct halo_vr_view *v){*v=fresh;return 1;}
+int halo_vr_render_view_commit(int valid){return valid;}
+void halo_vr_eye_size(int *w,int *h){*w=1728;*h=1728;}
+static long local_player_get_player_index(short index){(void)index;return 2;}
+static long seated_unit(void){return 7;}
+void vr_render_vehicle_reference(long index){CHECK(index==7);}
+static boolean vr_render_vehicle_camera_position(long index,real_point3d *p){CHECK(index==7);*p=anchor;return TRUE;}
+static void unit_get_head_position(long index,real_point3d *p){(void)index;*p=anchor;}
+static real view_pitch(void){return 0;}
+static void player_control_get_facing_direction(short index,real_vector3d *v){(void)index;v->i=1;v->j=v->k=0;}
+static real dot_product3d(const real_vector3d *a,const real_vector3d *b){return a->i*b->i+a->j*b->j+a->k*b->k;}
+static void cross_product3d(const real_vector3d *a,const real_vector3d *b,real_vector3d *c){c->i=a->j*b->k-a->k*b->j;c->j=a->k*b->i-a->i*b->k;c->k=a->i*b->j-a->j*b->i;}
+static real normalize3d(real_vector3d *a){real m=sqrtf(dot_product3d(a,a));if(m>0){a->i/=m;a->j/=m;a->k/=m;}return m;}
+static void render_camera_build_frustum(const struct render_camera *c,const real_rectangle2d *b,struct render_frustum *f,int projection){CHECK(c&&b&&f&&projection);builds++;}
+@@PRODUCTION@@
+@@HULL@@
+static void reset(void){int eye;memset(&vr_render,0,sizeof(vr_render));memset(&fresh,0,sizeof(fresh));
+ fresh.world_units_per_metre=1/3.048f;fresh.vehicle_first_person=1;fresh.vehicle_basis_valid=1;fresh.head_forward[0]=fresh.head_up[2]=1;
+ fresh.head_position[0]=.13f;fresh.head_position[1]=-.21f;fresh.head_position[2]=.09f;anchor.x=5;anchor.y=-3;anchor.z=2;
+ for(eye=0;eye<2;eye++){fresh.fov[eye][0]=fresh.fov[eye][3]=-1;fresh.fov[eye][1]=fresh.fov[eye][2]=1;fresh.eye_offset[eye][1]=eye?-.032f:.032f;}
+ builds=0;
+}
+static struct render_camera camera(void){struct render_camera c;memset(&c,0,sizeof(c));c.forward.i=c.up.k=1;c.z_near=.0625f;c.z_far=100;c.viewport_bounds.x1=640;c.viewport_bounds.y1=480;return c;}
+static void rendered_peer_alignment(void){int step,axis;struct render_camera c,r;float axes[3][3],v[3];real_point3d hand;real_vector3d direction;
+ for(step=-40;step<=40;step++){
+ reset();hull(step*.081f,step*.031f,step*.041f,axes);CHECK(halo_vr_vehicle_basis(axes,step*.081f,step*.081f+.3f,fresh.vehicle_basis));
+ c=camera();r=c;anchor.z+=step*.007f;CHECK(vr_render_camera(&c,&r));CHECK(builds==4);
+ halo_vr_vehicle_rotate(fresh.vehicle_basis,fresh.head_forward,v);CHECK(near(c.forward.i,v[0])&&near(c.forward.j,v[1])&&near(c.forward.k,v[2]));
+ halo_vr_vehicle_rotate(fresh.vehicle_basis,fresh.head_up,v);CHECK(near(c.up.i,v[0])&&near(c.up.j,v[1])&&near(c.up.k,v[2]));
+ halo_vr_vehicle_rotate(fresh.vehicle_basis,fresh.head_position,v);CHECK(near(c.position.x,anchor.x+v[0]*fresh.world_units_per_metre)&&near(c.position.y,anchor.y+v[1]*fresh.world_units_per_metre)&&near(c.position.z,anchor.z+v[2]*fresh.world_units_per_metre));
+ CHECK(!memcmp(&c.position,&r.position,sizeof(c.position)));hand_point(fresh.head_position,&hand);CHECK(!memcmp(&hand,&c.position,sizeof(hand)));
+ view_vector(&fresh,9,8,fresh.head_forward,&direction);CHECK(!memcmp(&direction,&c.forward,sizeof(direction)));
+ for(axis=0;axis<2;axis++){CHECK(near(vr_render.eye_cameras[axis].position.x,c.position.x+fresh.vehicle_basis[1][0]*fresh.eye_offset[axis][1]*fresh.world_units_per_metre));CHECK(near(vr_render.eye_cameras[axis].position.y,c.position.y+fresh.vehicle_basis[1][1]*fresh.eye_offset[axis][1]*fresh.world_units_per_metre));CHECK(near(vr_render.eye_cameras[axis].position.z,c.position.z+fresh.vehicle_basis[1][2]*fresh.eye_offset[axis][1]*fresh.world_units_per_metre));}
+ }
+ /* Existing on-foot yaw/pitch transform stays bit-identical. */
+ fresh.vehicle_basis_valid=0;view_vector(&fresh,.7f,-.3f,fresh.head_position,&direction);{real_vector3d old;view_rotate(.7f,-.3f,fresh.head_position,&old);CHECK(!memcmp(&direction,&old,sizeof(old)));}
+ for(step=0;step<11;step++){reset();hull(0,0,0,fresh.vehicle_basis);c=camera();r=c;switch(step){case 0:c.z_near=NAN;break;case 1:c.z_near=0;break;case 2:c.viewport_bounds.x1=0;break;case 3:anchor.x=INFINITY;break;case 4:fresh.fov[1][2]=NAN;break;case 5:fresh.head_forward[0]=0;break;case 6:fresh.head_up[2]=2;break;case 7:memcpy(fresh.head_up,fresh.head_forward,12);break;case 8:fresh.fov[0][2]=fresh.fov[1][2]=1e20f;break;case 9:c.z_far=NAN;break;case 10:c.z_far=c.z_near;break;}CHECK(!vr_render_camera(&c,&r)&&!vr_render.active&&!builds);}
+}
+static void stereo_coverage(void){int step,eye,edge;float bounds[4],fov[2][4]={{-1,1,1,-1},{-1,1,1,-1}},o[2][3]={{0,.032f,0},{0,-.032f,0}},units=1/3.048f;
+ CHECK(halo_vr_stereo_union(fov,o,units,.0625f,1.03f,bounds));CHECK(bounds[0]<-1.168f&&bounds[1]>1.168f);CHECK(near(bounds[2],1.03f)&&near(bounds[3],-1.03f));
+ /* Reproduce old near-cockpit culling: -1.15 is outside old -1.03,
+ but inside the translated left eye at the stock world near plane. */
+ {float cockpit=-1.15f,old_bound=-1.03f;CHECK(cockpit<old_bound);CHECK(cockpit>-1-o[0][1]*units/.0625f);CHECK(cockpit>bounds[0]);}
+ for(step=1;step<=120;step++){float depth=.0625f*(float)step;CHECK(halo_vr_stereo_union(fov,o,units,.0625f,1,bounds));
+ for(eye=0;eye<2;eye++)for(edge=0;edge<4;edge++){float shifted=fov[eye][edge]+(edge<2?-o[eye][1]:o[eye][2])*units/depth;CHECK((edge==0||edge==3)?shifted>=bounds[edge]-.00001f:shifted<=bounds[edge]+.00001f);}}
+ o[0][2]=.009f;o[1][2]=-.009f;CHECK(halo_vr_stereo_union(fov,o,units,.0625f,1,bounds));CHECK(bounds[2]>1&&bounds[3]<-1);
+ for(step=0;step<6;step++){float n=.0625f;switch(step){case 0:n=0;break;case 1:n=-1;break;case 2:n=NAN;break;case 3:n=INFINITY;break;case 4:n=1e-20f;break;case 5:o[1][0]=NAN;break;}CHECK(!halo_vr_stereo_union(fov,o,units,n,1,bounds));}
+}
+static void shoulder_alignment(void){struct vr_hands_frame frame;real_point3d s[2];real_vector3d f,l,u;boolean visible[2]={FALSE,FALSE};float axes[3][3];int step,axis;
+ for(step=-25;step<=25;step++){memset(&frame,0,sizeof(frame));memset(&vr_hands,0,sizeof(vr_hands));hull(step*.08f,step*.03f,step*.04f,axes);memcpy(frame.vehicle_basis,axes,sizeof(axes));frame.vehicle_basis_valid=1;frame.units=1;frame.shoulder[0]=.2f;frame.shoulder[1]=-.25f;frame.shoulder[2]=-.07f;
+ shoulders_place(&frame,visible,s,&f,&l,&u);for(axis=0;axis<3;axis++){CHECK(near(((float*)&f)[axis],axes[0][axis]));CHECK(near(((float*)&l)[axis],axes[1][axis]));CHECK(near(((float*)&u)[axis],axes[2][axis]));CHECK(near(((float*)&s[0])[axis],axes[1][axis]*.2f-axes[2][axis]*.25f-axes[0][axis]*.07f));}
+ }
+ frame.vehicle_basis_valid=0;frame.base_yaw=.7f;memset(&vr_hands,0,sizeof(vr_hands));shoulders_place(&frame,visible,s,&f,&l,&u);CHECK(near(f.i,cosf(.7f))&&near(f.j,sinf(.7f))&&near(f.k,0)&&near(u.k,1));
+}
+int main(void){rendered_peer_alignment();stereo_coverage();shoulder_alignment();printf("PASS: %u production rigid camera/hand/shoulder/stereo assertions\n",checks);return 0;}
+'''
+
+SNAPSHOT = r'''
+#define _USE_MATH_DEFINES
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "vr.h"
+#include "vr_host.h"
+#include "vr_locomotion.h"
+#include "vr_vehicle.h"
+#ifndef M_PI
+#define M_PI 3.14159265358979323846
+#endif
+#define TEST_HANDS_POSES 8
+#define HUD_WIDTH 1280
+#define HUD_HEIGHT 960
+#define METRES_PER_WORLD_UNIT 3.048f
+typedef unsigned long long Uint64;
+@@STATE@@
+static unsigned checks,ends;static int tag,locate_ok=1,wait_ok=1;
+static struct vr_host_layers ended;
+#define CHECK(c) do{checks++;if(!(c)){fprintf(stderr,"line %d: %s\n",__LINE__,#c);exit(1);}}while(0)
+static void settings_read(void){}
+static Uint64 SDL_GetTicksNS(void){return 1;}
+static void timing_update(void){}
+static void jitter_new(void){}
+static void test_head_apply(void){}
+static void jitter_apply(void){}
+static void test_hands_apply(void){}
+static void input_update(void){}
+static void recentre(void){vr.origin_valid=1;}
+void halo_vr_vehicle_reference(int u,int p,int s,float y){(void)u;(void)p;(void)s;(void)y;}
+static void weapon_pointing(float f[3],float u[3]){f[0]=f[1]=0;f[2]=-1;u[0]=u[2]=0;u[1]=1;}
+int host_vr_frame_wait(struct vr_host_views *v){v->should_render=v->focused=1;v->display_elapsed=v->display_period=1/72.0f;return wait_ok;}
+int host_vr_locate(struct vr_host_views *v){int eye;float yaw=(float)tag*.1f;
+ if(!locate_ok)return 0;
+ memset(&v->head,0,sizeof(v->head));v->head.valid=1;v->head.orientation[1]=sinf(yaw/2);v->head.orientation[3]=cosf(yaw/2);v->head.position[0]=(float)tag*.01f;v->head.position[1]=1.7f;
+ for(eye=0;eye<2;eye++){v->eye[eye]=v->head;v->eye[eye].position[0]+=eye?.032f:-.032f;v->fov[eye][0]=v->fov[eye][3]=-1;v->fov[eye][1]=v->fov[eye][2]=1+(float)tag*.01f;}return 1;}
+int host_vr_input(struct vr_host_input *i){memset(i,0,sizeof(*i));return 1;}
+void host_vr_frame_end(const struct vr_host_layers *l){ended=*l;ends++;}
+@@PRODUCTION@@
+static void reset(void){int i;memset(&vr,0,sizeof(vr));memset(&settings,0,sizeof(settings));memset(&ended,0,sizeof(ended));
+ vr.initialized=vr.origin_valid=vr.aim_synced=1;settings.world_scale=1;settings.hud_size=60;settings.hud_distance=1;settings.menu_distance=1.5f;settings.menu_width=1.8f;
+ locate_ok=wait_ok=1;tag=1;ends=0;for(i=0;i<VR_SWAPCHAIN_COUNT;i++)vr.acquired[i]=0;}
+static void exact_draw_sample(void){struct halo_vr_view view;struct vr_host_views drawn;struct vr_host_layers pending;
+ reset();CHECK(frame_begin());CHECK(halo_vr_view(&view));drawn=vr.views;CHECK(halo_vr_render_view_commit(1));
+ tag=7;CHECK(halo_vr_view(&view));CHECK(memcmp(drawn.eye,vr.views.eye,sizeof(drawn.eye))!=0);CHECK(memcmp(drawn.fov,vr.views.fov,sizeof(drawn.fov))!=0);
+ vr.depth_written=1;vr.depth_near=.1f;vr.depth_far=100;halo_vr_present(1,1);
+ CHECK(vr.pending_layers.projection&&vr.pending_layers.depth&&!vr.render_pose_valid&&!vr.frame);
+ CHECK(!memcmp(vr.pending_layers.eye,drawn.eye,sizeof(drawn.eye)));CHECK(!memcmp(vr.pending_layers.fov,drawn.fov,sizeof(drawn.fov)));
+ pending=vr.pending_layers;CHECK(frame_begin());CHECK(ends==1&&!memcmp(&ended,&pending,sizeof(pending))&&!vr.render_pose_valid);
+ CHECK(halo_vr_view(&view)&&halo_vr_render_view_commit(1));drawn=vr.views;vr.acquired[VR_SWAPCHAIN_EYES]=0;
+ halo_vr_present(1,0);CHECK(vr.pending_layers.projection&&!memcmp(vr.pending_layers.eye,drawn.eye,sizeof(drawn.eye)));
+ /* A later fresh menu locate remains live and is never replaced by A. */
+ reset();CHECK(frame_begin());CHECK(halo_vr_view(&view)&&halo_vr_render_view_commit(1));tag=8;CHECK(halo_vr_view(&view));vr.menus=1;
+ halo_vr_present(0,1);CHECK(!vr.pending_layers.projection&&vr.pending_layers.hud&&vr.menu_open&&!vr.render_pose_valid);
+ CHECK(fabsf(vr.menu_pose.position[0]-vr.views.head.position[0])>0.1f);
+}
+static void missing_failed_and_lost_frames(void){struct halo_vr_view view;int failure;
+ reset();CHECK(frame_begin());halo_vr_present(1,0);CHECK(!vr.pending_layers.projection&&!vr.render_pose_valid); /* no camera commit */
+ reset();CHECK(frame_begin());CHECK(halo_vr_view(&view)&&halo_vr_render_view_commit(1));halo_vr_render_view_commit(0);halo_vr_present(1,0);CHECK(!vr.pending_layers.projection);
+ reset();CHECK(frame_begin());CHECK(halo_vr_view(&view)&&halo_vr_render_view_commit(1));locate_ok=0;CHECK(!halo_vr_view(&view)&&!vr.render_pose_valid);halo_vr_present(1,0);CHECK(!vr.pending_layers.projection);
+ for(failure=0;failure<12;failure++){
+ reset();CHECK(frame_begin());CHECK(halo_vr_view(&view)&&halo_vr_render_view_commit(1));
+ switch(failure){case 0:vr.views.focused=0;break;case 1:vr.views.should_render=0;break;case 2:vr.views.head.valid=0;break;case 3:vr.views.eye[0].valid=0;break;case 4:vr.views.eye[1].position[0]=NAN;break;case 5:vr.views.eye[0].orientation[3]=2;break;case 6:vr.views.fov[1][2]=INFINITY;break;case 7:vr.views.fov[0][0]=vr.views.fov[0][1];break;case 8:vr.views.head.position[1]=NAN;break;case 9:vr.views.head.orientation[0]=NAN;break;case 10:vr.acquired[VR_SWAPCHAIN_EYES]=-1;break;case 11:vr.frame=0;break;}
+ vr.depth_written=1;vr.depth_near=.1f;vr.depth_far=100;halo_vr_present(1,0);CHECK(!vr.pending_layers.projection&&!vr.pending_layers.depth&&!vr.render_pose_valid);
+ }
+ reset();CHECK(frame_begin());CHECK(halo_vr_view(&view)&&halo_vr_render_view_commit(1));halo_vr_present(0,0);CHECK(!vr.render_pose_valid&&!vr.pending_layers.projection); /* cancelled world */
+ wait_ok=0;CHECK(!frame_begin()&&!vr.render_pose_valid);vr.attempted=0;wait_ok=1;CHECK(frame_begin()&&!vr.render_pose_valid);vr.acquired[VR_SWAPCHAIN_EYES]=0;halo_vr_present(1,0);CHECK(!vr.pending_layers.projection);
+}
+int main(void){exact_draw_sample();missing_failed_and_lost_frames();printf("PASS: %u production world pose/submission lifetime assertions\n",checks);return 0;}
+'''
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ args = parser.parse_args()
+ vr = (ROOT / 'port/linux/src/vr.c').read_text()
+ render = (ROOT / 'port/linux/game/vr_render.c').read_text()
+ hands = (ROOT / 'port/linux/game/vr_hands.c').read_text()
+ matrix = (ROOT / 'source/math/matrix_math.c').read_text()
+ raster = (ROOT / 'source/rasterizer/rasterizer_transparent_geometry.c').read_text()
+ seat = SEAT_HARNESS.replace('@@PLATFORM@@', '\n\n'.join(function(vr, name) for name in (
+ 'quaternion_rotate', 'quaternion_yaw', 'game_axes', 'vehicle_view_active', 'vehicle_capture',
+ 'halo_vr_vehicle_reference', 'halo_vr_vehicle_orientation', 'vehicle_basis',
+ 'halo_vr_vehicle_clear_windshield', 'recentre', 'halo_vr_view', 'halo_vr_aim')))
+ seat = seat.replace('@@MATH@@', '\n\n'.join(matrix_function(matrix, name) for name in (
+ 'matrix4x3_transform_point', 'matrix4x3_inverse_transform_point')))
+ seat = seat.replace('@@GAME@@', '\n\n'.join(function(render, name) for name in (
+ 'vr_render_vehicle_camera_position', 'vr_render_vehicle_yaw', 'vr_render_vehicle_basis',
+ 'vr_render_vehicle_reference', 'vr_render_hides_vehicle_glass')))
+ seat = seat.replace('@@DRAW@@', function(raster, 'rasterizer_transparent_geometry_draw'))
+ seat = seat.replace('int main(void) {', SEAT_CASES + '\nint main(void) {\n full_rigid_seat();')
+ compile_and_run(seat, args)
+ state = re.search(r'static struct\s*\{.*?\}\s*vr_render;', render, re.S).group()
+ source = CAMERA.replace('@@STATE@@', state).replace('@@HULL@@', HULL).replace('@@PRODUCTION@@',
+ '\n\n'.join(function(render, name) for name in ('yaw_rotate', 'view_rotate', 'view_vector', 'vr_render_camera', 'hand_point')) + '\n' +
+ '\n\n'.join(function(hands, name) for name in ('point_add', 'angle_wrap', 'shoulders_place')))
+ compile_and_run(source, args, (ROOT / 'port/linux/game',))
+ # Actual state layouts avoid a parallel hand-maintained submission contract.
+ state = re.search(r'static struct\s*\{.*?\}\s*vr;', vr, re.S).group()
+ settings = re.search(r'static struct\s*\{\s*unsigned long read_at;.*?\}\s*settings\s*=\s*\{.*?\};', vr, re.S).group()
+ settings = re.sub(r'=\s*\{.*?\};', '= {0};', settings, flags=re.S)
+ snapshot = SNAPSHOT.replace('@@STATE@@', state + '\n' + settings).replace('@@PRODUCTION@@',
+ '\n\n'.join(function(vr, name) for name in (
+ 'quaternion_rotate', 'quaternion_multiply', 'game_axes', 'vehicle_view_active', 'vehicle_basis',
+ 'frame_begin', 'halo_vr_frame_active', 'render_tracking_valid', 'halo_vr_render_view_commit',
+ 'menu_tracking_valid', 'menu_level_orientation', 'menu_place', 'halo_vr_present', 'halo_vr_view')))
+ camera = function(render, 'vr_render_camera')
+ assert camera.index('halo_vr_render_view_commit(0)') < camera.index('halo_vr_view(view)')
+ assert camera.index('halo_vr_render_view_commit(1)') > camera.index('vr_render.eye_cameras[eye] = eye_camera')
+ compile_and_run(snapshot, args)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/test_vr_zoom_aim.py b/tools/test_vr_zoom_aim.py
new file mode 100644
index 0000000000000000000000000000000000000000..b676bf4273f3132f0db68e6d75e4246cc3f54af4
--- /dev/null
+++ b/tools/test_vr_zoom_aim.py
@@ -0,0 +1,228 @@
+#!/usr/bin/env python3
+"""Strict host regression of stock VR aiming, zoom and render scheduling.
+
+Extracts the actual platform functions and state layouts. No headset, game
+data, renderer or network is used; timing and tracking are deterministic.
+"""
+import argparse
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tempfile
+
+from test_vr_projectile_reticle import function, without_visibility_guard
+
+ROOT = Path(__file__).resolve().parents[1]
+PIN = '2ae0ee4e3e8a4dfdadfd528a5b085ca699fc9ea4'
+
+HARNESS = r'''
+#define _USE_MATH_DEFINES
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "vr.h"
+#include "vr_host.h"
+#include "vr_locomotion.h"
+#include "vr_vehicle.h"
+#include "vr_driving.h"
+#ifndef M_PI
+#define M_PI 3.14159265358979323846
+#endif
+#define TEST_HANDS_POSES 8
+#define HUD_WIDTH 1280
+#define HUD_HEIGHT 960
+#define METRES_PER_WORLD_UNIT 3.048f
+typedef unsigned long long Uint64;
+@@STATE@@
+static struct halo_vr_driving_state driving;
+static unsigned checks;
+#define CHECK(c) do { checks++; if (!(c)) { fprintf(stderr,"line %d: %s\n",__LINE__,#c); exit(1); } } while(0)
+static int near(float a,float b) { return fabsf(a-b)<0.00002f; }
+static void equal(const float a[3],const float b[3]) { int i;for(i=0;i<3;i++)CHECK(near(a[i],b[i])); }
+static float dot(const float a[3],const float b[3]) { return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]; }
+static void test_head_apply(void) {}
+static void jitter_apply(void) {}
+static void test_hands_apply(void) {}
+static int locate_ok;
+static int frame_aim_mode;
+int host_vr_locate(struct vr_host_views *views) { (void)views;return locate_ok; }
+int halo_vr_frame_active(void) { if(frame_aim_mode>=0){settings.controller_aim=frame_aim_mode==0;settings.gamepad_aim=frame_aim_mode==2;frame_aim_mode=-1;}return vr.frame; }
+void halo_vr_vehicle_reference(int unit,int vehicle,int seat,float yaw) { (void)unit;(void)vehicle;(void)seat;(void)yaw; }
+@@FUNCTIONS@@
+@@LEGACY@@
+static void reset(void) {
+ int eye;memset(&vr,0,sizeof(vr));memset(&settings,0,sizeof(settings));
+ vr.initialized=vr.frame=vr.origin_valid=vr.aim_synced=1;
+ vr.views.should_render=vr.views.focused=vr.views.head.valid=vr.views.aim[1].valid=1;
+ vr.views.head.orientation[3]=vr.views.aim[1].orientation[3]=1;
+ for(eye=0;eye<2;eye++){vr.views.eye[eye]=vr.views.head;vr.views.eye[eye].position[0]=eye?0.032f:-0.032f;}
+ vr.views.display_elapsed=vr.views.display_period=1.0f/90.0f;
+ settings.controller_aim=1;settings.world_scale=1;settings.hud_size=60;
+ locate_ok=1;
+ frame_aim_mode=-1;
+}
+static void pose(float yaw,float roll) {
+ float y[4]={0,sinf(yaw*.5f),0,cosf(yaw*.5f)},z[4]={0,0,sinf(roll*.5f),cosf(roll*.5f)};
+ vr.views.aim[1].orientation[0]=y[1]*z[2];vr.views.aim[1].orientation[1]=y[1]*z[3];
+ vr.views.aim[1].orientation[2]=y[3]*z[2];vr.views.aim[1].orientation[3]=y[3]*z[3];
+}
+static void orthogonal(const float f[3],const float u[3]) { CHECK(near(dot(f,f),1));CHECK(near(dot(u,u),1));CHECK(fabsf(dot(f,u))<0.00002f); }
+
+static void direct_aim_and_late_poses(void) {
+ int i,rate,repeat;float f[3],u[3],raw[3],raw_up[3],yaw,pitch,world[3];struct halo_vr_view view;
+ static const int rates[]={72,80,90,120};
+ for(rate=0;rate<4;rate++)for(i=-60;i<=60;i++){
+ reset();vr.views.display_elapsed=1.0f/(float)rates[rate];pose((float)i*.025f,(float)i*.017f);
+ vr.two_handed_weight=(float)(i+60)/120;
+ vr.two_handed_line[0]=.1f;vr.two_handed_line[1]=.2f;vr.two_handed_line[2]=-.9746794f;
+ weapon_pointing(f,u);legacy_weapon_pointing(raw,raw_up);equal(f,raw);equal(u,raw_up);orthogonal(f,u);
+ for(repeat=0;repeat<5;repeat++) {weapon_pointing(f,u);equal(f,raw);equal(u,raw_up);}
+ CHECK(halo_vr_aim(0,1,&yaw,&pitch)==1);CHECK(halo_vr_view(&view));
+ world[0]=cosf(pitch)*cosf(yaw-vr.body_yaw);world[1]=cosf(pitch)*sinf(yaw-vr.body_yaw);world[2]=sinf(pitch);
+ equal(world,view.hand_forward);game_axes(f,vr.origin_yaw,world);equal(world,view.hand_forward);
+ /* The late controller pose updates immediately, without a zoom filter's history. */
+ pose(.73f,-.31f);vr.two_handed_weight=0;CHECK(halo_vr_view(&view));
+ legacy_weapon_pointing(raw,raw_up);game_axes(raw,vr.origin_yaw,world);equal(world,view.hand_forward);
+ CHECK(halo_vr_aim(0,1,&yaw,&pitch)==1);
+ world[0]=cosf(pitch)*cosf(yaw-vr.body_yaw);world[1]=cosf(pitch)*sinf(yaw-vr.body_yaw);world[2]=sinf(pitch);equal(world,view.hand_forward);
+ }
+ reset();pose(.8f,0);CHECK(halo_vr_view(&view));CHECK(view.controller_aim);
+ vr.views.aim[1].valid=0;CHECK(halo_vr_view(&view));CHECK(!view.controller_aim);
+ vr.views.aim[1].valid=1;pose(-.8f,0);CHECK(halo_vr_view(&view));
+ legacy_weapon_pointing(raw,raw_up);game_axes(raw,vr.origin_yaw,world);equal(world,view.hand_forward);
+}
+
+static void controller_pose_ownership(void){
+ int invalid;struct halo_vr_view view;float body;
+ reset();pose(2.6f,.1f);vr.views.aim[1].position[2]=2;vr.input.active[1]=0;vr.body_yaw=.73f;body=vr.body_yaw;
+ CHECK(halo_vr_view(&view)&&view.controller_aim_requested&&view.controller_aim);
+ /* A fresh VALID controller behind the head remains an aim pose without
+ * requiring an active input action, visibility or a TRACKED flag. */
+ CHECK(near(body,vr.body_yaw)&&vr.aim_synced);
+ for(invalid=0;invalid<5;invalid++){
+ reset();view.controller_aim=view.left_hand_valid=1;
+ switch(invalid){case 0:vr.views.aim[1].valid=0;break;case 1:vr.views.aim[1].orientation[3]=0;break;
+ case 2:vr.views.aim[1].orientation[0]=NAN;break;case 3:vr.views.aim[1].position[2]=INFINITY;break;case 4:locate_ok=0;break;}
+ CHECK(halo_vr_view(&view)==(invalid!=4));CHECK(view.controller_aim_requested&&!view.controller_aim);
+ CHECK(vr.aim_synced&&near(vr.body_yaw,0));
+ locate_ok=1;vr.views.aim[1].valid=1;memset(vr.views.aim[1].orientation,0,sizeof(vr.views.aim[1].orientation));
+ vr.views.aim[1].orientation[3]=1;vr.views.aim[1].position[2]=0;
+ CHECK(halo_vr_view(&view)&&view.controller_aim_requested&&view.controller_aim);
+ }
+ reset();vr.frame=0;view.controller_aim=view.left_hand_valid=1;CHECK(!halo_vr_view(&view));
+ CHECK(view.controller_aim_requested&&!view.controller_aim&&!view.left_hand_valid);
+ reset();settings.controller_aim=0;CHECK(halo_vr_view(&view)&&!view.controller_aim_requested&&!view.controller_aim);
+ settings.gamepad_aim=1;CHECK(halo_vr_view(&view)&&view.gamepad_aim&&!view.controller_aim_requested);
+ reset();frame_aim_mode=2;CHECK(halo_vr_view(&view)&&view.gamepad_aim&&!view.controller_aim_requested&&!view.controller_aim);
+ reset();settings.controller_aim=0;frame_aim_mode=0;CHECK(halo_vr_view(&view)&&view.controller_aim_requested&&view.controller_aim&&!view.gamepad_aim);
+ for(invalid=0;invalid<3;invalid++){
+ reset();settings.floating_hands=1;vr.views.aim[0]=vr.views.aim[1];vr.views.grip[0]=vr.views.aim[0];
+ switch(invalid){case 0:vr.views.grip[0].position[1]=NAN;break;case 1:vr.views.aim[0].position[0]=INFINITY;break;
+ case 2:vr.views.aim[0].orientation[3]=0;break;}
+ CHECK(halo_vr_view(&view)&&view.controller_aim&&!view.left_hand_valid);
+ }
+}
+
+typedef int boolean;
+#define TRUE 1
+#define FALSE 0
+struct camera_fixture {int position;};
+struct render_window {struct camera_fixture render_camera,rasterizer_camera;};
+static boolean render_vr_eye_pass;
+static int camera_available, eye_draws, hud_draws, end_calls, pass_count, pass_order[2];
+static int vr_render_camera(struct camera_fixture *camera,struct camera_fixture *rasterizer){
+ CHECK(camera&&rasterizer);rasterizer->position=17;return camera_available;}
+void halo_vr_pass(int pass){CHECK(pass_count<2);pass_order[pass_count++]=pass;}
+static void render_player_frame(struct render_window *window,void *unused){
+ CHECK(window&&unused==NULL&&render_vr_eye_pass&&pass_order[pass_count-1]==HALO_VR_PASS_EYES);eye_draws++;}
+static void vr_render_end(void){CHECK(!render_vr_eye_pass);end_calls++;}
+static void render_vr_hud(struct render_window *window){CHECK(window&&!render_vr_eye_pass);hud_draws++;}
+@@RENDER@@
+static void stock_render_passes(void){
+ int available;for(available=0;available<2;available++){
+ struct render_window window={{1},{2}};camera_available=available;eye_draws=hud_draws=end_calls=pass_count=0;
+ render_vr_player_frame(&window);CHECK(eye_draws==available&&hud_draws==1&&end_calls==available);
+ CHECK(pass_count==2*available);if(available){CHECK(pass_order[0]==HALO_VR_PASS_EYES&&pass_order[1]==HALO_VR_PASS_HUD);CHECK(window.rasterizer_camera.position==17);}
+ }
+}
+int main(void){direct_aim_and_late_poses();controller_pose_ownership();stock_render_passes();printf("PASS: %u direct aiming, stock zoom render schedule and late-pose assertions\n",checks);return 0;}
+'''
+
+def build_source():
+ vr=(ROOT/'port/linux/src/vr.c').read_text()
+ original=subprocess.check_output(['git','show',PIN+':port/linux/src/vr.c'],cwd=ROOT,text=True)
+ assert function(vr,'weapon_pointing_raw').replace('weapon_pointing_raw','weapon_pointing') == function(original,'weapon_pointing')
+ assert re.sub(r'\s+','',function(vr,'weapon_pointing')) == 'staticvoidweapon_pointing(floatforward[3],floatup[3]){weapon_pointing_raw(forward,up);}'
+ # Old true-valued INI/environment settings cannot reactivate removed code.
+ for obsolete in ('vr.scope','vr.scope_size','vr.zoom_stabilization','zoom_aim_pointing','halo_vr_scope_prepare','halo_vr_weapon_zoom'):
+ assert obsolete not in vr, obsolete
+ for path in ('port/linux/game/vr_render.c','port/linux/src/d3d8_gl.c','source/render/render.c',
+ 'source/interface/first_person_weapons.c','source/interface/hud_weapon.c','source/interface/interface.c',
+ 'port/linux/arm64/host_vr.c'):
+ text=(ROOT/path).read_text()
+ for obsolete in ('HALO_VR_PASS_SCOPE','VR_SWAPCHAIN_SCOPE','vr_render_scope','vr_copy_scope','scope_layer_valid'):
+ assert obsolete not in text, (path,obsolete)
+ host=(ROOT/'port/linux/arm64/host_vr.c').read_text()
+ assert 'VR_SWAPCHAIN_RESERVED' not in host, 'Reserved optics slot must never be allocated/acquired/submitted'
+ assert 'reserved_optic' not in host, 'Reserved optics payload must never be submitted'
+ for path in ('source/interface/first_person_weapons.c','source/interface/hud_weapon.c','source/interface/interface.c'):
+ text=(ROOT/path).read_text()
+ if path.endswith('first_person_weapons.c'):
+ text=without_visibility_guard(text,'vr_render_first_person_visible')
+ elif path.endswith('hud_weapon.c'):
+ text=without_visibility_guard(text,'vr_render_hud_crosshairs_visible')
+ assert text == subprocess.check_output(['git','show',PIN+':'+path],cwd=ROOT,text=True), path
+ state=re.search(r'static struct\s*\{.*?\}\s*vr;',vr,re.S).group()
+ settings=re.search(r'static struct\s*\{\s*unsigned long read_at;.*?\}\s*settings\s*=\s*\{.*?\};',vr,re.S).group()
+ settings=re.sub(r'=\s*\{.*?\};','= {0};',settings,flags=re.S)
+ names=('quaternion_rotate','game_axes','vehicle_view_active','halo_vr_vehicle_orientation','vehicle_basis','normalize','weapon_pointing_raw','weapon_pointing','halo_vr_view','halo_vr_aim')
+ inactive_driver=('static int driving_aim(float *yaw,float *pitch){(void)yaw;(void)pitch;return 0;}\n'
+ 'static void driving_reset(int require_release){(void)require_release;}\n')
+ render=(ROOT/'source/render/render.c').read_text()
+ return HARNESS.replace('@@STATE@@',state+'\n'+settings).replace('@@FUNCTIONS@@',inactive_driver+'\n\n'.join(function(vr,name) for name in names)).replace(
+ '@@LEGACY@@',function(original,'weapon_pointing').replace('weapon_pointing','legacy_weapon_pointing')).replace(
+ '@@RENDER@@',function(render,'render_vr_player_frame'))
+
+def compile_and_run(source, args, extra_includes=()):
+ compiler, vcvars = args.cc or os.environ.get('CC'), args.vcvarsall
+ if not compiler and os.name == 'nt':
+ candidates = sorted((Path(os.environ.get('ProgramFiles', 'C:/Program Files')) / 'Microsoft Visual Studio').glob(
+ '*/**/VC/Tools/MSVC/*/bin/Hostx64/x64/cl.exe'))
+ if candidates:
+ compiler = str(candidates[-1]); vcvars = vcvars or candidates[-1].parents[6] / 'Auxiliary/Build/vcvarsall.bat'
+ compiler = compiler or shutil.which('cc') or shutil.which('clang') or shutil.which('gcc')
+ if not compiler:
+ raise SystemExit('Select a C11 compiler with --cc.')
+ with tempfile.TemporaryDirectory(prefix='halo-vr-zoom-', ignore_cleanup_errors=True) as directory:
+ temp = Path(directory); cfile = temp / 'test.c'; exe = temp / ('test.exe' if os.name == 'nt' else 'test')
+ cfile.write_text(source, encoding='utf-8'); includes = [ROOT / 'port/linux/src', *extra_includes]
+ if Path(compiler).name.lower() in ('cl', 'cl.exe'):
+ command = [compiler, '/nologo', '/std:c11', '/W4', '/WX', '/DHALO_VR', '/D_CRT_SECURE_NO_WARNINGS',
+ *('/I' + str(include) for include in includes), str(cfile), '/Fe:' + str(exe), '/Fo:' + str(temp / 'test.obj')]
+ if vcvars:
+ for path in (vcvars, Path(compiler), temp, ROOT):
+ if any(c in str(path) for c in '\r\n"&|<>^%!'):
+ raise SystemExit('Unsupported compiler/temp path.')
+ batch = temp / 'compile.cmd'
+ batch.write_text('@echo off\ncall "' + str(vcvars) + '" x64 >nul\nif errorlevel 1 exit /b 1\n' + subprocess.list2cmdline(command) + '\n')
+ command = ['cmd.exe', '/d', '/c', str(batch)]
+ else:
+ command = [compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-DHALO_VR',
+ *(item for include in includes for item in ('-I', str(include))),
+ str(cfile), '-o', str(exe)] + ([] if os.name == 'nt' else ['-lm'])
+ subprocess.run(command, cwd=temp, check=True)
+ subprocess.run([str(exe)], cwd=temp, check=True)
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--cc')
+ parser.add_argument('--vcvarsall', type=Path)
+ compile_and_run(build_source(), parser.parse_args())
+
+
+if __name__ == '__main__':
+ main()