@solidtv/renderer 1.5.6 → 1.6.1

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.
Files changed (106) hide show
  1. package/dist/src/core/CoreNode.js +2 -2
  2. package/dist/src/core/CoreNode.js.map +1 -1
  3. package/dist/src/core/CoreTextureManager.d.ts +31 -0
  4. package/dist/src/core/CoreTextureManager.js +87 -6
  5. package/dist/src/core/CoreTextureManager.js.map +1 -1
  6. package/dist/src/core/Stage.js +11 -2
  7. package/dist/src/core/Stage.js.map +1 -1
  8. package/dist/src/core/lib/WebGlContextWrapper.d.ts +17 -1
  9. package/dist/src/core/lib/WebGlContextWrapper.js +21 -0
  10. package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
  11. package/dist/src/core/lib/colorParser.d.ts +4 -1
  12. package/dist/src/core/lib/colorParser.js +16 -6
  13. package/dist/src/core/lib/colorParser.js.map +1 -1
  14. package/dist/src/core/platforms/web/WebPlatform.js +60 -50
  15. package/dist/src/core/platforms/web/WebPlatform.js.map +1 -1
  16. package/dist/src/core/renderers/canvas/CanvasRenderer.d.ts +4 -0
  17. package/dist/src/core/renderers/canvas/CanvasRenderer.js +28 -13
  18. package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
  19. package/dist/src/core/renderers/canvas/CanvasShaderNode.js +1 -0
  20. package/dist/src/core/renderers/canvas/CanvasShaderNode.js.map +1 -1
  21. package/dist/src/core/renderers/webgl/SdfRenderOp.js +2 -2
  22. package/dist/src/core/renderers/webgl/SdfRenderOp.js.map +1 -1
  23. package/dist/src/core/renderers/webgl/WebGlRenderer.d.ts +43 -0
  24. package/dist/src/core/renderers/webgl/WebGlRenderer.js +109 -10
  25. package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
  26. package/dist/src/core/renderers/webgl/WebGlShaderProgram.js +21 -13
  27. package/dist/src/core/renderers/webgl/WebGlShaderProgram.js.map +1 -1
  28. package/dist/src/core/renderers/webgl/internal/ShaderUtils.d.ts +8 -3
  29. package/dist/src/core/renderers/webgl/internal/ShaderUtils.js +12 -11
  30. package/dist/src/core/renderers/webgl/internal/ShaderUtils.js.map +1 -1
  31. package/dist/src/core/shaders/canvas/Rounded.js +3 -3
  32. package/dist/src/core/shaders/canvas/Rounded.js.map +1 -1
  33. package/dist/src/core/shaders/canvas/RoundedWithShadow.js +3 -3
  34. package/dist/src/core/shaders/canvas/RoundedWithShadow.js.map +1 -1
  35. package/dist/src/core/shaders/canvas/utils/render.js +3 -3
  36. package/dist/src/core/shaders/canvas/utils/render.js.map +1 -1
  37. package/dist/src/core/shaders/webgl/Border.js +12 -11
  38. package/dist/src/core/shaders/webgl/Border.js.map +1 -1
  39. package/dist/src/core/shaders/webgl/HolePunch.js +12 -5
  40. package/dist/src/core/shaders/webgl/HolePunch.js.map +1 -1
  41. package/dist/src/core/shaders/webgl/LinearGradient.js +3 -19
  42. package/dist/src/core/shaders/webgl/LinearGradient.js.map +1 -1
  43. package/dist/src/core/shaders/webgl/RadialGradient.js +3 -20
  44. package/dist/src/core/shaders/webgl/RadialGradient.js.map +1 -1
  45. package/dist/src/core/shaders/webgl/RadialProgress.js +15 -23
  46. package/dist/src/core/shaders/webgl/RadialProgress.js.map +1 -1
  47. package/dist/src/core/shaders/webgl/RoundedWithBorder.js +35 -32
  48. package/dist/src/core/shaders/webgl/RoundedWithBorder.js.map +1 -1
  49. package/dist/src/core/shaders/webgl/RoundedWithBorderAndShadow.js +58 -45
  50. package/dist/src/core/shaders/webgl/RoundedWithBorderAndShadow.js.map +1 -1
  51. package/dist/src/core/shaders/webgl/RoundedWithShadow.js +15 -13
  52. package/dist/src/core/shaders/webgl/RoundedWithShadow.js.map +1 -1
  53. package/dist/src/core/shaders/webgl/Shadow.js +4 -5
  54. package/dist/src/core/shaders/webgl/Shadow.js.map +1 -1
  55. package/dist/src/core/text-rendering/SdfTextRenderer.js +23 -10
  56. package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
  57. package/dist/src/core/text-rendering/TextLayoutEngine.js +14 -2
  58. package/dist/src/core/text-rendering/TextLayoutEngine.js.map +1 -1
  59. package/dist/src/main-api/Renderer.d.ts +28 -5
  60. package/dist/src/main-api/Renderer.js +5 -1
  61. package/dist/src/main-api/Renderer.js.map +1 -1
  62. package/dist/src/utils.js +1 -1
  63. package/dist/src/utils.js.map +1 -1
  64. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  65. package/package.json +1 -1
  66. package/src/core/CoreNode.ts +2 -2
  67. package/src/core/CoreTextureManager.test.ts +147 -0
  68. package/src/core/CoreTextureManager.ts +97 -8
  69. package/src/core/Stage.renderListUpdate.test.ts +73 -0
  70. package/src/core/Stage.ts +13 -1
  71. package/src/core/lib/WebGlContextWrapper.test.ts +23 -1
  72. package/src/core/lib/WebGlContextWrapper.ts +23 -1
  73. package/src/core/lib/colorParser.test.ts +46 -0
  74. package/src/core/lib/colorParser.ts +17 -6
  75. package/src/core/platforms/web/WebPlatform.ts +63 -52
  76. package/src/core/renderers/canvas/CanvasRenderer.test.ts +230 -0
  77. package/src/core/renderers/canvas/CanvasRenderer.ts +29 -15
  78. package/src/core/renderers/canvas/CanvasShaderNode.test.ts +73 -0
  79. package/src/core/renderers/canvas/CanvasShaderNode.ts +1 -0
  80. package/src/core/renderers/webgl/SdfRenderOp.ts +2 -2
  81. package/src/core/renderers/webgl/WebGlRenderer.reuseRenderOp.test.ts +122 -0
  82. package/src/core/renderers/webgl/WebGlRenderer.sdfBuffer.test.ts +302 -0
  83. package/src/core/renderers/webgl/WebGlRenderer.ts +151 -11
  84. package/src/core/renderers/webgl/WebGlShaderProgram.reuseRenderOp.test.ts +150 -0
  85. package/src/core/renderers/webgl/WebGlShaderProgram.ts +27 -15
  86. package/src/core/renderers/webgl/WebGlShaderProgram.uniformDedup.test.ts +4 -2
  87. package/src/core/renderers/webgl/internal/ShaderUtils.test.ts +124 -0
  88. package/src/core/renderers/webgl/internal/ShaderUtils.ts +14 -13
  89. package/src/core/shaders/canvas/Rounded.ts +3 -3
  90. package/src/core/shaders/canvas/RoundedWithShadow.ts +3 -3
  91. package/src/core/shaders/canvas/utils/render.ts +3 -3
  92. package/src/core/shaders/webgl/Border.ts +12 -11
  93. package/src/core/shaders/webgl/HolePunch.ts +12 -5
  94. package/src/core/shaders/webgl/LinearGradient.ts +3 -19
  95. package/src/core/shaders/webgl/RadialGradient.ts +3 -20
  96. package/src/core/shaders/webgl/RadialProgress.ts +15 -23
  97. package/src/core/shaders/webgl/RoundedWithBorder.ts +35 -32
  98. package/src/core/shaders/webgl/RoundedWithBorderAndShadow.ts +58 -45
  99. package/src/core/shaders/webgl/RoundedWithShadow.ts +15 -13
  100. package/src/core/shaders/webgl/Shadow.ts +4 -5
  101. package/src/core/text-rendering/SdfTextRenderer.test.ts +177 -0
  102. package/src/core/text-rendering/SdfTextRenderer.ts +38 -10
  103. package/src/core/text-rendering/TextLayoutEngine.ts +15 -2
  104. package/src/core/text-rendering/tests/TextLayoutEngine.test.ts +99 -0
  105. package/src/main-api/Renderer.ts +35 -6
  106. package/src/utils.ts +1 -1
