@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.
- package/README.md +39 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/readback-matrix-fixture.d.ts +32 -0
- package/dist/__tests__/readback-matrix-fixture.d.ts.map +1 -0
- package/dist/frame-model.d.ts +84 -52
- package/dist/frame-model.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +395 -40
- package/dist/index.mjs.map +1 -1
- package/dist/replay/device-request.d.ts +8 -0
- package/dist/replay/device-request.d.ts.map +1 -0
- package/dist/replay/readback.d.ts +7 -0
- package/dist/replay/readback.d.ts.map +1 -1
- package/dist/replay/session.d.ts +17 -0
- package/dist/replay/session.d.ts.map +1 -1
- package/dist/texel-decode.d.ts +5 -4
- package/dist/texel-decode.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/__tests__/consumer-inventory.unit.test.ts +49 -1
- package/src/__tests__/e2e.browser.test.ts +6 -1
- package/src/__tests__/frame-model-parity.test-d.ts +13 -1
- package/src/__tests__/frame-model-parity.unit.test.ts +262 -15
- package/src/__tests__/guard-gates.test.ts +176 -1
- package/src/__tests__/public-surface.integration.test.ts +8 -0
- package/src/__tests__/readback-format-matrix.dawn.test.ts +226 -3
- package/src/__tests__/readback-format-matrix.unit.test.ts +25 -1
- package/src/__tests__/readback-matrix-fixture.ts +20 -0
- package/src/__tests__/replay-fail-fast.unit.test.ts +12 -0
- package/src/__tests__/replay-session.dawn.test.ts +3 -0
- package/src/__tests__/replay-session.test-d.ts +7 -1
- package/src/__tests__/replay-session.unit.test.ts +16 -0
- package/src/__tests__/rhi-debug-fresh-replay.dawn.test.ts +2 -0
- package/src/__tests__/tape-index.unit.test.ts +3 -0
- package/src/__tests__/tree-shake.unit.test.ts +24 -0
- package/src/frame-model.ts +381 -83
- package/src/index.ts +17 -10
- package/src/replay/device-request.ts +38 -0
- package/src/replay/readback.ts +106 -18
- package/src/replay/session.ts +47 -2
- package/src/texel-decode.ts +37 -3
package/src/replay/readback.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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<
|
|
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<
|
|
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
|
|
132
|
-
if (
|
|
197
|
+
if (isDepthStencilTextureFormat(format)) {
|
|
198
|
+
if (requestedAspect === 'all') {
|
|
133
199
|
return readbackUnsupported(
|
|
134
200
|
resourceId,
|
|
135
201
|
format,
|
|
136
|
-
|
|
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<
|
|
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
|
|
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,
|
|
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,
|
|
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
|
}
|
package/src/replay/session.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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) {
|
package/src/texel-decode.ts
CHANGED
|
@@ -146,21 +146,26 @@ function readTexel(
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
/**
|
|
149
|
-
* Decode tight raw GPU bytes of
|
|
150
|
-
* canvas can paint via putImageData. Returns null when the format has
|
|
151
|
-
* {@link formatInfo} entry
|
|
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).
|