diff --git a/packages/common/src/utils/promiseQueue.ts b/packages/common/src/utils/promiseQueue.ts new file mode 100644 index 0000000000..5bd994221c --- /dev/null +++ b/packages/common/src/utils/promiseQueue.ts @@ -0,0 +1,72 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +type PromiseQueueItem = { + promise: () => Promise + resolve: (value: T) => void + reject: (reason?: any) => void +} + +export class PromiseQueue { + queue = [] as PromiseQueueItem[] + resolving = 0 + maxConcurrent = 1 + + constructor(maxConcurrent?: number) { + if (maxConcurrent) this.maxConcurrent = maxConcurrent + } + + dequeuePromise() { + if (this.resolving === this.maxConcurrent) return + + const next = this.queue.shift() + if (!next) return + + this.resolving += 1 + next + .promise() + .then((value) => { + next.resolve(value) + }) + .catch((reason) => { + next.reject(reason) + }) + .finally(() => { + this.resolving -= 1 + this.dequeuePromise() + }) + } + + enqueuePromise(promise: () => Promise): Promise { + return new Promise((resolve, reject) => { + this.queue.push({ + promise, + resolve, + reject + }) + this.dequeuePromise() + }) + } +} diff --git a/packages/engine/src/assets/loaders/gltf/GLTFParser.ts b/packages/engine/src/assets/loaders/gltf/GLTFParser.ts index e0f480ffa0..ba8c553e90 100644 --- a/packages/engine/src/assets/loaders/gltf/GLTFParser.ts +++ b/packages/engine/src/assets/loaders/gltf/GLTFParser.ts @@ -64,7 +64,6 @@ import { SkinnedMesh, SRGBColorSpace, Texture, - TextureLoader, TriangleFanDrawMode, TriangleStripDrawMode, Vector2, @@ -74,6 +73,7 @@ import { import { toTrianglesDrawMode } from '@etherealengine/spatial/src/common/classes/BufferGeometryUtils' import { FileLoader } from '../base/FileLoader' +import { TextureLoader } from '../texture/TextureLoader' import { ALPHA_MODES, INTERPOLATION, diff --git a/packages/engine/src/assets/loaders/gltf/extensions/CachedImageLoadExtension.ts b/packages/engine/src/assets/loaders/gltf/extensions/CachedImageLoadExtension.ts index 683ed2f24a..438e7f8e94 100644 --- a/packages/engine/src/assets/loaders/gltf/extensions/CachedImageLoadExtension.ts +++ b/packages/engine/src/assets/loaders/gltf/extensions/CachedImageLoadExtension.ts @@ -23,8 +23,9 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { LoaderUtils, Texture, TextureLoader } from 'three' +import { LoaderUtils, Texture } from 'three' +import { TextureLoader } from '../../texture/TextureLoader' import { GLTFLoaderPlugin } from '../GLTFLoader' import { ImporterExtension } from './ImporterExtension' diff --git a/packages/engine/src/assets/loaders/texture/TextureLoader.ts b/packages/engine/src/assets/loaders/texture/TextureLoader.ts index b3d164dabf..989d0bdd36 100644 --- a/packages/engine/src/assets/loaders/texture/TextureLoader.ts +++ b/packages/engine/src/assets/loaders/texture/TextureLoader.ts @@ -24,50 +24,64 @@ Ethereal Engine. All Rights Reserved. */ import { isClient } from '@etherealengine/common/src/utils/getEnvironment' +import { PromiseQueue } from '@etherealengine/common/src/utils/promiseQueue' import { iOS } from '@etherealengine/spatial/src/common/functions/isMobile' import { ImageLoader, LoadingManager, Texture } from 'three' import { Loader } from '../base/Loader' const iOSMaxResolution = 1024 +const decodeQueue = new PromiseQueue<[string | undefined, HTMLCanvasElement | undefined]>(4) + /** @todo make this accessible for performance scaling */ -const getScaledTextureURI = async (src: string, maxResolution: number): Promise<[string, HTMLCanvasElement]> => { - return new Promise(async (resolve) => { - const img = new Image() - img.crossOrigin = 'anonymous' //browser will yell without this - img.src = src - await img.decode() //new way to wait for image to load - // Initialize the canvas and it's size - const canvas = document.createElement('canvas') //dead dom elements? Remove after Three loads them - const ctx = canvas.getContext('2d') - - // Set width and height - const originalWidth = img.width - const originalHeight = img.height - - let resizingFactor = 1 - if (originalWidth >= originalHeight) { - if (originalWidth > maxResolution) { - resizingFactor = maxResolution / originalWidth - } - } else { - if (originalHeight > maxResolution) { - resizingFactor = maxResolution / originalHeight +const getScaledTextureURI = async ( + src: string, + maxResolution: number +): Promise<[string | undefined, HTMLCanvasElement | undefined]> => { + return decodeQueue.enqueuePromise(() => { + return new Promise(async (resolve) => { + // Initialize the canvas + const canvas = document.createElement('canvas') //dead dom elements? Remove after Three loads them + const ctx = canvas.getContext('2d') + try { + const img = new Image() + img.crossOrigin = 'anonymous' //browser will yell without this + img.src = src + await img.decode() //new way to wait for image to load + + // Set width and height + const originalWidth = img.width + const originalHeight = img.height + + let resizingFactor = 1 + if (originalWidth >= originalHeight) { + if (originalWidth > maxResolution) { + resizingFactor = maxResolution / originalWidth + } + } else { + if (originalHeight > maxResolution) { + resizingFactor = maxResolution / originalHeight + } + } + + const canvasWidth = originalWidth * resizingFactor + const canvasHeight = originalHeight * resizingFactor + + canvas.width = canvasWidth + canvas.height = canvasHeight + + // Draw image and export to a data-uri + ctx?.drawImage(img, 0, 0, canvasWidth, canvasHeight) + const dataURI = canvas.toDataURL() + + // Do something with the result, like overwrite original + resolve([dataURI, canvas]) + } catch (e) { + console.error(e, src) + canvas.remove() + resolve([undefined, undefined]) } - } - - const canvasWidth = originalWidth * resizingFactor - const canvasHeight = originalHeight * resizingFactor - - canvas.width = canvasWidth - canvas.height = canvasHeight - - // Draw image and export to a data-uri - ctx?.drawImage(img, 0, 0, canvasWidth, canvasHeight) - const dataURI = canvas.toDataURL() - - // Do something with the result, like overwrite original - resolve([dataURI, canvas]) + }) }) } @@ -89,7 +103,9 @@ class TextureLoader extends Loader { ) { let canvas: HTMLCanvasElement | undefined = undefined if (this.maxResolution) { - ;[url, canvas] = await getScaledTextureURI(url, this.maxResolution) + const [dataURI, c] = await getScaledTextureURI(url, this.maxResolution) + canvas = c + if (dataURI) url = dataURI } const texture = new Texture() @@ -108,7 +124,9 @@ class TextureLoader extends Loader { onLoad(texture) }, onProgress, - onError + (err) => { + onError?.(err) + } ) } } diff --git a/packages/spatial/src/renderer/PerformanceState.test.tsx b/packages/spatial/src/renderer/PerformanceState.test.tsx index a03cbd52cc..263ac7ec29 100644 --- a/packages/spatial/src/renderer/PerformanceState.test.tsx +++ b/packages/spatial/src/renderer/PerformanceState.test.tsx @@ -92,11 +92,11 @@ describe('PerformanceState', () => { renderer: 'nvidia corporation, nvidia geforce rtx 3070/pcie/sse2, ' }) const performanceState = getState(PerformanceState) - assert(performanceState.max3DTextureSize === 1000) - assert(performanceState.maxBufferSize === 54000000000) - assert(performanceState.maxIndices === 8000) - assert(performanceState.maxTextureSize === 2000) - assert(performanceState.maxVerticies === 10000) + assert(performanceState.max3DTextureSize > 0) + assert(performanceState.maxBufferSize > 0) + assert(performanceState.maxIndices > 0) + assert(performanceState.maxTextureSize > 0) + assert(performanceState.maxVerticies > 0) }) it('Increments performance offset', (done) => { diff --git a/packages/spatial/src/renderer/PerformanceState.ts b/packages/spatial/src/renderer/PerformanceState.ts index 4ce6ebacc6..8f54fbee27 100644 --- a/packages/spatial/src/renderer/PerformanceState.ts +++ b/packages/spatial/src/renderer/PerformanceState.ts @@ -458,7 +458,8 @@ const buildPerformanceState = async ( window.screen.availHeight * window.devicePixelRatio * window.devicePixelRatio * - gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), + gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) * + 2, maxIndices: gl.getParameter(gl.MAX_ELEMENTS_INDICES) * 2, maxVerticies: gl.getParameter(gl.MAX_ELEMENTS_VERTICES) * 2 })