implementing more chunk ops based on GPU

composing, toEdits
This commit is contained in:
りき萌 2025-09-08 22:10:55 +02:00
parent bb55e23979
commit 1bbf1b1d94
5 changed files with 259 additions and 38 deletions

View file

@ -1,3 +1,5 @@
import { compileProgram } from "rkgk/webgl.js";
class Atlas {
static getInitBuffer(chunkSize, nChunks) {
let imageSize = chunkSize * nChunks;
@ -81,7 +83,7 @@ class Atlas {
this.chunkSize,
gl.RGBA,
gl.UNSIGNED_BYTE,
null,
0,
);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
@ -96,6 +98,35 @@ class Atlas {
}
}
const compositeVertexShader = `#version 300 es
precision highp float;
layout (location = 0) in vec2 a_position;
layout (location = 1) in vec2 a_uv;
out vec2 vf_uv;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
vf_uv = a_uv;
}
`;
const compositeFragmentShader = `#version 300 es
precision highp float;
uniform sampler2D u_chunk;
in vec2 vf_uv;
out vec4 f_color;
void main() {
f_color = texture(u_chunk, vf_uv);
// f_color = vec4(vec3(0.0), 1.0);
}
`;
export class AtlasAllocator {
atlases = [];
@ -113,6 +144,74 @@ export class AtlasAllocator {
this.chunkSize = chunkSize;
this.nChunks = nChunks;
this.initBuffer = Atlas.getInitBuffer(chunkSize, nChunks);
// Compositing pipeline
let compositeProgramId = compileProgram(gl, compositeVertexShader, compositeFragmentShader);
this.compositeProgram = {
id: compositeProgramId,
u_chunk: gl.getUniformLocation(compositeProgramId, "u_chunk"),
};
// prettier-ignore
this.compositeRectData = new Float32Array([
// a_position
-1, 1, // 0: top left
1, 1, // 1: top right
1, -1, // 2: bottom right
-1, -1, // 3: bottom left
// a_uv - filled out later when compositing
0, 0,
0, 0,
0, 0,
0, 0,
]);
let compositeRectIndices = new Uint16Array([0, 1, 2, 2, 3, 0]);
this.compositeRectUv = this.compositeRectData.subarray(8);
this.compositeRectVao = gl.createVertexArray();
this.compositeRectVbo = gl.createBuffer();
this.compositeRectIbo = gl.createBuffer();
gl.bindVertexArray(this.compositeRectVao);
gl.bindBuffer(gl.ARRAY_BUFFER, this.compositeRectVbo);
gl.bufferData(gl.ARRAY_BUFFER, this.compositeRectData, gl.DYNAMIC_DRAW);
console.log(this.compositeRectData.byteLength);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.compositeRectIbo);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, compositeRectIndices, gl.DYNAMIC_DRAW);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 2 * 4, 0);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 2 * 4, this.compositeRectUv.byteOffset);
for (let i = 0; i < 2; ++i) gl.enableVertexAttribArray(i);
this.compositeTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, this.compositeTexture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA8,
chunkSize,
chunkSize,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
null,
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
this.compositeFramebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, this.compositeFramebuffer);
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D,
this.compositeTexture,
0,
);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
#obtainId(allocInfo) {
@ -190,7 +289,7 @@ export class AtlasAllocator {
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);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
// Add for ticking
@ -207,6 +306,8 @@ export class AtlasAllocator {
// Call every frame to poll for download completion.
tickDownloads() {
if (this.#pendingDownloads.length == 0) return;
let gl = this.gl;
for (let i = 0; i < this.#pendingDownloads.length; ++i) {
@ -218,16 +319,22 @@ export class AtlasAllocator {
let arrayBuffer = this.#downloadBufferPool.pop() ?? new ArrayBuffer(dataSize);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pending.pbo);
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, arrayBuffer);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, 0);
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, new Uint8Array(arrayBuffer));
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
gl.deleteSync(pending.fence);
pending.resolve(arrayBuffer);
pending.resolve({
width: this.chunkSize,
height: this.chunkSize,
data: arrayBuffer,
});
let last = this.#pendingDownloads.pop();
if (last != null) {
if (this.#pendingDownloads.length > 0) {
this.#pendingDownloads[i] = last;
--i;
} else {
break; // now empty
}
}
}
@ -258,4 +365,82 @@ export class AtlasAllocator {
resetCanvas,
};
}
// NOTE: I was thinking a bit about whether the chunk allocator is the right place to put
// compositing operations like this. After much consideration, I've decided that it's pretty
// much the only sensible place, because it's the only place concerned with the layout of
// chunks in memory, and rendering of laid out chunks is quite implementation dependent.
//
// This does break the purity of the "allocator" role a bit though, but I don't really know if
// there's a good way around that.
//
// Maybe. But I don't feel like breaking this apart to 10 smaller classes, either.
#drawComposite(u, v, uvScale) {
// Assumes bound source texture, destination framebuffer, and viewport set.
let gl = this.gl;
gl.bindVertexArray(this.compositeRectVao);
gl.bindBuffer(gl.ARRAY_BUFFER, this.compositeRectVbo);
gl.useProgram(this.compositeProgram.id);
gl.uniform1i(this.compositeProgram.u_chunk, 0);
let uv = this.compositeRectUv;
uv[0] = u * uvScale;
uv[1] = v * uvScale;
uv[2] = (u + 1) * uvScale;
uv[3] = v * uvScale;
uv[4] = (u + 1) * uvScale;
uv[5] = (v + 1) * uvScale;
uv[6] = u * uvScale;
uv[7] = (v + 1) * uvScale;
gl.bufferSubData(gl.ARRAY_BUFFER, uv.byteOffset, uv);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
}
composite(dstId, srcId, op) {
// NOTE: This has to go through an intermediate buffer in case the source and destination
// atlas are the same.
console.assert(op == "alphaBlend", "composite operation must be alphaBlend");
let gl = this.gl;
let dstAtlas = this.atlases[this.getAtlasIndex(dstId)];
let srcAtlas = this.atlases[this.getAtlasIndex(srcId)];
let dstAllocation = this.getAllocation(dstId);
let srcAllocation = this.getAllocation(srcId);
// Source -> intermediate buffer
gl.disable(gl.BLEND);
gl.disable(gl.SCISSOR_TEST);
gl.bindFramebuffer(gl.FRAMEBUFFER, this.compositeFramebuffer);
gl.bindTexture(gl.TEXTURE_2D, srcAtlas.texture);
gl.viewport(0, 0, this.chunkSize, this.chunkSize);
this.#drawComposite(srcAllocation.x, srcAllocation.y, 1 / this.nChunks);
// Intermediate buffer -> destination
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.bindFramebuffer(gl.FRAMEBUFFER, dstAtlas.framebuffer);
gl.bindTexture(gl.TEXTURE_2D, this.compositeTexture);
gl.viewport(
dstAllocation.x * this.chunkSize,
dstAllocation.y * this.chunkSize,
this.chunkSize,
this.chunkSize,
);
this.#drawComposite(0, 0, 1);
// Cleanup
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
}