@forgeax/engine-rhi-debug 0.1.4 → 0.1.7

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 (48) hide show
  1. package/README.md +39 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/readback-matrix-fixture.d.ts +32 -0
  4. package/dist/__tests__/readback-matrix-fixture.d.ts.map +1 -0
  5. package/dist/frame-model.d.ts +84 -52
  6. package/dist/frame-model.d.ts.map +1 -1
  7. package/dist/index.d.ts +3 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.mjs +4651 -46
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/recorder/device.d.ts.map +1 -1
  12. package/dist/recorder/encoder.d.ts.map +1 -1
  13. package/dist/replay/device-request.d.ts +8 -0
  14. package/dist/replay/device-request.d.ts.map +1 -0
  15. package/dist/replay/readback.d.ts +7 -0
  16. package/dist/replay/readback.d.ts.map +1 -1
  17. package/dist/replay/session.d.ts +17 -0
  18. package/dist/replay/session.d.ts.map +1 -1
  19. package/dist/texel-decode.d.ts +5 -4
  20. package/dist/texel-decode.d.ts.map +1 -1
  21. package/dist/types.d.ts +24 -6
  22. package/dist/types.d.ts.map +1 -1
  23. package/package.json +7 -7
  24. package/src/__tests__/consumer-inventory.unit.test.ts +49 -1
  25. package/src/__tests__/e2e.browser.test.ts +6 -1
  26. package/src/__tests__/frame-model-parity.test-d.ts +13 -1
  27. package/src/__tests__/frame-model-parity.unit.test.ts +262 -15
  28. package/src/__tests__/guard-gates.test.ts +176 -1
  29. package/src/__tests__/public-surface.integration.test.ts +8 -0
  30. package/src/__tests__/readback-format-matrix.dawn.test.ts +226 -3
  31. package/src/__tests__/readback-format-matrix.unit.test.ts +25 -1
  32. package/src/__tests__/readback-matrix-fixture.ts +20 -0
  33. package/src/__tests__/replay-fail-fast.unit.test.ts +12 -0
  34. package/src/__tests__/replay-session.dawn.test.ts +3 -0
  35. package/src/__tests__/replay-session.test-d.ts +7 -1
  36. package/src/__tests__/replay-session.unit.test.ts +16 -0
  37. package/src/__tests__/rhi-debug-fresh-replay.dawn.test.ts +2 -0
  38. package/src/__tests__/tape-index.unit.test.ts +3 -0
  39. package/src/__tests__/tree-shake.unit.test.ts +24 -0
  40. package/src/frame-model.ts +381 -83
  41. package/src/index.ts +17 -10
  42. package/src/recorder/device.ts +20 -2
  43. package/src/recorder/encoder.ts +22 -5
  44. package/src/replay/device-request.ts +38 -0
  45. package/src/replay/readback.ts +106 -18
  46. package/src/replay/session.ts +47 -2
  47. package/src/texel-decode.ts +37 -3
  48. package/src/types.ts +30 -6
