Skip to content

[mod:darktide] Remove RenoDX lock contention from per-batch buffer SRVs - #720

Open
mortan wants to merge 2 commits into
clshortfuse:mainfrom
mortan:perf/darktide-resource-views
Open

mortan wants to merge 2 commits into
clshortfuse:mainfrom
mortan:perf/darktide-resource-views

Conversation

@mortan

@mortan mortan commented Oct 25, 2026

Copy link
Copy Markdown

What changed

Two commits:

  1. perf(utils::resource): avoid exclusive locks on repeated and D3D12 buffer view inits
    • OnInitResourceView now reads the resource info under the shared lock. It only takes the exclusive lock to record a view handle that is not yet known. The result is the same as before; the old insert was a no-op in that case.
    • New opt-out utils::resource::use_d3d12_buffer_view_tracking, default true. It is OR-merged in SharedData, so tracking stays on as long as any loaded module wants it.
    • When every module opts out, D3D12 buffer and acceleration-structure views are not tracked. RenoDX never clones or upgrades D3D12 buffers: CloneResource returns {0} for non-Vulkan buffers, and the create/init upgrade paths only handle textures.
    • A live entry at a rewritten descriptor is retired through OnDestroyResourceView. The handle then stops resolving to the previous resource, and a clone view is cleaned up.
    • OnAfterDestroy returns early when nothing is queued. It runs after every in-place view rewrite, and MSVC std::set allocates its sentinel node on construction.
    • OnDestroyResourceView moved above OnInitResourceView unchanged, so no forward declaration is needed.
  2. perf(darktide): use D3D12-only cloning and skip buffer view tracking
    • use_resource_cloning_dx12_only = true. Starfield and Infinity Nikki already use this with proxy + HDR10 + cloning. Every hook it unregisters returns early on D3D12. On D3D12 the clones are applied by descriptor copy at view creation, and that path stays registered.
    • use_d3d12_buffer_view_tracking = false.

Why

Darktide's deferred skinning job (Darktide.exe 1.13.0 at 0x1407C9D30) recreates three buffer SRVs per batch on every worker. They target fixed CPU descriptors and one shared upload buffer.

With full view tracking, every CreateShaderResourceView took:

  • an exclusive lock on that buffer's resource_infos shard, plus a resource_view_handles insert,
  • an exclusive lock on the view shard, plus destroy/init callbacks for the in-place rewrite.

All workers serialized on that one shard. The renderer and main threads did too, because they help with jobs while waiting.

The dx12_only switch removes the registration of push_descriptors, update_descriptor_tables, barrier, clear and render-pass hooks, and a global per-draw command_action callback. On D3D12 these only make ReShade resolve GPU virtual addresses on root CBV/SRV binds and dispatch events into early-returning handlers. This part is a cleanup; the captures below do not show a measurable saving from it.

Measurements

Setup: Windows Performance Recorder, 30 s captures, ray tracing on, DLSS-G, ReShade 6.8.0, Ryzen 9850X3D / RTX 5090.

Means over captures. Values are renderer / main / per worker thread.

before (upstream build, 9 captures) after (this branch, 5 captures) without RenoDX (3 captures)
RenoDX on stack (inclusive) 22.0 / 19.9 / 34.4 % 1.7 / 0.8 / 2.5 % –
RenoDX CPU per real frame 2.71 / 2.33 / 2.52 ms 0.18 / 0.07 / 0.15 ms –
skinning-kernel waits through RenoDX ~10,300 / 9,700 / 10,800 per s none –
waits per second per thread 11,981 / 10,770 / 14,358 338 / 717 / 2,883 498 / 491 / 2,900
context switches per second 13,409 / 12,038 / 15,650 619 / 1,011 / 3,029 726 / 702 / 3,031
kernel share 22.6 / 17.3 / 23.1 % 12.9 / 7.2 / 4.9 % 12.9 / 5.6 / 4.9 %

