Skip to content

Optimization part 2 - #120

Merged
doubleailes merged 4 commits into
mainfrom
optimization_part_2
Jul 28, 2026
Merged

Optimization part 2#120
doubleailes merged 4 commits into
mainfrom
optimization_part_2

Conversation

@doubleailes

Copy link
Copy Markdown
Owner

No description provided.

doubleailes and others added 4 commits July 28, 2026 12:18
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>
@doubleailes
doubleailes merged commit 14a28c1 into main Jul 28, 2026
2 checks passed
@doubleailes
doubleailes deleted the optimization_part_2 branch July 28, 2026 10:25
@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

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