compileStage function
- String glsl, {
- required String name,
- bool fragment = false,
- bool compute = false,
- ShaderStageKind? kind,
- ShaderTarget target = ShaderTarget.wgsl,
Compiles glsl to SPIR-V and translates it to target — WGSL unless
asked otherwise — and reads back the offsets glslang assigned.
name appears in every failure message and in the temporary file names, so
a refusal names the stage rather than a path under /tmp.
offsets is glslang's own Offset decoration per uniform block member,
read out of -H's human-readable SPIR-V. It is not used to build the
reflection — the packer computes std140 itself, from the GLSL — but it is
the one independent witness to whether that arithmetic is right, and
generate_shaders.dart checks every block against it. The two agreeing is
what makes "std140 and WGSL's uniform address space lay this out the same
way" a measurement.
kind says what the stage runs as. Without it, compute and fragment
say it as they always have: a compute stage when compute is true, a
fragment stage when fragment is, and a vertex stage otherwise.
target ShaderTarget.spirv stops after glslang and hands back the
SPIR-V alone. WGSL is read back through naga a second time, as before;
the other languages are naga's to write and the backend's to load.
Implementation
CompiledStage compileStage(
String glsl, {
required String name,
bool fragment = false,
bool compute = false,
ShaderStageKind? kind,
ShaderTarget target = ShaderTarget.wgsl,
}) {
final stage =
kind ??
(compute
? ShaderStageKind.compute
: fragment
? ShaderStageKind.fragment
: ShaderStageKind.vertex);
final directory = Directory.systemTemp.createTempSync('flutter3d_wgsl_');
try {
// glslang picks the stage from the extension, so the extension is the
// one place a compute stage (`H6`) differs from the other two here.
final source = File(
'${directory.path}/$name.'
'${_extensionOf(stage)}',
)..writeAsStringSync(glsl);
final spirv = '${directory.path}/$name.spv';
final extension = switch (target) {
// naga picks the GLSL stage from the extension, as glslang does.
ShaderTarget.glsl => _extensionOf(stage),
_ => target.extension,
};
final translatedPath = extension == null
? null
: '${directory.path}/$name.out.$extension';
final compiled = Process.runSync(glslangExecutable, <String>[
'-V',
'--auto-map-locations',
'-H',
source.path,
'-o',
spirv,
]);
if (compiled.exitCode != 0) {
throw WgslCompileException(
'$glslangExecutable refused $name:\n${compiled.stdout}${compiled.stderr}',
);
}
final offsets = _offsets(compiled.stdout as String);
final binary = File(spirv).readAsBytesSync();
if (translatedPath == null) {
return CompiledStage(
spirv: binary,
target: target,
text: null,
offsets: offsets,
);
}
final translated = Process.runSync(nagaExecutable, <String>[
'--keep-coordinate-space',
spirv,
translatedPath,
]);
if (translated.exitCode != 0) {
throw WgslCompileException(
'$nagaExecutable refused $name:\n${translated.stdout}${translated.stderr}',
);
}
// Back through naga the other way, for WGSL. It costs a process and it
// answers a question the forward pass cannot: naga writing a file does
// not mean naga would accept that file, and the browser is a third
// implementation with an opinion of its own. Anything WGSL's own
// validator rejects would reach the device as a compile error at first
// draw, on a user's machine, with the build long finished.
if (target == ShaderTarget.wgsl) {
final revalidated = Process.runSync(nagaExecutable, <String>[
'--input-kind',
'wgsl',
translatedPath,
]);
if (revalidated.exitCode != 0) {
throw WgslCompileException(
'$nagaExecutable would not read back the WGSL it wrote for $name:\n'
'${revalidated.stdout}${revalidated.stderr}',
);
}
}
return CompiledStage(
spirv: binary,
target: target,
text: File(translatedPath).readAsStringSync(),
offsets: offsets,
);
} on ProcessException catch (error) {
throw WgslCompileException(
'could not run ${error.executable} for $name: ${error.message}. '
'glslangValidator comes with the Vulkan SDK or `brew install '
'glslang`; naga comes from `cargo install naga-cli`.',
);
} finally {
directory.deleteSync(recursive: true);
}
}