diff --git a/examples/webgpu/hello-triangle/app.ts b/examples/webgpu/hello-triangle/app.ts index 775d56a1f..2f9fe1317 100644 --- a/examples/webgpu/hello-triangle/app.ts +++ b/examples/webgpu/hello-triangle/app.ts @@ -1,4 +1,4 @@ -/// +/// import {Model, WebGPUDevice} from '@luma.gl/webgpu'; @@ -16,7 +16,7 @@ void main() { } `, - fragment: `#version 450 + fragment: `#version 450 layout(location = 0) out vec4 outColor; void main() { @@ -52,8 +52,20 @@ export async function init(canvas: HTMLCanvasElement, language: 'glsl' | 'wgsl') const model = new Model(device, { vs: SHADERS[language].vertex, fs: SHADERS[language].fragment, - topology: "triangle-list", - vertexCount: 3 + topology: 'triangle-list', + vertexCount: 3, + parameters: { + // Enable depth testing so that the fragment closest to the camera + // is rendered in front. + depthWriteEnabled: true, + depthCompare: 'less', + depthFormat: 'depth24plus', + + // Backface culling since the cube is solid piece of geometry. + // Faces pointing away from the camera will be occluded by faces + // pointing toward the camera. + cullMode: 'back', + } }); function frame() { diff --git a/examples/webgpu/hello-triangle/package.json b/examples/webgpu/hello-triangle/package.json index 065c90ad1..a01ef17d1 100644 --- a/examples/webgpu/hello-triangle/package.json +++ b/examples/webgpu/hello-triangle/package.json @@ -1,5 +1,5 @@ { - "name": "luma.gl-examples-hello-triangle-webgpu", + "name": "luma.gl-examples-webgpu-hello-triangle", "version": "1.0.0", "private": true, "scripts": { diff --git a/examples/webgpu/rotating-cube/app.ts b/examples/webgpu/rotating-cube/app.ts new file mode 100644 index 000000000..b04adcd3a --- /dev/null +++ b/examples/webgpu/rotating-cube/app.ts @@ -0,0 +1,166 @@ +import {Model, WebGPUDevice} from '@luma.gl/webgpu'; +import {Matrix4} from '@math.gl/core'; + +import { + cubePositions, + cubeUVs, + cubeVertexCount +} from './cube'; + +export const title = 'Rotating Cube'; +export const description = 'Shows rendering a basic triangle.'; + +/** Provide both GLSL and WGSL shaders */ +const SHADERS = { + wgsl: { + vertex: ` +struct Uniforms { + modelViewProjectionMatrix : mat4x4; +}; +[[binding(0), group(0)]] var uniforms : Uniforms; + +struct VertexOutput { + [[builtin(position)]] Position : vec4; + [[location(0)]] fragUV : vec2; + [[location(1)]] fragPosition: vec4; +}; + +[[stage(vertex)]] +fn main([[location(0)]] position : vec4, + [[location(1)]] uv : vec2) -> VertexOutput { + var output : VertexOutput; + output.Position = uniforms.modelViewProjectionMatrix * position; + output.fragUV = uv; + output.fragPosition = 0.5 * (position + vec4(1.0, 1.0, 1.0, 1.0)); + return output; +} + `, + fragment: ` +[[stage(fragment)]] +fn main([[location(0)]] fragUV: vec2, + [[location(1)]] fragPosition: vec4) -> [[location(0)]] vec4 { + return fragPosition; +} + ` + } +}; + +// const vertexBufferLayout = { +// arrayStride: cubeVertexSize, +// attributes: [ +// { +// // position +// shaderLocation: 0, +// offset: cubePositionOffset, +// format: 'float32x4', +// }, +// { +// // uv +// shaderLocation: 1, +// offset: cubeUVOffset, +// format: 'float32x2', +// }, +// ], +// }; + +// const uniformBindGroup = device.handle.createBindGroup({ +// layout: pipeline.handle.getBindGroupLayout(0), +// entries: [ +// { +// binding: 0, +// resource: { +// buffer: uniformBuffer.handle, +// }, +// }, +// ], +// }); + +const UNIFORM_BUFFER_SIZE = 4 * 16; // 4x4 matrix + +export async function init(canvas: HTMLCanvasElement, language: 'glsl' | 'wgsl') { + const device = await WebGPUDevice.create({canvas}); + + // Create a vertex buffer from the cube data. + const positionBuffer = device.createBuffer({id: 'cube-positions', data: cubePositions}); + const uvBuffer = device.createBuffer({id: 'cube-uvs', data: cubeUVs}); + + + const uniformBuffer = device.createBuffer({ + byteLength: UNIFORM_BUFFER_SIZE, + // TODO - use API constants instead of WebGPU constants + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + const model = new Model(device, { + id: 'cube', + vs: SHADERS[language].vertex, + fs: SHADERS[language].fragment, + topology: 'triangle-list', + attributeLayouts: [ + {name: 'position', location: 0, accessor: {format: 'float32x4'}}, + {name: 'uv', location: 1, accessor: {format: 'float32x2'}} + ], + attributeBuffers: [positionBuffer, uvBuffer], + bindings: [uniformBuffer], + vertexCount: cubeVertexCount, + parameters: { + // Enable depth testing so that the fragment closest to the camera + // is rendered in front. + depthWriteEnabled: true, + depthCompare: 'less', + depthFormat: 'depth24plus', + + // Backface culling since the cube is solid piece of geometry. + // Faces pointing away from the camera will be occluded by faces + // pointing toward the camera. + cullMode: 'back', + }, + }); + + const projectionMatrix = new Matrix4(); + const viewMatrix = new Matrix4(); + const modelViewProjectionMatrix = new Matrix4(); + + function frame() { + const aspect = canvas.width / canvas.height; + const now = Date.now() / 1000; + + viewMatrix.identity().translate([0, 0, -4]).rotateAxis(1, [Math.sin(now), Math.cos(now), 0]); + projectionMatrix.perspective({fov: (2 * Math.PI) / 5, aspect, near: 1, far: 100.0}); + modelViewProjectionMatrix.copy(viewMatrix).multiplyLeft(projectionMatrix); + uniformBuffer.write(new Float32Array(modelViewProjectionMatrix)); + + device.beginRenderPass(); + model.draw(); + + device.submit(); + requestAnimationFrame(frame); + } + + requestAnimationFrame(frame); +} + +init(document.getElementById('canvas') as HTMLCanvasElement, 'wgsl'); + + +/* +const projectionMatrix = mat4.create(); +mat4.perspective(projectionMatrix, ); + +function getTransformationMatrix() { + const viewMatrix = mat4.create(); + mat4.translate(viewMatrix, viewMatrix, vec3.fromValues(0, 0, -4)); + const now = Date.now() / 1000; + mat4.rotate( + viewMatrix, + viewMatrix, + 1, + vec3.fromValues(Math.sin(now), Math.cos(now), 0) + ); + + const modelViewProjectionMatrix = mat4.create(); + mat4.multiply(modelViewProjectionMatrix, projectionMatrix, viewMatrix); + + return modelViewProjectionMatrix as Float32Array; +} +*/ diff --git a/examples/webgpu/rotating-cube/cube.ts b/examples/webgpu/rotating-cube/cube.ts new file mode 100644 index 000000000..4aff0493a --- /dev/null +++ b/examples/webgpu/rotating-cube/cube.ts @@ -0,0 +1,142 @@ +export const cubeVertexSize = 4 * 10; // Byte size of one cube vertex. +export const cubePositionOffset = 0; +export const cubeColorOffset = 4 * 4; // Byte offset of cube vertex color attribute. +export const cubeUVOffset = 4 * 8; +export const cubeVertexCount = 36; + +// float4 position +// prettier-ignore +export const cubePositions = new Float32Array([ + 1, -1, 1, 1, + -1, -1, 1, 1, + -1, -1, -1, 1, + 1, -1, -1, 1, + 1, -1, 1, 1, + -1, -1, -1, 1, + + 1, 1, 1, 1, + 1, -1, 1, 1, + 1, -1, -1, 1, + 1, 1, -1, 1, + 1, 1, 1, 1, + 1, -1, -1, 1, + + -1, 1, 1, 1, + 1, 1, 1, 1, + 1, 1, -1, 1, + -1, 1, -1, 1, + -1, 1, 1, 1, + 1, 1, -1, 1, + + -1, -1, 1, 1, + -1, 1, 1, 1, + -1, 1, -1, 1, + -1, -1, -1, 1, + -1, -1, 1, 1, + -1, 1, -1, 1, + + 1, 1, 1, 1, + -1, 1, 1, 1, + -1, -1, 1, 1, + -1, -1, 1, 1, + 1, -1, 1, 1, + 1, 1, 1, 1, + + 1, -1, -1, 1, + -1, -1, -1, 1, + -1, 1, -1, 1, + 1, 1, -1, 1, + 1, -1, -1, 1, + -1, 1, -1, 1, +]); + +// float2 uv, +// prettier-ignore +export const cubeUVs = new Float32Array([ + 1, 1, + 0, 1, + 0, 0, + 1, 0, + 1, 1, + 0, 0, + + 1, 1, + 0, 1, + 0, 0, + 1, 0, + 1, 1, + 0, 0, + + 1, 1, + 0, 1, + 0, 0, + 1, 0, + 1, 1, + 0, 0, + + 1, 1, + 0, 1, + 0, 0, + 1, 0, + 1, 1, + 0, 0, + + 1, 1, + 0, 1, + 0, 0, + 0, 0, + 1, 0, + 1, 1, + + 1, 1, + 0, 1, + 0, 0, + 1, 0, + 1, 1, + 0, 0, +]); + +export const cubeColors = new Float32Array([ + // float4 position, float4 color, float2 uv, + 1, 0, 1, 1, + 0, 0, 1, 1, + 0, 0, 0, 1, + 1, 0, 0, 1, + 1, 0, 1, 1, + 0, 0, 0, 1, + + 1, 1, 1, 1, + 1, 0, 1, 1, + 1, 0, 0, 1, + 1, 1, 0, 1, + 1, 1, 1, 1, + 1, 0, 0, 1, + + 0, 1, 1, 1, + 1, 1, 1, 1, + 1, 1, 0, 1, + 0, 1, 0, 1, + 0, 1, 1, 1, + 1, 1, 0, 1, + + 0, 0, 1, 1, + 0, 1, 1, 1, + 0, 1, 0, 1, + 0, 0, 0, 1, + 0, 0, 1, 1, + 0, 1, 0, 1, + + 1, 1, 1, 1, + 0, 1, 1, 1, + 0, 0, 1, 1, + 0, 0, 1, 1, + 1, 0, 1, 1, + 1, 1, 1, 1, + + 1, 0, 0, 1, + 0, 0, 0, 1, + 0, 1, 0, 1, + 1, 1, 0, 1, + 1, 0, 0, 1, + 0, 1, 0, 1, +]); diff --git a/examples/webgpu/rotating-cube/index.html b/examples/webgpu/rotating-cube/index.html new file mode 100644 index 000000000..c155a807d --- /dev/null +++ b/examples/webgpu/rotating-cube/index.html @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/webgpu/rotating-cube/package.json b/examples/webgpu/rotating-cube/package.json new file mode 100644 index 000000000..ef828eba8 --- /dev/null +++ b/examples/webgpu/rotating-cube/package.json @@ -0,0 +1,20 @@ +{ + "name": "luma.gl-examples-webgpu-rotating-cube", + "version": "1.0.0", + "private": true, + "scripts": { + "start": "vite", + "build": "tsc && vite build", + "serve": "vite preview" + }, + "dependencies": { + "@luma.gl/engine": "8.6.0-alpha.5", + "@luma.gl/shadertools": "8.6.0-alpha.5", + "@luma.gl/webgpu": "8.6.0-alpha.5", + "@math.gl/core": "^3.5.0" + }, + "devDependencies": { + "typescript": "^4.3.2", + "vite": "^2.6.4" + } +} diff --git a/examples/webgpu/rotating-cube/vite.config.ts b/examples/webgpu/rotating-cube/vite.config.ts new file mode 100644 index 000000000..46bb698c1 --- /dev/null +++ b/examples/webgpu/rotating-cube/vite.config.ts @@ -0,0 +1,18 @@ +import fs from 'fs/promises'; +import { defineConfig } from 'vite' + +/** @see https://vitejs.dev/config/ */ +export default defineConfig(async () => ({ + resolve: {alias: await getAliases('@luma.gl', `${__dirname}/../../..`)}, + server: {open: true} +})); + +/** Run against local source */ +const getAliases = async (frameworkName, frameworkRootDir) => { + const modules = await fs.readdir(`${frameworkRootDir}/modules`) + const aliases = {} + for (const module of modules) { + aliases[`${frameworkName}/${module}`] = `${frameworkRootDir}/modules/${module}/src` + } + return aliases +} diff --git a/modules/api/src/adapter/device.ts b/modules/api/src/adapter/device.ts index e40c29c12..7763c6246 100644 --- a/modules/api/src/adapter/device.ts +++ b/modules/api/src/adapter/device.ts @@ -1,8 +1,8 @@ // luma.gl, MIT license import StatsManager, {lumaStats} from '../utils/stats-manager'; -import type {default as Buffer, BufferProps} from './buffer'; -import type {default as Texture, TextureProps} from './texture'; -import type {default as Shader, ShaderProps} from './shader'; +import type {default as Buffer, BufferProps} from './resources/buffer'; +import type {default as Texture, TextureProps} from './resources/texture'; +import type {default as Shader, ShaderProps} from './resources/shader'; // import type {RenderPipeline, RenderPipelineProps, ComputePipeline, ComputePipelineProps} from './pipeline'; export type ShadingLanguage = 'glsl' | 'wgsl'; diff --git a/modules/api/src/adapter/buffer.ts b/modules/api/src/adapter/resources/buffer.ts similarity index 94% rename from modules/api/src/adapter/buffer.ts rename to modules/api/src/adapter/resources/buffer.ts index 822dddb34..6cd8dc4ef 100644 --- a/modules/api/src/adapter/buffer.ts +++ b/modules/api/src/adapter/resources/buffer.ts @@ -1,5 +1,5 @@ // luma.gl, MIT license -import type Device from './device'; +import type Device from '../device'; import Resource, {ResourceProps, DEFAULT_RESOURCE_PROPS} from './resource'; export type BufferProps = ResourceProps & { @@ -30,7 +30,7 @@ const DEFAULT_BUFFER_PROPS: Required = { byteLength: 0, data: undefined, usage: undefined, // GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC - mappedAtCreation: true + mappedAtCreation: false }; /** Abstract GPU buffer */ @@ -53,16 +53,7 @@ export default abstract class Buffer extends Resource { super(device, props, DEFAULT_BUFFER_PROPS); } - // Mapped API (WebGPU) - - /** Maps the memory so that it can be read */ - // abstract mapAsync(mode, byteOffset, byteLength): Promise - - /** Get the mapped range of data for reading or writing */ - // abstract getMappedRange(byteOffset, byteLength): ArrayBuffer; - - /** unmap makes the contents of the buffer available to the GPU again */ - // abstract unmap(): void; + write(data: ArrayBufferView, byteOffset: number = 0): void { throw new Error('not implemented'); } // Convenience API @@ -103,4 +94,15 @@ export default abstract class Buffer extends Resource { } } */ + + // Mapped API (WebGPU) + + /** Maps the memory so that it can be read */ + // abstract mapAsync(mode, byteOffset, byteLength): Promise + + /** Get the mapped range of data for reading or writing */ + // abstract getMappedRange(byteOffset, byteLength): ArrayBuffer; + + /** unmap makes the contents of the buffer available to the GPU again */ + // abstract unmap(): void; } diff --git a/modules/api/src/adapter/render-pipeline.ts b/modules/api/src/adapter/resources/render-pipeline.ts similarity index 80% rename from modules/api/src/adapter/render-pipeline.ts rename to modules/api/src/adapter/resources/render-pipeline.ts index eafbe724a..2cb20896c 100644 --- a/modules/api/src/adapter/render-pipeline.ts +++ b/modules/api/src/adapter/resources/render-pipeline.ts @@ -1,9 +1,10 @@ // luma.gl, MIT license -import type Device from './device'; +import type Device from '../device'; import Resource, {ResourceProps, DEFAULT_RESOURCE_PROPS} from './resource'; import type {default as Shader} from './shader'; -import {RenderPipelineParameters} from './parameters'; -import type {BindingLayout} from './types'; +import type {RenderPipelineParameters} from '../types/parameters'; +import type {BindingLayout} from '../types/types'; +import type {AttributeBinding} from '../types/program-bindings'; export type RenderPipelineProps = ResourceProps & { vertexShader: Shader; @@ -16,6 +17,7 @@ export type RenderPipelineProps = ResourceProps & { topology?: 'point-list' | 'line-list' | 'line-strip' | 'triangle-list' | 'triangle-strip'; parameters?: RenderPipelineParameters; + attributeLayouts?: AttributeBinding[]; layout?: BindingLayout[]; }; diff --git a/modules/api/src/adapter/resource.ts b/modules/api/src/adapter/resources/resource.ts similarity index 80% rename from modules/api/src/adapter/resource.ts rename to modules/api/src/adapter/resources/resource.ts index fe255e6a9..6571aaaf4 100644 --- a/modules/api/src/adapter/resource.ts +++ b/modules/api/src/adapter/resources/resource.ts @@ -1,6 +1,6 @@ -// -import Device from './device'; -// import {uid} from '../utils'; +// luma.gl, MIT license +import type Device from '../device'; +import {uid} from '../../utils/utils'; export type ResourceProps = { id?: string; @@ -38,8 +38,10 @@ export default abstract class Resource { throw new Error('no device'); } this._device = device; - this.props = this.initializeProps(props, defaultProps); - this.id = this.props.id || 'no-id'; // TODO uid(this[Symbol.toStringTag] || this.constructor.name); + this.props = selectivelyMerge(props, defaultProps); + this.props.id = this.props.id || uid(this[Symbol.toStringTag]); + + this.id = this.props.id; this.userData = this.props.userData || {}; this.addStats(); } @@ -105,19 +107,20 @@ export default abstract class Resource { stats.get(`${name}s Active`).incrementCount(); } - /** - * Combines a map of user props and default props, only including props from defaultProps - * @param props - * @param defaultProps - * @returns returns a map of overridden default props - */ - private initializeProps(props: Props, defaultProps: Required): Required { - const mergedProps = {...defaultProps}; - for (const key in props) { - if (props[key] !== undefined) { - mergedProps[key] = props[key]; - } - } - return mergedProps; - } +} + +/** + * Combines a map of user props and default props, only including props from defaultProps + * @param props + * @param defaultProps + * @returns returns a map of overridden default props + */ + function selectivelyMerge(props: Props, defaultProps: Required): Required { + const mergedProps = {...defaultProps}; + for (const key in props) { + if (props[key] !== undefined) { + mergedProps[key] = props[key]; + } + } + return mergedProps; } diff --git a/modules/api/src/adapter/sampler.ts b/modules/api/src/adapter/resources/sampler.ts similarity index 93% rename from modules/api/src/adapter/sampler.ts rename to modules/api/src/adapter/resources/sampler.ts index 33f863fbe..86dcc86c7 100644 --- a/modules/api/src/adapter/sampler.ts +++ b/modules/api/src/adapter/resources/sampler.ts @@ -1,6 +1,6 @@ -import {CompareFunction} from './parameters'; +import type Device from '../device'; +import {CompareFunction} from '../types/parameters'; import Resource, {ResourceProps, DEFAULT_RESOURCE_PROPS} from './resource'; -import type Device from './device'; export type SamplerAddressMode = 'clamp-to-edge' | 'repeat' | 'mirror-repeat'; export type SamplerFilterMode = 'nearest' | 'linear'; diff --git a/modules/api/src/adapter/shader.ts b/modules/api/src/adapter/resources/shader.ts similarity index 97% rename from modules/api/src/adapter/shader.ts rename to modules/api/src/adapter/resources/shader.ts index f81ccc80f..032aa4b30 100644 --- a/modules/api/src/adapter/shader.ts +++ b/modules/api/src/adapter/resources/shader.ts @@ -1,5 +1,5 @@ // luma.gl, MIT license -import type Device from './device'; +import type Device from '../device'; import Resource, {ResourceProps, DEFAULT_RESOURCE_PROPS} from './resource'; export type CompilerMessageType = 'error' | 'warning' | 'info'; diff --git a/modules/api/src/adapter/texture.ts b/modules/api/src/adapter/resources/texture.ts similarity index 69% rename from modules/api/src/adapter/texture.ts rename to modules/api/src/adapter/resources/texture.ts index 3ac7c64c4..0b1b00a47 100644 --- a/modules/api/src/adapter/texture.ts +++ b/modules/api/src/adapter/resources/texture.ts @@ -1,6 +1,7 @@ // luma.gl, MIT license +import type Device from '../device'; +import type {TextureFormat} from '../types/types'; import Resource, {ResourceProps, DEFAULT_RESOURCE_PROPS} from './resource'; -import type Device from './device'; // required GPUExtent3D size; // GPUIntegerCoordinate mipLevelCount = 1; @@ -11,35 +12,42 @@ import type Device from './device'; /** Abstract Texture interface */ export type TextureProps = ResourceProps & { - data?: any; + format?: TextureFormat | number; + dimension?: '1d' | '2d' | '3d'; width?: number; height?: number; depth?: number; + usage?: number; + + data?: any; + mipmaps?: boolean; + parameters?: object; - pixels?: any; - format?: number; - dataFormat?: number; - border?: number; - recreate?: boolean; type?: number; compressed?: boolean; - mipmaps?: boolean; - parameters?: object; + /** @deprecated use data */ + pixels?: any; + /** @deprecated use format */ + dataFormat?: number; + /** @deprecated rarely supported */ + border?: number; + /** @deprecated WebGL only. */ pixelStore?: object; + /** @deprecated WebGL only. */ textureUnit?: number; - + /** @deprecated WebGL only. Use dimension. */ target?: number; + /** @deprecated not supported */ + recreate?: boolean; }; export type WebGPUTextureProps = ResourceProps & { - dimension?: '1d' | '2d' | '3d'; width: number; height: number; depth?: number; mipLevels?: number; format?: string; - usage?: number; }; export type TextureViewProps = { @@ -48,22 +56,35 @@ export type TextureViewProps = { aspect?: 'all', 'stencil-only', 'depth-only'; arrayLayerCount: number; baseArrayLayer?: number; - mipLevelCount: number; + mipLevels?: number; baseMipLevel?: number; }; +// @ts-expect-error const DEFAULT_TEXTURE_PROPS: Required = { ...DEFAULT_RESOURCE_PROPS, + data: undefined, dimension: '2d', width: 1, height: 1, depth: 1, - mipLevels: 1, - // @ts-expect-error - format: 'unorm8', + mipmaps: false, + parameters: {}, + type: undefined, + compressed: false, + // mipLevels: 1, + format: 'rgba8unorm', usage: 0 }; +// const DEFAULT_TEXTURE_PROPS: Required = { +// handle: undefined, +// id: undefined, +// depth: 1, +// format: 'rgba8unorm', +// usage: GPUTextureUsage.COPY_DST +// }; + /** * Abstract Texture interface * Texture Object diff --git a/modules/api/src/adapter/types.ts b/modules/api/src/adapter/types.ts deleted file mode 100644 index 97c1f8757..000000000 --- a/modules/api/src/adapter/types.ts +++ /dev/null @@ -1,135 +0,0 @@ -// luma.gl, MIT license -enum TextureFormat { - // 8-bit formats - 'r8unorm', - 'r8snorm', - 'r8uint', - 'r8sint', - - // 16-bit formats - 'r16uint', - 'r16sint', - 'r16float', - 'rg8unorm', - 'rg8snorm', - 'rg8uint', - 'rg8sint', - - // 32-bit formats - 'r32uint', - 'r32sint', - 'r32float', - 'rg16uint', - 'rg16sint', - 'rg16float', - 'rgba8unorm', - 'rgba8unorm-srgb', - 'rgba8snorm', - 'rgba8uint', - 'rgba8sint', - 'bgra8unorm', - 'bgra8unorm-srgb', - // Packed 32-bit formats - 'rgb9e5ufloat', - 'rgb10a2unorm', - 'rg11b10ufloat', - - // 64-bit formats - 'rg32uint', - 'rg32sint', - 'rg32float', - 'rgba16uint', - 'rgba16sint', - 'rgba16float', - - // 128-bit formats - 'rgba32uint', - 'rgba32sint', - 'rgba32float', - - // Depth and stencil formats - 'stencil8', - 'depth16unorm', - 'depth24plus', - 'depth24plus-stencil8', - 'depth32float', - - // BC compressed formats usable if 'texture-compression-bc' is both - // supported by the device/user agent and enabled in requestDevice. - 'bc1-rgba-unorm', - 'bc1-rgba-unorm-srgb', - 'bc2-rgba-unorm', - 'bc2-rgba-unorm-srgb', - 'bc3-rgba-unorm', - 'bc3-rgba-unorm-srgb', - 'bc4-r-unorm', - 'bc4-r-snorm', - 'bc5-rg-unorm', - 'bc5-rg-snorm', - 'bc6h-rgb-ufloat', - 'bc6h-rgb-float', - 'bc7-rgba-unorm', - 'bc7-rgba-unorm-srgb', - - // 'depth24unorm-stencil8' feature - 'depth24unorm-stencil8', - - // 'depth32float-stencil8' feature - 'depth32float-stencil8', -}; - -// BINDING LAYOUTS - -type BufferBindingLayout = { - location?: number; - visibility: number; - type: 'uniform' | 'storage' | 'read-only-storage'; - hasDynamicOffset?: boolean; - minBindingSize?: number; -} - -type TextureBindingLayout = { - location?: number; - visibility: number; - viewDimension?: '1d' | '2d' | '2d-array' | 'cube' | 'cube-array' | '3d'; - sampleType?: 'float' | 'unfilterable-float' | 'depth' | 'sint' | 'uint'; - multisampled?: boolean; -}; - -type StorageTextureBindingLayout = { - location?: number; - visibility: number; - access?: 'write-only'; - format: TextureFormat; - viewDimension?: '1d' | '2d' | '2d-array' | 'cube' | 'cube-array' | '3d'; -}; - -export type BindingLayout = BufferBindingLayout | TextureBindingLayout | StorageTextureBindingLayout; - -// BINDINGS - -import type Buffer from './buffer'; -import type Texture from './texture'; // TextureView... - -export type Binding = Texture | Buffer | {buffer: Buffer, offset?: number, size?: number}; - -// Attachments - -export type ColorAttachment = { - // attachment: GPUTextureView; - // resolveTarget?: GPUTextureView; - // loadValue: GPULoadOp | GPUColor; - // storeOp?: GPUStoreOp; -}; - -export type DepthStencilAttachment = { - // attachment: GPUTextureView; - - // depthLoadValue: GPULoadOp | number; - // depthStoreOp: GPUStoreOp; - // depthReadOnly?: boolean; - - // stencilLoadValue: GPULoadOp | number; - // stencilStoreOp: GPUStoreOp; - // stencilReadOnly?: boolean; -}; diff --git a/modules/api/src/adapter/accessor.ts b/modules/api/src/adapter/types/accessor.ts similarity index 56% rename from modules/api/src/adapter/accessor.ts rename to modules/api/src/adapter/types/accessor.ts index a87e2a199..2759ea96f 100644 --- a/modules/api/src/adapter/accessor.ts +++ b/modules/api/src/adapter/types/accessor.ts @@ -1,30 +1,38 @@ // luma.gl, MIT license +import type {VertexFormat} from './types'; + // ACCESSORS /** * Attribute descriptor object */ export type Accessor = { - format?: string; + format: VertexFormat; offset?: number; - location?: number; - // can now be described with single WebGPU-style `format` string - type?: number; - size?: number; - normalized?: boolean; - integer?: boolean; - // deprecated - now shared between accessors in the surrounding BufferAccessor object + // stride?: number; + stepMode?: 'vertex' | 'instance'; + + /** @deprecated - Use accessor.stepMode */ divisor?: number; + + /** @deprecated - Infer from format */ + type?: number; + /** @deprecated - Infer from format */ + size?: number; + /** @deprecated - Infer from format */ + normalized?: boolean; + /** @deprecated - Infer from format */ + integer?: boolean; }; /** * List of attribute descriptors for one interleaved buffer */ -export type BufferAccessors = { +export type InterleavedAccessors = { stride?: number; stepMode?: 'vertex' | 'instance'; attributes: Accessor[]; -} +}; diff --git a/modules/api/src/adapter/parameters.ts b/modules/api/src/adapter/types/parameters.ts similarity index 92% rename from modules/api/src/adapter/parameters.ts rename to modules/api/src/adapter/types/parameters.ts index ecb5ef650..b5c538cb3 100644 --- a/modules/api/src/adapter/parameters.ts +++ b/modules/api/src/adapter/types/parameters.ts @@ -1,3 +1,5 @@ +import {DepthOrStencilTextureFormat} from './types'; + export type CompareFunction = 'never' | 'less' | @@ -53,6 +55,7 @@ export type StencilOperation = export type DepthStencilParameters = { depthWriteEnabled?: boolean; depthCompare?: CompareFunction; + depthFormat?: DepthOrStencilTextureFormat; stencilReadMask?: number; stencilWriteMask?: number; @@ -156,8 +159,8 @@ export type RenderPipelineParameters = export type Parameters = _RenderParameters & DepthStencilParameters & - ColorParameters; - // MultisampleParameters; + ColorParameters & + MultisampleParameters; // export const DEFAULT_PARAMETERS: Parameters; @@ -167,15 +170,19 @@ export const DEFAULT_PARAMETERS: Required = { cullMode: 'none', frontFace: 'ccw', + + // Depth Parameters + + depthWriteEnabled: false, + depthCompare: 'always', + depthFormat: 'depth24plus', + depthClamp: false, depthBias: 0, depthBiasSlopeScale: 0, depthBiasClamp: 0, - // Depth Stencil Parameters - - depthWriteEnabled: false, - depthCompare: 'always', + // Stencil parameters stencilReadMask: 0xFFFFFFFF, stencilWriteMask: 0xFFFFFFFF, @@ -185,6 +192,11 @@ export const DEFAULT_PARAMETERS: Required = { stencilFailOperation: 'keep', stencilDepthFailOperation: 'keep', + // Multisample parameters + sampleCount: 0, + sampleMask: 0xFFFFFFFF, + sampleAlphaToCoverageEnabled: false, + // Color and blend parameters blendColorOperation: 'add', diff --git a/modules/webgl/src/helpers/program-bindings.ts b/modules/api/src/adapter/types/program-bindings.ts similarity index 100% rename from modules/webgl/src/helpers/program-bindings.ts rename to modules/api/src/adapter/types/program-bindings.ts diff --git a/modules/api/src/adapter/types/types.ts b/modules/api/src/adapter/types/types.ts new file mode 100644 index 000000000..442b03a46 --- /dev/null +++ b/modules/api/src/adapter/types/types.ts @@ -0,0 +1,173 @@ +// luma.gl, MIT license + +/** Depth and stencil texture formats */ +export type DepthOrStencilTextureFormat = + 'stencil8' | + 'depth16unorm' | + 'depth24plus' | + 'depth24plus-stencil8' | + 'depth32float' | + // device.features.has('depth24unorm-stencil8') + 'depth24unorm-stencil8' | + // device.features.has('depth32float-stencil8') + 'depth32float-stencil8'; + +/** Texture formats */ +export type TextureFormat = DepthOrStencilTextureFormat | + // 8-bit formats + 'r8unorm' | + 'r8snorm' | + 'r8uint' | + 'r8sint' | + + // 16-bit formats + 'r16uint' | + 'r16sint' | + 'r16float' | + 'rg8unorm' | + 'rg8snorm' | + 'rg8uint' | + 'rg8sint' | + + // 32-bit formats + 'r32uint' | + 'r32sint' | + 'r32float' | + 'rg16uint' | + 'rg16sint' | + 'rg16float' | + 'rgba8unorm' | + 'rgba8unorm-srgb' | + 'rgba8snorm' | + 'rgba8uint' | + 'rgba8sint' | + 'bgra8unorm' | + 'bgra8unorm-srgb' | + // Packed 32-bit formats + 'rgb9e5ufloat' | + 'rgb10a2unorm' | + 'rg11b10ufloat' | + + // 64-bit formats + 'rg32uint' | + 'rg32sint' | + 'rg32float' | + 'rgba16uint' | + 'rgba16sint' | + 'rgba16float' | + + // 128-bit formats + 'rgba32uint' | + 'rgba32sint' | + 'rgba32float' | + + // BC compressed formats usable if 'texture-compression-bc' is both + // supported by the device/user agent and enabled in requestDevice. + 'bc1-rgba-unorm' | + 'bc1-rgba-unorm-srgb' | + 'bc2-rgba-unorm' | + 'bc2-rgba-unorm-srgb' | + 'bc3-rgba-unorm' | + 'bc3-rgba-unorm-srgb' | + 'bc4-r-unorm' | + 'bc4-r-snorm' | + 'bc5-rg-unorm' | + 'bc5-rg-snorm' | + 'bc6h-rgb-ufloat' | + 'bc6h-rgb-float' | + 'bc7-rgba-unorm' | + 'bc7-rgba-unorm-srgb'; + +/** Attribute formats */ +export type VertexFormat = + 'uint8x2' | + 'uint8x4' | + 'sint8x2' | + 'sint8x4' | + 'unorm8x2' | + 'unorm8x4' | + 'snorm8x2' | + 'snorm8x4' | + 'uint16x2' | + 'uint16x4' | + 'sint16x2' | + 'sint16x4' | + 'unorm16x2' | + 'unorm16x4' | + 'snorm16x2' | + 'snorm16x4' | + 'float16x2' | + 'float16x4' | + 'float32' | + 'float32x2' | + 'float32x3' | + 'float32x4' | + 'uint32' | + 'uint32x2' | + 'uint32x3' | + 'uint32x4' | + 'sint32' | + 'sint32x2' | + 'sint32x3' | + 'sint32x4'; + +// BINDING LAYOUTS + +type BufferBindingLayout = { + location?: number; + visibility: number; + type: 'uniform' | 'storage' | 'read-only-storage'; + hasDynamicOffset?: boolean; + minBindingSize?: number; +} + +type TextureBindingLayout = { + location?: number; + visibility: number; + viewDimension?: '1d' | '2d' | '2d-array' | 'cube' | 'cube-array' | '3d'; + sampleType?: 'float' | 'unfilterable-float' | 'depth' | 'sint' | 'uint'; + multisampled?: boolean; +}; + +type StorageTextureBindingLayout = { + location?: number; + visibility: number; + access?: 'write-only'; + format: TextureFormat; + viewDimension?: '1d' | '2d' | '2d-array' | 'cube' | 'cube-array' | '3d'; +}; + +export type BindingLayout = BufferBindingLayout | TextureBindingLayout | StorageTextureBindingLayout; + +// BINDINGS + +import type Buffer from '../resources/buffer'; +import type Texture from '../resources/texture'; // TextureView... + +export type Binding = Texture | Buffer | {buffer: Buffer, offset?: number, size?: number}; + +// ATTRIBUTE LAYOUTS + + + + +// Attachments + +export type ColorAttachment = { + // attachment: GPUTextureView; + // resolveTarget?: GPUTextureView; + // loadValue: GPULoadOp | GPUColor; + // storeOp?: GPUStoreOp; +}; + +export type DepthStencilAttachment = { + // attachment: GPUTextureView; + + // depthLoadValue: GPULoadOp | number; + // depthStoreOp: GPUStoreOp; + // depthReadOnly?: boolean; + + // stencilLoadValue: GPULoadOp | number; + // stencilStoreOp: GPUStoreOp; + // stencilReadOnly?: boolean; +}; diff --git a/modules/api/src/index.ts b/modules/api/src/index.ts index 7bf1934ea..3977f41f2 100644 --- a/modules/api/src/index.ts +++ b/modules/api/src/index.ts @@ -1,24 +1,27 @@ +// Initialize any global state +import './init'; + // MAIN API ACCESS POINTS export {default as luma} from './lib/luma'; export type {DeviceLimits, DeviceInfo} from './adapter/device'; export {default as Device} from './adapter/device'; // GPU RESOURCES -export type {ResourceProps} from './adapter/resource'; -export {default as Resource} from './adapter/resource'; -export type {BufferProps} from './adapter/buffer'; -export {default as Buffer} from './adapter/buffer'; -export type {TextureProps} from './adapter/texture'; -export {default as Texture} from './adapter/texture'; -export type {ShaderProps, CompilerMessage} from './adapter/shader'; -export {default as Shader} from './adapter/shader'; -export type {SamplerProps} from './adapter/sampler'; -export {default as Sampler} from './adapter/sampler'; -export type {RenderPipelineProps} from './adapter/render-pipeline'; -export {default as RenderPipeline} from './adapter/render-pipeline'; +export type {ResourceProps} from './adapter/resources/resource'; +export {default as Resource} from './adapter/resources/resource'; +export type {BufferProps} from './adapter/resources/buffer'; +export {default as Buffer} from './adapter/resources/buffer'; +export type {TextureProps} from './adapter/resources/texture'; +export {default as Texture} from './adapter/resources/texture'; +export type {ShaderProps, CompilerMessage} from './adapter/resources/shader'; +export {default as Shader} from './adapter/resources/shader'; +export type {SamplerProps} from './adapter/resources/sampler'; +export {default as Sampler} from './adapter/resources/sampler'; +export type {RenderPipelineProps} from './adapter/resources/render-pipeline'; +export {default as RenderPipeline} from './adapter/resources/render-pipeline'; // API TYPES -export type {Accessor, BufferAccessors} from './adapter/accessor'; +export type {Accessor, InterleavedAccessors} from './adapter/types/accessor'; export type { Parameters, PrimitiveTopology, @@ -35,9 +38,24 @@ export type { MultisampleParameters, RenderPassParameters, RenderPipelineParameters -} from './adapter/parameters'; +} from './adapter/types/parameters'; -export type {BindingLayout, Binding, ColorAttachment, DepthStencilAttachment} from './adapter/types'; +export type { + TextureFormat, + VertexFormat, + BindingLayout, + Binding, + ColorAttachment, + DepthStencilAttachment +} from './adapter/types/types'; + +export type { + ProgramBindings, + AttributeBinding, + UniformBinding, + UniformBlockBinding, + VaryingBinding +} from './adapter/types/program-bindings'; // UTILS export type {TypedArray, NumberArray} from './types'; diff --git a/modules/api/src/init.ts b/modules/api/src/init.ts new file mode 100644 index 000000000..16cc33a38 --- /dev/null +++ b/modules/api/src/init.ts @@ -0,0 +1,35 @@ +import {isBrowser} from '@probe.gl/env'; +import {log} from './utils/log'; +import {lumaStats} from './utils/stats-manager'; + +// Version detection using babel plugin +// @ts-expect-error +const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'untranspiled source'; + +const STARTUP_MESSAGE = 'set luma.log.level=1 (or higher) to trace rendering'; +// Assign luma.log.level in console to control logging: \ +// 0: none, 1: minimal, 2: verbose, 3: attribute/uniforms, 4: gl logs +// luma.log.break[], set to gl funcs, luma.log.profile[] set to model names`; + +if (globalThis.luma && globalThis.luma.VERSION !== VERSION) { + throw new Error(`luma.gl - multiple VERSIONs detected: ${globalThis.luma.VERSION} vs ${VERSION}`); +} + +if (!globalThis.luma) { + if (isBrowser()) { + log.log(1, `luma.gl ${VERSION} - ${STARTUP_MESSAGE}`)(); + } + + globalThis.luma = globalThis.luma || { + VERSION, + version: VERSION, + log, + + // A global stats object that various components can add information to + // E.g. see webgl/resource.js + stats: lumaStats, + }; +} + +export {lumaStats}; +export default globalThis.luma; diff --git a/modules/engine/test/lib/model.spec.js b/modules/engine/test/lib/model.spec.js index fc49153ff..d1cd2c15b 100644 --- a/modules/engine/test/lib/model.spec.js +++ b/modules/engine/test/lib/model.spec.js @@ -2,7 +2,7 @@ import test from 'tape-promise/tape'; import {webgl1TestDevice} from '@luma.gl/test-utils'; import GL from '@luma.gl/constants'; -import luma from '@luma.gl/webgl/init'; +import {luma} from '@luma.gl/api'; // TODO - Model test should not depend on Cube import {Model, ProgramManager} from '@luma.gl/engine'; import {Buffer} from '@luma.gl/webgl'; diff --git a/modules/webgl/src/classes/program-configuration.ts b/modules/webgl/src/classes/program-configuration.ts index 2c7415349..d6394c950 100644 --- a/modules/webgl/src/classes/program-configuration.ts +++ b/modules/webgl/src/classes/program-configuration.ts @@ -1,9 +1,9 @@ // Contains metadata describing attribute configurations for a program's shaders // Much of this is automatically extracted from shaders after program linking // import Accessor from './accessor'; -import type Program from './program'; -import type {AttributeBinding, VaryingBinding} from '../helpers/program-bindings'; +import type {AttributeBinding, VaryingBinding} from '@luma.gl/api'; import {getProgramBindings} from '../helpers/get-program-bindings'; +import type Program from './program'; /** * is a mechanism for taking a program object and querying information diff --git a/modules/webgl/src/classes/texture-2d.ts b/modules/webgl/src/classes/texture-2d.ts index 01d0247f5..bc24f2c30 100644 --- a/modules/webgl/src/classes/texture-2d.ts +++ b/modules/webgl/src/classes/texture-2d.ts @@ -4,6 +4,7 @@ import Texture, {TextureProps, TextureSupportOptions} from './texture'; export type Texture2DProps = TextureProps & { + format?: number; }; export default class Texture2D extends Texture { diff --git a/modules/webgl/src/classes/vertex-array.ts b/modules/webgl/src/classes/vertex-array.ts index 47f7dcede..319b5b111 100644 --- a/modules/webgl/src/classes/vertex-array.ts +++ b/modules/webgl/src/classes/vertex-array.ts @@ -1,9 +1,8 @@ -import {log, assert} from '@luma.gl/api'; +import {log, assert, AttributeBinding} from '@luma.gl/api'; import GL from '@luma.gl/constants'; import Accessor from './accessor'; import Buffer from './webgl-buffer'; import Program from './program'; -import type {AttributeBinding} from '../helpers/program-bindings' import ProgramConfiguration from './program-configuration'; import VertexArrayObject, {VertexArrayObjectProps} from './vertex-array-object'; diff --git a/modules/webgl/src/helpers/get-program-bindings.ts b/modules/webgl/src/helpers/get-program-bindings.ts index a2ca0393f..40fcbe5ae 100644 --- a/modules/webgl/src/helpers/get-program-bindings.ts +++ b/modules/webgl/src/helpers/get-program-bindings.ts @@ -1,6 +1,6 @@ +import {ProgramBindings, AttributeBinding, UniformBinding, UniformBlockBinding, VaryingBinding} from '@luma.gl/api'; import GL from '@luma.gl/constants'; import {isWebGL2} from '../context/context/webgl-checks'; -import {ProgramBindings, AttributeBinding, UniformBinding, UniformBlockBinding, VaryingBinding} from './program-bindings' import Accessor from '../classes/accessor'; import {decomposeCompositeGLType} from '../webgl-utils/attribute-utils'; @@ -44,6 +44,7 @@ function readAttributeBindings(gl: WebGL2RenderingContext, program: WebGLProgram inferProperties(location, name, accessor); const attributeInfo = {location, name, accessor: new Accessor(accessor)}; // Base values + // @ts-expect-error attributes.push(attributeInfo); } } diff --git a/modules/webgl/src/index.ts b/modules/webgl/src/index.ts index cbb83d202..fd7f9a4eb 100644 --- a/modules/webgl/src/index.ts +++ b/modules/webgl/src/index.ts @@ -5,7 +5,8 @@ // Higher level abstractions can be built on these classes // Initialize any global state -import './init'; +import '@luma.gl/api'; +import './init' // export type {WebGLDeviceProps, WebGLDeviceInfo, WebGPUDeviceLimits} from './lib/webgl-device'; export type {WebGLDeviceProps} from './adapter/webgl-device'; @@ -116,7 +117,7 @@ export {getProgramBindings} from './helpers/get-program-bindings'; // DEPRECATED // Deprecated re-exports -export {lumaStats} from './init'; +export {lumaStats} from '@luma.gl/api'; export {log, assert, uid, isObjectEmpty} from '@luma.gl/api'; export {setPathPrefix, loadFile, loadImage} from '@luma.gl/api'; diff --git a/modules/webgl/src/init.ts b/modules/webgl/src/init.ts index 762439eff..7f65a7678 100644 --- a/modules/webgl/src/init.ts +++ b/modules/webgl/src/init.ts @@ -1,48 +1,4 @@ -import {log} from '@luma.gl/api'; -import {isBrowser} from '@probe.gl/env'; -import {luma, lumaStats} from '@luma.gl/api'; +import {luma} from '@luma.gl/api'; import WebGLDevice from './adapter/webgl-device'; -// Version detection using babel plugin -// @ts-expect-error -const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'untranspiled source'; - -const STARTUP_MESSAGE = 'set luma.log.level=1 (or higher) to trace rendering'; -// Assign luma.log.level in console to control logging: \ -// 0: none, 1: minimal, 2: verbose, 3: attribute/uniforms, 4: gl logs -// luma.log.break[], set to gl funcs, luma.log.profile[] set to model names`; - -if (globalThis.luma && globalThis.luma.VERSION !== VERSION) { - throw new Error(`luma.gl - multiple VERSIONs detected: ${globalThis.luma.VERSION} vs ${VERSION}`); -} - -if (!globalThis.luma) { - luma.registerDevices([WebGLDevice]); - - if (isBrowser()) { - log.log(1, `luma.gl ${VERSION} - ${STARTUP_MESSAGE}`)(); - } - - globalThis.luma = globalThis.luma || { - VERSION, - version: VERSION, - log, - - // A global stats object that various components can add information to - // E.g. see webgl/resource.js - stats: lumaStats, - - // Keep some luma globals in a sub-object - // This allows us to dynamically detect if certain modules have been - // included (such as IO and headless) and enable related functionality, - // without unconditionally requiring and thus bundling big dependencies - // into the app. - globals: { - modules: {}, - nodeIO: {} - } - }; -} - -export {lumaStats}; -export default globalThis.luma; +luma.registerDevices([WebGLDevice]); diff --git a/modules/webgpu/src/adapter/helpers/accessor-to-format.ts b/modules/webgpu/src/adapter/helpers/accessor-to-format.ts new file mode 100644 index 000000000..edaf8ca20 --- /dev/null +++ b/modules/webgpu/src/adapter/helpers/accessor-to-format.ts @@ -0,0 +1,101 @@ +/* +import {assert} from '@luma.gl/api'; +import GL from '@luma.gl/constants'; + +type Accessor = Record; + +const FORMAT_TO_ACCESSOR: Record = { + uchar2: {type: 'uchar', size: 2}, + uchar4: {type: 'uchar', size: 4}, + char2: {type: 'char', size: 2}, + char4: {type: 'char', size: 4}, + uchar2norm: {type: 'uchar', size: 2, normalized: true}, + uchar4norm: {type: 'uchar', size: 4, normalized: true}, + char2norm: {type: 'char', size: 2, normalized: true}, + char4norm: {type: 'char', size: 4, normalized: true}, + ushort2: {type: 'ushort', size: 2}, + ushort4: {type: 'ushort', size: 4}, + short2: {type: 'short', size: 2}, + short4: {type: 'short', size: 4}, + ushort2norm: {type: 'ushort', size: 2, normalized: true}, + ushort4norm: {type: 'ushort', size: 4, normalized: true}, + short2norm: {type: 'short', size: 1, normalized: true}, + short4norm: {type: 'short', size: 1, normalized: true}, + half2: {type: 'half', size: 2}, + half4: {type: 'half', size: 4}, + float: {type: 'float', size: 1}, + float2: {type: 'float', size: 2}, + float3: {type: 'float', size: 3}, + float4: {type: 'float', size: 4}, + uint: {type: 'uint', size: 1, integer: true}, + uint2: {type: 'uint', size: 2, integer: true}, + uint3: {type: 'uint', size: 3, integer: true}, + uint4: {type: 'uint', size: 4, integer: true}, + int: {type: 'int', size: 1, integer: true}, + int2: {type: 'int', size: 2, integer: true}, + int3: {type: 'int', size: 3, integer: true}, + int4: {type: 'int', size: 4, integer: true} +}; + +/** + * Convert from WebGPU attribute format strings to accessor {type, size, normalized, integer} + * @param {*} format + * +export function mapWebGPUFormatToAccessor(format) { + const accessorDefinition = FORMAT_TO_ACCESSOR[format]; + assert(accessorDefinition, 'invalid attribute format'); + return Object.freeze(accessorDefinition); +} + +/** + * Convert from accessor {type, size, normalized, integer} to WebGPU attribute format strings + * @param {*} format + * +export function mapAccessorToWebGPUFormat(accessor) { + const {type = GL.FLOAT, size = 1, normalized = false, integer = false} = accessor; + assert(size >=1 && size <=4); + // `norm` suffix (uchar4norm) + const norm = normalized ? 'norm' : ''; + // size 1 is ommitted in format names (float vs float2) + const count = size === 1 ? '' : size; + switch (type) { + case GL.UNSIGNED_BYTE: + switch (size) { + case 2: + case 4: + return `uchar${count}${norm}`; + } + case GL.BYTE: + switch (size) { + case 2: + case 4: + return `char${count}${norm}`; + } + case GL.UNSIGNED_SHORT: + switch (size) { + case 2: + case 4: + return `ushort${count}${norm}`; + } + case GL.SHORT: + switch (size) { + case 2: + case 4: + return `short${count}${norm}`; + } + case GL.HALF_FLOAT: + switch (size) { + case 2: + case 4: + return `half${count}`; + } + case GL.FLOAT: + return `float${count}`; + case GL.UNSIGNED_INT: + return `uint${count}`; + case GL.INT: + return `int${count}`; + } + throw new Error('illegal accessor'); +} +*/ \ No newline at end of file diff --git a/modules/webgpu/src/adapter/helpers/get-vertex-buffer-layout.ts b/modules/webgpu/src/adapter/helpers/get-vertex-buffer-layout.ts new file mode 100644 index 000000000..06ccd7d0a --- /dev/null +++ b/modules/webgpu/src/adapter/helpers/get-vertex-buffer-layout.ts @@ -0,0 +1,107 @@ +import {VertexFormat, AttributeBinding, assert} from '@luma.gl/api'; + +export type Attribute = { + name: string; + location: number; + accessor: { + format: VertexFormat; + offset?: number; + stride?: number; + stepMode?: 'vertex' | 'instance'; + divisor?: number; + } +}; + +// type InterleavedAttributes = { +// byteStride?: number; +// stepMode?: +// attributes: Attribute[]; +// } + +export function convertAttributesVertexBufferToLayout(attributes: Attribute[]): GPUVertexBufferLayout[] { + const vertexBufferLayouts: GPUVertexBufferLayout[] = []; + + for (const attribute of attributes) { + const arrayStride = attribute.accessor.stride || getVertexFormatBytes(attribute.accessor.format); + const stepMode = attribute.accessor.stepMode || (attribute.accessor.divisor ? 'instance' : 'vertex'); + vertexBufferLayouts.push({ + arrayStride, + stepMode, + attributes: [ + { + format: attribute.accessor.format, + offset: attribute.accessor.offset || 0, + shaderLocation: attribute.location + } + ] + }); + } + + return vertexBufferLayouts; +} + +function getVertexFormatBytes(format: GPUVertexFormat): number { + const [type, count] = format.split('x'); + const bytes = getTypeBytes(type); + assert(bytes); + return bytes * parseInt(count); +} + +const TYPE_SIZES = { + uint8: 1, + sint8: 1, + unorm8: 1, + snorm8: 1, + uint16: 2, + sint16: 2, + unorm16: 2, + snorm16: 2, + float16: 2, + float32: 4, + uint32: 4, + sint32: 4, +}; + +function getTypeBytes(type: string): number { + const bytes = TYPE_SIZES[type]; + assert(bytes); + return bytes; +} + +/** + * Attempt to convert legacy luma.gl accessors to attribute infos + * @param bufferAccessors + * + function getVertexBuffers(bufferAccessors: BufferAccessors[]) { + const vertexBuffers = []; + + for (const buffer of bufferAccessors) { + let stride = null; + let divisor = null; + const attributes = []; + + for (const accessor of buffer.attributes) { + if ('stride' in accessor) { + stride = accessor.stride; + } + if ('divisor' in accessor) { + divisor = accessor.divisor; + } + + attributes.push({ + format: accessor.format || mapAccessorToWebGPUFormat(accessor), + offset: accessor.offset || 0, + location: accessor.location + }); + } + + vertexBuffers.push({ + stride: buffer.stride || stride || 0, + stepMode: buffer.stepMode || (divisor ? 'instance' : 'vertex'), + attributes + }); + } + + return vertexBuffers; +} +*/ \ No newline at end of file diff --git a/modules/webgpu/src/adapter/helpers/make-bind-group-layout.ts b/modules/webgpu/src/adapter/helpers/make-bind-group-layout.ts index 6cee4c414..a9c087743 100644 --- a/modules/webgpu/src/adapter/helpers/make-bind-group-layout.ts +++ b/modules/webgpu/src/adapter/helpers/make-bind-group-layout.ts @@ -1,8 +1,8 @@ // import {Binding, Buffer, Sampler, Texture, cast} from '@luma.gl/api'; -import type WebGPUBuffer from '../webgpu-buffer'; -import type WebGPUSampler from '../webgpu-sampler'; -import type WebGPUTexture from '../webgpu-texture'; +import type WebGPUBuffer from '../resources/webgpu-buffer'; +import type WebGPUSampler from '../resources/webgpu-sampler'; +import type WebGPUTexture from '../resources/webgpu-texture'; /** * Create a WebGPU bind group layout from an array of luma.gl bindings diff --git a/modules/webgpu/src/adapter/helpers/make-bind-group.ts b/modules/webgpu/src/adapter/helpers/make-bind-group.ts index 250bbe28e..a60cd200e 100644 --- a/modules/webgpu/src/adapter/helpers/make-bind-group.ts +++ b/modules/webgpu/src/adapter/helpers/make-bind-group.ts @@ -1,8 +1,8 @@ // import {Binding, Buffer, Sampler, Texture, cast} from '@luma.gl/api'; -import type WebGPUBuffer from '../webgpu-buffer'; -import type WebGPUSampler from '../webgpu-sampler'; -import type WebGPUTexture from '../webgpu-texture'; +import type WebGPUBuffer from '../resources/webgpu-buffer'; +import type WebGPUSampler from '../resources/webgpu-sampler'; +import type WebGPUTexture from '../resources/webgpu-texture'; /** * Create a WebGPU bind group from an array of luma.gl bindings diff --git a/modules/webgpu/src/adapter/helpers/webgpu-parameters.ts b/modules/webgpu/src/adapter/helpers/webgpu-parameters.ts index da790088e..5e559aab1 100644 --- a/modules/webgpu/src/adapter/helpers/webgpu-parameters.ts +++ b/modules/webgpu/src/adapter/helpers/webgpu-parameters.ts @@ -2,131 +2,178 @@ import {Parameters} from '@luma.gl/api'; type PipelineDescriptor = Omit; + +function addDepthStencil(descriptor: GPURenderPipelineDescriptor): void { + descriptor.depthStencil = descriptor.depthStencil || { + // required, set something + format: 'depth24plus', + stencilFront: {}, + stencilBack: {}, + }; +} + /** * Supports for luma.gl's flat parameter space * Populates the corresponding sub-objects in a PipelineDescriptor */ -export const PARAMETER_TABLE = { +// @ts-expect-error +export const PARAMETER_TABLE: Record = { // RASTERIZATION PARAMETERS - /* cullMode: (parameter, value, descriptor: PipelineDescriptor) => { - descriptor.rasterizationState.cullMode = value; + descriptor.primitive.cullMode = value; }, - cullFace: (parameter, value, descriptor: PipelineDescriptor) => { - descriptor.rasterizationState.frontFace = value; - }, - - depthBias: (parameter, value, descriptor: PipelineDescriptor) => { - descriptor.rasterizationState.depthBias = value; - }, - - depthBiasSlopeScale: (parameter, value, descriptor: PipelineDescriptor) => { - descriptor.rasterizationState.depthBiasSlopeScale = value; - }, - - depthBiasClamp: (parameter, value, descriptor: PipelineDescriptor) => { - descriptor.rasterizationState.depthBiasClamp = value; - }, - - // STENCIL - - stencilReadMask: (parameter, value, descriptor: PipelineDescriptor) => { - descriptor.depthStencilState.stencilReadMask = value; - }, - - stencilWriteMask: (parameter, value, descriptor: PipelineDescriptor) => { - descriptor.depthStencilState.stencilWriteMask = value; - }, - - stencilCompare: (parameter, value, descriptor: PipelineDescriptor) => { - descriptor.depthStencilState.stencilFront.compare = value; - descriptor.depthStencilState.stencilBack.compare = value; - }, - - stencilPassOperation: (parameter, value, descriptor: PipelineDescriptor) => { - descriptor.depthStencilState.stencilFront.passOp = value; - descriptor.depthStencilState.stencilBack.passOp = value; - }, - - stencilFailOperation: (parameter, value, descriptor: PipelineDescriptor) => { - descriptor.depthStencilState.stencilFront.failOp = value; - descriptor.depthStencilState.stencilBack.failOp = value; - }, - - stencilDepthFailOperation: (parameter, value, descriptor: PipelineDescriptor) => { - descriptor.depthStencilState.stencilFront.depthFailOp = value; - descriptor.depthStencilState.stencilBack.depthFailOp = value; + + frontFace: (parameter, value, descriptor: PipelineDescriptor) => { + descriptor.primitive.frontFace = value; }, // DEPTH depthWriteEnabled: (parameter, value, descriptor: PipelineDescriptor) => { - descriptor.depthStencilState.depthWriteEnabled = value; + addDepthStencil(descriptor); + descriptor.depthStencil.depthWriteEnabled = value; }, depthCompare: (parameter, value, descriptor: PipelineDescriptor) => { - descriptor.depthStencilState.depthCompare = value; + addDepthStencil(descriptor); + descriptor.depthStencil.depthCompare = value; }, + + depthFormat: (parameter, value, descriptor: PipelineDescriptor) => { + addDepthStencil(descriptor); + descriptor.depthStencil.format = value; + }, + + depthBias: (parameter, value, descriptor: PipelineDescriptor) => { + addDepthStencil(descriptor); + descriptor.depthStencil.depthBias = value; + }, + + depthBiasSlopeScale: (parameter, value, descriptor: PipelineDescriptor) => { + addDepthStencil(descriptor); + descriptor.depthStencil.depthBiasSlopeScale = value; + }, + + depthBiasClamp: (parameter, value, descriptor: PipelineDescriptor) => { + addDepthStencil(descriptor); + descriptor.depthStencil.depthBiasClamp = value; + }, + + // STENCIL + + stencilReadMask: (parameter, value, descriptor: PipelineDescriptor) => { + addDepthStencil(descriptor); + descriptor.depthStencil.stencilReadMask = value; + }, + + stencilWriteMask: (parameter, value, descriptor: PipelineDescriptor) => { + addDepthStencil(descriptor); + descriptor.depthStencil.stencilWriteMask = value; + }, + + stencilCompare: (parameter, value, descriptor: PipelineDescriptor) => { + addDepthStencil(descriptor); + descriptor.depthStencil.stencilFront.compare = value; + descriptor.depthStencil.stencilBack.compare = value; + }, + + stencilPassOperation: (parameter, value, descriptor: PipelineDescriptor) => { + addDepthStencil(descriptor); + descriptor.depthStencil.stencilFront.passOp = value; + descriptor.depthStencil.stencilBack.passOp = value; + }, + + stencilFailOperation: (parameter, value, descriptor: PipelineDescriptor) => { + addDepthStencil(descriptor); + descriptor.depthStencil.stencilFront.failOp = value; + descriptor.depthStencil.stencilBack.failOp = value; + }, + + stencilDepthFailOperation: (parameter, value, descriptor: PipelineDescriptor) => { + addDepthStencil(descriptor); + descriptor.depthStencil.stencilFront.depthFailOp = value; + descriptor.depthStencil.stencilBack.depthFailOp = value; + }, + + // MULTISAMPLE + + sampleCount: (parameter, value, descriptor: PipelineDescriptor) => { + descriptor.multisample = descriptor.multisample || {}; + descriptor.multisample.count = value; + }, + + sampleMask: (parameter, value, descriptor: PipelineDescriptor) => { + descriptor.multisample = descriptor.multisample || {}; + descriptor.multisample.mask = value; + }, + + sampleAlphaToCoverageEnabled: (parameter, value, descriptor: PipelineDescriptor) => { + descriptor.multisample = descriptor.multisample || {}; + descriptor.multisample.alphaToCoverageEnabled = value; + }, + + // COLOR colorMask: (parameter, value, descriptor: PipelineDescriptor) => { addColorState(descriptor); - descriptor.colorStates[0].writeMask = value; + descriptor.fragment.targets[0].writeMask = value; }, blendColorOperation: (parameter, value, descriptor: PipelineDescriptor) => { addColorState(descriptor); - descriptor.colorStates[0].blend = descriptor.colorStates[0].blend || {}; - descriptor.colorStates[0].blend.color = descriptor.colorStates[0].blend.color || {}; - descriptor.colorStates[0].blend.color.operation = value; + descriptor.fragment.targets[0].blend = descriptor.fragment.targets[0].blend || {}; + descriptor.fragment.targets[0].blend.color = descriptor.fragment.targets[0].blend.color || {}; + descriptor.fragment.targets[0].blend.color.operation = value; }, + /* blendColorSrcTarget: (parameter, value, descriptor: PipelineDescriptor) => { addColorState(descriptor); - descriptor.colorStates[0].blend = descriptor.colorStates[0].blend || {}; - descriptor.colorStates[0].blend.color = descriptor.colorStates[0].blend.color || {}; - descriptor.colorStates[0].blend.color.srcTarget = value; + descriptor.fragment.targets[0].blend = descriptor.fragment.targets[0].blend || {}; + descriptor.fragment.targets[0].blend.color = descriptor.fragment.targets[0].blend.color || {}; + descriptor.fragment.targets[0].blend.color.srcTarget = value; }, blendColorDstTarget: (parameter, value, descriptor: PipelineDescriptor) => { addColorState(descriptor); - descriptor.colorStates[0].blend = descriptor.colorStates[0].blend || {}; - descriptor.colorStates[0].blend.color = descriptor.colorStates[0].blend.color || {}; - descriptor.colorStates[0].blend.color.dstTarget = value; + descriptor.fragment.targets[0].blend = descriptor.fragment.targets[0].blend || {}; + descriptor.fragment.targets[0].blend.color = descriptor.fragment.targets[0].blend.color || {}; + descriptor.fragment.targets[0].blend.color.dstTarget = value; }, blendAlphaOperation: (parameter, value, descriptor: PipelineDescriptor) => { addColorState(descriptor); - descriptor.colorStates[0].blend = descriptor.colorStates[0].blend || {}; - descriptor.colorStates[0].blend.alpha = descriptor.colorStates[0].blend.alpha || {}; - descriptor.colorStates[0].blend.alpha.operation = value; + descriptor.fragment.targets[0].blend = descriptor.fragment.targets[0].blend || {}; + descriptor.fragment.targets[0].blend.alpha = descriptor.fragment.targets[0].blend.alpha || {}; + descriptor.fragment.targets[0].blend.alpha.operation = value; }, blendAlphaSrcTarget: (parameter, value, descriptor: PipelineDescriptor) => { addColorState(descriptor); - descriptor.colorStates[0].blend = descriptor.colorStates[0].blend || {}; - descriptor.colorStates[0].blend.alpha = descriptor.colorStates[0].blend.alpha || {}; - descriptor.colorStates[0].blend.alpha.srcTarget = value; + descriptor.fragment.targets[0].blend = descriptor.fragment.targets[0].blend || {}; + descriptor.fragment.targets[0].blend.alpha = descriptor.fragment.targets[0].blend.alpha || {}; + descriptor.fragment.targets[0].blend.alpha.srcTarget = value; }, blendAlphaDstTarget: (parameter, value, descriptor: PipelineDescriptor) => { addColorState(descriptor); - descriptor.colorStates[0].blend = descriptor.colorStates[0].blend || {}; - descriptor.colorStates[0].blend.alpha = descriptor.colorStates[0].blend.alpha || {}; - descriptor.colorStates[0].blend.alpha.dstTarget = value; + descriptor.fragment.targets[0].blend = descriptor.fragment.targets[0].blend || {}; + descriptor.fragment.targets[0].blend.alpha = descriptor.fragment.targets[0].blend.alpha || {}; + descriptor.fragment.targets[0].blend.alpha.dstTarget = value; }, */ }; const DEFAULT_PIPELINE_DESCRIPTOR: PipelineDescriptor = { - depthStencil: { - stencilFront: {}, - stencilBack: {}, - depthWriteEnabled: true, - depthCompare: 'less', - format: 'depth24plus-stencil8', - }, + // depthStencil: { + // stencilFront: {}, + // stencilBack: {}, + // // depthWriteEnabled: true, + // // depthCompare: 'less', + // // format: 'depth24plus-stencil8', + // }, primitive: { cullMode: 'back', @@ -142,35 +189,18 @@ const DEFAULT_PIPELINE_DESCRIPTOR: PipelineDescriptor = { module: undefined, entryPoint: 'main', targets: [ + // { format: props.color0Format || 'bgra8unorm' } ] } } -export function getRenderPipelineDescriptor(parameters: Parameters = {}, props = {}): PipelineDescriptor { - const pipelineDescriptor: PipelineDescriptor = {...DEFAULT_PIPELINE_DESCRIPTOR}; - // pipelineDescriptor. - // rasterizationState: { - // cullMode: 'back', - // }, - - // depthStencilState: { - // depthWriteEnabled: true, - // depthCompare: 'less', - // format: props.depthStencilFormat || 'depth24plus-stencil8', - // }, - - // colorStates: [ - // { - // format: props.color0Format || 'bgra8unorm', - // } - // ], - +export function applyParametersToRenderPipelineDescriptor(pipelineDescriptor, parameters: Parameters = {}): PipelineDescriptor { + pipelineDescriptor = {...DEFAULT_PIPELINE_DESCRIPTOR, ...pipelineDescriptor}; setParameters(pipelineDescriptor, parameters); - return pipelineDescriptor; } - // Apply any supplied parameters +// Apply any supplied parameters function setParameters(pipelineDescriptor: PipelineDescriptor, parameters: Parameters): void { for (const key in parameters) { const value = parameters[key]; @@ -182,11 +212,11 @@ function setParameters(pipelineDescriptor: PipelineDescriptor, parameters: Param } } -// function addColorState(descriptor: PipelineDescriptor): void { -// descriptor.colorStates = descriptor.colorStates || []; -// // @ts-expect-error -// if (descriptor.colorStates.length === 0) { -// // @ts-expect-error -// descriptor.colorStates.push({}); -// } -// } +function addColorState(descriptor: PipelineDescriptor): void { + descriptor.fragment.targets = descriptor.fragment.targets || []; + // @ts-expect-error + if (descriptor.fragment.targets.length === 0) { + // @ts-expect-error + descriptor.fragment.targets.push({}); + } +} diff --git a/modules/webgpu/src/adapter/resources/webgpu-buffer.ts b/modules/webgpu/src/adapter/resources/webgpu-buffer.ts new file mode 100644 index 000000000..a2cba4035 --- /dev/null +++ b/modules/webgpu/src/adapter/resources/webgpu-buffer.ts @@ -0,0 +1,77 @@ +// WEBGPU Buffer implementation +import {Buffer, BufferProps, assert} from '@luma.gl/api'; +import type WebGPUDevice from '../webgpu-device'; + +function getByteLength(props: BufferProps): number { + return props.byteLength >= 0 ? props.byteLength : props.data.byteLength; +} + +export default class WebGPUBuffer extends Buffer { + readonly device: WebGPUDevice; + readonly handle: GPUBuffer; + readonly byteLength: number; + + constructor(device: WebGPUDevice, props: BufferProps) { + super(device, props); + this.device = device; + + this.byteLength = getByteLength(props); + const mapBuffer = Boolean(props.data); + + this.handle = this.props.handle || this.createHandle(mapBuffer); + this.handle.label = this.props.id; + + if (props.data) { + this._writeMapped(props.data); + // this.handle.writeAsync({data: props.data, map: false, unmap: false}); + } + + if (mapBuffer && !props.mappedAtCreation) { + this.handle.unmap(); + } + } + + protected createHandle(mapBuffer: boolean): GPUBuffer { + return this.device.handle.createBuffer({ + size: this.byteLength, + // usage defaults to vertex + usage: this.props.usage || (GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST), + mappedAtCreation: this.props.mappedAtCreation || mapBuffer + }); + } + + destroy(): void { + this.handle.destroy(); + } + + // WebGPU provides multiple ways to write a buffer... + write(data: ArrayBufferView, byteOffset = 0) { + this.device.handle.queue.writeBuffer( + this.handle, + byteOffset, + data.buffer, + data.byteOffset, + data.byteLength + ); + } + + _writeMapped(typedArray: TypedArray): void { + const arrayBuffer = this.handle.getMappedRange(); + // @ts-expect-error + new typedArray.constructor(arrayBuffer).set(typedArray); + } + + // WEBGPU API + + mapAsync(mode: number, offset: number = 0, size?: number): Promise { + return this.handle.mapAsync(mode, offset, size); + } + + getMappedRange(offset: number = 0, size?: number): ArrayBuffer { + return this.handle.getMappedRange(offset, size); + } + + unmap(): void { + this.handle.unmap(); + } +} diff --git a/modules/webgpu/src/adapter/resources/webgpu-framebuffer.ts b/modules/webgpu/src/adapter/resources/webgpu-framebuffer.ts new file mode 100644 index 000000000..65f2fcc25 --- /dev/null +++ b/modules/webgpu/src/adapter/resources/webgpu-framebuffer.ts @@ -0,0 +1,51 @@ +import WebGPUDevice from "../webgpu-device"; + +export type FramebufferProps = { // ResourceProps & { + width: number; + height: number; + attachments?: Record; + readBuffer?: number; + drawBuffers?: number[]; + check?: boolean; +}; + +export class WebGPUFramebuffer { + readonly device: WebGPUDevice; + renderPassDescriptor: GPURenderPassDescriptor; + + constructor(device: WebGPUDevice, props: FramebufferProps) { + this.device = device; + + const depthTexture = this.device.createTexture({ + id: "depth-stencil", + width: props.width, + height: props.height, + depth: 1, + format: "depth24plus", + usage: GPUTextureUsage.RENDER_ATTACHMENT + }); + + const depthStencilAttachment = depthTexture.handle.createView(); + depthStencilAttachment.label = 'depth-stencil-attachment'; + + const renderPassDescriptor: GPURenderPassDescriptor = { + colorAttachments: [{ + // @ts-expect-error + attachment: undefined, // Assigned later + loadValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 }, + }], + + depthStencil: { + attachment: depthStencilAttachment, + depthLoadValue: 1.0, + depthStoreOp: "store", + // stencilLoadValue: 0, + // stencilStoreOp: "store", + } + }; + + + } + + +} \ No newline at end of file diff --git a/modules/webgpu/src/adapter/webgpu-render-pipeline.ts b/modules/webgpu/src/adapter/resources/webgpu-render-pipeline.ts similarity index 82% rename from modules/webgpu/src/adapter/webgpu-render-pipeline.ts rename to modules/webgpu/src/adapter/resources/webgpu-render-pipeline.ts index 10ef74770..7ad37e58c 100644 --- a/modules/webgpu/src/adapter/webgpu-render-pipeline.ts +++ b/modules/webgpu/src/adapter/resources/webgpu-render-pipeline.ts @@ -1,12 +1,13 @@ /// -import {RenderPipeline, RenderPipelineProps, cast } from '@luma.gl/api'; +import {RenderPipeline, RenderPipelineProps, cast, log} from '@luma.gl/api'; -import WebGPUDevice from './webgpu-device'; +import type WebGPUDevice from '../webgpu-device'; -import {getRenderPipelineDescriptor} from './helpers/webgpu-parameters'; +import {applyParametersToRenderPipelineDescriptor} from '../helpers/webgpu-parameters'; +import {convertAttributesVertexBufferToLayout} from '../helpers/get-vertex-buffer-layout'; // import {mapAccessorToWebGPUFormat} from './helpers/accessor-to-format'; -import {WebGPUShader} from '..'; +import WebGPUShader from './webgpu-shader'; // import type {BufferAccessors} from './webgpu-pipeline'; // BIND GROUP LAYOUTS @@ -57,7 +58,8 @@ export default class WebGPURenderPipeline extends RenderPipeline { const vertex: GPUVertexState = { module: cast(this.props.vertexShader).handle, - entryPoint: this.props.vertexShaderEntryPoint || 'main' + entryPoint: this.props.vertexShaderEntryPoint || 'main', + buffers: convertAttributesVertexBufferToLayout(this.props.attributeLayouts) }; let fragment: GPUFragmentState | undefined; @@ -73,27 +75,19 @@ export default class WebGPURenderPipeline extends RenderPipeline { }; } - const descriptor: GPURenderPipelineDescriptor = { + let descriptor: GPURenderPipelineDescriptor = { vertex, fragment, primitive: { topology: this.props.topology } - - // WebGPU spec seems updated - // primitive: { - // topology: this.props.topology - // }, - }; - // const ceDescriptor: GPUCommandEncoderDescriptor; - // const commandEncoder = this.device.handle.createCommandEncoder({ - // // label - // // measureExecutionTime - // }); + descriptor = applyParametersToRenderPipelineDescriptor(descriptor, this.props.parameters); - getRenderPipelineDescriptor(this.props.parameters, descriptor); + log.groupCollapsed(1, 'RenderPipeline.GPURenderPipelineDescriptor')(); + log.log(1, JSON.stringify(descriptor, null, 2))(); + log.groupEnd(1)(); const renderPipeline = this.device.handle.createRenderPipeline(descriptor); return renderPipeline; diff --git a/modules/webgpu/src/adapter/webgpu-sampler.ts b/modules/webgpu/src/adapter/resources/webgpu-sampler.ts similarity index 91% rename from modules/webgpu/src/adapter/webgpu-sampler.ts rename to modules/webgpu/src/adapter/resources/webgpu-sampler.ts index a397154a9..0da3f763b 100644 --- a/modules/webgpu/src/adapter/webgpu-sampler.ts +++ b/modules/webgpu/src/adapter/resources/webgpu-sampler.ts @@ -1,5 +1,5 @@ import {Sampler, SamplerProps} from '@luma.gl/api'; -import type WebGPUDevice from './webgpu-device'; +import type WebGPUDevice from '../webgpu-device'; export type WebGPUSamplerProps = SamplerProps & { handle?: GPUSampler; diff --git a/modules/webgpu/src/adapter/webgpu-shader.ts b/modules/webgpu/src/adapter/resources/webgpu-shader.ts similarity index 96% rename from modules/webgpu/src/adapter/webgpu-shader.ts rename to modules/webgpu/src/adapter/resources/webgpu-shader.ts index b2d7587f0..7b4f333b4 100644 --- a/modules/webgpu/src/adapter/webgpu-shader.ts +++ b/modules/webgpu/src/adapter/resources/webgpu-shader.ts @@ -1,5 +1,5 @@ import {Shader, ShaderProps, CompilerMessage} from '@luma.gl/api'; -import type WebGPUDevice from './webgpu-device'; +import type WebGPUDevice from '../webgpu-device'; export type WebGPUShaderProps = ShaderProps & { handle?: GPUShaderModule; @@ -14,7 +14,6 @@ export default class WebGPUShader extends Shader { constructor(device: WebGPUDevice, props: WebGPUShaderProps) { super(device, props); - this.device = device; this.handle = this.props.handle || this.createHandle(); diff --git a/modules/webgpu/src/adapter/webgpu-texture.ts b/modules/webgpu/src/adapter/resources/webgpu-texture.ts similarity index 89% rename from modules/webgpu/src/adapter/webgpu-texture.ts rename to modules/webgpu/src/adapter/resources/webgpu-texture.ts index cc9848654..a65a0581e 100644 --- a/modules/webgpu/src/adapter/webgpu-texture.ts +++ b/modules/webgpu/src/adapter/resources/webgpu-texture.ts @@ -1,18 +1,10 @@ // Inspired by webgpu samples at https://github.com/austinEng/webgpu-samples/blob/master/src/glslang.ts // under BSD 3-clause license /// -import {Resource, Texture, TextureProps, Device, Sampler, SamplerProps} from '@luma.gl/api'; -import type WebGPUDevice from './webgpu-device'; +import {Texture, TextureProps, Sampler, SamplerProps, assert} from '@luma.gl/api'; +import type WebGPUDevice from '../webgpu-device'; import WebGPUSampler from './webgpu-sampler'; -// const DEFAULT_TEXTURE_PROPS: Required = { -// handle: undefined, -// id: undefined, -// depth: 1, -// format: 'rgba8unorm', -// usage: GPUTextureUsage.COPY_DST -// }; - export default class WebGPUTexture extends Texture { readonly device: WebGPUDevice; readonly handle: GPUTexture; @@ -28,26 +20,26 @@ export default class WebGPUTexture extends Texture { // static createFromImage(img, usage = 0) { // return new WebGPUTexture({width: img.width, height:img.height, usage}).setImage(image, usage); // } + constructor(device: WebGPUDevice, props: TextureProps) { super(device, props); - this.device = device; this.handle = this.props.handle || this.createHandle(); - this.sampler = null; } protected createHandle(): GPUTexture { + if (typeof this.props.format === 'number') { + throw new Error('number format'); + } return this.device.handle.createTexture({ size: { width: this.props.width, height: this.props.height, depthOrArrayLayers: this.props.depth }, - // @ts-expect-error format: this.props.format, - // @ts-expect-error - usage: GPUTextureUsage.COPY_DST | this.props.usage + usage: this.props.usage }); } diff --git a/modules/webgpu/src/adapter/webgpu-buffer.ts b/modules/webgpu/src/adapter/webgpu-buffer.ts deleted file mode 100644 index 575824f65..000000000 --- a/modules/webgpu/src/adapter/webgpu-buffer.ts +++ /dev/null @@ -1,47 +0,0 @@ -// WEBGPU Buffer implementation -import {Buffer, BufferProps} from '@luma.gl/api'; -import WebGPUDevice from './webgpu-device'; - -export default class WebGPUBuffer extends Buffer { - readonly device: WebGPUDevice; - readonly handle: GPUBuffer; - - constructor(device: WebGPUDevice, props: BufferProps) { - super(device, props); - - this.handle = this.props.handle || this.createHandle(); - this.handle.label = this.props.id; - - if (props.data) { - // this.handle.writeAsync({data: props.data, map: false, unmap: false}); - } - - if (!props.mappedAtCreation) { - this.handle.unmap(); - } - } - - protected createHandle(): GPUBuffer { - return this.device.handle.createBuffer({ - size: this.props.byteLength, - usage: this.props.usage || (GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC), - mappedAtCreation: this.props.mappedAtCreation - }); - } - - destroy(): void { - this.handle.destroy(); - } - - mapAsync(mode: number, offset: number = 0, size?: number): Promise { - return this.handle.mapAsync(mode, offset, size); - } - - getMappedRange(offset: number = 0, size?: number): ArrayBuffer { - return this.handle.getMappedRange(offset, size); - } - - unmap(): void { - this.handle.unmap(); - } -} diff --git a/modules/webgpu/src/adapter/webgpu-device.ts b/modules/webgpu/src/adapter/webgpu-device.ts index 167e904a8..2c71cb70a 100644 --- a/modules/webgpu/src/adapter/webgpu-device.ts +++ b/modules/webgpu/src/adapter/webgpu-device.ts @@ -9,7 +9,8 @@ import { TextureProps, RenderPipelineProps, // ComputePipelineProps, - assert + assert, + log } from '@luma.gl/api'; // import type { // CopyBufferToBufferOptions, @@ -17,11 +18,11 @@ import { // CopyTextureToBufferOptions, // CopyTextureToTextureOptions // } from '@luma.gl/api'; -import WebGPUBuffer from './webgpu-buffer'; -import WebGPUTexture from './webgpu-texture'; -import WebGPUSampler from './webgpu-sampler'; -import WebGPUShader from './webgpu-shader'; -import WebGPURenderPipeline from './webgpu-render-pipeline'; +import WebGPUBuffer from './resources/webgpu-buffer'; +import WebGPUTexture from './resources/webgpu-texture'; +import WebGPUSampler from './resources/webgpu-sampler'; +import WebGPUShader from './resources/webgpu-shader'; +import WebGPURenderPipeline from './resources/webgpu-render-pipeline'; // import WebGPUComputePipeline from './webgpu-compute-pipeline'; // import {loadGlslangModule} from '../glsl/glslang'; @@ -41,6 +42,7 @@ export default class WebGPUDevice extends Device { readonly presentationFormat: GPUTextureFormat; presentationSize = [1, 1]; private _renderPassDescriptor: GPURenderPassDescriptor; + private _info: DeviceInfo; static isSupported(): boolean { return true; @@ -48,13 +50,19 @@ export default class WebGPUDevice extends Device { static async create(props) { if (!navigator.gpu) { - throw new Error('WebGPU not available'); + throw new Error('WebGPU not available. Use Chrome Canary and turn on chrome://flags/#enable-unsafe-webgpu'); } + log.groupCollapsed(1, 'Creating device')(); const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" }); - const device = await adapter.requestDevice(); - return new WebGPUDevice(device, adapter, props); + log.log(1, "Adapter available")(); + const gpuDevice = await adapter.requestDevice(); + log.log(1, "GPUDevice available")(); + const device = new WebGPUDevice(gpuDevice, adapter, props); + log.log(1, "Device created", device.info)(); + log.groupEnd(1)(); + return device; } constructor(device: GPUDevice, adapter: GPUAdapter, props: DeviceProps) { @@ -62,6 +70,21 @@ export default class WebGPUDevice extends Device { this.handle = device; this.adapter = adapter; + this._info = { + type: 'webgpu', + vendor: this.adapter.name, + renderer: '', + version: '', + gpuVendor: 'UNKNOWN', // 'NVIDIA' | 'AMD' | 'INTEL' | 'APPLE' | 'UNKNOWN', + shadingLanguages: ['glsl', 'wgsl'], + shadingLanguageVersions: { + glsl: '450', + wgsl: '100' + }, + vendorMasked: '', + rendererMasked: '' + }; + // Configure swap chain assert(props.canvas); this.context = props.canvas.getContext('webgpu') as GPUCanvasContext; @@ -78,7 +101,6 @@ export default class WebGPUDevice extends Device { format: this.presentationFormat, size: this.presentationSize, }); - this._initializeRenderPassDescriptor(props.canvas) } destroy() { @@ -86,20 +108,7 @@ export default class WebGPUDevice extends Device { } get info(): DeviceInfo { - return { - type: 'webgpu', - vendor: '', - renderer: '', - version: '', - gpuVendor: 'UNKNOWN', // 'NVIDIA' | 'AMD' | 'INTEL' | 'APPLE' | 'UNKNOWN', - shadingLanguages: ['glsl', 'wgsl'], - shadingLanguageVersions: { - glsl: '450', - wgsl: '100' - }, - vendorMasked: '', - rendererMasked: '' - }; + return this._info; } get features(): Set { @@ -136,18 +145,7 @@ export default class WebGPUDevice extends Device { beginRenderPass(): GPURenderPassEncoder { if (!this.renderPass) { this.commandEncoder = this.handle.createCommandEncoder(); - - const textureView = this.context.getCurrentTexture().createView(); - const renderPassDescriptor: GPURenderPassDescriptor = { - colorAttachments: [ - { - view: textureView, - loadValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, - storeOp: 'store', - }, - ], - }; - + const renderPassDescriptor = this._updateRenderPassDescriptor(); this.renderPass = this.commandEncoder.beginRenderPass(renderPassDescriptor); } return this.renderPass; @@ -165,25 +163,27 @@ export default class WebGPUDevice extends Device { } // TODO: Possible to support multiple canvases with one device? - _initializeRenderPassDescriptor(canvas) { + _initializeRenderPassDescriptor() { const depthTexture = this.createTexture({ - width: canvas.width, - height: canvas.height, + width: this.presentationSize[0], + height: this.presentationSize[1], depth: 1, - // @ts-expect-error - format: "depth24plus-stencil8", + format: "depth24plus", usage: GPUTextureUsage.RENDER_ATTACHMENT }); - const renderPassDescriptor: GPURenderPassDescriptor = { + const depthStencilAttachment = depthTexture.handle.createView(); + depthStencilAttachment.label = 'depth-stencil-attachment'; + + this._renderPassDescriptor = { colorAttachments: [{ - // @ts-expect-error - attachment: undefined, // Assigned later + view: undefined, // Assigned later loadValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 }, + storeOp: 'store' }], - depthStencil: { - attachment: depthTexture.handle.createView(), + depthStencilAttachment: { + view: depthStencilAttachment, depthLoadValue: 1.0, depthStoreOp: "store", stencilLoadValue: 0, @@ -191,7 +191,18 @@ export default class WebGPUDevice extends Device { } }; - this._renderPassDescriptor = renderPassDescriptor; + log.groupCollapsed(1, 'Device.GPURenderPassDescriptor')(); + log.log(1, JSON.stringify(this._renderPassDescriptor, null, 2))(); + log.groupEnd(1)(); + } + + _updateRenderPassDescriptor() { + if (!this._renderPassDescriptor) { + this._initializeRenderPassDescriptor(); + } + const textureView = this.context.getCurrentTexture().createView(); + this._renderPassDescriptor.colorAttachments[0].view = textureView; + return this._renderPassDescriptor; } } diff --git a/modules/webgpu/src/engine/webgpu-model.ts b/modules/webgpu/src/engine/webgpu-model.ts index d0868e442..aefc5902b 100644 --- a/modules/webgpu/src/engine/webgpu-model.ts +++ b/modules/webgpu/src/engine/webgpu-model.ts @@ -1,81 +1,112 @@ - -import {Shader, RenderPipeline, PrimitiveTopology, assert} from '@luma.gl/api'; +import { + Buffer, + Shader, + RenderPipeline, + RenderPipelineParameters, + PrimitiveTopology, + assert, + AttributeBinding, + Binding +} from '@luma.gl/api'; import {cast} from '@luma.gl/api'; -import {VertexShader} from '@luma.gl/webgl/'; import {WebGPUShader} from '..'; import WebGPUDevice from '../adapter/webgpu-device'; -import WebGPURenderPipeline from '../adapter/webgpu-render-pipeline'; -// import glslangModule from '../glslang'; +import WebGPUBuffer from '../adapter/resources/webgpu-buffer'; +import WebGPURenderPipeline from '../adapter/resources/webgpu-render-pipeline'; +import {makeBindGroup} from '../adapter/helpers/make-bind-group'; export type ModelProps = { id?: string; pipeline?: RenderPipeline; - vertex?: string | Shader; - fragment?: string | Shader; - topology: PrimitiveTopology; - vertexCount: number; - instanceCount?: number; - - // backwards compatibility vs?: string | Shader; fs?: string | Shader; -} + topology: PrimitiveTopology; + attributeLayouts?: AttributeBinding[]; + vertexCount: number; + instanceCount?: number; + parameters?: RenderPipelineParameters; + + attributeBuffers?: Buffer[]; // | Record + bindings?: Binding[]; +}; const DEFAULT_MODEL_PROPS: Required = { id: 'unnamed', - vertex: undefined, - fragment: undefined, - pipeline: undefined, - topology: 'triangle-list', - vertexCount: 0, - instanceCount: 1, vs: undefined, fs: undefined, + pipeline: undefined, + topology: 'triangle-list', + attributeLayouts: [], + vertexCount: 0, + instanceCount: 1, + parameters: {}, + + attributeBuffers: [], + bindings: [] }; export default class Model { device: WebGPUDevice; pipeline: WebGPURenderPipeline; - vertex: WebGPUShader; - fragment: WebGPUShader | undefined; + vs: WebGPUShader; + fs: WebGPUShader | undefined; props: Required; + _bindGroup: GPUBindGroup; + constructor(device: WebGPUDevice, props: ModelProps) { this.props = {...DEFAULT_MODEL_PROPS, ...props}; props = this.props; - this.device = device; + + // Create the pipeline if (props.pipeline) { this.pipeline = cast(props.pipeline); } else { - const vertex = props.vertex || props.vs; - const fragment = props.fragment || props.fs; + const vertex = props.vs; + const fragment = props.fs; assert(vertex); - this.vertex = (typeof vertex === 'string') - ? new WebGPUShader(device, {stage: 'vertex', source: vertex}) - : cast(vertex); - - if (fragment) { - this.fragment = (typeof fragment === 'string') - ? new WebGPUShader(device, {stage: 'fragment', source: fragment}) - : cast(fragment); - } + this.vs = + typeof vertex === 'string' + ? new WebGPUShader(device, {stage: 'vertex', source: vertex}) + : cast(vertex); + if (fragment) { + this.fs = + typeof fragment === 'string' + ? new WebGPUShader(device, {stage: 'fragment', source: fragment}) + : cast(fragment); + } + this.pipeline = device.createRenderPipeline({ - vertexShader: this.vertex, - fragmentShader: this.fragment, - topology: props.topology + vertexShader: this.vs, + fragmentShader: this.fs, + topology: props.topology, + parameters: props.parameters, // Geometry in the vertex shader! + attributeLayouts: props.attributeLayouts }); } + + // Set up the bindings + this._bindGroup = makeBindGroup(this.device.handle, this.pipeline._getBindGroupLayout(), this.props.bindings); } - + draw(renderPass?: GPURenderPassEncoder) { renderPass = renderPass || this.device.getActiveRenderPass(); renderPass.setPipeline(this.pipeline.handle); - // renderPass.setBindGroup(0, this.uniformBindGroup); - // renderPass.setVertexBuffer(0, this.verticesBuffer); + + // Set up attributes + for (let i = 0; i < this.props.attributeBuffers.length; ++i) { + const buffer = cast(this.props.attributeBuffers[i]); + const location = this.props.attributeLayouts[i].location; + renderPass.setVertexBuffer(location, buffer.handle); + } + + // Set up bindings (uniform buffers, textures etc) + renderPass.setBindGroup(0, this._bindGroup); + renderPass.draw(this.props.vertexCount, this.props.instanceCount, 0, 0); // firstVertex, firstInstance); } @@ -225,11 +256,8 @@ export default class Model { }); } */ - } - - /* private: void _initializeVertexState(utils::ComboVertexStateDescriptor* descriptor, @@ -444,7 +472,7 @@ export default class Model { this.userData = {}; this.needsRedraw = true; - // Attributes and buffers + // Attributes and attributeBuffers // Model manages auto Buffer creation from typed arrays this._attributes = {}; // All attributes this.attributes = {}; // User defined attributes @@ -467,7 +495,7 @@ export default class Model { this.drawMode = props.drawMode !== undefined ? props.drawMode : GL.TRIANGLES; this.vertexCount = props.vertexCount || 0; - // Track buffers created by setGeometry + // Track attributeBuffers created by setGeometry this.geometryBuffers = {}; // geometry might have set drawMode and vertexCount diff --git a/modules/webgpu/src/index.ts b/modules/webgpu/src/index.ts index fe73a49de..6be31d0cc 100644 --- a/modules/webgpu/src/index.ts +++ b/modules/webgpu/src/index.ts @@ -1,12 +1,17 @@ + +// Initialize any global state +import '@luma.gl/api'; +import './init' + // WEBGPU ADAPTER export {default as WebGPUDevice} from './adapter/webgpu-device'; -// WEBGPU CLASSES -export {default as WebGPUBuffer} from './adapter/webgpu-buffer'; -export {default as WebGPUTexture} from './adapter/webgpu-texture'; -export {default as WebGPUSampler} from './adapter/webgpu-sampler'; -export {default as WebGPUShader} from './adapter/webgpu-shader'; +// WEBGPU CLASSES (typically not accessed directly) +export {default as WebGPUBuffer} from './adapter/resources/webgpu-buffer'; +export {default as WebGPUTexture} from './adapter/resources/webgpu-texture'; +export {default as WebGPUSampler} from './adapter/resources/webgpu-sampler'; +export {default as WebGPUShader} from './adapter/resources/webgpu-shader'; -// WEBGPU ENGINE CLASSES +// WEBGPU ENGINE CLASSES (until we can make the engine module truly platform independent) export type {ModelProps} from './engine/webgpu-model'; export {default as Model} from './engine/webgpu-model'; diff --git a/modules/webgpu/src/init.ts b/modules/webgpu/src/init.ts new file mode 100644 index 000000000..aa4d10c78 --- /dev/null +++ b/modules/webgpu/src/init.ts @@ -0,0 +1,4 @@ +import {luma} from '@luma.gl/api'; +import WebGPUDevice from './adapter/webgpu-device'; + +luma.registerDevices([WebGPUDevice]); diff --git a/scripts/bundle.config.js b/scripts/bundle.config.js index d5fd0bdd6..f791c3e6d 100644 --- a/scripts/bundle.config.js +++ b/scripts/bundle.config.js @@ -102,7 +102,7 @@ const config = { externals: getExternals(PACKAGE_INFO), plugins: [ - // This is used to define the __VERSION__ constant in core/lib/init.js + // This is used to define the __VERSION__ constant in api/src/init.js // babel-plugin-version-inline uses the package version from the working directory // Therefore we need to manually import the correct version from the core // This is called in prepublishOnly, after lerna bumps the package versions