<!-- https://minecraftdocs.dev/systems/rendering/the-frame — this page as markdown. The book's index: https://minecraftdocs.dev/llms.txt; the whole book in one file: https://minecraftdocs.dev/llms-full.txt -->

# The frame

> Verified against **Minecraft 26.3** · Part XI · one frame: from acquiring a surface texture to handing it back, and the wall in the middle that the drawing half is not allowed to look past.

The number in the top-left says 143 fps, and each of those 143 is one call to
`Minecraft.renderFrame` — one method with two halves. The first walks the
live game and copies everything drawable into a `GameRenderState`; the second
draws that state, and reads the live game only where the wall below leaks. Before either half runs, the frame asks
the surface [the window](the-window) was made with for somewhere to put the picture, and the
surprising thing is what happens when that request fails. It does not skip
the frame. It skips the *picture*. The world still renders in full into
`GameRenderer.mainRenderTarget`, the GUI still goes on top, the framerate
limiter still parks; only the blit and the present are guarded on a surface
having been acquired, and both are quietly skipped. A minimized window is the same
story — a client drawing complete frames that nobody will ever see.

All of this is one thread — the same one that ticked the world a moment
earlier. How many ticks ran before the
frame, and what paced it afterwards, is [the client
loop](../client/the-client-loop); this page starts where that page's
*frame* zone opens.

## The cast

| class | what it decides | thread |
|---|---|---|
| `Minecraft` | the zones, the acquire guard, and whether this frame advances game time at all | Render thread |
| `GameRenderer` | the frame's owner of everything drawn — the snapshot target, the main render target, the camera, the lightmaps | Render thread |
| `GameRenderState` | what the drawing half is allowed to read, and therefore where the wall is | written in *extract*, read in *render* |
| `Camera` | where the eye is, how wide the view is, and how wide the *cull* frustum is — which is not the same number | Render thread, but ticked from `GameRenderer.tick` |
| `LevelExtractor` | which of the live world becomes drawable state, at one partial tick **per entity** | Render thread |
| `LevelRenderer` | the world half — its render handed a `CameraRenderState`, never a `Camera` | Render thread |
| `GuiRenderer` | the GUI half, with its own `StagedVertexBuffer` | Render thread |
| `GpuSurface` | whether there is anywhere to put the picture: acquire, blit, present | Render thread |

## The zones a frame is made of

The profiler zones in order are the shortest honest description of what a
frame is, and the second column is the part a name does not tell you: who
pushes it. Eleven of the fifteen are `Minecraft.renderFrame`'s
own. *render* belongs to `GameRenderer.render`, which is why the F3 pie chart
puts the whole of the drawing half under one slice that `Minecraft` never
named.

| zone | pushed by | what happens in it |
|---|---|---|
| *update window* | `Minecraft.renderFrame` | the window's one per-frame call, a surface reconfigure if one is due, then `GpuSurface.acquireNextTexture` |
| *update* | `Minecraft.renderFrame` | the real-time clock, the GPU timer query when something wants it, `Minecraft.pauseIfInactive`, the GUI, the client light engine on a ticking frame, then `Minecraft.pick` |
| *camera* | `GameRenderer.update`, inside *update* | `Camera.update` alone |
| *extract* | `Minecraft.renderFrame` | the wall: the window and the options, then on a frame with a world to draw the lightmap, the camera and the level, then the GUI, copied into `GameRenderState` |
| *gpuAsync* | `Minecraft.renderFrame` | `RenderSystem.executePendingTasks` drains signalled fences, and nothing else; it is closed before the drawing starts |
| *render* | `GameRenderer.render` | the resize, the clear, on a frame with a world to draw the lightmap, then the two zones below |
| *world* | `GameRenderer.render`, inside *render* | only on a frame with a level: *matrices*, *fog*, *level*, *hand*, *screenEffects*, then the outline composite and the post-effect chains |
| *gui* | `GameRenderer.render`, inside *render* | the GUI, drawn under the one-pixel lightmap |
| *swapchainBlit* | `Minecraft.renderFrame` | the blit from `GameRenderer.mainRenderTarget` into the surface texture |
| *tracyCapture* | `Minecraft.renderFrame` | only with Tracy frame capture on: the main target copied out for Tracy |
| *submit* | `Minecraft.renderFrame` | `CommandEncoder.submit`, surface or no surface |
| *present* | `Minecraft.renderFrame` | `GpuSurface.present` |
| *endFrame* | `Minecraft.renderFrame` | Tracy's end of frame if it is capturing, the dynamic uniforms reset, `LevelRenderer.endFrame` |
| *frameLimiter* | `Minecraft.renderFrame` | the parking, spending the limit *extract* snapshotted |
| *fpsUpdate* | `Minecraft.renderFrame` | the counter behind the number in the top-left |

