A static helper that embeds a Slang shader runtime module used by the Prism compiler backend. It provides module name, filename, import statement, environment and material struct names, the prelude shader source (with configurable newline normalization), and a GeneratedArtifact factory.
using Editor.Prism.Core;
namespace Editor.Prism.Compiler.Backends;
/// <summary>
/// The small <c>prism.core</c> Slang module every generated Prism module imports.
/// <para>
/// It carries the three things a standalone <c>.slang</c> artifact cannot get from the engine: the
/// environment parameter block (camera, viewport, object transform, time), the handful of math and
/// colour-space helpers the emitted code calls into, and the <c>PrismMaterial</c> struct a surface
/// graph fills in. It is emitted as a <see cref="GeneratedArtifact"/> beside the main module, so the
/// pair compiles with nothing but <c>slangc</c> and an include path.
/// </para>
/// </summary>
public static class SlangRuntimeModule
{
/// <summary>The module name an emitted Prism module imports.</summary>
public const string ModuleName = PrismConstants.SlangRuntimeModule;
/// <summary>
/// Path of the emitted file, relative to the main module. <c>import prism.core;</c> resolves a
/// dotted module name to this path, so the directory is part of the contract.
/// </summary>
public const string FileName = "prism/core.slang";
/// <summary>The import statement an emitted module writes.</summary>
public const string ImportStatement = "import " + ModuleName + ";";
/// <summary>Name of the environment parameter block this module declares.</summary>
public const string EnvironmentBlock = SlangIntrinsics.EnvironmentBlock;
/// <summary>Name of the material struct a surface graph fills in.</summary>
public const string MaterialStruct = "PrismMaterial";
/// <summary>The prelude source, with CRLF line endings.</summary>
public static string Source => SourceWith( "\r\n" );
/// <summary>The prelude source with a chosen line ending.</summary>
public static string SourceWith( string newLine )
{
if ( string.IsNullOrEmpty( newLine ) ) newLine = "\r\n";
// The literal below picks up whatever line ending this file happens to be saved with, so it is
// normalised before substituting. Without this a CRLF source would emit CR CR LF.
var normalised = s_source.Replace( "\r\n", "\n" ).Replace( '\r', '\n' );
return newLine == "\n" ? normalised : normalised.Replace( "\n", newLine );
}
/// <summary>
/// The prelude packaged as an artifact the backend returns alongside its main result. It is
/// written beside the saved document, which is also where the module's include path points.
/// </summary>
public static GeneratedArtifact Artifact( string newLine = "\r\n" ) =>
new( FileName, SourceWith( newLine ) ) { BesideDocument = true };
// The source is stored with plain LF and normalised on the way out, so the literal below stays
// readable and the emitted file still honours BackendEmitOptions.NewLine.
const string s_source = """
#language slang 2026
module "prism/core";
// =============================================================================
// prism.core - the shared prelude for Prism-generated Slang modules
//
// Generated by Prism. Editing this file is fine, but regenerating a graph
// overwrites it: keep local changes in a module of your own and import both.
//
// Contents
// 1. Material-UI attributes - reflected into `-reflection-json` userAttribs
// 2. Environment - camera, viewport, object transform, time
// 3. Transforms - object/world/clip space conversions
// 4. Math - the safe-by-default helpers emitted code calls
// 5. Textures - value-returning wrappers over out-param methods
// 6. Colour - sRGB, HSV and luminance
// 7. PrismMaterial - what a surface graph fills in
// =============================================================================
// -----------------------------------------------------------------------------
// 1. Material-UI attributes
//
// Prism annotates every generated shader parameter with these. They carry no
// runtime cost: `slangc -reflection-json` reports them under "userAttribs",
// which is how a host application rebuilds the material inspector.
// -----------------------------------------------------------------------------
/// Display name of a parameter.
[__AttributeUsage( _AttributeTargets.Var )]
public struct UiLabelAttribute { string text; }
/// Group heading and sort order in the material inspector.
[__AttributeUsage( _AttributeTargets.Var )]
public struct UiGroupAttribute { string group; int order; }
/// Inclusive numeric range of a slider.
[__AttributeUsage( _AttributeTargets.Var )]
public struct UiRangeAttribute { float min; float max; }
/// Which editor to show: slider, color, toggle, dropdown, vector, texture.
[__AttributeUsage( _AttributeTargets.Var )]
public struct UiControlAttribute { string control; }
/// Hover text.
[__AttributeUsage( _AttributeTargets.Var )]
public struct UiTooltipAttribute { string text; }
/// Default value, splatted across the parameter's components.
[__AttributeUsage( _AttributeTargets.Var )]
public struct UiDefaultAttribute { float x; float y; float z; float w; }
/// Default asset path for a texture parameter.
[__AttributeUsage( _AttributeTargets.Var )]
public struct UiAssetAttribute { string path; }
/// Render-attribute name, so a host can push a value without recompiling.
[__AttributeUsage( _AttributeTargets.Var )]
public struct PrismAttributeAttribute { string name; }
/// Non-zero when a texture's contents are sRGB encoded.
[__AttributeUsage( _AttributeTargets.Var )]
public struct PrismSrgbAttribute { int srgb; }
// -----------------------------------------------------------------------------
// 2. Environment
//
// Everything a shader knows about the frame and the object it is drawing.
// A ParameterBlock gets its own descriptor set / register space, so binding it
// once per frame and once per object is the natural split for a host renderer.
// -----------------------------------------------------------------------------
/// Per-frame constants.
public struct PrismFrameParams
{
float4x4 WorldToView;
float4x4 ViewToProjection;
float4x4 WorldToProjection;
float3 CameraPosition;
float CameraNear;
float3 CameraForward;
float CameraFar;
float2 ViewportSize;
float2 ViewportInvSize;
float2 ViewportOffset;
float3 SunDirection;
float3 SunColor;
float Time;
float DeltaTime;
int FrameCount;
}
/// Per-object constants.
public struct PrismObjectParams
{
float4x4 ObjectToWorld;
float4x4 WorldToObject;
float3 ObjectOrigin;
float3 ObjectScale;
float4 TintColor;
}
/// The environment a Prism module is rendered in.
public struct PrismEnvironment
{
PrismFrameParams Frame;
PrismObjectParams Object;
}
/// The one environment binding every generated module reads from.
public ParameterBlock<PrismEnvironment> gPrismEnv;
// -----------------------------------------------------------------------------
// 3. Transforms
// -----------------------------------------------------------------------------
/// Object space to world space, as a position.
public float3 PrismObjectToWorldPoint( float3 positionOs )
{
return mul( gPrismEnv.Object.ObjectToWorld, float4( positionOs, 1.0 ) ).xyz;
}
/// Object space to world space, as a direction. Not normalised; scale is preserved.
public float3 PrismObjectToWorldDirection( float3 directionOs )
{
return mul( gPrismEnv.Object.ObjectToWorld, float4( directionOs, 0.0 ) ).xyz;
}
/// Object space to world space, as a normal. Uses the inverse transpose, so non-uniform scale is safe.
public float3 PrismObjectToWorldNormal( float3 normalOs )
{
return normalize( mul( float4( normalOs, 0.0 ), gPrismEnv.Object.WorldToObject ).xyz );
}
/// World space to object space, as a position.
public float3 PrismWorldToObjectPoint( float3 positionWs )
{
return mul( gPrismEnv.Object.WorldToObject, float4( positionWs, 1.0 ) ).xyz;
}
/// World space to clip space.
public float4 PrismWorldToClip( float3 positionWs )
{
return mul( gPrismEnv.Frame.WorldToProjection, float4( positionWs, 1.0 ) );
}
/// Clip space to a 0..1 screen UV, with the origin in the top left.
public float2 PrismScreenUvFromClip( float4 positionPs )
{
float2 ndc = positionPs.xy / max( abs( positionPs.w ), 1.0e-6 );
return ndc * float2( 0.5, -0.5 ) + 0.5;
}
// -----------------------------------------------------------------------------
// 4. Math
//
// The emitted code prefers these over the raw intrinsics wherever a zero or a
// denormal would otherwise produce a NaN that is invisible until it is not.
// -----------------------------------------------------------------------------
/// Normalise, returning a zero vector instead of a NaN for a zero-length input.
public float3 PrismSafeNormalize( float3 v )
{
float lengthSquared = dot( v, v );
return lengthSquared > 1.0e-12 ? v * rsqrt( lengthSquared ) : float3( 0.0 );
}
/// Normalise a 2D vector, returning zero instead of a NaN for a zero-length input.
public float2 PrismSafeNormalize( float2 v )
{
float lengthSquared = dot( v, v );
return lengthSquared > 1.0e-12 ? v * rsqrt( lengthSquared ) : float2( 0.0 );
}
/// Reciprocal that returns zero rather than an infinity at zero.
public float PrismSafeRcp( float v )
{
return abs( v ) > 1.0e-12 ? 1.0 / v : 0.0;
}
/// Divide, returning zero rather than a NaN or an infinity when the denominator vanishes.
public float3 PrismSafeDivide( float3 a, float3 b )
{
bool3 ok = abs( b ) > float3( 1.0e-12 );
float3 divisor = select( ok, b, float3( 1.0 ) );
return select( ok, a / divisor, float3( 0.0 ) );
}
/// Linear remap from one inclusive range to another. Both ranges are packed as (min, max).
public float PrismRemap( float value, float2 fromRange, float2 toRange )
{
float t = ( value - fromRange.x ) * PrismSafeRcp( fromRange.y - fromRange.x );
return lerp( toRange.x, toRange.y, t );
}
/// Build a tangent-to-world basis from an interpolated normal and tangent.
public float3x3 PrismTangentBasis( float3 normalWs, float3 tangentUWs, float3 tangentVWs )
{
float3 n = PrismSafeNormalize( normalWs );
float3 t = PrismSafeNormalize( tangentUWs - n * dot( n, tangentUWs ) );
float3 b = PrismSafeNormalize( tangentVWs );
return float3x3( t, b, n );
}
// -----------------------------------------------------------------------------
// 5. Textures
//
// GetDimensions writes through out parameters and therefore cannot appear in an
// expression. These wrappers give the graph a value it can feed into math.
// -----------------------------------------------------------------------------
/// Width and height of a 2D texture, in texels.
public float2 PrismTextureSize( Texture2D texture )
{
uint width, height;
texture.GetDimensions( width, height );
return float2( width, height );
}
/// Width, height and slice count of a 2D texture array, in texels.
public float3 PrismTextureSize( Texture2DArray texture )
{
uint width, height, slices;
texture.GetDimensions( width, height, slices );
return float3( width, height, slices );
}
/// Width, height and depth of a 3D texture, in texels.
public float3 PrismTextureSize( Texture3D texture )
{
uint width, height, depth;
texture.GetDimensions( width, height, depth );
return float3( width, height, depth );
}
/// Face width and height of a cube map, in texels.
public float2 PrismTextureSize( TextureCube texture )
{
uint width, height;
texture.GetDimensions( width, height );
return float2( width, height );
}
// -----------------------------------------------------------------------------
// 6. Colour
// -----------------------------------------------------------------------------
/// sRGB to linear, using the exact piecewise transfer function.
public float3 PrismSrgbToLinear( float3 srgb )
{
float3 low = srgb / 12.92;
float3 high = pow( max( ( srgb + 0.055 ) / 1.055, 0.0 ), 2.4 );
return select( srgb <= float3( 0.04045 ), low, high );
}
/// sRGB to linear, leaving alpha alone.
public float4 PrismSrgbToLinear( float4 srgb )
{
return float4( PrismSrgbToLinear( srgb.rgb ), srgb.a );
}
/// Linear to sRGB, using the exact piecewise transfer function.
public float3 PrismLinearToSrgb( float3 linearColor )
{
float3 low = linearColor * 12.92;
float3 high = 1.055 * pow( max( linearColor, 0.0 ), 1.0 / 2.4 ) - 0.055;
return select( linearColor <= float3( 0.0031308 ), low, high );
}
/// Linear to sRGB, leaving alpha alone.
public float4 PrismLinearToSrgb( float4 linearColor )
{
return float4( PrismLinearToSrgb( linearColor.rgb ), linearColor.a );
}
/// Rec. 709 relative luminance of a linear colour.
public float PrismLuminance( float3 linearColor )
{
return dot( linearColor, float3( 0.2126, 0.7152, 0.0722 ) );
}
/// RGB to HSV. Hue is 0..1, not degrees.
public float3 PrismRgbToHsv( float3 rgb )
{
const float4 k = float4( 0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0 );
const float epsilon = 1.0e-10;
float4 p = select( bool4( rgb.g < rgb.b ), float4( rgb.bg, k.wz ), float4( rgb.gb, k.xy ) );
float4 q = select( bool4( rgb.r < p.x ), float4( p.xyw, rgb.r ), float4( rgb.r, p.yzx ) );
float chroma = q.x - min( q.w, q.y );
return float3( abs( q.z + ( q.w - q.y ) / ( 6.0 * chroma + epsilon ) ), chroma / ( q.x + epsilon ), q.x );
}
/// HSV to RGB. Hue is 0..1, not degrees.
public float3 PrismHsvToRgb( float3 hsv )
{
const float4 k = float4( 1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0 );
float3 p = abs( frac( hsv.xxx + k.xyz ) * 6.0 - k.www );
return hsv.z * lerp( k.xxx, saturate( p - k.xxx ), hsv.y );
}
/// Blend two linear colours with the classic overlay operator.
public float3 PrismOverlay( float3 baseColor, float3 blend )
{
float3 low = 2.0 * baseColor * blend;
float3 high = 1.0 - 2.0 * ( 1.0 - baseColor ) * ( 1.0 - blend );
return select( baseColor <= float3( 0.5 ), low, high );
}
// -----------------------------------------------------------------------------
// 7. PrismMaterial
//
// What a surface graph produces. A host renderer reads these fields and runs
// whatever shading model it likes; `ToUnlitColor` is the trivial one.
// -----------------------------------------------------------------------------
/// The surface description a Prism surface graph fills in.
public struct PrismMaterial
{
/// Linear base colour.
float3 Albedo;
/// Coverage. Compared against the alpha-test threshold for a masked material.
float Opacity;
/// Tangent-space normal, with the usual (0, 0, 1) meaning "unperturbed".
float3 Normal;
/// Perceptual roughness, 0 mirror to 1 fully rough.
float Roughness;
/// Metalness, 0 dielectric to 1 conductor.
float Metalness;
/// Baked ambient occlusion.
float AmbientOcclusion;
/// Linear emissive radiance.
float3 Emission;
/// Light transmitted through the surface.
float3 Transmission;
/// Where a per-instance tint applies.
float TintMask;
/// A sensible neutral surface: white, opaque, flat, rough, dielectric.
public static PrismMaterial Init()
{
PrismMaterial m;
m.Albedo = float3( 1.0 );
m.Opacity = 1.0;
m.Normal = float3( 0.0, 0.0, 1.0 );
m.Roughness = 1.0;
m.Metalness = 0.0;
m.AmbientOcclusion = 1.0;
m.Emission = float3( 0.0 );
m.Transmission = float3( 0.0 );
m.TintMask = 1.0;
return m;
}
/// Replace the tangent-space normal. Mutates, so it carries [mutating].
[mutating]
public void SetNormal( float3 tangentSpaceNormal )
{
Normal = PrismSafeNormalize( tangentSpaceNormal );
}
/// Kill the fragment when coverage falls below a threshold. Pixel stage only.
public void AlphaTest( float threshold )
{
if ( Opacity < threshold ) discard;
}
/// The world-space normal implied by this material's tangent-space normal.
public float3 WorldNormal( float3x3 tangentBasis )
{
return PrismSafeNormalize( mul( Normal, tangentBasis ) );
}
/// The unlit resolve: albedo plus emission, with coverage in alpha.
public float4 ToUnlitColor()
{
return float4( Albedo + Emission, Opacity );
}
}
""";
}