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
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue