diff --git a/dist/index.js b/dist/index.js index 36ee19b..2181b3e 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,12 +1,8 @@ (() => { - // src/utils/errors.js + // src/utils/errors.ts var WebGPUError = class _WebGPUError extends Error { - /** - * @param {string} message - * @param {ErrorOptions} [options] - */ - constructor(message, options) { - super(`WebGPUError: ${message}`, options); + constructor(message, options2) { + super(`WebGPUError: ${message}`, options2); } static unsupported() { return new _WebGPUError("WebGPU is not supported on this browser"); @@ -32,58 +28,32 @@ } }; var WebGPUObjectError = class _WebGPUObjectError extends Error { - /** - * @template T - * @param {Error} cause - * @param {string | Newable} [type] - */ static from(cause, type) { const name = typeof type === "string" ? type : type.name; return new _WebGPUObjectError(`could not create ${name}`, { cause }); } }; var BufferError = class _BufferError extends WebGPUObjectError { - /** - * @param {string} message - * @param {ErrorOptions} [options] - */ - constructor(message, options) { - super(`BufferError: ${message}`, options); + constructor(message, options2) { + super(`BufferError: ${message}`, options2); } - /** - * @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`); } }; var MaterialError = class extends WebGPUObjectError { - /** - * @param {string} message - * @param {ErrorOptions} [options] - */ - constructor(message, options) { - super(`MaterialError: ${message}`, options); + constructor(message, options2) { + super(`MaterialError: ${message}`, options2); } - /** - * @param {Error} cause - */ static from(cause) { return new BufferError("could not create material", { cause }); } - /** - * @param {string} shaderType - */ static missingShader(shaderType) { return new BufferError(`missing ${shaderType} shader`); } @@ -166,82 +136,53 @@ } }; - // src/utils.js - var uppercase = (s2) => ( - /** @type {Uppercase} */ - s2.toUpperCase() + // src/utils/index.ts + var uppercase = (s2) => s2.toUpperCase(); + var split = (delim, s2) => s2.split(delim); + 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) => ( - /** @type {Split} */ - s2.split("-") - ); - var toPascal = (xs) => ( - /** @type {PascalCase} */ - uppercase(xs[0]) + xs.slice(1) - ); - var pascal = (xs) => ( - /** @type {PascalCase} */ - xs.map(toPascal).join("") - ); - var Enum = (...values) => ( - /** @type {Readonly<{ [K in T as PascalCase]: 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 FlagEnum = (...values) => Object.freeze( + values.reduce((acc, x2, i2) => { + const key = pascal(fromKebab(x2)).toString(); + acc[key] = 1 << i2; + return acc; + }, {}) ); var EventEmitter = class { - _listeners = {}; - /** - * @param {PropertyKey} event - * @param {Listener} callback - */ + constructor() { + this._listeners = {}; + } 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 ); } } }; - // src/utils/internal-enums.js + // src/utils/internal-enums.ts var ShaderStage = FlagEnum( "vertex", "fragment", @@ -450,23 +391,19 @@ } }; - // src/utils/events.js + // src/utils/events.ts var GraphicsDeviceInitialized = class { - static EventName = "graphics-device:initialized"; - graphicsDevice; - /** - * @param {GraphicsDevice} graphicsDevice - */ + static { + this.EventName = "graphics-device:initialized"; + } constructor(graphicsDevice) { this.graphicsDevice = graphicsDevice; } }; var GraphicsDeviceLost = class { - static EventName = "graphics-device:device-lost"; - info; - /** - * @param {GPUDeviceLostInfo} info - */ + static { + this.EventName = "graphics-device:device-lost"; + } constructor(info) { this.info = info; } @@ -5752,7 +5689,7 @@ } }; - // src/utils/bindings.js + // src/utils/bindings.ts var BindingMap = class extends Map { }; var GroupBindingMap = class extends Map { @@ -6079,7 +6016,7 @@ "instance" ); - // src/utils/wgsl-to-wgpu.js + // src/utils/wgsl-to-wgpu.ts var parseTextureType = (typeName) => { const chevronIndex = typeName.indexOf("<"); const type = typeName.slice(0, chevronIndex); @@ -6227,6 +6164,20 @@ 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) => { switch (format) { 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 var Texture = class _Texture { + static _defaultUsage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT; _device; _handle; /** @type {GPUTextureView | undefined} */ @@ -6857,6 +6831,95 @@ 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] * @throws {TextureError} @@ -6879,7 +6942,7 @@ return this.createDefaultView(); } toBindingResource() { - return this._handle; + return this.getView(); } destroy() { this._handle?.destroy(); @@ -7067,21 +7130,21 @@ /** * @param {GPURequestAdapterOptions} [options] */ - withAdapter(options) { + withAdapter(options2) { if (!this.isSupported()) { throw WebGPUError.unsupported(); } - this._adapter_options = options; + this._adapter_options = options2; return this; } /** * @param {GPUDeviceDescriptor} [options] */ - withDevice(options) { + withDevice(options2) { if (!this.isSupported()) { throw WebGPUError.unsupported(); } - this._device_descriptor = options; + this._device_descriptor = options2; return this; } async build() { @@ -7389,23 +7452,23 @@ * @param {boolean} [options.generateMipmaps=false] * @param {boolean} [options.flipY=false] */ - createTextureFromBitmap(bitmap, options) { + createTextureFromBitmap(bitmap, options2) { 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 usage = (options2.usage ?? GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT) | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST; const mipLevelCount = 1; const descriptor = { - label: options.label, + label: options2.label, size: { width: bitmap.width, height: bitmap.height, depthOrArrayLayers: 1 }, - format: options.format ?? "rgba8unorm", + format: options2.format ?? "rgba8unorm", usage, dimension: "2d", mipLevelCount, @@ -7414,7 +7477,7 @@ try { const texture = this.device.createTexture(descriptor); 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 } }, descriptor.size ); diff --git a/package-lock.json b/package-lock.json index 3e00f70..dc1365f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ }, "devDependencies": { "@webgpu/types": "^0.1.60", - "esbuild": "^0.25.2" + "esbuild": "^0.25.2", + "typescript": "^5.8.3" } }, "node_modules/@esbuild/aix-ppc64": { @@ -489,6 +490,20 @@ "@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": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/wgsl_reflect/-/wgsl_reflect-1.2.0.tgz", diff --git a/package.json b/package.json index 715e547..411fcf0 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "description": "", "devDependencies": { "@webgpu/types": "^0.1.60", - "esbuild": "^0.25.2" + "esbuild": "^0.25.2", + "typescript": "^5.8.3" }, "dependencies": { "wgsl_reflect": "^1.2.0" diff --git a/src/core/command-recorder.js b/src/core/command-recorder.ts similarity index 64% rename from src/core/command-recorder.js rename to src/core/command-recorder.ts index e0c91ab..40feb61 100644 --- a/src/core/command-recorder.js +++ b/src/core/command-recorder.ts @@ -5,32 +5,26 @@ import { SwapChain } from './swap-chain.js' export class CommandRecorder { static _defaultClearValue = { r: 0, g: 0, b: 0, a: 1 } - _device - _swapChain - _label + _device: GPUDevice + _swapChain: SwapChain - _encoder + _encoder: GPUCommandEncoder - /** @type {GPURenderPassEncoder | undefined} */ - _passEncoder + _passEncoder?: GPURenderPassEncoder - /** - * @param {GPUDevice} device - * @param {SwapChain} swapChain - * @param {string} [label] - */ - constructor(device, swapChain, label) { + get label() { + return this._encoder.label + } + + + constructor(device: GPUDevice, swapChain: SwapChain, label: string) { this._device = device this._swapChain = swapChain - this._label = label this._encoder = device.createCommandEncoder({ label }) } - /** - * @returns {[GPURenderPassColorAttachment]} - */ - _defaultColorAttachment() { + _defaultColorAttachment(): [GPURenderPassColorAttachment] { const view = this._swapChain.getCurrentTextureView() return [{ @@ -40,12 +34,8 @@ export class CommandRecorder { storeOp: StoreOp.Store }] } - /** - * @param {GPURenderPassColorAttachment[]} [colorAttachments] - * @param {GPURenderPassDepthStencilAttachment} [depthStencilAttachment] - * @returns {GPURenderPassEncoder} - */ - beginRenderPass(colorAttachments, depthStencilAttachment) { + + beginRenderPass(colorAttachments: GPURenderPassColorAttachment[], depthStencilAttachment: GPURenderPassDepthStencilAttachment): GPURenderPassEncoder { if (this._passEncoder) { throw CommandRecorderError.activeRenderPass() } @@ -53,7 +43,7 @@ export class CommandRecorder { const attachments = colorAttachments || this._defaultColorAttachment() const descriptor = { - label: this._label || 'RenderPass', + label: this.label || 'RenderPass', colorAttachments: attachments, depthStencilAttachment } diff --git a/src/core/graphics-device.js b/src/core/graphics-device.ts similarity index 63% rename from src/core/graphics-device.js rename to src/core/graphics-device.ts index 2f1d2a3..9f0b6fd 100644 --- a/src/core/graphics-device.js +++ b/src/core/graphics-device.ts @@ -2,57 +2,54 @@ 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 { EventEmitter } from '../utils/index.js' +import { ShaderModule, ShaderPairStateDescriptor } 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' import { ResourceType } from '../utils/internal-enums.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 { - _canvas + _canvas: HTMLCanvasElement + + _adapterOptions: GPURequestAdapterOptions + _deviceDescriptor: GPUDeviceDescriptor get canvas() { return this._canvas } - /** @type {GPURequestAdapterOptions} */ - _adapter_options - /** @type {GPUDeviceDescriptor} */ - _device_descriptor - - /** - * @param {HTMLCanvasElement} [canvasElement] - */ - constructor(canvasElement) { + constructor(canvasElement: HTMLCanvasElement) { this._canvas = canvasElement } @@ -60,35 +57,26 @@ class GraphicsDeviceBuilder { return navigator.gpu } - /** - * @param {HTMLCanvasElement} canvasElement - */ - withCanvas(canvasElement) { + withCanvas(canvasElement: HTMLCanvasElement) { this._canvas = canvasElement return this } - /** - * @param {GPURequestAdapterOptions} [options] - */ - withAdapter(options) { + withAdapter(options: GPURequestAdapterOptions) { if (!this.isSupported()) { throw WebGPUError.unsupported() } - this._adapter_options = options + this._adapterOptions = options return this } - /** - * @param {GPUDeviceDescriptor} [options] - */ - withDevice(options) { + withDevice(options: GPUDeviceDescriptor) { if (!this.isSupported()) { throw WebGPUError.unsupported() } - this._device_descriptor = options + this._deviceDescriptor = options return this } @@ -96,39 +84,28 @@ class GraphicsDeviceBuilder { return new GraphicsDevice( this._canvas, new DeviceHandler( - this._adapter_options, - this._device_descriptor + this._adapterOptions, + this._deviceDescriptor ) ) } } class DeviceHandler { - /** @type {GPURequestAdapterOptions} */ - _adapterOptions - - /** @type {GPUAdapter} */ - _adapter + _adapterOptions: GPURequestAdapterOptions + _adapter: GPUAdapter + _deviceDescriptor: GPUDeviceDescriptor + _device: GPUDevice get adapter() { return this._adapter } - /** @type {GPUDeviceDescriptor} */ - _deviceDescriptor - - /** @type {GPUDevice} */ - _device - get device() { return this._device } - /** - * @param {GPURequestAdapterOptions} adapterOptions - * @param {GPUDeviceDescriptor} deviceDescriptor - */ - constructor(adapterOptions, deviceDescriptor) { + constructor(adapterOptions: GPURequestAdapterOptions, deviceDescriptor: GPUDeviceDescriptor) { this._adapterOptions = adapterOptions this._deviceDescriptor = deviceDescriptor } @@ -149,14 +126,10 @@ class DeviceHandler { } export class GraphicsDevice extends EventEmitter { - _canvas - _deviceHandler - - /** @type {SwapChain} */ - _swapChain - - /** @type {GPUQueue} */ - _queue + _canvas: HTMLCanvasElement + _deviceHandler: DeviceHandler + _swapChain: SwapChain + _queue: GPUQueue _isInitialized = false @@ -180,22 +153,14 @@ export class GraphicsDevice extends EventEmitter { return this._swapChain } - - /** - * @param {HTMLCanvasElement} canvas - * @param {DeviceHandler} deviceHandler - */ - constructor(canvas, deviceHandler) { + constructor(canvas: HTMLCanvasElement, deviceHandler: DeviceHandler) { super() this._canvas = canvas this._deviceHandler = deviceHandler } - /** - * @param {HTMLCanvasElement} [canvas] - */ - static build(canvas) { + static build(canvas: HTMLCanvasElement) { return new GraphicsDeviceBuilder(canvas) } @@ -204,7 +169,7 @@ export class GraphicsDevice extends EventEmitter { this._swapChain = new SwapChain( this._canvas, - this._deviceHandler.device + this._deviceHandler.device, ) this._swapChain.configure() @@ -230,19 +195,7 @@ export class GraphicsDevice extends EventEmitter { return true } - /** - * @typedef {Omit} BufferDescriptor - */ - - /** - * Create a GPU buffer - * @param {BufferDescriptor} descriptor - * @param {ArrayBufferView | ArrayBuffer} [data] - * @returns {Buffer} - * - * @throws {GPUBufferError} - */ - createBuffer(descriptor, data) { + createBuffer(descriptor: GPUBufferDescriptor, data: ArrayBufferView | ArrayBuffer): Buffer { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } try { @@ -258,12 +211,7 @@ export class GraphicsDevice extends EventEmitter { } } - /** - * @param {number} size - * @param {ArrayBufferView | ArrayBuffer} [data] - * @param {string} [label] - */ - createUniformBuffer(size, data, label) { + createUniformBuffer(size: number, data: ArrayBufferView | ArrayBuffer, label: string) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } const buffer = UniformBuffer.create(this.device, { @@ -278,13 +226,7 @@ export class GraphicsDevice extends EventEmitter { return buffer } - /** - * Creates a shader module from WGSL code. - * @param {string} code - * @param {string} [label] - * @returns {ShaderModule} - */ - createShaderModule(code, label) { + createShaderModule(code: string, label: string): ShaderModule { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } try { @@ -294,12 +236,7 @@ export class GraphicsDevice extends EventEmitter { } } - /** - * Creates a render pipeline. - * @param {GPURenderPipelineDescriptor} descriptor - Raw render pipeline descriptor. - * @returns {RenderPipeline} - */ - createRenderPipeline(descriptor) { + createRenderPipeline(descriptor: GPURenderPipelineDescriptor): RenderPipeline { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } try { @@ -310,10 +247,7 @@ export class GraphicsDevice extends EventEmitter { } } - /** - * @param {import('../resources/material.js').ShaderPairDescriptor} shaders - */ - createMaterial(shaders) { + createMaterial(shaders: ShaderPairStateDescriptor) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } try { @@ -323,22 +257,13 @@ export class GraphicsDevice extends EventEmitter { } } - /** - * Creates a CommandRecorder to begin recording GPU commands. - * @param {string} [label] - * @returns {CommandRecorder} - */ - createCommandRecorder(label) { + createCommandRecorder(label?: string): CommandRecorder { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } return new CommandRecorder(this.device, this._swapChain, label) } - /** - * @param {GPUBindGroupLayoutEntry[]} entries - * @param {string} [label] - */ - createBindGroupLayout(entries, label) { + createBindGroupLayout(entries: GPUBindGroupLayoutEntry[], label: string) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } return BindGroupLayout.create(this.device, { @@ -347,11 +272,7 @@ export class GraphicsDevice extends EventEmitter { }) } - /** - * @param {BindGroupEntry} binding - * @returns {GPUBindingResource} - */ - _getBindingResource(binding) { + _getBindingResource(binding: BindGroupEntry): GPUBindingResource { const resource = binding.resource switch (resource.resourceType) { case ResourceType.Sampler: @@ -382,12 +303,7 @@ export class GraphicsDevice extends EventEmitter { } } - /** - * @param {BindGroupLayout} layout - * @param {BindGroupEntry[]} bindings - * @param {string} [label] - */ - createBindGroup(layout, bindings, label) { + createBindGroup(layout: BindGroupLayout, bindings: BindGroupEntry[], label?: string) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } @@ -403,11 +319,7 @@ export class GraphicsDevice extends EventEmitter { }) } - /** - * @param {Array} layouts - * @param {string} [label] - */ - createPipelineLayout(layouts, label) { + createPipelineLayout(layouts: Array, label?: string) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } const bindGroupLayouts = layouts.map(layout => layout.handle) @@ -418,19 +330,13 @@ export class GraphicsDevice extends EventEmitter { }) } - /** - * @param {GPUSamplerDescriptor} [descriptor] - */ - createSampler(descriptor) { + createSampler(descriptor?: GPUSamplerDescriptor) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } return Sampler.create(this.device, descriptor) } - /** - * @param {GPUTextureDescriptor} descriptor - */ - createTexture(descriptor) { + createTexture(descriptor: GPUTextureDescriptor) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } try { @@ -440,16 +346,7 @@ export class GraphicsDevice extends EventEmitter { } } - /** - * @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) { + createTextureFromBitmap(bitmap: ImageBitmap, options: BitmapTextureOptions) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } if (!bitmap) { throw new TypeError('Provided bitmap is null.') } @@ -458,8 +355,7 @@ export class GraphicsDevice extends EventEmitter { const mipLevelCount = 1 - /** @type {GPUTextureDescriptor} */ - const descriptor = { + const descriptor: GPUTextureDescriptor = { label: options.label, size: { width: bitmap.width, @@ -487,11 +383,7 @@ export class GraphicsDevice extends EventEmitter { } } - /** - * Submits an array of command buffers to the GPU queue. - * @param {GPUCommandBuffer[]} commandBuffers - */ - submitCommands(commandBuffers) { + submitCommands(commandBuffers: GPUCommandBuffer[]) { if (!this._isInitialized || !commandBuffers || commandBuffers.length === 0) return this.queue.submit(commandBuffers) diff --git a/src/core/swap-chain.js b/src/core/swap-chain.ts similarity index 60% rename from src/core/swap-chain.js rename to src/core/swap-chain.ts index a3e3e85..43b576f 100644 --- a/src/core/swap-chain.js +++ b/src/core/swap-chain.ts @@ -1,21 +1,17 @@ import { WebGPUError } from '../utils/errors.js' +import { PositiveInteger } from '../utils/index.js' +type SwapChainConfiguration = Omit -/** @import { PositiveInteger } from '../utils.js' */ - -/** - * @typedef {Omit} SwapChainConfiguration - */ export class SwapChain { - _canvas - _device - _context - _format - _width - _height + _canvas: HTMLCanvasElement + _device: GPUDevice + _context: GPUCanvasContext + _format: GPUTextureFormat + _width: number + _height: number - /** @type {SwapChainConfiguration} */ - _configuration + _configuration: SwapChainConfiguration get context() { return this._context @@ -33,15 +29,7 @@ export class SwapChain { 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) { + constructor(canvas: HTMLCanvasElement, device: GPUDevice, context?: GPUCanvasContext) { this._canvas = canvas this._device = device @@ -56,10 +44,7 @@ export class SwapChain { this._height = canvas.height } - /** - * @param {SwapChainConfiguration} [configuration] - */ - configure(configuration) { + configure(configuration?: SwapChainConfiguration) { if (configuration) { this._configuration = configuration } @@ -79,12 +64,7 @@ export class SwapChain { return this._context.getCurrentTexture().createView() } - /** - * @template {number} const T - * @param {PositiveInteger} width - * @param {PositiveInteger} height - */ - resize(width, height) { + resize(width: PositiveInteger, height: PositiveInteger) { if (width <= 0 || height <= 0) { return } diff --git a/src/enum.js b/src/enum.ts similarity index 99% rename from src/enum.js rename to src/enum.ts index 5dad3a0..6068794 100644 --- a/src/enum.js +++ b/src/enum.ts @@ -1,4 +1,4 @@ -import { Enum } from './utils.js' +import { Enum } from './utils/index.js' export const AddressMode = Enum( diff --git a/src/index.js b/src/index.ts similarity index 100% rename from src/index.js rename to src/index.ts diff --git a/src/rendering/render-graph.js b/src/rendering/render-graph.ts similarity index 100% rename from src/rendering/render-graph.js rename to src/rendering/render-graph.ts diff --git a/src/rendering/render-pipeline.js b/src/rendering/render-pipeline.ts similarity index 100% rename from src/rendering/render-pipeline.js rename to src/rendering/render-pipeline.ts diff --git a/src/resources/bind-group-layout.js b/src/resources/bind-group-layout.ts similarity index 100% rename from src/resources/bind-group-layout.js rename to src/resources/bind-group-layout.ts diff --git a/src/resources/bind-group.js b/src/resources/bind-group.ts similarity index 100% rename from src/resources/bind-group.js rename to src/resources/bind-group.ts diff --git a/src/resources/buffer.js b/src/resources/buffer.ts similarity index 66% rename from src/resources/buffer.js rename to src/resources/buffer.ts index 40ec222..028ecf4 100644 --- a/src/resources/buffer.js +++ b/src/resources/buffer.ts @@ -1,17 +1,17 @@ import { BufferError, WebGPUError } from '../utils/errors.js' +import { TypedArray, TypedArrayConstructor } from '../utils/index.js' import { ResourceType } from '../utils/internal-enums.js' -/** @import { TypedArray, TypedArrayConstructor } from '../utils.js' */ -/** @import { BindGroupEntry, BindingResource, BufferBindingResource } from '../core/graphics-device.js' */ +type SmallTypedArray = Exclude +type SmallTypedArrayConstructor = Exclude export class Buffer { - _device - _handle + _device: GPUDevice + _handle: GPUBuffer _mapped = false - /** @type {GPUBuffer} */ - _defaultStagingBuffer + _defaultStagingBuffer: GPUBuffer get handle() { return this._handle @@ -29,20 +29,12 @@ export class Buffer { return ResourceType.Buffer } - /** - * @param {GPUDevice} device - * @param {GPUBuffer} texture - */ - constructor(device, texture) { + constructor(device: GPUDevice, texture: GPUBuffer) { this._device = device this._handle = texture } - /** - * @param {GPUDevice} device - * @param {GPUBufferDescriptor} descriptor - */ - static create(device, descriptor) { + static create(device: GPUDevice, descriptor: GPUBufferDescriptor) { try { return new Buffer( device, @@ -53,29 +45,18 @@ export class Buffer { } } - /** - * @param {number} [size] - */ - _createStagingOptions(size = this.size) { + _createStagingOptions(size: number = this.size) { return { size, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST } } - /** - * @param {number} [size] - */ - _getStagingBuffer(size) { + _getStagingBuffer(size: number) { 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) { + write(data: ArrayBufferView | ArrayBuffer, offset: number = 0, dataOffset: number = 0) { if (!(this.usage & GPUBufferUsage.COPY_DST)) { console.warn('Buffer usage does not include COPY_DST. Buffer.write may fail.') } @@ -92,17 +73,8 @@ export class Buffer { ) } - /** - * @typedef {Exclude} SmallTypedArray - * @typedef {Exclude} SmallTypedArrayConstructor - */ - /** - * @param {SmallTypedArray | DataView | undefined} [out] - * @param {number} [byteOffset=0] - * @param {number} [byteSize] - */ - async read(out, byteOffset = 0, byteSize = -1) { + async read(out: SmallTypedArray | DataView | undefined, byteOffset: number = 0, byteSize: number = -1) { if (!this._device) { throw WebGPUError.deviceUnavailable() } @@ -124,15 +96,15 @@ export class Buffer { if (out.byteLength < byteSize) { throw new RangeError(`Provided output buffer too small`) } } - let result - let range + let result: SmallTypedArray | ArrayBuffer | DataView + let range: ArrayBuffer 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 SourceView = out.constructor as SmallTypedArrayConstructor const bytesPerElement = SourceView.BYTES_PER_ELEMENT if (!bytesPerElement) { @@ -173,12 +145,7 @@ export class Buffer { return result } - /** - * @param {Object} [descriptor={}] - * @param {number} [descriptor.offset=0] - * @param {number} [descriptor.size] - */ - toBindingResource({ offset, size } = {}) { + toBindingResource({ offset, size }: { offset?: number; size?: number } = {}) { return { buffer: this._handle, offset: offset || 0, @@ -194,19 +161,11 @@ export class Buffer { export class UniformBuffer extends Buffer { - /** - * @param {GPUDevice} device - * @param {GPUBuffer} buffer - */ - constructor(device, buffer) { + constructor(device: GPUDevice, buffer: GPUBuffer) { super(device, buffer) } - /** - * @param {GPUDevice} device - * @param {Omit} descriptor - */ - static create(device, descriptor) { + static create(device: GPUDevice, descriptor: Omit) { const usage = GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST return super.create(device, { usage, diff --git a/src/resources/geometry.js b/src/resources/geometry.ts similarity index 100% rename from src/resources/geometry.js rename to src/resources/geometry.ts diff --git a/src/resources/material.js b/src/resources/material.ts similarity index 100% rename from src/resources/material.js rename to src/resources/material.ts diff --git a/src/resources/newFile.ts b/src/resources/newFile.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/resources/sampler.js b/src/resources/sampler.ts similarity index 100% rename from src/resources/sampler.js rename to src/resources/sampler.ts diff --git a/src/resources/shader-module.js b/src/resources/shader-module.ts similarity index 66% rename from src/resources/shader-module.js rename to src/resources/shader-module.ts index cf6ebd2..2d9330c 100644 --- a/src/resources/shader-module.js +++ b/src/resources/shader-module.ts @@ -16,11 +16,10 @@ import { BufferBindingType } from '../enum.js' /** @import { WGSLAccess, WGSLSamplerType } from '../utils/wgsl-to-wgpu.js' */ export class ShaderModule { - _handle - _code + _handle: GPUShaderModule + _code: string - /** @type {WgslReflect | undefined} */ - _reflection + _reflection: WgslReflect | undefined get handle() { return this._handle @@ -30,11 +29,7 @@ export class ShaderModule { return this._handle.label } - /** - * @param {GPUDevice} device - * @param {GPUShaderModuleDescriptor} descriptor - */ - constructor(device, descriptor) { + constructor(device: GPUDevice, descriptor: GPUShaderModuleDescriptor) { this._code = descriptor.code try { @@ -44,11 +39,7 @@ export class ShaderModule { } } - /** - * @param {GPUDevice} device - * @param {GPUShaderModuleDescriptor} descriptor - */ - static create(device, descriptor) { + static create(device: GPUDevice, descriptor: GPUShaderModuleDescriptor) { return new ShaderModule(device, descriptor) } @@ -63,49 +54,41 @@ export class ShaderModule { } } -/** - * @typedef FragmentStateDescriptor - * @property {Record} [constants={}] - * @property {GPUColorTargetState[]} [targets=[]] - * - * @typedef VertexStateDescriptor - * @property {Record} [constants={}] - * @property {GPUVertexBufferLayout[]} [buffers=[]] - * - * @typedef ShaderPairStateDescriptor - * @property {FragmentStateDescriptor} [fragment] - * @property {VertexStateDescriptor} vertex - * - */ +export interface FragmentStateDescriptor { + constants?: Record + targets?: GPUColorTargetState[] +} + +export interface VertexStateDescriptor { + constants?: Record + buffers?: GPUVertexBufferLayout[] +} + +export interface ShaderPairStateDescriptor { + fragment?: FragmentStateDescriptor + vertex: VertexStateDescriptor +} export class ReflectedShader { static _reflectTypes = ['uniforms', 'storage', 'textures', 'samplers'] - _module + _module: ShaderModule get module() { return this._module } - /** - * @param {ShaderModule} shader - */ - constructor(shader) { + constructor(shader: ShaderModule) { this._module = shader } - /** - * @param {string} name - * @param {number} group - * @returns {VariableInfo | undefined} - */ - findVariableInfo(name, group) { + findVariableInfo(name: string, group: number): VariableInfo | undefined { const reflection = this.module.reflect() for (const type of ReflectedShader._reflectTypes) { const variables = reflection[type] 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) { return variable } @@ -113,11 +96,7 @@ export class ReflectedShader { } } - /** - * @param {string} stageName - * @returns {string | undefined} - */ - getEntrypoint(stageName) { + getEntrypoint(stageName: string): string | undefined { const entry = this.module.reflect().entry // TODO: determine how to correctly handle @@ -126,10 +105,7 @@ export class ReflectedShader { entry[stageName][0].name : undefined } - /** - * @returns {GPUShaderStageFlags} - */ - getShaderStages() { + getShaderStages(): GPUShaderStageFlags { const entry = this._module.reflect().entry let stages = 0 @@ -145,19 +121,12 @@ export class ReflectedShader { return stages } - /** - * @param {GPUShaderStageFlags} stages - */ - hasStage(stages) { + hasStage(stages: GPUShaderStageFlags) { return this.getShaderStages() & stages } - /** - * @param {GPUShaderStageFlags} stages - * @param {GroupBindingMap} [out=new GroupBindingMap()] - */ - getBindingsForStage(stages, out = new GroupBindingMap()) { + getBindingsForStage(stages: GPUShaderStageFlags, out: GroupBindingMap = new GroupBindingMap()) { const groups = this._module.reflect().getBindGroups() groups.forEach((bindings, groupIndex) => { @@ -179,19 +148,11 @@ export class ReflectedShader { return out } - /** - * @param {Map} map - * @returns {number[]} - */ - static _sortKeyIndices(map) { + static _sortKeyIndices(map: Map): number[] { return Array.from(map.keys()).sort((a, b) => a - b) } - /** - * @param {VariableInfo} _variableInfo - * @returns {GPUBufferBindingLayout} - */ - static _parseUniform(_variableInfo) { + static _parseUniform(_variableInfo: VariableInfo): GPUBufferBindingLayout { return { type: BufferBindingType.Uniform, // TODO: infer these two properties @@ -200,15 +161,10 @@ export class ReflectedShader { } } - /** - * @param {VariableInfo} variableInfo - * @returns {GPUBufferBindingLayout} - */ - static _parseStorage(variableInfo) { + static _parseStorage(variableInfo: VariableInfo): GPUBufferBindingLayout { return { type: accessToBufferType( - /** @type {WGSLAccess} */ - (variableInfo.access) + variableInfo.access ), // TODO: infer these two properties hasDynamicOffset: false, @@ -216,11 +172,7 @@ export class ReflectedShader { } } - /** - * @param {VariableInfo} variableInfo - * @returns {GPUTextureBindingLayout} - */ - static _parseTexture(variableInfo) { + static _parseTexture(variableInfo: VariableInfo): GPUTextureBindingLayout { const [type, sampledType] = parseTextureType( variableInfo.type.name ) @@ -232,39 +184,27 @@ export class ReflectedShader { } } - /** - * @param {VariableInfo} variableInfo - * @returns {GPUSamplerBindingLayout} - */ - static _parseSampler(variableInfo) { + static _parseSampler(variableInfo: VariableInfo): GPUSamplerBindingLayout { return { type: typeToSamplerBindingType( - /** @type {WGSLSamplerType} */(variableInfo.type.name) + variableInfo.type.name ) } } - /** - * @param {VariableInfo} variableInfo - * @returns {GPUStorageTextureBindingLayout} - */ - static _parseStorageTexture(variableInfo) { + static _parseStorageTexture(variableInfo: VariableInfo): GPUStorageTextureBindingLayout { const [type] = parseTextureType(variableInfo.type.name) return { access: accessToStorageTextureAccess( - /** @type {WGSLAccess} */(variableInfo.access) + variableInfo.access ), format: wgslToWgpuFormat(variableInfo.type.name), viewDimension: typeToViewDimension(type) } } - /** - * @param {VariableStageInfo} variableStageInfo - * @returns {GPUBindGroupLayoutEntry} - */ - static _variableInfoToEntry(variableStageInfo) { + static _variableInfoToEntry(variableStageInfo: VariableStageInfo): GPUBindGroupLayoutEntry { const { stages: visibility, variableInfo } = variableStageInfo switch (variableInfo.resourceType) { @@ -304,10 +244,7 @@ export class ReflectedShader { } } - /** - * @param {GroupBindingMap} groupBindings - */ - static createBindGroupLayoutEntries(groupBindings) { + static createBindGroupLayoutEntries(groupBindings: GroupBindingMap) { const sortedGroupIndices = this._sortKeyIndices(groupBindings) return sortedGroupIndices.map(groupIndex => { @@ -321,16 +258,10 @@ export class ReflectedShader { } export class ShaderPair { - /** @type {ReflectedShader} */ - _vertex - /** @type {ReflectedShader} */ - _fragment + _vertex: ReflectedShader + _fragment: ReflectedShader - /** - * @param {ReflectedShader} vertex - * @param {ReflectedShader} [fragment] - */ - constructor(vertex, fragment) { + constructor(vertex: ReflectedShader, fragment?: ReflectedShader) { if (!vertex) { throw new Error('Missing vertex shader') } @@ -355,20 +286,16 @@ export class ShaderPair { } } - /** @param {ShaderModule} shader */ - static fromUnifiedShader(shader) { + static fromUnifiedShader(shader: ShaderModule) { return new ShaderPair( new ReflectedShader(shader) ) } - /** - * @param {{ - vertex: ShaderModule, - fragment?: ShaderModule - * }} value - */ - static fromPair(value) { + static fromPair(value: { + vertex: ShaderModule + fragment?: ShaderModule + }) { const vert = new ReflectedShader(value.vertex) const frag = value.fragment && new ReflectedShader(value.fragment) return new ShaderPair(vert, frag) @@ -396,11 +323,7 @@ export class ShaderPair { ) } - /** - * @param {FragmentStateDescriptor} descriptor - * @returns {GPUFragmentState} - */ - _getFragmentState(descriptor) { + _getFragmentState(descriptor: FragmentStateDescriptor): GPUFragmentState { return { module: this._fragment.module.handle, entryPoint: this._fragment.getEntrypoint('fragment'), @@ -409,11 +332,7 @@ export class ShaderPair { } } - /** - * @param {VertexStateDescriptor} descriptor - * @returns {GPUVertexState} - */ - _getVertexState(descriptor) { + _getVertexState(descriptor: VertexStateDescriptor): GPUVertexState { return { module: this._vertex.module.handle, entryPoint: this._vertex.getEntrypoint('vertex'), @@ -422,12 +341,7 @@ export class ShaderPair { } } - /** - * @param {string} name - * @param {number} group - * @returns {VariableInfo | undefined} - */ - findVariableInfo(name, group) { + findVariableInfo(name: string, group: number): VariableInfo | undefined { let variableInfo = this._vertex.findVariableInfo(name, group) if (!variableInfo && this._fragment !== this._vertex) { @@ -437,11 +351,7 @@ export class ShaderPair { return variableInfo } - /** - * @param {ShaderPairStateDescriptor} descriptor - * @returns {Pick} - */ - getRenderPipelineStates(descriptor) { + getRenderPipelineStates(descriptor: ShaderPairStateDescriptor): Pick { return { fragment: this._getFragmentState(descriptor.fragment), vertex: this._getVertexState(descriptor.vertex), diff --git a/src/resources/texture.js b/src/resources/texture.js deleted file mode 100644 index 33f8aa7..0000000 --- a/src/resources/texture.js +++ /dev/null @@ -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 - } -} - diff --git a/src/resources/texture.ts b/src/resources/texture.ts new file mode 100644 index 0000000..9ce01d6 --- /dev/null +++ b/src/resources/texture.ts @@ -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 + } +} + diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index 436c782..0000000 --- a/src/utils.js +++ /dev/null @@ -1,317 +0,0 @@ -/** - * @template T - * @template {keyof T} K - * @typedef {Pick, K> & Omit} Optional - */ - -/** - * @template T, U - * @typedef { - { [P in keyof T]: T[P] } & - { [P in keyof U]?: never} - * } Only - */ - -/** - * @template T, U - * @typedef {Only | Only} Either - */ - -/** - * @template T - * @typedef {new (...args: any[]) => T} Newable - */ - -/** - * @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} T - * @typedef {T extends readonly [] ? [] : T extends readonly [infer Head extends string, ... infer Tail extends ReadonlyArray] ? [Capitalize, ...CapitalizeAll] : string[]} CapitalizeAll - */ - -/** - * @template {ReadonlyArray} 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] ? `${Head}${Sep}${Join}` : string} Join - */ - -/** - * @template {string} S - * @template {string} D - * @typedef {D extends '' ? [S] : S extends `${infer Head}${D}${infer Tail}` ? [Head, ...Split] : [S]} Split - */ - -/* - * @typedef {'-' | '_' | ' '} WordSeparator - */ - -/** - * @template {string} S - * @template {string} D - * @typedef {S extends `${infer Left}${D}${infer Right}` ? `${PascalCaseFromDelimiter}${Capitalize>}` : S} PascalCaseFromDelimiter - */ - -/** - * @template {string} S - * @typedef {Capitalize, '-'>, '_'>, ' '>>} PascalCaseString - */ - -/** - * @template {string | ReadonlyArray} T - * @typedef {T extends String ? PascalCaseString : T extends ReadonlyArray ? Join, ''> : 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 - : never - * } IndexOf - */ - -/** - * @template {string} T - * @param {T} s - * @returns {Uppercase} - */ -export const uppercase = s => /** @type {Uppercase} */(s.toUpperCase()) - -/** - * @template {string} T - * @param {T} s - * @returns {Lowercase} - */ -export const lowercase = s => /** @type {Lowercase} */(s.toLowerCase()) - -/** - * @template {string} const T - * @param {T} s - * @returns {Split} - */ -export const fromKebab = s => /** @type {Split} */(s.split('-')) - -/** - * @template {string} const T - * @param {T} xs - * @returns {PascalCase} - */ -export const toPascal = xs => /** @type {PascalCase} */(uppercase(xs[0]) + xs.slice(1)) - -/** - * @template {string[]} const T - * @param {T} xs - * @returns {PascalCase} - */ -const pascal = xs => /** @type {PascalCase} */(xs.map(toPascal).join('')) - -/** - * @template {string} const T - * @param {...T} values - * @returns {Readonly<{ [K in T as PascalCase]: K }>} - */ -export const Enum = (...values) => /** @type {Readonly<{ [K in T as PascalCase]: 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 : never]: PowersOfTwo[ParseInt] }>} - */ -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} - */ -export const pick = (obj, ...keys) => /** @type {Pick} */(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} - */ -export const omit = (obj, ...keys) => /** @type {Omit} */(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) - } -} - diff --git a/src/utils/bindings.js b/src/utils/bindings.js deleted file mode 100644 index a30f2f3..0000000 --- a/src/utils/bindings.js +++ /dev/null @@ -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 - */ -export class BindingMap extends Map { } - -/** - * @extends Map - */ -export class GroupBindingMap extends Map { } - - - diff --git a/src/utils/bindings.ts b/src/utils/bindings.ts new file mode 100644 index 0000000..7c838e5 --- /dev/null +++ b/src/utils/bindings.ts @@ -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 { } + +export class GroupBindingMap extends Map { } + + + diff --git a/src/utils/bitflags.js b/src/utils/bitflags.js deleted file mode 100644 index 5402171..0000000 --- a/src/utils/bitflags.js +++ /dev/null @@ -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) - } -} - diff --git a/src/utils/bitflags.ts b/src/utils/bitflags.ts new file mode 100644 index 0000000..2b5333a --- /dev/null +++ b/src/utils/bitflags.ts @@ -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) + } +} + diff --git a/src/utils/errors.js b/src/utils/errors.ts similarity index 67% rename from src/utils/errors.js rename to src/utils/errors.ts index 12f8d38..bb65a6b 100644 --- a/src/utils/errors.js +++ b/src/utils/errors.ts @@ -1,11 +1,7 @@ -/** @import { Newable } from '../utils.js' */ +import { Newable } from './index.js' export class WebGPUError extends Error { - /** - * @param {string} message - * @param {ErrorOptions} [options] - */ - constructor(message, options) { + constructor(message: string, options?: ErrorOptions) { super(`WebGPUError: ${message}`, options) } @@ -39,30 +35,18 @@ export class CommandRecorderError extends Error { } export class WebGPUObjectError extends Error { - /** - * @template T - * @param {Error} cause - * @param {string | Newable} [type] - */ - static from(cause, type) { + static from(cause: T, type: string | Newable) { 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) { + constructor(message: string, options?: ErrorOptions) { super(`BufferError: ${message}`, options) } - /** - * @param {Error} cause - */ - static from(cause) { + static from(cause: T) { 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') } - /** - * @param {number} offset - * @param {number} size - */ - static outOfBounds(offset, size) { + static outOfBounds(offset: number, size: number) { return new BufferError(`buffer offset/size (${offset}/${size}) exceeds buffer dimensions`) } } export class MaterialError extends WebGPUObjectError { - /** - * @param {string} message - * @param {ErrorOptions} [options] - */ - constructor(message, options) { + constructor(message: string, options?: ErrorOptions) { super(`MaterialError: ${message}`, options) } - /** - * @param {Error} cause - */ - static from(cause) { + static from(cause: T) { return new BufferError('could not create material', { cause }) } - /** - * @param {string} shaderType - */ - static missingShader(shaderType) { + static missingShader(shaderType: string) { return new BufferError(`missing ${shaderType} shader`) } } diff --git a/src/utils/events.js b/src/utils/events.ts similarity index 63% rename from src/utils/events.js rename to src/utils/events.ts index 538d45c..dc9596f 100644 --- a/src/utils/events.js +++ b/src/utils/events.ts @@ -2,24 +2,18 @@ import { GraphicsDevice } from '../core/graphics-device.js' export class GraphicsDeviceInitialized { static EventName = 'graphics-device:initialized' - graphicsDevice + graphicsDevice: GraphicsDevice - /** - * @param {GraphicsDevice} graphicsDevice - */ - constructor(graphicsDevice) { + constructor(graphicsDevice: GraphicsDevice) { this.graphicsDevice = graphicsDevice } } export class GraphicsDeviceLost { static EventName = 'graphics-device:device-lost' - info + info: GPUDeviceLostInfo - /** - * @param {GPUDeviceLostInfo} info - */ - constructor(info) { + constructor(info: GPUDeviceLostInfo) { this.info = info } } diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..98d9d29 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,160 @@ +export type Optional = Pick, K & Omit> + +export type Only = { [P in keyof T]: T[P] } & { [P in keyof U]?: never } + +export type Either = Only | Only + +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 `-${string}` | '0' | `${string}.${string}` ? never : T + +export type CapitalizeAll> = T extends readonly [] ? [] : T extends readonly [infer Head extends string, ...infer Tail extends ReadonlyArray] ? [Capitalize, ...CapitalizeAll] : string[] + +export type Join, Sep extends string = ''> = T extends readonly [] ? '' : T extends readonly [infer Head] ? Head : T extends readonly [infer Head extends string, ...infer Tail extends ReadonlyArray] ? `${Head}${Sep}${Join}` : string + +export type Split = D extends '' ? [S] : S extends `${infer Head}${D}${infer Tail}` ? [Head, ...Split] : [S] + +export type PascalCaseFromDelimiter = S extends `${infer Left}${D}${infer Right}` ? `${PascalCaseFromDelimiter}${Capitalize>}` : S + +export type PascalCaseString = Capitalize, '-'>, '_'>, ' '>> + +export type PascalCase> = T extends string ? PascalCaseString : T extends ReadonlyArray ? Join, ''> : T + +export type IndexOf = T extends readonly [infer Head, ...infer Tail] + ? Head extends V ? Acc['length'] : IndexOf + : never + +export const uppercase = (s: T) => s.toUpperCase() as Uppercase + +export const lowercase = (s: T) => s.toLowerCase() as Lowercase + +export const split = (delim: D, s: T) => s.split(delim) as Split + +export const fromKebab = (s: T) => split('-', s) as Split + +export const toPascal = (xs: T) => uppercase(xs[0]) + xs.slice(1) as PascalCase + +const pascal = (xs: T) => xs.map(toPascal).join('') as PascalCase + +export const Enum = (...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 }> + +type ParseInt = T extends `${infer N extends number}` ? N : never + +type FlagEnum = Readonly<{ [K in keyof A as A[K] extends T ? PascalCase : never]: PowersOfTwo[ParseInt] }> +export const FlagEnum = (...values: A) => Object.freeze( + values.reduce((acc, x, i) => { + const key = pascal(fromKebab(x)).toString() + acc[key] = 1 << i + return acc + }, {}) +) as FlagEnum + + +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 = (obj: T, ...keys: K[]): Pick => (Object.fromEntries( + keys + .filter(key => key in obj) + .map(key => [key, obj[key]]) +)) as Pick + +export const inclusivePick = (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 = (obj: T, ...keys: K[]) => Object.fromEntries( + Object.entries(obj) + .filter(([key]: K[]) => !keys.includes(key)) +) as Omit + +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) + } +} + diff --git a/src/utils/internal-enums.js b/src/utils/internal-enums.ts similarity index 73% rename from src/utils/internal-enums.js rename to src/utils/internal-enums.ts index c2718ff..3167a7b 100644 --- a/src/utils/internal-enums.js +++ b/src/utils/internal-enums.ts @@ -1,4 +1,4 @@ -import { FlagEnum } from '../utils.js' +import { FlagEnum } from '../utils/index.js' export const ShaderStage = FlagEnum( 'vertex', @@ -6,11 +6,7 @@ export const ShaderStage = FlagEnum( 'compute' ) -/** - * @param {GPUShaderStageFlags} stages - * @returns {string[]} - */ -export const stageFlagToName = stages => { +export const stageFlagToName = (stages: GPUShaderStageFlags): string[] => { const names = [] if (stages & GPUShaderStage.FRAGMENT) { @@ -28,10 +24,9 @@ export const stageFlagToName = stages => { return names } -/** - * @param {('fragment' | 'vertex' | 'compute')[]} names - */ -export const nameToStageFlag = names => { +export type StageFlag = 'fragment' | 'vertex' | 'compute' + +export const nameToStageFlag = (names: StageFlag[]) => { return names.reduce((flags, name) => { switch (name.toLowerCase()) { case 'fragment': return flags | GPUShaderStage.FRAGMENT diff --git a/src/utils/mip-generator.ts b/src/utils/mip-generator.ts new file mode 100644 index 0000000..f9a7053 --- /dev/null +++ b/src/utils/mip-generator.ts @@ -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 + + _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 + } +} + diff --git a/src/utils/mip-shader.wgsl b/src/utils/mip-shader.wgsl new file mode 100644 index 0000000..8daaff7 --- /dev/null +++ b/src/utils/mip-shader.wgsl @@ -0,0 +1,23 @@ +var pos : array, 3> = array, 3>( + vec2(-1.0, -1.0), vec2(-1.0, 3.0), vec2(3.0, -1.0)); + +struct VertexOutput { + @builtin(position) position : vec4, + @location(0) texCoord : vec2, +}; + +@vertex +fn vertexMain(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput { + var output : VertexOutput; + output.texCoord = pos[vertexIndex] * vec2(0.5, -0.5) + vec2(0.5); + output.position = vec4(pos[vertexIndex], 0.0, 1.0); + return output; +} + +@group(0) @binding(0) var imgSampler : sampler; +@group(0) @binding(1) var img : texture_2d; + +@fragment +fn fragmentMain(@location(0) texCoord : vec2) -> @location(0) vec4 { + return textureSample(img, imgSampler, texCoord); +} diff --git a/src/utils/wgsl-to-wgpu.js b/src/utils/wgsl-to-wgpu.ts similarity index 100% rename from src/utils/wgsl-to-wgpu.js rename to src/utils/wgsl-to-wgpu.ts diff --git a/jsconfig.json b/tsconfig.json similarity index 80% rename from jsconfig.json rename to tsconfig.json index 835507d..145c729 100644 --- a/jsconfig.json +++ b/tsconfig.json @@ -1,11 +1,11 @@ { "compilerOptions": { - "module": "es2022", + "module": "esnext", "moduleResolution": "node", "target": "es6", "lib": ["es2022", "dom"], "types": ["@webgpu/types"], - "checkJs": "true" + "checkJs": true }, "exclude": [ "node_modules"