Drawn as calls rather than zones, the same frame shows who calls whom:
`Minecraft.renderFrame` hands `GameRenderer` its update, its extract and its
render, and only the two ends of the frame touch the surface's texture.

```mermaid
sequenceDiagram
    participant MC as Minecraft
    participant GpuS as GpuSurface
    participant GR as GameRenderer
    participant LX as LevelExtractor
    participant LR as LevelRenderer
    participant GuiR as GuiRenderer

    MC->>GpuS: GpuSurface.acquireNextTexture, if the surface is valid — a throw invalidates it
    MC->>MC: update — clock, pause check, GUI, ClientLevel.update on a ticking frame
    MC->>GR: GameRenderer.update — the camera zone, then the frame's post chains listed
    MC->>MC: Minecraft.pick writes Minecraft.hitResult
    MC->>GR: GameRenderer.extract — the window and the options
    opt there is a world to draw
        GR->>GR: the lightmap and the camera
        GR->>LX: LevelExtractor.extract — the level
    end
    GR->>GR: the GUI
    Note over MC,GuiR: the wall — nearly sealed at LevelRenderer.render, leaky one level up
    MC->>MC: gpuAsync — the signalled fences drained
    MC->>GR: GameRenderer.render — resize, clear
    opt there is a world to draw
        GR->>GR: the lightmap, then the world zone
        GR->>LR: LevelRenderer.render with a CameraRenderState
        GR->>GR: the hand and the screen effects
        GR->>LR: LevelRenderer.blitEntityOutline
        GR->>GR: the post chains
    end
    GR->>GuiR: GuiRenderer.render, then GuiRenderer.endFrame
    opt GpuSurface.isAcquired
        MC->>GpuS: GpuSurface.blitFromTexture from the main target
    end
    MC->>MC: submit — the frame's GPU work, surface or none
    opt GpuSurface.isAcquired
        MC->>GpuS: GpuSurface.present
    end
    Note over MC: the zones endFrame, frameLimiter, fpsUpdate
```

*One frame as the calls `Minecraft.renderFrame` makes, with `GameRenderer`'s
work under the three it hands over. The surface's texture is touched only at
the two ends, and only the blit and the present are guarded on having one.*

Read it as **acquire, snapshot, draw, present**: everything but the blit and
the present is paid for whether or not a surface was acquired.

## Acquire, and the frame that carries on without one

