Intel hybrid CPUs: the reserved core (Presentation Thread / replay) lands on an E-core #1286

Closed
opened 2026-11-09 20:49:04 +00:00 by hedigrrr1 · 4 comments
hedigrrr1 commented 2026-11-09 20:49:04 +00:00 (Migrated from github.com)

Hookshot version

v2.0.11 (source d6369f1e)

What kind of problem

Low fps or stutter

What happened

Version: v2.0.11 (source d6369f1e), Windows 11, i5-14600KF (6 P-cores + 8 E-cores, 20 logical processors), RTX 4080 SUPER, no DLSS, no frame generation, 1080p.

What happens: hos_reserved_cpus() keeps the core with the highest processor mask. On Intel 12th-14th gen the E-cores come after the P-cores, so the reserved core is an E-core. The log says:

threads: Presentation Thread on the reserved core (logical processors 0x80000; HOS_PIN_RENDER=0: off)

0x80000 is logical processor 19, an E-core. In dense parts of Kokiri Forest (3,000-4,000 draws a frame) the replay takes 19-23 ms a frame (vk: a frame's replay time: submit 19-23), the GPU is busy only 6-8 ms (vk: GPU time) and the game runs at 37-45 fps.

Proposed fix (below): read PROCESSOR_RELATIONSHIP.EfficiencyClass with GetLogicalProcessorInformationEx(RelationProcessorCore) and keep the last core of the highest class. On CPUs whose cores are all alike every class is 0, so the choice is the last core, as now.

Workaround meanwhile: HOS_RESERVE_CORE=0 in hookshot_env.txt.

--- a/host/hos_thread.c
+++ b/host/hos_thread.c
@@ -555,8 +555,8 @@
         if (strcmp(s[i].pat, "*") ? strstr(name, s[i].pat) != NULL : 1) return &s[i];
     return NULL;
 }
-/* The last physical core (its logical processors' mask), kept for the Vulkan back end's replay thread (gpu/vk
-   vk_replay.cpp pins it there): the game's threads run on the others. The replay is on the frame's critical path
+/* The last physical core of the fastest kind (its logical processors' mask), kept for the Vulkan back end's replay
+   thread (gpu/vk vk_replay.cpp pins it there): the game's threads run on the others. The replay is on the frame's critical path
    (MKW waits for the last frame's GPU work before it submits the next), and on a core shared with game threads
    it ran ~1.7x slower than alone. Only on machines with 8+ logical processors; HOS_RESERVE_CORE=0: none. */
 uint64_t hos_reserved_cpus(void) {
@@ -564,16 +564,29 @@
     if (mask != ~0ull) return mask;
     mask = 0;
     if (getenv("HOS_RESERVE_CORE") && !atoi(getenv("HOS_RESERVE_CORE"))) return mask;
-    SYSTEM_LOGICAL_PROCESSOR_INFORMATION info[256];
-    DWORD len = sizeof info;
-    if (!GetLogicalProcessorInformation(info, &len)) return mask;
-    int logical = 0;
+    /* (hookshot) On a hybrid CPU the last core is an efficiency core (Intel 12th-14th gen: the E-cores come after the
+       P-cores), slower than a performance core: an i5-14600KF pinned the Presentation Thread to logical processor 19
+       (an E-core) and replayed 19-23 ms a frame in dense parts of Kokiri Forest (37-45 fps), with the GPU busy 6-8 ms.
+       So the last core of the highest efficiency class is kept (PROCESSOR_RELATIONSHIP.EfficiencyClass, the byte after
+       Flags; 0 on CPUs whose cores are all alike, where this is the last core as before). */
+    DWORD len = 0;
+    GetLogicalProcessorInformationEx(RelationProcessorCore, NULL, &len);
+    SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *buf = len ? malloc(len) : NULL;
+    if (!buf || !GetLogicalProcessorInformationEx(RelationProcessorCore, buf, &len)) {
+        free(buf);
+        return mask;
+    }
+    int logical = 0, best = -1;
     uint64_t last = 0;
-    for (DWORD i = 0; i < len / sizeof info[0]; i++)
-        if (info[i].Relationship == RelationProcessorCore) {
-            logical += __builtin_popcountll((uint64_t)info[i].ProcessorMask);
-            if ((uint64_t)info[i].ProcessorMask > last) last = (uint64_t)info[i].ProcessorMask;
-        }
+    for (DWORD off = 0; off < len;) {
+        SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *e = (void *)((char *)buf + off);
+        uint64_t m = e->Processor.GroupMask[0].Group == 0 ? (uint64_t)e->Processor.GroupMask[0].Mask : 0;
+        int cls = ((const BYTE *)&e->Processor.Flags)[1];
+        logical += __builtin_popcountll(m);
+        if (m && (cls > best || (cls == best && m > last))) best = cls, last = m;
+        off += e->Size;
+    }
+    free(buf);
     if (logical >= 8) mask = last;
     return mask;
 }

