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 { 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' 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: HTMLCanvasElement _adapterOptions: GPURequestAdapterOptions _deviceDescriptor: GPUDeviceDescriptor get canvas() { return this._canvas } constructor(canvasElement?: HTMLCanvasElement) { this._canvas = canvasElement } isSupported() { return navigator.gpu } withCanvas(canvasElement: HTMLCanvasElement) { this._canvas = canvasElement return this } withAdapter(options: GPURequestAdapterOptions) { if (!this.isSupported()) { throw WebGPUError.unsupported() } this._adapterOptions = options return this } withDevice(options: GPUDeviceDescriptor) { if (!this.isSupported()) { throw WebGPUError.unsupported() } this._deviceDescriptor = options return this } async build() { return new GraphicsDevice( this._canvas, new DeviceHandler( this._adapterOptions, this._deviceDescriptor ) ) } } class DeviceHandler { _adapterOptions: GPURequestAdapterOptions _adapter: GPUAdapter _deviceDescriptor: GPUDeviceDescriptor _device: GPUDevice get adapter() { return this._adapter } get device() { return this._device } constructor(adapterOptions: GPURequestAdapterOptions, deviceDescriptor: GPUDeviceDescriptor) { this._adapterOptions = adapterOptions this._deviceDescriptor = deviceDescriptor } async create() { this._adapter = await navigator.gpu.requestAdapter(this._adapterOptions) if (!this._adapter) { throw WebGPUError.adapterUnavailable() } this._device = await this._adapter.requestDevice(this._deviceDescriptor) if (!this._device) { throw WebGPUError.deviceUnavailable() } } } export class GraphicsDevice extends EventEmitter { _canvas: HTMLCanvasElement _deviceHandler: DeviceHandler _swapChain: SwapChain _queue: GPUQueue _isInitialized = false get isInitialized() { return this._isInitialized } get adapter() { return this._deviceHandler.adapter } get device() { return this._deviceHandler.device } get queue() { return this._queue } get swapChain() { return this._swapChain } constructor(canvas: HTMLCanvasElement, deviceHandler: DeviceHandler) { super() this._canvas = canvas this._deviceHandler = deviceHandler } static build(canvas: HTMLCanvasElement) { return new GraphicsDeviceBuilder(canvas) } async initialize() { await this._deviceHandler.create() this._swapChain = new SwapChain( this._canvas, this._deviceHandler.device, ) this._swapChain.configure() this._queue = this.device.queue this._deviceHandler.device.lost.then(info => { this._isInitialized = false this.emit( GraphicsDeviceLost.EventName, new GraphicsDeviceLost(info) ) }) this._isInitialized = true this.emit( GraphicsDeviceInitialized.EventName, new GraphicsDeviceInitialized(this) ) return true } createBuffer(descriptor: GPUBufferDescriptor, data: ArrayBufferView | ArrayBuffer): Buffer { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } try { const buffer = Buffer.create(this.device, descriptor) if (data) { buffer.write(data) } return buffer } catch (err) { throw BufferError.from(err) } } createUniformBuffer(size: number, data: ArrayBufferView | ArrayBuffer, label: string) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } const buffer = UniformBuffer.create(this.device, { size, label }) if (data) { buffer.write(data) } return buffer } createShaderModule(code: string, label: string): ShaderModule { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } try { return ShaderModule.create(this.device, { code, label }) } catch (err) { throw WebGPUObjectError.from(err, ShaderModule) } } createRenderPipeline(descriptor: GPURenderPipelineDescriptor): RenderPipeline { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } try { const pipeline = this.device.createRenderPipeline(descriptor) return new RenderPipeline(pipeline, descriptor.label) } catch (err) { throw WebGPUObjectError.from(err, RenderPipeline) } } createMaterial(shaders: ShaderPairStateDescriptor) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } try { return new Material(this.device, shaders) } catch (err) { throw WebGPUObjectError.from(err, Material) } } createCommandRecorder(label?: string): CommandRecorder { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } return new CommandRecorder(this.device, this._swapChain, label) } createBindGroupLayout(entries: GPUBindGroupLayoutEntry[], label: string) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } return BindGroupLayout.create(this.device, { label, entries }) } _getBindingResource(binding: BindGroupEntry): GPUBindingResource { const resource = binding.resource switch (resource.resourceType) { case ResourceType.Sampler: return resource.handle case ResourceType.TextureView: return 'textureView' in binding ? binding.textureView : resource.getView() case ResourceType.Buffer: const bufSize = resource.size const offset = 'offset' in binding ? binding.offset : 0 const size = 'size' in binding ? binding.size : bufSize - offset if (offset + size > bufSize) { throw BufferError.outOfBounds(offset, size) } return { buffer: resource.handle, offset, size } } } createBindGroup(layout: BindGroupLayout, bindings: BindGroupEntry[], label?: string) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } const entries = bindings.map(def => ({ binding: def.binding, resource: this._getBindingResource(def) })) return BindGroup.create(this.device, { layout: layout.handle, entries, label }) } createPipelineLayout(layouts: Array, label?: string) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } const bindGroupLayouts = layouts.map(layout => layout.handle) return this.device.createPipelineLayout({ label, bindGroupLayouts }) } createSampler(descriptor?: GPUSamplerDescriptor) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } return Sampler.create(this.device, descriptor) } createTexture(descriptor: GPUTextureDescriptor) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } try { return Texture.create(this.device, descriptor) } catch (err) { throw WebGPUObjectError.from(err, Texture) } } createTextureFromBitmap(bitmap: ImageBitmap, options: BitmapTextureOptions) { if (!this._isInitialized) { throw GraphicsDeviceError.uninitialized() } if (!bitmap) { throw new TypeError('Provided bitmap is null.') } const usage = (options.usage ?? (GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT)) | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST const mipLevelCount = 1 const descriptor: GPUTextureDescriptor = { label: options.label, size: { width: bitmap.width, height: bitmap.height, depthOrArrayLayers: 1 }, format: options.format ?? 'rgba8unorm', usage, dimension: '2d', mipLevelCount, sampleCount: 1, } try { const texture = this.device.createTexture(descriptor) this.queue.copyExternalImageToTexture( { source: bitmap, flipY: options.flipY ?? false }, { texture, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } }, descriptor.size ) const wrapper = new Texture(this.device, texture) return wrapper } catch (err) { throw WebGPUObjectError.from(err, Texture) } } submitCommands(commandBuffers: GPUCommandBuffer[]) { if (!this._isInitialized || !commandBuffers || commandBuffers.length === 0) return this.queue.submit(commandBuffers) } /** * Cleans up GPU resources. Call when the application exits. */ destroy() { if (this.device) { this.device.destroy() } this._deviceHandler = null this._queue = null this._swapChain = null this._isInitialized = false } }