Merge pull request #11697 from mvanhorn/fix/6763-memory-first-view-chunk-batch

🤖 Agent: the single-player memory connection sends every ready view column in one batch before PlayerLoaded (about 0.5 s saved at render distance 12), then the 64-per-tick cap comes back; socket guests keep vanilla pacing. (union of 74 heads on main: fmt, clippy -D warnings, tests green).
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-11-07 12:30:04 +01:00
commit f98c9b1f74
1 file changed
+119 -3
+119 -3
View File
@@ -2839,7 +2839,10 @@ impl Hub {
/// PlayerChunkSender.sendNextChunks' choice: ready chunks, closest first,
/// as many as the batch quota the client's acknowledgements pace allows,
/// with their places in the queue. What the view leaves of the quota goes
/// to the far ring, as far as its brake lets it reach.
/// to the far ring, as far as its brake lets it reach. A memory connection
/// that has not loaded yet sends every ready view column in that one
/// batch; the quota recorded for it covers them, so the tick after
/// `PlayerLoaded` is capped again.
fn next_batch<'w>(
&mut self,
i: usize,
@@ -2855,6 +2858,10 @@ impl Hub {
return None;
}
let budget = g.batch_quota.floor() as usize;
// The integrated server's own player, still on the loading screen.
// LAN guests and a client that has loaded keep the quota above.
let burst = g.address == super::conn::MEMORY_ADDRESS && !g.loaded;
let near_take = if burst { g.chunk_queue.len() } else { budget };
// ChunkStatus.LIGHT needs its neighbours at INITIALIZE_LIGHT: a
// column whose light layers would leave one out waits for it.
let ready = |c: &Chunk| {
@@ -2875,11 +2882,12 @@ impl Hub {
.enumerate()
.filter_map(|(idx, key)| Some((idx, &**world.chunks.get(key)?)))
.filter(|(_, c)| ready(c))
.take(budget)
.take(near_take)
.collect();
let mut far = Vec::new();
if let (Some(ring), Some((cx, cz))) = (self.far.as_mut(), g.center) {
let mut left = budget - near.len();
// What the view left of the vanilla quota. A first-view burst can take more than that.
let mut left = budget.saturating_sub(near.len());
for (idx, &(x, z)) in g.far_queue.iter().enumerate().take(super::far::LOOKS) {
if left == 0 {
break;
@@ -2900,6 +2908,11 @@ impl Hub {
far.push((idx, column));
}
}
// `update_chunks` subtracts the batch. Covering the near columns keeps that from going
// negative, so the next tick's `.min(chunks_per_tick)` cap holds again.
if burst {
g.batch_quota = g.batch_quota.max(near.len() as f32);
}
(!near.is_empty() || !far.is_empty()).then_some(Batch { near, far })
}
@@ -6327,4 +6340,107 @@ mod tests {
}
}
}
/// A single-player memory connection sends every ready view column before
/// `PlayerLoaded`. After that, on a respawn, and for a socket guest, the
/// batch is `chunks_per_tick` again (64). An unlit column stays out, and
/// the gates that send nothing still do.
#[test]
fn memory_connection_first_view_sends_every_ready_column() {
let (mut hub, _rx) = hub_with_a_guest();
hub.ticks = 3;
{
let g = &mut hub.guests[0];
g.chunks_per_tick = 64.0;
g.batch_quota = 0.0;
g.address = crate::lan::conn::MEMORY_ADDRESS;
g.loaded = false;
}
// The unlit column first, then 80 lit ones (`floor_at`'s empty column, lit).
let mut world = World::new(horizon_world::registry::BlockStateId(0), 0);
let mut dark = crate::chunk::Chunk::empty(100, 100);
dark.lit = false;
world.chunks.insert((100, 100), std::sync::Arc::new(dark));
hub.guests[0].chunk_queue.push((100, 100));
for i in 0..80 {
let (x, z) = (i % 16, i / 16);
let mut chunk = crate::chunk::Chunk::empty(x, z);
chunk.lit = true;
world.chunks.insert((x, z), std::sync::Arc::new(chunk));
hub.guests[0].chunk_queue.push((x, z));
}
let batch = hub
.next_batch(0, &world, None)
.expect("the first view sends");
assert_eq!(
batch.near.len(),
80,
"every ready column leaves in one batch"
);
assert!(
batch.near.iter().all(|(_, c)| c.lit),
"an unlit column is not ready"
);
assert!(batch.far.is_empty());
assert!(hub.guests[0].batch_quota >= 80.0);
hub.guests[0].loaded = true;
let loaded = hub
.next_batch(0, &world, None)
.expect("a loaded client still receives chunks");
assert_eq!(loaded.near.len(), 64, "PlayerLoaded puts the cap back");
hub.guests[0].address = "127.0.0.1:25565".parse().unwrap();
hub.guests[0].loaded = false;
hub.guests[0].batch_quota = 0.0;
let socket = hub
.next_batch(0, &world, None)
.expect("a socket guest still receives chunks");
assert_eq!(socket.near.len(), 64, "a socket guest keeps the quota");
hub.guests[0].address = crate::lan::conn::MEMORY_ADDRESS;
hub.guests[0].loaded = true;
hub.guests[0].loading = true;
hub.guests[0].batch_quota = 0.0;
let respawn = hub
.next_batch(0, &world, None)
.expect("a respawned player still receives chunks");
assert_eq!(
respawn.near.len(),
64,
"a respawn stays on the vanilla pace"
);
// Seventy lit columns, the unlit one still queued: all seventy, in one batch.
hub.guests[0].loaded = false;
hub.guests[0].batch_quota = 0.0;
hub.guests[0].chunk_queue.truncate(71);
let seventy = hub
.next_batch(0, &world, None)
.expect("seventy ready columns send");
assert_eq!(seventy.near.len(), 70);
assert!(seventy.near.iter().all(|(_, c)| c.lit));
hub.guests[0].unacknowledged = hub.guests[0].max_unacknowledged;
assert!(
hub.next_batch(0, &world, None).is_none(),
"too many batches still waiting sends nothing"
);
hub.guests[0].unacknowledged = 0;
hub.ticks = hub.guests[0].placed_at + 1;
assert!(
hub.next_batch(0, &world, None).is_none(),
"the tick after placement still waits"
);
hub.ticks = 3;
hub.guests[0].chunk_queue.clear();
hub.guests[0].far_queue.clear();
assert!(
hub.next_batch(0, &world, None).is_none(),
"an empty queue sends nothing"
);
}
}