spliceMaterialWgsl function

PackedStage spliceMaterialWgsl(
  1. MaterialProgram program,
  2. PackedStage host, {
  3. required String from,
})

program, already specialised, spliced into host's main — see the library comment. from names the source in every refusal.

Throws a RuntimeShadersException for a material past the limits above, and for a host that is not shaped as the engine's Unlit stage is.

Implementation

PackedStage spliceMaterialWgsl(
  MaterialProgram program,
  PackedStage host, {
  required String from,
}) {
  Never refuse(String why) => throw RuntimeShadersException('$from: $why');

  if (program.light != null) {
    refuse(
      'a material with a light block is compiled for WebGPU by the build '
      '(assets_src/), not while the game runs: at run time WebGPU takes a '
      'material that returns the light its surface emits',
    );
  }
  if (program.inputsUsed.contains('instance')) {
    refuse(
      'a material reading "instance" is compiled for WebGPU by the build '
      '(assets_src/): the stage it runs in at run time has no instance '
      'varying',
    );
  }
  // Version 2, refused for the reason the light block is: each is a stage
  // of another shape than the one spliced into.
  if (program.kind != MaterialStageKind.surface) {
    refuse(
      'a ${program.kind} stage is not spliced into a material\'s host; '
      '${program.kind == MaterialStageKind.fullscreen ? 'build it with fullscreenMaterialWgsl' : 'no GPU backend runs a compute kernel written in the language'}',
    );
  }
  if (program.vertex != null) {
    refuse(
      'a material with a vertex block is compiled for WebGPU by the build '
      '(assets_src/): at run time WebGPU has no vertex stage to splice one '
      'into, and the engine\'s mesh stages stay what they are',
    );
  }
  if (program.readsSceneDepth) {
    refuse(
      'a material reading the scene behind it is compiled for WebGPU by the '
      'build (assets_src/): the stage it runs in at run time binds no scene '
      'depth',
    );
  }
  _refuseOwnSlots(program, refuse);

  final wgsl = host.wgsl;
  String found(RegExp pattern, String what) =>
      pattern.firstMatch(wgsl)?.group(1) ??
      refuse(
        'the WebGPU host stage has no $what; hand RuntimeShaders the '
        'engine\'s own (webGpuMaterialHost)',
      );

  final readSurface = found(
    RegExp(r'fn (ReadSurface\w*)\(\) -> Surface'),
    'ReadSurface',
  );
  final writeSurface = found(
    RegExp(
      r'fn (WriteSurface\w*)\(\w+: ptr<function, vec3<f32>>, '
      r'\w+: ptr<function, f32>\)',
    ),
    'two-argument WriteSurface',
  );
  final privates = <String>{
    for (final match in RegExp(
      r'^var<private> (\w+):',
      multiLine: true,
    ).allMatches(wgsl))
      match.group(1)!,
  };
  String varying(String glsl) {
    // Version 2's `viewDepth`, read into a local of the spliced `main`.
    if (glsl == 'f3d_view_depth') return glsl;
    final type = switch (glsl) {
      'v_texcoord' => 'vec2<f32>',
      'v_world_position' => 'vec3<f32>',
      _ => refuse('the input "$glsl" has no place in the WebGPU host stage'),
    };
    return found(
      RegExp('var<private> (${glsl}_\\d+): ${RegExp.escape(type)};'),
      'varying $glsl',
    );
  }

  // `main_1` is the GLSL `main` naga wrote; the entry point calls it.
  final start = wgsl.indexOf('fn main_1() {');
  if (start < 0) refuse('the WebGPU host stage has no main_1');
  var depth = 0;
  var end = -1;
  for (var i = wgsl.indexOf('{', start); i < wgsl.length; i++) {
    if (wgsl[i] == '{') depth++;
    if (wgsl[i] == '}' && --depth == 0) {
      end = i + 1;
      break;
    }
  }
  if (end < 0) refuse('the WebGPU host stage\'s main_1 is never closed');

  // What the GLSL's global initialisers became: naga writes them as the
  // first assignments of `main_1`, before the surface is read. Only those
  // to the module's own private variables, so nothing local is carried over.
  final prologue = <String>[
    for (final line
        in wgsl
            .substring(start, end)
            .split('\n')
            .skip(1)
            .takeWhile((l) => !l.contains(readSurface)))
      if (RegExp(r'^\s*(\w+) = [^;]*;$').firstMatch(line) case final m?
          when privates.contains(m.group(1)))
        line,
  ];

  // The texture slots, held to what the host binds.
  final bound = <String>{for (final s in host.prepared.samplers) s.name};
  for (final slot in program.textures) {
    if (!bound.contains(slot.bindingName) ||
        !wgsl.contains('var ${slot.bindingName}_tex:')) {
      refuse(
        'the texture "${slot.name}" reads ${slot.bindingName}, and the stage '
        'a material runs in on WebGPU at run time binds '
        '${bound.join(', ')}; a material sampling the others is compiled by '
        'the build (assets_src/)',
      );
    }
  }

  // The uniforms, as the block the engine binds `RenderMaterial.parameters` to.
  final uniforms = <MaterialParameter>[
    for (final parameter in program.parameters)
      if (parameter.uniform) parameter,
  ];
  final binding =
      <int>[
        for (final block in host.prepared.blocks) block.binding,
        for (final sampler in host.prepared.samplers) ...<int>[
          sampler.textureBinding,
          sampler.samplerBinding,
        ],
      ].fold(-1, (int a, int b) => a > b ? a : b) +
      1;
  final group = host.prepared.blocks.isEmpty
      ? 1
      : host.prepared.blocks.first.group;
  final params = uniforms.isEmpty
      ? null
      : _paramsBlock(uniforms, group, binding);

  final writer = _WgslWriter(
    surface: 'f3d_s',
    varying: varying,
    params: 'f3d_material_params',
  );
  final body = StringBuffer();
  for (final statement in program.body) {
    body.write(
      statement.accept(
        _WgslStatements(
          writer,
          returns: (value) =>
              '    let f3d_result: vec4<f32> = $value;\n'
              '    var f3d_rgb: vec3<f32> = f3d_result.xyz;\n'
              '    var f3d_alpha: f32 = f3d_result.w;\n'
              '    $writeSurface((&f3d_rgb), (&f3d_alpha));\n'
              '    return;\n',
          // Refused above: only a vertex block writes outputs.
          output: () =>
              refuse('a vertex block has no place in a fragment stage'),
          unknown: refuse,
        ),
      ),
    );
  }

  // Version 2: what `main` sets before the body — the blend that leaves
  // the colour as returned, and this fragment's depth along the view axis.
  final prelude = StringBuffer();
  if (program.state.blend == MaterialBlend.premultiplied) {
    final premultiply = found(
      RegExp(r'^var<private> (g_premultiply\w*): bool', multiLine: true),
      'g_premultiply',
    );
    prelude.writeln('    $premultiply = false;');
  }
  if (program.inputsUsed.contains('viewDepth')) {
    final viewDepth = found(
      RegExp(r'fn (ViewDepth\w*)\(\) -> f32'),
      'ViewDepth',
    );
    prelude.writeln('    let f3d_view_depth: f32 = $viewDepth();');
  }

  final declarations = StringBuffer();
  if (params != null) {
    declarations
      ..writeln('struct F3dMaterialParams {')
      ..writeAll(<String>[
        for (final p in uniforms) '    m_${p.name}: ${_type(p.type)},\n',
      ])
      ..writeln('}')
      ..writeln()
      ..writeln('@group($group) @binding($binding) ')
      ..writeln('var<uniform> f3d_material_params: F3dMaterialParams;')
      ..writeln();
  }
  final main = StringBuffer()
    ..writeln('// The material "${program.name}", spliced in at run time.')
    ..writeln('fn main_1() {')
    ..writeAll(<String>[for (final line in prologue) '$line\n'])
    ..writeln('    var f3d_s: Surface = $readSurface();')
    ..write(prelude)
    ..write(body)
    ..write('}');

  return (
    wgsl: '${wgsl.substring(0, start)}$declarations$main${wgsl.substring(end)}',
    prepared: PreparedStage(
      glsl: emitMaterialFragment(program),
      attributes: host.prepared.attributes,
      blocks: <PreparedBlock>[...host.prepared.blocks, ?params],
      samplers: host.prepared.samplers,
    ),
  );
}