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). For unit models 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. But it is only one of THREE separate draw-budget systems (units / districts / terrain), each limiting something different — terrain, notably, stores no vertices at all. The full map is in Three pipelines, three different budgets below; the sections in between are the unit pipeline’s detail.

The mechanism (decompiled: Amplitude.Graphics.FxComponentMeshContentManager)

Measured live (F8 window — the always-on “GPU mesh buffer (live)” readout, 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.

Do not confuse that vertex total with Model Factory’s Reduce to ~tris field. The field is a Blender triangle ceiling; it is not a vertex target and there is no stable tris→verts conversion. UV seams, material boundaries, skin weights, and import splitting can make the baked vertex count substantially higher than the triangle count. Use the bake log for the model’s actual verts= cost and F8 for the roster-wide total.

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.

Three pipelines, three different budgets — the map of every draw limit

There is no single “vertex limit” in this game. Three separate rendering systems each budget something different, and they fail the same way (silently not drawn) for different reasons. Everything below is measured, not assumed:

Pipeline What is stored Pool limit (shared) Per-mesh limit Raise it with
Units (pawns) baked skinned meshes, one copy per TYPE (instances free) ~1,000,000 verts (pawn layer; ~700k used by the roster) 16,320 quads per FRAGMENT (compiled shader stride) [Buffers] BufferOverrides for the pool; the 0.5.7 multi-fragment split (both bake paths since 2026-09-20) for the per-mesh cap

THE TWO CEILINGS PULL AGAINST EACH OTHER (2026-09-20, the steam frigate). The split is the cure for the per-fragment quad ceiling, and it is paid for out of the pool: each chunk duplicates the vertices on its seam. How much depends entirely on where the cuts fall — that ship went 99,676 → 125,369 verts (+26%) as four static fragments, but 99,676 → 100,648 (+1%) as four animated ones. Read your own bake’s BAKED MESH lines rather than assuming either figure. The pool is shared by every unit type, and when it fills the game stops uploading meshes entirely — units and districts both stop drawing, with no error. That day’s buffer was already doubled to 2,000,000 by BufferOverrides and sat at 1,999,968; one static re-bake of one ship was the straw. Read the fill with F8 (L2 … verts %), and raise the pool with BufferOverrides = MeshWithSkeleton:verts=+2000000 (roughly +48 MB VRAM per million) before splitting a second large model. Reducing triangles pays both ceilings at once; splitting pays one by spending the other. | Districts / buildings | baked static meshes in the shared Visual layer | 3,000,000 verts, ~99% full late-game — the tightest real vertex wall | 255 sub-particles × PPC (PPC dynamic, auto-boosted since 0.5.7) | DistrictBufferHeadroom for the pool; DistrictMeshDensityBoost floor for the ceiling — District-Visuals | | Terrain tiles | NO stored vertices at allProceduralTerrainRenderer GENERATES the hex geometry on the GPU every frame (visibility kernel → repack → draw-procedural + tessellation) and throws it away | n/a — vertices are manufactured per frame | 10,000 visible hexagons per frame (post-culling) and 800,000 draw commands, both plain ints on the technical-settings asset | [Terrain] TerrainHexagonBufferMultiplier (0.5.7, experimental — section below) |

The practical consequences:

Other engine ceilings — the terrain tile limit (community lead, UNVERIFIED)

Recorded 2026-09-13 after the multi-fragment unit split shipped (0.5.7): a community report (Discord) says map tiles beyond roughly 21,000 stop rendering, that each tile draws as 3 parts, and asks whether the unit trick generalizes. Everything below is a hypothesis on one probe session — treat it per the Review-Backlog rules (re-verify before acting), it is not a finding.