Make replay_workers default to auto, which picks the count from the CPU's cores.

Auto gives half the physical cores less one, at most 6: 2 on 6 cores, 3 on 8 (the count measured best on an 8-core CPU), 5 on 12. Four cores or fewer get none, and so does AMD's Windows driver for RX 6000 and older, where a player's device was lost with workers on and the cause is not known. The menu's row lists Automatic first with the count it gives on this PC, and the log says how many were started and why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 00:33:14 -05:00
parent f2849c8b26
commit f37d7fed6d
5 changed files with 54 additions and 14 deletions

View File

@@ -72,12 +72,14 @@
# the game has sometimes rewritten by then.
# replay_async = 0
# EXPERIMENTAL. Record the large groups of draws of a submission on this many replay worker threads beside the
# submitting one, 0 to 16. Default: 0, the replay records everything on one thread. 3 gave 51.7 instead of 43.6 fps
# standing in Hyrule Field on an RX 7900 XT, and more than 3 gave nothing more; some players' games ended with a lost
# graphics device with it on, so it is off unless you turn it on (also in the menu: Game, Testing, CPU helper threads).
# submitting one: auto or 0 to 16. Default: auto, half the CPU's physical cores less one (2 on 6 cores, 3 on 8, 5 on
# 12, at most 6), none on four cores or fewer and none on AMD's driver for RX 6000 and older Radeon cards, where a
# player's game lost its graphics device with workers on; the log's "replay workers automatic" line says how many.
# 0: the replay records everything on one thread. 3 gave 51.7 instead of 43.6 fps standing in Hyrule Field on an RX
# 7900 XT, and more than 3 gave nothing more there. Also in the menu: Game, Testing, CPU helper threads.
# With 1 a worker records each long run of draws while the submitting thread waits for it: no faster, it checks that
# the pictures stay the same.
# replay_workers = 0
# replay_workers = auto
# EXPERIMENTAL. 1 lets the game start on its next frame while the graphics card still runs the end of the last one: the
# upscaler (FSR 4, FSR 3.1 or DLSS) and the interface drawn after it. The graphics card runs the same work, with the
# copies of what it wrote for the game moved before that end, and the game's waits end once those copies and the

View File

@@ -283,7 +283,7 @@ static const CfgMeta metas[] = {
{"COOP_ACCEPT", "ask", NULL, LIVE},
// Advanced: scheduling
{"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},
{"REPLAY_WORKERS", "auto", "auto|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16", RESTART}, // vk_exec.c rw_init, vk_exec_replay_workers_auto {"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},

View File

@@ -87,6 +87,7 @@ const char *vk_fg4_name(void); // ... the version the library runs here
int vk_texture_detail_auto(void); // vk_texture.c: the texture detail auto picks on this card, 0 full to 2 low
int vk_mesh_shader_native(void); // vk_device.c: the GPU has mesh shaders (the mesh emulation does nothing)
int vk_low_end_card(void); // vk_device.c: no mesh shaders, or under 8 GiB of video memory
int vk_exec_replay_workers_auto(const char **why); // vk_exec.c: replay_workers = auto's count on this PC
double frame_step_limit_hz(void);
int frame_limit_adjustable(void);
int rtss_limit_state(void);
@@ -1451,8 +1452,17 @@ static void tab_game(float scale) {
// it. A row whose setting does nothing without another one (rw_init) is greyed out until that one is set. The two
// rows under the workers are switches: on writes the value measured best (4 groups at once, groups from 16 draws),
// and any other value set in nx2navi.ini shows as on and stays.
opt_slider("NX2NAVI_REPLAY_WORKERS", UI_MENU_REPLAY_WORKERS, 0, 8, UI_MENU_REPLAY_WORKERS_DESC, 1, "%.0f");
const bool workers = cfg_int("NX2NAVI_REPLAY_WORKERS") > 0, pipe = workers && cfg_int("NX2NAVI_REPLAY_PIPE") > 0;
// Automatic first, the default (vk_exec.c vk_exec_replay_workers_auto), named with the count it gives on this PC
{
static const char *const vals[] = {"auto", "0", "1", "2", "3", "4", "5", "6", "7", "8"};
static char auto_name[64];
snprintf(auto_name, sizeof auto_name, "%s (%d)", menu_text(UI_MENU_AUTO), vk_exec_replay_workers_auto(nullptr));
const char *names[] = {auto_name, "0", "1", "2", "3", "4", "5", "6", "7", "8"};
opt_choice_list("NX2NAVI_REPLAY_WORKERS", UI_MENU_REPLAY_WORKERS, vals, names, 10, UI_MENU_REPLAY_WORKERS_DESC);
}
const char *wset = cfg_value("NX2NAVI_REPLAY_WORKERS");
const bool workers = (!strcmp(wset, "auto") ? vk_exec_replay_workers_auto(nullptr) : atoi(wset)) > 0,
pipe = workers && cfg_int("NX2NAVI_REPLAY_PIPE") > 0;
if (!workers) opt_disable_next(UI_MENU_NEEDS_WORKERS);
opt_toggle_value("NX2NAVI_REPLAY_PIPE", UI_MENU_REPLAY_PIPE, UI_MENU_REPLAY_PIPE_DESC, "4", "0");
if (!pipe) opt_disable_next(workers ? UI_MENU_NEEDS_PIPE : UI_MENU_NEEDS_WORKERS);

View File

@@ -507,10 +507,10 @@ static const char *const text_en[UI_TEXT_COUNT] = {
"prepares frames on a thread of their own. Measured on a Radeon RX 580 only: it can show a dark or flickering "
"frame now and then, so leave it off on a newer card.",
[UI_MENU_REPLAY_WORKERS] = "CPU helper threads (experimental)",
[UI_MENU_REPLAY_WORKERS_DESC] = "Uses more of your CPU's cores to prepare each frame for the graphics card. 0 turns it "
"off. 3 gave about 20% more frames a second on an 8-core CPU, and 5 a little more "
"with the settings below. Some players' games crashed with this on: set it back to "
"0 if yours does.",
[UI_MENU_REPLAY_WORKERS_DESC] = "Uses more of your CPU's cores to prepare each frame for the graphics card. Automatic "
"picks the number for your CPU: half its cores less one, none on four cores or fewer "
"and none on RX 6000 and older Radeon cards. 0 turns it off. 3 gave about 20% more "
"frames a second on an 8-core CPU. If your game crashes with this on, set it to 0.",
[UI_MENU_REPLAY_PIPE] = "Helpers work ahead (experimental)",
[UI_MENU_REPLAY_PIPE_DESC] = "The game carries on with the rest of the frame while the helper threads are still "
"busy, instead of waiting for them. A small gain: 1 to 2% more frames a second.",

View File

@@ -1836,8 +1836,9 @@ static void op_one(VkExec *ex, const op_hdr *h) {
}
}
// ---- replay workers (NX2NAVI_REPLAY_WORKERS, default 0: the replay records every op on the submitting thread; the
// Game tab's CPU helper threads row sets it)
// ---- replay workers (NX2NAVI_REPLAY_WORKERS, default auto: as many as the CPU's cores make worth it,
// vk_exec_replay_workers_auto; 0: the replay records every op on the submitting thread; the Game tab's CPU helper
// threads row sets it)
// 3 workers: on an RX 7900 XT with a Ryzen 7 5800X3D, Hyrule Field standing went from 41.2 to 50.4 fps (2026-11-12, one
// batch), with the same pictures there and in Kokiri Forest, Kakariko, Goron City, a graveyard house and Gerudo
// Valley; 5 or 7 workers measured the same as 3. Default 0 again after players' games crashed with 3 on by default
@@ -2047,9 +2048,36 @@ static void rw_main(void *arg) {
}
}
// NX2NAVI_REPLAY_WORKERS = auto: the workers this PC gets. The game's own threads, the submitting thread, the present
// thread and the pipeline builders already keep about half of the cores busy, and on the 8-core CPU above 3 workers
// gave all there was (5 and 7 measured the same), so the count is half the physical cores less one: 2 on 6 cores, 3 on
// 8, 5 on 12, at most 6. Four cores or fewer get none: a worker would take a core from the game's threads. AMD's
// Windows driver for RX 6000 and older (vk_caps.amd_sc) gets none either, as that is where a player's device was lost
// with workers on and the cause is not known; a number set in nx2navi.ini or the menu still applies there. Physical
// cores of the process's affinity (plat_cpu_cores), or half the usable logical processors where that is unknown.
#define RW_AUTO_MAX 6
int vk_exec_replay_workers_auto(const char **why) {
uint64_t all, first;
int cores = plat_cpu_cores(&all, &first);
if (cores <= 0) cores = (int)plat_cpu_usable() / 2;
int n = cores <= 4 ? 0 : cores / 2 - 1;
if (n > RW_AUTO_MAX) n = RW_AUTO_MAX;
const char *w = cores <= 4 ? "four cores or fewer" : "half the cores less one";
if (vk_caps.amd_sc) n = 0, w = "AMD's driver for RX 6000 and older, where a game with workers lost its device";
if (why) *why = w;
return n;
}
static void rw_init(void) {
int n = cfg_int("NX2NAVI_REPLAY_WORKERS");
const char *set = cfg_value("NX2NAVI_REPLAY_WORKERS"), *why = NULL;
int autoset = !strcmp(set, "auto");
int n = autoset ? vk_exec_replay_workers_auto(&why) : atoi(set);
RW.n = n < 0 ? 0 : n > RW_MAX ? RW_MAX : n;
if (autoset) {
uint64_t all, first;
rt_log("vk: replay workers automatic: %d for %d physical cores (%s; NX2NAVI_REPLAY_WORKERS = a number sets "
"them)", RW.n, plat_cpu_cores(&all, &first), why);
}
for (int k = 0; k < RW.n; k++) {
RwWorker *w = calloc(1, sizeof *w);
if (!w) rt_fatal("vk: out of memory (replay worker)");