@forgeax/engine-rhi-debug 0.1.6 → 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 (41) 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 +395 -40
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/replay/device-request.d.ts +8 -0
  12. package/dist/replay/device-request.d.ts.map +1 -0
  13. package/dist/replay/readback.d.ts +7 -0
  14. package/dist/replay/readback.d.ts.map +1 -1
  15. package/dist/replay/session.d.ts +17 -0
  16. package/dist/replay/session.d.ts.map +1 -1
  17. package/dist/texel-decode.d.ts +5 -4
  18. package/dist/texel-decode.d.ts.map +1 -1
  19. package/package.json +7 -7
  20. package/src/__tests__/consumer-inventory.unit.test.ts +49 -1
  21. package/src/__tests__/e2e.browser.test.ts +6 -1
  22. package/src/__tests__/frame-model-parity.test-d.ts +13 -1
  23. package/src/__tests__/frame-model-parity.unit.test.ts +262 -15
  24. package/src/__tests__/guard-gates.test.ts +176 -1
  25. package/src/__tests__/public-surface.integration.test.ts +8 -0
  26. package/src/__tests__/readback-format-matrix.dawn.test.ts +226 -3
  27. package/src/__tests__/readback-format-matrix.unit.test.ts +25 -1
  28. package/src/__tests__/readback-matrix-fixture.ts +20 -0
  29. package/src/__tests__/replay-fail-fast.unit.test.ts +12 -0
  30. package/src/__tests__/replay-session.dawn.test.ts +3 -0
  31. package/src/__tests__/replay-session.test-d.ts +7 -1
  32. package/src/__tests__/replay-session.unit.test.ts +16 -0
  33. package/src/__tests__/rhi-debug-fresh-replay.dawn.test.ts +2 -0
  34. package/src/__tests__/tape-index.unit.test.ts +3 -0
  35. package/src/__tests__/tree-shake.unit.test.ts +24 -0
  36. package/src/frame-model.ts +381 -83
  37. package/src/index.ts +17 -10
  38. package/src/replay/device-request.ts +38 -0
  39. package/src/replay/readback.ts +106 -18
  40. package/src/replay/session.ts +47 -2
  41. package/src/texel-decode.ts +37 -3
package/dist/index.mjs CHANGED
@@ -4718,30 +4718,254 @@ function computeTextureLayout(format, width, height, layerCount, mipLevelCount)
4718
4718
  }
4719
4719
 
4720
4720
  // src/frame-model.ts
