From c7c48d03e90734ee414d3f0bf05b28f9a8ed3fbd Mon Sep 17 00:00:00 2001 From: WarmUpTill Date: Fri, 30 Aug 2024 21:21:21 +0200 Subject: [PATCH] Add option for short circuit evaluation of conditions --- data/locale/en-US.ini | 3 ++ lib/macro/macro-settings.cpp | 49 ++++++++++++----- lib/macro/macro-settings.hpp | 10 ++-- lib/macro/macro-tab.cpp | 3 +- lib/macro/macro.cpp | 96 ++++++++++++++++++++++++++-------- lib/macro/macro.hpp | 10 +++- lib/utils/condition-logic.cpp | 21 +++++--- lib/utils/condition-logic.hpp | 5 ++ tests/test-condition-logic.cpp | 17 ++++++ 9 files changed, 168 insertions(+), 46 deletions(-) diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index 16061c45..530a746b 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -194,7 +194,10 @@ AdvSceneSwitcher.macroTab.highlightExecutedMacros="Highlight recently executed m AdvSceneSwitcher.macroTab.highlightTrueConditions="Highlight conditions of currently selected macro that evaluated to true recently" AdvSceneSwitcher.macroTab.highlightPerformedActions="Highlight recently performed actions of currently selected macro" AdvSceneSwitcher.macroTab.newMacroRegisterHotkey="Register hotkeys to control the pause state of new macros" +AdvSceneSwitcher.macroTab.newMacroUseShortCircuitEvaluation="Enable short circuit evaluation of macro conditions for new macros" AdvSceneSwitcher.macroTab.currentDisableHotkeys="Register hotkeys to control the pause state of selected macro" +AdvSceneSwitcher.macroTab.currentUseShortCircuitEvaluation="Enable short circuit evaluation of macro conditions for currently selected macro" +AdvSceneSwitcher.macroTab.shortCircuit.tooltip="Enabling short circuit evaluation might improve the performance, as some condition checks are skipped, if the overall macro cannot be evaluated to \"true\" anymore.\nHowever, please note that condition checks, which are skipped over, will also not update their duration modifier checks." AdvSceneSwitcher.macroTab.currentSkipExecutionOnStartup="Skip execution of actions of current macro on startup" AdvSceneSwitcher.macroTab.currentStopActionsIfNotDone="Stop and rerun actions of the currently selected macro, if the actions are still running, when a new execution is triggered" AdvSceneSwitcher.macroTab.currentRegisterDock="Register dock widget to control the pause state of selected macro or run it manually" diff --git a/lib/macro/macro-settings.cpp b/lib/macro/macro-settings.cpp index c1382ff7..d417a34e 100644 --- a/lib/macro/macro-settings.cpp +++ b/lib/macro/macro-settings.cpp @@ -20,6 +20,8 @@ void GlobalMacroSettings::Save(obs_data_t *obj) const obs_data_set_bool(data, "highlightActions", _highlightActions); obs_data_set_bool(data, "newMacroRegisterHotkey", _newMacroRegisterHotkeys); + obs_data_set_bool(data, "newMacroUseShortCircuitEvaluation", + _newMacroUseShortCircuitEvaluation); obs_data_set_obj(obj, "macroSettings", data); obs_data_release(data); } @@ -38,6 +40,8 @@ void GlobalMacroSettings::Load(obs_data_t *obj) _highlightActions = obs_data_get_bool(data, "highlightActions"); _newMacroRegisterHotkeys = obs_data_get_bool(data, "newMacroRegisterHotkey"); + _newMacroUseShortCircuitEvaluation = + obs_data_get_bool(data, "newMacroUseShortCircuitEvaluation"); obs_data_release(data); } @@ -45,16 +49,20 @@ MacroSettingsDialog::MacroSettingsDialog(QWidget *parent, const GlobalMacroSettings &settings, Macro *macro) : QDialog(parent), - _executed(new QCheckBox(obs_module_text( + _highlightExecutedMacros(new QCheckBox(obs_module_text( "AdvSceneSwitcher.macroTab.highlightExecutedMacros"))), - _conditions(new QCheckBox(obs_module_text( + _highlightConditions(new QCheckBox(obs_module_text( "AdvSceneSwitcher.macroTab.highlightTrueConditions"))), - _actions(new QCheckBox(obs_module_text( + _highlightActions(new QCheckBox(obs_module_text( "AdvSceneSwitcher.macroTab.highlightPerformedActions"))), _newMacroRegisterHotkeys(new QCheckBox(obs_module_text( "AdvSceneSwitcher.macroTab.newMacroRegisterHotkey"))), + _newMacroUseShortCircuitEvaluation(new QCheckBox(obs_module_text( + "AdvSceneSwitcher.macroTab.newMacroUseShortCircuitEvaluation"))), _currentMacroRegisterHotkeys(new QCheckBox(obs_module_text( "AdvSceneSwitcher.macroTab.currentDisableHotkeys"))), + _currentUseShortCircuitEvaluation(new QCheckBox(obs_module_text( + "AdvSceneSwitcher.macroTab.currentUseShortCircuitEvaluation"))), _currentSkipOnStartup(new QCheckBox(obs_module_text( "AdvSceneSwitcher.macroTab.currentSkipExecutionOnStartup"))), _currentStopActionsIfNotDone(new QCheckBox(obs_module_text( @@ -83,12 +91,17 @@ MacroSettingsDialog::MacroSettingsDialog(QWidget *parent, setWindowModality(Qt::WindowModality::WindowModal); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + _newMacroUseShortCircuitEvaluation->setToolTip(obs_module_text( + "AdvSceneSwitcher.macroTab.shortCircuit.tooltip")); + _currentUseShortCircuitEvaluation->setToolTip(obs_module_text( + "AdvSceneSwitcher.macroTab.shortCircuit.tooltip")); + auto highlightOptions = new QGroupBox( obs_module_text("AdvSceneSwitcher.macroTab.highlightSettings")); auto highlightLayout = new QVBoxLayout; - highlightLayout->addWidget(_executed); - highlightLayout->addWidget(_conditions); - highlightLayout->addWidget(_actions); + highlightLayout->addWidget(_highlightExecutedMacros); + highlightLayout->addWidget(_highlightConditions); + highlightLayout->addWidget(_highlightActions); highlightOptions->setLayout(highlightLayout); auto hotkeyOptions = new QGroupBox( @@ -103,6 +116,8 @@ MacroSettingsDialog::MacroSettingsDialog(QWidget *parent, auto generalLayout = new QVBoxLayout; generalLayout->addWidget(_currentSkipOnStartup); generalLayout->addWidget(_currentStopActionsIfNotDone); + generalLayout->addWidget(_currentUseShortCircuitEvaluation); + generalLayout->addWidget(_newMacroUseShortCircuitEvaluation); generalOptions->setLayout(generalLayout); auto inputOptions = new QGroupBox( @@ -198,10 +213,12 @@ MacroSettingsDialog::MacroSettingsDialog(QWidget *parent, dialogLayout->addWidget(buttonbox); setLayout(dialogLayout); - _executed->setChecked(settings._highlightExecuted); - _conditions->setChecked(settings._highlightConditions); - _actions->setChecked(settings._highlightActions); + _highlightExecutedMacros->setChecked(settings._highlightExecuted); + _highlightConditions->setChecked(settings._highlightConditions); + _highlightActions->setChecked(settings._highlightActions); _newMacroRegisterHotkeys->setChecked(settings._newMacroRegisterHotkeys); + _newMacroUseShortCircuitEvaluation->setChecked( + settings._newMacroUseShortCircuitEvaluation); if (!macro || macro->IsGroup()) { hotkeyOptions->hide(); @@ -212,6 +229,8 @@ MacroSettingsDialog::MacroSettingsDialog(QWidget *parent, } _currentMacroRegisterHotkeys->setChecked(macro->PauseHotkeysEnabled()); + _currentUseShortCircuitEvaluation->setChecked( + macro->ShortCircuitEvaluationEnabled()); _currentSkipOnStartup->setChecked(macro->SkipExecOnStart()); _currentStopActionsIfNotDone->setChecked(macro->StopActionsIfNotDone()); _currentInputs->SetInputs(macro->GetInputVariables()); @@ -322,17 +341,23 @@ bool MacroSettingsDialog::AskForSettings(QWidget *parent, if (dialog.exec() != DialogCode::Accepted) { return false; } - userInput._highlightExecuted = dialog._executed->isChecked(); - userInput._highlightConditions = dialog._conditions->isChecked(); - userInput._highlightActions = dialog._actions->isChecked(); + userInput._highlightExecuted = + dialog._highlightExecutedMacros->isChecked(); + userInput._highlightConditions = + dialog._highlightConditions->isChecked(); + userInput._highlightActions = dialog._highlightActions->isChecked(); userInput._newMacroRegisterHotkeys = dialog._newMacroRegisterHotkeys->isChecked(); + userInput._newMacroUseShortCircuitEvaluation = + dialog._newMacroUseShortCircuitEvaluation->isChecked(); if (!macro) { return true; } macro->EnablePauseHotkeys( dialog._currentMacroRegisterHotkeys->isChecked()); + macro->SetShortCircuitEvaluation( + dialog._currentUseShortCircuitEvaluation->isChecked()); macro->SetSkipExecOnStart(dialog._currentSkipOnStartup->isChecked()); macro->SetStopActionsIfNotDone( dialog._currentStopActionsIfNotDone->isChecked()); diff --git a/lib/macro/macro-settings.hpp b/lib/macro/macro-settings.hpp index 5ae7c5a7..0a0ed51e 100644 --- a/lib/macro/macro-settings.hpp +++ b/lib/macro/macro-settings.hpp @@ -23,6 +23,7 @@ public: bool _highlightConditions = false; bool _highlightActions = false; bool _newMacroRegisterHotkeys = true; + bool _newMacroUseShortCircuitEvaluation = false; }; // Dialog for configuring global and individual macro specific settings @@ -44,12 +45,15 @@ private slots: private: void Resize(); - QCheckBox *_executed; - QCheckBox *_conditions; - QCheckBox *_actions; + // Global macro settings + QCheckBox *_highlightExecutedMacros; + QCheckBox *_highlightConditions; + QCheckBox *_highlightActions; QCheckBox *_newMacroRegisterHotkeys; + QCheckBox *_newMacroUseShortCircuitEvaluation; // Current macro specific settings QCheckBox *_currentMacroRegisterHotkeys; + QCheckBox *_currentUseShortCircuitEvaluation; QCheckBox *_currentSkipOnStartup; QCheckBox *_currentStopActionsIfNotDone; MacroInputSelection *_currentInputs; diff --git a/lib/macro/macro-tab.cpp b/lib/macro/macro-tab.cpp index 90a16ea7..1a3b89b0 100644 --- a/lib/macro/macro-tab.cpp +++ b/lib/macro/macro-tab.cpp @@ -87,7 +87,8 @@ bool AdvSceneSwitcher::AddNewMacro(std::shared_ptr &res, } res = std::make_shared( - name, GetGlobalMacroSettings()._newMacroRegisterHotkeys); + name, GetGlobalMacroSettings()._newMacroRegisterHotkeys, + GetGlobalMacroSettings()._newMacroUseShortCircuitEvaluation); return true; } diff --git a/lib/macro/macro.cpp b/lib/macro/macro.cpp index cf2909ad..ccbb7fd5 100644 --- a/lib/macro/macro.cpp +++ b/lib/macro/macro.cpp @@ -76,13 +76,15 @@ namespace advss { static std::deque> macros; -Macro::Macro(const std::string &name, const bool addHotkey) +Macro::Macro(const std::string &name, const bool addHotkey, + const bool shortCircuitEvaluation) { SetName(name); if (addHotkey) { SetupHotkeys(); } _registerHotkeys = addHotkey; + _useShortCircuitEvaluation = shortCircuitEvaluation; } Macro::~Macro() @@ -181,6 +183,61 @@ static bool checkCondition(const std::shared_ptr &condition) return conditionMatched; } +bool Macro::CheckConditionHelper( + const std::shared_ptr &condition) const +{ + bool conditionMatched = false; + bool wasEvaluated = false; + + const auto evaluateCondition = [&condition, &conditionMatched, + &wasEvaluated]() -> bool { + conditionMatched = checkCondition(condition); + conditionMatched = + condition->CheckDurationModifier(conditionMatched); + wasEvaluated = true; + return conditionMatched; + }; + + const auto logicType = condition->GetLogicType(); + if (logicType == Logic::Type::NONE) { + vblog(LOG_INFO, "ignoring condition '%s' for '%s'", + condition->GetId().c_str(), _name.c_str()); + if (!_useShortCircuitEvaluation) { + (void)evaluateCondition(); + } + return _matched; + } + + bool result = _useShortCircuitEvaluation + // Evaluate the condition result if needed + ? Logic::ApplyConditionLogic(logicType, _matched, + evaluateCondition, + _name.c_str()) + // Evaluate the condition result right away + : Logic::ApplyConditionLogic(logicType, _matched, + evaluateCondition(), + _name.c_str()); + + const bool isNegativeLogicType = Logic::IsNegationType(logicType); + if (wasEvaluated && ((conditionMatched && !isNegativeLogicType) || + (!conditionMatched && isNegativeLogicType))) { + condition->EnableHighlight(); + } + + if (VerboseLoggingEnabled()) { + if (wasEvaluated) { + blog(LOG_INFO, "condition %s returned %d", + condition->GetId().c_str(), conditionMatched); + } else { + blog(LOG_INFO, + "condition %s evaluation skipped (short circuit)", + condition->GetId().c_str()); + } + } + + return result; +} + bool Macro::CeckMatch(bool ignorePause) { if (_isGroup) { @@ -194,28 +251,7 @@ bool Macro::CeckMatch(bool ignorePause) return false; } - bool conditionMatched = checkCondition(condition); - conditionMatched = - condition->CheckDurationModifier(conditionMatched); - - const auto logicType = condition->GetLogicType(); - if (logicType == Logic::Type::NONE) { - vblog(LOG_INFO, "ignoring condition '%s' for '%s'", - condition->GetId().c_str(), _name.c_str()); - continue; - } - vblog(LOG_INFO, "condition %s returned %d", - condition->GetId().c_str(), conditionMatched); - - const bool isNegativeLogicType = - Logic::IsNegationType(logicType); - if ((conditionMatched && !isNegativeLogicType) || - (!conditionMatched && isNegativeLogicType)) { - condition->EnableHighlight(); - } - - _matched = Logic::ApplyConditionLogic( - logicType, _matched, conditionMatched, _name.c_str()); + _matched = CheckConditionHelper(condition); } vblog(LOG_INFO, "Macro %s returned %d", _name.c_str(), _matched); @@ -394,6 +430,16 @@ void Macro::SetStopActionsIfNotDone(bool stopActionsIfNotDone) _stopActionsIfNotDone = stopActionsIfNotDone; } +void Macro::SetShortCircuitEvaluation(bool useShortCircuitEvaluation) +{ + _useShortCircuitEvaluation = useShortCircuitEvaluation; +} + +bool Macro::ShortCircuitEvaluationEnabled() const +{ + return _useShortCircuitEvaluation; +} + void Macro::SetPaused(bool pause) { if (_paused && !pause) { @@ -618,6 +664,8 @@ bool Macro::Save(obs_data_t *obj, bool saveForCopy) const obs_data_set_bool(obj, "onChange", _performActionsOnChange); obs_data_set_bool(obj, "skipExecOnStart", _skipExecOnStart); obs_data_set_bool(obj, "stopActionsIfNotDone", _stopActionsIfNotDone); + obs_data_set_bool(obj, "useShortCircuitEvaluation", + _useShortCircuitEvaluation); obs_data_set_bool(obj, "group", _isGroup); if (_isGroup) { @@ -681,6 +729,8 @@ bool Macro::Load(obs_data_t *obj) _performActionsOnChange = obs_data_get_bool(obj, "onChange"); _skipExecOnStart = obs_data_get_bool(obj, "skipExecOnStart"); _stopActionsIfNotDone = obs_data_get_bool(obj, "stopActionsIfNotDone"); + _useShortCircuitEvaluation = + obs_data_get_bool(obj, "useShortCircuitEvaluation"); _isGroup = obs_data_get_bool(obj, "group"); if (_isGroup) { diff --git a/lib/macro/macro.hpp b/lib/macro/macro.hpp index a73ec917..ba6918a3 100644 --- a/lib/macro/macro.hpp +++ b/lib/macro/macro.hpp @@ -23,7 +23,8 @@ class MacroDock; class Macro { public: - Macro(const std::string &name = "", const bool addHotkey = false); + Macro(const std::string &name = "", const bool addHotkey = false, + const bool shortCircuitEvaluation = false); virtual ~Macro(); std::string Name() const { return _name; } @@ -54,6 +55,9 @@ public: void SetStopActionsIfNotDone(bool stopActionsIfNotDone); bool StopActionsIfNotDone() const { return _stopActionsIfNotDone; } + void SetShortCircuitEvaluation(bool useShortCircuitEvaluation); + bool ShortCircuitEvaluationEnabled() const; + int RunCount() const { return _runCount; }; void ResetRunCount() { _runCount = 0; }; @@ -149,6 +153,9 @@ private: void ClearHotkeys() const; void SetHotkeysDesc() const; + bool + CheckConditionHelper(const std::shared_ptr &) const; + bool RunActionsHelper( const std::deque> &actions, bool ignorePause); @@ -179,6 +186,7 @@ private: bool _isGroup = false; bool _isCollapsed = false; + bool _useShortCircuitEvaluation = false; bool _runInParallel = false; bool _matched = false; bool _lastMatched = false; diff --git a/lib/utils/condition-logic.cpp b/lib/utils/condition-logic.cpp index ebe6c88e..67deac74 100644 --- a/lib/utils/condition-logic.cpp +++ b/lib/utils/condition-logic.cpp @@ -19,6 +19,15 @@ const std::map Logic::localeMap = { bool Logic::ApplyConditionLogic(Type type, bool currentMatchResult, bool conditionMatched, const char *context) +{ + return ApplyConditionLogic( + type, currentMatchResult, + [conditionMatched]() { return conditionMatched; }, context); +} + +bool Logic::ApplyConditionLogic(Type type, bool currentMatchResult, + const std::function &evaluateCondition, + const char *context) { if (!context) { context = ""; @@ -26,22 +35,22 @@ bool Logic::ApplyConditionLogic(Type type, bool currentMatchResult, switch (type) { case Type::ROOT_NONE: - return conditionMatched; + return evaluateCondition(); case Type::ROOT_NOT: - return !conditionMatched; + return !evaluateCondition(); case Type::ROOT_LAST: break; case Type::NONE: vblog(LOG_INFO, "skipping condition check for '%s'", context); return currentMatchResult; case Type::AND: - return currentMatchResult && conditionMatched; + return currentMatchResult && evaluateCondition(); case Type::OR: - return currentMatchResult || conditionMatched; + return currentMatchResult || evaluateCondition(); case Type::AND_NOT: - return currentMatchResult && !conditionMatched; + return currentMatchResult && !evaluateCondition(); case Type::OR_NOT: - return currentMatchResult || !conditionMatched; + return currentMatchResult || !evaluateCondition(); case Type::LAST: default: blog(LOG_WARNING, "ignoring invalid logic check (%s)", context); diff --git a/lib/utils/condition-logic.hpp b/lib/utils/condition-logic.hpp index 4fc8a733..cfb12750 100644 --- a/lib/utils/condition-logic.hpp +++ b/lib/utils/condition-logic.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include #include @@ -39,6 +40,10 @@ public: static bool ApplyConditionLogic(Type, bool currentMatchResult, bool conditionMatched, const char *context); + static bool + ApplyConditionLogic(Type, bool currentMatchResult, + const std::function &evaluateCondition, + const char *context); static void PopulateLogicTypeSelection(QComboBox *list, bool isRootCondition); diff --git a/tests/test-condition-logic.cpp b/tests/test-condition-logic.cpp index 0f1c4f96..add10cc7 100644 --- a/tests/test-condition-logic.cpp +++ b/tests/test-condition-logic.cpp @@ -171,3 +171,20 @@ TEST_CASE("Logic", "[conditon-logic]") REQUIRE(advss::Logic::ApplyConditionLogic(logic, true, false, "")); REQUIRE(advss::Logic::ApplyConditionLogic(logic, true, true, "")); } + +TEST_CASE("Short circuit", "[conditon-logic]") +{ + bool functionWasRun = false; + const auto testFunction = [&functionWasRun]() { + functionWasRun = true; + return true; + }; + + advss::Logic::ApplyConditionLogic(advss::Logic::Type::AND, false, + testFunction, ""); + REQUIRE_FALSE(functionWasRun); + + advss::Logic::ApplyConditionLogic(advss::Logic::Type::OR, false, + testFunction, ""); + REQUIRE(functionWasRun); +}