Skip to content

feat(wasm-mesher): parse 1.21.5 – 26.3 map_chunk packets, fix 1.18+ light order - #101

Merged
zardoy merged 3 commits into
zardoy:mainfrom
mc-zuri:feat/wasm-mesher-new-chunk-formats
Oct 16, 2026
Merged

zardoy merged 3 commits into
zardoy:mainfrom
mc-zuri:feat/wasm-mesher-new-chunk-formats

Conversation

@mc-zuri

@mc-zuri mc-zuri commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

parseMapChunkV18Plus (and the fused generateGeometryFromMapChunkV18Plus* paths) only understood the 1.18–1.21.4 map_chunk layout. From 1.21.5 on it failed, so every chunk fell back to the slower JS convertChunkToWasm path, and on 26.x it couldn't be used at all. This PR adds the newer layouts and fixes a long-standing light-order bug in the 1.18+ path.

1. Parse 1.21.5 – 26.3 map_chunk packets

The layout is chosen by protocol in McVersionFlags::for_protocol:

Version (protocol) Change
1.21.5 (770) heightmaps are an array of { type, long[] } instead of NBT
1.21.5 (770) palette containers no longer send the data array length (nor the single-value container's empty array); the long count is derived from the capacity (4096 blocks / 64 biomes) and bits per value
26.1 (775) each section has a fluid count after its block count
26.3 (777) the four light masks are byte arrays (BitSet.valueOf(byte[])) instead of long arrays

2. Read 1.18+ map_chunk light in vanilla nibble order

The light arrays in a map_chunk are vanilla DataLayer bytes: byte i holds blocks 2i (low nibble) and 2i + 1. The parser decoded them as long-encoded BitArray dumps, which reverses each 8-byte run, so every 16-block row came out mirrored along X. With smooth lighting this showed up as dark bands or shadows in the wrong place wherever light changed along a row. The 1.16/1.17 update_light paths already used the natural order.

The existing 1.18.2 – 1.21 parity references had been generated through a prismarine-chunk that reads the same mirrored order, so they agreed with the bug.

  • Physics check: in open air, sky light never differs by more than 1 between neighbouring blocks. The stored references break that rule 25 – 623 times per chunk; with their light un-mirrored they break it 0 times.
  • Fix in this PR: the references are corrected, with blocks and biomes untouched.
  • New unit test: a synthetic one-section packet checks the nibble order directly.

3. Size and decode containers like vanilla

PalettedContainer.read maps the bits byte on the wire through a Strategy configuration. That configuration's bits in memory decode the data array, and from 1.21.5 on they also size it:

  • block palettes of 1–4 bits are stored with 4 (FOUR_BITS_LINEAR); biome palettes keep 1–3 bits;
  • direct containers use the global palette bits (ceillog2 of the registry size), whatever the wire byte says.

parse_wire_container follows this for map_chunk sections, and parse_container once again only reads prismarine-chunk dumps. The worker used to pass 8 and 3 as the global bits, so any direct container (a section with more than 256 distinct block states) decoded as noise. It now computes them from minecraft-data per version: 15 bits for blocks up to 26.2, 16 for 26.3, and 6 or 7 for biomes.

Testing

  • New parity fixtures hold three real map_chunk packets each for 1.21.5, 1.21.11, 26.1 and 26.3, taken from vanilla client recordings. References come from prismarine-chunk with 26.x support (loaded like mineflayer loads a map_chunk). Blocks, biomes, sky and block light match byte for byte.
  • Unit tests cover the cases the fixture world doesn't: palette data without a length, the single-value container, the biome capacity and the byte-array BitSet.
  • Container unit tests cover a direct container with the global bits, a direct container whose wire byte differs from them, its length-prefixed form, and a 2-bit block palette stored with 4.
  • cargo test: 51 passed. vitest: 451 passed. The build.sh web snapshot test and test-section-boundary pass.
  • Given the worker's global bits, the built WASM parsed all 12,098 map_chunk packets from vanilla recordings of 19 versions (1.18.2 – 26.3) with no failures. Blocks, biomes, sky and block light match prismarine-chunk at every position.

Notes

  • runtime-build is rebuilt with wasm-pack 0.15. The generated .d.ts only lists the exports in a different order; signatures are unchanged.
  • To mesh 26.x worlds, the workers also need a minecraft-data that knows 26.x and a prismarine-chunk with 26.x support. Both are dependency bumps outside this PR.

🤖 Generated with Claude Code

The light arrays in a 1.18+ map_chunk are vanilla DataLayer bytes:
byte i holds blocks 2i (low nibble) and 2i + 1 (high nibble).
parse_map_chunk_v18plus decoded them as long-encoded BitArray dumps,
which reverse the bytes of every 8-byte run, so each 16-block row
came out mirrored along X. Smooth lighting then showed dark bands and
shadows in the wrong place wherever light changed along a row. The
1.16 and 1.17 update_light paths already read the natural order.

The parity references for 1.18.2, 1.19.4, 1.20.4 and 1.21 had been
generated through a prismarine-chunk that also reads the mirrored
order, so they agreed with the bug. In open air, sky light never
differs by more than 1 between neighbouring blocks. As stored, those
references broke that rule 25 to 623 times per chunk. With their sky
and block light un-mirrored, which is what this commit stores, they
break it 0 times. Blocks and biomes are unchanged.

A synthetic one-section packet checks the nibble order directly.
runtime-build is rebuilt with wasm-pack 0.15; the bindings only list
their exports in a different order.
parse_map_chunk_v18plus only knew the 1.18 to 1.21.4 layout, so from
1.21.5 on it failed and every chunk fell back to the slower JS
conversion. It now follows the format changes, chosen by protocol:

- 1.21.5 (770): heightmaps are an array of { type, long[] } instead
  of NBT, and palette containers no longer send the data array length
  (or the single value container's empty array). The long count
  follows from the capacity, 4096 blocks or 64 biomes, and the bits
  per value.
- 26.1 (775): each section has a fluid count after its block count.
- 26.3 (777): the four light masks are byte arrays
  (java.util.BitSet.valueOf(byte[])) instead of long arrays.

New parity fixtures hold three map_chunk packets each for 1.21.5,
1.21.11, 26.1 and 26.3, taken from vanilla client recordings. Their
references come from prismarine-chunk with 26.x support, loaded the
way mineflayer loads a map_chunk. Blocks, biomes and both light
arrays match byte for byte. Unit tests cover the cases the fixture
world doesn't: palette data without a length, the single value
container and the byte array BitSet.
PalettedContainer.read does not use the bits byte on the wire as is.
It picks a Strategy configuration from it, and that configuration's
bits in memory decode the data array (and, from 1.21.5 on, size it:
SimpleBitStorage holds ceil(capacity / (64 / bits)) longs):

- block palettes of 1 to 4 bits are stored with 4 (FOUR_BITS_LINEAR);
  biome palettes keep 1 to 3 bits;
- direct containers are stored with the global palette bits,
  ceillog2 of the registry size, whatever the wire byte says.

parse_wire_container now follows that for map_chunk sections, and
parse_container goes back to reading prismarine-chunk dumps only.

The global palette bits also have to be right: the worker passed 8
for blocks and 3 for biomes, so every direct container, a section with
more than 256 distinct block states, decoded as noise. They now come
from minecraft-data for the version: 15 bits for blocks up to 26.2,
16 for 26.3, and 6 or 7 for biomes.

Vanilla servers send the wire byte equal to the bits in memory, so all
12,098 recorded chunks of 19 versions from 1.18.2 to 26.3 still parse
identically to prismarine-chunk. New tests
cover a direct container with the global bits, a direct container
whose wire byte differs from them, its prefixed form, and a 2-bit
block palette stored with 4.
@zardoy
zardoy marked this pull request as ready for review October 16, 2026 11:28
@zardoy
zardoy merged commit faba68f into zardoy:main Oct 16, 2026
1 check passed
@zardoy

zardoy commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

I trust you, all good

@zardoy

zardoy commented Oct 25, 2026

Copy link
Copy Markdown
Owner

@mc-zuri TYSM your changes are finally released!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants