Skip to content

Rendering Optimizations and XeGTAO Integration - #562

Draft
brodrigz wants to merge 6 commits into
OGSR:mainfrom
brodrigz:optimization/rendering-improvements
Draft

brodrigz wants to merge 6 commits into
OGSR:mainfrom
brodrigz:optimization/rendering-improvements

Conversation

@brodrigz

@brodrigz brodrigz commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Rendering optimizations and XeGTAO

Summary

A collection of R4 rendering cleanup/optimizations, plus a dedicated AO pass and selectable XeGTAO implementation.

The optimization work mostly removes GPU work which was either unconditional despite having no consumer, or existed only to move the same display-sized image back and forth between render targets.

The AO work allows SSDO/GTAO evaluation to run separately from combine_1, adds optional half-resolution evaluation for the existing methods, and provides the common AO output used by the new XeGTAO path. The original inline SSDO/GTAO path remains selectable.

XeGTAO is adapted from Intel's MIT-licensed implementation.

Also adds a small Tracy profiler UI improvement for filtering GPU passes and exporting the current frame's GPU timings to CSV for easier comparison.

Skip unused depth snapshots

During normal scene rendering, the renderer previously copied the scene depth buffer three times per frame:

  • $user$temp_zb before HUD rendering for 3D-scope depth;
  • $user$zbuffer_dof after HUD rendering, before scope depth, for DOF;
  • $user$zbuffer after HUD/scope depth for TAA, DLSS, FSR3 and other depth consumers.

The first two copies are now conditional.

$user$temp_zb is only updated when the 3D-scope HUD depth draw map is non-empty.

$user$zbuffer_dof is only updated when the exported DOF parameters pass the same non-zero check used by phase_dof.

The main $user$zbuffer copy remains unconditional in the normal scene-rendering path. It preserves a separate sampleable depth snapshot while the original scene depth is used as a DSV. Removing it would require reviewing the depth bindings and consumers, rather than simply redirecting them to the original texture.

The copies also use rt_Base_Depth->pSurface directly instead of calling GetResource() / Release() on the DSV each time.

While testing this, CWeapon::UpdateDof() also turned out to export the DOF fade before clamping it. The last fade-out frame could leave non-zero parameters behind, including negative values. Since the caller stopped updating once the clamped fade reached zero, those parameters could remain active after aiming.

Both phase_dof and the new depth-copy gate use the same zero check, so this could keep both the DOF pass and its depth copy running.

The fade is now clamped before exporting the parameters so the final fade-out frame actually writes (0,0,0,0).

Performance comparison

GPU timings on relevant passes, captured in Cordon:

Pass Baseline Depth optimization Difference
copy_zbuffer_scope 0.0229 — −0.0229
copy_zbuffer_scope_depth 0.0211 — −0.0211

Display post-processing ping-pong

A number of post-processing passes followed the same pattern:

  1. sample $user$postprocess0;
  2. render into $user$generic_combine;
  3. CopyResource the result back into $user$postprocess0.

That copy was repeated after CAS, screen-space sunshafts, combine_2, DOF, LUT, gasmask effects, night vision, rain drops and other display-sized passes.

rt_Postprocess_0 and rt_Generic_combine are now treated as a ping-pong pair.

The renderer tracks which one currently contains the latest image. Post effects participating in the pair read from the current buffer, write into the other one, then flip the state.

Shader bindings for $user$postprocess0 / $user$generic_combine are remapped to the current source while inside the post-processing chain.

This removes the per-effect full-resolution RGBA16F copies while preserving those shader-facing texture names. The CAS shader setup also has a separate source binding for the temporal-upscaler path.

There are still seed/synchronization copies where they are actually required.

In particular, the existing 3D-scope reticle path expects $user$generic_combine and cannot always participate in the remap because that could bind the same texture as both SRV and render target. If the latest image is already in generic_combine, no copy is needed. Otherwise the scope path synchronizes it first.

