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:
Rezmason
2025-05-25 03:30:26 -07:00
parent 1da1feb356
commit b6570de106
15 changed files with 405 additions and 351 deletions

View File

@@ -1,18 +1,12 @@
TODO: TODO:
WebGPU formulate is expensive Minimum react requirement?
Mirror pass clicks bug
Minify bundles Minify bundles
Naming "matrix" for the github repo, "digital-rain" and "DigitalRain" for everything else Naming "matrix" for the github repo, "digital-rain" and "DigitalRain" for everything else
Minimum react requirement?
Retire fetchLibraries? Retire fetchLibraries?
Preserve time across configure calls, move times into renderer and uniforms
Move off of regl Move off of regl
Unify implementations? Unify implementations?
Responsive changes
Move start time to rain object
Matrix component should record, then overwrite it
Reshape all passes to react to config changes, ie. "configure" Reshape all passes to react to config changes, ie. "configure"
main.js "formulate" --> "configure"
simple deltas only require updating the uniforms simple deltas only require updating the uniforms
return boolean of whether all deltas are simple return boolean of whether all deltas are simple
Resource changes are simple if they're cached and loaded, false otherwise Resource changes are simple if they're cached and loaded, false otherwise

View File

@@ -31,7 +31,7 @@ import makeConfig from "./utils/config";
* volumetric?: boolean, * volumetric?: boolean,
* loops?: boolean, * loops?: boolean,
* skipIntro?: boolean, * skipIntro?: boolean,
* renderer?: "regl" | "three" | string, * renderer?: "regl" | "webgpu" | string,
* suppressWarnings?: boolean, * suppressWarnings?: boolean,
* useHalfFloat?: boolean, * useHalfFloat?: boolean,
* isometric?: boolean, * isometric?: boolean,
@@ -109,9 +109,10 @@ export const Matrix = memo((props) => {
const elProps = { style, className }; const elProps = { style, className };
const domElement = useRef(null); const domElement = useRef(null);
const [rRenderer, setRenderer] = useState(null); const [rRenderer, setRenderer] = useState(null);
const rendererType = useRef(null);
const [rSize, setSize] = useState([1, 1]); const [rSize, setSize] = useState([1, 1]);
const [rConfig, setConfig] = useState(makeConfig({})); const [rConfig, setConfig] = useState(makeConfig({}));
const rendererClasses = {}; const rendererModules = {};
const resizeObserver = new ResizeObserver(entries => { const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) { for (const entry of entries) {
@@ -126,14 +127,6 @@ export const Matrix = memo((props) => {
resizeObserver.observe(domElement.current); resizeObserver.observe(domElement.current);
}, [domElement]); }, [domElement]);
useEffect(() => {
setConfig(makeConfig({
...Object.fromEntries(
Object.entries(rawConfigProps).filter(([_, value]) => value != null),
)
}));
}, [props]);
const supportsWebGPU = () => { const supportsWebGPU = () => {
return ( return (
window.GPUQueue != null && window.GPUQueue != null &&
@@ -142,6 +135,18 @@ export const Matrix = memo((props) => {
); );
}; };
useEffect(() => {
const config = makeConfig({
...Object.fromEntries(
Object.entries(rawConfigProps).filter(([_, value]) => value != null),
)
});
if (config.renderer === "webgpu" && !supportsWebGPU()) {
config.renderer = "regl";
}
setConfig(config);
}, [props]);
const cleanup = () => { const cleanup = () => {
if (rRenderer == null) return; if (rRenderer == null) return;
rRenderer.canvas.remove(); rRenderer.canvas.remove();
@@ -150,37 +155,40 @@ export const Matrix = memo((props) => {
}; };
useEffect(() => { useEffect(() => {
const useWebGPU = supportsWebGPU() && rConfig.renderer === "webgpu"; rendererType.current = rConfig.renderer;
const isWebGPU = rRenderer?.type === "webgpu"; let rendererModule;
if (rConfig.renderer === "webgpu") {
const loadRain = async () => { rendererModules.webgpu ??= import("./webgpu/renderer.js");
let renderer; rendererModule = rendererModules.webgpu;
if (useWebGPU) {
rendererClasses.webgpu ??= (await import("./webgpu/renderer.js")).default;
renderer = new (rendererClasses.webgpu)();
} else { } else {
rendererClasses.regl ??= (await import("./regl/renderer.js")).default; rendererModules.regl ??= import("./regl/renderer.js");
renderer = new (rendererClasses.regl)(); rendererModule = rendererModules.regl;
} }
setRenderer(renderer);
(async () => {
const rendererClass = (await rendererModule).default;
if (rendererType.current !== rConfig.renderer) return;
const renderer = new rendererClass();
await renderer.ready; await renderer.ready;
if (rendererType.current !== rConfig.renderer) {
console.warn("Destroyed a redundant renderer late.");
renderer.destroy();
return;
}
cleanup();
setRenderer(renderer);
const canvas = renderer.canvas; const canvas = renderer.canvas;
canvas.style.width = "100%"; canvas.style.width = "100%";
canvas.style.height = "100%"; canvas.style.height = "100%";
domElement.current.appendChild(canvas); domElement.current.appendChild(canvas);
}; })();
if (rRenderer == null || useWebGPU !== isWebGPU) {
cleanup();
loadRain();
}
return cleanup; return cleanup;
}, [rConfig.renderer]); }, [rConfig.renderer]);
useEffect(() => { useEffect(() => {
if (rRenderer?.destroyed ?? true) return; if (rRenderer?.destroyed ?? true) return;
rRenderer.formulate(rConfig); rRenderer.configure(rConfig);
}, [rRenderer, rConfig]); }, [rRenderer, rConfig]);
useEffect(() => { useEffect(() => {

View File

@@ -37,7 +37,7 @@ document.body.onload = async () => {
renderer.fullscreen = !renderer.fullscreen; renderer.fullscreen = !renderer.fullscreen;
}); });
document.body.appendChild(renderer.canvas); document.body.appendChild(renderer.canvas);
await renderer.formulate(config); await renderer.configure(config);
}; };
if (isRunningSwiftShader() && !config.suppressWarnings) { if (isRunningSwiftShader() && !config.suppressWarnings) {

View File

@@ -1,19 +1,21 @@
import { loadText, makePassFBO, makePass } from "./utils.js"; import { loadText, makePassFBO, makePass } from "./utils.js";
let start; export default ({ regl, canvas, cache, config, cameraTex, cameraAspectRatio }, inputs) => {
const numClicks = 5;
const clicks = Array(numClicks).fill([0, 0, -Infinity]).flat();
let aspectRatio = 1;
let index = 0; let start;
window.onclick = (e) => { const numClicks = 5;
clicks[index * 3 + 0] = 0 + e.clientX / e.srcElement.clientWidth; const clicks = Array(numClicks).fill().map(_ => ([0, 0, -Infinity]));
clicks[index * 3 + 1] = 1 - e.clientY / e.srcElement.clientHeight; let aspectRatio = 1;
clicks[index * 3 + 2] = (Date.now() - start) / 1000;
let index = 0;
canvas.onmousedown = (e) => {
const rect = e.srcElement.getBoundingClientRect();
clicks[index][0] = 0 + (e.clientX - rect.x) / rect.width;
clicks[index][1] = 1 - (e.clientY - rect.y) / rect.height;
clicks[index][2] = (performance.now() - start) / 1000;
index = (index + 1) % numClicks; index = (index + 1) % numClicks;
}; };
export default ({ regl, cache, config, cameraTex, cameraAspectRatio }, inputs) => {
const output = makePassFBO(regl, config.useHalfFloat); const output = makePassFBO(regl, config.useHalfFloat);
const mirrorPassFrag = loadText(cache, "shaders/glsl/mirrorPass.frag.glsl"); const mirrorPassFrag = loadText(cache, "shaders/glsl/mirrorPass.frag.glsl");
const render = regl({ const render = regl({
@@ -23,14 +25,19 @@ export default ({ regl, cache, config, cameraTex, cameraAspectRatio }, inputs) =
tex: inputs.primary, tex: inputs.primary,
bloomTex: inputs.bloom, bloomTex: inputs.bloom,
cameraTex, cameraTex,
clicks: () => clicks, // REGL bug can misinterpret array uniforms
["clicks[0]"]: () => clicks[0],
["clicks[1]"]: () => clicks[1],
["clicks[2]"]: () => clicks[2],
["clicks[3]"]: () => clicks[3],
["clicks[4]"]: () => clicks[4],
aspectRatio: () => aspectRatio, aspectRatio: () => aspectRatio,
cameraAspectRatio, cameraAspectRatio,
}, },
framebuffer: output, framebuffer: output,
}); });
start = Date.now(); start = performance.now();
return makePass( return makePass(
{ {

View File

@@ -24,7 +24,7 @@ const effects = {
export default class REGLRenderer extends Renderer { export default class REGLRenderer extends Renderer {
#tick; #renderFunc;
#regl; #regl;
#glMatrix; #glMatrix;
@@ -43,26 +43,24 @@ export default class REGLRenderer extends Renderer {
}); });
} }
async formulate(config) { async configure(config) {
await super.formulate(config); await super.configure(config);
const canvas = this.canvas;
const cache = this.cache;
const regl = this.#regl;
const glMatrix = this.#glMatrix;
const dimensions = { width: 1, height: 1 };
if (config.useCamera) { if (config.useCamera) {
await setupCamera(); await setupCamera();
} }
const canvas = this.canvas;
const cache = this.cache;
const regl = this.#regl;
const glMatrix = this.#glMatrix;
const dimensions = { width: 1, height: 1 };
const cameraTex = regl.texture(cameraCanvas); const cameraTex = regl.texture(cameraCanvas);
// All this takes place in a full screen quad. // All this takes place in a full screen quad.
const fullScreenQuad = makeFullScreenQuad(regl); const fullScreenQuad = makeFullScreenQuad(regl);
const effectName = config.effect in effects ? config.effect : "palette"; const effectName = config.effect in effects ? config.effect : "palette";
const context = { regl, cache, config, cameraTex, cameraAspectRatio, glMatrix }; const context = { regl, canvas, cache, config, cameraTex, cameraAspectRatio, glMatrix };
const pipeline = makePipeline(context, [makeRain, makeBloomPass, effects[effectName]]); const pipeline = makePipeline(context, [makeRain, makeBloomPass, effects[effectName]]);
const screenUniforms = { tex: pipeline[pipeline.length - 1].outputs.primary }; const screenUniforms = { tex: pipeline[pipeline.length - 1].outputs.primary };
@@ -75,17 +73,15 @@ export default class REGLRenderer extends Renderer {
const targetFrameTimeMilliseconds = 1000 / config.fps; const targetFrameTimeMilliseconds = 1000 / config.fps;
let last = NaN; let last = NaN;
resetREGLTime: { const reset = regl.frame((reglContext) => {
const reset = regl.frame((o) => { reglContext.tick = 0;
o.time = 0;
o.tick = 0;
reset.cancel(); reset.cancel();
}); });
}
const tick = regl.frame(({ viewportWidth, viewportHeight }) => { this.#renderFunc = (reglContext) => {
if (config.once) { if (config.once) {
tick.cancel(); this.stop();
} }
const now = regl.now() * 1000; const now = regl.now() * 1000;
@@ -106,6 +102,7 @@ export default class REGLRenderer extends Renderer {
if (config.useCamera) { if (config.useCamera) {
cameraTex(cameraCanvas); cameraTex(cameraCanvas);
} }
const {viewportWidth, viewportHeight} = reglContext;
if (dimensions.width !== viewportWidth || dimensions.height !== viewportHeight) { if (dimensions.width !== viewportWidth || dimensions.height !== viewportHeight) {
dimensions.width = viewportWidth; dimensions.width = viewportWidth;
dimensions.height = viewportHeight; dimensions.height = viewportHeight;
@@ -119,20 +116,31 @@ export default class REGLRenderer extends Renderer {
} }
drawToScreen(); drawToScreen();
}); });
}); };
if (this.#tick != null) { const frame = this.#regl.frame(o => {
this.#tick.cancel(); this.#renderFunc(o);
frame.cancel();
});
} }
this.#tick = tick; stop() {
super.stop();
this.#renderFunc = null;
}
update(now) {
if (this.#renderFunc != null) {
const frame = this.#regl.frame(o => {
this.#renderFunc(o);
frame.cancel();
})
}
super.update(now);
} }
destroy() { destroy() {
if (this.destroyed) { if (this.destroyed) return;
return;
}
this.#tick.cancel(); // stop RAF
this.#regl.destroy(); // releases all GPU resources & event listeners this.#regl.destroy(); // releases all GPU resources & event listeners
super.destroy(); super.destroy();
} }

View File

@@ -11,6 +11,7 @@ export default class Renderer {
#fullscreen = false; #fullscreen = false;
#cache = new Map(); #cache = new Map();
#destroyed = false; #destroyed = false;
#running = false;
constructor(type, ready) { constructor(type, ready) {
this.#type = type; this.#type = type;
@@ -18,48 +19,46 @@ export default class Renderer {
this.#ready = Renderer.libraries.then(libraries => { this.#ready = Renderer.libraries.then(libraries => {
this.#cache = new Map(libraries.staticAssets); this.#cache = new Map(libraries.staticAssets);
}).then(ready); }).then(ready);
this.#ready.then(() => this.start());
} }
get canvas() { get running() { return this.#running; }
return this.#canvas;
start() {
this.#running = true;
this.update();
} }
get cache() { stop() {
return this.#cache; this.#running = false;
} }
get type () { update(now) {
return this.#type; if (!this.#running) return;
requestAnimationFrame(now => this.update(now));
} }
get ready () { get canvas() { return this.#canvas; }
return this.#ready;
}
get size() { get cache() { return this.#cache; }
return [this.#width, this.#height];
} get type () { return this.#type; }
get ready () { return this.#ready; }
get size() { return ([this.#width, this.#height]); }
set size([width, height]) { set size([width, height]) {
[width, height] = [Math.ceil(width), Math.ceil(height)]; [width, height] = [Math.ceil(width), Math.ceil(height)];
if (width === this.#width && height === this.#height) { if (width === this.#width && height === this.#height) return;
return;
}
[this.#canvas.width, this.#canvas.height] = [this.#width, this.#height] = [width, height]; [this.#canvas.width, this.#canvas.height] = [this.#width, this.#height] = [width, height];
} }
get fullscreen() { get fullscreen() { return this.#fullscreen; }
return this.#fullscreen;
}
set fullscreen(value) { set fullscreen(value) {
if (!!value === this.#fullscreen) { if (!!value === this.#fullscreen) return;
return; if (!document.fullscreenEnabled && !document.webkitFullscreenEnabled) return;
}
if (!document.fullscreenEnabled && !document.webkitFullscreenEnabled) {
return;
}
this.#fullscreen = value; this.#fullscreen = value;
if (document.fullscreenElement != null) { if (document.fullscreenElement != null) {
@@ -74,18 +73,17 @@ export default class Renderer {
} }
} }
async formulate(config) { async configure(config) {
await this.ready; await this.ready;
if (this.destroyed) { if (this.destroyed) {
throw new Error("Cannot formulate a destroyed rain instance."); throw new Error("Cannot configure a destroyed rain instance.");
} }
} }
get destroyed() { get destroyed() { return this.#destroyed; }
return this.#destroyed;
}
destroy() { destroy() {
this.stop();
this.#destroyed = true; this.#destroyed = true;
this.#cache.clear(); this.#cache.clear();
} }

View File

@@ -26,7 +26,7 @@ const makePyramid = (device, size, pyramidHeight) =>
.map((_, index) => .map((_, index) =>
makeComputeTarget( makeComputeTarget(
device, 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) => { const build = (screenSize, inputs) => {
// Since the bloom is blurry, we downscale everything // 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); destroyPyramid(hBlurPyramid);
hBlurPyramid = makePyramid(device, scaledScreenSize, pyramidHeight); hBlurPyramid = makePyramid(device, scaledScreenSize, pyramidHeight);
@@ -169,8 +169,8 @@ export default ({ config, device, cache }) => {
computePass.setPipeline(blurPipeline); computePass.setPipeline(blurPipeline);
for (let i = 0; i < pyramidHeight; i++) { for (let i = 0; i < pyramidHeight; i++) {
const dispatchSize = [ const dispatchSize = [
Math.ceil(Math.floor(scaledScreenSize[0] * 2 ** -i) / 32), Math.max(1, Math.ceil(Math.floor(scaledScreenSize[0] * 2 ** -i) / 32)),
Math.floor(Math.floor(scaledScreenSize[1] * 2 ** -i)), Math.max(1, Math.floor(Math.floor(scaledScreenSize[1] * 2 ** -i))),
1, 1,
]; ];
computePass.setBindGroup(0, hBlurBindGroups[i]); computePass.setBindGroup(0, hBlurBindGroups[i]);

View File

@@ -49,7 +49,7 @@ export default ({ device, cache, canvasFormat, canvasContext }) => {
nearestSampler, nearestSampler,
inputs.primary.createView(), inputs.primary.createView(),
]); ]);
return null; return {};
}; };
const run = (encoder, shouldRender) => { const run = (encoder, shouldRender) => {

View File

@@ -7,24 +7,7 @@ import {
makePass, makePass,
} from "./utils.js"; } from "./utils.js";
let start; export default ({ config, device, canvas, cache, cameraTex, cameraAspectRatio, timeBuffer }) => {
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 }) => {
const assets = [loadShader(device, cache, "shaders/wgsl/mirrorPass.wgsl")]; const assets = [loadShader(device, cache, "shaders/wgsl/mirrorPass.wgsl")];
const linearSampler = device.createSampler({ const linearSampler = device.createSampler({
@@ -32,6 +15,24 @@ export default ({ config, device, cache, cameraTex, cameraAspectRatio, timeBuffe
minFilter: "linear", 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 computePipeline;
let configBuffer; let configBuffer;
let sceneUniforms; let sceneUniforms;
@@ -109,7 +110,7 @@ export default ({ config, device, cache, cameraTex, cameraAspectRatio, timeBuffe
computePass.end(); computePass.end();
}; };
start = Date.now(); start = performance.now();
return makePass("Mirror", loaded, build, run); return makePass("Mirror", loaded, build, run);
}; };

View File

@@ -27,47 +27,59 @@ const effects = {
export default class REGLRenderer extends Renderer { export default class REGLRenderer extends Renderer {
#glMatrix; #glMatrix;
#canvasContext;
#adapter;
#device; #device;
#renderLoop; #canvasContext;
#canvasFormat;
#renderFunc;
#renewingDevice;
#configureIndex = 0;
#rebuildingPipeline;
constructor() { constructor() {
super("webgpu", async () => { super("webgpu", async () => {
const libraries = await Renderer.libraries; const libraries = await Renderer.libraries;
this.#glMatrix = libraries.glMatrix; 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) { async configure(config) {
await super.formulate(config); const index = ++this.#configureIndex;
await super.configure(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;
if (config.useCamera) { if (config.useCamera) {
await setupCamera(); await setupCamera();
} }
const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); if (this.#rebuildingPipeline != null) {
await this.#rebuildingPipeline;
}
// console.table(device.limits); const oldDevice = this.#device;
canvasContext.configure({ if (this.#renewingDevice == null) {
device, this.#renewingDevice = (async () => {
format: canvasFormat, this.#canvasContext = this.canvas.getContext("webgpu");
alphaMode: "opaque", this.#canvasFormat = navigator.gpu.getPreferredCanvasFormat();
usage: const adapter = await navigator.gpu.requestAdapter();
// GPUTextureUsage.STORAGE_BINDING | this.#device = await adapter.requestDevice();
GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST, })();
}); }
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 timeUniforms = structs.from(`struct Time { seconds : f32, frames : i32, };`).Time;
const timeBuffer = makeUniformBuffer(device, timeUniforms); const timeBuffer = makeUniformBuffer(device, timeUniforms);
@@ -81,17 +93,17 @@ export default class REGLRenderer extends Renderer {
}); });
const context = { const context = {
glMatrix,
config, config,
cache, cache,
adapter,
device, device,
canvas,
canvasContext, canvasContext,
timeBuffer,
canvasFormat, canvasFormat,
timeBuffer,
cameraTex, cameraTex,
cameraAspectRatio, cameraAspectRatio,
cameraSize, cameraSize,
glMatrix,
}; };
const effectName = config.effect in effects ? config.effect : "palette"; const effectName = config.effect in effects ? config.effect : "palette";
@@ -102,13 +114,28 @@ export default class REGLRenderer extends Renderer {
makeEndPass, 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; const targetFrameTimeMilliseconds = 1000 / config.fps;
let frames = 0; let frames = 0;
let start = NaN; let start = NaN;
let last = NaN; let last = NaN;
let outputs; let outputs;
const renderLoop = (now) => { this.#renderFunc = (now) => {
if (config.once) {
this.stop();
}
if (isNaN(start)) { if (isNaN(start)) {
start = now; start = now;
} }
@@ -125,11 +152,10 @@ export default class REGLRenderer extends Renderer {
} }
} }
const devicePixelRatio = window.devicePixelRatio ?? 1;
const size = this.size; const size = this.size;
const [width, height] = size; const [width, height] = size;
if (outputs == null || canvas.width !== width || canvas.height !== height) { if (outputs == null || dimensions.width !== width || dimensions.height !== height) {
[canvas.width, canvas.height] = size; [dimensions.width, dimensions.height] = size;
outputs = pipeline.build(size); outputs = pipeline.build(size);
} }
@@ -153,26 +179,33 @@ export default class REGLRenderer extends Renderer {
// Eventually, when WebGPU allows it, we'll remove the endPass and just copy from our pipeline's output to the canvas texture. // 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); // encoder.copyTextureToTexture({ texture: outputs?.primary }, { texture: canvasContext.getCurrentTexture() }, canvasSize);
device.queue.submit([encoder.finish()]); device.queue.submit([encoder.finish()]);
if (!config.once) {
requestAnimationFrame(renderLoop);
}
}; };
})();
if (this.#renderLoop != null) { await this.#rebuildingPipeline;
cancelAnimationFrame(this.#renderLoop); this.#renderFunc(performance.now());
if (oldDevice != null) {
oldDevice.destroy();
}
} }
renderLoop(performance.now()); stop() {
this.#renderLoop = renderLoop; super.stop();
this.#renderFunc = null;
}
update(now) {
if (this.#renderFunc != null) {
this.#renderFunc(now);
}
super.update(now);
} }
destroy() { destroy() {
if (this.destroyed) { if (this.destroyed) return;
return; if (this.#device != null) {
}
cancelAnimationFrame(this.#renderLoop); // stop RAF
this.#device.destroy(); // This also destroys any objects created with the device this.#device.destroy(); // This also destroys any objects created with the device
this.#device = null;
}
super.destroy(); super.destroy();
} }
} }

View File

@@ -1,20 +1,23 @@
const loadTexture = async (device, cache, url) => { const loadTexture = async (device, cache, url) => {
const key = url;
if (cache.has(key)) {
return cache.get(key);
}
let texture; const format = "rgba8unorm";
const usage =
if (url == null) {
texture = device.createTexture({
size: [1, 1, 1],
format: "rgba8unorm",
usage:
GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT, GPUTextureUsage.RENDER_ATTACHMENT;
if (url == null) {
return device.createTexture({
size: [1, 1, 1],
format,
usage,
}); });
}
let source;
const key = url;
if (cache.has(key)) {
source = cache.get(key);
} else { } else {
let imageURL; let imageURL;
if (typeof cache.get(`url::${url}`) === "function") { if (typeof cache.get(`url::${url}`) === "function") {
@@ -25,23 +28,17 @@ const loadTexture = async (device, cache, url) => {
const response = await fetch(imageURL); const response = await fetch(imageURL);
const data = await response.blob(); const data = await response.blob();
const source = await createImageBitmap(data); source = await createImageBitmap(data);
const size = [source.width, source.height, 1]; cache.set(key, source);
texture = device.createTexture({
size,
format: "rgba8unorm",
usage:
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
});
device.queue.copyExternalImageToTexture({ source, flipY: true }, { texture }, size);
} }
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; return texture;
}; };
@@ -71,15 +68,17 @@ const makeComputeTarget = (device, size, mipLevelCount = 1) =>
const loadShader = async (device, cache, url) => { const loadShader = async (device, cache, url) => {
const key = url; const key = url;
if (cache.has(key)) {
return cache.get(key);
}
let code; let code;
if (cache.has(key)) {
code = cache.get(key);
} else {
if (typeof cache.get(`raw::${url}`) === "function") { if (typeof cache.get(`raw::${url}`) === "function") {
code = (await cache.get(`raw::${url}`)()).default; code = (await cache.get(`raw::${url}`)()).default;
} else { } else {
code = await (await fetch(url)).text(); code = await (await fetch(url)).text();
} }
cache.set(key, code);
}
return { return {
code, code,
module: device.createShaderModule({ code }), module: device.createShaderModule({ code }),

View File

@@ -11,6 +11,7 @@
"README.md" "README.md"
], ],
"scripts": { "scripts": {
"test": "npm run format && npm run build && vite --config tools/dev.config.js --open /tools/test/index.html",
"dev": "npm run format && vite --config tools/dev.config.js", "dev": "npm run format && vite --config tools/dev.config.js",
"build": "npm run format && rm -rf ./dist/* && vite build --config tools/build/core.config.js && vite build --config tools/build/full.config.js", "build": "npm run format && rm -rf ./dist/* && vite build --config tools/build/core.config.js && vite build --config tools/build/full.config.js",
"format": "eslint . && prettier --write --no-error-on-unmatched-pattern 'src/**/*.{js,jsx,mjs,json}' '*.{js,jsx,mjs,json,html}' 'assets/**/*.{json,css}'" "format": "eslint . && prettier --write --no-error-on-unmatched-pattern 'src/**/*.{js,jsx,mjs,json}' '*.{js,jsx,mjs,json,html}' 'assets/**/*.{json,css}'"

View File

@@ -283,7 +283,6 @@ fn computeSymbol (simTime : f32, isFirstFrame : bool, glyphPos : vec2<f32>, scre
var symbol = previousSymbol; var symbol = previousSymbol;
if (time.frames % config.cycleFrameSkip == 0) { if (time.frames % config.cycleFrameSkip == 0) {
age += cycleSpeed * f32(config.cycleFrameSkip); age += cycleSpeed * f32(config.cycleFrameSkip);
var advance = floor(age);
if (age > 1.0) { if (age > 1.0) {
symbol = floor(config.glyphSequenceLength * randomFloat(screenPos + simTime)); symbol = floor(config.glyphSequenceLength * randomFloat(screenPos + simTime));
age = fract(age); age = fract(age);

View File

@@ -24,7 +24,7 @@
const renderer = new RendererClass(); const renderer = new RendererClass();
await renderer.ready; await renderer.ready;
document.querySelector("#test-core-bundled").appendChild(renderer.canvas); document.querySelector("#test-core-bundled").appendChild(renderer.canvas);
await renderer.formulate(makeConfig({once: false})); await renderer.configure(makeConfig({once: false}));
</script> </script>
</details> </details>
<details id="test-core-react-bundled"> <details id="test-core-react-bundled">
@@ -41,7 +41,7 @@
const renderer = new RendererClass(); const renderer = new RendererClass();
await renderer.ready; await renderer.ready;
document.querySelector("#test-full-bundled").appendChild(renderer.canvas); document.querySelector("#test-full-bundled").appendChild(renderer.canvas);
await renderer.formulate(makeConfig({once: false, version: "twilight"})); await renderer.configure(makeConfig({once: false, version: "twilight"}));
</script> </script>
</details> </details>
</body> </body>

View File

@@ -38,7 +38,7 @@ const App = () => {
const [resolution, setResolution] = useState(0.75); const [resolution, setResolution] = useState(0.75);
const [cursorColor, setCursorColor] = useState(null); const [cursorColor, setCursorColor] = useState(null);
const [backgroundColor, setBackgroundColor] = useState("0,0,0"); const [backgroundColor, setBackgroundColor] = useState("0,0,0");
const [rendererType, setRendererType] = useState(null); const [rendererType, setRendererType] = useState("webgpu");
const [density, setDensity] = useState(2); const [density, setDensity] = useState(2);
const [destroyed, setDestroyed] = useState(false); const [destroyed, setDestroyed] = useState(false);
const onVersionButtonClick = () => { const onVersionButtonClick = () => {
@@ -68,7 +68,7 @@ const App = () => {
setBackgroundColor(null); setBackgroundColor(null);
}; };
const onRendererButtonClick = () => { const onRendererButtonClick = () => {
setRendererType(() => (rendererType === "webgpu" ? "regl" : "webgpu")); setRendererType(rendererType === "webgpu" ? "regl" : "webgpu");
}; };
const onDestroyButtonClick = () => { const onDestroyButtonClick = () => {
setDestroyed(true); setDestroyed(true);
@@ -76,10 +76,13 @@ const App = () => {
return ( return (
<> <>
<div>
<button onClick={onVersionButtonClick}>Version: "{version}"</button> <button onClick={onVersionButtonClick}>Version: "{version}"</button>
<button onClick={onEffectButtonClick}>Effect: "{effect}"</button> <button onClick={onEffectButtonClick}>Effect: "{effect}"</button>
<button onClick={onRendererButtonClick}>Renderer: {rendererType ?? "default (regl)"}</button> <button onClick={onRendererButtonClick}>Renderer: {rendererType ?? "default (regl)"}</button>
<button onClick={onDestroyButtonClick}>Destroy</button> <button onClick={onDestroyButtonClick}>Destroy</button>
</div>
<div>
<label htmlFor="cursor-color">Cursor color: </label> <label htmlFor="cursor-color">Cursor color: </label>
<input <input
name="cursor-color" name="cursor-color"
@@ -104,6 +107,8 @@ const App = () => {
setBackgroundColor(values); setBackgroundColor(values);
}} }}
/> />
</div>
<div>
<label htmlFor="num-columns"># of columns:</label> <label htmlFor="num-columns"># of columns:</label>
<input <input
name="num-columns" name="num-columns"
@@ -124,6 +129,7 @@ const App = () => {
step="0.01" step="0.01"
onInput={(e) => setResolution(parseFloat(e.target.value))} onInput={(e) => setResolution(parseFloat(e.target.value))}
/> />
</div>
{!destroyed && ( {!destroyed && (
<Matrix <Matrix