@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.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/wgpuDraw.d.ts +3 -2
- package/dist/wgpuDraw.d.ts.map +1 -1
- package/dist/wgpuDraw.js +92 -15
- package/dist/wgpuDraw.js.map +1 -1
- package/dist/wgpuFullscreenPass.js +1 -1
- package/dist/wgpuFullscreenPass.js.map +1 -1
- package/dist/wgpuMipmap.d.ts +4 -0
- package/dist/wgpuMipmap.d.ts.map +1 -0
- package/dist/wgpuMipmap.js +92 -0
- package/dist/wgpuMipmap.js.map +1 -0
- package/dist/wgpuRenderState.d.ts +3 -2
- package/dist/wgpuRenderState.d.ts.map +1 -1
- package/dist/wgpuRenderState.js +44 -1
- package/dist/wgpuRenderState.js.map +1 -1
- package/dist/wgpuRenderTarget.d.ts +4 -3
- package/dist/wgpuRenderTarget.d.ts.map +1 -1
- package/dist/wgpuRenderTarget.js +61 -11
- package/dist/wgpuRenderTarget.js.map +1 -1
- package/dist/wgpuShader.d.ts.map +1 -1
- package/dist/wgpuShader.js +21 -21
- package/dist/wgpuShader.js.map +1 -1
- package/dist/wgpuTestHelper.d.ts.map +1 -1
- package/dist/wgpuTestHelper.js +8 -3
- package/dist/wgpuTestHelper.js.map +1 -1
- package/dist/wgpuTextureUpload.d.ts +5 -0
- package/dist/wgpuTextureUpload.d.ts.map +1 -0
- package/dist/wgpuTextureUpload.js +25 -0
- package/dist/wgpuTextureUpload.js.map +1 -0
- package/package.json +11 -6
- package/src/wgpuDraw.test.ts +81 -12
- package/src/wgpuMipmap.test.ts +73 -0
- package/src/wgpuRenderState.test.ts +46 -0
- package/src/wgpuRenderTarget.test.ts +32 -14
- package/src/wgpuShader.test.ts +40 -0
- 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
|
-
|
|
6
|
+
beginWgpuRenderPass,
|
|
7
7
|
createWgpuRenderTarget,
|
|
8
8
|
destroyWgpuRenderTarget,
|
|
9
9
|
drawWgpuRenderTargetResult,
|
|
10
|
-
|
|
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('
|
|
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
|
-
|
|
25
|
+
beginWgpuRenderPass(state, target);
|
|
25
26
|
expect(getWgpuRenderStateRuntime(state).renderTargetViewport?.width).toBe(64);
|
|
26
|
-
|
|
27
|
+
endWgpuRenderPass(state);
|
|
27
28
|
submitWgpuRenderPass(state);
|
|
28
29
|
});
|
|
29
|
-
});
|
|
30
30
|
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
103
|
-
|
|
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
|
+
});
|
package/src/wgpuShader.test.ts
CHANGED
|
@@ -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
|
+
});
|