From 2b389304471bdca81d5cc98be8a7525f65f9738a Mon Sep 17 00:00:00 2001 From: Haluk Oral Date: Tue, 28 Jul 2026 11:15:06 +0300 Subject: [PATCH 1/2] SharcUpdateHit/SharcUpdateMiss: skip HASH_GRID_INVALID_CACHE_INDEX before the cache-index mask When a hash-grid bucket is full, HashGridInsertEntry returns HASH_GRID_INVALID_CACHE_INDEX and SharcUpdateHit stores that sentinel into sharcState.cacheIndices verbatim. The propagation loops in SharcUpdateHit and SharcUpdateMiss then run the stored index through an unconditional '& SHARC_CACHE_INDEX_BIT_MASK' (compiled in whenever SHARC_ENABLE_RESPONSIVE_LIGHTING is defined, regardless of the runtime toggle), which rewrites 0xFFFFFFFF to SHARC_CACHE_INDEX_BIT_MASK. That forged value passes SharcAddVoxelData's INVALID guard and the interlocked adds land ~2 GiB past the accumulation buffer (with the default 26-bit index layout and the 32-byte SH accumulation stride), losing the device once the table gets dense enough for buckets to fill. Verified against a driver-reported fault address: the faulting page was exactly accumulationBase + SHARC_CACHE_INDEX_BIT_MASK * 32 rounded to page granularity, bit-identical across independent device losses. Skip the sentinel at the top of both loops, before the responsive offset code: a post-mask compare would not be enough because SharcGetResponsiveIndexOffset can shift the sentinel first when responsive lighting is enabled at runtime. Fixes #4 --- include/SharcCommon.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/include/SharcCommon.h b/include/SharcCommon.h index 5531961..9a3210c 100644 --- a/include/SharcCommon.h +++ b/include/SharcCommon.h @@ -564,6 +564,12 @@ void SharcUpdateMiss(in SharcParameters sharcParameters, in SharcState sharcStat for (int i = 0; i < sharcState.pathLength; ++i) { HashGridIndex hashGridIndex = sharcState.cacheIndices[i]; + // A full bucket makes HashGridInsertEntry return HASH_GRID_INVALID_CACHE_INDEX and SharcUpdateHit stores + // that sentinel into cacheIndices verbatim. It must be skipped before the responsive offset/mask block: + // masking it unconditionally rewrites it to SHARC_CACHE_INDEX_BIT_MASK, which is no longer INVALID, so + // SharcAddVoxelData's guard passes and the atomics land far past the end of the accumulation buffer + if (hashGridIndex == HASH_GRID_INVALID_CACHE_INDEX) + continue; bool isNewSample = false; #if SHARC_ENABLE_RESPONSIVE_LIGHTING if (isResponsiveLighting) @@ -659,6 +665,9 @@ bool SharcUpdateHit(in SharcParameters sharcParameters, inout SharcState sharcSt for (i = 0; i < sharcState.pathLength; ++i) { HashGridIndex tempHashGridIndex = sharcState.cacheIndices[i]; + // Skip full-bucket sentinels before the responsive offset/mask block — same reasoning as SharcUpdateMiss + if (tempHashGridIndex == HASH_GRID_INVALID_CACHE_INDEX) + continue; bool isNewSample = false; #if SHARC_ENABLE_RESPONSIVE_LIGHTING if (responsiveCacheIndex != HASH_GRID_INVALID_CACHE_INDEX) From 247520e2fe0fa56b9f8f3c4a6b6eae53e72a5427 Mon Sep 17 00:00:00 2001 From: Haluk Oral Date: Tue, 28 Jul 2026 11:15:19 +0300 Subject: [PATCH 2/2] SharcResolveEntry: clamp the recovery linear probe to the table capacity The sampleNumPrev == 0 recovery path probes forward blindly by SHARC_LINEAR_PROBE_WINDOW_SIZE entries. An application is allowed to allocate exactly 'capacity' entries per buffer (as the samples do), so for entries in the last probe window this loop reads past the end of both the hash-entry and resolved buffers. With buffer-device-address access there is no robustness backstop, and once the high table slots become occupied the read crosses the allocation's last page and the device is lost. Before those slots fill up the loop breaks early on empty keys (HASH_GRID_LIMIT_EMPTY_SLOTS), which is why the bug hides in light scenes. HashGridGetBaseSlot keeps normal bucket probes in range; this entry-relative probe needs its own clamp. Fixes #5 --- include/SharcCommon.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/include/SharcCommon.h b/include/SharcCommon.h index 9a3210c..aee0cdc 100644 --- a/include/SharcCommon.h +++ b/include/SharcCommon.h @@ -901,10 +901,13 @@ void SharcResolveEntry(uint entryIndex, SharcParameters sharcParameters, SharcRe // Performs hash map lookup to find existing entries in case previous insertions // encountered collisions and a different slot was assigned. - // Uses a fixed-size linear probe window + // Uses a fixed-size linear probe window, clamped to the table capacity: an application is allowed to + // allocate exactly `capacity` entries, so entries in the last window cannot blindly look forward — + // doing so reads past the end of the hash-entry and resolved buffers once those slots become occupied if (sampleNumPrev == 0) { - for (uint i = entryIndex + 1; i < entryIndex + 1 + SHARC_LINEAR_PROBE_WINDOW_SIZE; ++i) + uint probeEnd = min(entryIndex + 1 + SHARC_LINEAR_PROBE_WINDOW_SIZE, sharcParameters.hashGridData.capacity); + for (uint i = entryIndex + 1; i < probeEnd; ++i) { HashGridKey hashKeyOld = BUFFER_AT_OFFSET(sharcParameters.hashGridData.hashEntriesBuffer, i); if (hashKeyOld == hashGridKey)