Dumping WGSL Structure Information
2025-02-07
Writing a WebGPU Native program with Dawn one of the issues I often run into is remembering the layout rules for filling buffers. There is the excellent WebGPU Offset Calculator but I wanted something more automatic.
Tint, the WGSL compiler used by Dawn, doesn't really have a public API, but we can still access it's pieces and do what we want, we just may have to fix things up when upgrading Dawn if the paths/methods change.
Now when I run my program, whenever a shader is loaded I emit all of the structure information to the console. I can quickly see where Tint had to insert padding into structures to match the WGSL alignment rules and I can make sure the structures on the C++ side have matching padding.
There are two pieces of Tint we will be using for this, the WGSL parser which will
parse the given source and create a tint::Program
and the StyledTextPrinter
to
emit the various structures.
We will need to link against libtint
and provide two includes to do what we
want. One is the primary Tint header which would be mostly stable, the other is
an internal printer tool we're going to use to print the structure information.
Tint itself has this printer as part of it's error messages, so it already has
the ability to emit structures with padding information annotated.
#include "third_party/dawn/include/tint/tint.h"
#include "third_party/dawn/src/tint/utils/text/styled_text_printer.h"
I'm using a helper method where I provide the WGSL filename and a
std::vector<char>
which contains the WGSL program. (The filename doesn't
actually matter, it's just used for error output if there are any). The Tint
API accepts a std::string
for the WGSL program, but it just happened that
I already had a std::vector<char>
which I turn into a std::string
internally. If you have a std::string
you should just use that instead.
void dump_shader_struct_info(std::string_view filename,
std::vector<char>& data) {
tint::wgsl::reader::Options options{
.allowed_features = tint::wgsl::AllowedFeatures::Everything(),
};
auto file = std::make_unique<tint::Source::File>(
std::string(filename), std::string(data.begin(), data.end() - 1));
auto program = tint::wgsl::reader::Parse(file.get(), options);
if (program.Diagnostics().ContainsErrors()) {
std::println(stderr, "failed to parse: {}", program.Diagnostics().Str());
}
for (const auto* ty : program.Types()) {
if (!ty->Is<tint::core::type::Struct>()) {
continue;
}
const auto* s = ty->As<tint::core::type::Struct>();
tint::StyledTextPrinter::Create(stderr)->Print(s->Layout() << "\n\n");
}
}
Tint allows controlling which features are available. If a feature is used but
not available it's an error. To that end, we enable the
tint::wgsl::AllowedFeatures::Everything()
to turn on all internal Tint
features. We want the structure information, and don't want to worry about
hitting parsing errors because we forgot to turn on a feature like float16
.
The Tint parser expects to receive a tint::Source::File
containing the source
WGSL string. This File
needs to live as long as the Program
which is
returned by the Parse
method. We provide that file to the
tint::wgsl::reader::Parse()
method which will parse and create our
tint::Program
. If there were any parse errors they're contained in the parsed
Diagnostic
which we can check with program.Diagnostics().ContainsErrors()
.
If there are errors, the Diagnostic
can be converted to a string with the
Str()
method and printed.
Once we've successfully created a program, we can iterate through all of the
Types()
in the program and, if the type is a tint::core::type::Struct
we
create a tint::StyledTextPrinter
and Print()
the Layout()
of the
structure.
With that, you should see output like what's listed below logged to stderr
.
/* align(16) size(144) */ struct Uniforms {
/* offset( 0) align( 4) size( 4) */ time : f32,
/* offset( 4) align( 1) size( 12) */ // -- implicit field alignment padding --
/* offset( 16) align(16) size( 64) */ m : mat4x4<f32>,
/* offset( 80) align(16) size( 64) */ vp : mat4x4<f32>,
/* */ };
/* align(16) size(80) */ struct Material {
/* offset( 0) align(16) size(12) */ ambient : vec3<f32>,
/* offset(12) align( 1) size( 4) */ // -- implicit field alignment padding --
/* offset(16) align(16) size(12) */ diffuse : vec3<f32>,
/* offset(28) align( 1) size( 4) */ // -- implicit field alignment padding --
/* offset(32) align(16) size(12) */ specular : vec3<f32>,
/* offset(44) align( 1) size( 4) */ // -- implicit field alignment padding --
/* offset(48) align(16) size(12) */ emissive : vec3<f32>,
/* offset(60) align( 4) size( 4) */ specular_exponent : f32,
/* offset(64) align( 4) size( 4) */ disolve_factor : f32,
/* offset(68) align( 4) size( 4) */ optical_density : f32,
/* offset(72) align( 1) size( 8) */ // -- implicit struct size padding --
/* */ };
/* align(16) size(16) */ struct VertexInput {
/* offset( 0) align(16) size(16) */ pos : vec4<f32>,
/* */ };
/* align(16) size(32) */ struct Vertex_Normal_Input {
/* offset( 0) align(16) size(16) */ pos : vec4<f32>,
/* offset(16) align(16) size(12) */ normal : vec3<f32>,
/* offset(28) align( 1) size( 4) */ // -- implicit struct size padding --
/* */ };
/* align(16) size(48) */ struct Vertex_Normal_Texture_Material_Input {
/* offset( 0) align(16) size(16) */ pos : vec4<f32>,
/* offset(16) align(16) size(12) */ normal : vec3<f32>,
/* offset(28) align( 1) size( 4) */ // -- implicit field alignment padding --
/* offset(32) align(16) size(12) */ texture : vec3<f32>,
/* offset(44) align( 4) size( 4) */ material : u32,
/* */ };
/* align(16) size(32) */ struct VertexOutput {
/* offset( 0) align(16) size(16) */ pos : vec4<f32>,
/* offset(16) align(16) size(16) */ normal : vec4<f32>,
/* */ };
/* align(16) size(48) */ struct VertexMatOutput {
/* offset( 0) align(16) size(16) */ pos : vec4<f32>,
/* offset(16) align(16) size(16) */ normal : vec4<f32>,
/* offset(32) align( 4) size( 4) */ material : u32,
/* offset(36) align( 1) size(12) */ // -- implicit struct size padding --
/* */ };
Now it's a lot easier to see when I need to add padding into a structure, or
between structures if I'm putting them into an array
. It also shows me where,
in the future I might want to shuffle some of the structures around to save
space.