TAA, SMAA and the non-temporal paths keep their existing initial seed behavior. A seed copy also remains after temporal upscaling when the CAS path which avoids it is not used.

Performance comparison

GPU timings on relevant passes, captured in Cordon:

Pass Baseline After Difference
CAS 0.0775 0.0625 −0.0150
phase_ss_ss 0.2325 0.2130 −0.0195
combine_2 0.2482 0.2202 −0.0280
copy_zbuffer 0.0211 0.0222 +0.0011

Dedicated AO pass

SSDO and GTAO were previously evaluated directly inside combine_1.

They can now render visibility into a separate R16F AO texture which the AO-buffer variant of combine_1 consumes before the existing colored-AO / ambient-light treatment.

Added:

r_ao_resolution

with:

  • legacy - original inline AO evaluation inside combine_1;
  • full - dedicated AO evaluation at full internal render resolution, and the default setting;
  • half - AO evaluation at one selected covered sample per 2x2 block, followed by depth/normal-aware reconstruction.

full is the default dedicated path and the baseline for comparing the separate pass against legacy.

half is the mode which actually reduces AO evaluation work. It trades some spatial detail for fewer evaluations, and still pays for the reconstruction pass.

The half-resolution path selects the nearest covered G-buffer sample in each 2x2 block and stores its visibility, view depth and packed normal in an RGBA16F texture. Reconstruction combines four neighboring half-resolution samples with bilinear, depth and normal weights.

Odd render dimensions use rounded-up half-size targets with clamped integer coordinates.

There is no extra temporal history or depth copy involved in this SSDO/GTAO path.

With DLSS/FSR enabled, these resolutions are relative to the internal render size, not the final display resolution. That size can be reduced by the selected upscaling preset, or remain native for DLAA/native-AA modes.

When AO is disabled at renderer initialization or after vid_restart, the dedicated AO resources are not allocated and no AO pass runs.

Performance comparison

GPU timings on relevant passes, captured in Cordon:

Pass Baseline Dedicated AO pass
phase_ao --- 1.7664
combine_1 1.9866 0.0952
Total 1.9866 1.8616

Final GPU timing cost decreased by 0.1250 (~6.3%).

XeGTAO

Added:

r_ao_mode st_xegtao

This uses an adapted version of Intel's XeGTAO implementation.

The renderer already has linear view depth and G-buffer normals, so the integration reuses those rather than generating another set of normals or converting a hardware-depth snapshot.

The path is:

  1. compute prefilter builds five R16F depth mip levels, including mip 0;
  2. XeGTAO evaluates visibility using the existing octahedral G-buffer normals, writing R8_UINT visibility and packed R8_UNORM edge weights;
  3. one edge-aware XeGTAO denoise pass filters visibility into a separate R8_UINT texture;
  4. a fullscreen export converts the result into the same R16F AO texture used by the dedicated SSDO/GTAO paths.

combine_1 therefore still owns the existing OGSR colored-AO and ambient-light treatment instead of XeGTAO becoming a second unrelated lighting path.

The r2_ssao quality presets map as:

  • st_opt_low -> 1 slice / 2 steps;
  • st_opt_medium -> 2 slices / 2 steps;
  • st_opt_high -> 3 slices / 3 steps.

Samples are taken on both sides of each slice.

Added:

r_xegtao_radius

Default is 0.5, with a range of 0.05 to 4.0 in view-space units.

The radius can be changed live and only affects XeGTAO.

r_xegtao_bent_normals on/off adds optional directional ambient lighting. Default is off, requires vid_restart, and only affects XeGTAO. It calculates and denoises the average unoccluded direction, which combine_1 uses for diffuse environment sampling. Reflections and the material BRDF keep the surface normal.

r_ao_resolution only changes the existing SSDO/GTAO paths. Selecting legacy or half does not change XeGTAO's resolution or send it through another implementation.

Currently XeGTAO currently always runs at full internal render resolution.

With DLSS/FSR, "full" internal resolution may of course already be significantly smaller than the display resolution, depending on the preset.

There is currently no separate XeGTAO temporal accumulation pass or AO history buffer.

When TAA, DLSS or FSR3 is selected, XeGTAO advances its sampling noise using the frame number modulo 64. Projection reconstruction separately accounts for the existing camera jitter. Without one of those temporal AA paths, the sampling noise remains fixed.

GTAO vs XeGTAO

GPU timings in milliseconds from the tracy captures:

Pass / stage GTAO XeGTAO Difference (XeGTAO − GTAO)
phase_ao 1.8125 0.2386 −1.5739
combine_1 0.0983 0.0983 0.0000
phase_ao + combine_1 1.9108 0.3369 −1.5739
DEFER_COMBINE (context) 3.4017 1.8319 −1.5698
GPU Frame (context) 6.0191 4.4595 −1.5596

XeGTAO used 86.8% less GPU time in phase_ao in these captures, including its prefilter, evaluation, denoise and export. Frame and DEFER_COMBINE totals include other rendering work.

Renderer state and resources

XeGTAO exposed a missing compute-SRV invalidation in the D3D11 state cache. Compute SRV state is now reset alongside the other shader stages, and XeGTAO explicitly unbinds UAVs before reusing their outputs as SRVs.

Dedicated AO resources follow the render-target lifecycle and are recreated on renderer reset. XeGTAO compute resources are allocated only when XeGTAO is selected and AO is enabled; half-resolution SSDO/GTAO resources are not allocated for XeGTAO.

With SSDO/GTAO, both dedicated AO targets remain allocated so r_ao_resolution can change live. Changing the AO method, r2_ssao or r_xegtao_bent_normals requires vid_restart. r_ao_resolution is runtime-adjustable for SSDO/GTAO, while r_xegtao_radius is runtime-adjustable for XeGTAO.

Compatibility

The existing modes remain available. The default method remains SSDO, and the default r_ao_resolution is full. Existing saved settings and graphics presets can override defaults.

XeGTAO is a different AO implementation and its radius/art direction has not been calibrated to reproduce OGSR GTAO exactly.

Benchmarks

WIP
I'm still benchmarking improvements with tracy build, bellow are some of the comparisons I've done.
RTX 4070, Default High Settings with DLAA

3.564 GTAO

ss_l4marr_2026-09-07_16-27-27-(l01_escape)

Optimized GTAO (full AO)

ss_l4marr_2026-09-07_16-28-42-(l01_escape)

XeGTAO (full AO, r_xegtao_radius 0.5)

ss_l4marr_2026-09-07_17-14-45-(l01_escape)

Some with more focus on visual difference:

3.564 GTAO

ss_l4marr_2026-09-07_17-32-44-(l01_escape)

XeGTAO (full AO, r_xegtao_radius 0.5)

ss_l4marr_2026-09-07_17-28-08-(l01_escape)

Test plan

  • SSDO/GTAO shader permutation validation: 33 variants
  • XeGTAO shaders compile in-game for the tested configuration
  • D3D11 WARP AO smoke tests: 14 SSDO/GTAO and reconstruction cases
  • XeGTAO running in-game
  • Scope/DOF depth-copy gates use the corresponding consumer conditions
  • Main $user$zbuffer copy remains unconditional during normal scene rendering
  • Display post-processing uses RT ping-pong and the measured copy-back markers are removed
  • 3D-scope synchronization path retained where required
  • Full-resolution SSDO/GTAO visibility matches the legacy calculation in the synthetic WARP test cases
  • Compare XeGTAO against AO off / SSDO / GTAO at equivalent acceptable visual quality
  • Check thin foliage, fences, corners, silhouettes and screen borders
  • Check HUD weapons and aimed 3D scopes
  • Check AA off, SMAA, TAA, DLSS and FSR
  • Check odd internal resolutions and render-scale changes in-game
  • Check repeated vid_restart across methods, quality settings and AO off
  • Benchmark XeGTAO bent normals cost (off vs on)
  • Benchmark half-resolution SSDO/GTAO cost & visual difference (full vs half)

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