removing server-side brush rendering

brush rendering is now completely client-side.
the server only receives edits the client would like to do, in the form of PNG images of chunks, that are then composited onto the wall

known issue: it is possible to brush up against the current 256 chunk edit limit pretty easily.
I'm not sure it can be solved very easily though. the perfect solution would involve splitting up the interaction into multiple edits, and I tried to do that, but there's a noticable stutter for some reason that I haven't managed to track down yet.
so it'll be kinda crap for the time being.
This commit is contained in:
りき萌 2025-06-30 00:48:49 +02:00
parent 15a1bf8036
commit bff899c9c0
24 changed files with 613 additions and 1170 deletions

View file

@ -1,6 +1,6 @@
import { listen } from "rkgk/framework.js";
import { listen, Pool } from "rkgk/framework.js";
import { Viewport } from "rkgk/viewport.js";
import { Wall } from "rkgk/wall.js";
import { Wall, chunkKey } from "rkgk/wall.js";
class CanvasRenderer extends HTMLElement {
viewport = new Viewport();
@ -196,7 +196,8 @@ class CanvasRenderer extends HTMLElement {
console.debug("initialized atlas allocator", this.atlasAllocator);
this.chunksThisFrame = new Map();
this.batches = [];
this.batchPool = new Pool();
console.debug("GL error state", this.gl.getError());
@ -294,31 +295,55 @@ class CanvasRenderer extends HTMLElement {
this.#collectChunksThisFrame();
for (let [i, chunks] of this.chunksThisFrame) {
let atlas = this.atlasAllocator.atlases[i];
this.gl.bindTexture(this.gl.TEXTURE_2D, atlas.id);
this.#resetRectBuffer();
for (let chunk of chunks) {
let { i, allocation } = this.getChunkAllocation(chunk.x, chunk.y);
for (let batch of this.batches) {
for (let [i, chunks] of batch) {
let atlas = this.atlasAllocator.atlases[i];
this.#pushRect(
chunk.x * this.wall.chunkSize,
chunk.y * this.wall.chunkSize,
this.wall.chunkSize,
this.wall.chunkSize,
(allocation.x * atlas.chunkSize) / atlas.textureSize,
(allocation.y * atlas.chunkSize) / atlas.textureSize,
atlas.chunkSize / atlas.textureSize,
atlas.chunkSize / atlas.textureSize,
);
this.gl.bindTexture(this.gl.TEXTURE_2D, atlas.id);
this.#resetRectBuffer();
for (let chunk of chunks) {
let { i, allocation } = this.getChunkAllocation(
chunk.layerId,
chunk.x,
chunk.y,
);
let atlas = this.atlasAllocator.atlases[i];
this.#pushRect(
chunk.x * this.wall.chunkSize,
chunk.y * this.wall.chunkSize,
this.wall.chunkSize,
this.wall.chunkSize,
(allocation.x * atlas.chunkSize) / atlas.textureSize,
(allocation.y * atlas.chunkSize) / atlas.textureSize,
atlas.chunkSize / atlas.textureSize,
atlas.chunkSize / atlas.textureSize,
);
}
this.#drawRects();
}
this.#drawRects();
}
// TODO: This is a nice debug view.
// There should be a switch to it somewhere in the app.
/*
let x = 0;
let y = 0;
for (let atlas of this.atlasAllocator.atlases) {
this.#resetRectBuffer();
this.gl.bindTexture(this.gl.TEXTURE_2D, atlas.id);
this.#pushRect(x, y, atlas.textureSize, atlas.textureSize, 0, 0, 1, 1);
this.#drawRects();
if (x > atlas.textureSize * 16) {
y += atlas.textureSize;
x = 0;
}
x += atlas.textureSize;
}
*/
}
getChunkAllocation(chunkX, chunkY) {
let key = Wall.chunkKey(chunkX, chunkY);
getChunkAllocation(layerId, chunkX, chunkY) {
let key = `${layerId}/${chunkKey(chunkX, chunkY)}`;
if (this.chunkAllocations.has(key)) {
return this.chunkAllocations.get(key);
} else {
@ -328,36 +353,54 @@ class CanvasRenderer extends HTMLElement {
}
}
deallocateChunks(layer) {
for (let chunkKey of layer.chunks.keys()) {
let key = `${layer.id}/${chunkKey}`;
if (this.chunkAllocations.has(key)) {
let allocation = this.chunkAllocations.get(key);
this.atlasAllocator.dealloc(allocation);
this.chunkAllocations.delete(key);
}
}
}
#collectChunksThisFrame() {
// NOTE: Not optimal that we don't preserve the arrays anyhow; it would be better if we
// preserved the allocations.
this.chunksThisFrame.clear();
for (let batch of this.batches) {
batch.clear();
this.batchPool.free(batch);
}
this.batches.splice(0, this.batches.length);
let visibleRect = this.viewport.getVisibleRect(this.getWindowSize());
let left = Math.floor(visibleRect.x / this.wall.chunkSize);
let top = Math.floor(visibleRect.y / this.wall.chunkSize);
let right = Math.ceil((visibleRect.x + visibleRect.width) / this.wall.chunkSize);
let bottom = Math.ceil((visibleRect.y + visibleRect.height) / this.wall.chunkSize);
for (let chunkY = top; chunkY < bottom; ++chunkY) {
for (let chunkX = left; chunkX < right; ++chunkX) {
let chunk = this.wall.getChunk(chunkX, chunkY);
if (chunk != null) {
if (chunk.renderDirty) {
this.#updateChunkTexture(chunkX, chunkY);
chunk.renderDirty = false;
for (let layer of this.wall.layers) {
let batch = this.batchPool.alloc(Map);
for (let chunkY = top; chunkY < bottom; ++chunkY) {
for (let chunkX = left; chunkX < right; ++chunkX) {
let chunk = layer.getChunk(chunkX, chunkY);
if (chunk != null) {
if (chunk.renderDirty) {
this.#updateChunkTexture(layer, chunkX, chunkY);
chunk.renderDirty = false;
}
let allocation = this.getChunkAllocation(layer.id, chunkX, chunkY);
let array = batch.get(allocation.i);
if (array == null) {
array = [];
batch.set(allocation.i, array);
}
array.push({ layerId: layer.id, x: chunkX, y: chunkY });
}
let allocation = this.getChunkAllocation(chunkX, chunkY);
let array = this.chunksThisFrame.get(allocation.i);
if (array == null) {
array = [];
this.chunksThisFrame.set(allocation.i, array);
}
array.push({ x: chunkX, y: chunkY });
}
}
this.batches.push(batch);
}
}
@ -395,9 +438,9 @@ class CanvasRenderer extends HTMLElement {
this.gl.drawArraysInstanced(this.gl.TRIANGLES, 0, 6, this.uboRectsNum);
}
#updateChunkTexture(chunkX, chunkY) {
let allocation = this.getChunkAllocation(chunkX, chunkY);
let chunk = this.wall.getChunk(chunkX, chunkY);
#updateChunkTexture(layer, chunkX, chunkY) {
let allocation = this.getChunkAllocation(layer.id, chunkX, chunkY);
let chunk = layer.getChunk(chunkX, chunkY);
this.atlasAllocator.upload(this.gl, allocation, chunk.pixmap);
}
@ -474,6 +517,10 @@ class CanvasRenderer extends HTMLElement {
}
}
}
commitInteraction() {
this.dispatchEvent(new Event(".commitInteraction"));
}
}
customElements.define("rkgk-canvas-renderer", CanvasRenderer);
@ -513,6 +560,7 @@ class InteractEvent extends Event {
if (event.type == "mouseup" && event.button == 0) {
// Break the loop.
this.canvasRenderer.commitInteraction();
return;
}
})();
@ -576,6 +624,10 @@ class Atlas {
return this.free.pop();
}
dealloc(xy) {
this.free.push(xy);
}
upload(gl, { x, y }, pixmap) {
gl.bindTexture(gl.TEXTURE_2D, this.id);
gl.texSubImage2D(
@ -621,6 +673,11 @@ class AtlasAllocator {
return { i, allocation };
}
dealloc({ i, allocation }) {
let atlas = this.atlases[i];
atlas.dealloc(allocation);
}
upload(gl, { i, allocation }, pixmap) {
this.atlases[i].upload(gl, allocation, pixmap);
}