From 319b53919b66c519eeb703a3b9f396b06fdcb7cd Mon Sep 17 00:00:00 2001 From: Rezmason Date: Thu, 8 May 2025 12:52:48 -0700 Subject: [PATCH] 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". --- js/Matrix.js | 64 +++++++++++++++++++++++++++++-------------- js/bundle-contents.js | 8 ++++++ js/index.js | 10 +++++-- js/regl/main.js | 12 +++++++- js/webgpu/main.js | 15 ++++++++-- js/webgpu/rainPass.js | 1 - rollup.config.mjs | 2 +- 7 files changed, 84 insertions(+), 28 deletions(-) create mode 100644 js/bundle-contents.js diff --git a/js/Matrix.js b/js/Matrix.js index b0fbdf3..a2c3fea 100644 --- a/js/Matrix.js +++ b/js/Matrix.js @@ -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
; }); diff --git a/js/bundle-contents.js b/js/bundle-contents.js new file mode 100644 index 0000000..ef1adf5 --- /dev/null +++ b/js/bundle-contents.js @@ -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; diff --git a/js/index.js b/js/index.js index 415b392..45ea819 100644 --- a/js/index.js +++ b/js/index.js @@ -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 (

Rain

- - {/* */} + +
diff --git a/js/regl/main.js b/js/regl/main.js index 4ea3bc5..1c90664 100644 --- a/js/regl/main.js +++ b/js/regl/main.js @@ -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"; diff --git a/js/webgpu/main.js b/js/webgpu/main.js index 669f4e7..ca69d8b 100644 --- a/js/webgpu/main.js +++ b/js/webgpu/main.js @@ -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"; diff --git a/js/webgpu/rainPass.js b/js/webgpu/rainPass.js index 6e4143c..497bd90 100644 --- a/js/webgpu/rainPass.js +++ b/js/webgpu/rainPass.js @@ -29,7 +29,6 @@ const makeConfigBuffer = (device, configUniforms, config, density, gridSize, gly }; // console.table(configData); - console.log(configUniforms, configData); return makeUniformBuffer(device, configUniforms, configData); }; diff --git a/rollup.config.mjs b/rollup.config.mjs index 3ca14f9..cc77b43 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -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