This commit is contained in:
Rowan 2025-03-31 12:36:07 -05:00
parent c039544ec6
commit 722a484bb5
10 changed files with 430 additions and 544 deletions

2
package-lock.json generated
View file

@ -1,5 +1,5 @@
{
"name": "canvas",
"name": "untitled-game-engine",
"lockfileVersion": 3,
"requires": true,
"packages": {

View file

@ -0,0 +1,427 @@
export class WebGPUError extends Error {
/**
* @param {string} message
*/
constructor(message) {
super(message)
}
static unsupported() {
return new WebGPUError("WebGPU is unsupported in this browser")
}
static adapterUnavailable() {
return new WebGPUError("Could not request a WebGPU adapter.")
}
}
export class WebGPUBuilderError extends Error {
/**
* @param {string} message
*/
constructor(message) {
super(message)
}
/**
* @param {string} property
*/
static missing(property) {
return new WebGPUBuilderError(`Missing required property: ${property}`)
}
}
class GPUCanvasConfigurationBuilder {
/** @type {WebGPUContextBuilder} */
#contextBuilder
/** @type {GPUTextureUsageFlags} */
#usage = 0x10
/** @type {GPUTextureFormat[]} */
#viewFormats = []
/** @type {PredefinedColorSpace} */
#colorSpace = 'srgb'
/** @type {GPUCanvasToneMapping} */
#toneMapping = {}
/** @type {GPUCanvasAlphaMode} */
#alphaMode = 'opaque'
/**
* @param {WebGPUContextBuilder} builder
*/
constructor(builder) {
this.#contextBuilder = builder
}
/**
* @param {GPUTextureUsageFlags} usage
*/
usage(usage) {
this.#usage = usage
return this
}
/**
* @param {GPUTextureUsageFlags} usage
*/
addUsage(usage) {
this.#usage = this.#usage | usage
return this
}
/**
* @param {GPUTextureFormat[]} formats
*/
viewFormats(formats) {
this.#viewFormats = formats
return this
}
/**
* @param {GPUTextureFormat} format
*/
addViewFormat(format) {
this.#viewFormats.push(format)
return this
}
/**
* @param {PredefinedColorSpace} space
*/
colorSpace(space) {
this.#colorSpace = space
return this
}
/**
* @param {GPUCanvasToneMappingMode} mode
*/
toneMappingMode(mode) {
this.#toneMapping = { mode }
return this
}
/**
* @param {GPUCanvasAlphaMode} mode
*/
alphaMode(mode) {
this.#alphaMode = mode
return this
}
/**
* @returns {GPUCanvasBuilderConfiguration}
*/
build() {
return {
usage: this.#usage,
viewFormats: this.#viewFormats,
colorSpace: this.#colorSpace,
toneMapping: this.#toneMapping,
alphaMode: this.#alphaMode
}
}
/**
* @returns {WebGPUContextBuilder}
*/
apply() {
return this.#contextBuilder.configureCanvas(this.build())
}
}
/** @typedef {'core' | 'compatibility'} GPUFeatureLevel */
class GPUAdapterOptionsBuilder {
#contextBuilder
/** @type {GPUFeatureLevel} */
#featureLevel = 'core'
/** @type {GPUPowerPreference} */
#powerPreference
/** @type {boolean} */
#forceFallbackAdapter = false
/** @type {boolean} */
#xrCompatible = false
/**
* @param {WebGPUContextBuilder} builder
*/
constructor(builder) {
this.#contextBuilder = builder
}
/**
* @param {GPUFeatureLevel} featureLevel
*/
featureLevel(featureLevel) {
this.#featureLevel = featureLevel
return this
}
/**
* @param {GPUPowerPreference?} preference
*/
powerPreference(preference) {
this.#powerPreference = preference
return this
}
/**
* @param {boolean} force
*/
forceFallbackAdapter(force) {
this.#forceFallbackAdapter = force
return this
}
/**
* @param {boolean} compatible
*/
xrCompatible(compatible) {
this.#xrCompatible = compatible
return this
}
/**
* @returns {GPURequestAdapterOptions}
*/
build() {
return {
featureLevel: this.#featureLevel,
powerPreference: this.#powerPreference,
forceFallbackAdapter: this.#forceFallbackAdapter,
xrCompatible: this.#xrCompatible
}
}
apply() {
return this.#contextBuilder.adapter(this.build())
}
}
class GPUDeviceDescriptorBuilder {
/** @type {WebGPUContextBuilder} */
#contextBuilder
/** @type {GPUFeatureName[]} */
#features = []
/** @type {Record<string, GPUSize64?>} */
#limits = {}
/**
* @param {WebGPUContextBuilder} builder
*/
constructor(builder) {
this.#contextBuilder = builder
}
/**
* @param {GPUFeatureName[]} features
*/
requiredFeatures(features) {
this.#features = features
return this
}
/**
* @param {GPUFeatureName} feature
*/
addRequiredFeature(feature) {
this.#features.push(feature)
return this
}
/**
* @param {Record<string, GPUSize64?>} limits
*/
requiredLimits(limits) {
this.#limits = limits
return this
}
/**
* @param {string} key
* @param {GPUSize64?} value
*/
setRequiredLimit(key, value) {
this.#limits[key] = value
return this
}
/**
* @returns {GPUDeviceDescriptor}
*/
build() {
return {
requiredFeatures: this.#features,
requiredLimits: this.#limits
}
}
apply() {
return this.#contextBuilder.device(this.build())
}
}
/**
* @typedef {Object} GPUCanvasBuilderConfiguration
* @property {GPUTextureUsageFlags} [usage=0x10]
* @property {GPUTextureFormat[]} [viewFormats=[]]
* @property {PredefinedColorSpace} [colorSpace='srgb']
* @property {GPUCanvasToneMapping} [toneMapping = {}]
* @property {GPUCanvasAlphaMode} [alphaMode='opaque']
*/
export class WebGPUContextBuilder {
/** @type {Promise<GPUAdapter>} */
#adapter
/** @type {GPUCanvasBuilderConfiguration} */
#canvasConfig
/** @type {GPUCanvasContext} */
#context
/** @type {Promise<GPUDevice>} */
#device
/** @type {number} */
#dpr = window.devicePixelRatio || 1
/**
* @param {string} type
*/
#warnDefault(type) {
console.warn(`WARN: Requesting WebGPU ${type} with default options.`)
}
/**
* @param {GPURequestAdapterOptions} [descriptor]
* @returns {WebGPUContextBuilder}
* @throws {WebGPUError} Throws if WebGPU is not supported
*/
adapter(descriptor) {
if (!navigator.gpu) {
throw WebGPUError.unsupported()
}
this.#adapter = navigator.gpu.requestAdapter(descriptor)
return this
}
buildAdapterConfiguration() {
return new GPUAdapterOptionsBuilder(this)
}
/**
* @param {HTMLCanvasElement} canvas
* @returns {WebGPUContextBuilder}
*/
context(canvas) {
this.#context = canvas.getContext('webgpu')
return this
}
buildCanvasConfiguration() {
return new GPUCanvasConfigurationBuilder(this)
}
/**
* @param {GPUDeviceDescriptor} [descriptor]
* @returns {WebGPUContextBuilder}
* @throws {WebGPUError} Throws if WebGPU is not supported
*/
device(descriptor) {
if (!this.#adapter) {
this.#warnDefault('adapter')
this.adapter()
}
this.#device = this.#adapter.then(adapter => adapter.requestDevice(descriptor))
return this
}
buildDeviceDescriptor() {
return new GPUDeviceDescriptorBuilder(this)
}
/**
* @param {GPUCanvasBuilderConfiguration} [configuration]
* @returns {WebGPUContextBuilder}
*/
configureCanvas(configuration) {
this.#canvasConfig = configuration
return this
}
/**
* @returns {Promise<WebGPUContext>}
* @throws {WebGPUBuilderError} Throws if context is undefined or if WebGPU is unsupported
*/
async build() {
if (!this.#context) {
throw WebGPUBuilderError.missing('context')
}
if (!this.#device) {
this.#warnDefault('device')
this.device()
}
const [adapter, device] = await Promise.all([this.#adapter, this.#device])
const format = navigator.gpu.getPreferredCanvasFormat()
if (this.#canvasConfig) {
this.#context.configure({
device,
format,
...this.#canvasConfig
})
}
return new WebGPUContext(
this.#context,
adapter,
device,
format,
this.#dpr
)
}
}
export class WebGPUContext {
#adapter
#context
#device
#format
#dpr
/**
* @param {GPUCanvasContext} context
* @param {GPUAdapter} adapter
* @param {GPUDevice} device
* @param {GPUTextureFormat} format
* @param {number} [dpr]
*/
constructor(context, adapter, device, format, dpr) {
this.#context = context
this.#adapter = adapter
this.#device = device
this.#format = format
this.#dpr = dpr || window.devicePixelRatio || 1
}
get adapter() { return this.#adapter }
get context() { return this.#context }
get device() { return this.#device }
get queue() { return this.#device.queue }
get format() { return this.#format }
get devicePixelRatio() { return this.#dpr }
static create() {
return new WebGPUContextBuilder()
}
}

View file

@ -1,8 +1,8 @@
// https://www.w3.org/TR/webgpu/#enumdef-gpuvertexformat
import { Union } from '/public/vendor/kojima/union.js'
import { capitalizen } from '/src/utils/index'
import { capitalizen } from "/src/utils/index"
// TODO: move this somewhere that makes sense
/** @typedef {Uint8ArrayConstructor | Int8ArrayConstructor | Uint16ArrayConstructor | Int16ArrayConstructor | Uint32ArrayConstructor | Int32ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor} TypedArrayConstructor */
/** @type {GPUVertexFormat[]} */

View file

@ -1,427 +0,0 @@
export class WebGPUError extends Error {
/**
* @param {string} message
*/
constructor(message) {
super(message)
}
static unsupported() {
return new WebGPUError("WebGPU is unsupported in this browser")
}
static adapterUnavailable() {
return new WebGPUError("Could not request a WebGPU adapter.")
}
}
export class WebGPUBuilderError extends Error {
/**
* @param {string} message
*/
constructor(message) {
super(message)
}
/**
* @param {string} property
*/
static missing(property) {
return new WebGPUBuilderError(`Missing required property: ${property}`)
}
}
class GPUCanvasConfigurationBuilder {
/** @type {WebGPUContextBuilder} */
#contextBuilder
/** @type {GPUTextureUsageFlags} */
#usage = 0x10
/** @type {GPUTextureFormat[]} */
#viewFormats = []
/** @type {PredefinedColorSpace} */
#colorSpace = 'srgb'
/** @type {GPUCanvasToneMapping} */
#toneMapping = {}
/** @type {GPUCanvasAlphaMode} */
#alphaMode = 'opaque'
/**
* @param {WebGPUContextBuilder} builder
*/
constructor(builder) {
this.#contextBuilder = builder
}
/**
* @param {GPUTextureUsageFlags} usage
*/
usage(usage) {
this.#usage = usage
return this
}
/**
* @param {GPUTextureUsageFlags} usage
*/
addUsage(usage) {
this.#usage = this.#usage | usage
return this
}
/**
* @param {GPUTextureFormat[]} formats
*/
viewFormats(formats) {
this.#viewFormats = formats
return this
}
/**
* @param {GPUTextureFormat} format
*/
addViewFormat(format) {
this.#viewFormats.push(format)
return this
}
/**
* @param {PredefinedColorSpace} space
*/
colorSpace(space) {
this.#colorSpace = space
return this
}
/**
* @param {GPUCanvasToneMappingMode} mode
*/
toneMappingMode(mode) {
this.#toneMapping = { mode }
return this
}
/**
* @param {GPUCanvasAlphaMode} mode
*/
alphaMode(mode) {
this.#alphaMode = mode
return this
}
/**
* @returns {GPUCanvasBuilderConfiguration}
*/
build() {
return {
usage: this.#usage,
viewFormats: this.#viewFormats,
colorSpace: this.#colorSpace,
toneMapping: this.#toneMapping,
alphaMode: this.#alphaMode
}
}
/**
* @returns {WebGPUContextBuilder}
*/
apply() {
return this.#contextBuilder.configureCanvas(this.build())
}
}
/** @typedef {'core' | 'compatibility'} GPUFeatureLevel */
class GPUAdapterOptionsBuilder {
#contextBuilder
/** @type {GPUFeatureLevel} */
#featureLevel = 'core'
/** @type {GPUPowerPreference} */
#powerPreference
/** @type {boolean} */
#forceFallbackAdapter = false
/** @type {boolean} */
#xrCompatible = false
/**
* @param {WebGPUContextBuilder} builder
*/
constructor(builder) {
this.#contextBuilder = builder
}
/**
* @param {GPUFeatureLevel} featureLevel
*/
featureLevel(featureLevel) {
this.#featureLevel = featureLevel
return this
}
/**
* @param {GPUPowerPreference?} preference
*/
powerPreference(preference) {
this.#powerPreference = preference
return this
}
/**
* @param {boolean} force
*/
forceFallbackAdapter(force) {
this.#forceFallbackAdapter = force
return this
}
/**
* @param {boolean} compatible
*/
xrCompatible(compatible) {
this.#xrCompatible = compatible
return this
}
/**
* @returns {GPURequestAdapterOptions}
*/
build() {
return {
featureLevel: this.#featureLevel,
powerPreference: this.#powerPreference,
forceFallbackAdapter: this.#forceFallbackAdapter,
xrCompatible: this.#xrCompatible
}
}
apply() {
return this.#contextBuilder.adapter(this.build())
}
}
class GPUDeviceDescriptorBuilder {
/** @type {WebGPUContextBuilder} */
#contextBuilder
/** @type {GPUFeatureName[]} */
#features = []
/** @type {Record<string, GPUSize64?>} */
#limits = {}
/**
* @param {WebGPUContextBuilder} builder
*/
constructor(builder) {
this.#contextBuilder = builder
}
/**
* @param {GPUFeatureName[]} features
*/
requiredFeatures(features) {
this.#features = features
return this
}
/**
* @param {GPUFeatureName} feature
*/
addRequiredFeature(feature) {
this.#features.push(feature)
return this
}
/**
* @param {Record<string, GPUSize64?>} limits
*/
requiredLimits(limits) {
this.#limits = limits
return this
}
/**
* @param {string} key
* @param {GPUSize64?} value
*/
setRequiredLimit(key, value) {
this.#limits[key] = value
return this
}
/**
* @returns {GPUDeviceDescriptor}
*/
build() {
return {
requiredFeatures: this.#features,
requiredLimits: this.#limits
}
}
apply() {
return this.#contextBuilder.device(this.build())
}
}
/**
* @typedef {Object} GPUCanvasBuilderConfiguration
* @property {GPUTextureUsageFlags} [usage=0x10]
* @property {GPUTextureFormat[]} [viewFormats=[]]
* @property {PredefinedColorSpace} [colorSpace='srgb']
* @property {GPUCanvasToneMapping} [toneMapping = {}]
* @property {GPUCanvasAlphaMode} [alphaMode='opaque']
*/
export class WebGPUContextBuilder {
/** @type {Promise<GPUAdapter>} */
#adapter
/** @type {GPUCanvasBuilderConfiguration} */
#canvasConfig
/** @type {GPUCanvasContext} */
#context
/** @type {Promise<GPUDevice>} */
#device
/** @type {number} */
#dpr = window.devicePixelRatio || 1
/**
* @param {string} type
*/
#warnDefault(type) {
console.warn(`WARN: Requesting WebGPU ${type} with default options.`)
}
/**
* @param {GPURequestAdapterOptions} [descriptor]
* @returns {WebGPUContextBuilder}
* @throws {WebGPUError} Throws if WebGPU is not supported
*/
adapter(descriptor) {
if (!navigator.gpu) {
throw WebGPUError.unsupported()
}
this.#adapter = navigator.gpu.requestAdapter(descriptor)
return this
}
buildAdapterConfiguration() {
return new GPUAdapterOptionsBuilder(this)
}
/**
* @param {HTMLCanvasElement} canvas
* @returns {WebGPUContextBuilder}
*/
context(canvas) {
this.#context = canvas.getContext('webgpu')
return this
}
buildCanvasConfiguration() {
return new GPUCanvasConfigurationBuilder(this)
}
/**
* @param {GPUDeviceDescriptor} [descriptor]
* @returns {WebGPUContextBuilder}
* @throws {WebGPUError} Throws if WebGPU is not supported
*/
device(descriptor) {
if (!this.#adapter) {
this.#warnDefault('adapter')
this.adapter()
}
this.#device = this.#adapter.then(adapter => adapter.requestDevice(descriptor))
return this
}
buildDeviceDescriptor() {
return new GPUDeviceDescriptorBuilder(this)
}
/**
* @param {GPUCanvasBuilderConfiguration} [configuration]
* @returns {WebGPUContextBuilder}
*/
configureCanvas(configuration) {
this.#canvasConfig = configuration
return this
}
/**
* @returns {Promise<WebGPUContext>}
* @throws {WebGPUBuilderError} Throws if context is undefined or if WebGPU is unsupported
*/
async build() {
if (!this.#context) {
throw WebGPUBuilderError.missing('context')
}
if (!this.#device) {
this.#warnDefault('device')
this.device()
}
const [adapter, device] = await Promise.all([this.#adapter, this.#device])
const format = navigator.gpu.getPreferredCanvasFormat()
if (this.#canvasConfig) {
this.#context.configure({
device,
format,
...this.#canvasConfig
})
}
return new WebGPUContext(
this.#context,
adapter,
device,
format,
this.#dpr
)
}
}
export class WebGPUContext {
#adapter
#context
#device
#format
#dpr
/**
* @param {GPUCanvasContext} context
* @param {GPUAdapter} adapter
* @param {GPUDevice} device
* @param {GPUTextureFormat} format
* @param {number} [dpr]
*/
constructor(context, adapter, device, format, dpr) {
this.#context = context
this.#adapter = adapter
this.#device = device
this.#format = format
this.#dpr = dpr || window.devicePixelRatio || 1
}
get adapter() { return this.#adapter }
get context() { return this.#context }
get device() { return this.#device }
get queue() { return this.#device.queue }
get format() { return this.#format }
get devicePixelRatio() { return this.#dpr }
static create() {
return new WebGPUContextBuilder()
}
}

View file

@ -1,105 +0,0 @@
/** @typedef {-1 | 0 | 1} Ternary */
/**
* @template T
* @typedef {(a: T, b: T) => Ternary | NaN} Comparer
*/
/**
* @template X, Y
* @param {X | Y} a
* @param {Y} b
* @returns [X | undefined, Y | undefined]
*/
const optionalSecond = (a, b) => {
let x = a
let y = b
if (!b) {
x = undefined
y = b
}
return [x, y]
}
/**
* @template K, V
*/
export class OrderedMap extends Map {
_sort
/**
* @overload
* @param {Iterable.<[K, V]>} iterable
* @param {Comparer<[K, V]>} comparer
*/
/**
* @overload
* @param {Comparer<[K, V]>} comparer
*/
/**
* @param {Iterable.<[K, V]> | Comparer<[K, V]>} iterable
* @param {Comparer<[K, V]>} [comparer]
*/
constructor(iterable, comparer) {
const [iter, sort] = optionalSecond(iterable, comparer)
/** @ts-ignore */
super(iter)
this._sort = sort
}
/**
* @param {Comparer<[K, V]>} comparer
*/
sortWith(comparer) {
this._sort = comparer
}
*[Symbol.iterator]() {
/** @ts-ignore */
yield* [...this.entries()].sort(this._sort)
}
}
/**
* @template T
* @param {(value: T) => number} get
* @returns {Comparer<T>}
*/
const prioritySort = get => (a, b) => get(a) - get(b)
/**
* @template K, V
*/
export class PriorityMap extends OrderedMap {
/**
* @typedef {(value: [K, V]) => number} PriorityGetter
*/
/** @type PriorityGetter */
_getter
/**
* @overload
* @param {Iterable.<[K, V]>} iterable
* @param {PriorityGetter} getter
*/
/**
* @overload
* @param {PriorityGetter} getter
*/
/**
* @param {Iterable.<[K, V]> | PriorityGetter} iterable
* @param {PriorityGetter} [getter]
*/
constructor(iterable, getter) {
const [iter, get] = optionalSecond(iterable, getter)
/** @ts-ignore */
super(iter, prioritySort(get))
/** @ts-ignore */
this._getter = get
}
}

View file

@ -1,9 +0,0 @@
export class Attribute {
#name
#format
#location
#buffer
#count
#components
}