brendan-duncan/wgsl_reflect

Support for arrays of arrays and other stuff

greggman opened this issue ยท 10 comments

I think there is some inconsistency in how nested types are handled.

For example I have this test


const reflect = new WgslReflect(`
    struct InnerUniforms {
        bar: u32,
    };

    struct VSUniforms {
        foo: u32,
        moo: InnerUniforms,
    };
@group(0) @binding(0) var<uniform> foo0: vec3f;
@group(0) @binding(1) var<uniform> foo1: array<vec3f, 5>;
@group(0) @binding(2) var<uniform> foo2: array<array<vec3f, 5>, 6>;
@group(0) @binding(3) var<uniform> foo3: array<array<array<vec3f, 5>, 6>, 7>;

@group(0) @binding(4) var<uniform> foo4: VSUniforms;
@group(0) @binding(5) var<uniform> foo5: array<VSUniforms, 5>;
@group(0) @binding(6) var<uniform> foo6: array<array<VSUniforms, 5>, 6>;
@group(0) @binding(7) var<uniform> foo7: array<array<array<VSUniforms, 5>, 6>, 7>;
`);

If I then print out stuff like this

reflect.uniforms.map(uniform => {
    const info = reflect.getUniformBufferInfo(uniform);
    console.log('---------:', uniform.name);
    console.log(JSON.stringify(info, null, 2));
});

I get this

---------: foo0
{
  "align": 16,
  "size": 12,
  "name": "foo0",
  "type": {
    "name": "vec3f",
    "attributes": null
  },
  "isArray": false,
  "isStruct": false,
  "arrayStride": 12,
  "arrayCount": 0,
  "group": 0,
  "binding": 0
}
---------: foo1
{
  "align": 16,
  "size": 80,
  "name": "foo1",
  "type": {
    "name": "array",
    "attributes": null,
    "format": {
      "name": "vec3f"
    },
    "count": 5
  },
  "isArray": true,
  "isStruct": false,
  "arrayStride": 12,
  "arrayCount": 5,
  "group": 0,
  "binding": 1
}
---------: foo2
{
  "align": 16,
  "size": 480,
  "name": "foo2",
  "type": {
    "name": "array",
    "attributes": null,
    "format": {
      "name": "array",
      "attributes": null,
      "format": {
        "name": "vec3f"
      },
      "count": 5
    },
    "count": 6
  },
  "isArray": true,
  "isStruct": false,
  "arrayStride": 80,
  "arrayCount": 6,
  "group": 0,
  "binding": 2
}
---------: foo3
{
  "align": 16,
  "size": 3360,
  "name": "foo3",
  "type": {
    "name": "array",
    "attributes": null,
    "format": {
      "name": "array",
      "attributes": null,
      "format": {
        "name": "array",
        "attributes": null,
        "format": {
          "name": "vec3f"
        },
        "count": 5
      },
      "count": 6
    },
    "count": 7
  },
  "isArray": true,
  "isStruct": false,
  "arrayStride": 480,
  "arrayCount": 7,
  "group": 0,
  "binding": 3
}
---------: foo4
{
  "align": 4,
  "size": 8,
  "name": "foo4",
  "type": {
    "name": "VSUniforms",
    "attributes": null
  },
  "members": [
    {
      "node": {
        "name": "foo",
        "type": {
          "name": "u32",
          "attributes": null
        },
        "attributes": null
      },
      "name": "foo",
      "offset": 0,
      "size": 4,
      "type": {
        "name": "u32",
        "attributes": null
      },
      "isArray": false,
      "arrayCount": 0,
      "arrayStride": 4,
      "isStruct": false
    },
    {
      "node": {
        "name": "moo",
        "type": {
          "name": "InnerUniforms",
          "attributes": null
        },
        "attributes": null
      },
      "name": "moo",
      "offset": 4,
      "size": 4,
      "type": {
        "name": "InnerUniforms",
        "attributes": null
      },
      "isArray": false,
      "arrayCount": 0,
      "isStruct": true,
      "members": [
        {
          "node": {
            "name": "bar",
            "type": {
              "name": "u32",
              "attributes": null
            },
            "attributes": null
          },
          "name": "bar",
          "offset": 0,
          "size": 4,
          "type": {
            "name": "u32",
            "attributes": null
          },
          "isArray": false,
          "arrayCount": 0,
          "arrayStride": 4,
          "isStruct": false
        }
      ]
    }
  ],
  "isArray": false,
  "isStruct": true,
  "arrayCount": 0,
  "group": 0,
  "binding": 4
}
---------: foo5
{
  "align": 4,
  "size": 40,
  "name": "foo5",
  "type": {
    "name": "array",
    "attributes": null,
    "format": {
      "name": "VSUniforms"
    },
    "count": 5
  },
  "isArray": true,
  "isStruct": true,
  "members": [
    {
      "node": {
        "name": "foo",
        "type": {
          "name": "u32",
          "attributes": null
        },
        "attributes": null
      },
      "name": "foo",
      "offset": 0,
      "size": 4,
      "type": {
        "name": "u32",
        "attributes": null
      },
      "isArray": false,
      "arrayCount": 0,
      "arrayStride": 4,
      "isStruct": false
    },
    {
      "node": {
        "name": "moo",
        "type": {
          "name": "InnerUniforms",
          "attributes": null
        },
        "attributes": null
      },
      "name": "moo",
      "offset": 4,
      "size": 4,
      "type": {
        "name": "InnerUniforms",
        "attributes": null
      },
      "isArray": false,
      "arrayCount": 0,
      "isStruct": true,
      "members": [
        {
          "node": {
            "name": "bar",
            "type": {
              "name": "u32",
              "attributes": null
            },
            "attributes": null
          },
          "name": "bar",
          "offset": 0,
          "size": 4,
          "type": {
            "name": "u32",
            "attributes": null
          },
          "isArray": false,
          "arrayCount": 0,
          "arrayStride": 4,
          "isStruct": false
        }
      ]
    }
  ],
  "arrayStride": 8,
  "arrayCount": 5,
  "group": 0,
  "binding": 5
}
---------: foo6
{
  "align": 4,
  "size": 240,
  "name": "foo6",
  "type": {
    "name": "array",
    "attributes": null,
    "format": {
      "name": "array",
      "attributes": null,
      "format": {
        "name": "VSUniforms"
      },
      "count": 5
    },
    "count": 6
  },
  "isArray": true,
  "isStruct": false,
  "arrayStride": 40,
  "arrayCount": 6,
  "group": 0,
  "binding": 6
}
---------: foo7
{
  "align": 4,
  "size": 1680,
  "name": "foo7",
  "type": {
    "name": "array",
    "attributes": null,
    "format": {
      "name": "array",
      "attributes": null,
      "format": {
        "name": "array",
        "attributes": null,
        "format": {
          "name": "VSUniforms"
        },
        "count": 5
      },
      "count": 6
    },
    "count": 7
  },
  "isArray": true,
  "isStruct": false,
  "arrayStride": 240,
  "arrayCount": 7,
  "group": 0,
  "binding": 7
}
--end--

You can see foo4 has members ['foo', 'bar']

foo5 also does but lists the top level as an array

foo6 and foo7 are missing all "member" info.

Similar issues exist for foo2 and foo3 vs foo0 and foo1.

How should this be handled? Maybe some other info for array should then nest to a type definition rather than how they are currently combined at the top level? In other words, right now the top level has things like "arrayCount" and "arrayStride" and "isArray" and "isStruct" but as these are nested deeper this seems to break?

You're right, this is a bit of a mess.
My thought is I'll move isArray and isStruct out of BufferInfo and isArray: true to ArrayType and isStruct: true to Struct.
ArrayType.format will be either an ArrayType, Struct, or Type.

Then you could put together a string of the type with something like (untested):

function getTypeString(type) {
  if (type.isArray) {
    return `array<${getTypeString(type.format)}, ${type.arrayCount}>`;
  } else if (type.isStruct) {
    return type.name + " {\n" + type.members.map((m) => `  ${m.name}: ${getTypeString(m.type)}\n`) + " }";
  }
  return info.name;
}

console.log(getTypeString(foo7_info.type));

In the foo7 case it should print (in theory):

