{"TotalCount":201,"Files":[{"Ident":"f4industries.prism","Path":"Editor/Prism/Compiler/Backends/BackendCapabilities.cs","FileName":"BackendCapabilities.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using Editor.Prism.Core;\r\n\r\nnamespace Editor.Prism.Compiler.Backends;\r\n\r\n/// \u003Csummary\u003E\r\n/// What a backend can express.\r\n/// \u003Cpara\u003E\r\n/// Consulted during \u003Cem\u003Evalidation\u003C/em\u003E, not at emit time, so the user is told \u0022this graph uses a\r\n/// loop, which the strict-HLSL dialect cannot express\u0022 long before anything is written to disk.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\npublic sealed record BackendCapabilities(\r\n\tShaderModel MaxShaderModel,\r\n\tStageMask Stages,\r\n\tbool Loops,\r\n\tbool RealBranching,\r\n\tbool StructMethods,\r\n\tbool Interpolators,\r\n\tbool Combos,\r\n\tint MaxVaryingSlots,\r\n\tint MaxSamplers )\r\n{\r\n\t/// \u003Csummary\u003ETrue when the backend can emit this stage.\u003C/summary\u003E\r\n\tpublic bool Supports( ShaderStage stage ) =\u003E Stages.Contains( stage );\r\n\r\n\t/// \u003Csummary\u003ETrue when the backend can provide a capability at its maximum shader model.\u003C/summary\u003E\r\n\tpublic bool Supports( Capability capability )\r\n\t{\r\n\t\tif ( Capabilities.MinShaderModel( capability ) \u003E MaxShaderModel ) return false;\r\n\r\n\t\treturn capability switch\r\n\t\t{\r\n\t\t\tCapability.Loops =\u003E Loops,\r\n\t\t\tCapability.DynamicBranching =\u003E RealBranching,\r\n\t\t\tCapability.StructMethods =\u003E StructMethods,\r\n\t\t\tCapability.Interpolators =\u003E Interpolators,\r\n\t\t\tCapability.Combos =\u003E Combos,\r\n\t\t\t_ =\u003E true\r\n\t\t};\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The s\u0026amp;box VFX target: SM 6.0 Vulkan, no hull or domain stage (the engine\u0027s block parser\r\n\t/// throws on those), everything else available.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static readonly BackendCapabilities Sbox = new(\r\n\t\tShaderModel.Sm6_0,\r\n\t\tStageMask.Vertex | StageMask.Pixel | StageMask.Geometry | StageMask.Compute,\r\n\t\tLoops: true,\r\n\t\tRealBranching: true,\r\n\t\tStructMethods: true,\r\n\t\tInterpolators: true,\r\n\t\tCombos: true,\r\n\t\tMaxVaryingSlots: PrismConstants.MaxVaryingSlots,\r\n\t\tMaxSamplers: PrismConstants.MaxSamplers );\r\n\r\n\t/// \u003Csummary\u003EThe portable Slang target: no engine combos, no engine interpolator budget.\u003C/summary\u003E\r\n\tpublic static readonly BackendCapabilities Slang = new(\r\n\t\tShaderModel.Sm6_5,\r\n\t\tStageMask.All,\r\n\t\tLoops: true,\r\n\t\tRealBranching: true,\r\n\t\tStructMethods: true,\r\n\t\tInterpolators: true,\r\n\t\tCombos: false,\r\n\t\tMaxVaryingSlots: 32,\r\n\t\tMaxSamplers: 32 );\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/Compiler/Backends/SboxShaderWriter.cs","FileName":"SboxShaderWriter.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using Editor.Prism.Compiler.Ir;\r\nusing Editor.Prism.Core;\r\n\r\nnamespace Editor.Prism.Compiler.Backends;\r\n\r\n/// \u003Csummary\u003E\r\n/// Wraps the HLSL an \u003Csee cref=\u0022HlslEmitter\u0022/\u003E produces in a complete s\u0026amp;box VFX\r\n/// \u003Cc\u003E.shader\u003C/c\u003E file.\r\n/// \u003Cpara\u003E\r\n/// A \u003Cc\u003E.shader\u003C/c\u003E is not plain HLSL: it is a block language the engine\u0027s native front-end parses\r\n/// before anything reaches a compiler. The block order, the placement of the blend defines relative\r\n/// to \u003Cc\u003Ecommon/pixel.hlsl\u003C/c\u003E, and the exact spelling of the annotation grammar all decide whether\r\n/// the result renders correctly, renders wrongly, or fails to parse with no diagnostic at all.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\npublic sealed class SboxShaderWriter\r\n{\r\n\treadonly HlslSourceBuilder _builder;\r\n\treadonly HlslEmitter _emitter;\r\n\treadonly IReadOnlyList\u003CHelperFunction\u003E _helpers;\r\n\r\n\t// Non-null only while writing an instrumented build. It doubles as the \u0022is this the real pass\u0022\r\n\t// flag, which is what keeps the throw-away numbering pass from instrumenting itself.\r\n\tIReadOnlyDictionary\u003CNodeId, int\u003E _stageIds;\r\n\r\n\t/// \u003Csummary\u003EPrepare to write one module.\u003C/summary\u003E\r\n\tpublic SboxShaderWriter( IrModule module, BackendEmitOptions options, DiagnosticSink diagnostics )\r\n\t{\r\n\t\tModule = module;\r\n\t\tOptions = options ?? BackendEmitOptions.Default;\r\n\t\tDiagnostics = diagnostics ?? new DiagnosticSink();\r\n\r\n\t\t_builder = new HlslSourceBuilder( Options.Indent, Options.NewLine );\r\n\t\t_emitter = new HlslEmitter( Module, Options, Diagnostics );\r\n\t\t_helpers = _emitter.OrderedHelpers();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe module being written.\u003C/summary\u003E\r\n\tpublic IrModule Module { get; }\r\n\r\n\t/// \u003Csummary\u003EEmission options.\u003C/summary\u003E\r\n\tpublic BackendEmitOptions Options { get; }\r\n\r\n\t/// \u003Csummary\u003EWhere problems go.\u003C/summary\u003E\r\n\tpublic DiagnosticSink Diagnostics { get; }\r\n\r\n\tModuleMetadata Meta =\u003E Module.Meta;\r\n\r\n\tShaderDomain Domain =\u003E Meta.Domain;\r\n\r\n\tbool IsSurface =\u003E Domain is ShaderDomain.Surface or ShaderDomain.PostProcess;\r\n\r\n\t/// \u003Csummary\u003EWrite the module as a complete \u003Cc\u003E.shader\u003C/c\u003E file, with its source map.\u003C/summary\u003E\r\n\tpublic BackendEmitResult Write()\r\n\t{\r\n\t\tif ( Module is null )\r\n\t\t{\r\n\t\t\tDiagnostics.Error( DiagnosticCode.InvalidBlock, \u0022There is no module to write.\u0022 );\r\n\t\t\treturn BackendEmitResult.Empty( PrismConstants.BackendHlsl, PrismConstants.ShaderExtension );\r\n\t\t}\r\n\r\n\t\tif ( Domain == ShaderDomain.Subgraph )\r\n\t\t{\r\n\t\t\tDiagnostics.Error( DiagnosticCode.SubgraphUnavailable,\r\n\t\t\t\t\u0022A subgraph has no shader of its own.\u0022, null,\r\n\t\t\t\t\u0022Subgraphs are inlined into the graph that instances them; only a shader or post-process graph produces a .shader file.\u0022 );\r\n\t\t\treturn BackendEmitResult.Empty( PrismConstants.BackendHlsl, PrismConstants.ShaderExtension );\r\n\t\t}\r\n\r\n\t\tif ( !VfxBlockValidator.Validate( Module, Diagnostics ) )\r\n\t\t{\r\n\t\t\t// The engine\u0027s block parser reports these only to the native log, with an empty program\r\n\t\t\t// list and no line numbers. Refusing to write is far kinder than letting that happen.\r\n\t\t\treturn BackendEmitResult.Empty( PrismConstants.BackendHlsl, PrismConstants.ShaderExtension );\r\n\t\t}\r\n\r\n\t\tif ( PreviewInstrumentation.IsEnabled( Options.Mode ) \u0026\u0026 Domain != ShaderDomain.Compute )\r\n\t\t{\r\n\t\t\t// A node\u0027s stage id is its rank among the nodes appearing in the finished artifact\u0027s source\r\n\t\t\t// map, so it cannot be known until the file has been written once. Writing it twice is far\r\n\t\t\t// cheaper and far safer than predicting that order: the throw-away pass costs one more text\r\n\t\t\t// generation and its diagnostics are discarded, because the real pass reports exactly the\r\n\t\t\t// same set. Adding the instrumentation never changes the order \u2014 every line it writes is\r\n\t\t\t// attributed either to a node that already appeared above it, or to nothing at all.\r\n\t\t\tvar probe = new SboxShaderWriter( Module, Options, new DiagnosticSink() );\r\n\r\n\t\t\tprobe.WriteBlocks();\r\n\r\n\t\t\t_stageIds = PreviewInstrumentation.BuildStageMap( probe._builder.SourceMap );\r\n\t\t}\r\n\r\n\t\tWriteBlocks();\r\n\r\n\t\tvar text = _builder.ToString();\r\n\t\tvar map = _builder.SourceMap;\r\n\r\n\t\tmap.File = $\u0022{Options.OutputName}.{PrismConstants.ShaderExtension}\u0022;\r\n\r\n\t\treturn new BackendEmitResult( text, PrismConstants.ShaderExtension, map, Array.Empty\u003CGeneratedArtifact\u003E() )\r\n\t\t{\r\n\t\t\tBackendId = PrismConstants.BackendHlsl,\r\n\t\t\tLineCount = _builder.LineCount\r\n\t\t};\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EWrite every block of the file, in the order the engine\u0027s parser expects them.\u003C/summary\u003E\r\n\tvoid WriteBlocks()\r\n\t{\r\n\t\tWriteHeaderBlock();\r\n\t\tWriteModesBlock();\r\n\t\tWriteFeaturesBlock();\r\n\t\tWriteCommonBlock();\r\n\r\n\t\tif ( Domain != ShaderDomain.Compute )\r\n\t\t{\r\n\t\t\tWriteVertexInputStruct();\r\n\t\t\tWritePixelInputStruct();\r\n\t\t\tWriteVertexBlock();\r\n\t\t\tWritePixelBlock();\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tWriteComputeBlock();\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EWrite a module straight to text, for callers that only want the string.\u003C/summary\u003E\r\n\tpublic static string WriteText( IrModule module, BackendEmitOptions options, DiagnosticSink diagnostics ) =\u003E\r\n\t\tnew SboxShaderWriter( module, options, diagnostics ).Write().Text;\r\n\r\n\t// ---- HEADER -----------------------------------------------------------\r\n\r\n\tvoid WriteHeaderBlock()\r\n\t{\r\n\t\t_builder.Write( SboxShaderTemplates.BlockHeader );\r\n\t\t_builder.Open();\r\n\r\n\t\tvar description = string.IsNullOrWhiteSpace( Meta.Description )\r\n\t\t\t? $\u0022{Meta.Name} \u2014 generated by {PrismConstants.ProductName}\u0022\r\n\t\t\t: Meta.Description;\r\n\r\n\t\t_builder.Write( $\u0022Description = \\\u0022{SboxShaderTemplates.QuoteSafe( description )}\\\u0022;\u0022 );\r\n\t\t_builder.Write( $\u0022DevShader = {( Options.Mode == CompileMode.Final ? \u0022false\u0022 : \u0022true\u0022 )};\u0022 );\r\n\t\t_builder.Write( $\u0022Version = {HeaderVersion()};\u0022 );\r\n\r\n\t\tif ( Options.DebugSymbols ) _builder.Write( \u0022DebugInfo = true;\u0022 );\r\n\r\n\t\t_builder.Close();\r\n\t\t_builder.Blank();\r\n\t}\r\n\r\n\tint HeaderVersion()\r\n\t{\r\n\t\tvar version = Meta.Version;\r\n\r\n\t\tif ( string.IsNullOrWhiteSpace( version ) ) return 1;\r\n\t\tif ( int.TryParse( version, out var whole ) \u0026\u0026 whole \u003E 0 ) return whole;\r\n\r\n\t\tvar dot = version.IndexOf( \u0027.\u0027 );\r\n\r\n\t\tif ( dot \u003E 0 \u0026\u0026 int.TryParse( version[..dot], out var major ) \u0026\u0026 major \u003E 0 ) return major;\r\n\r\n\t\treturn 1;\r\n\t}\r\n\r\n\t// ---- MODES ------------------------------------------------------------\r\n\r\n\tvoid WriteModesBlock()\r\n\t{\r\n\t\t_builder.Write( SboxShaderTemplates.BlockModes );\r\n\t\t_builder.Open();\r\n\r\n\t\tforeach ( var mode in DeclaredModes() )\r\n\t\t{\r\n\t\t\tvar statement = SboxShaderTemplates.ModeStatement( mode );\r\n\r\n\t\t\tif ( !string.IsNullOrEmpty( statement ) ) _builder.Write( statement );\r\n\t\t}\r\n\r\n\t\t_builder.Close();\r\n\t\t_builder.Blank();\r\n\t}\r\n\r\n\tIReadOnlyList\u003Cstring\u003E DefaultModes() =\u003E SboxShaderTemplates.DefaultModesFor( Domain );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The render passes this file actually declares.\r\n\t/// \u003Cpara\u003E\r\n\t/// The domain and the pass list are authored independently, so a graph switched to PostProcess after\r\n\t/// the fact still carries the surface passes. Declaring \u003Cc\u003EDepth()\u003C/c\u003E on a full-screen pass asks the\r\n\t/// engine to render a full-screen triangle into the depth buffer, and a post-process material invoked\r\n\t/// through the standard path needs \u003Cc\u003EDefault()\u003C/c\u003E whether or not the document remembered it \u2014 so a\r\n\t/// non-surface domain starts from its own pass set and only then takes whatever the document adds.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tIReadOnlyList\u003Cstring\u003E DeclaredModes()\r\n\t{\r\n\t\tvar declared = Meta.Modes.Count \u003E 0 ? Meta.Modes : DefaultModes();\r\n\t\tvar modes = new List\u003Cstring\u003E( declared.Count \u002B 2 );\r\n\r\n\t\tbool Has( string mode ) =\u003E modes.Any( x =\u003E string.Equals( x, mode, StringComparison.OrdinalIgnoreCase ) );\r\n\r\n\t\tif ( Domain != ShaderDomain.Surface ) modes.AddRange( DefaultModes() );\r\n\r\n\t\tforeach ( var mode in declared )\r\n\t\t{\r\n\t\t\tif ( string.IsNullOrWhiteSpace( mode ) || Has( mode ) ) continue;\r\n\r\n\t\t\tif ( !SboxShaderTemplates.IsModeLegalFor( Domain, mode ) )\r\n\t\t\t{\r\n\t\t\t\tDiagnostics.Info( DiagnosticCode.InvalidBlock,\r\n\t\t\t\t\t$\u0022Render pass \u0027{mode}\u0027 means nothing to a {Domain} graph and was not declared.\u0022 );\r\n\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tmodes.Add( mode );\r\n\t\t}\r\n\r\n\t\tif ( modes.Count == 0 ) modes.AddRange( DefaultModes() );\r\n\r\n\t\treturn modes;\r\n\t}\r\n\r\n\t// ---- FEATURES ---------------------------------------------------------\r\n\r\n\tvoid WriteFeaturesBlock()\r\n\t{\r\n\t\tif ( Domain == ShaderDomain.Compute ) return;\r\n\r\n\t\t_builder.Write( SboxShaderTemplates.BlockFeatures );\r\n\t\t_builder.Open();\r\n\t\t_builder.Write( $\u0022#include \\\u0022{SboxShaderTemplates.IncludeFeatures}\\\u0022\u0022 );\r\n\r\n\t\tforeach ( var combo in Meta.Combos )\r\n\t\t{\r\n\t\t\tif ( combo is null || combo.Kind != ComboKind.Feature ) continue;\r\n\r\n\t\t\t_builder.Write( FeatureStatement( combo ) );\r\n\t\t}\r\n\r\n\t\t_builder.Close();\r\n\t\t_builder.Blank();\r\n\t}\r\n\r\n\tstatic string FeatureStatement( ComboDecl combo )\r\n\t{\r\n\t\tvar group = string.IsNullOrWhiteSpace( combo.Group ) ? \u0022Features\u0022 : SboxShaderTemplates.QuoteSafe( combo.Group );\r\n\t\tvar values = combo.Values ?? Array.Empty\u003Cstring\u003E();\r\n\r\n\t\t// A two-value feature whose labels say nothing beyond \u0022off\u0022 and \u0022on\u0022 is a checkbox in the material\r\n\t\t// editor. Spelling those labels out explicitly turns it into a two-item combo box instead, which\r\n\t\t// is the wrong control for a boolean, so the bare range is emitted for the conventional pairs.\r\n\t\tif ( values.Count \u003C 2 || IsPlainToggle( values ) ) return $\u0022Feature( {combo.Name}, 0..1, \\\u0022{group}\\\u0022 );\u0022;\r\n\r\n\t\tvar labels = new List\u003Cstring\u003E( values.Count );\r\n\r\n\t\tfor ( int i = 0; i \u003C values.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar label = values[i] ?? string.Empty;\r\n\t\t\tvar separator = label.IndexOf( \u0027=\u0027 );\r\n\r\n\t\t\tif ( separator \u003E= 0 ) label = label[( separator \u002B 1 )..];\r\n\r\n\t\t\tlabels.Add( $\u0022{i}=\\\u0022{SboxShaderTemplates.QuoteSafe( label.Trim().Trim( \u0027\u0022\u0027 ) )}\\\u0022\u0022 );\r\n\t\t}\r\n\r\n\t\treturn $\u0022Feature( {combo.Name}, 0..{values.Count - 1} ( {string.Join( \u0022, \u0022, labels )} ), \\\u0022{group}\\\u0022 );\u0022;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// True when a two-value combo\u0027s labels carry no information a checkbox does not already convey.\r\n\t/// \u003C/summary\u003E\r\n\tstatic bool IsPlainToggle( IReadOnlyList\u003Cstring\u003E values )\r\n\t{\r\n\t\tif ( values.Count != 2 ) return false;\r\n\r\n\t\tvar off = ( values[0] ?? string.Empty ).Trim().Trim( \u0027\u0022\u0027 );\r\n\t\tvar on = ( values[1] ?? string.Empty ).Trim().Trim( \u0027\u0022\u0027 );\r\n\r\n\t\tforeach ( var (a, b) in s_toggleLabels )\r\n\t\t{\r\n\t\t\tif ( string.Equals( off, a, StringComparison.OrdinalIgnoreCase ) \u0026\u0026\r\n\t\t\t\tstring.Equals( on, b, StringComparison.OrdinalIgnoreCase ) )\r\n\t\t\t{\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\tstatic readonly (string Off, string On)[] s_toggleLabels =\r\n\t[\r\n\t\t(\u0022Off\u0022, \u0022On\u0022), (\u00220\u0022, \u00221\u0022), (\u0022False\u0022, \u0022True\u0022), (\u0022No\u0022, \u0022Yes\u0022), (\u0022Disabled\u0022, \u0022Enabled\u0022)\r\n\t];\r\n\r\n\t// ---- COMMON -----------------------------------------------------------\r\n\r\n\tvoid WriteCommonBlock()\r\n\t{\r\n\t\t_builder.Write( SboxShaderTemplates.BlockCommon );\r\n\t\t_builder.Open();\r\n\r\n\t\tif ( Domain == ShaderDomain.Compute )\r\n\t\t{\r\n\t\t\t// A compute program has no render state, no material and no pixel input; the shipped\r\n\t\t\t// compute shaders include nothing but the macro header.\r\n\t\t\t_builder.Write( $\u0022#include \\\u0022{SboxShaderTemplates.IncludeSystem}\\\u0022\u0022 );\r\n\t\t\tWriteModuleIncludes();\r\n\t\t\t_builder.Close();\r\n\t\t\t_builder.Blank();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// Everything that steers render state has to be defined BEFORE common/pixel.hlsl pulls in\r\n\t\t// sbox_pixel.fxc, which reads S_TRANSLUCENT and S_ALPHA_TEST at include time. Defining them\r\n\t\t// afterwards silently produces opaque render state, which is the single most common way a\r\n\t\t// generated transparent shader comes out wrong.\r\n\t\tvar blend = Meta.BlendMode;\r\n\t\tvar alphaTest = blend == SurfaceBlendMode.Masked;\r\n\t\tvar translucent = blend is SurfaceBlendMode.Translucent or SurfaceBlendMode.Additive or SurfaceBlendMode.Multiply;\r\n\r\n\t\tWriteDefine( \u0022S_ALPHA_TEST\u0022, alphaTest ? \u00221\u0022 : \u00220\u0022 );\r\n\t\tWriteDefine( \u0022S_TRANSLUCENT\u0022, translucent ? \u00221\u0022 : \u00220\u0022 );\r\n\r\n\t\tif ( blend == SurfaceBlendMode.Additive ) WriteDefine( \u0022S_ADDITIVE_BLEND\u0022, \u00221\u0022 );\r\n\r\n\t\tif ( blend == SurfaceBlendMode.Multiply )\r\n\t\t{\r\n\t\t\t// Multiply is not one of the engine\u0027s built-in blend paths, so we take ownership of the\r\n\t\t\t// blend state and write it ourselves in the pixel block.\r\n\t\t\tWriteDefine( \u0022BLEND_MODE_ALREADY_SET\u0022, \u00221\u0022 );\r\n\t\t}\r\n\r\n\t\tif ( Meta.UsesUv2 || Domain == ShaderDomain.Surface ) WriteDefine( \u0022S_UV2\u0022, \u00221\u0022 );\r\n\r\n\t\tif ( Meta.ShadingModel == ShadingModel.Unlit \u0026\u0026 Domain == ShaderDomain.Surface )\r\n\t\t{\r\n\t\t\tWriteDefine( \u0022S_UNLIT\u0022, \u00221\u0022 );\r\n\t\t}\r\n\r\n\t\t_builder.Blank();\r\n\t\t_builder.Write( $\u0022#include \\\u0022{SboxShaderTemplates.IncludeShared}\\\u0022\u0022 );\r\n\r\n\t\tif ( IsSurface ) _builder.Write( $\u0022#include \\\u0022{SboxShaderTemplates.IncludeProcedural}\\\u0022\u0022 );\r\n\r\n\t\tWriteModuleIncludes();\r\n\r\n\t\t_builder.Close();\r\n\t\t_builder.Blank();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Emit the module\u0027s includes plus every include its helpers asked for, deduplicated and in a\r\n\t/// stable order. Folding the helper includes in here means a helper that needs a header still\r\n\t/// compiles even if nothing upstream remembered to register it on the module.\r\n\t/// \u003C/summary\u003E\r\n\tvoid WriteModuleIncludes()\r\n\t{\r\n\t\tvar seen = new HashSet\u003Cstring\u003E( StringComparer.OrdinalIgnoreCase );\r\n\r\n\t\tforeach ( var include in Module.Includes ) WriteInclude( include );\r\n\r\n\t\tforeach ( var helper in _helpers )\r\n\t\t{\r\n\t\t\tforeach ( var include in helper.Includes ?? Array.Empty\u003Cstring\u003E() ) WriteInclude( include );\r\n\t\t}\r\n\r\n\t\tvoid WriteInclude( string include )\r\n\t\t{\r\n\t\t\tif ( string.IsNullOrWhiteSpace( include ) ) return;\r\n\t\t\tif ( IsImplicitInclude( include ) ) return;\r\n\t\t\tif ( !seen.Add( include ) ) return;\r\n\r\n\t\t\t_builder.Write( $\u0022#include \\\u0022{include}\\\u0022\u0022 );\r\n\t\t}\r\n\t}\r\n\r\n\tvoid WriteDefine( string name, string value )\r\n\t{\r\n\t\t_builder.Write( $\u0022#ifndef {name}\u0022 );\r\n\t\t_builder.Write( $\u0022#define {name} {value}\u0022 );\r\n\t\t_builder.Write( \u0022#endif\u0022 );\r\n\t}\r\n\r\n\tstatic bool IsImplicitInclude( string include ) =\u003E\r\n\t\tinclude is SboxShaderTemplates.IncludeShared or SboxShaderTemplates.IncludeProcedural or\r\n\t\t\tSboxShaderTemplates.IncludeSystem or SboxShaderTemplates.IncludePixel or\r\n\t\t\tSboxShaderTemplates.IncludeVertex or SboxShaderTemplates.IncludeFeatures or\r\n\t\t\tSboxShaderTemplates.IncludeVertexInput or SboxShaderTemplates.IncludePixelInput;\r\n\r\n\t// ---- structs ----------------------------------------------------------\r\n\r\n\tvoid WriteVertexInputStruct()\r\n\t{\r\n\t\t_builder.Write( $\u0022struct {SboxShaderTemplates.StructVertexInput}\u0022 );\r\n\t\t_builder.Open();\r\n\t\t_builder.Write( $\u0022#include \\\u0022{SboxShaderTemplates.IncludeVertexInput}\\\u0022\u0022 );\r\n\r\n\t\tif ( Domain == ShaderDomain.Surface ) _builder.WriteBlock( SboxShaderTemplates.SurfaceVertexInputExtras );\r\n\r\n\t\tWriteUserStructFields( SboxShaderTemplates.StructVertexInput );\r\n\r\n\t\t_builder.Close( \u0022;\u0022 );\r\n\t\t_builder.Blank();\r\n\t}\r\n\r\n\tvoid WritePixelInputStruct()\r\n\t{\r\n\t\t_builder.Write( $\u0022struct {SboxShaderTemplates.StructPixelInput}\u0022 );\r\n\t\t_builder.Open();\r\n\t\t_builder.Write( $\u0022#include \\\u0022{SboxShaderTemplates.IncludePixelInput}\\\u0022\u0022 );\r\n\r\n\t\tif ( Domain == ShaderDomain.Surface )\r\n\t\t{\r\n\t\t\t_builder.WriteBlock( SboxShaderTemplates.SurfacePixelInputExtras );\r\n\t\t}\r\n\r\n\t\tforeach ( var varying in Module.Varyings )\r\n\t\t{\r\n\t\t\tif ( varying is null ) continue;\r\n\r\n\t\t\tvar semantic = SboxShaderTemplates.VaryingSemantic( varying );\r\n\r\n\t\t\t_builder.Write(\r\n\t\t\t\t$\u0022{HlslBackend.Interpolation( varying.Interpolation )}{varying.Type.Hlsl} {varying.Name} : {semantic};\u0022,\r\n\t\t\t\tvarying.Origin );\r\n\t\t}\r\n\r\n\t\tWriteUserStructFields( SboxShaderTemplates.StructPixelInput );\r\n\r\n\t\tif ( Domain == ShaderDomain.Surface ) _builder.WriteBlock( SboxShaderTemplates.PixelInputFrontFacing );\r\n\r\n\t\t_builder.Close( \u0022;\u0022 );\r\n\t\t_builder.Blank();\r\n\t}\r\n\r\n\tvoid WriteUserStructFields( string name )\r\n\t{\r\n\t\tvar structure = Module.FindStruct( name );\r\n\r\n\t\tif ( structure is null ) return;\r\n\r\n\t\tforeach ( var include in structure.Includes )\r\n\t\t{\r\n\t\t\tif ( string.IsNullOrWhiteSpace( include ) ) continue;\r\n\t\t\tif ( IsImplicitInclude( include ) ) continue;\r\n\r\n\t\t\t_builder.Write( $\u0022#include \\\u0022{include}\\\u0022\u0022 );\r\n\t\t}\r\n\r\n\t\tforeach ( var field in structure.Fields )\r\n\t\t{\r\n\t\t\tif ( field is null ) continue;\r\n\r\n\t\t\tvar semantic = string.IsNullOrEmpty( field.Semantic ) ? string.Empty : $\u0022 : {field.Semantic}\u0022;\r\n\r\n\t\t\t_builder.Write(\r\n\t\t\t\t$\u0022{HlslBackend.Interpolation( field.Interpolation )}{field.Type.Hlsl} {field.Name}{semantic};\u0022 );\r\n\t\t}\r\n\t}\r\n\r\n\t// ---- VS ---------------------------------------------------------------\r\n\r\n\tvoid WriteVertexBlock()\r\n\t{\r\n\t\t_emitter.Stage = ShaderStage.Vertex;\r\n\r\n\t\t_builder.Write( ShaderStage.Vertex.BlockName() );\r\n\t\t_builder.Open();\r\n\r\n\t\tif ( Domain == ShaderDomain.Surface )\r\n\t\t{\r\n\t\t\t_builder.Write( $\u0022#include \\\u0022{SboxShaderTemplates.IncludeVertex}\\\u0022\u0022 );\r\n\t\t\t_builder.Blank();\r\n\t\t}\r\n\r\n\t\tWriteCombos( ShaderStage.Vertex );\r\n\t\tSboxMaterialBinding.WriteGlobals( _builder, _emitter, ShaderStage.Vertex );\r\n\t\t_emitter.WriteHelpers( _builder, ShaderStage.Vertex, _helpers );\r\n\t\t_emitter.WriteFunctions( _builder, ShaderStage.Vertex );\r\n\r\n\t\tvar entry = Module.EntryPoint( ShaderStage.Vertex );\r\n\r\n\t\t// SV_VertexID has no stream in common/vertexinput.hlsl, so it rides in as a second entry-point\r\n\t\t// parameter \u2014 and only when the graph reads it, so every other shader keeps the stock signature.\r\n\t\tvar parameters = $\u0022{SboxShaderTemplates.StructVertexInput} {SboxShaderTemplates.VertexInputLocal}\u0022;\r\n\r\n\t\tif ( UsesBuiltin( entry?.Body, Builtin.VertexId ) )\r\n\t\t{\r\n\t\t\tparameters \u002B= $\u0022, {SboxShaderTemplates.VertexIdParameterDeclaration}\u0022;\r\n\t\t}\r\n\r\n\t\t_builder.Write(\r\n\t\t\t$\u0022{SboxShaderTemplates.StructPixelInput} {PrismConstants.EntryPointVertex}( {parameters} )\u0022 );\r\n\t\t_builder.Open();\r\n\r\n\t\tif ( Domain == ShaderDomain.PostProcess )\r\n\t\t{\r\n\t\t\t_builder.WriteBlock( SboxShaderTemplates.PostProcessVertexPrologue );\r\n\r\n\t\t\t// The graph\u0027s own vertex statements go here, not nowhere. GraphCompiler.EmitRoots builds a\r\n\t\t\t// real vertex entry for a post-process domain and the pixel input struct declares every\r\n\t\t\t// varying, so dropping the body left every interpolated value reading zero.\r\n\t\t\tif ( entry is not null )\r\n\t\t\t{\r\n\t\t\t\t_builder.Blank();\r\n\t\t\t\tWriteStatements( entry.Body );\r\n\t\t\t}\r\n\r\n\t\t\t_builder.Blank();\r\n\t\t\t_builder.Write( SboxShaderTemplates.PostProcessVertexEpilogue );\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t_builder.WriteBlock( SboxShaderTemplates.SurfaceVertexPrologue );\r\n\t\t\t_builder.Blank();\r\n\r\n\t\t\tif ( entry is not null )\r\n\t\t\t{\r\n\t\t\t\tWriteStatements( entry.Body );\r\n\r\n\t\t\t\tif ( WritesWorldPosition( entry.Body ) )\r\n\t\t\t\t{\r\n\t\t\t\t\t_builder.Write( SboxShaderTemplates.VertexPositionResync );\r\n\t\t\t\t}\r\n\r\n\t\t\t\t_builder.Blank();\r\n\t\t\t}\r\n\r\n\t\t\t_builder.Write( SboxShaderTemplates.SurfaceVertexEpilogue );\r\n\t\t}\r\n\r\n\t\t_builder.Close();\r\n\t\t_builder.Close();\r\n\t\t_builder.Blank();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// True when the vertex program moved the world-space position, so clip space has to be recomputed\r\n\t/// before \u003Cc\u003EFinalizeVertex\u003C/c\u003E subtracts the high-precision offset.\r\n\t/// \u003C/summary\u003E\r\n\tstatic bool WritesWorldPosition( IrBlock block )\r\n\t{\r\n\t\tforeach ( var statement in IrWalk.Statements( block ) )\r\n\t\t{\r\n\t\t\tif ( statement is IrAssign assign \u0026\u0026 TouchesWorldPosition( assign.Target ) ) return true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETrue when any expression anywhere in a block reads a particular environment value.\u003C/summary\u003E\r\n\tstatic bool UsesBuiltin( IrBlock block, Builtin id )\r\n\t{\r\n\t\tforeach ( var statement in IrWalk.Statements( block ) )\r\n\t\t{\r\n\t\t\tforeach ( var expression in IrWalk.Expressions( statement ) )\r\n\t\t\t{\r\n\t\t\t\tif ( Reads( expression, id ) ) return true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\tstatic bool Reads( IrExpr expr, Builtin id )\r\n\t{\r\n\t\tif ( expr is null ) return false;\r\n\r\n\t\tforeach ( var node in IrExprUtil.Walk( expr ) )\r\n\t\t{\r\n\t\t\tif ( node is IrBuiltinRef reference \u0026\u0026 reference.Id == id ) return true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\tstatic bool TouchesWorldPosition( IrExpr target )\r\n\t{\r\n\t\tforeach ( var node in IrExprUtil.Walk( target ) )\r\n\t\t{\r\n\t\t\tif ( node is IrMember member \u0026\u0026 member.Field == \u0022vPositionWs\u0022 ) return true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\t// ---- PS ---------------------------------------------------------------\r\n\r\n\tvoid WritePixelBlock()\r\n\t{\r\n\t\t_emitter.Stage = ShaderStage.Pixel;\r\n\r\n\t\t_builder.Write( ShaderStage.Pixel.BlockName() );\r\n\t\t_builder.Open();\r\n\t\t_builder.Write( $\u0022#include \\\u0022{SboxShaderTemplates.IncludePixel}\\\u0022\u0022 );\r\n\r\n\t\tif ( Domain == ShaderDomain.PostProcess )\r\n\t\t{\r\n\t\t\t_builder.Write( $\u0022#include \\\u0022{SboxShaderTemplates.IncludePostProcessCommon}\\\u0022\u0022 );\r\n\t\t\t_builder.Write( $\u0022#include \\\u0022{SboxShaderTemplates.IncludePostProcessFunctions}\\\u0022\u0022 );\r\n\t\t}\r\n\r\n\t\t_builder.Blank();\r\n\r\n\t\tWriteCombos( ShaderStage.Pixel );\r\n\t\tWriteRenderState();\r\n\r\n\t\t// The colour buffer is boilerplate for a post-process pass, but a node that reads it declares it\r\n\t\t// too \u2014 and Slang rejects the second declaration outright rather than merging them, so a graph\r\n\t\t// that actually sampled the frame buffer used to fail to compile. The node\u0027s declaration wins:\r\n\t\t// it carries the node\u0027s own sRGB and attribute metadata, and it is emitted with the rest of the\r\n\t\t// module globals a few lines below.\r\n\t\tif ( Domain == ShaderDomain.PostProcess \u0026\u0026\r\n\t\t\tModule.FindGlobal( SboxShaderTemplates.PostProcessColorBufferSymbol ) is null )\r\n\t\t{\r\n\t\t\t_builder.WriteBlock( SboxShaderTemplates.PostProcessColorBuffer );\r\n\t\t\t_builder.Blank();\r\n\t\t}\r\n\r\n\t\tSboxMaterialBinding.WriteGlobals( _builder, _emitter, ShaderStage.Pixel );\r\n\t\tWriteInstrumentationDeclarations();\r\n\t\t_emitter.WriteHelpers( _builder, ShaderStage.Pixel, _helpers );\r\n\t\t_emitter.WriteFunctions( _builder, ShaderStage.Pixel );\r\n\r\n\t\t_builder.Write(\r\n\t\t\t$\u0022float4 {PrismConstants.EntryPointPixel}( {SboxShaderTemplates.StructPixelInput} {SboxShaderTemplates.PixelInputLocal} ) : SV_Target0\u0022 );\r\n\t\t_builder.Open();\r\n\r\n\t\tvar entry = Module.EntryPoint( ShaderStage.Pixel );\r\n\t\tvar returned = SboxMaterialBinding.EndsWithReturn( entry?.Body );\r\n\r\n\t\tif ( !returned ) SboxMaterialBinding.WritePixelPrologue( _builder, Module );\r\n\r\n\t\tif ( entry is not null )\r\n\t\t{\r\n\t\t\tWritePixelBody( entry.Body );\r\n\t\t\t_builder.Blank();\r\n\t\t}\r\n\r\n\t\tif ( !returned ) WriteChannelTail();\r\n\r\n\t\tif ( !returned ) SboxMaterialBinding.WritePixelEpilogue( _builder, Module, _emitter );\r\n\r\n\t\t_builder.Close();\r\n\t\t_builder.Close();\r\n\t\t_builder.Blank();\r\n\t}\r\n\r\n\tvoid WriteRenderState()\r\n\t{\r\n\t\tvar wrote = false;\r\n\r\n\t\t// The engine only fills the frame-buffer copy for a material that asks for it, and the ask is a\r\n\t\t// PS-block attribute rather than anything a node can declare. Emitting it here means a graph that\r\n\t\t// reads scene colour gets a filled texture instead of last frame\u0027s stale contents.\r\n\t\tif ( WantsFrameBufferCopy() )\r\n\t\t{\r\n\t\t\t_builder.Write( $\u0022BoolAttribute( {SboxShaderTemplates.FrameBufferCopyFlag}, true );\u0022 );\r\n\t\t\twrote = true;\r\n\t\t}\r\n\r\n\t\tif ( Meta.BlendMode == SurfaceBlendMode.Multiply )\r\n\t\t{\r\n\t\t\t_builder.WriteBlock( SboxShaderTemplates.MultiplyBlendState );\r\n\t\t\twrote = true;\r\n\t\t}\r\n\r\n\t\tif ( Options.Mode is CompileMode.Preview or CompileMode.Thumbnail )\r\n\t\t{\r\n\t\t\t// The preview toggles backface rendering without recompiling the material.\r\n\t\t\t_builder.WriteBlock( SboxShaderTemplates.CullModePreview );\r\n\t\t\twrote = true;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tvar cull = Meta.RenderBackfaces ? CullMode.None : Meta.CullMode;\r\n\r\n\t\t\tswitch ( cull )\r\n\t\t\t{\r\n\t\t\t\tcase CullMode.None:\r\n\t\t\t\t\t_builder.Write( \u0022RenderState( CullMode, NONE );\u0022 );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase CullMode.Front:\r\n\t\t\t\t\t_builder.Write( \u0022RenderState( CullMode, FRONT );\u0022 );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tdefault:\r\n\t\t\t\t\t_builder.Write( SboxShaderTemplates.CullModeFromFeature );\r\n\t\t\t\t\tbreak;\r\n\t\t\t}\r\n\r\n\t\t\twrote = true;\r\n\t\t}\r\n\r\n\t\tif ( wrote ) _builder.Blank();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETrue when the module reads the frame-buffer copy and must therefore request it.\u003C/summary\u003E\r\n\tbool WantsFrameBufferCopy()\r\n\t{\r\n\t\tif ( Module?.Globals is null ) return false;\r\n\r\n\t\tforeach ( var global in Module.Globals )\r\n\t\t{\r\n\t\t\tif ( global is null ) continue;\r\n\r\n\t\t\tif ( string.Equals( global.Name, SboxShaderTemplates.FrameBufferCopyTexture, StringComparison.Ordinal ) )\r\n\t\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\t// ---- CS ---------------------------------------------------------------\r\n\r\n\tvoid WriteComputeBlock()\r\n\t{\r\n\t\t_emitter.Stage = ShaderStage.Compute;\r\n\r\n\t\t_builder.Write( ShaderStage.Compute.BlockName() );\r\n\t\t_builder.Open();\r\n\r\n\t\tWriteCombos( ShaderStage.Compute );\r\n\t\tSboxMaterialBinding.WriteGlobals( _builder, _emitter, ShaderStage.Compute );\r\n\t\t_emitter.WriteHelpers( _builder, ShaderStage.Compute, _helpers );\r\n\t\t_emitter.WriteFunctions( _builder, ShaderStage.Compute );\r\n\r\n\t\tvar entry = Module.EntryPoint( ShaderStage.Compute );\r\n\t\tvar threads = entry?.Attributes.FirstOrDefault( x =\u003E x?.StartsWith( \u0022[numthreads\u0022, StringComparison.OrdinalIgnoreCase ) == true );\r\n\r\n\t\t_builder.Write( threads ?? SboxShaderTemplates.ComputeDefaultNumThreads );\r\n\t\t_builder.Write( $\u0022void {PrismConstants.EntryPointCompute}( {SboxShaderTemplates.ComputeEntryParameters} )\u0022 );\r\n\t\t_builder.Open();\r\n\r\n\t\tif ( entry is not null ) WriteStatements( entry.Body );\r\n\r\n\t\t_builder.Close();\r\n\t\t_builder.Close();\r\n\t\t_builder.Blank();\r\n\t}\r\n\r\n\t// ---- statements -------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Write a block\u0027s statements.\r\n\t/// \u003Cpara\u003E\r\n\t/// Everything \u003Csee cref=\u0022HlslEmitter\u0022/\u003E already knows how to write is handed straight back to it,\r\n\t/// character for character. This layer exists for the one statement the emitter cannot see \u2014\r\n\t/// \u003Csee cref=\u0022IrPreprocessorIf\u0022/\u003E, which lowers to directives rather than to an expression \u2014 and\r\n\t/// the block-carrying statements are reproduced here only so that a preprocessor branch nested\r\n\t/// inside a loop or a conditional still reaches this writer.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tvoid WriteStatements( IrBlock block )\r\n\t{\r\n\t\tif ( block is null ) return;\r\n\r\n\t\tforeach ( var statement in block.Statements ) WriteStatement( statement );\r\n\t}\r\n\r\n\tvoid WriteStatement( IrStmt statement )\r\n\t{\r\n\t\tif ( statement is null ) return;\r\n\r\n\t\tvar previous = _emitter.CurrentOrigin;\r\n\r\n\t\tswitch ( statement )\r\n\t\t{\r\n\t\t\tcase IrPreprocessorIf guard:\r\n\t\t\t\t_emitter.CurrentOrigin = guard.Origin;\r\n\t\t\t\tWritePreprocessorIf( guard );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase IrIf branch:\r\n\t\t\t\t_emitter.CurrentOrigin = branch.Origin;\r\n\t\t\t\t_builder.Write( $\u0022if ( {_emitter.Expression( branch.Cond )} )\u0022, branch.Origin );\r\n\t\t\t\tWriteBraced( branch.Then, branch.Origin );\r\n\r\n\t\t\t\tif ( branch.Else is { IsEmpty: false } )\r\n\t\t\t\t{\r\n\t\t\t\t\t_builder.Write( \u0022else\u0022, branch.Origin );\r\n\t\t\t\t\tWriteBraced( branch.Else, branch.Origin );\r\n\t\t\t\t}\r\n\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase IrFor loop:\r\n\t\t\t\t_emitter.CurrentOrigin = loop.Origin;\r\n\r\n\t\t\t\tvar counter = string.IsNullOrEmpty( loop.Var ) ? \u0022n\u0022 : loop.Var;\r\n\r\n\t\t\t\t_builder.Write(\r\n\t\t\t\t\t$\u0022for ( int {counter} = 0; {counter} \u003C ( int )( {_emitter.Expression( loop.Count )} ); {counter}\u002B\u002B )\u0022,\r\n\t\t\t\t\tloop.Origin );\r\n\t\t\t\tWriteBraced( loop.Body, loop.Origin );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase IrWhile loop:\r\n\t\t\t\t_emitter.CurrentOrigin = loop.Origin;\r\n\t\t\t\t_builder.Write( $\u0022while ( {_emitter.Expression( loop.Cond )} )\u0022, loop.Origin );\r\n\t\t\t\tWriteBraced( loop.Body, loop.Origin );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase IrScope scope:\r\n\t\t\t\t_emitter.CurrentOrigin = scope.Origin;\r\n\t\t\t\tWriteBraced( scope.Body, scope.Origin );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tdefault:\r\n\t\t\t\t_emitter.WriteStatement( _builder, statement );\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\r\n\t\t_emitter.CurrentOrigin = previous;\r\n\t}\r\n\r\n\tvoid WriteBraced( IrBlock block, NodeId origin )\r\n\t{\r\n\t\t_builder.Open( origin );\r\n\t\tWriteStatements( block );\r\n\t\t_builder.Close( origin: origin );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Write a preprocessor branch as real \u003Cc\u003E#if\u003C/c\u003E / \u003Cc\u003E#else\u003C/c\u003E / \u003Cc\u003E#endif\u003C/c\u003E directives, so\r\n\t/// only the taken side ever reaches the compiler.\r\n\t/// \u003Cpara\u003E\r\n\t/// This is what a static combo is supposed to cost. A run-time \u003Cc\u003Eselect\u003C/c\u003E evaluates both sides\r\n\t/// and pays for the texture samples and the loops in the one that was never wanted; a\r\n\t/// preprocessor branch deletes them.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tvoid WritePreprocessorIf( IrPreprocessorIf guard )\r\n\t{\r\n\t\tvar directive = IrPreprocessor.OpenDirective( guard.Condition );\r\n\r\n\t\tif ( string.IsNullOrEmpty( directive ) )\r\n\t\t{\r\n\t\t\t// A condition we cannot spell must not become a directive the preprocessor rejects, because\r\n\t\t\t// a preprocessor error has no line we can map back to a node. Folding both sides in keeps\r\n\t\t\t// the shader compiling and costs only the exclusion.\r\n\t\t\t_emitter.Report( DiagnosticSeverity.Warning, DiagnosticCode.InvalidBlock,\r\n\t\t\t\t\u0022A compile-time branch had no usable combo condition, so both of its sides were emitted.\u0022,\r\n\t\t\t\t\u0022Bind the node to a graph keyword. Without one there is nothing for the preprocessor to test, and the branch cannot be resolved at compile time.\u0022 );\r\n\r\n\t\t\tWriteStatements( guard.Then );\r\n\t\t\tWriteStatements( guard.Else );\r\n\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_builder.Write( directive, guard.Origin );\r\n\t\tWriteStatements( guard.Then );\r\n\r\n\t\tif ( guard.HasElse )\r\n\t\t{\r\n\t\t\t_builder.Write( IrPreprocessor.ElseDirective, guard.Origin );\r\n\t\t\tWriteStatements( guard.Else );\r\n\t\t}\r\n\r\n\t\t_builder.Write( IrPreprocessor.EndDirective, guard.Origin );\r\n\t}\r\n\r\n\t// ---- preview instrumentation ------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Declare the two preview uniforms. They are attribute-bound and default to zero, so a shader\r\n\t/// built with instrumentation still renders normally until something pushes them, and switching\r\n\t/// what the viewport displays costs one attribute write rather than a recompile.\r\n\t/// \u003C/summary\u003E\r\n\tvoid WriteInstrumentationDeclarations()\r\n\t{\r\n\t\tif ( _stageIds is null ) return;\r\n\r\n\t\tforeach ( var line in PreviewInstrumentation.Banner() ) _builder.Write( line );\r\n\t\tforeach ( var line in PreviewInstrumentation.HlslDeclarations() ) _builder.Write( line );\r\n\r\n\t\t_builder.Blank();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Write the pixel entry\u0027s body, interleaving the preview stage switch between statements.\r\n\t/// \u003Cpara\u003E\r\n\t/// The test sits next to the temp it reads rather than in a tail at the end of the function, and\r\n\t/// that is not a stylistic choice: a temp bound inside a loop or a branch has gone out of scope by\r\n\t/// the time the function ends. One test per node, after the last statement that node produced, so\r\n\t/// what the switch returns is the node\u0027s result rather than an intermediate.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tvoid WritePixelBody( IrBlock body )\r\n\t{\r\n\t\tif ( body is null ) return;\r\n\r\n\t\tif ( _stageIds is null )\r\n\t\t{\r\n\t\t\tWriteStatements( body );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar statements = body.Statements;\r\n\t\tvar cases = StageCases( statements );\r\n\r\n\t\tfor ( int i = 0; i \u003C statements.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tWriteStatement( statements[i] );\r\n\r\n\t\t\tif ( !cases.TryGetValue( i, out var line ) ) continue;\r\n\r\n\t\t\t_builder.Write( line, statements[i].Origin );\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The switch case for each top-level statement that ends a node\u0027s contribution to the pixel\r\n\t/// program. Nodes whose work the stage planner put in the vertex program, and values that cannot\r\n\t/// be shown as a colour at all, produce no case \u2014 selecting one of those shows the shaded result\r\n\t/// rather than a wrong one.\r\n\t/// \u003C/summary\u003E\r\n\tDictionary\u003Cint, string\u003E StageCases( IReadOnlyList\u003CIrStmt\u003E statements )\r\n\t{\r\n\t\tvar last = new Dictionary\u003CNodeId, int\u003E();\r\n\r\n\t\tfor ( int i = 0; i \u003C statements.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( statements[i] is not IrDecl decl || !decl.Origin.IsValid ) continue;\r\n\t\t\tif ( !PreviewInstrumentation.CanShow( decl.Type ) ) continue;\r\n\r\n\t\t\tlast[decl.Origin] = i;\r\n\t\t}\r\n\r\n\t\tvar cases = new Dictionary\u003Cint, string\u003E();\r\n\r\n\t\tforeach ( var (origin, index) in last )\r\n\t\t{\r\n\t\t\tif ( statements[index] is not IrDecl decl ) continue;\r\n\r\n\t\t\tvar line = PreviewInstrumentation.StageCase(\r\n\t\t\t\tPreviewInstrumentation.StageIdOf( _stageIds, origin ), decl.Name, decl.Type );\r\n\r\n\t\t\tif ( line is not null ) cases[index] = line;\r\n\t\t}\r\n\r\n\t\treturn cases;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Write the debug-channel tail, just before the shading epilogue so the material the graph filled\r\n\t/// in is still in scope and still unclamped.\r\n\t/// \u003C/summary\u003E\r\n\tvoid WriteChannelTail()\r\n\t{\r\n\t\tif ( _stageIds is null ) return;\r\n\r\n\t\tvar lines = PreviewInstrumentation.ChannelLines( ChannelEnvironment() );\r\n\r\n\t\tif ( lines.Count == 0 ) return;\r\n\r\n\t\tforeach ( var line in lines ) _builder.Write( line );\r\n\r\n\t\t_builder.Blank();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// What this shader can answer about itself, channel by channel. A null entry means the generated\r\n\t/// shader has no honest expression for that channel \u2014 no material struct under a custom shading\r\n\t/// model, no vertex colour outside a surface graph \u2014 and the channel is then simply absent, which\r\n\t/// the viewport reads as \u0022keep showing the shaded result\u0022.\r\n\t/// \u003C/summary\u003E\r\n\tPreviewChannelEnvironment ChannelEnvironment()\r\n\t{\r\n\t\tvar surface = Domain == ShaderDomain.Surface;\r\n\t\tvar input = SboxShaderTemplates.PixelInputLocal;\r\n\t\tvar material = SboxMaterialBinding.UsesMaterial( Module );\r\n\r\n\t\treturn new PreviewChannelEnvironment\r\n\t\t{\r\n\t\t\tAlbedo = MaterialField( material, \u0022Albedo\u0022 ),\r\n\t\t\tOpacity = MaterialField( material, \u0022Opacity\u0022 ),\r\n\t\t\tNormalTangent = MaterialField( material, \u0022Normal\u0022 ),\r\n\r\n\t\t\t// The graph authors the normal in tangent space; the same conversion the shading epilogue\r\n\t\t\t// performs is what makes this channel comparable with the engine\u0027s own normal debug view.\r\n\t\t\tNormalWorld = material \u0026\u0026 surface\r\n\t\t\t\t? $\u0022TransformNormal( {SboxShaderTemplates.MaterialLocal}.Normal, {input}.vNormalWs, {input}.vTangentUWs, {input}.vTangentVWs )\u0022\r\n\t\t\t\t: null,\r\n\r\n\t\t\tRoughness = MaterialField( material, \u0022Roughness\u0022 ),\r\n\t\t\tMetalness = MaterialField( material, \u0022Metalness\u0022 ),\r\n\t\t\tAmbientOcclusion = MaterialField( material, \u0022AmbientOcclusion\u0022 ),\r\n\t\t\tEmission = MaterialField( material, \u0022Emission\u0022 ),\r\n\t\t\tTransmission = MaterialField( material, \u0022Transmission\u0022 ),\r\n\t\t\tTintMask = MaterialField( material, \u0022TintMask\u0022 ),\r\n\r\n\t\t\tUv0 = $\u0022{input}.vTextureCoords.xy\u0022,\r\n\t\t\tUv1 = $\u0022{input}.vTextureCoords.zw\u0022,\r\n\t\t\tVertexColor = surface ? $\u0022{input}.vColor\u0022 : null,\r\n\t\t\tWorldPosition = surface\r\n\t\t\t\t? HlslIntrinsics.BuiltinExpression( Builtin.WorldPosition, ShaderStage.Pixel, Domain )\r\n\t\t\t\t: null,\r\n\r\n\t\t\tDerivativeSource = $\u0022{input}.vTextureCoords.xy\u0022\r\n\t\t};\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe expression for one material field, or null when this shader has no material.\u003C/summary\u003E\r\n\tstatic string MaterialField( bool material, string name ) =\u003E\r\n\t\tmaterial \u0026\u0026 SboxMaterialBinding.TryGetField( name, out var field ) ? field.Reference : null;\r\n\r\n\t// ---- combos -----------------------------------------------------------\r\n\r\n\tvoid WriteCombos( ShaderStage stage )\r\n\t{\r\n\t\tvar wrote = false;\r\n\r\n\t\tforeach ( var combo in Meta.Combos )\r\n\t\t{\r\n\t\t\tif ( combo is null ) continue;\r\n\r\n\t\t\tswitch ( combo.Kind )\r\n\t\t\t{\r\n\t\t\t\tcase ComboKind.Feature:\r\n\t\t\t\t\t// A feature is only visible to a program through a static combo bound to it.\r\n\t\t\t\t\t_builder.Write( $\u0022StaticCombo( {StaticNameFor( combo.Name )}, {combo.Name}, Sys( ALL ) );\u0022 );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase ComboKind.Static:\r\n\t\t\t\t\t_builder.Write( $\u0022StaticCombo( {combo.Name}, 0..{ComboMaximum( combo )}, Sys( ALL ) );\u0022 );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tdefault:\r\n\t\t\t\t\t_builder.Write( $\u0022DynamicCombo( {combo.Name}, 0..{ComboMaximum( combo )}, Sys( ALL ) );\u0022 );\r\n\t\t\t\t\tbreak;\r\n\t\t\t}\r\n\r\n\t\t\twrote = true;\r\n\t\t}\r\n\r\n\t\tif ( wrote ) _builder.Blank();\r\n\t}\r\n\r\n\tstatic int ComboMaximum( ComboDecl combo )\r\n\t{\r\n\t\tvar count = combo.Values?.Count ?? 0;\r\n\r\n\t\treturn count \u003C 2 ? 1 : count - 1;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe static-combo symbol a feature is bound to: \u003Cc\u003EF_PUDDLES\u003C/c\u003E becomes \u003Cc\u003ES_PUDDLES\u003C/c\u003E.\u003C/summary\u003E\r\n\tpublic static string StaticNameFor( string featureName )\r\n\t{\r\n\t\tif ( string.IsNullOrEmpty( featureName ) ) return \u0022S_UNNAMED\u0022;\r\n\r\n\t\treturn featureName.StartsWith( \u0022F_\u0022, StringComparison.Ordinal )\r\n\t\t\t? \u0022S_\u0022 \u002B featureName[2..]\r\n\t\t\t: \u0022S_\u0022 \u002B featureName;\r\n\t}\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/Compiler/Backends/SlangRuntimeModule.cs","FileName":"SlangRuntimeModule.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using Editor.Prism.Core;\r\n\r\nnamespace Editor.Prism.Compiler.Backends;\r\n\r\n/// \u003Csummary\u003E\r\n/// The small \u003Cc\u003Eprism.core\u003C/c\u003E Slang module every generated Prism module imports.\r\n/// \u003Cpara\u003E\r\n/// It carries the three things a standalone \u003Cc\u003E.slang\u003C/c\u003E artifact cannot get from the engine: the\r\n/// environment parameter block (camera, viewport, object transform, time), the handful of math and\r\n/// colour-space helpers the emitted code calls into, and the \u003Cc\u003EPrismMaterial\u003C/c\u003E struct a surface\r\n/// graph fills in. It is emitted as a \u003Csee cref=\u0022GeneratedArtifact\u0022/\u003E beside the main module, so the\r\n/// pair compiles with nothing but \u003Cc\u003Eslangc\u003C/c\u003E and an include path.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\npublic static class SlangRuntimeModule\r\n{\r\n\t/// \u003Csummary\u003EThe module name an emitted Prism module imports.\u003C/summary\u003E\r\n\tpublic const string ModuleName = PrismConstants.SlangRuntimeModule;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Path of the emitted file, relative to the main module. \u003Cc\u003Eimport prism.core;\u003C/c\u003E resolves a\r\n\t/// dotted module name to this path, so the directory is part of the contract.\r\n\t/// \u003C/summary\u003E\r\n\tpublic const string FileName = \u0022prism/core.slang\u0022;\r\n\r\n\t/// \u003Csummary\u003EThe import statement an emitted module writes.\u003C/summary\u003E\r\n\tpublic const string ImportStatement = \u0022import \u0022 \u002B ModuleName \u002B \u0022;\u0022;\r\n\r\n\t/// \u003Csummary\u003EName of the environment parameter block this module declares.\u003C/summary\u003E\r\n\tpublic const string EnvironmentBlock = SlangIntrinsics.EnvironmentBlock;\r\n\r\n\t/// \u003Csummary\u003EName of the material struct a surface graph fills in.\u003C/summary\u003E\r\n\tpublic const string MaterialStruct = \u0022PrismMaterial\u0022;\r\n\r\n\t/// \u003Csummary\u003EThe prelude source, with CRLF line endings.\u003C/summary\u003E\r\n\tpublic static string Source =\u003E SourceWith( \u0022\\r\\n\u0022 );\r\n\r\n\t/// \u003Csummary\u003EThe prelude source with a chosen line ending.\u003C/summary\u003E\r\n\tpublic static string SourceWith( string newLine )\r\n\t{\r\n\t\tif ( string.IsNullOrEmpty( newLine ) ) newLine = \u0022\\r\\n\u0022;\r\n\r\n\t\t// The literal below picks up whatever line ending this file happens to be saved with, so it is\r\n\t\t// normalised before substituting. Without this a CRLF source would emit CR CR LF.\r\n\t\tvar normalised = s_source.Replace( \u0022\\r\\n\u0022, \u0022\\n\u0022 ).Replace( \u0027\\r\u0027, \u0027\\n\u0027 );\r\n\r\n\t\treturn newLine == \u0022\\n\u0022 ? normalised : normalised.Replace( \u0022\\n\u0022, newLine );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The prelude packaged as an artifact the backend returns alongside its main result. It is\r\n\t/// written beside the saved document, which is also where the module\u0027s include path points.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static GeneratedArtifact Artifact( string newLine = \u0022\\r\\n\u0022 ) =\u003E\r\n\t\tnew( FileName, SourceWith( newLine ) ) { BesideDocument = true };\r\n\r\n\t// The source is stored with plain LF and normalised on the way out, so the literal below stays\r\n\t// readable and the emitted file still honours BackendEmitOptions.NewLine.\r\n\tconst string s_source = \u0022\u0022\u0022\r\n#language slang 2026\r\nmodule \u0022prism/core\u0022;\r\n\r\n// =============================================================================\r\n//  prism.core - the shared prelude for Prism-generated Slang modules\r\n//\r\n//  Generated by Prism. Editing this file is fine, but regenerating a graph\r\n//  overwrites it: keep local changes in a module of your own and import both.\r\n//\r\n//  Contents\r\n//    1. Material-UI attributes  - reflected into \u0060-reflection-json\u0060 userAttribs\r\n//    2. Environment             - camera, viewport, object transform, time\r\n//    3. Transforms              - object/world/clip space conversions\r\n//    4. Math                    - the safe-by-default helpers emitted code calls\r\n//    5. Textures                - value-returning wrappers over out-param methods\r\n//    6. Colour                  - sRGB, HSV and luminance\r\n//    7. PrismMaterial           - what a surface graph fills in\r\n// =============================================================================\r\n\r\n\r\n// -----------------------------------------------------------------------------\r\n//  1. Material-UI attributes\r\n//\r\n//  Prism annotates every generated shader parameter with these. They carry no\r\n//  runtime cost: \u0060slangc -reflection-json\u0060 reports them under \u0022userAttribs\u0022,\r\n//  which is how a host application rebuilds the material inspector.\r\n// -----------------------------------------------------------------------------\r\n\r\n/// Display name of a parameter.\r\n[__AttributeUsage( _AttributeTargets.Var )]\r\npublic struct UiLabelAttribute { string text; }\r\n\r\n/// Group heading and sort order in the material inspector.\r\n[__AttributeUsage( _AttributeTargets.Var )]\r\npublic struct UiGroupAttribute { string group; int order; }\r\n\r\n/// Inclusive numeric range of a slider.\r\n[__AttributeUsage( _AttributeTargets.Var )]\r\npublic struct UiRangeAttribute { float min; float max; }\r\n\r\n/// Which editor to show: slider, color, toggle, dropdown, vector, texture.\r\n[__AttributeUsage( _AttributeTargets.Var )]\r\npublic struct UiControlAttribute { string control; }\r\n\r\n/// Hover text.\r\n[__AttributeUsage( _AttributeTargets.Var )]\r\npublic struct UiTooltipAttribute { string text; }\r\n\r\n/// Default value, splatted across the parameter\u0027s components.\r\n[__AttributeUsage( _AttributeTargets.Var )]\r\npublic struct UiDefaultAttribute { float x; float y; float z; float w; }\r\n\r\n/// Default asset path for a texture parameter.\r\n[__AttributeUsage( _AttributeTargets.Var )]\r\npublic struct UiAssetAttribute { string path; }\r\n\r\n/// Render-attribute name, so a host can push a value without recompiling.\r\n[__AttributeUsage( _AttributeTargets.Var )]\r\npublic struct PrismAttributeAttribute { string name; }\r\n\r\n/// Non-zero when a texture\u0027s contents are sRGB encoded.\r\n[__AttributeUsage( _AttributeTargets.Var )]\r\npublic struct PrismSrgbAttribute { int srgb; }\r\n\r\n\r\n// -----------------------------------------------------------------------------\r\n//  2. Environment\r\n//\r\n//  Everything a shader knows about the frame and the object it is drawing.\r\n//  A ParameterBlock gets its own descriptor set / register space, so binding it\r\n//  once per frame and once per object is the natural split for a host renderer.\r\n// -----------------------------------------------------------------------------\r\n\r\n/// Per-frame constants.\r\npublic struct PrismFrameParams\r\n{\r\n\tfloat4x4 WorldToView;\r\n\tfloat4x4 ViewToProjection;\r\n\tfloat4x4 WorldToProjection;\r\n\r\n\tfloat3 CameraPosition;\r\n\tfloat  CameraNear;\r\n\tfloat3 CameraForward;\r\n\tfloat  CameraFar;\r\n\r\n\tfloat2 ViewportSize;\r\n\tfloat2 ViewportInvSize;\r\n\tfloat2 ViewportOffset;\r\n\r\n\tfloat3 SunDirection;\r\n\tfloat3 SunColor;\r\n\r\n\tfloat Time;\r\n\tfloat DeltaTime;\r\n\tint   FrameCount;\r\n}\r\n\r\n/// Per-object constants.\r\npublic struct PrismObjectParams\r\n{\r\n\tfloat4x4 ObjectToWorld;\r\n\tfloat4x4 WorldToObject;\r\n\r\n\tfloat3 ObjectOrigin;\r\n\tfloat3 ObjectScale;\r\n\tfloat4 TintColor;\r\n}\r\n\r\n/// The environment a Prism module is rendered in.\r\npublic struct PrismEnvironment\r\n{\r\n\tPrismFrameParams  Frame;\r\n\tPrismObjectParams Object;\r\n}\r\n\r\n/// The one environment binding every generated module reads from.\r\npublic ParameterBlock\u003CPrismEnvironment\u003E gPrismEnv;\r\n\r\n\r\n// -----------------------------------------------------------------------------\r\n//  3. Transforms\r\n// -----------------------------------------------------------------------------\r\n\r\n/// Object space to world space, as a position.\r\npublic float3 PrismObjectToWorldPoint( float3 positionOs )\r\n{\r\n\treturn mul( gPrismEnv.Object.ObjectToWorld, float4( positionOs, 1.0 ) ).xyz;\r\n}\r\n\r\n/// Object space to world space, as a direction. Not normalised; scale is preserved.\r\npublic float3 PrismObjectToWorldDirection( float3 directionOs )\r\n{\r\n\treturn mul( gPrismEnv.Object.ObjectToWorld, float4( directionOs, 0.0 ) ).xyz;\r\n}\r\n\r\n/// Object space to world space, as a normal. Uses the inverse transpose, so non-uniform scale is safe.\r\npublic float3 PrismObjectToWorldNormal( float3 normalOs )\r\n{\r\n\treturn normalize( mul( float4( normalOs, 0.0 ), gPrismEnv.Object.WorldToObject ).xyz );\r\n}\r\n\r\n/// World space to object space, as a position.\r\npublic float3 PrismWorldToObjectPoint( float3 positionWs )\r\n{\r\n\treturn mul( gPrismEnv.Object.WorldToObject, float4( positionWs, 1.0 ) ).xyz;\r\n}\r\n\r\n/// World space to clip space.\r\npublic float4 PrismWorldToClip( float3 positionWs )\r\n{\r\n\treturn mul( gPrismEnv.Frame.WorldToProjection, float4( positionWs, 1.0 ) );\r\n}\r\n\r\n/// Clip space to a 0..1 screen UV, with the origin in the top left.\r\npublic float2 PrismScreenUvFromClip( float4 positionPs )\r\n{\r\n\tfloat2 ndc = positionPs.xy / max( abs( positionPs.w ), 1.0e-6 );\r\n\treturn ndc * float2( 0.5, -0.5 ) \u002B 0.5;\r\n}\r\n\r\n\r\n// -----------------------------------------------------------------------------\r\n//  4. Math\r\n//\r\n//  The emitted code prefers these over the raw intrinsics wherever a zero or a\r\n//  denormal would otherwise produce a NaN that is invisible until it is not.\r\n// -----------------------------------------------------------------------------\r\n\r\n/// Normalise, returning a zero vector instead of a NaN for a zero-length input.\r\npublic float3 PrismSafeNormalize( float3 v )\r\n{\r\n\tfloat lengthSquared = dot( v, v );\r\n\treturn lengthSquared \u003E 1.0e-12 ? v * rsqrt( lengthSquared ) : float3( 0.0 );\r\n}\r\n\r\n/// Normalise a 2D vector, returning zero instead of a NaN for a zero-length input.\r\npublic float2 PrismSafeNormalize( float2 v )\r\n{\r\n\tfloat lengthSquared = dot( v, v );\r\n\treturn lengthSquared \u003E 1.0e-12 ? v * rsqrt( lengthSquared ) : float2( 0.0 );\r\n}\r\n\r\n/// Reciprocal that returns zero rather than an infinity at zero.\r\npublic float PrismSafeRcp( float v )\r\n{\r\n\treturn abs( v ) \u003E 1.0e-12 ? 1.0 / v : 0.0;\r\n}\r\n\r\n/// Divide, returning zero rather than a NaN or an infinity when the denominator vanishes.\r\npublic float3 PrismSafeDivide( float3 a, float3 b )\r\n{\r\n\tbool3  ok       = abs( b ) \u003E float3( 1.0e-12 );\r\n\tfloat3 divisor  = select( ok, b, float3( 1.0 ) );\r\n\treturn select( ok, a / divisor, float3( 0.0 ) );\r\n}\r\n\r\n/// Linear remap from one inclusive range to another. Both ranges are packed as (min, max).\r\npublic float PrismRemap( float value, float2 fromRange, float2 toRange )\r\n{\r\n\tfloat t = ( value - fromRange.x ) * PrismSafeRcp( fromRange.y - fromRange.x );\r\n\treturn lerp( toRange.x, toRange.y, t );\r\n}\r\n\r\n/// Build a tangent-to-world basis from an interpolated normal and tangent.\r\npublic float3x3 PrismTangentBasis( float3 normalWs, float3 tangentUWs, float3 tangentVWs )\r\n{\r\n\tfloat3 n = PrismSafeNormalize( normalWs );\r\n\tfloat3 t = PrismSafeNormalize( tangentUWs - n * dot( n, tangentUWs ) );\r\n\tfloat3 b = PrismSafeNormalize( tangentVWs );\r\n\treturn float3x3( t, b, n );\r\n}\r\n\r\n\r\n// -----------------------------------------------------------------------------\r\n//  5. Textures\r\n//\r\n//  GetDimensions writes through out parameters and therefore cannot appear in an\r\n//  expression. These wrappers give the graph a value it can feed into math.\r\n// -----------------------------------------------------------------------------\r\n\r\n/// Width and height of a 2D texture, in texels.\r\npublic float2 PrismTextureSize( Texture2D texture )\r\n{\r\n\tuint width, height;\r\n\ttexture.GetDimensions( width, height );\r\n\treturn float2( width, height );\r\n}\r\n\r\n/// Width, height and slice count of a 2D texture array, in texels.\r\npublic float3 PrismTextureSize( Texture2DArray texture )\r\n{\r\n\tuint width, height, slices;\r\n\ttexture.GetDimensions( width, height, slices );\r\n\treturn float3( width, height, slices );\r\n}\r\n\r\n/// Width, height and depth of a 3D texture, in texels.\r\npublic float3 PrismTextureSize( Texture3D texture )\r\n{\r\n\tuint width, height, depth;\r\n\ttexture.GetDimensions( width, height, depth );\r\n\treturn float3( width, height, depth );\r\n}\r\n\r\n/// Face width and height of a cube map, in texels.\r\npublic float2 PrismTextureSize( TextureCube texture )\r\n{\r\n\tuint width, height;\r\n\ttexture.GetDimensions( width, height );\r\n\treturn float2( width, height );\r\n}\r\n\r\n\r\n// -----------------------------------------------------------------------------\r\n//  6. Colour\r\n// -----------------------------------------------------------------------------\r\n\r\n/// sRGB to linear, using the exact piecewise transfer function.\r\npublic float3 PrismSrgbToLinear( float3 srgb )\r\n{\r\n\tfloat3 low  = srgb / 12.92;\r\n\tfloat3 high = pow( max( ( srgb \u002B 0.055 ) / 1.055, 0.0 ), 2.4 );\r\n\treturn select( srgb \u003C= float3( 0.04045 ), low, high );\r\n}\r\n\r\n/// sRGB to linear, leaving alpha alone.\r\npublic float4 PrismSrgbToLinear( float4 srgb )\r\n{\r\n\treturn float4( PrismSrgbToLinear( srgb.rgb ), srgb.a );\r\n}\r\n\r\n/// Linear to sRGB, using the exact piecewise transfer function.\r\npublic float3 PrismLinearToSrgb( float3 linearColor )\r\n{\r\n\tfloat3 low  = linearColor * 12.92;\r\n\tfloat3 high = 1.055 * pow( max( linearColor, 0.0 ), 1.0 / 2.4 ) - 0.055;\r\n\treturn select( linearColor \u003C= float3( 0.0031308 ), low, high );\r\n}\r\n\r\n/// Linear to sRGB, leaving alpha alone.\r\npublic float4 PrismLinearToSrgb( float4 linearColor )\r\n{\r\n\treturn float4( PrismLinearToSrgb( linearColor.rgb ), linearColor.a );\r\n}\r\n\r\n/// Rec. 709 relative luminance of a linear colour.\r\npublic float PrismLuminance( float3 linearColor )\r\n{\r\n\treturn dot( linearColor, float3( 0.2126, 0.7152, 0.0722 ) );\r\n}\r\n\r\n/// RGB to HSV. Hue is 0..1, not degrees.\r\npublic float3 PrismRgbToHsv( float3 rgb )\r\n{\r\n\tconst float4 k = float4( 0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0 );\r\n\tconst float  epsilon = 1.0e-10;\r\n\r\n\tfloat4 p = select( bool4( rgb.g \u003C rgb.b ), float4( rgb.bg, k.wz ), float4( rgb.gb, k.xy ) );\r\n\tfloat4 q = select( bool4( rgb.r \u003C p.x ), float4( p.xyw, rgb.r ), float4( rgb.r, p.yzx ) );\r\n\r\n\tfloat chroma = q.x - min( q.w, q.y );\r\n\treturn float3( abs( q.z \u002B ( q.w - q.y ) / ( 6.0 * chroma \u002B epsilon ) ), chroma / ( q.x \u002B epsilon ), q.x );\r\n}\r\n\r\n/// HSV to RGB. Hue is 0..1, not degrees.\r\npublic float3 PrismHsvToRgb( float3 hsv )\r\n{\r\n\tconst float4 k = float4( 1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0 );\r\n\r\n\tfloat3 p = abs( frac( hsv.xxx \u002B k.xyz ) * 6.0 - k.www );\r\n\treturn hsv.z * lerp( k.xxx, saturate( p - k.xxx ), hsv.y );\r\n}\r\n\r\n/// Blend two linear colours with the classic overlay operator.\r\npublic float3 PrismOverlay( float3 baseColor, float3 blend )\r\n{\r\n\tfloat3 low  = 2.0 * baseColor * blend;\r\n\tfloat3 high = 1.0 - 2.0 * ( 1.0 - baseColor ) * ( 1.0 - blend );\r\n\treturn select( baseColor \u003C= float3( 0.5 ), low, high );\r\n}\r\n\r\n\r\n// -----------------------------------------------------------------------------\r\n//  7. PrismMaterial\r\n//\r\n//  What a surface graph produces. A host renderer reads these fields and runs\r\n//  whatever shading model it likes; \u0060ToUnlitColor\u0060 is the trivial one.\r\n// -----------------------------------------------------------------------------\r\n\r\n/// The surface description a Prism surface graph fills in.\r\npublic struct PrismMaterial\r\n{\r\n\t/// Linear base colour.\r\n\tfloat3 Albedo;\r\n\t/// Coverage. Compared against the alpha-test threshold for a masked material.\r\n\tfloat Opacity;\r\n\t/// Tangent-space normal, with the usual (0, 0, 1) meaning \u0022unperturbed\u0022.\r\n\tfloat3 Normal;\r\n\t/// Perceptual roughness, 0 mirror to 1 fully rough.\r\n\tfloat Roughness;\r\n\t/// Metalness, 0 dielectric to 1 conductor.\r\n\tfloat Metalness;\r\n\t/// Baked ambient occlusion.\r\n\tfloat AmbientOcclusion;\r\n\t/// Linear emissive radiance.\r\n\tfloat3 Emission;\r\n\t/// Light transmitted through the surface.\r\n\tfloat3 Transmission;\r\n\t/// Where a per-instance tint applies.\r\n\tfloat TintMask;\r\n\r\n\t/// A sensible neutral surface: white, opaque, flat, rough, dielectric.\r\n\tpublic static PrismMaterial Init()\r\n\t{\r\n\t\tPrismMaterial m;\r\n\t\tm.Albedo = float3( 1.0 );\r\n\t\tm.Opacity = 1.0;\r\n\t\tm.Normal = float3( 0.0, 0.0, 1.0 );\r\n\t\tm.Roughness = 1.0;\r\n\t\tm.Metalness = 0.0;\r\n\t\tm.AmbientOcclusion = 1.0;\r\n\t\tm.Emission = float3( 0.0 );\r\n\t\tm.Transmission = float3( 0.0 );\r\n\t\tm.TintMask = 1.0;\r\n\t\treturn m;\r\n\t}\r\n\r\n\t/// Replace the tangent-space normal. Mutates, so it carries [mutating].\r\n\t[mutating]\r\n\tpublic void SetNormal( float3 tangentSpaceNormal )\r\n\t{\r\n\t\tNormal = PrismSafeNormalize( tangentSpaceNormal );\r\n\t}\r\n\r\n\t/// Kill the fragment when coverage falls below a threshold. Pixel stage only.\r\n\tpublic void AlphaTest( float threshold )\r\n\t{\r\n\t\tif ( Opacity \u003C threshold ) discard;\r\n\t}\r\n\r\n\t/// The world-space normal implied by this material\u0027s tangent-space normal.\r\n\tpublic float3 WorldNormal( float3x3 tangentBasis )\r\n\t{\r\n\t\treturn PrismSafeNormalize( mul( Normal, tangentBasis ) );\r\n\t}\r\n\r\n\t/// The unlit resolve: albedo plus emission, with coverage in alpha.\r\n\tpublic float4 ToUnlitColor()\r\n\t{\r\n\t\treturn float4( Albedo \u002B Emission, Opacity );\r\n\t}\r\n}\r\n\u0022\u0022\u0022;\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/Compiler/CompileMode.cs","FileName":"CompileMode.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"namespace Editor.Prism.Compiler;\r\n\r\n/// \u003Csummary\u003E\r\n/// What a compile is for. The mode changes what the compiler emits, not just where it writes it.\r\n/// \u003C/summary\u003E\r\npublic enum CompileMode\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// The artifact saved beside the document. Literals are baked, every declared mode and combo is\r\n\t/// emitted, and no preview instrumentation is added.\r\n\t/// \u003C/summary\u003E\r\n\tFinal,\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The live viewport shader. Literals become named uniforms recorded in\r\n\t/// \u003Cc\u003ECompileResult.PreviewAttributes\u003C/c\u003E, so dragging a slider updates at frame rate with zero\r\n\t/// recompiles. The minimum combo set is declared to keep compile latency down.\r\n\t/// \u003C/summary\u003E\r\n\tPreview,\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// One shader containing every previewable node\u0027s expression behind a stage-id switch, used to\r\n\t/// render all node thumbnails from a single compile.\r\n\t/// \u003C/summary\u003E\r\n\tThumbnail,\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Type-check and emit far enough to produce diagnostics, then stop. Used by the debounced\r\n\t/// validation pass and by the code panel\u0027s IR tab.\r\n\t/// \u003C/summary\u003E\r\n\tSyntaxOnly\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/Compiler/CompileResult.cs","FileName":"CompileResult.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using Editor.Prism.Compiler.Backends;\r\nusing Editor.Prism.Compiler.Ir;\r\nusing Editor.Prism.Core;\r\n\r\nnamespace Editor.Prism.Compiler;\r\n\r\n/// \u003Csummary\u003E\r\n/// A uniform the preview can push straight to the GPU. In \u003Csee cref=\u0022CompileMode.Preview\u0022/\u003E every\r\n/// literal and every graph parameter becomes one of these, which is why dragging a slider costs zero\r\n/// compiles. Pushed with a dictionary indexer, never \u003Cc\u003EDictionary.Add\u003C/c\u003E \u2014 the built-in editor\u0027s\r\n/// attribute helper throws on a duplicate name.\r\n/// \u003C/summary\u003E\r\npublic sealed record PreviewAttribute( string Name, ShaderType Type, ConstValue Value )\r\n{\r\n\t/// \u003Csummary\u003EThe node whose literal this is, when it came from one.\u003C/summary\u003E\r\n\tpublic NodeId Node { get; init; }\r\n\r\n\t/// \u003Csummary\u003EThe port whose literal this is, when it came from one.\u003C/summary\u003E\r\n\tpublic PortId Port { get; init; }\r\n\r\n\t/// \u003Csummary\u003EThe blackboard parameter this came from, when it came from one.\u003C/summary\u003E\r\n\tpublic ParamId Parameter { get; init; }\r\n\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tpublic override string ToString() =\u003E $\u0022{Type.Hlsl} {Name} = {Value}\u0022;\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// A texture slot the preview has to fill itself, because nothing bakes it for a shader rendered\r\n/// without a material. See \u003Cc\u003ENodeEmitter.PreviewTextureBinding\u003C/c\u003E for why this exists.\r\n/// \u003C/summary\u003E\r\n/// \u003Cparam name=\u0022Name\u0022\u003EThe render-attribute name the shader binds the slot to.\u003C/param\u003E\r\n/// \u003Cparam name=\u0022Asset\u0022\u003EPath of the source image the graph asked for.\u003C/param\u003E\r\n/// \u003Cparam name=\u0022Srgb\u0022\u003ETrue when the slot holds sRGB-encoded colour rather than linear data.\u003C/param\u003E\r\npublic sealed record PreviewTexture( string Name, string Asset, bool Srgb )\r\n{\r\n\t/// \u003Csummary\u003EThe blackboard parameter behind the slot, when it came from one.\u003C/summary\u003E\r\n\tpublic ParamId Parameter { get; init; }\r\n\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tpublic override string ToString() =\u003E $\u0022{Name} = \\\u0022{Asset}\\\u0022{( Srgb ? \u0022 (srgb)\u0022 : string.Empty )}\u0022;\r\n}\r\n\r\n/// \u003Csummary\u003ECounters for the status bar and for spotting performance regressions between builds.\u003C/summary\u003E\r\npublic sealed record CompileStats\r\n{\r\n\t/// \u003Csummary\u003ENodes visited during emission.\u003C/summary\u003E\r\n\tpublic int NodeCount { get; init; }\r\n\r\n\t/// \u003Csummary\u003EStatements in the emitted module.\u003C/summary\u003E\r\n\tpublic int StatementCount { get; init; }\r\n\r\n\t/// \u003Csummary\u003ETemps bound by the emitter after CSE.\u003C/summary\u003E\r\n\tpublic int TempCount { get; init; }\r\n\r\n\t/// \u003Csummary\u003EExpressions removed by CSE, folding and dead-code elimination.\u003C/summary\u003E\r\n\tpublic int OptimizedAway { get; init; }\r\n\r\n\t/// \u003Csummary\u003EModule-level declarations emitted.\u003C/summary\u003E\r\n\tpublic int GlobalCount { get; init; }\r\n\r\n\t/// \u003Csummary\u003EInterpolators allocated.\u003C/summary\u003E\r\n\tpublic int VaryingCount { get; init; }\r\n\r\n\t/// \u003Csummary\u003EHelper functions emitted.\u003C/summary\u003E\r\n\tpublic int HelperCount { get; init; }\r\n\r\n\t/// \u003Csummary\u003EMilliseconds spent in validation, solving and stage planning.\u003C/summary\u003E\r\n\tpublic double AnalysisMs { get; init; }\r\n\r\n\t/// \u003Csummary\u003EMilliseconds spent building and optimising the IR.\u003C/summary\u003E\r\n\tpublic double EmitMs { get; init; }\r\n\r\n\t/// \u003Csummary\u003EMilliseconds spent in the backends.\u003C/summary\u003E\r\n\tpublic double BackendMs { get; init; }\r\n\r\n\t/// \u003Csummary\u003ETotal wall time of the compile.\u003C/summary\u003E\r\n\tpublic double TotalMs { get; init; }\r\n\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tpublic override string ToString() =\u003E\r\n\t\t$\u0022{NodeCount} nodes, {StatementCount} statements, {TotalMs:0} ms\u0022;\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The result of a compile: one artifact per requested backend, everything that went wrong, the\r\n/// uniforms the preview can push live, and the counters for the status bar.\r\n/// \u003C/summary\u003E\r\npublic sealed record CompileResult(\r\n\tbool Ok,\r\n\tIReadOnlyDictionary\u003Cstring, BackendEmitResult\u003E Artifacts,\r\n\tIReadOnlyList\u003CDiagnostic\u003E Diagnostics,\r\n\tIReadOnlyList\u003CPreviewAttribute\u003E PreviewAttributes,\r\n\tCompileStats Stats )\r\n{\r\n\t/// \u003Csummary\u003EThe IR the artifacts were generated from. Kept so the code panel can print it.\u003C/summary\u003E\r\n\tpublic IrModule Module { get; init; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Texture slots the preview must push itself. Empty for every mode but\r\n\t/// \u003Csee cref=\u0022CompileMode.Preview\u0022/\u003E, where a shipping \u003Cc\u003ECreateInputTexture2D\u003C/c\u003E slot would never\r\n\t/// be filled because nothing compiles a material for the preview.\r\n\t/// \u003C/summary\u003E\r\n\tpublic IReadOnlyList\u003CPreviewTexture\u003E PreviewTextures { get; init; } = Array.Empty\u003CPreviewTexture\u003E();\r\n\r\n\t/// \u003Csummary\u003EThe request this result answers.\u003C/summary\u003E\r\n\tpublic CompileRequest Request { get; init; }\r\n\r\n\t/// \u003Csummary\u003ENumber of errors reported.\u003C/summary\u003E\r\n\tpublic int ErrorCount =\u003E Diagnostics?.Count( x =\u003E x.Severity == DiagnosticSeverity.Error ) ?? 0;\r\n\r\n\t/// \u003Csummary\u003ENumber of warnings reported.\u003C/summary\u003E\r\n\tpublic int WarningCount =\u003E Diagnostics?.Count( x =\u003E x.Severity == DiagnosticSeverity.Warning ) ?? 0;\r\n\r\n\t/// \u003Csummary\u003EThe artifact produced by a backend, or null when that backend was not requested.\u003C/summary\u003E\r\n\tpublic BackendEmitResult Artifact( string backendId ) =\u003E\r\n\t\tArtifacts is not null \u0026\u0026 Artifacts.TryGetValue( backendId, out var result ) ? result : null;\r\n\r\n\t/// \u003Csummary\u003EThe generated \u003Cc\u003E.shader\u003C/c\u003E text, when the s\u0026amp;box backend ran.\u003C/summary\u003E\r\n\tpublic string ShaderText =\u003E Artifact( PrismConstants.BackendHlsl )?.Text;\r\n\r\n\t/// \u003Csummary\u003EThe generated \u003Cc\u003E.slang\u003C/c\u003E text, when the Slang backend ran.\u003C/summary\u003E\r\n\tpublic string SlangText =\u003E Artifact( PrismConstants.BackendSlang )?.Text;\r\n\r\n\t/// \u003Csummary\u003EA failed result carrying only diagnostics.\u003C/summary\u003E\r\n\tpublic static CompileResult Failed( IReadOnlyList\u003CDiagnostic\u003E diagnostics, CompileRequest request = null ) =\u003E\r\n\t\tnew( false, new Dictionary\u003Cstring, BackendEmitResult\u003E(), diagnostics ?? Array.Empty\u003CDiagnostic\u003E(),\r\n\t\t\tArray.Empty\u003CPreviewAttribute\u003E(), new CompileStats() )\r\n\t\t{\r\n\t\t\tRequest = request\r\n\t\t};\r\n\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tpublic override string ToString() =\u003E\r\n\t\t$\u0022{( Ok ? \u0022ok\u0022 : \u0022failed\u0022 )}: {ErrorCount} errors, {WarningCount} warnings, {Stats}\u0022;\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/Compiler/IrConversions.cs","FileName":"IrConversions.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using Editor.Prism.Compiler.Ir;\r\nusing Editor.Prism.Core;\r\n\r\nnamespace Editor.Prism.Compiler;\r\n\r\n/// \u003Csummary\u003E\r\n/// The one lowering of an implicit conversion into IR.\r\n/// \u003Cpara\u003E\r\n/// Two callers need it and used to spell it differently. \u003Cc\u003ENodeEmitContext.Coerce\u003C/c\u003E lowered a\r\n/// narrowing as a swizzle plus an optional convert; \u003Cc\u003EGraphCompiler.Fit\u003C/c\u003E lowered the identical\r\n/// conversion as a single \u003Cc\u003ECastKind.Truncate\u003C/c\u003E, which the HLSL backend only renders as a mask when\r\n/// the scalar kinds already agree and otherwise falls back to a C-style \u003Cc\u003E( float3 )v\u003C/c\u003E. Both are\r\n/// legal, but two structurally different expressions for one conversion never hash-cons against each\r\n/// other, so CSE missed and the generated text differed between two paths for no reason.\r\n/// \u003C/para\u003E\r\n/// \u003Cpara\u003E\r\n/// This class holds no diagnostics on purpose: reporting a lossy or illegal conversion is the caller\u0027s\r\n/// job, because only the caller knows which port to attach it to. An unclassifiable conversion comes\r\n/// back as \u003Csee cref=\u0022IrValue.Invalid\u0022/\u003E.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\npublic static class IrConversions\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Emit the conversion of \u003Cparamref name=\u0022value\u0022/\u003E to \u003Cparamref name=\u0022target\u0022/\u003E, or\r\n\t/// \u003Csee cref=\u0022IrValue.Invalid\u0022/\u003E when the two types cannot be converted at all.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Cparam name=\u0022builder\u0022\u003EThe builder to emit into.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022value\u0022\u003EThe value being converted.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022target\u0022\u003EThe type wanted.\u003C/param\u003E\r\n\t/// \u003Cparam name=\u0022fill\u0022\u003EWhat to pad a widening with, or null for \u003Cc\u003ETypeRules.DefaultFill\u003C/c\u003E.\u003C/param\u003E\r\n\tpublic static IrValue Emit( IrBuilder builder, IrValue value, ShaderType target, float? fill = null )\r\n\t{\r\n\t\tif ( builder is null || !value.IsValid ) return IrValue.Invalid;\r\n\t\tif ( target.IsVoid || value.Type == target ) return value;\r\n\r\n\t\tvar from = value.Type;\r\n\r\n\t\tswitch ( TypeRules.Classify( from, target ) )\r\n\t\t{\r\n\t\t\tcase ConversionKind.Identity:\r\n\t\t\t\treturn value;\r\n\r\n\t\t\tcase ConversionKind.Splat:\r\n\t\t\t\treturn builder.Cast( target, value, CastKind.Splat );\r\n\r\n\t\t\tcase ConversionKind.Widen:\r\n\t\t\tcase ConversionKind.IntToFloat:\r\n\t\t\t\treturn builder.Cast( target, value, CastKind.Convert );\r\n\r\n\t\t\tcase ConversionKind.Pad:\r\n\t\t\t{\r\n\t\t\t\tvar actual = fill ?? TypeRules.DefaultFill( from, target, target.Components - 1 );\r\n\r\n\t\t\t\t// Convert the components before widening, so the pad literal and the existing lanes are\r\n\t\t\t\t// already the same scalar kind by the time the constructor is printed.\r\n\t\t\t\tvar widened = from.Scalar == target.Scalar\r\n\t\t\t\t\t? value\r\n\t\t\t\t\t: builder.Cast( from.WithScalar( target.Scalar ), value, CastKind.Convert );\r\n\r\n\t\t\t\treturn builder.Cast( target, widened, CastKind.Pad, actual );\r\n\t\t\t}\r\n\r\n\t\t\tcase ConversionKind.Truncate:\r\n\t\t\t{\r\n\t\t\t\tvar narrowed = value;\r\n\r\n\t\t\t\tif ( from.IsScalarOrVector \u0026\u0026 target.IsScalarOrVector \u0026\u0026 from.Components \u003E target.Components )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar mask = \u0022xyzw\u0022[..Math.Clamp( target.Components, 1, 4 )];\r\n\t\t\t\t\tnarrowed = builder.Swizzle( ShaderType.Vec( from.Scalar, target.Components ), value, mask );\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif ( narrowed.Type == target ) return narrowed;\r\n\r\n\t\t\t\treturn builder.Cast( target, narrowed, CastKind.Convert );\r\n\t\t\t}\r\n\r\n\t\t\tdefault:\r\n\t\t\t\treturn IrValue.Invalid;\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/Compiler/TypeSolver.cs","FileName":"TypeSolver.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using Editor.Prism.Core;\r\nusing Editor.Prism.Model;\r\n\r\nnamespace Editor.Prism.Compiler;\r\n\r\n/// \u003Csummary\u003E\r\n/// The result of running \u003Csee cref=\u0022TypeSolver\u0022/\u003E over a graph: a concrete \u003Csee cref=\u0022ShaderType\u0022/\u003E\r\n/// for every port, the conversion each edge performs, and a topological node order the rest of the\r\n/// pipeline can reuse.\r\n/// \u003C/summary\u003E\r\npublic sealed class TypeSolution\r\n{\r\n\tinternal TypeSolution(\r\n\t\tIReadOnlyDictionary\u003CPortRef, ShaderType\u003E types,\r\n\t\tIReadOnlyDictionary\u003CEdgeId, ConversionKind\u003E conversions,\r\n\t\tIReadOnlyList\u003CNodeId\u003E order,\r\n\t\tint unresolved,\r\n\t\tbool ok )\r\n\t{\r\n\t\tTypes = types;\r\n\t\tConversions = conversions;\r\n\t\tTopologicalOrder = order;\r\n\t\tUnresolvedCount = unresolved;\r\n\t\tOk = ok;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EAn empty solution, used when there is nothing to solve.\u003C/summary\u003E\r\n\tpublic static TypeSolution Empty { get; } = new(\r\n\t\tnew Dictionary\u003CPortRef, ShaderType\u003E(), new Dictionary\u003CEdgeId, ConversionKind\u003E(),\r\n\t\tArray.Empty\u003CNodeId\u003E(), 0, true );\r\n\r\n\t/// \u003Csummary\u003ETrue when every port resolved and no unification failed.\u003C/summary\u003E\r\n\tpublic bool Ok { get; }\r\n\r\n\t/// \u003Csummary\u003EHow many ports had to fall back to a default type.\u003C/summary\u003E\r\n\tpublic int UnresolvedCount { get; }\r\n\r\n\t/// \u003Csummary\u003EThe solved type of every port in the graph.\u003C/summary\u003E\r\n\tpublic IReadOnlyDictionary\u003CPortRef, ShaderType\u003E Types { get; }\r\n\r\n\t/// \u003Csummary\u003EThe conversion each edge performs, for wire markers and tooltips.\u003C/summary\u003E\r\n\tpublic IReadOnlyDictionary\u003CEdgeId, ConversionKind\u003E Conversions { get; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Producers before consumers. Nodes caught in a cycle are appended at the end in document order,\r\n\t/// so this is always a total order even for a malformed graph.\r\n\t/// \u003C/summary\u003E\r\n\tpublic IReadOnlyList\u003CNodeId\u003E TopologicalOrder { get; }\r\n\r\n\t/// \u003Csummary\u003EThe solved type of one port, or void when it is not in the solution.\u003C/summary\u003E\r\n\tpublic ShaderType TypeOf( NodeId node, PortId port ) =\u003E\r\n\t\tTypes.TryGetValue( new PortRef( node, port ), out var type ) ? type : ShaderType.Void;\r\n\r\n\t/// \u003Csummary\u003EThe solved type of one port.\u003C/summary\u003E\r\n\tpublic ShaderType TypeOf( Port port ) =\u003E\r\n\t\tport is null ? ShaderType.Void : TypeOf( port.Node?.Id ?? NodeId.None, port.Id );\r\n\r\n\t/// \u003Csummary\u003EThe conversion an edge performs, or \u003Csee cref=\u0022ConversionKind.Identity\u0022/\u003E when unknown.\u003C/summary\u003E\r\n\tpublic ConversionKind ConversionOn( EdgeId edge ) =\u003E\r\n\t\tConversions.TryGetValue( edge, out var kind ) ? kind : ConversionKind.Identity;\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Hindley\u2013Milner-lite unification over a whole graph.\r\n/// \u003Cpara\u003E\r\n/// A port\u0027s declared type is either a concrete spelling (\u003Cc\u003Efloat3\u003C/c\u003E, \u003Cc\u003ETexture2D\u003C/c\u003E) or a term in\r\n/// a small algebra: \u003Cc\u003ET\u003C/c\u003E \u2014 a variable shared by every port on the node that names it; \u003Cc\u003ET.scalar\u003C/c\u003E\r\n/// \u2014 the component type of \u003Cc\u003ET\u003C/c\u003E; \u003Cc\u003EvecN\u003C/c\u003E \u2014 a float vector whose width unifies; \u003Cc\u003Efloat{N}\u003C/c\u003E \u2014\r\n/// a float vector sharing the width variable \u003Cc\u003EN\u003C/c\u003E; \u003Cc\u003Eany\u003C/c\u003E \u2014 a passthrough that adopts whatever\r\n/// reaches it. Every unrecognised spelling is treated as a fresh variable named after itself, so\r\n/// \u003Cc\u003EU\u003C/c\u003E and \u003Cc\u003EElement\u003C/c\u003E work exactly like \u003Cc\u003ET\u003C/c\u003E.\r\n/// \u003C/para\u003E\r\n/// \u003Cpara\u003E\r\n/// The solver runs forward along the topological order, then backward for anything still open, then\r\n/// defaults what is left to \u003Cc\u003Efloat\u003C/c\u003E. Afterwards every \u003Csee cref=\u0022Port.ResolvedType\u0022/\u003E is concrete\r\n/// and every edge has been classified, which is what lets the IR be built without a single type guess.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\npublic sealed class TypeSolver\r\n{\r\n\tconst string PassthroughGroup = \u0022\u0001passthrough\u0022;\r\n\r\n\treadonly IPrismGraph _graph;\r\n\treadonly DiagnosticSink _diagnostics;\r\n\r\n\treadonly List\u003CSlot\u003E _slots = new();\r\n\treadonly Dictionary\u003C(NodeId Node, string Name), int\u003E _vars = new();\r\n\treadonly Dictionary\u003CPortRef, Term\u003E _terms = new();\r\n\r\n\tbool _failed;\r\n\r\n\t/// \u003Csummary\u003EBuild a solver for one graph.\u003C/summary\u003E\r\n\tpublic TypeSolver( IPrismGraph graph, DiagnosticSink diagnostics )\r\n\t{\r\n\t\t_graph = graph;\r\n\t\t_diagnostics = diagnostics ?? new DiagnosticSink();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EHow many forward/backward sweeps to run before giving up on convergence.\u003C/summary\u003E\r\n\tpublic int MaxIterations { get; set; } = 8;\r\n\r\n\t/// \u003Csummary\u003EWrite the solved types back onto \u003Csee cref=\u0022Port.ResolvedType\u0022/\u003E. On by default.\u003C/summary\u003E\r\n\tpublic bool ApplyToPorts { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003EReport warnings for lossy and padded edge conversions. On by default.\u003C/summary\u003E\r\n\tpublic bool ReportConversions { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003ESolve a graph in one call.\u003C/summary\u003E\r\n\tpublic static TypeSolution Solve( IPrismGraph graph, DiagnosticSink diagnostics ) =\u003E\r\n\t\tnew TypeSolver( graph, diagnostics ).Solve();\r\n\r\n\t/// \u003Csummary\u003ERun the solver.\u003C/summary\u003E\r\n\tpublic TypeSolution Solve()\r\n\t{\r\n\t\tif ( _graph?.Nodes is null || _graph.Nodes.Count == 0 ) return TypeSolution.Empty;\r\n\r\n\t\tvar order = TopologicalOrder( _graph );\r\n\r\n\t\tSeed();\r\n\r\n\t\tvar edges = ValidEdges().ToArray();\r\n\r\n\t\tfor ( int pass = 0; pass \u003C Math.Max( 1, MaxIterations ); pass\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar changed = Forward( order, edges );\r\n\t\t\tchanged |= Backward( edges );\r\n\r\n\t\t\tif ( !changed ) break;\r\n\t\t}\r\n\r\n\t\tDefaultUnresolved();\r\n\r\n\t\tvar types = new Dictionary\u003CPortRef, ShaderType\u003E();\r\n\t\tvar unresolved = 0;\r\n\r\n\t\tforeach ( var node in _graph.Nodes )\r\n\t\t{\r\n\t\t\tif ( node is null ) continue;\r\n\r\n\t\t\tforeach ( var port in AllPorts( node ) )\r\n\t\t\t{\r\n\t\t\t\tvar key = new PortRef( node.Id, port.Id );\r\n\t\t\t\tvar type = Read( key );\r\n\r\n\t\t\t\tif ( type.IsVoid )\r\n\t\t\t\t{\r\n\t\t\t\t\ttype = ShaderType.Float;\r\n\t\t\t\t\tunresolved\u002B\u002B;\r\n\r\n\t\t\t\t\tif ( !port.Def.IsGeneric )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\t// A concrete declaration that came back void means the spelling is unparseable.\r\n\t\t\t\t\t\t_diagnostics.Warn( DiagnosticCode.UnresolvedType,\r\n\t\t\t\t\t\t\t$\u0022Port \u0027{port.DisplayName}\u0027 declares an unrecognised type \u0027{port.DeclaredType}\u0027; assuming float\u0022,\r\n\t\t\t\t\t\t\tGraphRef.ForPort( node.Id, port.Id ) );\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\ttypes[key] = type;\r\n\r\n\t\t\t\tif ( ApplyToPorts ) port.ResolvedType = type;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tvar conversions = ClassifyEdges( edges, types );\r\n\r\n\t\treturn new TypeSolution( types, conversions, order, unresolved, !_failed );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Producers before consumers, cycles appended in document order. Kahn\u0027s algorithm, so a cyclic\r\n\t/// graph degrades into a stable-but-arbitrary order instead of hanging.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003CNodeId\u003E TopologicalOrder( IPrismGraph graph )\r\n\t{\r\n\t\tif ( graph?.Nodes is null ) return Array.Empty\u003CNodeId\u003E();\r\n\r\n\t\tvar indegree = new Dictionary\u003CNodeId, int\u003E();\r\n\t\tvar successors = new Dictionary\u003CNodeId, List\u003CNodeId\u003E\u003E();\r\n\r\n\t\tforeach ( var node in graph.Nodes )\r\n\t\t{\r\n\t\t\tif ( node is null ) continue;\r\n\r\n\t\t\tindegree.TryAdd( node.Id, 0 );\r\n\t\t\tsuccessors.TryAdd( node.Id, new List\u003CNodeId\u003E() );\r\n\t\t}\r\n\r\n\t\tforeach ( var edge in graph.Edges ?? Array.Empty\u003CEdge\u003E() )\r\n\t\t{\r\n\t\t\tif ( edge is null || !edge.IsValid ) continue;\r\n\t\t\tif ( !indegree.ContainsKey( edge.FromNode ) || !indegree.ContainsKey( edge.ToNode ) ) continue;\r\n\t\t\tif ( edge.FromNode == edge.ToNode ) continue;\r\n\r\n\t\t\tsuccessors[edge.FromNode].Add( edge.ToNode );\r\n\t\t\tindegree[edge.ToNode] = indegree[edge.ToNode] \u002B 1;\r\n\t\t}\r\n\r\n\t\t// Seed in document order so the result is deterministic run to run.\r\n\t\tvar ready = new List\u003CNodeId\u003E();\r\n\r\n\t\tforeach ( var node in graph.Nodes )\r\n\t\t{\r\n\t\t\tif ( node is null ) continue;\r\n\t\t\tif ( indegree[node.Id] == 0 ) ready.Add( node.Id );\r\n\t\t}\r\n\r\n\t\tvar order = new List\u003CNodeId\u003E( indegree.Count );\r\n\t\tvar cursor = 0;\r\n\r\n\t\twhile ( cursor \u003C ready.Count )\r\n\t\t{\r\n\t\t\tvar id = ready[cursor\u002B\u002B];\r\n\t\t\torder.Add( id );\r\n\r\n\t\t\tforeach ( var next in successors[id] )\r\n\t\t\t{\r\n\t\t\t\tvar remaining = indegree[next] - 1;\r\n\t\t\t\tindegree[next] = remaining;\r\n\r\n\t\t\t\tif ( remaining == 0 ) ready.Add( next );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( order.Count \u003C indegree.Count )\r\n\t\t{\r\n\t\t\tvar seen = new HashSet\u003CNodeId\u003E( order );\r\n\r\n\t\t\tforeach ( var node in graph.Nodes )\r\n\t\t\t{\r\n\t\t\t\tif ( node is null || seen.Contains( node.Id ) ) continue;\r\n\r\n\t\t\t\torder.Add( node.Id );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn order;\r\n\t}\r\n\r\n\t// ---- seeding ----------------------------------------------------------\r\n\r\n\tvoid Seed()\r\n\t{\r\n\t\tforeach ( var node in _graph.Nodes )\r\n\t\t{\r\n\t\t\tif ( node is null ) continue;\r\n\r\n\t\t\tforeach ( var port in AllPorts( node ) )\r\n\t\t\t{\r\n\t\t\t\t_terms[new PortRef( node.Id, port.Id )] = MakeTerm( node.Id, port );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tTerm MakeTerm( NodeId node, Port port )\r\n\t{\r\n\t\tvar declared = port.DeclaredType;\r\n\r\n\t\tif ( ( port.Flags \u0026 PortFlags.Passthrough ) != 0 )\r\n\t\t{\r\n\t\t\treturn Term.Variable( VarSlot( node, PassthroughGroup ), null );\r\n\t\t}\r\n\r\n\t\tif ( !TypeRules.IsTypeVariable( declared ) \u0026\u0026 ShaderType.TryParse( declared, out var concrete ) )\r\n\t\t{\r\n\t\t\treturn Term.Fixed( concrete );\r\n\t\t}\r\n\r\n\t\tvar text = ( declared ?? string.Empty ).Trim();\r\n\r\n\t\tif ( text.Length == 0 || text == TypeRules.TypeVarAny )\r\n\t\t{\r\n\t\t\treturn Term.Variable( VarSlot( node, PassthroughGroup ), null );\r\n\t\t}\r\n\r\n\t\tif ( text == TypeRules.TypeVarVecN )\r\n\t\t{\r\n\t\t\treturn Term.Variable( VarSlot( node, TypeRules.TypeVarVecN ), ScalarKind.Float );\r\n\t\t}\r\n\r\n\t\t// \u0022T.scalar\u0022 \u2014 the component type of another variable on the same node.\r\n\t\tvar dot = text.IndexOf( \u0027.\u0027 );\r\n\r\n\t\tif ( dot \u003E 0 \u0026\u0026 text[( dot \u002B 1 )..].Equals( \u0022scalar\u0022, StringComparison.OrdinalIgnoreCase ) )\r\n\t\t{\r\n\t\t\treturn Term.ScalarOf( VarSlot( node, text[..dot] ) );\r\n\t\t}\r\n\r\n\t\t// \u0022float{N}\u0022 \u2014 a vector of the shared width variable N, with the component kind pinned.\r\n\t\tvar open = text.IndexOf( \u0027{\u0027 );\r\n\t\tvar close = text.IndexOf( \u0027}\u0027 );\r\n\r\n\t\tif ( open \u003E 0 \u0026\u0026 close \u003E open \u002B 1 )\r\n\t\t{\r\n\t\t\tvar prefix = text[..open];\r\n\t\t\tvar width = text[( open \u002B 1 )..close];\r\n\t\t\tvar scalar = ShaderType.TryParse( prefix, out var prefixType ) \u0026\u0026 prefixType.IsNumeric\r\n\t\t\t\t? prefixType.Scalar\r\n\t\t\t\t: ScalarKind.Float;\r\n\r\n\t\t\treturn Term.Variable( VarSlot( node, width ), scalar );\r\n\t\t}\r\n\r\n\t\treturn Term.Variable( VarSlot( node, text ), null );\r\n\t}\r\n\r\n\tint VarSlot( NodeId node, string name )\r\n\t{\r\n\t\tvar key = (node, name ?? string.Empty);\r\n\r\n\t\tif ( _vars.TryGetValue( key, out var index ) ) return index;\r\n\r\n\t\tindex = _slots.Count;\r\n\t\t_slots.Add( new Slot() );\r\n\t\t_vars[key] = index;\r\n\r\n\t\treturn index;\r\n\t}\r\n\r\n\t// ---- propagation ------------------------------------------------------\r\n\r\n\tbool Forward( IReadOnlyList\u003CNodeId\u003E order, IReadOnlyList\u003CEdge\u003E edges )\r\n\t{\r\n\t\tvar incoming = new Dictionary\u003CPortRef, List\u003CEdge\u003E\u003E();\r\n\r\n\t\tforeach ( var edge in edges )\r\n\t\t{\r\n\t\t\tvar key = edge.To;\r\n\r\n\t\t\tif ( !incoming.TryGetValue( key, out var list ) )\r\n\t\t\t{\r\n\t\t\t\tlist = new List\u003CEdge\u003E();\r\n\t\t\t\tincoming[key] = list;\r\n\t\t\t}\r\n\r\n\t\t\tlist.Add( edge );\r\n\t\t}\r\n\r\n\t\tvar changed = false;\r\n\r\n\t\tforeach ( var id in order )\r\n\t\t{\r\n\t\t\tvar node = _graph.FindNode( id );\r\n\t\t\tif ( node is null ) continue;\r\n\r\n\t\t\tforeach ( var input in node.Inputs )\r\n\t\t\t{\r\n\t\t\t\tvar key = new PortRef( id, input.Id );\r\n\r\n\t\t\t\tif ( !incoming.TryGetValue( key, out var sources ) ) continue;\r\n\r\n\t\t\t\tforeach ( var edge in sources )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar produced = Read( edge.From );\r\n\t\t\t\t\tif ( produced.IsVoid ) continue;\r\n\r\n\t\t\t\t\tchanged |= Constrain( key, produced, GraphRef.ForPort( id, input.Id ), edge );\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn changed;\r\n\t}\r\n\r\n\tbool Backward( IReadOnlyList\u003CEdge\u003E edges )\r\n\t{\r\n\t\tvar changed = false;\r\n\r\n\t\tfor ( int i = edges.Count - 1; i \u003E= 0; i-- )\r\n\t\t{\r\n\t\t\tvar edge = edges[i];\r\n\t\t\tvar consumed = Read( edge.To );\r\n\r\n\t\t\tif ( consumed.IsVoid ) continue;\r\n\t\t\tif ( !Read( edge.From ).IsVoid ) continue;\r\n\r\n\t\t\tchanged |= Constrain( edge.From, consumed, GraphRef.ForPort( edge.FromNode, edge.FromPort ), edge );\r\n\t\t}\r\n\r\n\t\treturn changed;\r\n\t}\r\n\r\n\tvoid DefaultUnresolved()\r\n\t{\r\n\t\tfor ( int i = 0; i \u003C _slots.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar root = _slots[i];\r\n\r\n\t\t\tif ( root.Type.IsVoid ) root.Type = ShaderType.Float;\r\n\t\t}\r\n\t}\r\n\r\n\t// ---- term access ------------------------------------------------------\r\n\r\n\tShaderType Read( PortRef port )\r\n\t{\r\n\t\tif ( !_terms.TryGetValue( port, out var term ) ) return ShaderType.Void;\r\n\r\n\t\tswitch ( term.Kind )\r\n\t\t{\r\n\t\t\tcase TermKind.Fixed:\r\n\t\t\t\treturn term.Concrete;\r\n\r\n\t\t\tcase TermKind.Variable:\r\n\t\t\t{\r\n\t\t\t\tvar type = _slots[term.Slot].Type;\r\n\r\n\t\t\t\tif ( type.IsVoid ) return ShaderType.Void;\r\n\r\n\t\t\t\treturn term.Force.HasValue \u0026\u0026 type.IsNumeric ? type.WithScalar( term.Force.Value ) : type;\r\n\t\t\t}\r\n\r\n\t\t\tcase TermKind.ScalarOf:\r\n\t\t\t{\r\n\t\t\t\tvar type = _slots[term.Slot].Type;\r\n\t\t\t\treturn type.IsVoid ? ShaderType.Void : type.ScalarType;\r\n\t\t\t}\r\n\r\n\t\t\tdefault:\r\n\t\t\t\treturn ShaderType.Void;\r\n\t\t}\r\n\t}\r\n\r\n\tbool Constrain( PortRef port, ShaderType incoming, GraphRef where, Edge edge )\r\n\t{\r\n\t\tif ( incoming.IsVoid ) return false;\r\n\t\tif ( !_terms.TryGetValue( port, out var term ) ) return false;\r\n\r\n\t\tswitch ( term.Kind )\r\n\t\t{\r\n\t\t\tcase TermKind.Fixed:\r\n\t\t\t\treturn false;\r\n\r\n\t\t\tcase TermKind.Variable:\r\n\t\t\t{\r\n\t\t\t\tvar wanted = term.Force.HasValue \u0026\u0026 incoming.IsNumeric\r\n\t\t\t\t\t? incoming.WithScalar( term.Force.Value )\r\n\t\t\t\t\t: incoming;\r\n\r\n\t\t\t\treturn Bind( term.Slot, wanted, where, edge );\r\n\t\t\t}\r\n\r\n\t\t\tcase TermKind.ScalarOf:\r\n\t\t\t{\r\n\t\t\t\tvar slot = _slots[term.Slot];\r\n\t\t\t\tvar wanted = slot.Type.IsVoid\r\n\t\t\t\t\t? incoming.ScalarType\r\n\t\t\t\t\t: slot.Type.WithScalar( TypeRules.PromoteScalar( slot.Type.Scalar, incoming.Scalar ) );\r\n\r\n\t\t\t\treturn Bind( term.Slot, wanted, where, edge );\r\n\t\t\t}\r\n\r\n\t\t\tdefault:\r\n\t\t\t\treturn false;\r\n\t\t}\r\n\t}\r\n\r\n\tbool Bind( int slotIndex, ShaderType incoming, GraphRef where, Edge edge )\r\n\t{\r\n\t\tif ( incoming.IsVoid ) return false;\r\n\r\n\t\tvar slot = _slots[slotIndex];\r\n\r\n\t\tif ( slot.Type.IsVoid )\r\n\t\t{\r\n\t\t\tslot.Type = incoming;\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tif ( slot.Type == incoming ) return false;\r\n\r\n\t\tif ( !TypeRules.Unify( slot.Type, incoming, out var unified ) )\r\n\t\t{\r\n\t\t\tif ( slot.Failed ) return false;\r\n\r\n\t\t\tslot.Failed = true;\r\n\t\t\t_failed = true;\r\n\r\n\t\t\tvar detail = edge is null ? null : $\u0022Connection {edge}\u0022;\r\n\r\n\t\t\t_diagnostics.Error( DiagnosticCode.UnificationFailure,\r\n\t\t\t\t$\u0022Cannot reconcile {slot.Type.Hlsl} and {incoming.Hlsl} on the same generic port group\u0022,\r\n\t\t\t\twhere, detail );\r\n\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tif ( unified == slot.Type ) return false;\r\n\r\n\t\tslot.Type = unified;\r\n\t\treturn true;\r\n\t}\r\n\r\n\t// ---- edges ------------------------------------------------------------\r\n\r\n\tIEnumerable\u003CEdge\u003E ValidEdges()\r\n\t{\r\n\t\tforeach ( var edge in _graph.Edges ?? Array.Empty\u003CEdge\u003E() )\r\n\t\t{\r\n\t\t\tif ( edge is null || !edge.IsValid ) continue;\r\n\t\t\tif ( !_terms.ContainsKey( edge.From ) || !_terms.ContainsKey( edge.To ) ) continue;\r\n\r\n\t\t\tyield return edge;\r\n\t\t}\r\n\t}\r\n\r\n\tDictionary\u003CEdgeId, ConversionKind\u003E ClassifyEdges( IReadOnlyList\u003CEdge\u003E edges,\r\n\t\tIReadOnlyDictionary\u003CPortRef, ShaderType\u003E types )\r\n\t{\r\n\t\tvar conversions = new Dictionary\u003CEdgeId, ConversionKind\u003E();\r\n\r\n\t\tforeach ( var edge in edges )\r\n\t\t{\r\n\t\t\tif ( !types.TryGetValue( edge.From, out var from ) ) continue;\r\n\t\t\tif ( !types.TryGetValue( edge.To, out var to ) ) continue;\r\n\r\n\t\t\tvar kind = TypeRules.Classify( from, to );\r\n\t\t\tconversions[edge.Id] = kind;\r\n\r\n\t\t\tif ( !ReportConversions ) continue;\r\n\r\n\t\t\tvar where = new GraphRef( edge.ToNode, edge.ToPort, edge.Id );\r\n\r\n\t\t\tswitch ( kind )\r\n\t\t\t{\r\n\t\t\t\tcase ConversionKind.Illegal:\r\n\t\t\t\t\t_failed = true;\r\n\t\t\t\t\t_diagnostics.Error( DiagnosticCode.IllegalConversion,\r\n\t\t\t\t\t\t$\u0022{from.Hlsl} cannot connect to {to.Hlsl}\u0022, where,\r\n\t\t\t\t\t\tTypeRules.Describe( from, to, kind ) );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase ConversionKind.Truncate:\r\n\t\t\t\t\t_diagnostics.Warn( DiagnosticCode.LossyConversion,\r\n\t\t\t\t\t\t$\u0022{from.Hlsl} narrows to {to.Hlsl}\u0022, where,\r\n\t\t\t\t\t\tTypeRules.Describe( from, to, kind ) );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase ConversionKind.Pad:\r\n\t\t\t\t\tvar fill = edge.Fill ?? TypeRules.DefaultFill( from, to, to.Components - 1 );\r\n\t\t\t\t\t_diagnostics.Warn( DiagnosticCode.PaddedConversion,\r\n\t\t\t\t\t\t$\u0022{from.Hlsl} widens to {to.Hlsl}, filling with {fill}\u0022, where,\r\n\t\t\t\t\t\tTypeRules.Describe( from, to, kind ) );\r\n\t\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn conversions;\r\n\t}\r\n\r\n\tstatic IEnumerable\u003CPort\u003E AllPorts( PrismNode node )\r\n\t{\r\n\t\tforeach ( var input in node.Inputs ) yield return input;\r\n\t\tforeach ( var output in node.Outputs ) yield return output;\r\n\t}\r\n\r\n\tenum TermKind\r\n\t{\r\n\t\tFixed,\r\n\t\tVariable,\r\n\t\tScalarOf\r\n\t}\r\n\r\n\treadonly struct Term\r\n\t{\r\n\t\tTerm( TermKind kind, ShaderType concrete, int slot, ScalarKind? force )\r\n\t\t{\r\n\t\t\tKind = kind;\r\n\t\t\tConcrete = concrete;\r\n\t\t\tSlot = slot;\r\n\t\t\tForce = force;\r\n\t\t}\r\n\r\n\t\tpublic TermKind Kind { get; }\r\n\t\tpublic ShaderType Concrete { get; }\r\n\t\tpublic int Slot { get; }\r\n\t\tpublic ScalarKind? Force { get; }\r\n\r\n\t\tpublic static Term Fixed( ShaderType type ) =\u003E new( TermKind.Fixed, type, -1, null );\r\n\t\tpublic static Term Variable( int slot, ScalarKind? force ) =\u003E new( TermKind.Variable, ShaderType.Void, slot, force );\r\n\t\tpublic static Term ScalarOf( int slot ) =\u003E new( TermKind.ScalarOf, ShaderType.Void, slot, null );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// One type variable\u0027s current binding.\r\n\t/// \u003Cpara\u003E\r\n\t/// There is deliberately no union-find here. Prism\u0027s type algebra is per-(node, variable name):\r\n\t/// \u003Cc\u003EVarSlot\u003C/c\u003E mints one slot for each and nothing ever merges two, because a constraint that\r\n\t/// spans nodes is expressed by propagating a concrete type along an edge rather than by equating two\r\n\t/// variables. The class used to carry a \u003Cc\u003EParent\u003C/c\u003E field and a path-compressing \u003Cc\u003EFind\u003C/c\u003E that\r\n\t/// could only ever return its own argument \u2014 it read as Hindley-Milner and behaved as a lookup, and\r\n\t/// a later pass adding a real cross-node constraint would have assumed the merging worked. If one is\r\n\t/// ever needed, add \u003Cc\u003EUnion\u003C/c\u003E and \u003Cc\u003EFind\u003C/c\u003E together.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tsealed class Slot\r\n\t{\r\n\t\tpublic ShaderType Type;\r\n\t\tpublic bool Failed;\r\n\t}\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/Integration/CodeFileEditor.cs","FileName":"CodeFileEditor.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using Editor.Prism.Core;\r\nusing System.IO;\r\n\r\nnamespace Editor.Prism.Integration;\r\n\r\n/// \u003Csummary\u003E\r\n/// Which files Prism\u0027s code window is willing to own.\r\n/// \u003Cpara\u003E\r\n/// Shader sources only. C#, Razor and SCSS belong to a real IDE and Prism never claims them, which is\r\n/// what makes it safe to let Prism act as the editor-wide code editor.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\npublic static class PrismShaderFiles\r\n{\r\n\t/// \u003Csummary\u003EExtensions, without the leading dot, that Prism opens as shader source.\u003C/summary\u003E\r\n\tpublic static readonly IReadOnlyList\u003Cstring\u003E Extensions = new[]\r\n\t{\r\n\t\tPrismConstants.ShaderExtension,   // shader \u2014 the engine\u0027s VFX block format\r\n\t\tPrismConstants.HlslExtension,     // hlsl\r\n\t\t\u0022hlsli\u0022,\r\n\t\t\u0022fxc\u0022,\r\n\t\tPrismConstants.SlangExtension,    // slang\r\n\t\t\u0022slangh\u0022,\r\n\t\t\u0022vfx\u0022\r\n\t};\r\n\r\n\t/// \u003Csummary\u003ETrue when Prism\u0027s code window is the right place for this path.\u003C/summary\u003E\r\n\tpublic static bool IsShaderSource( string path )\r\n\t{\r\n\t\tif ( string.IsNullOrWhiteSpace( path ) ) return false;\r\n\r\n\t\tvar extension = Path.GetExtension( path );\r\n\r\n\t\tif ( string.IsNullOrEmpty( extension ) ) return false;\r\n\r\n\t\textension = extension.TrimStart( \u0027.\u0027 );\r\n\r\n\t\tforeach ( var candidate in Extensions )\r\n\t\t{\r\n\t\t\tif ( extension.Equals( candidate, StringComparison.OrdinalIgnoreCase ) ) return true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EA name filter string suitable for \u003Csee cref=\u0022FileDialog.SetNameFilter\u0022/\u003E.\u003C/summary\u003E\r\n\tpublic static string NameFilter =\u003E\r\n\t\t\u0022Shader Source (\u0022 \u002B string.Join( \u0022 \u0022, Extensions.Select( x =\u003E $\u0022*.{x}\u0022 ) ) \u002B \u0022)\u0022;\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Prism as an editor-wide code editor, offered but never imposed.\r\n/// \u003Cpara\u003E\r\n/// Any type implementing \u003Cc\u003EICodeEditor\u003C/c\u003E is listed in \u003Ci\u003EEditor Settings \u25B8 General \u25B8 Code Editor\u003C/i\u003E\r\n/// automatically, so this shows up as a choice the moment the assembly loads. Selecting it routes\r\n/// shader sources into Prism\u0027s code window; everything else \u2014 C#, Razor, SCSS, solutions, addons \u2014 is\r\n/// handed straight to whichever editor was selected before, so picking Prism never costs you your IDE.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Prism\u0022 ), Icon( \u0022gradient\u0022 )]\r\npublic sealed class PrismCodeEditor : ICodeEditor\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Always available: it ships inside the editor assembly, so unlike an external IDE there is\r\n\t/// nothing to find on disk. Note that selecting it only takes over \u003Cem\u003Eshader\u003C/em\u003E sources \u2014\r\n\t/// everything else is forwarded to \u003Csee cref=\u0022CodeFileEditor.Fallback\u0022/\u003E, which is why this is not\r\n\t/// gated on one existing.\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool IsInstalled() =\u003E true;\r\n\r\n\t/// \u003Csummary\u003EShader sources open in Prism; everything else goes to the fallback editor.\u003C/summary\u003E\r\n\tpublic void OpenFile( string path, int? line = null, int? column = null )\r\n\t{\r\n\t\tif ( string.IsNullOrWhiteSpace( path ) ) return;\r\n\r\n\t\tif ( PrismShaderFiles.IsShaderSource( path ) )\r\n\t\t{\r\n\t\t\tPrismLauncher.OpenCode( path, line ?? 0, column ?? 1 );\r\n\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar fallback = CodeFileEditor.Fallback;\r\n\r\n\t\tif ( fallback is null )\r\n\t\t{\r\n\t\t\t// Prism is a shader editor; a .cs file has to go somewhere else. Saying so beats a\r\n\t\t\t// double-click that appears to do nothing at all.\r\n\t\t\tPrismLog.Warn( $\u0022Prism cannot open \u0027{Path.GetFileName( path )}\u0027 \u2014 it edits shader sources \u0022 \u002B\r\n\t\t\t\t\u0022only, and no other code editor is available to hand it to. Pick one in \u0022 \u002B\r\n\t\t\t\t\u0022Editor Settings \u25B8 Code Editor.\u0022 );\r\n\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tfallback.OpenFile( path, line, column );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EPrism has no notion of a solution. Delegated.\u003C/summary\u003E\r\n\tpublic void OpenSolution() =\u003E CodeFileEditor.Fallback?.OpenSolution();\r\n\r\n\t/// \u003Csummary\u003EPrism has no notion of an addon workspace. Delegated.\u003C/summary\u003E\r\n\tpublic void OpenAddon( Project addon ) =\u003E CodeFileEditor.Fallback?.OpenAddon( addon );\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Routes shader text files into Prism\u0027s code window.\r\n/// \u003Cpara\u003E\r\n/// Three separate paths reach a text file in this editor, and none of them can be intercepted the\r\n/// same way:\r\n/// \u003C/para\u003E\r\n/// \u003Clist type=\u0022number\u0022\u003E\r\n/// \u003Citem\u003E\u003Cdescription\u003E\u003Cc\u003E.shader\u003C/c\u003E is a native asset type whose \u003Cc\u003EOpenInEditor\u003C/c\u003E short-circuits\r\n/// to \u003Cc\u003EEditorEvent.Run( \u0022open.shader\u0022, path )\u003C/c\u003E before \u003Cc\u003EIAssetEditor\u003C/c\u003E is ever consulted, so\r\n/// the only hook is the event \u2014 which is multicast and uncancellable, meaning the tools addon still\r\n/// launches VS Code alongside us if it is installed. Hence the preference.\u003C/description\u003E\u003C/item\u003E\r\n/// \u003Citem\u003E\u003Cdescription\u003E\u003Cc\u003E.hlsl\u003C/c\u003E and \u003Cc\u003E.slang\u003C/c\u003E cannot be registered as asset types at all; they\r\n/// arrive as plain files through the asset browser\u0027s \u003Cc\u003EOnFileSelected\u003C/c\u003E delegate, which we chain\r\n/// rather than replace.\u003C/description\u003E\u003C/item\u003E\r\n/// \u003Citem\u003E\u003Cdescription\u003EAnything routed through \u003Cc\u003ECodeEditor.OpenFile\u003C/c\u003E reaches\r\n/// \u003Csee cref=\u0022PrismCodeEditor\u0022/\u003E, but only if the user opted in.\u003C/description\u003E\u003C/item\u003E\r\n/// \u003C/list\u003E\r\n/// \u003C/summary\u003E\r\npublic static class CodeFileEditor\r\n{\r\n\tstatic ICodeEditor s_fallback;\r\n\tstatic Action\u003Cstring\u003E s_previousFileSelected;\r\n\tstatic AssetBrowser s_routedBrowser;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The editor Prism hands non-shader files to. Resolved lazily, cached until hotload, and never\r\n\t/// resolves to Prism itself.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static ICodeEditor Fallback\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\ts_fallback ??= ResolveFallback();\r\n\r\n\t\t\treturn s_fallback;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EFriendly name of the fallback editor, for the preferences page.\u003C/summary\u003E\r\n\tpublic static string FallbackTitle =\u003E\r\n\t\tPrismLog.Guard( \u0022Describing the fallback code editor\u0022,\r\n\t\t\t() =\u003E Fallback?.Title, null ) ?? \u0022no external editor\u0022;\r\n\r\n\t/// \u003Csummary\u003ETrue when Prism is currently the editor-wide code editor.\u003C/summary\u003E\r\n\tpublic static bool IsCurrentCodeEditor =\u003E\r\n\t\tPrismLog.Guard( \u0022Reading the current code editor\u0022,\r\n\t\t\t() =\u003E CodeEditor.Current is PrismCodeEditor, false );\r\n\r\n\t// ---- the .shader event ------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Double-clicking a \u003Cc\u003E.shader\u003C/c\u003E lands here. Runs early so Prism is up before any external\r\n\t/// editor steals focus.\r\n\t/// \u003C/summary\u003E\r\n\t[Event( \u0022open.shader\u0022, Priority = -100 )]\r\n\tpublic static void OnOpenShader( string absolutePath )\r\n\t{\r\n\t\tif ( !PrismCookies.ClaimShaderFiles ) return;\r\n\t\tif ( string.IsNullOrWhiteSpace( absolutePath ) ) return;\r\n\r\n\t\tPrismLauncher.OpenCode( absolutePath );\r\n\t}\r\n\r\n\t// ---- asset browser routing --------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Chain ourselves onto the asset browser\u0027s plain-file handler, so an unregistered\r\n\t/// \u003Cc\u003E.hlsl\u003C/c\u003E/\u003Cc\u003E.slang\u003C/c\u003E opens in Prism instead of the operating system\u0027s shell handler.\r\n\t/// \u003Cpara\u003E\r\n\t/// Idempotent, and safe to call every frame: it re-installs if the browser is recreated or if\r\n\t/// something else has overwritten the delegate since.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void EnsureAssetBrowserRouting()\r\n\t{\r\n\t\tPrismLog.Guard( \u0022Routing plain files through Prism\u0022, () =\u003E\r\n\t\t{\r\n\t\t\tvar local = MainAssetBrowser.Instance?.Local;\r\n\r\n\t\t\tif ( local is null || !local.IsValid ) return;\r\n\t\t\tif ( ReferenceEquals( s_routedBrowser, local ) \u0026\u0026 IsOurs( local.OnFileSelected ) ) return;\r\n\r\n\t\t\tvar previous = local.OnFileSelected;\r\n\r\n\t\t\t// Never chain to ourselves \u2014 after a hotload the delegate sitting there is our own\r\n\t\t\t// handler from the outgoing assembly, and chaining would grow a new link every reload.\r\n\t\t\ts_previousFileSelected = IsOurs( previous ) ? null : previous;\r\n\t\t\ts_routedBrowser = local;\r\n\t\t\tlocal.OnFileSelected = OnFileSelected;\r\n\t\t} );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EHand the plain-file handler back to whoever had it. Called when the preference goes off.\u003C/summary\u003E\r\n\tpublic static void RemoveAssetBrowserRouting()\r\n\t{\r\n\t\tPrismLog.Guard( \u0022Restoring the asset browser file handler\u0022, () =\u003E\r\n\t\t{\r\n\t\t\tvar local = MainAssetBrowser.Instance?.Local;\r\n\r\n\t\t\tif ( local is null || !local.IsValid ) return;\r\n\t\t\tif ( !IsOurs( local.OnFileSelected ) ) return;\r\n\r\n\t\t\tlocal.OnFileSelected = s_previousFileSelected ?? ( f =\u003E EditorUtility.OpenFile( f ) );\r\n\t\t\ts_routedBrowser = null;\r\n\t\t\ts_previousFileSelected = null;\r\n\t\t} );\r\n\t}\r\n\r\n\tstatic void OnFileSelected( string absolutePath )\r\n\t{\r\n\t\tif ( PrismCookies.ClaimShaderFiles \u0026\u0026 PrismShaderFiles.IsShaderSource( absolutePath ) )\r\n\t\t{\r\n\t\t\tPrismLauncher.OpenCode( absolutePath );\r\n\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( PrismAssetEditor.IsPrismDocument( absolutePath ) )\r\n\t\t{\r\n\t\t\tPrismAssetEditor.Open( absolutePath );\r\n\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( s_previousFileSelected is not null )\r\n\t\t{\r\n\t\t\ts_previousFileSelected( absolutePath );\r\n\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// Same behaviour MainAssetBrowser installs by default.\r\n\t\tPrismLog.Guard( \u0022Opening a file with the shell handler\u0022, () =\u003E EditorUtility.OpenFile( absolutePath ) );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A delegate is one of ours when it was declared on this type \u2014 compared by full name, so it\r\n\t/// still matches an instance left behind by the previous assembly.\r\n\t/// \u003C/summary\u003E\r\n\tstatic bool IsOurs( Action\u003Cstring\u003E handler )\r\n\t{\r\n\t\tvar declaring = handler?.Method?.DeclaringType;\r\n\r\n\t\treturn declaring is not null\r\n\t\t\t\u0026\u0026 string.Equals( declaring.FullName, typeof( CodeFileEditor ).FullName, StringComparison.Ordinal );\r\n\t}\r\n\r\n\t// ---- the editor-wide code editor preference ---------------------------\r\n\r\n\tstatic bool s_reconciling;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Startup reconciliation.\r\n\t/// \u003Cpara\u003E\r\n\t/// If the user picked Prism directly in \u003Ci\u003EEditor Settings \u25B8 Code Editor\u003C/i\u003E, that choice wins and\r\n\t/// the preference is updated to match \u2014 reverting it would be the tool arguing with the person\r\n\t/// using it. Otherwise the preference is applied.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void ApplyCodeEditorPreference()\r\n\t{\r\n\t\tPrismLog.Guard( \u0022Applying the Prism code editor preference\u0022, () =\u003E\r\n\t\t{\r\n\t\t\t// Read the raw cookie rather than CodeEditor.Current: the getter instantiates the selected\r\n\t\t\t// editor and probes the filesystem and registry for it, and no editor session should pay\r\n\t\t\t// that at startup just because Prism happens to be installed.\r\n\t\t\tvar selected = EditorCookie?.GetString( CodeEditorCookie, null );\r\n\r\n\t\t\tif ( string.Equals( selected, nameof( PrismCodeEditor ), StringComparison.Ordinal ) )\r\n\t\t\t{\r\n\t\t\t\tif ( !PrismCookies.RouteCodeFiles ) PrismCookies.RouteCodeFiles = true;\r\n\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\tif ( !PrismCookies.RouteCodeFiles ) return;\r\n\r\n\t\t\tReconcileCodeEditorPreference();\r\n\t\t} );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The engine\u0027s own key for the selected code editor. Hard-coded in \u003Cc\u003ECodeEditor.Current\u003C/c\u003E, and\r\n\t/// stored as the implementing type\u0027s short name.\r\n\t/// \u003C/summary\u003E\r\n\tconst string CodeEditorCookie = \u0022CodeEditor\u0022;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Make \u003Cc\u003ECodeEditor.Current\u003C/c\u003E agree with \u003Csee cref=\u0022PrismCookies.RouteCodeFiles\u0022/\u003E.\r\n\t/// \u003Cpara\u003E\r\n\t/// Turning it on remembers whatever was selected before, so turning it off puts that back rather\r\n\t/// than leaving the editor with no code editor at all. Subscribed to\r\n\t/// \u003Csee cref=\u0022PrismCookies.Changed\u0022/\u003E, so flipping the toggle in the preferences page takes effect\r\n\t/// immediately.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void ReconcileCodeEditorPreference()\r\n\t{\r\n\t\tif ( s_reconciling ) return;\r\n\r\n\t\ts_reconciling = true;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tPrismLog.Guard( \u0022Reconciling the Prism code editor preference\u0022, () =\u003E\r\n\t\t\t{\r\n\t\t\t\tvar current = CodeEditor.Current;\r\n\t\t\t\tvar wanted = PrismCookies.RouteCodeFiles;\r\n\r\n\t\t\t\tif ( wanted )\r\n\t\t\t\t{\r\n\t\t\t\t\t// Compared by full name, not with \u0060is\u0060. CodeEditor.Current is cached in a private\r\n\t\t\t\t\t// static on Sandbox.Tools, which does not hotload, so after the editor assembly is\r\n\t\t\t\t\t// swapped that field still holds a PrismCodeEditor from the OUTGOING assembly \u2014 a\r\n\t\t\t\t\t// different Type identity, so \u0060is\u0060 says false. The old code then recorded\r\n\t\t\t\t\t// \u0022PrismCodeEditor\u0022 as the user\u0027s fallback IDE, permanently, and ResolveFallback\r\n\t\t\t\t\t// excludes PrismCodeEditor by type, so the remembered name could never match again\r\n\t\t\t\t\t// and the user silently got whichever of VisualStudio/VSCode/Rider probed first.\r\n\t\t\t\t\tif ( IsPrism( current ) ) return;\r\n\r\n\t\t\t\t\tif ( current is not null )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tPrismCookies.FallbackCodeEditor = current.GetType().Name;\r\n\t\t\t\t\t\ts_fallback = current;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tCodeEditor.Current = new PrismCodeEditor();\r\n\r\n\t\t\t\t\treturn;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif ( !IsPrism( current ) ) return;\r\n\r\n\t\t\t\tvar restored = Fallback;\r\n\r\n\t\t\t\tif ( restored is not null ) CodeEditor.Current = restored;\r\n\t\t\t} );\r\n\t\t}\r\n\t\tfinally\r\n\t\t{\r\n\t\t\ts_reconciling = false;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Whether an \u003Cc\u003EICodeEditor\u003C/c\u003E is Prism\u0027s, judged by full type name rather than by type identity.\r\n\t/// A hotload leaves an instance of the outgoing assembly\u0027s \u003Cc\u003EPrismCodeEditor\u003C/c\u003E in a static that\r\n\t/// does not hotload, and that instance fails \u003Cc\u003Eis PrismCodeEditor\u003C/c\u003E against the new type.\r\n\t/// \u003C/summary\u003E\r\n\tstatic bool IsPrism( ICodeEditor editor ) =\u003E\r\n\t\teditor is not null \u0026\u0026\r\n\t\tstring.Equals( editor.GetType().FullName, typeof( PrismCodeEditor ).FullName, StringComparison.Ordinal );\r\n\r\n\t/// \u003Csummary\u003EDrop the cached fallback so it is resolved again after a hotload or a settings change.\u003C/summary\u003E\r\n\tpublic static void FlushFallback()\r\n\t{\r\n\t\ts_fallback = null;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EForget the chained delegate \u2014 it points into the outgoing assembly after a hotload.\u003C/summary\u003E\r\n\tpublic static void ForgetRouting()\r\n\t{\r\n\t\ts_previousFileSelected = null;\r\n\t\ts_routedBrowser = null;\r\n\t}\r\n\r\n\tstatic ICodeEditor ResolveFallback()\r\n\t{\r\n\t\treturn PrismLog.Guard( \u0022Resolving the fallback code editor\u0022, () =\u003E\r\n\t\t{\r\n\t\t\tvar types = EditorTypeLibrary.GetTypes\u003CICodeEditor\u003E()\r\n\t\t\t\t.Where( x =\u003E !x.IsInterface \u0026\u0026 !x.IsAbstract )\r\n\t\t\t\t.Where( x =\u003E x.TargetType != typeof( PrismCodeEditor ) )\r\n\t\t\t\t.ToList();\r\n\r\n\t\t\tICodeEditor Instantiate( TypeDescription type )\r\n\t\t\t{\r\n\t\t\t\tvar editor = type?.Create\u003CICodeEditor\u003E();\r\n\r\n\t\t\t\treturn editor is not null \u0026\u0026 editor.IsInstalled() ? editor : null;\r\n\t\t\t}\r\n\r\n\t\t\tvar remembered = PrismCookies.FallbackCodeEditor;\r\n\r\n\t\t\tif ( !string.IsNullOrWhiteSpace( remembered ) )\r\n\t\t\t{\r\n\t\t\t\tvar match = Instantiate( types.FirstOrDefault( x =\u003E x.Name == remembered ) );\r\n\r\n\t\t\t\tif ( match is not null ) return match;\r\n\t\t\t}\r\n\r\n\t\t\tforeach ( var preferred in new[] { \u0022VisualStudio\u0022, \u0022VisualStudioCode\u0022, \u0022Rider\u0022 } )\r\n\t\t\t{\r\n\t\t\t\tvar match = Instantiate( types.FirstOrDefault( x =\u003E x.Name == preferred ) );\r\n\r\n\t\t\t\tif ( match is not null ) return match;\r\n\t\t\t}\r\n\r\n\t\t\tforeach ( var type in types )\r\n\t\t\t{\r\n\t\t\t\tvar match = Instantiate( type );\r\n\r\n\t\t\t\tif ( match is not null ) return match;\r\n\t\t\t}\r\n\r\n\t\t\treturn null;\r\n\t\t}, null );\r\n\t}\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/Model/GraphQueries.cs","FileName":"GraphQueries.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using Editor.Prism.Core;\r\n\r\nnamespace Editor.Prism.Model;\r\n\r\n/// \u003Csummary\u003E\r\n/// Read-only analysis of a document: reachability, topological order, real cycle detection,\r\n/// dependency subtrees and orphan detection.\r\n/// \u003Cpara\u003E\r\n/// Every traversal here is iterative rather than recursive, so a pathological graph produces a\r\n/// diagnostic instead of a stack overflow, and \u003Cb\u003Eevery traversal includes reroute nodes\u003C/b\u003E. The\r\n/// built-in editor exempts reroutes from its cycle check, which is why a reroute loop can hang it;\r\n/// treating every node identically is both simpler and correct.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\npublic static class GraphQueries\r\n{\r\n\t/// \u003Csummary\u003EThe node a port reference points at. Null when it does not resolve.\u003C/summary\u003E\r\n\tpublic static PrismNode NodeOf( IPrismGraph graph, PortRef reference ) =\u003E graph?.FindNode( reference.Node );\r\n\r\n\t/// \u003Csummary\u003EResolve a port reference to a live port.\u003C/summary\u003E\r\n\tpublic static bool TryGetPort( IPrismGraph graph, PortRef reference, out Port port )\r\n\t{\r\n\t\tport = graph?.FindNode( reference.Node )?.FindPort( reference.Port );\r\n\t\treturn port is not null;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EEvery edge terminating on a node.\u003C/summary\u003E\r\n\tpublic static IEnumerable\u003CEdge\u003E IncomingEdges( IPrismGraph graph, NodeId node )\r\n\t{\r\n\t\tif ( graph?.Edges is null ) yield break;\r\n\r\n\t\tforeach ( var edge in graph.Edges )\r\n\t\t{\r\n\t\t\tif ( edge is not null \u0026\u0026 edge.ToNode == node ) yield return edge;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EEvery edge leaving a node.\u003C/summary\u003E\r\n\tpublic static IEnumerable\u003CEdge\u003E OutgoingEdges( IPrismGraph graph, NodeId node )\r\n\t{\r\n\t\tif ( graph?.Edges is null ) yield break;\r\n\r\n\t\tforeach ( var edge in graph.Edges )\r\n\t\t{\r\n\t\t\tif ( edge is not null \u0026\u0026 edge.FromNode == node ) yield return edge;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EEvery node that directly feeds this one.\u003C/summary\u003E\r\n\tpublic static IEnumerable\u003CNodeId\u003E Predecessors( IPrismGraph graph, NodeId node ) =\u003E\r\n\t\tIncomingEdges( graph, node ).Select( x =\u003E x.FromNode ).Distinct();\r\n\r\n\t/// \u003Csummary\u003EEvery node this one directly feeds.\u003C/summary\u003E\r\n\tpublic static IEnumerable\u003CNodeId\u003E Successors( IPrismGraph graph, NodeId node ) =\u003E\r\n\t\tOutgoingEdges( graph, node ).Select( x =\u003E x.ToNode ).Distinct();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The nodes a compile starts from: registered output nodes when there are any, otherwise every\r\n\t/// node with no outgoing edge. The fallback is what makes a half-built graph still previewable.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003CNodeId\u003E OutputNodes( IPrismGraph graph )\r\n\t{\r\n\t\tif ( graph?.Nodes is null ) return Array.Empty\u003CNodeId\u003E();\r\n\r\n\t\tvar outputs = new List\u003CNodeId\u003E();\r\n\r\n\t\tforeach ( var node in graph.Nodes )\r\n\t\t{\r\n\t\t\tif ( node is null ) continue;\r\n\t\t\tif ( !IsOutputNode( node ) ) continue;\r\n\r\n\t\t\toutputs.Add( node.Id );\r\n\t\t}\r\n\r\n\t\tif ( outputs.Count \u003E 0 ) return outputs;\r\n\r\n\t\treturn TerminalNodes( graph );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETrue when a node looks like a graph terminal: an output-category node with no outputs.\u003C/summary\u003E\r\n\tpublic static bool IsOutputNode( PrismNode node )\r\n\t{\r\n\t\tif ( node is null ) return false;\r\n\t\tif ( node.Outputs.Count \u003E 0 ) return false;\r\n\r\n\t\tvar id = node.Descriptor?.Id;\r\n\r\n\t\tif ( !string.IsNullOrEmpty( id ) \u0026\u0026 id.StartsWith( \u0022prism.output.\u0022, StringComparison.OrdinalIgnoreCase ) )\r\n\t\t{\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tvar category = node.Descriptor?.Category;\r\n\r\n\t\treturn !string.IsNullOrEmpty( category ) \u0026\u0026\r\n\t\t\tcategory.StartsWith( \u0022Output\u0022, StringComparison.OrdinalIgnoreCase );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EEvery node with no outgoing edge.\u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003CNodeId\u003E TerminalNodes( IPrismGraph graph )\r\n\t{\r\n\t\tif ( graph?.Nodes is null ) return Array.Empty\u003CNodeId\u003E();\r\n\r\n\t\tvar hasOutgoing = new HashSet\u003CNodeId\u003E();\r\n\r\n\t\tforeach ( var edge in graph.Edges ?? Array.Empty\u003CEdge\u003E() )\r\n\t\t{\r\n\t\t\tif ( edge is not null ) hasOutgoing.Add( edge.FromNode );\r\n\t\t}\r\n\r\n\t\tvar result = new List\u003CNodeId\u003E();\r\n\r\n\t\tforeach ( var node in graph.Nodes )\r\n\t\t{\r\n\t\t\tif ( node is null || hasOutgoing.Contains( node.Id ) ) continue;\r\n\r\n\t\t\tresult.Add( node.Id );\r\n\t\t}\r\n\r\n\t\treturn result;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Every node reachable by walking \u003Cem\u003Ebackwards\u003C/em\u003E from the given roots \u2014 that is, everything\r\n\t/// that contributes to the roots\u0027 values. Disabled nodes stop the walk, because a disabled node\r\n\t/// falls back to inline values and its inputs are not evaluated.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static IReadOnlyCollection\u003CNodeId\u003E Reachable( IPrismGraph graph, IEnumerable\u003CNodeId\u003E roots,\r\n\t\tbool stopAtDisabled = true )\r\n\t{\r\n\t\tvar visited = new HashSet\u003CNodeId\u003E();\r\n\r\n\t\tif ( graph is null || roots is null ) return visited;\r\n\r\n\t\tvar stack = new Stack\u003CNodeId\u003E();\r\n\r\n\t\tforeach ( var root in roots )\r\n\t\t{\r\n\t\t\tif ( root.IsValid \u0026\u0026 visited.Add( root ) ) stack.Push( root );\r\n\t\t}\r\n\r\n\t\tvar incoming = BuildIncomingMap( graph );\r\n\r\n\t\twhile ( stack.Count \u003E 0 )\r\n\t\t{\r\n\t\t\tvar current = stack.Pop();\r\n\r\n\t\t\tif ( stopAtDisabled \u0026\u0026 IsDisabled( graph, current ) ) continue;\r\n\t\t\tif ( !incoming.TryGetValue( current, out var sources ) ) continue;\r\n\r\n\t\t\tforeach ( var source in sources )\r\n\t\t\t{\r\n\t\t\t\tif ( visited.Add( source ) ) stack.Push( source );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn visited;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EEvery node reachable backwards from the graph\u0027s output nodes.\u003C/summary\u003E\r\n\tpublic static IReadOnlyCollection\u003CNodeId\u003E ReachableFromOutputs( IPrismGraph graph ) =\u003E\r\n\t\tReachable( graph, OutputNodes( graph ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Nodes that contribute to nothing: not reachable backwards from any output and not an output\r\n\t/// themselves. Purely informational \u2014 an orphan is a perfectly legal work in progress.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003CNodeId\u003E Orphans( IPrismGraph graph, IEnumerable\u003CNodeId\u003E roots = null )\r\n\t{\r\n\t\tif ( graph?.Nodes is null ) return Array.Empty\u003CNodeId\u003E();\r\n\r\n\t\tvar reachable = Reachable( graph, roots ?? OutputNodes( graph ), false );\r\n\t\tvar result = new List\u003CNodeId\u003E();\r\n\r\n\t\tforeach ( var node in graph.Nodes )\r\n\t\t{\r\n\t\t\tif ( node is null || reachable.Contains( node.Id ) ) continue;\r\n\r\n\t\t\tresult.Add( node.Id );\r\n\t\t}\r\n\r\n\t\treturn result;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Every node the given node depends on, including itself, in dependency-first order. This is the\r\n\t/// subtree a \u0022compile just this node\u0022 preview needs.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003CNodeId\u003E DependencySubtree( IPrismGraph graph, NodeId node )\r\n\t{\r\n\t\tvar subtree = Reachable( graph, new[] { node }, false );\r\n\r\n\t\treturn TopologicalOrder( graph, new[] { node } ).Where( subtree.Contains ).ToArray();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EEvery node that depends, directly or transitively, on the given node.\u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003CNodeId\u003E Dependents( IPrismGraph graph, NodeId node )\r\n\t{\r\n\t\tvar visited = new HashSet\u003CNodeId\u003E();\r\n\r\n\t\tif ( graph is null || !node.IsValid ) return Array.Empty\u003CNodeId\u003E();\r\n\r\n\t\tvar outgoing = BuildOutgoingMap( graph );\r\n\t\tvar stack = new Stack\u003CNodeId\u003E();\r\n\t\tstack.Push( node );\r\n\r\n\t\twhile ( stack.Count \u003E 0 )\r\n\t\t{\r\n\t\t\tvar current = stack.Pop();\r\n\r\n\t\t\tif ( !outgoing.TryGetValue( current, out var targets ) ) continue;\r\n\r\n\t\t\tforeach ( var target in targets )\r\n\t\t\t{\r\n\t\t\t\tif ( visited.Add( target ) ) stack.Push( target );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn visited.ToArray();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Dependency-first order over the nodes reachable from \u003Cparamref name=\u0022roots\u0022/\u003E, or over the whole\r\n\t/// document when roots are omitted. Nodes involved in a cycle are appended at the end rather than\r\n\t/// dropped, so a cyclic graph still produces a usable ordering for the UI.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003CNodeId\u003E TopologicalOrder( IPrismGraph graph, IEnumerable\u003CNodeId\u003E roots = null )\r\n\t{\r\n\t\tif ( graph?.Nodes is null ) return Array.Empty\u003CNodeId\u003E();\r\n\r\n\t\tvar scope = roots is null\r\n\t\t\t? new HashSet\u003CNodeId\u003E( graph.Nodes.Where( x =\u003E x is not null ).Select( x =\u003E x.Id ) )\r\n\t\t\t: new HashSet\u003CNodeId\u003E( Reachable( graph, roots, false ) );\r\n\r\n\t\tif ( scope.Count == 0 ) return Array.Empty\u003CNodeId\u003E();\r\n\r\n\t\tvar incoming = BuildIncomingMap( graph );\r\n\t\tvar order = new List\u003CNodeId\u003E( scope.Count );\r\n\t\tvar state = new Dictionary\u003CNodeId, byte\u003E( scope.Count );\r\n\r\n\t\t// Iterative post-order DFS. 0 = unvisited, 1 = on the stack (grey), 2 = emitted (black).\r\n\t\tvar work = new Stack\u003C(NodeId Node, int Index)\u003E();\r\n\r\n\t\tforeach ( var root in Ordered( graph, scope ) )\r\n\t\t{\r\n\t\t\tif ( state.TryGetValue( root, out var seen ) \u0026\u0026 seen == 2 ) continue;\r\n\r\n\t\t\twork.Push( (root, 0) );\r\n\t\t\tstate[root] = 1;\r\n\r\n\t\t\twhile ( work.Count \u003E 0 )\r\n\t\t\t{\r\n\t\t\t\tvar (node, index) = work.Pop();\r\n\t\t\t\tvar sources = incoming.TryGetValue( node, out var list ) ? list : s_noIds;\r\n\r\n\t\t\t\tif ( index \u003C sources.Count )\r\n\t\t\t\t{\r\n\t\t\t\t\twork.Push( (node, index \u002B 1) );\r\n\r\n\t\t\t\t\tvar source = sources[index];\r\n\r\n\t\t\t\t\tif ( !scope.Contains( source ) ) continue;\r\n\r\n\t\t\t\t\tstate.TryGetValue( source, out var sourceState );\r\n\r\n\t\t\t\t\tif ( sourceState == 0 )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tstate[source] = 1;\r\n\t\t\t\t\t\twork.Push( (source, 0) );\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tstate[node] = 2;\r\n\t\t\t\torder.Add( node );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// Anything still grey belongs to a cycle: emit it so callers see every node exactly once.\r\n\t\tforeach ( var node in Ordered( graph, scope ) )\r\n\t\t{\r\n\t\t\tif ( state.TryGetValue( node, out var seen ) \u0026\u0026 seen == 2 ) continue;\r\n\r\n\t\t\torder.Add( node );\r\n\t\t\tstate[node] = 2;\r\n\t\t}\r\n\r\n\t\treturn order;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Find one cycle, reporting the full path in traversal order. Reroutes participate exactly like\r\n\t/// any other node. Returns false when the document is acyclic.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static bool TryFindCycle( IPrismGraph graph, out IReadOnlyList\u003CNodeId\u003E cycle )\r\n\t{\r\n\t\tvar cycles = FindCycles( graph, 1 );\r\n\r\n\t\tcycle = cycles.Count \u003E 0 ? cycles[0] : Array.Empty\u003CNodeId\u003E();\r\n\r\n\t\treturn cycles.Count \u003E 0;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Find up to \u003Cparamref name=\u0022limit\u0022/\u003E distinct cycles, each reported as the full node path with\r\n\t/// the entry node repeated at the end so the loop reads naturally in a diagnostic.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003CIReadOnlyList\u003CNodeId\u003E\u003E FindCycles( IPrismGraph graph, int limit = 8 )\r\n\t{\r\n\t\tvar found = new List\u003CIReadOnlyList\u003CNodeId\u003E\u003E();\r\n\r\n\t\tif ( graph?.Nodes is null ) return found;\r\n\r\n\t\tvar outgoing = BuildOutgoingMap( graph );\r\n\t\tvar state = new Dictionary\u003CNodeId, byte\u003E();\r\n\t\tvar path = new List\u003CNodeId\u003E();\r\n\t\tvar onPath = new HashSet\u003CNodeId\u003E();\r\n\t\tvar seenCycles = new HashSet\u003Cstring\u003E();\r\n\r\n\t\tforeach ( var start in graph.Nodes.Where( x =\u003E x is not null ).Select( x =\u003E x.Id ) )\r\n\t\t{\r\n\t\t\tif ( state.TryGetValue( start, out var seen ) \u0026\u0026 seen == 2 ) continue;\r\n\t\t\tif ( found.Count \u003E= limit ) break;\r\n\r\n\t\t\tvar work = new Stack\u003C(NodeId Node, int Index)\u003E();\r\n\t\t\twork.Push( (start, 0) );\r\n\r\n\t\t\twhile ( work.Count \u003E 0 )\r\n\t\t\t{\r\n\t\t\t\tvar (node, index) = work.Pop();\r\n\r\n\t\t\t\tif ( index == 0 )\r\n\t\t\t\t{\r\n\t\t\t\t\tstate[node] = 1;\r\n\t\t\t\t\tpath.Add( node );\r\n\t\t\t\t\tonPath.Add( node );\r\n\t\t\t\t}\r\n\r\n\t\t\t\tvar targets = outgoing.TryGetValue( node, out var list ) ? list : s_noIds;\r\n\r\n\t\t\t\tif ( index \u003C targets.Count )\r\n\t\t\t\t{\r\n\t\t\t\t\twork.Push( (node, index \u002B 1) );\r\n\r\n\t\t\t\t\tvar next = targets[index];\r\n\r\n\t\t\t\t\tif ( onPath.Contains( next ) )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar at = path.LastIndexOf( next );\r\n\r\n\t\t\t\t\t\tif ( at \u003E= 0 \u0026\u0026 found.Count \u003C limit )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tvar loop = new List\u003CNodeId\u003E( path.Count - at \u002B 1 );\r\n\r\n\t\t\t\t\t\t\tfor ( int i = at; i \u003C path.Count; i\u002B\u002B )\r\n\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\tloop.Add( path[i] );\r\n\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\tloop.Add( next );\r\n\r\n\t\t\t\t\t\t\tvar key = string.Join( \u0022\u003E\u0022, loop.Select( x =\u003E x.Value ).OrderBy( x =\u003E x, StringComparer.Ordinal ) );\r\n\r\n\t\t\t\t\t\t\tif ( seenCycles.Add( key ) ) found.Add( loop );\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tcontinue;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tstate.TryGetValue( next, out var nextState );\r\n\r\n\t\t\t\t\tif ( nextState == 0 ) work.Push( (next, 0) );\r\n\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tstate[node] = 2;\r\n\t\t\t\tonPath.Remove( node );\r\n\r\n\t\t\t\tif ( path.Count \u003E 0 \u0026\u0026 path[^1] == node ) path.RemoveAt( path.Count - 1 );\r\n\t\t\t}\r\n\r\n\t\t\tpath.Clear();\r\n\t\t\tonPath.Clear();\r\n\t\t}\r\n\r\n\t\treturn found;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Would adding this connection close a loop? Answered without mutating the document, so the plug\r\n\t/// setter can refuse a drop before anything changes.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static bool WouldCreateCycle( IPrismGraph graph, PortRef from, PortRef to )\r\n\t{\r\n\t\tif ( graph is null ) return false;\r\n\t\tif ( !from.IsValid || !to.IsValid ) return false;\r\n\t\tif ( from.Node == to.Node ) return true;\r\n\r\n\t\t// The new edge runs from.Node -\u003E to.Node. It closes a loop when from.Node is already\r\n\t\t// reachable downstream of to.Node.\r\n\t\tvar outgoing = BuildOutgoingMap( graph );\r\n\t\tvar visited = new HashSet\u003CNodeId\u003E { to.Node };\r\n\t\tvar stack = new Stack\u003CNodeId\u003E();\r\n\t\tstack.Push( to.Node );\r\n\r\n\t\twhile ( stack.Count \u003E 0 )\r\n\t\t{\r\n\t\t\tvar current = stack.Pop();\r\n\r\n\t\t\tif ( current == from.Node ) return true;\r\n\t\t\tif ( !outgoing.TryGetValue( current, out var targets ) ) continue;\r\n\r\n\t\t\tforeach ( var target in targets )\r\n\t\t\t{\r\n\t\t\t\tif ( visited.Add( target ) ) stack.Push( target );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// How many nodes of a cycle path are named before the description gives up and counts the rest.\r\n\t/// A cycle through a thousand nodes is not more informative than a cycle through twenty, and the\r\n\t/// text ends up in a diagnostic detail body, a tooltip and a log line.\r\n\t/// \u003C/summary\u003E\r\n\tpublic const int MaxDescribedCycleNodes = 24;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Render a cycle path as \u003Cc\u003ETitle #id \u2192 Title #id \u2192 \u2026\u003C/c\u003E for a diagnostic detail body. Long cycles\r\n\t/// are elided in the middle: the two ends are what identifies the loop, and the length is stated.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string DescribeCycle( IPrismGraph graph, IReadOnlyList\u003CNodeId\u003E cycle )\r\n\t{\r\n\t\tif ( cycle is null || cycle.Count == 0 ) return string.Empty;\r\n\r\n\t\tstring Name( NodeId id )\r\n\t\t{\r\n\t\t\tvar node = graph?.FindNode( id );\r\n\r\n\t\t\tvar title = node switch\r\n\t\t\t{\r\n\t\t\t\tUnknownNode unknown =\u003E unknown.DisplayTitle,\r\n\t\t\t\tnull =\u003E \u0022\u003Cmissing\u003E\u0022,\r\n\t\t\t\t_ =\u003E node.Descriptor?.Title ?? node.GetType().Name\r\n\t\t\t};\r\n\r\n\t\t\treturn $\u0022{title} #{id}\u0022;\r\n\t\t}\r\n\r\n\t\tif ( cycle.Count \u003C= MaxDescribedCycleNodes )\r\n\t\t{\r\n\t\t\treturn string.Join( \u0022 \u2192 \u0022, cycle.Select( Name ) );\r\n\t\t}\r\n\r\n\t\tvar head = MaxDescribedCycleNodes / 2;\r\n\t\tvar tail = MaxDescribedCycleNodes - head;\r\n\r\n\t\tvar parts = cycle.Take( head ).Select( Name ).ToList();\r\n\r\n\t\tparts.Add( $\u0022\u2026 {cycle.Count - MaxDescribedCycleNodes} more \u2026\u0022 );\r\n\t\tparts.AddRange( cycle.Skip( cycle.Count - tail ).Select( Name ) );\r\n\r\n\t\treturn string.Join( \u0022 \u2192 \u0022, parts );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The static checks that do not need the type solver: cycles, dangling edges, missing required\r\n\t/// inputs, unresolved parameter references and a missing output node. Never throws; a node whose\r\n\t/// \u003Cc\u003EOnValidate\u003C/c\u003E misbehaves is isolated and reported.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003CDiagnostic\u003E Validate( IPrismGraph graph, DiagnosticSink sink = null )\r\n\t{\r\n\t\tvar target = sink ?? new DiagnosticSink();\r\n\r\n\t\tif ( graph is null ) return target.All;\r\n\r\n\t\tforeach ( var cycle in FindCycles( graph ) )\r\n\t\t{\r\n\t\t\ttarget.Error( DiagnosticCode.Cycle, \u0022This graph contains a cycle\u0022,\r\n\t\t\t\tGraphRef.ForNode( cycle.Count \u003E 0 ? cycle[0] : NodeId.None ), DescribeCycle( graph, cycle ) );\r\n\t\t}\r\n\r\n\t\tforeach ( var edge in graph.Edges ?? Array.Empty\u003CEdge\u003E() )\r\n\t\t{\r\n\t\t\tif ( edge is null ) continue;\r\n\r\n\t\t\tif ( graph.FindNode( edge.FromNode ) is null || graph.FindNode( edge.ToNode ) is null )\r\n\t\t\t{\r\n\t\t\t\ttarget.Error( DiagnosticCode.DanglingEdge, \u0022Connection references a node that does not exist\u0022,\r\n\t\t\t\t\tGraphRef.ForEdge( edge.Id ), edge.ToString() );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tforeach ( var node in graph.Nodes ?? Array.Empty\u003CPrismNode\u003E() )\r\n\t\t{\r\n\t\t\tif ( node is null ) continue;\r\n\r\n\t\t\tforeach ( var input in node.Inputs )\r\n\t\t\t{\r\n\t\t\t\tif ( !input.Required ) continue;\r\n\t\t\t\tif ( input.IsConnected ) continue;\r\n\t\t\t\tif ( input.InlineValue is not null ) continue;\r\n\r\n\t\t\t\ttarget.Error( DiagnosticCode.MissingInput,\r\n\t\t\t\t\t$\u0022\u0027{input.DisplayName}\u0027 is required and has nothing connected\u0022,\r\n\t\t\t\t\tGraphRef.ForPort( node.Id, input.Id ) );\r\n\t\t\t}\r\n\r\n\t\t\tvar scoped = target.Scoped( GraphRef.ForNode( node.Id ) );\r\n\r\n\t\t\tPrismLog.Try( $\u0022Validate node {node.Id}\u0022,\r\n\t\t\t\t() =\u003E node.OnValidate( new ValidationContext( node, graph, scoped ) ),\r\n\t\t\t\ttarget, DiagnosticCode.NodeEmitFailed, GraphRef.ForNode( node.Id ) );\r\n\t\t}\r\n\r\n\t\tif ( OutputNodes( graph ).Count == 0 )\r\n\t\t{\r\n\t\t\ttarget.Error( DiagnosticCode.NoOutput, \u0022This graph has no output node\u0022 );\r\n\t\t}\r\n\r\n\t\treturn target.All;\r\n\t}\r\n\r\n\tstatic bool IsDisabled( IPrismGraph graph, NodeId id ) =\u003E\r\n\t\tgraph?.FindNode( id ) is { } node \u0026\u0026 ( node.Flags \u0026 NodeFlags.Disabled ) != 0;\r\n\r\n\tstatic IEnumerable\u003CNodeId\u003E Ordered( IPrismGraph graph, HashSet\u003CNodeId\u003E scope )\r\n\t{\r\n\t\t// Iterate in document order so the result is stable between runs, which is what makes\r\n\t\t// regenerated shader text byte-identical for an unchanged graph.\r\n\t\tforeach ( var node in graph.Nodes ?? Array.Empty\u003CPrismNode\u003E() )\r\n\t\t{\r\n\t\t\tif ( node is null || !scope.Contains( node.Id ) ) continue;\r\n\r\n\t\t\tyield return node.Id;\r\n\t\t}\r\n\t}\r\n\r\n\tstatic Dictionary\u003CNodeId, List\u003CNodeId\u003E\u003E BuildIncomingMap( IPrismGraph graph )\r\n\t{\r\n\t\tvar map = new Dictionary\u003CNodeId, List\u003CNodeId\u003E\u003E();\r\n\r\n\t\tforeach ( var edge in graph.Edges ?? Array.Empty\u003CEdge\u003E() )\r\n\t\t{\r\n\t\t\tif ( edge is null ) continue;\r\n\r\n\t\t\tif ( !map.TryGetValue( edge.ToNode, out var list ) )\r\n\t\t\t{\r\n\t\t\t\tlist = new List\u003CNodeId\u003E();\r\n\t\t\t\tmap[edge.ToNode] = list;\r\n\t\t\t}\r\n\r\n\t\t\tif ( !list.Contains( edge.FromNode ) ) list.Add( edge.FromNode );\r\n\t\t}\r\n\r\n\t\treturn map;\r\n\t}\r\n\r\n\tstatic Dictionary\u003CNodeId, List\u003CNodeId\u003E\u003E BuildOutgoingMap( IPrismGraph graph )\r\n\t{\r\n\t\tvar map = new Dictionary\u003CNodeId, List\u003CNodeId\u003E\u003E();\r\n\r\n\t\tforeach ( var edge in graph.Edges ?? Array.Empty\u003CEdge\u003E() )\r\n\t\t{\r\n\t\t\tif ( edge is null ) continue;\r\n\r\n\t\t\tif ( !map.TryGetValue( edge.FromNode, out var list ) )\r\n\t\t\t{\r\n\t\t\t\tlist = new List\u003CNodeId\u003E();\r\n\t\t\t\tmap[edge.FromNode] = list;\r\n\t\t\t}\r\n\r\n\t\t\tif ( !list.Contains( edge.ToNode ) ) list.Add( edge.ToNode );\r\n\t\t}\r\n\r\n\t\treturn map;\r\n\t}\r\n\r\n\tstatic readonly List\u003CNodeId\u003E s_noIds = new();\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/PrismHotload.cs","FileName":"PrismHotload.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using Editor.Prism.Compiler;\r\nusing Editor.Prism.Core;\r\nusing Editor.Prism.Model;\r\nusing Editor.Prism.Nodes;\r\nusing Editor.Prism.Serialization;\r\nusing Editor.Prism.Text;\r\nusing Editor.Prism.Toolchain;\r\n\r\nnamespace Editor.Prism;\r\n\r\n/// \u003Csummary\u003E\r\n/// The one place every static cache in Prism is dropped when the editor hotloads this assembly.\r\n/// \u003Cpara\u003E\r\n/// Almost every package caches something keyed by \u003Csee cref=\u0022Type\u0022/\u003E, \u003Cc\u003EPropertyInfo\u003C/c\u003E or a live\r\n/// instance \u2014 the node registry, the port and property reflection tables, the backend list, the\r\n/// subgraph document cache, the legacy import table. Every one of those holds the outgoing assembly\r\n/// alive and hands out stale metadata after a reload, so they are all flushed together here rather\r\n/// than each package hoping someone else remembered.\r\n/// \u003C/para\u003E\r\n/// \u003Cpara\u003E\r\n/// Order matters: the registry is flushed last because flushing it re-registers the descriptor\r\n/// provider, and nothing should be able to rebuild the catalogue from half-cleared tables.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\npublic static class PrismHotload\r\n{\r\n\t/// \u003Csummary\u003ERaised after every cache has been dropped, so a window can rebuild whatever it holds.\u003C/summary\u003E\r\n\tpublic static event Action Flushed;\r\n\r\n\t/// \u003Csummary\u003EDrop every static cache in Prism. Safe to call at any time; never throws.\u003C/summary\u003E\r\n\tpublic static void FlushAll()\r\n\t{\r\n\t\t// First, because a compile in flight is holding a graph, a backend and a pile of callbacks that\r\n\t\t// are all about to be replaced underneath it. Nothing below is safe while one is running.\r\n\t\tPrismLog.Guard( \u0022Cancel compiles in flight\u0022, () =\u003E ShaderCompileService.CancelAll() );\r\n\r\n\t\tPrismLog.Guard( \u0022Flush subgraph documents\u0022, SubgraphLibrary.Flush );\r\n\t\tPrismLog.Guard( \u0022Flush compiler backends\u0022, GraphCompiler.FlushBackends );\r\n\t\tPrismLog.Guard( \u0022Flush the legacy import table\u0022, LegacyShaderGraphImporter.Reset );\r\n\r\n\t\t// Migration steps are delegates, so an outgoing assembly\u0027s upgraders would otherwise stay\r\n\t\t// registered and run against documents loaded by the new one. Anything that registers steps must\r\n\t\t// do so again from \u003Csee cref=\u0022Flushed\u0022/\u003E, which is raised at the end of this method.\r\n\t\t//\r\n\t\t// Both registries, not just the per-node one: a document-level upgrader is the same delegate held\r\n\t\t// the same way, and it runs on the path that turns an older file into the current schema \u2014 the one\r\n\t\t// place a stale function body would silently rewrite somebody\u0027s document.\r\n\t\tPrismLog.Guard( \u0022Flush node migrations\u0022, NodeMigrations.Reset );\r\n\t\tPrismLog.Guard( \u0022Flush schema migrations\u0022, SchemaMigrations.Reset );\r\n\r\n\t\tPrismLog.Guard( \u0022Flush port reflection\u0022, PortBuilder.FlushCache );\r\n\t\tPrismLog.Guard( \u0022Flush node properties\u0022, NodeProperties.Flush );\r\n\r\n\t\t// The lexers, the language databases, the include resolver and the header symbol tables. A\r\n\t\t// hotload that skipped these would leave every open document lexing against word tables and\r\n\t\t// delegate-backed lazies belonging to the assembly that just went away.\r\n\t\tPrismLog.Guard( \u0022Flush the text editor caches\u0022, TextCaches.Flush );\r\n\r\n\t\tPrismLog.Guard( \u0022Flush the node registry\u0022, NodeRegistry.Flush );\r\n\r\n\t\tPrismLog.Guard( \u0022Raising PrismHotload.Flushed\u0022, () =\u003E Flushed?.Invoke() );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EDrop every cache when the editor reloads this assembly.\u003C/summary\u003E\r\n\t[EditorEvent.Hotload]\r\n\tstatic void OnHotload() =\u003E FlushAll();\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/Serialization/ValueCodec.cs","FileName":"ValueCodec.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using Editor.Prism.Compiler.Ir;\r\nusing Editor.Prism.Core;\r\nusing System.Globalization;\r\n\r\nnamespace Editor.Prism.Serialization;\r\n\r\n/// \u003Csummary\u003E\r\n/// A texture reference as it appears in a document: an asset path plus the import intent that decides\r\n/// how the sampler is generated. Stored as an object rather than a bare string so colour space and\r\n/// processor survive a round-trip.\r\n/// \u003C/summary\u003E\r\npublic sealed record TextureValue\r\n{\r\n\t/// \u003Csummary\u003ERelative asset path, e.g. \u003Cc\u003Ematerials/dev/white_color.tga\u003C/c\u003E.\u003C/summary\u003E\r\n\tpublic string Path { get; init; }\r\n\r\n\t/// \u003Csummary\u003EHow the texture is read: \u003Cc\u003ESrgb\u003C/c\u003E or \u003Cc\u003ELinear\u003C/c\u003E.\u003C/summary\u003E\r\n\tpublic string ColorSpace { get; init; } = \u0022Srgb\u0022;\r\n\r\n\t/// \u003Csummary\u003EImport processor name, e.g. \u003Cc\u003ENone\u003C/c\u003E, \u003Cc\u003ENormalizeNormals\u003C/c\u003E.\u003C/summary\u003E\r\n\tpublic string Processor { get; init; } = \u0022None\u0022;\r\n\r\n\t/// \u003Csummary\u003ETrue when no asset is referenced.\u003C/summary\u003E\r\n\tpublic bool IsEmpty =\u003E string.IsNullOrWhiteSpace( Path );\r\n\r\n\t/// \u003Csummary\u003ETrue when the texture should be sampled through an sRGB view.\u003C/summary\u003E\r\n\tpublic bool IsSrgb =\u003E string.Equals( ColorSpace, \u0022Srgb\u0022, StringComparison.OrdinalIgnoreCase );\r\n\r\n\t/// \u003Csummary\u003EEmit the document shape: \u003Cc\u003E{ path, colorSpace, processor }\u003C/c\u003E.\u003C/summary\u003E\r\n\tpublic JsonObject ToJson()\r\n\t{\r\n\t\tvar json = new JsonObject { [\u0022path\u0022] = Path };\r\n\r\n\t\tif ( !string.IsNullOrEmpty( ColorSpace ) \u0026\u0026 ColorSpace != \u0022Srgb\u0022 ) json[\u0022colorSpace\u0022] = ColorSpace;\r\n\t\tif ( !string.IsNullOrEmpty( Processor ) \u0026\u0026 Processor != \u0022None\u0022 ) json[\u0022processor\u0022] = Processor;\r\n\r\n\t\treturn json;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ERead the document shape. A bare string is accepted as a path-only descriptor.\u003C/summary\u003E\r\n\tpublic static TextureValue From( JsonNode node )\r\n\t{\r\n\t\tif ( node is null ) return null;\r\n\r\n\t\tif ( node is JsonValue value \u0026\u0026 value.TryGetValue\u003Cstring\u003E( out var path ) )\r\n\t\t{\r\n\t\t\treturn new TextureValue { Path = path };\r\n\t\t}\r\n\r\n\t\tif ( node is not JsonObject obj ) return null;\r\n\r\n\t\treturn new TextureValue\r\n\t\t{\r\n\t\t\tPath = ValueCodec.StringOf( obj[\u0022path\u0022] ),\r\n\t\t\tColorSpace = ValueCodec.StringOf( obj[\u0022colorSpace\u0022] ) ?? \u0022Srgb\u0022,\r\n\t\t\tProcessor = ValueCodec.StringOf( obj[\u0022processor\u0022] ) ?? \u0022None\u0022\r\n\t\t};\r\n\t}\r\n\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tpublic override string ToString() =\u003E IsEmpty ? \u0022(no texture)\u0022 : Path;\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Typed literal encoding: the bridge between the boxed values the model stores in port inline slots\r\n/// and parameter defaults, and the JSON a document holds.\r\n/// \u003Cpara\u003E\r\n/// Every float goes out through \u003Csee cref=\u0022Number(float)\u0022/\u003E, which formats with \u003Cc\u003E\u0022R\u0022\u003C/c\u003E so a value\r\n/// read back is bit-identical to the one written. That is what makes \u0022save an unchanged graph and get\r\n/// a byte-identical file\u0022 true, which in turn is what makes the text-diff short-circuit before a\r\n/// recompile trustworthy.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\npublic static class ValueCodec\r\n{\r\n\t// ---------------------------------------------------------------- writing ----\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Encode a boxed value using the shape implied by its CLR type. Colours become \u003Cc\u003E\u0022r,g,b,a\u0022\u003C/c\u003E,\r\n\t/// vectors become arrays, enums become their declared names, textures become objects.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static JsonNode Write( object value )\r\n\t{\r\n\t\tswitch ( value )\r\n\t\t{\r\n\t\t\tcase null:\r\n\t\t\t\treturn null;\r\n\t\t\tcase bool b:\r\n\t\t\t\treturn JsonValue.Create( b );\r\n\t\t\tcase int i:\r\n\t\t\t\treturn JsonValue.Create( i );\r\n\t\t\tcase uint u:\r\n\t\t\t\treturn JsonValue.Create( u );\r\n\t\t\tcase long l:\r\n\t\t\t\treturn JsonValue.Create( l );\r\n\t\t\tcase float f:\r\n\t\t\t\treturn Number( f );\r\n\t\t\tcase double d:\r\n\t\t\t\treturn Number( (float)d );\r\n\t\t\tcase string s:\r\n\t\t\t\treturn JsonValue.Create( s );\r\n\t\t\tcase Color c:\r\n\t\t\t\treturn JsonValue.Create( FormatColor( c ) );\r\n\t\t\tcase Vector2 v2:\r\n\t\t\t\treturn new JsonArray( Number( v2.x ), Number( v2.y ) );\r\n\t\t\tcase Vector3 v3:\r\n\t\t\t\treturn new JsonArray( Number( v3.x ), Number( v3.y ), Number( v3.z ) );\r\n\t\t\tcase Vector4 v4:\r\n\t\t\t\treturn new JsonArray( Number( v4.x ), Number( v4.y ), Number( v4.z ), Number( v4.w ) );\r\n\t\t\tcase TextureValue texture:\r\n\t\t\t\treturn texture.ToJson();\r\n\t\t\tcase Enum e:\r\n\t\t\t\treturn JsonValue.Create( e.ToString() );\r\n\t\t\tcase JsonNode json:\r\n\t\t\t\treturn json.DeepClone();\r\n\t\t\tcase float[] array:\r\n\t\t\t\treturn Vector( array );\r\n\t\t\tdefault:\r\n\t\t\t\treturn JsonValue.Create( PrismLog.Guard( \u0022encode value\u0022, () =\u003E value.ToString(), string.Empty ) );\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Encode a boxed value for a known shader type, coercing it into that type\u0027s canonical shape\r\n\t/// first. This is the form used for port inline literals and parameter defaults.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static JsonNode Write( ShaderType type, object value ) =\u003E Write( Coerce( value, type ) );\r\n\r\n\t/// \u003Csummary\u003EFormat a float exactly, so reading it back yields the same bits.\u003C/summary\u003E\r\n\tpublic static JsonNode Number( float value )\r\n\t{\r\n\t\tif ( float.IsNaN( value ) ) return JsonValue.Create( \u0022NaN\u0022 );\r\n\t\tif ( float.IsPositiveInfinity( value ) ) return JsonValue.Create( \u0022Infinity\u0022 );\r\n\t\tif ( float.IsNegativeInfinity( value ) ) return JsonValue.Create( \u0022-Infinity\u0022 );\r\n\r\n\t\tvar text = value.ToString( \u0022R\u0022, CultureInfo.InvariantCulture );\r\n\r\n\t\treturn JsonNode.Parse( text ) ?? JsonValue.Create( 0 );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EFormat a component array as a JSON array of exact floats.\u003C/summary\u003E\r\n\tpublic static JsonArray Vector( params float[] components )\r\n\t{\r\n\t\tvar array = new JsonArray();\r\n\r\n\t\tforeach ( var component in components ?? Array.Empty\u003Cfloat\u003E() )\r\n\t\t{\r\n\t\t\tarray.Add( Number( component ) );\r\n\t\t}\r\n\r\n\t\treturn array;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EFormat a colour the way the engine does: four exact components separated by commas.\u003C/summary\u003E\r\n\tpublic static string FormatColor( Color color ) =\u003E\r\n\t\tstring.Join( \u0022,\u0022,\r\n\t\t\tcolor.r.ToString( \u0022R\u0022, CultureInfo.InvariantCulture ),\r\n\t\t\tcolor.g.ToString( \u0022R\u0022, CultureInfo.InvariantCulture ),\r\n\t\t\tcolor.b.ToString( \u0022R\u0022, CultureInfo.InvariantCulture ),\r\n\t\t\tcolor.a.ToString( \u0022R\u0022, CultureInfo.InvariantCulture ) );\r\n\r\n\t// ---------------------------------------------------------------- reading ----\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Decode a literal for a known shader type. Returns the type\u0027s default rather than throwing when\r\n\t/// the JSON is the wrong shape \u2014 a corrupt literal must never take a document down.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static object Read( ShaderType type, JsonNode node )\r\n\t{\r\n\t\tTryRead( type, node, out var value );\r\n\t\treturn value;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EDecode a literal, reporting whether the JSON actually matched the requested type.\u003C/summary\u003E\r\n\tpublic static bool TryRead( ShaderType type, JsonNode node, out object value )\r\n\t{\r\n\t\tvalue = Default( type );\r\n\r\n\t\tif ( node is null ) return false;\r\n\r\n\t\tif ( type.IsObject )\r\n\t\t{\r\n\t\t\tif ( type.IsTexture )\r\n\t\t\t{\r\n\t\t\t\tvar texture = TextureValue.From( node );\r\n\r\n\t\t\t\tif ( texture is null ) return false;\r\n\r\n\t\t\t\tvalue = texture;\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\r\n\t\t\tvar path = StringOf( node );\r\n\r\n\t\t\tif ( path is null ) return false;\r\n\r\n\t\t\tvalue = path;\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tif ( type.IsBoolean \u0026\u0026 type.IsScalar )\r\n\t\t{\r\n\t\t\tif ( !TryNumbers( node, out var bits ) || bits.Length == 0 ) return false;\r\n\r\n\t\t\tvalue = bits[0] != 0f;\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tif ( type.IsScalar \u0026\u0026 type.IsIntegral )\r\n\t\t{\r\n\t\t\tif ( !TryNumbers( node, out var ints ) || ints.Length == 0 ) return false;\r\n\r\n\t\t\tvalue = (int)ints[0];\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tif ( type.IsScalar )\r\n\t\t{\r\n\t\t\tif ( !TryNumbers( node, out var scalars ) || scalars.Length == 0 ) return false;\r\n\r\n\t\t\tvalue = scalars[0];\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tif ( type.IsVector || type.IsMatrix )\r\n\t\t{\r\n\t\t\tvar wantsColor = node is JsonValue jv \u0026\u0026 jv.TryGetValue\u003Cstring\u003E( out var text ) \u0026\u0026\r\n\t\t\t\ttext.Contains( \u0027,\u0027 );\r\n\r\n\t\t\tif ( wantsColor \u0026\u0026 TryParseColor( StringOf( node ), out var color ) )\r\n\t\t\t{\r\n\t\t\t\tvalue = type.Components == 4 ? color : ToComponents( color, type.Components );\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\r\n\t\t\tif ( !TryNumbers( node, out var numbers ) ) return false;\r\n\r\n\t\t\tvalue = ToComponents( numbers, type.Components );\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Decode a literal with no declared type, guessing from the JSON shape. Used for the inline slots\r\n\t/// of an unregistered node, where we know nothing about the port.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static object ReadUntyped( JsonNode node )\r\n\t{\r\n\t\tswitch ( node )\r\n\t\t{\r\n\t\t\tcase null:\r\n\t\t\t\treturn null;\r\n\t\t\tcase JsonArray array:\r\n\t\t\t{\r\n\t\t\t\tvar numbers = new float[array.Count];\r\n\r\n\t\t\t\tfor ( int i = 0; i \u003C array.Count; i\u002B\u002B )\r\n\t\t\t\t{\r\n\t\t\t\t\tnumbers[i] = NumberOf( array[i] );\r\n\t\t\t\t}\r\n\r\n\t\t\t\treturn ToComponents( numbers, numbers.Length );\r\n\t\t\t}\r\n\t\t\tcase JsonObject obj when obj.ContainsKey( \u0022path\u0022 ):\r\n\t\t\t\treturn TextureValue.From( obj );\r\n\t\t\tcase JsonObject obj:\r\n\t\t\t\treturn obj.DeepClone();\r\n\t\t\tcase JsonValue value:\r\n\t\t\t{\r\n\t\t\t\tif ( value.TryGetValue\u003Cbool\u003E( out var b ) ) return b;\r\n\t\t\t\tif ( value.TryGetValue\u003Cint\u003E( out var i ) ) return i;\r\n\r\n\t\t\t\tif ( value.TryGetValue\u003Cstring\u003E( out var s ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( TryParseColor( s, out var color ) ) return color;\r\n\r\n\t\t\t\t\treturn s;\r\n\t\t\t\t}\r\n\r\n\t\t\t\t// Anything numeric that was not an int, whatever CLR type is behind it.\r\n\t\t\t\tif ( IsNumber( value ) ) return NumberOf( value );\r\n\r\n\t\t\t\treturn null;\r\n\t\t\t}\r\n\t\t\tdefault:\r\n\t\t\t\treturn null;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EParse the engine\u0027s \u003Cc\u003E\u0022r,g,b,a\u0022\u003C/c\u003E colour form. Accepts three or four components.\u003C/summary\u003E\r\n\tpublic static bool TryParseColor( string text, out Color color )\r\n\t{\r\n\t\tcolor = Color.White;\r\n\r\n\t\tif ( string.IsNullOrWhiteSpace( text ) ) return false;\r\n\r\n\t\tvar parts = text.Split( \u0027,\u0027, StringSplitOptions.TrimEntries );\r\n\r\n\t\tif ( parts.Length is \u003C 3 or \u003E 4 ) return false;\r\n\r\n\t\tvar values = new float[4];\r\n\t\tvalues[3] = 1f;\r\n\r\n\t\tfor ( int i = 0; i \u003C parts.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( !float.TryParse( parts[i], NumberStyles.Float, CultureInfo.InvariantCulture, out values[i] ) )\r\n\t\t\t{\r\n\t\t\t\treturn false;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tcolor = new Color( values[0], values[1], values[2], values[3] );\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe string behind a JSON value, or null when it is not a string.\u003C/summary\u003E\r\n\tpublic static string StringOf( JsonNode node )\r\n\t{\r\n\t\tif ( node is not JsonValue value ) return null;\r\n\r\n\t\treturn value.TryGetValue\u003Cstring\u003E( out var text ) ? text : null;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The number behind a JSON value, tolerating numbers written as strings.\r\n\t/// \u003Cpara\u003E\r\n\t/// Every numeric backing has to be tried by hand. A \u003Csee cref=\u0022JsonValue\u0022/\u003E parsed from text wraps a\r\n\t/// \u003Cc\u003EJsonElement\u003C/c\u003E and converts to anything numeric, but one built in memory wraps the exact CLR\r\n\t/// type it was created from \u2014 and \u003Cc\u003ETryGetValue\u0026lt;float\u0026gt;\u003C/c\u003E on a \u003Cc\u003EJsonValue\u0026lt;int\u0026gt;\u003C/c\u003E\r\n\t/// returns \u003Cb\u003Efalse\u003C/b\u003E. Documents reach us both ways: parsed from disk, and handed over as a live\r\n\t/// \u003Cc\u003EJsonObject\u003C/c\u003E by the asset system or by a migration step that synthesised it. Asking for only\r\n\t/// one type would silently read every number in the second kind as zero.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tpublic static float NumberOf( JsonNode node, float fallback = 0f )\r\n\t{\r\n\t\tif ( node is not JsonValue value ) return fallback;\r\n\r\n\t\tif ( value.TryGetValue\u003Cfloat\u003E( out var f ) ) return f;\r\n\t\tif ( value.TryGetValue\u003Cdouble\u003E( out var d ) ) return (float)d;\r\n\t\tif ( value.TryGetValue\u003Cint\u003E( out var i ) ) return i;\r\n\t\tif ( value.TryGetValue\u003Clong\u003E( out var l ) ) return l;\r\n\t\tif ( value.TryGetValue\u003Cuint\u003E( out var u ) ) return u;\r\n\t\tif ( value.TryGetValue\u003Culong\u003E( out var ul ) ) return ul;\r\n\t\tif ( value.TryGetValue\u003Cdecimal\u003E( out var m ) ) return (float)m;\r\n\t\tif ( value.TryGetValue\u003Cbool\u003E( out var b ) ) return b ? 1f : 0f;\r\n\r\n\t\tif ( value.TryGetValue\u003Cstring\u003E( out var text ) )\r\n\t\t{\r\n\t\t\tif ( float.TryParse( text, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed ) )\r\n\t\t\t{\r\n\t\t\t\treturn parsed;\r\n\t\t\t}\r\n\r\n\t\t\treturn text switch\r\n\t\t\t{\r\n\t\t\t\t\u0022NaN\u0022 =\u003E float.NaN,\r\n\t\t\t\t\u0022Infinity\u0022 =\u003E float.PositiveInfinity,\r\n\t\t\t\t\u0022-Infinity\u0022 =\u003E float.NegativeInfinity,\r\n\t\t\t\t_ =\u003E fallback\r\n\t\t\t};\r\n\t\t}\r\n\r\n\t\treturn fallback;\r\n\t}\r\n\r\n\t// ---------------------------------------------------------------- shaping ----\r\n\r\n\t/// \u003Csummary\u003EThe zero value of a shader type, in the boxed shape the model stores.\u003C/summary\u003E\r\n\tpublic static object Default( ShaderType type )\r\n\t{\r\n\t\tif ( type.IsTexture ) return new TextureValue();\r\n\t\tif ( type.IsObject ) return string.Empty;\r\n\t\tif ( type.IsBoolean \u0026\u0026 type.IsScalar ) return false;\r\n\t\tif ( type.IsScalar \u0026\u0026 type.IsIntegral ) return 0;\r\n\t\tif ( type.IsScalar ) return 0f;\r\n\r\n\t\treturn type.Components switch\r\n\t\t{\r\n\t\t\t2 =\u003E Vector2.Zero,\r\n\t\t\t3 =\u003E Vector3.Zero,\r\n\t\t\t4 =\u003E new Vector4( 0f, 0f, 0f, 0f ),\r\n\t\t\t_ =\u003E 0f\r\n\t\t};\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Reshape a boxed value into the canonical form for a type: widening a scalar into a vector by\r\n\t/// splat, truncating a wider vector, and converting between colours and vectors.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static object Coerce( object value, ShaderType type )\r\n\t{\r\n\t\t// There is no such thing as a value of type void, so there is nothing to keep. Saying so here\r\n\t\t// rather than letting it fall through the vector path keeps encoding a void slot idempotent.\r\n\t\tif ( type.IsVoid ) return Default( type );\r\n\r\n\t\tif ( value is null ) return Default( type );\r\n\r\n\t\tif ( type.IsTexture )\r\n\t\t{\r\n\t\t\treturn value switch\r\n\t\t\t{\r\n\t\t\t\tTextureValue texture =\u003E texture,\r\n\t\t\t\tstring path =\u003E new TextureValue { Path = path },\r\n\t\t\t\t_ =\u003E new TextureValue()\r\n\t\t\t};\r\n\t\t}\r\n\r\n\t\tif ( type.IsObject ) return value as string ?? string.Empty;\r\n\r\n\t\tif ( type.IsBoolean \u0026\u0026 type.IsScalar )\r\n\t\t{\r\n\t\t\treturn value switch\r\n\t\t\t{\r\n\t\t\t\tbool b =\u003E b,\r\n\t\t\t\tfloat f =\u003E f != 0f,\r\n\t\t\t\tint i =\u003E i != 0,\r\n\t\t\t\t_ =\u003E false\r\n\t\t\t};\r\n\t\t}\r\n\r\n\t\tif ( type.IsScalar \u0026\u0026 type.IsIntegral )\r\n\t\t{\r\n\t\t\treturn value switch\r\n\t\t\t{\r\n\t\t\t\tint i =\u003E i,\r\n\t\t\t\tfloat f =\u003E (int)f,\r\n\t\t\t\tbool b =\u003E b ? 1 : 0,\r\n\t\t\t\t_ =\u003E 0\r\n\t\t\t};\r\n\t\t}\r\n\r\n\t\tvar components = ToFloats( value );\r\n\r\n\t\tif ( components.Length == 0 ) return Default( type );\r\n\r\n\t\tif ( type.IsScalar ) return components[0];\r\n\r\n\t\t// Keep a colour a colour: it is what tells the writer to use the \u0022r,g,b,a\u0022 form.\r\n\t\tif ( value is Color \u0026\u0026 type.Components == 4 ) return value;\r\n\r\n\t\treturn ToComponents( components, type.Components );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EFlatten any supported boxed value into its float components.\u003C/summary\u003E\r\n\tpublic static float[] ToFloats( object value ) =\u003E value switch\r\n\t{\r\n\t\tnull =\u003E Array.Empty\u003Cfloat\u003E(),\r\n\t\tfloat f =\u003E new[] { f },\r\n\t\tdouble d =\u003E new[] { (float)d },\r\n\t\tint i =\u003E new[] { (float)i },\r\n\t\tbool b =\u003E new[] { b ? 1f : 0f },\r\n\t\tVector2 v2 =\u003E new[] { v2.x, v2.y },\r\n\t\tVector3 v3 =\u003E new[] { v3.x, v3.y, v3.z },\r\n\t\tVector4 v4 =\u003E new[] { v4.x, v4.y, v4.z, v4.w },\r\n\t\tColor c =\u003E new[] { c.r, c.g, c.b, c.a },\r\n\t\tfloat[] array =\u003E array,\r\n\t\tstring s =\u003E TryParseColor( s, out var parsed )\r\n\t\t\t? new[] { parsed.r, parsed.g, parsed.b, parsed.a }\r\n\t\t\t: Array.Empty\u003Cfloat\u003E(),\r\n\t\t_ =\u003E Array.Empty\u003Cfloat\u003E()\r\n\t};\r\n\r\n\t/// \u003Csummary\u003EBox a component array as the vector or scalar type of that width, splatting when short.\u003C/summary\u003E\r\n\tpublic static object ToComponents( float[] components, int width )\r\n\t{\r\n\t\tif ( components is null || components.Length == 0 ) components = new[] { 0f };\r\n\r\n\t\tfloat At( int index ) =\u003E\r\n\t\t\tindex \u003C components.Length ? components[index] : components.Length == 1 ? components[0] : 0f;\r\n\r\n\t\treturn width switch\r\n\t\t{\r\n\t\t\t\u003C= 1 =\u003E At( 0 ),\r\n\t\t\t2 =\u003E new Vector2( At( 0 ), At( 1 ) ),\r\n\t\t\t3 =\u003E new Vector3( At( 0 ), At( 1 ), At( 2 ) ),\r\n\t\t\t_ =\u003E new Vector4( At( 0 ), At( 1 ), At( 2 ), components.Length \u003E 3 ? At( 3 ) : 1f )\r\n\t\t};\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EBox a colour as the vector type of a given width.\u003C/summary\u003E\r\n\tpublic static object ToComponents( Color color, int width ) =\u003E\r\n\t\tToComponents( new[] { color.r, color.g, color.b, color.a }, width );\r\n\r\n\t/// \u003Csummary\u003ELower a boxed literal into the IR\u0027s constant representation.\u003C/summary\u003E\r\n\tpublic static ConstValue ToConst( object value )\r\n\t{\r\n\t\tvar components = ToFloats( value );\r\n\r\n\t\treturn components.Length switch\r\n\t\t{\r\n\t\t\t0 =\u003E ConstValue.Zero,\r\n\t\t\t1 =\u003E new ConstValue( components[0], 0, 0, 0 ),\r\n\t\t\t2 =\u003E new ConstValue( components[0], components[1], 0, 0 ),\r\n\t\t\t3 =\u003E new ConstValue( components[0], components[1], components[2], 0 ),\r\n\t\t\t_ =\u003E new ConstValue( components[0], components[1], components[2], components[3] )\r\n\t\t};\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Value equality across the boxed shapes, so a \u0022did this literal change\u0022 test does not report a\r\n\t/// change when a \u003Cc\u003Efloat\u003C/c\u003E and a one-element vector describe the same thing.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static bool Equal( object a, object b )\r\n\t{\r\n\t\tif ( ReferenceEquals( a, b ) ) return true;\r\n\t\tif ( a is null || b is null ) return false;\r\n\r\n\t\tif ( a is TextureValue ta \u0026\u0026 b is TextureValue tb ) return ta == tb;\r\n\t\tif ( a is string sa \u0026\u0026 b is string sb ) return string.Equals( sa, sb, StringComparison.Ordinal );\r\n\t\tif ( a is bool ba \u0026\u0026 b is bool bb ) return ba == bb;\r\n\r\n\t\tvar fa = ToFloats( a );\r\n\t\tvar fb = ToFloats( b );\r\n\r\n\t\tif ( fa.Length == 0 \u0026\u0026 fb.Length == 0 ) return Equals( a, b );\r\n\t\tif ( fa.Length != fb.Length ) return false;\r\n\r\n\t\tfor ( int i = 0; i \u003C fa.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( !fa[i].Equals( fb[i] ) ) return false;\r\n\t\t}\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EA short, human-readable form for inline pills and tooltips.\u003C/summary\u003E\r\n\tpublic static string Describe( object value )\r\n\t{\r\n\t\tswitch ( value )\r\n\t\t{\r\n\t\t\tcase null:\r\n\t\t\t\treturn \u0022-\u0022;\r\n\t\t\tcase bool b:\r\n\t\t\t\treturn b ? \u0022true\u0022 : \u0022false\u0022;\r\n\t\t\tcase int i:\r\n\t\t\t\treturn i.ToString( CultureInfo.InvariantCulture );\r\n\t\t\tcase float f:\r\n\t\t\t\treturn f.ToString( \u00220.###\u0022, CultureInfo.InvariantCulture );\r\n\t\t\tcase string s:\r\n\t\t\t\treturn s;\r\n\t\t\tcase TextureValue texture:\r\n\t\t\t\treturn texture.ToString();\r\n\t\t\tcase Color c:\r\n\t\t\t\treturn $\u0022{c.r:0.##}, {c.g:0.##}, {c.b:0.##}, {c.a:0.##}\u0022;\r\n\t\t\tdefault:\r\n\t\t\t{\r\n\t\t\t\tvar components = ToFloats( value );\r\n\r\n\t\t\t\tif ( components.Length == 0 ) return value.ToString();\r\n\r\n\t\t\t\treturn string.Join( \u0022, \u0022, components.Select( x =\u003E x.ToString( \u00220.###\u0022, CultureInfo.InvariantCulture ) ) );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETrue when a JSON value holds a number, whatever CLR type is behind it.\u003C/summary\u003E\r\n\tpublic static bool IsNumber( JsonNode node )\r\n\t{\r\n\t\tif ( node is not JsonValue value ) return false;\r\n\t\tif ( value.TryGetValue\u003Cbool\u003E( out _ ) ) return false;\r\n\t\tif ( value.TryGetValue\u003Cstring\u003E( out _ ) ) return false;\r\n\r\n\t\treturn value.TryGetValue\u003Cfloat\u003E( out _ ) || value.TryGetValue\u003Cdouble\u003E( out _ ) ||\r\n\t\t\tvalue.TryGetValue\u003Cint\u003E( out _ ) || value.TryGetValue\u003Clong\u003E( out _ ) ||\r\n\t\t\tvalue.TryGetValue\u003Cuint\u003E( out _ ) || value.TryGetValue\u003Culong\u003E( out _ ) ||\r\n\t\t\tvalue.TryGetValue\u003Cdecimal\u003E( out _ );\r\n\t}\r\n\r\n\tstatic bool TryNumbers( JsonNode node, out float[] numbers )\r\n\t{\r\n\t\tswitch ( node )\r\n\t\t{\r\n\t\t\tcase JsonArray array:\r\n\t\t\t{\r\n\t\t\t\tnumbers = new float[array.Count];\r\n\r\n\t\t\t\tfor ( int i = 0; i \u003C array.Count; i\u002B\u002B )\r\n\t\t\t\t{\r\n\t\t\t\t\tnumbers[i] = NumberOf( array[i] );\r\n\t\t\t\t}\r\n\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\t\t\tcase JsonValue value when value.TryGetValue\u003Cstring\u003E( out var text ) \u0026\u0026 text.Contains( \u0027,\u0027 ):\r\n\t\t\t{\r\n\t\t\t\tvar parts = text.Split( \u0027,\u0027, StringSplitOptions.TrimEntries );\r\n\t\t\t\tnumbers = new float[parts.Length];\r\n\r\n\t\t\t\tfor ( int i = 0; i \u003C parts.Length; i\u002B\u002B )\r\n\t\t\t\t{\r\n\t\t\t\t\tfloat.TryParse( parts[i], NumberStyles.Float, CultureInfo.InvariantCulture, out numbers[i] );\r\n\t\t\t\t}\r\n\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\t\t\tcase JsonValue:\r\n\t\t\t\tnumbers = new[] { NumberOf( node ) };\r\n\t\t\t\treturn true;\r\n\t\t\tdefault:\r\n\t\t\t\tnumbers = Array.Empty\u003Cfloat\u003E();\r\n\t\t\t\treturn false;\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/Text/CodeWindow.cs","FileName":"CodeWindow.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using Editor.Prism.Core;\r\nusing Editor.Prism.Integration;\r\nusing Editor.Prism.Ui;\r\nusing Margin = Sandbox.UI.Margin;\r\nusing System.IO;\r\nusing System.Text;\r\nusing PrismDiagnostic = Editor.Prism.Core.Diagnostic;\r\n\r\nnamespace Editor.Prism.Text;\r\n\r\n/// \u003Csummary\u003EOne open file or buffer in the \u003Csee cref=\u0022CodeWindow\u0022/\u003E.\u003C/summary\u003E\r\npublic sealed class CodeTab\r\n{\r\n\t/// \u003Csummary\u003EThe editor widget showing this buffer.\u003C/summary\u003E\r\n\tpublic CodeEditorWidget Editor { get; init; }\r\n\r\n\t/// \u003Csummary\u003EThe document. Shorthand for \u003Cc\u003EEditor.Document\u003C/c\u003E.\u003C/summary\u003E\r\n\tpublic TextDocument Document =\u003E Editor?.Document;\r\n\r\n\t/// \u003Csummary\u003EAbsolute path on disk, or null for a synthetic buffer such as generated code.\u003C/summary\u003E\r\n\tpublic string FilePath { get; set; }\r\n\r\n\t/// \u003Csummary\u003ETab caption.\u003C/summary\u003E\r\n\tpublic string Title { get; set; } = \u0022untitled\u0022;\r\n\r\n\t/// \u003Csummary\u003ELanguage id driving highlighting.\u003C/summary\u003E\r\n\tpublic string Language { get; set; } = \u0022hlsl\u0022;\r\n\r\n\t/// \u003Csummary\u003EWhether the buffer can be edited.\u003C/summary\u003E\r\n\tpublic bool ReadOnly { get; set; }\r\n\r\n\t/// \u003Csummary\u003EWhether the buffer differs from disk.\u003C/summary\u003E\r\n\tpublic bool IsModified =\u003E Document is { IsModified: true };\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Set when the file changed on disk while this buffer had unsaved edits, so the tab can say the\r\n\t/// two have diverged. Cleared by a reload or a save.\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool ChangedOnDisk { get; set; }\r\n\r\n\t/// \u003Csummary\u003ECaption with the modified and diverged markers, as drawn on the tab.\u003C/summary\u003E\r\n\tpublic string DisplayTitle =\u003E ChangedOnDisk ? Title \u002B \u0022 \u26A0\u0022 : IsModified ? Title \u002B \u0022 \u2022\u0022 : Title;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Completion, hover and background validation for this buffer. Owned by the tab and disposed with\r\n\t/// it, because every part of it holds a reference to the editor widget.\r\n\t/// \u003C/summary\u003E\r\n\tinternal Completion.CodeIntelligence Intelligence { get; set; }\r\n\r\n\t/// \u003Csummary\u003ECached tab width from the last paint, used for hit testing.\u003C/summary\u003E\r\n\tinternal Rect TabRect { get; set; }\r\n\r\n\t/// \u003Csummary\u003EDiagnostic rendering.\u003C/summary\u003E\r\n\tpublic override string ToString() =\u003E Title;\r\n}\r\n\r\n/// \u003Csummary\u003EOne entry in the outline dock.\u003C/summary\u003E\r\npublic sealed record CodeSymbol( string Name, string Detail, int Line, string Icon, int Depth );\r\n\r\n/// \u003Csummary\u003E\r\n/// The code editor window: a tab strip over a stack of \u003Csee cref=\u0022CodeEditorWidget\u0022/\u003Es, a find bar, a\r\n/// diagnostics dock and an outline dock. This is the shell WP-11 owns; the graph window docks its own\r\n/// generated-code panel separately.\r\n/// \u003C/summary\u003E\r\npublic sealed class CodeWindow : DockWindow\r\n{\r\n\tstatic CodeWindow s_instance;\r\n\r\n\treadonly List\u003CCodeTab\u003E _tabs = new();\r\n\r\n\tCodeTabStrip _strip;\r\n\tFindReplaceBar _findBar;\r\n\tWidget _editorStack;\r\n\tListView _diagnosticsList;\r\n\tListView _outlineList;\r\n\tLineEdit _outlineFilter;\r\n\tLabel _statusPosition;\r\n\tLabel _statusSelection;\r\n\tLabel _statusLanguage;\r\n\tLabel _statusEncoding;\r\n\tWidget _diagnosticsPanel;\r\n\tWidget _outlinePanel;\r\n\r\n\tCodeTab _active;\r\n\tRealTimeSince _sinceOutlineRefresh;\r\n\tint _outlineVersion = -1;\r\n\r\n\t/// \u003Csummary\u003EThe live window, or null when it has never been opened or was closed.\u003C/summary\u003E\r\n\tpublic static CodeWindow Instance =\u003E s_instance is { IsValid: true } ? s_instance : null;\r\n\r\n\t/// \u003Csummary\u003EOpens the window, or raises it when it is already open.\u003C/summary\u003E\r\n\tpublic static CodeWindow Open()\r\n\t{\r\n\t\tif ( Instance is not null )\r\n\t\t{\r\n\t\t\tInstance.Show();\r\n\t\t\tInstance.Focus();\r\n\t\t\treturn Instance;\r\n\t\t}\r\n\r\n\t\tvar window = new CodeWindow();\r\n\t\twindow.Show();\r\n\t\treturn window;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EOpens a file in the window, creating the window if needed. Line and column are one-based.\u003C/summary\u003E\r\n\tpublic static CodeWindow OpenFile( string absolutePath, int line = 0, int column = 1 )\r\n\t{\r\n\t\tvar window = Open();\r\n\r\n\t\tif ( window is null )\r\n\t\t\treturn null;\r\n\r\n\t\tvar tab = window.OpenDocument( absolutePath );\r\n\r\n\t\tif ( tab is not null \u0026\u0026 line \u003E 0 )\r\n\t\t\ttab.Editor.GoToLine( line, Math.Max( 1, column ) );\r\n\r\n\t\treturn window;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ECreates the window. Prefer \u003Csee cref=\u0022Open\u0022/\u003E.\u003C/summary\u003E\r\n\tpublic CodeWindow()\r\n\t{\r\n\t\ts_instance = this;\r\n\r\n\t\tDeleteOnClose = true;\r\n\t\tTitle = $\u0022{PrismConstants.ProductName} \u2014 Code\u0022;\r\n\t\tSize = new Vector2( 1280, 820 );\r\n\r\n\t\tPrismLog.Guard( \u0022Prism.Text: window icon\u0022, () =\u003E SetWindowIcon( \u0022code\u0022 ) );\r\n\r\n\t\tBuildMenu();\r\n\t\tBuildStatusBar();\r\n\r\n\t\tvar host = BuildHost();\r\n\t\tDockManager.SetCentralWidget( host );\r\n\r\n\t\tBuildDocks();\r\n\r\n\t\t// Assigning the cookie restores window geometry and the saved dock layout, so every dock has\r\n\t\t// to exist by now or the restore has nothing to place.\r\n\t\tStateCookie = \u0022PrismCodeWindow\u0022;\r\n\r\n\t\t// External-change detection. AssetHooks watches the content folder; without this subscriber a\r\n\t\t// file edited in another program stayed stale in the buffer here and was silently overwritten\r\n\t\t// by the next save. Nothing reloads behind the user\u0027s back \u2014 an unmodified buffer refreshes in\r\n\t\t// place, a modified one is marked and says so.\r\n\t\tAssetHooks.ShaderSourceChangedOnDisk \u002B= OnFileChangedOnDisk;\r\n\t\tAssetHooks.DocumentChangedOnDisk \u002B= OnFileChangedOnDisk;\r\n\r\n\t\tUpdateStatus();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A file this window has open was changed by something else.\r\n\t/// \u003Cpara\u003E\r\n\t/// An untouched buffer is re-read on the spot: it has nothing to lose and showing stale text is\r\n\t/// strictly worse. A buffer with unsaved edits is left exactly as it is and the status bar says the\r\n\t/// file moved underneath it, because silently discarding the user\u0027s work \u2014 or silently keeping it\r\n\t/// and overwriting theirs \u2014 are both worse than telling them.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tvoid OnFileChangedOnDisk( string absolutePath )\r\n\t{\r\n\t\tif ( string.IsNullOrWhiteSpace( absolutePath ) || !this.IsValid() ) return;\r\n\r\n\t\tPrismLog.Guard( \u0022Handling an external file change\u0022, () =\u003E\r\n\t\t{\r\n\t\t\tvar full = Path.GetFullPath( absolutePath );\r\n\r\n\t\t\tforeach ( var tab in _tabs )\r\n\t\t\t{\r\n\t\t\t\tif ( tab?.FilePath is null ) continue;\r\n\t\t\t\tif ( !string.Equals( Path.GetFullPath( tab.FilePath ), full, StringComparison.OrdinalIgnoreCase ) ) continue;\r\n\r\n\t\t\t\tif ( tab.IsModified )\r\n\t\t\t\t{\r\n\t\t\t\t\ttab.ChangedOnDisk = true;\r\n\r\n\t\t\t\t\t_strip?.Update();\r\n\t\t\t\t\tStatusBar?.ShowMessage(\r\n\t\t\t\t\t\t$\u0022\\\u0022{tab.Title}\\\u0022 changed on disk and has unsaved edits \u2014 File \u25B8 Reload From Disk to take theirs\u0022 );\r\n\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tReloadTab( tab );\r\n\t\t\t}\r\n\t\t} );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EEvery open tab, in strip order.\u003C/summary\u003E\r\n\tpublic IReadOnlyList\u003CCodeTab\u003E Tabs =\u003E _tabs;\r\n\r\n\t/// \u003Csummary\u003EThe tab currently showing, or null.\u003C/summary\u003E\r\n\tpublic CodeTab ActiveTab =\u003E _active;\r\n\r\n\t/// \u003Csummary\u003EThe editor currently showing, or null.\u003C/summary\u003E\r\n\tpublic CodeEditorWidget ActiveEditor =\u003E _active?.Editor;\r\n\r\n\t// ---- construction -----------------------------------------------------\r\n\r\n\tWidget BuildHost()\r\n\t{\r\n\t\tvar host = new Widget( null );\r\n\t\thost.Layout = Layout.Column();\r\n\t\thost.Layout.Margin = 0;\r\n\t\thost.Layout.Spacing = 0;\r\n\r\n\t\t_strip = new CodeTabStrip( host );\r\n\t\t_strip.TabSelected = SetActiveTab;\r\n\t\t_strip.TabClosed = tab =\u003E CloseTab( tab );\r\n\t\t_strip.NewTabRequested = () =\u003E NewDocument();\r\n\t\thost.Layout.Add( _strip );\r\n\r\n\t\t_findBar = new FindReplaceBar( host );\r\n\t\thost.Layout.Add( _findBar );\r\n\r\n\t\t_editorStack = new Widget( host );\r\n\t\t_editorStack.Layout = Layout.Column();\r\n\t\t_editorStack.Layout.Margin = 0;\r\n\t\thost.Layout.Add( _editorStack, 1 );\r\n\r\n\t\treturn host;\r\n\t}\r\n\r\n\tvoid BuildDocks()\r\n\t{\r\n\t\t_diagnosticsPanel = BuildDiagnosticsPanel();\r\n\t\t_outlinePanel = BuildOutlinePanel();\r\n\r\n\t\tDockManager.AddDock( \u0022Diagnostics\u0022, \u0022error_outline\u0022, _diagnosticsPanel, DockArea.Bottom );\r\n\t\tDockManager.AddDock( \u0022Outline\u0022, \u0022list\u0022, _outlinePanel, DockArea.Right );\r\n\t}\r\n\r\n\tWidget BuildDiagnosticsPanel()\r\n\t{\r\n\t\tvar panel = new Widget( null );\r\n\t\tpanel.Layout = Layout.Column();\r\n\t\tpanel.Layout.Margin = 0;\r\n\r\n\t\t_diagnosticsList = new ListView( panel )\r\n\t\t{\r\n\t\t\tItemSize = new Vector2( -1, 24 ),\r\n\t\t\tItemPaint = PaintDiagnosticRow,\r\n\t\t\tItemActivated = OnDiagnosticActivated,\r\n\t\t\tItemClicked = OnDiagnosticActivated\r\n\t\t};\r\n\r\n\t\tpanel.Layout.Add( _diagnosticsList, 1 );\r\n\t\treturn panel;\r\n\t}\r\n\r\n\tWidget BuildOutlinePanel()\r\n\t{\r\n\t\tvar panel = new Widget( null );\r\n\t\tpanel.Layout = Layout.Column();\r\n\t\tpanel.Layout.Margin = new Margin( 4, 4, 4, 4 );\r\n\t\tpanel.Layout.Spacing = 4;\r\n\r\n\t\t_outlineFilter = new LineEdit( panel ) { PlaceholderText = \u0022Filter symbols\u0022 };\r\n\t\t_outlineFilter.TextEdited \u002B= _ =\u003E RefreshOutline( true );\r\n\t\tpanel.Layout.Add( _outlineFilter );\r\n\r\n\t\t_outlineList = new ListView( panel )\r\n\t\t{\r\n\t\t\tItemSize = new Vector2( -1, 22 ),\r\n\t\t\tItemPaint = PaintOutlineRow,\r\n\t\t\tItemActivated = OnOutlineActivated,\r\n\t\t\tItemClicked = OnOutlineActivated\r\n\t\t};\r\n\r\n\t\tpanel.Layout.Add( _outlineList, 1 );\r\n\t\treturn panel;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EPlaces the docks in their default arrangement.\u003C/summary\u003E\r\n\tprotected override void BuildDefaultLayout()\r\n\t{\r\n\t\tvar diagnostics = DockManager.OpenDock( \u0022Diagnostics\u0022, DockArea.Bottom );\r\n\t\tvar outline = DockManager.OpenDock( \u0022Outline\u0022, DockArea.Right );\r\n\r\n\t\tPrismLog.Guard( \u0022Prism.Text: default layout\u0022, () =\u003E\r\n\t\t{\r\n\t\t\tDockManager.SetSplitterProportions( outline, 0.78f, 0.22f );\r\n\t\t\tDockManager.SetSplitterProportions( diagnostics, 0.76f, 0.24f );\r\n\t\t} );\r\n\t}\r\n\r\n\tvoid BuildStatusBar()\r\n\t{\r\n\t\tStatusBar = new StatusBar( this );\r\n\r\n\t\t_statusPosition = new Label( \u0022Ln 1, Col 1\u0022 ) { Color = PrismTheme.TextSecondary };\r\n\t\t_statusSelection = new Label( \u0022\u0022 ) { Color = PrismTheme.TextMuted };\r\n\t\t_statusLanguage = new Label( \u0022\u0022 ) { Color = PrismTheme.TextSecondary };\r\n\t\t_statusEncoding = new Label( \u0022\u0022 ) { Color = PrismTheme.TextMuted };\r\n\r\n\t\tStatusBar.AddWidgetLeft( _statusPosition );\r\n\t\tStatusBar.AddWidgetLeft( _statusSelection );\r\n\t\tStatusBar.AddWidgetRight( _statusLanguage );\r\n\t\tStatusBar.AddWidgetRight( _statusEncoding );\r\n\t}\r\n\r\n\tvoid BuildMenu()\r\n\t{\r\n\t\tvar menu = new MenuBar( this );\r\n\t\tMenuBar = menu;\r\n\r\n\t\tmenu.AddOption( \u0022File/New\u0022, \u0022note_add\u0022, () =\u003E NewDocument(), \u0022Ctrl\u002BN\u0022 );\r\n\t\tmenu.AddOption( \u0022File/Open\u2026\u0022, \u0022folder_open\u0022, PromptOpen, \u0022Ctrl\u002BO\u0022 );\r\n\t\tmenu.AddSeparator();\r\n\t\tmenu.AddOption( \u0022File/Save\u0022, \u0022save\u0022, () =\u003E SaveActive(), \u0022Ctrl\u002BS\u0022 );\r\n\t\tmenu.AddOption( \u0022File/Save As\u2026\u0022, \u0022save_as\u0022, PromptSaveAs );\r\n\t\tmenu.AddOption( \u0022File/Save All\u0022, \u0022done_all\u0022, () =\u003E SaveAll(), \u0022Ctrl\u002BShift\u002BS\u0022 );\r\n\t\tmenu.AddSeparator();\r\n\t\tmenu.AddOption( \u0022File/Reload From Disk\u0022, \u0022refresh\u0022, () =\u003E ReloadTab( _active ) );\r\n\t\tmenu.AddOption( \u0022File/Open in External Editor\u0022, \u0022open_in_new\u0022, OpenExternally );\r\n\t\tmenu.AddSeparator();\r\n\t\tmenu.AddOption( \u0022File/Close Tab\u0022, \u0022close\u0022, () =\u003E { if ( _active is not null ) CloseTab( _active ); }, \u0022Ctrl\u002BW\u0022 );\r\n\t\tmenu.AddOption( \u0022File/Close Window\u0022, \u0022logout\u0022, Close );\r\n\r\n\t\tmenu.AddOption( \u0022Edit/Undo\u0022, \u0022undo\u0022, () =\u003E WithEditor( e =\u003E { e.Controller.PerformUndo(); e.EnsureCaretVisible(); } ), \u0022Ctrl\u002BZ\u0022 );\r\n\t\tmenu.AddOption( \u0022Edit/Redo\u0022, \u0022redo\u0022, () =\u003E WithEditor( e =\u003E { e.Controller.PerformRedo(); e.EnsureCaretVisible(); } ), \u0022Ctrl\u002BShift\u002BZ\u0022 );\r\n\t\tmenu.AddSeparator();\r\n\t\tmenu.AddOption( \u0022Edit/Cut\u0022, \u0022content_cut\u0022, () =\u003E WithEditor( e =\u003E e.Controller.Cut() ), \u0022Ctrl\u002BX\u0022 );\r\n\t\tmenu.AddOption( \u0022Edit/Copy\u0022, \u0022content_copy\u0022, () =\u003E WithEditor( e =\u003E e.Controller.Copy() ), \u0022Ctrl\u002BC\u0022 );\r\n\t\tmenu.AddOption( \u0022Edit/Paste\u0022, \u0022content_paste\u0022, () =\u003E WithEditor( e =\u003E e.Controller.Paste() ), \u0022Ctrl\u002BV\u0022 );\r\n\t\tmenu.AddSeparator();\r\n\t\tmenu.AddOption( \u0022Edit/Find\u2026\u0022, \u0022search\u0022, () =\u003E ShowFind( false ), \u0022Ctrl\u002BF\u0022 );\r\n\t\tmenu.AddOption( \u0022Edit/Replace\u2026\u0022, \u0022find_replace\u0022, () =\u003E ShowFind( true ), \u0022Ctrl\u002BH\u0022 );\r\n\t\tmenu.AddOption( \u0022Edit/Go To Line\u2026\u0022, \u0022my_location\u0022, ShowGoToLine, \u0022Ctrl\u002BG\u0022 );\r\n\t\tmenu.AddSeparator();\r\n\t\tmenu.AddOption( \u0022Edit/Toggle Comment\u0022, \u0022comment\u0022, () =\u003E WithEditor( e =\u003E e.Controller.ToggleLineComment() ), \u0022Ctrl\u002B/\u0022 );\r\n\t\tmenu.AddOption( \u0022Edit/Toggle Block Comment\u0022, \u0022notes\u0022, () =\u003E WithEditor( e =\u003E e.Controller.ToggleBlockComment() ) );\r\n\t\tmenu.AddOption( \u0022Edit/Trim Trailing Whitespace\u0022, \u0022cleaning_services\u0022, () =\u003E WithEditor( e =\u003E e.Controller.TrimTrailingWhitespace() ) );\r\n\r\n\t\tAddToggle( menu, \u0022View/Line Numbers\u0022, () =\u003E ActiveEditor?.ShowLineNumbers ?? true, value =\u003E ForEachEditor( e =\u003E e.ShowLineNumbers = value ) );\r\n\t\tAddToggle( menu, \u0022View/Indent Guides\u0022, () =\u003E ActiveEditor?.ShowIndentGuides ?? true, value =\u003E ForEachEditor( e =\u003E e.ShowIndentGuides = value ) );\r\n\t\tAddToggle( menu, \u0022View/Whitespace\u0022, () =\u003E ActiveEditor?.ShowWhitespace ?? false, value =\u003E ForEachEditor( e =\u003E e.ShowWhitespace = value ) );\r\n\t\tAddToggle( menu, \u0022View/Current Line Highlight\u0022, () =\u003E ActiveEditor?.HighlightCurrentLine ?? true, value =\u003E ForEachEditor( e =\u003E e.HighlightCurrentLine = value ) );\r\n\t\tAddToggle( menu, \u0022View/Occurrence Highlight\u0022, () =\u003E ActiveEditor?.HighlightOccurrences ?? true, value =\u003E ForEachEditor( e =\u003E e.HighlightOccurrences = value ) );\r\n\t\tAddToggle( menu, \u0022View/Column Ruler\u0022, () =\u003E ActiveEditor?.ShowRuler ?? false, value =\u003E ForEachEditor( e =\u003E e.ShowRuler = value ) );\r\n\t\tmenu.AddSeparator();\r\n\t\tmenu.AddOption( \u0022View/Zoom In\u0022, \u0022zoom_in\u0022, () =\u003E ForEachEditor( e =\u003E e.FontSize\u002B\u002B ), \u0022Ctrl\u002B\u002B\u0022 );\r\n\t\tmenu.AddOption( \u0022View/Zoom Out\u0022, \u0022zoom_out\u0022, () =\u003E ForEachEditor( e =\u003E e.FontSize-- ), \u0022Ctrl\u002B-\u0022 );\r\n\t\tmenu.AddOption( \u0022View/Reset Zoom\u0022, \u0022search\u0022, () =\u003E ForEachEditor( e =\u003E e.FontSize = PrismTheme.CodeSize ) );\r\n\t\tmenu.AddSeparator();\r\n\t\tmenu.AddOption( \u0022View/Fold All\u0022, \u0022unfold_less\u0022, () =\u003E WithEditor( e =\u003E { e.Folding?.CollapseAll(); e.LayoutScrollbars(); e.Update(); } ) );\r\n\t\tmenu.AddOption( \u0022View/Unfold All\u0022, \u0022unfold_more\u0022, () =\u003E WithEditor( e =\u003E { e.Folding?.ExpandAll(); e.LayoutScrollbars(); e.Update(); } ) );\r\n\r\n\t\tvar view = menu.FindOrCreateMenu( \u0022View\u0022 );\r\n\r\n\t\tif ( view is not null )\r\n\t\t{\r\n\t\t\tview.AddSeparator();\r\n\t\t\tvar docks = view.AddMenu( \u0022Panels\u0022, \u0022dashboard\u0022 );\r\n\t\t\tdocks.AboutToShow \u002B= () =\u003E CreateDynamicViewMenu( docks );\r\n\t\t}\r\n\r\n\t\tmenu.AddOption( \u0022Go/Next Problem\u0022, \u0022arrow_downward\u0022, () =\u003E StepDiagnostic( 1 ), \u0022F8\u0022 );\r\n\t\tmenu.AddOption( \u0022Go/Previous Problem\u0022, \u0022arrow_upward\u0022, () =\u003E StepDiagnostic( -1 ), \u0022Shift\u002BF8\u0022 );\r\n\t\tmenu.AddSeparator();\r\n\t\tmenu.AddOption( \u0022Go/Next Match\u0022, \u0022navigate_next\u0022, () =\u003E _findBar?.FindNext(), \u0022F3\u0022 );\r\n\t\tmenu.AddOption( \u0022Go/Previous Match\u0022, \u0022navigate_before\u0022, () =\u003E _findBar?.FindNext( false ), \u0022Shift\u002BF3\u0022 );\r\n\t}\r\n\r\n\tstatic void AddToggle( MenuBar menu, string path, Func\u003Cbool\u003E get, Action\u003Cbool\u003E set )\r\n\t{\r\n\t\tvar option = menu.AddOption( path, null, null );\r\n\t\toption.Checkable = true;\r\n\t\toption.FetchCheckedState = get;\r\n\t\toption.Toggled \u002B= set;\r\n\t}\r\n\r\n\t// ---- tabs -------------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003ECreates an empty buffer and focuses it.\u003C/summary\u003E\r\n\tpublic CodeTab NewDocument( string language = \u0022hlsl\u0022 )\r\n\t{\r\n\t\tvar tab = CreateTab( new TextDocument(), \u0022untitled\u0022, null, language, false );\r\n\t\tSetActiveTab( tab );\r\n\t\treturn tab;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Opens a file, focusing the existing tab when it is already open. Returns null when the file\r\n\t/// could not be read.\r\n\t/// \u003C/summary\u003E\r\n\tpublic CodeTab OpenDocument( string absolutePath )\r\n\t{\r\n\t\tif ( string.IsNullOrWhiteSpace( absolutePath ) )\r\n\t\t\treturn null;\r\n\r\n\t\tvar full = PrismLog.Guard( \u0022Prism.Text: resolve path\u0022, () =\u003E Path.GetFullPath( absolutePath ), absolutePath );\r\n\r\n\t\tfor ( var i = 0; i \u003C _tabs.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( !string.Equals( _tabs[i].FilePath, full, StringComparison.OrdinalIgnoreCase ) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tSetActiveTab( _tabs[i] );\r\n\t\t\treturn _tabs[i];\r\n\t\t}\r\n\r\n\t\t// Breadcrumbs. Opening a file walks straight into the engine\u0027s shader compiler, and a native fault\r\n\t\t// there takes the process down with no managed exception and nothing in the log \u2014 so the log has\r\n\t\t// to say how far we got before it happened.\r\n\t\tPrismLog.Info( $\u0022Prism.Text: opening \u0027{full}\u0027\u0022 );\r\n\r\n\t\tvar document = new TextDocument();\r\n\r\n\t\tif ( !document.Load( full ) )\r\n\t\t{\r\n\t\t\tStatusBar?.ShowMessage( $\u0022Could not open {Path.GetFileName( full )}: {document.LoadError}\u0022 );\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tvar language = LanguageForPath( full );\r\n\r\n\t\tPrismLog.Info( $\u0022Prism.Text: loaded {document.LineCount} line(s), language \u0027{language}\u0027 \u2014 building the tab\u0022 );\r\n\r\n\t\tvar tab = CreateTab( document, Path.GetFileName( full ), full, language, false );\r\n\t\tSetActiveTab( tab );\r\n\r\n\t\tPrismLog.Info( $\u0022Prism.Text: \u0027{Path.GetFileName( full )}\u0027 is open\u0022 );\r\n\r\n\t\treturn tab;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EOpens an in-memory buffer, such as generated shader text. Returns the new tab.\u003C/summary\u003E\r\n\tpublic CodeTab OpenText( string title, string text, string language, bool readOnly = true )\r\n\t{\r\n\t\tfor ( var i = 0; i \u003C _tabs.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( _tabs[i].FilePath is not null || !string.Equals( _tabs[i].Title, title, StringComparison.Ordinal ) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t_tabs[i].Editor.SetText( text, language );\r\n\t\t\tSetActiveTab( _tabs[i] );\r\n\t\t\treturn _tabs[i];\r\n\t\t}\r\n\r\n\t\tvar document = new TextDocument( text ?? string.Empty );\r\n\t\tdocument.MarkSaved();\r\n\r\n\t\tvar tab = CreateTab( document, title, null, language, readOnly );\r\n\t\tSetActiveTab( tab );\r\n\t\treturn tab;\r\n\t}\r\n\r\n\tCodeTab CreateTab( TextDocument document, string title, string path, string language, bool readOnly )\r\n\t{\r\n\t\tvar editor = new CodeEditorWidget( _editorStack )\r\n\t\t{\r\n\t\t\tReadOnly = readOnly\r\n\t\t};\r\n\r\n\t\teditor.SetDocument( document, language );\r\n\t\teditor.ReadOnly = readOnly;\r\n\r\n\t\tvar tab = new CodeTab\r\n\t\t{\r\n\t\t\tEditor = editor,\r\n\t\t\tFilePath = path,\r\n\t\t\tTitle = string.IsNullOrEmpty( title ) ? \u0022untitled\u0022 : title,\r\n\t\t\tLanguage = language,\r\n\t\t\tReadOnly = readOnly\r\n\t\t};\r\n\r\n\t\teditor.UnhandledKey = ( _, key ) =\u003E HandleWindowKey( key );\r\n\t\teditor.SaveRequested \u002B= _ =\u003E SaveTab( tab );\r\n\t\teditor.FindRequested \u002B= ( _, replace ) =\u003E ShowFind( replace );\r\n\t\teditor.GoToLineRequested \u002B= _ =\u003E ShowGoToLine();\r\n\t\teditor.FindStepRequested \u002B= ( _, direction ) =\u003E _findBar?.FindNext( direction \u003E= 0 );\r\n\t\teditor.CaretMoved \u002B= _ =\u003E UpdateStatus();\r\n\t\teditor.TextChanged \u002B= _ =\u003E\r\n\t\t{\r\n\t\t\t_strip?.Update();\r\n\t\t\tUpdateStatus();\r\n\t\t};\r\n\r\n\t\t// Completion, signature help, hover and background validation, all in one attach. A read-only\r\n\t\t// buffer still gets hover and highlighting, but not the compiler tier: it is generated text the\r\n\t\t// user cannot fix, and probe-compiling it on every keystroke it will never receive is waste.\r\n\t\ttab.Intelligence = PrismLog.Guard( \u0022Prism.Text: attach code intelligence\u0022,\r\n\t\t\t() =\u003E Completion.CodeIntelligence.Attach( editor, path, !readOnly ), null );\r\n\r\n\t\t_tabs.Add( tab );\r\n\t\t_editorStack.Layout.Add( editor, 1 );\r\n\t\teditor.Visible = false;\r\n\r\n\t\t_strip.SetTabs( _tabs );\r\n\t\treturn tab;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EShows one tab and hides the rest.\u003C/summary\u003E\r\n\tpublic void SetActiveTab( CodeTab tab )\r\n\t{\r\n\t\tif ( tab is null || !_tabs.Contains( tab ) )\r\n\t\t\treturn;\r\n\r\n\t\t_active = tab;\r\n\r\n\t\tfor ( var i = 0; i \u003C _tabs.Count; i\u002B\u002B )\r\n\t\t\t_tabs[i].Editor.Visible = ReferenceEquals( _tabs[i], tab );\r\n\r\n\t\t_findBar.Editor = tab.Editor;\r\n\r\n\t\tif ( _findBar.Visible )\r\n\t\t\t_findBar.Refresh();\r\n\r\n\t\t_strip.SetActive( tab );\r\n\t\t_outlineVersion = -1;\r\n\r\n\t\tRefreshDiagnostics();\r\n\t\tRefreshOutline( true );\r\n\t\tUpdateStatus();\r\n\r\n\t\ttab.Editor.Focus();\r\n\t\ttab.Editor.Update();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Closes a tab. A modified buffer raises a non-blocking prompt and returns false; answering the\r\n\t/// prompt closes the tab. Pass \u003Cparamref name=\u0022force\u0022/\u003E to skip the prompt.\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool CloseTab( CodeTab tab, bool force = false )\r\n\t{\r\n\t\tif ( tab is null || !_tabs.Contains( tab ) )\r\n\t\t\treturn false;\r\n\r\n\t\tif ( !force \u0026\u0026 tab.IsModified \u0026\u0026 !tab.ReadOnly )\r\n\t\t{\r\n\t\t\tPromptUnsaved( $\u0022\\\u0022{tab.Title}\\\u0022 has unsaved changes.\u0022,\r\n\t\t\t\t() =\u003E { if ( SaveTab( tab ) ) CloseTab( tab, true ); },\r\n\t\t\t\t() =\u003E CloseTab( tab, true ) );\r\n\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tvar index = _tabs.IndexOf( tab );\r\n\t\t_tabs.Remove( tab );\r\n\r\n\t\tPrismLog.Guard( \u0022Prism.Text: destroy editor\u0022, () =\u003E\r\n\t\t{\r\n\t\t\t// Before the widget, not after: the completion popup, the hover watcher and the validator\r\n\t\t\t// all hold the editor and all unsubscribe from it on dispose.\r\n\t\t\ttab.Intelligence?.Dispose();\r\n\t\t\ttab.Intelligence = null;\r\n\r\n\t\t\ttab.Editor.Teardown();\r\n\t\t\ttab.Editor.Destroy();\r\n\t\t} );\r\n\r\n\t\t_strip.SetTabs( _tabs );\r\n\r\n\t\tif ( ReferenceEquals( _active, tab ) )\r\n\t\t{\r\n\t\t\t_active = null;\r\n\r\n\t\t\tif ( _tabs.Count \u003E 0 )\r\n\t\t\t\tSetActiveTab( _tabs[Math.Clamp( index, 0, _tabs.Count - 1 )] );\r\n\t\t\telse\r\n\t\t\t\tUpdateStatus();\r\n\t\t}\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ESaves the active tab.\u003C/summary\u003E\r\n\tpublic bool SaveActive() =\u003E SaveTab( _active );\r\n\r\n\t/// \u003Csummary\u003ESaves one tab, prompting for a path when it has none.\u003C/summary\u003E\r\n\tpublic bool SaveTab( CodeTab tab )\r\n\t{\r\n\t\tif ( tab is null || tab.ReadOnly )\r\n\t\t\treturn false;\r\n\r\n\t\tif ( string.IsNullOrEmpty( tab.FilePath ) )\r\n\t\t\treturn SaveTabAs( tab );\r\n\r\n\t\tif ( !tab.Document.Save( tab.FilePath ) )\r\n\t\t{\r\n\t\t\tStatusBar?.ShowMessage( $\u0022Could not save {tab.Title}: {tab.Document.SaveError}\u0022 );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\t// Whatever the file said before, this buffer is now what is on disk.\r\n\t\ttab.ChangedOnDisk = false;\r\n\r\n\t\tStatusBar?.ShowMessage( $\u0022Saved {tab.Title}\u0022 );\r\n\t\t_strip?.Update();\r\n\t\tUpdateStatus();\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ESaves one tab to a path chosen by the user.\u003C/summary\u003E\r\n\tpublic bool SaveTabAs( CodeTab tab )\r\n\t{\r\n\t\tif ( tab is null )\r\n\t\t\treturn false;\r\n\r\n\t\tvar dialog = new FileDialog( this ) { Title = \u0022Save Shader Source\u0022 };\r\n\t\tdialog.SetModeSave();\r\n\t\tdialog.SetNameFilter( \u0022Shader source (*.hlsl *.slang *.shader *.fxc *.h *.txt)\u0022 );\r\n\t\tdialog.DefaultSuffix = tab.Language == \u0022slang\u0022 ? PrismConstants.SlangExtension : PrismConstants.HlslExtension;\r\n\r\n\t\tif ( !string.IsNullOrEmpty( tab.FilePath ) )\r\n\t\t\tdialog.SelectFile( tab.FilePath );\r\n\r\n\t\tif ( !dialog.Execute() )\r\n\t\t\treturn false;\r\n\r\n\t\tvar path = dialog.SelectedFile;\r\n\r\n\t\tif ( string.IsNullOrWhiteSpace( path ) )\r\n\t\t\treturn false;\r\n\r\n\t\tif ( !tab.Document.Save( path ) )\r\n\t\t{\r\n\t\t\tStatusBar?.ShowMessage( $\u0022Could not save: {tab.Document.SaveError}\u0022 );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\ttab.FilePath = path;\r\n\t\ttab.Title = Path.GetFileName( path );\r\n\t\ttab.Language = LanguageForPath( path );\r\n\t\ttab.Editor.Language = tab.Language;\r\n\r\n\t\t// Include resolution is relative to the including file\u0027s own directory, so a buffer that just\r\n\t\t// moved resolves its includes from somewhere else now.\r\n\t\tif ( tab.Intelligence is not null ) tab.Intelligence.FilePath = path;\r\n\r\n\t\t_strip.SetTabs( _tabs );\r\n\t\tUpdateStatus();\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Re-reads a tab from disk, keeping the viewport. A modified buffer prompts first; answering the\r\n\t/// prompt performs the reload.\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool ReloadTab( CodeTab tab )\r\n\t{\r\n\t\tif ( tab?.FilePath is null )\r\n\t\t\treturn false;\r\n\r\n\t\tif ( tab.IsModified )\r\n\t\t{\r\n\t\t\tPromptUnsaved( $\u0022\\\u0022{tab.Title}\\\u0022 has unsaved changes that reloading will discard.\u0022,\r\n\t\t\t\t() =\u003E { if ( SaveTab( tab ) ) ReloadTab( tab ); },\r\n\t\t\t\t() =\u003E { tab.Document.MarkSaved(); ReloadTab( tab ); } );\r\n\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tvar fresh = new TextDocument();\r\n\r\n\t\tif ( !fresh.Load( tab.FilePath ) )\r\n\t\t{\r\n\t\t\tStatusBar?.ShowMessage( $\u0022Could not reload {tab.Title}: {fresh.LoadError}\u0022 );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\ttab.Document.LineEnding = fresh.LineEnding;\r\n\t\ttab.Document.Encoding = fresh.Encoding;\r\n\t\ttab.Document.HasByteOrderMark = fresh.HasByteOrderMark;\r\n\t\ttab.Editor.SetText( fresh.Text, tab.Language );\r\n\t\ttab.ChangedOnDisk = false;\r\n\r\n\t\tStatusBar?.ShowMessage( $\u0022Reloaded {tab.Title}\u0022 );\r\n\t\t_strip?.Update();\r\n\t\tUpdateStatus();\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ESaves every modified tab that has a path.\u003C/summary\u003E\r\n\tpublic int SaveAll()\r\n\t{\r\n\t\tvar saved = 0;\r\n\r\n\t\tfor ( var i = 0; i \u003C _tabs.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( _tabs[i].IsModified \u0026\u0026 !_tabs[i].ReadOnly \u0026\u0026 SaveTab( _tabs[i] ) )\r\n\t\t\t\tsaved\u002B\u002B;\r\n\t\t}\r\n\r\n\t\treturn saved;\r\n\t}\r\n\r\n\tvoid PromptOpen()\r\n\t{\r\n\t\tvar dialog = new FileDialog( this ) { Title = \u0022Open Shader Source\u0022 };\r\n\t\tdialog.SetModeOpen();\r\n\t\tdialog.SetFindExistingFile();\r\n\t\tdialog.SetNameFilter( \u0022Shader source (*.hlsl *.slang *.shader *.fxc *.h *.txt)\u0022 );\r\n\r\n\t\tif ( dialog.Execute() )\r\n\t\t\tOpenDocument( dialog.SelectedFile );\r\n\t}\r\n\r\n\tvoid PromptSaveAs() =\u003E SaveTabAs( _active );\r\n\r\n\tvoid OpenExternally()\r\n\t{\r\n\t\tvar tab = _active;\r\n\r\n\t\tif ( tab?.FilePath is null )\r\n\t\t\treturn;\r\n\r\n\t\tPrismLog.Guard( \u0022Prism.Text: external editor\u0022,\r\n\t\t\t() =\u003E CodeEditor.OpenFile( tab.FilePath, tab.Editor.CaretPosition.Line \u002B 1, tab.Editor.CaretPosition.Column \u002B 1 ) );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EMaps a file extension to a lexer language id.\u003C/summary\u003E\r\n\tpublic static string LanguageForPath( string path )\r\n\t{\r\n\t\tif ( string.IsNullOrEmpty( path ) )\r\n\t\t\treturn \u0022hlsl\u0022;\r\n\r\n\t\tvar extension = Path.GetExtension( path ).ToLowerInvariant();\r\n\r\n\t\treturn extension switch\r\n\t\t{\r\n\t\t\t\u0022.slang\u0022 =\u003E \u0022slang\u0022,\r\n\t\t\t\u0022.shader\u0022 =\u003E \u0022vfx\u0022,\r\n\t\t\t\u0022.vfx\u0022 =\u003E \u0022vfx\u0022,\r\n\t\t\t_ =\u003E \u0022hlsl\u0022\r\n\t\t};\r\n\t}\r\n\r\n\t// ---- find and navigation ----------------------------------------------\r\n\r\n\t/// \u003Csummary\u003EShows the find bar, optionally with the replace row.\u003C/summary\u003E\r\n\tpublic void ShowFind( bool replace )\r\n\t{\r\n\t\tif ( _active is null )\r\n\t\t\treturn;\r\n\r\n\t\t_findBar.Editor = _active.Editor;\r\n\t\t_findBar.Open( replace );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EOpens the go-to-line prompt.\u003C/summary\u003E\r\n\tpublic void ShowGoToLine()\r\n\t{\r\n\t\tvar editor = ActiveEditor;\r\n\r\n\t\tif ( editor is null )\r\n\t\t\treturn;\r\n\r\n\t\tvar popup = new PopupWidget( this );\r\n\t\tpopup.Layout = Layout.Row();\r\n\t\tpopup.Layout.Margin = new Margin( 8, 6, 8, 6 );\r\n\t\tpopup.Layout.Spacing = 6;\r\n\r\n\t\tpopup.Layout.Add( new Label( $\u0022Go to line (1 \u2013 {editor.Document.LineCount}):\u0022 ) );\r\n\r\n\t\tvar entry = new LineEdit( popup ) { PlaceholderText = \u0022line[:column]\u0022 };\r\n\t\tentry.MinimumWidth = 120;\r\n\r\n\t\tentry.ReturnPressed \u002B= () =\u003E\r\n\t\t{\r\n\t\t\tvar parts = (entry.Text ?? string.Empty).Split( \u0027:\u0027, StringSplitOptions.RemoveEmptyEntries );\r\n\r\n\t\t\tif ( parts.Length \u003E 0 \u0026\u0026 int.TryParse( parts[0].Trim(), out var line ) )\r\n\t\t\t{\r\n\t\t\t\tvar column = 1;\r\n\r\n\t\t\t\tif ( parts.Length \u003E 1 )\r\n\t\t\t\t\tint.TryParse( parts[1].Trim(), out column );\r\n\r\n\t\t\t\teditor.GoToLine( line, Math.Max( 1, column ) );\r\n\t\t\t}\r\n\r\n\t\t\tpopup.Destroy();\r\n\t\t};\r\n\r\n\t\tpopup.Layout.Add( entry );\r\n\t\tpopup.OpenAtCursor();\r\n\t\tentry.Focus();\r\n\t}\r\n\r\n\tvoid StepDiagnostic( int direction )\r\n\t{\r\n\t\tvar editor = ActiveEditor;\r\n\r\n\t\tif ( editor is null || editor.Diagnostics.Count == 0 )\r\n\t\t\treturn;\r\n\r\n\t\tvar ordered = new List\u003CCodeDiagnostic\u003E( editor.Diagnostics );\r\n\t\tordered.Sort( static ( a, b ) =\u003E a.Range.Min.CompareTo( b.Range.Min ) );\r\n\r\n\t\tvar caret = editor.CaretPosition;\r\n\t\tvar target = -1;\r\n\r\n\t\tif ( direction \u003E= 0 )\r\n\t\t{\r\n\t\t\tfor ( var i = 0; i \u003C ordered.Count; i\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tif ( ordered[i].Range.Min \u003E caret )\r\n\t\t\t\t{\r\n\t\t\t\t\ttarget = i;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif ( target \u003C 0 )\r\n\t\t\t\ttarget = 0;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tfor ( var i = ordered.Count - 1; i \u003E= 0; i-- )\r\n\t\t\t{\r\n\t\t\t\tif ( ordered[i].Range.Min \u003C caret )\r\n\t\t\t\t{\r\n\t\t\t\t\ttarget = i;\r\n\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif ( target \u003C 0 )\r\n\t\t\t\ttarget = ordered.Count - 1;\r\n\t\t}\r\n\r\n\t\teditor.Reveal( ordered[target].Range, true, 4 );\r\n\t\t_diagnosticsList?.SelectItem( ordered[target] );\r\n\t}\r\n\r\n\t// ---- diagnostics ------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003EPushes pipeline diagnostics onto a tab and refreshes the dock.\u003C/summary\u003E\r\n\tpublic void SetDiagnostics( CodeTab tab, IEnumerable\u003CPrismDiagnostic\u003E diagnostics )\r\n\t{\r\n\t\tif ( tab is null )\r\n\t\t\treturn;\r\n\r\n\t\ttab.Editor.SetDiagnostics( diagnostics );\r\n\r\n\t\tif ( ReferenceEquals( tab, _active ) )\r\n\t\t\tRefreshDiagnostics();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ERebuilds the diagnostics dock from the active tab.\u003C/summary\u003E\r\n\tpublic void RefreshDiagnostics()\r\n\t{\r\n\t\tif ( _diagnosticsList is not { IsValid: true } )\r\n\t\t\treturn;\r\n\r\n\t\tvar editor = ActiveEditor;\r\n\r\n\t\tif ( editor is null )\r\n\t\t{\r\n\t\t\t_diagnosticsList.SetItems( Array.Empty\u003Cobject\u003E() );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar items = new List\u003CCodeDiagnostic\u003E( editor.Diagnostics );\r\n\t\titems.Sort( static ( a, b ) =\u003E\r\n\t\t{\r\n\t\t\tvar bySeverity = b.Severity.CompareTo( a.Severity );\r\n\t\t\treturn bySeverity != 0 ? bySeverity : a.Range.Min.CompareTo( b.Range.Min );\r\n\t\t} );\r\n\r\n\t\t_diagnosticsList.SetItems( items );\r\n\t}\r\n\r\n\tvoid OnDiagnosticActivated( object item )\r\n\t{\r\n\t\tif ( item is not CodeDiagnostic diagnostic || ActiveEditor is null )\r\n\t\t\treturn;\r\n\r\n\t\tActiveEditor.Reveal( diagnostic.Range, true, 4 );\r\n\t\tActiveEditor.Focus();\r\n\t}\r\n\r\n\tvoid PaintDiagnosticRow( VirtualWidget item )\r\n\t{\r\n\t\tif ( item.Object is not CodeDiagnostic diagnostic )\r\n\t\t\treturn;\r\n\r\n\t\tvar rect = item.Rect;\r\n\r\n\t\tif ( item.Selected )\r\n\t\t\titem.PaintBackground( PrismTheme.AccentSoft, 3f );\r\n\t\telse if ( item.Hovered )\r\n\t\t\titem.PaintBackground( PrismTheme.PanelAlt, 3f );\r\n\r\n\t\tvar color = PrismTheme.ForSeverity( diagnostic.Severity );\r\n\r\n\t\tPaint.SetPen( color );\r\n\t\tPaint.DrawIcon( new Rect( rect.Left \u002B 4f, rect.Top, 18f, rect.Height ), IconFor( diagnostic.Severity ), 13f, TextFlag.Center );\r\n\r\n\t\tPaint.SetDefaultFont( PrismTheme.BodySize, 400, false, true );\r\n\r\n\t\tvar lineText = $\u0022{diagnostic.Range.Min.Line \u002B 1}\u0022;\r\n\t\tPaint.SetPen( PrismTheme.TextMuted );\r\n\t\tPaint.DrawText( new Rect( rect.Left \u002B 24f, rect.Top, 42f, rect.Height ), lineText, TextFlag.LeftCenter | TextFlag.SingleLine );\r\n\r\n\t\tif ( !string.IsNullOrEmpty( diagnostic.Code ) )\r\n\t\t{\r\n\t\t\tPaint.SetPen( PrismTheme.TextDisabled );\r\n\t\t\tPaint.DrawText( new Rect( rect.Left \u002B 66f, rect.Top, 54f, rect.Height ), diagnostic.Code, TextFlag.LeftCenter | TextFlag.SingleLine );\r\n\t\t}\r\n\r\n\t\tPaint.SetPen( item.Selected ? PrismTheme.TextPrimary : PrismTheme.TextSecondary );\r\n\t\tPaint.DrawText( new Rect( rect.Left \u002B 124f, rect.Top, rect.Width - 130f, rect.Height ),\r\n\t\t\tdiagnostic.Message ?? string.Empty, TextFlag.LeftCenter | TextFlag.SingleLine );\r\n\t}\r\n\r\n\tstatic string IconFor( DiagnosticSeverity severity ) =\u003E severity switch\r\n\t{\r\n\t\tDiagnosticSeverity.Error =\u003E \u0022error\u0022,\r\n\t\tDiagnosticSeverity.Warning =\u003E \u0022warning\u0022,\r\n\t\t_ =\u003E \u0022info\u0022\r\n\t};\r\n\r\n\t// ---- outline ----------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003ERebuilds the outline dock from the active document.\u003C/summary\u003E\r\n\tpublic void RefreshOutline( bool force = false )\r\n\t{\r\n\t\tif ( _outlineList is not { IsValid: true } )\r\n\t\t\treturn;\r\n\r\n\t\tvar editor = ActiveEditor;\r\n\r\n\t\tif ( editor is null )\r\n\t\t{\r\n\t\t\t_outlineList.SetItems( Array.Empty\u003Cobject\u003E() );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( !force \u0026\u0026 _outlineVersion == editor.Document.Version )\r\n\t\t\treturn;\r\n\r\n\t\t_outlineVersion = editor.Document.Version;\r\n\r\n\t\t// The real parser rather than the regex fallback: it runs over the token stream the lexer has\r\n\t\t// already produced, so it is not fooled by a declaration inside a comment or a string, and it\r\n\t\t// knows about containers and parameters the regex pass cannot see.\r\n\t\tvar symbols = PrismLog.Guard( \u0022Prism.Text: outline\u0022,\r\n\t\t\t() =\u003E Completion.DocumentSymbols.For( editor.Document, editor.Language ).ToOutline(),\r\n\t\t\tnull ) ?? CodeOutline.Scan( editor.Document );\r\n\t\tvar filter = _outlineFilter?.Text;\r\n\r\n\t\tif ( !string.IsNullOrWhiteSpace( filter ) )\r\n\t\t{\r\n\t\t\tvar narrowed = new List\u003CCodeSymbol\u003E();\r\n\r\n\t\t\tfor ( var i = 0; i \u003C symbols.Count; i\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tif ( symbols[i].Name is not null \u0026\u0026\r\n\t\t\t\t     symbols[i].Name.Contains( filter, StringComparison.OrdinalIgnoreCase ) )\r\n\t\t\t\t\tnarrowed.Add( symbols[i] );\r\n\t\t\t}\r\n\r\n\t\t\tsymbols = narrowed;\r\n\t\t}\r\n\r\n\t\t_outlineList.SetItems( symbols );\r\n\t}\r\n\r\n\tvoid OnOutlineActivated( object item )\r\n\t{\r\n\t\tif ( item is not CodeSymbol symbol || ActiveEditor is null )\r\n\t\t\treturn;\r\n\r\n\t\tActiveEditor.GoToLine( symbol.Line \u002B 1 );\r\n\t}\r\n\r\n\tvoid PaintOutlineRow( VirtualWidget item )\r\n\t{\r\n\t\tif ( item.Object is not CodeSymbol symbol )\r\n\t\t\treturn;\r\n\r\n\t\tvar rect = item.Rect;\r\n\r\n\t\tif ( item.Selected )\r\n\t\t\titem.PaintBackground( PrismTheme.AccentSoft, 3f );\r\n\t\telse if ( item.Hovered )\r\n\t\t\titem.PaintBackground( PrismTheme.PanelAlt, 3f );\r\n\r\n\t\tvar indent = 6f \u002B symbol.Depth * 12f;\r\n\r\n\t\tPaint.SetPen( PrismTheme.TextMuted );\r\n\t\tPaint.DrawIcon( new Rect( rect.Left \u002B indent, rect.Top, 16f, rect.Height ), symbol.Icon, 12f, TextFlag.Center );\r\n\r\n\t\tPaint.SetDefaultFont( PrismTheme.BodySize, 400, false, true );\r\n\t\tPaint.SetPen( item.Selected ? PrismTheme.TextPrimary : PrismTheme.TextSecondary );\r\n\t\tPaint.DrawText( new Rect( rect.Left \u002B indent \u002B 20f, rect.Top, rect.Width - indent - 26f, rect.Height ),\r\n\t\t\tsymbol.Name ?? string.Empty, TextFlag.LeftCenter | TextFlag.SingleLine );\r\n\r\n\t\tif ( string.IsNullOrEmpty( symbol.Detail ) )\r\n\t\t\treturn;\r\n\r\n\t\tPaint.SetPen( PrismTheme.TextDisabled );\r\n\t\tPaint.DrawText( new Rect( rect.Left, rect.Top, rect.Width - 8f, rect.Height ),\r\n\t\t\tsymbol.Detail, TextFlag.RightCenter | TextFlag.SingleLine );\r\n\t}\r\n\r\n\t// ---- status -----------------------------------------------------------\r\n\r\n\tvoid UpdateStatus()\r\n\t{\r\n\t\tif ( _statusPosition is not { IsValid: true } )\r\n\t\t\treturn;\r\n\r\n\t\tvar editor = ActiveEditor;\r\n\r\n\t\tif ( editor is null )\r\n\t\t{\r\n\t\t\t_statusPosition.Text = \u0022\u0022;\r\n\t\t\t_statusSelection.Text = \u0022\u0022;\r\n\t\t\t_statusLanguage.Text = \u0022\u0022;\r\n\t\t\t_statusEncoding.Text = \u0022\u0022;\r\n\t\t\tTitle = $\u0022{PrismConstants.ProductName} \u2014 Code\u0022;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar caret = editor.CaretPosition;\r\n\t\t_statusPosition.Text = $\u0022Ln {caret.Line \u002B 1}, Col {caret.Column \u002B 1}\u0022;\r\n\r\n\t\tvar selection = editor.Selection;\r\n\t\tvar selected = 0;\r\n\r\n\t\tfor ( var i = 0; i \u003C selection.Count; i\u002B\u002B )\r\n\t\t\tselected \u002B= editor.Document.GetText( selection[i].Selection ).Length;\r\n\r\n\t\tif ( selection.Count \u003E 1 )\r\n\t\t\t_statusSelection.Text = $\u0022{selection.Count} carets \u00B7 {selected} selected\u0022;\r\n\t\telse if ( selected \u003E 0 )\r\n\t\t\t_statusSelection.Text = $\u0022{selected} selected\u0022;\r\n\t\telse\r\n\t\t\t_statusSelection.Text = \u0022\u0022;\r\n\r\n\t\tvar indent = editor.Controller.UseTabs ? \u0022Tabs\u0022 : $\u0022Spaces: {editor.Controller.IndentSize}\u0022;\r\n\t\t_statusLanguage.Text = $\u0022{editor.Language.ToUpperInvariant()} \u00B7 {indent}\u0022;\r\n\r\n\t\tvar ending = editor.Document.LineEnding switch\r\n\t\t{\r\n\t\t\tLineEndingStyle.Lf =\u003E \u0022LF\u0022,\r\n\t\t\tLineEndingStyle.Cr =\u003E \u0022CR\u0022,\r\n\t\t\t_ =\u003E \u0022CRLF\u0022\r\n\t\t};\r\n\r\n\t\t_statusEncoding.Text = $\u0022{ending} \u00B7 {(editor.Document.HasByteOrderMark ? \u0022UTF-8 BOM\u0022 : \u0022UTF-8\u0022)}\u0022;\r\n\r\n\t\tvar title = _active?.DisplayTitle ?? string.Empty;\r\n\t\tTitle = string.IsNullOrEmpty( title )\r\n\t\t\t? $\u0022{PrismConstants.ProductName} \u2014 Code\u0022\r\n\t\t\t: $\u0022{title} \u2014 {PrismConstants.ProductName}\u0022;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Window-level accelerators, routed from the focused editor because it consumes every shortcut.\r\n\t/// Returns true when the key was consumed.\r\n\t/// \u003C/summary\u003E\r\n\tbool HandleWindowKey( CodeKeyInfo key )\r\n\t{\r\n\t\tif ( key.Ctrl \u0026\u0026 !key.Alt )\r\n\t\t{\r\n\t\t\tswitch ( key.Key )\r\n\t\t\t{\r\n\t\t\t\tcase KeyCode.N when !key.Shift:\r\n\t\t\t\t\tNewDocument();\r\n\t\t\t\t\treturn true;\r\n\r\n\t\t\t\tcase KeyCode.O when !key.Shift:\r\n\t\t\t\t\tPromptOpen();\r\n\t\t\t\t\treturn true;\r\n\r\n\t\t\t\tcase KeyCode.W when !key.Shift:\r\n\t\t\t\t\tif ( _active is not null )\r\n\t\t\t\t\t\tCloseTab( _active );\r\n\r\n\t\t\t\t\treturn true;\r\n\r\n\t\t\t\tcase KeyCode.S when key.Shift:\r\n\t\t\t\t\tSaveAll();\r\n\t\t\t\t\treturn true;\r\n\r\n\t\t\t\tcase KeyCode.Tab:\r\n\t\t\t\tcase KeyCode.Backtab:\r\n\t\t\t\t\tStepTab( key.Shift ? -1 : 1 );\r\n\t\t\t\t\treturn true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( key.Key == KeyCode.F8 )\r\n\t\t{\r\n\t\t\tStepDiagnostic( key.Shift ? -1 : 1 );\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EMoves to the next or previous tab, wrapping around.\u003C/summary\u003E\r\n\tpublic void StepTab( int direction )\r\n\t{\r\n\t\tif ( _tabs.Count \u003C 2 || _active is null )\r\n\t\t\treturn;\r\n\r\n\t\tvar index = _tabs.IndexOf( _active );\r\n\r\n\t\tif ( index \u003C 0 )\r\n\t\t\treturn;\r\n\r\n\t\tindex = (index \u002B direction \u002B _tabs.Count) % _tabs.Count;\r\n\t\tSetActiveTab( _tabs[index] );\r\n\t}\r\n\r\n\tvoid WithEditor( Action\u003CCodeEditorWidget\u003E action )\r\n\t{\r\n\t\tvar editor = ActiveEditor;\r\n\r\n\t\tif ( editor is null )\r\n\t\t\treturn;\r\n\r\n\t\tPrismLog.Guard( \u0022Prism.Text: command\u0022, () =\u003E action( editor ) );\r\n\t\teditor.Update();\r\n\t}\r\n\r\n\tvoid ForEachEditor( Action\u003CCodeEditorWidget\u003E action )\r\n\t{\r\n\t\tfor ( var i = 0; i \u003C _tabs.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar editor = _tabs[i].Editor;\r\n\r\n\t\t\tif ( editor is { IsValid: true } )\r\n\t\t\t\tPrismLog.Guard( \u0022Prism.Text: view option\u0022, () =\u003E action( editor ) );\r\n\t\t}\r\n\t}\r\n\r\n\t[EditorEvent.Frame]\r\n\tvoid CodeWindowFrame()\r\n\t{\r\n\t\tif ( !IsValid || _sinceOutlineRefresh \u003C 0.75f )\r\n\t\t\treturn;\r\n\r\n\t\t_sinceOutlineRefresh = 0;\r\n\t\tRefreshOutline();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EShows the non-blocking save / discard / cancel prompt.\u003C/summary\u003E\r\n\tvoid PromptUnsaved( string message, Action onSave, Action onDiscard )\r\n\t{\r\n\t\tPrismLog.Guard( \u0022Prism.Text: unsaved prompt\u0022, () =\u003E\r\n\t\t{\r\n\t\t\tvar popup = new PopupDialogWidget( \u0022\u2753\u0022 );\r\n\t\t\tpopup.WindowTitle = \u0022Unsaved Changes\u0022;\r\n\t\t\tpopup.MessageLabel.Text = message;\r\n\r\n\t\t\tpopup.ButtonLayout.AddStretchCell();\r\n\t\t\tpopup.ButtonLayout.Add( new Button( \u0022Cancel\u0022 ) { Clicked = () =\u003E popup.Destroy() } );\r\n\t\t\tpopup.ButtonLayout.Add( new Button( \u0022Discard\u0022 ) { Clicked = () =\u003E { popup.Destroy(); onDiscard?.Invoke(); } } );\r\n\t\t\tpopup.ButtonLayout.Add( new Button.Primary( \u0022Save\u0022 ) { Clicked = () =\u003E { popup.Destroy(); onSave?.Invoke(); } } );\r\n\r\n\t\t\tpopup.SetModal( true, true );\r\n\t\t\tpopup.Hide();\r\n\t\t\tpopup.Show();\r\n\t\t} );\r\n\t}\r\n\r\n\tbool _forceClose;\r\n\r\n\tprotected override bool OnClose()\r\n\t{\r\n\t\tif ( _forceClose )\r\n\t\t\treturn base.OnClose();\r\n\r\n\t\tvar modified = 0;\r\n\r\n\t\tfor ( var i = 0; i \u003C _tabs.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( _tabs[i].IsModified \u0026\u0026 !_tabs[i].ReadOnly )\r\n\t\t\t\tmodified\u002B\u002B;\r\n\t\t}\r\n\r\n\t\tif ( modified == 0 )\r\n\t\t\treturn base.OnClose();\r\n\r\n\t\tPromptUnsaved( $\u0022{modified} file(s) have unsaved changes.\u0022,\r\n\t\t\t() =\u003E { SaveAll(); _forceClose = true; Close(); },\r\n\t\t\t() =\u003E { _forceClose = true; Close(); } );\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\tprotected override void OnClosed()\r\n\t{\r\n\t\t// AssetHooks is static and would otherwise pin this window, its tabs and every document in them\r\n\t\t// for the rest of the session.\r\n\t\tAssetHooks.ShaderSourceChangedOnDisk -= OnFileChangedOnDisk;\r\n\t\tAssetHooks.DocumentChangedOnDisk -= OnFileChangedOnDisk;\r\n\r\n\t\tif ( ReferenceEquals( s_instance, this ) )\r\n\t\t\ts_instance = null;\r\n\r\n\t\tbase.OnClosed();\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The tab strip. Painted rather than composed, so it matches \u003Csee cref=\u0022PrismTheme\u0022/\u003E exactly \u2014 the\r\n/// engine\u0027s \u003Cc\u003ETabBar\u003C/c\u003E binding exposes no managed API at all.\r\n/// \u003C/summary\u003E\r\ninternal sealed class CodeTabStrip : Widget\r\n{\r\n\treadonly List\u003CCodeTab\u003E _tabs = new();\r\n\r\n\tCodeTab _active;\r\n\tCodeTab _hovered;\r\n\tbool _hoverClose;\r\n\tfloat _scroll;\r\n\r\n\tpublic CodeTabStrip( Widget parent ) : base( parent )\r\n\t{\r\n\t\tFixedHeight = 30f;\r\n\t\tMouseTracking = true;\r\n\t\tCursor = CursorShape.Finger;\r\n\t}\r\n\r\n\tpublic Action\u003CCodeTab\u003E TabSelected { get; set; }\r\n\tpublic Action\u003CCodeTab\u003E TabClosed { get; set; }\r\n\tpublic Action NewTabRequested { get; set; }\r\n\r\n\tpublic void SetTabs( IReadOnlyList\u003CCodeTab\u003E tabs )\r\n\t{\r\n\t\t_tabs.Clear();\r\n\r\n\t\tif ( tabs is not null )\r\n\t\t\t_tabs.AddRange( tabs );\r\n\r\n\t\tUpdate();\r\n\t}\r\n\r\n\tpublic void SetActive( CodeTab tab )\r\n\t{\r\n\t\t_active = tab;\r\n\t\tUpdate();\r\n\t}\r\n\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tPaint.ClearPen();\r\n\t\tPaint.SetBrush( PrismTheme.Panel );\r\n\t\tPaint.DrawRect( LocalRect );\r\n\r\n\t\tPaint.SetPen( PrismTheme.BorderSubtle, 1f );\r\n\t\tPaint.DrawLine( new Vector2( 0f, LocalRect.Bottom - 0.5f ), new Vector2( LocalRect.Right, LocalRect.Bottom - 0.5f ) );\r\n\r\n\t\tPaint.SetDefaultFont( PrismTheme.BodySize, 400, false, true );\r\n\r\n\t\tvar x = 4f - _scroll;\r\n\r\n\t\tfor ( var i = 0; i \u003C _tabs.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar tab = _tabs[i];\r\n\t\t\tvar caption = tab.DisplayTitle;\r\n\t\t\tvar width = Math.Clamp( Paint.MeasureText( caption ).x \u002B 46f, 90f, 240f );\r\n\r\n\t\t\tvar rect = new Rect( x, 3f, width, LocalRect.Height - 3f );\r\n\t\t\ttab.TabRect = rect;\r\n\t\t\tx \u002B= width \u002B 2f;\r\n\r\n\t\t\tif ( rect.Right \u003C 0f || rect.Left \u003E LocalRect.Right )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar isActive = ReferenceEquals( tab, _active );\r\n\t\t\tvar isHovered = ReferenceEquals( tab, _hovered );\r\n\r\n\t\t\tPaint.ClearPen();\r\n\t\t\tPaint.SetBrush( isActive ? PrismTheme.Code.Background : isHovered ? PrismTheme.PanelAlt : PrismTheme.Panel );\r\n\t\t\tPaint.DrawRect( rect, PrismTheme.RadiusChip );\r\n\r\n\t\t\tif ( isActive )\r\n\t\t\t{\r\n\t\t\t\tPaint.SetBrush( PrismTheme.Accent );\r\n\t\t\t\tPaint.DrawRect( new Rect( rect.Left, rect.Top, rect.Width, 2f ), 1f );\r\n\t\t\t}\r\n\r\n\t\t\tPaint.SetPen( isActive ? PrismTheme.TextPrimary : PrismTheme.TextSecondary );\r\n\t\t\tPaint.DrawText( new Rect( rect.Left \u002B 10f, rect.Top, rect.Width - 34f, rect.Height ),\r\n\t\t\t\tcaption, TextFlag.LeftCenter | TextFlag.SingleLine );\r\n\r\n\t\t\tvar closeRect = CloseRect( rect );\r\n\r\n\t\t\tPaint.SetPen( isHovered \u0026\u0026 _hoverClose ? PrismTheme.Error : PrismTheme.TextMuted );\r\n\t\t\tPaint.DrawIcon( closeRect, \u0022close\u0022, 12f, TextFlag.Center );\r\n\t\t}\r\n\r\n\t\tvar plus = new Rect( x \u002B 4f, 4f, 22f, LocalRect.Height - 8f );\r\n\r\n\t\tPaint.SetPen( PrismTheme.TextMuted );\r\n\t\tPaint.DrawIcon( plus, \u0022add\u0022, 14f, TextFlag.Center );\r\n\t}\r\n\r\n\tstatic Rect CloseRect( Rect tabRect ) =\u003E new( tabRect.Right - 24f, tabRect.Top \u002B 4f, 18f, tabRect.Height - 8f );\r\n\r\n\tprotected override void OnMouseMove( MouseEvent e )\r\n\t{\r\n\t\tvar previous = _hovered;\r\n\t\tvar previousClose = _hoverClose;\r\n\r\n\t\t_hovered = HitTest( e.LocalPosition, out _hoverClose );\r\n\r\n\t\tif ( !ReferenceEquals( previous, _hovered ) || previousClose != _hoverClose )\r\n\t\t\tUpdate();\r\n\t}\r\n\r\n\tprotected override void OnMouseLeave()\r\n\t{\r\n\t\t_hovered = null;\r\n\t\t_hoverClose = false;\r\n\t\tUpdate();\r\n\t}\r\n\r\n\tprotected override void OnMousePress( MouseEvent e )\r\n\t{\r\n\t\tvar tab = HitTest( e.LocalPosition, out var onClose );\r\n\r\n\t\tif ( tab is null )\r\n\t\t{\r\n\t\t\tif ( e.LeftMouseButton \u0026\u0026 e.LocalPosition.x \u003E LastTabRight() )\r\n\t\t\t\tNewTabRequested?.Invoke();\r\n\r\n\t\t\te.Accepted = true;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( e.MiddleMouseButton || (e.LeftMouseButton \u0026\u0026 onClose) )\r\n\t\t\tTabClosed?.Invoke( tab );\r\n\t\telse if ( e.LeftMouseButton )\r\n\t\t\tTabSelected?.Invoke( tab );\r\n\r\n\t\te.Accepted = true;\r\n\t}\r\n\r\n\tprotected override void OnMouseWheel( WheelEvent e )\r\n\t{\r\n\t\t_scroll = Math.Max( 0f, _scroll \u002B (e.Delta \u003E 0 ? -40f : 40f) );\r\n\t\tUpdate();\r\n\t\te.Accept();\r\n\t}\r\n\r\n\tfloat LastTabRight() =\u003E _tabs.Count == 0 ? 4f : _tabs[^1].TabRect.Right;\r\n\r\n\tCodeTab HitTest( Vector2 local, out bool onClose )\r\n\t{\r\n\t\tonClose = false;\r\n\r\n\t\tfor ( var i = 0; i \u003C _tabs.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( !_tabs[i].TabRect.IsInside( local ) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tonClose = CloseRect( _tabs[i].TabRect ).IsInside( local );\r\n\t\t\treturn _tabs[i];\r\n\t\t}\r\n\r\n\t\treturn null;\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// A deliberately small structural scanner that feeds the outline dock: VFX block headers, macros,\r\n/// structs, constant buffers and top-level function definitions. It is a placeholder for the richer\r\n/// document-symbol parser the language-intelligence package owns; swapping it out only changes this\r\n/// file.\r\n/// \u003C/summary\u003E\r\ninternal static class CodeOutline\r\n{\r\n\tstatic readonly string[] s_blocks =\r\n\t{\r\n\t\t\u0022HEADER\u0022, \u0022MODES\u0022, \u0022FEATURES\u0022, \u0022COMMON\u0022, \u0022VS\u0022, \u0022PS\u0022, \u0022GS\u0022, \u0022CS\u0022, \u0022PS_RENDER_STATE\u0022, \u0022RTX\u0022\r\n\t};\r\n\r\n\tstatic readonly HashSet\u003Cstring\u003E s_notFunctions = new( StringComparer.Ordinal )\r\n\t{\r\n\t\t\u0022if\u0022, \u0022for\u0022, \u0022while\u0022, \u0022switch\u0022, \u0022return\u0022, \u0022else\u0022, \u0022do\u0022, \u0022case\u0022, \u0022sizeof\u0022, \u0022defined\u0022\r\n\t};\r\n\r\n\tpublic static List\u003CCodeSymbol\u003E Scan( TextDocument document )\r\n\t{\r\n\t\tvar symbols = new List\u003CCodeSymbol\u003E();\r\n\r\n\t\tif ( document is null )\r\n\t\t\treturn symbols;\r\n\r\n\t\tvar depth = 0;\r\n\r\n\t\tfor ( var line = 0; line \u003C document.LineCount; line\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar raw = document.GetLine( line );\r\n\t\t\tvar text = raw.Trim();\r\n\t\t\tvar startDepth = depth;\r\n\r\n\t\t\tdepth \u002B= CountUnquoted( raw, \u0027{\u0027 ) - CountUnquoted( raw, \u0027}\u0027 );\r\n\r\n\t\t\tif ( text.Length == 0 || text.StartsWith( \u0022//\u0022, StringComparison.Ordinal ) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif ( startDepth == 0 \u0026\u0026 TryBlock( text, out var block ) )\r\n\t\t\t{\r\n\t\t\t\tsymbols.Add( new CodeSymbol( block, \u0022block\u0022, line, \u0022widgets\u0022, 0 ) );\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( text.StartsWith( \u0022#define \u0022, StringComparison.Ordinal ) )\r\n\t\t\t{\r\n\t\t\t\tvar name = ReadIdentifier( text, 8 );\r\n\r\n\t\t\t\tif ( !string.IsNullOrEmpty( name ) )\r\n\t\t\t\t\tsymbols.Add( new CodeSymbol( name, \u0022define\u0022, line, \u0022tag\u0022, startDepth \u003E 0 ? 1 : 0 ) );\r\n\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( text.StartsWith( \u0022struct \u0022, StringComparison.Ordinal ) )\r\n\t\t\t{\r\n\t\t\t\tvar name = ReadIdentifier( text, 7 );\r\n\r\n\t\t\t\tif ( !string.IsNullOrEmpty( name ) )\r\n\t\t\t\t\tsymbols.Add( new CodeSymbol( name, \u0022struct\u0022, line, \u0022data_object\u0022, startDepth \u003E 0 ? 1 : 0 ) );\r\n\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( text.StartsWith( \u0022cbuffer \u0022, StringComparison.Ordinal ) )\r\n\t\t\t{\r\n\t\t\t\tvar name = ReadIdentifier( text, 8 );\r\n\r\n\t\t\t\tif ( !string.IsNullOrEmpty( name ) )\r\n\t\t\t\t\tsymbols.Add( new CodeSymbol( name, \u0022cbuffer\u0022, line, \u0022view_list\u0022, startDepth \u003E 0 ? 1 : 0 ) );\r\n\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( startDepth \u003E 1 )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif ( TryFunction( text, out var function, out var signature ) )\r\n\t\t\t\tsymbols.Add( new CodeSymbol( function, signature, line, \u0022functions\u0022, startDepth \u003E 0 ? 1 : 0 ) );\r\n\t\t}\r\n\r\n\t\treturn symbols;\r\n\t}\r\n\r\n\tstatic bool TryBlock( string text, out string block )\r\n\t{\r\n\t\tblock = null;\r\n\r\n\t\tvar candidate = text.EndsWith( \u0022{\u0022, StringComparison.Ordinal ) ? text[..^1].Trim() : text;\r\n\r\n\t\tfor ( var i = 0; i \u003C s_blocks.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( !string.Equals( candidate, s_blocks[i], StringComparison.Ordinal ) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tblock = s_blocks[i];\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\tstatic bool TryFunction( string text, out string name, out string signature )\r\n\t{\r\n\t\tname = null;\r\n\t\tsignature = null;\r\n\r\n\t\tif ( text.StartsWith( \u0022#\u0022, StringComparison.Ordinal ) )\r\n\t\t\treturn false;\r\n\r\n\t\tvar open = text.IndexOf( \u0027(\u0027 );\r\n\r\n\t\tif ( open \u003C= 0 )\r\n\t\t\treturn false;\r\n\r\n\t\tif ( text.EndsWith( \u0022;\u0022, StringComparison.Ordinal ) )\r\n\t\t\treturn false;\r\n\r\n\t\tvar head = text[..open].Trim();\r\n\r\n\t\tif ( head.Length == 0 )\r\n\t\t\treturn false;\r\n\r\n\t\tvar lastSpace = head.LastIndexOfAny( new[] { \u0027 \u0027, \u0027\\t\u0027, \u0027*\u0027, \u0027\u0026\u0027, \u0027:\u0027 } );\r\n\r\n\t\tif ( lastSpace \u003C= 0 || lastSpace \u003E= head.Length - 1 )\r\n\t\t\treturn false;\r\n\r\n\t\tvar candidate = head[(lastSpace \u002B 1)..].Trim();\r\n\r\n\t\tif ( candidate.Length == 0 || s_notFunctions.Contains( candidate ) )\r\n\t\t\treturn false;\r\n\r\n\t\tfor ( var i = 0; i \u003C candidate.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( !char.IsLetterOrDigit( candidate[i] ) \u0026\u0026 candidate[i] != \u0027_\u0027 )\r\n\t\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tname = candidate;\r\n\t\tsignature = head[..lastSpace].Trim();\r\n\t\treturn true;\r\n\t}\r\n\r\n\tstatic string ReadIdentifier( string text, int start )\r\n\t{\r\n\t\tvar index = start;\r\n\r\n\t\twhile ( index \u003C text.Length \u0026\u0026 char.IsWhiteSpace( text[index] ) )\r\n\t\t\tindex\u002B\u002B;\r\n\r\n\t\tvar builder = new StringBuilder();\r\n\r\n\t\twhile ( index \u003C text.Length \u0026\u0026 (char.IsLetterOrDigit( text[index] ) || text[index] == \u0027_\u0027) )\r\n\t\t\tbuilder.Append( text[index\u002B\u002B] );\r\n\r\n\t\treturn builder.ToString();\r\n\t}\r\n\r\n\tstatic int CountUnquoted( string text, char target )\r\n\t{\r\n\t\tvar count = 0;\r\n\t\tvar inString = false;\r\n\r\n\t\tfor ( var i = 0; i \u003C text.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar c = text[i];\r\n\r\n\t\t\tif ( c == \u0027\u0022\u0027 \u0026\u0026 (i == 0 || text[i - 1] != \u0027\\\\\u0027) )\r\n\t\t\t{\r\n\t\t\t\tinString = !inString;\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( inString )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif ( c == \u0027/\u0027 \u0026\u0026 i \u002B 1 \u003C text.Length \u0026\u0026 text[i \u002B 1] == \u0027/\u0027 )\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tif ( c == target )\r\n\t\t\t\tcount\u002B\u002B;\r\n\t\t}\r\n\r\n\t\treturn count;\r\n\t}\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/Text/Diagnostics/TextDiagnosticService.cs","FileName":"TextDiagnosticService.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using Editor.Prism.Core;\r\nusing Editor.Prism.Text.Completion;\r\nusing Editor.Prism.Text.Lexer;\r\nusing Editor.Prism.Text.LanguageDb;\r\nusing Editor.Prism.Toolchain;\r\nusing Sandbox.Engine.Shaders;\r\nusing PrismDiagnostic = Editor.Prism.Core.Diagnostic;\r\n\r\nnamespace Editor.Prism.Text.Diagnostics;\r\n\r\n/// \u003Csummary\u003E\r\n/// Diagnostic codes produced by the text editor\u0027s own analysis, as opposed to the graph pipeline\u0027s.\r\n/// \u003Cpara\u003E\r\n/// \u003Cb\u003EThese are now forwarders.\u003C/b\u003E The \u003Cc\u003EPR6xxx\u003C/c\u003E range was folded into\r\n/// \u003Csee cref=\u0022Core.DiagnosticCode\u0022/\u003E so there is one table of codes rather than two lists of identical\r\n/// string literals that could drift apart. Every member below is defined as the corresponding\r\n/// \u003Cc\u003EDiagnosticCode\u003C/c\u003E constant, so the two agree by construction and not by coincidence. New\r\n/// text-tier codes go in \u003Cc\u003EDiagnosticCode\u003C/c\u003E; this type stays for the callers that already name it.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\npublic static class TextDiagnosticCode\r\n{\r\n\t/// \u003Csummary\u003EA call to something nothing in scope declares.\u003C/summary\u003E\r\n\tpublic const string UnknownIdentifier = DiagnosticCode.UnknownIdentifier;\r\n\r\n\t/// \u003Csummary\u003EA Direct3D 9 sampler intrinsic DXC removed.\u003C/summary\u003E\r\n\tpublic const string DeprecatedIntrinsic = DiagnosticCode.DeprecatedIntrinsic;\r\n\r\n\t/// \u003Csummary\u003EBraces, parentheses or brackets do not balance.\u003C/summary\u003E\r\n\tpublic const string Unbalanced = DiagnosticCode.Unbalanced;\r\n\r\n\t/// \u003Csummary\u003EA string literal or block comment is never closed.\u003C/summary\u003E\r\n\tpublic const string Unterminated = DiagnosticCode.Unterminated;\r\n\r\n\t/// \u003Csummary\u003EA \u003Cc\u003E#\u003C/c\u003E directive the preprocessor does not know.\u003C/summary\u003E\r\n\tpublic const string UnknownDirective = DiagnosticCode.UnknownDirective;\r\n\r\n\t/// \u003Csummary\u003EAn \u003Cc\u003E#include\u003C/c\u003E that resolves to no file on any search path.\u003C/summary\u003E\r\n\tpublic const string MissingInclude = DiagnosticCode.MissingInclude;\r\n\r\n\t/// \u003Csummary\u003EAn \u003Cc\u003E#include \u0026lt;\u2026\u0026gt;\u003C/c\u003E, which the engine\u0027s preprocessor never expands.\u003C/summary\u003E\r\n\tpublic const string AngleBracketInclude = DiagnosticCode.AngleBracketInclude;\r\n\r\n\t/// \u003Csummary\u003EAn \u003Cc\u003E#include\u003C/c\u003E whose spacing the engine\u0027s regex does not match.\u003C/summary\u003E\r\n\tpublic const string IncludeSpacing = DiagnosticCode.IncludeSpacing;\r\n\r\n\t/// \u003Csummary\u003EA VFX block the engine\u0027s \u003Cc\u003E.shader\u003C/c\u003E parser throws on.\u003C/summary\u003E\r\n\tpublic const string RejectedBlock = DiagnosticCode.RejectedBlock;\r\n\r\n\t/// \u003Csummary\u003EA declaration that shadows an engine global.\u003C/summary\u003E\r\n\tpublic const string ShadowedGlobal = DiagnosticCode.ShadowedGlobal;\r\n\r\n\t/// \u003Csummary\u003EThe Slang toolchain is absent, so a \u003Cc\u003E.slang\u003C/c\u003E buffer only gets local checks.\u003C/summary\u003E\r\n\tpublic const string SlangNotValidated = DiagnosticCode.SlangNotValidated;\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Runs the right validator for a buffer and hands back diagnostics, debounced and cancellable.\r\n/// \u003Cpara\u003E\r\n/// Three tiers, in the order they arrive. Local checks are instant, in-process and always on: unknown\r\n/// calls, DX9 intrinsics DXC removed, intrinsics above Shader Model 6.0, unbalanced brackets, unknown\r\n/// directives and unresolvable includes. They land on the editor before the user has stopped typing.\r\n/// Then the authoritative tier: a \u003Cc\u003E.shader\u003C/c\u003E is compiled for real by the engine, and a bare\r\n/// \u003Cc\u003E.hlsl\u003C/c\u003E is wrapped in the smallest legal shader that will hold it and compiled the same way \u2014\r\n/// which is how this editor gets real compiler errors for an include file, something nothing else in\r\n/// s\u0026amp;box does. A \u003Cc\u003E.slang\u003C/c\u003E goes to \u003Cc\u003Eslangc\u003C/c\u003E when the user installed one, and quietly does\r\n/// without when they did not.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\npublic sealed class TextDiagnosticService : IDisposable\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Ceiling on unknown-identifier warnings in one file. Past this the file is not wrong, it is\r\n\t/// \u003Ci\u003Eincomplete\u003C/i\u003E, and the whole set is dropped \u2014 see \u003Csee cref=\u0022MaxDistinctUnknownIdentifiers\u0022/\u003E.\r\n\t/// \u003C/summary\u003E\r\n\tconst int MaxUnknownIdentifiers = 8;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Ceiling on \u003Ci\u003Edistinct\u003C/i\u003E unknown names in one file.\r\n\t/// \u003Cpara\u003E\r\n\t/// The check exists to catch an isolated mistake \u2014 a misspelled intrinsic, a helper that was renamed.\r\n\t/// Once five different names in one buffer are unresolved, the far likelier explanation is that the\r\n\t/// buffer is an include fragment whose scope its callers supply. The engine\u0027s own headers do exactly\r\n\t/// this: \u003Cc\u003Effx_fsr1.h\u003C/c\u003E calls \u003Cc\u003EARcpF1\u003C/c\u003E fourteen times and deliberately does not include\r\n\t/// \u003Cc\u003Effx_a.h\u003C/c\u003E, and every \u003Cc\u003Effx_denoiser_reflections_*.h\u003C/c\u003E calls twenty callbacks the including\r\n\t/// shader is required to define. Reporting those as mistakes is simply wrong, so nothing is reported.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tconst int MaxDistinctUnknownIdentifiers = 4;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Ceiling on how often one unknown name may appear before the file is treated as incomplete. Nobody\r\n\t/// misspells the same identifier three times; a missing header goes wrong on every use.\r\n\t/// \u003C/summary\u003E\r\n\tconst int MaxUsesOfOneUnknown = 2;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// How deep the include graph is walked when harvesting the names a buffer can see. Deeper than\r\n\t/// \u003Csee cref=\u0022CompletenessDepth\u0022/\u003E on purpose: every extra name found can only remove a false\r\n\t/// positive, never create one.\r\n\t/// \u003C/summary\u003E\r\n\tconst int IncludeDepth = 4;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// How deep the \u0022did every include resolve?\u0022 test looks. Kept shallow deliberately: engine header\r\n\t/// trees fan out into the fourteen compiler-embedded includes within three or four hops, and a\r\n\t/// stricter test would simply stop checking two thirds of the shipped shaders.\r\n\t/// \u003C/summary\u003E\r\n\tconst int CompletenessDepth = 2;\r\n\r\n\t/// \u003Csummary\u003ECeiling on files walked while harvesting, so a pathological graph cannot stall a check.\u003C/summary\u003E\r\n\tconst int IncludeFiles = 96;\r\n\r\n\tstatic bool s_collected;\r\n\r\n\treadonly object _lock = new();\r\n\r\n\tCancellationTokenSource _inFlight;\r\n\tTempWorkspace _workspace;\r\n\tint _generation;\r\n\tbool _disposed;\r\n\r\n\tCodeEditorWidget _editor;\r\n\tAction\u003CCodeEditorWidget\u003E _settled;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Creates a service with its own scratch folder. The folder is per instance rather than per\r\n\t/// session because two tabs editing files with the same name would otherwise compile over each\r\n\t/// other, and the output path the engine picks is derived from the file name.\r\n\t/// \u003C/summary\u003E\r\n\tpublic TextDiagnosticService( string sessionId = null )\r\n\t{\r\n\t\tSessionId = string.IsNullOrWhiteSpace( sessionId )\r\n\t\t\t? $\u0022text-{Ids.NewShortId()}\u0022\r\n\t\t\t: sessionId;\r\n\r\n\t\t// A crashed editor leaves its scratch folders behind and nobody comes back for them. Once per\r\n\t\t// process is enough; every tab does not need to rescan the directory.\r\n\t\tif ( !s_collected )\r\n\t\t{\r\n\t\t\ts_collected = true;\r\n\r\n\t\t\tPrismLog.Guard( \u0022Prism.Text: collect stale scratch sessions\u0022,\r\n\t\t\t\t() =\u003E TempWorkspace.CollectGarbage( PrismConstants.TempSessionLifetimeHours, SessionId ) );\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Wires a service to an editor: validates when typing settles, pushes the result onto the editor\u0027s\r\n\t/// squiggles, and validates once immediately so a freshly opened file is not silently unchecked.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static TextDiagnosticService Attach( CodeEditorWidget editor, string filePath = null )\r\n\t{\r\n\t\tif ( editor is not { IsValid: true } )\r\n\t\t\treturn null;\r\n\r\n\t\tvar service = new TextDiagnosticService\r\n\t\t{\r\n\t\t\tFilePath = filePath ?? editor.Document?.FilePath,\r\n\t\t\t_editor = editor\r\n\t\t};\r\n\r\n\t\tservice._settled = _ =\u003E service.RequestFor( editor );\r\n\t\teditor.TextSettled \u002B= service._settled;\r\n\r\n\t\tservice.Completed \u002B= diagnostics =\u003E\r\n\t\t{\r\n\t\t\tif ( editor is { IsValid: true } )\r\n\t\t\t\teditor.SetDiagnostics( diagnostics );\r\n\t\t};\r\n\r\n\t\tservice.RequestFor( editor );\r\n\r\n\t\treturn service;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe scratch session this service compiles through.\u003C/summary\u003E\r\n\tpublic string SessionId { get; }\r\n\r\n\t/// \u003Csummary\u003EPath of the buffer being validated. Keep it in step with Save As.\u003C/summary\u003E\r\n\tpublic string FilePath { get; set; }\r\n\r\n\t/// \u003Csummary\u003EWhether the authoritative compiler tier runs at all. Local checks always do.\u003C/summary\u003E\r\n\tpublic bool UseCompiler { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003EWhether unknown calls are reported. On by default; off for buffers full of generated macros.\u003C/summary\u003E\r\n\tpublic bool ReportUnknownIdentifiers { get; set; } = true;\r\n\r\n\t/// \u003Csummary\u003EHow long after the last keystroke a validation starts.\u003C/summary\u003E\r\n\tpublic int DebounceMs { get; set; } = PrismConstants.TextDebounceMs;\r\n\r\n\t/// \u003Csummary\u003ETrue while a validation is running.\u003C/summary\u003E\r\n\tpublic bool IsRunning { get; private set; }\r\n\r\n\t/// \u003Csummary\u003EThe most recent result. Never null.\u003C/summary\u003E\r\n\tpublic IReadOnlyList\u003CPrismDiagnostic\u003E Last { get; private set; } = Array.Empty\u003CPrismDiagnostic\u003E();\r\n\r\n\t/// \u003Csummary\u003ERaised on the main thread when a validation starts.\u003C/summary\u003E\r\n\tpublic event Action Started;\r\n\r\n\t/// \u003Csummary\u003ERaised on the main thread with every completed result, in order.\u003C/summary\u003E\r\n\tpublic event Action\u003CIReadOnlyList\u003CPrismDiagnostic\u003E\u003E Completed;\r\n\r\n\t// ---- driving ----------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003EValidates an editor\u0027s buffer. Safe to call on every keystroke.\u003C/summary\u003E\r\n\tpublic void RequestFor( CodeEditorWidget editor )\r\n\t{\r\n\t\tif ( editor is not { IsValid: true } || editor.Document is null )\r\n\t\t\treturn;\r\n\r\n\t\tRequest( editor.Document.Text, FilePath ?? editor.Document.FilePath, editor.Language );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Validates a buffer after the debounce, cancelling whatever was already running. The local checks\r\n\t/// are published as soon as they are done so the editor never waits on the compiler to show an\r\n\t/// obviously broken line.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void Request( string text, string filePath, string language )\r\n\t{\r\n\t\tif ( _disposed )\r\n\t\t\treturn;\r\n\r\n\t\tvar generation = Interlocked.Increment( ref _generation );\r\n\r\n\t\t// Resolving includes enumerates mounted projects, which is editor state. Do it here, on the\r\n\t\t// thread the caller is on, rather than from the worker below.\r\n\t\tPrismLog.Guard( \u0022Prism.Text: warm include roots\u0022, IncludeResolver.Warm );\r\n\r\n\t\tCancellationTokenSource cancellation;\r\n\r\n\t\tlock ( _lock )\r\n\t\t{\r\n\t\t\t_inFlight?.Cancel();\r\n\t\t\t_inFlight?.Dispose();\r\n\t\t\t_inFlight = new CancellationTokenSource();\r\n\t\t\tcancellation = _inFlight;\r\n\t\t}\r\n\r\n\t\tvar token = cancellation.Token;\r\n\r\n\t\t_ = Task.Run( async () =\u003E\r\n\t\t{\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tawait Task.Delay( Math.Max( 0, DebounceMs ), token ).ConfigureAwait( false );\r\n\r\n\t\t\t\tif ( generation != Volatile.Read( ref _generation ) )\r\n\t\t\t\t\treturn;\r\n\r\n\t\t\t\tMainThread.Queue( () =\u003E\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( generation != Volatile.Read( ref _generation ) )\r\n\t\t\t\t\t\treturn;\r\n\r\n\t\t\t\t\tIsRunning = true;\r\n\t\t\t\t\tPrismLog.Guard( \u0022Prism.Text: diagnostics started\u0022, () =\u003E Started?.Invoke() );\r\n\t\t\t\t} );\r\n\r\n\t\t\t\tvar definition = LanguageDefinition.For( language );\r\n\t\t\t\tvar local = LocalChecks( text, filePath, definition, ReportUnknownIdentifiers );\r\n\r\n\t\t\t\tPublish( generation, local );\r\n\r\n\t\t\t\tif ( !UseCompiler )\r\n\t\t\t\t{\r\n\t\t\t\t\tFinish( generation );\r\n\t\t\t\t\treturn;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tvar deep = await Compile( text, filePath, definition, token ).ConfigureAwait( false );\r\n\r\n\t\t\t\tif ( token.IsCancellationRequested )\r\n\t\t\t\t\treturn;\r\n\r\n\t\t\t\tvar all = new List\u003CPrismDiagnostic\u003E( local );\r\n\r\n\t\t\t\tall.AddRange( deep );\r\n\r\n\t\t\t\tPublish( generation, all );\r\n\t\t\t\tFinish( generation );\r\n\t\t\t}\r\n\t\t\tcatch ( OperationCanceledException )\r\n\t\t\t{\r\n\t\t\t\t// Superseded by a newer request; the newer one publishes.\r\n\t\t\t}\r\n\t\t\tcatch ( Exception e )\r\n\t\t\t{\r\n\t\t\t\tPrismLog.Error( e, \u0022Prism.Text: validation failed\u0022 );\r\n\t\t\t\tFinish( generation );\r\n\t\t\t}\r\n\t\t} );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ERuns a validation right now, with no debounce, and hands back the result.\u003C/summary\u003E\r\n\tpublic async Task\u003CIReadOnlyList\u003CPrismDiagnostic\u003E\u003E Validate( string text, string filePath, string language,\r\n\t\tCancellationToken ct )\r\n\t{\r\n\t\tvar definition = LanguageDefinition.For( language );\r\n\t\tvar results = new List\u003CPrismDiagnostic\u003E( LocalChecks( text, filePath, definition, ReportUnknownIdentifiers ) );\r\n\r\n\t\tif ( UseCompiler )\r\n\t\t\tresults.AddRange( await Compile( text, filePath, definition, ct ).ConfigureAwait( false ) );\r\n\r\n\t\treturn results;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ECancels whatever is running and leaves the last published result in place.\u003C/summary\u003E\r\n\tpublic void Cancel()\r\n\t{\r\n\t\tInterlocked.Increment( ref _generation );\r\n\r\n\t\tlock ( _lock )\r\n\t\t{\r\n\t\t\t_inFlight?.Cancel();\r\n\t\t}\r\n\t}\r\n\r\n\tvoid Publish( int generation, IReadOnlyList\u003CPrismDiagnostic\u003E diagnostics )\r\n\t{\r\n\t\tMainThread.Queue( () =\u003E\r\n\t\t{\r\n\t\t\tif ( _disposed || generation != Volatile.Read( ref _generation ) )\r\n\t\t\t\treturn;\r\n\r\n\t\t\tLast = diagnostics;\r\n\r\n\t\t\tPrismLog.Guard( \u0022Prism.Text: diagnostics published\u0022, () =\u003E Completed?.Invoke( diagnostics ) );\r\n\t\t} );\r\n\t}\r\n\r\n\tvoid Finish( int generation )\r\n\t{\r\n\t\tMainThread.Queue( () =\u003E\r\n\t\t{\r\n\t\t\tif ( generation != Volatile.Read( ref _generation ) )\r\n\t\t\t\treturn;\r\n\r\n\t\t\tIsRunning = false;\r\n\t\t} );\r\n\t}\r\n\r\n\t// ---- local checks -----------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Everything that can be decided without a compiler, in a single lexer pass. Fast enough to run on\r\n\t/// a keystroke and precise enough that the squiggle lands on the right token.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003CPrismDiagnostic\u003E LocalChecks( string text, string filePath,\r\n\t\tLanguageDefinition language, bool reportUnknownIdentifiers = true )\r\n\t{\r\n\t\tvar results = new List\u003CPrismDiagnostic\u003E();\r\n\r\n\t\tif ( string.IsNullOrEmpty( text ) )\r\n\t\t\treturn results;\r\n\r\n\t\tlanguage ??= LanguageDefinition.Hlsl;\r\n\r\n\t\tPrismLog.Guard( \u0022Prism.Text: local checks\u0022,\r\n\t\t\t() =\u003E RunLocalChecks( text, filePath, language, reportUnknownIdentifiers, results ) );\r\n\r\n\t\treturn results;\r\n\t}\r\n\r\n\tstatic void RunLocalChecks( string text, string filePath, LanguageDefinition language,\r\n\t\tbool reportUnknownIdentifiers, List\u003CPrismDiagnostic\u003E results )\r\n\t{\r\n\t\tvar file = filePath ?? string.Empty;\r\n\t\tvar lines = text.Replace( \u0022\\r\\n\u0022, \u0022\\n\u0022 ).Replace( \u0027\\r\u0027, \u0027\\n\u0027 ).Split( \u0027\\n\u0027 );\r\n\t\tvar lexer = Lexers.For( language.Id );\r\n\t\tvar state = LexState.Default;\r\n\t\tvar tokens = new List\u003CToken\u003E( 64 );\r\n\r\n\t\tvar symbols = DocumentSymbols.Parse( text, language.Id, filePath );\r\n\t\tvar declared = new HashSet\u003Cstring\u003E( StringComparer.Ordinal );\r\n\r\n\t\t// A name declared anywhere in the buffer counts, wherever the caret is and whichever branch of\r\n\t\t// the preprocessor it sits in: this pass answers \u0022does anything declare it\u0022, not \u0022is it in scope\r\n\t\t// on line N\u0022, and a forward reference to a function defined lower down is perfectly normal.\r\n\t\tforeach ( var symbol in symbols.All )\r\n\t\t\tdeclared.Add( symbol.Name );\r\n\r\n\t\tvar includedMacros = new HashSet\u003Cstring\u003E( StringComparer.Ordinal );\r\n\r\n\t\t// The same walk the completeness test does, so a name declared in a header we did read can never\r\n\t\t// be reported as unknown just because the harvest stopped one level shallower than the test.\r\n\t\tforeach ( var path in IncludeResolver.Transitive( text, filePath, IncludeDepth, IncludeFiles ) )\r\n\t\t{\r\n\t\t\tforeach ( var symbol in DocumentSymbols.ForFile( path, language.Id ).All )\r\n\t\t\t{\r\n\t\t\t\tdeclared.Add( symbol.Name );\r\n\r\n\t\t\t\tif ( symbol.Kind == DocumentSymbolKind.Macro )\r\n\t\t\t\t\tincludedMacros.Add( symbol.Name );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// If any include could not be read \u2014 because it is missing, or because it is one of the\r\n\t\t// fourteen that live inside the compiler and have no file at all \u2014 then we genuinely do not\r\n\t\t// know what is in scope, and every \u0022undeclared\u0022 warning would be a guess. Almost every real\r\n\t\t// s\u0026box shader reaches a compiler-embedded header eventually, so this is the common case, and\r\n\t\t// staying quiet is the only honest thing to do. The real compile still catches everything.\r\n\t\tif ( reportUnknownIdentifiers \u0026\u0026 !IncludeResolver.IsGraphComplete( text, filePath, CompletenessDepth, IncludeFiles ) )\r\n\t\t\treportUnknownIdentifiers = false;\r\n\r\n\t\tvar conditionals = reportUnknownIdentifiers ? new PreprocessorRegions( symbols, includedMacros ) : null;\r\n\t\tvar braces = new Stack\u003C(char Kind, int Line, int Column)\u003E();\r\n\t\tvar unknown = reportUnknownIdentifiers ? new List\u003CPrismDiagnostic\u003E() : null;\r\n\t\tvar unknownNames = reportUnknownIdentifiers ? new Dictionary\u003Cstring, int\u003E( StringComparer.Ordinal ) : null;\r\n\r\n\t\tfor ( var line = 0; line \u003C lines.Length; line\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar content = lines[line];\r\n\t\t\tvar continued = ( state.Flags \u0026 LexFlags.PreprocessorContinuation ) != 0;\r\n\r\n\t\t\ttokens.Clear();\r\n\t\t\tstate = lexer.Lex( content, state, tokens );\r\n\r\n\t\t\tif ( conditionals is not null \u0026\u0026 !continued )\r\n\t\t\t\tconditionals.Feed( content, line );\r\n\r\n\t\t\t// A directive is its own little language: \u0060defined(X)\u0060 is not a call, and a macro body\u0027s\r\n\t\t\t// braces do not have to balance on the line they are written on.\r\n\t\t\tvar directive = continued || FirstKind( tokens ) == TokenKind.Preprocessor;\r\n\r\n\t\t\tfor ( var i = 0; i \u003C tokens.Count; i\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar token = tokens[i];\r\n\r\n\t\t\t\tif ( token.Start \u003C 0 || token.Length \u003C= 0 || token.Start \u002B token.Length \u003E content.Length )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tif ( directive \u0026\u0026 token.Kind != TokenKind.Preprocessor )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tvar word = content.Substring( token.Start, token.Length );\r\n\r\n\t\t\t\tswitch ( token.Kind )\r\n\t\t\t\t{\r\n\t\t\t\t\tcase TokenKind.String:\r\n\t\t\t\t\t\t// A literal that runs to the end of the line without a closing quote never ends.\r\n\t\t\t\t\t\tif ( token.Start \u002B token.Length == content.Length \u0026\u0026 token.Length \u003E= 1 \u0026\u0026\r\n\t\t\t\t\t\t\t ( token.Length == 1 || content[^1] != word[0] ) )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tresults.Add( PrismDiagnostic.AtSpan( DiagnosticSeverity.Error,\r\n\t\t\t\t\t\t\t\tTextDiagnosticCode.Unterminated, \u0022Unterminated string literal\u0022,\r\n\t\t\t\t\t\t\t\tSpan( file, line, token ) ) );\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t\tcase TokenKind.Comment:\r\n\t\t\t\t\tcase TokenKind.DocComment:\r\n\t\t\t\t\tcase TokenKind.Whitespace:\r\n\t\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t\tcase TokenKind.Punctuation:\r\n\t\t\t\t\t\tBalance( results, braces, word, file, line, token );\r\n\t\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t\tcase TokenKind.Preprocessor:\r\n\t\t\t\t\t\t// Only at the head of a line: \u0060##\u0060 inside a macro body lexes the same way.\r\n\t\t\t\t\t\tif ( IsFirstOnLine( tokens, i ) )\r\n\t\t\t\t\t\t\tCheckDirective( results, language, content, line, token, tokens, i, file );\r\n\r\n\t\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t\tcase TokenKind.IncludePath:\r\n\t\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t\tcase TokenKind.BlockKeyword:\r\n\t\t\t\t\t\tif ( SboxSymbols.RejectedBlockNames.Contains( word ) )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tresults.Add( PrismDiagnostic.AtSpan( DiagnosticSeverity.Error,\r\n\t\t\t\t\t\t\t\tTextDiagnosticCode.RejectedBlock,\r\n\t\t\t\t\t\t\t\t$\u0022s\u0026box cannot compile a {word} block\u0022,\r\n\t\t\t\t\t\t\t\tSpan( file, line, token ),\r\n\t\t\t\t\t\t\t\t$\u0022The engine\u0027s .shader parser throws \\\u0022{word} does nothing!\\\u0022 and the whole file \u0022 \u002B\r\n\t\t\t\t\t\t\t\t\u0022fails to load, with no diagnostic of its own.\u0022 ) );\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif ( token.Kind is not ( TokenKind.Identifier or TokenKind.Intrinsic or TokenKind.FunctionName ) )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t// A member access is resolved by the compiler, not by us.\r\n\t\t\t\tif ( PrecededByAccess( content, token.Start ) )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tif ( IntrinsicDb.TryGet( word, out var intrinsic ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( intrinsic.Deprecated )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tresults.Add( PrismDiagnostic.AtSpan( DiagnosticSeverity.Error,\r\n\t\t\t\t\t\t\tTextDiagnosticCode.DeprecatedIntrinsic,\r\n\t\t\t\t\t\t\t$\u0022\u0027{word}\u0027 was removed by Shader Model 6\u0022,\r\n\t\t\t\t\t\t\tSpan( file, line, token ), intrinsic.UnavailableReason ) );\r\n\r\n\t\t\t\t\t\tcontinue;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif ( intrinsic.NeedsHigherShaderModel )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tresults.Add( PrismDiagnostic.AtSpan( DiagnosticSeverity.Error,\r\n\t\t\t\t\t\t\tDiagnosticCode.ShaderModelTooHigh, intrinsic.UnavailableReason,\r\n\t\t\t\t\t\t\tSpan( file, line, token ) ) );\r\n\r\n\t\t\t\t\t\tcontinue;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif ( !reportUnknownIdentifiers )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t// Code the preprocessor may never reach cannot be judged: \u0060ffx_a.h\u0060 calls \u0060fract\u0060 and\r\n\t\t\t\t// \u0060mix\u0060 inside \u0060#ifdef A_GLSL\u0060, and whether that branch exists is decided by whoever\r\n\t\t\t\t// includes it. Only unconditional code, and code a condition we could actually evaluate\r\n\t\t\t\t// selected, is checked.\r\n\t\t\t\tif ( !conditionals.IsLive )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t// Only calls are reported. An unknown bare identifier is far more often a macro, a\r\n\t\t\t\t// combo or something a header we could not resolve declares than a real mistake.\r\n\t\t\t\tif ( !FollowedByCall( content, token.Start \u002B token.Length ) )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tif ( declared.Contains( word ) || language.IsKnownIdentifier( word ) ||\r\n\t\t\t\t\t SboxSymbols.IsComboSymbol( word ) || SboxSymbols.IsEngineGlobal( word ) ||\r\n\t\t\t\t\t SboxSymbols.IsModeFunction( word ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tunknownNames.TryGetValue( word, out var uses );\r\n\t\t\t\tunknownNames[word] = uses \u002B 1;\r\n\r\n\t\t\t\tunknown.Add( PrismDiagnostic.AtSpan( DiagnosticSeverity.Warning,\r\n\t\t\t\t\tTextDiagnosticCode.UnknownIdentifier,\r\n\t\t\t\t\t$\u0022Nothing in scope declares \u0027{word}\u0027\u0022,\r\n\t\t\t\t\tSpan( file, line, token ),\r\n\t\t\t\t\t\u0022It is not an intrinsic, an s\u0026box symbol, or declared in this file or any include \u0022 \u002B\r\n\t\t\t\t\t\u0022Prism could resolve. If it comes from a header, check the #include path.\u0022 ) );\r\n\r\n\t\t\t\t// Past either ceiling the verdict is already \u0022incomplete file\u0022, so stop collecting: a\r\n\t\t\t\t// generated header can otherwise pile up thousands of diagnostics nobody will ever see.\r\n\t\t\t\tif ( unknown.Count \u003E MaxUnknownIdentifiers || unknownNames.Count \u003E MaxDistinctUnknownIdentifiers )\r\n\t\t\t\t{\r\n\t\t\t\t\treportUnknownIdentifiers = false;\r\n\t\t\t\t\tunknown.Clear();\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( unknown is { Count: \u003E 0 } \u0026\u0026 !LooksLikeFragment( unknown.Count, unknownNames ) )\r\n\t\t\tresults.AddRange( unknown );\r\n\r\n\t\tif ( ( state.Flags \u0026 LexFlags.BlockComment ) != 0 )\r\n\t\t{\r\n\t\t\tresults.Add( PrismDiagnostic.AtSpan( DiagnosticSeverity.Error, TextDiagnosticCode.Unterminated,\r\n\t\t\t\t\u0022Unterminated block comment\u0022,\r\n\t\t\t\tSourceSpan.AtLine( file, Math.Max( 1, lines.Length ) ),\r\n\t\t\t\t\u0022Everything after the last /* is being treated as a comment.\u0022 ) );\r\n\t\t}\r\n\r\n\t\twhile ( braces.Count \u003E 0 )\r\n\t\t{\r\n\t\t\tvar (kind, line, column) = braces.Pop();\r\n\r\n\t\t\tresults.Add( PrismDiagnostic.AtSpan( DiagnosticSeverity.Error, TextDiagnosticCode.Unbalanced,\r\n\t\t\t\t$\u0022\u0027{kind}\u0027 is never closed\u0022,\r\n\t\t\t\tSourceSpan.At( file, line \u002B 1, column \u002B 1 ) ) );\r\n\t\t}\r\n\r\n\t\tresults.AddRange( IncludeResolver.Validate( text, filePath, language ) );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Whether the unknown names found in a buffer say \u0022this file has a mistake in it\u0022 or \u0022this file is\r\n\t/// half of a translation unit\u0022. See \u003Csee cref=\u0022MaxDistinctUnknownIdentifiers\u0022/\u003E for the reasoning.\r\n\t/// \u003C/summary\u003E\r\n\tstatic bool LooksLikeFragment( int total, Dictionary\u003Cstring, int\u003E names )\r\n\t{\r\n\t\tif ( total \u003E MaxUnknownIdentifiers || names.Count \u003E MaxDistinctUnknownIdentifiers )\r\n\t\t\treturn true;\r\n\r\n\t\tforeach ( var uses in names.Values )\r\n\t\t{\r\n\t\t\tif ( uses \u003E MaxUsesOfOneUnknown )\r\n\t\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A three-valued \u003Cc\u003E#if\u003C/c\u003E tracker: a region is \u003Cb\u003Elive\u003C/b\u003E, \u003Cb\u003Edead\u003C/b\u003E, or \u2014 the case that\r\n\t/// matters \u2014 \u003Cb\u003Eundecidable\u003C/b\u003E.\r\n\t/// \u003Cpara\u003E\r\n\t/// Only the conditions that can be settled from the buffer alone are evaluated: \u003Cc\u003E#if 0\u003C/c\u003E,\r\n\t/// \u003Cc\u003E#if 1\u003C/c\u003E, and \u003Cc\u003E#ifdef\u003C/c\u003E / \u003Cc\u003E#ifndef\u003C/c\u003E / \u003Cc\u003Edefined(X)\u003C/c\u003E where \u003Cc\u003EX\u003C/c\u003E is\r\n\t/// \u003Cc\u003E#define\u003C/c\u003Ed earlier in this file or in an include we read. The \u0022earlier\u0022 matters: an include\r\n\t/// guard defines its own symbol \u003Ci\u003Einside\u003C/i\u003E the \u003Cc\u003E#ifndef\u003C/c\u003E it opens, and treating that as\r\n\t/// already-defined would mark every file dead. Everything else \u2014 \u003Cc\u003E#ifdef A_GLSL\u003C/c\u003E,\r\n\t/// \u003Cc\u003E#if ( S_MODE == 2 )\u003C/c\u003E, any arithmetic \u2014 stays undecidable, because the symbol may be defined\r\n\t/// by the shader that includes this one or by the engine\u0027s own preprocessor.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tsealed class PreprocessorRegions\r\n\t{\r\n\t\tenum Branch { Live, Dead, Unknown }\r\n\r\n\t\treadonly Dictionary\u003Cstring, int\u003E _defined = new( StringComparer.Ordinal );\r\n\t\treadonly List\u003CBranch\u003E _stack = new();\r\n\r\n\t\tstring _guard;\r\n\t\tint _guardDepth;\r\n\r\n\t\tpublic PreprocessorRegions( DocumentSymbols symbols, HashSet\u003Cstring\u003E fromIncludes )\r\n\t\t{\r\n\t\t\tif ( symbols is not null )\r\n\t\t\t{\r\n\t\t\t\tforeach ( var symbol in symbols.All )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( symbol.Kind != DocumentSymbolKind.Macro )\r\n\t\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t\tif ( !_defined.TryGetValue( symbol.Name, out var first ) || symbol.Line \u003C first )\r\n\t\t\t\t\t\t_defined[symbol.Name] = symbol.Line;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\t// A macro from an include is in scope from the first line, so it gets a line number no\r\n\t\t\t// directive in this buffer can precede.\r\n\t\t\tif ( fromIncludes is null )\r\n\t\t\t\treturn;\r\n\r\n\t\t\tforeach ( var name in fromIncludes )\r\n\t\t\t\t_defined.TryAdd( name, int.MinValue );\r\n\t\t}\r\n\r\n\t\t/// \u003Csummary\u003ETrue when nothing on the conditional stack is dead or undecidable.\u003C/summary\u003E\r\n\t\tpublic bool IsLive\r\n\t\t{\r\n\t\t\tget\r\n\t\t\t{\r\n\t\t\t\tfor ( var i = 0; i \u003C _stack.Count; i\u002B\u002B )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( _stack[i] != Branch.Live )\r\n\t\t\t\t\t\treturn false;\r\n\t\t\t\t}\r\n\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t/// \u003Csummary\u003EFeeds one physical line, which is a no-op unless it opens or closes a region.\u003C/summary\u003E\r\n\t\tpublic void Feed( string line, int lineIndex )\r\n\t\t{\r\n\t\t\tvar i = 0;\r\n\r\n\t\t\twhile ( i \u003C line.Length \u0026\u0026 ( line[i] == \u0027 \u0027 || line[i] == \u0027\\t\u0027 ) )\r\n\t\t\t\ti\u002B\u002B;\r\n\r\n\t\t\tif ( i \u003E= line.Length || line[i] != \u0027#\u0027 )\r\n\t\t\t\treturn;\r\n\r\n\t\t\ti\u002B\u002B;\r\n\r\n\t\t\twhile ( i \u003C line.Length \u0026\u0026 ( line[i] == \u0027 \u0027 || line[i] == \u0027\\t\u0027 ) )\r\n\t\t\t\ti\u002B\u002B;\r\n\r\n\t\t\tvar nameStart = i;\r\n\r\n\t\t\twhile ( i \u003C line.Length \u0026\u0026 ( char.IsLetterOrDigit( line[i] ) || line[i] == \u0027_\u0027 ) )\r\n\t\t\t\ti\u002B\u002B;\r\n\r\n\t\t\tvar directive = line.Substring( nameStart, i - nameStart );\r\n\t\t\tvar rest = i \u003C line.Length ? line.Substring( i ) : string.Empty;\r\n\t\t\tvar guard = _guard;\r\n\r\n\t\t\t_guard = null;\r\n\r\n\t\t\tswitch ( directive )\r\n\t\t\t{\r\n\t\t\t\tcase \u0022if\u0022:\r\n\t\t\t\t\t_stack.Add( Evaluate( rest, lineIndex ) );\r\n\t\t\t\t\treturn;\r\n\r\n\t\t\t\tcase \u0022ifdef\u0022:\r\n\t\t\t\t\t_stack.Add( DefinedBefore( FirstWord( rest ), lineIndex ) ? Branch.Live : Branch.Unknown );\r\n\t\t\t\t\treturn;\r\n\r\n\t\t\t\tcase \u0022ifndef\u0022:\r\n\t\t\t\t\tvar undefined = FirstWord( rest );\r\n\r\n\t\t\t\t\t_stack.Add( DefinedBefore( undefined, lineIndex ) ? Branch.Dead : Branch.Unknown );\r\n\r\n\t\t\t\t\t// Remember it in case the next directive turns out to be its include guard.\r\n\t\t\t\t\t_guard = undefined;\r\n\t\t\t\t\t_guardDepth = _stack.Count;\r\n\t\t\t\t\treturn;\r\n\r\n\t\t\t\tcase \u0022define\u0022:\r\n\t\t\t\t\t// \u0060#ifndef FOO_H\u0060 / \u0060#define FOO_H\u0060 is an include guard, and the first inclusion always\r\n\t\t\t\t\t// takes it. Without this, every guarded header would be one big undecidable region and\r\n\t\t\t\t\t// nothing in it would ever be checked.\r\n\t\t\t\t\tif ( guard is not null \u0026\u0026 guard.Length \u003E 0 \u0026\u0026 _stack.Count == _guardDepth \u0026\u0026\r\n\t\t\t\t\t\t _stack[^1] == Branch.Unknown \u0026\u0026 FirstWord( rest ) == guard )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\t_stack[^1] = Branch.Live;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\treturn;\r\n\r\n\t\t\t\tcase \u0022elif\u0022:\r\n\t\t\t\t\t// The branch before this one was live, so this one cannot be; otherwise re-evaluate.\r\n\t\t\t\t\tif ( _stack.Count \u003E 0 )\r\n\t\t\t\t\t\t_stack[^1] = _stack[^1] == Branch.Live ? Branch.Dead : Evaluate( rest, lineIndex );\r\n\r\n\t\t\t\t\treturn;\r\n\r\n\t\t\t\tcase \u0022else\u0022:\r\n\t\t\t\t\tif ( _stack.Count \u003E 0 )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\t_stack[^1] = _stack[^1] switch\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tBranch.Live =\u003E Branch.Dead,\r\n\t\t\t\t\t\t\tBranch.Dead =\u003E Branch.Live,\r\n\t\t\t\t\t\t\t_ =\u003E Branch.Unknown\r\n\t\t\t\t\t\t};\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\treturn;\r\n\r\n\t\t\t\tcase \u0022endif\u0022:\r\n\t\t\t\t\tif ( _stack.Count \u003E 0 )\r\n\t\t\t\t\t\t_stack.RemoveAt( _stack.Count - 1 );\r\n\r\n\t\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tBranch Evaluate( string expression, int lineIndex )\r\n\t\t{\r\n\t\t\tvar text = expression.Trim();\r\n\r\n\t\t\t// Strip one layer of wrapping parentheses: \u0060#if ( 0 )\u0060 is written as often as \u0060#if 0\u0060.\r\n\t\t\twhile ( text.Length \u003E 2 \u0026\u0026 text[0] == \u0027(\u0027 \u0026\u0026 text[^1] == \u0027)\u0027 )\r\n\t\t\t\ttext = text.Substring( 1, text.Length - 2 ).Trim();\r\n\r\n\t\t\tif ( text == \u00220\u0022 )\r\n\t\t\t\treturn Branch.Dead;\r\n\r\n\t\t\tif ( text == \u00221\u0022 )\r\n\t\t\t\treturn Branch.Live;\r\n\r\n\t\t\tvar negated = text.StartsWith( \u0022!\u0022, StringComparison.Ordinal );\r\n\r\n\t\t\tif ( negated )\r\n\t\t\t\ttext = text.Substring( 1 ).TrimStart();\r\n\r\n\t\t\tif ( !text.StartsWith( \u0022defined\u0022, StringComparison.Ordinal ) )\r\n\t\t\t\treturn Branch.Unknown;\r\n\r\n\t\t\tvar argument = text.Substring( \u0022defined\u0022.Length ).Trim();\r\n\r\n\t\t\twhile ( argument.Length \u003E 2 \u0026\u0026 argument[0] == \u0027(\u0027 \u0026\u0026 argument[^1] == \u0027)\u0027 )\r\n\t\t\t\targument = argument.Substring( 1, argument.Length - 2 ).Trim();\r\n\r\n\t\t\tvar name = FirstWord( argument );\r\n\r\n\t\t\t// \u0060defined(A) \u0026\u0026 defined(B)\u0060 leaves a tail behind; anything left over is not decidable.\r\n\t\t\tif ( name.Length == 0 || name.Length != argument.Length )\r\n\t\t\t\treturn Branch.Unknown;\r\n\r\n\t\t\tif ( !DefinedBefore( name, lineIndex ) )\r\n\t\t\t\treturn Branch.Unknown;\r\n\r\n\t\t\treturn negated ? Branch.Dead : Branch.Live;\r\n\t\t}\r\n\r\n\t\tbool DefinedBefore( string name, int lineIndex ) =\u003E\r\n\t\t\tname.Length \u003E 0 \u0026\u0026 _defined.TryGetValue( name, out var line ) \u0026\u0026 line \u003C lineIndex;\r\n\r\n\t\tstatic string FirstWord( string text )\r\n\t\t{\r\n\t\t\tvar i = 0;\r\n\r\n\t\t\twhile ( i \u003C text.Length \u0026\u0026 ( text[i] == \u0027 \u0027 || text[i] == \u0027\\t\u0027 || text[i] == \u0027(\u0027 ) )\r\n\t\t\t\ti\u002B\u002B;\r\n\r\n\t\t\tvar start = i;\r\n\r\n\t\t\twhile ( i \u003C text.Length \u0026\u0026 ( char.IsLetterOrDigit( text[i] ) || text[i] == \u0027_\u0027 ) )\r\n\t\t\t\ti\u002B\u002B;\r\n\r\n\t\t\treturn text.Substring( start, i - start );\r\n\t\t}\r\n\t}\r\n\r\n\tstatic void Balance( List\u003CPrismDiagnostic\u003E results, Stack\u003C(char, int, int)\u003E braces, string word,\r\n\t\tstring file, int line, Token token )\r\n\t{\r\n\t\tif ( word.Length != 1 )\r\n\t\t\treturn;\r\n\r\n\t\tvar c = word[0];\r\n\r\n\t\tif ( c is \u0027{\u0027 or \u0027(\u0027 or \u0027[\u0027 )\r\n\t\t{\r\n\t\t\tbraces.Push( (c, line, token.Start) );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( c is not ( \u0027}\u0027 or \u0027)\u0027 or \u0027]\u0027 ) )\r\n\t\t\treturn;\r\n\r\n\t\tvar expected = c switch { \u0027}\u0027 =\u003E \u0027{\u0027, \u0027)\u0027 =\u003E \u0027(\u0027, _ =\u003E \u0027[\u0027 };\r\n\r\n\t\tif ( braces.Count == 0 )\r\n\t\t{\r\n\t\t\tresults.Add( PrismDiagnostic.AtSpan( DiagnosticSeverity.Error, TextDiagnosticCode.Unbalanced,\r\n\t\t\t\t$\u0022\u0027{c}\u0027 has no matching \u0027{expected}\u0027\u0022, Span( file, line, token ) ) );\r\n\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar top = braces.Peek();\r\n\r\n\t\tif ( top.Item1 != expected )\r\n\t\t{\r\n\t\t\tresults.Add( PrismDiagnostic.AtSpan( DiagnosticSeverity.Error, TextDiagnosticCode.Unbalanced,\r\n\t\t\t\t$\u0022\u0027{c}\u0027 closes a \u0027{top.Item1}\u0027 opened on line {top.Item2 \u002B 1}\u0022, Span( file, line, token ) ) );\r\n\t\t}\r\n\r\n\t\tbraces.Pop();\r\n\t}\r\n\r\n\tstatic TokenKind FirstKind( List\u003CToken\u003E tokens )\r\n\t{\r\n\t\tfor ( var i = 0; i \u003C tokens.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( tokens[i].Kind != TokenKind.Whitespace )\r\n\t\t\t\treturn tokens[i].Kind;\r\n\t\t}\r\n\r\n\t\treturn TokenKind.None;\r\n\t}\r\n\r\n\tstatic bool IsFirstOnLine( List\u003CToken\u003E tokens, int index )\r\n\t{\r\n\t\tfor ( var i = 0; i \u003C index; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( tokens[i].Kind != TokenKind.Whitespace )\r\n\t\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\tstatic void CheckDirective( List\u003CPrismDiagnostic\u003E results, LanguageDefinition language, string content,\r\n\t\tint line, Token hash, List\u003CToken\u003E tokens, int index, string file )\r\n\t{\r\n\t\t// The lexer emits \u0060#include\u0060 as one token; a lexer that splits the hash off has to work too.\r\n\t\tvar name = content.Substring( hash.Start, hash.Length ).TrimStart( \u0027#\u0027 ).Trim();\r\n\t\tvar end = hash.Start \u002B hash.Length;\r\n\r\n\t\tif ( name.Length == 0 )\r\n\t\t{\r\n\t\t\tif ( index \u002B 1 \u003E= tokens.Count )\r\n\t\t\t\treturn;\r\n\r\n\t\t\tvar next = tokens[index \u002B 1];\r\n\r\n\t\t\tif ( next.Start \u003C 0 || next.Start \u002B next.Length \u003E content.Length )\r\n\t\t\t\treturn;\r\n\r\n\t\t\tname = content.Substring( next.Start, next.Length );\r\n\t\t\tend = next.Start \u002B next.Length;\r\n\t\t}\r\n\r\n\t\tif ( language.IsDirective( name ) )\r\n\t\t\treturn;\r\n\r\n\t\t// \u0060# 42 \u0022file\u0022\u0060 is a line marker the preprocessor emits; never a mistake in authored code.\r\n\t\tif ( name.Length == 0 || char.IsDigit( name[0] ) )\r\n\t\t\treturn;\r\n\r\n\t\tresults.Add( PrismDiagnostic.AtSpan( DiagnosticSeverity.Error, TextDiagnosticCode.UnknownDirective,\r\n\t\t\t$\u0022Unknown preprocessor directive \u0027#{name}\u0027\u0022,\r\n\t\t\tnew SourceSpan( file, line \u002B 1, hash.Start \u002B 1, line \u002B 1, end \u002B 1 ) ) );\r\n\t}\r\n\r\n\tstatic bool PrecededByAccess( string content, int start )\r\n\t{\r\n\t\tvar i = start - 1;\r\n\r\n\t\twhile ( i \u003E= 0 \u0026\u0026 content[i] == \u0027 \u0027 )\r\n\t\t\ti--;\r\n\r\n\t\tif ( i \u003C 0 )\r\n\t\t\treturn false;\r\n\r\n\t\tif ( content[i] == \u0027.\u0027 )\r\n\t\t\treturn true;\r\n\r\n\t\treturn i \u003E= 1 \u0026\u0026 content[i] == \u0027:\u0027 \u0026\u0026 content[i - 1] == \u0027:\u0027;\r\n\t}\r\n\r\n\tstatic bool FollowedByCall( string content, int end )\r\n\t{\r\n\t\tfor ( var i = end; i \u003C content.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( content[i] == \u0027 \u0027 || content[i] == \u0027\\t\u0027 )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\treturn content[i] == \u0027(\u0027;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\tstatic SourceSpan Span( string file, int line, Token token ) =\u003E\r\n\t\tnew( file, line \u002B 1, token.Start \u002B 1, line \u002B 1, token.Start \u002B token.Length \u002B 1 );\r\n\r\n\t// ---- compiler tier ----------------------------------------------------\r\n\r\n\tasync Task\u003CIReadOnlyList\u003CPrismDiagnostic\u003E\u003E Compile( string text, string filePath,\r\n\t\tLanguageDefinition definition, CancellationToken ct )\r\n\t{\r\n\t\tif ( string.IsNullOrWhiteSpace( text ) )\r\n\t\t\treturn Array.Empty\u003CPrismDiagnostic\u003E();\r\n\r\n\t\tvar kind = definition?.Id ?? \u0022hlsl\u0022;\r\n\r\n\t\tif ( string.Equals( kind, \u0022slang\u0022, StringComparison.OrdinalIgnoreCase ) )\r\n\t\t\treturn await ValidateSlang( text, filePath, ct ).ConfigureAwait( false );\r\n\r\n\t\treturn await ValidateShader( text, filePath, kind, ct ).ConfigureAwait( false );\r\n\t}\r\n\r\n\tTempWorkspace Workspace\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tlock ( _lock )\r\n\t\t\t{\r\n\t\t\t\t_workspace ??= new TempWorkspace( SessionId );\r\n\t\t\t\treturn _workspace;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tasync Task\u003CIReadOnlyList\u003CPrismDiagnostic\u003E\u003E ValidateShader( string text, string filePath, string kind,\r\n\t\tCancellationToken ct )\r\n\t{\r\n\t\tvar results = new List\u003CPrismDiagnostic\u003E();\r\n\t\tvar stem = Stem( filePath );\r\n\r\n\t\t// A .shader is already a block file. A bare .hlsl is an include, and the engine refuses to\r\n\t\t// compile one, so it gets wrapped in the smallest legal shader that will hold it.\r\n\t\tvar probe = string.Equals( kind, \u0022vfx\u0022, StringComparison.OrdinalIgnoreCase )\r\n\t\t\t? ShaderProbeBuilder.ForShaderFile( text, stem )\r\n\t\t\t: ShaderProbeBuilder.ForHlsl( text, new ShaderProbeOptions { Name = stem } );\r\n\r\n\t\tvar fileName = $\u0022{stem}.{PrismConstants.ShaderExtension}\u0022;\r\n\t\tvar workspace = Workspace;\r\n\r\n\t\tif ( !workspace.IsValid || !workspace.Write( fileName, probe.Text ) )\r\n\t\t{\r\n\t\t\tresults.Add( PrismDiagnostic.Info( DiagnosticCode.CompilerRaw,\r\n\t\t\t\t\u0022Prism could not write its scratch shader, so only local checks ran\u0022 ) );\r\n\r\n\t\t\treturn results;\r\n\t\t}\r\n\r\n\t\tvar relative = workspace.Relative( fileName );\r\n\r\n\t\tvar options = new ShaderCompileOptions\r\n\t\t{\r\n\t\t\tForceRecompile = false,\r\n\t\t\tConsoleOutput = false,\r\n\t\t\tSingleThreaded = false\r\n\t\t};\r\n\r\n\t\tShaderCompile.Results compiled = null;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\t// Back to the main thread before touching the engine compiler.\r\n\t\t\t//\r\n\t\t\t// EditorUtility.CompileShader reaches straight into native code: Shader.LoadFromSource, the\r\n\t\t\t// vfx_vulkan.dll interface (lazily loaded by ShaderCompile\u0027s static constructor, on whatever\r\n\t\t\t// thread happens to touch it first), FinalizeCompile, InitializeWrite and the native resource\r\n\t\t\t// compiler. None of that is thread-affine by contract, and none of it is documented as safe to\r\n\t\t\t// call from anywhere.\r\n\t\t\t//\r\n\t\t\t// Every call site in the engine invokes it from the main thread and simply awaits \u2014 see\r\n\t\t\t// ShaderGraph\u0027s MainWindow, ShaderHooks and StartupLoadProject. The engine offloads the part\r\n\t\t\t// that is actually parallel itself: ProgramSource.CompileCore wraps the combo loop in its own\r\n\t\t\t// Task.Run/Parallel.ForEach. Wrapping the whole call in Task.Run, as this used to, put the\r\n\t\t\t// serial native prologue and epilogue on a pool thread instead, which no engine code ever\r\n\t\t\t// does. Awaiting from the main thread does not block the editor \u2014 the await yields, and the\r\n\t\t\t// expensive combo loop still runs on the pool where the engine put it.\r\n\t\t\tawait MainThread.Wait();\r\n\r\n\t\t\tPrismLog.Info( $\u0022Prism.Text: compiling \u0027{relative}\u0027 through the engine shader compiler\u0022 );\r\n\r\n\t\t\tcompiled = await EditorUtility.CompileShader( Editor.FileSystem.Root, relative, options, ct );\r\n\r\n\t\t\tPrismLog.Info( $\u0022Prism.Text: engine compile of \u0027{relative}\u0027 returned \u0022 \u002B\r\n\t\t\t\t$\u0022success={compiled?.Success}, programs={compiled?.Programs?.Count ?? 0}\u0022 );\r\n\t\t}\r\n\t\tcatch ( OperationCanceledException )\r\n\t\t{\r\n\t\t\tthrow;\r\n\t\t}\r\n\t\tcatch ( Exception e )\r\n\t\t{\r\n\t\t\tPrismLog.Error( e, \u0022Prism.Text: the engine shader compiler threw\u0022 );\r\n\r\n\t\t\tresults.Add( PrismDiagnostic.Error( DiagnosticCode.CompilerRaw,\r\n\t\t\t\t\u0022The engine shader compiler failed\u0022, null, e.Message ) );\r\n\r\n\t\t\treturn results;\r\n\t\t}\r\n\r\n\t\tif ( compiled is null )\r\n\t\t\treturn results;\r\n\r\n\t\tvar programs = compiled.Programs ?? new List\u003CShaderCompile.Results.Program\u003E();\r\n\r\n\t\tif ( !compiled.Success \u0026\u0026 programs.Count == 0 )\r\n\t\t{\r\n\t\t\tresults.Add( probe.MapBack( CompilerOutputParser.BlockHeaderFailure( fileName ), filePath ) );\r\n\t\t\treturn results;\r\n\t\t}\r\n\r\n\t\tvar seen = new HashSet\u003Cstring\u003E( StringComparer.Ordinal );\r\n\r\n\t\tforeach ( var program in programs )\r\n\t\t{\r\n\t\t\tif ( program?.Output is not { Count: \u003E 0 } )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar map = LineDirectiveMap.Build( program.Source, fileName ).Calibrate( probe.Text );\r\n\t\t\tvar parsed = CompilerOutputParser.Parse( program.Output, fileName );\r\n\t\t\tvar stage = CompilerOutputParser.Pretty( program.Name );\r\n\r\n\t\t\tforeach ( var diagnostic in map.RemapAll( parsed, null, fileName ) )\r\n\t\t\t{\r\n\t\t\t\tvar mapped = probe.MapBack( diagnostic, filePath );\r\n\r\n\t\t\t\tif ( mapped is null )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\t// The same COMMON-block error is reported once per program; show it once.\r\n\t\t\t\tif ( !seen.Add( $\u0022{mapped.Severity}|{mapped.Code}|{mapped.Span}|{mapped.Message}\u0022 ) )\r\n\t\t\t\t\tcontinue;\r\n\r\n\t\t\t\tresults.Add( string.IsNullOrEmpty( stage )\r\n\t\t\t\t\t? mapped\r\n\t\t\t\t\t: mapped with\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tDetail = string.IsNullOrWhiteSpace( mapped.Detail )\r\n\t\t\t\t\t\t\t? $\u0022Reported while compiling {stage}.\u0022\r\n\t\t\t\t\t\t\t: $\u0022{mapped.Detail}\\nReported while compiling {stage}.\u0022\r\n\t\t\t\t\t} );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn results;\r\n\t}\r\n\r\n\tasync Task\u003CIReadOnlyList\u003CPrismDiagnostic\u003E\u003E ValidateSlang( string text, string filePath, CancellationToken ct )\r\n\t{\r\n\t\tvar validator = SlangToolchain.CreateValidator();\r\n\r\n\t\tif ( validator is null || !validator.Available )\r\n\t\t{\r\n\t\t\treturn new[]\r\n\t\t\t{\r\n\t\t\t\tPrismDiagnostic.Info( TextDiagnosticCode.SlangNotValidated,\r\n\t\t\t\t\t\u0022Slang is not validated: no slangc was found\u0022,\r\n\t\t\t\t\tnull,\r\n\t\t\t\t\t\u0022Install the Slang toolchain from Preferences to have slangc check this file. Local \u0022 \u002B\r\n\t\t\t\t\t\u0022checks still run, and nothing else in Prism depends on it.\u0022 )\r\n\t\t\t};\r\n\t\t}\r\n\r\n\t\tvar probe = ShaderProbeBuilder.ForSlang( text, new ShaderProbeOptions { Name = Stem( filePath ) } );\r\n\t\tvar entries = ShaderProbeBuilder.DiscoverSlangEntryPoints( probe.Text );\r\n\r\n\t\tvar request = new SlangValidationRequest\r\n\t\t{\r\n\t\t\tSource = probe.Text,\r\n\t\t\tEntryPoints = entries,\r\n\t\t\tDisplayName = probe.FileName,\r\n\t\t\tIncludePaths = IncludeResolver.SearchRoots.ToArray()\r\n\t\t};\r\n\r\n\t\tvar diagnostics = await validator.Validate( request, ct ).ConfigureAwait( false );\r\n\r\n\t\treturn probe.MapBack( diagnostics, filePath );\r\n\t}\r\n\r\n\tstatic string Stem( string filePath )\r\n\t{\r\n\t\tvar name = string.IsNullOrWhiteSpace( filePath )\r\n\t\t\t? \u0022buffer\u0022\r\n\t\t\t: System.IO.Path.GetFileNameWithoutExtension( filePath );\r\n\r\n\t\tif ( string.IsNullOrWhiteSpace( name ) )\r\n\t\t\tname = \u0022buffer\u0022;\r\n\r\n\t\tvar clean = new System.Text.StringBuilder( name.Length );\r\n\r\n\t\tforeach ( var c in name )\r\n\t\t\tclean.Append( char.IsLetterOrDigit( c ) || c == \u0027_\u0027 ? c : \u0027_\u0027 );\r\n\r\n\t\treturn \u0022prism_text_\u0022 \u002B clean;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EStops any work, drops the scratch folder and detaches from the editor.\u003C/summary\u003E\r\n\tpublic void Dispose()\r\n\t{\r\n\t\tif ( _disposed )\r\n\t\t\treturn;\r\n\r\n\t\t_disposed = true;\r\n\r\n\t\tCancel();\r\n\r\n\t\tif ( _editor is { IsValid: true } \u0026\u0026 _settled is not null )\r\n\t\t\t_editor.TextSettled -= _settled;\r\n\r\n\t\t_editor = null;\r\n\t\t_settled = null;\r\n\r\n\t\tlock ( _lock )\r\n\t\t{\r\n\t\t\tPrismLog.Guard( \u0022Prism.Text: drop scratch workspace\u0022, () =\u003E _workspace?.Dispose() );\r\n\r\n\t\t\t_workspace = null;\r\n\r\n\t\t\t_inFlight?.Dispose();\r\n\t\t\t_inFlight = null;\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/Ui/HistoryPanel.cs","FileName":"HistoryPanel.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using Editor.Prism.Core;\r\nusing Editor.Prism.Undo;\r\nusing Margin = Sandbox.UI.Margin;\r\n\r\nnamespace Editor.Prism.Ui;\r\n\r\n/// \u003Csummary\u003EOne row of the History panel: a level in the undo stack.\u003C/summary\u003E\r\ninternal sealed class HistoryRow\r\n{\r\n\t/// \u003Csummary\u003EThe stack level this row jumps to. Zero is the document as opened.\u003C/summary\u003E\r\n\tpublic int Level { get; init; }\r\n\r\n\t/// \u003Csummary\u003EThe label of the edit.\u003C/summary\u003E\r\n\tpublic string Name { get; init; }\r\n\r\n\t/// \u003Csummary\u003ETrue when this is where the document currently sits.\u003C/summary\u003E\r\n\tpublic bool IsCurrent { get; init; }\r\n\r\n\t/// \u003Csummary\u003ETrue when this row is ahead of the current level, i.e. redoable.\u003C/summary\u003E\r\n\tpublic bool IsFuture { get; init; }\r\n\r\n\t/// \u003Csummary\u003EWhen the edit was committed.\u003C/summary\u003E\r\n\tpublic DateTime Time { get; init; }\r\n\r\n\t/// \u003Csummary\u003EHow many characters the snapshot pair costs.\u003C/summary\u003E\r\n\tpublic int Size { get; init; }\r\n\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tpublic override string ToString() =\u003E $\u0022{Level}. {Name}\u0022;\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The History dock: the undo stack, as a list you can click.\r\n/// \u003Cpara\u003E\r\n/// Prism records snapshots rather than commands, so every level is a complete, valid document and\r\n/// jumping to any of them is exactly as safe as jumping to the one next door. That makes a clickable\r\n/// history honest rather than a trap \u2014 which is why it gets a panel instead of two toolbar arrows.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\npublic sealed class HistoryPanel : Widget\r\n{\r\n\t/// \u003Csummary\u003EThe dock name this panel registers under. Frozen.\u003C/summary\u003E\r\n\tpublic const string DockName = \u0022History\u0022;\r\n\r\n\treadonly List\u003CHistoryRow\u003E _rows = new();\r\n\r\n\tPrismSession _session;\r\n\tPrismUndoStack _undo;\r\n\tListView _list;\r\n\tPrismEmptyState _empty;\r\n\tLabel _status;\r\n\r\n\tbool _rebuildQueued;\r\n\r\n\t/// \u003Csummary\u003EBuild the panel. A null session is legal and shows the empty state.\u003C/summary\u003E\r\n\tpublic HistoryPanel( PrismSession session ) : base( null )\r\n\t{\r\n\t\tName = \u0022PrismHistory\u0022;\r\n\t\tWindowTitle = DockName;\r\n\r\n\t\tLayout = Layout.Column();\r\n\t\tLayout.Margin = 0;\r\n\t\tLayout.Spacing = 0;\r\n\r\n\t\t_list = new ListView( this )\r\n\t\t{\r\n\t\t\tItemSize = new Vector2( -1, PrismPanelChrome.RowHeight ),\r\n\t\t\tItemPaint = PaintRow,\r\n\t\t\tItemClicked = OnRowClicked,\r\n\t\t\tItemActivated = OnRowClicked,\r\n\t\t\tItemContextMenu = OnRowContextMenu,\r\n\t\t\tMultiSelect = false\r\n\t\t};\r\n\r\n\t\tLayout.Add( _list, 1 );\r\n\r\n\t\t_empty = new PrismEmptyState( this, \u0022history\u0022, \u0022Nothing to undo yet\u0022,\r\n\t\t\t\u0022Every edit you make appears here. Click one to jump back to it.\u0022 );\r\n\r\n\t\tLayout.Add( _empty, 1 );\r\n\r\n\t\t_status = new Label( string.Empty ) { Color = PrismTheme.TextMuted };\r\n\t\t_status.ContentMargins = new Margin( 8, 2, 8, 4 );\r\n\t\tLayout.Add( _status );\r\n\r\n\t\tBind( session );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EMaterial icon shown on the dock tab.\u003C/summary\u003E\r\n\tpublic string DockIcon =\u003E \u0022history\u0022;\r\n\r\n\t/// \u003Csummary\u003EThe session this panel is bound to. Null is legal.\u003C/summary\u003E\r\n\tpublic PrismSession Session =\u003E _session;\r\n\r\n\t// ---------------------------------------------------------------- binding ----\r\n\r\n\tvoid Bind( PrismSession session )\r\n\t{\r\n\t\tUnbind();\r\n\r\n\t\t_session = session;\r\n\r\n\t\tif ( _session is not null )\r\n\t\t{\r\n\t\t\t_session.DocumentReplaced \u002B= OnDocumentReplaced;\r\n\t\t\tAttach( _session.Undo );\r\n\t\t}\r\n\r\n\t\tRebuild();\r\n\t}\r\n\r\n\tvoid Attach( PrismUndoStack undo )\r\n\t{\r\n\t\tif ( ReferenceEquals( _undo, undo ) ) return;\r\n\r\n\t\tif ( _undo is not null )\r\n\t\t{\r\n\t\t\t_undo.Changed -= QueueRebuild;\r\n\t\t\t_undo.Restored -= QueueRebuild;\r\n\t\t}\r\n\r\n\t\t_undo = undo;\r\n\r\n\t\tif ( _undo is null ) return;\r\n\r\n\t\t_undo.Changed \u002B= QueueRebuild;\r\n\t\t_undo.Restored \u002B= QueueRebuild;\r\n\t}\r\n\r\n\tvoid Unbind()\r\n\t{\r\n\t\tAttach( null );\r\n\r\n\t\tif ( _session is null ) return;\r\n\r\n\t\t_session.DocumentReplaced -= OnDocumentReplaced;\r\n\t\t_session = null;\r\n\t}\r\n\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tpublic override void OnDestroyed()\r\n\t{\r\n\t\tUnbind();\r\n\t\tbase.OnDestroyed();\r\n\t}\r\n\r\n\tvoid OnDocumentReplaced()\r\n\t{\r\n\t\tAttach( _session?.Undo );\r\n\t\tQueueRebuild();\r\n\t}\r\n\r\n\tvoid QueueRebuild()\r\n\t{\r\n\t\tif ( _rebuildQueued ) return;\r\n\r\n\t\t_rebuildQueued = true;\r\n\r\n\t\tMainThread.Queue( () =\u003E\r\n\t\t{\r\n\t\t\t_rebuildQueued = false;\r\n\r\n\t\t\tif ( !this.IsValid() ) return;\r\n\r\n\t\t\tRebuild();\r\n\t\t} );\r\n\t}\r\n\r\n\t// ---------------------------------------------------------------- model ----\r\n\r\n\tvoid Rebuild()\r\n\t{\r\n\t\t_rows.Clear();\r\n\r\n\t\tif ( _undo is null )\r\n\t\t{\r\n\t\t\t_empty.Set( \u0022No document\u0022, \u0022Open a graph to see its edit history.\u0022 );\r\n\t\t\t_empty.Visible = true;\r\n\t\t\t_list.Visible = false;\r\n\t\t\t_status.Text = string.Empty;\r\n\t\t\t_list.SetItems( _rows );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar level = _undo.Level;\r\n\r\n\t\tforeach ( var item in _undo.History )\r\n\t\t{\r\n\t\t\t_rows.Add( new HistoryRow\r\n\t\t\t{\r\n\t\t\t\tLevel = item.Level,\r\n\t\t\t\tName = item.Name,\r\n\t\t\t\tIsCurrent = item.IsCurrent,\r\n\t\t\t\tIsFuture = item.Level \u003E level,\r\n\t\t\t\tTime = item.Time,\r\n\t\t\t\tSize = item.Size\r\n\t\t\t} );\r\n\t\t}\r\n\r\n\t\tvar meaningful = _undo.Count \u003E 0;\r\n\r\n\t\t_empty.Visible = !meaningful;\r\n\t\t_list.Visible = meaningful;\r\n\r\n\t\tif ( !meaningful )\r\n\t\t{\r\n\t\t\t_empty.Set( \u0022Nothing to undo yet\u0022, \u0022Every edit you make appears here. Click one to jump back to it.\u0022 );\r\n\t\t}\r\n\r\n\t\t_list.SetItems( _rows );\r\n\r\n\t\tvar current = _rows.FirstOrDefault( x =\u003E x.IsCurrent );\r\n\r\n\t\tif ( current is not null )\r\n\t\t{\r\n\t\t\t_list.SelectItem( current, false, true );\r\n\t\t\tPrismLog.Guard( \u0022History: scroll to current\u0022, () =\u003E _list.ScrollTo( current ) );\r\n\t\t}\r\n\r\n\t\t_status.Text = _undo.Describe();\r\n\t}\r\n\r\n\t// ---------------------------------------------------------------- painting ----\r\n\r\n\tvoid PaintRow( VirtualWidget item )\r\n\t{\r\n\t\tif ( item.Object is not HistoryRow row ) return;\r\n\r\n\t\tvar rect = item.Rect;\r\n\t\tvar index = _rows.IndexOf( row );\r\n\r\n\t\tPrismPanelChrome.PaintRow( rect, index, item.Hovered, row.IsCurrent );\r\n\r\n\t\tvar alpha = row.IsFuture ? 0.42f : 1f;\r\n\t\tvar inner = rect.Shrink( PrismPanelChrome.Pad, 0f, PrismPanelChrome.Pad, 0f );\r\n\r\n\t\tvar icon = IconFor( row );\r\n\t\tvar color = row.IsCurrent ? PrismTheme.Accent : PrismTheme.TextMuted;\r\n\r\n\t\tPaint.SetPen( color.WithAlpha( alpha ) );\r\n\t\tPaint.DrawIcon( new Rect( inner.Left, inner.Top, 16f, inner.Height ), icon, 13f, TextFlag.Center );\r\n\r\n\t\tvar right = inner.Right;\r\n\r\n\t\tif ( inner.Width \u003E 170f )\r\n\t\t{\r\n\t\t\tvar time = row.Time == default ? string.Empty : row.Time.ToLocalTime().ToString( \u0022HH:mm:ss\u0022 );\r\n\r\n\t\t\tif ( !string.IsNullOrEmpty( time ) )\r\n\t\t\t{\r\n\t\t\t\tPaint.SetFont( PrismTheme.FontFamily, 10, 400, false, true );\r\n\t\t\t\tPaint.SetPen( PrismTheme.TextDisabled.WithAlpha( alpha ) );\r\n\t\t\t\tPaint.DrawText( new Rect( right - 52f, inner.Top, 52f, inner.Height ), time,\r\n\t\t\t\t\tTextFlag.RightCenter | TextFlag.SingleLine );\r\n\r\n\t\t\t\tright -= 58f;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( row.IsCurrent \u0026\u0026 inner.Width \u003E 220f )\r\n\t\t{\r\n\t\t\tPaint.SetFont( PrismTheme.FontFamily, 9, 600, false, true );\r\n\t\t\tPaint.SetPen( PrismTheme.Accent );\r\n\t\t\tPaint.DrawText( new Rect( right - 44f, inner.Top, 44f, inner.Height ), \u0022CURRENT\u0022,\r\n\t\t\t\tTextFlag.RightCenter | TextFlag.SingleLine );\r\n\r\n\t\t\tright -= 50f;\r\n\t\t}\r\n\r\n\t\tvar nameRect = new Rect( inner.Left \u002B 20f, inner.Top,\r\n\t\t\tMathF.Max( 20f, right - inner.Left - 20f ), inner.Height );\r\n\r\n\t\tPrismPaint.Text( nameRect, row.Name,\r\n\t\t\t( row.IsCurrent ? PrismTheme.TextPrimary : PrismTheme.TextSecondary ).WithAlpha( alpha ),\r\n\t\t\tPrismTheme.BodySize, row.IsCurrent ? 500 : 400 );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The glyph for an edit, matched on its label. Undo entries are labelled by the mutation API from\r\n\t/// a small, stable vocabulary, so a prefix match is reliable and one unknown label degrades to a\r\n\t/// generic pencil rather than a blank row.\r\n\t/// \u003C/summary\u003E\r\n\tstatic string IconFor( HistoryRow row )\r\n\t{\r\n\t\tif ( row.Level == 0 ) return PrismIcons.Open;\r\n\r\n\t\tvar name = row.Name ?? string.Empty;\r\n\r\n\t\tif ( name.StartsWith( \u0022Add\u0022, StringComparison.OrdinalIgnoreCase ) ) return PrismIcons.Add;\r\n\t\tif ( name.StartsWith( \u0022Delete\u0022, StringComparison.OrdinalIgnoreCase ) ) return PrismIcons.Delete;\r\n\t\tif ( name.StartsWith( \u0022Move\u0022, StringComparison.OrdinalIgnoreCase ) ) return \u0022open_with\u0022;\r\n\t\tif ( name.StartsWith( \u0022Resize\u0022, StringComparison.OrdinalIgnoreCase ) ) return \u0022aspect_ratio\u0022;\r\n\t\tif ( name.StartsWith( \u0022Create Connection\u0022, StringComparison.OrdinalIgnoreCase ) ) return PrismIcons.Connect;\r\n\t\tif ( name.StartsWith( \u0022Disconnect\u0022, StringComparison.OrdinalIgnoreCase ) ) return PrismIcons.Disconnect;\r\n\t\tif ( name.StartsWith( \u0022Reroute\u0022, StringComparison.OrdinalIgnoreCase ) ) return PrismIcons.Reroute;\r\n\t\tif ( name.StartsWith( \u0022Route\u0022, StringComparison.OrdinalIgnoreCase ) ) return PrismIcons.Reroute;\r\n\t\tif ( name.StartsWith( \u0022Paste\u0022, StringComparison.OrdinalIgnoreCase ) ) return PrismIcons.Paste;\r\n\t\tif ( name.StartsWith( \u0022Cut\u0022, StringComparison.OrdinalIgnoreCase ) ) return PrismIcons.Cut;\r\n\t\tif ( name.StartsWith( \u0022Duplicate\u0022, StringComparison.OrdinalIgnoreCase ) ) return PrismIcons.Duplicate;\r\n\t\tif ( name.StartsWith( \u0022Rename\u0022, StringComparison.OrdinalIgnoreCase ) ) return \u0022edit\u0022;\r\n\t\tif ( name.StartsWith( \u0022Reorder\u0022, StringComparison.OrdinalIgnoreCase ) ) return \u0022swap_vert\u0022;\r\n\t\tif ( name.StartsWith( \u0022Change Settings\u0022, StringComparison.OrdinalIgnoreCase ) ) return \u0022settings\u0022;\r\n\t\tif ( name.StartsWith( \u0022Change Preview\u0022, StringComparison.OrdinalIgnoreCase ) ) return PrismIcons.Preview;\r\n\t\tif ( name.StartsWith( \u0022Set\u0022, StringComparison.OrdinalIgnoreCase ) ) return PrismIcons.Parameter;\r\n\t\tif ( name.StartsWith( \u0022Edit\u0022, StringComparison.OrdinalIgnoreCase ) ) return \u0022edit\u0022;\r\n\r\n\t\treturn \u0022edit_note\u0022;\r\n\t}\r\n\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tPaint.ClearPen();\r\n\t\tPaint.SetBrush( PrismTheme.Panel );\r\n\t\tPaint.DrawRect( LocalRect );\r\n\t}\r\n\r\n\t// ---------------------------------------------------------------- interaction ----\r\n\r\n\tvoid OnRowClicked( object item )\r\n\t{\r\n\t\tif ( item is not HistoryRow row || _undo is null ) return;\r\n\t\tif ( row.IsCurrent ) return;\r\n\r\n\t\tif ( !_undo.JumpTo( row.Level ) ) return;\r\n\r\n\t\t_session?.MarkDirty();\r\n\t\t_session?.Touch();\r\n\t}\r\n\r\n\tvoid OnRowContextMenu( object item )\r\n\t{\r\n\t\tvar menu = new Menu( this );\r\n\r\n\t\tif ( item is HistoryRow row \u0026\u0026 !row.IsCurrent )\r\n\t\t{\r\n\t\t\tmenu.AddOption( $\u0022Jump To \u201C{row.Name}\u201D\u0022, \u0022history\u0022, () =\u003E OnRowClicked( row ) );\r\n\t\t\tmenu.AddSeparator();\r\n\t\t}\r\n\r\n\t\tmenu.AddOption( \u0022Undo\u0022, PrismIcons.Undo, () =\u003E { _undo?.Undo(); _session?.Touch(); } )\r\n\t\t\t.Enabled = _undo is { CanUndo: true };\r\n\r\n\t\tmenu.AddOption( \u0022Redo\u0022, PrismIcons.Redo, () =\u003E { _undo?.Redo(); _session?.Touch(); } )\r\n\t\t\t.Enabled = _undo is { CanRedo: true };\r\n\r\n\t\tmenu.AddSeparator();\r\n\t\tmenu.AddOption( \u0022Clear History\u0022, PrismIcons.Delete, () =\u003E { _undo?.Clear(); Rebuild(); } );\r\n\t\tmenu.AddOption( \u0022Copy Stack Dump\u0022, PrismIcons.Copy,\r\n\t\t\t() =\u003E EditorUtility.Clipboard.Copy( _undo?.Dump() ?? string.Empty ) );\r\n\r\n\t\tmenu.OpenAtCursor( false );\r\n\t}\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/Compiler/Backends/HlslBackend.cs","FileName":"HlslBackend.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using System.Text;\r\nusing Editor.Prism.Compiler.Ir;\r\nusing Editor.Prism.Core;\r\n\r\nnamespace Editor.Prism.Compiler.Backends;\r\n\r\n/// \u003Csummary\u003E\r\n/// A text buffer that remembers which graph node produced each line it wrote.\r\n/// \u003Cpara\u003E\r\n/// Every backend writes through this rather than a bare \u003Csee cref=\u0022StringBuilder\u0022/\u003E, because the\r\n/// generated-line to \u003Csee cref=\u0022NodeId\u0022/\u003E map is what turns a raw compiler error into a selected\r\n/// node. Losing it is losing the feature the built-in editor structurally cannot have.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\npublic sealed class HlslSourceBuilder\r\n{\r\n\treadonly StringBuilder _text = new();\r\n\treadonly SourceMap _map = new();\r\n\tint _line = 1;\r\n\tint _indent;\r\n\r\n\t/// \u003Csummary\u003ECreate a builder. Generated code uses hard tabs and CRLF, like everything else here.\u003C/summary\u003E\r\n\tpublic HlslSourceBuilder( string indent = \u0022\\t\u0022, string newLine = \u0022\\r\\n\u0022 )\r\n\t{\r\n\t\tIndent = indent ?? \u0022\\t\u0022;\r\n\t\tNewLine = string.IsNullOrEmpty( newLine ) ? \u0022\\r\\n\u0022 : newLine;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EOne level of indentation.\u003C/summary\u003E\r\n\tpublic string Indent { get; }\r\n\r\n\t/// \u003Csummary\u003EThe line ending written after every line.\u003C/summary\u003E\r\n\tpublic string NewLine { get; }\r\n\r\n\t/// \u003Csummary\u003EThe map from emitted line to originating node.\u003C/summary\u003E\r\n\tpublic SourceMap SourceMap =\u003E _map;\r\n\r\n\t/// \u003Csummary\u003EThe 1-based number of the line that will be written next.\u003C/summary\u003E\r\n\tpublic int LineNumber =\u003E _line;\r\n\r\n\t/// \u003Csummary\u003ELines written so far.\u003C/summary\u003E\r\n\tpublic int LineCount =\u003E _line - 1;\r\n\r\n\t/// \u003Csummary\u003ECurrent indentation depth.\u003C/summary\u003E\r\n\tpublic int IndentLevel\r\n\t{\r\n\t\tget =\u003E _indent;\r\n\t\tset =\u003E _indent = Math.Max( 0, value );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EIndent until the returned scope is disposed.\u003C/summary\u003E\r\n\tpublic IDisposable Indented() =\u003E new IndentScope( this );\r\n\r\n\t/// \u003Csummary\u003EWrite an empty line.\u003C/summary\u003E\r\n\tpublic HlslSourceBuilder Blank()\r\n\t{\r\n\t\t_text.Append( NewLine );\r\n\t\t_line\u002B\u002B;\r\n\t\treturn this;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EWrite one indented line with no origin.\u003C/summary\u003E\r\n\tpublic HlslSourceBuilder Write( string text ) =\u003E Write( text, NodeId.None );\r\n\r\n\t/// \u003Csummary\u003EWrite one indented line and record which node produced it.\u003C/summary\u003E\r\n\tpublic HlslSourceBuilder Write( string text, NodeId origin )\r\n\t{\r\n\t\tif ( text is null ) return this;\r\n\r\n\t\tif ( text.Length \u003E 0 )\r\n\t\t{\r\n\t\t\tfor ( int i = 0; i \u003C _indent; i\u002B\u002B ) _text.Append( Indent );\r\n\t\t\t_text.Append( text );\r\n\t\t}\r\n\r\n\t\t_text.Append( NewLine );\r\n\t\t_map.Add( _line, origin );\r\n\t\t_line\u002B\u002B;\r\n\r\n\t\treturn this;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Write a multi-line chunk, stripping the shared leading whitespace so a template written at any\r\n\t/// C# indentation lands correctly. Every produced line is attributed to \u003Cparamref name=\u0022origin\u0022/\u003E.\r\n\t/// \u003C/summary\u003E\r\n\tpublic HlslSourceBuilder WriteBlock( string text, NodeId origin = default )\r\n\t{\r\n\t\tif ( string.IsNullOrEmpty( text ) ) return this;\r\n\r\n\t\tforeach ( var line in SboxShaderTemplates.Dedent( text ).Split( \u0027\\n\u0027 ) )\r\n\t\t{\r\n\t\t\tWrite( line.TrimEnd(), origin );\r\n\t\t}\r\n\r\n\t\treturn this;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EWrite an opening brace and indent.\u003C/summary\u003E\r\n\tpublic HlslSourceBuilder Open( NodeId origin = default )\r\n\t{\r\n\t\tWrite( \u0022{\u0022, origin );\r\n\t\t_indent\u002B\u002B;\r\n\t\treturn this;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EOutdent and write a closing brace.\u003C/summary\u003E\r\n\tpublic HlslSourceBuilder Close( string suffix = null, NodeId origin = default )\r\n\t{\r\n\t\t_indent = Math.Max( 0, _indent - 1 );\r\n\t\tWrite( \u0022}\u0022 \u002B ( suffix ?? string.Empty ), origin );\r\n\t\treturn this;\r\n\t}\r\n\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tpublic override string ToString() =\u003E _text.ToString();\r\n\r\n\tsealed class IndentScope : IDisposable\r\n\t{\r\n\t\treadonly HlslSourceBuilder _owner;\r\n\r\n\t\tpublic IndentScope( HlslSourceBuilder owner )\r\n\t\t{\r\n\t\t\t_owner = owner;\r\n\t\t\t_owner._indent\u002B\u002B;\r\n\t\t}\r\n\r\n\t\tpublic void Dispose() =\u003E _owner._indent = Math.Max( 0, _owner._indent - 1 );\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Lowers \u003Csee cref=\u0022IrModule\u0022/\u003E expressions, statements, helpers and declarations into HLSL text.\r\n/// \u003Cpara\u003E\r\n/// Deliberately knows nothing about the VFX block file \u2014 that is \u003Csee cref=\u0022SboxShaderWriter\u0022/\u003E\u0027s\r\n/// job. The split is what lets the same HLSL feed a probe compile from the text editor, a\r\n/// \u003Cc\u003E.shader\u003C/c\u003E, or a future material-only target.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\npublic sealed class HlslEmitter\r\n{\r\n\tconst int PrecedencePrimary = 16;\r\n\tconst int PrecedencePostfix = 15;\r\n\tconst int PrecedenceUnary = 13;\r\n\tconst int PrecedenceLowest = 0;\r\n\r\n\treadonly HashSet\u003Cstring\u003E _reported = new();\r\n\r\n\t/// \u003Csummary\u003ECreate an emitter for one module.\u003C/summary\u003E\r\n\tpublic HlslEmitter( IrModule module, BackendEmitOptions options, DiagnosticSink diagnostics )\r\n\t{\r\n\t\tModule = module;\r\n\t\tOptions = options ?? BackendEmitOptions.Default;\r\n\t\tDiagnostics = diagnostics ?? new DiagnosticSink();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe module being lowered.\u003C/summary\u003E\r\n\tpublic IrModule Module { get; }\r\n\r\n\t/// \u003Csummary\u003EEmission options.\u003C/summary\u003E\r\n\tpublic BackendEmitOptions Options { get; }\r\n\r\n\t/// \u003Csummary\u003EWhere problems go. A backend never throws for user error.\u003C/summary\u003E\r\n\tpublic DiagnosticSink Diagnostics { get; }\r\n\r\n\t/// \u003Csummary\u003EWhich HLSL flavour to write.\u003C/summary\u003E\r\n\tpublic HlslDialect Dialect =\u003E Options.Dialect;\r\n\r\n\t/// \u003Csummary\u003EThe stage currently being written. Drives builtin lowering and stage legality.\u003C/summary\u003E\r\n\tpublic ShaderStage Stage { get; set; }\r\n\r\n\t/// \u003Csummary\u003EThe domain the module targets.\u003C/summary\u003E\r\n\tpublic ShaderDomain Domain =\u003E Module?.Meta?.Domain ?? ShaderDomain.Surface;\r\n\r\n\t/// \u003Csummary\u003ETrue when comments should be written into the output.\u003C/summary\u003E\r\n\tpublic bool WantsComments =\u003E Options.EmitComments || Options.DebugSymbols;\r\n\r\n\tNodeId _origin;\r\n\r\n\t// ---- expressions ------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003ERender an expression as HLSL, parenthesised only where precedence requires it.\u003C/summary\u003E\r\n\tpublic string Expression( IrExpr expr ) =\u003E Expression( expr, PrecedenceLowest );\r\n\r\n\tstring Expression( IrExpr expr, int minPrecedence )\r\n\t{\r\n\t\tif ( expr is null ) return \u00220\u0022;\r\n\r\n\t\tvar (text, precedence) = Render( expr );\r\n\r\n\t\treturn precedence \u003C minPrecedence ? $\u0022( {text} )\u0022 : text;\r\n\t}\r\n\r\n\t(string Text, int Precedence) Render( IrExpr expr )\r\n\t{\r\n\t\tswitch ( expr )\r\n\t\t{\r\n\t\t\tcase IrConst c:\r\n\t\t\t\treturn ( HlslIntrinsics.Literal( c.Type, c.Value ), PrecedencePrimary );\r\n\r\n\t\t\tcase IrVar v:\r\n\t\t\t\treturn ( v.Name ?? \u00220\u0022, PrecedencePrimary );\r\n\r\n\t\t\t// Sanitised to match SboxMaterialBinding.Declare: the block parser is ASCII only, so a\r\n\t\t\t// declaration and every reference to it have to agree on the same renamed spelling.\r\n\t\t\tcase IrGlobalRef g:\r\n\t\t\t\treturn ( SboxShaderTemplates.SafeIdentifier( g.Decl?.Name ) ?? \u00220\u0022, PrecedencePrimary );\r\n\r\n\t\t\tcase IrBuiltinRef b:\r\n\t\t\t\treturn ( RenderBuiltin( b ), PrecedencePrimary );\r\n\r\n\t\t\tcase IrCall call:\r\n\t\t\t\treturn ( RenderCall( call ), PrecedencePrimary );\r\n\r\n\t\t\tcase IrHelperCall helper:\r\n\t\t\t\treturn ( RenderHelperCall( helper ), PrecedencePrimary );\r\n\r\n\t\t\tcase IrBinary binary:\r\n\t\t\t\treturn RenderBinary( binary );\r\n\r\n\t\t\tcase IrUnary unary:\r\n\t\t\t{\r\n\t\t\t\tvar symbol = UnaryOps.Symbol( unary.Op );\r\n\t\t\t\tvar operand = Expression( unary.V, PrecedenceUnary );\r\n\r\n\t\t\t\t// A unary operand is not parenthesised \u2014 unary binds tighter than everything below it \u2014\r\n\t\t\t\t// so a nested negate would print \u0060--x\u0060, which DXC and Slang both lex as pre-decrement:\r\n\t\t\t\t// an error on a non-lvalue and a different program on one. \u0060-(-1.0f)\u0060 has the same\r\n\t\t\t\t// shape. A single space separates the two tokens and costs nothing; \u0060!!x\u0060 and \u0060~~x\u0060 are\r\n\t\t\t\t// legal but read better spaced too. Reachable with folding off, which the docs\r\n\t\t\t\t// recommend for bug reports.\r\n\t\t\t\tvar gap = operand.Length \u003E 0 \u0026\u0026 operand[0] == symbol[0] ? \u0022 \u0022 : string.Empty;\r\n\r\n\t\t\t\treturn ( $\u0022{symbol}{gap}{operand}\u0022, PrecedenceUnary );\r\n\t\t\t}\r\n\r\n\t\t\tcase IrSwizzle swizzle:\r\n\t\t\t\treturn ( $\u0022{Expression( swizzle.V, PrecedencePostfix )}.{HlslIntrinsics.NormalizeSwizzle( swizzle.Mask )}\u0022,\r\n\t\t\t\t\tPrecedencePostfix );\r\n\r\n\t\t\tcase IrConstruct construct:\r\n\t\t\t\treturn ( RenderConstruct( construct ), PrecedencePrimary );\r\n\r\n\t\t\tcase IrCast cast:\r\n\t\t\t\treturn RenderCast( cast );\r\n\r\n\t\t\tcase IrSelect select:\r\n\t\t\t\treturn ( $\u0022select( {Expression( select.C )}, {Expression( select.A )}, {Expression( select.B )} )\u0022,\r\n\t\t\t\t\tPrecedencePrimary );\r\n\r\n\t\t\tcase IrIndex index:\r\n\t\t\t\treturn ( $\u0022{Expression( index.V, PrecedencePostfix )}[{Expression( index.I )}]\u0022, PrecedencePostfix );\r\n\r\n\t\t\tcase IrMember member:\r\n\t\t\t\treturn ( $\u0022{Expression( member.V, PrecedencePostfix )}.{member.Field}\u0022, PrecedencePostfix );\r\n\r\n\t\t\tdefault:\r\n\t\t\t\tReport( DiagnosticSeverity.Error, DiagnosticCode.NodeEmitFailed,\r\n\t\t\t\t\t$\u0022The HLSL backend does not know how to write a {expr.GetType().Name}.\u0022 );\r\n\t\t\t\treturn ( HlslIntrinsics.Fallback( expr.Type ), PrecedencePrimary );\r\n\t\t}\r\n\t}\r\n\r\n\tstring RenderBuiltin( IrBuiltinRef builtin )\r\n\t{\r\n\t\tvar text = HlslIntrinsics.BuiltinExpression( builtin.Id, Stage, Domain );\r\n\r\n\t\tif ( !string.IsNullOrEmpty( text ) ) return text;\r\n\r\n\t\tReport( DiagnosticSeverity.Error, DiagnosticCode.BackendUnsupported,\r\n\t\t\t$\u0022\u0027{builtin.Id}\u0027 has no representation in the {Stage.DisplayName().ToLowerInvariant()} stage of a {Domain} shader.\u0022,\r\n\t\t\t$\u0022The s\u0026box shader environment provides no expression for it here. Compute the value where it exists and pass it through a varying, or bind it as a render attribute.\u0022 );\r\n\r\n\t\treturn HlslIntrinsics.Fallback( builtin.Type );\r\n\t}\r\n\r\n\tstring RenderCall( IrCall call )\r\n\t{\r\n\t\tvar id = call.Id;\r\n\t\tvar args = call.Args ?? Array.Empty\u003CIrExpr\u003E();\r\n\t\tvar rendered = new string[args.Length];\r\n\t\tvar types = new ShaderType[args.Length];\r\n\r\n\t\tfor ( int i = 0; i \u003C args.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\trendered[i] = Expression( args[i] );\r\n\t\t\ttypes[i] = args[i]?.Type ?? ShaderType.Void;\r\n\t\t}\r\n\r\n\t\tvar info = IntrinsicCatalog.Get( id );\r\n\r\n\t\tif ( !info.IsAvailableOnTarget )\r\n\t\t{\r\n\t\t\tReport( DiagnosticSeverity.Error, DiagnosticCode.ShaderModelTooHigh,\r\n\t\t\t\t$\u0022\u0027{info.Name}\u0027 requires SM {info.MinShaderModel}; s\u0026box compiles at SM {ShaderModel.Target} (Vulkan).\u0022,\r\n\t\t\t\tinfo.Description );\r\n\t\t}\r\n\t\telse if ( !IntrinsicCatalog.IsLegalIn( id, Stage ) )\r\n\t\t{\r\n\t\t\tif ( HlslIntrinsics.TryLowerForStage( id, Stage, out var lowered ) )\r\n\t\t\t{\r\n\t\t\t\tReport( DiagnosticSeverity.Info, DiagnosticCode.SampleLowered,\r\n\t\t\t\t\t$\u0022\u0027{info.Name}\u0027 was lowered to \u0027{IntrinsicCatalog.Name( lowered )}\u0027 because the {Stage.DisplayName().ToLowerInvariant()} stage has no screen-space derivatives.\u0022,\r\n\t\t\t\t\t\u0022Mip selection falls back to level 0. Feed an explicit LOD if that is not what you want.\u0022 );\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tReport( DiagnosticSeverity.Error, DiagnosticCode.StageViolation,\r\n\t\t\t\t\t$\u0022\u0027{info.Name}\u0027 is not legal in the {Stage.DisplayName().ToLowerInvariant()} stage.\u0022,\r\n\t\t\t\t\t\u0022There is no meaning-preserving substitute. Move the operation to the pixel stage.\u0022 );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( !info.AcceptsArity( args.Length ) )\r\n\t\t{\r\n\t\t\tReport( DiagnosticSeverity.Error, DiagnosticCode.NodeEmitFailed,\r\n\t\t\t\t$\u0022\u0027{info.Name}\u0027 takes {info.MinArgs}..{info.MaxArgs} arguments but was given {args.Length}.\u0022 );\r\n\t\t}\r\n\r\n\t\treturn HlslIntrinsics.Call( id, rendered, types, Stage, Dialect );\r\n\t}\r\n\r\n\tstring RenderHelperCall( IrHelperCall call )\r\n\t{\r\n\t\tvar args = call.Args ?? Array.Empty\u003CIrExpr\u003E();\r\n\r\n\t\tif ( call.Fn is null )\r\n\t\t{\r\n\t\t\tReport( DiagnosticSeverity.Error, DiagnosticCode.NodeEmitFailed, \u0022A helper call has no helper attached.\u0022 );\r\n\t\t\treturn HlslIntrinsics.Fallback( call.Type );\r\n\t\t}\r\n\r\n\t\tif ( args.Length == 0 ) return $\u0022{call.Fn.Name}()\u0022;\r\n\r\n\t\tvar parts = new string[args.Length];\r\n\r\n\t\tfor ( int i = 0; i \u003C args.Length; i\u002B\u002B ) parts[i] = Expression( args[i] );\r\n\r\n\t\treturn $\u0022{call.Fn.Name}( {string.Join( \u0022, \u0022, parts )} )\u0022;\r\n\t}\r\n\r\n\t(string Text, int Precedence) RenderBinary( IrBinary binary )\r\n\t{\r\n\t\tvar precedence = BinaryOps.Precedence( binary.Op );\r\n\r\n\t\tif ( BinaryOps.IsShortCircuit( binary.Op ) \u0026\u0026 !( binary.L?.Type.IsScalar ?? true ) )\r\n\t\t{\r\n\t\t\t// \u0026\u0026 and || short-circuit and therefore never work component-wise. The IR is supposed to\r\n\t\t\t// use Intrinsic.AndFn / OrFn for vectors; recover instead of emitting silently wrong code.\r\n\t\t\tvar fn = binary.Op == BinaryOp.LogicalAnd ? Intrinsic.AndFn : Intrinsic.OrFn;\r\n\r\n\t\t\tReport( DiagnosticSeverity.Warning, DiagnosticCode.BackendUnsupported,\r\n\t\t\t\t$\u0022\u0027{BinaryOps.Symbol( binary.Op )}\u0027 short-circuits and cannot be applied component-wise; emitted \u0027{HlslIntrinsics.Spelling( fn )}\u0027 instead.\u0022 );\r\n\r\n\t\t\treturn ( $\u0022{HlslIntrinsics.Spelling( fn )}( {Expression( binary.L )}, {Expression( binary.R )} )\u0022,\r\n\t\t\t\tPrecedencePrimary );\r\n\t\t}\r\n\r\n\t\tvar left = Expression( binary.L, precedence );\r\n\t\tvar right = Expression( binary.R, precedence \u002B 1 );\r\n\r\n\t\treturn ( $\u0022{left} {BinaryOps.Symbol( binary.Op )} {right}\u0022, precedence );\r\n\t}\r\n\r\n\tstring RenderConstruct( IrConstruct construct )\r\n\t{\r\n\t\tvar parts = construct.Parts ?? Array.Empty\u003CIrExpr\u003E();\r\n\t\tvar rendered = new string[parts.Length];\r\n\r\n\t\tfor ( int i = 0; i \u003C parts.Length; i\u002B\u002B ) rendered[i] = Expression( parts[i] );\r\n\r\n\t\tif ( construct.Type.IsStruct )\r\n\t\t{\r\n\t\t\tif ( parts.Length == 0 ) return $\u0022( {construct.Type.Hlsl} )0\u0022;\r\n\r\n\t\t\t// Slang gives every struct a synthesised constructor, which composes anywhere an expression\r\n\t\t\t// can appear. Plain HLSL only has the initialiser list, which is legal solely as the\r\n\t\t\t// right-hand side of a declaration \u2014 the one place the IR ever builds a struct.\r\n\t\t\treturn Dialect == HlslDialect.SboxSlang\r\n\t\t\t\t? $\u0022{construct.Type.Hlsl}( {string.Join( \u0022, \u0022, rendered )} )\u0022\r\n\t\t\t\t: $\u0022{{ {string.Join( \u0022, \u0022, rendered )} }}\u0022;\r\n\t\t}\r\n\r\n\t\tif ( parts.Length == 0 ) return HlslIntrinsics.Fallback( construct.Type );\r\n\r\n\t\treturn $\u0022{construct.Type.Hlsl}( {string.Join( \u0022, \u0022, rendered )} )\u0022;\r\n\t}\r\n\r\n\t(string Text, int Precedence) RenderCast( IrCast cast )\r\n\t{\r\n\t\tvar source = cast.V?.Type ?? ShaderType.Void;\r\n\t\tvar target = cast.Type;\r\n\r\n\t\tswitch ( cast.Kind )\r\n\t\t{\r\n\t\t\tcase CastKind.Bitcast:\r\n\t\t\t\tvar reinterpret = target.Scalar switch\r\n\t\t\t\t{\r\n\t\t\t\t\tScalarKind.Int =\u003E Intrinsic.AsInt,\r\n\t\t\t\t\tScalarKind.UInt =\u003E Intrinsic.AsUint,\r\n\t\t\t\t\t_ =\u003E Intrinsic.AsFloat\r\n\t\t\t\t};\r\n\r\n\t\t\t\treturn ( $\u0022{IntrinsicCatalog.Name( reinterpret )}( {Expression( cast.V )} )\u0022, PrecedencePrimary );\r\n\r\n\t\t\tcase CastKind.Truncate when source.IsScalarOrVector \u0026\u0026 target.IsScalarOrVector \u0026\u0026\r\n\t\t\t\t\t\t\t\t\t\ttarget.Components \u003C source.Components \u0026\u0026\r\n\t\t\t\t\t\t\t\t\t\ttarget.Scalar == source.Scalar:\r\n\t\t\t\treturn ( $\u0022{Expression( cast.V, PrecedencePostfix )}.{HlslIntrinsics.LeadingMask( target.Components )}\u0022,\r\n\t\t\t\t\tPrecedencePostfix );\r\n\r\n\t\t\tcase CastKind.Pad when source.IsScalarOrVector \u0026\u0026 target.IsScalarOrVector \u0026\u0026\r\n\t\t\t\t\t\t\t\t   target.Components \u003E source.Components:\r\n\t\t\t\tvar padded = new List\u003Cstring\u003E( target.Components ) { Expression( cast.V ) };\r\n\r\n\t\t\t\tfor ( int i = source.Components; i \u003C target.Components; i\u002B\u002B )\r\n\t\t\t\t{\r\n\t\t\t\t\tpadded.Add( HlslIntrinsics.Number( cast.Fill, target.Scalar ) );\r\n\t\t\t\t}\r\n\r\n\t\t\t\treturn ( $\u0022{target.Hlsl}( {string.Join( \u0022, \u0022, padded )} )\u0022, PrecedencePrimary );\r\n\r\n\t\t\tdefault:\r\n\t\t\t\treturn ( $\u0022( {target.Hlsl} ){Expression( cast.V, PrecedenceUnary )}\u0022, PrecedenceUnary );\r\n\t\t}\r\n\t}\r\n\r\n\t// ---- statements -------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003EWrite a block\u0027s statements at the builder\u0027s current indentation.\u003C/summary\u003E\r\n\tpublic void WriteStatements( HlslSourceBuilder builder, IrBlock block )\r\n\t{\r\n\t\tif ( builder is null || block is null ) return;\r\n\r\n\t\tforeach ( var statement in block.Statements ) WriteStatement( builder, statement );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EWrite a braced block.\u003C/summary\u003E\r\n\tpublic void WriteBracedBlock( HlslSourceBuilder builder, IrBlock block, NodeId origin )\r\n\t{\r\n\t\tbuilder.Open( origin );\r\n\t\tWriteStatements( builder, block );\r\n\t\tbuilder.Close( origin: origin );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EWrite one statement, recording every line it produces against its originating node.\u003C/summary\u003E\r\n\tpublic void WriteStatement( HlslSourceBuilder builder, IrStmt statement )\r\n\t{\r\n\t\tif ( builder is null || statement is null ) return;\r\n\r\n\t\tvar previous = _origin;\r\n\t\t_origin = statement.Origin;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tswitch ( statement )\r\n\t\t\t{\r\n\t\t\t\tcase IrDecl decl:\r\n\t\t\t\t\tbuilder.Write( decl.Init is null\r\n\t\t\t\t\t\t? $\u0022{decl.Type.Hlsl} {decl.Name};\u0022\r\n\t\t\t\t\t\t: $\u0022{decl.Type.Hlsl} {decl.Name} = {Expression( decl.Init )};\u0022, decl.Origin );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase IrAssign assign:\r\n\t\t\t\t\tbuilder.Write( $\u0022{Expression( assign.Target )} = {Expression( assign.Value )};\u0022, assign.Origin );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase IrIf branch:\r\n\t\t\t\t\tbuilder.Write( $\u0022if ( {Expression( branch.Cond )} )\u0022, branch.Origin );\r\n\t\t\t\t\tWriteBracedBlock( builder, branch.Then, branch.Origin );\r\n\r\n\t\t\t\t\tif ( branch.Else is { IsEmpty: false } )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tbuilder.Write( \u0022else\u0022, branch.Origin );\r\n\t\t\t\t\t\tWriteBracedBlock( builder, branch.Else, branch.Origin );\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase IrFor loop:\r\n\t\t\t\t\tvar counter = string.IsNullOrEmpty( loop.Var ) ? \u0022n\u0022 : loop.Var;\r\n\r\n\t\t\t\t\tbuilder.Write(\r\n\t\t\t\t\t\t$\u0022for ( int {counter} = 0; {counter} \u003C ( int )( {Expression( loop.Count )} ); {counter}\u002B\u002B )\u0022,\r\n\t\t\t\t\t\tloop.Origin );\r\n\t\t\t\t\tWriteBracedBlock( builder, loop.Body, loop.Origin );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase IrWhile loop:\r\n\t\t\t\t\tbuilder.Write( $\u0022while ( {Expression( loop.Cond )} )\u0022, loop.Origin );\r\n\t\t\t\t\tWriteBracedBlock( builder, loop.Body, loop.Origin );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase IrBreak:\r\n\t\t\t\t\tbuilder.Write( \u0022break;\u0022, statement.Origin );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase IrContinue:\r\n\t\t\t\t\tbuilder.Write( \u0022continue;\u0022, statement.Origin );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase IrReturn ret:\r\n\t\t\t\t\tbuilder.Write( ret.Value is null ? \u0022return;\u0022 : $\u0022return {Expression( ret.Value )};\u0022, ret.Origin );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase IrDiscard:\r\n\t\t\t\t\tif ( !Stage.CanDiscard() )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tReport( DiagnosticSeverity.Error, DiagnosticCode.StageViolation,\r\n\t\t\t\t\t\t\t$\u0022A fragment can only be discarded in the pixel stage, not in the {Stage.DisplayName().ToLowerInvariant()} stage.\u0022 );\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tbuilder.Write( \u0022discard;\u0022, statement.Origin );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase IrExprStmt expression:\r\n\t\t\t\t\tbuilder.Write( $\u0022{Expression( expression.Value )};\u0022, expression.Origin );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase IrComment comment:\r\n\t\t\t\t\tif ( !WantsComments || string.IsNullOrWhiteSpace( comment.Text ) ) break;\r\n\r\n\t\t\t\t\tforeach ( var line in comment.Text.Replace( \u0022\\r\\n\u0022, \u0022\\n\u0022 ).Split( \u0027\\n\u0027 ) )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tbuilder.Write( $\u0022// {line.Trim()}\u0022, comment.Origin );\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase IrScope scope:\r\n\t\t\t\t\tWriteBracedBlock( builder, scope.Body, scope.Origin );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase IrPreprocessorIf guard:\r\n\t\t\t\t{\r\n\t\t\t\t\t// The .shader writer handles guards itself; this is the standalone emit the text\r\n\t\t\t\t\t// editor probe-compiles, and it has to produce the same directives rather than\r\n\t\t\t\t\t// reporting the statement as one the backend does not understand.\r\n\t\t\t\t\tvar directive = IrPreprocessor.OpenDirective( guard.Condition );\r\n\r\n\t\t\t\t\tif ( string.IsNullOrEmpty( directive ) )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\t// An unusable condition must not become a directive the preprocessor rejects: a\r\n\t\t\t\t\t\t// preprocessor error has no line that maps back to a node. Emitting both sides\r\n\t\t\t\t\t\t// unguarded keeps the shader compiling and costs only the exclusion.\r\n\t\t\t\t\t\tReport( DiagnosticSeverity.Warning, DiagnosticCode.InvalidBlock,\r\n\t\t\t\t\t\t\t\u0022A compile-time branch had no usable combo condition, so both of its sides were emitted.\u0022,\r\n\t\t\t\t\t\t\t\u0022Bind the node to a graph keyword. Without one there is nothing for the preprocessor to test, and the branch cannot be resolved at compile time.\u0022 );\r\n\r\n\t\t\t\t\t\tWriteStatements( builder, guard.Then );\r\n\t\t\t\t\t\tWriteStatements( builder, guard.Else );\r\n\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tbuilder.Write( directive, guard.Origin );\r\n\t\t\t\t\tWriteStatements( builder, guard.Then );\r\n\r\n\t\t\t\t\tif ( guard.HasElse )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tbuilder.Write( IrPreprocessor.ElseDirective, guard.Origin );\r\n\t\t\t\t\t\tWriteStatements( builder, guard.Else );\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tbuilder.Write( IrPreprocessor.EndDirective, guard.Origin );\r\n\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tdefault:\r\n\t\t\t\t\tReport( DiagnosticSeverity.Error, DiagnosticCode.NodeEmitFailed,\r\n\t\t\t\t\t\t$\u0022The HLSL backend does not know how to write a {statement.GetType().Name}.\u0022 );\r\n\t\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t}\r\n\t\tfinally\r\n\t\t{\r\n\t\t\t_origin = previous;\r\n\t\t}\r\n\t}\r\n\r\n\t// ---- declarations -----------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The module\u0027s helpers in dependency order, deduplicated by name.\r\n\t/// \u003Cpara\u003E\r\n\t/// A same-name, different-body collision is a hard error naming both bodies, unlike the built-in\r\n\t/// editor\u0027s process-global function table which silently keeps whichever registered first.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tpublic IReadOnlyList\u003CHelperFunction\u003E OrderedHelpers()\r\n\t{\r\n\t\tvar ordered = new List\u003CHelperFunction\u003E();\r\n\r\n\t\tif ( Module is null ) return ordered;\r\n\r\n\t\tvar accepted = new Dictionary\u003Cstring, HelperFunction\u003E( StringComparer.Ordinal );\r\n\t\tvar visiting = new HashSet\u003Cstring\u003E( StringComparer.Ordinal );\r\n\r\n\t\tforeach ( var helper in Module.Helpers ) Visit( helper );\r\n\r\n\t\treturn ordered;\r\n\r\n\t\tvoid Visit( HelperFunction helper )\r\n\t\t{\r\n\t\t\tif ( helper is null || string.IsNullOrWhiteSpace( helper.Name ) ) return;\r\n\r\n\t\t\tif ( accepted.TryGetValue( helper.Name, out var existing ) )\r\n\t\t\t{\r\n\t\t\t\tif ( existing.ConflictsWith( helper ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tReport( DiagnosticSeverity.Error, DiagnosticCode.HelperCollision,\r\n\t\t\t\t\t\t$\u0022Two different helper functions are both named \u0027{helper.Name}\u0027.\u0022,\r\n\t\t\t\t\t\t$\u0022Signatures: \u0027{existing.SignatureHlsl}\u0027 and \u0027{helper.SignatureHlsl}\u0027. Helper names are the deduplication key, so they must be unique per module.\u0022 );\r\n\t\t\t\t}\r\n\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\tif ( !visiting.Add( helper.Name ) )\r\n\t\t\t{\r\n\t\t\t\tReport( DiagnosticSeverity.Error, DiagnosticCode.HelperCollision,\r\n\t\t\t\t\t$\u0022Helper function \u0027{helper.Name}\u0027 depends on itself.\u0022,\r\n\t\t\t\t\t\u0022Helper requirement chains must form a directed acyclic graph.\u0022 );\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\tforeach ( var requirement in helper.Requires ?? Array.Empty\u003CHelperFunction\u003E() ) Visit( requirement );\r\n\r\n\t\t\tvisiting.Remove( helper.Name );\r\n\t\t\taccepted[helper.Name] = helper;\r\n\t\t\tordered.Add( helper );\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The helpers a stage\u0027s own code can actually reach, transitively.\r\n\t/// \u003Cpara\u003E\r\n\t/// \u003Csee cref=\u0022HelperFunction.Stages\u0022/\u003E says where a helper \u003Ci\u003Emay\u003C/i\u003E be written, not where it is\r\n\t/// wanted: a pure-maths helper declares \u003Csee cref=\u0022StageMask.All\u0022/\u003E and would otherwise be emitted\r\n\t/// into the vertex program of every graph whose pixel program happens to call it. DXC drops the dead\r\n\t/// code, but Prism ships a viewer for the generated text, and hundreds of lines of functions the\r\n\t/// program never calls is the difference between a shader a person can read and one they cannot.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tpublic HashSet\u003Cstring\u003E ReachableHelpers( ShaderStage stage, IReadOnlyList\u003CHelperFunction\u003E helpers )\r\n\t{\r\n\t\tvar reachable = new HashSet\u003Cstring\u003E( StringComparer.Ordinal );\r\n\r\n\t\tif ( Module is null || helpers is null || helpers.Count == 0 ) return reachable;\r\n\r\n\t\tvar byName = new Dictionary\u003Cstring, HelperFunction\u003E( StringComparer.Ordinal );\r\n\r\n\t\tforeach ( var helper in helpers )\r\n\t\t{\r\n\t\t\tif ( !string.IsNullOrEmpty( helper?.Name ) ) byName[helper.Name] = helper;\r\n\t\t}\r\n\r\n\t\tforeach ( var function in Module.Functions )\r\n\t\t{\r\n\t\t\tif ( function is null ) continue;\r\n\r\n\t\t\tvar belongs = function.IsEntryPoint\r\n\t\t\t\t? function.Stage == stage\r\n\t\t\t\t: function.Stage == ShaderStage.None || function.Stage == stage;\r\n\r\n\t\t\tif ( !belongs ) continue;\r\n\r\n\t\t\tforeach ( var statement in WalkStatements( function.Body ) )\r\n\t\t\t{\r\n\t\t\t\tforeach ( var expression in StatementExpressions( statement ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tforeach ( var node in IrExprUtil.Walk( expression ) )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tif ( node is IrHelperCall call \u0026\u0026 !string.IsNullOrEmpty( call.Fn?.Name ) ) Pull( call.Fn.Name );\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn reachable;\r\n\r\n\t\tvoid Pull( string name )\r\n\t\t{\r\n\t\t\tif ( !byName.TryGetValue( name, out var helper ) ) return;\r\n\t\t\tif ( !reachable.Add( name ) ) return;\r\n\r\n\t\t\tforeach ( var requirement in helper.Requires ?? Array.Empty\u003CHelperFunction\u003E() )\r\n\t\t\t{\r\n\t\t\t\tif ( !string.IsNullOrEmpty( requirement?.Name ) ) Pull( requirement.Name );\r\n\t\t\t}\r\n\r\n\t\t\t// A helper body is author-supplied text, so one helper calling another need not have been\r\n\t\t\t// declared through Requires. Naming another helper anywhere in the body pulls it in: over-\r\n\t\t\t// including costs one dead function, under-including costs a compile error.\r\n\t\t\tvar body = helper.BodyFor( PrismConstants.BackendHlsl );\r\n\r\n\t\t\tif ( string.IsNullOrEmpty( body ) ) return;\r\n\r\n\t\t\tforeach ( var candidate in byName.Keys.ToArray() )\r\n\t\t\t{\r\n\t\t\t\tif ( reachable.Contains( candidate ) ) continue;\r\n\t\t\t\tif ( body.Contains( candidate, StringComparison.Ordinal ) ) Pull( candidate );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EEvery statement in a block, including the ones nested inside control flow.\u003C/summary\u003E\r\n\tstatic IEnumerable\u003CIrStmt\u003E WalkStatements( IrBlock block )\r\n\t{\r\n\t\tif ( block is null ) yield break;\r\n\r\n\t\tforeach ( var statement in block.Statements )\r\n\t\t{\r\n\t\t\tif ( statement is null ) continue;\r\n\r\n\t\t\tyield return statement;\r\n\r\n\t\t\tIrBlock[] nested = statement switch\r\n\t\t\t{\r\n\t\t\t\tIrIf branch =\u003E [branch.Then, branch.Else],\r\n\t\t\t\tIrFor loop =\u003E [loop.Body],\r\n\t\t\t\tIrWhile loop =\u003E [loop.Body],\r\n\t\t\t\tIrScope scope =\u003E [scope.Body],\r\n\t\t\t\t_ =\u003E null\r\n\t\t\t};\r\n\r\n\t\t\tif ( nested is null ) continue;\r\n\r\n\t\t\tforeach ( var child in nested )\r\n\t\t\t{\r\n\t\t\t\tforeach ( var inner in WalkStatements( child ) ) yield return inner;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe expressions one statement holds directly.\u003C/summary\u003E\r\n\tstatic IEnumerable\u003CIrExpr\u003E StatementExpressions( IrStmt statement )\r\n\t{\r\n\t\tswitch ( statement )\r\n\t\t{\r\n\t\t\tcase IrDecl decl:\r\n\t\t\t\tyield return decl.Init;\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase IrAssign assign:\r\n\t\t\t\tyield return assign.Target;\r\n\t\t\t\tyield return assign.Value;\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase IrIf branch:\r\n\t\t\t\tyield return branch.Cond;\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase IrFor loop:\r\n\t\t\t\tyield return loop.Count;\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase IrWhile loop:\r\n\t\t\t\tyield return loop.Cond;\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase IrReturn returned:\r\n\t\t\t\tyield return returned.Value;\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase IrExprStmt expression:\r\n\t\t\t\tyield return expression.Value;\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EWrite the helper bodies a stage actually calls, in dependency order.\u003C/summary\u003E\r\n\tpublic void WriteHelpers( HlslSourceBuilder builder, ShaderStage stage, IReadOnlyList\u003CHelperFunction\u003E helpers )\r\n\t{\r\n\t\tif ( builder is null || helpers is null ) return;\r\n\r\n\t\tvar reachable = ReachableHelpers( stage, helpers );\r\n\r\n\t\tforeach ( var helper in helpers )\r\n\t\t{\r\n\t\t\tif ( !helper.Stages.Contains( stage ) ) continue;\r\n\t\t\tif ( !reachable.Contains( helper.Name ) ) continue;\r\n\r\n\t\t\tvar body = helper.BodyFor( PrismConstants.BackendHlsl );\r\n\r\n\t\t\tif ( string.IsNullOrWhiteSpace( body ) )\r\n\t\t\t{\r\n\t\t\t\tReport( DiagnosticSeverity.Error, DiagnosticCode.HelperCollision,\r\n\t\t\t\t\t$\u0022Helper function \u0027{helper.Name}\u0027 has no HLSL body.\u0022 );\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( helper.MinShaderModel \u003E ShaderModel.Target )\r\n\t\t\t{\r\n\t\t\t\tReport( DiagnosticSeverity.Error, DiagnosticCode.ShaderModelTooHigh,\r\n\t\t\t\t\t$\u0022Helper function \u0027{helper.Name}\u0027 requires SM {helper.MinShaderModel}; s\u0026box compiles at SM {ShaderModel.Target} (Vulkan).\u0022 );\r\n\t\t\t}\r\n\r\n\t\t\tif ( Dialect == HlslDialect.StrictHlsl2021 ) CheckStrictDialect( helper, body );\r\n\r\n\t\t\tbuilder.WriteBlock( body );\r\n\t\t\tbuilder.Blank();\r\n\t\t}\r\n\t}\r\n\r\n\tstatic readonly string[] s_slangOnlySyntax =\r\n\t[\r\n\t\t\u0022[mutating]\u0022, \u0022__init\u0022, \u0022extension \u0022, \u0022interface \u0022, \u0022associatedtype\u0022, \u0022no_diff\u0022, \u0022__generic\u0022,\r\n\t\t\u0022property \u0022, \u0022[ForceInline]\u0022, \u0022[Differentiable]\u0022\r\n\t];\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Warn about Slang-only syntax in a helper body when the graph asked for portable HLSL 2021.\r\n\t/// \u003Cpara\u003E\r\n\t/// Prism\u0027s own emission already avoids these constructs in the strict dialect; a helper\u0027s body is\r\n\t/// author-supplied text, so the best we can do is name the construct rather than let DXC reject it\r\n\t/// with a message pointing at a generated line.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tvoid CheckStrictDialect( HelperFunction helper, string body )\r\n\t{\r\n\t\tforeach ( var syntax in s_slangOnlySyntax )\r\n\t\t{\r\n\t\t\tif ( body.IndexOf( syntax, StringComparison.Ordinal ) \u003C 0 ) continue;\r\n\r\n\t\t\tReport( DiagnosticSeverity.Warning, DiagnosticCode.BackendUnsupported,\r\n\t\t\t\t$\u0022Helper function \u0027{helper.Name}\u0027 uses the Slang-only construct \u0027{syntax.Trim()}\u0027, but this graph targets strict HLSL 2021.\u0022,\r\n\t\t\t\t\u0022Either rewrite the helper in plain HLSL or switch the graph\u0027s dialect back to s\u0026box Slang, which the engine\u0027s own headers already require.\u0022 );\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EWrite the non-entry-point functions the module carries for a stage.\u003C/summary\u003E\r\n\tpublic void WriteFunctions( HlslSourceBuilder builder, ShaderStage stage )\r\n\t{\r\n\t\tif ( builder is null || Module is null ) return;\r\n\r\n\t\tvar previous = Stage;\r\n\t\tStage = stage;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tforeach ( var function in Module.Functions )\r\n\t\t\t{\r\n\t\t\t\tif ( function is null || function.IsEntryPoint ) continue;\r\n\t\t\t\tif ( function.Stage != ShaderStage.None \u0026\u0026 function.Stage != stage ) continue;\r\n\r\n\t\t\t\tforeach ( var attribute in function.Attributes ) builder.Write( attribute );\r\n\r\n\t\t\t\tbuilder.Write( function.SignatureHlsl );\r\n\t\t\t\tbuilder.Open();\r\n\t\t\t\tWriteStatements( builder, function.Body );\r\n\t\t\t\tbuilder.Close();\r\n\t\t\t\tbuilder.Blank();\r\n\t\t\t}\r\n\t\t}\r\n\t\tfinally\r\n\t\t{\r\n\t\t\tStage = previous;\r\n\t\t}\r\n\t}\r\n\r\n\t// ---- diagnostics ------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Report a problem against the node currently being written, once per distinct message. A backend\r\n\t/// never throws for user error and never floods the panel with one repeated line.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void Report( DiagnosticSeverity severity, string code, string message, string detail = null )\r\n\t{\r\n\t\tvar key = $\u0022{code}|{message}|{_origin}\u0022;\r\n\r\n\t\tif ( !_reported.Add( key ) ) return;\r\n\r\n\t\tGraphRef? graph = _origin.IsValid ? GraphRef.ForNode( _origin ) : null;\r\n\r\n\t\tDiagnostics.Report( new Diagnostic( severity, code, message, detail, null, graph ) );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe node whose statement is currently being written, for diagnostics attribution.\u003C/summary\u003E\r\n\tpublic NodeId CurrentOrigin\r\n\t{\r\n\t\tget =\u003E _origin;\r\n\t\tset =\u003E _origin = value;\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The s\u0026amp;box HLSL backend: turns an \u003Csee cref=\u0022IrModule\u0022/\u003E into a complete VFX \u003Cc\u003E.shader\u003C/c\u003E\r\n/// file that the engine compiles and the preview renders.\r\n/// \u003Cpara\u003E\r\n/// The heavy lifting is split in two on purpose. \u003Csee cref=\u0022HlslEmitter\u0022/\u003E lowers IR to HLSL\r\n/// declarations and function bodies; \u003Csee cref=\u0022SboxShaderWriter\u0022/\u003E wraps those in the block file.\r\n/// That separation is what lets the same HLSL feed a probe compile, a \u003Cc\u003E.shader\u003C/c\u003E, or a future\r\n/// target, and it keeps the block-file knowledge in one auditable place.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\npublic sealed class HlslBackend : IShaderBackend\r\n{\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tpublic string Id =\u003E PrismConstants.BackendHlsl;\r\n\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tpublic string DisplayName =\u003E \u0022s\u0026box Shader (HLSL / VFX)\u0022;\r\n\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tpublic string FileExtension =\u003E PrismConstants.ShaderExtension;\r\n\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tpublic BackendCapabilities Capabilities =\u003E BackendCapabilities.Sbox;\r\n\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tpublic BackendEmitResult Emit( IrModule module, BackendEmitOptions options, DiagnosticSink diagnostics )\r\n\t{\r\n\t\tdiagnostics ??= new DiagnosticSink();\r\n\t\toptions ??= BackendEmitOptions.Default;\r\n\r\n\t\tif ( module is null )\r\n\t\t{\r\n\t\t\tdiagnostics.Error( DiagnosticCode.InvalidBlock, \u0022There is nothing to emit: the compiler produced no module.\u0022 );\r\n\t\t\treturn BackendEmitResult.Empty( Id, FileExtension );\r\n\t\t}\r\n\r\n\t\treturn PrismLog.Guard( \u0022HlslBackend.Emit\u0022,\r\n\t\t\t() =\u003E new SboxShaderWriter( module, options, diagnostics ).Write(),\r\n\t\t\tBackendEmitResult.Empty( Id, FileExtension ) );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Emit one stage as a plain HLSL translation unit, with no VFX blocks around it.\r\n\t/// \u003Cpara\u003E\r\n\t/// This is what the text editor\u0027s probe compiler and the IR debug view want: declarations, helper\r\n\t/// bodies and the entry point, in a form a bare DXC or slangc invocation can read.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tpublic string EmitStandalone( IrModule module, ShaderStage stage, BackendEmitOptions options,\r\n\t\tDiagnosticSink diagnostics )\r\n\t{\r\n\t\tdiagnostics ??= new DiagnosticSink();\r\n\t\toptions ??= BackendEmitOptions.Default;\r\n\r\n\t\tif ( module is null ) return string.Empty;\r\n\r\n\t\treturn PrismLog.Guard( \u0022HlslBackend.EmitStandalone\u0022, () =\u003E\r\n\t\t{\r\n\t\t\tvar builder = new HlslSourceBuilder( options.Indent, options.NewLine );\r\n\t\t\tvar emitter = new HlslEmitter( module, options, diagnostics ) { Stage = stage };\r\n\r\n\t\t\tforeach ( var include in module.Includes ) builder.Write( $\u0022#include \\\u0022{include}\\\u0022\u0022 );\r\n\r\n\t\t\tif ( module.Includes.Count \u003E 0 ) builder.Blank();\r\n\r\n\t\t\tforeach ( var structure in module.Structs )\r\n\t\t\t{\r\n\t\t\t\tbuilder.Write( $\u0022struct {structure.Name}\u0022 );\r\n\t\t\t\tbuilder.Open();\r\n\r\n\t\t\t\tforeach ( var include in structure.Includes ) builder.Write( $\u0022#include \\\u0022{include}\\\u0022\u0022 );\r\n\r\n\t\t\t\tforeach ( var field in structure.Fields )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar semantic = string.IsNullOrEmpty( field.Semantic ) ? string.Empty : $\u0022 : {field.Semantic}\u0022;\r\n\t\t\t\t\tbuilder.Write( $\u0022{Interpolation( field.Interpolation )}{field.Type.Hlsl} {field.Name}{semantic};\u0022 );\r\n\t\t\t\t}\r\n\r\n\t\t\t\tbuilder.Close( \u0022;\u0022 );\r\n\t\t\t\tbuilder.Blank();\r\n\t\t\t}\r\n\r\n\t\t\tSboxMaterialBinding.WriteGlobals( builder, emitter, stage );\r\n\t\t\temitter.WriteHelpers( builder, stage, emitter.OrderedHelpers() );\r\n\t\t\temitter.WriteFunctions( builder, stage );\r\n\r\n\t\t\tvar entry = module.EntryPoint( stage );\r\n\r\n\t\t\tif ( entry is not null ) WriteStandaloneEntry( builder, emitter, module, entry, stage );\r\n\r\n\t\t\treturn builder.ToString();\r\n\t\t}, string.Empty );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Write an entry point with the fixed signature the engine expects, plus the material prologue\r\n\t/// and tail. The IR\u0027s own signature is deliberately not used: entry-point names, parameters and\r\n\t/// semantics are fixed by the engine, and a probe compile is only useful if the locals the body\r\n\t/// refers to actually exist.\r\n\t/// \u003C/summary\u003E\r\n\tstatic void WriteStandaloneEntry( HlslSourceBuilder builder, HlslEmitter emitter, IrModule module,\r\n\t\tIrFunction entry, ShaderStage stage )\r\n\t{\r\n\t\tforeach ( var attribute in entry.Attributes ) builder.Write( attribute );\r\n\r\n\t\tswitch ( stage )\r\n\t\t{\r\n\t\t\tcase ShaderStage.Vertex:\r\n\t\t\t\tbuilder.Write(\r\n\t\t\t\t\t$\u0022{SboxShaderTemplates.StructPixelInput} {PrismConstants.EntryPointVertex}( {SboxShaderTemplates.StructVertexInput} {SboxShaderTemplates.VertexInputLocal} )\u0022 );\r\n\t\t\t\tbuilder.Open();\r\n\t\t\t\tbuilder.WriteBlock( SboxShaderTemplates.SurfaceVertexPrologue );\r\n\t\t\t\temitter.WriteStatements( builder, entry.Body );\r\n\t\t\t\tbuilder.Write( SboxShaderTemplates.SurfaceVertexEpilogue );\r\n\t\t\t\tbuilder.Close();\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase ShaderStage.Pixel:\r\n\t\t\t\tvar returned = SboxMaterialBinding.EndsWithReturn( entry.Body );\r\n\r\n\t\t\t\tbuilder.Write(\r\n\t\t\t\t\t$\u0022float4 {PrismConstants.EntryPointPixel}( {SboxShaderTemplates.StructPixelInput} {SboxShaderTemplates.PixelInputLocal} ) : SV_Target0\u0022 );\r\n\t\t\t\tbuilder.Open();\r\n\r\n\t\t\t\tif ( !returned ) SboxMaterialBinding.WritePixelPrologue( builder, module );\r\n\r\n\t\t\t\temitter.WriteStatements( builder, entry.Body );\r\n\r\n\t\t\t\tif ( !returned ) SboxMaterialBinding.WritePixelEpilogue( builder, module, emitter );\r\n\r\n\t\t\t\tbuilder.Close();\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase ShaderStage.Compute:\r\n\t\t\t\tif ( entry.Attributes.Count == 0 ) builder.Write( SboxShaderTemplates.ComputeDefaultNumThreads );\r\n\r\n\t\t\t\tbuilder.Write(\r\n\t\t\t\t\t$\u0022void {PrismConstants.EntryPointCompute}( {SboxShaderTemplates.ComputeEntryParameters} )\u0022 );\r\n\t\t\t\tbuilder.Open();\r\n\t\t\t\temitter.WriteStatements( builder, entry.Body );\r\n\t\t\t\tbuilder.Close();\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tdefault:\r\n\t\t\t\tbuilder.Write( entry.SignatureHlsl );\r\n\t\t\t\tbuilder.Open();\r\n\t\t\t\temitter.WriteStatements( builder, entry.Body );\r\n\t\t\t\tbuilder.Close();\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe HLSL interpolation modifier prefix for a field, including its trailing space.\u003C/summary\u003E\r\n\tpublic static string Interpolation( IrInterpolation interpolation ) =\u003E interpolation switch\r\n\t{\r\n\t\tIrInterpolation.NoPerspective =\u003E \u0022noperspective \u0022,\r\n\t\tIrInterpolation.NoInterpolation =\u003E \u0022nointerpolation \u0022,\r\n\t\tIrInterpolation.Centroid =\u003E \u0022centroid \u0022,\r\n\t\tIrInterpolation.Sample =\u003E \u0022sample \u0022,\r\n\t\t_ =\u003E string.Empty\r\n\t};\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/Compiler/Backends/SlangIntrinsics.cs","FileName":"SlangIntrinsics.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using System.Globalization;\r\nusing System.Text;\r\nusing Editor.Prism.Compiler.Ir;\r\nusing Editor.Prism.Core;\r\n\r\nnamespace Editor.Prism.Compiler.Backends;\r\n\r\n/// \u003Csummary\u003E\r\n/// The spelling table that turns Prism\u0027s canonical, backend-independent operations into Slang syntax.\r\n/// \u003Cpara\u003E\r\n/// Everything the \u003Csee cref=\u0022SlangBackend\u0022/\u003E writes goes through here: intrinsic names, operator\r\n/// symbols, type spellings, literals, identifiers and the per-stage lowering of every\r\n/// \u003Csee cref=\u0022Core.ShaderStage\u0022/\u003E-dependent \u003Csee cref=\u0022Compiler.Builtin\u0022/\u003E. Keeping it in one place is\r\n/// what makes the emitted module consistent, and what makes \u0022never emit \u003Cc\u003E?:\u003C/c\u003E on a vector\u0022 a rule\r\n/// the backend cannot accidentally break.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\npublic static class SlangIntrinsics\r\n{\r\n\t// ---- well-known names the emitted module and the prelude agree on -------\r\n\r\n\t/// \u003Csummary\u003EName of the single parameter every graphics entry point takes.\u003C/summary\u003E\r\n\tpublic const string InputParameter = \u0022i\u0022;\r\n\r\n\t/// \u003Csummary\u003EName of the pixel entry point\u0027s \u003Cc\u003ESV_IsFrontFace\u003C/c\u003E parameter.\u003C/summary\u003E\r\n\tpublic const string FrontFaceParameter = \u0022isFrontFace\u0022;\r\n\r\n\t/// \u003Csummary\u003EName of the compute entry point\u0027s \u003Cc\u003ESV_DispatchThreadID\u003C/c\u003E parameter.\u003C/summary\u003E\r\n\tpublic const string DispatchThreadIdParameter = \u0022dispatchThreadId\u0022;\r\n\r\n\t/// \u003Csummary\u003EName of the compute entry point\u0027s \u003Cc\u003ESV_GroupThreadID\u003C/c\u003E parameter.\u003C/summary\u003E\r\n\tpublic const string GroupThreadIdParameter = \u0022groupThreadId\u0022;\r\n\r\n\t/// \u003Csummary\u003EName of the compute entry point\u0027s \u003Cc\u003ESV_GroupID\u003C/c\u003E parameter.\u003C/summary\u003E\r\n\tpublic const string GroupIdParameter = \u0022groupId\u0022;\r\n\r\n\t/// \u003Csummary\u003EName of the generated vertex input struct.\u003C/summary\u003E\r\n\tpublic const string VertexInputStruct = \u0022VsIn\u0022;\r\n\r\n\t/// \u003Csummary\u003EName of the generated vertex output / pixel input struct.\u003C/summary\u003E\r\n\tpublic const string VertexOutputStruct = \u0022VsOut\u0022;\r\n\r\n\t/// \u003Csummary\u003EName of the environment parameter block declared by the \u003Cc\u003Eprism.core\u003C/c\u003E prelude.\u003C/summary\u003E\r\n\tpublic const string EnvironmentBlock = \u0022gPrismEnv\u0022;\r\n\r\n\t/// \u003Csummary\u003EPrefix given to the locals an entry point prologue declares for builtins.\u003C/summary\u003E\r\n\tpublic const string LocalPrefix = \u0022prism\u0022;\r\n\r\n\t// ---- intrinsics --------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The Slang spelling of a canonical intrinsic. Falls back to the HLSL spelling in\r\n\t/// \u003Csee cref=\u0022IntrinsicCatalog\u0022/\u003E, because Slang accepts the whole HLSL intrinsic surface.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string Name( Intrinsic id ) =\u003E id switch\r\n\t{\r\n\t\t// Slang spells the centroid evaluator the DXC way.\r\n\t\tIntrinsic.EvaluateAttributeCentroid =\u003E \u0022EvaluateAttributeAtCentroid\u0022,\r\n\r\n\t\t// GetDimensions is an out-parameter method in Slang and cannot appear in an expression,\r\n\t\t// so the prelude provides a value-returning wrapper instead.\r\n\t\tIntrinsic.TextureSize =\u003E \u0022PrismTextureSize\u0022,\r\n\r\n\t\t// Component-wise logic. Never \u0060\u0026\u0026\u0060 / \u0060||\u0060, which only short-circuit for scalars.\r\n\t\tIntrinsic.AndFn =\u003E \u0022and\u0022,\r\n\t\tIntrinsic.OrFn =\u003E \u0022or\u0022,\r\n\r\n\t\t_ =\u003E IntrinsicCatalog.Name( id )\r\n\t};\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// True when the intrinsic is a method on its first argument \u2014 \u003Cc\u003Etex.Sample( s, uv )\u003C/c\u003E rather\r\n\t/// than \u003Cc\u003ESample( tex, s, uv )\u003C/c\u003E.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static bool IsObjectMethod( Intrinsic id ) =\u003E id is\r\n\t\tIntrinsic.Sample or Intrinsic.SampleLevel or Intrinsic.SampleBias or Intrinsic.SampleGrad or\r\n\t\tIntrinsic.SampleCmp or Intrinsic.SampleCmpLevelZero or\r\n\t\tIntrinsic.Gather or Intrinsic.GatherRed or Intrinsic.GatherGreen or Intrinsic.GatherBlue or\r\n\t\tIntrinsic.GatherAlpha or Intrinsic.GatherCmp or\r\n\t\tIntrinsic.Load or\r\n\t\tIntrinsic.CalculateLevelOfDetail or Intrinsic.CalculateLevelOfDetailUnclamped;\r\n\r\n\t/// \u003Csummary\u003ETrue when the operation is provided by the emitted \u003Cc\u003Eprism.core\u003C/c\u003E prelude.\u003C/summary\u003E\r\n\tpublic static bool IsPreludeHelper( Intrinsic id ) =\u003E id is Intrinsic.TextureSize;\r\n\r\n\t/// \u003Csummary\u003ETrue when the intrinsic writes through an \u003Cc\u003Eout\u003C/c\u003E parameter and is a statement, not a value.\u003C/summary\u003E\r\n\tpublic static bool IsVoidResult( Intrinsic id ) =\u003E id is\r\n\t\tIntrinsic.SinCos or Intrinsic.Clip or\r\n\t\tIntrinsic.AllMemoryBarrier or Intrinsic.AllMemoryBarrierWithGroupSync or\r\n\t\tIntrinsic.DeviceMemoryBarrier or Intrinsic.DeviceMemoryBarrierWithGroupSync or\r\n\t\tIntrinsic.GroupMemoryBarrier or Intrinsic.GroupMemoryBarrierWithGroupSync or\r\n\t\tIntrinsic.InterlockedAdd or Intrinsic.InterlockedMin or Intrinsic.InterlockedMax or\r\n\t\tIntrinsic.InterlockedAnd or Intrinsic.InterlockedOr or Intrinsic.InterlockedXor or\r\n\t\tIntrinsic.InterlockedExchange or Intrinsic.InterlockedCompareExchange or\r\n\t\tIntrinsic.InterlockedCompareStore;\r\n\r\n\t// ---- operators ---------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003EThe Slang symbol for a binary operator.\u003C/summary\u003E\r\n\tpublic static string Symbol( BinaryOp op ) =\u003E BinaryOps.Symbol( op );\r\n\r\n\t/// \u003Csummary\u003EThe Slang symbol for a unary operator.\u003C/summary\u003E\r\n\tpublic static string Symbol( UnaryOp op ) =\u003E UnaryOps.Symbol( op );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// True when an operator must be written in function form instead of symbol form.\r\n\t/// \u003Cpara\u003E\r\n\t/// \u003Cc\u003E\u0026amp;\u0026amp;\u003C/c\u003E and \u003Cc\u003E||\u003C/c\u003E only short-circuit for scalar operands; on a vector Slang\r\n\t/// evaluates both sides and warns. The core module\u0027s \u003Cc\u003Eand()\u003C/c\u003E / \u003Cc\u003Eor()\u003C/c\u003E are the\r\n\t/// component-wise spellings, so that is what we emit.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tpublic static bool RequiresFunctionForm( BinaryOp op, ShaderType operandType ) =\u003E\r\n\t\tBinaryOps.IsShortCircuit( op ) \u0026\u0026 !operandType.IsScalar \u0026\u0026 !operandType.IsVoid;\r\n\r\n\t/// \u003Csummary\u003EThe function spelling of a short-circuit operator, for component-wise use.\u003C/summary\u003E\r\n\tpublic static string FunctionForm( BinaryOp op ) =\u003E op == BinaryOp.LogicalOr ? \u0022or\u0022 : \u0022and\u0022;\r\n\r\n\t// ---- types -------------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Element type given to a buffer whose element type the IR does not carry. Slang\u0027s buffer types\r\n\t/// are generic with no default, unlike its textures, so a spelling has to be chosen.\r\n\t/// \u003C/summary\u003E\r\n\tpublic const string DefaultBufferElement = \u0022float4\u0022;\r\n\r\n\t/// \u003Csummary\u003EThe Slang spelling of a type.\u003C/summary\u003E\r\n\tpublic static string TypeName( ShaderType type )\r\n\t{\r\n\t\tif ( type.IsObject )\r\n\t\t{\r\n\t\t\tswitch ( type.Object )\r\n\t\t\t{\r\n\t\t\t\tcase ObjectKind.Buffer:\r\n\t\t\t\tcase ObjectKind.StructuredBuffer:\r\n\t\t\t\tcase ObjectKind.RWBuffer:\r\n\t\t\t\tcase ObjectKind.RWStructuredBuffer:\r\n\t\t\t\t\treturn $\u0022{ShaderType.ObjectName( type.Object )}\u003C{DefaultBufferElement}\u003E\u0022;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn type.Slang;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe Slang interpolation modifier, or an empty string for the default.\u003C/summary\u003E\r\n\tpublic static string Interpolation( IrInterpolation interpolation ) =\u003E interpolation switch\r\n\t{\r\n\t\tIrInterpolation.NoPerspective =\u003E \u0022noperspective\u0022,\r\n\t\tIrInterpolation.NoInterpolation =\u003E \u0022nointerpolation\u0022,\r\n\t\tIrInterpolation.Centroid =\u003E \u0022centroid\u0022,\r\n\t\tIrInterpolation.Sample =\u003E \u0022sample\u0022,\r\n\t\t_ =\u003E string.Empty\r\n\t};\r\n\r\n\t/// \u003Csummary\u003EThe \u003Cc\u003E[shader(\u0022...\u0022)]\u003C/c\u003E attribute for a stage, or null when the stage has none.\u003C/summary\u003E\r\n\tpublic static string StageAttribute( ShaderStage stage )\r\n\t{\r\n\t\tvar name = stage.SlangStage();\r\n\t\treturn string.IsNullOrEmpty( name ) ? null : $\u0022[shader(\\\u0022{name}\\\u0022)]\u0022;\r\n\t}\r\n\r\n\t// ---- literals ----------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003EFormat one component of a literal according to the component type.\u003C/summary\u003E\r\n\tpublic static string Scalar( double value, ScalarKind kind ) =\u003E kind switch\r\n\t{\r\n\t\tScalarKind.Bool =\u003E value != 0 ? \u0022true\u0022 : \u0022false\u0022,\r\n\t\tScalarKind.Int =\u003E ( (long)Math.Clamp( value, int.MinValue, int.MaxValue ) ).ToString( CultureInfo.InvariantCulture ),\r\n\t\tScalarKind.UInt =\u003E ( (ulong)Math.Clamp( value, 0, uint.MaxValue ) ).ToString( CultureInfo.InvariantCulture ) \u002B \u0022u\u0022,\r\n\t\t_ =\u003E Real( value )\r\n\t};\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Format a literal of any type. Vectors whose components are all equal collapse to the\r\n\t/// single-argument constructor, which is both shorter and how a human would write it.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string Literal( ShaderType type, ConstValue value )\r\n\t{\r\n\t\tif ( type.IsVoid ) return \u00220\u0022;\r\n\r\n\t\tif ( type.IsScalar ) return Scalar( value[0], type.Scalar );\r\n\r\n\t\tif ( type.IsVector )\r\n\t\t{\r\n\t\t\tvar components = Math.Clamp( type.Components, 1, 4 );\r\n\r\n\t\t\tif ( value.AllEqual( value[0], components ) )\r\n\t\t\t{\r\n\t\t\t\treturn $\u0022{TypeName( type )}( {Scalar( value[0], type.Scalar )} )\u0022;\r\n\t\t\t}\r\n\r\n\t\t\tvar parts = new string[components];\r\n\t\t\tfor ( int i = 0; i \u003C components; i\u002B\u002B ) parts[i] = Scalar( value[i], type.Scalar );\r\n\r\n\t\t\treturn $\u0022{TypeName( type )}( {string.Join( \u0022, \u0022, parts )} )\u0022;\r\n\t\t}\r\n\r\n\t\t// A matrix literal cannot be fully represented by four components, so a matrix constant is\r\n\t\t// always a broadcast of its first component. The IR builds real matrices with IrConstruct.\r\n\t\tif ( type.IsMatrix ) return $\u0022{TypeName( type )}( {Scalar( value[0], type.Scalar )} )\u0022;\r\n\r\n\t\treturn $\u0022( {TypeName( type )} )0\u0022;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EFormat a floating-point literal so it round-trips and always reads as a float.\u003C/summary\u003E\r\n\tpublic static string Real( double value )\r\n\t{\r\n\t\tif ( double.IsNaN( value ) ) value = 0;\r\n\t\tif ( double.IsPositiveInfinity( value ) ) value = 3.402823466e\u002B38;\r\n\t\tif ( double.IsNegativeInfinity( value ) ) value = -3.402823466e\u002B38;\r\n\r\n\t\tvar text = ( (float)value ).ToString( \u0022R\u0022, CultureInfo.InvariantCulture );\r\n\r\n\t\tif ( text.IndexOf( \u0027.\u0027 ) \u003C 0 \u0026\u0026 text.IndexOf( \u0027E\u0027 ) \u003C 0 \u0026\u0026 text.IndexOf( \u0027e\u0027 ) \u003C 0 )\r\n\t\t{\r\n\t\t\ttext \u002B= \u0022.0\u0022;\r\n\t\t}\r\n\r\n\t\treturn text;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EEscape a string so it can appear inside a Slang string literal.\u003C/summary\u003E\r\n\tpublic static string QuotedString( string value )\r\n\t{\r\n\t\tif ( string.IsNullOrEmpty( value ) ) return \u0022\\\u0022\\\u0022\u0022;\r\n\r\n\t\tvar builder = new StringBuilder( value.Length \u002B 2 );\r\n\t\tbuilder.Append( \u0027\u0022\u0027 );\r\n\r\n\t\tforeach ( var c in value )\r\n\t\t{\r\n\t\t\tswitch ( c )\r\n\t\t\t{\r\n\t\t\t\tcase \u0027\u0022\u0027: builder.Append( \u0022\\\\\\\u0022\u0022 ); break;\r\n\t\t\t\tcase \u0027\\\\\u0027: builder.Append( \u0022\\\\\\\\\u0022 ); break;\r\n\t\t\t\tcase \u0027\\r\u0027: break;\r\n\t\t\t\tcase \u0027\\n\u0027: builder.Append( \u0027 \u0027 ); break;\r\n\t\t\t\tcase \u0027\\t\u0027: builder.Append( \u0027 \u0027 ); break;\r\n\t\t\t\tdefault: builder.Append( c ); break;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tbuilder.Append( \u0027\u0022\u0027 );\r\n\t\treturn builder.ToString();\r\n\t}\r\n\r\n\t// ---- identifiers -------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003ETrue when the identifier collides with a Slang keyword or modifier.\u003C/summary\u003E\r\n\tpublic static bool IsReserved( string identifier ) =\u003E\r\n\t\t!string.IsNullOrEmpty( identifier ) \u0026\u0026 s_reserved.Contains( identifier );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Turn arbitrary text into a legal Slang identifier, preserving as much of the original as\r\n\t/// possible so the generated module still reads like the graph that produced it.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string SanitizeIdentifier( string name, string fallback = \u0022prismValue\u0022 )\r\n\t{\r\n\t\tif ( string.IsNullOrWhiteSpace( name ) ) return fallback;\r\n\r\n\t\tvar builder = new StringBuilder( name.Length );\r\n\r\n\t\tforeach ( var c in name )\r\n\t\t{\r\n\t\t\tif ( char.IsLetterOrDigit( c ) || c == \u0027_\u0027 ) builder.Append( c );\r\n\t\t\telse if ( builder.Length \u003E 0 \u0026\u0026 builder[^1] != \u0027_\u0027 ) builder.Append( \u0027_\u0027 );\r\n\t\t}\r\n\r\n\t\twhile ( builder.Length \u003E 0 \u0026\u0026 builder[^1] == \u0027_\u0027 ) builder.Length--;\r\n\r\n\t\tif ( builder.Length == 0 ) return fallback;\r\n\t\tif ( char.IsDigit( builder[0] ) ) builder.Insert( 0, \u0027_\u0027 );\r\n\r\n\t\tvar result = builder.ToString();\r\n\t\treturn IsReserved( result ) ? result \u002B \u0022_\u0022 : result;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EPascalCase an identifier, dropping the shader-world hungarian prefixes on the way.\u003C/summary\u003E\r\n\tpublic static string PascalCase( string name )\r\n\t{\r\n\t\tvar identifier = SanitizeIdentifier( name, \u0022Value\u0022 );\r\n\t\tidentifier = StripPrefix( identifier );\r\n\r\n\t\tif ( identifier.Length == 0 ) return \u0022Value\u0022;\r\n\r\n\t\tvar builder = new StringBuilder( identifier.Length );\r\n\t\tvar upper = true;\r\n\r\n\t\tforeach ( var c in identifier )\r\n\t\t{\r\n\t\t\tif ( c == \u0027_\u0027 )\r\n\t\t\t{\r\n\t\t\t\tupper = true;\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tbuilder.Append( upper ? char.ToUpperInvariant( c ) : c );\r\n\t\t\tupper = false;\r\n\t\t}\r\n\r\n\t\tif ( builder.Length == 0 ) return \u0022Value\u0022;\r\n\t\tif ( char.IsDigit( builder[0] ) ) builder.Insert( 0, \u0027_\u0027 );\r\n\r\n\t\tvar result = builder.ToString();\r\n\t\treturn IsReserved( result ) ? result \u002B \u0022_\u0022 : result;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Normalise an engine field spelling to the Slang-idiomatic name the generated interface structs\r\n\t/// use. Anything unrecognised passes through untouched, so a struct field the graph invented still\r\n\t/// resolves against the declaration we copied from the module.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string FieldName( string name )\r\n\t{\r\n\t\tif ( string.IsNullOrEmpty( name ) ) return name;\r\n\t\tif ( s_fieldAliases.TryGetValue( name, out var alias ) ) return alias;\r\n\r\n\t\treturn SanitizeIdentifier( name, \u0022Field\u0022 );\r\n\t}\r\n\r\n\tstatic string StripPrefix( string identifier )\r\n\t{\r\n\t\t// g_flRoughness -\u003E Roughness, g_vTint -\u003E Tint, m_Foo -\u003E Foo.\r\n\t\tforeach ( var prefix in s_symbolPrefixes )\r\n\t\t{\r\n\t\t\tif ( identifier.Length \u003C= prefix.Length ) continue;\r\n\t\t\tif ( !identifier.StartsWith( prefix, StringComparison.Ordinal ) ) continue;\r\n\r\n\t\t\tvar tail = identifier[prefix.Length..];\r\n\t\t\tif ( tail.Length \u003E 0 \u0026\u0026 ( char.IsLetter( tail[0] ) || tail[0] == \u0027_\u0027 ) ) return tail.TrimStart( \u0027_\u0027 );\r\n\t\t}\r\n\r\n\t\treturn identifier;\r\n\t}\r\n\r\n\t// ---- builtins ----------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003EThe name of the prologue local an entry point binds a builtin to.\u003C/summary\u003E\r\n\tpublic static string BuiltinLocal( Builtin id ) =\u003E LocalPrefix \u002B id;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Builtins this one is derived from. The prologue emits dependencies first, so\r\n\t/// \u003Cc\u003EViewDirection\u003C/c\u003E can be written in terms of the already-bound \u003Cc\u003EWorldPosition\u003C/c\u003E local.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003CBuiltin\u003E Dependencies( Builtin id, ShaderStage stage )\r\n\t{\r\n\t\tif ( stage != ShaderStage.Vertex )\r\n\t\t{\r\n\t\t\treturn id == Builtin.ViewDirection ? s_dependsWorldPosition : Array.Empty\u003CBuiltin\u003E();\r\n\t\t}\r\n\r\n\t\treturn id switch\r\n\t\t{\r\n\t\t\tBuiltin.WorldTangentV =\u003E s_dependsTangentFrame,\r\n\t\t\tBuiltin.ClipPosition =\u003E s_dependsWorldPosition,\r\n\t\t\tBuiltin.ScreenUv =\u003E s_dependsClipPosition,\r\n\t\t\tBuiltin.ViewDirection =\u003E s_dependsWorldPosition,\r\n\t\t\t_ =\u003E Array.Empty\u003CBuiltin\u003E()\r\n\t\t};\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// True when a builtin has to travel from the vertex stage to the pixel stage through an\r\n\t/// interpolator, and therefore becomes a field of the generated \u003Cc\u003EVsOut\u003C/c\u003E struct.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static bool IsInterpolated( Builtin id ) =\u003E InterpolantField( id ) is not null;\r\n\r\n\t/// \u003Csummary\u003EThe \u003Cc\u003EVsOut\u003C/c\u003E field that carries a builtin, or null when it is not interpolated.\u003C/summary\u003E\r\n\tpublic static string InterpolantField( Builtin id ) =\u003E id switch\r\n\t{\r\n\t\tBuiltin.WorldPosition =\u003E \u0022WorldPosition\u0022,\r\n\t\tBuiltin.ObjectPosition =\u003E \u0022ObjectPosition\u0022,\r\n\t\tBuiltin.WorldNormal =\u003E \u0022WorldNormal\u0022,\r\n\t\tBuiltin.ObjectNormal =\u003E \u0022ObjectNormal\u0022,\r\n\t\tBuiltin.WorldTangentU =\u003E \u0022WorldTangentU\u0022,\r\n\t\tBuiltin.WorldTangentV =\u003E \u0022WorldTangentV\u0022,\r\n\t\tBuiltin.ObjectTangentU =\u003E \u0022ObjectTangentU\u0022,\r\n\t\tBuiltin.VertexColor =\u003E \u0022Color\u0022,\r\n\t\tBuiltin.TexCoord0 =\u003E \u0022Uv\u0022,\r\n\t\tBuiltin.TexCoord1 =\u003E \u0022Uv2\u0022,\r\n\t\tBuiltin.VertexId =\u003E \u0022VertexId\u0022,\r\n\t\tBuiltin.InstanceId =\u003E \u0022InstanceId\u0022,\r\n\t\t_ =\u003E null\r\n\t};\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The \u003Cc\u003EVsIn\u003C/c\u003E attribute a builtin is derived from in the vertex stage, or null when it needs\r\n\t/// none. A domain whose vertex input does not carry that attribute \u2014 a full-screen post-process\r\n\t/// pass, for instance \u2014 binds the builtin to zero instead of naming a field that does not exist.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string VertexInputField( Builtin id ) =\u003E id switch\r\n\t{\r\n\t\tBuiltin.WorldPosition or Builtin.ObjectPosition or\r\n\t\tBuiltin.ClipPosition or Builtin.ScreenUv =\u003E \u0022Position\u0022,\r\n\t\tBuiltin.WorldNormal or Builtin.ObjectNormal =\u003E \u0022Normal\u0022,\r\n\t\tBuiltin.WorldTangentU or Builtin.WorldTangentV or Builtin.ObjectTangentU =\u003E \u0022Tangent\u0022,\r\n\t\tBuiltin.VertexColor =\u003E \u0022Color\u0022,\r\n\t\tBuiltin.TexCoord0 =\u003E \u0022Uv\u0022,\r\n\t\tBuiltin.TexCoord1 =\u003E \u0022Uv2\u0022,\r\n\t\tBuiltin.VertexId =\u003E \u0022VertexId\u0022,\r\n\t\tBuiltin.InstanceId =\u003E \u0022InstanceId\u0022,\r\n\t\t_ =\u003E null\r\n\t};\r\n\r\n\t/// \u003Csummary\u003EHow an interpolated builtin\u0027s field interpolates across a triangle.\u003C/summary\u003E\r\n\tpublic static IrInterpolation InterpolantMode( Builtin id ) =\u003E\r\n\t\tid is Builtin.VertexId or Builtin.InstanceId ? IrInterpolation.NoInterpolation : IrInterpolation.Linear;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The Slang expression a builtin lowers to in a given stage.\r\n\t/// \u003Cpara\u003E\r\n\t/// Vertex-stage expressions are computed from the \u003Cc\u003EVsIn\u003C/c\u003E attributes and the environment\r\n\t/// parameter block; pixel-stage expressions read the interpolated \u003Cc\u003EVsOut\u003C/c\u003E field. This is the\r\n\t/// single place that knows a builtin is a different expression in each stage \u2014 nodes never do.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string BuiltinExpression( Builtin id, ShaderStage stage )\r\n\t{\r\n\t\tvar input = InputParameter;\r\n\t\tvar frame = EnvironmentBlock \u002B \u0022.Frame\u0022;\r\n\t\tvar obj = EnvironmentBlock \u002B \u0022.Object\u0022;\r\n\r\n\t\tswitch ( id )\r\n\t\t{\r\n\t\t\t// -- uniform: identical in every stage\r\n\t\t\tcase Builtin.ObjectOrigin: return $\u0022{obj}.ObjectOrigin\u0022;\r\n\t\t\tcase Builtin.ObjectScale: return $\u0022{obj}.ObjectScale\u0022;\r\n\t\t\tcase Builtin.TintColor: return $\u0022{obj}.TintColor\u0022;\r\n\t\t\tcase Builtin.ObjectToWorld: return $\u0022{obj}.ObjectToWorld\u0022;\r\n\t\t\tcase Builtin.WorldToObject: return $\u0022{obj}.WorldToObject\u0022;\r\n\t\t\tcase Builtin.CameraPosition: return $\u0022{frame}.CameraPosition\u0022;\r\n\t\t\tcase Builtin.CameraForward: return $\u0022{frame}.CameraForward\u0022;\r\n\t\t\tcase Builtin.CameraNear: return $\u0022{frame}.CameraNear\u0022;\r\n\t\t\tcase Builtin.CameraFar: return $\u0022{frame}.CameraFar\u0022;\r\n\t\t\tcase Builtin.ViewportSize: return $\u0022{frame}.ViewportSize\u0022;\r\n\t\t\tcase Builtin.ViewportInvSize: return $\u0022{frame}.ViewportInvSize\u0022;\r\n\t\t\tcase Builtin.ViewportOffset: return $\u0022{frame}.ViewportOffset\u0022;\r\n\t\t\tcase Builtin.SunDirection: return $\u0022{frame}.SunDirection\u0022;\r\n\t\t\tcase Builtin.SunColor: return $\u0022{frame}.SunColor\u0022;\r\n\t\t\tcase Builtin.Time: return $\u0022{frame}.Time\u0022;\r\n\t\t\tcase Builtin.DeltaTime: return $\u0022{frame}.DeltaTime\u0022;\r\n\t\t\tcase Builtin.FrameCount: return $\u0022{frame}.FrameCount\u0022;\r\n\t\t\tcase Builtin.ViewMatrix: return $\u0022{frame}.WorldToView\u0022;\r\n\t\t\tcase Builtin.ProjectionMatrix: return $\u0022{frame}.ViewToProjection\u0022;\r\n\t\t\tcase Builtin.ViewProjectionMatrix: return $\u0022{frame}.WorldToProjection\u0022;\r\n\r\n\t\t\t// -- compute\r\n\t\t\tcase Builtin.DispatchThreadId:\r\n\t\t\t\treturn stage == ShaderStage.Compute ? DispatchThreadIdParameter : \u0022uint3( 0 )\u0022;\r\n\t\t\tcase Builtin.GroupThreadId:\r\n\t\t\t\treturn stage == ShaderStage.Compute ? GroupThreadIdParameter : \u0022uint3( 0 )\u0022;\r\n\t\t\tcase Builtin.GroupId:\r\n\t\t\t\treturn stage == ShaderStage.Compute ? GroupIdParameter : \u0022uint3( 0 )\u0022;\r\n\r\n\t\t\t// -- view dependent\r\n\t\t\tcase Builtin.ViewDirection:\r\n\t\t\t\treturn $\u0022PrismSafeNormalize( {frame}.CameraPosition - {BuiltinLocal( Builtin.WorldPosition )} )\u0022;\r\n\t\t}\r\n\r\n\t\tif ( stage == ShaderStage.Vertex ) return VertexExpression( id, input );\r\n\t\tif ( stage == ShaderStage.Pixel ) return PixelExpression( id, input );\r\n\r\n\t\treturn Zero( Builtins.TypeOf( id ) );\r\n\t}\r\n\r\n\tstatic string VertexExpression( Builtin id, string input ) =\u003E id switch\r\n\t{\r\n\t\tBuiltin.WorldPosition =\u003E $\u0022PrismObjectToWorldPoint( {input}.Position )\u0022,\r\n\t\tBuiltin.ObjectPosition =\u003E $\u0022{input}.Position\u0022,\r\n\t\tBuiltin.WorldNormal =\u003E $\u0022PrismObjectToWorldNormal( {input}.Normal )\u0022,\r\n\t\tBuiltin.ObjectNormal =\u003E $\u0022{input}.Normal\u0022,\r\n\t\tBuiltin.WorldTangentU =\u003E $\u0022PrismObjectToWorldDirection( {input}.Tangent.xyz )\u0022,\r\n\t\tBuiltin.WorldTangentV =\u003E\r\n\t\t\t$\u0022cross( {BuiltinLocal( Builtin.WorldNormal )}, {BuiltinLocal( Builtin.WorldTangentU )} ) * {input}.Tangent.w\u0022,\r\n\t\tBuiltin.ObjectTangentU =\u003E $\u0022{input}.Tangent.xyz\u0022,\r\n\t\tBuiltin.VertexColor =\u003E $\u0022{input}.Color\u0022,\r\n\t\tBuiltin.TexCoord0 =\u003E $\u0022{input}.Uv\u0022,\r\n\t\tBuiltin.TexCoord1 =\u003E $\u0022{input}.Uv2\u0022,\r\n\t\tBuiltin.ClipPosition =\u003E $\u0022PrismWorldToClip( {BuiltinLocal( Builtin.WorldPosition )} )\u0022,\r\n\t\tBuiltin.ScreenUv =\u003E $\u0022PrismScreenUvFromClip( {BuiltinLocal( Builtin.ClipPosition )} )\u0022,\r\n\t\tBuiltin.VertexId =\u003E $\u0022{input}.VertexId\u0022,\r\n\t\tBuiltin.InstanceId =\u003E $\u0022{input}.InstanceId\u0022,\r\n\t\tBuiltin.IsFrontFace =\u003E \u0022true\u0022,\r\n\t\t_ =\u003E Zero( Builtins.TypeOf( id ) )\r\n\t};\r\n\r\n\tstatic string PixelExpression( Builtin id, string input ) =\u003E id switch\r\n\t{\r\n\t\tBuiltin.WorldPosition =\u003E $\u0022{input}.WorldPosition\u0022,\r\n\t\tBuiltin.ObjectPosition =\u003E $\u0022{input}.ObjectPosition\u0022,\r\n\t\tBuiltin.WorldNormal =\u003E $\u0022PrismSafeNormalize( {input}.WorldNormal )\u0022,\r\n\t\tBuiltin.ObjectNormal =\u003E $\u0022PrismSafeNormalize( {input}.ObjectNormal )\u0022,\r\n\t\tBuiltin.WorldTangentU =\u003E $\u0022PrismSafeNormalize( {input}.WorldTangentU )\u0022,\r\n\t\tBuiltin.WorldTangentV =\u003E $\u0022PrismSafeNormalize( {input}.WorldTangentV )\u0022,\r\n\t\tBuiltin.ObjectTangentU =\u003E $\u0022{input}.ObjectTangentU\u0022,\r\n\t\tBuiltin.VertexColor =\u003E $\u0022{input}.Color\u0022,\r\n\t\tBuiltin.TexCoord0 =\u003E $\u0022{input}.Uv\u0022,\r\n\t\tBuiltin.TexCoord1 =\u003E $\u0022{input}.Uv2\u0022,\r\n\t\tBuiltin.ClipPosition =\u003E $\u0022{input}.Position\u0022,\r\n\t\tBuiltin.ScreenUv =\u003E $\u0022{input}.Position.xy * {EnvironmentBlock}.Frame.ViewportInvSize\u0022,\r\n\t\tBuiltin.PixelPosition =\u003E $\u0022{input}.Position.xy\u0022,\r\n\t\tBuiltin.FragmentDepth =\u003E $\u0022{input}.Position.z\u0022,\r\n\t\tBuiltin.IsFrontFace =\u003E FrontFaceParameter,\r\n\t\tBuiltin.VertexId =\u003E $\u0022{input}.VertexId\u0022,\r\n\t\tBuiltin.InstanceId =\u003E $\u0022{input}.InstanceId\u0022,\r\n\t\t_ =\u003E Zero( Builtins.TypeOf( id ) )\r\n\t};\r\n\r\n\t/// \u003Csummary\u003EA zero value of a type, used where a builtin has no meaning in the current stage.\u003C/summary\u003E\r\n\tpublic static string Zero( ShaderType type )\r\n\t{\r\n\t\tif ( type.IsVoid ) return \u00220\u0022;\r\n\t\tif ( type.IsScalar ) return Scalar( 0, type.Scalar );\r\n\r\n\t\treturn $\u0022{TypeName( type )}( {Scalar( 0, type.Scalar )} )\u0022;\r\n\t}\r\n\r\n\tstatic readonly Builtin[] s_dependsWorldPosition = [Builtin.WorldPosition];\r\n\tstatic readonly Builtin[] s_dependsClipPosition = [Builtin.WorldPosition, Builtin.ClipPosition];\r\n\tstatic readonly Builtin[] s_dependsTangentFrame = [Builtin.WorldNormal, Builtin.WorldTangentU];\r\n\r\n\tstatic readonly string[] s_symbolPrefixes =\r\n\t[\r\n\t\t\u0022g_fl\u0022, \u0022g_v\u0022, \u0022g_col\u0022, \u0022g_b\u0022, \u0022g_n\u0022, \u0022g_i\u0022, \u0022g_t\u0022, \u0022g_m\u0022, \u0022g_s\u0022, \u0022g_\u0022, \u0022m_\u0022, \u0022s_\u0022, \u0022_\u0022\r\n\t];\r\n\r\n\tstatic readonly Dictionary\u003Cstring, string\u003E s_fieldAliases = new( StringComparer.Ordinal )\r\n\t{\r\n\t\t[\u0022vPositionOs\u0022] = \u0022Position\u0022,\r\n\t\t[\u0022vPositionWs\u0022] = \u0022WorldPosition\u0022,\r\n\t\t[\u0022vPositionPs\u0022] = \u0022Position\u0022,\r\n\t\t[\u0022vPositionSs\u0022] = \u0022Position\u0022,\r\n\t\t[\u0022vPositionWithOffsetWs\u0022] = \u0022WorldPosition\u0022,\r\n\t\t[\u0022vNormalOs\u0022] = \u0022Normal\u0022,\r\n\t\t[\u0022vNormalWs\u0022] = \u0022WorldNormal\u0022,\r\n\t\t[\u0022vTangentUOs_flTangentVSign\u0022] = \u0022Tangent\u0022,\r\n\t\t[\u0022vTangentUWs\u0022] = \u0022WorldTangentU\u0022,\r\n\t\t[\u0022vTangentVWs\u0022] = \u0022WorldTangentV\u0022,\r\n\t\t[\u0022vTexCoord\u0022] = \u0022Uv\u0022,\r\n\t\t[\u0022vTextureCoords\u0022] = \u0022Uv\u0022,\r\n\t\t[\u0022vTexCoord2\u0022] = \u0022Uv2\u0022,\r\n\t\t[\u0022vVertexColor\u0022] = \u0022Color\u0022,\r\n\t\t[\u0022vColor\u0022] = \u0022Color\u0022,\r\n\t\t[\u0022vBlendValues\u0022] = \u0022BlendValues\u0022,\r\n\t\t[\u0022nInstanceTransformID\u0022] = \u0022InstanceId\u0022,\r\n\t\t[\u0022nVertexIndex\u0022] = \u0022VertexId\u0022,\r\n\t\t[\u0022vLightmapUVs\u0022] = \u0022LightmapUv\u0022\r\n\t};\r\n\r\n\tstatic readonly HashSet\u003Cstring\u003E s_reserved = new( StringComparer.Ordinal )\r\n\t{\r\n\t\t// control flow\r\n\t\t\u0022if\u0022, \u0022else\u0022, \u0022switch\u0022, \u0022case\u0022, \u0022default\u0022, \u0022return\u0022, \u0022try\u0022, \u0022throw\u0022, \u0022throws\u0022, \u0022catch\u0022,\r\n\t\t\u0022while\u0022, \u0022for\u0022, \u0022do\u0022, \u0022break\u0022, \u0022continue\u0022, \u0022discard\u0022, \u0022defer\u0022,\r\n\t\t// declarations\r\n\t\t\u0022let\u0022, \u0022var\u0022, \u0022func\u0022, \u0022typedef\u0022, \u0022typealias\u0022, \u0022property\u0022, \u0022get\u0022, \u0022set\u0022,\r\n\t\t\u0022class\u0022, \u0022struct\u0022, \u0022interface\u0022, \u0022enum\u0022, \u0022extension\u0022, \u0022associatedtype\u0022,\r\n\t\t\u0022namespace\u0022, \u0022using\u0022, \u0022import\u0022, \u0022module\u0022, \u0022implementing\u0022,\r\n\t\t\u0022cbuffer\u0022, \u0022tbuffer\u0022, \u0022where\u0022, \u0022syntax\u0022, \u0022semantic\u0022, \u0022type_param\u0022, \u0022typename\u0022,\r\n\t\t// modifiers\r\n\t\t\u0022static\u0022, \u0022const\u0022, \u0022extern\u0022, \u0022inline\u0022, \u0022public\u0022, \u0022private\u0022, \u0022internal\u0022, \u0022protected\u0022,\r\n\t\t\u0022uniform\u0022, \u0022groupshared\u0022, \u0022shared\u0022, \u0022volatile\u0022, \u0022coherent\u0022, \u0022restrict\u0022,\r\n\t\t\u0022readonly\u0022, \u0022writeonly\u0022, \u0022export\u0022, \u0022override\u0022, \u0022param\u0022, \u0022require\u0022,\r\n\t\t\u0022row_major\u0022, \u0022column_major\u0022, \u0022nointerpolation\u0022, \u0022noperspective\u0022, \u0022linear\u0022, \u0022sample\u0022,\r\n\t\t\u0022centroid\u0022, \u0022precise\u0022, \u0022in\u0022, \u0022out\u0022, \u0022inout\u0022, \u0022ref\u0022, \u0022dyn\u0022, \u0022some\u0022, \u0022implicit\u0022,\r\n\t\t\u0022noncopyable\u0022, \u0022constexpr\u0022, \u0022mutating\u0022, \u0022point\u0022, \u0022line\u0022, \u0022triangle\u0022, \u0022lineadj\u0022,\r\n\t\t\u0022triangleadj\u0022, \u0022vertices\u0022, \u0022indices\u0022, \u0022primitives\u0022, \u0022payload\u0022, \u0022layout\u0022,\r\n\t\t// expressions and literals\r\n\t\t\u0022as\u0022, \u0022is\u0022, \u0022this\u0022, \u0022This\u0022, \u0022sizeof\u0022, \u0022alignof\u0022, \u0022countof\u0022, \u0022each\u0022, \u0022expand\u0022,\r\n\t\t\u0022optional\u0022, \u0022nonempty\u0022, \u0022true\u0022, \u0022false\u0022, \u0022nullptr\u0022, \u0022none\u0022, \u0022no_diff\u0022,\r\n\t\t// types\r\n\t\t\u0022void\u0022, \u0022bool\u0022, \u0022int\u0022, \u0022uint\u0022, \u0022half\u0022, \u0022float\u0022, \u0022double\u0022, \u0022string\u0022,\r\n\t\t\u0022vector\u0022, \u0022matrix\u0022, \u0022functype\u0022, \u0022int8_t\u0022, \u0022int16_t\u0022, \u0022int32_t\u0022, \u0022int64_t\u0022,\r\n\t\t\u0022uint8_t\u0022, \u0022uint16_t\u0022, \u0022uint32_t\u0022, \u0022uint64_t\u0022, \u0022float16_t\u0022, \u0022float32_t\u0022, \u0022float64_t\u0022\r\n\t};\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/Compiler/NodeEmitter.cs","FileName":"NodeEmitter.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using Editor.Prism.Compiler.Ir;\r\nusing Editor.Prism.Core;\r\nusing Editor.Prism.Model;\r\nusing System.Globalization;\r\nusing System.Reflection;\r\n\r\nnamespace Editor.Prism.Compiler;\r\n\r\n/// \u003Csummary\u003ECheap lookup tables built once per compile so traversal never rescans the edge list.\u003C/summary\u003E\r\npublic static class GraphIndex\r\n{\r\n\t/// \u003Csummary\u003EEvery edge terminating on each input port.\u003C/summary\u003E\r\n\tpublic static Dictionary\u003CPortRef, List\u003CEdge\u003E\u003E IncomingEdges( IPrismGraph graph )\r\n\t{\r\n\t\tvar map = new Dictionary\u003CPortRef, List\u003CEdge\u003E\u003E();\r\n\r\n\t\tif ( graph?.Edges is null ) return map;\r\n\r\n\t\tforeach ( var edge in graph.Edges )\r\n\t\t{\r\n\t\t\tif ( edge is null || !edge.IsValid ) continue;\r\n\r\n\t\t\tvar key = edge.To;\r\n\r\n\t\t\tif ( !map.TryGetValue( key, out var list ) )\r\n\t\t\t{\r\n\t\t\t\tlist = new List\u003CEdge\u003E();\r\n\t\t\t\tmap[key] = list;\r\n\t\t\t}\r\n\r\n\t\t\tlist.Add( edge );\r\n\t\t}\r\n\r\n\t\treturn map;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EEvery edge leaving each output port.\u003C/summary\u003E\r\n\tpublic static Dictionary\u003CPortRef, List\u003CEdge\u003E\u003E OutgoingEdges( IPrismGraph graph )\r\n\t{\r\n\t\tvar map = new Dictionary\u003CPortRef, List\u003CEdge\u003E\u003E();\r\n\r\n\t\tif ( graph?.Edges is null ) return map;\r\n\r\n\t\tforeach ( var edge in graph.Edges )\r\n\t\t{\r\n\t\t\tif ( edge is null || !edge.IsValid ) continue;\r\n\r\n\t\t\tvar key = edge.From;\r\n\r\n\t\t\tif ( !map.TryGetValue( key, out var list ) )\r\n\t\t\t{\r\n\t\t\t\tlist = new List\u003CEdge\u003E();\r\n\t\t\t\tmap[key] = list;\r\n\t\t\t}\r\n\r\n\t\t\tlist.Add( edge );\r\n\t\t}\r\n\r\n\t\treturn map;\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The demand-driven, memoised, post-order traversal that turns a graph into IR.\r\n/// \u003Cpara\u003E\r\n/// A value is computed by asking for it. The memo key is \u003Cc\u003E(NodeId, PortId, ShaderStage)\u003C/c\u003E, so a\r\n/// node used in both stages is emitted twice \u2014 which is correct, because the expressions genuinely\r\n/// differ there \u2014 while a node used twice within one stage is emitted once.\r\n/// \u003C/para\u003E\r\n/// \u003Cpara\u003E\r\n/// Three properties matter more than the traversal itself. Cycles are detected with an explicit\r\n/// visit-state map \u003Cem\u003Eincluding reroutes\u003C/em\u003E, and reported with the full path rather than hanging.\r\n/// A node that throws inside \u003Csee cref=\u0022PrismNode.Emit\u0022/\u003E is quarantined: the exception is logged, a\r\n/// \u003Cc\u003EPR3001\u003C/c\u003E diagnostic is attached to that node, its outputs become \u003Csee cref=\u0022IrValue.Invalid\u0022/\u003E\r\n/// and traversal continues. And emission order is a deterministic function of the graph, which is what\r\n/// makes \u0022regenerate, compare text, skip the compile\u0022 reliable.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\npublic sealed class NodeEmitter\r\n{\r\n\t/// \u003Csummary\u003EThe name of the pixel-stage input struct instance the backends emit.\u003C/summary\u003E\r\n\tpublic const string PixelInputVariable = \u0022i\u0022;\r\n\r\n\t/// \u003Csummary\u003EThe name of the pixel-stage input struct type the backends emit.\u003C/summary\u003E\r\n\tpublic const string PixelInputStruct = \u0022PixelInput\u0022;\r\n\r\n\treadonly Dictionary\u003CShaderStage, IrBuilder\u003E _builders = new();\r\n\treadonly Dictionary\u003C(NodeId Node, PortId Port, ShaderStage Stage), IrValue\u003E _outputs = new();\r\n\treadonly Dictionary\u003C(NodeId Node, ShaderStage Stage), VisitState\u003E _visited = new();\r\n\treadonly Dictionary\u003C(NodeId Node, string Name), IrValue\u003E _varyingSources = new();\r\n\treadonly Dictionary\u003Cstring, VaryingBinding\u003E _varyingBindings = new( StringComparer.Ordinal );\r\n\treadonly List\u003C(NodeId Node, ShaderStage Stage)\u003E _path = new();\r\n\treadonly List\u003CPreviewAttribute\u003E _previewAttributes = new();\r\n\treadonly List\u003CPreviewTexture\u003E _previewTextures = new();\r\n\treadonly HashSet\u003Cstring\u003E _reportedCycles = new( StringComparer.Ordinal );\r\n\r\n\tDictionary\u003CPortRef, List\u003CEdge\u003E\u003E _incoming = new();\r\n\tint _previewSerial;\r\n\tint _depthExceeded;\r\n\r\n\t/// \u003Csummary\u003EBuild an emitter for one compile.\u003C/summary\u003E\r\n\tpublic NodeEmitter(\r\n\t\tIPrismGraph graph,\r\n\t\tIrModule module,\r\n\t\tCompileRequest request,\r\n\t\tDiagnosticSink diagnostics,\r\n\t\tBackends.IShaderBackend backend,\r\n\t\tStagePlan plan,\r\n\t\tVaryingAllocator varyings )\r\n\t{\r\n\t\tGraph = graph;\r\n\t\tModule = module ?? new IrModule();\r\n\t\tRequest = request;\r\n\t\tDiagnostics = diagnostics ?? new DiagnosticSink();\r\n\t\tBackend = backend;\r\n\t\tPlan = plan ?? StagePlan.Empty;\r\n\t\tVaryings = varyings ?? new VaryingAllocator();\r\n\r\n\t\t_incoming = GraphIndex.IncomingEdges( graph );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe document being compiled.\u003C/summary\u003E\r\n\tpublic IPrismGraph Graph { get; }\r\n\r\n\t/// \u003Csummary\u003EThe module being built.\u003C/summary\u003E\r\n\tpublic IrModule Module { get; }\r\n\r\n\t/// \u003Csummary\u003EThe request this emission answers.\u003C/summary\u003E\r\n\tpublic CompileRequest Request { get; }\r\n\r\n\t/// \u003Csummary\u003EWhat the compile is for.\u003C/summary\u003E\r\n\tpublic CompileMode Mode =\u003E Request?.Mode ?? CompileMode.Final;\r\n\r\n\t/// \u003Csummary\u003EWhere problems go.\u003C/summary\u003E\r\n\tpublic DiagnosticSink Diagnostics { get; }\r\n\r\n\t/// \u003Csummary\u003EThe primary target backend, or null when the module serves more than one.\u003C/summary\u003E\r\n\tpublic Backends.IShaderBackend Backend { get; }\r\n\r\n\t/// \u003Csummary\u003EWhere every node runs.\u003C/summary\u003E\r\n\tpublic StagePlan Plan { get; }\r\n\r\n\t/// \u003Csummary\u003EThe interpolator budget.\u003C/summary\u003E\r\n\tpublic VaryingAllocator Varyings { get; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A prefix that scopes every interpolator this emitter allocates.\r\n\t/// \u003Cpara\u003E\r\n\t/// Empty for the document\u0027s own emitter. A subgraph splice builds a \u003Cem\u003Esecond\u003C/em\u003E emitter over the\r\n\t/// inlined document while sharing the outer \u003Csee cref=\u0022VaryingAllocator\u0022/\u003E, and the inner document\u0027s\r\n\t/// node ids are the same for every instance of the same \u003Cc\u003E.prismfn\u003C/c\u003E \u2014 so without a per-instance\r\n\t/// prefix two instances produce the same interpolator key, the allocator hands the second instance\r\n\t/// the first one\u0027s register, and the second instance\u0027s vertex-side write clobbers the first\u0027s. The\r\n\t/// shader compiles and renders wrong, which is why this is a prefix rather than a comment.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tpublic string KeyPrefix { get; init; } = string.Empty;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The interpolator name one logical key resolves to. Scoped by \u003Csee cref=\u0022KeyPrefix\u0022/\u003E so keys\r\n\t/// minted by two emitters sharing one allocator cannot collide.\r\n\t/// \u003C/summary\u003E\r\n\tstring ScopedKey( string key ) =\u003E string.IsNullOrEmpty( KeyPrefix ) ? key : KeyPrefix \u002B key;\r\n\r\n\t/// \u003Csummary\u003EEmit descriptive temp names and per-node comments.\u003C/summary\u003E\r\n\tpublic bool DebugSymbols =\u003E Request?.DebugSymbols ?? false;\r\n\r\n\t/// \u003Csummary\u003EEmit explanatory comments alongside the generated code.\u003C/summary\u003E\r\n\tpublic bool EmitComments =\u003E Request?.EmitComments ?? false;\r\n\r\n\t/// \u003Csummary\u003ETrue when literals should become live-pushable uniforms instead of constants.\u003C/summary\u003E\r\n\tpublic bool PreviewUniforms =\u003E Mode == CompileMode.Preview;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Promote \u003Cem\u003Eevery\u003C/em\u003E literal a node creates in preview mode, not just the inline port values and\r\n\t/// parameters a user can actually drag.\r\n\t/// \u003Cpara\u003E\r\n\t/// Off by default, and deliberately. Inline literals and blackboard parameters are the values a\r\n\t/// slider moves, and those are promoted unconditionally; a magic number baked into a noise node\u0027s\r\n\t/// hash is not, and turning it into a uniform would cost a register, defeat constant folding and\r\n\t/// make the preview shader slower for no benefit.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool PromoteAllConstants { get; set; }\r\n\r\n\t/// \u003Csummary\u003EUniforms the preview can push without a recompile.\u003C/summary\u003E\r\n\tpublic IReadOnlyList\u003CPreviewAttribute\u003E PreviewAttributes =\u003E _previewAttributes;\r\n\r\n\t/// \u003Csummary\u003ETexture slots the preview has to fill itself. See \u003Csee cref=\u0022PreviewTextureBinding\u0022/\u003E.\u003C/summary\u003E\r\n\tpublic IReadOnlyList\u003CPreviewTexture\u003E PreviewTextures =\u003E _previewTextures;\r\n\r\n\t/// \u003Csummary\u003EHow many node emissions ran.\u003C/summary\u003E\r\n\tpublic int NodesEmitted { get; private set; }\r\n\r\n\t/// \u003Csummary\u003EHow many nodes were quarantined after throwing.\u003C/summary\u003E\r\n\tpublic int FailedNodes { get; private set; }\r\n\r\n\t/// \u003Csummary\u003EHow many cycles were detected and cut.\u003C/summary\u003E\r\n\tpublic int Cycles { get; private set; }\r\n\r\n\t/// \u003Csummary\u003EThe builder for one stage, created on first use.\u003C/summary\u003E\r\n\tpublic IrBuilder Builder( ShaderStage stage )\r\n\t{\r\n\t\tif ( _builders.TryGetValue( stage, out var builder ) ) return builder;\r\n\r\n\t\tbuilder = new IrBuilder( stage, DebugSymbols );\r\n\t\t_builders[stage] = builder;\r\n\r\n\t\treturn builder;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EEvery stage that had code emitted into it, in stage order.\u003C/summary\u003E\r\n\tpublic IEnumerable\u003CShaderStage\u003E ActiveStages =\u003E\r\n\t\tShaderStages.All.Where( x =\u003E _builders.ContainsKey( x ) \u0026\u0026 !_builders[x].Root.IsEmpty );\r\n\r\n\t/// \u003Csummary\u003EStatements emitted across every stage.\u003C/summary\u003E\r\n\tpublic int StatementCount =\u003E _builders.Values.Sum( x =\u003E Count( x.Root ) );\r\n\r\n\t/// \u003Csummary\u003ETemps declared across every stage.\u003C/summary\u003E\r\n\tpublic int TempCount =\u003E _builders.Values.Sum( x =\u003E x.TempCount );\r\n\r\n\t/// \u003Csummary\u003EExpressions answered from an existing temp instead of being recomputed.\u003C/summary\u003E\r\n\tpublic int CseHits =\u003E _builders.Values.Sum( x =\u003E x.CseHits );\r\n\r\n\t// ---- demand -----------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The value of one producer port in one stage, emitting whatever is needed to produce it.\r\n\t/// Returns \u003Csee cref=\u0022IrValue.Invalid\u0022/\u003E for a disabled node, a cycle or a node that threw.\r\n\t/// \u003C/summary\u003E\r\n\tpublic IrValue Demand( PortRef producer, ShaderStage stage )\r\n\t{\r\n\t\tif ( !producer.IsValid ) return IrValue.Invalid;\r\n\r\n\t\tvar key = (producer.Node, producer.Port, stage);\r\n\r\n\t\tif ( _outputs.TryGetValue( key, out var cached ) ) return cached;\r\n\r\n\t\t// Traversal is demand-driven and therefore recursive: one managed frame per node in the\r\n\t\t// dependency chain. A long enough chain \u2014 measured at around 290 chained add nodes \u2014 exhausts\r\n\t\t// the stack, and a .NET StackOverflowException cannot be caught: it bypasses the PrismLog.Guard\r\n\t\t// quarantine entirely and takes the whole editor process down, with no diagnostic and no\r\n\t\t// autosave. 290 nodes is a large graph but not an absurd one.\r\n\t\t//\r\n\t\t// TryEnsureSufficientExecutionStack asks the runtime whether there is room for another frame\r\n\t\t// rather than guessing a depth limit, so this stays correct whatever the stack size and whatever\r\n\t\t// the frames happen to cost in a given build. Failing here costs one wrong value and a\r\n\t\t// diagnostic that names the node.\r\n\t\tif ( !System.Runtime.CompilerServices.RuntimeHelpers.TryEnsureSufficientExecutionStack() )\r\n\t\t{\r\n\t\t\t// Reported once. The guard trips at whatever depth the stack ran out and then trips again on\r\n\t\t\t// every frame as the recursion unwinds and re-descends, which would bury the diagnostics\r\n\t\t\t// panel under hundreds of copies of the same sentence.\r\n\t\t\tif ( _depthExceeded == 0 )\r\n\t\t\t{\r\n\t\t\t\t_depthExceeded = 1;\r\n\r\n\t\t\t\tDiagnostics.Error( DiagnosticCode.NodeEmitFailed,\r\n\t\t\t\t\t\u0022This graph\u0027s dependency chain is too deep to compile\u0022,\r\n\t\t\t\t\tGraphRef.ForPort( producer.Node, producer.Port ),\r\n\t\t\t\t\t\u0022Values are produced by walking backwards from the output, one step per node, and \u0022 \u002B\r\n\t\t\t\t\t\u0022this chain ran out of room. Break it up with a subgraph, or fold a run of \u0022 \u002B\r\n\t\t\t\t\t\u0022operations into one Custom Code node.\u0022 );\r\n\t\t\t}\r\n\r\n\t\t\t_outputs[key] = IrValue.Invalid;\r\n\t\t\treturn IrValue.Invalid;\r\n\t\t}\r\n\r\n\t\tvar node = Graph?.FindNode( producer.Node );\r\n\r\n\t\tif ( node is null )\r\n\t\t{\r\n\t\t\tDiagnostics.Error( DiagnosticCode.DanglingEdge,\r\n\t\t\t\t$\u0022Connection reads from node \u0027{producer.Node}\u0027, which is not in this document\u0022,\r\n\t\t\t\tGraphRef.ForNode( producer.Node ) );\r\n\r\n\t\t\t_outputs[key] = IrValue.Invalid;\r\n\t\t\treturn IrValue.Invalid;\r\n\t\t}\r\n\r\n\t\tif ( ( node.Flags \u0026 NodeFlags.Disabled ) != 0 )\r\n\t\t{\r\n\t\t\t_outputs[key] = IrValue.Invalid;\r\n\t\t\treturn IrValue.Invalid;\r\n\t\t}\r\n\r\n\t\t// The stage planner decided this value is produced per vertex and interpolated. Honour that here\r\n\t\t// rather than in the node, so nodes never have to know which side of the boundary they are on.\r\n\t\tif ( stage == ShaderStage.Pixel \u0026\u0026 Plan.IsVarying( producer ) )\r\n\t\t{\r\n\t\t\tvar vertex = Demand( producer, ShaderStage.Vertex );\r\n\r\n\t\t\tif ( vertex.IsValid )\r\n\t\t\t{\r\n\t\t\t\t// \u0022port:\u0022 namespaces this against the \u0022user:\u0022 keys EmitContext.Varying mints, so a node\r\n\t\t\t\t// whose output port is called Result and which also calls Varying( \u0022Result\u0022, \u2026 ) gets two\r\n\t\t\t\t// interpolators rather than one shared by accident.\r\n\t\t\t\tvar interpolated = Interpolate( $\u0022port:{producer.Node}.{producer.Port}\u0022, vertex, producer.Node );\r\n\r\n\t\t\t\t_outputs[key] = interpolated;\r\n\t\t\t\treturn interpolated;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tEmitNode( node, stage );\r\n\r\n\t\tif ( _outputs.TryGetValue( key, out var produced ) ) return produced;\r\n\r\n\t\t// The node ran but never wrote this port.\r\n\t\tDiagnostics.Warn( DiagnosticCode.MissingInput,\r\n\t\t\t$\u0022\u0027{Describe( node )}\u0027 produced no value for output \u0027{producer.Port}\u0027\u0022,\r\n\t\t\tGraphRef.ForPort( producer.Node, producer.Port ) );\r\n\r\n\t\t_outputs[key] = IrValue.Invalid;\r\n\t\treturn IrValue.Invalid;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The value feeding one input port in one stage: the connected producer coerced to the port\u0027s\r\n\t/// resolved type, or the port\u0027s inline literal, or \u003Csee cref=\u0022IrValue.Invalid\u0022/\u003E. Silent when the\r\n\t/// port is simply unconnected \u2014 reporting that is the caller\u0027s decision.\r\n\t/// \u003C/summary\u003E\r\n\tpublic IrValue DemandInput( PrismNode node, InputPort port, ShaderStage stage, NodeEmitContext context )\r\n\t{\r\n\t\tif ( node is null || port is null ) return IrValue.Invalid;\r\n\r\n\t\tvar edges = IncomingEdges( node.Id, port.Id );\r\n\r\n\t\tif ( edges.Count \u003E 0 )\r\n\t\t{\r\n\t\t\tvar edge = edges[0];\r\n\t\t\tvar value = Demand( edge.From, stage );\r\n\r\n\t\t\tif ( !value.IsValid ) return IrValue.Invalid;\r\n\r\n\t\t\tvar target = port.EffectiveType;\r\n\r\n\t\t\tif ( target.IsVoid || target == value.Type ) return value;\r\n\r\n\t\t\treturn context is null ? value : context.Coerce( value, target, edge.Fill, port.Id );\r\n\t\t}\r\n\r\n\t\t// The inline literal goes through the same coercion as a connected value. Without this a port\r\n\t\t// answers with a different type depending on whether anything is plugged into it: an\r\n\t\t// [In( \u0022float3\u0022 )] port whose [InlineValue] property is a Color reads back as float4 when\r\n\t\t// unwired \u2014 TryReadConstant keeps the literal\u0027s own component count and only adopts the port\u0027s\r\n\t\t// scalar kind \u2014 and float3 once wired. EmitContext.Out then trusts the node and retypes the\r\n\t\t// output port, so the whole downstream chain widens on a port nobody connected.\r\n\t\tvar literal = InlineValue( node, port, stage );\r\n\r\n\t\tif ( !literal.IsValid || context is null ) return literal;\r\n\r\n\t\tvar wanted = port.EffectiveType;\r\n\r\n\t\tif ( wanted.IsVoid || wanted == literal.Type ) return literal;\r\n\r\n\t\treturn context.Coerce( literal, wanted, null, port.Id );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe literal a port falls back to when nothing is connected.\u003C/summary\u003E\r\n\tpublic IrValue InlineValue( PrismNode node, InputPort port, ShaderStage stage )\r\n\t{\r\n\t\tif ( node is null || port is null ) return IrValue.Invalid;\r\n\r\n\t\tvar hint = port.EffectiveType;\r\n\r\n\t\tif ( hint.IsObject ) return IrValue.Invalid;\r\n\r\n\t\tobject raw = port.InlineValue;\r\n\r\n\t\tif ( raw is null \u0026\u0026 !string.IsNullOrEmpty( port.Def.InlineValueProperty ) )\r\n\t\t{\r\n\t\t\traw = ReadProperty( node, port.Def.InlineValueProperty );\r\n\t\t}\r\n\r\n\t\tif ( raw is null ) return IrValue.Invalid;\r\n\t\tif ( !TryReadConstant( raw, hint, out var type, out var value ) ) return IrValue.Invalid;\r\n\r\n\t\tif ( PreviewUniforms \u0026\u0026 type.IsNumeric \u0026\u0026 type.Components \u003C= 4 )\r\n\t\t{\r\n\t\t\treturn PreviewUniform( node.Id, port.Id, type, value, stage );\r\n\t\t}\r\n\r\n\t\treturn Builder( stage ).Const( type, value );\r\n\t}\r\n\r\n\tIReadOnlyList\u003CEdge\u003E IncomingEdges( NodeId node, PortId port ) =\u003E\r\n\t\t_incoming.TryGetValue( new PortRef( node, port ), out var edges ) ? edges : Array.Empty\u003CEdge\u003E();\r\n\r\n\t/// \u003Csummary\u003EPublish the value of one output port. Called by \u003Cc\u003EEmitContext.Out\u003C/c\u003E.\u003C/summary\u003E\r\n\tpublic void SetOutput( NodeId node, PortId port, ShaderStage stage, IrValue value ) =\u003E\r\n\t\t_outputs[(node, port, stage)] = value;\r\n\r\n\t/// \u003Csummary\u003ETrue when a value has already been published for this output.\u003C/summary\u003E\r\n\tpublic bool HasOutput( NodeId node, PortId port, ShaderStage stage ) =\u003E\r\n\t\t_outputs.ContainsKey( (node, port, stage) );\r\n\r\n\t// ---- node emission ----------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Run one node\u0027s \u003Csee cref=\u0022PrismNode.Emit\u0022/\u003E for one stage, at most once. Cycles are cut and\r\n\t/// reported with their full path; exceptions are quarantined to the node that threw.\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool EmitNode( PrismNode node, ShaderStage stage )\r\n\t{\r\n\t\tif ( node is null ) return false;\r\n\r\n\t\tvar key = (node.Id, stage);\r\n\r\n\t\tif ( _visited.TryGetValue( key, out var state ) )\r\n\t\t{\r\n\t\t\tif ( state == VisitState.Done ) return true;\r\n\t\t\tif ( state == VisitState.Failed ) return false;\r\n\r\n\t\t\tReportCycle( node, stage );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\t_visited[key] = VisitState.Visiting;\r\n\t\t_path.Add( key );\r\n\r\n\t\tvar builder = Builder( stage );\r\n\t\tvar context = new NodeEmitContext( this, node, stage, builder );\r\n\r\n\t\tvar ok = true;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( EmitComments || DebugSymbols )\r\n\t\t\t{\r\n\t\t\t\tbuilder.Comment( node.Id, $\u0022{Describe( node )} #{node.Id}\u0022 );\r\n\t\t\t}\r\n\r\n\t\t\tnode.Emit( context );\r\n\t\t}\r\n\t\tcatch ( Exception e )\r\n\t\t{\r\n\t\t\tok = false;\r\n\t\t\tFailedNodes\u002B\u002B;\r\n\r\n\t\t\tPrismLog.Error( e, $\u0022Node \u0027{Describe( node )}\u0027 ({node.Id}) threw while emitting\u0022 );\r\n\r\n\t\t\tDiagnostics.Report( new Diagnostic( DiagnosticSeverity.Error, DiagnosticCode.NodeEmitFailed,\r\n\t\t\t\t$\u0022\u0027{Describe( node )}\u0027 failed to emit: {e.Message}\u0022, e.ToString(), null,\r\n\t\t\t\tGraphRef.ForNode( node.Id ) ) );\r\n\t\t}\r\n\t\tfinally\r\n\t\t{\r\n\t\t\t_path.RemoveAt( _path.Count - 1 );\r\n\t\t\tNodesEmitted\u002B\u002B;\r\n\t\t}\r\n\r\n\t\t// Every output the node did not write becomes invalid, so a partial failure degrades one wire\r\n\t\t// at a time rather than taking the compile down.\r\n\t\tforeach ( var output in node.Outputs )\r\n\t\t{\r\n\t\t\tvar outputKey = (node.Id, output.Id, stage);\r\n\r\n\t\t\tif ( _outputs.ContainsKey( outputKey ) ) continue;\r\n\t\t\tif ( ok ) continue;\r\n\r\n\t\t\t_outputs[outputKey] = IrValue.Invalid;\r\n\t\t}\r\n\r\n\t\t_visited[key] = ok ? VisitState.Done : VisitState.Failed;\r\n\t\treturn ok;\r\n\t}\r\n\r\n\tvoid ReportCycle( PrismNode node, ShaderStage stage )\r\n\t{\r\n\t\tCycles\u002B\u002B;\r\n\r\n\t\tvar start = _path.FindIndex( x =\u003E x.Node == node.Id \u0026\u0026 x.Stage == stage );\r\n\t\tvar names = new List\u003Cstring\u003E();\r\n\r\n\t\tfor ( int i = Math.Max( 0, start ); i \u003C _path.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar member = Graph?.FindNode( _path[i].Node );\r\n\t\t\tnames.Add( member is null ? _path[i].Node.ToString() : $\u0022{Describe( member )} #{_path[i].Node}\u0022 );\r\n\t\t}\r\n\r\n\t\tnames.Add( $\u0022{Describe( node )} #{node.Id}\u0022 );\r\n\r\n\t\tvar path = string.Join( \u0022  -\u003E  \u0022, names );\r\n\r\n\t\t// Only the first report per distinct cycle; a diamond above a cycle would otherwise repeat it.\r\n\t\tif ( !_reportedCycles.Add( path ) ) return;\r\n\r\n\t\tDiagnostics.Error( DiagnosticCode.Cycle,\r\n\t\t\t$\u0022\u0027{Describe( node )}\u0027 is part of a feedback loop and cannot be compiled\u0022,\r\n\t\t\tGraphRef.ForNode( node.Id ),\r\n\t\t\t$\u0022Cycle: {path}\u0022 );\r\n\t}\r\n\r\n\t// ---- varyings ---------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Move a value across the vertex-to-pixel boundary.\r\n\t/// \u003Cpara\u003E\r\n\t/// Called from the vertex stage this only records the source expression and hands the value straight\r\n\t/// back. Called from the pixel stage it re-runs the owning node in the vertex stage \u2014 the emission\r\n\t/// is memoised per stage, so this costs nothing the second time \u2014 takes the value the node\r\n\t/// registered there, allocates an interpolator, emits the vertex-side write and returns the\r\n\t/// interpolated read.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tpublic IrValue Varying( PrismNode node, string name, IrValue vsValue, ShaderStage consumerStage )\r\n\t{\r\n\t\tif ( node is null || string.IsNullOrWhiteSpace( name ) ) return vsValue;\r\n\r\n\t\tvar key = $\u0022user:{node.Id}.{name}\u0022;\r\n\r\n\t\tif ( consumerStage == ShaderStage.Vertex )\r\n\t\t{\r\n\t\t\tif ( vsValue.IsValid ) _varyingSources[(node.Id, name)] = vsValue;\r\n\t\t\treturn vsValue;\r\n\t\t}\r\n\r\n\t\tif ( consumerStage != ShaderStage.Pixel )\r\n\t\t{\r\n\t\t\t// Geometry and compute have no interpolators of ours; the value stays where it was computed.\r\n\t\t\treturn vsValue;\r\n\t\t}\r\n\r\n\t\tif ( _varyingBindings.TryGetValue( ScopedKey( key ), out var known ) )\r\n\t\t{\r\n\t\t\treturn ReadVarying( known, consumerStage );\r\n\t\t}\r\n\r\n\t\t// Ask the vertex stage for the value. Guard against a node that calls Varying while it is\r\n\t\t// already being emitted in the vertex stage.\r\n\t\tif ( !_varyingSources.TryGetValue( (node.Id, name), out var source ) )\r\n\t\t{\r\n\t\t\tif ( !IsVisiting( node.Id, ShaderStage.Vertex ) ) EmitNode( node, ShaderStage.Vertex );\r\n\r\n\t\t\t_varyingSources.TryGetValue( (node.Id, name), out source );\r\n\t\t}\r\n\r\n\t\tif ( !source.IsValid )\r\n\t\t{\r\n\t\t\tDiagnostics.Info( DiagnosticCode.SampleLowered,\r\n\t\t\t\t$\u0022\u0027{Describe( node )}\u0027 could not produce \u0027{name}\u0027 in the vertex stage; it is computed per pixel instead\u0022,\r\n\t\t\t\tGraphRef.ForNode( node.Id ) );\r\n\r\n\t\t\treturn vsValue;\r\n\t\t}\r\n\r\n\t\tvar interpolated = Interpolate( key, source, node.Id );\r\n\r\n\t\treturn interpolated.IsValid ? interpolated : vsValue;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Allocate an interpolator for a vertex-stage value, emit the vertex-side write and return the\r\n\t/// pixel-stage read. Idempotent per key, so demanding the same value twice costs one register.\r\n\t/// \u003Cpara\u003E\r\n\t/// \u003Cparamref name=\u0022key\u0022/\u003E is a \u003Cem\u003Elogical\u003C/em\u003E key. It is scoped by \u003Csee cref=\u0022KeyPrefix\u0022/\u003E before\r\n\t/// it reaches the shared allocator, so a caller never has to know whether this emitter is the\r\n\t/// document\u0027s own or one splicing a subgraph into it.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tpublic IrValue Interpolate( string key, IrValue source, NodeId origin )\r\n\t{\r\n\t\tif ( string.IsNullOrEmpty( key ) || !source.IsValid ) return IrValue.Invalid;\r\n\r\n\t\tvar scoped = ScopedKey( key );\r\n\r\n\t\tif ( _varyingBindings.TryGetValue( scoped, out var known ) ) return ReadVarying( known, ShaderStage.Pixel );\r\n\r\n\t\tvar binding = Varyings.Allocate( scoped, source.Type, InterpolationFor( source.Type ), origin, Diagnostics );\r\n\r\n\t\tif ( !binding.IsValid ) return IrValue.Invalid;\r\n\r\n\t\t_varyingBindings[scoped] = binding;\r\n\r\n\t\tvar vertex = Builder( ShaderStage.Vertex );\r\n\r\n\t\tvertex.Assign( origin, VaryingAccess( vertex, binding ), source );\r\n\r\n\t\treturn ReadVarying( binding, ShaderStage.Pixel );\r\n\t}\r\n\r\n\tstatic IrInterpolation InterpolationFor( ShaderType type ) =\u003E\r\n\t\ttype.IsFloatingPoint ? IrInterpolation.Linear : IrInterpolation.NoInterpolation;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The expression that names one packed value inside its interpolator register.\r\n\t/// \u003Cpara\u003E\r\n\t/// The register is typed as a full four components on purpose. Its real width is not known until\r\n\t/// allocation has finished \u2014 another value may still be packed alongside this one \u2014 and a swizzle\r\n\t/// whose base claims the narrower width would look like an identity to the optimiser and be folded\r\n\t/// away, silently widening the read once the register grew.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tIrValue VaryingAccess( IrBuilder builder, VaryingBinding binding )\r\n\t{\r\n\t\tvar input = builder.Var( ShaderType.Struct( PixelInputStruct ), PixelInputVariable );\r\n\t\tvar register = ShaderType.Vec( binding.Type.Scalar, 4 );\r\n\t\tvar field = builder.Member( register, input, binding.Slot );\r\n\r\n\t\treturn binding.IsWholeSlot ? field : builder.Swizzle( binding.Type, field, binding.Swizzle );\r\n\t}\r\n\r\n\tIrValue ReadVarying( VaryingBinding binding, ShaderStage stage ) =\u003E VaryingAccess( Builder( stage ), binding );\r\n\r\n\tbool IsVisiting( NodeId node, ShaderStage stage ) =\u003E\r\n\t\t_visited.TryGetValue( (node, stage), out var state ) \u0026\u0026 state == VisitState.Visiting;\r\n\r\n\t// ---- module registration ----------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Add a global to the module, or reuse the identical declaration already there. A different\r\n\t/// declaration claiming the same name is reported rather than silently overwriting.\r\n\t/// \u003C/summary\u003E\r\n\tpublic GlobalDecl RegisterGlobal( GlobalDecl decl, NodeId origin )\r\n\t{\r\n\t\tif ( decl is null || string.IsNullOrEmpty( decl.Name ) ) return null;\r\n\r\n\t\tdecl = PreviewTextureBinding( decl );\r\n\r\n\t\tvar existing = Module.FindGlobal( decl.Name );\r\n\r\n\t\tif ( existing is null )\r\n\t\t{\r\n\t\t\tModule.Globals.Add( decl );\r\n\r\n\t\t\t// Recorded here rather than in PreviewTextureBinding: four nodes sampling one slot register\r\n\t\t\t// the same declaration four times, and the preview only needs to be told about it once.\r\n\t\t\tRecordPreviewTexture( decl );\r\n\r\n\t\t\treturn decl;\r\n\t\t}\r\n\r\n\t\tif ( existing.ConflictsWith( decl ) )\r\n\t\t{\r\n\t\t\tDiagnostics.Error( DiagnosticCode.GlobalCollision,\r\n\t\t\t\t$\u0022Two different declarations both claim the name \u0027{decl.Name}\u0027\u0022,\r\n\t\t\t\tGraphRef.ForNode( origin ),\r\n\t\t\t\t$\u0022{existing} vs {decl}\u0022 );\r\n\t\t}\r\n\r\n\t\treturn existing;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Add a helper and everything it needs to the module, in dependency order. A same-name helper with\r\n\t/// a different body is a hard error naming both, not a silent first-one-wins.\r\n\t/// \u003C/summary\u003E\r\n\tpublic HelperFunction RegisterHelper( HelperFunction fn, NodeId origin )\r\n\t{\r\n\t\tif ( fn is null || string.IsNullOrEmpty( fn.Name ) ) return null;\r\n\r\n\t\tvar existing = Module.Helpers.FirstOrDefault( x =\u003E x.Name == fn.Name );\r\n\r\n\t\tif ( existing is not null )\r\n\t\t{\r\n\t\t\tif ( existing.ConflictsWith( fn ) )\r\n\t\t\t{\r\n\t\t\t\tDiagnostics.Error( DiagnosticCode.HelperCollision,\r\n\t\t\t\t\t$\u0022Two different helper functions are both called \u0027{fn.Name}\u0027\u0022,\r\n\t\t\t\t\tGraphRef.ForNode( origin ),\r\n\t\t\t\t\t\u0022Helpers are deduplicated by name per module. Rename one of them.\u0022 );\r\n\t\t\t}\r\n\r\n\t\t\treturn existing;\r\n\t\t}\r\n\r\n\t\tforeach ( var required in fn.Requires ?? Array.Empty\u003CHelperFunction\u003E() )\r\n\t\t{\r\n\t\t\tif ( required is null || ReferenceEquals( required, fn ) ) continue;\r\n\r\n\t\t\tRegisterHelper( required, origin );\r\n\t\t}\r\n\r\n\t\tModule.Helpers.Add( fn );\r\n\r\n\t\tforeach ( var include in fn.Includes ?? Array.Empty\u003Cstring\u003E() ) Module.AddInclude( include );\r\n\r\n\t\tforeach ( var capability in fn.Capabilities ?? Array.Empty\u003CCapability\u003E() )\r\n\t\t{\r\n\t\t\tif ( capability != Capability.None ) Module.Meta.Capabilities.Add( capability );\r\n\t\t}\r\n\r\n\t\treturn fn;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Rebind a texture slot to a render attribute for the preview, and remember which asset belongs in\r\n\t/// it so the viewport can push the real texture.\r\n\t/// \u003Cremarks\u003E\r\n\t/// A shipping shader declares a texture as \u003Cc\u003ECreateInputTexture2D\u003C/c\u003E plus a \u003Cc\u003EChannel( \u2026 Box( \u2026 ) )\u003C/c\u003E\r\n\t/// slot. That pair is resolved by the \u003Cem\u003Eresource compiler\u003C/em\u003E when a material is built: the source\r\n\t/// image named by \u003Cc\u003EDefaultFile\u003C/c\u003E is baked into the material\u0027s own texture. The preview has no\r\n\t/// material \u2014 it renders the shader straight onto a scene object with render attributes \u2014 so nothing\r\n\t/// ever performs that bake and every sampler reads black. A graph whose output is multiplied by its\r\n\t/// textures then previews as a black surface, and any animation in it is invisible because it is\r\n\t/// being multiplied by zero.\r\n\t/// \u003Cpara\u003E\r\n\t/// Binding to an attribute instead is what the built-in editor does for exactly this reason (see its\r\n\t/// \u003Cc\u003EGraphCompiler\u003C/c\u003E preview branch), and \u003Cc\u003EDeclareTexture\u003C/c\u003E already emits that form for any\r\n\t/// declaration carrying an \u003Cc\u003EAttributeName\u003C/c\u003E. The asset path travels out on the compile result so\r\n\t/// the viewport can load it once and push it through \u003Csee cref=\u0022Preview.PreviewAttributeBus\u0022/\u003E.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/remarks\u003E\r\n\t/// \u003C/summary\u003E\r\n\tGlobalDecl PreviewTextureBinding( GlobalDecl decl )\r\n\t{\r\n\t\tif ( Mode != CompileMode.Preview || decl.Kind != GlobalKind.Texture ) return decl;\r\n\r\n\t\t// Already attribute-bound: the graph asked for that itself, and whatever drives it owns the push.\r\n\t\tif ( !string.IsNullOrEmpty( decl.AttributeName ) ) return decl;\r\n\r\n\t\treturn decl with { AttributeName = decl.Name };\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Note a newly declared preview texture slot so the viewport can fill it. Only slots naming an\r\n\t/// asset are recorded; one with nothing to load would push white over a slot the user may be driving\r\n\t/// themselves through the parameter panel.\r\n\t/// \u003C/summary\u003E\r\n\tvoid RecordPreviewTexture( GlobalDecl decl )\r\n\t{\r\n\t\tif ( Mode != CompileMode.Preview || decl.Kind != GlobalKind.Texture ) return;\r\n\t\tif ( string.IsNullOrWhiteSpace( decl.DefaultAsset ) || string.IsNullOrEmpty( decl.AttributeName ) ) return;\r\n\r\n\t\t_previewTextures.Add( new PreviewTexture( decl.AttributeName, decl.DefaultAsset, decl.Srgb )\r\n\t\t{\r\n\t\t\tParameter = decl.Parameter\r\n\t\t} );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Turn a literal into a uniform the preview can push straight to the GPU, so dragging a slider\r\n\t/// updates the frame without recompiling anything.\r\n\t/// \u003C/summary\u003E\r\n\tpublic IrValue PreviewUniform( NodeId node, PortId port, ShaderType type, ConstValue value, ShaderStage stage )\r\n\t{\r\n\t\tvar tag = stage switch\r\n\t\t{\r\n\t\t\tShaderStage.Vertex =\u003E \u0022vs\u0022,\r\n\t\t\tShaderStage.Pixel =\u003E \u0022ps\u0022,\r\n\t\t\tShaderStage.Geometry =\u003E \u0022gs\u0022,\r\n\t\t\tShaderStage.Compute =\u003E \u0022cs\u0022,\r\n\t\t\t_ =\u003E \u0022any\u0022\r\n\t\t};\r\n\r\n\t\tvar name = $\u0022{PrismConstants.SymbolPrefix}_{tag}_{_previewSerial\u002B\u002B}\u0022;\r\n\r\n\t\tvar decl = new GlobalDecl( name, type, GlobalKind.Uniform )\r\n\t\t{\r\n\t\t\tAttributeName = name,\r\n\t\t\tDefault = value,\r\n\t\t\tPreviewOnly = true,\r\n\t\t\tStages = stage.ToMask()\r\n\t\t};\r\n\r\n\t\tRegisterGlobal( decl, node );\r\n\r\n\t\t_previewAttributes.Add( new PreviewAttribute( name, type, value )\r\n\t\t{\r\n\t\t\tNode = node,\r\n\t\t\tPort = port\r\n\t\t} );\r\n\r\n\t\treturn Builder( stage ).GlobalRef( decl );\r\n\t}\r\n\r\n\t// ---- literals ---------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003ERead a node property by name, tolerating anything that is not there.\u003C/summary\u003E\r\n\tpublic static object ReadProperty( PrismNode node, string name )\r\n\t{\r\n\t\tif ( node is null || string.IsNullOrEmpty( name ) ) return null;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar property = node.GetType().GetProperty( name,\r\n\t\t\t\tBindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy );\r\n\r\n\t\t\tif ( property is not null \u0026\u0026 property.CanRead ) return property.GetValue( node );\r\n\r\n\t\t\tvar field = node.GetType().GetField( name,\r\n\t\t\t\tBindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance );\r\n\r\n\t\t\treturn field?.GetValue( node );\r\n\t\t}\r\n\t\tcatch ( Exception e )\r\n\t\t{\r\n\t\t\tPrismLog.Error( e, $\u0022Reading property \u0027{name}\u0027 from {node.GetType().Name} failed\u0022 );\r\n\t\t\treturn null;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Turn a boxed authored literal into a typed constant. Deliberately permissive: an inline value\r\n\t/// arrives from JSON, from a node property or from a paste, and none of those are trustworthy.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static bool TryReadConstant( object raw, ShaderType hint, out ShaderType type, out ConstValue value )\r\n\t{\r\n\t\ttype = ShaderType.Void;\r\n\t\tvalue = default;\r\n\r\n\t\tif ( raw is null ) return false;\r\n\r\n\t\tswitch ( raw )\r\n\t\t{\r\n\t\t\tcase bool b:\r\n\t\t\t\ttype = Shape( hint, ShaderType.Bool );\r\n\t\t\t\tvalue = ConstValue.From( b );\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tcase float f:\r\n\t\t\t\ttype = Shape( hint, ShaderType.Float );\r\n\t\t\t\tvalue = ConstValue.From( f );\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tcase double d:\r\n\t\t\t\ttype = Shape( hint, ShaderType.Float );\r\n\t\t\t\tvalue = ConstValue.From( (float)d );\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tcase int i:\r\n\t\t\t\ttype = Shape( hint, hint.IsFloatingPoint ? ShaderType.Float : ShaderType.Int );\r\n\t\t\t\tvalue = ConstValue.From( i );\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tcase long l:\r\n\t\t\t\ttype = Shape( hint, hint.IsFloatingPoint ? ShaderType.Float : ShaderType.Int );\r\n\t\t\t\tvalue = ConstValue.From( (int)l );\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tcase short s:\r\n\t\t\t\ttype = Shape( hint, ShaderType.Int );\r\n\t\t\t\tvalue = ConstValue.From( (int)s );\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tcase byte by:\r\n\t\t\t\ttype = Shape( hint, ShaderType.Int );\r\n\t\t\t\tvalue = ConstValue.From( (int)by );\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tcase Vector2 v2:\r\n\t\t\t\ttype = Shape( hint, ShaderType.Float2 );\r\n\t\t\t\tvalue = ConstValue.From( v2 );\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tcase Vector3 v3:\r\n\t\t\t\ttype = Shape( hint, ShaderType.Float3 );\r\n\t\t\t\tvalue = ConstValue.From( v3 );\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tcase Vector4 v4:\r\n\t\t\t\ttype = Shape( hint, ShaderType.Float4 );\r\n\t\t\t\tvalue = ConstValue.From( v4 );\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tcase Color color:\r\n\t\t\t\ttype = Shape( hint, ShaderType.Float4 );\r\n\t\t\t\tvalue = ConstValue.From( color );\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tcase Enum e:\r\n\t\t\t\ttype = Shape( hint, ShaderType.Int );\r\n\t\t\t\tvalue = ConstValue.From( Convert.ToInt32( e, CultureInfo.InvariantCulture ) );\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tcase ConstValue constant:\r\n\t\t\t\ttype = hint.IsNumeric ? hint : ShaderType.Float4;\r\n\t\t\t\tvalue = constant;\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tcase string text:\r\n\t\t\t\treturn TryParseText( text, hint, out type, out value );\r\n\r\n\t\t\tcase JsonNode json:\r\n\t\t\t\treturn TryReadJson( json, hint, out type, out value );\r\n\t\t}\r\n\r\n\t\tif ( raw is System.Collections.IEnumerable sequence and not string )\r\n\t\t{\r\n\t\t\tvar numbers = new List\u003Cdouble\u003E( 4 );\r\n\r\n\t\t\tforeach ( var item in sequence )\r\n\t\t\t{\r\n\t\t\t\tif ( item is null ) continue;\r\n\r\n\t\t\t\ttry\r\n\t\t\t\t{\r\n\t\t\t\t\tnumbers.Add( Convert.ToDouble( item, CultureInfo.InvariantCulture ) );\r\n\t\t\t\t}\r\n\t\t\t\tcatch ( Exception )\r\n\t\t\t\t{\r\n\t\t\t\t\treturn false;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif ( numbers.Count == 4 ) break;\r\n\t\t\t}\r\n\r\n\t\t\tif ( numbers.Count == 0 ) return false;\r\n\r\n\t\t\ttype = Shape( hint, ShaderType.Vec( hint.IsNumeric ? hint.Scalar : ScalarKind.Float, numbers.Count ) );\r\n\t\t\tvalue = FromList( numbers );\r\n\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\tstatic bool TryParseText( string text, ShaderType hint, out ShaderType type, out ConstValue value )\r\n\t{\r\n\t\ttype = ShaderType.Void;\r\n\t\tvalue = default;\r\n\r\n\t\tif ( string.IsNullOrWhiteSpace( text ) ) return false;\r\n\r\n\t\tif ( bool.TryParse( text, out var flag ) )\r\n\t\t{\r\n\t\t\ttype = Shape( hint, ShaderType.Bool );\r\n\t\t\tvalue = ConstValue.From( flag );\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tvar parts = text.Split( \u0027,\u0027, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries );\r\n\t\tvar numbers = new List\u003Cdouble\u003E( 4 );\r\n\r\n\t\tforeach ( var part in parts )\r\n\t\t{\r\n\t\t\tif ( !double.TryParse( part, NumberStyles.Float, CultureInfo.InvariantCulture, out var number ) )\r\n\t\t\t{\r\n\t\t\t\treturn false;\r\n\t\t\t}\r\n\r\n\t\t\tnumbers.Add( number );\r\n\r\n\t\t\tif ( numbers.Count == 4 ) break;\r\n\t\t}\r\n\r\n\t\tif ( numbers.Count == 0 ) return false;\r\n\r\n\t\ttype = Shape( hint, ShaderType.Vec( hint.IsNumeric ? hint.Scalar : ScalarKind.Float, numbers.Count ) );\r\n\t\tvalue = FromList( numbers );\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\tstatic bool TryReadJson( JsonNode json, ShaderType hint, out ShaderType type, out ConstValue value )\r\n\t{\r\n\t\ttype = ShaderType.Void;\r\n\t\tvalue = default;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( json is JsonArray array )\r\n\t\t\t{\r\n\t\t\t\tvar numbers = new List\u003Cdouble\u003E( 4 );\r\n\r\n\t\t\t\tforeach ( var item in array )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( item is null ) continue;\r\n\r\n\t\t\t\t\tnumbers.Add( item.GetValue\u003Cdouble\u003E() );\r\n\r\n\t\t\t\t\tif ( numbers.Count == 4 ) break;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif ( numbers.Count == 0 ) return false;\r\n\r\n\t\t\t\ttype = Shape( hint, ShaderType.Vec( hint.IsNumeric ? hint.Scalar : ScalarKind.Float, numbers.Count ) );\r\n\t\t\t\tvalue = FromList( numbers );\r\n\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\r\n\t\t\tif ( json is JsonValue scalar )\r\n\t\t\t{\r\n\t\t\t\tif ( scalar.TryGetValue\u003Cbool\u003E( out var flag ) )\r\n\t\t\t\t{\r\n\t\t\t\t\ttype = Shape( hint, ShaderType.Bool );\r\n\t\t\t\t\tvalue = ConstValue.From( flag );\r\n\t\t\t\t\treturn true;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif ( scalar.TryGetValue\u003Cdouble\u003E( out var number ) )\r\n\t\t\t\t{\r\n\t\t\t\t\ttype = Shape( hint, hint.IsIntegral ? ShaderType.Int : ShaderType.Float );\r\n\t\t\t\t\tvalue = ConstValue.From( (float)number );\r\n\t\t\t\t\treturn true;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif ( scalar.TryGetValue\u003Cstring\u003E( out var text ) ) return TryParseText( text, hint, out type, out value );\r\n\t\t\t}\r\n\t\t}\r\n\t\tcatch ( Exception )\r\n\t\t{\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\tstatic ConstValue FromList( IReadOnlyList\u003Cdouble\u003E numbers )\r\n\t{\r\n\t\tdouble At( int index ) =\u003E index \u003C numbers.Count ? numbers[index] : numbers.Count == 1 ? numbers[0] : 0;\r\n\r\n\t\treturn new ConstValue( At( 0 ), At( 1 ), At( 2 ), At( 3 ) );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Reconcile the shape the literal arrived in with the shape the port wants. A scalar feeding a\r\n\t/// vector port stays a scalar and is splatted by the conversion machinery; anything else adopts the\r\n\t/// port\u0027s component kind so an authored \u003Cc\u003E1\u003C/c\u003E on a float port is a float, not an int.\r\n\t/// \u003C/summary\u003E\r\n\tstatic ShaderType Shape( ShaderType hint, ShaderType natural )\r\n\t{\r\n\t\tif ( !hint.IsNumeric ) return natural;\r\n\t\tif ( !natural.IsNumeric ) return natural;\r\n\r\n\t\tif ( natural.Components == 1 \u0026\u0026 hint.Components \u003E 1 ) return ShaderType.Vec( hint.Scalar, 1 );\r\n\r\n\t\treturn ShaderType.Vec( hint.Scalar, natural.Components );\r\n\t}\r\n\r\n\tstatic int Count( IrBlock block )\r\n\t{\r\n\t\tif ( block is null ) return 0;\r\n\r\n\t\tvar total = 0;\r\n\r\n\t\tforeach ( var statement in block.Statements )\r\n\t\t{\r\n\t\t\ttotal\u002B\u002B;\r\n\r\n\t\t\tswitch ( statement )\r\n\t\t\t{\r\n\t\t\t\tcase IrIf branch:\r\n\t\t\t\t\ttotal \u002B= Count( branch.Then ) \u002B Count( branch.Else );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase IrFor loop:\r\n\t\t\t\t\ttotal \u002B= Count( loop.Body );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase IrWhile loop:\r\n\t\t\t\t\ttotal \u002B= Count( loop.Body );\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tcase IrScope scope:\r\n\t\t\t\t\ttotal \u002B= Count( scope.Body );\r\n\t\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn total;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EA node\u0027s display name, falling back to its type name.\u003C/summary\u003E\r\n\tpublic static string Describe( PrismNode node )\r\n\t{\r\n\t\tif ( node is null ) return \u0022\u003Cmissing node\u003E\u0022;\r\n\r\n\t\tvar title = PrismLog.Guard( \u0022Reading node descriptor\u0022, () =\u003E node.Descriptor?.Title, null );\r\n\r\n\t\treturn string.IsNullOrEmpty( title ) ? node.GetType().Name : title;\r\n\t}\r\n\r\n\tenum VisitState\r\n\t{\r\n\t\tVisiting,\r\n\t\tDone,\r\n\t\tFailed\r\n\t}\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/Integration/PrismAssetEditor.cs","FileName":"PrismAssetEditor.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using Editor.Prism.Core;\r\nusing System.IO;\r\n\r\nnamespace Editor.Prism.Integration;\r\n\r\n/// \u003Csummary\u003E\r\n/// Routes a double-clicked \u003Cc\u003E.prism\u003C/c\u003E or \u003Cc\u003E.prismfn\u003C/c\u003E into the Prism window.\r\n/// \u003Cpara\u003E\r\n/// These are deliberately \u003Cb\u003Estatic method\u003C/b\u003E handlers rather than an \u003Cc\u003EIAssetEditor\u003C/c\u003E window\r\n/// class. \u003Cc\u003EIAssetEditor.OpenInEditor\u003C/c\u003E runs \u003Cc\u003ETryOpenUsingStaticMethod\u003C/c\u003E first, so a static\r\n/// handler is the only registration that resolves deterministically \u2014 the class path picks a winner\r\n/// with \u003Cc\u003EFirstOrDefault()\u003C/c\u003E over an unordered type list. It also keeps us out of the two static\r\n/// dictionaries \u003Cc\u003EIAssetEditor\u003C/c\u003E keeps alive across hotloads, which are the usual source of\r\n/// \u0022double-clicking the asset does nothing after a reload\u0022.\r\n/// \u003C/para\u003E\r\n/// \u003Cpara\u003E\r\n/// The method must take exactly one \u003Csee cref=\u0022Asset\u0022/\u003E parameter and be static, or the dispatcher\r\n/// silently ignores it.\r\n/// \u003C/para\u003E\r\n/// \u003C/summary\u003E\r\npublic static class PrismAssetEditor\r\n{\r\n\t/// \u003Csummary\u003EOpen a shader graph document. Bound to the \u003Cc\u003Eprism\u003C/c\u003E extension.\u003C/summary\u003E\r\n\t[EditorForAssetType( PrismConstants.GraphExtension )]\r\n\tpublic static void OpenGraph( Asset asset )\r\n\t{\r\n\t\tOpen( asset );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EOpen a subgraph document. Bound to the \u003Cc\u003Eprismfn\u003C/c\u003E extension.\u003C/summary\u003E\r\n\t[EditorForAssetType( PrismConstants.SubgraphExtension )]\r\n\tpublic static void OpenSubgraph( Asset asset )\r\n\t{\r\n\t\tOpen( asset );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Open an asset in Prism, reporting rather than throwing when it cannot be opened. Safe to call\r\n\t/// from a context menu, a drag-drop handler or a console command.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static bool Open( Asset asset )\r\n\t{\r\n\t\tif ( asset is null ) return false;\r\n\r\n\t\tif ( asset.IsDeleted )\r\n\t\t{\r\n\t\t\tPrismLog.Warn( $\u0022\u0027{asset.Name}\u0027 has been deleted\u0022 );\r\n\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\treturn PrismLauncher.OpenAsset( asset );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EOpen by absolute path, registering the file with the asset system first if we can.\u003C/summary\u003E\r\n\tpublic static bool Open( string absolutePath )\r\n\t{\r\n\t\tif ( string.IsNullOrWhiteSpace( absolutePath ) ) return false;\r\n\r\n\t\tvar asset = PrismLog.Guard( \u0022Finding the asset for a Prism document\u0022,\r\n\t\t\t() =\u003E AssetSystem.FindByPath( absolutePath ), null );\r\n\r\n\t\tif ( asset is not null ) return Open( asset );\r\n\r\n\t\t// Not registered \u2014 either it lives outside a mounted content path, or the asset system has\r\n\t\t// not caught up with a file we only just wrote. Opening by path always works.\r\n\t\treturn PrismLauncher.OpenDocument( absolutePath );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETrue when the path is a Prism document we own.\u003C/summary\u003E\r\n\tpublic static bool IsPrismDocument( string path )\r\n\t{\r\n\t\tif ( string.IsNullOrWhiteSpace( path ) ) return false;\r\n\r\n\t\tvar extension = Path.GetExtension( path );\r\n\r\n\t\tif ( string.IsNullOrEmpty( extension ) ) return false;\r\n\r\n\t\textension = extension.TrimStart( \u0027.\u0027 );\r\n\r\n\t\treturn extension.Equals( PrismConstants.GraphExtension, StringComparison.OrdinalIgnoreCase )\r\n\t\t\t|| extension.Equals( PrismConstants.SubgraphExtension, StringComparison.OrdinalIgnoreCase );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETrue when the path is specifically a subgraph.\u003C/summary\u003E\r\n\tpublic static bool IsSubgraphDocument( string path )\r\n\t{\r\n\t\tif ( string.IsNullOrWhiteSpace( path ) ) return false;\r\n\r\n\t\treturn Path.GetExtension( path )\r\n\t\t\t.TrimStart( \u0027.\u0027 )\r\n\t\t\t.Equals( PrismConstants.SubgraphExtension, StringComparison.OrdinalIgnoreCase );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Drop dead entries out of the two static maps \u003Cc\u003EIAssetEditor\u003C/c\u003E keeps.\r\n\t/// \u003Cpara\u003E\r\n\t/// Both survive a hotload because they are static fields on an interface, and both are keyed by\r\n\t/// strings that outlive the windows they point at. Entries whose window has been destroyed, or\r\n\t/// whose type came from an assembly that has since been swapped out, leave the asset browser\r\n\t/// believing a document is already open and silently doing nothing on double-click.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tpublic static int PruneStaleEditors()\r\n\t{\r\n\t\tvar removed = 0;\r\n\r\n\t\tPrismLog.Guard( \u0022Pruning stale asset editors\u0022, () =\u003E\r\n\t\t{\r\n\t\t\tremoved \u002B= Prune( IAssetEditor.OpenSingleEditors );\r\n\t\t\tremoved \u002B= Prune( IAssetEditor.OpenMultiAssetEditors );\r\n\t\t} );\r\n\r\n\t\treturn removed;\r\n\t}\r\n\r\n\tstatic int Prune( Dictionary\u003Cstring, IAssetEditor\u003E map )\r\n\t{\r\n\t\tif ( map is null || map.Count == 0 ) return 0;\r\n\r\n\t\tvar dead = new List\u003Cstring\u003E();\r\n\r\n\t\tforeach ( var pair in map )\r\n\t\t{\r\n\t\t\tvar editor = pair.Value;\r\n\r\n\t\t\tif ( editor is null || !editor.IsValid )\r\n\t\t\t{\r\n\t\t\t\tdead.Add( pair.Key );\r\n\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\t// One of our windows left behind by a hotload is a zombie: the native widget is still\r\n\t\t\t// alive so IsValid answers true, but every delegate on it points into the old assembly.\r\n\t\t\t// Someone else\u0027s editor is none of our business, current or not.\r\n\t\t\tvar type = editor.GetType();\r\n\r\n\t\t\tif ( type.Assembly == typeof( PrismAssetEditor ).Assembly ) continue;\r\n\t\t\tif ( type.FullName is null ) continue;\r\n\t\t\tif ( !type.FullName.StartsWith( \u0022Editor.Prism\u0022, StringComparison.Ordinal ) ) continue;\r\n\r\n\t\t\tdead.Add( pair.Key );\r\n\t\t}\r\n\r\n\t\tforeach ( var key in dead )\r\n\t\t{\r\n\t\t\tmap.Remove( key );\r\n\t\t}\r\n\r\n\t\treturn dead.Count;\r\n\t}\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/Integration/PrismDocumentation.cs","FileName":"PrismDocumentation.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using Editor.Prism.Core;\r\nusing Editor.Prism.Model;\r\nusing Editor.Prism.Ui;\r\nusing System.Text;\r\n\r\nnamespace Editor.Prism.Integration;\r\n\r\n/// \u003Csummary\u003E\r\n/// Everything Prism knows how to explain about one node type, assembled from the same metadata the\r\n/// graph and the node library already use \u2014 never a second, drifting copy.\r\n/// \u003C/summary\u003E\r\npublic sealed record PrismNodeHelp(\r\n\tstring Id, string Title, string Category, string Icon, string Summary,\r\n\tIReadOnlyList\u003Cstring\u003E Keywords, NodeTier Tier, string Since, string DeprecatedBy,\r\n\tIReadOnlyList\u003CPortDef\u003E Inputs, IReadOnlyList\u003CPortDef\u003E Outputs )\r\n{\r\n\t/// \u003Csummary\u003ETrue when there is nothing useful to show.\u003C/summary\u003E\r\n\tpublic bool IsEmpty =\u003E string.IsNullOrEmpty( Id );\r\n\r\n\t/// \u003Csummary\u003EA one-line status for the header: tier, availability and replacement.\u003C/summary\u003E\r\n\tpublic string Status\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar parts = new List\u003Cstring\u003E();\r\n\r\n\t\t\tif ( Tier != NodeTier.Common ) parts.Add( Tier.ToString() );\r\n\t\t\tif ( !string.IsNullOrWhiteSpace( Since ) ) parts.Add( $\u0022since {Since}\u0022 );\r\n\t\t\tif ( !string.IsNullOrWhiteSpace( DeprecatedBy ) ) parts.Add( $\u0022replaced by {DeprecatedBy}\u0022 );\r\n\r\n\t\t\treturn string.Join( \u0022 \u00B7 \u0022, parts );\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EPlain-text rendering, for a tooltip or the clipboard.\u003C/summary\u003E\r\n\tpublic string ToPlainText()\r\n\t{\r\n\t\tvar builder = new StringBuilder();\r\n\r\n\t\tbuilder.AppendLine( Title );\r\n\r\n\t\tif ( !string.IsNullOrWhiteSpace( Category ) ) builder.AppendLine( Category );\r\n\r\n\t\tbuilder.AppendLine();\r\n\r\n\t\tif ( !string.IsNullOrWhiteSpace( Summary ) )\r\n\t\t{\r\n\t\t\tbuilder.AppendLine( Summary );\r\n\t\t\tbuilder.AppendLine();\r\n\t\t}\r\n\r\n\t\tAppend( builder, \u0022Inputs\u0022, Inputs );\r\n\t\tAppend( builder, \u0022Outputs\u0022, Outputs );\r\n\r\n\t\tbuilder.AppendLine( $\u0022Type id: {Id}\u0022 );\r\n\r\n\t\treturn builder.ToString();\r\n\t}\r\n\r\n\tstatic void Append( StringBuilder builder, string heading, IReadOnlyList\u003CPortDef\u003E ports )\r\n\t{\r\n\t\tif ( ports is null || ports.Count == 0 ) return;\r\n\r\n\t\tbuilder.AppendLine( heading );\r\n\r\n\t\tforeach ( var port in ports )\r\n\t\t{\r\n\t\t\tbuilder.Append( \u0022  \u0022 ).Append( port.DisplayName ).Append( \u0022  \u0022 ).Append( port.DeclaredType );\r\n\r\n\t\t\tif ( !string.IsNullOrWhiteSpace( port.Tooltip ) ) builder.Append( \u0022 \u2014 \u0022 ).Append( port.Tooltip );\r\n\r\n\t\t\tbuilder.AppendLine();\r\n\t\t}\r\n\r\n\t\tbuilder.AppendLine();\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// In-editor help: the orientation panel a new user sees once, and the per-node reference every user\r\n/// reaches from the node library, the inspector or the \u003Cc\u003EPrism\u003C/c\u003E menu.\r\n/// \u003C/summary\u003E\r\npublic static class PrismDocumentation\r\n{\r\n\tstatic PrismWelcomeWindow s_welcome;\r\n\tstatic PrismNodeReferenceWindow s_reference;\r\n\r\n\t/// \u003Csummary\u003EDocumentation for one registered node type, or an empty record when it is unknown.\u003C/summary\u003E\r\n\tpublic static PrismNodeHelp Lookup( string typeId )\r\n\t{\r\n\t\tif ( string.IsNullOrWhiteSpace( typeId ) ) return Empty;\r\n\r\n\t\treturn PrismLog.Guard( \u0022Looking up node documentation\u0022, () =\u003E\r\n\t\t{\r\n\t\t\tNodeRegistry.EnsureBuilt();\r\n\r\n\t\t\treturn NodeRegistry.TryResolve( typeId, out var descriptor ) ? For( descriptor ) : Empty;\r\n\t\t}, Empty );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EDocumentation for a node instance.\u003C/summary\u003E\r\n\tpublic static PrismNodeHelp For( PrismNode node ) =\u003E node is null ? Empty : For( node.Descriptor );\r\n\r\n\t/// \u003Csummary\u003EDocumentation built from a descriptor.\u003C/summary\u003E\r\n\tpublic static PrismNodeHelp For( NodeDescriptor descriptor )\r\n\t{\r\n\t\tif ( descriptor is null ) return Empty;\r\n\r\n\t\treturn new PrismNodeHelp(\r\n\t\t\tdescriptor.Id,\r\n\t\t\tstring.IsNullOrWhiteSpace( descriptor.Title ) ? descriptor.Id : descriptor.Title,\r\n\t\t\tdescriptor.Category,\r\n\t\t\tstring.IsNullOrWhiteSpace( descriptor.Icon ) ? \u0022extension\u0022 : descriptor.Icon,\r\n\t\t\tdescriptor.Description,\r\n\t\t\tdescriptor.Keywords ?? Array.Empty\u003Cstring\u003E(),\r\n\t\t\tdescriptor.Tier,\r\n\t\t\tdescriptor.Since,\r\n\t\t\tdescriptor.DeprecatedBy,\r\n\t\t\tdescriptor.Inputs ?? Array.Empty\u003CPortDef\u003E(),\r\n\t\t\tdescriptor.Outputs ?? Array.Empty\u003CPortDef\u003E() );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe \u0022nothing to show\u0022 record.\u003C/summary\u003E\r\n\tpublic static PrismNodeHelp Empty { get; } = new( null, null, null, null, null,\r\n\t\tArray.Empty\u003Cstring\u003E(), NodeTier.Common, null, null,\r\n\t\tArray.Empty\u003CPortDef\u003E(), Array.Empty\u003CPortDef\u003E() );\r\n\r\n\t// ---- windows -----------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003EOpen the node reference, optionally scrolled to one node.\u003C/summary\u003E\r\n\tpublic static void ShowNodeReference( string typeId = null )\r\n\t{\r\n\t\tPrismLog.Guard( \u0022Opening the Prism node reference\u0022, () =\u003E\r\n\t\t{\r\n\t\t\tif ( s_reference is null || !s_reference.IsValid )\r\n\t\t\t{\r\n\t\t\t\ts_reference = new PrismNodeReferenceWindow();\r\n\t\t\t}\r\n\r\n\t\t\ts_reference.Show();\r\n\t\t\ts_reference.Focus();\r\n\r\n\t\t\tif ( !string.IsNullOrWhiteSpace( typeId ) ) s_reference.SelectNode( typeId );\r\n\t\t} );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EOpen the orientation panel on demand.\u003C/summary\u003E\r\n\tpublic static void ShowWelcome()\r\n\t{\r\n\t\tPrismLog.Guard( \u0022Opening the Prism welcome panel\u0022, () =\u003E\r\n\t\t{\r\n\t\t\tif ( s_welcome is null || !s_welcome.IsValid )\r\n\t\t\t{\r\n\t\t\t\ts_welcome = new PrismWelcomeWindow();\r\n\t\t\t}\r\n\r\n\t\t\ts_welcome.Show();\r\n\t\t\ts_welcome.Focus();\r\n\t\t} );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Show the orientation panel the very first time Prism is opened, and never again unless it is\r\n\t/// asked for. Called from every path that opens a window.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void ShowWelcomeIfFirstRun()\r\n\t{\r\n\t\tif ( PrismCookies.WelcomeShown ) return;\r\n\r\n\t\tPrismCookies.WelcomeShown = true;\r\n\r\n\t\tShowWelcome();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EDrop the cached windows outright.\u003C/summary\u003E\r\n\tpublic static void Reset()\r\n\t{\r\n\t\ts_welcome = null;\r\n\t\ts_reference = null;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// What hotload calls. A window that is still on screen is kept \u2014 the hotload system migrates the\r\n\t/// instance rather than destroying it, and dropping the reference here would leave the user with a\r\n\t/// second copy the next time they asked for one.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void Revalidate()\r\n\t{\r\n\t\tif ( s_welcome is not null \u0026\u0026 !s_welcome.IsValid ) s_welcome = null;\r\n\t\tif ( s_reference is not null \u0026\u0026 !s_reference.IsValid ) s_reference = null;\r\n\r\n\t\tPrismLog.Guard( \u0022Reloading the Prism node reference\u0022, () =\u003E\r\n\t\t{\r\n\t\t\tif ( s_reference is not null \u0026\u0026 s_reference.IsValid ) s_reference.Reload();\r\n\t\t} );\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The first-run orientation panel: what Prism is, what makes it different from the built-in shader\r\n/// graph, and the six things worth knowing before the first graph.\r\n/// \u003C/summary\u003E\r\npublic sealed class PrismWelcomeWindow : BaseWindow\r\n{\r\n\t/// \u003Csummary\u003EBuild the panel.\u003C/summary\u003E\r\n\tpublic PrismWelcomeWindow()\r\n\t{\r\n\t\tWindowTitle = \u0022What Is Prism?\u0022;\r\n\t\tSetWindowIcon( \u0022gradient\u0022 );\r\n\r\n\t\tSize = new Vector2( 720f, 660f );\r\n\t\tMinimumSize = new Vector2( 560f, 420f );\r\n\r\n\t\tLayout = Layout.Column();\r\n\t\tLayout.Margin = 0f;\r\n\r\n\t\tvar scroll = new ScrollArea( this );\r\n\r\n\t\tscroll.Canvas = new Widget( scroll );\r\n\t\tscroll.Canvas.Layout = Layout.Column();\r\n\t\tscroll.Canvas.Layout.Margin = 28f;\r\n\t\tscroll.Canvas.Layout.Spacing = 10f;\r\n\r\n\t\tBuild( scroll.Canvas.Layout );\r\n\r\n\t\tLayout.Add( scroll, 1 );\r\n\r\n\t\tvar footer = Layout.AddRow();\r\n\r\n\t\tfooter.Margin = new Sandbox.UI.Margin( 28f, 0f, 28f, 20f );\r\n\t\tfooter.Spacing = 8f;\r\n\r\n\t\tvar reference = footer.Add( new Button( \u0022Node Reference\u0022, \u0022menu_book\u0022, this ) );\r\n\r\n\t\treference.Clicked = () =\u003E PrismDocumentation.ShowNodeReference();\r\n\r\n\t\tfooter.AddStretchCell();\r\n\r\n\t\tvar close = footer.Add( new Button.Primary( \u0022Start Building\u0022, \u0022arrow_forward\u0022, this ) );\r\n\r\n\t\tclose.Clicked = Close;\r\n\t}\r\n\r\n\tvoid Build( Layout layout )\r\n\t{\r\n\t\tvar title = layout.Add( new Label.Title( \u0022Prism\u0022 ) );\r\n\r\n\t\ttitle.Color = PrismTheme.TextPrimary;\r\n\r\n\t\tvar lead = layout.Add( new Label.Subtitle(\r\n\t\t\t\u0022A node-based shader editor for s\u0026box that treats generated code as something you are meant to read.\u0022 ) );\r\n\r\n\t\tlead.Color = PrismTheme.TextSecondary;\r\n\t\tlead.WordWrap = true;\r\n\r\n\t\tlayout.AddSpacingCell( 8f );\r\n\r\n\t\tSection( layout, \u0022gradient\u0022, \u0022Graphs compile to real HLSL\u0022,\r\n\t\t\t\u0022Everything you wire up becomes a readable .shader beside the document, with the same block \u0022\r\n\t\t\t\u002B \u0022structure a hand-written one has. The Code panel shows it live, and clicking a line \u0022\r\n\t\t\t\u002B \u0022selects the node that produced it.\u0022 );\r\n\r\n\t\tSection( layout, \u0022rule\u0022, \u0022Connections are type-checked\u0022,\r\n\t\t\t\u0022Free conversions connect silently. A lossy or padded one connects, warns, and draws a marker \u0022\r\n\t\t\t\u002B \u0022on the wire telling you exactly what it did \u2014 the built-in editor pads float2 to float3 with \u0022\r\n\t\t\t\u002B \u0022zero and never says so. Illegal connections are refused at the drop.\u0022 );\r\n\r\n\t\tSection( layout, \u0022history\u0022, \u0022Nothing is quietly destroyed\u0022,\r\n\t\t\t\u0022Node ids are minted once and never renumbered. A node whose plugin is missing survives as a \u0022\r\n\t\t\t\u002B \u0022placeholder and re-saves byte-identically. A connection that cannot resolve stays as a \u0022\r\n\t\t\t\u002B \u0022visible ghost instead of vanishing.\u0022 );\r\n\r\n\t\tSection( layout, \u0022bolt\u0022, \u0022The preview is the shader\u0022,\r\n\t\t\t\u0022There is no separate preview path. Edits are debounced and recompiled with the minimum combo \u0022\r\n\t\t\t\u002B \u0022set, so the sphere shows the same code the material will use. The status strip tells you \u0022\r\n\t\t\t\u002B \u0022how long each compile took.\u0022 );\r\n\r\n\t\tSection( layout, \u0022functions\u0022, \u0022Subgraphs and custom code are first class\u0022,\r\n\t\t\t\u0022A .prismfn is a reusable function with its own inputs and outputs. When a node does not exist \u0022\r\n\t\t\t\u002B \u0022yet, the Custom Code node takes HLSL directly \u2014 a missing node is an inconvenience, not a wall.\u0022 );\r\n\r\n\t\tSection( layout, \u0022keyboard\u0022, \u0022Worth learning early\u0022,\r\n\t\t\t\u0022Space or double-click on empty canvas opens the node search. Dragging a wire into empty space \u0022\r\n\t\t\t\u002B \u0022opens it filtered by type. Ctrl\u002BZ and Ctrl\u002BY are per-document. Ctrl\u002BS saves the document and \u0022\r\n\t\t\t\u002B \u0022regenerates the shader beside it.\u0022 );\r\n\r\n\t\tlayout.AddSpacingCell( 8f );\r\n\r\n\t\tvar footnote = layout.Add( new Label.Small(\r\n\t\t\t\u0022Prism never registers the built-in .shdrgrph or .shdrfunc extensions. To bring an existing graph \u0022\r\n\t\t\t\u002B \u0022across, right-click it and choose Import into Prism.\u0022 ) );\r\n\r\n\t\tfootnote.Color = PrismTheme.TextMuted;\r\n\t\tfootnote.WordWrap = true;\r\n\r\n\t\tlayout.AddStretchCell();\r\n\t}\r\n\r\n\tvoid Section( Layout layout, string icon, string heading, string body )\r\n\t{\r\n\t\tlayout.AddSpacingCell( 12f );\r\n\r\n\t\tvar row = layout.AddRow();\r\n\r\n\t\trow.Spacing = 12f;\r\n\r\n\t\trow.Add( new PrismGlyph( this, icon, PrismTheme.Accent ) );\r\n\r\n\t\tvar column = row.AddColumn( 1 );\r\n\r\n\t\tcolumn.Spacing = 3f;\r\n\r\n\t\tvar header = column.Add( new Label.Header( heading ) );\r\n\r\n\t\theader.Color = PrismTheme.TextPrimary;\r\n\r\n\t\tvar text = column.Add( new Label.Body( body ) );\r\n\r\n\t\ttext.Color = PrismTheme.TextSecondary;\r\n\t\ttext.WordWrap = true;\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// A fixed-size material icon as a layout item. Qt labels cannot render one, and a whole\r\n/// \u003Cc\u003EIconButton\u003C/c\u003E would bring click behaviour and hover states nobody asked for.\r\n/// \u003C/summary\u003E\r\ninternal sealed class PrismGlyph : Widget\r\n{\r\n\treadonly string _icon;\r\n\treadonly Color _color;\r\n\treadonly float _size;\r\n\r\n\t/// \u003Csummary\u003EBuild a glyph of the given size, in the given colour.\u003C/summary\u003E\r\n\tpublic PrismGlyph( Widget parent, string icon, Color color, float size = 20f ) : base( parent )\r\n\t{\r\n\t\t_icon = string.IsNullOrWhiteSpace( icon ) ? \u0022circle\u0022 : icon;\r\n\t\t_color = color;\r\n\t\t_size = size;\r\n\r\n\t\tFixedSize = new Vector2( size \u002B 6f, size \u002B 6f );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EDraw the glyph, centred.\u003C/summary\u003E\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tPaint.Antialiasing = true;\r\n\t\tPaint.SetPen( _color );\r\n\t\tPaint.DrawIcon( LocalRect, _icon, _size, TextFlag.Center );\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Every registered node type, searchable, with the ports and description the compiler and the node\r\n/// library read from the same metadata.\r\n/// \u003C/summary\u003E\r\npublic sealed class PrismNodeReferenceWindow : BaseWindow\r\n{\r\n\treadonly List\u003CPrismNodeType\u003E _all = new();\r\n\r\n\tListView _list;\r\n\tLineEdit _search;\r\n\tWidget _detail;\r\n\tLabel _count;\r\n\r\n\t/// \u003Csummary\u003EBuild the window and load the registry.\u003C/summary\u003E\r\n\tpublic PrismNodeReferenceWindow()\r\n\t{\r\n\t\tWindowTitle = \u0022Prism Node Reference\u0022;\r\n\t\tSetWindowIcon( \u0022menu_book\u0022 );\r\n\r\n\t\tSize = new Vector2( 1040f, 700f );\r\n\t\tMinimumSize = new Vector2( 720f, 460f );\r\n\r\n\t\tLayout = Layout.Column();\r\n\t\tLayout.Margin = 16f;\r\n\t\tLayout.Spacing = 10f;\r\n\r\n\t\tBuildHeader();\r\n\t\tBuildBody();\r\n\r\n\t\tReload();\r\n\t}\r\n\r\n\tvoid BuildHeader()\r\n\t{\r\n\t\tvar row = Layout.AddRow();\r\n\r\n\t\trow.Spacing = 8f;\r\n\r\n\t\t_search = row.Add( new LineEdit( this ), 1 );\r\n\t\t_search.PlaceholderText = \u0022Search nodes by name, category or keyword\u0022;\r\n\t\t_search.TextEdited \u002B= _ =\u003E Populate();\r\n\r\n\t\tvar refresh = row.Add( new Button( \u0022\u0022, \u0022refresh\u0022, this ) );\r\n\r\n\t\trefresh.Clicked = Reload;\r\n\t\trefresh.StatusTip = \u0022Rebuild the node registry\u0022;\r\n\r\n\t\t_count = Layout.Add( new Label.Small( \u0022\u0022 ) );\r\n\t\t_count.Color = PrismTheme.TextMuted;\r\n\t}\r\n\r\n\tvoid BuildBody()\r\n\t{\r\n\t\tvar row = Layout.AddRow( 1 );\r\n\r\n\t\trow.Spacing = 12f;\r\n\r\n\t\t_list = row.Add( new ListView( this ), 1 );\r\n\t\t_list.ItemSize = new Vector2( -1f, 34f );\r\n\t\t_list.ItemSpacing = new Vector2( 0f, 2f );\r\n\t\t_list.Margin = 2f;\r\n\t\t_list.ItemPaint = PaintRow;\r\n\t\t_list.ItemSelected = item =\u003E ShowDetail( item as PrismNodeType );\r\n\r\n\t\tvar scroll = new ScrollArea( this );\r\n\r\n\t\tscroll.Canvas = new Widget( scroll );\r\n\t\tscroll.Canvas.Layout = Layout.Column();\r\n\t\tscroll.Canvas.Layout.Margin = 4f;\r\n\t\tscroll.Canvas.Layout.Spacing = 6f;\r\n\r\n\t\t_detail = scroll.Canvas;\r\n\r\n\t\trow.Add( scroll, 2 );\r\n\r\n\t\tShowDetail( null );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ERebuild from the registry \u2014 useful after a hotload adds node types.\u003C/summary\u003E\r\n\tpublic void Reload()\r\n\t{\r\n\t\tPrismLog.Guard( \u0022Loading the Prism node registry\u0022, () =\u003E\r\n\t\t{\r\n\t\t\tNodeRegistry.EnsureBuilt();\r\n\r\n\t\t\t_all.Clear();\r\n\t\t\t_all.AddRange( NodeRegistry.Types\r\n\t\t\t\t.OrderBy( x =\u003E x.Category ?? string.Empty, StringComparer.OrdinalIgnoreCase )\r\n\t\t\t\t.ThenBy( x =\u003E x.Title ?? string.Empty, StringComparer.OrdinalIgnoreCase ) );\r\n\t\t} );\r\n\r\n\t\tPopulate();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ESelect and reveal one node type by its stable id.\u003C/summary\u003E\r\n\tpublic void SelectNode( string typeId )\r\n\t{\r\n\t\tPrismLog.Guard( \u0022Selecting a node in the reference\u0022, () =\u003E\r\n\t\t{\r\n\t\t\tvar match = _all.FirstOrDefault( x =\u003E string.Equals( x.Id, typeId, StringComparison.Ordinal ) );\r\n\r\n\t\t\tif ( match is null ) return;\r\n\r\n\t\t\t_list?.ScrollTo( match );\r\n\t\t\tShowDetail( match );\r\n\t\t} );\r\n\t}\r\n\r\n\tvoid Populate()\r\n\t{\r\n\t\tPrismLog.Guard( \u0022Filtering the Prism node reference\u0022, () =\u003E\r\n\t\t{\r\n\t\t\tvar text = _search?.Text ?? string.Empty;\r\n\r\n\t\t\tvar matches = string.IsNullOrWhiteSpace( text )\r\n\t\t\t\t? _all\r\n\t\t\t\t: NodeRegistry.Search( text ).ToList();\r\n\r\n\t\t\t_list?.SetItems( matches.Cast\u003Cobject\u003E() );\r\n\r\n\t\t\tif ( _count is not null )\r\n\t\t\t{\r\n\t\t\t\t_count.Text = matches.Count == _all.Count\r\n\t\t\t\t\t? $\u0022{_all.Count} node types\u0022\r\n\t\t\t\t\t: $\u0022{matches.Count} of {_all.Count} node types\u0022;\r\n\t\t\t}\r\n\t\t} );\r\n\t}\r\n\r\n\tvoid PaintRow( VirtualWidget item )\r\n\t{\r\n\t\tif ( item?.Object is not PrismNodeType type ) return;\r\n\r\n\t\tvar rect = item.Rect;\r\n\r\n\t\tPaint.Antialiasing = true;\r\n\t\tPaint.ClearPen();\r\n\r\n\t\tif ( item.Selected ) Paint.SetBrush( PrismTheme.AccentSoft );\r\n\t\telse if ( item.Hovered ) Paint.SetBrush( PrismTheme.PanelAlt );\r\n\t\telse Paint.ClearBrush();\r\n\r\n\t\tif ( item.Selected || item.Hovered ) Paint.DrawRect( rect, PrismTheme.RadiusChip );\r\n\r\n\t\tvar iconRect = new Rect( rect.Left \u002B 8f, rect.Top \u002B ( rect.Height - 16f ) * 0.5f, 16f, 16f );\r\n\r\n\t\tPaint.SetPen( item.Selected ? PrismTheme.Accent : PrismTheme.TextMuted );\r\n\t\tPaint.DrawIcon( iconRect, string.IsNullOrWhiteSpace( type.Icon ) ? \u0022extension\u0022 : type.Icon, 15f );\r\n\r\n\t\tvar textRect = new Rect( rect.Left \u002B 32f, rect.Top, rect.Width - 40f, rect.Height );\r\n\r\n\t\tPaint.SetPen( item.Selected ? PrismTheme.TextPrimary : PrismTheme.TextSecondary );\r\n\t\tPaint.SetFont( PrismTheme.FontFamily, PrismTheme.BodySize, 500, false, false );\r\n\t\tPaint.DrawText( textRect, type.Title ?? type.Id, TextFlag.LeftCenter );\r\n\r\n\t\tPaint.SetPen( PrismTheme.TextDisabled );\r\n\t\tPaint.SetFont( PrismTheme.FontFamily, PrismTheme.PortLabelSize, 400, false, false );\r\n\t\tPaint.DrawText( textRect, type.Category ?? string.Empty, TextFlag.RightCenter );\r\n\t}\r\n\r\n\tvoid ShowDetail( PrismNodeType type )\r\n\t{\r\n\t\tif ( _detail is null ) return;\r\n\r\n\t\t_detail.Layout.Clear( true );\r\n\r\n\t\tif ( type is null )\r\n\t\t{\r\n\t\t\tvar empty = _detail.Layout.Add( new Label.Body(\r\n\t\t\t\t\u0022Pick a node on the left to see what it does, what it takes and what it returns.\u0022 ) );\r\n\r\n\t\t\tempty.Color = PrismTheme.TextMuted;\r\n\t\t\tempty.WordWrap = true;\r\n\t\t\t_detail.Layout.AddStretchCell();\r\n\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar help = PrismDocumentation.For( type.Descriptor );\r\n\r\n\t\tvar title = _detail.Layout.Add( new Label.Title( help.Title ) );\r\n\r\n\t\ttitle.Color = PrismTheme.TextPrimary;\r\n\r\n\t\tvar subtitle = _detail.Layout.Add( new Label.Small(\r\n\t\t\tstring.Join( \u0022 \u00B7 \u0022, new[] { help.Category, help.Status }.Where( x =\u003E !string.IsNullOrWhiteSpace( x ) ) ) ) );\r\n\r\n\t\tsubtitle.Color = PrismTheme.TextMuted;\r\n\r\n\t\tif ( !string.IsNullOrWhiteSpace( help.Summary ) )\r\n\t\t{\r\n\t\t\t_detail.Layout.AddSpacingCell( 6f );\r\n\r\n\t\t\tvar summary = _detail.Layout.Add( new Label.Body( help.Summary ) );\r\n\r\n\t\t\tsummary.Color = PrismTheme.TextSecondary;\r\n\t\t\tsummary.WordWrap = true;\r\n\t\t}\r\n\r\n\t\tPorts( \u0022Inputs\u0022, help.Inputs );\r\n\t\tPorts( \u0022Outputs\u0022, help.Outputs );\r\n\r\n\t\tif ( help.Keywords.Count \u003E 0 )\r\n\t\t{\r\n\t\t\t_detail.Layout.AddSpacingCell( 8f );\r\n\r\n\t\t\tvar keywords = _detail.Layout.Add( new Label.Small( \u0022Also found by: \u0022 \u002B string.Join( \u0022, \u0022, help.Keywords ) ) );\r\n\r\n\t\t\tkeywords.Color = PrismTheme.TextDisabled;\r\n\t\t\tkeywords.WordWrap = true;\r\n\t\t}\r\n\r\n\t\t_detail.Layout.AddSpacingCell( 8f );\r\n\r\n\t\tvar id = _detail.Layout.Add( new Label.Small( $\u0022Type id  {help.Id}\u0022 ) );\r\n\r\n\t\tid.Color = PrismTheme.TextDisabled;\r\n\t\tid.TextSelectable = true;\r\n\r\n\t\tvar copy = _detail.Layout.Add( new Button( \u0022Copy Documentation\u0022, \u0022content_copy\u0022, this ) );\r\n\r\n\t\tcopy.Clicked = () =\u003E PrismLog.Guard( \u0022Copying node documentation\u0022,\r\n\t\t\t() =\u003E EditorUtility.Clipboard.Copy( help.ToPlainText() ) );\r\n\r\n\t\t_detail.Layout.AddStretchCell();\r\n\t}\r\n\r\n\tvoid Ports( string heading, IReadOnlyList\u003CPortDef\u003E ports )\r\n\t{\r\n\t\tif ( ports is null || ports.Count == 0 ) return;\r\n\r\n\t\t_detail.Layout.AddSpacingCell( 10f );\r\n\r\n\t\tvar header = _detail.Layout.Add( new Label.Header( heading ) );\r\n\r\n\t\theader.Color = PrismTheme.TextPrimary;\r\n\r\n\t\tforeach ( var port in ports )\r\n\t\t{\r\n\t\t\tvar row = _detail.Layout.AddRow();\r\n\r\n\t\t\trow.Spacing = 8f;\r\n\r\n\t\t\tvar name = row.Add( new Label( port.DisplayName ?? port.Id.ToString(), this ) );\r\n\r\n\t\t\tname.Color = PrismTheme.TextSecondary;\r\n\t\t\tname.MinimumWidth = 130f;\r\n\r\n\t\t\tvar declared = row.Add( new Label( port.DeclaredType ?? \u0022float\u0022, this ) );\r\n\r\n\t\t\tdeclared.Color = PrismTheme.TypeGeneric;\r\n\t\t\tdeclared.MinimumWidth = 70f;\r\n\r\n\t\t\tvar tooltip = row.Add( new Label( port.Tooltip ?? string.Empty, this ), 1 );\r\n\r\n\t\t\ttooltip.Color = PrismTheme.TextMuted;\r\n\t\t\ttooltip.WordWrap = true;\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"f4industries.prism","Path":"Editor/Prism/Model/Port.cs","FileName":"Port.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":340919,"Code":"using Editor.Prism.Core;\r\nusing System.ComponentModel;\r\nusing System.Reflection;\r\n\r\nnamespace Editor.Prism.Model;\r\n\r\n/// \u003Csummary\u003EWhich side of a node a port lives on.\u003C/summary\u003E\r\npublic enum PortDirection\r\n{\r\n\t/// \u003Csummary\u003EConsumes a value. At most one incoming edge.\u003C/summary\u003E\r\n\tInput,\r\n\t/// \u003Csummary\u003EProduces a value. Any number of outgoing edges.\u003C/summary\u003E\r\n\tOutput\r\n}\r\n\r\n/// \u003Csummary\u003EBehavioural flags on a port.\u003C/summary\u003E\r\n[Flags]\r\npublic enum PortFlags\r\n{\r\n\t/// \u003Csummary\u003ENothing special.\u003C/summary\u003E\r\n\tNone = 0,\r\n\t/// \u003Csummary\u003ELeaving this input unconnected with no inline value is an error.\u003C/summary\u003E\r\n\tRequired = 1 \u003C\u003C 0,\r\n\t/// \u003Csummary\u003ENot drawn on the card. Still connectable programmatically.\u003C/summary\u003E\r\n\tHidden = 1 \u003C\u003C 1,\r\n\t/// \u003Csummary\u003ENever draw an inline value pill for this input.\u003C/summary\u003E\r\n\tNoInlineEditor = 1 \u003C\u003C 2,\r\n\t/// \u003Csummary\u003EPart of a variadic group; the node grows another socket as this one is filled.\u003C/summary\u003E\r\n\tVariadic = 1 \u003C\u003C 3,\r\n\t/// \u003Csummary\u003EOpts out of type inference \u2014 the value passes through unchanged (reroute, custom code).\u003C/summary\u003E\r\n\tPassthrough = 1 \u003C\u003C 4,\r\n\t/// \u003Csummary\u003EDrawn in the node\u0027s title bar rather than a port row.\u003C/summary\u003E\r\n\tInTitleBar = 1 \u003C\u003C 5,\r\n\t/// \u003Csummary\u003EThis input accepts more than one incoming edge (variadic sums, subgraph fan-in).\u003C/summary\u003E\r\n\tAllowMultiple = 1 \u003C\u003C 6,\r\n\t/// \u003Csummary\u003ECreated by \u003Csee cref=\u0022PortBuilder\u0022/\u003E at runtime rather than by an attribute.\u003C/summary\u003E\r\n\tDynamic = 1 \u003C\u003C 7\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// A reference to one port of one node. This is the property type used by \u003Cc\u003E[In]\u003C/c\u003E and\r\n/// \u003Cc\u003E[Out]\u003C/c\u003E declarations, and the shape both ends of an \u003Csee cref=\u0022Edge\u0022/\u003E serialize as.\r\n/// \u003C/summary\u003E\r\npublic readonly record struct PortRef( NodeId Node, PortId Port )\r\n{\r\n\t/// \u003Csummary\u003EThe unset reference.\u003C/summary\u003E\r\n\tpublic static readonly PortRef None = default;\r\n\r\n\t/// \u003Csummary\u003ETrue when both halves are set.\u003C/summary\u003E\r\n\tpublic bool IsValid =\u003E Node.IsValid \u0026\u0026 Port.IsValid;\r\n\r\n\t/// \u003Csummary\u003EBuild a reference from raw strings.\u003C/summary\u003E\r\n\tpublic static PortRef Parse( string node, string port ) =\u003E new( NodeId.Parse( node ), PortId.Parse( port ) );\r\n\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tpublic override string ToString() =\u003E IsValid ? $\u0022{Node}.{Port}\u0022 : \u0022\u003Cnone\u003E\u0022;\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The immutable declaration of a port: what it is called, what type it claims to be and how it\r\n/// behaves. Produced by reflection over \u003Cc\u003E[In]\u003C/c\u003E/\u003Cc\u003E[Out]\u003C/c\u003E properties and then optionally\r\n/// amended by \u003Csee cref=\u0022PortBuilder\u0022/\u003E inside \u003Cc\u003EPrismNode.OnDefinePorts\u003C/c\u003E.\r\n/// \u003C/summary\u003E\r\npublic sealed record PortDef( PortId Id, string DisplayName, string DeclaredType, PortDirection Direction )\r\n{\r\n\t/// \u003Csummary\u003EOptional collapsible group on the card.\u003C/summary\u003E\r\n\tpublic string Group { get; init; }\r\n\r\n\t/// \u003Csummary\u003ETooltip shown on the handle and the label.\u003C/summary\u003E\r\n\tpublic string Tooltip { get; init; }\r\n\r\n\t/// \u003Csummary\u003EBehavioural flags.\u003C/summary\u003E\r\n\tpublic PortFlags Flags { get; init; }\r\n\r\n\t/// \u003Csummary\u003ESort key within the node. Ties keep declaration order.\u003C/summary\u003E\r\n\tpublic int Order { get; init; }\r\n\r\n\t/// \u003Csummary\u003EName of the \u003Cc\u003E[In]\u003C/c\u003E/\u003Cc\u003E[Out]\u003C/c\u003E property that declared this port, when there is one.\u003C/summary\u003E\r\n\tpublic string PropertyName { get; init; }\r\n\r\n\t/// \u003Csummary\u003EName of the \u003Cc\u003E[InlineValue]\u003C/c\u003E property that supplies the unconnected value, when there is one.\u003C/summary\u003E\r\n\tpublic string InlineValueProperty { get; init; }\r\n\r\n\t/// \u003Csummary\u003EFormer ids that must still deserialize into this port.\u003C/summary\u003E\r\n\tpublic IReadOnlyList\u003Cstring\u003E FormerIds { get; init; }\r\n\r\n\t/// \u003Csummary\u003ETrue when the declared type is a type variable rather than a concrete spelling.\u003C/summary\u003E\r\n\tpublic bool IsGeneric =\u003E TypeRules.IsTypeVariable( DeclaredType );\r\n\r\n\t/// \u003Csummary\u003EThe concrete declared type, or \u003Csee cref=\u0022ShaderType.Void\u0022/\u003E when the port is generic.\u003C/summary\u003E\r\n\tpublic ShaderType FixedType =\u003E ShaderType.Parse( DeclaredType );\r\n\r\n\t/// \u003Csummary\u003ETrue when leaving this input unconnected is an error.\u003C/summary\u003E\r\n\tpublic bool Required =\u003E ( Flags \u0026 PortFlags.Required ) != 0;\r\n\r\n\t/// \u003Csummary\u003ETrue when the port should not be drawn.\u003C/summary\u003E\r\n\tpublic bool Hidden =\u003E ( Flags \u0026 PortFlags.Hidden ) != 0;\r\n\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tpublic override string ToString() =\u003E $\u0022{Direction} {Id}:{DeclaredType}\u0022;\r\n}\r\n\r\n/// \u003Csummary\u003EA live port on a live node. Carries the declaration plus everything the solver resolves.\u003C/summary\u003E\r\npublic abstract class Port\r\n{\r\n\t/// \u003Csummary\u003EBuild a port from its declaration.\u003C/summary\u003E\r\n\tprotected Port( PrismNode node, PortDef def )\r\n\t{\r\n\t\tNode = node;\r\n\t\tDef = def;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The node this port belongs to.\r\n\t/// \u003Cpara\u003E\r\n\t/// Hidden from reflection-driven UI: this is a back-reference, so a \u003Cc\u003ESerializedObject\u003C/c\u003E walk\r\n\t/// that reaches a port would loop \u003Cc\u003Eport -\u0026gt; Node -\u0026gt; Inputs -\u0026gt; port\u003C/c\u003E forever. That is an\r\n\t/// uncatchable \u003Cc\u003EStackOverflowException\u003C/c\u003E inside engine code, which kills the whole editor.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\t[Hide, Browsable( false ), JsonIgnore]\r\n\tpublic PrismNode Node { get; }\r\n\r\n\t/// \u003Csummary\u003EThe declaration this port was built from.\u003C/summary\u003E\r\n\tpublic PortDef Def { get; internal set; }\r\n\r\n\t/// \u003Csummary\u003EStable id, unique within the node.\u003C/summary\u003E\r\n\tpublic PortId Id =\u003E Def.Id;\r\n\r\n\t/// \u003Csummary\u003EDisplay label. May be empty for an unlabelled socket.\u003C/summary\u003E\r\n\tpublic string DisplayName =\u003E Def.DisplayName;\r\n\r\n\t/// \u003Csummary\u003EThe declared type spelling, concrete or generic.\u003C/summary\u003E\r\n\tpublic string DeclaredType =\u003E Def.DeclaredType;\r\n\r\n\t/// \u003Csummary\u003EOptional port group.\u003C/summary\u003E\r\n\tpublic string Group =\u003E Def.Group;\r\n\r\n\t/// \u003Csummary\u003ETooltip text.\u003C/summary\u003E\r\n\tpublic string Tooltip =\u003E Def.Tooltip;\r\n\r\n\t/// \u003Csummary\u003EBehavioural flags.\u003C/summary\u003E\r\n\tpublic PortFlags Flags =\u003E Def.Flags;\r\n\r\n\t/// \u003Csummary\u003ETrue when leaving this input unconnected is an error.\u003C/summary\u003E\r\n\tpublic bool Required =\u003E Def.Required;\r\n\r\n\t/// \u003Csummary\u003EWhich side of the node this port is on.\u003C/summary\u003E\r\n\tpublic abstract PortDirection Direction { get; }\r\n\r\n\t/// \u003Csummary\u003EPosition within the node\u0027s port list. Assigned when the collection is built.\u003C/summary\u003E\r\n\tpublic int Index { get; internal set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The concrete type assigned by the type solver. Void until the first successful solve;\r\n\t/// for a non-generic port it always ends up equal to \u003Csee cref=\u0022PortDef.FixedType\u0022/\u003E.\r\n\t/// \u003C/summary\u003E\r\n\tpublic ShaderType ResolvedType { get; set; }\r\n\r\n\t/// \u003Csummary\u003EThe best type we know: the resolved one when solved, otherwise the declared one.\u003C/summary\u003E\r\n\tpublic ShaderType EffectiveType =\u003E ResolvedType.IsVoid ? Def.FixedType : ResolvedType;\r\n\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tpublic override string ToString() =\u003E $\u0022{Node?.Id}.{Id}\u0022;\r\n}\r\n\r\n/// \u003Csummary\u003EAn input port. At most one incoming edge unless \u003Csee cref=\u0022PortFlags.AllowMultiple\u0022/\u003E is set.\u003C/summary\u003E\r\npublic sealed class InputPort : Port\r\n{\r\n\t/// \u003Csummary\u003EBuild an input port.\u003C/summary\u003E\r\n\tpublic InputPort( PrismNode node, PortDef def ) : base( node, def ) { }\r\n\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tpublic override PortDirection Direction =\u003E PortDirection.Input;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The literal used when nothing is connected. Boxed because it may be any of the value shapes\r\n\t/// \u003Cc\u003EValueCodec\u003C/c\u003E understands; the node\u0027s \u003Cc\u003E[InlineValue]\u003C/c\u003E property is the authored source\r\n\t/// when \u003Csee cref=\u0022PortDef.InlineValueProperty\u0022/\u003E is set.\r\n\t/// \u003C/summary\u003E\r\n\tpublic object InlineValue { get; set; }\r\n\r\n\t/// \u003Csummary\u003ETrue when an edge terminates on this port.\u003C/summary\u003E\r\n\tpublic bool IsConnected =\u003E\r\n\t\tNode?.Graph is { } graph \u0026\u0026 graph.TryGetIncomingEdge( Node.Id, Id, out _ );\r\n}\r\n\r\n/// \u003Csummary\u003EAn output port. May fan out to any number of inputs.\u003C/summary\u003E\r\npublic sealed class OutputPort : Port\r\n{\r\n\t/// \u003Csummary\u003EBuild an output port.\u003C/summary\u003E\r\n\tpublic OutputPort( PrismNode node, PortDef def ) : base( node, def ) { }\r\n\r\n\t/// \u003Cinheritdoc/\u003E\r\n\tpublic override PortDirection Direction =\u003E PortDirection.Output;\r\n\r\n\t/// \u003Csummary\u003ETrue when at least one edge starts at this port.\u003C/summary\u003E\r\n\tpublic bool IsConnected =\u003E\r\n\t\tNode?.Graph is { } graph \u0026\u0026 graph.GetOutgoingEdges( Node.Id, Id ).Any();\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Builds the port list for a node: first from reflection over \u003Cc\u003E[In]\u003C/c\u003E/\u003Cc\u003E[Out]\u003C/c\u003E properties,\r\n/// then amended by the node\u0027s \u003Cc\u003EOnDefinePorts\u003C/c\u003E override. Ports that cannot be expressed as\r\n/// properties \u2014 variadic sockets, subgraph signatures, mode-dependent sets \u2014 are added here.\r\n/// \u003C/summary\u003E\r\npublic sealed class PortBuilder\r\n{\r\n\treadonly List\u003CPortDef\u003E _inputs = new();\r\n\treadonly List\u003CPortDef\u003E _outputs = new();\r\n\r\n\t/// \u003Csummary\u003EInput declarations, in socket order.\u003C/summary\u003E\r\n\tpublic IReadOnlyList\u003CPortDef\u003E Inputs =\u003E _inputs;\r\n\r\n\t/// \u003Csummary\u003EOutput declarations, in socket order.\u003C/summary\u003E\r\n\tpublic IReadOnlyList\u003CPortDef\u003E Outputs =\u003E _outputs;\r\n\r\n\t/// \u003Csummary\u003EAppend an input port.\u003C/summary\u003E\r\n\tpublic PortBuilder Input( string id, string type = \u0022float\u0022, string name = null, string group = null,\r\n\t\tPortFlags flags = PortFlags.None, string tooltip = null, int order = 0 )\r\n\t{\r\n\t\t_inputs.Add( new PortDef( PortId.Parse( id ), name ?? id, type ?? \u0022float\u0022, PortDirection.Input )\r\n\t\t{\r\n\t\t\tGroup = group,\r\n\t\t\tTooltip = tooltip,\r\n\t\t\tFlags = flags | PortFlags.Dynamic,\r\n\t\t\tOrder = order\r\n\t\t} );\r\n\r\n\t\treturn this;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EAppend an output port.\u003C/summary\u003E\r\n\tpublic PortBuilder Output( string id, string type = \u0022float\u0022, string name = null, string group = null,\r\n\t\tPortFlags flags = PortFlags.None, string tooltip = null, int order = 0 )\r\n\t{\r\n\t\t_outputs.Add( new PortDef( PortId.Parse( id ), name ?? id, type ?? \u0022float\u0022, PortDirection.Output )\r\n\t\t{\r\n\t\t\tGroup = group,\r\n\t\t\tTooltip = tooltip,\r\n\t\t\tFlags = flags | PortFlags.Dynamic,\r\n\t\t\tOrder = order\r\n\t\t} );\r\n\r\n\t\treturn this;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EAppend a declaration built elsewhere.\u003C/summary\u003E\r\n\tpublic PortBuilder Add( PortDef def )\r\n\t{\r\n\t\tif ( def is null ) return this;\r\n\r\n\t\tif ( def.Direction == PortDirection.Input ) _inputs.Add( def );\r\n\t\telse _outputs.Add( def );\r\n\r\n\t\treturn this;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ERemove a port by id from whichever side it is on.\u003C/summary\u003E\r\n\tpublic PortBuilder Remove( string id )\r\n\t{\r\n\t\tvar portId = PortId.Parse( id );\r\n\t\t_inputs.RemoveAll( x =\u003E x.Id == portId );\r\n\t\t_outputs.RemoveAll( x =\u003E x.Id == portId );\r\n\t\treturn this;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EChange a port\u0027s declared type.\u003C/summary\u003E\r\n\tpublic PortBuilder Retype( string id, string declaredType )\r\n\t{\r\n\t\tMutate( id, def =\u003E def with { DeclaredType = declaredType } );\r\n\t\treturn this;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EChange a port\u0027s display label.\u003C/summary\u003E\r\n\tpublic PortBuilder Rename( string id, string displayName )\r\n\t{\r\n\t\tMutate( id, def =\u003E def with { DisplayName = displayName } );\r\n\t\treturn this;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EAdd flags to a port.\u003C/summary\u003E\r\n\tpublic PortBuilder SetFlags( string id, PortFlags flags )\r\n\t{\r\n\t\tMutate( id, def =\u003E def with { Flags = def.Flags | flags } );\r\n\t\treturn this;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETrue when a port with this id exists on either side.\u003C/summary\u003E\r\n\tpublic bool Has( string id )\r\n\t{\r\n\t\tvar portId = PortId.Parse( id );\r\n\t\treturn _inputs.Any( x =\u003E x.Id == portId ) || _outputs.Any( x =\u003E x.Id == portId );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EDrop every declaration. Used by nodes that build their entire signature dynamically.\u003C/summary\u003E\r\n\tpublic PortBuilder Clear()\r\n\t{\r\n\t\t_inputs.Clear();\r\n\t\t_outputs.Clear();\r\n\t\treturn this;\r\n\t}\r\n\r\n\tvoid Mutate( string id, Func\u003CPortDef, PortDef\u003E mutate )\r\n\t{\r\n\t\tvar portId = PortId.Parse( id );\r\n\r\n\t\tfor ( int i = 0; i \u003C _inputs.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( _inputs[i].Id == portId ) _inputs[i] = mutate( _inputs[i] );\r\n\t\t}\r\n\r\n\t\tfor ( int i = 0; i \u003C _outputs.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tif ( _outputs[i].Id == portId ) _outputs[i] = mutate( _outputs[i] );\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EBuild a builder pre-populated with the reflected declarations of a node type.\u003C/summary\u003E\r\n\tpublic static PortBuilder FromReflection( Type nodeType )\r\n\t{\r\n\t\tvar builder = new PortBuilder();\r\n\t\tvar (inputs, outputs) = Reflect( nodeType );\r\n\r\n\t\tbuilder._inputs.AddRange( inputs );\r\n\t\tbuilder._outputs.AddRange( outputs );\r\n\r\n\t\treturn builder;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The port declarations implied by a node type\u0027s \u003Cc\u003E[In]\u003C/c\u003E/\u003Cc\u003E[Out]\u003C/c\u003E properties, in\r\n\t/// declaration order (base class first). Cached per type; call \u003Csee cref=\u0022FlushCache\u0022/\u003E on hotload.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static (IReadOnlyList\u003CPortDef\u003E Inputs, IReadOnlyList\u003CPortDef\u003E Outputs) Reflect( Type nodeType )\r\n\t{\r\n\t\tif ( nodeType is null ) return ( Array.Empty\u003CPortDef\u003E(), Array.Empty\u003CPortDef\u003E() );\r\n\r\n\t\tlock ( s_cacheLock )\r\n\t\t{\r\n\t\t\tif ( s_cache.TryGetValue( nodeType, out var cached ) ) return cached;\r\n\t\t}\r\n\r\n\t\tvar inputs = new List\u003CPortDef\u003E();\r\n\t\tvar outputs = new List\u003CPortDef\u003E();\r\n\r\n\t\tvar properties = nodeType\r\n\t\t\t.GetProperties( BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy )\r\n\t\t\t.OrderBy( DeclarationDepth )\r\n\t\t\t.ThenBy( x =\u003E x.MetadataToken )\r\n\t\t\t.ToArray();\r\n\r\n\t\t// Map port name -\u003E the [InlineValue] property that feeds it.\r\n\t\tvar inlineValues = new Dictionary\u003Cstring, string\u003E();\r\n\r\n\t\tforeach ( var property in properties )\r\n\t\t{\r\n\t\t\tvar inline = property.GetCustomAttribute\u003CInlineValueAttribute\u003E();\r\n\t\t\tif ( inline is null || string.IsNullOrEmpty( inline.PortName ) ) continue;\r\n\r\n\t\t\tinlineValues[inline.PortName] = property.Name;\r\n\t\t}\r\n\r\n\t\tforeach ( var property in properties )\r\n\t\t{\r\n\t\t\tvar formerly = property.GetCustomAttributes\u003CFormerlyKnownAsAttribute\u003E()\r\n\t\t\t\t.Select( x =\u003E x.OldName )\r\n\t\t\t\t.Where( x =\u003E !string.IsNullOrEmpty( x ) )\r\n\t\t\t\t.ToArray();\r\n\r\n\t\t\tif ( property.GetCustomAttribute\u003CInAttribute\u003E() is { } input )\r\n\t\t\t{\r\n\t\t\t\tinlineValues.TryGetValue( property.Name, out var inlineProperty );\r\n\r\n\t\t\t\tinputs.Add( new PortDef( PortId.Parse( property.Name ), input.Name ?? property.Name,\r\n\t\t\t\t\tinput.Type ?? \u0022float\u0022, PortDirection.Input )\r\n\t\t\t\t{\r\n\t\t\t\t\tGroup = input.Group,\r\n\t\t\t\t\tTooltip = input.Tooltip,\r\n\t\t\t\t\tFlags = input.Required ? PortFlags.Required : PortFlags.None,\r\n\t\t\t\t\tOrder = input.Order,\r\n\t\t\t\t\tPropertyName = property.Name,\r\n\t\t\t\t\tInlineValueProperty = inlineProperty,\r\n\t\t\t\t\tFormerIds = formerly.Length \u003E 0 ? formerly : null\r\n\t\t\t\t} );\r\n\t\t\t}\r\n\r\n\t\t\tif ( property.GetCustomAttribute\u003COutAttribute\u003E() is { } output )\r\n\t\t\t{\r\n\t\t\t\toutputs.Add( new PortDef( PortId.Parse( property.Name ), output.Name ?? property.Name,\r\n\t\t\t\t\toutput.Type ?? \u0022float\u0022, PortDirection.Output )\r\n\t\t\t\t{\r\n\t\t\t\t\tGroup = output.Group,\r\n\t\t\t\t\tTooltip = output.Tooltip,\r\n\t\t\t\t\tOrder = output.Order,\r\n\t\t\t\t\tPropertyName = property.Name,\r\n\t\t\t\t\tFormerIds = formerly.Length \u003E 0 ? formerly : null\r\n\t\t\t\t} );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tvar result = ( (IReadOnlyList\u003CPortDef\u003E)StableSort( inputs ), (IReadOnlyList\u003CPortDef\u003E)StableSort( outputs ) );\r\n\r\n\t\tlock ( s_cacheLock )\r\n\t\t{\r\n\t\t\ts_cache[nodeType] = result;\r\n\t\t}\r\n\r\n\t\treturn result;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EDrop the reflection cache. Must run on hotload \u2014 \u003Csee cref=\u0022PortDef\u0022/\u003Es outlive the assembly otherwise.\u003C/summary\u003E\r\n\tpublic static void FlushCache()\r\n\t{\r\n\t\tlock ( s_cacheLock )\r\n\t\t{\r\n\t\t\ts_cache.Clear();\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Sort by explicit order, keeping declaration order for ties, and drop duplicate ids \u2014 a derived\r\n\t/// class that redeclares a base port wins, because its declaration is the more specific one.\r\n\t/// \u003C/summary\u003E\r\n\tstatic PortDef[] StableSort( List\u003CPortDef\u003E defs )\r\n\t{\r\n\t\tvar deduped = new List\u003CPortDef\u003E( defs.Count );\r\n\r\n\t\tfor ( int i = 0; i \u003C defs.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar later = false;\r\n\r\n\t\t\tfor ( int j = i \u002B 1; j \u003C defs.Count; j\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tif ( defs[j].Id != defs[i].Id ) continue;\r\n\r\n\t\t\t\tlater = true;\r\n\t\t\t\tbreak;\r\n\t\t\t}\r\n\r\n\t\t\tif ( !later ) deduped.Add( defs[i] );\r\n\t\t}\r\n\r\n\t\treturn deduped.OrderBy( x =\u003E x.Order ).ToArray();\r\n\t}\r\n\r\n\tstatic int DeclarationDepth( PropertyInfo property )\r\n\t{\r\n\t\tvar depth = 0;\r\n\t\tvar type = property.DeclaringType;\r\n\r\n\t\twhile ( type is not null \u0026\u0026 type != typeof( object ) )\r\n\t\t{\r\n\t\t\tdepth\u002B\u002B;\r\n\t\t\ttype = type.BaseType;\r\n\t\t}\r\n\r\n\t\treturn depth;\r\n\t}\r\n\r\n\tstatic readonly Dictionary\u003CType, (IReadOnlyList\u003CPortDef\u003E Inputs, IReadOnlyList\u003CPortDef\u003E Outputs)\u003E s_cache = new();\r\n\tstatic readonly object s_cacheLock = new();\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The live ports of one node. Rebuilding preserves the resolved type and inline value of every\r\n/// port whose id survives; ports that disappear leave their edges to be converted into\r\n/// \u003Csee cref=\u0022BrokenEdge\u0022/\u003E ghosts by the graph, never silently deleted.\r\n/// \u003C/summary\u003E\r\npublic sealed class PortCollection\r\n{\r\n\treadonly List\u003CInputPort\u003E _inputs = new();\r\n\treadonly List\u003COutputPort\u003E _outputs = new();\r\n\r\n\t/// \u003Csummary\u003EInput ports, in socket order.\u003C/summary\u003E\r\n\tpublic IReadOnlyList\u003CInputPort\u003E Inputs =\u003E _inputs;\r\n\r\n\t/// \u003Csummary\u003EOutput ports, in socket order.\u003C/summary\u003E\r\n\tpublic IReadOnlyList\u003COutputPort\u003E Outputs =\u003E _outputs;\r\n\r\n\t/// \u003Csummary\u003EFind an input port by id.\u003C/summary\u003E\r\n\tpublic InputPort FindInput( PortId id ) =\u003E _inputs.FirstOrDefault( x =\u003E x.Id == id );\r\n\r\n\t/// \u003Csummary\u003EFind an output port by id.\u003C/summary\u003E\r\n\tpublic OutputPort FindOutput( PortId id ) =\u003E _outputs.FirstOrDefault( x =\u003E x.Id == id );\r\n\r\n\t/// \u003Csummary\u003EFind a port of either direction by id.\u003C/summary\u003E\r\n\tpublic Port Find( PortId id ) =\u003E (Port)FindInput( id ) ?? FindOutput( id );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Replace the port set with the declarations in \u003Cparamref name=\u0022builder\u0022/\u003E, carrying over state\r\n\t/// from ports whose ids survive. Returns the ids that disappeared.\r\n\t/// \u003Cpara\u003E\r\n\t/// Duplicate ids are tolerated rather than fatal: a node whose \u003Cc\u003EOnDefinePorts\u003C/c\u003E re-declares a\r\n\t/// reflected port keeps the last declaration, matching the \u0022more specific wins\u0022 rule the reflection\r\n\t/// pass already uses. A malformed node must never take the document down.\r\n\t/// \u003C/para\u003E\r\n\t/// \u003C/summary\u003E\r\n\tpublic IReadOnlyList\u003CPortId\u003E Apply( PrismNode node, PortBuilder builder )\r\n\t{\r\n\t\tvar removed = new List\u003CPortId\u003E();\r\n\r\n\t\tvar oldInputs = ToLookup( _inputs );\r\n\t\tvar oldOutputs = ToLookup( _outputs );\r\n\r\n\t\t_inputs.Clear();\r\n\t\t_outputs.Clear();\r\n\r\n\t\tforeach ( var def in Dedupe( builder.Inputs ) )\r\n\t\t{\r\n\t\t\tvar port = new InputPort( node, def ) { Index = _inputs.Count };\r\n\r\n\t\t\tif ( oldInputs.TryGetValue( def.Id, out var old ) )\r\n\t\t\t{\r\n\t\t\t\tport.ResolvedType = old.ResolvedType;\r\n\t\t\t\tport.InlineValue = old.InlineValue;\r\n\t\t\t\toldInputs.Remove( def.Id );\r\n\t\t\t}\r\n\r\n\t\t\t_inputs.Add( port );\r\n\t\t}\r\n\r\n\t\tforeach ( var def in Dedupe( builder.Outputs ) )\r\n\t\t{\r\n\t\t\tvar port = new OutputPort( node, def ) { Index = _outputs.Count };\r\n\r\n\t\t\tif ( oldOutputs.TryGetValue( def.Id, out var old ) )\r\n\t\t\t{\r\n\t\t\t\tport.ResolvedType = old.ResolvedType;\r\n\t\t\t\toldOutputs.Remove( def.Id );\r\n\t\t\t}\r\n\r\n\t\t\t_outputs.Add( port );\r\n\t\t}\r\n\r\n\t\tremoved.AddRange( oldInputs.Keys );\r\n\t\tremoved.AddRange( oldOutputs.Keys );\r\n\r\n\t\treturn removed;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EKeep the last declaration for each id, preserving declaration order otherwise.\u003C/summary\u003E\r\n\tstatic List\u003CPortDef\u003E Dedupe( IReadOnlyList\u003CPortDef\u003E defs )\r\n\t{\r\n\t\tvar result = new List\u003CPortDef\u003E( defs.Count );\r\n\r\n\t\tfor ( int i = 0; i \u003C defs.Count; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar later = false;\r\n\r\n\t\t\tfor ( int j = i \u002B 1; j \u003C defs.Count; j\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tif ( defs[j].Id != defs[i].Id ) continue;\r\n\r\n\t\t\t\tlater = true;\r\n\t\t\t\tbreak;\r\n\t\t\t}\r\n\r\n\t\t\tif ( !later ) result.Add( defs[i] );\r\n\t\t}\r\n\r\n\t\treturn result;\r\n\t}\r\n\r\n\tstatic Dictionary\u003CPortId, T\u003E ToLookup\u003CT\u003E( List\u003CT\u003E ports ) where T : Port\r\n\t{\r\n\t\tvar map = new Dictionary\u003CPortId, T\u003E();\r\n\r\n\t\tforeach ( var port in ports )\r\n\t\t{\r\n\t\t\tmap[port.Id] = port;\r\n\t\t}\r\n\r\n\t\treturn map;\r\n\t}\r\n}\r\n"}]}