diff --git a/docs/building.md b/docs/building.md
index 9f9d4ebd..36524f67 100644
--- a/docs/building.md
+++ b/docs/building.md
@@ -486,6 +486,13 @@ A change to which frames FSR 3.1 frame generation is told are camera cuts (`runt
frame id gets its own frame's cut, that an id never noted or no longer held counts as a cut, and that a slot rewritten
on another thread never answers an old id with a newer frame's value. No GPU, no game files.
+A change to which frames the flight recorder takes for a one-frame flash (`runtime/flight_flash.h`, `runtime/flight.c`
+`flight_cells`) passes `python tests\flight_flash_check.py`, which builds the header alone, draws pictures of the
+thumbnails' size and checks that a patch or a whole frame shown once is a flash, that a camera cut, a fade, a pan and
+shapes crossing the picture at any speed are none, where each of the test's rules decides, and that a frame beside one
+without a thumbnail is not judged. With `time` it prints what the cell means of one thumbnail cost. No GPU, no game
+files.
+
The lifter (`recomp/`) and the CPU headers (`runtime/cpu.h`, `runtime/simd.h`) are checked against Unicorn. Every lifter
change passes these, all run with `.venv`:
diff --git a/nx2navi.ini.example b/nx2navi.ini.example
index cbcfa12a..6a1335f8 100644
--- a/nx2navi.ini.example
+++ b/nx2navi.ini.example
@@ -858,6 +858,15 @@
# takes longer than flight_spike_ms (0 to 60000).
# flight = 0
# flight_spike_ms = 100
+# The flight recorder also saves them after a one-frame flash: a frame whose picture differs from the frame before and
+# the frame after while those two are alike (0: not). The three pictures are compared as 16x9 cells of mean
+# brightness: at least flight_flash_cells cells (1 to 144) must be more than flight_flash_diff (4 to 255, of 255)
+# brighter or darker than in both other frames. Lower values catch smaller and fainter flashes and take more of the
+# game's own effects for one. At most one such save every 30 seconds; a flash the game means (lightning) counts
+# too, and one while the camera turns fast does not. It needs the recorder's thumbnails. Read at start.
+# flight_flash = 1
+# flight_flash_diff = 24
+# flight_flash_cells = 3
# GPU pass marks: after a GPU crash (device lost) the log names the render pass or dispatch the GPU had started and the
# last one it finished. Unset: on AMD GPUs (VK_AMD_buffer_marker). 1: also on NVIDIA GPUs
# (VK_NV_device_diagnostic_checkpoints), which NVIDIA says costs a little. 0: off.
diff --git a/runtime/cfg.c b/runtime/cfg.c
index df6e0a50..5204429b 100644
--- a/runtime/cfg.c
+++ b/runtime/cfg.c
@@ -46,7 +46,7 @@ static const char *const known[] = {
"COLOR_PRESET", "COLOR_SATURATION", "CONTACT_DEBUG", "CONTACT_SHADOWS", "CORE_DIAG", "CRIT", "CRIT_OUT",
"DESC_TEMPLATE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_CAM_KEEP", "DLAA_CUT", "DLSS_IDLE_FREE", "DLAA_EDGE_FILL", "DLAA_AT_UPSCALER", "FFX_DEBUG", "FFX_KEYS", "FSR3_SHARPEN", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TARGET_KEEP", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_REACTIVE", "DUMP", "DUMP_BUF", "DUMP_DIR",
"DUMP_LOG", "DUMP_RAW", "DUMP_SHADERS", "DUMP_FILE", "DS3", "LOG_CS_BINDS", "LOG_TEX_FLAGS", "LOG_MESH", "MESH", "POOL", "NATIVE_TENSOR", "LOG_RCOND", "LOG_RT_SIZES", "DUMP_WHITE", "DUMP_AFTER", "DUMP_MOVIE", "SUBMIT_FINISH", "ACQUIRE_WAIT", "CRASH_FILTER_GUARD", "CS_FALLBACK", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES", "GPU_TIMELINE",
- "FFX_PIPE_CACHE", "FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLIGHT_EVERY", "FLUSH_SPAN", "FLUSH_UPLOAD", "FOG_CULL", "FOLIAGE_SHADOW_CULL", "FOLIAGE_SHADOW_VRS", "FOV", "FOV_PATCH", "FG_HUDLESS_SHOW", "FG_MULT", "FPS60", "FPS_LIMIT", "FRAMEGEN", "FRAMEGEN_MULTIPLIER", "FREEZE_IDLE", "FREEZE_ASYNC", "FREEZE_BUDGET_US", "FREE_CORES",
+ "FFX_PIPE_CACHE", "FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLIGHT_EVERY", "FLIGHT_FLASH", "FLIGHT_FLASH_DIFF", "FLIGHT_FLASH_CELLS", "FLUSH_SPAN", "FLUSH_UPLOAD", "FOG_CULL", "FOLIAGE_SHADOW_CULL", "FOLIAGE_SHADOW_VRS", "FOV", "FOV_PATCH", "FG_HUDLESS_SHOW", "FG_MULT", "FPS60", "FPS_LIMIT", "FRAMEGEN", "FRAMEGEN_MULTIPLIER", "FREEZE_IDLE", "FREEZE_ASYNC", "FREEZE_BUDGET_US", "FREE_CORES",
"FULLSCREEN", "FXAA", "GAME_ASPECT", "GAME_RES", "GPA", "GPA_EMPTY", "GPA_TWIN", "GPA_TWIN_MAX_KB", "MIRROR_CAPTURE_KB", "WB_SKIP_MB", "GPL", "GPL_DRAW_BUILD", "GPL_FEEDBACK", "PIPE_AHEAD", "PIPE_FAST_WORKERS", "PIPE_FRAME_WAIT_MS", "PIPE_LINK_DRAW_MS", "PIPE_SKIP_STEADY", "DRAW_WRITERS_WAIT", "PROG_LEAD", "GPU", "GPU_INDEX", "GPU_PASSES", "GPU_NAME", "HDR",
"HDR_GAME_NITS", "HDR_LIVE", "HEAP_LAZY", "HDR_PEAK_NITS", "HDR_UI_NITS",
// retired: the HDR look's settings of earlier versions, now fixed (vk_hdr.c, shaders/hdr_tonemap.frag); read by
@@ -152,6 +152,7 @@ static const struct { const char *key, *section; } sections[] = {
{"VK_DYN_VERTEX", "performance"}, {"VK_DYN_A2C", "performance"}, {"VK_SUBGROUP_PIN", "performance"}, {"VK_RECONVERGE", "performance"},
{"VK_NAN_PRESERVE", "performance"}, {"VK_NAN_BITS", "performance"},
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"PASS_CRUMBS", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
+ {"FLIGHT_FLASH", "diagnostics"}, {"FLIGHT_FLASH_DIFF", "diagnostics"}, {"FLIGHT_FLASH_CELLS", "diagnostics"},
{"WB_FENCE_LIMIT", "diagnostics"}, {"WB_MERGE", "diagnostics"}, {"WB_EXACT", "diagnostics"}, {"MEM_CENSUS", "diagnostics"}, {"WB_NO_REUP", "diagnostics"}, {"WB_PROBE", "diagnostics"}, {"WB_SPLIT", "performance"}, {"SHADOW_BULK_WRITE", "performance"}, {"WB_LAZY", "performance"}, {"HEAP_LAZY", "performance"}, {"BARRIER_GAME", "performance"}, {"MESH_AHEAD", "performance"}, {"MESH_FALLBACK", "performance"}, {"MESH_FALLBACK_MB", "performance"}, {"MESH_FALLBACK_WORKERS", "performance"}, {"WB_LAZY_TOUCH", "diagnostics"}, {"WB_LAZY_CHUNK", "performance"}, {"FLUSH_UPLOAD", "diagnostics"}, {"GPA_TWIN_MAX_KB", "diagnostics"}, {"MIRROR_CAPTURE_KB", "diagnostics"}, {"WB_SKIP_MB", "diagnostics"},
{"WB_MERGE_MAX", "diagnostics"}, {"MV_SURVEY", "diagnostics"}, {"TEXUP_LOG_MS", "diagnostics"},
{"WAIT_WATCH_S", "diagnostics"},
@@ -354,6 +355,8 @@ static const CfgMeta metas[] = {
{"VK_NOOPT", "0", "0|1|mesh", RESTART}, // vk_pipeline.c noopt_flag_for
// diagnostics
{"TRACE", "", NULL, RESTART}, {"FLIGHT", "0", "0|1", RESTART}, {"FLIGHT_SPIKE_MS", "100", "0..60000", RESTART},
+ // flight.c "The flash trigger": on or off, and flight_flash.h flash_test's two thresholds
+ {"FLIGHT_FLASH", "1", "0|1", RESTART}, {"FLIGHT_FLASH_DIFF", "24", "4..255", RESTART}, {"FLIGHT_FLASH_CELLS", "3", "1..144", RESTART},
{"WB_FENCE_LIMIT", "65536", "0..1073741824", RESTART}, {"WB_MERGE", "1", "0|1", RESTART}, {"WB_EXACT", "1", "0|1", RESTART}, {"MEM_CENSUS", "0", "0|1", RESTART}, {"WB_NO_REUP", "1", "0|1", RESTART}, {"WB_PROBE", "0", "0|1", RESTART}, {"WB_SPLIT", "1", "0|1|2|3", RESTART}, {"WB_LAZY", WB_LAZY_DEFAULT, "0|1", RESTART}, {"HEAP_LAZY", "0", "0|1", RESTART}, {"WB_LAZY_TOUCH", "0", "0..16", RESTART}, {"WB_LAZY_CHUNK", "0", "0|1", RESTART}, {"FLUSH_UPLOAD", "0", "0|1", RESTART}, {"GPA_TWIN_MAX_KB", "0", "0..1048576", RESTART},
{"MIRROR_CAPTURE_KB", "0", "0..1048576", RESTART}, {"WB_SKIP_MB", "0", "0..4096", RESTART}, // vk_memory.c capture_cap, wb_skip
{"WB_MERGE_MAX", "32", "1..256", RESTART}, {"TEXUP_LOG_MS", "0", "0..60000", RESTART}, // vk_texture.c vk_texstat_frame
diff --git a/runtime/flight.c b/runtime/flight.c
index aaaffb79..5f6a0f14 100644
--- a/runtime/flight.c
+++ b/runtime/flight.c
@@ -4,7 +4,8 @@
// frames, flight_tex), f.png per thumbnail and frames.tsv, written in that order by a background thread. Triggers: F8 while the game window has focus, a frame interval over
// NX2NAVI_FLIGHT_SPIKE_MS (100; 0 off; at most one dump per 10 s), NX2NAVI_FLIGHT_AT=[,...] and
// NX2NAVI_FLIGHT_EVERY= (every n-th frame: with n under the ring's length the dumps of a run hold every frame of it
-// but the last n). A dump holds the frames up to FLIGHT_POST presents after the trigger, so the aftermath is in it
+// but the last n), and a one-frame flash (NX2NAVI_FLIGHT_FLASH, 1; at most one dump per 30 s; "The flash trigger"
+// below). A dump holds the frames up to FLIGHT_POST presents after the trigger, so the aftermath is in it
// too. tools/flight_view.py reads it.
//
// Records come from perf_present (prof_win.c) on the render thread; the counts are differences of the NX2NAVI_TRACE=perf
@@ -15,6 +16,7 @@
#include "cfg.h"
#include "flight.h"
+#include "flight_flash.h"
#include "gfx.h"
#include "host.h"
#include "input.h"
@@ -24,6 +26,7 @@
#define FLIGHT_POST 30 // presents recorded after the trigger before the dump starts
#define FLIGHT_SPIKE_GAP_NS 10000000000ull
+#define FLIGHT_FLASH_GAP_NS 30000000000ull // a flash dump at most this often
// Record columns after frame, time and interval: blocked time per PERF_* kind (us), then the counts.
enum { C_SUBMITS, C_PASSES, C_DRAWS, C_DRAWS_SEEN, C_CLEARS, C_COPIES, C_DISPATCHES, C_XLATED, C_PERF0, C_N = C_PERF0 + PERF_CNT_N };
@@ -77,6 +80,25 @@ static volatile uint64_t cost_ns, cost_max_ns; // render-thread time in flight
static TexEv *tex_ring;
static volatile uint64_t tex_n; // texture events written so far
+// The flash trigger: a frame whose picture differs strongly from the frame before and the frame after while those two
+// agree, which is what a one-frame flicker looks like (flight_flash.h has the test and what it leaves out). The
+// present thread hands in each thumbnail's cell means as the thumbnail completes (gfx_present.c thumb_cells), a few
+// presents after its frame, and flight_cells judges the frame before it there, once both its neighbours are known: a
+// frame beside one without a thumbnail is not judged. A flash found is left in flash_found for the render thread, whose
+// flight_frame starts the dump: named for the flash's frame, some presents back by then, and no more often than
+// FLIGHT_FLASH_GAP_NS. Only with thumbnails kept; NX2NAVI_FLIGHT_FLASH=0 turns it off, NX2NAVI_FLIGHT_FLASH_DIFF and
+// NX2NAVI_FLIGHT_FLASH_CELLS are the test's two thresholds. Their defaults (24 of 255, 3 cells) were tried on the
+// thumbnails of earlier dumps, 48000 frames of scripted runs. In the runs of builds that showed the scene white for
+// single frames they flagged about 980 frames; the ones looked at were such white frames (68 to 133 cells each) and
+// one frame with other lighting than its neighbours (9 cells). In the 16000 frames of the runs without that fault
+// they flagged nothing, and neither did a threshold of 10. Play with more effects on screen than those runs had is
+// not measured.
+int flight_flash_on;
+static int flash_diff, flash_min_cells;
+static FlashTrack flash_track; // the present thread's
+static volatile uint64_t flash_found; // (frame << 8 | cells counted) of the newest flash, 0 once flight_frame took it
+static uint64_t last_flash_ns, flash_n; // the render thread's: the last flash dump's time, flashes found so far
+
static unsigned env_u(const char *name, unsigned def) {
const char *e = cfg_get(name);
return e && *e ? (unsigned)strtoul(e, NULL, 10) : def;
@@ -108,6 +130,13 @@ void flight_init(void) {
if (!plat_full_path(tmp, base_dir, sizeof base_dir)) snprintf(base_dir, sizeof base_dir, "%s", tmp);
thumbs_on = env_u("NX2NAVI_FLIGHT_THUMBS", 1) != 0;
if (thumbs_on) vk_thumbs_enable(ring_n);
+ if (thumbs_on && cfg_bool("NX2NAVI_FLIGHT_FLASH")) {
+ flash_diff = cfg_int("NX2NAVI_FLIGHT_FLASH_DIFF");
+ flash_diff = flash_diff < 4 ? 4 : flash_diff > 255 ? 255 : flash_diff;
+ flash_min_cells = cfg_int("NX2NAVI_FLIGHT_FLASH_CELLS");
+ flash_min_cells = flash_min_cells < 1 ? 1 : flash_min_cells > FLASH_CELLS ? FLASH_CELLS : flash_min_cells;
+ flight_flash_on = 1;
+ }
if (!(tex_ring = calloc(TEX_RING, sizeof *tex_ring))) rt_log("flight: no memory for the texture events, none kept");
flight_on = 1;
rt_log("flight: on: last %d frames, %s; dump on F8, on a frame over %llu ms (one per 10 s)%s, to %s", ring_n,
@@ -117,6 +146,18 @@ void flight_init(void) {
rt_log("flight: memory: records %llu KB, thumbnail ring %llu MB (host-visible)",
(unsigned long long)((size_t)ring_n * sizeof *ring >> 10),
(unsigned long long)((uint64_t)ring_n * FLIGHT_THUMB_W * FLIGHT_THUMB_H * 4 >> 20));
+ if (flight_flash_on)
+ rt_log("flight: dump on a one-frame flash too (one per 30 s): at least %d of %d cells of the picture more than "
+ "%d of 255 brighter or darker than in the frames before and after, which agree (NX2NAVI_FLIGHT_FLASH=0: "
+ "off)", flash_min_cells, FLASH_CELLS, flash_diff);
+ else
+ rt_log("flight: no dump on a one-frame flash (%s)", thumbs_on ? "NX2NAVI_FLIGHT_FLASH=0" : "it needs the thumbnails");
+}
+
+void flight_cells(uint64_t frame, const uint8_t *cells) {
+ int n = flash_note(&flash_track, frame, cells, flash_diff, flash_min_cells);
+ // the frame judged is the one before; a flash not yet taken gives way to the newer one
+ if (n) __atomic_store_n(&flash_found, (frame - 1) << 8 | (uint64_t)n, __ATOMIC_RELEASE);
}
void flight_tex(int kind, uint64_t addr, uint32_t format, uint32_t w, uint32_t h, uint32_t layers, uint32_t flags,
@@ -435,6 +476,17 @@ static void trigger(uint64_t frame, uint64_t interval_ns, const char *why) {
dump_at = frame + (FLIGHT_POST < ring_n / 4 ? FLIGHT_POST : (uint64_t)ring_n / 4);
}
+// The interval recorded for `frame` in ns, 0 when the ring no longer holds it; the newest records are looked at first.
+static uint64_t interval_of(uint64_t frame) {
+ uint64_t n = recorded < (uint64_t)ring_n ? recorded : (uint64_t)ring_n;
+ for (uint64_t k = 1; k <= n; k++) {
+ const Rec *r = &ring[(recorded - k) % (uint64_t)ring_n];
+ if (r->frame == frame) return (uint64_t)r->interval_us * 1000;
+ if (r->frame < frame) break;
+ }
+ return 0;
+}
+
// ---------------------------------------------------------------- per frame (render thread)
void flight_frame(uint64_t frame, uint64_t now_ns, uint64_t interval_ns, const uint64_t blocked_ns[],
const volatile int64_t counts[]) {
@@ -479,6 +531,21 @@ void flight_frame(uint64_t frame, uint64_t now_ns, uint64_t interval_ns, const u
if (!dump_at) last_spike_ns = now_ns;
trigger(frame, interval_ns, "spike");
}
+ // A flash flight_cells found since the last present. Its frame is a few presents back (the thumbnails complete
+ // late): the dump carries that frame's number and interval, and like any other ends FLIGHT_POST presents after it.
+ uint64_t fl = flight_flash_on ? __atomic_exchange_n(&flash_found, 0, __ATOMIC_ACQ_REL) : 0;
+ if (fl) {
+ flash_n++;
+ if (!last_flash_ns || now_ns - last_flash_ns >= FLIGHT_FLASH_GAP_NS) {
+ if (!dump_at) { // else a dump is pending already, and its frames hold this one
+ last_flash_ns = now_ns;
+ rt_log("flight: frame %llu looks like a one-frame flash: %u of %d cells unlike the frames before and "
+ "after, which agree (flash %llu of this run)", (unsigned long long)(fl >> 8), (unsigned)(fl & 255),
+ FLASH_CELLS, (unsigned long long)flash_n);
+ }
+ trigger(fl >> 8, interval_of(fl >> 8), "flash");
+ }
+ }
if (dump_at && frame >= dump_at) {
start_dump();
dump_at = 0;
diff --git a/runtime/flight.h b/runtime/flight.h
index 173c529f..1bbf37fd 100644
--- a/runtime/flight.h
+++ b/runtime/flight.h
@@ -1,5 +1,6 @@
// NX2NAVI_FLIGHT=1 flight recorder (flight.c): the last frames' timing and event counts plus a thumbnail each, written to
-// disk on F8 or after a slow frame, so a glitch or stutter seen in play can be read after the fact.
+// disk on F8, after a slow frame or after a one-frame flash, so a glitch or stutter seen in play can be read after the
+// fact.
#pragma once
#include
@@ -51,3 +52,9 @@ void vk_thumbs_frame(uint64_t frame); // the present thread names the frame o
// Copies frame's thumbnail as RGBA when the ring still holds it complete; 0 when it was never written, is still in
// flight or was overwritten. Takes the present lock for one slot copy (about 20 us); any thread.
int vk_thumbs_read(uint64_t frame, uint8_t *rgba);
+
+// The flash trigger (flight_flash.h): while flight_flash_on (set once by flight_init), gfx_present.c hands in the cell
+// means (flash_cells, FLASH_CELLS bytes) of each thumbnail when it completes, on the present thread, in frame order.
+// flight_cells judges the frame before and leaves a flash for flight_frame to dump.
+extern int flight_flash_on;
+void flight_cells(uint64_t frame, const uint8_t *cells);
diff --git a/runtime/flight_flash.h b/runtime/flight_flash.h
new file mode 100644
index 00000000..b29cc576
--- /dev/null
+++ b/runtime/flight_flash.h
@@ -0,0 +1,107 @@
+// The flight recorder's flash trigger (flight.c flight_cells): whether a frame's picture is a one-frame flash, judged
+// from the thumbnails of three frames in a row. A flash is a frame that differs strongly from the frame before and
+// from the frame after while those two agree with each other: a texture that was blank for one frame, a draw that went
+// missing, a picture from somewhere else shown once. Own header so tests/flight_flash_check.c checks it without a GPU.
+//
+// The pictures are compared as FLASH_CELLS_X x FLASH_CELLS_Y cells of mean brightness (flash_cells), which noise
+// hardly changes and a moving camera changes slowly. A cell counts for the flash when
+// - the frame's mean is brighter than both neighbours' or darker than both, by more than `diff` (of 255) each, and
+// - the two neighbours are close to each other in that cell and in the eight cells around it: no further apart
+// than a quarter of what the frame differs by.
+// The frame is a flash when at least `min_cells` cells count, and the neighbours agree over the picture as a whole:
+// in no cell are they further apart than `diff`, and in at most FLASH_MOVING cells further than a quarter of it.
+// What that leaves out:
+// - a camera cut: the frame agrees with the neighbour on its side of the cut;
+// - a fade: the frame lies between its neighbours;
+// - steady motion: a shape that crosses a cell makes that cell's mean rise and fall, and in the frame at the top
+// the cell is unlike both neighbours, which are alike. But what left the cell went into the next one, so the
+// neighbours differ there by as much: the cells around are what tells a shape passing from a flash in place.
+// A shape that jumps further than a cell a frame leaves its marks further away, where the frame before and
+// the frame after differ by as much as a flash does: such a frame is not judged;
+// - fast motion and anything else that changes much of the picture between the neighbours: not judged.
+// It cannot tell a fault from a flash the game means (lightning, the white frame of a hit), and a flash while the
+// camera turns fast goes unseen: the F8 dump is for those.
+#pragma once
+#include
+#include
+
+#define FLASH_CELLS_X 16
+#define FLASH_CELLS_Y 9
+#define FLASH_CELLS (FLASH_CELLS_X * FLASH_CELLS_Y)
+#define FLASH_MOVING (FLASH_CELLS / 4) // more cells than this in which the neighbours disagree: the picture moves
+#define FLASH_STEP 2 // flash_cells reads every FLASH_STEP-th pixel of every FLASH_STEP-th row
+
+// The cell means of a picture of w x h pixels, four bytes a pixel with red or blue first (the weights are the same
+// for both, so the thumbnail's byte order does not matter): (first + 2 * second + third) / 4 over the pixels read.
+// A 640x360 thumbnail has cells of 40x40 pixels, of which this reads 400 each: 57600 pixels a picture.
+static inline void flash_cells(const uint8_t *px, int w, int h, uint8_t out[FLASH_CELLS]) {
+ for (int cy = 0; cy < FLASH_CELLS_Y; cy++) {
+ int y0 = cy * h / FLASH_CELLS_Y, y1 = (cy + 1) * h / FLASH_CELLS_Y;
+ uint32_t sum[FLASH_CELLS_X] = {0}, n[FLASH_CELLS_X] = {0};
+ for (int y = y0; y < y1; y += FLASH_STEP) {
+ const uint8_t *row = px + (size_t)y * (size_t)w * 4;
+ for (int cx = 0; cx < FLASH_CELLS_X; cx++) {
+ int x0 = cx * w / FLASH_CELLS_X, x1 = (cx + 1) * w / FLASH_CELLS_X;
+ uint32_t s = 0;
+ for (int x = x0; x < x1; x += FLASH_STEP) s += row[4 * x] + 2u * row[4 * x + 1] + row[4 * x + 2];
+ sum[cx] += s;
+ n[cx] += (uint32_t)(x1 - x0 + FLASH_STEP - 1) / FLASH_STEP;
+ }
+ }
+ for (int cx = 0; cx < FLASH_CELLS_X; cx++)
+ out[cy * FLASH_CELLS_X + cx] = n[cx] ? (uint8_t)(sum[cx] / (4 * n[cx])) : 0;
+ }
+}
+
+// The test over the cell means of three frames in a row: the number of cells that count for a flash of `cur` when it
+// is one (at least min_cells), else 0.
+static inline int flash_test(const uint8_t prev[FLASH_CELLS], const uint8_t cur[FLASH_CELLS],
+ const uint8_t next[FLASH_CELLS], int diff, int min_cells) {
+ uint8_t apart[FLASH_CELLS]; // how far apart the two neighbours are in each cell
+ int moving = 0, agree = diff / 4;
+ for (int i = 0; i < FLASH_CELLS; i++) {
+ int d = (int)prev[i] - (int)next[i];
+ apart[i] = (uint8_t)(d < 0 ? -d : d);
+ moving += apart[i] > agree;
+ if (apart[i] > diff) return 0; // something crossed this cell between the two neighbours
+ }
+ if (moving > FLASH_MOVING) return 0;
+ int n = 0;
+ for (int y = 0; y < FLASH_CELLS_Y; y++)
+ for (int x = 0; x < FLASH_CELLS_X; x++) {
+ int i = y * FLASH_CELLS_X + x, a = (int)cur[i] - (int)prev[i], b = (int)cur[i] - (int)next[i];
+ if ((a > 0) != (b > 0)) continue; // between its neighbours
+ if (a < 0) a = -a, b = -b;
+ int by = a < b ? a : b; // what the frame differs by from the nearer neighbour
+ if (by <= diff) continue;
+ int around = 0; // the neighbours' largest distance in this cell and those around it
+ for (int v = y > 0 ? y - 1 : 0; v <= y + 1 && v < FLASH_CELLS_Y; v++)
+ for (int u = x > 0 ? x - 1 : 0; u <= x + 1 && u < FLASH_CELLS_X; u++)
+ if (apart[v * FLASH_CELLS_X + u] > around) around = apart[v * FLASH_CELLS_X + u];
+ n += around * 4 <= by;
+ }
+ return n >= min_cells ? n : 0;
+}
+
+// The cell means of the last thumbnails, by frame. A thumbnail completes a few presents after its frame and some
+// frames have none (a present that was skipped), so each slot names the frame it holds; 0 is an empty slot (frames
+// with a thumbnail start at 1).
+#define FLASH_KEEP 4 // the frame judged and its two neighbours fall into three different slots
+typedef struct {
+ uint64_t frame[FLASH_KEEP];
+ uint8_t cells[FLASH_KEEP][FLASH_CELLS];
+} FlashTrack;
+
+// Takes the cell means of `frame`'s thumbnail and judges the frame before it, the newest whose two neighbours can
+// both be known. Returns flash_test's count when frame - 1 is a flash; 0 when it is none, and when it or frame - 2
+// has no thumbnail here: a frame without both neighbours is not judged.
+static inline int flash_note(FlashTrack *t, uint64_t frame, const uint8_t cells[FLASH_CELLS], int diff, int min_cells) {
+ if (!frame) return 0;
+ unsigned s = (unsigned)(frame % FLASH_KEEP);
+ t->frame[s] = frame;
+ memcpy(t->cells[s], cells, FLASH_CELLS);
+ if (frame < 3) return 0;
+ unsigned c = (unsigned)((frame - 1) % FLASH_KEEP), p = (unsigned)((frame - 2) % FLASH_KEEP);
+ if (t->frame[c] != frame - 1 || t->frame[p] != frame - 2) return 0;
+ return flash_test(t->cells[p], t->cells[c], t->cells[s], diff, min_cells);
+}
diff --git a/runtime/gfx_present.c b/runtime/gfx_present.c
index 86aa44b8..06344f7f 100644
--- a/runtime/gfx_present.c
+++ b/runtime/gfx_present.c
@@ -19,6 +19,7 @@
#include "cfg.h"
#include "coop.h"
#include "flight.h"
+#include "flight_flash.h"
#include "fs.h"
#include "gfx.h"
#include "gfx_wsys.h"
@@ -657,6 +658,18 @@ int vk_thumbs_read(uint64_t frame, uint8_t *rgba) {
return ok != 0;
}
+// The flight recorder's flash trigger (flight.c flight_cells) gets the cell means of frame's thumbnail, which has just
+// completed in its ring slot (g_pl held). flash_cells reads a quarter of the slot's pixels, about 15 us measured on
+// ordinary memory (tests/flight_flash_check.py time); the ring is mapped host memory allocated for random access
+// (ensure_thumbs), which is cached memory where the device has it, as vk_thumbs_read's copy of a whole slot relies on.
+static void thumb_cells(uint64_t frame) {
+ unsigned ring = (unsigned)(frame % (uint64_t)g_th_n);
+ uint8_t cells[FLASH_CELLS];
+ vmaInvalidateAllocation(vk_ctx()->vma, g_th_alloc, TH_BYTES * ring, TH_BYTES);
+ flash_cells(g_th_ptr + TH_BYTES * ring, FLIGHT_THUMB_W, FLIGHT_THUMB_H, cells);
+ flight_cells(frame, cells);
+}
+
void vk_present_capture(int on) {
plat_rmutex_lock(&g_pl);
g_capture = on;
@@ -1451,8 +1464,10 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
vkResetFences(c->device, 1, &g_fence[slot]);
if (g_sc_hdr) vk_hdr_metadata(g_sc, 0); // a changed peak or white goes to the display
if (g_th_pending[slot]) { // that fence has signalled: the thumbnail its submission wrote is complete
- g_th_tag[g_th_pending[slot] % (uint64_t)g_th_n] = g_th_pending[slot];
+ uint64_t done = g_th_pending[slot];
+ g_th_tag[done % (uint64_t)g_th_n] = done;
g_th_pending[slot] = 0;
+ if (flight_flash_on && g_th_ptr) thumb_cells(done);
}
int cx = 0, cy = 0, cw = w, ch = h;
diff --git a/tests/flight_flash_check.c b/tests/flight_flash_check.c
new file mode 100644
index 00000000..9d4d69ed
--- /dev/null
+++ b/tests/flight_flash_check.c
@@ -0,0 +1,271 @@
+// runtime/flight_flash.h: the flight recorder's flash trigger, which frames count as a one-frame flash. Pictures of the
+// thumbnails' size are drawn here, turned into cell means (flash_cells) and judged three at a time (flash_test), or
+// handed in frame by frame as the present thread does (flash_note). Built and run by tests/flight_flash_check.py;
+// prints each failed check and exits 1 on any. With the argument "time" it also prints what one flash_cells and one
+// flash_test cost.
+#include
+#include
+#include
+#include
+
+#include "flight_flash.h"
+
+#define W 640 // FLIGHT_THUMB_W x FLIGHT_THUMB_H (flight.h): cells of 40x40 pixels
+#define H 360
+#define DIFF 24 // the defaults of NX2NAVI_FLIGHT_FLASH_DIFF and NX2NAVI_FLIGHT_FLASH_CELLS (cfg.c)
+#define MIN_CELLS 3
+
+static int fails;
+#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
+
+typedef struct { uint8_t px[W * H * 4]; } Pic;
+typedef struct { uint8_t c[FLASH_CELLS]; } Cells;
+
+// A rectangle of one grey level (clipped to the picture).
+static void fill(Pic *p, int x0, int y0, int x1, int y1, int v) {
+ for (int y = y0 < 0 ? 0 : y0; y < y1 && y < H; y++)
+ for (int x = x0 < 0 ? 0 : x0; x < x1 && x < W; x++) {
+ uint8_t *q = &p->px[4 * (y * W + x)];
+ q[0] = q[1] = q[2] = (uint8_t)v, q[3] = 255;
+ }
+}
+
+// A scene with something in every cell: bands of brightness that differ from cell to cell, shifted by (dx, dy) pixels.
+static void scene(Pic *p, int dx, int dy) {
+ for (int y = 0; y < H; y++)
+ for (int x = 0; x < W; x++) {
+ int u = x + dx, v = y + dy;
+ int g = 60 + ((u / 40 * 37 + v / 40 * 91) % 7) * 20 + ((u / 5 + v / 5) & 1) * 10;
+ uint8_t *q = &p->px[4 * (y * W + x)];
+ q[0] = q[1] = q[2] = (uint8_t)g, q[3] = 255;
+ }
+}
+
+static Cells cells_of(const Pic *p) {
+ Cells c;
+ flash_cells(p->px, W, H, c.c);
+ return c;
+}
+
+static int test3(const Pic *a, const Pic *b, const Pic *c) {
+ Cells ca = cells_of(a), cb = cells_of(b), cc = cells_of(c);
+ return flash_test(ca.c, cb.c, cc.c, DIFF, MIN_CELLS);
+}
+
+static Pic A, B, C;
+
+int main(int argc, char **argv) {
+ // ---- flash_cells: the cells are where they should be, and the byte order does not matter
+ memset(&A, 0, sizeof A);
+ fill(&A, 0, 0, W, H, 40);
+ fill(&A, 80, 40, 160, 120, 200); // cells (2..3, 1..2) whole
+ fill(&A, 600, 320, 640, 360, 120); // the last cell
+ Cells ca = cells_of(&A);
+ int wrong = 0;
+ for (int y = 0; y < FLASH_CELLS_Y; y++)
+ for (int x = 0; x < FLASH_CELLS_X; x++) {
+ int want = x >= 2 && x <= 3 && y >= 1 && y <= 2 ? 200 : x == 15 && y == 8 ? 120 : 40;
+ wrong += ca.c[y * FLASH_CELLS_X + x] != want;
+ }
+ CHECK(!wrong, "%d cells do not hold their rectangle's level", wrong);
+ for (int i = 0; i < W * H; i++) A.px[4 * i] = 255, A.px[4 * i + 2] = 0; // first byte full, third none
+ Cells rb = cells_of(&A);
+ for (int i = 0; i < W * H; i++) A.px[4 * i] = 0, A.px[4 * i + 2] = 255; // the same picture in the other order
+ Cells br = cells_of(&A);
+ CHECK(!memcmp(rb.c, br.c, FLASH_CELLS), "the means differ with the order of the first and the third byte");
+
+ // ---- a flash: one frame with a bright patch over a steady scene, the frames around it alike
+ scene(&A, 0, 0);
+ B = A;
+ fill(&B, 200, 120, 320, 200, 255); // 3x2 cells turn white for the frame
+ int n = test3(&A, &B, &A);
+ CHECK(n == 6, "a white patch of 6 cells for one frame counts %d cells", n);
+ fill(&B, 200, 120, 320, 200, 0);
+ n = test3(&A, &B, &A);
+ CHECK(n == 6, "a black patch of 6 cells for one frame counts %d cells", n);
+ fill(&B, 0, 0, W, H, 255);
+ n = test3(&A, &B, &A);
+ CHECK(n == FLASH_CELLS, "a white frame counts %d cells", n);
+ // ... and with a little change between the neighbours, as a scene with slow motion has
+ scene(&C, 1, 0);
+ B = A;
+ fill(&B, 200, 120, 320, 200, 255);
+ n = test3(&A, &B, &C);
+ CHECK(n == 6, "the patch between two frames a pixel apart counts %d cells", n);
+
+ // ---- no flash: too small, too weak, or not there for one frame only
+ B = A;
+ fill(&B, 200, 120, 280, 160, 255); // 2 cells
+ CHECK(!test3(&A, &B, &A), "a patch of 2 cells is a flash with %d as the least", MIN_CELLS);
+ Cells flat, mid;
+ memset(flat.c, 100, FLASH_CELLS);
+ mid = flat;
+ for (int i = 0; i < 5; i++) mid.c[40 + i] = 100 + DIFF; // differs by the threshold itself, not by more
+ CHECK(!flash_test(flat.c, mid.c, flat.c, DIFF, MIN_CELLS), "cells that differ by the threshold count");
+ for (int i = 0; i < 5; i++) mid.c[40 + i] = 100 + DIFF + 1;
+ CHECK(flash_test(flat.c, mid.c, flat.c, DIFF, MIN_CELLS) == 5, "cells one past the threshold do not count");
+ B = A;
+ fill(&B, 200, 120, 320, 200, 255);
+ CHECK(!test3(&A, &B, &B), "a patch that stays is a flash"); // appears and stays
+ CHECK(!test3(&B, &B, &A), "a patch that goes is a flash"); // was there and goes
+ CHECK(!test3(&A, &A, &A), "three frames alike are a flash");
+
+ // ---- a camera cut: two different scenes, the cut before the frame or after it
+ scene(&C, 333, 222);
+ for (int i = 0; i < W * H * 4; i++) C.px[i] = (uint8_t)(255 - C.px[i]);
+ CHECK(!test3(&A, &C, &C), "a cut before the frame is a flash");
+ CHECK(!test3(&A, &A, &C), "a cut after the frame is a flash");
+ // three unrelated pictures (a cut on both sides, a fast turn): the neighbours disagree, nothing is judged
+ scene(&B, 77, 140);
+ CHECK(!test3(&A, &B, &C), "three unrelated pictures are a flash");
+ // a fade: every cell between its neighbours
+ fill(&A, 0, 0, W, H, 200), fill(&B, 0, 0, W, H, 120), fill(&C, 0, 0, W, H, 40);
+ CHECK(!test3(&A, &B, &C), "a fade is a flash");
+
+ // ---- steady motion
+ // the whole scene pans, slowly and fast
+ for (int v = 1; v <= 64; v *= 2) {
+ scene(&A, 0, 0), scene(&B, v, 0), scene(&C, 2 * v, 0);
+ n = test3(&A, &B, &C);
+ CHECK(!n, "a pan of %d pixels a frame is a flash of %d cells", v, n);
+ scene(&B, v, v), scene(&C, 2 * v, 2 * v);
+ n = test3(&A, &B, &C);
+ CHECK(!n, "a diagonal pan of %d pixels a frame is a flash of %d cells", v, n);
+ }
+ // A bright bar as wide as a cell and five cells high crosses a flat picture. In the frame in which it fills its
+ // cells, they are brighter than in the frame before and the frame after, which are alike there: only the cells
+ // beside them, which the bar leaves and enters, tell this from a flash. Every speed and every phase.
+ int hits = 0;
+ for (int v = 1; v <= 80; v++)
+ for (int x = 160; x < 160 + 40; x++) {
+ for (int k = 0; k < 3; k++) {
+ Pic *p = k == 0 ? &A : k == 1 ? &B : &C;
+ fill(p, 0, 0, W, H, 50);
+ fill(p, x + (k - 1) * v, 80, x + (k - 1) * v + 40, 280, 230);
+ }
+ hits += test3(&A, &B, &C) != 0;
+ }
+ CHECK(!hits, "a bar crossing the picture is a flash at %d of 3200 speeds and places", hits);
+ // ... and the same bar moving down
+ hits = 0;
+ for (int v = 1; v <= 60; v++)
+ for (int y = 120; y < 120 + 40; y++) {
+ for (int k = 0; k < 3; k++) {
+ Pic *p = k == 0 ? &A : k == 1 ? &B : &C;
+ fill(p, 0, 0, W, H, 50);
+ fill(p, 120, y + (k - 1) * v, 440, y + (k - 1) * v + 40, 230);
+ }
+ hits += test3(&A, &B, &C) != 0;
+ }
+ CHECK(!hits, "a bar moving down the picture is a flash at %d of 2400 speeds and places", hits);
+ // the bar where it stands for one frame only is a flash: nothing beside it changed
+ fill(&A, 0, 0, W, H, 50);
+ B = A;
+ fill(&B, 160, 80, 200, 280, 230);
+ n = test3(&A, &B, &A);
+ CHECK(n == 5, "the bar shown for one frame counts %d cells", n);
+ // Three squares as large as a cell move diagonally. What leaves a square's cell is shared between three cells
+ // beside it, so none of those changes by as much as a flash does: only the cells around a cell that are compared
+ // with what that cell differs by tell these from three cells that flash.
+ hits = 0;
+ for (int v = 1; v < 40; v++)
+ for (int ph = 0; ph < 40; ph++) {
+ for (int k = 0; k < 3; k++) {
+ Pic *p = k == 0 ? &A : k == 1 ? &B : &C;
+ int o = ph + (k - 1) * v;
+ fill(p, 0, 0, W, H, 50);
+ fill(p, 120 + o, 80 + o, 160 + o, 120 + o, 230);
+ fill(p, 280 + o, 160 + o, 320 + o, 200 + o, 230);
+ fill(p, 440 + o, 240 + o, 480 + o, 280 + o, 230);
+ }
+ hits += test3(&A, &B, &C) != 0;
+ }
+ CHECK(!hits, "three squares moving diagonally are a flash at %d of 1560 speeds and places", hits);
+
+ // ---- the rules cell by cell: a row of three cells 60 brighter for one frame, and what the neighbours do around it
+ Cells p0, c0, n0;
+ memset(p0.c, 100, FLASH_CELLS);
+ c0 = n0 = p0;
+ for (int i = 0; i < 3; i++) c0.c[4 * FLASH_CELLS_X + 5 + i] = 160;
+ CHECK(flash_test(p0.c, c0.c, n0.c, DIFF, MIN_CELLS) == 3, "three cells 60 brighter for one frame are no flash");
+ n0.c[3 * FLASH_CELLS_X + 4] = 115; // diagonally beside the first cell: 15 apart, a quarter of 60
+ CHECK(flash_test(p0.c, c0.c, n0.c, DIFF, MIN_CELLS) == 3, "a cell beside 15 apart, a quarter of the flash, hides it");
+ n0.c[3 * FLASH_CELLS_X + 4] = 116; // one more: the first cell no longer counts, and two are too few
+ CHECK(!flash_test(p0.c, c0.c, n0.c, DIFF, MIN_CELLS), "a cell beside 16 apart does not hide a flash of 60");
+ CHECK(flash_test(p0.c, c0.c, n0.c, DIFF, 2) == 2, "the two cells further from it do not count");
+ n0 = p0;
+ n0.c[8 * FLASH_CELLS_X + 15] = 100 + DIFF; // far from the flash, as far apart as allowed
+ CHECK(flash_test(p0.c, c0.c, n0.c, DIFF, MIN_CELLS) == 3, "a far cell %d apart keeps the frame from being judged", DIFF);
+ n0.c[8 * FLASH_CELLS_X + 15] = 100 + DIFF + 1; // further than a flash must differ by: not judged
+ CHECK(!flash_test(p0.c, c0.c, n0.c, DIFF, MIN_CELLS), "a far cell %d apart does not", DIFF + 1);
+ // the neighbours a little apart (more than a quarter of the threshold) in the lower rows: FLASH_MOVING cells
+ // leave the frame judged, one more does not
+ n0 = p0;
+ for (int i = 0; i < FLASH_MOVING; i++) n0.c[6 * FLASH_CELLS_X + i] = 100 + DIFF / 4 + 1;
+ CHECK(flash_test(p0.c, c0.c, n0.c, DIFF, MIN_CELLS) == 3, "%d cells a little apart keep the frame from being judged", FLASH_MOVING);
+ n0.c[6 * FLASH_CELLS_X + FLASH_MOVING] = 100 + DIFF / 4 + 1;
+ CHECK(!flash_test(p0.c, c0.c, n0.c, DIFF, MIN_CELLS), "%d cells a little apart do not", FLASH_MOVING + 1);
+ // darker than one neighbour and brighter than the other is no flash, however the thresholds are set
+ for (int i = 0; i < FLASH_CELLS; i++) p0.c[i] = 40, c0.c[i] = 100, n0.c[i] = 160;
+ CHECK(!flash_test(p0.c, c0.c, n0.c, 4, 1) && !flash_test(n0.c, c0.c, p0.c, 4, 1), "a frame between its neighbours is a flash");
+
+ // ---- frame by frame, with thumbnails missing
+ scene(&A, 0, 0);
+ B = A;
+ fill(&B, 200, 120, 320, 200, 255);
+ Cells steady = cells_of(&A), flash = cells_of(&B);
+ static FlashTrack t;
+ int r[40] = {0};
+ // frames 1..9 steady, 5 the flash: found when 6 arrives, as frame 5
+ for (uint64_t f = 1; f <= 9; f++) r[f] = flash_note(&t, f, f == 5 ? flash.c : steady.c, DIFF, MIN_CELLS);
+ wrong = 0;
+ for (int f = 1; f <= 9; f++) wrong += (r[f] != 0) != (f == 6);
+ CHECK(!wrong && r[6] == 6, "a flash at frame 5 is reported at %d other frames, with %d cells at frame 6", wrong, r[6]);
+ // the flash at 12, the frame after it without a thumbnail: not judged, at 13 (missing), 14 or later
+ wrong = 0;
+ for (uint64_t f = 10; f <= 18; f++)
+ if (f != 13) wrong += flash_note(&t, f, f == 12 ? flash.c : steady.c, DIFF, MIN_CELLS) != 0;
+ CHECK(!wrong, "a flash whose next frame has no thumbnail is reported %d times", wrong);
+ // the frame before the flash without a thumbnail
+ wrong = 0;
+ for (uint64_t f = 20; f <= 28; f++)
+ if (f != 22) wrong += flash_note(&t, f, f == 23 ? flash.c : steady.c, DIFF, MIN_CELLS) != 0;
+ CHECK(!wrong, "a flash whose frame before has no thumbnail is reported %d times", wrong);
+ // the flash frame itself without a thumbnail, and its neighbours four frames apart from older ones in the slots
+ wrong = 0;
+ for (uint64_t f = 30; f <= 38; f++)
+ if (f != 33) wrong += flash_note(&t, f, steady.c, DIFF, MIN_CELLS) != 0;
+ CHECK(!wrong, "steady frames with one missing are reported %d times", wrong);
+ // only every other frame has a thumbnail, and they alternate: no three in a row, nothing judged
+ wrong = 0;
+ for (uint64_t f = 40; f <= 60; f += 2) wrong += flash_note(&t, f, f % 4 ? flash.c : steady.c, DIFF, MIN_CELLS) != 0;
+ CHECK(!wrong, "frames two apart are judged %d times", wrong);
+ // the first frames of a run, and frame 0, which names no frame
+ memset(&t, 0, sizeof t);
+ CHECK(!flash_note(&t, 0, flash.c, DIFF, MIN_CELLS) && !flash_note(&t, 1, steady.c, DIFF, MIN_CELLS) &&
+ !flash_note(&t, 2, flash.c, DIFF, MIN_CELLS),
+ "the first two frames are judged");
+ CHECK(flash_note(&t, 3, steady.c, DIFF, MIN_CELLS) == 6, "a flash at frame 2 is not found at frame 3");
+
+ if (argc > 1 && !strcmp(argv[1], "time")) {
+ enum { N = 2000 };
+ scene(&A, 0, 0);
+ unsigned sum = 0;
+ clock_t t0 = clock();
+ for (int i = 0; i < N; i++) {
+ A.px[(i * 4099) % (W * H * 4)] ^= 1; // a picture the compiler cannot take as known
+ Cells c = cells_of(&A);
+ sum += c.c[i % FLASH_CELLS];
+ }
+ clock_t t1 = clock();
+ for (int i = 0; i < N * 100; i++) {
+ flash.c[i % FLASH_CELLS] ^= 1;
+ sum += (unsigned)flash_test(steady.c, flash.c, steady.c, DIFF, MIN_CELLS);
+ }
+ clock_t t2 = clock();
+ printf("flight_flash_check: flash_cells %.1f us a picture, flash_test %.2f us a frame (sum %u)\n",
+ (double)(t1 - t0) * 1e6 / CLOCKS_PER_SEC / N, (double)(t2 - t1) * 1e6 / CLOCKS_PER_SEC / (N * 100), sum);
+ }
+ printf(fails ? "flight_flash_check: %d failed\n" : "flight_flash_check: ok\n", fails);
+ return fails != 0;
+}
diff --git a/tests/flight_flash_check.py b/tests/flight_flash_check.py
new file mode 100644
index 00000000..7111e71d
--- /dev/null
+++ b/tests/flight_flash_check.py
@@ -0,0 +1,44 @@
+"""Build and run tests/flight_flash_check.c: runtime/flight_flash.h, which frames the flight recorder takes for a
+one-frame flash (a flash, a camera cut, steady motion, frames whose neighbours have no thumbnail).
+
+No GPU, no game files, any Python. Built as the runtime is (-O2, x86-64-v3 on x86-64), so the "time" run measures what
+the present thread pays.
+
+Usage: flight_flash_check.py [time]
+"""
+import os
+import platform
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
+
+
+def compiler():
+ """$CC, else MSYS2 UCRT64's clang, else clang on PATH (as tests/diff/common.py picks it, without its Unicorn)."""
+ if os.environ.get('CC'):
+ return os.environ['CC']
+ if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists():
+ return str(MSYS_BIN / 'clang.exe')
+ return shutil.which('clang') or 'clang'
+
+
+def main():
+ work = ROOT / 'build' / 'test' / 'flight_flash'
+ work.mkdir(parents=True, exist_ok=True)
+ exe = work / ('flight_flash_check.exe' if os.name == 'nt' else 'flight_flash_check')
+ env = dict(os.environ)
+ if os.name == 'nt' and MSYS_BIN.exists():
+ env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
+ arch = ['-march=x86-64-v3'] if platform.machine().lower() in ('amd64', 'x86_64') else []
+ subprocess.check_call([compiler(), '-std=gnu11', '-O2', *arch, '-Wall', '-Wextra', '-Werror',
+ '-I', str(ROOT / 'runtime'), str(ROOT / 'tests' / 'flight_flash_check.c'), '-o', str(exe)],
+ env=env)
+ return subprocess.run([str(exe), *sys.argv[1:2]], env=env).returncode
+
+
+if __name__ == '__main__':
+ sys.exit(main())