diff --git a/apps/typegpu-docs/src/content/examples/simulation/fluid-with-atomics/index.ts b/apps/typegpu-docs/src/content/examples/simulation/fluid-with-atomics/index.ts index 3a216fe1a..5a87a2103 100644 --- a/apps/typegpu-docs/src/content/examples/simulation/fluid-with-atomics/index.ts +++ b/apps/typegpu-docs/src/content/examples/simulation/fluid-with-atomics/index.ts @@ -1,15 +1,9 @@ -// @ts-nocheck -// TODO: Reenable type checking when new pipelines are implemented. - -import * as d from 'typegpu/data'; import tgpu, { - asMutable, - asReadonly, - asUniform, - asVertex, - builtin, - type TgpuBuffer, -} from 'typegpu/experimental'; + unstable_asReadonly, + unstable_asMutable, + unstable_asUniform, +} from 'typegpu'; +import * as d from 'typegpu/data'; const root = await tgpu.init(); @@ -29,11 +23,15 @@ canvas.addEventListener('contextmenu', (event) => { } }); +const MAX_WATER_LEVEL_UNPRESSURIZED = tgpu['~unstable'].const(d.u32, 0xff); +const MAX_WATER_LEVEL = tgpu['~unstable'].const(d.u32, (1 << 24) - 1); +const MAX_PRESSURE = tgpu['~unstable'].const(d.u32, 12); + const options = { - size: 64, - timestep: 25, + size: 32, + timestep: 50, stepsPerTimestep: 1, - workgroupSize: 16, + workgroupSize: 1, viscosity: 1000, brushSize: 0, brushType: 'water', @@ -54,254 +52,342 @@ function encodeBrushType(brushType: (typeof BrushTypes)[number]) { } const sizeBuffer = root.createBuffer(d.vec2u).$name('size').$usage('uniform'); +const sizeUniform = unstable_asUniform(sizeBuffer); + const viscosityBuffer = root .createBuffer(d.u32) .$name('viscosity') .$usage('uniform'); +const viscosityUniform = unstable_asUniform(viscosityBuffer); const currentStateBuffer = root .createBuffer(d.arrayOf(d.u32, 1024 ** 2)) .$name('current') .$usage('storage', 'vertex'); +const currentStateStorage = unstable_asReadonly(currentStateBuffer); const nextStateBuffer = root .createBuffer(d.arrayOf(d.atomic(d.u32), 1024 ** 2)) .$name('next') .$usage('storage'); - -const viscosityData = asUniform(viscosityBuffer); -const currentStateData = asReadonly(currentStateBuffer); -const currentStateVertex = asVertex(currentStateBuffer, 'instance'); -const sizeData = asUniform(sizeBuffer); -const nextStateData = asMutable(nextStateBuffer); - -const maxWaterLevelUnpressurized = wgsl.constant(wgsl`510u`); -const maxWaterLevel = wgsl.constant(wgsl`(1u << 24) - 1u`); -const maxCompress = wgsl.constant(wgsl`12u`); +const nextStateStorage = unstable_asMutable(nextStateBuffer); const squareBuffer = root - .createBuffer(d.arrayOf(d.vec2u, 4), [ - d.vec2u(0, 0), - d.vec2u(0, 1), - d.vec2u(1, 0), - d.vec2u(1, 1), + .createBuffer(d.arrayOf(d.vec2f, 4), [ + d.vec2f(0, 0), + d.vec2f(0, 1), + d.vec2f(1, 0), + d.vec2f(1, 1), ]) - .$usage('uniform', 'vertex') + .$usage('vertex') .$name('square'); -const squareBufferData = asVertex(squareBuffer, 'vertex'); - -const getIndex = wgsl.fn`(x: u32, y: u32) -> u32 { - let h = ${sizeData}.y; - let w = ${sizeData}.x; - return (y % h) * w + (x % w); -}`; - -const getCell = wgsl.fn`(x: u32, y: u32) -> u32 { - return ${currentStateData}[${getIndex}(x, y)]; -}`; - -const getCellNext = wgsl.fn`(x: u32, y: u32) -> u32 { - return atomicLoad(&${nextStateData}[${getIndex}(x, y)]); -}`; - -const updateCell = wgsl.fn`(x: u32, y: u32, value: u32) { - atomicStore(&${nextStateData}[${getIndex}(x, y)], value); -}`; - -const addToCell = wgsl.fn`(x: u32, y: u32, value: u32) { - let cell = ${getCellNext}(x, y); - let waterLevel = cell & ${maxWaterLevel}; - let newWaterLevel = min(waterLevel + value, ${maxWaterLevel}); - atomicAdd(&${nextStateData}[${getIndex}(x, y)], newWaterLevel - waterLevel); -}`; - -const subtractFromCell = wgsl.fn`(x: u32, y: u32, value: u32) { - let cell = ${getCellNext}(x, y); - let waterLevel = cell & ${maxWaterLevel}; - let newWaterLevel = max(waterLevel - min(value, waterLevel), 0u); - atomicSub(&${nextStateData}[${getIndex}(x, y)], waterLevel - newWaterLevel); -}`; - -const persistFlags = wgsl.fn`(x: u32, y: u32) { - let cell = ${getCell}(x, y); - let waterLevel = cell & ${maxWaterLevel}; - let flags = cell >> 24; - ${updateCell}(x, y, (flags << 24) | waterLevel); -}`; - -const getStableStateBelow = wgsl.fn`(upper: u32, lower: u32) -> u32 { - let totalMass = upper + lower; - if (totalMass <= ${maxWaterLevelUnpressurized}) { - return totalMass; - } else if (totalMass >= ${maxWaterLevelUnpressurized}*2 && upper > lower) { - return totalMass/2 + ${maxCompress}; - } - return ${maxWaterLevelUnpressurized}; -}`; +const getIndex = tgpu['~unstable'] + .fn([d.u32, d.u32], d.u32) + .does(/* wgsl */ `(x: u32, y: u32) -> u32 { + let h = sizeData.y; + let w = sizeData.x; + return (y % h) * w + (x % w); + }`) + .$uses({ sizeData: sizeUniform }); + +const getCell = tgpu['~unstable'] + .fn([d.u32, d.u32], d.u32) + .does(/* wgsl */ `(x: u32, y: u32) -> u32 { + return currentStateData[getIndex(x, y)]; + }`) + .$uses({ currentStateData: currentStateStorage, getIndex }); + +const getCellNext = tgpu['~unstable'] + .fn([d.u32, d.u32], d.u32) + .does(/* wgsl */ `(x: u32, y: u32) -> u32 { + return atomicLoad(&nextStateData[getIndex(x, y)]); + }`) + .$uses({ nextStateData: nextStateStorage, getIndex }); + +const updateCell = tgpu['~unstable'] + .fn([d.u32, d.u32, d.u32]) + .does(/* wgsl */ `(x: u32, y: u32, value: u32) { + atomicStore(&nextStateData[getIndex(x, y)], value); + }`) + .$uses({ nextStateData: nextStateStorage, getIndex }); + +const addToCell = tgpu['~unstable'] + .fn([d.u32, d.u32, d.u32]) + .does(/* wgsl */ `(x: u32, y: u32, value: u32) { + let cell = getCellNext(x, y); + let waterLevel = cell & MAX_WATER_LEVEL; + let newWaterLevel = min(waterLevel + value, MAX_WATER_LEVEL); + atomicAdd(&nextStateData[getIndex(x, y)], newWaterLevel - waterLevel); + }`) + .$uses({ + getCellNext, + nextStateData: nextStateStorage, + getIndex, + MAX_WATER_LEVEL, + }); -const isWall = wgsl.fn`(x: u32, y: u32) -> bool { - return (${getCell}(x, y) >> 24) == 1u; -}`; +const subtractFromCell = tgpu['~unstable'] + .fn([d.u32, d.u32, d.u32]) + .does(/* wgsl */ `(x: u32, y: u32, value: u32) { + let cell = getCellNext(x, y); + let waterLevel = cell & MAX_WATER_LEVEL; + let newWaterLevel = max(waterLevel - min(value, waterLevel), 0u); + atomicSub(&nextStateData[getIndex(x, y)], waterLevel - newWaterLevel); + }`) + .$uses({ + getCellNext, + nextStateData: nextStateStorage, + getIndex, + MAX_WATER_LEVEL, + }); -const isWaterSource = wgsl.fn`(x: u32, y: u32) -> bool { - return (${getCell}(x, y) >> 24) == 2u; -}`; +const persistFlags = tgpu['~unstable'] + .fn([d.u32, d.u32]) + .does(/* wgsl */ `(x: u32, y: u32) { + let cell = getCell(x, y); + let waterLevel = cell & MAX_WATER_LEVEL; + let flags = cell >> 24; + updateCell(x, y, (flags << 24) | waterLevel); + }`) + .$uses({ getCell, updateCell, MAX_WATER_LEVEL }); + +const getStableStateBelow = tgpu['~unstable'] + .fn([d.u32, d.u32], d.u32) + .does(/* wgsl */ `(upper: u32, lower: u32) -> u32 { + let totalMass = upper + lower; + if (totalMass <= MAX_WATER_LEVEL_UNPRESSURIZED) { + return totalMass; + } else if (totalMass >= MAX_WATER_LEVEL_UNPRESSURIZED * 2 && upper > lower) { + return totalMass/2 + MAX_PRESSURE; + } + return MAX_WATER_LEVEL_UNPRESSURIZED; + }`) + .$uses({ MAX_PRESSURE, MAX_WATER_LEVEL_UNPRESSURIZED }); + +const isWall = tgpu['~unstable'] + .fn([d.u32, d.u32], d.bool) + .does(/* wgsl */ `(x: u32, y: u32) -> bool { + return (getCell(x, y) >> 24) == 1u; + }`) + .$uses({ getCell }); + +const isWaterSource = tgpu['~unstable'] + .fn([d.u32, d.u32], d.bool) + .does(/* wgsl */ `(x: u32, y: u32) -> bool { + return (getCell(x, y) >> 24) == 2u; + }`) + .$uses({ getCell }); + +const isWaterDrain = tgpu['~unstable'] + .fn([d.u32, d.u32], d.bool) + .does(/* wgsl */ `(x: u32, y: u32) -> bool { + return (getCell(x, y) >> 24) == 3u; + }`) + .$uses({ getCell }); + +const isClearCell = tgpu['~unstable'] + .fn([d.u32, d.u32], d.bool) + .does(/* wgsl */ `(x: u32, y: u32) -> bool { + return (getCell(x, y) >> 24) == 4u; + }`) + .$uses({ getCell }); + +const getWaterLevel = tgpu['~unstable'] + .fn([d.u32, d.u32], d.u32) + .does(/* wgsl */ `(x: u32, y: u32) -> u32 { + return getCell(x, y) &MAX_WATER_LEVEL; + }`) + .$uses({ getCell, MAX_WATER_LEVEL }); + +const checkForFlagsAndBounds = tgpu['~unstable'] + .fn([d.u32, d.u32], d.bool) + .does(/* wgsl */ `(x: u32, y: u32) -> bool { + if (isClearCell(x, y)) { + updateCell(x, y, 0u); + return true; + } -const isWaterDrain = wgsl.fn`(x: u32, y: u32) -> bool { - return (${getCell}(x, y) >> 24) == 3u; -}`; + if (isWall(x, y)) { + persistFlags(x, y); + return true; + } -const isClearCell = wgsl.fn`(x: u32, y: u32) -> bool { - return (${getCell}(x, y) >> 24) == 4u; -}`; + if (isWaterSource(x, y)) { + persistFlags(x, y); + addToCell(x, y, 20u); + return false; + } -const getWaterLevel = wgsl.fn`(x: u32, y: u32) -> u32 { - return ${getCell}(x, y) & ${maxWaterLevel}; -}`; + if (isWaterDrain(x, y)) { + persistFlags(x, y); + updateCell(x, y, 3u << 24); + return true; + } + + if (y == 0 || y == sizeData.y - 1u || x == 0 || x == sizeData.x - 1u) { + subtractFromCell(x, y, getWaterLevel(x, y)); + return true; + } -const checkForFlagsAndBounds = wgsl.fn`(x: u32, y: u32) -> bool { - if (${isClearCell}(x, y)) { - ${updateCell}(x, y, 0u); - return true; - } - if (${isWall}(x, y)) { - ${persistFlags}(x, y); - return true; - } - if (${isWaterSource}(x, y)) { - ${persistFlags}(x, y); - ${addToCell}(x, y, 10u); return false; - } - if (${isWaterDrain}(x, y)) { - ${persistFlags}(x, y); - ${updateCell}(x, y, 3u << 24); - return true; - } - if (y == 0 || y == ${sizeData}.y - 1u || x == 0 || x == ${sizeData}.x - 1u) { - ${subtractFromCell}(x, y, ${getWaterLevel}(x, y)); - return true; - } - return false; -}`; + }`) + .$uses({ + isClearCell, + updateCell, + persistFlags, + addToCell, + subtractFromCell, + isWall, + isWaterSource, + isWaterDrain, + getWaterLevel, + sizeData: sizeUniform, + }); -const decideWaterLevel = wgsl.fn`(x: u32, y: u32) { - if (${checkForFlagsAndBounds}(x, y)) { - return; - } +const decideWaterLevel = tgpu['~unstable'] + .fn([d.u32, d.u32]) + .does(/* wgsl */ `(x: u32, y: u32) { + if (checkForFlagsAndBounds(x, y)) { + return; + } - var remainingWater: u32 = ${getWaterLevel}(x, y); + var remainingWater = getWaterLevel(x, y); - if (remainingWater == 0u) { - return; - } + if (remainingWater == 0u) { + return; + } - if (!${isWall}(x, y - 1u)) { - let waterLevelBelow = ${getWaterLevel}(x, y - 1u); - let stable = ${getStableStateBelow}(remainingWater, waterLevelBelow); - if (waterLevelBelow < stable) { - let change = stable - waterLevelBelow; - let flow = min(change, ${viscosityData}); - ${subtractFromCell}(x, y, flow); - ${addToCell}(x, y - 1u, flow); - remainingWater -= flow; + if (!isWall(x, y - 1u)) { + let waterLevelBelow = getWaterLevel(x, y - 1u); + let stable = getStableStateBelow(remainingWater, waterLevelBelow); + if (waterLevelBelow < stable) { + let change = stable - waterLevelBelow; + let flow = min(change, viscosityData); + subtractFromCell(x, y, flow); + addToCell(x, y - 1u, flow); + remainingWater -= flow; + } } - } - if (remainingWater == 0u) { - return; - } + if (remainingWater == 0u) { + return; + } - let waterLevelBefore = remainingWater; - if (!${isWall}(x - 1u, y)) { - let flowRaw = (i32(waterLevelBefore) - i32(${getWaterLevel}(x - 1u, y))); - if (flowRaw > 0) { - let change = max(min(4u, remainingWater), u32(flowRaw)/4); - let flow = min(change, ${viscosityData}); - ${subtractFromCell}(x, y, flow); - ${addToCell}(x - 1u, y, flow); - remainingWater -= flow; + let waterLevelBefore = remainingWater; + if (!isWall(x - 1u, y)) { + let flowRaw = (i32(waterLevelBefore) - i32(getWaterLevel(x - 1u, y))); + if (flowRaw > 0) { + let change = max(min(4u, remainingWater), u32(flowRaw)/4); + let flow = min(change, viscosityData); + subtractFromCell(x, y, flow); + addToCell(x - 1u, y, flow); + remainingWater -= flow; + } } - } - if (remainingWater == 0u) { - return; - } + if (remainingWater == 0u) { + return; + } - if (!${isWall}(x + 1u, y)) { - let flowRaw = (i32(waterLevelBefore) - i32(${getWaterLevel}(x + 1, y))); - if (flowRaw > 0) { - let change = max(min(4u, remainingWater), u32(flowRaw)/4); - let flow = min(change, ${viscosityData}); - ${subtractFromCell}(x, y, flow); - ${addToCell}(x + 1u, y, flow); - remainingWater -= flow; + if (!isWall(x + 1u, y)) { + let flowRaw = (i32(waterLevelBefore) - i32(getWaterLevel(x + 1, y))); + if (flowRaw > 0) { + let change = max(min(4u, remainingWater), u32(flowRaw)/4); + let flow = min(change, viscosityData); + subtractFromCell(x, y, flow); + addToCell(x + 1u, y, flow); + remainingWater -= flow; + } } - } - if (remainingWater == 0u) { - return; - } + if (remainingWater == 0u) { + return; + } - if (!${isWall}(x, y + 1u)) { - let stable = ${getStableStateBelow}(${getWaterLevel}(x, y + 1u), remainingWater); - if (stable < remainingWater) { - let flow = min(remainingWater - stable, ${viscosityData}); - ${subtractFromCell}(x, y, flow); - ${addToCell}(x, y + 1u, flow); - remainingWater -= flow; + if (!isWall(x, y + 1u)) { + let stable = getStableStateBelow(getWaterLevel(x, y + 1u), remainingWater); + if (stable < remainingWater) { + let flow = min(remainingWater - stable, viscosityData); + subtractFromCell(x, y, flow); + addToCell(x, y + 1u, flow); + remainingWater -= flow; + } } - } -}`; - -const computeWGSL = wgsl` - let x = ${builtin.globalInvocationId}.x; - let y = ${builtin.globalInvocationId}.y; - ${decideWaterLevel}(x, y); -`; - -const vertWGSL = wgsl` - let w = ${sizeData}.x; - let h = ${sizeData}.y; - let x = (f32(${builtin.instanceIndex} % w + ${squareBufferData}.x) / f32(w) - 0.5) * 2. * f32(w) / f32(max(w, h)); - let y = (f32((${builtin.instanceIndex} - (${builtin.instanceIndex} % w)) / w + ${squareBufferData}.y) / f32(h) - 0.5) * 2. * f32(h) / f32(max(w, h)); - let cellFlags = ${currentStateVertex} >> 24; - let cellVal = f32(${currentStateVertex} & 0xFFFFFF); - let pos = vec4(x, y, 0., 1.); - var cell: f32; - cell = cellVal; - if (cellFlags == 1u) { - cell = -1.; - } - if (cellFlags == 2u) { - cell = -2.; - } - if (cellFlags == 3u) { - cell = -3.; - } -`; + }`) + .$uses({ + checkForFlagsAndBounds, + getWaterLevel, + isWall, + getStableStateBelow, + subtractFromCell, + addToCell, + updateCell, + viscosityData: viscosityUniform, + }); -const fragWGSL = wgsl` - if (cell == -1.) { - return vec4f(0.5, 0.5, 0.5, 1.); - } - if (cell == -2.) { - return vec4f(0., 1., 0., 1.); - } - if (cell == -3.) { - return vec4f(1., 0., 0., 1.); - } +const vertex = tgpu['~unstable'] + .vertexFn( + { + squareData: d.vec2f, + currentStateData: d.u32, + idx: d.builtin.instanceIndex, + }, + { pos: d.builtin.position, cell: d.f32 }, + ) + .does(/* wgsl */ `(@builtin(instance_index) idx: u32, @location(0) squareData: vec2f, @location(1) currentStateData: u32) -> VertexOut { + let w = sizeData.x; + let h = sizeData.y; + let x = ((f32(idx % w) + squareData.x) / f32(w) - 0.5) * 2. * f32(w) / f32(max(w, h)); + let y = (f32((idx - (idx % w)) / w + u32(squareData.y)) / f32(h) - 0.5) * 2. * f32(h) / f32(max(w, h)); + let cellFlags = currentStateData >> 24; + var cell = f32(currentStateData & 0xFFFFFF); + if (cellFlags == 1u) { + cell = -1.; + } + if (cellFlags == 2u) { + cell = -2.; + } + if (cellFlags == 3u) { + cell = -3.; + } + return VertexOut(vec4f(x, y, 0., 1.), cell); + }`) + .$uses({ + sizeData: sizeUniform, + }); + +const fragment = tgpu['~unstable'] + .fragmentFn({ cell: d.f32 }, d.vec4f) + .does(/* wgsl */ `(@location(0) cell: f32) -> @location(0) vec4f { + if (cell == -1.) { + return vec4f(0.5, 0.5, 0.5, 1.); + } + if (cell == -2.) { + return vec4f(0., 1., 0., 1.); + } + if (cell == -3.) { + return vec4f(1., 0., 0., 1.); + } + + let normalized = min(cell / f32(0xFF), 1.); - var r = f32((u32(cell) >> 16) & 0xFF)/255.; - var g = f32((u32(cell) >> 8) & 0xFF)/255.; - var b = f32(u32(cell) & 0xFF)/255.; - if (r > 0.) { g = 1.;} - if (g > 0.) { b = 1.;} - if (b > 0. && b < 0.5) { b = 0.5;} + if (normalized == 0.) { + return vec4f(); + } - return vec4f(r, g, b, 1.); -`; + let res = 1. / (1. + exp(-(normalized - 0.2) * 10.)); + return vec4f(0, 0, max(0.5, res), res); + }`); + +const vertexInstanceLayout = tgpu['~unstable'].vertexLayout( + (n: number) => d.arrayOf(d.u32, n), + 'instance', +); +const vertexLayout = tgpu['~unstable'].vertexLayout( + (n: number) => d.arrayOf(d.vec2f, n), + 'vertex', +); let drawCanvasData = new Uint32Array(options.size * options.size); @@ -313,65 +399,60 @@ let renderChanges: () => void; function resetGameData() { drawCanvasData = new Uint32Array(options.size * options.size); - const computePipeline = root.makeComputePipeline({ - workgroupSize: [options.workgroupSize, options.workgroupSize], - code: computeWGSL, - }); - - const renderPipeline = root.makeRenderPipeline({ - vertex: { - code: vertWGSL, - output: { - [builtin.position.s]: 'pos', - cell: d.f32, - }, - }, - fragment: { - code: fragWGSL, - target: [{ format: presentationFormat }], - }, - primitive: { topology: 'triangle-strip' }, - }); + const compute = tgpu['~unstable'] + .computeFn([d.builtin.globalInvocationId], { + workgroupSize: [options.workgroupSize, options.workgroupSize], + }) + .does(/* wgsl */ `(@builtin(global_invocation_id) gid: vec3u) { + decideWaterLevel(gid.x, gid.y); + }`) + .$uses({ decideWaterLevel }); + + const computePipeline = root['~unstable'] + .withCompute(compute) + .createPipeline(); + const renderPipeline = root['~unstable'] + .withVertex(vertex, { + squareData: vertexLayout.attrib, + currentStateData: vertexInstanceLayout.attrib, + }) + .withFragment(fragment, { + format: presentationFormat, + }) + .withPrimitive({ + topology: 'triangle-strip', + }) + .createPipeline() + .with(vertexLayout, squareBuffer) + .with(vertexInstanceLayout, currentStateBuffer); currentStateBuffer.write(Array.from({ length: 1024 ** 2 }, () => 0)); nextStateBuffer.write(Array.from({ length: 1024 ** 2 }, () => 0)); sizeBuffer.write(d.vec2u(options.size, options.size)); render = () => { - const view = context.getCurrentTexture().createView(); - // compute - computePipeline.execute({ - workgroups: [ - options.size / options.workgroupSize, - options.size / options.workgroupSize, - ], - }); + computePipeline.dispatchWorkgroups( + options.size / options.workgroupSize, + options.size / options.workgroupSize, + ); // render - renderPipeline.execute({ - colorAttachments: [ - { - view, - loadOp: 'clear', - storeOp: 'store', - }, - ], - vertexCount: 4, - instanceCount: options.size ** 2, - }); - - root.flush(); - - currentStateBuffer.write( - // The atomic<> prevents this from being a 1-to-1 match. - nextStateBuffer as unknown as TgpuBuffer>, - ); + renderPipeline + .withColorAttachment({ + view: context.getCurrentTexture().createView(), + clearValue: [0, 0, 0, 0], + loadOp: 'clear' as const, + storeOp: 'store' as const, + }) + .draw(4, options.size ** 2); + + root['~unstable'].flush(); + + currentStateBuffer.copyFrom(nextStateBuffer); }; applyDrawCanvas = () => { - const commandEncoder = root.device.createCommandEncoder(); - for (let i = 0; i < options.size; i++) { for (let j = 0; j < options.size; j++) { if (drawCanvasData[j * options.size + i] === 0) { @@ -380,7 +461,7 @@ function resetGameData() { const index = j * options.size + i; root.device.queue.writeBuffer( - currentStateBuffer.buffer, + nextStateBuffer.buffer, index * Uint32Array.BYTES_PER_ELEMENT, drawCanvasData, index, @@ -389,24 +470,21 @@ function resetGameData() { } } - root.device.queue.submit([commandEncoder.finish()]); drawCanvasData.fill(0); }; renderChanges = () => { - const view = context.getCurrentTexture().createView(); - renderPipeline.execute({ - colorAttachments: [ - { - view, - loadOp: 'clear', - storeOp: 'store', - }, - ], - vertexCount: 4, - instanceCount: options.size ** 2, - }); - root.flush(); + renderPipeline + .withColorAttachment({ + view: context.getCurrentTexture().createView(), + clearValue: [0, 0, 0, 0], + loadOp: 'clear' as const, + storeOp: 'store' as const, + }) + .with(vertexLayout, squareBuffer) + .with(vertexInstanceLayout, currentStateBuffer) + .draw(4, options.size ** 2); + root['~unstable'].flush(); }; createSampleScene(); @@ -416,29 +494,25 @@ function resetGameData() { let isDrawing = false; let isErasing = false; +let longTouchTimeout: number | null = null; +let touchMoved = false; -canvas.onmousedown = (event) => { +const startDrawing = (erase: boolean) => { isDrawing = true; - isErasing = event.button === 2; + isErasing = erase; }; -canvas.onmouseup = () => { +const stopDrawing = () => { isDrawing = false; renderChanges(); -}; - -canvas.onmousemove = (event) => { - if (!isDrawing) { - return; + if (longTouchTimeout) { + clearTimeout(longTouchTimeout); + longTouchTimeout = null; } +}; - const cellSize = canvas.width / options.size; - const x = Math.floor((event.offsetX * window.devicePixelRatio) / cellSize); - const y = - options.size - - Math.floor((event.offsetY * window.devicePixelRatio) / cellSize) - - 1; - const allAffectedCells = []; +const handleDrawing = (x: number, y: number) => { + const allAffectedCells = new Set<{ x: number; y: number }>(); for (let i = -options.brushSize; i <= options.brushSize; i++) { for (let j = -options.brushSize; j <= options.brushSize; j++) { if ( @@ -448,7 +522,7 @@ canvas.onmousemove = (event) => { y + j >= 0 && y + j < options.size ) { - allAffectedCells.push({ x: x + i, y: y + j }); + allAffectedCells.add({ x: x + i, y: y + j }); } } } @@ -469,6 +543,66 @@ canvas.onmousemove = (event) => { renderChanges(); }; +canvas.onmousedown = (event) => { + startDrawing(event.button === 2); +}; + +canvas.onmouseup = stopDrawing; + +canvas.onmousemove = (event) => { + if (!isDrawing) { + return; + } + + const cellSize = canvas.width / options.size; + const x = Math.floor((event.offsetX * window.devicePixelRatio) / cellSize); + const y = + options.size - + Math.floor((event.offsetY * window.devicePixelRatio) / cellSize) - + 1; + + handleDrawing(x, y); +}; + +canvas.ontouchstart = (event) => { + event.preventDefault(); + touchMoved = false; + longTouchTimeout = window.setTimeout(() => { + if (!touchMoved) { + startDrawing(true); + } + }, 500); + startDrawing(false); +}; + +canvas.ontouchend = (event) => { + event.preventDefault(); + stopDrawing(); +}; + +canvas.ontouchmove = (event) => { + event.preventDefault(); + touchMoved = true; + if (!isDrawing) { + return; + } + + const touch = event.touches[0]; + const cellSize = canvas.width / options.size; + const canvasPos = canvas.getBoundingClientRect(); + const x = Math.floor( + ((touch.clientX - canvasPos.left) * window.devicePixelRatio) / cellSize, + ); + const y = + options.size - + Math.floor( + ((touch.clientY - canvasPos.top) * window.devicePixelRatio) / cellSize, + ) - + 1; + + handleDrawing(x, y); +}; + const createSampleScene = () => { const middlePoint = Math.floor(options.size / 2); const radius = Math.floor(options.size / 8); @@ -536,7 +670,7 @@ onFrame((deltaTime: number) => { export const controls = { size: { - initial: '64', + initial: '32', options: [16, 32, 64, 128, 256, 512, 1024].map((x) => x.toString()), onSelectChange: (value: string) => { options.size = Number.parseInt(value); @@ -545,7 +679,7 @@ export const controls = { }, 'timestep (ms)': { - initial: 15, + initial: 50, min: 15, max: 100, step: 1, @@ -555,7 +689,7 @@ export const controls = { }, 'steps per timestep': { - initial: 10, + initial: 1, min: 1, max: 50, step: 1, @@ -565,7 +699,7 @@ export const controls = { }, 'workgroup size': { - initial: '16', + initial: '1', options: [1, 2, 4, 8, 16].map((x) => x.toString()), onSelectChange: (value: string) => { options.workgroupSize = Number.parseInt(value); @@ -574,23 +708,23 @@ export const controls = { }, viscosity: { - initial: 1000, - min: 10, - max: 1000, - step: 1, + initial: 0, + min: 0, + max: 1, + step: 0.01, onSliderChange: (value: number) => { - options.viscosity = value; - viscosityBuffer.write(value); + options.viscosity = 1000 - value * 990; + viscosityBuffer.write(options.viscosity); }, }, 'brush size': { - initial: 0, - min: 0, - max: 10, + initial: 1, + min: 1, + max: 20, step: 1, onSliderChange: (value: number) => { - options.brushSize = value; + options.brushSize = value - 1; }, }, diff --git a/packages/typegpu/src/core/buffer/buffer.ts b/packages/typegpu/src/core/buffer/buffer.ts index caee7a449..d3cab4f6e 100644 --- a/packages/typegpu/src/core/buffer/buffer.ts +++ b/packages/typegpu/src/core/buffer/buffer.ts @@ -7,6 +7,7 @@ import type { WgslTypeLiteral } from '../../data/wgslTypes'; import type { Storage } from '../../extension'; import type { TgpuNamable } from '../../namable'; import type { Infer } from '../../shared/repr'; +import type { MemIdentity } from '../../shared/repr'; import type { UnionToIntersection } from '../../shared/utilityTypes'; import { isGPUBuffer } from '../../types'; import type { ExperimentalTgpuRoot } from '../root/rootTypes'; @@ -50,7 +51,7 @@ export interface TgpuBuffer extends TgpuNamable { $addFlags(flags: GPUBufferUsageFlags): this; write(data: Infer): void; - copyFrom(srcBuffer: TgpuBuffer): void; + copyFrom(srcBuffer: TgpuBuffer>): void; read(): Promise>; destroy(): void; } @@ -228,7 +229,7 @@ class TgpuBufferImpl implements TgpuBuffer { device.queue.writeBuffer(gpuBuffer, 0, hostBuffer, 0, size); } - copyFrom(srcBuffer: TgpuBuffer): void { + copyFrom(srcBuffer: TgpuBuffer>): void { if (this.buffer.mapState === 'mapped') { throw new Error('Cannot copy to a mapped buffer.'); } diff --git a/packages/typegpu/src/data/alignmentOf.ts b/packages/typegpu/src/data/alignmentOf.ts index afe3a2ff3..af10cfe18 100644 --- a/packages/typegpu/src/data/alignmentOf.ts +++ b/packages/typegpu/src/data/alignmentOf.ts @@ -34,6 +34,7 @@ const knownAlignmentMap: Record = { mat2x2f: 8, mat3x3f: 16, mat4x4f: 16, + atomic: 4, }; function computeAlignment(data: object): number { diff --git a/packages/typegpu/src/data/array.ts b/packages/typegpu/src/data/array.ts index 981e1deb9..bff20d3f8 100644 --- a/packages/typegpu/src/data/array.ts +++ b/packages/typegpu/src/data/array.ts @@ -1,4 +1,4 @@ -import type { Infer } from '../shared/repr'; +import type { Infer, MemIdentity } from '../shared/repr'; import type { Exotic } from './exotic'; import { sizeOf } from './sizeOf'; import type { AnyWgslData, WgslArray } from './wgslTypes'; @@ -48,6 +48,8 @@ class TgpuArrayImpl public readonly '~repr'!: Infer[]; /** Type-token, not available at runtime */ public readonly '~exotic'!: WgslArray>; + /** Type-token, not available at runtime */ + public readonly '~memIdent'!: WgslArray>; constructor( public readonly elementType: TElement, diff --git a/packages/typegpu/src/data/atomic.ts b/packages/typegpu/src/data/atomic.ts index fe250c972..7a632e7fc 100644 --- a/packages/typegpu/src/data/atomic.ts +++ b/packages/typegpu/src/data/atomic.ts @@ -1,4 +1,4 @@ -import type { Infer } from '../shared/repr'; +import type { Infer, MemIdentity } from '../shared/repr'; import type { Exotic } from './exotic'; import type { Atomic, I32, U32 } from './wgslTypes'; @@ -29,6 +29,8 @@ class AtomicImpl implements Atomic { public readonly type = 'atomic'; /** Type-token, not available at runtime */ public readonly '~repr'!: Infer; + /** Type-token, not available at runtime */ + public readonly '~memIdent'!: MemIdentity; constructor(public readonly inner: TSchema) {} } diff --git a/packages/typegpu/src/data/attributes.ts b/packages/typegpu/src/data/attributes.ts index d1013f428..428693e41 100644 --- a/packages/typegpu/src/data/attributes.ts +++ b/packages/typegpu/src/data/attributes.ts @@ -1,4 +1,4 @@ -import type { Infer } from '../shared/repr'; +import type { Infer, MemIdentity } from '../shared/repr'; import { alignmentOf } from './alignmentOf'; import { type AnyData, @@ -337,6 +337,9 @@ class DecoratedImpl implements Decorated { public readonly type = 'decorated'; + public readonly '~memIdent'!: TAttribs extends Location[] + ? MemIdentity | Decorated, TAttribs> + : Decorated, TAttribs>; } class LooseDecoratedImpl< diff --git a/packages/typegpu/src/data/sizeOf.ts b/packages/typegpu/src/data/sizeOf.ts index 167ed4f4f..90e2dbb0b 100644 --- a/packages/typegpu/src/data/sizeOf.ts +++ b/packages/typegpu/src/data/sizeOf.ts @@ -72,6 +72,7 @@ const knownSizesMap: Record = { sint32x4: 16, 'unorm10-10-10-2': 4, 'unorm8x4-bgra': 4, + atomic: 4, } satisfies Partial>; function sizeOfStruct(struct: WgslStruct) { diff --git a/packages/typegpu/src/data/wgslTypes.ts b/packages/typegpu/src/data/wgslTypes.ts index 865d17e93..b1df7c50d 100644 --- a/packages/typegpu/src/data/wgslTypes.ts +++ b/packages/typegpu/src/data/wgslTypes.ts @@ -1,4 +1,14 @@ -import type { Infer, InferRecord } from '../shared/repr'; +import type { + Infer, + InferRecord, + MemIdentity, + MemIdentityRecord, +} from '../shared/repr'; + +type DecoratedLocation = Decorated< + T, + Location[] +>; export interface NumberArrayView { readonly length: number; @@ -574,12 +584,14 @@ export interface I32 { readonly type: 'i32'; /** Type-token, not available at runtime */ readonly '~repr': number; + readonly '~memIdent': I32 | Atomic | DecoratedLocation; } export interface U32 { readonly type: 'u32'; /** Type-token, not available at runtime */ readonly '~repr': number; + readonly '~memIdent': U32 | Atomic | DecoratedLocation; } export interface Vec2f { @@ -681,6 +693,7 @@ export interface WgslStruct< readonly propTypes: TProps; /** Type-token, not available at runtime */ readonly '~repr': InferRecord; + readonly '~memIdent': WgslStruct>; } // biome-ignore lint/suspicious/noExplicitAny: > @@ -692,6 +705,7 @@ export interface WgslArray { readonly elementType: TElement; /** Type-token, not available at runtime */ readonly '~repr': Infer[]; + readonly '~memIdent': WgslArray>; } /** @@ -702,6 +716,7 @@ export interface Atomic { readonly inner: TInner; /** Type-token, not available at runtime */ readonly '~repr': Infer; + readonly '~memIdent': MemIdentity; } export interface Align { @@ -745,6 +760,9 @@ export interface Decorated< readonly attribs: TAttribs; /** Type-token, not available at runtime */ readonly '~repr': Infer; + readonly '~memIdent': TAttribs extends Location[] + ? MemIdentity | Decorated, TAttribs> + : Decorated, TAttribs>; } export const wgslTypeLiterals = [ diff --git a/packages/typegpu/src/shared/repr.ts b/packages/typegpu/src/shared/repr.ts index a89b73c7c..0a3bec5ea 100644 --- a/packages/typegpu/src/shared/repr.ts +++ b/packages/typegpu/src/shared/repr.ts @@ -1,3 +1,5 @@ +import type { AnyData } from '../data/dataTypes'; + /** * Extracts the inferred representation of a resource. * @example @@ -9,3 +11,15 @@ export type Infer = T extends { readonly '~repr': infer TRepr } ? TRepr : T; export type InferRecord> = { [Key in keyof T]: Infer; }; + +export type MemIdentity = T extends { + readonly '~memIdent': infer TMemIdent extends AnyData; +} + ? TMemIdent + : T; + +export type MemIdentityRecord< + T extends Record, +> = { + [Key in keyof T]: MemIdentity; +}; diff --git a/packages/typegpu/tests/buffer.test.ts b/packages/typegpu/tests/buffer.test.ts index 39776b776..500ffdf87 100644 --- a/packages/typegpu/tests/buffer.test.ts +++ b/packages/typegpu/tests/buffer.test.ts @@ -181,4 +181,65 @@ describe('TgpuBuffer', () => { .$usage('storage'); }).toThrow(); }); + + it('should be able to copy from a buffer identical on the byte level', ({ + root, + }) => { + const buffer = root.createBuffer(d.u32); + const copy = root.createBuffer(d.atomic(d.u32)); + + buffer.copyFrom(copy); + + const buffer2 = root.createBuffer( + d.struct({ + one: d.location(0, d.f32), // does nothing outside an IO struct + two: d.atomic(d.u32), + three: d.arrayOf(d.u32, 3), + }), + ); + const copy2 = root.createBuffer( + d.struct({ + one: d.f32, + two: d.u32, + three: d.arrayOf(d.atomic(d.u32), 3), + }), + ); + + buffer2.copyFrom(copy2); + + const buffer3 = root.createBuffer( + d.struct({ + one: d.size(16, d.f32), + two: d.atomic(d.u32), + }), + ); + + const copy3 = root.createBuffer( + d.struct({ + one: d.f32, + two: d.u32, + }), + ); + + const copy31 = root.createBuffer( + d.struct({ + one: d.location(0, d.f32), + two: d.u32, + }), + ); + + const copy32 = root.createBuffer( + d.struct({ + one: d.size(12, d.f32), + two: d.u32, + }), + ); + + // @ts-expect-error + buffer3.copyFrom(copy3); + // @ts-expect-error + buffer3.copyFrom(copy31); + // @ts-expect-error + buffer3.copyFrom(copy32); + }); }); diff --git a/packages/typegpu/tests/data/atomic.test.ts b/packages/typegpu/tests/data/atomic.test.ts index 20cfa59b7..d054834f1 100644 --- a/packages/typegpu/tests/data/atomic.test.ts +++ b/packages/typegpu/tests/data/atomic.test.ts @@ -23,6 +23,7 @@ describe('d.atomic', () => { type: 'u32' as const, '~repr': undefined as unknown as number, '~exotic': undefined as unknown as d.U32, + '~memIdent': undefined as unknown as d.U32, }; const u32Atomic = d.atomic(exoticU32); @@ -34,6 +35,7 @@ describe('d.atomic', () => { type: 'i32' as const, '~repr': undefined as unknown as number, '~exotic': undefined as unknown as d.I32, + '~memIdent': undefined as unknown as d.I32, }; const i32Atomic = d.atomic(exoticI32); @@ -51,11 +53,13 @@ describe('d.isAtomic', () => { const atomicU32 = d.atomic({ type: 'u32', '~repr': undefined as unknown as number, + '~memIdent': undefined as unknown as d.U32, }); const atomicI32 = d.atomic({ type: 'i32', '~repr': undefined as unknown as number, + '~memIdent': undefined as unknown as d.I32, }); expect(d.isAtomic(atomicU32)).toEqual(true);