diff --git a/src/core/command-recorder.ts b/src/core/command-recorder.ts index d2cecd4..29e927a 100644 --- a/src/core/command-recorder.ts +++ b/src/core/command-recorder.ts @@ -2,7 +2,7 @@ import { LoadOp, StoreOp } from '../enum.js' import { CommandRecorderError } from '../utils/errors.js' import { SwapChain } from './swap-chain.js' -export class CommandRecorder { +export class CommandEncoder implements GPUCommandEncoder { static _defaultClearValue = { r: 0, g: 0, b: 0, a: 1 } _device: GPUDevice @@ -29,24 +29,24 @@ export class CommandRecorder { return [{ view, - clearValue: CommandRecorder._defaultClearValue, + clearValue: CommandEncoder._defaultClearValue, loadOp: LoadOp.Clear, storeOp: StoreOp.Store }] } - beginRenderPass(colorAttachments?: GPURenderPassColorAttachment[], depthStencilAttachment?: GPURenderPassDepthStencilAttachment): GPURenderPassEncoder { + beginRenderPass(descriptor: GPURenderPassDescriptor): GPURenderPassEncoder { if (this._passEncoder) { throw CommandRecorderError.activeRenderPass() } const attachments = colorAttachments || this._defaultColorAttachment() - const descriptor = { - label: this.label || 'RenderPass', - colorAttachments: attachments, - depthStencilAttachment - } + //const descriptor = { + // label: this.label || 'RenderPass', + // colorAttachments: attachments, + // depthStencilAttachment + //} this._passEncoder = this._encoder.beginRenderPass(descriptor) diff --git a/src/core/graphics-device.ts b/src/core/graphics-device.ts index 0590701..32d460d 100644 --- a/src/core/graphics-device.ts +++ b/src/core/graphics-device.ts @@ -1,11 +1,10 @@ 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/index.js' +import { EventEmitter, GraphicsDeviceInitialized, GraphicsDeviceLost } from '../utils/events.js' import { ShaderModule, ShaderPairDescriptor } from '../resources/shader-module.js' import { RenderPipeline } from '../rendering/render-pipeline.js' -import { CommandRecorder } from './command-recorder.js' +import { CommandEncoder } 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' @@ -125,7 +124,9 @@ class DeviceHandler { } } -export class GraphicsDevice extends EventEmitter { +export class GraphicsDevice extends EventEmitter implements GPUDevice { + __brand: 'GPUDevice' + _canvas: HTMLCanvasElement _deviceHandler: DeviceHandler _swapChain: SwapChain @@ -153,6 +154,34 @@ export class GraphicsDevice extends EventEmitter { return this._swapChain } + get features() { + return this.device.features + } + + get limits() { + return this.device.limits + } + + get adapterInfo() { + return this.device.adapterInfo + } + + get lost() { + return this.device.lost + } + + get onuncapturederror() { + return this.device.onuncapturederror + } + + set onuncapturederror(value) { + this.device.onuncapturederror = value + } + + get label() { + return this.device.label + } + constructor(canvas: HTMLCanvasElement, deviceHandler: DeviceHandler) { super() @@ -160,6 +189,34 @@ export class GraphicsDevice extends EventEmitter { this._deviceHandler = deviceHandler } + importExternalTexture(descriptor: GPUExternalTextureDescriptor): GPUExternalTexture { + throw new Error('Method not implemented.') + } + createComputePipeline(descriptor: GPUComputePipelineDescriptor): GPUComputePipeline { + throw new Error('Method not implemented.') + } + createComputePipelineAsync(descriptor: GPUComputePipelineDescriptor): Promise { + throw new Error('Method not implemented.') + } + createRenderPipelineAsync(descriptor: GPURenderPipelineDescriptor): Promise { + throw new Error('Method not implemented.') + } + createCommandEncoder(descriptor?: GPUCommandEncoderDescriptor): GPUCommandEncoder { + throw new Error('Method not implemented.') + } + createRenderBundleEncoder(descriptor: GPURenderBundleEncoderDescriptor): GPURenderBundleEncoder { + throw new Error('Method not implemented.') + } + createQuerySet(descriptor: GPUQuerySetDescriptor): GPUQuerySet { + throw new Error('Method not implemented.') + } + pushErrorScope(filter: GPUErrorFilter): undefined { + throw new Error('Method not implemented.') + } + popErrorScope(): Promise { + throw new Error('Method not implemented.') + } + static build(canvas?: HTMLCanvasElement) { return new GraphicsDeviceBuilder(canvas) } @@ -226,11 +283,11 @@ export class GraphicsDevice extends EventEmitter { return buffer } - createShaderModule(code: string, label: string): ShaderModule { + createShaderModule(descriptor: GPUShaderModuleDescriptor): ShaderModule { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } try { - return ShaderModule.create(this.device, { code, label }) + return ShaderModule.create(this.device, descriptor) } catch (err) { throw WebGPUObjectError.from(err, ShaderModule) } @@ -257,19 +314,16 @@ export class GraphicsDevice extends EventEmitter { } } - createCommandRecorder(label?: string): CommandRecorder { + createCommandRecorder(label?: string): CommandEncoder { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } - return new CommandRecorder(this.device, this._swapChain, label) + return new CommandEncoder(this.device, this._swapChain, label) } - createBindGroupLayout(entries: GPUBindGroupLayoutEntry[], label: string) { + createBindGroupLayout(descriptor: GPUBindGroupLayoutDescriptor) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } - return BindGroupLayout.create(this.device, { - label, - entries - }) + return BindGroupLayout.create(this.device, descriptor) } _getBindingResource(binding: BindGroupEntry): GPUBindingResource { @@ -303,31 +357,24 @@ export class GraphicsDevice extends EventEmitter { } } - createBindGroup(layout: BindGroupLayout, bindings: BindGroupEntry[], label?: string) { + createBindGroup(descriptor: GPUBindGroupDescriptor) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } - const entries = bindings.map(def => ({ - binding: def.binding, - resource: this._getBindingResource(def) - })) + //const entries = bindings.map(def => ({ + // binding: def.binding, + // resource: this._getBindingResource(def) + //})) - return BindGroup.create(this.device, { - layout: layout.handle, - entries, - label - }) + return BindGroup.create(this.device, descriptor) } - createPipelineLayout(layouts: Array, label?: string) { + createPipelineLayout(descriptor: GPUPipelineLayoutDescriptor) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } - const bindGroupLayouts = layouts.map(layout => layout.handle) + //const bindGroupLayouts = layouts.map(layout => layout.handle) - return this.device.createPipelineLayout({ - label, - bindGroupLayouts - }) + return this.device.createPipelineLayout(descriptor) } createSampler(descriptor?: GPUSamplerDescriptor) { @@ -389,10 +436,7 @@ export class GraphicsDevice extends EventEmitter { this.queue.submit(commandBuffers) } - /** - * Cleans up GPU resources. Call when the application exits. - */ - destroy() { + destroy(): undefined { if (this.device) { this.device.destroy() } diff --git a/src/rendering/render-pipeline.ts b/src/rendering/render-pipeline.ts index 93cb3f7..3166985 100644 --- a/src/rendering/render-pipeline.ts +++ b/src/rendering/render-pipeline.ts @@ -1,4 +1,6 @@ -export class RenderPipeline { +export class RenderPipeline implements GPURenderPipeline { + __brand: 'GPURenderPipeline' + _handle: GPURenderPipeline _label: string @@ -14,5 +16,9 @@ export class RenderPipeline { this._handle = pipeline this._label = label } + + getBindGroupLayout(index: number): GPUBindGroupLayout { + return this.getBindGroupLayout(index) + } } diff --git a/src/resources/bind-group-layout.ts b/src/resources/bind-group-layout.ts index 3fc44e7..13d1a6e 100644 --- a/src/resources/bind-group-layout.ts +++ b/src/resources/bind-group-layout.ts @@ -1,6 +1,8 @@ import { WebGPUObjectError } from '../utils/errors.js' -export class BindGroupLayout { +export class BindGroupLayout implements GPUBindGroupLayout { + __brand: 'GPUBindGroupLayout' + _device: GPUDevice _handle: GPUBindGroupLayout diff --git a/src/resources/bind-group.ts b/src/resources/bind-group.ts index 260918e..a2400b5 100644 --- a/src/resources/bind-group.ts +++ b/src/resources/bind-group.ts @@ -1,6 +1,8 @@ import { WebGPUObjectError } from '../utils/errors.js' -export class BindGroup { +export class BindGroup implements GPUBindGroup { + __brand: 'GPUBindGroup' + _device: GPUDevice _handle: GPUBindGroup @@ -8,6 +10,10 @@ export class BindGroup { return this._handle } + get label() { + return this._handle.label + } + /** * @param {GPUDevice} device * @param {GPUBindGroup} bindGroup diff --git a/src/resources/buffer.ts b/src/resources/buffer.ts index 8e5951d..44b48c3 100644 --- a/src/resources/buffer.ts +++ b/src/resources/buffer.ts @@ -5,7 +5,9 @@ import { ResourceType } from '../utils/internal-enums.js' type SmallTypedArray = Exclude type SmallTypedArrayConstructor = Exclude -export class Buffer { +export class Buffer implements GPUBuffer { + __brand: 'GPUBuffer' + _device: GPUDevice _handle: GPUBuffer @@ -25,6 +27,14 @@ export class Buffer { return this._handle.usage } + get mapState() { + return this._handle.mapState + } + + get label() { + return this._handle.label + } + get resourceType() { return ResourceType.Buffer } @@ -33,7 +43,6 @@ export class Buffer { this._device = device this._handle = texture } - static create(device: GPUDevice, descriptor: GPUBufferDescriptor) { try { return new Buffer( @@ -153,7 +162,20 @@ export class Buffer { } } - destroy() { + mapAsync(mode: GPUMapModeFlags, offset?: GPUSize64, size?: GPUSize64): Promise { + return this._handle.mapAsync(mode, offset, size) + } + + getMappedRange(offset?: GPUSize64, size?: GPUSize64): ArrayBuffer { + return this._handle.getMappedRange(offset, size) + } + + unmap(): undefined { + this._handle.unmap() + } + + + destroy(): undefined { this._handle?.destroy() this._handle = null } diff --git a/src/resources/sampler.ts b/src/resources/sampler.ts index 0a755b3..c9ce882 100644 --- a/src/resources/sampler.ts +++ b/src/resources/sampler.ts @@ -1,9 +1,9 @@ import { WebGPUObjectError } from '../utils/errors.js' import { ResourceType } from '../utils/internal-enums.js' -/** @import { BindGroupEntry } from '../core/graphics-device.js' */ +export class Sampler implements GPUSampler { + __brand: 'GPUSampler' -export class Sampler { _device: GPUDevice _handle: GPUSampler diff --git a/src/resources/shader-module.ts b/src/resources/shader-module.ts index 0bdc91f..27eba70 100644 --- a/src/resources/shader-module.ts +++ b/src/resources/shader-module.ts @@ -13,9 +13,8 @@ import { } from '../utils/wgsl-to-wgpu.js' import { BufferBindingType } from '../enum.js' -/** @import { WGSLAccess, WGSLSamplerType } from '../utils/wgsl-to-wgpu.js' */ - -export class ShaderModule { +export class ShaderModule implements GPUShaderModule { + __brand: 'GPUShaderModule' _handle: GPUShaderModule _code: string @@ -44,14 +43,17 @@ export class ShaderModule { } reflect() { - if (this._reflection == null) { + if (!this._reflection) { this._reflection = new WgslReflect(this._code) - // no longer needed allow the GC to collect it this._code = undefined } return this._reflection } + + getCompilationInfo(): Promise { + return this._handle.getCompilationInfo() + } } export interface FragmentStateDescriptor { @@ -83,7 +85,7 @@ export class ReflectedShader { } - constructor(shader: ShaderModule) { + constructor(shader: ShaderModule, reflection: WgslReflect) { this._module = shader } diff --git a/src/resources/texture.ts b/src/resources/texture.ts index 5f6fa9e..fd7205b 100644 --- a/src/resources/texture.ts +++ b/src/resources/texture.ts @@ -3,18 +3,19 @@ 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' */ interface UploadTextureInfo { destination: GPUTexelCopyTextureInfo dataLayout: GPUTexelCopyBufferLayout size: GPUExtent3DStrict } -export class Texture { +export class Texture implements GPUTexture { static _defaultUsage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT + __brand: 'GPUTexture' + _device: GPUDevice _handle: GPUTexture @@ -48,6 +49,10 @@ export class Texture { return this._handle.dimension } + get sampleCount() { + return this._handle.sampleCount + } + get mipLevelCount() { return this._handle.mipLevelCount } @@ -81,7 +86,6 @@ export class Texture { return 1 + Math.log2(max) | 0 } - static async fromUrl(device: GPUDevice, url: string | URL, descriptor: GPUTextureDescriptor) { try { const response = await fetch(url) @@ -127,7 +131,6 @@ export class Texture { }) } - upload(source: GPUAllowSharedBufferSource, options?: UploadTextureInfo) { const mipLevel = options.destination.mipLevel || 0 const size = options.size || [ @@ -148,8 +151,8 @@ export class Texture { } } - generateMipmaps(_commandEncoder: GPUCommandEncoder) { - // TODO: use MipGenerator + createView(descriptor?: GPUTextureViewDescriptor) { + return this._handle.createView(descriptor) } createDefaultView(descriptor?: GPUTextureViewDescriptor) { @@ -178,7 +181,7 @@ export class Texture { return this.getView() } - destroy() { + destroy(): undefined { this._handle?.destroy() this._handle = undefined this._defaultView = undefined diff --git a/src/utils/events.ts b/src/utils/events.ts index dc9596f..3f0b2a1 100644 --- a/src/utils/events.ts +++ b/src/utils/events.ts @@ -1,5 +1,54 @@ import { GraphicsDevice } from '../core/graphics-device.js' +interface Listener { + (...args: any): void +} + +export class EventEmitter { + _listeners = {} + + addEventListener(type: PropertyKey, listener: Listener, options?: object): void { + this.on(type, listener, options) + } + + removeEventListener(type: PropertyKey, listener: Listener, options?: object): void { + this.off(type, listener, options) + } + + dispatchEvent(event: Event): boolean { + const details = 'detail' in event ? event.detail : undefined + return this.emit(event.type, ...Object.values(details)) + } + + on(event: PropertyKey, callback: Listener, _options?: object) { + 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) + ) + } + + return true + } + + off(event: PropertyKey, callback: Listener, _options?: object) { + const listeners = this._listeners[event] + + if (listeners) { + this._listeners[event] = listeners.filter( + (cb: Listener) => cb !== callback + ) + } + } +} + + export class GraphicsDeviceInitialized { static EventName = 'graphics-device:initialized' graphicsDevice: GraphicsDevice diff --git a/src/utils/index.ts b/src/utils/index.ts index 98d9d29..03effbf 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -88,38 +88,6 @@ export const FlagEnum = (...values: ) 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) diff --git a/src/utils/mip-generator.ts b/src/utils/mip-generator.ts index ef55ae5..8a7e730 100644 --- a/src/utils/mip-generator.ts +++ b/src/utils/mip-generator.ts @@ -1,3 +1,6 @@ +// taken from PixiJS +// https://pixijs.download/v8.0.0-rc.2/docs/rendering_renderers_gpu_texture_utils_GpuMipmapGenerator.ts.html + import code from './mip-shader.wgsl' const mip = (n: number) => Math.max(1, n >>> 1) @@ -81,33 +84,37 @@ export class MipGenerator { return pipeline } - generateMipmap(texture: GPUTexture, descriptor: GPUTextureDescriptor) { - const pipeline = this.getPipeline(descriptor.format) + generateMipmap(texture: GPUTexture, commandEncoder?: GPUCommandEncoder): GPUTexture { + const pipeline = this.getPipeline(texture.format) - if (descriptor.dimension !== '2d') { + const { dimension, format, mipLevelCount, width, height, depthOrArrayLayers } = texture + + const encoder = commandEncoder || this._device.createCommandEncoder() + + if (texture.dimension !== '2d') { throw new Error('Generating mipmaps for anything except 2d is unsupported.') } - let mipTexture = texture - const { width, height, depthOrArrayLayers } = descriptor.size as GPUExtent3DDict + let tempTexture = texture - const renderToSource = descriptor.usage & GPUTextureUsage.RENDER_ATTACHMENT + + const renderToSource = texture.usage & GPUTextureUsage.RENDER_ATTACHMENT if (!renderToSource) { - mipTexture = this._device.createTexture({ + tempTexture = this._device.createTexture({ size: { width: mip(width), height: mip(height), depthOrArrayLayers }, - format: descriptor.format, + format, usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT, - mipLevelCount: descriptor.mipLevelCount - 1 + mipLevelCount: mipLevelCount - 1, + dimension, + sampleCount: 1 }) } - const encoder = this._device.createCommandEncoder({}) - for (let layer = 0; layer < depthOrArrayLayers; ++layer) { let srcView = texture.createView({ baseMipLevel: 0, @@ -117,10 +124,9 @@ export class MipGenerator { arrayLayerCount: 1 }) - let dstMipLevel = renderToSource ? 1 : 0 - for (let i = 1; i < descriptor.mipLevelCount; ++i) { - const dstView = mipTexture.createView({ - baseMipLevel: dstMipLevel++, + for (let i = 1; i < mipLevelCount; ++i) { + const dstView = tempTexture.createView({ + baseMipLevel: i - (renderToSource ? 0 : 1), mipLevelCount: 1, dimension: '2d', baseArrayLayer: layer, @@ -156,33 +162,42 @@ export class MipGenerator { } if (!renderToSource) { - const mipLevelSize = { + const mipSize = { width: mip(width), height: mip(height), depthOrArrayLayers } - for (let i = 1; i < descriptor.mipLevelCount; ++i) { + for (let i = 1; i < texture.mipLevelCount; ++i) { encoder.copyTextureToTexture({ - texture: mipTexture, + texture: tempTexture, mipLevel: i - 1 }, { texture, mipLevel: i, - }, mipLevelSize) + }, mipSize) - mipLevelSize.width = mip(mipLevelSize.width) - mipLevelSize.height = mip(mipLevelSize.height) + mipSize.width = mip(mipSize.width) + mipSize.height = mip(mipSize.height) } } this._device.queue.submit([encoder.finish()]) if (!renderToSource) { - mipTexture.destroy() + tempTexture.destroy() } return texture } + + destroy() { + this._device = undefined + this._sampler = undefined + this._shader = undefined + this._pipelines = undefined + this._bindGroupLayout = undefined + this._pipelineLayout = undefined + } } diff --git a/src/utils/mip-shader.wgsl b/src/utils/mip-shader.wgsl index 8daaff7..5070141 100644 --- a/src/utils/mip-shader.wgsl +++ b/src/utils/mip-shader.wgsl @@ -7,7 +7,7 @@ struct VertexOutput { }; @vertex -fn vertexMain(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput { +fn vs_main(@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); @@ -18,6 +18,6 @@ fn vertexMain(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput { @group(0) @binding(1) var img : texture_2d; @fragment -fn fragmentMain(@location(0) texCoord : vec2) -> @location(0) vec4 { +fn fs_main(@location(0) texCoord : vec2) -> @location(0) vec4 { return textureSample(img, imgSampler, texCoord); }