Skip to content

Tutorial Custom Validation Policy

Marko Koljancic edited this page May 25, 2026 · 5 revisions

Home

Tutorial: Build a custom validation policy

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

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.


Setup

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.


Step 1: Start from defaults and observe

Create an empty solarxy.toml:

#:schema https://raw.githubusercontent.com/marko-koljancic/solarxy/main/schemas/solarxy-config.v1.json

format_version = 1

Run 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.


Step 2: Define per-category triangle budgets

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 matches

Pick 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.


Step 3: Map filenames to categories

[[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 ^ so hero_ doesn't accidentally match villain_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.


Step 4: Tune validation toggles

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
Loading

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 = true

Decisions to make explicitly:

  • allow_open_mesh - default off. Flip to true only 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_refs and index_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.


Step 5: Tune thresholds

[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.5

The 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.


Step 6: Re-run and iterate

Re-run your local validator after every meaningful change:

solarxy-cli --mode analyze --paths "**/*.glb" "**/*.gltf" --fail-on error

Goal: 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.

The annotated solarxy.toml with schema completions in the editor


Verify

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.


What you learned

  • 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_mesh is an inverse modifier. It is not a check - it suppresses one branch of non_manifold_edges. Use it sparingly.
  • Discovery is bounded. When you don't pass --config, Solarxy walks upward from the start directory until it finds a solarxy.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.

Where to go next


See also: Configuration · Validation Reference · CI/CD Integration · Tutorial: Validate in CI

Clone this wiki locally