Skip to content

Commit 52da085

Browse files
committed
Merge branch 'REMIX-4728-clamp-at-resolve' into 'main'
[REMIX-4728][REMIX-4729] Moving all option clamping behavior after all relevant option layers are composited. Additionally, all callbacks resulting from option changes are handled within the same frame instead of in subsequent frames. Closes REMIX-4728 See merge request lightspeedrtx/dxvk-remix-nv!1803
2 parents 6beec7d + 82feed2 commit 52da085

File tree

2 files changed

+97
-35
lines changed

2 files changed

+97
-35
lines changed

src/dxvk/rtx_render/rtx_option.cpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -365,8 +365,7 @@ namespace dxvk {
365365
}
366366
}
367367

368-
bool RtxOptionImpl::clampValue(ValueType valueType) {
369-
auto& value = getGenericValue(valueType);
368+
bool RtxOptionImpl::clampValue(GenericValue& value) {
370369
bool changed = false;
371370

372371
switch (type) {
@@ -431,6 +430,11 @@ namespace dxvk {
431430
return changed;
432431
}
433432

433+
bool RtxOptionImpl::clampValue(ValueType valueType) {
434+
auto& value = const_cast<GenericValue&>(getGenericValue(valueType));
435+
return clampValue(value);
436+
}
437+
434438
void RtxOptionImpl::readValue(const Config& options, const std::string& fullName, GenericValue& value) {
435439
const char* env = environment == nullptr || strlen(environment) == 0 ? nullptr : environment;
436440

@@ -481,8 +485,6 @@ namespace dxvk {
481485
auto& value = getGenericValue(valueType);
482486
readValue(options, fullName, value);
483487

484-
clampValue(valueType);
485-
486488
if (valueType == ValueType::PendingValue) {
487489
// If reading into the pending value, need to mark the option as dirty so it gets resolved to the value at the end of the frame.
488490
markDirty();
@@ -831,6 +833,9 @@ namespace dxvk {
831833
}
832834
}
833835

836+
// Clamp the resolved value. There is no need to check if the clamp changed the value because we are already in the middle of changing the value
837+
clampValue(optionValue.data);
838+
834839
// If a runtime option layer exists, recompute the resolved value without it
835840
// to check whether the layer actually changes the final result. If the recomputed value
836841
// matches the current resolved value, it means the real-time layer is redundant,
@@ -859,6 +864,8 @@ namespace dxvk {
859864
}
860865
}
861866

867+
clampValue(originalResolvedValue.data);
868+
862869
if (isEqual(originalResolvedValue.data, optionValue.data)) {
863870
disableTopLayer();
864871
}

src/dxvk/rtx_render/rtx_option.h

Lines changed: 86 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)