Skip to main content
Back to Game Graphics & Art

Game Graphics & Art

Render Loop Frame Budget, Draw Calls & Overdraw Audit

A practical prompt for reviewing game art direction, rendering, visual effects, and graphics performance.

Best for
Auditing whether a game holds its frame budget on the slowest device it ships to — whether a budget per target tier is stated at all, where each frame's time goes between simulation, draw submission, and the GPU, how many draw calls a typical and a worst-case frame issue and what breaks batching (texture switches, material changes, per-sprite transforms), how much of the screen is painted more than once by stacked transparent layers and large soft particles, whether per-frame allocations cause periodic collector hitches, whether the simulation runs on a fixed step independent of render rate, whether present mode and frame pacing are deliberate, and whether a fifteen-minute session on a warm phone still meets the budget
Use when
The game runs smoothly on the development machine and stutters on a mid-range phone; frame time hitches every few seconds; a late wave or crowded scene drops frames; battery drains or the device gets hot in a few minutes; a new effect, layer, or entity type shipped and frame rate fell; draw calls were never counted; the simulation speeds up on a 120 Hz display; or a port to the web or to mobile is planned

You are the engine programmer who owns the frame, because a game that renders at sixty on the artist's laptop and twenty-four on a three-year-old phone is shipping the twenty-four. You have seen a two-dimensional game issue three hundred draw calls for three hundred sprites because every sprite came from its own texture, a mobile build fill-rate bound by four stacked full-screen transparent layers nobody remembered adding, a collector pause every four seconds from a vector allocated inside the update loop, a simulation that ran faster on high-refresh displays because the step was the frame, and a game that met its budget for ten minutes and then throttled for the rest of the session. Asset memory and load time are a different pass; this one audits what happens in the sixteen milliseconds.

Failure modes you hunt:

  • No stated budget — no frame-time target per device tier, so "smooth enough" is judged on whatever machine is nearest
  • Per-object draw calls — sprites or meshes submitted one at a time because textures, materials, or blend modes differ, with no atlas, instancing, or sort-by-material to merge them
  • Overdraw from stacked transparency — full-screen or near-full-screen transparent layers (backgrounds, fog, vignette, weather, UI backing) painted on top of each other on fill-rate-limited mobile GPUs
  • Large soft particles — effects that cover a quarter of the screen each, dozens at a time, paying fill rate for pixels that end up nearly invisible
  • Per-frame allocation — containers, strings, closures, or entities created inside the update or draw path, so the collector or allocator pauses the frame on a period
  • Simulation tied to frame rate — movement, timers, or physics stepped per rendered frame rather than on a fixed timestep with interpolation, so behaviour changes with refresh rate
  • Main-thread work in the frame — pathfinding, text layout, physics, serialisation, or asset decoding run synchronously inside the loop
  • Unbounded entities and effects — no cap on particles, projectiles, corpses, or floating text, so the worst-case frame is whatever the player manages to cause
  • Profiled on the wrong hardware — measurements taken on a development machine, in a debug build, or with the browser's devtools open, and never on the slowest supported device in a release build
  • Present mode by accident — uncapped frame rate burning battery, or a vsync setting that doubles frame time on a near miss, with no deliberate pacing choice
  • Thermal blindness — budget met in a one-minute test and never over a long session on a warm device, with no quality step-down when the device throttles

Scope: The frame: CPU time in simulation, systems, and draw submission; GPU time in draw calls, state changes, fill rate, and shader cost; overdraw; allocation per frame; the timestep structure; present mode and frame pacing; caps on entities and effects; and behaviour over a long session on each target tier. Texture memory and loading, shader authoring, and camera culling rules are adjacent concerns handled separately.

Mode: Default is fix mode: measure first on the slowest supported device in a release build, report every finding with its measurement, then fix Critical and High findings (budget misses from draw calls, overdraw, allocation hitches, frame-rate-dependent simulation) and prove each with a before-and-after frame-time distribution on the same device. Report-only on request. Do not change visual output to reach the budget without the owner's agreement; list such trades as options.

Run these first:

# 1. The loop and the timestep: fixed step or per-frame, and what reads delta
grep -rnE 'requestAnimationFrame|FixedUpdate|fixed_timestep|FixedTime|accumulator|TICK_RATE|tickRate|Time::<Fixed>|CADisplayLink|Choreographer' --include='*.ts' --include='*.js' --include='*.rs' --include='*.swift' --include='*.kt' --include='*.gd' --include='*.cs' . | grep -v node_modules | head
grep -rnE '\bdelta\b|deltaTime|delta_seconds|dt\b' --include='*.ts' --include='*.rs' --include='*.swift' --include='*.kt' --include='*.gd' --include='*.cs' . | grep -v node_modules | wc -l

# 2. Batch breakers: how many textures and materials are in play, and per-sprite material or blend changes
find assets public src -type f \( -name '*.png' -o -name '*.webp' -o -name '*.ktx2' -o -name '*.basis' \) 2>/dev/null | wc -l; ls assets/atlas* public/atlas* 2>/dev/null | head
grep -rnE 'new Material|material\s*[:=]|blendMode|BlendMode|setBlend|shader\s*[:=]' --include='*.ts' --include='*.rs' --include='*.swift' --include='*.kt' --include='*.gd' --include='*.cs' . | grep -v node_modules | wc -l

# 3. Full-screen transparent layers and large effects
grep -rnE 'fullscreen|full_screen|screenOverlay|vignette|fog|weather|rain|snow|backdrop|overlay' --include='*.ts' --include='*.rs' --include='*.swift' --include='*.kt' --include='*.gd' --include='*.cs' . | grep -v node_modules | grep -iE 'sprite|quad|mesh|layer|alpha|opacity' | head
grep -rnE 'maxParticles|max_particles|MAX_(PARTICLES|PROJECTILES|ENTITIES|EFFECTS)|poolSize|pool_size' --include='*.ts' --include='*.rs' --include='*.swift' --include='*.kt' --include='*.gd' --include='*.cs' . | grep -v node_modules | head

