@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
@@ -0,0 +1,60 @@
1
+ import { beginWgpuRenderPass, endWgpuRenderPass } from './wgpuRenderPass';
2
+ import { enableWgpuScreenRenderTargetAntialias } from './wgpuScreenAntialias';
3
+ import { bindWgpuScreenRenderTarget } from './wgpuScreenRenderTarget';
4
+ import { createWgpuRenderStateForTest, createWgpuScreenRenderTargetForTest, installWgpuMock } from './wgpuTestHelper';
5
+
6
+ beforeAll(() => {
7
+ installWgpuMock();
8
+ });
9
+
10
+ describe('enableWgpuScreenRenderTargetAntialias', () => {
11
+ // ★ THE COST IS OPT-IN, AND THAT IS THE POINT OF THE SLOTS. A screen target that was never asked for
12
+ // antialiasing carries no resolve hook, so the module holding the resolve pipeline and its shader is
13
+ // unreachable from an ordinary frame loop and tree-shakes out of the bundle entirely.
14
+ it('installs the hooks a plain screen target does not carry', async () => {
15
+ const state = await createWgpuRenderStateForTest();
16
+ const screen = createWgpuScreenRenderTargetForTest(state);
17
+
18
+ expect(screen.antialias).toBe(false);
19
+ expect(screen.acquireAntialiasView).toBeNull();
20
+ expect(screen.encodeAntialiasResolve).toBeNull();
21
+
22
+ enableWgpuScreenRenderTargetAntialias(screen);
23
+
24
+ expect(screen.antialias).toBe(true);
25
+ expect(screen.acquireAntialiasView).not.toBeNull();
26
+ expect(screen.encodeAntialiasResolve).not.toBeNull();
27
+ });
28
+
29
+ it('draws into supersample storage twice the surface extent', async () => {
30
+ const state = await createWgpuRenderStateForTest();
31
+ const screen = createWgpuScreenRenderTargetForTest(state, { antialias: true }, 400, 300);
32
+
33
+ const view = bindWgpuScreenRenderTarget(state, screen);
34
+
35
+ expect(screen.width).toBe(800);
36
+ expect(view).toBe(screen.antialiasView);
37
+ expect(view).not.toBe(screen.presentationView);
38
+ expect(screen.antialiasTexture?.width).toBe(800);
39
+ });
40
+
41
+ it('resolves the supersample texture into the presentation view', async () => {
42
+ const state = await createWgpuRenderStateForTest();
43
+ const screen = createWgpuScreenRenderTargetForTest(state, { antialias: true });
44
+ const pass = beginWgpuRenderPass(state, screen, { color: [0, 0, 0, 0] });
45
+ const presentationView = screen.presentationView;
46
+ const encoded: GPURenderPassDescriptor[] = [];
47
+ const encoder = {
48
+ beginRenderPass: (descriptor: GPURenderPassDescriptor) => {
49
+ encoded.push(descriptor);
50
+ return { draw: () => {}, end: () => {}, setBindGroup: () => {}, setPipeline: () => {} };
51
+ },
52
+ } as unknown as GPUCommandEncoder;
53
+
54
+ screen.encodeAntialiasResolve!(state, screen, encoder);
55
+
56
+ expect(encoded.length).toBe(1);
57
+ expect(Array.from(encoded[0]!.colorAttachments)[0]!.view).toBe(presentationView);
58
+ endWgpuRenderPass(pass);
59
+ });
60
+ });
@@ -0,0 +1,156 @@
1
+ import type { WgpuScreenRenderTarget } from '@flighthq/types/contract';
2
+ import { beforeAll, describe, expect, it } from 'vitest';
3
+
4
+ import { beginWgpuRenderPass, endWgpuRenderPass } from './wgpuRenderPass';
5
+ import {
6
+ createBitmapFromWgpuScreenRenderTarget,
7
+ enableWgpuScreenRenderTargetCapture,
8
+ encodeWgpuScreenRenderTargetCapture,
9
+ } from './wgpuScreenCapture';
10
+ import { createWgpuRenderStateForTest, createWgpuScreenRenderTargetForTest, installWgpuMock } from './wgpuTestHelper';
11
+
12
+ beforeAll(() => {
13
+ installWgpuMock();
14
+ });
15
+
16
+ describe('createBitmapFromWgpuScreenRenderTarget', () => {
17
+ it('throws when capture was never enabled (no buffer to read)', async () => {
18
+ const state = await createWgpuRenderStateForTest();
19
+ const screen = createWgpuScreenRenderTargetForTest(state);
20
+ await expect(createBitmapFromWgpuScreenRenderTarget(screen)).rejects.toThrow(/enableWgpuScreenRenderTargetCapture/);
21
+ });
22
+
23
+ it('reads the mapped buffer within the bound', async () => {
24
+ const state = await createWgpuRenderStateForTest();
25
+ const screen = createWgpuScreenRenderTargetForTest(state);
26
+ installCaptureBuffer(screen, () => Promise.resolve());
27
+
28
+ const bitmap = await createBitmapFromWgpuScreenRenderTarget(screen, 1_000);
29
+
30
+ expect(bitmap.width).toBe(1);
31
+ expect(bitmap.height).toBe(1);
32
+ });
33
+
34
+ it('fails by name when the buffer never maps, instead of awaiting forever', async () => {
35
+ // The driver failure this bounds: mapAsync neither resolves nor rejects. Before the bound, the
36
+ // caller sat here until something outside it gave up and reported a stall with no location.
37
+ const state = await createWgpuRenderStateForTest();
38
+ const screen = createWgpuScreenRenderTargetForTest(state);
39
+ installCaptureBuffer(screen, () => new Promise<void>(() => {}));
40
+
41
+ await expect(createBitmapFromWgpuScreenRenderTarget(screen, 25)).rejects.toThrow(/did not map within 25ms/);
42
+ });
43
+
44
+ it('leaves the wait unbounded when the caller passes 0', async () => {
45
+ const state = await createWgpuRenderStateForTest();
46
+ const screen = createWgpuScreenRenderTargetForTest(state);
47
+ let settle = (): void => {};
48
+ installCaptureBuffer(screen, () => new Promise<void>((resolve) => (settle = resolve)));
49
+
50
+ let done = false;
51
+ const pending = createBitmapFromWgpuScreenRenderTarget(screen, 0).then(() => (done = true));
52
+ await new Promise((resolve) => setTimeout(resolve, 40));
53
+ expect(done).toBe(false);
54
+
55
+ settle();
56
+ await pending;
57
+ expect(done).toBe(true);
58
+ });
59
+ });
60
+
61
+ describe('enableWgpuScreenRenderTargetCapture', () => {
62
+ it('sets the capture flag on the screen target', async () => {
63
+ const state = await createWgpuRenderStateForTest();
64
+ const screen = createWgpuScreenRenderTargetForTest(state);
65
+ expect(screen.captureEnabled).toBe(false);
66
+ enableWgpuScreenRenderTargetCapture(screen);
67
+ expect(screen.captureEnabled).toBe(true);
68
+ });
69
+
70
+ it('redirects the frame into an offscreen texture rather than the swap chain', async () => {
71
+ // The whole reason capture exists: software and headless adapters never present the swap chain, and
72
+ // its texture reads back as zeros, so a captured frame must be drawn somewhere readable instead.
73
+ const state = await createWgpuRenderStateForTest();
74
+ const screen = createWgpuScreenRenderTargetForTest(state);
75
+ expect(screen.captureTexture).toBeNull();
76
+
77
+ enableWgpuScreenRenderTargetCapture(screen);
78
+ endWgpuRenderPass(beginWgpuRenderPass(state, screen, { color: [0, 0, 0, 0] }));
79
+
80
+ expect(screen.captureTexture).not.toBeNull();
81
+ });
82
+ });
83
+
84
+ describe('encodeWgpuScreenRenderTargetCapture', () => {
85
+ it('is a no-op when capture is disabled', async () => {
86
+ const state = await createWgpuRenderStateForTest();
87
+ const screen = createWgpuScreenRenderTargetForTest(state);
88
+ let copied = false;
89
+ const encoder = { copyTextureToBuffer: () => (copied = true) } as unknown as GPUCommandEncoder;
90
+ encodeWgpuScreenRenderTargetCapture(screen, encoder);
91
+ expect(copied).toBe(false);
92
+ });
93
+
94
+ it('skips the copy while a readback holds the buffer, and resumes once it lets go', async () => {
95
+ // The writer/reader handshake over the single retained buffer: an animating scene must not keep
96
+ // enqueueing copies into a buffer a readback has mapped or is waiting to map.
97
+ const state = await createWgpuRenderStateForTest();
98
+ const screen = captureReadyScreenRenderTarget(state);
99
+ let mapState: GPUBufferMapState = 'unmapped';
100
+ installCaptureBuffer(screen, () => Promise.resolve());
101
+ Object.defineProperty(screen.captureBuffer, 'mapState', { get: () => mapState });
102
+
103
+ let copies = 0;
104
+ const encoder = { copyTextureToBuffer: () => copies++ } as unknown as GPUCommandEncoder;
105
+
106
+ mapState = 'pending';
107
+ encodeWgpuScreenRenderTargetCapture(screen, encoder);
108
+ mapState = 'mapped';
109
+ encodeWgpuScreenRenderTargetCapture(screen, encoder);
110
+ expect(copies).toBe(0);
111
+
112
+ mapState = 'unmapped';
113
+ encodeWgpuScreenRenderTargetCapture(screen, encoder);
114
+ expect(copies).toBe(1);
115
+ });
116
+
117
+ it('still copies when the implementation reports no map state at all', async () => {
118
+ // Fails safe in the direction that keeps capturing: an undefined mapState must not read as "busy"
119
+ // and silence every copy for the rest of the run.
120
+ const state = await createWgpuRenderStateForTest();
121
+ const screen = captureReadyScreenRenderTarget(state);
122
+ installCaptureBuffer(screen, () => Promise.resolve());
123
+ Object.defineProperty(screen.captureBuffer, 'mapState', { get: () => undefined });
124
+
125
+ let copies = 0;
126
+ encodeWgpuScreenRenderTargetCapture(screen, {
127
+ copyTextureToBuffer: () => copies++,
128
+ } as unknown as GPUCommandEncoder);
129
+
130
+ expect(copies).toBe(1);
131
+ });
132
+ });
133
+
134
+ // A screen target that has drawn one captured frame, so its capture texture exists.
135
+ function captureReadyScreenRenderTarget(
136
+ state: Awaited<ReturnType<typeof createWgpuRenderStateForTest>>,
137
+ ): WgpuScreenRenderTarget {
138
+ const screen = createWgpuScreenRenderTargetForTest(state);
139
+ enableWgpuScreenRenderTargetCapture(screen);
140
+ endWgpuRenderPass(beginWgpuRenderPass(state, screen, { color: [0, 0, 0, 0] }));
141
+ return screen;
142
+ }
143
+
144
+ // Installs a 1x1 retained capture buffer whose map step the caller controls, so the map bound can be
145
+ // exercised without a GPU. The mock device's buffers carry no map surface of their own.
146
+ function installCaptureBuffer(target: WgpuScreenRenderTarget, mapAsync: () => Promise<void>): void {
147
+ target.captureBuffer = {
148
+ mapAsync,
149
+ getMappedRange: () => new ArrayBuffer(256),
150
+ unmap: () => {},
151
+ destroy: () => {},
152
+ } as unknown as GPUBuffer;
153
+ target.captureWidth = 1;
154
+ target.captureHeight = 1;
155
+ target.captureBytesPerRow = 256;
156
+ }
@@ -0,0 +1,152 @@
1
+ import { beginWgpuRenderPass, endWgpuRenderPass } from './wgpuRenderPass';
2
+ import { getWgpuRenderStateRuntime } from './wgpuRenderState';
3
+ import {
4
+ bindWgpuScreenRenderTarget,
5
+ createWgpuScreenRenderTarget,
6
+ destroyWgpuScreenRenderTarget,
7
+ endWgpuScreenRenderTargetFrame,
8
+ initializeWgpuScreenRenderTarget,
9
+ isWgpuScreenRenderTarget,
10
+ syncWgpuScreenRenderTargetExtent,
11
+ } from './wgpuScreenRenderTarget';
12
+ import { createWgpuRenderStateForTest, createWgpuScreenRenderTargetForTest, installWgpuMock } from './wgpuTestHelper';
13
+ import { createWgpuTextureRenderTarget } from './wgpuTextureRenderTarget';
14
+
15
+ beforeAll(() => {
16
+ installWgpuMock();
17
+ });
18
+
19
+ describe('bindWgpuScreenRenderTarget', () => {
20
+ it('acquires one swap-chain view per frame and releases it at the end of the frame', async () => {
21
+ // Nothing may hold a swap-chain view across frames — that is the property that makes a screen target
22
+ // unsampleable — so the view is acquired inside the frame and dropped when it is submitted.
23
+ const state = await createWgpuRenderStateForTest();
24
+ const screen = createWgpuScreenRenderTargetForTest(state);
25
+
26
+ const first = bindWgpuScreenRenderTarget(state, screen);
27
+ expect(bindWgpuScreenRenderTarget(state, screen)).toBe(first);
28
+ expect(screen.presentationView).toBe(first);
29
+
30
+ endWgpuScreenRenderTargetFrame(screen);
31
+ expect(screen.presentationView).toBeNull();
32
+ });
33
+
34
+ it('follows a surface that resized between frames', async () => {
35
+ const state = await createWgpuRenderStateForTest();
36
+ const canvas = document.createElement('canvas');
37
+ canvas.width = 320;
38
+ canvas.height = 240;
39
+ const screen = createWgpuScreenRenderTarget(state.device, canvas, { format: state.format });
40
+
41
+ canvas.width = 640;
42
+ canvas.height = 480;
43
+ bindWgpuScreenRenderTarget(state, screen);
44
+
45
+ expect(screen.width).toBe(640);
46
+ expect(screen.height).toBe(480);
47
+ expect(screen.depthStencilTexture.width).toBe(640);
48
+ });
49
+
50
+ it('draws into supersample storage when the target is antialiased', async () => {
51
+ const state = await createWgpuRenderStateForTest();
52
+ const screen = createWgpuScreenRenderTargetForTest(state, { antialias: true }, 400, 300);
53
+
54
+ const view = bindWgpuScreenRenderTarget(state, screen);
55
+
56
+ expect(screen.width).toBe(800);
57
+ expect(view).toBe(screen.antialiasView);
58
+ expect(view).not.toBe(screen.presentationView);
59
+ expect(screen.antialiasTexture?.width).toBe(800);
60
+ });
61
+ });
62
+
63
+ describe('createWgpuScreenRenderTarget', () => {
64
+ it('configures the surface context and declares one color attachment', async () => {
65
+ const state = await createWgpuRenderStateForTest();
66
+ const canvas = document.createElement('canvas');
67
+ canvas.width = 800;
68
+ canvas.height = 600;
69
+ const context = canvas.getContext('webgpu')!;
70
+ const configure = vi.spyOn(context, 'configure');
71
+
72
+ const screen = createWgpuScreenRenderTarget(state.device, canvas, { format: 'rgba8unorm' });
73
+
74
+ expect(configure).toHaveBeenCalledOnce();
75
+ expect(configure.mock.calls[0]![0].format).toBe('rgba8unorm');
76
+ expect(screen.context).toBe(context);
77
+ expect(screen.colorAttachments).toBe(1);
78
+ expect(screen.format).toBe('rgba8unorm');
79
+ expect(screen.width).toBe(800);
80
+ expect(screen.height).toBe(600);
81
+ });
82
+
83
+ it('reports a surface the host cannot present rather than returning a target that cannot bind', async () => {
84
+ const state = await createWgpuRenderStateForTest();
85
+ const surface = { getContext: () => null, height: 100, width: 100 };
86
+
87
+ expect(() => createWgpuScreenRenderTarget(state.device, surface)).toThrow(/cannot present WebGPU/);
88
+ });
89
+ });
90
+
91
+ describe('destroyWgpuScreenRenderTarget', () => {
92
+ it('frees the storage it owns and unconfigures the context', async () => {
93
+ const state = await createWgpuRenderStateForTest();
94
+ const screen = createWgpuScreenRenderTargetForTest(state, { antialias: true });
95
+ bindWgpuScreenRenderTarget(state, screen);
96
+ const depth = vi.spyOn(screen.depthStencilTexture, 'destroy');
97
+ const antialias = vi.spyOn(screen.antialiasTexture!, 'destroy');
98
+ const unconfigure = vi.spyOn(screen.context, 'unconfigure');
99
+
100
+ destroyWgpuScreenRenderTarget(screen);
101
+
102
+ expect(depth).toHaveBeenCalledOnce();
103
+ expect(antialias).toHaveBeenCalledOnce();
104
+ expect(unconfigure).toHaveBeenCalledOnce();
105
+ expect(screen.antialiasView).toBeNull();
106
+ expect(screen.presentationView).toBeNull();
107
+ });
108
+ });
109
+
110
+ describe('endWgpuScreenRenderTargetFrame', () => {
111
+ it('is what a submitted frame calls, leaving no view behind', async () => {
112
+ const state = await createWgpuRenderStateForTest();
113
+ const screen = createWgpuScreenRenderTargetForTest(state);
114
+
115
+ endWgpuRenderPass(beginWgpuRenderPass(state, screen, { color: [0, 0, 0, 0] }));
116
+
117
+ expect(screen.presentationView).toBeNull();
118
+ expect(getWgpuRenderStateRuntime(state).frameScreenTarget).toBeNull();
119
+ });
120
+ });
121
+
122
+ describe('initializeWgpuScreenRenderTarget', () => {
123
+ it('is the construction initializer of createWgpuScreenRenderTarget', () => {
124
+ expect(typeof initializeWgpuScreenRenderTarget).toBe('function');
125
+ });
126
+ });
127
+
128
+ describe('isWgpuScreenRenderTarget', () => {
129
+ it('separates the two realizations by the field that says where the pixels live', async () => {
130
+ const state = await createWgpuRenderStateForTest();
131
+
132
+ expect(isWgpuScreenRenderTarget(createWgpuScreenRenderTargetForTest(state))).toBe(true);
133
+ expect(isWgpuScreenRenderTarget(createWgpuTextureRenderTarget(state, 16, 16))).toBe(false);
134
+ });
135
+ });
136
+
137
+ describe('syncWgpuScreenRenderTargetExtent', () => {
138
+ it('follows the surface without a resize call', async () => {
139
+ const state = await createWgpuRenderStateForTest();
140
+ const canvas = document.createElement('canvas');
141
+ canvas.width = 320;
142
+ canvas.height = 240;
143
+ const screen = createWgpuScreenRenderTarget(state.device, canvas, { format: state.format });
144
+
145
+ canvas.width = 500;
146
+ canvas.height = 400;
147
+ syncWgpuScreenRenderTargetExtent(screen);
148
+
149
+ expect(screen.width).toBe(500);
150
+ expect(screen.height).toBe(400);
151
+ });
152
+ });
@@ -1,6 +1,5 @@
1
1
  import { AdvancedBlendMode, BlendMode } from '@flighthq/types/contract';
