@flighthq/render-wgpu 0.1.0

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 (79) hide show
  1. package/dist/index.d.ts +17 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +16 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/wgpuAdapterCapabilities.d.ts +3 -0
  6. package/dist/wgpuAdapterCapabilities.d.ts.map +1 -0
  7. package/dist/wgpuAdapterCapabilities.js +28 -0
  8. package/dist/wgpuAdapterCapabilities.js.map +1 -0
  9. package/dist/wgpuBackground.d.ts +4 -0
  10. package/dist/wgpuBackground.d.ts.map +1 -0
  11. package/dist/wgpuBackground.js +110 -0
  12. package/dist/wgpuBackground.js.map +1 -0
  13. package/dist/wgpuDraw.d.ts +21 -0
  14. package/dist/wgpuDraw.d.ts.map +1 -0
  15. package/dist/wgpuDraw.js +153 -0
  16. package/dist/wgpuDraw.js.map +1 -0
  17. package/dist/wgpuElement.d.ts +2 -0
  18. package/dist/wgpuElement.d.ts.map +1 -0
  19. package/dist/wgpuElement.js +9 -0
  20. package/dist/wgpuElement.js.map +1 -0
  21. package/dist/wgpuFullscreenPass.d.ts +5 -0
  22. package/dist/wgpuFullscreenPass.d.ts.map +1 -0
  23. package/dist/wgpuFullscreenPass.js +106 -0
  24. package/dist/wgpuFullscreenPass.js.map +1 -0
  25. package/dist/wgpuMaterialRegistry.d.ts +5 -0
  26. package/dist/wgpuMaterialRegistry.d.ts.map +1 -0
  27. package/dist/wgpuMaterialRegistry.js +27 -0
  28. package/dist/wgpuMaterialRegistry.js.map +1 -0
  29. package/dist/wgpuRenderState.d.ts +7 -0
  30. package/dist/wgpuRenderState.d.ts.map +1 -0
  31. package/dist/wgpuRenderState.js +153 -0
  32. package/dist/wgpuRenderState.js.map +1 -0
  33. package/dist/wgpuRenderTarget.d.ts +19 -0
  34. package/dist/wgpuRenderTarget.d.ts.map +1 -0
  35. package/dist/wgpuRenderTarget.js +142 -0
  36. package/dist/wgpuRenderTarget.js.map +1 -0
  37. package/dist/wgpuRenderTargetPool.d.ts +10 -0
  38. package/dist/wgpuRenderTargetPool.d.ts.map +1 -0
  39. package/dist/wgpuRenderTargetPool.js +32 -0
  40. package/dist/wgpuRenderTargetPool.js.map +1 -0
  41. package/dist/wgpuScissor.d.ts +5 -0
  42. package/dist/wgpuScissor.d.ts.map +1 -0
  43. package/dist/wgpuScissor.js +37 -0
  44. package/dist/wgpuScissor.js.map +1 -0
  45. package/dist/wgpuShader.d.ts +43 -0
  46. package/dist/wgpuShader.d.ts.map +1 -0
  47. package/dist/wgpuShader.js +304 -0
  48. package/dist/wgpuShader.js.map +1 -0
  49. package/dist/wgpuShaderBinding.d.ts +5 -0
  50. package/dist/wgpuShaderBinding.d.ts.map +1 -0
  51. package/dist/wgpuShaderBinding.js +28 -0
  52. package/dist/wgpuShaderBinding.js.map +1 -0
  53. package/dist/wgpuShaderRegistry.d.ts +3 -0
  54. package/dist/wgpuShaderRegistry.d.ts.map +1 -0
  55. package/dist/wgpuShaderRegistry.js +8 -0
  56. package/dist/wgpuShaderRegistry.js.map +1 -0
  57. package/dist/wgpuSurface.d.ts +6 -0
  58. package/dist/wgpuSurface.d.ts.map +1 -0
  59. package/dist/wgpuSurface.js +107 -0
  60. package/dist/wgpuSurface.js.map +1 -0
  61. package/dist/wgpuTestHelper.d.ts +4 -0
  62. package/dist/wgpuTestHelper.d.ts.map +1 -0
  63. package/dist/wgpuTestHelper.js +137 -0
  64. package/dist/wgpuTestHelper.js.map +1 -0
  65. package/package.json +42 -0
  66. package/src/wgpuAdapterCapabilities.test.ts +49 -0
  67. package/src/wgpuBackground.test.ts +52 -0
  68. package/src/wgpuDraw.test.ts +174 -0
  69. package/src/wgpuElement.test.ts +21 -0
  70. package/src/wgpuFullscreenPass.test.ts +64 -0
  71. package/src/wgpuMaterialRegistry.test.ts +56 -0
  72. package/src/wgpuRenderState.test.ts +88 -0
  73. package/src/wgpuRenderTarget.test.ts +120 -0
  74. package/src/wgpuRenderTargetPool.test.ts +58 -0
  75. package/src/wgpuScissor.test.ts +97 -0
  76. package/src/wgpuShader.test.ts +140 -0
  77. package/src/wgpuShaderBinding.test.ts +62 -0
  78. package/src/wgpuShaderRegistry.test.ts +30 -0
  79. package/src/wgpuSurface.test.ts +49 -0
