Game guide · source of truth
Tech

Performance budgets

Frame, memory, draw-call, triangle, texture, particle, audio, download and network budgets per quality tier, plus the techniques that keep Zoen smooth on browsers and phones.

Optimisation is a design input, not a final pass. Every asset and system has a budget before it is built, and the budget is checked by a logged test. Outcomes are identical on every tier. Tiers only change how things look.

Quality tiers (outcomes identical on all)

◆ Low

Target
30 fps
Draw calls
250
Triangles
400k
Players shown
25
Own particles
300
Shadows
blob
View
120 m

◆ Mid

Target
30 fps
Draw calls
600
Triangles
800k
Players shown
40
Own particles
800
Shadows
1 × 1024
View
180 m

◆ High

Target
60 fps
Draw calls
1,500
Triangles
2.5M
Players shown
60
Own particles
2,000
Shadows
2 × 2048
View
260 m

◆ Ultra

Target
60+ fps
Draw calls
2,500
Triangles
4M
Players shown
100
Own particles
3,000
Shadows
3 × 4096
View
400 m

◆Frame targets

TierTypical deviceTargetFrame budgetRender scale
Lowselected entry-level references below30 fps33.3 msdynamic 0.6–0.85
Midmid-range phones and laptops, additional references30 fps (60 optional)33.3 msdynamic 0.75–1.0
Highdesktop (owner's M3), high-end phones60 fps16.7 ms1.0
Ultradesktop dGPU60+ fps16.7 ms1.0 + extras

Within a frame on High: gameplay/sim ≤ 2 ms, animation ≤ 2.5 ms, culling/LOD ≤ 1 ms, render submit ≤ 4 ms, UI ≤ 1 ms, the rest goes to the GPU. Dynamic resolution holds the frame budget before any visual feature is dropped.

Platform limit (checked 2026-10-06): Unity supports dynamic resolution on Metal, Vulkan and DirectX 12 only (Unity manual). The Web build and any OpenGL ES device need a fixed render scale picked by the capability probe and changed only at safe moments. The GPU Resident Drawer has similar limits (Forward+ and compute shaders; not Web or OpenGL ES; Mesh Renderers only). The per-platform table, five named perf scenes and the extra targets (zero garbage in combat, shader prewarm, frame spread) are proposed in the AAA detail catalog.

◆Reference hardware (owner direction, 2026-10-05)

Use entry-level hardware as the floor, as delegated by the owner. These selected configurations require physical validation; their specifications do not establish measured game compatibility. Canonical record: reference-devices.json.

PlatformSelected Low referenceBasis and source
AndroidSamsung Galaxy A07 4G/LTE, Helio G99, 4 GB physical RAM, 128 GBCurrent entry-level configuration; distinguish from 5G/higher-RAM variants. Samsung specifications.
iOSiPhone SE 2nd generation (2020), A13 BionicOlder lower-end hardware supported on current iOS 27; small-screen touch/readability stress. Apple specifications, Apple compatibility.
WindowsASUS Vivobook Go 15 E1504TA-class, Intel N150, integrated Intel UHD, 8 GB RAM, SSD, Windows 11Entry-level laptop configuration; verify the exact local SKU rather than substituting a faster variant. ASUS specifications.

Sources checked 2026-10-05. Record each physical unit's exact SKU, OS build, GPU driver/API, real RAM and storage; RAM expansion is not physical RAM. No devices have been purchased or benchmarked by this documentation task. The capability probe (M0-05) logs one ZOEN_PROBE line per client run against the contract in reference-devices.json → probe; owner-device results are labelled with their probe and never validate a Low reference, whose probes stay pending until units exist. The owner's M3 stays a secondary development reference (Chrome; its native app is paused since 2026-10-11, ADR-032) and the owner's Windows PC (unprobed until WIN-01) is the High test machine; neither can substitute for the Windows Low probe. The Mac's native app and Chrome runs are measured (2026-10-08, M0-05C: ownerTestDevices[].probes, merged table logs/evidence/M0-05C/probe-summary.txt); so is the owner's phone (nubia NX709S) in Chrome (2026-10-08, M0-05D) and as the native APK (2026-10-08, M0-05E: Vulkan 1.1, 18 GB class memory, ASTC/BC/ETC2, Mid from the cached benchmark). Unity's Android player cuts a log entry at 1023 bytes, so on Android the probe is logged as ZOEN_PROBE_PART i/n lines of at most 800 bytes (reference-devices.json → probe.partNote).

The starting gate reuses Low 30 fps / 33.3 ms / process or tab ≤600 MB and the existing 30-minute playtest. Profile native and supported browsers separately: both views, camp/crowd/boss telegraphs, boss music, sustained mana-limited 0.3 s dash reuse, jumps, collisions, chunk streaming and background/resume. Log p50/p95/p99 frame time, draw calls, particles, memory, thermal degradation and input response. Keep warnings visible when reducing decoration. Exact acceptance targets remain proposed until the feasibility gate supplies measurements. A failure requires optimization or an explicit baseline revision; do not silently raise minimum hardware.

◆Platform caps (owner, round 4)

PlatformQuality rangeDefault
BrowserLow–Mid only (hard cap)Mid (Low on weak devices)
Windows appLow–UltraHigh
Android / iOS appsLow–Ultra (Ultra only on devices that pass the thermal/frame gate)Mid

The full High/Ultra experience is in the downloaded apps; the browser stays light and smooth. Data: feel-quality.json → platformCaps. The client's starting-tier rules (low-memory phone, low-end browser, native startup benchmark, the phone Ultra gate) and the per-tier frame caps above are proposed in feel-quality.json → tierSelection (M0-04C); the client reads them from there. Engine review: Engine & platforms. Capability probing selects Low on the entry-level references before heavier content loads; platform defaults do not force Mid/High onto weak hardware.

◆Budgets by tier

BudgetLowMidHighUltra
Draw calls (after instancing/SRP batching)2506001,5002,500
Triangles on screen400k800k2.5M4M
Visible players (default / max)25 / 4040 / 6060 / 100100 / 150
Live particles (own / total)300 / 900800 / 2,0002,000 / 5,0003,000 / 8,000
Shadowsblob only1 cascade 10242 cascades 20483 cascades 4096
Post FXtonemap only+ bloom (half-res)+ SSAO, colour grading+ volumetric fog
View distance120 m180 m260 m400 m
Vegetation density0.30.61.01.3
GPU texture memory256 MB512 MB1 GB1.5 GB
Memory (process; Web: tab)≤ 600 MB≤ 900 MB≤ 1.5 GB≤ 2 GB
Audio voices16243248

◆Asset budgets

AssetLOD0LOD1LOD2Crowd (VAT)Textures
Player (body + armour)22k tris9k3.5k1.5k2K atlas (BC7/ASTC via KTX2)
Monster small / medium / large4k / 8k / 14k½¼—1K / 1K / 2K
Boss40k18k8k—2K × 2
Prop0.3–5k½impostor—512–1K, atlased
Tree2–6k1.5kbillboard impostor—1K atlas
Terrain chunk (256 m)2× 129² grid + skirts65²33²—4 splat layers per chunk, 2K detail arrays

Textures compress per platform on import: BC7 (Windows), ASTC (Android/iOS), ASTC/ETC2 for the Web build. Meshes use Unity mesh compression; streamed content ships as Addressables bundles. Skinned meshes use ≤ 4 bone influences and ≤ 80 bones.

◆Download and startup

ItemBudget
First load to login screen≤ 15 MB, ≤ 6 s desktop broadband, ≤ 12 s 4G
Town (Amber Gate) playable+ 25 MB streamed
Per 256 m chunk0.5–2 MB, streamed 2 rings ahead of the player
Unity Web buildLow–Mid only; IL2CPP + managed stripping High, Brotli, Addressables per zone (no High/Ultra content in the web catalog)

◆Network and server

Downstream ≤ 12 KB/s typical, ≤ 32 KB/s cap per client. Upstream ≈ 0.3 KB/s. Server tick p99 ≤ 15 ms at 1,000 players. Details in 1,000 players per map.

◆Techniques we rely on

  • Instancing everywhere: GPU Resident Drawer / GPU instancing (RenderMeshInstanced) for vegetation, rocks, projectiles, damage numbers and baked-animation crowds.
  • SRP Batcher (all targets) and GPU Resident Drawer (native) for static geometry, plus frustum + distance culling per chunk.
  • Pools, not allocations: projectiles, emitters, decals, audio voices and damage numbers come from fixed pools. No new in the frame loop.
  • Animation LOD: update rate drops with distance (60 → 30 → 15 Hz), and off-screen characters skip bone evaluation.
  • Shader variant budget: ≤ 64 material variants, prewarmed in a loading phase (no hitch on the first cast).
  • VFX LOD: every effect has Low/High definitions with the same timing and footprint. Optional tails/sparks drop first. The per-tier feel layers (debris, decals, lights, screen effects) are listed in feel-quality.json and VFX.
  • UI: the UI Toolkit HUD updates bound values only when they change; no per-frame allocations.
  • Dynamic resolution and frame pacing: keep frame time stable instead of chasing peaks.

◆How budgets are tested

  • Perf harness scenes (Combat Lab, town crowd, boss fight) run as PlayMode tests with the Unity Performance Testing package (Web build: Playwright drives the page) and write perf.json with p50/p95/p99 frame time, frames over 33 ms, draw calls, triangles, particles, managed heap and GPU memory estimate.
  • Each milestone repeats them on the reference devices (manual run, logged with task-log.mjs test).
  • An asset validator script rejects assets over budget (tris, bones, texture size, missing LODs) before they enter the build.

Source: zoen/docs/tech/PERFORMANCE.md · 1,594 words · edit the Markdown, not this page.