{"article":{"slug":"testing-webgpu-data-layouts-with-facet","title":"Testing WebGPU data layouts with Facet","subtitle":null,"summary":"Matt Keeter shows how Facet reflection plus naga lets Rust unit tests catch WGSL/Rust struct layout mismatches—padding on mat3x3, dynamic arrays, and field offsets—before GPU hangs force a reboot.","content_type":"tutorial","language":"en","canonical_url":"https://www.mattkeeter.com/blog/2026-08-23-wgpu-facet/","author":{"name":"Matt Keeter","url":"https://www.mattkeeter.com/","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Matt Keeter","url":"https://www.mattkeeter.com/","listing_slug":null,"listing":null},"topics":[{"name":"Rust","slug":"rust","url":"https://listedarticles.com/topics/rust"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Tutorials","slug":"tutorials","url":"https://listedarticles.com/topics/tutorials"},{"name":"Hardware","slug":"hardware","url":"https://listedarticles.com/topics/hardware"},{"name":"Systems Programming","slug":"systems-programming","url":"https://listedarticles.com/topics/systems-programming"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":1444,"reading_minutes":6,"published_at":"2026-08-23T00:00:00.000Z","added_at":"2026-09-30T09:13:05.429Z","updated_at":"2026-09-30T09:13:05.429Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/testing-webgpu-data-layouts-with-facet","markdown_url":"https://listedarticles.com/articles/testing-webgpu-data-layouts-with-facet.md","example":false,"citation":"Matt Keeter, Matt Keeter. \"Testing WebGPU data layouts with Facet.\" 23 Aug 2026. https://www.mattkeeter.com/blog/2026-08-23-wgpu-facet/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://www.mattkeeter.com/blog/2026-08-23-wgpu-facet/"},"body_markdown":"## Testing WebGPU data layouts with Facet\n\nWhen working on GPU compute shaders, it's common to want to share snippets of\nconfiguration data between the host and the shader.  Here's a toy example,\nadapted from [`fidget-wgpu`](https://docs.rs/fidget-wgpu/latest/fidget_wgpu/):\n\n```\n// WGSL\nstruct Config {\n    /// Screen-to-model transform matrix\n    mat: mat3x3f,\n    /// Image size, in pixels\n    image_size: vec2u,\n    /// Z position at which to render the image\n    z: f32,\n}\n```\n```\n// Rust\n#[derive(\n    zerocopy::IntoBytes,\n    zerocopy::Immutable,\n    zerocopy::FromBytes,\n    zerocopy::KnownLayout\n)]\n#[repr(C)]\nstruct Config {\n    mat: [[f32; 3]; 3],\n    image_size: [u32; 2],\n    z: f32,\n}\n```\nThanks to the [`zerocopy`](https://docs.rs/zerocopy/) annotations,\nwe can call `as_bytes()` and write the configuration data directly into a WebGPU\nbuffer, which is very convenient!\n\nUnfortunately, there are often subtle difference between Rust and WebGPU's layout rules. Do you know what's wrong with the above example?\n\nEven *more* unfortunately, I'm writing bytecode VMs which run in compute\nshaders, and their failure mode is often \"congrats, your GPU now has\npersistently spinning threads which can only be killed by rebooting your\ncomputer\".\n\nAfter debugging the most recent reboot (thanks to the\n[WGSL offset computer](https://webgpufundamentals.org/webgpu/lessons/resources/wgsl-offset-computer.html)),\nI decided to fix the problem in a more systematic way.\n\nThere are existing options: [`wgsl_to_wgpu`](https://docs.rs/wgsl_to_wgpu),\n[`wgsl_bindgen`](https://docs.rs/wgsl_bindgen), and\n[`encase`](https://docs.rs/encase/latest/encase/) are all relevant to the\nproblem.  However, I decided to roll my own to avoid more external dependencies\nand build script wrangling.  Specifically, I decided to write **unit tests**,\nto add no overhead to a typical build.\n\n(If you want to be pedantic, using unit tests does have a failure mode of forgetting to test a new configuration object, but I'm not too worried about it)\n\nWe can find the WGSL `struct` layout using [`naga`](https://docs.rs/naga/),\nwhich is already in our dependency tree for shader compilation:\n\n```\nlet module = naga::front::wgsl::parse_str(code).expect(\"valid WGSL\");\nlet members = module\n    .types\n    .iter()\n    .find_map(|(_, ty)| {\n        if ty.name.as_deref() == Some(\"Config\")\n            && let naga::TypeInner::Struct { members, .. } = &ty.inner\n        {\n            Some(members)\n        } else {\n            None\n        }\n    })\n    .expect(\"could not find struct\");\n```\nThen, we can cross-check against each member of the `struct Config`:\n\n```\nlet expected_offsets = [\n    (\"mat\", std::mem::offset_of!(Config, mat)),\n    (\"image_size\", std::mem::offset_of!(Config, image_size)),\n    (\"z\", std::mem::offset_of!(Config, z)),\n];\nfor (field_name, rust_offset) in expected_offsets {\n    let wgsl_member = members\n        .iter()\n        .find(|m| m.name.as_deref() == Some(field_name))\n        .unwrap_or_else(|| {\n            panic!(\"field `{field_name}` missing in WGSL struct\")\n        });\n    assert_eq!(\n        wgsl_member.offset as usize, rust_offset,\n        \"offset mismatch for field `{field_name}`\"\n    );\n}\n```\nSure enough, this finds an issue!\n\n```\nthread 'pixel::test::blog_test' (532515) panicked at fidget-wgpu/src/pixel/mod.rs:1762:13:\nassertion `left == right` failed: offset mismatch for field `image_size`\n  left: 48\n right: 36\n```\nIn WGSL, each row of a `mat3x3` has 4 bytes of padding, so each row is 16 bytes\nin total and the whole matrix is 48 bytes.  In Rust, a `[[f32; 3]; 3]` object is\ntightly packed and therefore occupies only 36 bytes.\n\nThe test worked, but there are two problems with this approach:\n\n- First, we have to hard-code each member of the `Config` , which is awkward and\nerror-prone!  If we want to test multiple configuration objects, we'd have to\nhand-write each one and keep them in sync.\n- Second, the error message isn't great because we can't directly compare sizes\nof fields.  The *reason*`image_size` has the wrong offset is because`mat` has a mismatched size; we should report the proximate cause.  (This also means\nthat we wouldn't report an incorrectly-sized final member)\n\nWhat's to be done?\n\n[`facet`](https://facet.rs/) is a library for Rust which provides run-time\nreflection.  By annotating your `struct` with `#[derive(facet::Facet)]`, you get\na `SHAPE` associated type which can be inspected at runtime.\n\nWe can use this to automatically check a `Config` object!\n\nLet's walk through the generic checker function, which is parameterized by a `T: facet::Facet`.  We'll start by parsing the shader and extracting the\nconfiguration `struct` by name; this is basically the same as before:\n\n```\npub(crate) fn compare_struct_layout<T: facet::Facet<'static>>(\n    shader: &str,\n    struct_name: &str,\n) {\n    // [1] Parse the WGSL, same as before\n    let module = naga::front::wgsl::parse_str(shader)\n        .expect(\"valid WGSL\");\n    let (members, span) = module\n        .types\n        .iter()\n        .find_map(|(_, ty)| {\n            if ty.name.as_deref() == Some(struct_name)\n                && let naga::TypeInner::Struct { members, span } = &ty.inner\n            {\n                Some((members, *span))\n            } else {\n                None\n            }\n        })\n        .expect(\"could not find struct\");\n```\nNext, we'll check the overall object size.  There's one subtlety here: I often\nmake use of\n[runtime-sized arrays](https://www.w3.org/TR/WGSL/#runtime-sized) as the last\nmember of a configuration object.\n(Think of this as a\n[flexible array member](https://en.wikipedia.org/wiki/Flexible_array_member)\nin C or\n[dynamically sized types](https://doc.rust-lang.org/reference/dynamically-sized-types.html)\nin Rust)\n\nHere's what it looks like in WGSL:\n\n```\nstruct VoxelConfig {\n    mat: mat4x4f,\n    axes: vec3u,\n    tape_data_offset: atomic<u32>,\n    render_size: vec3u,\n    tape_data_capacity: u32,\n    image_size: vec3u,\n    root_tape_len: atomic<u32>,\n    tape_data: array<TapeWord>, // <- runtime-sized array\n}\n```\nWhen writing the Rust `struct` equivalent, that last member is omitted.\nIf we have a runtime-sized array in `naga`, then we don't check the total size\nof the object (as reported by `naga`); instead, we check that the `offset` of\n`tape_data` matches the size of the Rust `struct`.\n\nHere's that section of the checker function:\n\n```\n// [2] Check the overall `struct` size\n//\n// If the last member of the struct is a runtime-sized\n// array, we'll treat the beginning offset of the array as\n// our struct size.\nlet dynamic_array_offset = members.last().and_then(|m| {\n    let ty = &module.types[m.ty];\n    let naga::TypeInner::Array {\n        base: _,\n        size: naga::ir::ArraySize::Dynamic,\n        stride: _,\n    } = &ty.inner\n    else {\n        return None;\n    };\n    Some(m.offset)\n});\nif let Some(dynamic_array_offset) = dynamic_array_offset {\n    assert_eq!(\n        dynamic_array_offset as usize,\n        std::mem::size_of::<T>()\n    );\n} else {\n    assert_eq!(\n        span as usize,\n        std::mem::size_of::<T>()\n    );\n}\n```\nNext, we'll go ahead and check field sizes and offsets. This is the same idea as our manual field-by-field checking earlier, but uses Facet's introspection data to do the checks at runtime (and also checks member sizes).\n\n```\n// [3] Check field sizes and offset between Rust and WGSL\nlet facet::Type::User(facet::UserType::Struct(shape)) = T::SHAPE.ty\nelse {\n    panic!(\"must build a struct\");\n};\nlet mut shape_field_names = HashSet::new(); // for later\nfor field in shape.fields {\n    let field_name = field.name;\n    shape_field_names.insert(field_name);\n    let wgsl_member = members\n        .iter()\n        .find(|m| m.name.as_deref() == Some(field_name))\n        .unwrap_or_else(|| {\n            panic!(\"field `{field_name}` missing in WGSL struct\")\n        });\n    assert_eq!(\n        wgsl_member.offset as usize, field.offset,\n        \"offset mismatch for field `{field_name}`\"\n    );\n    assert_eq!(\n        module.types[wgsl_member.ty]\n            .inner\n            .size(module.to_ctx()) as usize,\n        field.shape().layout.sized_layout().unwrap().size(),\n        \"size mismatch for field `{field_name}`\"\n    );\n}\n```\nFinally, we'll check the opposite direction, confirming that every WGSL member\nappears in the Rust `struct` (with the exception of a trailing runtime-sized\narray):\n\n```\n// [4] Check that every WGSL member appears in Rust\nlet slice_len = if dynamic_array_offset.is_some() {\n    members.len() - 1\n} else {\n    members.len()\n};\nfor m in &members[..slice_len] {\n    assert!(\n        shape_field_names.contains(\n            m.name\n                .as_ref()\n                .expect(\"cannot check unnamed WGSL fields\")\n                .as_str(),\n        ),\n        \"field `{field_name}` missing in Rust struct\"\n    );\n}\n```\nThis generic checker function took a little bit of wrangling, but once written,\nit can be easily applied to all of my configuration objects.  I derive `Facet`\nconditionally for unit tests only, e.g.\n\n```\n#[derive(\n    zerocopy::IntoBytes,\n    zerocopy::Immutable,\n    zerocopy::FromBytes,\n    zerocopy::KnownLayout\n)]\n#[cfg_attr(test, derive(facet::Facet))] // <- only derived in tests\n#[repr(C)]\nstruct Config {\n    mat: [[f32; 3]; 3],\n    image_size: [u32; 2],\n    z: f32,\n}\n```\nThen, I get the tests almost for free:\n\n```\n#[test]\nfn color_config_layout() {\n    crate::test::compare_struct_layout::<ColorConfig>(\n        &color_shader(16),\n        \"ColorConfig\",\n    );\n}\n#[test]\nfn merge_config_layout() {\n    crate::test::compare_struct_layout::<MergeConfig>(\n        &merge_shader(),\n        \"MergeConfig\",\n    );\n}\n#[test]\nfn shade_config_layout() {\n    crate::test::compare_struct_layout::<ShadeConfig>(\n        &shade_shader(),\n        \"ShadeConfig\",\n    );\n}\n#[test]\nfn ssao_config_layout() {\n    crate::test::compare_struct_layout::<SsaoConfig>(\n        &ssao_shader(),\n        \"SsaoConfig\",\n    );\n}\n#[test]\nfn blur_config_layout() {\n    crate::test::compare_struct_layout::<BlurConfig>(\n        &blur_shader(),\n        \"BlurConfig\",\n    );\n}\n```\nFortunately, these tests did not discover any pre-existing issues in my configuration objects – not surprising, because everything works. Still, next time I add a new compute shader, it will be a lot easier to feel confident in the correctness of data loading!\n\nIt's also easy to imagine using these kind of tests for other CPU-GPU interchange, which is mostly a moot point here (all other data types in my system are trivial), but could be relevant for other projects!\n\nAs another closing thought, [Facet](https://facet.rs) is great and people should\nuse it more.  I should probably write another blog post about how it's used\nelsewhere in Fidget; it provides the backbone for some truly horrific\nautomatically-generated bindings from Rust shape definitions to\n[Rhai](https://rhai.rs/) functions.  If that sounds\ninteresting, feel free to subscribe via [RSS](/atom.xml) and/or follow me on\n[social](https://bsky.app/profile/mattkeeter.com)\n[media](https://hachyderm.io/@mjk).","body_html":"<h2 id=\"testing-webgpu-data-layouts-with-facet\">Testing WebGPU data layouts with Facet</h2>\n<p>When working on GPU compute shaders, it&#39;s common to want to share snippets of\nconfiguration data between the host and the shader.  Here&#39;s a toy example,\nadapted from <a href=\"https://docs.rs/fidget-wgpu/latest/fidget_wgpu/\" rel=\"nofollow ugc noopener\"><code>fidget-wgpu</code></a>:</p>\n<pre><code>// WGSL\nstruct Config {\n    /// Screen-to-model transform matrix\n    mat: mat3x3f,\n    /// Image size, in pixels\n    image_size: vec2u,\n    /// Z position at which to render the image\n    z: f32,\n}</code></pre>\n<pre><code>// Rust\n#[derive(\n    zerocopy::IntoBytes,\n    zerocopy::Immutable,\n    zerocopy::FromBytes,\n    zerocopy::KnownLayout\n)]\n#[repr(C)]\nstruct Config {\n    mat: [[f32; 3]; 3],\n    image_size: [u32; 2],\n    z: f32,\n}</code></pre>\n<p>Thanks to the <a href=\"https://docs.rs/zerocopy/\" rel=\"nofollow ugc noopener\"><code>zerocopy</code></a> annotations,\nwe can call <code>as_bytes()</code> and write the configuration data directly into a WebGPU\nbuffer, which is very convenient!</p>\n<p>Unfortunately, there are often subtle difference between Rust and WebGPU&#39;s layout rules. Do you know what&#39;s wrong with the above example?</p>\n<p>Even <em>more</em> unfortunately, I&#39;m writing bytecode VMs which run in compute\nshaders, and their failure mode is often &quot;congrats, your GPU now has\npersistently spinning threads which can only be killed by rebooting your\ncomputer&quot;.</p>\n<p>After debugging the most recent reboot (thanks to the\n<a href=\"https://webgpufundamentals.org/webgpu/lessons/resources/wgsl-offset-computer.html\" rel=\"nofollow ugc noopener\">WGSL offset computer</a>),\nI decided to fix the problem in a more systematic way.</p>\n<p>There are existing options: <a href=\"https://docs.rs/wgsl_to_wgpu\" rel=\"nofollow ugc noopener\"><code>wgsl_to_wgpu</code></a>,\n<a href=\"https://docs.rs/wgsl_bindgen\" rel=\"nofollow ugc noopener\"><code>wgsl_bindgen</code></a>, and\n<a href=\"https://docs.rs/encase/latest/encase/\" rel=\"nofollow ugc noopener\"><code>encase</code></a> are all relevant to the\nproblem.  However, I decided to roll my own to avoid more external dependencies\nand build script wrangling.  Specifically, I decided to write <strong>unit tests</strong>,\nto add no overhead to a typical build.</p>\n<p>(If you want to be pedantic, using unit tests does have a failure mode of forgetting to test a new configuration object, but I&#39;m not too worried about it)</p>\n<p>We can find the WGSL <code>struct</code> layout using <a href=\"https://docs.rs/naga/\" rel=\"nofollow ugc noopener\"><code>naga</code></a>,\nwhich is already in our dependency tree for shader compilation:</p>\n<pre><code>let module = naga::front::wgsl::parse_str(code).expect(&quot;valid WGSL&quot;);\nlet members = module\n    .types\n    .iter()\n    .find_map(|(_, ty)| {\n        if ty.name.as_deref() == Some(&quot;Config&quot;)\n            &amp;&amp; let naga::TypeInner::Struct { members, .. } = &amp;ty.inner\n        {\n            Some(members)\n        } else {\n            None\n        }\n    })\n    .expect(&quot;could not find struct&quot;);</code></pre>\n<p>Then, we can cross-check against each member of the <code>struct Config</code>:</p>\n<pre><code>let expected_offsets = [\n    (&quot;mat&quot;, std::mem::offset_of!(Config, mat)),\n    (&quot;image_size&quot;, std::mem::offset_of!(Config, image_size)),\n    (&quot;z&quot;, std::mem::offset_of!(Config, z)),\n];\nfor (field_name, rust_offset) in expected_offsets {\n    let wgsl_member = members\n        .iter()\n        .find(|m| m.name.as_deref() == Some(field_name))\n        .unwrap_or_else(|| {\n            panic!(&quot;field `{field_name}` missing in WGSL struct&quot;)\n        });\n    assert_eq!(\n        wgsl_member.offset as usize, rust_offset,\n        &quot;offset mismatch for field `{field_name}`&quot;\n    );\n}</code></pre>\n<p>Sure enough, this finds an issue!</p>\n<pre><code>thread &#39;pixel::test::blog_test&#39; (532515) panicked at fidget-wgpu/src/pixel/mod.rs:1762:13:\nassertion `left == right` failed: offset mismatch for field `image_size`\n  left: 48\n right: 36</code></pre>\n<p>In WGSL, each row of a <code>mat3x3</code> has 4 bytes of padding, so each row is 16 bytes\nin total and the whole matrix is 48 bytes.  In Rust, a <code>[[f32; 3]; 3]</code> object is\ntightly packed and therefore occupies only 36 bytes.</p>\n<p>The test worked, but there are two problems with this approach:</p>\n<ul><li><p>First, we have to hard-code each member of the <code>Config</code> , which is awkward and</p><p>error-prone!  If we want to test multiple configuration objects, we&#39;d have to\nhand-write each one and keep them in sync.</p></li><li><p>Second, the error message isn&#39;t great because we can&#39;t directly compare sizes</p><p>of fields.  The <em>reason</em><code>image_size</code> has the wrong offset is because<code>mat</code> has a mismatched size; we should report the proximate cause.  (This also means\nthat we wouldn&#39;t report an incorrectly-sized final member)</p></li></ul>\n<p>What&#39;s to be done?</p>\n<p><a href=\"https://facet.rs/\" rel=\"nofollow ugc noopener\"><code>facet</code></a> is a library for Rust which provides run-time\nreflection.  By annotating your <code>struct</code> with <code>#[derive(facet::Facet)]</code>, you get\na <code>SHAPE</code> associated type which can be inspected at runtime.</p>\n<p>We can use this to automatically check a <code>Config</code> object!</p>\n<p>Let&#39;s walk through the generic checker function, which is parameterized by a <code>T: facet::Facet</code>.  We&#39;ll start by parsing the shader and extracting the\nconfiguration <code>struct</code> by name; this is basically the same as before:</p>\n<pre><code>pub(crate) fn compare_struct_layout&lt;T: facet::Facet&lt;&#39;static&gt;&gt;(\n    shader: &amp;str,\n    struct_name: &amp;str,\n) {\n    // [1] Parse the WGSL, same as before\n    let module = naga::front::wgsl::parse_str(shader)\n        .expect(&quot;valid WGSL&quot;);\n    let (members, span) = module\n        .types\n        .iter()\n        .find_map(|(_, ty)| {\n            if ty.name.as_deref() == Some(struct_name)\n                &amp;&amp; let naga::TypeInner::Struct { members, span } = &amp;ty.inner\n            {\n                Some((members, *span))\n            } else {\n                None\n            }\n        })\n        .expect(&quot;could not find struct&quot;);</code></pre>\n<p>Next, we&#39;ll check the overall object size.  There&#39;s one subtlety here: I often\nmake use of\n<a href=\"https://www.w3.org/TR/WGSL/#runtime-sized\" rel=\"nofollow ugc noopener\">runtime-sized arrays</a> as the last\nmember of a configuration object.\n(Think of this as a\n<a href=\"https://en.wikipedia.org/wiki/Flexible_array_member\" rel=\"nofollow ugc noopener\">flexible array member</a>\nin C or\n<a href=\"https://doc.rust-lang.org/reference/dynamically-sized-types.html\" rel=\"nofollow ugc noopener\">dynamically sized types</a>\nin Rust)</p>\n<p>Here&#39;s what it looks like in WGSL:</p>\n<pre><code>struct VoxelConfig {\n    mat: mat4x4f,\n    axes: vec3u,\n    tape_data_offset: atomic&lt;u32&gt;,\n    render_size: vec3u,\n    tape_data_capacity: u32,\n    image_size: vec3u,\n    root_tape_len: atomic&lt;u32&gt;,\n    tape_data: array&lt;TapeWord&gt;, // &lt;- runtime-sized array\n}</code></pre>\n<p>When writing the Rust <code>struct</code> equivalent, that last member is omitted.\nIf we have a runtime-sized array in <code>naga</code>, then we don&#39;t check the total size\nof the object (as reported by <code>naga</code>); instead, we check that the <code>offset</code> of\n<code>tape_data</code> matches the size of the Rust <code>struct</code>.</p>\n<p>Here&#39;s that section of the checker function:</p>\n<pre><code>// [2] Check the overall `struct` size\n//\n// If the last member of the struct is a runtime-sized\n// array, we&#39;ll treat the beginning offset of the array as\n// our struct size.\nlet dynamic_array_offset = members.last().and_then(|m| {\n    let ty = &amp;module.types[m.ty];\n    let naga::TypeInner::Array {\n        base: _,\n        size: naga::ir::ArraySize::Dynamic,\n        stride: _,\n    } = &amp;ty.inner\n    else {\n        return None;\n    };\n    Some(m.offset)\n});\nif let Some(dynamic_array_offset) = dynamic_array_offset {\n    assert_eq!(\n        dynamic_array_offset as usize,\n        std::mem::size_of::&lt;T&gt;()\n    );\n} else {\n    assert_eq!(\n        span as usize,\n        std::mem::size_of::&lt;T&gt;()\n    );\n}</code></pre>\n<p>Next, we&#39;ll go ahead and check field sizes and offsets. This is the same idea as our manual field-by-field checking earlier, but uses Facet&#39;s introspection data to do the checks at runtime (and also checks member sizes).</p>\n<pre><code>// [3] Check field sizes and offset between Rust and WGSL\nlet facet::Type::User(facet::UserType::Struct(shape)) = T::SHAPE.ty\nelse {\n    panic!(&quot;must build a struct&quot;);\n};\nlet mut shape_field_names = HashSet::new(); // for later\nfor field in shape.fields {\n    let field_name = field.name;\n    shape_field_names.insert(field_name);\n    let wgsl_member = members\n        .iter()\n        .find(|m| m.name.as_deref() == Some(field_name))\n        .unwrap_or_else(|| {\n            panic!(&quot;field `{field_name}` missing in WGSL struct&quot;)\n        });\n    assert_eq!(\n        wgsl_member.offset as usize, field.offset,\n        &quot;offset mismatch for field `{field_name}`&quot;\n    );\n    assert_eq!(\n        module.types[wgsl_member.ty]\n            .inner\n            .size(module.to_ctx()) as usize,\n        field.shape().layout.sized_layout().unwrap().size(),\n        &quot;size mismatch for field `{field_name}`&quot;\n    );\n}</code></pre>\n<p>Finally, we&#39;ll check the opposite direction, confirming that every WGSL member\nappears in the Rust <code>struct</code> (with the exception of a trailing runtime-sized\narray):</p>\n<pre><code>// [4] Check that every WGSL member appears in Rust\nlet slice_len = if dynamic_array_offset.is_some() {\n    members.len() - 1\n} else {\n    members.len()\n};\nfor m in &amp;members[..slice_len] {\n    assert!(\n        shape_field_names.contains(\n            m.name\n                .as_ref()\n                .expect(&quot;cannot check unnamed WGSL fields&quot;)\n                .as_str(),\n        ),\n        &quot;field `{field_name}` missing in Rust struct&quot;\n    );\n}</code></pre>\n<p>This generic checker function took a little bit of wrangling, but once written,\nit can be easily applied to all of my configuration objects.  I derive <code>Facet</code>\nconditionally for unit tests only, e.g.</p>\n<pre><code>#[derive(\n    zerocopy::IntoBytes,\n    zerocopy::Immutable,\n    zerocopy::FromBytes,\n    zerocopy::KnownLayout\n)]\n#[cfg_attr(test, derive(facet::Facet))] // &lt;- only derived in tests\n#[repr(C)]\nstruct Config {\n    mat: [[f32; 3]; 3],\n    image_size: [u32; 2],\n    z: f32,\n}</code></pre>\n<p>Then, I get the tests almost for free:</p>\n<pre><code>#[test]\nfn color_config_layout() {\n    crate::test::compare_struct_layout::&lt;ColorConfig&gt;(\n        &amp;color_shader(16),\n        &quot;ColorConfig&quot;,\n    );\n}\n#[test]\nfn merge_config_layout() {\n    crate::test::compare_struct_layout::&lt;MergeConfig&gt;(\n        &amp;merge_shader(),\n        &quot;MergeConfig&quot;,\n    );\n}\n#[test]\nfn shade_config_layout() {\n    crate::test::compare_struct_layout::&lt;ShadeConfig&gt;(\n        &amp;shade_shader(),\n        &quot;ShadeConfig&quot;,\n    );\n}\n#[test]\nfn ssao_config_layout() {\n    crate::test::compare_struct_layout::&lt;SsaoConfig&gt;(\n        &amp;ssao_shader(),\n        &quot;SsaoConfig&quot;,\n    );\n}\n#[test]\nfn blur_config_layout() {\n    crate::test::compare_struct_layout::&lt;BlurConfig&gt;(\n        &amp;blur_shader(),\n        &quot;BlurConfig&quot;,\n    );\n}</code></pre>\n<p>Fortunately, these tests did not discover any pre-existing issues in my configuration objects – not surprising, because everything works. Still, next time I add a new compute shader, it will be a lot easier to feel confident in the correctness of data loading!</p>\n<p>It&#39;s also easy to imagine using these kind of tests for other CPU-GPU interchange, which is mostly a moot point here (all other data types in my system are trivial), but could be relevant for other projects!</p>\n<p>As another closing thought, <a href=\"https://facet.rs\" rel=\"nofollow ugc noopener\">Facet</a> is great and people should\nuse it more.  I should probably write another blog post about how it&#39;s used\nelsewhere in Fidget; it provides the backbone for some truly horrific\nautomatically-generated bindings from Rust shape definitions to\n<a href=\"https://rhai.rs/\" rel=\"nofollow ugc noopener\">Rhai</a> functions.  If that sounds\ninteresting, feel free to subscribe via <a href=\"/atom.xml\">RSS</a> and/or follow me on\n<a href=\"https://bsky.app/profile/mattkeeter.com\" rel=\"nofollow ugc noopener\">social</a>\n<a href=\"https://hachyderm.io/@mjk\" rel=\"nofollow ugc noopener\">media</a>.</p>","headings":[{"level":2,"text":"Testing WebGPU data layouts with Facet","id":"testing-webgpu-data-layouts-with-facet"}]}}