prove that shader reflection works

This commit is contained in:
Rowan 2025-04-20 23:05:23 -05:00
parent 37741ed9aa
commit cad8a1555e
11 changed files with 8283 additions and 148 deletions

7532
dist/index.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -24,7 +24,7 @@
<body>
<canvas id="webgpu-canvas"></canvas>
<script type="module" src="./index.js"></script>
<script type="module" src="./dist/index.js"></script>
</body>
</html>

View file

@ -1,6 +1,5 @@
import { GraphicsDevice } from './src/core/graphics-device.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,7 +26,7 @@ async function main() {
return
}
const shaderCode = `
const shaderSource = `
@group(0) @binding(0)
var<uniform> transform : mat4x4<f32>;
@ -43,9 +42,8 @@ async function main() {
`;
const shaderModule = graphicsDevice.createShaderModule(
shaderCode,
shaderSource,
'SquareShader',
ShaderType.Vertex | ShaderType.Fragment
)
const vertices = new Float32Array([
@ -96,17 +94,8 @@ async function main() {
matrixData
)
const bindings = [{
binding: 0,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
buffer: /** @type {GPUBufferBindingLayout} */ ({ type: 'uniform' })
}]
const bindGroupLayout = graphicsDevice.createBindGroupLayout(bindings, 'UniformLayout')
const material = graphicsDevice.createMaterial(
{ vertex: shaderModule, fragment: shaderModule },
[bindGroupLayout]
{ vertex: shaderModule },
)
@ -117,24 +106,29 @@ async function main() {
]
}
const pipelineDescriptor = material.getRenderPipelineDescriptor(
[vertexBufferLayout],
'SquarePipeline'
)
pipelineDescriptor.fragment.targets = [
{ format: graphicsDevice.swapChain.format }
]
const pipelineDescriptor = material.getRenderPipelineDescriptor({
label: 'SquarePipeline',
vertex: {
buffers: [vertexBufferLayout]
},
fragment: {
targets: [{ format: graphicsDevice.swapChain.format }]
}
})
const pipeline = graphicsDevice.createRenderPipeline(pipelineDescriptor)
/** @type {Array<import('./src/core/graphics-device.js').BindGroupEntry>} */
const entries = [{
const uniformBindings = [{
binding: 0,
resource: uniformBuffer
}]
const uniformBindGroup = graphicsDevice.createBindGroup(bindGroupLayout, entries, 'Uniforms')
const uniformBindGroup = graphicsDevice.createBindGroup(
material.bindGroupLayouts[0],
uniformBindings,
'Uniforms'
)
async function frame() {
if (!graphicsDevice.isInitialized) {

View file

@ -5,7 +5,7 @@
"target": "es6",
"lib": ["es2022", "dom"],
"types": ["@webgpu/types"],
"checkJs": true"
"checkJs": "true"
},
"exclude": [
"node_modules"

469
package-lock.json generated
View file

@ -12,7 +12,433 @@
"wgsl_reflect": "^1.2.0"
},
"devDependencies": {
"@webgpu/types": "^0.1.60"
"@webgpu/types": "^0.1.60",
"esbuild": "^0.25.2"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz",
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz",
"integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz",
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz",
"integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz",
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz",
"integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz",
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz",
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz",
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz",
"integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz",
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz",
"integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz",
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz",
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz",
"integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz",
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz",
"integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz",
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz",
"integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz",
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz",
"integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz",
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz",
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz",
"integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@webgpu/types": {
@ -22,6 +448,47 @@
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/esbuild": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.2",
"@esbuild/android-arm": "0.25.2",
"@esbuild/android-arm64": "0.25.2",
"@esbuild/android-x64": "0.25.2",
"@esbuild/darwin-arm64": "0.25.2",
"@esbuild/darwin-x64": "0.25.2",
"@esbuild/freebsd-arm64": "0.25.2",
"@esbuild/freebsd-x64": "0.25.2",
"@esbuild/linux-arm": "0.25.2",
"@esbuild/linux-arm64": "0.25.2",
"@esbuild/linux-ia32": "0.25.2",
"@esbuild/linux-loong64": "0.25.2",
"@esbuild/linux-mips64el": "0.25.2",
"@esbuild/linux-ppc64": "0.25.2",
"@esbuild/linux-riscv64": "0.25.2",
"@esbuild/linux-s390x": "0.25.2",
"@esbuild/linux-x64": "0.25.2",
"@esbuild/netbsd-arm64": "0.25.2",
"@esbuild/netbsd-x64": "0.25.2",
"@esbuild/openbsd-arm64": "0.25.2",
"@esbuild/openbsd-x64": "0.25.2",
"@esbuild/sunos-x64": "0.25.2",
"@esbuild/win32-arm64": "0.25.2",
"@esbuild/win32-ia32": "0.25.2",
"@esbuild/win32-x64": "0.25.2"
}
},
"node_modules/wgsl_reflect": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/wgsl_reflect/-/wgsl_reflect-1.2.0.tgz",

View file

@ -4,6 +4,7 @@
"type": "module",
"main": "index.js",
"scripts": {
"build": "esbuild index.js --bundle --outfile=./dist/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
@ -11,7 +12,8 @@
"license": "ISC",
"description": "",
"devDependencies": {
"@webgpu/types": "^0.1.60"
"@webgpu/types": "^0.1.60",
"esbuild": "^0.25.2"
},
"dependencies": {
"wgsl_reflect": "^1.2.0"

1
server
View file

@ -1,4 +1,5 @@
#!/usr/bin/env sh
npm run build
python -m http.server

View file

@ -105,7 +105,7 @@ class GraphicsDeviceBuilder {
class DeviceHandler {
/** @type {GPURequestAdapterOptions} */
_adapter_options
_adapterOptions
/** @type {GPUAdapter} */
_adapter
@ -115,7 +115,7 @@ class DeviceHandler {
}
/** @type {GPUDeviceDescriptor} */
_device_descriptor
_deviceDescriptor
/** @type {GPUDevice} */
_device
@ -129,18 +129,18 @@ class DeviceHandler {
* @param {GPUDeviceDescriptor} deviceDescriptor
*/
constructor(adapterOptions, deviceDescriptor) {
this._adapter_options = adapterOptions
this._device_descriptor = deviceDescriptor
this._adapterOptions = adapterOptions
this._deviceDescriptor = deviceDescriptor
}
async create() {
this._adapter = await navigator.gpu.requestAdapter(this._adapter_options)
this._adapter = await navigator.gpu.requestAdapter(this._adapterOptions)
if (!this._adapter) {
throw WebGPUError.adapterUnavailable()
}
this._device = await this._adapter.requestDevice(this._device_descriptor)
this._device = await this._adapter.requestDevice(this._deviceDescriptor)
if (!this._device) {
throw WebGPUError.deviceUnavailable()
@ -282,15 +282,13 @@ 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, shaderType, entryPoint) {
createShaderModule(code, label) {
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
try {
return ShaderModule.create(this.device, { code, label, shaderType, entryPoint })
return ShaderModule.create(this.device, { code, label })
} catch (err) {
throw WebGPUObjectError.from(err, ShaderModule)
}
@ -314,13 +312,12 @@ export class GraphicsDevice extends EventEmitter {
/**
* @param {import('../resources/material.js').ShaderPairDescriptor} shaders
* @param {BindGroupLayout[]} bindGroupLayouts
*/
createMaterial(shaders, bindGroupLayouts) {
createMaterial(shaders) {
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
try {
return new Material(this.device, shaders, bindGroupLayouts)
return new Material(this.device, shaders)
} catch (err) {
throw WebGPUObjectError.from(err, Material)
}

View file

@ -1,29 +1,20 @@
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 { ShaderPair, ShaderModule } from './shader-module.js'
import { MaterialError, WebGPUObjectError } from '../utils/errors.js'
/** @import {Either} from '../utils.js' */
/** @import { FragmentStateDescriptor, VertexStateDescriptor } from './shader-module.js' */
/**
* @typedef Shader2
* @typedef ShaderPairDescriptor
* @property {ShaderModule} vertex
* @property {ShaderModule} fragment
*
* @typedef {Omit<Shader2, 'fragment'>} VertexOnly
* @typedef {Omit<Shader2, 'vertex'>} FragmentOnly
*
* @typedef Unified1
* @property {ShaderModule} shaderModule
* @property {ShaderModule} [fragment]
*/
/**
* @typedef {
(Shader2 | Either<VertexOnly, FragmentOnly>) |
Unified1
* } ShaderPairDescriptor
ShaderPairDescriptor &
{ bindGroupLayouts?: BindGroupLayout[] }
* } MaterialDescriptor
*/
export class Material {
@ -42,18 +33,27 @@ export class Material {
/**
* @param {GPUDevice} device
* @param {ShaderPairDescriptor} shaders
* @param {BindGroupLayout[]} bindGroupLayouts
* @param {MaterialDescriptor} descriptor
*/
constructor(device, shaders, bindGroupLayouts) {
constructor(device, descriptor) {
this._device = device
this._shaders = Material._parseShaders(shaders)
this._bindGroupLayouts = bindGroupLayouts
this._shaders = Material._reflectShaders(descriptor)
const bgl = descriptor.bindGroupLayouts
if (bindGroupLayouts && bindGroupLayouts.length > 0) {
this._pipelineLayout = device.createPipelineLayout({
bindGroupLayouts: bindGroupLayouts.map(bgl => bgl.handle)
})
if (bgl && bgl.length > 0) {
this._bindGroupLayouts = bgl
} else {
this._bindGroupLayouts = this._reflectBindGroupLayouts(device, this._shaders)
}
if (this._bindGroupLayouts && this.bindGroupLayouts.length > 0) {
try {
this._pipelineLayout = device.createPipelineLayout({
bindGroupLayouts: this._bindGroupLayouts.map(bgl => bgl.handle)
})
} catch (err) {
throw WebGPUObjectError.from(err, Material)
}
}
}
/**
@ -61,26 +61,50 @@ export class Material {
* shader types.
*
* @param {ShaderPairDescriptor} shaders
* @returns {UnifiedShader | ShaderPair}
* @returns {ShaderPair}
*/
static _parseShaders(shaders) {
static _reflectShaders(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')
}
if ('vertex' in shaders) {
return ShaderPair.fromPair(shaders)
}
}
/**
* @param {GPUDevice} device
* @param {ShaderPair} shaders
* @returns {BindGroupLayout[]}
*/
_reflectBindGroupLayouts(device, shaders) {
const layouts = shaders.createBindGroupLayoutEntries()
return layouts.map(entries => BindGroupLayout.create(device, { entries }))
}
/**
* @typedef MaterialPipelineDescriptor
* @property {string} [label]
* @property {GPUPipelineLayout} [pipelineLayout]
* @property {VertexStateDescriptor} vertex
* @property {FragmentStateDescriptor} [fragment]
* @property {GPUPrimitiveState} [primitive]
*/
/**
* @param {MaterialPipelineDescriptor} descriptor
* @returns {GPURenderPipelineDescriptor}
*/
getRenderPipelineDescriptor(descriptor) {
const { fragment, vertex } = this.shaders.getRenderPipelineStates(descriptor)
return {
label: descriptor.label,
layout: descriptor.pipelineLayout || this._pipelineLayout,
fragment,
vertex,
primitive: descriptor.primitive || { topology: 'triangle-list' }
}
}
}

View file

@ -12,7 +12,6 @@ import {
wgslToWgpuFormat
} from '../utils/wgsl-to-wgpu.js'
import { BufferBindingType } from '../enum.js'
import { BitFlags } from '../utils/bitflags.js'
/** @import { WGSLAccess, WGSLSamplerType } from '../utils/wgsl-to-wgpu.js' */
@ -33,19 +32,26 @@ export class ShaderModule {
/**
* @param {GPUDevice} device
* @param {string} code
* @param {string} [label]
* @param {GPUShaderModuleDescriptor} descriptor
*/
constructor(device, code, label) {
this._code = code
constructor(device, descriptor) {
this._code = descriptor.code
try {
this._handle = device.createShaderModule({ code, label })
this._handle = device.createShaderModule(descriptor)
} catch (err) {
throw WebGPUObjectError.from(err, ShaderModule)
}
}
/**
* @param {GPUDevice} device
* @param {GPUShaderModuleDescriptor} descriptor
*/
static create(device, descriptor) {
return new ShaderModule(device, descriptor)
}
reflect() {
if (this._reflection == null) {
this._reflection = new WgslReflect(this._code)
@ -57,21 +63,54 @@ export class ShaderModule {
}
}
/**
* @typedef FragmentStateDescriptor
* @property {Record<string, GPUPipelineConstantValue>} [constants={}]
* @property {GPUColorTargetState[]} [targets=[]]
*
* @typedef VertexStateDescriptor
* @property {Record<string, GPUPipelineConstantValue>} [constants={}]
* @property {GPUVertexBufferLayout[]} [buffers=[]]
*
* @typedef ShaderPairStateDescriptor
* @property {FragmentStateDescriptor} [fragment]
* @property {VertexStateDescriptor} vertex
*
*/
export class ReflectedShader {
_shader
_module
get module() {
return this._module
}
/**
* @param {ShaderModule} shader
*/
constructor(shader) {
this._shader = shader
this._module = shader
}
/**
* @param {string} stageName
* @returns {string | undefined}
*/
getEntrypoint(stageName) {
const entry = this.module.reflect().entry
// TODO: determine how to correctly handle
// multiple entrypoints to the same stage
return entry[stageName].length === 1 ?
entry[stageName][0].name : undefined
}
/**
* @returns {GPUShaderStageFlags}
*/
getShaderStages() {
const entry = this._shader.reflect().entry
const entry = this._module.reflect().entry
let stages = 0
stages |= entry.vertex.length > 0 ?
@ -86,12 +125,20 @@ export class ReflectedShader {
return stages
}
/**
* @param {GPUShaderStageFlags} stages
*/
hasStage(stages) {
return this.getShaderStages()
& stages
}
/**
* @param {GPUShaderStageFlags} stages
* @param {GroupBindingMap} [out=new GroupBindingMap()]
*/
getBindingsForStage(stages, out = new GroupBindingMap()) {
const groups = this._shader.reflect().getBindGroups()
const groups = this._module.reflect().getBindGroups()
groups.forEach((bindings, groupIndex) => {
if (!out.has(groupIndex)) {
@ -116,7 +163,7 @@ export class ReflectedShader {
* @param {Map<any, any>} map
* @returns {number[]}
*/
_sortKeyIndices(map) {
static _sortKeyIndices(map) {
return Array.from(map.keys()).sort((a, b) => a - b)
}
@ -124,7 +171,7 @@ export class ReflectedShader {
* @param {VariableInfo} _variableInfo
* @returns {GPUBufferBindingLayout}
*/
_parseUniform(_variableInfo) {
static _parseUniform(_variableInfo) {
return {
type: BufferBindingType.Uniform,
// TODO: infer these two properties
@ -137,7 +184,7 @@ export class ReflectedShader {
* @param {VariableInfo} variableInfo
* @returns {GPUBufferBindingLayout}
*/
_parseStorage(variableInfo) {
static _parseStorage(variableInfo) {
return {
type: accessToBufferType(
/** @type {WGSLAccess} */
@ -153,7 +200,7 @@ export class ReflectedShader {
* @param {VariableInfo} variableInfo
* @returns {GPUTextureBindingLayout}
*/
_parseTexture(variableInfo) {
static _parseTexture(variableInfo) {
const [type, sampledType] = parseTextureType(
variableInfo.type.name
)
@ -169,7 +216,7 @@ export class ReflectedShader {
* @param {VariableInfo} variableInfo
* @returns {GPUSamplerBindingLayout}
*/
_parseSampler(variableInfo) {
static _parseSampler(variableInfo) {
return {
type: typeToSamplerBindingType(
/** @type {WGSLSamplerType} */(variableInfo.type.name)
@ -181,7 +228,7 @@ export class ReflectedShader {
* @param {VariableInfo} variableInfo
* @returns {GPUStorageTextureBindingLayout}
*/
_parseStorageTexture(variableInfo) {
static _parseStorageTexture(variableInfo) {
const [type] = parseTextureType(variableInfo.type.name)
return {
@ -197,7 +244,7 @@ export class ReflectedShader {
* @param {VariableStageInfo} variableStageInfo
* @returns {GPUBindGroupLayoutEntry}
*/
_variableInfoToEntry(variableStageInfo) {
static _variableInfoToEntry(variableStageInfo) {
const { stages: visibility, variableInfo } = variableStageInfo
switch (variableInfo.resourceType) {
@ -240,7 +287,7 @@ export class ReflectedShader {
/**
* @param {GroupBindingMap} groupBindings
*/
createBindGroupLayoutEntries(groupBindings) {
static createBindGroupLayoutEntries(groupBindings) {
const sortedGroupIndices = this._sortKeyIndices(groupBindings)
return sortedGroupIndices.map(groupIndex => {
@ -253,35 +300,10 @@ export class ReflectedShader {
}
}
export class UnifiedShader {
_shader
/**
* @param {ReflectedShader} shader
*/
constructor(shader) {
this._shader = shader
}
createBindGroupLayoutEntries() {
const stages = this._shader.getShaderStages()
const unifiedShader = GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT
if (!BitFlags.has(stages, unifiedShader)) {
throw new Error('cant do it')
}
return this._shader.createBindGroupLayoutEntries(
this._shader.getBindingsForStage(
unifiedShader
)
)
}
}
export class ReflectedShaderPair {
export class ShaderPair {
/** @type {ReflectedShader} */
_vertex
/** @type {ReflectedShader} */
_fragment
/**
@ -289,25 +311,52 @@ export class ReflectedShaderPair {
* @param {ReflectedShader} [fragment]
*/
constructor(vertex, fragment) {
if (!vertex) {
throw new Error('Missing vertex shader')
}
if (!vertex.hasStage(GPUShaderStage.VERTEX)) {
throw new Error('Vertex shader does not have a vertex entrypoint.')
}
this._vertex = vertex
this._fragment = fragment
if (fragment) {
if (fragment.hasStage(GPUShaderStage.FRAGMENT)) {
this._fragment = fragment
} else {
throw new Error('Fragment shader does not have a fragment entrypoint.')
}
} else if (this._vertex.hasStage(GPUShaderStage.FRAGMENT)) {
// this is a unified shader, use the vertex as the fragment
this._fragment = vertex
} else {
throw new Error('Missing fragment shader.')
}
}
/** @param {ShaderModule} shader */
static fromUnifiedShader(shader) {
return new ShaderPair(
new ReflectedShader(shader)
)
}
/**
* @param {{
vertex: ShaderModule,
fragment?: ShaderModule
* }} value
*/
static fromPair(value) {
const vert = new ReflectedShader(value.vertex)
const frag = value.fragment && new ReflectedShader(value.fragment)
return new ShaderPair(vert, frag)
}
_createGroupBindings() {
const groupBindings = new GroupBindingMap()
if (
!BitFlags.has(
this._vertex.getShaderStages(),
GPUShaderStage.VERTEX)
&& !BitFlags.has(
this._fragment.getShaderStages(),
GPUShaderStage.FRAGMENT
)
) {
throw new Error('nope')
}
this._vertex.getBindingsForStage(
GPUShaderStage.VERTEX,
groupBindings
@ -322,11 +371,46 @@ export class ReflectedShaderPair {
}
createBindGroupLayoutEntries() {
// FIXME: move this call and all the other calls
// somewhere else
return this._shader.createBindGroupLayoutEntries(
return ReflectedShader.createBindGroupLayoutEntries(
this._createGroupBindings()
)
}
/**
* @param {FragmentStateDescriptor} descriptor
* @returns {GPUFragmentState}
*/
_getFragmentState(descriptor) {
return {
module: this._fragment.module.handle,
entryPoint: this._fragment.getEntrypoint('fragment'),
constants: descriptor.constants || {},
targets: descriptor.targets || []
}
}
/**
* @param {VertexStateDescriptor} descriptor
* @returns {GPUVertexState}
*/
_getVertexState(descriptor) {
return {
module: this._vertex.module.handle,
entryPoint: this._vertex.getEntrypoint('vertex'),
constants: descriptor.constants || {},
buffers: descriptor.buffers || []
}
}
/**
* @param {ShaderPairStateDescriptor} descriptor
* @returns {Pick<GPURenderPipelineDescriptor, 'vertex' | 'fragment'>}
*/
getRenderPipelineStates(descriptor) {
return {
fragment: this._getFragmentState(descriptor.fragment),
vertex: this._getVertexState(descriptor.vertex),
}
}
}

View file

@ -1,12 +1,46 @@
import { FlagEnum } from '../utils.js'
export const ShaderType = FlagEnum(
'auto',
export const ShaderStage = FlagEnum(
'vertex',
'fragment',
'compute'
)
/**
* @param {GPUShaderStageFlags} stages
* @returns {string[]}
*/
export const stageFlagToName = stages => {
const names = []
if (stages & GPUShaderStage.FRAGMENT) {
names.push('fragment')
}
if (stages & GPUShaderStage.VERTEX) {
names.push('vertex')
}
if (stages & GPUShaderStage.COMPUTE) {
names.push('compute')
}
return names
}
/**
* @param {('fragment' | 'vertex' | 'compute')[]} names
*/
export const nameToStageFlag = names => {
return names.reduce((flags, name) => {
switch (name.toLowerCase()) {
case 'fragment': return flags | GPUShaderStage.FRAGMENT
case 'vertex': return flags | GPUShaderStage.VERTEX
case 'compute': return flags | GPUShaderStage.COMPUTE
}
}, 0)
}
export const ResourceType = Object.freeze({
Sampler: 0,
TextureView: 1,