From 577a54c516e4e6614782b0b4ed28db4ed67d4376 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 13 Jul 2026 18:19:34 -0400 Subject: [PATCH 1/8] Add block network hash conversion helpers --- server/world/chunk/block_network_hash.go | 62 +++++++++++++++++ server/world/chunk/block_network_hash_test.go | 69 +++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 server/world/chunk/block_network_hash.go create mode 100644 server/world/chunk/block_network_hash_test.go diff --git a/server/world/chunk/block_network_hash.go b/server/world/chunk/block_network_hash.go new file mode 100644 index 0000000000..13d94d438e --- /dev/null +++ b/server/world/chunk/block_network_hash.go @@ -0,0 +1,62 @@ +package chunk + +// ConvertBlockNetworkHashesToRuntimeIDs converts block palette values from network hashes to registry runtime IDs. +// Unknown hashes are preserved unchanged. +func (chunk *Chunk) ConvertBlockNetworkHashesToRuntimeIDs() { + if chunk == nil { + return + } + for _, sub := range chunk.sub { + sub.ConvertBlockNetworkHashesToRuntimeIDs(chunk.br) + } +} + +// ConvertBlockNetworkHashesToRuntimeIDs converts block palette values from network hashes to registry runtime IDs. +// Unknown hashes are preserved unchanged. +func (sub *SubChunk) ConvertBlockNetworkHashesToRuntimeIDs(br BlockRegistry) { + if sub == nil || br == nil { + return + } + for _, storage := range sub.storages { + if storage == nil { + continue + } + storage.palette.Replace(func(runtimeID uint32) uint32 { + if converted, ok := br.HashToRuntimeID(runtimeID); ok { + return converted + } + return runtimeID + }) + } +} + +// EncodeWithBlockNetworkHashes encodes c for the network with block palette runtime IDs converted to network hashes. +// The chunk is cloned before conversion, so the source chunk remains in registry runtime-ID form. Unknown runtime IDs +// are preserved unchanged. +func EncodeWithBlockNetworkHashes(c *Chunk) SerialisedData { + if c == nil { + return SerialisedData{} + } + networkChunk := c.Clone() + for _, sub := range networkChunk.sub { + sub.convertRuntimeIDsToBlockNetworkHashes(c.br) + } + return Encode(networkChunk, NetworkEncoding) +} + +func (sub *SubChunk) convertRuntimeIDsToBlockNetworkHashes(br BlockRegistry) { + if sub == nil || br == nil { + return + } + for _, storage := range sub.storages { + if storage == nil { + continue + } + storage.palette.Replace(func(runtimeID uint32) uint32 { + if hash, ok := br.RuntimeIDToHash(runtimeID); ok { + return hash + } + return runtimeID + }) + } +} diff --git a/server/world/chunk/block_network_hash_test.go b/server/world/chunk/block_network_hash_test.go new file mode 100644 index 0000000000..e2be9c94d6 --- /dev/null +++ b/server/world/chunk/block_network_hash_test.go @@ -0,0 +1,69 @@ +package chunk + +import ( + "bytes" + "testing" + + "github.com/df-mc/dragonfly/server/block/cube" +) + +func TestChunkConvertsBlockNetworkHashesToRuntimeIDs(t *testing.T) { + registry := networkHashTestRegistry{air: 0, hashToRuntimeID: map[uint32]uint32{100: 1}} + c := New(registry, cube.Range{0, 15}) + c.SetBlock(1, 2, 3, 0, 100) + + c.ConvertBlockNetworkHashesToRuntimeIDs() + + if got := c.Block(1, 2, 3, 0); got != 1 { + t.Fatalf("block runtime ID = %d, want 1", got) + } +} + +func TestEncodeWithBlockNetworkHashesDoesNotMutateChunk(t *testing.T) { + registry := networkHashTestRegistry{air: 0, runtimeIDToHash: map[uint32]uint32{1: 100}} + c := New(registry, cube.Range{0, 15}) + c.SetBlock(1, 2, 3, 0, 1) + + data := EncodeWithBlockNetworkHashes(c) + buf := bytes.NewBuffer(data.SubChunks[0]) + index := byte(0) + decoded, err := decodeSubChunk(buf, c, &index, NetworkEncoding) + if err != nil { + t.Fatal(err) + } + if got := decoded.Block(1, 2, 3, 0); got != 100 { + t.Fatalf("encoded block ID = %d, want network hash 100", got) + } + if got := c.Block(1, 2, 3, 0); got != 1 { + t.Fatalf("source block ID = %d after encoding, want 1", got) + } +} + +type networkHashTestRegistry struct { + air uint32 + hashToRuntimeID map[uint32]uint32 + runtimeIDToHash map[uint32]uint32 +} + +func (r networkHashTestRegistry) BlockCount() int { return 1000 } +func (r networkHashTestRegistry) AirRuntimeID() uint32 { return r.air } +func (networkHashTestRegistry) RuntimeIDToState(uint32) (string, map[string]any, bool) { + return "test:block", nil, true +} +func (networkHashTestRegistry) StateToRuntimeID(string, map[string]any) (uint32, bool) { + return 0, true +} +func (networkHashTestRegistry) FilteringBlock(uint32) uint8 { return 0 } +func (networkHashTestRegistry) LightBlock(uint32) uint8 { return 0 } +func (networkHashTestRegistry) RandomTickBlock(uint32) bool { return false } +func (networkHashTestRegistry) NBTBlock(uint32) bool { return false } +func (networkHashTestRegistry) LiquidDisplacingBlock(uint32) bool { return false } +func (networkHashTestRegistry) LiquidBlock(uint32) bool { return false } +func (r networkHashTestRegistry) HashToRuntimeID(hash uint32) (uint32, bool) { + runtimeID, ok := r.hashToRuntimeID[hash] + return runtimeID, ok +} +func (r networkHashTestRegistry) RuntimeIDToHash(runtimeID uint32) (uint32, bool) { + hash, ok := r.runtimeIDToHash[runtimeID] + return hash, ok +} From 7555d770febc37c3b2596fac8a2de58245a7aa99 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 13 Jul 2026 18:34:20 -0400 Subject: [PATCH 2/8] Add hashed subchunk encoding helper --- server/world/chunk/block_network_hash.go | 11 ++++++++++ server/world/chunk/block_network_hash_test.go | 20 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/server/world/chunk/block_network_hash.go b/server/world/chunk/block_network_hash.go index 13d94d438e..379561a858 100644 --- a/server/world/chunk/block_network_hash.go +++ b/server/world/chunk/block_network_hash.go @@ -44,6 +44,17 @@ func EncodeWithBlockNetworkHashes(c *Chunk) SerialisedData { return Encode(networkChunk, NetworkEncoding) } +// EncodeSubChunkWithBlockNetworkHashes encodes one sub-chunk for the network with block palette runtime IDs converted +// to network hashes. The chunk is cloned before conversion, so the source chunk remains unchanged. +func EncodeSubChunkWithBlockNetworkHashes(c *Chunk, index int) []byte { + if c == nil || index < 0 || index >= len(c.sub) { + return nil + } + networkChunk := c.Clone() + networkChunk.sub[index].convertRuntimeIDsToBlockNetworkHashes(c.br) + return EncodeSubChunk(networkChunk, NetworkEncoding, index) +} + func (sub *SubChunk) convertRuntimeIDsToBlockNetworkHashes(br BlockRegistry) { if sub == nil || br == nil { return diff --git a/server/world/chunk/block_network_hash_test.go b/server/world/chunk/block_network_hash_test.go index e2be9c94d6..2431c08bbe 100644 --- a/server/world/chunk/block_network_hash_test.go +++ b/server/world/chunk/block_network_hash_test.go @@ -39,6 +39,26 @@ func TestEncodeWithBlockNetworkHashesDoesNotMutateChunk(t *testing.T) { } } +func TestEncodeSubChunkWithBlockNetworkHashesDoesNotMutateChunk(t *testing.T) { + registry := networkHashTestRegistry{runtimeIDToHash: map[uint32]uint32{7: 70}} + c := New(registry, cube.Range{0, 15}) + c.SetBlock(1, 1, 1, 0, 7) + + encoded := EncodeSubChunkWithBlockNetworkHashes(c, 0) + buf := bytes.NewBuffer(encoded) + index := byte(0) + decoded, err := decodeSubChunk(buf, c, &index, NetworkEncoding) + if err != nil { + t.Fatal(err) + } + if got := decoded.Block(1, 1, 1, 0); got != 70 { + t.Fatalf("encoded block runtime ID = %d, want network hash 70", got) + } + if got := c.Block(1, 1, 1, 0); got != 7 { + t.Fatalf("source block runtime ID = %d, want 7", got) + } +} + type networkHashTestRegistry struct { air uint32 hashToRuntimeID map[uint32]uint32 From 2de5b48545f84ee4a46411c681affe850d9bbcd7 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 13 Jul 2026 18:45:37 -0400 Subject: [PATCH 3/8] Avoid cloning full chunks for hash encoding --- server/world/chunk/block_network_hash.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/world/chunk/block_network_hash.go b/server/world/chunk/block_network_hash.go index 379561a858..8b5a4e8af7 100644 --- a/server/world/chunk/block_network_hash.go +++ b/server/world/chunk/block_network_hash.go @@ -1,5 +1,7 @@ package chunk +import "slices" + // ConvertBlockNetworkHashesToRuntimeIDs converts block palette values from network hashes to registry runtime IDs. // Unknown hashes are preserved unchanged. func (chunk *Chunk) ConvertBlockNetworkHashesToRuntimeIDs() { @@ -50,9 +52,11 @@ func EncodeSubChunkWithBlockNetworkHashes(c *Chunk, index int) []byte { if c == nil || index < 0 || index >= len(c.sub) { return nil } - networkChunk := c.Clone() + networkChunk := *c + networkChunk.sub = slices.Clone(c.sub) + networkChunk.sub[index] = c.sub[index].Clone() networkChunk.sub[index].convertRuntimeIDsToBlockNetworkHashes(c.br) - return EncodeSubChunk(networkChunk, NetworkEncoding, index) + return EncodeSubChunk(&networkChunk, NetworkEncoding, index) } func (sub *SubChunk) convertRuntimeIDsToBlockNetworkHashes(br BlockRegistry) { From d3a42efadf412d89d38652ec43ad89b7eef0a5df Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 13 Jul 2026 18:58:44 -0400 Subject: [PATCH 4/8] Fix chunk decoder comment formatting --- server/world/chunk/decode.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/world/chunk/decode.go b/server/world/chunk/decode.go index 36a229b8fe..10c4a19414 100644 --- a/server/world/chunk/decode.go +++ b/server/world/chunk/decode.go @@ -41,7 +41,7 @@ func NetworkDecodeBuffer(br BlockRegistry, buf *bytes.Buffer, count int, r cube. if index > maxIndex { // TODO: This is a work-around for some JE -> BE converters where there are more sub chunks than expected. It is to be determined if this // will have any side-effects. For now, we will just ignore the sub chunks and not insert them. We still have to decode all of them out of the buffer, however. - //return nil, nil, fmt.Errorf("sub chunk index %v is greater than max %v", index, maxIndex) + // return nil, nil, fmt.Errorf("sub chunk index %v is greater than max %v", index, maxIndex) continue } newChunk.sub[index] = sub From a683785ee72ef4df1bf24f02f49a63e4a446cc57 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Mon, 13 Jul 2026 19:04:35 -0400 Subject: [PATCH 5/8] Test unmapped block hash preservation --- server/world/chunk/block_network_hash_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/server/world/chunk/block_network_hash_test.go b/server/world/chunk/block_network_hash_test.go index 2431c08bbe..cc99408633 100644 --- a/server/world/chunk/block_network_hash_test.go +++ b/server/world/chunk/block_network_hash_test.go @@ -11,18 +11,23 @@ func TestChunkConvertsBlockNetworkHashesToRuntimeIDs(t *testing.T) { registry := networkHashTestRegistry{air: 0, hashToRuntimeID: map[uint32]uint32{100: 1}} c := New(registry, cube.Range{0, 15}) c.SetBlock(1, 2, 3, 0, 100) + c.SetBlock(4, 5, 6, 0, 999) c.ConvertBlockNetworkHashesToRuntimeIDs() if got := c.Block(1, 2, 3, 0); got != 1 { t.Fatalf("block runtime ID = %d, want 1", got) } + if got := c.Block(4, 5, 6, 0); got != 999 { + t.Fatalf("unmapped block ID = %d, want 999", got) + } } func TestEncodeWithBlockNetworkHashesDoesNotMutateChunk(t *testing.T) { registry := networkHashTestRegistry{air: 0, runtimeIDToHash: map[uint32]uint32{1: 100}} c := New(registry, cube.Range{0, 15}) c.SetBlock(1, 2, 3, 0, 1) + c.SetBlock(4, 5, 6, 0, 999) data := EncodeWithBlockNetworkHashes(c) buf := bytes.NewBuffer(data.SubChunks[0]) @@ -34,15 +39,22 @@ func TestEncodeWithBlockNetworkHashesDoesNotMutateChunk(t *testing.T) { if got := decoded.Block(1, 2, 3, 0); got != 100 { t.Fatalf("encoded block ID = %d, want network hash 100", got) } + if got := decoded.Block(4, 5, 6, 0); got != 999 { + t.Fatalf("encoded unmapped block ID = %d, want 999", got) + } if got := c.Block(1, 2, 3, 0); got != 1 { t.Fatalf("source block ID = %d after encoding, want 1", got) } + if got := c.Block(4, 5, 6, 0); got != 999 { + t.Fatalf("source unmapped block ID = %d after encoding, want 999", got) + } } func TestEncodeSubChunkWithBlockNetworkHashesDoesNotMutateChunk(t *testing.T) { registry := networkHashTestRegistry{runtimeIDToHash: map[uint32]uint32{7: 70}} c := New(registry, cube.Range{0, 15}) c.SetBlock(1, 1, 1, 0, 7) + c.SetBlock(2, 2, 2, 0, 999) encoded := EncodeSubChunkWithBlockNetworkHashes(c, 0) buf := bytes.NewBuffer(encoded) @@ -54,9 +66,15 @@ func TestEncodeSubChunkWithBlockNetworkHashesDoesNotMutateChunk(t *testing.T) { if got := decoded.Block(1, 1, 1, 0); got != 70 { t.Fatalf("encoded block runtime ID = %d, want network hash 70", got) } + if got := decoded.Block(2, 2, 2, 0); got != 999 { + t.Fatalf("encoded unmapped block runtime ID = %d, want 999", got) + } if got := c.Block(1, 1, 1, 0); got != 7 { t.Fatalf("source block runtime ID = %d, want 7", got) } + if got := c.Block(2, 2, 2, 0); got != 999 { + t.Fatalf("source unmapped block runtime ID = %d, want 999", got) + } } type networkHashTestRegistry struct { From 014e92f76f450206ac3eaf9c30aedc03babe32b0 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 23 Jun 2026 14:25:30 -0400 Subject: [PATCH 6/8] Add runtime cache chunk compaction --- server/world/chunk/chunk.go | 8 + server/world/chunk/compact_runtime_test.go | 176 +++++++++++++++++++++ server/world/chunk/paletted_storage.go | 63 ++++++++ server/world/chunk/sub_chunk.go | 15 ++ 4 files changed, 262 insertions(+) create mode 100644 server/world/chunk/compact_runtime_test.go diff --git a/server/world/chunk/chunk.go b/server/world/chunk/chunk.go index ddee338b51..a23cfe55a6 100644 --- a/server/world/chunk/chunk.go +++ b/server/world/chunk/chunk.go @@ -217,6 +217,14 @@ func (chunk *Chunk) Compact() { } } +// CompactForRuntimeCache performs cheap in-memory compaction on chunk block storages. It collapses uniform +// storages and shrinks oversized storage widths, but avoids scanning multi-value storages for unused palette entries. +func (chunk *Chunk) CompactForRuntimeCache() { + for i := range chunk.sub { + chunk.sub[i].compactForRuntimeCache() + } +} + // SubChunk finds the correct SubChunk in the Chunk by a Y value. func (chunk *Chunk) SubChunk(y int16) *SubChunk { return chunk.sub[chunk.SubIndex(y)] diff --git a/server/world/chunk/compact_runtime_test.go b/server/world/chunk/compact_runtime_test.go new file mode 100644 index 0000000000..1eb40294f5 --- /dev/null +++ b/server/world/chunk/compact_runtime_test.go @@ -0,0 +1,176 @@ +package chunk + +import ( + "testing" + + "github.com/df-mc/dragonfly/server/block/cube" +) + +func TestPalettedStorageCompactForRuntimeCacheCollapsesSingleValueStorage(t *testing.T) { + storage := newPalettedStorage(make([]uint32, paletteSize(1).uint32s()), newPalette(1, []uint32{42})) + + storage.compactForRuntimeCache() + + if storage.bitsPerIndex != 0 { + t.Fatalf("bitsPerIndex = %d, want 0", storage.bitsPerIndex) + } + if storage.indices != nil { + t.Fatalf("indices = %v, want nil", storage.indices) + } + if storage.indicesStart != nil { + t.Fatalf("indicesStart = %v, want nil", storage.indicesStart) + } + if got := storage.At(3, 4, 5); got != 42 { + t.Fatalf("At() = %d, want retained single palette value 42", got) + } +} + +func TestSubChunkCompactForRuntimeCacheDropsSingleValueAirStorage(t *testing.T) { + sub := NewSubChunk(7) + sub.storages = []*PalettedStorage{ + newPalettedStorage(make([]uint32, paletteSize(1).uint32s()), newPalette(1, []uint32{7})), + } + + sub.compactForRuntimeCache() + + if len(sub.storages) != 0 { + t.Fatalf("len(storages) = %d, want 0", len(sub.storages)) + } +} + +func TestSubChunkCompactForRuntimeCacheDropsZeroIndexedAirStorage(t *testing.T) { + sub := NewSubChunk(7) + sub.storages = []*PalettedStorage{ + newPalettedStorage(make([]uint32, paletteSize(4).uint32s()), newPalette(4, []uint32{7, 11, 22})), + } + + sub.compactForRuntimeCache() + + if len(sub.storages) != 0 { + t.Fatalf("len(storages) = %d, want 0", len(sub.storages)) + } +} + +func TestPalettedStorageCompactForRuntimeCacheCollapsesZeroIndexedStorage(t *testing.T) { + storage := newPalettedStorage(make([]uint32, paletteSize(4).uint32s()), newPalette(4, []uint32{42, 11, 22})) + + storage.compactForRuntimeCache() + + if storage.bitsPerIndex != 0 { + t.Fatalf("bitsPerIndex = %d, want 0", storage.bitsPerIndex) + } + if storage.indices != nil { + t.Fatalf("indices = %v, want nil", storage.indices) + } + if got := storage.palette.Len(); got != 1 { + t.Fatalf("palette len = %d, want 1", got) + } + if got := storage.At(3, 4, 5); got != 42 { + t.Fatalf("At() = %d, want retained palette index 0 value 42", got) + } +} + +func TestPalettedStorageCompactForRuntimeCacheCollapsesUniformNonZeroIndexStorage(t *testing.T) { + storage := newPalettedStorage(make([]uint32, paletteSize(4).uint32s()), newPalette(4, []uint32{11, 42, 22})) + fillStorage(storage, 42) + + storage.compactForRuntimeCache() + + if storage.bitsPerIndex != 0 { + t.Fatalf("bitsPerIndex = %d, want 0", storage.bitsPerIndex) + } + if got := storage.palette.Len(); got != 1 { + t.Fatalf("palette len = %d, want 1", got) + } + if got := storage.At(3, 4, 5); got != 42 { + t.Fatalf("At() = %d, want retained uniform value 42", got) + } +} + +func TestSubChunkCompactForRuntimeCacheDropsUniformNonZeroIndexAirStorage(t *testing.T) { + sub := NewSubChunk(7) + storage := newPalettedStorage(make([]uint32, paletteSize(4).uint32s()), newPalette(4, []uint32{11, 7, 22})) + fillStorage(storage, 7) + sub.storages = []*PalettedStorage{storage} + + sub.compactForRuntimeCache() + + if len(sub.storages) != 0 { + t.Fatalf("len(storages) = %d, want 0", len(sub.storages)) + } +} + +func TestChunkCompactForRuntimeCacheDoesNotRepackMultiValueStorage(t *testing.T) { + c := New(testBlockRegistry{air: 0}, testRange()) + storage := newPalettedStorage(make([]uint32, paletteSize(1).uint32s()), newPalette(1, []uint32{1, 2})) + storage.Set(1, 2, 3, 2) + c.sub[0].storages = []*PalettedStorage{storage} + indices := &storage.indices[0] + paletteValues := len(storage.palette.values) + + c.CompactForRuntimeCache() + + if c.sub[0].storages[0] != storage { + t.Fatal("multi-value storage pointer changed; cheap compaction should not repack it") + } + if &storage.indices[0] != indices { + t.Fatal("multi-value storage indices were replaced; cheap compaction should not scan/rewrite them") + } + if got := len(storage.palette.values); got != paletteValues { + t.Fatalf("palette len = %d, want %d", got, paletteValues) + } +} + +func TestPalettedStorageCompactForRuntimeCacheShrinksOversizedMultiValueStorage(t *testing.T) { + storage := newPalettedStorage(make([]uint32, paletteSize(4).uint32s()), newPalette(4, []uint32{11, 22})) + storage.Set(1, 2, 3, 22) + + storage.compactForRuntimeCache() + + if got, want := storage.bitsPerIndex, uint16(1); got != want { + t.Fatalf("bitsPerIndex = %d, want %d", got, want) + } + if got, want := len(storage.indices), paletteSize(1).uint32s(); got != want { + t.Fatalf("len(indices) = %d, want %d", got, want) + } + if got := len(storage.palette.values); got != 2 { + t.Fatalf("palette len = %d, want 2", got) + } + if got := storage.At(1, 2, 3); got != 22 { + t.Fatalf("At(1,2,3) = %d, want 22", got) + } +} + +type testBlockRegistry struct { + air uint32 +} + +func (r testBlockRegistry) BlockCount() int { return 3 } +func (r testBlockRegistry) AirRuntimeID() uint32 { + return r.air +} +func (testBlockRegistry) RuntimeIDToState(runtimeID uint32) (string, map[string]any, bool) { + return "test:block", nil, true +} +func (testBlockRegistry) StateToRuntimeID(string, map[string]any) (uint32, bool) { return 0, true } +func (testBlockRegistry) FilteringBlock(uint32) uint8 { return 0 } +func (testBlockRegistry) LightBlock(uint32) uint8 { return 0 } +func (testBlockRegistry) RandomTickBlock(uint32) bool { return false } +func (testBlockRegistry) NBTBlock(uint32) bool { return false } +func (testBlockRegistry) LiquidDisplacingBlock(uint32) bool { return false } +func (testBlockRegistry) LiquidBlock(uint32) bool { return false } +func (testBlockRegistry) HashToRuntimeID(hash uint32) (uint32, bool) { return hash, true } + +func testRange() cube.Range { + return cube.Range{0, 15} +} + +func fillStorage(storage *PalettedStorage, runtimeID uint32) { + for x := byte(0); x < 16; x++ { + for y := byte(0); y < 16; y++ { + for z := byte(0); z < 16; z++ { + storage.Set(x, y, z, runtimeID) + } + } + } +} diff --git a/server/world/chunk/paletted_storage.go b/server/world/chunk/paletted_storage.go index ac49c67c44..e254a8e9c7 100644 --- a/server/world/chunk/paletted_storage.go +++ b/server/world/chunk/paletted_storage.go @@ -166,6 +166,69 @@ func (storage *PalettedStorage) resize(newPaletteSize paletteSize) { *storage = *newStorage } +// compactForRuntimeCache performs the cheap subset of compact that is useful for chunks kept in memory. +// It collapses single-value storages and shrinks oversized storage widths, but avoids scanning multi-value +// storages for unused palette entries. +func (storage *PalettedStorage) compactForRuntimeCache() { + if storage.palette.Len() == 0 { + return + } + if storage.palette.Len() == 1 { + storage.collapseToPaletteIndex(0) + return + } + if index, ok := storage.uniformPaletteIndex(); ok { + storage.collapseToPaletteIndex(index) + return + } + + if size := paletteSizeFor(storage.palette.Len()); size < storage.palette.size { + storage.resize(size) + } +} + +func (storage *PalettedStorage) uniformPaletteIndex() (uint16, bool) { + if storage.bitsPerIndex == 0 { + return 0, true + } + indicesPerWord := uint32BitSize / int(storage.bitsPerIndex) + fullWords := 4096 / indicesPerWord + remainder := 4096 % indicesPerWord + if len(storage.indices) != paletteSize(storage.bitsPerIndex).uint32s() { + return 0, false + } + + index := uint16(storage.indices[0] & storage.indexMask) + fullPattern := repeatedPaletteIndexWord(index, storage.bitsPerIndex, indicesPerWord) + for _, word := range storage.indices[:fullWords] { + if word != fullPattern { + return 0, false + } + } + if remainder != 0 && storage.indices[fullWords] != repeatedPaletteIndexWord(index, storage.bitsPerIndex, remainder) { + return 0, false + } + return index, true +} + +func (storage *PalettedStorage) collapseToPaletteIndex(index uint16) { + value := storage.palette.Value(index) + storage.bitsPerIndex = 0 + storage.filledBitsPerIndex = 0 + storage.indexMask = 0 + storage.indicesStart = nil + storage.indices = nil + storage.palette = newPalette(0, []uint32{value}) +} + +func repeatedPaletteIndexWord(index uint16, bitsPerIndex uint16, count int) uint32 { + var word uint32 + for i := 0; i < count; i++ { + word |= uint32(index) << (uint16(i) * bitsPerIndex) + } + return word +} + // compact clears unused indexes in the palette by scanning for usages in the PalettedStorage. This is a // relatively heavy task which should only happen right before the sub chunk holding this PalettedStorage is // saved to disk. compact also shrinks the palette size if possible. diff --git a/server/world/chunk/sub_chunk.go b/server/world/chunk/sub_chunk.go index 7a06680deb..74210e332b 100644 --- a/server/world/chunk/sub_chunk.go +++ b/server/world/chunk/sub_chunk.go @@ -133,6 +133,21 @@ func (sub *SubChunk) SkyLight(x, y, z byte) uint8 { return (sub.skyLight[index>>1] >> ((index & 1) << 2)) & 0xf } +// compactForRuntimeCache performs cheap in-memory compaction on the sub chunk. Unlike compact, it does not scan +// multi-value storages for unused palette entries unless they are uniform and can be detected from packed words. +func (sub *SubChunk) compactForRuntimeCache() { + storages := sub.storages[:0] + for _, storage := range sub.storages { + storage.compactForRuntimeCache() + if storage.palette.Len() == 1 && storage.palette.Value(0) == sub.air { + continue + } + storages = append(storages, storage) + } + clear(sub.storages[len(storages):]) + sub.storages = storages +} + // Compact cleans the garbage from all block storages that sub chunk contains, so that they may be // cleanly written to a database. func (sub *SubChunk) compact() { From 083ef3b8b7666f54103c10208ef73a93dfcbdf84 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 14 Jul 2026 01:20:58 -0400 Subject: [PATCH 7/8] test: focus chunk coverage on runtime compaction --- server/world/chunk/block_network_hash_test.go | 107 ------------------ server/world/chunk/compact_runtime_test.go | 1 + 2 files changed, 1 insertion(+), 107 deletions(-) delete mode 100644 server/world/chunk/block_network_hash_test.go diff --git a/server/world/chunk/block_network_hash_test.go b/server/world/chunk/block_network_hash_test.go deleted file mode 100644 index cc99408633..0000000000 --- a/server/world/chunk/block_network_hash_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package chunk - -import ( - "bytes" - "testing" - - "github.com/df-mc/dragonfly/server/block/cube" -) - -func TestChunkConvertsBlockNetworkHashesToRuntimeIDs(t *testing.T) { - registry := networkHashTestRegistry{air: 0, hashToRuntimeID: map[uint32]uint32{100: 1}} - c := New(registry, cube.Range{0, 15}) - c.SetBlock(1, 2, 3, 0, 100) - c.SetBlock(4, 5, 6, 0, 999) - - c.ConvertBlockNetworkHashesToRuntimeIDs() - - if got := c.Block(1, 2, 3, 0); got != 1 { - t.Fatalf("block runtime ID = %d, want 1", got) - } - if got := c.Block(4, 5, 6, 0); got != 999 { - t.Fatalf("unmapped block ID = %d, want 999", got) - } -} - -func TestEncodeWithBlockNetworkHashesDoesNotMutateChunk(t *testing.T) { - registry := networkHashTestRegistry{air: 0, runtimeIDToHash: map[uint32]uint32{1: 100}} - c := New(registry, cube.Range{0, 15}) - c.SetBlock(1, 2, 3, 0, 1) - c.SetBlock(4, 5, 6, 0, 999) - - data := EncodeWithBlockNetworkHashes(c) - buf := bytes.NewBuffer(data.SubChunks[0]) - index := byte(0) - decoded, err := decodeSubChunk(buf, c, &index, NetworkEncoding) - if err != nil { - t.Fatal(err) - } - if got := decoded.Block(1, 2, 3, 0); got != 100 { - t.Fatalf("encoded block ID = %d, want network hash 100", got) - } - if got := decoded.Block(4, 5, 6, 0); got != 999 { - t.Fatalf("encoded unmapped block ID = %d, want 999", got) - } - if got := c.Block(1, 2, 3, 0); got != 1 { - t.Fatalf("source block ID = %d after encoding, want 1", got) - } - if got := c.Block(4, 5, 6, 0); got != 999 { - t.Fatalf("source unmapped block ID = %d after encoding, want 999", got) - } -} - -func TestEncodeSubChunkWithBlockNetworkHashesDoesNotMutateChunk(t *testing.T) { - registry := networkHashTestRegistry{runtimeIDToHash: map[uint32]uint32{7: 70}} - c := New(registry, cube.Range{0, 15}) - c.SetBlock(1, 1, 1, 0, 7) - c.SetBlock(2, 2, 2, 0, 999) - - encoded := EncodeSubChunkWithBlockNetworkHashes(c, 0) - buf := bytes.NewBuffer(encoded) - index := byte(0) - decoded, err := decodeSubChunk(buf, c, &index, NetworkEncoding) - if err != nil { - t.Fatal(err) - } - if got := decoded.Block(1, 1, 1, 0); got != 70 { - t.Fatalf("encoded block runtime ID = %d, want network hash 70", got) - } - if got := decoded.Block(2, 2, 2, 0); got != 999 { - t.Fatalf("encoded unmapped block runtime ID = %d, want 999", got) - } - if got := c.Block(1, 1, 1, 0); got != 7 { - t.Fatalf("source block runtime ID = %d, want 7", got) - } - if got := c.Block(2, 2, 2, 0); got != 999 { - t.Fatalf("source unmapped block runtime ID = %d, want 999", got) - } -} - -type networkHashTestRegistry struct { - air uint32 - hashToRuntimeID map[uint32]uint32 - runtimeIDToHash map[uint32]uint32 -} - -func (r networkHashTestRegistry) BlockCount() int { return 1000 } -func (r networkHashTestRegistry) AirRuntimeID() uint32 { return r.air } -func (networkHashTestRegistry) RuntimeIDToState(uint32) (string, map[string]any, bool) { - return "test:block", nil, true -} -func (networkHashTestRegistry) StateToRuntimeID(string, map[string]any) (uint32, bool) { - return 0, true -} -func (networkHashTestRegistry) FilteringBlock(uint32) uint8 { return 0 } -func (networkHashTestRegistry) LightBlock(uint32) uint8 { return 0 } -func (networkHashTestRegistry) RandomTickBlock(uint32) bool { return false } -func (networkHashTestRegistry) NBTBlock(uint32) bool { return false } -func (networkHashTestRegistry) LiquidDisplacingBlock(uint32) bool { return false } -func (networkHashTestRegistry) LiquidBlock(uint32) bool { return false } -func (r networkHashTestRegistry) HashToRuntimeID(hash uint32) (uint32, bool) { - runtimeID, ok := r.hashToRuntimeID[hash] - return runtimeID, ok -} -func (r networkHashTestRegistry) RuntimeIDToHash(runtimeID uint32) (uint32, bool) { - hash, ok := r.runtimeIDToHash[runtimeID] - return hash, ok -} diff --git a/server/world/chunk/compact_runtime_test.go b/server/world/chunk/compact_runtime_test.go index 1eb40294f5..78ac722a1f 100644 --- a/server/world/chunk/compact_runtime_test.go +++ b/server/world/chunk/compact_runtime_test.go @@ -160,6 +160,7 @@ func (testBlockRegistry) NBTBlock(uint32) bool func (testBlockRegistry) LiquidDisplacingBlock(uint32) bool { return false } func (testBlockRegistry) LiquidBlock(uint32) bool { return false } func (testBlockRegistry) HashToRuntimeID(hash uint32) (uint32, bool) { return hash, true } +func (testBlockRegistry) RuntimeIDToHash(runtimeID uint32) (uint32, bool) { return runtimeID, true } func testRange() cube.Range { return cube.Range{0, 15} From 536885cf50413ad9af7a84fe7fb21549dc68e842 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 14 Jul 2026 01:24:56 -0400 Subject: [PATCH 8/8] test: remove runtime compaction coverage --- server/world/chunk/compact_runtime_test.go | 177 --------------------- 1 file changed, 177 deletions(-) delete mode 100644 server/world/chunk/compact_runtime_test.go diff --git a/server/world/chunk/compact_runtime_test.go b/server/world/chunk/compact_runtime_test.go deleted file mode 100644 index 78ac722a1f..0000000000 --- a/server/world/chunk/compact_runtime_test.go +++ /dev/null @@ -1,177 +0,0 @@ -package chunk - -import ( - "testing" - - "github.com/df-mc/dragonfly/server/block/cube" -) - -func TestPalettedStorageCompactForRuntimeCacheCollapsesSingleValueStorage(t *testing.T) { - storage := newPalettedStorage(make([]uint32, paletteSize(1).uint32s()), newPalette(1, []uint32{42})) - - storage.compactForRuntimeCache() - - if storage.bitsPerIndex != 0 { - t.Fatalf("bitsPerIndex = %d, want 0", storage.bitsPerIndex) - } - if storage.indices != nil { - t.Fatalf("indices = %v, want nil", storage.indices) - } - if storage.indicesStart != nil { - t.Fatalf("indicesStart = %v, want nil", storage.indicesStart) - } - if got := storage.At(3, 4, 5); got != 42 { - t.Fatalf("At() = %d, want retained single palette value 42", got) - } -} - -func TestSubChunkCompactForRuntimeCacheDropsSingleValueAirStorage(t *testing.T) { - sub := NewSubChunk(7) - sub.storages = []*PalettedStorage{ - newPalettedStorage(make([]uint32, paletteSize(1).uint32s()), newPalette(1, []uint32{7})), - } - - sub.compactForRuntimeCache() - - if len(sub.storages) != 0 { - t.Fatalf("len(storages) = %d, want 0", len(sub.storages)) - } -} - -func TestSubChunkCompactForRuntimeCacheDropsZeroIndexedAirStorage(t *testing.T) { - sub := NewSubChunk(7) - sub.storages = []*PalettedStorage{ - newPalettedStorage(make([]uint32, paletteSize(4).uint32s()), newPalette(4, []uint32{7, 11, 22})), - } - - sub.compactForRuntimeCache() - - if len(sub.storages) != 0 { - t.Fatalf("len(storages) = %d, want 0", len(sub.storages)) - } -} - -func TestPalettedStorageCompactForRuntimeCacheCollapsesZeroIndexedStorage(t *testing.T) { - storage := newPalettedStorage(make([]uint32, paletteSize(4).uint32s()), newPalette(4, []uint32{42, 11, 22})) - - storage.compactForRuntimeCache() - - if storage.bitsPerIndex != 0 { - t.Fatalf("bitsPerIndex = %d, want 0", storage.bitsPerIndex) - } - if storage.indices != nil { - t.Fatalf("indices = %v, want nil", storage.indices) - } - if got := storage.palette.Len(); got != 1 { - t.Fatalf("palette len = %d, want 1", got) - } - if got := storage.At(3, 4, 5); got != 42 { - t.Fatalf("At() = %d, want retained palette index 0 value 42", got) - } -} - -func TestPalettedStorageCompactForRuntimeCacheCollapsesUniformNonZeroIndexStorage(t *testing.T) { - storage := newPalettedStorage(make([]uint32, paletteSize(4).uint32s()), newPalette(4, []uint32{11, 42, 22})) - fillStorage(storage, 42) - - storage.compactForRuntimeCache() - - if storage.bitsPerIndex != 0 { - t.Fatalf("bitsPerIndex = %d, want 0", storage.bitsPerIndex) - } - if got := storage.palette.Len(); got != 1 { - t.Fatalf("palette len = %d, want 1", got) - } - if got := storage.At(3, 4, 5); got != 42 { - t.Fatalf("At() = %d, want retained uniform value 42", got) - } -} - -func TestSubChunkCompactForRuntimeCacheDropsUniformNonZeroIndexAirStorage(t *testing.T) { - sub := NewSubChunk(7) - storage := newPalettedStorage(make([]uint32, paletteSize(4).uint32s()), newPalette(4, []uint32{11, 7, 22})) - fillStorage(storage, 7) - sub.storages = []*PalettedStorage{storage} - - sub.compactForRuntimeCache() - - if len(sub.storages) != 0 { - t.Fatalf("len(storages) = %d, want 0", len(sub.storages)) - } -} - -func TestChunkCompactForRuntimeCacheDoesNotRepackMultiValueStorage(t *testing.T) { - c := New(testBlockRegistry{air: 0}, testRange()) - storage := newPalettedStorage(make([]uint32, paletteSize(1).uint32s()), newPalette(1, []uint32{1, 2})) - storage.Set(1, 2, 3, 2) - c.sub[0].storages = []*PalettedStorage{storage} - indices := &storage.indices[0] - paletteValues := len(storage.palette.values) - - c.CompactForRuntimeCache() - - if c.sub[0].storages[0] != storage { - t.Fatal("multi-value storage pointer changed; cheap compaction should not repack it") - } - if &storage.indices[0] != indices { - t.Fatal("multi-value storage indices were replaced; cheap compaction should not scan/rewrite them") - } - if got := len(storage.palette.values); got != paletteValues { - t.Fatalf("palette len = %d, want %d", got, paletteValues) - } -} - -func TestPalettedStorageCompactForRuntimeCacheShrinksOversizedMultiValueStorage(t *testing.T) { - storage := newPalettedStorage(make([]uint32, paletteSize(4).uint32s()), newPalette(4, []uint32{11, 22})) - storage.Set(1, 2, 3, 22) - - storage.compactForRuntimeCache() - - if got, want := storage.bitsPerIndex, uint16(1); got != want { - t.Fatalf("bitsPerIndex = %d, want %d", got, want) - } - if got, want := len(storage.indices), paletteSize(1).uint32s(); got != want { - t.Fatalf("len(indices) = %d, want %d", got, want) - } - if got := len(storage.palette.values); got != 2 { - t.Fatalf("palette len = %d, want 2", got) - } - if got := storage.At(1, 2, 3); got != 22 { - t.Fatalf("At(1,2,3) = %d, want 22", got) - } -} - -type testBlockRegistry struct { - air uint32 -} - -func (r testBlockRegistry) BlockCount() int { return 3 } -func (r testBlockRegistry) AirRuntimeID() uint32 { - return r.air -} -func (testBlockRegistry) RuntimeIDToState(runtimeID uint32) (string, map[string]any, bool) { - return "test:block", nil, true -} -func (testBlockRegistry) StateToRuntimeID(string, map[string]any) (uint32, bool) { return 0, true } -func (testBlockRegistry) FilteringBlock(uint32) uint8 { return 0 } -func (testBlockRegistry) LightBlock(uint32) uint8 { return 0 } -func (testBlockRegistry) RandomTickBlock(uint32) bool { return false } -func (testBlockRegistry) NBTBlock(uint32) bool { return false } -func (testBlockRegistry) LiquidDisplacingBlock(uint32) bool { return false } -func (testBlockRegistry) LiquidBlock(uint32) bool { return false } -func (testBlockRegistry) HashToRuntimeID(hash uint32) (uint32, bool) { return hash, true } -func (testBlockRegistry) RuntimeIDToHash(runtimeID uint32) (uint32, bool) { return runtimeID, true } - -func testRange() cube.Range { - return cube.Range{0, 15} -} - -func fillStorage(storage *PalettedStorage, runtimeID uint32) { - for x := byte(0); x < 16; x++ { - for y := byte(0); y < 16; y++ { - for z := byte(0); z < 16; z++ { - storage.Set(x, y, z, runtimeID) - } - } - } -}