move to ts

This commit is contained in:
Rowan 2025-04-21 23:36:59 -05:00
parent d75f3b4c49
commit 95b25c962a
33 changed files with 1020 additions and 1103 deletions

285
dist/index.js vendored
View file

@ -1,12 +1,8 @@
(() => { (() => {
// src/utils/errors.js // src/utils/errors.ts
var WebGPUError = class _WebGPUError extends Error { var WebGPUError = class _WebGPUError extends Error {
/** constructor(message, options2) {
* @param {string} message super(`WebGPUError: ${message}`, options2);
* @param {ErrorOptions} [options]
*/
constructor(message, options) {
super(`WebGPUError: ${message}`, options);
} }
static unsupported() { static unsupported() {
return new _WebGPUError("WebGPU is not supported on this browser"); return new _WebGPUError("WebGPU is not supported on this browser");
@ -32,58 +28,32 @@
} }
}; };
var WebGPUObjectError = class _WebGPUObjectError extends Error { var WebGPUObjectError = class _WebGPUObjectError extends Error {
/**
* @template T
* @param {Error} cause
* @param {string | Newable<T>} [type]
*/
static from(cause, type) { static from(cause, type) {
const name = typeof type === "string" ? type : type.name; const name = typeof type === "string" ? type : type.name;
return new _WebGPUObjectError(`could not create ${name}`, { cause }); return new _WebGPUObjectError(`could not create ${name}`, { cause });
} }
}; };
var BufferError = class _BufferError extends WebGPUObjectError { var BufferError = class _BufferError extends WebGPUObjectError {
/** constructor(message, options2) {
* @param {string} message super(`BufferError: ${message}`, options2);
* @param {ErrorOptions} [options]
*/
constructor(message, options) {
super(`BufferError: ${message}`, options);
} }
/**
* @param {Error} cause
*/
static from(cause) { static from(cause) {
return new _BufferError("could not create buffer", { cause }); return new _BufferError("could not create buffer", { cause });
} }
static invalidRead() { static invalidRead() {
return new _BufferError("cannot read a buffer without MAP_READ usage"); return new _BufferError("cannot read a buffer without MAP_READ usage");
} }
/**
* @param {number} offset
* @param {number} size
*/
static outOfBounds(offset, size) { static outOfBounds(offset, size) {
return new _BufferError(`buffer offset/size (${offset}/${size}) exceeds buffer dimensions`); return new _BufferError(`buffer offset/size (${offset}/${size}) exceeds buffer dimensions`);
} }
}; };
var MaterialError = class extends WebGPUObjectError { var MaterialError = class extends WebGPUObjectError {
/** constructor(message, options2) {
* @param {string} message super(`MaterialError: ${message}`, options2);
* @param {ErrorOptions} [options]
*/
constructor(message, options) {
super(`MaterialError: ${message}`, options);
} }
/**
* @param {Error} cause
*/
static from(cause) { static from(cause) {
return new BufferError("could not create material", { cause }); return new BufferError("could not create material", { cause });
} }
/**
* @param {string} shaderType
*/
static missingShader(shaderType) { static missingShader(shaderType) {
return new BufferError(`missing ${shaderType} shader`); return new BufferError(`missing ${shaderType} shader`);
} }
@ -166,82 +136,53 @@
} }
}; };
// src/utils.js // src/utils/index.ts
var uppercase = (s2) => ( var uppercase = (s2) => s2.toUpperCase();
/** @type {Uppercase<T>} */ var split = (delim, s2) => s2.split(delim);
s2.toUpperCase() var fromKebab = (s2) => split("-", s2);
var toPascal = (xs) => uppercase(xs[0]) + xs.slice(1);
var pascal = (xs) => xs.map(toPascal).join("");
var Enum = (...values) => Object.freeze(
values.reduce((acc, x2) => {
const key = pascal(fromKebab(x2)).toString();
acc[key] = x2;
return acc;
}, {})
); );
var fromKebab = (s2) => ( var FlagEnum = (...values) => Object.freeze(
/** @type {Split<T, '-'>} */ values.reduce((acc, x2, i2) => {
s2.split("-") const key = pascal(fromKebab(x2)).toString();
); acc[key] = 1 << i2;
var toPascal = (xs) => ( return acc;
/** @type {PascalCase<T>} */ }, {})
uppercase(xs[0]) + xs.slice(1)
);
var pascal = (xs) => (
/** @type {PascalCase<T>} */
xs.map(toPascal).join("")
);
var Enum = (...values) => (
/** @type {Readonly<{ [K in T as PascalCase<K>]: K }>} */
Object.freeze(
values.reduce((acc, x2) => {
const key = pascal(fromKebab(x2)).toString();
acc[key] = x2;
return acc;
}, {})
)
);
var FlagEnum = (...values) => (
/** @type {never} */
Object.freeze(
values.reduce((acc, x2, i2) => {
const key = pascal(fromKebab(x2)).toString();
acc[key] = 1 << i2;
return acc;
}, {})
)
); );
var EventEmitter = class { var EventEmitter = class {
_listeners = {}; constructor() {
/** this._listeners = {};
* @param {PropertyKey} event }
* @param {Listener} callback
*/
on(event, callback) { on(event, callback) {
this._listeners[event] = this._listeners[event] || []; this._listeners[event] = this._listeners[event] || [];
this._listeners[event].push(callback); this._listeners[event].push(callback);
} }
/**
* @param {PropertyKey} event
* @param {...any} args
*/
emit(event, ...args) { emit(event, ...args) {
const listeners = this._listeners[event]; const listeners = this._listeners[event];
if (listeners) { if (listeners) {
listeners.forEach( listeners.forEach(
/** @param {Listener} cb */
(cb) => cb(...args) (cb) => cb(...args)
); );
} }
} }
/**
* @param {PropertyKey} event
* @param {Listener} callback
*/
off(event, callback) { off(event, callback) {
const listeners = this._listeners[event]; const listeners = this._listeners[event];
if (listeners) { if (listeners) {
this._listeners[event] = listeners.filter( this._listeners[event] = listeners.filter(
/** @param {Listener} cb */
(cb) => cb !== callback (cb) => cb !== callback
); );
} }
} }
}; };
// src/utils/internal-enums.js // src/utils/internal-enums.ts
var ShaderStage = FlagEnum( var ShaderStage = FlagEnum(
"vertex", "vertex",
"fragment", "fragment",
@ -450,23 +391,19 @@
} }
}; };
// src/utils/events.js // src/utils/events.ts
var GraphicsDeviceInitialized = class { var GraphicsDeviceInitialized = class {
static EventName = "graphics-device:initialized"; static {
graphicsDevice; this.EventName = "graphics-device:initialized";
/** }
* @param {GraphicsDevice} graphicsDevice
*/
constructor(graphicsDevice) { constructor(graphicsDevice) {
this.graphicsDevice = graphicsDevice; this.graphicsDevice = graphicsDevice;
} }
}; };
var GraphicsDeviceLost = class { var GraphicsDeviceLost = class {
static EventName = "graphics-device:device-lost"; static {
info; this.EventName = "graphics-device:device-lost";
/** }
* @param {GPUDeviceLostInfo} info
*/
constructor(info) { constructor(info) {
this.info = info; this.info = info;
} }
@ -5752,7 +5689,7 @@
} }
}; };
// src/utils/bindings.js // src/utils/bindings.ts
var BindingMap = class extends Map { var BindingMap = class extends Map {
}; };
var GroupBindingMap = class extends Map { var GroupBindingMap = class extends Map {
@ -6079,7 +6016,7 @@
"instance" "instance"
); );
// src/utils/wgsl-to-wgpu.js // src/utils/wgsl-to-wgpu.ts
var parseTextureType = (typeName) => { var parseTextureType = (typeName) => {
const chevronIndex = typeName.indexOf("<"); const chevronIndex = typeName.indexOf("<");
const type = typeName.slice(0, chevronIndex); const type = typeName.slice(0, chevronIndex);
@ -6227,6 +6164,20 @@
return "2d"; return "2d";
} }
}; };
var textureToImageDimension = (dimension) => {
switch (dimension) {
case "1d":
return "1d";
case "2d":
case "2d-array":
case "cube-array":
return "2d";
case "3d":
return "3d";
default:
return "2d";
}
};
var wgslToWgpuFormat = (format) => { var wgslToWgpuFormat = (format) => {
switch (format) { switch (format) {
case "f32": case "f32":
@ -6799,8 +6750,31 @@
} }
}; };
// src/utils/bitflags.ts
var BitFlags = class _BitFlags {
get flags() {
return this._value;
}
constructor(value) {
this._value = value;
}
static has(a2, b2) {
return (a2 & b2) === b2;
}
static add(a2, b2) {
return a2 | b2;
}
has(b2) {
return _BitFlags.has(this._value, b2);
}
add(b2) {
return _BitFlags.add(this._value, b2);
}
};
// src/resources/texture.js // src/resources/texture.js
var Texture = class _Texture { var Texture = class _Texture {
static _defaultUsage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT;
_device; _device;
_handle; _handle;
/** @type {GPUTextureView | undefined} */ /** @type {GPUTextureView | undefined} */
@ -6857,6 +6831,95 @@
throw WebGPUObjectError.from(err, _Texture); throw WebGPUObjectError.from(err, _Texture);
} }
} }
static _generateMipLevels(size) {
const max = Math.max.apply(void 0, size);
return 1 + Math.log2(max) | 0;
}
/**
* @param {GPUDevice} device
* @param {string | URL} url
* @param {GPUTextureDescriptor} desciptor
*/
static async fromUrl(device, url, descriptor) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch remote resource: ${response.statusText}`);
}
const usage = options.usage || _Texture._defaultUsage;
const dimension = descriptor.dimension ? textureToImageDimension(descriptor.dimension) : "2d";
const blob = await response.blob();
const bitmap = await createImageBitmap(blob);
const size = [bitmap.width, bitmap.height];
const desc = {
usage,
dimension,
size,
format: descriptor.format || "rgba8unorm",
mipLevelCount: descriptor.mipLevelCount || _Texture._generateMipCount(...size),
...descriptor
};
const texture = _Texture.create(device, desc);
texture.upload(bitmap);
return texture;
} catch (err) {
throw WebGPUObjectError.from(err, _Texture);
}
}
/**
* @param {GPUDevice} device
* @param {GPUExtent3DStrict} size
* @param {GPUTextureFormat} format
* @param {GPUTextureDescriptor} descriptor
*/
static createRenderTarget(device, size, format, descriptor) {
const usage = descriptor.usage || _Texture._defaultUsage;
return _Texture.create(device, {
size,
format,
usage,
...descriptor
});
}
/**
* @typedef UploadTextureInfo
* @property {GPUTexelCopyTextureInfo} destination
* @property {GPUTexelCopyBufferLayout} dataLayout
* @property {GPUExtent3DStrict} size
*/
/**
* @param {GPUAllowSharedBufferSource} source
* @param {UploadTextureInfo} [options={}]
*/
upload(source, options2 = {}) {
const mipLevel = options2.destination.mipLevel || 0;
const size = options2.size || [
Math.max(1, this.width >> mipLevel),
Math.max(1, this.height >> mipLevel),
this.depthOrArrayLayers
];
try {
this._device.queue.writeTexture(
{ ...options2.destination, texture: this._handle },
source,
options2.dataLayout,
size
);
} catch (err) {
throw WebGPUObjectError.from(err, _Texture);
}
}
/**
* @param {GPUCommandEncoder} commandEncoder
*/
generateMipmaps(commandEncoder) {
const requiredUsage = GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT;
if (!BitFlags.has(this.usage & requiredUsage)) {
throw new Error("Texture does not have the required usage flags for mipmap generation");
}
for (let i2 = 1; i2 < this.mipLevelCount; ++i2) {
}
}
/** /**
* @param {GPUTextureViewDescriptor} [descriptor] * @param {GPUTextureViewDescriptor} [descriptor]
* @throws {TextureError} * @throws {TextureError}
@ -6879,7 +6942,7 @@
return this.createDefaultView(); return this.createDefaultView();
} }
toBindingResource() { toBindingResource() {
return this._handle; return this.getView();
} }
destroy() { destroy() {
this._handle?.destroy(); this._handle?.destroy();
@ -7067,21 +7130,21 @@
/** /**
* @param {GPURequestAdapterOptions} [options] * @param {GPURequestAdapterOptions} [options]
*/ */
withAdapter(options) { withAdapter(options2) {
if (!this.isSupported()) { if (!this.isSupported()) {
throw WebGPUError.unsupported(); throw WebGPUError.unsupported();
} }
this._adapter_options = options; this._adapter_options = options2;
return this; return this;
} }
/** /**
* @param {GPUDeviceDescriptor} [options] * @param {GPUDeviceDescriptor} [options]
*/ */
withDevice(options) { withDevice(options2) {
if (!this.isSupported()) { if (!this.isSupported()) {
throw WebGPUError.unsupported(); throw WebGPUError.unsupported();
} }
this._device_descriptor = options; this._device_descriptor = options2;
return this; return this;
} }
async build() { async build() {
@ -7389,23 +7452,23 @@
* @param {boolean} [options.generateMipmaps=false] * @param {boolean} [options.generateMipmaps=false]
* @param {boolean} [options.flipY=false] * @param {boolean} [options.flipY=false]
*/ */
createTextureFromBitmap(bitmap, options) { createTextureFromBitmap(bitmap, options2) {
if (!this._isInitialized) { if (!this._isInitialized) {
throw GraphicsDeviceError.uninitialized(); throw GraphicsDeviceError.uninitialized();
} }
if (!bitmap) { if (!bitmap) {
throw new TypeError("Provided bitmap is null."); 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 usage = (options2.usage ?? GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT) | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST;
const mipLevelCount = 1; const mipLevelCount = 1;
const descriptor = { const descriptor = {
label: options.label, label: options2.label,
size: { size: {
width: bitmap.width, width: bitmap.width,
height: bitmap.height, height: bitmap.height,
depthOrArrayLayers: 1 depthOrArrayLayers: 1
}, },
format: options.format ?? "rgba8unorm", format: options2.format ?? "rgba8unorm",
usage, usage,
dimension: "2d", dimension: "2d",
mipLevelCount, mipLevelCount,
@ -7414,7 +7477,7 @@
try { try {
const texture = this.device.createTexture(descriptor); const texture = this.device.createTexture(descriptor);
this.queue.copyExternalImageToTexture( this.queue.copyExternalImageToTexture(
{ source: bitmap, flipY: options.flipY ?? false }, { source: bitmap, flipY: options2.flipY ?? false },
{ texture, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } }, { texture, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
descriptor.size descriptor.size
); );

17
package-lock.json generated
View file

@ -13,7 +13,8 @@
}, },
"devDependencies": { "devDependencies": {
"@webgpu/types": "^0.1.60", "@webgpu/types": "^0.1.60",
"esbuild": "^0.25.2" "esbuild": "^0.25.2",
"typescript": "^5.8.3"
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
@ -489,6 +490,20 @@
"@esbuild/win32-x64": "0.25.2" "@esbuild/win32-x64": "0.25.2"
} }
}, },
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"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

@ -13,7 +13,8 @@
"description": "", "description": "",
"devDependencies": { "devDependencies": {
"@webgpu/types": "^0.1.60", "@webgpu/types": "^0.1.60",
"esbuild": "^0.25.2" "esbuild": "^0.25.2",
"typescript": "^5.8.3"
}, },
"dependencies": { "dependencies": {
"wgsl_reflect": "^1.2.0" "wgsl_reflect": "^1.2.0"

View file

@ -5,32 +5,26 @@ import { SwapChain } from './swap-chain.js'
export class CommandRecorder { export class CommandRecorder {
static _defaultClearValue = { r: 0, g: 0, b: 0, a: 1 } static _defaultClearValue = { r: 0, g: 0, b: 0, a: 1 }
_device _device: GPUDevice
_swapChain _swapChain: SwapChain
_label
_encoder _encoder: GPUCommandEncoder
/** @type {GPURenderPassEncoder | undefined} */ _passEncoder?: GPURenderPassEncoder
_passEncoder
/** get label() {
* @param {GPUDevice} device return this._encoder.label
* @param {SwapChain} swapChain }
* @param {string} [label]
*/
constructor(device, swapChain, label) { constructor(device: GPUDevice, swapChain: SwapChain, label: string) {
this._device = device this._device = device
this._swapChain = swapChain this._swapChain = swapChain
this._label = label
this._encoder = device.createCommandEncoder({ label }) this._encoder = device.createCommandEncoder({ label })
} }
/** _defaultColorAttachment(): [GPURenderPassColorAttachment] {
* @returns {[GPURenderPassColorAttachment]}
*/
_defaultColorAttachment() {
const view = this._swapChain.getCurrentTextureView() const view = this._swapChain.getCurrentTextureView()
return [{ return [{
@ -40,12 +34,8 @@ export class CommandRecorder {
storeOp: StoreOp.Store storeOp: StoreOp.Store
}] }]
} }
/**
* @param {GPURenderPassColorAttachment[]} [colorAttachments] beginRenderPass(colorAttachments: GPURenderPassColorAttachment[], depthStencilAttachment: GPURenderPassDepthStencilAttachment): GPURenderPassEncoder {
* @param {GPURenderPassDepthStencilAttachment} [depthStencilAttachment]
* @returns {GPURenderPassEncoder}
*/
beginRenderPass(colorAttachments, depthStencilAttachment) {
if (this._passEncoder) { if (this._passEncoder) {
throw CommandRecorderError.activeRenderPass() throw CommandRecorderError.activeRenderPass()
} }
@ -53,7 +43,7 @@ export class CommandRecorder {
const attachments = colorAttachments || this._defaultColorAttachment() const attachments = colorAttachments || this._defaultColorAttachment()
const descriptor = { const descriptor = {
label: this._label || 'RenderPass', label: this.label || 'RenderPass',
colorAttachments: attachments, colorAttachments: attachments,
depthStencilAttachment depthStencilAttachment
} }

View file

@ -2,57 +2,54 @@ import { SwapChain } from './swap-chain.js'
import { Buffer, UniformBuffer } from '../resources/buffer.js' import { Buffer, UniformBuffer } from '../resources/buffer.js'
import { GraphicsDeviceError, WebGPUError, BufferError, WebGPUObjectError } from '../utils/errors.js' import { GraphicsDeviceError, WebGPUError, BufferError, WebGPUObjectError } from '../utils/errors.js'
import { GraphicsDeviceInitialized, GraphicsDeviceLost } from '../utils/events.js' import { GraphicsDeviceInitialized, GraphicsDeviceLost } from '../utils/events.js'
import { EventEmitter } from '../utils.js' import { EventEmitter } from '../utils/index.js'
import { ShaderModule } from '../resources/shader-module.js' import { ShaderModule, ShaderPairStateDescriptor } from '../resources/shader-module.js'
import { RenderPipeline } from '../rendering/render-pipeline.js' import { RenderPipeline } from '../rendering/render-pipeline.js'
import { CommandRecorder } from './command-recorder.js' import { CommandRecorder } from './command-recorder.js'
import { BindGroupLayout } from '../resources/bind-group-layout.js' import { BindGroupLayout } from '../resources/bind-group-layout.js'
import { BindGroup } from '../resources/bind-group.js' import { BindGroup } from '../resources/bind-group.js'
import { Texture } from '../resources/texture.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' import { Sampler } from '../resources/sampler.js'
import { ResourceType } from '../utils/internal-enums.js' import { ResourceType } from '../utils/internal-enums.js'
import { Material } from '../resources/material.js' import { Material } from '../resources/material.js'
type BindingResource = Buffer | Texture | Sampler
interface TextureBindingResource {
textureView?: GPUTextureView
}
interface BufferBindingResource {
offset?: number
size?: number
}
interface BaseBindGroupEntry {
binding: GPUIndex32
resource: BindingResource
}
export type BindGroupEntry = (BufferBindingResource | TextureBindingResource) & BaseBindGroupEntry
interface BitmapTextureOptions {
label?: string
format: GPUTextureFormat
usage: GPUTextureUsageFlags
generateMipmaps: boolean
flipY: boolean
}
class GraphicsDeviceBuilder { class GraphicsDeviceBuilder {
_canvas _canvas: HTMLCanvasElement
_adapterOptions: GPURequestAdapterOptions
_deviceDescriptor: GPUDeviceDescriptor
get canvas() { get canvas() {
return this._canvas return this._canvas
} }
/** @type {GPURequestAdapterOptions} */
_adapter_options
/** @type {GPUDeviceDescriptor} */ constructor(canvasElement: HTMLCanvasElement) {
_device_descriptor
/**
* @param {HTMLCanvasElement} [canvasElement]
*/
constructor(canvasElement) {
this._canvas = canvasElement this._canvas = canvasElement
} }
@ -60,35 +57,26 @@ class GraphicsDeviceBuilder {
return navigator.gpu return navigator.gpu
} }
/** withCanvas(canvasElement: HTMLCanvasElement) {
* @param {HTMLCanvasElement} canvasElement
*/
withCanvas(canvasElement) {
this._canvas = canvasElement this._canvas = canvasElement
return this return this
} }
/** withAdapter(options: GPURequestAdapterOptions) {
* @param {GPURequestAdapterOptions} [options]
*/
withAdapter(options) {
if (!this.isSupported()) { if (!this.isSupported()) {
throw WebGPUError.unsupported() throw WebGPUError.unsupported()
} }
this._adapter_options = options this._adapterOptions = options
return this return this
} }
/** withDevice(options: GPUDeviceDescriptor) {
* @param {GPUDeviceDescriptor} [options]
*/
withDevice(options) {
if (!this.isSupported()) { if (!this.isSupported()) {
throw WebGPUError.unsupported() throw WebGPUError.unsupported()
} }
this._device_descriptor = options this._deviceDescriptor = options
return this return this
} }
@ -96,39 +84,28 @@ class GraphicsDeviceBuilder {
return new GraphicsDevice( return new GraphicsDevice(
this._canvas, this._canvas,
new DeviceHandler( new DeviceHandler(
this._adapter_options, this._adapterOptions,
this._device_descriptor this._deviceDescriptor
) )
) )
} }
} }
class DeviceHandler { class DeviceHandler {
/** @type {GPURequestAdapterOptions} */ _adapterOptions: GPURequestAdapterOptions
_adapterOptions _adapter: GPUAdapter
_deviceDescriptor: GPUDeviceDescriptor
/** @type {GPUAdapter} */ _device: GPUDevice
_adapter
get adapter() { get adapter() {
return this._adapter return this._adapter
} }
/** @type {GPUDeviceDescriptor} */
_deviceDescriptor
/** @type {GPUDevice} */
_device
get device() { get device() {
return this._device return this._device
} }
/** constructor(adapterOptions: GPURequestAdapterOptions, deviceDescriptor: GPUDeviceDescriptor) {
* @param {GPURequestAdapterOptions} adapterOptions
* @param {GPUDeviceDescriptor} deviceDescriptor
*/
constructor(adapterOptions, deviceDescriptor) {
this._adapterOptions = adapterOptions this._adapterOptions = adapterOptions
this._deviceDescriptor = deviceDescriptor this._deviceDescriptor = deviceDescriptor
} }
@ -149,14 +126,10 @@ class DeviceHandler {
} }
export class GraphicsDevice extends EventEmitter { export class GraphicsDevice extends EventEmitter {
_canvas _canvas: HTMLCanvasElement
_deviceHandler _deviceHandler: DeviceHandler
_swapChain: SwapChain
/** @type {SwapChain} */ _queue: GPUQueue
_swapChain
/** @type {GPUQueue} */
_queue
_isInitialized = false _isInitialized = false
@ -180,22 +153,14 @@ export class GraphicsDevice extends EventEmitter {
return this._swapChain return this._swapChain
} }
constructor(canvas: HTMLCanvasElement, deviceHandler: DeviceHandler) {
/**
* @param {HTMLCanvasElement} canvas
* @param {DeviceHandler} deviceHandler
*/
constructor(canvas, deviceHandler) {
super() super()
this._canvas = canvas this._canvas = canvas
this._deviceHandler = deviceHandler this._deviceHandler = deviceHandler
} }
/** static build(canvas: HTMLCanvasElement) {
* @param {HTMLCanvasElement} [canvas]
*/
static build(canvas) {
return new GraphicsDeviceBuilder(canvas) return new GraphicsDeviceBuilder(canvas)
} }
@ -204,7 +169,7 @@ export class GraphicsDevice extends EventEmitter {
this._swapChain = new SwapChain( this._swapChain = new SwapChain(
this._canvas, this._canvas,
this._deviceHandler.device this._deviceHandler.device,
) )
this._swapChain.configure() this._swapChain.configure()
@ -230,19 +195,7 @@ export class GraphicsDevice extends EventEmitter {
return true return true
} }
/** createBuffer(descriptor: GPUBufferDescriptor, data: ArrayBufferView | ArrayBuffer): Buffer {
* @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() } if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
try { try {
@ -258,12 +211,7 @@ export class GraphicsDevice extends EventEmitter {
} }
} }
/** createUniformBuffer(size: number, data: ArrayBufferView | ArrayBuffer, label: string) {
* @param {number} size
* @param {ArrayBufferView | ArrayBuffer} [data]
* @param {string} [label]
*/
createUniformBuffer(size, data, label) {
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
const buffer = UniformBuffer.create(this.device, { const buffer = UniformBuffer.create(this.device, {
@ -278,13 +226,7 @@ export class GraphicsDevice extends EventEmitter {
return buffer return buffer
} }
/** createShaderModule(code: string, label: string): ShaderModule {
* Creates a shader module from WGSL code.
* @param {string} code
* @param {string} [label]
* @returns {ShaderModule}
*/
createShaderModule(code, label) {
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
try { try {
@ -294,12 +236,7 @@ export class GraphicsDevice extends EventEmitter {
} }
} }
/** createRenderPipeline(descriptor: GPURenderPipelineDescriptor): RenderPipeline {
* Creates a render pipeline.
* @param {GPURenderPipelineDescriptor} descriptor - Raw render pipeline descriptor.
* @returns {RenderPipeline}
*/
createRenderPipeline(descriptor) {
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
try { try {
@ -310,10 +247,7 @@ export class GraphicsDevice extends EventEmitter {
} }
} }
/** createMaterial(shaders: ShaderPairStateDescriptor) {
* @param {import('../resources/material.js').ShaderPairDescriptor} shaders
*/
createMaterial(shaders) {
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
try { try {
@ -323,22 +257,13 @@ export class GraphicsDevice extends EventEmitter {
} }
} }
/** createCommandRecorder(label?: string): CommandRecorder {
* Creates a CommandRecorder to begin recording GPU commands.
* @param {string} [label]
* @returns {CommandRecorder}
*/
createCommandRecorder(label) {
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
return new CommandRecorder(this.device, this._swapChain, label) return new CommandRecorder(this.device, this._swapChain, label)
} }
/** createBindGroupLayout(entries: GPUBindGroupLayoutEntry[], label: string) {
* @param {GPUBindGroupLayoutEntry[]} entries
* @param {string} [label]
*/
createBindGroupLayout(entries, label) {
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
return BindGroupLayout.create(this.device, { return BindGroupLayout.create(this.device, {
@ -347,11 +272,7 @@ export class GraphicsDevice extends EventEmitter {
}) })
} }
/** _getBindingResource(binding: BindGroupEntry): GPUBindingResource {
* @param {BindGroupEntry} binding
* @returns {GPUBindingResource}
*/
_getBindingResource(binding) {
const resource = binding.resource const resource = binding.resource
switch (resource.resourceType) { switch (resource.resourceType) {
case ResourceType.Sampler: case ResourceType.Sampler:
@ -382,12 +303,7 @@ export class GraphicsDevice extends EventEmitter {
} }
} }
/** createBindGroup(layout: BindGroupLayout, bindings: BindGroupEntry[], label?: string) {
* @param {BindGroupLayout} layout
* @param {BindGroupEntry[]} bindings
* @param {string} [label]
*/
createBindGroup(layout, bindings, label) {
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
@ -403,11 +319,7 @@ export class GraphicsDevice extends EventEmitter {
}) })
} }
/** createPipelineLayout(layouts: Array<BindGroupLayout>, label?: string) {
* @param {Array<BindGroupLayout>} layouts
* @param {string} [label]
*/
createPipelineLayout(layouts, label) {
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
const bindGroupLayouts = layouts.map(layout => layout.handle) const bindGroupLayouts = layouts.map(layout => layout.handle)
@ -418,19 +330,13 @@ export class GraphicsDevice extends EventEmitter {
}) })
} }
/** createSampler(descriptor?: GPUSamplerDescriptor) {
* @param {GPUSamplerDescriptor} [descriptor]
*/
createSampler(descriptor) {
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
return Sampler.create(this.device, descriptor) return Sampler.create(this.device, descriptor)
} }
/** createTexture(descriptor: GPUTextureDescriptor) {
* @param {GPUTextureDescriptor} descriptor
*/
createTexture(descriptor) {
if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
try { try {
@ -440,16 +346,7 @@ export class GraphicsDevice extends EventEmitter {
} }
} }
/** createTextureFromBitmap(bitmap: ImageBitmap, options: BitmapTextureOptions) {
* @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 (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() }
if (!bitmap) { throw new TypeError('Provided bitmap is null.') } if (!bitmap) { throw new TypeError('Provided bitmap is null.') }
@ -458,8 +355,7 @@ export class GraphicsDevice extends EventEmitter {
const mipLevelCount = 1 const mipLevelCount = 1
/** @type {GPUTextureDescriptor} */ const descriptor: GPUTextureDescriptor = {
const descriptor = {
label: options.label, label: options.label,
size: { size: {
width: bitmap.width, width: bitmap.width,
@ -487,11 +383,7 @@ export class GraphicsDevice extends EventEmitter {
} }
} }
/** submitCommands(commandBuffers: GPUCommandBuffer[]) {
* Submits an array of command buffers to the GPU queue.
* @param {GPUCommandBuffer[]} commandBuffers
*/
submitCommands(commandBuffers) {
if (!this._isInitialized || !commandBuffers || commandBuffers.length === 0) return if (!this._isInitialized || !commandBuffers || commandBuffers.length === 0) return
this.queue.submit(commandBuffers) this.queue.submit(commandBuffers)

View file

@ -1,21 +1,17 @@
import { WebGPUError } from '../utils/errors.js' import { WebGPUError } from '../utils/errors.js'
import { PositiveInteger } from '../utils/index.js'
type SwapChainConfiguration = Omit<GPUCanvasConfiguration, 'device' | 'format'>
/** @import { PositiveInteger } from '../utils.js' */
/**
* @typedef {Omit<GPUCanvasConfiguration, 'device' | 'format'>} SwapChainConfiguration
*/
export class SwapChain { export class SwapChain {
_canvas _canvas: HTMLCanvasElement
_device _device: GPUDevice
_context _context: GPUCanvasContext
_format _format: GPUTextureFormat
_width _width: number
_height _height: number
/** @type {SwapChainConfiguration} */ _configuration: SwapChainConfiguration
_configuration
get context() { get context() {
return this._context return this._context
@ -33,15 +29,7 @@ export class SwapChain {
return this._height return this._height
} }
/** constructor(canvas: HTMLCanvasElement, device: GPUDevice, context?: GPUCanvasContext) {
* @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._canvas = canvas
this._device = device this._device = device
@ -56,10 +44,7 @@ export class SwapChain {
this._height = canvas.height this._height = canvas.height
} }
/** configure(configuration?: SwapChainConfiguration) {
* @param {SwapChainConfiguration} [configuration]
*/
configure(configuration) {
if (configuration) { if (configuration) {
this._configuration = configuration this._configuration = configuration
} }
@ -79,12 +64,7 @@ export class SwapChain {
return this._context.getCurrentTexture().createView() return this._context.getCurrentTexture().createView()
} }
/** resize<const T extends number>(width: PositiveInteger<T>, height: PositiveInteger<T>) {
* @template {number} const T
* @param {PositiveInteger<T>} width
* @param {PositiveInteger<T>} height
*/
resize(width, height) {
if (width <= 0 || height <= 0) { if (width <= 0 || height <= 0) {
return return
} }

View file

@ -1,4 +1,4 @@
import { Enum } from './utils.js' import { Enum } from './utils/index.js'
export const AddressMode = Enum( export const AddressMode = Enum(

View file

@ -1,17 +1,17 @@
import { BufferError, WebGPUError } from '../utils/errors.js' import { BufferError, WebGPUError } from '../utils/errors.js'
import { TypedArray, TypedArrayConstructor } from '../utils/index.js'
import { ResourceType } from '../utils/internal-enums.js' import { ResourceType } from '../utils/internal-enums.js'
/** @import { TypedArray, TypedArrayConstructor } from '../utils.js' */ type SmallTypedArray = Exclude<TypedArray, BigInt64Array | BigUint64Array>
/** @import { BindGroupEntry, BindingResource, BufferBindingResource } from '../core/graphics-device.js' */ type SmallTypedArrayConstructor = Exclude<TypedArrayConstructor, BigInt64ArrayConstructor | BigUint64ArrayConstructor>
export class Buffer { export class Buffer {
_device _device: GPUDevice
_handle _handle: GPUBuffer
_mapped = false _mapped = false
/** @type {GPUBuffer} */ _defaultStagingBuffer: GPUBuffer
_defaultStagingBuffer
get handle() { get handle() {
return this._handle return this._handle
@ -29,20 +29,12 @@ export class Buffer {
return ResourceType.Buffer return ResourceType.Buffer
} }
/** constructor(device: GPUDevice, texture: GPUBuffer) {
* @param {GPUDevice} device
* @param {GPUBuffer} texture
*/
constructor(device, texture) {
this._device = device this._device = device
this._handle = texture this._handle = texture
} }
/** static create(device: GPUDevice, descriptor: GPUBufferDescriptor) {
* @param {GPUDevice} device
* @param {GPUBufferDescriptor} descriptor
*/
static create(device, descriptor) {
try { try {
return new Buffer( return new Buffer(
device, device,
@ -53,29 +45,18 @@ export class Buffer {
} }
} }
/** _createStagingOptions(size: number = this.size) {
* @param {number} [size]
*/
_createStagingOptions(size = this.size) {
return { return {
size, size,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
} }
} }
/** _getStagingBuffer(size: number) {
* @param {number} [size]
*/
_getStagingBuffer(size) {
return this._device.createBuffer(this._createStagingOptions(size)) return this._device.createBuffer(this._createStagingOptions(size))
} }
/** write(data: ArrayBufferView | ArrayBuffer, offset: number = 0, dataOffset: number = 0) {
* @param {ArrayBufferView | ArrayBuffer} data
* @param {number} [offset=0]
* @param {number} [dataOffset=0]
*/
write(data, offset = 0, dataOffset = 0) {
if (!(this.usage & GPUBufferUsage.COPY_DST)) { if (!(this.usage & GPUBufferUsage.COPY_DST)) {
console.warn('Buffer usage does not include COPY_DST. Buffer.write may fail.') console.warn('Buffer usage does not include COPY_DST. Buffer.write may fail.')
} }
@ -92,17 +73,8 @@ export class Buffer {
) )
} }
/**
* @typedef {Exclude<TypedArray, BigInt64Array | BigUint64Array>} SmallTypedArray
* @typedef {Exclude<TypedArrayConstructor, BigInt64ArrayConstructor | BigUint64ArrayConstructor>} SmallTypedArrayConstructor
*/
/** async read(out: SmallTypedArray | DataView | undefined, byteOffset: number = 0, byteSize: number = -1) {
* @param {SmallTypedArray | DataView | undefined} [out]
* @param {number} [byteOffset=0]
* @param {number} [byteSize]
*/
async read(out, byteOffset = 0, byteSize = -1) {
if (!this._device) { if (!this._device) {
throw WebGPUError.deviceUnavailable() throw WebGPUError.deviceUnavailable()
} }
@ -124,15 +96,15 @@ export class Buffer {
if (out.byteLength < byteSize) { throw new RangeError(`Provided output buffer too small`) } if (out.byteLength < byteSize) { throw new RangeError(`Provided output buffer too small`) }
} }
let result let result: SmallTypedArray | ArrayBuffer | DataView<ArrayBufferLike>
let range let range: ArrayBuffer
try { try {
await this.handle.mapAsync(GPUMapMode.READ, byteOffset, byteSize) await this.handle.mapAsync(GPUMapMode.READ, byteOffset, byteSize)
range = this.handle.getMappedRange(byteOffset, byteSize) range = this.handle.getMappedRange(byteOffset, byteSize)
if (out != null) { if (out != null) {
const SourceView = /** @type {SmallTypedArrayConstructor} */ (out.constructor) const SourceView = out.constructor as SmallTypedArrayConstructor
const bytesPerElement = SourceView.BYTES_PER_ELEMENT const bytesPerElement = SourceView.BYTES_PER_ELEMENT
if (!bytesPerElement) { if (!bytesPerElement) {
@ -173,12 +145,7 @@ export class Buffer {
return result return result
} }
/** toBindingResource({ offset, size }: { offset?: number; size?: number } = {}) {
* @param {Object} [descriptor={}]
* @param {number} [descriptor.offset=0]
* @param {number} [descriptor.size]
*/
toBindingResource({ offset, size } = {}) {
return { return {
buffer: this._handle, buffer: this._handle,
offset: offset || 0, offset: offset || 0,
@ -194,19 +161,11 @@ export class Buffer {
export class UniformBuffer extends Buffer { export class UniformBuffer extends Buffer {
/** constructor(device: GPUDevice, buffer: GPUBuffer) {
* @param {GPUDevice} device
* @param {GPUBuffer} buffer
*/
constructor(device, buffer) {
super(device, buffer) super(device, buffer)
} }
/** static create(device: GPUDevice, descriptor: Omit<GPUBufferDescriptor, 'usage'>) {
* @param {GPUDevice} device
* @param {Omit<GPUBufferDescriptor, 'usage'>} descriptor
*/
static create(device, descriptor) {
const usage = GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST const usage = GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
return super.create(device, { return super.create(device, {
usage, usage,

0
src/resources/newFile.ts Normal file
View file

View file

@ -16,11 +16,10 @@ import { BufferBindingType } from '../enum.js'
/** @import { WGSLAccess, WGSLSamplerType } from '../utils/wgsl-to-wgpu.js' */ /** @import { WGSLAccess, WGSLSamplerType } from '../utils/wgsl-to-wgpu.js' */
export class ShaderModule { export class ShaderModule {
_handle _handle: GPUShaderModule
_code _code: string
/** @type {WgslReflect | undefined} */ _reflection: WgslReflect | undefined
_reflection
get handle() { get handle() {
return this._handle return this._handle
@ -30,11 +29,7 @@ export class ShaderModule {
return this._handle.label return this._handle.label
} }
/** constructor(device: GPUDevice, descriptor: GPUShaderModuleDescriptor) {
* @param {GPUDevice} device
* @param {GPUShaderModuleDescriptor} descriptor
*/
constructor(device, descriptor) {
this._code = descriptor.code this._code = descriptor.code
try { try {
@ -44,11 +39,7 @@ export class ShaderModule {
} }
} }
/** static create(device: GPUDevice, descriptor: GPUShaderModuleDescriptor) {
* @param {GPUDevice} device
* @param {GPUShaderModuleDescriptor} descriptor
*/
static create(device, descriptor) {
return new ShaderModule(device, descriptor) return new ShaderModule(device, descriptor)
} }
@ -63,49 +54,41 @@ export class ShaderModule {
} }
} }
/** export interface FragmentStateDescriptor {
* @typedef FragmentStateDescriptor constants?: Record<string, GPUPipelineConstantValue>
* @property {Record<string, GPUPipelineConstantValue>} [constants={}] targets?: GPUColorTargetState[]
* @property {GPUColorTargetState[]} [targets=[]] }
*
* @typedef VertexStateDescriptor export interface VertexStateDescriptor {
* @property {Record<string, GPUPipelineConstantValue>} [constants={}] constants?: Record<string, GPUPipelineConstantValue>
* @property {GPUVertexBufferLayout[]} [buffers=[]] buffers?: GPUVertexBufferLayout[]
* }
* @typedef ShaderPairStateDescriptor
* @property {FragmentStateDescriptor} [fragment] export interface ShaderPairStateDescriptor {
* @property {VertexStateDescriptor} vertex fragment?: FragmentStateDescriptor
* vertex: VertexStateDescriptor
*/ }
export class ReflectedShader { export class ReflectedShader {
static _reflectTypes = ['uniforms', 'storage', 'textures', 'samplers'] static _reflectTypes = ['uniforms', 'storage', 'textures', 'samplers']
_module _module: ShaderModule
get module() { get module() {
return this._module return this._module
} }
/** constructor(shader: ShaderModule) {
* @param {ShaderModule} shader
*/
constructor(shader) {
this._module = shader this._module = shader
} }
/** findVariableInfo(name: string, group: number): VariableInfo | undefined {
* @param {string} name
* @param {number} group
* @returns {VariableInfo | undefined}
*/
findVariableInfo(name, group) {
const reflection = this.module.reflect() const reflection = this.module.reflect()
for (const type of ReflectedShader._reflectTypes) { for (const type of ReflectedShader._reflectTypes) {
const variables = reflection[type] const variables = reflection[type]
if (variables) { if (variables) {
const variable = variables.find(v => v.name === name && v.group === group) const variable = variables.find((v: VariableInfo) => v.name === name && v.group === group)
if (variable) { if (variable) {
return variable return variable
} }
@ -113,11 +96,7 @@ export class ReflectedShader {
} }
} }
/** getEntrypoint(stageName: string): string | undefined {
* @param {string} stageName
* @returns {string | undefined}
*/
getEntrypoint(stageName) {
const entry = this.module.reflect().entry const entry = this.module.reflect().entry
// TODO: determine how to correctly handle // TODO: determine how to correctly handle
@ -126,10 +105,7 @@ export class ReflectedShader {
entry[stageName][0].name : undefined entry[stageName][0].name : undefined
} }
/** getShaderStages(): GPUShaderStageFlags {
* @returns {GPUShaderStageFlags}
*/
getShaderStages() {
const entry = this._module.reflect().entry const entry = this._module.reflect().entry
let stages = 0 let stages = 0
@ -145,19 +121,12 @@ export class ReflectedShader {
return stages return stages
} }
/** hasStage(stages: GPUShaderStageFlags) {
* @param {GPUShaderStageFlags} stages
*/
hasStage(stages) {
return this.getShaderStages() return this.getShaderStages()
& stages & stages
} }
/** getBindingsForStage(stages: GPUShaderStageFlags, out: GroupBindingMap = new GroupBindingMap()) {
* @param {GPUShaderStageFlags} stages
* @param {GroupBindingMap} [out=new GroupBindingMap()]
*/
getBindingsForStage(stages, out = new GroupBindingMap()) {
const groups = this._module.reflect().getBindGroups() const groups = this._module.reflect().getBindGroups()
groups.forEach((bindings, groupIndex) => { groups.forEach((bindings, groupIndex) => {
@ -179,19 +148,11 @@ export class ReflectedShader {
return out return out
} }
/** static _sortKeyIndices(map: Map<any, any>): number[] {
* @param {Map<any, any>} map
* @returns {number[]}
*/
static _sortKeyIndices(map) {
return Array.from(map.keys()).sort((a, b) => a - b) return Array.from(map.keys()).sort((a, b) => a - b)
} }
/** static _parseUniform(_variableInfo: VariableInfo): GPUBufferBindingLayout {
* @param {VariableInfo} _variableInfo
* @returns {GPUBufferBindingLayout}
*/
static _parseUniform(_variableInfo) {
return { return {
type: BufferBindingType.Uniform, type: BufferBindingType.Uniform,
// TODO: infer these two properties // TODO: infer these two properties
@ -200,15 +161,10 @@ export class ReflectedShader {
} }
} }
/** static _parseStorage(variableInfo: VariableInfo): GPUBufferBindingLayout {
* @param {VariableInfo} variableInfo
* @returns {GPUBufferBindingLayout}
*/
static _parseStorage(variableInfo) {
return { return {
type: accessToBufferType( type: accessToBufferType(
/** @type {WGSLAccess} */ variableInfo.access
(variableInfo.access)
), ),
// TODO: infer these two properties // TODO: infer these two properties
hasDynamicOffset: false, hasDynamicOffset: false,
@ -216,11 +172,7 @@ export class ReflectedShader {
} }
} }
/** static _parseTexture(variableInfo: VariableInfo): GPUTextureBindingLayout {
* @param {VariableInfo} variableInfo
* @returns {GPUTextureBindingLayout}
*/
static _parseTexture(variableInfo) {
const [type, sampledType] = parseTextureType( const [type, sampledType] = parseTextureType(
variableInfo.type.name variableInfo.type.name
) )
@ -232,39 +184,27 @@ export class ReflectedShader {
} }
} }
/** static _parseSampler(variableInfo: VariableInfo): GPUSamplerBindingLayout {
* @param {VariableInfo} variableInfo
* @returns {GPUSamplerBindingLayout}
*/
static _parseSampler(variableInfo) {
return { return {
type: typeToSamplerBindingType( type: typeToSamplerBindingType(
/** @type {WGSLSamplerType} */(variableInfo.type.name) variableInfo.type.name
) )
} }
} }
/** static _parseStorageTexture(variableInfo: VariableInfo): GPUStorageTextureBindingLayout {
* @param {VariableInfo} variableInfo
* @returns {GPUStorageTextureBindingLayout}
*/
static _parseStorageTexture(variableInfo) {
const [type] = parseTextureType(variableInfo.type.name) const [type] = parseTextureType(variableInfo.type.name)
return { return {
access: accessToStorageTextureAccess( access: accessToStorageTextureAccess(
/** @type {WGSLAccess} */(variableInfo.access) variableInfo.access
), ),
format: wgslToWgpuFormat(variableInfo.type.name), format: wgslToWgpuFormat(variableInfo.type.name),
viewDimension: typeToViewDimension(type) viewDimension: typeToViewDimension(type)
} }
} }
/** static _variableInfoToEntry(variableStageInfo: VariableStageInfo): GPUBindGroupLayoutEntry {
* @param {VariableStageInfo} variableStageInfo
* @returns {GPUBindGroupLayoutEntry}
*/
static _variableInfoToEntry(variableStageInfo) {
const { stages: visibility, variableInfo } = variableStageInfo const { stages: visibility, variableInfo } = variableStageInfo
switch (variableInfo.resourceType) { switch (variableInfo.resourceType) {
@ -304,10 +244,7 @@ export class ReflectedShader {
} }
} }
/** static createBindGroupLayoutEntries(groupBindings: GroupBindingMap) {
* @param {GroupBindingMap} groupBindings
*/
static createBindGroupLayoutEntries(groupBindings) {
const sortedGroupIndices = this._sortKeyIndices(groupBindings) const sortedGroupIndices = this._sortKeyIndices(groupBindings)
return sortedGroupIndices.map(groupIndex => { return sortedGroupIndices.map(groupIndex => {
@ -321,16 +258,10 @@ export class ReflectedShader {
} }
export class ShaderPair { export class ShaderPair {
/** @type {ReflectedShader} */ _vertex: ReflectedShader
_vertex _fragment: ReflectedShader
/** @type {ReflectedShader} */
_fragment
/** constructor(vertex: ReflectedShader, fragment?: ReflectedShader) {
* @param {ReflectedShader} vertex
* @param {ReflectedShader} [fragment]
*/
constructor(vertex, fragment) {
if (!vertex) { if (!vertex) {
throw new Error('Missing vertex shader') throw new Error('Missing vertex shader')
} }
@ -355,20 +286,16 @@ export class ShaderPair {
} }
} }
/** @param {ShaderModule} shader */ static fromUnifiedShader(shader: ShaderModule) {
static fromUnifiedShader(shader) {
return new ShaderPair( return new ShaderPair(
new ReflectedShader(shader) new ReflectedShader(shader)
) )
} }
/** static fromPair(value: {
* @param {{ vertex: ShaderModule
vertex: ShaderModule, fragment?: ShaderModule
fragment?: ShaderModule }) {
* }} value
*/
static fromPair(value) {
const vert = new ReflectedShader(value.vertex) const vert = new ReflectedShader(value.vertex)
const frag = value.fragment && new ReflectedShader(value.fragment) const frag = value.fragment && new ReflectedShader(value.fragment)
return new ShaderPair(vert, frag) return new ShaderPair(vert, frag)
@ -396,11 +323,7 @@ export class ShaderPair {
) )
} }
/** _getFragmentState(descriptor: FragmentStateDescriptor): GPUFragmentState {
* @param {FragmentStateDescriptor} descriptor
* @returns {GPUFragmentState}
*/
_getFragmentState(descriptor) {
return { return {
module: this._fragment.module.handle, module: this._fragment.module.handle,
entryPoint: this._fragment.getEntrypoint('fragment'), entryPoint: this._fragment.getEntrypoint('fragment'),
@ -409,11 +332,7 @@ export class ShaderPair {
} }
} }
/** _getVertexState(descriptor: VertexStateDescriptor): GPUVertexState {
* @param {VertexStateDescriptor} descriptor
* @returns {GPUVertexState}
*/
_getVertexState(descriptor) {
return { return {
module: this._vertex.module.handle, module: this._vertex.module.handle,
entryPoint: this._vertex.getEntrypoint('vertex'), entryPoint: this._vertex.getEntrypoint('vertex'),
@ -422,12 +341,7 @@ export class ShaderPair {
} }
} }
/** findVariableInfo(name: string, group: number): VariableInfo | undefined {
* @param {string} name
* @param {number} group
* @returns {VariableInfo | undefined}
*/
findVariableInfo(name, group) {
let variableInfo = this._vertex.findVariableInfo(name, group) let variableInfo = this._vertex.findVariableInfo(name, group)
if (!variableInfo && this._fragment !== this._vertex) { if (!variableInfo && this._fragment !== this._vertex) {
@ -437,11 +351,7 @@ export class ShaderPair {
return variableInfo return variableInfo
} }
/** getRenderPipelineStates(descriptor: ShaderPairStateDescriptor): Pick<GPURenderPipelineDescriptor, 'vertex' | 'fragment'> {
* @param {ShaderPairStateDescriptor} descriptor
* @returns {Pick<GPURenderPipelineDescriptor, 'vertex' | 'fragment'>}
*/
getRenderPipelineStates(descriptor) {
return { return {
fragment: this._getFragmentState(descriptor.fragment), fragment: this._getFragmentState(descriptor.fragment),
vertex: this._getVertexState(descriptor.vertex), vertex: this._getVertexState(descriptor.vertex),

View file

@ -1,113 +0,0 @@
import { WebGPUObjectError } from '../utils/errors.js'
import { ResourceType } from '../utils/internal-enums.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
}
get resourceType() {
return ResourceType.TextureView
}
/**
* @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)
}
}
getView() {
return this.createDefaultView()
}
toBindingResource() {
return this._handle
}
destroy() {
this._handle?.destroy()
this._handle = undefined
this._defaultView = undefined
}
}

230
src/resources/texture.ts Normal file
View file

@ -0,0 +1,230 @@
import { CommandRecorder } from '../core/command-recorder.js'
import { BitFlags } from '../utils/bitflags.js'
import { WebGPUObjectError } from '../utils/errors.js'
import { ResourceType } from '../utils/internal-enums.js'
import { textureToImageDimension } from '../utils/wgsl-to-wgpu.js'
/** @import { BindGroupEntry } from '../core/graphics-device.js' */
export class Texture {
static _defaultUsage = GPUTextureUsage.TEXTURE_BINDING
| GPUTextureUsage.COPY_DST
| GPUTextureUsage.RENDER_ATTACHMENT
_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
}
get resourceType() {
return ResourceType.TextureView
}
/**
* @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)
}
}
static _generateMipLevels(size) {
const max = Math.max.apply(undefined, size)
return 1 + Math.log2(max) | 0
}
/**
* @param {GPUDevice} device
* @param {string | URL} url
* @param {GPUTextureDescriptor} desciptor
*/
static async fromUrl(device, url, descriptor) {
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Failed to fetch remote resource: ${response.statusText}`)
}
const usage = options.usage || Texture._defaultUsage
const dimension = descriptor.dimension ? textureToImageDimension(descriptor.dimension) : '2d'
const blob = await response.blob()
const bitmap = await createImageBitmap(blob)
const size = [bitmap.width, bitmap.height]
const desc = {
usage,
dimension,
size,
format: descriptor.format || 'rgba8unorm',
mipLevelCount: descriptor.mipLevelCount || Texture._generateMipCount(...size),
...descriptor,
}
const texture = Texture.create(device, desc)
texture.upload(bitmap)
return texture
} catch (err) {
throw WebGPUObjectError.from(err, Texture)
}
}
/**
* @param {GPUDevice} device
* @param {GPUExtent3DStrict} size
* @param {GPUTextureFormat} format
* @param {GPUTextureDescriptor} descriptor
*/
static createRenderTarget(device, size, format, descriptor) {
const usage = descriptor.usage || Texture._defaultUsage
return Texture.create(device, {
size,
format,
usage,
...descriptor
})
}
/**
* @typedef UploadTextureInfo
* @property {GPUTexelCopyTextureInfo} destination
* @property {GPUTexelCopyBufferLayout} dataLayout
* @property {GPUExtent3DStrict} size
*/
/**
* @param {GPUAllowSharedBufferSource} source
* @param {UploadTextureInfo} [options={}]
*/
upload(source, options = {}) {
const mipLevel = options.destination.mipLevel || 0
const size = options.size || [
Math.max(1, this.width >> mipLevel),
Math.max(1, this.height >> mipLevel),
this.depthOrArrayLayers
]
try {
this._device.queue.writeTexture(
{ ...options.destination, texture: this._handle },
source,
options.dataLayout,
size
)
} catch (err) {
throw WebGPUObjectError.from(err, Texture)
}
}
/**
* @param {GPUCommandEncoder} commandEncoder
*/
generateMipmaps(commandEncoder) {
const requiredUsage = GPUTextureUsage.COPY_SRC
| GPUTextureUsage.COPY_DST
| GPUTextureUsage.RENDER_ATTACHMENT
if (!BitFlags.has(this.usage & requiredUsage)) {
throw new Error('Texture does not have the required usage flags for mipmap generation')
}
for (let i = 1; i < this.mipLevelCount; ++i) {
}
}
/**
* @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)
}
}
getView() {
return this.createDefaultView()
}
toBindingResource() {
return this.getView()
}
destroy() {
this._handle?.destroy()
this._handle = undefined
this._defaultView = undefined
}
}

View file

@ -1,317 +0,0 @@
/**
* @template T
* @template {keyof T} K
* @typedef {Pick<Partial<T>, K> & Omit<T, K>} Optional
*/
/**
* @template T, U
* @typedef {
{ [P in keyof T]: T[P] } &
{ [P in keyof U]?: never}
* } Only
*/
/**
* @template T, U
* @typedef {Only<T, U> | Only<U, T>} Either
*/
/**
* @template T
* @typedef {new (...args: any[]) => T} Newable<T>
*/
/**
* @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
*/
/**
* @typedef {
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024,
2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576,
2097152, 4194304, 8388608, 16777216, 33554432, 67108864, 134217728, 268435456,
536870912, 1073741824, 2147483648]
* } PowersOfTwo
*/
/**
* @template {number} T
* @typedef {`${T}` extends `-${string}` | '0' | `${string}.${string}` ? never : T } PositiveInteger
*/
/**
* @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 {readonly any[]} T
* @template V
* @template {any[]} [Acc=[]]
* @typedef {
T extends readonly [infer Head, ...infer Tail]
? Head extends V ? Acc['length'] : IndexOf<Tail, V, [...Acc, Head]>
: never
* } IndexOf
*/
/**
* @template {string} T
* @param {T} s
* @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
}, {})
))
/**
* @template T
* @typedef {T extends `${infer N extends number}` ? N : never } ParseInt
*/
/**
* @template {string} const T
* @template {T[]} const A
* @param {A} values
* @returns {Readonly<{ [K in keyof A as A[K] extends T ? PascalCase<A[K]> : never]: PowersOfTwo[ParseInt<K>] }>}
*/
export const FlagEnum = (...values) => /** @type {never} */(Object.freeze(
values.reduce((acc, x, i) => {
const key = pascal(fromKebab(x)).toString()
acc[key] = 1 << i
return acc
}, {})
))
/** @typedef {(...args: any) => void} Listener */
export class EventEmitter {
_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
)
}
}
}
/**
* @template {{}} T
* @template {keyof T} K
* @param {T} obj
* @param {...K} keys
* @returns {Pick<T, K>}
*/
export const pick = (obj, ...keys) => /** @type {Pick<T, K>} */(Object.fromEntries(
keys
.filter(key => key in obj)
.map(key => [key, obj[key]])
))
/**
* @template {{}} T
* @template {PropertyKey & keyof T} K
* @param {T} obj
* @param {...K} keys
* @returns {{ [Key in K]: Key extends keyof T ? T[Key] : never }}
*/
export const inclusivePick = (obj, ...keys) => /** @type {{[Key in K]: Key extends keyof T ? T[Key] : never}} */(Object.fromEntries(
keys.map(key => [key, obj[key]])
))
/**
* @template {{}} T
* @template {PropertyKey & keyof T} K
* @param {T} obj
* @param {...K} keys
* @returns {Omit<T, K>}
*/
export const omit = (obj, ...keys) => /** @type {Omit<T, K>} */(Object.fromEntries(
Object.entries(obj)
.filter(([key]) => !keys.includes(/** @type {K} */(key)))
))
/** @template {number} T */
export class Flags {
_value
/**
* @param {T} value
*/
constructor(value) {
this._value = value
}
/**
* Tests if `flag` exists in `bitflags`
*
* @template {number} T
* @param {T} flag
* @param {T} bitflags
*/
static has(flag, bitflags) {
return bitflags & flag
}
/**
* Combine two flags
*
* @template {number} T
* @param {T} a
* @param {T} b
*/
static add(a, b) {
return a | b
}
/**
* Tests if `flag` exists in these bitflags
*
* @param {T} flag
*/
has(flag) {
return Flags.has(flag, this._value)
}
/**
* Add `flag` to these bitflags
* @param {T} flag
*/
add(flag) {
return Flags.add(flag, this._value)
}
}

View file

@ -1,28 +0,0 @@
import { VariableInfo } from 'wgsl_reflect'
export class VariableStageInfo {
stages
variableInfo
/**
* @param {GPUShaderStageFlags} stages
* @param {VariableInfo} variableInfo
*/
constructor(stages, variableInfo) {
this.stages = stages
this.variableInfo = variableInfo
}
}
/**
* @extends Map<number, VariableStageInfo>
*/
export class BindingMap extends Map { }
/**
* @extends Map<number, BindingMap>
*/
export class GroupBindingMap extends Map { }

18
src/utils/bindings.ts Normal file
View file

@ -0,0 +1,18 @@
import { VariableInfo } from 'wgsl_reflect'
export class VariableStageInfo {
stages: GPUShaderStageFlags
variableInfo: VariableInfo
constructor(stages: GPUShaderStageFlags, variableInfo: VariableInfo) {
this.stages = stages
this.variableInfo = variableInfo
}
}
export class BindingMap extends Map<number, VariableStageInfo> { }
export class GroupBindingMap extends Map<number, BindingMap> { }

View file

@ -1,41 +0,0 @@
export class BitFlags {
_value
get flags() {
return this._value
}
constructor(value) {
this._value = value
}
/**
* @param {number} a
* @param {number} b
*/
static has(a, b) {
return (a & b) === b
}
/**
* @param {number} a
* @param {number} b
*/
static add(a, b) {
return a | b
}
/**
* @param {number} b
*/
has(b) {
return BitFlags.has(this._value, b)
}
/**
* @param {number} b
*/
add(b) {
return BitFlags.add(this._value, b)
}
}

