-
Notifications
You must be signed in to change notification settings - Fork 69
Expand file tree
/
Copy pathCFrontendIR.h
More file actions
1171 lines (1057 loc) · 52.2 KB
/
CFrontendIR.h
File metadata and controls
1171 lines (1057 loc) · 52.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (C) 2022-2025 - DevSH Graphics Programming Sp. z O.O.
// This file is part of the "Nabla Engine".
// For conditions of distribution and use, see copyright notice in nabla.h
#ifndef _NBL_ASSET_MATERIAL_COMPILER_V3_C_FRONTEND_IR_H_INCLUDED_
#define _NBL_ASSET_MATERIAL_COMPILER_V3_C_FRONTEND_IR_H_INCLUDED_
#include "nbl/system/ILogger.h"
#include "nbl/asset/material_compiler3/CNodePool.h"
#include "nbl/asset/format/EColorSpace.h"
#include "nbl/asset/ICPUImageView.h"
// temporary
#define NBL_API
namespace nbl::asset::material_compiler3
{
// You make the Materials with a classical expression IR, one Root Node per material.
//
// Materials form a Layer Stack, each layer is statistically uncorrelated unlike `Weidlich, A., and Wilkie, A. Arbitrarily layered micro-facet surfaces` 2007
// we don't require that for a Microfacet Cook Torrance layer the ray must enter and exist through the same microfacet. Such an assumption only helps if you
// plan on ignoring every transmission through the microfacets within the statistical pixel footprint as given by the VNDF except the perfectly specular one.
// The energy loss from that leads to pathologies like the glGTF Specular+Diffuse model, comparison: https://x.com/DS2LightingMod/status/1961502201228267595
//
// If you don't plan on ignoring the actual convolution of incoming light by the BSDF, such an assumption only speeds up the Importance Sampling slightly as
// on the way back through a layer we don't consume another 2D random variable, instead transforming the ray deterministically. This however would require one
// to keep a stack of cached interactions with each layer, and its just simpler to run local path tracing through layers which can account for multiple scattering
// through a medium layer, etc.
//
// Our Frontend is built around the IR, which wants to perform the following canonicalization of a BSDF Layer (not including emission):
//
// f(w_i,w_o) = Sum_i^N Product_j^{N_i} h_{ij}(w_i,w_o) l_i(w_i,w_o)
//
// Where `l(w_i,w_o)` is a Contributor Node BxDF such as Oren Nayar or Cook-Torrance, which is doesn't model absorption and is usually Monochrome.
// These are assumed to be 100% valid BxDFs with White Furnace Test <= 1 and obeying Helmholtz Reciprocity. This is why you can't multiply two "Contributor Nodes" together.
// We make an attempt to implement Energy Normalized versions of `l_i` but not always, so there might be energy loss due to single scattering assumptions.
//
// This convention greatly simplifies the Layering of BSDFs as when we model two layers combined we need only consider the Sum terms which are Products of a BTDF contibutor
// in convolution with the layer below or above. For emission this is equivalent to convolving the emission with BTDFs producing a custom emission profile.
// Some of these combinations can be approximated or solved outright without resolving to frequency space approaches or path tracing within the layers.
//
// To obtain a valid BxDF for the canonical expression, each product of weights also needs to exhibit Helmholtz Reciprocity:
//
// Product_j^{N_i} h(w_i,w_o) = Product_j^{N_i} h(w_o,w_i)
//
// Which means that direction dependant weight nodes need to know the underlying contributor they are weighting to determine their semantics, e.g. a Fresnel on:
// - Cook Torrance will use the Microfacet Normal for any calculation as that is symmetric between `w_o` and `w_i`
// - Diffuse will use both `NdotV` and `NdotL` (also known as `theta_i` and `theta_o`) symmetrically
// - A BTDF will use the compliments (`1-x`) of the Fresnels
//
// We cannot derive BTDF factors from top and bottom BRDF as the problem is underconstrained, we don't know which factor models absorption and which part transmission.
//
// Helmholtz Reciprocity allows us to use completely independent BRDFs per hemisphere, when `w_i` and `w_o` are in the same hemisphere (reflection).
// Note that transmission only occurs when `w_i` and `w_o` are in opposite hemispheres and the reciprocity forces one BTDF.
//
// There's an implicit Top and Bottom on the layer stack, but thats only for the purpose of interpreting the Etas (predivided ratios of Indices of Refraction),
// both the Top and Bottom BRDF treat the Eta as being the speed of light in the medium above over the speed of light in the medium below.
// This means that for modelling air-vs-glass you use the same Eta for the Top BRDF, the middle BTDF and Bottom BRDF.
// We don't track the IoRs per layer because that would deprive us of the option to model each layer interface as a mixture of materials (metalness workflow).
//
// The backend can expand the Top BRDF, Middle BTDF, Bottom BRDF into 4 separate instruction streams for Front-Back BRDF and BTDF. This is because we can
// throw away the first or last BRDF+BTDF in the stack, as well as use different pre-computed Etas if we know the sign of `cos(theta_i)` as we interact with each layer.
// Whether the backend actually generates a separate instruction stream depends on the impact of Instruction Cache misses due to not sharing streams for layers.
//
// Also note that a single null BTDF in the stack splits it into the two separate stacks, one per original interaction orientation.
//
// I've considered expressing the layers using only a BTDF and BRDF (same top and bottom hemisphere) but that would lead to more layers in for materials,
// requiring the placing of a mirror then vantablack layer for most one-sided materials, and most importantly disallow the expression of certain front-back correlations.
//
// Because we implement Schussler et. al 2017 we also ensure that signs of dot products with shading normals are identical to smooth normals.
// However the smooth normals are not identical to geometric normals, we reserve the right to use the "normal pull up trick" to make them consistent.
// Schussler can't help with disparity of Smooth Normal and Geometric Normal, it turns smooth surfaces into glistening "disco balls" really outlining the
// polygonization. Using PN-Triangles/displacement would be the optimal solution here.
class CFrontendIR final : public CNodePool
{
using block_allocator_type = CNodePool::obj_pool_type::block_allocator_type;
template<typename T>
using _typed_pointer_type = CNodePool::obj_pool_type::mem_pool_type::typed_pointer_type<T>;
public:
// constructor
using creation_params_type = typename obj_pool_type::creation_params_type;
static inline core::smart_refctd_ptr<CFrontendIR> create(creation_params_type&& params)
{
if (params.composed.blockSizeKBLog2<4)
return nullptr;
return core::smart_refctd_ptr<CFrontendIR>(new CFrontendIR(std::move(params)),core::dont_grab);
}
template<typename T, uint16_t N, uint16_t M>
static inline void printMatrix(std::ostringstream& sstr, const hlsl::matrix<T,N,M>& m)
{
for (uint16_t i=0; i<N; i++)
{
if (i)
sstr << "\\n";
for (uint16_t j=0; j<M; j++)
{
if (j)
sstr << ",";
sstr << std::to_string(m[i][j]);
}
}
}
struct SParameter
{
inline operator bool() const
{
return abs(scale)<std::numeric_limits<float>::infinity() && (!view || viewChannel<getFormatChannelCount(view->getCreationParameters().format));
}
inline bool operator!=(const SParameter& other) const
{
if (scale!=other.scale)
return true;
if (viewChannel!=other.viewChannel)
return true;
// don't compare paddings!
if (view!=other.view)
return true;
return sampler!=other.sampler;
}
inline bool operator==(const SParameter& other) const {return !operator!=(other);}
NBL_API2 void printDot(std::ostringstream& sstr, const core::string& selfID) const;
// at this stage we store the multipliers in highest precision
float scale = std::numeric_limits<float>::infinity();
// rest are ignored if the view is null
uint8_t viewChannel : 2 = 0;
uint8_t padding[3] = {0,0,0}; // TODO: padding stores metadata, shall we exclude from assignment and copy operators?
core::smart_refctd_ptr<const ICPUImageView> view = {};
// shadow comparison functions are ignored
// NOTE: could take only things that matter from the sampler and pack the viewChannel and reduce padding
ICPUSampler::SParams sampler = {};
};
// In the forest, this is not a node, we'll deduplicate later
template<uint8_t Count>
struct SParameterSet
{
private:
friend class CSpectralVariable;
template<typename StringConstIterator=const core::string*>
inline void printDot(const uint8_t _count, std::ostringstream& sstr, const core::string& selfID, StringConstIterator paramNameBegin={}, const bool uvRequired=false) const
{
bool imageUsed = false;
for (uint8_t i=0; i<_count; i++)
{
const auto paramID = selfID+"_param"+std::to_string(i);
if (params[i].view)
imageUsed = true;
params[i].printDot(sstr,paramID);
sstr << "\n\t" << selfID << " -> " << paramID;
if (paramNameBegin)
sstr <<" [label=\"" << *(paramNameBegin++) << "\"]";
else
sstr <<" [label=\"Param " << std::to_string(i) <<"\"]";
}
if (uvRequired || imageUsed)
{
const auto uvTransformID = selfID+"_uvTransform";
sstr << "\n\t" << uvTransformID << " [label=\"uvSlot = " << std::to_string(uvSlot()) << "\\n";
printMatrix(sstr,*reinterpret_cast<const decltype(uvTransform)*>(params+_count));
sstr << "\"]";
sstr << "\n\t" << selfID << " -> " << uvTransformID << "[label=\"UV Transform\"]";
}
}
public:
inline operator bool() const
{
for (uint8_t i=0; i<Count; i++)
if (!params[i])
return false;
return true;
}
// Ignored if no modulator textures and isotropic BxDF
uint8_t& uvSlot() {return params[0].padding[0];}
const uint8_t& uvSlot() const {return params[0].padding[0];}
// Note: the padding abuse
static_assert(sizeof(SParameter::padding)>0);
template<typename StringConstIterator=const core::string*>
inline void printDot(std::ostringstream& sstr, const core::string& selfID, StringConstIterator paramNameBegin={}, const bool uvRequired=false) const
{
printDot<StringConstIterator>(Count,sstr,selfID,std::forward<StringConstIterator>(paramNameBegin),uvRequired);
}
// identity transform by default, ignored if no UVs
// NOTE: a transform could be applied per-param, whats important that the UV slot remains the smae across all of them.
hlsl::float32_t2x3 uvTransform = hlsl::float32_t2x3(
1,0,0,
0,1,0
);
SParameter params[Count] = {};
// to make sure there will be no padding inbetween
static_assert(alignof(SParameter)>=alignof(hlsl::float32_t2x3));
};
// basic "built-in" nodes
class INode : public CNodePool::INode
{
public:
_typed_pointer_type<CNodePool::CDebugInfo> debugInfo;
};
//
template<typename T> requires std::is_base_of_v<INode,std::remove_const_t<T>>
using typed_pointer_type = _typed_pointer_type<T>;
class IExprNode;
#define TYPE_NAME_STR(NAME) "nbl::asset::material_compiler3::CFrontendIR::"#NAME
// All layers are modelled as coatings, most combinations are not feasible and what combos are feasible depend on the compiler backend you use.
// Do not use Coatings for things which can be achieved with linear blends! (e.g. alpha transparency)
// TODO: can we have an object with a v-table thats still trivially destructible (just to know the type, but not set-up/run down anything - so no alloc tracking)
class CLayer final : public obj_pool_type::INonTrivial, public INode
{
public:
inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CLayer);}
// you can set the children later
inline CLayer() = default;
// Whether the layer is a BSDF depends on having a non-null 2nd child (a transmission component)
// A whole material is a BSDF iff. all layers have a non-null BTDF, otherwise its two separate layered BRDFs.
inline bool isBSDF() const {return bool(btdf);}
// A null BRDF will not produce reflections, while a null BTDF will not allow any transmittance.
// The laws of BSDFs require reciprocity so we can only have one BTDF, but they allow separate/different BRDFs
// Concrete example, think Vantablack stuck to a Aluminimum foil on the other side.
_typed_pointer_type<IExprNode> brdfTop = {};
_typed_pointer_type<IExprNode> btdf = {};
// when dealing with refractice indices, we expect the `brdfTop` and `brdfBottom` to be in sync (reciprocals of each other)
_typed_pointer_type<IExprNode> brdfBottom = {};
// The layer below us, if in the stack there's a layer with a null BTDF, we reserve the right to split up the material into two separate
// materials, one for the front and one for the back face in the final IR. Everything between the first and last null BTDF will get discarded.
_typed_pointer_type<CLayer> coated = {};
};
//
class IExprNode : public INode
{
public:
// Only sane child count allowed
virtual uint8_t getChildCount() const = 0;
inline _typed_pointer_type<IExprNode> getChildHandle(const uint8_t ix)
{
if (ix<getChildCount())
return getChildHandle_impl(ix);
return {};
}
inline _typed_pointer_type<const IExprNode> getChildHandle(const uint8_t ix) const
{
auto retval = const_cast<IExprNode*>(this)->getChildHandle(ix);
return retval;
}
// A "contributor" of a term to the lighting equation: a BxDF (reflection or tranmission) or Emitter term
// Contributors are not allowed to be multiplied together, but every additive term in the Expression must contain a contributor factor.
enum class Type : uint8_t
{
Contributor = 0,
Mul = 1,
Add = 2,
Other = 3
};
virtual inline Type getType() const {return Type::Other;}
protected:
friend class CFrontendIR;
// copy
virtual _typed_pointer_type<IExprNode> copy(CFrontendIR* ir) const = 0;
#define COPY_DEFAULT_IMPL inline _typed_pointer_type<IExprNode> copy(CFrontendIR* ir) const override final \
{ \
auto& pool = ir->getObjectPool(); \
const auto copyH = pool.emplace<std::remove_const_t<std::remove_pointer_t<decltype(this)> > >(); \
if (auto* const copy = pool.deref(copyH); copyH) \
*copy = *this; \
return copyH; \
}
// child managment
virtual inline _typed_pointer_type<IExprNode> getChildHandle_impl(const uint8_t ix) const {assert(false); return {};}
inline void setChild(const uint8_t ix, _typed_pointer_type<IExprNode> newChild)
{
assert(ix<getChildCount());
setChild_impl(ix,newChild);
}
virtual inline void setChild_impl(const uint8_t ix, _typed_pointer_type<IExprNode> newChild) {assert(false);}
// default is no special checks beyond the above
struct SInvalidCheckArgs
{
const CFrontendIR* pool;
system::logger_opt_ptr logger;
bool isBTDF;
// there's space for 7 more bools
};
// by default all children are mandatory
virtual inline bool invalid(const SInvalidCheckArgs& args) const
{
const auto childCount = getChildCount();
for (uint8_t i=0u; i<childCount; i++)
if (const auto childHandle=getChildHandle_impl(i); !childHandle)
{
args.logger.log("Default `IExprNode::invalid` child #%u missing!",system::ILogger::ELL_ERROR,i);
return true;
}
return false;
}
virtual bool inline reciprocatable() const {return false;}
// unless you override it, you're not supposed to call it
virtual void reciprocate(IExprNode* dst) const {assert(reciprocatable() && dst);}
virtual inline core::string getLabelSuffix() const {return "";}
virtual inline std::string_view getChildName_impl(const uint8_t ix) const {return "";}
virtual inline void printDot(std::ostringstream& sstr, const core::string& selfID) const {}
};
//! Base class for leaf node quantities which contribute additively to the Lighting Integral
class IContributor : public IExprNode
{
public:
inline Type getType() const override final {return Type::Contributor;}
};
// This node could also represent non directional emission, but we have another node for that
class CSpectralVariable final : public obj_pool_type::IVariableSize, public IExprNode
{
public:
inline uint8_t getChildCount() const override final { return 0; }
inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CSpectralVariable);}
// Variable length but has no children
enum class Semantics : uint8_t
{
NoneUndefined = 0,
// 3 knots, their wavelengths are implied and fixed at color primaries
Fixed3_SRGB = 1,
Fixed3_DCI_P3 = 2,
Fixed3_BT2020 = 3,
Fixed3_AdobeRGB = 4,
Fixed3_AcesCG = 5,
// Ideas: each node is described by (wavelength,value) pair
// PairsLinear = 5, // linear interpolation
// PairsLogLinear = 5, // linear interpolation in wavelenght log space
};
//
template<uint8_t Count>
struct SCreationParams
{
// Knots are "data points" on the (wavelength,value) plot, from which we can interpolate the rest of the spectrum
SParameterSet<Count> knots = {};
// a little bit of abuse and padding reuse
static_assert(sizeof(SParameter::padding)>2);
template<bool Enable=true> requires (Enable==(Count>1))
Semantics& getSemantics() {return reinterpret_cast<Semantics&>(knots.params[0].padding[2]); }
template<bool Enable=true> requires (Enable==(Count>1))
const Semantics& getSemantics() const {return const_cast<const Semantics&>(const_cast<SCreationParams<Count>*>(this)->getSemantics());}
};
//
template<uint8_t Count>
inline CSpectralVariable(SCreationParams<Count>&& params)
{
// back up the count
params.knots.params[0].padding[1] = Count;
// set it correctly for monochrome
if constexpr (Count==1)
params.knots.params[0].padding[2] = static_cast<uint8_t>(Semantics::NoneUndefined);
else
{
assert(params.getSemantics()!=Semantics::NoneUndefined);
}
std::construct_at(reinterpret_cast<SCreationParams<Count>*>(this+1),std::move(params));
}
inline CSpectralVariable(const CSpectralVariable& other)
{
std::uninitialized_copy_n(other.pWonky(),other.getKnotCount(),pWonky());
}
// encapsulation due to padding abuse
inline uint8_t& uvSlot() {return pWonky()->knots.uvSlot();}
inline const uint8_t& uvSlot() const {return pWonky()->knots.uvSlot();}
// these getters are immutable
inline uint8_t getKnotCount() const
{
static_assert(sizeof(SParameter::padding)>1);
return paramsBeginPadding()[1];
}
inline Semantics getSemantics() const
{
static_assert(sizeof(SParameter::padding)>2);
const auto retval = static_cast<Semantics>(paramsBeginPadding()[2]);
assert((getKnotCount()==1)==(retval==Semantics::NoneUndefined));
return retval;
}
//
inline SParameter* getParam(const uint8_t i)
{
if (i<getKnotCount())
return &pWonky()->knots.params[i];
return nullptr;
}
inline const SParameter* getParam(const uint8_t i) const {return const_cast<const SParameter*>(const_cast<CSpectralVariable*>(this)->getParam(i));}
//
template<uint8_t Count>
static inline uint32_t calc_size(const SCreationParams<Count>&)
{
return sizeof(CSpectralVariable)+sizeof(SCreationParams<Count>);
}
// for copying
static inline uint32_t calc_size(const CSpectralVariable& other)
{
return sizeof(CSpectralVariable)+sizeof(SCreationParams<1>)+(other.getKnotCount()-1)*sizeof(SParameter);
}
inline operator bool() const {return !invalid(SInvalidCheckArgs{.pool=nullptr,.logger=nullptr});}
protected:
inline ~CSpectralVariable()
{
std::destroy_n(pWonky()->knots.params,getKnotCount());
}
inline _typed_pointer_type<IExprNode> copy(CFrontendIR* ir) const override final
{
auto& pool = ir->getObjectPool();
const uint8_t count = getKnotCount();
return pool.emplace<CSpectralVariable>(*this);
}
inline bool invalid(const SInvalidCheckArgs& args) const override
{
const auto knotCount = getKnotCount();
// non-monochrome spectral variable
if (const auto semantic=getSemantics(); knotCount>1)
switch (semantic)
{
case Semantics::Fixed3_SRGB: [[fallthrough]];
case Semantics::Fixed3_DCI_P3: [[fallthrough]];
case Semantics::Fixed3_BT2020: [[fallthrough]];
case Semantics::Fixed3_AdobeRGB: [[fallthrough]];
case Semantics::Fixed3_AcesCG:
if (knotCount!=3)
{
args.logger.log("Semantic %d is only usable with 3 knots, this has %d knots",system::ILogger::ELL_ERROR,static_cast<uint8_t>(semantic),knotCount);
return false;
}
break;
default:
args.logger.log("Invalid Semantic %d",system::ILogger::ELL_ERROR,static_cast<uint8_t>(semantic));
return true;
}
for (auto i=0u; i<knotCount; i++)
if (!*getParam(i))
{
args.logger.log("Knot %u parameters invalid",system::ILogger::ELL_ERROR,i);
return true;
}
return false;
}
NBL_API2 core::string getLabelSuffix() const override;
NBL_API2 void printDot(std::ostringstream& sstr, const core::string& selfID) const override;
private:
SCreationParams<1>* pWonky() {return reinterpret_cast<SCreationParams<1>*>(this+1);}
const SCreationParams<1>* pWonky() const {return reinterpret_cast<const SCreationParams<1>*>(this+1);}
const uint8_t* paramsBeginPadding() const {return pWonky()->knots.params[0].padding;}
};
//
class IUnaryOp : public obj_pool_type::INonTrivial, public IExprNode
{
protected:
inline typed_pointer_type<IExprNode> getChildHandle_impl(const uint8_t ix) const override final {return child;}
inline void setChild_impl(const uint8_t ix, _typed_pointer_type<IExprNode> newChild) override final {child = newChild;}
public:
inline uint8_t getChildCount() const override final {return 1;}
typed_pointer_type<IExprNode> child = {};
};
class IBinOp : public obj_pool_type::INonTrivial, public IExprNode
{
protected:
inline typed_pointer_type<IExprNode> getChildHandle_impl(const uint8_t ix) const override final {return ix ? rhs:lhs;}
inline void setChild_impl(const uint8_t ix, _typed_pointer_type<IExprNode> newChild) override final {*(ix ? &rhs:&lhs) = newChild;}
inline std::string_view getChildName_impl(const uint8_t ix) const override final {return ix ? "rhs":"lhs";}
public:
inline uint8_t getChildCount() const override final {return 2;}
typed_pointer_type<IExprNode> lhs = {};
typed_pointer_type<IExprNode> rhs = {};
};
//! Basic combiner nodes
class CMul final : public IBinOp
{
public:
inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CMul);}
inline Type getType() const override {return Type::Mul;}
// you can set the children later
inline CMul() = default;
protected:
COPY_DEFAULT_IMPL
};
class CAdd final : public IBinOp
{
public:
inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CAdd);}
inline Type getType() const override {return Type::Add;}
// you can set the children later
inline CAdd() = default;
protected:
COPY_DEFAULT_IMPL
};
// does `1-expression`
class CComplement final : public IUnaryOp
{
public:
inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CComplement);}
// you can set the children later
inline CComplement() = default;
protected:
COPY_DEFAULT_IMPL
};
// Emission nodes are only allowed in BRDF expressions, not BTDF. To allow different emission on both sides, expressed unambigously.
// Basic Emitter - note that it is of unit radiance so its easier to importance sample
class CEmitter final : public obj_pool_type::INonTrivial, public IContributor
{
public:
inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CEmitter);}
inline uint8_t getChildCount() const override {return 0;}
// you can set the members later
inline CEmitter() = default;
// This can be anything like an IES profile, if invalid, there's no directionality to the emission
// `profile.scale` can still be used to influence the light strength without influencing NEE light picking probabilities
SParameter profile = {};
hlsl::float32_t3x3 profileTransform = hlsl::float32_t3x3(
1,0,0,
0,1,0,
0,0,1
);
// TODO: semantic flags/metadata (symmetries of the profile)
protected:
COPY_DEFAULT_IMPL
NBL_API2 bool invalid(const SInvalidCheckArgs& args) const override;
NBL_API2 void printDot(std::ostringstream& sstr, const core::string& selfID) const override;
};
//! Special nodes meant to be used as `CMul::rhs`, their behaviour depends on the IContributor in its MUL node relative subgraph.
//! If you use a different contributor node type or normal for shading, these nodes get split and duplicated into two in our Final IR.
//! Due to the Helmholtz Reciprocity handling outlined in the comments for the entire front-end you can usually count on these nodes
//! getting applied once using `VdotH` for Cook-Torrance BRDF, twice using `VdotN` and `LdotN` for Diffuse BRDF, and using their
//! complements before multiplication for BTDFs.
class IContributorDependant : public obj_pool_type::INonTrivial, public IExprNode
{
};
// Beer's Law Node, behaves differently depending on where it is:
// - to get an extinction medium, multiply it with CDeltaTransmission BTDF placed between two BRDFs in the same medium
// - to get a scattering medium between two Layers, create a layer with just a BTDF set up like above
// - to apply the beer's law on a single microfacet or a BRDF or BTDF multiply it with a BxDF
// Note: Even it makes little sense, Beer can be applied to the most outermost BRDF to simulate a correllated "foggy" coating without an extra BRDF layer.
class CBeer final : public IContributorDependant
{
public:
inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CBeer);}
inline uint8_t getChildCount() const override {return 2;}
// you can set the members later
inline CBeer() = default;
// Effective transparency = exp2(log2(perpTransmittance)*thickness/dot(refract(V,X,eta),X)) = exp2(log2(perpTransmittance)*thickness*inversesqrt(1.f+(LdotX-1)*rcpEta))
// Eta and `LdotX` is taken from the leaf BTDF node. With refractions from Dielectrics, we get just `1/LdotX`, for Delta Transmission we get `1/VdotN` since its the same
typed_pointer_type<CSpectralVariable> perpTransmittance = {};
typed_pointer_type<CSpectralVariable> thickness = {};
protected:
COPY_DEFAULT_IMPL
inline typed_pointer_type<IExprNode> getChildHandle_impl(const uint8_t ix) const override {return ix ? perpTransmittance:thickness;}
inline void setChild_impl(const uint8_t ix, _typed_pointer_type<IExprNode> newChild) override
{
*(ix ? &perpTransmittance:&thickness) = block_allocator_type::_static_cast<CSpectralVariable>(newChild);
}
inline std::string_view getChildName_impl(const uint8_t ix) const override {return "Perpendicular\\nTransmittance";}
NBL_API2 bool invalid(const SInvalidCheckArgs& args) const override;
};
// The "oriented" in the Etas means from frontface to backface, so there's no need to reciprocate them when creating matching BTDF for BRDF
// @kept_secret TODO: Thin Film Interference Fresnel
class CFresnel final : public IContributorDependant
{
public:
inline uint8_t getChildCount() const override {return 2;}
inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CFresnel);}
inline CFresnel() = default;
// Already pre-divided Index of Refraction, e.g. exterior/interior since VdotG>0 the ray always arrives from the exterior.
typed_pointer_type<CSpectralVariable> orientedRealEta = {};
// Specifying this turns your Fresnel into a conductor one, note that currently these are disallowed on BTDFs!
typed_pointer_type<CSpectralVariable> orientedImagEta = {};
// if you want to reuse the same parameter but want to flip the interfaces around
uint8_t reciprocateEtas : 1 = false;
protected:
COPY_DEFAULT_IMPL
inline typed_pointer_type<IExprNode> getChildHandle_impl(const uint8_t ix) const override {return ix ? orientedImagEta:orientedRealEta;}
inline void setChild_impl(const uint8_t ix, _typed_pointer_type<IExprNode> newChild) override
{
*(ix ? &orientedImagEta:&orientedRealEta) = block_allocator_type::_static_cast<CSpectralVariable>(newChild);
}
NBL_API2 bool invalid(const SInvalidCheckArgs& args) const override;
inline bool reciprocatable() const override {return true;}
inline void reciprocate(IExprNode* dst) const override
{
(*static_cast<CFresnel*>(dst) = *this).reciprocateEtas = ~reciprocateEtas;
}
inline std::string_view getChildName_impl(const uint8_t ix) const override {return ix ? "Imaginary":"Real";}
NBL_API2 void printDot(std::ostringstream& sstr, const core::string& selfID) const override;
};
// Compute Inifinite Scatter and extinction between two parallel infinite planes.
//
// It's a specialization of what would be a layer of two identical smooth BRDF and BTDF with arbitrary Fresnel function and beer's
// extinction, all applied on a per micro-facet basis (layering per microfacet, not whole surface) so the NDFs of two layers would be correlated.
//
// We actually allow you to use different reflectance nodes R_u and R_b, the NDFs of both layers remain the same but Reflectance Functions differ.
// Note that e.g. using different Etas for the Fresnel used for the top and bottom reflectance will result in a compound Fresnel!=1.0
// meaning that in such case you can no longer optimize the BTDF contributor into a DeltaTransmission but need a zero-roughness CookTorrance with
// an Eta equal to the ratio of the first Eta over the second Eta (note that when they're equal the ratio is 1 which turns into Delta Trans).
// This will require you to make an AST that "seems wrong" that is where neither of the Etas of the CFresnel nodes match the Cook Torrance one.
//
// Because we split BRDF and BTDF into separate expressions, what this node computes differs depending on where it gets used:
// Note the transformation in the equations at the end just makes the prevention of 0/0 or 0*INF same as for a non-extinctive equation, just check `R_u*R_b < Threshold`
//
// BRDF: R_u + (1-R_u)^2 E^2 R_b Sum_{i=0}^{\Inf}{(R_b R_u E^2)^i} = R_u + (1-R_u)^2 E^2 R_b / (1 - R_u R_b E^2) = R_u + (1-R_u)^2 R_b / (E^-2 - R_u R_b)
// --------------------
// Top BRDF as multiplied with CThinInfiniteScatterCorrection node with `reflectanceTop`
// BTDF matching the BRDF above
// Bottom BRDF matching Top (but corellated so you always hit the same microfacet going back)
// Null BRDF
// Delta Transmission Beer extinction
// Null BRDF
// Top Smooth BRDF with `reflectanceBottom` applied to a Delta Reflection
// ------------------
//
// BTDF: (1-R_u) E (1-R_b) Sum_{i=0}^{\Inf}{(R_b R_u E^2)^i} = (1-R_u) E^2 (1-R_b) / (1 - R_u R_b E^2) = (1-R_u) (1-R_b) / (E^-2 - R_u R_b)
// --------------------
// Bottom BRDF as multiplied with CThinInfiniteScatterCorrection node with `reflectanceTop`
// Null BRDF
// Delta Transmission Beer extinction
// Null BRDF
// Top BRDF as multiplied with CThinInfiniteScatterCorrection node but with `reflectanceBottom` (but corellated so you always hit the same microfacet leading to no refraction)
// ------------------
//
// The obvious downside of using this node for transmission is that its impossible to get "milky" glass because a spread of refractions is needed
class CThinInfiniteScatterCorrection final : public obj_pool_type::INonTrivial, public IExprNode
{
protected:
COPY_DEFAULT_IMPL
inline typed_pointer_type<IExprNode> getChildHandle_impl(const uint8_t ix) const override {return ix ? (ix>1 ? reflectanceBottom:extinction):reflectanceTop;}
inline void setChild_impl(const uint8_t ix, _typed_pointer_type<IExprNode> newChild) override
{
*(ix ? (ix>1 ? &reflectanceBottom:&extinction):&reflectanceTop) = newChild;
}
NBL_API2 bool invalid(const SInvalidCheckArgs& args) const override;
inline std::string_view getChildName_impl(const uint8_t ix) const override {return ix ? (ix>1 ? "reflectanceBottom":"extinction"):"reflectanceTop";}
public:
inline uint8_t getChildCount() const override final {return 3;}
inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CThinInfiniteScatterCorrection);}
// you can set the children later
inline CThinInfiniteScatterCorrection() = default;
typed_pointer_type<IExprNode> reflectanceTop = {};
// optional
typed_pointer_type<IExprNode> extinction = {};
typed_pointer_type<IExprNode> reflectanceBottom = {};
};
//! Basic BxDF nodes
// Every BxDF leaf node is supposed to pass WFT test and must not create energy, color and extinction is added on later via multipliers
class IBxDF : public obj_pool_type::INonTrivial, public IContributor
{
public:
// Why are all of these kept together and forced to fetch from the same UV ?
// Because they're supposed to be filtered together with the knowledge of the NDF
// TODO: should really be 5 parameters (2+3) cause of rotatable anisotropic roughness
struct SBasicNDFParams : SParameterSet<4>
{
inline auto getDerivMap() {return std::span<SParameter,2>(params,2);}
inline auto getDerivMap() const {return std::span<const SParameter,2>(params,2);}
inline auto getRougness() {return std::span<SParameter,2>(params+2,2);}
inline auto getRougness() const {return std::span<const SParameter,2>(params+2,2);}
inline SBasicNDFParams()
{
// initialize with constant flat deriv map and smooth roughness
for (auto& param : params)
param.scale = 0.f;
}
// conservative check, checks if we can optimize certain things this way
inline bool definitelyIsotropic() const
{
// a derivative map from a texture allows for anisotropic NDFs at higher mip levels when pre-filtered properly
for (auto i=0; i<2; i++)
if (getDerivMap()[i].scale!=0.f && getDerivMap()[i].view)
return false;
// if roughness inputs are not equal (same scale, same texture) then NDF can be anisotropic in places
if (getRougness()[0]!=getRougness()[1])
return false;
// if a reference stretch is used, stretched triangles can turn the distribution anisotropic
return stretchInvariant();
}
// whether the derivative map and roughness is constant regardless of UV-space texture stretching
inline bool stretchInvariant() const {return !(abs(hlsl::determinant(reference))>std::numeric_limits<float>::min());}
NBL_API2 void printDot(std::ostringstream& sstr, const core::string& selfID) const;
// Ignored if not invertible, otherwise its the reference "stretch" (UV derivatives) at which identity roughness and normalmapping occurs
hlsl::float32_t2x2 reference = hlsl::float32_t2x2(0,0,0,0);
};
// For Schussler et. al 2017 we'll spawn 2-3 additional BRDF leaf nodes in the proper IR for every normalmap present
};
// Delta Transmission is the only Special Delta Distribution Node, because of how useful it is for compiling Anyhit shaders, the rest can be done easily with:
// - Delta Reflection -> Any Cook Torrance BxDF with roughness=0 attached as BRDF
// - Smooth Conductor -> above multiplied with Conductor-Fresnel
// - Smooth Dielectric -> Any Cook Torrance BxDF with roughness=0 attached as BRDF on both sides of a Layer and BTDF multiplied with Dielectric-Fresnel (no imaginary component)
// - Thindielectric Correlated -> Cook Torrance BxDF multiplied with Dielectric-Fresnel as top BRDF and its reciprocal as the bottom, then Delta Transmission as BTDF with fresnels of similar Eta
// - Thindielectric Uncorrelated -> BRDF and BTDF same as above, no bottom BRDF, then another layer with delta transmission BTDF
// For Smooth dielectrics it makes sense because fresnel of the interface is the same (microfacet equals macro surface normal, no confusion)
// For Rough its a little more complicated, but using the same BTDF still makes sense.
// Why? Because you enter all microfacets at once with a ray packet, and because their backfaces are correlated you don't refract.
// If we then assume that they're quite big in relation to the thickness, most of the Total Internal Reflection stays within the same microfacet slab.
// So for a single microfacet we have the thindielectric infinite TIR equation with `R_u = (1-Fresnel(VdotH))` and `R_b = (1-Fresnel(-LdotH))`,
// which when convolved with the VNDF (integral of complete TIR equation over all H) can be approximated by substitution of `...dotH` with `...dotN`.
// It also wouldn't matter if we dictate each slab have uniform perpendicular or geometric normal thickness, as the VNDF keeps projected surface area proportional to microfacet angle.
// So the average VdotH or LdotH are equal to NdotV and NdotL respectively, which doesn't guarantee average `inversesqrt(1-VdotH*VdotH)` equals `inversesqrt(1-NdotV*NdotV)` but difference is small.
// - Plastic -> Similar to layering the above over Diffuse BRDF, its of uttmost importance that the BTDF is Delta Transmission.
class CDeltaTransmission final : public IBxDF
{
public:
inline uint8_t getChildCount() const override {return 0;}
inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CDeltaTransmission);}
inline CDeltaTransmission() = default;
protected:
COPY_DEFAULT_IMPL
};
//! Because of Schussler et. al 2017 every one of these nodes splits into 2 (if no L dependence) or 3 during canonicalization
// Base diffuse node
class COrenNayar final : public IBxDF
{
public:
inline uint8_t getChildCount() const override {return 0;}
inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(COrenNayar);}
inline COrenNayar() = default;
SBasicNDFParams ndParams = {};
protected:
COPY_DEFAULT_IMPL
NBL_API2 bool invalid(const SInvalidCheckArgs& args) const override;
NBL_API2 void printDot(std::ostringstream& sstr, const core::string& selfID) const override;
};
// Supports anisotropy for all models
class CCookTorrance final : public IBxDF
{
public:
inline uint8_t getChildCount() const override {return 1;}
enum class NDF : uint8_t
{
GGX = 0,
Beckmann = 1
};
inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CCookTorrance);}
inline CCookTorrance() = default;
SBasicNDFParams ndParams = {};
// We need this eta to compute the refractions of `L` when importance sampling and the Jacobian during H to L generation for rough dielectrics
// It does not mean we compute the Fresnel weights though! You might ask why we don't do that given that state of the art importance sampling
// (at time of writing) is to decide upon reflection vs. refraction after the microfacet normal `H` is already sampled,
// producing an estimator with just Masking and Shadowing function ratios. The reason is because we can simplify our IR by separating out
// BRDFs and BTDFs components into separate expressions, and also importance sample much better, for details see comments in CTrueIR.
typed_pointer_type<CSpectralVariable> orientedRealEta = {};
//
NDF ndf : 7 = NDF::GGX;
uint8_t reciprocateEta : 1 = false;
protected:
COPY_DEFAULT_IMPL
inline typed_pointer_type<IExprNode> getChildHandle_impl(const uint8_t ix) const override {return orientedRealEta;}
inline void setChild_impl(const uint8_t ix, _typed_pointer_type<IExprNode> newChild) override {orientedRealEta = block_allocator_type::_static_cast<CSpectralVariable>(newChild);}
NBL_API2 bool invalid(const SInvalidCheckArgs& args) const override;
inline bool reciprocatable() const override {return true;}
inline void reciprocate(IExprNode* dst) const override
{
(*static_cast<CCookTorrance*>(dst) = *this).reciprocateEta = ~reciprocateEta;
}
inline core::string getLabelSuffix() const override {return ndf!=NDF::GGX ? "\\nNDF = Beckmann":"\\nNDF = GGX";}
inline std::string_view getChildName_impl(const uint8_t ix) const override {return "Oriented η";}
NBL_API2 void printDot(std::ostringstream& sstr, const core::string& selfID) const override;
};
#undef COPY_DEFAULT_IMPL
#undef TYPE_NAME_STR
// basic utilities
inline typed_pointer_type<CMul> createMul(const typed_pointer_type<IExprNode> lhs, const typed_pointer_type<IExprNode> rhs)
{
if (!lhs || !rhs) // acceptable premaute optimization
return {};
const auto mulH = getObjectPool().emplace<CMul>();
auto* const mul = getObjectPool().deref(mulH);
mul->lhs = lhs;
mul->rhs = rhs;
return mulH;
}
inline typed_pointer_type<IExprNode> createAdd(const typed_pointer_type<IExprNode> lhs, const typed_pointer_type<IExprNode> rhs)
{
if (!lhs)
return rhs;
if (!rhs)
return lhs;
const auto addH = getObjectPool().emplace<CAdd>();
auto* const add = getObjectPool().deref(addH);
add->lhs = lhs;
add->rhs = rhs;
return addH;
}
inline typed_pointer_type<IExprNode> createFMA(const typed_pointer_type<IExprNode> a, const typed_pointer_type<IExprNode> b, const typed_pointer_type<IExprNode> c)
{
return createAdd(createMul(a,b),c);
}
inline typed_pointer_type<IExprNode> createWeightedSum(const typed_pointer_type<IExprNode> x0, const typed_pointer_type<IExprNode> w0, const typed_pointer_type<IExprNode> x1, const typed_pointer_type<IExprNode> w1)
{
return createAdd(createMul(x0,w0),createMul(x1,w1));
}
inline typed_pointer_type<CComplement> createComplement(const typed_pointer_type<IExprNode> child)
{
if (!child)
return {};
const auto complH = getObjectPool().emplace<CComplement>();
getObjectPool().deref(complH)->child = child;
return complH;
}
// To quickly make a fresnel
NBL_API2 typed_pointer_type<CFresnel> createNamedFresnel(const std::string_view name);
inline typed_pointer_type<CFresnel> createConstantMonochromeRealFresnel(const hlsl::float32_t orientedRealEta)
{
auto& pool = getObjectPool();
const auto fresnelH = pool.emplace<CFresnel>();
CSpectralVariable::SCreationParams<1> params = {};
params.knots.params[0].scale = orientedRealEta;
if (auto* const fresnel=pool.deref(fresnelH); fresnel)
fresnel->orientedRealEta = pool.emplace<CSpectralVariable>(std::move(params));
return fresnelH;
}
// To quickly make a matching backface BxDF from a frontface or vice versa
NBL_API2 typed_pointer_type<const IExprNode> reciprocate(const typed_pointer_type<const IExprNode> orig);
// a deep copy of the layer stack, wont copy the BxDFs
NBL_API2 typed_pointer_type<CLayer> copyLayers(const typed_pointer_type<const CLayer> orig);
// Reverse the linked list of layers and reciprocate their Etas
NBL_API2 typed_pointer_type<CLayer> reverse(const typed_pointer_type<const CLayer> orig);
// first query, we check presence of btdf layers all the way through the layer stack
inline bool transmissive(const typed_pointer_type<const CLayer> rootHandle) const
{
auto& pool = getObjectPool();
for (auto layer=pool.deref(rootHandle); layer; layer=pool.deref(layer->coated))
{
// it takes only one layer without transmission to break the chain
if (!layer->btdf)
return false;
}
return true;
}
// IMPORTANT: Two BxDFs are not allowed to be multiplied together.
// NOTE: Right now all Spectral Variables are required to be Monochrome or 3 bucket fixed semantics, all the same wavelength.
// Some things we can't check such as the compatibility of the BTDF with the BRDF (matching indices of refraction, etc.)
NBL_API2 bool valid(const typed_pointer_type<const CLayer> rootHandle, system::logger_opt_ptr logger) const;
inline std::span<const typed_pointer_type<const CLayer>> getMaterials() {return m_rootNodes;}
// Each material comes down to this, YOU MUST NOT MODIFY THE NODES AFTER ADDING THEIR PARENT TO THE ROOT NODES!
// TODO: shall we copy and hand out a new handle? Allow RootNode from a foreign const pool
inline bool addMaterial(const typed_pointer_type<const CLayer> rootNode, system::logger_opt_ptr logger)
{
if (valid(rootNode,logger))
{
m_rootNodes.push_back(rootNode);
return true;
}
return false;
}
// For Debug Visualization
struct SDotPrinter final
{
public:
inline SDotPrinter(const CFrontendIR* ir) : m_ir(ir) {}
// assign in reverse because we want materials to print in order
inline SDotPrinter(const CFrontendIR* ir, std::span<const typed_pointer_type<const CLayer>> roots) : m_ir(ir), layerStack(roots.rbegin(),roots.rend())
{
// should probably size it better, if I knew total node count allocated or live
visitedNodes.reserve(roots.size()<<3);
}
NBL_API2 void operator()(std::ostringstream& output);
inline core::string operator()()
{
std::ostringstream tmp;
operator()(tmp);
return tmp.str();
}
core::unordered_set<typed_pointer_type<const INode>> visitedNodes;
// TODO: track layering depth and indent accordingly?
core::vector<typed_pointer_type<const CLayer>> layerStack;
core::stack<typed_pointer_type<const IExprNode>> exprStack;
private:
const CFrontendIR* m_ir;
};
protected:
using CNodePool::CNodePool;
inline core::string getNodeID(const typed_pointer_type<const INode> handle) const {return core::string("_")+std::to_string(handle.value);}
inline core::string getLabelledNodeID(const typed_pointer_type<const INode> handle) const
{
const INode* node = getObjectPool().deref(handle);
core::string retval = getNodeID(handle);
retval += " [label=\"";
retval += node->getTypeName();
if (const auto* debug=getObjectPool().deref<const CDebugInfo>(node->debugInfo); debug && !debug->data().empty())
{
retval += "\\n";
retval += std::string_view(reinterpret_cast<const char*>(debug->data().data()),debug->data().size()-1);
}
if (const auto* expr=dynamic_cast<const IExprNode*>(node); expr)
retval += expr->getLabelSuffix();
retval += "\"]";
return retval;
}
core::vector<typed_pointer_type<const CLayer>> m_rootNodes;
};
inline bool CFrontendIR::valid(const typed_pointer_type<const CLayer> rootHandle, system::logger_opt_ptr logger) const
{
constexpr auto ELL_ERROR = system::ILogger::E_LOG_LEVEL::ELL_ERROR;
enum class SubtreeContributorState : uint8_t
{
Required,
Forbidden
};
struct StackEntry
{