@@ -0,0 +1,137 @@
1
+ import { createWgpuRenderState } from './wgpuRenderState';
2
+ // Wgpu flag constants are only type-level in @webgpu/types; install runtime values for JSDOM.
3
+ function installWgpuConstants() {
4
+ const g = globalThis;
5
+ if (!g['GPUBufferUsage']) {
6
+ g['GPUBufferUsage'] = {
7
+ MAP_READ: 1,
8
+ MAP_WRITE: 2,
9
+ COPY_SRC: 4,
10
+ COPY_DST: 8,
11
+ INDEX: 16,
12
+ VERTEX: 32,
13
+ UNIFORM: 64,
14
+ STORAGE: 128,
15
+ INDIRECT: 256,
16
+ QUERY_RESOLVE: 512,
17
+ };
18
+ }
19
+ if (!g['GPUTextureUsage']) {
20
+ g['GPUTextureUsage'] = { COPY_SRC: 1, COPY_DST: 2, TEXTURE_BINDING: 4, STORAGE_BINDING: 8, RENDER_ATTACHMENT: 16 };
21
+ }
22
+ if (!g['GPUShaderStage']) {
23
+ g['GPUShaderStage'] = { VERTEX: 1, FRAGMENT: 2, COMPUTE: 4 };
24
+ }
25
+ if (!g['GPUColorWrite']) {
26
+ g['GPUColorWrite'] = { RED: 1, GREEN: 2, BLUE: 4, ALPHA: 8, ALL: 15 };
27
+ }
28
+ if (!g['GPUMapMode']) {
29
+ g['GPUMapMode'] = { READ: 1, WRITE: 2 };
30
+ }
31
+ }
32
+ // Minimal Wgpu mock for JSDOM test environments.
33
+ // Provides enough surface area for state creation and basic draw call recording.
34
+ function makeBindGroup() {
35
+ return {};
36
+ }
37
+ function makeBindGroupLayout() {
38
+ return {};
39
+ }
40
+ function makeBuffer() {
41
+ return { destroy: () => { } };
42
+ }
43
+ function makeTexture() {
44
+ return {
45
+ createView: () => ({}),
46
+ destroy: () => { },
47
+ };
48
+ }
49
+ function makeRenderPassEncoder() {
50
+ return {
51
+ draw: () => { },
52
+ end: () => { },
53
+ setBindGroup: () => { },
54
+ setPipeline: () => { },
55
+ setScissorRect: () => { },
56
+ setStencilReference: () => { },
57
+ setViewport: () => { },
58
+ };
59
+ }
60
+ function makeCommandEncoder() {
61
+ return {
62
+ beginRenderPass: () => makeRenderPassEncoder(),
63
+ finish: () => ({}),
64
+ };
65
+ }
66
+ function makePipeline() {
67
+ return {};
68
+ }
69
+ function makeShaderModule() {
70
+ return {};
71
+ }
72
+ function makeSampler() {
73
+ return {};
74
+ }
75
+ function makePipelineLayout() {
76
+ return {};
77
+ }
78
+ function makeDevice() {
79
+ return {
80
+ limits: { minUniformBufferOffsetAlignment: 256 },
81
+ createBindGroup: () => makeBindGroup(),
82
+ createBindGroupLayout: () => makeBindGroupLayout(),
83
+ createBuffer: () => makeBuffer(),
84
+ createCommandEncoder: () => makeCommandEncoder(),
85
+ createPipelineLayout: () => makePipelineLayout(),
86
+ createRenderPipeline: () => makePipeline(),
87
+ createSampler: () => makeSampler(),
88
+ createShaderModule: () => makeShaderModule(),
89
+ createTexture: () => makeTexture(),
90
+ queue: {
91
+ copyExternalImageToTexture: () => { },
92
+ submit: () => { },
93
+ writeBuffer: () => { },
94
+ writeTexture: () => { },
95
+ },
96
+ };
97
+ }
98
+ function makeAdapter() {
99
+ return {
100
+ requestDevice: () => Promise.resolve(makeDevice()),
101
+ };
102
+ }
103
+ export async function createWgpuRenderStateForTest() {
104
+ const canvas = document.createElement('canvas');
105
+ canvas.width = 800;
106
+ canvas.height = 600;
107
+ return createWgpuRenderState(canvas);
108
+ }
109
+ export function installWgpuMock() {
110
+ installWgpuConstants();
111
+ const gpu = {
112
+ getPreferredCanvasFormat: () => 'bgra8unorm',
113
+ requestAdapter: () => Promise.resolve(makeAdapter()),
114
+ };
115
+ // A shared (isolate:false) worker may reach this helper with no navigator present; defineProperty
116
+ // on a missing navigator throws, so ensure one exists before attaching the mock GPU.
117
+ if (globalThis.navigator == null) {
118
+ Object.defineProperty(globalThis, 'navigator', { value: {}, configurable: true, writable: true });
119
+ }
120
+ Object.defineProperty(globalThis.navigator, 'gpu', {
121
+ value: gpu,
122
+ configurable: true,
123
+ writable: true,
124
+ });
125
+ const origGetContext = HTMLCanvasElement.prototype.getContext;
126
+ HTMLCanvasElement.prototype.getContext = function (contextId, options) {
127
+ if (contextId === 'webgpu') {
128
+ return {
129
+ configure: () => { },
130
+ getCurrentTexture: () => makeTexture(),
131
+ };
132
+ }
133
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
134
+ return origGetContext.call(this, contextId, options);
135
+ };
136
+ }
137
+ //# sourceMappingURL=wgpuTestHelper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wgpuTestHelper.js","sourceRoot":"","sources":["../src/wgpuTestHelper.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@flighthq/render-wgpu",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/displayobject": "0.1.0",
31
+ "@flighthq/geometry": "0.1.0",
32
+ "@flighthq/render": "0.1.0",
33
+ "@flighthq/surface": "0.1.0",
34
+ "@flighthq/types": "0.1.0"
35
+ },
36
+ "devDependencies": {
37
+ "@webgpu/types": "*",
38
+ "typescript": "^5.3.0"
39
+ },
40
+ "description": "WebGPU renderer core: render state, targets, shaders, draw, surface, and background",
41
+ "sideEffects": false
42
+ }
@@ -0,0 +1,49 @@
1
+ import { getWgpuAdapterCapabilities } from './wgpuAdapterCapabilities';
2
+
3
+ function makeAdapter(features: ReadonlyArray<string> = [], limits: Record<string, number> = {}): GPUAdapter {
4
+ return {
5
+ features: new Set(features),
6
+ limits: { maxTextureDimension2D: 8192, ...limits },
7
+ } as unknown as GPUAdapter;
8
+ }
9
+
10
+ describe('getWgpuAdapterCapabilities', () => {
11
+ it('returns false for supportsFloat32Filterable when feature is absent', () => {
12
+ const caps = getWgpuAdapterCapabilities(makeAdapter([]));
13
+ expect(caps.supportsFloat32Filterable).toBe(false);
14
+ });
15
+
16
+ it('returns true for supportsFloat32Filterable when feature is present', () => {
17
+ const caps = getWgpuAdapterCapabilities(makeAdapter(['float32-filterable']));
18
+ expect(caps.supportsFloat32Filterable).toBe(true);
19
+ });
20
+
21
+ it('returns false for supportsTimestampQuery when feature is absent', () => {
22
+ const caps = getWgpuAdapterCapabilities(makeAdapter([]));
23
+ expect(caps.supportsTimestampQuery).toBe(false);
24
+ });
25
+
26
+ it('returns true for supportsTimestampQuery when feature is present', () => {
27
+ const caps = getWgpuAdapterCapabilities(makeAdapter(['timestamp-query']));
28
+ expect(caps.supportsTimestampQuery).toBe(true);
29
+ });
30
+
31
+ it('reads maxTextureDimension2D from adapter limits', () => {
32
+ const caps = getWgpuAdapterCapabilities(makeAdapter([], { maxTextureDimension2D: 16384 }));
33
+ expect(caps.maxTextureDimension2D).toBe(16384);
34
+ });
35
+
36
+ it('falls back to 8192 when maxTextureDimension2D is absent from limits', () => {
37
+ const adapter = {
38
+ features: new Set(),
39
+ limits: {},
40
+ } as unknown as GPUAdapter;
41
+ const caps = getWgpuAdapterCapabilities(adapter);
42
+ expect(caps.maxTextureDimension2D).toBe(8192);
43
+ });
44
+
45
+ it('reports maxSampleCount of 4', () => {
46
+ const caps = getWgpuAdapterCapabilities(makeAdapter([]));
47
+ expect(caps.maxSampleCount).toBe(4);
48
+ });
49
+ });
@@ -0,0 +1,52 @@
1
+ import { renderWgpuBackground, submitWgpuRenderPass } from './wgpuBackground';
2
+ import { getWgpuRenderStateRuntime } from './wgpuRenderState';
3
+ import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
4
+
5
+ beforeAll(() => {
6
+ installWgpuMock();
7
+ });
8
+
9
+ describe('renderWgpuBackground', () => {
10
+ it('opens a render pass on the state', async () => {
11
+ const state = await createWgpuRenderStateForTest();
12
+ const runtime = getWgpuRenderStateRuntime(state);
13
+ expect(runtime.renderPass).toBeNull();
14
+ renderWgpuBackground(state);
15
+ expect(runtime.renderPass).not.toBeNull();
16
+ });
17
+
18
+ it('resets uniformOffset to zero', async () => {
19
+ const state = await createWgpuRenderStateForTest();
20
+ const runtime = getWgpuRenderStateRuntime(state);
21
+ runtime.uniformOffset = 512;
22
+ renderWgpuBackground(state);
23
+ expect(runtime.uniformOffset).toBe(0);
24
+ });
25
+
26
+ it('creates a command encoder', async () => {
27
+ const state = await createWgpuRenderStateForTest();
28
+ renderWgpuBackground(state);
29
+ expect(getWgpuRenderStateRuntime(state).commandEncoder).not.toBeNull();
30
+ });
31
+ });
32
+
33
+ describe('submitWgpuRenderPass', () => {
34
+ it('clears commandEncoder after submit', async () => {
35
+ const state = await createWgpuRenderStateForTest();
36
+ renderWgpuBackground(state);
37
+ submitWgpuRenderPass(state);
38
+ expect(getWgpuRenderStateRuntime(state).commandEncoder).toBeNull();
39
+ });
40
+
41
+ it('clears renderPass after submit', async () => {
42
+ const state = await createWgpuRenderStateForTest();
43
+ renderWgpuBackground(state);
44
+ submitWgpuRenderPass(state);
45
+ expect(getWgpuRenderStateRuntime(state).renderPass).toBeNull();
46
+ });
47
+
48
+ it('is safe to call without an open pass', async () => {
49
+ const state = await createWgpuRenderStateForTest();
50
+ expect(() => submitWgpuRenderPass(state)).not.toThrow();
51
+ });
52
+ });
@@ -0,0 +1,174 @@
1
+ import { createBitmap } from '@flighthq/displayobject';
2
+ import { getOrCreateRenderProxy2D, prepareDisplayObjectRender } from '@flighthq/render';
3
+ import { BlendMode, ColorTransformMaterialKind, UniformColorTransformMaterialKind } from '@flighthq/types';
4
+
5
+ import { renderWgpuBackground, submitWgpuRenderPass } from './wgpuBackground';
6
+ import {
7
+ applyWgpuBlendMode,
8
+ bindWgpuTexture,
9
+ buildWgpuRenderTargetBindGroup,
10
+ createWgpuTextureEntry,
11
+ drawWgpuQuad,
12
+ drawWgpuQuadWithTransform,
13
+ enableWgpuBlendModeSupport,
14
+ getWgpuRenderProxyColorTransform,
15
+ submitWgpuQuadDraw,
16
+ updateWgpuTextureEntry,
17
+ warmWgpuPipelines,
18
+ } from './wgpuDraw';
19
+ import { getWgpuRenderStateRuntime } from './wgpuRenderState';
20
+ import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
21
+
22
+ beforeAll(() => {
23
+ installWgpuMock();
24
+ });
25
+
26
+ describe('applyWgpuBlendMode', () => {
27
+ it('updates currentBlendMode on the state', async () => {
28
+ const state = await createWgpuRenderStateForTest();
29
+ applyWgpuBlendMode(state, BlendMode.Add);
30
+ expect(getWgpuRenderStateRuntime(state).currentBlendMode).toBe(BlendMode.Add);
31
+ });
32
+
33
+ it('accepts null', async () => {
34
+ const state = await createWgpuRenderStateForTest();
35
+ applyWgpuBlendMode(state, null);
36
+ expect(getWgpuRenderStateRuntime(state).currentBlendMode).toBeNull();
37
+ });
38
+ });
39
+
40
+ describe('bindWgpuTexture', () => {
41
+ it('creates and caches a texture entry', async () => {
42
+ const state = await createWgpuRenderStateForTest();
43
+ const canvas = document.createElement('canvas');
44
+ canvas.width = 8;
45
+ canvas.height = 8;
46
+ const entry = bindWgpuTexture(state, canvas);
47
+ expect(entry).toBeDefined();
48
+ expect(entry.texture).toBeDefined();
49
+ expect(bindWgpuTexture(state, canvas)).toBe(entry);
50
+ });
51
+ });
52
+
53
+ describe('buildWgpuRenderTargetBindGroup', () => {
54
+ it('returns a bind group for a view', async () => {
55
+ const state = await createWgpuRenderStateForTest();
56
+ const fakeView = {} as GPUTextureView;
57
+ const bindGroup = buildWgpuRenderTargetBindGroup(state, fakeView);
58
+ expect(bindGroup).toBeDefined();
59
+ });
60
+ });
61
+
62
+ describe('createWgpuTextureEntry', () => {
63
+ it('creates a texture entry from a canvas', async () => {
64
+ const state = await createWgpuRenderStateForTest();
65
+ const canvas = document.createElement('canvas');
66
+ canvas.width = 4;
67
+ canvas.height = 4;
68
+ const entry = createWgpuTextureEntry(state, 4, 4, canvas);
69
+ expect(entry.texture).toBeDefined();
70
+ expect(entry.bindGroup).toBeDefined();
71
+ });
72
+ });
73
+
74
+ describe('drawWgpuQuad', () => {
75
+ it('does not throw when render pass is open', async () => {
76
+ const state = await createWgpuRenderStateForTest();
77
+ renderWgpuBackground(state);
78
+ const bitmap = createBitmap();
79
+ prepareDisplayObjectRender(state, bitmap);
80
+ const renderProxy = getOrCreateRenderProxy2D(state, bitmap);
81
+ const canvas = document.createElement('canvas');
82
+ canvas.width = 4;
83
+ canvas.height = 4;
84
+ const entry = bindWgpuTexture(state, canvas);
85
+ expect(() => drawWgpuQuad(state, renderProxy, entry, 0, 0, 4, 4, 0, 0, 1, 1)).not.toThrow();
86
+ submitWgpuRenderPass(state);
87
+ });
88
+ });
89
+
90
+ describe('drawWgpuQuadWithTransform', () => {
91
+ it('does not throw when render pass is open', async () => {
92
+ const state = await createWgpuRenderStateForTest();
93
+ renderWgpuBackground(state);
94
+ const bitmap = createBitmap();
95
+ prepareDisplayObjectRender(state, bitmap);
96
+ const renderProxy = getOrCreateRenderProxy2D(state, bitmap);
97
+ const canvas = document.createElement('canvas');
98
+ canvas.width = 4;
99
+ canvas.height = 4;
100
+ const entry = bindWgpuTexture(state, canvas);
101
+ const t = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
102
+ expect(() => drawWgpuQuadWithTransform(state, renderProxy, t, entry, 0, 0, 4, 4, 0, 0, 1, 1)).not.toThrow();
103
+ submitWgpuRenderPass(state);
104
+ });
105
+ });
106
+
107
+ describe('enableWgpuBlendModeSupport', () => {
108
+ it('sets applyBlendMode', async () => {
109
+ const state = await createWgpuRenderStateForTest();
110
+ enableWgpuBlendModeSupport(state);
111
+ expect(state.applyBlendMode).not.toBeNull();
112
+ });
113
+ });
114
+
115
+ describe('getWgpuRenderProxyColorTransform', () => {
116
+ it('returns null when the node has no material', () => {
117
+ expect(getWgpuRenderProxyColorTransform({ material: null } as never)).toBeNull();
118
+ });
119
+
120
+ it('returns the value carried on a uniform color transform material', () => {
121
+ const colorTransform = { redMultiplier: 0.5 };
122
+ const node = { material: { kind: UniformColorTransformMaterialKind, colorTransform } } as never;
123
+ expect(getWgpuRenderProxyColorTransform(node)).toBe(colorTransform);
124
+ });
125
+
126
+ it('returns the per-node material data for a color transform material', () => {
127
+ const colorTransform = { redMultiplier: 0.5 };
128
+ const node = { material: { kind: ColorTransformMaterialKind }, materialData: colorTransform } as never;
129
+ expect(getWgpuRenderProxyColorTransform(node)).toBe(colorTransform);
130
+ });
131
+ });
132
+
133
+ describe('submitWgpuQuadDraw', () => {
134
+ it('does not throw when render pass is open', async () => {
135
+ const state = await createWgpuRenderStateForTest();
136
+ renderWgpuBackground(state);
137
+ const canvas = document.createElement('canvas');
138
+ canvas.width = 4;
139
+ canvas.height = 4;
140
+ const entry = bindWgpuTexture(state, canvas);
141
+ expect(() => submitWgpuQuadDraw(state, 0, entry.bindGroup)).not.toThrow();
142
+ submitWgpuRenderPass(state);
143
+ });
144
+
145
+ it('is a no-op when render pass is null', async () => {
146
+ const state = await createWgpuRenderStateForTest();
147
+ const canvas = document.createElement('canvas');
148
+ canvas.width = 4;
149
+ canvas.height = 4;
150
+ const entry = bindWgpuTexture(state, canvas);
151
+ expect(() => submitWgpuQuadDraw(state, 0, entry.bindGroup)).not.toThrow();
152
+ });
153
+ });
154
+
155
+ describe('updateWgpuTextureEntry', () => {
156
+ it('does not throw when called with a canvas', async () => {
157
+ const state = await createWgpuRenderStateForTest();
158
+ const canvas = document.createElement('canvas');
159
+ canvas.width = 4;
160
+ canvas.height = 4;
161
+ const entry = createWgpuTextureEntry(state, 4, 4, canvas);
162
+ expect(() => updateWgpuTextureEntry(state, entry, canvas)).not.toThrow();
163
+ });
164
+ });
165
+
166
+ describe('warmWgpuPipelines', () => {
167
+ it('pre-populates the pipeline cache', async () => {
168
+ const state = await createWgpuRenderStateForTest();
169
+ const runtime = getWgpuRenderStateRuntime(state);
170
+ const before = runtime.pipelineCache.size;
171
+ warmWgpuPipelines(state);
172
+ expect(runtime.pipelineCache.size).toBeGreaterThanOrEqual(before);
173
+ });
174
+ });
@@ -0,0 +1,21 @@
1
+ import { createWgpuCanvasElement } from './wgpuElement';
2
+
3
+ describe('createWgpuCanvasElement', () => {
4
+ it('sets CSS dimensions from width and height', () => {
5
+ const canvas = createWgpuCanvasElement(400, 300);
6
+ expect(canvas.style.width).toBe('400px');
7
+ expect(canvas.style.height).toBe('300px');
8
+ });
9
+
10
+ it('scales pixel dimensions by pixelRatio', () => {
11
+ const canvas = createWgpuCanvasElement(200, 100, 2);
12
+ expect(canvas.width).toBe(400);
13
+ expect(canvas.height).toBe(200);
14
+ });
15
+
16
+ it('defaults pixelRatio to 1', () => {
17
+ const canvas = createWgpuCanvasElement(300, 200);
18
+ expect(canvas.width).toBe(300);
19
+ expect(canvas.height).toBe(200);
20
+ });
21
+ });
@@ -0,0 +1,64 @@
1
+ import { renderWgpuBackground, submitWgpuRenderPass } from './wgpuBackground';
2
+ import {
3
+ createWgpuFullscreenPipeline,
4
+ destroyWgpuFullscreenPipeline,
5
+ drawWgpuFullscreenPass,
6
+ } from './wgpuFullscreenPass';
7
+ import { createWgpuRenderTarget } from './wgpuRenderTarget';
8
+ import { createWgpuRenderStateForTest, installWgpuMock } from './wgpuTestHelper';
9
+
10
+ beforeAll(() => {
11
+ installWgpuMock();
12
+ });
13
+
14
+ const SIMPLE_FRAGMENT_WGSL = /* wgsl */ `
15
+ @group(1) @binding(0) var tex : texture_2d<f32>;
16
+ @group(1) @binding(1) var smp : sampler;
17
+ @fragment
18
+ fn fs_main(@builtin(position) pos : vec4f) -> @location(0) vec4f {
19
+ return textureSample(tex, smp, pos.xy);
20
+ }
21
+ `;
22
+
23
+ describe('createWgpuFullscreenPipeline', () => {
24
+ it('returns a pipeline with a layout and bind group layouts', async () => {
25
+ const state = await createWgpuRenderStateForTest();
26
+ const result = createWgpuFullscreenPipeline(state, SIMPLE_FRAGMENT_WGSL, 1);
27
+ expect(result.pipeline).toBeDefined();
28
+ expect(result.pipelineLayout).toBeDefined();
29
+ expect(result.uniformBindGroupLayout).toBeDefined();
30
+ expect(result.textureBindGroupLayouts.length).toBe(1);
31
+ });
32
+
33
+ it('builds multiple texture bind group layouts when textureInputCount > 1', async () => {
34
+ const state = await createWgpuRenderStateForTest();
35
+ const result = createWgpuFullscreenPipeline(state, SIMPLE_FRAGMENT_WGSL, 3);
36
+ expect(result.textureBindGroupLayouts.length).toBe(3);
37
+ });
38
+ });
39
+
40
+ describe('destroyWgpuFullscreenPipeline', () => {
41
+ it('does not throw', async () => {
42
+ const state = await createWgpuRenderStateForTest();
43
+ const pipeline = createWgpuFullscreenPipeline(state, SIMPLE_FRAGMENT_WGSL, 1);
44
+ expect(() => destroyWgpuFullscreenPipeline(state, pipeline)).not.toThrow();
45
+ });
46
+ });
47
+
48
+ describe('drawWgpuFullscreenPass', () => {
49
+ it('does not throw when render pass is open and no uniforms', async () => {
50
+ const state = await createWgpuRenderStateForTest();
51
+ renderWgpuBackground(state);
52
+ const pipeline = createWgpuFullscreenPipeline(state, SIMPLE_FRAGMENT_WGSL, 1);
53
+ const target = createWgpuRenderTarget(state, 64, 64);
54
+ expect(() => drawWgpuFullscreenPass(state, pipeline, [target], null, null)).not.toThrow();
55
+ submitWgpuRenderPass(state);
56
+ });
57
+
58
+ it('is a no-op when render pass is not open', async () => {
59
+ const state = await createWgpuRenderStateForTest();
60
+ const pipeline = createWgpuFullscreenPipeline(state, SIMPLE_FRAGMENT_WGSL, 1);
61
+ const target = createWgpuRenderTarget(state, 64, 64);
62
+ expect(() => drawWgpuFullscreenPass(state, pipeline, [target], null, null)).not.toThrow();
63
+ });
64
+ });
@@ -0,0 +1,56 @@
1
+ import type { Material, WgpuMaterialRenderer, WgpuRenderState } from '@flighthq/types';
2
+ import { DefaultMaterialKind, EntityRuntimeKey } from '@flighthq/types';
3
+
4
+ import {
5
+ getWgpuMaterialRenderer,
6
+ registerWgpuMaterialRenderer,
7
+ resolveWgpuMaterialRenderer,
8
+ } from './wgpuMaterialRegistry';
9
+ import { createWgpuRenderStateRuntime } from './wgpuRenderState';
10
+
11
+ const TestKind = 'TestMaterial';
12
+ const testRenderer: WgpuMaterialRenderer = { instanceFloatCount: 0, getShaderModule: () => ({}) as GPUShaderModule };
13
+
14
+ function makeState(): WgpuRenderState {
15
+ const state = {} as WgpuRenderState;
16
+ state[EntityRuntimeKey] = createWgpuRenderStateRuntime();
17
+ return state;
18
+ }
19
+
20
+ function makeMaterial(kind: string): Material {
21
+ return { kind } as Material;
22
+ }
23
+
24
+ describe('getWgpuMaterialRenderer', () => {
25
+ it('returns null when nothing is registered for the kind', () => {
26
+ expect(getWgpuMaterialRenderer(makeState(), TestKind)).toBeNull();
27
+ });
28
+ });
29
+
30
+ describe('registerWgpuMaterialRenderer', () => {
31
+ it('registers a renderer retrievable by kind', () => {
32
+ const state = makeState();
33
+ registerWgpuMaterialRenderer(state, TestKind, testRenderer);
34
+ expect(getWgpuMaterialRenderer(state, TestKind)).toBe(testRenderer);
35
+ });
36
+ });
37
+
38
+ describe('resolveWgpuMaterialRenderer', () => {
39
+ it('returns null when nothing is registered — no built-in fallback', () => {
40
+ expect(resolveWgpuMaterialRenderer(makeState(), null)).toBeNull();
41
+ expect(resolveWgpuMaterialRenderer(makeState(), makeMaterial(TestKind))).toBeNull();
42
+ });
43
+
44
+ it('returns the registered renderer for a material kind', () => {
45
+ const state = makeState();
46
+ registerWgpuMaterialRenderer(state, TestKind, testRenderer);
47
+ expect(resolveWgpuMaterialRenderer(state, makeMaterial(TestKind))).toBe(testRenderer);
48
+ });
49
+
50
+ it('falls back to the renderer registered for DefaultMaterialKind', () => {
51
+ const state = makeState();
52
+ registerWgpuMaterialRenderer(state, DefaultMaterialKind, testRenderer);
53
+ expect(resolveWgpuMaterialRenderer(state, makeMaterial('Other'))).toBe(testRenderer);
54
+ expect(resolveWgpuMaterialRenderer(state, null)).toBe(testRenderer);
55
+ });
56
+ });