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> <body>
<canvas id="webgpu-canvas"></canvas> <canvas id="webgpu-canvas"></canvas>
<script type="module" src="./index.js"></script> <script type="module" src="./dist/index.js"></script>
</body> </body>
</html> </html>

View file

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

View file

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

469
package-lock.json generated
View file

@ -12,7 +12,433 @@
"wgsl_reflect": "^1.2.0" "wgsl_reflect": "^1.2.0"
}, },
"devDependencies": { "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": { "node_modules/@webgpu/types": {
@ -22,6 +448,47 @@
"dev": true, "dev": true,
"license": "BSD-3-Clause" "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": { "node_modules/wgsl_reflect": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/wgsl_reflect/-/wgsl_reflect-1.2.0.tgz", "resolved": "https://registry.npmjs.org/wgsl_reflect/-/wgsl_reflect-1.2.0.tgz",

View file

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

1
server
View file

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

View file

@ -105,7 +105,7 @@ class GraphicsDeviceBuilder {
class DeviceHandler { class DeviceHandler {
/** @type {GPURequestAdapterOptions} */ /** @type {GPURequestAdapterOptions} */
_adapter_options _adapterOptions
/** @type {GPUAdapter} */ /** @type {GPUAdapter} */
_adapter _adapter
@ -115,7 +115,7 @@ class DeviceHandler {
} }
/** @type {GPUDeviceDescriptor} */ /** @type {GPUDeviceDescriptor} */
_device_descriptor _deviceDescriptor
/** @type {GPUDevice} */ /** @type {GPUDevice} */
_device _device
@ -129,18 +129,18 @@ class DeviceHandler {
* @param {GPUDeviceDescriptor} deviceDescriptor * @param {GPUDeviceDescriptor} deviceDescriptor
*/ */
constructor(adapterOptions, deviceDescriptor) { constructor(adapterOptions, deviceDescriptor) {
this._adapter_options = adapterOptions this._adapterOptions = adapterOptions
this._device_descriptor = deviceDescriptor this._deviceDescriptor = deviceDescriptor
} }
async create() { async create() {
this._adapter = await navigator.gpu.requestAdapter(this._adapter_options) this._adapter = await navigator.gpu.requestAdapter(this._adapterOptions)
if (!this._adapter) { if (!this._adapter) {
throw WebGPUError.adapterUnavailable() throw WebGPUError.adapterUnavailable()
} }
this._device = await this._adapter.requestDevice(this._device_descriptor) this._device = await this._adapter.requestDevice(this._deviceDescriptor)
if (!this._device) { if (!this._device) {
throw WebGPUError.deviceUnavailable() throw WebGPUError.deviceUnavailable()
@ -282,15 +282,13 @@ 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, shaderType, entryPoint) { createShaderModule(code, label) {
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
try { try {
return ShaderModule.create(this.device, { code, label, shaderType, entryPoint }) return ShaderModule.create(this.device, { code, label })
} catch (err) { } catch (err) {
throw WebGPUObjectError.from(err, ShaderModule) throw WebGPUObjectError.from(err, ShaderModule)
} }
@ -314,13 +312,12 @@ export class GraphicsDevice extends EventEmitter {
/** /**
* @param {import('../resources/material.js').ShaderPairDescriptor} shaders * @param {import('../resources/material.js').ShaderPairDescriptor} shaders
* @param {BindGroupLayout[]} bindGroupLayouts
*/ */
createMaterial(shaders, bindGroupLayouts) { createMaterial(shaders) {
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
try { try {
return new Material(this.device, shaders, bindGroupLayouts) return new Material(this.device, shaders)
} catch (err) { } catch (err) {
throw WebGPUObjectError.from(err, Material) 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 { BindGroupLayout } from './bind-group-layout.js'
import { ShaderModule } from './shader-module.js' import { ShaderPair, ShaderModule } from './shader-module.js'
import { MaterialError } from '../utils/errors.js' import { MaterialError, WebGPUObjectError } 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 { FragmentStateDescriptor, VertexStateDescriptor } from './shader-module.js' */
/** /**
* @typedef Shader2 * @typedef ShaderPairDescriptor
* @property {ShaderModule} vertex * @property {ShaderModule} vertex
* @property {ShaderModule} fragment * @property {ShaderModule} [fragment]
*
* @typedef {Omit<Shader2, 'fragment'>} VertexOnly
* @typedef {Omit<Shader2, 'vertex'>} FragmentOnly
*
* @typedef Unified1
* @property {ShaderModule} shaderModule
*/ */
/** /**
* @typedef { * @typedef {
(Shader2 | Either<VertexOnly, FragmentOnly>) | ShaderPairDescriptor &
Unified1 { bindGroupLayouts?: BindGroupLayout[] }
* } ShaderPairDescriptor * } MaterialDescriptor
*/ */
export class Material { export class Material {
@ -42,18 +33,27 @@ export class Material {
/** /**
* @param {GPUDevice} device * @param {GPUDevice} device
* @param {ShaderPairDescriptor} shaders * @param {MaterialDescriptor} descriptor
* @param {BindGroupLayout[]} bindGroupLayouts
*/ */
constructor(device, shaders, bindGroupLayouts) { constructor(device, descriptor) {
this._device = device this._device = device
this._shaders = Material._parseShaders(shaders) this._shaders = Material._reflectShaders(descriptor)
this._bindGroupLayouts = bindGroupLayouts const bgl = descriptor.bindGroupLayouts
if (bindGroupLayouts && bindGroupLayouts.length > 0) { if (bgl && bgl.length > 0) {
this._pipelineLayout = device.createPipelineLayout({ this._bindGroupLayouts = bgl
bindGroupLayouts: bindGroupLayouts.map(bgl => bgl.handle) } 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. * shader types.
* *
* @param {ShaderPairDescriptor} shaders * @param {ShaderPairDescriptor} shaders
* @returns {UnifiedShader | ShaderPair} * @returns {ShaderPair}
*/ */
static _parseShaders(shaders) { static _reflectShaders(shaders) {
if (shaders == null) { 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') 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 wgslToWgpuFormat
} from '../utils/wgsl-to-wgpu.js' } from '../utils/wgsl-to-wgpu.js'
import { BufferBindingType } from '../enum.js' import { BufferBindingType } from '../enum.js'
import { BitFlags } from '../utils/bitflags.js'
/** @import { WGSLAccess, WGSLSamplerType } from '../utils/wgsl-to-wgpu.js' */ /** @import { WGSLAccess, WGSLSamplerType } from '../utils/wgsl-to-wgpu.js' */
@ -33,19 +32,26 @@ export class ShaderModule {
/** /**
* @param {GPUDevice} device * @param {GPUDevice} device
* @param {string} code * @param {GPUShaderModuleDescriptor} descriptor
* @param {string} [label]
*/ */
constructor(device, code, label) { constructor(device, descriptor) {
this._code = code this._code = descriptor.code
try { try {
this._handle = device.createShaderModule({ code, label }) this._handle = device.createShaderModule(descriptor)
} catch (err) { } catch (err) {
throw WebGPUObjectError.from(err, ShaderModule) throw WebGPUObjectError.from(err, ShaderModule)
} }
} }
/**
* @param {GPUDevice} device
* @param {GPUShaderModuleDescriptor} descriptor
*/
static create(device, descriptor) {
return new ShaderModule(device, descriptor)
}
reflect() { reflect() {
if (this._reflection == null) { if (this._reflection == null) {
this._reflection = new WgslReflect(this._code) 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 { export class ReflectedShader {
_shader _module
get module() {
return this._module
}
/** /**
* @param {ShaderModule} shader * @param {ShaderModule} shader
*/ */
constructor(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} * @returns {GPUShaderStageFlags}
*/ */
getShaderStages() { getShaderStages() {
const entry = this._shader.reflect().entry const entry = this._module.reflect().entry
let stages = 0 let stages = 0
stages |= entry.vertex.length > 0 ? stages |= entry.vertex.length > 0 ?
@ -86,12 +125,20 @@ export class ReflectedShader {
return stages return stages
} }
/**
* @param {GPUShaderStageFlags} stages
*/
hasStage(stages) {
return this.getShaderStages()
& stages
}
/** /**
* @param {GPUShaderStageFlags} stages * @param {GPUShaderStageFlags} stages
* @param {GroupBindingMap} [out=new GroupBindingMap()] * @param {GroupBindingMap} [out=new GroupBindingMap()]
*/ */
getBindingsForStage(stages, out = new GroupBindingMap()) { getBindingsForStage(stages, out = new GroupBindingMap()) {
const groups = this._shader.reflect().getBindGroups() const groups = this._module.reflect().getBindGroups()
groups.forEach((bindings, groupIndex) => { groups.forEach((bindings, groupIndex) => {
if (!out.has(groupIndex)) { if (!out.has(groupIndex)) {
@ -116,7 +163,7 @@ export class ReflectedShader {
* @param {Map<any, any>} map * @param {Map<any, any>} map
* @returns {number[]} * @returns {number[]}
*/ */
_sortKeyIndices(map) { static _sortKeyIndices(map) {
return Array.from(map.keys()).sort((a, b) => a - b) return Array.from(map.keys()).sort((a, b) => a - b)
} }
@ -124,7 +171,7 @@ export class ReflectedShader {
* @param {VariableInfo} _variableInfo * @param {VariableInfo} _variableInfo
* @returns {GPUBufferBindingLayout} * @returns {GPUBufferBindingLayout}
*/ */
_parseUniform(_variableInfo) { static _parseUniform(_variableInfo) {
return { return {
type: BufferBindingType.Uniform, type: BufferBindingType.Uniform,
// TODO: infer these two properties // TODO: infer these two properties
@ -137,7 +184,7 @@ export class ReflectedShader {
* @param {VariableInfo} variableInfo * @param {VariableInfo} variableInfo
* @returns {GPUBufferBindingLayout} * @returns {GPUBufferBindingLayout}
*/ */
_parseStorage(variableInfo) { static _parseStorage(variableInfo) {
return { return {
type: accessToBufferType( type: accessToBufferType(
/** @type {WGSLAccess} */ /** @type {WGSLAccess} */
@ -153,7 +200,7 @@ export class ReflectedShader {
* @param {VariableInfo} variableInfo * @param {VariableInfo} variableInfo
* @returns {GPUTextureBindingLayout} * @returns {GPUTextureBindingLayout}
*/ */
_parseTexture(variableInfo) { static _parseTexture(variableInfo) {
const [type, sampledType] = parseTextureType( const [type, sampledType] = parseTextureType(
variableInfo.type.name variableInfo.type.name
) )
@ -169,7 +216,7 @@ export class ReflectedShader {
* @param {VariableInfo} variableInfo * @param {VariableInfo} variableInfo
* @returns {GPUSamplerBindingLayout} * @returns {GPUSamplerBindingLayout}
*/ */
_parseSampler(variableInfo) { static _parseSampler(variableInfo) {
return { return {
type: typeToSamplerBindingType( type: typeToSamplerBindingType(
/** @type {WGSLSamplerType} */(variableInfo.type.name) /** @type {WGSLSamplerType} */(variableInfo.type.name)
@ -181,7 +228,7 @@ export class ReflectedShader {
* @param {VariableInfo} variableInfo * @param {VariableInfo} variableInfo
* @returns {GPUStorageTextureBindingLayout} * @returns {GPUStorageTextureBindingLayout}
*/ */
_parseStorageTexture(variableInfo) { static _parseStorageTexture(variableInfo) {
const [type] = parseTextureType(variableInfo.type.name) const [type] = parseTextureType(variableInfo.type.name)
return { return {
@ -197,7 +244,7 @@ export class ReflectedShader {
* @param {VariableStageInfo} variableStageInfo * @param {VariableStageInfo} variableStageInfo
* @returns {GPUBindGroupLayoutEntry} * @returns {GPUBindGroupLayoutEntry}
*/ */
_variableInfoToEntry(variableStageInfo) { static _variableInfoToEntry(variableStageInfo) {
const { stages: visibility, variableInfo } = variableStageInfo const { stages: visibility, variableInfo } = variableStageInfo
switch (variableInfo.resourceType) { switch (variableInfo.resourceType) {
@ -240,7 +287,7 @@ export class ReflectedShader {
/** /**
* @param {GroupBindingMap} groupBindings * @param {GroupBindingMap} groupBindings
*/ */
createBindGroupLayoutEntries(groupBindings) { static createBindGroupLayoutEntries(groupBindings) {
const sortedGroupIndices = this._sortKeyIndices(groupBindings) const sortedGroupIndices = this._sortKeyIndices(groupBindings)
return sortedGroupIndices.map(groupIndex => { return sortedGroupIndices.map(groupIndex => {
@ -253,35 +300,10 @@ export class ReflectedShader {
} }
} }
export class UnifiedShader { export class ShaderPair {
_shader /** @type {ReflectedShader} */
/**
* @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 {
_vertex _vertex
/** @type {ReflectedShader} */
_fragment _fragment
/** /**
@ -289,25 +311,52 @@ export class ReflectedShaderPair {
* @param {ReflectedShader} [fragment] * @param {ReflectedShader} [fragment]
*/ */
constructor(vertex, 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._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() { _createGroupBindings() {
const groupBindings = new GroupBindingMap() 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( this._vertex.getBindingsForStage(
GPUShaderStage.VERTEX, GPUShaderStage.VERTEX,
groupBindings groupBindings
@ -322,11 +371,46 @@ export class ReflectedShaderPair {
} }
createBindGroupLayoutEntries() { createBindGroupLayoutEntries() {
// FIXME: move this call and all the other calls return ReflectedShader.createBindGroupLayoutEntries(
// somewhere else
return this._shader.createBindGroupLayoutEntries(
this._createGroupBindings() 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' import { FlagEnum } from '../utils.js'
export const ShaderType = FlagEnum( export const ShaderStage = FlagEnum(
'auto',
'vertex', 'vertex',
'fragment', 'fragment',
'compute' '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({ export const ResourceType = Object.freeze({
Sampler: 0, Sampler: 0,
TextureView: 1, TextureView: 1,