@flighthq/render-wgpu 0.5.0-next.1944.cd6b6e7 → 0.5.1-edge.638.d4dbd4e

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 (80) hide show
  1. package/README.md +2 -2
  2. package/dist/contract.d.ts +3 -0
  3. package/dist/contract.d.ts.map +1 -1
  4. package/dist/contract.js +3 -0
  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/wgpuAntialias.d.ts +8 -4
  11. package/dist/wgpuAntialias.d.ts.map +1 -1
  12. package/dist/wgpuAntialias.js +23 -4
  13. package/dist/wgpuAntialias.js.map +1 -1
  14. package/dist/wgpuBackground.d.ts +10 -4
  15. package/dist/wgpuBackground.d.ts.map +1 -1
  16. package/dist/wgpuBackground.js +87 -3
  17. package/dist/wgpuBackground.js.map +1 -1
  18. package/dist/wgpuCompressedTexture.js +9 -9
  19. package/dist/wgpuCompressedTexture.js.map +1 -1
  20. package/dist/wgpuDeviceLoss.d.ts +21 -0
  21. package/dist/wgpuDeviceLoss.d.ts.map +1 -0
  22. package/dist/wgpuDeviceLoss.js +62 -0
  23. package/dist/wgpuDeviceLoss.js.map +1 -0
  24. package/dist/wgpuDraw.d.ts +0 -1
  25. package/dist/wgpuDraw.d.ts.map +1 -1
  26. package/dist/wgpuDraw.js +73 -96
  27. package/dist/wgpuDraw.js.map +1 -1
  28. package/dist/wgpuElement.d.ts +5 -0
  29. package/dist/wgpuElement.d.ts.map +1 -1
  30. package/dist/wgpuElement.js +17 -6
  31. package/dist/wgpuElement.js.map +1 -1
  32. package/dist/wgpuExternalTexture.d.ts.map +1 -1
  33. package/dist/wgpuExternalTexture.js +12 -11
  34. package/dist/wgpuExternalTexture.js.map +1 -1
  35. package/dist/wgpuFullscreenPass.js +1 -1
  36. package/dist/wgpuFullscreenPass.js.map +1 -1
  37. package/dist/wgpuHost.d.ts +7 -0
  38. package/dist/wgpuHost.d.ts.map +1 -0
  39. package/dist/wgpuHost.js +80 -0
  40. package/dist/wgpuHost.js.map +1 -0
  41. package/dist/wgpuMipmap.js +3 -3
  42. package/dist/wgpuMipmap.js.map +1 -1
  43. package/dist/wgpuPipeline.d.ts +5 -0
  44. package/dist/wgpuPipeline.d.ts.map +1 -0
  45. package/dist/wgpuPipeline.js +29 -0
  46. package/dist/wgpuPipeline.js.map +1 -0
  47. package/dist/wgpuRenderState.d.ts +13 -6
  48. package/dist/wgpuRenderState.d.ts.map +1 -1
  49. package/dist/wgpuRenderState.js +220 -271
  50. package/dist/wgpuRenderState.js.map +1 -1
  51. package/dist/wgpuRenderTarget.d.ts.map +1 -1
  52. package/dist/wgpuRenderTarget.js +6 -5
  53. package/dist/wgpuRenderTarget.js.map +1 -1
  54. package/dist/wgpuRenderTexture.js +4 -4
  55. package/dist/wgpuRenderTexture.js.map +1 -1
  56. package/dist/wgpuShader.d.ts.map +1 -1
  57. package/dist/wgpuShader.js +6 -4
  58. package/dist/wgpuShader.js.map +1 -1
  59. package/dist/wgpuSurface.d.ts +5 -5
  60. package/dist/wgpuSurface.d.ts.map +1 -1
  61. package/dist/wgpuSurface.js +2 -2
  62. package/dist/wgpuSurface.js.map +1 -1
  63. package/dist/wgpuTestHelper.d.ts +2 -2
  64. package/dist/wgpuTestHelper.d.ts.map +1 -1
  65. package/dist/wgpuTestHelper.js +6 -2
  66. package/dist/wgpuTestHelper.js.map +1 -1
  67. package/package.json +11 -11
  68. package/src/wgpuAntialias.test.ts +26 -4
  69. package/src/wgpuBackground.test.ts +43 -73
  70. package/src/wgpuDeviceLoss.test.ts +240 -0
  71. package/src/wgpuDraw.test.ts +99 -24
  72. package/src/wgpuElement.test.ts +127 -13
  73. package/src/wgpuHost.test.ts +227 -0
  74. package/src/wgpuMaterialRegistry.test.ts +6 -3
  75. package/src/wgpuMipmap.test.ts +3 -3
  76. package/src/wgpuPipeline.test.ts +52 -0
  77. package/src/wgpuRenderState.test.ts +679 -88
  78. package/src/wgpuRenderTarget.test.ts +12 -8
  79. package/src/wgpuRenderTexture.test.ts +1 -1
  80. package/src/wgpuShader.test.ts +2 -2
@@ -1,107 +1,96 @@
1
+ import { createEntity } from '@flighthq/entity/contract';
1
2
  import { createMatrix } from '@flighthq/geometry/contract';
2
- import { createKeyedTable, createSlotTable } from '@flighthq/registry/contract';
3
- import { copyAllRenderersFromRenderState, copyRenderStateRegistrations, createRenderState as _createRenderState, createRenderStateRuntime, destroyRenderState, setRenderStateBackgroundColor, } from '@flighthq/render/contract';
3
+ import { createRenderState as _createRenderState, createRenderStateRuntime, destroyRenderState, setRenderStateBackgroundColor, } from '@flighthq/render/contract';
4
4
  import { EntityRuntimeKey, RegistryEntryState } from '@flighthq/types/contract';
5
+ import { observeWgpuDeviceLoss } from './wgpuDeviceLoss';
5
6
  import { warmWgpuPipelines } from './wgpuDraw';
7
+ import { getWgpuHostBackend } from './wgpuHost';
8
+ import { createEmptyWgpuRegistries, createWgpuPipeline } from './wgpuPipeline';
6
9
  import { createWgpuBindGroupLayouts, UNIFORM_BYTE_SIZE } from './wgpuShader';
7
10
  // Ring buffer: 4096 draw slots per frame. Stride is clamped to at least 256 by the spec.
8
11
  const RING_SLOT_COUNT = 4096;
9
- // Explicit snapshot re-copy. Device resources remain shared. Persistent tables share immutable
10
- // snapshots through distinct aggregates, so either pipeline can
11
- // still replace registration policy independently after derivation.
12
- export function copyWgpuRenderStateRegistrations(target, source) {
13
- const targetRuntime = getWgpuRenderStateRuntime(target);
12
+ // Acquires host handles the CALLER owns. Ownership is `caller`, so Flight never releases these: the caller
13
+ // ends their life with `releaseWgpuAcquisition`, and a state built on them leaves them intact when it is
14
+ // destroyed. Returns `null` rather than throwing, because "this environment has no WebGPU" is an expected
15
+ // outcome and not a programmer error.
16
+ export async function createWgpuAcquisitionFromCanvasElement(canvas, options = {}) {
17
+ try {
18
+ const acquired = await getWgpuHostBackend().acquire(canvas, options);
19
+ return { ...acquired, ownership: 'caller' };
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ export function createWgpuDeviceState(device) {
26
+ const deviceRuntime = createMinimalDeviceRuntime(device);
27
+ const state = createEntity({ device });
28
+ state[EntityRuntimeKey] = deviceRuntime;
29
+ return state;
30
+ }
31
+ export function createWgpuOffscreenRenderState(sourceOrDeviceState, pipeline, options = {}) {
32
+ if (pipeline !== undefined)
33
+ return initializeWgpuDeviceRenderState(sourceOrDeviceState, pipeline, options);
34
+ const source = sourceOrDeviceState;
14
35
  const sourceRuntime = getWgpuRenderStateRuntime(source);
15
- // Blend-mode wiring belongs to the screen pipeline and may be enabled after a derived state is
16
- // created. Keep the entity field plain; resolve its explicit parent at each draw seam.
17
- target.applyBlendMode = null;
18
- targetRuntime.applyBlendModeParent = source;
19
- targetRuntime.defaultBitmapShader = sourceRuntime.defaultBitmapShader;
20
- targetRuntime.mipmapDegradedGuard = sourceRuntime.mipmapDegradedGuard;
21
- targetRuntime.mipmapGenerator = sourceRuntime.mipmapGenerator;
22
- targetRuntime.webgpuShaderBindingResolver = sourceRuntime.webgpuShaderBindingResolver;
23
- targetRuntime.registries = {
24
- colorAdjustmentFeature: sourceRuntime.registries.colorAdjustmentFeature,
25
- colorAdjustmentFeatureGuard: sourceRuntime.registries.colorAdjustmentFeatureGuard,
26
- compressedTextureDecoder: sourceRuntime.registries.compressedTextureDecoder,
27
- compressedTextureUpload: sourceRuntime.registries.compressedTextureUpload,
28
- customMaterialShaders: sourceRuntime.registries.customMaterialShaders,
29
- materialRenderers: sourceRuntime.registries.materialRenderers,
30
- meshMaterialRenderers: sourceRuntime.registries.meshMaterialRenderers,
31
- modifierSnippets: sourceRuntime.registries.modifierSnippets,
32
- modifierSnippetRevision: sourceRuntime.registries.modifierSnippetRevision,
33
- renderEffects: sourceRuntime.registries.renderEffects,
34
- renderers: targetRuntime.registries.renderers,
35
- shapeRasterizer: sourceRuntime.registries.shapeRasterizer,
36
- strokeTessellator: targetRuntime.registries.strokeTessellator,
37
- textureResolvers: sourceRuntime.registries.textureResolvers,
38
- velocityWriters: sourceRuntime.registries.velocityWriters,
39
- };
40
- targetRuntime.wgpuRenderTextureGuard = sourceRuntime.wgpuRenderTextureGuard;
41
- copyRenderStateRegistrations(target, source);
36
+ const lost = sourceRuntime.context.lost;
37
+ if (lost !== null)
38
+ return { reason: 'device-lost', info: lost };
39
+ const derivedPipeline = createWgpuPipeline(sourceRuntime.registries);
40
+ const state = initializeWgpuDeviceRenderState(source.deviceState, derivedPipeline, {
41
+ backgroundColor: source.backgroundColor,
42
+ format: source.format,
43
+ imageSmoothingEnabled: source.allowSmoothing,
44
+ pixelRatio: source.pixelRatio,
45
+ roundPixels: source.roundPixels,
46
+ sceneGraphSyncPolicy: source.sceneGraphSyncPolicy,
47
+ });
48
+ const runtime = getWgpuRenderStateRuntime(state);
49
+ runtime.applyBlendModeParent = source;
50
+ runtime.defaultBitmapShader = sourceRuntime.defaultBitmapShader;
51
+ runtime.mipmapDegradedGuard = sourceRuntime.mipmapDegradedGuard;
52
+ runtime.mipmapGenerator = sourceRuntime.mipmapGenerator;
53
+ runtime.webgpuShaderBindingResolver = sourceRuntime.webgpuShaderBindingResolver;
54
+ runtime.wgpuRenderTextureGuard = sourceRuntime.wgpuRenderTextureGuard;
55
+ return { reason: 'ok', state };
42
56
  }
43
- /**
44
- * Creates a second render pipeline over `screenState`'s GPUDevice.
45
- *
46
- * The state receives a fresh command encoder, uniform ring, traversal/proxy tree, batch writer, clip
47
- * stack, and render transform. Immutable pipelines/layouts/samplers plus uploaded textures and render
48
- * texture realizations share a device tier. Registrations are a creation-time snapshot.
49
- */
50
- export function createWgpuOffscreenRenderState(screenState) {
51
- const state = _createRenderState({
52
- allowSmoothing: screenState.allowSmoothing,
53
- backgroundColor: screenState.backgroundColor,
54
- backgroundColorRgba: [...screenState.backgroundColorRgba],
55
- backgroundColorString: screenState.backgroundColorString,
56
- pixelRatio: screenState.pixelRatio,
57
- renderAlpha: screenState.renderAlpha,
58
- renderBlendMode: screenState.renderBlendMode,
59
- renderTransform2D: createMatrix(),
60
- roundPixels: screenState.roundPixels,
61
- sceneGraphSyncPolicy: screenState.sceneGraphSyncPolicy,
57
+ // Synchronous: with the handles already in hand there is nothing left to await. Everything asynchronous
58
+ // lives in acquisition.
59
+ export function createWgpuRenderState(acquisition, pipeline, options = {}) {
60
+ const hostBackend = getWgpuHostBackend();
61
+ try {
62
+ return initializeWgpuPresentationRenderState(options, acquisition, pipeline, hostBackend);
63
+ }
64
+ catch (error) {
65
+ // ★ NEVER release what the caller owns, not even while unwinding a failure. This is the path where
66
+ // borrowed handles were previously destroyed on the way out, and it was safe only because the shipped
67
+ // backend happened to check. The policy belongs here, where it holds for every backend.
68
+ if (acquisition.ownership !== 'caller')
69
+ hostBackend.release(acquisition);
70
+ throw error;
71
+ }
72
+ }
73
+ // The canvas convenience: acquire, then build. The handles are `flight`-owned, so destroying the state
74
+ // releases them — the behavior every existing caller had when it passed a canvas.
75
+ export async function createWgpuRenderStateFromCanvasElement(canvas, pipeline, options = {}) {
76
+ const acquisition = await getWgpuHostBackend().acquire(canvas, {
77
+ format: options.format,
78
+ powerPreference: options.powerPreference,
62
79
  });
63
- state.applyBlendMode = null;
64
- state.canvas = screenState.canvas;
65
- state.context = screenState.context;
66
- state.device = screenState.device;
67
- state.format = screenState.format;
68
- const screenRuntime = getWgpuRenderStateRuntime(screenState);
69
- const runtime = createWgpuRenderStateRuntime(screenRuntime);
70
- state[EntityRuntimeKey] = runtime;
71
- initializeOffscreenWgpuRuntime(state, runtime, screenRuntime);
72
- copyAllRenderersFromRenderState(state, screenState);
73
- copyWgpuRenderStateRegistrations(state, screenState);
74
- warmWgpuPipelines(state);
75
- return state;
80
+ return createWgpuRenderState(acquisition, pipeline, options);
81
+ }
82
+ export function createWgpuRenderStateRuntime(deviceStateOrRuntime, pipeline) {
83
+ const deviceRuntime = EntityRuntimeKey in deviceStateOrRuntime
84
+ ? getWgpuDeviceRuntime(deviceStateOrRuntime)
85
+ : deviceStateOrRuntime.context;
86
+ const resolvedPipeline = pipeline ??
87
+ createWgpuPipeline(EntityRuntimeKey in deviceStateOrRuntime
88
+ ? createEmptyWgpuRegistries()
89
+ : deviceStateOrRuntime.registries);
90
+ return createWgpuRenderStateRuntimeInternal(deviceRuntime, resolvedPipeline);
76
91
  }
77
- export async function createWgpuRenderState(canvas, options = {}) {
78
- if (!navigator.gpu)
79
- throw new Error('WebGPU is not supported in this browser.');
80
- const adapter = await navigator.gpu.requestAdapter(options.powerPreference != null ? { powerPreference: options.powerPreference } : undefined);
81
- if (!adapter)
82
- throw new Error('Failed to get WebGPU adapter.');
83
- // The forward-lit 3D pipeline binds 5 groups (Frame, Draw, Material, Shadow, Ibl); WebGPU's
84
- // guaranteed baseline maxBindGroups is only 4, so request 5 when the adapter allows it. Guarded by
85
- // the adapter limit: requiredLimits above the adapter's support makes requestDevice reject, and a
86
- // baseline-4 adapter simply keeps 4 (the 5-group lit pipeline is unavailable there until shadow+IBL
87
- // are folded into one group — a portability follow-up). 2D and unlit paths use ≤4 groups regardless.
88
- const requiredLimits = {};
89
- if (adapter.limits.maxBindGroups >= 5)
90
- requiredLimits.maxBindGroups = 5;
91
- // Compression features must be enabled at device creation; there is no later extension activation
92
- // like WebGL. Enable every family the adapter exposes so the opt-in compressed uploader can use the
93
- // native path when it is registered, while unsupported families retain their CPU decode fallback.
94
- const requiredFeatures = ['texture-compression-bc', 'texture-compression-etc2', 'texture-compression-astc'].filter((feature) => adapter.features.has(feature));
95
- const deviceDescriptor = {};
96
- if (Object.keys(requiredLimits).length > 0)
97
- deviceDescriptor.requiredLimits = requiredLimits;
98
- if (requiredFeatures.length > 0)
99
- deviceDescriptor.requiredFeatures = requiredFeatures;
100
- const device = await adapter.requestDevice(deviceDescriptor);
101
- const format = options.format ?? navigator.gpu.getPreferredCanvasFormat();
102
- const context = canvas.getContext('webgpu');
103
- if (!context)
104
- throw new Error('Failed to get WebGPU canvas context.');
92
+ function initializeWgpuPresentationRenderState(options, acquisition, pipeline, hostBackend) {
93
+ const { context, device, format } = acquisition;
105
94
  // COPY_SRC lets the canvas texture be read back via copyTextureToBuffer (createBitmapFromWgpuRenderState).
106
95
  // It is the only reliable way to read a Wgpu frame in headless/software contexts, where canvas
107
96
  // presentation does not surface the swapchain; it also backs user-facing screenshot/save-pixels needs.
@@ -111,7 +100,19 @@ export async function createWgpuRenderState(canvas, options = {}) {
111
100
  alphaMode: 'premultiplied',
112
101
  usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
113
102
  });
114
- // Align uniform ring buffer slots to device limits
103
+ const deviceState = createWgpuDeviceState(device);
104
+ const ownership = { acquisition, hostBackend, references: 0 };
105
+ _acquisitionByDeviceRuntime.set(getWgpuDeviceRuntime(deviceState), ownership);
106
+ const state = initializeWgpuDeviceRenderState(deviceState, pipeline, { ...options, format });
107
+ Object.assign(state, { context, surface: acquisition.surface });
108
+ getWgpuRenderStateRuntime(state).surfaceAntialiasEnabled = options.antialias ?? false;
109
+ return state;
110
+ }
111
+ function initializeWgpuDeviceRenderState(deviceState, pipeline, options) {
112
+ const device = deviceState.device;
113
+ const format = options.format ?? 'bgra8unorm';
114
+ const deviceRuntime = getWgpuDeviceRuntime(deviceState);
115
+ initializeWgpuDeviceRuntime(deviceRuntime);
115
116
  const uniformStride = Math.max(256, device.limits.minUniformBufferOffsetAlignment, UNIFORM_BYTE_SIZE);
116
117
  const ringByteSize = uniformStride * RING_SLOT_COUNT;
117
118
  const uniformBuffer = device.createBuffer({
@@ -119,24 +120,10 @@ export async function createWgpuRenderState(canvas, options = {}) {
119
120
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
120
121
  });
121
122
  const uniformData = new Float32Array(ringByteSize / 4);
122
- const uniformDataU32 = new Uint32Array(uniformData.buffer);
123
- const { uniformBindGroupLayout, textureBindGroupLayout } = createWgpuBindGroupLayouts(device);
124
123
  const uniformBindGroup = device.createBindGroup({
125
- layout: uniformBindGroupLayout,
124
+ layout: deviceRuntime.uniformBindGroupLayout,
126
125
  entries: [{ binding: 0, resource: { buffer: uniformBuffer, size: UNIFORM_BYTE_SIZE } }],
127
126
  });
128
- const linearSampler = device.createSampler({
129
- minFilter: 'linear',
130
- magFilter: 'linear',
131
- addressModeU: 'clamp-to-edge',
132
- addressModeV: 'clamp-to-edge',
133
- });
134
- const nearestSampler = device.createSampler({
135
- minFilter: 'nearest',
136
- magFilter: 'nearest',
137
- addressModeU: 'clamp-to-edge',
138
- addressModeV: 'clamp-to-edge',
139
- });
140
127
  const state = _createRenderState({
141
128
  allowSmoothing: options.imageSmoothingEnabled ?? true,
142
129
  pixelRatio: options.pixelRatio ?? 1,
@@ -147,36 +134,21 @@ export async function createWgpuRenderState(canvas, options = {}) {
147
134
  if (options.backgroundColor != null)
148
135
  setRenderStateBackgroundColor(state, options.backgroundColor);
149
136
  state.applyBlendMode = null;
150
- state.canvas = canvas;
151
- state.context = context;
152
- state.device = device;
153
- state.format = format;
154
- const runtime = createWgpuRenderStateRuntime();
137
+ Object.assign(state, { deviceState, device, format, pipeline });
138
+ const runtime = createWgpuRenderStateRuntime(deviceState, pipeline);
155
139
  state[EntityRuntimeKey] = runtime;
156
- runtime.surfaceAntialiasEnabled = options.antialias ?? false;
140
+ runtime.surfaceAntialiasEnabled = false;
157
141
  runtime.currentBlendMode = null;
158
142
  runtime.currentRenderTarget = null;
159
- runtime.uniformBindGroupLayout = uniformBindGroupLayout;
160
- runtime.textureBindGroupLayout = textureBindGroupLayout;
161
143
  runtime.uniformBuffer = uniformBuffer;
162
144
  runtime.uniformData = uniformData;
163
- runtime.uniformDataU32 = uniformDataU32;
145
+ runtime.uniformDataU32 = new Uint32Array(uniformData.buffer);
164
146
  runtime.uniformOffset = 0;
165
147
  runtime.uniformStride = uniformStride;
166
148
  runtime.uniformBindGroup = uniformBindGroup;
167
149
  runtime.matrixArray = new Float32Array(9);
168
- runtime.pipelineCache = new Map();
169
- runtime.linearSampler = linearSampler;
170
- runtime.nearestSampler = nearestSampler;
171
- runtime.samplerCache = new Map();
172
150
  runtime.mipmapDegradedGuard = null;
173
151
  runtime.mipmapGenerator = null;
174
- runtime.mipmapPipelineCache = new Map();
175
- runtime.textureCache = new WeakMap();
176
- runtime.textureSourcePremultipliedTextureCache = new WeakMap();
177
- runtime.textureSourcePremultipliedSrgbTextureCache = new WeakMap();
178
- runtime.textureSourceStraightTextureCache = new WeakMap();
179
- runtime.textureSourceStraightSrgbTextureCache = new WeakMap();
180
152
  runtime.defaultBitmapShader = null;
181
153
  runtime.particleInstanceBuffer = null;
182
154
  runtime.particleInstanceData = null;
@@ -191,14 +163,18 @@ export async function createWgpuRenderState(canvas, options = {}) {
191
163
  runtime.quadBatchWriterTexture = null;
192
164
  runtime.quadBatchWriterSampler = null;
193
165
  runtime.quadBatchWriterSmoothing = null;
194
- // Color-adjustment fold state (mode/data + the folded module) is not allocated here: it is owned by
195
- // the opt-in registerWgpuColorAdjustmentMaterialFeature, so a state that never tints carries none of it.
196
166
  runtime.quadBatchWriterBufferPool = [];
197
167
  runtime.quadBatchWriterBufferCursor = 0;
198
168
  runtime.commandEncoder = null;
199
169
  runtime.renderPass = null;
200
170
  runtime.canvasTextureView = null;
201
171
  runtime.canvasViewCleared = false;
172
+ runtime.frameCaptureEnabled = false;
173
+ runtime.frameCaptureTexture = null;
174
+ runtime.frameCaptureBuffer = null;
175
+ runtime.frameCaptureBytesPerRow = 0;
176
+ runtime.frameCaptureWidth = 0;
177
+ runtime.frameCaptureHeight = 0;
202
178
  runtime.depthStencilTexture = null;
203
179
  runtime.depthStencilView = null;
204
180
  runtime.depthStencilWidth = 0;
@@ -216,66 +192,23 @@ export async function createWgpuRenderState(canvas, options = {}) {
216
192
  warmWgpuPipelines(state);
217
193
  return state;
218
194
  }
219
- // Allocates the package-private GPU runtime for a WgpuRenderState. createWgpuRenderState attaches
220
- // one to each state under EntityRuntimeKey and populates its fields; getWgpuRenderStateRuntime reads
221
- // it back. The render path writes the returned object every frame, so the return is intentionally
222
- // mutable (not Readonly).
223
- export function createWgpuRenderStateRuntime(sharedRuntime) {
224
- const runtime = createRenderStateRuntime();
225
- runtime.applyBlendModeParent = null;
226
- runtime.surfaceAntialiasEnabled = false;
227
- runtime.surfaceAntialiasTexture = null;
228
- runtime.surfaceAntialiasView = null;
229
- runtime.surfaceAntialiasWidth = 0;
230
- runtime.surfaceAntialiasHeight = 0;
231
- runtime.surfaceAntialiasResolveBindGroupLayout = null;
232
- runtime.surfaceAntialiasResolvePipeline = null;
233
- runtime.surfaceAntialiasResolveBindGroup = null;
234
- runtime.surfacePresentationView = null;
235
- runtime.registries = {
236
- compressedTextureDecoder: createSlotTable('WgpuCompressedTextureDecoder', 'Unregistered'),
237
- compressedTextureUpload: createSlotTable('WgpuCompressedTextureUpload', 'Unregistered'),
238
- customMaterialShaders: createKeyedTable('WgpuCustomMaterialShader', 'Unregistered'),
239
- materialRenderers: createKeyedTable('WgpuMaterialRenderer', 'StandardMaterial'),
240
- meshMaterialRenderers: createKeyedTable('WgpuMeshMaterialRenderer', 'StandardMaterial'),
241
- modifierSnippets: createKeyedTable('WgpuModifierSnippet', 'Unregistered'),
242
- modifierSnippetRevision: 0,
243
- renderEffects: createKeyedTable('WgpuRenderEffect', 'Unregistered'),
244
- renderers: runtime.registries.renderers,
245
- shapeRasterizer: createSlotTable('WgpuShapeRasterizer', 'Unregistered'),
246
- strokeTessellator: runtime.registries.strokeTessellator,
247
- textureResolvers: createKeyedTable('WgpuTextureResolver', 'Unregistered'),
248
- velocityWriters: createKeyedTable('WgpuVelocityWriter', 'Unregistered'),
249
- };
250
- const deviceRuntime = sharedRuntime === undefined ? { fields: {}, references: 0 } : getWgpuDeviceRuntime(sharedRuntime);
251
- deviceRuntime.references++;
252
- _deviceRuntimeByStateRuntime.set(runtime, deviceRuntime);
253
- for (const key of WGPU_DEVICE_RUNTIME_KEYS) {
254
- Object.defineProperty(runtime, key, {
255
- configurable: true,
256
- enumerable: true,
257
- get: () => deviceRuntime.fields[key],
258
- set: (value) => {
259
- deviceRuntime.fields[key] = value;
260
- },
261
- });
262
- }
263
- return runtime;
264
- }
265
195
  // Destroys the GPU buffers and textures createWgpuRenderState (and the lazy quad-batch writer/particle
266
196
  // paths) allocated on `state`: the uniform buffer, particle instance buffer, depth-stencil texture,
267
197
  // and every quad-batch writer pool slot's instance/material buffers. Call when the render state is no
268
198
  // longer needed.
269
199
  //
270
- // Intentionally NOT touched: the GPUDevice (app-owned and shared — destroying it would tear down
271
- // every state on it), and GC-managed Wgpu objects with no destroy() (pipelines, bind groups,
272
- // layouts, samplers, shader modules, texture views). textureCache is a WeakMap and cannot be
273
- // enumerated; its entries' textures are freed per-node by the dispose* paths.
200
+ // GC-managed Wgpu objects with no destroy() (pipelines, bind groups, layouts, samplers, shader
201
+ // modules, texture views) are not touched. textureCache is a WeakMap and cannot be enumerated; its
202
+ // entries' textures are freed per-node by the dispose* paths. The shared device tier routes its
203
+ // acquisition through the originating host backend when its last state is destroyed.
274
204
  export function destroyWgpuRenderState(state) {
275
205
  if (_destroyedStates.has(state))
276
206
  return;
277
207
  _destroyedStates.add(state);
278
208
  const runtime = getWgpuRenderStateRuntime(state);
209
+ for (const teardown of [...runtime.teardowns])
210
+ teardown(state);
211
+ runtime.teardowns.length = 0;
279
212
  destroyRenderState(state);
280
213
  runtime.uniformBuffer?.destroy();
281
214
  runtime.particleInstanceBuffer?.destroy();
@@ -285,7 +218,45 @@ export function destroyWgpuRenderState(state) {
285
218
  slot.instanceBuffer?.destroy();
286
219
  slot.materialBuffer?.destroy();
287
220
  }
288
- getWgpuDeviceRuntime(runtime).references--;
221
+ const ctx = runtime.context;
222
+ ctx.references--;
223
+ if (ctx.references === 0) {
224
+ const device = ctx.device;
225
+ for (const teardown of ctx.teardowns)
226
+ teardown(device);
227
+ ctx.teardowns.length = 0;
228
+ }
229
+ const ownership = _acquisitionByStateRuntime.get(runtime);
230
+ if (ownership !== undefined) {
231
+ ownership.references--;
232
+ if (ownership.references === 0 && ownership.acquisition.ownership !== 'caller') {
233
+ ownership.hostBackend.release(ownership.acquisition);
234
+ }
235
+ }
236
+ }
237
+ function createWgpuRenderStateRuntimeInternal(deviceRuntime, pipeline) {
238
+ const runtime = createRenderStateRuntime();
239
+ runtime.applyBlendModeParent = null;
240
+ runtime.surfaceAntialiasEnabled = false;
241
+ runtime.surfaceAntialiasTexture = null;
242
+ runtime.surfaceAntialiasView = null;
243
+ runtime.surfaceAntialiasWidth = 0;
244
+ runtime.surfaceAntialiasHeight = 0;
245
+ runtime.surfaceAntialiasResolveBindGroupLayout = null;
246
+ runtime.surfaceAntialiasResolvePipeline = null;
247
+ runtime.surfaceAntialiasResolveBindGroup = null;
248
+ runtime.surfacePresentationView = null;
249
+ runtime.registries = { ...pipeline.registries };
250
+ runtime.teardowns = [];
251
+ runtime.borrowedSurfaceExtent = null;
252
+ const ownership = _acquisitionByDeviceRuntime.get(deviceRuntime);
253
+ if (ownership !== undefined) {
254
+ ownership.references++;
255
+ _acquisitionByStateRuntime.set(runtime, ownership);
256
+ }
257
+ deviceRuntime.references++;
258
+ runtime.context = deviceRuntime;
259
+ return runtime;
289
260
  }
290
261
  export function getWgpuColorAdjustmentMaterialFeature(state) {
291
262
  const entry = getWgpuRenderStateRuntime(state).registries.colorAdjustmentFeature?.entry;
@@ -295,6 +266,9 @@ export function getWgpuColorAdjustmentMaterialFeatureGuard(state) {
295
266
  const entry = getWgpuRenderStateRuntime(state).registries.colorAdjustmentFeatureGuard?.entry;
296
267
  return entry?.state === RegistryEntryState.Bound ? entry.value : null;
297
268
  }
269
+ export function getWgpuDeviceRuntime(deviceState) {
270
+ return deviceState[EntityRuntimeKey];
271
+ }
298
272
  // Resolves the package-private GPU runtime attached to a WgpuRenderState. Mutable by design: the
299
273
  // render path writes its fields every frame.
300
274
  export function getWgpuRenderStateRuntime(state) {
@@ -323,7 +297,8 @@ export function getWgpuSampler(state, minFilter, magFilter, wrapU, wrapV, mipmap
323
297
  (SAMPLER_WRAP_BITS[wrapV] << 4) |
324
298
  ((effectiveMipmapFilter === undefined ? 0 : SAMPLER_MIPMAP_BITS[effectiveMipmapFilter]) << 6) |
325
299
  (anisotropy << 8);
326
- let sampler = runtime.samplerCache.get(key);
300
+ const cache = runtime.context.samplerCache;
301
+ let sampler = cache.get(key);
327
302
  if (sampler === undefined) {
328
303
  const descriptor = {
329
304
  minFilter: effectiveMinFilter,
@@ -336,108 +311,82 @@ export function getWgpuSampler(state, minFilter, magFilter, wrapU, wrapV, mipmap
336
311
  if (anisotropy > 1)
337
312
  descriptor.maxAnisotropy = anisotropy;
338
313
  sampler = state.device.createSampler(descriptor);
339
- runtime.samplerCache.set(key, sampler);
314
+ cache.set(key, sampler);
340
315
  }
341
316
  return sampler;
342
317
  }
343
318
  export function isWgpuSupported() {
344
- return typeof navigator !== 'undefined' && 'gpu' in navigator && navigator.gpu !== null;
319
+ return getWgpuHostBackend().isSupported();
320
+ }
321
+ export function registerWgpuDeviceTeardown(state, teardown) {
322
+ getWgpuRenderStateRuntime(state).context.teardowns.push(teardown);
323
+ }
324
+ export function registerWgpuRenderStateTeardown(state, teardown) {
325
+ getWgpuRenderStateRuntime(state).teardowns.push(teardown);
345
326
  }
346
327
  // Small-integer codes for the sampler-cache numeric key (see getWgpuSampler). Module-level so the key
347
328
  // packing reads a field instead of allocating — no per-call table construction.
348
329
  const SAMPLER_FILTER_BITS = { nearest: 0, linear: 1 };
349
330
  const SAMPLER_WRAP_BITS = { 'clamp-to-edge': 0, 'mirror-repeat': 1, repeat: 2 };
350
331
  const SAMPLER_MIPMAP_BITS = { nearest: 1, linear: 2 };
351
- function initializeOffscreenWgpuRuntime(state, runtime, screenRuntime) {
352
- const ringByteSize = screenRuntime.uniformStride * RING_SLOT_COUNT;
353
- const uniformBuffer = state.device.createBuffer({
354
- size: ringByteSize,
355
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
332
+ function initializeWgpuDeviceRuntime(runtime) {
333
+ if (runtime.uniformBindGroupLayout !== null)
334
+ return;
335
+ const device = runtime.device;
336
+ const { uniformBindGroupLayout, textureBindGroupLayout } = createWgpuBindGroupLayouts(device);
337
+ runtime.uniformBindGroupLayout = uniformBindGroupLayout;
338
+ runtime.textureBindGroupLayout = textureBindGroupLayout;
339
+ runtime.pipelineCache = new Map();
340
+ runtime.linearSampler = device.createSampler({
341
+ minFilter: 'linear',
342
+ magFilter: 'linear',
343
+ addressModeU: 'clamp-to-edge',
344
+ addressModeV: 'clamp-to-edge',
356
345
  });
357
- const uniformData = new Float32Array(ringByteSize / 4);
358
- runtime.currentBlendMode = null;
359
- runtime.currentRenderTarget = null;
360
- runtime.uniformBindGroupLayout = screenRuntime.uniformBindGroupLayout;
361
- runtime.textureBindGroupLayout = screenRuntime.textureBindGroupLayout;
362
- runtime.uniformBuffer = uniformBuffer;
363
- runtime.uniformData = uniformData;
364
- runtime.uniformDataU32 = new Uint32Array(uniformData.buffer);
365
- runtime.uniformOffset = 0;
366
- runtime.uniformStride = screenRuntime.uniformStride;
367
- runtime.uniformBindGroup = state.device.createBindGroup({
368
- layout: runtime.uniformBindGroupLayout,
369
- entries: [{ binding: 0, resource: { buffer: uniformBuffer, size: UNIFORM_BYTE_SIZE } }],
346
+ runtime.nearestSampler = device.createSampler({
347
+ minFilter: 'nearest',
348
+ magFilter: 'nearest',
349
+ addressModeU: 'clamp-to-edge',
350
+ addressModeV: 'clamp-to-edge',
370
351
  });
371
- runtime.matrixArray = new Float32Array(9);
372
- runtime.defaultBitmapShader = screenRuntime.defaultBitmapShader;
373
- runtime.particleInstanceBuffer = null;
374
- runtime.particleInstanceData = null;
375
- runtime.particleInstanceCapacity = 0;
376
- runtime.quadBatchWriterBlendMode = null;
377
- runtime.quadBatchWriterMaterial = null;
378
- runtime.quadBatchWriterMaterialRenderer = null;
379
- runtime.quadBatchWriterMaterialFloats = 0;
380
- runtime.quadBatchWriterCount = 0;
381
- runtime.quadBatchWriterInstanceData = new Float32Array(13 * 256);
382
- runtime.quadBatchWriterMaterialData = new Float32Array(8 * 256);
383
- runtime.quadBatchWriterTexture = null;
384
- runtime.quadBatchWriterSampler = null;
385
- runtime.quadBatchWriterSmoothing = null;
386
- runtime.quadBatchWriterBufferPool = [];
387
- runtime.quadBatchWriterBufferCursor = 0;
388
- runtime.commandEncoder = null;
389
- runtime.renderPass = null;
390
- runtime.canvasTextureView = null;
391
- runtime.canvasViewCleared = false;
392
- runtime.frameCaptureEnabled = false;
393
- runtime.frameCaptureTexture = null;
394
- runtime.frameCaptureBuffer = null;
395
- runtime.frameCaptureBytesPerRow = 0;
396
- runtime.frameCaptureWidth = 0;
397
- runtime.frameCaptureHeight = 0;
398
- runtime.depthStencilTexture = null;
399
- runtime.depthStencilView = null;
400
- runtime.depthStencilWidth = 0;
401
- runtime.depthStencilHeight = 0;
402
- runtime.currentMaskDepth = 0;
403
- runtime.maskWriteMode = false;
404
- runtime.clipContourPipelines = undefined;
405
- runtime.clipContourStack = [];
406
- runtime.shapeMeshPipelines = undefined;
407
- runtime.clipForms = [];
408
- runtime.scissorStack = [];
409
- runtime.currentScissorRect = null;
410
- runtime.renderTargetViewport = null;
411
- runtime.renderTargetStack = [];
352
+ runtime.samplerCache = new Map();
353
+ runtime.mipmapPipelineCache = new Map();
354
+ runtime.textureCache = new WeakMap();
355
+ runtime.textureSourcePremultipliedTextureCache = new WeakMap();
356
+ runtime.textureSourcePremultipliedSrgbTextureCache = new WeakMap();
357
+ runtime.textureSourceStraightTextureCache = new WeakMap();
358
+ runtime.textureSourceStraightSrgbTextureCache = new WeakMap();
412
359
  }
413
- function getWgpuDeviceRuntime(runtime) {
414
- const deviceRuntime = _deviceRuntimeByStateRuntime.get(runtime);
415
- if (deviceRuntime === undefined)
416
- throw new Error('WgpuRenderState runtime has no device tier');
417
- return deviceRuntime;
360
+ // The caller's own teardown for an acquisition they own. Unconditional by design: the caller is asking.
361
+ export function releaseWgpuAcquisition(acquisition) {
362
+ getWgpuHostBackend().release(acquisition);
363
+ }
364
+ function createMinimalDeviceRuntime(device) {
365
+ const runtime = {
366
+ binding: null,
367
+ device,
368
+ lost: null,
369
+ signals: null,
370
+ references: 0,
371
+ teardowns: [],
372
+ uniformBindGroupLayout: null,
373
+ textureBindGroupLayout: null,
374
+ pipelineCache: new Map(),
375
+ linearSampler: null,
376
+ nearestSampler: null,
377
+ samplerCache: new Map(),
378
+ mipmapPipelineCache: new Map(),
379
+ textureCache: new WeakMap(),
380
+ textureSourcePremultipliedTextureCache: new WeakMap(),
381
+ textureSourcePremultipliedSrgbTextureCache: new WeakMap(),
382
+ textureSourceStraightTextureCache: new WeakMap(),
383
+ textureSourceStraightSrgbTextureCache: new WeakMap(),
384
+ };
385
+ observeWgpuDeviceLoss(runtime);
386
+ return runtime;
418
387
  }
419
- // These values are pure functions of (GPUDevice, source data). Pass/encoder, uniform-ring, proxy,
420
- // traversal, batch, and clip state remain local to each WgpuRenderState.
421
- const WGPU_DEVICE_RUNTIME_KEYS = [
422
- 'uniformBindGroupLayout',
423
- 'textureBindGroupLayout',
424
- 'pipelineCache',
425
- 'linearSampler',
426
- 'nearestSampler',
427
- 'samplerCache',
428
- 'mipmapPipelineCache',
429
- 'textureCache',
430
- 'textureSourcePremultipliedTextureCache',
431
- 'textureSourcePremultipliedSrgbTextureCache',
432
- 'textureSourceStraightTextureCache',
433
- 'textureSourceStraightSrgbTextureCache',
434
- 'videoTextureCache',
435
- 'videoSrgbTextureCache',
436
- 'wgpuExternalTextureCache',
437
- 'wgpuRenderTextureCache',
438
- 'sceneMeshUploadCache',
439
- ];
440
- const _deviceRuntimeByStateRuntime = new WeakMap();
388
+ const _acquisitionByStateRuntime = new WeakMap();
389
+ const _acquisitionByDeviceRuntime = new WeakMap();
441
390
  const _destroyedStates = new WeakSet();
442
391
  // Resolve the blend hook at the draw seam rather than hiding derived-state delegation behind an
443
392
  // entity accessor. A locally installed hook wins; otherwise walk toward the screen pipeline so