How to read this:

  • The lock-contention signature is gone. No "after" report lists a wait site through RenoDX (the analyzer prints the top 12 sites per thread). The remaining skinning waits, about 26–140 per s, go through ReShade's own register_resource_view lock and are at the level of the no-RenoDX runs. These metrics are specific to RenoDX, and skinning still runs in every capture, so mission differences cannot explain the drop. The ranges are disjoint (Mann-Whitney p = 0.001).
  • Renderer and workers are at the no-RenoDX level for waits, context switches and kernel share. Main is still somewhat above it, but its extra waits are in frame pacing and the message pump, not in RenoDX.
  • ReShade's own CPU per frame did not drop. The drop in the ReShade layer is the nested RenoDX frames. So these captures show no measurable saving from the dx12_only part.
  • RenoDX on the DLSS-G present thread is unchanged at about 4 %.
  • Caveats:
    • Graphics settings were identical across the three sessions, per the launcher log. The missions and binaries differ:
      • "Before" is an upstream build from 2026-10-06.
      • "After" is b6b32f47 plus these commits, built locally against the submodule's ReShade API 18 headers.
    • WPR stack walks cost the high-contention "before" build disproportionately.
    • Four of the five "after" captures ran with unrelated background CPU load.
    • "After" and "without RenoDX" were partly GPU or pacing limited.
    • fps and CPU-per-frame deltas are therefore not attributed to this change. A same-scene A/B against a same-commit baseline is still to do.

Verification

  • darktide target builds in Release with MSVC 14.51; no new warnings.
  • In game: the fixed build was loaded (checked by SHA256 and ReShade.log).
    • The swap chain is upgraded to r10g10b10a2_unorm with an hdr10_st2084 effect runtime.
    • The log shows no clone or fast-clone failures.
    • HDR output looked as usual to the tester. Whether the RenoDX tonemapper is actually active in that mission is unclear; see below.
  • Code review: every handler dropped by dx12_only returns before doing anything on D3D12. Untracked D3D12 buffer and acceleration-structure views were never modified by RenoDX.
  • WPR captures as above.

Unrelated observation

ReShade.log reports utils::shader::PipelineShaderDetails(Failed to replace pipeline) for tonemapper 0xB3F15364. This is independent of this PR: no shader or pipeline code changed. A likely cause is that the shared tonemapper.hlsl maps this minimal-layout variant to b1/b2, while the original uses b0/b1; 0x9347F823 looks the same. This needs confirmation. After a failed build the replacement is retried on every bind. Only 2 of the 14 custom shader hashes were seen in that session.

mortan and others added 2 commits October 25, 2026 15:48
…ffer view inits

OnInitResourceView took two exclusive shard locks per view creation, plus
a resource_view_handles insert. Engines that rewrite views in place on
many threads serialize on the shard of a shared resource.

- Read under the shared lock first and only take the exclusive lock to
  record a view handle that is not yet known (same result as before).
- Add the opt-out use_d3d12_buffer_view_tracking (default true, OR-merged
  in SharedData). When every loaded module opts out, D3D12 buffer and
  acceleration structure views are not tracked; RenoDX never clones or
  upgrades D3D12 buffers. A live entry at a rewritten descriptor is
  retired through OnDestroyResourceView so the handle stops resolving to
  the previous resource.
- OnAfterDestroy returns early when nothing is queued; MSVC std::set
  allocates its sentinel node on construction.

OnDestroyResourceView moved above OnInitResourceView unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The deferred skinning jobs recreate three buffer SRVs per batch on every
worker against one upload buffer. With full view tracking all workers
serialized on one resource_infos shard (WPR, dm_forge with RT: about
10k SRW lock waits/s per thread, RenoDX on 18-39 % of thread samples).

- use_resource_cloning_dx12_only: D3D12 clones are applied by descriptor
  copy at view creation. The generic descriptor, barrier, clear and
  render pass hooks return early on D3D12 but made ReShade resolve GPU
  addresses on every root CBV/SRV bind.
- use_d3d12_buffer_view_tracking = false.

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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant