a more robust system for syncing chunks

as long as the server tells you there are more chunks, there are definitely more chunks.
just wait for them a bit.
This commit is contained in:
liquidex 2024-08-17 22:16:31 +02:00
parent 2f7bcbb14e
commit 2594afcc1b
6 changed files with 57 additions and 49 deletions

View file

@ -1,4 +1,4 @@
use std::sync::Arc; use std::{collections::HashSet, sync::Arc};
use axum::{ use axum::{
extract::{ extract::{
@ -194,8 +194,11 @@ struct SessionLoop {
wall: Arc<Wall>, wall: Arc<Wall>,
chunk_encoder: Arc<ChunkEncoder>, chunk_encoder: Arc<ChunkEncoder>,
handle: SessionHandle, handle: SessionHandle,
render_commands_tx: mpsc::Sender<RenderCommand>, render_commands_tx: mpsc::Sender<RenderCommand>,
viewport_chunks: ChunkIterator, viewport_chunks: ChunkIterator,
sent_chunks: HashSet<ChunkPosition>,
} }
enum RenderCommand { enum RenderCommand {
@ -238,6 +241,7 @@ impl SessionLoop {
handle, handle,
render_commands_tx, render_commands_tx,
viewport_chunks: ChunkIterator::new(ChunkPosition::new(0, 0), ChunkPosition::new(0, 0)), viewport_chunks: ChunkIterator::new(ChunkPosition::new(0, 0), ChunkPosition::new(0, 0)),
sent_chunks: HashSet::new(),
}) })
} }
@ -316,10 +320,16 @@ impl SessionLoop {
let mut chunk_infos = vec![]; let mut chunk_infos = vec![];
let mut packet = vec![]; let mut packet = vec![];
// Number of chunks iterated is limited to 300 per packet, so as not to let the client // Number of chunks iterated is limited per packet, so as not to let the client
// stall the server by sending in a huge viewport. // stall the server by sending in a huge viewport.
for _ in 0..300 { let start = Instant::now();
let mut iterated = 0;
for i in 0..12000 {
if let Some(position) = self.viewport_chunks.next() { if let Some(position) = self.viewport_chunks.next() {
if self.sent_chunks.contains(&position) {
continue;
}
if let Some(encoded) = self.chunk_encoder.encoded(position).await { if let Some(encoded) = self.chunk_encoder.encoded(position).await {
let offset = packet.len(); let offset = packet.len();
packet.extend_from_slice(&encoded); packet.extend_from_slice(&encoded);
@ -335,25 +345,22 @@ impl SessionLoop {
// execute. We cap it to 256KiB in hopes that noone has Internet slow enough for // execute. We cap it to 256KiB in hopes that noone has Internet slow enough for
// this to cause a disconnect. // this to cause a disconnect.
if packet.len() >= 256 * 1024 { if packet.len() >= 256 * 1024 {
iterated = i;
break; break;
} }
} else {
// Length 0 indicates the server acknowledged the chunk, but it has no
// image data.
// This is used by clients to know that the chunk doesn't need downloading.
chunk_infos.push(ChunkInfo {
position,
offset: 0,
length: 0,
});
} }
self.sent_chunks.insert(position);
} else { } else {
iterated = i;
break; break;
} }
} }
info!(elapsed = ?start.elapsed(), iterated, "send_chunks");
ws.send(to_message(&Notify::Chunks { ws.send(to_message(&Notify::Chunks {
chunks: chunk_infos, chunks: chunk_infos,
has_more: self.viewport_chunks.clone().next().is_some(),
})) }))
.await?; .await?;
ws.send(Message::Binary(packet)).await?; ws.send(Message::Binary(packet)).await?;

View file

@ -94,6 +94,12 @@ pub struct ChunkInfo {
rename_all_fields = "camelCase" rename_all_fields = "camelCase"
)] )]
pub enum Notify { pub enum Notify {
Wall { wall_event: wall::Event }, Wall {
Chunks { chunks: Vec<ChunkInfo> }, wall_event: wall::Event,
},
Chunks {
chunks: Vec<ChunkInfo>,
has_more: bool,
},
} }

View file

@ -17,13 +17,14 @@ export class BrushEditor extends HTMLElement {
this.textArea = this.appendChild(document.createElement("pre")); this.textArea = this.appendChild(document.createElement("pre"));
this.textArea.classList.add("text-area"); this.textArea.classList.add("text-area");
this.textArea.textContent = defaultBrush; this.textArea.textContent = localStorage.getItem("rkgk.brushEditor.code") ?? defaultBrush;
this.textArea.contentEditable = true; this.textArea.contentEditable = true;
this.textArea.spellcheck = false; this.textArea.spellcheck = false;
this.textArea.addEventListener("input", () => { this.textArea.addEventListener("input", () => {
localStorage.setItem("rkgk.brushEditor.code", this.code);
this.dispatchEvent( this.dispatchEvent(
Object.assign(new Event(".codeChanged"), { Object.assign(new Event(".codeChanged"), {
newCode: this.textArea.value, newCode: this.code,
}), }),
); );
}); });

View file

@ -87,8 +87,13 @@ class CanvasRenderer extends HTMLElement {
let chunk = this.wall.getChunk(chunkX, chunkY); let chunk = this.wall.getChunk(chunkX, chunkY);
if (chunk != null) { if (chunk != null) {
this.ctx.globalCompositeOperation = "source-over";
this.ctx.drawImage(chunk.canvas, x, y); this.ctx.drawImage(chunk.canvas, x, y);
} }
this.ctx.globalCompositeOperation = "difference";
this.ctx.fillStyle = "white";
this.ctx.fillText(`${chunkX}, ${chunkY}`, x, y + 12);
} }
} }

View file

@ -83,54 +83,42 @@ reticleRenderer.connectViewport(canvasRenderer.viewport);
} }
}); });
let pendingChunks = 0; let sendViewportUpdate = debounce(updateInterval, () => {
let chunkDownloadStates = new Map();
function sendViewportUpdate() {
let visibleRect = canvasRenderer.getVisibleChunkRect(); let visibleRect = canvasRenderer.getVisibleChunkRect();
session.sendViewport(visibleRect); session.sendViewport(visibleRect);
console.log("visibleRect", visibleRect);
for (let chunkY = visibleRect.top; chunkY < visibleRect.bottom; ++chunkY) { });
for (let chunkX = visibleRect.left; chunkX < visibleRect.right; ++chunkX) {
let key = Wall.chunkKey(chunkX, chunkY);
let currentState = chunkDownloadStates.get(key);
if (currentState == null) {
chunkDownloadStates.set(key, "requested");
pendingChunks += 1;
}
}
}
console.info("pending chunks after viewport update", pendingChunks);
}
canvasRenderer.addEventListener(".viewportUpdate", sendViewportUpdate); canvasRenderer.addEventListener(".viewportUpdate", sendViewportUpdate);
sendViewportUpdate(); sendViewportUpdate();
session.addEventListener("chunks", (event) => { session.addEventListener("chunks", async (event) => {
let { chunkInfo, chunkData } = event; let { chunkInfo, chunkData, hasMore } = event;
console.info("received data for chunks", { console.info("received data for chunks", {
chunkInfoLength: chunkInfo.length, chunkInfo,
chunkDataSize: chunkData.size, chunkDataSize: chunkData.size,
}); });
let updatePromises = [];
for (let info of event.chunkInfo) { for (let info of event.chunkInfo) {
let key = Wall.chunkKey(info.position.x, info.position.y);
if (chunkDownloadStates.get(key) == "requested") {
pendingChunks -= 1;
}
chunkDownloadStates.set(key, "downloaded");
if (info.length > 0) { if (info.length > 0) {
let blob = chunkData.slice(info.offset, info.offset + info.length, "image/webp"); let blob = chunkData.slice(info.offset, info.offset + info.length, "image/webp");
createImageBitmap(blob).then((bitmap) => { updatePromises.push(
let chunk = wall.getOrCreateChunk(info.position.x, info.position.y); createImageBitmap(blob).then((bitmap) => {
chunk.ctx.globalCompositeOperation = "copy"; let chunk = wall.getOrCreateChunk(info.position.x, info.position.y);
chunk.ctx.drawImage(bitmap, 0, 0); chunk.ctx.globalCompositeOperation = "copy";
chunk.syncToPixmap(); chunk.ctx.drawImage(bitmap, 0, 0);
}); chunk.syncToPixmap();
}),
);
} }
} }
await Promise.all(updatePromises);
if (hasMore) {
console.info("more chunks are pending; requesting more");
session.sendMoreChunks();
}
}); });
let reportCursor = debounce(updateInterval, (x, y) => session.sendCursor(x, y)); let reportCursor = debounce(updateInterval, (x, y) => session.sendCursor(x, y));

View file

@ -208,6 +208,7 @@ class Session extends EventTarget {
Object.assign(new Event("chunks"), { Object.assign(new Event("chunks"), {
chunkInfo: notify.chunks, chunkInfo: notify.chunks,
chunkData, chunkData,
hasMore: notify.hasMore,
}), }),
); );
} }