Optimization part 2 - #120
Merged
Merged
Conversation
Adds a Ray Statistics block to --stats: camera rays, closest-hit and
occlusion queries, vertices shaded, mean path length, throughput against
the render phase alone, and why paths ended (escaped / depth cap /
roulette).
Counters rather than timers. An Instant::now() pair costs tens of
nanoseconds against a few hundred for a ray query, so timing individual
rays would both slow the render and distort what it measured. Each work
unit -- a tile or a scanline -- owns a private RayStats and the pass sums
them when it collects results, so no two threads touch one counter and
there is nothing to synchronise. Measured on cornellbox at 32spp, the
fastest scene and so the worst case for relative overhead:
0.420-0.442s with the counters against 0.414-0.444s without, i.e. inside
run-to-run noise. Output is bit-identical.
What it says, across a sparse exterior, a 19M-primitive mountain and an
enclosed box:
- no path anywhere reaches max_depth (0 depth-cap terminations), so the
depth ceiling of 32 is not being paid for;
- mean path length is 1.32-1.49 vertices per camera ray;
- Russian roulette kills 88-95% of the paths it tests, but only reaches
0.13%-25% of paths depending on how enclosed the scene is;
- closest-hit queries run 2.2-2.3 per camera ray, dominated by the
primary ray and first bounce -- which roulette cannot touch, since it
starts at the third vertex;
- throughput differs 17.5x between trivial and production geometry
(37.9 vs 2.16 Mray/s) at similar ray counts.
So the cost is traversal on large scenes, not deep paths.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Render throughput falls about 20x between a trivial scene and a
production one (37.9 Mray/s on cornellbox against 1.5-1.9 on the Moana
elements) at comparable ray counts, and the ray statistics could not say
why. These counters can: they report nodes visited, leaves visited and
primitive tests per query.
Behind the `traversal-stats` feature and compiled out by default, because
they sit in the innermost loop. With the feature on they are global
relaxed atomics, so the counts are trustworthy and the timings are not;
take timings from a default build.
What they show, on one mesh at growing size (traversal_probe):
prims nodes/ray leaves/ray
1 024 6.4 1.5
16 384 9.4 1.4
262 144 11.8 1.3
1 048 576 13.1 1.3
A 1024x increase in primitives costs 2.05x the node visits -- logarithmic,
as a good tree should be -- and leaf work stays flat at ~1.3, one SIMD
packet. So the BVH is not the problem and traversal is not doing more
work at scale; each visit is simply slower.
The arithmetic agrees: isMountainB runs 671 ns/ray over ~13 visits, about
52 ns each, which is a DRAM access. Its BVH nodes occupy 1.22 GiB, so a
visit is a near-certain cache miss. Traversal is memory-latency-bound,
which points at the bytes touched per visit -- WideNode is 128 B, two full
cache lines -- rather than at the tree or the intersectors.
The instanced cases in the probe are inconclusive as written: QUERIES
counts an instance descent as its own query, so their per-ray figures
average outer and inner trees together. Splitting the counters by tree
level would be needed to measure instancing overhead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Tasks 1 and 2 of the traversal investigation.
Counters are now `[top-level, inside an instance]`, keyed off a
thread-local nesting depth that InstancePrim brackets around its descent.
Per-ray figures divide by top-level queries, so instanced work reads as
what it adds per ray rather than being averaged into a query count the
descents themselves inflated.
That fixed a measurement I had got wrong: the first instanced probe packed
unit spheres at 0.05 spacing, so every instance box overlapped every other
and a ray descended into hundreds of them. It reported 609 descents/ray,
which said nothing about instancing and everything about the layout.
Spaced so they do not overlap, single-level instancing is modest -- about
3 descents and 1.3-1.8x the node visits of a flat mesh.
Nesting is not modest. Same 32768 placements of the same prototype,
partitioned into a second level:
flat 32768 descents/ray 3.10 nodes/ray 23.9 136 ms
nested 1024x32 descents/ray 7.22 nodes/ray 27.7 185 ms
nested 32x1024 descents/ray 12.41 nodes/ray 34.7 247 ms
Each level costs a scalar instance test, a ray transform, and a cold
descent into another tree's root. This corroborates a difference on real
data: isMountainB imported through element.usda (nested, 1078 top-level
instances) renders in 1.328s where the same geometry through instance.usda
(flat, 2.29M top-level) takes 0.282s -- 4.7x, at an identical closest-hit
count of 2.11M.
Task 2 also closed the NEE blind spot. Both earlier production
measurements had zero shadow rays because their lights were not LightList
entries. With four of the island's own RectLights, shadow rays are
1 221 390 against 2 114 141 closest-hit -- 37% of all queries, and exactly
one per shaded vertex.
Counters stay compiled out by default; default builds are bit-identical
and the SIMD matrix is unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Renders the original island at 204 us per ray query against 0.63 us for a
19M-primitive element -- 326x, on a scene only 6x larger. That is not a
scale effect, so this adds the two diagnostics that explain it.
Traversal counts now reach the CLI behind `traversal-stats` (forwarded
crust-render -> crust-core -> crust-rt), reported per camera ray. On the
island:
top-level queries 2.67 nodes 11531 leaves 25002 scalar 39439
instanced queries 40292 nodes 56363 leaves 463
Against a flat-instanced element, which runs at 0.63 us/query: 13.67
top-level nodes and 4.67 descents per ray. So the island performs 8600x
the instance descents and touches 25000 top-level leaves where a working
hierarchy touches one to four. It is not culling at all.
Scene::primitive_extents says why: a hierarchy can only separate
primitives whose bounds are small against the whole scene, and element
structure decides whether they are.
isCoastline (flat) 2 prims mean 0.52 of scene max 1.00
isBeach (flat) 22 157 058 prims mean 0.0000 max 0.9999
isMountainB (flat) 2 292 517 prims mean 0.0003 max 0.28
isCoastline presents 2.25M palm-debris instances plus terrain behind two
primitives that each span half to all of the scene, so every ray enters
both and descends into the element's inner tree. isBeach exposes 22M tiny
boxes and culls well.
The builder is not at fault: on one mesh it scales logarithmically, 6.4 to
13.1 nodes per ray across a 1024x growth in primitives, with leaf work
flat at 1.3. It is being handed geometry no hierarchy can organise. The
fix belongs in how the importer groups prototypes, not in the kernel --
which also means the compressed-node work I proposed earlier would have
optimised a traversal that should not be happening.
Counters stay compiled out by default; default builds are bit-identical.
Note the counter build costs ~7x (global atomics contending across 48
threads): trust its counts, never its timings.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Contributor
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.