GLBForge

Budget profiles: methodology and versions

GLBForge budgets are contracts, not advice: glbforge analyze exits non-zero when an asset breaks its profile, the GitHub Action gates on it, and optimize targets it. A contract you cannot pin is not a contract, so every profile is versioned. A cap never changes in place: any change is a new version appended here with the reason, and --profile mobile-hero@1 (CLI, Action, MCP) keeps meaning exactly what it meant when you wrote it. --profile mobile-hero resolves to the latest version. Reports carry the label (mobile-hero@1), and list_profiles rationale=true returns this text from the MCP server.

Live copy: https://glbforge.dev/budgets/ · Source of truth: packages/core/src/profiles.ts (every published version is frozen there and tested).

The model behind the numbers

The caps come from a small explicit model rather than from any one device:

These are working assumptions, stated so they can be argued with. When usage data disagrees, the cap moves in a new version — never silently.

How the score is computed

perf/* findings are budget violations (errors, −15 each; any error fails the asset). geo/*, topo/*, mat/*, tex/*, scene/* findings are warnings (−5) or info (0) describing defects typical of AI-generated assets, each with a concrete fix. fidelity/perceptual is an error when the measured SSIM is under the floor and an info finding carrying the number otherwise. Score = 100 − penalties, floored at 0.

Profiles (current versions)

mobile-hero@3 — single hero asset on a mobile landing page (4G, mid-range GPU)

capvaluewhy
maxTriangles150,000A mid-range phone GPU (2019+ Adreno 6xx / Mali-G7x / Apple A12 class) rasterizes 150k triangles in well under a millisecond; the binding constraint is payload. 150k welded, quantized, meshopt-compressed triangles land around 1-2MB, which is what leaves room for textures inside the file cap. Counted over the scene: a mesh placed by five nodes costs its triangles five times, because that is what is drawn.
maxDrawCalls4Counted as one call per primitive per node that places it — the floor a renderer without instancing support pays. Four keeps the per-frame state changes inside the budget of a mid-range mobile GPU driver; merging primitives that share a material is the cheapest way down.
maxTextureSize2048 pxA 2K RGBA8 texture with mipmaps is ~21MB of GPU memory; a color/normal/ORM set of three fits the VRAM cap. 4K quadruples that and rarely reads sharper on a phone-sized viewport.
maxTextureBytes4 MBCompressed image payload inside the GLB. WebP at quality 82 (near-lossless for normal maps) keeps three 2K maps around 1–3MB.
maxTextureVramBytes128 MBDecoded, mipmapped GPU memory. Mobile browsers share GPU memory with the OS and drop WebGL contexts that get greedy; 128MB leaves headroom for framebuffers, the DOM, and a second asset. KTX2/BasisU counts at its GPU-compressed size (~4–8x less), which is why the KTX2 path exists.
maxFileBytes6 MBAbout 3–5 seconds on a typical 4G link (10–15Mbps effective) — the most a hero can hide behind a poster image before it reads as broken; under a second on Wi-Fi or 5G.
maxMaterials2Materials multiply shader variants and texture sets. A hero is one material, two when a glass or emissive part is unavoidable.
minSsim0.94Weakest of four fixed-camera views (256px, 2x supersampled, smooth shading, textured) before vs after optimization. Calibrated on the Meshy 7 fixture: the budget pass measures 0.964 with 4K source textures (0.979 with 2K), a 40k-triangle version 0.913 with visibly merged hair strands. The floor sits between them.

desktop-hero@3 — hero asset on a desktop-first marketing page

capvaluewhy
maxTriangles500,000Integrated desktop GPUs handle 500k triangles per frame comfortably; beyond that, payload and parse time on first load dominate, not raster cost. Counted over the scene, so instanced placements each cost their triangles.
maxDrawCalls8Desktop browsers absorb more state changes per frame, and a desktop hero is often a small assembly (product + stand + shadow catcher). Counted as one call per primitive per node that places it.
maxTextureSize4096 pxA 4K color map can be justified on a large viewport where the hero fills half the screen; a 4K RGBA8 with mips is ~85MB decoded, so only the color map should be 4K.
maxTextureBytes12 MBOne 4K color map plus 2K normal/ORM maps in WebP.
maxTextureVramBytes256 MBDesktop GPU memory is plentiful but shared with tabs and the compositor; 256MB keeps a two-asset page under half a gigabyte.
maxFileBytes20 MBAbout 3 seconds on a 50Mbps connection; desktop visitors tolerate a progressive reveal behind a placeholder up to that.
maxMaterials4Four materials cover a typical product hero (body, glass, metal trim, screen) without turning into a material zoo.
minSsim0.96Desktop heroes are viewed larger, so the floor is stricter than mobile: the budget pass to 500k measures 0.986–0.993 on our fixtures, clearing it either way.

product-configurator@3 — interactive product viewer; many assets may coexist

capvaluewhy
maxTriangles250,000Configurators keep several variants resident and the camera gets close; 250k per asset balances close-up fidelity against having three or four assets loaded at once. Counted over the scene, so instanced placements each cost their triangles.
maxDrawCalls12Swappable parts are separate meshes by design (a draw call each), so the cap is higher than a hero — but still a dozen, not a hundred. Counted as one call per primitive per node that places it.
maxTextureSize2048 px2K keeps a multi-variant texture set inside the shared VRAM cap; configurators rarely benefit from 4K because the camera moves and materials swap.
maxTextureBytes8 MBRoom for a full PBR set (color, normal, ORM) at 2K in WebP plus one variant map.
maxTextureVramBytes128 MBSame ceiling as mobile because a configurator page often IS on mobile, and several assets share it.
maxFileBytes12 MBConfigurator assets load on demand behind an explicit user action, so a slightly larger file is acceptable than a hero that must appear on first paint.
maxMaterials8Materials are the point of a configurator (colorways, finishes); eight covers realistic part counts while keeping shader compilation bounded.
minSsim0.95Close-up viewing argues for strict, coexistence argues for lenient; 0.95 is the midpoint, and the budget pass to 250k measures 0.977–0.987 on our fixtures.

Changing a cap

  1. Append a new Profile object (version N+1) in profiles.ts; never edit a published one (the test suite freezes v1's numbers).
  2. Update the rationale for every cap that moved and add a changelog entry below with the evidence.
  3. getProfile('name') now returns N+1; anyone pinned to name@N is unaffected until they opt in.

Changelog