Testing hot-swapping renderers, which requires destroying and rebuilding the canvas after all. Fixed a few other related bugs and moved the imports into "bundle-contents.js".

This commit is contained in:
Rezmason
2025-05-08 12:52:48 -07:00
parent a1332d8f1a
commit 319b53919b
7 changed files with 84 additions and 28 deletions

View File

@@ -1,13 +1,6 @@
import inclusions from "./inclusions";
import React, { useEffect, useState, useRef, memo } from "react";
import * as reglRenderer from "./regl/main";
import * as webgpuRenderer from "./webgpu/main";
import makeConfig from "./utils/config";
globalThis.inclusions = inclusions;
console.log(webgpuRenderer.init, webgpuRenderer.formulate, webgpuRenderer.destroy);
/**
* @typedef {object} Colour
* @property {"hsl"|"rgb"} space
@@ -115,33 +108,64 @@ export const Matrix = memo((props) => {
const { style, className, ...rest } = props;
const elProps = { style, className };
const matrix = useRef(null);
const [rain, setRain] = useState(null);
const [rCanvas, setCanvas] = useState(null);
const [rRenderer, setRenderer] = useState(null);
const [rRain, setRain] = useState(null);
const supportsWebGPU = () => {
return (
window.GPUQueue != null &&
navigator.gpu != null &&
navigator.gpu.getPreferredCanvasFormat != null
);
};
useEffect(() => {
const useWebGPU = supportsWebGPU() && ["webgpu"].includes(rest.renderer?.toLowerCase());
const isWebGPU = rRenderer?.type === "webgpu";
if (rRenderer != null && useWebGPU === isWebGPU) {
return;
}
if (rCanvas != null) {
matrix.current.removeChild(rCanvas);
setCanvas(null);
}
if (rRain != null) {
rRenderer?.destroy(rRain);
setRain(null);
}
if (rRenderer != null) {
setRenderer(null);
}
const canvas = document.createElement("canvas");
matrix.current.appendChild(canvas);
canvas.style.width = "100%";
canvas.style.height = "100%";
const init = async () => {
setRain(await reglRenderer.init(canvas));
};
init();
matrix.current.appendChild(canvas);
setCanvas(canvas);
return () => {
reglRenderer.destroy(rain);
setRain(null);
const loadRain = async () => {
const renderer = await import(`./${useWebGPU ? "webgpu" : "regl"}/main.js`);
setRenderer(renderer);
const rain = await renderer.init(canvas);
setRain(rain);
};
}, []);
loadRain();
}, [props.renderer]);
useEffect(() => {
if (rain == null) {
if (rRain == null || rRain.destroyed) {
return;
}
const refresh = async () => {
await reglRenderer.formulate(rain, makeConfig({ ...rest }));
await rRenderer.formulate(rRain, makeConfig({ ...rest }));
};
refresh();
}, [props, rain]);
}, [props, rRain]);
return <div ref={matrix} {...elProps}></div>;
});

8
js/bundle-contents.js Normal file
View File

@@ -0,0 +1,8 @@
import { Matrix } from "./Matrix";
import inclusions from "./inclusions";
import * as reglRenderer from "./regl/main";
import * as webgpuRenderer from "./webgpu/main";
globalThis.inclusions = inclusions;
globalThis.reglRenderer = reglRenderer;
globalThis.webgpuRenderer = webgpuRenderer;
globalThis.Matrix = Matrix;

View File

@@ -1,7 +1,6 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { Matrix } from "./Matrix";
//import { Matrix } from "react-matrix-rain";
const root = createRoot(document.getElementById("root"));
let idx = 1;
@@ -23,6 +22,7 @@ const versions = [
const App = () => {
const [version, setVersion] = React.useState(versions[0]);
const [numColumns, setNumColumns] = React.useState(10);
const [rendererType, setRendererType] = React.useState(null);
const onButtonClick = () => {
setVersion((s) => {
const newVersion = versions[idx];
@@ -36,16 +36,20 @@ const App = () => {
return newColumns;
});
};
const onRendererButtonClick = () => {
setRendererType(() => (rendererType === "webgpu" ? "regl" : "webgpu"));
};
return (
<div>
<h1>Rain</h1>
<button onClick={onButtonClick}>Change</button>
{/* <button onClick={newNum}>change number</button> */}
<button onClick={onButtonClick}>Change properties</button>
<button onClick={onRendererButtonClick}>Renderer: {rendererType ?? "default (regl)"}</button>
<Matrix
style={{ width: "80vw", height: "45vh" }}
version={version}
numColumns={numColumns}
renderer={rendererType}
density={2.0}
/>
</div>

View File

@@ -69,6 +69,9 @@ export const init = async (canvas) => {
};
export const formulate = async (rain, config) => {
if (rain.destroyed) {
throw new Error("Cannot formulate a destroyed rain instance.");
}
const { resize, canvas, cache, regl } = rain;
rain.resolution = config.resolution;
resize();
@@ -150,10 +153,17 @@ export const formulate = async (rain, config) => {
rain.tick = tick;
};
export const destroy = ({ regl, cache, resize, doubleClick, tick, canvas }) => {
export const destroy = (rain) => {
if (rain.destroyed) {
return;
}
const { regl, cache, resize, doubleClick, tick, canvas } = rain;
window.removeEventListener("resize", resize);
window.removeEventListener("dblclick", doubleClick);
cache.clear();
tick.cancel(); // stop RAF
regl.destroy(); // release all GPU resources & event listeners
rain.destroyed = true;
};
export const type = "regl";

View File

@@ -75,6 +75,9 @@ export const init = async (canvas) => {
};
export const formulate = async (rain, config) => {
if (rain.destroyed) {
throw new Error("Cannot formulate a destroyed rain instance.");
}
const { resize, canvas, cache, canvasContext, adapter, device } = rain;
rain.resolution = config.resolution;
resize();
@@ -197,10 +200,18 @@ export const formulate = async (rain, config) => {
rain.renderLoop = renderLoop;
};
export const destroy = ({ device, resize, doubleClick, cache, canvas }) => {
export const destroy = (rain) => {
if (rain.destroyed) {
return;
}
const { device, resize, doubleClick, cache, canvas, renderLoop } = rain;
window.removeEventListener("resize", resize);
window.removeEventListener("dblclick", doubleClick);
cache.clear();
tick.cancel(); // stop RAF
cancelAnimationFrame(renderLoop); // stop RAF
// TODO: destroy WebGPU resources
device.destroy();
rain.destroyed = true;
};
export const type = "webgpu";

View File

@@ -29,7 +29,6 @@ const makeConfigBuffer = (device, configUniforms, config, density, gridSize, gly
};
// console.table(configData);
console.log(configUniforms, configData);
return makeUniformBuffer(device, configUniforms, configData);
};

View File

@@ -9,7 +9,7 @@ import { string } from "rollup-plugin-string";
import image from "@rollup/plugin-image";
export default {
input: "js/Matrix.js",
input: "js/bundle-contents.js",
external: ["react", "react-dom"], // keep them out of your bundle
plugins: [
peerDepsExternal(), // auto-exclude peerDeps