-
Notifications
You must be signed in to change notification settings - Fork 14
Features
This document overlaps https://www.o3de.org/docs/atom-guide/dev-guide/shaders/azsl/ in many regards. But unlike the o3de page, it intends to be exhaustive.
The goal of AZSLc is to transform an azsl
input with resources listed in groups (SRG) and emit an hlsl
form that DXC can comprehend.
In parallel to that, AZSLc will output reflection information about the program: like input assembly layout, computer shader thread count for API that needs it specified from CPU calls, and o3de/RHI SRG-constants management in a memory buffer to keep track of offsets. As well as register and spaces.
It lists options and rootconstants. It can alter its emission to suit alternative APIs. Mutate the source to remove multi sampled textures, etc.
It will also print warnings in case of overuse of registers or spaces for platforms with limitations (--min-descriptors
CLI), and provide various validation and advice that are independent of the prerogative of DXC.
Example: input:
ShaderResourceGroupSemantic slot1 { FrequencyId = 1; };
ShaderResourceGroup SRG : slot1
{
struct CB
{
float4 color;
};
ConstantBuffer<CB> m_uniforms;
};
float4 MainPS(float2 uv : TEXCOORD0) : SV_Target0
{
return SRG::m_uniforms.color;
}
output:
// HLSL emission by AZSL Compiler 1.8.9 Win64
struct SRG_CB
{
float4 color;
};
/* Generated code from ShaderResourceGroup SRG*/
ConstantBuffer <::SRG_CB> SRG_m_uniforms : register(b0, space0);
float4 MainPS( float2 uv :TEXCOORD0) :SV_Target0
{
return ::SRG_m_uniforms . color ;
}
You will note the transpilation of the azsl syntax to a concrete hlsl where the SRG are erased and their content flattened. Each access throughout the code is identified and mutated to the corresponding generated resource.
You'll also note the autogeneration of binding points register(b0, space0)
.
For vulkan, it will generate or pass-through, the proper attributes when necessary. Example:
rootconstant int varInt;
rootconstant float varFloat;
float4 MainPS() : SV_Target0
{
return (float4)varFloat;
}
output with CLI --namespace=vk
:
// HLSL emission by AZSL Compiler 1.8.9 Win64
int GetShaderRootConst_Root_Constants_varInt();
static const int _g_Root_Constants_varInt = GetShaderRootConst_Root_Constants_varInt();
float GetShaderRootConst_Root_Constants_varFloat();
static const float _g_Root_Constants_varFloat = GetShaderRootConst_Root_Constants_varFloat();
float4 MainPS() :SV_Target0
{
return ( float4 ) _g_Root_Constants_varFloat ;
}
struct Root_Constants
{
int varInt;
float varFloat;
};
[[vk::push_constant]]
Root_Constants rootconstantsCB;
int GetShaderRootConst_Root_Constants_varInt()
{
return ::rootconstantsCB.varInt;
}
float GetShaderRootConst_Root_Constants_varFloat()
{
return ::rootconstantsCB.varFloat;
}
You note the automatic generated struct Root_Constants
and its attribute. In case of --namespace=dx AZSLc will generate a constant buffer holder.
> o3de-azslc\bin\win_x64\Release\azslc.exe --help Amazon Shader Language Compiler Usage: D:\o3de-azslc\bin\win_x64\Release\azslc.exe [OPTIONS] [(- | FILE)] Positionals: (- | FILE) TEXT Input file (pass - to read from stdin). Options: -h,--help Print this help message and exit --version Prints version information -o TEXT Output file (writes to stdout if omitted). --unique-idx Use unique indices for all registers. e.g. b0, t0, u0, s0 becomes b0, t1, u2, s3. Use on platforms that don't differentiate registers by resource type. --cb-body Emit ConstantBuffer body rather than using . --root-sig Emit RootSignature for parameter binding in the shader. --root-const INT Maximum size in bytes of the root constants buffer. --pad-root-const Automatically append padding data to the root constant CB to keep it aligned to a 16-byte boundary. --Zpc Pack matrices in column-major order (default). Cannot be specified together with -Zpr. --Zpr Pack matrices in row-major order. Cannot be specified together with -Zpc. --pack-dx12 Pack buffers using strict DX12 packing rules. If not specified AZSLc will use relaxed packing rules. --pack-vulkan Pack buffers using strict Vulkan packing rules (Vector-relaxed std140 for uniforms and std430 for storage buffers). --pack-opengl Pack buffers using strict OpenGL packing rules (Vector-strict std140 for uniforms and std430 for storage buffers). --namespace TEXT ... Activate an attribute namespace. May be used multiple times to activate multiple namespaces. --ia Output a list of vs entries with their Input Assembler layouts *and* a list of CS entries and their numthreads. --om Output the Output Merger layout instead of the shader code. --srg Output the Shader Resource Group layout instead of the shader code. --options Output the list of available shader options for this shader. --dumpsym Dump symbols. --syntax Check syntax (no output means no complaints). --semantic Check semantics (no output means no complaints). --ast Output the abstract syntax tree. --bindingdep Output binding dependencies (what entry points access what external resources). --visitsym TEXT Output the locations of all relationships of the supplied symbol name. --full Output the shader code, IA layout, OM layout, SRG layout, the list of available shader options, and the binding dependencies. --strip-unused-srgs Strips unused SRGs. --no-ms Transforms usage of Texture2DMS/Texture2DMSArray and related functions and semantics into plain Texture2D/Texture2DArray equivalents. This is useful for allowing shader authors to easily write AZSL code that can be compiled into alternatives to work with both a multisample render pipeline and a non-MS render pipeline. --no-alignment-validation Skips checking for potential alignment issues related to differences between dxil and spirv.By default, potential alignment discrepancies will fail compilation. -d (Option of --visitsym) Visit direct references. -v (Option of --visitsym) Visit overload-set. -f (Option of --visitsym) Visit family. -r (Option of --visitsym) Visit recursively. --listpredefined Output a list of all predefined types in AZSLang. --max-spaces INT Will choose register spaces that do not extend past this limit. --W0 Suppresses all warnings. --W1 Activate severe warnings (default). --W2 Activate warnings that may be significant. --W3 Activate low-confidence diagnostic warnings. --Wx Treat activated warnings as errors. --Wx1 Treat level-1 warnings as errors. --Wx2 Treat level-2 and below warnings as errors. --Wx3 Treat level-3 and below warnings as errors. --min-descriptors TEXT Comma-separated list of limits corresponding to descriptors. Emits a warning if a count overshoots a limit. Use -1 to specify "no limit". --verbose
AZSLc utilizes AntlRv4 to parse the AZSL grammar which is a clone of HLSL with some additions.
Which means it understands context: e.g.
struct declaration, function declaration, function body, variable declaration, types, type modifiers, semantics, expressions...
It is possible to dump the parsed AST, in a lisp-like form, using --ast
CLI option.
Example:
input:
static int AA;
output:
(compilationUnit (topLevelDeclaration (variableDeclarationStatement (variableDeclaration (storageFlags (storageFlag static) ) (type (predefinedType (scalarType int) ) ) (variableDeclarators (namedVariableDeclarator AA unnamedVariableDeclarator) ) ) ;) ) )
During maintenance or debugging, or for curiosity, you can display an azsl input parsed AST in the form of a GUI tree, using launch_grun.bat
script: (don't hesitate to use your browser "open image in new tab" to get non-rescaled pixels)
You will need to have a version of the JDK installed. This is also a requirement for regenerating the parser in case of grammar changes.
AZSLc has inhouse semantic intelligence, it keeps track of scopes as it visits the AST. It will register all symbols into a symbol table, and construct a partial intermediate representation of the program, in a memory model. That model is mostly the symbol table, with each symbol being a specific C++ structure with dedicated fields. E.g. variables have a VarInfo
C++ type that holds: its identifier; its type identifier; an attempted boxed value of a folded integer constant, in case the initializer could be understood; whether it belongs to an SRG; and others. There is also FuncInfo
for functions, SRGInfo
, ClassInfo
etc.
AZSLc can also perfom lookups. During AST visit it will notice all occurrence of id-expression, which are when the programmer refers to a symbol by name. And it will lookup, what symbol it refers to. AZSLc uses the current scope, and the id-expression qualification to resolve the lookup.
However, the language doesn't support frontend namespaces (it has limited internal support: SRG are namespaces), and lookup doesn't support using namespace
or using anything
. However, lookup is able to do iterative scope creep (bind to nearest symbol = shadowing of more distant symbols), and recursive class inheritance field access (it will find base members). For functions, it is also capable or resolving what candidate is called at a call site of a function, if that function has multiple overloads (with some limitations, if it hits a limitation it will bind to an umbrella symbol called the overload-set).
The seenats is a vector that any symbol holds, and that lists all mention to itself throughout the program.
Example: input:
static const int AA = 2;
void f(int b = AA) {}
output:
Symbol '/AA':
kind: Variable
references:
- {line: 2, col: 16}
line: 1
type:
core: {name: "?int", validity: found, tclass: Scalar, underlying_scalar: int}
generic: <NA>
array dim: ""
has sampler state: no
storage: Static Const
val: 2
Symbol '/f(?int)':
kind: Function
references:
line: 2
def line: 2
must override: 0
is method: 0
is virtual: 0
return type:
core: {name: "?void", validity: found, tclass: Void, underlying_scalar: <NA>}
generic: <NA>
has overriding children:
is hiding base symbol: ''
parameters:
- name: 'b'
type:
core: {name: "?int", validity: found, tclass: Scalar, underlying_scalar: int}
generic: <NA>
Symbol '/f':
kind: OverloadSet
references:
functions:
- '/f(?int)'
Symbol '/f(?int)/b':
kind: Variable
references:
line: 2
type:
core: {name: "?int", validity: found, tclass: Scalar, underlying_scalar: int}
generic: <NA>
array dim: ""
has sampler state: no
storage:
You can note the YAML field "references" is where the seenats are listed. In the case of variable AA, one appears line 2 column 16.
AZSLc has an internal mangling scheme, to uniquify symbol names, it will store them in the type IdentifierUID
which is essentially a tainted string.
You can observe the mangling by using the __azslc_print_symbol
intrinsic.
Example:
class C
{
int i;
void f()
{
__azslc_print_symbol(i, __azslc_prtsym_fully_qualified);
__azslc_print_message("\n");
dword local;
__azslc_print_symbol(local, __azslc_prtsym_fully_qualified);
}
};
output using CLI --semantic
:
/C/i
/C/f()/local
This facility is used in the test suite, the python launcher will parse the output and try to execute predicates as printed by those. This is why you'll often see the 3 lines combo in the tests:
__azslc_print_message("@check predicate ");
__azslc_print_symbol(typeof(b.A0::a), __azslc_prtsym_least_qualified);
__azslc_print_message(" == 'int16_t'\n");
Anyway, the mangling scheme is made so that simple string splits can get a lot of work done on mangled symbols. So we change "." or "::" to "/" internally. Refer to https://github.com/o3de/o3de-azslc/blob/development/src/AzslcMangling.h for full information.
The goal is to transform ShaderResourceGroup
into a structure with a global ConstantBuffer instance for SRG-constants, and rename all accesses in code bodies to refer to the concrete global variable instead. Same concept for rootconstants, options, and view types (textures, buffers...).
Referring to the example on introduction of this page: in MainPS SRG::m_uniforms.color;
must change to ::SRG_m_uniforms.color;
The need for partial semantic understanding, a symbol table, and a type tracker, arise from "attack" vectors. Let's say, in a much simpler form of transpiler, we chose to query-replace SRG accesses and mutate them using a regexp.
struct Material
{
float4 color;
};
ShaderResourceGroupSemantic slot1 { FrequencyId = 1; };
ShaderResourceGroup SRG : slot1
{
Material mat;
};
float4 MainPS(float2 uv : TEXCOORD0) : SV_Target0
{
// decoy. may be malicious or spurious.
struct SRG
{
static Material mat;
};
return SRG::mat.color;
}
The regexp would search for SRG::mat.color
occurrences and replace with a transformed getter. In the case presented above it would cause a false positive. We could chose to force the usage of ::SRG::mat.color
but in most standard cases with no shadowing it would be unnatural. Also non-intuitive for programmers starting with AZSL which risks causing frustration and losing users. Also, SRG can host functions, access to variables within that scope needn't be qualified; for all these reasons, to make it work in that natural and robust way, a proper lookup system is needed.
Lookup may be varyingly qualified. Fully ::SRG::mat.color
, partially SRG::mat.color
or unqualified mat.color
. In AZSLc internal parlance, partially-qualified is treated as unqualified and is stored in tainted type UnqualifiedName
(
Flaws in the reference tracker got noticed case of unresolved lookup context because of lacking semantic data.
Example in an early naïve AZSLc version:
In that --dumpsym we notice that /func/c
which is the bool c
of line 30 has a reference, column 19, that is the appearance of the name c
in an id-expression at the right hand of the initializer clause. This is a mis-bind. param.c
should bind to bool c in S
declared line 6. This is because this expression is a MAE (Member Access Expression) in the form of (lhs)expr dot (rhs)id-expr
. The right hand side id-expr has a different starting scope for lookup, and that is the context of its left hand side. Members must be looked-up in the scope of their types since they must be fields of classes. Knowing that this sort of expression may appear: MyFunc().a
, it is not enough to treat "(lhs)expr" as a reference to a type directly, since it might be any expression: we need a system that can extract a type from an expression. That is a essentially a typeof()
feature.
Illustration:
With that done, we could fix the above case:
Having a workable reference tracker is essential for a working renaming system. Because during transpilation, SRG are flatten out of existence, so each reference to any SRG content gets impacted all over the code. SRG may contain variables, user-defined-types and functions. So enums, struct, with nested structs, classes, with methods, and all possible combinations.
In the example in introduction, struct CB
is mutated into SRG_CB
at global scope.
Since SRG are flattened, all these symbols are no longer protected by the namespace that the SRG constitutes. In the case of SRG_CB
there could have been a symbol existing with this name already, to avoid spurious collisions AZSLc detects and palliate those cases.
Example:
ShaderResourceGroupSemantic s1 { FrequencyId = 1; };
void SRG_CB(){}; // attack
ShaderResourceGroup SRG : s1
{
struct CB
{
float4 color;
};
ConstantBuffer<CB> m_uniforms;
};
Transpiles to:
void SRG_CB(){}
struct SRG_CB1
{
float4 color;
};
ConstantBuffer <::SRG_CB1> SRG_m_uniforms : register(b0, space0);
When we flatten during transpilation, it's not headache free, here is a demonstration of how slang does it: In AZSLc, to respect that pattern, there is a constraint solver. We use a pass where we establish dependency links between symbols. Source: https://github.com/o3de/o3de-azslc/blob/727f031231c609f5a77af540a4e8d0efae203163/src/AzslcSymbolAggregator.cpp#L246
If you recall MyFunc().a
which served as an example in an previous paragraph, we deduced that we needed typeof()
as a feature to detect the type of expr
in expr dot id-expr
. In the case of functions, it's their return type that will give us the typeof(expr)
. If we have only one function, the return type is registered in the FunctionInfo
of the symbol table. However, we also have inheritance, from class
and interface
which will allow for covariant return type. And we have overloading, which also accepts differing return types. In AZSLc we call the later heterogeneous return types.
In a word, to be able to determine the return type of MyFunc()
in MyFunc().a
we need to take into account the call-expression and do overload resolution. To do so, we use a metal symbol of kind OverloadSetInfo
which stores all the functions participating in an overload (only in the same scope in AZSL. There is no such thing as ADL in AZSL. That is one of the reasons namespaces are not supported).
The method SemanticOrchestrator::ResolveOverload
does the search and match. We can have failures in cases of unregistered types being used, especially all of the fundamental types (Texture2D, matrix<,,>, etc.) which have not been described by hardcode, therefore AZSLc treats typeof resolution failures silent and attempt to cope. Normally we only need to perform transpilation on SRG content, and fundamental types should not be able to have paths to SRG user content, so the codebase made the economy of their description. One exception is Buffer<SRG::CB>.Get(..)
type of construct. The fundamental type Buffer<>
being template, its member functions may return types that needs transpiling action (rename/flatten). It's an open flaw of AZSLc today.
Overrides are not as complicated since their return type is determined by the type of the object the method is called from.
During emission, we call the method CodeEmitter::MigrateASTSubTree()
on all SRG members, which will in turn register a rename using SymbolTranslation::RegisterLandingScope()
. That will act as a filter during emission to mutate the name and declaration position of transpiled symbols.
When we do RegisterLandingScope, we swipe all the symbol family that is required to be rename. So if there are overloads, they all get a registration. For this, we use the homonym visitor. It's a facility in the class HomonymVisitor
of AzslcHomonymVisitor.h
file that abstracts away the seenat
vector. It is more powerful to be certain not to leave out symbols that are related by name, but not by seenats.
Illustration:
This facility is accessible by command line.
Illustration:
AZSLc allows enough reflection so that the runtime can manage PSO/rootsig/descriptors/buffer boilerplate, for all the supported platforms without the need for any hardcode on the shader programmer end, and enough to abstract away setting of uniforms (SRG constants) into memory managed buffers by update frequency, without hardship to find their address.
The SRG reflection CLI option (or --full which includes it) gives data to the runtime in a json form about all external resources.
For instance
ShaderResourceGroup SRG : slot1
{
struct CB
{
float4 color;
};
ConstantBuffer<CB> m_uniforms;
float4 intensityScale;
};
Built with --srg
passed, gives:
"ShaderResourceGroups" :
[
{
"bindingSlot" : 1,
"bufferForSRGConstants" :
{
"count" : 1,
"id" : "SRG",
"index" : 0,
"index-merged" : 0,
"space" : 0,
"space-merged" : 0,
"usage" : "Read"
},
"id" : "SRG",
"inputsForBufferViews" :
[
{
"count" : 1,
"id" : "m_uniforms",
"index" : 1,
"index-merged" : 1,
"space" : 0,
"space-merged" : 0,
"stride" : 16,
"type" : "ConstantBuffer<CB>",
"usage" : "Read"
}
],
"inputsForImageViews" : [],
"inputsForSRGConstants" :
[
{
"constantByteOffset" : 0,
"constantByteSize" : 16,
"constantId" : "intensityScale",
"qualifiedName" : "/SRG/intensityScale",
"typeDimensions" : [],
"typeKind" : "Predefined",
"typeName" : "?float4"
}
],
"inputsForSamplers" : [],
}
In o3de, the runtime can use this reflection data to prepare the buffers bound to the shader. The builders will populate and seal a AZ::RHI::ShaderResourceGroupLayout
type at asset processing time. Later used by, e.g. AZ::DX12's RHI implementation PipelineLayout::Init
.
AZSLc will abstract access to options variable through a replacement by a function call that fetches its value from a few bits in the variant key, which itself is a uint4[]
in a constant buffer. Or through a preprocessor constant.
Example:
[range(1, 64)]
option int IntOption = 42;
Becomes:
int GetShaderVariantKey_IntOption();
#if defined(IntOption_OPTION_DEF)
static const int IntOption = IntOption_OPTION_DEF ;
#else
static const int IntOption = GetShaderVariantKey_IntOption();
#endif
// ...
int GetShaderVariantKey_IntOption()
{
uint shaderKey = (::ExampleSRG_SRGConstantBuffer.ExampleSRG_m_SHADER_VARIANT_KEY_NAME_[0].x >> 2) & 63;
return (int) shaderKey;
}
This input:
rootconstant float4 varFloat4;
rootconstant float3x3 mat3x3;
Will reflect into:
"bufferForRootConstants" : {
"count" : 1,
"id" : "Root_Constants",
"index" : 0,
"index-merged" : 0,
"sizeInBytes" : 60,
"space" : 0,
"space-merged" : 0,
"usage" : "Read"
},
"inputsForRootConstants" : [ {
"constantByteOffset" : 0,
"constantByteSize" : 16,
"qualifiedName" : "/Root_Constants/varFloat4",
"typeKind" : "Predefined",
"typeName" : "?float4"
},{
"constantByteOffset" : 16,
"constantByteSize" : 44,
"qualifiedName" : "/Root_Constants/mat3x3",
"typeKind" : "Predefined",
"typeName" : "?float3x3"
} ]
Since AZSLc has a good understanding of the program, it is capable to explore symbol usage and find out if resources are unused, and clean the resource and its reflection, for graphics API that do not tolerate declared but unused resources.
Internal representation of the action of CLI --strip-unused-srgs
AZSLc has a facility to forcefully remove support for multisample texture Texture2DMS
and transform them to Texture2D
. A supervariant with no MS can be generated from the same source.
Making the shader program buildable after such a change requires adaptation of SV_SampleIndex
, SV_Coverage
semantics where they appear, of Load
, GetSamplePosition
, GetDimensions
function call where they appear.
The mutation can take the form of function parameters being pulled out into local variables:
PSOutput mainPS(VSOutput IN, in uint sampleIndex : SV_SampleIndex) { ...
into:
PSOutput mainPS(VSOutput IN) { uint sampleIndex = 0; ...
More information by referring to the feature test https://github.com/o3de/o3de-azslc/blob/development/tests/Advanced/texture2DMS-to-texture2D.azsl
AZSLc tracks the currently emitted line count and if a discrepancy is observed with the original source file line for the currently emitted symbol, it will readjust, example:
#line 49 "C:/Users/siliconvoodoo/AppData/Local/Temp/tmp-297685u6hDk09wCiZ"
float2 PassSrg_GetSamplePosition( int sampleIndex )
{
return float2(0, 0)+ float2(0, 0)+ float2(0, 0);
}
With CLI option --Zpc/--Zpr matrices which are resources (extern) will be decorated with a qualifier.
Example:
ShaderResourceGroupSemantic slot1{ FrequencyId = 1; };
rootconstant float3x3 mat3x3;
ShaderResourceGroup SRG : slot1
{
float4x4 mat4x4;
};
float4 MainPS() : SV_Target0
{
float2x2 local;
return 0;
}
Built with --Zpr
becomes:
float3x3 GetShaderRootConst_Root_Constants_mat3x3();
static const float3x3 _g_Root_Constants_mat3x3 = GetShaderRootConst_Root_Constants_mat3x3();
struct SRG_SRGConstantsStruct
{
row_major float4x4 SRG_mat4x4;
};
ConstantBuffer<::SRG_SRGConstantsStruct> SRG_SRGConstantBuffer : register(b0, space0);
float4 MainPS() :SV_Target0
{
float2x2 local ;
return 0 ;
}
struct Root_Constants
{
row_major float3x3 mat3x3;
};
ConstantBuffer<::Root_Constants> rootconstantsCB : register(b0, space1);
float3x3 GetShaderRootConst_Root_Constants_mat3x3()
{
return ::rootconstantsCB.mat3x3;
}
In certain situations it can be desired to pass snippets of code that AZSL can't tolerate as valid entry syntax, but are nonetheless necessary. In that case, one may use the verbatim
attribute.
Example:
ShaderResourceGroupSemantic slot1{ FrequencyId = 1; };
ShaderResourceGroup SRG : slot1
{
[[verbatim("globallycoherent")]]
float3 var;
};
This is no longer needed since 1.8.8 now accepts globallycoherent
as a keyword, but you may encounter a similar situation that this construct can perhaps lift you out of.
The attribute can be activated conditionally to the command line option by using a namespace name that acts as a filter.
Example, as input:
void f();
[[dx::verbatim("#define PLATFORM DX")]]
[[vk::verbatim("#define PLATFORM VK")]]
static bool b;
compile with CLI --namespace=dx
results:
void f();
#define PLATFORM DX
static bool b;
Using the global reserved name as an attribute namespace will emit all the content at the beginning of the output. eg:
static bool b = false;
void f(){}
[[global::random_attr("stuff")]]
[[global::output_format(0, "FMT_32_R")]]
[[global::verbatim("#include \"header.azsli\"")]]
Results in
[[random_attr("stuff")]]
#pragma OutputFormatHint(target 0 FMT_32_R)
#include "header.azsli"
static bool b = false ;
void f(){}
AZSLc can reflect on resources usage by functions (that appears to be entry points). The runtime can later create descriptors for the strict set that is active on that entry, for APIs that are sensitive about unused resources that like metal.
CLI --bindingdep extract:
"SRG_SRGConstantBuffer" :
{
"binding" : { "count" : 1, "index" : 0, "index-merged" : 0, "space" : 0, "space-merged" : 0, "type" : "SrgConstantCB" },
"dependentFunctions" : [ "MainPS" ],
"participantConstants" : [ "color" ]
},
"uniforms" :
{
"binding" : { "count" : 1, "index" : 1, "index-merged" : 1, "space" : 0, "space-merged" : 0, "type" : "CBV" },
"dependentFunctions" : [ "MainPS" ]
}
By using an attribute, AZSLc will recognize, compute and insert proper filling variables for resource buffers with no surpises.
Example:
struct MyStructB
{
int m_data;
[[pad_to(16)]]
MyStructA m_a;
float2 m_b;
[[pad_to(64)]]
};
Will emit:
struct MyStructB
{
int m_data;
float3 __pad_at4;
::MyStructA m_a;
float2 m_b;
float2 __pad_at40;
float4 __pad_at48[1];
};
For subpass support on tiled memory hardware, AZSLc can help generate compatible code.
Example:
ShaderResourceGroup SRG : slot1
{
[[input_attachment_index(0)]]
SubpassInput<float4> m_sub;
}
Result with CLI --namespace=vk
#if !defined(AZ_USE_SUBPASSINPUT)
class SubpassInputStub
{
float4 SubpassLoad(){return (float4)0;}
};
class SubpassInputStubMS
{
float4 SubpassLoad(int sampleIndex){return (float4)0;}
};
#define SubpassInput SubpassInputStub
#define SubpassInputMS SubpassInputStubMS
#endif
#ifdef AZ_USE_SUBPASSINPUT
[[vk::input_attachment_index(0)]]
[[vk::binding(0, 0)]]
#else
static
#endif
SubpassInput SRG_m_sub;
AZSLc can assist with detection of common problems with resources, such as this case:
ShaderResourceGroupSemantic ExampleBinding { FrequencyId = 0; };
ShaderResourceGroup ExampleSRG : ExampleBinding
{
float3x3 mat;
bool b;
};
Which results in:
error #131: Detected potential alignment issues related with DXC flag '-fvk-use-dx-layout'.
- A 'float2' variable should be added before the variable 'b' in 'ShaderResourceGroup /ExampleSRG/b' at Line number 5
AZSL has these 3 operators: typedef
, typeof
and typealias
.
typedef
has a weird limitation in DXC and FXC in that it can't be used in scopes (struct, class...).
AZSLc therefore migrates them to global scope to let you use them anywhere transparently. Types also undergo this migration because it was perceived that that may be useful to help parsing by lesser tools just in case.
Example:
void Func()
{
typedef float Real;
struct S
{
int m;
typedef int Integer;
};
Real r,g,b;
S::Integer i;
}
results in HLSL output:
typedef float Func_vd_Real;
typedef int Func_vd_S_Integer;
struct Func_vd_S
{
int m;
};
void Func()
{
::Func_vd_Real r, g, b ;
::Func_vd_S_Integer i ;
}
typealias is just a swift way to use typedef, much like the using
operator of C++. It works as such:
void Func()
{
typealias Real = float;
typealias Real2 = Real;
Real r,g,b;
}
results in:
typedef float Func_vd_Real;
typedef float Func_vd_Real2;
void Func()
{
::Func_vd_Real r, g, b ;
}
Note that Real2 wasn't defined in term of Real, but with the directly resolved float
. This is just an implementation choice.
This operator comes from a gcc extension but is not meant to be officially supported in the front end. It is more a facility that has limited but sufficient support for the internal mechanisms of reference tracking. It is exposed to the front end for purpose of the test suite though.
You may attempt to use it in certain situations, but it is not guaranteed to always transpile correctly. Notably it has no token mutator in the body of functions. But it could work in UDT definitions. Example:
struct Input
{
float2 uv;
typeof(uv) uv2;
};
result:
struct Input
{
float2 uv;
float2 uv2;
};
Here is the most wide-angle view of the design:
It's not a feature, but an implementation detail. Most of what we call the intermediate representation is the symbol table. But we have two of them.
- Fixed (stores predefined types like:
float
,Texture2D
...) - Elastic (stores symbols present in the input source: functions, variables, user-defined-types...)
Their are both stored in the SymbolAggregator
which exists just to abstract the access to those two instances of SymbolTable
. When doing lookup, the elastic table is prioritized, and the fixed table used as a fallback. This allows to find types that don't have a declaration in the source, like float
. We call them, predefined types, and they are defined as being "all HLSL intrinsic types". They are different than fundamental types which are "void or arithmetic". You can refer to the full classification for AZSL in the AzslcTypes.h source: https://github.com/o3de/o3de-azslc/blob/development/src/AzslcTypes.h
This classification tries to respect Howard Hinnant type classification chart for C++:
The population of the fixed table is done at startup, using an autogenerated hardcoded source: https://github.com/o3de/o3de-azslc/blob/development/src/AzslcPlatformEmitter.h
Which is generated by a bootstrap build of azslc.exe using --listpredefined
CLI option. Here is the workflow picture:
AZSLc doesn't support C-preprocessor natively. The only syntax it can tolerate is the #line directive. All other, define
, include
and if
, if you use them, must be treated prior to input to AZSLc. In o3de this is performed in the asset builder stage by MCPP library in source https://github.com/o3de/o3de/blob/development/Gems/Atom/Asset/Shader/Code/Source/Editor/CommonFiles/Preprocessor.cpp