Skip to content

Uploader Auto LOD: skip the prune runs and preparations it throws away - #575

Merged
RyeMutt merged 3 commits into
developfrom
rye/uploader-auto-costs
Oct 26, 2026
Merged

RyeMutt merged 3 commits into
developfrom
rye/uploader-auto-costs

Conversation

@RyeMutt

@RyeMutt RyeMutt commented Oct 26, 2026

Copy link
Copy Markdown
Member

Cuts the two costs #571 found left in the mesh uploader's Auto LOD mode: the prune check's double run and a simplifier prepared again for every LOD. Every LOD Auto makes is unchanged, to the bit.

Measured on the mesh cache (4,000 meshes, 1,310 models of 1,000+ triangles, 3,930 Auto cases for LODs 2 to 0). Develop's simplifier was replayed beside this branch's on each case and the faces compared bit for bit.

develop this branch
Faces differing from develop's 0 of 3,930
Attribute-aware simplifications 7,369 5,642
Preparations 3,930 1,310
Auto 32.7 s 25.2 s (−23%)

Absolute times moved with the machine's load from run to run. Every figure here compares develop and the branch within one run.

A distant simplification of large parts runs once (ebea7a8)

LODs 1 and 0 simplify with meshoptimizer's Prune. Where that run dropped a part larger than 5% of the model, or every part, simplifyMesh threw it away and simplified again. The second pass prunes the parts within 5% first, then simplifies without pruning. That happened in 2,225 of the cache's 3,626 prune runs, each a whole simplification thrown away. That is far more than the 389 cases of six simplifications #571 counted.

meshoptimizer prunes only whole components. Its components are exactly the parts largestDropped measures: the same float-equal position remap, and the same union of each triangle's corners. So where every part is larger than 5%, the prune run always ends as the simplification that prunes nothing:

  • Drops nothing: it is that simplification bit for bit. Prune changes nothing else in meshoptimizer 1.3's edge collapse.
  • Drops a part: it is thrown away, and the second pass has nothing to prune.

"Larger than 5%" has to hold both ways: as largestDropped measures a part, with every distance finite, and as meshopt_simplifyPrune does. The simplifier checks that once per model, while preparing it. Where it holds, it goes straight to the plain simplification.

  • All 2,563 such runs on the cache were checked against the old path. None differed.
  • Simplifications: 7,369 → 5,779. Auto: 33.0 → 29.2 s.
  • The check adds about 0.25 ms to a model's preparation.

Faces simplified one at a time (Precise and Sloppy) keep the old path.

Auto skips NONE where it is UV (fe2dadf)

UV and NONE differ only in UV protecting vertices on texture seams. Where every such vertex is also at a position another face has, both modes already protect it, so they make the same simplification. Auto reaches NONE only after UV fell short, so NONE would fall exactly as short and be thrown away.

ALMeshSimplifier::same says when two modes are the same simplification, and the loop stops before NONE there. That holds for 131 of the cache's 520 NONE passes.

ALL and UV came out the same in 204 of 694 UV passes. Proving it needs meshoptimizer's own vertex classification, so it is left alone.

One simplifier for each base model (0965df5)

genMeshOptimizerLODs built a simplifier for every model on every call, and Auto generates each LOD in a call of its own. LLModelPreview now keeps one per base model in mSimplifiers, used for every LOD after the first. An entry is made again when its model is another object, or its material list has changed: matchMaterialOrder reorders faces only together with the material list. Every entry is dropped:

  • in generateNormals and restoreNormals, which rewrite faces;
  • wherever the base models are replaced.

The entry's LLPointer keeps its model alive, so a new model can't land on a freed one's address and pass for it. Preparations went from 3,930 to 1,310.

