Let the flight recorder dump on its own when one frame flashes.

With flight = 1 and its thumbnails kept, the recorder now also writes a dump when a frame's picture differs strongly from the frame before and the frame after while those two agree, which is what a one-frame flicker looks like. Each thumbnail is reduced to 16x9 cells of mean brightness where the present thread already sees it complete, and a frame is a flash when at least three cells are more than 24 of 255 brighter or darker than in both neighbours, the neighbours being alike in those cells, around them and over the picture as a whole. A camera cut, a fade, a pan and shapes crossing the picture are no flash, and a frame beside one without a thumbnail is not judged. The dump is the one F8 writes, named "flash" with the frame's number in its header line, at most one every 30 seconds. flight_flash = 0 turns it off; flight_flash_diff and flight_flash_cells set the two thresholds.

On the thumbnails of earlier dumps the defaults flag the frames whose scene was white once and nothing in the runs without that fault. tests/flight_flash_check.py checks the test itself on drawn pictures.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-12 15:15:45 -05:00
parent 95dc476ee7
commit 202fd47ceb
9 changed files with 534 additions and 4 deletions

View File

@@ -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`:

View File

@@ -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.

View File

@@ -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

View File

@@ -4,7 +4,8 @@
// frames, flight_tex), f<frame>.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=<frame>[,<frame>...] and
// NX2NAVI_FLIGHT_EVERY=<n> (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;

View File

@@ -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 <stdint.h>
@@ -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);

107
runtime/flight_flash.h Normal file
View File

@@ -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 <stdint.h>
#include <string.h>
#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);
}

View File

@@ -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;

271
tests/flight_flash_check.c Normal file
View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#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;
}

View File

@@ -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())