28
src/utils/bitflags.ts Normal file
View file

@ -0,0 +1,28 @@
export class BitFlags {
_value: number
get flags() {
return this._value
}
constructor(value: number) {
this._value = value
}
static has(a: number, b: number) {
return (a & b) === b
}
static add(a: number, b: number) {
return a | b
}
has(b: number) {
return BitFlags.has(this._value, b)
}
add(b: number) {
return BitFlags.add(this._value, b)
}
}

View file

@ -1,11 +1,7 @@
/** @import { Newable } from '../utils.js' */ import { Newable } from './index.js'
export class WebGPUError extends Error { export class WebGPUError extends Error {
/** constructor(message: string, options?: ErrorOptions) {
* @param {string} message
* @param {ErrorOptions} [options]
*/
constructor(message, options) {
super(`WebGPUError: ${message}`, options) super(`WebGPUError: ${message}`, options)
} }
@ -39,30 +35,18 @@ export class CommandRecorderError extends Error {
} }
export class WebGPUObjectError extends Error { export class WebGPUObjectError extends Error {
/** static from<T extends Error>(cause: T, type: string | Newable) {
* @template T
* @param {Error} cause
* @param {string | Newable<T>} [type]
*/
static from(cause, type) {
const name = typeof type === 'string' ? type : type.name const name = typeof type === 'string' ? type : type.name
return new WebGPUObjectError(`could not create ${name}`, { cause }) return new WebGPUObjectError(`could not create ${name}`, { cause })
} }
} }
export class BufferError extends WebGPUObjectError { export class BufferError extends WebGPUObjectError {
/** constructor(message: string, options?: ErrorOptions) {
* @param {string} message
* @param {ErrorOptions} [options]
*/
constructor(message, options) {
super(`BufferError: ${message}`, options) super(`BufferError: ${message}`, options)
} }
/** static from<T extends Error>(cause: T) {
* @param {Error} cause
*/
static from(cause) {
return new BufferError('could not create buffer', { cause }) return new BufferError('could not create buffer', { cause })
} }
@ -70,35 +54,21 @@ export class BufferError extends WebGPUObjectError {
return new BufferError('cannot read a buffer without MAP_READ usage') return new BufferError('cannot read a buffer without MAP_READ usage')
} }
/** static outOfBounds(offset: number, size: number) {
* @param {number} offset
* @param {number} size
*/
static outOfBounds(offset, size) {
return new BufferError(`buffer offset/size (${offset}/${size}) exceeds buffer dimensions`) return new BufferError(`buffer offset/size (${offset}/${size}) exceeds buffer dimensions`)
} }
} }
export class MaterialError extends WebGPUObjectError { export class MaterialError extends WebGPUObjectError {
/** constructor(message: string, options?: ErrorOptions) {
* @param {string} message
* @param {ErrorOptions} [options]
*/
constructor(message, options) {
super(`MaterialError: ${message}`, options) super(`MaterialError: ${message}`, options)
} }
/** static from<T extends Error>(cause: T) {
* @param {Error} cause
*/
static from(cause) {
return new BufferError('could not create material', { cause }) return new BufferError('could not create material', { cause })
} }
/** static missingShader(shaderType: string) {
* @param {string} shaderType
*/
static missingShader(shaderType) {
return new BufferError(`missing ${shaderType} shader`) return new BufferError(`missing ${shaderType} shader`)
} }
} }

