Hand a group of draws too small to cut to one replay worker, whole, with replay_small.

With replay_pipe the submitting thread goes on past the groups its workers record. With NX2NAVI_REPLAY_SMALL=N a
group of N draws or more that is too small to cut into chunks goes to one worker as it is, and the submitting thread
goes on without taking the chunk itself. Off by default. The early tail now waits for groups still out before it
cuts the part, instead of giving the frame's tail up.

Kokiri Forest standing, RX 7900 XT, FSR 4, the early tail on, 5 workers, 4 groups out: about 700 draws a frame were in
such groups, 3.6 ms of the submitting thread; with 16 the replay took 10.2 instead of 11.0 ms and the frame rate was
63.2 against 62.2, 45.4 with everything off. The screenshots matched those with everything off, standing and
through warps to five areas.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
epicmeme546
2026-11-12 18:58:39 -05:00
parent aec5e72051
commit 86131455ed
3 changed files with 32 additions and 5 deletions

View File

@@ -80,6 +80,12 @@
# Hyrule Field on an RX 7900 XT with FSR 4: 50.7 instead of 42.5 fps, and 63.5 instead of 49.4 with replay_workers = 3.
# Does nothing without one of those upscalers, nor with the lazy write-back (the default on NVIDIA and Intel cards).
# tail_early = 0
# EXPERIMENTAL, with replay_workers. replay_pipe = N (1 to 4, default 0): the submitting thread goes on past up to N
# groups its workers still record. replay_small = N (default 0, needs replay_pipe): a group of N draws or more that is
# too small to share out goes whole to one worker. Kokiri Forest standing on an RX 7900 XT with FSR 4, tail_early = 1,
# replay_workers = 5, replay_pipe = 4 and replay_small = 16: 63 instead of 45 fps with all of them off.
# replay_pipe = 0
# replay_small = 0
# EXPERIMENTAL, THE GAME CRASHES WITH 1. 1 lifts the virtual core gate: guest threads run without the console's limit
# of one thread per core, and the game keeps per-core data without locks. With 1 the game crashes or hangs,
# usually at start. Keep 0 to play.

View File

