The real limit on custom models is not download size (that compresses ~5:1 in the shipped bundle; a 190 MB bundle zips to ~69 MB, and mod.io’s soft limit is 100 MB with generous headroom above). The real limit is a GPU vertex buffer the game packs every skinned mesh into. This page records what that buffer actually is, measured live, and how to budget against it.
Amplitude.Graphics.FxComponentMeshContentManager)FxOneMeshStruct[]), filled by a running
currentVertexIndex / currentIndexIndex / currentMeshAddedCount cursor.DrawMeshInstancedIndirect — each unique mesh is
stored once and drawn for every pawn of that type. 1 tank or 100 tanks of the same type = one
entry. Units on screen are irrelevant to the budget."Unable to store mesh … vertex buffer is not large enough" and drops the mesh — you get
missing / see-through geometry in-game (this is the vanished-rotor-mast class of bug), never a crash.baseVertexBufferSize = 100000 / baseIndexBufferSize = 250000 / maxMeshCount = 256 constants
in the decompiled source are only default initializers — at runtime the game sizes the layers
far larger (see measured values below). Do not trust the source defaults; measure.maxMeshTriangleCount — a
per-mesh triangle ceiling whose overflow is even nastier than the buffer’s: quads beyond it are
silently truncated at encode (FillIndexBufferContentFromQuad clamps to the cap) — the model
renders with holes and nothing is logged. But the shipped layer data sets it to 0 = unlimited on
every layer (verified live), so a single unit’s practical limit is simply the free space left in
the shared pool.Three layers exist; custom models land in the pawn layer (FXMeshLayerIndex = 2):
| layer | name | vertices | indices | meshes | maxTris/mesh |
|---|---|---|---|---|---|
| 0 | Visual |
2,995,550 / 3,000,000 (99%)¹ | 5.73M / 9M (63%) | 4606 / 8000 | unlimited |
| 1 | Emitter |
2,700 / 10,000 (27%) | 10k / 90k (11%) | 20 / 2000 | unlimited |
| 2 | MeshWithSkeletonParticleIndexBuffer ← custom models |
694,126 / 1,000,000 (69%) | 1.48M / 6.5M (22%) | 695 / 2500 | unlimited |
¹ before DistrictBufferHeadroom; with the district headroom set it reads e.g. 5,000,000.
The pawn-layer ceiling is ~1,000,000 vertices / 6,500,000 indices / 2,500 meshes — 10× the source
default. Vertices are the binding constraint (69% used vs 22% indices / 28% mesh slots). Layer 0
Visual is the shared building/district buffer (the district axis draws from it), unrelated to
your unit models.
[Buffers] BufferOverridesAll four limits are plain fields set at layer creation, and the plugin can override any of them
(same Harmony seam as DistrictBufferHeadroom, generalized):
[Buffers]
BufferOverrides = MeshWithSkeleton:verts=+1000000,idx=+2000000,meshes=+1000
<layerNameSubstring>:verts=+N,idx=+N,meshes=+N,maxtris=N, semicolon-separated for several
layers. verts/idx/meshes add to the buffer sizes; maxtris sets the per-mesh cap
absolutely (0 = unlimited). Layer names come from the Mesh Budget dump.[Buffers] '<layer>' baseVertexBufferSize: 1000000 -> 2000000.verts=+1000000 ≈ +28 MB, idx=+2000000
≈ +8 MB. The shader reads buffer sizes dynamically, so the change is transparent.With the override above, a 100k-vert hero unit is entirely practical — the shipped default pool already fits one today (~300k free late-game); the override just restores comfortable headroom for the rest of the roster.
buffer used = Σ (vertices of each distinct loaded model type) — independent of instance count.
Originally we assumed “era-clustering”: that a late-game save loads more of the roster at once. The measurements say otherwise — a brand-new game reads 701,866 verts (70%), virtually identical to a Contemporary save’s 694,126 (69%). The pawn pool is filled at load with (nearly) the full roster’s meshes, regardless of era. Consequences:
BufferOverrides (above) and stop worrying.The plugin exposes the live buffer usage:
BepInEx/LogOutput.log ([Budget] lines). (Shift+F8 logs the same but proved flaky on
some keyboard layouts — prefer the button.)Spawning 10 more of the same unit won’t move the numbers (instancing — copies are free).