@@ -133,6 +133,183 @@ describe('SdfTextRenderer layout cache', () => {
133
133
  });
134
134
  });
135
135
 
136
+ describe('SdfTextRenderer renderQuads cache dispatch', () => {
137
+ type FakeRenderer = {
138
+ stage: unknown;
139
+ sdfBufferIdx: number;
140
+ fSdfBuffer: Float32Array;
141
+ addSdfQuads: ReturnType<typeof vi.fn>;
142
+ addSdfCachedQuads: ReturnType<typeof vi.fn>;
143
+ addSdfTranslatedQuads: ReturnType<typeof vi.fn>;
144
+ };
145
+
146
+ const GLYPHS = 2;
147
+
148
+ const makeRenderer = (): FakeRenderer => {
149
+ const renderer: FakeRenderer = {
150
+ stage: {
151
+ options: { textLayoutCacheSize: 10 },
152
+ shManager: {
153
+ registerShaderType: vi.fn(),
154
+ createShader: vi.fn(() => ({})),
155
+ },
156
+ },
157
+ sdfBufferIdx: 0,
158
+ fSdfBuffer: new Float32Array(1024),
159
+ // The miss path snapshots [startIdx, endIdx) of fSdfBuffer, so the
160
+ // fake must advance sdfBufferIdx like the real writer does.
161
+ addSdfQuads: vi.fn(() => {
162
+ renderer.sdfBufferIdx += GLYPHS * 24;
163
+ }),
164
+ addSdfCachedQuads: vi.fn(),
165
+ addSdfTranslatedQuads: vi.fn(),
166
+ };
167
+ return renderer;
168
+ };
169
+
170
+ const makeLayout = () => ({
171
+ glyphs: new Float32Array(GLYPHS * 8),
172
+ glyphCount: GLYPHS,
173
+ fontScale: 1,
174
+ distanceRange: 4,
175
+ width: 100,
176
+ height: 50,
177
+ });
178
+
179
+ const makeCache = () => ({
180
+ vertices: null as Float32Array | null,
181
+ glyphCount: 0,
182
+ color: 0,
183
+ alpha: -1,
184
+ transform: new Float32Array(6),
185
+ layoutRef: null as unknown,
186
+ });
187
+
188
+ const makeRenderProps = (
189
+ cache: ReturnType<typeof makeCache>,
190
+ transform: number[],
191
+ ) => ({
192
+ fontFamily: 'Test',
193
+ fontSize: 42,
194
+ color: 0xffffffff,
195
+ offsetY: 0,
196
+ worldAlpha: 1,
197
+ globalTransform: new Float32Array(transform),
198
+ clippingRect: { x: 0, y: 0, w: 0, h: 0, valid: false },
199
+ width: 100,
200
+ height: 50,
201
+ parentHasRenderTexture: false,
202
+ framebufferDimensions: null,
203
+ sdfCache: cache,
204
+ });
205
+
206
+ const IDENTITY = [1, 0, 0, 0, 1, 0, 0, 0, 1];
207
+ const TRANSLATED = [1, 0, 0, 0, 1, 0, 120, -40, 1];
208
+ const SCALED = [2, 0, 0, 0, 2, 0, 0, 0, 1];
209
+
210
+ const renderQuads = (
211
+ renderer: FakeRenderer,
212
+ layout: ReturnType<typeof makeLayout>,
213
+ renderProps: ReturnType<typeof makeRenderProps>,
214
+ ) =>
215
+ SdfTextRenderer.renderQuads(
216
+ renderer as never,
217
+ layout as never,
218
+ null as never,
219
+ renderProps as never,
220
+ );
221
+
222
+ const setupWithAtlas = () => {
223
+ initRenderer(10);
224
+ vi.mocked(SdfFontHandler.getAtlas).mockReturnValue({
225
+ ctxTexture: {},
226
+ } as never);
227
+ };
228
+
229
+ it('takes the miss path first and snapshots the cache', () => {
230
+ setupWithAtlas();
231
+ const renderer = makeRenderer();
232
+ const layout = makeLayout();
233
+ const cache = makeCache();
234
+
235
+ renderQuads(renderer, layout, makeRenderProps(cache, IDENTITY));
236
+
237
+ expect(renderer.addSdfQuads.mock.calls.length).toBe(1);
238
+ expect(cache.vertices).not.toBeNull();
239
+ expect(cache.layoutRef).toBe(layout);
240
+ expect(cache.glyphCount).toBe(GLYPHS);
241
+ });
242
+
243
+ it('takes the mem-copy path when nothing changed', () => {
244
+ setupWithAtlas();
245
+ const renderer = makeRenderer();
246
+ const layout = makeLayout();
247
+ const cache = makeCache();
248
+
249
+ renderQuads(renderer, layout, makeRenderProps(cache, IDENTITY));
250
+ renderQuads(renderer, layout, makeRenderProps(cache, IDENTITY));
251
+
252
+ expect(renderer.addSdfQuads.mock.calls.length).toBe(1);
253
+ expect(renderer.addSdfCachedQuads.mock.calls.length).toBe(1);
254
+ expect(renderer.addSdfTranslatedQuads.mock.calls.length).toBe(0);
255
+ });
256
+
257
+ it('takes the translation path when only tx/ty changed, keeping the cache base', () => {
258
+ setupWithAtlas();
259
+ const renderer = makeRenderer();
260
+ const layout = makeLayout();
261
+ const cache = makeCache();
262
+
263
+ renderQuads(renderer, layout, makeRenderProps(cache, IDENTITY));
264
+ const baseVertices = cache.vertices;
265
+
266
+ renderQuads(renderer, layout, makeRenderProps(cache, TRANSLATED));
267
+
268
+ expect(renderer.addSdfQuads.mock.calls.length).toBe(1);
269
+ expect(renderer.addSdfCachedQuads.mock.calls.length).toBe(0);
270
+ const calls = renderer.addSdfTranslatedQuads.mock.calls;
271
+ expect(calls.length).toBe(1);
272
+ // (cachedVertices, glyphCount, dx, dy, ...)
273
+ expect(calls[0]![0]).toBe(baseVertices);
274
+ expect(calls[0]![1]).toBe(GLYPHS);
275
+ expect(calls[0]![2]).toBe(120);
276
+ expect(calls[0]![3]).toBe(-40);
277
+ // The scroll path must not re-snapshot: base transform and vertices stay
278
+ expect(cache.vertices).toBe(baseVertices);
279
+ expect(cache.transform[4]).toBe(0);
280
+ expect(cache.transform[5]).toBe(0);
281
+ });
282
+
283
+ it('falls back to the miss path when scale or rotation changed', () => {
284
+ setupWithAtlas();
285
+ const renderer = makeRenderer();
286
+ const layout = makeLayout();
287
+ const cache = makeCache();
288
+
289
+ renderQuads(renderer, layout, makeRenderProps(cache, IDENTITY));
290
+ renderQuads(renderer, layout, makeRenderProps(cache, SCALED));
291
+
292
+ expect(renderer.addSdfQuads.mock.calls.length).toBe(2);
293
+ expect(renderer.addSdfTranslatedQuads.mock.calls.length).toBe(0);
294
+ });
295
+
296
+ it('falls back to the miss path when color or alpha changed', () => {
297
+ setupWithAtlas();
298
+ const renderer = makeRenderer();
299
+ const layout = makeLayout();
300
+ const cache = makeCache();
301
+
302
+ renderQuads(renderer, layout, makeRenderProps(cache, IDENTITY));
303
+
304
+ const faded = makeRenderProps(cache, TRANSLATED);
305
+ faded.worldAlpha = 0.5;
306
+ renderQuads(renderer, layout, faded);
307
+
308
+ expect(renderer.addSdfQuads.mock.calls.length).toBe(2);
309
+ expect(renderer.addSdfTranslatedQuads.mock.calls.length).toBe(0);
310
+ });
311
+ });
312
+
136
313
  describe('SdfTextRenderer lazy shader compile', () => {
137
314
  it('registers the SDF shader at init but defers compilation', () => {
138
315
  const shManager = {
@@ -150,11 +150,14 @@ const addQuads = (_layout?: TextLayout): Float32Array | null => {
150
150
  /**
151
151
  * Submit SDF glyphs to the renderer's shared batched buffer.
152
152
  *
153
- * Two paths:
154
- * 1. **Cache hit** — layout, transform, color, and alpha haven't changed.
155
- * The cached pre-transformed Float32Array is mem-copied directly into the
156
- * shared SDF buffer (no per-glyph matrix math).
157
- * 2. **Cache miss** — re-computes per-glyph world-space vertices via
153
+ * Three paths:
154
+ * 1. **Exact cache hit** — layout, transform, color, and alpha haven't
155
+ * changed. The cached pre-transformed Float32Array is mem-copied directly
156
+ * into the shared SDF buffer (no per-glyph matrix math).
157
+ * 2. **Translation hit** — only tx/ty changed (the scroll path). The cached
158
+ * vertices are copied with the position delta applied; the cache keeps its
159
+ * original base so nothing is re-snapshotted.
160
+ * 3. **Cache miss** — re-computes per-glyph world-space vertices via
158
161
  * `addSdfQuads`, then snapshots the result into the cache.
159
162
  */
160
163
  const renderQuads = (
@@ -176,7 +179,7 @@ const renderQuads = (
176
179
  // Compiles on the first real SDF draw; cheap memoized lookup thereafter.
177
180
  const shader = getSdfShader(webGlRenderer.stage);
178
181
 
179
- // --- Cache-hit fast path ------------------------------------------------
182
+ // --- Cache-hit fast paths -----------------------------------------------
180
183
  if (cache !== undefined && cache.vertices !== null) {
181
184
  const ct = cache.transform;
182
185
  const t = renderProps.globalTransform;
@@ -187,13 +190,38 @@ const renderQuads = (
187
190
  ct[0] === t[0] &&
188
191
  ct[1] === t[1] &&
189
192
  ct[2] === t[3] &&
190
- ct[3] === t[4] &&
191
- ct[4] === t[6] &&
192
- ct[5] === t[7]
193
+ ct[3] === t[4]
193
194
  ) {
194
- webGlRenderer.addSdfCachedQuads(
195
+ const dx = t[6]! - ct[4]!;
196
+ const dy = t[7]! - ct[5]!;
197
+
198
+ if (dx === 0 && dy === 0) {
199
+ // Fully static: mem-copy the cached vertices as-is.
200
+ webGlRenderer.addSdfCachedQuads(
201
+ cache.vertices,
202
+ cache.glyphCount,
203
+ ctxTexture,
204
+ renderProps.clippingRect,
205
+ renderProps.worldAlpha,
206
+ layout.width,
207
+ layout.height,
208
+ renderProps.parentHasRenderTexture,
209
+ renderProps.framebufferDimensions,
210
+ shader,
211
+ );
212
+ return null;
213
+ }
214
+
215
+ // Pure translation (the scroll path): same glyphs, same scale/rotation,
216
+ // only tx/ty moved. Copy the cached vertices shifted by the delta from
217
+ // the cached base transform. The cache keeps its original base, so
218
+ // every frame recomputes from the same reference — no drift and no
219
+ // per-frame re-snapshot.
220
+ webGlRenderer.addSdfTranslatedQuads(
195
221
  cache.vertices,
196
222
  cache.glyphCount,
223
+ dx,
224
+ dy,
197
225
  ctxTexture,
198
226
  renderProps.clippingRect,
199
227
  renderProps.worldAlpha,
@@ -368,6 +368,13 @@ export const wrapLine = (
368
368
  ): WrappedLinesStruct => {
369
369
  const words = line.split(spaceRegex);
370
370
  const spaces = line.match(spaceRegex) || [];
371
+ // Leading whitespace splits into an empty token; fold it into the next
372
+ // word so it isn't dropped and re-applied after the first word instead.
373
+ if (words[0] === '' && words.length > 1) {
374
+ const leadingSpace = spaces.shift() ?? '';
375
+ words.shift();
376
+ words[0] = leadingSpace + (words[0] ?? '');
377
+ }
371
378
  const wrappedLines: TextLineStruct[] = [];
372
379
  let currentLine = '';
373
380
  let currentLineWidth = 0;
@@ -454,8 +461,14 @@ export const wrapLine = (
454
461
  continue;
455
462
  }
456
463
  const space = spaces[spaceIdx++] || '';
457
- // For width calculation, treat ZWSP as having 0 width but regular space functionality
458
- const effectiveSpaceWidth = space === '\u200B' ? 0 : spaceWidth;
464
+ // Single ' ' is the dominant separator \u2014 reuse the precomputed width.
465
+ // Multi-space/ZWSP runs collapse into one match; measure those for real.
466
+ const effectiveSpaceWidth =
467
+ space === ' '
468
+ ? spaceWidth
469
+ : space.length > 0
470
+ ? measureText(space, fontFamily, letterSpacing)
471
+ : 0;
459
472
  const totalWidth = currentLineWidth + effectiveSpaceWidth + wordWidth;
460
473
 
461
474
  if (totalWidth < maxWidth) {
@@ -248,6 +248,105 @@ describe('SDF Text Utils', () => {
248
248
  expect(result[0][0]?.[2]).toBe(true);
249
249
  expect(result[1]).toBe(0);
250
250
  });
251
+
252
+ it('should keep leading whitespace attached to the first word, not the word after it', () => {
253
+ // Regression test: a line starting with padding spaces (used e.g. to
254
+ // reserve room for an inline badge before the first word) used to have
255
+ // those spaces silently reassigned as the gap AFTER the first word.
256
+ const result = wrapLine(
257
+ testMeasureText,
258
+ ' hello world',
259
+ 'Arial',
260
+ 1000,
261
+ 0,
262
+ 10, // spaceWidth
263
+ '',
264
+ 0,
265
+ 'break-word',
266
+ 10,
267
+ );
268
+
269
+ const [lines] = result;
270
+ expect(lines).toHaveLength(1);
271
+ expect(lines[0]?.[0]).toBe(' hello world');
272
+ });
273
+
274
+ it('should count the full width of a multi-space separator when wrapping', () => {
275
+ // 'aa bb' really measures 60 (6 chars * 10). Counting the separator
276
+ // as a single spaceWidth (50) used to keep it on one line past maxWidth.
277
+ const result = wrapLine(
278
+ testMeasureText,
279
+ 'aa bb',
280
+ 'Arial',
281
+ 55, // maxWidth — below the real 60, above the buggy 50
282
+ 0,
283
+ 10, // spaceWidth
284
+ '',
285
+ 0,
286
+ 'break-word',
287
+ 10,
288
+ );
289
+
290
+ const [lines] = result;
291
+ expect(lines).toHaveLength(2);
292
+ expect(lines[0]).toEqual(['aa', 20, false, 0, 0]);
293
+ expect(lines[1]).toEqual(['bb', 20, false, 0, 0]);
294
+ });
295
+
296
+ it('should preserve a multi-space separator and its width when the line fits', () => {
297
+ const result = wrapLine(
298
+ testMeasureText,
299
+ 'aa bb',
300
+ 'Arial',
301
+ 1000,
302
+ 0,
303
+ 10, // spaceWidth
304
+ '',
305
+ 0,
306
+ 'break-word',
307
+ 10,
308
+ );
309
+
310
+ expect(result[0][0]).toEqual(['aa bb', 60, false, 0, 0]);
311
+ });
312
+
313
+ it('should treat a run of multiple ZWSPs as a zero-width separator', () => {
314
+ const result = wrapLine(
315
+ testMeasureText,
316
+ 'aa\u200B\u200Bbb',
317
+ 'Arial',
318
+ 1000,
319
+ 0,
320
+ 10, // spaceWidth
321
+ '',
322
+ 0,
323
+ 'break-word',
324
+ 10,
325
+ );
326
+
327
+ // Zero-width separator: no visible gap added, width is just the glyphs
328
+ expect(result[0][0]).toEqual(['aabb', 40, false, 0, 0]);
329
+ });
330
+
331
+ it('should use the precomputed spaceWidth for a single-space separator', () => {
332
+ // spaceWidth (4) deliberately differs from testMeasureText(' ') (10):
333
+ // the single-space fast path must reuse the precomputed value instead
334
+ // of re-measuring the separator.
335
+ const result = wrapLine(
336
+ testMeasureText,
337
+ 'aa bb',
338
+ 'Arial',
339
+ 1000,
340
+ 0,
341
+ 4, // spaceWidth
342
+ '',
343
+ 0,
344
+ 'break-word',
345
+ 10,
346
+ );
347
+
348
+ expect(result[0][0]).toEqual(['aa bb', 44, false, 0, 0]);
349
+ });
251
350
  });
252
351
 
253
352
  describe('wrapText', () => {
@@ -481,6 +481,26 @@ export type RendererMainSettings = RendererRuntimeSettings & {
481
481
  */
482
482
  numImageWorkers: number;
483
483
 
484
+ /**
485
+ * Maximum number of image fetch+decode operations allowed to run at once
486
+ * when image workers are unavailable
487
+ *
488
+ * @remarks
489
+ * Only applies when there is no image worker manager (i.e.
490
+ * `numImageWorkers === 0`, or workers/`createImageBitmap` are unsupported).
491
+ * In that mode every image decode runs on the main thread, so a burst — such
492
+ * as a scroll that makes many image nodes renderable in a single tick — can
493
+ * fire dozens of decodes back-to-back and starve the render loop. This caps
494
+ * how many run concurrently; on-screen (priority) textures bypass the cap so
495
+ * they never wait behind off-screen prefetch.
496
+ *
497
+ * Has no effect when image workers are active — the worker pool already
498
+ * bounds concurrency by its size. Set to `0` to disable the cap (unbounded).
499
+ *
500
+ * @defaultValue `4`
501
+ */
502
+ imageDecodeConcurrency: number;
503
+
484
504
  /**
485
505
  * Renderer Engine
486
506
  *
@@ -610,13 +630,17 @@ export type RendererMainSettings = RendererRuntimeSettings & {
610
630
  * - Full - Supports createImageBitmap(image, sx, sy, sw, sh, options)
611
631
  *
612
632
  * Note with auto detection, the renderer will attempt to use the most advanced
613
- * version of the API available. If the API is not available, the renderer will
614
- * fall back to the next available version.
633
+ * version of the API available. If the API is not available (e.g. Chrome < 50,
634
+ * including the Chrome 38 support floor), the renderer falls back to loading
635
+ * images via `new Image()`.
615
636
  *
616
- * This will affect startup performance as the renderer will need to determine
617
- * the supported version of the API.
637
+ * `'auto'` runs a small startup probe (a 1x1 PNG) to determine support; this
638
+ * adds a negligible one-time startup cost. Set an explicit value (`'full'`,
639
+ * `'options'`, `'basic'`) to skip the probe ONLY when the target runtime is
640
+ * known to support that level — forcing a level the runtime lacks will fail
641
+ * to load images. When in doubt, use `'auto'`.
618
642
  *
619
- * @defaultValue `full`
643
+ * @defaultValue `auto`
620
644
  */
621
645
  createImageBitmapSupport: 'auto' | 'basic' | 'options' | 'full';
622
646
 
@@ -781,6 +805,10 @@ export class RendererMain extends EventEmitter {
781
805
  textLayoutCacheSize: settings.textLayoutCacheSize ?? 250,
782
806
  numImageWorkers:
783
807
  settings.numImageWorkers !== undefined ? settings.numImageWorkers : 2,
808
+ imageDecodeConcurrency:
809
+ settings.imageDecodeConcurrency !== undefined
810
+ ? settings.imageDecodeConcurrency
811
+ : 4,
784
812
  enableContextSpy: settings.enableContextSpy ?? false,
785
813
  forceWebGL2: settings.forceWebGL2 ?? false,
786
814
  disableVertexArrayObject: settings.disableVertexArrayObject ?? false,
@@ -792,7 +820,7 @@ export class RendererMain extends EventEmitter {
792
820
  textBaselineMode: settings.textBaselineMode ?? 'optical',
793
821
  textureProcessingTimeLimit: settings.textureProcessingTimeLimit || 10,
794
822
  canvas: settings.canvas,
795
- createImageBitmapSupport: settings.createImageBitmapSupport || 'full',
823
+ createImageBitmapSupport: settings.createImageBitmapSupport || 'auto',
796
824
  // undefined -> true (assume honored, no probe); 'auto' -> probe;
797
825
  // explicit boolean -> force the value.
798
826
  premultiplyAlphaHonored:
@@ -854,6 +882,7 @@ export class RendererMain extends EventEmitter {
854
882
  fpsUpdateInterval: settings.fpsUpdateInterval!,
855
883
  enableClear: settings.enableClear!,
856
884
  numImageWorkers: settings.numImageWorkers!,
885
+ imageDecodeConcurrency: settings.imageDecodeConcurrency!,
857
886
  renderEngine: settings.renderEngine!,
858
887
  textureMemory: resolvedTxSettings,
859
888
  eventBus: this,
package/src/utils.ts CHANGED
@@ -9,7 +9,7 @@ export function createWebGLContext(
9
9
  alpha: true,
10
10
  antialias: false,
11
11
  depth: false,
12
- stencil: true,
12
+ stencil: false,
13
13
  desynchronized: false,
14
14
  // Disabled because it prevents Visual Regression Tests from working
15
15
  // failIfMajorPerformanceCaveat: true,