perf(host): fast-path a64_cntpct_host on POSIX and restore memory counter queries #2491

Closed
Erfboom wants to merge 1 commit from perf/linux-cntpct-fastpath into main
Erfboom commented 2026-11-11 20:53:37 +00:00 (Migrated from github.com)

Problem

  1. a64_cntpct_host (which emulates CNTPCT_EL0 at 31.25 MHz and is queried frequently across game frames and timing loops) performed a 128-bit integer multiplication and 32-bit shift on every single invocation:
    (uint64_t)(((unsigned __int128)(now - base) * mult) >> 32)
    On Linux / POSIX systems, QueryPerformanceFrequency is fixed at 1 GHz (1000000000ULL). Because 1\text{ GHz} / 31.25\text{ MHz} = 32 exactly (2^5), this arithmetic can be replaced with a single right-shift by 5 bits ((now - base) >> 5).
  2. hos_process_memory dynamically looked up K32GetProcessMemoryInfo from kernel32.dll via GetProcAddress. On Linux/POSIX, K32GetProcessMemoryInfo is an inline function provided in <psapi.h> (reading /proc/self/status), so GetProcAddress always failed and process memory counters remained zero.

Solution

  • On non-Windows platforms, simplify a64_cntpct_host to (uint64_t)(now.QuadPart - base) >> 5, eliminating 128-bit multiplication overhead.
  • In hos_process_memory, include <psapi.h> directly on POSIX to populate resident set and private working set metrics accurately for memory logging and HUD display.

Impact

  • Accelerates frequent guest timer/tick queries on Linux.
  • Restores working set and private memory reporting in Linux debug/profiling logs.
  • Verified with tests/run_all.py (359 passed, 0 failed).
## Problem 1. `a64_cntpct_host` (which emulates CNTPCT_EL0 at 31.25 MHz and is queried frequently across game frames and timing loops) performed a 128-bit integer multiplication and 32-bit shift on every single invocation: `(uint64_t)(((unsigned __int128)(now - base) * mult) >> 32)` On Linux / POSIX systems, `QueryPerformanceFrequency` is fixed at 1 GHz (`1000000000ULL`). Because $1\text{ GHz} / 31.25\text{ MHz} = 32$ exactly ($2^5$), this arithmetic can be replaced with a single right-shift by 5 bits (`(now - base) >> 5`). 2. `hos_process_memory` dynamically looked up `K32GetProcessMemoryInfo` from `kernel32.dll` via `GetProcAddress`. On Linux/POSIX, `K32GetProcessMemoryInfo` is an inline function provided in `<psapi.h>` (reading `/proc/self/status`), so `GetProcAddress` always failed and process memory counters remained zero. ## Solution - On non-Windows platforms, simplify `a64_cntpct_host` to `(uint64_t)(now.QuadPart - base) >> 5`, eliminating 128-bit multiplication overhead. - In `hos_process_memory`, include `<psapi.h>` directly on POSIX to populate resident set and private working set metrics accurately for memory logging and HUD display. ## Impact - Accelerates frequent guest timer/tick queries on Linux. - Restores working set and private memory reporting in Linux debug/profiling logs. - Verified with `tests/run_all.py` (359 passed, 0 failed).
Joey-Magnus (Migrated from github.com) reviewed 2026-11-11 21:46:28 +00:00
Joey-Magnus (Migrated from github.com) left a comment

I don't think this changes anything at runtime, so I'd suggest closing it.

  • a64_cntpct_host: on POSIX, QueryPerformanceFrequency is 1e9, so main's mult is (31250000 << 32) / 1e9 = 2^27, and (d * 2^27) >> 32 is already exactly d >> 5. The PR returns the same values. It saves one 64x64 multiply beside a clock_gettime call that costs far more, and it adds a second copy of the function that silently depends on the 1 GHz frequency.
  • hos_process_memory: the diagnosis is right (GetModuleHandleA("kernel32.dll") is NULL on Linux, so the counters read 0). But its only caller is gpu/render/rx_core.cpp:1477, the WebGPU renderer, and nx2/build.py:886 builds that only when not LINUX. Nothing reads these counters on Linux. The Vulkan back end calls K32GetProcessMemoryInfo directly (vk_texture.cpp:3826).
    If you have a profile showing a64_cntpct_host is hot, I'd like to see it, but the arithmetic can't be the cost.
I don't think this changes anything at runtime, so I'd suggest closing it. - `a64_cntpct_host`: on POSIX, `QueryPerformanceFrequency` is 1e9, so main's `mult` is `(31250000 << 32) / 1e9` = 2^27, and `(d * 2^27) >> 32` is already exactly `d >> 5`. The PR returns the same values. It saves one 64x64 multiply beside a `clock_gettime` call that costs far more, and it adds a second copy of the function that silently depends on the 1 GHz frequency. - `hos_process_memory`: the diagnosis is right (`GetModuleHandleA("kernel32.dll")` is NULL on Linux, so the counters read 0). But its only caller is `gpu/render/rx_core.cpp:1477`, the WebGPU renderer, and `nx2/build.py:886` builds that only when `not LINUX`. Nothing reads these counters on Linux. The Vulkan back end calls `K32GetProcessMemoryInfo` directly (`vk_texture.cpp:3826`). If you have a profile showing `a64_cntpct_host` is hot, I'd like to see it, but the arithmetic can't be the cost.
hckrman101 (Migrated from github.com) reviewed 2026-11-11 22:20:29 +00:00
hckrman101 (Migrated from github.com) left a comment

Thanks for looking at this. I checked it against main and agree with @Joey-Magnus's review: neither half changes anything at runtime on Linux.

  • a64_cntpct_host: the POSIX QueryPerformanceFrequency shim (host/posix/windows.h) returns 1e9, so main's mult is already exactly 2^27 and (d * 2^27) >> 32 == d >> 5 for every d. The new branch returns identical values, and the multiply it removes sits next to a clock_gettime that costs far more. It also leaves two copies of the function, one of which silently assumes the shim's 1 GHz frequency. If you have a profile where this shows up, please share it.
  • hos_process_memory: right diagnosis (the GetProcAddress lookup fails on Linux), but its only caller is the WebGPU renderer in gpu/render/rx_core.cpp, which nx2/build.py doesn't build on Linux. The Vulkan back end already calls K32GetProcessMemoryInfo from host/posix/psapi.h directly, so the Linux memory log lines aren't zero today.

No overlap with your other open PRs (#2462, #2467, #2469, #2482, #2487 touch gpu/vk/ and host/posix/, not host/mkw_glue.c). CI hasn't run yet (workflows are waiting for approval), but given the above I'd also suggest closing this one.

Thanks for looking at this. I checked it against main and agree with @Joey-Magnus's review: neither half changes anything at runtime on Linux. - `a64_cntpct_host`: the POSIX `QueryPerformanceFrequency` shim (`host/posix/windows.h`) returns 1e9, so main's `mult` is already exactly 2^27 and `(d * 2^27) >> 32 == d >> 5` for every `d`. The new branch returns identical values, and the multiply it removes sits next to a `clock_gettime` that costs far more. It also leaves two copies of the function, one of which silently assumes the shim's 1 GHz frequency. If you have a profile where this shows up, please share it. - `hos_process_memory`: right diagnosis (the `GetProcAddress` lookup fails on Linux), but its only caller is the WebGPU renderer in `gpu/render/rx_core.cpp`, which `nx2/build.py` doesn't build on Linux. The Vulkan back end already calls `K32GetProcessMemoryInfo` from `host/posix/psapi.h` directly, so the Linux memory log lines aren't zero today. No overlap with your other open PRs (#2462, #2467, #2469, #2482, #2487 touch `gpu/vk/` and `host/posix/`, not `host/mkw_glue.c`). CI hasn't run yet (workflows are waiting for approval), but given the above I'd also suggest closing this one.
Erfboom commented 2026-11-11 23:48:18 +00:00 (Migrated from github.com)

Thanks @Joey-Magnus and @hckrman101 for checking this against main — agreed that POSIX QueryPerformanceFrequency=1e9 makes (d * 2^27) >> 32 already reduce to d >> 5, and the Vulkan backend queries K32GetProcessMemoryInfo directly rather than going through hos_process_memory. Closing this out!

Thanks @Joey-Magnus and @hckrman101 for checking this against main — agreed that POSIX QueryPerformanceFrequency=1e9 makes (d * 2^27) >> 32 already reduce to d >> 5, and the Vulkan backend queries K32GetProcessMemoryInfo directly rather than going through hos_process_memory. Closing this out!
Commenting is not possible because the repository is archived.
No description provided.