@solidtv/renderer 1.5.3 → 1.5.5
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/exports/canvas-shaders.d.ts +1 -0
- package/dist/exports/canvas-shaders.js +1 -0
- package/dist/exports/canvas-shaders.js.map +1 -1
- package/dist/exports/index.d.ts +1 -0
- package/dist/exports/index.js +1 -0
- package/dist/exports/index.js.map +1 -1
- package/dist/exports/webgl-shaders.d.ts +1 -0
- package/dist/exports/webgl-shaders.js +1 -0
- package/dist/exports/webgl-shaders.js.map +1 -1
- 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/CoreShaderManager.js +16 -2
- package/dist/src/core/CoreShaderManager.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/lib/WebGlContextWrapper.d.ts +13 -0
- package/dist/src/core/lib/WebGlContextWrapper.js +15 -0
- package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
- package/dist/src/core/renderers/canvas/CanvasRenderer.d.ts +10 -0
- package/dist/src/core/renderers/canvas/CanvasRenderer.js +19 -0
- package/dist/src/core/renderers/canvas/CanvasRenderer.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/renderers/webgl/WebGlRenderer.js +14 -1
- package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
- package/dist/src/core/shaders/canvas/EdgeFade.d.ts +10 -0
- package/dist/src/core/shaders/canvas/EdgeFade.js +92 -0
- package/dist/src/core/shaders/canvas/EdgeFade.js.map +1 -0
- package/dist/src/core/shaders/canvas/LinearGradient.js +1 -18
- package/dist/src/core/shaders/canvas/LinearGradient.js.map +1 -1
- package/dist/src/core/shaders/templates/EdgeFadeTemplate.d.ts +32 -0
- package/dist/src/core/shaders/templates/EdgeFadeTemplate.js +9 -0
- package/dist/src/core/shaders/templates/EdgeFadeTemplate.js.map +1 -0
- package/dist/src/core/shaders/webgl/EdgeFade.d.ts +9 -0
- package/dist/src/core/shaders/webgl/EdgeFade.js +74 -0
- package/dist/src/core/shaders/webgl/EdgeFade.js.map +1 -0
- package/dist/src/core/shaders/webgl/LinearGradient.js +31 -51
- package/dist/src/core/shaders/webgl/LinearGradient.js.map +1 -1
- package/dist/src/core/shaders/webgl/RadialGradient.js +2 -2
- package/dist/src/core/shaders/webgl/RadialProgress.js +7 -0
- package/dist/src/core/shaders/webgl/RadialProgress.js.map +1 -1
- package/dist/src/core/text-rendering/SdfFontHandler.d.ts +1 -0
- package/dist/src/core/text-rendering/SdfFontHandler.js +88 -51
- package/dist/src/core/text-rendering/SdfFontHandler.js.map +1 -1
- package/dist/src/core/textures/ImageTexture.js +2 -1
- package/dist/src/core/textures/ImageTexture.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/exports/canvas-shaders.ts +1 -0
- package/exports/index.ts +1 -0
- package/exports/webgl-shaders.ts +1 -0
- package/package.json +1 -1
- package/src/core/CoreNode.test.ts +165 -0
- package/src/core/CoreNode.ts +14 -0
- package/src/core/CoreShaderManager.contextLoss.test.ts +72 -0
- package/src/core/CoreShaderManager.ts +15 -2
- 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/lib/WebGlContextWrapper.ts +16 -0
- package/src/core/renderers/canvas/CanvasRenderer.ts +20 -0
- package/src/core/renderers/webgl/WebGlCtxTexture.ts +5 -4
- package/src/core/renderers/webgl/WebGlRenderer.contextLoss.test.ts +49 -0
- package/src/core/renderers/webgl/WebGlRenderer.ts +13 -1
- package/src/core/shaders/canvas/EdgeFade.ts +105 -0
- package/src/core/shaders/canvas/LinearGradient.test.ts +45 -0
- package/src/core/shaders/canvas/LinearGradient.ts +1 -18
- package/src/core/shaders/templates/EdgeFadeTemplate.test.ts +35 -0
- package/src/core/shaders/templates/EdgeFadeTemplate.ts +41 -0
- package/src/core/shaders/webgl/EdgeFade.ts +84 -0
- package/src/core/shaders/webgl/LinearGradient.test.ts +70 -0
- package/src/core/shaders/webgl/LinearGradient.ts +36 -51
- package/src/core/shaders/webgl/RadialGradient.ts +2 -2
- package/src/core/shaders/webgl/RadialProgress.ts +7 -0
- package/src/core/text-rendering/SdfFontHandler.ts +77 -30
- package/src/core/text-rendering/tests/SdfFontHandler.test.ts +181 -35
- package/src/core/textures/ImageTexture.test.ts +91 -0
- package/src/core/textures/ImageTexture.ts +4 -1
- 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
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
loadFont,
|
|
4
|
+
waitingForFont,
|
|
5
|
+
isFontLoaded,
|
|
6
|
+
MAX_FONT_LOAD_RETRIES,
|
|
7
|
+
} from '../SdfFontHandler.js';
|
|
3
8
|
import { EventEmitter } from '../../../common/EventEmitter.js';
|
|
4
9
|
import { TextureError, TextureErrorCode } from '../../TextureError.js';
|
|
5
10
|
import type { Stage } from '../../Stage.js';
|
|
@@ -14,7 +19,19 @@ class FakeXHR {
|
|
|
14
19
|
onerror: (() => void) | null = null;
|
|
15
20
|
open(): void {}
|
|
16
21
|
send(): void {
|
|
17
|
-
|
|
22
|
+
// Enough shape for processFontData to run on the success path without
|
|
23
|
+
// throwing: a chars array, an (empty) kernings array, and metrics so it
|
|
24
|
+
// skips the atlas-derived cap/x-height branches.
|
|
25
|
+
this.response = {
|
|
26
|
+
chars: [{}],
|
|
27
|
+
kernings: [],
|
|
28
|
+
lightningMetrics: {
|
|
29
|
+
ascender: 800,
|
|
30
|
+
descender: -200,
|
|
31
|
+
lineGap: 200,
|
|
32
|
+
unitsPerEm: 1000,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
18
35
|
if (this.onload !== null) {
|
|
19
36
|
this.onload();
|
|
20
37
|
}
|
|
@@ -34,15 +51,59 @@ function makeFakeTexture() {
|
|
|
34
51
|
return tex;
|
|
35
52
|
}
|
|
36
53
|
|
|
54
|
+
type FakeTexture = ReturnType<typeof makeFakeTexture>;
|
|
55
|
+
|
|
56
|
+
// A stage whose txManager hands out a fresh fake texture per loadFont attempt
|
|
57
|
+
// (so each retry has its own texture to fail/succeed) and records them.
|
|
58
|
+
function makeStage(): { stage: Stage; textures: FakeTexture[] } {
|
|
59
|
+
const textures: FakeTexture[] = [];
|
|
60
|
+
const stage = {
|
|
61
|
+
txManager: {
|
|
62
|
+
createTexture: () => {
|
|
63
|
+
const t = makeFakeTexture();
|
|
64
|
+
textures.push(t);
|
|
65
|
+
return t;
|
|
66
|
+
},
|
|
67
|
+
// loadFont evicts a failed atlas before retrying; a no-op is enough here.
|
|
68
|
+
removeTextureFromCache: () => {},
|
|
69
|
+
},
|
|
70
|
+
} as unknown as Stage;
|
|
71
|
+
return { stage, textures };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const opts = (fontFamily: string) =>
|
|
75
|
+
({
|
|
76
|
+
fontFamily,
|
|
77
|
+
atlasUrl: 'atlas.png',
|
|
78
|
+
atlasDataUrl: 'atlas.json',
|
|
79
|
+
} as Parameters<typeof loadFont>[1]);
|
|
80
|
+
|
|
37
81
|
// Drain microtasks + one macrotask so the async loader reaches listener setup.
|
|
38
82
|
const flush = (): Promise<void> => new Promise((r) => setTimeout(r, 0));
|
|
39
83
|
|
|
40
|
-
|
|
84
|
+
// Fail every attempt: each iteration waits for the next attempt to wire up its
|
|
85
|
+
// atlas listener, then fires 'failed' on that attempt's texture.
|
|
86
|
+
async function failAllAttempts(
|
|
87
|
+
textures: FakeTexture[],
|
|
88
|
+
error: TextureError,
|
|
89
|
+
attempts: number,
|
|
90
|
+
): Promise<void> {
|
|
91
|
+
for (let i = 0; i < attempts; i++) {
|
|
92
|
+
await flush();
|
|
93
|
+
textures[i]!.emit('failed', error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const TOTAL_ATTEMPTS = MAX_FONT_LOAD_RETRIES + 1; // initial + retries
|
|
98
|
+
|
|
99
|
+
describe('SdfFontHandler loadFont — failure after exhausting retries', () => {
|
|
41
100
|
let errSpy: ReturnType<typeof vi.spyOn>;
|
|
101
|
+
let warnSpy: ReturnType<typeof vi.spyOn>;
|
|
42
102
|
let originalXHR: unknown;
|
|
43
103
|
|
|
44
104
|
beforeEach(() => {
|
|
45
105
|
errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
106
|
+
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
46
107
|
originalXHR = (globalThis as unknown as { XMLHttpRequest: unknown })
|
|
47
108
|
.XMLHttpRequest;
|
|
48
109
|
(globalThis as unknown as { XMLHttpRequest: unknown }).XMLHttpRequest =
|
|
@@ -51,62 +112,147 @@ describe('SdfFontHandler loadFont — failed event argument', () => {
|
|
|
51
112
|
|
|
52
113
|
afterEach(() => {
|
|
53
114
|
errSpy.mockRestore();
|
|
115
|
+
warnSpy.mockRestore();
|
|
54
116
|
(globalThis as unknown as { XMLHttpRequest: unknown }).XMLHttpRequest =
|
|
55
117
|
originalXHR;
|
|
56
118
|
});
|
|
57
119
|
|
|
58
|
-
it('rejects with the TextureError (second emit arg), not the emitting texture', async () => {
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
txManager: { createTexture: () => tex },
|
|
62
|
-
} as unknown as Stage;
|
|
120
|
+
it('rejects with the last TextureError (second emit arg), not the emitting texture', async () => {
|
|
121
|
+
const { stage, textures } = makeStage();
|
|
122
|
+
const promise = loadFont(stage, opts('TestSdfFailFont'));
|
|
63
123
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
} as Parameters<typeof loadFont>[1]);
|
|
124
|
+
const error = new TextureError(
|
|
125
|
+
TextureErrorCode.TEXTURE_UPLOAD_FAILED,
|
|
126
|
+
'boom',
|
|
127
|
+
);
|
|
69
128
|
|
|
70
|
-
|
|
129
|
+
// Attach the rejection assertion before driving failures.
|
|
130
|
+
const assertion = expect(promise).rejects.toBe(error);
|
|
131
|
+
await failAllAttempts(textures, error, TOTAL_ATTEMPTS);
|
|
132
|
+
await assertion;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('logs the error, not the texture, after the final attempt', async () => {
|
|
136
|
+
const { stage, textures } = makeStage();
|
|
137
|
+
const promise = loadFont(stage, opts('TestSdfFailFontLog'));
|
|
71
138
|
|
|
72
139
|
const error = new TextureError(
|
|
73
140
|
TextureErrorCode.TEXTURE_UPLOAD_FAILED,
|
|
74
141
|
'boom',
|
|
75
142
|
);
|
|
143
|
+
const rejected = promise.catch(() => {});
|
|
144
|
+
await failAllAttempts(textures, error, TOTAL_ATTEMPTS);
|
|
145
|
+
await rejected;
|
|
76
146
|
|
|
77
|
-
|
|
78
|
-
|
|
147
|
+
const lastCall = errSpy.mock.calls[errSpy.mock.calls.length - 1]!;
|
|
148
|
+
expect(lastCall[1]).toBe(error);
|
|
149
|
+
expect(lastCall[1]).not.toBe(textures[textures.length - 1]);
|
|
150
|
+
});
|
|
79
151
|
|
|
80
|
-
|
|
81
|
-
|
|
152
|
+
it('attempts the load exactly initial + MAX_FONT_LOAD_RETRIES times', async () => {
|
|
153
|
+
const { stage, textures } = makeStage();
|
|
154
|
+
const promise = loadFont(stage, opts('TestSdfAttemptCount'));
|
|
82
155
|
|
|
83
|
-
|
|
156
|
+
const error = new TextureError(
|
|
157
|
+
TextureErrorCode.TEXTURE_UPLOAD_FAILED,
|
|
158
|
+
'boom',
|
|
159
|
+
);
|
|
160
|
+
const rejected = promise.catch(() => {});
|
|
161
|
+
await failAllAttempts(textures, error, TOTAL_ATTEMPTS);
|
|
162
|
+
await rejected;
|
|
163
|
+
|
|
164
|
+
// One texture is created per attempt; no further attempts after exhaustion.
|
|
165
|
+
expect(textures.length).toBe(TOTAL_ATTEMPTS);
|
|
166
|
+
// A warning per retried attempt, error only on the final failure.
|
|
167
|
+
expect(warnSpy).toHaveBeenCalledTimes(MAX_FONT_LOAD_RETRIES);
|
|
168
|
+
expect(errSpy).toHaveBeenCalledTimes(1);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('SdfFontHandler loadFont — automatic retry', () => {
|
|
173
|
+
let errSpy: ReturnType<typeof vi.spyOn>;
|
|
174
|
+
let warnSpy: ReturnType<typeof vi.spyOn>;
|
|
175
|
+
let originalXHR: unknown;
|
|
176
|
+
|
|
177
|
+
beforeEach(() => {
|
|
178
|
+
errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
179
|
+
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
180
|
+
originalXHR = (globalThis as unknown as { XMLHttpRequest: unknown })
|
|
181
|
+
.XMLHttpRequest;
|
|
182
|
+
(globalThis as unknown as { XMLHttpRequest: unknown }).XMLHttpRequest =
|
|
183
|
+
FakeXHR;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
afterEach(() => {
|
|
187
|
+
errSpy.mockRestore();
|
|
188
|
+
warnSpy.mockRestore();
|
|
189
|
+
(globalThis as unknown as { XMLHttpRequest: unknown }).XMLHttpRequest =
|
|
190
|
+
originalXHR;
|
|
84
191
|
});
|
|
85
192
|
|
|
86
|
-
it('
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
193
|
+
it('recovers when a retry succeeds and wakes nodes parked before the failure', async () => {
|
|
194
|
+
const { stage, textures } = makeStage();
|
|
195
|
+
const fontFamily = 'RetryRecoverFont';
|
|
196
|
+
|
|
197
|
+
const promise = loadFont(stage, opts(fontFamily));
|
|
91
198
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
199
|
+
// Park a node while the font is loading (waiter list exists synchronously).
|
|
200
|
+
const node = { id: 1, setUpdateType: vi.fn() };
|
|
201
|
+
waitingForFont(
|
|
202
|
+
fontFamily,
|
|
203
|
+
node as unknown as Parameters<typeof waitingForFont>[1],
|
|
204
|
+
);
|
|
97
205
|
|
|
206
|
+
// First attempt fails...
|
|
98
207
|
await flush();
|
|
208
|
+
textures[0]!.emit(
|
|
209
|
+
'failed',
|
|
210
|
+
new TextureError(TextureErrorCode.TEXTURE_UPLOAD_FAILED, 'boom'),
|
|
211
|
+
);
|
|
99
212
|
|
|
213
|
+
// ...the automatic retry succeeds.
|
|
214
|
+
await flush();
|
|
215
|
+
textures[1]!.emit('loaded');
|
|
216
|
+
await promise;
|
|
217
|
+
|
|
218
|
+
expect(isFontLoaded(fontFamily)).toBe(true);
|
|
219
|
+
expect(node.setUpdateType).toHaveBeenCalledTimes(1);
|
|
220
|
+
// Only two attempts were needed.
|
|
221
|
+
expect(textures.length).toBe(2);
|
|
222
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
223
|
+
expect(errSpy).not.toHaveBeenCalled();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('keeps parked nodes after every retry fails so a later loadFont still wakes them', async () => {
|
|
227
|
+
const { stage, textures } = makeStage();
|
|
228
|
+
const fontFamily = 'RetryExhaustReuseFont';
|
|
100
229
|
const error = new TextureError(
|
|
101
230
|
TextureErrorCode.TEXTURE_UPLOAD_FAILED,
|
|
102
231
|
'boom',
|
|
103
232
|
);
|
|
104
|
-
const rejected = promise.catch(() => {});
|
|
105
|
-
tex.emit('failed', error);
|
|
106
|
-
await rejected;
|
|
107
233
|
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
234
|
+
const first = loadFont(stage, opts(fontFamily));
|
|
235
|
+
const firstRejected = first.catch(() => {});
|
|
236
|
+
|
|
237
|
+
const node = { id: 1, setUpdateType: vi.fn() };
|
|
238
|
+
waitingForFont(
|
|
239
|
+
fontFamily,
|
|
240
|
+
node as unknown as Parameters<typeof waitingForFont>[1],
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
await failAllAttempts(textures, error, TOTAL_ATTEMPTS);
|
|
244
|
+
await firstRejected;
|
|
245
|
+
|
|
246
|
+
expect(isFontLoaded(fontFamily)).toBe(false);
|
|
247
|
+
expect(node.setUpdateType).not.toHaveBeenCalled();
|
|
248
|
+
|
|
249
|
+
// A fresh load reuses the still-parked node and wakes it on success.
|
|
250
|
+
const second = loadFont(stage, opts(fontFamily));
|
|
251
|
+
await flush();
|
|
252
|
+
textures[TOTAL_ATTEMPTS]!.emit('loaded');
|
|
253
|
+
await second;
|
|
254
|
+
|
|
255
|
+
expect(isFontLoaded(fontFamily)).toBe(true);
|
|
256
|
+
expect(node.setUpdateType).toHaveBeenCalledTimes(1);
|
|
111
257
|
});
|
|
112
258
|
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { ImageTexture } from './ImageTexture.js';
|
|
3
|
+
import type { CoreTextureManager } from '../CoreTextureManager.js';
|
|
4
|
+
|
|
5
|
+
describe('ImageTexture.createImageBitmap', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.stubGlobal('ImageBitmap', class {});
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('passes options to createImageBitmap when no crop is requested and options/full are supported', async () => {
|
|
11
|
+
const createImageBitmapMock = vi.fn(() =>
|
|
12
|
+
Promise.resolve({ close: () => {} }),
|
|
13
|
+
);
|
|
14
|
+
const txManager = {
|
|
15
|
+
imageBitmapSupported: {
|
|
16
|
+
basic: true,
|
|
17
|
+
options: true,
|
|
18
|
+
full: true,
|
|
19
|
+
premultiplyHonored: false,
|
|
20
|
+
},
|
|
21
|
+
platform: {
|
|
22
|
+
createImageBitmap: createImageBitmapMock,
|
|
23
|
+
},
|
|
24
|
+
} as unknown as CoreTextureManager;
|
|
25
|
+
|
|
26
|
+
const props = ImageTexture.resolveDefaults({
|
|
27
|
+
src: 'test.png',
|
|
28
|
+
premultiplyAlpha: true,
|
|
29
|
+
});
|
|
30
|
+
const texture = new ImageTexture(txManager, props);
|
|
31
|
+
|
|
32
|
+
const blob = new Blob([], { type: 'image/png' });
|
|
33
|
+
const result = await texture.createImageBitmap(
|
|
34
|
+
blob,
|
|
35
|
+
true,
|
|
36
|
+
null,
|
|
37
|
+
null,
|
|
38
|
+
null,
|
|
39
|
+
null,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// It should have called createImageBitmap with the options object
|
|
43
|
+
expect(createImageBitmapMock).toHaveBeenCalledWith(blob, {
|
|
44
|
+
premultiplyAlpha: 'none',
|
|
45
|
+
colorSpaceConversion: 'none',
|
|
46
|
+
imageOrientation: 'none',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Since premultiplyHonored is false, useGlPremultiply should be true
|
|
50
|
+
expect(result.premultiplyAlpha).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('falls back to basic createImageBitmap when options are not supported', async () => {
|
|
54
|
+
const createImageBitmapMock = vi.fn(() =>
|
|
55
|
+
Promise.resolve({ close: () => {} }),
|
|
56
|
+
);
|
|
57
|
+
const txManager = {
|
|
58
|
+
imageBitmapSupported: {
|
|
59
|
+
basic: true,
|
|
60
|
+
options: false,
|
|
61
|
+
full: false,
|
|
62
|
+
premultiplyHonored: null,
|
|
63
|
+
},
|
|
64
|
+
platform: {
|
|
65
|
+
createImageBitmap: createImageBitmapMock,
|
|
66
|
+
},
|
|
67
|
+
} as unknown as CoreTextureManager;
|
|
68
|
+
|
|
69
|
+
const props = ImageTexture.resolveDefaults({
|
|
70
|
+
src: 'test.png',
|
|
71
|
+
premultiplyAlpha: true,
|
|
72
|
+
});
|
|
73
|
+
const texture = new ImageTexture(txManager, props);
|
|
74
|
+
|
|
75
|
+
const blob = new Blob([], { type: 'image/png' });
|
|
76
|
+
const result = await texture.createImageBitmap(
|
|
77
|
+
blob,
|
|
78
|
+
true,
|
|
79
|
+
null,
|
|
80
|
+
null,
|
|
81
|
+
null,
|
|
82
|
+
null,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// It should have called basic createImageBitmap without options
|
|
86
|
+
expect(createImageBitmapMock).toHaveBeenCalledWith(blob);
|
|
87
|
+
|
|
88
|
+
// And premultiplyAlpha should be false (browser default is assumed to premultiply)
|
|
89
|
+
expect(result.premultiplyAlpha).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -219,7 +219,10 @@ export class ImageTexture extends Texture {
|
|
|
219
219
|
},
|
|
220
220
|
);
|
|
221
221
|
return { data: bitmap, premultiplyAlpha: useGlPremultiply };
|
|
222
|
-
} else if (
|
|
222
|
+
} else if (
|
|
223
|
+
imageBitmapSupported.options === false &&
|
|
224
|
+
imageBitmapSupported.full === false
|
|
225
|
+
) {
|
|
223
226
|
// basic createImageBitmap without options or crop
|
|
224
227
|
// this is supported for Chrome v50 to v52/54 that doesn't support options.
|
|
225
228
|
// The browser default premultiplies, so WebGL must not premultiply again.
|
|
@@ -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
|