Where in the game

Kokiri Forest (dense parts, fog, fast camera turns)

How often

Every time

Graphics card and driver version

NVIDIA GeForce RTX 4080 SUPER, driver 621.14

CPU, RAM and operating system

Intel Core i5-14600KF (6 P-cores + 8 E-cores, 20 logical processors), 32 GB, Windows 11

Changes from the defaults

No DLSS, no frame generation. Workaround tested: HOS_RESERVE_CORE=0 in hookshot_env.txt gives a steady 60 fps at native 1440p in the same places (replay ~8.4 ms a frame instead of 10-23 ms). The attached log is from the run without it.

Other software hooked into the game

No, nothing hooked in

hookshot_log.txt

Without the workaround (reserved core = E-core, logical processor 19), 1080p:
hookshot_log.3.txt
With HOS_RESERVE_CORE=0 in hookshot_env.txt, native 1440p, steady 60 fps:
hookshot_log.2.txt
(The second log ends with a crash when I switched frame generation to x8 by mistake; that is unrelated to this issue.)

Screenshots or video

No response

Before you send it

  • I'm on the newest release, or the problem is still there on it.
  • I searched the open issues for this problem.
  • I'm not attaching game files, keys, hookshot.exe, the .bin caches or saves.
### Hookshot version v2.0.11 (source d6369f1e) ### What kind of problem Low fps or stutter ### What happened **Version:** v2.0.11 (source d6369f1e), Windows 11, i5-14600KF (6 P-cores + 8 E-cores, 20 logical processors), RTX 4080 SUPER, no DLSS, no frame generation, 1080p. **What happens:** `hos_reserved_cpus()` keeps the core with the highest processor mask. On Intel 12th-14th gen the E-cores come after the P-cores, so the reserved core is an E-core. The log says: ``` threads: Presentation Thread on the reserved core (logical processors 0x80000; HOS_PIN_RENDER=0: off) ``` 0x80000 is logical processor 19, an E-core. In dense parts of Kokiri Forest (3,000-4,000 draws a frame) the replay takes 19-23 ms a frame (`vk: a frame's replay time: submit 19-23`), the GPU is busy only 6-8 ms (`vk: GPU time`) and the game runs at 37-45 fps. **Proposed fix (below):** read `PROCESSOR_RELATIONSHIP.EfficiencyClass` with `GetLogicalProcessorInformationEx(RelationProcessorCore)` and keep the last core of the highest class. On CPUs whose cores are all alike every class is 0, so the choice is the last core, as now. Workaround meanwhile: `HOS_RESERVE_CORE=0` in `hookshot_env.txt`. ```diff --- a/host/hos_thread.c +++ b/host/hos_thread.c @@ -555,8 +555,8 @@ if (strcmp(s[i].pat, "*") ? strstr(name, s[i].pat) != NULL : 1) return &s[i]; return NULL; } -/* The last physical core (its logical processors' mask), kept for the Vulkan back end's replay thread (gpu/vk - vk_replay.cpp pins it there): the game's threads run on the others. The replay is on the frame's critical path +/* The last physical core of the fastest kind (its logical processors' mask), kept for the Vulkan back end's replay + thread (gpu/vk vk_replay.cpp pins it there): the game's threads run on the others. The replay is on the frame's critical path (MKW waits for the last frame's GPU work before it submits the next), and on a core shared with game threads it ran ~1.7x slower than alone. Only on machines with 8+ logical processors; HOS_RESERVE_CORE=0: none. */ uint64_t hos_reserved_cpus(void) { @@ -564,16 +564,29 @@ if (mask != ~0ull) return mask; mask = 0; if (getenv("HOS_RESERVE_CORE") && !atoi(getenv("HOS_RESERVE_CORE"))) return mask; - SYSTEM_LOGICAL_PROCESSOR_INFORMATION info[256]; - DWORD len = sizeof info; - if (!GetLogicalProcessorInformation(info, &len)) return mask; - int logical = 0; + /* (hookshot) On a hybrid CPU the last core is an efficiency core (Intel 12th-14th gen: the E-cores come after the + P-cores), slower than a performance core: an i5-14600KF pinned the Presentation Thread to logical processor 19 + (an E-core) and replayed 19-23 ms a frame in dense parts of Kokiri Forest (37-45 fps), with the GPU busy 6-8 ms. + So the last core of the highest efficiency class is kept (PROCESSOR_RELATIONSHIP.EfficiencyClass, the byte after + Flags; 0 on CPUs whose cores are all alike, where this is the last core as before). */ + DWORD len = 0; + GetLogicalProcessorInformationEx(RelationProcessorCore, NULL, &len); + SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *buf = len ? malloc(len) : NULL; + if (!buf || !GetLogicalProcessorInformationEx(RelationProcessorCore, buf, &len)) { + free(buf); + return mask; + } + int logical = 0, best = -1; uint64_t last = 0; - for (DWORD i = 0; i < len / sizeof info[0]; i++) - if (info[i].Relationship == RelationProcessorCore) { - logical += __builtin_popcountll((uint64_t)info[i].ProcessorMask); - if ((uint64_t)info[i].ProcessorMask > last) last = (uint64_t)info[i].ProcessorMask; - } + for (DWORD off = 0; off < len;) { + SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX *e = (void *)((char *)buf + off); + uint64_t m = e->Processor.GroupMask[0].Group == 0 ? (uint64_t)e->Processor.GroupMask[0].Mask : 0; + int cls = ((const BYTE *)&e->Processor.Flags)[1]; + logical += __builtin_popcountll(m); + if (m && (cls > best || (cls == best && m > last))) best = cls, last = m; + off += e->Size; + } + free(buf); if (logical >= 8) mask = last; return mask; } ``` ### Where in the game Kokiri Forest (dense parts, fog, fast camera turns) ### How often Every time ### Graphics card and driver version NVIDIA GeForce RTX 4080 SUPER, driver 621.14 ### CPU, RAM and operating system Intel Core i5-14600KF (6 P-cores + 8 E-cores, 20 logical processors), 32 GB, Windows 11 ### Changes from the defaults No DLSS, no frame generation. Workaround tested: HOS_RESERVE_CORE=0 in hookshot_env.txt gives a steady 60 fps at native 1440p in the same places (replay ~8.4 ms a frame instead of 10-23 ms). The attached log is from the run without it. ### Other software hooked into the game No, nothing hooked in ### hookshot_log.txt Without the workaround (reserved core = E-core, logical processor 19), 1080p: [hookshot_log.3.txt](https://github.com/user-attachments/files/34599303/hookshot_log.3.txt) With HOS_RESERVE_CORE=0 in hookshot_env.txt, native 1440p, steady 60 fps: [hookshot_log.2.txt](https://github.com/user-attachments/files/34599340/hookshot_log.2.txt) (The second log ends with a crash when I switched frame generation to x8 by mistake; that is unrelated to this issue.) ### Screenshots or video _No response_ ### Before you send it - [x] I'm on the newest release, or the problem is still there on it. - [x] I searched the open issues for this problem. - [x] I'm not attaching game files, keys, hookshot.exe, the .bin caches or saves.
NotRexed commented 2026-11-09 21:20:49 +00:00 (Migrated from github.com)

