@flighthq/render-wgpu 0.5.1-next.1069.554a2ac → 0.5.1-next.1201.9b49dee

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 (105) hide show
  1. package/README.md +2 -2
  2. package/dist/contract.d.ts +9 -6
  3. package/dist/contract.d.ts.map +1 -1
  4. package/dist/contract.js +9 -6
  5. package/dist/contract.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/wgpuDraw.js +1 -1
  11. package/dist/wgpuDraw.js.map +1 -1
  12. package/dist/wgpuFrame.d.ts +23 -0
  13. package/dist/wgpuFrame.d.ts.map +1 -0
  14. package/dist/{wgpuBackground.js → wgpuFrame.js} +67 -121
  15. package/dist/wgpuFrame.js.map +1 -0
  16. package/dist/wgpuFullscreenPass.d.ts +2 -2
  17. package/dist/wgpuFullscreenPass.d.ts.map +1 -1
  18. package/dist/wgpuFullscreenPass.js +1 -2
  19. package/dist/wgpuFullscreenPass.js.map +1 -1
  20. package/dist/wgpuHost.d.ts +6 -6
  21. package/dist/wgpuHost.d.ts.map +1 -1
  22. package/dist/wgpuHost.js +17 -3
  23. package/dist/wgpuHost.js.map +1 -1
  24. package/dist/wgpuRenderPass.d.ts +9 -0
  25. package/dist/wgpuRenderPass.d.ts.map +1 -0
  26. package/dist/wgpuRenderPass.js +222 -0
  27. package/dist/wgpuRenderPass.js.map +1 -0
  28. package/dist/wgpuRenderState.d.ts +3 -4
  29. package/dist/wgpuRenderState.d.ts.map +1 -1
  30. package/dist/wgpuRenderState.js +17 -88
  31. package/dist/wgpuRenderState.js.map +1 -1
  32. package/dist/wgpuRenderTarget.d.ts +12 -29
  33. package/dist/wgpuRenderTarget.d.ts.map +1 -1
  34. package/dist/wgpuRenderTarget.js +19 -252
  35. package/dist/wgpuRenderTarget.js.map +1 -1
  36. package/dist/wgpuRenderTargetPool.d.ts +5 -5
  37. package/dist/wgpuRenderTargetPool.d.ts.map +1 -1
  38. package/dist/wgpuRenderTargetPool.js +7 -7
  39. package/dist/wgpuRenderTargetPool.js.map +1 -1
  40. package/dist/wgpuRenderTexture.d.ts +5 -4
  41. package/dist/wgpuRenderTexture.d.ts.map +1 -1
  42. package/dist/wgpuRenderTexture.js +14 -16
  43. package/dist/wgpuRenderTexture.js.map +1 -1
  44. package/dist/wgpuRenderTexturePool.js +1 -3
  45. package/dist/wgpuRenderTexturePool.js.map +1 -1
  46. package/dist/wgpuScissor.d.ts +1 -1
  47. package/dist/wgpuScissor.d.ts.map +1 -1
  48. package/dist/wgpuScissor.js +7 -4
  49. package/dist/wgpuScissor.js.map +1 -1
  50. package/dist/wgpuScreenAntialias.d.ts +3 -0
  51. package/dist/wgpuScreenAntialias.d.ts.map +1 -0
  52. package/dist/wgpuScreenAntialias.js +119 -0
  53. package/dist/wgpuScreenAntialias.js.map +1 -0
  54. package/dist/wgpuScreenCapture.d.ts +5 -0
  55. package/dist/wgpuScreenCapture.d.ts.map +1 -0
  56. package/dist/{wgpuSurface.js → wgpuScreenCapture.js} +55 -73
  57. package/dist/wgpuScreenCapture.js.map +1 -0
  58. package/dist/wgpuScreenRenderTarget.d.ts +9 -0
  59. package/dist/wgpuScreenRenderTarget.d.ts.map +1 -0
  60. package/dist/wgpuScreenRenderTarget.js +123 -0
  61. package/dist/wgpuScreenRenderTarget.js.map +1 -0
  62. package/dist/wgpuShader.js +2 -2
  63. package/dist/wgpuShader.js.map +1 -1
  64. package/dist/wgpuTestHelper.d.ts +8 -2
  65. package/dist/wgpuTestHelper.d.ts.map +1 -1
  66. package/dist/wgpuTestHelper.js +38 -3
  67. package/dist/wgpuTestHelper.js.map +1 -1
  68. package/dist/wgpuTextureRenderTarget.d.ts +18 -0
  69. package/dist/wgpuTextureRenderTarget.d.ts.map +1 -0
  70. package/dist/wgpuTextureRenderTarget.js +122 -0
  71. package/dist/wgpuTextureRenderTarget.js.map +1 -0
  72. package/package.json +11 -11
  73. package/src/wgpuDraw.test.ts +8 -8
  74. package/src/{wgpuBackground.test.ts → wgpuFrame.test.ts} +34 -20
  75. package/src/wgpuFullscreenPass.test.ts +7 -7
  76. package/src/wgpuHost.test.ts +28 -82
  77. package/src/wgpuRenderPass.test.ts +258 -0
  78. package/src/wgpuRenderState.test.ts +34 -313
  79. package/src/wgpuRenderTarget.test.ts +44 -219
  80. package/src/wgpuRenderTargetPool.test.ts +21 -21
  81. package/src/wgpuRenderTexture.test.ts +17 -15
  82. package/src/wgpuScissor.test.ts +43 -13
  83. package/src/wgpuScreenAntialias.test.ts +60 -0
  84. package/src/wgpuScreenCapture.test.ts +156 -0
  85. package/src/wgpuScreenRenderTarget.test.ts +152 -0
  86. package/src/wgpuShader.test.ts +5 -6
  87. package/src/wgpuTextureRenderTarget.test.ts +129 -0
  88. package/src/wgpuTextureResolver.test.ts +5 -5
  89. package/dist/wgpuAntialias.d.ts +0 -14
  90. package/dist/wgpuAntialias.d.ts.map +0 -1
  91. package/dist/wgpuAntialias.js +0 -155
  92. package/dist/wgpuAntialias.js.map +0 -1
  93. package/dist/wgpuBackground.d.ts +0 -20
  94. package/dist/wgpuBackground.d.ts.map +0 -1
  95. package/dist/wgpuBackground.js.map +0 -1
  96. package/dist/wgpuElement.d.ts +0 -7
  97. package/dist/wgpuElement.d.ts.map +0 -1
  98. package/dist/wgpuElement.js +0 -20
  99. package/dist/wgpuElement.js.map +0 -1
  100. package/dist/wgpuSurface.d.ts +0 -6
  101. package/dist/wgpuSurface.d.ts.map +0 -1
  102. package/dist/wgpuSurface.js.map +0 -1
  103. package/src/wgpuAntialias.test.ts +0 -239
  104. package/src/wgpuElement.test.ts +0 -145
  105. package/src/wgpuSurface.test.ts +0 -141
@@ -22,27 +22,23 @@ import type {
22
22
  WgpuColorAdjustmentMaterialFeature,
23
23
  WgpuColorAdjustmentMaterialFeatureGuard,
24
24
  WgpuHostAcquisition,
25
- WgpuHostBackend,
25
+ HostWgpuProvider,
26
26
  WgpuPipeline,
27
- WgpuPresentationRenderState,
28
- WgpuPresentationSurface,
29
27
  WgpuRenderOptions,
30
28
  WgpuRenderState,
31
29
  } from '@flighthq/types/contract';
32
30
  import { EntityRuntimeKey, RegistryEntryState } from '@flighthq/types/contract';
33
31
 
34
- import { getWgpuSurfaceRenderExtent } from './wgpuAntialias';
35
- import { beginWgpuFrame, withWgpuFrameBorrow } from './wgpuBackground';
36
32
  import { registerWgpuCompressedTextureDecoder, registerWgpuCompressedTextureUpload } from './wgpuCompressedTexture';
33
+ import { beginWgpuFrame, withWgpuFrameBorrow } from './wgpuFrame';
37
34
  import { setWgpuHostBackend } from './wgpuHost';
38
35
  import { registerWgpuMaterialRenderer } from './wgpuMaterialRegistry';
39
36
  import { createEmptyWgpuRegistries, createWgpuPipeline } from './wgpuPipeline';