View file

@ -2,24 +2,18 @@ import { GraphicsDevice } from '../core/graphics-device.js'
export class GraphicsDeviceInitialized { export class GraphicsDeviceInitialized {
static EventName = 'graphics-device:initialized' static EventName = 'graphics-device:initialized'
graphicsDevice graphicsDevice: GraphicsDevice
/** constructor(graphicsDevice: GraphicsDevice) {
* @param {GraphicsDevice} graphicsDevice
*/
constructor(graphicsDevice) {
this.graphicsDevice = graphicsDevice this.graphicsDevice = graphicsDevice
} }
} }
export class GraphicsDeviceLost { export class GraphicsDeviceLost {
static EventName = 'graphics-device:device-lost' static EventName = 'graphics-device:device-lost'
info info: GPUDeviceLostInfo
/** constructor(info: GPUDeviceLostInfo) {
* @param {GPUDeviceLostInfo} info
*/
constructor(info) {
this.info = info this.info = info
} }
} }

160
src/utils/index.ts Normal file
View file

@ -0,0 +1,160 @@
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K & Omit<T, K>>
export type Only<T, U> = { [P in keyof T]: T[P] } & { [P in keyof U]?: never }
export type Either<T, U> = Only<T, U> | Only<U, T>
export interface Newable {
new(...args: any[]): any
}
export type TypedArray = Int8Array
| Uint8Array
| Uint8ClampedArray
| Int16Array
| Uint16Array
| Int32Array
| Uint32Array
| Float32Array
| Float64Array
| BigInt64Array
| BigUint64Array
export type TypedArrayConstructor = Int8ArrayConstructor
| Uint8ArrayConstructor
| Uint8ClampedArrayConstructor
| Int16ArrayConstructor
| Uint16ArrayConstructor
| Int32ArrayConstructor
| Uint32ArrayConstructor
| Float32ArrayConstructor
| Float64ArrayConstructor
| BigInt64ArrayConstructor
| BigUint64ArrayConstructor
export type PowersOfTwo = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512,
1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144,
524288, 1048576, 2097152, 4194304, 8388608, 16777216,
33554432, 67108864, 134217728, 268435456, 536870912, 1073741824,
2147483648]
export type PositiveInteger<T extends number> = `${T}` extends `-${string}` | '0' | `${string}.${string}` ? never : T
export type CapitalizeAll<T extends ReadonlyArray<string>> = T extends readonly [] ? [] : T extends readonly [infer Head extends string, ...infer Tail extends ReadonlyArray<string>] ? [Capitalize<Head>, ...CapitalizeAll<Tail>] : string[]
export type Join<T extends ReadonlyArray<string>, Sep extends string = ''> = 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
export type Split<S extends string, D extends string> = D extends '' ? [S] : S extends `${infer Head}${D}${infer Tail}` ? [Head, ...Split<Tail, D>] : [S]
export type PascalCaseFromDelimiter<S extends string, D extends string> = S extends `${infer Left}${D}${infer Right}` ? `${PascalCaseFromDelimiter<Left, D>}${Capitalize<PascalCaseFromDelimiter<Right, D>>}` : S
export type PascalCaseString<S extends string> = Capitalize<PascalCaseFromDelimiter<PascalCaseFromDelimiter<PascalCaseFromDelimiter<Lowercase<S>, '-'>, '_'>, ' '>>
export type PascalCase<T extends string | ReadonlyArray<string>> = T extends string ? PascalCaseString<T> : T extends ReadonlyArray<string> ? Join<CapitalizeAll<T>, ''> : T
export type IndexOf<T extends readonly any[], V, Acc extends any[] = []> = T extends readonly [infer Head, ...infer Tail]
? Head extends V ? Acc['length'] : IndexOf<Tail, V, [...Acc, Head]>
: never
export const uppercase = <T extends string>(s: T) => s.toUpperCase() as Uppercase<T>
export const lowercase = <T extends string>(s: T) => s.toLowerCase() as Lowercase<T>
export const split = <const T extends string, const D extends string>(delim: D, s: T) => s.split(delim) as Split<T, D>
export const fromKebab = <const T extends string>(s: T) => split('-', s) as Split<T, '-'>
export const toPascal = <const T extends string>(xs: T) => uppercase(xs[0]) + xs.slice(1) as PascalCase<T>
const pascal = <const T extends string[]>(xs: T) => xs.map(toPascal).join('') as PascalCase<T>
export const Enum = <const T extends string>(...values: T[]) => Object.freeze(
values.reduce((acc, x) => {
const key = pascal(fromKebab(x)).toString()
acc[key] = x
return acc
}, {})
) as Readonly<{ [K in T as PascalCase<K>]: K }>
type ParseInt<T> = T extends `${infer N extends number}` ? N : never
type FlagEnum<T extends string, A extends T[]> = Readonly<{ [K in keyof A as A[K] extends T ? PascalCase<A[K]> : never]: PowersOfTwo[ParseInt<K>] }>
export const FlagEnum = <const T extends string, const A extends T[]>(...values: A) => Object.freeze(
values.reduce((acc, x, i) => {
const key = pascal(fromKebab(x)).toString()
acc[key] = 1 << i
return acc
}, {})
) as FlagEnum<T, A>
interface Listener {
(...args: any): void
}
export class EventEmitter {
_listeners = {}
on(event: PropertyKey, callback: Listener) {
this._listeners[event] = this._listeners[event] || []
this._listeners[event].push(callback)
}
emit(event: PropertyKey, ...args: any[]) {
const listeners = this._listeners[event]
if (listeners) {
listeners.forEach(
(cb: Listener) => cb(...args)
)
}
}
off(event: PropertyKey, callback: Listener) {
const listeners = this._listeners[event]
if (listeners) {
this._listeners[event] = listeners.filter(
(cb: Listener) => cb !== callback
)
}
}
}
export const pick = <T extends object, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> => (Object.fromEntries(
keys
.filter(key => key in obj)
.map(key => [key, obj[key]])
)) as Pick<T, K>
export const inclusivePick = <T extends object, K extends keyof T>(obj: T, ...keys: K[]) => Object.fromEntries(
keys.map(key => [key, obj[key]])
) as { [Key in K]: Key extends keyof T ? T[Key] : never }
export const omit = <T extends object, K extends keyof T>(obj: T, ...keys: K[]) => Object.fromEntries(
Object.entries(obj)
.filter(([key]: K[]) => !keys.includes(key))
) as Omit<T, K>
export class Flags {
_value: number
constructor(value: number) {
this._value = value
}
static has(flag: number, bitflags: number) {
return bitflags & flag
}
static add(a: number, b: number) {
return a | b
}
has(flag: number) {
return Flags.has(flag, this._value)
}
add(flag: number) {
return Flags.add(flag, this._value)
}
}

