diff --git a/docs/releases/v0.10.2.md b/docs/releases/v0.10.2.md index 55a78c30..a7112630 100644 --- a/docs/releases/v0.10.2.md +++ b/docs/releases/v0.10.2.md @@ -55,6 +55,11 @@ cards). - **Flashing or white foliage on cards that take the mesh shader fallback** (reported on a Radeon RX 580): the numbers each such draw goes by were written in an order the graphics card was free to change, and about half the draws got the wrong ones. Each is now written exactly once (by Tobim6). +- **The whole scene flashing for single frames with *Helpers take small jobs* on** (`replay_small`, with *Helpers work + ahead*; the interface stayed as it was), under DLSS or FSR 3.1: a helper thread could name an earlier image of the + frame as the scene after a later one had been named, and the upscaler then took that image for one frame. At one + place in Kakariko with both settings as a tester had them, 51 and 52 frames flashed in two runs of 100 seconds, and + none with the fix. Both settings stay off by default. - **Crashes and hangs found in a review of the whole runtime**; none was seen in our own play, and each fix has a check of its own: - a crash when the game read memory the lazy write-back was just handing to the GPU (NVIDIA and Intel cards); diff --git a/runtime/vk_exec.c b/runtime/vk_exec.c index 6ee1cab0..e708920b 100644 --- a/runtime/vk_exec.c +++ b/runtime/vk_exec.c @@ -27,6 +27,7 @@ #include "vk.h" #include "vk_exec.h" #include "vk_evo_peek.h" +#include "vk_pass_hooks.h" #include "vk_target_frames.h" #include "vk_dlaa.h" @@ -818,8 +819,10 @@ void vk_exec_pass_begin(VkExec *ex) { resolve_rt(ex); if (vk_texture_deferred()) return; // a chunk's lookup waits for its turn (chunk_record): the op is replayed again // A parallel group's targets do not change: its pass hooks, which follow the frame pass by pass, run in its first - // chunk, which the others start after (chunk_record). - if (!rw_chunk || !rw_chunk->idx) { + // chunk, which the others start after (chunk_record), and at that chunk's first pass alone: a pass it begins again + // later may come after the hooks of passes further on in the stream (vk_pass_hooks.h). + if (pass_hooks_due(rw_chunk != NULL, rw_chunk ? rw_chunk->idx : 0, + rw_chunk ? __atomic_load_n(&rw_chunk->g->hooks, __ATOMIC_RELAXED) : 0)) { // the submitting thread past a group: the hooks read what its first chunk's left, and wait for the whole // group where they change what its draws read (vk_exec_hook_turn) if (!rw_chunk) vk_exec_hooks_wait(); @@ -1929,8 +1932,9 @@ static void op_one(VkExec *ex, const op_hdr *h) { // group: up to N (at most LIVE_GROUPS) groups are handed out and not in their part yet, their chunks taken from one // queue in the order of the stream (Q). Hyrule Field, 3 workers (2026-11-12): the frame's groups took 6.8 ms of the // submitting thread and the ops between them 4.6. A group's chunk 0 runs its pass hooks after the group before began -// its pass, and waits for every earlier group where a hook acts; a turn (vk_exec_turn) waits for every earlier chunk, -// of earlier groups too. +// its pass, once (a pass it begins again later would run them after those of passes further on, vk_pass_hooks.h), and +// waits for every earlier group where a hook acts; a turn (vk_exec_turn) waits for every earlier chunk, of earlier +// groups too. // The submitting thread waits for the groups (group_wait), and puts their command buffers and query slots in place // (group_drain, oldest first: groups_put_done), only where the order needs it: // - for room when N groups are out, before a part is split off or submitted (a part due before a group goes to the diff --git a/runtime/vk_pass_hooks.h b/runtime/vk_pass_hooks.h new file mode 100644 index 00000000..79df9c08 --- /dev/null +++ b/runtime/vk_pass_hooks.h @@ -0,0 +1,19 @@ +// Which pass begin runs the frame's pass hooks (vk_exec.c vk_exec_pass_begin: vk_contact_pass_begin, vk_dlaa_pass). +// Header only, so that tests/pass_hooks_check.c checks the rule without a device. +// +// The hooks follow the game's frame pass by pass, and what they keep is the last pass's that ran them: the light +// target the upscaler takes is the one of the last scene pass before the output pass (vk_dlaa.c vk_dlaa_pass). A pass +// is begun more than once while it is recorded: a barrier, a texture lookup that waits for its turn or a pass of the +// replay's own ends it, and the next draw begins it again. On one thread the second begin runs the hooks in the same +// place of the stream, with the same targets, and changes nothing. A parallel group's pass is recorded by several +// threads, and its hooks run in the group's first chunk. With the submitting thread going on past the group +// (NX2NAVI_REPLAY_PIPE) a second begin in that chunk came after the hooks of passes later in the stream had run, and +// put the group's own target back as the light target: the upscaler then took an earlier image of the frame for one +// frame, a flash of the whole scene with the interface intact (Kakariko, DLSS Performance, 3 workers, 4 groups out: +// 51 and 52 such frames in two runs of 100 s, none with the helpers off). So a group's hooks run once, at its first +// chunk's first pass. +#pragma once + +// in_chunk: the calling thread records a chunk of a parallel group; idx: the chunk's place in its group; ran: the +// group's hooks ran already (its first chunk began a pass). +static inline int pass_hooks_due(int in_chunk, int idx, int ran) { return !in_chunk || (!idx && !ran); } diff --git a/tests/pass_hooks_check.c b/tests/pass_hooks_check.c new file mode 100644 index 00000000..3c682a48 --- /dev/null +++ b/tests/pass_hooks_check.c @@ -0,0 +1,86 @@ +// runtime/vk_pass_hooks.h: which pass begin runs the frame's pass hooks (vk_exec.c vk_exec_pass_begin). A parallel +// group's hooks run once, at its first chunk's first pass: a pass that chunk begins again later must not put its own +// target back after the hooks of later passes ran. Built and run by tests/pass_hooks_check.py; prints each failed check +// and exits 1 on any. +#include + +#include "vk_pass_hooks.h" + +static int fails; +#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0) + +// The hook as vk_dlaa_pass keeps the light target: the target of the last pass that ran it. +static int light; +// A group as vk_exec.c keeps it: whether its first chunk began a pass. +typedef struct { int hooks; } Grp; + +// A pass begin of a thread that records no chunk (g NULL), or chunk idx of group g, into `target`. +static void pass_begin(Grp *g, int idx, int target) { + if (pass_hooks_due(g != NULL, idx, g ? g->hooks : 0)) light = target; + if (g && !idx) g->hooks = 1; +} + +int main(void) { + enum { EARLY = 1, SCENE = 2, OUTPUT = 3 }; + // ---- the rule itself + CHECK(pass_hooks_due(0, 0, 0), "a thread outside a group does not run the hooks"); + CHECK(pass_hooks_due(0, 0, 1) && pass_hooks_due(0, 5, 1), "outside a group the other two values count"); + CHECK(pass_hooks_due(1, 0, 0), "a group's first chunk does not run the hooks at its first pass"); + CHECK(!pass_hooks_due(1, 0, 1), "a group's first chunk runs the hooks again at a pass it begins again"); + for (int idx = 1; idx < 17; idx++) + CHECK(!pass_hooks_due(1, idx, 0) && !pass_hooks_due(1, idx, 1), "chunk %d of a group runs the hooks", idx); + + // ---- one thread: a pass begun twice changes nothing, the last pass of the stream keeps the light target + light = 0; + pass_begin(NULL, 0, EARLY); + pass_begin(NULL, 0, EARLY); // after a barrier + pass_begin(NULL, 0, SCENE); + pass_begin(NULL, 0, SCENE); + CHECK(light == SCENE, "one thread: the light target is %d, not the last scene pass's", light); + + // ---- a group whose chunks the workers record while the submitting thread waits for it: its passes begin in any + // order after the first chunk's, and the pass after the group comes after all of them + Grp g = {0}; + light = 0; + pass_begin(&g, 0, EARLY); + pass_begin(&g, 2, EARLY); + pass_begin(&g, 1, EARLY); + pass_begin(&g, 0, EARLY); // the first chunk's pass again, after a texture lookup waited for its turn + pass_begin(NULL, 0, SCENE); + CHECK(light == SCENE, "a group waited for: the light target is %d", light); + + // ---- the submitting thread goes on past the group: the scene pass after it begins once the group's first chunk + // began its pass, and that chunk begins its pass again after that. The scene pass is later in the stream and keeps + // the light target; the hooks running at the second begin gave the upscaler the earlier pass's image for a frame. + g = (Grp){0}; + light = 0; + pass_begin(&g, 0, EARLY); + pass_begin(NULL, 0, SCENE); // vk_exec_hooks_wait let it through: the group's hooks ran + pass_begin(&g, 0, EARLY); // late: after a lookup's turn, a barrier, a pass of the replay's own + pass_begin(&g, 1, EARLY); + CHECK(light == SCENE, "the submitting thread past a group: the light target is %d, the earlier pass's", light); + + // ---- two groups out and the pass after them: each first chunk begins its pass after the group before began + // its own (chunk_record), and each begins it again out of order + Grp a = {0}, b = {0}; + light = 0; + pass_begin(&a, 0, EARLY); + pass_begin(&b, 0, SCENE); + pass_begin(&a, 0, EARLY); + pass_begin(NULL, 0, OUTPUT); + pass_begin(&b, 0, SCENE); + pass_begin(&a, 0, EARLY); + CHECK(light == OUTPUT, "two groups out: the light target is %d", light); + + // ---- a first chunk whose first pass begin waited (a deferred lookup returns before the hooks): the hooks run at + // the begin that goes through, which is the one that marks the group + g = (Grp){0}; + light = 0; + if (pass_hooks_due(1, 0, g.hooks)) light = EARLY; // the begin that goes through + g.hooks = 1; + CHECK(light == EARLY, "a group's hooks never ran"); + + if (fails) printf("%d checks failed\n", fails); + else printf("pass_hooks_check: ok\n"); + return fails != 0; +} diff --git a/tests/pass_hooks_check.py b/tests/pass_hooks_check.py new file mode 100644 index 00000000..87b7854b --- /dev/null +++ b/tests/pass_hooks_check.py @@ -0,0 +1,41 @@ +"""Build and run tests/pass_hooks_check.c: runtime/vk_pass_hooks.h, which pass begin runs the frame's pass hooks +(vk_exec.c vk_exec_pass_begin): a parallel group's run once, at its first chunk's first pass, so a pass begun again +later does not put its own target back as the upscaler's light target. + +No GPU, no game files, any Python. + +Usage: pass_hooks_check.py +""" +import os +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' / 'pass_hooks' + work.mkdir(parents=True, exist_ok=True) + exe = work / ('pass_hooks_check.exe' if os.name == 'nt' else 'pass_hooks_check') + env = dict(os.environ) + if os.name == 'nt' and MSYS_BIN.exists(): + env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '') + subprocess.check_call([compiler(), '-std=gnu11', '-O1', '-Wall', '-Werror', '-I', str(ROOT / 'runtime'), + str(ROOT / 'tests' / 'pass_hooks_check.c'), '-o', str(exe)], env=env) + return subprocess.run([str(exe)], env=env).returncode + + +if __name__ == '__main__': + sys.exit(main())