replayTrace function
Issues every call of trace against device and collects what it reads.
Stages are found by name: in the libraries the trace itself loaded, newest first, and then in the device's own. A trace recorded against a shader the replaying device does not have fails with that stage's name.
Implementation
Future<TraceReplay> replayTrace(Trace trace, GraphicsDevice device) async {
final libraries = <LoadedShaderLibrary>[];
final geometry = <int, GeometryBuffer>{};
final textures = <int, TextureHandle>{};
final pipelines = <int, PipelineHandle>{};
final passes = <int, CommandEncoder>{};
final storage = <int, StorageBuffer>{};
final computePipelines = <int, ComputePipelineHandle>{};
final computePasses = <int, ComputeEncoder>{};
final pixels = <Future<ByteData?>>[];
final readbacks = <Future<ByteData>>[];
final buffers = <Future<ByteData>>[];
ShaderHandle stage(String name) {
for (final library in libraries.reversed) {
final found = library[name];
if (found != null) return found;
}
return device.shaders[name] ??
(throw StateError('the trace names a stage "$name" this device lacks'));
}
GeometryBuffer range(TraceGeometryRange r) =>
geometry[r.buffer]!.slice(offset: r.offset, length: r.length);
TextureHandle texture(int id) =>
textures[id] ??
(throw StateError('the trace names texture $id before creating it'));
for (final event in trace.events) {
switch (event) {
case TraceBeginFrame():
device.beginFrame();
case TraceUploadGeometry(:final id, :final usage, :final bytes):
geometry[id] = device.uploadGeometry(bytes, usage);
case TraceOverwriteGeometry(:final target, :final offset, :final bytes):
device.overwriteGeometry(range(target), offset, bytes);
case TraceReleaseGeometry(:final buffer):
device.releaseGeometry(geometry.remove(buffer)!);
case TraceCreateTexture(:final id, :final spec):
textures[id] = device.createTexture(spec);
case TraceCreateTextureFromPixels():
final made = device.createTextureFromPixels(
width: event.width,
height: event.height,
format: event.format,
pixels: event.pixels,
mipLevels: event.mipLevels,
);
if (made != null) textures[event.id] = made;
case TraceCreateCubeTextureFromPixels():
final made = device.createCubeTextureFromPixels(
size: event.size,
format: event.format,
faces: event.faces,
mipLevels: event.mipLevels,
);
if (made != null) textures[event.id] = made;
case TraceCreateCubeRenderTarget():
final made = device.createCubeRenderTarget(
size: event.size,
format: event.format,
mipLevels: event.mipLevels,
);
if (made != null) textures[event.id] = made;
case TraceOverwriteTexture():
await device.overwriteTexture(
texture(event.texture),
event.rgba,
region: event.region,
mipLevel: event.mipLevel,
);
case TraceReleaseTexture(:final texture):
device.releaseTexture(textures.remove(texture)!);
case TraceLoadShaders(:final bytes):
libraries.add(await device.loadShaders(bytes));
case TraceCreatePipeline():
pipelines[event.id] = device.createPipeline(
stage(event.vertex),
stage(event.fragment),
layout: event.layout,
);
case TraceBeginRenderPass():
passes[event.pass] = device.beginRenderPass(
RenderPassDescriptor(
label: event.label,
colors: <ColorTarget>[
for (final c in event.colors)
ColorTarget(
texture: texture(c.texture),
resolveTexture: c.resolveTexture == null
? null
: texture(c.resolveTexture!),
loadAction: c.loadAction,
storeAction: c.storeAction,
clearValue: c.clearValue,
face: c.face,
mipLevel: c.mipLevel,
),
],
depth: switch (event.depth) {
null => null,
final d => DepthTarget(
texture: texture(d.texture),
clearValue: d.clearValue,
loadAction: d.loadAction,
storeAction: d.storeAction,
stencilLoadAction: d.stencilLoadAction,
stencilStoreAction: d.stencilStoreAction,
stencilClearValue: d.stencilClearValue,
),
},
),
);
case TraceReadPixels(:final texture):
pixels.add(device.readPixels(textures[texture]!));
case TraceReadback(:final texture, :final region):
readbacks.add(device.readback(textures[texture]!, region: region));
case TracePassEvent(:final pass):
_replayPassEvent(
event,
passes[pass] ??
(throw StateError(
'the trace names pass $pass before opening it',
)),
pipelines: pipelines,
range: range,
texture: texture,
stage: stage,
);
if (event is TraceSubmit) passes.remove(pass);
case TraceCreateStorageBuffer():
storage[event.id] = device.createStorageBuffer(
event.bytes,
hostReadable: event.hostReadable,
);
case TraceReleaseStorageBuffer(:final buffer):
device.releaseStorageBuffer(storage.remove(buffer)!);
case TraceCreateComputePipeline(:final id, :final shader):
computePipelines[id] = device.createComputePipeline(stage(shader));
case TraceBeginComputePass(:final pass, :final label):
computePasses[pass] = device.beginComputePass(label: label);
case TraceComputeBindPipeline(:final pass, :final pipeline):
computePasses[pass]!.bindPipeline(computePipelines[pipeline]!);
case TraceComputeBindStorageBuffer():
computePasses[event.pass]!.bindStorageBuffer(
stage(event.shader),
event.name,
storage[event.buffer]!,
);
case TraceComputeBindUniformBlock():
computePasses[event.pass]!.bindUniformBlock(
stage(event.shader),
event.block,
event.members,
);
case TraceDispatch(:final pass, :final x, :final y, :final z):
computePasses[pass]!.dispatch(x, y, z);
case TraceComputeSubmit(:final pass):
computePasses.remove(pass)!.submit();
case TraceReadBuffer(:final buffer):
buffers.add(device.readBuffer(storage[buffer]!));
}
}
return TraceReplay._(
await Future.wait(pixels),
await Future.wait(readbacks),
await Future.wait(buffers),
);
}