Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions server/world/chunk/block_network_hash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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() {
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)
}

// 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
networkChunk.sub = slices.Clone(c.sub)
networkChunk.sub[index] = c.sub[index].Clone()
networkChunk.sub[index].convertRuntimeIDsToBlockNetworkHashes(c.br)
return EncodeSubChunk(&networkChunk, NetworkEncoding, index)
}

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
})
}
}
8 changes: 8 additions & 0 deletions server/world/chunk/chunk.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
2 changes: 1 addition & 1 deletion server/world/chunk/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
63 changes: 63 additions & 0 deletions server/world/chunk/paletted_storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Comment on lines +169 to +188

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd palette.go --exact-path server/world/chunk
rg -n 'size' server/world/chunk/palette.go -B2 -A2
rg -n 'func.*Add' server/world/chunk/palette.go -A 20
rg -n '\.resize\(' server/world/chunk -B3 -A3
rg -n 'func newPalettedStorage' server/world/chunk/paletted_storage.go -A 15

Repository: oomph-ac/dragonfly

Length of output: 5286


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== paletted_storage.go relevant ranges =="
sed -n '1,220p' server/world/chunk/paletted_storage.go | cat -n | sed -n '1,220p'

echo
echo "== palette.go relevant ranges =="
sed -n '1,180p' server/world/chunk/palette.go | cat -n | sed -n '1,180p'

Repository: oomph-ac/dragonfly

Length of output: 16135


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== resize/addNew/compactForRuntimeCache references =="
rg -n 'compactForRuntimeCache|func \(storage \*PalettedStorage\) resize|func \(storage \*PalettedStorage\) addNew|storage\.palette\.size|needsResize\(\)' server/world/chunk -A 4 -B 4

Repository: oomph-ac/dragonfly

Length of output: 7739


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n 'compactForRuntimeCache\(' server/world/chunk -A 3 -B 3
rg -n '\.resize\(size\)|\.resize\(storage\.palette\.size\)|increaseSize\(\)' server/world/chunk -A 2 -B 2
rg -n 'type PalettedStorage|func \(storage \*PalettedStorage\) resize' server/world/chunk/paletted_storage.go -A 80 -B 10

Repository: oomph-ac/dragonfly

Length of output: 11297


Sync Palette.size when shrinking storage. compactForRuntimeCache() calls resize(size) without updating storage.palette.size, so the palette can keep a wider capacity than the packed indices. A later Palette.Add may then skip the grow resize and write an index that no longer fits indexMask, corrupting live chunk data. Update the palette size here or inside resize().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/world/chunk/paletted_storage.go` around lines 169 - 188, Update
compactForRuntimeCache and the resize path so shrinking packed storage also
synchronizes Palette.size with the new width. Ensure subsequent Palette.Add
operations detect when growth is needed and never write indices that exceed
indexMask.


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.
Expand Down
15 changes: 15 additions & 0 deletions server/world/chunk/sub_chunk.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading