diff --git a/crates/rkgk/src/api/wall.rs b/crates/rkgk/src/api/wall.rs index eb52cbd..d38c0b7 100644 --- a/crates/rkgk/src/api/wall.rs +++ b/crates/rkgk/src/api/wall.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{collections::HashSet, sync::Arc}; use axum::{ extract::{ @@ -194,8 +194,11 @@ struct SessionLoop { wall: Arc, chunk_encoder: Arc, handle: SessionHandle, + render_commands_tx: mpsc::Sender, + viewport_chunks: ChunkIterator, + sent_chunks: HashSet, } enum RenderCommand { @@ -238,6 +241,7 @@ impl SessionLoop { handle, render_commands_tx, 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 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. - 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 self.sent_chunks.contains(&position) { + continue; + } + if let Some(encoded) = self.chunk_encoder.encoded(position).await { let offset = packet.len(); 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 // this to cause a disconnect. if packet.len() >= 256 * 1024 { + iterated = i; 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 { + iterated = i; break; } } + info!(elapsed = ?start.elapsed(), iterated, "send_chunks"); ws.send(to_message(&Notify::Chunks { chunks: chunk_infos, + has_more: self.viewport_chunks.clone().next().is_some(), })) .await?; ws.send(Message::Binary(packet)).await?; diff --git a/crates/rkgk/src/api/wall/schema.rs b/crates/rkgk/src/api/wall/schema.rs index fb392bf..c2f070c 100644 --- a/crates/rkgk/src/api/wall/schema.rs +++ b/crates/rkgk/src/api/wall/schema.rs @@ -94,6 +94,12 @@ pub struct ChunkInfo { rename_all_fields = "camelCase" )] pub enum Notify { - Wall { wall_event: wall::Event }, - Chunks { chunks: Vec }, + Wall { + wall_event: wall::Event, + }, + + Chunks { + chunks: Vec, + has_more: bool, + }, } diff --git a/static/brush-editor.js b/static/brush-editor.js index 40baf88..4696332 100644 --- a/static/brush-editor.js +++ b/static/brush-editor.js @@ -17,13 +17,14 @@ export class BrushEditor extends HTMLElement { this.textArea = this.appendChild(document.createElement("pre")); 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.spellcheck = false; this.textArea.addEventListener("input", () => { + localStorage.setItem("rkgk.brushEditor.code", this.code); this.dispatchEvent( Object.assign(new Event(".codeChanged"), { - newCode: this.textArea.value, + newCode: this.code, }), ); }); diff --git a/static/canvas-renderer.js b/static/canvas-renderer.js index 3924050..ca93b35 100644 --- a/static/canvas-renderer.js +++ b/static/canvas-renderer.js @@ -87,8 +87,13 @@ class CanvasRenderer extends HTMLElement { let chunk = this.wall.getChunk(chunkX, chunkY); if (chunk != null) { + this.ctx.globalCompositeOperation = "source-over"; this.ctx.drawImage(chunk.canvas, x, y); } + + this.ctx.globalCompositeOperation = "difference"; + this.ctx.fillStyle = "white"; + this.ctx.fillText(`${chunkX}, ${chunkY}`, x, y + 12); } } diff --git a/static/index.js b/static/index.js index 8ff8bd7..131eec6 100644 --- a/static/index.js +++ b/static/index.js @@ -83,54 +83,42 @@ reticleRenderer.connectViewport(canvasRenderer.viewport); } }); - let pendingChunks = 0; - let chunkDownloadStates = new Map(); - - function sendViewportUpdate() { + let sendViewportUpdate = debounce(updateInterval, () => { let visibleRect = canvasRenderer.getVisibleChunkRect(); session.sendViewport(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); - } - + console.log("visibleRect", visibleRect); + }); canvasRenderer.addEventListener(".viewportUpdate", sendViewportUpdate); sendViewportUpdate(); - session.addEventListener("chunks", (event) => { - let { chunkInfo, chunkData } = event; + session.addEventListener("chunks", async (event) => { + let { chunkInfo, chunkData, hasMore } = event; console.info("received data for chunks", { - chunkInfoLength: chunkInfo.length, + chunkInfo, chunkDataSize: chunkData.size, }); + let updatePromises = []; 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) { let blob = chunkData.slice(info.offset, info.offset + info.length, "image/webp"); - createImageBitmap(blob).then((bitmap) => { - let chunk = wall.getOrCreateChunk(info.position.x, info.position.y); - chunk.ctx.globalCompositeOperation = "copy"; - chunk.ctx.drawImage(bitmap, 0, 0); - chunk.syncToPixmap(); - }); + updatePromises.push( + createImageBitmap(blob).then((bitmap) => { + let chunk = wall.getOrCreateChunk(info.position.x, info.position.y); + chunk.ctx.globalCompositeOperation = "copy"; + 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)); diff --git a/static/session.js b/static/session.js index 81b4688..7e6896b 100644 --- a/static/session.js +++ b/static/session.js @@ -208,6 +208,7 @@ class Session extends EventTarget { Object.assign(new Event("chunks"), { chunkInfo: notify.chunks, chunkData, + hasMore: notify.hasMore, }), ); }