#4981·shadPS4

[GAME BUG]: Diablo III (CUSA00242) - dynamic resolution collapses to sub-1080p at a locked 60fps; GetGpuClock64 returns wall-clock ns

Author: EviljubsCreated Sep 8, 2026Updated Sep 8, 2026

Checklist (we expect you to perform these steps before opening the issue)

  • I have searched for a similar issue in this repository and did not find one.
  • I am using an official build obtained from releases or updated one of those builds using its in-app updater.
  • I have re-dumped the game and performed a clean install without mods and the issue is still present.
  • I have disabled all patches and cheats and the issue is still present.
  • I have all the required system modules installed.

Describe the Bug

Diablo III uses dynamic resolution (1080p-2160p, targeting 60fps; not checkerboard). On stock v0.18.0 the game holds a locked 60fps at 16.7ms frame times, yet its DRS controller walks the resolution ladder down continuously and never recovers - sharp on menus and right after a level load, then visibly degrading over roughly 0.5-1s and staying there. The UI stays sharp while the 3D scene softens, which is the signature of the scene rendering into a shrinking viewport inside a full-size target.

The likely cause is GetGpuClock64() in src/video_core/amdgpu/pm4_cmds.h, which services EventWriteEop / ReleaseMem packets using DataSelect::GpuClock64 - how guest titles read GPU timestamps. It returns raw host wall-clock nanoseconds:

static u64 GetGpuClock64() {
    auto now = std::chrono::high_resolution_clock::now();
    auto duration = now.time_since_epoch();
    auto ticks = std::chrono::duration_cast<std::chrono::nanoseconds>(duration).count();
    return static_cast<u64>(ticks);
}

Two problems:

  1. Wrong units. Real GPU timestamps count at the GPU core clock. A guest computing elapsed = (t1 - t0) / sceGnmGetGpuCoreClockFrequency() divides nanoseconds by 911 MHz (Neo) or 800 MHz, over-reporting elapsed time by ~9.8% / ~25%.

  2. Wall-clock rather than GPU execution time. The value is sampled when the command processor processes the packet, so the delta across a frame absorbs presentation/vsync waiting. A vsync-locked title measures roughly a full frame period every frame regardless of actual GPU load, concludes it is permanently over budget, and can never scale back up because wall-clock time never improves.

Note GetGpuPerfCounter() directly below it does convert into the GPU clock domain via MultiplyAndDivide64, so the two are inconsistent.

--- Evidence (from a locally patched build, used only to investigate) ---

Rasterizer::BeginRendering was instrumented to log scene viewport dimensions on change. DRS here is viewport-based (fixed full-size target, shrinking viewport) in ~192px / 5% rungs. Logging render area is not sufficient - that only ever shows 3840x2160 plus a fixed half-res effects buffer.

An env var was added to scale the reported elapsed time, giving the guest artificial headroom. Sweeping it:

scale | lowest rung | sub-native events
------|-------------|------------------
1.0   | 2111x1187   | 499
0.5   | 3456x1944   | 198
0.25  | 3648x2052   | 108
0.1   | 3648x2052   |  27
0.02  | 3840x2160   |   0

At 1.0 (current upstream behaviour) the game descends to 1535x863 at worst. At 0.02 it is pinned at native 2160p with zero sub-native frames.

--- Caveats ---

  • Testing was very limited: one game, one machine, one GPU vendor. Sessions were short and not length- or activity-controlled, so the middle rows are indicative rather than rigorous; the 1.0 and 0.02 endpoints are clear-cut.
  • The scaled clock is NOT proposed as a fix - it is a diagnostic that demonstrates the cause. It appeared to work correctly in initial play testing with no visible problems in game speed, animation, audio sync or stability, but it has not been tested over long sessions, heavy combat, cutscenes or any other title, and no attempt was made to find regressions it may introduce. Suppressing DRS also removes the guest's ability to protect its framerate.
  • One link is inferred, not traced: it is confirmed that GetGpuClock64 returns an incorrect value and that scaling it eliminates the collapse, but NOT directly confirmed that Diablo III issues EventWriteEop packets with DataSelect::GpuClock64 - that path logs at Trace and did not appear in the captured logs. The mechanism above is the most plausible explanation for the correlation rather than a traced one.

--- Suggested fix ---

The unit conversion is straightforward on its own:

const auto gpu_freq = Libraries::GnmDriver::sceGnmGetGpuCoreClockFrequency();
return Common::MultiplyAndDivide64(elapsed_ns, gpu_freq, 1'000'000'000ULL);

The wall-clock issue is the larger one and needs timing derived from actual command buffer execution rather than host wall-clock.

Reproduction Steps

  1. Run Diablo III (CUSA00242, app v01.47) on stock v0.18.0 with Neo/PS4 Pro mode enabled.
  2. Load a save at New Tristram. It's the easiest spot to check/reproduce easily.
  3. Watch the image for the first second or after the area finishes loading, you'll see it go from sharp and resolute down to very aliased and jagged.

Expected: resolution stays at or near native while the game holds 60fps. Actual: the 3D scene visibly softens over ~0.5-1s and stays soft. The HUD remains sharp. Frame time stays pinned at 16.7ms throughout, so the game is not missing its target - it only believes it is.

Specify OS Version

Linux (Wayland) [Omarchy 4] -- but also observed same issue in Cachy OS and Win 11 (did not test fix there though)

CPU

AMD Ryzen 7 9800X3D

GPU

NVIDIA GeForce RTX 5080 (proprietary driver 610.57.04)

Amount of RAM in GB

32GB

Amount of VRAM in GB

16GB

Log File

shad_log.txt