initial commit
This commit is contained in:
commit
2c5ac481a4
23 changed files with 2052 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules/
|
||||||
|
|
3
client
Executable file
3
client
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
servo --pref dom_webgpu_enabled=true http://localhost:8000
|
30
index.html
Normal file
30
index.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WebGPU</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background-color: #222;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
border: 1px solid #555;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<canvas id="webgpu-canvas"></canvas>
|
||||||
|
<script type="module" src="./index.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
169
index.js
Normal file
169
index.js
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
import { GraphicsDevice } from './src/core/graphics-device.js'
|
||||||
|
import { PowerPreference } from './src/enum.js'
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const canvas = /** @type {HTMLCanvasElement} */ (document.getElementById('webgpu-canvas'))
|
||||||
|
|
||||||
|
if (!canvas) {
|
||||||
|
console.error("Canvas element not found!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = 800
|
||||||
|
canvas.height = 600
|
||||||
|
|
||||||
|
|
||||||
|
const graphicsDevice = await GraphicsDevice.build()
|
||||||
|
.withCanvas(canvas)
|
||||||
|
.withAdapter({ powerPreference: PowerPreference.HighPerformance })
|
||||||
|
.build()
|
||||||
|
|
||||||
|
const success = await graphicsDevice.initialize()
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
console.error("Failed to initialize WebGPU.")
|
||||||
|
document.body.innerHTML = "WebGPU initialization failed. Please use a supported browser and ensure hardware acceleration is enabled."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const shaderCode = `
|
||||||
|
@vertex
|
||||||
|
fn vs_main(@location(0) in_pos : vec3<f32>) -> @builtin(position) vec4<f32> {
|
||||||
|
return vec4<f32>(in_pos, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main() -> @location(0) vec4<f32> {
|
||||||
|
return vec4<f32>(0.0, 0.5, 1.0, 1.0);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const shaderModule = graphicsDevice.createShaderModule(shaderCode, 'TriangleShader')
|
||||||
|
|
||||||
|
const vertices = new Float32Array([
|
||||||
|
0.0, 0.5, 0.0,
|
||||||
|
-0.5, -0.5, 0.0,
|
||||||
|
0.5, -0.5, 0.0
|
||||||
|
])
|
||||||
|
|
||||||
|
const vertexBuffer = graphicsDevice.createBuffer(
|
||||||
|
{
|
||||||
|
label: 'TriangleVertexBuffer',
|
||||||
|
size: vertices.byteLength,
|
||||||
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
||||||
|
},
|
||||||
|
vertices
|
||||||
|
)
|
||||||
|
|
||||||
|
const matrixSize = 4 * 4 * Float32Array.BYTES_PER_ELEMENT
|
||||||
|
const matrixData = new Float32Array(16)
|
||||||
|
|
||||||
|
matrixData[0] = 1
|
||||||
|
matrixData[5] = 1
|
||||||
|
matrixData[10] = 1
|
||||||
|
matrixData[15] = 1
|
||||||
|
|
||||||
|
const uniformBuffer = graphicsDevice.createBuffer(
|
||||||
|
{
|
||||||
|
label: 'SceneUniformsBuffer',
|
||||||
|
size: matrixSize,
|
||||||
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
||||||
|
},
|
||||||
|
matrixData
|
||||||
|
)
|
||||||
|
|
||||||
|
const bindings = [{
|
||||||
|
binding: 0,
|
||||||
|
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
||||||
|
buffer: /** @type {GPUBufferBindingLayout} */ ({ type: 'uniform' })
|
||||||
|
}]
|
||||||
|
|
||||||
|
const bindGroupLayout = graphicsDevice.createBindGroupLayout(bindings, 'UniformLayout')
|
||||||
|
|
||||||
|
|
||||||
|
const pipelineLayout = graphicsDevice.createPipelineLayout([bindGroupLayout], 'PipelineLayout')
|
||||||
|
|
||||||
|
const pipeline = graphicsDevice.createRenderPipeline({
|
||||||
|
label: 'TrianglePipeline',
|
||||||
|
layout: pipelineLayout,
|
||||||
|
vertex: {
|
||||||
|
module: shaderModule.handle,
|
||||||
|
entryPoint: 'vs_main',
|
||||||
|
buffers: [
|
||||||
|
{
|
||||||
|
arrayStride: 3 * 4,
|
||||||
|
attributes: [
|
||||||
|
{ shaderLocation: 0, offset: 0, format: 'float32x3' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
fragment: {
|
||||||
|
module: shaderModule.handle,
|
||||||
|
entryPoint: 'fs_main',
|
||||||
|
targets: [
|
||||||
|
{ format: graphicsDevice.swapChain.format }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
primitive: {
|
||||||
|
topology: 'triangle-list',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
/** @type {Array<import('./src/core/graphics-device.js').BindGroupEntry>} */
|
||||||
|
const entries = [{
|
||||||
|
binding: 0,
|
||||||
|
resource: uniformBuffer
|
||||||
|
}]
|
||||||
|
|
||||||
|
const uniformBindGroup = graphicsDevice.createBindGroup(bindGroupLayout, entries, 'Uniforms')
|
||||||
|
|
||||||
|
async function frame() {
|
||||||
|
if (!graphicsDevice.isInitialized) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandRecorder = graphicsDevice.createCommandRecorder('FrameCommands')
|
||||||
|
|
||||||
|
const passEncoder = commandRecorder.beginRenderPass()
|
||||||
|
|
||||||
|
if (passEncoder) {
|
||||||
|
passEncoder.setPipeline(pipeline.handle)
|
||||||
|
passEncoder.setVertexBuffer(0, vertexBuffer.handle)
|
||||||
|
passEncoder.setBindGroup(0, uniformBindGroup.handle)
|
||||||
|
passEncoder.draw(3)
|
||||||
|
|
||||||
|
commandRecorder.endRenderPass()
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandBuffer = commandRecorder.finish()
|
||||||
|
graphicsDevice.submitCommands([commandBuffer])
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(frame)
|
||||||
|
|
||||||
|
// TODO: move to graphics device or somewhere else
|
||||||
|
const resizeObserver = new ResizeObserver(entries => {
|
||||||
|
for (let entry of entries) {
|
||||||
|
const { width, height } = entry.contentRect
|
||||||
|
if (graphicsDevice.isInitialized && width > 0 && height > 0) {
|
||||||
|
graphicsDevice.swapChain.resize(width, height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
resizeObserver.observe(canvas)
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
resizeObserver.disconnect()
|
||||||
|
graphicsDevice.destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', main)
|
||||||
|
} else {
|
||||||
|
main()
|
||||||
|
}
|
||||||
|
|
16
jsconfig.json
Normal file
16
jsconfig.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "es2020",
|
||||||
|
"target": "es6",
|
||||||
|
"lib": ["es2022", "dom"],
|
||||||
|
"types": ["@webgpu/types"],
|
||||||
|
"checkJs": true,
|
||||||
|
"paths": {
|
||||||
|
"/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
23
package-lock.json
generated
Normal file
23
package-lock.json
generated
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"name": "wgpu",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "wgpu",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@webgpu/types": "^0.1.60"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@webgpu/types": {
|
||||||
|
"version": "0.1.60",
|
||||||
|
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.60.tgz",
|
||||||
|
"integrity": "sha512-8B/tdfRFKdrnejqmvq95ogp8tf52oZ51p3f4QD5m5Paey/qlX4Rhhy5Y8tgFMi7Ms70HzcMMw3EQjH/jdhTwlA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
package.json
Normal file
16
package.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "wgpu",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"devDependencies": {
|
||||||
|
"@webgpu/types": "^0.1.60"
|
||||||
|
}
|
||||||
|
}
|
4
server
Executable file
4
server
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
python -m http.server
|
||||||
|
|
81
src/core/command-recorder.js
Normal file
81
src/core/command-recorder.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import { LoadOp, StoreOp } from '../enum.js'
|
||||||
|
import { CommandRecorderError } from '../utils/errors.js'
|
||||||
|
import { SwapChain } from './swap-chain.js'
|
||||||
|
|
||||||
|
export class CommandRecorder {
|
||||||
|
static _defaultClearValue = { r: 0, g: 0, b: 0, a: 1 }
|
||||||
|
|
||||||
|
_device
|
||||||
|
_swapChain
|
||||||
|
_label
|
||||||
|
|
||||||
|
_encoder
|
||||||
|
|
||||||
|
/** @type {GPURenderPassEncoder | undefined} */
|
||||||
|
_passEncoder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUDevice} device
|
||||||
|
* @param {SwapChain} swapChain
|
||||||
|
* @param {string} [label]
|
||||||
|
*/
|
||||||
|
constructor(device, swapChain, label) {
|
||||||
|
this._device = device
|
||||||
|
this._swapChain = swapChain
|
||||||
|
this._label = label
|
||||||
|
|
||||||
|
this._encoder = device.createCommandEncoder({ label })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {[GPURenderPassColorAttachment]}
|
||||||
|
*/
|
||||||
|
_defaultColorAttachment() {
|
||||||
|
const view = this._swapChain.getCurrentTextureView()
|
||||||
|
|
||||||
|
return [{
|
||||||
|
view,
|
||||||
|
clearValue: CommandRecorder._defaultClearValue,
|
||||||
|
loadOp: LoadOp.Clear,
|
||||||
|
storeOp: StoreOp.Store
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {GPURenderPassColorAttachment[]} [colorAttachments]
|
||||||
|
* @param {GPURenderPassDepthStencilAttachment} [depthStencilAttachment]
|
||||||
|
* @returns {GPURenderPassEncoder}
|
||||||
|
*/
|
||||||
|
beginRenderPass(colorAttachments, depthStencilAttachment) {
|
||||||
|
if (this._passEncoder) {
|
||||||
|
throw CommandRecorderError.activeRenderPass()
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachments = colorAttachments || this._defaultColorAttachment()
|
||||||
|
|
||||||
|
const descriptor = {
|
||||||
|
label: this._label || 'RenderPass',
|
||||||
|
colorAttachments: attachments,
|
||||||
|
depthStencilAttachment
|
||||||
|
}
|
||||||
|
|
||||||
|
this._passEncoder = this._encoder.beginRenderPass(descriptor)
|
||||||
|
|
||||||
|
return this._passEncoder
|
||||||
|
}
|
||||||
|
|
||||||
|
endRenderPass() {
|
||||||
|
if (!this._passEncoder) { return }
|
||||||
|
|
||||||
|
this._passEncoder.end()
|
||||||
|
this._passEncoder = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
finish() {
|
||||||
|
if (this._passEncoder) {
|
||||||
|
this.endRenderPass()
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._encoder.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
464
src/core/graphics-device.js
Normal file
464
src/core/graphics-device.js
Normal file
|
@ -0,0 +1,464 @@
|
||||||
|
import { SwapChain } from './swap-chain.js'
|
||||||
|
import { Buffer, UniformBuffer } from '../resources/buffer.js'
|
||||||
|
import { GraphicsDeviceError, WebGPUError, BufferError, WebGPUObjectError } from '../utils/errors.js'
|
||||||
|
import { GraphicsDeviceInitialized, GraphicsDeviceLost } from '../utils/events.js'
|
||||||
|
import { EventEmitter } from '../utils.js'
|
||||||
|
import { ShaderModule } from '../resources/shader-module.js'
|
||||||
|
import { RenderPipeline } from '../rendering/render-pipeline.js'
|
||||||
|
import { CommandRecorder } from './command-recorder.js'
|
||||||
|
import { BindGroupLayout } from '../resources/bind-group-layout.js'
|
||||||
|
import { BindGroup } from '../resources/bind-group.js'
|
||||||
|
import { Texture } from '../resources/texture.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef BaseBindGroupEntry
|
||||||
|
* @property {GPUIndex32} binding
|
||||||
|
* @property {BindingResource} resource
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{
|
||||||
|
offset?: number
|
||||||
|
size?: number
|
||||||
|
* }} BufferBindingResource
|
||||||
|
*
|
||||||
|
* @typedef {{ textureView?: GPUTextureView }} TextureBindingResource
|
||||||
|
*
|
||||||
|
* @typedef {Buffer | Texture | Sampler} BindingResource
|
||||||
|
*
|
||||||
|
* @typedef {
|
||||||
|
(BufferBindingResource | TextureBindingResource) &
|
||||||
|
BaseBindGroupEntry
|
||||||
|
* } BindGroupEntry
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Sampler } from '../resources/sampler.js'
|
||||||
|
|
||||||
|
class GraphicsDeviceBuilder {
|
||||||
|
_canvas
|
||||||
|
|
||||||
|
get canvas() {
|
||||||
|
return this._canvas
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {GPURequestAdapterOptions} */
|
||||||
|
_adapter_options
|
||||||
|
|
||||||
|
/** @type {GPUDeviceDescriptor} */
|
||||||
|
_device_descriptor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLCanvasElement} [canvasElement]
|
||||||
|
*/
|
||||||
|
constructor(canvasElement) {
|
||||||
|
this._canvas = canvasElement
|
||||||
|
}
|
||||||
|
|
||||||
|
isSupported() {
|
||||||
|
return navigator.gpu
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLCanvasElement} canvasElement
|
||||||
|
*/
|
||||||
|
withCanvas(canvasElement) {
|
||||||
|
this._canvas = canvasElement
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPURequestAdapterOptions} [options]
|
||||||
|
*/
|
||||||
|
withAdapter(options) {
|
||||||
|
if (!this.isSupported()) {
|
||||||
|
throw WebGPUError.unsupported()
|
||||||
|
}
|
||||||
|
|
||||||
|
this._adapter_options = options
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUDeviceDescriptor} [options]
|
||||||
|
*/
|
||||||
|
withDevice(options) {
|
||||||
|
if (!this.isSupported()) {
|
||||||
|
throw WebGPUError.unsupported()
|
||||||
|
}
|
||||||
|
|
||||||
|
this._device_descriptor = options
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
async build() {
|
||||||
|
return new GraphicsDevice(
|
||||||
|
this._canvas,
|
||||||
|
new DeviceHandler(
|
||||||
|
this._adapter_options,
|
||||||
|
this._device_descriptor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeviceHandler {
|
||||||
|
/** @type {GPURequestAdapterOptions} */
|
||||||
|
_adapter_options
|
||||||
|
|
||||||
|
/** @type {GPUAdapter} */
|
||||||
|
_adapter
|
||||||
|
|
||||||
|
get adapter() {
|
||||||
|
return this._adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {GPUDeviceDescriptor} */
|
||||||
|
_device_descriptor
|
||||||
|
|
||||||
|
/** @type {GPUDevice} */
|
||||||
|
_device
|
||||||
|
|
||||||
|
get device() {
|
||||||
|
return this._device
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPURequestAdapterOptions} adapterOptions
|
||||||
|
* @param {GPUDeviceDescriptor} deviceDescriptor
|
||||||
|
*/
|
||||||
|
constructor(adapterOptions, deviceDescriptor) {
|
||||||
|
this._adapter_options = adapterOptions
|
||||||
|
this._device_descriptor = deviceDescriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
async create() {
|
||||||
|
this._adapter = await navigator.gpu.requestAdapter(this._adapter_options)
|
||||||
|
|
||||||
|
if (!this._adapter) {
|
||||||
|
throw WebGPUError.adapterUnavailable()
|
||||||
|
}
|
||||||
|
|
||||||
|
this._device = await this._adapter.requestDevice(this._device_descriptor)
|
||||||
|
|
||||||
|
if (!this._device) {
|
||||||
|
throw WebGPUError.deviceUnavailable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GraphicsDevice extends EventEmitter {
|
||||||
|
_canvas
|
||||||
|
_deviceHandler
|
||||||
|
|
||||||
|
/** @type {SwapChain} */
|
||||||
|
_swapChain
|
||||||
|
|
||||||
|
/** @type {GPUQueue} */
|
||||||
|
_queue
|
||||||
|
|
||||||
|
_isInitialized = false
|
||||||
|
|
||||||
|
get isInitialized() {
|
||||||
|
return this._isInitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
get adapter() {
|
||||||
|
return this._deviceHandler.adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
get device() {
|
||||||
|
return this._deviceHandler.device
|
||||||
|
}
|
||||||
|
|
||||||
|
get queue() {
|
||||||
|
return this._queue
|
||||||
|
}
|
||||||
|
|
||||||
|
get swapChain() {
|
||||||
|
return this._swapChain
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLCanvasElement} canvas
|
||||||
|
* @param {DeviceHandler} deviceHandler
|
||||||
|
*/
|
||||||
|
constructor(canvas, deviceHandler) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this._canvas = canvas
|
||||||
|
this._deviceHandler = deviceHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLCanvasElement} [canvas]
|
||||||
|
*/
|
||||||
|
static build(canvas) {
|
||||||
|
return new GraphicsDeviceBuilder(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
await this._deviceHandler.create()
|
||||||
|
|
||||||
|
this._swapChain = new SwapChain(
|
||||||
|
this._canvas,
|
||||||
|
this._deviceHandler.device
|
||||||
|
)
|
||||||
|
|
||||||
|
this._swapChain.configure()
|
||||||
|
|
||||||
|
this._queue = this.device.queue
|
||||||
|
|
||||||
|
this._deviceHandler.device.lost.then(info => {
|
||||||
|
this._isInitialized = false
|
||||||
|
|
||||||
|
this.emit(
|
||||||
|
GraphicsDeviceLost.EventName,
|
||||||
|
new GraphicsDeviceLost(info)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
this._isInitialized = true
|
||||||
|
|
||||||
|
this.emit(
|
||||||
|
GraphicsDeviceInitialized.EventName,
|
||||||
|
new GraphicsDeviceInitialized(this)
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Omit<GPUBufferDescriptor, 'mappedAtCreation'>} BufferDescriptor
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a GPU buffer
|
||||||
|
* @param {BufferDescriptor} descriptor
|
||||||
|
* @param {ArrayBufferView | ArrayBuffer} [data]
|
||||||
|
* @returns {Buffer}
|
||||||
|
*
|
||||||
|
* @throws {GPUBufferError}
|
||||||
|
*/
|
||||||
|
createBuffer(descriptor, data) {
|
||||||
|
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buffer = Buffer.create(this.device, descriptor)
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
buffer.write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
} catch (err) {
|
||||||
|
throw BufferError.from(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} size
|
||||||
|
* @param {ArrayBufferView | ArrayBuffer} [data]
|
||||||
|
* @param {string} [label]
|
||||||
|
*/
|
||||||
|
createUniformBuffer(size, data, label) {
|
||||||
|
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
|
||||||
|
|
||||||
|
const buffer = UniformBuffer.create(this.device, {
|
||||||
|
size,
|
||||||
|
label
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
buffer.write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a shader module from WGSL code.
|
||||||
|
* @param {string} code
|
||||||
|
* @param {string} [label]
|
||||||
|
* @returns {ShaderModule}
|
||||||
|
*/
|
||||||
|
createShaderModule(code, label) {
|
||||||
|
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
|
||||||
|
|
||||||
|
try {
|
||||||
|
return ShaderModule.create(this.device, { code, label })
|
||||||
|
} catch (err) {
|
||||||
|
throw WebGPUObjectError.from(err, ShaderModule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a render pipeline.
|
||||||
|
* @param {GPURenderPipelineDescriptor} descriptor - Raw render pipeline descriptor.
|
||||||
|
* @returns {RenderPipeline}
|
||||||
|
*/
|
||||||
|
createRenderPipeline(descriptor) {
|
||||||
|
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const gpuPipeline = this.device.createRenderPipeline(descriptor)
|
||||||
|
return new RenderPipeline(gpuPipeline, descriptor.label)
|
||||||
|
} catch (err) {
|
||||||
|
throw WebGPUObjectError.from(err, RenderPipeline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a CommandRecorder to begin recording GPU commands.
|
||||||
|
* @param {string} [label]
|
||||||
|
* @returns {CommandRecorder}
|
||||||
|
*/
|
||||||
|
createCommandRecorder(label) {
|
||||||
|
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
|
||||||
|
|
||||||
|
return new CommandRecorder(this.device, this._swapChain, label)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUBindGroupLayoutEntry[]} entries
|
||||||
|
* @param {string} [label]
|
||||||
|
*/
|
||||||
|
createBindGroupLayout(entries, label) {
|
||||||
|
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
|
||||||
|
|
||||||
|
return BindGroupLayout.create(this.device, {
|
||||||
|
label,
|
||||||
|
entries
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {BindGroupLayout} layout
|
||||||
|
* @param {BindGroupEntry[]} bindings
|
||||||
|
* @param {string} [label]
|
||||||
|
*/
|
||||||
|
createBindGroup(layout, bindings, label) {
|
||||||
|
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
|
||||||
|
|
||||||
|
|
||||||
|
const entries = bindings.map(def => ({
|
||||||
|
binding: def.binding,
|
||||||
|
resource: def.resource.asBindingResource(def)
|
||||||
|
}))
|
||||||
|
|
||||||
|
return BindGroup.create(this.device, {
|
||||||
|
layout: layout.handle,
|
||||||
|
entries,
|
||||||
|
label
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array<BindGroupLayout>} layouts
|
||||||
|
* @param {string} [label]
|
||||||
|
*/
|
||||||
|
createPipelineLayout(layouts, label) {
|
||||||
|
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
|
||||||
|
|
||||||
|
const bindGroupLayouts = layouts.map(layout => layout.handle)
|
||||||
|
|
||||||
|
return this.device.createPipelineLayout({
|
||||||
|
label,
|
||||||
|
bindGroupLayouts
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUSamplerDescriptor} [descriptor]
|
||||||
|
*/
|
||||||
|
createSampler(descriptor) {
|
||||||
|
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
|
||||||
|
|
||||||
|
return Sampler.create(this.device, descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUTextureDescriptor} descriptor
|
||||||
|
*/
|
||||||
|
createTexture(descriptor) {
|
||||||
|
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Texture.create(this.device, descriptor)
|
||||||
|
} catch (err) {
|
||||||
|
throw WebGPUObjectError.from(err, Texture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ImageBitmap} bitmap
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {string} [options.label]
|
||||||
|
* @param {GPUTextureFormat} [options.format='rgba8unorm']
|
||||||
|
* @param {GPUTextureUsageFlags} [options.usage=TEXTURE_BINDING | COPY_DST | RENDER_ATTACHMENT]
|
||||||
|
* @param {boolean} [options.generateMipmaps=false]
|
||||||
|
* @param {boolean} [options.flipY=false]
|
||||||
|
*/
|
||||||
|
createTextureFromBitmap(bitmap, options) {
|
||||||
|
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
|
||||||
|
|
||||||
|
if (!bitmap) { throw new TypeError('Provided bitmap is null.') }
|
||||||
|
|
||||||
|
const usage = (options.usage ?? (GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT)) | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
|
||||||
|
|
||||||
|
const mipLevelCount = 1
|
||||||
|
|
||||||
|
/** @type {GPUTextureDescriptor} */
|
||||||
|
const descriptor = {
|
||||||
|
label: options.label,
|
||||||
|
size: {
|
||||||
|
width: bitmap.width,
|
||||||
|
height: bitmap.height,
|
||||||
|
depthOrArrayLayers: 1
|
||||||
|
},
|
||||||
|
format: options.format ?? 'rgba8unorm',
|
||||||
|
usage,
|
||||||
|
dimension: '2d',
|
||||||
|
mipLevelCount,
|
||||||
|
sampleCount: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const texture = this.device.createTexture(descriptor)
|
||||||
|
this.queue.copyExternalImageToTexture(
|
||||||
|
{ source: bitmap, flipY: options.flipY ?? false },
|
||||||
|
{ texture, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
|
||||||
|
descriptor.size
|
||||||
|
)
|
||||||
|
const wrapper = new Texture(this.device, texture)
|
||||||
|
return wrapper
|
||||||
|
} catch (err) {
|
||||||
|
throw WebGPUObjectError.from(err, Texture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits an array of command buffers to the GPU queue.
|
||||||
|
* @param {GPUCommandBuffer[]} commandBuffers
|
||||||
|
*/
|
||||||
|
submitCommands(commandBuffers) {
|
||||||
|
if (!this._isInitialized || !commandBuffers || commandBuffers.length === 0) return
|
||||||
|
|
||||||
|
this.queue.submit(commandBuffers)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up GPU resources. Call when the application exits.
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
if (this.device) {
|
||||||
|
this.device.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
this._deviceHandler = null
|
||||||
|
this._queue = null
|
||||||
|
this._swapChain = null
|
||||||
|
this._isInitialized = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
99
src/core/swap-chain.js
Normal file
99
src/core/swap-chain.js
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
import { WebGPUError } from '../utils/errors.js'
|
||||||
|
|
||||||
|
|
||||||
|
/** @import { PositiveInteger } from '../utils.js' */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Omit<GPUCanvasConfiguration, 'device' | 'format'>} SwapChainConfiguration
|
||||||
|
*/
|
||||||
|
export class SwapChain {
|
||||||
|
_canvas
|
||||||
|
_device
|
||||||
|
_context
|
||||||
|
_format
|
||||||
|
_width
|
||||||
|
_height
|
||||||
|
|
||||||
|
/** @type {SwapChainConfiguration} */
|
||||||
|
_configuration
|
||||||
|
|
||||||
|
get context() {
|
||||||
|
return this._context
|
||||||
|
}
|
||||||
|
|
||||||
|
get format() {
|
||||||
|
return this._format
|
||||||
|
}
|
||||||
|
|
||||||
|
get width() {
|
||||||
|
return this._width
|
||||||
|
}
|
||||||
|
|
||||||
|
get height() {
|
||||||
|
return this._height
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLCanvasElement} canvas
|
||||||
|
* @param {GPUDevice} device
|
||||||
|
* @param {GPUCanvasContext} [context]
|
||||||
|
*
|
||||||
|
* @throws {WebGPUError}
|
||||||
|
* Throws an error if unable to request a WebGPU context
|
||||||
|
*/
|
||||||
|
constructor(canvas, device, context) {
|
||||||
|
this._canvas = canvas
|
||||||
|
this._device = device
|
||||||
|
|
||||||
|
this._context = context || canvas.getContext('webgpu')
|
||||||
|
|
||||||
|
if (!this._context) {
|
||||||
|
throw WebGPUError.contextFailure()
|
||||||
|
}
|
||||||
|
|
||||||
|
this._format = navigator.gpu.getPreferredCanvasFormat()
|
||||||
|
this._width = canvas.width
|
||||||
|
this._height = canvas.height
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SwapChainConfiguration} [configuration]
|
||||||
|
*/
|
||||||
|
configure(configuration) {
|
||||||
|
if (configuration) {
|
||||||
|
this._configuration = configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
this._width = this._canvas.width
|
||||||
|
this._height = this._canvas.height
|
||||||
|
|
||||||
|
this._context.configure({
|
||||||
|
device: this._device,
|
||||||
|
format: this._format,
|
||||||
|
alphaMode: 'opaque',
|
||||||
|
...this._configuration
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentTextureView() {
|
||||||
|
return this._context.getCurrentTexture().createView()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {number} const T
|
||||||
|
* @param {PositiveInteger<T>} width
|
||||||
|
* @param {PositiveInteger<T>} height
|
||||||
|
*/
|
||||||
|
resize(width, height) {
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._width !== width || this._height !== height) {
|
||||||
|
this._canvas.width = width
|
||||||
|
this._canvas.height = height
|
||||||
|
this.configure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
356
src/enum.js
Normal file
356
src/enum.js
Normal file
|
@ -0,0 +1,356 @@
|
||||||
|
import { Enum } from './utils.js'
|
||||||
|
|
||||||
|
|
||||||
|
export const AddressMode = Enum(
|
||||||
|
'clamp-to-edge',
|
||||||
|
'repeat',
|
||||||
|
'mirror-repeat'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const AutoLayoutMode = Enum('auto')
|
||||||
|
|
||||||
|
export const BlendFactor = Enum(
|
||||||
|
'zero',
|
||||||
|
'one',
|
||||||
|
'src',
|
||||||
|
'one-minus-src',
|
||||||
|
'src-alpha',
|
||||||
|
'one-minus-src-alpha',
|
||||||
|
'dst',
|
||||||
|
'one-minus-dst',
|
||||||
|
'dst-alpha',
|
||||||
|
'one-minus-dst-alpha',
|
||||||
|
'src-alpha-saturated',
|
||||||
|
'constant',
|
||||||
|
'one-minus-constant',
|
||||||
|
'src1',
|
||||||
|
'one-minus-src1',
|
||||||
|
'src1-alpha',
|
||||||
|
'one-minus-src1-alpha'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
export const BlendOperation = Enum(
|
||||||
|
'add',
|
||||||
|
'subtract',
|
||||||
|
'reverse-subtract',
|
||||||
|
'min',
|
||||||
|
'max'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const BufferBindingType = Enum(
|
||||||
|
'uniform',
|
||||||
|
'storage',
|
||||||
|
'read-only-storage'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const BufferMapState = Enum(
|
||||||
|
'unmapped',
|
||||||
|
'pending',
|
||||||
|
'mapped'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const CanvasAlphaMode = Enum(
|
||||||
|
'opaque',
|
||||||
|
'premultiplied'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const CanvasToneMappingMode = Enum(
|
||||||
|
'standard',
|
||||||
|
'extended'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const CompareFunction = Enum(
|
||||||
|
'never',
|
||||||
|
'less',
|
||||||
|
'equal',
|
||||||
|
'less-equal',
|
||||||
|
'greater',
|
||||||
|
'not-equal',
|
||||||
|
'greater-equal',
|
||||||
|
'always'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const CompilationMessageType = Enum(
|
||||||
|
'error',
|
||||||
|
'warning',
|
||||||
|
'info'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const CullMode = Enum(
|
||||||
|
'none',
|
||||||
|
'front',
|
||||||
|
'back'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const DeviceLostReason = Enum(
|
||||||
|
'unknown',
|
||||||
|
'destroyed'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const ErrorFilter = Enum(
|
||||||
|
'validation',
|
||||||
|
'out-of-memory',
|
||||||
|
'internal'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const FeatureName = Enum(
|
||||||
|
'depth-clip-control',
|
||||||
|
'depth32float-stencil8',
|
||||||
|
'texture-compression-bc',
|
||||||
|
'texture-compression-bc-sliced-3d',
|
||||||
|
'texture-compression-etc2',
|
||||||
|
'texture-compression-astc',
|
||||||
|
'texture-compression-astc-sliced-3d',
|
||||||
|
'timestamp-query',
|
||||||
|
'indirect-first-instance',
|
||||||
|
'shader-f16',
|
||||||
|
'rg11b10ufloat-renderable',
|
||||||
|
'bgra8unorm-storage',
|
||||||
|
'float32-filterable',
|
||||||
|
'float32-blendable',
|
||||||
|
'clip-distances',
|
||||||
|
'dual-source-blending'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const FilterMode = Enum(
|
||||||
|
'nearest',
|
||||||
|
'linear'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const FrontFace = Enum(
|
||||||
|
'ccw',
|
||||||
|
'cw'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const IndexFormat = Enum(
|
||||||
|
'uint16',
|
||||||
|
'uint32'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const LoadOp = Enum(
|
||||||
|
'load',
|
||||||
|
'clear'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const MipmapFilterMode = Enum(
|
||||||
|
'nearest',
|
||||||
|
'linear'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const PipelineErrorReason = Enum(
|
||||||
|
'validation',
|
||||||
|
'internal'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const PowerPreference = Enum(
|
||||||
|
'low-power',
|
||||||
|
'high-performance'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const PrimitiveTopology = Enum(
|
||||||
|
'point-list',
|
||||||
|
'line-list',
|
||||||
|
'line-strip',
|
||||||
|
'triangle-list',
|
||||||
|
'triangle-strip'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const QueryType = Enum(
|
||||||
|
'occlusion',
|
||||||
|
'timestamp'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const SamplerBindingType = Enum(
|
||||||
|
'filtering',
|
||||||
|
'non-filtering',
|
||||||
|
'comparison'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const StencilOperation = Enum(
|
||||||
|
'keep',
|
||||||
|
'zero',
|
||||||
|
'replace',
|
||||||
|
'invert',
|
||||||
|
'increment-clamp',
|
||||||
|
'decrement-clamp',
|
||||||
|
'increment-wrap',
|
||||||
|
'decrement-wrap'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const StorageTextureAccess = Enum(
|
||||||
|
'write-only',
|
||||||
|
'read-only',
|
||||||
|
'read-write'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const StoreOp = Enum(
|
||||||
|
'store',
|
||||||
|
'discard'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const TextureAspect = Enum(
|
||||||
|
'all',
|
||||||
|
'stencil-only',
|
||||||
|
'depth-only'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const TextureDimension = Enum(
|
||||||
|
'1d',
|
||||||
|
'2d',
|
||||||
|
'3d'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const TextureFormat = Enum(
|
||||||
|
'r8unorm',
|
||||||
|
'r8snorm',
|
||||||
|
'r8uint',
|
||||||
|
'r8sint',
|
||||||
|
'r16uint',
|
||||||
|
'r16sint',
|
||||||
|
'r16float',
|
||||||
|
'rg8unorm',
|
||||||
|
'rg8snorm',
|
||||||
|
'rg8uint',
|
||||||
|
'rg8sint',
|
||||||
|
'r32uint',
|
||||||
|
'r32sint',
|
||||||
|
'r32float',
|
||||||
|
'rg16uint',
|
||||||
|
'rg16sint',
|
||||||
|
'rg16float',
|
||||||
|
'rgba8unorm',
|
||||||
|
'rgba8unorm-srgb',
|
||||||
|
'rgba8snorm',
|
||||||
|
'rgba8uint',
|
||||||
|
'rgba8sint',
|
||||||
|
'bgra8unorm',
|
||||||
|
'bgra8unorm-srgb',
|
||||||
|
'rgb9e5ufloat',
|
||||||
|
'rgb10a2uint',
|
||||||
|
'rgb10a2unorm',
|
||||||
|
'rg11b10ufloat',
|
||||||
|
'rg32uint',
|
||||||
|
'rg32sint',
|
||||||
|
'rg32float',
|
||||||
|
'rgba16uint',
|
||||||
|
'rgba16sint',
|
||||||
|
'rgba16float',
|
||||||
|
'rgba32uint',
|
||||||
|
'rgba32sint',
|
||||||
|
'rgba32float',
|
||||||
|
'stencil8',
|
||||||
|
'depth16unorm',
|
||||||
|
'depth24plus',
|
||||||
|
'depth24plus-stencil8',
|
||||||
|
'depth32float',
|
||||||
|
'depth32float-stencil8',
|
||||||
|
'bc1-rgba-unorm',
|
||||||
|
'bc1-rgba-unorm-srgb',
|
||||||
|
'bc2-rgba-unorm',
|
||||||
|
'bc2-rgba-unorm-srgb',
|
||||||
|
'bc3-rgba-unorm',
|
||||||
|
'bc3-rgba-unorm-srgb',
|
||||||
|
'bc4-r-unorm',
|
||||||
|
'bc4-r-snorm',
|
||||||
|
'bc5-rg-unorm',
|
||||||
|
'bc5-rg-snorm',
|
||||||
|
'bc6h-rgb-ufloat',
|
||||||
|
'bc6h-rgb-float',
|
||||||
|
'bc7-rgba-unorm',
|
||||||
|
'bc7-rgba-unorm-srgb',
|
||||||
|
'etc2-rgb8unorm',
|
||||||
|
'etc2-rgb8unorm-srgb',
|
||||||
|
'etc2-rgb8a1unorm',
|
||||||
|
'etc2-rgb8a1unorm-srgb',
|
||||||
|
'etc2-rgba8unorm',
|
||||||
|
'etc2-rgba8unorm-srgb',
|
||||||
|
'eac-r11unorm',
|
||||||
|
'eac-r11snorm',
|
||||||
|
'eac-rg11unorm',
|
||||||
|
'eac-rg11snorm',
|
||||||
|
'astc-4x4-unorm',
|
||||||
|
'astc-4x4-unorm-srgb',
|
||||||
|
'astc-5x4-unorm',
|
||||||
|
'astc-5x4-unorm-srgb',
|
||||||
|
'astc-5x5-unorm',
|
||||||
|
'astc-5x5-unorm-srgb',
|
||||||
|
'astc-6x5-unorm',
|
||||||
|
'astc-6x5-unorm-srgb',
|
||||||
|
'astc-6x6-unorm',
|
||||||
|
'astc-6x6-unorm-srgb',
|
||||||
|
'astc-8x5-unorm',
|
||||||
|
'astc-8x5-unorm-srgb',
|
||||||
|
'astc-8x6-unorm',
|
||||||
|
'astc-8x6-unorm-srgb',
|
||||||
|
'astc-8x8-unorm',
|
||||||
|
'astc-8x8-unorm-srgb',
|
||||||
|
'astc-10x5-unorm',
|
||||||
|
'astc-10x5-unorm-srgb',
|
||||||
|
'astc-10x6-unorm',
|
||||||
|
'astc-10x6-unorm-srgb',
|
||||||
|
'astc-10x8-unorm',
|
||||||
|
'astc-10x8-unorm-srgb',
|
||||||
|
'astc-10x10-unorm',
|
||||||
|
'astc-10x10-unorm-srgb',
|
||||||
|
'astc-12x10-unorm',
|
||||||
|
'astc-12x10-unorm-srgb',
|
||||||
|
'astc-12x12-unorm',
|
||||||
|
'astc-12x12-unorm-srgb'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const TextureSampleType = Enum(
|
||||||
|
'float',
|
||||||
|
'unfilterable-float',
|
||||||
|
'depth',
|
||||||
|
'sint',
|
||||||
|
'uint'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const TextureViewDimension = Enum(
|
||||||
|
'1d',
|
||||||
|
'2d',
|
||||||
|
'2d-array',
|
||||||
|
'cube',
|
||||||
|
'cube-array',
|
||||||
|
'3d'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const VertexFormat = Enum(
|
||||||
|
'uint8x2',
|
||||||
|
'uint8x4',
|
||||||
|
'sint8x2',
|
||||||
|
'sint8x4',
|
||||||
|
'unorm8x2',
|
||||||
|
'unorm8x4',
|
||||||
|
'snorm8x2',
|
||||||
|
'snorm8x4',
|
||||||
|
'uint16x2',
|
||||||
|
'uint16x4',
|
||||||
|
'sint16x2',
|
||||||
|
'sint16x4',
|
||||||
|
'unorm16x2',
|
||||||
|
'unorm16x4',
|
||||||
|
'snorm16x2',
|
||||||
|
'snorm16x4',
|
||||||
|
'float16x2',
|
||||||
|
'float16x4',
|
||||||
|
'float32',
|
||||||
|
'float32x2',
|
||||||
|
'float32x3',
|
||||||
|
'float32x4',
|
||||||
|
'uint32',
|
||||||
|
'uint32x2',
|
||||||
|
'uint32x3',
|
||||||
|
'uint32x4',
|
||||||
|
'sint32',
|
||||||
|
'sint32x2',
|
||||||
|
'sint32x3',
|
||||||
|
'sint32x4',
|
||||||
|
'unorm10-10-10-2'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const VertexStepMode = Enum(
|
||||||
|
'vertex',
|
||||||
|
'instance'
|
||||||
|
)
|
0
src/index.js
Normal file
0
src/index.js
Normal file
22
src/rendering/render-pipeline.js
Normal file
22
src/rendering/render-pipeline.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
export class RenderPipeline {
|
||||||
|
_handle
|
||||||
|
_label
|
||||||
|
|
||||||
|
get handle() {
|
||||||
|
return this._handle
|
||||||
|
}
|
||||||
|
|
||||||
|
get label() {
|
||||||
|
return this._label
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPURenderPipeline} pipeline
|
||||||
|
* @param {string} [label]
|
||||||
|
*/
|
||||||
|
constructor(pipeline, label) {
|
||||||
|
this._handle = pipeline
|
||||||
|
this._label = label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
39
src/resources/bind-group-layout.js
Normal file
39
src/resources/bind-group-layout.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { WebGPUObjectError } from '../utils/errors.js'
|
||||||
|
|
||||||
|
export class BindGroupLayout {
|
||||||
|
_device
|
||||||
|
_handle
|
||||||
|
|
||||||
|
get handle() {
|
||||||
|
return this._handle
|
||||||
|
}
|
||||||
|
|
||||||
|
get label() {
|
||||||
|
return this._handle.label
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUDevice} device
|
||||||
|
* @param {GPUBindGroupLayout} layout
|
||||||
|
*/
|
||||||
|
constructor(device, layout) {
|
||||||
|
this._device = device
|
||||||
|
this._handle = layout
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUDevice} device
|
||||||
|
* @param {GPUBindGroupLayoutDescriptor} descriptor
|
||||||
|
*/
|
||||||
|
static create(device, descriptor) {
|
||||||
|
try {
|
||||||
|
return new BindGroupLayout(
|
||||||
|
device,
|
||||||
|
device.createBindGroupLayout(descriptor)
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
throw WebGPUObjectError.from(err, BindGroupLayout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
39
src/resources/bind-group.js
Normal file
39
src/resources/bind-group.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { WebGPUObjectError } from '../utils/errors.js'
|
||||||
|
|
||||||
|
export class BindGroup {
|
||||||
|
_device
|
||||||
|
_handle
|
||||||
|
|
||||||
|
get handle() {
|
||||||
|
return this._handle
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUDevice} device
|
||||||
|
* @param {GPUBindGroup} bindGroup
|
||||||
|
*/
|
||||||
|
constructor(device, bindGroup) {
|
||||||
|
this._device = device
|
||||||
|
this._handle = bindGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUDevice} device
|
||||||
|
* @param {GPUBindGroupDescriptor} descriptor
|
||||||
|
*/
|
||||||
|
static create(device, descriptor) {
|
||||||
|
try {
|
||||||
|
return new BindGroup(
|
||||||
|
device,
|
||||||
|
device.createBindGroup(descriptor)
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
throw WebGPUObjectError.from(err, BindGroup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this._handle = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
217
src/resources/buffer.js
Normal file
217
src/resources/buffer.js
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
import { BufferError, WebGPUError } from '../utils/errors.js'
|
||||||
|
|
||||||
|
/** @import { TypedArray, TypedArrayConstructor } from '../utils.js' */
|
||||||
|
/** @import { BindGroupEntry, BindingResource, BufferBindingResource } from '../core/graphics-device.js' */
|
||||||
|
|
||||||
|
export class Buffer {
|
||||||
|
_device
|
||||||
|
_handle
|
||||||
|
|
||||||
|
_mapped = false
|
||||||
|
|
||||||
|
/** @type {GPUBuffer} */
|
||||||
|
_defaultStagingBuffer
|
||||||
|
|
||||||
|
get handle() {
|
||||||
|
return this._handle
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return this._handle.size
|
||||||
|
}
|
||||||
|
|
||||||
|
get usage() {
|
||||||
|
return this._handle.usage
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUDevice} device
|
||||||
|
* @param {GPUBuffer} texture
|
||||||
|
*/
|
||||||
|
constructor(device, texture) {
|
||||||
|
this._device = device
|
||||||
|
this._handle = texture
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUDevice} device
|
||||||
|
* @param {GPUBufferDescriptor} descriptor
|
||||||
|
*/
|
||||||
|
static create(device, descriptor) {
|
||||||
|
try {
|
||||||
|
return new Buffer(
|
||||||
|
device,
|
||||||
|
device.createBuffer(descriptor)
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
throw BufferError.from(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} [size]
|
||||||
|
*/
|
||||||
|
_createStagingOptions(size = this.size) {
|
||||||
|
return {
|
||||||
|
size,
|
||||||
|
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} [size]
|
||||||
|
*/
|
||||||
|
_getStagingBuffer(size) {
|
||||||
|
return this._device.createBuffer(this._createStagingOptions(size))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ArrayBufferView | ArrayBuffer} data
|
||||||
|
* @param {number} [offset=0]
|
||||||
|
* @param {number} [dataOffset=0]
|
||||||
|
*/
|
||||||
|
write(data, offset = 0, dataOffset = 0) {
|
||||||
|
if (!(this.usage & GPUBufferUsage.COPY_DST)) {
|
||||||
|
console.warn('Buffer usage does not include COPY_DST. Buffer.write may fail.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._device?.queue) {
|
||||||
|
throw WebGPUError.deviceUnavailable()
|
||||||
|
}
|
||||||
|
|
||||||
|
this._device.queue.writeBuffer(
|
||||||
|
this._handle,
|
||||||
|
offset,
|
||||||
|
data,
|
||||||
|
dataOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Exclude<TypedArray, BigInt64Array | BigUint64Array>} SmallTypedArray
|
||||||
|
* @typedef {Exclude<TypedArrayConstructor, BigInt64ArrayConstructor | BigUint64ArrayConstructor>} SmallTypedArrayConstructor
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SmallTypedArray | DataView | undefined} [out]
|
||||||
|
* @param {number} [byteOffset=0]
|
||||||
|
* @param {number} [byteSize]
|
||||||
|
*/
|
||||||
|
async read(out, byteOffset = 0, byteSize = -1) {
|
||||||
|
if (!this._device) {
|
||||||
|
throw WebGPUError.deviceUnavailable()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(this.usage & GPUBufferUsage.MAP_READ)) {
|
||||||
|
throw BufferError.invalidRead()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (byteOffset < 0) { throw new RangeError('Read byteOffset cannot be negative') }
|
||||||
|
if (byteSize < 0) { byteSize = this.size - byteOffset }
|
||||||
|
if (byteSize < 0) { throw new RangeError(`Invalid calculated byteSize (${byteSize})`) }
|
||||||
|
if (byteSize === 0) { return out ?? new ArrayBuffer(0) }
|
||||||
|
if (byteOffset + byteSize > this.size) { throw new RangeError(`Read range exceeds buffer size`) }
|
||||||
|
|
||||||
|
|
||||||
|
if (out != null) {
|
||||||
|
if (!ArrayBuffer.isView(out)) { throw new TypeError('"out" parameter must be a TypedArray or DataView.') }
|
||||||
|
|
||||||
|
if (out.byteLength < byteSize) { throw new RangeError(`Provided output buffer too small`) }
|
||||||
|
}
|
||||||
|
|
||||||
|
let result
|
||||||
|
let range
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.handle.mapAsync(GPUMapMode.READ, byteOffset, byteSize)
|
||||||
|
range = this.handle.getMappedRange(byteOffset, byteSize)
|
||||||
|
|
||||||
|
if (out != null) {
|
||||||
|
const SourceView = /** @type {SmallTypedArrayConstructor} */ (out.constructor)
|
||||||
|
const bytesPerElement = SourceView.BYTES_PER_ELEMENT
|
||||||
|
|
||||||
|
if (!bytesPerElement) {
|
||||||
|
if (out instanceof DataView) {
|
||||||
|
new Uint8Array(
|
||||||
|
out.buffer,
|
||||||
|
out.byteOffset,
|
||||||
|
byteSize
|
||||||
|
).set(new Uint8Array(range))
|
||||||
|
} else {
|
||||||
|
throw new TypeError('"out" is not a standard TypedArray or DataView')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (byteSize % bytesPerElement !== 0) {
|
||||||
|
throw new RangeError(`"byteSize" (${byteSize}) incompatible with "out" byte size (${bytesPerElement})`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = new SourceView(range)
|
||||||
|
const target = new SourceView(
|
||||||
|
out.buffer,
|
||||||
|
out.byteOffset,
|
||||||
|
byteSize / bytesPerElement
|
||||||
|
)
|
||||||
|
|
||||||
|
target.set(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = out
|
||||||
|
} else {
|
||||||
|
result = range.slice(0)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw BufferError.from(err)
|
||||||
|
} finally {
|
||||||
|
this.handle.unmap()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {BindGroupEntry} entry
|
||||||
|
*/
|
||||||
|
asBindingResource(entry) {
|
||||||
|
const offset = 'offset' in entry ? entry.offset : 0
|
||||||
|
const size = 'size' in entry ? entry.size : this.size - offset
|
||||||
|
|
||||||
|
if (offset + size > this.size) {
|
||||||
|
throw BufferError.outOfBounds(offset, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
buffer: this._handle,
|
||||||
|
offset,
|
||||||
|
size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this._handle?.destroy()
|
||||||
|
this._handle = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class UniformBuffer extends Buffer {
|
||||||
|
/**
|
||||||
|
* @param {GPUDevice} device
|
||||||
|
* @param {GPUBuffer} buffer
|
||||||
|
*/
|
||||||
|
constructor(device, buffer) {
|
||||||
|
super(device, buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUDevice} device
|
||||||
|
* @param {Omit<GPUBufferDescriptor, 'usage'>} descriptor
|
||||||
|
*/
|
||||||
|
static create(device, descriptor) {
|
||||||
|
const usage = GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
||||||
|
return super.create(device, {
|
||||||
|
usage,
|
||||||
|
...descriptor
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
48
src/resources/sampler.js
Normal file
48
src/resources/sampler.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { WebGPUObjectError } from '../utils/errors.js'
|
||||||
|
|
||||||
|
/** @import { BindGroupEntry } from '../core/graphics-device.js' */
|
||||||
|
|
||||||
|
export class Sampler {
|
||||||
|
_device
|
||||||
|
_handle
|
||||||
|
|
||||||
|
get handle() {
|
||||||
|
return this._handle
|
||||||
|
}
|
||||||
|
|
||||||
|
get label() {
|
||||||
|
return this._handle.label
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUDevice} device
|
||||||
|
* @param {GPUSampler} sampler
|
||||||
|
*/
|
||||||
|
constructor(device, sampler) {
|
||||||
|
this._device = device
|
||||||
|
this._handle = sampler
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUDevice} device
|
||||||
|
* @param {GPUSamplerDescriptor} descriptor
|
||||||
|
*/
|
||||||
|
static create(device, descriptor) {
|
||||||
|
try {
|
||||||
|
return new Sampler(
|
||||||
|
device,
|
||||||
|
device.createSampler(descriptor)
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
throw WebGPUObjectError.from(err, Sampler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {BindGroupEntry} _entry
|
||||||
|
*/
|
||||||
|
asBindingResource(_entry) {
|
||||||
|
return this._handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
35
src/resources/shader-module.js
Normal file
35
src/resources/shader-module.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { WebGPUObjectError } from '../utils/errors.js'
|
||||||
|
|
||||||
|
export class ShaderModule {
|
||||||
|
_handle
|
||||||
|
|
||||||
|
get label() {
|
||||||
|
return this._handle.label
|
||||||
|
}
|
||||||
|
|
||||||
|
get handle() {
|
||||||
|
return this._handle
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUShaderModule} module
|
||||||
|
*/
|
||||||
|
constructor(module) {
|
||||||
|
this._handle = module
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUDevice} device
|
||||||
|
* @param {GPUShaderModuleDescriptor} descriptor
|
||||||
|
*/
|
||||||
|
static create(device, descriptor) {
|
||||||
|
try {
|
||||||
|
return new ShaderModule(
|
||||||
|
device.createShaderModule(descriptor)
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
throw WebGPUObjectError.from(err, ShaderModule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
111
src/resources/texture.js
Normal file
111
src/resources/texture.js
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import { WebGPUObjectError } from '../utils/errors.js'
|
||||||
|
|
||||||
|
/** @import { BindGroupEntry } from '../core/graphics-device.js' */
|
||||||
|
|
||||||
|
export class Texture {
|
||||||
|
_device
|
||||||
|
_handle
|
||||||
|
|
||||||
|
/** @type {GPUTextureView | undefined} */
|
||||||
|
_defaultView
|
||||||
|
|
||||||
|
get handle() {
|
||||||
|
return this._handle
|
||||||
|
}
|
||||||
|
|
||||||
|
get width() {
|
||||||
|
return this._handle.width
|
||||||
|
}
|
||||||
|
|
||||||
|
get height() {
|
||||||
|
return this._handle.height
|
||||||
|
}
|
||||||
|
|
||||||
|
get format() {
|
||||||
|
return this._handle.format
|
||||||
|
}
|
||||||
|
|
||||||
|
get depthOrArrayLayers() {
|
||||||
|
return this._handle.depthOrArrayLayers
|
||||||
|
}
|
||||||
|
|
||||||
|
get usage() {
|
||||||
|
return this._handle.usage
|
||||||
|
}
|
||||||
|
|
||||||
|
get dimension() {
|
||||||
|
return this._handle.dimension
|
||||||
|
}
|
||||||
|
|
||||||
|
get mipLevelCount() {
|
||||||
|
return this._handle.mipLevelCount
|
||||||
|
}
|
||||||
|
|
||||||
|
get label() {
|
||||||
|
return this._handle.label
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUDevice} device
|
||||||
|
* @param {GPUTexture} texture
|
||||||
|
*/
|
||||||
|
constructor(device, texture) {
|
||||||
|
this._device = device
|
||||||
|
this._handle = texture
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUDevice} device
|
||||||
|
* @param {GPUTextureDescriptor} descriptor
|
||||||
|
*/
|
||||||
|
static create(device, descriptor) {
|
||||||
|
try {
|
||||||
|
return new Texture(
|
||||||
|
device,
|
||||||
|
device.createTexture(descriptor)
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
throw WebGPUObjectError.from(err, Texture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUTextureViewDescriptor} [descriptor]
|
||||||
|
* @throws {TextureError}
|
||||||
|
*/
|
||||||
|
createDefaultView(descriptor) {
|
||||||
|
if (!descriptor && this._defaultView) {
|
||||||
|
return this._defaultView
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const view = this._handle.createView(descriptor)
|
||||||
|
|
||||||
|
if (!descriptor) {
|
||||||
|
this._defaultView = view
|
||||||
|
}
|
||||||
|
|
||||||
|
return view
|
||||||
|
} catch (err) {
|
||||||
|
throw WebGPUObjectError.from(err, Texture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {BindGroupEntry} entry
|
||||||
|
*/
|
||||||
|
asBindingResource(entry) {
|
||||||
|
return 'textureView' in entry ? entry.textureView : this.getView()
|
||||||
|
}
|
||||||
|
|
||||||
|
getView() {
|
||||||
|
return this.createDefaultView()
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this._handle?.destroy()
|
||||||
|
this._handle = undefined
|
||||||
|
this._defaultView = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
171
src/utils.js
Normal file
171
src/utils.js
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @typedef {new (...args: any[]) => T} Newable<T>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {
|
||||||
|
Int8Array
|
||||||
|
| Uint8Array
|
||||||
|
| Uint8ClampedArray
|
||||||
|
| Int16Array
|
||||||
|
| Uint16Array
|
||||||
|
| Int32Array
|
||||||
|
| Uint32Array
|
||||||
|
| Float32Array
|
||||||
|
| Float64Array
|
||||||
|
| BigInt64Array
|
||||||
|
| BigUint64Array
|
||||||
|
} TypedArray
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {
|
||||||
|
Int8ArrayConstructor
|
||||||
|
| Uint8ArrayConstructor
|
||||||
|
| Uint8ClampedArrayConstructor
|
||||||
|
| Int16ArrayConstructor
|
||||||
|
| Uint16ArrayConstructor
|
||||||
|
| Int32ArrayConstructor
|
||||||
|
| Uint32ArrayConstructor
|
||||||
|
| Float32ArrayConstructor
|
||||||
|
| Float64ArrayConstructor
|
||||||
|
| BigInt64ArrayConstructor
|
||||||
|
| BigUint64ArrayConstructor
|
||||||
|
} TypedArrayConstructor
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {number} T
|
||||||
|
* @typedef {`${T}` extends `-${string}` | '0' | `${string}.${string}` ? never : T } PositiveInteger
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {ReadonlyArray<string>} T
|
||||||
|
* @typedef {T extends readonly [] ? [] : T extends readonly [infer Head extends string, ... infer Tail extends ReadonlyArray<string>] ? [Capitalize<Head>, ...CapitalizeAll<Tail>] : string[]} CapitalizeAll
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {ReadonlyArray<string>} T
|
||||||
|
* @template {string} [Sep='']
|
||||||
|
* @typedef {T extends readonly [] ? '' : T extends readonly [infer Head] ? Head : T extends readonly [infer Head extends string, ...infer Tail extends ReadonlyArray<string>] ? `${Head}${Sep}${Join<Tail, Sep>}` : string} Join
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {string} S
|
||||||
|
* @template {string} D
|
||||||
|
* @typedef {D extends '' ? [S] : S extends `${infer Head}${D}${infer Tail}` ? [Head, ...Split<Tail, D>] : [S]} Split
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @typedef {'-' | '_' | ' '} WordSeparator
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {string} S
|
||||||
|
* @template {string} D
|
||||||
|
* @typedef {S extends `${infer Left}${D}${infer Right}` ? `${PascalCaseFromDelimiter<Left, D>}${Capitalize<PascalCaseFromDelimiter<Right, D>>}` : S} PascalCaseFromDelimiter
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {string} S
|
||||||
|
* @typedef {Capitalize<PascalCaseFromDelimiter<PascalCaseFromDelimiter<PascalCaseFromDelimiter<Lowercase<S>, '-'>, '_'>, ' '>>} PascalCaseString
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {string | ReadonlyArray<string>} T
|
||||||
|
* @typedef {T extends String ? PascalCaseString<T> : T extends ReadonlyArray<string> ? Join<CapitalizeAll<T>, ''> : T} PascalCase
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {string} T
|
||||||
|
* @param {T} s
|
||||||
|
* @returns {Uppercase<T>}
|
||||||
|
*/
|
||||||
|
export const uppercase = s => /** @type {Uppercase<T>} */(s.toUpperCase())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {string} T
|
||||||
|
* @param {T} s
|
||||||
|
* @returns {Lowercase<T>}
|
||||||
|
*/
|
||||||
|
export const lowercase = s => /** @type {Lowercase<T>} */(s.toLowerCase())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {string} const T
|
||||||
|
* @param {T} s
|
||||||
|
* @returns {Split<T, '-'>}
|
||||||
|
*/
|
||||||
|
export const fromKebab = s => /** @type {Split<T, '-'>} */(s.split('-'))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {string} const T
|
||||||
|
* @param {T} xs
|
||||||
|
* @returns {PascalCase<T>}
|
||||||
|
*/
|
||||||
|
export const toPascal = xs => /** @type {PascalCase<T>} */(uppercase(xs[0]) + xs.slice(1))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {string[]} const T
|
||||||
|
* @param {T} xs
|
||||||
|
* @returns {PascalCase<T>}
|
||||||
|
*/
|
||||||
|
const pascal = xs => /** @type {PascalCase<T>} */(xs.map(toPascal).join(''))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template {string} const T
|
||||||
|
* @param {...T} values
|
||||||
|
* @returns {Readonly<{ [K in T as PascalCase<K>]: K }>}
|
||||||
|
*/
|
||||||
|
export const Enum = (...values) => /** @type {Readonly<{ [K in T as PascalCase<K>]: K }>} */(Object.freeze(
|
||||||
|
values.reduce((acc, x) => {
|
||||||
|
const key = pascal(fromKebab(x)).toString()
|
||||||
|
acc[key] = x
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
/** @typedef {(...args: any) => void} Listener */
|
||||||
|
export class EventEmitter {
|
||||||
|
_listeners = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {PropertyKey} event
|
||||||
|
* @param {Listener} callback
|
||||||
|
*/
|
||||||
|
on(event, callback) {
|
||||||
|
this._listeners[event] = this._listeners[event] || []
|
||||||
|
this._listeners[event].push(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {PropertyKey} event
|
||||||
|
* @param {...any} args
|
||||||
|
*/
|
||||||
|
emit(event, ...args) {
|
||||||
|
const listeners = this._listeners[event]
|
||||||
|
|
||||||
|
if (listeners) {
|
||||||
|
listeners.forEach(
|
||||||
|
/** @param {Listener} cb */
|
||||||
|
cb => cb(...args)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {PropertyKey} event
|
||||||
|
* @param {Listener} callback
|
||||||
|
*/
|
||||||
|
off(event, callback) {
|
||||||
|
const listeners = this._listeners[event]
|
||||||
|
|
||||||
|
if (listeners) {
|
||||||
|
this._listeners[event] = listeners.filter(
|
||||||
|
/** @param {Listener} cb */
|
||||||
|
cb => cb !== callback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
80
src/utils/errors.js
Normal file
80
src/utils/errors.js
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/** @import { Newable } from '../utils.js' */
|
||||||
|
|
||||||
|
export class WebGPUError extends Error {
|
||||||
|
/**
|
||||||
|
* @param {string} message
|
||||||
|
* @param {ErrorOptions} [options]
|
||||||
|
*/
|
||||||
|
constructor(message, options) {
|
||||||
|
super(`WebGPUError: ${message}`, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
static unsupported() {
|
||||||
|
return new WebGPUError('WebGPU is not supported on this browser')
|
||||||
|
}
|
||||||
|
|
||||||
|
static contextFailure() {
|
||||||
|
return new WebGPUError('Failed to request WebGPU context')
|
||||||
|
}
|
||||||
|
|
||||||
|
static adapterUnavailable() {
|
||||||
|
return new WebGPUError('Failed to request GPU adapter')
|
||||||
|
}
|
||||||
|
|
||||||
|
static deviceUnavailable() {
|
||||||
|
return new WebGPUError('Failed to request GPU device')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GraphicsDeviceError extends Error {
|
||||||
|
static uninitialized() {
|
||||||
|
return new GraphicsDeviceError('GraphicsDeviceError: GraphicsDevice not initialized')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CommandRecorderError extends Error {
|
||||||
|
static activeRenderPass() {
|
||||||
|
return new CommandRecorderError('CommandRecorderError: can\'t begin new render pass while another is active. call CommandRecorder.end()')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WebGPUObjectError extends Error {
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {Error} cause
|
||||||
|
* @param {string | Newable<T>} [type]
|
||||||
|
*/
|
||||||
|
static from(cause, type) {
|
||||||
|
const name = typeof type === 'string' ? type : type.name
|
||||||
|
return new WebGPUObjectError(`could not create ${name}`, { cause })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BufferError extends WebGPUObjectError {
|
||||||
|
/**
|
||||||
|
* @param {string} message
|
||||||
|
* @param {ErrorOptions} [options]
|
||||||
|
*/
|
||||||
|
constructor(message, options) {
|
||||||
|
super(`BufferError: ${message}`, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Error} cause
|
||||||
|
*/
|
||||||
|
static from(cause) {
|
||||||
|
return new BufferError('could not create buffer', { cause })
|
||||||
|
}
|
||||||
|
|
||||||
|
static invalidRead() {
|
||||||
|
return new BufferError('cannot read a buffer without MAP_READ usage')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} offset
|
||||||
|
* @param {number} size
|
||||||
|
*/
|
||||||
|
static outOfBounds(offset, size) {
|
||||||
|
return new BufferError(`buffer offset/size (${offset}/${size}) exceeds buffer dimensions`)
|
||||||
|
}
|
||||||
|
}
|
27
src/utils/events.js
Normal file
27
src/utils/events.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { GraphicsDevice } from '../core/graphics-device.js'
|
||||||
|
|
||||||
|
export class GraphicsDeviceInitialized {
|
||||||
|
static EventName = 'graphics-device:initialized'
|
||||||
|
graphicsDevice
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GraphicsDevice} graphicsDevice
|
||||||
|
*/
|
||||||
|
constructor(graphicsDevice) {
|
||||||
|
this.graphicsDevice = graphicsDevice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GraphicsDeviceLost {
|
||||||
|
static EventName = 'graphics-device:device-lost'
|
||||||
|
info
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {GPUDeviceLostInfo} info
|
||||||
|
*/
|
||||||
|
constructor(info) {
|
||||||
|
this.info = info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue