using Lib.render._dx11.api;
using T3.Core.DataTypes.ShaderGraph;
using T3.Core.Rendering;
using T3.Core.Stats;
namespace Lib.field.render._;
/** *
* Handles triggering the generation of shader graph code and updating the float parameters.
*
* Also see for more documentation.
*
*
* The focus of this is to avoid all unnecessary updates or even recompilations. For this we
* follow the following strategy:
*
* ## Field is not dirty:
* No Updates necessary. All parameters code and graph are already up to date.
*
* ## Field is dirty:
* Update the invalidated part of connected FieldGraph-instances and their GraphNodes. Each GraphNode
* will fetch is updated float parameters. It will try to detected changes in the graph setup by comparing
* if the connected graph nodes have changed.
*
* They will recursively return an invalidate settings for CodeNeedsUpdate and ParamsNeedUpdate.
*
* If CodeNeedsUpdate is true, the shader code will be updated.
*/
[Guid("73c028d1-3de2-4269-b503-97f62bbce320")]
internal sealed class GenerateShaderGraphCode : Instance
,IStatusProvider
{
[Output(Guid = "A1AB0C16-ED15-4334-A529-10E3C217DF1A")]
public readonly Slot ShaderCode = new();
[Output(Guid = "1a9b5e15-e9a7-4ed4-aa1a-2072398921b4")]
public readonly Slot FloatParams = new();
// This is a hack so the additional resources can be shared to ops like [SetVertexAndPixelShaderStage]
[Output(Guid = "ADF247CD-79CC-4D4E-B3C1-6A8B2D54683D")]
public readonly Slot Resources = new();
public GenerateShaderGraphCode()
{
_fileContents = new Resource(TemplateFilePath, TryLoadTemplate);
_fileContents.AddDependentSlots(ShaderCode, FloatParams, Resources);
_fileContents.Changed += TemplateChanged;
ShaderCode.UpdateAction += Update;
_graphId = GetHashCode();
}
private void TemplateChanged()
{
_templateChanged = true;
}
private bool _templateChanged = false;
///
/// This is only updated if subgraph has been changed.
///
private void Update(EvaluationContext context)
{
_lastErrorMessage = null;
var hasTemplateChanged = _templateChanged;
var definesAreDirty = AdditionalDefines.DirtyFlag.IsDirty;
if (definesAreDirty)
{
var additionalDefines = AdditionalDefines.GetValue(context);
if (additionalDefines != _additionalDefines)
{
_additionalDefines = additionalDefines;
hasTemplateChanged= true;
}
}
var templateCode = _fileContents.GetValue(context);// TemplateCode.GetValue(context);
if (string.IsNullOrEmpty(templateCode))
{
this.LogErrorState("Missing input template code");
return;
}
if (_needsInvalidation)
{
_needsInvalidation = false;
}
_needsInvalidation = hasTemplateChanged;
// Recursively update complete shader graph and collect changes
_graphNode = Field.GetValue(context);
if (_graphNode == null)
{
_needsInvalidation = true;
ShaderCode.Value = templateCode;
return;
}
var changes = _graphNode.CollectedChanges;
if (_graphNode.StructureHash != _lastStructureHash)
{
_lastStructureHash = _graphNode.StructureHash;
changes |= ShaderGraphNode.ChangedFlags.Structural;
}
if (changes == ShaderGraphNode.ChangedFlags.None && !hasTemplateChanged)
return;
AssembleParams();
AssembleResources();
Resources.Value = _resourceViews;
var floatParams = AllFloatValues;
if (floatParams.Count > 0)
{
CreateParameterBuffer(FloatParams, floatParams);
}
if (hasTemplateChanged || (changes & (ShaderGraphNode.ChangedFlags.Code|ShaderGraphNode.ChangedFlags.Structural)) != 0)
{
ShaderCode.Value = GenerateShaderCode(_additionalDefines + "\n\n" + templateCode); // Should probably use a string builder here...
}
_templateChanged = false;
_updateCycleCount++;
}
private string GenerateShaderCode(string templateCode)
{
AssembleAndInjectParameters(ref templateCode);
AssembleAndInjectCode(ref templateCode);
InjectResourcesCode(ref templateCode);
return templateCode;
}
private void AssembleAndInjectParameters(ref string templateCode)
{
var commentHook = ToHlslTemplateTag("FLOAT_PARAMS");
if (!templateCode.Contains(commentHook))
return;
var sb = new StringBuilder();
foreach (var name in _allShaderCodeParams)
{
sb.AppendLine($"\t{name.ShaderTypeName} {name.Name};");
}
templateCode = templateCode.Replace(commentHook, sb.ToString());
}
/**
* Before generating the complete shader, we need to collect all parameters of all nodes
* into to lists: One for the float constant buffer and one of parameter names and definition
*/
private void AssembleParams()
{
_allFloatParameterValues.Clear();
_allShaderCodeParams.Clear();
_graphNode.CollectAllNodeParams(_allFloatParameterValues, _allShaderCodeParams, _updateCycleCount, _graphId);
}
// Should change if resources structure changed
private void AssembleResources()
{
_resourceReferences.Clear();
_graphNode.CollectResources(_resourceReferences, _updateCycleCount,_graphId);
_resourceViews.Clear();
foreach (var r in _resourceReferences)
{
_resourceViews.Add(r.Srv);
}
}
private readonly List _resourceReferences = [];
private readonly List _resourceViews = [];
private readonly List _allShaderCodeParams = [];
private IReadOnlyList AllFloatValues => _allFloatParameterValues;
private readonly List _allFloatParameterValues = [];
private int _lastStructureHash;
private readonly int _graphId;
private void AssembleAndInjectCode(ref string templateCode)
{
_codeAssembleContext.Reset();
// Recursively collect code from connected nodes
_graphNode.CollectEmbeddedShaderCode(_codeAssembleContext);
// Build and inject definitions (must be injected before resources to support custom struct type definition)
_definitionsBuilder.Clear();
_definitionsBuilder.AppendLine("// --- globals -------------------");
foreach (var code in _codeAssembleContext.Globals.Values)
{
_definitionsBuilder.AppendLine(code);
_definitionsBuilder.AppendLine("");
}
_definitionsBuilder.AppendLine(_codeAssembleContext.Definitions.ToString());
TryInject("GLOBALS", ref templateCode, _definitionsBuilder.ToString());
// Build and inject functions...
_functionsBuilder.Clear();
_functionsBuilder.AppendLine("// --- instance functions -------------------");
TryInject("FIELD_FUNCTIONS", ref templateCode, _functionsBuilder.ToString());
// Build and inject calls...
TryInject("FIELD_CALL", ref templateCode, _codeAssembleContext.Calls.ToString());
}
///
/// Injects resource definitions starting with the giving index defined by the look like...
///
/// /*{RESOURCES(t6)}*/
///
/// with definitions like...
///
/// StructuredBuffer<Point> Points : register(t6);
///
///
///
///
private void InjectResourcesCode(ref string templateCode)
{
// Find target span...
const string resourcesStartHook = "/*{RESOURCES(";
var resourceHookIndex = templateCode.IndexOf(resourcesStartHook, StringComparison.Ordinal);
if (resourceHookIndex == -1)
return;
var subIndex = resourceHookIndex + resourcesStartHook.Length;
const string resourcesEndHook = ")}*/";
var resourceHookEndIndex = templateCode.IndexOf(resourcesEndHook, resourceHookIndex, StringComparison.Ordinal);
if (resourceHookEndIndex == -1)
return;
var t = templateCode[subIndex];
var indexSpan = templateCode.AsSpan(subIndex + 1, resourceHookEndIndex - subIndex-1);
var xxx = indexSpan.ToString();
if (!int.TryParse(xxx, out var index))
return;
// Build and inject...
_resourceDefinitionsBuilder.Clear();
// ... resource definition (like structs, etc)
foreach (var rd in _codeAssembleContext.ResourceTypes.Values)
{
_resourceDefinitionsBuilder.AppendLine(rd);
}
// ... resource registrations
foreach (var rr in _resourceReferences)
{
_resourceDefinitionsBuilder.AppendLine($"{rr.Definition}:register({t}{index});");
index++;
}
var resourceHook = templateCode.AsSpan(resourceHookIndex, resourceHookEndIndex + resourcesEndHook.Length- resourceHookIndex ).ToString();
templateCode = templateCode.Replace(resourceHook, _resourceDefinitionsBuilder.ToString());
}
private static void TryInject(string hookId, ref string code, string insert)
{
var hookString = ToHlslTemplateTag(hookId);
// Avoid ArgumentException
if (string.IsNullOrEmpty(code))
return;
code = code.Replace(hookString, insert);
}
private readonly CodeAssembleContext _codeAssembleContext = new();
private int _updateCycleCount;
private static readonly StringBuilder _definitionsBuilder = new();
private static readonly StringBuilder _functionsBuilder = new();
private static readonly StringBuilder _resourceDefinitionsBuilder = new();
private static void CreateParameterBuffer(Slot floatSlotBuffer, IReadOnlyList floatParams)
{
try
{
if (floatParams == null || floatParams.Count == 0)
return;
var arraySize = floatParams.Count;
var array = new float[arraySize];
for (var i = 0; i < floatParams.Count; i++)
{
array[i] = floatParams[i];
}
var device = ResourceManager.Device;
var size = sizeof(int) * array.Length;
if (ResourceUtils.GetDynamicConstantBuffer(device, ref floatSlotBuffer.Value, size))
{
floatSlotBuffer.Value.DebugName = nameof(FloatsToBuffer); // no need to copy string every frame if constant
}
ResourceUtils.WriteDynamicBufferData(device.ImmediateContext, floatSlotBuffer.Value, array.AsSpan());
}
catch (Exception e)
{
Log.Warning("Failed to creat float value buffer" + e.Message);
}
}
private bool TryLoadTemplate(FileResource file, string currentValue, out string newValue, out string failureReason)
{
if (!file.TryOpenFileStream(out var stream, out failureReason, FileAccess.Read))
{
newValue = null;
return false;
}
try
{
using var fileStream = stream;
using var reader = new StreamReader(fileStream);
newValue = reader.ReadToEnd();
this.ClearErrorState();
_templateChanged = true;
return true;
}
catch (Exception e)
{
failureReason = $"Failed to read file {file.AbsolutePath}:" + e.Message;
this.LogErrorState( failureReason);
newValue = null;
return false;
}
}
#region Implementation of IStatusProvider
IStatusProvider.StatusLevel IStatusProvider.GetStatusLevel() =>
string.IsNullOrEmpty(_lastErrorMessage) ? IStatusProvider.StatusLevel.Success : IStatusProvider.StatusLevel.Warning;
string IStatusProvider.GetStatusMessage() => _lastErrorMessage;
private string _lastErrorMessage = string.Empty;
#endregion
private ShaderGraphNode _graphNode;
private bool _needsInvalidation = true;
private string _additionalDefines = "";
private readonly Resource _fileContents;
[Input(Guid = "FFC1C70E-B717-4337-916D-C3A13343E9CC")]
public readonly InputSlot Field = new();
[Input(Guid = "0B5CBBDF-AFE7-4DAF-9F1E-0E79792691F1")]
public readonly InputSlot TemplateFilePath = new();
[Input(Guid = "F6FB3BE8-53F2-4D68-BF0F-3F519BC09FF4")]
public readonly InputSlot AdditionalDefines = new();
public static string ToHlslTemplateTag(string hook)
{
return $"/*{{{hook}}}*/";
}
}