diff --git a/devices/rtx/device/gpu/gpu_objects.h b/devices/rtx/device/gpu/gpu_objects.h index 88a5dc17c..1fc0a6bbe 100644 --- a/devices/rtx/device/gpu/gpu_objects.h +++ b/devices/rtx/device/gpu/gpu_objects.h @@ -833,12 +833,18 @@ struct WorldGPUData // Power-proportional Light Pick (built in World::buildInstanceLightGPUData). // Normalized cumulative Pick Power over lightInstances (length - // numLightInstances, last entry == 1); pick a slot with inverseSampleCDF. The - // slot's discrete pick probability is lightPickCdf[i]-lightPickCdf[i-1]. - // Double so a dim light's mass (below float epsilon of the total) is - // preserved as a nonzero interval — else it is unselectable while the - // hit-side pNee is still positive, biasing the deposit's MIS weight. + // numLightInstances, last entry == 1); pick a slot with inverseSampleCDF. + // Double so a dim light's mass (below float epsilon of the total) survives the + // adjacent-difference of two ≈1.0 CDF values — else it is unselectable while + // the hit-side pNee is still positive, biasing the deposit's MIS weight. (The + // 24-bit pick sample floors selection at ~2^-24 regardless; the double CDF only + // widens which dim slots the differencing can resolve, not which are reachable.) const double *lightPickCdf; + // Per-slot discrete pick probability power_i/total (length numLightInstances), + // precomputed host-side. Read this directly instead of differencing adjacent + // lightPickCdf entries: a standalone float represents a selected dim light's + // mass accurately, where the adjacent-difference of two ≈1.0 values would not. + const float *lightPickDelta; float totalLightPower; // sum of un-normalized instance Pick Powers float hdriPower; // subset sum over HDRI instances (env-MIS pick probability) float sceneRadius; // bounding-sphere radius, for the ambient term's power diff --git a/devices/rtx/device/renderer/Interactive_ptx.cu b/devices/rtx/device/renderer/Interactive_ptx.cu index 173da0b95..f53d64368 100644 --- a/devices/rtx/device/renderer/Interactive_ptx.cu +++ b/devices/rtx/device/renderer/Interactive_ptx.cu @@ -184,8 +184,9 @@ struct InteractiveShadingPolicy idx = glm::min(size_t(detail::inverseSampleCDF( world.lightPickCdf, int(numLights), u)), numLights - 1); - const float lo = idx > 0 ? world.lightPickCdf[idx - 1] : 0.0f; - pPick = world.lightPickCdf[idx] - lo; + // Precomputed per-slot mass (power_i/total). Read raw: this pick pool + // has no ambient stratum, unlike Quality's instancePickProbability. + pPick = world.lightPickDelta[idx]; } else { idx = glm::min(size_t(u * float(numLights)), numLights - 1); pPick = 1.0f / float(numLights); diff --git a/devices/rtx/device/renderer/Quality_ptx.cu b/devices/rtx/device/renderer/Quality_ptx.cu index 75509671a..7dcaa3e1c 100644 --- a/devices/rtx/device/renderer/Quality_ptx.cu +++ b/devices/rtx/device/renderer/Quality_ptx.cu @@ -169,19 +169,17 @@ VISRTX_DEVICE size_t pickLightInstance(const WorldGPUData &world, float u) } // Discrete probability that pickLightInstance selected `idx`, folded with the -// ambient stratum so P(pick) sums to 1 across every pick candidate. The CDF is -// normalized by totalLightPower, so its per-slot delta is -// power_i/totalLightPower. +// ambient stratum so P(pick) sums to 1 across every pick candidate. lightPickDelta +// holds power_i normalized by the double cumulative total, so folding in +// totalLightPower/totalPower reweights it onto the ambient-inclusive partition. VISRTX_DEVICE float instancePickProbability( const WorldGPUData &world, size_t idx, float totalPower) { - // Double delta: the CDF preserves masses below float epsilon, so the delta of - // adjacent entries must be differenced in double or a dim light's interval - // collapses to zero here even though it is selectable. - const double lo = idx > 0 ? world.lightPickCdf[idx - 1] : 0.0; - const double conditional = world.lightPickCdf[idx] - lo; - return float( - conditional * double(world.totalLightPower) / double(totalPower)); + // Read the precomputed per-slot mass (power_i/total): a dim light's interval, + // stored directly as float, survives here — differencing adjacent ≈1.0 CDF + // entries at float would collapse it even though the double CDF made it + // selectable. + return world.lightPickDelta[idx] * world.totalLightPower / totalPower; } // Aggregate probability that the Light Pick lands on the HDRI environment. Both diff --git a/devices/rtx/device/world/World.cpp b/devices/rtx/device/world/World.cpp index 4ad294d28..5417218eb 100644 --- a/devices/rtx/device/world/World.cpp +++ b/devices/rtx/device/world/World.cpp @@ -217,6 +217,7 @@ WorldGPUData World::gpuData() const retval.numHdriLightInstances = m_instanceHdriLightGPUData.size(); retval.lightPickCdf = m_lightPickCdf.dataDevice(); + retval.lightPickDelta = m_lightPickDelta.dataDevice(); retval.totalLightPower = m_totalLightPower; retval.hdriPower = m_hdriPower; retval.sceneRadius = m_sceneRadius; @@ -592,10 +593,11 @@ void World::buildInstanceLightGPUData() geometryLights, totalHdriLights); - // Allocate both arrays + // Allocate the instance-data, HDRI, and pick arrays. m_instanceLightGPUData.resize(totalLights); m_instanceHdriLightGPUData.resize(totalHdriLights); m_lightPickCdf.resize(totalLights); + m_lightPickDelta.resize(totalLights); // Bounding-sphere radius over the committed scene, sizing the infinite // lights' Pick Power. Fall back to unit radius so an empty scene still @@ -626,6 +628,7 @@ void World::buildInstanceLightGPUData() // Filled with each instance's raw Pick Power, then normalized into the // cumulative CDF in place once the total is known. auto *pickCdf = m_lightPickCdf.dataHost(); + auto *pickDelta = m_lightPickDelta.dataHost(); m_totalLightPower = 0.0f; m_hdriPower = 0.0f; @@ -700,6 +703,9 @@ void World::buildInstanceLightGPUData() // stays positive — bias. The double total makes the last entry exactly 1.0 // and preserves those masses. A zero total (every light dark) leaves the CDF // unused: uniform pick. + // All-dark: pickDelta is zeroed here and pickCdf stays at its zero raw powers + // (never renormalized), so the uniform-pick fallback reads neither. + std::fill(pickDelta, pickDelta + totalLights, 0.0f); if (m_totalLightPower > 0.0f) { double total = 0.0; for (size_t i = 0; i < totalLights; ++i) @@ -707,6 +713,10 @@ void World::buildInstanceLightGPUData() const double invTotal = total > 0.0 ? 1.0 / total : 0.0; double cumulative = 0.0; for (size_t i = 0; i < totalLights; ++i) { + // delta from the raw power (still in pickCdf[i]) before it is overwritten + // by the cumulative value; storing the mass directly as float avoids the + // adjacent-difference of two ≈1.0 doubles the kernel used to do. + pickDelta[i] = float(pickCdf[i] * invTotal); cumulative += pickCdf[i]; pickCdf[i] = cumulative * invTotal; } @@ -715,6 +725,7 @@ void World::buildInstanceLightGPUData() m_instanceLightGPUData.upload(); m_instanceHdriLightGPUData.upload(); m_lightPickCdf.upload(); + m_lightPickDelta.upload(); } } // namespace visrtx diff --git a/devices/rtx/device/world/World.h b/devices/rtx/device/world/World.h index 8a9194870..536302efe 100644 --- a/devices/rtx/device/world/World.h +++ b/devices/rtx/device/world/World.h @@ -116,6 +116,8 @@ struct World : public Object // Power-proportional Light Pick, rebuilt with the light instances. HostDeviceArray m_lightPickCdf; + // Per-slot discrete pick probability (power_i/total), parallel to m_lightPickCdf. + HostDeviceArray m_lightPickDelta; float m_totalLightPower{0.f}; float m_hdriPower{0.f}; float m_sceneRadius{0.f};