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 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; 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.
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
— then a reconfigure of the surface if it needs one, then
GpuSurface.acquireNextTexture. Vsync is a
GpuSurface.PresentMode 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 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),
and a section compiled on the spot reads tints and light live through its
region (section meshing).
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
owns what a submission is), drawn either inside the frame graph’s main pass in
visibility and the frame graph
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;
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.
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.
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.extractcopies the live game into aGameRenderState, andLevelRenderer.renderis handed aCameraRenderStaterather than aCamera. Names to stop hunting for: Minecraft.getMainRenderTarget (nowGameRenderer.mainRenderTarget), Camera.setup (nowCamera.updateplusCamera.extractRenderState), GameRenderer.getProjectionMatrix, Window.updateDisplay, LightTexture (nowLightmap), ItemInHandRenderer (nowFirstPersonHandsAndItemsRenderer) 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.
Rules: names, never code · how the system works, not how the code reads ·
newest version only · every backticked name passes tools/verify_names.py.