@supersoniks/concorde 4.9.98-visual-stack.0 → 4.9.98-visual-stack.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 (164) hide show
  1. package/build-infos.json +1 -1
  2. package/concorde-3d.bundle.js +201 -152
  3. package/concorde-3d.es.js +14687 -13905
  4. package/concorde-core.bundle.js +1 -1
  5. package/concorde-core.es.js +1 -1
  6. package/concorde-hugging-face-infer.bundle.js +1 -1
  7. package/concorde-hugging-face-infer.es.js +1 -1
  8. package/concorde-shader.bundle.js +31 -20
  9. package/concorde-shader.es.js +2773 -2547
  10. package/concorde-webgpu.bundle.js +43 -32
  11. package/concorde-webgpu.es.js +1819 -1553
  12. package/dist/concorde-3d.bundle.js +201 -152
  13. package/dist/concorde-3d.es.js +14687 -13905
  14. package/dist/concorde-core.bundle.js +1 -1
  15. package/dist/concorde-core.es.js +1 -1
  16. package/dist/concorde-hugging-face-infer.bundle.js +1 -1
  17. package/dist/concorde-hugging-face-infer.es.js +1 -1
  18. package/dist/concorde-shader.bundle.js +31 -20
  19. package/dist/concorde-shader.es.js +2773 -2547
  20. package/dist/concorde-webgpu.bundle.js +43 -32
  21. package/dist/concorde-webgpu.es.js +1819 -1553
  22. package/dist/js/core/components/functional/3d/3d.js +90 -2
  23. package/dist/js/core/components/functional/3d/3d.js.map +1 -1
  24. package/dist/js/core/components/functional/3d/constants.js +1 -0
  25. package/dist/js/core/components/functional/3d/constants.js.map +1 -1
  26. package/dist/js/core/components/functional/3d/ensure.js +6 -3
  27. package/dist/js/core/components/functional/3d/ensure.js.map +1 -1
  28. package/dist/js/core/components/functional/3d/form-params.js +2 -0
  29. package/dist/js/core/components/functional/3d/form-params.js.map +1 -1
  30. package/dist/js/core/components/functional/3d/life.js +2 -1
  31. package/dist/js/core/components/functional/3d/life.js.map +1 -1
  32. package/dist/js/core/components/functional/3d/out-publish.js +3 -2
  33. package/dist/js/core/components/functional/3d/out-publish.js.map +1 -1
  34. package/dist/js/core/components/functional/3d/playback.js +6 -1
  35. package/dist/js/core/components/functional/3d/playback.js.map +1 -1
  36. package/dist/js/core/components/functional/3d/post-bind.js +207 -0
  37. package/dist/js/core/components/functional/3d/post-bind.js.map +1 -0
  38. package/dist/js/core/components/functional/3d/post-channels.js +39 -0
  39. package/dist/js/core/components/functional/3d/post-channels.js.map +1 -0
  40. package/dist/js/core/components/functional/3d/post-fx.js +495 -0
  41. package/dist/js/core/components/functional/3d/post-fx.js.map +1 -0
  42. package/dist/js/core/components/functional/3d/props.js +142 -0
  43. package/dist/js/core/components/functional/3d/props.js.map +1 -1
  44. package/dist/js/core/components/functional/3d/reload.js +2 -0
  45. package/dist/js/core/components/functional/3d/reload.js.map +1 -1
  46. package/dist/js/core/components/functional/3d/runtime.js +80 -3
  47. package/dist/js/core/components/functional/3d/runtime.js.map +1 -1
  48. package/dist/js/core/components/functional/3d/scene-runtime.js +1 -1
  49. package/dist/js/core/components/functional/3d/scene-runtime.js.map +1 -1
  50. package/dist/js/core/components/functional/3d/tune.js +21 -0
  51. package/dist/js/core/components/functional/3d/tune.js.map +1 -1
  52. package/dist/js/core/components/functional/3d/updated.js +13 -1
  53. package/dist/js/core/components/functional/3d/updated.js.map +1 -1
  54. package/dist/js/core/components/functional/shader/gl-runtime.js +128 -14
  55. package/dist/js/core/components/functional/shader/gl-runtime.js.map +1 -1
  56. package/dist/js/core/components/functional/shader/shader.js +154 -57
  57. package/dist/js/core/components/functional/shader/shader.js.map +1 -1
  58. package/dist/js/core/components/functional/shader/shadertoy-prelude.js +39 -5
  59. package/dist/js/core/components/functional/shader/shadertoy-prelude.js.map +1 -1
  60. package/dist/js/core/components/functional/shader/types.js +58 -7
  61. package/dist/js/core/components/functional/shader/types.js.map +1 -1
  62. package/dist/js/core/components/functional/webgpu/gl-fallback.js +27 -6
  63. package/dist/js/core/components/functional/webgpu/gl-fallback.js.map +1 -1
  64. package/dist/js/core/components/functional/webgpu/gpu-runtime.js +115 -13
  65. package/dist/js/core/components/functional/webgpu/gpu-runtime.js.map +1 -1
  66. package/dist/js/core/components/functional/webgpu/helpers.js +8 -2
  67. package/dist/js/core/components/functional/webgpu/helpers.js.map +1 -1
  68. package/dist/js/core/components/functional/webgpu/webgpu.js +34 -1
  69. package/dist/js/core/components/functional/webgpu/webgpu.js.map +1 -1
  70. package/dist/js/core/utils/mediaRef.js +37 -0
  71. package/dist/js/core/utils/mediaRef.js.map +1 -1
  72. package/dist/types/core/components/functional/3d/3d.d.ts +26 -3
  73. package/dist/types/core/components/functional/3d/3d.d.ts.map +1 -1
  74. package/dist/types/core/components/functional/3d/constants.d.ts +1 -1
  75. package/dist/types/core/components/functional/3d/constants.d.ts.map +1 -1
  76. package/dist/types/core/components/functional/3d/ensure.d.ts +3 -0
  77. package/dist/types/core/components/functional/3d/ensure.d.ts.map +1 -1
  78. package/dist/types/core/components/functional/3d/form-params.d.ts.map +1 -1
  79. package/dist/types/core/components/functional/3d/life.d.ts +2 -0
  80. package/dist/types/core/components/functional/3d/life.d.ts.map +1 -1
  81. package/dist/types/core/components/functional/3d/out-publish.d.ts +1 -0
  82. package/dist/types/core/components/functional/3d/out-publish.d.ts.map +1 -1
  83. package/dist/types/core/components/functional/3d/playback.d.ts +9 -0
  84. package/dist/types/core/components/functional/3d/playback.d.ts.map +1 -1
  85. package/dist/types/core/components/functional/3d/post-bind.d.ts +52 -0
  86. package/dist/types/core/components/functional/3d/post-bind.d.ts.map +1 -0
  87. package/dist/types/core/components/functional/3d/post-channels.d.ts +40 -0
  88. package/dist/types/core/components/functional/3d/post-channels.d.ts.map +1 -0
  89. package/dist/types/core/components/functional/3d/post-fx.d.ts +73 -0
  90. package/dist/types/core/components/functional/3d/post-fx.d.ts.map +1 -0
  91. package/dist/types/core/components/functional/3d/props.d.ts +42 -0
  92. package/dist/types/core/components/functional/3d/props.d.ts.map +1 -1
  93. package/dist/types/core/components/functional/3d/reload.d.ts.map +1 -1
  94. package/dist/types/core/components/functional/3d/runtime.d.ts +16 -1
  95. package/dist/types/core/components/functional/3d/runtime.d.ts.map +1 -1
  96. package/dist/types/core/components/functional/3d/scene-runtime.d.ts +1 -1
  97. package/dist/types/core/components/functional/3d/scene-runtime.d.ts.map +1 -1
  98. package/dist/types/core/components/functional/3d/tune.d.ts +5 -0
  99. package/dist/types/core/components/functional/3d/tune.d.ts.map +1 -1
  100. package/dist/types/core/components/functional/3d/types.d.ts +9 -0
  101. package/dist/types/core/components/functional/3d/types.d.ts.map +1 -1
  102. package/dist/types/core/components/functional/3d/updated.d.ts.map +1 -1
  103. package/dist/types/core/components/functional/shader/gl-runtime.d.ts +33 -1
  104. package/dist/types/core/components/functional/shader/gl-runtime.d.ts.map +1 -1
  105. package/dist/types/core/components/functional/shader/shader.d.ts +28 -7
  106. package/dist/types/core/components/functional/shader/shader.d.ts.map +1 -1
  107. package/dist/types/core/components/functional/shader/shadertoy-prelude.d.ts +16 -2
  108. package/dist/types/core/components/functional/shader/shadertoy-prelude.d.ts.map +1 -1
  109. package/dist/types/core/components/functional/shader/types.d.ts +29 -5
  110. package/dist/types/core/components/functional/shader/types.d.ts.map +1 -1
  111. package/dist/types/core/components/functional/webgpu/gl-fallback.d.ts +4 -0
  112. package/dist/types/core/components/functional/webgpu/gl-fallback.d.ts.map +1 -1
  113. package/dist/types/core/components/functional/webgpu/gpu-runtime.d.ts +14 -0
  114. package/dist/types/core/components/functional/webgpu/gpu-runtime.d.ts.map +1 -1
  115. package/dist/types/core/components/functional/webgpu/helpers.d.ts +1 -0
  116. package/dist/types/core/components/functional/webgpu/helpers.d.ts.map +1 -1
  117. package/dist/types/core/components/functional/webgpu/webgpu.d.ts +7 -1
  118. package/dist/types/core/components/functional/webgpu/webgpu.d.ts.map +1 -1
  119. package/dist/types/core/utils/mediaRef.d.ts +15 -0
  120. package/dist/types/core/utils/mediaRef.d.ts.map +1 -1
  121. package/package.json +37 -1
  122. package/src/core/components/functional/3d/3d.md +34 -1
  123. package/src/core/components/functional/3d/3d.spec.ts +42 -0
  124. package/src/core/components/functional/3d/3d.ts +101 -3
  125. package/src/core/components/functional/3d/constants.ts +1 -0
  126. package/src/core/components/functional/3d/ensure.ts +11 -3
  127. package/src/core/components/functional/3d/form-params.ts +2 -0
  128. package/src/core/components/functional/3d/life.ts +4 -1
  129. package/src/core/components/functional/3d/out-publish.ts +4 -3
  130. package/src/core/components/functional/3d/playback.ts +13 -1
  131. package/src/core/components/functional/3d/post-bind.ts +262 -0
  132. package/src/core/components/functional/3d/post-channels.spec.ts +36 -0
  133. package/src/core/components/functional/3d/post-channels.ts +69 -0
  134. package/src/core/components/functional/3d/post-fx.ts +594 -0
  135. package/src/core/components/functional/3d/props.ts +45 -0
  136. package/src/core/components/functional/3d/reload.ts +2 -0
  137. package/src/core/components/functional/3d/runtime.ts +93 -4
  138. package/src/core/components/functional/3d/scene-runtime.ts +3 -0
  139. package/src/core/components/functional/3d/tune.ts +21 -0
  140. package/src/core/components/functional/3d/types.ts +9 -0
  141. package/src/core/components/functional/3d/updated.ts +17 -1
  142. package/src/core/components/functional/shader/gl-runtime.ts +169 -12
  143. package/src/core/components/functional/shader/shader.md +27 -4
  144. package/src/core/components/functional/shader/shader.spec.ts +267 -0
  145. package/src/core/components/functional/shader/shader.ts +177 -55
  146. package/src/core/components/functional/shader/shadertoy-prelude.ts +49 -5
  147. package/src/core/components/functional/shader/types.ts +89 -12
  148. package/src/core/components/functional/webgpu/gl-fallback.ts +28 -4
  149. package/src/core/components/functional/webgpu/gpu-runtime.ts +154 -18
  150. package/src/core/components/functional/webgpu/helpers.ts +8 -2
  151. package/src/core/components/functional/webgpu/webgpu.md +13 -2
  152. package/src/core/components/functional/webgpu/webgpu.spec.ts +37 -0
  153. package/src/core/components/functional/webgpu/webgpu.ts +39 -1
  154. package/src/core/utils/mediaRef.spec.ts +25 -0
  155. package/src/core/utils/mediaRef.ts +45 -0
  156. package/src/docs/_misc/visual-stack.md +19 -1
  157. package/src/docs/components/docs-demo-sources.ts +28 -0
  158. package/src/docs/docs.ts +2 -0
  159. package/src/docs/example/docs-3d-post-depth-demo.ts +68 -0
  160. package/src/docs/example/docs-3d-webgpu-live-demo.ts +83 -0
  161. package/src/docs/example/docs-harris-webgpu-demo.ts +53 -55
  162. package/src/docs/search/docs-search.json +252 -2
  163. package/src/tsconfig.json +27 -0
  164. package/src/tsconfig.tsbuildinfo +1 -1
