406 lines
9.9 KiB
TypeScript
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
|
|
}
|
|
}
|
|
|