runtimeMaterialBundle function
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 compiledUnlitstage, whichflutter3d_webgpu'swebGpuMaterialHost()hands over — and carried with its reflection in awebgpusection (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 alightblock, one readinginstance, or one sampling a map other than the base colour is refused, with the reason, and is built fromassets_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',
);
}
}