Background predictive chunk streaming
Chunk generation and sending no longer block the read loop. The read loop pushes a non-blocking recenter request and stays free to handle movement, chat, and keep-alive acks immediately; a per-connection streamer goroutine generates chunks in a worker pool and sends them serially under the write mutex. - network/streamer.go: per-connection streamer. spiralOrder emits chunks centre-outward; parallelSend fans Cache.Frame across a worker pool (Cache.Frame is already goroutine-safe), parallelGenerate warms a one-ring predictive border so movement finds ready chunks; the loaded-set is owned solely by the streamer. - network/play.go: beginPlay launches the streamer and pushes an initial recenter instead of the old blocking streamAround; onPlayerMove now just calls requestRecenter (non-blocking). - network/handler.go: ctx (connection lifetime) + streamer field; the streamer stops when the read loop ends (cancel on serve exit). - network/configuration.go: client view_distance is saved (clamped 2..16) and drives the streamer radius. - Tests: spiral order (centre-first, ring structure) and the non-blocking recenter guarantee the read loop relies on.
This commit is contained in:
parent
d1cc29bb60
commit
d7c80a8858
5 changed files with 478 additions and 58 deletions
|
|
@ -18,13 +18,9 @@ const (
|
|||
spawnZ = 8.5
|
||||
)
|
||||
|
||||
// chunkRadius is how many chunks around the player we send (a square of side
|
||||
// 2*radius+1). Kept modest until streaming by view distance exists.
|
||||
const chunkRadius = 4
|
||||
|
||||
// beginPlay sends the join sequence once the client enters the Play phase and
|
||||
// starts the keep-alive loop. In milestone 4a no chunks are sent, so the client
|
||||
// reaches the "loading terrain" screen and waits.
|
||||
// hands chunk streaming off to the background streamer. The streamer stops when
|
||||
// h.ctx (the connection lifetime context) is cancelled.
|
||||
func (h *handler) beginPlay() error {
|
||||
for i := range h.hotbar {
|
||||
h.hotbar[i] = -1 // empty
|
||||
|
|
@ -40,56 +36,24 @@ func (h *handler) beginPlay() error {
|
|||
if err := h.sendPlayerPosition(1); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.streamAround(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
// Launch the background chunk streamer. It owns generation + sending so the
|
||||
// read loop stays free; requestRecenter is a non-blocking push.
|
||||
h.streamer = newStreamer(h.srv.Chunks(), h.conn, h.log, h.viewDistance)
|
||||
go h.streamer.run(h.ctx)
|
||||
h.streamer.requestRecenter(0, 0)
|
||||
go h.keepAliveLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
// streamAround recenters the client's chunk cache on (centerX, centerZ) and
|
||||
// sends the chunks newly in range. Chunks that fall out of range are dropped by
|
||||
// the client automatically once it receives the new cache center, so we only
|
||||
// send the difference and track the currently-loaded set.
|
||||
func (h *handler) streamAround(centerX, centerZ int32) error {
|
||||
cc := protocol.NewWriter(8)
|
||||
cc.VarInt(centerX)
|
||||
cc.VarInt(centerZ)
|
||||
if err := h.conn.SendWriter(protocol.PlayChunkCacheCenter, cc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cache := h.srv.Chunks()
|
||||
next := make(map[[2]int32]bool, (2*chunkRadius+1)*(2*chunkRadius+1))
|
||||
sent := 0
|
||||
for cx := centerX - chunkRadius; cx <= centerX+chunkRadius; cx++ {
|
||||
for cz := centerZ - chunkRadius; cz <= centerZ+chunkRadius; cz++ {
|
||||
key := [2]int32{cx, cz}
|
||||
next[key] = true
|
||||
if h.loaded[key] {
|
||||
continue
|
||||
}
|
||||
if err := h.conn.SendFramed(cache.Frame(cx, cz)); err != nil {
|
||||
return err
|
||||
}
|
||||
sent++
|
||||
}
|
||||
}
|
||||
h.loaded = next
|
||||
h.centerX, h.centerZ, h.hasCenter = centerX, centerZ, true
|
||||
h.log.Debug("streamed chunks", "center_x", centerX, "center_z", centerZ, "new", sent)
|
||||
return nil
|
||||
}
|
||||
|
||||
// onPlayerMove recenters chunk streaming when the player crosses into a new
|
||||
// chunk. X and Z are the player's block-precise coordinates.
|
||||
// onPlayerMove recenters the streamer when the player crosses into a new chunk.
|
||||
// It is a non-blocking push; the read loop never waits on generation.
|
||||
func (h *handler) onPlayerMove(x, z float64) error {
|
||||
cx := int32(int64(math.Floor(x)) >> 4)
|
||||
cz := int32(int64(math.Floor(z)) >> 4)
|
||||
if h.hasCenter && cx == h.centerX && cz == h.centerZ {
|
||||
return nil
|
||||
if h.streamer != nil {
|
||||
h.streamer.requestRecenter(cx, cz)
|
||||
}
|
||||
return h.streamAround(cx, cz)
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendPlayLogin writes the clientbound play "login" packet. Field layout was
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue