Compare commits

..
17 Commits
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 4e9de9a11c Merge upstream/main (2026-10-22): co-op script fixes, cached high_res_hud, menu cursor
Network version stays 32. The conflict in D3DDevice_Present: the swap
stays in the window's branch (VR presents through vr.c); upstream's
visibility_read_batch, after the swap, is desktop GL only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:23:25 +02:00
bnunu ce95f7deed Merge pull request #424 from eltanschauung/fix/coop-a10-softlock
Fix co-op Pillar of Autumn softlock glitch
2026-10-22 08:36:58 -04:00
bnunu a20ced034c Merge branch 'main' into fix/coop-a10-softlock 2026-10-22 08:36:44 -04:00
bnunu fc22e8467d Merge pull request #442 from bnunu/cache-high-res-hud-setting
Read display.high_res_hud once per settings change, not at every bind
2026-10-22 06:56:16 -04:00
bnunuandClaude Opus 5.5 038252b1ae Read display.high_res_hud once per settings change, not at every bind
texture_entry_result, reached from every texture bind (even the recent
table's fast path in xgpu_texture_get), called
config_boolean("display.high_res_hud"): the settings' lock and a strcmp
over every setting's name, about 2000 times a frame. It is now read again
only when config_changes() moves, as the renderer's other settings are
(per_pixel_lighting in d3d8_gl.c). config_write, the only writer once the
settings are loaded, counts each change, so a change in the menus takes
effect at the next bind as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 19:48:16 -07:00
bnunu 61621e6513 Merge pull request #435 from bnunu/halo-ce-universal/menu-cursor
The menus show a blue arrow cursor in place of the system's
2026-10-21 19:41:11 -04:00
bnunuandClaude Opus 5.5 1d79584ea4 Hide the menus' cursor while a controller is used
The menus turn the pointer on whenever they are up, so a player on a
controller had the arrow left in the middle of the screen until the menu
closed (as with the system's cursor before). Now a controller's button,
or a stick or trigger past its rest (8000, as gamepad_idle has it), hides
the cursor while a menu is up, and the mouse moved or clicked shows it
again. A menu opened while the controller was used last opens without
it: platform_ui_pointer_set_active hides it again after the release's
SDL_ShowCursor.

The warps are not taken for the mouse being used: the one to the middle
as a menu opens (SDL reports it as motion), and the ones back to where
the mouse is held without relative mode. Hidden, the cursor cannot move
the focus, so the d-pad keeps it. Desktop builds only, as the cursor.

Checked: the new functions built with clang -Wall -Wextra -Werror
against SDL 3.4.16 and run on Windows (a controller hides the cursor in
the menus, not in play; the mouse shows it again). The whole game not
built here, nor played with a controller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 16:40:04 -07:00
bnunuandClaude Opus 5.5 577f718a63 Put the menus' cursor's click point on the arrow's tip
The picture's arrow has a soft edge, so its tip is at (2, 2), not the
picture's corner, where the click point was. MENU_CURSOR_TIP is scaled to
the cursor's size with the picture (rounded): (1, 1) up to 1080p, (2, 2)
at 1440p and 2160p. From GuvenHuso's review.

Checked: built with clang -Wall -Wextra -Werror against SDL 3.4.16 and
run on Windows from 640x480 to 3840x2160, the click point SDL kept read
back at each size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 16:32:10 -07:00
bnunuandClaude Opus 5.5 b68d259c81 Size the menus' cursor to the window, as Halo PC's was
Halo PC drew its cursor in the menus' 640 x 480, so it took the same part
of the screen at any resolution. The cursor was a fixed 20 x 29 (or SDL's
scaling of it on Windows), small at 1440p and 4K. It is now 32 of the
menus' 480 lines tall: 48 pixels at 720p, 72 at 1080p, 96 at 1440p, 144 at
2160p. The lines are the window's height, or for a window narrower than
4:3 its width's three quarters, and the cursor is never under 16 pixels.

menu_cursor_fit makes it from the embedded picture (halved while that is
twice the height or more, then scaled once) and is called at window
creation and at every event pump, where it does nothing unless the
window's size in pixels has changed: resizes and fullscreen toggles
follow. With a pixel density above 1 (macOS, Wayland at a scale) the
cursor is made at its size in points with the pixels' image as its
alternate; SDL_HINT_MOUSE_DPI_SCALE_CURSORS is no longer set, since the
size already comes from the window's pixels.

Checked: the new functions built with clang -Wall -Wextra -Werror
against SDL 3.4.16 and run on Windows at 640x480 to 3840x2160 (cursors
22x32 to 99x144, one made per size change and none when the size is the
same). The cursor SDL makes on Windows matches the picture's rows (the
upside-down arrow seen in an OBS recording was OBS's). The whole game
not built here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 16:03:37 -07:00
bnunu 600edf7bb9 Merge pull request #433 from hkmodd/fix/visibility-readback-nosync
Ask the driver nothing before the swap in the visibility readback
2026-10-21 18:55:09 -04:00
hkmoddandClaude Opus 5.5 a0f22cbba7 Ask the driver nothing before the swap in the visibility readback
The desktop readback through the query buffer (711aa46d, aecdd895) asked
the driver for answers at every present before the swap: a GL error three
times, a fence's state once a batch, and a flush. A threaded driver
(NVIDIA's) answers only once its thread has carried out every command
queued before the question, here the whole frame just drawn, so the
render thread stopped working ahead of the driver at every frame. Build
220's readback asked nothing.

Now a batch is sent before the swap and read after it:

- visibility_send_batch, before the swap, writes VISIBILITY_UNCOPIED over
  the batch's places in the coherent mapping, sends the copies and a
  fence, and asks nothing. GL errors are asked for with debug.gl_debug
  only, as the renderer's other per-frame checks are, and the glFlush is
  gone: the swap sends the commands.
- visibility_read_batch, after the swap, reads a batch only once its
  fence has passed (glClientWaitSync with GL_SYNC_FLUSH_COMMANDS_BIT and
  no wait), so completion is still known from the fence; the batch sent
  that present is looked for from the next one. A batch the GPU is late
  with is waited for however long; none is given up on. A place that
  still holds VISIBILITY_UNCOPIED after the fence is a copy that failed,
  seen without asking for GL errors.
- A batch whose copies or fence failed is dropped, as before, and from
  then on the counts are read from each query, as with AMD's driver: a
  dropped batch is not used again, and with the buffer the readback would
  stop once all three had been.

AMD's CPU readback, the counts, the batches and the query ownership from
711aa46d are unchanged.

Checked: tools/harness/tests/test_visibility_readback.py, 5 cases and 7
negative controls. On the code before (its readback taken as the send,
before the swap), 2 cases fail: the driver asked 46 times before the swap
in 12 presents, and a count that was never copied taken for one.
A 4-machine a50 co-op on an RTX 4080 (Windows, 1440p, 4x MSAA,
debug.gl_debug), 120 seconds: 13000 batches sent and 13000 read, every
fence passed by the next present (none unavailable), no GL error, none
dropped, the buffer used throughout, 8649 nonzero counts. The harness
suite fails the same tests with and without the change (those that need
POSIX headers, on Windows). The Windows release build compiles.

Not checked: Linux, Mesa, Intel, AMD (whose path changes only in being
read after the swap), and a 32-player co-op game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 18:23:30 +02:00
bnunu 9f04a51259 Merge pull request #429 from eltanschauung/fix/coop-gravity-lift
Fix co-op gravity lift soft lock glitch
2026-10-21 05:35:42 -04:00
Jonas VolmanandClaude Opus 5.5 47981601da Credit pugzysixtyfour for the menus' cursor (#435)
The blue arrow cursor this pull request adds (port/assets/cursor) is
pugzysixtyfour's art. AUTHORS.md says so beside the other art credits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:49:00 -07:00
Jonas VolmanandClaude Opus 5.5 a4e4bd8317 The menus show a blue arrow cursor in place of the system's.
With a mouse, the menus' pointer (platform_ui_pointer_set_active) was the
system's cursor. The game window now has a cursor of its own:
port/assets/cursor/menu_cursor.png, an 80 x 116 blue arrow with a
transparent background, embedded by tools/embed_assets.py as the window
icon is and made into an SDL colour cursor when the window is created.

At 100% display scale the cursor is the picture at a quarter of its size
(20 x 29, by two halvings); the picture itself is the alternate image SDL
scales from at larger display scales (SDL_HINT_MOUSE_DPI_SCALE_CURSORS,
which SDL implements on Windows). The hot spot is the arrow's tip, the
picture's top-left corner.

Desktop builds only: the code sits beside the window icon's, so Android
and the web keep theirs. The cursor shows wherever SDL shows one over the
window, which is the menus' pointer, the scoreboard's and a mouse freed
with F12; in play the mouse stays captured and hidden as before.

Checked on Windows: the build, and one start to the main menu with a
hidden window. A check line (not in this commit) logged the cursor made
and active, 20 x 29 with the 80 x 116 alternate, and saved SDL's 20 x 29
image, which looks as intended. Not built here for Linux, where SDL uses
the 20 x 29 image at every display scale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:40:55 -07:00
eltanschauung 9044950d78 Remove unused co-op script test counter (#429)
The CI compiler treats -Wunused-but-set-global as an error. list_count was incremented by the fixture but never read, so remove it instead of weakening compiler warnings. All 12 targeted regressions, including their negative controls, pass. This changes test code only.
2026-10-20 14:23:46 -04:00
eltanschauung 9d7436bd64 Fix co-op gravity lift teleports for extra players
Match the reverse unit order of hs_players when selecting player0 and followers. The forward datum-order assumption returned no followers, leaving players beyond the two named by a50's lift script in the unloaded outdoor BSP.

Tested the actual a50 transition with a host and two clients, including the played and skipped cutscene placement sequences and settled floor positions. Regression coverage exercises 0-128 players, sparse/dead/recycled slots, output bounds, host authority, and every follower action, with negative controls. Windows and Linux release builds pass. This is host-side placement only; the network protocol is unchanged.

(cherry picked from commit 51abcfff9968eee5b91de2bba9d273b70be91850)
2026-10-20 13:25:38 -04:00
eltanschauung cdce33647a Fix co-op Pillar of Autumn softlock glitch
A co-op ALL-player trigger could advance after an unchecked teleport, or while (players) was empty during respawn. Pillar of Autumn then permanently closed its blast door with players still outside.

Preserve player-list provenance in the existing unused header word, block empty or unavailable player-list waits only on the co-op host, and recheck all arrivals after gathering. Use the existing crowd fallback and restore rejected placements instead of leaving a player outside the volume or on a BSP switch. Empty AI lists, non-waiting tests and non-host behavior retain their semantics; no network message or protocol changes.

Validated with 17 production-source gate tests and negative controls, covering 1-128 players, failed or displaced teleports, BSP boundaries, retries, list reuse, allocation failure and empty AI/player lists. Windows and Linux release builds and the full local regression suite pass.

(cherry picked from commit 49b4716c4be187a16800300671221a964094e6fb)
2026-10-20 07:25:20 -04:00
19 changed files with 1249 additions and 93 deletions

No files matched your search

+2
View File
@@ -10,6 +10,8 @@
weapons' reticles): redraws by MrBruh (`port/assets/hud`), based on
reference photos by Aerocatia. The sniper rifle's zoom labels (2x, 10x)
are set in Overpass (`port/assets/fonts`).
- **The menus' mouse cursor** (the blue arrow the menus show for the
mouse): by pugzysixtyfour (`port/assets/cursor`).
## Third-party art
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

+5
View File
@@ -47,6 +47,11 @@ with ideas from VALORANT's netcode articles, keeping the 30 Hz tick:
host's is once that stops, or if they drift apart while it moves;
- which named objects exist, so scripted creates and deletes match.
Script actions naming player0 also reach the players beyond player1.
They use the same reverse datum order as the script's `(players)` list,
skipping players without units, so a scripted BSP transition does not
leave the extra players in the unloaded part of the level.
Every machine follows the host's structure BSP: a switch is sent at once
and reliably, and each client's input says which BSP it has loaded.
Until a client has the host's, the host takes none of its players'
+3 -1
View File
@@ -263,7 +263,9 @@ second get a numeric suffix so previous screenshots are preserved.
One movement of the mouse wheel changes the weapon one time. A second
movement after a short pause changes it again.
In the menus, the mouse moves a pointer:
In the menus, the mouse moves a pointer, a blue arrow
(`port/assets/cursor/menu_cursor.png`) in place of the system's, sized to
the window as Halo PC's was (32 of the menus' 480 lines tall):
- The item below the pointer gets the focus.
- A left click selects the item. On a setting with values, a click on the
+83 -34
View File
@@ -103,33 +103,11 @@ boolean coop_scripts_any_player_will_do(
return any;
}
void coop_scripts_gather_in_volume(
short trigger_volume_index,
boolean coop_scripts_empty_player_list(
long object_list_index)
{
long reference_index;
long object_index;
long inside_index = NONE;
for (object_index = object_list_get_first(object_list_index, &reference_index);
object_index != NONE && inside_index == NONE;
object_index = object_list_get_next(object_list_index, &reference_index))
{
if (scenario_trigger_volume_test_object(trigger_volume_index, object_index))
inside_index = object_index;
}
if (inside_index == NONE)
return;
for (object_index = object_list_get_first(object_list_index, &reference_index);
object_index != NONE;
object_index = object_list_get_next(object_list_index, &reference_index))
{
if (!scenario_trigger_volume_test_object(trigger_volume_index, object_index))
{
player_teleport(player_index_from_unit_index(object_index), inside_index,
&object_get(inside_index)->object.bounding_sphere_center);
}
}
return coop_scripts_host() && (object_list_index == NONE ||
(object_list_is_players(object_list_index) && object_list_count(object_list_index) == 0));
}
/* the units of the players following player0, if `unit_index` is
@@ -141,22 +119,29 @@ static short players_following(
{
struct data_iterator iterator;
struct player_datum *player;
short position = 0;
long players[HALO_PORT_MAXIMUM_NETWORK_PLAYERS];
short player_count = 0;
short position;
short count = 0;
if (unit_index == NONE || !coop_scripts_host())
return 0;
/* (in the order (players) lists them: player0 is the first with a unit) */
/* hs_players adds each unit to the head of a reference list. Its
player0 is the LAST live unit in datum order, not the first. Keep that
same order here, including gaps left by dead or disconnected players. */
data_iterator_new(&iterator, player_data);
while ((player = data_iterator_next(&iterator)) != NULL)
{
if (player->unit_index == NONE)
continue;
if (position == 0 && player->unit_index != unit_index)
return 0;
if (position >= NAMED_PLAYERS && count < maximum_count)
unit_indices[count++] = player->unit_index;
position++;
if (player->unit_index != NONE && player_count < NUMBEROF(players))
players[player_count++] = player->unit_index;
}
if (player_count == 0 || players[player_count - 1] != unit_index)
return 0;
for (position = player_count - NAMED_PLAYERS - 1;
position >= 0 && count < maximum_count;
position--)
{
unit_indices[count++] = players[position];
}
return count;
}
@@ -221,6 +206,70 @@ static boolean teleport_to(
return arrived;
}
/* A placement may fail after moving the unit, or find free ground outside
this volume. Restore that unit before retrying; never move the loaded BSP.
The second attempt uses the same crowd fallback as scripted teleports. */
static void gather_player_in_volume(
short trigger_volume_index,
long unit_index,
long anchor_index,
real_point3d const *destination)
{
long player_index = player_index_from_unit_index(unit_index);
real_point3d original;
real_point3d placed;
short attempt;
if (player_index == NONE)
return;
object_get_origin(unit_index, &original);
for (attempt = 0; attempt < 2; attempt++)
{
boolean arrived = teleport_to(player_index, anchor_index, destination, attempt != 0);
if (arrived && scenario_trigger_volume_test_object(trigger_volume_index, unit_index) &&
!player_position_switches_bsp(object_get_origin(unit_index, &placed)))
{
return;
}
/* This was the unit's valid position at entry, on the same tick. */
object_set_position(unit_index, &original, NULL, NULL);
}
}
void coop_scripts_gather_in_volume(
short trigger_volume_index,
long object_list_index)
{
long reference_index;
long object_index;
long inside_index = NONE;
real_point3d destination;
if (!coop_scripts_host())
return;
for (object_index = object_list_get_first(object_list_index, &reference_index);
object_index != NONE;
object_index = object_list_get_next(object_list_index, &reference_index))
{
if (scenario_trigger_volume_test_object(trigger_volume_index, object_index) &&
!player_position_switches_bsp(object_get_origin(object_index, &destination)))
{
inside_index = object_index;
break;
}
}
if (inside_index == NONE)
return;
for (object_index = object_list_get_first(object_list_index, &reference_index);
object_index != NONE;
object_index = object_list_get_next(object_list_index, &reference_index))
{
if (!scenario_trigger_volume_test_object(trigger_volume_index, object_index))
gather_player_in_volume(trigger_volume_index, object_index, inside_index, &destination);
}
}
boolean coop_scripts_unit_near(
long unit_index,
real_point3d const *destination)
+6 -1
View File
@@ -16,9 +16,14 @@ Campaign scripts made to work for every player in network co-op
list as inside once any of it is, because every object in it is a player */
boolean coop_scripts_any_player_will_do(
long object_list_index);
/* An empty (players) result or failed list allocation cannot advance a
co-op gate. Does not change valid empty AI lists or non-host behavior. */
boolean coop_scripts_empty_player_list(
long object_list_index);
/* and when a script waits on it and one player is inside, the players of
the list outside come beside that one, so a gate (the Maw's lift, the run
to its bridge, a30's Pelicans) leaves no one behind */
to its bridge, a30's Pelicans) leaves no one behind. The caller must test
the volume again: a failed placement leaves the script waiting. */
void coop_scripts_gather_in_volume(
short trigger_volume_index,
long object_list_index);
+117 -53
View File
@@ -634,6 +634,9 @@ vertices. */
#ifndef HALO_GLES
#define VISIBILITY_BATCH_COUNT 3
/* what a test's place in the buffer holds until the GPU copies its count
there: more than any test covers (8K with 4x MSAA has 133 million samples) */
#define VISIBILITY_UNCOPIED 0xFFFFFFFFUL
/* a frame's tests: their queries (kept until every count is read), the slot
and area of each, and (once the frame is presented) the fence its copies
wait on */
@@ -3082,7 +3085,7 @@ static void visibility_results_initialize(void)
{
const char *vendor = (const char *)glGetString(GL_VENDOR);
GLsizeiptr size = VISIBILITY_BATCH_COUNT * VISIBILITY_TEST_SLOTS * sizeof(GLuint);
GLbitfield flags = GL_MAP_READ_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT;
GLbitfield flags = GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT;
BOOL success;
device.visibility_logging = config_boolean("debug.gl_debug");
@@ -7389,20 +7392,90 @@ void d3d8_clear_back_buffer_region(float left, float top, float right, float bot
}
#ifndef HALO_GLES
/* the counts read from each query, as with AMD's driver, once a batch's
copies into the buffer have failed: a batch that fails is not used again,
and with the buffer the readback would stop once all of them had. The
batches still waiting keep their queries and are read from them. */
static void visibility_fall_back_to_queries(void)
{
unsigned long i;
for (i = 0; i < VISIBILITY_BATCH_COUNT; i++)
{
if (device.visibility_batches[i].fence)
glDeleteSync(device.visibility_batches[i].fence);
device.visibility_batches[i].fence = NULL;
}
device.visibility_mapped = NULL;
platform_log("GPU: visibility test counts read from each query from now on");
}
/* this frame's batch sent, before the swap. A copy into the buffer right
after each test stopped the GPU until that test's draws were through,
hundreds of times a frame in The Library; the GPU copies a batch's counts
when its frame is presented, over the CPU's VISIBILITY_UNCOPIED, with a
fence after the copies. Nothing here waits for an answer from the driver:
with a threaded driver (NVIDIA's) any such call waits until its thread has
carried out the whole frame queued before it, and here, before the swap,
that held the frame back every time. GL errors are asked for with
debug.gl_debug; the swap, next, sends the commands to the GPU. */
static void visibility_send_batch(void)
{
struct visibility_batch *batch;
unsigned long i;
if (device.visibility_unavailable || device.visibility_batch >= VISIBILITY_BATCH_COUNT)
return;
batch = &device.visibility_batches[device.visibility_batch];
if (!batch->count)
return;
if (device.visibility_mapped)
{
volatile GLuint *counts = device.visibility_mapped + device.visibility_batch * VISIBILITY_TEST_SLOTS;
if (device.visibility_logging)
visibility_readback_gl_ok("before query copies");
/* (the coherent mapping makes these writes visible to the copies
sent after them; a place that still holds VISIBILITY_UNCOPIED
once the fence has passed is a copy that failed) */
for (i = 0; i < batch->count; i++)
counts[i] = VISIBILITY_UNCOPIED;
for (i = 0; i < batch->count; i++)
glGetQueryBufferObjectuiv(batch->queries[i], device.visibility_buffer, GL_QUERY_RESULT,
(GLintptr)((device.visibility_batch * VISIBILITY_TEST_SLOTS + i) * sizeof(GLuint)));
batch->fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
if (!batch->fence || (device.visibility_logging && !visibility_readback_gl_ok("query copies")))
{
batch->failed = TRUE;
device.visibility_stats.failures++;
if (batch->fence)
glDeleteSync(batch->fence);
batch->fence = NULL;
platform_log("GPU: visibility test batch %lu dropped: its counts could not be copied",
device.visibility_batch);
visibility_fall_back_to_queries();
}
}
batch->sealed = TRUE;
batch->serial = ++device.visibility_serial;
batch->frame = device.frame;
device.visibility_batch = VISIBILITY_BATCH_COUNT;
device.visibility_stats.submitted++;
}
/* the counts of earlier frames' tests, read into their slots oldest batch
first, and this frame's batch sent. A copy into the buffer right after each
test stopped the GPU until that test's draws were through, hundreds of times
a frame in The Library; the GPU copies a batch's counts when its frame is
presented, and the CPU reads them once the fence after the copies has
passed, a frame or more later. With AMD's driver the CPU reads each query
once it says it is available. A batch is not reused until all its counts
have been read. */
first once the fence after their copies has passed, after the swap, when
the driver has the least queued before the question; with AMD's driver the
CPU reads each query once it says it is available. A batch is not reused
until all its counts have been read; one the GPU is late with is waited for
at the next presents, however long (its slots keep their earlier counts
meanwhile, and with every batch waiting, tests are dropped). */
static void visibility_read_batch(void)
{
struct visibility_batch *batch;
unsigned long i, oldest, pending = 0, oldest_age = 0;
GLint binding = 0;
BOOL bound = TRUE;
BOOL bound = TRUE, fall_back = FALSE;
if (device.visibility_unavailable)
return;
@@ -7430,17 +7503,21 @@ static void visibility_read_batch(void)
if (oldest == VISIBILITY_BATCH_COUNT)
break;
batch = &device.visibility_batches[oldest];
if (device.visibility_mapped)
/* (the batch this present sent: looked for from the next one) */
if (batch->frame == device.frame)
break;
if (device.visibility_mapped && batch->fence)
{
GLenum status = glClientWaitSync(batch->fence, 0, 0);
BOOL success = visibility_readback_gl_ok("copy completion");
/* (the flush makes sure the fence reaches the GPU without
counting on the swap's; after the swap it has nothing to send) */
GLenum status = glClientWaitSync(batch->fence, GL_SYNC_FLUSH_COMMANDS_BIT, 0);
if (success && status == GL_TIMEOUT_EXPIRED)
if (status == GL_TIMEOUT_EXPIRED)
{
device.visibility_stats.unavailable++;
break;
}
if (!success || (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED))
if (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED)
{
batch->failed = TRUE;
device.visibility_stats.failures++;
@@ -7449,7 +7526,10 @@ static void visibility_read_batch(void)
glDeleteSync(batch->fence);
batch->fence = NULL;
if (batch->failed)
continue;
{
fall_back = TRUE;
break;
}
}
while (batch->read < batch->count)
{
@@ -7458,7 +7538,15 @@ static void visibility_read_batch(void)
float area = batch->areas[test];
if (device.visibility_mapped)
{
samples = device.visibility_mapped[oldest * VISIBILITY_TEST_SLOTS + test];
if (samples == VISIBILITY_UNCOPIED)
{
batch->failed = TRUE;
device.visibility_stats.failures++;
break;
}
}
else
{
glGetQueryObjectuiv(batch->queries[test], GL_QUERY_RESULT_AVAILABLE, &available);
@@ -7491,7 +7579,12 @@ static void visibility_read_batch(void)
{
/* (its slots keep their earlier counts, and its queries, whose
state is unknown, are not used again) */
platform_log("GPU: visibility test batch %lu dropped: its queries could not be read", oldest);
platform_log("GPU: visibility test batch %lu dropped: its counts could not be read", oldest);
if (device.visibility_mapped)
{
fall_back = TRUE;
break;
}
continue;
}
if (batch->read < batch->count)
@@ -7505,42 +7598,10 @@ static void visibility_read_batch(void)
glBindBuffer(GL_QUERY_BUFFER, (GLuint)binding);
visibility_readback_gl_ok("restore query buffer");
}
if (device.visibility_batch < VISIBILITY_BATCH_COUNT)
{
batch = &device.visibility_batches[device.visibility_batch];
if (batch->count)
{
if (device.visibility_mapped)
{
BOOL success;
visibility_readback_gl_ok("before query copies");
for (i = 0; i < batch->count; i++)
glGetQueryBufferObjectuiv(batch->queries[i], device.visibility_buffer, GL_QUERY_RESULT,
(GLintptr)((device.visibility_batch * VISIBILITY_TEST_SLOTS + i) * sizeof(GLuint)));
batch->fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
success = visibility_readback_gl_ok("query copies");
if (!success || !batch->fence)
{
batch->failed = TRUE;
device.visibility_stats.failures++;
if (batch->fence)
glDeleteSync(batch->fence);
batch->fence = NULL;
platform_log("GPU: visibility test batch %lu dropped: its counts could not be copied",
device.visibility_batch);
}
}
batch->sealed = TRUE;
batch->serial = ++device.visibility_serial;
batch->frame = device.frame;
device.visibility_batch = VISIBILITY_BATCH_COUNT;
device.visibility_stats.submitted++;
/* (sent to the GPU now, whether or not presenting flushes: the
counts are first looked for at the next present) */
glFlush();
}
}
/* (after the reads from the buffer: those from each query unbind it
first, from the next present) */
if (fall_back)
visibility_fall_back_to_queries();
for (i = 0; i < VISIBILITY_BATCH_COUNT; i++)
{
batch = &device.visibility_batches[i];
@@ -7683,7 +7744,7 @@ void WINAPI D3DDevice_Present(CONST RECT *source_rectangle, CONST RECT *destinat
back_buffer->target.texture);
render_target_resolve(&back_buffer->target);
#ifndef HALO_GLES
visibility_read_batch();
visibility_send_batch();
#endif
if (screenshot_every > 0 && device.frame % (unsigned long)screenshot_every == 0)
write_screenshot(back_buffer);
@@ -7732,6 +7793,9 @@ void WINAPI D3DDevice_Present(CONST RECT *source_rectangle, CONST RECT *destinat
glBindVertexArray(device.vertex_array);
platform_video_swap();
}
#ifndef HALO_GLES
visibility_read_batch();
#endif
xgpu_gl_state_invalidate();
memory_watch_begin_frame();
xgpu_texture_cache_begin_frame();
+152
View File
@@ -117,6 +117,9 @@ static void screen_keyboard_update(void);
/* the windows' icon, a PNG (tools/embed_assets.py, from port/assets/icon) */
extern const unsigned int platform_window_icon[];
extern const unsigned long platform_window_icon_size;
/* the menus' pointer, a PNG (tools/embed_assets.py, from port/assets/cursor) */
extern const unsigned int platform_menu_cursor[];
extern const unsigned long platform_menu_cursor_size;
#endif
#endif
@@ -1005,6 +1008,116 @@ BOOL platform_video_hdr(float *sdr_white, float *headroom)
}
#endif
#ifndef HALO_ANDROID
#ifndef HALO_GLES
/* ---------- the menus' cursor
The blue arrow the menus show for the mouse, in place of the system's. Halo
PC drew its cursor in the menus' 640 x 480, so it took the same part of the
screen at any resolution; this one is made again whenever the window's size
in pixels changes (menu_cursor_fit, from platform_pump_events). */
/* the arrow's height in the menus' 480 lines (72 pixels at 1080p) */
#define MENU_CURSOR_HEIGHT 32
/* the arrow's tip in the picture, past its soft edge: the click point */
#define MENU_CURSOR_TIP 2
static SDL_Surface *menu_cursor_picture;
static SDL_Cursor *menu_cursor;
static int menu_cursor_height;
/* the picture at a height, halved first while it is twice that or more (one
linear scaling to less than half the size skips pixels) */
static SDL_Surface *menu_cursor_scale(int height)
{
int width = (menu_cursor_picture->w * height + menu_cursor_picture->h / 2) / menu_cursor_picture->h;
SDL_Surface *surface = SDL_DuplicateSurface(menu_cursor_picture);
while (surface && surface->h / 2 >= height)
{
SDL_Surface *half = SDL_ScaleSurface(surface, surface->w / 2, surface->h / 2, SDL_SCALEMODE_LINEAR);
SDL_DestroySurface(surface);
surface = half;
}
if (surface && surface->h != height)
{
SDL_Surface *sized = SDL_ScaleSurface(surface, width > 0 ? width : 1, height, SDL_SCALEMODE_LINEAR);
SDL_DestroySurface(surface);
surface = sized;
}
return surface;
}
/* the cursor made for the window's size, when that has changed: its height
the part of the window the menus' 480 lines take (their 4:3 fits a window of
another shape), in pixels; where the desktop counts in points (macOS, Wayland
at a scale) the cursor is that size in points, with the pixels' image as its
alternate */
static void menu_cursor_fit(void)
{
int width = 0, height = 0, lines, cursor_height;
float density;
SDL_Surface *pixels, *points;
SDL_Cursor *cursor = NULL;
if (!menu_cursor_picture || !SDL_GetWindowSizeInPixels(platform_window, &width, &height) ||
width <= 0 || height <= 0)
return;
lines = height < width * 3 / 4 ? height : width * 3 / 4;
cursor_height = lines * MENU_CURSOR_HEIGHT / 480;
if (cursor_height < 16)
cursor_height = 16;
if (cursor_height == menu_cursor_height)
return;
menu_cursor_height = cursor_height;
density = SDL_GetWindowPixelDensity(platform_window);
pixels = menu_cursor_scale(cursor_height);
points = density > 1.0f ? menu_cursor_scale((int)(cursor_height / density + 0.5f)) : pixels;
if (pixels && points && (points == pixels || SDL_AddSurfaceAlternateImage(points, pixels)))
cursor = SDL_CreateColorCursor(points,
(MENU_CURSOR_TIP * points->w + menu_cursor_picture->w / 2) / menu_cursor_picture->w,
(MENU_CURSOR_TIP * points->h + menu_cursor_picture->h / 2) / menu_cursor_picture->h);
if (cursor && SDL_SetCursor(cursor))
{
SDL_DestroyCursor(menu_cursor);
menu_cursor = cursor;
}
else
{
platform_log("cannot set the menus' cursor: %s", SDL_GetError());
if (cursor)
SDL_DestroyCursor(cursor);
}
if (points != pixels)
SDL_DestroySurface(points);
SDL_DestroySurface(pixels);
}
/* A controller's buttons or sticks, used while a menu is up, hide the
cursor, and the mouse moved or clicked shows it again, so a player on a
controller has no arrow left over the menus; a menu opened while the
controller was used last opens without it
(platform_ui_pointer_set_active). */
static BOOL menu_cursor_controller;
static void menu_cursor_controller_used(BOOL in_menus)
{
menu_cursor_controller = TRUE;
if (in_menus && SDL_CursorVisible())
SDL_HideCursor();
}
static void menu_cursor_mouse_used(BOOL in_menus)
{
menu_cursor_controller = FALSE;
if (in_menus && !SDL_CursorVisible())
SDL_ShowCursor();
}
#endif
#endif
BOOL platform_video_initialize(unsigned long width, unsigned long height)
{
int version;
@@ -1097,6 +1210,16 @@ BOOL platform_video_initialize(unsigned long width, unsigned long height)
platform_log("cannot set the window's icon: %s", SDL_GetError());
SDL_DestroySurface(icon);
}
/* the pointer the menus show (platform_ui_pointer_set_active), in place
of the system's, at the window's size */
if (platform_menu_cursor_size && !menu_cursor_picture)
{
menu_cursor_picture = SDL_LoadPNG_IO(SDL_IOFromConstMem(platform_menu_cursor, platform_menu_cursor_size), true);
if (!menu_cursor_picture)
platform_log("cannot read the menus' cursor: %s", SDL_GetError());
}
menu_cursor_height = 0;
menu_cursor_fit();
#endif
platform_fullscreen_requested = platform_fullscreen_setting();
platform_window_mode = platform_display_mode();
@@ -1934,6 +2057,9 @@ void platform_pump_events(void)
halo_vr_frame_begin();
exit_after_check();
platform_show_pending_message();
#if !defined(HALO_ANDROID) && !defined(HALO_GLES)
menu_cursor_fit();
#endif
#ifndef HALO_ANDROID
updater_poll(platform_window);
/* (Settings > Audio's output device, as it changes: dsound_sdl.c) */
@@ -1999,6 +2125,13 @@ void platform_pump_events(void)
break;
case SDL_EVENT_MOUSE_MOTION:
#ifndef HALO_ANDROID
#ifndef HALO_GLES
/* (not the warps: to the middle as a menu opens, and back to
where the mouse is held without relative mode) */
if (input_state.ui_pointer ? event.motion.x != ui_pointer.x || event.motion.y != ui_pointer.y :
!mouse_warp_capture || (int)event.motion.x != mouse_warp_x || (int)event.motion.y != mouse_warp_y)
menu_cursor_mouse_used(input_state.ui_pointer);
#endif
if (scoreboard_pointer_active)
{
scoreboard_pointer.x = event.motion.x;
@@ -2028,6 +2161,10 @@ void platform_pump_events(void)
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
#if !defined(HALO_ANDROID) && !defined(HALO_GLES)
if (event.button.down)
menu_cursor_mouse_used(input_state.ui_pointer);
#endif
if (event.button.button < 32)
{
if (event.button.down)
@@ -2197,6 +2334,17 @@ void platform_pump_events(void)
}
#endif
break;
#if !defined(HALO_ANDROID) && !defined(HALO_GLES)
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
menu_cursor_controller_used(input_state.ui_pointer);
break;
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
/* (past a stick's or trigger's rest, as gamepad_idle in
xinput_sdl.c has it) */
if (event.gaxis.value > 8000 || event.gaxis.value < -8000)
menu_cursor_controller_used(input_state.ui_pointer);
break;
#endif
case SDL_EVENT_GAMEPAD_REMOVED:
/* (SDL keeps a gamepad open until it is closed, even once the
controller has gone) */
@@ -2284,6 +2432,10 @@ void platform_ui_pointer_set_active(BOOL active)
memset(mouse_buttons_pressed, 0, sizeof(mouse_buttons_pressed));
pthread_mutex_unlock(&input_lock);
platform_mouse_capture(!active && !input_state.mouse_released);
#ifndef HALO_GLES
if (active && menu_cursor_controller)
SDL_HideCursor();
#endif
if (active)
{
int width, height;
+17 -1
View File
@@ -964,6 +964,22 @@ static unsigned long palette_hash(const D3DCOLOR *palette)
return hash ? hash : 1;
}
/* display.high_res_hud, read again only when a setting changes: it is asked
at every bind, and config_boolean takes the settings' lock and looks the name
up among them all */
static BOOL high_res_hud(void)
{
static unsigned long read_at = (unsigned long)-1;
static BOOL enabled;
if (read_at != config_changes())
{
read_at = config_changes();
enabled = config_boolean("display.high_res_hud") != 0;
}
return enabled;
}
/* an entry's GL texture and description: its replacement's, if it has one,
with the bitmap's own size (which its coordinates are in) */
static GLuint texture_entry_result(struct texture_entry *entry, GLenum *target,
@@ -1018,7 +1034,7 @@ static GLuint texture_entry_result(struct texture_entry *entry, GLenum *target,
}
/* These sheets keep the map's art and coordinates. Decode through the
* normal format reader; the HUD module owns only the interpolation. */
if (config_boolean("display.high_res_hud"))
if (high_res_hud())
{
if (!entry->hud_upscale_checked)
{
+13 -1
View File
@@ -201,6 +201,8 @@ long hs_players(
if (player->unit_index != NONE)
object_list_add(object_list_index, player->unit_index);
}
/* port: even an empty (players) list must not open a co-op gate. */
object_list_mark_players(object_list_index);
return object_list_index;
}
@@ -894,14 +896,24 @@ boolean hs_trigger_volume_test_objects_all(
long object_list_index)
{
boolean inside;
boolean waiting = hs_runtime_waiting_on_call();
/* port: an empty player list during respawn is not an arrival. Empty
AI lists retain the original ALL semantics. */
if (waiting && coop_scripts_empty_player_list(object_list_index))
return FALSE;
if (!coop_scripts_any_player_will_do(object_list_index))
return hs_trigger_volume_test_objects(trigger_volume_index, object_list_index, TRUE);
/* port: in network co-op, waiting for every player means waiting for
any one of them, and the rest are brought to them (coop_scripts.c) */
inside = hs_trigger_volume_test_objects(trigger_volume_index, object_list_index, FALSE);
if (inside && hs_runtime_waiting_on_call())
if (inside && waiting)
{
coop_scripts_gather_in_volume(trigger_volume_index, object_list_index);
/* port: gathering may fail or choose a point outside this volume.
Do not close an irreversible gate until everyone actually arrived. */
inside = hs_trigger_volume_test_objects(trigger_volume_index, object_list_index, TRUE);
}
return inside;
}
+18
View File
@@ -70,6 +70,8 @@ enum
player */
MAXIMUM_OBJECT_LISTS_PER_MAP = HALO_PORT_MAXIMUM_OBJECT_LISTS_PER_MAP,
MAXIMUM_LISTED_OBJECTS_PER_MAP = HALO_PORT_MAXIMUM_LISTED_OBJECTS_PER_MAP,
/* port: list provenance; does not change the header's size. */
_object_list_players_bit = 0,
};
/* ---------- macros */
@@ -131,12 +133,26 @@ long object_list_new(
{
struct object_list_header_datum* list = object_list_header_get(list_index);
list->count = 0;
list->flags = 0; /* port: a recycled slot has no previous provenance */
list->first_reference_index = NONE;
}
return list_index;
}
/* port: only hs_players marks its completed result. */
void object_list_mark_players(long list_index)
{
if (list_index != NONE)
SET_FLAG(object_list_header_get(list_index)->flags, _object_list_players_bit, TRUE);
}
boolean object_list_is_players(long list_index)
{
return list_index != NONE &&
TEST_FLAG(object_list_header_get(list_index)->flags, _object_list_players_bit);
}
void object_list_delete(
long list_index)
{
@@ -164,6 +180,8 @@ void object_list_add(
return;
list = object_list_header_get(object_list_index);
/* port: a changed list no longer has hs_players' provenance. */
list->flags = 0;
reference_list_add(object_list_data, &list->first_reference_index, object_index);
list->count++;
+4 -1
View File
@@ -19,7 +19,7 @@ OBJECT_LISTS.H
struct object_list_header_datum
{
short identifier;
word pad;
word flags; /* port: provenance, using the original unused word */
short reference_count;
short count;
long first_reference_index;
@@ -32,6 +32,9 @@ void object_lists_dispose(void);
void object_lists_initialize_for_new_map(void);
void object_lists_dispose_from_old_map(void);
long object_list_new(void);
/* port: distinguish an empty (players) result from an empty AI list. */
void object_list_mark_players(long list_index);
boolean object_list_is_players(long list_index);
void object_list_delete(long list_index);
void object_list_add(long object_list_index, long object_index);
long object_list_get_first(
+11 -1
View File
@@ -50,6 +50,8 @@ SMAA_FILES = (("SMAA.hlsl", "xgpu_smaa_shader"), ("area_tex.zlib", "xgpu_smaa_ar
("search_tex.zlib", "xgpu_smaa_search_texture"))
# the desktop windows' icon (tools/app_icon.py; port/linux/src/sdl_platform.c)
WINDOW_ICON = Path("port/assets/icon/opence-icon-256.png")
# the pointer the menus show in place of the system's (port/linux/src/sdl_platform.c)
MENU_CURSOR = Path("port/assets/cursor/menu_cursor.png")
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
@@ -92,7 +94,8 @@ def hud_asset_inputs() -> List[Path]:
return [*inputs, *(folder / f"{asset['name']}.png" for folder, asset, _ in textures()),
*(FONT_ASSETS / name for name in font_files()), *(MENU_ASSETS / name for name in menu_files()),
*(SMAA_ASSETS / name for name, _ in smaa_files()),
*([WINDOW_ICON] if (ROOT / WINDOW_ICON).is_file() else [])]
*([WINDOW_ICON] if (ROOT / WINDOW_ICON).is_file() else []),
*([MENU_CURSOR] if (ROOT / MENU_CURSOR).is_file() else [])]
def hud_configure_inputs() -> List[Path]:
@@ -251,6 +254,13 @@ def main() -> None:
lines.extend(words(data) if data else ["\t0,"])
lines.append("};")
lines.append(f"const unsigned long platform_window_icon_size = {len(data)};")
# the menus' pointer, a PNG (of size 0 where the checkout has none)
data = (ROOT / MENU_CURSOR).read_bytes() if (ROOT / MENU_CURSOR).is_file() else b""
lines.append("")
lines.append("const unsigned int platform_menu_cursor[] = {")
lines.extend(words(data) if data else ["\t0,"])
lines.append("};")
lines.append(f"const unsigned long platform_menu_cursor_size = {len(data)};")
lines.append("")
lines.append("#endif")
output = Path(sys.argv[1])
+98
View File
@@ -0,0 +1,98 @@
#include "harness.h"
#include <string.h>
typedef unsigned short word;
#define SET_FLAG(f,b,v) ((f)=(v)?(f)|(1U<<(b)):(f)&~(1U<<(b)))
#define MAX_PLAYERS 128
#include "list_header.inc"
static struct object_list_header_datum headers[8];
static int used[8], fail_allocate;
static struct {long value,next;} refs[2048];
static int references;
static void *object_list_header_data,*object_list_data;
#define object_list_header_get(i) (&headers[(i)])
#define datum_new gate_datum_new
static long gate_datum_new(void *data){int i;if(fail_allocate)return NONE;for(i=0;i<8;i++)if(!used[i]){used[i]=1;return i;}return NONE;}
static void reference_list_add(void *data,long *head,long object){long p=references++;CHECK(p<2048,"fixture references");refs[p].value=object;refs[p].next=*head;*head=p;}
static long object_list_get_first(long list,long *ref){if(list==NONE)return NONE;*ref=headers[list].first_reference_index;if(*ref==NONE)return NONE;long v=refs[*ref].value;*ref=refs[*ref].next;return v;}
static long object_list_get_next(long list,long *ref){if(*ref==NONE)return NONE;long v=refs[*ref].value;*ref=refs[*ref].next;return v;}
static short object_list_count(long list){return list==NONE?0:headers[list].count;}
#include "lists.inc"
static int host=TRUE,waiting=TRUE,ignore_players,teleports,rollbacks,failures,bad_place,crowded,side_effects;
static int player_count;
struct data_iterator {int index;};
struct player_datum {long unit_index;};
static struct player_datum people[MAX_PLAYERS];
static struct {real_point3d position;} objects[MAX_PLAYERS+1];
static void *player_data;
static long data_next_index(void *data,long previous){return previous+1<player_count?previous+1:NONE;}
static struct player_datum *player_get(long i){CHECK(i>=0&&i<player_count,"invalid player");return &people[i];}
static boolean coop_scripts_host(void){return host;}
static long player_index_from_unit_index(long index){int i;for(i=0;i<player_count;i++)if(people[i].unit_index==index)return i;return NONE;}
static real_point3d *object_get_origin(long i,real_point3d *p){*p=objects[i].position;return p;}
static void object_set_position(long i,real_point3d const *p,void *forward,void *up){objects[i].position=*p;rollbacks++;}
static boolean scenario_trigger_volume_test_object(short volume,long i){return objects[i].position.x>10&&objects[i].position.x<20;}
static boolean player_position_switches_bsp(real_point3d const *p){return p->y>50;}
static void biped_fix_ignore_players(boolean value){ignore_players=value;}
static boolean player_teleport(long p,long anchor,real_point3d const *point){
long u=people[p].unit_index;teleports++;CHECK(u!=NONE,"teleport dead player");
if(failures){if(side_effects)objects[u].position.x=25;return FALSE;}
if(crowded&&!ignore_players)return FALSE;
objects[u].position=*point;
if(bad_place==1)objects[u].position.x=25;
if(bad_place==2)objects[u].position.y=100;
return TRUE;
}
static boolean hs_runtime_waiting_on_call(void){return waiting;}
#include "players.inc"
#include "gather.inc"
/* The original volume implementation is compiled too, including ALL of an
empty list; provenance must preserve that behavior for AI lists. */
#define MAXIMUM_TRIGGER_VOLUMES_PER_SCENARIO 256
#define VALID_INDEX(i,n) ((unsigned)(i)<(n))
#define BIT_VECTOR_SET_FLAG(bits,i,value) ((void)0)
#define _error_silent 0
#define error(...) ((void)0)
#include "volume.inc"
#include "gate.inc"
static void reset(int count){int i;memset(used,0,sizeof(used));memset(headers,0,sizeof(headers));memset(objects,0,sizeof(objects));references=0;fail_allocate=0;host=waiting=TRUE;ignore_players=teleports=rollbacks=failures=bad_place=crowded=side_effects=0;player_count=count;for(i=0;i<count;i++){people[i].unit_index=i;objects[i].position.x=i?1:15;}}
static boolean gate(long list){boolean r=hs_trigger_volume_test_objects_all(107,list);CHECK(!ignore_players,"collision override leaked");return r;}
static int all_inside(void){int i;for(i=0;i<player_count;i++)if(people[i].unit_index!=NONE&&!scenario_trigger_volume_test_object(107,people[i].unit_index))return FALSE;return TRUE;}
int main(int argc,char **argv){char const *name=argc>1?argv[1]:"";long list;int n;
CHECK(sizeof(struct object_list_header_datum)==12,"checkpoint ABI changed");
if(!strcmp(name,"crowds")||!strcmp(name,"refused")||!strcmp(name,"outside")||!strcmp(name,"bsp-result")){
for(n=2;n<=128;n*=2){reset(n);list=hs_players();failures=!strcmp(name,"refused");bad_place=!strcmp(name,"outside")?1:!strcmp(name,"bsp-result")?2:0;crowded=!strcmp(name,"crowds");side_effects=TRUE;
CHECK(gate(list)==(!failures&&!bad_place),"unsafe result for %d players",n);
CHECK(all_inside()==(!failures&&!bad_place),"incorrect final positions");
CHECK(teleports<=2*(n-1),"unbounded placement search");
if(failures||bad_place){for(int i=1;i<n;i++)CHECK(objects[i].position.x==1&&objects[i].position.y==0,"bad placement not rolled back");CHECK(rollbacks==2*(n-1),"missing rollback");failures=bad_place=0;CHECK(gate(list)&&all_inside(),"failed gate did not retry");}
}
}else if(!strcmp(name,"one-player")){
reset(1);list=hs_players();CHECK(gate(list)&&!teleports,"one-player host changed");objects[0].position.x=1;CHECK(!gate(list)&&!teleports,"one player outside passed gate");
}else if(!strcmp(name,"empty")){
reset(4);for(n=0;n<4;n++)people[n].unit_index=NONE;
list=hs_players();CHECK(object_list_is_players(list),"empty players lost provenance");CHECK(!gate(list)&&!teleports,"empty players advanced gate");
long ai=object_list_new();CHECK(gate(ai)&&!teleports,"empty AI semantics changed");
waiting=FALSE;CHECK(gate(list),"non-waiting empty ALL semantics changed");
waiting=TRUE;host=FALSE;CHECK(gate(list),"local/client empty ALL semantics changed");
}else if(!strcmp(name,"allocation")){
reset(2);fail_allocate=1;list=hs_players();CHECK(list==NONE&&!gate(list)&&!teleports,"allocation failure opened gate");
}else if(!strcmp(name,"lifetime")){
reset(0);list=hs_players();CHECK(object_list_is_players(list),"mark failed");used[list]=0;
long recycled=object_list_new();CHECK(recycled==list&&!object_list_is_players(recycled),"provenance survived slot reuse");
object_list_mark_players(recycled);object_list_add(recycled,MAX_PLAYERS);CHECK(!object_list_is_players(recycled),"changed list retained provenance");
}else if(!strcmp(name,"contexts")){
reset(2);list=hs_players();waiting=FALSE;CHECK(gate(list)&&!teleports,"if/not test moved players or lost ANY semantics");
waiting=TRUE;host=FALSE;CHECK(!gate(list)&&!teleports,"non-host gathered players");
objects[1].position.x=15;CHECK(gate(list),"non-host ALL changed");
}else if(!strcmp(name,"mixed")){
reset(2);list=hs_players();object_list_add(list,MAX_PLAYERS);CHECK(!gate(list)&&!teleports,"mixed AI/player list gathered");
}else if(!strcmp(name,"no-arrival")){
reset(3);objects[0].position.x=1;list=hs_players();CHECK(!gate(list)&&!teleports,"gathered with nobody inside");
}else if(!strcmp(name,"bsp-anchor")){
reset(3);objects[0].position.y=100;list=hs_players();CHECK(!gate(list)&&!teleports,"used a loading-zone anchor");
objects[1].position.x=15;CHECK(gate(list)&&all_inside(),"did not choose a safe second anchor");
}else return 2;
return 0;
}
+261
View File
@@ -0,0 +1,261 @@
#include "harness.h"
#include <ctype.h>
typedef unsigned short word;
#define NUMBEROF(a) (sizeof(a) / sizeof((a)[0]))
struct player_datum
{
long unit_index;
};
struct data_iterator
{
int slot;
};
static struct player_datum people[128];
static int occupied[128], host = TRUE, calls[128];
static long ref_head;
static struct data_array *player_data, *object_list_data;
static long data_next_index(struct data_array *data, long previous)
{
int i;
for (i = previous + 1; i < 128; i++)
if (occupied[i])
return i;
return NONE;
}
static struct player_datum *player_get(long i)
{
return &people[i];
}
static void data_iterator_new(struct data_iterator *it, struct data_array *data)
{
it->slot = NONE;
}
static struct player_datum *data_iterator_next(struct data_iterator *it)
{
it->slot = data_next_index(player_data, it->slot);
return it->slot == NONE ? NULL : &people[it->slot];
}
static boolean coop_scripts_host(void)
{
return host;
}
static void reference_list_port_full(struct data_array *data)
{
CHECK(FALSE, "fixture full");
}
static void data_delete_all(struct data_array *data)
{
memset(data->data, 0, data->maximum_count * data->size);
data->count = 0;
}
#include "reference.inc"
static long object_list_new(void)
{
ref_head = NONE;
return 0;
}
static void object_list_add(long list, long unit)
{
reference_list_add(object_list_data, &ref_head, unit);
}
static void object_list_mark_players(long list) {}
static void object_list_delete(long list)
{
data_delete_all(object_list_data);
}
#include "followers.inc"
#include "players.inc"
static long player_index_from_unit_index(long unit)
{
int i;
for (i = 0; i < 128; i++)
if (occupied[i] && people[i].unit_index == unit)
return i;
return NONE;
}
static boolean coop_scripts_unit_near(long unit, real_point3d const *p)
{
return TRUE;
}
static boolean player_position_switches_bsp(real_point3d const *p)
{
return FALSE;
}
static real_point3d *object_get_origin(long unit, real_point3d *p)
{
memset(p, 0, sizeof(*p));
return p;
}
static boolean coop_scripts_arrive_at(long player, real_point3d const *p, long const *arrived,
short count)
{
calls[player]++;
return TRUE;
}
static void unit_scripting_suspended(long unit, boolean value)
{
calls[player_index_from_unit_index(unit)]++;
}
static struct
{
struct
{
long parent_object_index;
} object;
} objects[128];
#define object_get(unit) (&objects[player_index_from_unit_index(unit)])
static void unit_scripting_exit_vehicle(long unit)
{
calls[player_index_from_unit_index(unit)]++;
}
static char *strlwr(char *p)
{
char *s = p;
while (*s)
{
*s = tolower((unsigned char)*s);
s++;
}
return p;
}
static void vehicle_scripting_load_magic(long vehicle, char const *seat, long list)
{
struct data_reference *r = datum_get(object_list_data, ref_head);
CHECK(!strcmp(seat, "p-rider"), "seat prefix changed");
calls[player_index_from_unit_index(r->datum_index)]++;
}
static void player_add_equipment(long unit, short profile, boolean reset)
{
calls[player_index_from_unit_index(unit)]++;
}
#include "actions.inc"
static int script_units(long *units)
{
int n = 0;
long ref;
hs_players();
for (ref = ref_head; ref != NONE;)
{
struct data_reference *r = datum_get(object_list_data, ref);
units[n++] = r->datum_index;
ref = r->next_reference_index;
}
object_list_delete(0);
return n;
}
static void verify(void)
{
long named[128], followers[130];
int n = script_units(named), i, j, count;
for (i = 0; i < n; i++)
{
memset(followers, 0x55, sizeof(followers));
count = players_following(named[i], followers, 128);
CHECK(count == (i == 0 && n > 2 ? n - 2 : 0),
"script player %d, %d live units: %d followers", i, n, count);
for (j = 0; j < count; j++)
CHECK(followers[j] == named[j + 2], "follower %d disagrees with hs_players", j);
}
CHECK(!players_following(NONE, followers, 128), "NONE has followers");
CHECK(!players_following(123456, followers, 128), "nonplayer has followers");
}
static void reset(int count)
{
int i;
memset(occupied, 0, sizeof(occupied));
memset(calls, 0, sizeof(calls));
host = TRUE;
for (i = 0; i < count; i++)
{
occupied[i] = 1;
people[i].unit_index = 1000 + 13 * i;
objects[i].object.parent_object_index = 555;
}
data_delete_all(object_list_data);
}
int main(int argc, char **argv)
{
char const *name = argc > 1 ? argv[1] : "";
int n, i;
long units[128], out[128];
real_point3d destination = {1, 2, 3};
object_list_data = game_state_data_new("test refs", 128, sizeof(struct data_reference));
if (!strcmp(name, "counts"))
{
for (n = 0; n <= 128; n++)
{
reset(n);
verify();
}
}
else if (!strcmp(name, "gaps"))
{
reset(128);
for (i = 0; i < 128; i++)
{
if (i % 3 == 0)
occupied[i] = 0;
if (i % 5 == 0)
people[i].unit_index = NONE;
}
verify();
}
else if (!strcmp(name, "respawn"))
{
reset(12);
for (i = 11; i >= 0; i--)
{
people[i].unit_index = NONE;
verify();
people[i].unit_index = 9000 + i;
verify();
occupied[i] = 0;
verify();
}
}
else if (!strcmp(name, "bounded"))
{
reset(12);
n = script_units(units);
for (i = 0; i <= n; i++)
{
out[i] = 123456;
CHECK(players_following(units[0], out, i) == (i < n - 2 ? i : n - 2),
"capacity ignored");
CHECK(out[i] == 123456, "output overrun");
}
}
else if (!strcmp(name, "nonhost"))
{
reset(12);
n = script_units(units);
host = FALSE;
CHECK(!players_following(units[0], out, 128), "nonhost followers");
}
else
{
reset(12);
n = script_units(units);
if (!strcmp(name, "teleport"))
coop_scripts_teleport_followers(units[0], &destination);
else if (!strcmp(name, "suspend"))
coop_scripts_suspend_followers(units[0], TRUE);
else if (!strcmp(name, "exit"))
{
objects[player_index_from_unit_index(units[4])].object.parent_object_index = 556;
coop_scripts_exit_followers(units[0], 555);
}
else if (!strcmp(name, "board"))
coop_scripts_board_followers(units[0], 555, "P-RiderLF");
else if (!strcmp(name, "equipment"))
coop_scripts_player_add_equipment(units[0], 2, TRUE);
else
CHECK(FALSE, "unknown case");
for (i = 0; i < n; i++)
CHECK(calls[player_index_from_unit_index(units[i])] ==
((i >= 2 && (!strcmp(name, "exit") ? i != 4 : TRUE)) ||
(i == 0 && !strcmp(name, "equipment"))),
"wrong action recipient %d", i);
}
return 0;
}
+30
View File
@@ -0,0 +1,30 @@
"""Co-op one-way gates: real player lists, volume tests and placement code."""
import sys
from pathlib import Path
import pytest
sys.path.insert(0,str(Path(__file__).resolve().parents[2]))
from harness import build,read,run,function,structure,constant,mutated,CHECK_FAILED
def generated(fault=None):
lists=read('source/hs/object_lists.c')
code='\n'.join(function(lists,n) for n in ['object_list_new','object_list_mark_players','object_list_is_players','object_list_add'])
players=function(read('source/hs/hs_library_external.c'),'hs_players')
port=read('port/linux/game/coop_scripts.c')
gather='\n'.join(function(port,n) for n in ['coop_scripts_any_player_will_do','coop_scripts_empty_player_list','teleport_to','gather_player_in_volume','coop_scripts_gather_in_volume'])
gate=function(read('source/hs/hs_library_external.c'),'hs_trigger_volume_test_objects_all')
if fault=='unverified':gate=mutated(gate,'inside = hs_trigger_volume_test_objects(trigger_volume_index, object_list_index, TRUE);','inside = TRUE;')
if fault=='empty':players=mutated(players,'object_list_mark_players(object_list_index);',';')
if fault=='recycled':code=mutated(code,'list->flags = 0; /* port: a recycled slot has no previous provenance */',';')
if fault=='rollback':gather=mutated(gather,'object_set_position(unit_index, &original, NULL, NULL);',';')
if fault=='stack':gather=mutated(gather,'attempt != 0','FALSE')
return (('list_header.inc','#define _object_list_players_bit '+str(constant(lists,'_object_list_players_bit'))+'\n'+structure(read('source/hs/object_lists.h'),'object_list_header_datum')),('lists.inc',code),('players.inc',players),('gather.inc',gather),('volume.inc',function(read('source/hs/hs_library_external.c'),'hs_trigger_volume_test_objects')),('gate.inc',gate))
@pytest.mark.parametrize('case',['one-player','crowds','refused','outside','bsp-result','empty','allocation','lifetime','contexts','mixed','no-arrival','bsp-anchor'])
def test_gate(case):
status,output=run(build('coop_gate',generated()),case)
assert status==0,output
@pytest.mark.parametrize('fault,case',[('unverified','refused'),('empty','empty'),('recycled','lifetime'),('rollback','outside'),('stack','crowds')])
def test_negative(fault,case):
status,output=run(build('coop_gate',generated(fault)),case)
assert status==CHECK_FAILED,output
@@ -0,0 +1,39 @@
"""Script player0/followers must agree with the engine's actual players list."""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from harness import build, read, run, function, inline, constant, structure, mutated, CHECK_FAILED
def generated(fault=None):
port = read('port/linux/game/coop_scripts.c')
followers = function(port, 'players_following')
if fault == 'forward':
followers = mutated(followers, 'players[player_count - 1] != unit_index', 'players[0] != unit_index')
if fault == 'named':
followers = mutated(followers, 'player_count - NAMED_PLAYERS - 1', 'player_count - 2')
return (
('reference.inc', structure(read('source/objects/reference_lists.h'), 'data_reference') + '\n' +
inline(read('source/objects/reference_lists.h'), 'reference_list_add')),
('players.inc', function(read('source/hs/hs_library_external.c'), 'hs_players')),
('followers.inc', '#define HALO_PORT_MAXIMUM_NETWORK_PLAYERS ' +
str(constant(read('port/linux/include/halo_port_limits.h'), 'HALO_PORT_MAXIMUM_NETWORK_PLAYERS')) +
'\n#define NAMED_PLAYERS ' + str(constant(port, 'NAMED_PLAYERS')) + '\n' + followers),
('actions.inc', '\n'.join(function(port, name) for name in [
'coop_scripts_teleport_followers', 'coop_scripts_suspend_followers',
'coop_scripts_exit_followers', 'follower_seat_name',
'coop_scripts_board_followers', 'coop_scripts_player_add_equipment'])),
)
@pytest.mark.parametrize('case', ['counts', 'gaps', 'respawn', 'bounded', 'nonhost', 'teleport', 'suspend', 'exit', 'board', 'equipment'])
def test_script_players(case):
status, output = run(build('coop_script_players', generated()), case)
assert status == 0, output
@pytest.mark.parametrize('fault', ['forward', 'named'])
def test_negative(fault):
status, output = run(build('coop_script_players', generated(fault)), 'counts')
assert status == CHECK_FAILED, output
@@ -0,0 +1,58 @@
"""The desktop visibility readback asks the driver for nothing before the swap (a GL error, a fence's state), and at
most one question after it, so that a threaded driver need not catch up with the frame just drawn before it is
shown. Each batch's counts are taken once the fence after their copies has passed; a GPU however late is waited for,
never given up on; a copy that failed is seen by the place it left uncopied, without asking for GL errors."""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from harness import CHECK_FAILED, build, constant, function, mutated, read, run, structure
CASES = ['quiet', 'late', 'lost_copy', 'fence_failed', 'logging']
SOURCE = 'port/linux/src/d3d8_gl.c'
def generated(fault=None):
source = read(SOURCE)
config = '#define VISIBILITY_TEST_SLOTS %d\n#define VISIBILITY_BATCH_COUNT %d\n%s;\n' % (
constant(source, 'VISIBILITY_TEST_SLOTS'), constant(source, 'VISIBILITY_BATCH_COUNT'),
structure(source, 'visibility_batch'))
code = '\n'.join(function(source, name) for name in (
'visibility_readback_gl_ok', 'visibility_fall_back_to_queries', 'visibility_send_batch',
'visibility_read_batch'))
if fault:
code = mutated(code, *fault)
return (('config.inc', config), ('under_test.inc', code))
@pytest.mark.parametrize('case', CASES)
def test_case(case):
status, output = run(build('visibility_readback', generated()), case)
assert status == 0, output
@pytest.mark.parametrize('fault,case', [
# GL errors asked for before every swap
(('if (device.visibility_logging)\n\t\t\tvisibility_readback_gl_ok("before query copies");',
'visibility_readback_gl_ok("before query copies");'), 'quiet'),
# the batch just sent asked about too: two questions a present
(('if (batch->frame == device.frame)\n\t\t\tbreak;', 'if (0)\n\t\t\tbreak;'), 'quiet'),
# a late GPU's batches given up after a while
(('if (status == GL_TIMEOUT_EXPIRED)\n',
'if (status == GL_TIMEOUT_EXPIRED && device.frame - batch->frame < 240)\n'), 'late'),
# a copy that wrote nothing taken for a count
(('if (samples == VISIBILITY_UNCOPIED)', 'if (0)'), 'lost_copy'),
# a fence waited on, counting on the swap to have sent it
(('glClientWaitSync(batch->fence, GL_SYNC_FLUSH_COMMANDS_BIT, 0)', 'glClientWaitSync(batch->fence, 0, 0)'),
'quiet'),
# the buffer read on after a lost copy, whose batch is not used again (with all three lost, no more counts)
(('if (fall_back)\n\t\tvisibility_fall_back_to_queries();', 'if (fall_back)\n\t\tfall_back = FALSE;'),
'lost_copy'),
# a failed fence taken for a passed one
(('if (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED)', 'if (0)'), 'fence_failed'),
])
def test_negative_control(fault, case):
status, output = run(build('visibility_readback', generated(fault)), case)
assert status == CHECK_FAILED, output
+332
View File
@@ -0,0 +1,332 @@
#include "harness.h"
#include <limits.h>
#include <stdarg.h>
#include <stdint.h>
typedef unsigned int GLuint;
typedef int GLint;
typedef unsigned int GLenum;
typedef unsigned int GLbitfield;
typedef unsigned long long GLuint64;
typedef intptr_t GLintptr;
typedef unsigned long long ULONGLONG;
typedef int BOOL;
typedef struct fence *GLsync;
#define GL_NO_ERROR 0
#define GL_INVALID_OPERATION 0x0502
#define GL_QUERY_RESULT 0x8866
#define GL_QUERY_RESULT_AVAILABLE 0x8867
#define GL_QUERY_BUFFER 0x9192
#define GL_QUERY_BUFFER_BINDING 0x9193
#define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
#define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001
#define GL_ALREADY_SIGNALED 0x911A
#define GL_TIMEOUT_EXPIRED 0x911B
#define GL_CONDITION_SATISFIED 0x911C
#define GL_WAIT_FAILED 0x911D
#include "config.inc"
/* (the code before the change has none) */
#ifndef VISIBILITY_UNCOPIED
#define VISIBILITY_UNCOPIED 0xFFFFFFFFUL
#endif
static struct
{
unsigned long frame;
GLuint visibility_known[VISIBILITY_TEST_SLOTS];
struct visibility_batch visibility_batches[VISIBILITY_BATCH_COUNT];
unsigned long visibility_batch;
ULONGLONG visibility_serial;
BOOL visibility_unavailable;
BOOL visibility_logging;
GLuint visibility_buffer;
volatile GLuint *visibility_mapped;
struct
{
unsigned long submitted, completed, samples, nonzero, maximum;
unsigned long unavailable, dropped, failures;
} visibility_stats;
} device;
/* the GPU: each query's count, and the commands it has yet to carry out, in
the order they were sent (gpu_run carries them out) */
struct fence
{
BOOL signaled, broken;
};
enum { COMMAND_COPY, COMMAND_FENCE };
static struct
{
int type;
GLuint query;
GLintptr offset;
struct fence *fence;
} commands[512];
static unsigned long command_count;
static GLuint query_samples[64];
static GLuint results[VISIBILITY_BATCH_COUNT * VISIBILITY_TEST_SLOTS];
static struct fence fences[512];
static unsigned long fence_count;
static BOOL copy_lost, copy_error, fence_broken;
static GLenum gl_error;
/* calls that make a threaded driver catch up with every command before them
(they answer the CPU), all of them and those before the swap; the flushes */
static unsigned long driver_answers, answers_before_swap, flushes, logs;
static BOOL before_swap;
static void answered(void)
{
driver_answers++;
answers_before_swap += before_swap;
}
static GLenum glGetError(void)
{
GLenum error = gl_error;
answered();
gl_error = GL_NO_ERROR;
return error;
}
static void glGetQueryBufferObjectuiv(GLuint query, GLuint buffer, GLenum name, GLintptr offset)
{
CHECK(buffer == device.visibility_buffer && name == GL_QUERY_RESULT, "a copy into another buffer");
commands[command_count].type = COMMAND_COPY;
commands[command_count].query = query;
commands[command_count++].offset = offset;
if (copy_error)
gl_error = GL_INVALID_OPERATION;
}
static GLsync glFenceSync(GLenum condition, GLbitfield flags)
{
struct fence *fence = &fences[fence_count++ % 512];
fence->signaled = FALSE;
fence->broken = fence_broken;
commands[command_count].type = COMMAND_FENCE;
commands[command_count++].fence = fence;
return fence;
}
static GLenum glClientWaitSync(GLsync fence, GLbitfield flags, GLuint64 timeout)
{
/* (a fence not yet sent to the GPU may never pass without a flush) */
CHECK(flags & GL_SYNC_FLUSH_COMMANDS_BIT, "a fence waited on without asking for a flush");
answered();
if (fence->broken)
return GL_WAIT_FAILED;
return fence->signaled ? GL_ALREADY_SIGNALED : GL_TIMEOUT_EXPIRED;
}
static void glDeleteSync(GLsync fence) { }
static void glFlush(void) { flushes++; }
static void glBindBuffer(GLenum target, GLuint buffer) { }
static void glGetIntegerv(GLenum name, GLint *value) { answered(); *value = 0; }
static void glGetQueryObjectuiv(GLuint query, GLenum name, GLuint *value)
{
answered();
*value = name == GL_QUERY_RESULT_AVAILABLE ? 1 : query_samples[query];
}
static void platform_log(const char *format, ...)
{
va_list arguments;
logs++;
va_start(arguments, format);
vfprintf(stderr, format, arguments);
va_end(arguments);
fputc('\n', stderr);
}
#include "under_test.inc"
/* the GPU carries out the first count of its commands (all of them with
ULONG_MAX): a copy writes its query's count (none when copy_lost), a fence
passes */
static void gpu_run(unsigned long count)
{
unsigned long i;
if (count > command_count)
count = command_count;
for (i = 0; i < count; i++)
{
if (commands[i].type == COMMAND_COPY && !copy_lost)
results[commands[i].offset / sizeof(GLuint)] = query_samples[commands[i].query];
else if (commands[i].type == COMMAND_FENCE)
commands[i].fence->signaled = TRUE;
}
for (i = count; i < command_count; i++)
commands[i - count] = commands[i];
command_count -= count;
}
/* a present: the batch sent before the swap, the counts read after it */
static void present(unsigned long index)
{
device.frame = index;
before_swap = TRUE;
visibility_send_batch();
before_swap = FALSE;
visibility_read_batch();
}
/* a frame's two tests in the batch being filled (slots 10 and 11, counts by
frame), then its present */
static struct visibility_batch *frame(unsigned long index)
{
struct visibility_batch *batch = &device.visibility_batches[device.visibility_batch];
CHECK(device.visibility_batch < VISIBILITY_BATCH_COUNT, "no batch free at frame %lu", index);
batch->queries[0] = (GLuint)(1 + (index * 2) % 62);
batch->queries[1] = (GLuint)(2 + (index * 2) % 62);
query_samples[batch->queries[0]] = (GLuint)(100 + index);
query_samples[batch->queries[1]] = (GLuint)(200 + index);
batch->slots[0] = 10;
batch->slots[1] = 11;
batch->areas[0] = batch->areas[1] = 1.0f;
batch->count = 2;
present(index);
return batch;
}
static void start(BOOL logging)
{
device.visibility_buffer = 7;
device.visibility_mapped = results;
device.visibility_logging = logging;
device.visibility_known[10] = 77;
device.visibility_known[11] = 88;
}
static void known(GLuint first, GLuint second, const char *what)
{
CHECK(device.visibility_known[10] == first && device.visibility_known[11] == second,
"%s: the slots hold %u and %u, not %u and %u", what, device.visibility_known[10], device.visibility_known[11],
first, second);
}
/* frames whose commands the GPU carries out before the next present, which
reads their counts */
static void play(unsigned long frames)
{
unsigned long index;
for (index = 0; index < frames; index++)
{
frame(index);
if (index)
known((GLuint)(100 + index - 1), (GLuint)(200 + index - 1), "a frame on");
gpu_run(ULONG_MAX);
}
CHECK(device.visibility_stats.completed == frames - 1, "%lu batches read, not %lu",
device.visibility_stats.completed, frames - 1);
}
int main(int argc, char **argv)
{
const char *case_name = argv[1];
CASE("quiet")
{
/* without debug.gl_debug, nothing before the swap asks the driver
for an answer, no present flushes (the swap does), and after the
swap at most one question a present */
start(FALSE);
play(12);
CHECK(answers_before_swap == 0, "the driver was asked %lu times before the swap in 12 presents",
answers_before_swap);
CHECK(flushes == 0, "%lu flushes in 12 presents", flushes);
CHECK(driver_answers <= 12, "the driver was asked %lu times in 12 presents", driver_answers);
}
CASE("late")
{
/* a GPU 1000 presents behind: its counts are not taken before their
fence passes, no batch is given up, and the counts are read once
they come (visibility goes on updating) */
unsigned long index;
start(FALSE);
frame(0);
frame(1);
frame(2);
for (index = 3; index < 1003; index++)
present(index);
known(77, 88, "nothing carried out");
for (index = 0; index < VISIBILITY_BATCH_COUNT; index++)
CHECK(!device.visibility_batches[index].failed, "batch %lu given up while the GPU was late", index);
gpu_run(2);
present(1003);
known(77, 88, "copies done, fence not passed");
gpu_run(ULONG_MAX);
present(1004);
known(102, 202, "all carried out");
frame(1005);
gpu_run(ULONG_MAX);
present(1006);
known(1105, 1205, "the next batch after a late GPU");
}
CASE("lost_copy")
{
/* a copy that wrote nothing (a failed one) is seen without asking
for GL errors: its place still holds VISIBILITY_UNCOPIED once the
fence has passed. Its batch is dropped, the slots keep their earlier
counts, and the counts are read from each query from then on: the
batch sent meanwhile, and those after it. */
start(FALSE);
frame(0);
copy_lost = TRUE;
gpu_run(ULONG_MAX);
copy_lost = FALSE;
frame(1);
CHECK(device.visibility_batches[0].failed, "a batch whose counts were never copied was kept");
known(77, 88, "after a lost copy");
CHECK(logs > 0 && device.visibility_stats.failures, "a lost copy not reported");
CHECK(driver_answers <= 1, "GL errors asked for without debug.gl_debug (%lu answers)", driver_answers);
CHECK(!device.visibility_mapped, "the buffer still read after its copies failed");
gpu_run(ULONG_MAX);
present(2);
known(101, 201, "the batch sent before the lost copy was seen");
frame(3);
present(4);
known(103, 203, "a batch sent after the lost copy");
}
CASE("fence_failed")
{
/* a fence that cannot be waited on drops its batch, keeping the
slots' earlier counts; the counts are read from each query from
then on */
start(FALSE);
fence_broken = TRUE;
frame(0);
fence_broken = FALSE;
gpu_run(ULONG_MAX);
frame(1);
CHECK(device.visibility_batches[0].failed, "a batch whose fence failed was read");
known(77, 88, "after a failed fence");
CHECK(!device.visibility_mapped, "the buffer still read after a fence failed");
present(2);
known(101, 201, "the batch after a failed fence");
}
CASE("logging")
{
/* with debug.gl_debug, GL errors are asked for, and a copy that
failed drops its batch as it is sent */
start(TRUE);
play(6);
CHECK(driver_answers > 6, "GL errors never asked for with debug.gl_debug");
copy_error = TRUE;
CHECK(frame(6)->failed, "a batch whose copies failed was kept");
CHECK(logs > 0 && device.visibility_stats.failures, "a failed copy not reported");
CHECK(!device.visibility_mapped, "the buffer still read after its copies failed");
copy_error = FALSE;
frame(7);
present(8);
known(107, 207, "a batch after the failed copies");
}
return 0;
}