@@ -64,7 +64,7 @@ static const char *const known[] = {
"POOL_FAST", "POOL_GROUP", "POOL_GROUP_CHECK", "POOL_PRESNAP", "POOL_PRESNAP_RUNS",
"SUBMIT_FINISH_GPU", "SVC_WAITS", "SNAP_WIDEN", "SUBMIT_FINISH_RESULTS", "SUBMIT_FINISH_KINDS", "FOLIAGE", "MIRROR_STAGE", "FLUSH_SKIP_CLEAN", "MIRROR_HOT", "MESH_EMULATION", "MESH_EMU_MB", "MESH_EMU_TASKS", "MESH_EMU_BATCH", "GUEST_OPT", "LOG_TEX_NEW", "DUMP_KEY", "VEC_INDEX_FIX", "PASS_TIMES",
"PROFILE_MAIN", "PROFILE_RENDER", "PROMPTS", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",
"REFLEX", "RENDER_SCALE", "REPLAY_ASYNC", "REPLAY_PIPE", "REPLAY_RUNS", "TAIL_EARLY", "REPLAY_WORKERS", "RTSS", "RTSS_ENUM", "SCENE_SHARE",
"REFLEX", "RENDER_SCALE", "REPLAY_ASYNC", "REPLAY_PIPE", "REPLAY_RUNS", "REPLAY_SMALL", "TAIL_EARLY", "REPLAY_WORKERS", "RTSS", "RTSS_ENUM", "SCENE_SHARE",
"PRESNAP_PLACE",
"REPLAY_CHECK", "RESOLVE_CHECK", "RESTART_WAIT", "ROMFS", "ROMFS_INDEX", "ROMFS_MMAP", "ROMFS_MODS", "ROMFS_PATCHES",
"ROMFS_PROOF_DIR", "ROMFS_VERIFY", "RSTICK_DEADZONE", "RUMBLE", "RUMBLE_CURVE", "RUMBLE_STRENGTH", "RUMBLE_TRIGGERS", "SHADER_CACHE",
@@ -130,7 +130,7 @@ static const struct { const char *key, *section; } sections[] = {
{"BIND_RIGHT", "input"}, {"BIND_DOWN", "input"}, {"BIND_LSTICK_LEFT", "input"}, {"BIND_LSTICK_UP", "input"},
{"BIND_LSTICK_RIGHT", "input"}, {"BIND_LSTICK_DOWN", "input"}, {"BIND_RSTICK_LEFT", "input"},
{"BIND_RSTICK_UP", "input"}, {"BIND_RSTICK_RIGHT", "input"}, {"BIND_RSTICK_DOWN", "input"}, {"BIND_SHAKE", "input"},
{"ASYNC_PREEMPT", "performance"}, {"REPLAY_ASYNC", "performance"}, {"REPLAY_WORKERS", "performance"}, {"REPLAY_PIPE", "performance"}, {"TAIL_EARLY", "performance"},
{"ASYNC_PREEMPT", "performance"}, {"REPLAY_ASYNC", "performance"}, {"REPLAY_WORKERS", "performance"}, {"REPLAY_PIPE", "performance"}, {"TAIL_EARLY", "performance"}, {"REPLAY_SMALL", "performance"},
{"FREE_CORES", "performance"},
{"DYN_RES", "performance"}, {"DYN_RES_TARGET", "performance"}, {"DYN_RES_STEP", "performance"},
{"RECORDER_CORE", "performance"}, {"RECORDER_PRIO", "performance"}, {"BIND_REUSE", "performance"},
@@ -261,6 +261,7 @@ static const CfgMeta metas[] = {
{"ASYNC_PREEMPT", "1", "0|1|2", RESTART}, {"REPLAY_ASYNC", "0", "0|1", RESTART},
{"REPLAY_WORKERS", "0", "0..16", RESTART}, {"REPLAY_PIPE", "0", "0..4", RESTART},
{"TAIL_EARLY", "0", "0|1", RESTART}, // vk_exec.c "the early tail"
{"REPLAY_SMALL", "0", "0..1024", RESTART}, // vk_exec.c group_start
{"FREE_CORES", "0", "0|1", RESTART}, {"TIMER_1MS", "1", "0|1", RESTART},
// dynamic resolution (dynres.c): the scene follows the GPU time toward DYN_RES_TARGET milliseconds
{"DYN_RES", "0", "0|1", LIVE}, {"DYN_RES_TARGET", "16.7", "1..100", LIVE}, {"DYN_RES_STEP", "45", "8..600", LIVE},

View File

@@ -1929,6 +1929,8 @@ static const char *const pw_name[PW_N] = {"next group", "op", "pass hooks", "tur
static struct {
int n; // workers; 0 off, -1 not read yet
int pipe; // NX2NAVI_REPLAY_PIPE: the groups out at once at most, 0 none
int small; // NX2NAVI_REPLAY_SMALL: the fewest draws of a group one worker takes whole
uint64_t small_groups, small_draws; // such groups and their draws since the last perf line
void *help_dc; // the submitting thread's draw context for chunks it takes while it waits
RwWorker *w[RW_MAX];
const op_hdr *skip_to[CALL_DEPTH + 1]; // per call depth: the walk read ahead to this op, a group too small
@@ -2060,6 +2062,10 @@ static void rw_init(void) {
int pipe = cfg_int("NX2NAVI_REPLAY_PIPE"); // the groups out at once
RW.pipe = RW.n <= 0 || pipe <= 0 ? 0 : pipe > LIVE_GROUPS ? LIVE_GROUPS : pipe;
if (RW.pipe) RW.help_dc = vk_exec_draw_ctx_new();
RW.small = RW.pipe ? cfg_int("NX2NAVI_REPLAY_SMALL") : 0;
if (RW.small > 0)
rt_log("vk: a group of %d draws or more goes whole to one replay worker (NX2NAVI_REPLAY_SMALL)", RW.small);
else RW.small = 0;
if (RW.pipe)
rt_log("vk: groups of %d draws or more are recorded by %d replay worker%s while the submitting thread goes on "
"past them, %d at once at most (NX2NAVI_REPLAY_WORKERS, NX2NAVI_REPLAY_PIPE)", 2 * CHUNK_DRAWS, RW.n,
@@ -2371,7 +2377,14 @@ static const op_hdr *group_start(VkExec *ex, const op_hdr *h, int depth) {
int n = (int)(draws / CHUNK_DRAWS);
if (n > RW.n + 1) n = RW.n + 1;
if (n > GROUP_CHUNKS) n = GROUP_CHUNKS;
if (n < 2 || vk_dump_armed || TL.on || !vk_sync_append_room(n)) {
// NX2NAVI_REPLAY_SMALL=N (default 0, with NX2NAVI_REPLAY_PIPE): a group of N draws or more that is too small to cut
// goes whole to one worker, and the submitting thread goes on with what follows it without taking the chunk
// itself. Kokiri Forest standing, 5 workers, 4 groups out, the early tail on (2026-11-13): about 700 draws a frame
// were in such groups, 3.6 ms of the submitting thread; with 16 the replay took 10.2 instead of 11.0 ms a frame
// (63.2 against 62.2 fps: the game then waits longer for the GPU, which is busy 83% of the frame there).
int small = RW.pipe && RW.small && n < 2 && draws >= (uint32_t)RW.small;
if (small) n = 1;
if (n < 1 + !small || vk_dump_armed || TL.on || !vk_sync_append_room(n)) {
RW.skip_to[depth] = end, RW.serial_draws += draws, RW.small_end[why] += draws;
return h;
}
@@ -2445,7 +2458,8 @@ static const op_hdr *group_start(VkExec *ex, const op_hdr *h, int depth) {
// the submitting thread takes chunks too, as the workers wake up, and goes on once none is left to take: the
// GPU waits for the group's part until its last chunk is done
rw_behind = 1;
group_take(RW.n);
if (small) RW.small_groups++, RW.small_draws += draws; // a worker takes it: this thread goes on at once
else group_take(RW.n);
uint64_t to = kern_now_ns();
RW.front_ns += tf - t0, RW.own_ns += to - tf, RW.wait_ns += to - t0;
return end;
@@ -2566,7 +2580,8 @@ static void rec_claim(void) {
// The tail begins here (the early tail): what was recorded so far waits in the chain, the tail gets a command buffer of
// its own. Not in a chunk or a continuation, nor with groups still out: the cut needs every piece of the part.
void vk_exec_tail_begin(VkExec *ex) {
if (!tail_enabled() || TL.on || ex != x_cur || rw_chunk || evo_in || Q.drained != Q.out) return;
if (!tail_enabled() || TL.on || ex != x_cur || rw_chunk || evo_in) return;
if (Q.drained != Q.out) group_drain(ex, PW_OP); // the cut needs every piece of the part
if (!vk_sync_wb_split() || !vk_mesh_shader_native() || vk_dump_armed || vk_gpu_passes_capturing()) return;
if (!vk_sync_tail_ok()) return;
if (ex->in_pass) vk_exec_pass_end(ex);
@@ -2802,8 +2817,13 @@ int vk_exec_record(VkCommandBuffer cb, const uint64_t *handles, int n, uint64_t
memset(RW.pw_n, 0, sizeof RW.pw_n), memset(RW.pw_ns, 0, sizeof RW.pw_ns);
memset(RW.pd_op, 0, sizeof RW.pd_op);
RW.behind_ns = 0;
if (RW.small)
rt_log("perf: replay workers: groups one worker took whole (NX2NAVI_REPLAY_SMALL): %.2f per "
"submission, %.1f draws each", (double)RW.small_groups / (double)n_subs,
RW.small_groups ? (double)RW.small_draws / (double)RW.small_groups : 0.0);
}
}
RW.small_groups = RW.small_draws = 0;
RW.groups = RW.group_draws = RW.chunks = RW.serial_draws = RW.wait_ns = 0;
evo_held = evo_resumed = evo_forced = evo_switches = 0;
n_subs = n_parts = n_idle_parts = n_pass_splits = n_cuts = 0;