@lightningjs/renderer
Advanced tools
Comparing version 0.7.6 to 0.8.0
@@ -49,2 +49,8 @@ import type { CoreNodeRenderState } from '../core/CoreNode.js'; | ||
/** | ||
* Payload for when texture failed to load | ||
*/ | ||
export type NodeTextureFreedPayload = { | ||
type: 'texture'; | ||
}; | ||
/** | ||
* Combined type for all failed payloads | ||
@@ -51,0 +57,0 @@ */ |
@@ -59,2 +59,6 @@ import type { ShaderMap } from './CoreShaderManager.js'; | ||
* Scale/Rotate transform update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `scaleRotateTransform` | ||
*/ | ||
@@ -64,6 +68,16 @@ ScaleRotate = 2, | ||
* Translate transform update (x/y/width/height/pivot/mount) | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `localTransform` | ||
*/ | ||
Local = 4, | ||
/** | ||
* Global transform update | ||
* Global Transform update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `globalTransform` | ||
* - `renderCoords` | ||
* - `renderBound` | ||
*/ | ||
@@ -73,2 +87,6 @@ Global = 8, | ||
* Clipping rect update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `clippingRect` | ||
*/ | ||
@@ -78,2 +96,6 @@ Clipping = 16, | ||
* Calculated ZIndex update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `calcZIndex` | ||
*/ | ||
@@ -83,16 +105,44 @@ CalculatedZIndex = 32, | ||
* Z-Index Sorted Children update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `children` (sorts children by their `calcZIndex`) | ||
*/ | ||
ZIndexSortedChildren = 64, | ||
/** | ||
* Premultiplied Colors | ||
* Premultiplied Colors update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `premultipliedColorTl` | ||
* - `premultipliedColorTr` | ||
* - `premultipliedColorBl` | ||
* - `premultipliedColorBr` | ||
*/ | ||
PremultipliedColors = 128, | ||
/** | ||
* World Alpha | ||
* World Alpha update | ||
* | ||
* @remarks | ||
* World Alpha = Parent World Alpha * Alpha | ||
* CoreNode Properties Updated: | ||
* - `worldAlpha` = `parent.worldAlpha` * `alpha` | ||
*/ | ||
WorldAlpha = 256, | ||
/** | ||
* Render State update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `renderState` | ||
*/ | ||
RenderState = 512, | ||
/** | ||
* Is Renderable update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `isRenderable` | ||
*/ | ||
IsRenderable = 1024, | ||
/** | ||
* None | ||
@@ -104,3 +154,3 @@ */ | ||
*/ | ||
All = 511 | ||
All = 2047 | ||
} | ||
@@ -133,2 +183,3 @@ export declare class CoreNode extends EventEmitter implements ICoreNode { | ||
private onTextureFailed; | ||
private onTextureFreed; | ||
loadShader<Type extends keyof ShaderMap>(shaderType: Type, props: ExtractProps<ShaderMap[Type]>): void; | ||
@@ -156,3 +207,9 @@ /** | ||
setRenderState(state: CoreNodeRenderState): void; | ||
updateIsRenderable(): false | undefined; | ||
/** | ||
* This function updates the `isRenderable` property based on certain conditions. | ||
* | ||
* @returns | ||
*/ | ||
updateIsRenderable(): void; | ||
onChangeIsRenderable(isRenderable: boolean): void; | ||
calculateRenderCoords(): void; | ||
@@ -159,0 +216,0 @@ updateBoundingRect(): void; |
@@ -44,2 +44,6 @@ /* | ||
* Scale/Rotate transform update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `scaleRotateTransform` | ||
*/ | ||
@@ -49,6 +53,16 @@ UpdateType[UpdateType["ScaleRotate"] = 2] = "ScaleRotate"; | ||
* Translate transform update (x/y/width/height/pivot/mount) | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `localTransform` | ||
*/ | ||
UpdateType[UpdateType["Local"] = 4] = "Local"; | ||
/** | ||
* Global transform update | ||
* Global Transform update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `globalTransform` | ||
* - `renderCoords` | ||
* - `renderBound` | ||
*/ | ||
@@ -58,2 +72,6 @@ UpdateType[UpdateType["Global"] = 8] = "Global"; | ||
* Clipping rect update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `clippingRect` | ||
*/ | ||
@@ -63,2 +81,6 @@ UpdateType[UpdateType["Clipping"] = 16] = "Clipping"; | ||
* Calculated ZIndex update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `calcZIndex` | ||
*/ | ||
@@ -68,16 +90,44 @@ UpdateType[UpdateType["CalculatedZIndex"] = 32] = "CalculatedZIndex"; | ||
* Z-Index Sorted Children update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `children` (sorts children by their `calcZIndex`) | ||
*/ | ||
UpdateType[UpdateType["ZIndexSortedChildren"] = 64] = "ZIndexSortedChildren"; | ||
/** | ||
* Premultiplied Colors | ||
* Premultiplied Colors update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `premultipliedColorTl` | ||
* - `premultipliedColorTr` | ||
* - `premultipliedColorBl` | ||
* - `premultipliedColorBr` | ||
*/ | ||
UpdateType[UpdateType["PremultipliedColors"] = 128] = "PremultipliedColors"; | ||
/** | ||
* World Alpha | ||
* World Alpha update | ||
* | ||
* @remarks | ||
* World Alpha = Parent World Alpha * Alpha | ||
* CoreNode Properties Updated: | ||
* - `worldAlpha` = `parent.worldAlpha` * `alpha` | ||
*/ | ||
UpdateType[UpdateType["WorldAlpha"] = 256] = "WorldAlpha"; | ||
/** | ||
* Render State update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `renderState` | ||
*/ | ||
UpdateType[UpdateType["RenderState"] = 512] = "RenderState"; | ||
/** | ||
* Is Renderable update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `isRenderable` | ||
*/ | ||
UpdateType[UpdateType["IsRenderable"] = 1024] = "IsRenderable"; | ||
/** | ||
* None | ||
@@ -89,3 +139,3 @@ */ | ||
*/ | ||
UpdateType[UpdateType["All"] = 511] = "All"; | ||
UpdateType[UpdateType["All"] = 2047] = "All"; | ||
})(UpdateType || (UpdateType = {})); | ||
@@ -140,3 +190,3 @@ export class CoreNode extends EventEmitter { | ||
this.props.textureOptions = options; | ||
this.updateIsRenderable(); | ||
this.setUpdateType(UpdateType.IsRenderable); | ||
// If texture is already loaded / failed, trigger loaded event manually | ||
@@ -153,4 +203,8 @@ // so that users get a consistent event experience. | ||
} | ||
else if (texture.state === 'freed') { | ||
this.onTextureFreed(texture); | ||
} | ||
texture.on('loaded', this.onTextureLoaded); | ||
texture.on('failed', this.onTextureFailed); | ||
texture.on('freed', this.onTextureFreed); | ||
}); | ||
@@ -160,8 +214,11 @@ } | ||
if (this.props.texture) { | ||
this.props.texture.off('loaded', this.onTextureLoaded); | ||
this.props.texture.off('failed', this.onTextureFailed); | ||
const { texture } = this.props; | ||
texture.off('loaded', this.onTextureLoaded); | ||
texture.off('failed', this.onTextureFailed); | ||
texture.off('freed', this.onTextureFreed); | ||
texture.setRenderableOwner(this, false); | ||
} | ||
this.props.texture = null; | ||
this.props.textureOptions = null; | ||
this.updateIsRenderable(); | ||
this.setUpdateType(UpdateType.IsRenderable); | ||
} | ||
@@ -183,2 +240,7 @@ onTextureLoaded = (target, dimensions) => { | ||
}; | ||
onTextureFreed = (target) => { | ||
this.emit('freed', { | ||
type: 'texture', | ||
}); | ||
}; | ||
//#endregion Textures | ||
@@ -191,3 +253,3 @@ loadShader(shaderType, props) { | ||
this.props.shaderProps = p; | ||
this.updateIsRenderable(); | ||
this.setUpdateType(UpdateType.IsRenderable); | ||
} | ||
@@ -251,4 +313,3 @@ /** | ||
this.updateBoundingRect(); | ||
this.updateRenderState(); | ||
this.setUpdateType(UpdateType.Clipping | UpdateType.Children); | ||
this.setUpdateType(UpdateType.Clipping | UpdateType.RenderState | UpdateType.Children); | ||
childUpdateType |= UpdateType.Global; | ||
@@ -287,5 +348,9 @@ } | ||
} | ||
} | ||
if (this.updateType & UpdateType.RenderState) { | ||
this.updateRenderState(); | ||
this.setUpdateType(UpdateType.IsRenderable); | ||
} | ||
if (this.updateType & UpdateType.IsRenderable) { | ||
this.updateIsRenderable(); | ||
this.setUpdateType(UpdateType.Children); | ||
childUpdateType |= UpdateType.PremultipliedColors; | ||
} | ||
@@ -385,3 +450,3 @@ // No need to update zIndex if there is no parent | ||
if (previous === CoreNodeRenderState.InViewport) { | ||
this.emit('outOfViewPort', { | ||
this.emit('outOfViewport', { | ||
previous, | ||
@@ -398,3 +463,2 @@ current: renderState, | ||
} | ||
this.updateIsRenderable(); | ||
} | ||
@@ -407,10 +471,23 @@ setRenderState(state) { | ||
} | ||
// This function checks if the current node is renderable based on certain properties. | ||
// It returns true if any of the specified properties are truthy or if any color property is not 0, otherwise it returns false. | ||
/** | ||
* This function updates the `isRenderable` property based on certain conditions. | ||
* | ||
* @returns | ||
*/ | ||
updateIsRenderable() { | ||
let newIsRenderable; | ||
if (!this.checkRenderProps()) { | ||
return (this.isRenderable = false); | ||
newIsRenderable = false; | ||
} | ||
this.isRenderable = this.renderState > CoreNodeRenderState.OutOfBounds; | ||
else { | ||
newIsRenderable = this.renderState > CoreNodeRenderState.OutOfBounds; | ||
} | ||
if (this.isRenderable !== newIsRenderable) { | ||
this.isRenderable = newIsRenderable; | ||
this.onChangeIsRenderable(newIsRenderable); | ||
} | ||
} | ||
onChangeIsRenderable(isRenderable) { | ||
this.props.texture?.setRenderableOwner(this, isRenderable); | ||
} | ||
calculateRenderCoords() { | ||
@@ -417,0 +494,0 @@ const { width, height, globalTransform: transform } = this; |
@@ -64,4 +64,9 @@ import type { TextRenderer, TextRendererMap, TrProps, TextRendererState } from './text-rendering/renderers/TextRenderer.js'; | ||
checkRenderProps(): boolean; | ||
onChangeIsRenderable(isRenderable: boolean): void; | ||
renderQuads(renderer: CoreRenderer): void; | ||
/** | ||
* Destroy the node and cleanup all resources | ||
*/ | ||
destroy(): void; | ||
/** | ||
* Resolve a text renderer and a new state based on the current text renderer props provided | ||
@@ -68,0 +73,0 @@ * @param props |
@@ -53,3 +53,3 @@ /* | ||
overflowSuffix: props.overflowSuffix, | ||
}, undefined); | ||
}); | ||
this.textRenderer = resolvedTextRenderer; | ||
@@ -135,3 +135,4 @@ this.trState = textRendererState; | ||
this._textRendererOverride = value; | ||
const { resolvedTextRenderer, textRendererState } = this.resolveTextRendererAndState(this.trState.props, this.trState); | ||
this.textRenderer.destroyState(this.trState); | ||
const { resolvedTextRenderer, textRendererState } = this.resolveTextRendererAndState(this.trState.props); | ||
this.textRenderer = resolvedTextRenderer; | ||
@@ -265,2 +266,6 @@ this.trState = textRendererState; | ||
} | ||
onChangeIsRenderable(isRenderable) { | ||
super.onChangeIsRenderable(isRenderable); | ||
this.textRenderer.setIsRenderable(this.trState, isRenderable); | ||
} | ||
renderQuads(renderer) { | ||
@@ -271,2 +276,9 @@ assertTruthy(this.globalTransform); | ||
/** | ||
* Destroy the node and cleanup all resources | ||
*/ | ||
destroy() { | ||
super.destroy(); | ||
this.textRenderer.destroyState(this.trState); | ||
} | ||
/** | ||
* Resolve a text renderer and a new state based on the current text renderer props provided | ||
@@ -276,12 +288,5 @@ * @param props | ||
*/ | ||
resolveTextRendererAndState(props, prevState) { | ||
resolveTextRendererAndState(props) { | ||
const resolvedTextRenderer = this.stage.resolveTextRenderer(props, this._textRendererOverride); | ||
const textRendererState = resolvedTextRenderer.createState(props); | ||
const stateEvents = ['loading', 'loaded', 'failed']; | ||
if (prevState) { | ||
// Remove the old event listeners from previous state obj there was one | ||
stateEvents.forEach((eventName) => { | ||
prevState.emitter.off(eventName); | ||
}); | ||
} | ||
textRendererState.emitter.on('loaded', this.onTextLoaded); | ||
@@ -288,0 +293,0 @@ textRendererState.emitter.on('failed', this.onTextFailed); |
@@ -159,2 +159,4 @@ /* | ||
} | ||
// Free the ctx texture if it exists. | ||
this.ctxTextureCache.get(texture)?.free(); | ||
} | ||
@@ -161,0 +163,0 @@ /** |
@@ -23,8 +23,16 @@ /* | ||
export const startLoop = (stage) => { | ||
let isIdle = false; | ||
const runLoop = () => { | ||
stage.updateAnimations(); | ||
if (!stage.hasSceneUpdates()) { | ||
// We still need to calculate the fps else it looks like the app is frozen | ||
stage.calculateFps(); | ||
setTimeout(runLoop, 16.666666666666668); | ||
if (!isIdle) { | ||
stage.emit('idle'); | ||
isIdle = true; | ||
} | ||
return; | ||
} | ||
isIdle = false; | ||
stage.drawFrame(); | ||
@@ -31,0 +39,0 @@ requestAnimationFrame(runLoop); |
@@ -0,6 +1,10 @@ | ||
import type { TextureMemoryManager } from '../TextureMemoryManager.js'; | ||
import type { Texture } from '../textures/Texture.js'; | ||
export declare abstract class CoreContextTexture { | ||
readonly memManager: TextureMemoryManager; | ||
readonly textureSource: Texture; | ||
constructor(textureSource: Texture); | ||
constructor(memManager: TextureMemoryManager, textureSource: Texture); | ||
abstract load(): void; | ||
abstract free(): void; | ||
abstract get renderable(): boolean; | ||
} |
@@ -20,4 +20,6 @@ /* | ||
export class CoreContextTexture { | ||
memManager; | ||
textureSource; | ||
constructor(textureSource) { | ||
constructor(memManager, textureSource) { | ||
this.memManager = memManager; | ||
this.textureSource = textureSource; | ||
@@ -24,0 +26,0 @@ } |
@@ -32,9 +32,19 @@ /* | ||
const colors = props.colors ?? [0xff000000, 0xffffffff]; | ||
let stops = props.stops; | ||
if (!stops) { | ||
stops = []; | ||
const calc = colors.length - 1; | ||
for (let i = 0; i < colors.length; i++) { | ||
stops.push(i * (1 / calc)); | ||
let stops = props.stops || []; | ||
if (stops.length === 0 || stops.length !== colors.length) { | ||
const colorsL = colors.length; | ||
let i = 0; | ||
const tmp = stops; | ||
for (; i < colorsL; i++) { | ||
if (stops[i]) { | ||
tmp[i] = stops[i]; | ||
if (stops[i - 1] === undefined && tmp[i - 2] !== undefined) { | ||
tmp[i - 1] = tmp[i - 2] + (stops[i] - tmp[i - 2]) / 2; | ||
} | ||
} | ||
else { | ||
tmp[i] = i * (1 / (colors.length - 1)); | ||
} | ||
} | ||
stops = tmp; | ||
} | ||
@@ -65,22 +75,2 @@ return { | ||
value: [], | ||
validator: (value, props) => { | ||
const colors = props.colors ?? []; | ||
let stops = value; | ||
const tmp = value; | ||
if (stops.length === 0 || (stops && stops.length !== colors.length)) { | ||
for (let i = 0; i < colors.length; i++) { | ||
if (stops[i]) { | ||
tmp[i] = stops[i]; | ||
if (stops[i - 1] === undefined && tmp[i - 2] !== undefined) { | ||
tmp[i - 1] = tmp[i - 2] + (stops[i] - tmp[i - 2]) / 2; | ||
} | ||
} | ||
else { | ||
tmp[i] = i * (1 / (colors.length - 1)); | ||
} | ||
} | ||
stops = tmp; | ||
} | ||
return tmp; | ||
}, | ||
size: (props) => props.colors.length, | ||
@@ -131,6 +121,3 @@ method: 'uniform1fv', | ||
vec4 colorOut = $fromLinear(mix($toLinear(colors[0]), $toLinear(colors[1]), stopCalc)); | ||
for(int i = 1; i < ${colors}-1; i++) { | ||
stopCalc = (dist - stops[i]) / (stops[i + 1] - stops[i]); | ||
colorOut = mix(colorOut, colors[i + 1], clamp(stopCalc, 0.0, 1.0)); | ||
} | ||
${this.ColorLoop(colors)} | ||
return mix(maskColor, colorOut, clamp(colorOut.a, 0.0, 1.0)); | ||
@@ -137,0 +124,0 @@ `; |
@@ -37,3 +37,4 @@ import { type DefaultEffectProps, ShaderEffect, type ShaderEffectUniforms } from './ShaderEffect.js'; | ||
static uniforms: ShaderEffectUniforms; | ||
static ColorLoop: (amount: number) => string; | ||
static onColorize: (props: RadialGradientEffectProps) => string; | ||
} |
@@ -29,9 +29,19 @@ /* | ||
const colors = props.colors ?? [0xff000000, 0xffffffff]; | ||
let stops = props.stops; | ||
if (!stops) { | ||
stops = []; | ||
const calc = colors.length - 1; | ||
for (let i = 0; i < colors.length; i++) { | ||
stops.push(i * (1 / calc)); | ||
let stops = props.stops || []; | ||
if (stops.length === 0 || stops.length !== colors.length) { | ||
const colorsL = colors.length; | ||
let i = 0; | ||
const tmp = stops; | ||
for (; i < colorsL; i++) { | ||
if (stops[i]) { | ||
tmp[i] = stops[i]; | ||
if (stops[i - 1] === undefined && tmp[i - 2] !== undefined) { | ||
tmp[i - 1] = tmp[i - 2] + (stops[i] - tmp[i - 2]) / 2; | ||
} | ||
} | ||
else { | ||
tmp[i] = i * (1 / (colors.length - 1)); | ||
} | ||
} | ||
stops = tmp; | ||
} | ||
@@ -74,22 +84,2 @@ return { | ||
value: [], | ||
validator: (value, props) => { | ||
const colors = props.colors ?? []; | ||
let stops = value; | ||
const tmp = value; | ||
if (stops.length === 0 || (stops && stops.length !== colors.length)) { | ||
for (let i = 0; i < colors.length; i++) { | ||
if (stops[i]) { | ||
tmp[i] = stops[i]; | ||
if (stops[i - 1] === undefined && tmp[i - 2] !== undefined) { | ||
tmp[i - 1] = tmp[i - 2] + (stops[i] - tmp[i - 2]) / 2; | ||
} | ||
} | ||
else { | ||
tmp[i] = i * (1 / (colors.length - 1)); | ||
} | ||
} | ||
stops = tmp; | ||
} | ||
return tmp; | ||
}, | ||
size: (props) => props.colors.length, | ||
@@ -100,2 +90,9 @@ method: 'uniform1fv', | ||
}; | ||
static ColorLoop = (amount) => { | ||
let loop = ''; | ||
for (let i = 2; i < amount; i++) { | ||
loop += `colorOut = mix(colorOut, colors[${i}], clamp((dist - stops[${i - 1}]) / (stops[${i}] - stops[${i - 1}]), 0.0, 1.0));`; | ||
} | ||
return loop; | ||
}; | ||
static onColorize = (props) => { | ||
@@ -111,6 +108,3 @@ const colors = props.colors.length || 1; | ||
vec4 colorOut = mix(colors[0], colors[1], stopCalc); | ||
for(int i = 1; i < ${colors}-1; i++) { | ||
stopCalc = (dist - stops[i]) / (stops[i + 1] - stops[i]); | ||
colorOut = mix(colorOut, colors[i + 1], clamp(stopCalc, 0.0, 1.0)); | ||
} | ||
${this.ColorLoop(colors)} | ||
return mix(maskColor, colorOut, clamp(colorOut.a, 0.0, 1.0)); | ||
@@ -117,0 +111,0 @@ `; |
import type { Dimensions } from '../../../common/CommonTypes.js'; | ||
import type { TextureMemoryManager } from '../../TextureMemoryManager.js'; | ||
import type { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js'; | ||
@@ -6,4 +7,4 @@ import type { SubTexture } from '../../textures/SubTexture.js'; | ||
export declare class WebGlCoreCtxSubTexture extends WebGlCoreCtxTexture { | ||
constructor(glw: WebGlContextWrapper, textureSource: SubTexture); | ||
constructor(glw: WebGlContextWrapper, memManager: TextureMemoryManager, textureSource: SubTexture); | ||
onLoadRequest(): Promise<Dimensions>; | ||
} |
@@ -21,4 +21,4 @@ /* | ||
export class WebGlCoreCtxSubTexture extends WebGlCoreCtxTexture { | ||
constructor(glw, textureSource) { | ||
super(glw, textureSource); | ||
constructor(glw, memManager, textureSource) { | ||
super(glw, memManager, textureSource); | ||
} | ||
@@ -25,0 +25,0 @@ async onLoadRequest() { |
import type { Dimensions } from '../../../common/CommonTypes.js'; | ||
import type { TextureMemoryManager } from '../../TextureMemoryManager.js'; | ||
import type { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js'; | ||
@@ -22,4 +23,5 @@ import type { Texture } from '../../textures/Texture.js'; | ||
private _h; | ||
constructor(glw: WebGlContextWrapper, textureSource: Texture); | ||
constructor(glw: WebGlContextWrapper, memManager: TextureMemoryManager, textureSource: Texture); | ||
get ctxTexture(): WebGLTexture; | ||
get renderable(): boolean; | ||
get w(): number; | ||
@@ -26,0 +28,0 @@ get h(): number; |
@@ -41,4 +41,4 @@ /* | ||
_h = 0; | ||
constructor(glw, textureSource) { | ||
super(textureSource); | ||
constructor(glw, memManager, textureSource) { | ||
super(memManager, textureSource); | ||
this.glw = glw; | ||
@@ -53,2 +53,5 @@ } | ||
} | ||
get renderable() { | ||
return this.textureSource.renderable; | ||
} | ||
get w() { | ||
@@ -76,4 +79,8 @@ return this._w; | ||
this.textureSource.setState('loading'); | ||
this._nativeCtxTexture = this.createNativeCtxTexture(); | ||
this.onLoadRequest() | ||
.then(({ width, height }) => { | ||
if (this._state === 'freed') { | ||
return; | ||
} | ||
this._state = 'loaded'; | ||
@@ -96,4 +103,3 @@ this._w = width; | ||
async onLoadRequest() { | ||
this._nativeCtxTexture = this.createNativeCtxTexture(); | ||
const { glw } = this; | ||
const { glw, memManager } = this; | ||
// On initial load request, create a 1x1 transparent texture to use until | ||
@@ -110,3 +116,9 @@ // the texture data is finally loaded. | ||
glw.texImage2D(0, glw.RGBA, 1, 1, 0, glw.RGBA, glw.UNSIGNED_BYTE, TRANSPARENT_TEXTURE_DATA); | ||
memManager.setTextureMemUse(this, 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'); | ||
return { width: 0, height: 0 }; | ||
} | ||
let width = 0; | ||
@@ -128,2 +140,3 @@ let height = 0; | ||
glw.texImage2D(0, glw.RGBA, glw.RGBA, glw.UNSIGNED_BYTE, data); | ||
memManager.setTextureMemUse(this, width * height * 4); | ||
// generate mipmaps for power-of-2 textures or in WebGL2RenderingContext | ||
@@ -140,2 +153,3 @@ if (glw.isWebGl2() || (isPowerOfTwo(width) && isPowerOfTwo(height))) { | ||
glw.texImage2D(0, glw.RGBA, 1, 1, 0, glw.RGBA, glw.UNSIGNED_BYTE, TRANSPARENT_TEXTURE_DATA); | ||
memManager.setTextureMemUse(this, TRANSPARENT_TEXTURE_DATA.byteLength); | ||
} | ||
@@ -153,2 +167,3 @@ else if ('mipmaps' in textureData.data && textureData.data.mipmaps) { | ||
glw.texParameteri(glw.TEXTURE_MIN_FILTER, glw.LINEAR); | ||
memManager.setTextureMemUse(this, view.byteLength); | ||
} | ||
@@ -173,2 +188,3 @@ else { | ||
this._state = 'freed'; | ||
this.textureSource.setState('freed'); | ||
this._w = 0; | ||
@@ -179,4 +195,5 @@ this._h = 0; | ||
} | ||
const { glw } = this; | ||
const { glw, memManager } = this; | ||
glw.deleteTexture(this._nativeCtxTexture); | ||
memManager.setTextureMemUse(this, 0); | ||
this._nativeCtxTexture = null; | ||
@@ -183,0 +200,0 @@ } |
@@ -13,2 +13,3 @@ import { CoreRenderer, type QuadOptions } from '../CoreRenderer.js'; | ||
import { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js'; | ||
import type { TextureMemoryManager } from '../../TextureMemoryManager.js'; | ||
export interface WebGlCoreRendererOptions { | ||
@@ -19,2 +20,3 @@ stage: Stage; | ||
txManager: CoreTextureManager; | ||
txMemManager: TextureMemoryManager; | ||
shManager: CoreShaderManager; | ||
@@ -33,2 +35,3 @@ clearColor: number; | ||
txManager: CoreTextureManager; | ||
txMemManager: TextureMemoryManager; | ||
shManager: CoreShaderManager; | ||
@@ -35,0 +38,0 @@ options: Required<WebGlCoreRendererOptions>; |
@@ -43,2 +43,3 @@ /* | ||
txManager; | ||
txMemManager; | ||
shManager; | ||
@@ -68,2 +69,3 @@ //// Options | ||
this.txManager = options.txManager; | ||
this.txMemManager = options.txMemManager; | ||
this.shManager = options.shManager; | ||
@@ -146,5 +148,5 @@ this.defaultTexture = new ColorTexture(this.txManager); | ||
if (textureSource instanceof SubTexture) { | ||
return new WebGlCoreCtxSubTexture(this.glw, textureSource); | ||
return new WebGlCoreCtxSubTexture(this.glw, this.txMemManager, textureSource); | ||
} | ||
return new WebGlCoreCtxTexture(this.glw, textureSource); | ||
return new WebGlCoreCtxTexture(this.glw, this.txMemManager, textureSource); | ||
} | ||
@@ -151,0 +153,0 @@ /** |
@@ -11,2 +11,3 @@ import { WebGlCoreRenderer } from './renderers/webgl/WebGlCoreRenderer.js'; | ||
import type { FpsUpdatePayload, FrameTickPayload } from '../common/CommonTypes.js'; | ||
import { TextureMemoryManager } from './TextureMemoryManager.js'; | ||
export interface StageOptions { | ||
@@ -16,2 +17,3 @@ rootId: number; | ||
appHeight: number; | ||
txMemByteThreshold: number; | ||
boundsMargin: number | [number, number, number, number]; | ||
@@ -35,2 +37,3 @@ deviceLogicalPixelRatio: number; | ||
readonly txManager: CoreTextureManager; | ||
readonly txMemManager: TextureMemoryManager; | ||
readonly fontManager: TrFontManager; | ||
@@ -65,2 +68,3 @@ readonly textRenderers: Partial<TextRendererMap>; | ||
drawFrame(): void; | ||
calculateFps(): void; | ||
addQuads(node: CoreNode): void; | ||
@@ -67,0 +71,0 @@ /** |
@@ -31,2 +31,3 @@ /* | ||
import { ContextSpy } from './lib/ContextSpy.js'; | ||
import { TextureMemoryManager } from './TextureMemoryManager.js'; | ||
const bufferMemory = 2e6; | ||
@@ -39,2 +40,3 @@ const autoStart = true; | ||
txManager; | ||
txMemManager; | ||
fontManager; | ||
@@ -61,4 +63,5 @@ textRenderers; | ||
this.options = options; | ||
const { canvas, clearColor, rootId, debug, appWidth, appHeight, boundsMargin, enableContextSpy, numImageWorkers, } = options; | ||
const { canvas, clearColor, rootId, debug, appWidth, appHeight, boundsMargin, enableContextSpy, numImageWorkers, txMemByteThreshold, } = options; | ||
this.txManager = new CoreTextureManager(numImageWorkers); | ||
this.txMemManager = new TextureMemoryManager(txMemByteThreshold); | ||
this.shManager = new CoreShaderManager(); | ||
@@ -89,2 +92,3 @@ this.animationManager = new AnimationManager(); | ||
txManager: this.txManager, | ||
txMemManager: this.txMemManager, | ||
shManager: this.shManager, | ||
@@ -180,2 +184,3 @@ contextSpy: this.contextSpy, | ||
renderer?.render(); | ||
this.calculateFps(); | ||
// Reset renderRequested flag if it was set | ||
@@ -185,2 +190,4 @@ if (renderRequested) { | ||
} | ||
} | ||
calculateFps() { | ||
// If there's an FPS update interval, emit the FPS update event | ||
@@ -187,0 +194,0 @@ // when the specified interval has elapsed. |
@@ -44,2 +44,4 @@ import type { Stage } from '../../Stage.js'; | ||
renderQuads(state: CanvasTextRendererState, transform: Matrix3d, clippingRect: RectWithValid, alpha: number): void; | ||
setIsRenderable(state: CanvasTextRendererState, renderable: boolean): void; | ||
destroyState(state: CanvasTextRendererState): void; | ||
/** | ||
@@ -46,0 +48,0 @@ * Invalidate the visible window stored in the state. This will cause a new |
@@ -216,2 +216,3 @@ /* | ||
fontFaceLoadedHandler: undefined, | ||
isRenderable: false, | ||
debugData: { | ||
@@ -400,5 +401,7 @@ updateCount: 0, | ||
if (pageInfo.lineNumStart < 0) { | ||
pageInfo.texture?.setRenderableOwner(state, false); | ||
pageInfo.texture = this.stage.txManager.loadTexture('ImageTexture', { | ||
src: '', | ||
}); | ||
pageInfo.texture.setRenderableOwner(state, state.isRenderable); | ||
pageInfo.valid = true; | ||
@@ -412,2 +415,3 @@ continue; | ||
if (!(this.canvas.width === 0 || this.canvas.height === 0)) { | ||
pageInfo.texture?.setRenderableOwner(state, false); | ||
pageInfo.texture = this.stage.txManager.loadTexture('ImageTexture', { | ||
@@ -418,2 +422,3 @@ src: this.context.getImageData(0, 0, this.canvas.width, this.canvas.height), | ||
}); | ||
pageInfo.texture.setRenderableOwner(state, state.isRenderable); | ||
} | ||
@@ -559,2 +564,15 @@ pageInfo.valid = true; | ||
} | ||
setIsRenderable(state, renderable) { | ||
super.setIsRenderable(state, renderable); | ||
// Set state object owner from any canvas page textures | ||
state.canvasPages?.forEach((pageInfo) => { | ||
pageInfo.texture?.setRenderableOwner(state, renderable); | ||
}); | ||
} | ||
destroyState(state) { | ||
// Remove state object owner from any canvas page textures | ||
state.canvasPages?.forEach((pageInfo) => { | ||
pageInfo.texture?.setRenderableOwner(state, false); | ||
}); | ||
} | ||
//#endregion Overrides | ||
@@ -561,0 +579,0 @@ /** |
@@ -145,4 +145,2 @@ /* | ||
lastWord.codepointIndex !== -1 && | ||
// We have advanced at least one character since the last word started | ||
lastWord.codepointIndex < glyph.cluster && | ||
// Prevents infinite loop when a single word is longer than the width | ||
@@ -203,4 +201,4 @@ lastWord.xStart > 0) { | ||
maxY = Math.max(maxY, quadY + glyph.height); | ||
maxX = Math.max(maxX, quadX + glyph.width); | ||
curX += glyph.xAdvance; | ||
maxX = Math.max(maxX, curX); | ||
} | ||
@@ -207,0 +205,0 @@ } |
@@ -65,4 +65,12 @@ import { type BoundWithValid, type RectWithValid } from '../../../lib/utils.js'; | ||
renderQuads(state: SdfTextRendererState, transform: Matrix3d, clippingRect: Readonly<RectWithValid>, alpha: number): void; | ||
setIsRenderable(state: SdfTextRendererState, renderable: boolean): void; | ||
destroyState(state: SdfTextRendererState): void; | ||
resolveFontFace(props: TrFontProps): SdfTrFontFace | undefined; | ||
/** | ||
* Release the loaded SDF font face | ||
* | ||
* @param state | ||
*/ | ||
protected releaseFontFace(state: SdfTextRendererState): void; | ||
/** | ||
* Invalidate the layout cache stored in the state. This will cause the text | ||
@@ -69,0 +77,0 @@ * to be re-layed out on the next update. |
@@ -73,3 +73,3 @@ /* | ||
state.props.fontFamily = value; | ||
state.trFontFace = undefined; | ||
this.releaseFontFace(state); | ||
this.invalidateLayoutCache(state); | ||
@@ -79,3 +79,3 @@ }, | ||
state.props.fontWeight = value; | ||
state.trFontFace = undefined; | ||
this.releaseFontFace(state); | ||
this.invalidateLayoutCache(state); | ||
@@ -85,3 +85,3 @@ }, | ||
state.props.fontStyle = value; | ||
state.trFontFace = undefined; | ||
this.releaseFontFace(state); | ||
this.invalidateLayoutCache(state); | ||
@@ -91,3 +91,3 @@ }, | ||
state.props.fontStretch = value; | ||
state.trFontFace = undefined; | ||
this.releaseFontFace(state); | ||
this.invalidateLayoutCache(state); | ||
@@ -276,2 +276,3 @@ }, | ||
trFontFace: undefined, | ||
isRenderable: false, | ||
debugData: { | ||
@@ -306,2 +307,3 @@ updateCount: 0, | ||
} | ||
trFontFace.texture.setRenderableOwner(state, state.isRenderable); | ||
} | ||
@@ -528,2 +530,11 @@ // If the font hasn't been loaded yet, stop here. | ||
} | ||
setIsRenderable(state, renderable) { | ||
super.setIsRenderable(state, renderable); | ||
state.trFontFace?.texture.setRenderableOwner(state, renderable); | ||
} | ||
destroyState(state) { | ||
super.destroyState(state); | ||
// If there's a Font Face assigned we must free the owner relation to its texture | ||
state.trFontFace?.texture.setRenderableOwner(state, false); | ||
} | ||
//#endregion Overrides | ||
@@ -534,2 +545,13 @@ resolveFontFace(props) { | ||
/** | ||
* Release the loaded SDF font face | ||
* | ||
* @param state | ||
*/ | ||
releaseFontFace(state) { | ||
if (state.trFontFace) { | ||
state.trFontFace.texture.setRenderableOwner(state, false); | ||
state.trFontFace = undefined; | ||
} | ||
} | ||
/** | ||
* Invalidate the layout cache stored in the state. This will cause the text | ||
@@ -536,0 +558,0 @@ * to be re-layed out on the next update. |
@@ -40,2 +40,3 @@ import type { EventEmitter } from '../../../common/EventEmitter.js'; | ||
textH: number | undefined; | ||
isRenderable: boolean; | ||
debugData: { | ||
@@ -307,2 +308,10 @@ updateCount: number; | ||
/** | ||
* Allows the CoreTextNode to communicate changes to the isRenderable state of | ||
* the itself. | ||
* | ||
* @param state | ||
* @param renderable | ||
*/ | ||
setIsRenderable(state: StateT, renderable: boolean): void; | ||
/** | ||
* Called by constructor to get a map of property setter functions for this renderer. | ||
@@ -337,2 +346,12 @@ */ | ||
/** | ||
* Destroy/Clean up the state object | ||
* | ||
* @remarks | ||
* Opposite of createState(). Frees any event listeners / resources held by | ||
* the state that may not reliably get garbage collected. | ||
* | ||
* @param state | ||
*/ | ||
destroyState(state: StateT): void; | ||
/** | ||
* Schedule a state update via queueMicrotask | ||
@@ -339,0 +358,0 @@ * |
@@ -128,2 +128,28 @@ /* | ||
/** | ||
* Allows the CoreTextNode to communicate changes to the isRenderable state of | ||
* the itself. | ||
* | ||
* @param state | ||
* @param renderable | ||
*/ | ||
setIsRenderable(state, renderable) { | ||
state.isRenderable = renderable; | ||
} | ||
/** | ||
* Destroy/Clean up the state object | ||
* | ||
* @remarks | ||
* Opposite of createState(). Frees any event listeners / resources held by | ||
* the state that may not reliably get garbage collected. | ||
* | ||
* @param state | ||
*/ | ||
destroyState(state) { | ||
const stateEvents = ['loading', 'loaded', 'failed']; | ||
// Remove the old event listeners from previous state obj there was one | ||
stateEvents.forEach((eventName) => { | ||
state.emitter.off(eventName); | ||
}); | ||
} | ||
/** | ||
* Schedule a state update via queueMicrotask | ||
@@ -130,0 +156,0 @@ * |
@@ -6,2 +6,6 @@ import type { CoreTextureManager } from '../CoreTextureManager.js'; | ||
/** | ||
* Event handler for when a Texture is freed | ||
*/ | ||
export type TextureFreedEventHandler = (target: any) => void; | ||
/** | ||
* Event handler for when a Texture is loading | ||
@@ -60,4 +64,5 @@ */ | ||
} | ||
export type TextureState = 'loading' | 'loaded' | 'failed'; | ||
export type TextureState = 'freed' | 'loading' | 'loaded' | 'failed'; | ||
export interface TextureStateEventMap { | ||
freed: TextureFreedEventHandler; | ||
loading: TextureLoadingEventHandler; | ||
@@ -93,4 +98,24 @@ loaded: TextureLoadedEventHandler; | ||
readonly state: TextureState; | ||
readonly renderableOwners: Set<unknown>; | ||
constructor(txManager: CoreTextureManager); | ||
/** | ||
* Add/remove an owner to/from the Texture based on its renderability. | ||
* | ||
* @remarks | ||
* Any object can own a texture, be it a CoreNode or even the state object | ||
* from a Text Renderer. | ||
* | ||
* When the reference to the texture that an owner object holds is replaced | ||
* or cleared it must call this with `renderable=false` to release the owner | ||
* association. | ||
* | ||
* @param owner | ||
* @param renderable | ||
*/ | ||
setRenderableOwner(owner: unknown, renderable: boolean): void; | ||
/** | ||
* Returns true if the texture is assigned to any Nodes that are renderable. | ||
*/ | ||
get renderable(): boolean; | ||
/** | ||
* Set the state of the texture | ||
@@ -97,0 +122,0 @@ * |
@@ -40,3 +40,4 @@ /* | ||
error = null; | ||
state = 'loading'; | ||
state = 'freed'; | ||
renderableOwners = new Set(); | ||
constructor(txManager) { | ||
@@ -47,2 +48,30 @@ super(); | ||
/** | ||
* Add/remove an owner to/from the Texture based on its renderability. | ||
* | ||
* @remarks | ||
* Any object can own a texture, be it a CoreNode or even the state object | ||
* from a Text Renderer. | ||
* | ||
* When the reference to the texture that an owner object holds is replaced | ||
* or cleared it must call this with `renderable=false` to release the owner | ||
* association. | ||
* | ||
* @param owner | ||
* @param renderable | ||
*/ | ||
setRenderableOwner(owner, renderable) { | ||
if (renderable) { | ||
this.renderableOwners.add(owner); | ||
} | ||
else { | ||
this.renderableOwners.delete(owner); | ||
} | ||
} | ||
/** | ||
* Returns true if the texture is assigned to any Nodes that are renderable. | ||
*/ | ||
get renderable() { | ||
return this.renderableOwners.size > 0; | ||
} | ||
/** | ||
* Set the state of the texture | ||
@@ -49,0 +78,0 @@ * |
@@ -23,2 +23,3 @@ import type { FpsUpdatePayload, FrameTickPayload } from '../common/CommonTypes.js'; | ||
onFrameTick(frameTickData: FrameTickPayload): void; | ||
onIdle?(): void; | ||
} |
import {} from './RendererMain.js'; | ||
import { isProductionEnvironment } from '../utils.js'; | ||
const stylePropertyMap = { | ||
@@ -105,3 +106,3 @@ alpha: (v) => { | ||
constructor(canvas, settings) { | ||
if (import.meta.env.PROD) | ||
if (isProductionEnvironment()) | ||
return; | ||
@@ -108,0 +109,0 @@ if (!settings) { |
@@ -84,6 +84,15 @@ import type { ShaderMap } from '../core/CoreShaderManager.js'; | ||
/** | ||
* Bounds margin to extend the boundary in which a CoreNode is added as Quad. | ||
* Texture Memory Byte Threshold | ||
* | ||
* @remarks | ||
* When the amount of GPU VRAM used by textures exceeds this threshold, | ||
* the Renderer will free up all the textures that are current not visible | ||
* within the configured `boundsMargin`. | ||
* | ||
* When set to `0`, the threshold-based texture memory manager is disabled. | ||
*/ | ||
txMemByteThreshold?: number; | ||
/** | ||
* Bounds margin to extend the boundary in which a CoreNode is added as Quad. | ||
*/ | ||
boundsMargin?: number | [number, number, number, number]; | ||
@@ -90,0 +99,0 @@ /** |
@@ -24,2 +24,3 @@ /* | ||
import { santizeCustomDataMap } from '../render-drivers/utils.js'; | ||
import { isProductionEnvironment } from '../utils.js'; | ||
/** | ||
@@ -76,2 +77,3 @@ * The Renderer Main API | ||
appHeight: settings.appHeight || 1080, | ||
txMemByteThreshold: settings.txMemByteThreshold || 124e6, | ||
boundsMargin: settings.boundsMargin || 0, | ||
@@ -131,4 +133,7 @@ deviceLogicalPixelRatio: settings.deviceLogicalPixelRatio || 1, | ||
}; | ||
driver.onIdle = () => { | ||
this.emit('idle'); | ||
}; | ||
targetEl.appendChild(canvas); | ||
if (enableInspector && !import.meta.env.PROD) { | ||
if (enableInspector && !isProductionEnvironment()) { | ||
this.inspector = new Inspector(canvas, resolvedSettings); | ||
@@ -135,0 +140,0 @@ } |
@@ -20,2 +20,3 @@ import type { ICoreDriver } from '../../main-api/ICoreDriver.js'; | ||
onFrameTick(frameTickData: FrameTickPayload): void; | ||
onIdle(): void; | ||
} |
@@ -33,2 +33,3 @@ /* | ||
appHeight: rendererSettings.appHeight, | ||
txMemByteThreshold: rendererSettings.txMemByteThreshold, | ||
boundsMargin: rendererSettings.boundsMargin, | ||
@@ -63,2 +64,5 @@ deviceLogicalPixelRatio: rendererSettings.deviceLogicalPixelRatio, | ||
})); | ||
this.stage.on('idle', () => { | ||
this.onIdle(); | ||
}); | ||
} | ||
@@ -108,3 +112,6 @@ createNode(props) { | ||
} | ||
onIdle() { | ||
throw new Error('Method not implemented.'); | ||
} | ||
} | ||
//# sourceMappingURL=MainCoreDriver.js.map |
@@ -84,2 +84,3 @@ import type { CustomDataMap, INode, INodeAnimatableProps, INodeWritableProps } from '../../main-api/INode.js'; | ||
private onTextureFailed; | ||
private onTextureFreed; | ||
private onOutOfBounds; | ||
@@ -86,0 +87,0 @@ private onInBounds; |
@@ -85,2 +85,3 @@ /* | ||
this.coreNode.on('failed', this.onTextureFailed); | ||
this.coreNode.on('freed', this.onTextureFreed); | ||
this.coreNode.on('outOfBounds', this.onOutOfBounds); | ||
@@ -330,2 +331,5 @@ this.coreNode.on('inBounds', this.onInBounds); | ||
}; | ||
onTextureFreed = (target, payload) => { | ||
this.emit('freed', payload); | ||
}; | ||
onOutOfBounds = (target, payload) => { | ||
@@ -364,9 +368,9 @@ this.emit('outOfBounds', payload); | ||
this.emit('beforeDestroy', {}); | ||
//use while loop since setting parent to null removes it from array | ||
let child = this.children[0]; | ||
while (child) { | ||
child.destroy(); | ||
child = this.children[0]; | ||
} | ||
this.coreNode.destroy(); | ||
// destroy children | ||
const length = this.children.length; | ||
for (let i = 0; i < length; i++) { | ||
this.children[i]?.destroy(); | ||
} | ||
this.children.length = 0; | ||
this.parent = null; | ||
@@ -373,0 +377,0 @@ this.texture = null; |
@@ -76,2 +76,3 @@ /* | ||
appHeight: rendererSettings.appHeight, | ||
txMemByteThreshold: rendererSettings.txMemByteThreshold, | ||
boundsMargin: rendererSettings.boundsMargin, | ||
@@ -78,0 +79,0 @@ deviceLogicalPixelRatio: rendererSettings.deviceLogicalPixelRatio, |
@@ -22,2 +22,3 @@ import type { FpsUpdatePayload, FrameTickPayload } from '../../common/CommonTypes.js'; | ||
appHeight: number; | ||
txMemByteThreshold: number; | ||
boundsMargin: number | [number, number, number, number]; | ||
@@ -24,0 +25,0 @@ deviceLogicalPixelRatio: number; |
@@ -64,2 +64,3 @@ /* | ||
appHeight: message.appHeight, | ||
txMemByteThreshold: message.txMemByteThreshold, | ||
boundsMargin: message.boundsMargin, | ||
@@ -66,0 +67,0 @@ deviceLogicalPixelRatio: message.deviceLogicalPixelRatio, |
@@ -95,2 +95,5 @@ /* | ||
}); | ||
this.coreNode.on('freed', (target, payload) => { | ||
this.emit('freed', payload); | ||
}); | ||
} | ||
@@ -97,0 +100,0 @@ onPropertyChange(propName, newValue, oldValue) { |
@@ -84,1 +84,7 @@ import type { ContextSpy } from './core/lib/ContextSpy.js'; | ||
export declare function deg2Rad(degrees: number): number; | ||
/** | ||
* Checks import.meta if env is production | ||
* | ||
* @returns | ||
*/ | ||
export declare function isProductionEnvironment(): boolean; |
@@ -68,3 +68,3 @@ /* | ||
export function assertTruthy(condition, message) { | ||
if (import.meta.env.PROD) | ||
if (isProductionEnvironment()) | ||
return; | ||
@@ -177,2 +177,10 @@ if (!condition) { | ||
} | ||
/** | ||
* Checks import.meta if env is production | ||
* | ||
* @returns | ||
*/ | ||
export function isProductionEnvironment() { | ||
return import.meta.env && import.meta.env.PROD; | ||
} | ||
//# sourceMappingURL=utils.js.map |
{ | ||
"name": "@lightningjs/renderer", | ||
"version": "0.7.6", | ||
"version": "0.8.0", | ||
"description": "Lightning 3 Renderer", | ||
@@ -5,0 +5,0 @@ "type": "module", |
@@ -84,2 +84,6 @@ # Lightning 3 Renderer (Beta) | ||
## Manual Regression Tests | ||
See [docs/ManualRegressionTests.md]. | ||
## Release Procedure | ||
@@ -86,0 +90,0 @@ |
@@ -78,2 +78,9 @@ /* | ||
/** | ||
* Payload for when texture failed to load | ||
*/ | ||
export type NodeTextureFreedPayload = { | ||
type: 'texture'; | ||
}; | ||
/** | ||
* Combined type for all failed payloads | ||
@@ -80,0 +87,0 @@ */ |
@@ -33,2 +33,3 @@ /* | ||
TextureFailedEventHandler, | ||
TextureFreedEventHandler, | ||
TextureLoadedEventHandler, | ||
@@ -38,2 +39,3 @@ } from './textures/Texture.js'; | ||
NodeTextureFailedPayload, | ||
NodeTextureFreedPayload, | ||
NodeTextureLoadedPayload, | ||
@@ -116,2 +118,6 @@ } from '../common/CommonTypes.js'; | ||
* Scale/Rotate transform update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `scaleRotateTransform` | ||
*/ | ||
@@ -122,2 +128,6 @@ ScaleRotate = 2, | ||
* Translate transform update (x/y/width/height/pivot/mount) | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `localTransform` | ||
*/ | ||
@@ -127,3 +137,9 @@ Local = 4, | ||
/** | ||
* Global transform update | ||
* Global Transform update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `globalTransform` | ||
* - `renderCoords` | ||
* - `renderBound` | ||
*/ | ||
@@ -134,2 +150,6 @@ Global = 8, | ||
* Clipping rect update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `clippingRect` | ||
*/ | ||
@@ -140,2 +160,6 @@ Clipping = 16, | ||
* Calculated ZIndex update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `calcZIndex` | ||
*/ | ||
@@ -146,2 +170,6 @@ CalculatedZIndex = 32, | ||
* Z-Index Sorted Children update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `children` (sorts children by their `calcZIndex`) | ||
*/ | ||
@@ -151,3 +179,10 @@ ZIndexSortedChildren = 64, | ||
/** | ||
* Premultiplied Colors | ||
* Premultiplied Colors update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `premultipliedColorTl` | ||
* - `premultipliedColorTr` | ||
* - `premultipliedColorBl` | ||
* - `premultipliedColorBr` | ||
*/ | ||
@@ -157,6 +192,7 @@ PremultipliedColors = 128, | ||
/** | ||
* World Alpha | ||
* World Alpha update | ||
* | ||
* @remarks | ||
* World Alpha = Parent World Alpha * Alpha | ||
* CoreNode Properties Updated: | ||
* - `worldAlpha` = `parent.worldAlpha` * `alpha` | ||
*/ | ||
@@ -166,2 +202,20 @@ WorldAlpha = 256, | ||
/** | ||
* Render State update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `renderState` | ||
*/ | ||
RenderState = 512, | ||
/** | ||
* Is Renderable update | ||
* | ||
* @remarks | ||
* CoreNode Properties Updated: | ||
* - `isRenderable` | ||
*/ | ||
IsRenderable = 1024, | ||
/** | ||
* None | ||
@@ -174,3 +228,3 @@ */ | ||
*/ | ||
All = 511, | ||
All = 2047, | ||
} | ||
@@ -234,3 +288,3 @@ | ||
this.props.textureOptions = options; | ||
this.updateIsRenderable(); | ||
this.setUpdateType(UpdateType.IsRenderable); | ||
@@ -246,5 +300,8 @@ // If texture is already loaded / failed, trigger loaded event manually | ||
this.onTextureFailed(texture, texture.error!); | ||
} else if (texture.state === 'freed') { | ||
this.onTextureFreed(texture); | ||
} | ||
texture.on('loaded', this.onTextureLoaded); | ||
texture.on('failed', this.onTextureFailed); | ||
texture.on('freed', this.onTextureFreed); | ||
}); | ||
@@ -255,8 +312,11 @@ } | ||
if (this.props.texture) { | ||
this.props.texture.off('loaded', this.onTextureLoaded); | ||
this.props.texture.off('failed', this.onTextureFailed); | ||
const { texture } = this.props; | ||
texture.off('loaded', this.onTextureLoaded); | ||
texture.off('failed', this.onTextureFailed); | ||
texture.off('freed', this.onTextureFreed); | ||
texture.setRenderableOwner(this, false); | ||
} | ||
this.props.texture = null; | ||
this.props.textureOptions = null; | ||
this.updateIsRenderable(); | ||
this.setUpdateType(UpdateType.IsRenderable); | ||
} | ||
@@ -280,2 +340,8 @@ | ||
}; | ||
private onTextureFreed: TextureFreedEventHandler = (target: Texture) => { | ||
this.emit('freed', { | ||
type: 'texture', | ||
} satisfies NodeTextureFreedPayload); | ||
}; | ||
//#endregion Textures | ||
@@ -292,3 +358,3 @@ | ||
this.props.shaderProps = p; | ||
this.updateIsRenderable(); | ||
this.setUpdateType(UpdateType.IsRenderable); | ||
} | ||
@@ -373,4 +439,5 @@ | ||
this.updateBoundingRect(); | ||
this.updateRenderState(); | ||
this.setUpdateType(UpdateType.Clipping | UpdateType.Children); | ||
this.setUpdateType( | ||
UpdateType.Clipping | UpdateType.RenderState | UpdateType.Children, | ||
); | ||
childUpdateType |= UpdateType.Global; | ||
@@ -429,5 +496,11 @@ } | ||
} | ||
} | ||
if (this.updateType & UpdateType.RenderState) { | ||
this.updateRenderState(); | ||
this.setUpdateType(UpdateType.IsRenderable); | ||
} | ||
if (this.updateType & UpdateType.IsRenderable) { | ||
this.updateIsRenderable(); | ||
this.setUpdateType(UpdateType.Children); | ||
childUpdateType |= UpdateType.PremultipliedColors; | ||
} | ||
@@ -561,3 +634,3 @@ | ||
if (previous === CoreNodeRenderState.InViewport) { | ||
this.emit('outOfViewPort', { | ||
this.emit('outOfViewport', { | ||
previous, | ||
@@ -574,3 +647,2 @@ current: renderState, | ||
} | ||
this.updateIsRenderable(); | ||
} | ||
@@ -585,11 +657,24 @@ | ||
// This function checks if the current node is renderable based on certain properties. | ||
// It returns true if any of the specified properties are truthy or if any color property is not 0, otherwise it returns false. | ||
/** | ||
* This function updates the `isRenderable` property based on certain conditions. | ||
* | ||
* @returns | ||
*/ | ||
updateIsRenderable() { | ||
let newIsRenderable; | ||
if (!this.checkRenderProps()) { | ||
return (this.isRenderable = false); | ||
newIsRenderable = false; | ||
} else { | ||
newIsRenderable = this.renderState > CoreNodeRenderState.OutOfBounds; | ||
} | ||
this.isRenderable = this.renderState > CoreNodeRenderState.OutOfBounds; | ||
if (this.isRenderable !== newIsRenderable) { | ||
this.isRenderable = newIsRenderable; | ||
this.onChangeIsRenderable(newIsRenderable); | ||
} | ||
} | ||
onChangeIsRenderable(isRenderable: boolean) { | ||
this.props.texture?.setRenderableOwner(this, isRenderable); | ||
} | ||
calculateRenderCoords() { | ||
@@ -596,0 +681,0 @@ const { width, height, globalTransform: transform } = this; |
@@ -58,31 +58,28 @@ /* | ||
const { resolvedTextRenderer, textRendererState } = | ||
this.resolveTextRendererAndState( | ||
{ | ||
x: this.absX, | ||
y: this.absY, | ||
width: props.width, | ||
height: props.height, | ||
textAlign: props.textAlign, | ||
color: props.color, | ||
zIndex: props.zIndex, | ||
contain: props.contain, | ||
scrollable: props.scrollable, | ||
scrollY: props.scrollY, | ||
offsetY: props.offsetY, | ||
letterSpacing: props.letterSpacing, | ||
debug: props.debug, | ||
fontFamily: props.fontFamily, | ||
fontSize: props.fontSize, | ||
fontStretch: props.fontStretch, | ||
fontStyle: props.fontStyle, | ||
fontWeight: props.fontWeight, | ||
text: props.text, | ||
lineHeight: props.lineHeight, | ||
maxLines: props.maxLines, | ||
textBaseline: props.textBaseline, | ||
verticalAlign: props.verticalAlign, | ||
overflowSuffix: props.overflowSuffix, | ||
}, | ||
undefined, | ||
); | ||
this.resolveTextRendererAndState({ | ||
x: this.absX, | ||
y: this.absY, | ||
width: props.width, | ||
height: props.height, | ||
textAlign: props.textAlign, | ||
color: props.color, | ||
zIndex: props.zIndex, | ||
contain: props.contain, | ||
scrollable: props.scrollable, | ||
scrollY: props.scrollY, | ||
offsetY: props.offsetY, | ||
letterSpacing: props.letterSpacing, | ||
debug: props.debug, | ||
fontFamily: props.fontFamily, | ||
fontSize: props.fontSize, | ||
fontStretch: props.fontStretch, | ||
fontStyle: props.fontStyle, | ||
fontWeight: props.fontWeight, | ||
text: props.text, | ||
lineHeight: props.lineHeight, | ||
maxLines: props.maxLines, | ||
textBaseline: props.textBaseline, | ||
verticalAlign: props.verticalAlign, | ||
overflowSuffix: props.overflowSuffix, | ||
}); | ||
this.textRenderer = resolvedTextRenderer; | ||
@@ -183,4 +180,6 @@ this.trState = textRendererState; | ||
this.textRenderer.destroyState(this.trState); | ||
const { resolvedTextRenderer, textRendererState } = | ||
this.resolveTextRendererAndState(this.trState.props, this.trState); | ||
this.resolveTextRendererAndState(this.trState.props); | ||
this.textRenderer = resolvedTextRenderer; | ||
@@ -353,2 +352,7 @@ this.trState = textRendererState; | ||
override onChangeIsRenderable(isRenderable: boolean) { | ||
super.onChangeIsRenderable(isRenderable); | ||
this.textRenderer.setIsRenderable(this.trState, isRenderable); | ||
} | ||
override renderQuads(renderer: CoreRenderer) { | ||
@@ -365,2 +369,11 @@ assertTruthy(this.globalTransform); | ||
/** | ||
* Destroy the node and cleanup all resources | ||
*/ | ||
override destroy(): void { | ||
super.destroy(); | ||
this.textRenderer.destroyState(this.trState); | ||
} | ||
/** | ||
* Resolve a text renderer and a new state based on the current text renderer props provided | ||
@@ -370,6 +383,3 @@ * @param props | ||
*/ | ||
private resolveTextRendererAndState( | ||
props: TrProps, | ||
prevState?: TextRendererState, | ||
): { | ||
private resolveTextRendererAndState(props: TrProps): { | ||
resolvedTextRenderer: TextRenderer; | ||
@@ -385,11 +395,2 @@ textRendererState: TextRendererState; | ||
const stateEvents = ['loading', 'loaded', 'failed']; | ||
if (prevState) { | ||
// Remove the old event listeners from previous state obj there was one | ||
stateEvents.forEach((eventName) => { | ||
prevState.emitter.off(eventName); | ||
}); | ||
} | ||
textRendererState.emitter.on('loaded', this.onTextLoaded); | ||
@@ -396,0 +397,0 @@ textRendererState.emitter.on('failed', this.onTextFailed); |
@@ -291,2 +291,4 @@ /* | ||
} | ||
// Free the ctx texture if it exists. | ||
this.ctxTextureCache.get(texture)?.free(); | ||
} | ||
@@ -293,0 +295,0 @@ |
@@ -26,2 +26,3 @@ /* | ||
export const startLoop = (stage: Stage) => { | ||
let isIdle = false; | ||
const runLoop = () => { | ||
@@ -31,6 +32,13 @@ stage.updateAnimations(); | ||
if (!stage.hasSceneUpdates()) { | ||
// We still need to calculate the fps else it looks like the app is frozen | ||
stage.calculateFps(); | ||
setTimeout(runLoop, 16.666666666666668); | ||
if (!isIdle) { | ||
stage.emit('idle'); | ||
isIdle = true; | ||
} | ||
return; | ||
} | ||
isIdle = false; | ||
stage.drawFrame(); | ||
@@ -37,0 +45,0 @@ requestAnimationFrame(runLoop); |
@@ -20,8 +20,11 @@ /* | ||
import type { TextureMemoryManager } from '../TextureMemoryManager.js'; | ||
import type { Texture } from '../textures/Texture.js'; | ||
export abstract class CoreContextTexture { | ||
readonly memManager: TextureMemoryManager; | ||
readonly textureSource: Texture; | ||
constructor(textureSource: Texture) { | ||
constructor(memManager: TextureMemoryManager, textureSource: Texture) { | ||
this.memManager = memManager; | ||
this.textureSource = textureSource; | ||
@@ -31,2 +34,4 @@ } | ||
abstract load(): void; | ||
abstract free(): void; | ||
abstract get renderable(): boolean; | ||
} |
@@ -64,9 +64,18 @@ /* | ||
let stops = props.stops; | ||
if (!stops) { | ||
stops = []; | ||
const calc = colors.length - 1; | ||
for (let i = 0; i < colors.length; i++) { | ||
stops.push(i * (1 / calc)); | ||
let stops = props.stops || []; | ||
if (stops.length === 0 || stops.length !== colors.length) { | ||
const colorsL = colors.length; | ||
let i = 0; | ||
const tmp = stops; | ||
for (; i < colorsL; i++) { | ||
if (stops[i]) { | ||
tmp[i] = stops[i]!; | ||
if (stops[i - 1] === undefined && tmp[i - 2] !== undefined) { | ||
tmp[i - 1] = tmp[i - 2]! + (stops[i]! - tmp[i - 2]!) / 2; | ||
} | ||
} else { | ||
tmp[i] = i * (1 / (colors.length - 1)); | ||
} | ||
} | ||
stops = tmp; | ||
} | ||
@@ -98,24 +107,2 @@ return { | ||
value: [], | ||
validator: ( | ||
value: number[], | ||
props: LinearGradientEffectProps, | ||
): number[] => { | ||
const colors = props.colors ?? []; | ||
let stops = value; | ||
const tmp: number[] = value; | ||
if (stops.length === 0 || (stops && stops.length !== colors.length)) { | ||
for (let i = 0; i < colors.length; i++) { | ||
if (stops[i]) { | ||
tmp[i] = stops[i]!; | ||
if (stops[i - 1] === undefined && tmp[i - 2] !== undefined) { | ||
tmp[i - 1] = tmp[i - 2]! + (stops[i]! - tmp[i - 2]!) / 2; | ||
} | ||
} else { | ||
tmp[i] = i * (1 / (colors.length - 1)); | ||
} | ||
} | ||
stops = tmp; | ||
} | ||
return tmp; | ||
}, | ||
size: (props: LinearGradientEffectProps) => props.colors!.length, | ||
@@ -171,6 +158,3 @@ method: 'uniform1fv', | ||
vec4 colorOut = $fromLinear(mix($toLinear(colors[0]), $toLinear(colors[1]), stopCalc)); | ||
for(int i = 1; i < ${colors}-1; i++) { | ||
stopCalc = (dist - stops[i]) / (stops[i + 1] - stops[i]); | ||
colorOut = mix(colorOut, colors[i + 1], clamp(stopCalc, 0.0, 1.0)); | ||
} | ||
${this.ColorLoop(colors)} | ||
return mix(maskColor, colorOut, clamp(colorOut.a, 0.0, 1.0)); | ||
@@ -177,0 +161,0 @@ `; |
@@ -69,9 +69,18 @@ /* | ||
let stops = props.stops; | ||
if (!stops) { | ||
stops = []; | ||
const calc = colors.length - 1; | ||
for (let i = 0; i < colors.length; i++) { | ||
stops.push(i * (1 / calc)); | ||
let stops = props.stops || []; | ||
if (stops.length === 0 || stops.length !== colors.length) { | ||
const colorsL = colors.length; | ||
let i = 0; | ||
const tmp = stops; | ||
for (; i < colorsL; i++) { | ||
if (stops[i]) { | ||
tmp[i] = stops[i]!; | ||
if (stops[i - 1] === undefined && tmp[i - 2] !== undefined) { | ||
tmp[i - 1] = tmp[i - 2]! + (stops[i]! - tmp[i - 2]!) / 2; | ||
} | ||
} else { | ||
tmp[i] = i * (1 / (colors.length - 1)); | ||
} | ||
} | ||
stops = tmp; | ||
} | ||
@@ -115,24 +124,2 @@ return { | ||
value: [], | ||
validator: ( | ||
value: number[], | ||
props: RadialGradientEffectProps, | ||
): number[] => { | ||
const colors = props.colors ?? []; | ||
let stops = value; | ||
const tmp: number[] = value; | ||
if (stops.length === 0 || (stops && stops.length !== colors.length)) { | ||
for (let i = 0; i < colors.length; i++) { | ||
if (stops[i]) { | ||
tmp[i] = stops[i]!; | ||
if (stops[i - 1] === undefined && tmp[i - 2] !== undefined) { | ||
tmp[i - 1] = tmp[i - 2]! + (stops[i]! - tmp[i - 2]!) / 2; | ||
} | ||
} else { | ||
tmp[i] = i * (1 / (colors.length - 1)); | ||
} | ||
} | ||
stops = tmp; | ||
} | ||
return tmp; | ||
}, | ||
size: (props: RadialGradientEffectProps) => props.colors!.length, | ||
@@ -144,2 +131,12 @@ method: 'uniform1fv', | ||
static ColorLoop = (amount: number): string => { | ||
let loop = ''; | ||
for (let i = 2; i < amount; i++) { | ||
loop += `colorOut = mix(colorOut, colors[${i}], clamp((dist - stops[${ | ||
i - 1 | ||
}]) / (stops[${i}] - stops[${i - 1}]), 0.0, 1.0));`; | ||
} | ||
return loop; | ||
}; | ||
static override onColorize = (props: RadialGradientEffectProps) => { | ||
@@ -155,6 +152,3 @@ const colors = props.colors!.length || 1; | ||
vec4 colorOut = mix(colors[0], colors[1], stopCalc); | ||
for(int i = 1; i < ${colors}-1; i++) { | ||
stopCalc = (dist - stops[i]) / (stops[i + 1] - stops[i]); | ||
colorOut = mix(colorOut, colors[i + 1], clamp(stopCalc, 0.0, 1.0)); | ||
} | ||
${this.ColorLoop(colors)} | ||
return mix(maskColor, colorOut, clamp(colorOut.a, 0.0, 1.0)); | ||
@@ -161,0 +155,0 @@ `; |
@@ -21,2 +21,3 @@ /* | ||
import type { Dimensions } from '../../../common/CommonTypes.js'; | ||
import type { TextureMemoryManager } from '../../TextureMemoryManager.js'; | ||
import type { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js'; | ||
@@ -27,4 +28,8 @@ import type { SubTexture } from '../../textures/SubTexture.js'; | ||
export class WebGlCoreCtxSubTexture extends WebGlCoreCtxTexture { | ||
constructor(glw: WebGlContextWrapper, textureSource: SubTexture) { | ||
super(glw, textureSource); | ||
constructor( | ||
glw: WebGlContextWrapper, | ||
memManager: TextureMemoryManager, | ||
textureSource: SubTexture, | ||
) { | ||
super(glw, memManager, textureSource); | ||
} | ||
@@ -31,0 +36,0 @@ |
@@ -22,2 +22,3 @@ /* | ||
import { assertTruthy } from '../../../utils.js'; | ||
import type { TextureMemoryManager } from '../../TextureMemoryManager.js'; | ||
import type { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js'; | ||
@@ -48,4 +49,8 @@ import type { Texture } from '../../textures/Texture.js'; | ||
constructor(protected glw: WebGlContextWrapper, textureSource: Texture) { | ||
super(textureSource); | ||
constructor( | ||
protected glw: WebGlContextWrapper, | ||
memManager: TextureMemoryManager, | ||
textureSource: Texture, | ||
) { | ||
super(memManager, textureSource); | ||
} | ||
@@ -61,2 +66,6 @@ | ||
get renderable(): boolean { | ||
return this.textureSource.renderable; | ||
} | ||
get w() { | ||
@@ -86,4 +95,8 @@ return this._w; | ||
this.textureSource.setState('loading'); | ||
this._nativeCtxTexture = this.createNativeCtxTexture(); | ||
this.onLoadRequest() | ||
.then(({ width, height }) => { | ||
if (this._state === 'freed') { | ||
return; | ||
} | ||
this._state = 'loaded'; | ||
@@ -107,4 +120,3 @@ this._w = width; | ||
async onLoadRequest(): Promise<Dimensions> { | ||
this._nativeCtxTexture = this.createNativeCtxTexture(); | ||
const { glw } = this; | ||
const { glw, memManager } = this; | ||
@@ -134,6 +146,13 @@ // On initial load request, create a 1x1 transparent texture to use until | ||
); | ||
memManager.setTextureMemUse(this, 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'); | ||
return { width: 0, height: 0 }; | ||
} | ||
let width = 0; | ||
let height = 0; | ||
assertTruthy(this._nativeCtxTexture); | ||
@@ -159,2 +178,3 @@ glw.activeTexture(0); | ||
glw.texImage2D(0, glw.RGBA, glw.RGBA, glw.UNSIGNED_BYTE, data); | ||
memManager.setTextureMemUse(this, width * height * 4); | ||
@@ -170,2 +190,3 @@ // generate mipmaps for power-of-2 textures or in WebGL2RenderingContext | ||
glw.bindTexture(this._nativeCtxTexture); | ||
glw.texImage2D( | ||
@@ -181,2 +202,3 @@ 0, | ||
); | ||
memManager.setTextureMemUse(this, TRANSPARENT_TEXTURE_DATA.byteLength); | ||
} else if ('mipmaps' in textureData.data && textureData.data.mipmaps) { | ||
@@ -196,4 +218,4 @@ const { | ||
glw.bindTexture(this._nativeCtxTexture); | ||
glw.compressedTexImage2D(0, glInternalFormat, width, height, 0, view); | ||
glw.texParameteri(glw.TEXTURE_WRAP_S, glw.CLAMP_TO_EDGE); | ||
@@ -203,2 +225,4 @@ glw.texParameteri(glw.TEXTURE_WRAP_T, glw.CLAMP_TO_EDGE); | ||
glw.texParameteri(glw.TEXTURE_MIN_FILTER, glw.LINEAR); | ||
memManager.setTextureMemUse(this, view.byteLength); | ||
} else { | ||
@@ -216,2 +240,3 @@ console.error( | ||
} | ||
/** | ||
@@ -227,2 +252,3 @@ * Free the WebGLTexture from the GPU | ||
this._state = 'freed'; | ||
this.textureSource.setState('freed'); | ||
this._w = 0; | ||
@@ -233,4 +259,6 @@ this._h = 0; | ||
} | ||
const { glw } = this; | ||
const { glw, memManager } = this; | ||
glw.deleteTexture(this._nativeCtxTexture); | ||
memManager.setTextureMemUse(this, 0); | ||
this._nativeCtxTexture = null; | ||
@@ -237,0 +265,0 @@ } |
@@ -60,2 +60,3 @@ /* | ||
import { WebGlContextWrapper } from '../../lib/WebGlContextWrapper.js'; | ||
import type { TextureMemoryManager } from '../../TextureMemoryManager.js'; | ||
@@ -70,2 +71,3 @@ const WORDS_PER_QUAD = 24; | ||
txManager: CoreTextureManager; | ||
txMemManager: TextureMemoryManager; | ||
shManager: CoreShaderManager; | ||
@@ -89,2 +91,3 @@ clearColor: number; | ||
txManager: CoreTextureManager; | ||
txMemManager: TextureMemoryManager; | ||
shManager: CoreShaderManager; | ||
@@ -120,2 +123,3 @@ | ||
this.txManager = options.txManager; | ||
this.txMemManager = options.txMemManager; | ||
this.shManager = options.shManager; | ||
@@ -205,5 +209,9 @@ this.defaultTexture = new ColorTexture(this.txManager); | ||
if (textureSource instanceof SubTexture) { | ||
return new WebGlCoreCtxSubTexture(this.glw, textureSource); | ||
return new WebGlCoreCtxSubTexture( | ||
this.glw, | ||
this.txMemManager, | ||
textureSource, | ||
); | ||
} | ||
return new WebGlCoreCtxTexture(this.glw, textureSource); | ||
return new WebGlCoreCtxTexture(this.glw, this.txMemManager, textureSource); | ||
} | ||
@@ -210,0 +218,0 @@ |
@@ -40,2 +40,3 @@ /* | ||
} from '../common/CommonTypes.js'; | ||
import { TextureMemoryManager } from './TextureMemoryManager.js'; | ||
@@ -46,2 +47,3 @@ export interface StageOptions { | ||
appHeight: number; | ||
txMemByteThreshold: number; | ||
boundsMargin: number | [number, number, number, number]; | ||
@@ -78,2 +80,3 @@ deviceLogicalPixelRatio: number; | ||
public readonly txManager: CoreTextureManager; | ||
public readonly txMemManager: TextureMemoryManager; | ||
public readonly fontManager: TrFontManager; | ||
@@ -112,5 +115,7 @@ public readonly textRenderers: Partial<TextRendererMap>; | ||
numImageWorkers, | ||
txMemByteThreshold, | ||
} = options; | ||
this.txManager = new CoreTextureManager(numImageWorkers); | ||
this.txMemManager = new TextureMemoryManager(txMemByteThreshold); | ||
this.shManager = new CoreShaderManager(); | ||
@@ -145,2 +150,3 @@ this.animationManager = new AnimationManager(); | ||
txManager: this.txManager, | ||
txMemManager: this.txMemManager, | ||
shManager: this.shManager, | ||
@@ -251,2 +257,4 @@ contextSpy: this.contextSpy, | ||
this.calculateFps(); | ||
// Reset renderRequested flag if it was set | ||
@@ -256,3 +264,5 @@ if (renderRequested) { | ||
} | ||
} | ||
calculateFps() { | ||
// If there's an FPS update interval, emit the FPS update event | ||
@@ -259,0 +269,0 @@ // when the specified interval has elapsed. |
@@ -293,2 +293,3 @@ /* | ||
fontFaceLoadedHandler: undefined, | ||
isRenderable: false, | ||
debugData: { | ||
@@ -504,5 +505,7 @@ updateCount: 0, | ||
if (pageInfo.lineNumStart < 0) { | ||
pageInfo.texture?.setRenderableOwner(state, false); | ||
pageInfo.texture = this.stage.txManager.loadTexture('ImageTexture', { | ||
src: '', | ||
}); | ||
pageInfo.texture.setRenderableOwner(state, state.isRenderable); | ||
pageInfo.valid = true; | ||
@@ -522,2 +525,3 @@ continue; | ||
if (!(this.canvas.width === 0 || this.canvas.height === 0)) { | ||
pageInfo.texture?.setRenderableOwner(state, false); | ||
pageInfo.texture = this.stage.txManager.loadTexture( | ||
@@ -537,2 +541,3 @@ 'ImageTexture', | ||
); | ||
pageInfo.texture.setRenderableOwner(state, state.isRenderable); | ||
} | ||
@@ -701,2 +706,20 @@ pageInfo.valid = true; | ||
} | ||
override setIsRenderable( | ||
state: CanvasTextRendererState, | ||
renderable: boolean, | ||
): void { | ||
super.setIsRenderable(state, renderable); | ||
// Set state object owner from any canvas page textures | ||
state.canvasPages?.forEach((pageInfo) => { | ||
pageInfo.texture?.setRenderableOwner(state, renderable); | ||
}); | ||
} | ||
override destroyState(state: CanvasTextRendererState): void { | ||
// Remove state object owner from any canvas page textures | ||
state.canvasPages?.forEach((pageInfo) => { | ||
pageInfo.texture?.setRenderableOwner(state, false); | ||
}); | ||
} | ||
//#endregion Overrides | ||
@@ -703,0 +726,0 @@ |
@@ -215,4 +215,2 @@ /* | ||
lastWord.codepointIndex !== -1 && | ||
// We have advanced at least one character since the last word started | ||
lastWord.codepointIndex < glyph.cluster && | ||
// Prevents infinite loop when a single word is longer than the width | ||
@@ -290,4 +288,4 @@ lastWord.xStart > 0 | ||
maxY = Math.max(maxY, quadY + glyph.height); | ||
maxX = Math.max(maxX, quadX + glyph.width); | ||
curX += glyph.xAdvance; | ||
maxX = Math.max(maxX, curX); | ||
} | ||
@@ -294,0 +292,0 @@ } else { |
@@ -153,3 +153,3 @@ /* | ||
state.props.fontFamily = value; | ||
state.trFontFace = undefined; | ||
this.releaseFontFace(state); | ||
this.invalidateLayoutCache(state); | ||
@@ -159,3 +159,3 @@ }, | ||
state.props.fontWeight = value; | ||
state.trFontFace = undefined; | ||
this.releaseFontFace(state); | ||
this.invalidateLayoutCache(state); | ||
@@ -165,3 +165,3 @@ }, | ||
state.props.fontStyle = value; | ||
state.trFontFace = undefined; | ||
this.releaseFontFace(state); | ||
this.invalidateLayoutCache(state); | ||
@@ -171,3 +171,3 @@ }, | ||
state.props.fontStretch = value; | ||
state.trFontFace = undefined; | ||
this.releaseFontFace(state); | ||
this.invalidateLayoutCache(state); | ||
@@ -367,2 +367,3 @@ }, | ||
trFontFace: undefined, | ||
isRenderable: false, | ||
debugData: { | ||
@@ -400,2 +401,3 @@ updateCount: 0, | ||
} | ||
trFontFace.texture.setRenderableOwner(state, state.isRenderable); | ||
} | ||
@@ -750,2 +752,16 @@ | ||
} | ||
override setIsRenderable( | ||
state: SdfTextRendererState, | ||
renderable: boolean, | ||
): void { | ||
super.setIsRenderable(state, renderable); | ||
state.trFontFace?.texture.setRenderableOwner(state, renderable); | ||
} | ||
override destroyState(state: SdfTextRendererState): void { | ||
super.destroyState(state); | ||
// If there's a Font Face assigned we must free the owner relation to its texture | ||
state.trFontFace?.texture.setRenderableOwner(state, false); | ||
} | ||
//#endregion Overrides | ||
@@ -760,2 +776,14 @@ | ||
/** | ||
* Release the loaded SDF font face | ||
* | ||
* @param state | ||
*/ | ||
protected releaseFontFace(state: SdfTextRendererState) { | ||
if (state.trFontFace) { | ||
state.trFontFace.texture.setRenderableOwner(state, false); | ||
state.trFontFace = undefined; | ||
} | ||
} | ||
/** | ||
* Invalidate the layout cache stored in the state. This will cause the text | ||
@@ -762,0 +790,0 @@ * to be re-layed out on the next update. |
@@ -69,2 +69,4 @@ /* | ||
isRenderable: boolean; | ||
debugData: { | ||
@@ -315,2 +317,3 @@ updateCount: number; | ||
zIndex: number; | ||
debug: Partial<TextRendererDebugProps>; | ||
@@ -456,2 +459,13 @@ } | ||
/** | ||
* Allows the CoreTextNode to communicate changes to the isRenderable state of | ||
* the itself. | ||
* | ||
* @param state | ||
* @param renderable | ||
*/ | ||
setIsRenderable(state: StateT, renderable: boolean) { | ||
state.isRenderable = renderable; | ||
} | ||
/** | ||
* Called by constructor to get a map of property setter functions for this renderer. | ||
@@ -491,2 +505,20 @@ */ | ||
/** | ||
* Destroy/Clean up the state object | ||
* | ||
* @remarks | ||
* Opposite of createState(). Frees any event listeners / resources held by | ||
* the state that may not reliably get garbage collected. | ||
* | ||
* @param state | ||
*/ | ||
destroyState(state: StateT) { | ||
const stateEvents = ['loading', 'loaded', 'failed']; | ||
// Remove the old event listeners from previous state obj there was one | ||
stateEvents.forEach((eventName) => { | ||
state.emitter.off(eventName); | ||
}); | ||
} | ||
/** | ||
* Schedule a state update via queueMicrotask | ||
@@ -493,0 +525,0 @@ * |
@@ -26,2 +26,7 @@ /* | ||
/** | ||
* Event handler for when a Texture is freed | ||
*/ | ||
export type TextureFreedEventHandler = (target: any) => void; | ||
/** | ||
* Event handler for when a Texture is loading | ||
@@ -98,5 +103,6 @@ */ | ||
export type TextureState = 'loading' | 'loaded' | 'failed'; | ||
export type TextureState = 'freed' | 'loading' | 'loaded' | 'failed'; | ||
export interface TextureStateEventMap { | ||
freed: TextureFreedEventHandler; | ||
loading: TextureLoadingEventHandler; | ||
@@ -140,4 +146,6 @@ loaded: TextureLoadedEventHandler; | ||
readonly state: TextureState = 'loading'; | ||
readonly state: TextureState = 'freed'; | ||
readonly renderableOwners = new Set<unknown>(); | ||
constructor(protected txManager: CoreTextureManager) { | ||
@@ -148,2 +156,31 @@ super(); | ||
/** | ||
* Add/remove an owner to/from the Texture based on its renderability. | ||
* | ||
* @remarks | ||
* Any object can own a texture, be it a CoreNode or even the state object | ||
* from a Text Renderer. | ||
* | ||
* When the reference to the texture that an owner object holds is replaced | ||
* or cleared it must call this with `renderable=false` to release the owner | ||
* association. | ||
* | ||
* @param owner | ||
* @param renderable | ||
*/ | ||
setRenderableOwner(owner: unknown, renderable: boolean): void { | ||
if (renderable) { | ||
this.renderableOwners.add(owner); | ||
} else { | ||
this.renderableOwners.delete(owner); | ||
} | ||
} | ||
/** | ||
* Returns true if the texture is assigned to any Nodes that are renderable. | ||
*/ | ||
get renderable(): boolean { | ||
return this.renderableOwners.size > 0; | ||
} | ||
/** | ||
* Set the state of the texture | ||
@@ -150,0 +187,0 @@ * |
@@ -66,2 +66,4 @@ /* | ||
onFrameTick(frameTickData: FrameTickPayload): void; | ||
onIdle?(): void; | ||
} |
@@ -12,2 +12,3 @@ import type { | ||
import type { IAnimationController } from '../common/IAnimationController.js'; | ||
import { isProductionEnvironment } from '../utils.js'; | ||
@@ -156,3 +157,3 @@ /** | ||
constructor(canvas: HTMLCanvasElement, settings: RendererMainSettings) { | ||
if (import.meta.env.PROD) return; | ||
if (isProductionEnvironment()) return; | ||
@@ -159,0 +160,0 @@ if (!settings) { |
@@ -43,2 +43,3 @@ /* | ||
import { santizeCustomDataMap } from '../render-drivers/utils.js'; | ||
import { isProductionEnvironment } from '../utils.js'; | ||
@@ -131,7 +132,18 @@ /** | ||
/** | ||
* Bounds margin to extend the boundary in which a CoreNode is added as Quad. | ||
* Texture Memory Byte Threshold | ||
* | ||
* @remarks | ||
* When the amount of GPU VRAM used by textures exceeds this threshold, | ||
* the Renderer will free up all the textures that are current not visible | ||
* within the configured `boundsMargin`. | ||
* | ||
* When set to `0`, the threshold-based texture memory manager is disabled. | ||
*/ | ||
txMemByteThreshold?: number; | ||
/** | ||
* Bounds margin to extend the boundary in which a CoreNode is added as Quad. | ||
*/ | ||
boundsMargin?: number | [number, number, number, number]; | ||
/** | ||
@@ -322,2 +334,3 @@ * Factor to convert app-authored logical coorindates to device logical coordinates | ||
appHeight: settings.appHeight || 1080, | ||
txMemByteThreshold: settings.txMemByteThreshold || 124e6, | ||
boundsMargin: settings.boundsMargin || 0, | ||
@@ -403,5 +416,9 @@ deviceLogicalPixelRatio: settings.deviceLogicalPixelRatio || 1, | ||
driver.onIdle = () => { | ||
this.emit('idle'); | ||
}; | ||
targetEl.appendChild(canvas); | ||
if (enableInspector && !import.meta.env.PROD) { | ||
if (enableInspector && !isProductionEnvironment()) { | ||
this.inspector = new Inspector(canvas, resolvedSettings); | ||
@@ -408,0 +425,0 @@ } |
@@ -58,2 +58,3 @@ /* | ||
appHeight: rendererSettings.appHeight, | ||
txMemByteThreshold: rendererSettings.txMemByteThreshold, | ||
boundsMargin: rendererSettings.boundsMargin, | ||
@@ -96,2 +97,6 @@ deviceLogicalPixelRatio: rendererSettings.deviceLogicalPixelRatio, | ||
}) satisfies StageFrameTickHandler); | ||
this.stage.on('idle', () => { | ||
this.onIdle(); | ||
}); | ||
} | ||
@@ -150,3 +155,7 @@ | ||
} | ||
onIdle() { | ||
throw new Error('Method not implemented.'); | ||
} | ||
//#endregion | ||
} |
@@ -111,2 +111,3 @@ /* | ||
this.coreNode.on('failed', this.onTextureFailed); | ||
this.coreNode.on('freed', this.onTextureFreed); | ||
@@ -424,2 +425,6 @@ this.coreNode.on('outOfBounds', this.onOutOfBounds); | ||
private onTextureFreed: NodeLoadedEventHandler = (target, payload) => { | ||
this.emit('freed', payload); | ||
}; | ||
private onOutOfBounds: NodeRenderStateEventHandler = (target, payload) => { | ||
@@ -466,9 +471,10 @@ this.emit('outOfBounds', payload); | ||
this.emit('beforeDestroy', {}); | ||
//use while loop since setting parent to null removes it from array | ||
let child = this.children[0]; | ||
while (child) { | ||
child.destroy(); | ||
child = this.children[0]; | ||
} | ||
this.coreNode.destroy(); | ||
// destroy children | ||
const length = this.children.length; | ||
for (let i = 0; i < length; i++) { | ||
this.children[i]?.destroy(); | ||
} | ||
this.children.length = 0; | ||
this.parent = null; | ||
@@ -475,0 +481,0 @@ this.texture = null; |
@@ -115,2 +115,3 @@ /* | ||
appHeight: rendererSettings.appHeight, | ||
txMemByteThreshold: rendererSettings.txMemByteThreshold, | ||
boundsMargin: rendererSettings.boundsMargin, | ||
@@ -117,0 +118,0 @@ deviceLogicalPixelRatio: rendererSettings.deviceLogicalPixelRatio, |
@@ -47,2 +47,3 @@ /* | ||
appHeight: number; | ||
txMemByteThreshold: number; | ||
boundsMargin: number | [number, number, number, number]; | ||
@@ -49,0 +50,0 @@ deviceLogicalPixelRatio: number; |
@@ -69,2 +69,3 @@ /* | ||
appHeight: message.appHeight, | ||
txMemByteThreshold: message.txMemByteThreshold, | ||
boundsMargin: message.boundsMargin, | ||
@@ -71,0 +72,0 @@ deviceLogicalPixelRatio: message.deviceLogicalPixelRatio, |
@@ -36,2 +36,3 @@ /* | ||
NodeFailedPayload, | ||
NodeTextureFreedPayload, | ||
} from '../../../common/CommonTypes.js'; | ||
@@ -147,2 +148,8 @@ | ||
); | ||
this.coreNode.on( | ||
'freed', | ||
(target: CoreNode, payload: NodeTextureFreedPayload) => { | ||
this.emit('freed', payload); | ||
}, | ||
); | ||
} | ||
@@ -149,0 +156,0 @@ |
@@ -82,3 +82,3 @@ /* | ||
): asserts condition { | ||
if (import.meta.env.PROD) return; | ||
if (isProductionEnvironment()) return; | ||
if (!condition) { | ||
@@ -209,1 +209,10 @@ throw new Error(message || 'Assertion failed'); | ||
} | ||
/** | ||
* Checks import.meta if env is production | ||
* | ||
* @returns | ||
*/ | ||
export function isProductionEnvironment(): boolean { | ||
return import.meta.env && import.meta.env.PROD; | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1825998
462
41003
234