runtimeMaterialBundle function

ShaderBundle runtimeMaterialBundle(
  1. String source, {
  2. required String? backend,
  3. required String from,
  4. Map<String, String>? engineSources,
  5. PackedStage? webGpuHost,
  6. int? webGpuFullscreenUvLocation,
  7. MaterialCompilerRegistry? compilers,
})

One material written in the material language, as a bundle backend can load with GraphicsDevice.loadShaders — decision 4: shaders compiled at run time work where the backend compiles at run time.

  • cpu: the bundle carries the source alone, in its material section, which the software device evaluates through its materialCompiler.
  • webgl: the source is emitted as GLSL, translated to GLSL ES 3.00 against engineSources — the engine's own GLSL headers, which a browser has no disk to read them from — and carried beside the source.
  • webgpu: the material's body is written as WGSL and spliced into webGpuHost — the engine's own compiled Unlit stage, which flutter3d_webgpu's webGpuMaterialHost() hands over — and carried with its reflection in a webgpu section (spliceMaterialWgsl). No compiler runs: glslang and naga, which make the engine's WGSL in the build, do not ship with a game. A material with a light block, one reading instance, or one sampling a map other than the base colour is refused, with the reason, and is built from assets_src/ instead.
  • impeller: refused, for good: its shaders are compiled ahead of time.

A backend's own compiler first (D26): when compilers has one for backend that compiles while a game runs, the material is handed to it — the stages as GLSL, the engine's headers, and the WebGPU host and full-screen location among its options — and its section goes into the bundle beside the source. One that compiles only in the build refuses, naming the backend, as Impeller does. Without one, the three above.

Throws a RuntimeShadersException for a backend that cannot, for a WebGL2 build with no engineSources, a WebGPU build with no webGpuHost, and for a source that does not parse — with its line and column. from names the source in every message.

Version 2 of the language. A material's vertex block becomes its two vertex stages beside the fragment stage on the software backend and WebGL2, and is refused on WebGPU at run time (the build compiles it). A fullscreen stage is built for all three: as its source, as GLSL ES, and on WebGPU as WGSL written whole (fullscreenMaterialWgsl) at webGpuFullscreenUvLocation. A compute stage is lowered to the kernel language and carried as a bundle of its kernel for a backend that computes; Impeller and WebGL2 refuse it with an UnsupportedCapabilityException naming DeviceFeature.compute.

Implementation

