refactoring shaders for reflection and automating binding

This commit is contained in:
Rowan 2025-04-20 02:00:18 -05:00
parent 2c5ac481a4
commit 42be3fcaa8
16 changed files with 1039 additions and 95 deletions

100
index.js
View file

@ -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<f32>) -> @builtin(position) vec4<f32> {
return vec4<f32>(in_pos, 1.0);
}
@group(0) @binding(0)
var<uniform> transform : mat4x4<f32>;
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(0.0, 0.5, 1.0, 1.0);
}
`
@vertex
fn vs_main(@location(0) in_pos : vec3<f32>) -> @builtin(position) vec4<f32> {
return transform * vec4<f32>(in_pos, 1.0);
}
const shaderModule = graphicsDevice.createShaderModule(shaderCode, 'TriangleShader')
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return vec4<f32>(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<import('./src/core/graphics-device.js').BindGroupEntry>} */
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()
}

View file

@ -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"

9
package-lock.json generated
View file

@ -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"
}
}
}

View file

@ -12,5 +12,8 @@
"description": "",
"devDependencies": {
"@webgpu/types": "^0.1.60"
},
"dependencies": {
"wgsl_reflect": "^1.2.0"
}
}

View file

@ -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, {

View file

@ -0,0 +1,3 @@
export class RenderGraph {
}

View file

@ -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

87
src/resources/material.js Normal file
View file

@ -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<Shader2, 'fragment'>} VertexOnly
* @typedef {Omit<Shader2, 'vertex'>} FragmentOnly
*
* @typedef Unified1
* @property {ShaderModule} shaderModule
*/
/**
* @typedef {
(Shader2 | Either<VertexOnly, FragmentOnly>) |
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')
}
}
}

View file

@ -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
}
}

View file

@ -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<any, any>} 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()
)
}
}

View file

@ -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()
}

View file

@ -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<T, U> | Only<U, T>} Either
*/
/**
* @template T
* @typedef {new (...args: any[]) => T} Newable<T>
@ -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> : T extends ReadonlyArray<string> ? Join<CapitalizeAll<T>, ''> : 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<Tail, V, [...Acc, Head]>
: never
* } IndexOf
*/
/**
* @template {string} T
* @param {T} s
@ -125,6 +158,25 @@ export const Enum = (...values) => /** @type {Readonly<{ [K in T as PascalCase<K
}, {})
))
/**
* @template T
* @typedef {T extends `${infer N extends number}` ? N : never } ParseInt
*/
/**
* @template {string} const T
* @template {T[]} const A
* @param {A} values
* @returns {Readonly<{ [K in keyof A as A[K] extends T ? PascalCase<A[K]> : never]: PowersOfTwo[ParseInt<K>] }>}
*/
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<T, K>}
*/
export const pick = (obj, ...keys) => /** @type {Pick<T, K>} */(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<T, K>}
*/
export const omit = (obj, ...keys) => /** @type {Omit<T, K>} */(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)
}
}

28
src/utils/bindings.js Normal file
View file

@ -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<number, VariableStageInfo>
*/
export class BindingMap extends Map { }
/**
* @extends Map<number, BindingMap>
*/
export class GroupBindingMap extends Map { }

View file

@ -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`)
}
}

View file

@ -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
})

301
src/utils/wgsl-to-wgpu.js Normal file
View file

@ -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<f32>':
case 'vec4f':
return TextureFormat.Bgra8unorm
case 'vec4<u32>':
case 'vec4u':
return TextureFormat.Rgba32uint
case 'vec4<i32>':
case 'vec4i':
return TextureFormat.Rgba32sint
case 'vec2<f32>':
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<f16>':
return TextureFormat.Rgba16float
case 'vec2<f16>':
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<u8>':
return TextureFormat.Rgba8uint
case 'vec4<i8>':
return TextureFormat.Rgba8sint
default:
return TextureFormat.Rgba8unorm
}
}