HumankindAssetFramework

The GPU Vertex Budget — how many custom models you can add

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.

The mechanism (decompiled: Amplitude.Graphics.FxComponentMeshContentManager)

Measured live (F8 window ▸ “Mesh Budget”, a Contemporary-era save)

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

Raising the ceilings — [Buffers] BufferOverrides

All 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

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.

What actually counts

buffer used = Σ (vertices of each distinct loaded model type) — independent of instance count.

The fill is roster-wide at load, not per-era

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:

How to measure (in-game)

The plugin exposes the live buffer usage:

Spawning 10 more of the same unit won’t move the numbers (instancing — copies are free).

Answered (previously open) questions

  1. When do meshes register? At load, roster-wide: a fresh Era-0 game already reads ~70% — the same as a Contemporary save. Not on-demand per spawn, not per era.
  2. Per-era unload vs accumulate? Moot — the pool is filled up-front and stays ~constant. The real ceiling is “the whole loaded roster,” identical in every era, and it’s ~700k/1M with vanilla + the current ENC set.