mirror of
https://github.com/Rezmason/matrix.git
synced 2026-04-16 21:39:29 -07:00
Fixed some major bugs: the WebGPU cache should store loaded images and text, never GPU resource handles; renamed renderer "formulate" to "configure"; WebGPU renderer's configure function needs early returns after each major await, in case there's a new config; the render loops are now locally stored closures; renderers now have start and stop functions; fixed bugs in the REGL and WebGPU mirror passes; WebGPU bloom pass now enforces texture dimensions are greater than zero; the react component now stores the renderer type in a useRef and returns early from renderer init awaits to prevent multiple renderers from instantiating.
This commit is contained in:
@@ -26,7 +26,7 @@ const makePyramid = (device, size, pyramidHeight) =>
|
||||
.map((_, index) =>
|
||||
makeComputeTarget(
|
||||
device,
|
||||
size.map((x) => Math.floor(x * 2 ** -index)),
|
||||
size.map((x) => Math.max(1, Math.floor(x * 2 ** -index))),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -111,7 +111,7 @@ export default ({ config, device, cache }) => {
|
||||
|
||||
const build = (screenSize, inputs) => {
|
||||
// Since the bloom is blurry, we downscale everything
|
||||
scaledScreenSize = screenSize.map((x) => Math.floor(x * bloomSize));
|
||||
scaledScreenSize = screenSize.map((x) => Math.max(1, Math.floor(x * bloomSize)));
|
||||
|
||||
destroyPyramid(hBlurPyramid);
|
||||
hBlurPyramid = makePyramid(device, scaledScreenSize, pyramidHeight);
|
||||
@@ -169,8 +169,8 @@ export default ({ config, device, cache }) => {
|
||||
computePass.setPipeline(blurPipeline);
|
||||
for (let i = 0; i < pyramidHeight; i++) {
|
||||
const dispatchSize = [
|
||||
Math.ceil(Math.floor(scaledScreenSize[0] * 2 ** -i) / 32),
|
||||
Math.floor(Math.floor(scaledScreenSize[1] * 2 ** -i)),
|
||||
Math.max(1, Math.ceil(Math.floor(scaledScreenSize[0] * 2 ** -i) / 32)),
|
||||
Math.max(1, Math.floor(Math.floor(scaledScreenSize[1] * 2 ** -i))),
|
||||
1,
|
||||
];
|
||||
computePass.setBindGroup(0, hBlurBindGroups[i]);
|
||||
|
||||
@@ -49,7 +49,7 @@ export default ({ device, cache, canvasFormat, canvasContext }) => {
|
||||
nearestSampler,
|
||||
inputs.primary.createView(),
|
||||
]);
|
||||
return null;
|
||||
return {};
|
||||
};
|
||||
|
||||
const run = (encoder, shouldRender) => {
|
||||
|
||||
@@ -7,24 +7,7 @@ import {
|
||||
makePass,
|
||||
} from "./utils.js";
|
||||
|
||||
let start;
|
||||
const numTouches = 5;
|
||||
const touches = Array(numTouches)
|
||||
.fill()
|
||||
.map((_) => [0, 0, -Infinity, 0]);
|
||||
let aspectRatio = 1;
|
||||
|
||||
let index = 0;
|
||||
let touchesChanged = true;
|
||||
window.onclick = (e) => {
|
||||
touches[index][0] = 0 + e.clientX / e.srcElement.clientWidth;
|
||||
touches[index][1] = 1 - e.clientY / e.srcElement.clientHeight;
|
||||
touches[index][2] = (Date.now() - start) / 1000;
|
||||
index = (index + 1) % numTouches;
|
||||
touchesChanged = true;
|
||||
};
|
||||
|
||||
export default ({ config, device, cache, cameraTex, cameraAspectRatio, timeBuffer }) => {
|
||||
export default ({ config, device, canvas, cache, cameraTex, cameraAspectRatio, timeBuffer }) => {
|
||||
const assets = [loadShader(device, cache, "shaders/wgsl/mirrorPass.wgsl")];
|
||||
|
||||
const linearSampler = device.createSampler({
|
||||
@@ -32,6 +15,24 @@ export default ({ config, device, cache, cameraTex, cameraAspectRatio, timeBuffe
|
||||
minFilter: "linear",
|
||||
});
|
||||
|
||||
let start;
|
||||
const numTouches = 5;
|
||||
const touches = Array(numTouches)
|
||||
.fill()
|
||||
.map((_) => [0, 0, -Infinity, 0]);
|
||||
let aspectRatio = 1;
|
||||
|
||||
let index = 0;
|
||||
let touchesChanged = true;
|
||||
canvas.onmousedown = (e) => {
|
||||
const rect = e.srcElement.getBoundingClientRect();
|
||||
touches[index][0] = 0 + (e.clientX - rect.x) / rect.width;
|
||||
touches[index][1] = 1 - (e.clientY - rect.y) / rect.height;
|
||||
touches[index][2] = (performance.now() - start) / 1000;
|
||||
index = (index + 1) % numTouches;
|
||||
touchesChanged = true;
|
||||
};
|
||||
|
||||
let computePipeline;
|
||||
let configBuffer;
|
||||
let sceneUniforms;
|
||||
@@ -109,7 +110,7 @@ export default ({ config, device, cache, cameraTex, cameraAspectRatio, timeBuffe
|
||||
computePass.end();
|
||||
};
|
||||
|
||||
start = Date.now();
|
||||
start = performance.now();
|
||||
|
||||
return makePass("Mirror", loaded, build, run);
|
||||
};
|
||||
|
||||
@@ -27,152 +27,185 @@ const effects = {
|
||||
export default class REGLRenderer extends Renderer {
|
||||
|
||||
#glMatrix;
|
||||
#canvasContext;
|
||||
#adapter;
|
||||
#device;
|
||||
#renderLoop;
|
||||
#canvasContext;
|
||||
#canvasFormat;
|
||||
#renderFunc;
|
||||
|
||||
#renewingDevice;
|
||||
#configureIndex = 0;
|
||||
#rebuildingPipeline;
|
||||
|
||||
constructor() {
|
||||
super("webgpu", async () => {
|
||||
const libraries = await Renderer.libraries;
|
||||
this.#glMatrix = libraries.glMatrix;
|
||||
this.#canvasContext = this.canvas.getContext("webgpu");
|
||||
this.#adapter = await navigator.gpu.requestAdapter();
|
||||
this.#device = await this.#adapter.requestDevice();
|
||||
});
|
||||
}
|
||||
|
||||
async formulate(config) {
|
||||
await super.formulate(config);
|
||||
|
||||
const canvas = this.canvas;
|
||||
const cache = this.cache;
|
||||
const canvasContext = this.#canvasContext;
|
||||
const adapter = this.#adapter;
|
||||
const device = this.#device;
|
||||
const glMatrix = this.#glMatrix;
|
||||
|
||||
async configure(config) {
|
||||
const index = ++this.#configureIndex;
|
||||
await super.configure(config);
|
||||
if (config.useCamera) {
|
||||
await setupCamera();
|
||||
}
|
||||
|
||||
const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
|
||||
|
||||
// console.table(device.limits);
|
||||
|
||||
canvasContext.configure({
|
||||
device,
|
||||
format: canvasFormat,
|
||||
alphaMode: "opaque",
|
||||
usage:
|
||||
// GPUTextureUsage.STORAGE_BINDING |
|
||||
GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST,
|
||||
});
|
||||
|
||||
const timeUniforms = structs.from(`struct Time { seconds : f32, frames : i32, };`).Time;
|
||||
const timeBuffer = makeUniformBuffer(device, timeUniforms);
|
||||
const cameraTex = device.createTexture({
|
||||
size: cameraSize,
|
||||
format: "rgba8unorm",
|
||||
usage:
|
||||
GPUTextureUsage.TEXTURE_BINDING |
|
||||
GPUTextureUsage.COPY_DST |
|
||||
GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
});
|
||||
|
||||
const context = {
|
||||
config,
|
||||
cache,
|
||||
adapter,
|
||||
device,
|
||||
canvasContext,
|
||||
timeBuffer,
|
||||
canvasFormat,
|
||||
cameraTex,
|
||||
cameraAspectRatio,
|
||||
cameraSize,
|
||||
glMatrix,
|
||||
};
|
||||
|
||||
const effectName = config.effect in effects ? config.effect : "palette";
|
||||
const pipeline = await makePipeline(context, [
|
||||
makeRain,
|
||||
makeBloomPass,
|
||||
effects[effectName],
|
||||
makeEndPass,
|
||||
]);
|
||||
|
||||
const targetFrameTimeMilliseconds = 1000 / config.fps;
|
||||
let frames = 0;
|
||||
let start = NaN;
|
||||
let last = NaN;
|
||||
let outputs;
|
||||
|
||||
const renderLoop = (now) => {
|
||||
if (isNaN(start)) {
|
||||
start = now;
|
||||
}
|
||||
|
||||
if (isNaN(last)) {
|
||||
last = start;
|
||||
}
|
||||
|
||||
const shouldRender =
|
||||
config.fps >= 60 || now - last >= targetFrameTimeMilliseconds || config.once;
|
||||
if (shouldRender) {
|
||||
while (now - targetFrameTimeMilliseconds > last) {
|
||||
last += targetFrameTimeMilliseconds;
|
||||
}
|
||||
}
|
||||
|
||||
const devicePixelRatio = window.devicePixelRatio ?? 1;
|
||||
const size = this.size;
|
||||
const [width, height] = size;
|
||||
if (outputs == null || canvas.width !== width || canvas.height !== height) {
|
||||
[canvas.width, canvas.height] = size;
|
||||
outputs = pipeline.build(size);
|
||||
}
|
||||
|
||||
if (config.useCamera) {
|
||||
device.queue.copyExternalImageToTexture(
|
||||
{ source: cameraCanvas },
|
||||
{ texture: cameraTex },
|
||||
cameraSize,
|
||||
);
|
||||
}
|
||||
|
||||
device.queue.writeBuffer(
|
||||
timeBuffer,
|
||||
0,
|
||||
timeUniforms.toBuffer({ seconds: (now - start) / 1000, frames }),
|
||||
);
|
||||
frames++;
|
||||
|
||||
const encoder = device.createCommandEncoder();
|
||||
pipeline.run(encoder, shouldRender);
|
||||
// Eventually, when WebGPU allows it, we'll remove the endPass and just copy from our pipeline's output to the canvas texture.
|
||||
// encoder.copyTextureToTexture({ texture: outputs?.primary }, { texture: canvasContext.getCurrentTexture() }, canvasSize);
|
||||
device.queue.submit([encoder.finish()]);
|
||||
|
||||
if (!config.once) {
|
||||
requestAnimationFrame(renderLoop);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.#renderLoop != null) {
|
||||
cancelAnimationFrame(this.#renderLoop);
|
||||
if (this.#rebuildingPipeline != null) {
|
||||
await this.#rebuildingPipeline;
|
||||
}
|
||||
|
||||
renderLoop(performance.now());
|
||||
this.#renderLoop = renderLoop;
|
||||
const oldDevice = this.#device;
|
||||
|
||||
if (this.#renewingDevice == null) {
|
||||
this.#renewingDevice = (async () => {
|
||||
this.#canvasContext = this.canvas.getContext("webgpu");
|
||||
this.#canvasFormat = navigator.gpu.getPreferredCanvasFormat();
|
||||
const adapter = await navigator.gpu.requestAdapter();
|
||||
this.#device = await adapter.requestDevice();
|
||||
})();
|
||||
}
|
||||
await this.#renewingDevice;
|
||||
this.#renewingDevice = null;
|
||||
|
||||
if (this.#configureIndex !== index || this.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#rebuildingPipeline = (async () => {
|
||||
|
||||
const glMatrix = this.#glMatrix;
|
||||
const canvas = this.canvas;
|
||||
const cache = this.cache;
|
||||
const device = this.#device;
|
||||
const canvasContext = this.#canvasContext;
|
||||
const canvasFormat = this.#canvasFormat;
|
||||
const dimensions = { width: 1, height: 1 };
|
||||
|
||||
const timeUniforms = structs.from(`struct Time { seconds : f32, frames : i32, };`).Time;
|
||||
const timeBuffer = makeUniformBuffer(device, timeUniforms);
|
||||
const cameraTex = device.createTexture({
|
||||
size: cameraSize,
|
||||
format: "rgba8unorm",
|
||||
usage:
|
||||
GPUTextureUsage.TEXTURE_BINDING |
|
||||
GPUTextureUsage.COPY_DST |
|
||||
GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
});
|
||||
|
||||
const context = {
|
||||
glMatrix,
|
||||
config,
|
||||
cache,
|
||||
device,
|
||||
canvas,
|
||||
canvasContext,
|
||||
canvasFormat,
|
||||
timeBuffer,
|
||||
cameraTex,
|
||||
cameraAspectRatio,
|
||||
cameraSize,
|
||||
};
|
||||
|
||||
const effectName = config.effect in effects ? config.effect : "palette";
|
||||
const pipeline = await makePipeline(context, [
|
||||
makeRain,
|
||||
makeBloomPass,
|
||||
effects[effectName],
|
||||
makeEndPass,
|
||||
]);
|
||||
|
||||
this.#canvasContext.configure({
|
||||
device: this.#device,
|
||||
format: this.#canvasFormat,
|
||||
alphaMode: "opaque",
|
||||
usage:
|
||||
// GPUTextureUsage.STORAGE_BINDING |
|
||||
GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST,
|
||||
});
|
||||
|
||||
dimensions.width = canvas.width;
|
||||
dimensions.height = canvas.height;
|
||||
const targetFrameTimeMilliseconds = 1000 / config.fps;
|
||||
let frames = 0;
|
||||
let start = NaN;
|
||||
let last = NaN;
|
||||
let outputs;
|
||||
|
||||
this.#renderFunc = (now) => {
|
||||
if (config.once) {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
if (isNaN(start)) {
|
||||
start = now;
|
||||
}
|
||||
|
||||
if (isNaN(last)) {
|
||||
last = start;
|
||||
}
|
||||
|
||||
const shouldRender =
|
||||
config.fps >= 60 || now - last >= targetFrameTimeMilliseconds || config.once;
|
||||
if (shouldRender) {
|
||||
while (now - targetFrameTimeMilliseconds > last) {
|
||||
last += targetFrameTimeMilliseconds;
|
||||
}
|
||||
}
|
||||
|
||||
const size = this.size;
|
||||
const [width, height] = size;
|
||||
if (outputs == null || dimensions.width !== width || dimensions.height !== height) {
|
||||
[dimensions.width, dimensions.height] = size;
|
||||
outputs = pipeline.build(size);
|
||||
}
|
||||
|
||||
if (config.useCamera) {
|
||||
device.queue.copyExternalImageToTexture(
|
||||
{ source: cameraCanvas },
|
||||
{ texture: cameraTex },
|
||||
cameraSize,
|
||||
);
|
||||
}
|
||||
|
||||
device.queue.writeBuffer(
|
||||
timeBuffer,
|
||||
0,
|
||||
timeUniforms.toBuffer({ seconds: (now - start) / 1000, frames }),
|
||||
);
|
||||
frames++;
|
||||
|
||||
const encoder = device.createCommandEncoder();
|
||||
pipeline.run(encoder, shouldRender);
|
||||
// Eventually, when WebGPU allows it, we'll remove the endPass and just copy from our pipeline's output to the canvas texture.
|
||||
// encoder.copyTextureToTexture({ texture: outputs?.primary }, { texture: canvasContext.getCurrentTexture() }, canvasSize);
|
||||
device.queue.submit([encoder.finish()]);
|
||||
};
|
||||
})();
|
||||
await this.#rebuildingPipeline;
|
||||
this.#renderFunc(performance.now());
|
||||
if (oldDevice != null) {
|
||||
oldDevice.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
super.stop();
|
||||
this.#renderFunc = null;
|
||||
}
|
||||
|
||||
update(now) {
|
||||
if (this.#renderFunc != null) {
|
||||
this.#renderFunc(now);
|
||||
}
|
||||
super.update(now);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.destroyed) {
|
||||
return;
|
||||
if (this.destroyed) return;
|
||||
if (this.#device != null) {
|
||||
this.#device.destroy(); // This also destroys any objects created with the device
|
||||
this.#device = null;
|
||||
}
|
||||
cancelAnimationFrame(this.#renderLoop); // stop RAF
|
||||
this.#device.destroy(); // This also destroys any objects created with the device
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
const loadTexture = async (device, cache, url) => {
|
||||
const key = url;
|
||||
if (cache.has(key)) {
|
||||
return cache.get(key);
|
||||
}
|
||||
|
||||
let texture;
|
||||
const format = "rgba8unorm";
|
||||
const usage =
|
||||
GPUTextureUsage.TEXTURE_BINDING |
|
||||
GPUTextureUsage.COPY_DST |
|
||||
GPUTextureUsage.RENDER_ATTACHMENT;
|
||||
|
||||
if (url == null) {
|
||||
texture = device.createTexture({
|
||||
return device.createTexture({
|
||||
size: [1, 1, 1],
|
||||
format: "rgba8unorm",
|
||||
usage:
|
||||
GPUTextureUsage.TEXTURE_BINDING |
|
||||
GPUTextureUsage.COPY_DST |
|
||||
GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
format,
|
||||
usage,
|
||||
});
|
||||
}
|
||||
|
||||
let source;
|
||||
const key = url;
|
||||
if (cache.has(key)) {
|
||||
source = cache.get(key);
|
||||
} else {
|
||||
let imageURL;
|
||||
if (typeof cache.get(`url::${url}`) === "function") {
|
||||
@@ -25,23 +28,17 @@ const loadTexture = async (device, cache, url) => {
|
||||
|
||||
const response = await fetch(imageURL);
|
||||
const data = await response.blob();
|
||||
const source = await createImageBitmap(data);
|
||||
const size = [source.width, source.height, 1];
|
||||
|
||||
texture = device.createTexture({
|
||||
size,
|
||||
format: "rgba8unorm",
|
||||
usage:
|
||||
GPUTextureUsage.TEXTURE_BINDING |
|
||||
GPUTextureUsage.COPY_DST |
|
||||
GPUTextureUsage.RENDER_ATTACHMENT,
|
||||
});
|
||||
|
||||
device.queue.copyExternalImageToTexture({ source, flipY: true }, { texture }, size);
|
||||
source = await createImageBitmap(data);
|
||||
cache.set(key, source);
|
||||
}
|
||||
|
||||
cache.set(key, texture);
|
||||
|
||||
const size = [source.width, source.height, 1];
|
||||
const texture = device.createTexture({
|
||||
size,
|
||||
format,
|
||||
usage,
|
||||
});
|
||||
device.queue.copyExternalImageToTexture({ source, flipY: true }, { texture }, size);
|
||||
return texture;
|
||||
};
|
||||
|
||||
@@ -71,14 +68,16 @@ const makeComputeTarget = (device, size, mipLevelCount = 1) =>
|
||||
|
||||
const loadShader = async (device, cache, url) => {
|
||||
const key = url;
|
||||
if (cache.has(key)) {
|
||||
return cache.get(key);
|
||||
}
|
||||
let code;
|
||||
if (typeof cache.get(`raw::${url}`) === "function") {
|
||||
code = (await cache.get(`raw::${url}`)()).default;
|
||||
if (cache.has(key)) {
|
||||
code = cache.get(key);
|
||||
} else {
|
||||
code = await (await fetch(url)).text();
|
||||
if (typeof cache.get(`raw::${url}`) === "function") {
|
||||
code = (await cache.get(`raw::${url}`)()).default;
|
||||
} else {
|
||||
code = await (await fetch(url)).text();
|
||||
}
|
||||
cache.set(key, code);
|
||||
}
|
||||
return {
|
||||
code,
|
||||
|
||||
Reference in New Issue
Block a user