A backend that converts an IR shader module into a self-contained Slang (.slang) shader source file. It prepares globals, parameters, builtins and structs, then emits helpers, interfaces, functions and entry points with source mapping and optional preview instrumentation.
using System.Globalization;
using System.Text;
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;
namespace Editor.Prism.Compiler.Backends;
/// <summary>
/// Turns an <see cref="IrModule"/> into a self-contained, idiomatic Slang module.
/// <para>
/// The artifact is a real, portable <c>.slang</c> file: it pins the language version, declares a
/// module, imports the emitted <c>prism.core</c> prelude, groups every shader parameter into one
/// <c>ParameterBlock</c>, and exposes <c>[shader("vertex")]</c> / <c>[shader("pixel")]</c> /
/// <c>[shader("compute")]</c> entry points. It compiles with nothing but <c>slangc</c> and an include
/// path, which is exactly what makes it useful outside s&box.
/// </para>
/// <para>
/// Two rules are structural rather than stylistic. <c>?:</c> is never emitted with a vector condition
/// and <c>&&</c> / <c>||</c> never appear on a vector, because neither is component-wise;
/// <see cref="SlangIntrinsics"/> owns both decisions so this file cannot break them by accident.
/// </para>
/// </summary>
public sealed class SlangBackend : IShaderBackend
{
/// <inheritdoc/>
public string Id => PrismConstants.BackendSlang;
/// <inheritdoc/>
public string DisplayName => "Slang";
/// <inheritdoc/>
public string FileExtension => PrismConstants.SlangExtension;
/// <inheritdoc/>
public BackendCapabilities Capabilities => BackendCapabilities.Slang;
/// <summary>
/// Emit the module. Never throws: a malformed module, a helper with no Slang body or an
/// unexpected fault all come back as diagnostics plus an empty result.
/// </summary>
public BackendEmitResult Emit( IrModule module, BackendEmitOptions options, DiagnosticSink diagnostics )
{
var sink = diagnostics ?? new DiagnosticSink();
var emitOptions = options ?? BackendEmitOptions.Default;
if ( module is null )
{
sink.Error( DiagnosticCode.InvalidBlock, "The Slang backend was given no module to emit." );
return BackendEmitResult.Empty( Id, FileExtension );
}
try
{
if ( !PreviewInstrumentation.IsEnabled( emitOptions.Mode ) )
{
return new Emitter( this, module, emitOptions, sink ).Run();
}
// A node's stage id is its rank among the nodes appearing in the finished module's source
// map, so the module has to be written once before the numbers it bakes in can be known.
// The throw-away pass reports into a sink nobody reads, because the real pass reports the
// same diagnostics.
var probe = new Emitter( this, module, emitOptions, new DiagnosticSink() );
probe.Run();
return new Emitter( this, module, emitOptions, sink )
{
StageIds = PreviewInstrumentation.BuildStageMap( probe.SourceMap )
}.Run();
}
catch ( Exception e )
{
PrismLog.Error( e, "Slang emission failed" );
sink.Error( DiagnosticCode.InvalidBlock,
"Slang emission failed and produced no artifact.", null, e.Message );
return BackendEmitResult.Empty( Id, FileExtension );
}
}
// =========================================================================
// Writer
// =========================================================================
/// <summary>A line-counting, indenting text writer. Every line it writes can be source-mapped.</summary>
sealed class Writer
{
readonly StringBuilder _text = new();
readonly string _indent;
readonly string _newLine;
int _depth;
int _line = 1;
bool _lastWasBlank = true;
public Writer( string indent, string newLine )
{
_indent = indent ?? "\t";
_newLine = string.IsNullOrEmpty( newLine ) ? "\r\n" : newLine;
}
/// <summary>The line the next call to <see cref="Line"/> will land on.</summary>
public int NextLine => _line;
/// <summary>Number of lines written so far.</summary>
public int LineCount => _line - 1;
public void Push() => _depth++;
public void Pop() => _depth = Math.Max( 0, _depth - 1 );
/// <summary>Write one line at the current indent and return the line number it landed on.</summary>
public int Line( string text = null )
{
var at = _line;
if ( !string.IsNullOrEmpty( text ) )
{
for ( int i = 0; i < _depth; i++ ) _text.Append( _indent );
_text.Append( text );
_lastWasBlank = false;
}
else
{
_lastWasBlank = true;
}
_text.Append( _newLine );
_line++;
return at;
}
/// <summary>Write a blank line, collapsing runs of them so the output never double-spaces.</summary>
public void Blank()
{
if ( _lastWasBlank ) return;
Line();
}
/// <summary>
/// Write a block of pre-formatted text — a helper body — re-indenting it to the current depth
/// and counting every line so the source map stays accurate.
/// </summary>
public void Verbatim( string text )
{
if ( string.IsNullOrEmpty( text ) ) return;
var normalised = text.Replace( "\r\n", "\n" ).Replace( '\r', '\n' );
foreach ( var raw in normalised.Split( '\n' ) )
{
Line( raw.TrimEnd() );
}
}
public override string ToString() => _text.ToString();
}
// =========================================================================
// Emitter
// =========================================================================
/// <summary>One emission of one module. Single use.</summary>
sealed class Emitter
{
/// <summary>One field of a generated interface struct.</summary>
sealed class InterfaceField
{
public string Name;
public ShaderType Type;
public string Semantic;
public IrInterpolation Interpolation;
public Builtin Source;
public NodeId Origin;
}
/// <summary>One field of the generated parameter block.</summary>
sealed class ParameterField
{
public GlobalDecl Decl;
public string Name;
}
const int PrecPrimary = 100;
const int PrecUnary = 90;
readonly SlangBackend _backend;
readonly IrModule _module;
readonly BackendEmitOptions _options;
readonly DiagnosticSink _sink;
readonly Writer _w;
readonly SourceMap _map = new();
readonly Dictionary<string, string> _globalPath = new( StringComparer.Ordinal );
readonly Dictionary<string, string> _structRename = new( StringComparer.Ordinal );
readonly List<GlobalDecl> _constants = new();
readonly List<ParameterField> _parameters = new();
readonly List<InterfaceField> _vsIn = new();
readonly List<InterfaceField> _vsOut = new();
readonly List<Builtin> _vertexBuiltins = new();
readonly List<Builtin> _pixelBuiltins = new();
readonly List<Builtin> _computeBuiltins = new();
readonly List<IrStruct> _userStructs = new();
readonly HashSet<string> _reportedHelpers = new( StringComparer.Ordinal );
IrFunction _vertex;
IrFunction _pixel;
IrFunction _geometry;
IrFunction _compute;
string _moduleName = "prism_shader";
string _paramsStruct = "PrismParams";
string _paramsBlock = "gParams";
bool _needsFrontFace;
ShaderStage _stage = ShaderStage.None;
// The VsOut local the vertex entry point is currently building, and whether that entry point
// displaces the vertex. WP-2 writes both interpolants and the position offset through one
// synthetic "PixelInput i" struct, which is an s&box block-file convention with no meaning in a
// standalone Slang module — these two let those writes be redirected to what Slang actually has.
string _vertexOutput;
bool _vertexMovesPosition;
public Emitter( SlangBackend backend, IrModule module, BackendEmitOptions options, DiagnosticSink sink )
{
_backend = backend;
_module = module;
_options = options;
_sink = sink;
_w = new Writer( options.Indent, options.NewLine );
}
/// <summary>The map this emitter built, whether or not the options asked for one to be shipped.</summary>
public SourceMap SourceMap => _map;
/// <summary>
/// Per-node preview stage ids. Non-null only on the real pass of an instrumented build, so it
/// doubles as the flag that keeps the numbering pass from instrumenting itself.
/// </summary>
public IReadOnlyDictionary<NodeId, int> StageIds { get; init; }
public BackendEmitResult Run()
{
Prepare();
WriteBanner();
WriteCombos();
WriteConstants();
WriteInstrumentation();
WriteParameters();
WriteUserStructs();
WriteHelpers();
WriteInterfaces();
WriteFunctions();
WriteEntryPoints();
var text = _w.ToString();
_map.File = $"{_moduleName}.{PrismConstants.SlangExtension}";
var extra = new List<GeneratedArtifact> { SlangRuntimeModule.Artifact( _options.NewLine ) };
return new BackendEmitResult( text, PrismConstants.SlangExtension,
_options.EmitSourceMap ? _map : new SourceMap(), extra )
{
BackendId = _backend.Id,
LineCount = _w.LineCount
};
}
// ---- preparation ---------------------------------------------------
void Prepare()
{
var name = string.IsNullOrWhiteSpace( _options.OutputName ) ? _module.Meta.Name : _options.OutputName;
_moduleName = SlangIntrinsics.SanitizeIdentifier( name, "prism_shader" );
_paramsStruct = SlangIntrinsics.PascalCase( _moduleName ) + "Params";
_vertex = _module.EntryPoint( ShaderStage.Vertex );
_pixel = _module.EntryPoint( ShaderStage.Pixel );
_geometry = _module.EntryPoint( ShaderStage.Geometry );
_compute = _module.EntryPoint( ShaderStage.Compute );
PrepareGlobals();
PrepareStructNames();
PrepareBuiltins();
PrepareVertexInput();
PrepareVertexOutput();
PrepareUserStructs();
}
void PrepareGlobals()
{
var used = new HashSet<string>( StringComparer.Ordinal ) { _paramsBlock, _paramsStruct };
var declared = new List<GlobalDecl>();
foreach ( var global in _module.Globals )
{
if ( global is null || string.IsNullOrWhiteSpace( global.Name ) ) continue;
if ( _globalPath.ContainsKey( global.Name ) ) continue;
declared.Add( global );
AddGlobal( global, used );
}
// A global referenced by an expression but missing from the module list would otherwise emit
// an undeclared identifier. Adopt it rather than producing text that cannot compile.
foreach ( var expr in AllExpressions() )
{
if ( expr is not IrGlobalRef reference ) continue;
if ( reference.Decl is null || string.IsNullOrWhiteSpace( reference.Decl.Name ) ) continue;
if ( _globalPath.ContainsKey( reference.Decl.Name ) ) continue;
declared.Add( reference.Decl );
AddGlobal( reference.Decl, used );
_sink.Warn( DiagnosticCode.GlobalCollision,
$"'{reference.Decl.Name}' is used but was not declared in the module; the Slang backend declared it." );
}
// A texture may name a sampler that nothing declared separately. Declare it beside the texture.
foreach ( var global in declared )
{
if ( global.Kind != GlobalKind.Texture ) continue;
if ( string.IsNullOrWhiteSpace( global.SamplerName ) ) continue;
if ( _globalPath.ContainsKey( global.SamplerName ) ) continue;
AddGlobal( new GlobalDecl( global.SamplerName, ShaderType.Sampler, GlobalKind.Sampler ), used );
}
// Resources first, then numeric uniforms: the shape a reader expects of a parameter block.
// OrderBy is a stable sort, which keeps emission byte-identical across runs.
var ordered = _parameters.OrderBy( x => GroupOf( x.Decl ) ).ToList();
_parameters.Clear();
_parameters.AddRange( ordered );
}
void AddGlobal( GlobalDecl global, HashSet<string> used )
{
if ( global.Kind == GlobalKind.Constant )
{
var constant = Unique( SlangIntrinsics.SanitizeIdentifier( global.Name, "kConstant" ), used );
_constants.Add( global with { Name = constant } );
_globalPath[global.Name] = constant;
return;
}
var preferred = SlangIntrinsics.PascalCase( global.Name );
// A sampler usually shares its texture's name once the hungarian prefix is gone.
// "BaseColorSampler" reads far better than "BaseColor2".
if ( global.Kind == GlobalKind.Sampler && used.Contains( preferred ) ) preferred += "Sampler";
var field = Unique( preferred, used );
_parameters.Add( new ParameterField { Decl = global, Name = field } );
_globalPath[global.Name] = $"{_paramsBlock}.{field}";
}
static int GroupOf( GlobalDecl global ) => global.Kind switch
{
GlobalKind.Texture or GlobalKind.RwTexture => 0,
GlobalKind.Sampler => 1,
GlobalKind.Buffer or GlobalKind.RwBuffer => 2,
_ => 3
};
static string Unique( string name, HashSet<string> used )
{
if ( used.Add( name ) ) return name;
for ( int i = 2; i < 1000; i++ )
{
var candidate = name + i.ToString( CultureInfo.InvariantCulture );
if ( used.Add( candidate ) ) return candidate;
}
return name + "_x";
}
void PrepareStructNames()
{
var inputName = StructParameterName( _vertex );
var outputName = _vertex?.ReturnStruct;
if ( string.IsNullOrWhiteSpace( outputName ) && _vertex is not null && _vertex.ReturnType.IsStruct )
{
outputName = _vertex.ReturnType.StructName;
}
outputName ??= StructParameterName( _pixel );
if ( !string.IsNullOrWhiteSpace( inputName ) )
{
_structRename[inputName] = SlangIntrinsics.VertexInputStruct;
}
if ( !string.IsNullOrWhiteSpace( outputName ) )
{
_structRename[outputName] = SlangIntrinsics.VertexOutputStruct;
}
}
static string StructParameterName( IrFunction function )
{
if ( function is null ) return null;
foreach ( var parameter in function.Parameters )
{
if ( parameter.Type.IsStruct && !string.IsNullOrEmpty( parameter.Type.StructName ) )
{
return parameter.Type.StructName;
}
}
return null;
}
void PrepareBuiltins()
{
CollectBuiltins( _pixel, _pixelBuiltins, ShaderStage.Pixel );
CollectBuiltins( _compute, _computeBuiltins, ShaderStage.Compute );
var vertex = new HashSet<Builtin>();
foreach ( var id in ReferencedBuiltins( _vertex ) ) vertex.Add( id );
// Whatever else happens, the vertex program has to produce a clip-space position, and it has
// to produce every interpolant the pixel program is going to read back — but only the ones
// its own body did not already write, so the prologue never binds a value nothing reads.
var output = VertexOutputVariable( _vertex );
var assigned = _vertex is not null && output is not null
? AssignedFields( _vertex.Body, output )
: new HashSet<string>( StringComparer.Ordinal );
if ( !assigned.Contains( "Position" ) ) vertex.Add( Builtin.ClipPosition );
// A graph that displaces the vertex writes through the world position and then re-derives clip
// space from it, so both locals must exist even when nothing in the body reads them as builtins.
if ( _vertex is not null && MovesWorldPosition( _vertex.Body ) )
{
vertex.Add( Builtin.WorldPosition );
vertex.Add( Builtin.ClipPosition );
}
foreach ( var id in _pixelBuiltins )
{
var field = SlangIntrinsics.InterpolantField( id );
if ( field is null || assigned.Contains( field ) ) continue;
vertex.Add( id );
}
Close( vertex, ShaderStage.Vertex );
_vertexBuiltins.AddRange( vertex.OrderBy( x => (int)x ) );
_needsFrontFace = _pixelBuiltins.Contains( Builtin.IsFrontFace );
}
void CollectBuiltins( IrFunction function, List<Builtin> into, ShaderStage stage )
{
var set = new HashSet<Builtin>();
foreach ( var id in ReferencedBuiltins( function ) ) set.Add( id );
Close( set, stage );
into.AddRange( set.OrderBy( x => (int)x ) );
}
void Close( HashSet<Builtin> set, ShaderStage stage )
{
// Enum order already satisfies every dependency edge, so one pass over a snapshot is enough
// for the two-level chains the table actually contains, and a second pass covers the rest.
for ( int pass = 0; pass < 3; pass++ )
{
var added = false;
foreach ( var id in set.ToList() )
{
foreach ( var dependency in SlangIntrinsics.Dependencies( id, stage ) )
{
if ( set.Add( dependency ) ) added = true;
}
}
if ( !added ) break;
}
}
static IEnumerable<Builtin> ReferencedBuiltins( IrFunction function )
{
if ( function is null ) yield break;
foreach ( var expr in Expressions( function.Body ) )
{
if ( expr is IrBuiltinRef reference && reference.Id != Builtin.None )
{
yield return reference.Id;
}
}
}
void PrepareVertexInput()
{
if ( _module.Meta.Domain == ShaderDomain.PostProcess )
{
_vsIn.Add( Field( "VertexId", ShaderType.UInt, "SV_VertexID" ) );
}
else
{
_vsIn.Add( Field( "Position", ShaderType.Float3, "POSITION" ) );
_vsIn.Add( Field( "Normal", ShaderType.Float3, "NORMAL" ) );
_vsIn.Add( Field( "Tangent", ShaderType.Float4, "TANGENT" ) );
_vsIn.Add( Field( "Uv", ShaderType.Float2, "TEXCOORD0" ) );
_vsIn.Add( Field( "Uv2", ShaderType.Float2, "TEXCOORD1" ) );
_vsIn.Add( Field( "Color", ShaderType.Float4, "COLOR0" ) );
_vsIn.Add( Field( "VertexId", ShaderType.UInt, "SV_VertexID" ) );
_vsIn.Add( Field( "InstanceId", ShaderType.UInt, "SV_InstanceID" ) );
}
MergeStructFields( StructParameterName( _vertex ), _vsIn, "TEXCOORD", 8 );
}
void PrepareVertexOutput()
{
_vsOut.Add( Field( "Position", ShaderType.Float4, "SV_Position" ) );
foreach ( var id in _pixelBuiltins )
{
var name = SlangIntrinsics.InterpolantField( id );
if ( name is null ) continue;
if ( _vsOut.Any( x => x.Name == name ) ) continue;
_vsOut.Add( new InterfaceField
{
Name = name,
Type = Builtins.TypeOf( id ),
Interpolation = SlangIntrinsics.InterpolantMode( id ),
Source = id
} );
}
var outputName = _vertex?.ReturnStruct;
if ( string.IsNullOrWhiteSpace( outputName ) ) outputName = StructParameterName( _pixel );
MergeStructFields( outputName, _vsOut, "TEXCOORD", 0 );
foreach ( var varying in _module.Varyings )
{
if ( varying is null ) continue;
var name = SlangIntrinsics.FieldName( varying.Name );
if ( string.IsNullOrEmpty( name ) ) continue;
if ( _vsOut.Any( x => x.Name == name ) ) continue;
var slot = PrismConstants.FirstFreeTexcoord + Math.Max( 0, varying.Slot );
_vsOut.Add( new InterfaceField
{
Name = name,
Type = varying.Type,
Semantic = string.IsNullOrWhiteSpace( varying.Semantic ) ? $"TEXCOORD{slot}" : varying.Semantic,
Interpolation = varying.Interpolation,
Origin = varying.Origin
} );
}
AssignSemantics( _vsOut, "TEXCOORD" );
}
void MergeStructFields( string structName, List<InterfaceField> into, string semanticPrefix, int firstIndex )
{
if ( string.IsNullOrWhiteSpace( structName ) ) return;
var source = _module.FindStruct( structName );
if ( source is null ) return;
foreach ( var field in source.Fields )
{
if ( field is null || string.IsNullOrWhiteSpace( field.Name ) ) continue;
var name = SlangIntrinsics.FieldName( field.Name );
if ( into.Any( x => x.Name == name ) ) continue;
into.Add( new InterfaceField
{
Name = name,
Type = field.Type,
Semantic = field.Semantic,
Interpolation = field.Interpolation
} );
}
if ( source.Includes.Count > 0 && into == _vsIn )
{
_sink.Info( DiagnosticCode.BackendUnsupported,
$"'{structName}' pulls in engine headers, which a standalone Slang module cannot use; " +
"the Slang backend declared the fields directly instead." );
}
AssignSemantics( into, semanticPrefix, firstIndex );
}
static void AssignSemantics( List<InterfaceField> fields, string prefix, int firstIndex = 0 )
{
var used = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
foreach ( var field in fields )
{
if ( !string.IsNullOrWhiteSpace( field.Semantic ) ) used.Add( field.Semantic );
}
var next = firstIndex;
foreach ( var field in fields )
{
if ( !string.IsNullOrWhiteSpace( field.Semantic ) ) continue;
string candidate;
do
{
candidate = $"{prefix}{next.ToString( CultureInfo.InvariantCulture )}";
next++;
}
while ( !used.Add( candidate ) );
field.Semantic = candidate;
}
}
static InterfaceField Field( string name, ShaderType type, string semantic ) =>
new() { Name = name, Type = type, Semantic = semantic };
void PrepareUserStructs()
{
foreach ( var structure in _module.Structs )
{
if ( structure is null || string.IsNullOrWhiteSpace( structure.Name ) ) continue;
if ( _structRename.ContainsKey( structure.Name ) ) continue;
_userStructs.Add( structure );
}
}
// ---- sections ------------------------------------------------------
void WriteBanner()
{
var meta = _module.Meta;
_w.Line( $"#language slang {PrismConstants.SlangLanguageVersion}" );
_w.Line( $"module {_moduleName};" );
_w.Blank();
_w.Line( Rule() );
_w.Line( $"// {_moduleName}" );
if ( !string.IsNullOrWhiteSpace( meta.Description ) )
{
_w.Line( $"// {Single( meta.Description )}" );
}
_w.Line( $"// Generated by {PrismConstants.ProductName} {meta.Version} - regenerate from the graph, do not hand edit." );
_w.Line( "//" );
_w.Line( $"// Domain {meta.Domain}" );
_w.Line( $"// Shading model {meta.ShadingModel}" );
_w.Line( $"// Blend {meta.BlendMode}" );
_w.Line( $"// Cull {meta.CullMode}" );
_w.Line( $"// Stages {StageList()}" );
if ( meta.Capabilities.Count > 0 )
{
var capabilities = meta.Capabilities.Select( x => x.ToString() ).OrderBy( x => x, StringComparer.Ordinal );
_w.Line( $"// Capabilities {string.Join( ", ", capabilities )}" );
}
if ( _module.Includes.Count > 0 )
{
_w.Line( "//" );
_w.Line( "// The graph asked for these engine headers. They do not exist outside s&box, so" );
_w.Line( "// this module does not include them; anything it needed from them is inlined." );
foreach ( var include in _module.Includes )
{
_w.Line( $"// {Single( include )}" );
}
}
_w.Line( Rule() );
_w.Blank();
_w.Line( SlangRuntimeModule.ImportStatement );
_w.Blank();
}
string StageList()
{
var stages = _module.Meta.Stages.Stages().Select( x => x.DisplayName() ).ToList();
return stages.Count == 0 ? "None" : string.Join( ", ", stages );
}
static string Rule() => "// " + new string( '=', 74 );
void Section( string title )
{
_w.Blank();
_w.Line( $"// {new string( '-', 74 )}" );
_w.Line( $"// {title}" );
_w.Line( $"// {new string( '-', 74 )}" );
_w.Blank();
}
void WriteCombos()
{
if ( _module.Meta.Combos.Count == 0 ) return;
Section( "Combos" );
_w.Line( "// Graph keywords become link-time specialization constants: cheaper than a" );
_w.Line( "// preprocessor permutation, because the compiler front end is reused." );
_w.Blank();
foreach ( var combo in _module.Meta.Combos )
{
if ( combo is null || string.IsNullOrWhiteSpace( combo.Name ) ) continue;
var name = SlangIntrinsics.SanitizeIdentifier( combo.Name, "F_COMBO" );
if ( combo.Values is { Count: > 0 } )
{
var values = combo.Values.Select( ( x, i ) => $"{i} = {Single( x )}" );
_w.Line( $"// {combo.Kind}: {string.Join( ", ", values )}" );
}
else
{
_w.Line( $"// {combo.Kind}" );
}
_w.Line( $"extern static const int {name} = {combo.Default.ToString( CultureInfo.InvariantCulture )};" );
}
}
void WriteConstants()
{
if ( _constants.Count == 0 ) return;
Section( "Constants" );
foreach ( var constant in _constants )
{
var value = constant.Default.HasValue
? SlangIntrinsics.Literal( constant.Type, constant.Default.Value )
: SlangIntrinsics.Zero( constant.Type );
_w.Line( $"static const {TypeName( constant.Type )} {constant.Name} = {value};" );
}
}
/// <summary>
/// Declare the two preview uniforms. They are plain module-scope uniforms rather than members of
/// the parameter block: they are not part of the material's interface, they exist only while the
/// graph is being previewed, and a host pushes them per draw.
/// </summary>
void WriteInstrumentation()
{
if ( StageIds is null ) return;
Section( "Preview instrumentation" );
foreach ( var line in PreviewInstrumentation.Banner() ) _w.Line( line );
foreach ( var line in PreviewInstrumentation.SlangDeclarations() ) _w.Line( line );
}
/// <summary>
/// Write the pixel body, interleaving the preview stage switch between statements. The test sits
/// next to the temp it reads because a temp bound inside a loop or a branch is out of scope by
/// the end of the function.
/// </summary>
void WritePixelBody( IrBlock body )
{
if ( body is null ) return;
if ( StageIds is null )
{
WriteBlock( body );
return;
}
var statements = body.Statements;
var last = new Dictionary<NodeId, int>();
for ( int i = 0; i < statements.Count; i++ )
{
if ( statements[i] is not IrDecl decl || !decl.Origin.IsValid ) continue;
if ( !PreviewInstrumentation.CanShow( decl.Type ) ) continue;
last[decl.Origin] = i;
}
for ( int i = 0; i < statements.Count; i++ )
{
var statement = statements[i];
WriteStatement( statement );
if ( statement is not IrDecl decl ) continue;
if ( !last.TryGetValue( decl.Origin, out var index ) || index != i ) continue;
var line = PreviewInstrumentation.StageCase(
PreviewInstrumentation.StageIdOf( StageIds, decl.Origin ),
SlangIntrinsics.SanitizeIdentifier( decl.Name, "value" ), decl.Type );
if ( line is not null ) Emit( line, decl.Origin );
}
}
/// <summary>
/// Write the debug-channel tail. A standalone module has no engine shading model, so the
/// channels read the <c>PrismMaterial</c> the graph filled in and the interpolants the module
/// actually declared — anything it did not declare has no case and falls through.
/// </summary>
void WriteChannelTail( string material )
{
if ( StageIds is null ) return;
var lines = PreviewInstrumentation.ChannelLines( ChannelEnvironment( material ) );
if ( lines.Count == 0 ) return;
_w.Blank();
foreach ( var line in lines ) _w.Line( line );
}
PreviewChannelEnvironment ChannelEnvironment( string material )
{
var uv0 = Interpolant( Builtin.TexCoord0 );
var normal = Interpolant( Builtin.WorldNormal );
var tangentU = Interpolant( Builtin.WorldTangentU );
var tangentV = Interpolant( Builtin.WorldTangentV );
return new PreviewChannelEnvironment
{
Albedo = Field( material, "Albedo" ),
Opacity = Field( material, "Opacity" ),
NormalTangent = Field( material, "Normal" ),
// PrismMaterial::WorldNormal takes the tangent basis, so the channel is only available
// when the module carries a complete frame.
NormalWorld = material is not null && normal is not null && tangentU is not null && tangentV is not null
? $"{material}.WorldNormal( float3x3( {tangentU}, {tangentV}, {normal} ) )"
: null,
Roughness = Field( material, "Roughness" ),
Metalness = Field( material, "Metalness" ),
AmbientOcclusion = Field( material, "AmbientOcclusion" ),
Emission = Field( material, "Emission" ),
Transmission = Field( material, "Transmission" ),
TintMask = Field( material, "TintMask" ),
Uv0 = uv0,
Uv1 = Interpolant( Builtin.TexCoord1 ),
VertexColor = Interpolant( Builtin.VertexColor ),
WorldPosition = Interpolant( Builtin.WorldPosition ),
DerivativeSource = uv0
};
}
/// <summary>The interpolant carrying a builtin, or null when this module never asked for it.</summary>
string Interpolant( Builtin id )
{
foreach ( var field in _vsOut )
{
if ( field.Source == id ) return $"{SlangIntrinsics.InputParameter}.{field.Name}";
}
return null;
}
static string Field( string material, string name ) =>
material is null ? null : $"{material}.{name}";
void WriteParameters()
{
if ( _parameters.Count == 0 ) return;
Section( "Parameters" );
_w.Line( "// One ParameterBlock means one descriptor set (Vulkan) or register space (D3D12)," );
_w.Line( "// and it is the only Slang container that may hold textures and samplers directly." );
_w.Blank();
_w.Line( $"struct {_paramsStruct}" );
_w.Line( "{" );
_w.Push();
var group = -1;
foreach ( var parameter in _parameters )
{
var next = GroupOf( parameter.Decl );
if ( group >= 0 && next != group ) _w.Blank();
WriteParameterField( parameter, group >= 0 );
group = next;
}
_w.Pop();
_w.Line( "};" );
_w.Blank();
_w.Line( $"ParameterBlock<{_paramsStruct}> {_paramsBlock};" );
}
void WriteParameterField( ParameterField parameter, bool allowLeadingBlank )
{
var decl = parameter.Decl;
var attributes = UiAttributes( decl );
if ( attributes.Count > 0 )
{
var joined = string.Join( " ", attributes );
// An annotated parameter gets breathing room, and a long annotation set breaks onto one
// line per attribute rather than running off the right of the screen.
if ( allowLeadingBlank ) _w.Blank();
if ( joined.Length <= 96 )
{
_w.Line( joined );
}
else
{
foreach ( var attribute in attributes ) _w.Line( attribute );
}
}
var array = decl.ArraySize > 0
? $"[{decl.ArraySize.ToString( CultureInfo.InvariantCulture )}]"
: string.Empty;
var suffix = decl.PreviewOnly ? " // preview only" : string.Empty;
_w.Line( $"{TypeName( decl.Type )} {parameter.Name}{array};{suffix}" );
}
List<string> UiAttributes( GlobalDecl decl )
{
var attributes = new List<string>();
// A sampler is plumbing, not a material parameter. Annotating it would only add noise to
// the reflection output the material inspector is built from.
if ( decl.Kind == GlobalKind.Sampler ) return attributes;
var ui = decl.Ui;
var display = SplitWords( decl.Name );
if ( !string.IsNullOrWhiteSpace( display ) )
{
attributes.Add( $"[UiLabel( {SlangIntrinsics.QuotedString( display )} )]" );
}
if ( ui is not null )
{
if ( !string.IsNullOrWhiteSpace( ui.Group ) )
{
attributes.Add( $"[UiGroup( {SlangIntrinsics.QuotedString( ui.Group )}, {Int( ui.Order )} )]" );
}
if ( ui.Control != UiControl.Default )
{
attributes.Add( $"[UiControl( {SlangIntrinsics.QuotedString( ui.Control.ToString() )} )]" );
}
if ( ui.Control == UiControl.Slider || ui.Min != 0f || ui.Max != 1f )
{
attributes.Add( $"[UiRange( {SlangIntrinsics.Real( ui.Min )}, {SlangIntrinsics.Real( ui.Max )} )]" );
}
if ( !string.IsNullOrWhiteSpace( ui.Tooltip ) )
{
attributes.Add( $"[UiTooltip( {SlangIntrinsics.QuotedString( ui.Tooltip )} )]" );
}
}
if ( decl.Default.HasValue )
{
var value = decl.Default.Value;
attributes.Add( $"[UiDefault( {SlangIntrinsics.Real( value.X )}, {SlangIntrinsics.Real( value.Y )}, " +
$"{SlangIntrinsics.Real( value.Z )}, {SlangIntrinsics.Real( value.W )} )]" );
}
if ( !string.IsNullOrWhiteSpace( decl.DefaultAsset ) )
{
attributes.Add( $"[UiAsset( {SlangIntrinsics.QuotedString( decl.DefaultAsset )} )]" );
}
if ( !string.IsNullOrWhiteSpace( decl.AttributeName ) )
{
attributes.Add( $"[PrismAttribute( {SlangIntrinsics.QuotedString( decl.AttributeName )} )]" );
}
if ( decl.Srgb )
{
attributes.Add( "[PrismSrgb( 1 )]" );
}
return attributes;
}
void WriteUserStructs()
{
if ( _userStructs.Count == 0 ) return;
Section( "Types" );
foreach ( var structure in _userStructs )
{
_w.Line( $"struct {SlangIntrinsics.SanitizeIdentifier( structure.Name, "PrismStruct" )}" );
_w.Line( "{" );
_w.Push();
foreach ( var field in structure.Fields )
{
if ( field is null || string.IsNullOrWhiteSpace( field.Name ) ) continue;
var semantic = string.IsNullOrWhiteSpace( field.Semantic ) ? string.Empty : $" : {field.Semantic}";
var interpolation = SlangIntrinsics.Interpolation( field.Interpolation );
var prefix = string.IsNullOrEmpty( interpolation ) ? string.Empty : interpolation + " ";
_w.Line( $"{prefix}{TypeName( field.Type )} {SlangIntrinsics.FieldName( field.Name )}{semantic};" );
}
_w.Pop();
_w.Line( "};" );
_w.Blank();
}
}
void WriteHelpers()
{
if ( _module.Helpers.Count == 0 ) return;
Section( "Helpers" );
var emitted = new HashSet<string>( StringComparer.Ordinal );
foreach ( var helper in _module.Helpers )
{
if ( helper is null || string.IsNullOrWhiteSpace( helper.Name ) ) continue;
if ( !emitted.Add( helper.Name ) ) continue;
if ( helper.Includes.Count > 0 )
{
_w.Line( $"// {helper.Name} was written against: {string.Join( ", ", helper.Includes.Select( Single ) )}" );
}
var body = helper.BodyFor( PrismConstants.BackendSlang );
if ( string.IsNullOrWhiteSpace( body ) )
{
WriteHelperStub( helper );
continue;
}
_w.Verbatim( body.Trim() );
_w.Blank();
}
}
void WriteHelperStub( HelperFunction helper )
{
if ( _reportedHelpers.Add( helper.Name ) )
{
_sink.Warn( DiagnosticCode.BackendUnsupported,
$"Helper '{helper.Name}' has no body for the Slang backend.",
null, "The generated module declares it and returns zero so the rest of the shader still compiles." );
}
_w.Line( $"// !! {helper.Name} has no Slang body. Returning zero so the module still compiles." );
_w.Line( Signature( helper ) );
_w.Line( "{" );
_w.Push();
_w.Line( helper.ReturnType.IsVoid ? "return;" : $"return {SlangIntrinsics.Zero( helper.ReturnType )};" );
_w.Pop();
_w.Line( "}" );
_w.Blank();
}
string Signature( HelperFunction helper )
{
var parameters = helper.Parameters.Select( ParameterText );
return $"{TypeName( helper.ReturnType )} {helper.Name}( {string.Join( ", ", parameters )} )";
}
string ParameterText( HelperParam parameter )
{
var modifier = parameter.Modifier switch
{
IrParamModifier.Out => "out ",
IrParamModifier.InOut => "inout ",
IrParamModifier.Uniform => "uniform ",
_ => string.Empty
};
return $"{modifier}{TypeName( parameter.Type )} {SlangIntrinsics.SanitizeIdentifier( parameter.Name, "value" )}";
}
void WriteInterfaces()
{
// Only a module that emits a rasterisation entry point needs a stage interface.
if ( _vertex is null && _pixel is null && _geometry is null ) return;
Section( "Stage interfaces" );
WriteInterface( SlangIntrinsics.VertexInputStruct, _vsIn,
"What the vertex program reads per vertex." );
WriteInterface( SlangIntrinsics.VertexOutputStruct, _vsOut,
"What travels from the vertex program to the pixel program." );
}
void WriteInterface( string name, List<InterfaceField> fields, string description )
{
_w.Line( $"// {description}" );
_w.Line( $"struct {name}" );
_w.Line( "{" );
_w.Push();
foreach ( var field in fields )
{
var interpolation = SlangIntrinsics.Interpolation( field.Interpolation );
var prefix = string.IsNullOrEmpty( interpolation ) ? string.Empty : interpolation + " ";
var semantic = string.IsNullOrWhiteSpace( field.Semantic ) ? string.Empty : $" : {field.Semantic}";
var line = _w.Line( $"{prefix}{TypeName( field.Type )} {field.Name}{semantic};" );
if ( field.Origin.IsValid ) _map.Add( line, field.Origin );
}
_w.Pop();
_w.Line( "};" );
_w.Blank();
}
void WriteFunctions()
{
var functions = _module.Functions.Where( x => x is not null && !x.IsEntryPoint ).ToList();
if ( functions.Count == 0 ) return;
Section( "Functions" );
foreach ( var function in functions )
{
_stage = function.Stage;
foreach ( var attribute in function.Attributes )
{
if ( string.IsNullOrWhiteSpace( attribute ) ) continue;
if ( attribute.Contains( "shader(", StringComparison.Ordinal ) ) continue;
_w.Line( attribute.Trim() );
}
var parameters = string.Join( ", ", function.Parameters.Select( ParameterText ) );
var returnType = string.IsNullOrWhiteSpace( function.ReturnStruct )
? TypeName( function.ReturnType )
: TypeName( ShaderType.Struct( function.ReturnStruct ) );
_w.Line( $"{returnType} {SlangIntrinsics.SanitizeIdentifier( function.Name, "PrismFunction" )}( {parameters} )" );
_w.Line( "{" );
_w.Push();
WriteBlock( function.Body );
_w.Pop();
_w.Line( "}" );
_w.Blank();
}
_stage = ShaderStage.None;
}
// ---- entry points --------------------------------------------------
void WriteEntryPoints()
{
var domain = _module.Meta.Domain;
var wantsGraphics = domain is ShaderDomain.Surface or ShaderDomain.PostProcess;
if ( _vertex is not null || ( wantsGraphics && _pixel is not null ) )
{
Section( "Vertex" );
WriteVertexEntry();
}
if ( _pixel is not null )
{
Section( "Pixel" );
WritePixelEntry();
}
if ( _geometry is not null )
{
Section( "Geometry" );
WriteGeometryEntry();
}
if ( _compute is not null )
{
Section( "Compute" );
WriteComputeEntry();
}
}
void WriteVertexEntry()
{
_stage = ShaderStage.Vertex;
var name = EntryName( _vertex, ShaderStage.Vertex );
_w.Line( SlangIntrinsics.StageAttribute( ShaderStage.Vertex ) );
WriteExtraAttributes( _vertex );
_w.Line( $"{SlangIntrinsics.VertexOutputStruct} {name}( {SlangIntrinsics.VertexInputStruct} {SlangIntrinsics.InputParameter} )" );
_w.Line( "{" );
_w.Push();
var output = VertexOutputVariable( _vertex );
var fullscreen = _vertex is null && _module.Meta.Domain == ShaderDomain.PostProcess;
_vertexMovesPosition = _vertex is not null && MovesWorldPosition( _vertex.Body );
// A generated full-screen pass reads nothing but the vertex id, so a prologue would only
// bind values it is about to overwrite.
if ( !fullscreen ) WritePrologue( _vertexBuiltins, ShaderStage.Vertex );
if ( _vertex is null )
{
WriteSynthesizedVertexBody();
}
else
{
WriteVertexBody( output );
}
_w.Pop();
_w.Line( "}" );
_stage = ShaderStage.None;
}
/// <summary>
/// Emit the graph's own vertex program, extended with whatever interpolants the pixel program
/// needs and the body did not already write. The body is never discarded: when its shape leaves
/// no place to insert the extension, it is emitted verbatim and the gap is stated in a comment.
/// </summary>
void WriteVertexBody( string output )
{
if ( output is null )
{
if ( EndsWithReturn( _vertex.Body ) )
{
WriteBlock( _vertex.Body );
_w.Line( "// The vertex program returns an expression rather than a local, so Prism could" );
_w.Line( "// not append the interpolants the pixel program reads. Check them by hand." );
return;
}
// The output local is declared *before* the body, not after it: the body's own interpolant
// writes are redirected onto it, so it has to already exist when they are emitted.
var local = FreeLocal( _vertex.Body, "o" );
_vertexOutput = local;
_w.Line( $"{SlangIntrinsics.VertexOutputStruct} {local};" );
_w.Blank();
WriteBlock( _vertex.Body );
WriteVertexPositionResync();
WriteInterpolantWriteback( local, AssignedFields( _vertex.Body, local ), true );
_w.Blank();
_w.Line( $"return {local};" );
_vertexOutput = null;
return;
}
_vertexOutput = output;
var assigned = AssignedFields( _vertex.Body, output );
var wroteInterpolants = false;
foreach ( var statement in _vertex.Body.Statements )
{
if ( !wroteInterpolants && statement is IrReturn )
{
WriteVertexPositionResync();
WriteInterpolantWriteback( output, assigned );
wroteInterpolants = true;
}
WriteStatement( statement );
}
if ( !wroteInterpolants )
{
WriteVertexPositionResync();
WriteInterpolantWriteback( output, assigned );
_w.Line( $"return {output};" );
}
_vertexOutput = null;
}
/// <summary>
/// Recompute clip space when the graph displaced the vertex, mirroring what the s&box backend
/// emits before <c>FinalizeVertex</c>. Without it the interpolant writeback would hand the pixel
/// program a clip position computed from the position the vertex had before it moved.
/// </summary>
void WriteVertexPositionResync()
{
if ( !_vertexMovesPosition ) return;
var world = SlangIntrinsics.BuiltinLocal( Builtin.WorldPosition );
var clip = SlangIntrinsics.BuiltinLocal( Builtin.ClipPosition );
_w.Blank();
_w.Line( "// The graph moved the vertex, so clip space is recomputed from the new position." );
_w.Line( $"{clip} = PrismWorldToClip( {world} );" );
}
void WriteSynthesizedVertexBody()
{
var output = "o";
if ( _module.Meta.Domain == ShaderDomain.PostProcess )
{
_w.Line( "// A fullscreen triangle, generated from the vertex id. No vertex buffer needed." );
_w.Line( $"float2 uv = float2( ( {SlangIntrinsics.InputParameter}.VertexId << 1 ) & 2, {SlangIntrinsics.InputParameter}.VertexId & 2 );" );
_w.Blank();
_w.Line( $"{SlangIntrinsics.VertexOutputStruct} {output};" );
_w.Line( $"{output}.Position = float4( uv * float2( 2.0, -2.0 ) + float2( -1.0, 1.0 ), 0.0, 1.0 );" );
foreach ( var field in _vsOut )
{
if ( field.Name == "Position" ) continue;
_w.Line( field.Name == "Uv"
? $"{output}.Uv = uv;"
: $"{output}.{field.Name} = {SlangIntrinsics.Zero( field.Type )};" );
}
_w.Blank();
_w.Line( $"return {output};" );
return;
}
_w.Line( "// The graph produced no vertex program, so this is the standard one: transform the" );
_w.Line( "// vertex and hand the pixel program every interpolant it asked for." );
_w.Line( $"{SlangIntrinsics.VertexOutputStruct} {output};" );
WriteInterpolantWriteback( output, new HashSet<string>( StringComparer.Ordinal ), true );
_w.Blank();
_w.Line( $"return {output};" );
}
void WriteInterpolantWriteback( string output, HashSet<string> assigned, bool includeVaryings = false )
{
var lines = new List<string>();
foreach ( var field in _vsOut )
{
if ( assigned.Contains( field.Name ) ) continue;
if ( field.Name == "Position" )
{
lines.Add( $"{output}.Position = {SlangIntrinsics.BuiltinLocal( Builtin.ClipPosition )};" );
continue;
}
if ( field.Source != Builtin.None )
{
var local = SlangIntrinsics.BuiltinLocal( field.Source );
lines.Add( $"{output}.{field.Name} = {Convert( local, Builtins.TypeOf( field.Source ), field.Type )};" );
continue;
}
if ( !includeVaryings ) continue;
lines.Add( $"{output}.{field.Name} = {SlangIntrinsics.Zero( field.Type )}; // TODO(WP-2): no vertex program wrote this varying" );
}
if ( lines.Count == 0 ) return;
_w.Blank();
_w.Line( "// Interpolants" );
foreach ( var line in lines ) _w.Line( line );
}
void WritePixelEntry()
{
_stage = ShaderStage.Pixel;
var name = EntryName( _pixel, ShaderStage.Pixel );
var returnType = _pixel.ReturnType.IsVoid || _pixel.ReturnType.IsStruct
? ShaderType.Float4
: _pixel.ReturnType;
var semantic = string.IsNullOrWhiteSpace( _pixel.ReturnSemantic ) ? "SV_Target0" : _pixel.ReturnSemantic;
var parameters = $"{SlangIntrinsics.VertexOutputStruct} {SlangIntrinsics.InputParameter}";
if ( _needsFrontFace ) parameters += $", bool {SlangIntrinsics.FrontFaceParameter} : SV_IsFrontFace";
_w.Line( SlangIntrinsics.StageAttribute( ShaderStage.Pixel ) );
WriteExtraAttributes( _pixel );
_w.Line( $"{TypeName( returnType )} {name}( {parameters} ) : {semantic}" );
_w.Line( "{" );
_w.Push();
WritePrologue( _pixelBuiltins, ShaderStage.Pixel );
// A surface graph writes its results into a material struct rather than returning a colour.
// The engine's Material and its shading model do not exist here, so the prelude's stand-in is
// declared instead and resolved at the end — the module stays self-contained and compilable,
// and stays an honest description of the *material* rather than pretending to be s&box's
// lighting. See PrismMaterial in prism.core.
var material = MaterialLocal( _pixel.Body );
if ( material is not null )
{
_w.Line( $"{SlangRuntimeModule.MaterialStruct} {material} = {SlangRuntimeModule.MaterialStruct}::Init();" );
_w.Blank();
}
WritePixelBody( _pixel.Body );
if ( !EndsWithReturn( _pixel.Body ) )
{
WriteChannelTail( material );
_w.Blank();
if ( material is not null )
{
_w.Line( "// No engine shading model in a standalone module: resolve the material unlit." );
_w.Line( $"return {material}.ToUnlitColor();" );
}
else
{
_w.Line( $"return {SlangIntrinsics.Zero( returnType )};" );
}
}
_w.Pop();
_w.Line( "}" );
_stage = ShaderStage.None;
}
void WriteGeometryEntry()
{
_stage = ShaderStage.Geometry;
var name = EntryName( _geometry, ShaderStage.Geometry );
var parameters = string.Join( ", ", _geometry.Parameters.Select( ParameterText ) );
var returnType = string.IsNullOrWhiteSpace( _geometry.ReturnStruct )
? TypeName( _geometry.ReturnType )
: TypeName( ShaderType.Struct( _geometry.ReturnStruct ) );
_w.Line( SlangIntrinsics.StageAttribute( ShaderStage.Geometry ) );
WriteExtraAttributes( _geometry );
_w.Line( $"{returnType} {name}( {parameters} )" );
_w.Line( "{" );
_w.Push();
WriteBlock( _geometry.Body );
_w.Pop();
_w.Line( "}" );
_stage = ShaderStage.None;
}
void WriteComputeEntry()
{
_stage = ShaderStage.Compute;
var name = EntryName( _compute, ShaderStage.Compute );
var threads = _compute.Attributes.FirstOrDefault( x =>
!string.IsNullOrWhiteSpace( x ) && x.Contains( "numthreads", StringComparison.OrdinalIgnoreCase ) );
var parameters = new List<string>();
if ( _computeBuiltins.Contains( Builtin.DispatchThreadId ) || _computeBuiltins.Count == 0 )
{
parameters.Add( $"uint3 {SlangIntrinsics.DispatchThreadIdParameter} : SV_DispatchThreadID" );
}
if ( _computeBuiltins.Contains( Builtin.GroupThreadId ) )
{
parameters.Add( $"uint3 {SlangIntrinsics.GroupThreadIdParameter} : SV_GroupThreadID" );
}
if ( _computeBuiltins.Contains( Builtin.GroupId ) )
{
parameters.Add( $"uint3 {SlangIntrinsics.GroupIdParameter} : SV_GroupID" );
}
_w.Line( SlangIntrinsics.StageAttribute( ShaderStage.Compute ) );
_w.Line( string.IsNullOrWhiteSpace( threads ) ? "[numthreads(8, 8, 1)]" : threads.Trim() );
WriteExtraAttributes( _compute, skipNumThreads: true );
_w.Line( $"void {name}( {string.Join( ", ", parameters )} )" );
_w.Line( "{" );
_w.Push();
WritePrologue( _computeBuiltins, ShaderStage.Compute );
WriteBlock( _compute.Body );
_w.Pop();
_w.Line( "}" );
_stage = ShaderStage.None;
}
void WriteExtraAttributes( IrFunction function, bool skipNumThreads = false )
{
if ( function is null ) return;
foreach ( var attribute in function.Attributes )
{
if ( string.IsNullOrWhiteSpace( attribute ) ) continue;
if ( attribute.Contains( "shader(", StringComparison.Ordinal ) ) continue;
if ( skipNumThreads && attribute.Contains( "numthreads", StringComparison.OrdinalIgnoreCase ) ) continue;
_w.Line( attribute.Trim() );
}
}
void WritePrologue( List<Builtin> builtins, ShaderStage stage )
{
var emitted = builtins.Where( x => !Builtins.TypeOf( x ).IsVoid ).ToList();
if ( emitted.Count == 0 ) return;
_w.Line( "// Environment" );
foreach ( var id in emitted )
{
var type = Builtins.TypeOf( id );
var expression = SlangIntrinsics.BuiltinExpression( id, stage );
var comment = string.Empty;
// A builtin derived from a vertex attribute this domain does not carry binds to zero
// rather than naming a field the generated VsIn struct never declared.
if ( stage == ShaderStage.Vertex )
{
var required = SlangIntrinsics.VertexInputField( id );
if ( required is not null && !_vsIn.Any( x => x.Name == required ) )
{
expression = SlangIntrinsics.Zero( type );
comment = $" // this domain has no {required} attribute";
}
}
// A displaced vertex writes through the world-position local and then re-derives clip
// space from it, so neither of those two can be const in that case.
var mutable = stage == ShaderStage.Vertex && _vertexMovesPosition &&
id is Builtin.WorldPosition or Builtin.ClipPosition;
var qualifier = mutable ? string.Empty : "const ";
_w.Line( $"{qualifier}{TypeName( type )} {SlangIntrinsics.BuiltinLocal( id )} = {expression};{comment}" );
}
_w.Blank();
}
/// <summary>
/// True when a block assigns through the compiler's world-position member, i.e. the graph drives a
/// vertex offset. Mirrors the same test in the s&box backend.
/// </summary>
static bool MovesWorldPosition( IrBlock block )
{
if ( block is null ) return false;
foreach ( var statement in Statements( block ) )
{
if ( statement is not IrAssign assign ) continue;
foreach ( var node in IrExprUtil.Walk( assign.Target ) )
{
if ( node is not IrMember member || !IsPixelInputVar( member.V ) ) continue;
if ( string.Equals( SlangIntrinsics.FieldName( member.Field ), s_worldPositionField,
StringComparison.Ordinal ) )
{
return true;
}
}
}
return false;
}
string EntryName( IrFunction function, ShaderStage stage )
{
if ( function is not null && !string.IsNullOrWhiteSpace( function.Name ) )
{
return SlangIntrinsics.SanitizeIdentifier( function.Name, stage.EntryPoint() );
}
var prefix = _options.EntryPointPrefix ?? string.Empty;
return SlangIntrinsics.SanitizeIdentifier( prefix + stage.EntryPoint(), "Main" );
}
/// <summary>
/// The local the vertex program builds its output in: the variable it returns, or failing that
/// the local it declared with the vertex-output struct type. Finding it is what lets Prism add
/// the interpolants the pixel program needs without disturbing the program the graph wrote.
/// </summary>
string VertexOutputVariable( IrFunction function )
{
if ( function is null ) return null;
foreach ( var statement in function.Body.Statements )
{
if ( statement is IrReturn { Value: IrVar variable } ) return variable.Name;
}
foreach ( var statement in function.Body.Statements )
{
if ( statement is IrDecl declaration && declaration.Type.IsStruct &&
TypeName( declaration.Type ) == SlangIntrinsics.VertexOutputStruct )
{
return SlangIntrinsics.SanitizeIdentifier( declaration.Name, "o" );
}
}
return null;
}
/// <summary>A local name no declaration in the block has already claimed.</summary>
static string FreeLocal( IrBlock block, string preferred )
{
var used = new HashSet<string>( StringComparer.Ordinal );
foreach ( var statement in Statements( block ) )
{
if ( statement is IrDecl declaration && !string.IsNullOrEmpty( declaration.Name ) )
{
used.Add( SlangIntrinsics.SanitizeIdentifier( declaration.Name, "value" ) );
}
}
if ( !used.Contains( preferred ) ) return preferred;
for ( int i = 2; i < 1000; i++ )
{
var candidate = preferred + i.ToString( CultureInfo.InvariantCulture );
if ( !used.Contains( candidate ) ) return candidate;
}
return preferred + "_x";
}
static bool EndsWithReturn( IrBlock block )
{
if ( block is null || block.Statements.Count == 0 ) return false;
return block.Statements[^1] is IrReturn;
}
HashSet<string> AssignedFields( IrBlock block, string variable )
{
var assigned = new HashSet<string>( StringComparer.Ordinal );
foreach ( var statement in Statements( block ) )
{
if ( statement is not IrAssign assign ) continue;
if ( assign.Target is not IrMember member ) continue;
if ( member.V is not IrVar target || target.Name != variable ) continue;
assigned.Add( SlangIntrinsics.FieldName( member.Field ) );
}
return assigned;
}
// ---- statements ----------------------------------------------------
void WriteBlock( IrBlock block )
{
if ( block is null ) return;
foreach ( var statement in block.Statements ) WriteStatement( statement );
}
void WriteStatement( IrStmt statement )
{
switch ( statement )
{
case IrComment comment:
if ( _options.EmitComments || _module.Meta.DebugSymbols )
{
Emit( $"// {Single( comment.Text )}", comment.Origin );
}
break;
// clip(), the barriers and the interlocked operations produce nothing. Binding one to a
// local would not compile, so it becomes a bare statement instead.
case IrDecl { Init: IrCall call } voidCall when SlangIntrinsics.IsVoidResult( call.Id ):
Emit( $"{Expr( call )};", voidCall.Origin );
break;
case IrDecl decl:
var name = SlangIntrinsics.SanitizeIdentifier( decl.Name, "value" );
Emit( decl.Init is null
? $"{TypeName( decl.Type )} {name};"
: $"{TypeName( decl.Type )} {name} = {Expr( decl.Init )};", decl.Origin );
break;
case IrAssign assign:
Emit( $"{Expr( assign.Target, PrecPrimary )} = {Expr( assign.Value )};", assign.Origin );
break;
case IrIf branch:
Emit( $"if ( {Expr( branch.Cond )} )", branch.Origin );
Emit( "{", branch.Origin );
_w.Push();
WriteBlock( branch.Then );
_w.Pop();
Emit( "}", branch.Origin );
if ( branch.Else is { IsEmpty: false } )
{
Emit( "else", branch.Origin );
Emit( "{", branch.Origin );
_w.Push();
WriteBlock( branch.Else );
_w.Pop();
Emit( "}", branch.Origin );
}
break;
case IrFor loop:
var index = SlangIntrinsics.SanitizeIdentifier( loop.Var, "index" );
Emit( $"for ( int {index} = 0; {index} < {Expr( loop.Count, PrecPrimary )}; ++{index} )", loop.Origin );
Emit( "{", loop.Origin );
_w.Push();
WriteBlock( loop.Body );
_w.Pop();
Emit( "}", loop.Origin );
break;
case IrWhile loop:
Emit( $"while ( {Expr( loop.Cond )} )", loop.Origin );
Emit( "{", loop.Origin );
_w.Push();
WriteBlock( loop.Body );
_w.Pop();
Emit( "}", loop.Origin );
break;
case IrBreak stop:
Emit( "break;", stop.Origin );
break;
case IrContinue skip:
Emit( "continue;", skip.Origin );
break;
case IrReturn ret:
Emit( ret.Value is null ? "return;" : $"return {Expr( ret.Value )};", ret.Origin );
break;
case IrDiscard kill:
Emit( "discard;", kill.Origin );
break;
case IrExprStmt expression:
Emit( $"{Expr( expression.Value )};", expression.Origin );
break;
case IrScope scope:
Emit( "{", scope.Origin );
_w.Push();
WriteBlock( scope.Body );
_w.Pop();
Emit( "}", scope.Origin );
break;
// Slang runs a C preprocessor of its own, so a compile-time branch lowers to exactly the
// same directives the .shader gets and only the taken side reaches the compiler.
case IrPreprocessorIf guard:
WritePreprocessorIf( guard );
break;
}
}
void WritePreprocessorIf( IrPreprocessorIf guard )
{
var directive = IrPreprocessor.OpenDirective( guard.Condition );
if ( string.IsNullOrEmpty( directive ) )
{
// A condition we cannot spell must not become a directive the preprocessor rejects.
// Folding both sides in keeps the module compiling and costs only the exclusion.
_sink.Warn( DiagnosticCode.BackendUnsupported,
"A compile-time branch had no usable combo condition, so both of its sides were emitted.",
guard.Origin.IsValid ? GraphRef.ForNode( guard.Origin ) : null,
"Bind the node to a graph keyword. Without one there is nothing for the preprocessor to test." );
WriteBlock( guard.Then );
WriteBlock( guard.Else );
return;
}
Emit( directive, guard.Origin );
WriteBlock( guard.Then );
if ( guard.HasElse )
{
Emit( IrPreprocessor.ElseDirective, guard.Origin );
WriteBlock( guard.Else );
}
Emit( IrPreprocessor.EndDirective, guard.Origin );
}
void Emit( string text, NodeId origin )
{
var line = _w.Line( text );
if ( origin.IsValid ) _map.Add( line, origin );
}
// ---- expressions ---------------------------------------------------
string Expr( IrExpr expr, int minPrecedence = 0 )
{
if ( expr is null ) return "0";
switch ( expr )
{
case IrConst constant:
return SlangIntrinsics.Literal( constant.Type, constant.Value );
case IrVar variable:
return SlangIntrinsics.SanitizeIdentifier( variable.Name, "value" );
case IrGlobalRef reference:
return GlobalPath( reference );
case IrBuiltinRef builtin:
return BuiltinPath( builtin.Id );
case IrCall call:
return CallText( call );
case IrHelperCall helper:
return $"{SlangIntrinsics.SanitizeIdentifier( helper.Fn?.Name, "PrismHelper" )}( {Args( helper.Args )} )";
case IrBinary binary:
return BinaryText( binary, minPrecedence );
case IrUnary unary:
{
var symbol = SlangIntrinsics.Symbol( unary.Op );
var operand = Expr( unary.V, PrecUnary );
// See HlslEmitter: without the space a nested negate prints `--x`, which lexes as
// pre-decrement rather than as two negations.
var gap = operand.Length > 0 && operand[0] == symbol[0] ? " " : string.Empty;
return Wrap( $"{symbol}{gap}{operand}", PrecUnary, minPrecedence );
}
case IrSwizzle swizzle:
return $"{Expr( swizzle.V, PrecPrimary )}.{Mask( swizzle )}";
case IrConstruct construct:
return $"{TypeName( construct.Type )}( {Args( construct.Parts )} )";
case IrCast cast:
return CastText( cast );
case IrSelect select:
return SelectText( select.Type, select.C, select.A, select.B );
case IrIndex index:
return $"{Expr( index.V, PrecPrimary )}[{Expr( index.I )}]";
case IrMember member:
if ( TryRedirectStageMember( member, out var redirected ) ) return redirected;
return $"{Expr( member.V, PrecPrimary )}.{SlangIntrinsics.FieldName( member.Field )}";
}
return SlangIntrinsics.Zero( expr.Type );
}
/// <summary>
/// Resolve a member access on the compiler's synthetic pixel-input struct to something that exists
/// in a standalone Slang module.
/// <para>
/// WP-2 emits every vertex-stage result as a write through <c>PixelInput i</c>, because that is the
/// shape an s&box <c>.shader</c> block wants. Slang has no such struct: the vertex entry point
/// takes a <c>VsIn</c> and returns a <c>VsOut</c>, so writing <c>i.Something</c> would either not
/// compile or, worse, mutate the input. Two cases: the world position is a prologue-bound local,
/// because displacing the vertex has to feed the clip-space transform; everything else is an
/// interpolant and belongs on the output struct.
/// </para>
/// </summary>
bool TryRedirectStageMember( IrMember member, out string text )
{
text = null;
if ( _stage != ShaderStage.Vertex ) return false;
if ( member?.V is not IrVar variable || !variable.Type.IsStruct ) return false;
if ( !string.Equals( variable.Type.StructName, GraphCompiler.PixelInputStruct, StringComparison.Ordinal ) )
{
return false;
}
var field = SlangIntrinsics.FieldName( member.Field );
if ( string.Equals( field, s_worldPositionField, StringComparison.Ordinal ) )
{
text = SlangIntrinsics.BuiltinLocal( Builtin.WorldPosition );
return true;
}
if ( string.IsNullOrEmpty( _vertexOutput ) ) return false;
text = $"{_vertexOutput}.{field}";
return true;
}
/// <summary>
/// The name of the material local a pixel body writes through, or null when it does not use one.
/// </summary>
static string MaterialLocal( IrBlock block )
{
if ( block is null ) return null;
foreach ( var statement in Statements( block ) )
{
if ( statement is not IrAssign assign ) continue;
foreach ( var node in IrExprUtil.Walk( assign.Target ) )
{
if ( node is not IrMember member ) continue;
if ( member.V is not IrVar variable || !variable.Type.IsStruct ) continue;
if ( string.Equals( variable.Type.StructName, GraphCompiler.MaterialStruct, StringComparison.Ordinal ) )
{
return SlangIntrinsics.SanitizeIdentifier( variable.Name, "prismMaterial" );
}
}
}
return null;
}
/// <summary>True when this expression is the compiler's synthetic pixel-input struct instance.</summary>
static bool IsPixelInputVar( IrExpr expr ) =>
expr is IrVar variable && variable.Type.IsStruct &&
string.Equals( variable.Type.StructName, GraphCompiler.PixelInputStruct, StringComparison.Ordinal );
static readonly string s_worldPositionField = SlangIntrinsics.FieldName( GraphCompiler.WorldPositionField );
string GlobalPath( IrGlobalRef reference )
{
var declName = reference.Decl?.Name;
if ( !string.IsNullOrEmpty( declName ) && _globalPath.TryGetValue( declName, out var path ) ) return path;
return SlangIntrinsics.SanitizeIdentifier( declName, "prismGlobal" );
}
string BuiltinPath( Builtin id )
{
if ( id == Builtin.None ) return "0";
var list = _stage switch
{
ShaderStage.Vertex => _vertexBuiltins,
ShaderStage.Pixel => _pixelBuiltins,
ShaderStage.Compute => _computeBuiltins,
_ => null
};
// Inside an entry point the prologue has already bound the value to a readable local.
// Anywhere else (a generated free function) there is no prologue, so inline the expression.
if ( list is not null && list.Contains( id ) ) return SlangIntrinsics.BuiltinLocal( id );
return SlangIntrinsics.BuiltinExpression( id, _stage );
}
string CallText( IrCall call )
{
var args = call.Args ?? Array.Empty<IrExpr>();
if ( call.Id == Intrinsic.Select && args.Length >= 3 )
{
return SelectText( call.Type, args[0], args[1], args[2] );
}
var name = SlangIntrinsics.Name( call.Id );
if ( SlangIntrinsics.IsObjectMethod( call.Id ) && args.Length >= 1 )
{
var receiver = Expr( args[0], PrecPrimary );
var rest = args.Skip( 1 ).Select( x => Expr( x ) );
return rest.Any()
? $"{receiver}.{name}( {string.Join( ", ", rest )} )"
: $"{receiver}.{name}()";
}
return args.Length == 0 ? $"{name}()" : $"{name}( {Args( args )} )";
}
string SelectText( ShaderType type, IrExpr condition, IrExpr whenTrue, IrExpr whenFalse )
{
// select() takes a bool of the same shape as its operands. A scalar condition driving a
// vector result has to be splatted, and a non-boolean condition has to be converted.
var conditionType = condition?.Type ?? ShaderType.Bool;
var wanted = type.IsScalarOrVector
? ShaderType.Vec( ScalarKind.Bool, Math.Max( 1, type.Components ) )
: ShaderType.Bool;
var text = Expr( condition );
if ( conditionType != wanted && !wanted.IsVoid )
{
text = $"{TypeName( wanted )}( {text} )";
}
return $"select( {text}, {Expr( whenTrue )}, {Expr( whenFalse )} )";
}
string BinaryText( IrBinary binary, int minPrecedence )
{
var operandType = binary.L?.Type ?? binary.R?.Type ?? binary.Type;
if ( SlangIntrinsics.RequiresFunctionForm( binary.Op, operandType ) )
{
return $"{SlangIntrinsics.FunctionForm( binary.Op )}( {Expr( binary.L )}, {Expr( binary.R )} )";
}
var precedence = BinaryOps.Precedence( binary.Op ) * 5;
var text = $"{Expr( binary.L, precedence )} {SlangIntrinsics.Symbol( binary.Op )} {Expr( binary.R, precedence + 1 )}";
return Wrap( text, precedence, minPrecedence );
}
string CastText( IrCast cast )
{
if ( cast.V is null ) return SlangIntrinsics.Zero( cast.Type );
var target = cast.Type;
var source = cast.V.Type;
if ( source == target ) return Expr( cast.V );
switch ( cast.Kind )
{
case CastKind.Pad:
{
var missing = target.Components - source.Components;
if ( missing <= 0 ) break;
var fill = SlangIntrinsics.Scalar( cast.Fill, target.Scalar );
var parts = new List<string> { Expr( cast.V ) };
for ( int i = 0; i < missing; i++ ) parts.Add( fill );
return $"{TypeName( target )}( {string.Join( ", ", parts )} )";
}
case CastKind.Truncate:
{
if ( !source.IsScalarOrVector || !target.IsScalarOrVector ) break;
if ( target.Components >= source.Components ) break;
var swizzled = $"{Expr( cast.V, PrecPrimary )}{TypeRules.SwizzleFor( target.Components )}";
return target.Scalar == source.Scalar ? swizzled : $"{TypeName( target )}( {swizzled} )";
}
case CastKind.Bitcast:
return BitcastText( cast, target );
}
return $"{TypeName( target )}( {Expr( cast.V )} )";
}
string BitcastText( IrCast cast, ShaderType target )
{
var inner = Expr( cast.V );
return target.Scalar switch
{
ScalarKind.Float => $"asfloat( {inner} )",
ScalarKind.Int => $"asint( {inner} )",
ScalarKind.UInt => $"asuint( {inner} )",
_ => $"reinterpret< {TypeName( target )} >( {inner} )"
};
}
static string Mask( IrSwizzle swizzle )
{
var mask = swizzle.Mask;
if ( string.IsNullOrEmpty( mask ) ) return "x";
var builder = new StringBuilder( mask.Length );
foreach ( var c in mask )
{
var lower = char.ToLowerInvariant( c );
if ( lower is 'x' or 'y' or 'z' or 'w' or 'r' or 'g' or 'b' or 'a' ) builder.Append( lower );
}
return builder.Length == 0 ? "x" : builder.ToString();
}
string Args( IrExpr[] args )
{
if ( args is null || args.Length == 0 ) return string.Empty;
return string.Join( ", ", args.Select( x => Expr( x ) ) );
}
string Convert( string text, ShaderType from, ShaderType to )
{
if ( from == to || to.IsVoid ) return text;
if ( from.IsScalarOrVector && to.IsScalarOrVector && to.Components < from.Components )
{
var swizzled = $"{text}{TypeRules.SwizzleFor( to.Components )}";
return to.Scalar == from.Scalar ? swizzled : $"{TypeName( to )}( {swizzled} )";
}
return $"{TypeName( to )}( {text} )";
}
static string Wrap( string text, int precedence, int minPrecedence ) =>
precedence < minPrecedence ? $"( {text} )" : text;
string TypeName( ShaderType type )
{
if ( type.IsStruct && !string.IsNullOrEmpty( type.StructName ) )
{
return _structRename.TryGetValue( type.StructName, out var renamed )
? renamed
: SlangIntrinsics.SanitizeIdentifier( type.StructName, "PrismStruct" );
}
return SlangIntrinsics.TypeName( type );
}
static string Int( int value ) => value.ToString( CultureInfo.InvariantCulture );
/// <summary>Collapse text onto one line so it can never break out of a comment.</summary>
static string Single( string text )
{
if ( string.IsNullOrEmpty( text ) ) return string.Empty;
return text.Replace( "\r", string.Empty ).Replace( '\n', ' ' ).Replace( "*/", "* /" ).Trim();
}
/// <summary>Turn <c>g_flBaseRoughness</c> into <c>Base Roughness</c> for the material inspector.</summary>
static string SplitWords( string name )
{
var identifier = SlangIntrinsics.PascalCase( name );
if ( string.IsNullOrEmpty( identifier ) ) return string.Empty;
var builder = new StringBuilder( identifier.Length + 8 );
for ( int i = 0; i < identifier.Length; i++ )
{
var c = identifier[i];
if ( c == '_' )
{
if ( builder.Length > 0 && builder[^1] != ' ' ) builder.Append( ' ' );
continue;
}
if ( i > 0 && char.IsUpper( c ) && !char.IsUpper( identifier[i - 1] ) && builder.Length > 0 )
{
builder.Append( ' ' );
}
builder.Append( c );
}
return builder.ToString().Trim();
}
// ---- traversal -----------------------------------------------------
IEnumerable<IrExpr> AllExpressions()
{
foreach ( var function in _module.Functions )
{
if ( function is null ) continue;
foreach ( var expr in Expressions( function.Body ) ) yield return expr;
}
}
static IEnumerable<IrStmt> Statements( IrBlock block )
{
if ( block is null ) yield break;
foreach ( var statement in block.Statements )
{
yield return statement;
switch ( statement )
{
case IrIf branch:
foreach ( var nested in Statements( branch.Then ) ) yield return nested;
foreach ( var nested in Statements( branch.Else ) ) yield return nested;
break;
case IrFor loop:
foreach ( var nested in Statements( loop.Body ) ) yield return nested;
break;
case IrWhile loop:
foreach ( var nested in Statements( loop.Body ) ) yield return nested;
break;
case IrScope scope:
foreach ( var nested in Statements( scope.Body ) ) yield return nested;
break;
case IrPreprocessorIf guard:
foreach ( var nested in Statements( guard.Then ) ) yield return nested;
foreach ( var nested in Statements( guard.Else ) ) yield return nested;
break;
}
}
}
static IEnumerable<IrExpr> Expressions( IrBlock block )
{
foreach ( var statement in Statements( block ) )
{
switch ( statement )
{
case IrDecl decl:
foreach ( var expr in IrExprUtil.Walk( decl.Init ) ) yield return expr;
break;
case IrAssign assign:
foreach ( var expr in IrExprUtil.Walk( assign.Target ) ) yield return expr;
foreach ( var expr in IrExprUtil.Walk( assign.Value ) ) yield return expr;
break;
case IrIf branch:
foreach ( var expr in IrExprUtil.Walk( branch.Cond ) ) yield return expr;
break;
case IrFor loop:
foreach ( var expr in IrExprUtil.Walk( loop.Count ) ) yield return expr;
break;
case IrWhile loop:
foreach ( var expr in IrExprUtil.Walk( loop.Cond ) ) yield return expr;
break;
case IrReturn ret:
foreach ( var expr in IrExprUtil.Walk( ret.Value ) ) yield return expr;
break;
case IrExprStmt expression:
foreach ( var expr in IrExprUtil.Walk( expression.Value ) ) yield return expr;
break;
}
}
}
}
}