588 lines
21 KiB
Markdown
588 lines
21 KiB
Markdown
# 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
|
|
|
|
Note: Bug https://crbug.com/tint/1039 is open to support passing
|
|
textures and samplers as function arguments.
|
|
|
|
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.
|
|
|
|
The tracing logic described in the previous section does not know
|
|
what to do when it bottoms out on a formal parameter,
|
|
e.g. OpFunctionParameter.
|
|
|
|
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 : ptr<none, void>, sparam : ptr<none, void>) -> 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();
|
|
}
|
|
|
|
with an error:
|
|
|
|
error: function parameter of pointer type cannot be in 'none' address space
|
|
|
|
Instead, the generated WGSL should have formal parameters with texture
|
|
and sampler types, rather than as pointers to them. So the generated WGSL
|
|
should look like this instead:
|
|
|
|
@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.
|