move to ts
This commit is contained in:
parent
d75f3b4c49
commit
95b25c962a
33 changed files with 1020 additions and 1103 deletions
265
dist/index.js
vendored
265
dist/index.js
vendored
|
@ -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<T>} [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<T>} */
|
||||
s2.toUpperCase()
|
||||
);
|
||||
var fromKebab = (s2) => (
|
||||
/** @type {Split<T, '-'>} */
|
||||
s2.split("-")
|
||||
);
|
||||
var toPascal = (xs) => (
|
||||
/** @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(
|
||||
// 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 FlagEnum = (...values) => (
|
||||
/** @type {never} */
|
||||
Object.freeze(
|
||||
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
|
||||
);
|
||||
|
|
17
package-lock.json
generated
17
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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<GPUBufferDescriptor, 'mappedAtCreation'>} 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<BindGroupLayout>} layouts
|
||||
* @param {string} [label]
|
||||
*/
|
||||
createPipelineLayout(layouts, label) {
|
||||
createPipelineLayout(layouts: Array<BindGroupLayout>, 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)
|
|
@ -1,21 +1,17 @@
|
|||
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 {
|
||||
_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<T>} width
|
||||
* @param {PositiveInteger<T>} height
|
||||
*/
|
||||
resize(width, height) {
|
||||
resize<const T extends number>(width: PositiveInteger<T>, height: PositiveInteger<T>) {
|
||||
if (width <= 0 || height <= 0) {
|
||||
return
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { Enum } from './utils.js'
|
||||
import { Enum } from './utils/index.js'
|
||||
|
||||
|
||||
export const AddressMode = Enum(
|
|
@ -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<TypedArray, BigInt64Array | BigUint64Array>
|
||||
type SmallTypedArrayConstructor = Exclude<TypedArrayConstructor, BigInt64ArrayConstructor | BigUint64ArrayConstructor>
|
||||
|
||||
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<TypedArray, BigInt64Array | BigUint64Array>} SmallTypedArray
|
||||
* @typedef {Exclude<TypedArrayConstructor, BigInt64ArrayConstructor | BigUint64ArrayConstructor>} SmallTypedArrayConstructor
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {SmallTypedArray | DataView | undefined} [out]
|
||||
* @param {number} [byteOffset=0]
|
||||
* @param {number} [byteSize]
|
||||
*/
|
||||
async read(out, byteOffset = 0, byteSize = -1) {
|
||||
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<ArrayBufferLike>
|
||||
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<GPUBufferDescriptor, 'usage'>} descriptor
|
||||
*/
|
||||
static create(device, descriptor) {
|
||||
static create(device: GPUDevice, descriptor: Omit<GPUBufferDescriptor, 'usage'>) {
|
||||
const usage = GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
||||
return super.create(device, {
|
||||
usage,
|
0
src/resources/newFile.ts
Normal file
0
src/resources/newFile.ts
Normal file
|
@ -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<string, GPUPipelineConstantValue>} [constants={}]
|
||||
* @property {GPUColorTargetState[]} [targets=[]]
|
||||
*
|
||||
* @typedef VertexStateDescriptor
|
||||
* @property {Record<string, GPUPipelineConstantValue>} [constants={}]
|
||||
* @property {GPUVertexBufferLayout[]} [buffers=[]]
|
||||
*
|
||||
* @typedef ShaderPairStateDescriptor
|
||||
* @property {FragmentStateDescriptor} [fragment]
|
||||
* @property {VertexStateDescriptor} vertex
|
||||
*
|
||||
*/
|
||||
export interface FragmentStateDescriptor {
|
||||
constants?: Record<string, GPUPipelineConstantValue>
|
||||
targets?: GPUColorTargetState[]
|
||||
}
|
||||
|
||||
export interface VertexStateDescriptor {
|
||||
constants?: Record<string, GPUPipelineConstantValue>
|
||||
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<any, any>} map
|
||||
* @returns {number[]}
|
||||
*/
|
||||
static _sortKeyIndices(map) {
|
||||
static _sortKeyIndices(map: Map<any, any>): 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,
|
||||
static fromPair(value: {
|
||||
vertex: ShaderModule
|
||||
fragment?: ShaderModule
|
||||
* }} value
|
||||
*/
|
||||
static fromPair(value) {
|
||||
}) {
|
||||
const vert = new ReflectedShader(value.vertex)
|
||||
const frag = value.fragment && new ReflectedShader(value.fragment)
|
||||
return new ShaderPair(vert, frag)
|
||||
|
@ -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<GPURenderPipelineDescriptor, 'vertex' | 'fragment'>}
|
||||
*/
|
||||
getRenderPipelineStates(descriptor) {
|
||||
getRenderPipelineStates(descriptor: ShaderPairStateDescriptor): Pick<GPURenderPipelineDescriptor, 'vertex' | 'fragment'> {
|
||||
return {
|
||||
fragment: this._getFragmentState(descriptor.fragment),
|
||||
vertex: this._getVertexState(descriptor.vertex),
|
|
@ -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
230
src/resources/texture.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
317
src/utils.js
317
src/utils.js
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
18
src/utils/bindings.ts
Normal 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> { }
|
||||
|
||||
|
||||
|
|
@ -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
28
src/utils/bitflags.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<T>} [type]
|
||||
*/
|
||||
static from(cause, type) {
|
||||
static from<T extends Error>(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<T extends Error>(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<T extends Error>(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`)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
160
src/utils/index.ts
Normal file
160
src/utils/index.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
188
src/utils/mip-generator.ts
Normal file
188
src/utils/mip-generator.ts
Normal 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
23
src/utils/mip-shader.wgsl
Normal 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);
|
||||
}
|
|
@ -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"
|
Loading…
Add table
Reference in a new issue