<!-- https://minecraftdocs.dev/systems/anatomy/anatomy — 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 -->

# Anatomy

> Verified against **Minecraft 26.3** · Part I · Clicking Singleplayer, picking a world, and standing in it a few seconds later.

A player clicks Singleplayer, picks a world from the list and waits. One
thread has been running since *main*; by the time the world appears there
are two that matter, and the second was created by the first, mid-frame,
while the first went on drawing. They are two programs sharing a JVM. The
server is the world — every chunk, entity and block, and the only thing
allowed to change them; the client is a window, a frame loop and a copy of
the world it is told about. Between them runs a Netty channel that never
touches a socket, and over it the client walks the same handshake, login,
configuration and play state machine it would walk against a server on the
other side of the planet. The packets are real. What leaks between the two
halves is not world state but a setting: pause is *decided* on the client,
by `Minecraft.isPaused`, and *enforced* on the server, by
`IntegratedServer.tickServer` running `IntegratedServer.tickPaused` instead
of the world. The decision has a third term that nobody sees in the menu:
the client only sets the flag while `IntegratedServer.isPublished` is false
— which is why a world published to LAN never pauses, however deep in the
options screen you are.

## The cast

| class | what it decides | thread |
|---|---|---|
| `Minecraft` | the client: the `Window`, the resource system, the renderers, input and `Options` — and, in three fields, whether we are in a world at all | Render |
| `MinecraftServer` | the world and the loop that advances it. Abstract, with three concrete subclasses: `IntegratedServer`, `DedicatedServer`, and `GameTestServer`, the headless harness the gametest entry point launches | Server |
| `IntegratedServer` | singleplayer's server: the pause, LAN publishing, the player cap, the relaxed limits | Server |
| `BlockableEventLoop` | the queue-and-thread pairing both loops are — `Minecraft` and `MinecraftServer` each extend `ReentrantBlockableEventLoop` | one per queue, and a thread may own more than one |
| `Connection` | one channel, and which `PacketListener` is currently on it | Netty |
| `ServerConnectionListener` | which channels the server listens on, including the in-memory one singleplayer uses | the binding thread (Render in singleplayer), into Netty |
| `PacketProcessor` | which decoded packets are waiting to be handled on the thread that owns their state | filled from Netty, drained by the owner |
| `Util` | the pools everything else is serialised onto: `Util.backgroundExecutor`, `Util.ioPool`, `Util.nonCriticalIoPool` | — |

Three of `Minecraft`'s nullable fields between them mean "we are in a
world": `Minecraft.level` (a `ClientLevel`), `Minecraft.player` (a
`LocalPlayer`) and `Minecraft.gameMode` (a `MultiPlayerGameMode`). A fourth,
`Minecraft.singleplayerServer`, holds the `IntegratedServer` when one is
running, and is the client's answer to "am I the host".

## From *main* to a world

```mermaid
sequenceDiagram
    box transparent the client
    participant Main as Main
    participant MC as Minecraft
    participant Conn as Connection
    end
    box transparent the server
    participant IS as IntegratedServer
    participant SCL as ServerConnection<br/>Listener
    end

    Main->>Main: tryDetectVersion, loadLibraries, bootStrap, validate
    Note over Main: from here this thread is the Render thread
    Main->>MC: the constructor: a backend, a Window, every reload listener
    MC->>MC: the first ReloadInstance, the LoadingOverlay on screen
    Main->>MC: run: pollEvents, then runTick, until running goes false
    Note over Main,Conn: one loop so far. The next line makes the second.
    MC->>IS: spin builds it on this thread, then starts the Server thread
    par on the Server thread
        IS->>IS: runServer: initServer loads the level
    and meanwhile, on the Render thread
        MC->>MC: doWorldLoad: draw a frame, drain the queue, until isReady
    end
    MC->>SCL: startMemoryChannel: a Netty local address, no socket
    MC->>Conn: connectToLocalServer: the client's end of that channel
    Conn->>Conn: handshake, login, configuration, play — the walk any client makes
```

