Skip to content
Open
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
1 change: 1 addition & 0 deletions server/world/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ func (conf Config) New() *World {
entities: make(map[*EntityHandle]ChunkPos),
viewers: make(map[*Loader]Viewer),
chunks: make(map[ChunkPos]*Column),
chunkRequests: make(map[ChunkPos]*chunkRequest),
queueClosing: make(chan struct{}),
closing: make(chan struct{}),
queue: make(chan transaction, 128),
Expand Down
57 changes: 57 additions & 0 deletions server/world/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,60 @@ func (NopGenerator) GenerateChunk(ChunkPos, *chunk.Chunk) {}

// DefaultSpawn ...
func (NopGenerator) DefaultSpawn(Dimension) cube.Pos { return cube.Pos{} }

// chunkRequest ...
type chunkRequest struct {
pos ChunkPos
callbacks []chunkCallback
generating bool

close chan struct{}
col *chunk.Column
result *Column
}

// Do adds callback to list of all callbacks.
func (r *chunkRequest) Do(tx *Tx, receiver chunkCallback) {
r.callbacks = append(r.callbacks, receiver)
if !r.generating {
r.generating = true
w := tx.World()
go r.load(w)
}
}

// doImmediate waits till chunk is loaded and returns it.
func (r *chunkRequest) doImmediate(tx *Tx) *Column {
<-r.close
r.signal(tx)
return r.result
}

// load loads chunk or generates it.
func (r *chunkRequest) load(w *World) {
r.col = w.loadChunk(r.pos)
close(r.close)
select {
case <-w.closing:
return
default:
w.Exec(r.signal)
}
}
Comment thread
HashimTheArab marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// signal calls all callbacks and adds chunk to the world.
func (r *chunkRequest) signal(tx *Tx) {
if r.result != nil {
return
}
w := tx.World()
pos := r.pos

delete(w.chunkRequests, pos)
r.result = w.addChunk(pos, r.col)
for _, recv := range r.callbacks {
recv(tx, r.result)
}
}

type chunkCallback = func(tx *Tx, chunk *Column)
27 changes: 19 additions & 8 deletions server/world/loader.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package world

import (
"github.com/go-gl/mathgl/mgl64"
"maps"
"math"
"sync"

"github.com/go-gl/mathgl/mgl64"
)

// Loader implements the loading of the world. A loader can typically be moved around the world to load
Expand Down Expand Up @@ -98,21 +99,31 @@ func (l *Loader) Load(tx *Tx, n int) {
if len(l.loadQueue) == 0 {
break
}

pos := l.loadQueue[0]
c := tx.w.chunk(pos)

l.viewer.ViewChunk(pos, l.w.Dimension(), c.BlockEntities, c.Chunk)
l.w.addViewer(tx, c, l)

l.loaded[pos] = c
w := tx.World()
w.loadChunkAsync(tx, pos, func(tx2 *Tx, chunk *Column) {
if tx == tx2 || l.World() == tx2.World() {
l.viewChunk(tx2, pos, chunk)
}
})
Comment thread
HashimTheArab marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Shift the first element from the load queue off so that we can take a new one during the next
// iteration.
l.loadQueue = l.loadQueue[1:]
}
}

// viewChunk adds chunk to the Viewer.
func (l *Loader) viewChunk(tx *Tx, pos ChunkPos, c *Column) {
if l.viewer == nil {
return
}
l.viewer.ViewChunk(pos, l.w.Dimension(), c.BlockEntities, c.Chunk)
l.w.addViewer(tx, c, l)

l.loaded[pos] = c
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Async callback writes to map without mutex

High Severity

viewChunk writes to l.loaded (a map) without holding l.mu, but Chunk() reads l.loaded under l.mu.RLock(). Previously, the equivalent write happened inside Load which held l.mu.Lock(). Now, when viewChunk is called as an async callback from signal in a later transaction, l.mu is not held. In Go, concurrent map read and write causes a fatal runtime panic. Since Chunk() is a public method with explicit locking for concurrent access, any concurrent call to Chunk() while viewChunk is executing will crash the program.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8b4d0de. Configure here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

map is protected by transaction

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

the finding is still valid pls ask ai


// Chunk attempts to return a chunk at the given ChunkPos. If the chunk is not loaded, the second return value will
// be false.
func (l *Loader) Chunk(pos ChunkPos) (*Column, bool) {
Expand Down
2 changes: 2 additions & 0 deletions server/world/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ type normalTransaction struct {
// ntx.c.
func (ntx normalTransaction) Run(w *World) {
tx := &Tx{w: w}
w.currentTx = tx
ntx.f(tx)
tx.close()
close(ntx.c)
Expand All @@ -323,6 +324,7 @@ func (wtx weakTransaction) Run(w *World) {
valid := !wtx.invalid.Load()
if valid {
tx := &Tx{w: w}
w.currentTx = tx
wtx.f(tx)
tx.close()
}
Expand Down
96 changes: 67 additions & 29 deletions server/world/world.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ type World struct {

// chunks holds a cache of chunks currently loaded. These chunks are cleared
// from this map after some time of not being used.
chunks map[ChunkPos]*Column
chunks map[ChunkPos]*Column
chunkRequests map[ChunkPos]*chunkRequest

// entities holds a map of entities currently loaded and the last ChunkPos
// that the Entity was in. These are tracked so that a call to RemoveEntity
Expand All @@ -68,6 +69,8 @@ type World struct {

viewerMu sync.Mutex
viewers map[*Loader]Viewer

currentTx *Tx
}

// transaction is a type that may be added to the transaction queue of a World.
Expand Down Expand Up @@ -568,7 +571,11 @@ func (w *World) light(pos cube.Pos) uint8 {
// Above the rest of the world, so full skylight.
return 15
}
return w.chunk(chunkPosFromBlockPos(pos)).Light(uint8(pos[0]), int16(pos[1]), uint8(pos[2]))
c, ok := w.loadedChunk(chunkPosFromBlockPos(pos))
if ok {
return c.Light(uint8(pos[0]), int16(pos[1]), uint8(pos[2]))
}
return 0
}

// skyLight returns the skylight level at the position passed. This light level
Expand Down Expand Up @@ -1174,6 +1181,15 @@ func showEntity(e Entity, viewer Viewer) {
viewer.ViewEntityArmour(e)
}

// loadedChunk returns chunk & true only if chunk at position passed is loaded.
func (w *World) loadedChunk(pos ChunkPos) (*Column, bool) {
c, ok := w.chunks[pos]
if ok {
return c, true
}
return nil, false
}

// chunk reads a chunk from the position passed. If a chunk at that position is
// not yet loaded, the chunk is loaded from the provider, or generated if it
// did not yet exist. Additionally, chunks newly loaded have the light in them
Expand All @@ -1183,41 +1199,68 @@ func (w *World) chunk(pos ChunkPos) *Column {
if ok {
return c
}
c, err := w.loadChunk(pos)
chunk.LightArea([]*chunk.Chunk{c.Chunk}, int(pos[0]), int(pos[1])).Fill()
if err != nil {
w.conf.Log.Error("load chunk: "+err.Error(), "X", pos[0], "Z", pos[1])
c, ok = w.chunkFromAsyncPool(w.currentTx, pos)
if ok {
return c
}
w.calculateLight(pos)
return c
return w.addChunk(pos, w.loadChunk(pos))
}

// loadChunk attempts to load a chunk from the provider, or generates a chunk
// if one doesn't currently exist.
func (w *World) loadChunk(pos ChunkPos) (*Column, error) {
func (w *World) loadChunk(pos ChunkPos) *chunk.Column {
column, err := w.conf.Provider.LoadColumn(pos, w.conf.Dim)
switch {
case err == nil:
col := w.columnFrom(column, pos)
w.chunks[pos] = col
for _, e := range col.Entities {
w.entities[e] = pos
e.w = w
}
return col, nil
case errors.Is(err, leveldb.ErrNotFound):
// The provider doesn't have a chunk saved at this position, so we generate a new one.
col := newColumn(chunk.New(w.conf.Blocks, w.Range()))
w.chunks[pos] = col

w.conf.Generator.GenerateChunk(pos, col.Chunk)
return col, nil
return column
default:
return newColumn(chunk.New(w.conf.Blocks, w.Range())), err
if !errors.Is(err, leveldb.ErrNotFound) {
w.conf.Log.Error("load chunk: "+err.Error(), "X", pos[0], "Z", pos[1])
}
ch := chunk.New(w.conf.Blocks, w.Range())
w.conf.Generator.GenerateChunk(pos, ch)
return &chunk.Column{Chunk: ch}
}
Comment thread
HashimTheArab marked this conversation as resolved.
}

// loadChunkAsync ...
func (w *World) loadChunkAsync(tx *Tx, pos ChunkPos, callback chunkCallback) {
if c, ok := w.chunks[pos]; ok {
callback(tx, c)
return
}
req, ok := w.chunkRequests[pos]
if ok {
req.Do(w.currentTx, callback)
return
}
req = &chunkRequest{pos: pos, close: make(chan struct{})}
req.Do(w.currentTx, callback)
w.chunkRequests[pos] = req
}

// addChunk ...
func (w *World) addChunk(pos ChunkPos, c *chunk.Column) *Column {
column := w.columnFrom(c, pos)
w.chunks[pos] = column
for _, e := range column.Entities {
w.entities[e] = pos
e.w = w
}
chunk.LightArea([]*chunk.Chunk{column.Chunk}, int(pos[0]), int(pos[1])).Fill()
w.calculateLight(pos)
return column
}

// chunkFromAsyncPool ...
func (w *World) chunkFromAsyncPool(tx *Tx, pos ChunkPos) (*Column, bool) {
req, ok := w.chunkRequests[pos]
if ok {
return req.doImmediate(tx), true
}
return nil, false
}

// calculateLight calculates the light in the chunk passed and spreads the
// light of any surrounding neighbours if they have all chunks loaded around it
// as a result of the one passed.
Expand Down Expand Up @@ -1300,11 +1343,6 @@ type Column struct {
loaders []*Loader
}

// newColumn returns a new Column wrapper around the chunk.Chunk passed.
func newColumn(c *chunk.Chunk) *Column {
return &Column{Chunk: c, BlockEntities: map[cube.Pos]Block{}}
}

// columnTo converts a Column to a chunk.Column so that it can be written to
// a provider.
func (w *World) columnTo(col *Column, pos ChunkPos) *chunk.Column {
Expand Down