mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-23 00:00:32 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e9de9a11c | ||
|
|
ce95f7deed | ||
|
|
a20ced034c | ||
|
|
fc22e8467d | ||
|
|
038252b1ae | ||
|
|
61621e6513 | ||
|
|
1d79584ea4 | ||
|
|
577f718a63 | ||
|
|
b68d259c81 | ||
|
|
600edf7bb9 | ||
|
|
a0f22cbba7 | ||
|
|
9f04a51259 | ||
|
|
47981601da | ||
|
|
a4e4bd8317 | ||
|
|
9044950d78 | ||
|
|
9d7436bd64 | ||
|
|
cdce33647a |
No files matched your search
@@ -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 |
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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++;
|
||||
|
||||
|
||||
@@ -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
@@ -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])
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user