TypeError: Cannot read properties of undefined (reading 'getBindGroupLayout')
    at createBindGroups (app.js:193:33)
    at initWebGPU (app.js:56:5)
createBindGroups @ app.js:193
initWebGPU @ app.js:56
await in initWebGPU
(anonymous) @ app.js:337

// Globals
let canvas, adapter, device, context, renderPipeline, computePipeline, particleBuffer, colorBuffer, uniformBuffer;
let particleCount = 10000;
let particles; // Typed array to store particle data
let directionMapImage, speedMapImage;
let mapInfluence = 0;
let colorizationType = "hsv";
let computeBindGroup, renderBindGroup;
let simulationShaderModule, renderShaderModule;
const PARTICLE_SIZE = 2; // Adjust as needed for visualization
const SIMULATION_BLOCK_SIZE = 256;
const WIDTH = 800;
const HEIGHT = 600;

async function initWebGPU() {
    canvas = document.getElementById('canvas');
    // Check for WebGPU support
    if (!navigator.gpu) {
        console.error("WebGPU is not supported on this browser.");
        return;
    }

    adapter = await navigator.gpu.requestAdapter();
    if (!adapter) {
        console.error("No appropriate GPUAdapter found.");
        return;
    }

    device = await adapter.requestDevice();

    context = canvas.getContext('webgpu');
    const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
    context.configure({
        device: device,
        format: canvasFormat,
        alphaMode: "premultiplied"
    });

    // Load shaders (async)
    try {
        simulationShaderModule = device.createShaderModule({
            code: await fetch("shaders.wgsl").then((response) => response.text()),
        });
        renderShaderModule = device.createShaderModule({
            code: await fetch("shaders.wgsl").then((response) => response.text()),
        });
    } catch (e) {
        console.error("Shader loading error:", e);
        return;
    }

    await createParticles();
    createUniformBuffer();
    createComputePipeline();
    createRenderPipeline(canvasFormat);
    createBindGroups();

    requestAnimationFrame(render);
}


async function createParticles() {
    particles = new Float32Array(particleCount * 9); // H, S, L, x, y, vx, vy, mass, padding

    for (let i = 0; i < particleCount; i++) {
        const base = i * 9;
        particles[base + 0] = Math.random();   // H
        particles[base + 1] = Math.random();   // S
        particles[base + 2] = Math.random();   // L
        particles[base + 3] = Math.random() * WIDTH;   // x
        particles[base + 4] = Math.random() * HEIGHT;  // y
        particles[base + 5] = (Math.random() - 0.5) * 10; // vx
        particles[base + 6] = (Math.random() - 0.5) * 10; // vy
        particles[base + 7] = Math.random() * 5 + 1; // mass
        particles[base + 8] = 0;   //padding
    }


    particleBuffer = device.createBuffer({
        size: particles.byteLength,
        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
        mappedAtCreation: false
    });

    device.queue.writeBuffer(particleBuffer, 0, particles); // Initial data upload
}

function createUniformBuffer() {
  uniformBuffer = device.createBuffer({
    size: 16, // mapInfluence (float), particleCount (uint), colorizationType (uint), padding
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
  });
}

function updateUniformBuffer() {
  const uniformData = new Float32Array(4);
  uniformData[0] = mapInfluence / 100.0; // Normalize to 0-1
  uniformData[1] = particleCount;
  // Map colorizationType string to an integer (shader uses a switch)
  switch (colorizationType) {
    case "hsv": uniformData[2] = 0; break;
    case "mass": uniformData[2] = 1; break;
    case "speed": uniformData[2] = 2; break;
    case "direction": uniformData[2] = 3; break;
    default: uniformData[2] = 0;
  }
  uniformData[3] = 0;

  device.queue.writeBuffer(uniformBuffer, 0, uniformData.buffer);
}

async function createComputePipeline() {
    const computePipelineDescriptor = {
        layout: device.createPipelineLayout({
            bindGroupLayouts: [
              device.createBindGroupLayout({
                entries: [
                  {
                    binding: 0,
                    visibility: GPUShaderStage.COMPUTE,
                    buffer: { type: "storage" },
                  },
                  {
                    binding: 1,
                    visibility: GPUShaderStage.COMPUTE,
                    buffer: { type: "uniform" },
                  },
                ],
              }),
            ],
        }),
        compute: {
            module: simulationShaderModule,
            entryPoint: "computeMain",
        },
    };

    computePipeline = await device.createComputePipelineAsync(computePipelineDescriptor);
}

