| # Converting SPIR-V to WGSL | 
 |  | 
 | This document describes the challenges in converting SPIR-V into WGSL. | 
 |  | 
 | Note: Unless otherwise specified, the namespace for C++ code is | 
 | `tint::reader::spirv::`. | 
 |  | 
 | ## Overall flow | 
 |  | 
 | 1. Validate the SPIR-V input. | 
 |  | 
 |    The SPIR-V module (binary blob) is validated against rules for | 
 |    Vulkan 1.1, using the SPIRV-Tools validator. | 
 |  | 
 |    This allows the rest of the flow to ignore invalid inputs. | 
 |    However, the SPIR-V might still be rejected in a later step because: | 
 |  | 
 |    - it uses features unavailable in WGSL, or | 
 |    - the SPIR-V Reader is insufficiently smart, or | 
 |    - the translated program tries to do something rejected by WGSL's rules | 
 |      (which are checked by Tint's Resolver). | 
 |  | 
 | 2. Load the SPIR-V binary into an in-memory representation. | 
 |  | 
 |    The SPIR-V reader uses the in-memory representation of the SPIR-V | 
 |    module defined by the SPIRV-Tools optimizer.  That provides | 
 |    convenient representation of basic structures such as: | 
 |  | 
 |     - instructions | 
 |     - types | 
 |     - constants | 
 |     - functions | 
 |     - basic blocks | 
 |  | 
 |    and provides analyses for: | 
 |  | 
 |     - relating definitions to uses (spvtools::opt::analysis::DefUseMgr) | 
 |     - types (spvtools::opt::analysis:TypeManager) | 
 |     - constants (spvtools::opt::analysis:ConstantManager) | 
 |  | 
 |    Note: The SPIR-V is not modified by the SPIR-V Reader. | 
 |  | 
 | 3. Translate the SPIR-V module into Tint's AST. | 
 |  | 
 |    The AST is valid for WGSL except for some small exceptions which are | 
 |    cleaned up by transformations. | 
 |  | 
 | 4. Post-process the AST to make it valid for WGSL. | 
 |  | 
 |    Example: | 
 |    - Rewrite strided arrays and matrices (remove `@stride` attribute) | 
 |    - Rewrite atomic functions | 
 |    - Remove unreachable statements, to satisfy WGSL's behaviour analysis. | 
 |  | 
 |  | 
 | ## Overcoming mismatches between SPIR-V and WGSL | 
 |  | 
 | ### Remapping builtin inputs and outputs | 
 |  | 
 | SPIR-V for Vulkan models builtin inputs and outputs as variables | 
 | in Input and Output storage classes. | 
 |  | 
 | WGSL builtin inputs are parameters to the entry point, and | 
 | builtin outputs are result values of the entry point. | 
 |  | 
 | See [spirv-input-output-variables.md](spirv-input-output-variables.md) | 
 |  | 
 | ### We only care about `gl_Position` from `gl_PerVertex` | 
 |  | 
 | Glslang SPIR-V output for a vertex shader has a `gl_PerVertex` | 
 | output variable with four members: | 
 |  | 
 | - `gl_Position` | 
 | - `gl_PointSize` | 
 | - `gl_ClipDistance` | 
 | - `gl_CullDistance` | 
 |  | 
 | WGSL only supports the `position` builtin variable. | 
 |  | 
 | The SPIR-V Reader has a bunch of carveouts so it only generates the | 
 | position variable. In partcular, it tracks which expressions are actually | 
 | accesses into the per-vertex variable, and ignores accesses to other | 
 | parts of the structure, and remaps accesses of the position member. | 
 |  | 
 | ### `gl_PointSize` must be 1.0 | 
 |  | 
 | It's a WGSL rule.  SPIR-V is more flexible, and the SPIR-V Reader | 
 | checks that any assignment to (the equivalent of) `gl_PointSize` | 
 | must the constant value 1.0. | 
 |  | 
 | ### Remapping sample mask inputs and outputs | 
 |  | 
 | There's some shenanigans here I don't recall. | 
 | See the SkipReason enum. | 
 |  | 
 | ### Integer signedness | 
 |  | 
 | In SPIR-V, the instruction determines the signedness of an operation, | 
 | not the types of its operands. | 
 |  | 
 | For example: | 
 |  | 
 |     %uint = OpTypeInt 32 0 ; u32 type | 
 |     %int = OpTypeInt 32 1 ; i32 type | 
 |  | 
 |     %int_1 = OpConstant %int 1  ;  WGSL 1i | 
 |     %uint_2 = OpConstant %uint 2 ; WGSL 2u | 
 |  | 
 |     ; You can mix signs of an operand, and the instruction | 
 |     ; tells you the result type. | 
 |     %sum_uint = OpIAdd %uint %int %int_1 %uint_2 | 
 |     %sum_int = OpIAdd %int %int %int_1 %uint_2 | 
 |  | 
 | However, WGSL arithmetic tends to require the operands and | 
 | result type for an operation to all have the same signedness. | 
 |  | 
 | So the above might translate to WGSL as: | 
 |  | 
 |     let sum_uint: u32 = bitcast<u32>(1i) + 2u; | 
 |     let sum_int: i32 = 1i + bitcast<i32>(2u); | 
 |  | 
 | See: | 
 | * ParserImpl::RectifyOperandSignedness | 
 | * ParserImpl::RectifySecondOperandSignedness | 
 | * ParserImpl::RectifyForcedResultType | 
 |  | 
 | ### Translating textures and samplers | 
 |  | 
 | SPIR-V textures and samplers are module-scope variables | 
 | in UniformConstant storage class. | 
 | These map directly to WGSL variables. | 
 |  | 
 | For a sampled-image operation, SPIR-V will: | 
 | - load the image value from a texture variable | 
 | - load the sampler value from a sampler variable | 
 | - form a "sampled image" value using `SpvOpSampledImage` | 
 | - then use that sampled image value in a image operation | 
 |   such as `SpvOpImageSampleImplicitLod` | 
 |  | 
 | For an image operation that is not a sampled-image operation | 
 | (e.g. OpImageLoad or OpImageWrite), then the steps are similar | 
 | except without a sampler (clearly), and without invoking | 
 | `OpSampledImage`. | 
 |  | 
 | In contrast to the SPIR-V code pattern, the WGSL builtin requires | 
 | the texture and sampler value to be passed in as separate parameters. | 
 | Secondly, they are passed in by value, by naming the variables | 
 | themselves and relying on WGSL's "Load Rule" to pass the handle | 
 | value into the callee. | 
 |  | 
 | When the SPIR-V Reader translates a texture builtin, it traces | 
 | backward through the `OpSampledImage` operation (if any), | 
 | back through the load, and all the way back to the `OpVariable` | 
 | declaration.  It does this for both the image/texture variable and | 
 | the sampler variable (if applicable).  It then uses the names | 
 | of those variables as the corresponding arguments to the WGSL | 
 | texture builtin. | 
 |  | 
 | ### Passing textures and samplers into helper functions | 
 |  | 
 | Glslang generates SPIR-V where texture and sampler formal parameters | 
 | are as pointer-to-UniformConstant. | 
 |  | 
 | WGSL models them as passing texture and sampler values themselves, | 
 | conceptually as opaque handles.  This is similar to GLSL, but unlike | 
 | SPIR-V. | 
 |  | 
 | To support textures and samplers as arguments to user-defined functions, | 
 | we extend the tracing logic so it knows to bottom out at OpFunctionParameter. | 
 |  | 
 | Also, code that generates function declarations now understands formal | 
 | parameters declared as a pointer to uniform-constant as | 
 | well as direct image and sampler values. | 
 |  | 
 | Example GLSL compute shader: | 
 |  | 
 |     #version 450 | 
 |  | 
 |     layout(set=0,binding=0) uniform texture2D im; | 
 |     layout(set=0,binding=1) uniform sampler s; | 
 |  | 
 |     vec4 helper(texture2D imparam, sampler sparam) { | 
 |       return texture(sampler2D(imparam,sparam),vec2(0)); | 
 |     } | 
 |  | 
 |     void main() { | 
 |       vec4 v = helper(im,s); | 
 |     } | 
 |  | 
 | SPIR-V generated by Glslang (Shaderc's glslc): | 
 |  | 
 |     ; SPIR-V | 
 |     ; Version: 1.0 | 
 |     ; Generator: Google Shaderc over Glslang; 10 | 
 |     ; Bound: 32 | 
 |     ; Schema: 0 | 
 |                    OpCapability Shader | 
 |               %1 = OpExtInstImport "GLSL.std.450" | 
 |                    OpMemoryModel Logical GLSL450 | 
 |                    OpEntryPoint GLCompute %main "main" | 
 |                    OpExecutionMode %main LocalSize 1 1 1 | 
 |                    OpSource GLSL 450 | 
 |                    OpSourceExtension "GL_GOOGLE_cpp_style_line_directive" | 
 |                    OpSourceExtension "GL_GOOGLE_include_directive" | 
 |                    OpName %main "main" | 
 |                    OpName %helper_t21_p1_ "helper(t21;p1;" | 
 |                    OpName %imparam "imparam" | 
 |                    OpName %sparam "sparam" | 
 |                    OpName %v "v" | 
 |                    OpName %im "im" | 
 |                    OpName %s "s" | 
 |                    OpDecorate %im DescriptorSet 0 | 
 |                    OpDecorate %im Binding 0 | 
 |                    OpDecorate %s DescriptorSet 0 | 
 |                    OpDecorate %s Binding 1 | 
 |            %void = OpTypeVoid | 
 |               %3 = OpTypeFunction %void | 
 |           %float = OpTypeFloat 32 | 
 |               %7 = OpTypeImage %float 2D 0 0 0 1 Unknown | 
 |     %_ptr_UniformConstant_7 = OpTypePointer UniformConstant %7 | 
 |               %9 = OpTypeSampler | 
 |     %_ptr_UniformConstant_9 = OpTypePointer UniformConstant %9 | 
 |         %v4float = OpTypeVector %float 4 | 
 |              %12 = OpTypeFunction %v4float %_ptr_UniformConstant_7 %_ptr_UniformConstant_9 | 
 |              %19 = OpTypeSampledImage %7 | 
 |         %v2float = OpTypeVector %float 2 | 
 |         %float_0 = OpConstant %float 0 | 
 |              %23 = OpConstantComposite %v2float %float_0 %float_0 | 
 |     %_ptr_Function_v4float = OpTypePointer Function %v4float | 
 |              %im = OpVariable %_ptr_UniformConstant_7 UniformConstant | 
 |               %s = OpVariable %_ptr_UniformConstant_9 UniformConstant | 
 |            %main = OpFunction %void None %3 | 
 |               %5 = OpLabel | 
 |               %v = OpVariable %_ptr_Function_v4float Function | 
 |              %31 = OpFunctionCall %v4float %helper_t21_p1_ %im %s | 
 |                    OpStore %v %31 | 
 |                    OpReturn | 
 |                    OpFunctionEnd | 
 |     %helper_t21_p1_ = OpFunction %v4float None %12 | 
 |         %imparam = OpFunctionParameter %_ptr_UniformConstant_7 | 
 |          %sparam = OpFunctionParameter %_ptr_UniformConstant_9 | 
 |              %16 = OpLabel | 
 |              %17 = OpLoad %7 %imparam | 
 |              %18 = OpLoad %9 %sparam | 
 |              %20 = OpSampledImage %19 %17 %18 | 
 |              %24 = OpImageSampleExplicitLod %v4float %20 %23 Lod %float_0 | 
 |                    OpReturnValue %24 | 
 |                    OpFunctionEnd | 
 |  | 
 | What the SPIR-V Reader currently generates: | 
 |  | 
 |     @group(0) @binding(0) var im : texture_2d<f32>; | 
 |  | 
 |     @group(0) @binding(1) var s : sampler; | 
 |  | 
 |     fn helper_t21_p1_(imparam : texture_2d<f32>, sparam : sampler) -> vec4<f32> { | 
 |       let x_24 : vec4<f32> = textureSampleLevel(imparam, sparam, vec2<f32>(0.0f, 0.0f), 0.0f); | 
 |       return x_24; | 
 |     } | 
 |  | 
 |     fn main_1() { | 
 |       var v : vec4<f32>; | 
 |       let x_31 : vec4<f32> = helper_t21_p1_(im, s); | 
 |       v = x_31; | 
 |       return; | 
 |     } | 
 |  | 
 |     @compute @workgroup_size(1i, 1i, 1i) | 
 |     fn main() { | 
 |       main_1(); | 
 |     } | 
 |  | 
 | ### Dimensionality mismatch in texture builtins | 
 |  | 
 | Vulkan SPIR-V is fairly forgiving in the dimensionality | 
 | of input coordinates and result values of texturing operations. | 
 | There is some localized rewriting of values to satisfy the overloads | 
 | of WGSL's texture builtin functions. | 
 |  | 
 | ### Reconstructing structured control flow | 
 |  | 
 | This is subtle. | 
 |  | 
 | - Use structural dominance (but we didn't have the name at the time). | 
 |   See SPIR-V 1.6 Rev 2 for updated definitions. | 
 | - See the big comment at the start of reader/spirv/function.cc | 
 | - See internal presentations. | 
 |  | 
 | Basically: | 
 | * Compute a "structured order" for structurally reachable basic blocks. | 
 | * Traversing in structured order, use a stack-based algorithn to | 
 |   identify intervals of blocks corresponding to structured constructs. | 
 |   For example, loop construct, continue construct, if-selection, | 
 |   switch-selection, and case-construct. Constructs can be nested, | 
 |   hence the need for a stack.  This is akin to "drawing braces" | 
 |   around statements, to form block-statements that will appear in | 
 |   the output. This step performs some validation, which may now be | 
 |   redundant with the SPIRV-Tools validator. This is defensive | 
 |   programming, and some tests skip use of the SPIRV-Tools validator. | 
 | * Traversing in structured order, identify structured exits from the | 
 |   constructs identified in the previous step. This determines what | 
 |   control flow edges correspond to `break`, `continue`, and `return`, | 
 |   as needed. | 
 | * Traversing in structured order, generate statements for instructions. | 
 |   This uses a stack corresponding to nested constructs. The kind of | 
 |   each construct being entered or exited determines emission of control | 
 |   flow constructs (WGSL's `if`, `loop`, `continuing`, `switch`, `case`). | 
 |  | 
 | ### Preserving execution order | 
 |  | 
 | An instruction inside a SPIR-V instruction is one of: | 
 |  | 
 | - control flow: see the previous section | 
 | - combinatorial: think of this as an ALU operation, i.e. the effect | 
 |   is purely to evaluate a result value from the values of its operands. | 
 |   It has no side effects, and is not affected by external state such | 
 |   as memory or the actions of other invocations in its subgroup. | 
 |   Examples:  arithmetic, OpCopyObject | 
 | - interacts with memory or other invocations in some way. | 
 |   Examples: load, store, atomics, barriers, (subgroup operations when we | 
 |   get them) | 
 | - function calls: functions are not analyzed to see if they are pure, | 
 |   so we assume function calls are non-combinatorial. | 
 |  | 
 | To preserve execution order, all non-combinatorial instructions must | 
 | be translated as their own separate statement.  For example, an OpStore | 
 | maps to an assignment statement. | 
 |  | 
 | However, combinatorial instructions can be emitted at any point | 
 | in evaluation, provided data flow constraints are satisfied: input | 
 | values are available, and such that the resulting value is generated | 
 | in time for consumption by downstream uses. | 
 |  | 
 | The SPIR-V Reader uses a heuristic to choose when to emit combinatorial | 
 | values: | 
 | - if a combinatorial expression only has one use, *and* | 
 | - its use is in the same structured construct as its definition, *then* | 
 | - emit the expression at the place where it is consumed. | 
 |  | 
 | Otherwise, make a `let` declaration for the value. | 
 |  | 
 | Why: | 
 | - If a value has many uses, then computing it once can save effort. | 
 |   Preserve that choice if it was made by an upstream optimizing compiler. | 
 | - If a value is consumed in a different structured construct, then the | 
 |   site of its consumption may be inside a loop, and we don't want to | 
 |   sink the computation into the loop, thereby causing spurious extra | 
 |   evaluation. | 
 |  | 
 | This heuristic generates halfway-readable code, greatly reducing the | 
 | varbosity of code in the common case. | 
 |  | 
 | ### Hoisting and phis | 
 |  | 
 | SPIR-V uses SSA (static single assignment).  The key requirement is | 
 | that the definition of a value must dominate its uses. | 
 |  | 
 | WGSL uses lexical scoping. | 
 |  | 
 | It is easy enough for a human or an optimizing compiler to generate | 
 | SSA cases which do not map cleanly to a lexically scoped value. | 
 |  | 
 | Example pseudo-GLSL: | 
 |  | 
 |     void main() { | 
 |       if (cond) { | 
 |         const uint x = 1; | 
 |       } else { | 
 |         return; | 
 |       } | 
 |       const uint y = x;  // x's definition dominates this use. | 
 |     } | 
 |  | 
 | This isn't valid GLSL and its analog would not be a valid WGSL | 
 | program because x is used outside the scope of its declaration. | 
 |  | 
 | Additionally, SSA uses `phi` nodes to transmit values from predecessor | 
 | basic blocks that would otherwise not be visible (because the | 
 | parent does not dominate the consuming basic block).  An example | 
 | is sending the updated value of a loop induction variable back to | 
 | the top of the loop. | 
 |  | 
 | The SPIR-V reader handles these cases by tracking: | 
 | - where a value definition occurs | 
 | - the span of basic blocks, in structured order, where there | 
 |   are uses of the value. | 
 |  | 
 | If the uses of a value span structured contructs which are not | 
 | contained by the construct containing the definition (or | 
 | if the value is a `phi` node), then we "hoist" the value | 
 | into a variable: | 
 |  | 
 | - create a function-scope variable at the top of the structured | 
 |   construct that spans all the uses, so that all the uses | 
 |   are in scope of that variable declaration. | 
 |  | 
 | - for a non-phi: generate an assignment to that variable corresponding | 
 |   to the value definition in the original SPIR-V. | 
 |  | 
 | - for a phi: generate an assigment to that variable at the end of | 
 |   each predecessor block for that phi, assigning the value to be | 
 |   transmitted from that phi. | 
 |  | 
 | This scheme works for values which can be the stored in a variable. | 
 |  | 
 | It does not work for pointers. However, we don't think we need | 
 | to solve this case any time soon as it is uncommon or hard/impossible | 
 | to generate via standard tooling. | 
 | See https://crbug.com/tint/98 and https://crbug.com/tint/837 | 
 |  | 
 | ## Mapping types | 
 |  | 
 | SPIR-V has a recursive type system. Types are defined, given result IDs, | 
 | before any functions are defined, and before any constant values using | 
 | the corresponding types. | 
 |  | 
 | WGSL also has a recursive type system. However, except for structure types, | 
 | types are spelled inline at their uses. | 
 |  | 
 | ## Texture and sampler types | 
 |  | 
 | SPIR-V image types map to WGSL types, but the WGSL type is determined | 
 | more by usage (what operations are performed on it) than by declaration. | 
 |  | 
 | For example, Vulkan ignores the "Depth" operand of the image type | 
 | declaration (OpTypeImage). | 
 | See [16.1 Image Operations Overview](https://registry.khronos.org/vulkan/specs/1.3/html/vkspec.html#_image_operations_overview). | 
 | Instead, we must infer that a texture is a depth texture because | 
 | it is used by image instructions using a depth-reference, e.g. | 
 | OpImageSampleDrefImplicitLod vs. OpImageSampleImplicitLod. | 
 |  | 
 | Similarly, SPIR-V only has one sampler type.  The use of the | 
 | sampler determines whether it maps to a WGSL `sampler` or | 
 | `sampler_comparison` (for depth sampling). | 
 |  | 
 | The SPIR-V Reader scans uses of each texture and sampler | 
 | in the module to infer the appropriate target WGSL type. | 
 | See ParserImpl::RegisterHandleUsage | 
 |  | 
 | In Vulkan SPIR-V it is possible to use the same sampler for regular | 
 | sampling and depth-reference sampling.  In this case the SPIR-V Reader | 
 | will infer a depth texture, but then the generated program will fail WGSL | 
 | validation. | 
 |  | 
 | For example, this GLSL fragment shader: | 
 |  | 
 |     #version 450 | 
 |  | 
 |     layout(set=1,binding=0) uniform texture2D tInput; | 
 |     layout(set=1,binding=1) uniform sampler s; | 
 |  | 
 |     void main() { | 
 |       vec4 v = texture(sampler2D(tInput,s),vec2(0)); | 
 |       float f = texture(sampler2DShadow(tInput,s),vec3(0)); | 
 |     } | 
 |  | 
 | Converts to this WGSL shader: | 
 |  | 
 |     @group(1) @binding(0) var tInput : texture_depth_2d; | 
 |  | 
 |     @group(1) @binding(1) var s : sampler_comparison; | 
 |  | 
 |     fn main_1() { | 
 |       var v : vec4<f32>; | 
 |       var f : f32; | 
 |       let x_23 : vec4<f32> = vec4<f32>(textureSample(tInput, s, vec2<f32>(0.0f, 0.0f)), 0.0f, 0.0f, 0.0f); | 
 |       v = x_23; | 
 |       let x_34 : f32 = textureSampleCompare(tInput, s, vec3<f32>(0.0f, 0.0f, 0.0f).xy, vec3<f32>(0.0f, 0.0f, 0.0f).z); | 
 |       f = x_34; | 
 |       return; | 
 |     } | 
 |  | 
 |     @fragment | 
 |     fn main() { | 
 |       main_1(); | 
 |     } | 
 |  | 
 | But then this fails validation: | 
 |  | 
 |     error: no matching call to textureSample(texture_depth_2d, sampler_comparison, vec2<f32>) | 
 |     15 candidate functions: ... | 
 |  | 
 | ## References and pointers | 
 |  | 
 | SPIR-V has a pointer type. | 
 |  | 
 | A SPIR-V pointer type corresponds to a WGSL memory view. WGSL has two | 
 | memory view types: a reference type, and a pointer type. | 
 |  | 
 | See [spirv-ptr-ref.md](spirv-ptr-ref.md) for details on the translation. | 
 |  | 
 | ## Mapping buffer types | 
 |  | 
 | Vulkan SPIR-V expresses a Uniform Buffer Object (UBO), or | 
 | a WGSL 'uniform buffer' as: | 
 |  | 
 | - an OpVariable in Uniform storage class | 
 | - its pointee type (store type) is a Block-decorated structure type | 
 |  | 
 | Vulkan SPIR-V has two ways to express a Shader Storage Buffer Object (SSBO), | 
 | or a WGSL 'storage buffer' as either deprecated-style: | 
 |  | 
 | - an OpVariable in Uniform storage class | 
 | - its pointee type (store type) is a BufferBlock-decorated structure type | 
 |  | 
 | or as new-style: | 
 |  | 
 | - an OpVariable in StorageBuffer storage class | 
 | - its pointee type (store type) is a Block-decorated structure type | 
 |  | 
 | Deprecated-style storage buffer was the only option in un-extended | 
 | Vulkan 1.0. It is generated by tools that want to generate code for | 
 | the broadest reach.  This includes DXC. | 
 |  | 
 | New-style storage buffer requires the use of the `OpExtension | 
 | "SPV_KHR_storage_buffer_storage_class"` or SPIR-V 1.3 or later | 
 | (Vulkan 1.1 or later). | 
 |  | 
 | Additionally, a storage buffer in SPIR-V may be marked as NonWritable. | 
 | Perhaps surprisingly, this is typically done by marking *all* the | 
 | members of the top-level (Buffer)Block-decorated structure as NonWritable. | 
 | (This is the common translation because that's what Glslang does.) | 
 |  | 
 | Translation of uniform buffers is straightforward. | 
 |  | 
 | However, the SPIR-V Reader must support both the deprecated and the new | 
 | styles of storage buffers. | 
 |  | 
 | Additionally: | 
 | - a storage buffer with all NonWritable members is translated with `read` | 
 |   access mode. This becomes a part of its WGSL reference type (and hence | 
 |   corresponding pointer type). | 
 | - a storage buffer without all NonWritable members is translated with | 
 |   an explicit `read_write` access mode. This becomes a part of its | 
 |   WGSL reference type (and hence corresponding pointer type). | 
 |  | 
 | Note that read-only vs. read-write is a property of the pointee-type in SPIR-V, | 
 | but in WGSL it's part of the reference type (not the store type). | 
 |  | 
 | To handle this mismatch, the SPIR-V Reader has bookkeeping to map | 
 | each pointer value (inside a function) back to through to the originating | 
 | variable. This originating variable may be a buffer variable which then | 
 | tells us which address space and access mode to use for a locally-defined | 
 | pointer value. | 
 |  | 
 | Since baseline SPIR-V does not allow passing pointers to buffers into | 
 | user-defined helper functions, we don't need to handle this buffer type | 
 | remapping into function formal parameters. | 
 |  | 
 | ## Mapping OpArrayLength | 
 |  | 
 | The OpArrayLength instruction takes a pointer to the enclosing | 
 | structure (the pointee type of the storage buffer variable). | 
 |  | 
 | But the WGSL arrayLength builtin variable takes a pointer to the | 
 | member inside that structure. | 
 |  | 
 | A small local adjustment is sufficient here. |