replayTrace function

Future<TraceReplay> replayTrace(
  1. Trace trace,
  2. GraphicsDevice device
)

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),
  );
}