@@ -32,6 +32,13 @@ export interface TextureReadbackSubresource {
32
32
 
33
33
  export type ReplayReadbackRequest = BufferReadbackRange | TextureReadbackSubresource;
34
34
 
35
+ export interface ReplayReadbackProvenance {
36
+ readonly generation: number;
37
+ readonly resourceId: string;
38
+ readonly subresource: ReplayReadbackRequest | null;
39
+ readonly selectedWorkIndex?: number;
40
+ }
41
+
35
42
  export interface ReplayReadbackResult {
36
43
  readonly resourceId: string;
37
44
  readonly kind: 'buffer' | 'texture';
@@ -39,8 +46,11 @@ export interface ReplayReadbackResult {
39
46
  readonly width?: number;
40
47
  readonly height?: number;
41
48
  readonly bytes: Uint8Array;
49
+ readonly provenance: ReplayReadbackProvenance;
42
50
  }
43
51
 
52
+ type ReplayReadbackPayload = Omit<ReplayReadbackResult, 'provenance'>;
53
+
44
54
  export async function readReplayResource(
45
55
  device: RhiDevice,
46
56
  table: ResourceTable,
@@ -51,18 +61,74 @@ export async function readReplayResource(
51
61
  const entry = table.get(resourceId);
52
62
  if (entry === undefined)
53
63
  return readbackFailure(`resource ${resourceId} is not present in the current generation`);
64
+ let result: Result<ReplayReadbackPayload, RhiDebugError>;
54
65
  if (entry.resource.kind === 'texture-view') {
55
66
  const sourceId = stringField(entry.descriptor, 'sourceHandleId');
56
67
  const source = sourceId === undefined ? undefined : table.get(sourceId);
57
68
  if (source === undefined || source.resource.kind !== 'texture') {
58
69
  return readbackFailure(`texture view ${resourceId} has no readable source texture`);
59
70
  }
60
- return readTexture(device, source, resourceId, subresource, createShaderModule);
71
+ const sourceSubresource = resolveTextureViewSubresource(entry, source, subresource);
72
+ if (!sourceSubresource.ok) return sourceSubresource;
73
+ result = await readTexture(
74
+ device,
75
+ source,
76
+ resourceId,
77
+ sourceSubresource.value,
78
+ createShaderModule,
79
+ );
80
+ } else if (entry.resource.kind === 'texture') {
81
+ result = await readTexture(device, entry, resourceId, subresource, createShaderModule);
82
+ } else if (entry.resource.kind === 'buffer') {
83
+ result = await readBuffer(device, entry, resourceId, subresource);
84
+ } else {
85
+ return readbackFailure(`resource ${resourceId} is not readable by the v7 core matrix`);
86
+ }
87
+ if (!result.ok) return result;
88
+ return ok({
89
+ ...result.value,
90
+ provenance: {
91
+ generation: table.generation,
92
+ resourceId,
93
+ subresource: subresource ?? null,
94
+ },
95
+ });
96
+ }
97
+
98
+ function resolveTextureViewSubresource(
99
+ view: ResourceTableEntry,
100
+ source: ResourceTableEntry,
101
+ requested: ReplayReadbackRequest | undefined,
102
+ ): Result<ReplayReadbackRequest | undefined, RhiDebugError> {
103
+ if (requested === undefined || isBufferRange(requested)) return ok(requested);
104
+ const sourceDescriptor = recordField(source.descriptor, 'desc');
105
+ const sourceSize = textureSize(sourceDescriptor?.size);
106
+ const sourceMipCount = numberField(sourceDescriptor, 'mipLevelCount') ?? 1;
107
+ const viewDescriptor = recordField(view.descriptor, 'desc');
108
+ const baseMipLevel = numberField(viewDescriptor, 'baseMipLevel') ?? 0;
109
+ const baseArrayLayer = numberField(viewDescriptor, 'baseArrayLayer') ?? 0;
110
+ const mipLevelCount =
111
+ numberField(viewDescriptor, 'mipLevelCount') ?? sourceMipCount - baseMipLevel;
112
+ const arrayLayerCount =
113
+ numberField(viewDescriptor, 'arrayLayerCount') ??
114
+ sourceSize.depthOrArrayLayers - baseArrayLayer;
115
+ const localMipLevel = requested.mipLevel ?? 0;
116
+ const localArrayLayer = requested.arrayLayer ?? 0;
117
+ if (
118
+ !validIndex(baseMipLevel, sourceMipCount + 1) ||
119
+ !validIndex(baseArrayLayer, sourceSize.depthOrArrayLayers + 1) ||
120
+ !validIndex(localMipLevel, mipLevelCount) ||
121
+ !validIndex(localArrayLayer, arrayLayerCount) ||
122
+ baseMipLevel + mipLevelCount > sourceMipCount ||
123
+ baseArrayLayer + arrayLayerCount > sourceSize.depthOrArrayLayers
124
+ ) {
125
+ return readbackFailure('texture view subresource is outside the recorded view extent');
61
126
  }
62
- if (entry.resource.kind === 'texture')
63
- return readTexture(device, entry, resourceId, subresource, createShaderModule);
64
- if (entry.resource.kind === 'buffer') return readBuffer(device, entry, resourceId, subresource);
65
- return readbackFailure(`resource ${resourceId} is not readable by the v7 core matrix`);
127
+ return ok({
128
+ ...requested,
129
+ mipLevel: baseMipLevel + localMipLevel,
130
+ arrayLayer: baseArrayLayer + localArrayLayer,
131
+ });
66
132
  }
67
133
 
68
134
  async function readBuffer(
@@ -70,7 +136,7 @@ async function readBuffer(
70
136
  entry: ResourceTableEntry,
71
137
  resourceId: string,
72
138
  subresource: ReplayReadbackRequest | undefined,
73
- ): Promise<Result<ReplayReadbackResult, RhiDebugError>> {
139
+ ): Promise<Result<ReplayReadbackPayload, RhiDebugError>> {
74
140
  const size = numberField(recordField(entry.descriptor, 'desc'), 'size');
75
141
  if (size === undefined || !Number.isSafeInteger(size) || size < 0)
76
142
  return readbackFailure(`buffer ${resourceId} has no valid recorded size`);
@@ -97,7 +163,7 @@ async function readTexture(
97
163
  resourceId: string,
98
164
  subresource: ReplayReadbackRequest | undefined,
99
165
  createShaderModule: CreateShaderModuleFn,
100
- ): Promise<Result<ReplayReadbackResult, RhiDebugError>> {
166
+ ): Promise<Result<ReplayReadbackPayload, RhiDebugError>> {
101
167
  const descriptor = recordField(entry.descriptor, 'desc');
102
168
  const format = stringField(descriptor, 'format');
103
169
  const dimension = stringField(descriptor, 'dimension') ?? '2d';
@@ -128,18 +194,15 @@ async function readTexture(
128
194
  const width = Math.max(1, Math.floor(size.width / 2 ** mipLevel));
129
195
  const height = Math.max(1, Math.floor(size.height / 2 ** mipLevel));
130
196
  const requestedAspect = textureAspect(subresource);
131
- if (format === 'depth24plus' || format === 'depth24plus-stencil8') {
132
- if (format === 'depth24plus-stencil8' && requestedAspect === 'all') {
197
+ if (isDepthStencilTextureFormat(format)) {
198
+ if (requestedAspect === 'all') {
133
199
  return readbackUnsupported(
134
200
  resourceId,
135
201
  format,
136
- 'depth24plus-stencil8 requires an explicit depth-only or stencil-only aspect',
202
+ `${format} requires an explicit depth-only or stencil-only aspect`,
137
203
  );
138
204
  }
139
205
  if (requestedAspect === 'stencil-only') {
140
- if (format !== 'depth24plus-stencil8') {
141
- return readbackUnsupported(resourceId, format, 'depth24plus has no stencil aspect');
142
- }
143
206
  const bytes = await copyTextureBytes(
144
207
  device,
145
208
  entry.resource.value as Texture,
@@ -157,6 +220,24 @@ async function readTexture(
157
220
  if (!bytes.ok) return bytes;
158
221
  return ok({ resourceId, kind: 'texture', format, width, height, bytes: bytes.value });
159
222
  }
223
+ if (format === 'depth24plus-stencil8') {
224
+ return blitDepth24PlusTexture(
225
+ device,
226
+ entry.resource.value as Texture,
227
+ resourceId,
228
+ format,
229
+ width,
230
+ height,
231
+ mipLevel,
232
+ arrayLayer,
233
+ createShaderModule,
234
+ );
235
+ }
236
+ }
237
+ if (format === 'depth24plus') {
238
+ if (requestedAspect === 'stencil-only') {
239
+ return readbackUnsupported(resourceId, format, 'depth24plus has no stencil aspect');
240
+ }
160
241
  return blitDepth24PlusTexture(
161
242
  device,
162
243
  entry.resource.value as Texture,
@@ -254,7 +335,7 @@ async function blitDepth24PlusTexture(
254
335
  mipLevel: number,
255
336
  arrayLayer: number,
256
337
  createShaderModule: CreateShaderModuleFn,
257
- ): Promise<Result<ReplayReadbackResult, RhiDebugError>> {
338
+ ): Promise<Result<ReplayReadbackPayload, RhiDebugError>> {
258
339
  let output: Texture | undefined;
259
340
  let staging: Buffer | undefined;
260
341
  let mapped: MappedBuffer | undefined;
@@ -395,7 +476,10 @@ async function copyBufferBytes(
395
476
  let staging: Buffer | undefined;
396
477
  let mapped: MappedBuffer | undefined;
397
478
  try {
398
- const created = device.createBuffer({ size: Math.max(4, size), usage: COPY_DST_MAP_READ });
479
+ const alignedSourceOffset = sourceOffset - (sourceOffset % 4);
480
+ const leadingBytes = sourceOffset - alignedSourceOffset;
481
+ const copySize = align4(leadingBytes + size);
482
+ const created = device.createBuffer({ size: Math.max(4, copySize), usage: COPY_DST_MAP_READ });
399
483
  if (!created.ok)
400
484
  return readbackFailure(`staging buffer creation failed: ${created.error.code}`);
401
485
  staging = created.value;
@@ -403,7 +487,7 @@ async function copyBufferBytes(
403
487
  if (!encoderResult.ok)
404
488
  return readbackFailure(`readback encoder creation failed: ${encoderResult.error.code}`);
405
489
  const encoder = encoderResult.value;
406
- encoder.copyBufferToBuffer(source, sourceOffset, staging, 0, size);
490
+ encoder.copyBufferToBuffer(source, alignedSourceOffset, staging, 0, copySize);
407
491
  const finished = encoder.finish();
408
492
  if (!finished.ok)
409
493
  return readbackFailure(`readback encoder finish failed: ${finished.error.code}`);
@@ -413,9 +497,9 @@ async function copyBufferBytes(
413
497
  const mappedResult = await staging.mapAsync(GPU_MAP_MODE_READ);
414
498
  if (!mappedResult.ok) return readbackFailure(`readback map failed: ${mappedResult.error.code}`);
415
499
  mapped = mappedResult.value;
416
- const range = mapped.getMappedRange(0, size);
500
+ const range = mapped.getMappedRange(0, copySize);
417
501
  if (!range.ok) return readbackFailure(`readback mapped range failed: ${range.error.code}`);
418
- return ok(new Uint8Array(range.value).slice());
502
+ return ok(new Uint8Array(range.value).slice(leadingBytes, leadingBytes + size));
419
503
  } catch (cause) {
420
504
  return readbackFailure(`buffer readback failed: ${messageOf(cause)}`);
421
505
  } finally {
@@ -578,6 +662,10 @@ function align256(value: number): number {
578
662
  return Math.max(256, Math.ceil(value / 256) * 256);
579
663
  }
580
664
 
665
+ function align4(value: number): number {
666
+ return Math.ceil(value / 4) * 4;
667
+ }
668
+
581
669
  function messageOf(cause: unknown): string {
582
670
  return cause instanceof Error ? cause.message : String(cause);
583
671
  }
@@ -6,6 +6,7 @@ import type {
6
6
  } from '@forgeax/engine-rhi';
7
7
  import { err, ok, type Result } from '@forgeax/engine-types';
8
8
  import { createRhiDebugError, type RhiDebugError } from '../errors';
9
+ import { buildFrameModel, type WorkBinding, type WorkPipeline } from '../frame-model';
9
10
  import { buildTapeIndex, type TapeIndex, type TapeWorkEntry } from '../protocol/tape-index';
10
11
  import type { BootstrapResource, RhiCallEvent, Tape } from '../protocol/types';
11
12
  import type { CreateShaderModuleFn } from '../recorder';
@@ -38,6 +39,22 @@ export interface WorkInspection {
38
39
  readonly eventIndex: number;
39
40
  readonly passIndex: number;
40
41
  readonly attachment: ReplayReadbackResult | undefined;
42
+ readonly pipeline?: WorkPipeline;
43
+ readonly bindings?: readonly WorkBinding[];
44
+ readonly vertexBuffers?: readonly {
45
+ readonly slot: number;
46
+ readonly bufferHandleId: string;
47
+ readonly offset: number;
48
+ readonly size: number | null;
49
+ }[];
50
+ readonly indexBuffer?: {
51
+ readonly bufferHandleId: string;
52
+ readonly format: string;
53
+ readonly offset: number;
54
+ readonly size: number | null;
55
+ } | null;
56
+ readonly shaders?: WorkPipeline['shaders'];
57
+ readonly resourceIds?: readonly string[];
41
58
  }
42
59
 
43
60
  export interface ReplaySession {
@@ -71,6 +88,7 @@ export async function openReplay(
71
88
  if (capabilityFailure !== undefined) return err(capabilityFailure);
72
89
 
73
90
  const index = buildTapeIndex(tape);
91
+ const model = buildFrameModel(tape);
74
92
  const table = new ResourceTable(backend.device, 0);
75
93
  let disposed = false;
76
94
  let prepared = false;
@@ -112,7 +130,9 @@ export async function openReplay(
112
130
  if (disposed) return positionError(workIndex, index.works.length);
113
131
  if (signal?.aborted) return positionError(workIndex, index.works.length);
114
132
  const work = index.works[workIndex];
133
+ const modelWork = model.works[workIndex];
115
134
  if (work === undefined) return positionError(workIndex, index.works.length);
135
+ if (modelWork === undefined) return positionError(workIndex, index.works.length);
116
136
  const cleared = await reset();
117
137
  if (!cleared.ok) return cleared;
118
138
  const bootstrapped = await prepare();
@@ -123,11 +143,36 @@ export async function openReplay(
123
143
  ? await readWorkAttachment(context, index, work)
124
144
  : undefined;
125
145
  if (attachment !== undefined && !attachment.ok) return attachment;
126
- return ok({
146
+ const selectedAttachment =
147
+ attachment?.ok === true
148
+ ? {
149
+ ...attachment.value,
150
+ provenance: {
151
+ ...attachment.value.provenance,
152
+ selectedWorkIndex: work.workIndex,
153
+ },
154
+ }
155
+ : undefined;
156
+ const baseInspection = {
127
157
  workIndex: work.workIndex,
128
158
  eventIndex: work.eventIndex,
129
159
  passIndex: work.passIndex,
130
- attachment: attachment === undefined ? undefined : attachment.value,
160
+ attachment: selectedAttachment,
161
+ };
162
+ return ok({
163
+ ...baseInspection,
164
+ ...(fields?.includes('pipeline') ? { pipeline: modelWork.pipeline } : {}),
165
+ ...(fields?.includes('bindings')
166
+ ? {
167
+ bindings: modelWork.bindings,
168
+ vertexBuffers: modelWork.vertexBuffers,
169
+ indexBuffer: modelWork.indexBuffer,
170
+ shaders: modelWork.pipeline.shaders,
171
+ resourceIds: modelWork.bindings
172
+ .map((binding) => binding.resourceId)
173
+ .filter((resourceId): resourceId is string => resourceId !== null),
174
+ }
175
+ : {}),
131
176
  });
132
177
  },
133
178
  async readResource(resourceId, subresource, signal) {
@@ -146,21 +146,26 @@ function readTexel(
146
146
  }
147
147
 
148
148
  /**
149
- * Decode tight raw GPU bytes of an uncompressed color texture into RGBA8 the
150
- * canvas can paint via putImageData. Returns null when the format has no
151
- * {@link formatInfo} entry (compressed / depth / unknown -> caller falls back).
149
+ * Decode tight raw GPU bytes of a supported color or depth/stencil plane into
150
+ * RGBA8 the canvas can paint via putImageData. Returns null when the format has
151
+ * no {@link formatInfo} entry or the requested aspect is invalid.
152
152
  *
153
153
  * @param bytes - Tight readback bytes (no row padding), length = w*h*bytesPerTexel.
154
154
  * @param format - The texture's real format string.
155
155
  * @param width - Texture width in pixels.
156
156
  * @param height - Texture height in pixels.
157
+ * @param aspect - Explicit plane selected by readback for depth/stencil formats.
157
158
  */
158
159
  export function decodeToRgba8(
159
160
  bytes: Uint8Array,
160
161
  format: string,
161
162
  width: number,
162
163
  height: number,
164
+ aspect: 'all' | 'depth-only' | 'stencil-only' = 'all',
163
165
  ): Uint8ClampedArray<ArrayBuffer> | null {
166
+ if (format.startsWith('depth')) {
167
+ return decodeDepthToRgba8(bytes, format, width, height, aspect);
168
+ }
164
169
  const info = formatInfo(format);
165
170
  const texBytes = bytesPerTexel(format as never);
166
171
  if (!info || texBytes === undefined) return null;
@@ -198,6 +203,35 @@ export function decodeToRgba8(
198
203
  return out;
199
204
  }
200
205
 
206
+ function decodeDepthToRgba8(
207
+ bytes: Uint8Array,
208
+ format: string,
209
+ width: number,
210
+ height: number,
211
+ aspect: 'all' | 'depth-only' | 'stencil-only',
212
+ ): Uint8ClampedArray<ArrayBuffer> | null {
213
+ const combined = format.endsWith('-stencil8');
214
+ if (combined && aspect === 'all') return null;
215
+ if (!combined && aspect === 'stencil-only') return null;
216
+ const stencil = aspect === 'stencil-only';
217
+ const bytesPerValue = stencil ? 1 : 4;
218
+ const texelCount = width * height;
219
+ if (bytes.byteLength < texelCount * bytesPerValue) return null;
220
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
221
+ const out = new Uint8ClampedArray(new ArrayBuffer(texelCount * 4));
222
+ for (let index = 0; index < texelCount; index++) {
223
+ const value = stencil
224
+ ? view.getUint8(index)
225
+ : Math.round(clamp01(view.getFloat32(index * 4, true)) * 255);
226
+ const target = index * 4;
227
+ out[target] = value;
228
+ out[target + 1] = value;
229
+ out[target + 2] = value;
230
+ out[target + 3] = 255;
231
+ }
232
+ return out;
233
+ }
234
+
201
235
  /**
202
236
  * Decode a single texel from tight raw GPU bytes into raw float RGBA values
203
237
  * without the display clamp (D-4: raw byte bypass for HDR fidelity).
package/src/types.ts CHANGED
@@ -169,11 +169,21 @@ export interface RhiCallEventCreateRenderPipeline {
169
169
  readonly kind: 'createRenderPipeline';
170
170
  readonly handleId: HandleId;
171
171
  readonly desc: {
172
- readonly vertex?: GPUVertexState | undefined;
172
+ /** Serialized stage omits opaque shader module; the handle id is authoritative. */
173
+ readonly vertex?: {
174
+ readonly entryPoint?: string | undefined;
175
+ readonly buffers: Iterable<GPUVertexBufferLayout | null | undefined>;
176
+ readonly constants?: Record<string, number> | undefined;
177
+ };
173
178
  readonly primitive?: GPUPrimitiveState | undefined;
174
179
  readonly depthStencil?: GPUDepthStencilState | undefined;
175
180
  readonly multisample?: GPUMultisampleState | undefined;
176
- readonly fragment?: GPUFragmentState | undefined;
181
+ /** Serialized stage omits opaque shader module; the handle id is authoritative. */
182
+ readonly fragment?: {
183
+ readonly entryPoint?: string | undefined;
184
+ readonly targets: Iterable<GPUColorTargetState | null | undefined>;
185
+ readonly constants?: Record<string, number> | undefined;
186
+ };
177
187
  };
178
188
  readonly layoutHandleId: HandleId;
179
189
  /** HandleId of the re-created vertex shader module for cross-device binding. */
@@ -261,10 +271,24 @@ export interface RhiCallEventBeginRenderPass {
261
271
  readonly cmdHandleId: HandleId;
262
272
  readonly passHandleId: HandleId;
263
273
  readonly desc: {
264
- readonly colorAttachments: Iterable<GPURenderPassColorAttachment | null | undefined>;
265
- readonly depthStencilAttachment?: GPURenderPassDepthStencilAttachment | undefined;
266
- readonly occlusionQuerySet?: GPUQuerySet | undefined;
267
- readonly timestampWrites?: GPURenderPassTimestampWrites | undefined;
274
+ /** Attachment handles are replaced by the parallel HandleId arrays on replay. */
275
+ readonly colorAttachments: Iterable<
276
+ | {
277
+ readonly view: unknown;
278
+ readonly resolveTarget?: unknown;
279
+ readonly [key: string]: unknown;
280
+ }
281
+ | null
282
+ | undefined
283
+ >;
284
+ readonly depthStencilAttachment?:
285
+ | {
286
+ readonly view: unknown;
287
+ readonly [key: string]: unknown;
288
+ }
289
+ | undefined;
290
+ readonly occlusionQuerySet?: unknown | undefined;
291
+ readonly timestampWrites?: unknown | undefined;
268
292
  readonly maxDrawCount?: number | undefined;
269
293
  };
270
294
  readonly colorAttachmentViewHandleIds: readonly (HandleId | undefined)[];