Testing

  • llmath_tests: 482 cases (develop's 479 plus 3 new). All pass apart from the corpus case, which skips without a corpus.
  • The hidden [almeshsimplifier][corpus] case passes on 1,472 cached meshes.
  • Each new test fails against a deliberately broken version of what it guards:
    • prune in the shortcut: the large-parts test fails;
    • same() ignoring texture seams: the sphere check fails;
    • a simplifier that remembers its last call: the reuse test fails.
  • The existing small-parts test fails if the check takes a model with small parts for one of large parts.
  • llmodelpreview.cpp compiles against a freshly built viewer PCH.
  • Not checked in the viewer: it wasn't linked, and no upload was tried in it.

Left out

The per-face prune check, which Precise and Sloppy use, still runs twice. The same check would apply there face by face, but it wasn't measured, so it is unchanged.

🤖 Generated with Claude Code

RyeMutt and others added 3 commits October 25, 2026 23:33
A distant level of detail (LOD 1 and 0) is simplified with meshoptimizer's
Prune. Where that run dropped a part larger than PRUNE_LIMIT, or every
part, simplifyMesh threw it away and simplified again: first pruning the
parts within the limit, then without pruning. In Auto, over the mesh
cache, 2,225 of 3,626 prune runs were thrown away like that, each a whole
attribute-aware simplification spent for nothing.

meshoptimizer prunes only whole components, and its components are the
parts largestDropped measures: the same float-equal position remap, the
same union of each triangle's corners. So where every part is larger than
PRUNE_LIMIT, both as largestDropped measures it (with every distance
finite) and as meshopt_simplifyPrune does (it keeps every index at the
limit), the prune run comes to the simplification that prunes nothing,
whatever it does. If it drops no part, it is that simplification bit for
bit: Prune changes nothing else in meshoptimizer 1.3's edge collapse. If
it drops one, it is thrown away, and the second path prunes nothing
before simplifying. The simplifier now finds this once, when it prepares
the model, and goes straight to the plain simplification. largestDropped
and the new check share measureParts, so both see each part's radius
alike.

Faces simplified one at a time (PRECISE and SLOPPY) keep the old path.

Measured on the mesh cache (4,000 meshes, 1,310 models of 1,000+
triangles, 3,930 Auto LOD cases), replaying develop's simplifier and this
one side by side on each case:
- Every LOD's faces are identical to develop's, indices and attributes.
- In the 2,563 prune runs where every part was large, 1,590 of them thrown
  away, the old path's result was compared with the plain simplification.
  None differed.
- Attribute-aware simplifications: 7,369 -> 5,779.
- Auto: 33.0 -> 29.2 s.
- Preparation costs about 0.25 ms more a model, for the check.

The new test fails if the shortcut prunes; the existing small-parts test
fails if the check takes a model with small parts as all large.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UV and NONE differ only in UV protecting the vertices on texture seams.
Where every such vertex is at a position another face has too, which both
modes protect already, they hold the same vertices with the same options
and make the same simplification. Auto reaches NONE only once UV has
fallen short; NONE then falls exactly as short, is never the closer, and
is thrown away. ALMeshSimplifier::same says when two modes are one, and
the Auto loop stops before NONE there.

Measured on the mesh cache (3,930 Auto LOD cases, develop's simplifier
replayed beside this one):
- 131 of Auto's 520 NONE passes are provably UV's. Comparing outputs,
  138 came out the same; the other 7 only happened to.
- Every LOD's faces are identical to develop's.
- Attribute-aware simplifications: 5,779 -> 5,642, with the previous
  change.

ALL and UV came out the same in 204 of 694 UV passes. Proving that needs
meshoptimizer's own vertex classification (every vertex it takes as a
seam or locked would have to be protected), so it is left alone.

The new test fails if same() takes a model with a texture seam inside a
face as one whose UV and NONE simplifications agree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
genMeshOptimizerLODs built an ALMeshSimplifier for every model on every
call. Auto generates each level of detail in a call of its own, so every
model was prepared three or four times over: the face welds, attribute
weights, seam and border flags, and now the parts check, all the same
each time. That preparation was 13% of Auto's time.

LLModelPreview now keeps a simplifier for each of mBaseModel's models in
mSimplifiers, made the first time a level of detail is generated from the
model and used for every level after. A simplifier reads its base as it
was, so an entry is made again whenever its model is another object, or
its material list differs from the one it was made with. rebuildUploadData's
matchMaterialOrder reorders a model's faces only together with its
material list. Every entry is dropped where generateNormals or
restoreNormals rewrite faces, and where the base models are replaced. The
entry's LLPointer keeps its model alive, so a new model can never take a
freed one's address and pass for it.

Measured on the mesh cache (3,930 Auto LOD cases for LODs 2 to 0, develop
replayed beside this branch):
- Every LOD's faces are identical to develop's.
- Preparations: 3,930 -> 1,310.
- Auto: 32.7 -> 25.2 s, -23% with the two changes before it, of which
  23.4 s is the simplifications and 1.9 s the one preparation of each
  model.

The viewer also regenerates LOD 3 through the simplifier when it
generates every level, which shares the preparation once more.

The new test fails for a simplifier whose result depends on what it
simplified before, which a kept simplifier must not do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 14 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: bd0a0012-d36d-46ae-a4bb-d53f4420c974
Commits

Reviewing files that changed from the base of the PR and between 35ed2ff and 0965df5.

Files selected for processing: 5
  • indra/llmath/almeshsimplifier.cpp
  • indra/llmath/almeshsimplifier.h
  • indra/llmath/tests/almeshsimplifier_test.cpp
  • indra/newview/llmodelpreview.cpp
  • indra/newview/llmodelpreview.h
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@RyeMutt
RyeMutt merged commit dc247a5 into develop Oct 26, 2026
23 checks passed
@RyeMutt
RyeMutt deleted the rye/uploader-auto-costs branch October 26, 2026 11:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant