spliceMaterialWgsl function
PackedStage
spliceMaterialWgsl(
- MaterialProgram program,
- PackedStage host, {
- required String from,
program, already specialised, spliced into host's main — see the
library comment. from names the source in every refusal.
Throws a RuntimeShadersException for a material past the limits above, and
for a host that is not shaped as the engine's Unlit stage is.
Implementation
PackedStage spliceMaterialWgsl(
MaterialProgram program,
PackedStage host, {
required String from,
}) {
Never refuse(String why) => throw RuntimeShadersException('$from: $why');
if (program.light != null) {
refuse(
'a material with a light block is compiled for WebGPU by the build '
'(assets_src/), not while the game runs: at run time WebGPU takes a '
'material that returns the light its surface emits',
);
}
if (program.inputsUsed.contains('instance')) {
refuse(
'a material reading "instance" is compiled for WebGPU by the build '
'(assets_src/): the stage it runs in at run time has no instance '
'varying',
);
}
// Version 2, refused for the reason the light block is: each is a stage
// of another shape than the one spliced into.
if (program.kind != MaterialStageKind.surface) {
refuse(
'a ${program.kind} stage is not spliced into a material\'s host; '
'${program.kind == MaterialStageKind.fullscreen ? 'build it with fullscreenMaterialWgsl' : 'no GPU backend runs a compute kernel written in the language'}',
);
}
if (program.vertex != null) {
refuse(
'a material with a vertex block is compiled for WebGPU by the build '
'(assets_src/): at run time WebGPU has no vertex stage to splice one '
'into, and the engine\'s mesh stages stay what they are',
);
}
if (program.readsSceneDepth) {
refuse(
'a material reading the scene behind it is compiled for WebGPU by the '
'build (assets_src/): the stage it runs in at run time binds no scene '
'depth',
);
}
_refuseOwnSlots(program, refuse);
final wgsl = host.wgsl;
String found(RegExp pattern, String what) =>
pattern.firstMatch(wgsl)?.group(1) ??
refuse(
'the WebGPU host stage has no $what; hand RuntimeShaders the '
'engine\'s own (webGpuMaterialHost)',
);
final readSurface = found(
RegExp(r'fn (ReadSurface\w*)\(\) -> Surface'),
'ReadSurface',
);
final writeSurface = found(
RegExp(
r'fn (WriteSurface\w*)\(\w+: ptr<function, vec3<f32>>, '
r'\w+: ptr<function, f32>\)',
),
'two-argument WriteSurface',
);
final privates = <String>{
for (final match in RegExp(
r'^var<private> (\w+):',
multiLine: true,
).allMatches(wgsl))
match.group(1)!,
};
String varying(String glsl) {
// Version 2's `viewDepth`, read into a local of the spliced `main`.
if (glsl == 'f3d_view_depth') return glsl;
final type = switch (glsl) {
'v_texcoord' => 'vec2<f32>',
'v_world_position' => 'vec3<f32>',
_ => refuse('the input "$glsl" has no place in the WebGPU host stage'),
};
return found(
RegExp('var<private> (${glsl}_\\d+): ${RegExp.escape(type)};'),
'varying $glsl',
);
}
// `main_1` is the GLSL `main` naga wrote; the entry point calls it.
final start = wgsl.indexOf('fn main_1() {');
if (start < 0) refuse('the WebGPU host stage has no main_1');
var depth = 0;
var end = -1;
for (var i = wgsl.indexOf('{', start); i < wgsl.length; i++) {
if (wgsl[i] == '{') depth++;
if (wgsl[i] == '}' && --depth == 0) {
end = i + 1;
break;
}
}
if (end < 0) refuse('the WebGPU host stage\'s main_1 is never closed');
// What the GLSL's global initialisers became: naga writes them as the
// first assignments of `main_1`, before the surface is read. Only those
// to the module's own private variables, so nothing local is carried over.
final prologue = <String>[
for (final line
in wgsl
.substring(start, end)
.split('\n')
.skip(1)
.takeWhile((l) => !l.contains(readSurface)))
if (RegExp(r'^\s*(\w+) = [^;]*;$').firstMatch(line) case final m?
when privates.contains(m.group(1)))
line,
];
// The texture slots, held to what the host binds.
final bound = <String>{for (final s in host.prepared.samplers) s.name};
for (final slot in program.textures) {
if (!bound.contains(slot.bindingName) ||
!wgsl.contains('var ${slot.bindingName}_tex:')) {
refuse(
'the texture "${slot.name}" reads ${slot.bindingName}, and the stage '
'a material runs in on WebGPU at run time binds '
'${bound.join(', ')}; a material sampling the others is compiled by '
'the build (assets_src/)',
);
}
}
// The uniforms, as the block the engine binds `RenderMaterial.parameters` to.
final uniforms = <MaterialParameter>[
for (final parameter in program.parameters)
if (parameter.uniform) parameter,
];
final binding =
<int>[
for (final block in host.prepared.blocks) block.binding,
for (final sampler in host.prepared.samplers) ...<int>[
sampler.textureBinding,
sampler.samplerBinding,
],
].fold(-1, (int a, int b) => a > b ? a : b) +
1;
final group = host.prepared.blocks.isEmpty
? 1
: host.prepared.blocks.first.group;
final params = uniforms.isEmpty
? null
: _paramsBlock(uniforms, group, binding);
final writer = _WgslWriter(
surface: 'f3d_s',
varying: varying,
params: 'f3d_material_params',
);
final body = StringBuffer();
for (final statement in program.body) {
body.write(
statement.accept(
_WgslStatements(
writer,
returns: (value) =>
' let f3d_result: vec4<f32> = $value;\n'
' var f3d_rgb: vec3<f32> = f3d_result.xyz;\n'
' var f3d_alpha: f32 = f3d_result.w;\n'
' $writeSurface((&f3d_rgb), (&f3d_alpha));\n'
' return;\n',
// Refused above: only a vertex block writes outputs.
output: () =>
refuse('a vertex block has no place in a fragment stage'),
unknown: refuse,
),
),
);
}
// Version 2: what `main` sets before the body — the blend that leaves
// the colour as returned, and this fragment's depth along the view axis.
final prelude = StringBuffer();
if (program.state.blend == MaterialBlend.premultiplied) {
final premultiply = found(
RegExp(r'^var<private> (g_premultiply\w*): bool', multiLine: true),
'g_premultiply',
);
prelude.writeln(' $premultiply = false;');
}
if (program.inputsUsed.contains('viewDepth')) {
final viewDepth = found(
RegExp(r'fn (ViewDepth\w*)\(\) -> f32'),
'ViewDepth',
);
prelude.writeln(' let f3d_view_depth: f32 = $viewDepth();');
}
final declarations = StringBuffer();
if (params != null) {
declarations
..writeln('struct F3dMaterialParams {')
..writeAll(<String>[
for (final p in uniforms) ' m_${p.name}: ${_type(p.type)},\n',
])
..writeln('}')
..writeln()
..writeln('@group($group) @binding($binding) ')
..writeln('var<uniform> f3d_material_params: F3dMaterialParams;')
..writeln();
}
final main = StringBuffer()
..writeln('// The material "${program.name}", spliced in at run time.')
..writeln('fn main_1() {')
..writeAll(<String>[for (final line in prologue) '$line\n'])
..writeln(' var f3d_s: Surface = $readSurface();')
..write(prelude)
..write(body)
..write('}');
return (
wgsl: '${wgsl.substring(0, start)}$declarations$main${wgsl.substring(end)}',
prepared: PreparedStage(
glsl: emitMaterialFragment(program),
attributes: host.prepared.attributes,
blocks: <PreparedBlock>[...host.prepared.blocks, ?params],
samplers: host.prepared.samplers,
),
);
}