initial implementation of WebGL-based brush renderer

This commit is contained in:
りき萌 2025-09-05 20:20:45 +02:00
parent b4c3260f49
commit bb55e23979
14 changed files with 385 additions and 247 deletions

View file

@ -1,7 +1,7 @@
class Atlas {
static getInitBuffer(chunkSize, nChunks) {
let imageSize = chunkSize * nChunks;
return new Uint8Array(imageSize * imageSize * 4).fill(0xaa);
return new Uint8Array(imageSize * imageSize * 4).fill(0x00);
}
constructor(gl, chunkSize, nChunks, initBuffer) {
@ -44,8 +44,12 @@ class Atlas {
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
alloc() {
return this.free.pop();
alloc(gl, initBuffer) {
let xy = this.free.pop();
if (xy != null) {
this.upload(gl, xy, initBuffer);
}
return xy;
}
dealloc(xy) {
@ -81,11 +85,24 @@ class Atlas {
);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
getFramebufferRect({ x, y }) {
return {
x: x * this.chunkSize,
y: y * this.chunkSize,
width: this.chunkSize,
height: this.chunkSize,
};
}
}
export class AtlasAllocator {
atlases = [];
// Allocation names
#ids = new Map();
#idCounter = 1;
// Download buffers
#pboPool = [];
#downloadBufferPool = [];
@ -98,6 +115,28 @@ export class AtlasAllocator {
this.initBuffer = Atlas.getInitBuffer(chunkSize, nChunks);
}
#obtainId(allocInfo) {
let id = this.#idCounter++;
this.#ids.set(id, allocInfo);
return id;
}
#releaseId(id) {
this.#ids.delete(id);
}
#getAllocInfo(id) {
return this.#ids.get(id);
}
getAtlasIndex(id) {
return this.#getAllocInfo(id).i;
}
getAllocation(id) {
return this.#getAllocInfo(id).allocation;
}
alloc() {
// Right now we do a dumb linear scan through all atlases, but in the future it would be
// really nice to optimize this by storing information about which atlases have free slots
@ -105,33 +144,35 @@ export class AtlasAllocator {
for (let i = 0; i < this.atlases.length; ++i) {
let atlas = this.atlases[i];
let allocation = atlas.alloc();
let allocation = atlas.alloc(this.gl, this.initBuffer);
if (allocation != null) {
return { i, allocation };
return this.#obtainId({ i, allocation });
}
}
let i = this.atlases.length;
let atlas = new Atlas(this.gl, this.chunkSize, this.nChunks, this.initBuffer);
let allocation = atlas.alloc();
let allocation = atlas.alloc(this.gl, this.initBuffer);
this.atlases.push(atlas);
return { i, allocation };
return this.#obtainId({ i, allocation });
}
dealloc(id) {
let { i, allocation } = id;
let { i, allocation } = this.#getAllocInfo(id);
let atlas = this.atlases[i];
atlas.dealloc(allocation);
this.#releaseId(id);
}
upload(id, source) {
let { i, allocation } = id;
let { i, allocation } = this.#getAllocInfo(id);
this.atlases[i].upload(this.gl, allocation, source);
}
async download(id) {
let gl = this.gl;
let allocInfo = this.#getAllocInfo(id);
// Get PBO
@ -146,7 +187,7 @@ export class AtlasAllocator {
// Initiate download
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo);
this.atlases[id.i].download(gl, id);
this.atlases[allocInfo.i].download(gl, allocInfo.allocation);
let fence = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, 0);
@ -191,4 +232,30 @@ export class AtlasAllocator {
}
}
}
canvasSource() {
let useCanvas = (gl, id) => {
let allocInfo = this.#getAllocInfo(id);
let atlas = this.atlases[allocInfo.i];
let viewport = atlas.getFramebufferRect(allocInfo.allocation);
gl.enable(gl.SCISSOR_TEST);
gl.bindFramebuffer(gl.FRAMEBUFFER, atlas.framebuffer);
gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
gl.scissor(viewport.x, viewport.y, viewport.width, viewport.height);
return viewport;
};
let resetCanvas = (gl) => {
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.disable(gl.SCISSOR_TEST);
};
return {
useCanvas,
resetCanvas,
};
}
}