diff --git a/.eslintrc.cjs b/.eslintrc.cjs index f0827e38c7..d188569c44 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -29,7 +29,8 @@ module.exports = getESLintConfig({ 'default-case': ['warn'], 'no-eq-null': ['warn'], eqeqeq: ['warn'], - radix: 0 + radix: 0, + 'spaced-comment': ["error", "always", { "exceptions": ["/ <"] }] // 'accessor-pairs': ['error', {getWithoutSet: false, setWithoutGet: false}] }, diff --git a/examples/api/cubemap/app.ts b/examples/api/cubemap/app.ts index 105dadf5c4..5f40361328 100644 --- a/examples/api/cubemap/app.ts +++ b/examples/api/cubemap/app.ts @@ -1,6 +1,5 @@ import {Device, loadImage, glsl} from '@luma.gl/core'; import {AnimationLoopTemplate, AnimationProps, CubeGeometry, Model, ModelProps} from '@luma.gl/engine'; -import {GL} from '@luma.gl/constants'; import {Matrix4, radians} from '@math.gl/core'; const INFO_HTML = ` @@ -102,18 +101,19 @@ export default class AppAnimationLoopTemplate extends AnimationLoopTemplate { const cubemap = device.createTexture({ dimension: 'cube', mipmaps: true, + // @ts-ignore data: { - [GL.TEXTURE_CUBE_MAP_POSITIVE_X]: loadImage('sky-posx.png'), - [GL.TEXTURE_CUBE_MAP_NEGATIVE_X]: loadImage('sky-negx.png'), - [GL.TEXTURE_CUBE_MAP_POSITIVE_Y]: loadImage('sky-posy.png'), - [GL.TEXTURE_CUBE_MAP_NEGATIVE_Y]: loadImage('sky-negy.png'), - [GL.TEXTURE_CUBE_MAP_POSITIVE_Z]: loadImage('sky-posz.png'), - [GL.TEXTURE_CUBE_MAP_NEGATIVE_Z]: loadImage('sky-negz.png') + '+X': loadImage('sky-posx.png'), + '-X': loadImage('sky-negx.png'), + '+Y': loadImage('sky-posy.png'), + '-Y': loadImage('sky-negy.png'), + '+Z': loadImage('sky-posz.png'), + '-Z': loadImage('sky-negz.png') } }); const texture = device.createTexture({ - data: 'vis-logo.png', + data: loadImage('vis-logo.png'), mipmaps: true, sampler: { magFilter: 'linear', diff --git a/examples/tutorials/hello-cube/app.ts b/examples/tutorials/hello-cube/app.ts index bd9c5004e6..2ff7a25b72 100644 --- a/examples/tutorials/hello-cube/app.ts +++ b/examples/tutorials/hello-cube/app.ts @@ -1,5 +1,5 @@ // luma.gl, MIT license -import {glsl, UniformStore, NumberArray, ShaderUniformType} from '@luma.gl/core'; +import {glsl, UniformStore, NumberArray, ShaderUniformType, loadImage} from '@luma.gl/core'; import {AnimationLoopTemplate, AnimationProps, Model, CubeGeometry} from '@luma.gl/engine'; import {Matrix4} from '@math.gl/core'; @@ -70,7 +70,7 @@ export default class AppAnimationLoopTemplate extends AnimationLoopTemplate { super(); const texture = device.createTexture({ - data: 'vis-logo.png', + data: loadImage('vis-logo.png'), mipmaps: true, sampler: device.createSampler({ minFilter: 'linear', diff --git a/examples/tutorials/lighting/app.ts b/examples/tutorials/lighting/app.ts index 8a423d6b3c..c077f9b53d 100644 --- a/examples/tutorials/lighting/app.ts +++ b/examples/tutorials/lighting/app.ts @@ -1,4 +1,4 @@ -import {glsl, NumberArray} from '@luma.gl/core'; +import {glsl, NumberArray, loadImage} from '@luma.gl/core'; import {AnimationLoopTemplate, AnimationProps, Model, CubeGeometry, _ShaderInputs} from '@luma.gl/engine'; import {phongMaterial, lighting, ShaderModule} from '@luma.gl/shadertools'; import {Matrix4} from '@math.gl/core'; @@ -75,7 +75,6 @@ const app: ShaderModule = { } }; - // APPLICATION const eyePosition = [0, 0, 5]; @@ -112,7 +111,7 @@ export default class AppAnimationLoopTemplate extends AnimationLoopTemplate { } }); - const texture = device.createTexture({data: 'vis-logo.png'}); + const texture = device.createTexture({data: loadImage('vis-logo.png')}); this.model = new Model(device, { vs, diff --git a/modules/constants/src/index.ts b/modules/constants/src/index.ts index 431c6bb5ef..fd4d6c6a79 100644 --- a/modules/constants/src/index.ts +++ b/modules/constants/src/index.ts @@ -7,6 +7,8 @@ export {GL} from './webgl-constants'; // WebGL types export type { + GLTextureTarget, + GLTextureCubeMapTarget, GLPrimitiveTopology, GLPrimitive, GLDataType, diff --git a/modules/constants/src/webgl-types.ts b/modules/constants/src/webgl-types.ts index 36858dd583..8035a63e21 100644 --- a/modules/constants/src/webgl-types.ts +++ b/modules/constants/src/webgl-types.ts @@ -11,6 +11,7 @@ export type NumberArray = number[] | TypedArray; export type NumericArray = TypedArray | number[]; /** TypeScript type covering all typed arrays */ + export type TypedArray = | Int8Array | Uint8Array @@ -27,6 +28,22 @@ export type TypedArray = /** We don't know the type of Framebuffer at this stage */ type Framebuffer = unknown; +/** All possible texture targets */ +export type GLTextureTarget = + | GL.TEXTURE_2D + | GL.TEXTURE_CUBE_MAP + | GL.TEXTURE_2D_ARRAY + | GL.TEXTURE_3D; + +/** All possible cube face targets for textImage2D */ +export type GLTextureCubeMapTarget = + | GL.TEXTURE_CUBE_MAP_POSITIVE_X + | GL.TEXTURE_CUBE_MAP_NEGATIVE_X + | GL.TEXTURE_CUBE_MAP_POSITIVE_Y + | GL.TEXTURE_CUBE_MAP_NEGATIVE_Y + | GL.TEXTURE_CUBE_MAP_POSITIVE_Z + | GL.TEXTURE_CUBE_MAP_NEGATIVE_Z; + /** Rendering primitives. Constants passed to drawElements() or drawArrays() to specify what kind of primitive to render. */ export type GLPrimitiveTopology = | GL.POINTS @@ -50,9 +67,9 @@ export type GLDataType = | GL.SHORT | GL.INT; -/** Pixel Type */ +/** Pixel Data Type */ export type GLPixelType = - | GL.UNSIGNED_BYTE + | GLDataType | GL.UNSIGNED_SHORT_5_6_5 | GL.UNSIGNED_SHORT_4_4_4_4 | GL.UNSIGNED_SHORT_5_5_5_1; diff --git a/modules/core-tests/test/adapter/device.spec.ts b/modules/core-tests/test/adapter/device.spec.ts index 9ee9748de9..3954b053ed 100644 --- a/modules/core-tests/test/adapter/device.spec.ts +++ b/modules/core-tests/test/adapter/device.spec.ts @@ -1,6 +1,6 @@ // luma.gl, MIT license import test from 'tape-promise/tape'; -import {getWebGLTestDevices} from '@luma.gl/test-utils'; +import {getWebGLTestDevices, getTestDevices} from '@luma.gl/test-utils'; // import {luma} from '@luma.gl/core'; @@ -13,6 +13,16 @@ test('WebGLDevice#info', (t) => { t.end(); }); +// Minimal test, extensive test in texture-formats.spec +test('WebGLDevice#isTextureFormatCompressed', async (t) => { + for (const device of await getTestDevices()) { + // Just sanity check two types + t.equal(device.isTextureFormatCompressed('rgba8unorm'), false); + t.equal(device.isTextureFormatCompressed('bc3-rgba-unorm'), true) + } + t.end(); +}); + test('WebGLDevice#lost (Promise)', async (t) => { // const device = await luma.createDevice({webgl2: false}); diff --git a/modules/core-tests/test/adapter/resources/texture.spec.ts b/modules/core-tests/test/adapter/resources/texture.spec.ts index 34b421ff3d..bd7a7ea08d 100644 --- a/modules/core-tests/test/adapter/resources/texture.spec.ts +++ b/modules/core-tests/test/adapter/resources/texture.spec.ts @@ -132,18 +132,24 @@ function testFormatDeduction(t, device: Device) { } } -test.skip('WebGL#Texture format deduction', t => { - testFormatDeduction(t, webglDevice); +test.skip('WebGL#Texture format deduction', async (t) => { + for (const device of await getTestDevices()) { + testFormatDeduction(t, device); + } t.end(); }); -test.skip('WebGL#Texture format creation', t => { - testFormatCreation(t, webglDevice); +test.skip('WebGL#Texture format creation', async (t) => { + for (const device of await getTestDevices()) { + testFormatCreation(t, device); + } t.end(); }); -test.skip('WebGL#Texture format creation with data', t => { - testFormatCreation(t, webglDevice, true); +test.skip('WebGL#Texture format creation with data', async (t) => { + for (const device of await getTestDevices()) { + testFormatCreation(t, device, true); + } t.end(); }); diff --git a/modules/core-tests/test/adapter/texture-formats.spec.ts b/modules/core-tests/test/adapter/texture-formats.spec.ts new file mode 100644 index 0000000000..069e7387ca --- /dev/null +++ b/modules/core-tests/test/adapter/texture-formats.spec.ts @@ -0,0 +1,13 @@ +import test from 'tape-promise/tape'; +import {getTestDevices} from '@luma.gl/test-utils'; + +// import {luma} from '@luma.gl/core'; + +// TODO - add full reference table, more exhaustive test +test('WebGLDevice#isTextureFormatCompressed', async (t) => { + for (const device of await getTestDevices()) { + t.equal(device.isTextureFormatCompressed('rgba8unorm'), false); + t.equal(device.isTextureFormatCompressed('bc3-rgba-unorm'), true) + } + t.end(); +}); diff --git a/modules/core-tests/test/index.ts b/modules/core-tests/test/index.ts index b499a22648..0d92ee4010 100644 --- a/modules/core-tests/test/index.ts +++ b/modules/core-tests/test/index.ts @@ -17,6 +17,9 @@ import './adapter/device-helpers/set-device-parameters.spec'; // import './adapter/webgl-canvas-context.spec'; // Resources +import './adapter/texture-formats.spec'; + +// Resources - TODO these tests only depend on Device and could move to API... import './adapter/resources/buffer.spec'; import './adapter/resources/command-buffer.spec'; import './adapter/resources/framebuffer.spec'; diff --git a/modules/core/src/adapter/device.ts b/modules/core/src/adapter/device.ts index fe5711229c..8b2e7757c4 100644 --- a/modules/core/src/adapter/device.ts +++ b/modules/core/src/adapter/device.ts @@ -20,6 +20,8 @@ import type {CommandEncoder, CommandEncoderProps} from './resources/command-enco import type {VertexArray, VertexArrayProps} from './resources/vertex-array'; import type {TransformFeedback, TransformFeedbackProps} from './resources/transform-feedback'; +import {isTextureFormatCompressed} from './type-utils/decode-texture-format'; + /** * Identifies the GPU vendor and driver. * @note Chrome WebGPU does not provide much information, though more can be enabled with @@ -105,7 +107,7 @@ export abstract class DeviceLimits { abstract maxComputeWorkgroupSizeZ: number; /** max ComputeWorkgroupsPerDimension */ abstract maxComputeWorkgroupsPerDimension: number; -}; +} /** Set-like class for features (lets apps check for WebGL / WebGPU extensions) */ export class DeviceFeatures { @@ -130,7 +132,6 @@ export type DeviceFeature = | WebGLDeviceFeature | WebGLCompressedTextureFeatures; - export type WebGPUDeviceFeature = | 'depth-clip-control' | 'indirect-first-instance' @@ -144,8 +145,8 @@ export type WebGPUDeviceFeature = | 'texture-compression-bc' | 'texture-compression-etc2' | 'texture-compression-astc'; - // | 'depth-clamping' // removed from the WebGPU spec... - // | 'pipeline-statistics-query' // removed from the WebGPU spec... +// | 'depth-clamping' // removed from the WebGPU spec... +// | 'pipeline-statistics-query' // removed from the WebGPU spec... export type WebGLDeviceFeature = // webgl extension features @@ -156,7 +157,7 @@ export type WebGLDeviceFeature = // GLSL extension features | 'shader-noperspective-interpolation-webgl' // Vertex outputs & fragment inputs can have a `noperspective` interpolation qualifier. - | 'shader-conservative-depth-webgl' // GLSL `gl_FragDepth` qualifiers `depth_unchanged` etc can enable early depth test + | 'shader-conservative-depth-webgl' // GLSL `gl_FragDepth` qualifiers `depth_unchanged` etc can enable early depth test | 'shader-clip-cull-distance-webgl' // Makes gl_ClipDistance and gl_CullDistance available in shaders // texture rendering @@ -202,8 +203,9 @@ export type DeviceProps = { // preserveDrawingBuffer?: boolean; // Default render target buffers will not be automatically cleared and will preserve their values until cleared or overwritten // failIfMajorPerformanceCaveat?: boolean; // Do not create if the system performance is low. + onError?: (error: Error) => unknown; /** Instrument context (at the expense of performance) */ - debug?: boolean; + debug?: boolean; /** Initialize the SpectorJS WebGL debugger */ spector?: boolean; @@ -239,7 +241,10 @@ export abstract class Device { // preserveDrawingBuffer: undefined, // failIfMajorPerformanceCaveat: undefined - gl: null + gl: null, + + // Callbacks + onError: (error: Error) => log.error(error.message) }; get [Symbol.toStringTag](): string { @@ -286,6 +291,11 @@ export abstract class Device { /** Check if device supports rendering to a specific texture format */ abstract isTextureFormatRenderable(format: TextureFormat): boolean; + /** Check if a specific texture format is GPU compressed */ + isTextureFormatCompressed(format: TextureFormat): boolean { + return isTextureFormatCompressed(format); + } + // Device loss /** `true` if device is already lost */ @@ -437,7 +447,14 @@ export abstract class Device { throw new Error('not implemented'); } - // Implementation + // IMPLEMENTATION + + // Error Handling + + /** Report unhandled device errors */ + onError(error: Error) { + this.props.onError(error); + } protected _getBufferProps(props: BufferProps | ArrayBuffer | ArrayBufferView): BufferProps { if (props instanceof ArrayBuffer || ArrayBuffer.isView(props)) { diff --git a/modules/core/src/adapter/type-utils/decode-texture-format.ts b/modules/core/src/adapter/type-utils/decode-texture-format.ts index 2f905a5443..4416d3850b 100644 --- a/modules/core/src/adapter/type-utils/decode-texture-format.ts +++ b/modules/core/src/adapter/type-utils/decode-texture-format.ts @@ -2,6 +2,12 @@ import {TextureFormat} from '../types/texture-formats'; import {VertexType} from '../types/vertex-formats'; import {decodeVertexType} from './decode-data-type'; + +// prettier-ignore +const COMPRESSED_TEXTURE_FORMAT_PREFIXES = [ + 'bc1', 'bc2', 'bc3', 'bc4', 'bc5', 'bc6', 'bc7', 'etc1', 'etc2', 'eac', 'atc', 'astc', 'pvrtc' +]; + const REGEX = /^(rg?b?a?)([0-9]*)([a-z]*)(-srgb)?(-webgl|-unsized)?$/; export type DecodedTextureFormat = { @@ -17,6 +23,13 @@ export type DecodedTextureFormat = { normalized: boolean; } +/** + * Returns true if a texture format is GPU compressed + */ +export function isTextureFormatCompressed(textureFormat: TextureFormat): boolean { + return COMPRESSED_TEXTURE_FORMAT_PREFIXES.some(prefix => textureFormat.startsWith(prefix)); +} + /** * Decodes a vertex format, returning type, components, byte length and flags (integer, signed, normalized) */ diff --git a/modules/core/src/utils/load-file.ts b/modules/core/src/utils/load-file.ts index 183f41ea30..66eeac8ce1 100644 --- a/modules/core/src/utils/load-file.ts +++ b/modules/core/src/utils/load-file.ts @@ -49,7 +49,7 @@ export async function loadImage( url: string, opts?: {crossOrigin?: string} ): Promise { - return new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { try { const image = new Image(); image.onload = () => resolve(image); diff --git a/modules/webgpu/src/adapter/webgpu-canvas-context.ts b/modules/webgpu/src/adapter/webgpu-canvas-context.ts index 3cb9fa7841..ab550516ea 100644 --- a/modules/webgpu/src/adapter/webgpu-canvas-context.ts +++ b/modules/webgpu/src/adapter/webgpu-canvas-context.ts @@ -15,8 +15,7 @@ export class WebGPUCanvasContext extends CanvasContext { readonly device: WebGPUDevice; readonly gpuCanvasContext: GPUCanvasContext; /** Format of returned textures: "bgra8unorm", "rgba8unorm", "rgba16float". */ - // @ts-ignore - TODO - fix this - readonly format: TextureFormat = navigator.gpu.getPreferredCanvasFormat(); + readonly format: TextureFormat = navigator.gpu.getPreferredCanvasFormat() as TextureFormat; /** Default stencil format for depth textures */ depthStencilFormat: TextureFormat = 'depth24plus';