Fix terrain streaming and surface spawning
This commit is contained in:
parent
f0279cdb65
commit
2b07d6be20
17 changed files with 424 additions and 111 deletions
|
|
@ -40,7 +40,7 @@ type Cache struct {
|
|||
maxChunks int // LRU capacity; 0 = unbounded
|
||||
|
||||
mu sync.Mutex
|
||||
lightMu sync.Mutex
|
||||
lightMu sync.RWMutex
|
||||
chunks map[[2]int32]*Chunk
|
||||
frames map[[2]int32][]byte
|
||||
dirty map[[2]int32]uint64
|
||||
|
|
@ -248,6 +248,22 @@ func (c *Cache) FrameErrContext(ctx context.Context, cx, cz int32) ([]byte, erro
|
|||
return c.frameErr(cx, cz)
|
||||
}
|
||||
|
||||
// PreloadErrContext loads or generates a chunk without calculating lighting or
|
||||
// encoding a network frame. Streamers use it for predictive terrain work so a
|
||||
// prefetch ring does not recursively expand through lighting neighborhoods.
|
||||
func (c *Cache) PreloadErrContext(ctx context.Context, cx, cz int32) error {
|
||||
select {
|
||||
case c.frameSlots <- struct{}{}:
|
||||
defer func() { <-c.frameSlots }()
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
release := c.beginUse([2]int32{cx, cz})
|
||||
defer release()
|
||||
_, err := c.chunkAtErr(cx, cz)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Cache) frameErr(cx, cz int32) ([]byte, error) {
|
||||
key := [2]int32{cx, cz}
|
||||
|
||||
|
|
@ -307,6 +323,33 @@ func (c *Cache) GetBlock(x, y, z int) uint16 {
|
|||
return ch.GetBlock(x, y, z)
|
||||
}
|
||||
|
||||
// SafeSpawnY returns a feet-level Y with a supporting floor and two air blocks
|
||||
// above it. Water columns and decorative plants are skipped rather than
|
||||
// spawning an entity inside them. Loading happens once for the whole column.
|
||||
func (c *Cache) SafeSpawnY(x, z int) (int, bool) {
|
||||
cx := int32(x >> 4)
|
||||
cz := int32(z >> 4)
|
||||
release := c.beginUse([2]int32{cx, cz})
|
||||
defer release()
|
||||
ch, err := c.chunkAtErr(cx, cz)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
ch.mu.RLock()
|
||||
defer ch.mu.RUnlock()
|
||||
for y := MinY + WorldHeight - 3; y >= MinY; y-- {
|
||||
floor := ch.getBlock(x, y, z)
|
||||
if !supportsEntitySpawn(floor) {
|
||||
continue
|
||||
}
|
||||
if ch.getBlock(x, y+1, z) == StateAir && ch.getBlock(x, y+2, z) == StateAir {
|
||||
return y + 1, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// LightUpdate returns the standalone light_update body for a loaded chunk.
|
||||
func (c *Cache) LightUpdate(cx, cz int32) ([]byte, error) {
|
||||
release := c.beginUse([2]int32{cx, cz})
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import (
|
|||
// neighborhood. Light attenuates to zero within 15 blocks, so chunks outside
|
||||
// that neighborhood cannot affect the center chunk.
|
||||
func (c *Cache) ensureLight(chunk *Chunk) error {
|
||||
c.lightMu.Lock()
|
||||
defer c.lightMu.Unlock()
|
||||
c.lightMu.RLock()
|
||||
defer c.lightMu.RUnlock()
|
||||
return c.ensureLightLocked(chunk)
|
||||
}
|
||||
|
||||
|
|
@ -65,44 +65,57 @@ func (c *Cache) ensureLightLocked(chunk *Chunk) error {
|
|||
}
|
||||
}
|
||||
|
||||
// lightInputSnapshot returns blocks for a neighboring chunk without inserting
|
||||
// a cache miss into the LRU. This keeps lighting correct even for very small
|
||||
// cache limits and avoids an eight-chunk eviction cascade per frame.
|
||||
// lightInputSnapshot returns a stable neighbor snapshot. Cache misses are kept
|
||||
// in the LRU so adjacent frames reuse the same expensive terrain instead of
|
||||
// regenerating up to eight neighbors for every lighting calculation.
|
||||
func (c *Cache) lightInputSnapshot(cx, cz int32) (*Chunk, error) {
|
||||
key := [2]int32{cx, cz}
|
||||
c.mu.Lock()
|
||||
if chunk := c.chunks[key]; chunk != nil {
|
||||
c.touch(key)
|
||||
c.mu.Unlock()
|
||||
snapshot, _ := chunk.snapshot()
|
||||
return snapshot, nil
|
||||
}
|
||||
if pending := c.loads[key]; pending != nil {
|
||||
c.mu.Unlock()
|
||||
<-pending.done
|
||||
if pending.err != nil {
|
||||
return nil, pending.err
|
||||
}
|
||||
snapshot, _ := pending.ch.snapshot()
|
||||
return snapshot, nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
if c.store != nil {
|
||||
loaded, err := c.store.LoadChunk(cx, cz)
|
||||
if err == nil {
|
||||
snapshot, _ := loaded.snapshot()
|
||||
// A cache smaller than the required 3x3 neighborhood cannot retain these
|
||||
// inputs usefully. Keep misses detached to avoid evicting the requested
|
||||
// center chunk and churning the LRU on every frame.
|
||||
if c.maxChunks > 0 && c.maxChunks < 9 {
|
||||
c.mu.Lock()
|
||||
if chunk := c.chunks[key]; chunk != nil {
|
||||
c.touch(key)
|
||||
c.mu.Unlock()
|
||||
snapshot, _ := chunk.snapshot()
|
||||
return snapshot, nil
|
||||
}
|
||||
if !errors.Is(err, ErrChunkNotFound) {
|
||||
return nil, fmt.Errorf("world: load light neighbor (%d,%d): %w", cx, cz, err)
|
||||
if pending := c.loads[key]; pending != nil {
|
||||
c.mu.Unlock()
|
||||
<-pending.done
|
||||
if pending.err != nil {
|
||||
return nil, pending.err
|
||||
}
|
||||
snapshot, _ := pending.ch.snapshot()
|
||||
return snapshot, nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if c.store != nil {
|
||||
loaded, err := c.store.LoadChunk(cx, cz)
|
||||
if err == nil {
|
||||
snapshot, _ := loaded.snapshot()
|
||||
return snapshot, nil
|
||||
}
|
||||
if !errors.Is(err, ErrChunkNotFound) {
|
||||
return nil, fmt.Errorf("world: load light neighbor (%d,%d): %w", cx, cz, err)
|
||||
}
|
||||
}
|
||||
generated := c.gen(cx, cz)
|
||||
if generated == nil {
|
||||
return nil, fmt.Errorf("world: generator returned nil light neighbor (%d,%d)", cx, cz)
|
||||
}
|
||||
snapshot, _ := generated.snapshot()
|
||||
return snapshot, nil
|
||||
}
|
||||
generated := c.gen(cx, cz)
|
||||
if generated == nil {
|
||||
return nil, fmt.Errorf("world: generator returned nil light neighbor (%d,%d)", cx, cz)
|
||||
|
||||
release := c.beginUse(key)
|
||||
defer release()
|
||||
chunk, err := c.chunkAtErr(cx, cz)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("world: load light neighbor (%d,%d): %w", cx, cz, err)
|
||||
}
|
||||
snapshot, _ := generated.snapshot()
|
||||
snapshot, _ := chunk.snapshot()
|
||||
return snapshot, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
35
internal/world/cache_light_reuse_test.go
Normal file
35
internal/world/cache_light_reuse_test.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package world
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAdjacentFramesReuseGeneratedLightNeighbors(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
generated := make(map[[2]int32]int)
|
||||
cache := NewCache(-1, func(cx, cz int32) *Chunk {
|
||||
mu.Lock()
|
||||
generated[[2]int32{cx, cz}]++
|
||||
mu.Unlock()
|
||||
return NewChunk(cx, cz, BiomePlains)
|
||||
})
|
||||
|
||||
if _, err := cache.FrameErr(0, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := cache.FrameErr(1, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if len(generated) != 12 {
|
||||
t.Fatalf("generated chunks = %d, want 12 shared neighborhood chunks", len(generated))
|
||||
}
|
||||
for pos, count := range generated {
|
||||
if count != 1 {
|
||||
t.Fatalf("chunk %v generated %d times, want once", pos, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
41
internal/world/spawn_surface_test.go
Normal file
41
internal/world/spawn_surface_test.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package world
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSafeSpawnYUsesGeneratedSurface(t *testing.T) {
|
||||
cache := NewCache(-1, GenerateFlat)
|
||||
y, ok := cache.SafeSpawnY(8, 8)
|
||||
if !ok || y != FlatSurfaceY+1 {
|
||||
t.Fatalf("SafeSpawnY = %d, %v; want %d, true", y, ok, FlatSurfaceY+1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeSpawnYRejectsUnderwaterColumn(t *testing.T) {
|
||||
cache := NewCache(-1, func(cx, cz int32) *Chunk {
|
||||
chunk := NewChunk(cx, cz, BiomePlains)
|
||||
chunk.setBlockRaw(8, 60, 8, StateStone)
|
||||
for y := 61; y <= SeaLevel; y++ {
|
||||
chunk.setBlockRaw(8, y, 8, StateWater)
|
||||
}
|
||||
return chunk
|
||||
})
|
||||
if y, ok := cache.SafeSpawnY(8, 8); ok {
|
||||
t.Fatalf("SafeSpawnY = %d, true; want underwater column rejected", y)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeSpawnYAcceptsNonOpaqueSolidFloor(t *testing.T) {
|
||||
stairs := nameToStateID("minecraft:oak_stairs", nil)
|
||||
if stairs == StateAir {
|
||||
t.Fatal("oak stairs state is unavailable")
|
||||
}
|
||||
cache := NewCache(-1, func(cx, cz int32) *Chunk {
|
||||
chunk := NewChunk(cx, cz, BiomePlains)
|
||||
chunk.setBlockRaw(8, 70, 8, stairs)
|
||||
return chunk
|
||||
})
|
||||
y, ok := cache.SafeSpawnY(8, 8)
|
||||
if !ok || y != 71 {
|
||||
t.Fatalf("SafeSpawnY = %d, %v; want 71, true for stairs", y, ok)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package world
|
|||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"regionio/internal/nbt"
|
||||
|
|
@ -39,6 +40,41 @@ func stateByID(id uint16) (stateName, bool) {
|
|||
return s, ok
|
||||
}
|
||||
|
||||
// supportsEntitySpawn distinguishes collision floors from decorative blocks.
|
||||
// Light opacity is not sufficient here: stairs and slabs can have opacity zero
|
||||
// while still supporting an entity.
|
||||
func supportsEntitySpawn(id uint16) bool {
|
||||
if id == StateAir || id == StateWater {
|
||||
return false
|
||||
}
|
||||
if lightOpacity(id) > 0 {
|
||||
return true
|
||||
}
|
||||
state, ok := stateByID(id)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
name := state.Name
|
||||
for _, suffix := range []string{
|
||||
"_sapling", "_flower", "_tulip", "_mushroom", "_torch",
|
||||
"_rail", "_button", "_pressure_plate", "_carpet", "_banner",
|
||||
"_sign", "_hanging_sign",
|
||||
} {
|
||||
if strings.HasSuffix(name, suffix) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
switch name {
|
||||
case "minecraft:short_grass", "minecraft:tall_grass", "minecraft:fern",
|
||||
"minecraft:large_fern", "minecraft:dead_bush", "minecraft:dandelion",
|
||||
"minecraft:poppy", "minecraft:allium", "minecraft:azure_bluet",
|
||||
"minecraft:oxeye_daisy", "minecraft:cornflower",
|
||||
"minecraft:lily_of_the_valley", "minecraft:sunflower":
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func buildStateTable() {
|
||||
var blocks map[string]struct {
|
||||
States []struct {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue