mirror of
https://github.com/ClassicWasTaken/HaloCE-SteamFrame.git
synced 2026-10-17 22:00:27 +02:00
16141 lines
850 KiB
Diff
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. 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(¶meters);
|
|
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(¤t,0,sizeof(current));
|
|
+ CHECK(vr_render_hands(¤t));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(¤t,&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,¢er,&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&¶meters->rasterizer_target==_render_target_primary);
|
|
+ CHECK(parameters->window_index==6&¶meters->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==¢er_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(¤t_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, ¢er, &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, ¢er, &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 ⌖ }
|
|
+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(¤t);
|
|
+ CHECK(!memcmp(&original,¤t,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()
|