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 { 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() { async function main() {
const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('webgpu-canvas')) const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('webgpu-canvas'))
@ -27,25 +28,48 @@ async function main() {
} }
const shaderCode = ` const shaderCode = `
@vertex @group(0) @binding(0)
fn vs_main(@location(0) in_pos : vec3<f32>) -> @builtin(position) vec4<f32> { var<uniform> transform : mat4x4<f32>;
return vec4<f32>(in_pos, 1.0);
}
@fragment @vertex
fn fs_main() -> @location(0) vec4<f32> { fn vs_main(@location(0) in_pos : vec3<f32>) -> @builtin(position) vec4<f32> {
return vec4<f32>(0.0, 0.5, 1.0, 1.0); 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([ 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,
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( const vertexBuffer = graphicsDevice.createBuffer(
{ {
label: 'TriangleVertexBuffer', label: 'TriangleVertexBuffer',
@ -80,35 +104,29 @@ async function main() {
const bindGroupLayout = graphicsDevice.createBindGroupLayout(bindings, 'UniformLayout') 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({ const vertexBufferLayout = {
label: 'TrianglePipeline', arrayStride: 3 * 4,
layout: pipelineLayout, attributes: [
vertex: { { shaderLocation: 0, offset: 0, format: VertexFormat.Float32x3 }
module: shaderModule.handle, ]
entryPoint: 'vs_main', }
buffers: [
{ const pipelineDescriptor = material.getRenderPipelineDescriptor(
arrayStride: 3 * 4, [vertexBufferLayout],
attributes: [ 'SquarePipeline'
{ shaderLocation: 0, offset: 0, format: 'float32x3' } )
]
} pipelineDescriptor.fragment.targets = [
] { format: graphicsDevice.swapChain.format }
}, ]
fragment: {
module: shaderModule.handle, const pipeline = graphicsDevice.createRenderPipeline(pipelineDescriptor)
entryPoint: 'fs_main',
targets: [
{ format: graphicsDevice.swapChain.format }
]
},
primitive: {
topology: 'triangle-list',
},
})
/** @type {Array<import('./src/core/graphics-device.js').BindGroupEntry>} */ /** @type {Array<import('./src/core/graphics-device.js').BindGroupEntry>} */
const entries = [{ const entries = [{
@ -123,6 +141,7 @@ async function main() {
return return
} }
graphicsDevice.queue.writeBuffer(uniformBuffer.handle, 0, matrixData)
const commandRecorder = graphicsDevice.createCommandRecorder('FrameCommands') const commandRecorder = graphicsDevice.createCommandRecorder('FrameCommands')
const passEncoder = commandRecorder.beginRenderPass() const passEncoder = commandRecorder.beginRenderPass()
@ -130,8 +149,9 @@ async function main() {
if (passEncoder) { if (passEncoder) {
passEncoder.setPipeline(pipeline.handle) passEncoder.setPipeline(pipeline.handle)
passEncoder.setVertexBuffer(0, vertexBuffer.handle) passEncoder.setVertexBuffer(0, vertexBuffer.handle)
passEncoder.setIndexBuffer(indexBuffer.handle, 'uint16')
passEncoder.setBindGroup(0, uniformBindGroup.handle) passEncoder.setBindGroup(0, uniformBindGroup.handle)
passEncoder.draw(3) passEncoder.drawIndexed(indices.length)
commandRecorder.endRenderPass() commandRecorder.endRenderPass()
} }

View file

@ -1,13 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "es2020", "module": "es2022",
"moduleResolution": "node",
"target": "es6", "target": "es6",
"lib": ["es2022", "dom"], "lib": ["es2022", "dom"],
"types": ["@webgpu/types"], "types": ["@webgpu/types"],
"checkJs": true, "checkJs": true"
"paths": {
"/*": ["./*"]
}
}, },
"exclude": [ "exclude": [
"node_modules" "node_modules"

9
package-lock.json generated
View file

@ -8,6 +8,9 @@
"name": "wgpu", "name": "wgpu",
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": {
"wgsl_reflect": "^1.2.0"
},
"devDependencies": { "devDependencies": {
"@webgpu/types": "^0.1.60" "@webgpu/types": "^0.1.60"
} }
@ -18,6 +21,12 @@
"integrity": "sha512-8B/tdfRFKdrnejqmvq95ogp8tf52oZ51p3f4QD5m5Paey/qlX4Rhhy5Y8tgFMi7Ms70HzcMMw3EQjH/jdhTwlA==", "integrity": "sha512-8B/tdfRFKdrnejqmvq95ogp8tf52oZ51p3f4QD5m5Paey/qlX4Rhhy5Y8tgFMi7Ms70HzcMMw3EQjH/jdhTwlA==",
"dev": true, "dev": true,
"license": "BSD-3-Clause" "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": "", "description": "",
"devDependencies": { "devDependencies": {
"@webgpu/types": "^0.1.60" "@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 { Sampler } from '../resources/sampler.js'
import { ResourceType } from '../utils/internal-enums.js'
import { Material } from '../resources/material.js'
class GraphicsDeviceBuilder { class GraphicsDeviceBuilder {
_canvas _canvas
@ -280,13 +282,15 @@ export class GraphicsDevice extends EventEmitter {
* Creates a shader module from WGSL code. * Creates a shader module from WGSL code.
* @param {string} code * @param {string} code
* @param {string} [label] * @param {string} [label]
* @param {number} [shaderType]
* @param {string} [entryPoint]
* @returns {ShaderModule} * @returns {ShaderModule}
*/ */
createShaderModule(code, label) { createShaderModule(code, label, shaderType, entryPoint) {
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
try { try {
return ShaderModule.create(this.device, { code, label }) return ShaderModule.create(this.device, { code, label, shaderType, entryPoint })
} catch (err) { } catch (err) {
throw WebGPUObjectError.from(err, ShaderModule) throw WebGPUObjectError.from(err, ShaderModule)
} }
@ -301,13 +305,27 @@ export class GraphicsDevice extends EventEmitter {
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
try { try {
const gpuPipeline = this.device.createRenderPipeline(descriptor) const pipeline = this.device.createRenderPipeline(descriptor)
return new RenderPipeline(gpuPipeline, descriptor.label) return new RenderPipeline(pipeline, descriptor.label)
} catch (err) { } catch (err) {
throw WebGPUObjectError.from(err, RenderPipeline) 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. * Creates a CommandRecorder to begin recording GPU commands.
* @param {string} [label] * @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 {BindGroupLayout} layout
* @param {BindGroupEntry[]} bindings * @param {BindGroupEntry[]} bindings
@ -343,7 +396,7 @@ export class GraphicsDevice extends EventEmitter {
const entries = bindings.map(def => ({ const entries = bindings.map(def => ({
binding: def.binding, binding: def.binding,
resource: def.resource.asBindingResource(def) resource: this._getBindingResource(def)
})) }))
return BindGroup.create(this.device, { 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 { BufferError, WebGPUError } from '../utils/errors.js'
import { ResourceType } from '../utils/internal-enums.js'
/** @import { TypedArray, TypedArrayConstructor } from '../utils.js' */ /** @import { TypedArray, TypedArrayConstructor } from '../utils.js' */
/** @import { BindGroupEntry, BindingResource, BufferBindingResource } from '../core/graphics-device.js' */ /** @import { BindGroupEntry, BindingResource, BufferBindingResource } from '../core/graphics-device.js' */
@ -24,6 +25,10 @@ export class Buffer {
return this._handle.usage return this._handle.usage
} }
get resourceType() {
return ResourceType.Buffer
}
/** /**
* @param {GPUDevice} device * @param {GPUDevice} device
* @param {GPUBuffer} texture * @param {GPUBuffer} texture
@ -168,24 +173,6 @@ export class Buffer {
return result 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() { destroy() {
this._handle?.destroy() this._handle?.destroy()
this._handle = null 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 { WebGPUObjectError } from '../utils/errors.js'
import { ResourceType } from '../utils/internal-enums.js'
/** @import { BindGroupEntry } from '../core/graphics-device.js' */ /** @import { BindGroupEntry } from '../core/graphics-device.js' */
@ -14,6 +15,10 @@ export class Sampler {
return this._handle.label return this._handle.label
} }
get resourceType() {
return ResourceType.Sampler
}
/** /**
* @param {GPUDevice} device * @param {GPUDevice} device
* @param {GPUSampler} sampler * @param {GPUSampler} sampler
@ -37,12 +42,5 @@ export class Sampler {
throw WebGPUObjectError.from(err, 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 { 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 { export class ShaderModule {
_handle _handle
_code
get label() { /** @type {WgslReflect | undefined} */
return this._handle.label _reflection
}
get handle() { get handle() {
return this._handle return this._handle
} }
/** get label() {
* @param {GPUShaderModule} module return this._handle.label
*/
constructor(module) {
this._handle = module
} }
/** /**
* @param {GPUDevice} device * @param {GPUDevice} device
* @param {GPUShaderModuleDescriptor} descriptor * @param {string} code
* @param {string} [label]
*/ */
static create(device, descriptor) { constructor(device, code, label) {
this._code = code
try { try {
return new ShaderModule( this._handle = device.createShaderModule({ code, label })
device.createShaderModule(descriptor)
)
} catch (err) { } catch (err) {
throw WebGPUObjectError.from(err, ShaderModule) 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 { WebGPUObjectError } from '../utils/errors.js'
import { ResourceType } from '../utils/internal-enums.js'
/** @import { BindGroupEntry } from '../core/graphics-device.js' */ /** @import { BindGroupEntry } from '../core/graphics-device.js' */
@ -45,6 +46,10 @@ export class Texture {
return this._handle.label return this._handle.label
} }
get resourceType() {
return ResourceType.TextureView
}
/** /**
* @param {GPUDevice} device * @param {GPUDevice} device
* @param {GPUTexture} texture * @param {GPUTexture} texture
@ -91,13 +96,6 @@ export class Texture {
} }
} }
/**
* @param {BindGroupEntry} entry
*/
asBindingResource(entry) {
return 'textureView' in entry ? entry.textureView : this.getView()
}
getView() { getView() {
return this.createDefaultView() 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 * @template T
* @typedef {new (...args: any[]) => T} Newable<T> * @typedef {new (...args: any[]) => T} Newable<T>
@ -35,6 +48,15 @@
} TypedArrayConstructor } 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 * @template {number} T
* @typedef {`${T}` extends `-${string}` | '0' | `${string}.${string}` ? never : T } PositiveInteger * @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 * @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 * @template {string} T
* @param {T} s * @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 */ /** @typedef {(...args: any) => void} Listener */
export class EventEmitter { 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`) 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
}
}