@flighthq/render-wgpu 0.1.0 → 0.2.1-next.410.a23f189

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 (39) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/wgpuDraw.d.ts +3 -2
  6. package/dist/wgpuDraw.d.ts.map +1 -1
  7. package/dist/wgpuDraw.js +92 -15
  8. package/dist/wgpuDraw.js.map +1 -1
  9. package/dist/wgpuFullscreenPass.js +1 -1
  10. package/dist/wgpuFullscreenPass.js.map +1 -1
  11. package/dist/wgpuMipmap.d.ts +4 -0
  12. package/dist/wgpuMipmap.d.ts.map +1 -0
  13. package/dist/wgpuMipmap.js +92 -0
  14. package/dist/wgpuMipmap.js.map +1 -0
  15. package/dist/wgpuRenderState.d.ts +3 -2
  16. package/dist/wgpuRenderState.d.ts.map +1 -1
  17. package/dist/wgpuRenderState.js +44 -1
  18. package/dist/wgpuRenderState.js.map +1 -1
  19. package/dist/wgpuRenderTarget.d.ts +4 -3
  20. package/dist/wgpuRenderTarget.d.ts.map +1 -1
  21. package/dist/wgpuRenderTarget.js +61 -11
  22. package/dist/wgpuRenderTarget.js.map +1 -1
  23. package/dist/wgpuShader.d.ts.map +1 -1
  24. package/dist/wgpuShader.js +21 -21
  25. package/dist/wgpuShader.js.map +1 -1
  26. package/dist/wgpuTestHelper.d.ts.map +1 -1
  27. package/dist/wgpuTestHelper.js +8 -3
  28. package/dist/wgpuTestHelper.js.map +1 -1
  29. package/dist/wgpuTextureUpload.d.ts +5 -0
  30. package/dist/wgpuTextureUpload.d.ts.map +1 -0
  31. package/dist/wgpuTextureUpload.js +25 -0
  32. package/dist/wgpuTextureUpload.js.map +1 -0
  33. package/package.json +11 -6
  34. package/src/wgpuDraw.test.ts +81 -12
  35. package/src/wgpuMipmap.test.ts +73 -0
  36. package/src/wgpuRenderState.test.ts +46 -0
  37. package/src/wgpuRenderTarget.test.ts +32 -14
  38. package/src/wgpuShader.test.ts +40 -0
  39. package/src/wgpuTextureUpload.test.ts +60 -0
@@ -2,6 +2,7 @@ import {
2
2
  createWgpuRenderStateRuntime,
3
3
  destroyWgpuRenderState,
4
4
  getWgpuRenderStateRuntime,
5
+ getWgpuSampler,
5
6
  isWgpuSupported,
6
7
  } from './wgpuRenderState';
7
8
  import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
@@ -81,6 +82,51 @@ describe('getWgpuRenderStateRuntime', () => {
81
82
  });
82
83
  });
83
84
 
