diff --git a/index.js b/index.js index 5509dca..69c25af 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ import { GraphicsDevice } from './src/core/graphics-device.js' -import { PowerPreference } from './src/enum.js' +import { PowerPreference, VertexFormat } from './src/enum.js' +import { ShaderType } from './src/utils/internal-enums.js' async function main() { const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('webgpu-canvas')) @@ -27,25 +28,48 @@ async function main() { } const shaderCode = ` - @vertex - fn vs_main(@location(0) in_pos : vec3) -> @builtin(position) vec4 { - return vec4(in_pos, 1.0); - } + @group(0) @binding(0) + var transform : mat4x4; - @fragment - fn fs_main() -> @location(0) vec4 { - return vec4(0.0, 0.5, 1.0, 1.0); - } - ` + @vertex + fn vs_main(@location(0) in_pos : vec3) -> @builtin(position) vec4 { + return transform * vec4(in_pos, 1.0); + } - const shaderModule = graphicsDevice.createShaderModule(shaderCode, 'TriangleShader') + @fragment + fn fs_main() -> @location(0) vec4 { + return vec4(0.0, 0.5, 1.0, 1.0); + } + `; + + const shaderModule = graphicsDevice.createShaderModule( + shaderCode, + 'SquareShader', + ShaderType.Vertex | ShaderType.Fragment + ) const vertices = new Float32Array([ - 0.0, 0.5, 0.0, + -0.5, 0.5, 0.0, + -0.5, -0.5, 0.0, + 0.5, 0.5, 0.0, + + 0.5, 0.5, 0.0, -0.5, -0.5, 0.0, 0.5, -0.5, 0.0 ]) + const indices = new Uint16Array([ + 0, 1, 2, + 3, 4, 5 + ]) + + const indexBuffer = graphicsDevice.createBuffer({ + size: indices.byteLength, + usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST + }) + + indexBuffer.write(indices) + const vertexBuffer = graphicsDevice.createBuffer( { label: 'TriangleVertexBuffer', @@ -80,35 +104,29 @@ async function main() { const bindGroupLayout = graphicsDevice.createBindGroupLayout(bindings, 'UniformLayout') + const material = graphicsDevice.createMaterial( + { vertex: shaderModule, fragment: shaderModule }, + [bindGroupLayout] + ) - const pipelineLayout = graphicsDevice.createPipelineLayout([bindGroupLayout], 'PipelineLayout') - const pipeline = graphicsDevice.createRenderPipeline({ - label: 'TrianglePipeline', - layout: pipelineLayout, - vertex: { - module: shaderModule.handle, - entryPoint: 'vs_main', - buffers: [ - { - arrayStride: 3 * 4, - attributes: [ - { shaderLocation: 0, offset: 0, format: 'float32x3' } - ] - } - ] - }, - fragment: { - module: shaderModule.handle, - entryPoint: 'fs_main', - targets: [ - { format: graphicsDevice.swapChain.format } - ] - }, - primitive: { - topology: 'triangle-list', - }, - }) + const vertexBufferLayout = { + arrayStride: 3 * 4, + attributes: [ + { shaderLocation: 0, offset: 0, format: VertexFormat.Float32x3 } + ] + } + + const pipelineDescriptor = material.getRenderPipelineDescriptor( + [vertexBufferLayout], + 'SquarePipeline' + ) + + pipelineDescriptor.fragment.targets = [ + { format: graphicsDevice.swapChain.format } + ] + + const pipeline = graphicsDevice.createRenderPipeline(pipelineDescriptor) /** @type {Array} */ const entries = [{ @@ -123,6 +141,7 @@ async function main() { return } + graphicsDevice.queue.writeBuffer(uniformBuffer.handle, 0, matrixData) const commandRecorder = graphicsDevice.createCommandRecorder('FrameCommands') const passEncoder = commandRecorder.beginRenderPass() @@ -130,8 +149,9 @@ async function main() { if (passEncoder) { passEncoder.setPipeline(pipeline.handle) passEncoder.setVertexBuffer(0, vertexBuffer.handle) + passEncoder.setIndexBuffer(indexBuffer.handle, 'uint16') passEncoder.setBindGroup(0, uniformBindGroup.handle) - passEncoder.draw(3) + passEncoder.drawIndexed(indices.length) commandRecorder.endRenderPass() } diff --git a/jsconfig.json b/jsconfig.json index be1904d..5f69749 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,13 +1,11 @@ { "compilerOptions": { - "module": "es2020", + "module": "es2022", + "moduleResolution": "node", "target": "es6", "lib": ["es2022", "dom"], "types": ["@webgpu/types"], - "checkJs": true, - "paths": { - "/*": ["./*"] - } + "checkJs": true" }, "exclude": [ "node_modules" diff --git a/package-lock.json b/package-lock.json index c5cb17f..b75f5be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "wgpu", "version": "1.0.0", "license": "ISC", + "dependencies": { + "wgsl_reflect": "^1.2.0" + }, "devDependencies": { "@webgpu/types": "^0.1.60" } @@ -18,6 +21,12 @@ "integrity": "sha512-8B/tdfRFKdrnejqmvq95ogp8tf52oZ51p3f4QD5m5Paey/qlX4Rhhy5Y8tgFMi7Ms70HzcMMw3EQjH/jdhTwlA==", "dev": true, "license": "BSD-3-Clause" + }, + "node_modules/wgsl_reflect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/wgsl_reflect/-/wgsl_reflect-1.2.0.tgz", + "integrity": "sha512-bDYcmWfbg4WsrBmPv6lsyjqXx02r8dkNAzR77OCNqIcR8snO4aNSBTjir9zqgR7rLnw6PaisiZxtCitSCIUlnQ==", + "license": "MIT" } } } diff --git a/package.json b/package.json index 285509e..c540907 100644 --- a/package.json +++ b/package.json @@ -12,5 +12,8 @@ "description": "", "devDependencies": { "@webgpu/types": "^0.1.60" + }, + "dependencies": { + "wgsl_reflect": "^1.2.0" } } diff --git a/src/core/graphics-device.js b/src/core/graphics-device.js index 56ce122..547d0f6 100644 --- a/src/core/graphics-device.js +++ b/src/core/graphics-device.js @@ -33,6 +33,8 @@ import { Texture } from '../resources/texture.js' */ import { Sampler } from '../resources/sampler.js' +import { ResourceType } from '../utils/internal-enums.js' +import { Material } from '../resources/material.js' class GraphicsDeviceBuilder { _canvas @@ -280,13 +282,15 @@ export class GraphicsDevice extends EventEmitter { * Creates a shader module from WGSL code. * @param {string} code * @param {string} [label] + * @param {number} [shaderType] + * @param {string} [entryPoint] * @returns {ShaderModule} */ - createShaderModule(code, label) { + createShaderModule(code, label, shaderType, entryPoint) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } try { - return ShaderModule.create(this.device, { code, label }) + return ShaderModule.create(this.device, { code, label, shaderType, entryPoint }) } catch (err) { throw WebGPUObjectError.from(err, ShaderModule) } @@ -301,13 +305,27 @@ export class GraphicsDevice extends EventEmitter { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } try { - const gpuPipeline = this.device.createRenderPipeline(descriptor) - return new RenderPipeline(gpuPipeline, descriptor.label) + const pipeline = this.device.createRenderPipeline(descriptor) + return new RenderPipeline(pipeline, descriptor.label) } catch (err) { throw WebGPUObjectError.from(err, RenderPipeline) } } + /** + * @param {import('../resources/material.js').ShaderPairDescriptor} shaders + * @param {BindGroupLayout[]} bindGroupLayouts + */ + createMaterial(shaders, bindGroupLayouts) { + if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } + + try { + return new Material(this.device, shaders, bindGroupLayouts) + } catch (err) { + throw WebGPUObjectError.from(err, Material) + } + } + /** * Creates a CommandRecorder to begin recording GPU commands. * @param {string} [label] @@ -332,6 +350,41 @@ export class GraphicsDevice extends EventEmitter { }) } + /** + * @param {BindGroupEntry} binding + * @returns {GPUBindingResource} + */ + _getBindingResource(binding) { + const resource = binding.resource + switch (resource.resourceType) { + case ResourceType.Sampler: + return resource.handle + + case ResourceType.TextureView: + return 'textureView' in binding ? + binding.textureView : + resource.getView() + + case ResourceType.Buffer: + const bufSize = resource.size + + const offset = 'offset' in binding ? binding.offset : 0 + const size = 'size' in binding ? + binding.size : + bufSize - offset + + if (offset + size > bufSize) { + throw BufferError.outOfBounds(offset, size) + } + + return { + buffer: resource.handle, + offset, + size + } + } + } + /** * @param {BindGroupLayout} layout * @param {BindGroupEntry[]} bindings @@ -343,7 +396,7 @@ export class GraphicsDevice extends EventEmitter { const entries = bindings.map(def => ({ binding: def.binding, - resource: def.resource.asBindingResource(def) + resource: this._getBindingResource(def) })) return BindGroup.create(this.device, { diff --git a/src/rendering/render-graph.js b/src/rendering/render-graph.js new file mode 100644 index 0000000..7ebc11a --- /dev/null +++ b/src/rendering/render-graph.js @@ -0,0 +1,3 @@ +export class RenderGraph { +} + diff --git a/src/resources/buffer.js b/src/resources/buffer.js index ac4fc45..8e8d408 100644 --- a/src/resources/buffer.js +++ b/src/resources/buffer.js @@ -1,4 +1,5 @@ import { BufferError, WebGPUError } from '../utils/errors.js' +import { ResourceType } from '../utils/internal-enums.js' /** @import { TypedArray, TypedArrayConstructor } from '../utils.js' */ /** @import { BindGroupEntry, BindingResource, BufferBindingResource } from '../core/graphics-device.js' */ @@ -24,6 +25,10 @@ export class Buffer { return this._handle.usage } + get resourceType() { + return ResourceType.Buffer + } + /** * @param {GPUDevice} device * @param {GPUBuffer} texture @@ -168,24 +173,6 @@ export class Buffer { return result } - /** - * @param {BindGroupEntry} entry - */ - asBindingResource(entry) { - const offset = 'offset' in entry ? entry.offset : 0 - const size = 'size' in entry ? entry.size : this.size - offset - - if (offset + size > this.size) { - throw BufferError.outOfBounds(offset, size) - } - - return { - buffer: this._handle, - offset, - size - } - } - destroy() { this._handle?.destroy() this._handle = null diff --git a/src/resources/material.js b/src/resources/material.js new file mode 100644 index 0000000..6da1977 --- /dev/null +++ b/src/resources/material.js @@ -0,0 +1,87 @@ +import { ResourceType, TemplateInfo, VariableInfo } from 'wgsl_reflect' +import { BindGroupLayout } from './bind-group-layout.js' +import { ShaderModule } from './shader-module.js' +import { MaterialError } from '../utils/errors.js' +import { GroupBindingMap } from '../utils/bindings.js' +import { accessToBufferType, accessToStorageTextureAccess, parseTextureType, typeToViewDimension, wgslToWgpuFormat } from '../utils/wgsl-to-wgpu.js' + +/** @import {Either} from '../utils.js' */ +/** @import { WGSLAccess, WGSLTextureType } from '../utils/wgsl-to-wgpu.js' */ + +/** + * @typedef Shader2 + * @property {ShaderModule} vertex + * @property {ShaderModule} fragment + * + * @typedef {Omit} VertexOnly + * @typedef {Omit} FragmentOnly + * + * @typedef Unified1 + * @property {ShaderModule} shaderModule + */ + +/** + * @typedef { + (Shader2 | Either) | + Unified1 + * } ShaderPairDescriptor + */ + +export class Material { + _device + _shaders + _bindGroupLayouts + _pipelineLayout + + get shaders() { + return this._shaders + } + + get bindGroupLayouts() { + return this._bindGroupLayouts + } + + /** + * @param {GPUDevice} device + * @param {ShaderPairDescriptor} shaders + * @param {BindGroupLayout[]} bindGroupLayouts + */ + constructor(device, shaders, bindGroupLayouts) { + this._device = device + this._shaders = Material._parseShaders(shaders) + this._bindGroupLayouts = bindGroupLayouts + + if (bindGroupLayouts && bindGroupLayouts.length > 0) { + this._pipelineLayout = device.createPipelineLayout({ + bindGroupLayouts: bindGroupLayouts.map(bgl => bgl.handle) + }) + } + } + /** + * Attempts to handle shader modules which represent multiple + * shader types. + * + * @param {ShaderPairDescriptor} shaders + * @returns {UnifiedShader | ShaderPair} + */ + static _parseShaders(shaders) { + if (shaders == null) { + throw MaterialError.missingShader('both') + } + + if (shaders instanceof ShaderModule) { + return new UnifiedShader(shaders) + } + + const hasVertex = 'vertex' in shaders + const hasFragment = 'fragment' in shaders + + if (hasVertex && hasFragment) { + return new ShaderPair(shaders.vertex, shaders.fragment) + } else if (!hasFragment) { + throw MaterialError.missingShader('fragment') + } else if (!hasVertex) { + throw MaterialError.missingShader('vertex') + } + } +} diff --git a/src/resources/sampler.js b/src/resources/sampler.js index d5ba5a1..860631c 100644 --- a/src/resources/sampler.js +++ b/src/resources/sampler.js @@ -1,4 +1,5 @@ import { WebGPUObjectError } from '../utils/errors.js' +import { ResourceType } from '../utils/internal-enums.js' /** @import { BindGroupEntry } from '../core/graphics-device.js' */ @@ -14,6 +15,10 @@ export class Sampler { return this._handle.label } + get resourceType() { + return ResourceType.Sampler + } + /** * @param {GPUDevice} device * @param {GPUSampler} sampler @@ -37,12 +42,5 @@ export class Sampler { throw WebGPUObjectError.from(err, Sampler) } } - - /** - * @param {BindGroupEntry} _entry - */ - asBindingResource(_entry) { - return this._handle - } } diff --git a/src/resources/shader-module.js b/src/resources/shader-module.js index c63ca16..3038594 100644 --- a/src/resources/shader-module.js +++ b/src/resources/shader-module.js @@ -1,35 +1,314 @@ +import { ResourceType, VariableInfo, WgslReflect } from 'wgsl_reflect' import { WebGPUObjectError } from '../utils/errors.js' +import { BindingMap, GroupBindingMap, VariableStageInfo } from '../utils/bindings.js' + +import { + accessToBufferType, + accessToStorageTextureAccess, + parseTextureType, + typeToViewDimension, + wgslToWgpuFormat +} from '../utils/wgsl-to-wgpu.js' + +/** @import { WGSLAccess, WGSLSampledType, WGSLSamplerType, WGSLTextureType } from '../utils/wgsl-to-wgpu.js' */ export class ShaderModule { _handle + _code - get label() { - return this._handle.label - } + /** @type {WgslReflect | undefined} */ + _reflection get handle() { return this._handle } - /** - * @param {GPUShaderModule} module - */ - constructor(module) { - this._handle = module + get label() { + return this._handle.label } /** * @param {GPUDevice} device - * @param {GPUShaderModuleDescriptor} descriptor + * @param {string} code + * @param {string} [label] */ - static create(device, descriptor) { + constructor(device, code, label) { + this._code = code + try { - return new ShaderModule( - device.createShaderModule(descriptor) - ) + this._handle = device.createShaderModule({ code, label }) } catch (err) { throw WebGPUObjectError.from(err, ShaderModule) } } + + reflect() { + if (this._reflection == null) { + this._reflection = new WgslReflect(this._code) + this._code = undefined + } + + return this._reflection + } +} + +export class ReflectedShader { + /** + * @param {WgslReflect} reflection + * @param {GPUShaderStageFlags} stages + * @param {GroupBindingMap} [out=new GroupBindingMap()] + */ + _getBindingsForStage(reflection, stages, out = new GroupBindingMap()) { + const groups = reflection.getBindGroups() + + groups.forEach((bindings, groupIndex) => { + if (!out.has(groupIndex)) { + out.set(groupIndex, new BindingMap()) + } + + const bindingsMap = out.get(groupIndex) + + bindings.filter(x => x != null).forEach(variableInfo => { + if (!bindingsMap.has(variableInfo.binding)) { + bindingsMap.set(variableInfo.binding, { variableInfo, stages }) + } else { + bindingsMap.get(variableInfo.binding).stages |= stages + } + }) + }) + + return out + } + /** + * @param {Map} map + * @returns {number[]} + */ + _sortKeyIndices(map) { + return Array.from(map.keys()).sort((a, b) => a - b) + } + + /** + * @param {VariableInfo} _variableInfo + * @returns {GPUBufferBindingLayout} + */ + _parseUniform(_variableInfo) { + return { + type: 'uniform', + // TODO: infer these two properties + hasDynamicOffset: false, + minBindingSize: 0 + } + } + + /** + * @param {VariableInfo} variableInfo + * @returns {GPUBufferBindingLayout} + */ + _parseStorage(variableInfo) { + return { + type: accessToBufferType( + /** @type {WGSLAccess} */ + (variableInfo.access) + ), + // TODO: infer these two properties + hasDynamicOffset: false, + minBindingSize: 0 + } + } + + /** + * @param {WGSLTextureType} type + * @param {WGSLSampledType} sampledType + * @returns {GPUTextureSampleType} + */ + _parseSampleType(type, sampledType) { + if (type.includes('depth')) { + return 'depth' + } + + switch (sampledType) { + case 'f32': + case 'i32': + case 'u32': + default: + return 'float' + } + } + + /** + * @param {VariableInfo} variableInfo + * @returns {GPUTextureBindingLayout} + */ + _parseTexture(variableInfo) { + const [type, sampledType] = parseTextureType(variableInfo.type.name) + + return { + sampleType: this._parseSampleType(type, sampledType), + viewDimension: typeToViewDimension(type), + multisampled: type.includes('multisampled') + } + } + + /** + * @param {WGSLSamplerType} type + * @returns {GPUSamplerBindingType} + */ + _parseSamplerType(type) { + switch (type) { + case 'sampler_comparison': + return 'comparison' + case 'sampler': + default: + return 'filtering' + } + } + + /** + * @param {VariableInfo} variableInfo + * @returns {GPUSamplerBindingLayout} + */ + _parseSampler(variableInfo) { + return { + type: this._parseSamplerType( + /** @type {WGSLSamplerType} */(variableInfo.type.name) + ) + } + } + + /** + * @param {VariableInfo} variableInfo + * @returns {GPUStorageTextureBindingLayout} + */ + _parseStorageTexture(variableInfo) { + const [type] = parseTextureType(variableInfo.type.name) + + return { + access: accessToStorageTextureAccess( + /** @type {WGSLAccess} */(variableInfo.access) + ), + format: wgslToWgpuFormat(variableInfo.type.name), + viewDimension: typeToViewDimension(type) + } + } + + /** + * @param {VariableStageInfo} variableStageInfo + * @returns {GPUBindGroupLayoutEntry} + */ + _variableInfoToEntry(variableStageInfo) { + const { stages: visibility, variableInfo } = variableStageInfo + + switch (variableInfo.resourceType) { + case ResourceType.Uniform: + return { + binding: variableInfo.binding, + visibility, + buffer: this._parseUniform(variableInfo) + } + case ResourceType.Storage: + return { + binding: variableInfo.binding, + visibility, + buffer: this._parseStorage(variableInfo) + } + case ResourceType.Texture: + return { + binding: variableInfo.binding, + visibility, + texture: this._parseTexture(variableInfo) + } + case ResourceType.Sampler: + return { + binding: variableInfo.binding, + visibility, + sampler: this._parseSampler(variableInfo) + } + case ResourceType.StorageTexture: + return { + binding: variableInfo.binding, + visibility, + storageTexture: this._parseStorageTexture(variableInfo) + } + default: + console.warn(`Unsupported resource type for reflection: ${ResourceType[variableInfo.resourceType]}`) + return + } + } + + /** + * @param {GroupBindingMap} groupBindings + */ + _createBindGroupLayoutEntries(groupBindings) { + const sortedGroupIndices = this._sortKeyIndices(groupBindings) + + return sortedGroupIndices.map(groupIndex => { + const bindingsMap = groupBindings.get(groupIndex) + return this._sortKeyIndices(bindingsMap).map(bindingIndex => ( + this._variableInfoToEntry(bindingsMap.get(bindingIndex)) + )) + }) + .filter(desc => desc.length > 0) + } +} + +export class UnifiedShader extends ReflectedShader { + _shader + + /** + * @param {ShaderModule} shader + */ + constructor(shader) { + super() + + this._shader = shader + } + + createBindGroupLayoutEntries() { + return this._createBindGroupLayoutEntries( + this._getBindingsForStage( + this._shader.reflect(), + GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT + ) + ) + } +} + +export class ReflectedShaderPair extends ReflectedShader { + _vertex + _fragment + + /** + * @param {ShaderModule} vertex + * @param {ShaderModule} [fragment] + */ + constructor(vertex, fragment) { + super() + + this._vertex = vertex + this._fragment = fragment + } + + _createGroupBindings() { + const groupBindings = new GroupBindingMap() + this.getBindingsForStage( + this._vertex.reflect(), + GPUShaderStage.VERTEX, + groupBindings + ) + + this.getBindingsForStage( + this._fragment.reflect(), + GPUShaderStage.FRAGMENT, + groupBindings + ) + + return groupBindings + } + + createBindGroupLayoutEntries() { + return this._createBindGroupLayoutEntries( + this._createGroupBindings() + ) + } } diff --git a/src/resources/texture.js b/src/resources/texture.js index 3dfefec..4a3774d 100644 --- a/src/resources/texture.js +++ b/src/resources/texture.js @@ -1,4 +1,5 @@ import { WebGPUObjectError } from '../utils/errors.js' +import { ResourceType } from '../utils/internal-enums.js' /** @import { BindGroupEntry } from '../core/graphics-device.js' */ @@ -45,6 +46,10 @@ export class Texture { return this._handle.label } + get resourceType() { + return ResourceType.TextureView + } + /** * @param {GPUDevice} device * @param {GPUTexture} texture @@ -91,13 +96,6 @@ export class Texture { } } - /** - * @param {BindGroupEntry} entry - */ - asBindingResource(entry) { - return 'textureView' in entry ? entry.textureView : this.getView() - } - getView() { return this.createDefaultView() } diff --git a/src/utils.js b/src/utils.js index d26029f..189552d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,3 +1,16 @@ +/** + * @template T, U + * @typedef { + { [P in keyof T]: T[P] } & + { [P in keyof U]?: never} + * } Only + */ + +/** + * @template T, U + * @typedef {Only | Only} Either + */ + /** * @template T * @typedef {new (...args: any[]) => T} Newable @@ -35,6 +48,15 @@ } TypedArrayConstructor */ +/** + * @typedef { + [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, + 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, + 2097152, 4194304, 8388608, 16777216, 33554432, 67108864, 134217728, 268435456, + 536870912, 1073741824, 2147483648] + * } PowersOfTwo + */ + /** * @template {number} T * @typedef {`${T}` extends `-${string}` | '0' | `${string}.${string}` ? never : T } PositiveInteger @@ -77,6 +99,17 @@ * @typedef {T extends String ? PascalCaseString : T extends ReadonlyArray ? Join, ''> : T} PascalCase */ +/** + * @template {readonly any[]} T + * @template V + * @template {any[]} [Acc=[]] + * @typedef { + T extends readonly [infer Head, ...infer Tail] + ? Head extends V ? Acc['length'] : IndexOf + : never + * } IndexOf + */ + /** * @template {string} T * @param {T} s @@ -125,6 +158,25 @@ export const Enum = (...values) => /** @type {Readonly<{ [K in T as PascalCase : never]: PowersOfTwo[ParseInt] }>} + */ +export const FlagEnum = (...values) => /** @type {never} */(Object.freeze( + values.reduce((acc, x, i) => { + const key = pascal(fromKebab(x)).toString() + acc[key] = 1 << i + return acc + }, {}) +)) + /** @typedef {(...args: any) => void} Listener */ export class EventEmitter { @@ -169,3 +221,91 @@ export class EventEmitter { } } } + +/** + * @template {{}} T + * @template {keyof T} K + * @param {T} obj + * @param {...K} keys + * @returns {Pick} + */ +export const pick = (obj, ...keys) => /** @type {Pick} */(Object.fromEntries( + keys + .filter(key => key in obj) + .map(key => [key, obj[key]]) +)) + +/** + * @template {{}} T + * @template {PropertyKey & keyof T} K + * @param {T} obj + * @param {...K} keys + * @returns {{ [Key in K]: Key extends keyof T ? T[Key] : never }} + */ +export const inclusivePick = (obj, ...keys) => /** @type {{[Key in K]: Key extends keyof T ? T[Key] : never}} */(Object.fromEntries( + keys.map(key => [key, obj[key]]) +)) + +/** + * @template {{}} T + * @template {PropertyKey & keyof T} K + * @param {T} obj + * @param {...K} keys + * @returns {Omit} + */ +export const omit = (obj, ...keys) => /** @type {Omit} */(Object.fromEntries( + Object.entries(obj) + .filter(([key]) => !keys.includes(/** @type {K} */(key))) +)) + +/** @template {number} T */ +export class Flags { + _value + + /** + * @param {T} value + */ + constructor(value) { + this._value = value + } + + /** + * Tests if `flag` exists in `bitflags` + * + * @template {number} T + * @param {T} flag + * @param {T} bitflags + */ + static has(flag, bitflags) { + return bitflags & flag + } + + /** + * Combine two flags + * + * @template {number} T + * @param {T} a + * @param {T} b + */ + static add(a, b) { + return a | b + } + + /** + * Tests if `flag` exists in these bitflags + * + * @param {T} flag + */ + has(flag) { + return Flags.has(flag, this._value) + } + + /** + * Add `flag` to these bitflags + * @param {T} flag + */ + add(flag) { + return Flags.add(flag, this._value) + } +} + diff --git a/src/utils/bindings.js b/src/utils/bindings.js new file mode 100644 index 0000000..a30f2f3 --- /dev/null +++ b/src/utils/bindings.js @@ -0,0 +1,28 @@ +import { VariableInfo } from 'wgsl_reflect' + +export class VariableStageInfo { + stages + variableInfo + + /** + * @param {GPUShaderStageFlags} stages + * @param {VariableInfo} variableInfo + */ + constructor(stages, variableInfo) { + this.stages = stages + this.variableInfo = variableInfo + } +} + +/** + * @extends Map + */ +export class BindingMap extends Map { } + +/** + * @extends Map + */ +export class GroupBindingMap extends Map { } + + + diff --git a/src/utils/errors.js b/src/utils/errors.js index 811fb11..12f8d38 100644 --- a/src/utils/errors.js +++ b/src/utils/errors.js @@ -78,3 +78,27 @@ export class BufferError extends WebGPUObjectError { return new BufferError(`buffer offset/size (${offset}/${size}) exceeds buffer dimensions`) } } + +export class MaterialError extends WebGPUObjectError { + /** + * @param {string} message + * @param {ErrorOptions} [options] + */ + constructor(message, options) { + super(`MaterialError: ${message}`, options) + } + + /** + * @param {Error} cause + */ + static from(cause) { + return new BufferError('could not create material', { cause }) + } + + /** + * @param {string} shaderType + */ + static missingShader(shaderType) { + return new BufferError(`missing ${shaderType} shader`) + } +} diff --git a/src/utils/internal-enums.js b/src/utils/internal-enums.js new file mode 100644 index 0000000..4f8f92e --- /dev/null +++ b/src/utils/internal-enums.js @@ -0,0 +1,16 @@ +import { FlagEnum } from '../utils.js' + +export const ShaderType = FlagEnum( + 'auto', + 'vertex', + 'fragment', + 'compute' +) + +export const ResourceType = Object.freeze({ + Sampler: 0, + TextureView: 1, + Buffer: 2, + ExternalTexture: 3 +}) + diff --git a/src/utils/wgsl-to-wgpu.js b/src/utils/wgsl-to-wgpu.js new file mode 100644 index 0000000..57ec775 --- /dev/null +++ b/src/utils/wgsl-to-wgpu.js @@ -0,0 +1,301 @@ +/** + * @typedef {'read' | 'write' | 'read_write'} WGSLAccess + * @typedef {'f32' | 'i32' | 'u32'} WGSLSampledType + */ + +import { BufferBindingType, StorageTextureAccess, TextureFormat } from '../enum.js' + +/** + * @typedef { + 'texture_1d' + | 'texture_2d' + | 'texture_2d_array' + | 'texture_3d' + | 'texture_cube' + | 'texture_cube_array' + * } WGSLSampledTextureType + * + * @typedef { + 'texture_multisampled_2d' + | 'texture_depth_multisampled_2d' + * } WGSLMultisampledTextureType + * + * @typedef { + 'texture_storage_1d' + | 'texture_storage_2d' + | 'texture_storage_2d_array' + | 'texture_storage_3d' + * } WGSLStorageTextureType + * + * @typedef { + 'texture_depth_2d' + | 'texture_depth_2d_array' + | 'texture_depth_cube' + | 'texture_depth_cube_array' + * } WGSLDepthTextureType + * + * @typedef { + WGSLSampledTextureType + | WGSLMultisampledTextureType + | WGSLStorageTextureType + | WGSLDepthTextureType + * } WGSLTextureType + */ + +/** + * @typedef { + 'sampler' + | 'sampler_comparison' + * } WGSLSamplerType + */ + +/** + * @param {string} typeName + * @returns {[WGSLTextureType, WGSLSampledType]} + */ +export const parseTextureType = (typeName) => { + const chevronIndex = typeName.indexOf('<') + const type = typeName.slice(0, chevronIndex) + const sampledType = typeName.slice(chevronIndex + 1, -1) + return [ + /** @type {WGSLTextureType} */ (type), + /** @type {WGSLSampledType} */ (sampledType) + ] +} + +/** + * @param {WGSLAccess} access + * @returns {GPUBufferBindingType} + */ +export const accessToBufferType = access => { + switch (access) { + case 'read': return BufferBindingType.ReadOnlyStorage + case 'write': + case 'read_write': + default: + return BufferBindingType.Storage + } +} + +/** + * @param {WGSLAccess} access + * @returns {GPUStorageTextureAccess} + */ +export const accessToStorageTextureAccess = access => { + switch (access) { + case 'read': return StorageTextureAccess.ReadOnly + case 'write': return StorageTextureAccess.WriteOnly + case 'read_write': return StorageTextureAccess.ReadWrite + } +} + +export const FormatToFilterType = Object.freeze({ + r8unorm: 'float', + r8snorm: 'float', + r8uint: 'uint', + r8sint: 'sint', + r16uint: 'uint', + r16sint: 'sint', + r16float: 'float', + rg8unorm: 'float', + rg8snorm: 'float', + rg8uint: 'uint', + rg8sint: 'sint', + r32uint: 'uint', + r32sint: 'sint', + r32float: 'unfilterable-float', + rg16uint: 'uint', + rg16sint: 'sint', + rg16float: 'float', + rgba8unorm: 'float', + 'rgba8unorm-srgb': 'float', + rgba8snorm: 'float', + rgba8uint: 'uint', + rgba8sint: 'sint', + bgra8unorm: 'float', + 'bgra8unorm-srgb': 'float', + rgb10a2unorm: 'float', + rg11b10ufloat: 'float', + rgb9e5ufloat: 'float', + rg32uint: 'uint', + rg32sint: 'sint', + rg32float: 'unfilterable-float', + rgba16uint: 'uint', + rgba16sint: 'sint', + rgba16float: 'float', + rgba32uint: 'uint', + rgba32sint: 'sint', + rgba32float: 'unfilterable-float', + + depth16unorm: 'depth', + depth24plus: 'depth', + 'depth24plus-stencil8': 'depth', + depth32float: 'unfilterable-float', + 'depth32float-stencil8': 'unfilterable-float', + stencil8: 'uint' +}) + +/** @param {string} format */ +export const formatToFilterType = format => ( + FormatToFilterType[format] || 'float' +) + +export const FormatToStride = Object.freeze({ + r8unorm: 1, + r8snorm: 1, + r8uint: 1, + r8sint: 1, + r16uint: 2, + r16sint: 2, + r16float: 2, + rg8unorm: 2, + rg8snorm: 2, + rg8uint: 2, + rg8sint: 2, + r32uint: 4, + r32sint: 4, + r32float: 4, + rg16uint: 4, + rg16sint: 4, + rg16float: 4, + rgba8unorm: 4, + 'rgba8unorm-srgb': 4, + rgba8snorm: 4, + rgba8uint: 4, + rgba8sint: 4, + bgra8unorm: 4, + 'bgra8unorm-srgb': 4, + rgb10a2unorm: 4, + rg11b10ufloat: 4, + rgb9e5ufloat: 4, + rg32uint: 4, + rg32sint: 4, + rg32float: 4, + rgba16uint: 4, + rgba16sint: 4, + rgba16float: 4, + rgba32uint: 4, + rgba32sint: 4, + rgba32float: 4, + + depth16unorm: 1, + depth24plus: 1, + 'depth24plus-stencil8': 1, + depth32float: 1, + 'depth32float-stencil8': 1, + stencil8: 1 +}) + +/** @param {string} typeName */ +export const typeToStride = typeName => ( + FormatToStride[typeName] || 1 +) + + +/** @param {WGSLTextureType} typeName */ +export const typeToViewDimension = typeName => { + switch (typeName) { + case 'texture_1d': + case 'texture_storage_1d': + return '1d' + + case 'texture_2d': + case 'texture_storage_2d': + case 'texture_multisampled_2d': + case 'texture_depth_2d': + case 'texture_depth_multisampled_2d': + return '2d' + + case 'texture_2d_array': + case 'texture_storage_2d_array': + case 'texture_depth_2d_array': + return '2d-array' + + case 'texture_3d': + case 'texture_storage_3d': + return '3d' + + case 'texture_cube': + case 'texture_depth_cube': + return 'cube' + + case 'texture_cube_array': + case 'texture_depth_cube_array': + return 'cube-array' + + default: + return '2d' + } +} + +/** + * @param {GPUTextureViewDimension} dimension + * @returns {GPUTextureDimension} + */ +export const textureToImageDimension = dimension => { + switch (dimension) { + case '1d': + return '1d' + + case '2d': + case '2d-array': + case 'cube-array': + return '2d' + + case '3d': + return '3d' + + default: + return '2d' + } +} + +/** + * @param {string} format + * @returns {GPUTextureFormat} + */ +export const wgslToWgpuFormat = (format) => { + switch (format) { + case 'f32': + return TextureFormat.Bgra8unorm + case 'vec4': + case 'vec4f': + return TextureFormat.Bgra8unorm + case 'vec4': + case 'vec4u': + return TextureFormat.Rgba32uint + case 'vec4': + case 'vec4i': + return TextureFormat.Rgba32sint + case 'vec2': + case 'vec2f': + return TextureFormat.Rg32float + case 'u32': + case 'u': + return TextureFormat.R32uint + case 'i32': + case 'i': + return TextureFormat.R32sint + case 'f16': + return TextureFormat.Rgba16float + case 'vec4': + return TextureFormat.Rgba16float + case 'vec2': + return TextureFormat.Rg16float + case 'u16': + return TextureFormat.R16uint + case 'i16': + return TextureFormat.R16sint + case 'vec4unorm': + return TextureFormat.Rgba8unorm + case 'vec4snorm': + return TextureFormat.Rgba8snorm + case 'vec4': + return TextureFormat.Rgba8uint + case 'vec4': + return TextureFormat.Rgba8sint + default: + return TextureFormat.Rgba8unorm + } +} +