Editor/Prism/Compiler/CompileResult.cs

Data model for compile outputs used by the editor. Defines preview uniform/texture records (PreviewAttribute, PreviewTexture), compile statistics (CompileStats), and the CompileResult record that holds artifacts, diagnostics, stats, preview data and helper accessors.

File Access
using Editor.Prism.Compiler.Backends;
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;

namespace Editor.Prism.Compiler;

/// <summary>
/// A uniform the preview can push straight to the GPU. In <see cref="CompileMode.Preview"/> every
/// literal and every graph parameter becomes one of these, which is why dragging a slider costs zero
/// compiles. Pushed with a dictionary indexer, never <c>Dictionary.Add</c> — the built-in editor's
/// attribute helper throws on a duplicate name.
/// </summary>
public sealed record PreviewAttribute( string Name, ShaderType Type, ConstValue Value )
{
	/// <summary>The node whose literal this is, when it came from one.</summary>
	public NodeId Node { get; init; }

	/// <summary>The port whose literal this is, when it came from one.</summary>
	public PortId Port { get; init; }

	/// <summary>The blackboard parameter this came from, when it came from one.</summary>
	public ParamId Parameter { get; init; }

	/// <inheritdoc/>
	public override string ToString() => $"{Type.Hlsl} {Name} = {Value}";
}

/// <summary>
/// A texture slot the preview has to fill itself, because nothing bakes it for a shader rendered
/// without a material. See <c>NodeEmitter.PreviewTextureBinding</c> for why this exists.
/// </summary>
/// <param name="Name">The render-attribute name the shader binds the slot to.</param>
/// <param name="Asset">Path of the source image the graph asked for.</param>
/// <param name="Srgb">True when the slot holds sRGB-encoded colour rather than linear data.</param>
public sealed record PreviewTexture( string Name, string Asset, bool Srgb )
{
	/// <summary>The blackboard parameter behind the slot, when it came from one.</summary>
	public ParamId Parameter { get; init; }

	/// <inheritdoc/>
	public override string ToString() => $"{Name} = \"{Asset}\"{( Srgb ? " (srgb)" : string.Empty )}";
}

/// <summary>Counters for the status bar and for spotting performance regressions between builds.</summary>
public sealed record CompileStats
{
	/// <summary>Nodes visited during emission.</summary>
	public int NodeCount { get; init; }

	/// <summary>Statements in the emitted module.</summary>
	public int StatementCount { get; init; }

	/// <summary>Temps bound by the emitter after CSE.</summary>
	public int TempCount { get; init; }

	/// <summary>Expressions removed by CSE, folding and dead-code elimination.</summary>
	public int OptimizedAway { get; init; }

	/// <summary>Module-level declarations emitted.</summary>
	public int GlobalCount { get; init; }

	/// <summary>Interpolators allocated.</summary>
	public int VaryingCount { get; init; }

	/// <summary>Helper functions emitted.</summary>
	public int HelperCount { get; init; }

	/// <summary>Milliseconds spent in validation, solving and stage planning.</summary>
	public double AnalysisMs { get; init; }

	/// <summary>Milliseconds spent building and optimising the IR.</summary>
	public double EmitMs { get; init; }

	/// <summary>Milliseconds spent in the backends.</summary>
	public double BackendMs { get; init; }

	/// <summary>Total wall time of the compile.</summary>
	public double TotalMs { get; init; }

	/// <inheritdoc/>
	public override string ToString() =>
		$"{NodeCount} nodes, {StatementCount} statements, {TotalMs:0} ms";
}

/// <summary>
/// The result of a compile: one artifact per requested backend, everything that went wrong, the
/// uniforms the preview can push live, and the counters for the status bar.
/// </summary>
public sealed record CompileResult(
	bool Ok,
	IReadOnlyDictionary<string, BackendEmitResult> Artifacts,
	IReadOnlyList<Diagnostic> Diagnostics,
	IReadOnlyList<PreviewAttribute> PreviewAttributes,
	CompileStats Stats )
{
	/// <summary>The IR the artifacts were generated from. Kept so the code panel can print it.</summary>
	public IrModule Module { get; init; }

	/// <summary>
	/// Texture slots the preview must push itself. Empty for every mode but
	/// <see cref="CompileMode.Preview"/>, where a shipping <c>CreateInputTexture2D</c> slot would never
	/// be filled because nothing compiles a material for the preview.
	/// </summary>
	public IReadOnlyList<PreviewTexture> PreviewTextures { get; init; } = Array.Empty<PreviewTexture>();

	/// <summary>The request this result answers.</summary>
	public CompileRequest Request { get; init; }

	/// <summary>Number of errors reported.</summary>
	public int ErrorCount => Diagnostics?.Count( x => x.Severity == DiagnosticSeverity.Error ) ?? 0;

	/// <summary>Number of warnings reported.</summary>
	public int WarningCount => Diagnostics?.Count( x => x.Severity == DiagnosticSeverity.Warning ) ?? 0;

	/// <summary>The artifact produced by a backend, or null when that backend was not requested.</summary>
	public BackendEmitResult Artifact( string backendId ) =>
		Artifacts is not null && Artifacts.TryGetValue( backendId, out var result ) ? result : null;

	/// <summary>The generated <c>.shader</c> text, when the s&amp;box backend ran.</summary>
	public string ShaderText => Artifact( PrismConstants.BackendHlsl )?.Text;

	/// <summary>The generated <c>.slang</c> text, when the Slang backend ran.</summary>
	public string SlangText => Artifact( PrismConstants.BackendSlang )?.Text;

	/// <summary>A failed result carrying only diagnostics.</summary>
	public static CompileResult Failed( IReadOnlyList<Diagnostic> diagnostics, CompileRequest request = null ) =>
		new( false, new Dictionary<string, BackendEmitResult>(), diagnostics ?? Array.Empty<Diagnostic>(),
			Array.Empty<PreviewAttribute>(), new CompileStats() )
		{
			Request = request
		};

	/// <inheritdoc/>
	public override string ToString() =>
		$"{( Ok ? "ok" : "failed" )}: {ErrorCount} errors, {WarningCount} warnings, {Stats}";
}