@@ -8,6 +8,88 @@ import { buildFragmentSource } from "./shadertoy-prelude";
8
8
  import "./shader";
9
9
  import { SonicShader, channelFormValue } from "./shader";
10
10
 
11
+ describe("parseBufferFormat", () => {
12
+ it("accepte rgba8 / rgba16f / rgba32f", async () => {
13
+ const { parseBufferFormat } = await import("./types");
14
+ expect(parseBufferFormat("rgba8")).toBe("rgba8");
15
+ expect(parseBufferFormat("rgba16f")).toBe("rgba16f");
16
+ expect(parseBufferFormat("half")).toBe("rgba16f");
17
+ expect(parseBufferFormat("rgba32f")).toBe("rgba32f");
18
+ expect(parseBufferFormat("float")).toBe("rgba32f");
19
+ expect(parseBufferFormat("")).toBe("rgba8");
20
+ expect(parseBufferFormat("nope")).toBe("rgba8");
21
+ });
22
+ });
23
+
24
+ describe("resolveBufferGlFormat", () => {
25
+ it("reste rgba8 sans extension float", async () => {
26
+ const { resolveBufferGlFormat, resetBufferFormatWarnFlag } = await import(
27
+ "./gl-runtime"
28
+ );
29
+ resetBufferFormatWarnFlag();
30
+ const gl = {
31
+ RGBA8: 0x8058,
32
+ RGBA16F: 0x881a,
33
+ RGBA32F: 0x8814,
34
+ RGBA: 0x1908,
35
+ UNSIGNED_BYTE: 0x1401,
36
+ HALF_FLOAT: 0x140b,
37
+ FLOAT: 0x1406,
38
+ LINEAR: 0x2601,
39
+ NEAREST: 0x2600,
40
+ getExtension: () => null,
41
+ } as unknown as WebGL2RenderingContext;
42
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
43
+ const fmt = resolveBufferGlFormat(gl, "rgba16f");
44
+ expect(fmt.active).toBe("rgba8");
45
+ expect(fmt.float).toBe(false);
46
+ expect(warn).toHaveBeenCalledOnce();
47
+ warn.mockRestore();
48
+ });
49
+
50
+ it("accepte rgba16f avec EXT_color_buffer_float", async () => {
51
+ const { resolveBufferGlFormat, resetBufferFormatWarnFlag } = await import(
52
+ "./gl-runtime"
53
+ );
54
+ resetBufferFormatWarnFlag();
55
+ const gl = {
56
+ RGBA8: 0x8058,
57
+ RGBA16F: 0x881a,
58
+ RGBA32F: 0x8814,
59
+ RGBA: 0x1908,
60
+ UNSIGNED_BYTE: 0x1401,
61
+ HALF_FLOAT: 0x140b,
62
+ FLOAT: 0x1406,
63
+ LINEAR: 0x2601,
64
+ NEAREST: 0x2600,
65
+ getExtension: (name: string) =>
66
+ name === "EXT_color_buffer_float" ? {} : null,
67
+ } as unknown as WebGL2RenderingContext;
68
+ const fmt = resolveBufferGlFormat(gl, "rgba16f");
69
+ expect(fmt.active).toBe("rgba16f");
70
+ expect(fmt.float).toBe(true);
71
+ expect(fmt.type).toBe(gl.HALF_FLOAT);
72
+ });
73
+ });
74
+
75
+ describe("findPeaks float", () => {
76
+ it("lit des scores Float32 déjà normalisés", async () => {
77
+ const { findPeaks } = await import("./types");
78
+ const w = 8;
79
+ const h = 8;
80
+ const data = new Float32Array(w * h * 4);
81
+ data[(3 * w + 3) * 4] = 0.9;
82
+ data[(3 * w + 4) * 4] = 0.4;
83
+ const peaks = findPeaks(data, w, h, {
84
+ threshold: 0.5,
85
+ maxCount: 2,
86
+ minDistance: 2,
87
+ });
88
+ expect(peaks.length).toBeGreaterThanOrEqual(1);
89
+ expect(peaks[0].score).toBeCloseTo(0.9, 5);
90
+ });
91
+ });
92
+
11
93
  describe("parseChannelSource", () => {
12
94
  it("résout self / buffers / url / fallback", () => {
13
95
  expect(parseChannelSource("self", "")).toEqual({ kind: "self" });
@@ -224,6 +306,39 @@ describe("meanRgba", () => {
224
306
  });
225
307
  });
226
308
 
309
+ describe("release-offscreen auto / self", () => {
310
+ it("parseReleaseOffscreen", async () => {
311
+ const { parseReleaseOffscreen } = await import("./types");
312
+ expect(parseReleaseOffscreen(null)).toBe("auto");
313
+ expect(parseReleaseOffscreen("auto")).toBe("auto");
314
+ expect(parseReleaseOffscreen("")).toBe(true);
315
+ expect(parseReleaseOffscreen("true")).toBe(true);
316
+ expect(parseReleaseOffscreen("false")).toBe(false);
317
+ });
318
+
319
+ it("shouldReleaseWhenOffscreen", async () => {
320
+ const { shouldReleaseWhenOffscreen } = await import("./types");
321
+ expect(shouldReleaseWhenOffscreen("auto", true)).toBe(false);
322
+ expect(shouldReleaseWhenOffscreen("auto", false)).toBe(true);
323
+ expect(shouldReleaseWhenOffscreen(true, true)).toBe(true);
324
+ expect(shouldReleaseWhenOffscreen(false, false)).toBe(false);
325
+ });
326
+
327
+ it("channelsUseSelf détecte self", async () => {
328
+ const { channelsUseSelf } = await import("./types");
329
+ expect(
330
+ channelsUseSelf([
331
+ [{ kind: "url", url: "/a.png" }, { kind: "none" }, { kind: "none" }, { kind: "none" }],
332
+ ]),
333
+ ).toBe(false);
334
+ expect(
335
+ channelsUseSelf([
336
+ [{ kind: "self" }, { kind: "none" }, { kind: "none" }, { kind: "none" }],
337
+ ]),
338
+ ).toBe(true);
339
+ });
340
+ });
341
+
227
342
  describe("SonicShader", () => {
228
343
  afterEach(() => {
229
344
  document.body.innerHTML = "";
@@ -365,4 +480,156 @@ describe("SonicShader", () => {
365
480
  expect(el.shadowRoot?.querySelector("canvas")).toBeTruthy();
366
481
  }
367
482
  });
483
+
484
+ it("changement de canal → rebindChannels, pas rebuild", async () => {
485
+ const el = TestUtils.bootstrap(
486
+ `<sonic-shader style="width:32px;height:32px"></sonic-shader>`,
487
+ true,
488
+ )[0] as SonicShader;
489
+ el.image =
490
+ "void mainImage(out vec4 fragColor, in vec2 fragCoord){ fragColor=vec4(1.0); }";
491
+ await el.updateComplete;
492
+
493
+ const rt = {
494
+ isLost: false,
495
+ setPassChannels: vi.fn(),
496
+ ensureTextures: vi.fn(async () => {}),
497
+ ensureElementTexture: vi.fn(),
498
+ resize: vi.fn(),
499
+ dispose: vi.fn(),
500
+ setVideosPlaying: vi.fn(),
501
+ };
502
+ const internal = el as unknown as {
503
+ runtime: typeof rt;
504
+ ready: boolean;
505
+ rebuild: () => Promise<void>;
506
+ rebindChannels: () => Promise<void>;
507
+ drawFrame: (now: number) => void;
508
+ };
509
+ internal.runtime = rt;
510
+ internal.ready = true;
511
+ vi.spyOn(internal, "drawFrame").mockImplementation(() => {});
512
+
513
+ const rebuildSpy = vi.spyOn(internal, "rebuild");
514
+ const rebindSpy = vi.spyOn(internal, "rebindChannels");
515
+ rebuildSpy.mockClear();
516
+ rebindSpy.mockClear();
517
+ rt.setPassChannels.mockClear();
518
+
519
+ el.bufferACh1 = "#autre";
520
+ await el.updateComplete;
521
+ await new Promise((r) => setTimeout(r, 0));
522
+
523
+ expect(rebuildSpy).not.toHaveBeenCalled();
524
+ expect(rebindSpy).toHaveBeenCalled();
525
+ expect(rt.setPassChannels).toHaveBeenCalled();
526
+ });
527
+
528
+ it("release-offscreen=auto : conserve le runtime si self hors écran", async () => {
529
+ const el = TestUtils.bootstrap(
530
+ `<sonic-shader release-offscreen="auto" style="width:32px;height:32px"></sonic-shader>`,
531
+ true,
532
+ )[0] as SonicShader;
533
+ await el.updateComplete;
534
+ expect(el.releaseOffscreen).toBe("auto");
535
+
536
+ const dispose = vi.fn();
537
+ const rt = {
538
+ isLost: false,
539
+ dispose,
540
+ setVideosPlaying: vi.fn(),
541
+ setPassChannels: vi.fn(),
542
+ ensureTextures: vi.fn(async () => {}),
543
+ ensureElementTexture: vi.fn(),
544
+ resize: vi.fn(),
545
+ };
546
+ const internal = el as unknown as {
547
+ runtime: typeof rt | null;
548
+ ready: boolean;
549
+ inView: boolean;
550
+ syncVisibility: () => void;
551
+ drawFrame: (now: number) => void;
552
+ };
553
+ internal.runtime = rt;
554
+ internal.ready = true;
555
+ internal.inView = true;
556
+ vi.spyOn(internal, "drawFrame").mockImplementation(() => {});
557
+
558
+ el.bufferACh0 = "self";
559
+ await el.updateComplete;
560
+ await new Promise((r) => setTimeout(r, 0));
561
+
562
+ internal.inView = false;
563
+ internal.syncVisibility();
564
+ expect(dispose).not.toHaveBeenCalled();
565
+ expect(internal.runtime).toBe(rt);
566
+ });
567
+
568
+ it("release-offscreen=auto : libère hors écran sans self", async () => {
569
+ const el = TestUtils.bootstrap(
570
+ `<sonic-shader release-offscreen="auto" style="width:32px;height:32px"></sonic-shader>`,
571
+ true,
572
+ )[0] as SonicShader;
573
+ await el.updateComplete;
574
+ const dispose = vi.fn();
575
+ const rt = {
576
+ isLost: false,
577
+ dispose,
578
+ setVideosPlaying: vi.fn(),
579
+ };
580
+ const internal = el as unknown as {
581
+ runtime: typeof rt | null;
582
+ ready: boolean;
583
+ inView: boolean;
584
+ syncVisibility: () => void;
585
+ drawFrame: (now: number) => void;
586
+ };
587
+ internal.runtime = rt;
588
+ internal.ready = true;
589
+ internal.inView = false;
590
+ vi.spyOn(internal, "drawFrame").mockImplementation(() => {});
591
+ internal.syncVisibility();
592
+ expect(dispose).toHaveBeenCalledWith(false);
593
+ expect(internal.runtime).toBeNull();
594
+ });
595
+
596
+ it("release-offscreen=true : libère même avec self", async () => {
597
+ const el = TestUtils.bootstrap(
598
+ `<sonic-shader release-offscreen style="width:32px;height:32px"></sonic-shader>`,
599
+ true,
600
+ )[0] as SonicShader;
601
+ await el.updateComplete;
602
+ expect(el.releaseOffscreen).toBe(true);
603
+
604
+ const dispose = vi.fn();
605
+ const rt = {
606
+ isLost: false,
607
+ dispose,
608
+ setVideosPlaying: vi.fn(),
609
+ setPassChannels: vi.fn(),
610
+ ensureTextures: vi.fn(async () => {}),
611
+ ensureElementTexture: vi.fn(),
612
+ resize: vi.fn(),
613
+ };
614
+ const internal = el as unknown as {
615
+ runtime: typeof rt | null;
616
+ ready: boolean;
617
+ inView: boolean;
618
+ syncVisibility: () => void;
619
+ drawFrame: (now: number) => void;
620
+ };
621
+ internal.runtime = rt;
622
+ internal.ready = true;
623
+ internal.inView = true;
624
+ vi.spyOn(internal, "drawFrame").mockImplementation(() => {});
625
+
626
+ el.bufferACh0 = "self";
627
+ await el.updateComplete;
628
+ await new Promise((r) => setTimeout(r, 0));
629
+
630
+ internal.inView = false;
631
+ internal.syncVisibility();
632
+ expect(dispose).toHaveBeenCalledWith(false);
633
+ expect(internal.runtime).toBeNull();
634
+ });
368
635
  });
@@ -1,7 +1,7 @@
1
1
  import { css, html, LitElement } from "lit";
2
2
  import { customElement, property, state } from "lit/decorators.js";
3
3
  import HTML from "@supersoniks/concorde/core/utils/HTML";
4
- import { revokeMediaUrls, toMediaUrl, mediaElementKey, lookupLiveElement } from "@supersoniks/concorde/core/utils/mediaRef";
4
+ import { revokeMediaUrls, toMediaUrl, mediaElementKey, lookupLiveElement, FrameConsumerRegistry } from "@supersoniks/concorde/core/utils/mediaRef";
5
5
  import { PublisherManager } from "@supersoniks/concorde/utils";
6
6
  import {
7
7
  parseChannelSource,
@@ -9,6 +9,8 @@ import {
9
9
  } from "./gl-runtime";
10
10
  import {
11
11
  PassChannels,
12
+ ReleaseOffscreen,
13
+ ShaderBufferFormat,
12
14
  ShaderCompileError,
13
15
  ShaderExtractItem,
14
16
  ShaderExtractOutput,
@@ -18,17 +20,61 @@ import {
18
20
  ShaderRoi,
19
21
  ShaderSources,
20
22
  SonicShaderOutput,
23
+ channelsUseSelf,
21
24
  findPeaks,
22
25
  meanRgba,
26
+ parseBufferFormat,
23
27
  parseExtractPass,
24
28
  parseHoverParam,
29
+ parseReleaseOffscreen,
25
30
  roiToPixels,
31
+ rgbaToUint8,
32
+ shouldReleaseWhenOffscreen,
26
33
  } from "./types";
27
34
 
28
35
  const tagName = "sonic-shader";
29
36
 
30
37
  const MAX_PATCH_PX = 128;
31
38
 
39
+ /** Clés qui forcent une recompilation / recreate FBO. */
40
+ const PROGRAM_KEYS = [
41
+ "image",
42
+ "bufferA",
43
+ "bufferB",
44
+ "bufferC",
45
+ "bufferD",
46
+ "common",
47
+ "bufferFormat",
48
+ ] as const;
49
+
50
+ /** Clés qui ne font que re-lier les textures (pas de rebuild). */
51
+ const CHANNEL_KEYS = [
52
+ "channel0",
53
+ "channel1",
54
+ "channel2",
55
+ "channel3",
56
+ "imageCh0",
57
+ "imageCh1",
58
+ "imageCh2",
59
+ "imageCh3",
60
+ "bufferACh0",
61
+ "bufferACh1",
62
+ "bufferACh2",
63
+ "bufferACh3",
64
+ "bufferBCh0",
65
+ "bufferBCh1",
66
+ "bufferBCh2",
67
+ "bufferBCh3",
68
+ "bufferCCh0",
69
+ "bufferCCh1",
70
+ "bufferCCh2",
71
+ "bufferCCh3",
72
+ "bufferDCh0",
73
+ "bufferDCh1",
74
+ "bufferDCh2",
75
+ "bufferDCh3",
76
+ ] as const;
77
+
32
78
  /**
33
79
  * Champs méta publiés vers le out-dataProvider.
34
80
  * `param0…3` sont exclus du gate « consommateur » : un `sonic-input name="param0"`
@@ -294,12 +340,32 @@ export class SonicShader extends LitElement {
294
340
  /** Vitesse d’easing hover on/off (1/s, défaut 10). */
295
341
  @property({ type: Number, attribute: "hover-speed" }) hoverSpeed = 10;
296
342
  /**
297
- * Libère le contexte WebGL hors viewport (défaut true).
298
- * Les navigateurs limitent ~8–16 contextes.
343
+ * Hors viewport : libère textures/FBO (garde le contexte).
344
+ * `auto` (défaut) : ne libère pas si une passe lit `self`.
345
+ * `true` / `false` forcent le comportement.
299
346
  */
300
- @property({ type: Boolean, attribute: "release-offscreen" })
301
- releaseOffscreen = true;
347
+ @property({
348
+ attribute: "release-offscreen",
349
+ converter: {
350
+ fromAttribute: (value: string | null) => parseReleaseOffscreen(value),
351
+ toAttribute: (value: ReleaseOffscreen) => {
352
+ if (value === "auto") return "auto";
353
+ if (value === false) return "false";
354
+ return "";
355
+ },
356
+ },
357
+ })
358
+ releaseOffscreen: ReleaseOffscreen = "auto";
302
359
  @property({ type: Number, attribute: "dpr-max" }) dprMax = 2;
360
+ /**
361
+ * Format des buffers A–D : `rgba8` (défaut) | `rgba16f` | `rgba32f`.
362
+ * Attribut reflété `active-buffer-format` = format réellement utilisé.
363
+ */
364
+ @property({ attribute: "buffer-format" })
365
+ bufferFormat: ShaderBufferFormat | string = "rgba8";
366
+ /** Format actif après repli éventuel (lecture seule, reflect). */
367
+ @property({ attribute: "active-buffer-format", reflect: true })
368
+ activeBufferFormat: ShaderBufferFormat = "rgba8";
303
369
 
304
370
  /** Generic float uniforms `uParam0`…`uParam3` (no shader rebuild). */
305
371
  @property({ type: Number, attribute: "param0" }) param0 = 0;
@@ -399,6 +465,9 @@ export class SonicShader extends LitElement {
399
465
  private resizeObserver: ResizeObserver | null = null;
400
466
  private intersectionObserver: IntersectionObserver | null = null;
401
467
  private rebuildToken = 0;
468
+ /** Annule un `rebindChannels` en vol si rebuild / release intervient. */
469
+ private rebindToken = 0;
470
+ private frameConsumers = new FrameConsumerRegistry();
402
471
  private ready = false;
403
472
  private formPublisher: ReturnType<typeof PublisherManager.get> | null = null;
404
473
  private outPublisher: ReturnType<typeof PublisherManager.get> | null = null;
@@ -479,9 +548,11 @@ export class SonicShader extends LitElement {
479
548
  }
480
549
  this.pendingRevokeUrls = [];
481
550
  this.stopLoop();
551
+ this.frameConsumers.clear();
482
552
  this.resizeObserver?.disconnect();
483
553
  this.intersectionObserver?.disconnect();
484
554
  this.rebuildToken++;
555
+ this.rebindToken++;
485
556
  this.ready = false;
486
557
  this.releasing = true;
487
558
  this.runtime?.dispose(true); // libère le slot navigateur
@@ -505,7 +576,7 @@ export class SonicShader extends LitElement {
505
576
  this.bindOutPublisher();
506
577
  this.bindExtractPublisher();
507
578
  requestAnimationFrame(() => {
508
- if (this.inView || !this.releaseOffscreen) {
579
+ if (this.inView || !this.wantsReleaseOffscreen()) {
509
580
  void this.ensureRuntime();
510
581
  }
511
582
  });
@@ -513,44 +584,19 @@ export class SonicShader extends LitElement {
513
584
 
514
585
  protected updated(changed: Map<string, unknown>) {
515
586
  if (!this.runtime) return;
516
- const shaderKeys = [
517
- "image",
518
- "bufferA",
519
- "bufferB",
520
- "bufferC",
521
- "bufferD",
522
- "common",
523
- "channel0",
524
- "channel1",
525
- "channel2",
526
- "channel3",
527
- "imageCh0",
528
- "imageCh1",
529
- "imageCh2",
530
- "imageCh3",
531
- "bufferACh0",
532
- "bufferACh1",
533
- "bufferACh2",
534
- "bufferACh3",
535
- "bufferBCh0",
536
- "bufferBCh1",
537
- "bufferBCh2",
538
- "bufferBCh3",
539
- "bufferCCh0",
540
- "bufferCCh1",
541
- "bufferCCh2",
542
- "bufferCCh3",
543
- "bufferDCh0",
544
- "bufferDCh1",
545
- "bufferDCh2",
546
- "bufferDCh3",
547
- ];
548
- if (shaderKeys.some((k) => changed.has(k))) {
587
+ const programChanged = PROGRAM_KEYS.some((k) => changed.has(k));
588
+ const channelChanged = CHANNEL_KEYS.some((k) => changed.has(k));
589
+ if (programChanged) {
549
590
  void this.rebuild();
591
+ } else if (channelChanged) {
592
+ void this.rebindChannels();
550
593
  }
551
594
  if (changed.has("play")) {
552
595
  this.syncPlayback();
553
596
  }
597
+ if (changed.has("releaseOffscreen") || channelChanged) {
598
+ this.syncVisibility();
599
+ }
554
600
  if (
555
601
  changed.has("dataProvider") ||
556
602
  changed.has("outDataProvider") ||
@@ -696,6 +742,7 @@ export class SonicShader extends LitElement {
696
742
  */
697
743
  private releaseRuntime() {
698
744
  this.rebuildToken++;
745
+ this.rebindToken++;
699
746
  this.stopLoop();
700
747
  this.ready = false;
701
748
  this.releasing = true;
@@ -752,6 +799,7 @@ export class SonicShader extends LitElement {
752
799
  e.preventDefault();
753
800
  if (this.releasing) return;
754
801
  this.rebuildToken++;
802
+ this.rebindToken++;
755
803
  this.stopLoop();
756
804
  this.ready = false;
757
805
  this.runtime = null;
@@ -766,7 +814,8 @@ export class SonicShader extends LitElement {
766
814
  if (this.runtime && !this.runtime.isLost) return;
767
815
  this.runtime = null;
768
816
  await this.updateComplete;
769
- if (!this.isConnected || (!this.inView && this.releaseOffscreen)) return;
817
+ if (!this.isConnected || (!this.inView && this.wantsReleaseOffscreen()))
818
+ return;
770
819
 
771
820
  const canvas = this.getCanvas();
772
821
  if (!canvas) return;
@@ -775,6 +824,9 @@ export class SonicShader extends LitElement {
775
824
  this.errorMessage = "";
776
825
  this.runtime = new ShaderToyRuntime(canvas);
777
826
  this.runtime.setElementResolver((key) => this.lookupElement(key));
827
+ this.activeBufferFormat = this.runtime.setBufferFormat(
828
+ parseBufferFormat(this.bufferFormat),
829
+ );
778
830
  this.bindContextEvents();
779
831
  this.syncSize();
780
832
  await this.rebuild();
@@ -784,7 +836,7 @@ export class SonicShader extends LitElement {
784
836
  }
785
837
 
786
838
  private syncVisibility() {
787
- if (!this.inView && this.releaseOffscreen) {
839
+ if (!this.inView && this.wantsReleaseOffscreen()) {
788
840
  this.releaseRuntime();
789
841
  return;
790
842
  }
@@ -871,8 +923,72 @@ export class SonicShader extends LitElement {
871
923
  return [...new Set(keys)];
872
924
  }
873
925
 
926
+ /** Enregistre les producteurs live (`sonic-3d`…) pour play-offscreen auto. */
927
+ private syncFrameConsumers(channels: Map<ShaderPassId, PassChannels>) {
928
+ const els = this.collectElementKeys(channels).map((k) =>
929
+ this.lookupElement(k),
930
+ );
931
+ this.frameConsumers.sync(els);
932
+ }
933
+
934
+ /** Map canaux courante (tous les passes). */
935
+ private collectPassChannels(): Map<ShaderPassId, PassChannels> {
936
+ const channels = new Map<ShaderPassId, PassChannels>();
937
+ for (const pass of [
938
+ "bufferA",
939
+ "bufferB",
940
+ "bufferC",
941
+ "bufferD",
942
+ "image",
943
+ ] as ShaderPassId[]) {
944
+ channels.set(pass, this.buildChannels(pass));
945
+ }
946
+ return channels;
947
+ }
948
+
949
+ /** `true` si hors viewport on doit libérer textures/FBO. */
950
+ private wantsReleaseOffscreen(): boolean {
951
+ return shouldReleaseWhenOffscreen(
952
+ this.releaseOffscreen,
953
+ channelsUseSelf(this.collectPassChannels().values()),
954
+ );
955
+ }
956
+
957
+ /**
958
+ * Re-lie les canaux sans recompiler ni vider les FBO (Vague A Lot 2).
959
+ */
960
+ private async rebindChannels() {
961
+ if (!this.runtime || this.runtime.isLost || !this.image.trim()) return;
962
+ if (!this.ready) {
963
+ void this.rebuild();
964
+ return;
965
+ }
966
+ const token = ++this.rebindToken;
967
+ const channels = this.collectPassChannels();
968
+ try {
969
+ const urls = this.collectUrls(channels);
970
+ await this.runtime.ensureTextures(urls);
971
+ for (const key of this.collectElementKeys(channels)) {
972
+ this.runtime.ensureElementTexture(key);
973
+ }
974
+ if (token !== this.rebindToken || !this.runtime || this.runtime.isLost) {
975
+ return;
976
+ }
977
+ this.syncFrameConsumers(channels);
978
+ const aspectHint =
979
+ this.channel0.startsWith("#") ? "" : this.channel0 || urls[0];
980
+ this.applyAspectRatio(aspectHint);
981
+ this.runtime.setPassChannels(channels);
982
+ this.syncSize();
983
+ } catch (err) {
984
+ if (token !== this.rebindToken) return;
985
+ this.emitError(err);
986
+ }
987
+ }
988
+
874
989
  private async rebuild() {
875
990
  if (!this.runtime || this.runtime.isLost || !this.image.trim()) return;
991
+ this.rebindToken++;
876
992
  const token = ++this.rebuildToken;
877
993
  this.errorMessage = "";
878
994
  const sources: ShaderSources = {
@@ -883,18 +999,12 @@ export class SonicShader extends LitElement {
883
999
  bufferC: this.bufferC,
884
1000
  bufferD: this.bufferD,
885
1001
  };
886
- const channels = new Map<ShaderPassId, PassChannels>();
887
- for (const pass of [
888
- "bufferA",
889
- "bufferB",
890
- "bufferC",
891
- "bufferD",
892
- "image",
893
- ] as ShaderPassId[]) {
894
- channels.set(pass, this.buildChannels(pass));
895
- }
1002
+ const channels = this.collectPassChannels();
896
1003
  try {
897
1004
  this.syncSize();
1005
+ this.activeBufferFormat = this.runtime.setBufferFormat(
1006
+ parseBufferFormat(this.bufferFormat),
1007
+ );
898
1008
  const urls = this.collectUrls(channels);
899
1009
  await this.runtime.ensureTextures(urls);
900
1010
  for (const key of this.collectElementKeys(channels)) {
@@ -903,6 +1013,7 @@ export class SonicShader extends LitElement {
903
1013
  if (token !== this.rebuildToken || !this.runtime || this.runtime.isLost) {
904
1014
  return;
905
1015
  }
1016
+ this.syncFrameConsumers(channels);
906
1017
  const aspectHint =
907
1018
  this.channel0.startsWith("#") ? "" : this.channel0 || urls[0];
908
1019
  this.applyAspectRatio(aspectHint);
@@ -1312,8 +1423,8 @@ export class SonicShader extends LitElement {
1312
1423
  : this.runtime.readBufferRect(pass, px.x, px.y, px.w, px.h);
1313
1424
  if (!rect) continue;
1314
1425
  // Copie : le buffer WebGL peut être réutilisé / invalidé ensuite
1315
- const rgba = new Uint8Array(rect.data);
1316
- const mean = meanRgba(rgba);
1426
+ const rgba = rgbaToUint8(rect.data);
1427
+ const mean = meanRgba(rect.data);
1317
1428
  const item: ShaderExtractItem = {
1318
1429
  id: roi.id || `${source}-${px.x}-${px.y}`,
1319
1430
  x: this.round3(rect.x / bufW),
@@ -1410,7 +1521,11 @@ export class SonicShader extends LitElement {
1410
1521
  if (typeof score === "number") item.score = this.round3(score);
1411
1522
  if (this.extractBitmaps) {
1412
1523
  const token = this.rebuildToken;
1413
- const url = await rgbaToBlobUrl(rect.data, rect.width, rect.height);
1524
+ const url = await rgbaToBlobUrl(
1525
+ rgbaToUint8(rect.data),
1526
+ rect.width,
1527
+ rect.height,
1528
+ );
1414
1529
  if (token !== this.rebuildToken || !this.isConnected) {
1415
1530
  if (url) revokeMediaUrls([url]);
1416
1531
  return null;
@@ -1581,10 +1696,12 @@ export class SonicShader extends LitElement {
1581
1696
  }
1582
1697
  }
1583
1698
 
1584
- export { parseChannelSource, ShaderToyRuntime, shouldUploadElementFrame } from "./gl-runtime";
1699
+ export { parseChannelSource, ShaderToyRuntime, shouldUploadElementFrame, resolveBufferGlFormat, resetBufferFormatWarnFlag } from "./gl-runtime";
1585
1700
  export { buildFragmentSource } from "./shadertoy-prelude";
1586
1701
  export type {
1587
1702
  ChannelSource,
1703
+ ReleaseOffscreen,
1704
+ ShaderBufferFormat,
1588
1705
  ShaderExtractItem,
1589
1706
  ShaderExtractOutput,
1590
1707
  ShaderExtractPass,
@@ -1609,11 +1726,16 @@ export {
1609
1726
  mediaElementKey,
1610
1727
  } from "@supersoniks/concorde/core/utils/mediaRef";
1611
1728
  export {
1729
+ channelsUseSelf,
1612
1730
  findPeaks,
1613
1731
  meanRgba,
1732
+ parseBufferFormat,
1614
1733
  parseExtractPass,
1615
1734
  parseHoverParam,
1735
+ parseReleaseOffscreen,
1616
1736
  roiToPixels,
1737
+ rgbaToUint8,
1738
+ shouldReleaseWhenOffscreen,
1617
1739
  ShaderCompileError,
1618
1740
  } from "./types";
1619
1741