function createRenderPipeline(canvasFormat) {
    const renderPipelineDescriptor = {
        layout: device.createPipelineLayout({
            bindGroupLayouts: [
              device.createBindGroupLayout({
                entries: [
                  {
                    binding: 1,
                    visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
                    buffer: { type: "uniform" },
                  },
                  {
                    binding: 0,
                    visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
                    buffer: { type: "read-only-storage" },
                  },
                ],
              }),
            ],
        }),
        vertex: {
            module: renderShaderModule,
            entryPoint: "vertexMain",
            buffers: [
                {
                    arrayStride: 4 * 9, // Size of each particle data in bytes
                    stepMode: "instance",
                    attributes: [
                        { format: "float32x3", offset: 0, shaderLocation: 0 }, // hsv
                        { format: "float32x2", offset: 4 * 3, shaderLocation: 1 }, // xy
                        { format: "float32x2", offset: 4 * 5, shaderLocation: 2 }, // velocity
                        { format: "float32", offset: 4 * 7, shaderLocation: 3 }, // mass
                    ]
                }
            ],
        },
        fragment: {
            module: renderShaderModule,
            entryPoint: "fragmentMain",
            targets: [{ format: canvasFormat }]
        },
        primitive: {
            topology: "triangle-strip", // Draw squares using triangle strip
            stripIndexFormat: "uint32"
        }
    };

    renderPipeline = device.createRenderPipeline(renderPipelineDescriptor);
}

function createBindGroups() {
    computeBindGroup = device.createBindGroup({
        layout: computePipeline.getBindGroupLayout(0),
        entries: [
          {
            binding: 0,
            resource: { buffer: particleBuffer },
          },
          {
            binding: 1,
            resource: { buffer: uniformBuffer },
          },
        ],
    });

    renderBindGroup = device.createBindGroup({
        layout: renderPipeline.getBindGroupLayout(0),
        entries: [
          {
            binding: 0,
            resource: { buffer: particleBuffer },
          },
          {
            binding: 1,
            resource: { buffer: uniformBuffer },
          },
        ],
    });
}


function render() {
    updateUniformBuffer(); // Update uniform buffer before each frame

    // Compute Pass
    const commandEncoder = device.createCommandEncoder();
    const computePass = commandEncoder.beginComputePass();
    computePass.setPipeline(computePipeline);
    computePass.setBindGroup(0, computeBindGroup);

    const workgroupCount = Math.ceil(particleCount / SIMULATION_BLOCK_SIZE);
    computePass.dispatchWorkgroups(workgroupCount, 1, 1); // Execute the compute shader
    computePass.end();

    // Render Pass
    const textureView = context.getCurrentTexture().createView();
    const renderPassDescriptor = {
        colorAttachments: [{
            view: textureView,
            loadOp: 'clear',
            storeOp: 'store',
            clearValue: { r: 0, g: 0, b: 0, a: 1 },
        }],
    };

    const renderPass = commandEncoder.beginRenderPass(renderPassDescriptor);
    renderPass.setPipeline(renderPipeline);
    renderPass.setBindGroup(0, renderBindGroup);

    // Setup vertex buffer for quad
    const vertexBuffer = device.createBuffer({
        size: new Float32Array([
            -1, -1,
             1, -1,
            -1,  1,
             1,  1
        ]).byteLength,
        usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
        mappedAtCreation: true
    });
    new Float32Array(vertexBuffer.getMappedRange()).set([
        -1, -1,
         1, -1,
        -1,  1,
         1,  1
    ]);
    vertexBuffer.unmap();
    renderPass.setVertexBuffer(0, vertexBuffer);

    //Setup index buffer for quad
    const indexBuffer = device.createBuffer({
        size: new Uint32Array([0, 1, 2, 3]).byteLength,
        usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
        mappedAtCreation: true
    });
    new Uint32Array(indexBuffer.getMappedRange()).set([0, 1, 2, 3]);
    indexBuffer.unmap();
    renderPass.setIndexBuffer(indexBuffer, "uint32");


    renderPass.drawIndexed(4, particleCount, 0, 0); // 4 vertices make a quad, instanceCount = particleCount
    renderPass.end();

    device.queue.submit([commandEncoder.finish()]);
    requestAnimationFrame(render);
}

// UI Event Listeners
document.getElementById('particleCount').addEventListener('input', (event) => {
    particleCount = parseInt(event.target.value);
    document.getElementById('particleCountValue').textContent = particleCount;
    createParticles(); // Recreate particles when count changes
    createBindGroups(); // Recreate bind groups
});

document.getElementById('mapInfluence').addEventListener('input', (event) => {
    mapInfluence = parseInt(event.target.value);
    document.getElementById('mapInfluenceValue').textContent = mapInfluence + '%';
});

document.getElementById('colorizationType').addEventListener('change', (event) => {
    colorizationType = event.target.value;
});

document.getElementById('directionImage').addEventListener('change', (event) => {
    const file = event.target.files[0];
    if (file) {
        loadImage(file, 'direction');
    }
});

document.getElementById('speedImage').addEventListener('change', (event) => {
    const file = event.target.files[0];
    if (file) {
        loadImage(file, 'speed');
    }
});

async function loadImage(file, type) {
    const img = new Image();
    img.onload = async () => {
        let bitmap = await createImageBitmap(img);
        if (type === 'direction') {
            directionMapImage = bitmap;
            console.log("direction image is loaded")
        } else if (type === 'speed') {
            speedMapImage = bitmap;
            console.log("speed image is loaded")
        }

        // Optionally rescale the image if it's larger than the canvas
        // This is a placeholder; implement your rescaling logic here.
    };
    img.src = URL.createObjectURL(file);
}

initWebGPU();