@@ -113,12 +113,24 @@ export class FolkSandWebGPU extends FolkBaseSet {
113113 } ) ;
114114 }
115115
116- #createBuffer( size : number , usage : GPUBufferUsageFlags ) : GPUBuffer {
116+ #createBuffer( size : number , type : 'uniform' | 'storage' | 'storage-rw' ) : GPUBuffer {
117+ const usage = {
118+ uniform : GPUBufferUsage . UNIFORM | GPUBufferUsage . COPY_DST ,
119+ storage : GPUBufferUsage . STORAGE ,
120+ 'storage-rw' : GPUBufferUsage . STORAGE | GPUBufferUsage . COPY_DST ,
121+ } [ type ] ;
117122 const buffer = this . #device. createBuffer ( { size, usage } ) ;
118123 this . #resources. push ( buffer ) ;
119124 return buffer ;
120125 }
121126
127+ #createBindGroup( pipeline : GPUComputePipeline | GPURenderPipeline , buffers : GPUBuffer [ ] ) : GPUBindGroup {
128+ return this . #device. createBindGroup ( {
129+ layout : pipeline . getBindGroupLayout ( 0 ) ,
130+ entries : buffers . map ( ( buffer , i ) => ( { binding : i , resource : { buffer } } ) ) ,
131+ } ) ;
132+ }
133+
122134 #stateBufferSize( ) {
123135 return this . #bufferWidth * this . #bufferHeight * 8 ; // Particle struct: 2 x u32
124136 }
@@ -196,48 +208,30 @@ export class FolkSandWebGPU extends FolkBaseSet {
196208 }
197209
198210 #createResources( ) {
199- this . #paramsBuffer = this . #createBuffer( 32 , GPUBufferUsage . UNIFORM | GPUBufferUsage . COPY_DST ) ;
200- this . #mouseBuffer = this . #createBuffer( 16 , GPUBufferUsage . UNIFORM | GPUBufferUsage . COPY_DST ) ;
201- this . #collisionParamsBuffer = this . #createBuffer( 16 , GPUBufferUsage . UNIFORM | GPUBufferUsage . COPY_DST ) ;
211+ this . #paramsBuffer = this . #createBuffer( 32 , 'uniform' ) ;
212+ this . #mouseBuffer = this . #createBuffer( 16 , 'uniform' ) ;
213+ this . #collisionParamsBuffer = this . #createBuffer( 16 , 'uniform' ) ;
202214
203215 const stateSize = this . #stateBufferSize( ) ;
204- this . #stateBuffers = [
205- this . #createBuffer( stateSize , GPUBufferUsage . STORAGE | GPUBufferUsage . COPY_SRC ) ,
206- this . #createBuffer( stateSize , GPUBufferUsage . STORAGE | GPUBufferUsage . COPY_SRC ) ,
207- ] ;
208- this . #collisionBuffer = this . #createBuffer( stateSize , GPUBufferUsage . STORAGE ) ;
216+ this . #stateBuffers = [ this . #createBuffer( stateSize , 'storage' ) , this . #createBuffer( stateSize , 'storage' ) ] ;
217+ this . #collisionBuffer = this . #createBuffer( stateSize , 'storage' ) ;
209218 }
210219
211220 #createBindGroups( ) {
212- this . #initBindGroup = this . #device. createBindGroup ( {
213- layout : this . #initPipeline. getBindGroupLayout ( 0 ) ,
214- entries : [
215- { binding : 0 , resource : { buffer : this . #stateBuffers[ 0 ] } } ,
216- { binding : 1 , resource : { buffer : this . #paramsBuffer } } ,
217- ] ,
218- } ) ;
221+ this . #initBindGroup = this . #createBindGroup( this . #initPipeline, [ this . #stateBuffers[ 0 ] , this . #paramsBuffer] ) ;
219222
220223 this . #simBindGroups = [ 0 , 1 ] . map ( ( i ) =>
221- this . #device. createBindGroup ( {
222- layout : this . #simulationPipeline. getBindGroupLayout ( 0 ) ,
223- entries : [
224- { binding : 0 , resource : { buffer : this . #stateBuffers[ i ] } } ,
225- { binding : 1 , resource : { buffer : this . #stateBuffers[ 1 - i ] } } ,
226- { binding : 2 , resource : { buffer : this . #collisionBuffer } } ,
227- { binding : 3 , resource : { buffer : this . #paramsBuffer } } ,
228- { binding : 4 , resource : { buffer : this . #mouseBuffer } } ,
229- ] ,
230- } ) ,
224+ this . #createBindGroup( this . #simulationPipeline, [
225+ this . #stateBuffers[ i ] ,
226+ this . #stateBuffers[ 1 - i ] ,
227+ this . #collisionBuffer,
228+ this . #paramsBuffer,
229+ this . #mouseBuffer,
230+ ] ) ,
231231 ) as [ GPUBindGroup , GPUBindGroup ] ;
232232
233233 this . #renderBindGroups = [ 0 , 1 ] . map ( ( i ) =>
234- this . #device. createBindGroup ( {
235- layout : this . #renderPipeline. getBindGroupLayout ( 0 ) ,
236- entries : [
237- { binding : 0 , resource : { buffer : this . #stateBuffers[ i ] } } ,
238- { binding : 1 , resource : { buffer : this . #paramsBuffer } } ,
239- ] ,
240- } ) ,
234+ this . #createBindGroup( this . #renderPipeline, [ this . #stateBuffers[ i ] , this . #paramsBuffer] ) ,
241235 ) as [ GPUBindGroup , GPUBindGroup ] ;
242236
243237 this . #updateCollisionBindGroup( ) ;
@@ -249,14 +243,11 @@ export class FolkSandWebGPU extends FolkBaseSet {
249243 this . #collisionBindGroup = undefined ;
250244 return ;
251245 }
252- this . #collisionBindGroup = this . #device. createBindGroup ( {
253- layout : this . #collisionPipeline. getBindGroupLayout ( 0 ) ,
254- entries : [
255- { binding : 0 , resource : { buffer : this . #collisionBuffer } } ,
256- { binding : 1 , resource : { buffer : this . #collisionParamsBuffer } } ,
257- { binding : 2 , resource : { buffer : this . #shapeDataBuffer } } ,
258- ] ,
259- } ) ;
246+ this . #collisionBindGroup = this . #createBindGroup( this . #collisionPipeline, [
247+ this . #collisionBuffer,
248+ this . #collisionParamsBuffer,
249+ this . #shapeDataBuffer,
250+ ] ) ;
260251 }
261252
262253 // === Uniform Updates ===
@@ -344,11 +335,8 @@ export class FolkSandWebGPU extends FolkBaseSet {
344335
345336 // Recreate buffers (old ones stay in #resources for cleanup)
346337 const stateSize = this . #stateBufferSize( ) ;
347- this . #stateBuffers = [
348- this . #createBuffer( stateSize , GPUBufferUsage . STORAGE | GPUBufferUsage . COPY_SRC ) ,
349- this . #createBuffer( stateSize , GPUBufferUsage . STORAGE | GPUBufferUsage . COPY_SRC ) ,
350- ] ;
351- this . #collisionBuffer = this . #createBuffer( stateSize , GPUBufferUsage . STORAGE ) ;
338+ this . #stateBuffers = [ this . #createBuffer( stateSize , 'storage' ) , this . #createBuffer( stateSize , 'storage' ) ] ;
339+ this . #collisionBuffer = this . #createBuffer( stateSize , 'storage' ) ;
352340 this . #currentStateIndex = 0 ;
353341
354342 this . #createBindGroups( ) ;
@@ -384,10 +372,7 @@ export class FolkSandWebGPU extends FolkBaseSet {
384372 // Resize buffer if needed, and update collision bind group
385373 const requiredSize = shapeData . length * 4 ;
386374 if ( ! this . #shapeDataBuffer || this . #shapeDataBuffer. size < requiredSize ) {
387- this . #shapeDataBuffer = this . #createBuffer(
388- Math . max ( requiredSize , 64 ) ,
389- GPUBufferUsage . STORAGE | GPUBufferUsage . COPY_DST ,
390- ) ;
375+ this . #shapeDataBuffer = this . #createBuffer( Math . max ( requiredSize , 64 ) , 'storage-rw' ) ;
391376 this . #updateCollisionBindGroup( ) ;
392377 }
393378
@@ -473,17 +458,6 @@ struct Mouse {
473458 prevY: f32,
474459}` ;
475460
476- const particleTypes = /*wgsl*/ `
477- const AIR: u32 = 0u;
478- const SMOKE: u32 = 1u;
479- const WATER: u32 = 2u;
480- const LAVA: u32 = 3u;
481- const SAND: u32 = 4u;
482- const STONE: u32 = 5u;
483- const WALL: u32 = 6u;
484- const COLLISION: u32 = 99u;
485- ` ;
486-
487461const hashFunctions = /*wgsl*/ `
488462fn hash12(p: vec2f) -> f32 {
489463 var p3 = fract(vec3f(p.x, p.y, p.x) * 0.1031);
@@ -504,26 +478,45 @@ fn hash44(p: vec4f) -> vec4f {
504478}
505479` ;
506480
507- const particleStruct = /*wgsl*/ `
508- struct Particle {
509- ptype: u32, // particle type (only needs ~4 bits, but u32 is WGSL's smallest)
510- rand: u32, // visual variation (only needs 8 bits)
511- }
512-
481+ const wgslUtils = /*wgsl*/ `
513482fn getIndex(x: u32, y: u32, width: u32) -> u32 {
514483 return y * width + x;
515484}
516485
486+ fn getIndexI(p: vec2i, width: u32) -> u32 {
487+ return u32(p.y) * width + u32(p.x);
488+ }
489+
490+ fn inBounds(p: vec2i, width: u32, height: u32) -> bool {
491+ return p.x >= 0 && p.y >= 0 && p.x < i32(width) && p.y < i32(height);
492+ }
493+ ` ;
494+
495+ const particleDefs = /*wgsl*/ `
496+ const AIR: u32 = 0u;
497+ const SMOKE: u32 = 1u;
498+ const WATER: u32 = 2u;
499+ const LAVA: u32 = 3u;
500+ const SAND: u32 = 4u;
501+ const STONE: u32 = 5u;
502+ const WALL: u32 = 6u;
503+ const COLLISION: u32 = 99u;
504+
505+ struct Particle {
506+ ptype: u32,
507+ rand: u32,
508+ }
509+
517510fn particle(ptype: u32, rand: u32) -> Particle {
518511 return Particle(ptype, rand);
519512}
520513` ;
521514
522515const initShader = /*wgsl*/ `
523516${ paramsStruct }
524- ${ particleTypes }
517+ ${ particleDefs }
525518${ hashFunctions }
526- ${ particleStruct }
519+ ${ wgslUtils }
527520
528521@group(0) @binding(0) var<storage, read_write> output: array<Particle>;
529522@group(0) @binding(1) var<uniform> params: Params;
@@ -576,9 +569,9 @@ fn main(@builtin(global_invocation_id) gid: vec3u) {
576569const simulationShader = /*wgsl*/ `
577570${ paramsStruct }
578571${ mouseStruct }
579- ${ particleTypes }
572+ ${ particleDefs }
580573${ hashFunctions }
581- ${ particleStruct }
574+ ${ wgslUtils }
582575
583576@group(0) @binding(0) var<storage, read> input: array<Particle>;
584577@group(0) @binding(1) var<storage, read_write> output: array<Particle>;
@@ -601,13 +594,9 @@ fn getOffset(frame: u32) -> vec2i {
601594}
602595
603596fn getData(p: vec2i) -> Particle {
604- if (p.x < 0 || p.y < 0 || p.x >= i32(params.width) || p.y >= i32(params.height)) {
605- return particle(WALL, 0u);
606- }
607- let idx = getIndex(u32(p.x), u32(p.y), params.width);
608- if (collision[idx] > 0u) {
609- return particle(COLLISION, 0u);
610- }
597+ if (!inBounds(p, params.width, params.height)) { return particle(WALL, 0u); }
598+ let idx = getIndexI(p, params.width);
599+ if (collision[idx] > 0u) { return particle(COLLISION, 0u); }
611600 return input[idx];
612601}
613602
@@ -617,12 +606,14 @@ fn newParticle(ptype: u32, coord: vec2i, frame: u32) -> Particle {
617606}
618607
619608fn isCollision(p: vec2i) -> bool {
620- if (p.x < 0 || p.y < 0 || p.x >= i32( params.width) || p.y >= i32( params.height)) { return false; }
621- return collision[getIndex(u32(p.x), u32(p.y) , params.width)] > 0u;
609+ if (!inBounds(p, params.width, params.height)) { return false; }
610+ return collision[getIndexI(p , params.width)] > 0u;
622611}
623612
624- fn inBounds(p: vec2i) -> bool {
625- return p.x >= 0 && p.y >= 0 && p.x < i32(params.width) && p.y < i32(params.height);
613+ fn writeIfInBounds(p: vec2i, val: Particle) {
614+ if (inBounds(p, params.width, params.height)) {
615+ output[getIndexI(p, params.width)] = val;
616+ }
626617}
627618
628619// Block-based: each thread processes one 2x2 Margolus block
@@ -741,17 +732,17 @@ fn main(@builtin(global_invocation_id) gid: vec3u) {
741732 if (t11.ptype == COLLISION && !isCollision(p + vec2i(1, 1))) { t11 = newParticle(AIR, p + vec2i(1, 1), params.frame); }
742733
743734 // Write only in-bounds cells
744- if (inBounds(p)) { output[getIndex(u32(p.x), u32(p.y), params.width)] = t00; }
745- if (inBounds( p + vec2i(1, 0))) { output[getIndex(u32(p.x + 1), u32(p.y), params.width)] = t10; }
746- if (inBounds( p + vec2i(0, 1))) { output[getIndex(u32(p.x), u32(p.y + 1), params.width)] = t01; }
747- if (inBounds( p + vec2i(1, 1))) { output[getIndex(u32(p.x + 1), u32(p.y + 1), params.width)] = t11; }
735+ writeIfInBounds(p, t00);
736+ writeIfInBounds( p + vec2i(1, 0), t10);
737+ writeIfInBounds( p + vec2i(0, 1), t01);
738+ writeIfInBounds( p + vec2i(1, 1), t11);
748739}
749740` ;
750741
751742const renderShader = /*wgsl*/ `
752743${ paramsStruct }
753- ${ particleTypes }
754- ${ particleStruct }
744+ ${ particleDefs }
745+ ${ wgslUtils }
755746
756747struct VertexOutput {
757748 @builtin(position) position: vec4f,
0 commit comments