Skip to content

Created concepts for samplers, added quotient_and_pdf variants to satisfy the concepts#1001

Open
karimsayedre wants to merge 39 commits intomasterfrom
sampler-concepts
Open

Created concepts for samplers, added quotient_and_pdf variants to satisfy the concepts#1001
karimsayedre wants to merge 39 commits intomasterfrom
sampler-concepts

Conversation

@karimsayedre
Copy link
Contributor

@karimsayedre karimsayedre commented Feb 18, 2026

Examples PR

Notes:

  • The quotient_and_pdf() methods in UniformHemisphere, UniformSphere, ProjectedHemisphere, and ProjectedSphere shadow the struct type sampling::quotient_and_pdf<Q, P> from quotient_and_pdf.hlsl. DXC can't resolve the return type because the method name takes precedence over the struct name during lookup. Fixed by fully qualifying with ::nbl::hlsl::sampling::quotient_and_pdf<U, T>.
  • Obv. there's some refactoring to be done to satisfy all the concepts, so for not Basic (Level1) samplers are concept tested

…concepts

- Move codomain_and_*Pdf and domain_and_*Pdf structs into their own warp_and_pdf.hlsl header
- Keeping quotient_and_pdf.hlsl focused on importance sampling quotients for BxDFs
- Add SampleWithPDF, SampleWithRcpPDF, and SampleWithDensity concepts to validate sample types
- Used concept composition (NBL_CONCEPT_REQ_TYPE_ALIAS_CONCEPT) to build ResamplableSampler on TractableSampler and BijectiveSampler on ResamplableSampler
vector3_type tri_vertices[3];
scalar_type triCosC;
scalar_type triCscB;
scalar_type triCscC;
Copy link
Member

@devshgraphicsprogramming devshgraphicsprogramming Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

take a template whether you want the SphericalTriangle to be bijective (have a generateInverse method) and implement the Bijective in terms of Resamplable & Tractable Spherical Trangle + the triCscC member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems that triCscB doesn't need to be kept #1001 (comment)

# Conflicts:
#	examples_tests
#	include/nbl/builtin/hlsl/shapes/spherical_triangle.hlsl
// Builds a normalized cumulative histogram from an array of non-negative weights.
// Output has N-1 entries (last bucket implicitly 1.0).
template<typename T>
void computeNormalizedCumulativeHistogram(const T* weights, uint32_t N, T* outCumProb)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe take a span<T> weights then the N is implicit, also do a special path for size()<2 because right now it will crash

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved, alias table too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea alias table too