The *update window* zone does three things, and the first belongs to
somebody else: `Window.updateFullscreenIfChanged` at the very top of it — the one per-frame
call of [the window](the-window#what-the-window-does-per-frame-which-is-almost-nothing)
— then a reconfigure of the surface if it needs one, then
`GpuSurface.acquireNextTexture`. Vsync is a
[`GpuSurface.PresentMode`](blaze3d#how-a-frame-reaches-the-screen) in that
configuration, which the OpenGL surface turns into a swap interval, so toggling
it in the options calls `Minecraft.invalidateSurfaceConfiguration`, and the
next frame's *update window* reconfigures the surface.

When the acquisition throws, the surface is marked invalid, a reconfigure is
scheduled, and the frame goes on as if nothing had happened but for a line in
the log. Exactly two later statements re-test whether a surface is
held, and both are in `Minecraft.renderFrame` itself: the blit and the
present are each guarded on `GpuSurface.isAcquired`, with nothing on the
other branch.
The tolerance is the caller's, not the surface's — `GpuSurface.blitFromTexture`
and `GpuSurface.present` both throw, in the façade the game holds, if you
reach them without one. Everything
before those two guards — the extract, the world, the GUI — is paid in
full.

There is one guard on the whole method, and it is about re-entry rather than
failure: if the surface is *already* acquired when `Minecraft.renderFrame` is
called, the call is a silent no-op.

### What a minimized client actually stops doing

On the OpenGL backend, a minimized window is the same story after one refused
acquire. `Minecraft.renderFrame` holds any surface reconfigure back until the
window is restored, and `GlSurface.acquireNextTexture` refuses an iconified
window outright, so the first minimized frame fails to acquire, the surface is
marked invalid, and no acquire is tried again until the window comes back. The
three statements that drop out are the acquire and the two guarded
ones — which is nothing next to what the frame still pays for. The saving
comes from somewhere else entirely.
`FramerateLimitTracker.getThrottleReason` tests iconification *first*, ahead
of idleness and the menu, and `FramerateLimitTracker.getFramerateLimit` turns
that answer into a limit of ten — so *frameLimiter*
parks the thread for most of every hundred milliseconds and the client draws
its unseen frames about ten times a second instead of at the player's
setting. On top of that, losing focus for half a second, with no screen up and *pause on lost focus*
on, opens the pause menu through `Minecraft.pauseIfInactive`, and that menu's vote stops a singleplayer world not opened to LAN, so every
frame after that draws a world that has stopped.

## Update and extract: six clocks in one frame

The *update* zone advances the real-time clock, reads
`Minecraft.timerQuery` — a `TimerQuery`, the GPU-side stopwatch behind the F3
utilisation figure. It brackets almost the whole frame, from here through
the blit, runs only while the debug screen's GPU line is shown or a metrics
recording is running, and is only *started* when the last one has been
collected, so not every frame is measured — runs
`Minecraft.pauseIfInactive` and updates the GUI. Then
`GameRenderer.update` calls `Camera.update` — that one call is the *camera*
zone — and `Minecraft.renderFrame` follows it with `Minecraft.pick`, a
private method of `Minecraft` and not the renderer's, which writes
`Minecraft.hitResult` for the crosshair and the block outline to find later.

Behind all of this sits the flag the rest of the page keeps leaning on.
`Minecraft.renderFrame` is passed a boolean saying whether this frame
advances game time, and a frame passed false is a **non-ticking frame**: no
ticks ran before it, `ClientLevel.update` does not run the client's own
light engine, and the whole world half of the drawing is skipped, leaving a
GUI-only picture — the half [GUI and
screens](../client/gui-and-screens) owns. Four call sites always pass false:
the `Minecraft` constructor, once, as it finishes; the two loops that wait for
the integrated server to start and to stop; and
`Minecraft.setScreenAndShow`, which forces a single frame so that a screen
appears during blocking work. And the loop's own call passes false for every
frame after the client has recovered from running out of memory. Every other
frame is a ticking one, and the term means only that.

`Camera` is split three ways across the client, and two of the three are here.
`Camera.tick` — driven from `GameRenderer.tick`, not from the frame — smooths
the eye height and the field-of-view modifier and advances the camera's
`EnvironmentAttributeProbe`. `Camera.update` does the frame's work:
`Camera.alignWithEntity`, `Camera.calculateFov`, `Camera.prepareCullFrustum`,
`Camera.setupPerspective`. `Camera.extractRenderState` then copies the result
across the wall.

### Six partial ticks, and none of them is *the* one

*Extract* opens by snapshotting the framerate limit into
`GameRenderState.framerateLimit` and goes on to copy the window and the options,
and on a frame with a world to draw the lightmap, the camera and the level. It is here that the frame's oddest
number appears: there is no such thing as *the* partial tick of a frame.
There are six, they disagree on purpose, and two of them are not partial
ticks at all.

| who is interpolated | which value | what it ignores |
|---|---|---|
| the world | `DeltaTracker.getGameTimeDeltaPartialTick` | nothing — frozen time is honoured |
| the camera, and the held item as it is posed | `Camera.getCameraEntityPartialTicks` | freezing, unless the *camera entity itself* is frozen, which a player never is |
| the lightmap | a literal one | everything — it is extracted fully advanced |
| screens and overlays | `DeltaTracker.getGameTimeDeltaTicks` | the fraction — this is the whole delta since the last frame, not a position inside a tick |
| the autosave indicator and the menus' panorama | `DeltaTracker.getRealtimeDeltaTicks` | the game clock, and anything past seven ticks, where it clamps to a half |
| each entity, separately | its own frozen-honouring value, asked for by `LevelExtractor` | the world's single answer |

The last row is the one you can see. Under `/tick freeze` a mob pins at the
end of its last tick while players go on interpolating — in the same frame,
from the same extract. The exception is worth knowing, because it is the one
a player rides: `TickRateManager.isEntityFrozen` excludes anything with a
player aboard, so the horse under you keeps moving smoothly while the horse
beside you is a statue.

## The wall, and the one level at which it is real

`LevelRenderer.render` is handed state and reads no live *game* state: no
`ClientLevel`, and nothing from `Minecraft` but the level renderer itself,
which it reaches through `Minecraft.getInstance`. That is the wall, and one level down it holds but for a few reaches by reference: a moving block's
render state carries the level's light engine and is lit from it at prepare, a
sign's carries the block entity's own text ([block-entity
rendering](block-entity-rendering#what-a-block-entitys-snapshot-carries)),
and a section compiled on the spot reads tints and light live through its
region ([section meshing](section-meshing#what-a-mesher-is-allowed-to-read)).
Beyond those it reaches back only into live *renderer* objects, for the main
target and the shader manager.

At the top it leaks, and the leak is sharper than the naming suggests.
`GameRenderer.render` takes whether there is a world to draw from the
snapshot, as `GameRenderState.shouldRenderLevel`, but inside the world half
`GameRenderer.shouldRenderBlockOutline` reads the
**live camera entity during rendering**, and in adventure or spectator mode
goes further: for a player who may not build it reads `Minecraft.hitResult`,
asks the game mode what it is and looks a `BlockState` up in the level, all
mid-draw. It is not the only reach: the boss bar's world fog, the hand's check
for spectator mode and the world-icon screenshot read live state inside
`GameRenderer.render` too. The interesting fact is not that a wall exists but that it is
drawn one level below where *extract then render* implies it is.

### The resize the snapshot has already decided

A resize is handled inside the render half rather than before it:
`GameRenderer.render` opens by comparing `GameRenderState.windowRenderState`
against the main target's size and resizing the renderer inline when they
differ. The snapshot is what the frame believes the window size to be, even
if the window has changed since.

### What the drawing half holds, which is four buffers and a storage per drawer

The buffers are few. `RenderBuffers` holds `RenderBuffers.fixedBufferPack`
and the section-meshing scratch `RenderBuffers.sectionBufferPool` — capped by
processor count and again by a memory budget — which only section meshing
uses, and a single `RenderBuffers.stagedVertexBuffer` released by
`RenderBuffers.endFrame`, which the feature dispatcher writes every prepared
feature into, with the GUI keeping a second staged buffer of its own inside
`GuiRenderer`. Geometry submission goes through `SubmitNodeCollector` into a
`SubmitNodeStorage`, one per drawer
([entity rendering](entity-rendering#submit-describing-a-draw-without-making-one)
owns what a submission is), drawn either inside the frame graph's *main* pass in
[visibility and the frame graph](visibility-and-the-frame-graph#declaring-the-passes-and-why-none-of-them-is-ever-culled)
or by `FeatureRenderDispatcher.renderAllFeatures` — which, despite the name,
is **not** how the level is drawn. It has four call sites: the held item, the
screen effects, the GUI's item atlas and picture-in-picture. The last two are
why the GUI needs submit storage at all. The world's submitted features are
prepared into the frame graph and drawn inside its *main* pass.

### The hand and the screen effects, in a storage the level never sees

The first two of those four call sites share a storage of their own.
`GameRenderer.handAndScreenSubmitNodeStorage` collects both
`GameRenderer.renderItemInHand` — which hands the hand to `FirstPersonHandsAndItemsRenderer`, drawn under
its own projection after a depth clear so a held sword never intersects the
world — and, in the *screenEffects* zone that follows it,
`ScreenEffectRenderer`: the underwater overlay, the fire quad when you are
burning, the sprite of whatever block your head is inside, and the
item-activation flourish a totem plays. Each is submitted, prepared and
drawn inside its own zone, and neither ever reaches the level's frame graph.

## Blit, submit and present

The picture leaves the frame in three zones. *swapchainBlit* does the blit
from `GameRenderer.mainRenderTarget` to the acquired surface texture;
*submit* closes the frame's GPU work with `CommandEncoder.submit`, surface or
no surface; and *present* hands the texture back with `GpuSurface.present`,
which on the OpenGL backend is a single call to SDL's *SDL_GL_SwapWindow*.
With Tracy frame capture on, a *tracyCapture* zone between the blit and the
submit copies the main target out for Tracy.

The last three zones are bookkeeping. *endFrame* ends Tracy's frame if Tracy
is capturing, resets the dynamic uniforms and calls `LevelRenderer.endFrame`.
*frameLimiter* spends the limit that *extract* snapshotted into `GameRenderState.framerateLimit`, parking only
below a threshold, so the top slider position parks only when
`FramerateLimitTracker` has capped or replaced the limit. What that
limit is — and the four cases in which `FramerateLimitTracker` has already
capped or replaced the player's option — belongs to [the client
loop](../client/the-client-loop#the-frame-cap-is-usually-the-option-and-sometimes-is-not);
this page only spends it. Then *fpsUpdate*, and the frame is over.

## Questions players ask

**Why does lowering a spyglass never reveal a hole in the world?** Because
the cull frustum is deliberately wider than the camera.
`Camera.createProjectionMatrixForCulling` builds its matrix from the larger
of the current and the *configured* field of view; `Camera.prepareCullFrustum`
turns that into `Camera.cullFrustum`, and `Camera.extractRenderState` copies
it across the wall into `CameraRenderState.cullFrustum`. Sprinting and flying raise the live FOV
above the option, so for them the maximum does nothing. It bites when
something **narrows** the view — a spyglass, a drawn bow, the dying-camera
effect — where culling against the configured FOV keeps the geometry a
narrowed view would have thrown away.

**Why is the HUD not shaded by the light the player is standing in?**
Because for the whole of the *gui* zone `GameRenderer.lightmap` hands out a
one-pixel white texture instead of the real one. `GameRenderer.useUiLightmap`
is set on either side of exactly that zone and nowhere else; which two
textures it is switching between belongs to [lightmap, fog and
sky](lightmap-fog-and-sky#how-bright-one-draw-per-tick-and-no-partial-ticks-at-all).

**Why does the world go strange when spectating a creeper?** Because the
post-effect chain is chosen by what you are spectating rather than by an
option, at the end of the world block and before any GUI. Which chain,
through which door, and what each does to the picture belong to
[post-processing](post-processing#the-five-chains).

**Where does the main menu's panorama come from?** From six images loaded as
one cube-map texture and drawn as a turning cube, and the game can take the six itself on the same two halves,
though only with a debug flag set: `Minecraft.grabPanoramixScreenshot` runs `GameRenderer.update`,
`GameRenderer.extract` and `GameRenderer.renderLevel` six times at
4096×4096, with a fixed delta of one and the camera in panoramic mode —
which is what `CameraRenderState.isPanoramicMode` exists for. A second,
silent screenshot path inside the world half writes the world icon,
singleplayer only, and only once enough sections have been rendered.

> **For a 1.21-era reader.** The frame is now **extract then render**:
> `GameRenderer.extract` copies the live game into a `GameRenderState`, and
> `LevelRenderer.render` is handed a `CameraRenderState` rather than a `Camera`.
> Names to stop hunting for: *Minecraft.getMainRenderTarget* (now
> `GameRenderer.mainRenderTarget`), *Camera.setup* (now `Camera.update` plus
> `Camera.extractRenderState`), *GameRenderer.getProjectionMatrix*,
> *Window.updateDisplay*, *LightTexture* (now `Lightmap`), *ItemInHandRenderer*
> (now `FirstPersonHandsAndItemsRenderer`) and *MultiBufferSource*.

## Where to look

`Minecraft.renderFrame` — the frame is one method, and its profiler zones are
its table of contents. Then `GameRenderer.extract` and `GameRenderer.render`
for the wall between live objects and drawing, `GameRenderer.tick` for the
per-tick half nobody expects a renderer to have, `Camera.update` for how the
view is decided, and `GpuSurface.present` for where a frame ends. The GPU
abstraction underneath all of it has its own page, [blaze3d](blaze3d).

---

*Rules: names, never code · how the system works, not how the code reads ·
newest version only · every backticked name passes `tools/verify_names.py`.*
