Skip to content

Commit 0d5e4c4

Browse files
authored
fix: duplicate orphaned texture entries, check for renderable and timing (#512)
Regression introduced by #507 - Check if orphaned entry is already in the list, if so re-add it at the end (to avoid duplicate entries) - If a texture became renderable again during the cleanup routing, skip it - On non-critical cleanup spend max 10ms Fixes #511
2 parents 7b7d952 + 8068bfc commit 0d5e4c4

File tree

2 files changed

+204
-1
lines changed

2 files changed

+204
-1
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/*
2+
* If not stated otherwise in this file or this component's LICENSE file the
3+
* following copyright and licenses apply:
4+
*
5+
* Copyright 2023 Comcast Cable Communications Management, LLC.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the License);
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import type { INode, RendererMainSettings } from '@lightningjs/renderer';
21+
import type { ExampleSettings } from '../common/ExampleSettings.js';
22+
23+
export function customSettings(): Partial<RendererMainSettings> {
24+
return {
25+
textureMemory: {
26+
cleanupInterval: 5000,
27+
debugLogging: true,
28+
},
29+
};
30+
}
31+
32+
const COLORS = [
33+
0xff0000ff, // Red
34+
0x00ff00ff, // Green
35+
0x0000ffff, // Blue
36+
0xffff00ff, // Yellow
37+
0xff00ffff, // Magenta
38+
0x00ffffff, // Cyan
39+
0xffffffff, // White
40+
];
41+
42+
/**
43+
* Function that chooses a random color from the `COLORS` array
44+
*/
45+
function randomColor() {
46+
return COLORS[Math.floor(Math.random() * COLORS.length)];
47+
}
48+
49+
export default async function test({ renderer, testRoot }: ExampleSettings) {
50+
const nodeSize = 128; // Each node will be 128x128 pixels
51+
const memoryThreshold = 130 * 1024 * 1024; // 130 MB
52+
const textureSize = nodeSize * nodeSize * 4 * 1.1; // RGBA bytes per pixel
53+
const memoryBaseline = 25e6; // 25 MB
54+
const maxNodes = Math.ceil((memoryThreshold - memoryBaseline) / textureSize);
55+
const nodes: INode[] = [];
56+
57+
console.log(`Creating ${maxNodes} nodes...`);
58+
59+
let lastNoiseNodePosition = 0;
60+
const generateNoiseNodes = (count: number) => {
61+
// Create nodes with unique noise textures until the memory threshold is reached
62+
for (let i = 0; i < count; i++) {
63+
const x = (i % 27) * 10;
64+
const y = ~~(i / 27) * 10;
65+
66+
const node = renderer.createNode({
67+
x,
68+
y: lastNoiseNodePosition + y,
69+
width: nodeSize,
70+
height: nodeSize,
71+
parent: testRoot,
72+
color: randomColor(),
73+
texture: renderer.createTexture('NoiseTexture', {
74+
width: nodeSize,
75+
height: nodeSize,
76+
cacheId: i + Math.random(),
77+
}),
78+
});
79+
nodes.push(node);
80+
}
81+
82+
lastNoiseNodePosition = nodes[nodes.length - 1]!.y + 10;
83+
};
84+
85+
generateNoiseNodes(maxNodes);
86+
console.log(`Created ${nodes.length} nodes. Memory threshold reached.`);
87+
88+
const clippingNode = renderer.createNode({
89+
x: 600,
90+
y: 200,
91+
width: 1300,
92+
height: 800,
93+
parent: testRoot,
94+
color: 0xff0000ff,
95+
clipping: true,
96+
});
97+
98+
const containerNode = renderer.createNode({
99+
x: 0,
100+
y: 0,
101+
width: 1300,
102+
height: 800,
103+
parent: clippingNode,
104+
color: 0x000000ff,
105+
clipping: false,
106+
});
107+
108+
const nodeWidth = 200;
109+
const nodeHeight = 200;
110+
const gap = 10; // Define the gap between items
111+
112+
const spawnRow = function (rowIndex = 0, amount = 20) {
113+
const items = [];
114+
115+
let totalWidth = 0; // Track the total width used in the current row
116+
const y = rowIndex * (nodeHeight + gap);
117+
118+
const rowNode = renderer.createNode({
119+
x: 0,
120+
y: y,
121+
width: containerNode.width,
122+
height: nodeHeight,
123+
parent: containerNode,
124+
color: 0x000000ff,
125+
});
126+
127+
for (let i = 0; i < amount; i++) {
128+
totalWidth += nodeWidth + gap; // Include gap in total width calculation
129+
130+
const x = totalWidth - nodeWidth; // Adjust position by subtracting the node width
131+
const id = rowIndex * amount + i;
132+
133+
// Create the green node slightly smaller than the black rectangle
134+
const childNode = renderer.createNode({
135+
x: x, // Adjust position by subtracting the gap
136+
y: 0,
137+
width: nodeWidth, // Width of the green node
138+
height: nodeHeight, // Slightly smaller height
139+
parent: rowNode,
140+
src: `https://picsum.photos/id/${id}/${nodeWidth}/${nodeHeight}`, // Random images
141+
});
142+
143+
items.push(childNode);
144+
}
145+
146+
return items;
147+
};
148+
149+
// Generate up to 200 rows
150+
const amountOfRows = 20;
151+
for (let rowIndex = 0; rowIndex < amountOfRows; rowIndex++) {
152+
console.log(`Spawning row ${rowIndex + 1}`);
153+
spawnRow(rowIndex);
154+
}
155+
156+
// adjust container node size
157+
containerNode.height = amountOfRows * (nodeHeight + gap);
158+
159+
window.addEventListener('keydown', async (e) => {
160+
if (e.key === 'ArrowDown') {
161+
// move container down
162+
containerNode.y += 50;
163+
}
164+
165+
if (e.key === 'ArrowUp') {
166+
// move container up
167+
containerNode.y -= 50;
168+
}
169+
170+
if (e.key === 'ArrowLeft') {
171+
generateNoiseNodes(27);
172+
}
173+
174+
if (e.key === 'ArrowRight') {
175+
for (let i = 0; i < 27; i++) {
176+
const node = nodes.pop();
177+
if (node) {
178+
node.destroy();
179+
}
180+
}
181+
182+
lastNoiseNodePosition -= 10;
183+
}
184+
});
185+
}

src/core/TextureMemoryManager.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* limitations under the License.
1818
*/
1919
import { isProductionEnvironment } from '../utils.js';
20+
import { getTimeStamp } from './platform.js';
2021
import type { Stage } from './Stage.js';
2122
import type { Texture } from './textures/Texture.js';
2223
import { bytesToMb } from './utils.js';
@@ -168,6 +169,11 @@ export class TextureMemoryManager {
168169
* @param texture - The texture to add to the orphaned textures list
169170
*/
170171
addToOrphanedTextures(texture: Texture) {
172+
// if the texture is already in the orphaned textures list add it at the end
173+
if (this.orphanedTextures.includes(texture)) {
174+
this.removeFromOrphanedTextures(texture);
175+
}
176+
171177
// If the texture can be cleaned up, add it to the orphaned textures list
172178
if (texture.preventCleanup === false) {
173179
this.orphanedTextures.push(texture);
@@ -240,9 +246,21 @@ export class TextureMemoryManager {
240246
// Free non-renderable textures until we reach the target threshold
241247
const memTarget = this.targetThreshold;
242248
const txManager = this.stage.txManager;
249+
const timestamp = getTimeStamp();
243250

244-
while (this.memUsed >= memTarget && this.orphanedTextures.length > 0) {
251+
while (
252+
this.memUsed >= memTarget &&
253+
this.orphanedTextures.length > 0 &&
254+
// if it a non-critical cleanup, we will only cleanup for 10ms
255+
(critical || getTimeStamp() - timestamp < 10)
256+
) {
245257
const texture = this.orphanedTextures.shift()!;
258+
259+
if (texture.renderable === true) {
260+
// If the texture is renderable, we can't free it up
261+
continue;
262+
}
263+
246264
texture.free();
247265
txManager.removeTextureFromCache(texture);
248266
}

0 commit comments

Comments
 (0)