@@ -389,6 +389,9 @@ namespace dxvk {
389389
390390 void invokeOnChangeCallback (DxvkDevice* device) const ;
391391
392+ // Returns true if the value was changed
393+ bool clampValue (GenericValue& value);
394+
392395 // Returns true if the value was changed
393396 bool clampValue (ValueType type);
394397
@@ -450,11 +453,21 @@ namespace dxvk {
450453 // Use this for static operations that don't depend on a specific option type
451454 class RtxOptionManager {
452455 public:
453- static void setStartupConfig (const Config& options) { RtxOptionImpl::setStartupConfig (options); }
454- static void setCustomConfig (const Config& options) { RtxOptionImpl::setCustomConfig (options); }
455- static void readOptions (const Config& options) { RtxOptionImpl::readOptions (options); }
456- static void writeOptions (Config& options, bool changedOptionsOnly) { RtxOptionImpl::writeOptions (options, changedOptionsOnly); }
457- static void resetOptions () { RtxOptionImpl::resetOptions (); }
456+ static void setStartupConfig (const Config& options) {
457+ RtxOptionImpl::setStartupConfig (options);
458+ }
459+ static void setCustomConfig (const Config& options) {
460+ RtxOptionImpl::setCustomConfig (options);
461+ }
462+ static void readOptions (const Config& options) {
463+ RtxOptionImpl::readOptions (options);
464+ }
465+ static void writeOptions (Config& options, bool changedOptionsOnly) {
466+ RtxOptionImpl::writeOptions (options, changedOptionsOnly);
467+ }
468+ static void resetOptions () {
469+ RtxOptionImpl::resetOptions ();
470+ }
458471
459472 // Update all RTX options after setStartupConfig() and setCustomConfig() have been called
460473 static void initializeRtxOptions () {
@@ -465,7 +478,7 @@ namespace dxvk {
465478 // WAR: DxvkInstance() and subsequently this is called twice making the doc being re-written
466479 // with RtxOptions already updated from config files below
467480 static bool hasDocumentationBeenWritten = false ;
468-
481+
469482 // Write out to the markdown file before the RtxOptions defaults are updated
470483 // with those from configs
471484 if (!hasDocumentationBeenWritten && env::getEnvVar (" DXVK_DOCUMENTATION_WRITE_RTX_OPTIONS_MD" ) == " 1" ) {
@@ -547,7 +560,7 @@ namespace dxvk {
547560 for (auto & rtxOptionMapEntry : globalRtxOptions) {
548561 RtxOptionImpl& rtxOption = *rtxOptionMapEntry.second .get ();
549562 if (rtxOption.optionLayerValueQueue .begin ()->second .priority == RtxOptionLayer::s_runtimeOptionLayerPriority &&
550- ((rtxOption.flags & (uint32_t )RtxOptionFlags::NoReset) == 0 )) {
563+ ((rtxOption.flags & (uint32_t ) RtxOptionFlags::NoReset) == 0 )) {
551564 // Erase runtime option, so we can enable option layer configs
552565 rtxOption.disableTopLayer ();
553566 rtxOption.markDirty ();
@@ -564,25 +577,56 @@ namespace dxvk {
564577 // Before the first frame is rendered, it also needs to be called at least once during initialization.
565578 // It's currently called twice during init, due to multiple sections that set many Options then immediately use them.
566579 static void applyPendingValues (DxvkDevice* device) {
567- std::unique_lock<std::mutex> lock (RtxOptionImpl::s_updateMutex);
568-
569- auto & dirtyOptions = RtxOptionImpl::getDirtyRtxOptionMap ();
570- // Need a second array so that we can invoke onChange callbacks after updating values and clearing the dirty list.
571- std::vector<RtxOptionImpl*> dirtyOptionsVector;
572- dirtyOptionsVector.reserve (dirtyOptions.size ());
573- {
574- for (auto & rtxOption : dirtyOptions) {
575- rtxOption.second ->resolveValue (rtxOption.second ->resolvedValue , false );
576- dirtyOptionsVector.push_back (rtxOption.second );
580+
581+ constexpr static int32_t maxResolves = 4 ;
582+ int32_t numResolves = 0 ;
583+
584+ // Iteratively resolve the dirty options, invoke callbacks, rinse and repeat until until no
585+ // dirty options are left.
586+ while (numResolves < maxResolves) {
587+ std::unique_lock<std::mutex> lock (RtxOptionImpl::s_updateMutex);
588+
589+ auto & dirtyOptions = RtxOptionImpl::getDirtyRtxOptionMap ();
590+
591+ // Need a second array so that we can invoke onChange callbacks after updating values and clearing the dirty list.
592+ std::vector<RtxOptionImpl*> dirtyOptionsVector;
593+ dirtyOptionsVector.reserve (dirtyOptions.size ());
594+ {
595+ for (auto & rtxOption : dirtyOptions) {
596+ rtxOption.second ->resolveValue (rtxOption.second ->resolvedValue , false );
597+ dirtyOptionsVector.push_back (rtxOption.second );
598+ }
599+ }
600+ dirtyOptions.clear ();
601+ lock.unlock ();
602+
603+ // Invoke onChange callbacks after promoting all the values
604+ for (RtxOptionImpl* rtxOption : dirtyOptionsVector) {
605+ rtxOption->invokeOnChangeCallback (device);
606+ }
607+
608+ numResolves++;
609+
610+ // If the callbacks didn't generate any dirtied options, bail
611+ if (dirtyOptions.empty ()) {
612+ break ;
577613 }
578614 }
579- dirtyOptions.clear ();
580- lock.unlock ();
581615
582- // Invoke onChange callbacks after promoting all the values, so that newly set values will be updated at the end of the next frame
583- for (RtxOptionImpl* rtxOption : dirtyOptionsVector) {
584- rtxOption->invokeOnChangeCallback (device);
616+ #if RTX_OPTION_DEBUG_LOGGING
617+ const bool unresolvedChanges = numResolves == maxResolves && !dirtyOptions.empty ();
618+ if (unresolvedChanges) {
619+ auto & dirtyOptions = RtxOptionImpl::getDirtyRtxOptionMap ();
620+
621+ Logger::warn (str::format (" Dirty RtxOptions remaining after " , maxResolves, " passes of resolving callbacks, suggesting a cyclic dependency." ));
622+ for (auto & rtxOption : dirtyOptions) {
623+ Logger::warn (str::format (" - Abandoned resolve of option " , rtxOption.second ->name ));
624+ }
585625 }
626+ #endif
627+
628+ // Don't let dirty options persist across frames and explode the dirty option processing in the case of circular dependencies
629+ RtxOptionImpl::getDirtyRtxOptionMap ().clear ();
586630 }
587631 };
588632
@@ -717,8 +761,8 @@ namespace dxvk {
717761 template <typename = std::enable_if_t <isClampable()>>
718762 void setMinValue (const T& v) {
719763 std::lock_guard<std::mutex> lock (RtxOptionImpl::s_updateMutex);
720- setMinMaxValueHelper (v, pImpl-> minValue );
721- bool changed = pImpl->clampValue (RtxOptionImpl::ValueType::PendingValue );
764+
765+ bool changed = setMinMaxValueHelper (v, pImpl->minValue );
722766 if (changed) {
723767 pImpl->markDirty ();
724768 }
@@ -733,8 +777,7 @@ namespace dxvk {
733777 template <typename = std::enable_if_t <isClampable()>>
734778 void setMaxValue (const T& v) {
735779 std::lock_guard<std::mutex> lock (RtxOptionImpl::s_updateMutex);
736- setMinMaxValueHelper (v, pImpl->maxValue );
737- bool changed = pImpl->clampValue (RtxOptionImpl::ValueType::PendingValue);
780+ bool changed = setMinMaxValueHelper (v, pImpl->maxValue );
738781 if (changed) {
739782 pImpl->markDirty ();
740783 }
@@ -806,7 +849,7 @@ namespace dxvk {
806849 assert (RtxOptionImpl::s_isInitialized && " Trying to access an RtxOption before the config files have been loaded." );
807850 BasicType* valuePtr = getValuePtr<BasicType>(RtxOptionImpl::ValueType::PendingValue);
808851 *valuePtr = v;
809- pImpl-> clampValue (RtxOptionImpl::ValueType::PendingValue);
852+
810853 pImpl->markDirty ();
811854 }
812855
@@ -816,7 +859,7 @@ namespace dxvk {
816859 assert (RtxOptionImpl::s_isInitialized && " Trying to access an RtxOption before the config files have been loaded." );
817860 ClassType* valuePtr = getValuePtr<ClassType>(RtxOptionImpl::ValueType::PendingValue);
818861 *valuePtr = v;
819- pImpl-> clampValue (RtxOptionImpl::ValueType::PendingValue);
862+
820863 pImpl->markDirty ();
821864 }
822865
@@ -906,17 +949,24 @@ namespace dxvk {
906949 }
907950
908951 // Helper methods to reduce code duplication between numeric and vector types
909- void setMinMaxValueHelper (const T& v, std::optional<GenericValue>& targetValue) {
952+ bool setMinMaxValueHelper (const T& v, std::optional<GenericValue>& targetValue) {
953+ bool changed = false ;
954+
910955 if constexpr (std::is_pod_v<T>) {
911956 // For POD types (int, float, etc.), store directly in the value field
912957 GenericValue gv;
913958 if constexpr (std::is_same_v<T, float >) {
959+ changed = targetValue.has_value () ? targetValue.value ().f != v : true ;
914960 gv.f = v;
915961 } else {
916962 // For bool, int, and other POD types, use the value field
917- gv.value = static_cast <int64_t >(v);
963+ uint64_t value = static_cast <uint64_t >(v);
964+ changed = targetValue.has_value () ? targetValue.value ().value != value : true ;
965+ gv.value = value;
918966 }
967+
919968 targetValue = std::optional<GenericValue>(gv);
969+
920970 } else {
921971 // For non-POD types (vectors, etc.), store as pointer
922972 if (!targetValue.has_value ()) {
@@ -925,8 +975,13 @@ namespace dxvk {
925975 // RtxOptionImpl object is never destroyed, and follows the pattern used in the constructor.
926976 targetValue.value ().pointer = new T ();
927977 }
928- *reinterpret_cast <T*>(targetValue.value ().pointer ) = v;
978+
979+ T* valuePtr { reinterpret_cast <T*>(targetValue.value ().pointer ) };
980+ changed = *valuePtr != v;
981+ *valuePtr = v;
929982 }
983+
984+ return changed;
930985 }
931986
932987 std::optional<T> getMinMaxValueHelper (const std::optional<GenericValue>& sourceValue) const {
0 commit comments