-
Notifications
You must be signed in to change notification settings - Fork 0
Tutorial Custom Validation Policy
← Home
You're a technical director taking ownership of your studio's asset
validation gate. The defaults are sensible but don't match your pipeline:
your hero characters need a higher budget, your foliage is deliberately
open-mesh, and your filename convention encodes asset class in a way the
default rules don't recognize. This tutorial walks you through building a
solarxy.toml policy from scratch on a representative folder of your
assets, iterating until you have zero false positives on known-good assets
and expected findings on known-bad ones.
By the end you'll know how [budgets], [[filenames.rules]],
[validation], and [thresholds] compose, and you'll have made the
inverse-modifier call on allow_open_mesh for your pipeline.
- Goal
- Setup
- Step 1: Start from defaults and observe
- Step 2: Define per-category triangle budgets
- Step 3: Map filenames to categories
- Step 4: Tune validation toggles
- Step 5: Tune thresholds
- Step 6: Re-run and iterate
- Verify
- What you learned
- Where to go next
A solarxy.toml that gates your studio's CI without false positives on
known-good assets and that catches the failure modes your team has actually
hit in production.
You need:
- A folder of representative assets covering every category you care about: at least a couple of "obviously fine" assets per category, plus any known-bad asset you've used as a stress test.
- An editor with TOML schema support (VS Code with Even Better TOML, any
JetBrains IDE,
taplo). The schema URL goes in step 1.
Open a terminal at the asset folder root for the rest of the tutorial.
Create an empty solarxy.toml:
#:schema https://raw.githubusercontent.com/marko-koljancic/solarxy/main/schemas/solarxy-config.v1.json
format_version = 1Run validation against your asset folder with the defaults:
solarxy-cli --mode analyze --paths "**/*.glb" "**/*.gltf"The output tells you which assets the defaults flag. Note which findings are real and which are false positives - those are what you'll tune away in the next steps.
The default budgets are hero=100000, prop=20000, environment=50000,
default=30000. With no [[filenames.rules]], every asset is classified
as default. This is rarely what you want; step 3 fixes it.
Add a [budgets] table with your studio's actual budgets. Common shape:
[budgets]
hero = 250000 # main characters - high detail
mid = 80000 # secondary characters / important props
prop = 20000 # ordinary props
background = 10000 # set dressing, far-distance items
default = 30000 # the fallback when no rule matchesPick numbers that match your renderer's per-frame triangle budget divided by "how many of this class can be on-screen at once." If your draw-call overhead is the bottleneck, tighten props more than heroes; if your fragment load is the bottleneck, tighten background instead.
The category names you use here are arbitrary strings - you wire filenames to them in the next step.
[[filenames.rules]] is an ordered list of regex patterns matched against
the filename only (not the full path). First match wins; unmatched
files fall back to the default category.
[[filenames.rules]]
pattern = "^hero_"
category = "hero"
[[filenames.rules]]
pattern = "^char_(npc|player)_"
category = "mid"
[[filenames.rules]]
pattern = "^prop_"
category = "prop"
[[filenames.rules]]
pattern = "^(env|set|bg)_"
category = "background"Tips that pay off later:
-
Anchor with
^sohero_doesn't accidentally matchvillain_hero_v2. -
Group alternates with
(a|b|c)rather than three near-duplicate rules. Cheaper to read and to maintain. - Order matters. Put your most specific patterns first; once a rule matches it doesn't fall through to the next.
Re-run validation. Findings should now reflect each category's budget, not
the default.
flowchart TD
classDef warn fill:#1F2430,stroke:#FFC44C,color:#FFC44C
classDef err fill:#1F2430,stroke:#FF3333,color:#FF3333
classDef both fill:#1F2430,stroke:#A06DFF,color:#CCCAC2
classDef mod fill:#1F2430,stroke:#5C6773,color:#5C6773
classDef io fill:#33415E,stroke:#FFC44C,color:#FFC44C
L[Load model<br/>validate_raw_model_with_config]:::io
L --> A[triangle_budget<br/>Warning or Error]:::both
A --> B[normal_mismatch<br/>Error]:::err
B --> C[flipped_normals<br/>Warning]:::warn
C --> D{file is<br/>obj/gltf/glb?}
D -->|yes| E[uv_presence<br/>Warning]:::warn
D -->|no| F[index_buffer<br/>Error]:::err
E --> F
F --> G[material_refs<br/>Error]:::err
G --> H[non_manifold_edges<br/>Error or Warning]:::both
H --> I{allow_open_mesh?}:::mod
I -->|true| J[suppress boundary warning]:::mod
I -->|false| K[keep all warnings]:::both
J --> M[degenerate_triangles<br/>Warning]:::warn
K --> M
M --> R[ValidationReport]:::io
Where each toggle gates a check. Flip allow_open_mesh to true to suppress only the boundary-edge warning from non_manifold_edges.
[validation] toggles the nine checks. All default to on except
allow_open_mesh. The most common policy adjustments:
[validation]
# If your foliage / cards / hair planes are deliberately open mesh,
# suppress the boundary-edge warning that would otherwise fire on every
# leaf card.
allow_open_mesh = true
# If you process meshes through a non-triangulated path and re-triangulate
# downstream, the index_buffer check will fire on quads. Decide whether
# you want CI to gate on that.
# index_buffer = false
# UV checks fire on STL/PLY anyway (those formats have no UVs); for OBJ /
# glTF this catches missing or count-mismatched UVs.
# uv_presence = trueDecisions to make explicitly:
-
allow_open_mesh- default off. Flip totrueonly if your pipeline legitimately ships open meshes. Don't blanket-enable it just to silence noise on solid assets that have unintended boundary edges. -
non_manifold_edges- off only for deeply specialized pipelines (NURBS conversions, point-cloud-derived meshes). For most game pipelines, leave it on. -
material_refsandindex_buffer- the cheap structural checks. Leave on unless you have a very specific reason to suppress.
For the full per-check semantics and how to fix what each one flags, see Validation Reference.
[thresholds] holds the numeric knobs.
[thresholds]
# A triangle count over budget warns; over budget*(1+tolerance%) errors.
# Default 20.0 (a 20% grace band). Tighten to 10.0 for stricter gates.
triangle_budget_tolerance_percent = 15.0
# A triangle's averaged vertex normal vs. its geometric normal must dot
# below this to be flagged "flipped". Default -0.5 (~120 degrees).
# Tighten toward 0.0 to catch borderline flips; loosen toward -1.0 to
# suppress noisy flagging.
flipped_normal_dot = -0.5The triangle_budget_tolerance_percent knob is the soft-fail / hard-fail
boundary - the warning is what your artists see during their own runs, the
error is what blocks CI. Pick a tolerance band that gives artists room to
iterate without letting genuine budget breaks slip through.
Re-run your local validator after every meaningful change:
solarxy-cli --mode analyze --paths "**/*.glb" "**/*.gltf" --fail-on errorGoal: zero errors on every known-good asset, plus the expected findings on the known-bad ones. If a finding is real but unimportant for your pipeline, the right move is usually:
- A toggle in
[validation](suppress the check entirely if it doesn't apply to your pipeline). - A threshold loosening in
[thresholds](keep the check, accept a wider range).
Avoid the trap of accepting false positives by widening tolerance globally. Prefer per-check toggles or per-category budgets where the policy applies.
By the end of the iteration loop your asset folder should produce:
- Zero errors on every asset you consider known-good.
- Expected findings (matching what you put in deliberately) on every known-bad asset.
- The same behaviour on every developer's laptop, because the policy is checked into the repo.
Commit solarxy.toml alongside your assets. Pair it with the
CI tutorial to enforce the policy on every PR.
-
The four sections compose top-down.
[budgets]defines the per-category numbers;[[filenames.rules]]decides which budget any given asset uses;[validation]decides which checks run;[thresholds]tunes the checks that take a knob. -
allow_open_meshis an inverse modifier. It is not a check - it suppresses one branch ofnon_manifold_edges. Use it sparingly. -
Discovery is bounded. When you don't pass
--config, Solarxy walks upward from the start directory until it finds asolarxy.toml, hits.git/, or 20 levels - whichever comes first. See Configuration → Discovery order. - Strict parsing catches typos at load time. A misspelled key hard-fails with a line:column and a "did you mean?" hint - your policy is loud about its own mistakes.
- Tutorial: Validate in CI - put your policy in front of every pull request.
- Validation Reference - the per-check causes and fixes that inform which toggles you flip.
- Configuration - the full schema for every field used here.
See also: Configuration · Validation Reference · CI/CD Integration · Tutorial: Validate in CI
Solarxy - A lightweight, cross-platform 3D model viewer and validator built with Rust and wgpu.
GitHub Repository · Releases & Downloads
© 2026 Marko Koljancic · MIT License
Getting Started
Tutorials
Using Solarxy
Reference
Help
Project