diff --git a/runtime/kernel.c b/runtime/kernel.c index 194e3e1b..128f1b46 100644 --- a/runtime/kernel.c +++ b/runtime/kernel.c @@ -7,6 +7,7 @@ #include "hle.h" #include "kernel.h" +#include "platform.h" #include "prof.h" // ------------------------------------------------------------------ results @@ -394,6 +395,7 @@ void rt_unwind_point(void) { static void *thread_main(void *arg) { KThread *t = arg; t_cur = t; + if (t->id == 1) plat_thread_elevate(); { char pn[32]; snprintf(pn, sizeof pn, "guest-%x", t->handle); diff --git a/runtime/nvn/objects.c b/runtime/nvn/objects.c index 8fe09649..0578a4c3 100644 --- a/runtime/nvn/objects.c +++ b/runtime/nvn/objects.c @@ -5,6 +5,7 @@ #include "../framepace.h" #include "../game_sites.h" #include "../kernel.h" +#include "../platform.h" #include "../prof.h" #include "../perfmon.h" #include "nvn.h" @@ -224,6 +225,7 @@ static void qop_run(const QOp *o) { static void *queue_thread(void *arg) { (void)arg; prof_thread_name("nvn-queue"); + plat_thread_elevate(); kernel_sample_host_thread(); extern void control_prof_thread(const char *name); // runtime/control_prof.c (`threads`) control_prof_thread("render"); diff --git a/runtime/nvn/vulkan/record.c b/runtime/nvn/vulkan/record.c index 2d13fae9..6f6dfa31 100644 --- a/runtime/nvn/vulkan/record.c +++ b/runtime/nvn/vulkan/record.c @@ -181,6 +181,7 @@ static uint64_t spin_for_work(uint64_t t) { // the published end once it moves static void *run(void *arg) { (void)arg; prof_thread_name("vk-record"); + plat_thread_elevate(); extern void control_prof_thread(const char *name); // runtime/control_prof.c (`threads`) control_prof_thread("recorder"); uint64_t t = 0; diff --git a/runtime/platform.h b/runtime/platform.h index a359f0d7..108f366b 100644 --- a/runtime/platform.h +++ b/runtime/platform.h @@ -54,6 +54,7 @@ uint64_t plat_now_ns(void); // monotonic void plat_sleep_until_ns(uint64_t t); // precise wait for plat_now_ns() >= t void plat_wait_until_ns(uint64_t t); // wait for plat_now_ns() >= t without spinning (the OS timer's precision) void plat_thread_realtime(uint64_t period_ns); // periodic timing thread (vsync): don't coalesce its timers +void plat_thread_elevate(void); // latency-critical thread (game main, render, recorder): favor P-cores on hybrid architectures int plat_cpu_count(void); // logical processors this process may run on (its affinity, after plat_cpu_placement) void plat_cpu_placement(void); // (game start) one logical processor per core where plat_one_per_core_default says // Whether the game runs on one logical processor per core when SAVAGEWIND_ONE_PER_CORE doesn't say (cores: physical diff --git a/runtime/platform/linux.c b/runtime/platform/linux.c index 9cf53677..158c5a8e 100644 --- a/runtime/platform/linux.c +++ b/runtime/platform/linux.c @@ -134,6 +134,8 @@ void plat_thread_realtime(uint64_t period_ns) { setpriority(PRIO_PROCESS, (id_t)gettid(), -10); // (per thread on Linux; fails quietly without CAP_SYS_NICE) } +void plat_thread_elevate(void) {} + // ---- process // stdout / stderr go to savagewind.log in the data folder (runtime/paths.h; rt_paths_init runs later, so the same rule // is applied here). Processes started by this one (the start screen re-executing itself for the game) append to the diff --git a/runtime/platform/macos.c b/runtime/platform/macos.c index e251e879..1d3c0d09 100644 --- a/runtime/platform/macos.c +++ b/runtime/platform/macos.c @@ -83,6 +83,8 @@ void plat_thread_realtime(uint64_t period_ns) { THREAD_TIME_CONSTRAINT_POLICY_COUNT); } +void plat_thread_elevate(void) {} + const char *plat_log_path(void) { return ""; } // (the log goes to stderr) bool plat_exe_path(char *buf, size_t size) { diff --git a/runtime/platform/windows.c b/runtime/platform/windows.c index 446efd9f..75c2bc7b 100644 --- a/runtime/platform/windows.c +++ b/runtime/platform/windows.c @@ -325,6 +325,10 @@ void plat_thread_realtime(uint64_t period_ns) { SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL); } +void plat_thread_elevate(void) { + SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL); +} + // ---- process // Release builds are GUI-subsystem executables (no console window): stdout / stderr go to savagewind.log next to the // exe, and a fatal error shows a message box (plat_fatal_notice) instead of vanishing. Processes started by this one @@ -433,9 +437,11 @@ void plat_init(void) { // never share a core with another busy thread of the game. With fewer cores than the game's busy threads (main, two job // workers, render, recorder, audio, file loading) they queue for one instead: by default with SMT and 4 or more // physical cores (plat_one_per_core_default). SAVAGEWIND_ONE_PER_CORE=1: always, 0: never. Called when the game -// starts, so what the start screen runs (the game code build) keeps every logical processor. Under Wine / Proton the -// Linux kernel schedules the threads, and there the Steam Deck measured one processor per core as a loss -// (platform.h): the default keeps every logical processor, as the Linux build does. +// starts, so what the start screen runs (the game code build) keeps every logical processor. On hybrid architectures +// (Intel 12th+ Gen, Core Ultra, heterogeneous P/E cores) keeping all logical processors avoids starving P-core SMT +// execution slots and forcing critical threads onto low-clock E-cores. Under Wine / Proton the Linux kernel schedules +// the threads, and there the Steam Deck measured one processor per core as a loss (platform.h): the default keeps +// every logical processor, as the Linux build does. void plat_cpu_placement(void) { DWORD_PTR all, sys, one = 0; if (!GetProcessAffinityMask(GetCurrentProcess(), &all, &sys)) return; @@ -445,18 +451,28 @@ void plat_cpu_placement(void) { DWORD len = 0; GetLogicalProcessorInformationEx(RelationProcessorCore, NULL, &len); char *buf = len ? malloc(len) : NULL; + BYTE min_eff = 255, max_eff = 0; if (buf && GetLogicalProcessorInformationEx(RelationProcessorCore, (void *)buf, &len)) for (DWORD o = 0; o < len;) { SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *e = (void *)(buf + o); DWORD_PTR m = e->Processor.GroupMask[0].Group == 0 ? e->Processor.GroupMask[0].Mask & all : 0; - if (m) cores++, one |= m & -m; // (the core's first logical processor) + if (m) { + cores++; + one |= m & -m; // (the core's first logical processor) + BYTE eff = e->Processor.EfficiencyClass; + if (eff < min_eff) min_eff = eff; + if (eff > max_eff) max_eff = eff; + } o += e->Size; } free(buf); + bool hybrid = (cores > 0 && min_eff != max_eff); const char *x = getenv("SAVAGEWIND_ONE_PER_CORE"); - bool on = one && (x ? atoi(x) != 0 : !wine && plat_one_per_core_default(cores, logical)); - fprintf(stderr, "[cpu] %d cores, %d logical processors: %s%s\n", cores, logical, - on ? "one logical processor per core" : "all logical processors", wine && !x ? " (Wine)" : ""); + bool on = one && (x ? atoi(x) != 0 : !wine && !hybrid && plat_one_per_core_default(cores, logical)); + fprintf(stderr, "[cpu] %d cores%s, %d logical processors: %s%s\n", cores, + hybrid ? " (hybrid P/E)" : "", logical, + on ? "one logical processor per core" : "all logical processors", + wine && !x ? " (Wine)" : hybrid && !x ? " (hybrid architecture)" : ""); if (on) SetProcessAffinityMask(GetCurrentProcess(), one); }