everburning

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.