wip webgpu interfaces

This commit is contained in:
Rowan 2025-04-24 01:49:29 -05:00
parent 5e6b9c8fb1
commit 9a54bb90c9
13 changed files with 235 additions and 118 deletions

View file

@ -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)

View file

@ -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<GPUComputePipeline> {
throw new Error('Method not implemented.')
}
createRenderPipelineAsync(descriptor: GPURenderPipelineDescriptor): Promise<GPURenderPipeline> {
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<GPUError | null> {
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<BindGroupLayout>, 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()
}

View file

@ -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)
}
}

View file

@ -1,6 +1,8 @@
import { WebGPUObjectError } from '../utils/errors.js'
export class BindGroupLayout {
export class BindGroupLayout implements GPUBindGroupLayout {
__brand: 'GPUBindGroupLayout'
_device: GPUDevice
_handle: GPUBindGroupLayout

View file

@ -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

View file

@ -5,7 +5,9 @@ import { ResourceType } from '../utils/internal-enums.js'
type SmallTypedArray = Exclude<TypedArray, BigInt64Array | BigUint64Array>
type SmallTypedArrayConstructor = Exclude<TypedArrayConstructor, BigInt64ArrayConstructor | BigUint64ArrayConstructor>
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<undefined> {
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
}

View file

@ -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

View file

@ -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<GPUCompilationInfo> {
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
}

View file

@ -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

View file

@ -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

View file

@ -88,38 +88,6 @@ export const FlagEnum = <const T extends string, const A extends T[]>(...values:
) 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)

View file

@ -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
}
}

View file

@ -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<f32>(0.5, -0.5) + vec2<f32>(0.5);
output.position = vec4<f32>(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<f32>;
@fragment
fn fragmentMain(@location(0) texCoord : vec2<f32>) -> @location(0) vec4<f32> {
fn fs_main(@location(0) texCoord : vec2<f32>) -> @location(0) vec4<f32> {
return textureSample(img, imgSampler, texCoord);
}