*The one-time start-up, and the line where one loop becomes two: in the split
box the Render thread goes on drawing while `IntegratedServer.initServer` loads
the level. Everything below it is the client dialling a server that happens to
be in the same JVM.*

That is the book's first sequence diagram, and its lanes are abbreviated the
way every later one is: two or more letters of a class name, one meaning
throughout. The key is [diagram lanes](../../reference/lanes).

**Five *main* methods, and this trace starts in one of them.**
`client/main/Main` is the client's, `server/Main` the dedicated server's,
`data/Main` the data generator's, `client/data/Main` the generated client
assets' — models, atlases, equipment assets, waypoint styles — and
`gametest/Main` the one that launches `GameTestServer`. (The tree holds a
sixth *main*, `SnbtDatafixer`, which converts files and starts nothing.)
Each parses its own command line — the client's into a `GameConfig` the
`Minecraft` constructor is built from — and then reads its own settings
file: *options.txt* through `Options` on the client and *server.properties*
through `DedicatedServerProperties` on the dedicated server. The two generator entry
points are build-time programs, and
[what this book skips](what-this-book-skips#the-data-generators-and-why-data-driven-is-both-true-and-misleading)
says how far that is true.

**Bootstrap before anything exists.** `SharedConstants.tryDetectVersion`
reads *version.json* as the first statement of every one of those *main*
methods, before the option parser exists; `NativeLibrariesBootstrap.loadLibraries` unpacks
the natives, `CrashReport.preload` warms the reporter, and
`Bootstrap.bootStrap` builds and freezes the static registries — blocks,
items, entity types, the things that cannot be data-driven because the data
loader itself needs them
([identifiers and registries](../foundations/identifiers-and-registries#before-the-game-exists)
is what the freeze proves). `ClientBootstrap` does the client-only equivalents
between that and `Bootstrap.validate`, which checks the result.
`DataFixers.optimize` is kicked off concurrently before the registries are
built and joined much later.

**The GPU backend is chosen in the constructor.**
`RenderSystem.initBackendSystem` runs first and returns SDL's clock, which
`Minecraft` installs through `Util.setTimeSource` — so `Util`'s clock, on the
client and on a singleplayer server beside it, comes from the windowing
library. Then a `GpuBackend` is chosen by trying candidates in an order
`Options` sets, unless a launch argument names one, until one of them
loads its library and makes a `GpuDevice` — there are two, `GlBackend` and
`VulkanBackend`
([the window](../rendering/the-window#trying-backends-until-one-of-them-makes-a-device))
— and only then is the one `Window` made, through the backend that
succeeded. From then on the renderer only ever sees the `GpuDevice`
abstraction in `com/mojang/renderpearl`.

**Construction registers, it does not load.** The constructor creates each
manager and registers it on the `ReloadableResourceManager`; the loading is
one `ReloadInstance` whose *prepare* phases run on `Util.backgroundExecutor`
and whose *apply* phases run on the Render thread, with the `LoadingOverlay`
on screen. Pressing F3+T re-runs exactly that path — see
[the resource system](../foundations/resource-system#f3t-end-to-end).

**Opening a world spins a server.** `Minecraft.doWorldLoad` calls
`MinecraftServer.spin`, which constructs the `IntegratedServer` *on the
caller's thread* before starting the new one
([starting a server](../server/starting-a-server#minecraftserverspin-and-the-last-thing-main-does)
has that order in full); the new thread's body is `MinecraftServer.runServer`,
which calls `IntegratedServer.initServer` and enters the loop. Meanwhile
`Minecraft.doWorldLoad` keeps the Render thread drawing frames and draining its
own queue until `MinecraftServer.isReady`, waiting out each sixtieth of a
second in `BlockableEventLoop.managedBlock`: a thread that blocks on this half
of the game keeps running that half's queue while it waits, which is the
reason the wait cannot deadlock.

**The client connects like any other client.**
`ServerConnectionListener.startMemoryChannel` binds a Netty local address and
`Connection.connectToLocalServer` connects to it; the client then walks
handshake, login, configuration and play as it would against a remote server,
with `ClientHandshakePacketListenerImpl` as its login listener. The server
gives the local channel a handshake listener of its own,
`MemoryServerHandshakePacketListenerImpl`, and skips the encryption request
and compression it would send a remote client. Almost nothing in the play path
knows it is singleplayer: the exceptions are the handful of places that ask
`Connection.isMemoryConnection` or
`ServerCommonPacketListenerImpl.isSingleplayerOwner` directly, among them
`ClientPacketListener.handleUpdateTags`, which skips
applying the tags it was sent because the server's registries are already
the client's. [The
connection](../networking/the-connection#singleplayer-runs-the-same-pipeline)
has what the local channel does and does not install, and [protocol
phases](../networking/protocol-phases#the-five-phases) is the walk in full.

## Two loops, and a wire between them

The client's loop is a frame loop with ticks inside it; the server's is a
tick loop with no frames at all. They are not the same shape, and no page
later in this book is readable until that difference is fixed in mind.

```mermaid
flowchart TD
    subgraph Client["the Render thread"]
        CR["Minecraft.run: RenderSystem.pollEvents,<br/>then Minecraft.runTick, once per frame"]:::client
        subgraph Frame["Minecraft.runTick"]
            CD["the DeltaTracker counts the whole ticks owed"]:::client --> CP["PacketProcessor.processQueuedPackets"]:::client
            CP --> CQ["BlockableEventLoop.runAllTasks"]:::client
            CQ --> CT["Minecraft.tick, 0 to 10 times"]:::client
            CT --> CF["Minecraft.renderFrame, on the partial tick"]:::client
        end
        CR --> CD
        CF --> CR
    end
    subgraph Server["the Server thread"]
        SR["MinecraftServer.runServer: the deadline is set first"]:::server
        subgraph STick["one server tick"]
            SP["PacketProcessor.processQueuedPackets"]:::server --> SS["MinecraftServer.tickServer: every ServerLevel, then the connections"]:::server
        end
        SW["MinecraftServer.waitUntilNextTick:<br/>run queued tasks, then park"]:::server
        SR --> SP
        SS --> SW
        SW --> SR
    end
```

*The two loops side by side: the Render thread's ring is a frame with a tick
inside it, and the Server thread's ring is the tick with no frame anywhere in
it. The inner box in each is the call that holds the tick — `Minecraft.runTick`
on one side, `MinecraftServer.processPacketsAndTick` on the other.*


**The frame loop.** `Minecraft.run` polls SDL events and calls
`Minecraft.runTick` once per **frame**, as fast as vsync or the frame-rate
limit allow. Inside each frame a `DeltaTracker.Timer` running at twenty ticks
a second says how many whole game ticks have elapsed since the last frame —
usually zero or one, at most ten are run — and `Minecraft.tick` is called that
many times, with `BlockableEventLoop.runAllTasks` draining the thread's own
queue just before them. The fractional remainder is the partial tick the
renderers read, to interpolate with, from the `DeltaTracker` that
`Minecraft.renderFrame` hands them. So the client
*has* a 20 Hz tick, but it is a sub-step of
the frame loop rather than a loop of its own; [the client
loop](../client/the-client-loop#the-ten-and-the-arithmetic-behind-it) is the
arithmetic in detail.

**The tick loop.** `MinecraftServer.runServer` re-reads this tick's length
every iteration from `TickRateManager.nanosecondsPerTick` — 50 ms by default,
whatever `/tick rate` says otherwise, and zero under `/tick sprint` — and calls
`MinecraftServer.processPacketsAndTick`, which drains the `PacketProcessor`
and then runs `MinecraftServer.tickServer`. Afterwards
`MinecraftServer.waitUntilNextTick` spends the slack running queued tasks and
then parks until the next tick is due. There is no frame and no partial tick
here at all. The "Can't keep up!" warning is `MinecraftServer.runServer`'s
own, decided
before either call from how far behind the deadline already is; [the server
tick](../server/server-tick#what-minecraftservertickchildren-runs-and-in-what-order)
owns what is inside them — the tick budget,
the deferrable work and the flush bracket around outbound packets.

**Both are event loops first and game loops second.** `Minecraft` and
`MinecraftServer` both extend `ReentrantBlockableEventLoop` — the same base
class, not an analogy — so each is an `Executor` whose queue drains on its own
thread, and another thread that wants to touch that half's state submits a task, or leaves its work in one of the queues beside the loop's own that the owning thread drains — a packet in the `PacketProcessor` (below), a dedicated server's console line in a list `DedicatedServer.tickConnection` drains, a level's chunk work in its `ServerChunkCache.MainThreadExecutor` (below). `BlockableEventLoop.managedBlock` is the blocking form, and
the reason the owning thread can wait for a future without deadlocking: it
keeps draining its own queue while it waits.

**A packet is decoded on one thread and handled on another.** A packet
arrives on a Netty IO thread and `Connection.channelRead0` hands it to the
current `PacketListener`; a handler that touches game state calls
`PacketUtils.ensureRunningOnSameThread`, which, when it is off-thread, queues
the packet on the owning side's `PacketProcessor` instead of running it —
[the connection](../networking/the-connection#one-packet-there-and-one-back)
is that crossing in both directions. What matters here is *when* the queue is
drained, because the two loops do not agree: first thing in
`MinecraftServer.processPacketsAndTick`, but on the client early in
`Minecraft.runTick`, after the delta tracker advances and before the ticks —
so a client at 200 frames a second takes the server's packets ten times more
often than it ticks.

## Four threads worth memorising

| thread | made by | runs | notes |
|---|---|---|---|
| **Render thread** | the JVM main thread, renamed in `client/main/Main` | `Minecraft.run` | Also the client's game thread: `Minecraft.gameThread` is this thread. Priority 10 on machines with more than four processors. |
| **Server thread** | `MinecraftServer.spin` | `MinecraftServer.runServer` | One per server, so singleplayer has exactly one. Priority 8, on the same more-than-four-processors condition. |
| **Netty IO** | `EventLoopGroupHolder` | the `Connection` pipeline | Named *Netty NIO IO #n* — Epoll or Kqueue when native transport is on, *Netty Local IO #n* for the in-process singleplayer channel. Decode, decrypt, decompress — and, unlike the play phase, the handshake and login *handlers*, which never call `PacketUtils.ensureRunningOnSameThread`; the first handler that hops is in configuration. The login state machine is still advanced from the Server thread, because `ServerLoginPacketListenerImpl` is a `TickablePacketListener` and `MinecraftServer.tickConnection` ticks it. |
| **Worker-Main-n** | `Util.backgroundExecutor` | a `ForkJoinPool` sized to the JDK's available-processor count minus one | `Util.maxAllowedExecutorThreads` clamps it, and `Util.getMaxThreads` reads a *max.bg.threads* system property that overrides the ceiling. The shared CPU pool: chunk generation and lighting (`ChunkMap` through `ChunkTaskDispatcher`), section meshing (`SectionRenderDispatcher`), resource-reload *prepare* phases, chunk serialisation. |

That is the set worth memorising, not the set that exists. The IO workers,
the sound engine's event loop, the dedicated server's watchdog, console,
RCON, query and management threads, the timer hack thread and the
situational ones — authentication, chat filtering, server pinging, telemetry,
world upgrades, the shutdown hooks — are all in
[Threads](../../reference/threads), with who makes each and what it is
allowed to touch.

There is no fifth thread hiding on the client. The Render thread *is* the
game thread: `Minecraft.gameThread` and the thread `RenderSystem` guards with
`RenderSystem.assertOnRenderThread` are the same one, and there is no
*initGameThread* and no *isOnGameThread* anywhere in the tree — only
`RenderSystem.isOnRenderThread`. A slow client tick costs frames directly.
What that thread does with a world is animate and predict one (`Minecraft.tick`
calls `ClientLevel.tickEntities`, and block entities tick too), but nothing it
concludes is authoritative, and the server's packets overwrite whatever the
prediction got wrong — which is a claim about the *world*, and the client's
own player is the exception the book spends Part VIII and
[prediction and acknowledgement](../client/prediction-and-acks#two-state-machines-running-against-each-other)
on.

Everything else that matters is *serialised onto* a pool rather than given a
thread. `ConsecutiveExecutor` and `PriorityConsecutiveExecutor`, in
`util/thread`, are the mechanism: a queue that promises to run its tasks one at a time on a pool
that otherwise runs many, which is how "worldgen" and "light" stay ordered on
the worker pool, and how the `IOWorker` stays ordered on `Util.ioPool` —
which is its own pool of *IO-Worker-n* threads, not one of the four. And `ServerChunkCache.MainThreadExecutor` is a further event
loop layered on the server thread, which is why a tick that waits on a chunk
does not deadlock the chunk that needs the tick.

## What singleplayer shares by direct call

Every block, entity and inventory change a player makes crosses as a packet,
even in one process. But the two halves share a JVM, and a handful of things
do cross by direct call — settings, debug state and the options screen's own
updates — and a setting can reach the world before the call returns. The server reads `Minecraft.isPaused` every tick and the client's
render and simulation distances every tick it runs the world;
`WorldOptionsScreen.applyChanges` calls `IntegratedServer.publishServer` and
its siblings straight from the Render thread, and among them
`IntegratedServer.setPersonalGameType` sets the host's own game mode through
`ServerPlayer.setGameMode` on the Render thread, not the Server thread; and
`IntegratedServer.latestTicksGizmos` is a volatile list the server thread
writes and the client reads. Treat "everything crosses as a packet" as a rule
about play, not about the process.

Singleplayer differs in more than pausing, too. Beyond the pause and the
distances following `Options`, `IntegratedServer` caps the player list at
eight, owns LAN publishing and the `LanServerPinger`, drops the chat and
command spam thresholds to zero where a dedicated server defaults to ten,
takes native transport from the client's own option rather than a server
property, and answers the operator-permission questions differently.

## A crash in singleplayer surfaces on the wrong half

Neither loop throws. Both catch everything and wrap it in a `CrashReport`,
and a background thread that dies parks its report for a loop to pick up
([how a server dies](../server/how-a-server-dies#the-crash-that-saves)
owns that relay, and
[the client loop](../client/the-client-loop#starting-and-the-three-ways-of-stopping)
the client's own three exits). The asymmetry is Part I's, because it follows
from two programs sharing a JVM: only a loop constructed to propagate
crashes rethrows a parked report, and `IntegratedServer` is not one. So a
worker that dies doing the server's work in singleplayer takes down the
*client*, and the crash report names a thread the player was not watching.

## Questions players ask

**Does a dedicated server pause?** Yes — an empty server stops ticking on
its own ([the server tick](../server/server-tick#an-empty-server-stops-ticking)
has the counter and what still runs). Pausing is not a singleplayer concept;
only the client-decides-it half is.

**Is twenty ticks a second a constant?** No, it is a server field. The
`TickRateManager.nanosecondsPerTick` the loop re-reads lives on a
`ServerTickRateManager`, the server's subclass of it, which adds the sprint
`/tick` manipulates and sends clients the rate and the freeze; the client
mirrors those two in `ClientLevel`, so the `DeltaTracker` slows and freezes
with the server.

**Does a busy server skip work?** Less than you would think. The tick
budget is one predicate, `MinecraftServer.haveTime`, travelling from
`MinecraftServer.tickServer` down through every level, and what it actually
gates is a short list that does not include loading or generating a chunk. [The server
tick](../server/server-tick#what-the-budget-actually-gates) has that list,
and the sprint's inverted effect on it.

> **For a 1.21-era reader.** The client's clock is `DeltaTracker`, which was
> *Timer*, and the partial tick is a `DeltaTracker.Timer` you ask rather than
> a float you are handed — it appears in the frame loop above and in every
> frame's extract in Part XI. GLFW is gone; SDL makes the window, delivers its
> events and supplies the time source. The rest of the drift a 1.21 reader
> will trip on is [naming drift](../../reference/naming-drift).

## Where to look

`client/main/Main` · `Minecraft` · `server/Main` · `MinecraftServer` ·
`IntegratedServer` · `BlockableEventLoop` · `Util` (the executors) ·
`EventLoopGroupHolder` · `ServerConnectionListener` · `Connection` ·
`PacketProcessor` · `PacketUtils`

---

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