RegionIO/internal/world/cache_light.go
2026-07-21 11:11:19 +03:00

194 lines
4.9 KiB
Go

package world
import (
"errors"
"fmt"
"sort"
)
// ensureLight calculates an exact center-chunk solution from a 3x3 block
// 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.RLock()
defer c.lightMu.RUnlock()
return c.ensureLightLocked(chunk)
}
func (c *Cache) ensureLightLocked(chunk *Chunk) error {
for {
center, revision := chunk.snapshot()
if center.lightReady && center.lightValidated {
return nil
}
chunks := make(map[[2]int32]*Chunk, 9)
for dz := int32(-1); dz <= 1; dz++ {
for dx := int32(-1); dx <= 1; dx++ {
key := [2]int32{chunk.X + dx, chunk.Z + dz}
if dx == 0 && dz == 0 {
chunks[key] = center
continue
}
snapshot, err := c.lightInputSnapshot(key[0], key[1])
if err != nil {
return err
}
chunks[key] = snapshot
}
}
volume := newLightVolume(int(chunk.X-1), int(chunk.Z-1), 3, 3, chunks)
clear(volume.sky)
clear(volume.block)
volume.calculate()
chunk.mu.Lock()
if chunk.revision.Load() != revision {
chunk.mu.Unlock()
continue
}
changed := chunk.installLight(volume)
if center.lightReady && changed {
revision = chunk.revision.Add(1)
}
chunk.mu.Unlock()
c.mu.Lock()
key := [2]int32{chunk.X, chunk.Z}
delete(c.frames, key)
if c.store != nil && center.lightReady && changed {
c.dirty[key] = revision
}
c.mu.Unlock()
return nil
}
}
// 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}
// 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 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
}
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, _ := chunk.snapshot()
return snapshot, nil
}
func (c *Cache) cachedLightNeighborhood(cx, cz int32) map[[2]int32]*Chunk {
c.mu.Lock()
defer c.mu.Unlock()
chunks := make(map[[2]int32]*Chunk, 9)
for dz := int32(-1); dz <= 1; dz++ {
for dx := int32(-1); dx <= 1; dx++ {
key := [2]int32{cx + dx, cz + dz}
if chunk := c.chunks[key]; chunk != nil {
chunks[key] = chunk
c.touch(key)
}
}
}
return chunks
}
func (c *Cache) updateLightAfterBlockLocked(x, y, z int, live map[[2]int32]*Chunk) ([]ChunkPos, error) {
cx, cz := int32(x>>4), int32(z>>4)
inputs := make(map[[2]int32]*Chunk, 9)
for dz := int32(-1); dz <= 1; dz++ {
for dx := int32(-1); dx <= 1; dx++ {
key := [2]int32{cx + dx, cz + dz}
if chunk := live[key]; chunk != nil {
snapshot, _ := chunk.snapshot()
inputs[key] = snapshot
continue
}
snapshot, err := c.lightInputSnapshot(key[0], key[1])
if err != nil {
return nil, err
}
inputs[key] = snapshot
}
}
volume := newLightVolume(int(cx-1), int(cz-1), 3, 3, inputs)
volume.relaxBlockChange(x-volume.minX, y, z-volume.minZ)
keys := make([][2]int32, 0, len(live))
for key := range live {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i][0] != keys[j][0] {
return keys[i][0] < keys[j][0]
}
return keys[i][1] < keys[j][1]
})
changed := make([]ChunkPos, 0, len(keys))
for _, key := range keys {
chunk := live[key]
chunk.mu.Lock()
didChange := chunk.installLight(volume)
if didChange {
chunk.revision.Add(1)
changed = append(changed, ChunkPos{X: key[0], Z: key[1]})
}
revision := chunk.revision.Load()
chunk.mu.Unlock()
if didChange {
c.mu.Lock()
delete(c.frames, key)
if c.store != nil {
c.dirty[key] = revision
}
c.touch(key)
c.mu.Unlock()
}
}
return changed, nil
}