@@ -256,11 +256,9 @@ namespace dxvk {
256256
257257 // We still need to clear caches even if the scene wasn't rendered
258258 m_bufferCache.clear ();
259- m_surfaceMaterialCache.clear ();
260259 m_preCreationSurfaceMaterialMap.clear ();
261- m_surfaceMaterialExtensionCache.clear ();
262260 m_volumeMaterialCache.clear ();
263-
261+
264262 // Clear ReplacementInstances first: their destructors call clear() which
265263 // accesses prims[] to mark entities for GC and clear back-pointers.
266264 // Entities must still be alive at this point.
@@ -278,7 +276,34 @@ namespace dxvk {
278276 // map, not the vectors, so a bulk reset must drop the cache wholesale.
279277 m_accelManager.clear ();
280278
279+ // Instance destruction fires onInstanceDestroyed -> releaseSurfaceMaterial for every
280+ // game-submitted instance, decrementing texture ref counts and feature counts.
281+ // The surface material cache must still be valid here so those releases are not
282+ // silently skipped by the bounds check in releaseSurfaceMaterial.
281283 m_instanceManager.clear ();
284+
285+ // After all instances are destroyed their retain/release pairs must be balanced.
286+ // If any count is non-zero here there is a retain/release mismatch.
287+ if (m_activePOMCount != 0 ) {
288+ Logger::err (str::format (" [RTX] SceneManager::clear: POM count is " , m_activePOMCount, " after instance destruction (retain/release mismatch)" ));
289+ assert (false && " SceneManager::clear: POM count non-zero after instance destruction" );
290+ m_activePOMCount = 0 ;
291+ }
292+ if (m_sssCount != 0 ) {
293+ Logger::err (str::format (" [RTX] SceneManager::clear: SSS count is " , m_sssCount, " after instance destruction (retain/release mismatch)" ));
294+ assert (false && " SceneManager::clear: SSS count non-zero after instance destruction" );
295+ m_sssCount = 0 ;
296+ }
297+ if (m_thinOpaqueCount != 0 ) {
298+ Logger::err (str::format (" [RTX] SceneManager::clear: thin-opaque count is " , m_thinOpaqueCount, " after instance destruction (retain/release mismatch)" ));
299+ assert (false && " SceneManager::clear: thin-opaque count non-zero after instance destruction" );
300+ m_thinOpaqueCount = 0 ;
301+ }
302+ textureManager.assertAllRefCountsZero ();
303+
304+ // Now safe to clear material caches; all ref counts have been released.
305+ m_surfaceMaterialCache.clear ();
306+ m_surfaceMaterialExtensionCache.clear ();
282307 m_lightManager.clear ();
283308 m_graphManager.clear ();
284309 m_rayPortalManager.clear ();
@@ -299,7 +324,7 @@ namespace dxvk {
299324 ScopedCpuProfileZone ();
300325
301326 // BlasEntry GC: remove entries not touched recently.
302- // Only GC entries with no linked instances — instances still reference the BlasEntry
327+ // Only GC entries with no linked instances -- instances still reference the BlasEntry
303328 // for TLAS build, and destroying it would cause a one-frame visibility gap.
304329 if (m_device->getCurrentFrameId () > RtxOptions::numFramesToKeepGeometryData ()) {
305330 const size_t oldestFrame = m_device->getCurrentFrameId () - RtxOptions::numFramesToKeepGeometryData ();
@@ -540,7 +565,6 @@ namespace dxvk {
540565 m_opacityMicromapManager->onFrameEnd ();
541566 }
542567
543- m_activePOMCount = 0 ;
544568 m_startInMediumMaterialIndex = SURFACE_INDEX_INVALID ;
545569 m_fogStartInMediumMaterialIndex_inCache = UINT32_MAX ;
546570 m_startInMediumMaterialIndex_inCache = UINT32_MAX ;
@@ -553,8 +577,6 @@ namespace dxvk {
553577 // Not currently safe to cache these across frames (due to texture indices and rtx options potentially changing)
554578 m_preCreationSurfaceMaterialMap.clear ();
555579
556- m_thinOpaqueMaterialExist = false ;
557- m_sssMaterialExist = false ;
558580
559581 // execute graph updates after all garbage collection is complete (to avoid updating graphs that will just be deleted)
560582 // RtxOptions will still be pending, so any changes to them will apply next frame.
@@ -656,11 +678,11 @@ namespace dxvk {
656678 // Preserve path: L1 means this draw still matches the same ReplacementInstance by identity; replacer reload is
657679 // expected to clear the draw-call tracker (see SceneManager::clear), so we do not re-verify mesh prims or
658680 // activeReplacements pointers every frame. If a path exists where replacements bind without a clear, use dynamic
659- // (drawReplacements) for that transition — drawReplacements already reconciles activeReplacements and prims.
681+ // (drawReplacements) for that transition -- drawReplacements already reconciles activeReplacements and prims.
660682 //
661683 // Static path reuses each prim's BlasEntry::modifiedGeometryData as-is. If another draw earlier this frame
662684 // already entered DrawCallCache::get and re-bound a sibling-topology BlasEntry to its own data (kUpdateBVH),
663- // the cached buffers no longer correspond to this draw — fall back to dynamic so DrawCallCache::get's
685+ // the cached buffers no longer correspond to this draw -- fall back to dynamic so DrawCallCache::get's
664686 // "frameLastTouched skip" allocates a fresh BlasEntry and processSceneObject re-links the instance.
665687 auto blasAlreadyTouchedByOtherDraw = [replacementInstance, currentFrameId]() -> bool {
666688 for (const auto & prim : replacementInstance->prims ) {
@@ -1095,56 +1117,7 @@ namespace dxvk {
10951117
10961118 pBlas->frameLastTouched = m_device->getCurrentFrameId ();
10971119
1098- // Surface material and texture indices are generally stable across frames.
1099- // If textureManager::clear() is called, the texture cache generation will change,
1100- // and the draw calls will take the dynamic path the next frame.
1101- // Refresh texture streaming on the preserve path: fetchNoisyMipCounts clears m_related each
1102- // GC, so we must repeat preserveTexture(TextureRef,...) (not just associate). Opaque subsurface-extension maps are
1103- // not in RtSurfaceMaterial::forEachTextureIndex — include those explicitly. Material graph and
1104- // extension cache are scene-owned; leader stamp resolution stays here (RtxTextureManager::preserveTexture).
11051120 const uint32_t surfaceMatIdx = instance.surface .surfaceMaterialIndex ;
1106- if (surfaceMatIdx < m_surfaceMaterialCache.getTotalCount ()) {
1107- auto & textureManager = m_device->getCommon ()->getTextureManager ();
1108- const RtSurfaceMaterial& surfaceMat = m_surfaceMaterialCache.getObjectTable ()[surfaceMatIdx];
1109- const RtOpaqueSurfaceMaterial* pOpaqueMat = nullptr ;
1110- uint16_t leaderStamp = SAMPLER_FEEDBACK_INVALID ;
1111- if (surfaceMat.getType () == RtSurfaceMaterialType::Opaque) {
1112- pOpaqueMat = &surfaceMat.getOpaqueSurfaceMaterial ();
1113- leaderStamp = pOpaqueMat->getSamplerFeedbackStamp ();
1114- if (leaderStamp == SAMPLER_FEEDBACK_INVALID ) {
1115- const uint32_t albedoIdx = pOpaqueMat->getAlbedoOpacityTextureIndex ();
1116- const auto & textureTable = textureManager.getTextureTable ();
1117- if (albedoIdx < textureTable.size ()) {
1118- const Rc<ManagedTexture>& albedoMt = textureTable[albedoIdx].getManagedTexture ();
1119- if (albedoMt != nullptr ) {
1120- leaderStamp = albedoMt->m_samplerFeedbackStamp ;
1121- }
1122- }
1123- }
1124- }
1125-
1126- const auto touchTexture = [&](uint32_t texIdx) {
1127- textureManager.preserveTexture (texIdx, leaderStamp);
1128- };
1129-
1130- surfaceMat.forEachTextureIndex (touchTexture);
1131-
1132- if (pOpaqueMat != nullptr ) {
1133- // Per-frame aggregate flags/counts that createSurfaceMaterial sets on the
1134- // dynamic path are reset each frame in onFrameEnd. The preserve path skips
1135- // createSurfaceMaterial, so without this call a frame where every POM /
1136- // SSS / thin-opaque draw is preserved would leave the aggregates at their
1137- // reset value and silently disable POM (constants.pomMode is gated on
1138- // getActivePOMCount() > 0) or the SSS pipeline branches.
1139- accumulateOpaqueMaterialAggregates (*pOpaqueMat);
1140-
1141- const uint32_t subsurfaceIdx = pOpaqueMat->getSubsurfaceMaterialIndex ();
1142- if (subsurfaceIdx != SURFACE_INDEX_INVALID &&
1143- subsurfaceIdx < m_surfaceMaterialExtensionCache.getTotalCount ()) {
1144- m_surfaceMaterialExtensionCache.getObjectTable ()[subsurfaceIdx].forEachTextureIndex (touchTexture);
1145- }
1146- }
1147- }
11481121
11491122 // Ray Portal refresh on the preserve path. RayPortalManager::clear() wipes m_rayPortalInfos
11501123 // every frame in endFrame, so processRayPortalData must repopulate the slot for any portal
@@ -1306,10 +1279,16 @@ namespace dxvk {
13061279 }
13071280
13081281 // Create and bind the RT material
1309- const RtSurfaceMaterial& surfaceMaterial = createSurfaceMaterial (*material, drawCall);
1282+ uint32_t newMatIdx = kInvalidMaterialCacheIndex ;
1283+ const RtSurfaceMaterial& surfaceMaterial = createSurfaceMaterial (*material, drawCall, &newMatIdx);
13101284
13111285 if (isFirstUpdateThisFrame) {
1286+ const uint32_t oldMatIdx = instance.surface .surfaceMaterialIndex ;
13121287 m_instanceManager.bindMaterial (instance, surfaceMaterial);
1288+ if (newMatIdx != oldMatIdx) {
1289+ retainSurfaceMaterial (newMatIdx);
1290+ releaseSurfaceMaterial (oldMatIdx);
1291+ }
13131292 }
13141293
13151294 // Update portal
@@ -1318,7 +1297,96 @@ namespace dxvk {
13181297 }
13191298 }
13201299
1300+ void SceneManager::retainSurfaceMaterial (uint32_t matIdx) {
1301+ // Retain is always called with newMatIdx from createSurfaceMaterial, which must return
1302+ // a valid in-bounds index. An out-of-bounds index here indicates a logic error upstream.
1303+ if (matIdx >= m_surfaceMaterialCache.getTotalCount ()) {
1304+ Logger::err (str::format (" [RTX] retainSurfaceMaterial: matIdx " , matIdx,
1305+ " out of bounds (cache size " , m_surfaceMaterialCache.getTotalCount (),
1306+ " ; createSurfaceMaterial returned invalid index" ));
1307+ assert (false && " retainSurfaceMaterial: matIdx out of bounds" );
1308+ return ;
1309+ }
1310+ auto & textureManager = m_device->getCommon ()->getTextureManager ();
1311+ const RtSurfaceMaterial& mat = m_surfaceMaterialCache.getObjectTable ()[matIdx];
1312+ mat.forEachTextureIndex ([&](uint32_t texIdx) {
1313+ textureManager.retainTexture (texIdx);
1314+ });
1315+ if (mat.getType () == RtSurfaceMaterialType::Opaque) {
1316+ const RtOpaqueSurfaceMaterial& opaque = mat.getOpaqueSurfaceMaterial ();
1317+ if (opaque.hasValidDisplacement ()) {
1318+ ++m_activePOMCount;
1319+ }
1320+ const uint32_t subsurfaceIdx = opaque.getSubsurfaceMaterialIndex ();
1321+ if (subsurfaceIdx != SURFACE_INDEX_INVALID &&
1322+ subsurfaceIdx < m_surfaceMaterialExtensionCache.getTotalCount ()) {
1323+ const RtSurfaceMaterial& extMat = m_surfaceMaterialExtensionCache.getObjectTable ()[subsurfaceIdx];
1324+ extMat.forEachTextureIndex ([&](uint32_t texIdx) {
1325+ textureManager.retainTexture (texIdx);
1326+ });
1327+ if (extMat.getType () == RtSurfaceMaterialType::Subsurface) {
1328+ const float radiusScale = extMat.getSubsurfaceMaterial ().getSubsurfaceRadiusScale ();
1329+ if (radiusScale > 0 .0f ) ++m_sssCount;
1330+ else if (radiusScale < 0 .0f ) ++m_thinOpaqueCount;
1331+ }
1332+ }
1333+ }
1334+ }
1335+
1336+ void SceneManager::releaseSurfaceMaterial (uint32_t matIdx) {
1337+ // Out-of-bounds covers instances that were never bound (surfaceMaterialIndex ==
1338+ // kSurfaceInvalidSurfaceMaterialIndex). Renderer-created instances skip
1339+ // onInstanceDestroyedCallback entirely so they never reach here.
1340+ if (matIdx >= m_surfaceMaterialCache.getTotalCount ()) {
1341+ return ;
1342+ }
1343+ auto & textureManager = m_device->getCommon ()->getTextureManager ();
1344+ const RtSurfaceMaterial& mat = m_surfaceMaterialCache.getObjectTable ()[matIdx];
1345+ mat.forEachTextureIndex ([&](uint32_t texIdx) {
1346+ textureManager.releaseTexture (texIdx);
1347+ });
1348+ if (mat.getType () == RtSurfaceMaterialType::Opaque) {
1349+ const RtOpaqueSurfaceMaterial& opaque = mat.getOpaqueSurfaceMaterial ();
1350+ if (opaque.hasValidDisplacement ()) {
1351+ if (m_activePOMCount == 0 ) {
1352+ Logger::err (" [RTX] releaseSurfaceMaterial: POM count underflow (mismatched retain/release)" );
1353+ assert (false && " releaseSurfaceMaterial: POM count underflow" );
1354+ } else {
1355+ --m_activePOMCount;
1356+ }
1357+ }
1358+ const uint32_t subsurfaceIdx = opaque.getSubsurfaceMaterialIndex ();
1359+ if (subsurfaceIdx != SURFACE_INDEX_INVALID &&
1360+ subsurfaceIdx < m_surfaceMaterialExtensionCache.getTotalCount ()) {
1361+ const RtSurfaceMaterial& extMat = m_surfaceMaterialExtensionCache.getObjectTable ()[subsurfaceIdx];
1362+ extMat.forEachTextureIndex ([&](uint32_t texIdx) {
1363+ textureManager.releaseTexture (texIdx);
1364+ });
1365+ if (extMat.getType () == RtSurfaceMaterialType::Subsurface) {
1366+ const float radiusScale = extMat.getSubsurfaceMaterial ().getSubsurfaceRadiusScale ();
1367+ if (radiusScale > 0 .0f ) {
1368+ if (m_sssCount == 0 ) {
1369+ Logger::err (" [RTX] releaseSurfaceMaterial: SSS count underflow (mismatched retain/release)" );
1370+ assert (false && " releaseSurfaceMaterial: SSS count underflow" );
1371+ } else {
1372+ --m_sssCount;
1373+ }
1374+ } else if (radiusScale < 0 .0f ) {
1375+ if (m_thinOpaqueCount == 0 ) {
1376+ Logger::err (" [RTX] releaseSurfaceMaterial: thin-opaque count underflow (mismatched retain/release)" );
1377+ assert (false && " releaseSurfaceMaterial: thin-opaque count underflow" );
1378+ } else {
1379+ --m_thinOpaqueCount;
1380+ }
1381+ }
1382+ }
1383+ }
1384+ }
1385+ }
1386+
13211387 void SceneManager::onInstanceDestroyed (RtInstance& instance) {
1388+ releaseSurfaceMaterial (instance.surface .surfaceMaterialIndex );
1389+
13221390 // Evict from the AccelManager bucket cache to prevent stale pointer ABA issues.
13231391 m_accelManager.removeInstanceFromBucketCache (&instance);
13241392
@@ -1680,8 +1748,6 @@ namespace dxvk {
16801748 secondaryTextureIndex
16811749 };
16821750
1683- accumulateOpaqueMaterialAggregates (opaqueSurfaceMaterial);
1684-
16851751 surfaceMaterial.emplace (opaqueSurfaceMaterial);
16861752 } else if (renderMaterialDataType == MaterialDataType::Translucent) {
16871753 surfaceMaterial.emplace (createTranslucentSurfaceMaterial (renderMaterialData.getTranslucentMaterialData (), samplerIndex, hasTexcoords));
@@ -1718,35 +1784,6 @@ namespace dxvk {
17181784 return m_surfaceMaterialCache.at (index);
17191785 }
17201786
1721- void SceneManager::accumulateOpaqueMaterialAggregates (const RtOpaqueSurfaceMaterial& opaqueMat) {
1722- if (opaqueMat.hasValidDisplacement ()) {
1723- ++m_activePOMCount;
1724- }
1725-
1726- const uint32_t subsurfaceIdx = opaqueMat.getSubsurfaceMaterialIndex ();
1727- if (subsurfaceIdx == SURFACE_INDEX_INVALID ||
1728- subsurfaceIdx >= m_surfaceMaterialExtensionCache.getTotalCount ()) {
1729- return ;
1730- }
1731- const RtSurfaceMaterial& extMat = m_surfaceMaterialExtensionCache.getObjectTable ()[subsurfaceIdx];
1732- if (extMat.getType () != RtSurfaceMaterialType::Subsurface) {
1733- return ;
1734- }
1735- // createSurfaceMaterial's SSS/thin-opaque branch encodes
1736- // isSubsurfaceScatteringDiffusionProfile into the sign of radiusScale:
1737- // true → radiusScale clamped to >= 1e-5f (strictly > 0) → SSS material
1738- // false → radiusScale = -1 (strictly < 0) → thin-opaque material
1739- // (the asserts there enforce the sign, and the GPU side in rtx_materials.h
1740- // branches on the same convention). The sign of radiusScale is therefore the
1741- // cached form of the SSS-vs-thin-opaque selector and can be inspected here
1742- // without re-reading the original MaterialData.
1743- const float radiusScale = extMat.getSubsurfaceMaterial ().getSubsurfaceRadiusScale ();
1744- if (radiusScale > 0 .0f ) {
1745- m_sssMaterialExist = true ;
1746- } else if (radiusScale < 0 .0f ) {
1747- m_thinOpaqueMaterialExist = true ;
1748- }
1749- }
17501787
17511788 RtTranslucentSurfaceMaterial SceneManager::createTranslucentSurfaceMaterial (const TranslucentMaterialData& translucentMaterialData,
17521789 uint32_t samplerIndex,
@@ -2357,6 +2394,7 @@ namespace dxvk {
23572394 m_device->statCounters ().setCtr (DxvkStatCounter::RtxBlasCount, AccelManager::getBlasCount ());
23582395 m_device->statCounters ().setCtr (DxvkStatCounter::RtxBufferCount, m_bufferCache.getActiveCount ());
23592396 m_device->statCounters ().setCtr (DxvkStatCounter::RtxTextureCount, textureManager.getTextureTable ().size ());
2397+ m_device->statCounters ().setCtr (DxvkStatCounter::RtxReplacementTextureCount, textureManager.getActiveReplacementTextures ());
23602398 m_device->statCounters ().setCtr (DxvkStatCounter::RtxInstanceCount, m_instanceManager.getActiveCount ());
23612399 m_device->statCounters ().setCtr (DxvkStatCounter::RtxSurfaceMaterialCount, m_surfaceMaterialCache.getActiveCount ());
23622400 m_device->statCounters ().setCtr (DxvkStatCounter::RtxSurfaceMaterialExtensionCount, m_surfaceMaterialExtensionCache.getActiveCount ());
0 commit comments