diff --git a/.eslintignore b/.eslintignore index 9a0ecdac..63902168 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,2 @@ build/ -lib/wgputoy types/supabase.ts diff --git a/.github/workflows/cf-pages.yml b/.github/workflows/cf-pages.yml index 44804da0..5ee70f93 100644 --- a/.github/workflows/cf-pages.yml +++ b/.github/workflows/cf-pages.yml @@ -14,7 +14,7 @@ jobs: pull-requests: write deployments: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: echo Deploying ${{ github.event.pull_request.head.sha }} - name: Await CF Pages id: cf-pages diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ae39396f..81361ae5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -35,6 +35,7 @@ jobs: cache: 'yarn' - run: yarn - run: yarn build + - run: npm install -g vercel - name: Deploy to Vercel Action uses: BetaHuhn/deploy-to-vercel-action@v1 with: diff --git a/.gitmodules b/.gitmodules index 0cd43930..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "wgpu-compute-toy"] - path = wgpu-compute-toy - url = https://github.com/compute-toys/wgpu-compute-toy.git diff --git a/components/banner.tsx b/components/banner.tsx deleted file mode 100644 index 82b97a0a..00000000 --- a/components/banner.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import Alert from '@mui/material/Alert'; -import AlertTitle from '@mui/material/AlertTitle'; -import Typography from '@mui/material/Typography'; -import Logo from 'components/global/logo'; -import { theme } from 'theme/theme'; - -export default function Banner() { - return ( - - - - is an experimental editor for{' '} - - WebGPU compute shaders - - . - - At this time, only Chrome ( - v113+) is supported, - as{' '} - - WebGPU is not yet fully supported by other browsers - - . - - - ); -} diff --git a/components/editor/explainer.tsx b/components/editor/explainer.tsx index 9e98f31c..6b44ecb8 100644 --- a/components/editor/explainer.tsx +++ b/components/editor/explainer.tsx @@ -48,9 +48,11 @@ const ExplainerBody = () => { Timing information is in the time struct:
-                time.frame: u32
-                
time.elapsed: f32 +
+ time.delta: f32 +
+ time.frame: u32
Custom uniforms are in the custom struct:
@@ -109,9 +111,17 @@ const ExplainerBody = () => {
                     #dispatch_count ENTRYPOINT N for dispatching an entrypoint
                     multiple times in a row
                 
+                
  • + #dispatch_once ENTRYPOINT for initialization purposes, ensuring + the entrypoint is dispatched only once +
  • #storage NAME TYPE for declaring a storage buffer
  • +
  • + SCREEN_WIDTH and SCREEN_HEIGHT are predefined + variables for accessing the canvas dimensions +
  • Storage

    Read-write storage buffers can be declared using the #storage{' '} @@ -130,31 +140,53 @@ const ExplainerBody = () => { storage texture, which displays the result in the canvas on this page.

    + {/* Debugging assertions are supported with an assert helper function:
                     assert(0, isfinite(col.x))
                     
    assert(1, isfinite(col.y))
    + */}

    Examples

    + + Preprocessor #include + +
    +
    + + Storage usage + +
    +
    + + Texture usage + +
    +
    - Simple single pass shader + Texture pass

    - - Preprocessor #include + + Custom uniforms

    - - Terminal overlay + + Preprocessor #workgroup_count

    - - Storage usage + + Preprocessor #dispatch_once + +
    +
    + + Preprocessor #dispatch_count

    @@ -163,18 +195,28 @@ const ExplainerBody = () => {

    - - Preprocessor #dispatch_count + + Threads execution order

    - - Preprocessor #workgroup_count + + Enable WGSL extension + +
    +
    + + Terminal text overlay + +
    +
    + + Simple raymarcher

    - - Assert + + Simple rasterizer

    diff --git a/components/wgputoy.tsx b/components/wgputoy.tsx index bfbd9c35..b5df6b50 100644 --- a/components/wgputoy.tsx +++ b/components/wgputoy.tsx @@ -7,7 +7,6 @@ import { canvasElAtom, wgpuAvailabilityAtom } from 'lib/atoms/wgputoyatoms'; import dynamic from 'next/dynamic'; import { Suspense, useCallback, useState } from 'react'; import { theme } from 'theme/theme'; -import Logo from './global/logo'; export const WgpuToyWrapper = props => { const setCanvasEl = useSetAtom(canvasElAtom); @@ -68,8 +67,13 @@ export const WgpuToyWrapper = props => { WebGPU support was not detected in your browser. - For information on how to set up your browser to run WebGPU code, please see - the instructions linked on the homepage. + + Click here + {' '} + for further information about supported browsers. )} diff --git a/components/wgputoycontroller.tsx b/components/wgputoycontroller.tsx index 4c269e5b..ba7acd7d 100644 --- a/components/wgputoycontroller.tsx +++ b/components/wgputoycontroller.tsx @@ -26,13 +26,8 @@ import { titleAtom, widthAtom } from 'lib/atoms/atoms'; -import { - canvasElAtom, - canvasParentElAtom, - isSafeContext, - wgputoyAtom, - wgputoyPreludeAtom -} from 'lib/atoms/wgputoyatoms'; +import { canvasElAtom, canvasParentElAtom, wgputoyPreludeAtom } from 'lib/atoms/wgputoyatoms'; +import { ComputeEngine } from 'lib/engine'; import { useCallback, useEffect } from 'react'; import { theme } from 'theme/theme'; import { getDimensions } from 'types/canvasdimensions'; @@ -45,6 +40,7 @@ declare global { } const needsInitialResetAtom = atom(false); +const performingInitialResetAtom = atom(false); /* Controller component. Returns null because we expect to be notified @@ -65,6 +61,9 @@ const WgpuToyController = props => { const [sliderUpdateSignal, setSliderUpdateSignal] = useTransientAtom(sliderUpdateSignalAtom); const [manualReload, setManualReload] = useTransientAtom(manualReloadAtom); const [needsInitialReset, setNeedsInitialReset] = useTransientAtom(needsInitialResetAtom); + const [performingInitialReset, setPerformingInitialReset] = useTransientAtom( + performingInitialResetAtom + ); const [isPlaying, setIsPlaying] = useTransientAtom(isPlayingAtom); const [codeHot] = useTransientAtom(codeAtom); const [dbLoaded] = useTransientAtom(dbLoadedAtom); @@ -76,19 +75,18 @@ const WgpuToyController = props => { // "hot" access and effect hook access for code const code = useAtomValue(codeAtom); - const [parseError, setParseError] = useTransientAtom(parseErrorAtom); + const [, setParseError] = useTransientAtom(parseErrorAtom); const loadedTextures = useAtomValue(loadedTexturesAtom); const setEntryPoints = useSetAtom(entryPointsAtom); const setSaveColorTransitionSignal = useSetAtom(saveColorTransitionSignalAtom); - const wgputoy = useAtomValue(wgputoyAtom); const canvas = useAtomValue(canvasElAtom); const [, setPrelude] = useAtom(wgputoyPreludeAtom); const parentRef = useAtomValue(canvasParentElAtom); const [width, setWidth] = useTransientAtom(widthAtom); - const [, setHeight] = useTransientAtom(heightAtom); + const [height, setHeight] = useTransientAtom(heightAtom); const [scale, setScale] = useTransientAtom(scaleAtom); const [requestFullscreenSignal, setRequestFullscreenSignal] = useAtom(requestFullscreenAtom); @@ -96,50 +94,30 @@ const WgpuToyController = props => { const halfResolution = useAtomValue(halfResolutionAtom); const updateUniforms = useCallback(async () => { - if (isSafeContext(wgputoy)) { - const names: string[] = []; - const values: number[] = []; - [...sliderRefMap().keys()].map(uuid => { - names.push(sliderRefMap().get(uuid).getUniform()); - values.push(sliderRefMap().get(uuid).getVal()); - }, this); - if (names.length > 0) { - await wgputoy.set_custom_floats(names, Float32Array.from(values)); - } - setSliderUpdateSignal(false); + const names: string[] = []; + const values: number[] = []; + [...sliderRefMap().keys()].map(uuid => { + names.push(sliderRefMap().get(uuid).getUniform()); + values.push(sliderRefMap().get(uuid).getVal()); + }, this); + if (names.length > 0) { + // console.log(`Setting uniforms: ${names} with values: ${values}`); + await ComputeEngine.getInstance().setCustomFloats(names, Float32Array.from(values)); } + setSliderUpdateSignal(false); }, []); - const reloadCallback = useCallback(() => { - updateUniforms().then(() => { - if (isSafeContext(wgputoy)) { - wgputoy.preprocess(codeHot()).then(s => { - if (s) { - wgputoy.compile(s); - setPrelude(wgputoy.prelude()); - wgputoy.render(); - } - }); - setManualReload(false); - } - }); - }, []); - - const awaitableReloadCallback = async () => { - return updateUniforms().then(() => { - if (isSafeContext(wgputoy)) { - wgputoy.preprocess(codeHot()).then(s => { - if (s) { - wgputoy.compile(s); - setPrelude(wgputoy.prelude()); - wgputoy.render(); - } - }); - return true; - } else { - return false; - } - }); + const recompile = async () => { + await updateUniforms(); + console.log('Recompiling shader...'); + const s = await ComputeEngine.getInstance().preprocess(codeHot()); + if (s) { + await ComputeEngine.getInstance().compile(s); + setPrelude(ComputeEngine.getInstance().getPrelude()); + ComputeEngine.getInstance().render(); + } else { + console.error('Recompilation failed'); + } }; /* @@ -147,42 +125,65 @@ const WgpuToyController = props => { where manualReload gets set before the controller is loaded, which results in the effect hook for manualReload never getting called. */ - const liveReloadCallback = useCallback(() => { - if (needsInitialReset() && dbLoaded()) { - awaitableReloadCallback().then(ready => { - // we don't want to reset in general except on load - if (ready && parseError().success) { - resetCallback(); - setNeedsInitialReset(false); - } - }); - } else if (dbLoaded() && manualReload()) { - reloadCallback(); + useAnimationFrame(async e => { + if (sliderUpdateSignal() && !needsInitialReset()) { + await updateUniforms(); } - }, []); - - useAnimationFrame(e => { - if (isSafeContext(wgputoy)) { - if (sliderUpdateSignal()) { - updateUniforms().then(() => { - liveReloadCallback(); - }); - } else { - liveReloadCallback(); + if (performingInitialReset()) { + // wait for initial reset to complete + } else if (needsInitialReset() && dbLoaded()) { + console.log('Initialising engine...'); + setPerformingInitialReset(true); + await ComputeEngine.create(); + const engine = ComputeEngine.getInstance(); + if (!canvas) { + console.error('Canvas not found'); + return; } - if (sliderUpdateSignal() && !isPlaying()) { - wgputoy.set_time_delta(e.delta); - wgputoy.render(); - } else if (isPlaying() || manualReload()) { - let t = timer(); - if (!manualReload()) { - t += e.delta; - } - setTimer(t); - wgputoy.set_time_elapsed(t); - wgputoy.set_time_delta(e.delta); - wgputoy.render(); + engine.setSurface(canvas); + engine.onSuccess(handleSuccess); + engine.onError(handleError); + setTimer(0); + engine.setPassF32(float32Enabled); + updateResolution(); + engine.resize(width(), height(), scale()); + engine.reset(); + loadTexture(0, loadedTextures[0].img); + loadTexture(1, loadedTextures[1].img); + await updateUniforms(); + console.log('Compiling shader...'); + const s = await engine.preprocess(codeHot()); + if (!s) { + console.error('Initialisation aborted: shader compilation failed'); + return; + } + await engine.compile(s); + setPrelude(engine.getPrelude()); + engine.render(); + setManualReload(false); + setNeedsInitialReset(false); + setPerformingInitialReset(false); + console.log('Initialisation complete'); + } else if (dbLoaded() && manualReload()) { + console.log('Manual reload triggered'); + await recompile(); + setManualReload(false); + } + if (needsInitialReset()) { + return; + } + if (sliderUpdateSignal() && !isPlaying()) { + ComputeEngine.getInstance().setTimeDelta(e.delta); + ComputeEngine.getInstance().render(); + } else if (isPlaying() || manualReload()) { + let t = timer(); + if (!manualReload()) { + t += e.delta; } + setTimer(t); + ComputeEngine.getInstance().setTimeElapsed(t); + ComputeEngine.getInstance().setTimeDelta(e.delta); + ComputeEngine.getInstance().render(); } }); @@ -195,10 +196,11 @@ const WgpuToyController = props => { }, []); const resetCallback = useCallback(() => { - if (isSafeContext(wgputoy)) { + if (!needsInitialReset()) { + console.log('Resetting engine...'); setTimer(0); - wgputoy.reset(); - reloadCallback(); + ComputeEngine.getInstance().reset(); + recompile(); } }, []); @@ -221,34 +223,31 @@ const WgpuToyController = props => { if (!hotReloadHot()) setSaveColorTransitionSignal(theme.palette.dracula.orange); }, []); - if (window) window['wgsl_error_handler'] = handleError; + // if (window) window['wgsl_error_handler'] = handleError; const loadTexture = useCallback((index: number, uri: string) => { - if (isSafeContext(wgputoy)) { - fetch(uri) - .then(response => { - if (!response.ok) { - throw new Error('Failed to load image'); - } - return response.blob(); - }) - .then(b => b.arrayBuffer()) - .then(data => { - if (uri.match(/\.hdr/i)) { - wgputoy.load_channel_hdr(index, new Uint8Array(data)); - } else { - wgputoy.load_channel(index, new Uint8Array(data)); - } - }) - .catch(error => console.error(error)); - } + console.log(`Loading texture ${index} from ${uri}`); + fetch(uri) + .then(response => { + if (!response.ok) { + throw new Error('Failed to load image'); + } + return response.blob(); + }) + .then(b => b.arrayBuffer()) + .then(data => { + if (uri.match(/\.hdr/i)) { + ComputeEngine.getInstance().loadChannelHDR(index, new Uint8Array(data)); + } else { + ComputeEngine.getInstance().loadChannel(index, new Uint8Array(data)); + } + }) + .catch(error => console.error(error)); }, []); const requestFullscreen = useCallback(() => { - if (isSafeContext(wgputoy) && canvas !== false) { - if (!document.fullscreenElement) { - canvas.requestFullscreen({ navigationUI: 'hide' }); - } + if (canvas && !document.fullscreenElement) { + canvas.requestFullscreen({ navigationUI: 'hide' }); } }, []); @@ -257,9 +256,9 @@ const WgpuToyController = props => { useEffect(() => { const handleKeyDown = e => { - if (isSafeContext(wgputoy)) { - if (typeof e.keyCode === 'number') wgputoy.set_keydown(e.keyCode, true); - } + // console.log(`Key down: ${e.keyCode}`); + if (typeof e.keyCode === 'number') + ComputeEngine.getInstance().setKeydown(e.keyCode, true); }; if (canvas) { canvas.addEventListener('keydown', handleKeyDown); @@ -270,9 +269,9 @@ const WgpuToyController = props => { useEffect(() => { const handleKeyUp = e => { - if (isSafeContext(wgputoy)) { - if (typeof e.keyCode === 'number') wgputoy.set_keydown(e.keyCode, false); - } + // console.log(`Key up: ${e.keyCode}`); + if (typeof e.keyCode === 'number') + ComputeEngine.getInstance().setKeydown(e.keyCode, false); }; if (canvas) { canvas.addEventListener('keyup', handleKeyUp); @@ -399,30 +398,27 @@ const WgpuToyController = props => { useEffect(() => { if (canvas !== false) { const handleMouseMove = (e: MouseEvent) => { - if (isSafeContext(wgputoy)) { - wgputoy.set_mouse_pos( - e.offsetX / canvas.clientWidth, - e.offsetY / canvas.clientHeight - ); - if (!isPlaying()) { - wgputoy.render(); - } + // console.log(`Mouse move: ${e.offsetX}, ${e.offsetY}`); + ComputeEngine.getInstance().setMousePos( + e.offsetX / canvas.clientWidth, + e.offsetY / canvas.clientHeight + ); + if (!isPlaying()) { + ComputeEngine.getInstance().render(); } }; const handleMouseUp = () => { - if (isSafeContext(wgputoy)) { - wgputoy.set_mouse_click(false); - canvas.onmousemove = null; - } + // console.log('Mouse up'); + ComputeEngine.getInstance().setMouseClick(false); + canvas.onmousemove = null; }; const handleMouseDown = (e: MouseEvent) => { - if (isSafeContext(wgputoy)) { - wgputoy.set_mouse_click(true); - handleMouseMove(e); - canvas.onmousemove = handleMouseMove; - } + // console.log('Mouse down'); + ComputeEngine.getInstance().setMouseClick(true); + handleMouseMove(e); + canvas.onmousemove = handleMouseMove; }; canvas.onmousedown = handleMouseDown; @@ -431,12 +427,6 @@ const WgpuToyController = props => { } }, []); - useEffect(() => { - if (isSafeContext(wgputoy)) { - wgputoy.on_success(handleSuccess); - } - }, []); - useEffect(() => { if (!isPlaying()) { setPlay(true); @@ -462,56 +452,64 @@ const WgpuToyController = props => { special case where we're paused and a reload is called */ if (hotReload || (!isPlaying() && manualReload())) { - reloadCallback(); + console.log('Hot reload triggered...'); + recompile().then(() => setManualReload(false)); } }, [code, hotReload, manualReload()]); const updateResolution = () => { - if (isSafeContext(wgputoy)) { - let dimensions = { x: 0, y: 0 }; // dimensions in device (physical) pixels - if (document.fullscreenElement) { - // calculate actual screen resolution, accounting for both zoom and hidpi - // https://stackoverflow.com/a/55839671/78204 - dimensions.x = - Math.round( - (window.screen.width * window.devicePixelRatio) / - (window.outerWidth / window.innerWidth) / - 80 - ) * 80; - dimensions.y = - Math.round( - (window.screen.height * window.devicePixelRatio) / - (window.outerWidth / window.innerWidth) / - 60 - ) * 60; - } else if (props.embed) { - dimensions = getDimensions(window.innerWidth * window.devicePixelRatio); - } else { - const padding = 16; - dimensions = getDimensions( - (parentRef.offsetWidth - padding) * window.devicePixelRatio - ); - } - const newScale = halfResolution ? 0.5 : 1; - if (dimensions.x !== width() || newScale !== scale()) { - setWidth(dimensions.x); - setHeight(dimensions.y); - setScale(newScale); - wgputoy.resize(dimensions.x, dimensions.y, newScale); - reloadCallback(); - } - if (canvas) { - canvas.width = dimensions.x; - canvas.height = dimensions.y; - canvas.style.width = `${dimensions.x / window.devicePixelRatio}px`; - canvas.style.height = `${dimensions.y / window.devicePixelRatio}px`; - } + let dimensions = { x: 0, y: 0 }; // dimensions in device (physical) pixels + if (document.fullscreenElement) { + // calculate actual screen resolution, accounting for both zoom and hidpi + // https://stackoverflow.com/a/55839671/78204 + dimensions.x = + Math.round( + (window.screen.width * window.devicePixelRatio) / + (window.outerWidth / window.innerWidth) / + 80 + ) * 80; + dimensions.y = + Math.round( + (window.screen.height * window.devicePixelRatio) / + (window.outerWidth / window.innerWidth) / + 60 + ) * 60; + } else if (props.embed) { + dimensions = getDimensions(window.innerWidth * window.devicePixelRatio); + } else { + const padding = 16; + dimensions = getDimensions((parentRef.offsetWidth - padding) * window.devicePixelRatio); + } + if (canvas) { + canvas.width = dimensions.x; + canvas.height = dimensions.y; + canvas.style.width = `${dimensions.x / window.devicePixelRatio}px`; + canvas.style.height = `${dimensions.y / window.devicePixelRatio}px`; } + const newScale = halfResolution ? 0.5 : 1; + if (dimensions.x !== width() || newScale !== scale()) { + console.log(`Resizing to ${dimensions.x}x${dimensions.y} @ ${newScale}x`); + setWidth(dimensions.x); + setHeight(dimensions.y); + setScale(newScale); + return true; + } + return false; }; - useResizeObserver(parentRef, updateResolution); + useResizeObserver(parentRef, () => { + if (!needsInitialReset() && updateResolution()) { + ComputeEngine.getInstance().resize(width(), height(), scale()); + resetCallback(); + } + }); - useEffect(updateResolution, [halfResolution]); + useEffect(() => { + if (!needsInitialReset() && updateResolution()) { + ComputeEngine.getInstance().resize(width(), height(), scale()); + resetCallback(); + } + }, [halfResolution]); useEffect(() => { if (reset) { @@ -521,11 +519,15 @@ const WgpuToyController = props => { }, [reset]); useEffect(() => { - loadTexture(0, loadedTextures[0].img); + if (!needsInitialReset()) { + loadTexture(0, loadedTextures[0].img); + } }, [loadedTextures[0]]); useEffect(() => { - loadTexture(1, loadedTextures[1].img); + if (!needsInitialReset()) { + loadTexture(1, loadedTextures[1].img); + } }, [loadedTextures[1]]); useEffect(() => { @@ -536,10 +538,12 @@ const WgpuToyController = props => { }, [requestFullscreenSignal]); useEffect(() => { - if (isSafeContext(wgputoy)) { - wgputoy.set_pass_f32(float32Enabled); + if (!needsInitialReset()) { + console.log(`Setting passF32 to ${float32Enabled}`); + ComputeEngine.getInstance().setPassF32(float32Enabled); + ComputeEngine.getInstance().reset(); if (dbLoaded()) { - awaitableReloadCallback().then(() => { + recompile().then(() => { resetCallback(); }); } diff --git a/lib/atoms/wgputoyatoms.tsx b/lib/atoms/wgputoyatoms.tsx index 75772fe6..d22f523c 100644 --- a/lib/atoms/wgputoyatoms.tsx +++ b/lib/atoms/wgputoyatoms.tsx @@ -1,16 +1,5 @@ 'use client'; import { atom } from 'jotai'; -import { create_renderer, WgpuToyRenderer } from 'lib/wgputoy'; -import { getDimensions } from '../../types/canvasdimensions'; - -// just to check if the object has already been freed (ptr=0) -declare module 'lib/wgputoy' { - interface WgpuToyRenderer { - __wbg_ptr: number; - } -} - -const isSSR = typeof window === 'undefined'; // Using 'false' here to satisfy type checker for Jotai's function overloads export const canvasElAtom = atom(false); @@ -28,18 +17,4 @@ export const canvasParentElAtom = atom('unknown'); -export const wgputoyAtom = atom>(async get => { - if (!isSSR && get(canvasElAtom) !== false && get(canvasParentElAtom)) { - const parentEl = get(canvasParentElAtom); - const dim = getDimensions(parentEl.offsetWidth * window.devicePixelRatio); - return create_renderer(dim.x, dim.y, (get(canvasElAtom) as HTMLCanvasElement).id); - } else { - return false; - } -}); - export const wgputoyPreludeAtom = atom(''); - -// type predicate -export const isSafeContext = (context: WgpuToyRenderer | false): context is WgpuToyRenderer => - context !== false && context.__wbg_ptr !== 0; diff --git a/lib/engine/bind.ts b/lib/engine/bind.ts new file mode 100644 index 00000000..e459f041 --- /dev/null +++ b/lib/engine/bind.ts @@ -0,0 +1,628 @@ +// Constants +const NUM_KEYCODES = 256; +const MAX_CUSTOM_PARAMS = 32; +// export const NUM_ASSERT_COUNTERS = 10; +// const USER_DATA_BYTES = 4096; +const OFFSET_ALIGNMENT = 256; + +// Core data structures +class Time { + frame: number; + elapsed: number; + delta: number; + + constructor(frame: number = 0, elapsed: number = 0, delta: number = 0) { + this.frame = frame; + this.elapsed = elapsed; + this.delta = delta; + } + + toBuffer(): Uint8Array { + const buffer = new Uint8Array(12); + const view = new DataView(buffer.buffer); + + view.setUint32(0, this.frame, true); // true for little-endian + view.setFloat32(4, this.elapsed, true); + view.setFloat32(8, this.delta, true); + + return buffer; + } +} + +class Mouse { + pos: [number, number]; + click: number; + + constructor(x: number, y: number, click: number) { + this.pos = [x, y]; + this.click = click; + } + + toBuffer(): Uint8Array { + const buffer = new Uint8Array(12); + const view = new DataView(buffer.buffer); + + view.setInt32(0, this.pos[0], true); + view.setInt32(4, this.pos[1], true); + view.setInt32(8, this.click, true); + + return buffer; + } +} + +class BitArray { + private bits: Uint8Array; + + constructor(size: number) { + this.bits = new Uint8Array(Math.ceil(size / 8)); + } + + toBuffer(): Uint8Array { + return this.bits; + } + + get(index: number): boolean { + const byteIndex = Math.floor(index / 8); + const bitIndex = index % 8; + return (this.bits[byteIndex] & (1 << bitIndex)) !== 0; + } + + set(index: number, value: boolean): void { + const byteIndex = Math.floor(index / 8); + const bitIndex = index % 8; + if (value) { + this.bits[byteIndex] |= 1 << bitIndex; + } else { + this.bits[byteIndex] &= ~(1 << bitIndex); + } + } +} + +interface Binding { + getLayoutEntry(binding: number): GPUBindGroupLayoutEntry; + binding(): GPUBindingResource; + toWGSL(): string; +} + +class BufferBinding implements Binding { + host: H; + device: GPUBuffer; + layout: GPUBufferBindingLayout; + bindingSize?: GPUSize64; + decl: string; + + constructor(params: { + host: H; + device: GPUBuffer; + layout: GPUBufferBindingLayout; + bindingSize?: GPUSize64; + decl: string; + }) { + this.host = params.host; + this.device = params.device; + this.layout = params.layout; + this.bindingSize = params.bindingSize; + this.decl = params.decl; + } + + getLayoutEntry(binding: number): GPUBindGroupLayoutEntry { + return { + binding, + visibility: GPUShaderStage.COMPUTE, + buffer: this.layout + }; + } + + binding(): GPUBufferBinding { + return { buffer: this.device, offset: 0, size: this.bindingSize }; + } + + toWGSL(): string { + return this.decl; + } +} + +class TextureBinding implements Binding { + device: GPUTexture; + view: GPUTextureView; + layout: GPUTextureBindingLayout; + decl: string; + + constructor(params: { + device: GPUTexture; + view: GPUTextureView; + layout: GPUTextureBindingLayout; + decl: string; + }) { + this.device = params.device; + this.view = params.view; + this.layout = params.layout; + this.decl = params.decl; + } + + getLayoutEntry(binding: number): GPUBindGroupLayoutEntry { + return { + binding, + visibility: GPUShaderStage.COMPUTE, + texture: this.layout + }; + } + + binding(): GPUTextureView { + return this.view; + } + + toWGSL(): string { + return this.decl; + } + + texture(): GPUTexture { + return this.device; + } + + setTexture(texture: GPUTexture): void { + this.device = texture; + this.view = texture.createView(); + } +} + +class StorageTextureBinding implements Binding { + device: GPUTexture; + view: GPUTextureView; + layout: GPUStorageTextureBindingLayout; + decl: string; + + constructor(params: { + device: GPUTexture; + view: GPUTextureView; + layout: GPUStorageTextureBindingLayout; + decl: string; + }) { + this.device = params.device; + this.view = params.view; + this.layout = params.layout; + this.decl = params.decl; + } + + getLayoutEntry(binding: number): GPUBindGroupLayoutEntry { + return { + binding, + visibility: GPUShaderStage.COMPUTE, + storageTexture: this.layout + }; + } + + binding(): GPUTextureView { + return this.view; + } + + toWGSL(): string { + return this.decl; + } + + texture(): GPUTexture { + return this.device; + } + + setTexture(texture: GPUTexture): void { + this.device = texture; + this.view = texture.createView(); + } +} + +class SamplerBinding implements Binding { + layout: GPUSamplerBindingLayout; + bind: GPUSampler; + decl: string; + + constructor(params: { layout: GPUSamplerBindingLayout; bind: GPUSampler; decl: string }) { + this.layout = params.layout; + this.bind = params.bind; + this.decl = params.decl; + } + + getLayoutEntry(binding: number): GPUBindGroupLayoutEntry { + return { + binding, + visibility: GPUShaderStage.COMPUTE, + sampler: this.layout + }; + } + + binding(): GPUSampler { + return this.bind; + } + + toWGSL(): string { + return this.decl; + } +} + +// Main bindings class +export class Bindings { + time: BufferBinding