Merge task/pcore: frame-critical threads prefer the P-cores of a hybrid CPU

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	runtime/vk_rec.c
This commit is contained in:
Valiantsin Dzerakh committed 2026-11-08 12:03:08 +03:00
commit b16c4c8938
10 files changed
+113 -1

No files matched your search

+1 -1
View File
@@ -50,7 +50,7 @@ static const char *const known[] = {
"MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MOTION", "MOUSE_AIM_SCALE", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOVIE_ASPECT", "MOVIE_HOST", "MOVIE_PACE",
"MVOBJ_ALL", "MVOBJ_BANKS", "MVOBJ_EQUAL", "MVOBJ_EXACT", "MVOBJ_EXEC", "MVOBJ_NOC4", "MVOBJ_ONLY", "MVOBJ_TIMES", "MVPREV_CHECK", "MVPREV_GROW", "MVPREV_SKIP", "MVPREV_TRUST", "MV_CHECK",
"MV_SURVEY",
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_SCENE", "PCORE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
"PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_BENCH", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
"PROFILE_MAIN", "PROFILE_RENDER", "PROMPTS", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_ASYNC", "REC_END", "REC_SPIN_US", "REEXEC",
"RENDER_SCALE", "REPLAY_ASYNC",
+4
View File
@@ -36,6 +36,10 @@ void perf_end(int kind, uint64_t t0); // counts render-thread time onl
void perf_replay(uint64_t t0); // replay (NVN -> Vulkan recording) time on any thread, waits inside it included
void perf_replay_thread(void); // the calling thread replays for the render thread: perf_end counts its waits
void perf_main_thread(void); // the calling thread is the guest main thread (TOTK_PROFILE_MAIN)
// The calling thread is one every frame goes through: on a hybrid CPU it prefers the P-cores (main.c, TOTK_PCORE);
// the first thread of each role logs it.
enum { PCORE_MAIN, PCORE_RENDER, PCORE_REPLAY, PCORE_RECORDER, PCORE_PRESENT, PCORE_N };
void perf_core_thread(int role);
// Events per 60-frame window: graphics pipelines finished by the background workers, draws skipped while pending,
// first texture uploads and re-uploads after CPU writes, draws skipped while a shader translates in the background,
// draws that waited for their pending pipeline and got it, draws into a target the previous frame did not render that
+1
View File
@@ -1561,6 +1561,7 @@ uint32_t kern_main_thread(Cpu **cpu) {
t->cpu->sp = t->stack_top;
t->tid = plat_thread_id();
perf_main_thread();
perf_core_thread(PCORE_MAIN);
PlatIntr *self = ap_open_self();
lock();
t->hthr = self;
+10
View File
@@ -536,6 +536,16 @@ int main(int argc, char **argv) {
if (power >= 0)
rt_log("power: %s", power ? "OS power throttling off (EcoQoS, timer resolution in the background)"
: "OS power throttling: the opt-out was refused");
// Hybrid CPUs: the threads every frame goes through prefer the P-cores (perf_core_thread). TOTK_PCORE=0: off.
if (rt_knob("PCORE", "1")) {
char why[160];
int n = plat_perf_core_setup(why, sizeof why);
if (n) rt_log("power: %s; the game's main and render threads and the vk replay, recorder and present threads "
"prefer the %d P-core ones (TOTK_PCORE=0 turns it off)", why, n);
else rt_log("power: %s: no P-core preference", why);
} else {
rt_log("power: TOTK_PCORE=0: no P-core preference");
}
cfg = (RtConfig){.exefs = rt_default_exefs, .romfs = rt_default_romfs, .profile = rt_default_profile,
.gpu = cfg_get("TOTK_GPU")};
portable_paths(&cfg); // command-line paths below still win
+9
View File
@@ -62,6 +62,15 @@ uint64_t perf_begin(void) { return kern_now_ns(); }
void perf_replay_thread(void) { prof_replay_tid = plat_thread_id(); }
void perf_main_thread(void) { prof_main_tid = plat_thread_id(); }
void perf_core_thread(int role) {
static const char *const names[] = {"game main", "game render", "vk replay", "vk recorder", "vk present"};
static int32_t said;
int r = plat_thread_perf_core();
if (r < 0 || role < 0 || role >= PCORE_N) return;
if (__atomic_fetch_or(&said, 1 << role, __ATOMIC_RELAXED) & (1 << role)) return;
rt_log("power: the %s thread %s the P-cores", names[role], r ? "prefers" : "could not select");
}
void perf_end(int kind, uint64_t t0) {
if (!t0) return;
uint32_t tid = plat_thread_id();
+6
View File
@@ -483,6 +483,12 @@ void plat_timer_1ms(void) { prctl(PR_SET_TIMERSLACK, 1000ul, 0, 0, 0); }
int plat_power_full(void) { return -1; }
int plat_perf_core_setup(char *why, size_t n) {
snprintf(why, n, "not done on Linux");
return 0;
}
int plat_thread_perf_core(void) { return -1; }
// clock_nanosleep wakes within the timer slack (50 us by default) of its deadline; the last PLAT_SPIN_NS are spent
// yielding so the deadline is met closely, as on Windows.
#define PLAT_SPIN_NS 100000u
+5
View File
@@ -100,6 +100,11 @@ uint64_t plat_counter(void); // the OS's raw monotonic counter (QPC
uint64_t plat_counter_hz(void);
void plat_timer_1ms(void); // asks the OS for timed waits accurate to 1 ms or better (call before threads)
int plat_power_full(void); // opts the process out of OS power throttling: 1 done, 0 refused, -1 no such thing
// Hybrid CPUs: finds the performance-core logical processors the process may use, once, before any thread asks; returns
// their count (0: none to prefer, why says which case). plat_thread_perf_core: the calling thread prefers them; 1 done,
// 0 refused, -1 nothing selected.
int plat_perf_core_setup(char *why, size_t n);
int plat_thread_perf_core(void);
// t * num / den for a fixed num / den without a 128-bit division per call (__udivti3 took 5% of the game's running
// samples through plat_now_ns and rt_ticks): t * q + (t * f >> 64) with q = num / den and f = (num % den) * 2^64 / den.
// Monotonic in t and at most one below the exact quotient while t * (num % den) / den fits 64 bits.
+72
View File
@@ -296,6 +296,78 @@ uint32_t plat_cpu_usable(void) {
return plat_cpu_count();
}
// ---- performance cores of a hybrid CPU
// An Intel CPU with P-cores and E-cores (a Core 5 210H: 4 P-cores with two threads each, 4 E-cores) runs a thread on an
// E-core at about 60% of a P-core's speed, and the frame waits for its slowest critical thread. The few threads every
// frame goes through ask for the P-cores (plat_thread_perf_core): their selected CPU sets are the logical processors
// above the lowest efficiency class that the process affinity allows. A thread's CPU sets bind it like an affinity
// mask, except that processors the system reserves for itself are left out instead of failing. Nothing is selected
// on a CPU with one efficiency class, or when the affinity already leaves only P-cores or only E-cores. LAS Recomp
// does the same for its game, render and vsync threads (runtime/platform/windows.c plat_thread_perf_core).
static ULONG *pc_ids; // the CPU set ids of the allowed P-core logical processors; NULL: none selected
static ULONG pc_n;
int plat_perf_core_setup(char *why, size_t n) {
ULONG len = 0;
GetSystemCpuSetInformation(NULL, 0, &len, GetCurrentProcess(), 0);
uint8_t *buf = len ? malloc(len) : NULL;
if (!buf || !GetSystemCpuSetInformation((PSYSTEM_CPU_SET_INFORMATION)buf, len, &len, GetCurrentProcess(), 0)) {
free(buf);
snprintf(why, n, "no CPU set information (error %lu)", GetLastError());
return 0;
}
// The process affinity mask covers its first processor group only: it filters on a single-group system.
DWORD_PTR proc = 0, sys = 0;
int filter = GetActiveProcessorGroupCount() == 1 && GetProcessAffinityMask(GetCurrentProcess(), &proc, &sys) && proc;
int lo = 255, hi = -1, all = 0;
for (ULONG off = 0; off < len;) {
SYSTEM_CPU_SET_INFORMATION *i = (SYSTEM_CPU_SET_INFORMATION *)(buf + off);
if (!i->Size) break;
if (i->Type == CpuSetInformation) {
int ec = i->CpuSet.EfficiencyClass;
lo = ec < lo ? ec : lo, hi = ec > hi ? ec : hi, all++;
}
off += i->Size;
}
if (!all || hi <= lo) {
free(buf);
snprintf(why, n, "not a hybrid CPU (%d logical processors, one efficiency class)", all);
return 0;
}
ULONG *ids = calloc((size_t)all, sizeof *ids);
int np = 0, ne = 0, ok_p = 0, ok_e = 0;
for (ULONG off = 0; ids && off < len;) {
SYSTEM_CPU_SET_INFORMATION *i = (SYSTEM_CPU_SET_INFORMATION *)(buf + off);
if (!i->Size) break;
if (i->Type == CpuSetInformation) {
int perf = i->CpuSet.EfficiencyClass > lo;
int allowed = !filter || (i->CpuSet.Group == 0 && i->CpuSet.LogicalProcessorIndex < 64 &&
(proc >> i->CpuSet.LogicalProcessorIndex) & 1);
perf ? np++ : ne++;
if (allowed) perf ? ok_p++ : ok_e++;
if (allowed && perf) ids[pc_n++] = i->CpuSet.Id;
}
off += i->Size;
}
free(buf);
if (!ids || !ok_p || !ok_e) {
free(ids);
pc_n = 0;
snprintf(why, n, "hybrid CPU, %d P-core + %d E-core logical processors; the affinity allows %d + %d, so it decides",
np, ne, ok_p, ok_e);
return 0;
}
pc_ids = ids;
snprintf(why, n, "hybrid CPU, %d P-core + %d E-core logical processors%s", np, ne,
ok_p != np || ok_e != ne ? " (affinity-limited)" : "");
return (int)pc_n;
}
int plat_thread_perf_core(void) {
if (!pc_ids) return -1;
return SetThreadSelectedCpuSets(GetCurrentThread(), pc_ids, pc_n) ? 1 : 0;
}
// PLAT_PRIO_* are the THREAD_PRIORITY_* values.
void plat_thread_priority(int prio) { SetThreadPriority(GetCurrentThread(), prio); }
void plat_thread_set_priority(PlatThread *t, int prio) { SetThreadPriority(t->h, prio); }
+4
View File
@@ -49,6 +49,7 @@ static void wait_until(uint64_t at) {
static void present_main(void *arg) {
(void)arg;
perf_core_thread(PCORE_PRESENT);
uint64_t last = 0, last_queued = 0;
plat_mutex_lock(&pl);
for (;;) {
@@ -201,6 +202,7 @@ static uint64_t submit_sync(const uint64_t *handles, int n) {
static void replay_main(void *arg) {
(void)arg;
perf_replay_thread();
perf_core_thread(PCORE_REPLAY);
plat_mutex_lock(&rl);
for (;;) {
while (!rq_n) plat_cond_wait(&rcv, &rl);
@@ -326,6 +328,8 @@ void gfx_exit_log(void) {
}
static void vk_be_present(const tex_t *t, const int crop[4], uint64_t at_ns) {
static _Thread_local int pcore; // the game's render thread: the one that presents
if (!pcore) pcore = 1, perf_core_thread(PCORE_RENDER);
vk_sync_start();
if (!gfx_replay_async()) {
uint64_t frame = __atomic_add_fetch(&gfx_counters.frames, 1, __ATOMIC_RELAXED);
+1
View File
@@ -81,6 +81,7 @@ static void rec_main(void *arg) {
(void)arg;
plat_thread_priority(rec_prio);
on_rec = 1;
perf_core_thread(PCORE_RECORDER);
uint64_t t = 0;
for (;;) {
uint64_t h = __atomic_load_n(&Q.head, __ATOMIC_ACQUIRE);