ShaderBundle runtimeMaterialBundle(
  String source, {
  required String? backend,
  required String from,
  Map<String, String>? engineSources,
  PackedStage? webGpuHost,
  int? webGpuFullscreenUvLocation,
  MaterialCompilerRegistry? compilers,
}) {
  final MaterialProgram program;
  try {
    program = parseMaterial(source);
  } on MaterialSyntaxException catch (error) {
    throw RuntimeShadersException(
      '$from:${error.line}:${error.column}: ${error.message}',
    );
  }
  final name = program.name;
  final kind = RuntimeBackends.normalize(backend);
  if (program.kind == MaterialStageKind.compute) {
    // Lowered to the kernel language, for any backend that computes; the
    // two that do not are refused by naming what they lack.
    if (kind == RuntimeBackends.impeller || kind == RuntimeBackends.webgl) {
      throw UnsupportedCapabilityException(
        DeviceFeature.compute,
        backend: backend!,
        reason:
            '$from: compute stage "$name" runs only where a device '
            'computes; compute its texture on the CPU at load with '
            'MaterialComputeKernel.bake and upload that instead',
      );
    }
    try {
      return kernelBundle(name, <ComputeKernel>[
        lowerMaterialCompute(program).kernel,
      ]);
    } on MaterialSyntaxException catch (error) {
      throw RuntimeShadersException('$from: ${error.message}');
    }
  }
  final fullscreen = program.kind == MaterialStageKind.fullscreen;
  final vertexStages = emitMaterialVertexStages(
    specializeMaterial(program, MaterialVariant(name)),
  );
  final vertexNames = vertexStages.keys.toList();
  final material = encodeMaterialSection(<String, String>{
    name: source,
    for (final vertex in vertexNames) vertex: source,
  });
  ShaderBundle bundle(Map<String, ByteData> sections) => ShaderBundle(
    name: name,
    sdk: runtimeShaderSdk,
    stages: <ShaderBundleStage>[
      ShaderBundleStage(name, fragment: true),
      for (final vertex in vertexNames)
        ShaderBundleStage(vertex, fragment: false),
    ],
    sections: sections,
  );
  // The material's WGSL, written from its tree: a full-screen stage written
  // whole, or the body spliced into the engine's compiled `Unlit` stage. Null
  // when this engine was given nothing to write it against.
  PackedStage? webGpuStage() {
    final specialised = specializeMaterial(program, MaterialVariant(name));
    return switch ((webGpuFullscreenUvLocation, webGpuHost)) {
      (final int uvLocation, _) when fullscreen => fullscreenMaterialWgsl(
        specialised,
        uvLocation: uvLocation,
        from: from,
      ),
      (_, final PackedStage host) when !fullscreen => spliceMaterialWgsl(
        specialised,
        host,
        from: from,
      ),
      _ => null,
    };
  }

  // The binding budget, on every backend that can be handed the engine's
  // headers, counted from the GLSL the emitter writes: the WebGPU section
  // counted it alone before, so a stage over WebGL2's sixteen units was
  // made here and failed to link at its first draw.
  if (engineSources != null && !fullscreen) {
    final specialised = specializeMaterial(program, MaterialVariant(name));
    final glsl = <String, String>{
      name: emitMaterialFragment(specialised),
      ...vertexStages,
    };
    for (final MapEntry(key: stage, value: text) in glsl.entries) {
      final String resolved;
      try {
        resolved = resolveIncludes(text, engineSources, from: from);
      } on GlslTranslateException catch (error) {
        throw RuntimeShadersException('$from: ${error.message}');
      }
      if (MaterialStageBudget.problemOfGlsl(stage, resolved)
          case final problem?) {
        throw RuntimeShadersException('$from: $problem');
      }
    }
  }
  final compiler = kind == null ? null : compilers?.forBackend(kind);
  if (compiler != null) {
    if (!compiler.compilesAtRunTime) {
      throw RuntimeShadersException(
        '$from: the $kind compiler runs in the build only, so no shader is '
        'compiled on $kind while the game runs. Put the material in '
        'assets_src/ and let flutter3d_build compile it into the bundle',
      );
    }
    final specialised = specializeMaterial(program, MaterialVariant(name));
    try {
      final result = compiler.compile(
        MaterialCompileRequest(
          name: name,
          from: from,
          source: source,
          atRunTime: true,
          engineSources: engineSources ?? const <String, String>{},
          stages: <MaterialStageSource>[
            MaterialStageSource(
              name: name,
              fragment: true,
              glsl: emitMaterialFragment(specialised),
            ),
            for (final MapEntry(key: stage, value: glsl)
                in vertexStages.entries)
              MaterialStageSource(name: stage, fragment: false, glsl: glsl),
          ],
          options: <String, Object?>{
            'webGpuHost': ?webGpuHost,
            'webGpuFullscreenUvLocation': ?webGpuFullscreenUvLocation,
            // What `WebGpuSectionCompiler` packs: the stages written as WGSL,
            // which only the language's own tree can write.
            if (kind == RuntimeBackends.webgpu)
              if (webGpuStage() case final stage?)
                MaterialCompileRequest.webGpuStagesOption:
                    <String, PackedStage>{name: stage},
          },
        ),
      );
      return bundle(<String, ByteData>{
        compiler.bundleSection: result.section,
        ShaderBundle.materialSection: material,
      });
    } on MaterialCompileException catch (refused) {
      throw RuntimeShadersException('$from: ${refused.message}');
    }
  }
  switch (kind) {
    case RuntimeBackends.cpu:
      return bundle(<String, ByteData>{ShaderBundle.materialSection: material});
    case RuntimeBackends.webgl:
      if (engineSources == null) {
        throw RuntimeShadersException(
          '$from: a material is translated to GLSL ES against the engine\'s '
          'own GLSL headers, and this engine was given none. Hand '
          'RuntimeShaders the headers (flutter3d_shaders\' sources, shipped '
          'as assets) as engineSources',
        );
      }
      final specialised = specializeMaterial(program, MaterialVariant(name));
      final glsl = emitMaterialFragment(specialised);
      final String webgl;
      final Map<String, String> vertex;
      try {
        webgl = translateGlsl(glsl, engineSources, from: from, fragment: true);
        vertex = <String, String>{
          for (final MapEntry(key: stage, value: glsl) in vertexStages.entries)
            stage: translateGlsl(
              glsl,
              engineSources,
              from: from,
              fragment: false,
            ),
        };
      } on GlslTranslateException catch (error) {
        throw RuntimeShadersException('$from: ${error.message}');
      }
      return bundle(<String, ByteData>{
        ShaderBundle.webglSection: encodeWebGlSection(
          vertex: vertex,
          fragment: <String, String>{name: webgl},
        ),
        ShaderBundle.materialSection: material,
      });
    case RuntimeBackends.webgpu when fullscreen:
      if (webGpuFullscreenUvLocation == null) {
        throw RuntimeShadersException(
          '$from: a full-screen stage is written as WGSL at run time to meet '
          'the engine\'s full-screen vertex stage, and this engine was not '
          'told where it hands over the coordinates. Hand RuntimeShaders '
          'flutter3d_webgpu\'s webGpuFullscreenUvLocation() as '
          'webGpuFullscreenUvLocation',
        );
      }
      final String document;
      try {
        document = wgslSectionDocument(
          vertex: const <String, PackedStage>{},
          fragment: <String, PackedStage>{name: webGpuStage()!},
        );
      } on WgslSectionException catch (error) {
        throw RuntimeShadersException('$from: ${error.message}');
      }
      return bundle(<String, ByteData>{
        ShaderBundle.webgpuSection: Uint8List.fromList(
          utf8.encode(document),
        ).buffer.asByteData(),
        ShaderBundle.materialSection: material,
      });
    case RuntimeBackends.webgpu:
      if (webGpuHost == null) {
        throw RuntimeShadersException(
          '$from: a material is built for WebGPU at run time inside the '
          'engine\'s own compiled stage, and this engine was given none. Hand '
          'RuntimeShaders flutter3d_webgpu\'s webGpuMaterialHost() as '
          'webGpuHost',
        );
      }
      final spliced = webGpuStage()!;
      final String document;
      try {
        document = wgslSectionDocument(
          vertex: const <String, PackedStage>{},
          fragment: <String, PackedStage>{name: spliced},
        );
      } on WgslSectionException catch (error) {
        throw RuntimeShadersException('$from: ${error.message}');
      }
      return bundle(<String, ByteData>{
        ShaderBundle.webgpuSection: Uint8List.fromList(
          utf8.encode(document),
        ).buffer.asByteData(),
        ShaderBundle.materialSection: material,
      });
    case RuntimeBackends.impeller:
      throw RuntimeShadersException(
        '$from: Impeller runs shaders compiled ahead of time by impellerc, '
        'which ships with the Flutter SDK and not with a game, so no shader '
        'is compiled on it while the game runs. Put the material in '
        'assets_src/ and let flutter3d_build compile it into the bundle',
      );
    case null:
      throw RuntimeShadersException(
        '$from: this engine draws nothing, so there is no backend to compile '
        'the material for',
      );
    default:
      throw RuntimeShadersException(
        '$from: "$backend" is not a backend this engine can compile a shader '
        'for at run time; ${RuntimeBackends.webgl}, ${RuntimeBackends.webgpu} '
        'and ${RuntimeBackends.cpu} can',
      );
  }
}