85
+ describe('getWgpuSampler', () => {
86
+ it('caches a sampler per filter+wrap+mip+anisotropy key and reuses it', async () => {
87
+ const state = await createWgpuRenderStateForTest();
88
+ const a = getWgpuSampler(state, 'linear', 'repeat', 'repeat');
89
+ const b = getWgpuSampler(state, 'linear', 'repeat', 'repeat');
90
+ expect(a).toBe(b);
91
+ expect(getWgpuRenderStateRuntime(state).samplerCache.get('linear|repeat|repeat|none|1')).toBe(a);
92
+ });
93
+
94
+ it('returns a distinct sampler for a different wrap or filter', async () => {
95
+ const state = await createWgpuRenderStateForTest();
96
+ const repeat = getWgpuSampler(state, 'linear', 'repeat', 'repeat');
97
+ const clamp = getWgpuSampler(state, 'linear', 'clamp-to-edge', 'clamp-to-edge');
98
+ const nearest = getWgpuSampler(state, 'nearest', 'repeat', 'repeat');
99
+ expect(repeat).not.toBe(clamp);
100
+ expect(repeat).not.toBe(nearest);
101
+ });
102
+
103
+ it('keys the mip filter separately so a trilinear sampler differs from a non-mip one', async () => {
104
+ const state = await createWgpuRenderStateForTest();
105
+ const noMip = getWgpuSampler(state, 'linear', 'repeat', 'repeat');
106
+ const trilinear = getWgpuSampler(state, 'linear', 'repeat', 'repeat', 'linear');
107
+ expect(noMip).not.toBe(trilinear);
108
+ expect(getWgpuRenderStateRuntime(state).samplerCache.has('linear|repeat|repeat|linear|1')).toBe(true);
109
+ });
110
+
111
+ it('forces linear filtering and a linear mip filter when anisotropy exceeds 1', async () => {
112
+ // WebGPU rejects maxAnisotropy > 1 unless min/mag/mip are all linear, so a nearest+aniso request
113
+ // collapses to the linear anisotropic key.
114
+ const state = await createWgpuRenderStateForTest();
115
+ const sampler = getWgpuSampler(state, 'nearest', 'clamp-to-edge', 'clamp-to-edge', undefined, 8);
116
+ const cache = getWgpuRenderStateRuntime(state).samplerCache;
117
+ expect(cache.get('linear|clamp-to-edge|clamp-to-edge|linear|8')).toBe(sampler);
118
+ expect(cache.has('nearest|clamp-to-edge|clamp-to-edge|none|8')).toBe(false);
119
+ });
120
+
121
+ it('floors and clamps the anisotropy level into the cache key', async () => {
122
+ const state = await createWgpuRenderStateForTest();
123
+ const a = getWgpuSampler(state, 'linear', 'repeat', 'repeat', 'linear', 4.9);
124
+ const b = getWgpuSampler(state, 'linear', 'repeat', 'repeat', 'linear', 4);
125
+ expect(a).toBe(b);
126
+ expect(getWgpuRenderStateRuntime(state).samplerCache.has('linear|repeat|repeat|linear|4')).toBe(true);
127
+ });
128
+ });
129
+
84
130
  describe('isWgpuSupported', () => {
85
131
  it('returns true when navigator.gpu is present', () => {
86
132
  expect(isWgpuSupported()).toBe(true);
@@ -3,12 +3,13 @@ import { createMatrix } from '@flighthq/geometry';
3
3
  import { renderWgpuBackground, submitWgpuRenderPass } from './wgpuBackground';
4
4
  import { getWgpuRenderStateRuntime } from './wgpuRenderState';
5
5
  import {
6
- beginWgpuRenderTarget,
6
+ beginWgpuRenderPass,
7
7
  createWgpuRenderTarget,
8
8
  destroyWgpuRenderTarget,
9
9
  drawWgpuRenderTargetResult,
10
- endWgpuRenderTarget,
10
+ endWgpuRenderPass,
11
11
  resizeWgpuRenderTarget,
12
+ setWgpuRenderTransform2D,
12
13
  } from './wgpuRenderTarget';
13
14
  import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
14
15
 
@@ -16,31 +17,28 @@ beforeAll(() => {
16
17
  installWgpuMock();
17
18
  });
18
19
 
19
- describe('beginWgpuRenderTarget', () => {
20
+ describe('beginWgpuRenderPass', () => {
20
21
  it('sets the render target viewport', async () => {
21
22
  const state = await createWgpuRenderStateForTest();
22
23
  renderWgpuBackground(state);
23
24
  const target = createWgpuRenderTarget(state, 64, 64);
24
- beginWgpuRenderTarget(state, target, createMatrix());
25
+ beginWgpuRenderPass(state, target);
25
26
  expect(getWgpuRenderStateRuntime(state).renderTargetViewport?.width).toBe(64);
26
- endWgpuRenderTarget(state);
27
+ endWgpuRenderPass(state);
27
28
  submitWgpuRenderPass(state);
28
29
  });
29
- });
30
30
 
31
- describe('beginWgpuRenderTarget / endWgpuRenderTarget', () => {
32
- it('switches the render target and restores it', async () => {
31
+ it('switches the render target and restores it on end', async () => {
33
32
  const state = await createWgpuRenderStateForTest();
34
33
  renderWgpuBackground(state);
35
34
 
36
35
  const target = createWgpuRenderTarget(state, 128, 128);
37
- const transform = createMatrix();
38
- beginWgpuRenderTarget(state, target, transform);
36
+ beginWgpuRenderPass(state, target);
39
37
 
40
38
  const runtime = getWgpuRenderStateRuntime(state);
41
39
  expect(runtime.renderTargetViewport?.width).toBe(128);
42
40
 
43
- endWgpuRenderTarget(state);
41
+ endWgpuRenderPass(state);
44
42
  expect(runtime.renderTargetViewport).toBeNull();
45
43
 
46
44
  submitWgpuRenderPass(state);
@@ -94,13 +92,13 @@ describe('drawWgpuRenderTargetResult', () => {
94
92
  });
95
93
  });
96
94
 
97
- describe('endWgpuRenderTarget', () => {
95
+ describe('endWgpuRenderPass', () => {
98
96
  it('restores null renderTargetViewport', async () => {
99
97
  const state = await createWgpuRenderStateForTest();
100
98
  renderWgpuBackground(state);
101
99
  const target = createWgpuRenderTarget(state, 32, 32);
102
- beginWgpuRenderTarget(state, target, createMatrix());
103
- endWgpuRenderTarget(state);
100
+ beginWgpuRenderPass(state, target);
101
+ endWgpuRenderPass(state);
104
102
  expect(getWgpuRenderStateRuntime(state).renderTargetViewport).toBeNull();
105
103
  submitWgpuRenderPass(state);
106
104
  });
@@ -118,3 +116,23 @@ describe('resizeWgpuRenderTarget', () => {
118
116
  expect(target.bindGroup).not.toBe(previousBindGroup);
119
117
  });
120
118
  });
119
+
120
+ describe('setWgpuRenderTransform2D', () => {
121
+ it('installs a copy of the transform, restored by the enclosing pass', async () => {
122
+ const state = await createWgpuRenderStateForTest();
123
+ renderWgpuBackground(state);
124
+ const original = state.renderTransform2D;
125
+ const target = createWgpuRenderTarget(state, 32, 32);
126
+ const bake = createMatrix();
127
+ bake.tx = 42;
128
+
129
+ beginWgpuRenderPass(state, target);
130
+ setWgpuRenderTransform2D(state, bake);
131
+ expect(state.renderTransform2D?.tx).toBe(42);
132
+ expect(state.renderTransform2D).not.toBe(bake);
133
+ endWgpuRenderPass(state);
134
+
135
+ expect(state.renderTransform2D).toBe(original);
136
+ submitWgpuRenderPass(state);
137
+ });
138
+ });
@@ -1,3 +1,5 @@
1
+ import { AdvancedBlendMode, BlendMode } from '@flighthq/types';
2
+
1
3
  import { renderWgpuBackground } from './wgpuBackground';
2
4
  import { getWgpuRenderStateRuntime } from './wgpuRenderState';
3
5
  import {
@@ -63,6 +65,44 @@ describe('getWgpuPipeline', () => {
63
65
  const maskwrite = getWgpuPipeline(state, null, 'maskwrite');
64
66
  expect(normal).not.toBe(maskwrite);
65
67
  });
68
+
69
+ it('realizes each fixed-function blend mode with GL-parity factors', async () => {
70
+ const state = await createWgpuRenderStateForTest();
71
+ const blendOf = (mode: BlendMode) =>
72
+ [
73
+ ...(getWgpuPipeline(state, mode, 'normal') as unknown as { __descriptor: GPURenderPipelineDescriptor })
74
+ .__descriptor.fragment!.targets,
75
+ ][0]!.blend!.color;
76
+ expect(blendOf(BlendMode.Normal)).toEqual({ srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' });
77
+ expect(blendOf(BlendMode.Add)).toEqual({ srcFactor: 'one', dstFactor: 'one', operation: 'add' });
78
+ expect(blendOf(BlendMode.Multiply)).toEqual({
79
+ srcFactor: 'dst',
80
+ dstFactor: 'one-minus-src-alpha',
81
+ operation: 'add',
82
+ });
83
+ expect(blendOf(BlendMode.Screen)).toEqual({ srcFactor: 'one', dstFactor: 'one-minus-src', operation: 'add' });
84
+ expect(blendOf(BlendMode.Darken).operation).toBe('min');
85
+ expect(blendOf(BlendMode.Lighten).operation).toBe('max');
86
+ });
87
+
88
+ it('falls back to normal blend for shader-composited modes', async () => {
89
+ const state = await createWgpuRenderStateForTest();
90
+ const blendOf = (mode: BlendMode) =>
91
+ [
92
+ ...(getWgpuPipeline(state, mode, 'normal') as unknown as { __descriptor: GPURenderPipelineDescriptor })
93
+ .__descriptor.fragment!.targets,
94
+ ][0]!.blend!.color;
95
+ expect(blendOf(AdvancedBlendMode.Overlay)).toEqual({
96
+ srcFactor: 'one',
97
+ dstFactor: 'one-minus-src-alpha',
98
+ operation: 'add',
99
+ });
100
+ expect(blendOf(AdvancedBlendMode.HardLight)).toEqual({
101
+ srcFactor: 'one',
102
+ dstFactor: 'one-minus-src-alpha',
103
+ operation: 'add',
104
+ });
105
+ });
66
106
  });
67
107
 
68
108
  describe('setWgpuMatrixFromTransform', () => {
@@ -0,0 +1,60 @@
1
+ import type { ImageResource } from '@flighthq/types';
2
+
3
+ import { uploadWgpuTextureData, uploadWgpuTextureElement, uploadWgpuTextureImageResource } from './wgpuTextureUpload';
4
+
5
+ // A device whose queue records the two upload calls so a test can assert which path ran and with what.
6
+ function makeDevice(): GPUDevice {
7
+ return {
8
+ queue: { writeTexture: vi.fn(), copyExternalImageToTexture: vi.fn() },
9
+ } as unknown as GPUDevice;
10
+ }
11
+
12
+ describe('uploadWgpuTextureData', () => {
13
+ it('drives writeTexture with a tightly-packed rgba8 layout (bytesPerRow = width*4)', () => {
14
+ const device = makeDevice();
15
+ const texture = {} as GPUTexture;
16
+ const data = new Uint8ClampedArray(2 * 2 * 4);
17
+ uploadWgpuTextureData(device, texture, [0, 0, 3], 2, 2, data);
18
+ expect(device.queue.writeTexture).toHaveBeenCalledWith(
19
+ { texture, origin: [0, 0, 3] },
20
+ data,
21
+ { bytesPerRow: 8, rowsPerImage: 2 },
22
+ [2, 2, 1],
23
+ );
24
+ });
25
+ });
26
+
27
+ describe('uploadWgpuTextureElement', () => {
28
+ it('drives copyExternalImageToTexture with the external source', () => {
29
+ const device = makeDevice();
30
+ const texture = {} as GPUTexture;
31
+ const source = {} as GPUCopyExternalImageSource;
32
+ uploadWgpuTextureElement(device, texture, [0, 0, 0], 4, 4, source);
33
+ expect(device.queue.copyExternalImageToTexture).toHaveBeenCalledWith(
34
+ { source },
35
+ { texture, origin: [0, 0, 0] },
36
+ [4, 4, 1],
37
+ );
38
+ });
39
+ });
40
+
41
+ describe('uploadWgpuTextureImageResource', () => {
42
+ it('takes the element path when the resource carries a source', () => {
43
+ const device = makeDevice();
44
+ const texture = {} as GPUTexture;
45
+ const image = { source: {} as CanvasImageSource, data: null, width: 4, height: 4 } as ImageResource;
46
+ uploadWgpuTextureImageResource(device, texture, [0, 0, 0], image);
47
+ expect(device.queue.copyExternalImageToTexture).toHaveBeenCalledTimes(1);
48
+ expect(device.queue.writeTexture).not.toHaveBeenCalled();
49
+ });
50
+
51
+ it('takes the raw-pixel path when the resource is data-only', () => {
52
+ const device = makeDevice();
53
+ const texture = {} as GPUTexture;
54
+ const data = new Uint8ClampedArray(4 * 4 * 4);
55
+ const image = { source: null, data, width: 4, height: 4 } as ImageResource;
56
+ uploadWgpuTextureImageResource(device, texture, [0, 0, 0], image);
57
+ expect(device.queue.writeTexture).toHaveBeenCalledTimes(1);
58
+ expect(device.queue.copyExternalImageToTexture).not.toHaveBeenCalled();
59
+ });
60
+ });