compileStage function

CompiledStage compileStage(
  1. String glsl, {
  2. required String name,
  3. bool fragment = false,
  4. bool compute = false,
  5. ShaderStageKind? kind,
  6. 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);
  }
}