4721
+ function jsonValue(value) {
4722
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean")
4723
+ return value;
4724
+ if (value === void 0) return null;
4725
+ if (Array.isArray(value)) return value.map(jsonValue);
4726
+ if (ArrayBuffer.isView(value))
4727
+ return Array.from(value, jsonValue);
4728
+ if (typeof value === "object") {
4729
+ const output = {};
4730
+ for (const [key, child] of Object.entries(value)) {
4731
+ if (child !== void 0) output[key] = jsonValue(child);
4732
+ }
4733
+ return output;
4734
+ }
4735
+ return String(value);
4736
+ }
4737
+ function eventDescriptor(event) {
4738
+ return jsonValue(event);
4739
+ }
4740
+ function workPipeline(pipelineId, pipelineEvents, shaderEvents) {
4741
+ if (pipelineId === void 0)
4742
+ return { status: "unavailable", shaders: [], reason: "pipeline-not-bound" };
4743
+ const event = pipelineEvents.get(pipelineId);
4744
+ if (event === void 0)
4745
+ return { status: "unavailable", shaders: [], reason: "descriptor-unavailable" };
4746
+ const shaders = [];
4747
+ const shaderRefs = event.kind === "createRenderPipeline" ? [
4748
+ ["vertex", event.vertexShaderModuleHandleId, event.desc.vertex?.entryPoint],
4749
+ ["fragment", event.fragmentShaderModuleHandleId, event.desc.fragment?.entryPoint]
4750
+ ] : event.kind === "createComputePipeline" ? [["compute", event.computeShaderModuleHandleId, event.desc.compute.entryPoint]] : [];
4751
+ for (const [stage, moduleId, entryPoint] of shaderRefs) {
4752
+ if (typeof moduleId !== "string") continue;
4753
+ const shader = shaderEvents.get(moduleId);
4754
+ shaders.push({
4755
+ stage,
4756
+ moduleHandleId: moduleId,
4757
+ entryPoint: typeof entryPoint === "string" ? entryPoint : null,
4758
+ source: shader?.kind === "createShaderModule" ? shader.wgslCode : null
4759
+ });
4760
+ }
4761
+ return {
4762
+ status: "available",
4763
+ pipelineHandleId: pipelineId,
4764
+ kind: event.kind === "createRenderPipeline" ? "render" : "compute",
4765
+ descriptor: eventDescriptor(event),
4766
+ shaders
4767
+ };
4768
+ }
4721
4769
  function buildFrameModel(tape) {
4722
4770
  const index = buildTapeIndex(tape);
4723
- const works = index.works;
4724
- return {
4725
- tree: [],
4726
- draws: [],
4727
- meta: {
4728
- totalDraws: works.length,
4729
- totalPasses: index.passes.length,
4730
- hasCompute: works.some((work) => work.kind.startsWith("dispatch"))
4731
- },
4732
- commands: tape.events.map((event, eventIndex) => ({
4733
- passIdx: index.passIndexByEvent[eventIndex] ?? -1,
4734
- eventIdx: eventIndex,
4735
- isDraw: isWorkEvent(event.kind),
4771
+ const events = tape.events;
4772
+ const passCommandIndices = index.passes.map(() => []);
4773
+ const commands = [];
4774
+ const groupPath = [];
4775
+ const currentPassByEvent = index.passIndexByEvent;
4776
+ const passAttachmentByIndex = /* @__PURE__ */ new Map();
4777
+ for (const pass of index.passes) {
4778
+ passAttachmentByIndex.set(pass.passIndex, {
4779
+ passIndex: pass.passIndex,
4780
+ kind: pass.kind,
4781
+ beginEventIndex: pass.beginEventIndex,
4782
+ endEventIndex: pass.endEventIndex ?? null,
4783
+ workIndices: pass.workIndices,
4784
+ commandIndices: [],
4785
+ colorAttachmentViewHandleIds: [],
4786
+ depthStencilViewHandleId: null
4787
+ });
4788
+ const begin = events[pass.beginEventIndex];
4789
+ if (begin?.kind === "beginRenderPass") {
4790
+ const attachment = passAttachmentByIndex.get(pass.passIndex);
4791
+ if (attachment !== void 0) {
4792
+ attachment.colorAttachmentViewHandleIds = begin.colorAttachmentViewHandleIds.filter(
4793
+ (id) => typeof id === "string"
4794
+ );
4795
+ attachment.depthStencilViewHandleId = begin.depthStencilViewHandleId ?? null;
4796
+ }
4797
+ }
4798
+ }
4799
+ for (const [eventIndex, event] of events.entries()) {
4800
+ const passIndex = currentPassByEvent[eventIndex] ?? -1;
4801
+ const isPush = event.kind === "pushDebugGroup" || event.kind === "passPushDebugGroup";
4802
+ const isPop = event.kind === "popDebugGroup" || event.kind === "passPopDebugGroup";
4803
+ const group = isPush ? [...groupPath, event.groupLabel] : [...groupPath];
4804
+ const marker = event.kind === "insertDebugMarker" || event.kind === "passInsertDebugMarker" ? event.markerLabel : void 0;
4805
+ const commandIndex = commands.length;
4806
+ commands.push({
4807
+ eventIndex,
4808
+ passIndex,
4736
4809
  kind: event.kind,
4737
- groupLabel: void 0,
4738
- markerLabel: void 0
4739
- })),
4740
- resources: /* @__PURE__ */ new Map(),
4741
- resourceLifecycle: buildResourceLifecycle(tape.events),
4742
- works,
4743
- passes: index.passes,
4744
- resourceEntries: index.resources
4810
+ category: EVENT_SEMANTICS[event.kind].category,
4811
+ isWork: isWorkEvent(event.kind),
4812
+ params: eventDescriptor(event),
4813
+ group,
4814
+ ...marker === void 0 ? {} : { marker }
4815
+ });
4816
+ if (passIndex >= 0) passCommandIndices[passIndex]?.push(commandIndex);
4817
+ if (isPush) groupPath.push(event.groupLabel);
4818
+ if (isPop) groupPath.pop();
4819
+ }
4820
+ const pipelineEvents = /* @__PURE__ */ new Map();
4821
+ const shaderEvents = /* @__PURE__ */ new Map();
4822
+ const bindGroups = /* @__PURE__ */ new Map();
4823
+ const resourceRecords = /* @__PURE__ */ new Map();
4824
+ const workByEvent = /* @__PURE__ */ new Map();
4825
+ for (const work of index.works) workByEvent.set(work.eventIndex, work.workIndex);
4826
+ for (const bootstrap of tape.bootstrap) {
4827
+ const create = bootstrap.create;
4828
+ if (create.kind === "createRenderPipeline" || create.kind === "createComputePipeline")
4829
+ pipelineEvents.set(create.handleId, create);
4830
+ if (create.kind === "createShaderModule") shaderEvents.set(create.handleId, create);
4831
+ if (create.kind === "createBindGroup") bindGroups.set(create.handleId, create);
4832
+ resourceRecords.set(bootstrap.handleId, {
4833
+ resourceId: bootstrap.handleId,
4834
+ kind: bootstrap.kind,
4835
+ origin: "bootstrap",
4836
+ createEventIndex: null,
4837
+ destroyEventIndex: null,
4838
+ descriptor: jsonValue(bootstrap.create),
4839
+ consumers: [],
4840
+ lifecycle: { state: "unavailable", byteEstimate: null }
4841
+ });
4842
+ }
4843
+ for (const [eventIndex, event] of events.entries()) {
4844
+ if (event.kind === "createRenderPipeline" || event.kind === "createComputePipeline")
4845
+ pipelineEvents.set(event.handleId, event);
4846
+ if (event.kind === "createShaderModule") shaderEvents.set(event.handleId, event);
4847
+ if (event.kind === "createBindGroup") bindGroups.set(event.handleId, event);
4848
+ const kind = resourceKindForEvent(event.kind);
4849
+ const handleId = kind === void 0 ? void 0 : event.kind === "createTextureView" ? event.resultHandleId : event.kind === "createCommandEncoder" ? event.cmdHandleId : "handleId" in event ? event.handleId : void 0;
4850
+ if (kind !== void 0 && handleId !== void 0) {
4851
+ resourceRecords.set(handleId, {
4852
+ resourceId: handleId,
4853
+ kind,
4854
+ origin: "frame",
4855
+ createEventIndex: eventIndex,
4856
+ destroyEventIndex: null,
4857
+ descriptor: eventDescriptor(event),
4858
+ consumers: [],
4859
+ lifecycle: { state: "unavailable", byteEstimate: null }
4860
+ });
4861
+ }
4862
+ }
4863
+ const lifecycle = buildResourceLifecycle(events);
4864
+ for (const record of lifecycle.resources) {
4865
+ const resource = resourceRecords.get(record.handleId);
4866
+ if (resource === void 0) continue;
4867
+ resourceRecords.set(record.handleId, {
4868
+ ...resource,
4869
+ destroyEventIndex: record.destroyedEventIndex ?? null,
4870
+ lifecycle: { state: record.state, byteEstimate: record.byteEstimate }
4871
+ });
4872
+ }
4873
+ for (const [eventIndex, event] of events.entries()) {
4874
+ const workIndex = workByEvent.get(eventIndex) ?? null;
4875
+ for (const resourceId of EVENT_SEMANTICS[event.kind].read(event)) {
4876
+ const resource = resourceRecords.get(resourceId);
4877
+ if (resource !== void 0)
4878
+ resource.consumers = [...resource.consumers, { eventIndex, workIndex, access: "read" }];
4879
+ }
4880
+ for (const resourceId of EVENT_SEMANTICS[event.kind].written(event)) {
4881
+ const resource = resourceRecords.get(resourceId);
4882
+ if (resource !== void 0)
4883
+ resource.consumers = [...resource.consumers, { eventIndex, workIndex, access: "write" }];
4884
+ }
4885
+ }
4886
+ const works = [];
4887
+ const currentPipeline = /* @__PURE__ */ new Map();
4888
+ const currentVertexBuffers = /* @__PURE__ */ new Map();
4889
+ const currentIndexBuffers = /* @__PURE__ */ new Map();
4890
+ const currentBindGroups = /* @__PURE__ */ new Map();
4891
+ const workByEventEntry = new Map(index.works.map((work) => [work.eventIndex, work]));
4892
+ for (const [eventIndex, event] of events.entries()) {
4893
+ if (event === void 0) continue;
4894
+ const passHandleId = "passHandleId" in event ? event.passHandleId : "";
4895
+ if (event.kind === "setPipeline" || event.kind === "setComputePipeline")
4896
+ currentPipeline.set(passHandleId, event.pipelineHandleId);
4897
+ if (event.kind === "setVertexBuffer") {
4898
+ const buffers = currentVertexBuffers.get(passHandleId) ?? /* @__PURE__ */ new Map();
4899
+ buffers.set(event.slot, {
4900
+ bufferHandleId: event.bufferHandleId,
4901
+ offset: event.offset ?? 0,
4902
+ size: event.size ?? null
4903
+ });
4904
+ currentVertexBuffers.set(passHandleId, buffers);
4905
+ }
4906
+ if (event.kind === "setIndexBuffer")
4907
+ currentIndexBuffers.set(passHandleId, {
4908
+ bufferHandleId: event.bufferHandleId,
4909
+ format: event.format,
4910
+ offset: event.offset ?? 0,
4911
+ size: event.size ?? null
4912
+ });
4913
+ if (event.kind === "setBindGroup") {
4914
+ const groups2 = currentBindGroups.get(passHandleId) ?? /* @__PURE__ */ new Map();
4915
+ groups2.set(event.index, event.bindGroupHandleId);
4916
+ currentBindGroups.set(passHandleId, groups2);
4917
+ }
4918
+ if (!isWorkEvent(event.kind)) continue;
4919
+ const work = workByEventEntry.get(eventIndex);
4920
+ if (work === void 0) continue;
4921
+ const pipelineId = currentPipeline.get(passHandleId);
4922
+ const groups = currentBindGroups.get(passHandleId) ?? /* @__PURE__ */ new Map();
4923
+ const bindings = [];
4924
+ for (const [groupIndex, bindGroupId] of groups) {
4925
+ const bindGroup = bindGroups.get(bindGroupId);
4926
+ for (const [entryIndex, resourceId] of bindGroup?.resourceHandleIds.entries() ?? []) {
4927
+ const entry = bindGroup?.entries[entryIndex];
4928
+ bindings.push({
4929
+ groupIndex,
4930
+ binding: entry?.binding ?? entryIndex,
4931
+ bindGroupId,
4932
+ resourceId: resourceId ?? null,
4933
+ resourceKind: entry?.resourceKind ?? null,
4934
+ bufferOffset: entry?.bufferOffset ?? null,
4935
+ bufferSize: entry?.bufferSize ?? null
4936
+ });
4937
+ }
4938
+ }
4939
+ const attachment = passAttachmentByIndex.get(work.passIndex);
4940
+ works.push({
4941
+ ...work,
4942
+ commandIndex: commands.findIndex((command) => command.eventIndex === work.eventIndex),
4943
+ drawCall: eventDescriptor(event),
4944
+ pipeline: workPipeline(pipelineId, pipelineEvents, shaderEvents),
4945
+ bindings,
4946
+ vertexBuffers: [...currentVertexBuffers.get(passHandleId)?.entries() ?? []].map(
4947
+ ([slot, buffer]) => ({ slot, ...buffer })
4948
+ ),
4949
+ indexBuffer: currentIndexBuffers.get(passHandleId) ?? null,
4950
+ attachments: attachment === void 0 ? null : {
4951
+ colorViewHandleIds: attachment.colorAttachmentViewHandleIds,
4952
+ depthStencilViewHandleId: attachment.depthStencilViewHandleId
4953
+ }
4954
+ });
4955
+ }
4956
+ const passes = index.passes.map((pass) => ({
4957
+ ...pass,
4958
+ endEventIndex: pass.endEventIndex ?? null,
4959
+ commandIndices: passCommandIndices[pass.passIndex] ?? [],
4960
+ colorAttachmentViewHandleIds: passAttachmentByIndex.get(pass.passIndex)?.colorAttachmentViewHandleIds ?? [],
4961
+ depthStencilViewHandleId: passAttachmentByIndex.get(pass.passIndex)?.depthStencilViewHandleId ?? null
4962
+ }));
4963
+ return {
4964
+ commands,
4965
+ passes,
4966
+ resources: [...resourceRecords.values()],
4967
+ resourceLifecycle: lifecycle,
4968
+ works
4745
4969
  };
4746
4970
  }
4747
4971
  function asPositiveInteger(value) {
@@ -8128,6 +8352,31 @@ function attachRecorder(backend, options = {}) {
8128
8352
  };
8129
8353
  return ok(attachment);
8130
8354
  }
8355
+
8356
+ // src/replay/device-request.ts
8357
+ var RECORDED_CAPABILITY_FEATURES = [
8358
+ ["timestampQuery", "timestamp-query"],
8359
+ ["textureCompressionBc", "texture-compression-bc"],
8360
+ ["textureCompressionEtc2", "texture-compression-etc2"],
8361
+ ["textureCompressionAstc", "texture-compression-astc"],
8362
+ ["firstInstanceIndirect", "indirect-first-instance"],
8363
+ ["float32Filterable", "float32-filterable"],
8364
+ ["rg11b10ufloatRenderable", "rg11b10ufloat-renderable"]
8365
+ ];
8366
+ function replayDeviceRequest(tape, adapterFeatures, adapterLimits) {
8367
+ const requiredFeatures = RECORDED_CAPABILITY_FEATURES.filter(
8368
+ ([capability, feature]) => tape.header.rhiCaps[capability] === true && adapterFeatures.has(feature)
8369
+ ).map(([, feature]) => feature);
8370
+ const limitEntries = Object.entries(adapterLimits).filter(
8371
+ ([, value]) => Number.isFinite(value) && value >= 0
8372
+ );
8373
+ const request = {};
8374
+ if (requiredFeatures.length > 0) request.requiredFeatures = requiredFeatures;
8375
+ if (limitEntries.length > 0) {
8376
+ request.requiredLimits = Object.fromEntries(limitEntries);
8377
+ }
8378
+ return request;
8379
+ }
8131
8380
  function eventRecord(event) {
8132
8381
  return JSON.parse(JSON.stringify(event));
8133
8382
  }
@@ -9059,18 +9308,59 @@ async function readReplayResource(device, table, resourceId, subresource, create
9059
9308
  const entry = table.get(resourceId);
9060
9309
  if (entry === void 0)
9061
9310
  return readbackFailure(`resource ${resourceId} is not present in the current generation`);
9311
+ let result;
9062
9312
  if (entry.resource.kind === "texture-view") {
9063
9313
  const sourceId = stringField2(entry.descriptor, "sourceHandleId");
9064
9314
  const source = sourceId === void 0 ? void 0 : table.get(sourceId);
9065
9315
  if (source === void 0 || source.resource.kind !== "texture") {
9066
9316
  return readbackFailure(`texture view ${resourceId} has no readable source texture`);
9067
9317
  }
9068
- return readTexture(device, source, resourceId, subresource, createShaderModule2);
9318
+ const sourceSubresource = resolveTextureViewSubresource(entry, source, subresource);
9319
+ if (!sourceSubresource.ok) return sourceSubresource;
9320
+ result = await readTexture(
9321
+ device,
9322
+ source,
9323
+ resourceId,
9324
+ sourceSubresource.value,
9325
+ createShaderModule2
9326
+ );
9327
+ } else if (entry.resource.kind === "texture") {
9328
+ result = await readTexture(device, entry, resourceId, subresource, createShaderModule2);
9329
+ } else if (entry.resource.kind === "buffer") {
9330
+ result = await readBuffer(device, entry, resourceId, subresource);
9331
+ } else {
9332
+ return readbackFailure(`resource ${resourceId} is not readable by the v7 core matrix`);
9333
+ }
9334
+ if (!result.ok) return result;
9335
+ return ok({
9336
+ ...result.value,
9337
+ provenance: {
9338
+ generation: table.generation,
9339
+ resourceId,
9340
+ subresource: subresource ?? null
9341
+ }
9342
+ });
9343
+ }
9344
+ function resolveTextureViewSubresource(view, source, requested) {
9345
+ if (requested === void 0 || isBufferRange(requested)) return ok(requested);
9346
+ const sourceDescriptor = recordField(source.descriptor, "desc");
9347
+ const sourceSize = textureSize(sourceDescriptor?.size);
9348
+ const sourceMipCount = numberField(sourceDescriptor, "mipLevelCount") ?? 1;
9349
+ const viewDescriptor = recordField(view.descriptor, "desc");
9350
+ const baseMipLevel = numberField(viewDescriptor, "baseMipLevel") ?? 0;
9351
+ const baseArrayLayer = numberField(viewDescriptor, "baseArrayLayer") ?? 0;
9352
+ const mipLevelCount = numberField(viewDescriptor, "mipLevelCount") ?? sourceMipCount - baseMipLevel;
9353
+ const arrayLayerCount = numberField(viewDescriptor, "arrayLayerCount") ?? sourceSize.depthOrArrayLayers - baseArrayLayer;
9354
+ const localMipLevel = requested.mipLevel ?? 0;
9355
+ const localArrayLayer = requested.arrayLayer ?? 0;
9356
+ if (!validIndex(baseMipLevel, sourceMipCount + 1) || !validIndex(baseArrayLayer, sourceSize.depthOrArrayLayers + 1) || !validIndex(localMipLevel, mipLevelCount) || !validIndex(localArrayLayer, arrayLayerCount) || baseMipLevel + mipLevelCount > sourceMipCount || baseArrayLayer + arrayLayerCount > sourceSize.depthOrArrayLayers) {
9357
+ return readbackFailure("texture view subresource is outside the recorded view extent");
9069
9358
  }
9070
- if (entry.resource.kind === "texture")
9071
- return readTexture(device, entry, resourceId, subresource, createShaderModule2);
9072
- if (entry.resource.kind === "buffer") return readBuffer(device, entry, resourceId, subresource);
9073
- return readbackFailure(`resource ${resourceId} is not readable by the v7 core matrix`);
9359
+ return ok({
9360
+ ...requested,
9361
+ mipLevel: baseMipLevel + localMipLevel,
9362
+ arrayLayer: baseArrayLayer + localArrayLayer
9363
+ });
9074
9364
  }
9075
9365
  async function readBuffer(device, entry, resourceId, subresource) {
9076
9366
  const size = numberField(recordField(entry.descriptor, "desc"), "size");
@@ -9118,18 +9408,15 @@ async function readTexture(device, entry, resourceId, subresource, createShaderM
9118
9408
  const width = Math.max(1, Math.floor(size.width / 2 ** mipLevel));
9119
9409
  const height = Math.max(1, Math.floor(size.height / 2 ** mipLevel));
9120
9410
  const requestedAspect = textureAspect(subresource);
9121
- if (format === "depth24plus" || format === "depth24plus-stencil8") {
9122
- if (format === "depth24plus-stencil8" && requestedAspect === "all") {
9411
+ if (isDepthStencilTextureFormat(format)) {
9412
+ if (requestedAspect === "all") {
9123
9413
  return readbackUnsupported(
9124
9414
  resourceId,
9125
9415
  format,
9126
- "depth24plus-stencil8 requires an explicit depth-only or stencil-only aspect"
9416
+ `${format} requires an explicit depth-only or stencil-only aspect`
9127
9417
  );
9128
9418
  }
9129
9419
  if (requestedAspect === "stencil-only") {
9130
- if (format !== "depth24plus-stencil8") {
9131
- return readbackUnsupported(resourceId, format, "depth24plus has no stencil aspect");
9132
- }
9133
9420
  const bytes2 = await copyTextureBytes(
9134
9421
  device,
9135
9422
  entry.resource.value,
@@ -9147,6 +9434,24 @@ async function readTexture(device, entry, resourceId, subresource, createShaderM
9147
9434
  if (!bytes2.ok) return bytes2;
9148
9435
  return ok({ resourceId, kind: "texture", format, width, height, bytes: bytes2.value });
9149
9436
  }
9437
+ if (format === "depth24plus-stencil8") {
9438
+ return blitDepth24PlusTexture(
9439
+ device,
9440
+ entry.resource.value,
9441
+ resourceId,
9442
+ format,
9443
+ width,
9444
+ height,
9445
+ mipLevel,
9446
+ arrayLayer,
9447
+ createShaderModule2
9448
+ );
9449
+ }
9450
+ }
9451
+ if (format === "depth24plus") {
9452
+ if (requestedAspect === "stencil-only") {
9453
+ return readbackUnsupported(resourceId, format, "depth24plus has no stencil aspect");
9454
+ }
9150
9455
  return blitDepth24PlusTexture(
9151
9456
  device,
9152
9457
  entry.resource.value,
@@ -9354,7 +9659,10 @@ async function copyBufferBytes(device, source, sourceOffset, size) {
9354
9659
  let staging;
9355
9660
  let mapped;
9356
9661
  try {
9357
- const created = device.createBuffer({ size: Math.max(4, size), usage: COPY_DST_MAP_READ2 });
9662
+ const alignedSourceOffset = sourceOffset - sourceOffset % 4;
9663
+ const leadingBytes = sourceOffset - alignedSourceOffset;
9664
+ const copySize = align4(leadingBytes + size);
9665
+ const created = device.createBuffer({ size: Math.max(4, copySize), usage: COPY_DST_MAP_READ2 });
9358
9666
  if (!created.ok)
9359
9667
  return readbackFailure(`staging buffer creation failed: ${created.error.code}`);
9360
9668
  staging = created.value;
@@ -9362,7 +9670,7 @@ async function copyBufferBytes(device, source, sourceOffset, size) {
9362
9670
  if (!encoderResult.ok)
9363
9671
  return readbackFailure(`readback encoder creation failed: ${encoderResult.error.code}`);
9364
9672
  const encoder = encoderResult.value;
9365
- encoder.copyBufferToBuffer(source, sourceOffset, staging, 0, size);
9673
+ encoder.copyBufferToBuffer(source, alignedSourceOffset, staging, 0, copySize);
9366
9674
  const finished = encoder.finish();
9367
9675
  if (!finished.ok)
9368
9676
  return readbackFailure(`readback encoder finish failed: ${finished.error.code}`);
@@ -9372,9 +9680,9 @@ async function copyBufferBytes(device, source, sourceOffset, size) {
9372
9680
  const mappedResult = await staging.mapAsync(GPU_MAP_MODE_READ);
9373
9681
  if (!mappedResult.ok) return readbackFailure(`readback map failed: ${mappedResult.error.code}`);
9374
9682
  mapped = mappedResult.value;
9375
- const range = mapped.getMappedRange(0, size);
9683
+ const range = mapped.getMappedRange(0, copySize);
9376
9684
  if (!range.ok) return readbackFailure(`readback mapped range failed: ${range.error.code}`);
9377
- return ok(new Uint8Array(range.value).slice());
9685
+ return ok(new Uint8Array(range.value).slice(leadingBytes, leadingBytes + size));
9378
9686
  } catch (cause) {
9379
9687
  return readbackFailure(`buffer readback failed: ${messageOf(cause)}`);
9380
9688
  } finally {
@@ -9489,6 +9797,9 @@ function validIndex(value, count) {
9489
9797
  function align256(value) {
9490
9798
  return Math.max(256, Math.ceil(value / 256) * 256);
9491
9799
  }
9800
+ function align4(value) {
9801
+ return Math.ceil(value / 4) * 4;
9802
+ }
9492
9803
  function messageOf(cause) {
9493
9804
  return cause instanceof Error ? cause.message : String(cause);
9494
9805
  }
@@ -9584,6 +9895,7 @@ async function openReplay(tape, backend) {
9584
9895
  const capabilityFailure = checkCapabilities(tape, backend.device);
9585
9896
  if (capabilityFailure !== void 0) return err(capabilityFailure);
9586
9897
  const index = buildTapeIndex(tape);
9898
+ const model = buildFrameModel(tape);
9587
9899
  const table = new ResourceTable(backend.device, 0);
9588
9900
  let disposed = false;
9589
9901
  let prepared = false;
@@ -9621,7 +9933,9 @@ async function openReplay(tape, backend) {
9621
9933
  if (disposed) return positionError(workIndex, index.works.length);
9622
9934
  if (signal?.aborted) return positionError(workIndex, index.works.length);
9623
9935
  const work = index.works[workIndex];
9936
+ const modelWork = model.works[workIndex];
9624
9937
  if (work === void 0) return positionError(workIndex, index.works.length);
9938
+ if (modelWork === void 0) return positionError(workIndex, index.works.length);
9625
9939
  const cleared = await reset();
9626
9940
  if (!cleared.ok) return cleared;
9627
9941
  const bootstrapped = await prepare();
@@ -9630,11 +9944,29 @@ async function openReplay(tape, backend) {
9630
9944
  if (!replayed.ok) return replayed;
9631
9945
  const attachment = fields?.includes("pixels") ? await readWorkAttachment(context, index, work) : void 0;
9632
9946
  if (attachment !== void 0 && !attachment.ok) return attachment;
9633
- return ok({
9947
+ const selectedAttachment = attachment?.ok === true ? {
9948
+ ...attachment.value,
9949
+ provenance: {
9950
+ ...attachment.value.provenance,
9951
+ selectedWorkIndex: work.workIndex
9952
+ }
9953
+ } : void 0;
9954
+ const baseInspection = {
9634
9955
  workIndex: work.workIndex,
9635
9956
  eventIndex: work.eventIndex,
9636
9957
  passIndex: work.passIndex,
9637
- attachment: attachment === void 0 ? void 0 : attachment.value
9958
+ attachment: selectedAttachment
9959
+ };
9960
+ return ok({
9961
+ ...baseInspection,
9962
+ ...fields?.includes("pipeline") ? { pipeline: modelWork.pipeline } : {},
9963
+ ...fields?.includes("bindings") ? {
9964
+ bindings: modelWork.bindings,
9965
+ vertexBuffers: modelWork.vertexBuffers,
9966
+ indexBuffer: modelWork.indexBuffer,
9967
+ shaders: modelWork.pipeline.shaders,
9968
+ resourceIds: modelWork.bindings.map((binding) => binding.resourceId).filter((resourceId) => resourceId !== null)
9969
+ } : {}
9638
9970
  });
9639
9971
  },
9640
9972
  async readResource(resourceId, subresource, signal) {
@@ -10000,7 +10332,10 @@ function readTexel(view, off, info, channelBytes) {
10000
10332
  }
10001
10333
  return out;
10002
10334
  }
10003
- function decodeToRgba8(bytes, format, width, height) {
10335
+ function decodeToRgba8(bytes, format, width, height, aspect = "all") {
10336
+ if (format.startsWith("depth")) {
10337
+ return decodeDepthToRgba8(bytes, format, width, height, aspect);
10338
+ }
10004
10339
  const info = formatInfo(format);
10005
10340
  const texBytes = bytesPerTexel(format);
10006
10341
  if (!info || texBytes === void 0) return null;
@@ -10032,6 +10367,26 @@ function decodeToRgba8(bytes, format, width, height) {
10032
10367
  }
10033
10368
  return out;
10034
10369
  }
10370
+ function decodeDepthToRgba8(bytes, format, width, height, aspect) {
10371
+ const combined = format.endsWith("-stencil8");
10372
+ if (combined && aspect === "all") return null;
10373
+ if (!combined && aspect === "stencil-only") return null;
10374
+ const stencil = aspect === "stencil-only";
10375
+ const bytesPerValue = stencil ? 1 : 4;
10376
+ const texelCount = width * height;
10377
+ if (bytes.byteLength < texelCount * bytesPerValue) return null;
10378
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
10379
+ const out = new Uint8ClampedArray(new ArrayBuffer(texelCount * 4));
10380
+ for (let index = 0; index < texelCount; index++) {
10381
+ const value = stencil ? view.getUint8(index) : Math.round(clamp01(view.getFloat32(index * 4, true)) * 255);
10382
+ const target = index * 4;
10383
+ out[target] = value;
10384
+ out[target + 1] = value;
10385
+ out[target + 2] = value;
10386
+ out[target + 3] = 255;
10387
+ }
10388
+ return out;
10389
+ }
10035
10390
  function decodeTexelRaw(bytes, format, width, height, texelX, texelY) {
10036
10391
  if (texelX < 0 || texelX >= width || texelY < 0 || texelY >= height) return null;
10037
10392
  const info = formatInfo(format);
@@ -10055,6 +10410,6 @@ function decodeTexelRaw(bytes, format, width, height, texelX, texelY) {
10055
10410
  return [r, channels[1], b, a];
10056
10411
  }
10057
10412
 
10058
- export { EVENT_SEMANTICS, TAPE_MAGIC, TAPE_FORMAT_VERSION as V7_TAPE_FORMAT_VERSION, attachRecorder, buildFrameModel, buildTapeIndex, bytesPerTexel, createRhiDebugError, decodeTape, decodeTexelRaw, decodeToRgba8, encodeTape, eventKinds, formatInfo, halfToFloat, isWorkEvent, openReplay, readbackTexturePixels, resourceKindForEvent, workEventKinds };
10413
+ export { EVENT_SEMANTICS, TAPE_MAGIC, TAPE_FORMAT_VERSION as V7_TAPE_FORMAT_VERSION, attachRecorder, buildFrameModel, buildResourceLifecycle, buildTapeIndex, bytesPerTexel, createRhiDebugError, decodeTape, decodeTexelRaw, decodeToRgba8, encodeTape, eventKinds, formatInfo, halfToFloat, isWorkEvent, openReplay, readbackTexturePixels, replayDeviceRequest, resourceKindForEvent, workEventKinds };
10059
10414
  //# sourceMappingURL=index.mjs.map
10060
10415
  //# sourceMappingURL=index.mjs.map