Skip to content

Improved CPU timing with async thread - #6

Closed
breno-assis wants to merge 4 commits into
JRDevo:mainfrom
breno-assis:main
Closed

breno-assis wants to merge 4 commits into
JRDevo:mainfrom
breno-assis:main

Conversation

@breno-assis

Copy link
Copy Markdown
Contributor

Summary

Improve stability of the per-draw motion-vector path in VR-heavy city scenes by reducing render-thread disruption in the draw-id pipeline.

This change:

  • moves the CPU-only prepare grouping/sorting work off the render thread onto a persistent worker
  • adds timing breakdowns for prepare wait / scan / sort / upload to make render-thread stalls visible in logs
  • replaces getter-heavy DrawIdRecord::Record capture with shadowed D3D11 state for IA / VS / RS paths
  • extends the shadow path to cover VS constant-buffer binds, including D3D11.1 offsetted binds
  • adds runtime verification logs to confirm whether record capture is using the shadow path or falling back

The goal is improved timing stability and less noticeable CPU hitching in dense VR scenes, even where average CPU cost does not drop dramatically.

Observed Issue

In VR, especially in dense city scenes with lots of traffic and geometry, the per-draw motion-vector path could introduce noticeable CPU timing instability. The issue was less about one large average cost and more about where the work landed on the render thread and how uneven that cost felt during play.

What Changed

  • Off-threaded DrawIdMv::Prepare grouping/sorting work via a persistent worker thread
  • Added render-thread-visible wait timing and prepare subphase instrumentation
  • Added shadow-state capture for DrawIdRecord::Record
  • Added VS constant-buffer shadowing, including D3D11.1 VSSetConstantBuffers1
  • Added runtime shadow-hit logging to verify whether the capture path is using shadowed state

Validation Notes

Runtime logs confirm that the shadowed record path is active:

  • MV draw-ids shadow @blit reports 100% shadow-hit rate for all tracked fields, including cb

Measured results suggest:

  • no large reduction in average draw-id CPU cost in the tested scenes
  • improved timing structure and better visibility into render-thread stalls
  • subjective VR smoothness appears improved even when headline CPU timings remain similar

Testing

Tested on:

  • Meta Quest 3
  • Virtual Desktop
  • NVIDIA RTX 5080
  • AMD Ryzen 7 9800X3D

Scenario:

  • consistent daytime driving around San Francisco
  • debug = 1 enabled to collect runtime timing and draw-id diagnostics

- Introduced multi-threading support in draw_ids.h for non-blocking GPU timestamp spans, including new methods PrepareBegin and PrepareEnd to manage preparation of draw IDs.
- Updated the Forget method to include a preparation wait to ensure thread safety.
- Enhanced performance tracking by adding new tick counters for various preparation stages in draw_ids.h.
- Modified the DidPrepare method in motion_vectors.cpp to utilize the new PrepareBegin and PrepareEnd methods, improving clarity and separation of concerns.
- Updated the DidStatsTag function in inject.cpp to report detailed CPU timing metrics for preparation stages.
- Adjusted motion_vectors.h to include flags for tracking preparation state, ensuring proper synchronization between preparation and execution phases.
- Introduced `xr_hook.h` to manage OpenXR swapchain image releases and provide a callback mechanism for D3D11 textures.
- Implemented functions for tracking view poses and overlay parameters, enabling VR tuning panel and FPS box as OpenXR quad layers.
- Enhanced diagnostics for swapchain releases and view corrections.
- Updated `drawid_harness.cpp` to include new VR model shadow tests with union clipping for both eyes.
- Added support for new configuration options related to consensus failure tolerance and VR model shadow initialization.
@JRDevo

JRDevo commented Oct 11, 2026

Copy link
Copy Markdown
Owner

Hi @breno-assis, thank you for this PR!!!!
The core idea is right and it is in the mod now: remembering the game’s input state as the game sets it, instead of asking the driver for ~10 things at every draw. It is did_shadow = 1 by default in the test build, and I measured it: ~130 → ~105 ns per draw in ETS2 VR.

You are credited as co-author on the commit (e5db8ff) and in the README’s thanks line. The test build with it is here, if you want to try it:
https://github.com/JRDevo/ETS2-ATS-DLAA-DLSS-Injection-Mod/releases/tag/v0.11.0-rc1

PR did not merge as it stands.