Thanks for the patch and the EfficiencyClass approach. The same E-core finding (HOS_PIN_RENDER=0: about +24% fps on an i7-12700KF) is in #995; linking this there for the maintainer.

Thanks for the patch and the EfficiencyClass approach. The same E-core finding (HOS_PIN_RENDER=0: about +24% fps on an i7-12700KF) is in #995; linking this there for the maintainer.
NotRexed commented 2026-11-10 01:59:44 +00:00 (Migrated from github.com)

Fixed in the v2.0.12 branch, closing when it ships. The reserved core is now only used with 4 or more cores of the fastest kind and in the thread's own processor group, so on Intel hybrid CPUs it's a P-core, not an E-core.

Fixed in the v2.0.12 branch, closing when it ships. The reserved core is now only used with 4 or more cores of the fastest kind and in the thread's own processor group, so on Intel hybrid CPUs it's a P-core, not an E-core.
gdchiamulera commented 2026-11-10 05:13:37 +00:00 (Migrated from github.com)

Same here on an i9-12900K (8 P + 8 E, 24 logical processors), RTX 4080, Windows 11, v2.0.11. The log says:

threads: Presentation Thread on the reserved core (logical processors 0x800000; HOS_PIN_RENDER=0: off)

0x800000 is logical processor 23, which GetLogicalProcessorInformationEx(RelationProcessorCore) reports with EfficiencyClass 0 (E-core); logical 0–15 are the P-cores (EfficiencyClass 1). So hos_reserved_cpus() taking the highest ProcessorMask always lands on an E-core on these CPUs.