array<array<array<VSUniforms {
  foo: u32,
  moo: InnerUniforms {
    bar: u32
  }
}, 5>, 6, 7>

Nested arrayStride still needs to be figured out. I'll have to think about that more when I have more active brain cells.

I started refactoring the reflection code. I put it in a branch since it's 1) still a work in progress, a bunch to test and clean up, and 2) I'm not 100% confident on this direction.

My thought is getUniformBufferInfo and getStorageBufferInfo, which return a BufferInfo, isn't necessary.

Instead, AST.Type has a size property, Struct (now derived from Type) has an align property, ArrayType has count and stride properties. WgslReflect will populate these properties.

For your example,

const reflect = new WgslReflect(`
    struct InnerUniforms {
        bar: u32,
    };

    struct VSUniforms {
        foo: u32,
        moo: InnerUniforms,
    };
    @group(0) @binding(0) var<uniform> foo0: vec3f;
    @group(0) @binding(1) var<uniform> foo1: array<vec3f, 5>;
    @group(0) @binding(2) var<uniform> foo2: array<array<vec3f, 5>, 6>;
    @group(0) @binding(3) var<uniform> foo3: array<array<array<vec3f, 5>, 6>, 7>;

    @group(0) @binding(4) var<uniform> foo4: VSUniforms;
    @group(0) @binding(5) var<uniform> foo5: array<VSUniforms, 5>;
    @group(0) @binding(6) var<uniform> foo6: array<array<VSUniforms, 5>, 6>;
    @group(0) @binding(7) var<uniform> foo7: array<array<array<VSUniforms, 5>, 6>, 7>;`);
    
test.equals(reflect.uniforms.length, 8);

test.equals(reflect.uniforms[0].type.size, 12); // type is a AST.Type

test.equals(reflect.uniforms[1].type.isArray, true); // type is an AST.ArrayType
test.equals(reflect.uniforms[1].type.isStruct, false);
test.equals(reflect.uniforms[1].type.count, 5);
test.equals(reflect.uniforms[1].type.size, 80);

test.equals(reflect.uniforms[2].type.isArray, true); // type is an AST.ArrayType
test.equals(reflect.uniforms[2].type.isStruct, false);
test.equals(reflect.uniforms[2].type.count, 6);
test.equals(reflect.uniforms[2].type.size, 480);
test.equals(reflect.uniforms[2].type.format.isArray, true); // format is an AST.ArrayType
test.equals(reflect.uniforms[2].type.format.count, 5);
test.equals(reflect.uniforms[2].type.format.size, 80);

test.equals(reflect.uniforms[4].type.isStruct, true);
test.equals(reflect.uniforms[4].type.members.length, 2);
test.equals(reflect.uniforms[4].type.members[0].type.size, 4);
test.equals(reflect.uniforms[4].type.members[1].type.size, 4);
test.equals(reflect.uniforms[4].type.members[1].type.isStruct, true);
test.equals(reflect.uniforms[4].type.members[1].type.members,length, 1);

Now I'm thinking I'll make separate classes for the Type info for the reflection info, and not use the AST nodes directly in the reflection code. I think it'll make it easier to untangle.

Fell down the rabbit hole, started rewriting the reflection code to attempt to be cleaner and more consistent.

Thanks for doing this. I agree the direct AST would probably not be so useful because it would include expressions and constant references and other things.

I think it's mostly done, in the branch https://github.com/brendan-duncan/wgsl_reflect/tree/refactor_reflection_info. The README has the basics of the new design.

The get*Info (getUniformInfo, etc) methods are gone because the info is already available in the main reflection data. I kept getBindGroups because I think that's kinda useful to gather the bindGroups into a nested array as bindGroups[group][binding].

Look good to me. I will try to update my code to use it.

I'll push it over to main this evening. There are some more bugs in the parser I've neglected for a while, since I'm on a bit of a roll with this project lately I'll get to them...assuming the kids keep giving me enough time in the evening to work on it :-)

Merged into main.

Thanks Brendan. I updated my own library to use the new version and updated the wgsl offset computer which is based on both. I probably need more tests ๐Ÿ˜… but the ones I had are working. ๐ŸŽ‰