@lightningjs/renderer
Version:
Lightning 3 Renderer
512 lines • 18.9 kB
JavaScript
/*
* 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 { EventEmitter } from '../common/EventEmitter.js';
import { assertTruthy, isProductionEnvironment } from '../utils.js';
import { Stage } from '../core/Stage.js';
import { CoreNode } from '../core/CoreNode.js';
import {} from '../core/CoreTextNode.js';
import { WebPlatform } from '../core/platforms/web/WebPlatform.js';
import { Platform } from '../core/platforms/Platform.js';
/**
* The Renderer Main API
*
* @remarks
* This is the primary class used to configure and operate the Renderer.
*
* It is used to create and destroy Nodes, as well as Texture and Shader
* references.
*
* Example:
* ```ts
* import { RendererMain, MainCoreDriver } from '@lightningjs/renderer';
*
* // Initialize the Renderer
* const renderer = new RendererMain(
* {
* appWidth: 1920,
* appHeight: 1080
* },
* 'app',
* new MainCoreDriver(),
* );
* ```
*
* ## Event Handling
*
* Listen to events using the standard EventEmitter API:
* ```typescript
* renderer.on('fpsUpdate', (data: RendererMainFpsUpdateEvent) => {
* console.log(`FPS: ${data.fps}`);
* });
*
* renderer.on('idle', (data: RendererMainIdleEvent) => {
* // Renderer is idle - no scene changes
* });
* ```
*
* @see {@link RendererMainFpsUpdateEvent}
* @see {@link RendererMainFrameTickEvent}
* @see {@link RendererMainQuadsUpdateEvent}
* @see {@link RendererMainIdleEvent}
* @see {@link RendererMainCriticalCleanupEvent}
* @see {@link RendererMainCriticalCleanupFailedEvent}
*
* @fires RendererMain#fpsUpdate
* @fires RendererMain#frameTick
* @fires RendererMain#quadsUpdate
* @fires RendererMain#idle
* @fires RendererMain#criticalCleanup
* @fires RendererMain#criticalCleanupFailed
*/
export class RendererMain extends EventEmitter {
root;
canvas;
stage;
inspector = null;
/**
* Constructs a new Renderer instance
*
* @param settings Renderer settings
* @param target Element ID or HTMLElement to insert the canvas into
* @param driver Core Driver to use
*/
constructor(settings, target) {
super();
const resolvedTxSettings = this.resolveTxSettings(settings.textureMemory || {});
settings = {
appWidth: settings.appWidth || 1920,
appHeight: settings.appHeight || 1080,
textureMemory: resolvedTxSettings,
boundsMargin: settings.boundsMargin || 0,
deviceLogicalPixelRatio: settings.deviceLogicalPixelRatio || 1,
devicePhysicalPixelRatio: settings.devicePhysicalPixelRatio || this.windowDevicePixelRatio() || 1,
clearColor: settings.clearColor ?? 0x00000000,
fpsUpdateInterval: settings.fpsUpdateInterval || 0,
enableClear: settings.enableClear ?? true,
targetFPS: settings.targetFPS || 0,
numImageWorkers: settings.numImageWorkers !== undefined ? settings.numImageWorkers : 2,
enableContextSpy: settings.enableContextSpy ?? false,
forceWebGL2: settings.forceWebGL2 ?? false,
inspector: settings.inspector ?? false,
inspectorOptions: settings.inspectorOptions ?? {},
renderEngine: settings.renderEngine,
quadBufferSize: settings.quadBufferSize ?? 4 * 1024 * 1024,
fontEngines: settings.fontEngines ?? [],
textureProcessingTimeLimit: settings.textureProcessingTimeLimit || 42,
canvas: settings.canvas,
createImageBitmapSupport: settings.createImageBitmapSupport || 'full',
platform: settings.platform || WebPlatform,
maxRetryCount: settings.maxRetryCount ?? 5,
};
const { appWidth, appHeight, deviceLogicalPixelRatio, devicePhysicalPixelRatio, inspector, } = settings;
assertTruthy(settings.platform, 'A platform implementation must be provided in settings.platform');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const platform = new settings.platform({
numImageWorkers: settings.numImageWorkers,
forceWebGL2: settings.forceWebGL2,
canvas: settings.canvas,
});
const deviceLogicalWidth = appWidth * deviceLogicalPixelRatio;
const deviceLogicalHeight = appHeight * deviceLogicalPixelRatio;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unsafe-member-access
this.canvas = platform.canvas;
this.canvas.width = deviceLogicalWidth * devicePhysicalPixelRatio;
this.canvas.height = deviceLogicalHeight * devicePhysicalPixelRatio;
if (this.canvas.style) {
this.canvas.style.width = `${deviceLogicalWidth}px`;
this.canvas.style.height = `${deviceLogicalHeight}px`;
}
// Initialize the stage
this.stage = new Stage({
appWidth,
appHeight,
boundsMargin: settings.boundsMargin,
clearColor: settings.clearColor,
canvas: this.canvas,
deviceLogicalPixelRatio,
devicePhysicalPixelRatio,
enableContextSpy: settings.enableContextSpy,
forceWebGL2: settings.forceWebGL2,
fpsUpdateInterval: settings.fpsUpdateInterval,
enableClear: settings.enableClear,
numImageWorkers: settings.numImageWorkers,
renderEngine: settings.renderEngine,
textureMemory: resolvedTxSettings,
eventBus: this,
quadBufferSize: settings.quadBufferSize,
fontEngines: settings.fontEngines,
inspector: settings.inspector !== null,
targetFPS: settings.targetFPS,
textureProcessingTimeLimit: settings.textureProcessingTimeLimit,
createImageBitmapSupport: settings.createImageBitmapSupport,
platform,
maxRetryCount: settings.maxRetryCount ?? 5,
});
// Extract the root node
this.root = this.stage.root;
// Get the target element and attach the canvas to it
if (target) {
let targetEl;
if (typeof target === 'string') {
targetEl = document.getElementById(target);
}
else {
targetEl = target;
}
if (!targetEl) {
throw new Error('Could not find target element');
}
targetEl.appendChild(this.canvas);
}
else if (settings.canvas !== this.canvas) {
throw new Error('New canvas element could not be appended to undefined target');
}
// Initialize inspector (if enabled)
if (inspector && isProductionEnvironment === false) {
this.inspector = new inspector(this.canvas, settings);
}
}
/**
* Resolves the Texture Memory Manager values
*
* @param props
* @returns
*/
resolveTxSettings(textureMemory) {
const currentTxSettings = (this.stage && this.stage.options.textureMemory) || {};
return {
criticalThreshold: textureMemory?.criticalThreshold ??
currentTxSettings?.criticalThreshold ??
124e6,
targetThresholdLevel: textureMemory?.targetThresholdLevel ??
currentTxSettings?.targetThresholdLevel ??
0.5,
cleanupInterval: textureMemory?.cleanupInterval ??
currentTxSettings?.cleanupInterval ??
5000,
debugLogging: textureMemory?.debugLogging ?? currentTxSettings?.debugLogging ?? false,
baselineMemoryAllocation: textureMemory?.baselineMemoryAllocation ??
currentTxSettings?.baselineMemoryAllocation ??
26e6,
doNotExceedCriticalThreshold: textureMemory?.doNotExceedCriticalThreshold ??
currentTxSettings?.doNotExceedCriticalThreshold ??
false,
};
}
/**
* Create a new scene graph node
*
* @remarks
* A node is the main graphical building block of the Renderer scene graph. It
* can be a container for other nodes, or it can be a leaf node that renders a
* solid color, gradient, image, or specific texture, using a specific shader.
*
* To create a text node, see {@link createTextNode}.
*
* See {@link CoreNode} for more details.
*
* @param props
* @returns
*/
createNode(props) {
const node = this.stage.createNode(props);
if (this.inspector) {
return this.inspector.createNode(node);
}
return node;
}
/**
* Create a new scene graph text node
*
* @remarks
* A text node is the second graphical building block of the Renderer scene
* graph. It renders text using a specific text renderer that is automatically
* chosen based on the font requested and what type of fonts are installed
* into an app.
*
* See {@link ITextNode} for more details.
*
* @param props
* @returns
*/
createTextNode(props) {
const textNode = this.stage.createTextNode(props);
if (this.inspector) {
return this.inspector.createTextNode(textNode);
}
return textNode;
}
/**
* Destroy a node
*
* @remarks
* This method destroys a node
*
* @param node
* @returns
*/
destroyNode(node) {
if (this.inspector) {
this.inspector.destroyNode(node.id);
}
return node.destroy();
}
/**
* Create a new texture reference
*
* @remarks
* This method creates a new reference to a texture. The texture is not
* loaded until it is used on a node.
*
* It can be assigned to a node's `texture` property, or it can be used
* when creating a SubTexture.
*
* @param textureType
* @param props
* @param options
* @returns
*/
createTexture(textureType, props) {
return this.stage.txManager.createTexture(textureType, props);
}
/**
* Create a new shader controller for a shader type
*
* @remarks
* This method creates a new Shader Controller for a specific shader type.
*
* If the shader has not been loaded yet, it will be loaded. Otherwise, the
* existing shader will be reused.
*
* It can be assigned to a Node's `shader` property.
*
* @param shaderType
* @param props
* @returns
*/
createShader(shType, props) {
return this.stage.shManager.createShader(shType, props);
}
/**
* Get a Node by its ID
*
* @param id
* @returns
*/
getNodeById(id) {
const root = this.stage?.root;
if (!root) {
return null;
}
const findNode = (node) => {
if (node.id === id) {
return node;
}
for (const child of node.children) {
const found = findNode(child);
if (found) {
return found;
}
}
return null;
};
return findNode(root);
}
toggleFreeze() {
throw new Error('Not implemented');
}
advanceFrame() {
throw new Error('Not implemented');
}
getBufferInfo() {
return this.stage.renderer.getBufferInfo();
}
/**
* Re-render the current frame without advancing any running animations.
*
* @remarks
* Any state changes will be reflected in the re-rendered frame. Useful for
* debugging.
*
* May not do anything if the render loop is running on a separate worker.
*/
rerender() {
this.stage.requestRender();
}
/**
* Cleanup textures that are not being used
*
* @param aggressive - If true, will cleanup all textures, regardless of render status
*
* @remarks
* This can be used to free up GFX memory used by textures that are no longer
* being displayed.
*
* This routine is also called automatically when the memory used by textures
* exceeds the critical threshold on frame generation **OR** when the renderer
* is idle and the memory used by textures exceeds the target threshold.
*
* **NOTE**: This is a heavy operation and should be used sparingly.
* **NOTE2**: This will not cleanup textures that are currently being displayed.
* **NOTE3**: This will not cleanup textures that are marked as `preventCleanup`.
* **NOTE4**: This has nothing to do with the garbage collection of JavaScript.
*/
cleanup() {
this.stage.cleanup();
}
/**
* Sets the clear color for the stage.
*
* @param color - The color to set as the clear color.
*/
setClearColor(color) {
this.stage.setClearColor(color);
}
/**
* Set options for the renderer
*
* @param options
*/
setOptions(options) {
const stage = this.stage;
if (options.textureMemory !== undefined) {
const textureMemory = (options.textureMemory = this.resolveTxSettings(options.textureMemory));
stage.txMemManager.updateSettings(textureMemory);
stage.txMemManager.cleanup();
}
if (options.boundsMargin !== undefined) {
let bm = options.boundsMargin;
options.boundsMargin = Array.isArray(bm) ? bm : [bm, bm, bm, bm];
}
const stageOptions = stage.options;
for (let key in options) {
stageOptions[key] = options[key];
}
if (options.inspector !== undefined && !isProductionEnvironment) {
if (options.inspector === false) {
this.inspector?.destroy();
this.inspector = null;
}
else if (this.inspector === null ||
this.inspector.constructor !== options.inspector) {
this.inspector = new options.inspector(this.canvas, stage.options);
this.inspector?.createNodes(this.root);
}
}
let needDimensionsUpdate = false;
if (options.deviceLogicalPixelRatio ||
options.devicePhysicalPixelRatio !== undefined) {
this.stage.pixelRatio =
stageOptions.devicePhysicalPixelRatio *
stageOptions.deviceLogicalPixelRatio;
this.inspector?.updateViewport(stageOptions.appWidth, stageOptions.appHeight, stageOptions.deviceLogicalPixelRatio);
needDimensionsUpdate = true;
}
if (options.appWidth !== undefined || options.appHeight !== undefined) {
this.inspector?.updateViewport(stageOptions.appWidth, stageOptions.appHeight, stageOptions.deviceLogicalPixelRatio);
needDimensionsUpdate = true;
}
if (options.boundsMargin !== undefined) {
this.stage.setBoundsMargin(options.boundsMargin);
}
if (options.clearColor !== undefined) {
this.stage.setClearColor(options.clearColor);
}
if (needDimensionsUpdate) {
this.updateAppDimensions();
}
}
updateAppDimensions() {
const { appWidth, appHeight, deviceLogicalPixelRatio, devicePhysicalPixelRatio, } = this.stage.options;
const deviceLogicalWidth = appWidth * deviceLogicalPixelRatio;
const deviceLogicalHeight = appHeight * deviceLogicalPixelRatio;
this.canvas.width = deviceLogicalWidth * devicePhysicalPixelRatio;
this.canvas.height = deviceLogicalHeight * devicePhysicalPixelRatio;
this.canvas.style.width = `${deviceLogicalWidth}px`;
this.canvas.style.height = `${deviceLogicalHeight}px`;
this.stage.renderer.updateViewport();
this.root.w = appWidth;
this.root.h = appHeight;
this.stage.updateViewportBounds();
}
get settings() {
return this.stage.options;
}
/**
* Gets the target FPS for the global render loop
*
* @returns The current target FPS (0 means no throttling)
*
* @remarks
* This controls the maximum frame rate of the entire rendering system.
* When 0, the system runs at display refresh rate.
*/
get targetFPS() {
return this.stage.options.targetFPS || 0;
}
/**
* Sets the target FPS for the global render loop
*
* @param fps - The target FPS to set for the global render loop.
* Set to 0 or a negative value to disable throttling.
*
* @remarks
* This setting affects the entire rendering system immediately.
* All animations, rendering, and frame updates will be throttled
* to this target FPS. Provides global performance control.
*
* @example
* ```typescript
* // Set global target to 30fps for better performance
* renderer.targetFPS = 30;
*
* // Disable global throttling (use display refresh rate)
* renderer.targetFPS = 0;
* ```
*/
set targetFPS(fps) {
this.stage.options.targetFPS = fps > 0 ? fps : 0;
this.stage.updateTargetFrameTime();
}
windowDevicePixelRatio() {
return typeof window !== 'undefined' ? window.devicePixelRatio : undefined;
}
/**
* Close and destroy the renderer, releasing all resources.
*
* @remarks
* This method performs a full teardown of the renderer:
* - Stops the platform render loop
* - Destroys all scene nodes (including text node font resources)
* - Releases all texture memory and GPU resources
* - Terminates image worker threads
* - Removes the canvas element from the target div
* - Destroys the inspector if active
*/
close() {
// Destroy the inspector first
this.inspector?.destroy();
this.inspector = null;
// Destroy the stage (stops loop, destroys nodes, releases textures/GPU)
this.stage.destroy();
// Remove the canvas from the DOM
this.canvas.remove();
}
}
//# sourceMappingURL=Renderer.js.map