Comment on lines 167 to +169
{
impl::bound_t<Accessor,Comparator> implementation = impl::bound_t<Accessor,Comparator>::setup(begin,end,value,comp);
return implementation(accessor);
const uint32_t retval = implementation(accessor);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING impl::bound_t<Accessor,Comparator>::setup takes comp as const

need to change the impl and provide it brom a NBL_REF_ARG through the methods of impl::bound_t

Comment on lines 155 to +157
static weight_type backwardWeight(const codomain_type sample)
{
return backwardPdf(sample);
return T(0.5) * hemisphere_t::backwardWeight(sample);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remember sample is a HLSL keyword

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

Comment on lines +118 to +112
T z = T(1.0) - T(2.0) * _sample.x;
T r = hlsl::sqrt<T>(hlsl::max<T>(T(0.0), T(1.0) - z * z));
T phi = T(2.0) * numbers::pi<T> * _sample.y;
return vector_t3(r * hlsl::cos<T>(phi), r * hlsl::sin<T>(phi), z);
// Map _sample.x from [0,1] into hemisphere sample + sign flip:
// upper hemisphere when _sample.x < 0.5, lower when >= 0.5
const bool chooseLower = _sample.x >= T(0.5);
const T hemiX = chooseLower ? (T(2.0) * _sample.x - T(1.0)) : (T(2.0) * _sample.x);
vector_t3 retval = hemisphere_t::__generate(vector_t2(hemiX, _sample.y));
retval.z = chooseLower ? (-retval.z) : retval.z;
return retval;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ugh no the code is definitely worse here with all the conditionals, I just need a shared common function between both that takes a z coord externally

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

Comment on lines +35 to +38
NBL_PRIMARY_REQUIRES(
concepts::accessors::GenericReadAccessor<ProbabilityAccessor, T, Codomain> &&
concepts::accessors::GenericReadAccessor<AliasIndexAccessor, Codomain, Codomain> &&
concepts::accessors::GenericReadAccessor<PdfAccessor, T, Codomain>)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

require that Domain is an UnsignedIntegralScalar (or whatever the concept is called)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codomain you mean, resolved

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah codomain, sorry

Comment on lines 47 to +55
math::sincos<scalar_type>(scalar_type(2.0) * numbers::pi<scalar_type> * u.y - numbers::pi<scalar_type>, sinPhi, cosPhi);
const codomain_type outPos = vector2_type(cosPhi, sinPhi) * nbl::hlsl::sqrt(scalar_type(-2.0) * nbl::hlsl::log(u.x)) * stddev;
cache.pdf = backwardPdf(outPos);
cache.u_x = u.x;
return outPos;
}

density_type forwardPdf(const cache_type cache)
{
return cache.pdf;
return halfRcpStddev2 * numbers::inv_pi<scalar_type> * cache.u_x;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok so you check my math from the comment and it was wrong ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, It's actually correct, I brute forced it in python and it seems I miswrote stuff, it' much simpler now, resolved

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we don't really have a test for this did you plot or test somehow the new version works ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes I tested it, this python stuff is great!

Comment on lines 57 to 58
phi += hlsl::mix(T(0.0), twopi, phi < T(0.0));
return vector_t2(_sample.z, phi / twopi);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more accurate to divide phi by twopi first and add 1.0 if it was negative

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also use a select

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

vector_t2 hemiUV = hemisphere_t::__generateInverse(hemiSample);
// Recover _sample.x: upper hemisphere maps [0,0.5], lower maps [0.5,1]
hemiUV.x = isLower ? (hemiUV.x * T(0.5) + T(0.5)) : (hemiUV.x * T(0.5));
return hemiUV;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just overwrite the x coordinate, previous version was branchless

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

Comment on lines +67 to +69
cache_type c;
c.L_z = L.z;
return forwardPdf(c);
Copy link
Member

@devshgraphicsprogramming devshgraphicsprogramming Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert that L_z > 0

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved, in generate too

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generate spits it out, so should be no issue for it or forwardPdf

Its just special for backwardPdf because we need to clearly state that its invalid to call with such a parameter (since people can call backwardPdf with literally whatever)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should probably assert everywhere then? (all other samplers? some of them?)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only this one because its Hemisphere and the UniformHemisphere, because you take an L in R^3 and a negative L.z can be fed into it by accident if someone doesn't read the docs

Comment on lines +112 to 117
static domain_type generateInverse(const codomain_type L)
{
// NOTE: incomplete information to recover exact z component; we only know which hemisphere L came from,
// so we return a canonical value (0.0 for upper, 1.0 for lower) that round-trips correctly through __generate
return vector_t3(hemisphere_t::__generateInverse(L), hlsl::mix(T(1.0), T(0.0), L.z > T(0.0)));
}

static domain_type generateInverse(const codomain_type L, NBL_REF_ARG(cache_type) cache)
{
cache.pdf = __pdf(L.z);
return __generateInverse(L);
}

static T __pdf(T L_z)
{
return T(0.5) * hemisphere_t::__pdf(hlsl::abs(L_z));
}

static scalar_type pdf(T L_z)
{
return __pdf(L_z);
return vector_t3(hemisphere_t::generateInverse(L), hlsl::mix(T(1.0), T(0.0), L.z > T(0.0)));
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not bijective you can't tell the eact value of Z before it went in

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

Comment on lines 97 to 102
const bool chooseLower = _sample.z > T(0.5);
retval.z = chooseLower ? (-retval.z) : retval.z;
if (chooseLower)
_sample.z -= T(0.5);
_sample.z *= T(2.0);
return retval;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if

const scalar_type tmp = _sample.z -  scalar_type(0.5);
retval.z *= sign(tmp);
if (tmp)
   _sample.z = tmp;
_sample.z *= 2.0;

would be faster

Comment on lines +19 to +39
namespace concepts
{

// clang-format off
#define NBL_CONCEPT_NAME CumulativeProbabilityAccessor
#define NBL_CONCEPT_TPLT_PRM_KINDS (typename)(typename)
#define NBL_CONCEPT_TPLT_PRM_NAMES (T)(Scalar)
#define NBL_CONCEPT_PARAM_0 (accessor, T)
#define NBL_CONCEPT_PARAM_1 (index, uint32_t)
NBL_CONCEPT_BEGIN(2)
#define accessor NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_0
#define index NBL_CONCEPT_PARAM_T NBL_CONCEPT_PARAM_1
NBL_CONCEPT_END(
((NBL_CONCEPT_REQ_TYPE)(T::value_type))
((NBL_CONCEPT_REQ_EXPR_RET_TYPE)((accessor[index]), ::nbl::hlsl::is_same_v, Scalar)));
#undef index
#undef accessor
#include <nbl/builtin/hlsl/concepts/__end.hlsl>
// clang-format on

} // namespace concepts

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why can't you use the existing Generic Read accessor ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

Comment on lines +68 to 85
// Stateful comparator that tracks the CDF values seen during binary search.
// upper_bound uses lower_to_upper_comparator_transform_t which calls !comp(rhs, lhs),
// so our operator() receives (value=u, rhs=cumProb[testPoint]).
struct CdfComparator
{
bool operator()(const density_type value, const density_type rhs)
{
const bool retval = value < rhs;
if (retval)
upperBound = rhs;
else
oneBefore = rhs;
return retval;
}

density_type oneBefore;
density_type upperBound;
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw local structs are 100% okay in HLSL, and you can literally put its definition inside the generate so it doesn't leak :D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

Comment on lines +126 to +128
const density_type cur = (v < storedCount) ? cumProbAccessor[v] : density_type(1.0);
const density_type prev = (v > 0u) ? cumProbAccessor[v - 1u] : density_type(0.0);
return cur - prev;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without hlsl::select the ? wont compile to a cmov, because in C++ and HLSL the ? operator shortcircuits

NBL_CONSTEXPR_FUNC ResultType select(Condition condition, ResultType object1, ResultType object2)

but you can't pipe the memory accesses into select because you'll get OOB access

so you should write since you're going to have true if-statements anyway

density_type retval = 1.f;
if (v<storedCount)
   cumProbAccessor.get(v,retval); 
if (v)
{
   density_type prev;
   cumProbAccessor.get(v-1,prev);
   retval -= prev;
}

Comment on lines 69 to 70
if (pyramidAngles())
return 0.f;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead check for the solidAngle being 0 or below a threshold

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then you can fold pyramidAngles into the create

Comment on lines 103 to 109
const scalar_type u = subTriSolidAngleRatio > numeric_limits<scalar_type>::min ? subTriSolidAngleRatio : 0.0;

const scalar_type sinBC_s_product = sinB_ * sinC_;

// 1 ULP below 1.0, ensures (1.0 - cosBC_s) is strictly positive in float
const scalar_type one_below_one = bit_cast<scalar_type>(bit_cast<uint_type>(scalar_type(1)) - uint_type(1));
const scalar_type one_below_one = ieee754::nextTowardZero(scalar_type(1.0));
const scalar_type cosBC_s = sinBC_s_product > numeric_limits<scalar_type>::min ? (cosA + cosB_ * cosC_) / sinBC_s_product : triCosC;
const scalar_type v_denom = scalar_type(1.0) - (cosBC_s < one_below_one ? cosBC_s : triCosC);
Copy link
Member

@devshgraphicsprogramming devshgraphicsprogramming Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace the ?s with hlsl::select

Comment on lines +86 to +92
return backwardPdf(L);
cache_type cache;
// cache.bilinearCache; // unused
// approximate abs(cos_theta) via bilinear interpolation at the inverse-mapped point
const vector2_type u = sphtri.generateInverse(L);
cache.abs_cos_theta = bilinearPatch.backwardWeight(u);
return forwardWeight(cache);
Copy link
Member

@devshgraphicsprogramming devshgraphicsprogramming Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you absolutely don't want to call generateInverse here!

whole function should be

cache.abs_cos_theta = math::conditionalAbsOrMax(receiverWasBSDF,dot(receiverNormal,L),minimumProjSolidAngle);
return forwardWeight(cache);

whoops seems you need to store receiverWasBSDF after all

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but as we've already established the Projected Solid Angle MIS weight is actually useless (to be confirmed by testing later), so leave a comment we shouldn't store receiver and rcpProj angle as members and will probably use a mix of just clampedNdotL and rcpSolidAngle

Copy link
Member

@devshgraphicsprogramming devshgraphicsprogramming Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a template argument whether to use the Pdf as the weight (default would be to use the PDF, cause the fake MIS weight has been shown to be crap so far)

Copy link
Contributor Author

@karimsayedre karimsayedre Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved will obv. need work later

Comment on lines 79 to 83
density_type backwardPdf(const vector3_type L)
{
const density_type pdf = sphtri.backwardPdf(L);
typename SphericalTriangle<scalar_type>::cache_type dummyCache;
const vector2_type u = sphtri.generateInverse(L, dummyCache);
Bilinear<scalar_type> bilinear = computeBilinearPatch();
return pdf * bilinear.backwardPdf(u);
const vector2_type u = sphtri.generateInverse(L);
return sphtri.rcpSolidAngle * bilinearPatch.backwardPdf(u);
}
Copy link
Member

@devshgraphicsprogramming devshgraphicsprogramming Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you don't need a backwardPdf function, we won't be composing the warp further

but what you have now, you can move to backwardWeight for the WeightIsPdf template option because I want a toggle between fake MIS weight and true PDF https://github.com/Devsh-Graphics-Programming/Nabla/pull/1001/changes/254404b5cfa69581820833c5886dee5835a0bb42..edc3c3ed47d09ccf78eb3379fe7b7905c54495a4#r2988401827

Comment on lines -79 to +84
sampling::quotient_and_pdf<monochrome_type, scalar_type> qp;
scalar_type p;
NBL_IF_CONSTEXPR (IsBSDF)
qp = sampling::ProjectedSphere<scalar_type>::template quotientAndPdf(_sample.getNdotL(_clamp));
p = sampling::ProjectedSphere<scalar_type>::backwardPdf(vector<scalar_type, 3>(0, 0, _sample.getNdotL(_clamp)));
else
qp = sampling::ProjectedHemisphere<scalar_type>::template quotientAndPdf(_sample.getNdotL(_clamp));
return quotient_pdf_type::create(qp.quotient()[0], qp.pdf());
p = sampling::ProjectedHemisphere<scalar_type>::backwardPdf(vector<scalar_type, 3>(0, 0, _sample.getNdotL(_clamp)));
return quotient_pdf_type::create(scalar_type(1.0), p);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are you using bakwardPdf for a quotient function!? should be forwardPdf

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants