From 2b4d34c134e526560d5a6630cca1218f4a40ed1c Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Tue, 21 Apr 2026 21:04:00 +0200 Subject: [PATCH 01/45] start the True IR --- .../nbl/asset/material_compiler3/CTrueIR.h | 28 ++++++- src/nbl/asset/material_compiler3/CTrueIR.cpp | 73 +++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 src/nbl/asset/material_compiler3/CTrueIR.cpp diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index fd304b9375..d66914eac4 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -41,6 +41,32 @@ class CTrueIR : public CNodePool // template requires std::is_base_of_v> using typed_pointer_type = _typed_pointer_type; + // To use a bump map, the Material needs to be provided UVs (which can or can not have associated tangents and smooth normals), but that's the responsibility of backend. + // The bump map just needs to be provided a TBN basis thats orthogonal so that normals or derivatives can be transformed. + // Normal maps will use Olano so won't be assumed to be normalized (they wouldn't be anyway due to interpolation), and derivative maps are never normalized and have huge range. + // Depending on the mode (deformation stretch enabled or not) the TB of the matrix will be normalized before the multiplication so that TBN is orthonormal and does not impart a UV stretch. + // After multiplication the normal will be normalized again, which is when we should apply Schussler or Yining. + // + // Schussler and Yining require we define a tangential microfacet from the perturbed normal `P` but differently: + // - Schussler wants normalize({-P.x,-P.y,0}) which is totally tangential and orthogonal to the geometric normal + // - Yining wants normalize({-P.xy*P.z,1-P.z*P.z}) which is orthogonal to the perturbed normal + // Both give an NDF with average normal projected onto the geometric normal being colinear to the geometric normal. + // Each microfacet has different pros and cons: + // - Tangential microfacet can use 100% perfect mirror material because its impossible to get 1-order scattering from it (camera and light can't see it at the same time) + // - Mirror Tangential microfacet makes 2-nd order scattering tractable, just evaluate the BRDF 3 times with original L and V, then once with reflected L and reflected V + // - for Lambertian BRDF this is only 2 evaluations because reflected V doesn't change anything + // - the dot products with reflected L or V can be computed quickly from the regular ones, but `H` always changes in this case + // - Orthogonal microfacet makes sure zero masking is achieved when LdotP=1 and appearance is 100% preserved in this case + // - Orthogonal microfacet can be seen directly and therefore it needs to have the same BxDF as the perturbed microfacet + // - The above requirements makes 2-nd order scattering intractable for orthogonal microfacets without random walks in the surface profile + // - We only need to evaluate the BxDF twice with Yinning and all products with perturbed normal can be obtained via permutation and scaling + // Both approaches are imperfect and in the limit will cause "fresnel lens" like appearance with Cook Torrance low roughness materials, you'll see a blend between classical bump mapped appearance and: + // - for 2nd-order Schussler a reflection of the objects by a flipped normal {-P.xy,P.z} due to mirror microfacet making a "virtual normal" behind the mirror + // - for 1st-order Yinning a reflection of the objects by an orthogonal normal which approaches the geometric normal as the perturbed normal gets more extreme + // This means that a tangential microfacet will show no blend/retroreflection if both L and V are behind the tangential microfacet. + // Whereas an orthogonal microfacet will show no blend/retroreflection if both L and V are below the orthogonal microfacet, so tangential ensures this property for half the hemisphere. + // But with extreme normal perturbation, the appearance of Schussler will match that of a very deep V-groove losing a lot of energy, but Yining will look like a flat surface of the same material. + // This is because Schussler has no bound on the ratio of the projected microsurface area on the geometric surface, whereas Yining bounds it to be <= 2.0 // Each material comes down to this @@ -68,7 +94,7 @@ class CTrueIR : public CNodePool // - multiscatter compensation // - compilation failure to unsupported complex layering // - compilation failure to unsupported complex layering - bool addMaterials(const CFrontendIR* forest); + NBL_API2 bool addMaterials(const CFrontendIR* forest); protected: using CNodePool::CNodePool; diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp new file mode 100644 index 0000000000..9a32097d06 --- /dev/null +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -0,0 +1,73 @@ +// 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 +#include "nbl/asset/material_compiler3/CTrueIR.h" + +#include "nbl/builtin/hlsl/complex.hlsl" +#include "nbl/builtin/hlsl/portable/vector_t.hlsl" + + +namespace nbl::asset::material_compiler3 +{ + +constexpr auto ELL_ERROR = nbl::system::ILogger::E_LOG_LEVEL::ELL_ERROR; +using namespace nbl::system; + +// TODO: Move these notes, they're for the backend. +// +// Any material (unless its emission only) requires a shading normal to be computed and kept, this should be the first register. +// Materials are primitive agnostic, don't assume triangles, so any attribute interpolation happens in Material System attribute getting callbacks. +// In practice most renderers will apply them on triangles, the advice is to keep triangle indices in own live state raw or compressed (uint32_t base, and then uint16_t diffs). +// To use the material outside of Raytracing or Visibility Buffer one could use HW interpolators, so all interpolated attributes will sit in global input variables. +// Whenever Material Compiler calls `getUV` or `getTBN` its the user's choice how to implement that. +// +// However whenever HW interpolators are not used, to compute a UV it costs: +// - 3x index buffer fetch (from primitive ID and index buffer BDA) +// - 3x dependent UV attribute fetch +// - 2 FMA per channel to interpolate +// And to compute a TBN its: +// - 3x index buffer fetch (from primitive ID and index buffer BDA) +// - 3x dependent fetch of Tangent (and optional Bitangent), or Rotation Around normal + scales +// - 2 FMA per channel to interpolate +// - 1 reconstruction of a TBN matrix (can be expensive, i.e. from a quaternion or include renormalizations of interpolated tangents) +// For this reason, it makes sense to spill interpolated UVs and Tangent+BitangentScale instead of recomputing. +// The material compiler will keep a bitmask of UVs and TBNs (TBN depends on UV set) already requested and will spill if user marks sets as expensive to recompute. +// +// If everyone is using the same TBN set, we could back up the interaction and sample because these are just V and L transformed into the TBN. +// In theory this requires us to use anisotropic interactions whenever the isotropic BRDF has a normal perturbation which seems like an overhead. +// However specular AA mitigations which derive roughness from per-pixel surface curvature and path regularizations would force this upon us anyway. +// Problem is that importance sampling requires us to transform a sampled tangent space L into worldspace and that requires knowing the TBN. +// I can see some optimizations for TBN calc where we only recompute the tangent space .xy of L and V for each TBN since N stays constant and NdotX2 can be known. +// Since we must have full TBN for anisotropic BxDF, tangent space V can be computed quicker than fetching it. +// +// Already computed shading normals and other BRDF parameters will be computed on demand but also cached (statically or dynamically) in registers. +// Register leftovers can be passed between command streams to not re-fetch or recompute BxDF parameters. +// Register allocation should spill whole quantities (all of RGB of a color or XYZ of a normal) not singular channels. +// Registers spilled should be the ones with least upcoming uses (should assume streams will run as AOV emission, AOV throughputs, Emission, NEE Eval, Generate, Quotient order) +// Relative cost to recompute or refetch should be included in the decision whether to keep around when register allocating. +// Perturbed Normals should be spilled to VRAM instead of recomputed as they require fetching at least a quaternion and a texture sample. +// Spill should be a big circular buffer allocated per-subgroup - hard to control in Pipeline Shaders https://github.com/KhronosGroup/Vulkan-Docs/issues/2717 +// So need to allocate worst case spill for all rays in a dispatch (although can do persistent threads and reduce the dispatch size a little) + +bool CTrueIR::addMaterials(const CFrontendIR* forest) +{ + auto& forestPool = forest->getObjectPool(); + // + struct StackEntry + { + const IExprNode* node; + // using post-order like stack but with a pre-order DFS + uint8_t visited = false; + }; + core::stack exprStack; + for (const auto& rootH : forest->getMaterials()) + { + // AST is Sum Expression to the BRDF nodes + // We need to keep the Ancestor prefix as an unrolled linked list + } + // + return false; +} + + +} \ No newline at end of file From d73be345deb67518621f5246d6dc65ff0cb374a5 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Tue, 21 Apr 2026 21:48:58 +0200 Subject: [PATCH 02/45] more prose in the comments --- include/nbl/asset/material_compiler3/CFrontendIR.h | 2 +- include/nbl/asset/material_compiler3/CTrueIR.h | 13 ++++++++++++- src/nbl/CMakeLists.txt | 1 + src/nbl/asset/material_compiler3/CTrueIR.cpp | 8 +++++++- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index 4269af5d6f..b44649022a 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -921,7 +921,7 @@ class CFrontendIR final : public CNodePool // 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 rootHandle, system::logger_opt_ptr logger) const; - inline std::span> getMaterials() {return m_rootNodes;} + inline std::span> getMaterials() const {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 diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index d66914eac4..f0b99f24a3 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -59,7 +59,7 @@ class CTrueIR : public CNodePool // - Orthogonal microfacet makes sure zero masking is achieved when LdotP=1 and appearance is 100% preserved in this case // - Orthogonal microfacet can be seen directly and therefore it needs to have the same BxDF as the perturbed microfacet // - The above requirements makes 2-nd order scattering intractable for orthogonal microfacets without random walks in the surface profile - // - We only need to evaluate the BxDF twice with Yinning and all products with perturbed normal can be obtained via permutation and scaling + // - We only need to evaluate the BxDF twice with Yinning and all products with perturbed normal can be obtained similarly to obtaining the reflected L and V for schussler (permutations and scaling) // Both approaches are imperfect and in the limit will cause "fresnel lens" like appearance with Cook Torrance low roughness materials, you'll see a blend between classical bump mapped appearance and: // - for 2nd-order Schussler a reflection of the objects by a flipped normal {-P.xy,P.z} due to mirror microfacet making a "virtual normal" behind the mirror // - for 1st-order Yinning a reflection of the objects by an orthogonal normal which approaches the geometric normal as the perturbed normal gets more extreme @@ -67,6 +67,17 @@ class CTrueIR : public CNodePool // Whereas an orthogonal microfacet will show no blend/retroreflection if both L and V are below the orthogonal microfacet, so tangential ensures this property for half the hemisphere. // But with extreme normal perturbation, the appearance of Schussler will match that of a very deep V-groove losing a lot of energy, but Yining will look like a flat surface of the same material. // This is because Schussler has no bound on the ratio of the projected microsurface area on the geometric surface, whereas Yining bounds it to be <= 2.0 + // + // There's also a Schussler variant where the tangential microfacet has the same BRDF as the perturbed one, with only 1-st order scattering. + // Then with this you can actually see the tangential microfacet (smooth mirror makes it black with only 1st order), which works better for BSDF than 2nd order scattering with a mirror facet. + // Note that transmissive or smooth glass tangential face could never work, because it presents same issues as a non-tangential mirror facet (V and L can interact directly in 1-st order). + // Mirror facet would cause you to see refractions from a reflected V, which would be weird when the VdotP->0 and you'd expect 100% fresnel reflection. Worse yet you'd see them in the "wrong" direction. + // Having a tangential microfacet with same BSDF does blend towards transmission as `VdotP->0`, but at least the refractive rays correctly have `L.xy = -V.xy` and are going "forward" although + // they are bending upwards vs. the transmitted direction, not downwards like they would against the macro surface or the perturbed surface before VdotP==0. + // + // Both this and Yining are faster because of the 2 BxDF evaluations instead of 3, and they only differ by the inter-facet shadowing and masking functions and the tangent facet normal is computed. + // However an orthogonal microfacet will work better for BSDFs because the facet tangents and normals form a square and not a rhombus, which means that a V to the "left" of the perturbed normal + // will always be to the "right" of the orthogonal microfacet, meaning we'll get a consistent refraction (both L=refract(V,facet) will curve away from -V in the same direction. // Each material comes down to this diff --git a/src/nbl/CMakeLists.txt b/src/nbl/CMakeLists.txt index 9c994bfa41..502d0c70c8 100644 --- a/src/nbl/CMakeLists.txt +++ b/src/nbl/CMakeLists.txt @@ -169,6 +169,7 @@ set(NBL_ASSET_SOURCES # Materials asset/material_compiler3/CFrontendIR.cpp + asset/material_compiler3/CTrueIR.cpp # Shaders asset/utils/ISPIRVOptimizer.cpp diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp index 9a32097d06..0dd67bae46 100644 --- a/src/nbl/asset/material_compiler3/CTrueIR.cpp +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -40,6 +40,12 @@ using namespace nbl::system; // I can see some optimizations for TBN calc where we only recompute the tangent space .xy of L and V for each TBN since N stays constant and NdotX2 can be known. // Since we must have full TBN for anisotropic BxDF, tangent space V can be computed quicker than fetching it. // +// Pertubed normal can be spilled or refetched. If it gets refetched some coefficients need to be recomputed which allow for taking dot products of V and L with it. +// If the BRDF is anisotropic, a small nested TBN needs construction so perturbed normal is 3 coefficients. +// Spilling as a float16_t3 or float16_t4 is identical memory traffic to a bilinear derivative map tap assuming UNORM8 storage. +// Tangents consistent with unperturbed TBN can be worked out as `B_p = normalize(cross(N_p,T))` and `T_p = cross(B_p,N_p)`, unless `N_p==T` then cross N_p with B instead. +// If tangents want to be rotated/explicitly controlled, then the spilled normal plus a rotation are still 3 coefficients if encoded as a quaternion (painful to decode) or 4 coeffs. +// // Already computed shading normals and other BRDF parameters will be computed on demand but also cached (statically or dynamically) in registers. // Register leftovers can be passed between command streams to not re-fetch or recompute BxDF parameters. // Register allocation should spill whole quantities (all of RGB of a color or XYZ of a normal) not singular channels. @@ -55,7 +61,7 @@ bool CTrueIR::addMaterials(const CFrontendIR* forest) // struct StackEntry { - const IExprNode* node; + const CFrontendIR::IExprNode* node; // using post-order like stack but with a pre-order DFS uint8_t visited = false; }; From bc077bcd3a6c02411a739327a685e1cc9992e52a Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Fri, 24 Apr 2026 02:26:08 +0200 Subject: [PATCH 03/45] advance the TrueIR --- .../asset/material_compiler3/CFrontendIR.h | 8 +- .../nbl/asset/material_compiler3/CTrueIR.h | 229 +++++++++++++++++- 2 files changed, 220 insertions(+), 17 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index b44649022a..8d38fc9604 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -35,7 +35,7 @@ namespace nbl::asset::material_compiler3 // // 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. +// Where `l(w_i,w_o)` is a Contributor Node BxDF such as Oren Nayar or Cook-Torrance, which 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. // @@ -71,9 +71,9 @@ namespace nbl::asset::material_compiler3 // 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. +// Because we implement Schussler et. al 2017 or Yining 2019 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 +// Schussler and Yining 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 { @@ -748,8 +748,6 @@ class CFrontendIR final : public CNodePool // 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 diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index f0b99f24a3..282d7fc22c 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -13,7 +13,7 @@ namespace nbl::asset::material_compiler3 // You make the Materials with a classical expression IR, one Root Node per material's interface layer, but here they're in "Accumulator Form" // They appear "flipped upside down", its expected our backends will evaluate contributors first, and then bother with the attenuators. -class CTrueIR : public CNodePool +class CTrueIR : public CNodePool // TODO: turn into an asset! { template using _typed_pointer_type = CNodePool::obj_pool_type::mem_pool_type::typed_pointer_type; @@ -32,8 +32,20 @@ class CTrueIR : public CNodePool class INode : public CNodePool::INode { public: + const auto& getHash() const {return hash;} + + // only call once the nodes underneath are linked up, returning empty hash means error/invalid node + virtual core::blake3_hash_t computeHash() const = 0; + +// virtual void printDot(std::ostringstream& sstr, const core::string& selfID) const = 0; protected: + inline bool recomputeHash() + { + hash = computeHash(); + return hash!=core::blake3_hash_t{}; + } + // Each node is final and immutable, has a precomputed hash for the whole subtree beneath it. // Debug info does not form part of the hash, so can get wildly replaced. core::blake3_hash_t hash; @@ -41,6 +53,126 @@ class CTrueIR : public CNodePool // template requires std::is_base_of_v> using typed_pointer_type = _typed_pointer_type; + // Contributors are things which can either be importance sampled to continue a path or impart a contribution ot the integral + class IContributor : public INode + { + public: + // ? + }; + // + class IExprNode : public INode + { + public: + // Only sane child count allowed + virtual uint8_t getChildCount() const = 0; + inline _typed_pointer_type getChildHandle(const uint8_t ix) + { + if (ix getChildHandle(const uint8_t ix) const + { + auto retval = const_cast(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 + { + Add = 0, + Mul = 1, + Other = 2 + }; + virtual inline Type getType() const {return Type::Other;} + + protected: + // child managment + virtual inline _typed_pointer_type getChildHandle_impl(const uint8_t ix) const {assert(false); return {};} + }; +#define TYPE_NAME_STR(NAME) "nbl::asset::material_compiler3::CTrueIR::"#NAME + // Note that this is not a root node, its a flipped leaf! + class CWeightedContributor final : public obj_pool_type::INonTrivial, public INode + { + public: + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CWeightedContributor);} + + NBL_API2 core::blake3_hash_t computeHash() const; + + + typed_pointer_type contributor = {}; + // if null then assumed to be 1 + typed_pointer_type factor = {}; + }; + // One BRDF or BTDF component of a layer is represented as + // f(w_i,w_o) = Sum_i^N Product_j^{N_i} h_{ij}(w_i,w_o) l_i(w_i,w_o) + class CContributorSum final : public obj_pool_type::INonTrivial, public INode + { + public: + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CContributorSum);} + + // CANONICALIZATION NOTE: The conntributors shall be ordered in following order: + // - according to their type, emitters first, then BxDFs, within those + // + Emitters with IES profile, then without + // + BxDFs by their type + // * within the same type, by the parameters (with/without bump, then rest + // - all things being equal, order by hash + NBL_API2 core::blake3_hash_t computeHash() const; + + + // the product is ... + typed_pointer_type product = {}; + // the rest node is ... + _typed_pointer_type rest = {}; + }; + + // The oriented layer is a layer with already all the Etas reciprocated, etc. + // For codegen, we can compute total uncorrelated layering by convolving every `h_{ij}(w_i,w_o) l_i(w_i,w_o)` term in the layer above with layer below + // Corellated layering is a far far far TODO, all it means that certain convolutions don't happen - certain BxDFs don't layer over each other (tricky to express in AST and IR) + class COrientedLayer final : public obj_pool_type::INonTrivial, public INode + { + public: + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(COrientedLayer);} + + NBL_API2 core::blake3_hash_t computeHash() const; + + // you can set the children later + inline COrientedLayer() = default; + + // These are same as the frontend's except that all the etas are oriented and reciprocated + typed_pointer_type brdfTop = {}; + // this node must be non-null until the last layer + typed_pointer_type btdf = {}; + // because the layer is oriented, these last two members must be null in the last oriented layer + typed_pointer_type brdfBottom = {}; + typed_pointer_type coated = {}; + }; + + // Unit Radiance emitter modulated by an IES profile + class CEmitter final : public obj_pool_type::INonTrivial, public IContributor + { + public: + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CEmitter);} + + NBL_API2 core::blake3_hash_t computeHash() const; + + // you can set the members later + inline CEmitter() = default; + +#if 0 // TODO: share with AST ? No parameter needs to be a node (so we can dedup and hash)! + // 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) +#endif + }; + // To use a bump map, the Material needs to be provided UVs (which can or can not have associated tangents and smooth normals), but that's the responsibility of backend. // The bump map just needs to be provided a TBN basis thats orthogonal so that normals or derivatives can be transformed. // Normal maps will use Olano so won't be assumed to be normalized (they wouldn't be anyway due to interpolation), and derivative maps are never normalized and have huge range. @@ -68,7 +200,7 @@ class CTrueIR : public CNodePool // But with extreme normal perturbation, the appearance of Schussler will match that of a very deep V-groove losing a lot of energy, but Yining will look like a flat surface of the same material. // This is because Schussler has no bound on the ratio of the projected microsurface area on the geometric surface, whereas Yining bounds it to be <= 2.0 // - // There's also a Schussler variant where the tangential microfacet has the same BRDF as the perturbed one, with only 1-st order scattering. + // There's also a Schussler variant where the tangential microfacet has the same BRDF as the perturbed one, with only 1-st order scattering. This is nice because diffuse doesn't become a special case. // Then with this you can actually see the tangential microfacet (smooth mirror makes it black with only 1st order), which works better for BSDF than 2nd order scattering with a mirror facet. // Note that transmissive or smooth glass tangential face could never work, because it presents same issues as a non-tangential mirror facet (V and L can interact directly in 1-st order). // Mirror facet would cause you to see refractions from a reflected V, which would be weird when the VdotP->0 and you'd expect 100% fresnel reflection. Worse yet you'd see them in the "wrong" direction. @@ -78,24 +210,66 @@ class CTrueIR : public CNodePool // Both this and Yining are faster because of the 2 BxDF evaluations instead of 3, and they only differ by the inter-facet shadowing and masking functions and the tangent facet normal is computed. // However an orthogonal microfacet will work better for BSDFs because the facet tangents and normals form a square and not a rhombus, which means that a V to the "left" of the perturbed normal // will always be to the "right" of the orthogonal microfacet, meaning we'll get a consistent refraction (both L=refract(V,facet) will curve away from -V in the same direction. + class IBxDF final : public obj_pool_type::INonTrivial, public IContributor + { + public: + // TODO: + // - Share ParamSet with AST + // - Share SBasicNDFParams with AST + // - hash NDF Params + // - maybe share IBxDF + // - and base BxDFs ? + }; // Each material comes down to this - struct Material + struct SMaterial { -// TypedHandle root; - CNodePool::typed_pointer_type debugInfo; + // Stats needed by a renderer to skip loading parts of a material or remove expensive code alltogether + enum class EMetadataBits : uint16_t + { + None = 0, + // if any such contributor present + NotBlackhole = 0x1u<<0, // actually have a material + NonDelta = 0x1u<<1, // can evaluate against point lights (or other samplings) + DeltaTransmissive = 0x1u<<2, // can use stochastic transparency for closest hit rays and blending for anyhit + Emissive = 0x1u<<3, // maybe register for NEE, but definitely grab the emission + NonSpatiallyVaryingEmissive = 0x1u<<4, // definitely register for NEE + // TODO: 5,6 left + // Bits that help us remove expensive code from impl + DerivativeMap = 0x1u<<7, + DirectionallyVaryingEmissive = 0x1u<<8, // IES profile + SpatiallyVaryingEmissive = 0x1u<<9, // textured light + Lambertian = 0x1u<<10, + OrenNayar = 0x1u<<11, + GGX = 0x1u<<12, + AnisotropicGGX = 0x1u<<13, + Beckmann = 0x1u<<14, + AnisotropicBeckmann = 0x1u<<15, + }; + // + struct SOriented + { + // + typed_pointer_type root = {}; + // + constexpr static inline uint8_t MaxUVSlots = 32; + std::bitset usedUVSlots = {}; + // the tangent frames are a subset of used UV slots, unless there's an anisotropic BRDF involved + std::bitset usedTangentFrames = {}; + // + EMetadataBits metadata = {}; + }; + SOriented front = {}; + SOriented back = {}; // - constexpr static inline uint8_t MaxUVSlots = 32; - std::bitset usedUVSlots; - // the tangent frames are a subset of used UV slots, unless there's an anisotropic BRDF involved - std::bitset usedTangentFrames; + CNodePool::typed_pointer_type debugInfo = {}; }; - inline std::span getMaterials() const {return m_materials;} + inline std::span getMaterials() const {return m_materials;} // We take the trees from the forest, and canonicalize them into our weird Domain Specific IR with Upside down expression trees. // Process: - // 1. Schusslerization (for derivative map usage) and Decompression (duplicating nodes, etc.) + // 1. Decompression (duplicating nodes, etc.) // 2. Canonicalize Expressions (Transform into Sum-Product form, DCE, etc.) // 3. Split BTDFs (front vs. back part), reciprocate Etas // 4. Simplify and Hoist Layer terms (delta sampling property) @@ -106,11 +280,42 @@ class CTrueIR : public CNodePool // - compilation failure to unsupported complex layering // - compilation failure to unsupported complex layering NBL_API2 bool addMaterials(const CFrontendIR* forest); + + + // For Debug Visualization + struct SDotPrinter final + { + public: + inline SDotPrinter(const CTrueIR* ir) : m_ir(ir) {} + // assign in reverse because we want materials to print in order + inline SDotPrinter(const CTrueIR* ir, std::span 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()<<4); + } + + NBL_API2 void operator()(std::ostringstream& output); + inline core::string operator()() + { + std::ostringstream tmp; + operator()(tmp); + return tmp.str(); + } + + core::unordered_set> visitedNodes; +#if 0 + // TODO: track layering depth and indent accordingly? + core::vector> layerStack; + core::stack> exprStack; +#endif + private: + const CTrueIR* m_ir; + }; protected: using CNodePool::CNodePool; - core::vector m_materials; + core::vector m_materials; core::unordered_map> m_uniqueNodes; }; From c7fe341f07c625a1fd02b3c7152540acc64f49a8 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Fri, 24 Apr 2026 14:50:52 +0200 Subject: [PATCH 04/45] ok seems I needed to add corellated layering to the material compiler --- .../nbl/asset/material_compiler3/CTrueIR.h | 59 ++++++-- src/nbl/asset/material_compiler3/CTrueIR.cpp | 141 +++++++++++++++++- 2 files changed, 182 insertions(+), 18 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 282d7fc22c..bf588183f4 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -127,9 +127,28 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! _typed_pointer_type rest = {}; }; - // The oriented layer is a layer with already all the Etas reciprocated, etc. // For codegen, we can compute total uncorrelated layering by convolving every `h_{ij}(w_i,w_o) l_i(w_i,w_o)` term in the layer above with layer below + class COrientedLayer; // Corellated layering is a far far far TODO, all it means that certain convolutions don't happen - certain BxDFs don't layer over each other (tricky to express in AST and IR) + class CCorellatedTransmission final : public obj_pool_type::INonTrivial, public INode + { + public: + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CCorellatedTransmission);} + + NBL_API2 core::blake3_hash_t computeHash() const; + + // you can set the children later + inline CCorellatedTransmission() = default; + + // to get to the coated layer we must transmit through + typed_pointer_type btdf = {}; + // because the layer is oriented, these last two members must be null when the coating stops + typed_pointer_type brdfBottom = {}; + _typed_pointer_type coated = {}; + // optional, indicates a "sibling" transmission thats next to this one + _typed_pointer_type next = {}; + }; + // The oriented layer is a layer with already all the Etas reciprocated, etc. class COrientedLayer final : public obj_pool_type::INonTrivial, public INode { public: @@ -143,11 +162,10 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // These are same as the frontend's except that all the etas are oriented and reciprocated typed_pointer_type brdfTop = {}; // this node must be non-null until the last layer - typed_pointer_type btdf = {}; - // because the layer is oriented, these last two members must be null in the last oriented layer - typed_pointer_type brdfBottom = {}; - typed_pointer_type coated = {}; + typed_pointer_type firstTransmission = {}; }; + +// TODO: Parameter Node // Unit Radiance emitter modulated by an IES profile class CEmitter final : public obj_pool_type::INonTrivial, public IContributor @@ -222,10 +240,10 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! }; - // Each material comes down to this + // Each material comes down to this, this is the only struct we don't de-duplicate struct SMaterial { - // Stats needed by a renderer to skip loading parts of a material or remove expensive code alltogether + // Stats needed by a renderer to skip loading parts of a material or remove expensive code altogether enum class EMetadataBits : uint16_t { None = 0, @@ -250,7 +268,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // struct SOriented { - // + // null means no material typed_pointer_type root = {}; // constexpr static inline uint8_t MaxUVSlots = 32; @@ -258,15 +276,24 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // the tangent frames are a subset of used UV slots, unless there's an anisotropic BRDF involved std::bitset usedTangentFrames = {}; // - EMetadataBits metadata = {}; + core::bitflag metadata = {}; }; SOriented front = {}; SOriented back = {}; - // + // TODO: more detailed debug info CNodePool::typed_pointer_type debugInfo = {}; }; inline std::span getMaterials() const {return m_materials;} + struct SMaterialHandle + { + constexpr static inline uint32_t Invalid = ~0u; + explicit inline operator bool() const {return value!=Invalid;} + + uint32_t value = Invalid; + }; + constexpr static inline SMaterialHandle BlackholeMaterialHandle = { 0u }; + // Returns indices into `this->getMaterials()` for every `forest->getMaterials()` // We take the trees from the forest, and canonicalize them into our weird Domain Specific IR with Upside down expression trees. // Process: // 1. Decompression (duplicating nodes, etc.) @@ -279,7 +306,15 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // - multiscatter compensation // - compilation failure to unsupported complex layering // - compilation failure to unsupported complex layering - NBL_API2 bool addMaterials(const CFrontendIR* forest); + struct SAddMaterialsArgs + { + explicit inline operator bool() const {return forest;} + + const CFrontendIR* forest; + system::logger_opt_ptr logger; + + }; + NBL_API2 core::vector addMaterials(const SAddMaterialsArgs& args); // For Debug Visualization @@ -319,7 +354,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! core::unordered_map> m_uniqueNodes; }; -//! DAG (baked) +NBL_ENUM_ADD_BITWISE_OPERATORS(CTrueIR::SMaterial::EMetadataBits) } // namespace nbl::asset::material_compiler3 diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp index 0dd67bae46..1c2900f68d 100644 --- a/src/nbl/asset/material_compiler3/CTrueIR.cpp +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -11,6 +11,7 @@ namespace nbl::asset::material_compiler3 { constexpr auto ELL_ERROR = nbl::system::ILogger::E_LOG_LEVEL::ELL_ERROR; +constexpr auto ELL_DEBUG = nbl::system::ILogger::E_LOG_LEVEL::ELL_DEBUG; using namespace nbl::system; // TODO: Move these notes, they're for the backend. @@ -55,9 +56,18 @@ using namespace nbl::system; // Spill should be a big circular buffer allocated per-subgroup - hard to control in Pipeline Shaders https://github.com/KhronosGroup/Vulkan-Docs/issues/2717 // So need to allocate worst case spill for all rays in a dispatch (although can do persistent threads and reduce the dispatch size a little) -bool CTrueIR::addMaterials(const CFrontendIR* forest) +auto CTrueIR::addMaterials(const SAddMaterialsArgs& args) -> core::vector { - auto& forestPool = forest->getObjectPool(); + const auto logger = args.logger; + if (!args) + { + logger.log("Invalid Arguments to `CTrueIR::addMaterials`",ELL_ERROR); + return {}; + } + auto& astPool = args.forest->getObjectPool(); +# + core::unordered_map brdfs; + core::unordered_map btdfs; // struct StackEntry { @@ -65,14 +75,133 @@ bool CTrueIR::addMaterials(const CFrontendIR* forest) // using post-order like stack but with a pre-order DFS uint8_t visited = false; }; - core::stack exprStack; - for (const auto& rootH : forest->getMaterials()) + core::vector exprStack; + // + core::vector> ancestors; + auto findContributors = [&]()->void + { + // accumulate an ancestor prefix + ancestors.clear(); + while (!exprStack.empty()) + { + auto& entry = exprStack.back(); + // + bool isContributor = true; + if (isContributor) + { + // every contributor node gets its own SORTED ancestor prefix + std::sort(ancestors.begin(),ancestors.end()); // TODO: canonicallizing comparator + } + else if (entry.visited) + { + // remove self from the ancestor prefix +// std::remove(ancestors.begin(),ancestors.end(),self); + } + else + { + // spot prefix being null/zero to stop exploring + // can make the decision wholly on current factor + bool continueExploring = true; + if (continueExploring) + { + // TODO: add self after making self +// ancestors.push_back(entry.node); + // TODO: push the children nodes onto the stack + entry.visited = true; + continue; + } + } + exprStack.pop_back(); + } + }; + // + core::vector layerStack; + auto makeOrientedMaterial = [&](const CFrontendIR::typed_pointer_type rootH)->SMaterial::SOriented { + SMaterial::SOriented retval = {}; + + // go down through layers and create all the dependencies + layerStack.clear(); + for (const auto* layer=astPool.deref(rootH); layer; layer=astPool.deref(layer->coated)) + { + // TODO: actually re-check the expressions for being null after optimization + bool noTopReflection = !layer->brdfTop; + bool noTransmission = !layer->btdf; + // if there's literally nothing on the top level, you can't get to the next layer to retroreflect from it + if (noTopReflection && noTransmission) + { + logger.log("Skipping current layer and farther ones due to no transmission and reflection",ELL_DEBUG); + break; + } + layerStack.push_back(layer); + // find out rest of the layers don't matter because they're blocked from being seen, its not a complete check + if (noTransmission) + { + logger.log("Skipping remaining layers due to no transmission",ELL_DEBUG); + break; + } + // Only if we're not in the last layer do we care about the bottom BRDF (you can't hit it otherwise) + // Note that this won't catch the next layer being a blackhole and needs to be undone if it is + if (layer->coated && layer->brdfBottom) + { + // do stuff with brdfBottom + } + } + if (!layerStack.empty()) + retval.metadata |= SMaterial::EMetadataBits::NotBlackhole; + // then go back up and make the layers + while (!layerStack.empty()) + { + const auto* const layer = layerStack.back(); + layerStack.pop_back(); + // allocate a layer + //... + // process the BTDF + //... + // process the top BRDF + + // if BTDF has delta transmissions, then via the sampling property hoist next layer into current layer BRDFs with the DeltaTransmission weights applied + // hmm this would require decorrellation... because don't want rest of BTDF to affect + //... + } + // skip replace delta transmissions by the layer undernearth, if null then keep as delta + // AST is Sum Expression to the BRDF nodes // We need to keep the Ancestor prefix as an unrolled linked list + return retval; + }; + + const auto inputMaterials = args.forest->getMaterials(); + core::vector retval(inputMaterials.size(), {}); + auto outIt = retval.begin(); + for (const auto& rootH : inputMaterials) + { + auto& result = *(outIt++); + + const auto* astRoot = astPool.deref(rootH); + // no material + if (!astRoot) + { + result = BlackholeMaterialHandle; + continue; + } + SMaterial material = { + .front = makeOrientedMaterial(rootH), + .back = makeOrientedMaterial(rootH) // TODO: reversed + }; + + // TODO: better debug info + if (const auto* debug=astPool.deref(astRoot->debugInfo); debug && !debug->data().empty()) + { + material.debugInfo = getObjectPool().emplace(debug->data().data(),debug->data().size()); + } + + // + result.value = m_materials.size(); + m_materials.push_back(material); } - // - return false; + + return retval; } From c3515c4dc90452f467bdb82d1a01a05d95e6c806 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Fri, 24 Apr 2026 18:02:04 +0200 Subject: [PATCH 05/45] figure out we messed up, but add blake3_hasher specializations for `SImpleBlockBasedAllocator` Handle types --- .../nbl/asset/material_compiler3/CNodePool.h | 1 - .../nbl/asset/material_compiler3/CTrueIR.h | 125 +++++++++++++++--- .../core/alloc/SimpleBlockBasedAllocator.h | 27 ++++ src/nbl/asset/material_compiler3/CTrueIR.cpp | 78 ++++++++--- 4 files changed, 194 insertions(+), 37 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CNodePool.h b/include/nbl/asset/material_compiler3/CNodePool.h index 43dbfd1e1f..59d6b84529 100644 --- a/include/nbl/asset/material_compiler3/CNodePool.h +++ b/include/nbl/asset/material_compiler3/CNodePool.h @@ -94,6 +94,5 @@ class CNodePool : public core::IReferenceCounted obj_pool_type m_composed; }; - } // namespace nbl::asset::material_compiler3 #endif \ No newline at end of file diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index bf588183f4..7879a31691 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -63,13 +63,27 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! class IExprNode : public INode { public: + inline core::blake3_hash_t computeHash() const override final + { + core::blake3_hasher hasher = {}; + hasher << static_cast(getType()); + hasher << lhs; + continueHashing(hasher); + return hasher.operator core::blake3_hash_t(); + } + // Only sane child count allowed - virtual uint8_t getChildCount() const = 0; + inline uint8_t getChildCount() const {return getExtraChildCount()+1;} inline _typed_pointer_type getChildHandle(const uint8_t ix) { - if (ix getChildHandle(const uint8_t ix) const { @@ -81,15 +95,21 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // 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 { - Add = 0, - Mul = 1, - Other = 2 + Constant = 0, + Frensel = 1, + Add = 2, + Other = 3 }; virtual inline Type getType() const {return Type::Other;} + // optional, when null, it behaves as 1 promoted to the correct dimension + _typed_pointer_type lhs = {}; + protected: - // child managment - virtual inline _typed_pointer_type getChildHandle_impl(const uint8_t ix) const {assert(false); return {};} + virtual void continueHashing(core::blake3_hasher& hasher) const = 0; + virtual inline uint8_t getExtraChildCount() const {return 0u;} + // child managment, only gets called with `ix>0` + virtual inline _typed_pointer_type getExtraChildHandle(const uint8_t ix) const {assert(false); return {};} }; #define TYPE_NAME_STR(NAME) "nbl::asset::material_compiler3::CTrueIR::"#NAME // Note that this is not a root node, its a flipped leaf! @@ -98,7 +118,13 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! public: inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CWeightedContributor);} - NBL_API2 core::blake3_hash_t computeHash() const; + core::blake3_hash_t computeHash() const + { + core::blake3_hasher hasher = {}; + hasher << contributor; + hasher << factor; + return hasher.operator core::blake3_hash_t(); + } typed_pointer_type contributor = {}; @@ -118,7 +144,13 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // + BxDFs by their type // * within the same type, by the parameters (with/without bump, then rest // - all things being equal, order by hash - NBL_API2 core::blake3_hash_t computeHash() const; + core::blake3_hash_t computeHash() const + { + core::blake3_hasher hasher = {}; + hasher << product; + hasher << rest; + return hasher.operator core::blake3_hash_t(); + } // the product is ... @@ -134,8 +166,16 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! { public: inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CCorellatedTransmission);} - - NBL_API2 core::blake3_hash_t computeHash() const; + + core::blake3_hash_t computeHash() const + { + core::blake3_hasher hasher = {}; + hasher << btdf; + hasher << brdfBottom; + hasher << coated; + hasher << next; + return hasher.operator core::blake3_hash_t(); + } // you can set the children later inline CCorellatedTransmission() = default; @@ -154,7 +194,13 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! public: inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(COrientedLayer);} - NBL_API2 core::blake3_hash_t computeHash() const; + inline core::blake3_hash_t computeHash() const + { + core::blake3_hasher hasher = {}; + hasher << brdfTop; + hasher << firstTransmission; + return hasher.operator core::blake3_hash_t(); + } // you can set the children later inline COrientedLayer() = default; @@ -173,22 +219,27 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! public: inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CEmitter);} - NBL_API2 core::blake3_hash_t computeHash() const; + inline core::blake3_hash_t computeHash() const + { + core::blake3_hasher hasher = {}; +// hasher << parameter; + hasher << profileTransform; + return hasher.operator core::blake3_hash_t(); + } // you can set the members later inline CEmitter() = default; -#if 0 // TODO: share with AST ? No parameter needs to be a node (so we can dedup and hash)! // 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 = {}; +// TODO: this IR will have parameters hashed and deduped as actual nodes +// 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) -#endif }; // To use a bump map, the Material needs to be provided UVs (which can or can not have associated tangents and smooth normals), but that's the responsibility of backend. @@ -239,6 +290,44 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // - and base BxDFs ? }; + //! Basic factor nodes + class CConstant final : public obj_pool_type::INonTrivial, public IExprNode + { + public: + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CConstant);} + inline Type getType() const override {return Type::Constant;} + + // you can set the children later + inline CConstant() = default; + + protected: + inline void continueHashing(core::blake3_hasher& hasher) const override + { + } + }; + + // This one doesn't get used during canonicalization + class CAdd final : public obj_pool_type::INonTrivial, public IExprNode + { + 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; + + typed_pointer_type rhs = {}; + + protected: + inline void continueHashing(core::blake3_hasher& hasher) const override + { + hasher << rhs; + } + inline uint8_t getExtraChildCount() const override {return 1u;} + // child managment, only gets called with `ix>0` + inline typed_pointer_type getExtraChildHandle(const uint8_t ix) const override {return rhs;} + }; + // Each material comes down to this, this is the only struct we don't de-duplicate struct SMaterial diff --git a/include/nbl/core/alloc/SimpleBlockBasedAllocator.h b/include/nbl/core/alloc/SimpleBlockBasedAllocator.h index 503a673c78..a3679dba1a 100644 --- a/include/nbl/core/alloc/SimpleBlockBasedAllocator.h +++ b/include/nbl/core/alloc/SimpleBlockBasedAllocator.h @@ -533,6 +533,33 @@ class SimpleBlockBasedAllocatorMT final }; // no aliases + +// specialize the blake3 hasher +template +struct blake3_hasher::update_impl,Dummy> +{ + static inline void __call(blake3_hasher& hasher, const ConstHandle input) + { + update_impl::__call(hasher,input.value); + } +}; +template +struct blake3_hasher::update_impl >,Dummy> +{ + static inline void __call(blake3_hasher& hasher, const Handle > input) + { + update_impl::__call(hasher,input.value); + } +}; +template +struct blake3_hasher::update_impl > >,Dummy> +{ + static inline void __call(blake3_hasher& hasher, const TypedHandle > > input) + { + update_impl::__call(hasher,input.value); + } +}; + } namespace std diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp index 1c2900f68d..ae3384d129 100644 --- a/src/nbl/asset/material_compiler3/CTrueIR.cpp +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -71,15 +71,21 @@ auto CTrueIR::addMaterials(const SAddMaterialsArgs& args) -> core::vector factor = {}; }; core::vector exprStack; +#ifndef DIDNT_MESS_UP // - core::vector> ancestors; - auto findContributors = [&]()->void + core::vector ancestors; +#endif + auto getContributors = [&](const CFrontendIR::typed_pointer_type bxdfRootH)->auto { + typed_pointer_type headH = {}; + exprStack.push_back({.node=astPool.deref(bxdfRootH)}); // accumulate an ancestor prefix ancestors.clear(); while (!exprStack.empty()) @@ -89,30 +95,64 @@ auto CTrueIR::addMaterials(const SAddMaterialsArgs& args) -> core::vectorbool + { + // TODO: actually properly order the factors + return lhs(); + auto* const head = getObjectPool().deref(prevH); + // actually make the contributor + { + const auto weightedH = getObjectPool().emplace(); + head->product = weightedH; + auto* weighted = getObjectPool().deref(weightedH); + if (entry.factor) + weighted->factor = entry.factor; + // actually make the contributor + { + } + } + head->rest = headH; + headH = prevH; +#endif } - else + else if (entry.notVisited()) { +#ifndef DIDNT_MESS_UP // spot prefix being null/zero to stop exploring // can make the decision wholly on current factor bool continueExploring = true; if (continueExploring) { - // TODO: add self after making self -// ancestors.push_back(entry.node); + // make the actual factor + auto derivedFactorH = getObjectPool().emplace(); + // TODO: find the place where to insert it + { + // shtuff + } + entry.factor = derivedFactorH; + // add self after making self + ancestors.push_back(getObjectPool().deref(entry.factor)); // TODO: push the children nodes onto the stack - entry.visited = true; continue; } +#endif + } + else + { +#ifndef DIDNT_MESS_UP + // remove self from the ancestor prefix + std::remove(ancestors.begin(),ancestors.end(),getObjectPool().deref(entry.factor)); +#endif } exprStack.pop_back(); } + return headH; }; // core::vector layerStack; @@ -152,14 +192,16 @@ auto CTrueIR::addMaterials(const SAddMaterialsArgs& args) -> core::vector(); + auto* const outLayer = getObjectPool().deref(layerH); + retval.root = layerH; // process the BTDF //... // process the top BRDF - + outLayer->brdfTop = getContributors(inLayer->brdfTop); // if BTDF has delta transmissions, then via the sampling property hoist next layer into current layer BRDFs with the DeltaTransmission weights applied // hmm this would require decorrellation... because don't want rest of BTDF to affect //... @@ -187,7 +229,7 @@ auto CTrueIR::addMaterials(const SAddMaterialsArgs& args) -> core::vector Date: Sat, 25 Apr 2026 16:55:44 +0200 Subject: [PATCH 06/45] enforce some more validation --- include/nbl/asset/material_compiler3/CFrontendIR.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index 8d38fc9604..85e628ef43 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -265,7 +265,8 @@ class CFrontendIR final : public CNodePool Contributor = 0, Mul = 1, Add = 2, - Other = 3 + Complement = 3, + Other = 5 }; virtual inline Type getType() const {return Type::Other;} @@ -532,6 +533,7 @@ class CFrontendIR final : public CNodePool { public: inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CComplement);} + inline Type getType() const override {return Type::Complement;} // you can set the children later inline CComplement() = default; @@ -1056,7 +1058,8 @@ inline bool CFrontendIR::valid(const typed_pointer_type rootHandle const auto childHandle = node->getChildHandle(childIx); if (const auto child=getObjectPool().deref(childHandle); child) { - const bool noContribBelow = entry.contribState==SubtreeContributorState::Forbidden || childIx!=0 && nodeIsMul; + // Only Add nodes can have Contributors in any subtree, Mul and Complement only the first, and others can't have them at all + const bool noContribBelow = entry.contribState==SubtreeContributorState::Forbidden || childIx!=0 && !nodeIsAdd || nodeType==IExprNode::Type::Complement || nodeType==IExprNode::Type::Other; StackEntry newEntry = {.node=child,.handle=childHandle}; if (noContribBelow) { From 0e51eb6c84648158b6fb4873b4616fb6b1369c3d Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Sat, 25 Apr 2026 18:58:43 +0200 Subject: [PATCH 07/45] finally get an idea of how to rewrite the AST into the IR --- .../nbl/asset/material_compiler3/CTrueIR.h | 158 +++++------ src/nbl/asset/material_compiler3/CTrueIR.cpp | 247 ++++++++++++++---- 2 files changed, 260 insertions(+), 145 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 7879a31691..0ea2eb8433 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -35,14 +35,12 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! const auto& getHash() const {return hash;} // only call once the nodes underneath are linked up, returning empty hash means error/invalid node - virtual core::blake3_hash_t computeHash() const = 0; - -// virtual void printDot(std::ostringstream& sstr, const core::string& selfID) const = 0; + virtual core::blake3_hash_t computeHash(const obj_pool_type& pool) const = 0; protected: - inline bool recomputeHash() + inline bool recomputeHash(const obj_pool_type& pool) { - hash = computeHash(); + hash = computeHash(pool); return hash!=core::blake3_hash_t{}; } @@ -54,62 +52,58 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! template requires std::is_base_of_v> using typed_pointer_type = _typed_pointer_type; // Contributors are things which can either be importance sampled to continue a path or impart a contribution ot the integral + // We don't make the contributor hold its factor so that we still have a separate hash value and good codegen (all identical contributors go to same function call class IContributor : public INode { public: // ? }; - // - class IExprNode : public INode + // this is for a term in a mul or add expression + class IFactor : public INode + { + protected: + uint64_t padding; + }; + // we step away from our usual way of doing things via linked lists, because this is simpler to rearrange + class CFactorCombiner final : public obj_pool_type::IVariableSize, public IFactor { public: - inline core::blake3_hash_t computeHash() const override final + // There's only two ops we support + enum Type : uint8_t + { + Mul = 0, + Add = 1 + }; + struct SState + { + uint64_t type : 1 = Type::Mul; + uint64_t childCount : 6 = 0; + // which factors get `1-x` for an Add node or `-x` for a Mul node before getting used + uint64_t childIxComplementMask : 57 = 0x0u; + }; + static_assert(sizeof(SState) == sizeof(IFactor::padding)); + inline SState getState() const {return std::bit_cast(padding);} + + inline core::blake3_hash_t computeHash(const obj_pool_type& pool) const override final { core::blake3_hasher hasher = {}; - hasher << static_cast(getType()); - hasher << lhs; - continueHashing(hasher); + hasher << padding; // hash whole state at once + for (uint16_t i=0; igetHash(); return hasher.operator core::blake3_hash_t(); } // Only sane child count allowed - inline uint8_t getChildCount() const {return getExtraChildCount()+1;} - inline _typed_pointer_type getChildHandle(const uint8_t ix) - { - if (ix!=0) - { - if (ix getChildHandle(const uint8_t ix) const + inline typed_pointer_type getChildHandle(const uint8_t ix) { - auto retval = const_cast(this)->getChildHandle(ix); - return retval; + if (ix lhs = {}; - protected: - virtual void continueHashing(core::blake3_hasher& hasher) const = 0; - virtual inline uint8_t getExtraChildCount() const {return 0u;} - // child managment, only gets called with `ix>0` - virtual inline _typed_pointer_type getExtraChildHandle(const uint8_t ix) const {assert(false); return {};} + friend class CTrueIR; + typed_pointer_type child[1] = {{}}; }; #define TYPE_NAME_STR(NAME) "nbl::asset::material_compiler3::CTrueIR::"#NAME // Note that this is not a root node, its a flipped leaf! @@ -118,18 +112,18 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! public: inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CWeightedContributor);} - core::blake3_hash_t computeHash() const + core::blake3_hash_t computeHash(const obj_pool_type& pool) const { core::blake3_hasher hasher = {}; - hasher << contributor; - hasher << factor; + hasher << pool.deref(contributor)->getHash(); + hasher << pool.deref(factor)->getHash(); return hasher.operator core::blake3_hash_t(); } typed_pointer_type contributor = {}; // if null then assumed to be 1 - typed_pointer_type factor = {}; + typed_pointer_type factor = {}; }; // One BRDF or BTDF component of a layer is represented as // f(w_i,w_o) = Sum_i^N Product_j^{N_i} h_{ij}(w_i,w_o) l_i(w_i,w_o) @@ -144,11 +138,16 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // + BxDFs by their type // * within the same type, by the parameters (with/without bump, then rest // - all things being equal, order by hash - core::blake3_hash_t computeHash() const + // For factors we want to order from lowest spectrality to highest, the computational expense. + // Due to Schussler and Yining, BxDFs are highly unlikely to evaluate to 0 but they can produce invalid samples. + // Also BxDFs and Emitters (which only hold IES profiles) are monochromatic so accumulating their contribution first uses least registers. + // Fresnel, Beer extinction and other analytic modifiers never produce 0s so should be evaluated last. + // Function factors which have to be evaluated via composition go even later, but within their dimension category. + core::blake3_hash_t computeHash(const obj_pool_type& pool) const { core::blake3_hasher hasher = {}; - hasher << product; - hasher << rest; + hasher << pool.deref(product)->getHash(); + hasher << pool.deref(rest)->getHash(); return hasher.operator core::blake3_hash_t(); } @@ -167,13 +166,13 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! public: inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CCorellatedTransmission);} - core::blake3_hash_t computeHash() const + core::blake3_hash_t computeHash(const obj_pool_type& pool) const { core::blake3_hasher hasher = {}; - hasher << btdf; - hasher << brdfBottom; - hasher << coated; - hasher << next; + hasher << pool.deref(btdf)->getHash(); + hasher << pool.deref(brdfBottom)->getHash(); + hasher << pool.deref(coated)->getHash(); + hasher << pool.deref(next)->getHash(); return hasher.operator core::blake3_hash_t(); } @@ -194,11 +193,11 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! public: inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(COrientedLayer);} - inline core::blake3_hash_t computeHash() const + inline core::blake3_hash_t computeHash(const obj_pool_type& pool) const { core::blake3_hasher hasher = {}; - hasher << brdfTop; - hasher << firstTransmission; + hasher << pool.deref(brdfTop)->getHash(); + hasher << pool.deref(firstTransmission)->getHash(); return hasher.operator core::blake3_hash_t(); } @@ -219,10 +218,10 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! public: inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CEmitter);} - inline core::blake3_hash_t computeHash() const + inline core::blake3_hash_t computeHash(const obj_pool_type& pool) const { core::blake3_hasher hasher = {}; -// hasher << parameter; +// hasher << pool.deref(profile)->getHash(); hasher << profileTransform; return hasher.operator core::blake3_hash_t(); } @@ -291,41 +290,21 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! }; //! Basic factor nodes - class CConstant final : public obj_pool_type::INonTrivial, public IExprNode + class IFactorLeaf : public IFactor {}; + class CConstant final : public obj_pool_type::INonTrivial, public IFactorLeaf { public: inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CConstant);} - inline Type getType() const override {return Type::Constant;} - - // you can set the children later - inline CConstant() = default; - protected: - inline void continueHashing(core::blake3_hasher& hasher) const override + inline core::blake3_hash_t computeHash(const obj_pool_type& pool) const override final { + core::blake3_hasher hasher = {}; + // TODO: hash the parameter + return hasher.operator core::blake3_hash_t(); } - }; - - // This one doesn't get used during canonicalization - class CAdd final : public obj_pool_type::INonTrivial, public IExprNode - { - 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; - - typed_pointer_type rhs = {}; - - protected: - inline void continueHashing(core::blake3_hasher& hasher) const override - { - hasher << rhs; - } - inline uint8_t getExtraChildCount() const override {return 1u;} - // child managment, only gets called with `ix>0` - inline typed_pointer_type getExtraChildHandle(const uint8_t ix) const override {return rhs;} + inline CConstant() = default; }; @@ -439,6 +418,9 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! protected: using CNodePool::CNodePool; + // TODO: allocate them properly + const typed_pointer_type m_blackHoleBxDF = {}; + const typed_pointer_type m_errorBxDF = {}; core::vector m_materials; core::unordered_map> m_uniqueNodes; }; diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp index ae3384d129..194f6cc5a3 100644 --- a/src/nbl/asset/material_compiler3/CTrueIR.cpp +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -65,93 +65,226 @@ auto CTrueIR::addMaterials(const SAddMaterialsArgs& args) -> core::vectorgetObjectPool(); -# - core::unordered_map brdfs; - core::unordered_map btdfs; + + // debug what we're processing + auto printSubtree = [&](const CFrontendIR::typed_pointer_type nodeH)->void + { + CFrontendIR::SDotPrinter printer(args.forest); + printer.exprStack.push(nodeH); + logger.log("Subtree Dot3 : \n%s\n", ELL_DEBUG,printer().c_str()); + }; + +// core::unordered_map brdfs; +// core::unordered_map btdfs; + + // Some of the things we must canonicalize: + // A ( f_0 (B + C) + D f_1 ) = f_0 B A + f_0 C A + f_1 D A + // Expression nodes really come in 4 variants: + // - add + // - mul + // - complement, which is equivalent to 1 ADD (-1 MUL x) + // - function/other + // BRDFs can appear only under ADD and MUL nodes, so if we want to canonicalize: + // 1. The Add above can be ignored, we form full multiplication chain to the top + // 2. Adds in sibling nodes (below the last add) cause us to have to add a factored copy to the IR + // DFS from right-to-left (inverse order of adding children to stack), would cause us to keep postifxes of the multiplier chain every time we descend into ADD. + // We want to essentially visit the parent ADD node again after dealing with its subtree (in-order traversal) then mul chain can be reset just to the parent. + // If we perform DFS stack push left-to-right, we'll know the contributor already for all the leaf nodes if we push it onto the stack. + // Then for all other leaf nodes we can accumulate them in the MUL chain, and adding their weighted contributor whenever we're back at an ADD node (be it the ancestor or sibling/cousin). + // If the contributor is null or multiplied with a null we can keep draining the stack until we're back at its immediate parent ADD node + struct SContributor + { + // the "active" contributor, basically the leftmost item in the subbranch below and ADD + typed_pointer_type contributor; + }; + core::vector contributorStack; + // Every time we encounter an AST leaf we must add the current contributor together with all the factors multiplied together + struct SFactor + { + // We only track multiplicative factors, we break down every BRDF equally into the canonical form + typed_pointer_type handle; + uint8_t negate : 1 = false; + uint8_t monochrome : 1 = true; + // extend later when allowing variable buckets + uint8_t liveSpectralChannels : 3 = 0b111; + }; + // here we keep the multiplication chain unsorted so its each to add/remove nodes as we encounter them + core::vector mulChain; // struct StackEntry { - inline bool notVisited() const {return !factor;} + inline bool notVisited() const {return !visited;} const CFrontendIR::IExprNode* node; - // the counterpart of the ancestors racked up so far - typed_pointer_type factor = {}; + // the ancestor ADD node to go back to if we hit a 0 MUL + uint16_t nonMulImmediateAncestorStackEnd = 0; + // the length of the `mulChain` at the time we first visited the node + uint16_t mulChainLen = 0; + bool visited = false; + // only relevant for Add nodes + bool addContributor = false; }; core::vector exprStack; -#ifndef DIDNT_MESS_UP - // - core::vector ancestors; -#endif + // Multiplication Chain need to be sorted in a canonical order so its easier to spot them being the same + auto sortMuls = [](const SFactor& lhs, const SFactor& rhs)->bool + { + // monochrome is cheaper + if (lhs.monochrome!=rhs.monochrome) + return lhs.monochrome; + // not doing a complement is cheaper + if (lhs.handle.value==rhs.handle.value) + return lhs.negate mulChainSortScratch; auto getContributors = [&](const CFrontendIR::typed_pointer_type bxdfRootH)->auto { typed_pointer_type headH = {}; + if (!bxdfRootH) + return headH; + printSubtree(bxdfRootH); + + // scratches are initialized + assert(mulChain.empty()); + assert(contributorStack.empty()); exprStack.push_back({.node=astPool.deref(bxdfRootH)}); - // accumulate an ancestor prefix - ancestors.clear(); + typed_pointer_type tailH = {}; while (!exprStack.empty()) { auto& entry = exprStack.back(); + using ast_expr_type_e = CFrontendIR::IExprNode::Type; + const ast_expr_type_e astExprType = entry.node->getType(); + const bool isContributor = astExprType==CFrontendIR::IExprNode::Type::Contributor; // - bool isContributor = true; - if (isContributor) + if (entry.notVisited()) { -#ifndef DIDNT_MESS_UP - // every contributor node gets its own SORTED ancestor prefix - std::sort(ancestors.begin(),ancestors.end(),[](const IExprNode* lhs, const IExprNode* rhs)->bool + if (isContributor) + { + // TODO actually make the contributor + const auto contributorType = 0; + switch (contributorType) { - // TODO: actually properly order the factors - return lhs(); - auto* const head = getObjectPool().deref(prevH); - // actually make the contributor + // dont want to deal with the contributor again + exprStack.pop_back(); + } + else { - const auto weightedH = getObjectPool().emplace(); - head->product = weightedH; - auto* weighted = getObjectPool().deref(weightedH); - if (entry.factor) - weighted->factor = entry.factor; - // actually make the contributor +#if 0 // TODO: Other factors + // spot prefix being null/zero to stop exploring + // can make the decision wholly on current factor + bool continueExploring = true; + if (continueExploring) { + // make the actual factor + auto derivedFactorH = getObjectPool().emplace(); + // TODO: find the place where to insert it + { + // shtuff + } + entry.factor = derivedFactorH; + // add self after making self + ancestors.push_back(getObjectPool().deref(entry.factor)); + // TODO: push the children nodes onto the stack + continue; } - } - head->rest = headH; - headH = prevH; #endif - } - else if (entry.notVisited()) - { -#ifndef DIDNT_MESS_UP - // spot prefix being null/zero to stop exploring - // can make the decision wholly on current factor - bool continueExploring = true; - if (continueExploring) - { - // make the actual factor - auto derivedFactorH = getObjectPool().emplace(); - // TODO: find the place where to insert it + const bool isAdd = astExprType==ast_expr_type_e::Add; + if (isAdd) + { + entry.addContributor = true; + // Current Add node will perform the job of the parent add node for this subtree + if (entry.nonMulImmediateAncestorStackEnd) + exprStack[entry.nonMulImmediateAncestorStackEnd-1].addContributor = false; + } + const bool notMul = astExprType!=ast_expr_type_e::Mul; + // go through children + const auto childCount = entry.node->getChildCount(); + // add in reverse so stack processes in order + for (auto childIx=childCount; childIx; ) { - // shtuff + // making sure we visit this node again each time a subtree of an Add node is done + if (isAdd && childIx!=childCount) + { + auto& extraEntry = exprStack.emplace_back(entry); + extraEntry.visited = true; + extraEntry.addContributor = true; + } + // regular exploration + exprStack.push_back({ + .node = astPool.deref(entry.node->getChildHandle(--childIx)), + .nonMulImmediateAncestorStackEnd = notMul ? static_cast(exprStack.size()):entry.nonMulImmediateAncestorStackEnd + }); } - entry.factor = derivedFactorH; - // add self after making self - ancestors.push_back(getObjectPool().deref(entry.factor)); - // TODO: push the children nodes onto the stack - continue; } -#endif + entry.visited = true; } else { -#ifndef DIDNT_MESS_UP - // remove self from the ancestor prefix - std::remove(ancestors.begin(),ancestors.end(),getObjectPool().deref(entry.factor)); -#endif + assert(!isContributor); + // do stuff now + switch (astExprType) + { + case ast_expr_type_e::Add: + { + // we visit leftmost subtrees first so this is the right order + { + auto* const tail = getObjectPool().deref(tailH); + tailH = getObjectPool().emplace(); + if (tailH) + tail->rest = tailH; + else + headH = tailH; + } + // add current contributor with weight to BxDF Sum + { + const auto weightedH = getObjectPool().emplace(); + getObjectPool().deref(tailH)->product = weightedH; + auto* weighted = getObjectPool().deref(weightedH); + weighted->contributor = contributorStack.back().contributor; + if (!mulChain.empty()) + { + const CFactorCombiner::SState combinerState = { + .type = CFactorCombiner::Type::Mul, + .childCount = mulChain.size() + }; + // TODO: create the combiner node + //const auto factorH = getObjectPool().emplace(); + { + // every contributor node gets its own SORTED ancestor prefix + mulChainSortScratch = mulChain; + std::sort(mulChainSortScratch.begin(),mulChainSortScratch.end(),sortMuls); + //auto oit = getObjectPool().deref(factorH)->child; + //for (const auto& mul : mulChainSortScratch) + //*(oit++) = mul.handle; + } + //weighted->factor = factorH; + } + } + // when we are done we need to reset the mul chain back to its original state + mulChain.resize(entry.mulChainLen); + break; + } + default: + break; + } + exprStack.pop_back(); } - exprStack.pop_back(); } + // we got all the AST ADD nodes on the way back out + assert(mulChain.empty()); + assert(contributorStack.empty()); return headH; }; // From f61fad61b512927e84c231d6961a77c91047546b Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Tue, 28 Apr 2026 15:05:39 +0200 Subject: [PATCH 08/45] add ability to reciprocate and copy AST trees into another forest/pool create a session struct for the IR material adding, has a temporary IR to make temporary IR nodes before deduplication and compression --- .../asset/material_compiler3/CFrontendIR.h | 21 +- .../nbl/asset/material_compiler3/CTrueIR.h | 119 ++++- .../core/alloc/SimpleBlockBasedAllocator.h | 2 +- include/nbl/core/containers/CMemoryPool.h | 6 + include/nbl/core/containers/CObjectPool.h | 9 + .../asset/material_compiler3/CFrontendIR.cpp | 140 +++-- src/nbl/asset/material_compiler3/CTrueIR.cpp | 480 +++++++++--------- 7 files changed, 486 insertions(+), 291 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index 85e628ef43..bcb3ee7e9a 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -841,6 +841,12 @@ class CFrontendIR final : public CNodePool #undef COPY_DEFAULT_IMPL #undef TYPE_NAME_STR + // + inline void reset() + { + getObjectPool().reset(); + m_rootNodes.clear(); + } // basic utilities inline typed_pointer_type createMul(const typed_pointer_type lhs, const typed_pointer_type rhs) @@ -894,14 +900,17 @@ class CFrontendIR final : public CNodePool fresnel->orientedRealEta = pool.emplace(std::move(params)); return fresnelH; } + + // To copy every node in the tree keeping same dedup, optionally can take an `orig` from another AST/pool and have the reciprocal copy over to our pool + NBL_API2 typed_pointer_type deepCopy(const typed_pointer_type orig, const CFrontendIR* pSourceIR=nullptr); - // To quickly make a matching backface BxDF from a frontface or vice versa - NBL_API2 typed_pointer_type reciprocate(const typed_pointer_type orig); + // To quickly make a matching backface BxDF from a frontface or vice versa, optionally can take an `orig` from another AST/pool and have the reciprocal copy over to our pool + NBL_API2 typed_pointer_type reciprocate(const typed_pointer_type orig, const CFrontendIR* pSourceIR=nullptr); - // a deep copy of the layer stack, wont copy the BxDFs - NBL_API2 typed_pointer_type copyLayers(const typed_pointer_type orig); - // Reverse the linked list of layers and reciprocate their Etas - NBL_API2 typed_pointer_type reverse(const typed_pointer_type orig); + // a deep copy of the layer stack, wont copy the BxDFs, optionally can take an `orig` from another AST/pool and have the reciprocal copy over to our pool + NBL_API2 typed_pointer_type copyLayers(const typed_pointer_type orig, const CFrontendIR* pSourceIR=nullptr); + // Reverse the linked list of layers and reciprocate their Etas, optionally can take an `orig` from another AST/pool and have the reciprocal copy over to our pool + NBL_API2 typed_pointer_type reverse(const typed_pointer_type orig, const CFrontendIR* pSourceIR=nullptr); // first query, we check presence of btdf layers all the way through the layer stack inline bool transmissive(const typed_pointer_type rootHandle) const diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 0ea2eb8433..5bad48f2c6 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -38,6 +38,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! virtual core::blake3_hash_t computeHash(const obj_pool_type& pool) const = 0; protected: + friend class CTrueIR; inline bool recomputeHash(const obj_pool_type& pool) { hash = computeHash(pool); @@ -146,8 +147,8 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! core::blake3_hash_t computeHash(const obj_pool_type& pool) const { core::blake3_hasher hasher = {}; - hasher << pool.deref(product)->getHash(); - hasher << pool.deref(rest)->getHash(); + hasher << (product ? pool.deref(product)->getHash():core::blake3_hash_t()); + hasher << (rest ? pool.deref(rest)->getHash():core::blake3_hash_t()); return hasher.operator core::blake3_hash_t(); } @@ -288,7 +289,6 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // - maybe share IBxDF // - and base BxDFs ? }; - //! Basic factor nodes class IFactorLeaf : public IFactor {}; class CConstant final : public obj_pool_type::INonTrivial, public IFactorLeaf @@ -352,7 +352,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! CNodePool::typed_pointer_type debugInfo = {}; }; inline std::span getMaterials() const {return m_materials;} - + struct SMaterialHandle { constexpr static inline uint32_t Invalid = ~0u; @@ -361,6 +361,19 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! uint32_t value = Invalid; }; constexpr static inline SMaterialHandle BlackholeMaterialHandle = { 0u }; + // + inline void reset() + { + getObjectPool().reset(); + m_materials.clear(); + m_uniqueNodes.clear(); + // remake reinsert the unique nodes + const SBasicNodes tmp = SBasicNodes(this); + assert(m_basicNodes==tmp); + // create the `BlackholeMaterialHandle` + m_materials = {SMaterial{.debugInfo=getObjectPool().emplace("CTrueIR's BlackHole Material")}}; + } + // Returns indices into `this->getMaterials()` for every `forest->getMaterials()` // We take the trees from the forest, and canonicalize them into our weird Domain Specific IR with Upside down expression trees. // Process: @@ -414,15 +427,103 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! private: const CTrueIR* m_ir; }; + + struct SBasicNodes final + { + public: + inline bool operator==(const SBasicNodes& other) const = default; + + typed_pointer_type blackHoleBxDF = {}; + typed_pointer_type errorBxDF = {}; + + private: + friend class CTrueIR; + NBL_API2 SBasicNodes(CTrueIR* ir); + }; + const SBasicNodes& getBasicNodes() const {return m_basicNodes;} protected: - using CNodePool::CNodePool; + NBL_API2 CTrueIR(creation_params_type&& params); + + struct SAddSession + { + inline SAddSession(const SAddMaterialsArgs& _args) : args(_args) + { + tmpAST = CFrontendIR::create({.composed={.blockSizeKBLog2=10},.maxBlocks=64}); + // give slightly more memory to IR, since the AST tends to be a bit more compact + tmpIR = CTrueIR::create({.composed={.blockSizeKBLog2=12},.maxBlocks=64}); + } + + NBL_API2 SMaterial::SOriented makeOrientedMaterial(const CFrontendIR::typed_pointer_type rootH); + + NBL_API2 typed_pointer_type makeContributors(const CFrontendIR::typed_pointer_type bxdfRootH); + + // inputs to the addMaterials function + const SAddMaterialsArgs& args; + // for rewriting AST expressions + core::smart_refctd_ptr tmpAST; + // for making IR nodes before we Merkle Hash them and remove duplicates (so main IR doesn't get bloated) + core::smart_refctd_ptr tmpIR; + // for going over layers in the AST + core::vector layerStack; + // Some of the things we must canonicalize: + // A ( f_0 (B + C) + D f_1 ) = f_0 B A + f_0 C A + f_1 D A + // Expression nodes of the Frontend AST really come in 4 variants: + // - add + // - mul + // - complement, which is equivalent to 1 ADD (-1 MUL x) + // - function/other + // BRDFs can appear only under ADD and MUL nodes in the AST not the function/other/complement, so if we want to canonicalize: + // 1. The Add above can be ignored, we form full multiplication chain to the top + // 2. Adds in sibling nodes (below the last add) cause us to have to add a factored copy to the IR + // DFS from right-to-left (inverse order of adding children to stack), would cause us to keep postifxes of the multiplier chain every time we descend into ADD. + // We want to essentially visit the parent ADD node again after dealing with its subtree (in-order traversal) then mul chain can be reset just to the parent. + // If we perform DFS stack push left-to-right, we'll know the contributor already for all the leaf nodes if we push it onto the stack. + // Then for all other leaf nodes we can accumulate them in the MUL chain, and adding their weighted contributor whenever we're back at an ADD node (be it the ancestor or sibling/cousin). + // If the contributor is null or multiplied with a null we can keep draining the stack until we're back at its immediate parent ADD node. + struct SContributor + { + // the "active" contributor, basically the leftmost item in the subbranch below and ADD + typed_pointer_type contributor; + }; + core::vector contributorStack; + // Every time we encounter an AST leaf we must add the current contributor together with all the factors multiplied together + struct SFactor + { + // We only track multiplicative factors, we break down every BRDF equally into the canonical form + typed_pointer_type handle; + uint8_t negate : 1 = false; + uint8_t monochrome : 1 = true; + // extend later when allowing variable bucket count + uint8_t liveSpectralChannels : 3 = 0b111; + }; + // here we keep the multiplication chain unsorted so its each to add/remove nodes as we encounter them + core::vector mulChain; + // scratch for sorting the mul chain before adding a contributor + core::vector mulChainSortScratch; + // By maintaining a hash map of AST nodes which simplify to a Constant (unity, or zero, or other) we could resolve the issue of the `nonMulImmediateAncestorStackEnd` + // which has us adding the same non-mul node multiple times to stack during the traversal. + // However how much of that would be moving IR manipulation into the AST ? + struct StackEntry + { + inline bool notVisited() const {return !visited;} + + const CFrontendIR::IExprNode* node; + // the ancestor ADD node to go back to if we hit a 0 MUL, or if our ADD or any other node becomes 0 + uint16_t nonMulImmediateAncestorStackEnd = 0; + // the length of the `mulChain` at the time we first visited the node + uint16_t mulChainLen = 0; + bool visited = false; + // only relevant for Add nodes + bool addContributor = false; + }; + core::vector exprStack; + }; - // TODO: allocate them properly - const typed_pointer_type m_blackHoleBxDF = {}; - const typed_pointer_type m_errorBxDF = {}; core::vector m_materials; - core::unordered_map> m_uniqueNodes; + core::unordered_map> m_uniqueNodes; + friend struct SBasicNodes; + const SBasicNodes m_basicNodes; }; NBL_ENUM_ADD_BITWISE_OPERATORS(CTrueIR::SMaterial::EMetadataBits) diff --git a/include/nbl/core/alloc/SimpleBlockBasedAllocator.h b/include/nbl/core/alloc/SimpleBlockBasedAllocator.h index a3679dba1a..dce33987f8 100644 --- a/include/nbl/core/alloc/SimpleBlockBasedAllocator.h +++ b/include/nbl/core/alloc/SimpleBlockBasedAllocator.h @@ -404,7 +404,7 @@ class SimpleBlockBasedAllocator final : protected block_t* block = entry.second; if (recycledBlocks.size()addrAlloc.reset(); + block->getAllocator().reset(); const auto id = m_blockIndexAlloc.alloc_addr(1,1); assert(id!=block_id_alloc_t::invalid_address); recycledBlocks[id] = block; diff --git a/include/nbl/core/containers/CMemoryPool.h b/include/nbl/core/containers/CMemoryPool.h index 118a80a695..207cc36362 100644 --- a/include/nbl/core/containers/CMemoryPool.h +++ b/include/nbl/core/containers/CMemoryPool.h @@ -38,6 +38,12 @@ class CMemoryPool final : public Uncopyable using creation_params_type = block_allocator_st_type::SCreationParams; inline CMemoryPool(creation_params_type&& params) : m_block_alctr(std::move(params)) {} + + // + inline void reset() + { + m_block_alctr.reset(); + } // template requires (!std::is_const_v) diff --git a/include/nbl/core/containers/CObjectPool.h b/include/nbl/core/containers/CObjectPool.h index 71c54be5ed..b49c668e71 100644 --- a/include/nbl/core/containers/CObjectPool.h +++ b/include/nbl/core/containers/CObjectPool.h @@ -86,6 +86,15 @@ class CObjectPool final : public IObjectPoolBase for (auto& entry : m_allocations) destroy(deref(entry.first),entry.second.count,entry.second.stride); } + + // + inline void reset() + { + for (auto& entry : m_allocations) + destroy(deref(entry.first),entry.second.count,entry.second.stride); + m_allocations.clear(); + m_pool.reset(); + } // struct check_t diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index d77ac287da..fcfaa388f4 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -116,41 +116,94 @@ bool CFrontendIR::CCookTorrance::invalid(const SInvalidCheckArgs& args) const } -auto CFrontendIR::reciprocate(const typed_pointer_type orig) -> typed_pointer_type +auto CFrontendIR::deepCopy(const typed_pointer_type orig, const CFrontendIR* pSourceIR) -> typed_pointer_type { - auto& pool = getObjectPool(); - struct SEntry + auto& dstPool = getObjectPool(); + // if not explicitly other, then its ours + if (!pSourceIR) + pSourceIR = this; + const auto& srcPool = pSourceIR->getObjectPool(); + + core::vector> stack; + stack.reserve(32); + stack.push_back(orig); + // use a hashmap to not explore whole DAG + core::unordered_map,typed_pointer_type> substitutions; + while (!stack.empty()) { - typed_pointer_type handle; - bool visited = false; - }; - core::vector stack; + const auto entry = stack.back(); + const auto* const node = srcPool.deref(entry); + if (!node) // this is an error + return {}; + const auto childCount = node->getChildCount(); + if (auto& copyH=substitutions[entry]; !copyH) + { + for (uint8_t c=0; cgetChildHandle(c); + if (auto child=srcPool.deref(childH); !child) + continue; // this is not an error + stack.push_back(childH); + } + // copy copies everything including child handles + copyH = node->copy(this); + if (!copyH) + return {}; + } + else + { + auto* const copy = dstPool.deref(copyH); + for (uint8_t c=0; cgetChildHandle(c); + if (!childH) + continue; + auto found = substitutions.find(childH); + assert(found!=substitutions.end()); + copy->setChild(c,found->second); + } + stack.pop_back(); + } + } + return substitutions[orig]; +} + +auto CFrontendIR::reciprocate(const typed_pointer_type orig, const CFrontendIR* pSourceIR) -> typed_pointer_type +{ + auto& dstPool = getObjectPool(); + // if not explicitly other, then its ours + if (!pSourceIR) + pSourceIR = this; + const auto& srcPool = pSourceIR->getObjectPool(); + + core::vector> stack; stack.reserve(32); - stack.push_back({.handle=orig}); + stack.push_back(orig); + // use a hashmap to not explore whole DAG + core::unordered_set> visited; // use a hashmap because of holes in child arrays core::unordered_map,typed_pointer_type> substitutions; while (!stack.empty()) { - auto& entry = stack.back(); - const auto* const node = pool.deref(entry.handle); + const auto entry = stack.back(); + const auto* const node = srcPool.deref(entry); if (!node) // this is an error return {}; const auto childCount = node->getChildCount(); - if (entry.visited) + if (auto [it,inserted] = visited.insert(entry); inserted) { - entry.visited = true; for (uint8_t c=0; cgetChildHandle(c); - if (auto child=pool.deref(childH); !child) + if (auto child=srcPool.deref(childH); !child) continue; // this is not an error - stack.push_back({.handle=childH}); + stack.push_back(childH); } } else { const bool needToReciprocate = node->reciprocatable(); - bool needToCopy = needToReciprocate; + bool needToCopy = pSourceIR!=this || needToReciprocate; // if one descendant has changed then we need to copy node if (!needToCopy) { @@ -165,8 +218,8 @@ auto CFrontendIR::reciprocate(const typed_pointer_type orig) -> if (needToCopy) { const auto copyH = node->copy(this); - // copy copies everything including children - auto* const copy = pool.deref(copyH); + // copy copies everything including child handles + auto* const copy = dstPool.deref(copyH); if (!copy) return {}; if (needToReciprocate) @@ -180,7 +233,7 @@ auto CFrontendIR::reciprocate(const typed_pointer_type orig) -> if (auto found=substitutions.find(childH); found!=substitutions.end()) copy->setChild(c,found->second); } - substitutions.insert({entry.handle,copyH}); + substitutions.insert({entry,copyH}); } stack.pop_back(); } @@ -191,15 +244,27 @@ auto CFrontendIR::reciprocate(const typed_pointer_type orig) -> return substitutions[orig]; } -auto CFrontendIR::copyLayers(const typed_pointer_type orig) -> typed_pointer_type +auto CFrontendIR::copyLayers(const typed_pointer_type orig, const CFrontendIR* pSourceIR) -> typed_pointer_type { - auto& pool = getObjectPool(); - auto copyH = pool.emplace(); + auto& dstPool = getObjectPool(); + // if not explicitly other, then its ours + if (!pSourceIR) + pSourceIR = this; + const auto& srcPool = pSourceIR->getObjectPool(); + + auto copyH = dstPool.emplace(); { - auto* outLayer = pool.deref(copyH); - for (const auto* layer=pool.deref(orig); true; layer=pool.deref(layer->coated)) + auto* outLayer = dstPool.deref(copyH); + for (const auto* layer=srcPool.deref(orig); true; layer=srcPool.deref(layer->coated)) { *outLayer = *layer; + // need to deep copy the nodes + if (pSourceIR!=this) + { + outLayer->brdfBottom = deepCopy(layer->brdfBottom,pSourceIR)._const_cast(); + outLayer->btdf = deepCopy(layer->btdf,pSourceIR)._const_cast(); + outLayer->brdfTop = deepCopy(layer->brdfTop,pSourceIR)._const_cast(); + } if (!layer->coated) { // terminate the new stack @@ -207,33 +272,38 @@ auto CFrontendIR::copyLayers(const typed_pointer_type orig) -> typ break; } // continue the new stack - outLayer->coated = pool.emplace(); - outLayer = pool.deref(outLayer->coated); + outLayer->coated = dstPool.emplace(); + outLayer = dstPool.deref(outLayer->coated); } } return copyH; } -auto CFrontendIR::reverse(const typed_pointer_type orig) -> typed_pointer_type +auto CFrontendIR::reverse(const typed_pointer_type orig, const CFrontendIR* pSourceIR) -> typed_pointer_type { - auto& pool = getObjectPool(); + auto& dstPool = getObjectPool(); + // if not explicitly other, then its ours + if (!pSourceIR) + pSourceIR = this; + const auto& srcPool = pSourceIR->getObjectPool(); + // we build the new linked list from the tail - auto copyH = pool.emplace(); + auto copyH = dstPool.emplace(); { - auto* outLayer = pool.deref(copyH); + auto* outLayer = dstPool.deref(copyH); typed_pointer_type underLayerH={}; - for (const auto* layer=pool.deref(orig); true; layer=pool.deref(layer->coated)) + for (const auto* layer=srcPool.deref(orig); true; layer=srcPool.deref(layer->coated)) { outLayer->coated = underLayerH; // we reciprocate everything because numerator and denominator switch (top and bottom of layer stack) - outLayer->brdfBottom = reciprocate(layer->brdfTop)._const_cast(); - outLayer->btdf = reciprocate(layer->btdf)._const_cast(); - outLayer->brdfTop = reciprocate(layer->brdfBottom)._const_cast(); + outLayer->brdfBottom = reciprocate(layer->brdfTop,pSourceIR)._const_cast(); + outLayer->btdf = reciprocate(layer->btdf,pSourceIR)._const_cast(); + outLayer->brdfTop = reciprocate(layer->brdfBottom,pSourceIR)._const_cast(); if (!layer->coated) break; underLayerH = copyH; - copyH = pool.emplace(); - outLayer = pool.deref(copyH); + copyH = dstPool.emplace(); + outLayer = dstPool.deref(copyH); } } return copyH; diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp index 194f6cc5a3..9ea37e219b 100644 --- a/src/nbl/asset/material_compiler3/CTrueIR.cpp +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -56,77 +56,113 @@ using namespace nbl::system; // Spill should be a big circular buffer allocated per-subgroup - hard to control in Pipeline Shaders https://github.com/KhronosGroup/Vulkan-Docs/issues/2717 // So need to allocate worst case spill for all rays in a dispatch (although can do persistent threads and reduce the dispatch size a little) -auto CTrueIR::addMaterials(const SAddMaterialsArgs& args) -> core::vector + +CTrueIR::CTrueIR(creation_params_type&& params) : CNodePool(std::move(params)), m_basicNodes(this) { - const auto logger = args.logger; - if (!args) + reset(); +} + +CTrueIR::SBasicNodes::SBasicNodes(CTrueIR* ir) +{ + auto& pool = ir->getObjectPool(); + // + blackHoleBxDF = pool.emplace(); { - logger.log("Invalid Arguments to `CTrueIR::addMaterials`",ELL_ERROR); - return {}; + auto* const node = pool.deref(blackHoleBxDF._const_cast()); + node->product = {}; + const bool success = node->recomputeHash(pool); + assert(success); + ir->m_uniqueNodes[node->getHash()] = blackHoleBxDF; } + // + errorBxDF = pool.emplace(); + { + auto* const node = pool.deref(errorBxDF._const_cast()); + node->product = {}; // TODO: magenta diffuse and rest of setup + //const bool success = node->recomputeHash(pool); + //assert(success); + } +} + + +auto CTrueIR::SAddSession::makeOrientedMaterial(const CFrontendIR::typed_pointer_type rootH) -> SMaterial::SOriented +{ + SMaterial::SOriented retval = {}; + assert(layerStack.empty()); + auto& astPool = args.forest->getObjectPool(); + auto& irPool = tmpIR->getObjectPool(); + // go down through layers and create all the dependencies + for (const auto* layer=astPool.deref(rootH); layer; layer=astPool.deref(layer->coated)) + { + // TODO: actually re-check the expressions for being null after optimization + bool noTopReflection = !layer->brdfTop; + bool noTransmission = !layer->btdf; + // if there's literally nothing on the top level, you can't get to the next layer to retroreflect from it + if (noTopReflection && noTransmission) + { + args.logger.log("Skipping current layer and farther ones due to no transmission and reflection",ELL_DEBUG); + break; + } + layerStack.push_back(layer); + // find out rest of the layers don't matter because they're blocked from being seen, its not a complete check + if (noTransmission) + { + args.logger.log("Skipping remaining layers due to no transmission",ELL_DEBUG); + break; + } +// TODO: handle the rest of this stuff +//assert(false); + // Only if we're not in the last layer do we care about the bottom BRDF (you can't hit it otherwise) + // Note that this won't catch the next layer being a blackhole and needs to be undone if it is + if (layer->coated && layer->brdfBottom) + { + // do stuff with brdfBottom + } + } + if (!layerStack.empty()) + retval.metadata |= SMaterial::EMetadataBits::NotBlackhole; + // then go back up and make the layers + while (!layerStack.empty()) + { + const auto* const inLayer = layerStack.back(); + layerStack.pop_back(); + // allocate a layer + const auto layerH = irPool.emplace(); + auto* const outLayer = irPool.deref(layerH); + retval.root = layerH; + // process the BTDF + //... + // process the top BRDF + outLayer->brdfTop = makeContributors(inLayer->brdfTop); + // if BTDF has delta transmissions, then via the sampling property hoist next layer into current layer BRDFs with the DeltaTransmission weights applied + // hmm this would require decorrellation... because don't want rest of BTDF to affect + //... + } + // skip replace delta transmissions by the layer undernearth, if null then keep as delta + + // AST is Sum Expression to the BRDF nodes + // We need to keep the Ancestor prefix as an unrolled linked list + return retval; +} +// +auto CTrueIR::SAddSession::makeContributors(const CFrontendIR::typed_pointer_type bxdfRootH) -> typed_pointer_type +{ // debug what we're processing auto printSubtree = [&](const CFrontendIR::typed_pointer_type nodeH)->void { CFrontendIR::SDotPrinter printer(args.forest); printer.exprStack.push(nodeH); - logger.log("Subtree Dot3 : \n%s\n", ELL_DEBUG,printer().c_str()); - }; - -// core::unordered_map brdfs; -// core::unordered_map btdfs; - - // Some of the things we must canonicalize: - // A ( f_0 (B + C) + D f_1 ) = f_0 B A + f_0 C A + f_1 D A - // Expression nodes really come in 4 variants: - // - add - // - mul - // - complement, which is equivalent to 1 ADD (-1 MUL x) - // - function/other - // BRDFs can appear only under ADD and MUL nodes, so if we want to canonicalize: - // 1. The Add above can be ignored, we form full multiplication chain to the top - // 2. Adds in sibling nodes (below the last add) cause us to have to add a factored copy to the IR - // DFS from right-to-left (inverse order of adding children to stack), would cause us to keep postifxes of the multiplier chain every time we descend into ADD. - // We want to essentially visit the parent ADD node again after dealing with its subtree (in-order traversal) then mul chain can be reset just to the parent. - // If we perform DFS stack push left-to-right, we'll know the contributor already for all the leaf nodes if we push it onto the stack. - // Then for all other leaf nodes we can accumulate them in the MUL chain, and adding their weighted contributor whenever we're back at an ADD node (be it the ancestor or sibling/cousin). - // If the contributor is null or multiplied with a null we can keep draining the stack until we're back at its immediate parent ADD node - struct SContributor - { - // the "active" contributor, basically the leftmost item in the subbranch below and ADD - typed_pointer_type contributor; + args.logger.log("Subtree Dot3 : \n%s\n", ELL_DEBUG,printer().c_str()); }; - core::vector contributorStack; - // Every time we encounter an AST leaf we must add the current contributor together with all the factors multiplied together - struct SFactor - { - // We only track multiplicative factors, we break down every BRDF equally into the canonical form - typed_pointer_type handle; - uint8_t negate : 1 = false; - uint8_t monochrome : 1 = true; - // extend later when allowing variable buckets - uint8_t liveSpectralChannels : 3 = 0b111; - }; - // here we keep the multiplication chain unsorted so its each to add/remove nodes as we encounter them - core::vector mulChain; - // - struct StackEntry - { - inline bool notVisited() const {return !visited;} - - const CFrontendIR::IExprNode* node; - // the ancestor ADD node to go back to if we hit a 0 MUL - uint16_t nonMulImmediateAncestorStackEnd = 0; - // the length of the `mulChain` at the time we first visited the node - uint16_t mulChainLen = 0; - bool visited = false; - // only relevant for Add nodes - bool addContributor = false; - }; - core::vector exprStack; + typed_pointer_type headH = {}; + if (!bxdfRootH) + return headH; + printSubtree(bxdfRootH); + // Multiplication Chain need to be sorted in a canonical order so its easier to spot them being the same - auto sortMuls = [](const SFactor& lhs, const SFactor& rhs)->bool + auto sortMuls = [](const SAddSession::SFactor& lhs, const SAddSession::SFactor& rhs)->bool { // monochrome is cheaper if (lhs.monochrome!=rhs.monochrome) @@ -137,217 +173,176 @@ auto CTrueIR::addMaterials(const SAddMaterialsArgs& args) -> core::vector mulChainSortScratch; - auto getContributors = [&](const CFrontendIR::typed_pointer_type bxdfRootH)->auto - { - typed_pointer_type headH = {}; - if (!bxdfRootH) - return headH; - printSubtree(bxdfRootH); - // scratches are initialized - assert(mulChain.empty()); - assert(contributorStack.empty()); - exprStack.push_back({.node=astPool.deref(bxdfRootH)}); - typed_pointer_type tailH = {}; - while (!exprStack.empty()) + auto& astPool = args.forest->getObjectPool(); + auto& irPool = tmpIR->getObjectPool(); + // scratches are initialized + assert(mulChain.empty()); + assert(contributorStack.empty()); + exprStack.push_back({.node=astPool.deref(bxdfRootH)}); + typed_pointer_type tailH = {}; + while (!exprStack.empty()) + { + auto& entry = exprStack.back(); + using ast_expr_type_e = CFrontendIR::IExprNode::Type; + const ast_expr_type_e astExprType = entry.node->getType(); + const bool isContributor = astExprType==CFrontendIR::IExprNode::Type::Contributor; + // + if (entry.notVisited()) { - auto& entry = exprStack.back(); - using ast_expr_type_e = CFrontendIR::IExprNode::Type; - const ast_expr_type_e astExprType = entry.node->getType(); - const bool isContributor = astExprType==CFrontendIR::IExprNode::Type::Contributor; - // - if (entry.notVisited()) + if (isContributor) { - if (isContributor) + // TODO actually make the contributor + const auto contributorType = 0; + switch (contributorType) { - // TODO actually make the contributor - const auto contributorType = 0; - switch (contributorType) + case 45: { - case 45: - { - // TODO: add to contributorStack - contributorStack.push_back({.contributor={}}); - break; - } - // unsupported contributor - default: - logger.log("Unsupported contributor type %d skipping subtree",ELL_ERROR,contributorType); - return m_errorBxDF; + // TODO: add to contributorStack + contributorStack.push_back({.contributor={}}); + break; + } + // unsupported contributor + default: + { + args.logger.log("Unsupported contributor type %d skipping subtree",ELL_ERROR,contributorType); + exprStack.clear(); + mulChain.clear(); + contributorStack.clear(); + return tmpIR->getBasicNodes().errorBxDF; } - // dont want to deal with the contributor again - exprStack.pop_back(); } - else - { + // dont want to deal with the contributor again + exprStack.pop_back(); + } + else + { #if 0 // TODO: Other factors - // spot prefix being null/zero to stop exploring - // can make the decision wholly on current factor - bool continueExploring = true; - if (continueExploring) + // spot prefix being null/zero to stop exploring + // can make the decision wholly on current factor + bool continueExploring = true; + if (continueExploring) + { + // make the actual factor + auto derivedFactorH = getObjectPool().emplace(); + // TODO: find the place where to insert it { - // make the actual factor - auto derivedFactorH = getObjectPool().emplace(); - // TODO: find the place where to insert it - { - // shtuff - } - entry.factor = derivedFactorH; - // add self after making self - ancestors.push_back(getObjectPool().deref(entry.factor)); - // TODO: push the children nodes onto the stack - continue; + // shtuff } + entry.factor = derivedFactorH; + // add self after making self + ancestors.push_back(getObjectPool().deref(entry.factor)); + // TODO: push the children nodes onto the stack + continue; + } #endif - const bool isAdd = astExprType==ast_expr_type_e::Add; - if (isAdd) - { - entry.addContributor = true; - // Current Add node will perform the job of the parent add node for this subtree - if (entry.nonMulImmediateAncestorStackEnd) + const bool isAdd = astExprType==ast_expr_type_e::Add; + if (isAdd) + { + entry.addContributor = true; + // Current Add node will perform the job of the parent add node for this subtree + if (entry.nonMulImmediateAncestorStackEnd) exprStack[entry.nonMulImmediateAncestorStackEnd-1].addContributor = false; - } - const bool notMul = astExprType!=ast_expr_type_e::Mul; - // go through children - const auto childCount = entry.node->getChildCount(); - // add in reverse so stack processes in order - for (auto childIx=childCount; childIx; ) + } + const bool notMul = astExprType!=ast_expr_type_e::Mul; + // go through children + const auto childCount = entry.node->getChildCount(); + // add in reverse so stack processes in order + for (auto childIx=childCount; childIx; ) + { + // making sure we visit this node again each time a subtree of an Add node is done + if (isAdd && childIx!=childCount) { - // making sure we visit this node again each time a subtree of an Add node is done - if (isAdd && childIx!=childCount) - { - auto& extraEntry = exprStack.emplace_back(entry); - extraEntry.visited = true; - extraEntry.addContributor = true; - } - // regular exploration - exprStack.push_back({ - .node = astPool.deref(entry.node->getChildHandle(--childIx)), - .nonMulImmediateAncestorStackEnd = notMul ? static_cast(exprStack.size()):entry.nonMulImmediateAncestorStackEnd - }); + auto& extraEntry = exprStack.emplace_back(entry); + extraEntry.visited = true; + extraEntry.addContributor = true; } + // regular exploration + exprStack.push_back({ + .node = astPool.deref(entry.node->getChildHandle(--childIx)), + // to be able to go back to the non mul that is supposed to add our subtree + .nonMulImmediateAncestorStackEnd = notMul ? static_cast(exprStack.size()):entry.nonMulImmediateAncestorStackEnd + }); } - entry.visited = true; } - else + entry.visited = true; + } + else + { + assert(!isContributor); + // do stuff now + switch (astExprType) { - assert(!isContributor); - // do stuff now - switch (astExprType) + case ast_expr_type_e::Add: { - case ast_expr_type_e::Add: + // we visited the leftmost subtrees first so this is the right order { - // we visit leftmost subtrees first so this is the right order - { - auto* const tail = getObjectPool().deref(tailH); - tailH = getObjectPool().emplace(); - if (tailH) - tail->rest = tailH; - else - headH = tailH; - } - // add current contributor with weight to BxDF Sum + auto* const tail = irPool.deref(tailH); + tailH = irPool.emplace(); + if (tailH) + tail->rest = tailH; + else + headH = tailH; + } + // add current contributor with weight to BxDF Sum + { + const auto weightedH = irPool.emplace(); + irPool.deref(tailH)->product = weightedH; + auto* weighted = irPool.deref(weightedH); + weighted->contributor = contributorStack.back().contributor; + if (!mulChain.empty()) { - const auto weightedH = getObjectPool().emplace(); - getObjectPool().deref(tailH)->product = weightedH; - auto* weighted = getObjectPool().deref(weightedH); - weighted->contributor = contributorStack.back().contributor; - if (!mulChain.empty()) + const CFactorCombiner::SState combinerState = { + .type = CFactorCombiner::Type::Mul, + .childCount = mulChain.size() + }; + // TODO: create the combiner node + //const auto factorH = getObjectPool().emplace(); { - const CFactorCombiner::SState combinerState = { - .type = CFactorCombiner::Type::Mul, - .childCount = mulChain.size() - }; - // TODO: create the combiner node - //const auto factorH = getObjectPool().emplace(); - { - // every contributor node gets its own SORTED ancestor prefix - mulChainSortScratch = mulChain; - std::sort(mulChainSortScratch.begin(),mulChainSortScratch.end(),sortMuls); - //auto oit = getObjectPool().deref(factorH)->child; - //for (const auto& mul : mulChainSortScratch) - //*(oit++) = mul.handle; - } - //weighted->factor = factorH; + // every contributor node gets its own SORTED ancestor prefix + mulChainSortScratch = mulChain; + std::sort(mulChainSortScratch.begin(),mulChainSortScratch.end(),sortMuls); + //auto oit = getObjectPool().deref(factorH)->child; + //for (const auto& mul : mulChainSortScratch) + //*(oit++) = mul.handle; } + //weighted->factor = factorH; } - // when we are done we need to reset the mul chain back to its original state - mulChain.resize(entry.mulChainLen); - break; } - default: - break; + // when we are done we need to reset the mul chain back to its original state + mulChain.resize(entry.mulChainLen); + break; } - exprStack.pop_back(); + default: + break; } + exprStack.pop_back(); } - // we got all the AST ADD nodes on the way back out - assert(mulChain.empty()); - assert(contributorStack.empty()); - return headH; - }; - // - core::vector layerStack; - auto makeOrientedMaterial = [&](const CFrontendIR::typed_pointer_type rootH)->SMaterial::SOriented + } + // we got all the AST ADD nodes on the way back out + assert(mulChain.empty()); + assert(contributorStack.empty()); + return headH; +} + +// +auto CTrueIR::addMaterials(const SAddMaterialsArgs& args) -> core::vector +{ + const auto logger = args.logger; + if (!args) { - SMaterial::SOriented retval = {}; + logger.log("Invalid Arguments to `CTrueIR::addMaterials`",ELL_ERROR); + return {}; + } + const auto& astPool = args.forest->getObjectPool(); - // go down through layers and create all the dependencies - layerStack.clear(); - for (const auto* layer=astPool.deref(rootH); layer; layer=astPool.deref(layer->coated)) - { - // TODO: actually re-check the expressions for being null after optimization - bool noTopReflection = !layer->brdfTop; - bool noTransmission = !layer->btdf; - // if there's literally nothing on the top level, you can't get to the next layer to retroreflect from it - if (noTopReflection && noTransmission) - { - logger.log("Skipping current layer and farther ones due to no transmission and reflection",ELL_DEBUG); - break; - } - layerStack.push_back(layer); - // find out rest of the layers don't matter because they're blocked from being seen, its not a complete check - if (noTransmission) - { - logger.log("Skipping remaining layers due to no transmission",ELL_DEBUG); - break; - } - // Only if we're not in the last layer do we care about the bottom BRDF (you can't hit it otherwise) - // Note that this won't catch the next layer being a blackhole and needs to be undone if it is - if (layer->coated && layer->brdfBottom) - { - // do stuff with brdfBottom - } - } - if (!layerStack.empty()) - retval.metadata |= SMaterial::EMetadataBits::NotBlackhole; - // then go back up and make the layers - while (!layerStack.empty()) - { - const auto* const inLayer = layerStack.back(); - layerStack.pop_back(); - // allocate a layer - const auto layerH = getObjectPool().emplace(); - auto* const outLayer = getObjectPool().deref(layerH); - retval.root = layerH; - // process the BTDF - //... - // process the top BRDF - outLayer->brdfTop = getContributors(inLayer->brdfTop); - // if BTDF has delta transmissions, then via the sampling property hoist next layer into current layer BRDFs with the DeltaTransmission weights applied - // hmm this would require decorrellation... because don't want rest of BTDF to affect - //... - } - // skip replace delta transmissions by the layer undernearth, if null then keep as delta - // AST is Sum Expression to the BRDF nodes - // We need to keep the Ancestor prefix as an unrolled linked list - return retval; - }; +// core::unordered_map brdfs; +// core::unordered_map btdfs; + SAddSession session = {args}; const auto inputMaterials = args.forest->getMaterials(); - core::vector retval(inputMaterials.size(), {}); + core::vector retval(inputMaterials.size(),{}); auto outIt = retval.begin(); for (const auto& rootH : inputMaterials) { @@ -360,9 +355,14 @@ auto CTrueIR::addMaterials(const SAddMaterialsArgs& args) -> core::vectorreset(); + // reverse AST into another tree + session.tmpAST->reset(); + const auto backRootH = session.tmpAST->reverse(rootH,args.forest); + // TODO: hash, deduplicate, collect metadata and insert into current IR SMaterial material = { - .front = makeOrientedMaterial(rootH), -// .back = makeOrientedMaterial(rootH) // TODO: reverse AST into another tree + .front = session.makeOrientedMaterial(rootH), + .back = session.makeOrientedMaterial(backRootH) }; // TODO: better debug info From 36268317a2e314772e3547eec3eb6e155df0e5c9 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Tue, 28 Apr 2026 20:10:45 +0200 Subject: [PATCH 09/45] I had the whole design backwards, IR shouldn't know about the different frontends (much like it doesn't know about the backends) --- .../asset/material_compiler3/CFrontendIR.h | 159 ++++++++-- .../nbl/asset/material_compiler3/CTrueIR.h | 119 ++----- .../asset/material_compiler3/CFrontendIR.cpp | 282 +++++++++++++++++ src/nbl/asset/material_compiler3/CTrueIR.cpp | 295 +----------------- 4 files changed, 445 insertions(+), 410 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index bcb3ee7e9a..771f323483 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -1,20 +1,13 @@ -// Copyright (C) 2022-2025 - DevSH Graphics Programming Sp. z O.O. + +// 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/CTrueIR.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 { @@ -258,6 +251,7 @@ class CFrontendIR final : public CNodePool return retval; } + // TODO: rename to `EType` and the `getType` to `getExprNodeType` // 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 @@ -327,6 +321,11 @@ class CFrontendIR final : public CNodePool { public: inline Type getType() const override final {return Type::Contributor;} + + protected: + friend class CFrontendIR; + using ir_contributor_handle_t = CTrueIR::typed_pointer_type; + virtual ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR::obj_pool_type& pool) const = 0; }; // This node could also represent non directional emission, but we have another node for that @@ -568,6 +567,8 @@ class CFrontendIR final : public CNodePool NBL_API2 bool invalid(const SInvalidCheckArgs& args) const override; NBL_API2 void printDot(std::ostringstream& sstr, const core::string& selfID) const override; + + NBL_API2 ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR::obj_pool_type& pool) const; }; //! 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. @@ -775,6 +776,8 @@ class CFrontendIR final : public CNodePool protected: COPY_DEFAULT_IMPL + + NBL_API2 ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR::obj_pool_type& pool) const; }; //! 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 @@ -793,6 +796,8 @@ class CFrontendIR final : public CNodePool NBL_API2 bool invalid(const SInvalidCheckArgs& args) const override; NBL_API2 void printDot(std::ostringstream& sstr, const core::string& selfID) const override; + + NBL_API2 ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR::obj_pool_type& pool) const; }; // Supports anisotropy for all models class CCookTorrance final : public IBxDF @@ -837,6 +842,8 @@ class CFrontendIR final : public CNodePool 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; + + NBL_API2 ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR::obj_pool_type& pool) const; }; #undef COPY_DEFAULT_IMPL #undef TYPE_NAME_STR @@ -845,7 +852,6 @@ class CFrontendIR final : public CNodePool inline void reset() { getObjectPool().reset(); - m_rootNodes.clear(); } // basic utilities @@ -930,18 +936,46 @@ class CFrontendIR final : public CNodePool // 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 rootHandle, system::logger_opt_ptr logger) const; - inline std::span> getMaterials() const {return m_rootNodes;} + // Each material comes down to this, after lowering to the true IR ir the indices into `ir->getMaterials()` are returned + // We take the trees from the forest, and canonicalize them into our weird Domain Specific IR with Upside down expression trees. + // Process: + // 1. Decompression (duplicating nodes, etc.) + // 2. Canonicalize Expressions (Transform into Sum-Product form, DCE, etc.) + // 3. Split BTDFs (front vs. back part), reciprocate Etas + // 4. Simplify and Hoist Layer terms (delta sampling property) + // 5. Subexpression elimination + // Further transforms in the IR can be done by invoking IR passes + struct SAddMaterialsArgs + { + explicit inline operator bool() const {return !rootNodes.empty() && ir && result;} - // 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 rootNode, system::logger_opt_ptr logger) + std::span> rootNodes; + CTrueIR* ir; + CTrueIR::SMaterialHandle* result; + system::logger_opt_ptr logger; + }; + // returns the number of materials successfully converted + inline uint32_t addMaterials(const SAddMaterialsArgs args) const { - if (valid(rootNode,logger)) + uint32_t retval = 0; + if (!args) { - m_rootNodes.push_back(rootNode); - return true; + args.logger.log("Invalid Arguments to `CTrueIR::addMaterials`",system::ILogger::ELL_ERROR); + return retval; } - return false; + SAdd2IRSession session = {args}; + auto outIt = args.result; + for (const auto& rootH : args.rootNodes) + { + if (!rootH) // its a valid material (blackhole) + *outIt = CTrueIR::BlackholeMaterialHandle; + else if (valid(rootH,args.logger)) + *outIt = makeFinalIR(rootH,session); + // now check for failure + if (*outIt) + retval++; + } + return retval; } // For Debug Visualization @@ -974,6 +1008,86 @@ class CFrontendIR final : public CNodePool protected: using CNodePool::CNodePool; + + struct SAdd2IRSession + { + inline SAdd2IRSession(const SAddMaterialsArgs& _args) : args(_args) + { + tmpAST = CFrontendIR::create({.composed={.blockSizeKBLog2=10},.maxBlocks=64}); + // give slightly more memory to IR, since the AST tends to be a bit more compact + tmpIR = CTrueIR::create({.composed={.blockSizeKBLog2=12},.maxBlocks=64}); + } + + using oriented_material_t = CTrueIR::SMaterial::SOriented; + NBL_API2 oriented_material_t makeOrientedMaterial(const CFrontendIR::typed_pointer_type rootH, const CFrontendIR* _srcAST); + + NBL_API2 CTrueIR::typed_pointer_type makeContributors(const CFrontendIR::typed_pointer_type bxdfRootH); + + // inputs to the addMaterials function + const SAddMaterialsArgs& args; + // for rewriting AST expressions + core::smart_refctd_ptr tmpAST; + // for making IR nodes before we Merkle Hash them and remove duplicates (so main IR doesn't get bloated) + core::smart_refctd_ptr tmpIR; + // changes dynamically + const CFrontendIR* srcAST; + // for going over layers in the AST + core::vector layerStack; + // Some of the things we must canonicalize: + // A ( f_0 (B + C) + D f_1 ) = f_0 B A + f_0 C A + f_1 D A + // Expression nodes of the Frontend AST really come in 4 variants: + // - add + // - mul + // - complement, which is equivalent to 1 ADD (-1 MUL x) + // - function/other + // BRDFs can appear only under ADD and MUL nodes in the AST not the function/other/complement, so if we want to canonicalize: + // 1. The Add above can be ignored, we form full multiplication chain to the top + // 2. Adds in sibling nodes (below the last add) cause us to have to add a factored copy to the IR + // DFS from right-to-left (inverse order of adding children to stack), would cause us to keep postifxes of the multiplier chain every time we descend into ADD. + // We want to essentially visit the parent ADD node again after dealing with its subtree (in-order traversal) then mul chain can be reset just to the parent. + // If we perform DFS stack push left-to-right, we'll know the contributor already for all the leaf nodes if we push it onto the stack. + // Then for all other leaf nodes we can accumulate them in the MUL chain, and adding their weighted contributor whenever we're back at an ADD node (be it the ancestor or sibling/cousin). + // If the contributor is null or multiplied with a null we can keep draining the stack until we're back at its immediate parent ADD node. + struct SContributor + { + // the "active" contributor, basically the leftmost item in the subbranch below and ADD + CTrueIR::typed_pointer_type contributor; + }; + core::vector contributorStack; + // Every time we encounter an AST leaf we must add the current contributor together with all the factors multiplied together + struct SFactor + { + using handle_t = CTrueIR::typed_pointer_type; + // We only track multiplicative factors, we break down every BRDF equally into the canonical form + handle_t handle; + uint8_t negate : 1 = false; + uint8_t monochrome : 1 = true; + // extend later when allowing variable bucket count + uint8_t liveSpectralChannels : 3 = 0b111; + }; + // here we keep the multiplication chain unsorted so its each to add/remove nodes as we encounter them + core::vector mulChain; + // scratch for sorting the mul chain before adding a contributor + core::vector mulChainSortScratch; + // By maintaining a hash map of AST nodes which simplify to a Constant (unity, or zero, or other) we could resolve the issue of the `nonMulImmediateAncestorStackEnd` + // which has us adding the same non-mul node multiple times to stack during the traversal. + // However how much of that would be moving IR manipulation into the AST ? + struct StackEntry + { + inline bool notVisited() const {return !visited;} + + const CFrontendIR::IExprNode* node; + // the ancestor ADD node to go back to if we hit a 0 MUL, or if our ADD or any other node becomes 0 + uint16_t nonMulImmediateAncestorStackEnd = 0; + // the length of the `mulChain` at the time we first visited the node + uint16_t mulChainLen = 0; + bool visited = false; + // only relevant for Add nodes + bool addContributor = false; + }; + core::vector exprStack; + }; + NBL_API2 CTrueIR::SMaterialHandle makeFinalIR(const typed_pointer_type rootH, SAdd2IRSession& session) const; inline core::string getNodeID(const typed_pointer_type handle) const {return core::string("_")+std::to_string(handle.value);} inline core::string getLabelledNodeID(const typed_pointer_type handle) const @@ -992,8 +1106,6 @@ class CFrontendIR final : public CNodePool retval += "\"]"; return retval; } - - core::vector> m_rootNodes; }; inline bool CFrontendIR::valid(const typed_pointer_type rootHandle, system::logger_opt_ptr logger) const @@ -1017,10 +1129,9 @@ inline bool CFrontendIR::valid(const typed_pointer_type rootHandle core::stack exprStack; // why a separate stack to the main one? Because we don't push siblings. core::vector> ancestorPrefix; - // unused yet + // TODO: unused yet core::unordered_set> visitedNodes; - // should probably size it better, if I knew total node count allocated or live - visitedNodes.reserve(m_rootNodes.size()<<3); + visitedNodes.reserve(128); // auto validateExpression = [&](const typed_pointer_type exprRoot, const bool isBTDF) -> bool { diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 5bad48f2c6..0e0e2cbdfa 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -1,11 +1,15 @@ -// Copyright (C) 2018-2025 - DevSH Graphics Programming Sp. z O.O. +// Copyright (C) 2018-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_TRUE_IR_H_INCLUDED_ #define _NBL_ASSET_MATERIAL_COMPILER_V3_C_TRUE_IR_H_INCLUDED_ -#include "nbl/asset/material_compiler3/CFrontendIR.h" +#include "nbl/system/ILogger.h" + +#include "nbl/asset/material_compiler3/CNodePool.h" +#include "nbl/asset/format/EColorSpace.h" +#include "nbl/asset/ICPUImageView.h" namespace nbl::asset::material_compiler3 @@ -306,6 +310,10 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // you can set the children later inline CConstant() = default; }; +#undef TYPE_NAME_STR + + // +// inline typed_pointer_type createConstant() // Each material comes down to this, this is the only struct we don't de-duplicate @@ -373,30 +381,25 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // create the `BlackholeMaterialHandle` m_materials = {SMaterial{.debugInfo=getObjectPool().emplace("CTrueIR's BlackHole Material")}}; } - - // Returns indices into `this->getMaterials()` for every `forest->getMaterials()` - // We take the trees from the forest, and canonicalize them into our weird Domain Specific IR with Upside down expression trees. - // Process: - // 1. Decompression (duplicating nodes, etc.) - // 2. Canonicalize Expressions (Transform into Sum-Product form, DCE, etc.) - // 3. Split BTDFs (front vs. back part), reciprocate Etas - // 4. Simplify and Hoist Layer terms (delta sampling property) - // 5. Subexpression elimination + + // TODO: Optimization passes on the IR // It is the backend's job to handle: // - constant encoding precision (scale factors, UV matrices, IoRs) // - multiscatter compensation // - compilation failure to unsupported complex layering // - compilation failure to unsupported complex layering - struct SAddMaterialsArgs + SMaterialHandle addMaterial(SMaterial material, CTrueIR* srcIR=nullptr) { - explicit inline operator bool() const {return forest;} - - const CFrontendIR* forest; - system::logger_opt_ptr logger; - - }; - NBL_API2 core::vector addMaterials(const SAddMaterialsArgs& args); - + if (!srcIR) + srcIR = this; + if (rewrite(material,srcIR)) + { + m_materials.push_back(material); + return {.value=static_cast(m_materials.size()-1)}; + } + else + return {}; + } // For Debug Visualization struct SDotPrinter final @@ -444,81 +447,9 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! protected: NBL_API2 CTrueIR(creation_params_type&& params); - - struct SAddSession - { - inline SAddSession(const SAddMaterialsArgs& _args) : args(_args) - { - tmpAST = CFrontendIR::create({.composed={.blockSizeKBLog2=10},.maxBlocks=64}); - // give slightly more memory to IR, since the AST tends to be a bit more compact - tmpIR = CTrueIR::create({.composed={.blockSizeKBLog2=12},.maxBlocks=64}); - } - NBL_API2 SMaterial::SOriented makeOrientedMaterial(const CFrontendIR::typed_pointer_type rootH); - - NBL_API2 typed_pointer_type makeContributors(const CFrontendIR::typed_pointer_type bxdfRootH); - - // inputs to the addMaterials function - const SAddMaterialsArgs& args; - // for rewriting AST expressions - core::smart_refctd_ptr tmpAST; - // for making IR nodes before we Merkle Hash them and remove duplicates (so main IR doesn't get bloated) - core::smart_refctd_ptr tmpIR; - // for going over layers in the AST - core::vector layerStack; - // Some of the things we must canonicalize: - // A ( f_0 (B + C) + D f_1 ) = f_0 B A + f_0 C A + f_1 D A - // Expression nodes of the Frontend AST really come in 4 variants: - // - add - // - mul - // - complement, which is equivalent to 1 ADD (-1 MUL x) - // - function/other - // BRDFs can appear only under ADD and MUL nodes in the AST not the function/other/complement, so if we want to canonicalize: - // 1. The Add above can be ignored, we form full multiplication chain to the top - // 2. Adds in sibling nodes (below the last add) cause us to have to add a factored copy to the IR - // DFS from right-to-left (inverse order of adding children to stack), would cause us to keep postifxes of the multiplier chain every time we descend into ADD. - // We want to essentially visit the parent ADD node again after dealing with its subtree (in-order traversal) then mul chain can be reset just to the parent. - // If we perform DFS stack push left-to-right, we'll know the contributor already for all the leaf nodes if we push it onto the stack. - // Then for all other leaf nodes we can accumulate them in the MUL chain, and adding their weighted contributor whenever we're back at an ADD node (be it the ancestor or sibling/cousin). - // If the contributor is null or multiplied with a null we can keep draining the stack until we're back at its immediate parent ADD node. - struct SContributor - { - // the "active" contributor, basically the leftmost item in the subbranch below and ADD - typed_pointer_type contributor; - }; - core::vector contributorStack; - // Every time we encounter an AST leaf we must add the current contributor together with all the factors multiplied together - struct SFactor - { - // We only track multiplicative factors, we break down every BRDF equally into the canonical form - typed_pointer_type handle; - uint8_t negate : 1 = false; - uint8_t monochrome : 1 = true; - // extend later when allowing variable bucket count - uint8_t liveSpectralChannels : 3 = 0b111; - }; - // here we keep the multiplication chain unsorted so its each to add/remove nodes as we encounter them - core::vector mulChain; - // scratch for sorting the mul chain before adding a contributor - core::vector mulChainSortScratch; - // By maintaining a hash map of AST nodes which simplify to a Constant (unity, or zero, or other) we could resolve the issue of the `nonMulImmediateAncestorStackEnd` - // which has us adding the same non-mul node multiple times to stack during the traversal. - // However how much of that would be moving IR manipulation into the AST ? - struct StackEntry - { - inline bool notVisited() const {return !visited;} - - const CFrontendIR::IExprNode* node; - // the ancestor ADD node to go back to if we hit a 0 MUL, or if our ADD or any other node becomes 0 - uint16_t nonMulImmediateAncestorStackEnd = 0; - // the length of the `mulChain` at the time we first visited the node - uint16_t mulChainLen = 0; - bool visited = false; - // only relevant for Add nodes - bool addContributor = false; - }; - core::vector exprStack; - }; + // copies from other IR into ours and makes sure things get hashed properly + NBL_API2 bool rewrite(SMaterial& material, CTrueIR* srcIR); core::vector m_materials; core::unordered_map> m_uniqueNodes; diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index fcfaa388f4..e54616a43d 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -11,6 +11,7 @@ namespace nbl::asset::material_compiler3 { constexpr auto ELL_ERROR = nbl::system::ILogger::E_LOG_LEVEL::ELL_ERROR; +constexpr auto ELL_DEBUG = nbl::system::ILogger::E_LOG_LEVEL::ELL_DEBUG; using namespace nbl::system; bool CFrontendIR::CEmitter::invalid(const SInvalidCheckArgs& args) const @@ -582,4 +583,285 @@ void CFrontendIR::CCookTorrance::printDot(std::ostringstream& sstr, const core:: ndParams.printDot(sstr,selfID); } +//! AST-> IR methods +auto CFrontendIR::CEmitter::createIRNode(const CFrontendIR* ast, CTrueIR::obj_pool_type& pool) const -> ir_contributor_handle_t +{ + assert(false); // unimplemented + return {}; +} + +auto CFrontendIR::CDeltaTransmission::createIRNode(const CFrontendIR* ast, CTrueIR::obj_pool_type& pool) const -> ir_contributor_handle_t +{ + assert(false); // unimplemented + return {}; +} + +auto CFrontendIR::COrenNayar::createIRNode(const CFrontendIR* ast, CTrueIR::obj_pool_type& pool) const -> ir_contributor_handle_t +{ + assert(false); // unimplemented + return {}; +} + +auto CFrontendIR::CCookTorrance::createIRNode(const CFrontendIR* ast, CTrueIR::obj_pool_type& pool) const -> ir_contributor_handle_t +{ + assert(false); // unimplemented + return {}; +} + +auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_pointer_type rootH, const CFrontendIR* _srcAST) -> oriented_material_t +{ + oriented_material_t retval = {}; + + srcAST = _srcAST; + const auto& astPool = srcAST->getObjectPool(); + assert(layerStack.empty()); + + auto& irPool = tmpIR->getObjectPool(); + // go down through layers and create all the dependencies + for (const auto* layer=astPool.deref(rootH); layer; layer=astPool.deref(layer->coated)) + { + // TODO: actually re-check the expressions for being null after optimization + bool noTopReflection = !layer->brdfTop; + bool noTransmission = !layer->btdf; + // if there's literally nothing on the top level, you can't get to the next layer to retroreflect from it + if (noTopReflection && noTransmission) + { + args.logger.log("Skipping current layer and farther ones due to no transmission and reflection",ELL_DEBUG); + break; + } + layerStack.push_back(layer); + // find out rest of the layers don't matter because they're blocked from being seen, its not a complete check + if (noTransmission) + { + args.logger.log("Skipping remaining layers due to no transmission",ELL_DEBUG); + break; + } +// TODO: handle the rest of this stuff +//assert(false); + // Only if we're not in the last layer do we care about the bottom BRDF (you can't hit it otherwise) + // Note that this won't catch the next layer being a blackhole and needs to be undone if it is + if (layer->coated && layer->brdfBottom) + { + // do stuff with brdfBottom + } + } + if (!layerStack.empty()) + retval.metadata |= CTrueIR::SMaterial::EMetadataBits::NotBlackhole; + // then go back up and make the layers + while (!layerStack.empty()) + { + const auto* const inLayer = layerStack.back(); + layerStack.pop_back(); + // allocate a layer + const auto layerH = irPool.emplace(); + auto* const outLayer = irPool.deref(layerH); + retval.root = layerH; + // process the BTDF + //... + // process the top BRDF + outLayer->brdfTop = makeContributors(inLayer->brdfTop); + // if BTDF has delta transmissions, then via the sampling property hoist next layer into current layer BRDFs with the DeltaTransmission weights applied + // hmm this would require decorrellation... because don't want rest of BTDF to affect + //... + } + // skip replace delta transmissions by the layer undernearth, if null then keep as delta + + // AST is Sum Expression to the BRDF nodes + // We need to keep the Ancestor prefix as an unrolled linked list + return retval; +} + +// +auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_pointer_type bxdfRootH) -> CTrueIR::typed_pointer_type +{ + // debug what we're processing + auto printSubtree = [&](const CFrontendIR::typed_pointer_type nodeH)->void + { + SDotPrinter printer(srcAST); + printer.exprStack.push(nodeH); + args.logger.log("Subtree Dot3 : \n%s\n", ELL_DEBUG,printer().c_str()); + }; + + using contributor_sum_handle_t = CTrueIR::typed_pointer_type; + contributor_sum_handle_t headH = {}; + if (!bxdfRootH) + return headH; + printSubtree(bxdfRootH); + + // Multiplication Chain need to be sorted in a canonical order so its easier to spot them being the same + auto sortMuls = [](const SFactor& lhs, const SFactor& rhs)->bool + { + // monochrome is cheaper + if (lhs.monochrome!=rhs.monochrome) + return lhs.monochrome; + // not doing a complement is cheaper + if (lhs.handle.value==rhs.handle.value) + return lhs.negategetObjectPool(); + auto& irPool = tmpIR->getObjectPool(); + // scratches are initialized + assert(mulChain.empty()); + assert(contributorStack.empty()); + exprStack.push_back({.node=astPool.deref(bxdfRootH)}); + contributor_sum_handle_t tailH = {}; + while (!exprStack.empty()) + { + // how to report an error +#if 0 + { + args.logger.log("MESSAGE",ELL_ERROR); + exprStack.clear(); + mulChain.clear(); + contributorStack.clear(); + return tmpIR->getBasicNodes().errorBxDF; + } +#endif + auto& entry = exprStack.back(); + using ast_expr_type_e = CFrontendIR::IExprNode::Type; + const ast_expr_type_e astExprType = entry.node->getType(); + const bool isContributor = astExprType==CFrontendIR::IExprNode::Type::Contributor; + // + if (entry.notVisited()) + { + if (isContributor) + { + contributorStack.push_back({.contributor=static_cast(entry.node)->createIRNode(srcAST,irPool)}); + exprStack.pop_back(); + } + else + { + const bool isAdd = astExprType==ast_expr_type_e::Add; + if (isAdd) + { + entry.addContributor = true; + // Current Add node will perform the job of the parent add node for this subtree + if (entry.nonMulImmediateAncestorStackEnd) + exprStack[entry.nonMulImmediateAncestorStackEnd-1].addContributor = false; + } + const bool notMul = astExprType!=ast_expr_type_e::Mul; + // go through children + const auto childCount = entry.node->getChildCount(); + // add in reverse so stack processes in order + for (auto childIx=childCount; childIx; ) + { + // making sure we visit this node again each time a subtree of an Add node is done + if (isAdd && childIx!=childCount) + { + auto& extraEntry = exprStack.emplace_back(entry); + extraEntry.visited = true; + extraEntry.addContributor = true; + } + // regular exploration + exprStack.push_back({ + .node = astPool.deref(entry.node->getChildHandle(--childIx)), + // to be able to go back to the non mul that is supposed to add our subtree + .nonMulImmediateAncestorStackEnd = notMul ? static_cast(exprStack.size()):entry.nonMulImmediateAncestorStackEnd + }); + } + } + entry.visited = true; + } + else + { + assert(!isContributor); + // do stuff now + switch (astExprType) + { + case ast_expr_type_e::Add: + { + // we visited the leftmost subtrees first so this is the right order + { + auto* const tail = irPool.deref(tailH); + tailH = irPool.emplace(); + if (tailH) + tail->rest = tailH; + else + headH = tailH; + } + // add current contributor with weight to BxDF Sum + { + const auto weightedH = irPool.emplace(); + irPool.deref(tailH)->product = weightedH; + auto* weighted = irPool.deref(weightedH); + weighted->contributor = contributorStack.back().contributor; + if (!mulChain.empty()) + { + const CTrueIR::CFactorCombiner::SState combinerState = { + .type = CTrueIR::CFactorCombiner::Type::Mul, + .childCount = mulChain.size() + }; + // TODO: create the combiner node + //const auto factorH = getObjectPool().emplace(); + { + // every contributor node gets its own SORTED ancestor prefix + mulChainSortScratch = mulChain; + std::sort(mulChainSortScratch.begin(),mulChainSortScratch.end(),sortMuls); + //auto oit = getObjectPool().deref(factorH)->child; + //for (const auto& mul : mulChainSortScratch) + //*(oit++) = mul.handle; + } + //weighted->factor = factorH; + } + } + // when we are done we need to reset the mul chain back to its original state + mulChain.resize(entry.mulChainLen); + break; + } + default: + break; + } + exprStack.pop_back(); + } + } + // There was never an ADD node + if (!contributorStack.empty()) + { + // only one contributor could have been encountered + assert(contributorStack.size()==1); + // TODO: add the contributor + mulChain.clear(); + contributorStack.clear(); + } + // we got all the AST ADD nodes on the way back out + assert(mulChain.empty()); + return headH; +} + +// +CTrueIR::SMaterialHandle CFrontendIR::makeFinalIR(const typed_pointer_type rootH, SAdd2IRSession& session) const +{ + const auto& astPool = getObjectPool(); + +// core::unordered_map brdfs; +// core::unordered_map btdfs; + + const auto* astRoot = astPool.deref(rootH); + // no material + if (!astRoot) + return CTrueIR::BlackholeMaterialHandle; + session.tmpIR->reset(); + // reverse AST into another tree + session.tmpAST->reset(); + const auto backRootH = session.tmpAST->reverse(rootH,this); + CTrueIR::SMaterial material = { + .front = session.makeOrientedMaterial(rootH,this), + .back = session.makeOrientedMaterial(backRootH,session.tmpAST.get()) + }; + + auto retval = session.args.ir->addMaterial(material); + if (retval) + { + // TODO: better debug info + if (const auto* debug=astPool.deref(astRoot->debugInfo); debug && !debug->data().empty()) + { + material.debugInfo = session.args.ir->getObjectPool().emplace(debug->data().data(),debug->data().size()); + } + } + return retval; +} + } \ No newline at end of file diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp index 9ea37e219b..3a4a06df3f 100644 --- a/src/nbl/asset/material_compiler3/CTrueIR.cpp +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -84,299 +84,10 @@ CTrueIR::SBasicNodes::SBasicNodes(CTrueIR* ir) } } - -auto CTrueIR::SAddSession::makeOrientedMaterial(const CFrontendIR::typed_pointer_type rootH) -> SMaterial::SOriented +bool CTrueIR::rewrite(SMaterial& material, CTrueIR* srcIR) { - SMaterial::SOriented retval = {}; - assert(layerStack.empty()); - - auto& astPool = args.forest->getObjectPool(); - auto& irPool = tmpIR->getObjectPool(); - // go down through layers and create all the dependencies - for (const auto* layer=astPool.deref(rootH); layer; layer=astPool.deref(layer->coated)) - { - // TODO: actually re-check the expressions for being null after optimization - bool noTopReflection = !layer->brdfTop; - bool noTransmission = !layer->btdf; - // if there's literally nothing on the top level, you can't get to the next layer to retroreflect from it - if (noTopReflection && noTransmission) - { - args.logger.log("Skipping current layer and farther ones due to no transmission and reflection",ELL_DEBUG); - break; - } - layerStack.push_back(layer); - // find out rest of the layers don't matter because they're blocked from being seen, its not a complete check - if (noTransmission) - { - args.logger.log("Skipping remaining layers due to no transmission",ELL_DEBUG); - break; - } -// TODO: handle the rest of this stuff -//assert(false); - // Only if we're not in the last layer do we care about the bottom BRDF (you can't hit it otherwise) - // Note that this won't catch the next layer being a blackhole and needs to be undone if it is - if (layer->coated && layer->brdfBottom) - { - // do stuff with brdfBottom - } - } - if (!layerStack.empty()) - retval.metadata |= SMaterial::EMetadataBits::NotBlackhole; - // then go back up and make the layers - while (!layerStack.empty()) - { - const auto* const inLayer = layerStack.back(); - layerStack.pop_back(); - // allocate a layer - const auto layerH = irPool.emplace(); - auto* const outLayer = irPool.deref(layerH); - retval.root = layerH; - // process the BTDF - //... - // process the top BRDF - outLayer->brdfTop = makeContributors(inLayer->brdfTop); - // if BTDF has delta transmissions, then via the sampling property hoist next layer into current layer BRDFs with the DeltaTransmission weights applied - // hmm this would require decorrellation... because don't want rest of BTDF to affect - //... - } - // skip replace delta transmissions by the layer undernearth, if null then keep as delta - - // AST is Sum Expression to the BRDF nodes - // We need to keep the Ancestor prefix as an unrolled linked list - return retval; -} - -// -auto CTrueIR::SAddSession::makeContributors(const CFrontendIR::typed_pointer_type bxdfRootH) -> typed_pointer_type -{ - // debug what we're processing - auto printSubtree = [&](const CFrontendIR::typed_pointer_type nodeH)->void - { - CFrontendIR::SDotPrinter printer(args.forest); - printer.exprStack.push(nodeH); - args.logger.log("Subtree Dot3 : \n%s\n", ELL_DEBUG,printer().c_str()); - }; - typed_pointer_type headH = {}; - if (!bxdfRootH) - return headH; - printSubtree(bxdfRootH); - - // Multiplication Chain need to be sorted in a canonical order so its easier to spot them being the same - auto sortMuls = [](const SAddSession::SFactor& lhs, const SAddSession::SFactor& rhs)->bool - { - // monochrome is cheaper - if (lhs.monochrome!=rhs.monochrome) - return lhs.monochrome; - // not doing a complement is cheaper - if (lhs.handle.value==rhs.handle.value) - return lhs.negategetObjectPool(); - auto& irPool = tmpIR->getObjectPool(); - // scratches are initialized - assert(mulChain.empty()); - assert(contributorStack.empty()); - exprStack.push_back({.node=astPool.deref(bxdfRootH)}); - typed_pointer_type tailH = {}; - while (!exprStack.empty()) - { - auto& entry = exprStack.back(); - using ast_expr_type_e = CFrontendIR::IExprNode::Type; - const ast_expr_type_e astExprType = entry.node->getType(); - const bool isContributor = astExprType==CFrontendIR::IExprNode::Type::Contributor; - // - if (entry.notVisited()) - { - if (isContributor) - { - // TODO actually make the contributor - const auto contributorType = 0; - switch (contributorType) - { - case 45: - { - // TODO: add to contributorStack - contributorStack.push_back({.contributor={}}); - break; - } - // unsupported contributor - default: - { - args.logger.log("Unsupported contributor type %d skipping subtree",ELL_ERROR,contributorType); - exprStack.clear(); - mulChain.clear(); - contributorStack.clear(); - return tmpIR->getBasicNodes().errorBxDF; - } - } - // dont want to deal with the contributor again - exprStack.pop_back(); - } - else - { -#if 0 // TODO: Other factors - // spot prefix being null/zero to stop exploring - // can make the decision wholly on current factor - bool continueExploring = true; - if (continueExploring) - { - // make the actual factor - auto derivedFactorH = getObjectPool().emplace(); - // TODO: find the place where to insert it - { - // shtuff - } - entry.factor = derivedFactorH; - // add self after making self - ancestors.push_back(getObjectPool().deref(entry.factor)); - // TODO: push the children nodes onto the stack - continue; - } -#endif - const bool isAdd = astExprType==ast_expr_type_e::Add; - if (isAdd) - { - entry.addContributor = true; - // Current Add node will perform the job of the parent add node for this subtree - if (entry.nonMulImmediateAncestorStackEnd) - exprStack[entry.nonMulImmediateAncestorStackEnd-1].addContributor = false; - } - const bool notMul = astExprType!=ast_expr_type_e::Mul; - // go through children - const auto childCount = entry.node->getChildCount(); - // add in reverse so stack processes in order - for (auto childIx=childCount; childIx; ) - { - // making sure we visit this node again each time a subtree of an Add node is done - if (isAdd && childIx!=childCount) - { - auto& extraEntry = exprStack.emplace_back(entry); - extraEntry.visited = true; - extraEntry.addContributor = true; - } - // regular exploration - exprStack.push_back({ - .node = astPool.deref(entry.node->getChildHandle(--childIx)), - // to be able to go back to the non mul that is supposed to add our subtree - .nonMulImmediateAncestorStackEnd = notMul ? static_cast(exprStack.size()):entry.nonMulImmediateAncestorStackEnd - }); - } - } - entry.visited = true; - } - else - { - assert(!isContributor); - // do stuff now - switch (astExprType) - { - case ast_expr_type_e::Add: - { - // we visited the leftmost subtrees first so this is the right order - { - auto* const tail = irPool.deref(tailH); - tailH = irPool.emplace(); - if (tailH) - tail->rest = tailH; - else - headH = tailH; - } - // add current contributor with weight to BxDF Sum - { - const auto weightedH = irPool.emplace(); - irPool.deref(tailH)->product = weightedH; - auto* weighted = irPool.deref(weightedH); - weighted->contributor = contributorStack.back().contributor; - if (!mulChain.empty()) - { - const CFactorCombiner::SState combinerState = { - .type = CFactorCombiner::Type::Mul, - .childCount = mulChain.size() - }; - // TODO: create the combiner node - //const auto factorH = getObjectPool().emplace(); - { - // every contributor node gets its own SORTED ancestor prefix - mulChainSortScratch = mulChain; - std::sort(mulChainSortScratch.begin(),mulChainSortScratch.end(),sortMuls); - //auto oit = getObjectPool().deref(factorH)->child; - //for (const auto& mul : mulChainSortScratch) - //*(oit++) = mul.handle; - } - //weighted->factor = factorH; - } - } - // when we are done we need to reset the mul chain back to its original state - mulChain.resize(entry.mulChainLen); - break; - } - default: - break; - } - exprStack.pop_back(); - } - } - // we got all the AST ADD nodes on the way back out - assert(mulChain.empty()); - assert(contributorStack.empty()); - return headH; -} - -// -auto CTrueIR::addMaterials(const SAddMaterialsArgs& args) -> core::vector -{ - const auto logger = args.logger; - if (!args) - { - logger.log("Invalid Arguments to `CTrueIR::addMaterials`",ELL_ERROR); - return {}; - } - const auto& astPool = args.forest->getObjectPool(); - - -// core::unordered_map brdfs; -// core::unordered_map btdfs; - - SAddSession session = {args}; - const auto inputMaterials = args.forest->getMaterials(); - core::vector retval(inputMaterials.size(),{}); - auto outIt = retval.begin(); - for (const auto& rootH : inputMaterials) - { - auto& result = *(outIt++); - - const auto* astRoot = astPool.deref(rootH); - // no material - if (!astRoot) - { - result = BlackholeMaterialHandle; - continue; - } - session.tmpIR->reset(); - // reverse AST into another tree - session.tmpAST->reset(); - const auto backRootH = session.tmpAST->reverse(rootH,args.forest); - // TODO: hash, deduplicate, collect metadata and insert into current IR - SMaterial material = { - .front = session.makeOrientedMaterial(rootH), - .back = session.makeOrientedMaterial(backRootH) - }; - - // TODO: better debug info - if (const auto* debug=astPool.deref(astRoot->debugInfo); debug && !debug->data().empty()) - { - material.debugInfo = getObjectPool().emplace(debug->data().data(),debug->data().size()); - } - - // - result.value = m_materials.size(); - m_materials.push_back(material); - } - - return retval; + // TODO: hash, deduplicate, collect metadata and insert into current IR + return true; } From e46724d7082489034d40582e82a0a1ca797d8e08 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Tue, 28 Apr 2026 22:17:15 +0200 Subject: [PATCH 10/45] Move `CFrontendIR::SParameterSet` and `CFrontendIR::SBasicNDFParams` to common base class of `CNodePool` --- examples_tests | 2 +- .../asset/material_compiler3/CFrontendIR.h | 159 +---------- .../nbl/asset/material_compiler3/CNodePool.h | 268 ++++++++++++++++++ .../nbl/asset/material_compiler3/CTrueIR.h | 69 ++++- .../asset/material_compiler3/CFrontendIR.cpp | 63 +--- 5 files changed, 336 insertions(+), 225 deletions(-) diff --git a/examples_tests b/examples_tests index eebde787c2..9b000e150c 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit eebde787c233367ade8eb0580bc79c0d562e97aa +Subproject commit 9b000e150c98e902b57c4a4be240a047490eddf4 diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index 771f323483..593fd1babf 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -83,116 +83,6 @@ class CFrontendIR final : public CNodePool return nullptr; return core::smart_refctd_ptr(new CFrontendIR(std::move(params)),core::dont_grab); } - template - static inline void printMatrix(std::ostringstream& sstr, const hlsl::matrix& m) - { - for (uint16_t i=0; i::infinity() && (!view || viewChannelgetCreationParameters().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::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 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 - struct SParameterSet - { - private: - friend class CSpectralVariable; - template - 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(params+_count)); - sstr << "\"]"; - sstr << "\n\t" << selfID << " -> " << uvTransformID << "[label=\"UV Transform\"]"; - } - } - - public: - inline operator bool() const - { - for (uint8_t i=0; i0); - - template - inline void printDot(std::ostringstream& sstr, const core::string& selfID, StringConstIterator paramNameBegin={}, const bool uvRequired=false) const - { - printDot(Count,sstr,selfID,std::forward(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 @@ -325,7 +215,7 @@ class CFrontendIR final : public CNodePool protected: friend class CFrontendIR; using ir_contributor_handle_t = CTrueIR::typed_pointer_type; - virtual ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR::obj_pool_type& pool) const = 0; + virtual ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR* ir) const = 0; }; // This node could also represent non directional emission, but we have another node for that @@ -568,7 +458,7 @@ class CFrontendIR final : public CNodePool NBL_API2 void printDot(std::ostringstream& sstr, const core::string& selfID) const override; - NBL_API2 ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR::obj_pool_type& pool) const; + NBL_API2 ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR* ir) const; }; //! 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. @@ -713,44 +603,7 @@ class CFrontendIR final : public CNodePool 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(params,2);} - inline auto getDerivMap() const {return std::span(params,2);} - inline auto getRougness() {return std::span(params+2,2);} - inline auto getRougness() const {return std::span(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::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); - }; + // ? }; // 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 @@ -777,7 +630,7 @@ class CFrontendIR final : public CNodePool protected: COPY_DEFAULT_IMPL - NBL_API2 ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR::obj_pool_type& pool) const; + NBL_API2 ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR* ir) const; }; //! 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 @@ -797,7 +650,7 @@ class CFrontendIR final : public CNodePool NBL_API2 void printDot(std::ostringstream& sstr, const core::string& selfID) const override; - NBL_API2 ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR::obj_pool_type& pool) const; + NBL_API2 ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR* ir) const; }; // Supports anisotropy for all models class CCookTorrance final : public IBxDF @@ -843,7 +696,7 @@ class CFrontendIR final : public CNodePool 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; - NBL_API2 ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR::obj_pool_type& pool) const; + NBL_API2 ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR* ir) const; }; #undef COPY_DEFAULT_IMPL #undef TYPE_NAME_STR diff --git a/include/nbl/asset/material_compiler3/CNodePool.h b/include/nbl/asset/material_compiler3/CNodePool.h index 59d6b84529..43d98a089f 100644 --- a/include/nbl/asset/material_compiler3/CNodePool.h +++ b/include/nbl/asset/material_compiler3/CNodePool.h @@ -26,6 +26,22 @@ class CNodePool : public core::IReferenceCounted }; public: + template + static inline void printMatrix(std::ostringstream& sstr, const hlsl::matrix& m) + { + for (uint16_t i=0; i; @@ -33,6 +49,73 @@ class CNodePool : public core::IReferenceCounted inline obj_pool_type& getObjectPool() {return m_composed;} inline const obj_pool_type& getObjectPool() const {return m_composed;} + // + struct SParameter + { + inline operator bool() const + { + return abs(scale)::infinity() && (!view || viewChannelgetCreationParameters().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);} + + 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::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 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 + struct SParameterSet + { + inline operator bool() const + { + for (uint8_t i=0; i0); + + template + inline void printDot(std::ostringstream& sstr, const core::string& selfID, StringConstIterator paramNameBegin={}, const bool uvRequired=false) const + { + CNodePool::printDotParameterSet(*this,Count,sstr,selfID,std::forward(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)); + }; + // class INode { @@ -80,6 +163,80 @@ class CNodePool : public core::IReferenceCounted const uint32_t m_size; }; + // 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(params,2);} + inline auto getDerivMap() const {return std::span(params,2);} + inline auto getRougness() {return std::span(params+2,2);} + inline auto getRougness() const {return std::span(params+2,2);} + + inline SBasicNDFParams() + { + // initialize with constant flat deriv map and smooth roughness + for (auto& param : params) + param.scale = 0.f; + } + + // The usage of a normal modifier implies potential anisotropic roughness when filtering (CLEAR, CLEAN, Neural), so all 4 (or 5) parameters should come from a texture. + // When normal modifier is not used, the roughness can still come from a texture but can be isotropic or anisotropic. Weird combos will require making tiny textures when converting from AST. + enum class EParamType : uint8_t + { + TotallyMapped, + AnisotropicMapped, + IsotropicMapped, + AnisotropicConstant, + IsotropicConstant + }; + // This is about how we load our data into the NDF not whether the NDF is really isotropic + inline EParamType determineParamType() 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 EParamType::TotallyMapped; + const auto roughness = getRougness(); + // having one roughness be mapped and another not mapped, isn't very useful in any renderer + const bool roughnessIsMapped = roughness[0].scale!=0.f && roughness[0].view || roughness[1].scale!=0.f && roughness[1].view; + // if roughness inputs are not equal (same scale, same texture) then NDF can be anisotropic in places + if (roughness[0]!=roughness[1]) + { + return roughnessIsMapped ? EParamType::AnisotropicMapped:EParamType::AnisotropicConstant; + } + else if (roughnessIsMapped) + { + return EParamType::IsotropicMapped; + } + else + return EParamType::IsotropicConstant; + } + + // conservative check, checks if we can optimize certain things this way + inline bool definitelyIsotropic() const + { + switch (determineParamType()) + { + case EParamType::IsotropicMapped: [[fallthrough]]; + case EParamType::IsotropicConstant: + break; + default: + 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::min());} + + 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); + }; + + // template inline const std::string_view getTypeName(const typed_pointer_type h) const { @@ -91,8 +248,119 @@ class CNodePool : public core::IReferenceCounted protected: inline CNodePool(typename obj_pool_type::creation_params_type&& params) : m_composed(std::move(params)) {} + template + friend struct SParameterSet; + // Use `_count` instead of `Count` because of how wonkily this stuff gets used + template + static inline void printDotParameterSet(const SParameterSet& _set, const uint8_t _count, std::ostringstream& sstr, const core::string& selfID, StringConstIterator paramNameBegin={}, const bool uvRequired=false) + { + bool imageUsed = false; + for (uint8_t i=0; i<_count; i++) + { + const auto paramID = selfID+"_param"+std::to_string(i); + if (_set.params[i].view) + imageUsed = true; + _set.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(_set.uvSlot()) << "\\n"; + printMatrix(sstr,*reinterpret_cast(_set.params+_count)); + sstr << "\"]"; + sstr << "\n\t" << selfID << " -> " << uvTransformID << "[label=\"UV Transform\"]"; + } + } + obj_pool_type m_composed; }; +inline void CNodePool::SParameter::printDot(std::ostringstream& sstr, const core::string& selfID) const +{ + sstr << "\n\t" << selfID << "[label=\"scale = " << std::to_string(scale); + if (view) + { + sstr << "\\nchannel = " << std::to_string(viewChannel); + const auto& viewParams = view->getCreationParameters(); + sstr << "\\nWraps = {" << sampler.TextureWrapU; + if (viewParams.viewType!=ICPUImageView::ET_1D && viewParams.viewType!=ICPUImageView::ET_1D_ARRAY) + sstr << "," << sampler.TextureWrapV; + if (viewParams.viewType==ICPUImageView::ET_3D) + sstr << "," << sampler.TextureWrapW; + sstr << "}\\nBorder = " << sampler.BorderColor; + // don't bother printing the rest, we really don't care much about those + } + sstr << "\"]"; + // TODO: do specialized printing for image views (they need to be gathered into a view set -> need a printing context struct) + /* + struct SDotPrintContext + { + std::ostringstream* sstr; + core::unordered_map* usedViews; + uint16_t indentation = 0; + }; + */ + if (view) + sstr << "\n\t" << selfID << " -> _view_" << std::to_string(reinterpret_cast(view)); +} + +inline void CNodePool::SBasicNDFParams::printDot(std::ostringstream& sstr, const core::string& selfID) const +{ + constexpr const char* paramSemantics[] = { + "dh/du", + "dh/dv", + "alpha_u", + "alpha_v" + }; + SParameterSet<4>::printDot(sstr,selfID,paramSemantics,!definitelyIsotropic()); + if (!stretchInvariant()) + { + const auto referenceID = selfID+"_reference"; + sstr << "\n\t" << referenceID << " [label=\""; + printMatrix(sstr,reference); + sstr << "\"]"; + sstr << "\n\t" << selfID << " -> " << referenceID << " [label=\"Stretch Reference\"]"; + } +} + +#if 0 // TODO +template +struct core::blake3_hasher::update_impl +{ + using input_t = asset::material_compiler3::CNodePool::SBasicNDFParams; + + static inline void __call(blake3_hasher& hasher, const input_t& input) + { + using type_e = input_t::EParamType; + const type_e type = input.determineParamType(); + update_impl::__call(hasher,static_cast(type)); + const auto roughness = input.getRougness(); + switch (type) + { + case type_e::TotallyMapped: + break; + case type_e::AnisotropicMapped: + break; + case type_e::IsotropicMapped: + break; + case type_e::AnisotropicConstant: + update_impl::__call(hasher,roughness[1]); + [[fallthrough]]; + case type_e::IsotropicConstant: + update_impl::__call(hasher,roughness[0]); + break; + default: + assert(false); + break; + } + } +}; +#endif + } // namespace nbl::asset::material_compiler3 #endif \ No newline at end of file diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 0e0e2cbdfa..7e03f8f3b6 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -61,13 +61,13 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! class IContributor : public INode { public: - // ? + virtual bool isEmitter() const = 0; }; // this is for a term in a mul or add expression class IFactor : public INode { protected: - uint64_t padding; + uint64_t padding = 0; }; // we step away from our usual way of doing things via linked lists, because this is simpler to rearrange class CFactorCombiner final : public obj_pool_type::IVariableSize, public IFactor @@ -215,7 +215,13 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! typed_pointer_type firstTransmission = {}; }; -// TODO: Parameter Node + // + template + class CTextureSample : public obj_pool_type::INonTrivial, public INode + { + public: + SParameterSet paramSet = {}; + }; // Unit Radiance emitter modulated by an IES profile class CEmitter final : public obj_pool_type::INonTrivial, public IContributor @@ -226,18 +232,19 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! inline core::blake3_hash_t computeHash(const obj_pool_type& pool) const { core::blake3_hasher hasher = {}; -// hasher << pool.deref(profile)->getHash(); + hasher << pool.deref(profile)->getHash(); hasher << profileTransform; return hasher.operator core::blake3_hash_t(); } + inline bool isEmitter() const override {return true;} + // 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 -// TODO: this IR will have parameters hashed and deduped as actual nodes -// SParameter profile = {}; + typed_pointer_type> profile = {}; hlsl::float32_t3x3 profileTransform = hlsl::float32_t3x3( 1,0,0, 0,1,0, @@ -283,19 +290,50 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // Both this and Yining are faster because of the 2 BxDF evaluations instead of 3, and they only differ by the inter-facet shadowing and masking functions and the tangent facet normal is computed. // However an orthogonal microfacet will work better for BSDFs because the facet tangents and normals form a square and not a rhombus, which means that a V to the "left" of the perturbed normal // will always be to the "right" of the orthogonal microfacet, meaning we'll get a consistent refraction (both L=refract(V,facet) will curve away from -V in the same direction. - class IBxDF final : public obj_pool_type::INonTrivial, public IContributor + class IBxDF : public obj_pool_type::INonTrivial, public IContributor + { + public: + inline bool isEmitter() const override {return false;} + }; + class CDeltaTransmission final : public IBxDF + { + public: + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CDeltaTransmission);} + + core::blake3_hash_t computeHash(const obj_pool_type& pool) const + { + core::blake3_hasher hasher = {}; + // TODO: put the type in the hash + return hasher.operator core::blake3_hash_t(); + } + + inline CDeltaTransmission() = default; + }; + class IBxDFWithNDF : public IBxDF { public: - // TODO: - // - Share ParamSet with AST - // - Share SBasicNDFParams with AST - // - hash NDF Params - // - maybe share IBxDF - // - and base BxDFs ? }; + class COrenNayar final : public IBxDFWithNDF + { + public: + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(COrenNayar);} + + inline COrenNayar() = default; + }; + class CCookTorrance final : public IBxDFWithNDF + { + public: + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CCookTorrance);} + + inline CCookTorrance() = default; + }; + //! Parameter Nodes + // ScalarConstant + // SpectralConstant + // NormalMap -> impliu //! Basic factor nodes class IFactorLeaf : public IFactor {}; - class CConstant final : public obj_pool_type::INonTrivial, public IFactorLeaf + class CConstantFactor final : public obj_pool_type::INonTrivial, public IFactorLeaf { public: inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CConstant);} @@ -308,7 +346,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! } // you can set the children later - inline CConstant() = default; + inline CConstantFactor() = default; }; #undef TYPE_NAME_STR @@ -452,6 +490,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! NBL_API2 bool rewrite(SMaterial& material, CTrueIR* srcIR); core::vector m_materials; + // TODO: either we put the typeid in the hash, or we have a type of hashmap per type core::unordered_map> m_uniqueNodes; friend struct SBasicNodes; const SBasicNodes m_basicNodes; diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index e54616a43d..31a2810eb5 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -484,35 +484,6 @@ void CFrontendIR::SDotPrinter::operator()(std::ostringstream& str) str << "\n}\n"; } -void CFrontendIR::SParameter::printDot(std::ostringstream& sstr, const core::string& selfID) const -{ - sstr << "\n\t" << selfID << "[label=\"scale = " << std::to_string(scale); - if (view) - { - sstr << "\\nchannel = " << std::to_string(viewChannel); - const auto& viewParams = view->getCreationParameters(); - sstr << "\\nWraps = {" << sampler.TextureWrapU; - if (viewParams.viewType!=ICPUImageView::ET_1D && viewParams.viewType!=ICPUImageView::ET_1D_ARRAY) - sstr << "," << sampler.TextureWrapV; - if (viewParams.viewType==ICPUImageView::ET_3D) - sstr << "," << sampler.TextureWrapW; - sstr << "}\\nBorder = " << sampler.BorderColor; - // don't bother printing the rest, we really don't care much about those - } - sstr << "\"]"; - // TODO: do specialized printing for image views (they need to be gathered into a view set -> need a printing context struct) - /* - struct SDotPrintContext - { - std::ostringstream* sstr; - core::unordered_map* usedViews; - uint16_t indentation = 0; - }; - */ - if (view) - sstr << "\n\t" << selfID << " -> _view_" << std::to_string(reinterpret_cast(view)); -} - core::string CFrontendIR::CSpectralVariable::getLabelSuffix() const { if (getKnotCount()<2) @@ -532,7 +503,7 @@ core::string CFrontendIR::CSpectralVariable::getLabelSuffix() const void CFrontendIR::CSpectralVariable::printDot(std::ostringstream& sstr, const core::string& selfID) const { auto pWonky = reinterpret_cast*>(this+1); - pWonky->knots.printDot(getKnotCount(),sstr,selfID,{}); + CNodePool::printDotParameterSet(pWonky->knots,getKnotCount(),sstr,selfID,{}); } void CFrontendIR::CEmitter::printDot(std::ostringstream& sstr, const core::string& selfID) const @@ -554,25 +525,6 @@ void CFrontendIR::CFresnel::printDot(std::ostringstream& sstr, const core::strin { } -void CFrontendIR::IBxDF::SBasicNDFParams::printDot(std::ostringstream& sstr, const core::string& selfID) const -{ - constexpr const char* paramSemantics[] = { - "dh/du", - "dh/dv", - "alpha_u", - "alpha_v" - }; - SParameterSet<4>::printDot(sstr,selfID,paramSemantics,!definitelyIsotropic()); - if (!stretchInvariant()) - { - const auto referenceID = selfID+"_reference"; - sstr << "\n\t" << referenceID << " [label=\""; - printMatrix(sstr,reference); - sstr << "\"]"; - sstr << "\n\t" << selfID << " -> " << referenceID << " [label=\"Stretch Reference\"]"; - } -} - void CFrontendIR::COrenNayar::printDot(std::ostringstream& sstr, const core::string& selfID) const { ndParams.printDot(sstr,selfID); @@ -584,25 +536,24 @@ void CFrontendIR::CCookTorrance::printDot(std::ostringstream& sstr, const core:: } //! AST-> IR methods -auto CFrontendIR::CEmitter::createIRNode(const CFrontendIR* ast, CTrueIR::obj_pool_type& pool) const -> ir_contributor_handle_t +auto CFrontendIR::CEmitter::createIRNode(const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t { assert(false); // unimplemented return {}; } -auto CFrontendIR::CDeltaTransmission::createIRNode(const CFrontendIR* ast, CTrueIR::obj_pool_type& pool) const -> ir_contributor_handle_t +auto CFrontendIR::CDeltaTransmission::createIRNode(const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t { - assert(false); // unimplemented - return {}; + return ir->getObjectPool().emplace(); } -auto CFrontendIR::COrenNayar::createIRNode(const CFrontendIR* ast, CTrueIR::obj_pool_type& pool) const -> ir_contributor_handle_t +auto CFrontendIR::COrenNayar::createIRNode(const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t { assert(false); // unimplemented return {}; } -auto CFrontendIR::CCookTorrance::createIRNode(const CFrontendIR* ast, CTrueIR::obj_pool_type& pool) const -> ir_contributor_handle_t +auto CFrontendIR::CCookTorrance::createIRNode(const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t { assert(false); // unimplemented return {}; @@ -729,7 +680,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin { if (isContributor) { - contributorStack.push_back({.contributor=static_cast(entry.node)->createIRNode(srcAST,irPool)}); + contributorStack.push_back({.contributor=static_cast(entry.node)->createIRNode(srcAST,tmpIR.get())}); exprStack.pop_back(); } else From 4edfbd850f5ed36725218aaa98856efa169d5d9a Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Wed, 29 Apr 2026 14:49:01 +0200 Subject: [PATCH 11/45] do most of the CTrueIR node hashing --- .../nbl/asset/material_compiler3/CNodePool.h | 88 ++++++-- .../nbl/asset/material_compiler3/CTrueIR.h | 200 ++++++++++++------ .../asset/material_compiler3/CFrontendIR.cpp | 90 +++++--- 3 files changed, 259 insertions(+), 119 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CNodePool.h b/include/nbl/asset/material_compiler3/CNodePool.h index 43d98a089f..0466271727 100644 --- a/include/nbl/asset/material_compiler3/CNodePool.h +++ b/include/nbl/asset/material_compiler3/CNodePool.h @@ -77,7 +77,7 @@ class CNodePool : public core::IReferenceCounted 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 view = {}; - // shadow comparison functions are ignored + // lodbias and clamp shadow comparison functions, anisotropy and minFilter are ignored // NOTE: could take only things that matter from the sampler and pack the viewChannel and reduce padding ICPUSampler::SParams sampler = {}; }; @@ -328,7 +328,67 @@ inline void CNodePool::SBasicNDFParams::printDot(std::ostringstream& sstr, const } } -#if 0 // TODO +// specialization of parameter hashing +template +struct core::blake3_hasher::update_impl +{ + using input_t = asset::material_compiler3::CNodePool::SParameter; + + static inline void __call(blake3_hasher& hasher, const input_t& param) + { + hasher << param.scale; + if (!param.view) + return; + const auto& viewParams = param.view->getCreationParameters(); + // TODO: hash it like CAssetConverter + { + hasher << ptrdiff_t(param.view.get()); + } + // in the future this might change + hasher << param.viewChannel; + const auto& sampler = param.sampler; + hasher << param.sampler.BorderColor; + hasher << param.sampler.MaxFilter; + using view_type_e = asset::IImageView::E_TYPE; + switch (viewParams.viewType) + { + case view_type_e::ET_3D: + hasher << param.sampler.TextureWrapW; + [[fallthrough]]; + case view_type_e::ET_2D: [[fallthrough]]; + case view_type_e::ET_2D_ARRAY: [[fallthrough]]; + case view_type_e::ET_CUBE_MAP: [[fallthrough]]; + case view_type_e::ET_CUBE_MAP_ARRAY: + hasher << param.sampler.TextureWrapV; + [[fallthrough]]; + default: + hasher << param.sampler.TextureWrapU; + break; + } + } +}; +template +struct core::blake3_hasher::update_impl,Dummy> +{ + using input_t = asset::material_compiler3::CNodePool::SParameterSet; + + static inline void __call(blake3_hasher& hasher, const input_t& input) + { + bool noTextures = true; + for (uint8_t i=0; i struct core::blake3_hasher::update_impl { @@ -339,28 +399,12 @@ struct core::blake3_hasher::update_impl using type_e = input_t::EParamType; const type_e type = input.determineParamType(); update_impl::__call(hasher,static_cast(type)); - const auto roughness = input.getRougness(); - switch (type) - { - case type_e::TotallyMapped: - break; - case type_e::AnisotropicMapped: - break; - case type_e::IsotropicMapped: - break; - case type_e::AnisotropicConstant: - update_impl::__call(hasher,roughness[1]); - [[fallthrough]]; - case type_e::IsotropicConstant: - update_impl::__call(hasher,roughness[0]); - break; - default: - assert(false); - break; - } + update_impl>::__call(hasher,*this); + // reference stretch can be applied on non-mapped NDFs too + if (!input.stretchInvariant()) + hasher << input.reference; } }; -#endif } // namespace nbl::asset::material_compiler3 #endif \ No newline at end of file diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 7e03f8f3b6..dd8faf87dd 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -36,10 +36,36 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! class INode : public CNodePool::INode { public: + enum class EFinalType : uint8_t + { + COrientedLayer=0, + CCorellatedTransmission, + CContributorSum, + CWeightedContributor, + CEmitter, + CDeltaTransmission, + CParameters_1, + CParameters_2, + CParameters_3, + CParameters_4, + COrenNayar, + CCookTorrance, + CFactorCombiner, + CConstantFactor + }; + virtual EFinalType getFinalType() const = 0; + const auto& getHash() const {return hash;} - // only call once the nodes underneath are linked up, returning empty hash means error/invalid node - virtual core::blake3_hash_t computeHash(const obj_pool_type& pool) const = 0; + // only call once the nodes underneath are linked up (because it doesn't call recursively), returning empty hash means error/invalid node + inline core::blake3_hash_t computeHash(const obj_pool_type& pool) const + { + core::blake3_hasher hasher = {}; + // always put the node type into the hash + hasher << static_cast(getFinalType()); + computeHash_impl(pool,hasher); + return hasher.operator core::blake3_hash_t(); + } protected: friend class CTrueIR; @@ -48,6 +74,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! hash = computeHash(pool); return hash!=core::blake3_hash_t{}; } + virtual void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const = 0; // Each node is final and immutable, has a precomputed hash for the whole subtree beneath it. // Debug info does not form part of the hash, so can get wildly replaced. @@ -73,6 +100,8 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! class CFactorCombiner final : public obj_pool_type::IVariableSize, public IFactor { public: + inline EFinalType getFinalType() const override {return EFinalType::CFactorCombiner;} + // There's only two ops we support enum Type : uint8_t { @@ -88,15 +117,6 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! }; static_assert(sizeof(SState) == sizeof(IFactor::padding)); inline SState getState() const {return std::bit_cast(padding);} - - inline core::blake3_hash_t computeHash(const obj_pool_type& pool) const override final - { - core::blake3_hasher hasher = {}; - hasher << padding; // hash whole state at once - for (uint16_t i=0; igetHash(); - return hasher.operator core::blake3_hash_t(); - } // Only sane child count allowed inline typed_pointer_type getChildHandle(const uint8_t ix) @@ -107,6 +127,13 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! } protected: + inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override + { + hasher << padding; // hash whole state at once + for (uint16_t i=0; igetHash(); + } + friend class CTrueIR; typed_pointer_type child[1] = {{}}; }; @@ -114,17 +141,19 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // Note that this is not a root node, its a flipped leaf! class CWeightedContributor final : public obj_pool_type::INonTrivial, public INode { - public: - inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CWeightedContributor);} - - core::blake3_hash_t computeHash(const obj_pool_type& pool) const + inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override { - core::blake3_hasher hasher = {}; hasher << pool.deref(contributor)->getHash(); - hasher << pool.deref(factor)->getHash(); - return hasher.operator core::blake3_hash_t(); + if (factor) + hasher << pool.deref(factor)->getHash(); + // TODO: else where it hashes with a premade hash of a `IFactor = CConstantFactor` of monochrome 1.0 scalar } + public: + inline EFinalType getFinalType() const override {return EFinalType::CWeightedContributor;} + + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CWeightedContributor);} + typed_pointer_type contributor = {}; // if null then assumed to be 1 @@ -134,8 +163,6 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // f(w_i,w_o) = Sum_i^N Product_j^{N_i} h_{ij}(w_i,w_o) l_i(w_i,w_o) class CContributorSum final : public obj_pool_type::INonTrivial, public INode { - public: - inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CContributorSum);} // CANONICALIZATION NOTE: The conntributors shall be ordered in following order: // - according to their type, emitters first, then BxDFs, within those @@ -148,14 +175,23 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // Also BxDFs and Emitters (which only hold IES profiles) are monochromatic so accumulating their contribution first uses least registers. // Fresnel, Beer extinction and other analytic modifiers never produce 0s so should be evaluated last. // Function factors which have to be evaluated via composition go even later, but within their dimension category. - core::blake3_hash_t computeHash(const obj_pool_type& pool) const + inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override { - core::blake3_hasher hasher = {}; - hasher << (product ? pool.deref(product)->getHash():core::blake3_hash_t()); - hasher << (rest ? pool.deref(rest)->getHash():core::blake3_hash_t()); - return hasher.operator core::blake3_hash_t(); + // this is an invalid combo, because `rest->product` could be hoisted in place of `product` + assert(product || !rest); + if (product) + { + hasher << pool.deref(product)->getHash(); + if (rest) + hasher << pool.deref(rest)->getHash(); + } } + public: + inline EFinalType getFinalType() const override {return EFinalType::CContributorSum;} + + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CContributorSum);} + // the product is ... typed_pointer_type product = {}; @@ -168,23 +204,28 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // Corellated layering is a far far far TODO, all it means that certain convolutions don't happen - certain BxDFs don't layer over each other (tricky to express in AST and IR) class CCorellatedTransmission final : public obj_pool_type::INonTrivial, public INode { - public: - inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CCorellatedTransmission);} - - core::blake3_hash_t computeHash(const obj_pool_type& pool) const + inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override { - core::blake3_hasher hasher = {}; + // invalid combo, null btdf prevents you crossing over to the next layer and hitting current from below + // also if there's no btdf it makes no sense to have a `next` sibling. + assert(btdf); hasher << pool.deref(btdf)->getHash(); - hasher << pool.deref(brdfBottom)->getHash(); - hasher << pool.deref(coated)->getHash(); - hasher << pool.deref(next)->getHash(); - return hasher.operator core::blake3_hash_t(); + hasher << (brdfBottom ? pool.deref(brdfBottom)->getHash():core::blake3_hash_t()); + hasher << (coated ? pool.deref(coated)->getHash():core::blake3_hash_t()); + if (next) + hasher << pool.deref(next)->getHash(); } + public: + inline EFinalType getFinalType() const override {return EFinalType::CCorellatedTransmission;} + + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CCorellatedTransmission);} + // you can set the children later inline CCorellatedTransmission() = default; - // to get to the coated layer we must transmit through + // Obligatory if you don't want transmission then don't put a valid handle in `COrientedLayer::firstTransmission` + // Also shouldn't contain `CDeltaTransmission` in the BTDF unless `coated` is null (TODO a check for that) typed_pointer_type btdf = {}; // because the layer is oriented, these last two members must be null when the coating stops typed_pointer_type brdfBottom = {}; @@ -195,17 +236,18 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // The oriented layer is a layer with already all the Etas reciprocated, etc. class COrientedLayer final : public obj_pool_type::INonTrivial, public INode { - public: - inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(COrientedLayer);} - - inline core::blake3_hash_t computeHash(const obj_pool_type& pool) const + inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override { - core::blake3_hasher hasher = {}; - hasher << pool.deref(brdfTop)->getHash(); - hasher << pool.deref(firstTransmission)->getHash(); - return hasher.operator core::blake3_hash_t(); + hasher << (brdfTop ? pool.deref(brdfTop)->getHash():core::blake3_hash_t()); + if (firstTransmission) + hasher << pool.deref(firstTransmission)->getHash(); } + public: + inline EFinalType getFinalType() const override {return EFinalType::COrientedLayer;} + + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(COrientedLayer);} + // you can set the children later inline COrientedLayer() = default; @@ -217,26 +259,41 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // template - class CTextureSample : public obj_pool_type::INonTrivial, public INode + class CParameters final : public obj_pool_type::INonTrivial, public INode { + inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override + { + hasher << paramSet; + } + public: + inline EFinalType getFinalType() const override + { + static_assert(1<=Channels && Channels<=4); + return static_cast(static_cast(EFinalType::CParameters_1)-1+Channels); + } + + // TODO: improve the token pasting here + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CParameters);} + SParameterSet paramSet = {}; }; // Unit Radiance emitter modulated by an IES profile 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 core::blake3_hash_t computeHash(const obj_pool_type& pool) const + inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override { - core::blake3_hasher hasher = {}; - hasher << pool.deref(profile)->getHash(); hasher << profileTransform; - return hasher.operator core::blake3_hash_t(); + if (profile) + hasher << pool.deref(profile)->getHash(); } + public: + inline EFinalType getFinalType() const override {return EFinalType::CEmitter;} + + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CEmitter);} + inline bool isEmitter() const override {return true;} // you can set the members later @@ -244,7 +301,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // 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 - typed_pointer_type> profile = {}; + typed_pointer_type> profile = {}; hlsl::float32_t3x3 profileTransform = hlsl::float32_t3x3( 1,0,0, 0,1,0, @@ -297,25 +354,32 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! }; class CDeltaTransmission final : public IBxDF { + // nothing to do + inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override {} + public: - inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CDeltaTransmission);} + inline EFinalType getFinalType() const override {return EFinalType::CDeltaTransmission;} - core::blake3_hash_t computeHash(const obj_pool_type& pool) const - { - core::blake3_hasher hasher = {}; - // TODO: put the type in the hash - return hasher.operator core::blake3_hash_t(); - } + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CDeltaTransmission);} inline CDeltaTransmission() = default; }; class IBxDFWithNDF : public IBxDF { + inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override + { + assert(false); // unimplemented + } + public: + // CParameters<4> ? + //CNodePool::SBasicNDFParams ndfParams; }; class COrenNayar final : public IBxDFWithNDF { public: + inline EFinalType getFinalType() const override {return EFinalType::COrenNayar;} + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(COrenNayar);} inline COrenNayar() = default; @@ -323,6 +387,8 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! class CCookTorrance final : public IBxDFWithNDF { public: + inline EFinalType getFinalType() const override {return EFinalType::CCookTorrance;} + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CCookTorrance);} inline CCookTorrance() = default; @@ -330,21 +396,21 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! //! Parameter Nodes // ScalarConstant // SpectralConstant - // NormalMap -> impliu //! Basic factor nodes class IFactorLeaf : public IFactor {}; + // TODO use CParameters<1> or CParameters<3> + colorpsace semantics (part of `CSpectralVariable`) class CConstantFactor final : public obj_pool_type::INonTrivial, public IFactorLeaf { - public: - inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CConstant);} - - inline core::blake3_hash_t computeHash(const obj_pool_type& pool) const override final + inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override { - core::blake3_hasher hasher = {}; - // TODO: hash the parameter - return hasher.operator core::blake3_hash_t(); + assert(false);// TODO: hash the parameter } + public: + inline EFinalType getFinalType() const override {return EFinalType::CConstantFactor;} + + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CConstant);} + // you can set the children later inline CConstantFactor() = default; }; diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index 31a2810eb5..fe0a2bc283 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -568,7 +568,7 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ assert(layerStack.empty()); auto& irPool = tmpIR->getObjectPool(); - // go down through layers and create all the dependencies + // go down through layers and enqueue them so the layers can be added in reverse for (const auto* layer=astPool.deref(rootH); layer; layer=astPool.deref(layer->coated)) { // TODO: actually re-check the expressions for being null after optimization @@ -587,38 +587,62 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ args.logger.log("Skipping remaining layers due to no transmission",ELL_DEBUG); break; } -// TODO: handle the rest of this stuff -//assert(false); - // Only if we're not in the last layer do we care about the bottom BRDF (you can't hit it otherwise) - // Note that this won't catch the next layer being a blackhole and needs to be undone if it is - if (layer->coated && layer->brdfBottom) - { - // do stuff with brdfBottom - } } - if (!layerStack.empty()) - retval.metadata |= CTrueIR::SMaterial::EMetadataBits::NotBlackhole; - // then go back up and make the layers - while (!layerStack.empty()) + // Some metadata needed for us + bool layersBelowCanScatterBack = false; + CTrueIR::typed_pointer_type prevLayerH = {}; + // then go in reverse and do the layers + for (auto layerIt=layerStack.rbegin(); layerIt!=layerStack.rend(); layerIt++) { - const auto* const inLayer = layerStack.back(); - layerStack.pop_back(); + const auto& inLayer = *layerIt; // allocate a layer const auto layerH = irPool.emplace(); - auto* const outLayer = irPool.deref(layerH); - retval.root = layerH; - // process the BTDF - //... - // process the top BRDF - outLayer->brdfTop = makeContributors(inLayer->brdfTop); - // if BTDF has delta transmissions, then via the sampling property hoist next layer into current layer BRDFs with the DeltaTransmission weights applied - // hmm this would require decorrellation... because don't want rest of BTDF to affect - //... - } - // skip replace delta transmissions by the layer undernearth, if null then keep as delta + { + auto* const outLayer = irPool.deref(layerH); + // process the top BRDF + outLayer->brdfTop = makeContributors(inLayer->brdfTop); + // process the BTDF + const auto btdfH = makeContributors(inLayer->btdf); + // because we're oriented, the bottom brdf can't exist without a BTDF on top (there's no ray that can reach it from our oriented side) + if (btdfH) + { + const auto transmissionH = irPool.emplace(); + { + auto* const transmission = irPool.deref(transmissionH); + transmission->btdf = btdfH; + // Only if we have a layer below us capable of reflecting the ray back, do we care about the bottom BRDF (you can't hit it otherwise) + if (layersBelowCanScatterBack) + transmission->brdfBottom = makeContributors(inLayer->brdfBottom); + // we check if previous layer didn't get oprimized away, but we don't add its optimized version because don't want pointers across two pools (crash) + if (retval.root) + transmission->coated = prevLayerH; + } + } + } + prevLayerH = layerH; + // Now optimize everything inserting it into the proper IR + { + // TODO: pass which copies the layers over into full IR while optimizing + retval.root = layerH; // wrong pool handle + auto* const outLayer = irPool.deref(layerH); // wrong IR Pool + + // ideas for the pass: + // - skip replace delta transmissions by the layer undernearth, if null then keep as delta + // - if BTDF has delta transmissions, then via the sampling property hoist next layer into current layer BRDFs with the DeltaTransmission weights applied + // observations: + // - Any V-dependent factors cannot be commonalized across layers, most of the CSE has to be done within a layer + + // TODO: combine layer meta with `retval.metadata` - // AST is Sum Expression to the BRDF nodes - // We need to keep the Ancestor prefix as an unrolled linked list + // Set this for the next layer after us, we are reflective or previous layer scatters back towards us and we don't block it + layersBelowCanScatterBack = bool(outLayer->brdfTop) || layersBelowCanScatterBack && outLayer->firstTransmission; + } + } + // quick sanity check + assert((retval.root ? bool(irPool.deref(retval.root)->firstTransmission):false)==layersBelowCanScatterBack); + // might turn this into an assert + if (retval.root) + retval.metadata |= CTrueIR::SMaterial::EMetadataBits::NotBlackhole; return retval; } @@ -773,7 +797,13 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin { // only one contributor could have been encountered assert(contributorStack.size()==1); - // TODO: add the contributor + // add the contributor + headH = irPool.emplace(); + { + const auto weightedH = irPool.emplace(); + irPool.deref(weightedH)->contributor = contributorStack.back().contributor; + irPool.deref(headH)->product = weightedH; + } mulChain.clear(); contributorStack.clear(); } @@ -806,7 +836,7 @@ CTrueIR::SMaterialHandle CFrontendIR::makeFinalIR(const typed_pointer_typeaddMaterial(material); if (retval) { - // TODO: better debug info + // TODO: better debug info (e.g. concat all the layer info during `makeOrientedMaterial` via the `session` object if (const auto* debug=astPool.deref(astRoot->debugInfo); debug && !debug->data().empty()) { material.debugInfo = session.args.ir->getObjectPool().emplace(debug->data().data(),debug->data().size()); From 533fe89c28ff3028a640b368b985a78da6f8dac0 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Wed, 29 Apr 2026 15:42:52 +0200 Subject: [PATCH 12/45] fix some asserts --- .../asset/material_compiler3/CFrontendIR.cpp | 61 ++++++++++++++++--- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index fe0a2bc283..8c4f50800e 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -538,8 +538,22 @@ void CFrontendIR::CCookTorrance::printDot(std::ostringstream& sstr, const core:: //! AST-> IR methods auto CFrontendIR::CEmitter::createIRNode(const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t { - assert(false); // unimplemented - return {}; + auto& irPool = ir->getObjectPool(); + const auto profileH = irPool.emplace>(); + if (auto* const profile=irPool.deref(profileH); profile) + { + return {}; // unimplemented +// profile->paramSet = profile; + } + else + return {}; + const auto retval = irPool.emplace(); + if (auto* const contributor=irPool.deref(retval)) + { + contributor->profile = profileH; + contributor->profileTransform = profileTransform; + } + return retval; } auto CFrontendIR::CDeltaTransmission::createIRNode(const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t @@ -549,14 +563,31 @@ auto CFrontendIR::CDeltaTransmission::createIRNode(const CFrontendIR* ast, CTrue auto CFrontendIR::COrenNayar::createIRNode(const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t { - assert(false); // unimplemented - return {}; + auto& irPool = ir->getObjectPool(); + const auto retval = irPool.emplace(); + if (auto* const contributor = irPool.deref(retval)) + { + return {}; // unimplemented +// contributor->ndParams = ndParams; + } + return retval; } auto CFrontendIR::CCookTorrance::createIRNode(const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t { - assert(false); // unimplemented - return {}; + auto& irPool = ir->getObjectPool(); + return {}; // unimplemented +#if 0 + const auto etaH = irPool.emplace(); + if (!etaH) + return {}; + const auto retval = irPool.emplace(); + if (auto* const ct=irPool.deref(retval); ct) + { + ct->ndParams = ndParams; + } + return retval; +#endif } auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_pointer_type rootH, const CFrontendIR* _srcAST) -> oriented_material_t @@ -629,20 +660,30 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ // ideas for the pass: // - skip replace delta transmissions by the layer undernearth, if null then keep as delta // - if BTDF has delta transmissions, then via the sampling property hoist next layer into current layer BRDFs with the DeltaTransmission weights applied + // - order the `CWeightedContributor` within the `CContributorSum` linked list so emitters are first, and so on (null products go last) // observations: // - Any V-dependent factors cannot be commonalized across layers, most of the CSE has to be done within a layer // TODO: combine layer meta with `retval.metadata` // Set this for the next layer after us, we are reflective or previous layer scatters back towards us and we don't block it - layersBelowCanScatterBack = bool(outLayer->brdfTop) || layersBelowCanScatterBack && outLayer->firstTransmission; + if (outLayer) + layersBelowCanScatterBack = bool(outLayer->brdfTop) || layersBelowCanScatterBack && outLayer->firstTransmission; + else + layersBelowCanScatterBack = false; } } - // quick sanity check - assert((retval.root ? bool(irPool.deref(retval.root)->firstTransmission):false)==layersBelowCanScatterBack); - // might turn this into an assert + layerStack.clear(); + // last touch ups if (retval.root) + { + // might turn this into an assert retval.metadata |= CTrueIR::SMaterial::EMetadataBits::NotBlackhole; + } + else + { + assert(!layersBelowCanScatterBack); + } return retval; } From 7411f77dc07916a75f5879b815719c654d3bd1f5 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Wed, 29 Apr 2026 18:38:36 +0200 Subject: [PATCH 13/45] resolve crash from not copying debug infos across pools --- include/nbl/asset/material_compiler3/CFrontendIR.h | 2 +- include/nbl/asset/material_compiler3/CNodePool.h | 10 ++++++++++ include/nbl/asset/material_compiler3/CTrueIR.h | 2 +- src/nbl/asset/material_compiler3/CFrontendIR.cpp | 14 ++++++++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index 593fd1babf..5620a618a2 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -674,7 +674,7 @@ class CFrontendIR final : public CNodePool // 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 orientedRealEta = {}; - // + // TODO: these could be put in the padding of `ndParams.params` NDF ndf : 7 = NDF::GGX; uint8_t reciprocateEta : 1 = false; diff --git a/include/nbl/asset/material_compiler3/CNodePool.h b/include/nbl/asset/material_compiler3/CNodePool.h index 0466271727..3bc8565b38 100644 --- a/include/nbl/asset/material_compiler3/CNodePool.h +++ b/include/nbl/asset/material_compiler3/CNodePool.h @@ -162,6 +162,16 @@ class CNodePool : public core::IReferenceCounted protected: const uint32_t m_size; }; + // copy the debug nodes between pools + inline typed_pointer_type copyDebugInfo(const typed_pointer_type orig, const CNodePool* pSource) + { + assert(pSource); + if (!orig) + return {}; + const auto span = pSource->getObjectPool().deref(orig)->data(); + const auto oldView = std::string_view(reinterpret_cast(span.data()),span.size()-1); + return getObjectPool().emplace(oldView); + } // 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 diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index dd8faf87dd..a244cdc2b4 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -257,7 +257,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! typed_pointer_type firstTransmission = {}; }; - // + // TODO: break this into UV-sample-able params and regular params template class CParameters final : public obj_pool_type::INonTrivial, public INode { diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index 8c4f50800e..eadd5036dc 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -154,6 +154,7 @@ auto CFrontendIR::deepCopy(const typed_pointer_type orig, const else { auto* const copy = dstPool.deref(copyH); + copy->debugInfo = copyDebugInfo(node->debugInfo,pSourceIR); for (uint8_t c=0; cgetChildHandle(c); @@ -223,6 +224,8 @@ auto CFrontendIR::reciprocate(const typed_pointer_type orig, co auto* const copy = dstPool.deref(copyH); if (!copy) return {}; + if (pSourceIR!=this) + copy->debugInfo = copyDebugInfo(node->debugInfo,pSourceIR); if (needToReciprocate) node->reciprocate(copy); // only changed children need to be set @@ -262,6 +265,7 @@ auto CFrontendIR::copyLayers(const typed_pointer_type orig, const // need to deep copy the nodes if (pSourceIR!=this) { + outLayer->debugInfo = copyDebugInfo(layer->debugInfo,pSourceIR); outLayer->brdfBottom = deepCopy(layer->brdfBottom,pSourceIR)._const_cast(); outLayer->btdf = deepCopy(layer->btdf,pSourceIR)._const_cast(); outLayer->brdfTop = deepCopy(layer->brdfTop,pSourceIR)._const_cast(); @@ -293,8 +297,17 @@ auto CFrontendIR::reverse(const typed_pointer_type orig, const CFr { auto* outLayer = dstPool.deref(copyH); typed_pointer_type underLayerH={}; + std::string debugData; debugData.reserve(4096); for (const auto* layer=srcPool.deref(orig); true; layer=srcPool.deref(layer->coated)) { + debugData = "REVERSED {"; + if (auto* debugInfo=srcPool.deref(layer->debugInfo); debugInfo) + { + const auto span = debugInfo->data(); + debugData += std::string_view(reinterpret_cast(span.data()),span.size()-1); + } + debugData += '}'; + outLayer->debugInfo = dstPool.emplace(debugData); outLayer->coated = underLayerH; // we reciprocate everything because numerator and denominator switch (top and bottom of layer stack) outLayer->brdfBottom = reciprocate(layer->brdfTop,pSourceIR)._const_cast(); @@ -844,6 +857,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin const auto weightedH = irPool.emplace(); irPool.deref(weightedH)->contributor = contributorStack.back().contributor; irPool.deref(headH)->product = weightedH; + // TODO: do the factor as a mul chain! } mulChain.clear(); contributorStack.clear(); From b7cb389d8b82d0df7dd705e191bfb530725ed81b Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Wed, 29 Apr 2026 20:59:42 +0200 Subject: [PATCH 14/45] Move stuff only relevant to TrueIR and the blessed FrontendIR from `CNodePool` to `CTrueIR` repurpose the CTrueIR::INode hash computation to also validate --- .../asset/material_compiler3/CFrontendIR.h | 20 +- .../nbl/asset/material_compiler3/CNodePool.h | 295 ------------- .../nbl/asset/material_compiler3/CTrueIR.h | 414 ++++++++++++++++-- .../asset/material_compiler3/CFrontendIR.cpp | 12 +- 4 files changed, 381 insertions(+), 360 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index 5620a618a2..b118dc96f3 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -245,10 +245,10 @@ class CFrontendIR final : public CNodePool struct SCreationParams { // Knots are "data points" on the (wavelength,value) plot, from which we can interpolate the rest of the spectrum - SParameterSet knots = {}; + CTrueIR::SParameterSet knots = {}; // a little bit of abuse and padding reuse - static_assert(sizeof(SParameter::padding)>2); + static_assert(sizeof(CTrueIR::SParameter::padding)>2); template requires (Enable==(Count>1)) Semantics& getSemantics() {return reinterpret_cast(knots.params[0].padding[2]); } template requires (Enable==(Count>1)) @@ -281,25 +281,25 @@ class CFrontendIR final : public CNodePool // these getters are immutable inline uint8_t getKnotCount() const { - static_assert(sizeof(SParameter::padding)>1); + static_assert(sizeof(CTrueIR::SParameter::padding)>1); return paramsBeginPadding()[1]; } inline Semantics getSemantics() const { - static_assert(sizeof(SParameter::padding)>2); + static_assert(sizeof(CTrueIR::SParameter::padding)>2); const auto retval = static_cast(paramsBeginPadding()[2]); assert((getKnotCount()==1)==(retval==Semantics::NoneUndefined)); return retval; } // - inline SParameter* getParam(const uint8_t i) + inline CTrueIR::SParameter* getParam(const uint8_t i) { if (iknots.params[i]; return nullptr; } - inline const SParameter* getParam(const uint8_t i) const {return const_cast(const_cast(this)->getParam(i));} + inline const CTrueIR::SParameter* getParam(const uint8_t i) const {return const_cast(const_cast(this)->getParam(i));} // template @@ -310,7 +310,7 @@ class CFrontendIR final : public CNodePool // for copying static inline uint32_t calc_size(const CSpectralVariable& other) { - return sizeof(CSpectralVariable)+sizeof(SCreationParams<1>)+(other.getKnotCount()-1)*sizeof(SParameter); + return sizeof(CSpectralVariable)+sizeof(SCreationParams<1>)+(other.getKnotCount()-1)*sizeof(CTrueIR::SParameter); } inline operator bool() const {return !invalid(SInvalidCheckArgs{.pool=nullptr,.logger=nullptr});} @@ -443,7 +443,7 @@ class CFrontendIR final : public CNodePool // 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 = {}; + CTrueIR::SParameter profile = {}; hlsl::float32_t3x3 profileTransform = hlsl::float32_t3x3( 1,0,0, 0,1,0, @@ -641,7 +641,7 @@ class CFrontendIR final : public CNodePool inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(COrenNayar);} inline COrenNayar() = default; - SBasicNDFParams ndParams = {}; + CTrueIR::SBasicNDFParams ndParams = {}; protected: COPY_DEFAULT_IMPL @@ -667,7 +667,7 @@ class CFrontendIR final : public CNodePool inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CCookTorrance);} inline CCookTorrance() = default; - SBasicNDFParams ndParams = {}; + CTrueIR::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, diff --git a/include/nbl/asset/material_compiler3/CNodePool.h b/include/nbl/asset/material_compiler3/CNodePool.h index 3bc8565b38..cb7b9d5ff0 100644 --- a/include/nbl/asset/material_compiler3/CNodePool.h +++ b/include/nbl/asset/material_compiler3/CNodePool.h @@ -49,73 +49,6 @@ class CNodePool : public core::IReferenceCounted inline obj_pool_type& getObjectPool() {return m_composed;} inline const obj_pool_type& getObjectPool() const {return m_composed;} - // - struct SParameter - { - inline operator bool() const - { - return abs(scale)::infinity() && (!view || viewChannelgetCreationParameters().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);} - - 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::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 view = {}; - // lodbias and clamp shadow comparison functions, anisotropy and minFilter 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 - struct SParameterSet - { - inline operator bool() const - { - for (uint8_t i=0; i0); - - template - inline void printDot(std::ostringstream& sstr, const core::string& selfID, StringConstIterator paramNameBegin={}, const bool uvRequired=false) const - { - CNodePool::printDotParameterSet(*this,Count,sstr,selfID,std::forward(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)); - }; - // class INode { @@ -173,79 +106,6 @@ class CNodePool : public core::IReferenceCounted return getObjectPool().emplace(oldView); } - // 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(params,2);} - inline auto getDerivMap() const {return std::span(params,2);} - inline auto getRougness() {return std::span(params+2,2);} - inline auto getRougness() const {return std::span(params+2,2);} - - inline SBasicNDFParams() - { - // initialize with constant flat deriv map and smooth roughness - for (auto& param : params) - param.scale = 0.f; - } - - // The usage of a normal modifier implies potential anisotropic roughness when filtering (CLEAR, CLEAN, Neural), so all 4 (or 5) parameters should come from a texture. - // When normal modifier is not used, the roughness can still come from a texture but can be isotropic or anisotropic. Weird combos will require making tiny textures when converting from AST. - enum class EParamType : uint8_t - { - TotallyMapped, - AnisotropicMapped, - IsotropicMapped, - AnisotropicConstant, - IsotropicConstant - }; - // This is about how we load our data into the NDF not whether the NDF is really isotropic - inline EParamType determineParamType() 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 EParamType::TotallyMapped; - const auto roughness = getRougness(); - // having one roughness be mapped and another not mapped, isn't very useful in any renderer - const bool roughnessIsMapped = roughness[0].scale!=0.f && roughness[0].view || roughness[1].scale!=0.f && roughness[1].view; - // if roughness inputs are not equal (same scale, same texture) then NDF can be anisotropic in places - if (roughness[0]!=roughness[1]) - { - return roughnessIsMapped ? EParamType::AnisotropicMapped:EParamType::AnisotropicConstant; - } - else if (roughnessIsMapped) - { - return EParamType::IsotropicMapped; - } - else - return EParamType::IsotropicConstant; - } - - // conservative check, checks if we can optimize certain things this way - inline bool definitelyIsotropic() const - { - switch (determineParamType()) - { - case EParamType::IsotropicMapped: [[fallthrough]]; - case EParamType::IsotropicConstant: - break; - default: - 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::min());} - - 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); - }; - // template inline const std::string_view getTypeName(const typed_pointer_type h) const @@ -258,163 +118,8 @@ class CNodePool : public core::IReferenceCounted protected: inline CNodePool(typename obj_pool_type::creation_params_type&& params) : m_composed(std::move(params)) {} - template - friend struct SParameterSet; - // Use `_count` instead of `Count` because of how wonkily this stuff gets used - template - static inline void printDotParameterSet(const SParameterSet& _set, const uint8_t _count, std::ostringstream& sstr, const core::string& selfID, StringConstIterator paramNameBegin={}, const bool uvRequired=false) - { - bool imageUsed = false; - for (uint8_t i=0; i<_count; i++) - { - const auto paramID = selfID+"_param"+std::to_string(i); - if (_set.params[i].view) - imageUsed = true; - _set.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(_set.uvSlot()) << "\\n"; - printMatrix(sstr,*reinterpret_cast(_set.params+_count)); - sstr << "\"]"; - sstr << "\n\t" << selfID << " -> " << uvTransformID << "[label=\"UV Transform\"]"; - } - } - obj_pool_type m_composed; }; -inline void CNodePool::SParameter::printDot(std::ostringstream& sstr, const core::string& selfID) const -{ - sstr << "\n\t" << selfID << "[label=\"scale = " << std::to_string(scale); - if (view) - { - sstr << "\\nchannel = " << std::to_string(viewChannel); - const auto& viewParams = view->getCreationParameters(); - sstr << "\\nWraps = {" << sampler.TextureWrapU; - if (viewParams.viewType!=ICPUImageView::ET_1D && viewParams.viewType!=ICPUImageView::ET_1D_ARRAY) - sstr << "," << sampler.TextureWrapV; - if (viewParams.viewType==ICPUImageView::ET_3D) - sstr << "," << sampler.TextureWrapW; - sstr << "}\\nBorder = " << sampler.BorderColor; - // don't bother printing the rest, we really don't care much about those - } - sstr << "\"]"; - // TODO: do specialized printing for image views (they need to be gathered into a view set -> need a printing context struct) - /* - struct SDotPrintContext - { - std::ostringstream* sstr; - core::unordered_map* usedViews; - uint16_t indentation = 0; - }; - */ - if (view) - sstr << "\n\t" << selfID << " -> _view_" << std::to_string(reinterpret_cast(view)); -} - -inline void CNodePool::SBasicNDFParams::printDot(std::ostringstream& sstr, const core::string& selfID) const -{ - constexpr const char* paramSemantics[] = { - "dh/du", - "dh/dv", - "alpha_u", - "alpha_v" - }; - SParameterSet<4>::printDot(sstr,selfID,paramSemantics,!definitelyIsotropic()); - if (!stretchInvariant()) - { - const auto referenceID = selfID+"_reference"; - sstr << "\n\t" << referenceID << " [label=\""; - printMatrix(sstr,reference); - sstr << "\"]"; - sstr << "\n\t" << selfID << " -> " << referenceID << " [label=\"Stretch Reference\"]"; - } -} - -// specialization of parameter hashing -template -struct core::blake3_hasher::update_impl -{ - using input_t = asset::material_compiler3::CNodePool::SParameter; - - static inline void __call(blake3_hasher& hasher, const input_t& param) - { - hasher << param.scale; - if (!param.view) - return; - const auto& viewParams = param.view->getCreationParameters(); - // TODO: hash it like CAssetConverter - { - hasher << ptrdiff_t(param.view.get()); - } - // in the future this might change - hasher << param.viewChannel; - const auto& sampler = param.sampler; - hasher << param.sampler.BorderColor; - hasher << param.sampler.MaxFilter; - using view_type_e = asset::IImageView::E_TYPE; - switch (viewParams.viewType) - { - case view_type_e::ET_3D: - hasher << param.sampler.TextureWrapW; - [[fallthrough]]; - case view_type_e::ET_2D: [[fallthrough]]; - case view_type_e::ET_2D_ARRAY: [[fallthrough]]; - case view_type_e::ET_CUBE_MAP: [[fallthrough]]; - case view_type_e::ET_CUBE_MAP_ARRAY: - hasher << param.sampler.TextureWrapV; - [[fallthrough]]; - default: - hasher << param.sampler.TextureWrapU; - break; - } - } -}; -template -struct core::blake3_hasher::update_impl,Dummy> -{ - using input_t = asset::material_compiler3::CNodePool::SParameterSet; - - static inline void __call(blake3_hasher& hasher, const input_t& input) - { - bool noTextures = true; - for (uint8_t i=0; i -struct core::blake3_hasher::update_impl -{ - using input_t = asset::material_compiler3::CNodePool::SBasicNDFParams; - - static inline void __call(blake3_hasher& hasher, const input_t& input) - { - using type_e = input_t::EParamType; - const type_e type = input.determineParamType(); - update_impl::__call(hasher,static_cast(type)); - update_impl>::__call(hasher,*this); - // reference stretch can be applied on non-mapped NDFs too - if (!input.stretchInvariant()) - hasher << input.reference; - } -}; - } // namespace nbl::asset::material_compiler3 #endif \ No newline at end of file diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index a244cdc2b4..8af2bafe0b 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -32,6 +32,145 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! return core::smart_refctd_ptr(new CTrueIR(std::move(params)),core::dont_grab); } + //! Stuff thats kind-of common to CFrontendIR but doesn't make sense in CNodePool + struct SParameter + { + inline operator bool() const + { + return abs(scale)::infinity() && (!view || viewChannelgetCreationParameters().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);} + + 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::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 view = {}; + // lodbias and clamp shadow comparison functions, anisotropy and minFilter are ignored + // NOTE: could take only things that matter from the sampler and pack the viewChannel and reduce padding + ICPUSampler::SParams sampler = {}; + }; + // We worry about keeping parameters in the same image view later (the backend) + template + struct SParameterSet + { + inline operator bool() const + { + for (uint8_t i=0; i0); + + template + inline void printDot(std::ostringstream& sstr, const core::string& selfID, StringConstIterator paramNameBegin={}, const bool uvRequired=false) const + { + CTrueIR::printDotParameterSet(*this,Count,sstr,selfID,std::forward(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)); + }; + // 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(params,2);} + inline auto getDerivMap() const {return std::span(params,2);} + inline auto getRougness() {return std::span(params+2,2);} + inline auto getRougness() const {return std::span(params+2,2);} + + inline SBasicNDFParams() + { + // initialize with constant flat deriv map and smooth roughness + for (auto& param : params) + param.scale = 0.f; + } + + // The usage of a normal modifier implies potential anisotropic roughness when filtering (CLEAR, CLEAN, Neural), so all 4 (or 5) parameters should come from a texture. + // When normal modifier is not used, the roughness can still come from a texture but can be isotropic or anisotropic. Weird combos will require making tiny textures when converting from AST. + enum class EParamType : uint8_t + { + TotallyMapped, + AnisotropicMapped, + IsotropicMapped, + AnisotropicConstant, + IsotropicConstant + }; + // This is about how we load our data into the NDF not whether the NDF is really isotropic + inline EParamType determineParamType() 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 EParamType::TotallyMapped; + const auto roughness = getRougness(); + // having one roughness be mapped and another not mapped, isn't very useful in any renderer + const bool roughnessIsMapped = roughness[0].scale!=0.f && roughness[0].view || roughness[1].scale!=0.f && roughness[1].view; + // if roughness inputs are not equal (same scale, same texture) then NDF can be anisotropic in places + if (roughness[0]!=roughness[1]) + { + return roughnessIsMapped ? EParamType::AnisotropicMapped:EParamType::AnisotropicConstant; + } + else if (roughnessIsMapped) + { + return EParamType::IsotropicMapped; + } + else + return EParamType::IsotropicConstant; + } + + // conservative check, checks if we can optimize certain things this way + inline bool definitelyIsotropic() const + { + switch (determineParamType()) + { + case EParamType::IsotropicMapped: [[fallthrough]]; + case EParamType::IsotropicConstant: + break; + default: + 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::min());} + + 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); + }; + // basic "built-in" nodes class INode : public CNodePool::INode { @@ -63,7 +202,8 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! core::blake3_hasher hasher = {}; // always put the node type into the hash hasher << static_cast(getFinalType()); - computeHash_impl(pool,hasher); + if (!computeHash_impl(pool,hasher)) + return {}; return hasher.operator core::blake3_hash_t(); } @@ -74,7 +214,14 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! hash = computeHash(pool); return hash!=core::blake3_hash_t{}; } - virtual void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const = 0; + virtual bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const = 0; +#define HASH_REQUIREDS_HASH(HANDLE) { \ + if (const auto hash=pool.deref(HANDLE)->getHash(); hash==core::blake3_hash_t{}) \ + return false; \ + else \ + hasher << hash; \ + } +#define HASH_OPTIONALS_HASH(HANDLE) if (HANDLE) {HASH_REQUIREDS_HASH(HANDLE);} else {hasher << core::blake3_hash_t{};} // Each node is final and immutable, has a precomputed hash for the whole subtree beneath it. // Debug info does not form part of the hash, so can get wildly replaced. @@ -127,11 +274,18 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! } protected: - inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override + inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override { + if (getState().childCount==0) + return false; hasher << padding; // hash whole state at once for (uint16_t i=0; igetHash(); + { + if (!child[i]) + return false; + HASH_REQUIREDS_HASH(child[i]); + } + return true; } friend class CTrueIR; @@ -141,12 +295,14 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // Note that this is not a root node, its a flipped leaf! class CWeightedContributor final : public obj_pool_type::INonTrivial, public INode { - inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override + inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override { - hasher << pool.deref(contributor)->getHash(); - if (factor) - hasher << pool.deref(factor)->getHash(); + if (!contributor) + return false; + HASH_REQUIREDS_HASH(contributor); + HASH_OPTIONALS_HASH(factor); // TODO: else where it hashes with a premade hash of a `IFactor = CConstantFactor` of monochrome 1.0 scalar + return true; } public: @@ -175,16 +331,16 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // Also BxDFs and Emitters (which only hold IES profiles) are monochromatic so accumulating their contribution first uses least registers. // Fresnel, Beer extinction and other analytic modifiers never produce 0s so should be evaluated last. // Function factors which have to be evaluated via composition go even later, but within their dimension category. - inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override + inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override { - // this is an invalid combo, because `rest->product` could be hoisted in place of `product` - assert(product || !rest); if (product) { - hasher << pool.deref(product)->getHash(); - if (rest) - hasher << pool.deref(rest)->getHash(); + HASH_REQUIREDS_HASH(product); + HASH_OPTIONALS_HASH(rest); } + else if (rest) // this is an invalid combo, because `rest->product` could be hoisted in place of `product` + return false; + return true; } public: @@ -204,16 +360,17 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // Corellated layering is a far far far TODO, all it means that certain convolutions don't happen - certain BxDFs don't layer over each other (tricky to express in AST and IR) class CCorellatedTransmission final : public obj_pool_type::INonTrivial, public INode { - inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override + inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override { // invalid combo, null btdf prevents you crossing over to the next layer and hitting current from below // also if there's no btdf it makes no sense to have a `next` sibling. - assert(btdf); - hasher << pool.deref(btdf)->getHash(); - hasher << (brdfBottom ? pool.deref(brdfBottom)->getHash():core::blake3_hash_t()); - hasher << (coated ? pool.deref(coated)->getHash():core::blake3_hash_t()); - if (next) - hasher << pool.deref(next)->getHash(); + if (!btdf) + return false; + HASH_REQUIREDS_HASH(btdf); + HASH_OPTIONALS_HASH(brdfBottom); + HASH_OPTIONALS_HASH(coated); + HASH_OPTIONALS_HASH(next); + return true; } public: @@ -236,11 +393,11 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // The oriented layer is a layer with already all the Etas reciprocated, etc. class COrientedLayer final : public obj_pool_type::INonTrivial, public INode { - inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override + inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override { - hasher << (brdfTop ? pool.deref(brdfTop)->getHash():core::blake3_hash_t()); - if (firstTransmission) - hasher << pool.deref(firstTransmission)->getHash(); + HASH_OPTIONALS_HASH(brdfTop); + HASH_OPTIONALS_HASH(firstTransmission); + return true; } public: @@ -261,9 +418,10 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! template class CParameters final : public obj_pool_type::INonTrivial, public INode { - inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override + inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override { hasher << paramSet; + return true; } public: @@ -282,11 +440,20 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // Unit Radiance emitter modulated by an IES profile class CEmitter final : public obj_pool_type::INonTrivial, public IContributor { - inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override + inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override { hasher << profileTransform; - if (profile) - hasher << pool.deref(profile)->getHash(); + // we ignore most of the sampler, needs to be set always the same + const auto& sampler = profile.sampler; + using namespace ::nbl::asset; + if (sampler.BorderColor!=ISampler::E_TEXTURE_BORDER_COLOR::ETBC_FLOAT_TRANSPARENT_BLACK || sampler.MaxFilter!=ISampler::E_TEXTURE_FILTER::ETF_LINEAR) + return false; + using clamp_e = hlsl::TextureClamp; + if (sampler.TextureWrapW!=clamp_e::ETC_CLAMP_TO_EDGE) + return false; + // there's a limited set of symmetries we can exploit in our IES tabulations, TODO: check which (probably not REPEAT) + hasher << profile; + return true; } public: @@ -301,7 +468,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // 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 - typed_pointer_type> profile = {}; + SParameter profile = {}; hlsl::float32_t3x3 profileTransform = hlsl::float32_t3x3( 1,0,0, 0,1,0, @@ -355,7 +522,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! class CDeltaTransmission final : public IBxDF { // nothing to do - inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override {} + inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override {return true;} public: inline EFinalType getFinalType() const override {return EFinalType::CDeltaTransmission;} @@ -366,9 +533,10 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! }; class IBxDFWithNDF : public IBxDF { - inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override + inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override { assert(false); // unimplemented + return false; } public: @@ -401,9 +569,10 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // TODO use CParameters<1> or CParameters<3> + colorpsace semantics (part of `CSpectralVariable`) class CConstantFactor final : public obj_pool_type::INonTrivial, public IFactorLeaf { - inline void computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override + inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override { assert(false);// TODO: hash the parameter + return false; } public: @@ -415,6 +584,22 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! inline CConstantFactor() = default; }; #undef TYPE_NAME_STR +#undef HASH_THE_HASH + + // + struct SBasicNodes final + { + public: + inline bool operator==(const SBasicNodes& other) const = default; + + typed_pointer_type blackHoleBxDF = {}; + typed_pointer_type errorBxDF = {}; + + private: + friend class CTrueIR; + NBL_API2 SBasicNodes(CTrueIR* ir); + }; + const SBasicNodes& getBasicNodes() const {return m_basicNodes;} // // inline typed_pointer_type createConstant() @@ -535,19 +720,32 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! const CTrueIR* m_ir; }; - struct SBasicNodes final + // Use `_count` instead of `Count` because of how wonkily this stuff gets used + template + static inline void printDotParameterSet(const SParameterSet& _set, const uint8_t _count, std::ostringstream& sstr, const core::string& selfID, StringConstIterator paramNameBegin={}, const bool uvRequired=false) { - public: - inline bool operator==(const SBasicNodes& other) const = default; - - typed_pointer_type blackHoleBxDF = {}; - typed_pointer_type errorBxDF = {}; - - private: - friend class CTrueIR; - NBL_API2 SBasicNodes(CTrueIR* ir); - }; - const SBasicNodes& getBasicNodes() const {return m_basicNodes;} + bool imageUsed = false; + for (uint8_t i=0; i<_count; i++) + { + const auto paramID = selfID+"_param"+std::to_string(i); + if (_set.params[i].view) + imageUsed = true; + _set.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(_set.uvSlot()) << "\\n"; + printMatrix(sstr,*reinterpret_cast(_set.params+_count)); + sstr << "\"]"; + sstr << "\n\t" << selfID << " -> " << uvTransformID << "[label=\"UV Transform\"]"; + } + } protected: NBL_API2 CTrueIR(creation_params_type&& params); @@ -564,6 +762,132 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! NBL_ENUM_ADD_BITWISE_OPERATORS(CTrueIR::SMaterial::EMetadataBits) +inline void CTrueIR::SParameter::printDot(std::ostringstream& sstr, const core::string& selfID) const +{ + sstr << "\n\t" << selfID << "[label=\"scale = " << std::to_string(scale); + if (view) + { + sstr << "\\nchannel = " << std::to_string(viewChannel); + const auto& viewParams = view->getCreationParameters(); + sstr << "\\nWraps = {" << sampler.TextureWrapU; + if (viewParams.viewType!=ICPUImageView::ET_1D && viewParams.viewType!=ICPUImageView::ET_1D_ARRAY) + sstr << "," << sampler.TextureWrapV; + if (viewParams.viewType==ICPUImageView::ET_3D) + sstr << "," << sampler.TextureWrapW; + sstr << "}\\nBorder = " << sampler.BorderColor; + // don't bother printing the rest, we really don't care much about those + } + sstr << "\"]"; + // TODO: do specialized printing for image views (they need to be gathered into a view set -> need a printing context struct) + /* + struct SDotPrintContext + { + std::ostringstream* sstr; + core::unordered_map* usedViews; + uint16_t indentation = 0; + }; + */ + if (view) + sstr << "\n\t" << selfID << " -> _view_" << std::to_string(reinterpret_cast(view)); +} + +inline void CTrueIR::SBasicNDFParams::printDot(std::ostringstream& sstr, const core::string& selfID) const +{ + constexpr const char* paramSemantics[] = { + "dh/du", + "dh/dv", + "alpha_u", + "alpha_v" + }; + SParameterSet<4>::printDot(sstr,selfID,paramSemantics,!definitelyIsotropic()); + if (!stretchInvariant()) + { + const auto referenceID = selfID+"_reference"; + sstr << "\n\t" << referenceID << " [label=\""; + printMatrix(sstr,reference); + sstr << "\"]"; + sstr << "\n\t" << selfID << " -> " << referenceID << " [label=\"Stretch Reference\"]"; + } +} + +// specialization of parameter hashing +template +struct core::blake3_hasher::update_impl +{ + using input_t = asset::material_compiler3::CTrueIR::SParameter; + + static inline void __call(blake3_hasher& hasher, const input_t& param) + { + hasher << param.scale; + if (!param.view) + return; + const auto& viewParams = param.view->getCreationParameters(); + // TODO: hash it like CAssetConverter + { + hasher << ptrdiff_t(param.view.get()); + } + // in the future this might change + hasher << param.viewChannel; + const auto& sampler = param.sampler; + hasher << sampler.BorderColor; + hasher << sampler.MaxFilter; + using view_type_e = asset::IImageView::E_TYPE; + switch (viewParams.viewType) + { + case view_type_e::ET_3D: + hasher << sampler.TextureWrapW; + [[fallthrough]]; + case view_type_e::ET_2D: [[fallthrough]]; + case view_type_e::ET_2D_ARRAY: [[fallthrough]]; + case view_type_e::ET_CUBE_MAP: [[fallthrough]]; + case view_type_e::ET_CUBE_MAP_ARRAY: + hasher << sampler.TextureWrapV; + [[fallthrough]]; + default: + hasher << sampler.TextureWrapU; + break; + } + } +}; +template +struct core::blake3_hasher::update_impl,Dummy> +{ + using input_t = asset::material_compiler3::CTrueIR::SParameterSet; + + static inline void __call(blake3_hasher& hasher, const input_t& input) + { + bool noTextures = true; + for (uint8_t i=0; i +struct core::blake3_hasher::update_impl +{ + using input_t = asset::material_compiler3::CTrueIR::SBasicNDFParams; + + static inline void __call(blake3_hasher& hasher, const input_t& input) + { + using type_e = input_t::EParamType; + const type_e type = input.determineParamType(); + update_impl::__call(hasher,static_cast(type)); + update_impl>::__call(hasher,*this); + // reference stretch can be applied on non-mapped NDFs too + if (!input.stretchInvariant()) + hasher << input.reference; + } +}; + } // namespace nbl::asset::material_compiler3 #endif diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index eadd5036dc..92978ae971 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -516,7 +516,7 @@ core::string CFrontendIR::CSpectralVariable::getLabelSuffix() const void CFrontendIR::CSpectralVariable::printDot(std::ostringstream& sstr, const core::string& selfID) const { auto pWonky = reinterpret_cast*>(this+1); - CNodePool::printDotParameterSet(pWonky->knots,getKnotCount(),sstr,selfID,{}); + CTrueIR::printDotParameterSet(pWonky->knots,getKnotCount(),sstr,selfID,{}); } void CFrontendIR::CEmitter::printDot(std::ostringstream& sstr, const core::string& selfID) const @@ -552,18 +552,10 @@ void CFrontendIR::CCookTorrance::printDot(std::ostringstream& sstr, const core:: auto CFrontendIR::CEmitter::createIRNode(const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t { auto& irPool = ir->getObjectPool(); - const auto profileH = irPool.emplace>(); - if (auto* const profile=irPool.deref(profileH); profile) - { - return {}; // unimplemented -// profile->paramSet = profile; - } - else - return {}; const auto retval = irPool.emplace(); if (auto* const contributor=irPool.deref(retval)) { - contributor->profile = profileH; + contributor->profile = profile; contributor->profileTransform = profileTransform; } return retval; From bf7d8469e1cd0c75c2c23232a917059fb9eee381 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Thu, 30 Apr 2026 01:32:53 +0200 Subject: [PATCH 15/45] make a `CtrueIR::ISpectralVariable` and move the semantics enum there, stuff the Cook Torrance NDF and eta reciprocation into param.padding of the 3 params, move out contributor creation to a util function --- examples_tests | 2 +- .../asset/material_compiler3/CFrontendIR.h | 86 +++++----- .../nbl/asset/material_compiler3/CTrueIR.h | 155 +++++++++++++----- .../asset/material_compiler3/CFrontendIR.cpp | 148 +++++++++-------- 4 files changed, 231 insertions(+), 160 deletions(-) diff --git a/examples_tests b/examples_tests index 9b000e150c..d5ba19d879 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 9b000e150c98e902b57c4a4be240a047490eddf4 +Subproject commit d5ba19d879675aca3072d646b88f8894286de8c1 diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index b118dc96f3..12d7c5cb0a 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -226,19 +226,7 @@ class CFrontendIR final : public CNodePool 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 - }; + using semantics_e = CTrueIR::ISpectralVariable::ESemantics; // template @@ -248,24 +236,24 @@ class CFrontendIR final : public CNodePool CTrueIR::SParameterSet knots = {}; // a little bit of abuse and padding reuse - static_assert(sizeof(CTrueIR::SParameter::padding)>2); template requires (Enable==(Count>1)) - Semantics& getSemantics() {return reinterpret_cast(knots.params[0].padding[2]); } + semantics_e& getSemantics() {return reinterpret_cast(knots.params[1].padding[0]); } template requires (Enable==(Count>1)) - const Semantics& getSemantics() const {return const_cast(const_cast*>(this)->getSemantics());} + const semantics_e& getSemantics() const {return const_cast(const_cast*>(this)->getSemantics());} }; // template inline CSpectralVariable(SCreationParams&& params) { // back up the count + static_assert(sizeof(CTrueIR::SParameter::padding)>1); params.knots.params[0].padding[1] = Count; // set it correctly for monochrome if constexpr (Count==1) - params.knots.params[0].padding[2] = static_cast(Semantics::NoneUndefined); + params.knots.params[1].padding[0] = static_cast(semantics_e::NoneUndefined); else { - assert(params.getSemantics()!=Semantics::NoneUndefined); + assert(params.getSemantics()!=semantics_e::NoneUndefined); } std::construct_at(reinterpret_cast*>(this+1),std::move(params)); } @@ -282,14 +270,13 @@ class CFrontendIR final : public CNodePool inline uint8_t getKnotCount() const { static_assert(sizeof(CTrueIR::SParameter::padding)>1); - return paramsBeginPadding()[1]; + return pWonky()->knots.params[0].padding[1]; } - inline Semantics getSemantics() const + inline semantics_e getSemantics() const { - static_assert(sizeof(CTrueIR::SParameter::padding)>2); - const auto retval = static_cast(paramsBeginPadding()[2]); - assert((getKnotCount()==1)==(retval==Semantics::NoneUndefined)); - return retval; + if (getKnotCount()<2) + return semantics_e::NoneUndefined; + return static_cast(pWonky()->knots.params[1].padding[0]); } // @@ -334,11 +321,11 @@ class CFrontendIR final : public CNodePool 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: + case semantics_e::Fixed3_SRGB: [[fallthrough]]; + case semantics_e::Fixed3_DCI_P3: [[fallthrough]]; + case semantics_e::Fixed3_BT2020: [[fallthrough]]; + case semantics_e::Fixed3_AdobeRGB: [[fallthrough]]; + case semantics_e::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(semantic),knotCount); @@ -364,7 +351,6 @@ class CFrontendIR final : public CNodePool private: SCreationParams<1>* pWonky() {return reinterpret_cast*>(this+1);} const SCreationParams<1>* pWonky() const {return reinterpret_cast*>(this+1);} - const uint8_t* paramsBeginPadding() const {return pWonky()->knots.params[0].padding;} }; // class IUnaryOp : public obj_pool_type::INonTrivial, public IExprNode @@ -639,7 +625,10 @@ class CFrontendIR final : public CNodePool 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; + inline COrenNayar() + { + ndParams.getDistribution() = CTrueIR::SBasicNDFParams::EDistribution::Invalid; + } CTrueIR::SBasicNDFParams ndParams = {}; @@ -658,25 +647,19 @@ class CFrontendIR final : public CNodePool 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; + + inline CCookTorrance() + { + ndParams.getDistribution() = CTrueIR::SBasicNDFParams::EDistribution::GGX; + } + + inline bool isEtaReciprocal() const {return ndParams.params[2].padding[0];} + inline void setEtaReciprocal(const bool value) {ndParams.params[2].padding[0] = value;} CTrueIR::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. + // See the comments in CTrueIR about this on a matching class typed_pointer_type orientedRealEta = {}; - // TODO: these could be put in the padding of `ndParams.params` - NDF ndf : 7 = NDF::GGX; - uint8_t reciprocateEta : 1 = false; protected: COPY_DEFAULT_IMPL @@ -689,10 +672,13 @@ class CFrontendIR final : public CNodePool inline bool reciprocatable() const override {return true;} inline void reciprocate(IExprNode* dst) const override { - (*static_cast(dst) = *this).reciprocateEta = ~reciprocateEta; + (*static_cast(dst) = *this).setEtaReciprocal(!isEtaReciprocal()); } - inline core::string getLabelSuffix() const override {return ndf!=NDF::GGX ? "\\nNDF = Beckmann":"\\nNDF = GGX";} + inline core::string getLabelSuffix() const override + { + return ndParams.getDistribution()!=CTrueIR::SBasicNDFParams::EDistribution::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; @@ -876,6 +862,8 @@ class CFrontendIR final : public CNodePool NBL_API2 CTrueIR::typed_pointer_type makeContributors(const CFrontendIR::typed_pointer_type bxdfRootH); + NBL_API2 CTrueIR::typed_pointer_type popContributor(); + // inputs to the addMaterials function const SAddMaterialsArgs& args; // for rewriting AST expressions @@ -929,7 +917,7 @@ class CFrontendIR final : public CNodePool { inline bool notVisited() const {return !visited;} - const CFrontendIR::IExprNode* node; + CFrontendIR::typed_pointer_type nodeH; // the ancestor ADD node to go back to if we hit a 0 MUL, or if our ADD or any other node becomes 0 uint16_t nonMulImmediateAncestorStackEnd = 0; // the length of the `mulChain` at the time we first visited the node diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 8af2bafe0b..235659bdab 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -57,8 +57,8 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // at this stage we store the multipliers in highest precision float scale = std::numeric_limits::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? + uint16_t viewChannel : 2 = 0; + uint8_t padding[2] = {0,0}; // TODO: padding stores metadata, shall we exclude from assignment and copy operators? core::smart_refctd_ptr view = {}; // lodbias and clamp shadow comparison functions, anisotropy and minFilter are ignored // NOTE: could take only things that matter from the sampler and pack the viewChannel and reduce padding @@ -103,6 +103,15 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // TODO: should really be 5 parameters (2+3) cause of rotatable anisotropic roughness struct SBasicNDFParams : SParameterSet<4> { + enum class EDistribution : uint8_t + { + GGX = 0, + Beckmann = 1, + Invalid = 255 + }; + inline EDistribution& getDistribution() {return reinterpret_cast(params[1].padding[0]);} + inline const EDistribution& getDistribution() const {return reinterpret_cast(params[1].padding[0]);} + inline auto getDerivMap() {return std::span(params,2);} inline auto getDerivMap() const {return std::span(params,2);} inline auto getRougness() {return std::span(params+2,2);} @@ -110,6 +119,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! inline SBasicNDFParams() { + getDistribution() = EDistribution::Invalid; // initialize with constant flat deriv map and smooth roughness for (auto& param : params) param.scale = 0.f; @@ -183,14 +193,13 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! CWeightedContributor, CEmitter, CDeltaTransmission, - CParameters_1, - CParameters_2, - CParameters_3, - CParameters_4, + CSpectralVariable_1, + CSpectralVariable_2, + CSpectralVariable_3, + CSpectralVariable_4, COrenNayar, CCookTorrance, - CFactorCombiner, - CConstantFactor + CFactorCombiner }; virtual EFinalType getFinalType() const = 0; @@ -413,28 +422,73 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // this node must be non-null until the last layer typed_pointer_type firstTransmission = {}; }; + // + class IFactorLeaf : public IFactor + { + public: + inline bool isScalar() const {return getSpectralBins()==1;} - // TODO: break this into UV-sample-able params and regular params - template - class CParameters final : public obj_pool_type::INonTrivial, public INode + virtual uint8_t getSpectralBins() const = 0; + }; + class ISpectralVariable : public IFactorLeaf + { + public: + enum class ESemantics : 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 + class CSpectralVariable final : public obj_pool_type::INonTrivial, public ISpectralVariable { inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override { hasher << paramSet; + if constexpr (SpectralBins>1) + { + if (!getSemantics()==ESemantics::NoneUndefined) + return false; + hasher << getSemantics(); + } return true; } public: inline EFinalType getFinalType() const override { - static_assert(1<=Channels && Channels<=4); - return static_cast(static_cast(EFinalType::CParameters_1)-1+Channels); + static_assert(1<=SpectralBins && SpectralBins<=4); + return static_cast(static_cast(EFinalType::CSpectralVariable_1)-1+SpectralBins); } - + // TODO: improve the token pasting here - inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CParameters);} + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CSpectralVariable);} + + // + inline uint8_t getSpectralBins() const {return SpectralBins;} + + // + template requires (N>1 && N==SpectralBins) + inline ESemantics getSemantics() const {return static_cast(paramSet.params[1].padding[0]);} + template requires (N>1 && N==SpectralBins) + inline void setSemantics(const ESemantics value) {paramSet.params[1].padding[0] = static_cast(value);} - SParameterSet paramSet = {}; + // you can set the children later + inline CSpectralVariable() + { + if constexpr (SpectralBins>1) + setSemantics(ESemantics::Fixed3_SRGB); + } + + SParameterSet paramSet = {}; }; // Unit Radiance emitter modulated by an IES profile @@ -514,12 +568,12 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // Both this and Yining are faster because of the 2 BxDF evaluations instead of 3, and they only differ by the inter-facet shadowing and masking functions and the tangent facet normal is computed. // However an orthogonal microfacet will work better for BSDFs because the facet tangents and normals form a square and not a rhombus, which means that a V to the "left" of the perturbed normal // will always be to the "right" of the orthogonal microfacet, meaning we'll get a consistent refraction (both L=refract(V,facet) will curve away from -V in the same direction. - class IBxDF : public obj_pool_type::INonTrivial, public IContributor + class IBxDF : public IContributor { public: inline bool isEmitter() const override {return false;} }; - class CDeltaTransmission final : public IBxDF + class CDeltaTransmission final : public obj_pool_type::INonTrivial, public IBxDF { // nothing to do inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override {return true;} @@ -533,56 +587,66 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! }; class IBxDFWithNDF : public IBxDF { + protected: inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override { - assert(false); // unimplemented - return false; + hasher << ndfParams; + return true; } public: - // CParameters<4> ? - //CNodePool::SBasicNDFParams ndfParams; + SBasicNDFParams ndfParams = {}; }; - class COrenNayar final : public IBxDFWithNDF + class COrenNayar final : public obj_pool_type::INonTrivial, public IBxDFWithNDF { public: + inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override + { + if (ndfParams.getDistribution()!=SBasicNDFParams::EDistribution::Invalid) + return false; + IBxDFWithNDF::computeHash_impl(pool,hasher); + return true; + } + inline EFinalType getFinalType() const override {return EFinalType::COrenNayar;} inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(COrenNayar);} inline COrenNayar() = default; }; - class CCookTorrance final : public IBxDFWithNDF + class CCookTorrance final : public obj_pool_type::INonTrivial, public IBxDFWithNDF { + inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override + { + if (ndfParams.getDistribution()>SBasicNDFParams::EDistribution::Beckmann) + return false; + IBxDFWithNDF::computeHash_impl(pool,hasher); + hasher << static_cast(ndfParams.getDistribution()); + hasher << isEtaReciprocal(); + HASH_OPTIONALS_HASH(orientedRealEta); + return true; + } + public: inline EFinalType getFinalType() const override {return EFinalType::CCookTorrance;} inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CCookTorrance);} inline CCookTorrance() = default; + + // + inline bool isEtaReciprocal() const {return ndfParams.params[2].padding[0];} + inline void setEtaReciprocal(const bool value) {ndfParams.params[2].padding[0] = value;} + + // BTDF ONLY! 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. + typed_pointer_type orientedRealEta = {}; }; //! Parameter Nodes - // ScalarConstant - // SpectralConstant //! Basic factor nodes - class IFactorLeaf : public IFactor {}; - // TODO use CParameters<1> or CParameters<3> + colorpsace semantics (part of `CSpectralVariable`) - class CConstantFactor final : public obj_pool_type::INonTrivial, public IFactorLeaf - { - inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override - { - assert(false);// TODO: hash the parameter - return false; - } - - public: - inline EFinalType getFinalType() const override {return EFinalType::CConstantFactor;} - - inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CConstant);} - - // you can set the children later - inline CConstantFactor() = default; - }; #undef TYPE_NAME_STR #undef HASH_THE_HASH @@ -881,7 +945,8 @@ struct core::blake3_hasher::update_impl using type_e = input_t::EParamType; const type_e type = input.determineParamType(); update_impl::__call(hasher,static_cast(type)); - update_impl>::__call(hasher,*this); + update_impl>::__call(hasher,input); + hasher << input.getDistribution(); // reference stretch can be applied on non-mapped NDFs too if (!input.stretchInvariant()) hasher << input.reference; diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index 92978ae971..a4cfe2948d 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -401,7 +401,7 @@ auto CFrontendIR::createNamedFresnel(const std::string_view name) -> typed_point fr->debugInfo = getObjectPool().emplace(found->first); { CSpectralVariable::SCreationParams<3> params = {}; - params.getSemantics() = CSpectralVariable::Semantics::Fixed3_SRGB; + params.getSemantics() = CTrueIR::ISpectralVariable::ESemantics::Fixed3_SRGB; params.knots.params[0].scale = found->second.x.real(); params.knots.params[1].scale = found->second.y.real(); params.knots.params[2].scale = found->second.z.real(); @@ -409,7 +409,7 @@ auto CFrontendIR::createNamedFresnel(const std::string_view name) -> typed_point } { CSpectralVariable::SCreationParams<3> params = {}; - params.getSemantics() = CSpectralVariable::Semantics::Fixed3_SRGB; + params.getSemantics() = CTrueIR::ISpectralVariable::ESemantics::Fixed3_SRGB; params.knots.params[0].scale = found->second.x.imag(); params.knots.params[1].scale = found->second.y.imag(); params.knots.params[2].scale = found->second.z.imag(); @@ -568,31 +568,36 @@ auto CFrontendIR::CDeltaTransmission::createIRNode(const CFrontendIR* ast, CTrue auto CFrontendIR::COrenNayar::createIRNode(const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t { + if (ndParams.getDistribution()!=CTrueIR::SBasicNDFParams::EDistribution::Invalid) + return {}; auto& irPool = ir->getObjectPool(); const auto retval = irPool.emplace(); if (auto* const contributor = irPool.deref(retval)) - { - return {}; // unimplemented -// contributor->ndParams = ndParams; - } + contributor->ndfParams = ndParams; // the padding abuse is the same between the classes return retval; } auto CFrontendIR::CCookTorrance::createIRNode(const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t { - auto& irPool = ir->getObjectPool(); - return {}; // unimplemented -#if 0 - const auto etaH = irPool.emplace(); - if (!etaH) + if (ndParams.getDistribution()>CTrueIR::SBasicNDFParams::EDistribution::Beckmann) return {}; + auto& irPool = ir->getObjectPool(); + CTrueIR::typed_pointer_type etaH = {}; +#if 0 // TODO: pass the transmission info + if (isBTDF) + { + etaH = irPool.emplace>(); + if (!etaH) + return {}; + } +#endif const auto retval = irPool.emplace(); if (auto* const ct=irPool.deref(retval); ct) { - ct->ndParams = ndParams; + ct->ndfParams = ndParams; // the padding abuse is the same between the classes + ct->orientedRealEta = etaH; } return retval; -#endif } auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_pointer_type rootH, const CFrontendIR* _srcAST) -> oriented_material_t @@ -696,38 +701,28 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_pointer_type bxdfRootH) -> CTrueIR::typed_pointer_type { // debug what we're processing + SDotPrinter astPrinter(srcAST); auto printSubtree = [&](const CFrontendIR::typed_pointer_type nodeH)->void { - SDotPrinter printer(srcAST); - printer.exprStack.push(nodeH); - args.logger.log("Subtree Dot3 : \n%s\n", ELL_DEBUG,printer().c_str()); + assert(astPrinter.exprStack.empty()); + astPrinter.exprStack.push(nodeH); + args.logger.log("Subtree Dot3 : \n%s\n", ELL_DEBUG,astPrinter().c_str()); + assert(astPrinter.exprStack.empty()); }; using contributor_sum_handle_t = CTrueIR::typed_pointer_type; contributor_sum_handle_t headH = {}; if (!bxdfRootH) return headH; + // temporary debug for WIP printSubtree(bxdfRootH); - - // Multiplication Chain need to be sorted in a canonical order so its easier to spot them being the same - auto sortMuls = [](const SFactor& lhs, const SFactor& rhs)->bool - { - // monochrome is cheaper - if (lhs.monochrome!=rhs.monochrome) - return lhs.monochrome; - // not doing a complement is cheaper - if (lhs.handle.value==rhs.handle.value) - return lhs.negategetObjectPool(); auto& irPool = tmpIR->getObjectPool(); // scratches are initialized assert(mulChain.empty()); assert(contributorStack.empty()); - exprStack.push_back({.node=astPool.deref(bxdfRootH)}); + exprStack.push_back({.nodeH=bxdfRootH}); contributor_sum_handle_t tailH = {}; while (!exprStack.empty()) { @@ -742,15 +737,24 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin } #endif auto& entry = exprStack.back(); + const auto* const node = astPool.deref(entry.nodeH); using ast_expr_type_e = CFrontendIR::IExprNode::Type; - const ast_expr_type_e astExprType = entry.node->getType(); + const ast_expr_type_e astExprType = node->getType(); const bool isContributor = astExprType==CFrontendIR::IExprNode::Type::Contributor; // if (entry.notVisited()) { if (isContributor) { - contributorStack.push_back({.contributor=static_cast(entry.node)->createIRNode(srcAST,tmpIR.get())}); + const auto contributorH = static_cast(node)->createIRNode(srcAST,tmpIR.get()); + // TODO: recompute instead of compute + if (!contributorH || irPool.deref(contributorH)->computeHash(irPool)==core::blake3_hash_t{}) + { + args.logger.log("Failed to Create IR Contributor from AST",ELL_ERROR); + printSubtree(entry.nodeH); + assert(false); // unimplemented + } + contributorStack.push_back({.contributor=contributorH}); exprStack.pop_back(); } else @@ -765,7 +769,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin } const bool notMul = astExprType!=ast_expr_type_e::Mul; // go through children - const auto childCount = entry.node->getChildCount(); + const auto childCount = node->getChildCount(); // add in reverse so stack processes in order for (auto childIx=childCount; childIx; ) { @@ -778,7 +782,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin } // regular exploration exprStack.push_back({ - .node = astPool.deref(entry.node->getChildHandle(--childIx)), + .nodeH = node->getChildHandle(--childIx), // to be able to go back to the non mul that is supposed to add our subtree .nonMulImmediateAncestorStackEnd = notMul ? static_cast(exprStack.size()):entry.nonMulImmediateAncestorStackEnd }); @@ -804,30 +808,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin headH = tailH; } // add current contributor with weight to BxDF Sum - { - const auto weightedH = irPool.emplace(); - irPool.deref(tailH)->product = weightedH; - auto* weighted = irPool.deref(weightedH); - weighted->contributor = contributorStack.back().contributor; - if (!mulChain.empty()) - { - const CTrueIR::CFactorCombiner::SState combinerState = { - .type = CTrueIR::CFactorCombiner::Type::Mul, - .childCount = mulChain.size() - }; - // TODO: create the combiner node - //const auto factorH = getObjectPool().emplace(); - { - // every contributor node gets its own SORTED ancestor prefix - mulChainSortScratch = mulChain; - std::sort(mulChainSortScratch.begin(),mulChainSortScratch.end(),sortMuls); - //auto oit = getObjectPool().deref(factorH)->child; - //for (const auto& mul : mulChainSortScratch) - //*(oit++) = mul.handle; - } - //weighted->factor = factorH; - } - } + irPool.deref(tailH)->product = popContributor(); // when we are done we need to reset the mul chain back to its original state mulChain.resize(entry.mulChainLen); break; @@ -845,20 +826,57 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin assert(contributorStack.size()==1); // add the contributor headH = irPool.emplace(); - { - const auto weightedH = irPool.emplace(); - irPool.deref(weightedH)->contributor = contributorStack.back().contributor; - irPool.deref(headH)->product = weightedH; - // TODO: do the factor as a mul chain! - } + irPool.deref(headH)->product = popContributor(); mulChain.clear(); - contributorStack.clear(); } // we got all the AST ADD nodes on the way back out assert(mulChain.empty()); return headH; } +// +auto CFrontendIR::SAdd2IRSession::popContributor() -> CTrueIR::typed_pointer_type +{ + auto& irPool = tmpIR->getObjectPool(); + // + const auto retval = irPool.emplace(); + auto* weighted = irPool.deref(retval); + weighted->contributor = contributorStack.back().contributor; + contributorStack.pop_back(); + if (!mulChain.empty()) + { + assert(false); // unimplemented + const CTrueIR::CFactorCombiner::SState combinerState = { + .type = CTrueIR::CFactorCombiner::Type::Mul, + .childCount = mulChain.size() + }; + // TODO: create the combiner node + //const auto factorH = getObjectPool().emplace(); + { + // every contributor node gets its own SORTED ancestor prefix + mulChainSortScratch = mulChain; + // Multiplication Chain need to be sorted in a canonical order so its easier to spot them being the same + auto sortMuls = [](const SFactor& lhs, const SFactor& rhs)->bool + { + // monochrome is cheaper + if (lhs.monochrome!=rhs.monochrome) + return lhs.monochrome; + // not doing a complement is cheaper + if (lhs.handle.value==rhs.handle.value) + return lhs.negatechild; + //for (const auto& mul : mulChainSortScratch) + //*(oit++) = mul.handle; + } + //weighted->factor = factorH; + } + return retval; +} + // CTrueIR::SMaterialHandle CFrontendIR::makeFinalIR(const typed_pointer_type rootH, SAdd2IRSession& session) const { From bc2295b442c9f15ad009abfb82524c807f427e8b Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Fri, 1 May 2026 09:41:12 +0200 Subject: [PATCH 16/45] make things compile nicely and differentiate BTDF and BRDF --- .../asset/material_compiler3/CFrontendIR.h | 14 ++-- .../nbl/asset/material_compiler3/CTrueIR.h | 18 ++-- .../asset/material_compiler3/CFrontendIR.cpp | 82 +++++++++++++------ src/nbl/asset/material_compiler3/CTrueIR.cpp | 2 +- 4 files changed, 77 insertions(+), 39 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index 12d7c5cb0a..02030e3381 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -215,7 +215,7 @@ class CFrontendIR final : public CNodePool protected: friend class CFrontendIR; using ir_contributor_handle_t = CTrueIR::typed_pointer_type; - virtual ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR* ir) const = 0; + virtual ir_contributor_handle_t createIRNode(const bool forBTDF, const CFrontendIR* ast, CTrueIR* ir) const = 0; }; // This node could also represent non directional emission, but we have another node for that @@ -263,6 +263,9 @@ class CFrontendIR final : public CNodePool } // encapsulation due to padding abuse + inline auto& uvTransform() {return pWonky()->knots.uvTransform;} + inline const auto& uvTransform() const {return pWonky()->knots.uvTransform;} + inline uint8_t& uvSlot() {return pWonky()->knots.uvSlot();} inline const uint8_t& uvSlot() const {return pWonky()->knots.uvSlot();} @@ -444,7 +447,7 @@ class CFrontendIR final : public CNodePool NBL_API2 void printDot(std::ostringstream& sstr, const core::string& selfID) const override; - NBL_API2 ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR* ir) const; + NBL_API2 ir_contributor_handle_t createIRNode(const bool forBTDF, const CFrontendIR* ast, CTrueIR* ir) const; }; //! 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. @@ -616,7 +619,7 @@ class CFrontendIR final : public CNodePool protected: COPY_DEFAULT_IMPL - NBL_API2 ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR* ir) const; + NBL_API2 ir_contributor_handle_t createIRNode(const bool forBTDF, const CFrontendIR* ast, CTrueIR* ir) const; }; //! 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 @@ -639,7 +642,7 @@ class CFrontendIR final : public CNodePool NBL_API2 void printDot(std::ostringstream& sstr, const core::string& selfID) const override; - NBL_API2 ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR* ir) const; + NBL_API2 ir_contributor_handle_t createIRNode(const bool forBTDF, const CFrontendIR* ast, CTrueIR* ir) const; }; // Supports anisotropy for all models class CCookTorrance final : public IBxDF @@ -682,7 +685,7 @@ class CFrontendIR final : public CNodePool 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; - NBL_API2 ir_contributor_handle_t createIRNode(const CFrontendIR* ast, CTrueIR* ir) const; + NBL_API2 ir_contributor_handle_t createIRNode(const bool forBTDF, const CFrontendIR* ast, CTrueIR* ir) const; }; #undef COPY_DEFAULT_IMPL #undef TYPE_NAME_STR @@ -872,6 +875,7 @@ class CFrontendIR final : public CNodePool core::smart_refctd_ptr tmpIR; // changes dynamically const CFrontendIR* srcAST; + bool btdfSubtree = false; // for going over layers in the AST core::vector layerStack; // Some of the things we must canonicalize: diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 235659bdab..4748ea4401 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -455,9 +455,10 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! hasher << paramSet; if constexpr (SpectralBins>1) { - if (!getSemantics()==ESemantics::NoneUndefined) + const ESemantics semantics = getSemantics(); + if (semantics!=ESemantics::NoneUndefined) return false; - hasher << getSemantics(); + hasher << semantics; } return true; } @@ -485,7 +486,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! inline CSpectralVariable() { if constexpr (SpectralBins>1) - setSemantics(ESemantics::Fixed3_SRGB); + setSemantics(ESemantics::Fixed3_SRGB); } SParameterSet paramSet = {}; @@ -680,13 +681,12 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! NotBlackhole = 0x1u<<0, // actually have a material NonDelta = 0x1u<<1, // can evaluate against point lights (or other samplings) DeltaTransmissive = 0x1u<<2, // can use stochastic transparency for closest hit rays and blending for anyhit - Emissive = 0x1u<<3, // maybe register for NEE, but definitely grab the emission - NonSpatiallyVaryingEmissive = 0x1u<<4, // definitely register for NEE - // TODO: 5,6 left + NonSpatiallyVaryingEmissive = 0x1u<<3, // definitely register for NEE + SpatiallyVaryingEmissive = 0x1u<<4, // maybe register for NEE but needs different kind of NEE + // TODO: 5,6,7 left // Bits that help us remove expensive code from impl - DerivativeMap = 0x1u<<7, - DirectionallyVaryingEmissive = 0x1u<<8, // IES profile - SpatiallyVaryingEmissive = 0x1u<<9, // textured light + DerivativeMap = 0x1u<<8, + DirectionallyVaryingEmissive = 0x1u<<9, // IES profile Lambertian = 0x1u<<10, OrenNayar = 0x1u<<11, GGX = 0x1u<<12, diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index a4cfe2948d..da3b5edecc 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -549,8 +549,9 @@ void CFrontendIR::CCookTorrance::printDot(std::ostringstream& sstr, const core:: } //! AST-> IR methods -auto CFrontendIR::CEmitter::createIRNode(const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t +auto CFrontendIR::CEmitter::createIRNode(const bool forBTDF, const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t { + assert(!forBTDF); auto& irPool = ir->getObjectPool(); const auto retval = irPool.emplace(); if (auto* const contributor=irPool.deref(retval)) @@ -561,12 +562,13 @@ auto CFrontendIR::CEmitter::createIRNode(const CFrontendIR* ast, CTrueIR* ir) co return retval; } -auto CFrontendIR::CDeltaTransmission::createIRNode(const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t +auto CFrontendIR::CDeltaTransmission::createIRNode(const bool forBTDF, const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t { + assert(forBTDF); return ir->getObjectPool().emplace(); } -auto CFrontendIR::COrenNayar::createIRNode(const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t +auto CFrontendIR::COrenNayar::createIRNode(const bool forBTDF, const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t { if (ndParams.getDistribution()!=CTrueIR::SBasicNDFParams::EDistribution::Invalid) return {}; @@ -577,20 +579,44 @@ auto CFrontendIR::COrenNayar::createIRNode(const CFrontendIR* ast, CTrueIR* ir) return retval; } -auto CFrontendIR::CCookTorrance::createIRNode(const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t +auto CFrontendIR::CCookTorrance::createIRNode(const bool forBTDF, const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t { if (ndParams.getDistribution()>CTrueIR::SBasicNDFParams::EDistribution::Beckmann) return {}; auto& irPool = ir->getObjectPool(); CTrueIR::typed_pointer_type etaH = {}; -#if 0 // TODO: pass the transmission info - if (isBTDF) + if (forBTDF) { - etaH = irPool.emplace>(); - if (!etaH) + if (!orientedRealEta) return {}; + const auto* const srcEta = ast->getObjectPool().deref(orientedRealEta); + switch (srcEta->getSemantics()) + { + case CSpectralVariable::semantics_e::NoneUndefined: + { + assert(srcEta->getKnotCount()==1); + const auto handle = irPool.emplace>(); + auto* const dstEta = irPool.deref(handle); + if (!dstEta) + return {}; + dstEta->paramSet.uvTransform = srcEta->uvTransform(); + dstEta->paramSet.params[0] = *srcEta->getParam(0); + break; + } + default: + { + assert(srcEta->getKnotCount()==3); + const auto handle = irPool.emplace>(); + auto* const dstEta = irPool.deref(handle); + if (!dstEta) + return {}; + dstEta->paramSet.uvTransform = srcEta->uvTransform(); + for (auto i=0; i<3; i++) + dstEta->paramSet.params[i] = *srcEta->getParam(i); + break; + } + } } -#endif const auto retval = irPool.emplace(); if (auto* const ct=irPool.deref(retval); ct) { @@ -607,6 +633,7 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ srcAST = _srcAST; const auto& astPool = srcAST->getObjectPool(); assert(layerStack.empty()); + auto clearLayerStackOnExit = core::makeRAIIExiter([this]()->void{layerStack.clear();}); auto& irPool = tmpIR->getObjectPool(); // go down through layers and enqueue them so the layers can be added in reverse @@ -618,17 +645,20 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ // if there's literally nothing on the top level, you can't get to the next layer to retroreflect from it if (noTopReflection && noTransmission) { - args.logger.log("Skipping current layer and farther ones due to no transmission and reflection",ELL_DEBUG); + if (layer->coated) + args.logger.log("Skipping current layer and farther ones due to no transmission and reflection",ELL_DEBUG); break; } layerStack.push_back(layer); // find out rest of the layers don't matter because they're blocked from being seen, its not a complete check if (noTransmission) { - args.logger.log("Skipping remaining layers due to no transmission",ELL_DEBUG); + if (layer->coated) + args.logger.log("Skipping remaining layers due to no transmission",ELL_DEBUG); break; } } + const auto errorBxDF = tmpIR->getBasicNodes().errorBxDF; // Some metadata needed for us bool layersBelowCanScatterBack = false; CTrueIR::typed_pointer_type prevLayerH = {}; @@ -642,18 +672,28 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ auto* const outLayer = irPool.deref(layerH); // process the top BRDF outLayer->brdfTop = makeContributors(inLayer->brdfTop); + if (outLayer->brdfTop==errorBxDF) + return {}; // TODO: error material // process the BTDF + btdfSubtree = true; const auto btdfH = makeContributors(inLayer->btdf); + btdfSubtree = false; // because we're oriented, the bottom brdf can't exist without a BTDF on top (there's no ray that can reach it from our oriented side) if (btdfH) { + if (btdfH==errorBxDF) + return {}; // TODO: error material const auto transmissionH = irPool.emplace(); { auto* const transmission = irPool.deref(transmissionH); transmission->btdf = btdfH; // Only if we have a layer below us capable of reflecting the ray back, do we care about the bottom BRDF (you can't hit it otherwise) if (layersBelowCanScatterBack) + { transmission->brdfBottom = makeContributors(inLayer->brdfBottom); + if (transmission->brdfBottom==errorBxDF) + return {}; // TODO: error material + } // we check if previous layer didn't get oprimized away, but we don't add its optimized version because don't want pointers across two pools (crash) if (retval.root) transmission->coated = prevLayerH; @@ -683,7 +723,6 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ layersBelowCanScatterBack = false; } } - layerStack.clear(); // last touch ups if (retval.root) { @@ -726,16 +765,6 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin contributor_sum_handle_t tailH = {}; while (!exprStack.empty()) { - // how to report an error -#if 0 - { - args.logger.log("MESSAGE",ELL_ERROR); - exprStack.clear(); - mulChain.clear(); - contributorStack.clear(); - return tmpIR->getBasicNodes().errorBxDF; - } -#endif auto& entry = exprStack.back(); const auto* const node = astPool.deref(entry.nodeH); using ast_expr_type_e = CFrontendIR::IExprNode::Type; @@ -746,13 +775,17 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin { if (isContributor) { - const auto contributorH = static_cast(node)->createIRNode(srcAST,tmpIR.get()); + const auto contributorH = static_cast(node)->createIRNode(btdfSubtree,srcAST,tmpIR.get()); // TODO: recompute instead of compute if (!contributorH || irPool.deref(contributorH)->computeHash(irPool)==core::blake3_hash_t{}) { args.logger.log("Failed to Create IR Contributor from AST",ELL_ERROR); printSubtree(entry.nodeH); - assert(false); // unimplemented + // no point pushing an error contributor, don't want a best effort compilation within a layer, don't want contributors missing or substituted + exprStack.clear(); + mulChain.clear(); + contributorStack.clear(); + return tmpIR->getBasicNodes().errorBxDF; } contributorStack.push_back({.contributor=contributorH}); exprStack.pop_back(); @@ -882,6 +915,7 @@ CTrueIR::SMaterialHandle CFrontendIR::makeFinalIR(const typed_pointer_type brdfs; // core::unordered_map btdfs; diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp index 3a4a06df3f..d8266f8588 100644 --- a/src/nbl/asset/material_compiler3/CTrueIR.cpp +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -87,7 +87,7 @@ CTrueIR::SBasicNodes::SBasicNodes(CTrueIR* ir) bool CTrueIR::rewrite(SMaterial& material, CTrueIR* srcIR) { // TODO: hash, deduplicate, collect metadata and insert into current IR - return true; + return false; } From 11aed54faa3463913a0d7ddd6cae46df915c818a Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Fri, 1 May 2026 12:11:09 +0200 Subject: [PATCH 17/45] reciprocation fixes, move more stuff into the session, add enum for CFrontendIR::CSpectralVariable, epic debug mesages --- .../asset/material_compiler3/CFrontendIR.h | 224 +++++++++++------- .../nbl/asset/material_compiler3/CTrueIR.h | 2 + .../asset/material_compiler3/CFrontendIR.cpp | 126 ++++++---- 3 files changed, 227 insertions(+), 125 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index 02030e3381..0f37d5d44e 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -150,6 +150,7 @@ class CFrontendIR final : public CNodePool Mul = 1, Add = 2, Complement = 3, + SpectralVariable = 4, Other = 5 }; virtual inline Type getType() const {return Type::Other;} @@ -226,8 +227,10 @@ class CFrontendIR final : public CNodePool inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CSpectralVariable);} // Variable length but has no children - using semantics_e = CTrueIR::ISpectralVariable::ESemantics; + // + inline IExprNode::Type getType() const override {return Type::SpectralVariable;} + using semantics_e = CTrueIR::ISpectralVariable::ESemantics; // template struct SCreationParams @@ -812,7 +815,7 @@ class CFrontendIR final : public CNodePool if (!rootH) // its a valid material (blackhole) *outIt = CTrueIR::BlackholeMaterialHandle; else if (valid(rootH,args.logger)) - *outIt = makeFinalIR(rootH,session); + *outIt = session.makeFinalIR(rootH,this); // now check for failure if (*outIt) retval++; @@ -824,6 +827,7 @@ class CFrontendIR final : public CNodePool struct SDotPrinter final { public: + inline SDotPrinter() = default; 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> roots) : m_ir(ir), layerStack(roots.rbegin(),roots.rend()) @@ -832,6 +836,14 @@ class CFrontendIR final : public CNodePool visitedNodes.reserve(roots.size()<<3); } + inline void reset(const CFrontendIR* ir) + { + visitedNodes.clear(); + layerStack.clear(); + exprStack.clear(); + m_ir = ir; + } + NBL_API2 void operator()(std::ostringstream& output); inline core::string operator()() { @@ -843,96 +855,108 @@ class CFrontendIR final : public CNodePool core::unordered_set> visitedNodes; // TODO: track layering depth and indent accordingly? core::vector> layerStack; - core::stack> exprStack; + core::vector> exprStack; private: - const CFrontendIR* m_ir; + const CFrontendIR* m_ir = nullptr; }; protected: using CNodePool::CNodePool; - struct SAdd2IRSession + struct SAdd2IRSession final { - inline SAdd2IRSession(const SAddMaterialsArgs& _args) : args(_args) - { - tmpAST = CFrontendIR::create({.composed={.blockSizeKBLog2=10},.maxBlocks=64}); - // give slightly more memory to IR, since the AST tends to be a bit more compact - tmpIR = CTrueIR::create({.composed={.blockSizeKBLog2=12},.maxBlocks=64}); - } + public: + inline SAdd2IRSession(const SAddMaterialsArgs& _args) : args(_args) + { + tmpAST = CFrontendIR::create({.composed={.blockSizeKBLog2=10},.maxBlocks=64}); + // give slightly more memory to IR, since the AST tends to be a bit more compact + tmpIR = CTrueIR::create({.composed={.blockSizeKBLog2=12},.maxBlocks=64}); + } - using oriented_material_t = CTrueIR::SMaterial::SOriented; - NBL_API2 oriented_material_t makeOrientedMaterial(const CFrontendIR::typed_pointer_type rootH, const CFrontendIR* _srcAST); - - NBL_API2 CTrueIR::typed_pointer_type makeContributors(const CFrontendIR::typed_pointer_type bxdfRootH); - - NBL_API2 CTrueIR::typed_pointer_type popContributor(); - - // inputs to the addMaterials function - const SAddMaterialsArgs& args; - // for rewriting AST expressions - core::smart_refctd_ptr tmpAST; - // for making IR nodes before we Merkle Hash them and remove duplicates (so main IR doesn't get bloated) - core::smart_refctd_ptr tmpIR; - // changes dynamically - const CFrontendIR* srcAST; - bool btdfSubtree = false; - // for going over layers in the AST - core::vector layerStack; - // Some of the things we must canonicalize: - // A ( f_0 (B + C) + D f_1 ) = f_0 B A + f_0 C A + f_1 D A - // Expression nodes of the Frontend AST really come in 4 variants: - // - add - // - mul - // - complement, which is equivalent to 1 ADD (-1 MUL x) - // - function/other - // BRDFs can appear only under ADD and MUL nodes in the AST not the function/other/complement, so if we want to canonicalize: - // 1. The Add above can be ignored, we form full multiplication chain to the top - // 2. Adds in sibling nodes (below the last add) cause us to have to add a factored copy to the IR - // DFS from right-to-left (inverse order of adding children to stack), would cause us to keep postifxes of the multiplier chain every time we descend into ADD. - // We want to essentially visit the parent ADD node again after dealing with its subtree (in-order traversal) then mul chain can be reset just to the parent. - // If we perform DFS stack push left-to-right, we'll know the contributor already for all the leaf nodes if we push it onto the stack. - // Then for all other leaf nodes we can accumulate them in the MUL chain, and adding their weighted contributor whenever we're back at an ADD node (be it the ancestor or sibling/cousin). - // If the contributor is null or multiplied with a null we can keep draining the stack until we're back at its immediate parent ADD node. - struct SContributor - { - // the "active" contributor, basically the leftmost item in the subbranch below and ADD - CTrueIR::typed_pointer_type contributor; - }; - core::vector contributorStack; - // Every time we encounter an AST leaf we must add the current contributor together with all the factors multiplied together - struct SFactor - { - using handle_t = CTrueIR::typed_pointer_type; - // We only track multiplicative factors, we break down every BRDF equally into the canonical form - handle_t handle; - uint8_t negate : 1 = false; - uint8_t monochrome : 1 = true; - // extend later when allowing variable bucket count - uint8_t liveSpectralChannels : 3 = 0b111; - }; - // here we keep the multiplication chain unsorted so its each to add/remove nodes as we encounter them - core::vector mulChain; - // scratch for sorting the mul chain before adding a contributor - core::vector mulChainSortScratch; - // By maintaining a hash map of AST nodes which simplify to a Constant (unity, or zero, or other) we could resolve the issue of the `nonMulImmediateAncestorStackEnd` - // which has us adding the same non-mul node multiple times to stack during the traversal. - // However how much of that would be moving IR manipulation into the AST ? - struct StackEntry - { - inline bool notVisited() const {return !visited;} - - CFrontendIR::typed_pointer_type nodeH; - // the ancestor ADD node to go back to if we hit a 0 MUL, or if our ADD or any other node becomes 0 - uint16_t nonMulImmediateAncestorStackEnd = 0; - // the length of the `mulChain` at the time we first visited the node - uint16_t mulChainLen = 0; - bool visited = false; - // only relevant for Add nodes - bool addContributor = false; - }; - core::vector exprStack; + NBL_API2 CTrueIR::SMaterialHandle makeFinalIR(const typed_pointer_type rootH, const CFrontendIR* ast); + + private: + inline void printSubtree(const CFrontendIR::typed_pointer_type nodeH) + { + assert(astPrinter.exprStack.empty()); + astPrinter.exprStack.push_back(nodeH); + args.logger.log("Subtree Dot3 : \n%s\n",system::ILogger::ELL_DEBUG,astPrinter().c_str()); + assert(astPrinter.exprStack.empty()); + } + + using oriented_material_t = CTrueIR::SMaterial::SOriented; + NBL_API2 oriented_material_t makeOrientedMaterial(const CFrontendIR::typed_pointer_type rootH, const CFrontendIR* _srcAST); + + NBL_API2 CTrueIR::typed_pointer_type makeContributors(const CFrontendIR::typed_pointer_type bxdfRootH); + + NBL_API2 CTrueIR::typed_pointer_type popContributor(); + + // inputs to the addMaterials function + const SAddMaterialsArgs& args; + // for rewriting AST expressions + core::smart_refctd_ptr tmpAST; + // for making IR nodes before we Merkle Hash them and remove duplicates (so main IR doesn't get bloated) + core::smart_refctd_ptr tmpIR; + // changes dynamically + const CFrontendIR* srcAST; + SDotPrinter astPrinter; + bool btdfSubtree = false; + // for going over layers in the AST + core::vector layerStack; + // Some of the things we must canonicalize: + // A ( f_0 (B + C) + D f_1 ) = f_0 B A + f_0 C A + f_1 D A + // Expression nodes of the Frontend AST really come in 4 variants: + // - add + // - mul + // - complement, which is equivalent to 1 ADD (-1 MUL x) + // - function/other + // BRDFs can appear only under ADD and MUL nodes in the AST not the function/other/complement, so if we want to canonicalize: + // 1. The Add above can be ignored, we form full multiplication chain to the top + // 2. Adds in sibling nodes (below the last add) cause us to have to add a factored copy to the IR + // DFS from right-to-left (inverse order of adding children to stack), would cause us to keep postifxes of the multiplier chain every time we descend into ADD. + // We want to essentially visit the parent ADD node again after dealing with its subtree (in-order traversal) then mul chain can be reset just to the parent. + // If we perform DFS stack push left-to-right, we'll know the contributor already for all the leaf nodes if we push it onto the stack. + // Then for all other leaf nodes we can accumulate them in the MUL chain, and adding their weighted contributor whenever we're back at an ADD node (be it the ancestor or sibling/cousin). + // If the contributor is null or multiplied with a null we can keep draining the stack until we're back at its immediate parent ADD node. + struct SContributor + { + // the "active" contributor, basically the leftmost item in the subbranch below and ADD + CTrueIR::typed_pointer_type contributor; + }; + core::vector contributorStack; + // Every time we encounter an AST leaf we must add the current contributor together with all the factors multiplied together + struct SFactor + { + using handle_t = CTrueIR::typed_pointer_type; + // We only track multiplicative factors, we break down every BRDF equally into the canonical form + handle_t handle; + uint8_t negate : 1 = false; + uint8_t monochrome : 1 = true; + // extend later when allowing variable bucket count + uint8_t liveSpectralChannels : 3 = 0b111; + }; + // here we keep the multiplication chain unsorted so its each to add/remove nodes as we encounter them + core::vector mulChain; + // scratch for sorting the mul chain before adding a contributor + core::vector mulChainSortScratch; + // By maintaining a hash map of AST nodes which simplify to a Constant (unity, or zero, or other) we could resolve the issue of the `nonMulImmediateAncestorStackEnd` + // which has us adding the same non-mul node multiple times to stack during the traversal. + // However how much of that would be moving IR manipulation into the AST ? + struct StackEntry + { + inline bool notVisited() const {return !visited;} + + CFrontendIR::typed_pointer_type nodeH; + // the ancestor ADD node to go back to if we hit a 0 MUL, or if our ADD or any other node becomes 0 + uint16_t nonMulImmediateAncestorStackEnd = 0; + // the length of the `mulChain` at the time we first visited the node + uint16_t mulChainLen = 0; + bool visited = false; + // only relevant for Add nodes + bool addContributor = false; + }; + core::vector exprStack; }; - NBL_API2 CTrueIR::SMaterialHandle makeFinalIR(const typed_pointer_type rootH, SAdd2IRSession& session) const; inline core::string getNodeID(const typed_pointer_type handle) const {return core::string("_")+std::to_string(handle.value);} inline core::string getLabelledNodeID(const typed_pointer_type handle) const @@ -952,6 +976,40 @@ class CFrontendIR final : public CNodePool return retval; } }; +} + +// specialize the `to_string +namespace nbl::system::impl +{ +template<> +struct to_string_helper +{ + using type = nbl::asset::material_compiler3::CFrontendIR::IExprNode::Type; + + static inline std::string __call(const type value) + { + switch (value) + { + case type::Contributor: + return "Contributor"; + case type::Mul: + return "Mul"; + case type::Complement: + return "Complement"; + case type::SpectralVariable: + return "SpectralVariable"; + case type::Other: + return "Other"; + default: + break; + } + return ""; + } +}; +} + +namespace nbl::asset::material_compiler3 +{ inline bool CFrontendIR::valid(const typed_pointer_type rootHandle, system::logger_opt_ptr logger) const { diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 4748ea4401..d5f95139fe 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -6,6 +6,7 @@ #include "nbl/system/ILogger.h" +#include "nbl/system/to_string.h" #include "nbl/asset/material_compiler3/CNodePool.h" #include "nbl/asset/format/EColorSpace.h" @@ -657,6 +658,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! public: inline bool operator==(const SBasicNodes& other) const = default; + typed_pointer_type errorLayer = {}; typed_pointer_type blackHoleBxDF = {}; typed_pointer_type errorBxDF = {}; diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index da3b5edecc..82167166a5 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -172,6 +172,9 @@ auto CFrontendIR::deepCopy(const typed_pointer_type orig, const auto CFrontendIR::reciprocate(const typed_pointer_type orig, const CFrontendIR* pSourceIR) -> typed_pointer_type { + if (!orig) + return {}; + auto& dstPool = getObjectPool(); // if not explicitly other, then its ours if (!pSourceIR) @@ -224,10 +227,11 @@ auto CFrontendIR::reciprocate(const typed_pointer_type orig, co auto* const copy = dstPool.deref(copyH); if (!copy) return {}; - if (pSourceIR!=this) - copy->debugInfo = copyDebugInfo(node->debugInfo,pSourceIR); if (needToReciprocate) node->reciprocate(copy); + // reciprocate might take full copies, and copy pointers across, so do all modifications after + if (pSourceIR!=this) + copy->debugInfo = copyDebugInfo(node->debugInfo,pSourceIR); // only changed children need to be set for (uint8_t c=0; cgetNodeID(entry); str << "\n\t" << m_ir->getLabelledNodeID(entry); const auto* node = m_ir->getObjectPool().deref(entry); @@ -443,7 +447,7 @@ void CFrontendIR::SDotPrinter::operator()(std::ostringstream& str) const auto visited = visitedNodes.find(childHandle); if (visited!=visitedNodes.end()) continue; - exprStack.push(childHandle); + exprStack.push_back(childHandle); visitedNodes.insert(childHandle); } } @@ -483,7 +487,7 @@ void CFrontendIR::SDotPrinter::operator()(std::ostringstream& str) const auto visited = visitedNodes.find(root); if (visited!=visitedNodes.end()) return; - exprStack.push(root); + exprStack.push_back(root); visitedNodes.insert(root); }; pushExprRoot(layerNode->brdfTop,"Top BRDF"); @@ -631,6 +635,8 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ oriented_material_t retval = {}; srcAST = _srcAST; + astPrinter.reset(srcAST); + const auto& astPool = srcAST->getObjectPool(); assert(layerStack.empty()); auto clearLayerStackOnExit = core::makeRAIIExiter([this]()->void{layerStack.clear();}); @@ -673,7 +679,7 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ // process the top BRDF outLayer->brdfTop = makeContributors(inLayer->brdfTop); if (outLayer->brdfTop==errorBxDF) - return {}; // TODO: error material + return {}; // process the BTDF btdfSubtree = true; const auto btdfH = makeContributors(inLayer->btdf); @@ -682,7 +688,7 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ if (btdfH) { if (btdfH==errorBxDF) - return {}; // TODO: error material + return {}; const auto transmissionH = irPool.emplace(); { auto* const transmission = irPool.deref(transmissionH); @@ -692,7 +698,7 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ { transmission->brdfBottom = makeContributors(inLayer->brdfBottom); if (transmission->brdfBottom==errorBxDF) - return {}; // TODO: error material + return {}; } // we check if previous layer didn't get oprimized away, but we don't add its optimized version because don't want pointers across two pools (crash) if (retval.root) @@ -738,31 +744,36 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ // auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_pointer_type bxdfRootH) -> CTrueIR::typed_pointer_type -{ - // debug what we're processing - SDotPrinter astPrinter(srcAST); - auto printSubtree = [&](const CFrontendIR::typed_pointer_type nodeH)->void - { - assert(astPrinter.exprStack.empty()); - astPrinter.exprStack.push(nodeH); - args.logger.log("Subtree Dot3 : \n%s\n", ELL_DEBUG,astPrinter().c_str()); - assert(astPrinter.exprStack.empty()); - }; - - using contributor_sum_handle_t = CTrueIR::typed_pointer_type; - contributor_sum_handle_t headH = {}; +{ + CTrueIR::typed_pointer_type headH = {}; if (!bxdfRootH) return headH; // temporary debug for WIP printSubtree(bxdfRootH); + // error on exit + const auto errorRetval = tmpIR->getBasicNodes().errorBxDF; + auto printFailAndCleanupOnExit = core::makeRAIIExiter([&]()->void + { + if (headH!=errorRetval) + return; + printSubtree(exprStack.back().nodeH); + args.logger.log("Within BxDF:\n",ELL_DEBUG,astPrinter().c_str()); + printSubtree(bxdfRootH); + // no point pushing an error contributor, don't want a best effort compilation within a layer, don't want contributors missing or substituted + exprStack.clear(); + mulChain.clear(); + contributorStack.clear(); + } + ); + auto& astPool = srcAST->getObjectPool(); auto& irPool = tmpIR->getObjectPool(); // scratches are initialized assert(mulChain.empty()); assert(contributorStack.empty()); exprStack.push_back({.nodeH=bxdfRootH}); - contributor_sum_handle_t tailH = {}; + CTrueIR::typed_pointer_type tailH = {}; while (!exprStack.empty()) { auto& entry = exprStack.back(); @@ -780,12 +791,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin if (!contributorH || irPool.deref(contributorH)->computeHash(irPool)==core::blake3_hash_t{}) { args.logger.log("Failed to Create IR Contributor from AST",ELL_ERROR); - printSubtree(entry.nodeH); - // no point pushing an error contributor, don't want a best effort compilation within a layer, don't want contributors missing or substituted - exprStack.clear(); - mulChain.clear(); - contributorStack.clear(); - return tmpIR->getBasicNodes().errorBxDF; + return (headH=errorRetval); } contributorStack.push_back({.contributor=contributorH}); exprStack.pop_back(); @@ -841,13 +847,22 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin headH = tailH; } // add current contributor with weight to BxDF Sum - irPool.deref(tailH)->product = popContributor(); + if (const auto contributor=popContributor(); contributor) + irPool.deref(tailH)->product = contributor; + else + { + args.logger.log("Failed to Pop the Contributor from the Stack, most likely failed to create the factor node chain.",ELL_ERROR); + return (headH=errorRetval); + } // when we are done we need to reset the mul chain back to its original state mulChain.resize(entry.mulChainLen); break; } default: - break; + { + args.logger.log("Unsupported AST Expression type \"%s\"",ELL_ERROR,system::to_string(astExprType).c_str()); + return (headH=errorRetval); + } } exprStack.pop_back(); } @@ -859,7 +874,13 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin assert(contributorStack.size()==1); // add the contributor headH = irPool.emplace(); - irPool.deref(headH)->product = popContributor(); + if (const auto contributor=popContributor(); contributor) + irPool.deref(headH._const_cast())->product = contributor; + else + { + args.logger.log("Failed to Pop the Single Contributor from the Stack, most likely failed to create the factor node chain.",ELL_ERROR); + return (headH=errorRetval); + } mulChain.clear(); } // we got all the AST ADD nodes on the way back out @@ -878,6 +899,7 @@ auto CFrontendIR::SAdd2IRSession::popContributor() -> CTrueIR::typed_pointer_typ contributorStack.pop_back(); if (!mulChain.empty()) { + return {}; assert(false); // unimplemented const CTrueIR::CFactorCombiner::SState combinerState = { .type = CTrueIR::CFactorCombiner::Type::Mul, @@ -911,34 +933,54 @@ auto CFrontendIR::SAdd2IRSession::popContributor() -> CTrueIR::typed_pointer_typ } // -CTrueIR::SMaterialHandle CFrontendIR::makeFinalIR(const typed_pointer_type rootH, SAdd2IRSession& session) const +CTrueIR::SMaterialHandle CFrontendIR::SAdd2IRSession::makeFinalIR(const typed_pointer_type rootH, const CFrontendIR* ast) { - const auto& astPool = getObjectPool(); - // TODO // core::unordered_map brdfs; // core::unordered_map btdfs; + const auto& astPool = ast->getObjectPool(); const auto* astRoot = astPool.deref(rootH); // no material if (!astRoot) return CTrueIR::BlackholeMaterialHandle; - session.tmpIR->reset(); + tmpIR->reset(); // reverse AST into another tree - session.tmpAST->reset(); - const auto backRootH = session.tmpAST->reverse(rootH,this); + tmpAST->reset(); + const auto backRootH = tmpAST->reverse(rootH,ast); CTrueIR::SMaterial material = { - .front = session.makeOrientedMaterial(rootH,this), - .back = session.makeOrientedMaterial(backRootH,session.tmpAST.get()) + .front = makeOrientedMaterial(rootH,ast), + .back = makeOrientedMaterial(backRootH,tmpAST.get()) }; - auto retval = session.args.ir->addMaterial(material); + const auto errorLayer = args.ir->getBasicNodes().errorLayer; + auto printLayer = [&](const typed_pointer_type _rootH, const CFrontendIR* _ast)->void + { + astPrinter.reset(_ast); + astPrinter.layerStack.push_back(_rootH); + args.logger.log("Subtree Dot3 : \n%s\n",ELL_DEBUG,astPrinter().c_str()); + assert(astPrinter.layerStack.empty()); + }; + if (material.front.root==errorLayer) + { + args.logger.log("Failed to create Frontface Material",ELL_ERROR); + printLayer(rootH,ast); + return {}; + } + if (material.back.root==errorLayer) + { + args.logger.log("Failed to create Backface Material for reversed AST",ELL_ERROR); + printLayer(backRootH,tmpAST.get()); + return {}; + } + + auto retval = args.ir->addMaterial(material); if (retval) { // TODO: better debug info (e.g. concat all the layer info during `makeOrientedMaterial` via the `session` object if (const auto* debug=astPool.deref(astRoot->debugInfo); debug && !debug->data().empty()) { - material.debugInfo = session.args.ir->getObjectPool().emplace(debug->data().data(),debug->data().size()); + material.debugInfo = args.ir->getObjectPool().emplace(debug->data().data(),debug->data().size()); } } return retval; From 83a66bc13b6e4a6a1efc86b974dc99ac72bf9ebe Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Fri, 1 May 2026 13:27:17 +0200 Subject: [PATCH 18/45] rearrange the code in `CFrontendIR.cpp` so it reads linearly in-order, set up the error BxDF and layer in CTrueIR --- .../nbl/asset/material_compiler3/CTrueIR.h | 3 +- .../asset/material_compiler3/CFrontendIR.cpp | 225 +++++++++--------- src/nbl/asset/material_compiler3/CTrueIR.cpp | 25 +- 3 files changed, 137 insertions(+), 116 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index d5f95139fe..9a2a4aa201 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -658,9 +658,10 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! public: inline bool operator==(const SBasicNodes& other) const = default; - typed_pointer_type errorLayer = {}; typed_pointer_type blackHoleBxDF = {}; + // these are never meant to be hashed and inserted into `m_uniqueNodes` typed_pointer_type errorBxDF = {}; + typed_pointer_type errorLayer = {}; private: friend class CTrueIR; diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index 82167166a5..ace4902bd7 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -552,80 +552,57 @@ void CFrontendIR::CCookTorrance::printDot(std::ostringstream& sstr, const core:: ndParams.printDot(sstr,selfID); } -//! AST-> IR methods -auto CFrontendIR::CEmitter::createIRNode(const bool forBTDF, const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t -{ - assert(!forBTDF); - auto& irPool = ir->getObjectPool(); - const auto retval = irPool.emplace(); - if (auto* const contributor=irPool.deref(retval)) - { - contributor->profile = profile; - contributor->profileTransform = profileTransform; - } - return retval; -} -auto CFrontendIR::CDeltaTransmission::createIRNode(const bool forBTDF, const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t +//! IR making +CTrueIR::SMaterialHandle CFrontendIR::SAdd2IRSession::makeFinalIR(const typed_pointer_type rootH, const CFrontendIR* ast) { - assert(forBTDF); - return ir->getObjectPool().emplace(); -} + // TODO +// core::unordered_map brdfs; +// core::unordered_map btdfs; -auto CFrontendIR::COrenNayar::createIRNode(const bool forBTDF, const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t -{ - if (ndParams.getDistribution()!=CTrueIR::SBasicNDFParams::EDistribution::Invalid) - return {}; - auto& irPool = ir->getObjectPool(); - const auto retval = irPool.emplace(); - if (auto* const contributor = irPool.deref(retval)) - contributor->ndfParams = ndParams; // the padding abuse is the same between the classes - return retval; -} + const auto& astPool = ast->getObjectPool(); + const auto* astRoot = astPool.deref(rootH); + // no material + if (!astRoot) + return CTrueIR::BlackholeMaterialHandle; + tmpIR->reset(); + // reverse AST into another tree + tmpAST->reset(); + const auto backRootH = tmpAST->reverse(rootH,ast); + CTrueIR::SMaterial material = { + .front = makeOrientedMaterial(rootH,ast), + .back = makeOrientedMaterial(backRootH,tmpAST.get()) + }; -auto CFrontendIR::CCookTorrance::createIRNode(const bool forBTDF, const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t -{ - if (ndParams.getDistribution()>CTrueIR::SBasicNDFParams::EDistribution::Beckmann) + const auto errorLayer = args.ir->getBasicNodes().errorLayer; + auto printLayer = [&](const typed_pointer_type _rootH, const CFrontendIR* _ast)->void + { + astPrinter.reset(_ast); + astPrinter.layerStack.push_back(_rootH); + args.logger.log("Subtree Dot3 : \n%s\n",ELL_DEBUG,astPrinter().c_str()); + assert(astPrinter.layerStack.empty()); + }; + if (material.front.root==errorLayer) + { + args.logger.log("Failed to create Frontface Material",ELL_ERROR); + printLayer(rootH,ast); return {}; - auto& irPool = ir->getObjectPool(); - CTrueIR::typed_pointer_type etaH = {}; - if (forBTDF) + } + if (material.back.root==errorLayer) { - if (!orientedRealEta) - return {}; - const auto* const srcEta = ast->getObjectPool().deref(orientedRealEta); - switch (srcEta->getSemantics()) - { - case CSpectralVariable::semantics_e::NoneUndefined: - { - assert(srcEta->getKnotCount()==1); - const auto handle = irPool.emplace>(); - auto* const dstEta = irPool.deref(handle); - if (!dstEta) - return {}; - dstEta->paramSet.uvTransform = srcEta->uvTransform(); - dstEta->paramSet.params[0] = *srcEta->getParam(0); - break; - } - default: - { - assert(srcEta->getKnotCount()==3); - const auto handle = irPool.emplace>(); - auto* const dstEta = irPool.deref(handle); - if (!dstEta) - return {}; - dstEta->paramSet.uvTransform = srcEta->uvTransform(); - for (auto i=0; i<3; i++) - dstEta->paramSet.params[i] = *srcEta->getParam(i); - break; - } - } + args.logger.log("Failed to create Backface Material for reversed AST",ELL_ERROR); + printLayer(backRootH,tmpAST.get()); + return {}; } - const auto retval = irPool.emplace(); - if (auto* const ct=irPool.deref(retval); ct) + + auto retval = args.ir->addMaterial(material); + if (retval) { - ct->ndfParams = ndParams; // the padding abuse is the same between the classes - ct->orientedRealEta = etaH; + // TODO: better debug info (e.g. concat all the layer info during `makeOrientedMaterial` via the `session` object + if (const auto* debug=astPool.deref(astRoot->debugInfo); debug && !debug->data().empty()) + { + material.debugInfo = args.ir->getObjectPool().emplace(debug->data().data(),debug->data().size()); + } } return retval; } @@ -664,7 +641,9 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ break; } } + const auto errorBxDF = tmpIR->getBasicNodes().errorBxDF; + const auto errorLayer = tmpIR->getBasicNodes().errorLayer; // Some metadata needed for us bool layersBelowCanScatterBack = false; CTrueIR::typed_pointer_type prevLayerH = {}; @@ -679,7 +658,7 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ // process the top BRDF outLayer->brdfTop = makeContributors(inLayer->brdfTop); if (outLayer->brdfTop==errorBxDF) - return {}; + return {.root=errorLayer}; // process the BTDF btdfSubtree = true; const auto btdfH = makeContributors(inLayer->btdf); @@ -688,7 +667,7 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ if (btdfH) { if (btdfH==errorBxDF) - return {}; + return {.root=errorLayer}; const auto transmissionH = irPool.emplace(); { auto* const transmission = irPool.deref(transmissionH); @@ -698,7 +677,7 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ { transmission->brdfBottom = makeContributors(inLayer->brdfBottom); if (transmission->brdfBottom==errorBxDF) - return {}; + return {.root=errorLayer}; } // we check if previous layer didn't get oprimized away, but we don't add its optimized version because don't want pointers across two pools (crash) if (retval.root) @@ -932,57 +911,81 @@ auto CFrontendIR::SAdd2IRSession::popContributor() -> CTrueIR::typed_pointer_typ return retval; } -// -CTrueIR::SMaterialHandle CFrontendIR::SAdd2IRSession::makeFinalIR(const typed_pointer_type rootH, const CFrontendIR* ast) +// AST Node -> IR methods +auto CFrontendIR::CEmitter::createIRNode(const bool forBTDF, const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t { - // TODO -// core::unordered_map brdfs; -// core::unordered_map btdfs; - - const auto& astPool = ast->getObjectPool(); - const auto* astRoot = astPool.deref(rootH); - // no material - if (!astRoot) - return CTrueIR::BlackholeMaterialHandle; - tmpIR->reset(); - // reverse AST into another tree - tmpAST->reset(); - const auto backRootH = tmpAST->reverse(rootH,ast); - CTrueIR::SMaterial material = { - .front = makeOrientedMaterial(rootH,ast), - .back = makeOrientedMaterial(backRootH,tmpAST.get()) - }; - - const auto errorLayer = args.ir->getBasicNodes().errorLayer; - auto printLayer = [&](const typed_pointer_type _rootH, const CFrontendIR* _ast)->void - { - astPrinter.reset(_ast); - astPrinter.layerStack.push_back(_rootH); - args.logger.log("Subtree Dot3 : \n%s\n",ELL_DEBUG,astPrinter().c_str()); - assert(astPrinter.layerStack.empty()); - }; - if (material.front.root==errorLayer) + assert(!forBTDF); + auto& irPool = ir->getObjectPool(); + const auto retval = irPool.emplace(); + if (auto* const contributor=irPool.deref(retval)) { - args.logger.log("Failed to create Frontface Material",ELL_ERROR); - printLayer(rootH,ast); - return {}; + contributor->profile = profile; + contributor->profileTransform = profileTransform; } - if (material.back.root==errorLayer) - { - args.logger.log("Failed to create Backface Material for reversed AST",ELL_ERROR); - printLayer(backRootH,tmpAST.get()); + return retval; +} + +auto CFrontendIR::CDeltaTransmission::createIRNode(const bool forBTDF, const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t +{ + assert(forBTDF); + return ir->getObjectPool().emplace(); +} + +auto CFrontendIR::COrenNayar::createIRNode(const bool forBTDF, const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t +{ + if (ndParams.getDistribution()!=CTrueIR::SBasicNDFParams::EDistribution::Invalid) return {}; - } + auto& irPool = ir->getObjectPool(); + const auto retval = irPool.emplace(); + if (auto* const contributor = irPool.deref(retval)) + contributor->ndfParams = ndParams; // the padding abuse is the same between the classes + return retval; +} - auto retval = args.ir->addMaterial(material); - if (retval) +auto CFrontendIR::CCookTorrance::createIRNode(const bool forBTDF, const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t +{ + if (ndParams.getDistribution()>CTrueIR::SBasicNDFParams::EDistribution::Beckmann) + return {}; + auto& irPool = ir->getObjectPool(); + CTrueIR::typed_pointer_type etaH = {}; + if (forBTDF) { - // TODO: better debug info (e.g. concat all the layer info during `makeOrientedMaterial` via the `session` object - if (const auto* debug=astPool.deref(astRoot->debugInfo); debug && !debug->data().empty()) + if (!orientedRealEta) + return {}; + const auto* const srcEta = ast->getObjectPool().deref(orientedRealEta); + switch (srcEta->getSemantics()) { - material.debugInfo = args.ir->getObjectPool().emplace(debug->data().data(),debug->data().size()); + case CSpectralVariable::semantics_e::NoneUndefined: + { + assert(srcEta->getKnotCount()==1); + const auto handle = irPool.emplace>(); + auto* const dstEta = irPool.deref(handle); + if (!dstEta) + return {}; + dstEta->paramSet.uvTransform = srcEta->uvTransform(); + dstEta->paramSet.params[0] = *srcEta->getParam(0); + break; + } + default: + { + assert(srcEta->getKnotCount()==3); + const auto handle = irPool.emplace>(); + auto* const dstEta = irPool.deref(handle); + if (!dstEta) + return {}; + dstEta->paramSet.uvTransform = srcEta->uvTransform(); + for (auto i=0; i<3; i++) + dstEta->paramSet.params[i] = *srcEta->getParam(i); + break; + } } } + const auto retval = irPool.emplace(); + if (auto* const ct=irPool.deref(retval); ct) + { + ct->ndfParams = ndParams; // the padding abuse is the same between the classes + ct->orientedRealEta = etaH; + } return retval; } diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp index d8266f8588..3778e05ccc 100644 --- a/src/nbl/asset/material_compiler3/CTrueIR.cpp +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -74,13 +74,30 @@ CTrueIR::SBasicNodes::SBasicNodes(CTrueIR* ir) assert(success); ir->m_uniqueNodes[node->getHash()] = blackHoleBxDF; } - // + // we never compute the hashes on these ones, they're supposed to have invalid hash + errorLayer = pool.emplace(); + { + auto* const node = pool.deref(errorLayer._const_cast()); + node->brdfTop = errorBxDF; + node->firstTransmission = {}; + } errorBxDF = pool.emplace(); { auto* const node = pool.deref(errorBxDF._const_cast()); - node->product = {}; // TODO: magenta diffuse and rest of setup - //const bool success = node->recomputeHash(pool); - //assert(success); + node->product = pool.emplace(); + { + auto* const weighted = pool.deref(node->product._const_cast()); + weighted->contributor = pool.emplace(); + const auto factorH = pool.emplace>(); + { + auto* const factor = pool.deref(factorH); + // make a magenta constant color (can do checkerboard of green & magenta in the future with a small texture) + for (auto i=0; i<3; i++) + factor->paramSet.params[i].scale = i!=1 ? 1.f:0.f; + } + weighted->factor = pool.emplace>(); + } + node->rest = {}; } } From 5be57c26e72fb125df5edf66183a7ba076a4397f Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Fri, 1 May 2026 13:56:01 +0200 Subject: [PATCH 19/45] fix one copy op --- include/nbl/asset/material_compiler3/CFrontendIR.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index 0f37d5d44e..dfb52bef4a 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -262,7 +262,11 @@ class CFrontendIR final : public CNodePool } inline CSpectralVariable(const CSpectralVariable& other) { - std::uninitialized_copy_n(other.pWonky(),other.getKnotCount(),pWonky()); + const auto* const src = other.pWonky(); + auto* const dst = pWonky(); + std::uninitialized_copy_n(src,1,dst); + const size_t count = other.getKnotCount(); + std::uninitialized_copy_n(src->knots.params+1,count-1,dst->knots.params+1); } // encapsulation due to padding abuse From 0b28b76265d8a16793a82d7da669215d85b4d95b Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Fri, 1 May 2026 14:58:28 +0200 Subject: [PATCH 20/45] adjust the MitsubaLoader to changes in the Frontend, CI should pass now --- examples_tests | 2 +- include/nbl/asset/asset.h | 3 ++ include/nbl/ext/MitsubaLoader/SContext.h | 19 +++++--- src/nbl/ext/MitsubaLoader/CMitsubaLoader.cpp | 48 ++++++++++++-------- 4 files changed, 45 insertions(+), 27 deletions(-) diff --git a/examples_tests b/examples_tests index d5ba19d879..955a4ab8be 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit d5ba19d879675aca3072d646b88f8894286de8c1 +Subproject commit 955a4ab8befe7a2d5b707299f236d2bcfa211f3c diff --git a/include/nbl/asset/asset.h b/include/nbl/asset/asset.h index 3d21842195..1767908a6b 100644 --- a/include/nbl/asset/asset.h +++ b/include/nbl/asset/asset.h @@ -41,6 +41,9 @@ #include "nbl/asset/utils/CGLSLCompiler.h" #include "nbl/asset/utils/CSPIRVIntrospector.h" +// material compiler +#include "nbl/asset/material_compiler3/CFrontendIR.h" + // pipelines // skinning diff --git a/include/nbl/ext/MitsubaLoader/SContext.h b/include/nbl/ext/MitsubaLoader/SContext.h index 17b8f48bb3..84850c5596 100644 --- a/include/nbl/ext/MitsubaLoader/SContext.h +++ b/include/nbl/ext/MitsubaLoader/SContext.h @@ -38,7 +38,7 @@ struct SContext final inline void writeFrontendForestDot3(system::ISystem* system, const system::path& filepath) { - asset::material_compiler3::CFrontendIR::SDotPrinter printer = {frontIR.get(),frontIR->getMaterials()}; + asset::material_compiler3::CFrontendIR::SDotPrinter printer = {frontIR.get(),debugAllMaterials}; writeDot3File(system,filepath,printer); } @@ -48,6 +48,10 @@ struct SContext final meta->setGeometryCollectionMeta(std::move(groupCache)); } + using true_ir_t = asset::material_compiler3::CTrueIR; + using frontend_ir_t = asset::material_compiler3::CFrontendIR; + inline const frontend_ir_t* getMaterialFrontend() const {return frontIR.get();} + const asset::IAssetLoader::SAssetLoadContext inner; asset::IAssetLoader::IAssetLoaderOverride* override_; std::function interm_getAssetInHierarchy; @@ -56,7 +60,6 @@ struct SContext final core::smart_refctd_ptr scene; private: - using frontend_ir_t = asset::material_compiler3::CFrontendIR; using frontend_material_t = frontend_ir_t::typed_pointer_type; // not `frontend_ir_t::CEmitter` because the color factor gets multiplied in using frontend_emitter_t = frontend_ir_t::typed_pointer_type; @@ -65,10 +68,10 @@ struct SContext final // void writeDot3File(system::ISystem* system, const system::path& filepath, frontend_ir_t::SDotPrinter& printer); // - hlsl::float32_t2x3 getParameters(const std::span out, const CElementTexture::FloatOrTexture& src); - hlsl::float32_t2x3 getParameters(const std::span out, const CElementTexture::SpectrumOrTexture& src); - frontend_ir_t::SParameter getTexture(const CElementTexture* tex, hlsl::float32_t2x3* outUvTransform); - frontend_ir_t::SParameter genProfile(const CElementEmissionProfile* profile); + hlsl::float32_t2x3 getParameters(const std::span out, const CElementTexture::FloatOrTexture& src); + hlsl::float32_t2x3 getParameters(const std::span out, const CElementTexture::SpectrumOrTexture& src); + true_ir_t::SParameter getTexture(const CElementTexture* tex, hlsl::float32_t2x3* outUvTransform); + true_ir_t::SParameter genProfile(const CElementEmissionProfile* profile); // core::unordered_map shapeCache; @@ -77,7 +80,7 @@ struct SContext final // core::unordered_map emitterCache; core::unordered_map bsdfCache; - core::unordered_map profileCache; + core::unordered_map profileCache; #if 0 // stuff that belongs in the Material Compiler backend //image, sampler @@ -86,6 +89,8 @@ struct SContext final core::map,float> derivMapCache; #endif core::smart_refctd_ptr frontIR; + // for a debug print + core::vector> debugAllMaterials; // common frontend nodes frontend_ir_t::typed_pointer_type unityFactor; frontend_ir_t::typed_pointer_type errorBRDF; diff --git a/src/nbl/ext/MitsubaLoader/CMitsubaLoader.cpp b/src/nbl/ext/MitsubaLoader/CMitsubaLoader.cpp index dac1d783d0..1f82bbc975 100644 --- a/src/nbl/ext/MitsubaLoader/CMitsubaLoader.cpp +++ b/src/nbl/ext/MitsubaLoader/CMitsubaLoader.cpp @@ -271,8 +271,17 @@ SAssetBundle CMitsubaLoader::loadAsset(system::IFile* _file, const IAssetLoader: instances.getInitialTransforms()[index] = shape->getTransform(); // const core::string debugName = shape->id.empty() ? std::format("0x{:x}",ptrdiff_t(shape)):shape->id; - ctx.getMaterial(shape->bsdf,shape->obtainEmitter(),debugName,PrintMaterialDot3 ? m_system.get():nullptr); - // TODO: compile the material into the true IR and push it into the instances + const auto astRootH = ctx.getMaterial(shape->bsdf,shape->obtainEmitter(),debugName,PrintMaterialDot3 ? m_system.get():nullptr); + // TODO: get the output slot for the material in the ICPUScene from `instances.getMaterialTables()` + material_compiler3::CTrueIR::SMaterialHandle rMaterialHandle = {}; // instances.getMaterialTables() + if (astRootH) + { + // we can't batch because of how the parser works + const auto addedCount = ctx.getMaterialFrontend()->addMaterials({.rootNodes={&astRootH,1},.ir=ctx.scene->getMaterialPool(),.result=&rMaterialHandle,.logger=_params.logger}); + // we could check `addedCount` but the function above does its own logging + } + else // TODO: make a special Error material + rMaterialHandle = material_compiler3::CTrueIR::BlackholeMaterialHandle; }; // first go over all actually used shapes which are not shapegroups (regular shapes and instances) @@ -385,7 +394,8 @@ auto SContext::getMaterial( } else root->brdfTop = foundEmitter->second._const_cast(); - const bool success = frontIR->addMaterial(rootH,inner.params.logger); + // + const bool success = frontIR->valid(rootH,inner.params.logger); auto logger = inner.params.logger; if (!success) @@ -398,13 +408,14 @@ auto SContext::getMaterial( const frontend_ir_t::typed_pointer_type constRootH = rootH; frontend_ir_t::SDotPrinter printer = {frontIR.get(),{&constRootH,1}}; writeDot3File(debugFileWriter,DebugDir/"material_frontend"/(debugName+".dot"),printer); + debugAllMaterials.push_back(constRootH); } return rootH; } -using parameter_t = asset::material_compiler3::CFrontendIR::SParameter; +using parameter_t = asset::material_compiler3::CTrueIR::SParameter; parameter_t SContext::getTexture(const CElementTexture* const rootTex, hlsl::float32_t2x3* outUvTransform) { parameter_t retval = {}; @@ -594,22 +605,22 @@ auto SContext::genEmitter(const CElementEmitter* _emitter, system::ISystem* debu spectral_var_t::SCreationParams<3> params = {}; // if you wanted a textured emitter, this would be the place to do it MitsubaLoader::getParameters<3>(params.knots.params,_emitter->area.radiance); - params.getSemantics() = spectral_var_t::Semantics::Fixed3_SRGB; + params.getSemantics() = true_ir_t::ISpectralVariable::ESemantics::Fixed3_SRGB; mul->rhs = frontPool.emplace(std::move(params)); } if (debugFileWriter) { frontend_ir_t::SDotPrinter printer = {frontIR.get()}; - printer.exprStack.push(handle); + printer.exprStack.push_back(handle); writeDot3File(debugFileWriter,DebugDir/"material_frontend/emitters"/(debugName+".dot"),printer); } return handle; } -auto SContext::genProfile(const CElementEmissionProfile* profile) -> frontend_ir_t::SParameter +auto SContext::genProfile(const CElementEmissionProfile* profile) -> true_ir_t::SParameter { - frontend_ir_t::SParameter retval = {}; + true_ir_t::SParameter retval = {}; // load it! using namespace nbl::asset; const CIESProfileMetadata* iesMeta = nullptr; @@ -659,7 +670,7 @@ auto SContext::genMaterial(const CElementBSDF* bsdf, system::ISystem* debugFileW { spectral_var_t::SCreationParams<3> params = {}; params.knots.uvTransform = getParameters(params.knots.params,factor); - params.getSemantics() = spectral_var_t::Semantics::Fixed3_SRGB; + params.getSemantics() = true_ir_t::ISpectralVariable::ESemantics::Fixed3_SRGB; const auto factorH = frontPool.emplace(std::move(params)); frontPool.deref(factorH)->debugInfo = commonDebugNames[uint16_t(debug)]._const_cast(); return factorH; @@ -678,7 +689,7 @@ auto SContext::genMaterial(const CElementBSDF* bsdf, system::ISystem* debugFileW auto* mul = frontPool.deref(mulH); const auto ctH = frontPool.emplace(); { - using ndf_e = frontend_ir_t::CCookTorrance::NDF; + using ndf_e = true_ir_t::SBasicNDFParams::EDistribution; constexpr ndf_e ndfMap[4] = { ndf_e::Beckmann, ndf_e::GGX, @@ -690,7 +701,7 @@ auto SContext::genMaterial(const CElementBSDF* bsdf, system::ISystem* debugFileW if (base) { // ct->orientedRealEta gets set in the fresnel part - ct->ndf = ndfMap[base->distribution]; + ct->ndParams.getDistribution() = ndfMap[base->distribution]; // this function sets both roughnesses to same alpha ct->ndParams.uvTransform = getParameters(roughness,base->alpha); if (base->alphaV.texture || !hlsl::isnan(base->alphaV.value)) @@ -819,7 +830,7 @@ auto SContext::genMaterial(const CElementBSDF* bsdf, system::ISystem* debugFileW { spectral_var_t::SCreationParams<3> params = {}; params.knots.uvTransform = getParameters(params.knots.params,_bsdf->diffuse.reflectance); - params.getSemantics() = spectral_var_t::Semantics::Fixed3_SRGB; + params.getSemantics() = true_ir_t::ISpectralVariable::ESemantics::Fixed3_SRGB; const auto albedoH = frontPool.emplace(std::move(params)); frontPool.deref(albedoH)->debugInfo = commonDebugNames[uint16_t(ECommonDebug::Albedo)]._const_cast(); mul->rhs = albedoH; @@ -858,14 +869,14 @@ auto SContext::genMaterial(const CElementBSDF* bsdf, system::ISystem* debugFileW spectral_var_t::SCreationParams<3> params = {}; const hlsl::float32_t3 eta = _bsdf->conductor.eta.vvalue.xyz; MitsubaLoader::getParameters<3>(params.knots.params,eta/extEta); - params.getSemantics() = spectral_var_t::Semantics::Fixed3_SRGB; + params.getSemantics() = true_ir_t::ISpectralVariable::ESemantics::Fixed3_SRGB; fresnel->orientedRealEta = frontPool.emplace(std::move(params)); } { spectral_var_t::SCreationParams<3> params = {}; const hlsl::float32_t3 k = _bsdf->conductor.k.vvalue.xyz; MitsubaLoader::getParameters<3>(params.knots.params,k/extEta); - params.getSemantics() = spectral_var_t::Semantics::Fixed3_SRGB; + params.getSemantics() = true_ir_t::ISpectralVariable::ESemantics::Fixed3_SRGB; fresnel->orientedImagEta = frontPool.emplace(std::move(params)); } } @@ -1049,7 +1060,7 @@ auto SContext::genMaterial(const CElementBSDF* bsdf, system::ISystem* debugFileW { spectral_var_t::SCreationParams<3> params = {}; params.knots.uvTransform = getParameters(params.knots.params,sigmaA); - params.getSemantics() = spectral_var_t::Semantics::Fixed3_SRGB; + params.getSemantics() = true_ir_t::ISpectralVariable::ESemantics::Fixed3_SRGB; beer->perpTransmittance = frontPool.emplace(std::move(params)); } fillCoatingLayer(coating,_bsdf->coating,_bsdf->type==CElementBSDF::ROUGHCOATING,beerH); @@ -1172,7 +1183,7 @@ auto SContext::genMaterial(const CElementBSDF* bsdf, system::ISystem* debugFileW auto* const root = frontPool.deref(rootH); root->debugInfo = frontPool.emplace(debugName); - const bool success = frontIR->addMaterial(rootH,inner.params.logger); + const bool success = frontIR->valid(rootH,inner.params.logger); if (!success) { logger.log("Failed to add Material for %s",LoggerError,debugName.c_str()); @@ -1281,8 +1292,7 @@ SContext::SContext( ) : inner(_ctx), override_(_override), meta(_metadata) //,ir(core::make_smart_refctd_ptr()), frontend(this) { - auto materialPool = material_compiler3::CTrueIR::create({.composed={.blockSizeKBLog2=4}}); - scene = ICPUScene::create(core::smart_refctd_ptr(materialPool)); // TODO: feed it max shapes per group + scene = ICPUScene::create(material_compiler3::CTrueIR::create({.composed={.blockSizeKBLog2=4}})); // TODO: feed it max shapes per group // { frontIR = frontend_ir_t::create({.composed={.blockSizeKBLog2=4}}); @@ -1313,7 +1323,7 @@ SContext::SContext( mul->lhs = frontPool.emplace(); spectral_var_t::SCreationParams<3> params = {}; MitsubaLoader::getParameters<3>(params.knots.params,{1.f,0.f,1.f}); - params.getSemantics() = spectral_var_t::Semantics::Fixed3_SRGB; + params.getSemantics() = true_ir_t::ISpectralVariable::ESemantics::Fixed3_SRGB; mul->rhs = frontPool.emplace(std::move(params)); } errorBRDF = mulH; From edb8750acf6edbd12c9a4482eca36731a9444156 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Fri, 1 May 2026 19:05:46 +0200 Subject: [PATCH 21/45] starting making the factor (SpectralVariable from CTrueIR by CFrontendIR) but realize its all just duplicate code --- .../asset/material_compiler3/CFrontendIR.h | 3 + .../nbl/asset/material_compiler3/CTrueIR.h | 22 ++++++-- .../asset/material_compiler3/CFrontendIR.cpp | 55 +++++++++++++++++++ 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index dfb52bef4a..35046431f0 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -361,6 +361,9 @@ class CFrontendIR final : public CNodePool private: SCreationParams<1>* pWonky() {return reinterpret_cast*>(this+1);} const SCreationParams<1>* pWonky() const {return reinterpret_cast*>(this+1);} + + friend class CFrontendIR; + NBL_API2 CTrueIR::typed_pointer_type createIRNode(const CFrontendIR* ast, CTrueIR* ir) const; }; // class IUnaryOp : public obj_pool_type::INonTrivial, public IExprNode diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 9a2a4aa201..ddc1a9aa06 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -434,6 +434,11 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! class ISpectralVariable : public IFactorLeaf { public: + virtual uint8_t getSpectralBins() const = 0; + + virtual SParameter* getParameter(const uint8_t i) = 0; + inline const SParameter* getParameter(const uint8_t i) const {return const_cast(const_cast(this)->getParameter(i));} + enum class ESemantics : uint8_t { NoneUndefined = 0, @@ -447,6 +452,13 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // PairsLinear = 5, // linear interpolation // PairsLogLinear = 5, // linear interpolation in wavelenght log space }; + inline ESemantics getSemantics() const + { + if (getSpectralBins()>1) + return static_cast(getParameter(1)->padding[0]); + else + return ESemantics::NoneUndefined; + } }; template class CSpectralVariable final : public obj_pool_type::INonTrivial, public ISpectralVariable @@ -456,7 +468,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! hasher << paramSet; if constexpr (SpectralBins>1) { - const ESemantics semantics = getSemantics(); + const ESemantics semantics = getSemantics(); if (semantics!=ESemantics::NoneUndefined) return false; hasher << semantics; @@ -465,6 +477,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! } public: + // TODO: maybe undo this? inline EFinalType getFinalType() const override { static_assert(1<=SpectralBins && SpectralBins<=4); @@ -475,11 +488,12 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CSpectralVariable);} // - inline uint8_t getSpectralBins() const {return SpectralBins;} + inline uint8_t getSpectralBins() const override {return SpectralBins;} + + // + inline SParameter* getParameter(const uint8_t i) {return paramSet.params+i;} // - template requires (N>1 && N==SpectralBins) - inline ESemantics getSemantics() const {return static_cast(paramSet.params[1].padding[0]);} template requires (N>1 && N==SpectralBins) inline void setSemantics(const ESemantics value) {paramSet.params[1].padding[0] = static_cast(value);} diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index ace4902bd7..ca2b2b4928 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -753,6 +753,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin assert(contributorStack.empty()); exprStack.push_back({.nodeH=bxdfRootH}); CTrueIR::typed_pointer_type tailH = {}; + // the mul node gets visited after children are made while (!exprStack.empty()) { auto& entry = exprStack.back(); @@ -814,6 +815,11 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin // do stuff now switch (astExprType) { + case ast_expr_type_e::Mul: + { + // silently skip + break; + } case ast_expr_type_e::Add: { // we visited the leftmost subtrees first so this is the right order @@ -837,6 +843,26 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin mulChain.resize(entry.mulChainLen); break; } + case ast_expr_type_e::SpectralVariable: + { + const auto varH = static_cast(node)->createIRNode(srcAST,tmpIR.get()); + auto* const var = irPool.deref(varH); + if (!var) + { + args.logger.log("Failed to create the Spectral Variable.",ELL_ERROR); + return (headH=errorRetval); + } + auto& factor = mulChain.emplace_back(); + factor.handle = varH; + const auto channels = var->getSpectralBins(); + factor.monochrome = channels>1; + for (uint8_t c=0; cgetParameter(c)->scale>std::numeric_limits::min())) + factor.liveSpectralChannels &= ~(0b1< CTrueIR::typed_pointer_type +{ + return {}; + auto& irPool = ir->getObjectPool(); + uint8_t realCount = 1; + for (uint8_t c=0; c retval = {}; + // TODO: maybe make it wholly polymorphic like the AST ? Or even have both classes use the same struct ? + switch (realCount) + { + case 1: + { + retval = irPool.emplace>(); + break; + } + case 3: + { + retval = irPool.emplace>(); + break; + } + default: + break; + } +// == + return retval; +} + // auto CFrontendIR::SAdd2IRSession::popContributor() -> CTrueIR::typed_pointer_type { From 14772695d1bf932fbe08c9708c747c98b43f93de Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Sat, 2 May 2026 03:42:45 +0200 Subject: [PATCH 22/45] Refactor and unify the SpectralVariable in the IR and AST, I'm pleased with how it looks now --- examples_tests | 2 +- .../asset/material_compiler3/CFrontendIR.h | 169 +++--------- .../nbl/asset/material_compiler3/CTrueIR.h | 253 +++++++++++++++--- include/nbl/ext/MitsubaLoader/SContext.h | 4 +- .../asset/material_compiler3/CFrontendIR.cpp | 109 +++----- src/nbl/asset/material_compiler3/CTrueIR.cpp | 10 +- src/nbl/ext/MitsubaLoader/CMitsubaLoader.cpp | 119 ++++---- 7 files changed, 354 insertions(+), 312 deletions(-) diff --git a/examples_tests b/examples_tests index 955a4ab8be..ebd86b3a01 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 955a4ab8befe7a2d5b707299f236d2bcfa211f3c +Subproject commit ebd86b3a01de50631387d18d42c34fe7f6032b82 diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index 35046431f0..b436a125a3 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -220,151 +220,36 @@ class CFrontendIR final : public CNodePool }; // 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 + class ISpectralVariableExpr : public CTrueIR::ISpectralVariable, 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 + inline uint8_t getChildCount() const override final {return 0;} // - inline IExprNode::Type getType() const override {return Type::SpectralVariable;} + inline IExprNode::Type getType() const override final {return Type::SpectralVariable;} - using semantics_e = CTrueIR::ISpectralVariable::ESemantics; // - template - struct SCreationParams - { - // Knots are "data points" on the (wavelength,value) plot, from which we can interpolate the rest of the spectrum - CTrueIR::SParameterSet knots = {}; - - // a little bit of abuse and padding reuse - template requires (Enable==(Count>1)) - semantics_e& getSemantics() {return reinterpret_cast(knots.params[1].padding[0]); } - template requires (Enable==(Count>1)) - const semantics_e& getSemantics() const {return const_cast(const_cast*>(this)->getSemantics());} - }; - // - template - inline CSpectralVariable(SCreationParams&& params) - { - // back up the count - static_assert(sizeof(CTrueIR::SParameter::padding)>1); - params.knots.params[0].padding[1] = Count; - // set it correctly for monochrome - if constexpr (Count==1) - params.knots.params[1].padding[0] = static_cast(semantics_e::NoneUndefined); - else - { - assert(params.getSemantics()!=semantics_e::NoneUndefined); - } - std::construct_at(reinterpret_cast*>(this+1),std::move(params)); - } - inline CSpectralVariable(const CSpectralVariable& other) - { - const auto* const src = other.pWonky(); - auto* const dst = pWonky(); - std::uninitialized_copy_n(src,1,dst); - const size_t count = other.getKnotCount(); - std::uninitialized_copy_n(src->knots.params+1,count-1,dst->knots.params+1); - } - - // encapsulation due to padding abuse - inline auto& uvTransform() {return pWonky()->knots.uvTransform;} - inline const auto& uvTransform() const {return pWonky()->knots.uvTransform;} - - 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(CTrueIR::SParameter::padding)>1); - return pWonky()->knots.params[0].padding[1]; - } - inline semantics_e getSemantics() const - { - if (getKnotCount()<2) - return semantics_e::NoneUndefined; - return static_cast(pWonky()->knots.params[1].padding[0]); - } - - // - inline CTrueIR::SParameter* getParam(const uint8_t i) - { - if (iknots.params[i]; - return nullptr; - } - inline const CTrueIR::SParameter* getParam(const uint8_t i) const {return const_cast(const_cast(this)->getParam(i));} - - // - template - static inline uint32_t calc_size(const SCreationParams&) - { - return sizeof(CSpectralVariable)+sizeof(SCreationParams); - } - // for copying - static inline uint32_t calc_size(const CSpectralVariable& other) - { - return sizeof(CSpectralVariable)+sizeof(SCreationParams<1>)+(other.getKnotCount()-1)*sizeof(CTrueIR::SParameter); - } - - inline operator bool() const {return !invalid(SInvalidCheckArgs{.pool=nullptr,.logger=nullptr});} + inline operator bool() const {return valid(nullptr);} protected: - inline ~CSpectralVariable() - { - std::destroy_n(pWonky()->knots.params,getKnotCount()); - } inline _typed_pointer_type copy(CFrontendIR* ir) const override final { - auto& pool = ir->getObjectPool(); - const uint8_t count = getKnotCount(); - return pool.emplace(*this); + return static_cast(this)->copy(ir->getObjectPool()); } - 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_e::Fixed3_SRGB: [[fallthrough]]; - case semantics_e::Fixed3_DCI_P3: [[fallthrough]]; - case semantics_e::Fixed3_BT2020: [[fallthrough]]; - case semantics_e::Fixed3_AdobeRGB: [[fallthrough]]; - case semantics_e::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(semantic),knotCount); - return false; - } - break; - default: - args.logger.log("Invalid Semantic %d",system::ILogger::ELL_ERROR,static_cast(semantic)); - return true; - } - for (auto i=0u; i* pWonky() {return reinterpret_cast*>(this+1);} - const SCreationParams<1>* pWonky() const {return reinterpret_cast*>(this+1);} + // + NBL_API2 core::string getLabelSuffix() const override final; + NBL_API2 void printDot(std::ostringstream& sstr, const core::string& selfID) const override final; + // friend class CFrontendIR; - NBL_API2 CTrueIR::typed_pointer_type createIRNode(const CFrontendIR* ast, CTrueIR* ir) const; + NBL_API2 CTrueIR::typed_pointer_type createIRNode(const CFrontendIR* ast, CTrueIR* ir) const; }; + using CSpectralVariableExpr = CTrueIR::CSpectralVariable; // class IUnaryOp : public obj_pool_type::INonTrivial, public IExprNode { @@ -483,8 +368,8 @@ class CFrontendIR final : public CNodePool // 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 perpTransmittance = {}; - typed_pointer_type thickness = {}; + typed_pointer_type perpTransmittance = {}; + typed_pointer_type thickness = {}; protected: COPY_DEFAULT_IMPL @@ -492,7 +377,7 @@ class CFrontendIR final : public CNodePool inline typed_pointer_type getChildHandle_impl(const uint8_t ix) const override {return ix ? perpTransmittance:thickness;} inline void setChild_impl(const uint8_t ix, _typed_pointer_type newChild) override { - *(ix ? &perpTransmittance:&thickness) = block_allocator_type::_static_cast(newChild); + *(ix ? &perpTransmittance:&thickness) = block_allocator_type::_static_cast(newChild); } inline std::string_view getChildName_impl(const uint8_t ix) const override {return "Perpendicular\\nTransmittance";} @@ -509,9 +394,9 @@ class CFrontendIR final : public CNodePool 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 orientedRealEta = {}; + typed_pointer_type orientedRealEta = {}; // Specifying this turns your Fresnel into a conductor one, note that currently these are disallowed on BTDFs! - typed_pointer_type orientedImagEta = {}; + typed_pointer_type orientedImagEta = {}; // if you want to reuse the same parameter but want to flip the interfaces around uint8_t reciprocateEtas : 1 = false; @@ -521,7 +406,7 @@ class CFrontendIR final : public CNodePool inline typed_pointer_type getChildHandle_impl(const uint8_t ix) const override {return ix ? orientedImagEta:orientedRealEta;} inline void setChild_impl(const uint8_t ix, _typed_pointer_type newChild) override { - *(ix ? &orientedImagEta:&orientedRealEta) = block_allocator_type::_static_cast(newChild); + *(ix ? &orientedImagEta:&orientedRealEta) = block_allocator_type::_static_cast(newChild); } NBL_API2 bool invalid(const SInvalidCheckArgs& args) const override; @@ -672,13 +557,13 @@ class CFrontendIR final : public CNodePool CTrueIR::SBasicNDFParams ndParams = {}; // See the comments in CTrueIR about this on a matching class - typed_pointer_type orientedRealEta = {}; + typed_pointer_type orientedRealEta = {}; protected: COPY_DEFAULT_IMPL inline typed_pointer_type getChildHandle_impl(const uint8_t ix) const override {return orientedRealEta;} - inline void setChild_impl(const uint8_t ix, _typed_pointer_type newChild) override {orientedRealEta = block_allocator_type::_static_cast(newChild);} + inline void setChild_impl(const uint8_t ix, _typed_pointer_type newChild) override {orientedRealEta = block_allocator_type::_static_cast(newChild);} NBL_API2 bool invalid(const SInvalidCheckArgs& args) const override; @@ -752,10 +637,14 @@ class CFrontendIR final : public CNodePool { auto& pool = getObjectPool(); const auto fresnelH = pool.emplace(); - CSpectralVariable::SCreationParams<1> params = {}; - params.knots.params[0].scale = orientedRealEta; if (auto* const fresnel=pool.deref(fresnelH); fresnel) - fresnel->orientedRealEta = pool.emplace(std::move(params)); + { + fresnel->orientedRealEta = pool.emplace(uint8_t(1)); + if (auto* const var=pool.deref(fresnel->orientedRealEta); var) + var->setParameter(0,{.scale=orientedRealEta}); + else + return {}; + } return fresnelH; } @@ -983,6 +872,8 @@ class CFrontendIR final : public CNodePool return retval; } }; + +template class CTrueIR::CSpectralVariable; } // specialize the `to_string diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index ddc1a9aa06..02788e8587 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -194,10 +194,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! CWeightedContributor, CEmitter, CDeltaTransmission, - CSpectralVariable_1, - CSpectralVariable_2, - CSpectralVariable_3, - CSpectralVariable_4, + CSpectralVariable, COrenNayar, CCookTorrance, CFactorCombiner @@ -431,14 +428,12 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! virtual uint8_t getSpectralBins() const = 0; }; - class ISpectralVariable : public IFactorLeaf + // + class ISpectralVariable { - public: - virtual uint8_t getSpectralBins() const = 0; - - virtual SParameter* getParameter(const uint8_t i) = 0; - inline const SParameter* getParameter(const uint8_t i) const {return const_cast(const_cast(this)->getParameter(i));} + inline SParameter& getParameter(const uint8_t i) {return pWonky()->params[i];} + public: enum class ESemantics : uint8_t { NoneUndefined = 0, @@ -452,21 +447,144 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // PairsLinear = 5, // linear interpolation // PairsLogLinear = 5, // linear interpolation in wavelenght log space }; + + // essential! + inline uint8_t getKnotCount() const + { + static_assert(sizeof(SParameter::padding)>1); + return getParameter(0).padding[1]; + } + + // encapsulation due to padding abuse + inline auto& uvTransform() {return pWonky()->uvTransform;} + inline const auto& uvTransform() const {return pWonky()->uvTransform;} + + inline uint8_t& uvSlot() {return pWonky()->uvSlot();} + inline const uint8_t& uvSlot() const {return pWonky()->uvSlot();} + + inline const SParameter& getParameter(const uint8_t i) const {assert(iparams[i];} + inline void setParameter(const uint8_t i, const SParameter& value) + { + assert(i1) - return static_cast(getParameter(1)->padding[0]); + if (getKnotCount()>1) + return static_cast(getParameter(1).padding[0]); else return ESemantics::NoneUndefined; } + + inline void setSemantics(const ESemantics value) + { + if (getKnotCount()>1) + getParameter(1).padding[0] = static_cast(value); + } + + protected: + inline ISpectralVariable() = default; + // fill out the params later + inline void init(const uint8_t knotCount) + { + std::uninitialized_default_construct_n(pWonky(),1); + if (knotCount>1) + std::uninitialized_default_construct_n(pWonky()->params+1,knotCount-1); + // back up the count + static_assert(sizeof(SParameter::padding)>1); + getParameter(0).padding[1] = knotCount; + // set it correctly for monochrome + setSemantics(ESemantics::NoneUndefined); + } + inline void init(const ISpectralVariable& other) + { + const auto* const src = other.pWonky(); + auto* const dst = pWonky(); + std::uninitialized_copy_n(src,1,dst); + const size_t count = other.getKnotCount(); + if (count>1) + std::uninitialized_copy_n(src->params+1,count-1,dst->params+1); + } + inline void init(const uint8_t knotCount, const ISpectralVariable& other) + { + init(knotCount); + const auto count = hlsl::min(other.getKnotCount(),knotCount); + for (uint8_t c=0; c* pWonky() = 0; + inline const SParameterSet<1>* pWonky() const {return const_cast*>(const_cast(this)->pWonky());} }; - template - class CSpectralVariable final : public obj_pool_type::INonTrivial, public ISpectralVariable + // This node could also represent non directional emission, but we have another node for that + template requires std::is_base_of_v + class alignas(SParameterSet<1>) CSpectralVariable final : public obj_pool_type::IVariableSize, public OtherBase { - inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override + using this_t = CSpectralVariable; + + protected: + inline ~CSpectralVariable() { - hasher << paramSet; - if constexpr (SpectralBins>1) + const auto count = ISpectralVariable::getKnotCount(); + std::destroy_n(pWonky(),1); + if (count>1) + std::destroy_n(pWonky()->params+1,count-1); + } + + inline SParameterSet<1>* pWonky() override final {return reinterpret_cast*>(this+1);} + + public: + inline CSpectralVariable(const uint8_t knotCount) {ISpectralVariable::init(knotCount);} + template requires std::is_base_of_v + inline CSpectralVariable(const U& other) {ISpectralVariable::init(other);} + template requires std::is_base_of_v + inline CSpectralVariable(const uint8_t knotCount, const U& other) {ISpectralVariable::init(knotCount,other);} + + // + inline _typed_pointer_type copy(obj_pool_type& pool) const + { + return pool.emplace(*this); + } + + // + static inline uint32_t calc_size(const uint8_t knotCount) + { + return sizeof(this_t)+sizeof(SParameterSet<1>)+sizeof(SParameter)*(knotCount-1); + } + // for copying + template requires std::is_base_of_v + static inline uint32_t calc_size(const U& other) + { + return calc_size(other.getKnotCount()); + } + template requires std::is_base_of_v + static inline uint32_t calc_size(const uint8_t knotCount, const U& other) + { + return calc_size(knotCount); + } + + // TODO: improve the token pasting here + inline const std::string_view getTypeName() const override final {return TYPE_NAME_STR(CSpectralVariable);} + }; + // + class ISpectralVariableFactor : public ISpectralVariable, public IFactorLeaf + { + inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override final + { + hasher << *pWonky(); + for (uint8_t c=1; cparams[c]; + if (getKnotCount()>1) { const ESemantics semantics = getSemantics(); if (semantics!=ESemantics::NoneUndefined) @@ -477,35 +595,15 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! } public: - // TODO: maybe undo this? - inline EFinalType getFinalType() const override + inline EFinalType getFinalType() const override final { - static_assert(1<=SpectralBins && SpectralBins<=4); - return static_cast(static_cast(EFinalType::CSpectralVariable_1)-1+SpectralBins); + return EFinalType::CSpectralVariable; } - - // TODO: improve the token pasting here - inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CSpectralVariable);} - - // - inline uint8_t getSpectralBins() const override {return SpectralBins;} - - // - inline SParameter* getParameter(const uint8_t i) {return paramSet.params+i;} // - template requires (N>1 && N==SpectralBins) - inline void setSemantics(const ESemantics value) {paramSet.params[1].padding[0] = static_cast(value);} - - // you can set the children later - inline CSpectralVariable() - { - if constexpr (SpectralBins>1) - setSemantics(ESemantics::Fixed3_SRGB); - } - - SParameterSet paramSet = {}; + inline uint8_t getSpectralBins() const override final {return getKnotCount();} }; + using CSpectralVariableFactor = CSpectralVariable; // Unit Radiance emitter modulated by an IES profile class CEmitter final : public obj_pool_type::INonTrivial, public IContributor @@ -659,7 +757,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // (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. - typed_pointer_type orientedRealEta = {}; + typed_pointer_type orientedRealEta = {}; }; //! Parameter Nodes //! Basic factor nodes @@ -841,8 +939,79 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! const SBasicNodes m_basicNodes; }; +template class CTrueIR::CSpectralVariable; + NBL_ENUM_ADD_BITWISE_OPERATORS(CTrueIR::SMaterial::EMetadataBits) +} + +// +namespace nbl::system::impl +{ +template<> +struct system::impl::to_string_helper +{ + using type = asset::material_compiler3::CTrueIR::ISpectralVariable::ESemantics; + + static inline std::string __call(const type value) + { + switch (value) + { + case type::NoneUndefined: + return "NoneUndefined"; + case type::Fixed3_SRGB: + return "Fixed3_SRGB"; + case type::Fixed3_DCI_P3: + return "Fixed3_DCI_P3"; + case type::Fixed3_BT2020: + return "Fixed3_BT2020"; + case type::Fixed3_AdobeRGB: + return "Fixed3_AdobeRGB"; + case type::Fixed3_AcesCG: + return "Fixed3_AcesCG"; + default: + break; + } + return ""; + } +}; +} + +// +namespace nbl::asset::material_compiler3 +{ + +inline bool CTrueIR::ISpectralVariable::valid(const system::logger_opt_ptr logger) const +{ + const auto knotCount = getKnotCount(); + // non-monochrome spectral variable + if (const auto semantic=getSemantics(); knotCount>1) + switch (semantic) + { + case ESemantics::Fixed3_SRGB: [[fallthrough]]; + case ESemantics::Fixed3_DCI_P3: [[fallthrough]]; + case ESemantics::Fixed3_BT2020: [[fallthrough]]; + case ESemantics::Fixed3_AdobeRGB: [[fallthrough]]; + case ESemantics::Fixed3_AcesCG: + if (knotCount!=3) + { + logger.log("Semantic %s is only usable with 3 knots, this has %d knots",system::ILogger::ELL_ERROR,system::to_string(semantic).c_str(),knotCount); + return false; + } + break; + default: + logger.log("Invalid Semantic %s",system::ILogger::ELL_ERROR,system::to_string(semantic).c_str()); + return false; + } + for (auto i=0u; i out, const CElementTexture::FloatOrTexture& src); + void getParameters(asset::material_compiler3::CFrontendIR::CSpectralVariableExpr* const out, const CElementTexture::FloatOrTexture& src); hlsl::float32_t2x3 getParameters(const std::span out, const CElementTexture::SpectrumOrTexture& src); + void getParameters(asset::material_compiler3::CFrontendIR::CSpectralVariableExpr* const out, const CElementTexture::SpectrumOrTexture& src); true_ir_t::SParameter getTexture(const CElementTexture* tex, hlsl::float32_t2x3* outUvTransform); true_ir_t::SParameter genProfile(const CElementEmissionProfile* profile); @@ -92,7 +94,7 @@ struct SContext final // for a debug print core::vector> debugAllMaterials; // common frontend nodes - frontend_ir_t::typed_pointer_type unityFactor; + frontend_ir_t::typed_pointer_type unityFactor; frontend_ir_t::typed_pointer_type errorBRDF; frontend_ir_t::typed_pointer_type errorMaterial, unsupportedPhong, unsupportedWard; frontend_ir_t::typed_pointer_type deltaTransmission; diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index ca2b2b4928..733428a673 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -1,6 +1,7 @@ // 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 +#define _NBL_ASSET_MATERIAL_COMPILER3_C_FRONTEND_IR_CPP_ #include "nbl/asset/material_compiler3/CFrontendIR.h" #include "nbl/builtin/hlsl/complex.hlsl" @@ -55,7 +56,7 @@ bool CFrontendIR::CFresnel::invalid(const SInvalidCheckArgs& args) const const auto knotCount = imagEta->getKnotCount(); for (uint8_t i=0; igetParam(i); + const auto& param = imagEta->getParameter(i); if (param.scale==0.f) continue; args.logger.log("Fresnels used for BTDFs cannot have Imaginary Eta, scale must be 0.f for all knots, is %.*e at knot %u",ELL_ERROR,param.scale,i); @@ -403,21 +404,19 @@ auto CFrontendIR::createNamedFresnel(const std::string_view name) -> typed_point const auto frH = getObjectPool().emplace(); auto* fr = getObjectPool().deref(frH); fr->debugInfo = getObjectPool().emplace(found->first); + fr->orientedRealEta = getObjectPool().emplace(3); { - CSpectralVariable::SCreationParams<3> params = {}; - params.getSemantics() = CTrueIR::ISpectralVariable::ESemantics::Fixed3_SRGB; - params.knots.params[0].scale = found->second.x.real(); - params.knots.params[1].scale = found->second.y.real(); - params.knots.params[2].scale = found->second.z.real(); - fr->orientedRealEta = getObjectPool().emplace(std::move(params)); + auto* const eta = getObjectPool().deref(fr->orientedRealEta); + eta->setSemantics(CTrueIR::ISpectralVariable::ESemantics::Fixed3_SRGB); + for (uint8_t c=0; c<3; c++) + eta->setParameter(c,{.scale=found->second[c].real()}); } + fr->orientedImagEta = getObjectPool().emplace(3); { - CSpectralVariable::SCreationParams<3> params = {}; - params.getSemantics() = CTrueIR::ISpectralVariable::ESemantics::Fixed3_SRGB; - params.knots.params[0].scale = found->second.x.imag(); - params.knots.params[1].scale = found->second.y.imag(); - params.knots.params[2].scale = found->second.z.imag(); - fr->orientedImagEta = getObjectPool().emplace(std::move(params)); + auto* const eta = getObjectPool().deref(fr->orientedImagEta); + eta->setSemantics(CTrueIR::ISpectralVariable::ESemantics::Fixed3_SRGB); + for (uint8_t c=0; c<3; c++) + eta->setParameter(c,{.scale=found->second[c].imag()}); } return frH; } @@ -501,26 +500,25 @@ void CFrontendIR::SDotPrinter::operator()(std::ostringstream& str) str << "\n}\n"; } -core::string CFrontendIR::CSpectralVariable::getLabelSuffix() const +core::string CFrontendIR::ISpectralVariableExpr::getLabelSuffix() const { if (getKnotCount()<2) return ""; constexpr const char* SemanticNames[] = { - "", + "", "\\nSemantics = Fixed3_SRGB", "\\nSemantics = Fixed3_DCI_P3", "\\nSemantics = Fixed3_BT2020", "\\nSemantics = Fixed3_AdobeRGB", "\\nSemantics = Fixed3_AcesCG" }; - auto pWonky = reinterpret_cast*>(this+1); - return SemanticNames[static_cast(pWonky->getSemantics())]; + return SemanticNames[static_cast(getSemantics())]; } -void CFrontendIR::CSpectralVariable::printDot(std::ostringstream& sstr, const core::string& selfID) const +// TODO: move `printDot` to CNodePool ? +void CFrontendIR::ISpectralVariableExpr::printDot(std::ostringstream& sstr, const core::string& selfID) const { - auto pWonky = reinterpret_cast*>(this+1); - CTrueIR::printDotParameterSet(pWonky->knots,getKnotCount(),sstr,selfID,{}); + CTrueIR::printDotParameterSet(*pWonky(),getKnotCount(),sstr,selfID,{}); } void CFrontendIR::CEmitter::printDot(std::ostringstream& sstr, const core::string& selfID) const @@ -845,7 +843,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin } case ast_expr_type_e::SpectralVariable: { - const auto varH = static_cast(node)->createIRNode(srcAST,tmpIR.get()); + const auto varH = static_cast(node)->createIRNode(srcAST,tmpIR.get()); auto* const var = irPool.deref(varH); if (!var) { @@ -855,10 +853,11 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin auto& factor = mulChain.emplace_back(); factor.handle = varH; const auto channels = var->getSpectralBins(); - factor.monochrome = channels>1; + factor.monochrome = channels<2; for (uint8_t c=0; cgetParameter(c)->scale>std::numeric_limits::min())) + const auto* const cVar = var; + if (!(cVar->getParameter(c).scale>std::numeric_limits::min())) factor.liveSpectralChannels &= ~(0b1< CTrueIR::typed_pointer_type +auto CFrontendIR::ISpectralVariableExpr::createIRNode(const CFrontendIR* ast, CTrueIR* ir) const -> CTrueIR::typed_pointer_type { - return {}; auto& irPool = ir->getObjectPool(); uint8_t realCount = 1; for (uint8_t c=0; c retval = {}; - // TODO: maybe make it wholly polymorphic like the AST ? Or even have both classes use the same struct ? - switch (realCount) - { - case 1: - { - retval = irPool.emplace>(); - break; - } - case 3: - { - retval = irPool.emplace>(); - break; - } - default: - break; - } -// == - return retval; +// CTrueIR::ISpectralVariableFactor(realCount,*static_cast(this)); + CTrueIR::CSpectralVariableFactor::CSpectralVariable(realCount,*static_cast(this)); + return {}; +// return irPool.emplace(realCount,*this); } // @@ -1002,38 +985,15 @@ auto CFrontendIR::CCookTorrance::createIRNode(const bool forBTDF, const CFronten if (ndParams.getDistribution()>CTrueIR::SBasicNDFParams::EDistribution::Beckmann) return {}; auto& irPool = ir->getObjectPool(); - CTrueIR::typed_pointer_type etaH = {}; + CTrueIR::typed_pointer_type etaH = {}; if (forBTDF) { - if (!orientedRealEta) - return {}; const auto* const srcEta = ast->getObjectPool().deref(orientedRealEta); - switch (srcEta->getSemantics()) - { - case CSpectralVariable::semantics_e::NoneUndefined: - { - assert(srcEta->getKnotCount()==1); - const auto handle = irPool.emplace>(); - auto* const dstEta = irPool.deref(handle); - if (!dstEta) - return {}; - dstEta->paramSet.uvTransform = srcEta->uvTransform(); - dstEta->paramSet.params[0] = *srcEta->getParam(0); - break; - } - default: - { - assert(srcEta->getKnotCount()==3); - const auto handle = irPool.emplace>(); - auto* const dstEta = irPool.deref(handle); - if (!dstEta) - return {}; - dstEta->paramSet.uvTransform = srcEta->uvTransform(); - for (auto i=0; i<3; i++) - dstEta->paramSet.params[i] = *srcEta->getParam(i); - break; - } - } + if (!srcEta) + return {}; + etaH = srcEta->createIRNode(ast,ir); + if (!etaH) + return {}; } const auto retval = irPool.emplace(); if (auto* const ct=irPool.deref(retval); ct) @@ -1044,4 +1004,5 @@ auto CFrontendIR::CCookTorrance::createIRNode(const bool forBTDF, const CFronten return retval; } +template class CTrueIR::CSpectralVariable; } \ No newline at end of file diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp index 3778e05ccc..11821c15bd 100644 --- a/src/nbl/asset/material_compiler3/CTrueIR.cpp +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -1,6 +1,7 @@ // 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 +#define _NBL_ASSET_MATERIAL_COMPILER3_C_TRUE_IR_CPP_ #include "nbl/asset/material_compiler3/CTrueIR.h" #include "nbl/builtin/hlsl/complex.hlsl" @@ -9,7 +10,6 @@ namespace nbl::asset::material_compiler3 { - constexpr auto ELL_ERROR = nbl::system::ILogger::E_LOG_LEVEL::ELL_ERROR; constexpr auto ELL_DEBUG = nbl::system::ILogger::E_LOG_LEVEL::ELL_DEBUG; using namespace nbl::system; @@ -88,14 +88,14 @@ CTrueIR::SBasicNodes::SBasicNodes(CTrueIR* ir) { auto* const weighted = pool.deref(node->product._const_cast()); weighted->contributor = pool.emplace(); - const auto factorH = pool.emplace>(); + const auto factorH = pool.emplace(uint8_t(3)); { auto* const factor = pool.deref(factorH); // make a magenta constant color (can do checkerboard of green & magenta in the future with a small texture) for (auto i=0; i<3; i++) - factor->paramSet.params[i].scale = i!=1 ? 1.f:0.f; + factor->setParameter(i,{.scale = i!=1 ? 1.f:0.f}); } - weighted->factor = pool.emplace>(); + weighted->factor = factorH; } node->rest = {}; } @@ -107,5 +107,5 @@ bool CTrueIR::rewrite(SMaterial& material, CTrueIR* srcIR) return false; } - +template class CTrueIR::CSpectralVariable; } \ No newline at end of file diff --git a/src/nbl/ext/MitsubaLoader/CMitsubaLoader.cpp b/src/nbl/ext/MitsubaLoader/CMitsubaLoader.cpp index 1f82bbc975..2beb94eeb8 100644 --- a/src/nbl/ext/MitsubaLoader/CMitsubaLoader.cpp +++ b/src/nbl/ext/MitsubaLoader/CMitsubaLoader.cpp @@ -505,6 +505,7 @@ parameter_t SContext::getTexture(const CElementTexture* const rootTex, hlsl::flo } +using spectral_var_t = asset::material_compiler3::CFrontendIR::CSpectralVariableExpr; template void getParameters(const std::span out, const hlsl::vector value) { @@ -516,6 +517,21 @@ void getParameters(const std::span out, const float32_t value) for (auto it=out.begin(); it!=out.end(); it++) it->scale = value; } +template requires hlsl::concepts::FloatingPoint +void getParameters(spectral_var_t* const out, const T value) +{ + out->setSemantics(spectral_var_t::ESemantics::Fixed3_SRGB); + for (uint8_t i=0; igetKnotCount(); i++) + { + parameter_t param = {}; + if constexpr (hlsl::is_vector_v) + getParameters({¶m,1},value[i]); + else + getParameters({¶m,1},value); + out->setParameter(i,std::move(param)); + } +} + hlsl::float32_t2x3 SContext::getParameters(const std::span out, const CElementTexture::SpectrumOrTexture& src) { auto retval = hlsl::math::linalg::diagonal(0.f); @@ -556,6 +572,16 @@ hlsl::float32_t2x3 SContext::getParameters(const std::span out, c } return retval; } +void SContext::getParameters(spectral_var_t* const out, const CElementTexture::SpectrumOrTexture& src) +{ + assert(out->getKnotCount()==3); + out->setSemantics(spectral_var_t::ESemantics::Fixed3_SRGB); + parameter_t params[3] = {}; + out->uvTransform() = getParameters(params,src); + for (uint8_t i=0; igetKnotCount(); i++) + out->setParameter(i,std::move(params[i])); +} + hlsl::float32_t2x3 SContext::getParameters(const std::span out, const CElementTexture::FloatOrTexture& src) { auto retval = hlsl::math::linalg::diagonal(0.f); @@ -575,8 +601,15 @@ hlsl::float32_t2x3 SContext::getParameters(const std::span out, con MitsubaLoader::getParameters(out,src.value); return retval; } +void SContext::getParameters(spectral_var_t* const out, const CElementTexture::FloatOrTexture& src) +{ + out->setSemantics(spectral_var_t::ESemantics::Fixed3_SRGB); + parameter_t param = {}; + out->uvTransform() = getParameters({¶m,1},src); + for (uint8_t i=0; igetKnotCount(); i++) + out->setParameter(i,std::move(param)); +} -using spectral_var_t = asset::material_compiler3::CFrontendIR::CSpectralVariable; auto SContext::genEmitter(const CElementEmitter* _emitter, system::ISystem* debugFileWriter) -> frontend_emitter_t { auto& frontPool = frontIR->getObjectPool(); @@ -601,13 +634,9 @@ auto SContext::genEmitter(const CElementEmitter* _emitter, system::ISystem* debu emitter->profileTransform = hlsl::math::linalg::truncate<3,3>(_emitter->transform.matrix); } } - { - spectral_var_t::SCreationParams<3> params = {}; - // if you wanted a textured emitter, this would be the place to do it - MitsubaLoader::getParameters<3>(params.knots.params,_emitter->area.radiance); - params.getSemantics() = true_ir_t::ISpectralVariable::ESemantics::Fixed3_SRGB; - mul->rhs = frontPool.emplace(std::move(params)); - } + const auto varH = frontPool.emplace(3); + MitsubaLoader::getParameters(frontPool.deref(varH),_emitter->area.radiance); + mul->rhs = varH; if (debugFileWriter) { @@ -666,13 +695,12 @@ auto SContext::genMaterial(const CElementBSDF* bsdf, system::ISystem* debugFileW auto logger = inner.params.logger; const core::string debugName = bsdf->id.empty() ? std::format("0x{:x}",ptrdiff_t(bsdf)):bsdf->id; - auto createFactorNode = [&](const CElementTexture::SpectrumOrTexture& factor, const ECommonDebug debug)->auto + auto createFactorNode = [&](const CElementTexture::SpectrumOrTexture& _factor, const ECommonDebug debug)->auto { - spectral_var_t::SCreationParams<3> params = {}; - params.knots.uvTransform = getParameters(params.knots.params,factor); - params.getSemantics() = true_ir_t::ISpectralVariable::ESemantics::Fixed3_SRGB; - const auto factorH = frontPool.emplace(std::move(params)); - frontPool.deref(factorH)->debugInfo = commonDebugNames[uint16_t(debug)]._const_cast(); + const auto factorH = frontPool.emplace(3); + auto* const factor = frontPool.deref(factorH); + getParameters(factor,_factor); + factor->debugInfo = commonDebugNames[uint16_t(debug)]._const_cast(); return factorH; }; @@ -827,14 +855,13 @@ auto SContext::genMaterial(const CElementBSDF* bsdf, system::ISystem* debugFileW roughness[0].scale = roughness[1].scale = 0.f; mul->lhs = orenNayarH; } + const auto albedoH = frontPool.emplace(3); { - spectral_var_t::SCreationParams<3> params = {}; - params.knots.uvTransform = getParameters(params.knots.params,_bsdf->diffuse.reflectance); - params.getSemantics() = true_ir_t::ISpectralVariable::ESemantics::Fixed3_SRGB; - const auto albedoH = frontPool.emplace(std::move(params)); - frontPool.deref(albedoH)->debugInfo = commonDebugNames[uint16_t(ECommonDebug::Albedo)]._const_cast(); - mul->rhs = albedoH; + auto* const albedo = frontPool.deref(albedoH); + getParameters(albedo,_bsdf->diffuse.reflectance); + albedo->debugInfo = commonDebugNames[uint16_t(ECommonDebug::Albedo)]._const_cast(); } + mul->rhs = albedoH; } leaf->brdfTop = roughDiffuseH; break; @@ -866,18 +893,16 @@ auto SContext::genMaterial(const CElementBSDF* bsdf, system::ISystem* debugFileW auto* const fresnel = frontPool.deref(fresnelH); const float extEta = _bsdf->conductor.extEta; { - spectral_var_t::SCreationParams<3> params = {}; + const auto varH = frontPool.emplace(1); const hlsl::float32_t3 eta = _bsdf->conductor.eta.vvalue.xyz; - MitsubaLoader::getParameters<3>(params.knots.params,eta/extEta); - params.getSemantics() = true_ir_t::ISpectralVariable::ESemantics::Fixed3_SRGB; - fresnel->orientedRealEta = frontPool.emplace(std::move(params)); + MitsubaLoader::getParameters(frontPool.deref(varH),eta/extEta); + fresnel->orientedRealEta = varH; } { - spectral_var_t::SCreationParams<3> params = {}; + const auto varH = frontPool.emplace(1); const hlsl::float32_t3 k = _bsdf->conductor.k.vvalue.xyz; - MitsubaLoader::getParameters<3>(params.knots.params,k/extEta); - params.getSemantics() = true_ir_t::ISpectralVariable::ESemantics::Fixed3_SRGB; - fresnel->orientedImagEta = frontPool.emplace(std::move(params)); + MitsubaLoader::getParameters(frontPool.deref(varH),k/extEta); + fresnel->orientedImagEta = varH; } } else @@ -958,11 +983,9 @@ auto SContext::genMaterial(const CElementBSDF* bsdf, system::ISystem* debugFileW const CElementTexture::FloatOrTexture constZero = {0.f}; mul->lhs = createOrenNayar(_bsdf->difftrans.transmittance,constZero,constZero); // normalize the Oren Nayar over the full sphere - { - spectral_var_t::SCreationParams<1> params = {}; - params.knots.params[0].scale = 0.5f; - mul->rhs = frontPool.emplace(std::move(params)); - } + const auto varH = frontPool.emplace(1); + frontPool.deref(varH)->setParameter(0,{.scale=0.5f}); + mul->rhs = varH; } leaf->brdfTop = diffTransH; leaf->btdf = diffTransH; @@ -1058,10 +1081,9 @@ auto SContext::genMaterial(const CElementBSDF* bsdf, system::ISystem* debugFileW const auto beerH = hasExtinction ? frontPool.emplace():frontend_ir_t::typed_pointer_type{}; if (auto* const beer=frontPool.deref(beerH); beer) { - spectral_var_t::SCreationParams<3> params = {}; - params.knots.uvTransform = getParameters(params.knots.params,sigmaA); - params.getSemantics() = true_ir_t::ISpectralVariable::ESemantics::Fixed3_SRGB; - beer->perpTransmittance = frontPool.emplace(std::move(params)); + const auto varH = frontPool.emplace(1); + getParameters(frontPool.deref(varH),sigmaA); + beer->perpTransmittance = varH; } fillCoatingLayer(coating,_bsdf->coating,_bsdf->type==CElementBSDF::ROUGHCOATING,beerH); // attach the nested as layer @@ -1102,19 +1124,18 @@ auto SContext::genMaterial(const CElementBSDF* bsdf, system::ISystem* debugFileW auto* const mix = frontPool.deref(mixH); const uint8_t realChildCount = hlsl::min(childCount,_bsdf->mixturebsdf.weightCount); const auto firstH = realChildCount>=1 ? getChildFromCache(_bsdf->mixturebsdf.bsdf[0]):frontend_ir_t::typed_pointer_type{}; - spectral_var_t::SCreationParams<1> firstParams = {}; - firstParams.knots.params[0].scale = _bsdf->mixturebsdf.weights[0]; - const auto firstWeightH = frontPool.emplace(std::move(firstParams)); + const auto firstWeightH = frontPool.emplace(1); + frontPool.deref(firstWeightH)->setParameter(0,{.scale=_bsdf->mixturebsdf.weights[0]}); if (realChildCount<2) createWeightedSum(mix,firstH,firstWeightH,{},{}); else for (uint8_t c=1; cmixturebsdf.bsdf[c]); - spectral_var_t::SCreationParams<1> params = {}; - params.knots.params[0].scale = _bsdf->mixturebsdf.weights[c]; + const auto weightH = frontPool.emplace(1); + frontPool.deref(weightH)->setParameter(0,{.scale=_bsdf->mixturebsdf.weights[c]}); // don't feel like spending time on this, since its never used, trust CTrueIR optimizer to remove this during canonicalization - createWeightedSum(mix,mixH,unityFactor._const_cast(),otherH,frontPool.emplace(std::move(params))); + createWeightedSum(mix,mixH,unityFactor._const_cast(),otherH,weightH); } newMaterialH = mixH; break; @@ -1312,19 +1333,17 @@ SContext::SContext( // { { - spectral_var_t::SCreationParams<1> params = {}; - params.knots.params[0].scale = 1.f; - unityFactor = frontPool.emplace(std::move(params)); + unityFactor = frontPool.emplace(1); + frontPool.deref(unityFactor._const_cast())->setParameter(0,{.scale=1.f}); } deltaTransmission = frontPool.emplace(); const auto mulH = frontPool.emplace(); { auto* const mul = frontPool.deref(mulH); mul->lhs = frontPool.emplace(); - spectral_var_t::SCreationParams<3> params = {}; - MitsubaLoader::getParameters<3>(params.knots.params,{1.f,0.f,1.f}); - params.getSemantics() = true_ir_t::ISpectralVariable::ESemantics::Fixed3_SRGB; - mul->rhs = frontPool.emplace(std::move(params)); + const auto varH = frontPool.emplace(3); + MitsubaLoader::getParameters(frontPool.deref(varH),float32_t3(1.f,0.f,1.f)); + mul->rhs = varH; } errorBRDF = mulH; auto constructUnsupported = [&](const std::string_view debugName)->auto From e8c79aacb138dfdc26e61affd131ddabfadee149 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Sat, 2 May 2026 03:45:11 +0200 Subject: [PATCH 23/45] final touches --- src/nbl/asset/material_compiler3/CFrontendIR.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index 733428a673..1ed1e2e91f 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -899,10 +899,7 @@ auto CFrontendIR::ISpectralVariableExpr::createIRNode(const CFrontendIR* ast, CT for (uint8_t c=0; c(this)); - CTrueIR::CSpectralVariableFactor::CSpectralVariable(realCount,*static_cast(this)); - return {}; -// return irPool.emplace(realCount,*this); + return irPool.emplace(realCount,*this); } // From 07f8886f1a2cdbed8d2af88bba18c0dd29fd2b94 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Mon, 4 May 2026 11:06:58 +0200 Subject: [PATCH 24/45] fix crash stack overflow from recursion, also spot invalid iterator access messing up our traversal --- .../nbl/asset/material_compiler3/CTrueIR.h | 2 +- .../asset/material_compiler3/CFrontendIR.cpp | 25 ++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 02788e8587..6404540e6d 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -452,7 +452,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! inline uint8_t getKnotCount() const { static_assert(sizeof(SParameter::padding)>1); - return getParameter(0).padding[1]; + return pWonky()->params[0].padding[1]; } // encapsulation due to padding abuse diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index 1ed1e2e91f..85fa91e3b2 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -754,14 +754,15 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin // the mul node gets visited after children are made while (!exprStack.empty()) { - auto& entry = exprStack.back(); - const auto* const node = astPool.deref(entry.nodeH); + auto pEntry = &exprStack.back(); + const auto* const node = astPool.deref(pEntry->nodeH); using ast_expr_type_e = CFrontendIR::IExprNode::Type; const ast_expr_type_e astExprType = node->getType(); const bool isContributor = astExprType==CFrontendIR::IExprNode::Type::Contributor; // - if (entry.notVisited()) + if (pEntry->notVisited()) { + pEntry->visited = true; if (isContributor) { const auto contributorH = static_cast(node)->createIRNode(btdfSubtree,srcAST,tmpIR.get()); @@ -773,20 +774,25 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin } contributorStack.push_back({.contributor=contributorH}); exprStack.pop_back(); + // shouldn't invalidate but lets play it safe + pEntry = nullptr; } else { const bool isAdd = astExprType==ast_expr_type_e::Add; if (isAdd) { - entry.addContributor = true; + pEntry->addContributor = true; // Current Add node will perform the job of the parent add node for this subtree - if (entry.nonMulImmediateAncestorStackEnd) - exprStack[entry.nonMulImmediateAncestorStackEnd-1].addContributor = false; + if (pEntry->nonMulImmediateAncestorStackEnd) + exprStack[pEntry->nonMulImmediateAncestorStackEnd-1].addContributor = false; } const bool notMul = astExprType!=ast_expr_type_e::Mul; // go through children const auto childCount = node->getChildCount(); + // pushing back invalidates iterators + const auto entry = *pEntry; + pEntry = nullptr; // add in reverse so stack processes in order for (auto childIx=childCount; childIx; ) { @@ -794,7 +800,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin if (isAdd && childIx!=childCount) { auto& extraEntry = exprStack.emplace_back(entry); - extraEntry.visited = true; + assert(extraEntry.visited); extraEntry.addContributor = true; } // regular exploration @@ -805,7 +811,6 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin }); } } - entry.visited = true; } else { @@ -838,7 +843,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin return (headH=errorRetval); } // when we are done we need to reset the mul chain back to its original state - mulChain.resize(entry.mulChainLen); + mulChain.resize(pEntry->mulChainLen); break; } case ast_expr_type_e::SpectralVariable: @@ -869,6 +874,8 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin } } exprStack.pop_back(); + // shouldn't invalidate but lets play it safe + pEntry = nullptr; } } // There was never an ADD node From b0c2de58056f01d9f4b1c9b794e2624ad860dfe6 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Tue, 5 May 2026 00:41:56 +0200 Subject: [PATCH 25/45] I went off the deep end with the templates --- .../asset/material_compiler3/CFrontendIR.h | 1 + .../nbl/asset/material_compiler3/CTrueIR.h | 55 ++++++++++++---- .../asset/material_compiler3/CFrontendIR.cpp | 66 ++++++++++++------- 3 files changed, 85 insertions(+), 37 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index b436a125a3..e5b1a23a85 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -778,6 +778,7 @@ class CFrontendIR final : public CNodePool astPrinter.exprStack.push_back(nodeH); args.logger.log("Subtree Dot3 : \n%s\n",system::ILogger::ELL_DEBUG,astPrinter().c_str()); assert(astPrinter.exprStack.empty()); + astPrinter.visitedNodes.clear(); } using oriented_material_t = CTrueIR::SMaterial::SOriented; diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 6404540e6d..8dfe0e09d4 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -250,12 +250,15 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! protected: uint64_t padding = 0; }; +#define TYPE_NAME_STR(NAME) "nbl::asset::material_compiler3::CTrueIR::"#NAME // we step away from our usual way of doing things via linked lists, because this is simpler to rearrange class CFactorCombiner final : public obj_pool_type::IVariableSize, public IFactor { public: inline EFinalType getFinalType() const override {return EFinalType::CFactorCombiner;} + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CFactorCombiner);} + // There's only two ops we support enum Type : uint8_t { @@ -270,15 +273,40 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! uint64_t childIxComplementMask : 57 = 0x0u; }; static_assert(sizeof(SState) == sizeof(IFactor::padding)); + + // + static inline uint32_t calc_size(const SState state) + { + return sizeof(CFactorCombiner)+sizeof(child[0])*(state.childCount-1); + } + inline CFactorCombiner(const SState state) + { + padding = std::bit_cast(state); + } + + // inline SState getState() const {return std::bit_cast(padding);} + // + inline void setComplementMask(const uint64_t mask) + { + assert(mask < (0x1<<57)); + auto state = getState(); + state.childIxComplementMask = mask; + padding = std::bit_cast(state); + } // Only sane child count allowed - inline typed_pointer_type getChildHandle(const uint8_t ix) + inline typed_pointer_type getChildHandle(const uint8_t ix) const { if (ix handle) + { + if (ix child[1] = {{}}; }; -#define TYPE_NAME_STR(NAME) "nbl::asset::material_compiler3::CTrueIR::"#NAME // Note that this is not a root node, its a flipped leaf! class CWeightedContributor final : public obj_pool_type::INonTrivial, public INode { @@ -489,6 +515,11 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! protected: inline ISpectralVariable() = default; + // delete all these so we dont implicitly copy + inline ISpectralVariable(const ISpectralVariable&) = delete; + inline ISpectralVariable(ISpectralVariable&&) = delete; + inline ISpectralVariable& operator=(const ISpectralVariable&) = delete; + inline ISpectralVariable& operator=(ISpectralVariable&&) = delete; // fill out the params later inline void init(const uint8_t knotCount) { @@ -544,16 +575,16 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! inline SParameterSet<1>* pWonky() override final {return reinterpret_cast*>(this+1);} public: - inline CSpectralVariable(const uint8_t knotCount) {ISpectralVariable::init(knotCount);} - template requires std::is_base_of_v - inline CSpectralVariable(const U& other) {ISpectralVariable::init(other);} - template requires std::is_base_of_v - inline CSpectralVariable(const uint8_t knotCount, const U& other) {ISpectralVariable::init(knotCount,other);} + inline CSpectralVariable(const uint8_t knotCount) : OtherBase() {ISpectralVariable::init(knotCount);} + inline CSpectralVariable(const ISpectralVariable& other) : OtherBase() {ISpectralVariable::init(other);} + inline CSpectralVariable(const uint8_t knotCount, const ISpectralVariable& other) : OtherBase() {ISpectralVariable::init(knotCount,other);} // inline _typed_pointer_type copy(obj_pool_type& pool) const { - return pool.emplace(*this); + // need to coax the compiler into compiling + const ISpectralVariable& rThis = *this; + return pool.emplace(rThis); } // @@ -562,13 +593,11 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! return sizeof(this_t)+sizeof(SParameterSet<1>)+sizeof(SParameter)*(knotCount-1); } // for copying - template requires std::is_base_of_v - static inline uint32_t calc_size(const U& other) + static inline uint32_t calc_size(const ISpectralVariable& other) { return calc_size(other.getKnotCount()); } - template requires std::is_base_of_v - static inline uint32_t calc_size(const uint8_t knotCount, const U& other) + static inline uint32_t calc_size(const uint8_t knotCount, const ISpectralVariable& other) { return calc_size(knotCount); } diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index 85fa91e3b2..86fe88060f 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -599,7 +599,7 @@ CTrueIR::SMaterialHandle CFrontendIR::SAdd2IRSession::makeFinalIR(const typed_po // TODO: better debug info (e.g. concat all the layer info during `makeOrientedMaterial` via the `session` object if (const auto* debug=astPool.deref(astRoot->debugInfo); debug && !debug->data().empty()) { - material.debugInfo = args.ir->getObjectPool().emplace(debug->data().data(),debug->data().size()); + material.debugInfo = args.ir->getObjectPool().emplace(debug->data().data(),static_cast(debug->data().size())); } } return retval; @@ -735,7 +735,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin if (headH!=errorRetval) return; printSubtree(exprStack.back().nodeH); - args.logger.log("Within BxDF:\n",ELL_DEBUG,astPrinter().c_str()); + args.logger.log("Within BxDF:\n",ELL_DEBUG); printSubtree(bxdfRootH); // no point pushing an error contributor, don't want a best effort compilation within a layer, don't want contributors missing or substituted exprStack.clear(); @@ -859,12 +859,25 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin factor.handle = varH; const auto channels = var->getSpectralBins(); factor.monochrome = channels<2; + // this wont be affected by sorting + if (mulChain.size()>=2) + factor.liveSpectralChannels = mulChain[mulChain.size()-2].liveSpectralChannels; for (uint8_t c=0; cgetParameter(c).scale>std::numeric_limits::min())) factor.liveSpectralChannels &= ~(0b1<nodeH); + args.logger.log("Forms a 0 constant factor across its ancestors in the mul chain, within the BxDF:\n",logLevel); + printSubtree(bxdfRootH); + // TODO handle a 0 factor + assert(false); + } break; } default: @@ -920,35 +933,40 @@ auto CFrontendIR::SAdd2IRSession::popContributor() -> CTrueIR::typed_pointer_typ contributorStack.pop_back(); if (!mulChain.empty()) { - return {}; - assert(false); // unimplemented + assert(mulChain.back().liveSpectralChannels); + // now scan the stuff const CTrueIR::CFactorCombiner::SState combinerState = { .type = CTrueIR::CFactorCombiner::Type::Mul, .childCount = mulChain.size() }; - // TODO: create the combiner node - //const auto factorH = getObjectPool().emplace(); + // every contributor node gets its own SORTED ancestor prefix + mulChainSortScratch = mulChain; + // Multiplication Chain need to be sorted in a canonical order so its easier to spot them being the same + auto sortMuls = [](const SFactor& lhs, const SFactor& rhs)->bool { - // every contributor node gets its own SORTED ancestor prefix - mulChainSortScratch = mulChain; - // Multiplication Chain need to be sorted in a canonical order so its easier to spot them being the same - auto sortMuls = [](const SFactor& lhs, const SFactor& rhs)->bool + // monochrome is cheaper + if (lhs.monochrome!=rhs.monochrome) + return lhs.monochrome; + // not doing a complement/negation is cheaper + if (lhs.negate!=rhs.negate) + return rhs.negate; + // DO NOT sort by live spectral channels + // but want negations to show up together in the sorted list so easier to put back together + return lhs.handle.value(combinerState); + { + auto* const factor = irPool.deref(factorH); + auto i = 0; + for (const auto& mul : mulChainSortScratch) { - // monochrome is cheaper - if (lhs.monochrome!=rhs.monochrome) - return lhs.monochrome; - // not doing a complement is cheaper - if (lhs.handle.value==rhs.handle.value) - return lhs.negatechild; - //for (const auto& mul : mulChainSortScratch) - //*(oit++) = mul.handle; + assert(!mul.negate); // TODO: handle later + factor->setChildHandle(i++,mul.handle); + } } - //weighted->factor = factorH; + weighted->factor = factorH; } return retval; } From 14a3bf757ef9b77fa8fa24890e05c5b62eeaee7f Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Tue, 5 May 2026 01:04:16 +0200 Subject: [PATCH 26/45] handle exit due to RGB masking each other to 0 --- .../asset/material_compiler3/CFrontendIR.cpp | 52 +++++++++++++------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index 86fe88060f..e1ab1200f0 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -719,7 +719,7 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ return retval; } -// +// TODO: this really needs a visited AST nodes (or at least subtrees) cache! auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_pointer_type bxdfRootH) -> CTrueIR::typed_pointer_type { CTrueIR::typed_pointer_type headH = {}; @@ -825,22 +825,25 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin } case ast_expr_type_e::Add: { - // we visited the leftmost subtrees first so this is the right order + if (pEntry->addContributor) { - auto* const tail = irPool.deref(tailH); - tailH = irPool.emplace(); - if (tailH) - tail->rest = tailH; + // we visited the leftmost subtrees first so this is the right order + { + auto* const tail = irPool.deref(tailH); + tailH = irPool.emplace(); + if (tailH) + tail->rest = tailH; + else + headH = tailH; + } + // add current contributor with weight to BxDF Sum + if (const auto contributor=popContributor(); contributor) + irPool.deref(tailH)->product = contributor; else - headH = tailH; - } - // add current contributor with weight to BxDF Sum - if (const auto contributor=popContributor(); contributor) - irPool.deref(tailH)->product = contributor; - else - { - args.logger.log("Failed to Pop the Contributor from the Stack, most likely failed to create the factor node chain.",ELL_ERROR); - return (headH=errorRetval); + { + args.logger.log("Failed to Pop the Contributor from the Stack, most likely failed to create the factor node chain.",ELL_ERROR); + return (headH=errorRetval); + } } // when we are done we need to reset the mul chain back to its original state mulChain.resize(pEntry->mulChainLen); @@ -875,8 +878,23 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin printSubtree(pEntry->nodeH); args.logger.log("Forms a 0 constant factor across its ancestors in the mul chain, within the BxDF:\n",logLevel); printSubtree(bxdfRootH); - // TODO handle a 0 factor - assert(false); + // cancel exploration of all descendands of our first non mul node + exprStack.resize(pEntry->nonMulImmediateAncestorStackEnd); + pEntry = nullptr; + // if there was nothing else in the tree, we can bail out the whole material + if (exprStack.empty()) + { + mulChain.clear(); + // for the MUL subtree to form the whole tree, there needs to be only one contributor in the stack + assert(contributorStack.size()==1); + contributorStack.clear(); + args.logger.log("This MUL subtree is the only subtree, returning no contributors for this Tree.",logLevel); + return headH; + } + // don't add this MUL subtree island's contributor, it won't exist + pEntry = &exprStack.back(); + pEntry->addContributor = false; + contributorStack.pop_back(); } break; } From ccf4a6cc6789d868b6cd7cb2f58d2157276f1f1e Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Tue, 5 May 2026 01:16:43 +0200 Subject: [PATCH 27/45] dont hash emitter's sampler and image view state when IES profile is not used --- examples_tests | 2 +- .../nbl/asset/material_compiler3/CTrueIR.h | 25 +++++++++++-------- .../asset/material_compiler3/CFrontendIR.cpp | 2 +- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/examples_tests b/examples_tests index ebd86b3a01..d9f6b8d7c5 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit ebd86b3a01de50631387d18d42c34fe7f6032b82 +Subproject commit d9f6b8d7c52cd0248c2105cae6604d8063c68564 diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 8dfe0e09d4..d2fd492d62 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -640,16 +640,21 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override { hasher << profileTransform; - // we ignore most of the sampler, needs to be set always the same - const auto& sampler = profile.sampler; - using namespace ::nbl::asset; - if (sampler.BorderColor!=ISampler::E_TEXTURE_BORDER_COLOR::ETBC_FLOAT_TRANSPARENT_BLACK || sampler.MaxFilter!=ISampler::E_TEXTURE_FILTER::ETF_LINEAR) - return false; - using clamp_e = hlsl::TextureClamp; - if (sampler.TextureWrapW!=clamp_e::ETC_CLAMP_TO_EDGE) - return false; - // there's a limited set of symmetries we can exploit in our IES tabulations, TODO: check which (probably not REPEAT) - hasher << profile; + if (profile.view) + { + // we ignore most of the sampler, needs to be set always the same + const auto& sampler = profile.sampler; + using namespace ::nbl::asset; + if (sampler.BorderColor!=ISampler::E_TEXTURE_BORDER_COLOR::ETBC_FLOAT_TRANSPARENT_BLACK || sampler.MaxFilter!=ISampler::E_TEXTURE_FILTER::ETF_LINEAR) + return false; + using clamp_e = hlsl::TextureClamp; + if (sampler.TextureWrapW!=clamp_e::ETC_CLAMP_TO_EDGE) + return false; + // there's a limited set of symmetries we can exploit in our IES tabulations, TODO: check which (probably not REPEAT) + hasher << profile; + } + else + hasher << profile.scale; return true; } diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index e1ab1200f0..fe8d8ed36a 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -995,7 +995,7 @@ auto CFrontendIR::CEmitter::createIRNode(const bool forBTDF, const CFrontendIR* assert(!forBTDF); auto& irPool = ir->getObjectPool(); const auto retval = irPool.emplace(); - if (auto* const contributor=irPool.deref(retval)) + if (auto* const contributor=irPool.deref(retval); contributor) { contributor->profile = profile; contributor->profileTransform = profileTransform; From adc994897cd306779e2b7196313db855deb6bfd3 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Tue, 5 May 2026 15:34:13 +0200 Subject: [PATCH 28/45] separate out the oriented material metadata from the root layer, start the IR printing stub, start the rewriter --- examples_tests | 2 +- .../asset/material_compiler3/CFrontendIR.h | 8 + .../nbl/asset/material_compiler3/CTrueIR.h | 147 ++++++++++++++---- .../asset/material_compiler3/CFrontendIR.cpp | 61 ++++---- src/nbl/asset/material_compiler3/CTrueIR.cpp | 45 +++++- 5 files changed, 199 insertions(+), 64 deletions(-) diff --git a/examples_tests b/examples_tests index d9f6b8d7c5..1faea78b5d 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit d9f6b8d7c52cd0248c2105cae6604d8063c68564 +Subproject commit 1faea78b5dbd139f2baa41f399568ae7061a2f60 diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index e5b1a23a85..573c197860 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -780,6 +780,13 @@ class CFrontendIR final : public CNodePool assert(astPrinter.exprStack.empty()); astPrinter.visitedNodes.clear(); } + inline void printIRLayer(const CTrueIR::typed_pointer_type layerH, const CTrueIR* ir) + { + irPrinter.reset(ir); + irPrinter.layerStack.push_back(layerH); + args.logger.log("IR Layer Dot3 : \n%s\n",system::ILogger::ELL_DEBUG,irPrinter().c_str()); + irPrinter.visitedNodes.clear(); + } using oriented_material_t = CTrueIR::SMaterial::SOriented; NBL_API2 oriented_material_t makeOrientedMaterial(const CFrontendIR::typed_pointer_type rootH, const CFrontendIR* _srcAST); @@ -797,6 +804,7 @@ class CFrontendIR final : public CNodePool // changes dynamically const CFrontendIR* srcAST; SDotPrinter astPrinter; + CTrueIR::SDotPrinter irPrinter; bool btdfSubtree = false; // for going over layers in the AST core::vector layerStack; diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index d2fd492d62..1c69da4645 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -822,39 +822,45 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // Each material comes down to this, this is the only struct we don't de-duplicate struct SMaterial { - // Stats needed by a renderer to skip loading parts of a material or remove expensive code altogether - enum class EMetadataBits : uint16_t - { - None = 0, - // if any such contributor present - NotBlackhole = 0x1u<<0, // actually have a material - NonDelta = 0x1u<<1, // can evaluate against point lights (or other samplings) - DeltaTransmissive = 0x1u<<2, // can use stochastic transparency for closest hit rays and blending for anyhit - NonSpatiallyVaryingEmissive = 0x1u<<3, // definitely register for NEE - SpatiallyVaryingEmissive = 0x1u<<4, // maybe register for NEE but needs different kind of NEE - // TODO: 5,6,7 left - // Bits that help us remove expensive code from impl - DerivativeMap = 0x1u<<8, - DirectionallyVaryingEmissive = 0x1u<<9, // IES profile - Lambertian = 0x1u<<10, - OrenNayar = 0x1u<<11, - GGX = 0x1u<<12, - AnisotropicGGX = 0x1u<<13, - Beckmann = 0x1u<<14, - AnisotropicBeckmann = 0x1u<<15, - }; // - struct SOriented + struct SMetadata { - // null means no material - typed_pointer_type root = {}; + // Stats needed by a renderer to skip loading parts of a material or remove expensive code altogether + enum class ECapabilityBits : uint16_t + { + None = 0, + // if any such contributor present + NotBlackhole = 0x1u<<0, // actually have a material + NonDelta = 0x1u<<1, // can evaluate against point lights (or other samplings) + DeltaTransmissive = 0x1u<<2, // can use stochastic transparency for closest hit rays and blending for anyhit + NonSpatiallyVaryingEmissive = 0x1u<<3, // definitely register for NEE + SpatiallyVaryingEmissive = 0x1u<<4, // maybe register for NEE but needs different kind of NEE + // TODO: 5,6,7 left + // Bits that help us remove expensive code from impl + DerivativeMap = 0x1u<<8, + DirectionallyVaryingEmissive = 0x1u<<9, // IES profile + Lambertian = 0x1u<<10, + OrenNayar = 0x1u<<11, + GGX = 0x1u<<12, + AnisotropicGGX = 0x1u<<13, + Beckmann = 0x1u<<14, + AnisotropicBeckmann = 0x1u<<15, + }; // constexpr static inline uint8_t MaxUVSlots = 32; std::bitset usedUVSlots = {}; // the tangent frames are a subset of used UV slots, unless there's an anisotropic BRDF involved std::bitset usedTangentFrames = {}; // - core::bitflag metadata = {}; + core::bitflag capabilities = {}; + }; + // + struct SOriented + { + // null means no material + typed_pointer_type root = {}; + // + SMetadata metadata = {}; }; SOriented front = {}; SOriented back = {}; @@ -883,6 +889,41 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // create the `BlackholeMaterialHandle` m_materials = {SMaterial{.debugInfo=getObjectPool().emplace("CTrueIR's BlackHole Material")}}; } + + //! Utitilies for copying from other IR into ours while optimizing + // This one doesn't touch your coated nodes, but only the first coating + inline bool rewriteSingleLayer(typed_pointer_type& singleLayer, SMaterial::SMetadata& metadata, CTrueIR* srcIR) + { + SRewriteSession session = SRewriteArgs{.src=srcIR,.dst=this,.pMetadata=&metadata}; + return session.rewriteSingleLayer(singleLayer); + } + inline bool rewrite(SMaterial::SOriented& material, CTrueIR* srcIR) + { + SRewriteSession session = SRewriteArgs{.src=srcIR,.dst=this,.pMetadata=&material.metadata}; + return session.rewrite(material.root); + } + inline bool rewrite(SMaterial& material, CTrueIR* srcIR) + { + SRewriteSession session = SRewriteArgs{.src=srcIR,.dst=this}; + if (material.front.root) + { + session.changeMetadata(&material.front.metadata); + session.rewrite(material.front.root); + } + if (material.back.root) + { + session.changeMetadata(&material.back.metadata); + session.rewrite(material.back.root); + } + if (material.debugInfo && srcIR!=this) + { + // TODO: copy the debug info across into new pool + assert(false); + material = {}; + return false; + } + return true; + } // TODO: Optimization passes on the IR // It is the backend's job to handle: @@ -907,6 +948,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! struct SDotPrinter final { public: + inline SDotPrinter() = default; inline SDotPrinter(const CTrueIR* ir) : m_ir(ir) {} // assign in reverse because we want materials to print in order inline SDotPrinter(const CTrueIR* ir, std::span roots) : m_ir(ir)//, layerStack(roots.rbegin(),roots.rend()) @@ -915,6 +957,13 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! visitedNodes.reserve(roots.size()<<4); } + inline void reset(const CTrueIR* ir) + { + visitedNodes.clear(); + layerStack.clear(); + m_ir = ir; + } + NBL_API2 void operator()(std::ostringstream& output); inline core::string operator()() { @@ -924,11 +973,10 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! } core::unordered_set> visitedNodes; -#if 0 // TODO: track layering depth and indent accordingly? - core::vector> layerStack; - core::stack> exprStack; -#endif + core::vector> layerStack; +// core::stack> exprStack; + private: const CTrueIR* m_ir; }; @@ -954,17 +1002,48 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! { const auto uvTransformID = selfID+"_uvTransform"; sstr << "\n\t" << uvTransformID << " [label=\"uvSlot = " << std::to_string(_set.uvSlot()) << "\\n"; - printMatrix(sstr,*reinterpret_cast(_set.params+_count)); + printMatrix(sstr,_set.uvTransform); sstr << "\"]"; sstr << "\n\t" << selfID << " -> " << uvTransformID << "[label=\"UV Transform\"]"; } } protected: - NBL_API2 CTrueIR(creation_params_type&& params); + struct SRewriteArgs final + { + inline bool needDeepCopy() const {return src!=dst;} + + const CTrueIR* const src; + CTrueIR* const dst; + const SMaterial::SMetadata* pMetadata; + }; + struct SRewriteSession final + { + public: + inline SRewriteSession(const SRewriteArgs& _args) : args(_args) {} + NBL_API2 ~SRewriteSession(); + + inline void changeMetadata(SMaterial::SMetadata* pMetadata) {args.pMetadata = pMetadata;} + + NBL_API2 bool rewrite(typed_pointer_type& oriented); + NBL_API2 bool rewriteSingleLayer(typed_pointer_type& oriented); + + private: + template + inline typed_pointer_type emplace(FuncArgs&&... args) + { + const auto retval = args.dst->getObjectPool().emplace(1u,std::forward(args)...); + if (retval) + createdNodes.push_back(retval); + return retval; + } - // copies from other IR into ours and makes sure things get hashed properly - NBL_API2 bool rewrite(SMaterial& material, CTrueIR* srcIR); + SRewriteArgs args; + core::vector> createdNodes; + bool success = true; + }; + + NBL_API2 CTrueIR(creation_params_type&& params); core::vector m_materials; // TODO: either we put the typeid in the hash, or we have a type of hashmap per type @@ -975,7 +1054,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! template class CTrueIR::CSpectralVariable; -NBL_ENUM_ADD_BITWISE_OPERATORS(CTrueIR::SMaterial::EMetadataBits) +NBL_ENUM_ADD_BITWISE_OPERATORS(CTrueIR::SMaterial::SMetadata::ECapabilityBits) } diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index fe8d8ed36a..bf2c48ce7c 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -554,10 +554,6 @@ void CFrontendIR::CCookTorrance::printDot(std::ostringstream& sstr, const core:: //! IR making CTrueIR::SMaterialHandle CFrontendIR::SAdd2IRSession::makeFinalIR(const typed_pointer_type rootH, const CFrontendIR* ast) { - // TODO -// core::unordered_map brdfs; -// core::unordered_map btdfs; - const auto& astPool = ast->getObjectPool(); const auto* astRoot = astPool.deref(rootH); // no material @@ -567,6 +563,7 @@ CTrueIR::SMaterialHandle CFrontendIR::SAdd2IRSession::makeFinalIR(const typed_po // reverse AST into another tree tmpAST->reset(); const auto backRootH = tmpAST->reverse(rootH,ast); + // do not attempt and cache these, a normal person will not send the same material over and over to convert to IR CTrueIR::SMaterial material = { .front = makeOrientedMaterial(rootH,ast), .back = makeOrientedMaterial(backRootH,tmpAST.get()) @@ -593,7 +590,7 @@ CTrueIR::SMaterialHandle CFrontendIR::SAdd2IRSession::makeFinalIR(const typed_po return {}; } - auto retval = args.ir->addMaterial(material); + auto retval = args.ir->addMaterial(material,tmpIR.get()); if (retval) { // TODO: better debug info (e.g. concat all the layer info during `makeOrientedMaterial` via the `session` object @@ -609,6 +606,13 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ { oriented_material_t retval = {}; + // TODO: cache root expressions for layers since they tend to be reused quite a bit + // HOWEVER the `tmpAST` gets cleared every call to `makeOrientedMaterial` so only cache within a `makeOrientedMaterial` call, + // so clear when AST changes or keep separate caches for original AST and tmp. +// core::unordered_map layers; +// core::unordered_map topBRDFcache; +// core::unordered_map BTDFcache; + srcAST = _srcAST; astPrinter.reset(srcAST); @@ -644,7 +648,6 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ const auto errorLayer = tmpIR->getBasicNodes().errorLayer; // Some metadata needed for us bool layersBelowCanScatterBack = false; - CTrueIR::typed_pointer_type prevLayerH = {}; // then go in reverse and do the layers for (auto layerIt=layerStack.rbegin(); layerIt!=layerStack.rend(); layerIt++) { @@ -677,40 +680,46 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ if (transmission->brdfBottom==errorBxDF) return {.root=errorLayer}; } - // we check if previous layer didn't get oprimized away, but we don't add its optimized version because don't want pointers across two pools (crash) + // we check if previous layer didn't get oprimized away if (retval.root) - transmission->coated = prevLayerH; + transmission->coated = retval.root; } } } - prevLayerH = layerH; + retval.root = layerH; // Now optimize everything inserting it into the proper IR { - // TODO: pass which copies the layers over into full IR while optimizing - retval.root = layerH; // wrong pool handle - auto* const outLayer = irPool.deref(layerH); // wrong IR Pool - - // ideas for the pass: - // - skip replace delta transmissions by the layer undernearth, if null then keep as delta - // - if BTDF has delta transmissions, then via the sampling property hoist next layer into current layer BRDFs with the DeltaTransmission weights applied - // - order the `CWeightedContributor` within the `CContributorSum` linked list so emitters are first, and so on (null products go last) - // observations: - // - Any V-dependent factors cannot be commonalized across layers, most of the CSE has to be done within a layer - - // TODO: combine layer meta with `retval.metadata` - + // temporary debug print + constexpr bool DebugBeforeAndAfterOpt = true; + if constexpr(DebugBeforeAndAfterOpt) + { + args.logger.log("Before optimization:",ELL_DEBUG); + printIRLayer(layerH,tmpIR.get()); + } + // avoid O(Layer^2) scaling by processing bottom layers over and over + if (!tmpIR->rewriteSingleLayer(retval.root,retval.metadata,tmpIR.get())) + { + args.logger.log("Failed to rewrite and optimize IR layer (printing layer and everything it coats):\n",ELL_ERROR); + printIRLayer(layerH,tmpIR.get()); + return {.root=errorLayer}; + } + // Now remember that our rewriter can optimize us into a blackhole/null layer + if constexpr(DebugBeforeAndAfterOpt) + { + args.logger.log("After optimization:",ELL_DEBUG); + printIRLayer(retval.root,tmpIR.get()); + } // Set this for the next layer after us, we are reflective or previous layer scatters back towards us and we don't block it - if (outLayer) + if (auto* const outLayer=irPool.deref(retval.root); outLayer) layersBelowCanScatterBack = bool(outLayer->brdfTop) || layersBelowCanScatterBack && outLayer->firstTransmission; else layersBelowCanScatterBack = false; } } - // last touch ups + // last checks if (retval.root) { - // might turn this into an assert - retval.metadata |= CTrueIR::SMaterial::EMetadataBits::NotBlackhole; +// assert(retval.metadata.capabilities|CTrueIR::SMaterial::SMetadata::ECapabilityBits::NotBlackhole); } else { diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp index 11821c15bd..af55acec14 100644 --- a/src/nbl/asset/material_compiler3/CTrueIR.cpp +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -101,10 +101,49 @@ CTrueIR::SBasicNodes::SBasicNodes(CTrueIR* ir) } } -bool CTrueIR::rewrite(SMaterial& material, CTrueIR* srcIR) +void CTrueIR::SDotPrinter::operator()(std::ostringstream& output) { - // TODO: hash, deduplicate, collect metadata and insert into current IR - return false; + output << "IR PRINTING UnIMPLEMENTED!\n"; +} + +CTrueIR::SRewriteSession::~SRewriteSession() +{ + if (success) + return; + // if session was not a success, then clean up all the nodes + for (const auto& handle : createdNodes) + args.dst->getObjectPool()._delete(handle,1); +} + +bool CTrueIR::SRewriteSession::rewrite(typed_pointer_type& oriented) +{ + if (!success) + return false; + + success = args.src==args.dst; + // TODO: go through the layers +// assert(success); + return success; +} + +bool CTrueIR::SRewriteSession::rewriteSingleLayer(typed_pointer_type& oriented) +{ + if (!success) + return false; + // ideas for the pass: + // - skip replace delta transmissions by the layer undernearth, if null then keep as delta + // - if BTDF has delta transmissions, then via the sampling property hoist next layer into current layer BRDFs with the DeltaTransmission weights applied + // - order the `CWeightedContributor` within the `CContributorSum` linked list so emitters are first, and so on (null products go last) + // observations: + // - Any V-dependent factors cannot be commonalized across layers, most of the CSE has to be done within a layer + + // TODO: combine layer meta with `retval.metadata` + + // TODO: deduplicate, collect metadata and insert into current IR + + success = args.src==args.dst; + assert(success); + return success; } template class CTrueIR::CSpectralVariable; From 2c8ee90795e9954fc0c520b61dbe6bbadc66f945 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Tue, 5 May 2026 16:12:57 +0200 Subject: [PATCH 29/45] refactor the ISampler usage in CTrueIR::SParameter --- examples_tests | 2 +- .../nbl/asset/material_compiler3/CTrueIR.h | 83 ++++++++++++------- 2 files changed, 56 insertions(+), 29 deletions(-) diff --git a/examples_tests b/examples_tests index 1faea78b5d..ceb0b3fbeb 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit 1faea78b5dbd139f2baa41f399568ae7061a2f60 +Subproject commit ceb0b3fbeb03aeb8c821093f0eec4b2000b603e3 diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 1c69da4645..c2247403b1 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -44,12 +44,23 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! { if (scale!=other.scale) return true; - if (viewChannel!=other.viewChannel) - return true; + if (view) + { + if (viewChannel!=other.viewChannel) + return true; + if (wrapU!=other.wrapU) + return true; + if (wrapV!=other.wrapV) + return true; + if (wrapW!=other.wrapW) + return true; + if (borderColor!=other.borderColor) + return true; + if (linearMagnification!=other.linearMagnification) + return true; + } // don't compare paddings! - if (view!=other.view) - return true; - return sampler!=other.sampler; + return view!=other.view; } inline bool operator==(const SParameter& other) const {return !operator!=(other);} @@ -57,13 +68,17 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // at this stage we store the multipliers in highest precision float scale = std::numeric_limits::infinity(); - // rest are ignored if the view is null + // rest are ignored if the view is null, only take the things that matter from ISampler + static_assert(std::is_same_v,uint16_t>); uint16_t viewChannel : 2 = 0; + uint16_t linearMagnification : 1 = true; + hlsl::TextureClamp wrapU : 3 = hlsl::TextureClamp::ETC_REPEAT; + hlsl::TextureClamp wrapV : 3 = hlsl::TextureClamp::ETC_REPEAT; + hlsl::TextureClamp wrapW : 3 = hlsl::TextureClamp::ETC_REPEAT; + uint16_t borderColor : 3 = ISampler::E_TEXTURE_BORDER_COLOR::ETBC_FLOAT_OPAQUE_BLACK; + // 1 bit left over uint8_t padding[2] = {0,0}; // TODO: padding stores metadata, shall we exclude from assignment and copy operators? core::smart_refctd_ptr view = {}; - // lodbias and clamp shadow comparison functions, anisotropy and minFilter are ignored - // NOTE: could take only things that matter from the sampler and pack the viewChannel and reduce padding - ICPUSampler::SParams sampler = {}; }; // We worry about keeping parameters in the same image view later (the backend) template @@ -643,15 +658,14 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! if (profile.view) { // we ignore most of the sampler, needs to be set always the same - const auto& sampler = profile.sampler; - using namespace ::nbl::asset; - if (sampler.BorderColor!=ISampler::E_TEXTURE_BORDER_COLOR::ETBC_FLOAT_TRANSPARENT_BLACK || sampler.MaxFilter!=ISampler::E_TEXTURE_FILTER::ETF_LINEAR) - return false; - using clamp_e = hlsl::TextureClamp; - if (sampler.TextureWrapW!=clamp_e::ETC_CLAMP_TO_EDGE) - return false; + auto copy = profile; + copy.wrapU = hlsl::TextureClamp::ETC_CLAMP_TO_EDGE; + copy.wrapV = hlsl::TextureClamp::ETC_CLAMP_TO_EDGE; + copy.wrapW = hlsl::TextureClamp::ETC_CLAMP_TO_EDGE; + copy.linearMagnification = true; + copy.borderColor = ISampler::E_TEXTURE_BORDER_COLOR::ETBC_FLOAT_TRANSPARENT_BLACK; // there's a limited set of symmetries we can exploit in our IES tabulations, TODO: check which (probably not REPEAT) - hasher << profile; + hasher << copy; } else hasher << profile.scale; @@ -1132,13 +1146,13 @@ inline void CTrueIR::SParameter::printDot(std::ostringstream& sstr, const core:: { sstr << "\\nchannel = " << std::to_string(viewChannel); const auto& viewParams = view->getCreationParameters(); - sstr << "\\nWraps = {" << sampler.TextureWrapU; + sstr << "\\nWraps = {" << wrapU; if (viewParams.viewType!=ICPUImageView::ET_1D && viewParams.viewType!=ICPUImageView::ET_1D_ARRAY) - sstr << "," << sampler.TextureWrapV; + sstr << "," << wrapV; if (viewParams.viewType==ICPUImageView::ET_3D) - sstr << "," << sampler.TextureWrapW; - sstr << "}\\nBorder = " << sampler.BorderColor; - // don't bother printing the rest, we really don't care much about those + sstr << "," << wrapW; + // TODO: conditionally print this if wrap modes require it + sstr << "}\\nBorder = " << borderColor; } sstr << "\"]"; // TODO: do specialized printing for image views (they need to be gathered into a view set -> need a printing context struct) @@ -1191,25 +1205,38 @@ struct core::blake3_hasher::update_impl } // in the future this might change hasher << param.viewChannel; - const auto& sampler = param.sampler; - hasher << sampler.BorderColor; - hasher << sampler.MaxFilter; + hasher << param.linearMagnification; + bool wrapModeUsesBorder = false; + auto hashWrap = [&](const hlsl::TextureClamp wrap)->void + { + hasher << wrap; + switch (wrap) + { + case hlsl::TextureClamp::ETC_CLAMP_TO_BORDER: + wrapModeUsesBorder = true; + break; + default: + break; + } + }; using view_type_e = asset::IImageView::E_TYPE; switch (viewParams.viewType) { case view_type_e::ET_3D: - hasher << sampler.TextureWrapW; + hashWrap(param.wrapW); [[fallthrough]]; case view_type_e::ET_2D: [[fallthrough]]; case view_type_e::ET_2D_ARRAY: [[fallthrough]]; case view_type_e::ET_CUBE_MAP: [[fallthrough]]; case view_type_e::ET_CUBE_MAP_ARRAY: - hasher << sampler.TextureWrapV; + hashWrap(param.wrapV); [[fallthrough]]; default: - hasher << sampler.TextureWrapU; + hashWrap(param.wrapU); break; } + if (wrapModeUsesBorder) + hasher << param.borderColor; } }; template From 47f15f7d473ec4081e75f734aa01a80f51fea8d2 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Tue, 5 May 2026 18:24:41 +0200 Subject: [PATCH 30/45] properly account for mulchains not going all the way back to the root because there's non-MUL nodes along the way too --- .../asset/material_compiler3/CFrontendIR.cpp | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index bf2c48ce7c..b656dbceed 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -765,6 +765,8 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin { auto pEntry = &exprStack.back(); const auto* const node = astPool.deref(pEntry->nodeH); + assert(node); + // using ast_expr_type_e = CFrontendIR::IExprNode::Type; const ast_expr_type_e astExprType = node->getType(); const bool isContributor = astExprType==CFrontendIR::IExprNode::Type::Contributor; @@ -802,22 +804,37 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin // pushing back invalidates iterators const auto entry = *pEntry; pEntry = nullptr; + // making sure we visit this node again each time a subtree of an Add node is done + bool secondAdd = false; // add in reverse so stack processes in order for (auto childIx=childCount; childIx; ) { + const auto childH = node->getChildHandle(--childIx); + // to be able to figure out which substring of the prefix applies to current contributor + const auto mulChainLen = notMul ? static_cast(mulChain.size()):entry.mulChainLen; + // to be able to go back to the non mul that is supposed to add our subtree + const auto nonMulImmediateAncestorStackEnd = notMul ? static_cast(exprStack.size()):entry.nonMulImmediateAncestorStackEnd; + // skip null child + if (!childH) + { + // because non-contributors don't pop themselves when coming off the stack, we have this + assert(nonMulImmediateAncestorStackEnd); + // if our ancestor is a mul node this means we need to kill the subtree +// if (astPool.deref(exprStack[nonMulImmediateAncestorStackEnd-1].nodeH)->getType()!=) +// { +// } + continue; + } // making sure we visit this node again each time a subtree of an Add node is done - if (isAdd && childIx!=childCount) + if (isAdd && secondAdd) { auto& extraEntry = exprStack.emplace_back(entry); assert(extraEntry.visited); extraEntry.addContributor = true; } + secondAdd = true; // regular exploration - exprStack.push_back({ - .nodeH = node->getChildHandle(--childIx), - // to be able to go back to the non mul that is supposed to add our subtree - .nonMulImmediateAncestorStackEnd = notMul ? static_cast(exprStack.size()):entry.nonMulImmediateAncestorStackEnd - }); + exprStack.push_back({.nodeH=childH,.nonMulImmediateAncestorStackEnd=nonMulImmediateAncestorStackEnd,.mulChainLen=mulChainLen}); } } } @@ -871,8 +888,11 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin factor.handle = varH; const auto channels = var->getSpectralBins(); factor.monochrome = channels<2; - // this wont be affected by sorting - if (mulChain.size()>=2) + // we only want the mul chain length till the mul subtree's root + assert(exprStack.size()>=pEntry->nonMulImmediateAncestorStackEnd); + const auto mulChainBegin = pEntry->nonMulImmediateAncestorStackEnd ? (exprStack[pEntry->nonMulImmediateAncestorStackEnd-1].mulChainLen):uint16_t(0); + // this wont be affected by sorting, but we need to check if our prefix is large enough + if (mulChain.size()>=mulChainBegin+2) factor.liveSpectralChannels = mulChain[mulChain.size()-2].liveSpectralChannels; for (uint8_t c=0; cnonMulImmediateAncestorStackEnd); pEntry = nullptr; // if there was nothing else in the tree, we can bail out the whole material if (exprStack.empty()) { - mulChain.clear(); + // if there are no expression above, there must not be a mul chain + assert(mulChain.empty()); // for the MUL subtree to form the whole tree, there needs to be only one contributor in the stack assert(contributorStack.size()==1); contributorStack.clear(); @@ -958,16 +980,18 @@ auto CFrontendIR::SAdd2IRSession::popContributor() -> CTrueIR::typed_pointer_typ auto* weighted = irPool.deref(retval); weighted->contributor = contributorStack.back().contributor; contributorStack.pop_back(); - if (!mulChain.empty()) + // we only want the mul chain length till the mul subtree's root, this is the prefix + const size_t mulChainBegin = exprStack.empty() ? 0ull:(exprStack.back().mulChainLen); + if (mulChainBeginbool { From 53d0f9d3f1f9e5a1473babae8cb5a7642356baa1 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Tue, 5 May 2026 19:29:59 +0200 Subject: [PATCH 31/45] Take into account that ADD nodes can sit below non-ADD and non-MUL nodes and they shouldn't add contributors --- .../asset/material_compiler3/CFrontendIR.cpp | 70 +++++++++++++++---- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index b656dbceed..419ce32e84 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -769,7 +769,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin // using ast_expr_type_e = CFrontendIR::IExprNode::Type; const ast_expr_type_e astExprType = node->getType(); - const bool isContributor = astExprType==CFrontendIR::IExprNode::Type::Contributor; + const bool isContributor = astExprType==ast_expr_type_e::Contributor; // if (pEntry->notVisited()) { @@ -793,10 +793,20 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin const bool isAdd = astExprType==ast_expr_type_e::Add; if (isAdd) { - pEntry->addContributor = true; - // Current Add node will perform the job of the parent add node for this subtree + // There are no other nodes above current Add other than Mul, then we must add any potential contributors immediately below if (pEntry->nonMulImmediateAncestorStackEnd) - exprStack[pEntry->nonMulImmediateAncestorStackEnd-1].addContributor = false; + pEntry->addContributor = true; + // A contributor can only be in the leftmost branch of an Add node's subtree, it also cannot be under any other node than Add or Mul. + // Mostly making sure that Add nodes within complex weighting functions don't add contributors all of the sudden. + // Its like a flood fill on the AST, where any non-Mul and non-Add node stops filling below. + else if (auto& nonMulAncestor=exprStack[pEntry->nonMulImmediateAncestorStackEnd-1]; nonMulAncestor.addContributor) + { + // So use this knowledge to our advantage, however if we ever alias `addContributor` with anything else we'd need to check the ancestor type + assert(astPool.deref(nonMulAncestor.nodeH)->getType()==ast_expr_type_e::Add); + pEntry->addContributor = true; + // Current Add node will perform the job of the parent add node for this subtree, it takes over + nonMulAncestor.addContributor = false; + } } const bool notMul = astExprType!=ast_expr_type_e::Mul; // go through children @@ -805,7 +815,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin const auto entry = *pEntry; pEntry = nullptr; // making sure we visit this node again each time a subtree of an Add node is done - bool secondAdd = false; + bool pushedAChild = false; // add in reverse so stack processes in order for (auto childIx=childCount; childIx; ) { @@ -819,23 +829,42 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin { // because non-contributors don't pop themselves when coming off the stack, we have this assert(nonMulImmediateAncestorStackEnd); - // if our ancestor is a mul node this means we need to kill the subtree -// if (astPool.deref(exprStack[nonMulImmediateAncestorStackEnd-1].nodeH)->getType()!=) -// { -// } + // what should happen depends if we're in the middle of a MUL subtree + if (notMul) + { + // Generally this is the same as having an OpUndef, for Add we can skip adding this branch which we'll handle outside the loop + continue; + } + else // kill subtree + { + const auto logLevel = system::ILogger::ELL_WARNING; + args.logger.log("A null immediate child was encountered in the Mul node forming the subtree:\n",logLevel); + printSubtree(entry.nodeH); + args.logger.log("Forms a 0 constant factor across its ancestors in the mul chain, within the BxDF:\n",logLevel); + printSubtree(bxdfRootH); + mulChain.resize(mulChainLen); + // this is so we don't pop a contributor if we happen to be in the right hand subtree relative to the top contributor in the stack and below an `Other` function + if (entry.addContributor) + contributorStack.pop_back(); + exprStack.resize(nonMulImmediateAncestorStackEnd); + break; + } continue; } // making sure we visit this node again each time a subtree of an Add node is done - if (isAdd && secondAdd) + if (isAdd && pushedAChild) { auto& extraEntry = exprStack.emplace_back(entry); assert(extraEntry.visited); extraEntry.addContributor = true; } - secondAdd = true; + pushedAChild = true; // regular exploration exprStack.push_back({.nodeH=childH,.nonMulImmediateAncestorStackEnd=nonMulImmediateAncestorStackEnd,.mulChainLen=mulChainLen}); } + // didn't manage to add any child, dead ADD node + if (isAdd && !pushedAChild) + exprStack.back().addContributor = false; } } else @@ -871,7 +900,8 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin return (headH=errorRetval); } } - // when we are done we need to reset the mul chain back to its original state + // When we are done we need to reset the mul chain back to its original state, even if we don't add a contributor. + // Because this could have been MUL BXDF ADD F_0, F_1 in Reverse Polish Notation mulChain.resize(pEntry->mulChainLen); break; } @@ -884,6 +914,9 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin args.logger.log("Failed to create the Spectral Variable.",ELL_ERROR); return (headH=errorRetval); } + // Note that we push onto the mul-chain even if the first non-MUL ancestor node is not an ADD (e.g. Fresnel or other complex function). + // Also we may have ADD nodes which are disconnected from a contributor by having an Other ancestor + // TODO: mulChain probably needs to get renamed into something more semantically sound auto& factor = mulChain.emplace_back(); factor.handle = varH; const auto channels = var->getSpectralBins(); @@ -923,14 +956,21 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin return headH; } // don't add this MUL subtree island's contributor, it won't exist - pEntry = &exprStack.back(); - pEntry->addContributor = false; - contributorStack.pop_back(); + if (auto& nonMulAncestor=exprStack.back(); nonMulAncestor.addContributor) + { + // if we ever alias `addContributor` with anything else we'd need to check the ancestor type + assert(astPool.deref(nonMulAncestor.nodeH)->getType()==ast_expr_type_e::Add); + nonMulAncestor.addContributor = false; + contributorStack.pop_back(); + } } break; } default: { + // Due to the genious design of the AST, two BxDFs cannot be multiplied, and the BxDF must be in the leftmost branch of an Add. + // This makes it impossible to have an `IContributorDependant` which is involved in a MUL with more than 1 contributor. + // But one contributor can be multiplied with many `IContributorDependant` which is not a problem because they all go on the mulChain as needed. args.logger.log("Unsupported AST Expression type \"%s\"",ELL_ERROR,system::to_string(astExprType).c_str()); return (headH=errorRetval); } From 0a98657dacbf676ea59ad044eb77ef3b2dba22fa Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Wed, 6 May 2026 12:08:47 +0200 Subject: [PATCH 32/45] update submodule pointer --- examples_tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples_tests b/examples_tests index ceb0b3fbeb..e377c1f524 160000 --- a/examples_tests +++ b/examples_tests @@ -1 +1 @@ -Subproject commit ceb0b3fbeb03aeb8c821093f0eec4b2000b603e3 +Subproject commit e377c1f52402a79a6d1be6d0a1655910241ed08f From 905a992b6d9284b6b0cf83345a8298ec834c4dd5 Mon Sep 17 00:00:00 2001 From: keptsecret Date: Mon, 11 May 2026 14:52:13 +0700 Subject: [PATCH 33/45] basic printing of TrueIR --- .../nbl/asset/material_compiler3/CTrueIR.h | 32 ++- src/nbl/asset/material_compiler3/CTrueIR.cpp | 195 +++++++++++++++++- 2 files changed, 225 insertions(+), 2 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index c2247403b1..9dbbc48d2e 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -229,6 +229,8 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! return hasher.operator core::blake3_hash_t(); } + virtual inline void printDot(std::ostringstream& sstr, const core::string& selfID) const {} + protected: friend class CTrueIR; inline bool recomputeHash(const obj_pool_type& pool) @@ -646,6 +648,8 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // inline uint8_t getSpectralBins() const override final {return getKnotCount();} + + NBL_API2 void printDot(std::ostringstream& sstr, const core::string& selfID) const override final; }; using CSpectralVariableFactor = CSpectralVariable; @@ -691,6 +695,8 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! 0,0,1 ); // TODO: semantic flags/metadata (symmetries of the profile) + + NBL_API2 void printDot(std::ostringstream& sstr, const core::string& selfID) const override final; }; // To use a bump map, the Material needs to be provided UVs (which can or can not have associated tangents and smooth normals), but that's the responsibility of backend. @@ -775,6 +781,8 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(COrenNayar);} inline COrenNayar() = default; + + NBL_API2 void printDot(std::ostringstream& sstr, const core::string& selfID) const override final; }; class CCookTorrance final : public obj_pool_type::INonTrivial, public IBxDFWithNDF { @@ -800,6 +808,8 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! inline bool isEtaReciprocal() const {return ndfParams.params[2].padding[0];} inline void setEtaReciprocal(const bool value) {ndfParams.params[2].padding[0] = value;} + NBL_API2 void printDot(std::ostringstream& sstr, const core::string& selfID) const override final; + // BTDF ONLY! 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, @@ -969,12 +979,19 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! { // should probably size it better, if I knew total node count allocated or live visitedNodes.reserve(roots.size()<<4); + layerStack.reserve(roots.size()); + for (const auto& m : roots) + { + layerStack.push_back(m.front.root); + layerStack.push_back(m.back.root); + } } inline void reset(const CTrueIR* ir) { visitedNodes.clear(); layerStack.clear(); + nodeStack.clear(); m_ir = ir; } @@ -989,7 +1006,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! core::unordered_set> visitedNodes; // TODO: track layering depth and indent accordingly? core::vector> layerStack; -// core::stack> exprStack; + core::vector> nodeStack; private: const CTrueIR* m_ir; @@ -1057,6 +1074,19 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! bool success = true; }; + inline core::string getNodeID(const typed_pointer_type handle) const { return core::string("_") + std::to_string(handle.value); } + inline core::string getLabelledNodeID(const typed_pointer_type handle) const + { + const INode* node = getObjectPool().deref(handle); + core::string retval = getNodeID(handle); + retval += " [label=\""; + retval += node->getTypeName(); + // maybe also add hash? + // maybe label suffix? + retval += "\"]"; + return retval; + } + NBL_API2 CTrueIR(creation_params_type&& params); core::vector m_materials; diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp index af55acec14..f326d63564 100644 --- a/src/nbl/asset/material_compiler3/CTrueIR.cpp +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -103,7 +103,170 @@ CTrueIR::SBasicNodes::SBasicNodes(CTrueIR* ir) void CTrueIR::SDotPrinter::operator()(std::ostringstream& output) { - output << "IR PRINTING UnIMPLEMENTED!\n"; + output << "digraph {\n"; + + const auto errorBxDF = m_ir->getBasicNodes().errorBxDF; + const auto errorLayer = m_ir->getBasicNodes().errorLayer; + auto drainNodeStack = [&]()->void + { + while (!nodeStack.empty()) + { + const auto entry = nodeStack.back(); + nodeStack.pop_back(); + const auto nodeID = m_ir->getNodeID(entry); + const auto* node = m_ir->getObjectPool().deref(entry); + if (!node) + continue; + output << "\n\t" << m_ir->getLabelledNodeID(entry); + switch (node->getFinalType()) + { + case INode::EFinalType::CFactorCombiner: + { + const auto* combiner = dynamic_cast(node); + const auto state = combiner->getState(); + const auto childCount = state.childCount; + if (childCount) + { + for (auto childIx = 0; childIx < childCount; childIx++) + { + const auto childHandle = combiner->getChildHandle(childIx); + if (const auto child = m_ir->getObjectPool().deref(childHandle); child) + { + output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(childHandle); + const auto visited = visitedNodes.find(childHandle); + if (visited != visitedNodes.end()) + continue; + nodeStack.push_back(childHandle); + visitedNodes.insert(childHandle); + } + } + } + break; + } + case INode::EFinalType::CContributorSum: + { + const auto* contributeSum = dynamic_cast(node); + if (contributeSum) + { + typed_pointer_type children[] = {contributeSum->product, contributeSum->rest}; + for (const auto childHandle : children) + { + if (const auto child = m_ir->getObjectPool().deref(childHandle); child) + { + output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(childHandle); + const auto visited = visitedNodes.find(childHandle); + if (visited != visitedNodes.end()) + continue; + nodeStack.push_back(childHandle); + visitedNodes.insert(childHandle); + } + } + } + break; + } + case INode::EFinalType::CCorellatedTransmission: + { + // TODO: + const auto* transmission = dynamic_cast(node); + if (transmission) + { + typed_pointer_type children[] = { transmission->btdf, transmission->brdfBottom }; // TODO: what about coated and next? + for (const auto childHandle : children) + { + if (const auto child = m_ir->getObjectPool().deref(childHandle); child) + { + output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(childHandle); + const auto visited = visitedNodes.find(childHandle); + if (visited != visitedNodes.end()) + continue; + nodeStack.push_back(childHandle); + visitedNodes.insert(childHandle); + } + } + } + break; + } + case INode::EFinalType::CWeightedContributor: + { + const auto* contributor = dynamic_cast(node); + if (contributor) + { + typed_pointer_type children[] = { contributor->contributor, contributor->factor }; + for (const auto childHandle : children) + { + if (const auto child = m_ir->getObjectPool().deref(childHandle); child) + { + output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(childHandle); + const auto visited = visitedNodes.find(childHandle); + if (visited != visitedNodes.end()) + continue; + nodeStack.push_back(childHandle); + visitedNodes.insert(childHandle); + } + } + } + break; + } + case INode::EFinalType::CCookTorrance: + { + const auto* ct = dynamic_cast(node); + if (ct) + { + output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(ct->orientedRealEta); + const auto visited = visitedNodes.find(ct->orientedRealEta); + if (visited != visitedNodes.end()) + continue; + nodeStack.push_back(ct->orientedRealEta); + visitedNodes.insert(ct->orientedRealEta); + } + break; + } + default: + break; + } + // special printing + node->printDot(output, nodeID); + } + }; + drainNodeStack(); + + while (!layerStack.empty()) + { + const auto layerHandle = layerStack.back(); + layerStack.pop_back(); + // don't print layer nodes multiple times + const auto visited = visitedNodes.find(layerHandle); + if (visited != visitedNodes.end()) + continue; + visitedNodes.insert(layerHandle); + const auto* layerNode = m_ir->getObjectPool().deref(layerHandle); + if (!layerNode) + continue; + // + const auto layerID = m_ir->getNodeID(layerHandle); + output << "\n\t" << m_ir->getLabelledNodeID(layerHandle); + // + auto pushNodeRoot = [&](const typed_pointer_type root, const std::string_view edgeLabel)->void + { + if (!root) + return; + // print the link from the layer to the expression + output << "\n\t" << layerID << " -> " << m_ir->getNodeID(root) << "[label=\"" << edgeLabel << "\"]"; + // but not the expression again + const auto visited = visitedNodes.find(root); + if (visited != visitedNodes.end()) + return; + nodeStack.push_back(root); + visitedNodes.insert(root); + }; + pushNodeRoot(layerNode->brdfTop, "Top BRDF"); + pushNodeRoot(layerNode->firstTransmission, "BTDF"); + drainNodeStack(); + } + + // TODO: print image views + + output << "\n}\n"; } CTrueIR::SRewriteSession::~SRewriteSession() @@ -146,5 +309,35 @@ bool CTrueIR::SRewriteSession::rewriteSingleLayer(typed_pointer_type " << transformNodeID << "[label=\"Profile Transform\"]"; + } +} + +void CTrueIR::COrenNayar::printDot(std::ostringstream& sstr, const core::string& selfID) const +{ + ndfParams.printDot(sstr, selfID); +} + +void CTrueIR::CCookTorrance::printDot(std::ostringstream& sstr, const core::string& selfID) const +{ + ndfParams.printDot(sstr, selfID); +} + template class CTrueIR::CSpectralVariable; } \ No newline at end of file From 692c8e7d2d3b8f6b8d120ea5019bb39378e98059 Mon Sep 17 00:00:00 2001 From: keptsecret Date: Mon, 11 May 2026 15:35:21 +0700 Subject: [PATCH 34/45] some more labels for trueir graph --- .../nbl/asset/material_compiler3/CTrueIR.h | 5 +++- src/nbl/asset/material_compiler3/CTrueIR.cpp | 27 ++++++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 9dbbc48d2e..16b693508e 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -229,6 +229,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! return hasher.operator core::blake3_hash_t(); } + 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 {} protected: @@ -359,6 +360,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! inline EFinalType getFinalType() const override {return EFinalType::CWeightedContributor;} inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CWeightedContributor);} + inline std::string_view getChildName_impl(const uint8_t ix) const override final { return ix ? "factor" : "contributor"; } typed_pointer_type contributor = {}; @@ -397,7 +399,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! inline EFinalType getFinalType() const override {return EFinalType::CContributorSum;} inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CContributorSum);} - + inline std::string_view getChildName_impl(const uint8_t ix) const override final { return ix ? "rest" : "product"; } // the product is ... typed_pointer_type product = {}; @@ -427,6 +429,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! inline EFinalType getFinalType() const override {return EFinalType::CCorellatedTransmission;} inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CCorellatedTransmission);} + inline std::string_view getChildName_impl(const uint8_t ix) const override final { return ix ? "brdfBottom" : "btdf"; } // you can set the children later inline CCorellatedTransmission() = default; diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp index f326d63564..7c7fa981f8 100644 --- a/src/nbl/asset/material_compiler3/CTrueIR.cpp +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -149,17 +149,19 @@ void CTrueIR::SDotPrinter::operator()(std::ostringstream& output) if (contributeSum) { typed_pointer_type children[] = {contributeSum->product, contributeSum->rest}; + uint32_t childIx = 0u; for (const auto childHandle : children) { if (const auto child = m_ir->getObjectPool().deref(childHandle); child) { - output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(childHandle); + output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(childHandle) << "[label=\"" << node->getChildName_impl(childIx) << "\"]"; const auto visited = visitedNodes.find(childHandle); if (visited != visitedNodes.end()) continue; nodeStack.push_back(childHandle); visitedNodes.insert(childHandle); } + childIx++; } } break; @@ -171,17 +173,19 @@ void CTrueIR::SDotPrinter::operator()(std::ostringstream& output) if (transmission) { typed_pointer_type children[] = { transmission->btdf, transmission->brdfBottom }; // TODO: what about coated and next? + uint32_t childIx = 0u; for (const auto childHandle : children) { if (const auto child = m_ir->getObjectPool().deref(childHandle); child) { - output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(childHandle); + output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(childHandle) << "[label=\"" << node->getChildName_impl(childIx) << "\"]"; const auto visited = visitedNodes.find(childHandle); if (visited != visitedNodes.end()) continue; nodeStack.push_back(childHandle); visitedNodes.insert(childHandle); } + childIx++; } } break; @@ -192,17 +196,19 @@ void CTrueIR::SDotPrinter::operator()(std::ostringstream& output) if (contributor) { typed_pointer_type children[] = { contributor->contributor, contributor->factor }; + uint32_t childIx = 0u; for (const auto childHandle : children) { if (const auto child = m_ir->getObjectPool().deref(childHandle); child) { - output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(childHandle); + output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(childHandle) << "[label=\"" << node->getChildName_impl(childIx) << "\"]"; const auto visited = visitedNodes.find(childHandle); if (visited != visitedNodes.end()) continue; nodeStack.push_back(childHandle); visitedNodes.insert(childHandle); } + childIx++; } } break; @@ -212,12 +218,15 @@ void CTrueIR::SDotPrinter::operator()(std::ostringstream& output) const auto* ct = dynamic_cast(node); if (ct) { - output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(ct->orientedRealEta); - const auto visited = visitedNodes.find(ct->orientedRealEta); - if (visited != visitedNodes.end()) - continue; - nodeStack.push_back(ct->orientedRealEta); - visitedNodes.insert(ct->orientedRealEta); + if (const auto eta = m_ir->getObjectPool().deref(ct->orientedRealEta); eta) + { + output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(ct->orientedRealEta) << "[label=\"orientedRealEta\"]"; + const auto visited = visitedNodes.find(ct->orientedRealEta); + if (visited != visitedNodes.end()) + continue; + nodeStack.push_back(ct->orientedRealEta); + visitedNodes.insert(ct->orientedRealEta); + } } break; } From 69f61b208f68f3b6594def00524e0915ae25913a Mon Sep 17 00:00:00 2001 From: keptsecret Date: Mon, 11 May 2026 15:46:46 +0700 Subject: [PATCH 35/45] reduced duplicate code in printing children --- src/nbl/asset/material_compiler3/CTrueIR.cpp | 62 ++++++-------------- 1 file changed, 19 insertions(+), 43 deletions(-) diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp index 7c7fa981f8..0ae7459291 100644 --- a/src/nbl/asset/material_compiler3/CTrueIR.cpp +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -118,6 +118,22 @@ void CTrueIR::SDotPrinter::operator()(std::ostringstream& output) if (!node) continue; output << "\n\t" << m_ir->getLabelledNodeID(entry); + auto printChildren = [&](std::span> children, const INode* node)->void { + uint32_t childIx = 0u; + for (const auto childHandle : children) + { + if (const auto child = m_ir->getObjectPool().deref(childHandle); child) + { + output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(childHandle) << "[label=\"" << node->getChildName_impl(childIx) << "\"]"; + const auto visited = visitedNodes.find(childHandle); + if (visited != visitedNodes.end()) + continue; + nodeStack.push_back(childHandle); + visitedNodes.insert(childHandle); + } + childIx++; + } + }; switch (node->getFinalType()) { case INode::EFinalType::CFactorCombiner: @@ -149,44 +165,17 @@ void CTrueIR::SDotPrinter::operator()(std::ostringstream& output) if (contributeSum) { typed_pointer_type children[] = {contributeSum->product, contributeSum->rest}; - uint32_t childIx = 0u; - for (const auto childHandle : children) - { - if (const auto child = m_ir->getObjectPool().deref(childHandle); child) - { - output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(childHandle) << "[label=\"" << node->getChildName_impl(childIx) << "\"]"; - const auto visited = visitedNodes.find(childHandle); - if (visited != visitedNodes.end()) - continue; - nodeStack.push_back(childHandle); - visitedNodes.insert(childHandle); - } - childIx++; - } + printChildren(children, node); } break; } case INode::EFinalType::CCorellatedTransmission: { - // TODO: const auto* transmission = dynamic_cast(node); if (transmission) { typed_pointer_type children[] = { transmission->btdf, transmission->brdfBottom }; // TODO: what about coated and next? - uint32_t childIx = 0u; - for (const auto childHandle : children) - { - if (const auto child = m_ir->getObjectPool().deref(childHandle); child) - { - output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(childHandle) << "[label=\"" << node->getChildName_impl(childIx) << "\"]"; - const auto visited = visitedNodes.find(childHandle); - if (visited != visitedNodes.end()) - continue; - nodeStack.push_back(childHandle); - visitedNodes.insert(childHandle); - } - childIx++; - } + printChildren(children, node); } break; } @@ -196,20 +185,7 @@ void CTrueIR::SDotPrinter::operator()(std::ostringstream& output) if (contributor) { typed_pointer_type children[] = { contributor->contributor, contributor->factor }; - uint32_t childIx = 0u; - for (const auto childHandle : children) - { - if (const auto child = m_ir->getObjectPool().deref(childHandle); child) - { - output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(childHandle) << "[label=\"" << node->getChildName_impl(childIx) << "\"]"; - const auto visited = visitedNodes.find(childHandle); - if (visited != visitedNodes.end()) - continue; - nodeStack.push_back(childHandle); - visitedNodes.insert(childHandle); - } - childIx++; - } + printChildren(children, node); } break; } From 7c767be5661bec3b28a85822f3b8e43af31c6143 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Mon, 11 May 2026 10:57:28 +0200 Subject: [PATCH 36/45] completely rewrote the AST -> IR --- .../asset/material_compiler3/CFrontendIR.h | 29 +- .../nbl/asset/material_compiler3/CTrueIR.h | 3 - .../nbl/core/containers/DoublyLinkedList.h | 27 +- .../asset/material_compiler3/CFrontendIR.cpp | 546 ++++++++++++++++-- 4 files changed, 547 insertions(+), 58 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index 573c197860..053a525727 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -808,6 +808,7 @@ class CFrontendIR final : public CNodePool bool btdfSubtree = false; // for going over layers in the AST core::vector layerStack; +#if 0 // dead and wrong // Some of the things we must canonicalize: // A ( f_0 (B + C) + D f_1 ) = f_0 B A + f_0 C A + f_1 D A // Expression nodes of the Frontend AST really come in 4 variants: @@ -849,18 +850,24 @@ class CFrontendIR final : public CNodePool // However how much of that would be moving IR manipulation into the AST ? struct StackEntry { + constexpr static inline uint64_t DontAddContributor = (0x1u<<10)-1; + inline bool notVisited() const {return !visited;} CFrontendIR::typed_pointer_type nodeH; // the ancestor ADD node to go back to if we hit a 0 MUL, or if our ADD or any other node becomes 0 - uint16_t nonMulImmediateAncestorStackEnd = 0; - // the length of the `mulChain` at the time we first visited the node - uint16_t mulChainLen = 0; - bool visited = false; - // only relevant for Add nodes - bool addContributor = false; + uint64_t nonMulImmediateAncestorStackEnd : 11 = 0; + // the start of the `mulChain`, basically the bits that don't cross an Other node + uint64_t mulChainBegin : 21 = 0; + // the length of the `mulChain` at the time we first visited the node, so that we may reset the prefix back to what it was before continuing down another leg of the ADD + uint64_t mulChainPrefixEnd : 21 = 0; + // only relevant for Add nodes, the value tells you what to trim the contributor stack to + uint64_t contributorStackLen : 10 = DontAddContributor; + // + uint64_t visited : 1 = false; }; core::vector exprStack; +#endif }; inline core::string getNodeID(const typed_pointer_type handle) const {return core::string("_")+std::to_string(handle.value);} @@ -988,8 +995,8 @@ inline bool CFrontendIR::valid(const typed_pointer_type rootHandle const auto childHandle = node->getChildHandle(childIx); if (const auto child=getObjectPool().deref(childHandle); child) { - // Only Add nodes can have Contributors in any subtree, Mul and Complement only the first, and others can't have them at all - const bool noContribBelow = entry.contribState==SubtreeContributorState::Forbidden || childIx!=0 && !nodeIsAdd || nodeType==IExprNode::Type::Complement || nodeType==IExprNode::Type::Other; + // Only Add nodes can have Contributors in any subtree, Mul only the first, and others can't have them at all. Especially don't allow the complementing of a BxDF! + const bool noContribBelow = entry.contribState==SubtreeContributorState::Forbidden || childIx!=0 && !nodeIsAdd || nodeType==IExprNode::Type::Other || nodeType==IExprNode::Type::Complement; StackEntry newEntry = {.node=child,.handle=childHandle}; if (noContribBelow) { @@ -1040,8 +1047,12 @@ inline bool CFrontendIR::valid(const typed_pointer_type rootHandle logger.log("Node %u of type %s is invalid!",ELL_ERROR,entry.handle.value,node->getTypeName().data()); return false; } - if (entry.contribSlot createConstant() - // Each material comes down to this, this is the only struct we don't de-duplicate struct SMaterial diff --git a/include/nbl/core/containers/DoublyLinkedList.h b/include/nbl/core/containers/DoublyLinkedList.h index af7edcc01a..d6757d9843 100644 --- a/include/nbl/core/containers/DoublyLinkedList.h +++ b/include/nbl/core/containers/DoublyLinkedList.h @@ -1,9 +1,8 @@ -// Copyright (C) 2018-2020 - DevSH Graphics Programming Sp. z O.O. +// Copyright (C) 2018-2026 - 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_CORE_CONTAINERS_DOUBLY_LINKED_LIST_H_INCLUDED__ -#define __NBL_CORE_CONTAINERS_DOUBLY_LINKED_LIST_H_INCLUDED__ +#ifndef _NBL_CORE_CONTAINERS_DOUBLY_LINKED_LIST_H_INCLUDED_ +#define _NBL_CORE_CONTAINERS_DOUBLY_LINKED_LIST_H_INCLUDED_ #include "nbl/core/alloc/PoolAddressAllocator.h" @@ -11,9 +10,8 @@ #include -namespace nbl -{ -namespace core + +namespace nbl::core { //Struct for use in a doubly linked list. Stores data and pointers to next and previous elements the list, or invalid iterator if it is first/last @@ -65,6 +63,7 @@ struct alignas(void*) SDoublyLinkedNode } }; +// TODO: this could use the ObjectPool that CNodePool does template> > class DoublyLinkedList { @@ -99,6 +98,9 @@ class DoublyLinkedList return (m_array + address); } + // + inline bool empty() const {return m_begin==m_back;} + //get node ptr of the first item in the list inline node_t* getBegin() { return m_array + m_begin; } inline const node_t* getBegin() const { return m_array + m_begin; } @@ -120,9 +122,9 @@ class DoublyLinkedList } template - inline void emplaceFront(Args&&... args) + inline node_t* emplaceFront(Args&&... args) { - insertAt(reserveAddress(), std::forward(args)...); + return insertAt(reserveAddress(), std::forward(args)...); } /** @@ -328,7 +330,7 @@ class DoublyLinkedList //create a new node which stores data at already allocated address template - inline void insertAt(uint32_t addr, Args&&... args) + inline node_t* insertAt(uint32_t addr, Args&&... args) { assert(addr < m_cap); assert(addr != invalid_iterator); @@ -342,6 +344,7 @@ class DoublyLinkedList if (m_back == invalid_iterator) m_back = addr; m_begin = addr; + return n; } /** @@ -506,8 +509,6 @@ std::reverse_iterator::const_iterato return std::reverse_iterator(const_iterator(this, m_begin)); } -} //namespace core -} //namespace nbl - +} //namespace nbl::core #endif diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index 419ce32e84..f990475fc2 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -728,6 +728,7 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ return retval; } + // TODO: this really needs a visited AST nodes (or at least subtrees) cache! auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_pointer_type bxdfRootH) -> CTrueIR::typed_pointer_type { @@ -737,8 +738,414 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin // temporary debug for WIP printSubtree(bxdfRootH); - // error on exit + auto& astPool = srcAST->getObjectPool(); + auto& irPool = tmpIR->getObjectPool(); + + // Simple and inefficient Factor Gather with no Contributors: + // - want canonical form, so vector of mul chains + // - Solution, distribute/hoist the ADD over the MUL. So replace a `MUL ADD A B C` with `ADD MUL A C MUL B C` + // - However if there's a long `MUL` chain, then hoist all the way to the top above that MUL, then every unexplored (pushed) node goes in the mul chain + // - Bottom up or Top Down? + // + Bottom up requires descent to bottom anyway to find the ADD within MUL islands + // + Top down we can find the first ADD, then rewrite it and continue rewriting subexpressions + // - Visit Cache? Every original AST node has its own mul chain set ? + // + If 0 factor gets slapped on, chain dies, gets removed from sum + // - linked lists needed ? Or just MUL in tmpAST or IR? + // 1. somehow mark when an AST `IExprNode` doesn't need exploring anymore + // 2. explore on repeat + // Variant with contributors: + // - just sort the contributor last in the linked list, problem solved + + struct SFactor + { + struct SContributor + { + CTrueIR::typed_pointer_type handle = {}; + uint32_t zeroValue = 0; + }; + // these are the parts that need to be sorted + struct SOrdered + { + CTrueIR::typed_pointer_type handle = {}; + uint32_t monochrome : 1 = true; + // 0 for contributors and leaf factors, contributors can be told apart from leaf factors by having 0s in the whole DWORD here + uint32_t padding : 31 = 0; + }; + + // TODO: do this better, check whole DWORD is 0 + inline bool isContributor() const {return contributor.zeroValue==0;} + + union + { + CTrueIR::typed_pointer_type typeless; + // contributor is always monochrome, etc. + SContributor contributor; + // the fatter thing which actually can be monochrome, etc. + SOrdered factor = {}; + }; + }; + // Multiplication Chain need to be sorted in a canonical order so its easier to spot them being the same + auto sortMuls = [](const SFactor& lhs, const SFactor& rhs)->bool + { + // contributor goes first + if (lhs.isContributor()!=rhs.isContributor()) + return lhs.isContributor(); + // only one contributor allowed per chain! + assert(!lhs.isContributor() && !rhs.isContributor()); + // monochrome is cheaper + if (lhs.factor.monochrome!=rhs.factor.monochrome) + return lhs.factor.monochrome; + // then by handle + return lhs.typeless.value> astStack = {}; // its also a stack + core::vector irChain = {}; + uint8_t hasContributor : 1 = false; + // extend later when allowing variable bucket count + uint8_t negate : 3 = 0b000; + uint8_t liveSpectralChannels : 3 = 0b111; + }; + // TODO: for the visited cache, we'd somehow need to cache a "2D slice" from this, meaning skipping the `irChain` prefix of a node + // (would need linked list IR so span/front-back of an irChain subsection can be kept) + // but also keeping how many of these separate irChains get spawned by the AST node + core::list canonicalSum; + + // error value const auto errorRetval = tmpIR->getBasicNodes().errorBxDF; + + // good code start + assert(canonicalSum.empty()); + // add the first term to explore + canonicalSum.emplace_back().astStack.emplace_back() = bxdfRootH; + // error on exit + auto it=canonicalSum.begin(); + auto printFailAndCleanupOnExit = core::makeRAIIExiter([&]()->void + { + if (headH!=errorRetval) + return; + printSubtree(it->astStack.back()); + args.logger.log("Within BxDF:\n",ELL_DEBUG); + printSubtree(bxdfRootH); + // no point emitting an error contributor, don't want a best effort compilation within a layer, don't want contributors missing or substituted + canonicalSum.clear(); + } + ); + CTrueIR::typed_pointer_type tailH = {}; + for (; it!=canonicalSum.end(); it++) + { + auto& irChain = it->irChain; + auto& astStack = it->astStack; + while (!astStack.empty()) + { + auto* pEntry = &astStack.back(); + const auto nodeH = *pEntry; + const auto* const node = astPool.deref(nodeH); + // only non null nodes get pushed onto the stack + assert(node); + // depending on the type we have different things to do + using ast_expr_type_e = CFrontendIR::IExprNode::Type; + const ast_expr_type_e astExprType = node->getType(); + constexpr auto ELL_WARNING = system::ILogger::ELL_WARNING; + switch (astExprType) + { + case ast_expr_type_e::Contributor: + { + // This must be the only contributor, as a contributor can only be in the leftmost branch of a Mul node's subtree, it also cannot be under any other node than Add or Mul. + assert(!it->hasContributor); + // + astStack.pop_back(); + // shouldn't invalidate iterator, but underline our vector changes + pEntry = nullptr; + // push onto the irChain + const auto contributorH = static_cast(node)->createIRNode(btdfSubtree,srcAST,tmpIR.get()); + // TODO: recompute instead of compute hash + if (!contributorH || irPool.deref(contributorH)->computeHash(irPool)==core::blake3_hash_t{}) + { + args.logger.log("Failed to Create IR Contributor from AST",ELL_ERROR); + return (headH=errorRetval); + } + it->hasContributor = true; + irChain.emplace_back().contributor = {.handle=contributorH}; + break; + } + case ast_expr_type_e::Mul: // pop self but push the two children + { + // add in reverse so children are visited left to right + for (auto c=node->getChildCount(); c;) + { + const auto childH = node->getChildHandle(--c); + // if any child is null, kill everything + if (auto* const child=astPool.deref(childH); !child) + { + printSubtree(nodeH); + constexpr bool HardFail = true; + if constexpr (HardFail) + { + args.logger.log("Undef node in a MUL",ELL_ERROR); + return (headH=errorRetval); + } + else + { + args.logger.log("Undef node in a MUL, turning the MUL into a 0",ELL_WARNING); + irChain.clear(); + astStack.clear(); + break; + } + } + // replace self with second child + if (c) + *pEntry = childH; + else + { + astStack.emplace_back() = childH; + pEntry = nullptr; + } + } + break; + } + case ast_expr_type_e::Add: // start one new chain + { + constexpr bool HardFail = true; + bool takeOverChain = true; + // add in reverse so children are visited left to right + for (auto c=node->getChildCount(); c;) + { + const auto childH = node->getChildHandle(--c); + // if child is null, skip it + if (auto* const child=astPool.deref(childH); !child) + { + printSubtree(nodeH); + if constexpr (HardFail) + { + args.logger.log("Undef node in an ADD",ELL_ERROR); + return (headH=errorRetval); + } + else + { + args.logger.log("Undef node in an ADD, substituting with a 0\n",ELL_WARNING); + continue; + } + } + if (takeOverChain) + { + // second child (but first to be pushed) takes over our ASTchain spot + *pEntry = childH; + // does not invalidate but changes contents + pEntry = nullptr; + takeOverChain = false; + } + else + { + // first child (second to be pushed) copies our chains and carries on + auto afterIt = it; + // but we need to remove the first child from the copy's astStack + canonicalSum.insert(++afterIt,*it)->astStack.pop_back(); + } + } + // didn't push anything, need to kill current sum term + if constexpr (!HardFail) + if (takeOverChain) + { + args.logger.log("Both children of ADD are NULL, deleting whole Contributor Sum Term",ELL_WARNING); + irChain.clear(); + astStack.clear(); + } + break; + } + case ast_expr_type_e::Complement: // MUL PREFIX ADD 1.0 CHILD + { + constexpr bool HardFail = true; + if (const auto childH=node->getChildHandle(0); astPool.deref(childH)) + { + // insert a copy of the current chain before the current with AST cleared, so chain stopped (gets us our MUL PREFIX 1.0 term) + canonicalSum.insert(it,*it)->astStack.clear(); + // start a new chain with a copy of ours but negate it (now `it` points to the copy) + it->negate ^= 0b111; + // now the expression which was getting complemented needs to be on the AST stack so we don't visit the complement again + it->astStack.back() = childH; + // need to finalize the irChain, so go back to the entry stopped + it--; + } + else // the child is OpUndef, replace complement with 1 node + { + printSubtree(nodeH); + if constexpr (HardFail) + { + args.logger.log("Child of COMPLEMENT is null,",ELL_ERROR); + return (headH=errorRetval); + } + else + { + args.logger.log("Child of COMPLEMENT is null, replacing with 1.0",ELL_WARNING); + // keep irChain but stop AST exploration + astStack.clear(); + } + } + break; + } + case ast_expr_type_e::SpectralVariable: + { + const auto varH = static_cast(node)->createIRNode(srcAST,tmpIR.get()); + auto* const var = irPool.deref(varH); + // no soft fail, the node wasn't null to begin with + if (!var) + { + printSubtree(nodeH); + args.logger.log("Failed to create the Spectral Variable.",ELL_ERROR); + return (headH=errorRetval); + } + // see what channels are dead + uint8_t liveMask = 0b000; + const auto channels = var->getSpectralBins(); + for (uint8_t c=0; cgetParameter(c).scale; + if (std::numeric_limits::min()<=scale && scale::infinity()) + liveMask |= uint8_t(1)<liveSpectralChannels&=liveMask)==0) + { + const auto logLevel = system::ILogger::ELL_PERFORMANCE; + // if we REALLY want to we can print all the nodes in the `irChain` so far, but then the irChain needs to track debug data from AST + args.logger.log("Product turns to 0 by multiplying the chain so far with the Spectral Variable:\n",logLevel); + printSubtree(nodeH); + args.logger.log("Forms a 0 constant factor across its ancestors in the mul chain, within the BxDF:\n",logLevel); + printSubtree(bxdfRootH); + // kill whole term + irChain.clear(); + astStack.clear(); + pEntry = nullptr; + break; + } + irChain.emplace_back().factor = {.handle=varH,.monochrome=monochrome}; + break; + } + case ast_expr_type_e::Other: + { + // TODO: We need to start a new `canonicalSum` and save a link to it in the current IR + printSubtree(nodeH); + args.logger.log("Unsupported AST Expression type \"%s\"",ELL_ERROR,system::to_string(astExprType).c_str()); + return (headH=errorRetval); + break; + } + default: + assert(false); + return (headH=errorRetval); + } + } + // only once the astChain is empty, if the ir chain is empty skip it, but don't remove it because some Other AST node might need it + if (irChain.empty()) + { + printSubtree(bxdfRootH); + // can't really print the subtree, because a lot of ADDs and MULs have to collude to produce this + args.logger.log("Empty IR mul chain encountered, skipping...",system::ILogger::ELL_PERFORMANCE); + continue; + } + // should have gotten removed beforehand + assert(it->liveSpectralChannels); + // sort the factors in the mulchain + std::sort(irChain.begin(),irChain.end(),sortMuls); + // make the combiner + if (it->hasContributor) + { + // if we have a contributor first node in the sorted chain must be the contributor + assert(irChain.front().isContributor()); + // we visited the leftmost subtrees first so this is the right order + { + auto* const tail = irPool.deref(tailH); + // allocate new tail + tailH = irPool.emplace(); + // append it + if (tail) + tail->rest = tailH; + else + headH = tailH; + } + // now get the true tail + { + auto* const tail = irPool.deref(tailH); + // and slap the mul chain onto it + const auto weightedContribH = irPool.emplace(); + auto* const weightedContrib = irPool.deref(weightedContribH); + weightedContrib->contributor = irChain.front().contributor.handle; + // now the mul chain + if (irChain.size()>1) + { + CTrueIR::CFactorCombiner::SState combinerState = { + .type = CTrueIR::CFactorCombiner::Type::Mul, + .childCount = irChain.size() + }; + // if we negate we need to add one more mul factor, otherwise nothing + if (it->negate==0) + --combinerState.childCount; + const auto factorH = irPool.emplace(combinerState); + { + auto* const factor = irPool.deref(factorH); + auto i = 0; + // monochrome negation + if (it->negate && it->negate==0b111) + { + // TODO: do with premade node so sorting is correct (lowest) + assert(false); + } + bool monochromeNodes = true; + for (auto itChain=irChain.begin()+1; itChain!=irChain.end(); itChain++) + { + // only one contributor per chain is allowed + assert(!itChain->isContributor()); + if (monochromeNodes) + { + // not monochrome for the first time + if (!itChain->factor.monochrome) + { + monochromeNodes = false; + // make the non-monochrome negation node + if (it->negate && it->negate!=0b111) + { + // TODO: do with premade node so sorting is correct (lowest) + assert(false); + } + } + } + else + { + // once you go spectral, you never go back + assert(!itChain->factor.monochrome); + } + factor->setChildHandle(i++,itChain->factor.handle); + } + } + weightedContrib->factor = factorH; + } + tail->product = weightedContribH; + } + } + else + { + // TODO: make without a contributor + assert(false); + } + } + +#if 0 // old and dead code + // error on exit auto printFailAndCleanupOnExit = core::makeRAIIExiter([&]()->void { if (headH!=errorRetval) @@ -753,13 +1160,25 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin } ); - auto& astPool = srcAST->getObjectPool(); - auto& irPool = tmpIR->getObjectPool(); + // Either the same contributor has to be added multiple times and then later cleaned up - the `addContributor` solution + // or, we properly add a single contributor and MUL its different additive factors - easily done during a rewrite + // The former is better because it allows canonicalization of multiplicative factors and gather of identical contributors. To implement it: + // - every ADD node that's reachable via ADD and MUL must add a fully weighted contributor when its visited again by its child + // (all the factors on the way from the ROOT to the ADD node + MUL subtree island below) + // - therefore every ADD node must know the length of the mul prefix from itself to a non-ADD & non-MUL ancestor so it can both + // + be reset so the MUL island of the child node doesn't affect its siblings + // + we're not counting parts of the mul chain crossing an Other node (which models an arbitrary function - e.g. fresnel) + // - islands of MUL nodes must be able to jump back to their first non-MUL ancestor (ADD or Other) and prevent adding of any contributors weighted by them + // - when an ADD node doesn't add contributors it should still get its MUL Island properly taken care of + +// Rewrite Rules +// (a+b)(c+d) = ac+bd+bc+ad + + // scratches are initialized assert(mulChain.empty()); assert(contributorStack.empty()); exprStack.push_back({.nodeH=bxdfRootH}); - CTrueIR::typed_pointer_type tailH = {}; // the mul node gets visited after children are made while (!exprStack.empty()) { @@ -788,24 +1207,29 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin // shouldn't invalidate but lets play it safe pEntry = nullptr; } + else if (astExprType==ast_expr_type_e::SpectralVariable) + { + // this is just easier to read than going through the circus below to do nothing + } else { const bool isAdd = astExprType==ast_expr_type_e::Add; if (isAdd) { // There are no other nodes above current Add other than Mul, then we must add any potential contributors immediately below - if (pEntry->nonMulImmediateAncestorStackEnd) - pEntry->addContributor = true; - // A contributor can only be in the leftmost branch of an Add node's subtree, it also cannot be under any other node than Add or Mul. + if (!pEntry->nonMulImmediateAncestorStackEnd) + pEntry->contributorStackLen = contributorStack.size(); + // A contributor can only be in the leftmost branch of a Mul node's subtree, it also cannot be under any other node than Add or Mul. // Mostly making sure that Add nodes within complex weighting functions don't add contributors all of the sudden. // Its like a flood fill on the AST, where any non-Mul and non-Add node stops filling below. - else if (auto& nonMulAncestor=exprStack[pEntry->nonMulImmediateAncestorStackEnd-1]; nonMulAncestor.addContributor) + else if (auto& nonMulAncestor=exprStack[pEntry->nonMulImmediateAncestorStackEnd-1]; nonMulAncestor.contributorStackLen!=StackEntry::DontAddContributor) { // So use this knowledge to our advantage, however if we ever alias `addContributor` with anything else we'd need to check the ancestor type assert(astPool.deref(nonMulAncestor.nodeH)->getType()==ast_expr_type_e::Add); - pEntry->addContributor = true; // Current Add node will perform the job of the parent add node for this subtree, it takes over - nonMulAncestor.addContributor = false; + pEntry->contributorStackLen = contributorStack.size(); + // if we're in the leftmost subtree then reset to current, otherwise also reset to current because the contributor stack size will remain unchanged + nonMulAncestor.contributorStackLen = StackEntry::DontAddContributor; } } const bool notMul = astExprType!=ast_expr_type_e::Mul; @@ -816,37 +1240,75 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin pEntry = nullptr; // making sure we visit this node again each time a subtree of an Add node is done bool pushedAChild = false; + // we don't push onto mulchain in the loop so can compute once + const auto mulChainBegin = astExprType==ast_expr_type_e::Other ? static_cast(mulChain.size()):entry.mulChainBegin; + const auto mulChainPrefixEnd = notMul ? static_cast(mulChain.size()):entry.mulChainPrefixEnd; + assert(mulChainBegin<=mulChainPrefixEnd); // add in reverse so stack processes in order - for (auto childIx=childCount; childIx; ) + for (auto childIx=childCount; childIx;) { const auto childH = node->getChildHandle(--childIx); - // to be able to figure out which substring of the prefix applies to current contributor - const auto mulChainLen = notMul ? static_cast(mulChain.size()):entry.mulChainLen; // to be able to go back to the non mul that is supposed to add our subtree const auto nonMulImmediateAncestorStackEnd = notMul ? static_cast(exprStack.size()):entry.nonMulImmediateAncestorStackEnd; // skip null child if (!childH) { - // because non-contributors don't pop themselves when coming off the stack, we have this - assert(nonMulImmediateAncestorStackEnd); // what should happen depends if we're in the middle of a MUL subtree if (notMul) { - // Generally this is the same as having an OpUndef, for Add we can skip adding this branch which we'll handle outside the loop + // Generally this is the same as having an OpUndef, for Add we can skip adding this branch's contributor which we implicitly handle with `pushedAChild` continue; } - else // kill subtree + else // kill MUL subtree { + struct SHandleZeroMulArgs + { + std::string_view preSubtreePrintMsg; + std::string_view preTreePrintMsg; + std::string_view bailMsg; + // whether to assert contributor stack against <=1 or ==1 + bool sureContributorAlreadyAdded; + }; + const auto logLevel = system::ILogger::ELL_WARNING; args.logger.log("A null immediate child was encountered in the Mul node forming the subtree:\n",logLevel); printSubtree(entry.nodeH); args.logger.log("Forms a 0 constant factor across its ancestors in the mul chain, within the BxDF:\n",logLevel); printSubtree(bxdfRootH); - mulChain.resize(mulChainLen); - // this is so we don't pop a contributor if we happen to be in the right hand subtree relative to the top contributor in the stack and below an `Other` function - if (entry.addContributor) - contributorStack.pop_back(); + // this is mulChainLen to at the first non-MUL ancestor + mulChain.resize(mulChainPrefixEnd); + // go back to the ancestor exprStack.resize(nonMulImmediateAncestorStackEnd); + // we only had the root above our MUL node, we need to bail out of the whole material + if (exprStack.empty()) + { + // if there are no expression above, there must not be a mul chain + assert(mulChain.empty()); + // for the MUL subtree to form the whole tree, there needs to be only one contributor in the stack, but we might have not added it yet + assert(contributorStack.size()<=1); + contributorStack.clear(); + args.logger.log("This MUL subtree is the only subtree, and one of the nodes is UNDEF, returning no contributors for this Tree.",logLevel); + return headH; + } + // If we're in the MUL node with an ancestor, there are twp posibilities as to what's above us: + // - Other, in which case its enough just to clear out the mul chain prefix and the expression stack, any node in our subtree couldn't have pushed a contributor + // - ADD, then we need to distinguish between an ADD which can add a contributor or cannot, and this we'll know by looking at the node we went back to + if (auto& nonMulAncestor=exprStack.back(); nonMulAncestor.contributorStackLen!=StackEntry::DontAddContributor) + { + // if we ever alias `addContributor` with anything else we'd need to check the ancestor type + assert(astPool.deref(nonMulAncestor.nodeH)->getType()==ast_expr_type_e::Add); + // now if we're here it means there's an ADD node above us which meant to add the contributor, but due to what we do before the loop at the very start of the else case + for (auto ancestorEnd=nonMulAncestor.nonMulImmediateAncestorStackEnd; ancestorEnd; ancestorEnd=exprStack[ancestorEnd-1].nonMulImmediateAncestorStackEnd) + { + // all other ADDs above the first ADD above our MUL node CANNOT add a contributor (mul chain incomplete) + assert(exprStack[ancestorEnd-1].contributorStackLen==StackEntry::DontAddContributor); + } + // In the subtree of our current MUL to its immediate ADD there can only be one contributor, if even pushed at all yet + assert(contributorStack.size()==(nonMulAncestor.contributorStackLen+1) || contributorStack.size()==nonMulAncestor.contributorStackLen); + contributorStack.resize(nonMulAncestor.contributorStackLen); + // prevent the ADD we go back to from adding the contributor + nonMulAncestor.contributorStackLen = StackEntry::DontAddContributor; + } break; } continue; @@ -856,15 +1318,21 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin { auto& extraEntry = exprStack.emplace_back(entry); assert(extraEntry.visited); - extraEntry.addContributor = true; + // the `contributorStackLen` stays the same } pushedAChild = true; // regular exploration - exprStack.push_back({.nodeH=childH,.nonMulImmediateAncestorStackEnd=nonMulImmediateAncestorStackEnd,.mulChainLen=mulChainLen}); + exprStack.push_back({ + .nodeH = childH, + .nonMulImmediateAncestorStackEnd = nonMulImmediateAncestorStackEnd, + .mulChainBegin = mulChainBegin, + .mulChainPrefixEnd = mulChainPrefixEnd, + .contributorStackLen = contributorStackLen + }); } - // didn't manage to add any child, dead ADD node + // didn't manage to add any child, dead ADD node, even if couldn't add a contributor in the first place, just disable speculatively if (isAdd && !pushedAChild) - exprStack.back().addContributor = false; + exprStack.back().contributorStackLen = StackEntry::DontAddContributor; } } else @@ -880,8 +1348,9 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin } case ast_expr_type_e::Add: { - if (pEntry->addContributor) + if (pEntry->contributorStackLen!=StackEntry::DontAddContributor) { +// TODO: assert that contributor stack is at most one element more than current node's known contributor reset length // we visited the leftmost subtrees first so this is the right order { auto* const tail = irPool.deref(tailH); @@ -899,10 +1368,11 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin args.logger.log("Failed to Pop the Contributor from the Stack, most likely failed to create the factor node chain.",ELL_ERROR); return (headH=errorRetval); } +// TODO: let us resize the stack, not `popContributor` } // When we are done we need to reset the mul chain back to its original state, even if we don't add a contributor. - // Because this could have been MUL BXDF ADD F_0, F_1 in Reverse Polish Notation - mulChain.resize(pEntry->mulChainLen); + // Because this could have been `MUL BXDF ADD F_0, F_1` in Polish Notation, and when we go onto the `F_1` branch we need to remove the `F_0`'s factors from the chain. + mulChain.resize(pEntry->mulChainPrefixEnd); break; } case ast_expr_type_e::SpectralVariable: @@ -916,6 +1386,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin } // Note that we push onto the mul-chain even if the first non-MUL ancestor node is not an ADD (e.g. Fresnel or other complex function). // Also we may have ADD nodes which are disconnected from a contributor by having an Other ancestor + const bool belowAnOther = pEntry->mulChainBegin>0; // TODO: mulChain probably needs to get renamed into something more semantically sound auto& factor = mulChain.emplace_back(); factor.handle = varH; @@ -941,7 +1412,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin args.logger.log("Forms a 0 constant factor across its ancestors in the mul chain, within the BxDF:\n",logLevel); printSubtree(bxdfRootH); // cancel exploration of all descendands of our first non mul node - mulChain.resize(mulChainBegin); + mulChain.resize(pEntry->mulChainPrefixEnd); exprStack.resize(pEntry->nonMulImmediateAncestorStackEnd); pEntry = nullptr; // if there was nothing else in the tree, we can bail out the whole material @@ -956,7 +1427,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin return headH; } // don't add this MUL subtree island's contributor, it won't exist - if (auto& nonMulAncestor=exprStack.back(); nonMulAncestor.addContributor) + if (auto& nonMulAncestor=exprStack.back(); nonMulAncestor.contributorStackLen!=StackEntry::DontAddContributor) { // if we ever alias `addContributor` with anything else we'd need to check the ancestor type assert(astPool.deref(nonMulAncestor.nodeH)->getType()==ast_expr_type_e::Add); @@ -968,10 +1439,13 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin } default: { - // Due to the genious design of the AST, two BxDFs cannot be multiplied, and the BxDF must be in the leftmost branch of an Add. - // This makes it impossible to have an `IContributorDependant` which is involved in a MUL with more than 1 contributor. - // But one contributor can be multiplied with many `IContributorDependant` which is not a problem because they all go on the mulChain as needed. + // Due to the genious design of the AST, two BxDFs cannot be multiplied, and the BxDF must be in the leftmost branch of a Mul. + // We just need to handle an `IContributorDependant` that is involved in a MUL with more than 1 contributor. + // For example MUL ADD BXDF_0 BXDF_1 FRESNEL_0 + // One contributor can be multiplied with many `IContributorDependant` which is not a problem because they all go on the mulChain as needed. args.logger.log("Unsupported AST Expression type \"%s\"",ELL_ERROR,system::to_string(astExprType).c_str()); +// TODO: add readymade node to `mulChain` + return (headH=errorRetval); } } @@ -999,6 +1473,9 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin // we got all the AST ADD nodes on the way back out assert(mulChain.empty()); return headH; +#endif + + } auto CFrontendIR::ISpectralVariableExpr::createIRNode(const CFrontendIR* ast, CTrueIR* ir) const -> CTrueIR::typed_pointer_type @@ -1014,6 +1491,7 @@ auto CFrontendIR::ISpectralVariableExpr::createIRNode(const CFrontendIR* ast, CT // auto CFrontendIR::SAdd2IRSession::popContributor() -> CTrueIR::typed_pointer_type { +#if 0 // old wrong code auto& irPool = tmpIR->getObjectPool(); // const auto retval = irPool.emplace(); @@ -1060,6 +1538,8 @@ auto CFrontendIR::SAdd2IRSession::popContributor() -> CTrueIR::typed_pointer_typ weighted->factor = factorH; } return retval; +#endif + return {}; } // AST Node -> IR methods From 5e4663824f2574d4f7e8fb41b51f9b7ef34b2e1d Mon Sep 17 00:00:00 2001 From: keptsecret Date: Mon, 11 May 2026 15:59:32 +0700 Subject: [PATCH 37/45] handle coated and next in correlated transmission node --- include/nbl/asset/material_compiler3/CTrueIR.h | 2 +- src/nbl/asset/material_compiler3/CTrueIR.cpp | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 16b693508e..924de80925 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -429,7 +429,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! inline EFinalType getFinalType() const override {return EFinalType::CCorellatedTransmission;} inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CCorellatedTransmission);} - inline std::string_view getChildName_impl(const uint8_t ix) const override final { return ix ? "brdfBottom" : "btdf"; } + inline std::string_view getChildName_impl(const uint8_t ix) const override final { return ix ? (ix > 1 ? "next" : "brdfBottom") : "btdf"; } // you can set the children later inline CCorellatedTransmission() = default; diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp index 0ae7459291..1fa3f5287d 100644 --- a/src/nbl/asset/material_compiler3/CTrueIR.cpp +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -174,8 +174,9 @@ void CTrueIR::SDotPrinter::operator()(std::ostringstream& output) const auto* transmission = dynamic_cast(node); if (transmission) { - typed_pointer_type children[] = { transmission->btdf, transmission->brdfBottom }; // TODO: what about coated and next? + typed_pointer_type children[] = { transmission->btdf, transmission->brdfBottom, transmission->next }; printChildren(children, node); + layerStack.push_back(transmission->coated); } break; } From c9cd3f46c9229cfc53f8bde299388c124535f04a Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Mon, 11 May 2026 11:17:31 +0200 Subject: [PATCH 38/45] it compiles but crashes on printing cook torrance --- .../asset/material_compiler3/CFrontendIR.h | 70 ++++------- .../asset/material_compiler3/CFrontendIR.cpp | 115 ------------------ 2 files changed, 27 insertions(+), 158 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index 053a525727..262ee076f6 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -793,8 +793,6 @@ class CFrontendIR final : public CNodePool NBL_API2 CTrueIR::typed_pointer_type makeContributors(const CFrontendIR::typed_pointer_type bxdfRootH); - NBL_API2 CTrueIR::typed_pointer_type popContributor(); - // inputs to the addMaterials function const SAddMaterialsArgs& args; // for rewriting AST expressions @@ -824,50 +822,36 @@ class CFrontendIR final : public CNodePool // If we perform DFS stack push left-to-right, we'll know the contributor already for all the leaf nodes if we push it onto the stack. // Then for all other leaf nodes we can accumulate them in the MUL chain, and adding their weighted contributor whenever we're back at an ADD node (be it the ancestor or sibling/cousin). // If the contributor is null or multiplied with a null we can keep draining the stack until we're back at its immediate parent ADD node. - struct SContributor - { - // the "active" contributor, basically the leftmost item in the subbranch below and ADD - CTrueIR::typed_pointer_type contributor; - }; - core::vector contributorStack; - // Every time we encounter an AST leaf we must add the current contributor together with all the factors multiplied together + +#endif struct SFactor { - using handle_t = CTrueIR::typed_pointer_type; - // We only track multiplicative factors, we break down every BRDF equally into the canonical form - handle_t handle; - uint8_t negate : 1 = false; - uint8_t monochrome : 1 = true; - // extend later when allowing variable bucket count - uint8_t liveSpectralChannels : 3 = 0b111; - }; - // here we keep the multiplication chain unsorted so its each to add/remove nodes as we encounter them - core::vector mulChain; - // scratch for sorting the mul chain before adding a contributor - core::vector mulChainSortScratch; - // By maintaining a hash map of AST nodes which simplify to a Constant (unity, or zero, or other) we could resolve the issue of the `nonMulImmediateAncestorStackEnd` - // which has us adding the same non-mul node multiple times to stack during the traversal. - // However how much of that would be moving IR manipulation into the AST ? - struct StackEntry - { - constexpr static inline uint64_t DontAddContributor = (0x1u<<10)-1; - - inline bool notVisited() const {return !visited;} - - CFrontendIR::typed_pointer_type nodeH; - // the ancestor ADD node to go back to if we hit a 0 MUL, or if our ADD or any other node becomes 0 - uint64_t nonMulImmediateAncestorStackEnd : 11 = 0; - // the start of the `mulChain`, basically the bits that don't cross an Other node - uint64_t mulChainBegin : 21 = 0; - // the length of the `mulChain` at the time we first visited the node, so that we may reset the prefix back to what it was before continuing down another leg of the ADD - uint64_t mulChainPrefixEnd : 21 = 0; - // only relevant for Add nodes, the value tells you what to trim the contributor stack to - uint64_t contributorStackLen : 10 = DontAddContributor; - // - uint64_t visited : 1 = false; + struct SContributor + { + CTrueIR::typed_pointer_type handle = {}; + uint32_t zeroValue = 0; + }; + // these are the parts that need to be sorted + struct SOrdered + { + CTrueIR::typed_pointer_type handle = {}; + uint32_t monochrome : 1 = true; + // 0 for contributors and leaf factors, contributors can be told apart from leaf factors by having 0s in the whole DWORD here + uint32_t padding : 31 = 0; + }; + + // TODO: do this better, check whole DWORD is 0 + inline bool isContributor() const {return contributor.zeroValue==0;} + + union + { + CTrueIR::typed_pointer_type typeless; + // contributor is always monochrome, etc. + SContributor contributor; + // the fatter thing which actually can be monochrome, etc. + SOrdered factor = {}; + }; }; - core::vector exprStack; -#endif }; inline core::string getNodeID(const typed_pointer_type handle) const {return core::string("_")+std::to_string(handle.value);} diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index f990475fc2..d198836422 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -756,34 +756,6 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin // Variant with contributors: // - just sort the contributor last in the linked list, problem solved - struct SFactor - { - struct SContributor - { - CTrueIR::typed_pointer_type handle = {}; - uint32_t zeroValue = 0; - }; - // these are the parts that need to be sorted - struct SOrdered - { - CTrueIR::typed_pointer_type handle = {}; - uint32_t monochrome : 1 = true; - // 0 for contributors and leaf factors, contributors can be told apart from leaf factors by having 0s in the whole DWORD here - uint32_t padding : 31 = 0; - }; - - // TODO: do this better, check whole DWORD is 0 - inline bool isContributor() const {return contributor.zeroValue==0;} - - union - { - CTrueIR::typed_pointer_type typeless; - // contributor is always monochrome, etc. - SContributor contributor; - // the fatter thing which actually can be monochrome, etc. - SOrdered factor = {}; - }; - }; // Multiplication Chain need to be sorted in a canonical order so its easier to spot them being the same auto sortMuls = [](const SFactor& lhs, const SFactor& rhs)->bool { @@ -1145,20 +1117,6 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin } #if 0 // old and dead code - // error on exit - auto printFailAndCleanupOnExit = core::makeRAIIExiter([&]()->void - { - if (headH!=errorRetval) - return; - printSubtree(exprStack.back().nodeH); - args.logger.log("Within BxDF:\n",ELL_DEBUG); - printSubtree(bxdfRootH); - // no point pushing an error contributor, don't want a best effort compilation within a layer, don't want contributors missing or substituted - exprStack.clear(); - mulChain.clear(); - contributorStack.clear(); - } - ); // Either the same contributor has to be added multiple times and then later cleaned up - the `addContributor` solution // or, we properly add a single contributor and MUL its different additive factors - easily done during a rewrite @@ -1454,25 +1412,6 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin pEntry = nullptr; } } - // There was never an ADD node - if (!contributorStack.empty()) - { - // only one contributor could have been encountered - assert(contributorStack.size()==1); - // add the contributor - headH = irPool.emplace(); - if (const auto contributor=popContributor(); contributor) - irPool.deref(headH._const_cast())->product = contributor; - else - { - args.logger.log("Failed to Pop the Single Contributor from the Stack, most likely failed to create the factor node chain.",ELL_ERROR); - return (headH=errorRetval); - } - mulChain.clear(); - } - // we got all the AST ADD nodes on the way back out - assert(mulChain.empty()); - return headH; #endif @@ -1488,60 +1427,6 @@ auto CFrontendIR::ISpectralVariableExpr::createIRNode(const CFrontendIR* ast, CT return irPool.emplace(realCount,*this); } -// -auto CFrontendIR::SAdd2IRSession::popContributor() -> CTrueIR::typed_pointer_type -{ -#if 0 // old wrong code - auto& irPool = tmpIR->getObjectPool(); - // - const auto retval = irPool.emplace(); - auto* weighted = irPool.deref(retval); - weighted->contributor = contributorStack.back().contributor; - contributorStack.pop_back(); - // we only want the mul chain length till the mul subtree's root, this is the prefix - const size_t mulChainBegin = exprStack.empty() ? 0ull:(exprStack.back().mulChainLen); - if (mulChainBeginbool - { - // monochrome is cheaper - if (lhs.monochrome!=rhs.monochrome) - return lhs.monochrome; - // not doing a complement/negation is cheaper - if (lhs.negate!=rhs.negate) - return rhs.negate; - // DO NOT sort by live spectral channels - // but want negations to show up together in the sorted list so easier to put back together - return lhs.handle.value(combinerState); - { - auto* const factor = irPool.deref(factorH); - auto i = 0; - for (const auto& mul : mulChainSortScratch) - { - assert(!mul.negate); // TODO: handle later - factor->setChildHandle(i++,mul.handle); - } - } - weighted->factor = factorH; - } - return retval; -#endif - return {}; -} - // AST Node -> IR methods auto CFrontendIR::CEmitter::createIRNode(const bool forBTDF, const CFrontendIR* ast, CTrueIR* ir) const -> ir_contributor_handle_t { From 9558afe3e8356bdeac1fd5fd91cb3ce999110b87 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Mon, 11 May 2026 12:12:06 +0200 Subject: [PATCH 39/45] remove all the ugly code --- .../asset/material_compiler3/CFrontendIR.h | 45 ++- .../asset/material_compiler3/CFrontendIR.cpp | 347 +----------------- src/nbl/asset/material_compiler3/CTrueIR.cpp | 192 +++++----- 3 files changed, 133 insertions(+), 451 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index 262ee076f6..d578ae6a33 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -806,24 +806,7 @@ class CFrontendIR final : public CNodePool bool btdfSubtree = false; // for going over layers in the AST core::vector layerStack; -#if 0 // dead and wrong - // Some of the things we must canonicalize: - // A ( f_0 (B + C) + D f_1 ) = f_0 B A + f_0 C A + f_1 D A - // Expression nodes of the Frontend AST really come in 4 variants: - // - add - // - mul - // - complement, which is equivalent to 1 ADD (-1 MUL x) - // - function/other - // BRDFs can appear only under ADD and MUL nodes in the AST not the function/other/complement, so if we want to canonicalize: - // 1. The Add above can be ignored, we form full multiplication chain to the top - // 2. Adds in sibling nodes (below the last add) cause us to have to add a factored copy to the IR - // DFS from right-to-left (inverse order of adding children to stack), would cause us to keep postifxes of the multiplier chain every time we descend into ADD. - // We want to essentially visit the parent ADD node again after dealing with its subtree (in-order traversal) then mul chain can be reset just to the parent. - // If we perform DFS stack push left-to-right, we'll know the contributor already for all the leaf nodes if we push it onto the stack. - // Then for all other leaf nodes we can accumulate them in the MUL chain, and adding their weighted contributor whenever we're back at an ADD node (be it the ancestor or sibling/cousin). - // If the contributor is null or multiplied with a null we can keep draining the stack until we're back at its immediate parent ADD node. - -#endif + // Distribute/hoist the ADD over the MUL. So replace a `MUL ADD A B C` with `ADD MUL A C MUL B C` struct SFactor { struct SContributor @@ -852,6 +835,32 @@ class CFrontendIR final : public CNodePool SOrdered factor = {}; }; }; + // Holds the single `Product_j` of full expression in the form: + // f(w_i,w_o) = Sum_i^N Product_j^{N_i} h_{ij}(w_i,w_o) l_i(w_i,w_o) + // Everything on the `irChain` multiplies together, everything on the `astStack` before the current top is our relative through a MUL node. + // CONTRIBUTOR and OTHER are leaf nodes which don't add any children onto the `astStack`. + // ADD node (and COMPLEMENT which is a specialization of `ADD 1 (-X)`) duplicates the `astStack`, the ADD node at the top of the stack, + // itself was in a MUL relationship with all of the preceding AST nodes which are not explored yet, so its children will also be. + // The key is to not add both children of the ADD onto the same `astStack` because they themselves are not MUL together. + struct SCanonicalProduct + { + // Deal with optimizing this later on, not sure if `DoublyLinkedList` is appropriate, maybe I'd need a `DoublyLinkedBeadedCurtain` data structure + // also the mulChain needs to be sorted later on, and doubly linked list is PITA to sort + core::vector> astStack = {}; // its also a stack + core::vector irChain = {}; + // Expressions for `h_{ij}` can also have ADD/MUL inside and we distribute and canonicalize them at the same time + uint8_t hasContributor : 1 = false; + // extend later when allowing variable bucket count + uint8_t negate : 3 = 0b000; + uint8_t liveSpectralChannels : 3 = 0b111; + }; + // We rework the expression Top down because Bottom up would require descent from the top anyway to find ADD within MUL. + // The List> allows us to descend the AST dually with multiple traversals at once, so we never actually need to rewrite the AST into something else. + core::list canonicalSum; + // TODO: Visit Cache? Every original AST node has its own set of partial mul chains which can be slapped on later ? + // For the visited cache, we'd somehow need to cache a "2D slice" from this, meaning storing a part of the `irChain` prefix of a node + // (would need linked list IR so span/front-back of an irChain subsection can be kept) + // Maybe we could actually keep each IExprNode's irChains as reference to another IExprNode's irChains with a bitmask of the terms to take (merge sort to apply during traversal) }; inline core::string getNodeID(const typed_pointer_type handle) const {return core::string("_")+std::to_string(handle.value);} diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index d198836422..62aa65cfbd 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -729,7 +729,7 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ } -// TODO: this really needs a visited AST nodes (or at least subtrees) cache! +// auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_pointer_type bxdfRootH) -> CTrueIR::typed_pointer_type { CTrueIR::typed_pointer_type headH = {}; @@ -741,20 +741,8 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin auto& astPool = srcAST->getObjectPool(); auto& irPool = tmpIR->getObjectPool(); - // Simple and inefficient Factor Gather with no Contributors: - // - want canonical form, so vector of mul chains - // - Solution, distribute/hoist the ADD over the MUL. So replace a `MUL ADD A B C` with `ADD MUL A C MUL B C` - // - However if there's a long `MUL` chain, then hoist all the way to the top above that MUL, then every unexplored (pushed) node goes in the mul chain - // - Bottom up or Top Down? - // + Bottom up requires descent to bottom anyway to find the ADD within MUL islands - // + Top down we can find the first ADD, then rewrite it and continue rewriting subexpressions - // - Visit Cache? Every original AST node has its own mul chain set ? - // + If 0 factor gets slapped on, chain dies, gets removed from sum - // - linked lists needed ? Or just MUL in tmpAST or IR? - // 1. somehow mark when an AST `IExprNode` doesn't need exploring anymore - // 2. explore on repeat - // Variant with contributors: - // - just sort the contributor last in the linked list, problem solved + // basic checks + assert(canonicalSum.empty()); // Multiplication Chain need to be sorted in a canonical order so its easier to spot them being the same auto sortMuls = [](const SFactor& lhs, const SFactor& rhs)->bool @@ -770,28 +758,6 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin // then by handle return lhs.typeless.value> astStack = {}; // its also a stack - core::vector irChain = {}; - uint8_t hasContributor : 1 = false; - // extend later when allowing variable bucket count - uint8_t negate : 3 = 0b000; - uint8_t liveSpectralChannels : 3 = 0b111; - }; - // TODO: for the visited cache, we'd somehow need to cache a "2D slice" from this, meaning skipping the `irChain` prefix of a node - // (would need linked list IR so span/front-back of an irChain subsection can be kept) - // but also keeping how many of these separate irChains get spawned by the AST node - core::list canonicalSum; // error value const auto errorRetval = tmpIR->getBasicNodes().errorBxDF; @@ -969,9 +935,9 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin case ast_expr_type_e::SpectralVariable: { const auto varH = static_cast(node)->createIRNode(srcAST,tmpIR.get()); - auto* const var = irPool.deref(varH); + const auto* const var = irPool.deref(varH); // no soft fail, the node wasn't null to begin with - if (!var) + if (!var || var->computeHash(irPool)==core::blake3_hash_t{}) { printSubtree(nodeH); args.logger.log("Failed to create the Spectral Variable.",ELL_ERROR); @@ -1070,6 +1036,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin const auto factorH = irPool.emplace(combinerState); { auto* const factor = irPool.deref(factorH); +// TODO: !factor handle auto i = 0; // monochrome negation if (it->negate && it->negate==0b111) @@ -1103,6 +1070,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin } factor->setChildHandle(i++,itChain->factor.handle); } +// TODO: !factor-recomputeHash } weightedContrib->factor = factorH; } @@ -1115,306 +1083,9 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin assert(false); } } + canonicalSum.clear(); -#if 0 // old and dead code - - // Either the same contributor has to be added multiple times and then later cleaned up - the `addContributor` solution - // or, we properly add a single contributor and MUL its different additive factors - easily done during a rewrite - // The former is better because it allows canonicalization of multiplicative factors and gather of identical contributors. To implement it: - // - every ADD node that's reachable via ADD and MUL must add a fully weighted contributor when its visited again by its child - // (all the factors on the way from the ROOT to the ADD node + MUL subtree island below) - // - therefore every ADD node must know the length of the mul prefix from itself to a non-ADD & non-MUL ancestor so it can both - // + be reset so the MUL island of the child node doesn't affect its siblings - // + we're not counting parts of the mul chain crossing an Other node (which models an arbitrary function - e.g. fresnel) - // - islands of MUL nodes must be able to jump back to their first non-MUL ancestor (ADD or Other) and prevent adding of any contributors weighted by them - // - when an ADD node doesn't add contributors it should still get its MUL Island properly taken care of - -// Rewrite Rules -// (a+b)(c+d) = ac+bd+bc+ad - - - // scratches are initialized - assert(mulChain.empty()); - assert(contributorStack.empty()); - exprStack.push_back({.nodeH=bxdfRootH}); - // the mul node gets visited after children are made - while (!exprStack.empty()) - { - auto pEntry = &exprStack.back(); - const auto* const node = astPool.deref(pEntry->nodeH); - assert(node); - // - using ast_expr_type_e = CFrontendIR::IExprNode::Type; - const ast_expr_type_e astExprType = node->getType(); - const bool isContributor = astExprType==ast_expr_type_e::Contributor; - // - if (pEntry->notVisited()) - { - pEntry->visited = true; - if (isContributor) - { - const auto contributorH = static_cast(node)->createIRNode(btdfSubtree,srcAST,tmpIR.get()); - // TODO: recompute instead of compute - if (!contributorH || irPool.deref(contributorH)->computeHash(irPool)==core::blake3_hash_t{}) - { - args.logger.log("Failed to Create IR Contributor from AST",ELL_ERROR); - return (headH=errorRetval); - } - contributorStack.push_back({.contributor=contributorH}); - exprStack.pop_back(); - // shouldn't invalidate but lets play it safe - pEntry = nullptr; - } - else if (astExprType==ast_expr_type_e::SpectralVariable) - { - // this is just easier to read than going through the circus below to do nothing - } - else - { - const bool isAdd = astExprType==ast_expr_type_e::Add; - if (isAdd) - { - // There are no other nodes above current Add other than Mul, then we must add any potential contributors immediately below - if (!pEntry->nonMulImmediateAncestorStackEnd) - pEntry->contributorStackLen = contributorStack.size(); - // A contributor can only be in the leftmost branch of a Mul node's subtree, it also cannot be under any other node than Add or Mul. - // Mostly making sure that Add nodes within complex weighting functions don't add contributors all of the sudden. - // Its like a flood fill on the AST, where any non-Mul and non-Add node stops filling below. - else if (auto& nonMulAncestor=exprStack[pEntry->nonMulImmediateAncestorStackEnd-1]; nonMulAncestor.contributorStackLen!=StackEntry::DontAddContributor) - { - // So use this knowledge to our advantage, however if we ever alias `addContributor` with anything else we'd need to check the ancestor type - assert(astPool.deref(nonMulAncestor.nodeH)->getType()==ast_expr_type_e::Add); - // Current Add node will perform the job of the parent add node for this subtree, it takes over - pEntry->contributorStackLen = contributorStack.size(); - // if we're in the leftmost subtree then reset to current, otherwise also reset to current because the contributor stack size will remain unchanged - nonMulAncestor.contributorStackLen = StackEntry::DontAddContributor; - } - } - const bool notMul = astExprType!=ast_expr_type_e::Mul; - // go through children - const auto childCount = node->getChildCount(); - // pushing back invalidates iterators - const auto entry = *pEntry; - pEntry = nullptr; - // making sure we visit this node again each time a subtree of an Add node is done - bool pushedAChild = false; - // we don't push onto mulchain in the loop so can compute once - const auto mulChainBegin = astExprType==ast_expr_type_e::Other ? static_cast(mulChain.size()):entry.mulChainBegin; - const auto mulChainPrefixEnd = notMul ? static_cast(mulChain.size()):entry.mulChainPrefixEnd; - assert(mulChainBegin<=mulChainPrefixEnd); - // add in reverse so stack processes in order - for (auto childIx=childCount; childIx;) - { - const auto childH = node->getChildHandle(--childIx); - // to be able to go back to the non mul that is supposed to add our subtree - const auto nonMulImmediateAncestorStackEnd = notMul ? static_cast(exprStack.size()):entry.nonMulImmediateAncestorStackEnd; - // skip null child - if (!childH) - { - // what should happen depends if we're in the middle of a MUL subtree - if (notMul) - { - // Generally this is the same as having an OpUndef, for Add we can skip adding this branch's contributor which we implicitly handle with `pushedAChild` - continue; - } - else // kill MUL subtree - { - struct SHandleZeroMulArgs - { - std::string_view preSubtreePrintMsg; - std::string_view preTreePrintMsg; - std::string_view bailMsg; - // whether to assert contributor stack against <=1 or ==1 - bool sureContributorAlreadyAdded; - }; - - const auto logLevel = system::ILogger::ELL_WARNING; - args.logger.log("A null immediate child was encountered in the Mul node forming the subtree:\n",logLevel); - printSubtree(entry.nodeH); - args.logger.log("Forms a 0 constant factor across its ancestors in the mul chain, within the BxDF:\n",logLevel); - printSubtree(bxdfRootH); - // this is mulChainLen to at the first non-MUL ancestor - mulChain.resize(mulChainPrefixEnd); - // go back to the ancestor - exprStack.resize(nonMulImmediateAncestorStackEnd); - // we only had the root above our MUL node, we need to bail out of the whole material - if (exprStack.empty()) - { - // if there are no expression above, there must not be a mul chain - assert(mulChain.empty()); - // for the MUL subtree to form the whole tree, there needs to be only one contributor in the stack, but we might have not added it yet - assert(contributorStack.size()<=1); - contributorStack.clear(); - args.logger.log("This MUL subtree is the only subtree, and one of the nodes is UNDEF, returning no contributors for this Tree.",logLevel); - return headH; - } - // If we're in the MUL node with an ancestor, there are twp posibilities as to what's above us: - // - Other, in which case its enough just to clear out the mul chain prefix and the expression stack, any node in our subtree couldn't have pushed a contributor - // - ADD, then we need to distinguish between an ADD which can add a contributor or cannot, and this we'll know by looking at the node we went back to - if (auto& nonMulAncestor=exprStack.back(); nonMulAncestor.contributorStackLen!=StackEntry::DontAddContributor) - { - // if we ever alias `addContributor` with anything else we'd need to check the ancestor type - assert(astPool.deref(nonMulAncestor.nodeH)->getType()==ast_expr_type_e::Add); - // now if we're here it means there's an ADD node above us which meant to add the contributor, but due to what we do before the loop at the very start of the else case - for (auto ancestorEnd=nonMulAncestor.nonMulImmediateAncestorStackEnd; ancestorEnd; ancestorEnd=exprStack[ancestorEnd-1].nonMulImmediateAncestorStackEnd) - { - // all other ADDs above the first ADD above our MUL node CANNOT add a contributor (mul chain incomplete) - assert(exprStack[ancestorEnd-1].contributorStackLen==StackEntry::DontAddContributor); - } - // In the subtree of our current MUL to its immediate ADD there can only be one contributor, if even pushed at all yet - assert(contributorStack.size()==(nonMulAncestor.contributorStackLen+1) || contributorStack.size()==nonMulAncestor.contributorStackLen); - contributorStack.resize(nonMulAncestor.contributorStackLen); - // prevent the ADD we go back to from adding the contributor - nonMulAncestor.contributorStackLen = StackEntry::DontAddContributor; - } - break; - } - continue; - } - // making sure we visit this node again each time a subtree of an Add node is done - if (isAdd && pushedAChild) - { - auto& extraEntry = exprStack.emplace_back(entry); - assert(extraEntry.visited); - // the `contributorStackLen` stays the same - } - pushedAChild = true; - // regular exploration - exprStack.push_back({ - .nodeH = childH, - .nonMulImmediateAncestorStackEnd = nonMulImmediateAncestorStackEnd, - .mulChainBegin = mulChainBegin, - .mulChainPrefixEnd = mulChainPrefixEnd, - .contributorStackLen = contributorStackLen - }); - } - // didn't manage to add any child, dead ADD node, even if couldn't add a contributor in the first place, just disable speculatively - if (isAdd && !pushedAChild) - exprStack.back().contributorStackLen = StackEntry::DontAddContributor; - } - } - else - { - assert(!isContributor); - // do stuff now - switch (astExprType) - { - case ast_expr_type_e::Mul: - { - // silently skip - break; - } - case ast_expr_type_e::Add: - { - if (pEntry->contributorStackLen!=StackEntry::DontAddContributor) - { -// TODO: assert that contributor stack is at most one element more than current node's known contributor reset length - // we visited the leftmost subtrees first so this is the right order - { - auto* const tail = irPool.deref(tailH); - tailH = irPool.emplace(); - if (tailH) - tail->rest = tailH; - else - headH = tailH; - } - // add current contributor with weight to BxDF Sum - if (const auto contributor=popContributor(); contributor) - irPool.deref(tailH)->product = contributor; - else - { - args.logger.log("Failed to Pop the Contributor from the Stack, most likely failed to create the factor node chain.",ELL_ERROR); - return (headH=errorRetval); - } -// TODO: let us resize the stack, not `popContributor` - } - // When we are done we need to reset the mul chain back to its original state, even if we don't add a contributor. - // Because this could have been `MUL BXDF ADD F_0, F_1` in Polish Notation, and when we go onto the `F_1` branch we need to remove the `F_0`'s factors from the chain. - mulChain.resize(pEntry->mulChainPrefixEnd); - break; - } - case ast_expr_type_e::SpectralVariable: - { - const auto varH = static_cast(node)->createIRNode(srcAST,tmpIR.get()); - auto* const var = irPool.deref(varH); - if (!var) - { - args.logger.log("Failed to create the Spectral Variable.",ELL_ERROR); - return (headH=errorRetval); - } - // Note that we push onto the mul-chain even if the first non-MUL ancestor node is not an ADD (e.g. Fresnel or other complex function). - // Also we may have ADD nodes which are disconnected from a contributor by having an Other ancestor - const bool belowAnOther = pEntry->mulChainBegin>0; - // TODO: mulChain probably needs to get renamed into something more semantically sound - auto& factor = mulChain.emplace_back(); - factor.handle = varH; - const auto channels = var->getSpectralBins(); - factor.monochrome = channels<2; - // we only want the mul chain length till the mul subtree's root - assert(exprStack.size()>=pEntry->nonMulImmediateAncestorStackEnd); - const auto mulChainBegin = pEntry->nonMulImmediateAncestorStackEnd ? (exprStack[pEntry->nonMulImmediateAncestorStackEnd-1].mulChainLen):uint16_t(0); - // this wont be affected by sorting, but we need to check if our prefix is large enough - if (mulChain.size()>=mulChainBegin+2) - factor.liveSpectralChannels = mulChain[mulChain.size()-2].liveSpectralChannels; - for (uint8_t c=0; cgetParameter(c).scale>std::numeric_limits::min())) - factor.liveSpectralChannels &= ~(0b1<nodeH); - args.logger.log("Forms a 0 constant factor across its ancestors in the mul chain, within the BxDF:\n",logLevel); - printSubtree(bxdfRootH); - // cancel exploration of all descendands of our first non mul node - mulChain.resize(pEntry->mulChainPrefixEnd); - exprStack.resize(pEntry->nonMulImmediateAncestorStackEnd); - pEntry = nullptr; - // if there was nothing else in the tree, we can bail out the whole material - if (exprStack.empty()) - { - // if there are no expression above, there must not be a mul chain - assert(mulChain.empty()); - // for the MUL subtree to form the whole tree, there needs to be only one contributor in the stack - assert(contributorStack.size()==1); - contributorStack.clear(); - args.logger.log("This MUL subtree is the only subtree, returning no contributors for this Tree.",logLevel); - return headH; - } - // don't add this MUL subtree island's contributor, it won't exist - if (auto& nonMulAncestor=exprStack.back(); nonMulAncestor.contributorStackLen!=StackEntry::DontAddContributor) - { - // if we ever alias `addContributor` with anything else we'd need to check the ancestor type - assert(astPool.deref(nonMulAncestor.nodeH)->getType()==ast_expr_type_e::Add); - nonMulAncestor.addContributor = false; - contributorStack.pop_back(); - } - } - break; - } - default: - { - // Due to the genious design of the AST, two BxDFs cannot be multiplied, and the BxDF must be in the leftmost branch of a Mul. - // We just need to handle an `IContributorDependant` that is involved in a MUL with more than 1 contributor. - // For example MUL ADD BXDF_0 BXDF_1 FRESNEL_0 - // One contributor can be multiplied with many `IContributorDependant` which is not a problem because they all go on the mulChain as needed. - args.logger.log("Unsupported AST Expression type \"%s\"",ELL_ERROR,system::to_string(astExprType).c_str()); -// TODO: add readymade node to `mulChain` - - return (headH=errorRetval); - } - } - exprStack.pop_back(); - // shouldn't invalidate but lets play it safe - pEntry = nullptr; - } - } -#endif - - + return headH; } auto CFrontendIR::ISpectralVariableExpr::createIRNode(const CFrontendIR* ast, CTrueIR* ir) const -> CTrueIR::typed_pointer_type diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp index 1fa3f5287d..8c2ecf0938 100644 --- a/src/nbl/asset/material_compiler3/CTrueIR.cpp +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -108,112 +108,114 @@ void CTrueIR::SDotPrinter::operator()(std::ostringstream& output) const auto errorBxDF = m_ir->getBasicNodes().errorBxDF; const auto errorLayer = m_ir->getBasicNodes().errorLayer; auto drainNodeStack = [&]()->void + { + while (!nodeStack.empty()) { - while (!nodeStack.empty()) + const auto entry = nodeStack.back(); + // don't print null nodes + assert(entry); + nodeStack.pop_back(); + const auto nodeID = m_ir->getNodeID(entry); + const auto* node = m_ir->getObjectPool().deref(entry); + if (!node) + continue; + output << "\n\t" << m_ir->getLabelledNodeID(entry); + auto printChildren = [&](std::span> children, const INode* node)->void { + uint32_t childIx = 0u; + for (const auto childHandle : children) + { + if (const auto child = m_ir->getObjectPool().deref(childHandle); child) + { + output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(childHandle) << "[label=\"" << node->getChildName_impl(childIx) << "\"]"; + const auto visited = visitedNodes.find(childHandle); + if (visited != visitedNodes.end()) + continue; + nodeStack.push_back(childHandle); + visitedNodes.insert(childHandle); + } + childIx++; + } + }; + switch (node->getFinalType()) { - const auto entry = nodeStack.back(); - nodeStack.pop_back(); - const auto nodeID = m_ir->getNodeID(entry); - const auto* node = m_ir->getObjectPool().deref(entry); - if (!node) - continue; - output << "\n\t" << m_ir->getLabelledNodeID(entry); - auto printChildren = [&](std::span> children, const INode* node)->void { - uint32_t childIx = 0u; - for (const auto childHandle : children) + case INode::EFinalType::CFactorCombiner: + { + const auto* combiner = dynamic_cast(node); + const auto state = combiner->getState(); + const auto childCount = state.childCount; + if (childCount) + { + for (auto childIx = 0; childIx < childCount; childIx++) { + const auto childHandle = combiner->getChildHandle(childIx); if (const auto child = m_ir->getObjectPool().deref(childHandle); child) { - output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(childHandle) << "[label=\"" << node->getChildName_impl(childIx) << "\"]"; + output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(childHandle); const auto visited = visitedNodes.find(childHandle); if (visited != visitedNodes.end()) continue; nodeStack.push_back(childHandle); visitedNodes.insert(childHandle); } - childIx++; } - }; - switch (node->getFinalType()) - { - case INode::EFinalType::CFactorCombiner: - { - const auto* combiner = dynamic_cast(node); - const auto state = combiner->getState(); - const auto childCount = state.childCount; - if (childCount) - { - for (auto childIx = 0; childIx < childCount; childIx++) - { - const auto childHandle = combiner->getChildHandle(childIx); - if (const auto child = m_ir->getObjectPool().deref(childHandle); child) - { - output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(childHandle); - const auto visited = visitedNodes.find(childHandle); - if (visited != visitedNodes.end()) - continue; - nodeStack.push_back(childHandle); - visitedNodes.insert(childHandle); - } - } - } - break; } - case INode::EFinalType::CContributorSum: + break; + } + case INode::EFinalType::CContributorSum: + { + const auto* contributeSum = dynamic_cast(node); + if (contributeSum) { - const auto* contributeSum = dynamic_cast(node); - if (contributeSum) - { - typed_pointer_type children[] = {contributeSum->product, contributeSum->rest}; - printChildren(children, node); - } - break; + typed_pointer_type children[] = {contributeSum->product, contributeSum->rest}; + printChildren(children, node); } - case INode::EFinalType::CCorellatedTransmission: + break; + } + case INode::EFinalType::CCorellatedTransmission: + { + const auto* transmission = dynamic_cast(node); + if (transmission) { - const auto* transmission = dynamic_cast(node); - if (transmission) - { - typed_pointer_type children[] = { transmission->btdf, transmission->brdfBottom, transmission->next }; - printChildren(children, node); - layerStack.push_back(transmission->coated); - } - break; + typed_pointer_type children[] = { transmission->btdf, transmission->brdfBottom, transmission->next }; + printChildren(children, node); + layerStack.push_back(transmission->coated); } - case INode::EFinalType::CWeightedContributor: + break; + } + case INode::EFinalType::CWeightedContributor: + { + const auto* contributor = dynamic_cast(node); + if (contributor) { - const auto* contributor = dynamic_cast(node); - if (contributor) - { - typed_pointer_type children[] = { contributor->contributor, contributor->factor }; - printChildren(children, node); - } - break; + typed_pointer_type children[] = { contributor->contributor, contributor->factor }; + printChildren(children, node); } - case INode::EFinalType::CCookTorrance: + break; + } + case INode::EFinalType::CCookTorrance: + { + const auto* ct = dynamic_cast(node); + if (ct) { - const auto* ct = dynamic_cast(node); - if (ct) + if (const auto eta = m_ir->getObjectPool().deref(ct->orientedRealEta); eta) { - if (const auto eta = m_ir->getObjectPool().deref(ct->orientedRealEta); eta) - { - output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(ct->orientedRealEta) << "[label=\"orientedRealEta\"]"; - const auto visited = visitedNodes.find(ct->orientedRealEta); - if (visited != visitedNodes.end()) - continue; - nodeStack.push_back(ct->orientedRealEta); - visitedNodes.insert(ct->orientedRealEta); - } + output << "\n\t" << nodeID << " -> " << m_ir->getNodeID(ct->orientedRealEta) << "[label=\"orientedRealEta\"]"; + const auto visited = visitedNodes.find(ct->orientedRealEta); + if (visited != visitedNodes.end()) + continue; + nodeStack.push_back(ct->orientedRealEta); + visitedNodes.insert(ct->orientedRealEta); } - break; - } - default: - break; } - // special printing - node->printDot(output, nodeID); + break; } - }; + default: + break; + } + // special printing + node->printDot(output, nodeID); + } + }; drainNodeStack(); while (!layerStack.empty()) @@ -233,21 +235,21 @@ void CTrueIR::SDotPrinter::operator()(std::ostringstream& output) output << "\n\t" << m_ir->getLabelledNodeID(layerHandle); // auto pushNodeRoot = [&](const typed_pointer_type root, const std::string_view edgeLabel)->void - { - if (!root) - return; - // print the link from the layer to the expression - output << "\n\t" << layerID << " -> " << m_ir->getNodeID(root) << "[label=\"" << edgeLabel << "\"]"; - // but not the expression again - const auto visited = visitedNodes.find(root); - if (visited != visitedNodes.end()) - return; - nodeStack.push_back(root); - visitedNodes.insert(root); - }; + { + if (!root) + return; + // print the link from the layer to the expression + output << "\n\t" << layerID << " -> " << m_ir->getNodeID(root) << "[label=\"" << edgeLabel << "\"]"; + // but not the expression again + const auto visited = visitedNodes.find(root); + if (visited != visitedNodes.end()) + return; + nodeStack.push_back(root); + visitedNodes.insert(root); + drainNodeStack(); + }; pushNodeRoot(layerNode->brdfTop, "Top BRDF"); pushNodeRoot(layerNode->firstTransmission, "BTDF"); - drainNodeStack(); } // TODO: print image views From 1985f370a6144c3f8f1f61ca6e252cd7998777c4 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Mon, 11 May 2026 12:30:49 +0200 Subject: [PATCH 40/45] fix my typo --- src/nbl/asset/material_compiler3/CFrontendIR.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index 62aa65cfbd..5a754e2a79 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -684,6 +684,7 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ if (retval.root) transmission->coated = retval.root; } + outLayer->firstTransmission = transmissionH; } } retval.root = layerH; From f96c0c45aebafedd532894509bda6c892d7a0ffe Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Mon, 11 May 2026 13:39:11 +0200 Subject: [PATCH 41/45] fix more egregious bugs, add the negation nodes --- .../asset/material_compiler3/CFrontendIR.h | 6 +- .../nbl/asset/material_compiler3/CTrueIR.h | 3 +- .../asset/material_compiler3/CFrontendIR.cpp | 90 ++++++++++++------- src/nbl/asset/material_compiler3/CTrueIR.cpp | 9 ++ 4 files changed, 73 insertions(+), 35 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index d578ae6a33..675300dee1 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -812,7 +812,9 @@ class CFrontendIR final : public CNodePool struct SContributor { CTrueIR::typed_pointer_type handle = {}; - uint32_t zeroValue = 0; + uint32_t monochrome : 1 = true; + uint32_t isContributor : 1 = true; + uint32_t zeroValue : 30 = 0; }; // these are the parts that need to be sorted struct SOrdered @@ -824,7 +826,7 @@ class CFrontendIR final : public CNodePool }; // TODO: do this better, check whole DWORD is 0 - inline bool isContributor() const {return contributor.zeroValue==0;} + inline bool isContributor() const {return contributor.monochrome && contributor.isContributor && contributor.zeroValue==0;} union { diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 406fec36a0..76505445db 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -636,7 +636,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! if (getKnotCount()>1) { const ESemantics semantics = getSemantics(); - if (semantics!=ESemantics::NoneUndefined) + if (semantics==ESemantics::NoneUndefined) return false; hasher << semantics; } @@ -832,6 +832,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! inline bool operator==(const SBasicNodes& other) const = default; typed_pointer_type blackHoleBxDF = {}; + typed_pointer_type scalarNegation = {}; // these are never meant to be hashed and inserted into `m_uniqueNodes` typed_pointer_type errorBxDF = {}; typed_pointer_type errorLayer = {}; diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index 5a754e2a79..246161589e 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -752,7 +752,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin if (lhs.isContributor()!=rhs.isContributor()) return lhs.isContributor(); // only one contributor allowed per chain! - assert(!lhs.isContributor() && !rhs.isContributor()); + assert(&lhs==&rhs || !lhs.isContributor() && !rhs.isContributor()); // monochrome is cheaper if (lhs.factor.monochrome!=rhs.factor.monochrome) return lhs.factor.monochrome; @@ -762,26 +762,26 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin // error value const auto errorRetval = tmpIR->getBasicNodes().errorBxDF; + // negationNode + const auto scalarNegation = tmpIR->getBasicNodes().scalarNegation; // good code start assert(canonicalSum.empty()); // add the first term to explore canonicalSum.emplace_back().astStack.emplace_back() = bxdfRootH; // error on exit - auto it=canonicalSum.begin(); auto printFailAndCleanupOnExit = core::makeRAIIExiter([&]()->void { if (headH!=errorRetval) return; - printSubtree(it->astStack.back()); - args.logger.log("Within BxDF:\n",ELL_DEBUG); + args.logger.log("Within BxDF:",ELL_DEBUG); printSubtree(bxdfRootH); // no point emitting an error contributor, don't want a best effort compilation within a layer, don't want contributors missing or substituted canonicalSum.clear(); } ); CTrueIR::typed_pointer_type tailH = {}; - for (; it!=canonicalSum.end(); it++) + for (auto it=canonicalSum.begin(); it!=canonicalSum.end(); it++) { auto& irChain = it->irChain; auto& astStack = it->astStack; @@ -812,6 +812,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin if (!contributorH || irPool.deref(contributorH)->computeHash(irPool)==core::blake3_hash_t{}) { args.logger.log("Failed to Create IR Contributor from AST",ELL_ERROR); + printSubtree(nodeH); return (headH=errorRetval); } it->hasContributor = true; @@ -832,6 +833,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin if constexpr (HardFail) { args.logger.log("Undef node in a MUL",ELL_ERROR); + printSubtree(nodeH); return (headH=errorRetval); } else @@ -868,6 +870,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin if constexpr (HardFail) { args.logger.log("Undef node in an ADD",ELL_ERROR); + printSubtree(nodeH); return (headH=errorRetval); } else @@ -922,6 +925,7 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin if constexpr (HardFail) { args.logger.log("Child of COMPLEMENT is null,",ELL_ERROR); + printSubtree(nodeH); return (headH=errorRetval); } else @@ -935,13 +939,18 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin } case ast_expr_type_e::SpectralVariable: { + // + astStack.pop_back(); + // shouldn't invalidate iterator, but underline our vector changes + pEntry = nullptr; + // const auto varH = static_cast(node)->createIRNode(srcAST,tmpIR.get()); const auto* const var = irPool.deref(varH); // no soft fail, the node wasn't null to begin with if (!var || var->computeHash(irPool)==core::blake3_hash_t{}) { - printSubtree(nodeH); args.logger.log("Failed to create the Spectral Variable.",ELL_ERROR); + printSubtree(nodeH); return (headH=errorRetval); } // see what channels are dead @@ -978,14 +987,17 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin } case ast_expr_type_e::Other: { + // + astStack.pop_back(); // TODO: We need to start a new `canonicalSum` and save a link to it in the current IR - printSubtree(nodeH); args.logger.log("Unsupported AST Expression type \"%s\"",ELL_ERROR,system::to_string(astExprType).c_str()); + printSubtree(nodeH); return (headH=errorRetval); break; } default: assert(false); + printSubtree(nodeH); return (headH=errorRetval); } } @@ -1006,23 +1018,10 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin { // if we have a contributor first node in the sorted chain must be the contributor assert(irChain.front().isContributor()); - // we visited the leftmost subtrees first so this is the right order + // produce the weighted contributor + const auto weightedContribH = irPool.emplace(); + if (auto* const weightedContrib=irPool.deref(weightedContribH); weightedContrib) { - auto* const tail = irPool.deref(tailH); - // allocate new tail - tailH = irPool.emplace(); - // append it - if (tail) - tail->rest = tailH; - else - headH = tailH; - } - // now get the true tail - { - auto* const tail = irPool.deref(tailH); - // and slap the mul chain onto it - const auto weightedContribH = irPool.emplace(); - auto* const weightedContrib = irPool.deref(weightedContribH); weightedContrib->contributor = irChain.front().contributor.handle; // now the mul chain if (irChain.size()>1) @@ -1035,16 +1034,12 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin if (it->negate==0) --combinerState.childCount; const auto factorH = irPool.emplace(combinerState); + if (auto* const factor=irPool.deref(factorH); factor) { - auto* const factor = irPool.deref(factorH); -// TODO: !factor handle auto i = 0; // monochrome negation if (it->negate && it->negate==0b111) - { - // TODO: do with premade node so sorting is correct (lowest) - assert(false); - } + factor->setChildHandle(i++,scalarNegation); bool monochromeNodes = true; for (auto itChain=irChain.begin()+1; itChain!=irChain.end(); itChain++) { @@ -1056,11 +1051,17 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin if (!itChain->factor.monochrome) { monochromeNodes = false; - // make the non-monochrome negation node + // make the non-monochrome negation node, not using premade cause that would tie me up with spectral buckets if (it->negate && it->negate!=0b111) { - // TODO: do with premade node so sorting is correct (lowest) - assert(false); + const auto negationH = irPool.emplace(uint8_t(3)); + if (auto* const negation=irPool.deref(negationH); negation) + { + for (uint8_t c=0; c<3; c++) + negation->setParameter(c,{.scale=bool((it->negate>>c)&0x1u) ? (-1.f):1.f}); +// TODO: !negation->recomputeHash + } + factor->setChildHandle(i++,negationH); } } } @@ -1073,10 +1074,35 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin } // TODO: !factor-recomputeHash } + else + { + args.logger.log("Couldn't allocate a `CTrueIR::CFactorCombiner` node",ELL_ERROR); + return (headH=errorRetval); + } weightedContrib->factor = factorH; } + } + else + { + args.logger.log("Couldn't allocate a `CTrueIR::CWeightedContributor` node",ELL_ERROR); + return (headH=errorRetval); + } + // we visited the leftmost subtrees first so this is the right order + auto* const oldTail = irPool.deref(tailH); + // allocate new tail and slap the mul chain onto it + tailH = irPool.emplace(); + if (auto* const tail=irPool.deref(tailH); tail) tail->product = weightedContribH; + else + { + args.logger.log("Couldn't allocate a `CTrueIR::CContributorSum` node",ELL_ERROR); + return (headH=errorRetval); } + // append it + if (oldTail) + oldTail->rest = tailH; + else + headH = tailH; } else { diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp index 8c2ecf0938..dee2243e77 100644 --- a/src/nbl/asset/material_compiler3/CTrueIR.cpp +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -74,6 +74,15 @@ CTrueIR::SBasicNodes::SBasicNodes(CTrueIR* ir) assert(success); ir->m_uniqueNodes[node->getHash()] = blackHoleBxDF; } + // + scalarNegation = pool.emplace(uint8_t(1)); + { + auto* const node = pool.deref(scalarNegation._const_cast()); + node->setParameter(0,{.scale=-1.f}); + const bool success = node->recomputeHash(pool); + assert(success); + ir->m_uniqueNodes[node->getHash()] = scalarNegation; + } // we never compute the hashes on these ones, they're supposed to have invalid hash errorLayer = pool.emplace(); { From 7e34073ba64152eb01278209043790861e3502a6 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Tue, 12 May 2026 04:25:37 +0200 Subject: [PATCH 42/45] base64 encoded blake3 print of CTrueIR nodes, correct the hash initialization --- .../nbl/asset/material_compiler3/CTrueIR.h | 29 +++++++++++++---- include/nbl/core/hash/blake.h | 7 ++++ include/nbl/system/to_string.h | 25 +++++++++++++++ .../asset/material_compiler3/CFrontendIR.cpp | 32 +++++++++++-------- src/nbl/asset/material_compiler3/CTrueIR.cpp | 2 +- 5 files changed, 75 insertions(+), 20 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 76505445db..7abff85593 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -237,20 +237,20 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! inline bool recomputeHash(const obj_pool_type& pool) { hash = computeHash(pool); - return hash!=core::blake3_hash_t{}; + return hash!=core::blake3_hash_t::EmptyInput(); } virtual bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const = 0; #define HASH_REQUIREDS_HASH(HANDLE) { \ - if (const auto hash=pool.deref(HANDLE)->getHash(); hash==core::blake3_hash_t{}) \ + if (const auto hash=pool.deref(HANDLE)->getHash(); hash==core::blake3_hash_t::EmptyInput()) \ return false; \ else \ hasher << hash; \ } -#define HASH_OPTIONALS_HASH(HANDLE) if (HANDLE) {HASH_REQUIREDS_HASH(HANDLE);} else {hasher << core::blake3_hash_t{};} +#define HASH_OPTIONALS_HASH(HANDLE) if (HANDLE) {HASH_REQUIREDS_HASH(HANDLE);} else {hasher << core::blake3_hash_t::EmptyInput();} // Each node is final and immutable, has a precomputed hash for the whole subtree beneath it. // Debug info does not form part of the hash, so can get wildly replaced. - core::blake3_hash_t hash; + core::blake3_hash_t hash = core::blake3_hash_t::EmptyInput(); }; // template requires std::is_base_of_v> @@ -842,7 +842,23 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! NBL_API2 SBasicNodes(CTrueIR* ir); }; const SBasicNodes& getBasicNodes() const {return m_basicNodes;} - + + // + template requires std::is_base_of_v + inline typed_pointer_type hashNCache(const typed_pointer_type origH) + { + auto* const orig = getObjectPool().deref(origH); + if (orig) + if (orig->getHash()!=core::blake3_hash_t::EmptyInput() || orig->recomputeHash(getObjectPool())) + { + auto& slot = m_uniqueNodes[orig->getHash()]; + if (slot) + return CNodePool::obj_pool_type::block_allocator_type::_static_cast(slot); + slot = origH; + return origH; + } + return {}; + } // Each material comes down to this, this is the only struct we don't de-duplicate struct SMaterial @@ -1079,10 +1095,11 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! inline core::string getLabelledNodeID(const typed_pointer_type handle) const { const INode* node = getObjectPool().deref(handle); + assert(node); core::string retval = getNodeID(handle); retval += " [label=\""; retval += node->getTypeName(); - // maybe also add hash? + retval += "\\n" + system::to_string(node->getHash()); // maybe label suffix? retval += "\"]"; return retval; diff --git a/include/nbl/core/hash/blake.h b/include/nbl/core/hash/blake.h index fb91c9969f..a50d587e86 100644 --- a/include/nbl/core/hash/blake.h +++ b/include/nbl/core/hash/blake.h @@ -9,12 +9,19 @@ #include "blake3.h" #include +#include namespace nbl::core { struct blake3_hash_t final { + constexpr static inline core::blake3_hash_t EmptyInput() + { + constexpr uint64_t data[4] = {0xa6a1f9f5b94913afull,0x49c9dc36ea4d40a0ull,0xb712c1adc925cb9bull,0x62321fe4ca939accull}; + return std::bit_cast(data); + } + inline bool operator==(const blake3_hash_t&) const = default; // could initialize this to a hash of a zero-length array, diff --git a/include/nbl/system/to_string.h b/include/nbl/system/to_string.h index 1f8988566e..d746f66ce6 100644 --- a/include/nbl/system/to_string.h +++ b/include/nbl/system/to_string.h @@ -31,6 +31,31 @@ struct to_string_helper } }; +template<> +struct to_string_helper +{ + static std::string __call(const core::blake3_hash_t& value) + { + // fast base64 optimized for this without leaking deps + std::string retval(44,'='); + constexpr const char* base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + auto out = retval.data(); + // every 3 bytes of input create 4 bytes of output + for (auto i=0; i<11; i++) + { + const uint8_t bytes[3] = {value.data[3*i+0],value.data[3*i+1],i!=10 ? value.data[3*i+2]:uint8_t(0)}; + *(out++) = base[bytes[0]>>2]; + // take bottom bits of first byte to be top of the 6 bit, and 4 top bits of next byte to be bottom 4 bits of 6 bit + *(out++) = base[((bytes[0]&0x03u)<<4)|(bytes[1]>>4)]; + // take bottom bits of the second byte to be top 4 bits of next byte, and top 2 bits of last byte to be bottom 2 + *(out++) = base[((bytes[1]&0x0fu)<<2)|(bytes[2]>>6)]; + *(out++) = base[bytes[2]&0x3Fu]; + } + // padding is inferred from length + return retval; + } +}; + template<> struct to_string_helper { diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index 246161589e..0d08d33619 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -807,9 +807,8 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin // shouldn't invalidate iterator, but underline our vector changes pEntry = nullptr; // push onto the irChain - const auto contributorH = static_cast(node)->createIRNode(btdfSubtree,srcAST,tmpIR.get()); - // TODO: recompute instead of compute hash - if (!contributorH || irPool.deref(contributorH)->computeHash(irPool)==core::blake3_hash_t{}) + const auto contributorH = tmpIR->hashNCache(static_cast(node)->createIRNode(btdfSubtree,srcAST,tmpIR.get())); + if (!contributorH) { args.logger.log("Failed to Create IR Contributor from AST",ELL_ERROR); printSubtree(nodeH); @@ -944,10 +943,10 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin // shouldn't invalidate iterator, but underline our vector changes pEntry = nullptr; // - const auto varH = static_cast(node)->createIRNode(srcAST,tmpIR.get()); + const auto varH = tmpIR->hashNCache(static_cast(node)->createIRNode(srcAST,tmpIR.get())); const auto* const var = irPool.deref(varH); // no soft fail, the node wasn't null to begin with - if (!var || var->computeHash(irPool)==core::blake3_hash_t{}) + if (!var) { args.logger.log("Failed to create the Spectral Variable.",ELL_ERROR); printSubtree(nodeH); @@ -1059,7 +1058,12 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin { for (uint8_t c=0; c<3; c++) negation->setParameter(c,{.scale=bool((it->negate>>c)&0x1u) ? (-1.f):1.f}); -// TODO: !negation->recomputeHash + } + const auto uniqueH = tmpIR->hashNCache(negationH); + if (!uniqueH) + { + args.logger.log("Couldn't create a unique spectral negation node",ELL_ERROR); + return (headH=errorRetval); } factor->setChildHandle(i++,negationH); } @@ -1072,27 +1076,29 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin } factor->setChildHandle(i++,itChain->factor.handle); } -// TODO: !factor-recomputeHash } - else + const auto uniqueH = tmpIR->hashNCache(factorH); + if (!uniqueH) { - args.logger.log("Couldn't allocate a `CTrueIR::CFactorCombiner` node",ELL_ERROR); + args.logger.log("Couldn't allocate or hash a `CTrueIR::CFactorCombiner` node",ELL_ERROR); return (headH=errorRetval); } - weightedContrib->factor = factorH; + weightedContrib->factor = uniqueH; } } - else + const auto uniqueWeightedH = tmpIR->hashNCache(weightedContribH); + if (!uniqueWeightedH) { - args.logger.log("Couldn't allocate a `CTrueIR::CWeightedContributor` node",ELL_ERROR); + args.logger.log("Couldn't allocate or hash a `CTrueIR::CWeightedContributor` node",ELL_ERROR); return (headH=errorRetval); } // we visited the leftmost subtrees first so this is the right order auto* const oldTail = irPool.deref(tailH); // allocate new tail and slap the mul chain onto it tailH = irPool.emplace(); + // note that we hold off on hashing the tail node, cause its subject to change if (auto* const tail=irPool.deref(tailH); tail) - tail->product = weightedContribH; + tail->product = uniqueWeightedH; else { args.logger.log("Couldn't allocate a `CTrueIR::CContributorSum` node",ELL_ERROR); diff --git a/src/nbl/asset/material_compiler3/CTrueIR.cpp b/src/nbl/asset/material_compiler3/CTrueIR.cpp index dee2243e77..497bdf9f23 100644 --- a/src/nbl/asset/material_compiler3/CTrueIR.cpp +++ b/src/nbl/asset/material_compiler3/CTrueIR.cpp @@ -258,7 +258,7 @@ void CTrueIR::SDotPrinter::operator()(std::ostringstream& output) drainNodeStack(); }; pushNodeRoot(layerNode->brdfTop, "Top BRDF"); - pushNodeRoot(layerNode->firstTransmission, "BTDF"); + pushNodeRoot(layerNode->firstTransmission, "Corellated Trans"); } // TODO: print image views From 5dd0ad2bf8a1a8c9b18d0d2e9089b9fcc8392585 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Tue, 12 May 2026 08:06:49 +0200 Subject: [PATCH 43/45] commonalize the node copying code --- include/nbl/asset/material_compiler3/CFrontendIR.h | 6 +----- include/nbl/asset/material_compiler3/CNodePool.h | 12 ++++++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index 675300dee1..240813506c 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -161,11 +161,7 @@ class CFrontendIR final : public CNodePool virtual _typed_pointer_type copy(CFrontendIR* ir) const = 0; #define COPY_DEFAULT_IMPL inline _typed_pointer_type copy(CFrontendIR* ir) const override final \ { \ - auto& pool = ir->getObjectPool(); \ - const auto copyH = pool.emplace > >(); \ - if (auto* const copy = pool.deref(copyH); copyH) \ - *copy = *this; \ - return copyH; \ + return CNodePool::copyNode > >(this,ir); \ } // child managment diff --git a/include/nbl/asset/material_compiler3/CNodePool.h b/include/nbl/asset/material_compiler3/CNodePool.h index cb7b9d5ff0..a4f144e0a1 100644 --- a/include/nbl/asset/material_compiler3/CNodePool.h +++ b/include/nbl/asset/material_compiler3/CNodePool.h @@ -118,6 +118,18 @@ class CNodePool : public core::IReferenceCounted protected: inline CNodePool(typename obj_pool_type::creation_params_type&& params) : m_composed(std::move(params)) {} + // does a shallow copy (no need to deref any of the children/deeper references), the pool itself must have a `deepCopy` method for that + template requires std::is_copy_assignable_v + static typed_pointer_type copyNode(const T* src, CNodePool* dstPool) + { + assert(src); + auto& pool = dstPool->getObjectPool(); + const auto copyH = pool.emplace(); + if (auto* const copy=pool.deref(copyH); copyH) + *copy = *src; + return copyH; + } + obj_pool_type m_composed; }; From 879e299418cdbbd043f530a97ae58fd5eefc2cb3 Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Thu, 14 May 2026 02:12:08 +0200 Subject: [PATCH 44/45] Fix hashing of CTrueIR::ISpectralVariableFactor and the layers and sums themselves --- .../asset/material_compiler3/CFrontendIR.h | 2 + .../nbl/asset/material_compiler3/CTrueIR.h | 32 +++++++++----- .../asset/material_compiler3/CFrontendIR.cpp | 44 +++++++++++-------- 3 files changed, 48 insertions(+), 30 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index 240813506c..f929eec880 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -846,6 +846,8 @@ class CFrontendIR final : public CNodePool // also the mulChain needs to be sorted later on, and doubly linked list is PITA to sort core::vector> astStack = {}; // its also a stack core::vector irChain = {}; + // this is to help us hash in reverse properly + CTrueIR::typed_pointer_type sumTermH = {}; // Expressions for `h_{ij}` can also have ADD/MUL inside and we distribute and canonicalize them at the same time uint8_t hasContributor : 1 = false; // extend later when allowing variable bucket count diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index 7abff85593..af8efbcb10 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -225,7 +225,7 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // always put the node type into the hash hasher << static_cast(getFinalType()); if (!computeHash_impl(pool,hasher)) - return {}; + return {}; return hasher.operator core::blake3_hash_t(); } @@ -630,16 +630,27 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! { inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override final { - hasher << *pWonky(); - for (uint8_t c=1; cparams[c]; - if (getKnotCount()>1) + const auto count = getKnotCount(); + hasher << count; { const ESemantics semantics = getSemantics(); - if (semantics==ESemantics::NoneUndefined) + if (getKnotCount()>1 && semantics==ESemantics::NoneUndefined) return false; hasher << semantics; } + bool hasTextures = false; + const auto* const wonky = pWonky(); + for (uint8_t i=0; iparams[i]; + hasTextures = hasTextures || wonky->params[i].view; + } + if (hasTextures) + { + hasher << wonky->uvTransform; + hasher << wonky->uvSlot(); + } return true; } @@ -1296,17 +1307,16 @@ struct core::blake3_hasher::update_impl,Dummy> { bool noTextures = true; for (uint8_t i=0; i diff --git a/src/nbl/asset/material_compiler3/CFrontendIR.cpp b/src/nbl/asset/material_compiler3/CFrontendIR.cpp index 0d08d33619..5af5f25e01 100644 --- a/src/nbl/asset/material_compiler3/CFrontendIR.cpp +++ b/src/nbl/asset/material_compiler3/CFrontendIR.cpp @@ -610,6 +610,7 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ // HOWEVER the `tmpAST` gets cleared every call to `makeOrientedMaterial` so only cache within a `makeOrientedMaterial` call, // so clear when AST changes or keep separate caches for original AST and tmp. // core::unordered_map layers; +// core::unordered_map transmissions; // core::unordered_map topBRDFcache; // core::unordered_map BTDFcache; @@ -648,7 +649,7 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ const auto errorLayer = tmpIR->getBasicNodes().errorLayer; // Some metadata needed for us bool layersBelowCanScatterBack = false; - // then go in reverse and do the layers + // then go in reverse and do the layers bottom up for (auto layerIt=layerStack.rbegin(); layerIt!=layerStack.rend(); layerIt++) { const auto& inLayer = *layerIt; @@ -680,14 +681,14 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ if (transmission->brdfBottom==errorBxDF) return {.root=errorLayer}; } - // we check if previous layer didn't get oprimized away + // we check if previous layer didn't get optimized away if (retval.root) transmission->coated = retval.root; } - outLayer->firstTransmission = transmissionH; + outLayer->firstTransmission = tmpIR->hashNCache(transmissionH); } } - retval.root = layerH; + retval.root = tmpIR->hashNCache(layerH); // Now optimize everything inserting it into the proper IR { // temporary debug print @@ -711,7 +712,7 @@ auto CFrontendIR::SAdd2IRSession::makeOrientedMaterial(const CFrontendIR::typed_ printIRLayer(retval.root,tmpIR.get()); } // Set this for the next layer after us, we are reflective or previous layer scatters back towards us and we don't block it - if (auto* const outLayer=irPool.deref(retval.root); outLayer) + if (const auto* const outLayer=irPool.deref(retval.root); outLayer) layersBelowCanScatterBack = bool(outLayer->brdfTop) || layersBelowCanScatterBack && outLayer->firstTransmission; else layersBelowCanScatterBack = false; @@ -780,7 +781,6 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin canonicalSum.clear(); } ); - CTrueIR::typed_pointer_type tailH = {}; for (auto it=canonicalSum.begin(); it!=canonicalSum.end(); it++) { auto& irChain = it->irChain; @@ -958,8 +958,8 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin for (uint8_t c=0; cgetParameter(c).scale; - if (std::numeric_limits::min()<=scale && scale::infinity()) + const auto absScale = hlsl::abs(cVar->getParameter(c).scale); + if (std::numeric_limits::min()<=absScale && absScale::infinity()) liveMask |= uint8_t(1)<liveSpectralChannels&=liveMask)==0) { - const auto logLevel = system::ILogger::ELL_PERFORMANCE; + const auto logLevel = system::ILogger::ELL_PERFORMANCE; // if we REALLY want to we can print all the nodes in the `irChain` so far, but then the irChain needs to track debug data from AST args.logger.log("Product turns to 0 by multiplying the chain so far with the Spectral Variable:\n",logLevel); printSubtree(nodeH); @@ -1092,12 +1092,10 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin args.logger.log("Couldn't allocate or hash a `CTrueIR::CWeightedContributor` node",ELL_ERROR); return (headH=errorRetval); } - // we visited the leftmost subtrees first so this is the right order - auto* const oldTail = irPool.deref(tailH); // allocate new tail and slap the mul chain onto it - tailH = irPool.emplace(); + it->sumTermH = irPool.emplace(); // note that we hold off on hashing the tail node, cause its subject to change - if (auto* const tail=irPool.deref(tailH); tail) + if (auto* const tail=irPool.deref(it->sumTermH); tail) tail->product = uniqueWeightedH; else { @@ -1105,10 +1103,11 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin return (headH=errorRetval); } // append it - if (oldTail) - oldTail->rest = tailH; - else - headH = tailH; + if (it!=canonicalSum.begin()) + { + auto itCopy = it; + irPool.deref((--itCopy)->sumTermH)->rest = it->sumTermH; + } } else { @@ -1116,8 +1115,15 @@ auto CFrontendIR::SAdd2IRSession::makeContributors(const CFrontendIR::typed_poin assert(false); } } - canonicalSum.clear(); - + // now hash in reverse + for (auto it=canonicalSum.rbegin(); it!=canonicalSum.rend(); it++) + it->sumTermH = tmpIR->hashNCache(it->sumTermH)._const_cast(); + // set result + if (!canonicalSum.empty()) + { + headH = canonicalSum.begin()->sumTermH; + canonicalSum.clear(); + } return headH; } From 7710662b59aa61a704ac48ec12f7b5f6130dfa2d Mon Sep 17 00:00:00 2001 From: devshgraphicsprogramming Date: Thu, 14 May 2026 02:59:13 +0200 Subject: [PATCH 45/45] add the 3 missing IR nodes --- .../asset/material_compiler3/CFrontendIR.h | 5 +- .../nbl/asset/material_compiler3/CTrueIR.h | 103 +++++++++++++++++- 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/include/nbl/asset/material_compiler3/CFrontendIR.h b/include/nbl/asset/material_compiler3/CFrontendIR.h index f929eec880..f8427aba5e 100644 --- a/include/nbl/asset/material_compiler3/CFrontendIR.h +++ b/include/nbl/asset/material_compiler3/CFrontendIR.h @@ -362,8 +362,7 @@ class CFrontendIR final : public CNodePool // 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 + // See `CTrueIR::Beer` for docs typed_pointer_type perpTransmittance = {}; typed_pointer_type thickness = {}; @@ -376,7 +375,7 @@ class CFrontendIR final : public CNodePool *(ix ? &perpTransmittance:&thickness) = block_allocator_type::_static_cast(newChild); } - inline std::string_view getChildName_impl(const uint8_t ix) const override {return "Perpendicular\\nTransmittance";} + inline std::string_view getChildName_impl(const uint8_t ix) const override {return ix ? "Thickness":"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 diff --git a/include/nbl/asset/material_compiler3/CTrueIR.h b/include/nbl/asset/material_compiler3/CTrueIR.h index af8efbcb10..496ae3487f 100644 --- a/include/nbl/asset/material_compiler3/CTrueIR.h +++ b/include/nbl/asset/material_compiler3/CTrueIR.h @@ -212,7 +212,10 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! CSpectralVariable, COrenNayar, CCookTorrance, - CFactorCombiner + CFactorCombiner, + CBeer, + CFresnel, + CThinInfiniteScatterCorrection }; virtual EFinalType getFinalType() const = 0; @@ -831,8 +834,104 @@ class CTrueIR : public CNodePool // TODO: turn into an asset! // BRDFs and BTDFs components into separate expressions, and also importance sample much better. typed_pointer_type orientedRealEta = {}; }; - //! Parameter Nodes //! Basic factor nodes + // 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 contributor BxDF node. With refractions from Dielectrics, we get just `1/LdotX`, for Delta Transmission we get `1/VdotN` since its the same. + // Note: its allowed to apply Beer directly on BRDF as well as BTDF to simulate foggy extinction on the top layer + class CBeer final : public obj_pool_type::INonTrivial, public IFactorLeaf + { + inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override + { + hasher << channels; + HASH_REQUIREDS_HASH(perpTransmittance); + HASH_REQUIREDS_HASH(thickness); + return true; + } + + public: + inline EFinalType getFinalType() const override {return EFinalType::CBeer;} + + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CBeer);} + + inline std::string_view getChildName_impl(const uint8_t ix) const override final {return ix ? "Thickness":"Perpendicular\\nTransmittance";} + + inline uint8_t getSpectralBins() const override {return channels;} + + // cannot be null, otherwise no point being there as term will multiply to 0 + typed_pointer_type perpTransmittance = {}; + // cannot be null, otherwise its always exp2(0) and term will always be 1 + typed_pointer_type thickness = {}; + // can be worked out by analyzing what we point to, but not needed + uint8_t channels = 3; + }; + class CFresnel final : public obj_pool_type::INonTrivial, public IFactorLeaf + { + inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override + { + hasher << reciprocateEtas; + hasher << channels; + if (orientedImagEta) + { + HASH_OPTIONALS_HASH(orientedRealEta); + hasher << orientedImagEta; + } + else + { + HASH_REQUIREDS_HASH(orientedRealEta); + } + return true; + } + + public: + inline EFinalType getFinalType() const override {return EFinalType::CFresnel;} + + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CFresnel);} + + inline std::string_view getChildName_impl(const uint8_t ix) const override final {return ix ? "Imaginary":"Real";} + + inline uint8_t getSpectralBins() const override {return channels;} + + // cannot be null or a constant of 1 while imaginary is null + typed_pointer_type orientedRealEta = {}; + // If null, then treated as a 0 (important to optimize those to 0). MUST be null for BTDFs! + typed_pointer_type orientedImagEta = {}; + // easier on the codegen + uint8_t reciprocateEtas : 1 = false; + // can be worked out by analyzing what we point to, but not needed + uint8_t channels : 7 = 3; + }; + class CThinInfiniteScatterCorrection final : public obj_pool_type::INonTrivial, public IFactorLeaf + { + inline bool computeHash_impl(const obj_pool_type& pool, core::blake3_hasher& hasher) const override + { + hasher << channels; + HASH_REQUIREDS_HASH(reflectanceTop); + HASH_OPTIONALS_HASH(extinction); + if (reflectanceBottom) + hasher << reflectanceBottom; + else + hasher << reflectanceTop; + return true; + } + + public: + inline EFinalType getFinalType() const override {return EFinalType::CThinInfiniteScatterCorrection;} + + inline const std::string_view getTypeName() const override {return TYPE_NAME_STR(CThinInfiniteScatterCorrection);} + + inline std::string_view getChildName_impl(const uint8_t ix) const override final {return ix ? (ix>1 ? "reflectanceBottom":"extinction"):"reflectanceTop";} + + inline uint8_t getSpectralBins() const override {return channels;} + + // cannot be null otherwise no point being there + typed_pointer_type reflectanceTop = {}; + // optional, if null then treated as E=1.0 + typed_pointer_type extinction = {}; + // optional, if null then `reflectanceTop` used in its place + typed_pointer_type reflectanceBottom = {}; + // can be worked out by analyzing what we point to, but not needed + uint8_t channels = 3; + }; #undef TYPE_NAME_STR #undef HASH_THE_HASH