Skip to content

Reuse the froxel fog shadow shader table across frames - #146

Draft
JuhDuh26 wants to merge 2 commits into
carbonengine:mainfrom
JuhDuh26:volumetrics-rt-shader-table
Draft

JuhDuh26 wants to merge 2 commits into
carbonengine:mainfrom
JuhDuh26:volumetrics-rt-shader-table

Conversation

@JuhDuh26

@JuhDuh26 JuhDuh26 commented Nov 9, 2026

Copy link
Copy Markdown

Summary

The raytraced froxel fog shadow path in Tr2VolumetricsRenderer::RenderFog built a brand new Tr2RtShaderTableAL every frame. On DX12 that means a CreateCommittedResource for a fresh upload buffer, which is filled and then released at the end of the scope. It was the largest native per-frame cost found while profiling frame-rate drops in the EVE Frontier client: about 0.56 ms of main-thread time every frame, with spikes up to 1.7 ms, whenever raytraced shadows are on.

#123 already added Tr2RtShaderTableAL::Update, which rebuilds a table in place using persistent per-back-buffer upload buffers, and moved Tr2RaytracingManager and Tr2LightManager over to it. The volumetrics renderer was the one caller still left on Create-per-frame. This PR moves it over the same way.

Changes

1. Finer Tracy zones (Split the raytraced froxel fog zone into finer Tracy zones)

The old "Create shader table" zone was opened partway through the block and ran to the end of it. So it timed SetOption/SetParameter, ApplyMaterialDataForRtState, UseResources and DispatchRays as well, not just the table. Now:

Zone Covers
Raytraced froxel shadows Parent. The same span the old Create shader table zone covered, so old and new captures compare directly
Create shader table Adding the ray-gen/miss records and building the table
Set options and parameters SetOption ×2, SetParameter ×3
Apply material data SetRtPipelineState, ApplyMaterialDataForRtState, UseAccelerationStructure
renderContext.UseResources Unchanged
DispatchRays renderContext.DispatchRays

2. The fix (Reuse the froxel fog shadow shader table across frames)

  • Tr2VolumetricsRenderer now owns a Tr2RtShaderTableAL m_fogShadowShaderTable, declared after m_fogConstantBuffer so it is destroyed first. Each frame it calls Update, falls back to Create when the table isn't valid yet or Update fails, and logs and bails if the table is still invalid. This is the same pattern Tr2RaytracingManager::RenderShadows and Tr2LightManager::RenderRaytracedShadows use.
  • The technique, option, option-value and parameter names used in the block are hoisted into function-local static const BlueSharedStrings, so the block no longer interns ten strings per frame.

No behaviour change is intended: the table holds the same records, filled the same way, and only lives in a different buffer.

Why the table is still written every frame

The original idea was to build the table once and rebuild it only when the pipeline state or library changed. That doesn't hold here:

  • The description is not accumulating. EveSpaceScene::PrepareRaytracedShadows → Tr2RaytracingGeometry::PrepareShaderTableDescription calls Clear() on every raycaster's description each frame, including the volumetrics one, then refills the hit groups. The fog then appends one ray-gen and one miss record.
  • Hit-group records contain per-object constant buffer addresses (perObjectData, vertexBufferData). These come from the descriptor cache's per-frame upload ring, so they change from frame to frame and the records have to be rewritten each frame.
  • DescriptorStateCache::UploadConstants already uploads each constant buffer at most once per frame, shared across all tables. Re-filling the records is cheap, so the cost was entirely the per-frame resource allocation and release.
  • Tr2Effect::SetOption already returns early when the value hasn't changed, so the options needed no extra guard.

Measurements

EVE Frontier client, local dev stack, /buildflavor=trinitydev, DX12, shadow quality = raytraced. Same character and position in the same solar system for both runs; 12 s Tracy capture each (tracy-capture.exe, C++ zones only, all telemetry categories active), exported with tracy-csvexport.exe.

  • Before: deployed _trinity_dx12_trinitydev.pyd (v8.1.0, 21edab0).
  • After: this branch (8b532a7 + the two commits), built locally, TrinityDev config.

The fog shadow block