What I found when reviewing and running it:

  • The remembered state is never re-checked. Nothing drops it on ClearState, ExecuteCommandList or SwapDeviceContextState, and the D3D11 runtime also unbinds resources by itself (read / write hazards). The record could then copy a state the game no longer has.
  • The copied pointers are not owned (no AddRef). A resource the game releases between its Set and our record can dangle.
  • The prepare worker thread was slower on average in my runs, with 31 ms stalls under CPU load, and the exit path could hang.
  • motion_vectors.cpp: StaticGated() became m_didN != 0, which changes when the static gate opens.
  • The draw-id test harness drops to 50,349 / 136,840 checks with the shadow on. Part of that is a harness artefact (it drives the getters directly), but it hid the points above.

The version in the mod adds owned references, hooks on ClearState / ExecuteCommandList / SwapDeviceContextState, a check that the hooked entries still point at our detours, a drop on another thread’s Set, and a verify mode (did_shadow_verify) that compares every Nth record with the getters: 6490 compares, 0 mismatches in a VR drive. The harness passes 136,840 / 136,840 with it on.

Check the latest version.... If you spot something I got wrong in the rewrite, please let me know.

Many thanks for spending your time helping here :)

@JRDevo JRDevo closed this Oct 11, 2026
breno-assis added a commit to breno-assis/ETS2-ATS-DLAA-DLSS-Injection-Mod that referenced this pull request Oct 12, 2026
…cheaper draw-id record, GPU-per-pass timing

All tested in ETS2 VR on 2026-10-11 (RTX 5090, 9950X3D, Virtual Desktop). New features are OFF by default;
the mod behaves as v0.10.3 unless dlaa.ini or the tuning menu switches them on.

- phase 30 + 33: Turbo mode (xr_hook.cpp "TURBO MODE", ini xr_turbo = 0, menu row "Turbo mode (VR)"). Async xrWaitFrame
  ported from OpenXR Toolkit (MIT, THIRD_PARTY.md). 33 rewrote it for ETS2's split threads (wait/begin on the main
  thread, end on the render thread): no 1-s freeze at switch-on, clean switch-off. Tested: 0 failures, no fps gain when
  GPU-limited (100 % scaling + DLAA). Also OpenXR session / event logging ("started by the GAME / RUNTIME").
- phase 31: CPU profiler cpu_prof.h ("CPU prof" line per thread every 10 s, ini cpu_prof = -1 follows debug); log.h
  writes on a writer thread; OFXR rescan off-thread; PollKeys reads each key once. Mod CPU on the render thread measured
  ~2-2.5 ms/frame (not the ~3.5 ms estimated before).
- phase 32: cpu_pin.h, ini cpu_cache_pin: keeps the game on the V-Cache CCD of mixed-L3 X3D CPUs. Default 0 (OFF, opt-in):
  no fps gain was measured in an A/B, so a mod must not change process affinity on its own.
- phase 34 + 34b: VRS fixed foveated rendering (vrs.cpp, NVAPI via external/nvapi, MIT; ini vrs = 0, vrs_inner / vrs_outer /
  vrs_quality / vrs_debug; menu rows "Foveated (VRS)" + "Foveated size"; CMake WITH_VRS auto when external/nvapi exists).
  Coarse 2x2 only inside the game's world G-buffer + forward binds; cut-outs, SV_Depth writers and the mod's own passes
  stay full rate. 34b: the eye of a bind is learned from the blit-RT cycle (the eye order flips every 3rd frame), full
  rate when unsure. Tested: 0 eye misses, sharp centre OK, gain only ~0.5 ms GPU per frame.
- phase 35: did_shadow (draw_ids.h DidShadow, ini did_shadow = 1, did_shadow_verify = -1): the draw-id record copies
  IA / VS / RS state from hooked Set calls instead of ~10 getters per draw; dropped on ClearState / ExecuteCommandList /
  SwapDeviceContextState, another thread's Set, or a vtable entry that no longer points at the detour; verify mode compares
  every 64th record with debug = 1. Tested: 6490 verifies, 0 mismatches, did-rec ~130 -> ~105 ns/draw. Idea from
  pull request JRDevo#6 by Breno Assis, rewritten with owned references and the re-verification above. Harness S19.
- phase 36: gpu_pass.h "GPU per pass" line (shadow / gbuffer / lighting / forward / post / mirror / other ms per frame,
  measurement only, with debug = 1). First numbers: lighting ~4 ms, mod GPU alone ~3-6 ms per frame (both eyes).
- docs: README (Turbo / Foveated rows, dlaa.ini table, build with external/nvapi, third-party table, thanks to Breno Assis),
  THIRD_PARTY.md (NVIDIA NVAPI MIT, OpenXR Toolkit MIT), docs/dlaa.ini.sample.
- tests/drawid_harness.cpp: S19 did_shadow = 1 | 2. Harness 136840 / 136840 passed at this commit.

Co-authored-by: Breno Assis <breno.assis@outlook.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.

2 participants