@solidtv/renderer 1.5.3 → 1.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/core/CoreNode.d.ts +7 -0
- package/dist/src/core/CoreNode.js +13 -0
- package/dist/src/core/CoreNode.js.map +1 -1
- package/dist/src/core/CoreTextNode.js +3 -2
- package/dist/src/core/CoreTextNode.js.map +1 -1
- package/dist/src/core/CoreTextureManager.d.ts +39 -3
- package/dist/src/core/CoreTextureManager.js +72 -61
- package/dist/src/core/CoreTextureManager.js.map +1 -1
- package/dist/src/core/Stage.js +6 -4
- package/dist/src/core/Stage.js.map +1 -1
- package/dist/src/core/TextureMemoryManager.js +1 -1
- package/dist/src/core/TextureMemoryManager.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlCtxTexture.js +5 -3
- package/dist/src/core/renderers/webgl/WebGlCtxTexture.js.map +1 -1
- package/dist/src/core/textures/SubTexture.js +1 -1
- package/dist/src/core/textures/SubTexture.js.map +1 -1
- package/dist/src/core/textures/Texture.d.ts +1 -1
- package/dist/src/core/textures/Texture.js +10 -13
- package/dist/src/core/textures/Texture.js.map +1 -1
- package/dist/src/main-api/Inspector.js +1 -1
- package/dist/src/main-api/Inspector.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/core/CoreNode.test.ts +165 -0
- package/src/core/CoreNode.ts +14 -0
- package/src/core/CoreTextNode.ts +3 -2
- package/src/core/CoreTextureManager.ts +75 -70
- package/src/core/Stage.ts +9 -4
- package/src/core/TextureMemoryManager.test.ts +2 -2
- package/src/core/TextureMemoryManager.ts +1 -1
- package/src/core/renderers/webgl/WebGlCtxTexture.ts +5 -4
- package/src/core/textures/SubTexture.ts +1 -1
- package/src/core/textures/Texture.test.ts +124 -0
- package/src/core/textures/Texture.ts +10 -13
- package/src/main-api/Inspector.ts +1 -1
|
@@ -409,9 +409,9 @@ export class CoreTextureManager extends EventEmitter {
|
|
|
409
409
|
|
|
410
410
|
// Anything that arrived before initialization completed is now safe to
|
|
411
411
|
// process. Without this, queued textures would sit until the next frame
|
|
412
|
-
// tick happens to
|
|
412
|
+
// tick happens to drain them.
|
|
413
413
|
if (this.uploadTextureQueue.size > 0) {
|
|
414
|
-
this.
|
|
414
|
+
this.processUntil(Infinity).catch((err) => {
|
|
415
415
|
console.error('Failed to drain pre-init texture queue:', err);
|
|
416
416
|
});
|
|
417
417
|
}
|
|
@@ -587,11 +587,41 @@ export class CoreTextureManager extends EventEmitter {
|
|
|
587
587
|
}
|
|
588
588
|
|
|
589
589
|
/**
|
|
590
|
-
*
|
|
590
|
+
* Upload a single queued texture to the GPU.
|
|
591
591
|
*
|
|
592
|
-
* @
|
|
592
|
+
* @remarks
|
|
593
|
+
* Used while animations are running so uploads don't steal time from the
|
|
594
|
+
* animation. If the dequeued texture already died (failed/freed), nothing is
|
|
595
|
+
* uploaded this frame and the next call handles the following one.
|
|
596
|
+
*/
|
|
597
|
+
async processOne(): Promise<void> {
|
|
598
|
+
if (this.initialized === false) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const texture = this.uploadTextureQueue.shift();
|
|
603
|
+
if (texture === undefined) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
await this.uploadQueued(texture);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Upload queued textures to the GPU until the per-frame time budget runs out.
|
|
612
|
+
*
|
|
613
|
+
* @remarks
|
|
614
|
+
* Called once per frame when idle. Textures are uploaded one-by-one; after
|
|
615
|
+
* each, the elapsed time is rechecked and processing stops once it exceeds
|
|
616
|
+
* `maxProcessingTime`, leaving the rest queued for the next frame.
|
|
617
|
+
*
|
|
618
|
+
* In normal operation a queued texture's data is already decoded
|
|
619
|
+
* (`loadTexture` awaits `getTextureData` before enqueuing), so this budgets
|
|
620
|
+
* GPU upload time. Pass `Infinity` to drain the whole queue.
|
|
621
|
+
*
|
|
622
|
+
* @param maxProcessingTime - The time budget for this frame, in milliseconds
|
|
593
623
|
*/
|
|
594
|
-
async
|
|
624
|
+
async processUntil(maxProcessingTime: number): Promise<void> {
|
|
595
625
|
if (this.initialized === false) {
|
|
596
626
|
return;
|
|
597
627
|
}
|
|
@@ -599,79 +629,54 @@ export class CoreTextureManager extends EventEmitter {
|
|
|
599
629
|
const platform = this.platform;
|
|
600
630
|
const startTime = platform.getTimeStamp();
|
|
601
631
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const pending: Array<{ texture: Texture; data: Promise<unknown> }> = [];
|
|
608
|
-
|
|
609
|
-
// Helper avoids TS narrowing `texture.state` permanently after the first
|
|
610
|
-
// discriminated check — the property is mutable and can transition across
|
|
611
|
-
// awaits, so we need to re-read it freshly each time.
|
|
612
|
-
const isDead = (texture: Texture): boolean =>
|
|
613
|
-
texture.state === 'failed' || texture.state === 'freed';
|
|
614
|
-
|
|
615
|
-
const fillPrefetch = () => {
|
|
616
|
-
while (pending.length < prefetchLimit) {
|
|
617
|
-
const texture = this.uploadTextureQueue.shift();
|
|
618
|
-
if (texture === undefined) break;
|
|
619
|
-
|
|
620
|
-
if (isDead(texture)) {
|
|
621
|
-
continue;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Swallow the rejection here so an early failure doesn't surface as
|
|
625
|
-
// an unhandled promise rejection while it sits in the prefetch
|
|
626
|
-
// window; we re-check state after awaiting.
|
|
627
|
-
const data =
|
|
628
|
-
texture.textureData === null
|
|
629
|
-
? texture.getTextureData().catch((err) => {
|
|
630
|
-
console.error('Failed to fetch texture data:', err);
|
|
631
|
-
return null;
|
|
632
|
-
})
|
|
633
|
-
: Promise.resolve(texture.textureData);
|
|
634
|
-
|
|
635
|
-
pending.push({ texture, data });
|
|
632
|
+
while (platform.getTimeStamp() - startTime < maxProcessingTime) {
|
|
633
|
+
const texture = this.uploadTextureQueue.shift();
|
|
634
|
+
if (texture === undefined) {
|
|
635
|
+
// Queue drained.
|
|
636
|
+
break;
|
|
636
637
|
}
|
|
637
|
-
};
|
|
638
638
|
|
|
639
|
-
|
|
639
|
+
await this.uploadQueued(texture);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
640
642
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
643
|
+
/**
|
|
644
|
+
* Decode (if needed) and upload a single already-dequeued texture.
|
|
645
|
+
*
|
|
646
|
+
* @remarks
|
|
647
|
+
* Shared by {@link processOne} and {@link processUntil}. Dead (failed/freed)
|
|
648
|
+
* textures and upload failures are skipped without throwing.
|
|
649
|
+
*/
|
|
650
|
+
private async uploadQueued(texture: Texture): Promise<void> {
|
|
651
|
+
if (this.isTextureDead(texture)) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
650
654
|
|
|
651
|
-
|
|
652
|
-
|
|
655
|
+
try {
|
|
656
|
+
if (texture.textureData === null) {
|
|
657
|
+
await texture.getTextureData();
|
|
653
658
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
await next.data;
|
|
657
|
-
if (isDead(next.texture)) {
|
|
658
|
-
continue;
|
|
659
|
-
}
|
|
660
|
-
await this.uploadTexture(next.texture);
|
|
661
|
-
} catch (error) {
|
|
662
|
-
console.error('Failed to upload texture:', error);
|
|
663
|
-
// Continue with next texture instead of stopping entire queue
|
|
659
|
+
if (this.isTextureDead(texture)) {
|
|
660
|
+
return;
|
|
664
661
|
}
|
|
662
|
+
await this.uploadTexture(texture);
|
|
663
|
+
} catch (error) {
|
|
664
|
+
console.error('Failed to upload texture:', error);
|
|
665
|
+
// Skip this texture instead of stalling the queue.
|
|
665
666
|
}
|
|
667
|
+
}
|
|
666
668
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
669
|
+
/**
|
|
670
|
+
* A texture is "dead" once it has failed or been freed — both terminal for
|
|
671
|
+
* the upload pipeline.
|
|
672
|
+
*
|
|
673
|
+
* @remarks
|
|
674
|
+
* Kept as a method rather than an inline check so TypeScript doesn't
|
|
675
|
+
* permanently narrow `state` after the first comparison: the property is
|
|
676
|
+
* mutable and can transition across the awaits in {@link uploadQueued}.
|
|
677
|
+
*/
|
|
678
|
+
private isTextureDead(texture: Texture): boolean {
|
|
679
|
+
return texture.state === 'failed' || texture.state === 'freed';
|
|
675
680
|
}
|
|
676
681
|
|
|
677
682
|
public hasUpdates(): boolean {
|
package/src/core/Stage.ts
CHANGED
|
@@ -595,11 +595,16 @@ export class Stage {
|
|
|
595
595
|
// Process some textures asynchronously but don't block the frame
|
|
596
596
|
// Use a background task to prevent frame drops
|
|
597
597
|
if (this.txManager.hasUpdates() === true) {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
598
|
+
// While animating, upload at most one texture per frame so uploads don't
|
|
599
|
+
// steal time from the animation; otherwise fill the per-frame time budget.
|
|
600
|
+
const processing =
|
|
601
|
+
hasActiveAnimations === true
|
|
602
|
+
? this.txManager.processOne()
|
|
603
|
+
: this.txManager.processUntil(
|
|
604
|
+
this.options.textureProcessingTimeLimit,
|
|
605
|
+
);
|
|
601
606
|
|
|
602
|
-
|
|
607
|
+
processing.catch((err) => {
|
|
603
608
|
console.error('Error processing textures:', err);
|
|
604
609
|
});
|
|
605
610
|
}
|
|
@@ -175,7 +175,7 @@ function freedCachedTexture(cacheKey: string): Texture & {
|
|
|
175
175
|
state: 'freed',
|
|
176
176
|
type: TextureType.image,
|
|
177
177
|
preventCleanup: false,
|
|
178
|
-
renderableOwners:
|
|
178
|
+
renderableOwners: new Set<string | number>(),
|
|
179
179
|
cacheKey,
|
|
180
180
|
free: vi.fn(),
|
|
181
181
|
destroy: vi.fn(),
|
|
@@ -206,7 +206,7 @@ describe('TextureMemoryManager — orphaned freed texture eviction', () => {
|
|
|
206
206
|
it('keeps a freed texture that still has renderable owners', () => {
|
|
207
207
|
const { mgr, keyCache } = makeManager();
|
|
208
208
|
const texture = freedCachedTexture('img:poster.png');
|
|
209
|
-
|
|
209
|
+
texture.renderableOwners.add(1);
|
|
210
210
|
keyCache.set('img:poster.png', texture);
|
|
211
211
|
|
|
212
212
|
mgr.cleanup();
|
|
@@ -349,7 +349,7 @@ export class TextureMemoryManager {
|
|
|
349
349
|
if (
|
|
350
350
|
evictable === true &&
|
|
351
351
|
texture.preventCleanup === false &&
|
|
352
|
-
texture.renderableOwners.
|
|
352
|
+
texture.renderableOwners.size === 0 &&
|
|
353
353
|
texture.hasListeners() === false
|
|
354
354
|
) {
|
|
355
355
|
this.destroyTexture(texture);
|
|
@@ -161,10 +161,11 @@ export class WebGlCtxTexture extends CoreContextTexture {
|
|
|
161
161
|
);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
// No placeholder upload here: every branch below fully (re)uploads the
|
|
165
|
+
// texture before this method returns, and there is no `await` in between,
|
|
166
|
+
// so no frame can render an intermediate 1x1. The empty-texture case
|
|
167
|
+
// (`tdata === null`) does its own 1x1 upload. Uploading a throwaway 1x1
|
|
168
|
+
// first would be a wasted texImage2D + setTextureMemUse per texture.
|
|
168
169
|
let w = 0;
|
|
169
170
|
let h = 0;
|
|
170
171
|
|
|
@@ -79,7 +79,7 @@ export class SubTexture extends Texture {
|
|
|
79
79
|
// Resolve parent texture from cache or fallback to provided texture
|
|
80
80
|
this.parentTexture = txManager.resolveParentTexture(this.props.texture);
|
|
81
81
|
|
|
82
|
-
if (this.renderableOwners.
|
|
82
|
+
if (this.renderableOwners.size > 0) {
|
|
83
83
|
this.parentTexture.setRenderableOwner(this.subtextureId, true);
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { Texture, type TextureData } from './Texture.js';
|
|
3
|
+
import type { CoreTextureManager } from '../CoreTextureManager.js';
|
|
4
|
+
|
|
5
|
+
class TestTexture extends Texture {
|
|
6
|
+
override async getTextureSource(): Promise<TextureData> {
|
|
7
|
+
return { data: null };
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function makeTexture(): {
|
|
12
|
+
texture: TestTexture;
|
|
13
|
+
loadTexture: ReturnType<typeof vi.fn>;
|
|
14
|
+
onChangeIsRenderable: ReturnType<typeof vi.fn>;
|
|
15
|
+
} {
|
|
16
|
+
const loadTexture = vi.fn();
|
|
17
|
+
const txManager = {
|
|
18
|
+
maxRetryCount: 3,
|
|
19
|
+
loadTexture,
|
|
20
|
+
} as unknown as CoreTextureManager;
|
|
21
|
+
const texture = new TestTexture(txManager);
|
|
22
|
+
const onChangeIsRenderable = vi.fn();
|
|
23
|
+
texture.onChangeIsRenderable = onChangeIsRenderable;
|
|
24
|
+
return { texture, loadTexture, onChangeIsRenderable };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('Texture.setRenderableOwner', () => {
|
|
28
|
+
it('adding an owner fires the 0→1 transition exactly once and loads', () => {
|
|
29
|
+
const { texture, loadTexture, onChangeIsRenderable } = makeTexture();
|
|
30
|
+
|
|
31
|
+
texture.setRenderableOwner(1, true);
|
|
32
|
+
|
|
33
|
+
expect(texture.renderableOwners.size).toBe(1);
|
|
34
|
+
expect(texture.renderable).toBe(true);
|
|
35
|
+
expect(onChangeIsRenderable).toHaveBeenCalledTimes(1);
|
|
36
|
+
expect(onChangeIsRenderable).toHaveBeenCalledWith(true);
|
|
37
|
+
expect(loadTexture).toHaveBeenCalledTimes(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('double-add of the same owner is a no-op (size stays 1, one event)', () => {
|
|
41
|
+
const { texture, loadTexture, onChangeIsRenderable } = makeTexture();
|
|
42
|
+
|
|
43
|
+
texture.setRenderableOwner(1, true);
|
|
44
|
+
texture.setRenderableOwner(1, true);
|
|
45
|
+
|
|
46
|
+
expect(texture.renderableOwners.size).toBe(1);
|
|
47
|
+
expect(onChangeIsRenderable).toHaveBeenCalledTimes(1);
|
|
48
|
+
expect(loadTexture).toHaveBeenCalledTimes(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('a second distinct owner does not re-fire the transition', () => {
|
|
52
|
+
const { texture, loadTexture, onChangeIsRenderable } = makeTexture();
|
|
53
|
+
|
|
54
|
+
texture.setRenderableOwner(1, true);
|
|
55
|
+
texture.setRenderableOwner(2, true);
|
|
56
|
+
|
|
57
|
+
expect(texture.renderableOwners.size).toBe(2);
|
|
58
|
+
expect(onChangeIsRenderable).toHaveBeenCalledTimes(1);
|
|
59
|
+
expect(loadTexture).toHaveBeenCalledTimes(1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('removing a non-member owner does not fire a transition', () => {
|
|
63
|
+
const { texture, onChangeIsRenderable } = makeTexture();
|
|
64
|
+
|
|
65
|
+
texture.setRenderableOwner(1, false);
|
|
66
|
+
|
|
67
|
+
expect(texture.renderableOwners.size).toBe(0);
|
|
68
|
+
expect(texture.renderable).toBe(false);
|
|
69
|
+
expect(onChangeIsRenderable).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('1→0 fires onChangeIsRenderable(false) exactly once', () => {
|
|
73
|
+
const { texture, onChangeIsRenderable } = makeTexture();
|
|
74
|
+
|
|
75
|
+
texture.setRenderableOwner(1, true);
|
|
76
|
+
texture.setRenderableOwner(1, false);
|
|
77
|
+
texture.setRenderableOwner(1, false);
|
|
78
|
+
|
|
79
|
+
expect(texture.renderableOwners.size).toBe(0);
|
|
80
|
+
expect(texture.renderable).toBe(false);
|
|
81
|
+
expect(onChangeIsRenderable).toHaveBeenCalledTimes(2);
|
|
82
|
+
expect(onChangeIsRenderable).toHaveBeenLastCalledWith(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('re-adding an owner after 1→0 triggers load again (freed→reload cycle)', () => {
|
|
86
|
+
const { texture, loadTexture } = makeTexture();
|
|
87
|
+
|
|
88
|
+
texture.setRenderableOwner(1, true);
|
|
89
|
+
texture.setRenderableOwner(1, false);
|
|
90
|
+
texture.setRenderableOwner(1, true);
|
|
91
|
+
|
|
92
|
+
expect(texture.renderable).toBe(true);
|
|
93
|
+
expect(loadTexture).toHaveBeenCalledTimes(2);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('string and number owners coexist (node ids vs subtexture/font keys)', () => {
|
|
97
|
+
const { texture } = makeTexture();
|
|
98
|
+
|
|
99
|
+
texture.setRenderableOwner(7, true);
|
|
100
|
+
texture.setRenderableOwner('subtexture-7', true);
|
|
101
|
+
|
|
102
|
+
expect(texture.renderableOwners.size).toBe(2);
|
|
103
|
+
|
|
104
|
+
texture.setRenderableOwner(7, false);
|
|
105
|
+
expect(texture.renderableOwners.size).toBe(1);
|
|
106
|
+
expect(texture.renderable).toBe(true);
|
|
107
|
+
|
|
108
|
+
texture.setRenderableOwner('subtexture-7', false);
|
|
109
|
+
expect(texture.renderableOwners.size).toBe(0);
|
|
110
|
+
expect(texture.renderable).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('canBeCleanedUp respects remaining owners', () => {
|
|
114
|
+
const { texture } = makeTexture();
|
|
115
|
+
// Skip the startup grace period so owners are the deciding factor.
|
|
116
|
+
texture.isWithinStartupGracePeriod = () => false;
|
|
117
|
+
|
|
118
|
+
texture.setRenderableOwner(1, true);
|
|
119
|
+
expect(texture.canBeCleanedUp()).toBe(false);
|
|
120
|
+
|
|
121
|
+
texture.setRenderableOwner(1, false);
|
|
122
|
+
expect(texture.canBeCleanedUp()).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -141,7 +141,7 @@ export abstract class Texture extends EventEmitter {
|
|
|
141
141
|
// aggregate state
|
|
142
142
|
public state: TextureState = 'initial';
|
|
143
143
|
|
|
144
|
-
readonly renderableOwners:
|
|
144
|
+
readonly renderableOwners: Set<string | number> = new Set();
|
|
145
145
|
|
|
146
146
|
readonly renderable: boolean = false;
|
|
147
147
|
|
|
@@ -251,7 +251,7 @@ export abstract class Texture extends EventEmitter {
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
// Don't cleanup if there are still renderable owners
|
|
254
|
-
if (this.renderableOwners.
|
|
254
|
+
if (this.renderableOwners.size > 0) {
|
|
255
255
|
return false;
|
|
256
256
|
}
|
|
257
257
|
|
|
@@ -274,28 +274,25 @@ export abstract class Texture extends EventEmitter {
|
|
|
274
274
|
* @param renderable
|
|
275
275
|
*/
|
|
276
276
|
setRenderableOwner(owner: string | number, renderable: boolean): void {
|
|
277
|
-
const
|
|
278
|
-
const hasOwnerIndex = this.renderableOwners.indexOf(owner);
|
|
277
|
+
const owners = this.renderableOwners;
|
|
279
278
|
|
|
280
279
|
if (renderable === true) {
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
this.renderableOwners.push(owner);
|
|
280
|
+
if (owners.has(owner) === true) {
|
|
281
|
+
return;
|
|
284
282
|
}
|
|
285
283
|
|
|
286
|
-
|
|
287
|
-
if (
|
|
284
|
+
owners.add(owner);
|
|
285
|
+
if (owners.size === 1) {
|
|
288
286
|
(this.renderable as boolean) = true;
|
|
289
287
|
this.onChangeIsRenderable?.(true);
|
|
290
288
|
this.load();
|
|
291
289
|
}
|
|
292
290
|
} else {
|
|
293
|
-
if (
|
|
294
|
-
|
|
291
|
+
if (owners.delete(owner) === false) {
|
|
292
|
+
return;
|
|
295
293
|
}
|
|
296
294
|
|
|
297
|
-
|
|
298
|
-
if (oldSize !== newSize && newSize === 0) {
|
|
295
|
+
if (owners.size === 0) {
|
|
299
296
|
(this.renderable as boolean) = false;
|
|
300
297
|
this.onChangeIsRenderable?.(false);
|
|
301
298
|
|
|
@@ -1104,7 +1104,7 @@ export class Inspector {
|
|
|
1104
1104
|
// Update renderable owners count
|
|
1105
1105
|
div.setAttribute(
|
|
1106
1106
|
'data-texture-owners',
|
|
1107
|
-
String(texture.renderableOwners.
|
|
1107
|
+
String(texture.renderableOwners.size),
|
|
1108
1108
|
);
|
|
1109
1109
|
|
|
1110
1110
|
// Update retry count
|