wgpu/src/core/graphics-device.ts

406 lines
9.9 KiB
TypeScript

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<BindGroupLayout>, 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
}
}