Minecraft: Java Edition · C++23 + Vulkan

Written
from scratch

No engine, no fork of anything, no decompiled game code. Crit opens a TCP socket, speaks the protocol, and draws what the server sends. Today it joins real servers and renders the world. There is no window yet, so frames are written to PNG.

  • Joins live servers on protocol 774 and 776, tested against both
  • 0.247 ms per frame with 2.04 million triangles resident
  • Clean-room — no decompiled game code, no Mojang asset in the repo
  • Nothing a server would ban — no reach, aim assist, auto-clicker or ESP
Status in development, nothing to download Licence GPL-3.0
A savanna rendered by Crit: acacia trees, grass, flowers, a cliff edge and an ocean beyond.
240 chunks streamed from a live server, meshed and drawn in one pass. Textures are read at run time from the player's own Minecraft installation; none ship with the client and none are in the repository.
$ crit-client worldshot localhost:25601 world.png 240 eye
- connected to localhost:25601 as protocol 774
- compression from 256 bytes
- logged in as CritShot (1c47e8b1-f7e0-3479…)
- no known packs reported; server sends the full registries
- in the play state
- entered the world, entity 4048
- 240 chunks fetched
- chunks span x -208..64, z -112..160 (272 blocks wide)
  240 chunks, 2309276 triangles, 101.6 MB vertex data
  meshing 343 ms, upload 162 ms
  gpu time in the frame: 0.82 ms

Numbers

Measured on an RTX 4070 Laptop at 1920×1080, with 200 chunks of overworld resident and the camera turning, so the figure is not one lucky viewpoint.

MetricValueDetail
Frame0.247 ms4050 fps
1% low0.320 ms3126 fps
Meshing1.35 msper chunk, single thread
Vertex16 bytesposition, uv, light, ambient occlusion
Triangles2.04 M200 chunks
Block table29,671states, mmapped in 20 µs

This measures the draw path and nothing else. There is no game logic in the loop, no entities, no interface, and the world sits in one static buffer. It is not what a finished client will show, and quoting it as if it were would be dishonest.

What it is, and what it will not be

Crit is meant to be a client a server operator can allow. Everything in it has to be defensible on a server running anticheat, which rules out a whole category of features by definition: anything that hands the player information or input the vanilla client does not have.

There will be no reach, no aim assist, no auto-clicker, no click pattern generator, no ESP, no tracers and no hitbox edits. Not behind a toggle, not as a paid tier, not "for private servers". That line is the product, not a disclaimer attached to it.

The other half of the same idea is that a server gets a say. Crit listens on a plugin channel, crit:features, and a server can switch any feature off for as long as the player is connected. Toggle sprint and hitbox outlines are vanilla-renderable and still off by default, because plenty of servers object to them.

It also refuses to play on an account that does not own the game. That check runs before the handshake, including against offline-mode servers, where nothing would have forced it.

Build log

Nearly every bug below was found by measuring, not by reasoning about it. Several survived a confident argument that they did not exist. This is the honest version.

Packet IDs

The protocol wiki was wrong, and being wrong there is fatal

The community wiki is where everyone starts. It lists select_known_packs at 0x0F and code_of_conduct at 0x14. Both are wrong for protocols 774 and 776, and a wrong ID during configuration means the server closes the connection before a single chunk arrives.

The fix was to stop reading about the protocol and generate it instead. Mojang's own server jar emits a complete packet report when run with --reports. A script turns that into C++ tables, one set per protocol version. Supporting a new version is now one command rather than an afternoon of diffing, and 102 of the 139 clientbound play IDs turn out to differ between 774 and 776.

Depth

The world rendered inside out

The first frame with real terrain in it showed the far side of the landscape drawn straight through the near side. Three hypotheses came first, each argued confidently and each wrong: winding order, chunk boundaries, camera stuck inside a hill.

It was reverse-Z. The depth buffer was being cleared to 0 and tested with GREATER, which is what gives a floating point depth buffer usable precision far from the camera, but the projection matrix was still writing ordinary depth. So the test faithfully kept whatever was furthest away. One corrected matrix fixed every frame.

