compileMaterial function

ShaderBundle compileMaterial(
  1. String text, {
  2. required String source,
  3. required ShaderSet engine,
  4. required MaterialCompilers compilers,
  5. required Directory scratch,
  6. String? namespace,
})

One material text as a bundle with a fragment stage named after the material, compiled for every backend compilers can serve; a compute stage as a bundle of its kernel, lowered to the kernel language (lowerMaterialCompute), which every device with compute loads.

source is the path every failure names. scratch is where the generated .frag is written for impellerc, and where it stays when a compiler refuses it, so the line in that compiler's message is a line somebody can open.

namespace is the plugin's that ships the source, for a plugin's material: the bare name the file writes (material Hair) compiles as <namespace>__Hair (materialStageIn), every stage of it and the source the bundle carries renamed to match, so the plugin's lighting model (acme_hair.Hair) finds it. A name in another plugin's namespace is refused. Null compiles the name as written: the engine's own materials, and an application's.

Implementation

ShaderBundle compileMaterial(
  String text, {
  required String source,
  required ShaderSet engine,
  required MaterialCompilers compilers,
  required Directory scratch,
  String? namespace,
}) {
  final MaterialProgram program;
  // [text], in the plugin's namespace: what every stage is emitted from
  // and the bundle's material section carries.
  final String named;
  try {
    final parsed = parseMaterial(text);
    final stage = namespace == null
        ? parsed.name
        : materialStageIn(namespace, parsed.name);
    if (stage == null) {
      throw MaterialBuildException(
        'the material "${parsed.name}" is named in another plugin\'s '
        'namespace; a plugin of "$namespace" writes the bare name, which '
        'compiles as "${namespace}__<Name>"',
        source: source,
      );
    }
    named = stage == parsed.name ? text : renameMaterialSource(text, stage);
    program = stage == parsed.name ? parsed : parseMaterial(named);
  } on MaterialSyntaxException catch (error, stackTrace) {
    Error.throwWithStackTrace(
      MaterialBuildException(
        error.message,
        source: source,
        line: error.line,
        column: error.column,
      ),
      stackTrace,
    );
  }
  // One entry point per source, at its declared defaults. A variant is a
  // second entry point with other constants folded in, and naming those is
  // the manifest's to grow when somebody needs one.
  final name = program.name;
  if (program.kind == MaterialStageKind.compute) {
    // Lowered to the kernel language and carried in the kernel section,
    // which WebGPU, the wgpu sidecar and the software backend load as it
    // is. Impeller and WebGL2 have no compute: there the engine refuses the
    // stage naming `DeviceFeature.compute`, and the texture is baked on the
    // CPU at load (`MaterialComputeKernel.bake`).
    try {
      return kernelBundle(name, <ComputeKernel>[
        lowerMaterialCompute(program).kernel,
      ]);
    } on MaterialSyntaxException catch (error, stackTrace) {
      Error.throwWithStackTrace(
        MaterialBuildException(error.message, source: source),
        stackTrace,
      );
    }
  }
  final specialised = specializeMaterial(program, MaterialVariant(name));

  // The stages the source becomes: the fragment stage under the material's
  // name, and with a `vertex` block its vertex stages — the plain and the
  // skinned from version 2, the instanced and the lightmapped from 3. A
  // material lit by the standard model has a debug twin as the engine's
  // models do, `<name>Debug`, so the stage it draws every frame carries no
  // debug view and a frame with one on still shows it.
  final stages = <_Stage>[
    (name: name, fragment: true, glsl: emitMaterialFragment(specialised)),
    for (final MapEntry(key: stage, value: glsl) in emitMaterialDebugStages(
      specialised,
    ).entries)
      (name: stage, fragment: true, glsl: glsl),
    for (final MapEntry(key: stage, value: glsl) in emitMaterialVertexStages(
      specialised,
    ).entries)
      (name: stage, fragment: false, glsl: glsl),
  ];

  Never refused(String what, String message) =>
      throw MaterialBuildException('$what: $message', source: source);

  final generated = <String, File>{
    for (final stage in stages)
      stage.name:
          File(
              '${scratch.path}/${stage.name}.${stage.fragment ? 'frag' : 'vert'}',
            )
            ..parent.createSync(recursive: true)
            ..writeAsStringSync(stage.glsl),
  };

  final impeller = _impeller(
    name,
    stages,
    generated,
    engine,
    compilers,
    refused,
  );

  final webglVertex = <String, String>{};
  final webglFragment = <String, String>{};
  final resolved = <String, String>{};
  for (final stage in stages) {
    try {
      (stage.fragment
          ? webglFragment
          : webglVertex)[stage.name] = translateGlsl(
        stage.glsl,
        engine.sources,
        from: source,
        fragment: stage.fragment,
      );
      resolved[stage.name] = resolveIncludes(
        stage.glsl,
        engine.sources,
        from: source,
      );
    } on GlslTranslateException catch (error) {
      refused('the WebGL2 translation of ${stage.name}', error.message);
    }
  }
  // The budget, counted from the GLSL every backend's path has: before,
  // only the WebGPU section counted, and only where its compilers were
  // installed, so a stage over WebGL2's sixteen units shipped and failed to
  // link on a phone's browser. Mutation: skip this and
  // `material_budget_test.dart`'s stage of seventeen textures builds.
  for (final MapEntry(key: stage, value: glsl) in resolved.entries) {
    if (MaterialStageBudget.problemOfGlsl(stage, glsl) case final problem?) {
      refused('the binding budget', problem);
    }
  }

  final ByteData webgl;
  try {
    webgl = encodeWebGlSection(vertex: webglVertex, fragment: webglFragment);
  } on StageAgreementException catch (error) {
    refused('the WebGL2 section', error.message);
  }

  // D21: the material's own blocks and textures, numbered after the
  // engine's on WebGPU in the order its slot table lists them.
  final table = MaterialSlotTable.of(program);
  final webgpu = switch (compilers.wgsl) {
    null => null,
    final compile => _webgpu(
      stages,
      source,
      resolved,
      engine,
      compile,
      generated,
      refused,
      ownBindings: <String>[
        for (final block in table.blocks) block.name,
        for (final texture in table.textures) texture.binding,
      ],
    ),
  };

  final sections = <String, ByteData>{
    ShaderBundle.impellerSection: impeller,
    ShaderBundle.webglSection: webgl,
    ShaderBundle.webgpuSection: ?webgpu?.section,
    // The source itself, for the backend that compiles nothing and for a
    // runtime that builds the material's lighting model from it — `P8`.
    // Under every stage's name, so the software backend can answer each.
    ShaderBundle.materialSection: encodeMaterialSection(<String, String>{
      for (final stage in stages) stage.name: named,
    }),
  };

  // D26: every backend the registry holds a compiler for, after the three
  // above, so its section is the one the bundle carries.
  if (compilers.registry case final registry?) {
    final request = MaterialCompileRequest(
      name: name,
      from: source,
      source: named,
      stages: <MaterialStageSource>[
        for (final stage in stages)
          MaterialStageSource(
            name: stage.name,
            fragment: stage.fragment,
            glsl: stage.glsl,
          ),
      ],
      engineSources: engine.sources,
      options: <String, Object?>{
        // What WebGPU's compiler packs: the stages this build wrote as WGSL.
        MaterialCompileRequest.webGpuStagesOption: ?webgpu?.stages,
      },
    );
    for (final backend in registry.backends) {
      final compiler = registry.forBackend(backend)!;
      // No glslang or naga, so no WGSL for WebGPU's compiler to pack: the
      // bundle goes without the section, as it does with no registry.
      if (compiler.bundleSection == ShaderBundle.webgpuSection &&
          webgpu == null) {
        continue;
      }
      try {
        sections[compiler.bundleSection] = compiler.compile(request).section;
      } on MaterialCompileException catch (error) {
        // The section of a compiler whose section the build writes with a
        // tool of its own stays as written: Impeller's refuses because
        // `impellerc`, which it names, ran above. Any other refusal is the
        // build's failure, whatever this build wrote under the section.
        if (compiler.isSectionWrittenByBuild &&
            sections.containsKey(compiler.bundleSection)) {
          continue;
        }
        refused('the $backend compiler', error.message);
      } on StageAgreementException catch (error) {
        refused('the $backend compiler', error.message);
      }
    }
  }

  // Nothing went wrong, so nothing needs opening.
  for (final file in generated.values) {
    file.deleteSync();
  }

  return ShaderBundle(
    name: name,
    sdk: compilers.sdk,
    stages: <ShaderBundleStage>[
      for (final stage in stages)
        ShaderBundleStage(stage.name, fragment: stage.fragment),
    ],
    sections: sections,
  );
}