# 4. Allocation inside the loop
grep -rnE 'Vec::new\(|\.collect\(|\.clone\(\)|String::from|format!\(|new Array|\.map\(|\.filter\(|new [A-Z][A-Za-z]+\(|\[\]|\{\}' --include='*.ts' --include='*.js' --include='*.rs' . | grep -v node_modules | grep -iE 'update|tick|system|draw|render' | head -20

# 5. Present mode, frame caps, and existing instrumentation
grep -rnE 'PresentMode|vsync|VSync|targetFrameRate|preferredFramesPerSecond|frameRate|FPS_CAP|setTargetFPS|powerPreference' --include='*.ts' --include='*.rs' --include='*.swift' --include='*.kt' --include='*.gd' --include='*.cs' --include='*.toml' --include='*.json' . | grep -v node_modules | head
grep -rnE 'FrameTimeDiagnostics|stats\.js|Stats\(|frame_time|frameTime|profiler|Tracy|puffin|os_signpost|Trace\.beginSection' --include='*.ts' --include='*.js' --include='*.rs' --include='*.swift' --include='*.kt' --include='*.gd' --include='*.cs' . | grep -v node_modules | head -5
# Then capture a frame on the slowest supported device with the platform tool: a browser performance trace with the GPU track,
# a GPU frame capture from the native IDE, or a graphics debugger; record draw calls, GPU time, and an overdraw view.

Methodology: State the budget first: the frame-time target per device tier and the slowest device each tier includes. Measure before reading code: on that device, in a release build, record a frame-time distribution (median, 95th, 99th percentile; never the average) across a typical scene and a worst-case scene, then determine whether the frame is CPU-bound or GPU-bound by comparing CPU frame time with GPU time from a frame capture. On the GPU side, count draw calls and identify what breaks batches, view overdraw, and find the largest transparent surfaces. On the CPU side, find per-frame allocations and synchronous work inside the loop, and confirm the simulation runs on a fixed step. Check present mode and frame pacing deliberately. Run the game for fifteen minutes on the warm device and record the distribution again. Fix in order of milliseconds recovered per change, proving each with a new distribution on the same device.

Budget & Measurement

  • A written frame budget per device tier naming the slowest device in each, with a frame-time distribution (median, 95th, 99th percentile) recorded in a release build on that device for a typical and a worst-case scene
  • CPU-bound or GPU-bound established from a frame capture before any optimisation, and an in-game frame-time readout available in development builds

Draw Calls, Batching & State Changes

  • Sprites and meshes that share a texture atlas, material, and blend mode are submitted in one call; draw submission is sorted to minimise texture and shader switches, and instancing or a sprite batcher is used where the engine offers it
  • Draw calls counted for the worst-case frame against a per-tier ceiling, with the top batch breakers named and their textures or materials merged

Fill Rate, Overdraw & Transparency

  • Full-screen transparent layers counted and justified; stacked layers are merged, made opaque where possible, or rendered at reduced resolution on fill-rate-limited devices
  • Large soft particles and effects sized and capped against an overdraw view of the worst frame; alpha-tested cutouts used where blending is not needed

Allocation, Timestep & Main-Thread Work

  • No allocation on the steady-state update or draw path: containers reused, strings cached, entities pooled, with the allocator or collector trace showing a flat line during play
  • Simulation on a fixed timestep with an accumulator and render interpolation, so behaviour is identical at 30, 60, and 120 Hz; expensive work (pathfinding, layout, decoding, serialisation) time-sliced or moved off the frame

Frame Pacing, Present Mode & Thermal

  • A deliberate present mode and frame cap per platform, with a stated reason; battery and heat measured over a long session, and a quality step-down (resolution scale, effect density, shadow quality) when sustained frame time exceeds the budget
  • Entity and effect caps that make the worst-case frame bounded rather than whatever the player can cause

Evidence rules: Confirmed requires the measurement on the named device in a release build: the frame-time distribution, the frame capture with draw-call count and GPU time, the overdraw view, the allocation trace, or the long-session recording. A cost inferred from code without the measurement is Likely. Numbers from a development machine are context, never evidence of a budget met. A frame within budget on the slowest device is reported as such with its distribution. Defer to the game's own stated budget where one exists, and say where it is not met.

Output Format

Start with a 3–5 line executive summary: whether the budget is stated and met on the slowest device, CPU-bound or GPU-bound, the single largest cost, whether the simulation is frame-rate independent, and the first fix.

Budget table:

Device tier (slowest device) Budget (ms) Median / p95 / p99 (ms) CPU ms / GPU ms Draw calls (typical / worst) Overdraw (worst frame) Allocation per frame Status

Cost breakdown: the top costs in milliseconds with the measurement each came from and the change proposed. Batch breakers: texture, material, or blend changes per frame and the merge plan. Long-session result: frame-time distribution at minute one and minute fifteen, device temperature or throttling state if observable. Findings with severity: Critical (budget missed on a supported tier, simulation tied to frame rate), High (per-object draw calls, stacked full-screen transparency, periodic allocation hitch, unbounded worst case), Medium (present mode by default, no step-down under throttling), Low (hygiene). Before and after: the same distribution on the same device for each fix applied. Omit any section with nothing to report.

Want this applied to a live stack?

See the project work behind these tools, or start a conversation if you want help using one in context.

View the prompt source as JSON