diff --git a/server/world/chunk/block_network_hash.go b/server/world/chunk/block_network_hash.go new file mode 100644 index 0000000000..8b5a4e8af7 --- /dev/null +++ b/server/world/chunk/block_network_hash.go @@ -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 + }) + } +} 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/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 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() {