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:
Master290 2026-06-25 00:54:57 +03:00
parent d1cc29bb60
commit d7c80a8858
5 changed files with 478 additions and 58 deletions

View file

@ -1,6 +1,7 @@
package network
import (
"context"
"errors"
"io"
"log/slog"
@ -11,28 +12,38 @@ import (
)
// handler drives one connection through its state machine until it closes.
// It is owned by a single goroutine (the read loop), so the play fields below
// need no synchronization.
// It is owned by a single goroutine (the read loop); play-phase chunk streaming
// is delegated to a background streamer goroutine.
type handler struct {
conn *Conn
srv *server.Server
log *slog.Logger
// Play-phase chunk streaming state.
loaded map[[2]int32]bool // chunks currently sent to the client
centerX int32
centerZ int32
hasCenter bool
// ctx is the connection lifetime context, set in serve(). It cancels when
// the read loop ends, stopping the streamer and any derived work.
ctx context.Context
// Background chunk streamer (Play phase). requestRecenter pushes here.
streamer *streamer
// viewDistance is the client's requested view distance (from
// client_information), clamped; used to size the streamer.
viewDistance int
// Creative inventory state for block placement.
heldSlot int32 // selected hotbar index (0-8)
hotbar [9]int32 // item network IDs per hotbar slot (-1 = empty)
}
// serve runs the read/dispatch loop for a single connection.
// serve runs the read/dispatch loop for a single connection. It owns the
// streamer's lifecycle: the streamer context is cancelled when this loop exits,
// stopping generation and sending so no goroutine outlives the connection.
func (h *handler) serve() {
defer h.conn.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // stops the streamer when the read loop ends
h.ctx = ctx
for {
pkt, err := h.conn.ReadPacket()
if err != nil {
@ -49,7 +60,8 @@ func (h *handler) serve() {
}
}
// dispatch routes a packet to the handler for the current state.
// dispatch routes a packet to the handler for the current state. The Play
// handler reads h.ctx (the connection lifetime context) to launch the streamer.
func (h *handler) dispatch(pkt protocol.Packet) error {
switch h.conn.State() {
case protocol.StateHandshaking: