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.