View file

@ -1,4 +1,4 @@
import { FlagEnum } from '../utils.js' import { FlagEnum } from '../utils/index.js'
export const ShaderStage = FlagEnum( export const ShaderStage = FlagEnum(
'vertex', 'vertex',
@ -6,11 +6,7 @@ export const ShaderStage = FlagEnum(
'compute' 'compute'
) )
/** export const stageFlagToName = (stages: GPUShaderStageFlags): string[] => {
* @param {GPUShaderStageFlags} stages
* @returns {string[]}
*/
export const stageFlagToName = stages => {
const names = [] const names = []
if (stages & GPUShaderStage.FRAGMENT) { if (stages & GPUShaderStage.FRAGMENT) {
@ -28,10 +24,9 @@ export const stageFlagToName = stages => {
return names return names
} }
/** export type StageFlag = 'fragment' | 'vertex' | 'compute'
* @param {('fragment' | 'vertex' | 'compute')[]} names
*/ export const nameToStageFlag = (names: StageFlag[]) => {
export const nameToStageFlag = names => {
return names.reduce((flags, name) => { return names.reduce((flags, name) => {
switch (name.toLowerCase()) { switch (name.toLowerCase()) {
case 'fragment': return flags | GPUShaderStage.FRAGMENT case 'fragment': return flags | GPUShaderStage.FRAGMENT

188
src/utils/mip-generator.ts Normal file
View file

@ -0,0 +1,188 @@
import code from './mip-shader.wgsl' with { type: 'text' }
const mip = (n: number) => Math.max(1, n >>> 1)
export class MipGenerator {
_device: GPUDevice
_sampler: GPUSampler
_pipelines: Record<string, GPURenderPipeline>
_shader?: GPUShaderModule
_bindGroupLayout?: GPUBindGroupLayout
_pipelineLayout?: GPUPipelineLayout
constructor(device: GPUDevice) {
this._device = device
this._sampler = device.createSampler({ minFilter: 'linear' })
this._pipelines = {}
}
_getShader() {
if (!this._shader) {
this._shader = this._device.createShaderModule({
code
})
}
return this._shader
}
_getBindGroupLayout() {
if (!this._bindGroupLayout) {
this._bindGroupLayout = this._device.createBindGroupLayout({
entries: [{
binding: 0,
visibility: GPUShaderStage.FRAGMENT,
sampler: {}
}, {
binding: 1,
visibility: GPUShaderStage.FRAGMENT,
texture: {}
}]
})
}
return this._bindGroupLayout
}
_getPipelineLayout() {
if (!this._pipelineLayout) {
this._pipelineLayout = this._device.createPipelineLayout({
label: 'Mipmap Generator',
bindGroupLayouts: [this._getBindGroupLayout()],
})
}
return this._pipelineLayout
}
getPipeline(format: GPUTextureFormat) {
let pipeline = this._pipelines[format]
if (!pipeline) {
const shader = this._getShader()
pipeline = this._device.createRenderPipeline({
layout: this._getPipelineLayout(),
vertex: {
module: shader,
entryPoint: 'vs_main'
},
fragment: {
module: shader,
entryPoint: 'fs_main',
targets: [{ format }]
}
})
this._pipelines[format] = pipeline
}
return pipeline
}
generateMipmap(texture: GPUTexture, descriptor: GPUTextureDescriptor) {
const pipeline = this.getPipeline(descriptor.format)
if (descriptor.dimension !== '2d') {
throw new Error('Generating mipmaps for anything except 2d is unsupported.')
}
let mipTexture = texture
const { width, height, depthOrArrayLayers } = descriptor.size as GPUExtent3DDict
const renderToSource = descriptor.usage & GPUTextureUsage.RENDER_ATTACHMENT
if (!renderToSource) {
mipTexture = this._device.createTexture({
size: {
width: mip(width),
height: mip(height),
depthOrArrayLayers
},
format: descriptor.format,
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
mipLevelCount: descriptor.mipLevelCount - 1
})
}
const encoder = this._device.createCommandEncoder({})
for (let layer = 0; layer < depthOrArrayLayers; ++layer) {
let srcView = texture.createView({
baseMipLevel: 0,
mipLevelCount: 1,
dimension: '2d',
baseArrayLayer: layer,
arrayLayerCount: 1
})
let dstMipLevel = renderToSource ? 1 : 0
for (let i = 1; i < descriptor.mipLevelCount; ++i) {
const dstView = mipTexture.createView({
baseMipLevel: dstMipLevel++,
mipLevelCount: 1,
dimension: '2d',
baseArrayLayer: layer,
arrayLayerCount: 1
})
const passEncoder = encoder.beginRenderPass({
colorAttachments: [{
view: dstView,
loadOp: 'clear',
storeOp: 'store'
}]
})
const bindGroup = this._device.createBindGroup({
layout: this._bindGroupLayout,
entries: [{
binding: 0,
resource: this._sampler
}, {
binding: 1,
resource: srcView
}]
})
passEncoder.setPipeline(pipeline)
passEncoder.setBindGroup(0, bindGroup)
passEncoder.draw(3, 1, 0, 0)
passEncoder.end()
srcView = dstView
}
}
if (!renderToSource) {
const mipLevelSize = {
width: mip(width),
height: mip(height),
depthOrArrayLayers
}
for (let i = 1; i < descriptor.mipLevelCount; ++i) {
encoder.copyTextureToTexture({
texture: mipTexture,
mipLevel: i - 1
}, {
texture,
mipLevel: i,
}, mipLevelSize)
mipLevelSize.width = mip(mipLevelSize.width)
mipLevelSize.height = mip(mipLevelSize.height)
}
}
this._device.queue.submit([encoder.finish()])
if (!renderToSource) {
mipTexture.destroy()
}
return texture
}
}

23
src/utils/mip-shader.wgsl Normal file
View file

@ -0,0 +1,23 @@
var<private> pos : array<vec2<f32>, 3> = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0), vec2<f32>(-1.0, 3.0), vec2<f32>(3.0, -1.0));
struct VertexOutput {
@builtin(position) position : vec4<f32>,
@location(0) texCoord : vec2<f32>,
};
@vertex
fn vertexMain(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput {
var output : VertexOutput;
output.texCoord = pos[vertexIndex] * vec2<f32>(0.5, -0.5) + vec2<f32>(0.5);
output.position = vec4<f32>(pos[vertexIndex], 0.0, 1.0);
return output;
}
@group(0) @binding(0) var imgSampler : sampler;
@group(0) @binding(1) var img : texture_2d<f32>;
@fragment
fn fragmentMain(@location(0) texCoord : vec2<f32>) -> @location(0) vec4<f32> {
return textureSample(img, imgSampler, texCoord);
}

View file

@ -1,11 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "es2022", "module": "esnext",
"moduleResolution": "node", "moduleResolution": "node",
"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"