2
2
 
3
- import { renderWgpuBackground } from './wgpuBackground';
4
3
  import { getWgpuRenderStateDeviceResources, getWgpuRenderStateRuntime } from './wgpuRenderState';
5
4
  import {
6
5
  UNIFORM_BYTE_SIZE,
@@ -14,7 +13,7 @@ import {
14
13
  writeWgpuMatrixOnlyUniforms,
15
14
  writeWgpuQuadUniforms,
16
15
  } from './wgpuShader';
17
- import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
16
+ import { beginWgpuScreenRenderPassForTest, createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
18
17
 
19
18
  beforeAll(() => {
20
19
  installWgpuMock();
@@ -164,7 +163,7 @@ describe('writeWgpuMatrixOnlyUniforms', () => {
164
163
  it('advances uniformOffset', async () => {
165
164
  const state = await createWgpuRenderStateForTest();
166
165
  const runtime = getWgpuRenderStateRuntime(state);
167
- renderWgpuBackground(state);
166
+ beginWgpuScreenRenderPassForTest(state);
168
167
  const before = runtime.uniformOffset;
169
168
  const fakeNode = { alpha: 1, useColorScaleBias: false, colorScaleBias: null };
170
169
  const t = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
@@ -176,7 +175,7 @@ describe('writeWgpuQuadUniforms', () => {
176
175
  it('advances uniformOffset by uniformStride', async () => {
177
176
  const state = await createWgpuRenderStateForTest();
178
177
  const runtime = getWgpuRenderStateRuntime(state);
179
- renderWgpuBackground(state);
178
+ beginWgpuScreenRenderPassForTest(state);
180
179
  const before = runtime.uniformOffset;
181
180
  const fakeNode = {
182
181
  alpha: 1,
@@ -191,7 +190,7 @@ describe('writeWgpuQuadUniforms', () => {
191
190
  it('writes quad coordinates into uniform data at float offset 24', async () => {
192
191
  const state = await createWgpuRenderStateForTest();
193
192
  const runtime = getWgpuRenderStateRuntime(state);
194
- renderWgpuBackground(state);
193
+ beginWgpuScreenRenderPassForTest(state);
195
194
  const fakeNode = {
196
195
  alpha: 0.5,
197
196
  useColorScaleBias: false,
@@ -211,7 +210,7 @@ describe('writeWgpuQuadUniforms', () => {
211
210
  it('writes the native straight-alpha texture flag at u32 offset 14', async () => {
212
211
  const state = await createWgpuRenderStateForTest();
213
212
  const runtime = getWgpuRenderStateRuntime(state);
214
- renderWgpuBackground(state);
213
+ beginWgpuScreenRenderPassForTest(state);
215
214
  const fakeNode = {
216
215
  alpha: 1,
217
216
  useColorScaleBias: false,
@@ -0,0 +1,129 @@
1
+ import { createMatrix } from '@flighthq/geometry/contract';
2
+
3
+ import { resolveWgpuSmoothingBindGroup } from './wgpuDraw';
4
+ import { submitWgpuFrame } from './wgpuFrame';
5
+ import { getWgpuRenderStateDeviceResources } from './wgpuRenderState';
6
+ import { beginWgpuScreenRenderPassForTest, createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
7
+ import {
8
+ createWgpuTextureRenderTarget,
9
+ destroyWgpuTextureRenderTarget,
10
+ drawWgpuTextureRenderTargetResult,
11
+ initializeWgpuTextureRenderTarget,
12
+ resizeWgpuTextureRenderTarget,
13
+ } from './wgpuTextureRenderTarget';
14
+
15
+ beforeAll(() => {
16
+ installWgpuMock();
17
+ });
18
+
19
+ describe('createWgpuTextureRenderTarget', () => {
20
+ it('returns a target with texture, view, binding cache, and depth-stencil', async () => {
21
+ const state = await createWgpuRenderStateForTest();
22
+ const target = createWgpuTextureRenderTarget(state, 256, 256);
23
+ expect(target.texture).toBeDefined();
24
+ expect(target.view).toBeDefined();
25
+ expect(target.bindings).toBeDefined();
26
+ expect(target.depthStencilTexture).toBeDefined();
27
+ expect(target.width).toBe(256);
28
+ expect(target.height).toBe(256);
29
+ });
30
+
31
+ it('clamps to minimum 1×1', async () => {
32
+ const state = await createWgpuRenderStateForTest();
33
+ const target = createWgpuTextureRenderTarget(state, 0, 0);
34
+ expect(target.width).toBe(1);
35
+ expect(target.height).toBe(1);
36
+ });
37
+
38
+ it('defaults to sRGB and accepts a linear content declaration', async () => {
39
+ const state = await createWgpuRenderStateForTest();
40
+ expect(createWgpuTextureRenderTarget(state, 16, 16).colorSpace).toBe('srgb');
41
+ expect(createWgpuTextureRenderTarget(state, 16, 16, state.format, 'linear').colorSpace).toBe('linear');
42
+ });
43
+
44
+ it('realizes four coverage samples as a 2x extent in each axis', async () => {
45
+ const state = await createWgpuRenderStateForTest();
46
+ const target = createWgpuTextureRenderTarget(state, 64, 48, state.format, 'srgb', 4);
47
+ expect(target.width).toBe(128);
48
+ expect(target.height).toBe(96);
49
+ expect(target.sampleCount).toBe(4);
50
+ expect(target.texture.width).toBe(128);
51
+ expect(target.texture.height).toBe(96);
52
+ });
53
+ });
54
+
55
+ describe('destroyWgpuTextureRenderTarget', () => {
56
+ it('calls destroy on texture and depth-stencil', async () => {
57
+ const state = await createWgpuRenderStateForTest();
58
+ const target = createWgpuTextureRenderTarget(state, 64, 64);
59
+ const destroyTexture = vi.spyOn(target.texture, 'destroy');
60
+ const destroyDepth = vi.spyOn(target.depthStencilTexture, 'destroy');
61
+ destroyWgpuTextureRenderTarget(target);
62
+ expect(destroyTexture).toHaveBeenCalled();
63
+ expect(destroyDepth).toHaveBeenCalled();
64
+ });
65
+ });
66
+
67
+ describe('drawWgpuTextureRenderTargetResult', () => {
68
+ it('does not throw with an open render pass', async () => {
69
+ const state = await createWgpuRenderStateForTest();
70
+ beginWgpuScreenRenderPassForTest(state);
71
+ const target = createWgpuTextureRenderTarget(state, 64, 64);
72
+ const fakeNode = {
73
+ alpha: 1,
74
+ material: null,
75
+ transform2D: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 },
76
+ };
77
+ expect(() => drawWgpuTextureRenderTargetResult(state, fakeNode, target, createMatrix())).not.toThrow();
78
+ submitWgpuFrame(state);
79
+ });
80
+ });
81
+
82
+ describe('initializeWgpuTextureRenderTarget', () => {
83
+ it('is the construction initializer of createWgpuTextureRenderTarget', () => {
84
+ expect(typeof initializeWgpuTextureRenderTarget).toBe('function');
85
+ });
86
+ });
87
+
88
+ describe('resizeWgpuTextureRenderTarget', () => {
89
+ it('preserves the allocation when dimensions are unchanged', async () => {
90
+ const state = await createWgpuRenderStateForTest();
91
+ const target = createWgpuTextureRenderTarget(state, 64, 64);
92
+ const texture = target.texture;
93
+ const depthStencilTexture = target.depthStencilTexture;
94
+ const sampler = getWgpuRenderStateDeviceResources(state).linearSampler;
95
+ const bindGroup = resolveWgpuSmoothingBindGroup(state, target, true);
96
+
97
+ resizeWgpuTextureRenderTarget(state, target, 64, 64);
98
+
99
+ expect(target.texture).toBe(texture);
100
+ expect(target.depthStencilTexture).toBe(depthStencilTexture);
101
+ // The view survived, so its cached bindings must survive with it.
102
+ expect(target.bindings.get(sampler)).toBe(bindGroup);
103
+ });
104
+
105
+ it('updates width, height, and drops bindings that referenced the old view', async () => {
106
+ const state = await createWgpuRenderStateForTest();
107
+ const target = createWgpuTextureRenderTarget(state, 64, 64);
108
+ const previousBindGroup = resolveWgpuSmoothingBindGroup(state, target, true);
109
+ resizeWgpuTextureRenderTarget(state, target, 200, 150);
110
+ expect(target.width).toBe(200);
111
+ expect(target.height).toBe(150);
112
+ // Reallocation replaces the view, so every binding over the old one is dropped and rebuilt.
113
+ expect(target.bindings.size).toBe(0);
114
+ expect(resolveWgpuSmoothingBindGroup(state, target, true)).not.toBe(previousBindGroup);
115
+ });
116
+
117
+ it('reallocates when the effective sample count changes', async () => {
118
+ const state = await createWgpuRenderStateForTest();
119
+ const target = createWgpuTextureRenderTarget(state, 64, 48);
120
+ const previousTexture = target.texture;
121
+
122
+ resizeWgpuTextureRenderTarget(state, target, 64, 48, 4);
123
+
124
+ expect(target.width).toBe(128);
125
+ expect(target.height).toBe(96);
126
+ expect(target.sampleCount).toBe(4);
127
+ expect(target.texture).not.toBe(previousTexture);
128
+ });
129
+ });
@@ -8,10 +8,10 @@ import {
8
8
  RenderTargetTextureSourceKind,
9
9
  } from '@flighthq/types/contract';
10
10
 
11
- import { renderWgpuBackground, submitWgpuRenderPass } from './wgpuBackground';
11
+ import { submitWgpuFrame } from './wgpuFrame';
12
12
  import { getWgpuRenderStateRuntime } from './wgpuRenderState';
13
13
  import { renderIntoWgpuRenderTexture } from './wgpuRenderTexture';
14
- import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
14
+ import { beginWgpuScreenRenderPassForTest, createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
15
15
  import {
16
16
  registerWgpuImageTextureResolver,
17
17
  registerWgpuBitmapTextureResolver,
@@ -177,10 +177,10 @@ describe('registerWgpuRenderTextureResolver', () => {
177
177
  registerWgpuRenderTextureResolver(state);
178
178
  expect(resolveWgpuTexture(state, texture)).toBeNull();
179
179
 
180
- renderWgpuBackground(state);
181
- renderIntoWgpuRenderTexture(state, texture, () => {});
180
+ beginWgpuScreenRenderPassForTest(state);
181
+ renderIntoWgpuRenderTexture(state, texture, () => {}, { color: [0, 0, 0, 0], depth: 1.0, stencil: 0 });
182
182
  expect(resolveWgpuTexture(state, texture)).not.toBeNull();
183
- submitWgpuRenderPass(state);
183
+ submitWgpuFrame(state);
184
184
  });
185
185
  });
186
186
 
@@ -1,14 +0,0 @@
1
- import type { WgpuPresentationRenderState, WgpuRenderState } from '@flighthq/types/contract';
2
- export declare function acquireWgpuSurfaceAntialiasView(state: Readonly<WgpuPresentationRenderState>, presentationView: GPUTextureView): GPUTextureView | null;
3
- export declare function clearWgpuSurfacePresentation(state: Readonly<WgpuPresentationRenderState>): void;
4
- export declare function encodeWgpuSurfaceAntialiasResolve(state: Readonly<WgpuPresentationRenderState>, encoder: GPUCommandEncoder): void;
5
- export declare function getWgpuSurfaceLogicalExtent(state: Readonly<WgpuRenderState>): {
6
- width: number;
7
- height: number;
8
- };
9
- export declare function getWgpuSurfaceRenderExtent(state: Readonly<WgpuRenderState>): {
10
- width: number;
11
- height: number;
12
- };
13
- export declare function getWgpuSurfaceRenderScale(state: Readonly<WgpuRenderState>): number;
14
- //# sourceMappingURL=wgpuAntialias.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"wgpuAntialias.d.ts","sourceRoot":"","sources":["../src/wgpuAntialias.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAS7F,wBAAgB,+BAA+B,CAC7C,KAAK,EAAE,QAAQ,CAAC,2BAA2B,CAAC,EAC5C,gBAAgB,EAAE,cAAc,GAC/B,cAAc,GAAG,IAAI,CA8CvB;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,QAAQ,CAAC,2BAA2B,CAAC,GAAG,IAAI,CAE/F;AAKD,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,QAAQ,CAAC,2BAA2B,CAAC,EAC5C,OAAO,EAAE,iBAAiB,GACzB,IAAI,CAqBN;AAKD,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAQ/G;AAID,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAW9G;AAID,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG,MAAM,CAGlF"}