Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

classwhat it decidesthread
Minecraftthe zones, the acquire guard, and whether this frame advances game time at allRender thread
GameRendererthe frame’s owner of everything drawn — the snapshot target, the main render target, the camera, the lightmapsRender thread
GameRenderStatewhat the drawing half is allowed to read, and therefore where the wall iswritten in extract, read in render
Camerawhere the eye is, how wide the view is, and how wide the cull frustum is — which is not the same numberRender thread, but ticked from GameRenderer.tick
LevelExtractorwhich of the live world becomes drawable state, at one partial tick per entityRender thread
LevelRendererthe world half — its render handed a CameraRenderState, never a CameraRender thread
GuiRendererthe GUI half, with its own StagedVertexBufferRender thread
GpuSurfacewhether there is anywhere to put the picture: acquire, blit, presentRender 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.

zonepushed bywhat happens in it
update windowMinecraft.renderFramethe window’s one per-frame call, a surface reconfigure if one is due, then GpuSurface.acquireNextTexture
updateMinecraft.renderFramethe 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
cameraGameRenderer.update, inside updateCamera.update alone
extractMinecraft.renderFramethe 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
gpuAsyncMinecraft.renderFrameRenderSystem.executePendingTasks drains signalled fences, and nothing else; it is closed before the drawing starts
renderGameRenderer.renderthe resize, the clear, on a frame with a world to draw the lightmap, then the two zones below
worldGameRenderer.render, inside renderonly on a frame with a level: matrices, fog, level, hand, screenEffects, then the outline composite and the post-effect chains
guiGameRenderer.render, inside renderthe GUI, drawn under the one-pixel lightmap
swapchainBlitMinecraft.renderFramethe blit from GameRenderer.mainRenderTarget into the surface texture
tracyCaptureMinecraft.renderFrameonly with Tracy frame capture on: the main target copied out for Tracy
submitMinecraft.renderFrameCommandEncoder.submit, surface or no surface
presentMinecraft.renderFrameGpuSurface.present
endFrameMinecraft.renderFrameTracy’s end of frame if it is capturing, the dynamic uniforms reset, LevelRenderer.endFrame
frameLimiterMinecraft.renderFramethe parking, spending the limit extract snapshotted
fpsUpdateMinecraft.renderFramethe 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 interpolatedwhich valuewhat it ignores
the worldDeltaTracker.getGameTimeDeltaPartialTicknothing — frozen time is honoured
the camera, and the held item as it is posedCamera.getCameraEntityPartialTicksfreezing, unless the camera entity itself is frozen, which a player never is
the lightmapa literal oneeverything — it is extracted fully advanced
screens and overlaysDeltaTracker.getGameTimeDeltaTicksthe fraction — this is the whole delta since the last frame, not a position inside a tick
the autosave indicator and the menus’ panoramaDeltaTracker.getRealtimeDeltaTicksthe game clock, and anything past seven ticks, where it clamps to a half
each entity, separatelyits own frozen-honouring value, asked for by LevelExtractorthe 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.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.


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