From bc48d6dd2f9d74aafeca2b87a8c03bfa9146293c Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Wed, 4 Dec 2024 22:41:55 +0100 Subject: [PATCH 01/32] feature: Implement Texture Throttling --- examples/tests/stress-textures.ts | 37 +++ src/core/CoreNode.ts | 34 ++- src/core/CoreTextureManager.ts | 271 ++++++++++-------- src/core/Stage.ts | 11 + src/core/TextureMemoryManager.ts | 7 +- src/core/lib/ImageWorker.ts | 1 + .../renderers/canvas/CanvasCoreRenderer.ts | 4 +- .../renderers/webgl/WebGlCoreCtxTexture.ts | 5 +- src/core/renderers/webgl/WebGlCoreRenderer.ts | 18 +- .../SdfTrFontFace/SdfTrFontFace.ts | 9 +- src/core/textures/ColorTexture.ts | 2 +- src/core/textures/ImageTexture.ts | 34 ++- src/core/textures/NoiseTexture.ts | 3 +- src/core/textures/RenderTexture.ts | 2 +- src/core/textures/SubTexture.ts | 6 +- src/core/textures/Texture.ts | 59 ++-- 16 files changed, 333 insertions(+), 170 deletions(-) create mode 100644 examples/tests/stress-textures.ts diff --git a/examples/tests/stress-textures.ts b/examples/tests/stress-textures.ts new file mode 100644 index 00000000..9580a230 --- /dev/null +++ b/examples/tests/stress-textures.ts @@ -0,0 +1,37 @@ +import type { ExampleSettings } from '../common/ExampleSettings.js'; + +export default async function ({ renderer, testRoot }: ExampleSettings) { + const screenWidth = 1920; + const screenHeight = 1080; + const totalImages = 1000; + + // Calculate the grid dimensions for square images + const gridSize = Math.ceil(Math.sqrt(totalImages)); // Approximate grid size + const imageSize = Math.floor( + Math.min(screenWidth / gridSize, screenHeight / gridSize), + ); // Square size + + // Create a root node for the grid + const gridNode = renderer.createNode({ + x: 0, + y: 0, + width: screenWidth, + height: screenHeight, + parent: testRoot, + }); + + // Create and position images in the grid + new Array(totalImages).fill(0).forEach((_, i) => { + const x = (i % gridSize) * imageSize; + const y = Math.floor(i / gridSize) * imageSize; + + renderer.createNode({ + parent: gridNode, + x, + y, + width: imageSize, + height: imageSize, + src: `https://picsum.photos/id/${i}/${imageSize}/${imageSize}`, // Random images + }); + }); +} diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 90ec3843..9274ebd2 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -25,11 +25,12 @@ import { import type { TextureOptions } from './CoreTextureManager.js'; import type { CoreRenderer } from './renderers/CoreRenderer.js'; import type { Stage } from './Stage.js'; -import type { - Texture, - TextureFailedEventHandler, - TextureFreedEventHandler, - TextureLoadedEventHandler, +import { + TextureType, + type Texture, + type TextureFailedEventHandler, + type TextureFreedEventHandler, + type TextureLoadedEventHandler, } from './textures/Texture.js'; import type { Dimensions, @@ -767,6 +768,13 @@ export class CoreNode extends EventEmitter { UpdateType.RenderBounds | UpdateType.RenderState, ); + + // load default texture if no texture is set + if (!this.props.src && !this.props.texture && !this.props.rtt) { + this.texture = this.stage.txManager.loadTexture('ColorTexture', { + color: 0xffffffff, + }); + } } //#region Textures @@ -780,11 +788,6 @@ export class CoreNode extends EventEmitter { // synchronous task after calling loadTexture() queueMicrotask(() => { texture.preventCleanup = this.props.preventCleanup; - // Preload texture if required - if (this.textureOptions.preload) { - texture.ctxTexture.load(); - } - texture.on('loaded', this.onTextureLoaded); texture.on('failed', this.onTextureFailed); texture.on('freed', this.onTextureFreed); @@ -1554,6 +1557,13 @@ export class CoreNode extends EventEmitter { assertTruthy(this.globalTransform); assertTruthy(this.renderCoords); + if ( + this.texture?.ctxTexture === undefined || + this.texture.state !== 'loaded' + ) { + return; + } + // add to list of renderables to be sorted before rendering renderer.addQuad({ width: this.props.width, @@ -2241,12 +2251,12 @@ export class CoreNode extends EventEmitter { settings: Partial, ): IAnimationController { const animation = new CoreAnimation(this, props, settings); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call + const controller = new CoreAnimationController( this.stage.animationManager, animation, ); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return controller; } diff --git a/src/core/CoreTextureManager.ts b/src/core/CoreTextureManager.ts index 180d807a..70d6060a 100644 --- a/src/core/CoreTextureManager.ts +++ b/src/core/CoreTextureManager.ts @@ -149,18 +149,24 @@ export class CoreTextureManager extends EventEmitter { /** * Map of textures by cache key */ - keyCache: Map = new Map(); + // keyCache: Map = new Map(); /** * Map of cache keys by texture */ - inverseKeyCache: WeakMap = new WeakMap(); + // inverseKeyCache: WeakMap = new WeakMap(); /** * Map of texture constructors by their type name */ txConstructors: Partial = {}; + private downloadTextureSourceQueue: Array = []; + private uploadTextureQueue: Array = []; + + private maxItemsPerFrame = 25; // Configurable limit for items to process per frame + private initialized = false; + imageWorkerManager: ImageWorkerManager | null = null; hasCreateImageBitmap = !!self.createImageBitmap; imageBitmapSupported = { @@ -209,21 +215,17 @@ export class CoreTextureManager extends EventEmitter { this.hasWorker && numImageWorkers > 0 ) { - const imageWorkers = new ImageWorkerManager(numImageWorkers, result); - - // wait for the image worker manager to be initialized - imageWorkers.once('initialized', () => { - // enable image worker manager - this.imageWorkerManager = imageWorkers; - }); + this.imageWorkerManager = new ImageWorkerManager( + numImageWorkers, + result, + ); } else { console.warn( '[Lightning] Imageworker is 0 or not supported on this browser. Image loading will be slower.', ); } - // Do an early init event, we don't need to wait for the image worker manager to be initialized. - // Loading textures will be done on the main thread from this point on until the image worker manager is ready. + this.initialized = true; this.emit('initialized'); }) .catch((e) => { @@ -232,6 +234,7 @@ export class CoreTextureManager extends EventEmitter { ); // initialized without image worker manager and createImageBitmap + this.initialized = true; this.emit('initialized'); }); @@ -244,103 +247,37 @@ export class CoreTextureManager extends EventEmitter { private async validateCreateImageBitmap(): Promise { // Test if createImageBitmap is supported using a simple 1x1 PNG image - // prettier-ignore (this is a binary PNG image) + // prettier-ignore const pngBinaryData = new Uint8Array([ - 0x89, - 0x50, - 0x4e, - 0x47, - 0x0d, - 0x0a, - 0x1a, - 0x0a, // PNG signature - 0x00, - 0x00, - 0x00, - 0x0d, // IHDR chunk length - 0x49, - 0x48, - 0x44, - 0x52, // "IHDR" chunk type - 0x00, - 0x00, - 0x00, - 0x01, // Width: 1 - 0x00, - 0x00, - 0x00, - 0x01, // Height: 1 - 0x01, // Bit depth: 1 - 0x03, // Color type: Indexed - 0x00, // Compression method: Deflate - 0x00, // Filter method: None - 0x00, // Interlace method: None - 0x25, - 0xdb, - 0x56, - 0xca, // CRC for IHDR - 0x00, - 0x00, - 0x00, - 0x03, // PLTE chunk length - 0x50, - 0x4c, - 0x54, - 0x45, // "PLTE" chunk type - 0x00, - 0x00, - 0x00, // Palette entry: Black - 0xa7, - 0x7a, - 0x3d, - 0xda, // CRC for PLTE - 0x00, - 0x00, - 0x00, - 0x01, // tRNS chunk length - 0x74, - 0x52, - 0x4e, - 0x53, // "tRNS" chunk type - 0x00, // Transparency for black: Fully transparent - 0x40, - 0xe6, - 0xd8, - 0x66, // CRC for tRNS - 0x00, - 0x00, - 0x00, - 0x0a, // IDAT chunk length - 0x49, - 0x44, - 0x41, - 0x54, // "IDAT" chunk type - 0x08, - 0xd7, // Deflate header - 0x63, - 0x60, - 0x00, - 0x00, - 0x00, - 0x02, - 0x00, - 0x01, // Zlib-compressed data - 0xe2, - 0x21, - 0xbc, - 0x33, // CRC for IDAT - 0x00, - 0x00, - 0x00, - 0x00, // IEND chunk length - 0x49, - 0x45, - 0x4e, - 0x44, // "IEND" chunk type - 0xae, - 0x42, - 0x60, - 0x82, // CRC for IEND + 0x89, 0x50, 0x4e, 0x47, + 0x0d, 0x0a, 0x1a, 0x0a, // PNG signature + 0x00, 0x00, 0x00, 0x0d, // IHDR chunk length + 0x49, 0x48, 0x44, 0x52, // "IHDR" chunk type + 0x00, 0x00, 0x00, 0x01, // Width: 1 + 0x00, 0x00, 0x00, 0x01, // Height: 1 + 0x01, // Bit depth: 1 + 0x03, // Color type: Indexed + 0x00, // Compression method: Deflate + 0x00, // Filter method: None + 0x00, // Interlace method: None + 0x25, 0xdb, 0x56, 0xca, // CRC for IHDR + 0x00, 0x00, 0x00, 0x03, // PLTE chunk length + 0x50, 0x4c, 0x54, 0x45, // "PLTE" chunk type + 0x00, 0x00, 0x00, // Palette entry: Black + 0xa7, 0x7a, 0x3d, 0xda, // CRC for PLTE + 0x00, 0x00, 0x00, 0x01, // tRNS chunk length + 0x74, 0x52, 0x4e, 0x53, // "tRNS" chunk type + 0x00, // Transparency for black: Fully transparent + 0x40, 0xe6, 0xd8, 0x66, // CRC for tRNS + 0x00, 0x00, 0x00, 0x0a, // IDAT chunk length + 0x49, 0x44, 0x41, 0x54, // "IDAT" chunk type + 0x08, 0xd7, // Deflate header + 0x63, 0x60, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x01, // Zlib-compressed data + 0xe2, 0x21, 0xbc, 0x33, // CRC for IDAT + 0x00, 0x00, 0x00, 0x00, // IEND chunk length + 0x49, 0x45, 0x4e, 0x44, // "IEND" chunk type + 0xae, 0x42, 0x60, 0x82, // CRC for IEND ]); const support: CreateImageBitmapSupport = { @@ -386,6 +323,7 @@ export class CoreTextureManager extends EventEmitter { this.txConstructors[textureType] = textureClass; } + /* loadTexture( textureType: Type, props: ExtractProps, @@ -412,26 +350,115 @@ export class CoreTextureManager extends EventEmitter { } return texture as InstanceType; } + */ - private initTextureToCache(texture: Texture, cacheKey: string) { - const { keyCache, inverseKeyCache } = this; - keyCache.set(cacheKey, texture); - inverseKeyCache.set(texture, cacheKey); + /** + * Enqueue a texture for downloading its source image. + */ + enqueueDownloadTextureSource(texture: ImageTexture): void { + if (!this.downloadTextureSourceQueue.includes(texture)) { + this.downloadTextureSourceQueue.push(texture); + } } /** - * Remove a texture from the cache - * - * @remarks - * Called by Texture Cleanup when a texture is freed. - * - * @param texture + * Enqueue a texture for uploading to the GPU. */ - removeTextureFromCache(texture: Texture) { - const { inverseKeyCache, keyCache } = this; - const cacheKey = inverseKeyCache.get(texture); - if (cacheKey) { - keyCache.delete(cacheKey); + enqueueUploadTexture(texture: Texture): void { + if (!this.uploadTextureQueue.includes(texture)) { + this.uploadTextureQueue.push(texture); } } + + /** + * Override loadTexture to use the batched approach. + */ + loadTexture( + textureType: Type, + props: ExtractProps, + ): InstanceType { + const TextureClass = this.txConstructors[textureType]; + if (!TextureClass) { + throw new Error(`Texture type "${textureType}" is not registered`); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + const texture = new TextureClass(this, props as any); + + // only ImageTexture needs to be loaded in batch + if (texture instanceof ImageTexture) { + this.enqueueDownloadTextureSource(texture); + } else { + this.enqueueUploadTexture(texture); + } + + return texture as InstanceType; + } + + /** + * Process a limited number of downloads and uploads. + */ + processSome(maxItems = this.maxItemsPerFrame): void { + if (this.initialized === false) { + return; + } + + let itemsProcessed = 0; + + // Process uploads + while (this.uploadTextureQueue.length > 0 && itemsProcessed < maxItems) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const texture = this.uploadTextureQueue.shift()!; + queueMicrotask(() => { + texture.loadCtxTexture(); + }); + itemsProcessed++; + } + + // Process downloads + while ( + this.downloadTextureSourceQueue.length > 0 && + itemsProcessed < maxItems + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const texture = this.downloadTextureSourceQueue.shift()!; + + queueMicrotask(() => { + texture.setState('loading'); + texture + .getTextureData() + .then(() => { + this.enqueueUploadTexture(texture); + }) + .catch((err) => { + texture.setState('failed', err); + console.error(`Failed to download texture: ${texture.type}`, err); + }); + }); + + itemsProcessed++; + } + } + + // private initTextureToCache(texture: Texture, cacheKey: string) { + // const { keyCache, inverseKeyCache } = this; + // keyCache.set(cacheKey, texture); + // inverseKeyCache.set(texture, cacheKey); + // } + + // /** + // * Remove a texture from the cache + // * + // * @remarks + // * Called by Texture Cleanup when a texture is freed. + // * + // * @param texture + // */ + // removeTextureFromCache(texture: Texture) { + // const { inverseKeyCache, keyCache } = this; + // const cacheKey = inverseKeyCache.get(texture); + // if (cacheKey) { + // keyCache.delete(cacheKey); + // } + // } } diff --git a/src/core/Stage.ts b/src/core/Stage.ts index ee72e858..a56274fa 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -148,6 +148,13 @@ export class Stage { this.eventBus = options.eventBus; this.txManager = new CoreTextureManager(numImageWorkers); + + // Wait for the Texture Manager to initialize + // once it does, request a render + this.txManager.on('initialized', () => { + this.requestRender(); + }); + this.txMemManager = new TextureMemoryManager(this, textureMemory); this.shManager = new CoreShaderManager(); this.animationManager = new AnimationManager(); @@ -320,6 +327,10 @@ export class Stage { this.root.update(this.deltaTime, this.root.clippingRect); } + // Process some textures + // TODO this should have a configurable amount + this.txManager.processSome(); + // Reset render operations and clear the canvas renderer.reset(); diff --git a/src/core/TextureMemoryManager.ts b/src/core/TextureMemoryManager.ts index 625c8147..588d00b9 100644 --- a/src/core/TextureMemoryManager.ts +++ b/src/core/TextureMemoryManager.ts @@ -143,7 +143,6 @@ export class TextureMemoryManager { // If the threshold is 0, we disable the memory manager by replacing the // setTextureMemUse method with a no-op function. if (criticalThreshold === 0) { - // eslint-disable-next-line @typescript-eslint/no-empty-function this.setTextureMemUse = () => {}; } } @@ -228,8 +227,9 @@ export class TextureMemoryManager { break; } if (texture.preventCleanup === false) { - texture.ctxTexture.free(); - txManager.removeTextureFromCache(texture); + // texture.ctxTexture.free(); + texture.free(); + // txManager.removeTextureFromCache(texture); } if (this.memUsed <= memTarget) { // Stop once we've freed enough textures to reach under the target threshold @@ -258,7 +258,6 @@ export class TextureMemoryManager { getMemoryInfo(): MemoryInfo { let renderableTexturesLoaded = 0; const renderableMemUsed = [...this.loadedTextures.keys()].reduce( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion (acc, texture) => { renderableTexturesLoaded += texture.renderable ? 1 : 0; return ( diff --git a/src/core/lib/ImageWorker.ts b/src/core/lib/ImageWorker.ts index 0281953f..9cf02ff6 100644 --- a/src/core/lib/ImageWorker.ts +++ b/src/core/lib/ImageWorker.ts @@ -159,6 +159,7 @@ function createImageWorker() { self.postMessage({ id: id, src: src, data: data }); }) .catch(function (error) { + console.error('Error loading image:', error); self.postMessage({ id: id, src: src, error: error.message }); }); }; diff --git a/src/core/renderers/canvas/CanvasCoreRenderer.ts b/src/core/renderers/canvas/CanvasCoreRenderer.ts index d4c53ab6..bf79bdee 100644 --- a/src/core/renderers/canvas/CanvasCoreRenderer.ts +++ b/src/core/renderers/canvas/CanvasCoreRenderer.ts @@ -73,7 +73,6 @@ export class CanvasCoreRenderer extends CoreRenderer { } reset(): void { - // eslint-disable-next-line no-self-assign this.canvas.width = this.canvas.width; // quick reset canvas const ctx = this.context; @@ -122,7 +121,8 @@ export class CanvasCoreRenderer extends CoreRenderer { ctxTexture = texture.ctxTexture as CanvasCoreTexture; if (texture.state === 'freed') { - ctxTexture.load(); + // we're going to batch the texture loading so we don't have to wait for + // ctxTexture.load(); return; } if (texture.state !== 'loaded' || !ctxTexture.hasImage()) { diff --git a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts index 7308894e..01fb74ec 100644 --- a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts +++ b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts @@ -53,9 +53,10 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { super(memManager, textureSource); } - get ctxTexture(): WebGLTexture { + get ctxTexture(): WebGLTexture | null { if (this._state === 'freed') { - this.load(); + // this.load(); + return null; } assertTruthy(this._nativeCtxTexture); return this._nativeCtxTexture; diff --git a/src/core/renderers/webgl/WebGlCoreRenderer.ts b/src/core/renderers/webgl/WebGlCoreRenderer.ts index 4b505a34..1a986587 100644 --- a/src/core/renderers/webgl/WebGlCoreRenderer.ts +++ b/src/core/renderers/webgl/WebGlCoreRenderer.ts @@ -255,7 +255,11 @@ export class WebGlCoreRenderer extends CoreRenderer { } } - assertTruthy(texture.ctxTexture !== undefined, 'Invalid texture type'); + // assertTruthy(texture.ctxTexture !== undefined, 'Invalid texture type'); + if (!texture.ctxTexture) { + console.warn('Invalid texture type', texture); + return; + } let { curBufferIdx: bufferIdx, curRenderOp } = this; const targetDims = { width: -1, height: -1 }; @@ -270,7 +274,6 @@ export class WebGlCoreRenderer extends CoreRenderer { ); if (this.reuseRenderOp(params) === false) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.newRenderOp( targetShader, params.shaderProps as Record, @@ -353,7 +356,11 @@ export class WebGlCoreRenderer extends CoreRenderer { } const ctxTexture = texture.ctxTexture as WebGlCoreCtxTexture; - assertTruthy(ctxTexture.ctxTexture !== undefined); + if (!ctxTexture) { + console.warn('Invalid texture type', texture); + return; + } + const textureIdx = this.addTexture(ctxTexture, bufferIdx); assertTruthy(this.curRenderOp !== null); @@ -723,6 +730,11 @@ export class WebGlCoreRenderer extends CoreRenderer { continue; } + if (!node.texture || !node.texture.ctxTexture) { + console.warn('Texture not loaded for RTT node', node); + continue; + } + // Set the active RTT node to the current node // So we can prevent rendering children of nested RTT nodes this.activeRttNode = node; diff --git a/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.ts b/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.ts index 7cbd5200..c103eb0e 100644 --- a/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.ts +++ b/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.ts @@ -92,10 +92,11 @@ export class SdfTrFontFace< stage.requestRender(); }); - // Pre-load it - stage.txManager.once('initialized', () => { - this.texture.ctxTexture.load(); - }); + // // Pre-load it + // this should be done automatically with throttling + // stage.txManager.once('initialized', () => { + // this.texture.ctxTexture.load(); + // }); // Set this.data to the fetched data from dataUrl fetch(atlasDataUrl) diff --git a/src/core/textures/ColorTexture.ts b/src/core/textures/ColorTexture.ts index 8e1bbfb4..84cb4f0d 100644 --- a/src/core/textures/ColorTexture.ts +++ b/src/core/textures/ColorTexture.ts @@ -62,7 +62,7 @@ export class ColorTexture extends Texture { this.props.color = color; } - override async getTextureData(): Promise { + override async getTextureSource(): Promise { const pixelData = new Uint8Array(4); if (this.color === 0xffffffff) { diff --git a/src/core/textures/ImageTexture.ts b/src/core/textures/ImageTexture.ts index 3196cca1..c00c3a1a 100644 --- a/src/core/textures/ImageTexture.ts +++ b/src/core/textures/ImageTexture.ts @@ -222,7 +222,39 @@ export class ImageTexture extends Texture { return this.loadImageFallback(src, premultiplyAlpha ?? true); } - override async getTextureData(): Promise { + override async getTextureSource(): Promise { + const resp = await this.determineImageType(); + + if (resp.data === null) { + this.setState('failed', Error('ImageTexture: No image data')); + return { + data: null, + }; + } + + let width, height; + // check if resp.data is typeof Uint8ClampedArray else + // use resp.data.width and resp.data.height + if (resp.data instanceof Uint8Array) { + width = this.props.width ?? 0; + height = this.props.height ?? 0; + } else { + width = resp.data?.width ?? (this.props.width || 0); + height = resp.data?.height ?? (this.props.height || 0); + } + + // we're loaded! + this.setState('loaded', { + width, + height, + }); + + return { + data: resp.data, + }; + } + + determineImageType() { const { src, premultiplyAlpha, type } = this.props; if (src === null) { return { diff --git a/src/core/textures/NoiseTexture.ts b/src/core/textures/NoiseTexture.ts index 0a7522ba..5f5cf46b 100644 --- a/src/core/textures/NoiseTexture.ts +++ b/src/core/textures/NoiseTexture.ts @@ -63,7 +63,7 @@ export class NoiseTexture extends Texture { this.props = NoiseTexture.resolveDefaults(props); } - override async getTextureData(): Promise { + override async getTextureSource(): Promise { const { width, height } = this.props; const size = width * height * 4; const pixelData8 = new Uint8ClampedArray(size); @@ -74,6 +74,7 @@ export class NoiseTexture extends Texture { pixelData8[i + 2] = v; pixelData8[i + 3] = 255; } + return { data: new ImageData(pixelData8, width, height), }; diff --git a/src/core/textures/RenderTexture.ts b/src/core/textures/RenderTexture.ts index 874fbc00..d5440eeb 100644 --- a/src/core/textures/RenderTexture.ts +++ b/src/core/textures/RenderTexture.ts @@ -63,7 +63,7 @@ export class RenderTexture extends Texture { this.props.height = value; } - override async getTextureData(): Promise { + override async getTextureSource(): Promise { return { data: null, premultiplyAlpha: null, diff --git a/src/core/textures/SubTexture.ts b/src/core/textures/SubTexture.ts index 20eab7b0..d02afa99 100644 --- a/src/core/textures/SubTexture.ts +++ b/src/core/textures/SubTexture.ts @@ -104,6 +104,10 @@ export class SubTexture extends Texture { private onParentTxLoaded: TextureLoadedEventHandler = () => { // We ignore the parent's passed dimensions, and simply use the SubTexture's // configured dimensions (because that's all that matters here) + + // Load the core texture for this sub-texture + this.loadCtxTexture(); + this.setState('loaded', { width: this.props.width, height: this.props.height, @@ -119,7 +123,7 @@ export class SubTexture extends Texture { this.parentTexture.setRenderableOwner(this, isRenderable); } - override async getTextureData(): Promise { + override async getTextureSource(): Promise { return { data: this.props, }; diff --git a/src/core/textures/Texture.ts b/src/core/textures/Texture.ts index 8a445372..bfd5e5ac 100644 --- a/src/core/textures/Texture.ts +++ b/src/core/textures/Texture.ts @@ -164,6 +164,10 @@ export abstract class Texture extends EventEmitter { public preventCleanup = false; + public ctxTexture: CoreContextTexture | undefined; + + public textureData: TextureData | null = null; + constructor(protected txManager: CoreTextureManager) { super(); } @@ -188,7 +192,6 @@ export abstract class Texture extends EventEmitter { this.renderableOwners.add(owner); const newSize = this.renderableOwners.size; if (newSize > oldSize && newSize === 1) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion (this.renderable as boolean) = true; (this.lastRenderableChangeTime as number) = this.txManager.frameTime; this.onChangeIsRenderable?.(true); @@ -197,7 +200,6 @@ export abstract class Texture extends EventEmitter { this.renderableOwners.delete(owner); const newSize = this.renderableOwners.size; if (newSize < oldSize && newSize === 0) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion (this.renderable as boolean) = false; (this.lastRenderableChangeTime as number) = this.txManager.frameTime; this.onChangeIsRenderable?.(false); @@ -217,22 +219,33 @@ export abstract class Texture extends EventEmitter { onChangeIsRenderable?(isRenderable: boolean): void; /** - * Get the CoreContextTexture for this Texture - * - * @remarks - * Each Texture has a corresponding CoreContextTexture that is used to - * manage the texture's native data depending on the renderer's mode - * (WebGL, Canvas, etc). + * Load the core context texture for this Texture. + * The ctxTexture is created by the renderer and lives on the GPU. * - * The Texture and CoreContextTexture are always linked together in a 1:1 - * relationship. + * @returns */ - get ctxTexture() { - // The first time this is called, create the ctxTexture + loadCtxTexture(): void { + if (this.ctxTexture !== undefined) { + return; + } + const ctxTexture = this.txManager.renderer.createCtxTexture(this); - // And replace this getter with the value for future calls + ctxTexture.load(); + Object.defineProperty(this, 'ctxTexture', { value: ctxTexture }); - return ctxTexture; + } + + /** + * Free the core context texture for this Texture. + * + * @remarks + * The ctxTexture is created by the renderer and lives on the GPU. + */ + free(): void { + this.ctxTexture?.free(); + if (this.textureData !== null) { + this.textureData = null; + } } /** @@ -250,7 +263,6 @@ export abstract class Texture extends EventEmitter { ...args: ParametersSkipTarget ): void { if (this.state !== state) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion (this.state as TextureState) = state; if (state === 'loaded') { const loadedArgs = args as ParametersSkipTarget< @@ -277,7 +289,22 @@ export abstract class Texture extends EventEmitter { * @returns * The texture data for this texture. */ - abstract getTextureData(): Promise; + async getTextureData(): Promise { + if (this.textureData === null) { + this.textureData = await this.getTextureSource(); + } + + return this.textureData; + } + + /** + * Get the texture source for this texture. + * + * @remarks + * This method is called by the CoreContextTexture when the texture is loaded. + * The texture source is then used to populate the CoreContextTexture. + */ + abstract getTextureSource(): Promise; /** * Make a cache key for this texture. From 34a5e6d0b5c6d7a6ec8965d2eef972df3faf2e53 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Thu, 5 Dec 2024 21:59:19 +0100 Subject: [PATCH 02/32] feat: Add priority uploads and add default texture --- examples/index.ts | 20 ++++++++-- examples/tests/stress-images.ts | 37 +++++++++++++++++++ examples/tests/stress-textures.ts | 18 ++++++++- src/core/CoreNode.ts | 4 +- src/core/CoreTextureManager.ts | 35 +++++++++++++++--- src/core/Stage.ts | 23 ++++++++++++ src/core/renderers/webgl/WebGlCoreRenderer.ts | 31 +++------------- .../SdfTrFontFace/SdfTrFontFace.ts | 26 ++++++------- src/core/textures/ColorTexture.ts | 2 + 9 files changed, 144 insertions(+), 52 deletions(-) create mode 100644 examples/tests/stress-images.ts diff --git a/examples/index.ts b/examples/index.ts index cf455302..b8fa1a64 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -425,7 +425,7 @@ async function runAutomation( // Allow some time for all images to load and the RaF to unpause // and render if needed. - await delay(200); + await waitForRendererIdle(renderer); if (snapshot) { console.log(`Calling snapshot(${testName})`); await snapshot(testName, adjustedOptions); @@ -454,6 +454,20 @@ async function runAutomation( } } -function delay(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); +function waitForRendererIdle(renderer: RendererMain) { + return new Promise((resolve) => { + let timeout: NodeJS.Timeout | undefined; + const startTimeout = () => { + timeout = setTimeout(() => { + resolve(); + }, 200); + }; + + renderer.once('idle', () => { + if (timeout) { + clearTimeout(timeout); + } + startTimeout(); + }); + }); } diff --git a/examples/tests/stress-images.ts b/examples/tests/stress-images.ts new file mode 100644 index 00000000..9580a230 --- /dev/null +++ b/examples/tests/stress-images.ts @@ -0,0 +1,37 @@ +import type { ExampleSettings } from '../common/ExampleSettings.js'; + +export default async function ({ renderer, testRoot }: ExampleSettings) { + const screenWidth = 1920; + const screenHeight = 1080; + const totalImages = 1000; + + // Calculate the grid dimensions for square images + const gridSize = Math.ceil(Math.sqrt(totalImages)); // Approximate grid size + const imageSize = Math.floor( + Math.min(screenWidth / gridSize, screenHeight / gridSize), + ); // Square size + + // Create a root node for the grid + const gridNode = renderer.createNode({ + x: 0, + y: 0, + width: screenWidth, + height: screenHeight, + parent: testRoot, + }); + + // Create and position images in the grid + new Array(totalImages).fill(0).forEach((_, i) => { + const x = (i % gridSize) * imageSize; + const y = Math.floor(i / gridSize) * imageSize; + + renderer.createNode({ + parent: gridNode, + x, + y, + width: imageSize, + height: imageSize, + src: `https://picsum.photos/id/${i}/${imageSize}/${imageSize}`, // Random images + }); + }); +} diff --git a/examples/tests/stress-textures.ts b/examples/tests/stress-textures.ts index 9580a230..133935b4 100644 --- a/examples/tests/stress-textures.ts +++ b/examples/tests/stress-textures.ts @@ -1,5 +1,15 @@ import type { ExampleSettings } from '../common/ExampleSettings.js'; +export const Colors = { + Black: 0x000000ff, + Red: 0xff0000ff, + Green: 0x00ff00ff, + Blue: 0x0000ffff, + Magenta: 0xff00ffff, + Gray: 0x7f7f7fff, + White: 0xffffffff, +}; + export default async function ({ renderer, testRoot }: ExampleSettings) { const screenWidth = 1920; const screenHeight = 1080; @@ -25,13 +35,19 @@ export default async function ({ renderer, testRoot }: ExampleSettings) { const x = (i % gridSize) * imageSize; const y = Math.floor(i / gridSize) * imageSize; + // pick a random color from Colors + const clr = + Object.values(Colors)[ + Math.floor(Math.random() * Object.keys(Colors).length) + ]; + renderer.createNode({ parent: gridNode, x, y, width: imageSize, height: imageSize, - src: `https://picsum.photos/id/${i}/${imageSize}/${imageSize}`, // Random images + color: clr, }); }); } diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 9274ebd2..6ca68615 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -771,9 +771,7 @@ export class CoreNode extends EventEmitter { // load default texture if no texture is set if (!this.props.src && !this.props.texture && !this.props.rtt) { - this.texture = this.stage.txManager.loadTexture('ColorTexture', { - color: 0xffffffff, - }); + this.texture = this.stage.defaultTexture; } } diff --git a/src/core/CoreTextureManager.ts b/src/core/CoreTextureManager.ts index 70d6060a..2c8ab513 100644 --- a/src/core/CoreTextureManager.ts +++ b/src/core/CoreTextureManager.ts @@ -163,6 +163,7 @@ export class CoreTextureManager extends EventEmitter { private downloadTextureSourceQueue: Array = []; private uploadTextureQueue: Array = []; + private uploadPriorityQueue: Array = []; private maxItemsPerFrame = 25; // Configurable limit for items to process per frame private initialized = false; @@ -363,10 +364,24 @@ export class CoreTextureManager extends EventEmitter { /** * Enqueue a texture for uploading to the GPU. + * + * @param texture - The texture to upload + * @param priority - Whether to prioritize this texture for upload */ - enqueueUploadTexture(texture: Texture): void { - if (!this.uploadTextureQueue.includes(texture)) { + enqueueUploadTexture(texture: Texture, priority: boolean): void { + if ( + priority === false && + this.uploadTextureQueue.includes(texture) === false + ) { this.uploadTextureQueue.push(texture); + return; + } + + if ( + priority === true && + this.uploadPriorityQueue.includes(texture) === false + ) { + this.uploadPriorityQueue.push(texture); } } @@ -376,6 +391,7 @@ export class CoreTextureManager extends EventEmitter { loadTexture( textureType: Type, props: ExtractProps, + priority?: boolean, ): InstanceType { const TextureClass = this.txConstructors[textureType]; if (!TextureClass) { @@ -389,7 +405,7 @@ export class CoreTextureManager extends EventEmitter { if (texture instanceof ImageTexture) { this.enqueueDownloadTextureSource(texture); } else { - this.enqueueUploadTexture(texture); + this.enqueueUploadTexture(texture, priority || false); } return texture as InstanceType; @@ -405,6 +421,16 @@ export class CoreTextureManager extends EventEmitter { let itemsProcessed = 0; + // Process priority uploads + while (this.uploadPriorityQueue.length > 0 && itemsProcessed < maxItems) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const texture = this.uploadPriorityQueue.shift()!; + queueMicrotask(() => { + texture.loadCtxTexture(); + }); + itemsProcessed++; + } + // Process uploads while (this.uploadTextureQueue.length > 0 && itemsProcessed < maxItems) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -428,11 +454,10 @@ export class CoreTextureManager extends EventEmitter { texture .getTextureData() .then(() => { - this.enqueueUploadTexture(texture); + this.enqueueUploadTexture(texture, false); }) .catch((err) => { texture.setState('failed', err); - console.error(`Failed to download texture: ${texture.type}`, err); }); }); diff --git a/src/core/Stage.ts b/src/core/Stage.ts index a56274fa..dcb2019e 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -53,6 +53,8 @@ import { santizeCustomDataMap } from '../main-api/utils.js'; import type { SdfTextRenderer } from './text-rendering/renderers/SdfTextRenderer/SdfTextRenderer.js'; import type { CanvasTextRenderer } from './text-rendering/renderers/CanvasTextRenderer.js'; import { createBound, createPreloadBounds, type Bound } from './lib/utils.js'; +import type { Texture } from './textures/Texture.js'; +import { ColorTexture } from './textures/ColorTexture.js'; export interface StageOptions { appWidth: number; @@ -103,6 +105,7 @@ export class Stage { public readonly strictBound: Bound; public readonly preloadBound: Bound; public readonly strictBounds: boolean; + public readonly defaultTexture: Texture; /** * Renderer Event Bus for the Stage to emit events onto @@ -155,6 +158,26 @@ export class Stage { this.requestRender(); }); + this.defaultTexture = this.txManager.loadTexture( + 'ColorTexture', + { + color: 0xffffffff, + }, + true, + ); + assertTruthy(this.defaultTexture instanceof ColorTexture); + + // Mark the default texture as ALWAYS renderable + // This prevents it from ever being cleaned up. + // Fixes https://github.com/lightning-js/renderer/issues/262 + this.defaultTexture.setRenderableOwner(this, true); + + // When the default texture is loaded, request a render in case the + // RAF is paused. Fixes: https://github.com/lightning-js/renderer/issues/123 + this.defaultTexture.once('loaded', () => { + this.requestRender(); + }); + this.txMemManager = new TextureMemoryManager(this, textureMemory); this.shManager = new CoreShaderManager(); this.animationManager = new AnimationManager(); diff --git a/src/core/renderers/webgl/WebGlCoreRenderer.ts b/src/core/renderers/webgl/WebGlCoreRenderer.ts index 1a986587..afd95f61 100644 --- a/src/core/renderers/webgl/WebGlCoreRenderer.ts +++ b/src/core/renderers/webgl/WebGlCoreRenderer.ts @@ -95,7 +95,6 @@ export class WebGlCoreRenderer extends CoreRenderer { /** * White pixel texture used by default when no texture is specified. */ - defaultTexture: Texture; quadBufferUsage = 0; /** @@ -114,19 +113,6 @@ export class WebGlCoreRenderer extends CoreRenderer { const { canvas, clearColor, bufferMemory } = options; - this.defaultTexture = new ColorTexture(this.txManager); - - // Mark the default texture as ALWAYS renderable - // This prevents it from ever being cleaned up. - // Fixes https://github.com/lightning-js/renderer/issues/262 - this.defaultTexture.setRenderableOwner(this, true); - - // When the default texture is loaded, request a render in case the - // RAF is paused. Fixes: https://github.com/lightning-js/renderer/issues/123 - this.defaultTexture.once('loaded', () => { - this.stage.requestRender(); - }); - const gl = createWebGLContext( canvas, options.forceWebGL2, @@ -237,7 +223,10 @@ export class WebGlCoreRenderer extends CoreRenderer { */ addQuad(params: QuadOptions) { const { fQuadBuffer, uiQuadBuffer } = this; - let texture = params.texture || this.defaultTexture; + let texture = params.texture; + + assertTruthy(texture !== null, 'Texture is required'); + assertTruthy(texture.ctxTexture !== undefined, 'Invalid texture type'); /** * If the shader props contain any automatic properties, update it with the @@ -255,12 +244,6 @@ export class WebGlCoreRenderer extends CoreRenderer { } } - // assertTruthy(texture.ctxTexture !== undefined, 'Invalid texture type'); - if (!texture.ctxTexture) { - console.warn('Invalid texture type', texture); - return; - } - let { curBufferIdx: bufferIdx, curRenderOp } = this; const targetDims = { width: -1, height: -1 }; targetDims.width = params.width; @@ -356,11 +339,7 @@ export class WebGlCoreRenderer extends CoreRenderer { } const ctxTexture = texture.ctxTexture as WebGlCoreCtxTexture; - if (!ctxTexture) { - console.warn('Invalid texture type', texture); - return; - } - + assertTruthy(ctxTexture instanceof WebGlCoreCtxTexture); const textureIdx = this.addTexture(ctxTexture, bufferIdx); assertTruthy(this.curRenderOp !== null); diff --git a/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.ts b/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.ts index c103eb0e..8d79d701 100644 --- a/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.ts +++ b/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.ts @@ -77,14 +77,18 @@ export class SdfTrFontFace< ); // Load image - this.texture = stage.txManager.loadTexture('ImageTexture', { - src: atlasUrl, - // IMPORTANT: The SDF shader requires the alpha channel to NOT be - // premultiplied on the atlas texture. If it is premultiplied, the - // rendering of SDF glyphs (especially single-channel SDF fonts) will - // be very jagged. - premultiplyAlpha: false, - }); + this.texture = stage.txManager.loadTexture( + 'ImageTexture', + { + src: atlasUrl, + // IMPORTANT: The SDF shader requires the alpha channel to NOT be + // premultiplied on the atlas texture. If it is premultiplied, the + // rendering of SDF glyphs (especially single-channel SDF fonts) will + // be very jagged. + premultiplyAlpha: false, + }, + true, + ); this.texture.on('loaded', () => { this.checkLoaded(); @@ -92,12 +96,6 @@ export class SdfTrFontFace< stage.requestRender(); }); - // // Pre-load it - // this should be done automatically with throttling - // stage.txManager.once('initialized', () => { - // this.texture.ctxTexture.load(); - // }); - // Set this.data to the fetched data from dataUrl fetch(atlasDataUrl) .then(async (response) => { diff --git a/src/core/textures/ColorTexture.ts b/src/core/textures/ColorTexture.ts index 84cb4f0d..81b25f5b 100644 --- a/src/core/textures/ColorTexture.ts +++ b/src/core/textures/ColorTexture.ts @@ -77,6 +77,8 @@ export class ColorTexture extends Texture { pixelData[3] = (this.color >>> 24) & 0xff; // Alpha } + this.setState('loaded', { width: 1, height: 1 }); + return { data: pixelData, premultiplyAlpha: true, From 456b3dbf3716b114d39e2416f1ffd460997053db Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Thu, 5 Dec 2024 22:58:46 +0100 Subject: [PATCH 03/32] fix: Remove microTask for loading ctx Textures as it creates a race condition with shaders. --- src/core/CoreNode.ts | 3 ++- src/core/CoreTextureManager.ts | 8 ++------ src/core/renderers/webgl/WebGlCoreRenderer.ts | 7 +++++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 6ca68615..990ce3de 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -1556,7 +1556,8 @@ export class CoreNode extends EventEmitter { assertTruthy(this.renderCoords); if ( - this.texture?.ctxTexture === undefined || + this.texture === null || + this.texture === undefined || this.texture.state !== 'loaded' ) { return; diff --git a/src/core/CoreTextureManager.ts b/src/core/CoreTextureManager.ts index 2c8ab513..b9b860b7 100644 --- a/src/core/CoreTextureManager.ts +++ b/src/core/CoreTextureManager.ts @@ -425,9 +425,7 @@ export class CoreTextureManager extends EventEmitter { while (this.uploadPriorityQueue.length > 0 && itemsProcessed < maxItems) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const texture = this.uploadPriorityQueue.shift()!; - queueMicrotask(() => { - texture.loadCtxTexture(); - }); + texture.loadCtxTexture(); itemsProcessed++; } @@ -435,9 +433,7 @@ export class CoreTextureManager extends EventEmitter { while (this.uploadTextureQueue.length > 0 && itemsProcessed < maxItems) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const texture = this.uploadTextureQueue.shift()!; - queueMicrotask(() => { - texture.loadCtxTexture(); - }); + texture.loadCtxTexture(); itemsProcessed++; } diff --git a/src/core/renderers/webgl/WebGlCoreRenderer.ts b/src/core/renderers/webgl/WebGlCoreRenderer.ts index afd95f61..55ba5c64 100644 --- a/src/core/renderers/webgl/WebGlCoreRenderer.ts +++ b/src/core/renderers/webgl/WebGlCoreRenderer.ts @@ -226,7 +226,6 @@ export class WebGlCoreRenderer extends CoreRenderer { let texture = params.texture; assertTruthy(texture !== null, 'Texture is required'); - assertTruthy(texture.ctxTexture !== undefined, 'Invalid texture type'); /** * If the shader props contain any automatic properties, update it with the @@ -338,8 +337,12 @@ export class WebGlCoreRenderer extends CoreRenderer { [texCoordY1, texCoordY2] = [texCoordY2, texCoordY1]; } + if (texture.ctxTexture === null || texture.ctxTexture === undefined) { + return; + } + const ctxTexture = texture.ctxTexture as WebGlCoreCtxTexture; - assertTruthy(ctxTexture instanceof WebGlCoreCtxTexture); + // assertTruthy(ctxTexture instanceof WebGlCoreCtxTexture); const textureIdx = this.addTexture(ctxTexture, bufferIdx); assertTruthy(this.curRenderOp !== null); From 802feb43ebe30edc074bf369c02dacc52caa841b Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Mon, 9 Dec 2024 22:16:09 +0100 Subject: [PATCH 04/32] refactor: Enhance texture state management --- examples/tests/alpha.ts | 9 -- src/core/CoreNode.ts | 68 ++++----- src/core/CoreTextureManager.ts | 84 +++++------ src/core/Stage.ts | 6 +- src/core/renderers/CoreContextTexture.ts | 1 + .../renderers/canvas/CanvasCoreTexture.ts | 12 +- .../renderers/webgl/WebGlCoreCtxSubTexture.ts | 4 +- .../renderers/webgl/WebGlCoreCtxTexture.ts | 43 +++--- src/core/renderers/webgl/WebGlCoreRenderer.ts | 8 +- src/core/textures/ColorTexture.ts | 2 +- src/core/textures/ImageTexture.ts | 4 +- src/core/textures/NoiseTexture.ts | 2 + src/core/textures/RenderTexture.ts | 2 + src/core/textures/SubTexture.ts | 8 +- src/core/textures/Texture.ts | 138 +++++++++++++++--- 15 files changed, 226 insertions(+), 165 deletions(-) diff --git a/examples/tests/alpha.ts b/examples/tests/alpha.ts index e177025a..8f5ae06d 100644 --- a/examples/tests/alpha.ts +++ b/examples/tests/alpha.ts @@ -26,12 +26,6 @@ export async function automation(settings: ExampleSettings) { } export default async function test({ renderer, testRoot }: ExampleSettings) { - /* - * redRect will persist and change color every frame - * greenRect will persist and be detached and reattached to the root every second - * blueRect will be created and destroyed every 500 ms - */ - const parent = renderer.createNode({ x: 200, y: 240, @@ -55,8 +49,5 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { alpha: 1, }); - /* - * End: Sprite Map Demo - */ console.log('ready!'); } diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 990ce3de..28c2ffcd 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -830,6 +830,7 @@ export class CoreNode extends EventEmitter { private onTextureLoaded: TextureLoadedEventHandler = (_, dimensions) => { this.autosizeNode(dimensions); + this.setUpdateType(UpdateType.IsRenderable); // Texture was loaded. In case the RAF loop has already stopped, we request // a render to ensure the texture is rendered. @@ -852,6 +853,8 @@ export class CoreNode extends EventEmitter { }; private onTextureFailed: TextureFailedEventHandler = (_, error) => { + this.setUpdateType(UpdateType.IsRenderable); + // If parent has a render texture, flag that we need to update if (this.parentHasRenderTexture) { this.notifyParentRTTOfUpdate(); @@ -864,6 +867,8 @@ export class CoreNode extends EventEmitter { }; private onTextureFreed: TextureFreedEventHandler = () => { + this.setUpdateType(UpdateType.IsRenderable); + // If parent has a render texture, flag that we need to update if (this.parentHasRenderTexture) { this.notifyParentRTTOfUpdate(); @@ -1208,8 +1213,12 @@ export class CoreNode extends EventEmitter { //check if CoreNode is renderable based on props hasRenderableProperties(): boolean { - if (this.props.texture) { - return true; + if (this.texture !== null) { + if (this.texture.state === 'loaded') { + return true; + } + + return false; } if (!this.props.width || !this.props.height) { @@ -1220,7 +1229,7 @@ export class CoreNode extends EventEmitter { return true; } - if (this.props.clipping) { + if (this.props.clipping === true) { return true; } @@ -1230,37 +1239,19 @@ export class CoreNode extends EventEmitter { // Consider removing these checks and just using the color property check above. // Maybe add a forceRender prop for nodes that should always render. - if (this.props.colorTop !== 0) { - return true; - } - - if (this.props.colorBottom !== 0) { - return true; - } - - if (this.props.colorLeft !== 0) { - return true; - } - - if (this.props.colorRight !== 0) { - return true; - } - - if (this.props.colorTl !== 0) { - return true; - } - - if (this.props.colorTr !== 0) { - return true; - } - - if (this.props.colorBl !== 0) { + if ( + this.props.colorTop !== 0 || + this.props.colorBottom !== 0 || + this.props.colorLeft !== 0 || + this.props.colorRight !== 0 || + this.props.colorTl !== 0 || + this.props.colorTr !== 0 || + this.props.colorBl !== 0 || + this.props.colorBr !== 0 + ) { return true; } - if (this.props.colorBr !== 0) { - return true; - } return false; } @@ -1554,14 +1545,7 @@ export class CoreNode extends EventEmitter { assertTruthy(this.globalTransform); assertTruthy(this.renderCoords); - - if ( - this.texture === null || - this.texture === undefined || - this.texture.state !== 'loaded' - ) { - return; - } + assertTruthy(this.texture); // add to list of renderables to be sorted before rendering renderer.addQuad({ @@ -1656,7 +1640,7 @@ export class CoreNode extends EventEmitter { width: this.width, height: this.height, }); - this.textureOptions.preload = true; + this.setUpdateType(UpdateType.RenderTexture); } } @@ -1676,7 +1660,7 @@ export class CoreNode extends EventEmitter { width: this.width, height: this.height, }); - this.textureOptions.preload = true; + this.setUpdateType(UpdateType.RenderTexture); } } @@ -2038,7 +2022,7 @@ export class CoreNode extends EventEmitter { width: this.width, height: this.height, }); - this.textureOptions.preload = true; + this.stage.renderer?.renderToTexture(this); // Only this RTT node } diff --git a/src/core/CoreTextureManager.ts b/src/core/CoreTextureManager.ts index b9b860b7..7fd003e6 100644 --- a/src/core/CoreTextureManager.ts +++ b/src/core/CoreTextureManager.ts @@ -107,20 +107,6 @@ export type ResizeModeOptions = * multiple Nodes each using a different set of options. */ export interface TextureOptions { - /** - * Preload the texture immediately even if it's not being rendered to the - * screen. - * - * @remarks - * This allows the texture to be used immediately without any delay when it - * is first needed for rendering. Otherwise the loading process will start - * when the texture is first rendered, which may cause a delay in that texture - * being shown properly. - * - * @defaultValue `false` - */ - preload?: boolean; - /** * Flip the texture horizontally when rendering * @@ -161,9 +147,8 @@ export class CoreTextureManager extends EventEmitter { */ txConstructors: Partial = {}; - private downloadTextureSourceQueue: Array = []; + private downloadTextureSourceQueue: Array = []; private uploadTextureQueue: Array = []; - private uploadPriorityQueue: Array = []; private maxItemsPerFrame = 25; // Configurable limit for items to process per frame private initialized = false; @@ -356,7 +341,7 @@ export class CoreTextureManager extends EventEmitter { /** * Enqueue a texture for downloading its source image. */ - enqueueDownloadTextureSource(texture: ImageTexture): void { + enqueueDownloadTextureSource(texture: Texture): void { if (!this.downloadTextureSourceQueue.includes(texture)) { this.downloadTextureSourceQueue.push(texture); } @@ -366,27 +351,19 @@ export class CoreTextureManager extends EventEmitter { * Enqueue a texture for uploading to the GPU. * * @param texture - The texture to upload - * @param priority - Whether to prioritize this texture for upload */ - enqueueUploadTexture(texture: Texture, priority: boolean): void { - if ( - priority === false && - this.uploadTextureQueue.includes(texture) === false - ) { + enqueueUploadTexture(texture: Texture): void { + if (this.uploadTextureQueue.includes(texture) === false) { this.uploadTextureQueue.push(texture); - return; - } - - if ( - priority === true && - this.uploadPriorityQueue.includes(texture) === false - ) { - this.uploadPriorityQueue.push(texture); } } /** * Override loadTexture to use the batched approach. + * + * @param textureType - The type of texture to load + * @param props - The properties to use for the texture + * @param immediate - Whether to prioritize the texture for immediate loading */ loadTexture( textureType: Type, @@ -401,13 +378,24 @@ export class CoreTextureManager extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any const texture = new TextureClass(this, props as any); - // only ImageTexture needs to be loaded in batch - if (texture instanceof ImageTexture) { - this.enqueueDownloadTextureSource(texture); - } else { - this.enqueueUploadTexture(texture, priority || false); + // prioritize the texture for immediate loading + if (priority === true) { + texture + .getTextureData() + .then(() => { + const coreContext = texture.loadCtxTexture(); + coreContext.load(); + }) + .catch((err) => { + console.error(err); + }); + + return texture as InstanceType; } + // enqueue the texture for download and upload + this.enqueueDownloadTextureSource(texture); + return texture as InstanceType; } @@ -421,19 +409,12 @@ export class CoreTextureManager extends EventEmitter { let itemsProcessed = 0; - // Process priority uploads - while (this.uploadPriorityQueue.length > 0 && itemsProcessed < maxItems) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const texture = this.uploadPriorityQueue.shift()!; - texture.loadCtxTexture(); - itemsProcessed++; - } - // Process uploads while (this.uploadTextureQueue.length > 0 && itemsProcessed < maxItems) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const texture = this.uploadTextureQueue.shift()!; - texture.loadCtxTexture(); + const coreContext = texture.loadCtxTexture(); + coreContext.load(); itemsProcessed++; } @@ -444,16 +425,14 @@ export class CoreTextureManager extends EventEmitter { ) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const texture = this.downloadTextureSourceQueue.shift()!; - queueMicrotask(() => { - texture.setState('loading'); texture .getTextureData() .then(() => { - this.enqueueUploadTexture(texture, false); + this.enqueueUploadTexture(texture); }) .catch((err) => { - texture.setState('failed', err); + console.error(err); }); }); @@ -461,6 +440,13 @@ export class CoreTextureManager extends EventEmitter { } } + public hasUpdates(): boolean { + return ( + this.downloadTextureSourceQueue.length > 0 || + this.uploadTextureQueue.length > 0 + ); + } + // private initTextureToCache(texture: Texture, cacheKey: string) { // const { keyCache, inverseKeyCache } = this; // keyCache.set(cacheKey, texture); diff --git a/src/core/Stage.ts b/src/core/Stage.ts index dcb2019e..fefd74d2 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -335,7 +335,11 @@ export class Stage { * Check if the scene has updates */ hasSceneUpdates() { - return !!this.root.updateType || this.renderRequested; + return ( + !!this.root.updateType || + this.renderRequested || + this.txManager.hasUpdates() + ); } /** diff --git a/src/core/renderers/CoreContextTexture.ts b/src/core/renderers/CoreContextTexture.ts index 94ecde3a..d300844d 100644 --- a/src/core/renderers/CoreContextTexture.ts +++ b/src/core/renderers/CoreContextTexture.ts @@ -23,6 +23,7 @@ import type { Texture } from '../textures/Texture.js'; export abstract class CoreContextTexture { readonly textureSource: Texture; private memManager: TextureMemoryManager; + public state: 'freed' | 'loading' | 'loaded' | 'failed' = 'freed'; constructor(memManager: TextureMemoryManager, textureSource: Texture) { this.memManager = memManager; diff --git a/src/core/renderers/canvas/CanvasCoreTexture.ts b/src/core/renderers/canvas/CanvasCoreTexture.ts index 2831b678..8a049d76 100644 --- a/src/core/renderers/canvas/CanvasCoreTexture.ts +++ b/src/core/renderers/canvas/CanvasCoreTexture.ts @@ -35,21 +35,21 @@ export class CanvasCoreTexture extends CoreContextTexture { if (this.textureSource.state !== 'freed') { return; } - this.textureSource.setState('loading'); + this.textureSource.setCoreCtxState('loading'); this.onLoadRequest() .then((size) => { - this.textureSource.setState('loaded', size); + this.textureSource.setCoreCtxState('loaded', size); this.updateMemSize(); }) .catch((err) => { - this.textureSource.setState('failed', err as Error); + this.textureSource.setCoreCtxState('failed', err as Error); }); } free(): void { this.image = undefined; this.tintCache = undefined; - this.textureSource.setState('freed'); + this.textureSource.setCoreCtxState('freed'); this.setTextureMemUse(0); } @@ -120,7 +120,9 @@ export class CanvasCoreTexture extends CoreContextTexture { } private async onLoadRequest(): Promise { - const { data } = await this.textureSource.getTextureData(); + const data = this.textureSource.textureData; + assertTruthy(data, 'Texture data is null'); + // TODO: canvas from text renderer should be able to provide the canvas directly // instead of having to re-draw it into a new canvas... if (data instanceof ImageData) { diff --git a/src/core/renderers/webgl/WebGlCoreCtxSubTexture.ts b/src/core/renderers/webgl/WebGlCoreCtxSubTexture.ts index a2dfb5cd..a632e90e 100644 --- a/src/core/renderers/webgl/WebGlCoreCtxSubTexture.ts +++ b/src/core/renderers/webgl/WebGlCoreCtxSubTexture.ts @@ -18,6 +18,7 @@ */ import type { Dimensions } from '../../../common/CommonTypes.js'; +import { assertTruthy } from '../../../utils.js'; import type { TextureMemoryManager } from '../../TextureMemoryManager.js'; import type { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js'; import type { SubTexture } from '../../textures/SubTexture.js'; @@ -33,7 +34,8 @@ export class WebGlCoreCtxSubTexture extends WebGlCoreCtxTexture { } override async onLoadRequest(): Promise { - const props = await (this.textureSource as SubTexture).getTextureData(); + const props = (this.textureSource as SubTexture).textureData; + assertTruthy(props, 'SubTexture must have texture data'); if (props.data instanceof Uint8Array) { // its a 1x1 Color Texture diff --git a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts index 01fb74ec..b1a54854 100644 --- a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts +++ b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts @@ -41,7 +41,6 @@ const TRANSPARENT_TEXTURE_DATA = new Uint8Array([0, 0, 0, 0]); */ export class WebGlCoreCtxTexture extends CoreContextTexture { protected _nativeCtxTexture: WebGLTexture | null = null; - private _state: 'freed' | 'loading' | 'loaded' | 'failed' = 'freed'; private _w = 0; private _h = 0; @@ -54,8 +53,8 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { } get ctxTexture(): WebGLTexture | null { - if (this._state === 'freed') { - // this.load(); + if (this.state === 'freed') { + this.load(); return null; } assertTruthy(this._nativeCtxTexture); @@ -81,41 +80,45 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { */ load() { // If the texture is already loading or loaded, don't load it again. - if (this._state === 'loading' || this._state === 'loaded') { + if (this.state === 'loading' || this.state === 'loaded') { return; } - this._state = 'loading'; - this.textureSource.setState('loading'); + + this.state = 'loading'; + this.textureSource.setCoreCtxState('loading'); this._nativeCtxTexture = this.createNativeCtxTexture(); + if (this._nativeCtxTexture === null) { - this._state = 'failed'; - this.textureSource.setState( + this.state = 'failed'; + this.textureSource.setCoreCtxState( 'failed', new Error('Could not create WebGL Texture'), ); console.error('Could not create WebGL Texture'); return; } + this.onLoadRequest() .then(({ width, height }) => { // If the texture has been freed while loading, return early. - if (this._state === 'freed') { + if (this.state === 'freed') { return; } - this._state = 'loaded'; + + this.state = 'loaded'; this._w = width; this._h = height; // Update the texture source's width and height so that it can be used // for rendering. - this.textureSource.setState('loaded', { width, height }); + this.textureSource.setCoreCtxState('loaded', { width, height }); }) .catch((err) => { // If the texture has been freed while loading, return early. - if (this._state === 'freed') { + if (this.state === 'freed') { return; } - this._state = 'failed'; - this.textureSource.setState('failed', err); + this.state = 'failed'; + this.textureSource.setCoreCtxState('failed', err); console.error(err); }); } @@ -125,21 +128,21 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { */ async onLoadRequest(): Promise { const { glw } = this; + const textureData = this.textureSource.textureData; + assertTruthy(textureData, 'Texture data is null'); // Set to a 1x1 transparent texture glw.texImage2D(0, glw.RGBA, 1, 1, 0, glw.RGBA, glw.UNSIGNED_BYTE, null); this.setTextureMemUse(TRANSPARENT_TEXTURE_DATA.byteLength); - const textureData = await this.textureSource?.getTextureData(); // If the texture has been freed while loading, return early. if (!this._nativeCtxTexture) { - assertTruthy(this._state === 'freed'); + assertTruthy(this.state === 'freed'); return { width: 0, height: 0 }; } let width = 0; let height = 0; - assertTruthy(this._nativeCtxTexture); glw.activeTexture(0); // If textureData is null, the texture is empty (0, 0) and we don't need to // upload any data to the GPU. @@ -247,11 +250,11 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { * @returns */ free() { - if (this._state === 'freed') { + if (this.state === 'freed') { return; } - this._state = 'freed'; - this.textureSource.setState('freed'); + this.state = 'freed'; + this.textureSource.setCoreCtxState('freed'); this._w = 0; this._h = 0; if (!this._nativeCtxTexture) { diff --git a/src/core/renderers/webgl/WebGlCoreRenderer.ts b/src/core/renderers/webgl/WebGlCoreRenderer.ts index 55ba5c64..816010c7 100644 --- a/src/core/renderers/webgl/WebGlCoreRenderer.ts +++ b/src/core/renderers/webgl/WebGlCoreRenderer.ts @@ -36,7 +36,6 @@ import { } from './internal/RendererUtils.js'; import { WebGlCoreCtxTexture } from './WebGlCoreCtxTexture.js'; import { Texture, TextureType } from '../../textures/Texture.js'; -import { ColorTexture } from '../../textures/ColorTexture.js'; import { SubTexture } from '../../textures/SubTexture.js'; import { WebGlCoreCtxSubTexture } from './WebGlCoreCtxSubTexture.js'; import { CoreShaderManager } from '../../CoreShaderManager.js'; @@ -53,7 +52,6 @@ import { RenderTexture } from '../../textures/RenderTexture.js'; import type { CoreNode } from '../../CoreNode.js'; import { WebGlCoreCtxRenderTexture } from './WebGlCoreCtxRenderTexture.js'; import type { BaseShaderController } from '../../../main-api/ShaderController.js'; -import { ImageTexture } from '../../textures/ImageTexture.js'; const WORDS_PER_QUAD = 24; // const BYTES_PER_QUAD = WORDS_PER_QUAD * 4; @@ -337,12 +335,8 @@ export class WebGlCoreRenderer extends CoreRenderer { [texCoordY1, texCoordY2] = [texCoordY2, texCoordY1]; } - if (texture.ctxTexture === null || texture.ctxTexture === undefined) { - return; - } - const ctxTexture = texture.ctxTexture as WebGlCoreCtxTexture; - // assertTruthy(ctxTexture instanceof WebGlCoreCtxTexture); + assertTruthy(ctxTexture instanceof WebGlCoreCtxTexture); const textureIdx = this.addTexture(ctxTexture, bufferIdx); assertTruthy(this.curRenderOp !== null); diff --git a/src/core/textures/ColorTexture.ts b/src/core/textures/ColorTexture.ts index 81b25f5b..21f926ee 100644 --- a/src/core/textures/ColorTexture.ts +++ b/src/core/textures/ColorTexture.ts @@ -77,7 +77,7 @@ export class ColorTexture extends Texture { pixelData[3] = (this.color >>> 24) & 0xff; // Alpha } - this.setState('loaded', { width: 1, height: 1 }); + this.setSourceState('loaded', { width: 1, height: 1 }); return { data: pixelData, diff --git a/src/core/textures/ImageTexture.ts b/src/core/textures/ImageTexture.ts index c00c3a1a..6059cd09 100644 --- a/src/core/textures/ImageTexture.ts +++ b/src/core/textures/ImageTexture.ts @@ -226,7 +226,7 @@ export class ImageTexture extends Texture { const resp = await this.determineImageType(); if (resp.data === null) { - this.setState('failed', Error('ImageTexture: No image data')); + this.setSourceState('failed', Error('ImageTexture: No image data')); return { data: null, }; @@ -244,7 +244,7 @@ export class ImageTexture extends Texture { } // we're loaded! - this.setState('loaded', { + this.setSourceState('loaded', { width, height, }); diff --git a/src/core/textures/NoiseTexture.ts b/src/core/textures/NoiseTexture.ts index 5f5cf46b..943bf6cc 100644 --- a/src/core/textures/NoiseTexture.ts +++ b/src/core/textures/NoiseTexture.ts @@ -75,6 +75,8 @@ export class NoiseTexture extends Texture { pixelData8[i + 3] = 255; } + this.setSourceState('loaded'); + return { data: new ImageData(pixelData8, width, height), }; diff --git a/src/core/textures/RenderTexture.ts b/src/core/textures/RenderTexture.ts index d5440eeb..b3704b0b 100644 --- a/src/core/textures/RenderTexture.ts +++ b/src/core/textures/RenderTexture.ts @@ -64,6 +64,8 @@ export class RenderTexture extends Texture { } override async getTextureSource(): Promise { + this.setSourceState('loaded'); + return { data: null, premultiplyAlpha: null, diff --git a/src/core/textures/SubTexture.ts b/src/core/textures/SubTexture.ts index d02afa99..d1299f4e 100644 --- a/src/core/textures/SubTexture.ts +++ b/src/core/textures/SubTexture.ts @@ -104,18 +104,14 @@ export class SubTexture extends Texture { private onParentTxLoaded: TextureLoadedEventHandler = () => { // We ignore the parent's passed dimensions, and simply use the SubTexture's // configured dimensions (because that's all that matters here) - - // Load the core texture for this sub-texture - this.loadCtxTexture(); - - this.setState('loaded', { + this.setSourceState('loaded', { width: this.props.width, height: this.props.height, }); }; private onParentTxFailed: TextureFailedEventHandler = (target, error) => { - this.setState('failed', error); + this.setSourceState('failed', error); }; override onChangeIsRenderable(isRenderable: boolean): void { diff --git a/src/core/textures/Texture.ts b/src/core/textures/Texture.ts index bfd5e5ac..08f2cac9 100644 --- a/src/core/textures/Texture.ts +++ b/src/core/textures/Texture.ts @@ -119,6 +119,8 @@ export interface TextureStateEventMap { failed: TextureFailedEventHandler; } +export type UpdateType = 'source' | 'coreCtx'; + /** * Like the built-in Parameters<> type but skips the first parameter (which is * `target` currently) @@ -152,7 +154,12 @@ export abstract class Texture extends EventEmitter { readonly error: Error | null = null; - readonly state: TextureState = 'freed'; + // aggregate state + public state: TextureState = 'freed'; + // texture source state + private sourceState: TextureState = 'freed'; + // texture (gpu) state + private coreCtxState: TextureState = 'freed'; readonly renderableOwners = new Set(); @@ -224,15 +231,15 @@ export abstract class Texture extends EventEmitter { * * @returns */ - loadCtxTexture(): void { + loadCtxTexture(): CoreContextTexture { if (this.ctxTexture !== undefined) { - return; + return this.ctxTexture; } const ctxTexture = this.txManager.renderer.createCtxTexture(this); - ctxTexture.load(); - Object.defineProperty(this, 'ctxTexture', { value: ctxTexture }); + + return ctxTexture; } /** @@ -258,25 +265,112 @@ export abstract class Texture extends EventEmitter { * @param state * @param args */ - setState( - state: State, - ...args: ParametersSkipTarget + // setState( + // state: State, + // ...args: ParametersSkipTarget + // ): void { + // if (this.state === state) { + // return; + // } + + // // (this.state as TextureState) = state; + // if (state === 'loaded') { + // const loadedArgs = args as ParametersSkipTarget< + // TextureStateEventMap['loaded'] + // >; + // (this.dimensions as Dimensions) = loadedArgs[0]; + // } else if (state === 'failed') { + // const failedArgs = args as ParametersSkipTarget< + // TextureStateEventMap['failed'] + // >; + // (this.error as Error) = failedArgs[0]; + // } + // } + + private setState( + state: TextureState, + type: UpdateType, + errorOrDimensions?: Error | Dimensions, ): void { - if (this.state !== state) { - (this.state as TextureState) = state; - if (state === 'loaded') { - const loadedArgs = args as ParametersSkipTarget< - TextureStateEventMap['loaded'] - >; - (this.dimensions as Dimensions) = loadedArgs[0]; - } else if (state === 'failed') { - const failedArgs = args as ParametersSkipTarget< - TextureStateEventMap['failed'] - >; - (this.error as Error) = failedArgs[0]; - } - this.emit(state, ...args); + const stateObj = type === 'source' ? 'sourceState' : 'coreCtxState'; + + if (this[stateObj] === state) { + return; + } + + this[stateObj] = state; + + if (state === 'loaded') { + (this.dimensions as Dimensions) = errorOrDimensions as Dimensions; + } else if (state === 'failed') { + (this.error as Error) = errorOrDimensions as Error; + } + + this.updateState(); + } + + /** + * Set the source state of the texture + * + * @remarks + * The source of the texture can either be generated by the texture itself or + * loaded from an external source. + * + * @param state State of the texture + * @param errorOrDimensions Error or dimensions of the texture + */ + public setSourceState( + state: TextureState, + errorOrDimensions?: Error | Dimensions, + ): void { + this.setState(state, 'source', errorOrDimensions); + } + + /** + * Set the core context state of the texture + * + * @remarks + * The core context state of the texture is the state of the texture on the GPU. + * + * @param state State of the texture + * @param errorOrDimensions Error or dimensions of the texture + */ + public setCoreCtxState( + state: TextureState, + errorOrDimensions?: Error | Dimensions, + ): void { + this.setState(state, 'coreCtx', errorOrDimensions); + } + + private updateState(): void { + const ctxState = this.coreCtxState; + const sourceState = this.sourceState; + + let newState: TextureState = 'freed'; + let payload: Error | Dimensions | null = null; + if (sourceState === 'failed' || ctxState === 'failed') { + newState = 'failed'; + + // If the texture failed to load, the error is set by the source + payload = this.error; + } else if (sourceState === 'loading' || ctxState === 'loading') { + newState = 'loading'; + } else if (this.sourceState === 'loaded' && ctxState === 'loaded') { + newState = 'loaded'; + + // If the texture is loaded, the dimensions are set by the source + payload = this.dimensions; + } else { + newState = 'freed'; } + + if (this.state === newState) { + return; + } + + // emit the new state + this.state = newState; + this.emit(newState, payload); } /** From 13c343543af36bbea9a92a9b33d2212cff80ac0d Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Mon, 9 Dec 2024 23:25:57 +0100 Subject: [PATCH 05/32] feat: Add stress test for rendering various texture types and colors --- examples/tests/stress-mix.ts | 100 +++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 examples/tests/stress-mix.ts diff --git a/examples/tests/stress-mix.ts b/examples/tests/stress-mix.ts new file mode 100644 index 00000000..efc483f0 --- /dev/null +++ b/examples/tests/stress-mix.ts @@ -0,0 +1,100 @@ +import type { INode, ITextNode } from '../../dist/exports/index.js'; +import type { ExampleSettings } from '../common/ExampleSettings.js'; + +export const Colors = { + Black: 0x000000ff, + Red: 0xff0000ff, + Green: 0x00ff00ff, + Blue: 0x0000ffff, + Magenta: 0xff00ffff, + Gray: 0x7f7f7fff, + White: 0xffffffff, +}; + +const textureType = ['Image', 'Color', 'Text', 'Gradient']; + +const gradients = [ + 'colorTl', + 'colorTr', + 'colorBl', + 'colorBr', + 'colorTop', + 'colorBottom', + 'colorLeft', + 'colorRight', + 'color', +]; + +export default async function ({ renderer, testRoot }: ExampleSettings) { + const screenWidth = 1920; + const screenHeight = 1080; + const totalImages = 1000; + + // Calculate the grid dimensions for square images + const gridSize = Math.ceil(Math.sqrt(totalImages)); // Approximate grid size + const imageSize = Math.floor( + Math.min(screenWidth / gridSize, screenHeight / gridSize), + ); // Square size + + // Create a root node for the grid + const gridNode = renderer.createNode({ + x: 0, + y: 0, + width: screenWidth, + height: screenHeight, + parent: testRoot, + }); + + // Create and position images in the grid + new Array(totalImages).fill(0).forEach((_, i) => { + const x = (i % gridSize) * imageSize; + const y = Math.floor(i / gridSize) * imageSize; + + // pick a random texture type + const texture = textureType[Math.floor(Math.random() * textureType.length)]; + + // pick a random color from Colors + const clr = + Object.values(Colors)[ + Math.floor(Math.random() * Object.keys(Colors).length) + ]; + + const node = { + parent: gridNode, + x, + y, + width: imageSize, + height: imageSize, + } as Partial | Partial; + + if (texture === 'Image') { + node.src = `https://picsum.photos/id/${i}/${imageSize}/${imageSize}`; + } else if (texture === 'Text') { + (node as Partial).text = `Text ${i}`; + (node as Partial).fontSize = 18; + node.color = clr; + } else if (texture === 'Gradient') { + const gradient = gradients[Math.floor(Math.random() * gradients.length)]; + // @ts-ignore + node[gradient] = clr; + + const secondGradient = + gradients[Math.floor(Math.random() * gradients.length)]; + const secondColor = + Object.values(Colors)[ + Math.floor(Math.random() * Object.keys(Colors).length) + ]; + + // @ts-ignore + node[secondGradient] = secondColor; + } else { + node.color = clr; + } + + if (texture === 'Text') { + renderer.createTextNode(node as ITextNode); + } else { + renderer.createNode(node); + } + }); +} From 1cb85e81299a973606f19fa54c6e859a61d32d5f Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Mon, 9 Dec 2024 23:26:14 +0100 Subject: [PATCH 06/32] fix: Implement rerender method to request rendering from the stage --- src/main-api/Renderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main-api/Renderer.ts b/src/main-api/Renderer.ts index 0e557917..e44a7506 100644 --- a/src/main-api/Renderer.ts +++ b/src/main-api/Renderer.ts @@ -656,7 +656,7 @@ export class RendererMain extends EventEmitter { * May not do anything if the render loop is running on a separate worker. */ rerender() { - throw new Error('Not implemented'); + this.stage.requestRender(); } /** From c4cc9633823d7792b2e69bceebd48e555e9706eb Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Mon, 9 Dec 2024 23:26:38 +0100 Subject: [PATCH 07/32] fix: RTT texture should load immediately --- src/core/CoreNode.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 28c2ffcd..cfc375e2 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -2018,10 +2018,14 @@ export class CoreNode extends EventEmitter { } } private initRenderTexture() { - this.texture = this.stage.txManager.loadTexture('RenderTexture', { - width: this.width, - height: this.height, - }); + this.texture = this.stage.txManager.loadTexture( + 'RenderTexture', + { + width: this.width, + height: this.height, + }, + true, + ); this.stage.renderer?.renderToTexture(this); // Only this RTT node } From 749aa71c4e2ab5a425ab422c509939222ace4c88 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Tue, 10 Dec 2024 21:22:05 +0100 Subject: [PATCH 08/32] refactor: Replace loadTexture with createTexture for improved texture management Load Textures only when node becomes visible. --- src/core/CoreNode.ts | 37 ++++++--- src/core/CoreTextureManager.ts | 80 +++++++++++-------- src/core/Stage.ts | 13 ++- .../SdfTrFontFace/SdfTrFontFace.ts | 23 +++--- .../renderers/CanvasTextRenderer.ts | 7 +- src/main-api/Renderer.ts | 2 +- 6 files changed, 93 insertions(+), 69 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index cfc375e2..c6efbfa6 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -26,7 +26,6 @@ import type { TextureOptions } from './CoreTextureManager.js'; import type { CoreRenderer } from './renderers/CoreRenderer.js'; import type { Stage } from './Stage.js'; import { - TextureType, type Texture, type TextureFailedEventHandler, type TextureFreedEventHandler, @@ -1389,12 +1388,24 @@ export class CoreNode extends EventEmitter { * @returns */ updateIsRenderable() { - let newIsRenderable; + let newIsRenderable: boolean; if (this.worldAlpha === 0 || !this.hasRenderableProperties()) { newIsRenderable = false; } else { newIsRenderable = this.renderState > CoreNodeRenderState.OutOfBounds; } + + // If the texture is not loaded and the node is renderable, load the texture + // this only needs to happen once or until the texture is no longer loaded + if ( + this.texture !== null && + this.texture.state !== 'loaded' && + this.renderState > CoreNodeRenderState.OutOfBounds + ) { + console.log('Loading texture'); + this.stage.txManager.loadTexture(this.texture); + } + if (this.isRenderable !== newIsRenderable) { this.isRenderable = newIsRenderable; this.onChangeIsRenderable(newIsRenderable); @@ -1636,7 +1647,7 @@ export class CoreNode extends EventEmitter { this.setUpdateType(UpdateType.Local); if (this.props.rtt) { - this.texture = this.stage.txManager.loadTexture('RenderTexture', { + this.texture = this.stage.txManager.createTexture('RenderTexture', { width: this.width, height: this.height, }); @@ -1656,7 +1667,7 @@ export class CoreNode extends EventEmitter { this.setUpdateType(UpdateType.Local); if (this.props.rtt) { - this.texture = this.stage.txManager.loadTexture('RenderTexture', { + this.texture = this.stage.txManager.createTexture('RenderTexture', { width: this.width, height: this.height, }); @@ -2018,14 +2029,14 @@ export class CoreNode extends EventEmitter { } } private initRenderTexture() { - this.texture = this.stage.txManager.loadTexture( - 'RenderTexture', - { - width: this.width, - height: this.height, - }, - true, - ); + this.texture = this.stage.txManager.createTexture('RenderTexture', { + width: this.width, + height: this.height, + }); + + // call load immediately to ensure the texture is created + // WvB do we really need this? + this.stage.txManager.loadTexture(this.texture, true); this.stage.renderer?.renderToTexture(this); // Only this RTT node } @@ -2107,7 +2118,7 @@ export class CoreNode extends EventEmitter { return; } - this.texture = this.stage.txManager.loadTexture('ImageTexture', { + this.texture = this.stage.txManager.createTexture('ImageTexture', { src: imageUrl, width: this.props.width, height: this.props.height, diff --git a/src/core/CoreTextureManager.ts b/src/core/CoreTextureManager.ts index 7fd003e6..e940bfac 100644 --- a/src/core/CoreTextureManager.ts +++ b/src/core/CoreTextureManager.ts @@ -135,12 +135,12 @@ export class CoreTextureManager extends EventEmitter { /** * Map of textures by cache key */ - // keyCache: Map = new Map(); + keyCache: Map = new Map(); /** * Map of cache keys by texture */ - // inverseKeyCache: WeakMap = new WeakMap(); + inverseKeyCache: WeakMap = new WeakMap(); /** * Map of texture constructors by their type name @@ -359,25 +359,41 @@ export class CoreTextureManager extends EventEmitter { } /** - * Override loadTexture to use the batched approach. + * Create a texture * - * @param textureType - The type of texture to load + * @param textureType - The type of texture to create * @param props - The properties to use for the texture - * @param immediate - Whether to prioritize the texture for immediate loading */ - loadTexture( + createTexture( textureType: Type, props: ExtractProps, - priority?: boolean, ): InstanceType { + let texture: Texture | undefined; const TextureClass = this.txConstructors[textureType]; if (!TextureClass) { throw new Error(`Texture type "${textureType}" is not registered`); } - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any - const texture = new TextureClass(this, props as any); + const cacheKey = TextureClass.makeCacheKey(props as any); + if (cacheKey && this.keyCache.has(cacheKey)) { + console.log('Getting texture by cache key', cacheKey); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + texture = this.keyCache.get(cacheKey)!; + } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + texture = new TextureClass(this, props as any); + } + return texture as InstanceType; + } + + /** + * Override loadTexture to use the batched approach. + * + * @param texture - The texture to load + * @param immediate - Whether to prioritize the texture for immediate loading + */ + loadTexture(texture: Texture, priority?: boolean): void { // prioritize the texture for immediate loading if (priority === true) { texture @@ -389,14 +405,10 @@ export class CoreTextureManager extends EventEmitter { .catch((err) => { console.error(err); }); - - return texture as InstanceType; } // enqueue the texture for download and upload this.enqueueDownloadTextureSource(texture); - - return texture as InstanceType; } /** @@ -447,25 +459,25 @@ export class CoreTextureManager extends EventEmitter { ); } - // private initTextureToCache(texture: Texture, cacheKey: string) { - // const { keyCache, inverseKeyCache } = this; - // keyCache.set(cacheKey, texture); - // inverseKeyCache.set(texture, cacheKey); - // } - - // /** - // * Remove a texture from the cache - // * - // * @remarks - // * Called by Texture Cleanup when a texture is freed. - // * - // * @param texture - // */ - // removeTextureFromCache(texture: Texture) { - // const { inverseKeyCache, keyCache } = this; - // const cacheKey = inverseKeyCache.get(texture); - // if (cacheKey) { - // keyCache.delete(cacheKey); - // } - // } + private initTextureToCache(texture: Texture, cacheKey: string) { + const { keyCache, inverseKeyCache } = this; + keyCache.set(cacheKey, texture); + inverseKeyCache.set(texture, cacheKey); + } + + /** + * Remove a texture from the cache + * + * @remarks + * Called by Texture Cleanup when a texture is freed. + * + * @param texture + */ + removeTextureFromCache(texture: Texture) { + const { inverseKeyCache, keyCache } = this; + const cacheKey = inverseKeyCache.get(texture); + if (cacheKey) { + keyCache.delete(cacheKey); + } + } } diff --git a/src/core/Stage.ts b/src/core/Stage.ts index fefd74d2..ffe60b30 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -158,13 +158,12 @@ export class Stage { this.requestRender(); }); - this.defaultTexture = this.txManager.loadTexture( - 'ColorTexture', - { - color: 0xffffffff, - }, - true, - ); + this.defaultTexture = this.txManager.createTexture('ColorTexture', { + color: 0xffffffff, + }); + + this.txManager.loadTexture(this.defaultTexture, true); + assertTruthy(this.defaultTexture instanceof ColorTexture); // Mark the default texture as ALWAYS renderable diff --git a/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.ts b/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.ts index 8d79d701..5ae9fe31 100644 --- a/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.ts +++ b/src/core/text-rendering/font-face-types/SdfTrFontFace/SdfTrFontFace.ts @@ -77,18 +77,17 @@ export class SdfTrFontFace< ); // Load image - this.texture = stage.txManager.loadTexture( - 'ImageTexture', - { - src: atlasUrl, - // IMPORTANT: The SDF shader requires the alpha channel to NOT be - // premultiplied on the atlas texture. If it is premultiplied, the - // rendering of SDF glyphs (especially single-channel SDF fonts) will - // be very jagged. - premultiplyAlpha: false, - }, - true, - ); + this.texture = stage.txManager.createTexture('ImageTexture', { + src: atlasUrl, + // IMPORTANT: The SDF shader requires the alpha channel to NOT be + // premultiplied on the atlas texture. If it is premultiplied, the + // rendering of SDF glyphs (especially single-channel SDF fonts) will + // be very jagged. + premultiplyAlpha: false, + }); + + // Load the texture + stage.txManager.loadTexture(this.texture, true); this.texture.on('loaded', () => { this.checkLoaded(); diff --git a/src/core/text-rendering/renderers/CanvasTextRenderer.ts b/src/core/text-rendering/renderers/CanvasTextRenderer.ts index 6b0a14fa..60bc9d9c 100644 --- a/src/core/text-rendering/renderers/CanvasTextRenderer.ts +++ b/src/core/text-rendering/renderers/CanvasTextRenderer.ts @@ -99,7 +99,7 @@ export class CanvasTextRenderer extends TextRenderer { } else { this.canvas = document.createElement('canvas'); } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + let context = this.canvas.getContext('2d', { willReadFrequently: true, }) as OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D | null; @@ -338,7 +338,7 @@ export class CanvasTextRenderer extends TextRenderer { assertTruthy(state.renderInfo); const node = state.node; - const texture = this.stage.txManager.loadTexture('ImageTexture', { + const texture = this.stage.txManager.createTexture('ImageTexture', { src: function ( this: CanvasTextRenderer, lightning2TextRenderer: LightningTextTextureRenderer, @@ -361,6 +361,9 @@ export class CanvasTextRenderer extends TextRenderer { ); }.bind(this, state.lightning2TextRenderer, state.renderInfo), }); + + this.stage.txManager.loadTexture(texture); + if (state.textureNode) { // Use the existing texture node state.textureNode.texture = texture; diff --git a/src/main-api/Renderer.ts b/src/main-api/Renderer.ts index e44a7506..205d7a19 100644 --- a/src/main-api/Renderer.ts +++ b/src/main-api/Renderer.ts @@ -516,7 +516,7 @@ export class RendererMain extends EventEmitter { textureType: TxType, props: ExtractProps, ): InstanceType { - return this.stage.txManager.loadTexture(textureType, props); + return this.stage.txManager.createTexture(textureType, props); } /** From c2f0119907313a330b001a4a0317bd62bfc2aa4c Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Wed, 11 Dec 2024 19:46:08 +0100 Subject: [PATCH 09/32] fix: Fix canvas default texture handling --- src/core/CoreNode.ts | 9 +++- src/core/Stage.ts | 52 ++++++++++++------- .../renderers/canvas/CanvasCoreTexture.ts | 6 ++- 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index c6efbfa6..3d98d856 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -769,7 +769,12 @@ export class CoreNode extends EventEmitter { ); // load default texture if no texture is set - if (!this.props.src && !this.props.texture && !this.props.rtt) { + if ( + this.stage.defaultTexture !== null && + !this.props.src && + !this.props.texture && + !this.props.rtt + ) { this.texture = this.stage.defaultTexture; } } @@ -1556,7 +1561,7 @@ export class CoreNode extends EventEmitter { assertTruthy(this.globalTransform); assertTruthy(this.renderCoords); - assertTruthy(this.texture); + // assertTruthy(this.texture); // add to list of renderables to be sorted before rendering renderer.addQuad({ diff --git a/src/core/Stage.ts b/src/core/Stage.ts index ffe60b30..342fabe4 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -105,7 +105,7 @@ export class Stage { public readonly strictBound: Bound; public readonly preloadBound: Bound; public readonly strictBounds: boolean; - public readonly defaultTexture: Texture; + public readonly defaultTexture: Texture | null = null; /** * Renderer Event Bus for the Stage to emit events onto @@ -158,25 +158,6 @@ export class Stage { this.requestRender(); }); - this.defaultTexture = this.txManager.createTexture('ColorTexture', { - color: 0xffffffff, - }); - - this.txManager.loadTexture(this.defaultTexture, true); - - assertTruthy(this.defaultTexture instanceof ColorTexture); - - // Mark the default texture as ALWAYS renderable - // This prevents it from ever being cleaned up. - // Fixes https://github.com/lightning-js/renderer/issues/262 - this.defaultTexture.setRenderableOwner(this, true); - - // When the default texture is loaded, request a render in case the - // RAF is paused. Fixes: https://github.com/lightning-js/renderer/issues/123 - this.defaultTexture.once('loaded', () => { - this.requestRender(); - }); - this.txMemManager = new TextureMemoryManager(this, textureMemory); this.shManager = new CoreShaderManager(); this.animationManager = new AnimationManager(); @@ -212,6 +193,10 @@ export class Stage { this.renderer = new renderEngine(rendererOptions); const renderMode = this.renderer.mode || 'webgl'; + if (renderMode === 'webgl') { + this.createDefaultTexture(); + } + this.defShaderCtr = this.renderer.getDefShaderCtr(); setPremultiplyMode(renderMode); @@ -318,6 +303,33 @@ export class Stage { }); } + /** + * Create default PixelTexture + */ + createDefaultTexture() { + (this.defaultTexture as ColorTexture) = this.txManager.createTexture( + 'ColorTexture', + { + color: 0xffffffff, + }, + ); + + assertTruthy(this.defaultTexture instanceof ColorTexture); + + this.txManager.loadTexture(this.defaultTexture, true); + + // Mark the default texture as ALWAYS renderable + // This prevents it from ever being cleaned up. + // Fixes https://github.com/lightning-js/renderer/issues/262 + this.defaultTexture.setRenderableOwner(this, true); + + // When the default texture is loaded, request a render in case the + // RAF is paused. Fixes: https://github.com/lightning-js/renderer/issues/123 + this.defaultTexture.once('loaded', () => { + this.requestRender(); + }); + } + /** * Update animations */ diff --git a/src/core/renderers/canvas/CanvasCoreTexture.ts b/src/core/renderers/canvas/CanvasCoreTexture.ts index 25d5fdc7..b46b1b08 100644 --- a/src/core/renderers/canvas/CanvasCoreTexture.ts +++ b/src/core/renderers/canvas/CanvasCoreTexture.ts @@ -40,6 +40,7 @@ export class CanvasCoreTexture extends CoreContextTexture { return; } this.textureSource.setCoreCtxState('loading'); + this.onLoadRequest() .then((size) => { this.textureSource.setCoreCtxState('loaded', size); @@ -126,8 +127,8 @@ export class CanvasCoreTexture extends CoreContextTexture { } private async onLoadRequest(): Promise { - const data = this.textureSource.textureData; - assertTruthy(data, 'Texture data is null'); + assertTruthy(this.textureSource?.textureData?.data, 'Texture data is null'); + const { data } = this.textureSource.textureData; // TODO: canvas from text renderer should be able to provide the canvas directly // instead of having to re-draw it into a new canvas... @@ -146,6 +147,7 @@ export class CanvasCoreTexture extends CoreContextTexture { this.image = data; return { width: data.width, height: data.height }; } + return { width: 0, height: 0 }; } } From 0aed451383a41430e306f0bfcbbfe2d9e6faf693 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Wed, 11 Dec 2024 22:15:44 +0100 Subject: [PATCH 10/32] feat: Introduce textureProcessingLimit flag to control texture batching size --- src/core/CoreNode.ts | 1 - src/core/CoreTextureManager.ts | 42 +++++++--------------------------- src/core/Stage.ts | 3 ++- src/main-api/Renderer.ts | 13 +++++++++++ 4 files changed, 23 insertions(+), 36 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 3d98d856..2b4a4438 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -1407,7 +1407,6 @@ export class CoreNode extends EventEmitter { this.texture.state !== 'loaded' && this.renderState > CoreNodeRenderState.OutOfBounds ) { - console.log('Loading texture'); this.stage.txManager.loadTexture(this.texture); } diff --git a/src/core/CoreTextureManager.ts b/src/core/CoreTextureManager.ts index e940bfac..a3f005de 100644 --- a/src/core/CoreTextureManager.ts +++ b/src/core/CoreTextureManager.ts @@ -149,8 +149,6 @@ export class CoreTextureManager extends EventEmitter { private downloadTextureSourceQueue: Array = []; private uploadTextureQueue: Array = []; - - private maxItemsPerFrame = 25; // Configurable limit for items to process per frame private initialized = false; imageWorkerManager: ImageWorkerManager | null = null; @@ -309,35 +307,6 @@ export class CoreTextureManager extends EventEmitter { this.txConstructors[textureType] = textureClass; } - /* - loadTexture( - textureType: Type, - props: ExtractProps, - ): InstanceType { - let texture: Texture | undefined; - const TextureClass = this.txConstructors[textureType]; - if (!TextureClass) { - throw new Error(`Texture type "${textureType}" is not registered`); - } - - if (!texture) { - const cacheKey = TextureClass.makeCacheKey(props as any); - if (cacheKey && this.keyCache.has(cacheKey)) { - // console.log('Getting texture by cache key', cacheKey); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - texture = this.keyCache.get(cacheKey)!; - } else { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any - texture = new TextureClass(this, props as any); - if (cacheKey) { - this.initTextureToCache(texture, cacheKey); - } - } - } - return texture as InstanceType; - } - */ - /** * Enqueue a texture for downloading its source image. */ @@ -413,8 +382,10 @@ export class CoreTextureManager extends EventEmitter { /** * Process a limited number of downloads and uploads. + * + * @param maxItems - The maximum number of items to process */ - processSome(maxItems = this.maxItemsPerFrame): void { + processSome(maxItems = 0): void { if (this.initialized === false) { return; } @@ -422,7 +393,10 @@ export class CoreTextureManager extends EventEmitter { let itemsProcessed = 0; // Process uploads - while (this.uploadTextureQueue.length > 0 && itemsProcessed < maxItems) { + while ( + this.uploadTextureQueue.length > 0 && + (maxItems === 0 || itemsProcessed < maxItems) + ) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const texture = this.uploadTextureQueue.shift()!; const coreContext = texture.loadCtxTexture(); @@ -433,7 +407,7 @@ export class CoreTextureManager extends EventEmitter { // Process downloads while ( this.downloadTextureSourceQueue.length > 0 && - itemsProcessed < maxItems + (maxItems === 0 || itemsProcessed < maxItems) ) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const texture = this.downloadTextureSourceQueue.shift()!; diff --git a/src/core/Stage.ts b/src/core/Stage.ts index 342fabe4..fa070c82 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -75,6 +75,7 @@ export interface StageOptions { fontEngines: (typeof CanvasTextRenderer | typeof SdfTextRenderer)[]; inspector: boolean; strictBounds: boolean; + textureProcessingLimit: number; } export type StageFpsUpdateHandler = ( @@ -367,7 +368,7 @@ export class Stage { // Process some textures // TODO this should have a configurable amount - this.txManager.processSome(); + this.txManager.processSome(this.options.textureProcessingLimit); // Reset render operations and clear the canvas renderer.reset(); diff --git a/src/main-api/Renderer.ts b/src/main-api/Renderer.ts index 205d7a19..268a6a31 100644 --- a/src/main-api/Renderer.ts +++ b/src/main-api/Renderer.ts @@ -267,6 +267,17 @@ export interface RendererMainSettings { * @defaultValue `true` */ strictBounds?: boolean; + + /** + * Texture Processing Limit + * + * @remarks + * The maximum number of textures to process in a single frame. This is used to + * prevent the renderer from processing too many textures in a single frame. + * + * @defaultValue `0` + */ + textureProcessingLimit?: number; } /** @@ -358,6 +369,7 @@ export class RendererMain extends EventEmitter { quadBufferSize: settings.quadBufferSize ?? 4 * 1024 * 1024, fontEngines: settings.fontEngines, strictBounds: settings.strictBounds ?? true, + textureProcessingLimit: settings.textureProcessingLimit || 0, }; this.settings = resolvedSettings; @@ -400,6 +412,7 @@ export class RendererMain extends EventEmitter { fontEngines: this.settings.fontEngines, inspector: this.settings.inspector !== null, strictBounds: this.settings.strictBounds, + textureProcessingLimit: this.settings.textureProcessingLimit, }); // Extract the root node From 2a328466c746e6916124521044a9d911bdba2674 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Wed, 11 Dec 2024 22:28:57 +0100 Subject: [PATCH 11/32] chore: Remove leftover init to image worker --- src/core/lib/ImageWorker.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/core/lib/ImageWorker.ts b/src/core/lib/ImageWorker.ts index d38d78a7..e3251591 100644 --- a/src/core/lib/ImageWorker.ts +++ b/src/core/lib/ImageWorker.ts @@ -215,15 +215,7 @@ export class ImageWorkerManager { const blobURL: string = (self.URL ? URL : webkitURL).createObjectURL(blob); const workers: Worker[] = []; for (let i = 0; i < numWorkers; i++) { - const worker = new Worker(blobURL); - - // Pass `createImageBitmap` support level during worker initialization - worker.postMessage({ - type: 'init', - support: createImageBitmapSupport, - }); - - workers.push(worker); + workers.push(new Worker(blobURL)); } return workers; } From 25dcbfbf537c30edc32e5b47e5a9beecc5f59d9c Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Wed, 11 Dec 2024 23:33:20 +0100 Subject: [PATCH 12/32] fix: Fix caching, cleanup old comments, fix factory test --- examples/tests/texture-factory.ts | 4 ++-- src/core/CoreTextureManager.ts | 5 ++++- src/core/TextureMemoryManager.ts | 3 +-- src/core/textures/Texture.ts | 32 ------------------------------- 4 files changed, 7 insertions(+), 37 deletions(-) diff --git a/examples/tests/texture-factory.ts b/examples/tests/texture-factory.ts index 14e90b7d..5fd0a8b5 100644 --- a/examples/tests/texture-factory.ts +++ b/examples/tests/texture-factory.ts @@ -100,7 +100,7 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { }); return new Promise((resolve, reject) => { - setTimeout(() => { + renderer.once('idle', () => { let result = ''; if ((setKey && factoryRuns === 1) || (!setKey && factoryRuns === 2)) { textNode.color = 0x00ff00ff; @@ -112,7 +112,7 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { textNode.text += `: ${result}`; if (result === 'Pass') resolve(true); else reject({ setKey, factoryRuns }); - }, 50); + }); }); } diff --git a/src/core/CoreTextureManager.ts b/src/core/CoreTextureManager.ts index a3f005de..f0165b54 100644 --- a/src/core/CoreTextureManager.ts +++ b/src/core/CoreTextureManager.ts @@ -345,12 +345,15 @@ export class CoreTextureManager extends EventEmitter { const cacheKey = TextureClass.makeCacheKey(props as any); if (cacheKey && this.keyCache.has(cacheKey)) { - console.log('Getting texture by cache key', cacheKey); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion texture = this.keyCache.get(cacheKey)!; } else { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any texture = new TextureClass(this, props as any); + + if (cacheKey) { + this.initTextureToCache(texture, cacheKey); + } } return texture as InstanceType; diff --git a/src/core/TextureMemoryManager.ts b/src/core/TextureMemoryManager.ts index 588d00b9..37ec652a 100644 --- a/src/core/TextureMemoryManager.ts +++ b/src/core/TextureMemoryManager.ts @@ -227,9 +227,8 @@ export class TextureMemoryManager { break; } if (texture.preventCleanup === false) { - // texture.ctxTexture.free(); texture.free(); - // txManager.removeTextureFromCache(texture); + txManager.removeTextureFromCache(texture); } if (this.memUsed <= memTarget) { // Stop once we've freed enough textures to reach under the target threshold diff --git a/src/core/textures/Texture.ts b/src/core/textures/Texture.ts index 08f2cac9..0e332209 100644 --- a/src/core/textures/Texture.ts +++ b/src/core/textures/Texture.ts @@ -255,38 +255,6 @@ export abstract class Texture extends EventEmitter { } } - /** - * Set the state of the texture - * - * @remark - * Intended for internal-use only but declared public so that it can be set - * by it's associated {@link CoreContextTexture} - * - * @param state - * @param args - */ - // setState( - // state: State, - // ...args: ParametersSkipTarget - // ): void { - // if (this.state === state) { - // return; - // } - - // // (this.state as TextureState) = state; - // if (state === 'loaded') { - // const loadedArgs = args as ParametersSkipTarget< - // TextureStateEventMap['loaded'] - // >; - // (this.dimensions as Dimensions) = loadedArgs[0]; - // } else if (state === 'failed') { - // const failedArgs = args as ParametersSkipTarget< - // TextureStateEventMap['failed'] - // >; - // (this.error as Error) = failedArgs[0]; - // } - // } - private setState( state: TextureState, type: UpdateType, From 0205760f7e0d561d05d7459d76a281437e88c68b Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Thu, 12 Dec 2024 14:16:25 +0100 Subject: [PATCH 13/32] fix: Update overlayText event handling to filter by type --- examples/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/index.ts b/examples/index.ts index b8fa1a64..5a29a736 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -170,9 +170,13 @@ async function runTest( parent: renderer.root, fontSize: 50, }); - overlayText.once( + overlayText.on( 'loaded', - (target: any, { dimensions }: NodeLoadedPayload) => { + (target: any, { type, dimensions }: NodeLoadedPayload) => { + if (type !== 'text') { + return; + } + overlayText.x = renderer.settings.appWidth - dimensions.width - 20; overlayText.y = renderer.settings.appHeight - dimensions.height - 20; }, From 1fe55203f0b4460002608cc3f89620e3242e2fa0 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Fri, 13 Dec 2024 11:39:37 +0100 Subject: [PATCH 14/32] chore: Fix RTT with TT --- src/core/CoreNode.ts | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 2b4a4438..1a18933a 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -768,15 +768,7 @@ export class CoreNode extends EventEmitter { UpdateType.RenderState, ); - // load default texture if no texture is set - if ( - this.stage.defaultTexture !== null && - !this.props.src && - !this.props.texture && - !this.props.rtt - ) { - this.texture = this.stage.defaultTexture; - } + this.createDefaultTexture(); } //#region Textures @@ -816,6 +808,18 @@ export class CoreNode extends EventEmitter { }); } + createDefaultTexture(): void { + // load default texture if no texture is set + if ( + this.stage.defaultTexture !== null && + !this.props.src && + !this.props.texture && + !this.props.rtt + ) { + this.texture = this.stage.defaultTexture; + } + } + unloadTexture(): void { if (this.texture !== null) { this.texture.off('loaded', this.onTextureLoaded); @@ -2213,16 +2217,22 @@ export class CoreNode extends EventEmitter { if (this.props.texture === value) { return; } + const oldTexture = this.props.texture; if (oldTexture) { oldTexture.setRenderableOwner(this, false); this.unloadTexture(); } + this.props.texture = value; - if (value) { + if (value !== null) { value.setRenderableOwner(this, this.isRenderable); this.loadTexture(); + } else { + // If the texture is null, create a default texture + this.createDefaultTexture(); } + this.setUpdateType(UpdateType.IsRenderable); } From 6e4ff844cfda39e241fe61c87e1f6eda3bcfdac5 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Fri, 13 Dec 2024 14:35:30 +0100 Subject: [PATCH 15/32] fix: Ignore loaded events for 1x1 textures --- examples/index.ts | 2 +- src/core/CoreNode.ts | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/examples/index.ts b/examples/index.ts index 5a29a736..efe64795 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -429,7 +429,7 @@ async function runAutomation( // Allow some time for all images to load and the RaF to unpause // and render if needed. - await waitForRendererIdle(renderer); + await new Promise((resolve) => setTimeout(resolve, 200)); if (snapshot) { console.log(`Calling snapshot(${testName})`); await snapshot(testName, adjustedOptions); diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 1a18933a..2e2c9845 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -812,9 +812,10 @@ export class CoreNode extends EventEmitter { // load default texture if no texture is set if ( this.stage.defaultTexture !== null && - !this.props.src && - !this.props.texture && - !this.props.rtt + this.props.src === null && + this.props.texture === null && + this.props.rtt === false && + this.hasRenderableProperties() === true ) { this.texture = this.stage.defaultTexture; } @@ -849,10 +850,13 @@ export class CoreNode extends EventEmitter { this.notifyParentRTTOfUpdate(); } - this.emit('loaded', { - type: 'texture', - dimensions, - } satisfies NodeTextureLoadedPayload); + // ignore 1x1 pixel textures + if (dimensions.width > 1 && dimensions.height > 1) { + this.emit('loaded', { + type: 'texture', + dimensions, + } satisfies NodeTextureLoadedPayload); + } // Trigger a local update if the texture is loaded and the resizeMode is 'contain' if (this.props.textureOptions?.resizeMode?.type === 'contain') { @@ -1564,7 +1568,7 @@ export class CoreNode extends EventEmitter { assertTruthy(this.globalTransform); assertTruthy(this.renderCoords); - // assertTruthy(this.texture); + assertTruthy(this.texture); // add to list of renderables to be sorted before rendering renderer.addQuad({ From 93e5473b58134c64fd0b3e3c329dd7308d4527ee Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Fri, 13 Dec 2024 22:27:08 +0100 Subject: [PATCH 16/32] fix: Subtexture parent load and add test --- examples/tests/texture-spritemap.ts | 82 +++++++++++++++++++++++++++++ src/core/textures/SubTexture.ts | 5 ++ 2 files changed, 87 insertions(+) create mode 100644 examples/tests/texture-spritemap.ts diff --git a/examples/tests/texture-spritemap.ts b/examples/tests/texture-spritemap.ts new file mode 100644 index 00000000..59efdd38 --- /dev/null +++ b/examples/tests/texture-spritemap.ts @@ -0,0 +1,82 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Comcast Cable Communications Management, LLC. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ExampleSettings } from '../common/ExampleSettings.js'; +import spritemap from '../assets/spritemap.png'; + +export async function automation(settings: ExampleSettings) { + // Snapshot single page + await test(settings); + await settings.snapshot(); +} + +export default async function test({ renderer, testRoot }: ExampleSettings) { + const FONT_SIZE = 45; + + renderer.createTextNode({ + text: `Texture Spritemap Test`, + fontSize: FONT_SIZE, + offsetY: -5, + parent: testRoot, + }); + + const spriteMapTexture = renderer.createTexture('ImageTexture', { + src: spritemap, + }); + + spriteMapTexture.on('load', (dimensions) => { + console.log('Spritemap Texture loaded', dimensions); + }); + + function execTest(y: number, x: number, title: string): Promise { + renderer.createTextNode({ + text: title, + fontSize: FONT_SIZE, + y: y, + parent: testRoot, + }); + + const character = renderer.createTexture('SubTexture', { + texture: spriteMapTexture, + x: x, + y: 0, + width: 100, + height: 150, + }); + + renderer.createNode({ + x: 20, + y: y + 80, + width: 100, + height: 150, + texture: character, + parent: testRoot, + }); + + return new Promise((resolve, reject) => { + renderer.once('idle', () => { + resolve(true); + }); + }); + } + + await execTest(80, 0, 'Character 1'); + await execTest(300, 100, 'Character 2'); + await execTest(520, 200, 'Character 3'); +} diff --git a/src/core/textures/SubTexture.ts b/src/core/textures/SubTexture.ts index d1299f4e..b3612095 100644 --- a/src/core/textures/SubTexture.ts +++ b/src/core/textures/SubTexture.ts @@ -120,6 +120,11 @@ export class SubTexture extends Texture { } override async getTextureSource(): Promise { + // Check if parent texture is loaded + if (this.parentTexture.state !== 'loaded') { + await this.txManager.loadTexture(this.parentTexture); + } + return { data: this.props, }; From c676746a600f4bd4f90d189b928d262aa631da21 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Sun, 15 Dec 2024 22:43:17 +0100 Subject: [PATCH 17/32] fix: SubTexture caching & loading, duplicate textures, image error handling --- examples/tests/textures.ts | 6 ++-- src/core/CoreNode.ts | 2 +- src/core/CoreTextureManager.ts | 53 +++++++++++++++++++++++++------ src/core/textures/ImageTexture.ts | 16 +++++++--- src/core/textures/SubTexture.ts | 22 ++++++++++--- src/core/textures/Texture.ts | 17 ++++++---- 6 files changed, 87 insertions(+), 29 deletions(-) diff --git a/examples/tests/textures.ts b/examples/tests/textures.ts index 60009545..32bfc1e2 100644 --- a/examples/tests/textures.ts +++ b/examples/tests/textures.ts @@ -78,7 +78,7 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { await execLoadingTest(elevator, 200, 268); - // Test: Check that we capture a texture load failure + // // Test: Check that we capture a texture load failure const failure = renderer.createNode({ x: curX, y: curY, @@ -88,7 +88,7 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { await execFailureTest(failure); - // Test: Check that we capture a texture load failure + // // Test: Check that we capture a texture load failure const failure2 = renderer.createNode({ x: curX, y: curY, @@ -98,7 +98,7 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { await execFailureTest(failure2); - // Test: NoiseTexture + // // Test: NoiseTexture curX = renderer.settings.appWidth / 2; curY = BEGIN_Y; diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 2e2c9845..41cdb9d9 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -1412,7 +1412,7 @@ export class CoreNode extends EventEmitter { // this only needs to happen once or until the texture is no longer loaded if ( this.texture !== null && - this.texture.state !== 'loaded' && + this.texture.state === 'freed' && this.renderState > CoreNodeRenderState.OutOfBounds ) { this.stage.txManager.loadTexture(this.texture); diff --git a/src/core/CoreTextureManager.ts b/src/core/CoreTextureManager.ts index f0165b54..d50fa9d8 100644 --- a/src/core/CoreTextureManager.ts +++ b/src/core/CoreTextureManager.ts @@ -366,6 +366,13 @@ export class CoreTextureManager extends EventEmitter { * @param immediate - Whether to prioritize the texture for immediate loading */ loadTexture(texture: Texture, priority?: boolean): void { + if (texture.state === 'loaded' || texture.state === 'loading') { + return; + } + + texture.setSourceState('loading'); + texture.setCoreCtxState('loading'); + // prioritize the texture for immediate loading if (priority === true) { texture @@ -415,14 +422,9 @@ export class CoreTextureManager extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const texture = this.downloadTextureSourceQueue.shift()!; queueMicrotask(() => { - texture - .getTextureData() - .then(() => { - this.enqueueUploadTexture(texture); - }) - .catch((err) => { - console.error(err); - }); + texture.getTextureData().then(() => { + this.enqueueUploadTexture(texture); + }); }); itemsProcessed++; @@ -436,12 +438,27 @@ export class CoreTextureManager extends EventEmitter { ); } - private initTextureToCache(texture: Texture, cacheKey: string) { + /** + * Initialize a texture to the cache + * + * @param texture Texture to cache + * @param cacheKey Cache key for the texture + */ + initTextureToCache(texture: Texture, cacheKey: string) { const { keyCache, inverseKeyCache } = this; keyCache.set(cacheKey, texture); inverseKeyCache.set(texture, cacheKey); } + /** + * Get a texture from the cache + * + * @param cacheKey + */ + getTextureFromCache(cacheKey: string): Texture | undefined { + return this.keyCache.get(cacheKey); + } + /** * Remove a texture from the cache * @@ -457,4 +474,22 @@ export class CoreTextureManager extends EventEmitter { keyCache.delete(cacheKey); } } + + /** + * Resolve a parent texture from the cache or fallback to the provided texture. + * + * @param texture - The provided texture to resolve. + * @returns The cached or provided texture. + */ + resolveParentTexture(texture: ImageTexture): Texture { + if (!texture?.props) { + return texture; + } + + const cacheKey = ImageTexture.makeCacheKey(texture.props); + const cachedTexture = cacheKey + ? this.getTextureFromCache(cacheKey) + : undefined; + return cachedTexture ?? texture; + } } diff --git a/src/core/textures/ImageTexture.ts b/src/core/textures/ImageTexture.ts index 07382b62..84d563c7 100644 --- a/src/core/textures/ImageTexture.ts +++ b/src/core/textures/ImageTexture.ts @@ -119,7 +119,7 @@ export interface ImageTextureProps { * {@link ImageTextureProps.premultiplyAlpha} prop to `false`. */ export class ImageTexture extends Texture { - props: Required; + public props: Required; public override type: TextureType = TextureType.image; @@ -134,7 +134,7 @@ export class ImageTexture extends Texture { async loadImageFallback(src: string, hasAlpha: boolean) { const img = new Image(); - if (!(src.startsWith('data:'))) { + if (!src.startsWith('data:')) { img.crossOrigin = 'Anonymous'; } @@ -230,7 +230,15 @@ export class ImageTexture extends Texture { } override async getTextureSource(): Promise { - const resp = await this.determineImageType(); + let resp; + try { + resp = await this.determineImageTypeAndLoadImage(); + } catch (e) { + this.setSourceState('failed', e as Error); + return { + data: null, + }; + } if (resp.data === null) { this.setSourceState('failed', Error('ImageTexture: No image data')); @@ -261,7 +269,7 @@ export class ImageTexture extends Texture { }; } - determineImageType() { + determineImageTypeAndLoadImage() { const { src, premultiplyAlpha, type } = this.props; if (src === null) { return { diff --git a/src/core/textures/SubTexture.ts b/src/core/textures/SubTexture.ts index b3612095..6eb5aaf8 100644 --- a/src/core/textures/SubTexture.ts +++ b/src/core/textures/SubTexture.ts @@ -17,7 +17,9 @@ * limitations under the License. */ +import { assertTruthy } from '../../utils.js'; import type { CoreTextureManager } from '../CoreTextureManager.js'; +import { ImageTexture } from './ImageTexture.js'; import { Texture, TextureType, @@ -83,7 +85,21 @@ export class SubTexture extends Texture { constructor(txManager: CoreTextureManager, props: SubTextureProps) { super(txManager); this.props = SubTexture.resolveDefaults(props || {}); - this.parentTexture = this.props.texture; + + assertTruthy(this.props.texture, 'SubTexture requires a parent texture'); + assertTruthy( + this.props.texture instanceof ImageTexture, + 'SubTexture requires an ImageTexture parent', + ); + + // Resolve parent texture from cache or fallback to provided texture + this.parentTexture = txManager.resolveParentTexture(this.props.texture); + + if (this.parentTexture.state === 'freed') { + this.txManager.loadTexture(this.parentTexture); + } + + this.parentTexture.setRenderableOwner(this, true); // If parent texture is already loaded / failed, trigger loaded event manually // so that users get a consistent event experience. @@ -121,10 +137,6 @@ export class SubTexture extends Texture { override async getTextureSource(): Promise { // Check if parent texture is loaded - if (this.parentTexture.state !== 'loaded') { - await this.txManager.loadTexture(this.parentTexture); - } - return { data: this.props, }; diff --git a/src/core/textures/Texture.ts b/src/core/textures/Texture.ts index 0e332209..281cfabb 100644 --- a/src/core/textures/Texture.ts +++ b/src/core/textures/Texture.ts @@ -316,18 +316,21 @@ export abstract class Texture extends EventEmitter { let newState: TextureState = 'freed'; let payload: Error | Dimensions | null = null; + if (sourceState === 'failed' || ctxState === 'failed') { newState = 'failed'; - - // If the texture failed to load, the error is set by the source - payload = this.error; + payload = this.error; // Error set by the source } else if (sourceState === 'loading' || ctxState === 'loading') { newState = 'loading'; - } else if (this.sourceState === 'loaded' && ctxState === 'loaded') { + } else if (sourceState === 'loaded' && ctxState === 'loaded') { newState = 'loaded'; - - // If the texture is loaded, the dimensions are set by the source - payload = this.dimensions; + payload = this.dimensions; // Dimensions set by the source + } else if ( + (sourceState === 'loaded' && ctxState === 'freed') || + (ctxState === 'loaded' && sourceState === 'freed') + ) { + // If one is loaded and the other is freed, then we are in a loading state + newState = 'loading'; } else { newState = 'freed'; } From 32ea1484a45a6de15f86d0baf5e2feaaa2d8dc9c Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Mon, 16 Dec 2024 19:01:06 +0100 Subject: [PATCH 18/32] fix: Set core ctx state for parent texture if ctxTexture is defined --- src/core/textures/SubTexture.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/textures/SubTexture.ts b/src/core/textures/SubTexture.ts index 6eb5aaf8..c28ef98d 100644 --- a/src/core/textures/SubTexture.ts +++ b/src/core/textures/SubTexture.ts @@ -124,6 +124,14 @@ export class SubTexture extends Texture { width: this.props.width, height: this.props.height, }); + + // If the parent already has a ctxTexture, we can set the core ctx state + if (this.parentTexture.ctxTexture !== undefined) { + this.setCoreCtxState('loaded', { + width: this.props.width, + height: this.props.height, + }); + } }; private onParentTxFailed: TextureFailedEventHandler = (target, error) => { From 8158f2ee7d36760a0250cca9eb72f4293c172781 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Mon, 16 Dec 2024 19:19:29 +0100 Subject: [PATCH 19/32] fix: Simplify condition for setting default texture in CoreNode --- src/core/CoreNode.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 41cdb9d9..6cbcf0ff 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -814,8 +814,7 @@ export class CoreNode extends EventEmitter { this.stage.defaultTexture !== null && this.props.src === null && this.props.texture === null && - this.props.rtt === false && - this.hasRenderableProperties() === true + this.props.rtt === false ) { this.texture = this.stage.defaultTexture; } From aea2768d92cc5d95b76ad2710d82fc0ec5ea89d5 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Mon, 16 Dec 2024 19:22:28 +0100 Subject: [PATCH 20/32] fix: Avoid object assigning ctxTexture that just lives on the this --- src/core/textures/Texture.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/core/textures/Texture.ts b/src/core/textures/Texture.ts index 281cfabb..52a02cbd 100644 --- a/src/core/textures/Texture.ts +++ b/src/core/textures/Texture.ts @@ -232,14 +232,11 @@ export abstract class Texture extends EventEmitter { * @returns */ loadCtxTexture(): CoreContextTexture { - if (this.ctxTexture !== undefined) { - return this.ctxTexture; + if (this.ctxTexture === undefined) { + this.ctxTexture = this.txManager.renderer.createCtxTexture(this); } - const ctxTexture = this.txManager.renderer.createCtxTexture(this); - Object.defineProperty(this, 'ctxTexture', { value: ctxTexture }); - - return ctxTexture; + return this.ctxTexture; } /** From e60bda3d913eb16fa0ff9e2f65d5b040ddbf4fe0 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Tue, 17 Dec 2024 22:27:18 +0100 Subject: [PATCH 21/32] fix: Refactor CoreTextNode and CanvasTextRenderer for improved texture handling --- src/core/CoreTextNode.ts | 10 ++----- .../renderers/webgl/WebGlCoreCtxTexture.ts | 3 +- .../renderers/CanvasTextRenderer.ts | 29 +++++-------------- 3 files changed, 12 insertions(+), 30 deletions(-) diff --git a/src/core/CoreTextNode.ts b/src/core/CoreTextNode.ts index db6368a8..19410400 100644 --- a/src/core/CoreTextNode.ts +++ b/src/core/CoreTextNode.ts @@ -368,13 +368,6 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps { this.textRenderer.set.y(this.trState, this.globalTransform.ty); } - override hasRenderableProperties(): boolean { - if (this.trState && this.trState.props.text !== '') { - return true; - } - return super.hasRenderableProperties(); - } - override onChangeIsRenderable(isRenderable: boolean) { super.onChangeIsRenderable(isRenderable); this.textRenderer.setIsRenderable(this.trState, isRenderable); @@ -385,7 +378,7 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps { // If the text renderer does not support rendering quads, fallback to the // default renderQuads method - if (!this.textRenderer.renderQuads) { + if (this.textRenderer.type === 'canvas') { super.renderQuads(renderer); return; } @@ -412,6 +405,7 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps { } assertTruthy(this.globalTransform); + assertTruthy(this.textRenderer.renderQuads); this.textRenderer.renderQuads( this.trState, diff --git a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts index b1a54854..01195594 100644 --- a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts +++ b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts @@ -54,7 +54,6 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { get ctxTexture(): WebGLTexture | null { if (this.state === 'freed') { - this.load(); return null; } assertTruthy(this._nativeCtxTexture); @@ -84,6 +83,8 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { return; } + console.log('WebGlCoreCtxTexture.load'); + this.state = 'loading'; this.textureSource.setCoreCtxState('loading'); this._nativeCtxTexture = this.createNativeCtxTexture(); diff --git a/src/core/text-rendering/renderers/CanvasTextRenderer.ts b/src/core/text-rendering/renderers/CanvasTextRenderer.ts index 60bc9d9c..996b5742 100644 --- a/src/core/text-rendering/renderers/CanvasTextRenderer.ts +++ b/src/core/text-rendering/renderers/CanvasTextRenderer.ts @@ -26,7 +26,6 @@ import { getNormalizedRgbaComponents, getNormalizedAlphaComponent, } from '../../lib/utils.js'; -import type { ImageTexture } from '../../textures/ImageTexture.js'; import { TrFontManager, type FontFamilyMap } from '../TrFontManager.js'; import type { TrFontFace } from '../font-face-types/TrFontFace.js'; import { WebTrFontFace } from '../font-face-types/WebTrFontFace.js'; @@ -362,27 +361,15 @@ export class CanvasTextRenderer extends TextRenderer { }.bind(this, state.lightning2TextRenderer, state.renderInfo), }); - this.stage.txManager.loadTexture(texture); + // Create a new texture node + node.autosize = true; + node.texture = texture; + node.alpha = getNormalizedAlphaComponent(state.props.color); - if (state.textureNode) { - // Use the existing texture node - state.textureNode.texture = texture; - // Update the alpha - state.textureNode.alpha = getNormalizedAlphaComponent(state.props.color); - } else { - // Create a new texture node - const textureNode = this.stage.createNode({ - parent: node, - texture, - autosize: true, - // The alpha channel of the color is ignored when rasterizing the text - // texture so we need to pass it directly to the texture node. - alpha: getNormalizedAlphaComponent(state.props.color), - }); - state.textureNode = textureNode; - } - - this.setStatus(state, 'loaded'); + // once the texture is loaded, set the status to loaded + texture.once('loaded', () => { + this.setStatus(state, 'loaded'); + }); } loadFont = (state: CanvasTextRendererState): void => { From 4673e9047f0106b7d3f8f209f59685571cfb83f2 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Tue, 17 Dec 2024 22:28:13 +0100 Subject: [PATCH 22/32] fix: Implement priority queue when CoreTextreManager is still initializing --- src/core/CoreTextureManager.ts | 37 +++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/core/CoreTextureManager.ts b/src/core/CoreTextureManager.ts index d50fa9d8..31e6ba22 100644 --- a/src/core/CoreTextureManager.ts +++ b/src/core/CoreTextureManager.ts @@ -148,6 +148,7 @@ export class CoreTextureManager extends EventEmitter { txConstructors: Partial = {}; private downloadTextureSourceQueue: Array = []; + private priorityQueue: Array = []; private uploadTextureQueue: Array = []; private initialized = false; @@ -373,13 +374,18 @@ export class CoreTextureManager extends EventEmitter { texture.setSourceState('loading'); texture.setCoreCtxState('loading'); + // if we're not initialized, just queue the texture into the priority queue + if (this.initialized === false) { + this.priorityQueue.push(texture); + return; + } + // prioritize the texture for immediate loading if (priority === true) { texture .getTextureData() .then(() => { - const coreContext = texture.loadCtxTexture(); - coreContext.load(); + this.uploadTexture(texture); }) .catch((err) => { console.error(err); @@ -390,6 +396,16 @@ export class CoreTextureManager extends EventEmitter { this.enqueueDownloadTextureSource(texture); } + /** + * Upload a texture to the GPU + * + * @param texture Texture to upload + */ + uploadTexture(texture: Texture): void { + const coreContext = texture.loadCtxTexture(); + coreContext.load(); + } + /** * Process a limited number of downloads and uploads. * @@ -402,15 +418,26 @@ export class CoreTextureManager extends EventEmitter { let itemsProcessed = 0; + // Process priority queue + while ( + this.priorityQueue.length > 0 && + (maxItems === 0 || itemsProcessed < maxItems) + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const texture = this.priorityQueue.shift()!; + texture.getTextureData().then(() => { + this.uploadTexture(texture); + }); + itemsProcessed++; + } + // Process uploads while ( this.uploadTextureQueue.length > 0 && (maxItems === 0 || itemsProcessed < maxItems) ) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const texture = this.uploadTextureQueue.shift()!; - const coreContext = texture.loadCtxTexture(); - coreContext.load(); + this.uploadTexture(this.uploadTextureQueue.shift()!); itemsProcessed++; } From 18014a618ecf7c76d5c01d0f30e5526ba3536bfc Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Tue, 17 Dec 2024 22:28:41 +0100 Subject: [PATCH 23/32] test: Add automation tests for various ways of font rendering --- examples/tests/text-mixed.ts | 108 +++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 examples/tests/text-mixed.ts diff --git a/examples/tests/text-mixed.ts b/examples/tests/text-mixed.ts new file mode 100644 index 00000000..ceb2a506 --- /dev/null +++ b/examples/tests/text-mixed.ts @@ -0,0 +1,108 @@ +import type { INode, ITextNode } from '../../dist/exports/index.js'; +import type { ExampleSettings } from '../common/ExampleSettings.js'; + +export async function automation(settings: ExampleSettings) { + await test(settings); + await settings.snapshot(); +} + +/** + * Tests that Single-Channel Signed Distance Field (SSDF) fonts are rendered + * correctly. + * + * Text that is thinner than the certified snapshot may indicate that the + * SSDF font atlas texture was premultiplied before being uploaded to the GPU. + * + * @param settings + * @returns + */ +export default async function test(settings: ExampleSettings) { + const { renderer, testRoot } = settings; + + let ssdf: ITextNode | undefined, + canvas: ITextNode | undefined, + factory: INode | undefined; + + const textFactory = () => { + const canvas = document.createElement('canvas'); + canvas.width = 300; + canvas.height = 200; + const ctx = canvas.getContext('2d'); + if (!ctx) throw new Error('Unable to create canvas 2d context'); + ctx.fillStyle = 'red'; + ctx.font = '50px sans-serif'; + ctx.fillText('Factory', 0, 50); + return ctx.getImageData(0, 0, 300, 200); + }; + + const drawText = (x = 0) => { + // Set a smaller snapshot area + ssdf = renderer.createTextNode({ + x, + text: 'SSDF', + color: 0x00ff00ff, + fontFamily: 'Ubuntu-ssdf', + parent: testRoot, + fontSize: 80, + lineHeight: 80 * 1.2, + }); + + canvas = renderer.createTextNode({ + x, + color: 0xff0000ff, + y: 100, + text: `Canvas`, + parent: renderer.root, + fontSize: 50, + }); + + factory = renderer.createNode({ + x, + y: 150, + width: 300, + height: 200, + parent: testRoot, + texture: renderer.createTexture('ImageTexture', { + src: textFactory, + }), + }); + }; + + let offset = 0; + window.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + if (ssdf || canvas || factory) { + ssdf?.destroy(); + ssdf = undefined; + + canvas?.destroy(); + canvas = undefined; + + factory?.destroy(); + factory = undefined; + } + + setTimeout(() => { + drawText(); + }, 200); + } + + if (e.key === 'ArrowRight') { + offset += 10; + + if (ssdf) ssdf.x = offset; + if (canvas) canvas.x = offset; + if (factory) factory.x = offset; + } + + if (e.key === 'ArrowLeft') { + offset -= 10; + + if (ssdf) ssdf.x = offset; + if (canvas) canvas.x = offset; + if (factory) factory.x = offset; + } + }); + + drawText(); +} From a4f29ccf7c1776897b164eb0651fdfad07478636 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Tue, 17 Dec 2024 23:19:56 +0100 Subject: [PATCH 24/32] fix: Update premultiplyAlpha handling in WebGlCoreCtxTexture and related components --- src/core/renderers/webgl/WebGlCoreCtxTexture.ts | 15 +++++++-------- .../renderers/CanvasTextRenderer.ts | 1 + src/core/textures/ImageTexture.ts | 1 + 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts index 01195594..00da0fc8 100644 --- a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts +++ b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts @@ -158,10 +158,10 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { width = data.width; height = data.height; glw.bindTexture(this._nativeCtxTexture); - glw.pixelStorei( - glw.UNPACK_PREMULTIPLY_ALPHA_WEBGL, - !!textureData.premultiplyAlpha, - ); + + if (textureData.premultiplyAlpha === true) { + glw.pixelStorei(glw.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + } glw.texImage2D(0, glw.RGBA, glw.RGBA, glw.UNSIGNED_BYTE, data); this.setTextureMemUse(width * height * 4); @@ -215,10 +215,9 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { height = 1; glw.bindTexture(this._nativeCtxTexture); - glw.pixelStorei( - glw.UNPACK_PREMULTIPLY_ALPHA_WEBGL, - !!textureData.premultiplyAlpha, - ); + if (textureData.premultiplyAlpha === true) { + glw.pixelStorei(glw.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + } glw.texImage2D( 0, diff --git a/src/core/text-rendering/renderers/CanvasTextRenderer.ts b/src/core/text-rendering/renderers/CanvasTextRenderer.ts index 996b5742..e679c38c 100644 --- a/src/core/text-rendering/renderers/CanvasTextRenderer.ts +++ b/src/core/text-rendering/renderers/CanvasTextRenderer.ts @@ -338,6 +338,7 @@ export class CanvasTextRenderer extends TextRenderer { const node = state.node; const texture = this.stage.txManager.createTexture('ImageTexture', { + premultiplyAlpha: false, src: function ( this: CanvasTextRenderer, lightning2TextRenderer: LightningTextTextureRenderer, diff --git a/src/core/textures/ImageTexture.ts b/src/core/textures/ImageTexture.ts index 84d563c7..accfb51d 100644 --- a/src/core/textures/ImageTexture.ts +++ b/src/core/textures/ImageTexture.ts @@ -266,6 +266,7 @@ export class ImageTexture extends Texture { return { data: resp.data, + premultiplyAlpha: this.props.premultiplyAlpha ?? true, }; } From 895e76c4dcab474797a35f46469bf5ebc454d315 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Tue, 17 Dec 2024 23:21:41 +0100 Subject: [PATCH 25/32] fix: Update parent reference in text-mixed test example --- examples/tests/text-mixed.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tests/text-mixed.ts b/examples/tests/text-mixed.ts index ceb2a506..684efde1 100644 --- a/examples/tests/text-mixed.ts +++ b/examples/tests/text-mixed.ts @@ -52,7 +52,7 @@ export default async function test(settings: ExampleSettings) { color: 0xff0000ff, y: 100, text: `Canvas`, - parent: renderer.root, + parent: testRoot, fontSize: 50, }); From b2d5dbb0a519d8923e6c1452f34ca63d8bf5e7f2 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Tue, 17 Dec 2024 23:47:27 +0100 Subject: [PATCH 26/32] Revert "fix: Refactor CoreTextNode and CanvasTextRenderer for improved texture handling" This reverts commit e60bda3d913eb16fa0ff9e2f65d5b040ddbf4fe0. --- src/core/CoreTextNode.ts | 10 +++++-- .../renderers/webgl/WebGlCoreCtxTexture.ts | 3 +- .../renderers/CanvasTextRenderer.ts | 29 ++++++++++++++----- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/core/CoreTextNode.ts b/src/core/CoreTextNode.ts index 19410400..db6368a8 100644 --- a/src/core/CoreTextNode.ts +++ b/src/core/CoreTextNode.ts @@ -368,6 +368,13 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps { this.textRenderer.set.y(this.trState, this.globalTransform.ty); } + override hasRenderableProperties(): boolean { + if (this.trState && this.trState.props.text !== '') { + return true; + } + return super.hasRenderableProperties(); + } + override onChangeIsRenderable(isRenderable: boolean) { super.onChangeIsRenderable(isRenderable); this.textRenderer.setIsRenderable(this.trState, isRenderable); @@ -378,7 +385,7 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps { // If the text renderer does not support rendering quads, fallback to the // default renderQuads method - if (this.textRenderer.type === 'canvas') { + if (!this.textRenderer.renderQuads) { super.renderQuads(renderer); return; } @@ -405,7 +412,6 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps { } assertTruthy(this.globalTransform); - assertTruthy(this.textRenderer.renderQuads); this.textRenderer.renderQuads( this.trState, diff --git a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts index 00da0fc8..080b659f 100644 --- a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts +++ b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts @@ -54,6 +54,7 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { get ctxTexture(): WebGLTexture | null { if (this.state === 'freed') { + this.load(); return null; } assertTruthy(this._nativeCtxTexture); @@ -83,8 +84,6 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { return; } - console.log('WebGlCoreCtxTexture.load'); - this.state = 'loading'; this.textureSource.setCoreCtxState('loading'); this._nativeCtxTexture = this.createNativeCtxTexture(); diff --git a/src/core/text-rendering/renderers/CanvasTextRenderer.ts b/src/core/text-rendering/renderers/CanvasTextRenderer.ts index e679c38c..be3fb7d2 100644 --- a/src/core/text-rendering/renderers/CanvasTextRenderer.ts +++ b/src/core/text-rendering/renderers/CanvasTextRenderer.ts @@ -26,6 +26,7 @@ import { getNormalizedRgbaComponents, getNormalizedAlphaComponent, } from '../../lib/utils.js'; +import type { ImageTexture } from '../../textures/ImageTexture.js'; import { TrFontManager, type FontFamilyMap } from '../TrFontManager.js'; import type { TrFontFace } from '../font-face-types/TrFontFace.js'; import { WebTrFontFace } from '../font-face-types/WebTrFontFace.js'; @@ -362,15 +363,27 @@ export class CanvasTextRenderer extends TextRenderer { }.bind(this, state.lightning2TextRenderer, state.renderInfo), }); - // Create a new texture node - node.autosize = true; - node.texture = texture; - node.alpha = getNormalizedAlphaComponent(state.props.color); + this.stage.txManager.loadTexture(texture); - // once the texture is loaded, set the status to loaded - texture.once('loaded', () => { - this.setStatus(state, 'loaded'); - }); + if (state.textureNode) { + // Use the existing texture node + state.textureNode.texture = texture; + // Update the alpha + state.textureNode.alpha = getNormalizedAlphaComponent(state.props.color); + } else { + // Create a new texture node + const textureNode = this.stage.createNode({ + parent: node, + texture, + autosize: true, + // The alpha channel of the color is ignored when rasterizing the text + // texture so we need to pass it directly to the texture node. + alpha: getNormalizedAlphaComponent(state.props.color), + }); + state.textureNode = textureNode; + } + + this.setStatus(state, 'loaded'); } loadFont = (state: CanvasTextRendererState): void => { From 87554db549391dd04c2a7883149d66f0cb4e27e5 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Wed, 18 Dec 2024 16:24:37 +0100 Subject: [PATCH 27/32] fix: Refactor createImageWorker to utilize options for createImageBitmap support --- src/core/lib/ImageWorker.ts | 72 +++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/src/core/lib/ImageWorker.ts b/src/core/lib/ImageWorker.ts index e3251591..85d7d7f3 100644 --- a/src/core/lib/ImageWorker.ts +++ b/src/core/lib/ImageWorker.ts @@ -48,9 +48,6 @@ interface ImageWorkerMessage { /* eslint-disable */ function createImageWorker() { - var supportsOptionsCreateImageBitmap = false; - var supportsFullCreateImageBitmap = false; - function hasAlphaChannel(mimeType: string) { return mimeType.indexOf('image/png') !== -1; } @@ -62,8 +59,15 @@ function createImageWorker() { y: number | null, width: number | null, height: number | null, + options: { + supportsOptionsCreateImageBitmap: boolean; + supportsFullCreateImageBitmap: boolean; + }, ): Promise { return new Promise(function (resolve, reject) { + var supportsOptionsCreateImageBitmap = + options.supportsOptionsCreateImageBitmap; + var supportsFullCreateImageBitmap = options.supportsFullCreateImageBitmap; var xhr = new XMLHttpRequest(); xhr.open('GET', src, true); xhr.responseType = 'blob'; @@ -97,10 +101,7 @@ function createImageWorker() { reject(error); }); return; - } - - // createImageBitmap without crop but with options - if (supportsOptionsCreateImageBitmap === true) { + } else if (supportsOptionsCreateImageBitmap === true) { createImageBitmap(blob, { premultiplyAlpha: withAlphaChannel ? 'premultiply' : 'none', colorSpaceConversion: 'none', @@ -112,18 +113,17 @@ function createImageWorker() { .catch(function (error) { reject(error); }); - return; + } else { + // Fallback for browsers that do not support createImageBitmap with options + // this is supported for Chrome v50 to v52/54 that doesn't support options + createImageBitmap(blob) + .then(function (data) { + resolve({ data, premultiplyAlpha: premultiplyAlpha }); + }) + .catch(function (error) { + reject(error); + }); } - - // Fallback for browsers that do not support createImageBitmap with options - // this is supported for Chrome v50 to v52/54 that doesn't support options - createImageBitmap(blob) - .then(function (data) { - resolve({ data, premultiplyAlpha: premultiplyAlpha }); - }) - .catch(function (error) { - reject(error); - }); }; xhr.onerror = function () { @@ -145,12 +145,18 @@ function createImageWorker() { var width = event.data.sw; var height = event.data.sh; - getImage(src, premultiplyAlpha, x, y, width, height) + // these will be set to true if the browser supports the createImageBitmap options or full + var supportsOptionsCreateImageBitmap = false; + var supportsFullCreateImageBitmap = false; + + getImage(src, premultiplyAlpha, x, y, width, height, { + supportsOptionsCreateImageBitmap, + supportsFullCreateImageBitmap, + }) .then(function (data) { self.postMessage({ id: id, src: src, data: data }); }) .catch(function (error) { - console.error('Error loading image:', error); self.postMessage({ id: id, src: src, error: error.message }); }); }; @@ -198,18 +204,22 @@ export class ImageWorkerManager { let workerCode = `(${createImageWorker.toString()})()`; // Replace placeholders with actual initialization values - const supportsOptions = createImageBitmapSupport.options ? 'true' : 'false'; - const supportsFull = createImageBitmapSupport.full ? 'true' : 'false'; - workerCode = workerCode.replace( - 'var supportsOptionsCreateImageBitmap = false;', - `var supportsOptionsCreateImageBitmap = ${supportsOptions};`, - ); - workerCode = workerCode.replace( - 'var supportsFullCreateImageBitmap = false;', - `var supportsFullCreateImageBitmap = ${supportsFull};`, - ); + if (createImageBitmapSupport.options) { + workerCode = workerCode.replace( + 'var supportsOptionsCreateImageBitmap = false;', + 'var supportsOptionsCreateImageBitmap = true;', + ); + } + + if (createImageBitmapSupport.full) { + workerCode = workerCode.replace( + 'var supportsFullCreateImageBitmap = false;', + 'var supportsFullCreateImageBitmap = true;', + ); + } - const blob: Blob = new Blob([workerCode.replace('"use strict";', '')], { + workerCode = workerCode.replace('"use strict";', ''); + const blob: Blob = new Blob([workerCode], { type: 'application/javascript', }); const blobURL: string = (self.URL ? URL : webkitURL).createObjectURL(blob); From fac109e8ed1c34146c3c929caafa51476b6677f7 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Wed, 18 Dec 2024 16:30:31 +0100 Subject: [PATCH 28/32] chore: update CI snapshots SVG changed due to Alpha fix. --- .../chromium-ci/text-mixed-1.png | Bin 0 -> 15333 bytes .../chromium-ci/texture-spritemap-1.png | Bin 0 -> 33244 bytes .../chromium-ci/texture-svg-1.png | Bin 86583 -> 78417 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 visual-regression/certified-snapshots/chromium-ci/text-mixed-1.png create mode 100644 visual-regression/certified-snapshots/chromium-ci/texture-spritemap-1.png diff --git a/visual-regression/certified-snapshots/chromium-ci/text-mixed-1.png b/visual-regression/certified-snapshots/chromium-ci/text-mixed-1.png new file mode 100644 index 0000000000000000000000000000000000000000..bd4b1f066a4bea9b10fc48dd2c2da87ff48c5452 GIT binary patch literal 15333 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 literal 0 HcmV?d00001 diff --git a/visual-regression/certified-snapshots/chromium-ci/texture-spritemap-1.png b/visual-regression/certified-snapshots/chromium-ci/texture-spritemap-1.png new file mode 100644 index 0000000000000000000000000000000000000000..65403ec494306cfcdd439c1183d38d98c6cc9a44 GIT binary patch literal 33244 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 literal 0 HcmV?d00001 diff --git a/visual-regression/certified-snapshots/chromium-ci/texture-svg-1.png b/visual-regression/certified-snapshots/chromium-ci/texture-svg-1.png index da29d34e9d173d7bee7e859bba5c9b3fb3b4ca46..1870fd886bd190ed88e62b686e4744fdea01e0a1 100644 GIT binary patch literal 78417 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

it~bAxpYM;I#Z`TQF_u~LqV=V|jy@-Z=y8P6 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 VT8Q;g#J^`fQ+%$FFK71de*lAukTC!N literal 86583 zcma(3bx>SSus#kS+$BJe;6W4I9fAc276QS2(ctc|!JPoXodgda7I$}Nao6DP@Z;XP z@cF*~y{AsqE>&lzdwRN`etLQiA)ghbFi_v10sw#^BQ5a-01%$O!j2##K79;(EvP+ih#YSeM zX%R~WG?0y4C@oPH-^+0KR-_ymB^b@@InYoBn1Ij;%ghfJVrYtn%swXTDu`@B54NH> zl1KB^D;OhYx14kw%s15zZ}T37p_fg~Q@~W|GT1byS-{-8?Bh% zLEECxaRZP=1i3;Hv?{p=&5^){OeS@BY3r{-o`0O0jEM@^Gz<2Vs&U%w{KFf9IsbkS zf>AukKW;fSPG9(nw0!AZ?r?A?LAl!O<8kvEL;vyC=SVDo+^Ue|q^=+OyXO9c5n7@+ zKDo!pt9Fi&67)|HQrzW|!Neg*N_O1HlbW#^FI!P+_6O-EtyHd|@K&!lGPV7mbrhY; zgu*{wiT}ISC%6azO?=OXMxBP0wgCec-O2s(!nw75=%(xV7UDZ7h-_Yc<@9hLR{v1t zDipSbONgvmcNTdCHSX%icTJDj_PoWhu0a;usT^GVgi;y1>tsCg+NwCctJ6mGa3m?u z$0ir@FeDx_{(Nn8*;(zTyK>#xWRj*wPf3`}XKRp5X#-bthuiAd5BEm_sKXafdQ}ik zNoNISl&mUE?QfRed&LBLCw{Fj@Rap;DvN>IlBQ14gR&lS1Q+2no#TUm-60$i}p~539{dME_2p)6uRJsoA zRa|^hP14T>8FFXle1E*?DLFphs`t1A60E3l?;%!oa&tVWhUeiv^4sd4w=-UQAm%ZO zw0JNy~skt){m`+ zi(icnFw0WSoPFs9${g@`&;G-QL%duuSx^giDXhV7d4)70LPqj;{p@ylILO|nVf#on z__EsD+}9z27Th(qLI+{--0Zfo0a=wIwWtfP{(7`HGj10F2XTL`IV>1cYSeq4&${KZ z7>Hzi~g+?2f?PsE=66>YtuB#Ek>E66dRkZw%5G~-D*y5+EvV* z%|0NsjK>>1J)b|WBaO9x2e+2yI^!&qk+lS=yo!TXztG3=eHA>;i-R(D_sf>@uLy+2|!$b0c)6T;oM&8FT9?sKVfmz_{C`A(= z9xTLkFkkIzojFwcFo=9iDK_qfA%khWmO1Qm-F>Vkfbrs;;)7NXm$g1g>Lc@O*>N%t zM+-Xhrz!n4x0|Z&nwaEb`=@E9o9zuLnB|O489X|I%^jC5Kv{7w<&K7f&mmvHB<2|iN?mfjJ6Z_3Jk<5H zTk-@FS%!!23!UEKlmt9$&~xngS?tosM`alzLb&Si$j+u#3mKj(={;7=(C)Jsk|%d1 z2F_b-AHBbVtF>c#So70u)x!0uHeSxxzVGXC%^z89 zhK*Wg$qs*rh%2AI_UxbCVtD5gf#7g^TfB38CDIJ>pJNudN_oi{Bt`8dyN!=K?3W@s zCK~nqPx1%3uBM??7ma#^CYKVnBVLz68mmJ^$w1Bi38S8uV-v9zzk%JZB|R@I*-_7w zU;XVS!zoH8NZew|B$tHTaiJiy-neP(h+L%hEdPM7?EOhp6L)V$Q)w8D6|;qhknm-R zg0P(w@skAyc*s~rMVya#@Lya)6RC7ms3pDNM{}J|GM1N}5p%GtJAU(7;HJ3lzR!aD~+IfYqQM^k*c4=4aN7a=wF|scj$}M!5*8%+5!I)(&9DysL{0; zB*4IV?iQTPv(L}544;fGcCka}m^1Vi-4A1fmN*UrzTarO%ojm(@Hy%D;#B^z$qZA5 zt~@QL;O3T!SNoEuOWg}q~Sjpx6|aMDtPu>W(09*y(0_G=3#2*7~gh%s_`g5!n?TvA5YAehS?wTBe9F~-GYK>dc1~JKRU93ut z62mrU9&Y9jZ(ZPp$qqaUJM`uSxGpcR`?nIr)b6hj7h{xQ>7Ri)$P|v#)bh(>bpPIB zonCOY*EF4Xk|D%KP?-E=!R;%qO_~*Yg`G=Uo(q|q%;A!^kIBL1Wn)v)lTX`sc;L^u zWTY*1ABpo+F>a%J3gTfZGpYaSa=2yFok*aLRYCqwjtShnqf)%h2Q_9!VTvj zl1kkFxchc-vFF|3X`JgNX>Lfj@DmpjnN@*#)9S*?KCotWTQ=M*C@9j!vbfNWWNm5SRL#S)^n{YW8z{oI zAhZM$Z0uZI{O0pW%o42(TDK2?$HNKyp{Km{gANs1J zfaYDNxj2&y7lb~qzj+@*%|;CR%%?zB`YX_eqBgLayR)AGELF%7?2*oRJWvwQZ;x9dsr=30ibpB@ z>IqJ-i+xgg3xh=Js`_$UuUmGDqVV-L|B2s*dE{NKCv>|S-nJdja0j73-gNu=MW&f$ z+y~h`tS-L$nA?ufeTnsWH&V6<{?v(^vQcqR4)6U@E@{5= zBq1*0qZt6@q)?Y-UaTI`ms@cF>A0j#CAoJ6?j5bz;sQqQKeEVrXX(?R( zS{r>`)j(f1nI*KTO%!pnCR!rd+w@B!5zUdd(c(x4fi&;2gqABB>4wR8dtILtSPY_CK?=ibVB@yXN>7j6S_8Q6eY=vucn70Fu-fHCJde`ve zhaNHY^*#e0^Q9GSWFpbyFI6GmM%r(hmZLcl`eo)uOgITUIc)f=bADyNIU>(1T}`M? z*cLCNg^h`Z2SnJXFEzLybHqzOq6A&HVXg9gmZa+!R6eVy^1vz$g)CE zA~cxcu8rg-8P4H`q+M9zK+DZP<(XOct!j=4Tq{@1oXtBxw?5Ux8Tft+I1aH}^~%yy z<}t`NF3+WV@$FCiXUVg;`o=Di^>Z1&>s+XS`28Q&L`MJGpbo9Xk5r0}7kS4qWFac3 z0Ftb2lGh5r5+&EGmqTm$K! z*0iHdF?}OwKx+Npu=pVbODG%#DqLWY)H}6rf3#~;H{(encMVsAvj$U$<~jNtb3P4s zH?=&TZrGv5^%=^cfn6G#qqM#7Sfc6B3;mTEmNdfV3uXDLNriJmC7#_x!zfG@rp6qq z+a!QnVQKWUpg_iQ&cw=txK|q&VyslU9#TZyYcl3&tZbx=B8P|59hj*Lvi}zNrAndA z$^2X2Dh&>BHTm9-FuE^S<1*RB?oy9Mw`Z#UZ%DmQv7c3x;HRw^>*R_-34_4s9ingE ztR8V$(~sy_R)>^n&IPLNlEg0@z9-um=HcQD!Se`Bk2-D$r<)CQEeI4t#D|=>tp>{> zMR6C}h#>Th9rM7T%P|qo0hKIdL~UrR6DhDtpUuuC!xf%r`EARBih;n2d11la+Dkkw z0!v5&`|39i3dtO8w6y*llF!s1o>rFoJmWBK;b$AXQeOwKm$@>C^!M(fMw&KJFXR+B zc-eZVT19o|>mt6DOK&6j8%W@FUU6lF6bM#nM$m@`%gZ^?h8WL92*K0Q9ql@yPQH)J z&DndG7bOT@)1RdAy6an5_(@xBpuyc=Ct#-?L?4=eD{MfBE!Ap?0D6k>pP_Ci!@>~y zHQZ-4W=u!pfy2Y^qt;XBp25UWvB%A3qi>$R{s}D(LU(mhdY9#lM{qs|Wc?878q66L z99+qtd9*0IPVaNraXZ=TabfNQI%h5q_gz{`g2-tiF@#8qIgQ6Ao*tNdKOR8TMxR^{ zQ9th`2z`PW{y127x9*@@Gm9YJz!u-fThm$M&(Be=5FU#IlxU2GNhOg-5Nz++V#0q6~*H935RkbFYSukbfm12#&29^j!Nj)!mo?&r_OiZ_rnkR@6hl5 zmFjkTU8*Iem%rOUqw~ak!G#O;Gr-mj(>rIEZO>CgNgEJp`¬Fl%RWITW^*$oh@5 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

p>m1} z*xhW`N#;-6!4#-e$?j7l&0lp~U#7aw2ZH_J^T+-RY>NuNc$YjY2bVmdKcL?AxnN4& zy_YmhgyK2JM&GBh!RULr2iKW%HtR4d;jz_t@EG9X1Xix-k>JJ`5%zBiEX_iKe?9-; z6F zp=)$mj7rZ)Xf#EH3m{g#|IP8!P}-!@=odCm{%{)42R`<_cPfbk1SVK-1m_IDYGDpT z%s%DWl5QCJ#0e`xSB-;v3ENI*()MC2sg=Jd zfIz&n!$Z!yuIfkhX4MX7QhTd~za`B-gC5Mm6ldSj5btI?toFDpIzx&Oov`b?E=++zJY-THXn0bt|~Iz z9;?yYvyc!JUm%>4akD(4wStL}Q4recM4Ul|42v27i-_!*VKoO)O6*l;T4$@&@m7XN zHfd6EBC#A>?Q6GQdVkhP#d_h3C+rF-0Bcg;4{PgO_E@^fd5O2Nuq}k`%d#RJNf?J^ zZzw4~1WPhOS{1OdvAgZ4?$A6@m6g>Y5mr3TXr2pk2{FflCMG7+f6Z|_4ed&@hY7_$ zml|B4-S!n-Tw8%(X9(SCzjv_oIH7ex6+ZR$VN-{p!E<32S*FP1Z1Ew z<-5yh@u1F0JSM5+e;6cDnda{I)Hz0fqQG~^{mz85LOtoNqW`?TS~()vp7xG?HTUzo zX2z;QGv-C?1#@5E^`tOu+oZO8xC_SFlV-Icqxp6JG4fiXJ0UK489cPjcLrcd#{evv zq7WoRk=5bP-`M5&iwKKxa(y>8gdCD(;|C%I_*99l^ji+yX3Fk(pLR&N>nPKjlKht$;~ZhZj_j>N`(nW zPmrnE#(g~Ru*#d8oI&{q%BR^s)cpNWXj_@V;_*NJ%9W}LL(wwb1w;050&wJ@?fk39 z8LrYOLv~%l(Fd_28iF=24$!`F`F=`@7NQDQz(4Z)!HZrnH1L@vZj9DeaXK5N-iu<;iYfbmTp=6SRSZxq?II*^ihKwx_IDwe(#fBamkl$`WiyGNNR z;aEE;a*E^vhSWcbnmf>)P&q$fU_#6gG+uszNOniGM>_8@zeW43=gGIU3z%hLkvbow zlHKJ2Rp8Bd{sY<_*u6laQ6-n3Lww9($J&N=rKv)p@v8`}UPOK%B1nTe_MPr&s|aX! zqDoFah|ciS^KfrVlm8de_KG$tO&EakOx4m?6Ga_L!E3d@$KPqjpJQJK9qbVgb77e8 zp%9)vR=O;$*!|p^Z8=PuX<{({TKuinh1jq4%#gLyQH%lwg_81zm~>CVy1zJ%Ju+V} z$~*t{-?1P1WU@#x8uQBwOl?v*x;&$_M6AOEJaC%&p@)^?taLZ8@O^eG1$z4jPLe>%YuiU5rtN>9 zfmC1I;Tu9<3<2!JSv0137`<}DW@tbc=`dU%%%}F?wdV+FZKcDB3IZsKs zU|K$7fgi?n0OUZi5j=$C!tBy|0$&Hp8!?=x*AtJ17|wwcAk_?$0_caTh|k}?Ig>#5 zHS(jt)T8@+LJr55iUi($cuq;911=MsFeolnFo2`D6k|I0Pa;fR(<0#bHOOL!zo+~v zAJmgYD3O8-Q1H*Q2JR(jUw>%b7%@V8e8w?)0TM1E@bPDcJ-|rgBQD2+l#QTTBD`eVZeQ^eNZ$VNJeVoL-&BWWw3^sb1E>L3pCxqP$seHkRw$O||+ zA%1T=AVYd@@rWW#{|1lmyR9P2li?XR#W)X$q3WNg0l;=zq9~~cP}_MIELr(r_hN|V z#6OW{UkBcO{Nsei`8ALNdBn+Kvgv~ZgFfMi3dV)TmYC9+T|^$u4FGVzEG)`UXfj-Zy#l^ z`6&R`M9F+Fnn%Z05rMb#Rz8*)i~Q6F0<%9rRXV;Rl;`pz7y3%i2L-1JWU+B^3GWGM zoQiXyvU6EIM{hROdj<}m|C6AZ`wEQNU<9irH?p$;dOc~`3?(w`Nf7x^6=FuEOJh7>YM_&SdSV7y^w(9R~58y ztRf=hJl=obm@OdyBleriy>$Geqw{|Hj08fW#5l*jQoCm*3-sswD^Aj7VQ-Bzq^=9Y z*txu3ENUM86b){=2qof*qWJIt*y~|qdnTp!MEln*g+z+KU;ETzeRLqDI93N2nbUm@LS@ zzBIpUKZoUQwF9vu@-~O#wlCN>$`dDzw6z>537j43iAR8fjoy-5K(BjkK+pak@;N#t zK8>I!1i?k{(DMf>*VVqm%&=zO+_@f`Gp#|#sa}0R5e37@jF*@8B4yI8L0_6d$(S6p zz;>1SRac*%`YO8k@TpjWu~|89=06j2>rqR1Ee>+vXZyZ1wI%>$vygwc{tw%>x7k3S zz&5wjf{eWpV%Rj&%yfITeAs*3(~cOvg&*kC;8NzIk9SLS%L$W75P++~$o{LVm$(== z?}7*JnQX)kP$)(hQcjw(c5UJLM^7J^NoG|uj(73bB`8pyVoKLrv#a~%IC`=IM=rd2 zuwzzHElRdQ3qe@xigW8{mF+-GC3%YK;@2$|;d(sfrv^V==gm;0%FZLwz$$6S|IaEV zJe3G|UXsB9;>a#eS&dDY5sh9w{r!@XPOs71!7QUeB-Lty!$1FUJS=yDe7geegE?t zGn*wurHU-GArjt3-CeJz@F;kLE(%Z>pCvcSrzq=k!2S4tWV{7x{uH7gr1Jw{qw4m! zgcNr$5DZIU!YYxV{4*%jx0MF_{DI{;NV0$RhMU3KcluK7rTLX2C`o9P*Dhu-hq4MX zZ~xFYvaN$O(WdU0i(5Qew6p4$p&>szEU9s>5U}kapl{*9lT)V#b#!g8R$)#KOU%w} z%E9oZ_|T(|P$nPImL;iVX148zyudn*kock||F|x!_P@xiF=DC_c)#bf0P=3bBV5y@ zn7pZ&9k}d3W=TB(7wG~~{lRe_^LI4FPkV1Sr@@_=Fy3>2MP5!JXNM(hx0rD|W%scAe4Jo$$ax&ioF^mty7fhu#j* z3&bSjjFSPD{L>L)t@DuqHzN!4@IX&m92O8vy9xjaP?R8Xsnx;yqkuJq&fH0!W|gMewx7QULy;9jj__TP2o)xA03CDL>6}li&s$i<9cr&Ucc1Kawy}CA@(&HFU0d zfM!0_)>H^5OQGYY4jT;tJL-b+RS3naFtNA|8TsY=JDasXh8(cly||+bmQ6M;yc7)Y z@gxxd-UUoY&gzvGr25bN^*E!KO(8CaH>EN&Ur2xPz6_r{Y?FcibrC@7Z*u*BYHM(#x; zKz%Ebg7!ljn;Huznb@fcW?I|Lb~C>H;G|{cT--51tjvwh$>wF6y_P_kz5%oD!w$`l z_Ou@$(fnh=pAsa6Z$V3VipwpzYPM4wy8sXPid1VwaoCE32ogk!EIW{x8M z-dHMkle|#8|Kt5h5CQ=6As*Mv#tKXgY#8U1%}_f!|E0o1f3ghas|dq>wBfZoMXxu2 zZT*0Wn68LJ*I_-?BK3Qvo2O4FyU`21Ao>akt{m+lFdvs+z7X zqS%yEPjaBt!MzZWDTKrK=ayWb5b>ug`X@oeOt@(0%%{NJF1ZCZ2=17vUUFl$k5JpJ z)^7ObR#Wh%m7;t<{E(g|f;5gcGm=;|Vcl)^2nHN0II&Pn)rpXmqKIGH8cRYHT=FiM z*2po?Ft#q2J~Z(OOawEqbgR*XtI@3U;FzE8AX4Uf3``kcotWhx64Kc~3eXV;ujyW& zN!V4!CgAs~!IqE+OvSewNKUEg>eSUu^M5Fm?St{Z>U1&`j=CJV{XK9aB|X2TL_u8t zjJE1U8Z6~bDfR2~I=rKN%E2|+IlL&ieg8XDFE}+OfcF{=Yk4H=$Wih6Mc?3xpJf+Q zbp|_@wGZ0)A-X*)Wc1}_RbcOCN_@nSw8z#3Q9ByWf6ZCtQuDUmZnWCxjLe zW94;6X!}swhlWP0u@hX!$$qXmqOJ*3Y>9NKZjDeLH4KJ50&~Owc$+ z{nOolXJ6xGdwsHU_PT9{!B?P}q@)4WRo3D7pz9`l9_iTyM~#dGf&cmE7)iJc1>fsI zt&WO*|BJpAiM$KAS8C(67v%ZP>-EzfYh0i0`SYyEyiS)S`uUna407@OvTN5F=f--Q z+|V)>G3_-Tgk3Nzyr7)*Zp^zcb^*fOI3>AtBw3fJjM8H%Lo&!*_h|2k-we#4#M6d(PQ=uRYgX zbFH7f+EQQcdsPo_24&DH`mRXd)n*jEHlK_LKKaKGe!i_aARVndGzUa>Gd~-I;JtIlIr!}x7`I!gN9b;A z*jXx?$gzLJhW+i5%l5cG$!F?aX^%kHyR1K-yaxG*lloVPvrt6ICrYEGl|J%gG0*sl zdvd5M01Z@VV#*LPALckzNgD(KE7?VBeNJP$gdh^uYsgBIIv{egyx>QrD741cvtYM; zBUo5}HB`Sw`Dv{QiKZ0Y=MSth55?tbP<-iBmX7PJr_ZPYO-W+nLqwL%(w5YDG6Xo4 zf;9CrE-iSF2^7oHa_vSr$5qZZVPx{{WtxtCHWOY|*x%zrazT2XKI;BYIJBWZli*cN z-f(HL!M}?Qw0>Ba;Wx(pdT}lfz-E(Fh=G))OM)ivaNIkgOgr_v#Z-o*{^R_XV~igr zJH9Cs{%XQ5F3Lvs5?-vw=}snaujVEQ@|Bvllm*2}c{4i&lEV=Jf zf4K{-@edq>u^C^u^rm5yTkPSCl4UZ{h3nc}HdTqOlU(qaR3J?Jouw?g*mF{Qixx6xThI$7;p2kM||4lz=~XOl(@LN1G!wFb`xlLqb% z>beAf4gQ+a_CAcaMl?Y5tha>wXOGFhnnr6a;flyO@F9O~@fqbO2I}2M*4fk}aBpaS z{hh;_4gTiM)gJYl5LmvRdz9~a-6;7{i({Hy*!4-c0WrL;59NlUU~^#XC`H{0O;y@~ z8zA!KEggpEcCW^5Z&#j6jS7`k4PkR{Iud|qRgbe9?w3v4h$O#$1Mi*ln9d%3j!d%M z{|*WuH=u{?U2%N0crDMDdn#6&X}2Ssao#ZOKETR$7DXP;t7kU!68Viy;*bDF zfY)8(2Jc1beie&EN2hjV%aKpaO&H-VAvokGK!)*Lsp4g)=_}peZ~4=WH*>d!D|X+o zQ6uco-5Ki;r~iw7J<3qnTp_vG6@!-yL+#O?c0lXzo><;rEr@T{n>N#4svlM{$zFLj z~+# zPV|_?i`xod&(-*`fDim!d%=uik?ytHpGTnw# z4B-h|d^Dud(WJebZ3x`ayS~pfeVjEC?{%USxbo<(-@#HtN|_rQsqr5js*rngmVzC@ zMctuQ=2c}RX15?w_x>@b6Ogs-*XV(L=#>6qO$SX$tjM1Ax8KQ{sZ|6N_~J|^U=^}h zDC{Xv3vY1l5yrLt6_1e*Hq_)qv0nXhX!`~oall7VhxfF;@|2R(G-OKSAr^SQCM@V&p%j3(cKGZ_kwoYRyj z`gaXUyt5YAx+v5b;Q|E~|D*{2-isNizN><&*W{DOvbv+!*tXUAs+VhC6D|of(Mhm*Lj{4bX`KUGdZO&FZ9O& z+&$G$*hf#5X(N-Ll}wYKU#oYG1`G0tfPbn-vmTX0VLmLG&PQW^;k9QbUF5dpD9=vs z^RK2`vZ|C53nFfpNg@6A5Bcu9Tit@rKLgiouGWGS-RG!wS-mz*RE6$Nl-7qF|9F`3 zUB#OdraCzckiUt907(N^kF*9a(b1G1*9^9p3Ko#xv7|KPQ>BqJ&xCZMwnQv`pBUL? zb=jK!&`&kucs-*D^cA(K3456TSbk}@&^G_LZ&AMI#e~5?wy@OHLgO|9XSy3tma&_E zss(0j#$kPFHu%!!UGok{#(}EVdkmS3cJ~l$cU6!aG7o_R^svDBi;>OlbtU{&d zj)hL>v4B#8rgnQM502%r`5D7EmQj7r7q0389KX+dJ&*PSMTb2m^y5!&@LP!bF5e5E zc#i5H&fV6#W5*8O%mhyGlk!PJ;>1P8etz>vuXp^Bj?~JOmwL9Z0EiLQDA>JHbh@Y= z^4Pu$bH2?Io#~$r zcEU;Sg~xM6#}_1T&PYkGH<&l{Zis-4KZ)8S+)l$O*xStqB;B_JlkE~39|rA=!Udzf z`+gaov5BP_|I0x@`=%&6U;V&C{;ysF_2+~no{T$aTz9pY81L_azE$Gr(oNaNOIe_6 z8e?!ixS2=Kw}gwz)QY#Ue=#M4e{qA+ud?=ej(ySUwtm)!_U(Q%gSbHhs3;|?bBfS^CMhAg1zRIX81S5=ZLZb&ldcG3IkQ2h8R_Ds$IA<>*vp+Z{ zNJleL0(t}`x7A|v!G*g`g*^B>oz$a6b6u#U{@0VV+Kc^)Fs<>MXT~pdBZY9`t=J}? zACnA}wDqyM<--rm%WSzJvP}pH?gtI#JaM{nnE3m7UArZ7-K6M~bt`__)s&i%J+8)Y zr$~<5OHZA(;=hma4We*gkf;n2%bM*rjyt~I38Erfx6<-6^1OcbQ~%qe==wgGe)<NUfvRM+&AhQoR+QUDlV)Bet~9{i{<`Hu1oR{9$>_w3U3c zai-D$5I7BD;Ka?KU3a z%nCYKvllxNKws;x-&c3<5bl7=PtG7ow?ZI)p>R&8sz6~JS~GHM#ue55!j_+4;VeGJ z;XA2E4#<;S`|eCl#vQjoqC>2h9eG;H-x8BY54!* z5#m@A=yXL?Q}B2^-)RI!AVGh+c2g$h!$1GQ$SS^bf&eR|GX0T^Ak|yDOcbjqL;=rL zdY%Vbh{~yC0V2nsN%IE}nSD%PJM(Ry>(*z6y^at}E#d~hJ$<1fIA%16%hdo0cUL*u zyVBqSyHOedBbhSmGH6eOHF;veS1|ns84PS6bxedMSy;b1V>gLk{Ym&Oxar1Ju7?xF z(Yh?ns?N2tnzlG)#{W`w4=^y9R1^Nt^{I0##c<0V$e%oqRSFl9BX*DLL67(6M*Ua) zAd$b6h^UY{e7U{)*oN#wSaWb#ZH7gEyA&R0mea+N)Cx}`&9~*11v`vsg$rER5~#ne zeG73u5Cu%e^+2*p>-rcJ-i+=hrdd30I%8TV0HqI^8_k8oAH#`%Y$u<^CGJd?f>G$W zukzsl zBW-&3yThspMwctCsv?fcLmyOqv5Q`P(mV7M##vAH?_*&28}$=2m+8fZtplxbOk9h* z-L%o6Q!0|v)mt!H6$3GdO&r~aCGo{)nOS2mj-fhNnYBc>4Qi&lF(Z}dHEV$sW%g8m zUEYZueaA=kt(}8XyTq0)=&0@iGa%ArCMAYzWUKP}y&OkF8%1_>a=n~3Unx3Zu$O$< zR|>|iMp4_M?HvnZAUbBc>}e!M!s<=*PH>&%T=G``bhU(hbJ?TdwjfUem#_&gFLS5r zDx<;o=4y}V+LJ6VDPrLhNHu|s?drjt>$F6aMAx|Rp+(_m3@$q=-1`445&tEaIV}U# zjd91efhbun2!+>Ry?Uax+5C*H^~HW&>E~GlHK$i9r|9-k@UW&ED%L*XxNjFW+B3J! zTdxELMrtOf!_V%pQGo`*@cr$x;)~Z+Tl??i)<_V@Ek(c(%AfK*GLstwq&=^vgcW}y zQWy*V97;JW+8M5RN80urBqxgQx`F-tY)cmqSe_M|8*|je^RMi=T21-m@9m@9_VFgO z+8-mK+19d(TaF8fuwk(ZAv;|?1w>xo9BlAzdI12aHK6$BJ96yOF`lA$h7orVqZIHH6ZgAs; z9W=I&@nH_kqfj3v5A)o~kLo(Uc&2~twVXUck2sDir?+7L%&)MPLQfS{>9lpE0d2Tq zZu}7H{{@8BL5dDL#k!q}ugRY%GcL>b-tL(SqqH(1gsbkt1a&_Bb}A9I?*EEQVttoa zB56Bp0pize)cf41AXA18kMUEfFynu72)}wRfkhF50OmNJB2F86r!5?d7rIo)b7W#& z(s#bC(ec>Ka~9l3Y~s9d#$cdZ8O{-{;uD#~zGAH(*i%sv|Bm(4W9_pMsTGOF8+551 z*FU={!kaE~;vkBJPcb;<|J6ipVuaxUkoG3y0IDDPi=$_*b=ULY<$ibg(%1-g2?V7X zy&4cA)i8Q}atipQMQ?JG)$A`i1R%Pb2CvAe(&XWiyF0k2`~KdlfWknC3Ev%ype#E# zHro{)VSE!EGA{U+7i_({x3ga%gSV4 zJAR00ZIYda@c7F=gSDXCML+q0{Vy7cDSy}~D9v6i($6-VS*4$KT@B2s!)JBJ4=A}M z0V+SpuLpTa4v%AI+Rm)vVrZeWQFo+dS(7h!w@=s}q?*0xzef68F=0~`r?dg8HT9l`Q9z9IC2vLmm&~j;R)sr$R*Ww5?J~0&C@-mVV7*xvQ2n+)_9(1!ZzR1Uj;wpZ zWA_t38+o{#VDCyb2Y3b%$>u*v(cxi+LO7Ved=0-xa8P+8$MkPsi|nsA^0ogywJf zNL}sJ!7;S+Aut#fP;S^Iux{jBZCdFoIuIfUhG>j65)U+#8+uTtKMe>f+pn*rb%|h^ zw`N`KNiHKeW$uJ7v}19J%bq=LFl7Bp4Wpe(MLSUSi7R`$qN3gQ-`TZeQJOhUzI6ek z(Is>3&=g;~yrRZLCZ(akB#UneHJ^)qZ^>*Xqz-@)3QqqM)FIcky^I;7Ktl4DBlS+% zo3o8dRjV?pO~$rt~+cgBf;;C6LBbijr>(LK)JYTu5q`m6bufBV2C^T5)JTzGR!BU7=B z)4FPAT4QCi0b~r%V*-oeTTt^-EIWeoHWpI8$H*@c0%2PcWP7iLF&%8ldL|O+l`6$5 zd&)Wv6%Vc_Xx%*zk9bpDNFPaPvq~=eHyVIEOkL1CR`s;V9R&c917qt?;x*EqIEl0z z=XpHyr!VX5Tm1?-oXAA-4b?P-Y0dZ`kHJW#yT%16VW4w)BIQi^>E4F! z)F-7Ng*H>3`-!Q0!o_&LZml2syZ=qw7(03Nwjby(^mIZx(S>1@U#Y^C4t~E;yqpf( z(76H{!o9GDe>H>N5R~PdI_xx#^RE13e_Wjv1$Yg*f?n=Z$ujv%kR}44$J)sU#a4Q+ z3tCm7v#<{f0eFLce|8cN4+p)`bOEZ8OwEQ(S5Rxo20gK&Amq5n^?l#34QX67&@0IU zYkP>`4*5$KcaGhZE-%Btb4B)z^eY+)*LQ?*L-y`QM*BFz{HFysVzuGs+Fmrpi7+LsYd#%riFtgW1$oCV@vGtRqntqEOc8?HArtc4N* z_K1kdK?yk>>vG4hFV|K5L4T#{gyr{wCC%19go3BjREAJdKcfL5-s_~_sNZGq^4sAUd4zF`huQght<4h> zo@;}?a_Xs&Upys}n~x{5K~8VRzn2GYh)-*1*-Ke>KMwVU(oO}yIDcuujr(ooY4%Cq zLe>^<<&r{ggMNF<8R_|-iO?-Q(ADM7r!6J^Bp&DS!UZbZcSR-+>`@%`U?sP&IN>0} zU;k=vfpLk;gPwmJIrVG_LGhkb=h)+%B>eHo1y^Yc(p;ASq*bTVemMP>*Z4?|?(?9s z^yRGNPse{#O){o$QM3C+_)faCXW&+KsW!FCdZtggNG+`BqR00iPe;BLh`u{VV{$S7 zzK*%i{e**efaokboH+?oA-VoEaoa|;&b9Z;`tvc6Ko(AkgZ?Rya6I#BQnL7FbG-GC zyR@q2b8?Fuk~DkeMy?&Dr^3<4!Ygx-Ck$Byg@);#-{NarHX;?9;DX5(x%!C}rk4)> zJXBJ67>Kt=PNjny`!$QcsO8%{5v|yqaC;!`XS3h~!0xF;!r9{7UuM45ys{u`M7;6Q zdh#7L3JD$iRylF>d*he1raXC{b#mVas;E3$01ZGrJ1M%hL}KYG-e}+s1JPoOj7z62%^Y+5 z(`~E^3eLq$6i!{=M?#MV3F)&TCk-p3VnpnE|ppE?H6W(?3 zSo|NAYtL_(J`39^Z2hY0`nUKe=kqYIxXY>&g!!$3Z07a`PyCtVYgf{-C!`aEz{*|| zz!&fu9{5zqlYRQri<|(qoqR`&q03d5t2f1%t}wMHuQgKyW=_y7qS?rLEM7eMD^&3w z*x-_7FZzZoTJU1P66Ng|ans4mV|eu3kt+AQ zx>Ke2_~dcX&YGQLU%Xzr_(Am zdB%T#yLfzSoWnE1sx~!rr#8(W#AMRd@aL?2ioaa0;c!_ZFj-vDL0xuaS4FUy;mlm& z*#R2U9PeQQs(y9N=kzU&kM9+#YQ*g3pPaeis+HOKR_lTk(=f1o;uRW;Cqn=FZJ%o@ z>1bb!f5aE||(7hD^DnrV=1}{SgZ3LcQ zFp1gqQDioH9E6(p{p$dyD2=KW1q;)vWL~S;U^a{kz}p-JKZ+QsHNdU81y403t6j8d zsj0YI6L6$Ebw4#yY!=)QSW4f}Jm3VgLJ)=}v8Ij4*b5|pW>3dAtN80`j^7N6(r;s?42)J)HZ(8rKE9v$PR8O-Y4%SdDHqdBZq!>LycrNSh+`?a?CEWvXZ1YG z4%9h-#N4iL?P|cTb*YeWz|06_so<|%M9nD)&owcNw(d{bZEue5)BkcB)WRM5P+(5S7DzJeeXg$AvEb%P3KrR7xKJIGgp-PDl=b4-n1Zs z(mI^{dh(V!cj_HgRoaQg`JV`1K^=H8un?^Qi(vtwJ*_oq?~6&8Utvx5T2k_ol@@}+ z49;YhCy@^Z7}a^xuxfhGWHGT`$%7LIFxe!O(i{saruN#< zf>MhgM^v#h<$IS3-$yFY(#CVjBv1kH9QMVIQjNr@i^hQfK^^2)rnUvIIa83#e1GEv z1szjh5*#RINlmec7u0Ak(;)?R{swnt_&4S5dMjim7vjov()#NY+n5%64lrWy6&Hnb zRTl~a=TWI_`6;iPiI|f(cG`^)C|JeYU&k+Q7k6)vg8#5#MpeAt?TW13D>Cd-6|{Q{ zMkQK+$TEWP_e1?OlO0(-C?&szaLv5uY{G;N=!c7q@t)JD3|83OYAaq?Cc24o94855TO$;1c5__P|zUre~xK7`T(XP&J1wloJxp~ zQV_iEXh?jF|4NR4$kP@EEAvffSrs}ekkNz55eLrHGhAS8T1Skd_vM(jvW!`t%tbFx z8^KBT!FO^IJO50+c4QV;=AH5&AH+gxY&Ku*H7_R(-H!o(lf3m2w-aS&h3V&*q;;ou z;~tp0@$5ROF~o$=d{Sn{TXfxj_;|sAl!|K9{hD@boq})id7Q0R8H?7p$HRwn<`F%` zeE4?c^^BXON9zt5+E)C^0!VgbH=;k+*~t)QSYOR-dWrl`ULYPyr0%dQ+Zgl`*@>-v z`ZpElQv5e5ZXUtxwu+jcB0h0g&RNRC@mst@@b@{G^Opn_-oG>w3;;yHG6|#cTm?80 z8S`7SoLUuTa4Aj4aK{}Fe-ZwhSzV?qYOb~(B}ju4B>?Ebv}b;*`D4~PRL}*3(TLB* z-StHs;Yq}CFCUZngsGwMQl*#vvqvaK3CrQ(^5wA*nla@;^;0rBaJ8QJVkNTf9$(!@ax zf2E0F;}rf&9jGikb52%oYuS^ln<0#FZH`*QV=WRhm_juoBLwwpaN7iEYzl$l3wSN4 zvv<$bVt~_3KH-7Z2p+)kPa0)fY%`p`A>0Em)5V5LZxWcp_Rip6LXHDb4y~Rm?^R13v7*30npqcyKO%0hC=<3-KfcTYdZG#wBxyt-h%;#neZkn^5B#uf(~hOZ1>j} zWEYJrWzTtj)77g05p(G%09RK-4Ce~m4{zrjNv-3 zJjR+Y*^gP=AfV^XbcJH!(}C~>66}nPo8FPz&@kGIBYXItO8adR?ctle2YW+LqCWIH zYz+1m$xnz4(SX5eWGp+69$4=}G;yaux@r2g!PoijlnWGtKoaxg*MczHx5xiVmw$Ln z{+4f;#Qjy#(qa|h0QK1;%g$bPa}#s^GwZ69BUGT(r{(6E)zH6vjAop~dr&!Xag{>O zuf(r=`R)Y6I5NM+(VL0%POX`ys5Hj43IB8JBs@eQD6k^5^aGd7{PSe}V=%M?>kX3E zh=)~)=~IzlphzlrWcd$arPpQ$E1hEe3eql|c--AuzVP5_?_lKlL6 zfZ%#(;=c-|@5baU0{|=~g_KFJ!fX(*Odi~L1z|eTgo^S!F=-{puQ}!LL5|;iN!$()upf#4Innh$rTkg##=So%Zgt2> zUz;YKHQVO7kOMdsp{b3IVt5i#S;$H;6w*({r9aV2MOr@n;(aEL9ZfBY_v3aV+9iDS z?lzKr+wt+IY$jmiL*t@7FP1{L*t`>5vxoU$`BdL}6nKs@?~``8>VM%(T-@Hi;>6wo z6w(?Rl=QVt5f?k@Q@NsfKCSQ|GV5|aW`or(klxglhk67@o#wpz+_yMc)=tYFL~OFq zhA5tO5E^Dm#Q*zdwF9hJD4o3T(#m3@ULlCr#%53M$Ecn79JS2{2QwgoQM7K{v4&ES zx^n0E}K*@@kFB4JVkxR>z#L4|d{74jQT-4u61cu>?I`*iiYTde7obQeMM-rB zcD@ay`Hd{`b^{&p!|s&n+xhKhK&#iy_8YUk4cJ0@h{|iBrLsL0byaw(;dV09tgI#E zq{qoc6W{S1Uq8$p)!^Exb4Eb{u~}i!4}|q}^wKWhc@cvjpFcL~IX98<>SBO3*iz9` zpW6G#a4$J-&;qmS(=z07icQGCx^4hm^TolF+$Z{ADXbMYuO;`SWsTAVbrGLxgqP4Y z{Nas@Muizjji1OfRG z0I+(Zn{Q<%Zs<)OZ^erwv}X{nrx}%*KV4StpexB42P-Hrmkkw*B2r~O5l|b+ct&Ec z>sv%GMg8sv?j!N$--kS`)Tyqf8|ExvZQQD+ z9+TZvgl5yk_pz*R$Dv(+QNcxl2S}#>zFVf*CL)cqd&@-jr51gUp8l;(0cuP`vb92pgr$%qGDw;+~+@2x(ByjVt4fb|iyNr!* z-30tuJaWn0LTqq@V}OVx_xo!IcuO&P&x>FQ3PM!h#EOk^;^D7}!Eg)siH086rS)!# zynKyLCX#@ebF${blSn-myKmjdA|70`=g%m3 zhJtd<`(ft;Jv}|q(U2bx~00;;!_kU7*XC$UCVS=xNH-ND-m%eBrs%Agx0I4Q6NioKLCC_@f#!)9o0--H6QK3yCYfbCK5EgolVk~EQF zrm)ccF6VuUOHVb>-{U>A>c6ljNsR*mCM|KmlE8ZCTzV# zITkh)LjfWgn9lRo=(u28TBLIVpSaMQ<= zVMm-+C`zOZ6PCC{gmy~F+j$U8Z8#`Y36T~c2*};SMY*K0a!)R+zBhNe}$zVal$;v^CB{;pXH+Ut4Wc^9tk3lW)vvHL-) zs3@Z%^DHfu@P0Yo_BT|uP$v~Y0r8%ufNgZ+hnJB;goCja0g5%oa2RrG>Rg{VMk$Qw zMX>_l0l%2LciG-(BALQ)Y#}xe-?a5AQW;vbWkPMm-+1|226Rez@ZA;sH1SvnkhcwJ zt=mDLF|toh0Sr!_2h+_~PzJEn2@NC?lPIQDiMw_2;M=xM1Q3+?MDfBKL=tlo)6a$- z-rSEkhUx2-uVL&jTAZNG#Qradxbfm`vq?Th0!l>Y=&jp1n{YI(IuDI78)mW55MC^( zO!pK%K#HsroP{hmeCg*yfx!UTZF!YcTj7_R^Os`$`?Ik@mD}_O)4*8i4if-5}us}&&vmgl~91Q*v17ohz&NFa^kcT848O1 zOUgh;|16LV&FrSF$h$Q$$+({q&7<2rj{=%73%sLPbgm+*tIXaixgQZSAU-z~75J*i z9}B2p!TCa03Na%}2!0H%oQzk7DtWmjH>%3!WpXmIPh?o-ePdDk6a;uEn56W>MfAfYQArVwC!zYY# zX-@PRnAF61-#39Nxz?-@!mYMWbq+ZYA!4EVA>kZlJmyE616yE_CBVLd_C_8e zxP0M2)BuqeZdxPoF(Wv~D2nCTk5q14GkGH!>0SRAuovtVvcI<#62f5sE2y)NA~Bbe zuP&CkkEO|rr@2chmG1_z5h-BSCbl~Z5e*La~oX6VzI5>r# zqP1IE8xny%a8q?fsZ$9?Y|#AU_$w42uu~b40xqZFCNCKxob4sPTm~01veSH4lTlgt z*fnn9pj7|gbAwcclF;kaEy8O^pk$z@6n ziUyQB&sx5JOQgx`_ArtV#}hool&xJGju>{+_NxtN zF+1D*RgLNMd#qD(%`!wotO~tlX_z8FBQb+MkE~B9Njb5tocVF<$dFVZs?rZ+=bAH2 z{H*9g1k{i4av_GRzKZdBO-V8%eVT)7beE_g9R4wx~~ki(OTtS_?xQ+V$V4Mjv_ z0H7dObcAc8!P`tA&NVbrcd-905G4LqijU*oDxaYL@Xm;+$4Jrqm1G4Gk>|)bHC+|O z>hq^%?z9pQhCCg`~W_&)e*cZ2N_k zNc)a^-%+Sw{fL=xOn#D-oB3iWKtmv^e2df^#K|dwIp18gLyFkHW|eY;en|GZq> zbf6O(Dl1LCLqfm2y{o%5i)&#M$|b2IZ+wCL8z%pr9-q=Nc{26SiT45{17qteweq z;=f;k7^N)394}(5AKr*0nl*?SX+jgMe2Y$Xl=7l-R{NB_6J2Co9Ef2JLPW5CkU&!jD~x9G!;8fajo4uwm|G-Je?$PEmlOkR zF|BSPo$T%;S$yX+g+9qXcXZzhm=#)1+yDVr?4@FR>#oPu;)(OEu-n-aGD(*b^y{># zv4S%e**GR1oicQ8?p%Ky*GJ8MSidOw;FVz!pHoC|q+YCGNxfW50i=;XVPHJ!I5zRM znr$@Jj4RfAGvoC0tjo;Qw5CuqZS{5)C2218aG>D)SMj<>dJow|AWnK`?GVZdC}qwk^K}1=eUSzJxb* zKpq>CFV6LC>trE`1orJvmHwx^C$iD-F3vyg-$^}u%`WGZI1s5Ov4=pZS(e0%g`8Dr zgY`NdsQ{;ODuB1s{+69k?yw?7>*~ZQ&57I-@!OvIG%}zNI4zHLqr0-$=AA;_&JL`8b;V z0u<@E#gmH+dX!l*)_2mcD6l@+b#rwx%3+A!MR38%-49ZxkBsfpyMlt3N=1YKN|yWr zhF_vk3puyarze>codl2?2Qb7&fQJ*cdLkk!BI(5Xv9hAP@+)ygk-=qE1G}zK1u_xK ztCnbLmhZWpAP~AoiV8XuX74eRMkYi?kM0P`$#^>@IJ22YX=vL2hNgSnPNw0-ixoq`qMzi-|7jn`L_NW=|KOymW4 z{eiEp7nLTP5@jUDslpmBrJprd5)erjVHmi!j{RfN^XCI-Q$?!7G% zQNoh~Tg$HchC1$4Q3j?Jo4FJiXtoMvR_A-#?uM`{Fliwu!HM%aN& zfciT%s{2{ZAykSp77Lk?e$tyjG{GV;IfX8wA8YfnJJ@Ib*7Mv00>D_YL}V=<`|IpJ zYFrane5VfFyVD#F6W&4Do@7&G=!vwX&^iPB&P;qG1Fsc=g&FIx4eMr@-~P1AviT^YD<{s z#e~4C&^NsBBf0l>xiM58z*9Vj_MNDSkJJ(V#uJS{_0rBbr^867rFX}hx>xZXG54^4 zRu&Yr@t1rBOg&f<76@iaDxSr$4>6n)jw_H~00T5WJ`piGr$ZL8cBIvFJrYDi<5g19 zLjictwpG~U>}8{=x2H!!+uA6P&oDEpz97@3J2QRBfPcqZWv9L0ACdyKtVVhDfGBdR zYWT`*$oa7M!wjvRLo#Y=Xz2Bs&rqP=un&3c@(>I}n&L|ZB?@VrcM_s^Fx=oQ5j8xK zxZ;F62?WpJ_d7C*_qTeEd!vV`;|r&#)=_~Edu3fqm|VLX?&?_-YZc8T7H2F-fsROX ze2nqF?JXN1s>WRJskcG`Q&vp+5_Ru7hQ+bKdjR;sZ*P8B{gBL6_Us>=Hv0P{9r zTdK#`WG{}VlYahXsiZ}yln~DMCN4$D;<^bnD==e0I4Jue_B){Uf5`}enQbIla8+bc z&`VFp%qBoGaGX%en0hzJiR6eD!Lq};`&s~og6E`hiDw7S0%k8J5#vXUdw?Ncla@i7 z8=U_}H;gOH=>O)GD8s?%n0PH=!HVoLS^l%CZ3Qm6cX|;>;JiqGyk2^yIz}&x(?oVg z9pQg(i=D}o0Gd1k=xP#*=%w@6Ebw9FaQxD7nJ?uh0SPs#Gern~Z44d(tamf~2A zSSBa~_J)V>k2v13+7Zi~^tlr7f5S_Wlr~Q^p5SREsW!(2Ki|Mm!Os)jNu6Pc6ug zG$wAN){VS>T6fZS3*cGMMC*$t8cb%~Vr-%kAk?b~FQ(d76;n#AE!_~M3;|9=LT%~H znbwWyeq(Wp_Q{K=YE(cK@et5zH^LefvOx72&)Gw$Tju&%TSk!saZS)ucw{ViY*4Q7 z%X}1wFadlj_`Y}c`jq~Dvdw&mpshb3(}wa`ST^SsMQBcs!o6$?3bGm-3B5(G&C4++&ilP?DC0Yt~T{y+^wNc~yeIz4Y2OaWh(+?!*^Sj0M zz${3rLoNHJ?=$f-)-(98F}p4sF1yjnju(D0I)Cuw^V>`EA>8GFKZt9%`1_)-hv3EI zDF8Kme4%u5>-fitYVbagx;cxE%(!BEZcJ+;ohlnjf{anT^FryRdv2}S3W3x5@OR<=1A^F}mUWs!GDKcweh@fMk#pwSH1 zuFj+Hv}L!}FU$nnFYzeNG)0F*9 zO#SmiA4Uun5yM3fS=FepENS9E)bea0Z!=)CwxvUcLcKE_dxLO9DBn|U@)r4G-)j^~ z4Hb{c%~D~^@pq!(39c4>m$TMq`*!7fGHTuiB0j8(3J2eo_h=Zw*}8vD;IBj%6k9Wt z!VZ-0EBRk*hbeVuzc%{R)GiRhsA=*SWhtLjy~V`>-O|~`!}N|O!imspNvCv-L*=U7 zK4hI-sjA60`Rk)lCPx2!>Jvbzg!Osve*cNb#SqAg;EjyV@Tzz$lQOm@>~yqvQTj#p z72w0N`njjtA6V@p9BcrJRm4NCDs8;ej0b+#=Y{Drn-Dcnij??2-nwuDMFf=qlB-_N zTjQ~v#k{FmL;w`DFXM~<--`l)AuWh{w=;rG&^QjB)mJ%4jk+rd%4;iz*~j6V94>Pc^l!uG$5m8OIB;D&tg+FSkDISj6haV>?9cN!T5f?(SnBsoB!E?Ul8*t z>U(daZ}zjI!(aaX)u|%Eb?Rhvd29z16vUw@sd@aVa_9u}p8x((8R@~XRV4aLd?9F{ zS)e<>)1%B8(O>{ZHEp}^uZc+_!iSA(W3uG*G5a~}c2YG~f*dKQ{GMnHoBhxLH)>{N z`A63I4TN^D<#c*KHw9OettPLfOfA#C`x|jS#|CeN=A*m`$)a^-uTnr+(yb6I!Ur&T zAmsxOF2U>*NTO1{D1{E6Bg7k}-&*D{h2g{QZ?ehS09C{?sCNLmic$9`affV8=i1ZN z7=<3TqXSr~UjQ3>_WkKmOM;8Rvk|wh<(lG{)Q`KY534TQNK0XqNsasio-&8~DZ|c( zI^?;M6QSk%r^GxvB7qg4Fd}GB4s$peiEF(?8PSyXt%6_8~Kj1Ho;~k{90IJzef;|i)PD7cIaCV90GJcJ!?eR=7w7$k)PB)(jvNhuMYhuad!b{^ zXfk}*vg*&=FOi-o?<*`c*(l%r-zla7Na=!d0d2N%S#QS{X%6{&3Q$p0g$79K4xJJg zeo|30b~gIhC3-(fTsmgB)?q}gN6D}+8Z@TGog|s3U7ue(d`E2(uQ@i5#8K;PuD<>5 zHPNm9Z=)#J3e!x0&0SXRKZsES=g>%I4lRI|1(4ez`MT@mJk zh!u4TO7h!oUOmGV$`ei02S5-K1J|0grSaU7#xS$&xhuNF{nKO!!SM$#b0KIDU-1>G z0{L$kJ=fNR?l-K5;};2lSI=T9=NCH9h0wS(z|5zc_`ndTnV}-0fpuX@1XVez$nn&i zmCCs$l~cg~@%O3V%nJB@@NRU&SOZ zx@1@5z=ZGRCCnTzZc2RS9p*zul2$_%NQjL^P^!`}Ty*Aw5C_<4FbGsn5&swPHs*h^ zk+JH37iuI2&raIVfqDQBg0WKU3bsK7Y)}SbU{>^%Q}{med1?`i0vy@dWolU_6&j$O zs@@1w#jzoe#P?Q?yM30b`^Dk0dylDseOb^}Od*}H+gtA*qTc)*J%@PS4dqVC*-B*u z3N})+=XdufGs;^uK@DD#+|mk+xCQc9b7@aINg}gYrv?h0Qkn-jD8$L8&2F7ecR56wUmx>BC{s zJN7qOD6NWB@$}+VI;$&eYY*qIZZbYA$AL#x2Xf=?Y0yWA!_P3?RPJtYjRKD0}4C$5t{@86i7l%gj1PMz-ut_9mQz zv;H6N`+I-Cf7iv;kX4u0gjyrcPYh5W0|E*NwkG(E6FD!gvmQhW9QDpd~g5x@5Dy z)4IY$>`}$^-7=E@NTr~^iCB5$GY9>^s+R+qhi_bO{JArJ>GE?M=@iG2rKFqEDbc4q z{{>j~cS@uaRvq78WEK?okUneJAGq3rv2LeS5e`rI51l^AZ(TDIXBCDRLN+f7Pu6V{RSil%+gLgD-ZVthYZG(h*Ii^E&p<&FqhCVJ zG1U409~m<;u(GLm>q=bGW_UE^FW$kkmJnqgcmT&$YArJU{=&7s1j8;p=Xqgl)_XZ8rcDmV?|_RvEMHx#7;Idgyw62NAFVz8 zjm~lloaLMrLZohI4wCG%@ci5r^XKvv!(CAvRk-de`_FVff96WuY#G4fx1x2Cv5Kqv z!0-CWJGw`xOW??eP)g<$F}Msqc2Z;9Q%>0>3rMr>Qb8@|7p;t+P2YaJlv=+>?3qtw z8!_y3P8^rQYg<=|Fs7W=^p6>OyhC*I0R3`Lt@`jiZ2n$9dvK{=DatwM67nMS$zTxv zhj8Z9)om!DUrqMTKcM{nurJBJAe^{z;==yZRUS@A!-nY;7w=Eb7a40C(CGQSr=Hm02$1zZg8DOKcDkc`rDy>f~2ej?{j6s8!t$N-2D&p~$3399^4}XZvve>gYyXq4Yz^z7O|7gG#-mkIIaj1JZhRE^w((-vP zdnqSET=vi(_k-G4s9U*_q^fk5Qngw@_USMbeobEIL^3tH@OX{0|G}r?M$W2I1oW$G zl+j*R;$X#z$?ypgZ=o&w1L-gCl56n9S)b$z-0AxLcy{Y(_TzWdZk3W7?jdNx>UE;( z5omFO%J_GIg_7AEsDPj}lQ%nxB!6MKFr2;cmZ>3VuarY1z-gnsi8(+u$k*ehzEta^OP`A%Ve4ow1l7 zFR@|Dux|wzG#A6|55LM6QA?KuU#S@l+`f6TSEKowwtZKVfHl@s3jZC=5_L9FF7I{b zZ>O+Ar|YQN8?Y?K;8UE|KF})l+8@i6TcSB1oanmH3(L0LD^L3V*SE{rLO#BO^kOS(gG%=8BXdH=-QAUS8NvFzB^U7;A?Fa1m0 zYCWVw@wP1u7M1Box5#Y$-k8KHeYiE_tgsR*>#|y!okqv#k+m~Ieru$by@K1HQ5s<} z;~u4vQO4DmILFR}4Lj*h9`fAW8-fy~dVRL(pQA>w{4Uq038^_5tO(9LBVr}|W#EyA$v5`6@p{(}? z%hUcDZ9y^xh-_Zzot{(ph7L2BCenJHz%~;K#DVJ3079KWie1=1Un#Y5ZM{K$wtZ40 zP4^btj;NHt@9U^Yzzjr%Z6rQIN%Hx47IGE9_ z$%u6xa^*C4lFFH1CS4_9@IuH;%b2A~qIbM$W_!q|;)zcQz()&rA&wXCh2Zr1`zMO zW13D&lUk7lbWM@iRhx^99vQwH4EqLq&AM&u+l|}gNv9|28ieqlWrT)w@SPpHq}^k% z(CwH>h(Yb9bcX#8=@(#cR+^o_l8)E2?+;9ZWnXEw&!A805njkRX)YNj{)9%TMr_r! zkA(nL!uHskM0LlOs5wn5-jgFvt;M&9yv137_bs3pE{-`&TbL?61!DnA%?=^nQ%Xb zL+s)ks!9pGtG+%iUjC|(BotOm7yrxfYzZu!j+jyy>DrMYYwD?TU7}*h->SrGik(LS zdoqT_38g$crEim8n4vbV3C?+v zc?KLmg)}#B*DR)eC;dUb)~N);+^5SvPW7c;LSA067uB4@XEVf{FWcFN=_HWl9Vojy zk6-_hk42BwvPbWoVWt`g1Ynl?$iT?&V_0*GK2Kn21BH2~a{vFtc#TKS7bI*}M3N5k z@|Lu}6nbM6&QQ9zskc0A^m>t$$uVv81k6=8x|@&;Ljg|_f2$9gQsO=_wB+|zwzAY} zQmeg_4o*V!7Gd{(>&UiZ$}G_VJ`iyS+G&UR(Mn0!o(r1 zrreQ_*zrVx!?#F(SRJ!i@9Nj#`JTi3$575@G=`tDP(qj^E6CKyD&D*FTI0VxSmWZp zdHmcJiK@5O#?S}efCN(t)gW5QrJwp!S#&hhJGbapF@X3Tlj*ix#TgBPG=IIlMyoYu z4H=nvR<&=ma~D8%hDwLZ0yIv$FK6~1z!!1Q5M49sXP<7`zxp@jf`c6AKI4pZ{+id%9-v19ez%FI>v$j-t|ZNe z0RV3S9D{^%`brmD*QTS&VvPRBfSm_F{MVnJ(NSw*Sc6(U^RuEr(EIMQ6<1FXJP$~; zw%es_I-}ZN1)ds8u-Y|}<;uQM9=Mn{`z4!CU@_}1N%QhZsg`HDjV6|%B}9@U9RkjS z=lSQQZE{^?_E7Qkx+3TaP&qT>rQ8kWCNisA!n>y{k7Vi+a9nE9g^S=W45>jlP&Lv~ z`b&HB&SL*Sdr{6-I-31#t%=ro#i{GF=$jUY=csYMJbzkBl&B_t&PHfHd)sA8|2|c6 zLT9ndBcuC?;MsKP+`=o=cN>*&t%>;8d#G)H8}Sx-2byBstIcx=2QY~JD$=!GER(tv zy)QQgV?5*xtJl^5nCkZxa|>May&wK%^$!;crZbG)FQ}YO_53L@WIe^%Vw)aTYv&$- zGW|W`jIdvrfksn=;-NF$dHkaouxISu8>?Z!&AgTsUML=Eiv9G6&SCDt@Yc1IftDai z1Th5kbGQ*%llJ*9>p#&1oS1dyylz`}M0UkfXDF1$t`ac0vb`*Yh2*nu8{RN)2EZ?d zJiB?$QBV&vMYoh@O^`!3J73H{avyh42hn$^aeFW6pa$Abb0;U2=p3|jVSZ}ix&FbZ zkKA{LP{SYRANCpDhWTk4s7KaV(UI9U#a8R%178jhF57I0zV(gq(lVdXm(GSSLgPlK z-+OQ_6wD8n%Z4K<5JSn$uWhgX#*+U9dU2`4k6FA2#|6YB;S(j%sRejZd z=%P^j6PkV>7SIop!jLE6+6%ZLKg^`!#3@2D;4Mg(p9FQ|sOX68eiz9o%V&8TRe%G=PKLGsdO_?yr_yl-Or+6 z?xe%($zng4S{mlw7^7MZk<5Ugw9SyGOM;W5TCKZc0m7>T#5#ROM+3ezEwS~Qj1YO- zfP80`U3sCEB|bJJ-ccOCFFA6FcAgT&$%0lq?RyAd*!v`@Di`8n_f$4e#O{F~Eo?)1 zC+Y09;2g?(xk|KEcj-oD8crAznCHWh3w;e6DK9yDeKrnT++|oQQD=>P?!~j+6r6+s zE!Qqx2-|@KCd)Xp>z$B8q+v}>JH<0h=*EkTo_0(7jGAGHz_BXXdUxiA`1)J2TeI}h z|H=`9E1(C>BJ_ACi!1L@m!(FJX*szEq`oixUGS_x>#lHMO#=n^Vn`sEc8HMg zB^oij;;J{;*w3hzdF!pEw|6gJjN}UXLkt!tCB$(E>z{~4*1l`stJ}4AVawG7{8RM> zOk{)_YNkKVuRBODNqy^Yyv(ysT+YxVBZU`WXIJT3E|(U|E&!(LHY?b*ynSeM;+ zF}@tbz}xmox`#%4+W`^k*v} zVA>#45U`nzzoxvl3Pg}oOpGds$!~%j014VRa*n8Qm2#s?64pSQdqm)IX^S`14iYNf ziyP36?naark@j!!Z0ING+hB<`E$shpXX@b7S(9*{ zy$c1$+px_=nCCe4ZDoIF1Kz<<3bnic)dJMJkUkwa4YSa?+|xuwc!*2{JL%3&_ugj< zXXvSF z^pZsk!kK!^4JmHRE$1t=pPWD57ehDS>Gk>>pg*cNN)Bu5FZH8nY2XIEmKnRp%p zoj)5i{wIgZPTNyn<*{p8_JpWR3n4YZ>!U%ten{~48}AskcKr%1g5SHx*I!_FbOfi^ zXxB%XRJXxaV@*94Rg{_@4EP(8A0UH09`qe~=W`-+yuy>c8B(fY&d>O!V|c$+>7019 z`u|NyOs>n&z6;Qzzr*m%6MXlu{a20w%~|0ZO)h@imS)=`PX6Sf={Zj4O6~culnk~Z zg`8)izz^r^7!|#gZ)_2%8^3cEiMuvrFSv92iN3hF`fW};3v+1TJY3^GwHCEL@jfJv zV3fZ5w0!gF^V$bmrR0b8$Fu`%t_8|!QHz!t>y}>A`;wxQZp69njMbkl3Sq7+((``T z4SXS(0VEY5Z-Z!Or8O28oyn{{mclxzI1rS^kkt=m%vcY-(4R!kcpg&1187?soQ_1X z0)95WnX-yyzL9aWt|Skn(;3kA@`ybN#$9=ekcznwWF={Rlb_P2@yu5QzJba2JWxXH zpnI34PP%qw@64kNcAo#wfeoRDFRzrWK6DO2o`6Eg&Dpf=ImXhOUN=_KS@Px zJ3m_F$uRW1t<1HnF&W^ty<1q`3WapNOnH|KcN*c*)Rr`o@LN|@He;GN8S&QxFD}-M zAC?x|ZKMWquAgm+5&5Us_AUTs6rSa_2ziEWO`}kkR&z zm8lY!xW^IArlTA<6+hqhNye$87pZi6^{`#y7RIsi%b72({J@?@WoA#QK5v3?Q#}B| zF`tcHofrlWfc-;-Yaj_s4LST{)Vxy3Lw|Vc1;Dz^S~Qa5fxvID$ELJT`60z_6}!jl z*XWmT<~2F$xX~=JUTt%I?CCYlZ2PJ>fTnHh`=YE0W{S!m*Zo#UU-Vn$zMQfipA0HbTW5{Dytwe^KQ9KSQU`U4g$hD;d_cM!)rbq z^q#SHEtT2%0o&jTj{wX@WL}BGx8zhBq*UNhf)*q|WSUS?Ps0D?0ia2bR{ZR*f4nm| ze&HVtRPV+IvOtVz9WkK!WDv$3Mbg)$Gt!G8>~EX++w|EBl}{eb;DDS|V*J5d7~UfF z8uzB;V+}#jU%}0>99-s|atx83p++X`NTb_Jth~-CoRLNcY-i&}xuW%SjGlFLZdc_CeD8eMR`C-o5ZVABFRLBUjj|qhzZMwnyq4s}6XpzOpU83B27- zr21050G6AEwMm*myyW9%>Oq{vsDcpyu^nvqfN25QYC}+Nq0?_39pBw?akabc+@RO! za#ZNr?ItIzg^bfXdi`>JCCz*70>;@MZDEi~jXUN&_?QH$O5I!5`4?!!aJM&;?uN6E zS)y6m5Uih3+oem|*68@X(V8!k%Avfryrnkc52{n6|A^RbEY_xO-02~OXW^E~t|fFuBU$SeXPit7zgv zE^f9e>FM7$D@oC{Dhy?koqnP(`-ya$BiY&UW9j}i*Fqs`*GP)g;_f3r8^|O=F zS!haoY1Z7T;E&sAnA$XU^{eYVk4j+|C1X{K@ziB_`rYq!ZHCSr zqG=Hf@L3H(Hf6|EoyXg`*j~Im&VC;D9k!SMv~tOykq-sJQV6n^DZa4e;Sb`!nbikl z3?FwtBH?VgP;bP{ILM3|45#V4_Q>wCv6Z-i{5F(#8mO#Fy>voTu9UQ?r$CYRc^4Ec zY7pQ73QJZX3ob^P85^>V{D;*XVj9QOGFg7JHcw@a^(duYm6QG{{l@YTETnSGl#70H zN8Vm^{puI+91fPDyMi74;pQ|59f_IjR}%uv!&|O(YU8D~RBF*Yfbs<;9PR;Oks3<) zNFSC7)TWjaF&JI@BAYV4Ka}eu@u~S4>Rhq<)p2-5PD#>lGWj1HyC02=W3giQ<6-$`t6#5B=ziesnBB!TXpYuuip@ za^A8cUw}ZI1>~k7ACwr8-7}4Wp#hL_U+Eq<4+rv13`aMo)A=j5hu`Tnt#&7s(n$qG zp%@4aIQAv zIyxY7M&j0R)HYjUcTSUq<3O|OWf?Zg9*s+Ur~&hrl!RO*0@Wvfc4SDc-9?s_N5(U_ z%JH@KkK8e4D&NxbcQ1XO}^!P1&E(02+AN*4fta zLJ^SPGbHZo(~Vvtvqfn~gHV$J?qIQaDhR}HvJ9!-0`J4?!^*xTbridG(U%xGUJy=Q z{n^MN>re4AAm@45MJofqnOOY2eR8$)b4rtsB!&N)O&a2eCC48N+8!bp1xPW~PEjYf zR;}pJa|#^a#k+89l=>%$>BQ*-ktuCSW15#=0v!Py96czUSi0xmZN~Q?YQyj>sxHD(9yjBs#8ot#?nvd7FX%)YmP-=(*QXzV#?T=Ysh2jtpQNRzcfU1{<}D04t zlHaiQ27g$5cyicf=Ymq0z z$!V4>a#G;G#8Ho52n;gW%aR`c@|!1jri!B+Q&$RP+a3Fwtg}Yvt8$cq;`MOu z=_2btJEmiFKnomh#mmF#4LpD*!~oI!&RRJl9IfK+&8o|B%DDh=_(wZO*GVP^o#D>MI>SyyT0VFDz zr>{p#!vFymVZpOT>ky;C*1pLj??>q{0xzDo=@6Ox>R4IJ1<@Gg-xgeij{gv zeVZr)y}m|dw`bojP>zha6P-)zXHn0x){C|#3+xbo?xoR0d-Wfb_4oX|iULH#K`C#76rypDueCnzEc{HdaCId)@LP(ysb05KW9%=O9V4giJ|5ot|vxc{u z8jxj@(6jTD1-g0~H~v#;BlYXjIp$+cnurRCl|wnQSGRzQExVZ(tEI)oH=lj)>lQLl zj!p^iVyp$-2oT4zBIdqb6cGo+Ny1R2^S9rDpb}Q2#qV+ecXwf zXZVFv0OM25$K@EH_*}!w<%W*yMvtq!oDZ9NnjLJ_S-G}1Rv&`q#5GtHbL$+o69xv` z@up`y9FOBh8ECKie?16!WNnk?Dqnk}d*9$CVV9NA_x4(;O?eYnq16kX` z{EL4~Q~8FZ3(aE{h)ic0W&cv{60UwAyARiVEDjoj%FL`(TJA}Jhwu#g?gqulK)al2 z(+d&!LVjfn=s?rg#Y4{1?l-Y4j0%*=m0>uW2vfQ-T4Nyc5oZ`0%LCJDKu-)ZctfFQ zOaoroCxZ>1;j!haQM-0~$EBgJK1Ra`&d=F@1M>6|!&3!U#R;spo!6oh4$}hjYp&Gz z>iVVjZoFOuu{A>?5c-!qEmZrPM-mZH!Ab%UNKR@{X!Mgi!0U_%XF=pbJx3iMmDQMd zjz99BHZ2#}4RFlETR6*t7)WV=#1wxjC4z+2xAhtQd97^3$xTg7H~`B0-KIs`0?!x* z>a{mG9J}CpsQcLs$kcBLh>}^Tmr598pOBn07DVKB|K$0VDRnvh*cWh}f46&WV0~P4 z4s}Xz{A+cGj?l}N%#6G-ZGF-fd*FUmP#PC2@#^PyhPbGQoEP1;uW|Y@X@zfb;tBtj z7Y!|{(LsD!8gq=h4E;}-7_V*QbG$1I0MQ56_>!yU?YU$Dj_v`bR$~S4#nGm^md>4I zd-a&)ZhoVNd-Vwq2;Nw8qw60Jxd$_mM5UvL>C=e^3;rA(q~W1wV*C7hP1hTYOt*k+ zYdoe{P|isSkZQ2Tml^tQZV^)E*T*D2?%uPT8XdeS+SBy6QP!oSG2%sr_;Xrg`)(68 zJ^KKz2R2)+@gFiZxR-u8tp_+hPYq8IWEI~u!yALfJa!{zDGYs1CwP=mH0)kM`n>*g zuTM|iV}^`!S9f7K&h?vrj8|WC5-vgPOwD)xPoc{Tn|{y5<8cQL={E( zwVKB}fFm3C`d|L#Y*7F|+DS(ze_N`LE2k9yF7((=h}gNETWLG$E5ONPM{CktJ;9sI zjHWaG|LWgXL*tO!MgvUFat~p7H{!6XF73%cq^_O?&BR{$b+8;Jwi*B-`1*#jG(r@7 zeF&KQ{Ii8ZV1f_mO88B@#wDI-kb-kQ*MC|v1Fvsg9_8whx^Ih{5OSX5L@S?jWT0H= zH5~WSOC14KkXbelK>}35MS6hGdZ$B)aW&53B<-|@pDqb1C+%~}IlAlp*nGGS23JM5 z9LHg7)eTVIYm044EVJOE+%OH0yr}#A=s-?)SG(T(OFpp4GvGs%)V+?D-WbO6Gb$F`WP7|eJ-G-H$=Oz!J$$3y1ecn4^$VkI6 z&$&1O0XaGZdkq72%7bNY*~`+1G?X$+)ib*(cIwg6_R2K`J=)AV4!OrFYwLFrGJ)6~ z&?5|_^P(601W^!0LxsVb{q&@>zp(<#QyL(+y_r)NYlZn;0(xPXql{TFuzG`m)!XIr zGMY8`)cjlsZBDHj+{Y^AE-3&y;)ddltoEO)TupjKm$A0Kc5a41R62Bjyqqd3XRq-6 zCE2A^cIhjJSYV%Npo$&`*$X6bM8#$GY0&5Cw)v@fBVQGwpF{Q!;j+#&rw%BqHGi0{ zVl?gnFq%_3y?odmOQr~FU@kgOS}-~*wB1%WWU@ID^|_zkDoj@K$k0T~cqaI_)*2u3 z&TE$?7h3k~vlF<~Ks@f8YJw`1?2hZUG#h<)&+$L;%D+`g55n*q zp?N+j_+^yy^}Q1lu#WUym`&Kk4UnD&L>cZK5k6%fx}uN~rdZJ}F%^#>qS2`bqULAdvJ? z?4>y#u34Qy10rU}1E>J)sNEcYHsCUG=^^eR{By7)eCPVn%>ZWd{#uscqSvE_q}N`( za_>`E&86^!Qvmit`=t{{3Mt#8g8ojq9eQ2>!VRJA_t8Pj$t|e~($f7@>0fv936(JW zn)J7$PZrd|KmyNmFw6jE(k0PZ!N8RFncyxc+aa~co264Nr!&TW%cZS_=pLfrm8Gh7 zi~2yDTQ@BB)S)Mt6ra(XDdhP$UMINflXw7p&?|t=0yBgW=Wcw5$!6FmDiAco2cC&) zCq?X4rM)cJUE^FW@1+-@*uoNUF0fmFj_sL2fR&t+8Zev;2Ra~Ejot`gVi$L(v{;i- z6u>VmKTVK4UqiP_rz<_5gAq+ho;p{e5utf6gdrKIQB&&@O;dEY<2v;3yzX`Yr|h8& zaPoq<{W!og4r8#!58^it_ug5wX8u{SUnn3j0+=EPs0T(ptm|buaSD_!ZPW3+WYueC zNi&;#gizgCJ$U7LQBETj;jM})xu-coNdlA0SAk@H0Ob5Btz4(wV__>}Nz?$9*faX4 z#Q%0K5i~qwTrj9xzYh8a3;GxHLj>(C5-j%)WR~_oA4PlU$oLClOG?6d#YexIPPT`& zhc2;Qi{-KJ{OvG)Fx4c7_4)zMKz|SZf$$>&CXbX7k1!U5QYN&bBoBcBNQq19umYOJ zbdmQ|&NkZ{{nE}vtBP30+=f&032J>=!xTm){O3F|ZXe+Oj-`-7RBR~C9|!|u+MPRq z68ud9>2i_7?RjnYt)&BKyZOZYu28FPFt3y1O4ZGl1`HwWZ`Ion45$XsgGs8koh25C zys@;16~gvxpF?%P+itqjSa}%mR{nTOkwRiK|Mu4AnyC)|Rb$47Jp%Lk2sa)JB+C-0 zzzPZ&^YN8@W47y7g^?dn?k$(_a;fq8FxR|qKSB$gLQbE`5no(|e{MyBB9dog{0O## z#?S;x5Cn6(CojnjOZ{>0ki-mi>Q;-IO>zl3jJF|yeGhEM^o?V$HNi=bv<=S8{Q+^^ zkk7FM3Q%bx&Bf5Wr9S-<3D}_qM@liPT&iKx-*A-X#cOT$o2$xcdumI==&Z9Z97mm? zc#HMblk5ltQ1pL-1Sb;X?hl;RjMcuBqXH$wuYmKQIzDpjbCL*Ty9xqUhS6Dxe^u^| zea?lU(UE^s|6dm$I#1_axHqLLlQ1Z6cctX4%0)ULs%XwgRVb- zW$y3wd^Y8Yi0=1r%V*v12*RGwb4Od{fT)XBjKrkyp7ceCfxziD0#C>64 zJ*sDv(L^difPGh=#|peb;A^xp_Fnq~N+#yIzdXH{840X~_p?&^RA&0OL0mhzfwEK0 zwF>D=C+}3D`Ck8GeakfZr&mA|-vEnqdabpPG6cS(K|-mJ@%-f3;mQ?vP)JC}$yReF z8o#AJXPxCL$6?%N{Db3r8?f5wfMN*DOvPRNuc+mzxaCJIKnrb#O~_{`}%2^4h$41-VFTXLWlUm#1Bl}zfW1(6|cKBlzdC)OByFup?VDx@hzGMccyfF zST8*X#TBpawp483bk&E?LbAn2%E+3&`@ffr;LYcAXu1HF=X>z+FJR`q#{yMV+|V#& zjv-Zo0gw8PcfCJj|6^0S*?5RVD37i{E10z_$U1`zh#yftTq^Cmoo75eG;+hy z4P)v_81h3#?2sRV@&msU6EH|_)ApZS3d)iOh8?IO()g9@u37o$ypAk2n&*n{1cmtf z=$UG7SHc(8A#`9yE6i$lFlEvdx(m&Nh}_~yJtYElr~qT0Q^%j%ze2_wJtk_v(31v^ zB(DwKolg?yNM*6bH*9cx58S@hbo3Pa+m&=Psb>#4{yCVbclMhY9Ps%b4iuqciydhk z4^mR&<3FZdO{thW1o3NoAavs{We<>9}OPZsAv zECRt)SYq=(r!TWxLD5r}6=038U>G6@;O0@dHN4*k&ASY${T|uJGxbDv(ePKs0SNG? zfBqS@pT8rxd)(|>Y35q-1P#o4_-gE%4pTyd@$q$oW8vN8quhTB%AcE`9|7#j0;>SX z)Ipo*CQxXcNpV?l{q7nCLSq(0we8G*ZZ9s3B3dTDV43VWavB?%^tShc!uIDtPRNCj zKqd0`ih)YR@3Dc~#N985OI%HiA;IQSxcM~u2qkjCbh_tEkTRkV zI$_zM#RtS!l=lUZbY~{p`a@(h>Tu7+3$uFIUKo_@6jq*O6Qo%A$|Ks{v@6e{SbjWmtRC%*?$lXi)cU(jyWyQtC^Tnar4{q_MjrMb5+cV{s@ zRCk$q)uRJC&|sD!+i{o@5g2P!1!3uM;*@xOlD+JHK~QDTKA@4;if~vMD3dT|z1Zt= zGER{p;gNQt_TTXizU13kmp^5Y1^xXT=xeUC{l!eBgr@6=_>`6x${_d+(!F|RMGC^- z;>0@gUo8M!e+2?r$d_N}I$eBULem_r6M^0$D|57cw{buRqefZ?Ky<2!5VPz7yKy_s z#KOJf?i!qHT(nGkryrP)f5p9Hyx{db@3fqgq{%I(+8@$fg}f+`;hRPG-;ry+WDhIV zY%ZxD2zrV+Ulrc_C8vd%#5bHUKYc+k@F~&J-USCCQ6kpKHE@99rxuCslZzzb>FJaU zv567rcU=pYAp3Kh%ri$5f%uszsb&o|NbNJKGS`Y9AN1IjwGoOlNBp)Y<(#*6VjM5; z4`z#;EV_q__nZ*6XW?FEqhAOI=*D5&k^1Kkzt0GF1a2$@n(zjGqaeBb{i@01d-Kl` z_n$nzCq0=^J?U^yIy02!%GDr}3*mz;2KrI1dmrBUB<2pxU@+D>aUGlAZ5XiLN_2Ga z7yWYRR6XC-tvxwd(+o>$Dy<^EDrlzd*OySie`G1}OQI@u_rjaw;I-P`;_pVxKjB1L zpM!4Os!C}vae9e{E7<>iCf4Rctyp%YYhSfe%wOsdkNX@NE@zirru8Cnae=bUzc+;2 z^jB{uN|nAFzo;cy(TB9?E5qaF7Smb&=eP>JTym--$CUW2vV$;1E-}10!ZC;oLQvI{ zG**==3DYI;C27mV#fZ9fyGJ}n({xg$-YUH+rJX?c=d-U9opR@1F5%&Vi-JjIezH~y zS#V%U6MBP zuDA=~NO3R5THgF_#N?fu|J}V?rQEDH>DBBj)WfZj5pRwSXIx&yzqozGXly;EawuQ+ zQI9&2TyjHzjVsw)Ce>k~I`-suEYhXadby){phQP~9R=Yz*=#<v> z_w}9Iu%pvc+M5w#%1>@RpAnm#ZAbXa$~3-~Hj-T1Y11rXX8{8K;j0|m1lhz@^7l!?HiBkhMn^DVn3tIRM2^NWrii(fA zw-bp2nL@iE*n_^sLWqLWw=AByCa=u4k2HY_3huPHXG)zJCmC&VY}je4S`k4G9TJ28 zuy|R}rOuE0@(_1aD2H)#$g?|HGT~fqOCCXmIqaQQc0LSNv#P00khi*w#!k!!yO(H7 z5hEhad(d@iBsleOI)k;N<@&N8Vo#ptOsc$JC2vHSR$1u3wU8jd`#G>=Zp1wWj&?cB zo~(p_xQv@KaT=_D2W3_^SN>RNKE{BFB zA^y(>5!z$@DcxeOV0fDvA1g`2lY^^9)PHZxvL3gQpI-kniYVkpotihl<|m<#4Ub!; zaOqa~|7Hx5yYc<4rq6z2{2!4U?O%+8kwuJf)(gWKzLGzEK6+6pP8d-Svoy20b;*?q$e{_a4> z2Zqo*DhQ=0vp}!;D?VTJ72Du!(`@_(b;@`F+=tk?Agz(yd=XPKnaj zbmZofkfT5dhzQIVrq9Fk?`C}QXTb_c(P6iS(jx!S`8bzSt!3icUUrf@XSuh0gxtjJ z+JpDaR#;eVMUNBd;!}gk`4I13mDYYnT`Sy3#$6aioRT%=dTaVV_?_ptp|)-QTKJ$y z`T`A=koW0Xw$inK@Io$}h5|Cx&pInrdRb`l{%@`h`d_W%iYck)c{-C<9VFupCdwES z{R9+Fwxlf1>Ru#oV`L}FkJ36Sp58xEC|6WA=tK@&`KIWP_!&px{YqmkzrX)3u?W?(VWWIM zs%YCe?dk7OI?S`qQPX>;?&&!WebEvAmxo&A=kUKDK-@gjTClKL_ldnL5jwLr^BJd~ z=C$(&4(5HC;P4H`zV)uHG#p>%Z^ye2JSCQN=2J#k6-qJGT<;>j-H_CFNBApvn;L1? z5i&0tI$Srai`L!nbn{Gqu!~~S_bly`F~6VhCz&8DVZ{MEba1OF%~=^lk;V^4LWrkZ?Lh0pKO{4k9RbqvN{ z?{vbLV%t=HQ#M7}oh>s`%evngUmBp+-!IweAVVMb$DC6ZjeKc}T6+dVkBeEIEy4Jl zw|->DRIK^KGLJ(soelF*SxkZb><@ z_(IcTk*7M?B~>;)V#9zX6UXhwlltxYd$w$Ew@(HIChIq+JqCPE?Q~Z1*Pooo)|Z^} z(d>bC`!jvqQYYk>L?`breXglEaAzq;|JmaXlH)R`5q9FIwvVm69>+(*i9!tKn^iZfF&i1#!wMa0hq9+iA6 zhH2*$JNb`pSMS4J9=mr8V-753;WqRgic@Rt3XD|hq zxmcpE5_4@Z#4*d2b3?E2Cp@xQK_k)++pcrl2|R9iSMp)Y6_0>+ zSA1bl5Pr1u84h`01!p65hDp4_bV%-5?p>Qt8&qn&rl8MKeIG2ydpIq-&tWT3zopP} zDD#uOyHu!W3f9}cd&u$y79@{rQH}tUqOckKH0iTvgnp$ZPxWV#46mIkM&yAq_L!=i z=G`2dv&%vQ&yZ;qZJ1QfKoPUzT#Bs=}FS%J&+-Wx#s=Vgx5#hCVrIDL;Lh zFrV08Anbjt#$Rm@`<@(Lflw`TQ-Y+S9%PYXI$p@yu{z>)t`hx0Io0mI{ADyQ?o3nd#g{5WvovG0pfOA&!B{89%Xo7id;hx`{T-$h3ZoK%gy~z~#6Qe&^R?H%Vc!07q&WABPu=&T zQHhRipjN2k^*1ZhmxSiDa=g%2{EO#=tmL9e@=65^n!<8wZ14ClO^T4@Io`uSzieEw7r{Phq>EO)G0alM2A7|%KKv@q;}gR;bCX1Kug*00bluCy`v6H zXK&8bX{OGQ`QfWGhvF7Dc&hqoMLK&0yIW3KLO2sL16La%sl4nvRUq(5?zhq*6Z^m! z(-*3=Uaz_7r)nELJh?~hu~Bjw!!O~(-?2?m#3#`?9~(s0^!{#=cPb^eeXZHlLYc)= z-jU^#yVRV}Ijm>rUfWyP6@sDY|$7}dcV+^|jX|t&TbC0|PpCBlP z)3AJLF&k|$=E%PSU>-HVBI&<_p!U1b2NG3->;=+Ht5x;Ylr1Z z?&hrIU@pcx_jxJ9J54@8(ZBR+F3Y65(P`z)Yt?#~b{uN_zCSj{Ac(*-@H2Bx`X0ca zCAp(t%%Yb(RAc5gWhM|U2rCX|8+)hJ??ppk`wo4_!c_{&hAivn2{t}2njYOIG`|lB z@_#7gyFo@dZZ7_3LPP);v^pH1sS0#+T zS{Xa*olUvXA<1~Tu6mLkc1oc`R-EN|G;gZ>=E|{N{0KJcMG^^Qpr+x2!QuZo7S#GJBwogO#+QYX=PkDo;e_nz&PwLk+)Uj(`|--+a^8!Ywa3KA&QE zsG)(+DwfV-hs*chUJ;v&A>n$*qmZ*%@iD#%P4dU2cmplFLHp+oX}IIG($+P=zK8_< zPuti^!){2IFNMg^m<~3(X}nZA2#{Zcw$0S<^4iZIs7>8YJ?_zyEc-Lr_ibSdoA(*M zR;oGUM)|F@)8#ulRnaQFtxuX^yB7aZEw~+bhJ@3V=uVZQ{+ik;AIr4G?%W&HuSl3g zV;#T~GZ9}jD$pPH22D?y-^-^s7F-C7anU}jBg|A2q0aB#a>d8f{7^mXlsw<-Oj6Ba z9zKM&lf@t+kl&sSnc~PK2`A}nI`9F%Fh`zx?rD0SYSYRx+VPCu&4cx{(+6krw&O4P zt9$0Xu23ORc|uABR~{RrG@+^u>87tQ@|^f@QfuAxGQthkO!zESLAK409o^txg5q3k zD>n#oq>oMg1)iwOKCV>o-*NmRZdLD9Swqsr7gmW--pQ*3(Ulp zx{9t0&?&tS7$;Xwq{j;T9%MfsP5bmzr^Nz9z4)}`D(i+qeSSEGa_X%5mgj(X5LSzY zdb-3OMaAZScFoWH{9=cv^{J%I?jG2WFYFfa0_wgxjF0U<)%kd_Qr~yf*aW*2NBm`O zj6HI$ffDfGn0wMm_d)2lQ(mdraBQhc^#9@wpOe~T4XHtM(SSw;4)P~!rLeoF$s7mc zVpkXUa;y)mZ>PoGVAb3hDTK!j*hS&S?39>@(rq{Y1a1jW4kZm-iC<@=U=ib8Ap z9_CM{gv3``=`=~o>{H_Dx7I&sp7gILvAy_6Kk)JajEeqWRtdjc@5O-fOJPR+A zOFI4}@%rt6n|W`?nkZXho&*A*r$LgzjODZavg}!Jx>!md zbVfJ>pkfGpSOZKJ9$f%+Y8}Zq@Z3ekHRmTP_ztQ`RQc8hfS@PZ3|v&{tRGrD0PV^P zLA2YKa(yo@UD$b;gdZ2H*qR!MP5Ne|@CT4M733+hL59DlbN*I(k$-3Tss0}E<4G}c za)<(p;%8+{FI&IO+zy=X)WrhT+G%Hs5&*lr{n?s0Js^+Mzaump^~)Bs3`UK3ou_?& z6E*jH572Tz9vQOxbKmo~<4z5NFJO7VJlWXeg9SIkyozy3`JLb~=kG8jt zs%q`phZo)5odQaCcOxMoAxKC|cXuNtD&46d(%lWx-6h@K^;;evIp_WU|Mp-o#@>7F zb;s;$&UH_d(#SGS?mVcu^&uZmIj~6*C}QbNsCz@mKO!sk*+9FPF!J4xQu+f~A3T@? z_WtqE$ITAe1WGOBZM;gu#cTwtq1!ohelv>5;-o|3@v;k>?j`3Ch0p1d!qr`C>*n}% zlBv?y!eEoZsHovd&>!*3fwgUU}{A%++Wv8)2Hs>^2HST5w_5EV{Dt;qT8QV3I#yz9%D_7)hNz>m`{fs=3lK;hVqqG zJ?(8)(9;mQ`4X|L*KDrj?Uy%pK!ahHZ@98b0MXK_ote*8qW3UQs|tgMY%$I_@#)K% z6DT={W2IZiXAV^NO!$hug;TjuzL{nH?ATZVi`fz7ucVWsO<$#BK)z%-X3V`eU8r@J zV~JfAYF#Ory?d7&Ug=H$_btSP`ngP|fv>VriCBYPsK64_;`9B~~c+Vw!)W+rAa{pcbr|JW`GMs44~wNe#RRNF24 z8GxD@`~%gPM4xQ4>0yC(;>8+XH}dCt{`}lx>Dh3mf9T8hTIvpZ+q{rw`;dBNmvVbQ z(|cX%tKK!%&-7dDFVWuXGJ|$%si*03dU(y==b`)aZ`ZMZ)TdM#`*hh$ZKpgUUnd9S z+TiytTP6pTJKwNVLm*D-@%?JOGu%$OwY;gb1^YP4m0zaFgP{@tLI?fufb*aiDnPZJ zwH0}(^qy3;H{!Y%G}fS-LKQbAp)@c{vAOQVKn(wD&4#oGeJ|wb z4fN%~m_uxgOa6V@j1z5cF;!4|`l(ArXZh@?D7w#m7DhlL0DduAdV~PVeHM(zafJp% zq9i8a-~&ukS8a$RSoQ~+=`L%I;D?qUk&)|BG4K_bOf!jygJ=a^+Q(<6J+mHdHQnc& zL$bzp1d}o8hpmUozvJB!AB2O0GUe`5M`UQ;x%lsSUcEcY0XnM_dTOh$sjH9UWhe{3$QY2cZtsE-I-E$3D>uKeE7k;>*N4$~ zYkX&9pLWQC#}kDe^#wVGYQM%pYXzH`LC``OGkaQ!S$q}V^!ZH`@-MBtr=Kuz{sW)&sNr(~ zJfJa<*ONxN7y;X9lRkX|SC4k%(Jxwh=rnk-hy2vqtz4U@Px9G4x-*&so z0LmA{TO0>HqHMP>#vv}M!s-i&ecJsUi9B8f8TiU<51u*c`zO-1|LC+I_MR87AmhH7 zd?S-;IKlj;1g(z*QiUvumMAnOl-K3az72*MkxM6qNo=%qt5_V$_rn8X&Wm!?!3H87 z%eDx&88AkOaE*Qd^ckwnJGin5QYd)ew8mtcAKk9lBnWrivf7gBGUPue0T^^W7(v)e zRG>Z)^hC-Z=uMSRex74Dy77XTpDOpYY=O4Z?$_dER^c(S$I(#HOjmV_0(9|Csb4dU z`ZRXu*sB&vAqI)ls1`Dni@&CRWFCLl!+bs~t~Bsw^0B%6x!1Zo7=nl{xBSA`RDQVY z9y5hIxLa0Ov(l0sN!B1yAs@Dv0)hqG9SinrPb?}@Jegws|2X z(zdP(&*7K3uNBTUTlhkyJ|?JQDeXy4l~?WBfAQmoADa8|UR=Z!$Txx-cMdn6bzz}! zu!tTt?ov!~EO8Qge&}=ZDn5ffuQlMx1v32}0$x~0saLGn5~a+rkp}H*D0_4cP&W!z zFl#A*>PM-0vh<2I)@wo=X-Y3?#OCN$VDYk>3+vMh9)m8=Z3~OQ7eA!umWwZ!ii>K(0@+0kC0ckCaMhVMX>cZ`_dMRYUq$E;y=}(U-BepKD(9g|wLdHX0 zDNsI_RKP5ruv8~Ix57+Em+?`^*JDp|(v%s8VYm{KDIZF&F0!05p$4l7d|6sq8?=?! z)NLz^?16Tn&uAN7t2OQ)jyR(Z`)m6hT40CT31_cJa?BQRI8vRtOXp$hOq)eOLjYmM zf3V`13`X-qTZYJQ^n7lQ@egzwTteDUy`e7{jYpKovaDv(&2%65!KfQio3 zeNKUQK6+yUAjX1L!R!_o_sJ*z;q3?*@qwX51<512!@>1T%(a@}9ZI-vbJB+DLFRdm zuzoFNM|m>ng*1aP0{8g*BiSkv{<)n56v$uH3$1pSylPH`)}UK6uJ!4m>-x94n-AGW z9vQ{6k%0s1O{4A}p!S-NghyM2Wl~r1rt2f-NlYW%(!0vNJ0Fl9uw-6`qV7M9IzBMo#_YjnlY8rcXHoAxhFz7TRYSP_0(8|n`dhOc7`Suw3W6&yeSsgvtdhka5oN(|~CpUZh1an1%ZTP4dir1cL7!j!+7ENK%>}N+Odh4_!?b*7_ys{?-CqQkg8VmkiXcuX(T! zcCp7oC0fVrZZ*QKVtgldwfULWY&X5UByqiqr|j_9%zin@KQ>g!c&mCN;MvP*cb8zOFdbK|(&j{%-Xls0GUqAtIHBF$6Ip;MO% zK6H!IT)v~2Wq&>ZI@6C(Dmc@-Ot)R3;xvMd zaxkm@@U$5XHn5j;>UlFe#j#nUWWsh*of6+kbQyr6wK5riy46m1-R40=t18BFAaZJ~|5mh1NX3(^l21#x$KZ{1v|IfxJUfGw)cA5bM@OL*Y*UU zWMHNZYoWAqe=LjSE}Mxi7&IQP#9g266XmXVtXuKZW>Y#bR2%dQf$+gIW*^rKrNyh} zXgEA7#4lDPF3r6>!y3==^t_ZB;2 z{|e4rU^Pm4j@D~IJdNIMJz#q`7)Ln&dVERVl~h{pc=xcIKn^DMcUnQnu=J|3FvRK* zd0n~eqHY|jJ@4XYabyzA7x;?ktJzFK;_qf4I{mzUNHhF)2~FjTnzTT^Jv4|2N_j2V zOO8%OiMFyIX6&^`1t}rVajR5-V+!sXbNSrZb9p)A?7vi3GK&iNImXIpNQyO~&Ffxe zKFaxO#K|^yYGm*kiH_E6M6@v7Ogbmhu=f219V40oh+F(dBglQmuG?{W30@9GOi6hO zrY*ERH2op~u9HJZONUE-Rh>vpon}Kz%DROuKmTBJE)>kk*L* zm*sWG5ky9-oL-$Ds2~lx8r1~tkwDr=0~Za4>04>#!3~V`b`oU>@p~Ddl>$*a1d-*I z*t3gL?`!QrLolZWk+HJ(2r5gtgDQt3$2cgd9nXUKJmpQ*^B%@CwPsN;V+U@$*5kSZ zoz^V`sDW0t=sD2)3cI@Gg0oy|&pYQ7ejjTeAyIk@ymd^MvEs_jw|yS7|@GvNb+ zxck0cRr|x%a6KR4x2NfOUdP!T&zBqxTMfLR8F7LV$17P)G#pHY0@UdKzwgI>5?I4wr#0- z5p+kZt%ZX8tC=b4FO23>aS{-?5p1|yw{28?4q7hPB@k=Y4g{!PGxU^fr=8w?#(B4s zb}x1_OYlN}=(Qe6xf19R57AkJ$;l`ZvZjw9h8p|U1M|m$3UZ%p+mfQqInnPt>|1#^fvfaf4F;veqt)jyH|4z z;ys5o^ZWE_Fq;3=YeezT=CJ8lN~bIqI2oFt|AsYRqoD~pJYPBNaTS<_+Zj0tpdT*s&I>85#MZUhsw@@1fO&_Aw`5nIHy%x( z(v|L)lZT#Vhjar8iSH zW_5i3MybI^$AZr5I5?ua@wJV=!3d}rd^7 zYA?b!#*#0E%~DhIoS*_oE8Ry^(Pnl4%Qvnn(IT!4IDN)~pivpXOe_>=k|lS4Bn(TG zS;Q;ZBRtP#UJy}V2;)e*Z+2vGMeHMyYu>O*lsvZ3P<^jZ?u62qGMmx+&kOQY^>_Z4YJfFP$uNDA7fd2z>52}^d?NlazK z6^6ZYiY%DNAK`u6rq~Rxt^e7(=!H0lF_;8N2bK)2Sqp~zc3(IB^MfN+;5V0Tl0qI^ z-j!W2lGvquB;J`SOwVtO&EhVmoW--Hb2Hxi*#zPO3^#`%=3vGjEV8ie!BwDIp#c+I z1e*e=Ts-Zj9EL@9gA2_@kJrNgBxkGY-DeAW=bTZisJP?~#p_*Q=kzBVg(-YigDTRl zq`}Z9MM$G4z1aW)1Z1hbUjZzp-CCMI;04)riB232w-clmC%=3ad<=`Qs<;Qec|WSp z(qoCBia%X>cqt>1wg) z(1BVIYXwHe^*_Pd9}`O(*q$GPVXhgGi%Ctit|9so~9wL%_%;=TuEE$jtWcxx7FN6-Fi7vqt$9kjSy)VNZo@q zE^gC^%@4saAjtPl#)K?~?dK}5g3%OHHwc1X9H>e)D1qe&vX4|^_q@UiaDWxNU5~}@ zniP<47s~ZyOh2M+fJ$t6|HE^=Q#<076pZd|G!Z_^84#6Orx^ItI>3l6;#0E<|5`?Y z*~3Fq`GloTqa?VpZbAJTBRJ4=!s45H|3U(3w z+StFjfmKu`DXhe-V5CGkja5(|oAjYMAg~#$oy#up1T<>20#c(>sffDMG*M>iUFwa^ zPN2nP$VZqX=me{~CRFV^HuY=UG!Ixz`qSM|=an)0%;g7YS6U}jV}+4~BD^ zhi=M!Z-<d6R-L5@@pZiFn#;seW*->ZUD@&ypbY ze+BLH48;J6{^}+ZrCKS0X^F4#l82Mb#XP@)Kz)#=u3#KhFu~=DRWTu_{)OV_BPR^T z>ExT*a1T2d^$5))!xGR?b5FXmYud$18S$@0248gMB z%Pdca^eXE60QXx_&>r*$hO9oGuSX#3{Bf74jDKyLQBvBg;WWD8@YXBAs7y?W8&jn# z(Mc9&IU3*IRWer$rL?FK8(m=&^p^fwK|(S^j4dhN*)9 zHpVp(Zozb5v-ikIT5v?h|ME21f++u7!`t=ycK>^wFy%3}{Cz`t_zn?V^#5KBAO7!k z;C%o0>ZwooU%uk^FaLl3(Wxymo0kbEwMWS>1LXFO#McrFRdwyG6@MoaO6)I=3h16@ zzDF#>!VhEV)x>XSvE8IGwoeD_v!+V{ZIK*8sH` z?6GT#CMALh$GB9Sf&He_OWw=o8p;|nrYB|_C^zS$yvkm68eG8)tH)iP#y8DLwP;w<PhPOkEyeHQWWpNy6RNYIugYbd;6`eowhQDL1CgY$lI-jG{QoJ z8uu>>h8B>A{si-v;;#|^pr^n0644MA@X~^4@_dP*kUV8r(}<4>S7kLeKkRFzRYR;B zr$leKvZqzEU=27Q&I}X|oClIHQOYK#>@l1;uzv{T;HqABn*8*2plkCI12rh$Bfcy}~&jHkP>HDIs`qE<+Wa}Censjts4UzQjFXy&*wx9KF(###MtZJkOO(_B~ z@^7HqDZu{8D>9#585G$*8kzJmIq#>sP*2kTow~ie9VGUY+A6tNPQ739Aa=&p_zU9G;9GfMeCP2m5KFH%Frk$UV zRnH+ZWeXcxp(czNelsP)la^C*hCQ3a7ri$7BuI&h;oqXkY-*Nik`Hoem zd2U;Hn$c%pBt%o}QfnK2TC&Zx5$WjRf~TRPFcjxzA3oGki&br7fe}|MUhb# zsC5Ab-zA9KSLERy>-;Hnrz=u7_4JhJtXGcR?2=&@MSNvgc+Lx-#EMsn=WX6czEhio zJz-zh5~4B3;h9k}8)>+_c3HRAI6;z~6^XhHe)gDA_aZLi{ag^f_rQ|-?Wc~e)Fv=nVBLal86V+`zT!14SH@FlE{ zkg=dD7?AqPT#ff<&m3%Q7Q~FrpZ6*8r|!*D&c7Ta9nGgD&Z^A+Btq?P*Ckh%bUCuc zaSUxgQ{qOiaJ#D-`AJq_#d*&ay)Wr!?vuz6d!*PQy*Gqa9MjL6lf${h&#BH2A(^W} z+pa187B+2IsKsVO%K1{Mi*?IVc#NYL#pvVy<=DuWf|WoNPU&>B51L|i2-Gdnn3W(Y z9%(gFJTsAo(a*`}|H_IzjpYa1(XJrT4ig!Gg;Z&HGOplauHK3&EbYgcMXJbUo>N6d zwQ}R0(Fx=XsD+V{<;OZWkP+?e5jwLty4u#d>nU*8vk&!pzbCQjYBzs!r3{p{d8*gF)%4~uR4s`@kLy0&@Ih&cBpL@ z3aQ0KA=yn<2k2W^wvd3l8|otJVBzE+Inn-$G77Dsd!_oEJR*Hl!Nj7IuUEBu=Hd69*K z9|p8fJ4pFyb#H5jf_}OC@D#lgnZdF@fN+A?g_sjg5KsFOcR#Yoa_``ouz|rW*R91R zRwYI0gO!|#nxEBJqXpc7zgR>0=Bl=%Kwh6DB$`_6fG$$C_P$Opi%3GA{D9 zHf%ZA=}J*3KyyNVHkFs-oC_nolhH=1*Pj@6a5zS`A=HlNUCap{g2KMu9#u zhJkl>#&}76#77L^qeL1R!WX0~`Y^t(3;G*V3 z`B3t9E&LPd|NdvhCJ%OSFNwtkxVpZQ+^omPE3+EH;-W{=!b@jOdyR;XIW9+ZZfzkL zd@P_wUUsUtOQZ?AcRKsw4b(S&`IV%_VdD|*b0f!TUG1<&^dt~Hfw|nbT{H*~I**KF9CkeN^VWEsibPXavA{8GKtul5n66-&H~jT7Z@7+) zFssLc<~CbT+)9xHFpHC2IikiA`M8J6GHK0A$81YCMW`yZt4HSMWQ$poLsK~`>Hno& za6&5MJ}~Y4;-yVPt1#d&LOCzQOjdh`sra@Gz8&bR3X{&JHncL{v!?wrV~uPG`P^5;=z#bg6P|Z*d~@>|CGKE~C_`dLc(= z${fi%8x}cJ0nxTHTg7z6!TDOFS{)wwfH9_qIG=`fn!4FyUB7S7+IMDr&59gw zk^*>FWA!=8TLv}=5X#BP&T?salabZ#VZP7%V)%^lyDPCNT?D~F9~)>v2a+P2O=v(h zM%Wy!I@si|amljCi$QL=kuF1X69owbz-?&V3$>%xLDAV{P34Y6e1> ztkDz4STab$&<+!VOr2Z=^Jp;jLvaHni>*Pq?L@cMobwB~oq!OJxBoIl0&PEop|v^b zH52y?6D_YUbDnwB4C_ak;mMJ~D8YK(5;v^OAf)XhSBRd0!Cz%x=)og9F5zT&7N8Xc z5H!3E>U?@5U<4X!Bktkc^DOP2ZrsghsvH-sQ;pOv^BVF1HK>%qVqCFkU?6axx;H0i zY?WfmZmiA$u93X}p0n!J3voW~IO4 zwl-}t-+atipHYGyZ;lxu0e>Eq_OaUZlIh5u6;Do1L`jSxvP?hfy?IU+CQ|Tm~>S|H}~s$WE%SUWFRZNl>k@@=r72NGmYLVQyj3iVY`G8f0Rz7BS=vLYy%=`> zDAUxRaMjs(P2lKByB+yt1KaiIIt-O#kMXOFu?|s2G0V*>P$C9NxAy!7=2%MX_yju= zrC5}CHU$5!RbuVP_TAh!h9)D(xvK6xea&y=na0mR5t0aK)Qpp409{nXU$Pw;HpK0U zFnFa_#GUz=NO-xVe=>c)`4HpdkA92~om~^HhvyhDdpVKXkYc_qV^2v|~%;IBk+MQB2=W&r8* zuUZ;jdiUH+V&&=YJkgyE*HlXuA#=K=YJru%Mu*4Gl_z3sx03+bz);^X2g+Ag{=_7u zGww)k(KE*yF`xPAhuN%9x|cb8CLRqn^Q7}aWD9AYvm`Uq_!7u?jDtF}+1`A7*&%Lq zPuH_yDE3u#zrI`5$0^@bsW+u%szxgAmehvivrNuthZA9#5s^)kb;#%Z7UtFuJq4Xc z(`?G0=5Ove&=jTj9YyIq!(R>a(eO*6{2S_`FgQR^mzgvgFJw%zs@pH*X%U<&bS&H>?rpUO=(SAzf`CqVa3^#OvGO zEeKLzeh}(d@F=A9{M#K412p&S#L1p1e_0O*==jVHyd`5T51}+*KzdjjCoN1 z8tLjnKjNCAPc3gY_Fl+KnOB`b%sMW8XY^h8iG9z-Ez2z%BqM!TJIC`LgjcaxE#EzY zcsGPLXkAq5R?$N0{u~equJfZyr1&5Zs0M)=-5oTy4mz>&@ySMI#dOkoz%XQ*_p%<3 zPp_$~r?^>0d9&~AB7Wuu->+WiX)gYQdmp1@mhvp<;_g)IH&er6E=n;qsU7={zs|2& zEpZrdC+#T|g<9AtY79=z|Ag3~6|xY}CI?SXl~ zBu;uhl_^tP>QCZ+XAxyv@i*5qGU}lAaeaip!T;GfJe%MN6Ljo!c>X* zt)M8T7liZ^>3DS@TACP}b&KKh5z13=)9=^5FcfGZ2K((kmb)yi*r{z2wk#zU&E_rE zuTj==TSh!@%?m=!3q}_shZ%*ZjzO0pN1*2BrVhkZ@zrtP8pz{@g1rp4(yDSVrZ@LU zOuX%Wll|6QLtRVO+0|!gHKd%~)TAl#En~D$s)JA?Y3-O?41W=}2bP3~g|>SCB2WinGd&Sn++S3% zyS2;OaoR*2-_g3cJD}hv)O2MvrU#muIJP7{vU`vHSHzO3%^>RD;ZVv^)laYVV`EG= z=)gl+{`VqRL=b^N1VpkCQU?o1T43}CHj8ZWm82-d5NcEI1mu~S#YGJ$BosJf8T>*| zjhM6%VMLTn@#^Mgm$&vM$?*dlry%0T?6%(cew`3g!FClSt*CZsF&n1h<|nD0NtBs% zD|tT*FaCG9Eat)*Y>3v(Z*^JdasX^4VaIjLDegIzhjB@ZK{27I%CaL6Kc340aFlk!mr zIx|bF0QJHk^=#2nLw?_9cD0;OLuYI0VEX#I(m}YIy(poEhbMh8I5#{VrO{}t8D}aT zKNgm@s7p=zjDk*<=lBZJv3lQ+S2lR)P@*UXRoe!(ZU>coC`Z~~5l%K1Mk=*HiHJU5si(0X3GHNo@GP3MC2=ZynGco9&gWR%g@u zN4K7~Jj8%dKo>(B9y%ZzxaBM|^$JBaZrJ2d++Pu?z7IuGIb$O5M@7 zpLxw~5%)u4eJK1hL{M*5%q{$?Ye@&(p-xDcD$Z*F09IM#hb-JcCUk}{zJ!Gfw}7`W zKu%7-bpcHtWCAO$f}LY*1dO^K{%0_5aImFKN%G|ob&EjCpy2!Gh#AN`o5!%nH*p*~ zyv+ohw&IwQ(DO{L`>98cE@B_WgT$akMdBwAXC_c3AsVuL9kr0Ku&?|@T_GXP+&$3) z{SawHVc>s4X~@6O1&E0cQ=f#KEW5N z44V6Kee4X+8@7IaJ5It$0l(k@8wxu5uMXEE%Ll&H@G;or`0&AqVepwZRAp>2cUY1b zV~RXvJeu5#+D|9z@a-TlAI`A4mMnfftEkKzG6-2Z456eqvwRPsV{K^*HX366U=j-N z+ze~uB8W{lyn`(BlNUBP#o>75=G8AIW?GbP>OHBcAD18Bea1lk`N|Qu!r75xz$94x zAgH+*N~HzQ?jUfD*m9@_cOWhBc>XC!8u1CTiw5@0%mXu~L}RJ1JiTQggezdFmLt6S ztK%<c7AHk8s+h>X|^wdSogzNOYx z#YKf{=ik1q5)y_S51ar6`SK+d{^#Y>8*)0tep$EU(8#}B7uqE&;2A%Cr^^i4N^Yd( zx3n2REgNG@Fqu+&9R9btb!cdpK|g|s7GhRu9%30o`ypD(Haz1Ett{I{Y7fR!$6pC) ze^y$co}Yi&k(c5n8?l$ytIxwK{$K{z?T?a zY(zUlNhVzT-`jCG8_EGM!u|LBDS7ssp`kL@(lh96JsOAc$0%rWIyq(j$}1&dva$w5 z+%tsRi#<^XE5}R^L0G^+nYF9#6MN%zvqf7og8Nb1wcbWsHv;9te-;y^GtXG zq|EbPjWN^+06DOQ1dX8ycT`UY8)dPMk^TtyPG{?GOeeNgsa)84wq96TSq8u0O!N4}CQ(M>n7-kVbnwp} zAVp!AnxTu-1IYvP?wNQuxjdV?gRMPQu>)T|jh&r|B8TW-=AS_=4oeOXNe+}@zCg_y z3wys=?0HJ+dE!TA#VG&quS!IzU{n}@zk&iu1L&7%>MO`X*wPWWhO$-?+k7_B#;gxkl%P^sh5CTqoN=(S$e3X_xLq8=MB#tS>lrwtRu%B5LW=Nikj(vvt?{^!x z1eqd!m~jyDj9NlL6#w3{A@t(sCIoPjd6y1J9#d!kZ|qB%RfBO)N(+c6WA}tZilUE{ zWWNh<|0~^;S+3~kgcyVnF~;QRY8mVanVbpve1D!1`h86}6zhPcNWo0#I5dfbmY+~A zeq8_Px2UIJKJ=GJAUU|z2E)7h>{}n>J&5@T`Tp+>N}E9s;w0*ki_gngl>qH&&Yd4| z!y(}zk!3rl-x>suDRWP-K7FU05(0{ml|sBG?e$JzW03pPeJ1S0Jd8z&(wo04UoG_%6l?P1rrf z5+l&V7d4VK)EK@T6Qxg|DyNtWk_z9!%R3SO%X1JN%WBdhPw%m$#zTLpb^qrBiShRp zsw7bVj)$(S{`p(fpC)|2xsE1uD?Sl;lI|OuXRt=*1%~8*SDl4Gxs%Ta`S&el63Z+W zrlcpfncusTmW;7>@>}ws-aTc=#ot9nsWZ%~aFVMLRY~Zd28@iosIY|ST(A)vP5zh& zHi8f&{|V7k2CX3?W)Y1xgJ7bGr5cmsMeLXbK{g3DvavS)lzq*jO63}O7pz+->f}L- zU60oF-BE-JAY>%mE%?ZqjSZ_=>TV6R@BuCFtjL6BXCmhN4!f`(uaSKLI$uZ#|E~~% zM>mqLDDo29v&!3wW*tE139cGwZ!cAA_6M$OMH;WD(OmrjQE22Y) zLWvF&%K>sRI1pJ(5OL%3)TT<(Vyy`1Rt^-Qv%NluPjq_B9k zCM=$AFu`NLnParppAe9hizo8QMTl>o0afhpMI5^AG=O9z zY!;muI!tI5tiKdgq!g0qxX8F!=EzRQ$PUGO6m%z8Ge;UTWD_0|UFds1ip{YdCzd%( z0|nV2MnSSLty~#K-N61bH_b=y1&Kx4zkD_lA6s%06^|3y3Yj_rt!xuK#Q1Kis?iq+ zeMbjD_r60MLF9Oti%}vHH%lEUGU<*z7}M9CJp~mFQW^6z=BA@Lwp%RBM>vteh?x}le`kDhTWI>7^g#(_26n@6<}Q3=CkhI#+4u>%Nz zM^1>WsyN4`HCY`>PYw#(?&L*XcO-dx{2Mr%Hz@ua2hUL?C+pD!``qc?v6wXbf(@1( zmH=Qp)uY=e=+EJx*k7D;NGzDIo>2#20WpmSd5#bbXS6{L& zik>|bCcRQpiA4po2jW3WKw|>-xC*!nb?Q*V2Zs=@!NA+A{t{ujLg4BX!;CmCcuT=cr%AVGA3w)G8^TA9F;YI zF^7eg;@f|&LG|sOafh?&O9I5n#o#)JgFyOAB#xpRdiy1^r6GZTxNAf+k`6-CXYz2k zY47>D(?#9xFH)H)e+>wrDuDi%w{>{)!=**e$x@AzKV>2zzqdE>o$LUMGp^xcx)AXg zuTc@&1+PscZ}52PX4Lic@q4~?U5@lSa%=^Foy^NptazgK3Wf)%s{7zo3CY6gWXAEC zaGSapgZ)w?#$-}17U7uMuFf4Kac43B)E+)SBkYfboPDc1quqLQWk0^^VftPCFv6+l zdO4)WV>1s!ulgpI2(GEHFq%Ysjf(2oS>{bvdaKaw{?}WtUIjD32Mosn9~5>{A6GRC za<0@HJdR>d0Ejuh{zAo+Q&QzWppg#;LEyp*XV#`uxaXr_HkhpjBuGV7OwWZju^ zJ4t$x65AS9cj_HED9If4BCbpI@jK0+lcA~MUrAI9x&{A62Rgd zM{c#d6T0dkREPRS<1LgVa@P5k^vB1IA!#SCe{5xzD?S&5S^6n1;ozMhKyJ8-gOLq{ z0pyPZ=~Qlq-&9Gul+ABG`9#+(B&!F!6&54_G7bksub0x(`%9wCo((NQiMu?+?0e;= zXKsJ?m{7Kxz`IBOy?2*u^3I`HgF?G+Ysy~$Kw|n^7}4-M!b@7tLWH)!s>Foz;g{(C z?oQ3M214=*4z|lgOJH*b(npzo=!`6cto59b@k&_Qmn#-78O26Wv6pG#4KCh@tomw~ zx!@js;~(r?JWNNE{JcCk19kh|CFGv;z1|H+l42tsgvIUGU+^?%S#In103hxH@srFQ zY!V>3U}K$v=Fsk`rt8HCWSn1xgir&YE{gnf4Iomk`F~5zTI1;$#Ua5Tagz9;1>F&^ z61_9Q4F#w;ry~^68;|-^QWBzQPuS7jPO-Ca4?>|6rcO7VZ9k8C|1L0E|K*bVam;<= zn6UaB0TFYw%)|N1MUNK@PA>uAAVp9a$$}%z8LjH^1fTPo78di>-RNOA$)ilDA2fE8 zGaM+{xF7F!O|6qYfC67w{PubzKeU?k5#D*B0SuDGNEhi&MH1*zrxZcm*K;&50B9-Jt;|F z$fzaSfB6U{eJlljlM#yPIH-nSk7)2iFEl6o%kZV-V?Ve(?WvVMJr^AT-R9b?0+Qb^+m&j51_yrn4aK$~x)CGt!ga}O zM(M6D`Yf8{8(CPq*?2)1>0VfH_8FlNW(&_jHQY8@1BBf+V>&rzd0otv47tOl^+UxV zD}vmCzvsqM`MF)aQAdF^GBC&G+pg4XL7X(n3Vw-mft8nybH787Pl$~B<*@Jxd@Nrj zpVh8pkH@9d*`TJ&G`=7K9H(N=%ib!HUhpAUv5z|s z&tp|a4<^FexZ?oic8?k{w-3=2Jyef-FboDmY+ajVz*$s@sktqRTO9Z>mBfNurWoUb z!=l*id~P`EHt=~aq*7B(|H`AaNW9>!`&i*`{Rz4g zj60P3D?39CL`@~oU)(|2*h;I0CFx6RtDt=#J;iIy_y#wD2R24nKSY_?q+1M(!+P|~ z@yaKD(6ztF*l5#U`8h{Wy70*k9o%fu(_XZ!t6X8^#P_&AtU8;OT`p|KN9gZ$YU@Wh zAI}q-W0a`pT5))6yDtIYjmt?1LRnL6q#4htH6StwCrn({d@!$lKWx`u zF0){#+y8!4@9rwXVCrZ$wGpJF8Qe%69`BgYR~^Xq zA0Wt&o4CiF&!2H;IgGpne1EuEC|?|X%C2kvC}&1Q7mOie`C)nrB-d9-4S~el5!DA$ z@F+%F@M&=2_Yfb`PA-+pZ%m*W-UeK#wQJSDq<+xr_#UDgAp^WfB1iSS_fT#|o9Fe= zPrv7aB;uF)0_-I>=~~gMr{PydcUhp+SIbc!@(R8e_IZZ{wz{oUek6~nI)B$Q3ygB> zxlaqH1->*(hXvX6x1j=Bl*=I49}Y(-?iNN(o zvOl?n6!D)>bHo)z7T`mwF8Gjy*YHrN{8}UBMOs9_*P0899k8+45b~TKR>t}4vTU(` zxh=i#Fy(%0pe}afN&VMNR%aZsRc?w*Cz^^MU|fv*uqNd=ME&BP)7TYSg zT%QQ^fhO>lpuKF=#|wIkx`o4U%6+n%CNE9eRhr@bbAp(2AqM@ej`Ai^iMl0*U5kDT zyWaLDeA1rn#-VSnf#`UX*O<`jxiC1z&}j|OzkwPf$AJKCSV$8$5`^}gNUV8lAv%;4 zcJq~RdzaDP3;!lsIKpC>aJqJoZ9pFx$Z+b+25;QsbxF$_EsPoNi0s|`>4iFFrWFmf zA+!5WVc(@mM2$rz;)Xl81gyBvP=KC2RRL2yL^wV)I^k;*YfXSvUt)M3#br>phW%2y zh;7b>dMLwmL;8Ka&1`;*(;~ukyT1{5UPG&@UgTRz7Dt@6aTQcAJsN1)=!|bfplc}3 z#?pl@DG;etB;k{w$4_!2C#?Ga+I#D-D8I04d;q0GRFFmq0VSkFKtejC8zcmj zlI|Q-O1f22x|;#184#qTI|M{}=#GKmd*s;5q_TKk7 z1)MS3U<*x7l8EdR0yVR`@-rb2bt1V3r8Q=$KILQdYf`bHXs+%lgUAms1f7oG72i`o zdVf~hG4g5fPHkkVT6xsEmgf$=dTjti5}<_W3z`#y-NNk-oUX1dy~`hr^8Pyk6EhZL z9w_G+W`Jv+OdT>^d_V9gwo}XBdB(GP5s(~m?nLPj%!IqxyMU!U_m{WpACV+rC>@|x zBq5N(X0!0fvg~>y7f*oFeQhECVc$sotH>XEP;l_(x1C)O=Xe$089)fuGNM>Gk0Y|V zeN2xI<}?cS8eC7Z|E5$hTkpsX_L1d0ET@5A;0d089ug;E9h~E5c`zh+Ehvz7?(4p+ zA^=;vQfm2}b_xw@_H>M#T5uVW2D{*~7me2pbZW8OKUVb&&7G&#^51eMkftjK*B?KvJqr75;_CzkdYl9QYgNUnDyC$?(14_mev5 z9#q+0-bB{Y7ppkuNO=>BO7ZAcmhHN>685f2O{S_M7oN|rcc5)eO( zT>?cmr%}T5!~|w<%MmlvHKrMzIA@jRyJL8xf3EOyRgm3pr5-t$!qY$i5ESA4+6j|d z^16NXvzc_KyN)sdSzpuy;DPF3+sDO*mQGhqL(P-q(A*?|fLyAb;82IO$U#3~VIZEn zsW9t9E^-e6QQVbU5vq7G{0pIl(7LSC9Xpcv^$n|`{rRt;iT9_!(qWkd{|N0AZSsf* zu#7z(_u5zJRK%C~K)GMyD0{FrAP{}zy{xm~>nH^@R#s5~>mZ4C)jMGhpW|_Bz@I-6 z9F!W6pDd}sQ{zu|H>oY7qN310*lLlEheBc-4_|#rvFd96JWstjRy5h;t4bSxi4(Zx zKG~)Vqm#5YtYTY|>_DGbWU9{^WCqwN9FRdkDPlGm+;bu|aTa^JAm~2@y&`ycKT&^Y z(uh*bd9`<;=%FH>+Si>52SNzz;I*U70U3Ua-nrxNV)#zV^-SR|hyURlg}DGMh$MMl zST>=2$^-?D*5e2hxLtRn&4*lRu>1-ubcp>1{?`l8LhwH&WG%1$Tf1!8dY0`Rt#Gdt%u zc>+aCm=d1^Altq-BZk-0irD?GCXt&Lt~=UG8x`D+C58-wSt4|L^QvR7p6p#0G_xzt zc|8i)IIW~7j(T!rxzh#THID2pDVGIHsp=GUuQj~}4?O3_C|T>22F>B~&K+zRy?qhE zzZU9K?n4O9ga>TK0z6!#ZGS=0n3$)TlEp+pl|1LeZBbcz#nqnDpql$W zn6j+(XI!@LJPjmpAdaD?5@2}!tF5M*M`J%oAxz;EQ&tH6Giq&G_Gove1HK|7)0zClvh?DOZ)p+uZ=!-$)DU$_jWJXR-BC1{>GA+{Ik z^FutfeYi1U%wwT;Fk3f-J_4P6H?*X;e1A$EeYzHRxY)|by;ZZIQL>m~*sF0gGN(|i zPyPA3x#G0@Q$C4nTu9J_xv{qda)5+2_oe|qvIvpUr$qPtz!y2T2r@~yaQHKm8;YUT zp_!2i+9!#K&>13!V2P@m`4fD15l6l(q5eGbIqsy*R#?hiK|lX#Hg}^$tjcW0{j-W2 z#Du&3ue-FUM+<}WE?z!=Io{oY!&W@nnRauQDc)xhd&0wuulSaLI}Sbi03zd2h`8@DFfIX~Z#Iq|+hsHjB)O%H+ zT0aN|Zf|yG2*6M5WYzBg=U&984gsPeU+k*2a*d{9Nc)e9la)>()Dk=J$K-C<*N&LZ zbmRBECGfmx(gE<--BVt&znAD=PIx>2>D?{XN_G{$6#wv6bZcezfth_MSgqQZMO)G@ zeUWWn)`rhGan=OBV+?4RuWTp7``TXFS5xXb0~p3^lBY1CKXZ)uer74SPI18Yo{CktTtCqjMl7ZVY2Rr_}2 zH+iEi!R#x{dOQFShteI9W65l&dZyPW^}I@)8T7KC`!Bfoig^OikhhLKCj#hMsx!Yr z)_AmXJ+I06Pcdk+ogwpMhGVbX_>qHXu8}&^z@z@pZDY^vj)XY6v#uFdMH`XTj72>N z>KL)xaSFTo2Xr)pXN;gZz_eE5SjUQuRs};-(pmh2A-5cojDu;R>D$UG*TC&m;ID5ZUjvpd?DRQ$s=3jNWOm)A0Yq zYu@k)|Ml+Ik!$gPw7kk~u3I$Y*gebu3+aDuS!`zF z;u4pgQAbe;6mGZW_|d3c@u%p?M$-`LU{6T$iHybR4lhxCS$(9?*xpGV@!Yy`OMyLG zV2D&SfMw94n5ywGPT%`BCVA#q^EVg4Ku3!OX04zrwJ`PzLgW)FE$Bc3Ml}&A__p?r+lp)*?A+O7+aBT8O_N=EgAe5 z^nir}bVS^ci0hPwb9#h{)G=$Eovm%NF6hKTH~<1V{2|qn_5H(|60;(spou%zdF78c z2Bsq}(iVa;hMR;QfPZ0%2K~tEffmQCm1VqPkF5lJ{n-c9^wvh`XEVtX(Y0kI8b`gm zA*BW)bi8IkrH7;_|1m(*!@^VnaZPGK#OwcywQ_>{BU=Uc6`|s zQCoJe&Cs9o#02^PWI8}ulW(N*M6tgbzOkFo zxYSHQyuBCnjX_GCOm;KW(v7YT`Xo!GEH8?ZUR@^Lo%E)~YyYnPM6Rk4V%;LEjdQm5A$6@!wvh@ZVE^tV_WDd>8Lv_VyIpEdftyK#zeXA5wb!0I*Lobj;l}%gxu!n2#Hm-%S84bPLQq zxdX@nRiAjh)Ih!vF_DA~)#U$>pYg?C1e|YNXVGHSSTq}J#+Zw%i29FYCB`yk78sE zjF-1yYr)CV>*;9cu^VPXbpG2a|I%U4H&Rz6>s03{=tmR-!9_%pf*hV-sKIW3x^Y%r z6g{hLlP9LDGu!5gJD=7e??JTl){$ukY?;nFa7Kncnt5a>0kXLDTo3a_0mFhpt}6eb z^lWt^7*S*_n0r$FUZ~#yB~_fMArn^82e8E6q$;;NoU> zw$9<{@iQ#5%W1G~Z%^olV1Nlh%TPFrCy@hafy|f#=<`q;HHmN07p`|5H52BUhY=l; ztuqX~*dl47R*r>^o&hrPUQd+u7Za?1di~AYX`UTlGd=1xwyjE;r)JCnG<2v~-N_Hr z!qWuMOWZBJS_%3|t#W-@2<9uDS?nz$?zu8lGhv2dP?P+hE@1HhyLAgSX|`iJPkmx~ zknvt{L+RR;aTUEnp9qvCElMv5!1>yuob0|$TajoB!*qDGW&<``XS`QChwn*LP7tS+ zKpUs@_3iNFqNdj1!pNnY21G{VgpfeM)_sC8>j0VzD@ac7Bq<l~sJPo|&QA)!= zLLL)eK4Vt&BEP3~xb;0A+-&OP-UJTx?N6oyi9wY~FG!4iNKSt=7iXM7KpQQjP zX1tDV$t?1_RF93pT0|ql)C*f9?HenOA^hN$Pp>!xvfSd&UpoFv{qkV$iANCHdc>`_ zW&QE=tz&)Emgg9`7pPzC0ULWcqf2Gi2AxWEClOxBnnzaD=z!zv#eeTUSyy+syF&rF z0+gEj=HOuOWcZ3XY!39WdzN%zGZp)_zGFWa0TDpX>!Y-$q%*t28=1y0)UR@qx*YppKtob+7i`DnA85*$rCFp>8fAUd->RDVTdJhp;E=|x1%AcI^ zAQvD&-h~!(`i_<`KMGfU+MYF>5^u-g z52%IC=Xz&H6IGXsOHLSS(W&zq0(pn02ehY6tf1SMNW*9daA?;oP{y%YF?rmZcx%r_bUB~<;uAF_+^W7)~ z)kwLn_%*Uw`6YhlH>Y0#ruzh%_!pb|F_IJB8s5i3ai=H!3iGZa9f9Ov_%H;7VGLA8 z?CI$hEz0~$2&HGEmDAg1`6{7#d^3)2lNh?K0(r6O1N(-dE%$*BkOR z1t6j}QZ|PG8uCv)?DejDN!;nYT5RX}l@lIa^Er z3od=T#lHedqZMGE5twcy z8#g)iOOoYTYUid=Oz;@oYhto4!1LRSDv8H5cA6qj4u|;Dcj79?^0admEZAI8T{-aet2M_u~i9*Lgf}0o7mlp0lD>S<&PzwPdBgU z`Goq`X#V*Q1UCLAL)=!j!@aiIfAi(vFk7fSwl#nAY&3&L-?7B3e|wLuE*lUpK+{ln zpN_!_3YQ>eu-Crm^Op`-tka>tr<4W+T~Mw+ni5gu4hfG!WCKeN8S1*jU?)_G&!J?+ zJa7WYkl30EEZY{E6(7g$cPzALH2WH8la}dtBvzL(BH_5<5d82Np{yJM+k{o-Pm@ejl*+vi`p-CwnqNWjh5Hp^s=_UcR?VqwS|zGLuPrKH&D0q6v$SE5>Ze<=bIMV>;-|kR0h^` zeu{?Oxo_n{de9R`4e%k;IiHXkJ_}U|2OVh^pn|PU+!=2e_*!>leh6f(uC^ZtmB9DM z#rp1g^387X7qU>dOqAe5u7tM6?;HjT8~t1EEx+_JBGeeq2S8_U)yxMoDUomTVrRv5 z0OdAk>B}{M-cI15q!tozEc9Npzeq>PcAT06Vu!Rz_m4h6T2d-rD}HfYLuh(jT`-9h|i-mF-#4gxi1|fepfdDX8ZM-j;xHB-o z6a|MVEV%SkUPY$=loscIUwO?O9s^noBcquXJ&?)!lAykPg242En= zDt$k6^gGP%tIw^Yoh8s>WYgTCFh-ypT)nFNQ8D+XRB>z5LW43kwLtVy@k>1TwCfEmwp> zA4wMwhco)FvRHzdq&8Khq)euM@Y^%Z&HI~Tw?5#mqxj5hzf$lUPzy{ia!!!~1;(;vb7+l| z8Yrx0U=<#JY1$tZpnf=270XfGpr=ER_}V62+*x3C^F?Jck2-QK{Ha})d5rFhXeW5y*@b64*maMMgw&>;4Ho)?zBYl5;(xt@8kaJW zkVJQeu~PJT7%8Xs4g}RnkxXmUO1Y;$e&U{70 zZi-A}FXl$=7shaaG;j-t;Lff7F@hAIKRwSNK&`V73E&*$ebpIK5pzDX{}=qQXFVt{ zU@^!z$Nqc?P`)1}iU?k4@mRJHg+IjaX2L8rU_ub;mo;Dc1h6RY>;JU@UkQN|cNKAk z01_0*#!=2_Vk>_idDM6n|426)mGVKr31vRCm#2ot3S?3OOw?QLE+BG-5`@3C-8%c} z0xTD14qsxX$)a~$r+Lh_B>_aWh<;$vKH25BvjF!<@`-^;qZ(|VWytYd-swUC<6ph|KpThN2o00OSULugl>V2Hb`Jqj@w9i5}<$Z zQ+yUx%3c5)#>4GK`G_7ro@f?yNAmyS{$f%}2vD&*=Cj6rh#yZG{_Vc?Mi5&IA?PX~ z>l{!<0213GyBJ>leShbnnE1b-$<7&%B@etBsWX1I!%=6Ewc9T>-L9rGb9}JS{AQ%w zy_O^3+yS_BRNSfL^Qxg@^zKcMp=>995=X8{Lxv%Mvg7;untu50^AmQAthEh%4_Uxo z*Osn>CA%r#k2HF69g`^;E2wqr-?^OR!^3Rm10|MQTC;|Fy}5YOG6MP|aIGhYiv{0y zd$1s9AmIZagVz7X((o{|`3q4Bsml-;sxbjr zuQ2US`cCIp4#TFJtsF*#o1ET+3l-ai>XFt7a0bHOZoEe2IX2ud^wjJKki8>gy5WNn z&=b5NSgvfA>GxC9)tP++=j2bBT*|M=nf z>CBjB29Rj?fv#&U_DLhg9q9Wx)?mbecoNi>fd^6VxG$0{hBIs>H&aK{1+CEl;t?RP zZ_=n;n?gX@BjS4+pfa}rF!{4^p|%8LECh@m*l#sP+Jpy9U+K$A$4n`bOI+DWhR z4*t;ieiNNb$^L;DU^>7J0fvjI>aXQGsyc*j61GPHynzr$L$Jj!Ee(PoWCt6#T3?HP z$4n`&k@m7>M*10pG|3Md^p@Z`ruYm2 zk3#4>&$X==r_F-az(WV@B|Nqx=1eRM&lxZ_QAq2M&a-zP`ZSu!Nv!5cAnf|6Ut#{? zR5G9`k>w&>;+zVpTVjx&xNyw+b8Jd7%|lH;bv&978dXzp>5nm~9?Z9p3-4ZAPr-rA z;k2?#S1kstg{>?PAzgbAQ7abj&}-1Vpo;hrOs_uNg^-RnUyLPZ@mp))!`HT4sqqjp`J>dzNRPBp6Te|}T!ln`5OR=R(U}a&cc65X0@eLt*$?LtbOlD; zg$(gq&{vzYKu`%XAW3{N#1+?zDwxaJQ7+d1G-9mBzew`> zbED61`O|}0BjAD{F}=!EM=c{rQXyj^qfbz6S*;1ivLhK zd6b?d_TEr!qnQ+sNU}`yLnZ6X=tvS%dC--rSWC_yfqE=^B#648K(P4VHV@$KW4pl&hBRQ<>DbErFkzA_6to+x5WP?}`H!d&t zKC;6?3o#65p;NLD}ZUA4+ z05bY87GSUZ)oI`mM~Va4V=NtI2V5~E_q*><#!6JJR9?$5`_Y&HO6IjZEqMO`6k8mO z4TE=*OF*b!M}9&$T<8UP9=q1K7-I;M)SmPT5wq&j)oXRTRWDDXpNksydG}2m0nukFw_O}(J z6jf$azgWf(jpW99e9KpK{vQ6C(Bg^b{ikNFr=Ox&4eH(*7|xX#+vFMBfUOf^PKIBn z+qLuWPDWZ~%=t$z)Wpc~qt{U=e^w4F6p8c?M1X-W?#{GnqrKE=Qoy>oMU&l3!5tC# zW|f~c(c8;&F7uC_Y3-^VQ@{rPdZX8u^sGlS@)Bk;Md`Quwm! zw3I4qJtYV4`cv^;+S=~xwJ3!3w+FI@{lUl-52p@)`m&$Mv;axX+Flzcic$T<=F6BR z@rSOvrpOM~VVget!+Gt2I$D*?`T?l4rdxmAlmi_7m%g*Epz&|w=>!jz*5Xr!fzJ24 zOoT@ zY%$TpeT7lGdya+mh@*&k@Cd~cZ;hsp-S2sGpes|bHxaNgNw}*0*zs_?pfu8r(U`?* z>E%t=lDq6PW&Tjlr~{$-ye;F&NRfd5252Bigx&Q&lZ<(67@a`8Cn2b$oNhp+#|8v^;g@MVH1!7DexLqisiQng5C0 zxaraN87oJ+NpF5qlj0xsI}`iAa`EMYc`Bu+;GJ01YosFJq*B_884lA{K z-3(lE{*zkYD`fYrq?Cz3iO^CxoIP=-cz(H+ns!z@)mopT>ZfDxiCKB0w+k3*XpaZ1 z|2*MG5m%?cFSMY7`H&~4vz8SkfcPFGVr(0yK% zVh6SL8Jtfl52}>*k8UkQ;DDXWjcmp#Y`?D~#zl5j6yp(=O_!{*?w8>U#b+=Cqh=3M zyjp(^y-Je)eHpQAe39#uPEXj7s*Ny@Lh6-j%!k$^OYcF&-#cPqMkKIEsQhNr^Q2!< zkjlsPkGOGYVj6!jJpCb|cST3h5>UZMS}LHkHD-LlDB`h#=&*wDPHvbYxHHTjPaX)( z6@Bo8)1KO?$_$)ruJBBAE83b=&Rf%sx;VJei>R#}JQ!8C*tOru*-ZSK?w%rtnifbJ z6j}O{*Za(?NQCI51QT5#6g1s>FSBNM$F|rQ^g7u-tNHXLB-OYl-EQjAtD|KEu15UB3HI z6RDb$C%z7umN6^xXP%#0>rKDT-~6e(bxQ6b^z+ImI{CIm)8p$va8(Pmr^w2?rWXW@ zmWsmzwVJgTJX^LA0r0Dk2VKun@pj5;%&w_hcygrn1awtNDPyd*i&G0mJ~IscF!ldd zDyB!hd#XnMv_o4#?%iThQ_(Z)%qy#$+XB}uqS|LCWG@#pEgfcfX~5E^a6H4~x?Fj? z;?OYdh1iLgmw)~+Yq0z`myjKrc~fxd953p0b`}0c+ZY=&t4AkhY zzw8s{fzt?128i29&1j-NV0K6m1(sy*LYcM>7uOB~=VMWU!MpXbLpbgU~5#(959 zN}?v;|1~a{qWv!zM=koW%sMD_uX-SgI zbW2%t0@0jUbIz$4?>OPxDKO68Hm&(c5jpU@yu4Z3>K)yFz{Drxj0B_A;^`@~CX3F{ zoQwjVBIV=96WzXxqgukvyr0TXsmGHfeAd?Yt3C6&nrV#OtBME@1$0?(Fc0wLwgpKm zGBqHV1zHX=dC{1~+M_I<3yq2z>FpI-_rLL7AVG5}D%pGy%l&ArXOedUwKB`Ptag8) z6-Z8&QTztUImM<}+^`cs0r!sKNKECFw~MRVE<4MiE0cA3+W%zU^=ZHQMH^4?)s`oP z2Mjq{u~77*9#K(?%&QKjN4nq0Z&q@=5M9tY{BTUzWH|0J|4SrxCfjBgDs3ZADW7W3 zA~TjS@lA4{8#dsp^=1?8{$moUD3P_AkmQ@wGw)os;f}1jd7#bx*>M7uPg>{waY7?P zXYb@xrv(`rpUZ4|#dh=XQBup2VS{R40@t#3$9&FCpT$9vz3$X_{s&LA2ezkRBe)wm z*|lLi2KAm_5<21aul!FOZW5jrziIPHf*(n~DyO%r9&9`*HhomYtVf;s>g`3tw#t4F z7-Pa!UFG;!U2Cb)$`M7KrHJ)YrJ~Fduu?dxy7G4V3}OA=@*oQe^j&I#Q$VA};QnE7uoIPCtNd=+ZYY zohU9$7O|iX1P{N4>UeL~k8k2r(Hn;*Or<|kVSHf|7v)>y=;Q}x!1!M%myCLCamk@W@DtF~D= zhazU!j`dO6NO_pQnf=(2^keZm{W||mOQqtlY6?fxn|v}b9RP@?Z3|hWcPGe<-mHoH zz-<~+*Y-xdA}3iun7J}QmA;}Xp3HCV{-$;j`uYR$DM*lG-X*Ebm)+2Q{MzJWgKt?$ zK=f*+5t{#IPeFJ@3>yo(>dd@<#8 znP;@}7^N_ae$8HC?rYZ=0C8d&xU%nF0|Y%S%AfQ*u3-6O z0}Kz9%S%w0nLcIl8czysin&`cBox@5hqb9s)_x?syQ9-R$H|f15v%M0O{U{w1zFHO z<`IG-7(QnH)Q=L_nL3O*oo_sN1P~<8 zMU6GlW8U?+9GHF)j0m@^pK^S2>q)4P1=Xna(m}tEsj+twvIZ|{Dn0-1E~nNXL6d}Y zgUs%lo&B-L>di)C)Lr9NWJ+-|tmic=#hH)w=9)DsrjzuFB!{_!fA=eeO)Xn<#W$^D z=e$>NdsPNt7>AK(@xcPZqVJUXD}t;^qH8SBl1ZdjT=U6hm=ApbtIWG<&o|vWgSyo` zY5*NfOL0g?6Evh!u+L}h^erKkk%>&7KoRU6e_{k~SPniB8AH)m{>h_WbzR3i#;B(( zSgFKBWR;`)ziCP%&4=9j?>k5!EV6;<$^U#m$cQcZ@7vZN_>lh)3j{*M47u^|hn$iB q`&X4u{@*YDzv1~WD*o?5fo9|j`0W%r=n;$gB}G|PnQ|$!5C0!_eQC4+ From 048b2b22cdacb8d3081574ca4d0dfbb87bc900cd Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Wed, 18 Dec 2024 21:05:23 +0100 Subject: [PATCH 29/32] chore: cleanup --- src/core/CoreNode.ts | 1 - src/core/text-rendering/renderers/CanvasTextRenderer.ts | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 6cbcf0ff..cab56795 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -2046,7 +2046,6 @@ export class CoreNode extends EventEmitter { }); // call load immediately to ensure the texture is created - // WvB do we really need this? this.stage.txManager.loadTexture(this.texture, true); this.stage.renderer?.renderToTexture(this); // Only this RTT node diff --git a/src/core/text-rendering/renderers/CanvasTextRenderer.ts b/src/core/text-rendering/renderers/CanvasTextRenderer.ts index 180c38f5..1d42c5f8 100644 --- a/src/core/text-rendering/renderers/CanvasTextRenderer.ts +++ b/src/core/text-rendering/renderers/CanvasTextRenderer.ts @@ -338,7 +338,7 @@ export class CanvasTextRenderer extends TextRenderer { const node = state.node; const texture = this.stage.txManager.createTexture('ImageTexture', { - premultiplyAlpha: false, + premultiplyAlpha: true, src: function ( this: CanvasTextRenderer, lightning2TextRenderer: LightningTextTextureRenderer, @@ -362,8 +362,6 @@ export class CanvasTextRenderer extends TextRenderer { }.bind(this, state.lightning2TextRenderer, state.renderInfo), }); - this.stage.txManager.loadTexture(texture); - if (state.textureNode) { // Use the existing texture node state.textureNode.texture = texture; From e0cd04c7240f046ca200ee1e06997e57b6f196da Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Wed, 18 Dec 2024 21:05:59 +0100 Subject: [PATCH 30/32] refactor: WebGlCoreCtxTexture to support RGB format if texture has no alpha channel --- src/core/lib/WebGlContextWrapper.ts | 4 ++- .../renderers/webgl/WebGlCoreCtxTexture.ts | 33 +++++++++++-------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/core/lib/WebGlContextWrapper.ts b/src/core/lib/WebGlContextWrapper.ts index 2eba40ab..99564ef0 100644 --- a/src/core/lib/WebGlContextWrapper.ts +++ b/src/core/lib/WebGlContextWrapper.ts @@ -68,6 +68,7 @@ export class WebGlContextWrapper { public readonly TEXTURE_WRAP_T; public readonly LINEAR; public readonly CLAMP_TO_EDGE; + public readonly RGB; public readonly RGBA; public readonly UNSIGNED_BYTE; public readonly UNPACK_PREMULTIPLY_ALPHA_WEBGL; @@ -158,6 +159,7 @@ export class WebGlContextWrapper { this.TEXTURE_WRAP_T = gl.TEXTURE_WRAP_T; this.LINEAR = gl.LINEAR; this.CLAMP_TO_EDGE = gl.CLAMP_TO_EDGE; + this.RGB = gl.RGB; this.RGBA = gl.RGBA; this.UNSIGNED_BYTE = gl.UNSIGNED_BYTE; this.UNPACK_PREMULTIPLY_ALPHA_WEBGL = gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL; @@ -1269,7 +1271,7 @@ export class WebGlContextWrapper { // prettier-ignore type IsUniformMethod = MethodName extends `uniform${string}` - ? + ? MethodType extends (location: WebGLUniformLocation | null, ...args: any[]) => void ? true : false diff --git a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts index ebbaccc1..fa9910dd 100644 --- a/src/core/renderers/webgl/WebGlCoreCtxTexture.ts +++ b/src/core/renderers/webgl/WebGlCoreCtxTexture.ts @@ -144,7 +144,11 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { let height = 0; glw.activeTexture(0); + const tdata = textureData.data; + const format = textureData.premultiplyAlpha ? glw.RGBA : glw.RGB; + const formatBytes = format === glw.RGBA ? 4 : 3; + // If textureData is null, the texture is empty (0, 0) and we don't need to // upload any data to the GPU. if ( @@ -156,13 +160,13 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { width = tdata.width; height = tdata.height; glw.bindTexture(this._nativeCtxTexture); + glw.pixelStorei( + glw.UNPACK_PREMULTIPLY_ALPHA_WEBGL, + !!textureData.premultiplyAlpha, + ); - if (textureData.premultiplyAlpha === true) { - glw.pixelStorei(glw.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); - } - - glw.texImage2D(0, glw.RGBA, glw.RGBA, glw.UNSIGNED_BYTE, tdata); - this.setTextureMemUse(width * height * 4); + glw.texImage2D(0, format, format, glw.UNSIGNED_BYTE, tdata); + this.setTextureMemUse(width * height * formatBytes); // generate mipmaps for power-of-2 textures or in WebGL2RenderingContext if (glw.isWebGl2() || (isPowerOfTwo(width) && isPowerOfTwo(height))) { @@ -176,11 +180,11 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { glw.texImage2D( 0, - glw.RGBA, + format, 1, 1, 0, - glw.RGBA, + format, glw.UNSIGNED_BYTE, TRANSPARENT_TEXTURE_DATA, ); @@ -207,22 +211,23 @@ export class WebGlCoreCtxTexture extends CoreContextTexture { height = 1; glw.bindTexture(this._nativeCtxTexture); - if (textureData.premultiplyAlpha === true) { - glw.pixelStorei(glw.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); - } + glw.pixelStorei( + glw.UNPACK_PREMULTIPLY_ALPHA_WEBGL, + !!textureData.premultiplyAlpha, + ); glw.texImage2D( 0, - glw.RGBA, + format, width, height, 0, - glw.RGBA, + format, glw.UNSIGNED_BYTE, tdata, ); - this.setTextureMemUse(width * height * 4); + this.setTextureMemUse(width * height * formatBytes); } else { console.error( `WebGlCoreCtxTexture.onLoadRequest: Unexpected textureData returned`, From 4af99df861aa3cd17796e211f0b7edcc21203085 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Wed, 18 Dec 2024 21:17:23 +0100 Subject: [PATCH 31/32] feat: add textureProcessingLimit parameter to examples --- examples/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples/index.ts b/examples/index.ts index efe64795..9ead0828 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -90,6 +90,8 @@ const defaultPhysicalPixelRatio = 1; const resolution = Number(urlParams.get('resolution')) || 720; const enableInspector = urlParams.get('inspector') === 'true'; const forceWebGL2 = urlParams.get('webgl2') === 'true'; + const textureProcessingLimit = + Number(urlParams.get('textureProcessingLimit')) || 0; const physicalPixelRatio = Number(urlParams.get('ppr')) || defaultPhysicalPixelRatio; @@ -114,6 +116,7 @@ const defaultPhysicalPixelRatio = 1; perfMultiplier, enableInspector, forceWebGL2, + textureProcessingLimit, ); return; } @@ -136,6 +139,7 @@ async function runTest( perfMultiplier: number, enableInspector: boolean, forceWebGL2: boolean, + textureProcessingLimit: number, ) { const testModule = testModules[getTestPath(test)]; if (!testModule) { @@ -157,6 +161,7 @@ async function runTest( physicalPixelRatio, enableInspector, forceWebGL2, + textureProcessingLimit, customSettings, ); @@ -231,6 +236,7 @@ async function initRenderer( physicalPixelRatio: number, enableInspector: boolean, forceWebGL2?: boolean, + textureProcessingLimit?: number, customSettings?: Partial, ) { let inspector: typeof Inspector | undefined; @@ -250,6 +256,7 @@ async function initRenderer( renderEngine: renderMode === 'webgl' ? WebGlCoreRenderer : CanvasCoreRenderer, fontEngines: [SdfTextRenderer, CanvasTextRenderer], + textureProcessingLimit: textureProcessingLimit, ...customSettings, }, 'app', From 7a0fafbe00c530cc547268f4856f5aa87b614dc7 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Wed, 18 Dec 2024 21:57:37 +0100 Subject: [PATCH 32/32] fix: Support for color texture in Canvas renderer, align with latest texture changes --- src/core/Stage.ts | 5 +--- .../renderers/canvas/CanvasCoreRenderer.ts | 24 +++++++++++++------ .../renderers/canvas/CanvasCoreTexture.ts | 3 --- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/core/Stage.ts b/src/core/Stage.ts index fa070c82..a3c09cb1 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -194,10 +194,7 @@ export class Stage { this.renderer = new renderEngine(rendererOptions); const renderMode = this.renderer.mode || 'webgl'; - if (renderMode === 'webgl') { - this.createDefaultTexture(); - } - + this.createDefaultTexture(); this.defShaderCtr = this.renderer.getDefShaderCtr(); setPremultiplyMode(renderMode); diff --git a/src/core/renderers/canvas/CanvasCoreRenderer.ts b/src/core/renderers/canvas/CanvasCoreRenderer.ts index 7fe4a98e..8d6f3a6f 100644 --- a/src/core/renderers/canvas/CanvasCoreRenderer.ts +++ b/src/core/renderers/canvas/CanvasCoreRenderer.ts @@ -22,7 +22,7 @@ import type { CoreNode } from '../../CoreNode.js'; import type { CoreShaderManager } from '../../CoreShaderManager.js'; import { getRgbaComponents, type RGBA } from '../../lib/utils.js'; import { SubTexture } from '../../textures/SubTexture.js'; -import type { Texture } from '../../textures/Texture.js'; +import { TextureType, type Texture } from '../../textures/Texture.js'; import type { CoreContextTexture } from '../CoreContextTexture.js'; import { CoreRenderer, @@ -43,6 +43,7 @@ import { type IParsedColor, } from './internal/ColorUtils.js'; import { UnsupportedShader } from './shaders/UnsupportedShader.js'; +import { assertTruthy } from '../../../utils.js'; export class CanvasCoreRenderer extends CoreRenderer { private context: CanvasRenderingContext2D; @@ -118,6 +119,17 @@ export class CanvasCoreRenderer extends CoreRenderer { | { x: number; y: number; width: number; height: number } | undefined; + const textureType = texture?.type; + assertTruthy(textureType, 'Texture type is not defined'); + + // The Canvas2D renderer only supports image and color textures + if ( + textureType !== TextureType.image && + textureType !== TextureType.color + ) { + return; + } + if (texture) { if (texture instanceof SubTexture) { frame = texture.props; @@ -126,11 +138,9 @@ export class CanvasCoreRenderer extends CoreRenderer { ctxTexture = texture.ctxTexture as CanvasCoreTexture; if (texture.state === 'freed') { - // we're going to batch the texture loading so we don't have to wait for - // ctxTexture.load(); return; } - if (texture.state !== 'loaded' || !ctxTexture.hasImage()) { + if (texture.state !== 'loaded') { return; } } @@ -175,7 +185,7 @@ export class CanvasCoreRenderer extends CoreRenderer { ctx.clip(path); } - if (ctxTexture) { + if (textureType === TextureType.image && ctxTexture) { const image = ctxTexture.getImage(color); ctx.globalAlpha = color.a ?? alpha; if (frame) { @@ -199,7 +209,7 @@ export class CanvasCoreRenderer extends CoreRenderer { } } ctx.globalAlpha = 1; - } else if (hasGradient) { + } else if (textureType === TextureType.color && hasGradient) { let endX: number = tx; let endY: number = ty; let endColor: IParsedColor; @@ -219,7 +229,7 @@ export class CanvasCoreRenderer extends CoreRenderer { gradient.addColorStop(1, formatRgba(endColor)); ctx.fillStyle = gradient; ctx.fillRect(tx, ty, width, height); - } else { + } else if (textureType === TextureType.color) { ctx.fillStyle = formatRgba(color); ctx.fillRect(tx, ty, width, height); } diff --git a/src/core/renderers/canvas/CanvasCoreTexture.ts b/src/core/renderers/canvas/CanvasCoreTexture.ts index b46b1b08..19c41125 100644 --- a/src/core/renderers/canvas/CanvasCoreTexture.ts +++ b/src/core/renderers/canvas/CanvasCoreTexture.ts @@ -36,9 +36,6 @@ export class CanvasCoreTexture extends CoreContextTexture { | undefined; load(): void { - if (this.textureSource.state !== 'freed') { - return; - } this.textureSource.setCoreCtxState('loading'); this.onLoadRequest()