From 4776c290a24f9cdce3fc1c8be19349f7450df20d Mon Sep 17 00:00:00 2001 From: Zyie <24736175+Zyie@users.noreply.github.com> Date: Thu, 14 Jul 2022 15:11:38 +0100 Subject: [PATCH] Add detection parsers to Assets (#8482) --- packages/assets/src/Assets.ts | 58 ++++++++++----- packages/assets/src/detections/index.ts | 12 +++ .../src/detections/parsers/detectAvif.ts | 20 +++++ .../src/detections/parsers/detectBasis.ts | 11 +++ .../parsers/detectCompressedTextures.ts | 73 +++++++++++++++++++ .../src/detections/parsers/detectWebp.ts | 19 +++++ .../assets/src/detections/parsers/index.ts | 4 + .../src/detections/utils/detectUtils.ts | 26 +++++++ packages/assets/src/detections/utils/index.ts | 1 + packages/assets/src/index.ts | 1 + packages/assets/src/resolver/parsers/index.ts | 1 + .../parsers/resolveCompressedTextureUrl.ts | 49 +++++++++++++ .../assets/src/utils/detections/detectAvif.ts | 12 --- .../assets/src/utils/detections/detectWebp.ts | 11 --- packages/assets/src/utils/detections/index.ts | 2 - packages/assets/src/utils/index.ts | 1 - packages/assets/test/assets.tests.ts | 2 +- packages/assets/test/detections.tests.ts | 31 ++++++++ packages/assets/test/resolver.tests.ts | 38 +++++++++- packages/core/src/extensions.ts | 1 + 20 files changed, 327 insertions(+), 46 deletions(-) create mode 100644 packages/assets/src/detections/index.ts create mode 100644 packages/assets/src/detections/parsers/detectAvif.ts create mode 100644 packages/assets/src/detections/parsers/detectBasis.ts create mode 100644 packages/assets/src/detections/parsers/detectCompressedTextures.ts create mode 100644 packages/assets/src/detections/parsers/detectWebp.ts create mode 100644 packages/assets/src/detections/parsers/index.ts create mode 100644 packages/assets/src/detections/utils/detectUtils.ts create mode 100644 packages/assets/src/detections/utils/index.ts create mode 100644 packages/assets/src/resolver/parsers/resolveCompressedTextureUrl.ts delete mode 100644 packages/assets/src/utils/detections/detectAvif.ts delete mode 100644 packages/assets/src/utils/detections/detectWebp.ts delete mode 100644 packages/assets/src/utils/detections/index.ts create mode 100644 packages/assets/test/detections.tests.ts diff --git a/packages/assets/src/Assets.ts b/packages/assets/src/Assets.ts index 7262f94ac7..bff6c7ad2a 100644 --- a/packages/assets/src/Assets.ts +++ b/packages/assets/src/Assets.ts @@ -2,9 +2,12 @@ import { extensions, ExtensionType } from '@pixi/core'; import { BackgroundLoader } from './BackgroundLoader'; import { Cache } from './cache/Cache'; import { cacheSpritesheet, cacheTextureArray } from './cache/parsers'; +import type { FormatDetectionParser } from './detections'; +import { detectAvif, detectWebp } from './detections'; import type { LoadAsset, - LoaderParser } from './loader'; + LoaderParser +} from './loader'; import { loadJson, loadSpritesheet, @@ -18,8 +21,6 @@ import type { PreferOrder, ResolveAsset, ResolverBundle, ResolverManifest, Resol import { resolveSpriteSheetUrl, resolveTextureUrl } from './resolver'; import { Resolver } from './resolver/Resolver'; import { convertToList } from './utils/convertToList'; -import { detectAvif } from './utils/detections/detectAvif'; -import { detectWebp } from './utils/detections/detectWebp'; import { isSingleItem } from './utils/isSingleItem'; export type ProgressCallback = (progress: number) => void; @@ -230,6 +231,8 @@ export class AssetsClass /** takes care of loading assets in the background */ private readonly _backgroundLoader: BackgroundLoader; + private _detections: FormatDetectionParser[] = []; + private _initialized = false; constructor() @@ -284,32 +287,40 @@ export class AssetsClass const resolutionPref = options.texturePreference?.resolution ?? 1; const resolution = (typeof resolutionPref === 'number') ? [resolutionPref] : resolutionPref; - let format: string[]; + let formats: string[]; if (options.texturePreference?.format) { const formatPref = options.texturePreference?.format; - format = (typeof formatPref === 'string') ? [formatPref] : formatPref; + formats = (typeof formatPref === 'string') ? [formatPref] : formatPref; + + // we should remove any formats that are not supported by the browser + for (const detection of this._detections) + { + if (!await detection.test()) + { + formats = await detection.remove(formats); + } + } } else { - format = ['avif', 'webp', 'png', 'jpg', 'jpeg']; - } - - if (!(await detectWebp())) - { - format = format.filter((format) => format !== 'webp'); - } + formats = ['png', 'jpg', 'jpeg']; - if (!(await detectAvif())) - { - format = format.filter((format) => format !== 'avif'); + // we should add any formats that are supported by the browser + for (const detection of this._detections) + { + if (await detection.test()) + { + formats = await detection.add(formats); + } + } } this.resolver.prefer({ params: { - format, + format: formats, resolution, }, }); @@ -769,6 +780,12 @@ export class AssetsClass await this.loader.unload(resolveArray); } + + /** All the detection parsers currently added to the Assets class. */ + public get detections(): FormatDetectionParser[] + { + return this._detections; + } } export const Assets = new AssetsClass(); @@ -777,7 +794,8 @@ export const Assets = new AssetsClass(); extensions .handleByList(ExtensionType.LoadParser, Assets.loader.parsers) .handleByList(ExtensionType.ResolveParser, Assets.resolver.parsers) - .handleByList(ExtensionType.CacheParser, Assets.cache.parsers); + .handleByList(ExtensionType.CacheParser, Assets.cache.parsers) + .handleByList(ExtensionType.DetectionParser, Assets.detections); extensions.add( loadTextures, @@ -793,5 +811,9 @@ extensions.add( // resolve extensions resolveTextureUrl, - resolveSpriteSheetUrl + resolveSpriteSheetUrl, + + // detection extensions + detectWebp, + detectAvif ); diff --git a/packages/assets/src/detections/index.ts b/packages/assets/src/detections/index.ts new file mode 100644 index 0000000000..40fb7a9f1f --- /dev/null +++ b/packages/assets/src/detections/index.ts @@ -0,0 +1,12 @@ +import type { ExtensionMetadata } from '@pixi/core'; + +export interface FormatDetectionParser +{ + extension?: ExtensionMetadata; + test: () => Promise, + add: (formats: string[]) => Promise, + remove: (formats: string[]) => Promise, +} + +export * from './parsers'; +export * from './utils'; diff --git a/packages/assets/src/detections/parsers/detectAvif.ts b/packages/assets/src/detections/parsers/detectAvif.ts new file mode 100644 index 0000000000..80de34c23d --- /dev/null +++ b/packages/assets/src/detections/parsers/detectAvif.ts @@ -0,0 +1,20 @@ +import { ExtensionType } from '@pixi/core'; +import { settings } from '@pixi/settings'; +import type { FormatDetectionParser } from '..'; +import { addFormats, removeFormats } from '../utils/detectUtils'; + +export const detectAvif: FormatDetectionParser = { + extension: ExtensionType.DetectionParser, + test: async (): Promise => + { + if (!globalThis.createImageBitmap) return false; + + // eslint-disable-next-line max-len + const avifData = 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A='; + const blob = await settings.ADAPTER.fetch(avifData).then((r) => r.blob()); + + return createImageBitmap(blob).then(() => true, () => false); + }, + add: addFormats('avif'), + remove: removeFormats('avif') +}; diff --git a/packages/assets/src/detections/parsers/detectBasis.ts b/packages/assets/src/detections/parsers/detectBasis.ts new file mode 100644 index 0000000000..547080110d --- /dev/null +++ b/packages/assets/src/detections/parsers/detectBasis.ts @@ -0,0 +1,11 @@ +import { BasisParser } from '@pixi/basis'; +import { ExtensionType } from '@pixi/core'; +import type { FormatDetectionParser } from '..'; +import { addFormats, removeFormats } from '../utils/detectUtils'; + +export const detectBasis = { + extension: ExtensionType.DetectionParser, + test: async (): Promise => !!(BasisParser.basisBinding && BasisParser.TranscoderWorker.wasmSource), + add: addFormats('basis'), + remove: removeFormats('basis') +} as FormatDetectionParser; diff --git a/packages/assets/src/detections/parsers/detectCompressedTextures.ts b/packages/assets/src/detections/parsers/detectCompressedTextures.ts new file mode 100644 index 0000000000..238bf811b2 --- /dev/null +++ b/packages/assets/src/detections/parsers/detectCompressedTextures.ts @@ -0,0 +1,73 @@ +import type { CompressedTextureExtensionRef, CompressedTextureExtensions } from '@pixi/compressed-textures'; +import { ExtensionType } from '@pixi/core'; +import { settings } from '@pixi/settings'; +import type { FormatDetectionParser } from '..'; + +let storedGl: WebGLRenderingContext; +let extensions: Partial; + +function getCompressedTextureExtensions() +{ + extensions = { + s3tc: storedGl.getExtension('WEBGL_compressed_texture_s3tc'), + s3tc_sRGB: storedGl.getExtension('WEBGL_compressed_texture_s3tc_srgb'), /* eslint-disable-line camelcase */ + etc: storedGl.getExtension('WEBGL_compressed_texture_etc'), + etc1: storedGl.getExtension('WEBGL_compressed_texture_etc1'), + pvrtc: storedGl.getExtension('WEBGL_compressed_texture_pvrtc') + || storedGl.getExtension('WEBKIT_WEBGL_compressed_texture_pvrtc'), + atc: storedGl.getExtension('WEBGL_compressed_texture_atc'), + astc: storedGl.getExtension('WEBGL_compressed_texture_astc') + } as Partial; +} + +export const detectCompressedTextures = { + extension: ExtensionType.DetectionParser, + test: async (): Promise => + { + // Auto-detect WebGL compressed-texture extensions + const canvas = settings.ADAPTER.createCanvas(); + const gl = canvas.getContext('webgl'); + + if (!gl) + { + // #if _DEBUG + console.warn('WebGL not available for compressed textures.'); + // #endif + + return false; + } + + storedGl = gl; + + return true; + }, + add: async (formats: string[]): Promise => + { + if (!extensions) getCompressedTextureExtensions(); + + const textureFormats = []; + + // Assign all available compressed-texture formats + for (const extensionName in extensions) + { + const extension = extensions[extensionName as CompressedTextureExtensionRef]; + + if (!extension) + { + continue; + } + + textureFormats.push(extensionName); + } + + formats.unshift(...textureFormats); + + return formats; + }, + remove: async (formats: string[]): Promise => + { + if (!extensions) getCompressedTextureExtensions(); + + return formats.filter((f) => !(f in extensions)); + }, +} as FormatDetectionParser; diff --git a/packages/assets/src/detections/parsers/detectWebp.ts b/packages/assets/src/detections/parsers/detectWebp.ts new file mode 100644 index 0000000000..2e74d3170a --- /dev/null +++ b/packages/assets/src/detections/parsers/detectWebp.ts @@ -0,0 +1,19 @@ +import { ExtensionType } from '@pixi/core'; +import { settings } from '@pixi/settings'; +import type { FormatDetectionParser } from '..'; +import { addFormats, removeFormats } from '../utils/detectUtils'; + +export const detectWebp = { + extension: ExtensionType.DetectionParser, + test: async (): Promise => + { + if (!globalThis.createImageBitmap) return false; + + const webpData = 'data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA='; + const blob = await settings.ADAPTER.fetch(webpData).then((r) => r.blob()); + + return createImageBitmap(blob).then(() => true, () => false); + }, + add: addFormats('webp'), + remove: removeFormats('webp') +} as FormatDetectionParser; diff --git a/packages/assets/src/detections/parsers/index.ts b/packages/assets/src/detections/parsers/index.ts new file mode 100644 index 0000000000..a32016253f --- /dev/null +++ b/packages/assets/src/detections/parsers/index.ts @@ -0,0 +1,4 @@ +export * from './detectAvif'; +export * from './detectCompressedTextures'; +export * from './detectBasis'; +export * from './detectWebp'; diff --git a/packages/assets/src/detections/utils/detectUtils.ts b/packages/assets/src/detections/utils/detectUtils.ts new file mode 100644 index 0000000000..db3708666a --- /dev/null +++ b/packages/assets/src/detections/utils/detectUtils.ts @@ -0,0 +1,26 @@ +export function addFormats(...format: string[]): (formats: string[]) => Promise +{ + return async (formats: string[]) => + { + formats.unshift(...format); + + return formats; + }; +} +export function removeFormats(...format: string[]): (formats: string[]) => Promise +{ + return async (formats: string[]) => + { + for (const f of format) + { + const index = formats.indexOf(f); + + if (index !== -1) + { + formats.splice(index, 1); + } + } + + return formats; + }; +} diff --git a/packages/assets/src/detections/utils/index.ts b/packages/assets/src/detections/utils/index.ts new file mode 100644 index 0000000000..f1d13fdd37 --- /dev/null +++ b/packages/assets/src/detections/utils/index.ts @@ -0,0 +1 @@ +export * from './detectUtils'; diff --git a/packages/assets/src/index.ts b/packages/assets/src/index.ts index d3b2cfaff2..e7a1078ce4 100644 --- a/packages/assets/src/index.ts +++ b/packages/assets/src/index.ts @@ -3,4 +3,5 @@ export * from './utils'; export * from './cache'; export * from './loader'; export * from './resolver'; +export * from './detections'; diff --git a/packages/assets/src/resolver/parsers/index.ts b/packages/assets/src/resolver/parsers/index.ts index ae734c18c5..9efdf368c3 100644 --- a/packages/assets/src/resolver/parsers/index.ts +++ b/packages/assets/src/resolver/parsers/index.ts @@ -1,2 +1,3 @@ export * from './resolveSpriteSheetUrl'; export * from './resolveTextureUrl'; +export * from './resolveCompressedTextureUrl'; diff --git a/packages/assets/src/resolver/parsers/resolveCompressedTextureUrl.ts b/packages/assets/src/resolver/parsers/resolveCompressedTextureUrl.ts new file mode 100644 index 0000000000..62eb2d50f0 --- /dev/null +++ b/packages/assets/src/resolver/parsers/resolveCompressedTextureUrl.ts @@ -0,0 +1,49 @@ +import { ExtensionType } from '@pixi/core'; +import { settings } from '@pixi/settings'; + +import type { ResolveAsset, ResolveURLParser } from '../types'; + +export const resolveCompressedTextureUrl = { + extension: ExtensionType.ResolveParser, + test: (value: string) => + { + const temp = value.split('?')[0]; + const extension = temp.split('.').pop(); + + return ['basis', 'ktx', 'dds'].includes(extension); + }, + parse: (value: string): ResolveAsset => + { + const temp = value.split('?')[0]; + const extension = temp.split('.').pop(); + + if (extension === 'ktx') + { + const extensions = [ + '.s3tc.ktx', + '.s3tc_sRGB.ktx', + '.etc.ktx', + '.etc1.ktx', + '.pvrt.ktx', + '.atc.ktx', + '.astc.ktx' + ]; + + // check if value ends with one of the extensions + if (extensions.some((ext) => value.endsWith(ext))) + { + return { + resolution: parseFloat(settings.RETINA_PREFIX.exec(value)?.[1] ?? '1'), + format: extensions.find((ext) => value.endsWith(ext)), + src: value, + }; + } + } + + return { + resolution: parseFloat(settings.RETINA_PREFIX.exec(value)?.[1] ?? '1'), + format: value.split('.').pop(), + src: value, + }; + }, +} as ResolveURLParser; diff --git a/packages/assets/src/utils/detections/detectAvif.ts b/packages/assets/src/utils/detections/detectAvif.ts deleted file mode 100644 index 6218d41683..0000000000 --- a/packages/assets/src/utils/detections/detectAvif.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { settings } from '@pixi/settings'; - -export async function detectAvif(): Promise -{ - if (!globalThis.createImageBitmap) return false; - - // eslint-disable-next-line max-len - const avifData = 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A='; - const blob = await settings.ADAPTER.fetch(avifData).then((r) => r.blob()); - - return createImageBitmap(blob).then(() => true, () => false); -} diff --git a/packages/assets/src/utils/detections/detectWebp.ts b/packages/assets/src/utils/detections/detectWebp.ts deleted file mode 100644 index e059bd8c78..0000000000 --- a/packages/assets/src/utils/detections/detectWebp.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { settings } from '@pixi/settings'; - -export async function detectWebp(): Promise -{ - if (!globalThis.createImageBitmap) return false; - - const webpData = 'data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA='; - const blob = await settings.ADAPTER.fetch(webpData).then((r) => r.blob()); - - return createImageBitmap(blob).then(() => true, () => false); -} diff --git a/packages/assets/src/utils/detections/index.ts b/packages/assets/src/utils/detections/index.ts deleted file mode 100644 index 9090a8684a..0000000000 --- a/packages/assets/src/utils/detections/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './detectAvif'; -export * from './detectWebp'; diff --git a/packages/assets/src/utils/index.ts b/packages/assets/src/utils/index.ts index 99cb83b954..3fd6c37810 100644 --- a/packages/assets/src/utils/index.ts +++ b/packages/assets/src/utils/index.ts @@ -1,5 +1,4 @@ export * from './path'; -export * from './detections'; export * from './url'; export * from './convertToList'; export * from './createStringVariations'; diff --git a/packages/assets/test/assets.tests.ts b/packages/assets/test/assets.tests.ts index bfed561094..1325096dfa 100644 --- a/packages/assets/test/assets.tests.ts +++ b/packages/assets/test/assets.tests.ts @@ -148,7 +148,7 @@ describe('Assets', () => it('should map all names', async () => { - Assets.init({ + await Assets.init({ basePath, }); diff --git a/packages/assets/test/detections.tests.ts b/packages/assets/test/detections.tests.ts new file mode 100644 index 0000000000..e5d548b4a4 --- /dev/null +++ b/packages/assets/test/detections.tests.ts @@ -0,0 +1,31 @@ +import { Assets, detectCompressedTextures } from '@pixi/assets'; +import { extensions } from '@pixi/core'; + +describe('Detections', () => +{ + beforeEach(() => Assets.reset()); + + it('should have default detections', () => + { + expect(Assets['_detections']).toHaveLength(2); + }); + + it('should add compressed texture formats', async () => + { + extensions.add(detectCompressedTextures); + await Assets.init(); + expect((Assets.resolver['_preferredOrder'][0].params.format as string[]).includes('s3tc')).toBe(true); + extensions.remove(detectCompressedTextures); + }); + + it('should remove any unsupported formats', async () => + { + extensions.add(detectCompressedTextures); + detectCompressedTextures.test = jest.fn(async () => false); + await Assets.init(); + expect(Assets.resolver['_preferredOrder'][0].params.format).toEqual( + ['avif', 'webp', 'png', 'jpg', 'jpeg'] + ); + extensions.remove(detectCompressedTextures); + }); +}); diff --git a/packages/assets/test/resolver.tests.ts b/packages/assets/test/resolver.tests.ts index f2477eec0e..a57935ab09 100644 --- a/packages/assets/test/resolver.tests.ts +++ b/packages/assets/test/resolver.tests.ts @@ -1,9 +1,45 @@ -import { resolveSpriteSheetUrl, resolveTextureUrl } from '@pixi/assets'; +import { resolveCompressedTextureUrl, resolveSpriteSheetUrl, resolveTextureUrl } from '@pixi/assets'; import { Resolver } from '../src/resolver/Resolver'; import { manifest } from './sampleManifest'; describe('Resolver', () => { + it('should resolve asset', () => + { + const resolver = new Resolver(); + + resolver['_parsers'].push(resolveCompressedTextureUrl); + + resolver.prefer({ + priority: ['format'], + params: { + format: ['s3tc', 's3tc_sRGB', 'png', 'webp'], + resolution: 1 + } + }); + + resolver.add('test', [ + { + resolution: 1, + format: 'png', + src: 'my-image.png', + }, + { + resolution: 1, + format: 'webp', + src: 'my-image.webp', + }, + { + resolution: 1, + format: 's3tc', + src: 'my-image.s3tc.ktx', + }, + ]); + + const asset = resolver.resolveUrl('test'); + + expect(asset).toEqual('my-image.s3tc.ktx'); + }); it('should resolve asset', () => { const resolver = new Resolver(); diff --git a/packages/core/src/extensions.ts b/packages/core/src/extensions.ts index 8132516a4d..90e88a960c 100644 --- a/packages/core/src/extensions.ts +++ b/packages/core/src/extensions.ts @@ -19,6 +19,7 @@ enum ExtensionType LoadParser = 'load-parser', ResolveParser = 'resolve-parser', CacheParser = 'cache-parser', + DetectionParser = 'detection-parser', } interface ExtensionMetadataDetails