@forgeax/engine-rhi-debug 0.0.0-dev.8d955ade1c79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +178 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/bootstrap-integration.test.d.ts +2 -0
- package/dist/__tests__/bootstrap-integration.test.d.ts.map +1 -0
- package/dist/__tests__/consumer-inventory.unit.test.d.ts +2 -0
- package/dist/__tests__/consumer-inventory.unit.test.d.ts.map +1 -0
- package/dist/__tests__/copy-src-usage-validity.dawn.test.d.ts +15 -0
- package/dist/__tests__/copy-src-usage-validity.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/coverage-invariant.test-d.d.ts +2 -0
- package/dist/__tests__/coverage-invariant.test-d.d.ts.map +1 -0
- package/dist/__tests__/coverage-invariant.unit.test.d.ts +2 -0
- package/dist/__tests__/coverage-invariant.unit.test.d.ts.map +1 -0
- package/dist/__tests__/e2e.browser.test.d.ts +2 -0
- package/dist/__tests__/e2e.browser.test.d.ts.map +1 -0
- package/dist/__tests__/error-code-narrowing.test-d.d.ts +2 -0
- package/dist/__tests__/error-code-narrowing.test-d.d.ts.map +1 -0
- package/dist/__tests__/errors-v7.test-d.d.ts +2 -0
- package/dist/__tests__/errors-v7.test-d.d.ts.map +1 -0
- package/dist/__tests__/errors-v7.unit.test.d.ts +2 -0
- package/dist/__tests__/errors-v7.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors.unit.test.d.ts +2 -0
- package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/event-semantics-coverage.unit.test.d.ts +2 -0
- package/dist/__tests__/event-semantics-coverage.unit.test.d.ts.map +1 -0
- package/dist/__tests__/format-info.unit.test.d.ts +2 -0
- package/dist/__tests__/format-info.unit.test.d.ts.map +1 -0
- package/dist/__tests__/frame-model-parity.test-d.d.ts +2 -0
- package/dist/__tests__/frame-model-parity.test-d.d.ts.map +1 -0
- package/dist/__tests__/frame-model-parity.unit.test.d.ts +2 -0
- package/dist/__tests__/frame-model-parity.unit.test.d.ts.map +1 -0
- package/dist/__tests__/guard-gates.test.d.ts +2 -0
- package/dist/__tests__/guard-gates.test.d.ts.map +1 -0
- package/dist/__tests__/public-surface.integration.test.d.ts +2 -0
- package/dist/__tests__/public-surface.integration.test.d.ts.map +1 -0
- package/dist/__tests__/readback-fault-cleanup.unit.test.d.ts +2 -0
- package/dist/__tests__/readback-fault-cleanup.unit.test.d.ts.map +1 -0
- package/dist/__tests__/readback-format-matrix.dawn.test.d.ts +2 -0
- package/dist/__tests__/readback-format-matrix.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/readback-format-matrix.unit.test.d.ts +2 -0
- package/dist/__tests__/readback-format-matrix.unit.test.d.ts.map +1 -0
- package/dist/__tests__/readback-matrix-fixture.d.ts +32 -0
- package/dist/__tests__/readback-matrix-fixture.d.ts.map +1 -0
- package/dist/__tests__/recorder-bootstrap.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-bootstrap.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-budget.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-budget.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-fail-fast.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-fail-fast.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-lifecycle.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-lifecycle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-push-event.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-push-event.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-session.integration.test.d.ts +2 -0
- package/dist/__tests__/recorder-session.integration.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-session.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-session.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-steady-frame.dawn.test.d.ts +2 -0
- package/dist/__tests__/recorder-steady-frame.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/replay-fail-fast.unit.test.d.ts +2 -0
- package/dist/__tests__/replay-fail-fast.unit.test.d.ts.map +1 -0
- package/dist/__tests__/replay-session.dawn.test.d.ts +2 -0
- package/dist/__tests__/replay-session.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/replay-session.test-d.d.ts +2 -0
- package/dist/__tests__/replay-session.test-d.d.ts.map +1 -0
- package/dist/__tests__/replay-session.unit.test.d.ts +2 -0
- package/dist/__tests__/replay-session.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-texture-descriptor.unit.test.d.ts +2 -0
- package/dist/__tests__/resolve-texture-descriptor.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resource-lifecycle.unit.test.d.ts +2 -0
- package/dist/__tests__/resource-lifecycle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-debug-fresh-replay.dawn.test.d.ts +2 -0
- package/dist/__tests__/rhi-debug-fresh-replay.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/tape-index.unit.test.d.ts +2 -0
- package/dist/__tests__/tape-index.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tape-v7-invalid.unit.test.d.ts +2 -0
- package/dist/__tests__/tape-v7-invalid.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tape-v7-roundtrip.unit.test.d.ts +2 -0
- package/dist/__tests__/tape-v7-roundtrip.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tape-v7-version.unit.test.d.ts +2 -0
- package/dist/__tests__/tape-v7-version.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tree-shake.unit.test.d.ts +2 -0
- package/dist/__tests__/tree-shake.unit.test.d.ts.map +1 -0
- package/dist/__tests__/types.test.d.ts +2 -0
- package/dist/__tests__/types.test.d.ts.map +1 -0
- package/dist/browser.d.ts +45 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.mjs +83 -0
- package/dist/browser.mjs.map +1 -0
- package/dist/errors.d.ts +85 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/frame-model.d.ts +158 -0
- package/dist/frame-model.d.ts.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +10415 -0
- package/dist/index.mjs.map +1 -0
- package/dist/protocol/codec.d.ts +6 -0
- package/dist/protocol/codec.d.ts.map +1 -0
- package/dist/protocol/event-semantics.d.ts +15 -0
- package/dist/protocol/event-semantics.d.ts.map +1 -0
- package/dist/protocol/tape-index.d.ts +30 -0
- package/dist/protocol/tape-index.d.ts.map +1 -0
- package/dist/protocol/types.d.ts +44 -0
- package/dist/protocol/types.d.ts.map +1 -0
- package/dist/protocol/validation.d.ts +7 -0
- package/dist/protocol/validation.d.ts.map +1 -0
- package/dist/readback.d.ts +156 -0
- package/dist/readback.d.ts.map +1 -0
- package/dist/recorder/assemble.d.ts +11 -0
- package/dist/recorder/assemble.d.ts.map +1 -0
- package/dist/recorder/closure.d.ts +39 -0
- package/dist/recorder/closure.d.ts.map +1 -0
- package/dist/recorder/core.d.ts +296 -0
- package/dist/recorder/core.d.ts.map +1 -0
- package/dist/recorder/device.d.ts +4 -0
- package/dist/recorder/device.d.ts.map +1 -0
- package/dist/recorder/encoder.d.ts +5 -0
- package/dist/recorder/encoder.d.ts.map +1 -0
- package/dist/recorder/lifecycle.d.ts +20 -0
- package/dist/recorder/lifecycle.d.ts.map +1 -0
- package/dist/recorder/pass.d.ts +6 -0
- package/dist/recorder/pass.d.ts.map +1 -0
- package/dist/recorder/proxy.d.ts +19 -0
- package/dist/recorder/proxy.d.ts.map +1 -0
- package/dist/recorder/queue.d.ts +4 -0
- package/dist/recorder/queue.d.ts.map +1 -0
- package/dist/recorder/resource-registry.d.ts +15 -0
- package/dist/recorder/resource-registry.d.ts.map +1 -0
- package/dist/recorder/session.d.ts +25 -0
- package/dist/recorder/session.d.ts.map +1 -0
- package/dist/recorder/shader.d.ts +3 -0
- package/dist/recorder/shader.d.ts.map +1 -0
- package/dist/recorder/snapshot.d.ts +10 -0
- package/dist/recorder/snapshot.d.ts.map +1 -0
- package/dist/recorder/wrap.d.ts +4 -0
- package/dist/recorder/wrap.d.ts.map +1 -0
- package/dist/recorder.d.ts +7 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/replay/device-request.d.ts +8 -0
- package/dist/replay/device-request.d.ts.map +1 -0
- package/dist/replay/execute-support.d.ts +33 -0
- package/dist/replay/execute-support.d.ts.map +1 -0
- package/dist/replay/execute.d.ts +18 -0
- package/dist/replay/execute.d.ts.map +1 -0
- package/dist/replay/readback.d.ts +32 -0
- package/dist/replay/readback.d.ts.map +1 -0
- package/dist/replay/resources.d.ts +57 -0
- package/dist/replay/resources.d.ts.map +1 -0
- package/dist/replay/session.d.ts +49 -0
- package/dist/replay/session.d.ts.map +1 -0
- package/dist/replay/texture-format.d.ts +21 -0
- package/dist/replay/texture-format.d.ts.map +1 -0
- package/dist/texel-decode.d.ts +35 -0
- package/dist/texel-decode.d.ts.map +1 -0
- package/dist/texel-layout.d.ts +76 -0
- package/dist/texel-layout.d.ts.map +1 -0
- package/dist/types.d.ts +722 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +72 -0
- package/src/__tests__/bootstrap-integration.test.ts +268 -0
- package/src/__tests__/consumer-inventory.unit.test.ts +154 -0
- package/src/__tests__/copy-src-usage-validity.dawn.test.ts +117 -0
- package/src/__tests__/coverage-invariant.test-d.ts +115 -0
- package/src/__tests__/coverage-invariant.unit.test.ts +45 -0
- package/src/__tests__/e2e.browser.test.ts +239 -0
- package/src/__tests__/error-code-narrowing.test-d.ts +32 -0
- package/src/__tests__/errors-v7.test-d.ts +19 -0
- package/src/__tests__/errors-v7.unit.test.ts +15 -0
- package/src/__tests__/errors.unit.test.ts +83 -0
- package/src/__tests__/event-semantics-coverage.unit.test.ts +22 -0
- package/src/__tests__/format-info.unit.test.ts +85 -0
- package/src/__tests__/frame-model-parity.test-d.ts +20 -0
- package/src/__tests__/frame-model-parity.unit.test.ts +285 -0
- package/src/__tests__/guard-gates.test.ts +317 -0
- package/src/__tests__/public-surface.integration.test.ts +38 -0
- package/src/__tests__/readback-fault-cleanup.unit.test.ts +66 -0
- package/src/__tests__/readback-format-matrix.dawn.test.ts +378 -0
- package/src/__tests__/readback-format-matrix.unit.test.ts +120 -0
- package/src/__tests__/readback-matrix-fixture.ts +20 -0
- package/src/__tests__/recorder-bootstrap.unit.test.ts +302 -0
- package/src/__tests__/recorder-budget.unit.test.ts +42 -0
- package/src/__tests__/recorder-fail-fast.unit.test.ts +434 -0
- package/src/__tests__/recorder-lifecycle.unit.test.ts +42 -0
- package/src/__tests__/recorder-push-event.unit.test.ts +573 -0
- package/src/__tests__/recorder-session.integration.test.ts +32 -0
- package/src/__tests__/recorder-session.unit.test.ts +63 -0
- package/src/__tests__/recorder-steady-frame.dawn.test.ts +52 -0
- package/src/__tests__/replay-fail-fast.unit.test.ts +155 -0
- package/src/__tests__/replay-session.dawn.test.ts +220 -0
- package/src/__tests__/replay-session.test-d.ts +27 -0
- package/src/__tests__/replay-session.unit.test.ts +171 -0
- package/src/__tests__/resolve-texture-descriptor.unit.test.ts +144 -0
- package/src/__tests__/resource-lifecycle.unit.test.ts +114 -0
- package/src/__tests__/rhi-debug-fresh-replay.dawn.test.ts +178 -0
- package/src/__tests__/tape-index.unit.test.ts +56 -0
- package/src/__tests__/tape-v7-invalid.unit.test.ts +45 -0
- package/src/__tests__/tape-v7-roundtrip.unit.test.ts +66 -0
- package/src/__tests__/tape-v7-version.unit.test.ts +13 -0
- package/src/__tests__/tree-shake.unit.test.ts +151 -0
- package/src/__tests__/types.test.ts +257 -0
- package/src/browser.ts +146 -0
- package/src/errors.ts +173 -0
- package/src/frame-model.ts +753 -0
- package/src/index.ts +92 -0
- package/src/pako.d.ts +8 -0
- package/src/protocol/codec.ts +220 -0
- package/src/protocol/event-semantics.ts +180 -0
- package/src/protocol/tape-index.ts +120 -0
- package/src/protocol/types.ts +60 -0
- package/src/protocol/validation.ts +173 -0
- package/src/readback.ts +804 -0
- package/src/recorder/assemble.ts +192 -0
- package/src/recorder/closure.ts +408 -0
- package/src/recorder/core.ts +587 -0
- package/src/recorder/device.ts +474 -0
- package/src/recorder/encoder.ts +251 -0
- package/src/recorder/lifecycle.ts +847 -0
- package/src/recorder/pass.ts +241 -0
- package/src/recorder/proxy.ts +59 -0
- package/src/recorder/queue.ts +109 -0
- package/src/recorder/resource-registry.ts +53 -0
- package/src/recorder/session.ts +152 -0
- package/src/recorder/shader.ts +47 -0
- package/src/recorder/snapshot.ts +38 -0
- package/src/recorder/wrap.ts +157 -0
- package/src/recorder.ts +15 -0
- package/src/replay/device-request.ts +38 -0
- package/src/replay/execute-support.ts +243 -0
- package/src/replay/execute.ts +870 -0
- package/src/replay/readback.ts +671 -0
- package/src/replay/resources.ts +154 -0
- package/src/replay/session.ts +509 -0
- package/src/replay/texture-format.ts +99 -0
- package/src/texel-decode.ts +289 -0
- package/src/texel-layout.ts +324 -0
- package/src/types.ts +888 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Buffer,
|
|
3
|
+
MappedBuffer,
|
|
4
|
+
RhiCommandEncoder,
|
|
5
|
+
RhiDevice,
|
|
6
|
+
Texture,
|
|
7
|
+
} from '@forgeax/engine-rhi';
|
|
8
|
+
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
9
|
+
import { createRhiDebugError, type RhiDebugError } from '../errors';
|
|
10
|
+
import type { CreateShaderModuleFn } from '../recorder';
|
|
11
|
+
import type { ResourceTable, ResourceTableEntry } from './resources';
|
|
12
|
+
import {
|
|
13
|
+
getTextureReadbackPlan,
|
|
14
|
+
isDepthStencilTextureFormat,
|
|
15
|
+
isDepthTextureFormat,
|
|
16
|
+
textureBytesPerTexel,
|
|
17
|
+
} from './texture-format';
|
|
18
|
+
|
|
19
|
+
const COPY_DST_MAP_READ = 0x9;
|
|
20
|
+
const GPU_MAP_MODE_READ = 0x1;
|
|
21
|
+
|
|
22
|
+
export interface BufferReadbackRange {
|
|
23
|
+
readonly offset: number;
|
|
24
|
+
readonly size?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TextureReadbackSubresource {
|
|
28
|
+
readonly mipLevel?: number;
|
|
29
|
+
readonly arrayLayer?: number;
|
|
30
|
+
readonly aspect?: 'all' | 'depth-only' | 'stencil-only';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ReplayReadbackRequest = BufferReadbackRange | TextureReadbackSubresource;
|
|
34
|
+
|
|
35
|
+
export interface ReplayReadbackProvenance {
|
|
36
|
+
readonly generation: number;
|
|
37
|
+
readonly resourceId: string;
|
|
38
|
+
readonly subresource: ReplayReadbackRequest | null;
|
|
39
|
+
readonly selectedWorkIndex?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ReplayReadbackResult {
|
|
43
|
+
readonly resourceId: string;
|
|
44
|
+
readonly kind: 'buffer' | 'texture';
|
|
45
|
+
readonly format?: string;
|
|
46
|
+
readonly width?: number;
|
|
47
|
+
readonly height?: number;
|
|
48
|
+
readonly bytes: Uint8Array;
|
|
49
|
+
readonly provenance: ReplayReadbackProvenance;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type ReplayReadbackPayload = Omit<ReplayReadbackResult, 'provenance'>;
|
|
53
|
+
|
|
54
|
+
export async function readReplayResource(
|
|
55
|
+
device: RhiDevice,
|
|
56
|
+
table: ResourceTable,
|
|
57
|
+
resourceId: string,
|
|
58
|
+
subresource: ReplayReadbackRequest | undefined,
|
|
59
|
+
createShaderModule: CreateShaderModuleFn,
|
|
60
|
+
): Promise<Result<ReplayReadbackResult, RhiDebugError>> {
|
|
61
|
+
const entry = table.get(resourceId);
|
|
62
|
+
if (entry === undefined)
|
|
63
|
+
return readbackFailure(`resource ${resourceId} is not present in the current generation`);
|
|
64
|
+
let result: Result<ReplayReadbackPayload, RhiDebugError>;
|
|
65
|
+
if (entry.resource.kind === 'texture-view') {
|
|
66
|
+
const sourceId = stringField(entry.descriptor, 'sourceHandleId');
|
|
67
|
+
const source = sourceId === undefined ? undefined : table.get(sourceId);
|
|
68
|
+
if (source === undefined || source.resource.kind !== 'texture') {
|
|
69
|
+
return readbackFailure(`texture view ${resourceId} has no readable source texture`);
|
|
70
|
+
}
|
|
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');
|
|
126
|
+
}
|
|
127
|
+
return ok({
|
|
128
|
+
...requested,
|
|
129
|
+
mipLevel: baseMipLevel + localMipLevel,
|
|
130
|
+
arrayLayer: baseArrayLayer + localArrayLayer,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function readBuffer(
|
|
135
|
+
device: RhiDevice,
|
|
136
|
+
entry: ResourceTableEntry,
|
|
137
|
+
resourceId: string,
|
|
138
|
+
subresource: ReplayReadbackRequest | undefined,
|
|
139
|
+
): Promise<Result<ReplayReadbackPayload, RhiDebugError>> {
|
|
140
|
+
const size = numberField(recordField(entry.descriptor, 'desc'), 'size');
|
|
141
|
+
if (size === undefined || !Number.isSafeInteger(size) || size < 0)
|
|
142
|
+
return readbackFailure(`buffer ${resourceId} has no valid recorded size`);
|
|
143
|
+
const request = isBufferRange(subresource) ? subresource : undefined;
|
|
144
|
+
const offset = request?.offset ?? 0;
|
|
145
|
+
const requestedSize = request?.size ?? size - offset;
|
|
146
|
+
if (!validRange(offset, requestedSize, size))
|
|
147
|
+
return readbackFailure(
|
|
148
|
+
`buffer range ${offset}:${requestedSize} escapes ${resourceId} (${size} bytes)`,
|
|
149
|
+
);
|
|
150
|
+
const bytes = await copyBufferBytes(
|
|
151
|
+
device,
|
|
152
|
+
entry.resource.value as Buffer,
|
|
153
|
+
offset,
|
|
154
|
+
requestedSize,
|
|
155
|
+
);
|
|
156
|
+
if (!bytes.ok) return bytes;
|
|
157
|
+
return ok({ resourceId, kind: 'buffer', bytes: bytes.value });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function readTexture(
|
|
161
|
+
device: RhiDevice,
|
|
162
|
+
entry: ResourceTableEntry,
|
|
163
|
+
resourceId: string,
|
|
164
|
+
subresource: ReplayReadbackRequest | undefined,
|
|
165
|
+
createShaderModule: CreateShaderModuleFn,
|
|
166
|
+
): Promise<Result<ReplayReadbackPayload, RhiDebugError>> {
|
|
167
|
+
const descriptor = recordField(entry.descriptor, 'desc');
|
|
168
|
+
const format = stringField(descriptor, 'format');
|
|
169
|
+
const dimension = stringField(descriptor, 'dimension') ?? '2d';
|
|
170
|
+
if (format === undefined) return readbackFailure(`texture ${resourceId} has no recorded format`);
|
|
171
|
+
const plan = getTextureReadbackPlan({ format, dimension });
|
|
172
|
+
if (!plan.supported) {
|
|
173
|
+
return err(
|
|
174
|
+
createRhiDebugError('readback-unsupported', {
|
|
175
|
+
stage: 'readback',
|
|
176
|
+
resourceId,
|
|
177
|
+
format,
|
|
178
|
+
reason: plan.reason,
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const size = textureSize(recordField(descriptor, 'size'));
|
|
183
|
+
const mipLevel = textureSubresourceField(subresource, 'mipLevel') ?? 0;
|
|
184
|
+
const arrayLayer = textureSubresourceField(subresource, 'arrayLayer') ?? 0;
|
|
185
|
+
const mipCount = numberField(descriptor, 'mipLevelCount') ?? 1;
|
|
186
|
+
if (
|
|
187
|
+
!validIndex(mipLevel, mipCount) ||
|
|
188
|
+
!Number.isInteger(arrayLayer) ||
|
|
189
|
+
arrayLayer < 0 ||
|
|
190
|
+
arrayLayer >= size.depthOrArrayLayers
|
|
191
|
+
) {
|
|
192
|
+
return readbackFailure(`texture subresource for ${resourceId} is outside the recorded extent`);
|
|
193
|
+
}
|
|
194
|
+
const width = Math.max(1, Math.floor(size.width / 2 ** mipLevel));
|
|
195
|
+
const height = Math.max(1, Math.floor(size.height / 2 ** mipLevel));
|
|
196
|
+
const requestedAspect = textureAspect(subresource);
|
|
197
|
+
if (isDepthStencilTextureFormat(format)) {
|
|
198
|
+
if (requestedAspect === 'all') {
|
|
199
|
+
return readbackUnsupported(
|
|
200
|
+
resourceId,
|
|
201
|
+
format,
|
|
202
|
+
`${format} requires an explicit depth-only or stencil-only aspect`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (requestedAspect === 'stencil-only') {
|
|
206
|
+
const bytes = await copyTextureBytes(
|
|
207
|
+
device,
|
|
208
|
+
entry.resource.value as Texture,
|
|
209
|
+
align256(width) * height,
|
|
210
|
+
{
|
|
211
|
+
mipLevel,
|
|
212
|
+
arrayLayer,
|
|
213
|
+
aspect: 'stencil-only',
|
|
214
|
+
width,
|
|
215
|
+
height,
|
|
216
|
+
bytesPerRow: align256(width),
|
|
217
|
+
rowBytes: width,
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
if (!bytes.ok) return bytes;
|
|
221
|
+
return ok({ resourceId, kind: 'texture', format, width, height, bytes: bytes.value });
|
|
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
|
+
}
|
|
241
|
+
return blitDepth24PlusTexture(
|
|
242
|
+
device,
|
|
243
|
+
entry.resource.value as Texture,
|
|
244
|
+
resourceId,
|
|
245
|
+
format,
|
|
246
|
+
width,
|
|
247
|
+
height,
|
|
248
|
+
mipLevel,
|
|
249
|
+
arrayLayer,
|
|
250
|
+
createShaderModule,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
const texelBytes = textureBytesPerTexel(format);
|
|
254
|
+
if (texelBytes === undefined)
|
|
255
|
+
return readbackFailure(`texture format ${format} has no byte layout`);
|
|
256
|
+
const rowBytes = width * texelBytes;
|
|
257
|
+
const bytesPerRow = align256(rowBytes);
|
|
258
|
+
const bytes = await copyTextureBytes(
|
|
259
|
+
device,
|
|
260
|
+
entry.resource.value as Texture,
|
|
261
|
+
bytesPerRow * height,
|
|
262
|
+
{
|
|
263
|
+
mipLevel,
|
|
264
|
+
arrayLayer,
|
|
265
|
+
aspect: directTextureAspect(format, requestedAspect),
|
|
266
|
+
width,
|
|
267
|
+
height,
|
|
268
|
+
bytesPerRow,
|
|
269
|
+
rowBytes,
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
if (!bytes.ok) return bytes;
|
|
273
|
+
return ok({ resourceId, kind: 'texture', format, width, height, bytes: bytes.value });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function readbackUnsupported(
|
|
277
|
+
resourceId: string,
|
|
278
|
+
format: string,
|
|
279
|
+
reason: string,
|
|
280
|
+
): Result<never, RhiDebugError> {
|
|
281
|
+
return err(
|
|
282
|
+
createRhiDebugError('readback-unsupported', {
|
|
283
|
+
stage: 'readback',
|
|
284
|
+
resourceId,
|
|
285
|
+
format,
|
|
286
|
+
reason,
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function directTextureAspect(
|
|
292
|
+
format: string,
|
|
293
|
+
aspect: 'all' | 'depth-only' | 'stencil-only',
|
|
294
|
+
): 'all' | 'depth-only' | 'stencil-only' {
|
|
295
|
+
if (isDepthTextureFormat(format) && !isDepthStencilTextureFormat(format)) {
|
|
296
|
+
return 'depth-only';
|
|
297
|
+
}
|
|
298
|
+
if (isDepthStencilTextureFormat(format) && aspect === 'all') return 'depth-only';
|
|
299
|
+
return aspect;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const DEPTH_BLIT_SHADER = `
|
|
303
|
+
@group(0) @binding(0) var sourceDepth: texture_depth_2d;
|
|
304
|
+
|
|
305
|
+
struct VertexOutput {
|
|
306
|
+
@builtin(position) position: vec4<f32>,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
@vertex
|
|
310
|
+
fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
311
|
+
var positions = array<vec2<f32>, 3>(
|
|
312
|
+
vec2<f32>(-1.0, -1.0),
|
|
313
|
+
vec2<f32>( 3.0, -1.0),
|
|
314
|
+
vec2<f32>(-1.0, 3.0),
|
|
315
|
+
);
|
|
316
|
+
var output: VertexOutput;
|
|
317
|
+
output.position = vec4<f32>(positions[vertexIndex], 0.0, 1.0);
|
|
318
|
+
return output;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
@fragment
|
|
322
|
+
fn fs_main(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> {
|
|
323
|
+
let pixel = vec2<i32>(i32(position.x), i32(position.y));
|
|
324
|
+
return vec4<f32>(textureLoad(sourceDepth, pixel, 0), 0.0, 0.0, 1.0);
|
|
325
|
+
}
|
|
326
|
+
`;
|
|
327
|
+
|
|
328
|
+
async function blitDepth24PlusTexture(
|
|
329
|
+
device: RhiDevice,
|
|
330
|
+
source: Texture,
|
|
331
|
+
resourceId: string,
|
|
332
|
+
format: string,
|
|
333
|
+
width: number,
|
|
334
|
+
height: number,
|
|
335
|
+
mipLevel: number,
|
|
336
|
+
arrayLayer: number,
|
|
337
|
+
createShaderModule: CreateShaderModuleFn,
|
|
338
|
+
): Promise<Result<ReplayReadbackPayload, RhiDebugError>> {
|
|
339
|
+
let output: Texture | undefined;
|
|
340
|
+
let staging: Buffer | undefined;
|
|
341
|
+
let mapped: MappedBuffer | undefined;
|
|
342
|
+
try {
|
|
343
|
+
const sourceView = device.createTextureView(source, {
|
|
344
|
+
dimension: '2d',
|
|
345
|
+
baseMipLevel: mipLevel,
|
|
346
|
+
mipLevelCount: 1,
|
|
347
|
+
baseArrayLayer: arrayLayer,
|
|
348
|
+
arrayLayerCount: 1,
|
|
349
|
+
aspect: 'depth-only',
|
|
350
|
+
});
|
|
351
|
+
if (!sourceView.ok)
|
|
352
|
+
return readbackFailure(
|
|
353
|
+
`depth readback source view creation failed: ${sourceView.error.code}`,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
const layout = device.createBindGroupLayout({
|
|
357
|
+
entries: [
|
|
358
|
+
{
|
|
359
|
+
binding: 0,
|
|
360
|
+
visibility: 0x2,
|
|
361
|
+
texture: { sampleType: 'depth', viewDimension: '2d', multisampled: false },
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
});
|
|
365
|
+
if (!layout.ok)
|
|
366
|
+
return readbackFailure(`depth readback bind group layout failed: ${layout.error.code}`);
|
|
367
|
+
const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout.value] });
|
|
368
|
+
if (!pipelineLayout.ok)
|
|
369
|
+
return readbackFailure(`depth readback pipeline layout failed: ${pipelineLayout.error.code}`);
|
|
370
|
+
const shader = await createShaderModule(device, {
|
|
371
|
+
code: DEPTH_BLIT_SHADER,
|
|
372
|
+
label: 'rhi-debug-depth-readback',
|
|
373
|
+
});
|
|
374
|
+
if (!shader.ok) return readbackFailure(`depth readback shader failed: ${shader.error.code}`);
|
|
375
|
+
const pipeline = device.createRenderPipeline({
|
|
376
|
+
layout: pipelineLayout.value,
|
|
377
|
+
vertex: { module: shader.value, entryPoint: 'vs_main', buffers: [] },
|
|
378
|
+
fragment: {
|
|
379
|
+
module: shader.value,
|
|
380
|
+
entryPoint: 'fs_main',
|
|
381
|
+
targets: [{ format: 'rgba32float' }],
|
|
382
|
+
},
|
|
383
|
+
primitive: { topology: 'triangle-list' },
|
|
384
|
+
} as never);
|
|
385
|
+
if (!pipeline.ok)
|
|
386
|
+
return readbackFailure(`depth readback pipeline failed: ${pipeline.error.code}`);
|
|
387
|
+
const bindGroup = device.createBindGroup({
|
|
388
|
+
layout: layout.value,
|
|
389
|
+
entries: [{ binding: 0, resource: { kind: 'textureView', value: sourceView.value } }],
|
|
390
|
+
});
|
|
391
|
+
if (!bindGroup.ok)
|
|
392
|
+
return readbackFailure(`depth readback bind group failed: ${bindGroup.error.code}`);
|
|
393
|
+
|
|
394
|
+
const outputResult = device.createTexture({
|
|
395
|
+
size: { width, height, depthOrArrayLayers: 1 },
|
|
396
|
+
format: 'rgba32float',
|
|
397
|
+
dimension: '2d',
|
|
398
|
+
mipLevelCount: 1,
|
|
399
|
+
sampleCount: 1,
|
|
400
|
+
usage: 0x11,
|
|
401
|
+
});
|
|
402
|
+
if (!outputResult.ok)
|
|
403
|
+
return readbackFailure(`depth readback output texture failed: ${outputResult.error.code}`);
|
|
404
|
+
output = outputResult.value;
|
|
405
|
+
const outputView = device.createTextureView(output, { dimension: '2d' });
|
|
406
|
+
if (!outputView.ok)
|
|
407
|
+
return readbackFailure(`depth readback output view failed: ${outputView.error.code}`);
|
|
408
|
+
|
|
409
|
+
const encoderResult = device.createCommandEncoder({});
|
|
410
|
+
if (!encoderResult.ok)
|
|
411
|
+
return readbackFailure(`depth readback encoder failed: ${encoderResult.error.code}`);
|
|
412
|
+
const pass = encoderResult.value.beginRenderPass({
|
|
413
|
+
colorAttachments: [
|
|
414
|
+
{
|
|
415
|
+
view: outputView.value,
|
|
416
|
+
loadOp: 'clear',
|
|
417
|
+
storeOp: 'store',
|
|
418
|
+
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
419
|
+
},
|
|
420
|
+
],
|
|
421
|
+
} as never);
|
|
422
|
+
pass.setPipeline(pipeline.value);
|
|
423
|
+
pass.setBindGroup(0, bindGroup.value);
|
|
424
|
+
pass.draw(3, 1, 0, 0);
|
|
425
|
+
pass.end();
|
|
426
|
+
|
|
427
|
+
const bytesPerRow = align256(width * 16);
|
|
428
|
+
const created = device.createBuffer({ size: bytesPerRow * height, usage: COPY_DST_MAP_READ });
|
|
429
|
+
if (!created.ok)
|
|
430
|
+
return readbackFailure(`depth readback staging buffer failed: ${created.error.code}`);
|
|
431
|
+
staging = created.value;
|
|
432
|
+
encoderResult.value.copyTextureToBuffer(
|
|
433
|
+
{ texture: output, aspect: 'all' } as never,
|
|
434
|
+
{ buffer: staging, offset: 0, bytesPerRow, rowsPerImage: height } as never,
|
|
435
|
+
{ width, height, depthOrArrayLayers: 1 },
|
|
436
|
+
);
|
|
437
|
+
const finished = encoderResult.value.finish();
|
|
438
|
+
if (!finished.ok)
|
|
439
|
+
return readbackFailure(`depth readback finish failed: ${finished.error.code}`);
|
|
440
|
+
const submitted = device.queue.submit([finished.value]);
|
|
441
|
+
if (!submitted.ok)
|
|
442
|
+
return readbackFailure(`depth readback submit failed: ${submitted.error.code}`);
|
|
443
|
+
await device.queue.onSubmittedWorkDone();
|
|
444
|
+
const mappedResult = await staging.mapAsync(GPU_MAP_MODE_READ);
|
|
445
|
+
if (!mappedResult.ok)
|
|
446
|
+
return readbackFailure(`depth readback map failed: ${mappedResult.error.code}`);
|
|
447
|
+
mapped = mappedResult.value;
|
|
448
|
+
const range = mapped.getMappedRange(0, bytesPerRow * height);
|
|
449
|
+
if (!range.ok)
|
|
450
|
+
return readbackFailure(`depth readback mapped range failed: ${range.error.code}`);
|
|
451
|
+
const padded = new Uint8Array(range.value);
|
|
452
|
+
const bytes = new Uint8Array(width * height * 4);
|
|
453
|
+
for (let row = 0; row < height; row++) {
|
|
454
|
+
for (let column = 0; column < width; column++) {
|
|
455
|
+
const sourceOffset = row * bytesPerRow + column * 16;
|
|
456
|
+
const targetOffset = (row * width + column) * 4;
|
|
457
|
+
bytes.set(padded.subarray(sourceOffset, sourceOffset + 4), targetOffset);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return ok({ resourceId, kind: 'texture', format, width, height, bytes });
|
|
461
|
+
} catch (cause) {
|
|
462
|
+
return readbackFailure(`depth readback failed: ${messageOf(cause)}`);
|
|
463
|
+
} finally {
|
|
464
|
+
if (mapped !== undefined) mapped.unmap();
|
|
465
|
+
if (staging !== undefined) device.destroyBuffer(staging);
|
|
466
|
+
if (output !== undefined) device.destroyTexture(output);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function copyBufferBytes(
|
|
471
|
+
device: RhiDevice,
|
|
472
|
+
source: Buffer,
|
|
473
|
+
sourceOffset: number,
|
|
474
|
+
size: number,
|
|
475
|
+
): Promise<Result<Uint8Array, RhiDebugError>> {
|
|
476
|
+
let staging: Buffer | undefined;
|
|
477
|
+
let mapped: MappedBuffer | undefined;
|
|
478
|
+
try {
|
|
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 });
|
|
483
|
+
if (!created.ok)
|
|
484
|
+
return readbackFailure(`staging buffer creation failed: ${created.error.code}`);
|
|
485
|
+
staging = created.value;
|
|
486
|
+
const encoderResult = device.createCommandEncoder({});
|
|
487
|
+
if (!encoderResult.ok)
|
|
488
|
+
return readbackFailure(`readback encoder creation failed: ${encoderResult.error.code}`);
|
|
489
|
+
const encoder = encoderResult.value;
|
|
490
|
+
encoder.copyBufferToBuffer(source, alignedSourceOffset, staging, 0, copySize);
|
|
491
|
+
const finished = encoder.finish();
|
|
492
|
+
if (!finished.ok)
|
|
493
|
+
return readbackFailure(`readback encoder finish failed: ${finished.error.code}`);
|
|
494
|
+
const submitted = device.queue.submit([finished.value]);
|
|
495
|
+
if (!submitted.ok) return readbackFailure(`readback submit failed: ${submitted.error.code}`);
|
|
496
|
+
await device.queue.onSubmittedWorkDone();
|
|
497
|
+
const mappedResult = await staging.mapAsync(GPU_MAP_MODE_READ);
|
|
498
|
+
if (!mappedResult.ok) return readbackFailure(`readback map failed: ${mappedResult.error.code}`);
|
|
499
|
+
mapped = mappedResult.value;
|
|
500
|
+
const range = mapped.getMappedRange(0, copySize);
|
|
501
|
+
if (!range.ok) return readbackFailure(`readback mapped range failed: ${range.error.code}`);
|
|
502
|
+
return ok(new Uint8Array(range.value).slice(leadingBytes, leadingBytes + size));
|
|
503
|
+
} catch (cause) {
|
|
504
|
+
return readbackFailure(`buffer readback failed: ${messageOf(cause)}`);
|
|
505
|
+
} finally {
|
|
506
|
+
if (mapped !== undefined) mapped.unmap();
|
|
507
|
+
if (staging !== undefined) device.destroyBuffer(staging);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function copyTextureBytes(
|
|
512
|
+
device: RhiDevice,
|
|
513
|
+
source: Texture,
|
|
514
|
+
bufferSize: number,
|
|
515
|
+
copy: {
|
|
516
|
+
readonly mipLevel: number;
|
|
517
|
+
readonly arrayLayer: number;
|
|
518
|
+
readonly aspect: 'all' | 'depth-only' | 'stencil-only';
|
|
519
|
+
readonly width: number;
|
|
520
|
+
readonly height: number;
|
|
521
|
+
readonly bytesPerRow: number;
|
|
522
|
+
readonly rowBytes: number;
|
|
523
|
+
},
|
|
524
|
+
): Promise<Result<Uint8Array, RhiDebugError>> {
|
|
525
|
+
let staging: Buffer | undefined;
|
|
526
|
+
let mapped: MappedBuffer | undefined;
|
|
527
|
+
try {
|
|
528
|
+
const created = device.createBuffer({ size: bufferSize, usage: COPY_DST_MAP_READ });
|
|
529
|
+
if (!created.ok)
|
|
530
|
+
return readbackFailure(`staging buffer creation failed: ${created.error.code}`);
|
|
531
|
+
staging = created.value;
|
|
532
|
+
const encoderResult = device.createCommandEncoder({});
|
|
533
|
+
if (!encoderResult.ok)
|
|
534
|
+
return readbackFailure(`readback encoder creation failed: ${encoderResult.error.code}`);
|
|
535
|
+
const encoder: RhiCommandEncoder = encoderResult.value;
|
|
536
|
+
encoder.copyTextureToBuffer(
|
|
537
|
+
{
|
|
538
|
+
texture: source,
|
|
539
|
+
mipLevel: copy.mipLevel,
|
|
540
|
+
origin: { x: 0, y: 0, z: copy.arrayLayer },
|
|
541
|
+
aspect: copy.aspect,
|
|
542
|
+
} as never,
|
|
543
|
+
{
|
|
544
|
+
buffer: staging,
|
|
545
|
+
offset: 0,
|
|
546
|
+
bytesPerRow: copy.bytesPerRow,
|
|
547
|
+
rowsPerImage: copy.height,
|
|
548
|
+
} as never,
|
|
549
|
+
{ width: copy.width, height: copy.height, depthOrArrayLayers: 1 },
|
|
550
|
+
);
|
|
551
|
+
const finished = encoder.finish();
|
|
552
|
+
if (!finished.ok)
|
|
553
|
+
return readbackFailure(`readback encoder finish failed: ${finished.error.code}`);
|
|
554
|
+
const submitted = device.queue.submit([finished.value]);
|
|
555
|
+
if (!submitted.ok) return readbackFailure(`readback submit failed: ${submitted.error.code}`);
|
|
556
|
+
await device.queue.onSubmittedWorkDone();
|
|
557
|
+
const mappedResult = await staging.mapAsync(GPU_MAP_MODE_READ);
|
|
558
|
+
if (!mappedResult.ok) return readbackFailure(`readback map failed: ${mappedResult.error.code}`);
|
|
559
|
+
mapped = mappedResult.value;
|
|
560
|
+
const range = mapped.getMappedRange(0, bufferSize);
|
|
561
|
+
if (!range.ok) return readbackFailure(`readback mapped range failed: ${range.error.code}`);
|
|
562
|
+
const padded = new Uint8Array(range.value);
|
|
563
|
+
const bytes = new Uint8Array(copy.rowBytes * copy.height);
|
|
564
|
+
for (let row = 0; row < copy.height; row++) {
|
|
565
|
+
bytes.set(
|
|
566
|
+
padded.subarray(row * copy.bytesPerRow, row * copy.bytesPerRow + copy.rowBytes),
|
|
567
|
+
row * copy.rowBytes,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
return ok(bytes);
|
|
571
|
+
} catch (cause) {
|
|
572
|
+
return readbackFailure(`texture readback failed: ${messageOf(cause)}`);
|
|
573
|
+
} finally {
|
|
574
|
+
if (mapped !== undefined) mapped.unmap();
|
|
575
|
+
if (staging !== undefined) device.destroyBuffer(staging);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function readbackFailure(cause: string): Result<never, RhiDebugError> {
|
|
580
|
+
return err(createRhiDebugError('readback-failed', { stage: 'readback', cause }));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function recordField(
|
|
584
|
+
value: Record<string, unknown> | undefined,
|
|
585
|
+
key: string,
|
|
586
|
+
): Record<string, unknown> | undefined {
|
|
587
|
+
const field = value?.[key];
|
|
588
|
+
return field !== null && typeof field === 'object'
|
|
589
|
+
? (field as Record<string, unknown>)
|
|
590
|
+
: undefined;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function stringField(value: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
594
|
+
const field = value?.[key];
|
|
595
|
+
return typeof field === 'string' ? field : undefined;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function numberField(value: Record<string, unknown> | undefined, key: string): number | undefined {
|
|
599
|
+
const field = value?.[key];
|
|
600
|
+
return typeof field === 'number' ? field : undefined;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function textureSize(value: unknown): {
|
|
604
|
+
readonly width: number;
|
|
605
|
+
readonly height: number;
|
|
606
|
+
readonly depthOrArrayLayers: number;
|
|
607
|
+
} {
|
|
608
|
+
if (Array.isArray(value)) {
|
|
609
|
+
return {
|
|
610
|
+
width: integerOr(value[0], 1),
|
|
611
|
+
height: integerOr(value[1], integerOr(value[0], 1)),
|
|
612
|
+
depthOrArrayLayers: integerOr(value[2], 1),
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
const object =
|
|
616
|
+
value !== null && typeof value === 'object' ? (value as Record<string, unknown>) : undefined;
|
|
617
|
+
return {
|
|
618
|
+
width: integerOr(object?.width, 1),
|
|
619
|
+
height: integerOr(object?.height, integerOr(object?.width, 1)),
|
|
620
|
+
depthOrArrayLayers: integerOr(object?.depthOrArrayLayers, 1),
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function integerOr(value: unknown, fallback: number): number {
|
|
625
|
+
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : fallback;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function textureSubresourceField(
|
|
629
|
+
value: ReplayReadbackRequest | undefined,
|
|
630
|
+
key: 'mipLevel' | 'arrayLayer',
|
|
631
|
+
): number | undefined {
|
|
632
|
+
if (value === undefined || isBufferRange(value)) return undefined;
|
|
633
|
+
return value[key];
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function textureAspect(
|
|
637
|
+
value: ReplayReadbackRequest | undefined,
|
|
638
|
+
): 'all' | 'depth-only' | 'stencil-only' {
|
|
639
|
+
if (value === undefined || isBufferRange(value)) return 'all';
|
|
640
|
+
return value.aspect ?? 'all';
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function isBufferRange(value: ReplayReadbackRequest | undefined): value is BufferReadbackRange {
|
|
644
|
+
return value !== undefined && 'offset' in value;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function validRange(offset: number, size: number, total: number): boolean {
|
|
648
|
+
return (
|
|
649
|
+
Number.isSafeInteger(offset) &&
|
|
650
|
+
Number.isSafeInteger(size) &&
|
|
651
|
+
offset >= 0 &&
|
|
652
|
+
size >= 0 &&
|
|
653
|
+
offset + size <= total
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function validIndex(value: number, count: number): boolean {
|
|
658
|
+
return Number.isInteger(value) && value >= 0 && value < count;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function align256(value: number): number {
|
|
662
|
+
return Math.max(256, Math.ceil(value / 256) * 256);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function align4(value: number): number {
|
|
666
|
+
return Math.ceil(value / 4) * 4;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function messageOf(cause: unknown): string {
|
|
670
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
671
|
+
}
|