Winding

Every upward face was missing, and my own proof said otherwise

Blocks were drawing two faces instead of six. I wrote a numeric check over the vertices, worked the screen-space winding through by hand, and concluded the triangles were correct. They were, in the coordinate system I had in my head.

Vulkan decides winding in framebuffer coordinates, where Y points down. That flips the sign of every cross product, so counter-clockwise front faces are clockwise to the rasteriser, and every upward-facing quad was being culled. The check was not a check; it was the same mistake run a second time. What found it was someone looking at the picture and saying those were not whole blocks, just two faces of them.

raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;  // wrong
raster.frontFace = VK_FRONT_FACE_CLOCKWISE;          // Y points down
Occlusion

Holes in the ground, and grass that hid nothing

Two bugs wearing the same costume. The first: whether a block was a full cube was answered from geometry alone, so leaves, glass and ice were allowed to hide their neighbours and you could see through the world past them.

The second was worse. The full-cube test counted quads and demanded exactly six. Grass has ten, six sides plus four tinted overlays, so ordinary grass counted as not solid and culled nothing, anywhere, in any world. Both tests now ask the real question: whether all six directions are covered edge to edge by an opaque texture.

$ crit-client meshtest 1
state 1 minecraft:stone
  flags 0x0e  full cube yes  opaque yes  fluid no
  solid section: 1536 quads kept, 23040 culled
  (the shell alone would be 1536)
Atlas

The texture atlas bled at every tile border

Packing block textures into one grid atlas is the standard trick and it brings the standard defect: at a distance the GPU samples across tile edges, and dirt grows a thin green line from the grass sitting beside it in the atlas. The usual answer is padding and clamped mip levels, which is a lot of arithmetic to get slightly wrong.

Replacing the grid with a 2D array texture removes the problem instead of managing it. Each block texture gets its own layer with its own edges, 1098 of them for 1.21.11, so there is nothing adjacent to bleed from.

Fluids

Water with gaps in it

A water surface sits at fourteen sixteenths of a block rather than at the top. Applying that to every water block left a two-pixel gap between each block and the one above it. Only the top of a column should be lowered, and the mesher is the one place that knows what the neighbour above is, so that is where the rule belongs.

Method

Two renders prove nothing when the input moves

For a while progress was judged by rendering the world, changing something, and rendering again. That is worthless. A server does not send the same first two hundred chunks twice, so every pair of images differed for reasons unrelated to the change.

The tests pin their input now. A uniform section of a known block gives exact quad counts. A dumped chunk packet replays byte for byte. The renders are for looking at; the numbers decide.

Clean-room

Writing a client means knowing what the server expects, and there is a legitimate way to learn that and an illegitimate one. Crit uses only the first.

  • Protocol and registries come from the reports the official server jar generates itself with --reports.
  • Block geometry comes from the datapack JSON shipped inside the jar, which is data rather than code.
  • Textures are read at run time from the Minecraft installation already on the machine, by an account that owns the game.

Build scripts bake the first two into memory-mappable tables that the client reads as they are, with no JSON parsing and no allocation at startup. No decompiled game code is read or used anywhere in the project. Nothing of Mojang's is in the repository, and nothing of Mojang's ships with the client.

What's next

Finished so far: the protocol through handshake, login, configuration and play, with generated packet tables per version; chunk decoding, per-section meshing, ambient occlusion and frustum culling; and the Vulkan 1.3 renderer with reverse-Z, array textures and an on-disk pipeline cache.

In progress:

  • A window. Swapchain, input and a camera you steer. The step between a screenshot and a game.
  • Sign-in. Microsoft authentication, starting with the launcher handoff rather than a client ID of its own.

After that:

  • Movement and entities. Collision and physics that agree with the server, and other players on screen.
  • The PvP interface. The HUD a competitive client is actually used for, all of it lockable by the server.
  • Singleplayer. World generation and a local server, so the client stands on its own.