Publish generated chunk batches

This commit is contained in:
Daniar Mannanov 2026-08-18 00:58:12 +03:00
parent cdee55b613
commit 3f0f1f70b9
2 changed files with 144 additions and 3 deletions

View file

@ -16,6 +16,11 @@ import (
// Generator produces the chunk at the given coordinate.
type Generator func(cx, cz int32) *Chunk
// BatchGenerator builds the requested chunk and may return neighboring base
// terrain chunks in the same operation. Free coordinates are published
// together; coordinates with an active load retain that load's ownership.
type BatchGenerator func(cx, cz int32) (map[[2]int32]*Chunk, error)
// Cache is the live world: it owns the mutable chunk data and memoizes the
// framed, compression-ready level_chunk packet for each chunk. A block edit
// mutates the chunk and invalidates its cached frame so the next request
@ -36,6 +41,7 @@ type Generator func(cx, cz int32) *Chunk
type Cache struct {
threshold int32
gen Generator
batchGen BatchGenerator
store *Store // nil = in-memory only (tests, flat worlds)
maxChunks int // LRU capacity; 0 = unbounded
@ -55,6 +61,14 @@ type Cache struct {
loads map[[2]int32]*chunkLoad
}
// SetBatchGenerator enables optional batch generation. It must be called before
// the cache is used; existing single-chunk generators remain the default.
func (c *Cache) SetBatchGenerator(gen BatchGenerator) {
c.mu.Lock()
c.batchGen = gen
c.mu.Unlock()
}
// chunkLoad coordinates concurrent misses for the same coordinate. The first
// caller performs disk I/O or generation; all others wait for that exact result.
type chunkLoad struct {
@ -192,6 +206,7 @@ func (c *Cache) chunkAtErr(cx, cz int32) (*Chunk, error) {
// Try disk before generation so saved edits survive restarts.
var ch *Chunk
var loadErr error
var generatedBatch map[[2]int32]*Chunk
if c.store != nil {
if loaded, err := c.store.LoadChunk(cx, cz); err == nil {
ch = loaded
@ -200,14 +215,46 @@ func (c *Cache) chunkAtErr(cx, cz int32) (*Chunk, error) {
}
}
if ch == nil && loadErr == nil {
ch = c.gen(cx, cz) // generate outside the lock
if ch == nil {
loadErr = fmt.Errorf("world: generator returned nil chunk (%d,%d)", cx, cz)
c.mu.Lock()
batchGen := c.batchGen
c.mu.Unlock()
if batchGen != nil {
batch, err := batchGen(cx, cz)
if err != nil {
loadErr = err
} else {
ch = batch[key]
if ch == nil {
loadErr = fmt.Errorf("world: batch generator omitted chunk (%d,%d)", cx, cz)
} else if ch.X != cx || ch.Z != cz {
loadErr = fmt.Errorf("world: batch generator returned chunk (%d,%d) for target (%d,%d)", ch.X, ch.Z, cx, cz)
}
if loadErr == nil {
generatedBatch, loadErr = c.reconcileBatchStore(key, batch)
}
}
} else {
ch = c.gen(cx, cz) // generate outside the lock
if ch == nil {
loadErr = fmt.Errorf("world: generator returned nil chunk (%d,%d)", cx, cz)
}
}
}
c.mu.Lock()
if loadErr == nil {
for batchKey, batchChunk := range generatedBatch {
if batchKey == key || batchChunk == nil || batchChunk.X != batchKey[0] || batchChunk.Z != batchKey[1] {
continue
}
if _, loading := c.loads[batchKey]; loading {
continue
}
if _, exists := c.chunks[batchKey]; !exists {
c.chunks[batchKey] = batchChunk
c.touch(batchKey)
}
}
c.chunks[key] = ch
c.touch(key)
c.evictIfNeeded()
@ -219,6 +266,24 @@ func (c *Cache) chunkAtErr(cx, cz int32) (*Chunk, error) {
return ch, loadErr
}
func (c *Cache) reconcileBatchStore(target [2]int32, batch map[[2]int32]*Chunk) (map[[2]int32]*Chunk, error) {
if c.store == nil {
return batch, nil
}
for key, chunk := range batch {
if key == target || chunk == nil || chunk.X != key[0] || chunk.Z != key[1] {
continue
}
loaded, err := c.store.LoadChunk(key[0], key[1])
if err == nil {
batch[key] = loaded
} else if !errors.Is(err, ErrChunkNotFound) {
return nil, fmt.Errorf("world: load batch neighbor (%d,%d): %w", key[0], key[1], err)
}
}
return batch, nil
}
// Frame returns the prebuilt level_chunk packet for (cx, cz), building it on
// first request and caching until the chunk is edited. The slice must not be
// mutated.