Measurements, same save and area (Kokiri Forest and around), 2560x1440 output, 100% resolution:

Default (Presentation Thread on E-core 23), 18 min HOS_PIN_RENDER=0, 5 min
Game fps in heavy scenes (≥1500 draws/frame) 47.0 (avg ~2,160 draws) 59.7 (avg ~2,300 draws)
Game fps in light scenes 58.1 59.4
Replay submit per frame (log avg) 14.1 ms 7.1 ms
5 s windows below 45 fps 41 of 214 1 of 52

GPU-bound wait stayed ~0.1 ms, so it was the replay on the CPU. Suggestion: pick the reserved core by the highest EfficiencyClass (then the highest mask among those); on non-hybrid CPUs all cores are class 0, so nothing changes there. HOS_PIN_RENDER=0 in hookshot_env.txt works as a workaround meanwhile.

Same here on an **i9-12900K** (8 P + 8 E, 24 logical processors), RTX 4080, Windows 11, v2.0.11. The log says: ``` threads: Presentation Thread on the reserved core (logical processors 0x800000; HOS_PIN_RENDER=0: off) ``` `0x800000` is logical processor 23, which `GetLogicalProcessorInformationEx(RelationProcessorCore)` reports with **EfficiencyClass 0 (E-core)**; logical 0–15 are the P-cores (EfficiencyClass 1). So `hos_reserved_cpus()` taking the highest `ProcessorMask` always lands on an E-core on these CPUs. Measurements, same save and area (Kokiri Forest and around), 2560x1440 output, 100% resolution: | | Default (Presentation Thread on E-core 23), 18 min | `HOS_PIN_RENDER=0`, 5 min | |---|---|---| | Game fps in heavy scenes (≥1500 draws/frame) | 47.0 (avg ~2,160 draws) | 59.7 (avg ~2,300 draws) | | Game fps in light scenes | 58.1 | 59.4 | | Replay `submit` per frame (log avg) | 14.1 ms | 7.1 ms | | 5 s windows below 45 fps | 41 of 214 | 1 of 52 | GPU-bound wait stayed ~0.1 ms, so it was the replay on the CPU. Suggestion: pick the reserved core by the highest `EfficiencyClass` (then the highest mask among those); on non-hybrid CPUs all cores are class 0, so nothing changes there. `HOS_PIN_RENDER=0` in `hookshot_env.txt` works as a workaround meanwhile.
NotRexed commented 2026-11-10 10:07:56 +00:00 (Migrated from github.com)

Fixed in v2.0.12 (https://github.com/NotRexed/Hookshot/releases/tag/v2.0.12): the reserved core is one of the fastest kind and in the thread's own processor group (#1158). Update with the Hookshot Builder (or the game's update offer) and rebuild. If it still happens on v2.0.12, reopen this with your hookshot_log.txt.

Fixed in **v2.0.12** (https://github.com/NotRexed/Hookshot/releases/tag/v2.0.12): the reserved core is one of the fastest kind and in the thread's own processor group (#1158). Update with the Hookshot Builder (or the game's update offer) and rebuild. If it still happens on v2.0.12, reopen this with your `hookshot_log.txt`.
Commenting is not possible because the repository is archived.
No milestone
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Hookshot#1286
No description provided.