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