40
37
  import {
41
- createWgpuAcquisitionFromCanvasElement,
38
+ createWgpuAcquisition,
42
39
  createWgpuDeviceState,
43
40
  createWgpuOffscreenRenderState as createDeviceOnlyWgpuRenderState,
44
41
  createWgpuRenderState as createWgpuRenderStateWithPipeline,
45
- createWgpuRenderStateFromCanvasElement as createWgpuRenderStateFromCanvasElementWithPipeline,
46
42
  createWgpuRenderStateRuntime as createWgpuRenderStateRuntimeWithPipeline,
47
43
  destroyWgpuRenderState,
48
44
  getWgpuColorAdjustmentMaterialFeature,
@@ -77,22 +73,15 @@ beforeAll(() => {
77
73
 
78
74
  const _testPipeline = createWgpuPipeline(createEmptyWgpuRegistries());
79
75
 
80
- function createWgpuRenderState(acquisition: Readonly<WgpuHostAcquisition>, options: Readonly<WgpuRenderOptions> = {}) {
81
- return createWgpuRenderStateWithPipeline(acquisition, _testPipeline, options);
82
- }
83
-
84
- function createWgpuRenderStateFromCanvasElement(
85
- canvas: HTMLCanvasElement,
86
- options: Readonly<WgpuRenderOptions & { powerPreference?: GPUPowerPreference }> = {},
87
- ) {
88
- return createWgpuRenderStateFromCanvasElementWithPipeline(canvas, _testPipeline, options);
76
+ function createWgpuRenderState(device: GPUDevice, options: Readonly<WgpuRenderOptions> = {}) {
77
+ return createWgpuRenderStateWithPipeline(device, _testPipeline, options);
89
78
  }
90
79
 
91
80
  function createWgpuRenderStateRuntime(deviceState: ReturnType<typeof createWgpuDeviceState>) {
92
81
  return createWgpuRenderStateRuntimeWithPipeline(deviceState, _testPipeline);
93
82
  }
94
83
 
95
- function entityHostBackend(fields: Omit<WgpuHostBackend, keyof Entity>): WgpuHostBackend {
84
+ function entityHostBackend(fields: Omit<HostWgpuProvider, keyof Entity>): HostWgpuProvider {
96
85
  return (() => {
97
86
  const out = allocateEntity<any>();
98
87
  Object.assign(out, fields);
@@ -103,7 +92,6 @@ function entityHostBackend(fields: Omit<WgpuHostBackend, keyof Entity>): WgpuHos
103
92
  function createWgpuOffscreenRenderState(source: WgpuRenderState): WgpuRenderState {
104
93
  const pipeline: WgpuPipeline = createWgpuPipeline(getWgpuRenderStateRuntime(source).registries);
105
94
  const state = createDeviceOnlyWgpuRenderState(source.deviceState, pipeline, {
106
- backgroundColor: source.backgroundColor,
107
95
  format: source.format,
108
96
  imageSmoothingEnabled: source.allowSmoothing,
109
97
  pixelRatio: source.pixelRatio,
@@ -136,9 +124,9 @@ function registerPaddingResolver(state: RenderState, kind: string, resolver: Ren
136
124
  );
137
125
  }
138
126
 
139
- describe('createWgpuAcquisitionFromCanvasElement', () => {
127
+ describe('createWgpuAcquisition', () => {
140
128
  it('hands back handles the CALLER owns, so no state teardown can release them', async () => {
141
- const acquisition = await createWgpuAcquisitionFromCanvasElement(document.createElement('canvas'));
129
+ const acquisition = await createWgpuAcquisition(document.createElement('canvas'));
142
130
 
143
131
  expect(acquisition).not.toBeNull();
144
132
  expect(acquisition!.ownership).toBe('caller');
@@ -154,12 +142,13 @@ describe('createWgpuAcquisitionFromCanvasElement', () => {
154
142
  acquire: vi.fn(async () => {
155
143
  throw new Error('no adapter');
156
144
  }),
145
+ attachSurface: vi.fn(() => null),
157
146
  isSupported: vi.fn(() => false),
158
147
  release: vi.fn(),
159
148
  }),
160
149
  );
161
150
 
162
- await expect(createWgpuAcquisitionFromCanvasElement(document.createElement('canvas'))).resolves.toBeNull();
151
+ await expect(createWgpuAcquisition(document.createElement('canvas'))).resolves.toBeNull();
163
152
  setWgpuHostBackend(null);
164
153
  });
165
154
  });
@@ -195,14 +184,6 @@ describe('createWgpuDeviceState', () => {
195
184
  });
196
185
 
197
186
  describe('createWgpuOffscreenRenderState', () => {
198
- it('rejects a device-only state at presentation boundaries', () => {
199
- const deviceOnlyState = null as WgpuRenderState | null;
200
- if (deviceOnlyState === null) return;
201
-
202
- // @ts-expect-error — a device-only state has no presentation surface or canvas context.
203
- beginWgpuFrame(deviceOnlyState);
204
- });
205
-
206
187
  it('resolves late screen blend-mode wiring explicitly until locally overridden', async () => {
207
188
  const screen = await createWgpuRenderStateForTest();
208
189
  const offscreen = createWgpuOffscreenRenderState(screen);
@@ -382,7 +363,6 @@ describe('createWgpuOffscreenRenderState', () => {
382
363
 
383
364
  withWgpuFrameBorrow(screen, offscreen, () => {
384
365
  expect(offscreenRuntime.commandEncoder).toBe(encoder);
385
- expect(getWgpuSurfaceRenderExtent(offscreen)).toEqual({ height: 600, width: 800 });
386
366
  offscreenRuntime.uniformOffset = offscreenRuntime.uniformStride;
387
367
  });
388
368
 
@@ -403,7 +383,6 @@ describe('createWgpuOffscreenRenderState', () => {
403
383
  expect.anything(),
404
384
  );
405
385
  expect(submit).not.toHaveBeenCalled();
406
- expect(() => getWgpuSurfaceRenderExtent(offscreen)).toThrow(/outside an explicit frame borrow/);
407
386
  });
408
387
 
409
388
  it('submits a standalone borrow and restores both states when the callback throws', async () => {
@@ -421,7 +400,6 @@ describe('createWgpuOffscreenRenderState', () => {
421
400
  expect(submit).toHaveBeenCalledOnce();
422
401
  expect(getWgpuRenderStateRuntime(screen).commandEncoder).toBeNull();
423
402
  expect(getWgpuRenderStateRuntime(offscreen).commandEncoder).toBeNull();
424
- expect(() => getWgpuSurfaceRenderExtent(offscreen)).toThrow(/outside an explicit frame borrow/);
425
403
  });
426
404
 
427
405
  it('rejects nested borrowing by the same offscreen state', async () => {
@@ -474,73 +452,20 @@ describe('createWgpuOffscreenRenderState', () => {
474
452
  });
475
453
 
476
454
  describe('createWgpuRenderState', () => {
477
- it('keeps exact caller-owned handles usable through every shared-state teardown', async () => {
478
- const owner = await createWgpuRenderStateForTest();
479
- const canvas = document.createElement('canvas');
480
- const _acqOut = allocateEntity<WgpuHostAcquisition>();
481
- _acqOut.context = owner.context;
482
- _acqOut.device = owner.device;
483
- _acqOut.format = owner.format;
484
- _acqOut.ownership = 'caller' as const;
485
- _acqOut.surface = owner.surface;
486
- const acquisition = finishEntity(_acqOut);
487
- let contextUsable = true;
488
- let deviceUsable = true;
489
- const originalCreateBuffer = acquisition.device.createBuffer.bind(acquisition.device);
490
- const originalGetCurrentTexture = acquisition.context.getCurrentTexture.bind(acquisition.context);
491
- const createBuffer = vi.spyOn(acquisition.device, 'createBuffer').mockImplementation((descriptor) => {
492
- if (!deviceUsable) throw new Error('caller device was destroyed');
493
- return originalCreateBuffer(descriptor);
494
- });
495
- const getCurrentTexture = vi.spyOn(acquisition.context, 'getCurrentTexture').mockImplementation(() => {
496
- if (!contextUsable) throw new Error('caller context was unconfigured');
497
- return originalGetCurrentTexture();
498
- });
499
- const unconfigure = vi.spyOn(acquisition.context, 'unconfigure').mockImplementation(() => {
500
- contextUsable = false;
501
- });
502
- const destroy = vi.spyOn(acquisition.device, 'destroy').mockImplementation(() => {
503
- deviceUsable = false;
504
- });
505
- Object.defineProperty(globalThis.navigator, 'gpu', {
506
- configurable: true,
507
- get(): never {
508
- throw new Error('host getter failed');
509
- },
510
- });
511
-
512
- try {
513
- const state = createWgpuRenderState(acquisition);
514
- const offscreen = createWgpuOffscreenRenderState(state);
515
- expect(state.context).toBe(acquisition.context);
516
- expect(state.device).toBe(acquisition.device);
517
- expect(state.format).toBe(acquisition.format);
518
- expect('context' in offscreen).toBe(false);
519
- expect(offscreen.device).toBe(acquisition.device);
520
- expect(offscreen.format).toBe(acquisition.format);
521
- const createBufferCallsBeforeTeardown = createBuffer.mock.calls.length;
522
- const getCurrentTextureCallsBeforeTeardown = getCurrentTexture.mock.calls.length;
523
- destroyWgpuRenderState(state);
524
- destroyWgpuRenderState(offscreen);
525
- expect(unconfigure).not.toHaveBeenCalled();
526
- expect(destroy).not.toHaveBeenCalled();
527
-
528
- const buffer = acquisition.device.createBuffer({ size: 4, usage: GPUBufferUsage.COPY_DST });
529
- expect(buffer.size).toBe(4);
530
- expect(acquisition.context.getCurrentTexture().createView()).toBeDefined();
531
- expect(createBuffer).toHaveBeenCalledTimes(createBufferCallsBeforeTeardown + 1);
532
- expect(getCurrentTexture).toHaveBeenCalledTimes(getCurrentTextureCallsBeforeTeardown + 1);
533
- buffer.destroy();
534
- } finally {
535
- installWgpuMock();
536
- destroyWgpuRenderState(owner);
537
- }
538
- });
455
+ it('takes the device alone: no surface, no context, nothing that belongs to a screen', async () => {
456
+ // The state is "how to talk to the GPU" and the screen target is "which surface a frame lands on".
457
+ // Keeping them apart is what lets one state render to several windows, and lets an offscreen state
458
+ // exist without inventing a canvas for it.
459
+ const acquisition = await createWgpuAcquisition(document.createElement('canvas'));
460
+ const state = createWgpuRenderState(acquisition!.device, { format: acquisition!.format });
461
+
462
+ expect(state.device).toBe(acquisition!.device);
463
+ expect(state.format).toBe(acquisition!.format);
464
+ expect('context' in state).toBe(false);
465
+ expect('surface' in state).toBe(false);
539
466
 
540
- it('returns a render state with device and context', async () => {
541
- const state = await createWgpuRenderStateForTest();
542
- expect(state.device).toBeDefined();
543
- expect(state.context).toBeDefined();
467
+ destroyWgpuRenderState(state);
468
+ releaseWgpuAcquisition(acquisition!);
544
469
  });
545
470
 
546
471
  it('sets allowSmoothing to true by default', async () => {
@@ -556,60 +481,13 @@ describe('createWgpuRenderState', () => {
556
481
  expect(runtime.uniformOffset).toBe(0);
557
482
  });
558
483
 
559
- // C5: the state exposes the surface as SIZE ONLY. Asserting `instanceof HTMLCanvasElement` here would
560
- // re-impose the DOM dependency the surface type exists to remove — that the web backend's surface happens
561
- // to be a canvas is a property of that backend, pinned in wgpuHost.test.ts, not of the state contract.
562
- it('exposes the presentation surface as live size, carrying no DOM member', async () => {
563
- const state = await createWgpuRenderStateForTest();
564
- expect(typeof state.surface.width).toBe('number');
565
- expect(typeof state.surface.height).toBe('number');
566
- expect(Object.keys(state.surface as object).filter((key) => key !== 'width' && key !== 'height')).toEqual([]);
567
- });
568
-
569
- it('starts with null renderPass and commandEncoder', async () => {
484
+ it('starts with no open frame, pass, or target', async () => {
570
485
  const state = await createWgpuRenderStateForTest();
571
486
  const runtime = getWgpuRenderStateRuntime(state);
572
487
  expect(runtime.renderPass).toBeNull();
573
488
  expect(runtime.commandEncoder).toBeNull();
574
- });
575
- });
576
-
577
- describe('createWgpuRenderStateFromCanvasElement', () => {
578
- it('acquires flight-owned handles and releases them when the state is destroyed', async () => {
579
- const owner = await createWgpuRenderStateForTest();
580
- const acquisition = { ...ownerAcquisition(owner), ownership: 'flight' } as const;
581
- const released: Readonly<WgpuHostAcquisition>[] = [];
582
- setWgpuHostBackend(
583
- entityHostBackend({
584
- acquire: vi.fn(async () => acquisition),
585
- isSupported: vi.fn(() => true),
586
- release: vi.fn((held: Readonly<WgpuHostAcquisition>) => released.push(held)),
587
- }),
588
- );
589
-
590
- const state = await createWgpuRenderStateFromCanvasElement(document.createElement('canvas'));
591
- destroyWgpuRenderState(state);
592
-
593
- expect(released).toEqual([acquisition]);
594
- setWgpuHostBackend(null);
595
- destroyWgpuRenderState(owner);
596
- });
597
-
598
- it('forwards the acquisition options, which the render options no longer carry', async () => {
599
- const owner = await createWgpuRenderStateForTest();
600
- const acquire = vi.fn(async () => ({ ...ownerAcquisition(owner), ownership: 'flight' }) as const);
601
- setWgpuHostBackend(entityHostBackend({ acquire, isSupported: vi.fn(() => true), release: vi.fn() }));
602
- const canvas = document.createElement('canvas');
603
-
604
- const state = await createWgpuRenderStateFromCanvasElement(canvas, {
605
- format: 'rgba8unorm',
606
- powerPreference: 'low-power',
607
- });
608
-
609
- expect(acquire).toHaveBeenCalledWith(canvas, { format: 'rgba8unorm', powerPreference: 'low-power' });
610
- destroyWgpuRenderState(state);
611
- setWgpuHostBackend(null);
612
- destroyWgpuRenderState(owner);
489
+ expect(runtime.passStack).toEqual([]);
490
+ expect(runtime.currentRenderTarget).toBeNull();
613
491
  });
614
492
  });
615
493
 
@@ -704,21 +582,18 @@ describe('createWgpuRenderStateRuntime', () => {
704
582
  });
705
583
 
706
584
  describe('destroyWgpuRenderState', () => {
707
- it('releases a Flight-owned acquisition once after its last derived state', async () => {
585
+ it('never destroys the device, however many of its states are torn down', async () => {
586
+ // The device came from the caller and outlives every state built on it — including the last one.
708
587
  const state = await createWgpuRenderStateForTest();
709
588
  const offscreen = createWgpuOffscreenRenderState(state);
710
- const unconfigure = vi.spyOn(state.context, 'unconfigure');
711
589
  const destroy = vi.spyOn(state.device, 'destroy');
712
590
 
713
591
  destroyWgpuRenderState(state);
714
592
  destroyWgpuRenderState(state);
715
- expect(unconfigure).not.toHaveBeenCalled();
716
- expect(destroy).not.toHaveBeenCalled();
717
-
718
593
  destroyWgpuRenderState(offscreen);
719
594
  destroyWgpuRenderState(offscreen);
720
- expect(unconfigure).toHaveBeenCalledOnce();
721
- expect(destroy).toHaveBeenCalledOnce();
595
+
596
+ expect(destroy).not.toHaveBeenCalled();
722
597
  });
723
598
 
724
599
  it('destroys the state-owned uniform buffer', async () => {
@@ -992,15 +867,6 @@ describe('registerWgpuDeviceTeardown', () => {
992
867
  });
993
868
  });
994
869
 
995
- function ownerAcquisition(owner: WgpuPresentationRenderState): Omit<WgpuHostAcquisition, 'ownership'> {
996
- const out = allocateEntity<any>();
997
- out.context = owner.context;
998
- out.device = owner.device;
999
- out.format = owner.format;
1000
- out.surface = owner.surface;
1001
- return finishEntity(out);
1002
- }
1003
-
1004
870
  describe('registerWgpuRenderStateTeardown', () => {
1005
871
  it('runs a state-owned callback exactly once on teardown', async () => {
1006
872
  const state = await createWgpuRenderStateForTest();
@@ -1019,12 +885,13 @@ describe('releaseWgpuAcquisition', () => {
1019
885
  // Unconditional on purpose: this is the CALLER asking. Flight's own paths refuse to release caller-owned
1020
886
  // handles, so if this verb deferred to the same policy the caller would have no way to end their life.
1021
887
  it('releases caller-owned handles, which Flight itself never does', async () => {
1022
- const owner = await createWgpuRenderStateForTest();
1023
- const acquisition = { ...ownerAcquisition(owner), ownership: 'caller' } as const;
888
+ const acquired = await createWgpuAcquisition(document.createElement('canvas'));
889
+ const acquisition = acquired!;
1024
890
  const released: Readonly<WgpuHostAcquisition>[] = [];
1025
891
  setWgpuHostBackend(
1026
892
  entityHostBackend({
1027
893
  acquire: vi.fn(async () => acquisition),
894
+ attachSurface: vi.fn(() => null),
1028
895
  isSupported: vi.fn(() => true),
1029
896
  release: vi.fn((held: Readonly<WgpuHostAcquisition>) => released.push(held)),
1030
897
  }),
@@ -1034,9 +901,9 @@ describe('releaseWgpuAcquisition', () => {
1034
901
 
1035
902
  expect(released).toEqual([acquisition]);
1036
903
  setWgpuHostBackend(null);
1037
- destroyWgpuRenderState(owner);
1038
904
  });
1039
905
  });
906
+
1040
907
  describe('resolveWgpuApplyBlendMode', () => {
1041
908
  it('returns a hook installed directly on the state', async () => {
1042
909
  const state = await createWgpuRenderStateForTest();
@@ -1046,104 +913,6 @@ describe('resolveWgpuApplyBlendMode', () => {
1046
913
  });
1047
914
  });
1048
915
 
1049
- // ★ OWNERSHIP LIFECYCLE. Flight decides whether an acquisition is released; the backend only carries out
1050
- // the teardown. Each backend here would really destroy the handles if asked, so "not released" is a fact
1051
- // about Flight's decision rather than about a spy that could not have destroyed anything.
1052
- describe('wgpu acquisition lifecycle', () => {
1053
- const destroyingBackend = (
1054
- acquisition: Readonly<WgpuHostAcquisition>,
1055
- ): { backend: WgpuHostBackend; released: Readonly<WgpuHostAcquisition>[] } => {
1056
- const released: Readonly<WgpuHostAcquisition>[] = [];
1057
- return {
1058
- backend: entityHostBackend({
1059
- acquire: vi.fn(async () => acquisition),
1060
- isSupported: vi.fn(() => true),
1061
- release: vi.fn((held: Readonly<WgpuHostAcquisition>) => {
1062
- released.push(held);
1063
- held.device.destroy();
1064
- }),
1065
- }),
1066
- released,
1067
- };
1068
- };
1069
-
1070
- it('L1: releases a flight-owned acquisition exactly once, after the last sharer, in either order', async () => {
1071
- for (const destroyOffscreenFirst of [false, true]) {
1072
- const owner = await createWgpuRenderStateForTest();
1073
- const acquisition = { ...ownerAcquisition(owner), ownership: 'flight' } as const;
1074
- const { backend, released } = destroyingBackend(acquisition);
1075
- setWgpuHostBackend(backend);
1076
-
1077
- const state = createWgpuRenderState(acquisition);
1078
- const offscreen = createWgpuOffscreenRenderState(state);
1079
- const first = destroyOffscreenFirst ? offscreen : state;
1080
- const second = destroyOffscreenFirst ? state : offscreen;
1081
-
1082
- destroyWgpuRenderState(first);
1083
- expect(released, 'released before the last sharer was destroyed').toEqual([]);
1084
- destroyWgpuRenderState(second);
1085
- expect(released).toEqual([acquisition]);
1086
-
1087
- setWgpuHostBackend(null);
1088
- destroyWgpuRenderState(owner);
1089
- }
1090
- });
1091
-
1092
- it('L2: never releases a caller-owned acquisition, in either destroy order', async () => {
1093
- for (const destroyOffscreenFirst of [false, true]) {
1094
- const owner = await createWgpuRenderStateForTest();
1095
- const acquisition = { ...ownerAcquisition(owner), ownership: 'caller' } as const;
1096
- const { backend, released } = destroyingBackend(acquisition);
1097
- setWgpuHostBackend(backend);
1098
-
1099
- const state = createWgpuRenderState(acquisition);
1100
- const offscreen = createWgpuOffscreenRenderState(state);
1101
- destroyWgpuRenderState(destroyOffscreenFirst ? offscreen : state);
1102
- destroyWgpuRenderState(destroyOffscreenFirst ? state : offscreen);
1103
-
1104
- expect(released).toEqual([]);
1105
- // L5: the handles still work, which is what the caller actually depends on.
1106
- expect(acquisition.device.createBuffer({ size: 4, usage: GPUBufferUsage.COPY_DST }).size).toBe(4);
1107
-
1108
- setWgpuHostBackend(null);
1109
- destroyWgpuRenderState(owner);
1110
- }
1111
- });
1112
-
1113
- it('L3: destroying the same state twice releases once', async () => {
1114
- const owner = await createWgpuRenderStateForTest();
1115
- const acquisition = { ...ownerAcquisition(owner), ownership: 'flight' } as const;
1116
- const { backend, released } = destroyingBackend(acquisition);
1117
- setWgpuHostBackend(backend);
1118
-
1119
- const state = createWgpuRenderState(acquisition);
1120
- destroyWgpuRenderState(state);
1121
- destroyWgpuRenderState(state);
1122
-
1123
- expect(released).toEqual([acquisition]);
1124
- setWgpuHostBackend(null);
1125
- destroyWgpuRenderState(owner);
1126
- });
1127
-
1128
- it('L4: routes release to the backend that acquired, not whichever is installed at destroy time', async () => {
1129
- const owner = await createWgpuRenderStateForTest();
1130
- const acquisition = { ...ownerAcquisition(owner), ownership: 'flight' } as const;
1131
- const acquiring = destroyingBackend(acquisition);
1132
- const replacement = destroyingBackend(acquisition);
1133
- setWgpuHostBackend(acquiring.backend);
1134
-
1135
- const state = createWgpuRenderState(acquisition);
1136
- setWgpuHostBackend(replacement.backend);
1137
- destroyWgpuRenderState(state);
1138
-
1139
- expect(acquiring.released).toEqual([acquisition]);
1140
- expect(replacement.released, 'released through the backend installed later').toEqual([]);
1141
-
1142
- setWgpuHostBackend(null);
1143
- destroyWgpuRenderState(owner);
1144
- });
1145
- });
1146
-
1147
916
  describe('WgpuPipeline snapshots', () => {
1148
917
  it('captures late Wgpu registrations only when an explicit pipeline is created', async () => {
1149
918
  const screen = await createWgpuRenderStateForTest();
@@ -1205,51 +974,3 @@ describe('WgpuPipeline snapshots', () => {
1205
974
  // would satisfy every construction-time assertion in this repo and fail only on a resize, so liveness is
1206
975
  // tested here through a NON-DOM provider whose values change behind getters — which also proves the second
1207
976
  // half of the contract, that nothing on the path reads a DOM member.
1208
- describe('WgpuPresentationSurface', () => {
1209
- it('C1/C2: drives every size consumer from a live, non-DOM, size-only provider', async () => {
1210
- const owner = await createWgpuRenderStateForTest();
1211
- let width = 800;
1212
- let height = 600;
1213
- const surface: WgpuPresentationSurface = {
1214
- get height() {
1215
- return height;
1216
- },
1217
- get width() {
1218
- return width;
1219
- },
1220
- };
1221
- const acquisition = (() => {
1222
- const out = allocateEntity<any>();
1223
- out.context = owner.context;
1224
- out.device = owner.device;
1225
- out.format = owner.format;
1226
- out.ownership = 'caller' as const;
1227
- out.surface = surface;
1228
- return finishEntity(out);
1229
- })();
1230
- const canvas = document.createElement('canvas');
1231
-
1232
- const state = createWgpuRenderState(acquisition);
1233
- expect(getWgpuSurfaceRenderExtent(state)).toEqual({ width: 800, height: 600 });
1234
-
1235
- width = 320;
1236
- height = 240;
1237
-
1238
- // Every consumer re-reads: a captured `{ width, height }` would still answer 800x600 here.
1239
- expect(state.surface.width).toBe(320);
1240
- expect(state.surface.height).toBe(240);
1241
- expect(getWgpuSurfaceRenderExtent(state)).toEqual({ width: 320, height: 240 });
1242
-
1243
- destroyWgpuRenderState(state);
1244
- destroyWgpuRenderState(owner);
1245
- });
1246
-
1247
- it('C4: accepts an HTMLCanvasElement structurally, so the web path needs no wrapper', () => {
1248
- const canvas = document.createElement('canvas');
1249
- canvas.width = 128;
1250
- canvas.height = 64;
1251
- const surface: WgpuPresentationSurface = canvas;
1252
- expect(surface.width).toBe(128);
1253
- expect(surface.height).toBe(64);
1254
- });
1255
- });