Zone Before mean Before min Before max After mean After min After max
Whole block (Create shader table before, Raytraced froxel shadows after) 562 µs 390 µs 1,712 µs 32.7 µs 18.1 µs 177 µs
↳ Create shader table (table only) – – – 8.1 µs 4.2 µs 20.2 µs
↳ Set options and parameters – – – 4.0 µs 2.2 µs 18.6 µs
↳ Apply material data – – – 3.3 µs 2.2 µs 11.8 µs
↳ renderContext.UseResources 0.9 µs 0.4 µs 5.9 µs 0.2 µs 0.2 µs 1.0 µs
↳ DispatchRays – – – 16.0 µs 8.4 µs 155.6 µs
Reference: Tr2RaytracingManager Create shader table 11.2 µs 7.1 µs 49.1 µs 11.9 µs 6.9 µs 40.6 µs

The block is about 17× cheaper on average, and its worst frame drops from 1.7 ms to 0.18 ms. The Create shader table part on its own now costs about what the Tr2RaytracingManager one does. The remaining spikes are in DispatchRays (command list recording), not the table.

Whole frame (same captures)

Before After
Frames in the 12 s capture 1,091 (≈ 91 fps) 1,160 (≈ 97 fps)
BlueOS/PumpOS mean 10.43 ms 9.70 ms
Trinity/device/OnTick mean 8.96 ms 8.23 ms
Trinity/device/RenderJobs mean 7.92 ms 7.51 ms

Absolute frame rates in these captures are low because they use the TrinityDev flavour with Tracy capturing every telemetry category. Both runs are equally affected, so the comparison holds, but they shouldn't be compared with fps from an internal or release client.

Testing

  • Builds: trinity_dx12, TrinityDev config, VS 2026 / v145.
  • Runs in the EVE Frontier client in space with raytraced shadows and froxel fog, with no errors from the new CCP_LOGERR path.
  • Before/after Tracy captures as above.
  • Visual A/B of the fog shadows (not done side by side; the records and dispatch are unchanged, only the backing buffer differs).
  • Internal and Release flavours not measured.
  • Metal is not exercised. There Update forwards to Create, per Cache RT shader table state and skip unused RT work #123, so behaviour matches the other two callers.
  • Unit tests weren't built locally (-DBUILD_TESTING=OFF; see below). The existing Cache RT shader table state and skip unused RT work #123 test covers Update across frames with changing constants and record counts.

Notes for reviewers

These only affected the local build. None of them are part of this change:

  • Long paths break vcpkg port builds. Under a deep worktree path (.claude/worktrees/...), carbon-blueexposure's compiler check fails with rc.exe ... manifest.rc(3) : error RC2136 : missing '=' in EXSTYLE=<flags>. The embedded manifest path gets too long for rc's string literal; building through a short junction works.
  • SSMS breaks crashpad. crashpad (pulled in by the carbon-exefile host dependency) runs vswhere -products * -latest. With SQL Server Management Studio 22 installed, that returns SSMS instead of Visual Studio, and mini_chromium's win_helper.py fails running its vcvarsall.bat. For the measurement build I dropped carbon-exefile locally and configured with -DBUILD_TESTING=OFF.

🤖 Generated with Claude Code

JuhDuh26 and others added 2 commits November 9, 2026 15:06
"Create shader table" used to cover everything up to DispatchRays. It now
covers only the table build, under a new "Raytraced froxel shadows" parent
that spans the same work as before, with sibling zones for options and
parameters, material data, UseResources and DispatchRays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The raytraced froxel fog built a new Tr2RtShaderTableAL every frame, so each
frame paid for a CreateCommittedResource on a fresh upload buffer and then
released it. Keep the table as a member and rebuild it in place with
Update, as Tr2RaytracingManager and Tr2LightManager already do since carbonengine#123,
falling back to Create when Update fails.

The records still have to be written every frame: the hit groups carry
per-object constant addresses from the per-frame upload ring, and
PrepareRaytracedShadows clears and refills the description each frame.

Also hoist the option, value and parameter names into statics so the
block no longer interns ten BlueSharedStrings per frame.

Co-Authored-By: Claude Opus 5.5 <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.

1 participant