@lightningjs/renderer 3.0.2 → 3.0.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.
Files changed (119) hide show
  1. package/README.md +56 -196
  2. package/dist/src/core/CoreNode.d.ts +2 -1
  3. package/dist/src/core/CoreNode.js +31 -7
  4. package/dist/src/core/CoreNode.js.map +1 -1
  5. package/dist/src/core/CoreTextNode.d.ts +26 -6
  6. package/dist/src/core/CoreTextNode.js +163 -60
  7. package/dist/src/core/CoreTextNode.js.map +1 -1
  8. package/dist/src/core/CoreTextureManager.d.ts +8 -0
  9. package/dist/src/core/CoreTextureManager.js +13 -1
  10. package/dist/src/core/CoreTextureManager.js.map +1 -1
  11. package/dist/src/core/Stage.d.ts +8 -0
  12. package/dist/src/core/Stage.js +23 -0
  13. package/dist/src/core/Stage.js.map +1 -1
  14. package/dist/src/core/TextureMemoryManager.d.ts +8 -13
  15. package/dist/src/core/TextureMemoryManager.js +22 -27
  16. package/dist/src/core/TextureMemoryManager.js.map +1 -1
  17. package/dist/src/core/lib/ImageWorker.d.ts +2 -2
  18. package/dist/src/core/lib/ImageWorker.js +31 -12
  19. package/dist/src/core/lib/ImageWorker.js.map +1 -1
  20. package/dist/src/core/lib/WebGlContextWrapper.d.ts +105 -56
  21. package/dist/src/core/lib/WebGlContextWrapper.js +164 -158
  22. package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
  23. package/dist/src/core/lib/fps.d.ts +15 -0
  24. package/dist/src/core/lib/fps.js +62 -0
  25. package/dist/src/core/lib/fps.js.map +1 -0
  26. package/dist/src/core/lib/textureCompression.js +19 -10
  27. package/dist/src/core/lib/textureCompression.js.map +1 -1
  28. package/dist/src/core/lib/validateImageBitmap.d.ts +2 -1
  29. package/dist/src/core/lib/validateImageBitmap.js +4 -4
  30. package/dist/src/core/lib/validateImageBitmap.js.map +1 -1
  31. package/dist/src/core/platform.js +2 -2
  32. package/dist/src/core/platform.js.map +1 -1
  33. package/dist/src/core/platforms/Platform.d.ts +4 -0
  34. package/dist/src/core/platforms/Platform.js.map +1 -1
  35. package/dist/src/core/platforms/web/WebPlatform.d.ts +2 -0
  36. package/dist/src/core/platforms/web/WebPlatform.js +13 -0
  37. package/dist/src/core/platforms/web/WebPlatform.js.map +1 -1
  38. package/dist/src/core/renderers/CoreRenderer.d.ts +6 -0
  39. package/dist/src/core/renderers/CoreRenderer.js +8 -0
  40. package/dist/src/core/renderers/CoreRenderer.js.map +1 -1
  41. package/dist/src/core/renderers/canvas/CanvasRenderer.d.ts +1 -0
  42. package/dist/src/core/renderers/canvas/CanvasRenderer.js +5 -0
  43. package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
  44. package/dist/src/core/renderers/webgl/WebGlRenderOp.d.ts +45 -0
  45. package/dist/src/core/renderers/webgl/WebGlRenderOp.js +127 -0
  46. package/dist/src/core/renderers/webgl/WebGlRenderOp.js.map +1 -0
  47. package/dist/src/core/renderers/webgl/WebGlRenderer.d.ts +4 -2
  48. package/dist/src/core/renderers/webgl/WebGlRenderer.js +30 -22
  49. package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
  50. package/dist/src/core/renderers/webgl/WebGlShaderProgram.js +2 -3
  51. package/dist/src/core/renderers/webgl/WebGlShaderProgram.js.map +1 -1
  52. package/dist/src/core/text-rendering/CanvasFont.d.ts +14 -0
  53. package/dist/src/core/text-rendering/CanvasFont.js +120 -0
  54. package/dist/src/core/text-rendering/CanvasFont.js.map +1 -0
  55. package/dist/src/core/text-rendering/CanvasFontHandler.d.ts +1 -1
  56. package/dist/src/core/text-rendering/CanvasFontHandler.js +1 -1
  57. package/dist/src/core/text-rendering/CanvasFontHandler.js.map +1 -1
  58. package/dist/src/core/text-rendering/CanvasTextRenderer.d.ts +3 -5
  59. package/dist/src/core/text-rendering/CanvasTextRenderer.js +16 -22
  60. package/dist/src/core/text-rendering/CanvasTextRenderer.js.map +1 -1
  61. package/dist/src/core/text-rendering/CoreFont.d.ts +33 -0
  62. package/dist/src/core/text-rendering/CoreFont.js +48 -0
  63. package/dist/src/core/text-rendering/CoreFont.js.map +1 -0
  64. package/dist/src/core/text-rendering/FontManager.d.ts +11 -0
  65. package/dist/src/core/text-rendering/FontManager.js +41 -0
  66. package/dist/src/core/text-rendering/FontManager.js.map +1 -0
  67. package/dist/src/core/text-rendering/SdfFont.d.ts +29 -0
  68. package/dist/src/core/text-rendering/SdfFont.js +142 -0
  69. package/dist/src/core/text-rendering/SdfFont.js.map +1 -0
  70. package/dist/src/core/text-rendering/SdfTextRenderer.d.ts +4 -6
  71. package/dist/src/core/text-rendering/SdfTextRenderer.js +87 -168
  72. package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
  73. package/dist/src/core/text-rendering/TextGenerator.d.ts +10 -0
  74. package/dist/src/core/text-rendering/TextGenerator.js +36 -0
  75. package/dist/src/core/text-rendering/TextGenerator.js.map +1 -0
  76. package/dist/src/core/text-rendering/TextLayoutEngine.js +43 -12
  77. package/dist/src/core/text-rendering/TextLayoutEngine.js.map +1 -1
  78. package/dist/src/core/text-rendering/TextRenderer.d.ts +41 -27
  79. package/dist/src/core/text-rendering/Utils.d.ts +2 -0
  80. package/dist/src/core/text-rendering/Utils.js +3 -0
  81. package/dist/src/core/text-rendering/Utils.js.map +1 -1
  82. package/dist/src/main-api/Inspector.d.ts +1 -1
  83. package/dist/src/main-api/Inspector.js +25 -20
  84. package/dist/src/main-api/Inspector.js.map +1 -1
  85. package/dist/src/main-api/Renderer.d.ts +14 -0
  86. package/dist/src/main-api/Renderer.js +29 -3
  87. package/dist/src/main-api/Renderer.js.map +1 -1
  88. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  89. package/package.json +2 -1
  90. package/src/core/CoreNode.test.ts +1 -1
  91. package/src/core/CoreNode.ts +37 -8
  92. package/src/core/CoreTextNode.test.ts +350 -0
  93. package/src/core/CoreTextNode.ts +201 -74
  94. package/src/core/CoreTextureManager.ts +14 -2
  95. package/src/core/Stage.ts +29 -0
  96. package/src/core/TextureMemoryManager.test.ts +134 -0
  97. package/src/core/TextureMemoryManager.ts +23 -30
  98. package/src/core/platforms/Platform.ts +5 -0
  99. package/src/core/platforms/web/WebPlatform.ts +13 -0
  100. package/src/core/renderers/CoreRenderer.ts +10 -0
  101. package/src/core/renderers/canvas/CanvasRenderer.ts +6 -0
  102. package/src/core/renderers/webgl/WebGlRenderer.rtt.test.ts +551 -0
  103. package/src/core/renderers/webgl/WebGlRenderer.ts +40 -31
  104. package/src/core/renderers/webgl/WebGlShaderProgram.test.ts +274 -0
  105. package/src/core/renderers/webgl/WebGlShaderProgram.ts +7 -7
  106. package/src/core/text-rendering/CanvasFontHandler.ts +2 -2
  107. package/src/core/text-rendering/CanvasTextRenderer.ts +24 -45
  108. package/src/core/text-rendering/SdfTextRenderer.ts +106 -215
  109. package/src/core/text-rendering/TextLayoutEngine.ts +61 -28
  110. package/src/core/text-rendering/TextRenderer.ts +42 -33
  111. package/src/core/text-rendering/Utils.ts +5 -1
  112. package/src/core/text-rendering/tests/TextLayoutEngine.test.ts +20 -0
  113. package/src/main-api/Inspector.ts +25 -25
  114. package/src/main-api/Renderer.test.ts +153 -0
  115. package/src/main-api/Renderer.ts +33 -3
  116. package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.d.ts +0 -1
  117. package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.js +0 -2
  118. package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.js.map +0 -1
  119. package/src/core/renderers/webgl/SdfRenderOp.ts +0 -106
@@ -19,6 +19,7 @@
19
19
 
20
20
  import { describe, expect, it, vi, beforeEach } from 'vitest';
21
21
  import { CoreTextNode, type CoreTextNodeProps } from './CoreTextNode.js';
22
+ import { CoreNodeRenderState } from './CoreNode.js';
22
23
  import { Stage } from './Stage.js';
23
24
  import { CoreRenderer } from './renderers/CoreRenderer.js';
24
25
  import { mock } from 'vitest-mock-extended';
@@ -308,4 +309,353 @@ describe('CoreTextNode', () => {
308
309
  expect(node.isRenderable).toBe(false);
309
310
  });
310
311
  });
312
+
313
+ function makeStageWithDeleteBuffer(deleteBuffer: ReturnType<typeof vi.fn>) {
314
+ return mock<Stage>({
315
+ strictBound: createBound(0, 0, 1920, 1080),
316
+ preloadBound: createBound(0, 0, 1920, 1080),
317
+ defaultTexture: { state: 'loaded' },
318
+ renderer: { deleteBuffer } as unknown as CoreRenderer,
319
+ });
320
+ }
321
+
322
+ function createSdfRenderInfo() {
323
+ return {
324
+ type: 'sdf' as const,
325
+ width: 100,
326
+ height: 20,
327
+ atlasTexture: {} as any,
328
+ layout: {
329
+ glyphCount: 1,
330
+ width: 100,
331
+ height: 20,
332
+ fontScale: 1,
333
+ lineHeight: 20,
334
+ fontFamily: 'Arial',
335
+ distanceRange: 4,
336
+ vertexBuffer: new Float32Array([0, 0, 0, 0, 10, 0, 1, 0]),
337
+ truncatedTextLines: 0,
338
+ },
339
+ hasRemainingText: false,
340
+ remainingLines: 0,
341
+ };
342
+ }
343
+
344
+ describe('updateRenderState – SDF buffer release on OutOfBounds', () => {
345
+ it('should call renderer.deleteBuffer and clear _sdfBuffer when transitioning to OutOfBounds', () => {
346
+ const deleteBuffer = vi.fn();
347
+ const node = new CoreTextNode(
348
+ makeStageWithDeleteBuffer(deleteBuffer),
349
+ defaultTextProps,
350
+ mockTextRenderer,
351
+ );
352
+
353
+ const fakeBuffer = {};
354
+ (node as any)._sdfBuffer = fakeBuffer;
355
+ (node as any)._renderInfo = createSdfRenderInfo();
356
+
357
+ node.updateRenderState(CoreNodeRenderState.OutOfBounds);
358
+
359
+ expect(deleteBuffer).toHaveBeenCalledWith(fakeBuffer);
360
+ expect((node as any)._sdfBuffer).toBeNull();
361
+ });
362
+
363
+ it('should not call renderer.deleteBuffer when _sdfBuffer is already null', () => {
364
+ const deleteBuffer = vi.fn();
365
+ const node = new CoreTextNode(
366
+ makeStageWithDeleteBuffer(deleteBuffer),
367
+ defaultTextProps,
368
+ mockTextRenderer,
369
+ );
370
+
371
+ (node as any)._renderInfo = createSdfRenderInfo();
372
+ node.updateRenderState(CoreNodeRenderState.OutOfBounds);
373
+
374
+ expect(deleteBuffer).not.toHaveBeenCalled();
375
+ });
376
+
377
+ it('should not release the buffer when transitioning to InBounds', () => {
378
+ const deleteBuffer = vi.fn();
379
+ const node = new CoreTextNode(
380
+ makeStageWithDeleteBuffer(deleteBuffer),
381
+ defaultTextProps,
382
+ mockTextRenderer,
383
+ );
384
+
385
+ const fakeBuffer = {};
386
+ (node as any)._sdfBuffer = fakeBuffer;
387
+ (node as any)._renderInfo = createSdfRenderInfo();
388
+
389
+ node.updateRenderState(CoreNodeRenderState.InBounds);
390
+
391
+ expect(deleteBuffer).not.toHaveBeenCalled();
392
+ expect((node as any)._sdfBuffer).toBe(fakeBuffer);
393
+ });
394
+
395
+ it('should not release the buffer for a canvas-type text node', () => {
396
+ const deleteBuffer = vi.fn();
397
+ const canvasTextRenderer = {
398
+ ...mockTextRenderer,
399
+ type: 'canvas' as const,
400
+ } as any;
401
+
402
+ const node = new CoreTextNode(
403
+ makeStageWithDeleteBuffer(deleteBuffer),
404
+ defaultTextProps,
405
+ canvasTextRenderer,
406
+ );
407
+
408
+ (node as any)._renderInfo = {
409
+ type: 'canvas',
410
+ width: 100,
411
+ height: 20,
412
+ imageData: {} as ImageData,
413
+ hasRemainingText: false,
414
+ remainingLines: 0,
415
+ };
416
+
417
+ node.updateRenderState(CoreNodeRenderState.OutOfBounds);
418
+
419
+ expect(deleteBuffer).not.toHaveBeenCalled();
420
+ });
421
+ });
422
+
423
+ describe('SDF buffer release on layout regeneration', () => {
424
+ it('should call renderer.deleteBuffer before regenerating layout when font is already loaded', () => {
425
+ const deleteBuffer = vi.fn();
426
+ const props = { ...defaultTextProps, forceLoad: true };
427
+ const node = new CoreTextNode(
428
+ makeStageWithDeleteBuffer(deleteBuffer),
429
+ props,
430
+ mockTextRenderer,
431
+ );
432
+
433
+ const fakeBuffer = {} as WebGLBuffer;
434
+ (node as any)._sdfBuffer = fakeBuffer;
435
+
436
+ node.update(16, clippingRect);
437
+
438
+ expect(deleteBuffer).toHaveBeenCalledWith(fakeBuffer);
439
+ expect((node as any)._sdfBuffer).toBeNull();
440
+ });
441
+
442
+ it('should call renderer.deleteBuffer again on each subsequent layout regeneration', () => {
443
+ const deleteBuffer = vi.fn();
444
+ const props = { ...defaultTextProps, forceLoad: true };
445
+ const node = new CoreTextNode(
446
+ makeStageWithDeleteBuffer(deleteBuffer),
447
+ props,
448
+ mockTextRenderer,
449
+ );
450
+
451
+ node.update(16, clippingRect); // first layout – no buffer yet, no delete call
452
+ expect(deleteBuffer).not.toHaveBeenCalled();
453
+
454
+ // Trigger a second layout pass by invalidating the layout
455
+ node.fontSize = 24;
456
+ const secondBuffer = {} as WebGLBuffer;
457
+ (node as any)._sdfBuffer = secondBuffer;
458
+
459
+ node.update(16, clippingRect);
460
+
461
+ expect(deleteBuffer).toHaveBeenCalledWith(secondBuffer);
462
+ expect((node as any)._sdfBuffer).toBeNull();
463
+ });
464
+
465
+ it('should not call renderer.deleteBuffer when buffer is already null at regeneration time', () => {
466
+ const deleteBuffer = vi.fn();
467
+ const props = { ...defaultTextProps, forceLoad: true };
468
+ const node = new CoreTextNode(
469
+ makeStageWithDeleteBuffer(deleteBuffer),
470
+ props,
471
+ mockTextRenderer,
472
+ );
473
+
474
+ // _sdfBuffer is null by default
475
+ node.update(16, clippingRect);
476
+
477
+ expect(deleteBuffer).not.toHaveBeenCalled();
478
+ });
479
+ });
480
+
481
+ describe('SDF buffer release when text becomes invalid', () => {
482
+ it('should call renderer.deleteBuffer when text is cleared during update', () => {
483
+ const deleteBuffer = vi.fn();
484
+ const props = { ...defaultTextProps, text: 'Hello', forceLoad: true };
485
+ const node = new CoreTextNode(
486
+ makeStageWithDeleteBuffer(deleteBuffer),
487
+ props,
488
+ mockTextRenderer,
489
+ );
490
+
491
+ // Prime the node with a cached buffer
492
+ const fakeBuffer = {} as WebGLBuffer;
493
+ (node as any)._sdfBuffer = fakeBuffer;
494
+ (node as any)._layoutGenerated = true;
495
+
496
+ node.text = '';
497
+ node.update(16, clippingRect);
498
+
499
+ expect(deleteBuffer).toHaveBeenCalledWith(fakeBuffer);
500
+ expect((node as any)._sdfBuffer).toBeNull();
501
+ });
502
+
503
+ it('should not call renderer.deleteBuffer when text is invalid and buffer is already null', () => {
504
+ const deleteBuffer = vi.fn();
505
+ const props = { ...defaultTextProps, text: '', forceLoad: true };
506
+ const node = new CoreTextNode(
507
+ makeStageWithDeleteBuffer(deleteBuffer),
508
+ props,
509
+ mockTextRenderer,
510
+ );
511
+
512
+ node.update(16, clippingRect);
513
+
514
+ expect(deleteBuffer).not.toHaveBeenCalled();
515
+ });
516
+
517
+ it('should also clear _renderInfo when text becomes invalid', () => {
518
+ const deleteBuffer = vi.fn();
519
+ const props = { ...defaultTextProps, text: 'Hello', forceLoad: true };
520
+ const node = new CoreTextNode(
521
+ makeStageWithDeleteBuffer(deleteBuffer),
522
+ props,
523
+ mockTextRenderer,
524
+ );
525
+
526
+ (node as any)._renderInfo = createSdfRenderInfo();
527
+
528
+ node.text = '';
529
+ node.update(16, clippingRect);
530
+
531
+ expect((node as any)._renderInfo).toBeNull();
532
+ });
533
+ });
534
+
535
+ describe('SDF buffer release on destroy', () => {
536
+ it('should call renderer.deleteBuffer on destroy when a buffer is held', () => {
537
+ const deleteBuffer = vi.fn();
538
+ const node = new CoreTextNode(
539
+ makeStageWithDeleteBuffer(deleteBuffer),
540
+ defaultTextProps,
541
+ mockTextRenderer,
542
+ );
543
+
544
+ const fakeBuffer = {} as WebGLBuffer;
545
+ (node as any)._sdfBuffer = fakeBuffer;
546
+
547
+ node.destroy();
548
+
549
+ expect(deleteBuffer).toHaveBeenCalledWith(fakeBuffer);
550
+ expect((node as any)._sdfBuffer).toBeNull();
551
+ });
552
+
553
+ it('should not call renderer.deleteBuffer on destroy when buffer is already null', () => {
554
+ const deleteBuffer = vi.fn();
555
+ const node = new CoreTextNode(
556
+ makeStageWithDeleteBuffer(deleteBuffer),
557
+ defaultTextProps,
558
+ mockTextRenderer,
559
+ );
560
+
561
+ // _sdfBufferRef.current is null by default
562
+ node.destroy();
563
+
564
+ expect(deleteBuffer).not.toHaveBeenCalled();
565
+ });
566
+
567
+ it('should clear _renderInfo on destroy', () => {
568
+ const node = new CoreTextNode(stage, defaultTextProps, mockTextRenderer);
569
+
570
+ (node as any)._renderInfo = createSdfRenderInfo();
571
+
572
+ node.destroy();
573
+
574
+ expect((node as any)._renderInfo).toBeNull();
575
+ });
576
+ });
577
+
578
+ describe('SDF render path', () => {
579
+ it('reuses the uploaded SDF buffer across renderQuads calls', () => {
580
+ const createBuffer = vi.fn().mockReturnValue({ label: 'sdf-buffer' });
581
+ const arrayBufferData = vi.fn();
582
+ const glw = {
583
+ createBuffer,
584
+ arrayBufferData,
585
+ STATIC_DRAW: 0x88e4,
586
+ FLOAT: 0x1406,
587
+ };
588
+ const sdfStage = mock<Stage>({
589
+ strictBound: createBound(0, 0, 1920, 1080),
590
+ preloadBound: createBound(0, 0, 1920, 1080),
591
+ defaultTexture: { state: 'loaded' },
592
+ renderer: { glw } as unknown as CoreRenderer,
593
+ });
594
+ const node = new CoreTextNode(
595
+ sdfStage,
596
+ defaultTextProps,
597
+ mockTextRenderer,
598
+ );
599
+ const transform = new Float32Array([1, 0, 0, 1, 0, 0]);
600
+
601
+ (node as any).handleRenderResult(createSdfRenderInfo());
602
+ (node as any).globalTransform = {
603
+ getFloatArr: vi.fn().mockReturnValue(transform),
604
+ };
605
+
606
+ node.renderQuads(sdfStage.renderer);
607
+ node.renderQuads(sdfStage.renderer);
608
+
609
+ expect(createBuffer).toHaveBeenCalledTimes(1);
610
+ expect(arrayBufferData).toHaveBeenCalledTimes(1);
611
+ expect(mockTextRenderer.renderQuads).toHaveBeenCalledTimes(2);
612
+ expect((node as any)._sdfBuffer).toEqual({ label: 'sdf-buffer' });
613
+ expect(node.sdfShaderProps.transform).toBe(transform);
614
+ });
615
+
616
+ it('uses framebuffer-relative scissor coordinates for RTT draws', () => {
617
+ const bindRenderOp = vi.fn();
618
+ const useShader = vi.fn();
619
+ const setScissorTest = vi.fn();
620
+ const scissor = vi.fn();
621
+ const drawArrays = vi.fn();
622
+ const sdfStage = mock<Stage>({
623
+ strictBound: createBound(0, 0, 1920, 1080),
624
+ preloadBound: createBound(0, 0, 1920, 1080),
625
+ defaultTexture: { state: 'loaded' },
626
+ pixelRatio: 2,
627
+ platform: { canvas: { width: 1920, height: 1080 } } as any,
628
+ shManager: { useShader } as any,
629
+ });
630
+ const node = new CoreTextNode(
631
+ sdfStage,
632
+ defaultTextProps,
633
+ mockTextRenderer,
634
+ );
635
+ const shader = { program: { bindRenderOp } };
636
+
637
+ node.props.shader = shader as any;
638
+ node.numQuads = 2;
639
+ node.clippingRect = { x: 10, y: 20, w: 30, h: 40, valid: true };
640
+ node.parentHasRenderTexture = true;
641
+ node.rttParent = { framebufferDimensions: { w: 320, h: 180 } } as any;
642
+ node.props.h = 25;
643
+
644
+ node.draw({
645
+ glw: {
646
+ TRIANGLES: 4,
647
+ setScissorTest,
648
+ scissor,
649
+ drawArrays,
650
+ },
651
+ stage: sdfStage,
652
+ } as any);
653
+
654
+ expect(useShader).toHaveBeenCalledWith(shader.program);
655
+ expect(bindRenderOp).toHaveBeenCalledWith(node);
656
+ expect(setScissorTest).toHaveBeenCalledWith(true);
657
+ expect(scissor).toHaveBeenCalledWith(10, 155, 30, 40);
658
+ expect(drawArrays).toHaveBeenCalledWith(4, 0, 12);
659
+ });
660
+ });
311
661
  });