From 2beb19c46ff6b773066b606b92dded32b4011a2d Mon Sep 17 00:00:00 2001 From: Ivan Mogilko Date: Tue, 7 Jan 2025 01:39:01 +0300 Subject: [PATCH] Engine: implemented ScriptThreads and thread stack in ScriptExecutor ScriptThread class contains a saved state of a running script thread, with a data stack, a callstack and current exec position. ScriptExecutor always runs a script on a certain thread. ScriptExecutor maintains a nested list of active threads. These may only be run one at a time in AGS, so when a new script runs while there is another active thread, that thread gets pushed to a thread stack and popped back after that new script finishes running. Engine creates 2 default script threads: "Main" and "Non-blocking" for running rep-exec-always kind of callbacks. There could be more ScriptThread instances created as necessary. --- Engine/ac/game.cpp | 1 + Engine/game/game_init.cpp | 3 +- Engine/script/script.cpp | 39 +++++-- Engine/script/script.h | 4 + Engine/script/scriptexecutor.cpp | 192 +++++++++++++++++++++++-------- Engine/script/scriptexecutor.h | 95 +++++++++++---- 6 files changed, 258 insertions(+), 76 deletions(-) diff --git a/Engine/ac/game.cpp b/Engine/ac/game.cpp index 188d99a8b6..82c33cfcd2 100644 --- a/Engine/ac/game.cpp +++ b/Engine/ac/game.cpp @@ -502,6 +502,7 @@ void unload_game() pl_stop_plugins(); FreeAllScripts(); + ShutdownScriptExec(); charextra.clear(); mls.clear(); diff --git a/Engine/game/game_init.cpp b/Engine/game/game_init.cpp index c26cd4fd8b..42c5aa8769 100644 --- a/Engine/game/game_init.cpp +++ b/Engine/game/game_init.cpp @@ -504,8 +504,7 @@ HGameInitError InitGameState(const LoadedGameEntities &ents, GameDataVersion dat // NOTE: we must do this before plugin start, because some plugins may // require access to script API at initialization time. // - scriptExecutor = std::make_unique(); - ccSetScriptAliveTimer(1000 / 60u, 1000u, 150000u); + InitScriptExec(); setup_script_exports(base_api, compat_api); // diff --git a/Engine/script/script.cpp b/Engine/script/script.cpp index 15caf05052..bfe6f5e1a3 100644 --- a/Engine/script/script.cpp +++ b/Engine/script/script.cpp @@ -40,9 +40,11 @@ #include "debug/debug_log.h" #include "debug/out.h" #include "main/game_run.h" +#include "media/audio/audio_system.h" #include "script/script_runtime.h" +#include "util/memory_compat.h" #include "util/string_compat.h" -#include "media/audio/audio_system.h" + using namespace AGS::Common; using namespace AGS::Engine; @@ -85,6 +87,8 @@ std::vector moduleRepExecAddr; size_t numScriptModules = 0; std::unique_ptr scriptExecutor; +std::unique_ptr scriptThreadMain; +std::unique_ptr scriptThreadNonBlocking; static bool DoRunScriptFuncCantBlock(const RuntimeScript *script, NonBlockingScriptFunction* funcToRun, bool hasTheFunc); @@ -288,7 +292,7 @@ static bool DoRunScriptFuncCantBlock(const RuntimeScript *script, NonBlockingScr return(false); no_blocking_functions++; - const ScriptExecError result = scriptExecutor->Run( + const ScriptExecError result = scriptExecutor->Run(scriptThreadNonBlocking.get(), script, funcToRun->FunctionName, funcToRun->Params, funcToRun->ParamCount); if (result == kScExecErr_FuncNotFound) @@ -358,7 +362,8 @@ RunScFuncResult RunScriptFunction(const RuntimeScript *script, const String &tsn return res; } - const ScriptExecError inst_ret = scriptExecutor->Run(curscript->Script, tsname, params, numParam); + const ScriptExecError inst_ret = scriptExecutor->Run(scriptThreadNonBlocking.get(), + curscript->Script, tsname, params, numParam); if ((inst_ret != kScExecErr_None) && (inst_ret != kScExecErr_FuncNotFound) && (inst_ret != kScExecErr_Aborted)) { quit_with_script_error(tsname); @@ -503,11 +508,24 @@ bool RunScriptFunctionAuto(ScriptType sc_type, const ScriptFunctionRef &fn_ref, return RunEventInModule(fn_ref, param_count, params); } -void AllocScriptModules() +void InitScriptExec() { - if (!scriptExecutor) - scriptExecutor = std::make_unique(); + scriptExecutor = std::make_unique(); + scriptThreadMain = std::make_unique("Main"); + scriptThreadNonBlocking = std::make_unique("Non-Blocking"); + ccSetScriptAliveTimer(1000 / 60u, 1000u, 150000u); +} + +void ShutdownScriptExec() +{ + scriptExecutor = {}; + scriptThreadMain = {}; + scriptThreadNonBlocking = {}; +} + +void AllocScriptModules() +{ // NOTE: this preallocation possibly required to safeguard some algorithms scriptModules.resize(numScriptModules); moduleRepExecAddr.resize(numScriptModules); @@ -566,8 +584,6 @@ void FreeAllScripts() runDialogOptionTextInputHandlerFunc.ModuleHasFunction.clear(); runDialogOptionRepExecFunc.ModuleHasFunction.clear(); runDialogOptionCloseFunc.ModuleHasFunction.clear(); - - scriptExecutor = nullptr; } //============================================================================= @@ -779,6 +795,13 @@ bool get_script_position(ScriptPosition &script_pos) return false; } +String cc_get_callstack(int max_lines) +{ + if (scriptExecutor && max_lines > 0) + return scriptExecutor->FormatCallStack(max_lines); + return {}; +} + String cc_format_error(const String &message) { if (currentline > 0) diff --git a/Engine/script/script.h b/Engine/script/script.h index d87319a400..f7d15f9104 100644 --- a/Engine/script/script.h +++ b/Engine/script/script.h @@ -137,6 +137,10 @@ bool RunScriptFunctionInRoom(const String &tsname, size_t param_count = 0, co bool RunScriptFunctionAuto(ScriptType sc_type, const ScriptFunctionRef &fn_ref, size_t param_count = 0, const RuntimeScriptValue *params = nullptr); +// Allocates script executor and standard threads +void InitScriptExec(); +// Frees script executor and all threads +void ShutdownScriptExec(); // Preallocates script module instances void AllocScriptModules(); // Link all script modules into a single program, diff --git a/Engine/script/scriptexecutor.cpp b/Engine/script/scriptexecutor.cpp index e22624a874..47cac4ad53 100644 --- a/Engine/script/scriptexecutor.cpp +++ b/Engine/script/scriptexecutor.cpp @@ -31,19 +31,71 @@ extern new_line_hook_type new_line_hook; extern std::unique_ptr scriptExecutor; -String cc_get_callstack(int max_lines) +namespace AGS +{ +namespace Engine { - // TODO: support separation onto groups, which have engine calls between - if (scriptExecutor) - return scriptExecutor->GetCallStack(max_lines); - return {}; + +// A helper function, formatting provided script pos and callstack +// into a human-readable text +static String FormatCallStack(const ScriptExecPosition &pos, const std::deque &callstack, uint32_t max_lines) +{ + if (!pos.Script) + return {}; + + String buffer = String::FromFormat("in \"%s\", line %d\n", pos.Script->GetSectionName(pos.PC).GetCStr(), pos.LineNumber); + + uint32_t lines_done = 0u; + for (auto it = callstack.crbegin(); it != callstack.crend() && (lines_done < max_lines); ++lines_done, ++it) + { + String lineBuffer = String::FromFormat("from \"%s\", line %d\n", + it->Script->GetSectionName(it->PC).GetCStr(), it->LineNumber); + buffer.Append(lineBuffer); + if (lines_done == max_lines - 1) + buffer.Append("(and more...)\n"); + } + return buffer; } -namespace AGS +// Size of stack in RuntimeScriptValues (aka distinct variables) +#define CC_STACK_SIZE 256 +// Size of stack in bytes (raw data storage) +#define CC_STACK_DATA_SIZE (1024 * sizeof(int32_t)) +#define MAX_CALL_STACK 128 + + +ScriptThread::ScriptThread() { -namespace Engine + Alloc(); +} + +ScriptThread::ScriptThread(const String &name) + : _name(name) +{ + Alloc(); +} + +void ScriptThread::Alloc() +{ + // Create a stack + // The size of a stack is quite an arbitrary choice; there's no way to deduce number of stack + // entries needed without knowing amount of local variables (at least) + _stack.resize(CC_STACK_SIZE); + _stackdata.resize(CC_STACK_DATA_SIZE); +} + +String ScriptThread::FormatCallStack(uint32_t max_lines) const { + return ::FormatCallStack(_pos, _callstack, max_lines); +} + +void ScriptThread::SaveState(const ScriptExecPosition &pos, std::deque &callstack) +{ + _callstack = callstack; + _pos = pos; +} + // Function call stack is used to temporarily store // values before passing them to script function @@ -67,35 +119,19 @@ struct FunctionCallStack size_t Count = 0u; }; -// Size of stack in RuntimeScriptValues (aka distinct variables) -#define CC_STACK_SIZE 256 -// Size of stack in bytes (raw data storage) -#define CC_STACK_DATA_SIZE (1024 * sizeof(int32_t)) -#define MAX_CALL_STACK 128 - RuntimeScriptValue ScriptExecutor::_pluginReturnValue; -ScriptExecutor::ScriptExecutor() -{ - // Create a stack - // The size of a stack is quite an arbitrary choice; there's no way to deduce number of stack - // entries needed without knowing amount of local variables (at least) - _stack.resize(CC_STACK_SIZE); - _stackdata.resize(CC_STACK_DATA_SIZE); - _stackBegin = _stack.data(); - _stackdataBegin = _stackdata.data(); - _stackdataPtr = _stackdata.data(); -} - void ScriptExecutor::SetPluginReturnValue(const RuntimeScriptValue &value) { _pluginReturnValue = value; } -ScriptExecError ScriptExecutor::Run(const RuntimeScript *script, const String &funcname, const RuntimeScriptValue *params, size_t param_count) +ScriptExecError ScriptExecutor::Run(ScriptThread *thread, const RuntimeScript *script, const String &funcname, const RuntimeScriptValue *params, size_t param_count) { + assert(thread); + assert(script); assert(param_count == 0 || params); cc_clear_error(); @@ -105,7 +141,7 @@ ScriptExecError ScriptExecutor::Run(const RuntimeScript *script, const String &f return kScExecErr_Busy; } - if (param_count > 0 && !params) + if (!thread || !script || (param_count > 0) && !params) { cc_error("bad input arguments in ScriptExecutor::Run"); return kScExecErr_Generic; @@ -143,8 +179,12 @@ ScriptExecError ScriptExecutor::Run(const RuntimeScript *script, const String &f _returnValue = 0; currentline = 0; // FIXME: stop using a global variable + PushThread(thread); + const ScriptExecError reterr = Run(script, start_at, params, param_count); + PopThread(); + const bool was_aborted = (_flags & kScExecState_Aborted) != 0; const bool has_nested_calls = _callstack.size() > 0; // Clear exec state @@ -175,32 +215,94 @@ void ScriptExecutor::Abort() _flags |= kScExecState_Aborted; } +void ScriptExecutor::PushThread(ScriptThread *thread) +{ + assert(thread); + // If there's already an active thread, then save latest exec state, + // then push the previous thread to the thread stack. + ScriptThread *was_thread = _thread; + if (_thread) + { + if (_thread != thread) + { + _thread->SaveState(ScriptExecPosition(_current, _pc, _lineNumber), _callstack); + } + _threadStack.push_back(_thread); // push always, simpler to do PopThread this way + } + + // Assign new current thread, get its data if it's a different one + _thread = thread; + if (was_thread != _thread) + { + _callstack = _thread->GetCallStack(); + const auto &pos = _thread->GetPosition(); + SetCurrentScript(pos.Script); + _pc = pos.PC; + _lineNumber = pos.LineNumber; + } +} + +void ScriptExecutor::PopThread() +{ + assert(_thread); + // If the previous thread is not the same thread, then save latest state + if (_threadStack.empty() || _threadStack.back() != _thread) + { + _thread->SaveState(ScriptExecPosition(_current, _pc, _lineNumber), _callstack); + } + // If there's anything in the thread stack, pop one back and restore the state + if (_threadStack.empty()) + { + _thread = nullptr; + SetCurrentScript(nullptr); + _pc = 0; + _lineNumber = 0; + } + else + { + ScriptThread *was_thread = _thread; + _thread = _threadStack.back(); + _threadStack.pop_back(); + + if (was_thread != _thread) + { + _callstack = _thread->GetCallStack(); + const auto &pos = _thread->GetPosition(); + SetCurrentScript(pos.Script); + _pc = pos.PC; + _lineNumber = pos.LineNumber; + } + } +} + void ScriptExecutor::GetScriptPosition(ScriptPosition &script_pos) const { if (!_current) return; - script_pos.Section = _current->GetSectionName(_pc); - script_pos.Line = _lineNumber; + script_pos = ScriptPosition(_current->GetSectionName(_pc), _lineNumber); } -String ScriptExecutor::GetCallStack(const int maxLines) const +String ScriptExecutor::FormatCallStack(uint32_t max_lines) const { - if (!_current) - return {}; + if (!_thread) + return {}; // not running on any thread - String buffer = String::FromFormat("in \"%s\", line %d\n", _current->GetSectionName(_pc).GetCStr(), _lineNumber); + String callstack; + callstack.Append("in the active script:\n"); + callstack.Append(::FormatCallStack(ScriptExecPosition(_current, _pc, _lineNumber), _callstack, max_lines)); + callstack.Append("in the waiting script:\n"); - int linesDone = 0; - for (auto it = _callstack.crbegin(); it != _callstack.crend() && (linesDone < maxLines); linesDone++, ++it) + const ScriptThread *last_thread = nullptr; + for (auto thread = _threadStack.crbegin(); thread != _threadStack.crend(); ++thread) { - String lineBuffer = String::FromFormat("from \"%s\", line %d\n", - it->Script->GetSectionName(it->PC).GetCStr(), it->LineNumber); - buffer.Append(lineBuffer); - if (linesDone == maxLines - 1) - buffer.Append("(and more...)\n"); + if (last_thread == *thread) + continue; + + callstack.Append((*thread)->FormatCallStack(max_lines)); + last_thread = *thread; } - return buffer; + return callstack; } void ScriptExecutor::SetExecTimeout(unsigned sys_poll_ms, unsigned abort_ms, unsigned abort_loops) @@ -341,9 +443,9 @@ ScriptExecError ScriptExecutor::Run(const RuntimeScript *script, int32_t curpc, else { // On script thread entry we reset stack pointers to the actual beginning - // of executor's stack. - _registers[SREG_SP].SetStackPtr(_stack.data()); - _stackdataPtr = _stackdata.data(); + // of the thread's stack. + _registers[SREG_SP].SetStackPtr(_thread->GetStack().data()); + _stackdataPtr = _thread->GetStackData().data(); } SetCurrentScript(script); @@ -1582,7 +1684,6 @@ void ScriptExecutor::SetCurrentScript(const RuntimeScript *script) _code_fixups = _current->GetCodeFixups().data(); _strings = _current->GetStrings().data(); _stringsize = _current->GetStrings().size(); - // Table pointers _rtti = RuntimeScript::GetJointRTTI(); _typeidLocal2Global = &_current->GetLocal2GlobalTypeMap(); } @@ -1593,7 +1694,6 @@ void ScriptExecutor::SetCurrentScript(const RuntimeScript *script) _code_fixups = nullptr; _strings = nullptr; _stringsize = 0u; - // Table pointers _rtti = nullptr; _typeidLocal2Global = nullptr; } diff --git a/Engine/script/scriptexecutor.h b/Engine/script/scriptexecutor.h index 28420957f8..4fd29ccb30 100644 --- a/Engine/script/scriptexecutor.h +++ b/Engine/script/scriptexecutor.h @@ -50,21 +50,58 @@ struct ScriptExecPosition int32_t PC = 0; int32_t LineNumber = 0; + ScriptExecPosition() = default; ScriptExecPosition(const RuntimeScript *script, int pc, int linenumber) : Script(script), PC(pc), LineNumber(linenumber) {} }; -struct FunctionCallStack; +class ScriptThread +{ + using String = Common::String; +public: + ScriptThread(); + ScriptThread(const String &name); + + const String &GetName() const { return _name; } + std::vector &GetStack() { return _stack; } + std::vector &GetStackData() { return _stackdata; } + const std::deque &GetCallStack() const { return _callstack; } + const ScriptExecPosition &GetPosition() const { return _pos; } + + // Get the script's execution position and callstack as human-readable text + String FormatCallStack(uint32_t max_lines = UINT32_MAX) const; + + // Record script execution state in the thread object + void SaveState(const ScriptExecPosition &pos, std::deque &callstack); + +private: + void Alloc(); + + // An arbitrary name for this script thread + String _name; + // Data stack, contains function args, local variables, temporary values + std::vector _stack; + // An array for keeping stack data; stack entries reference data of variable size from here + std::vector _stackdata; + // Executed script callstack, contains *previous* script positions + std::deque _callstack; // deque for easier iterating over + // Latest recorded script position, used when thread gets suspended + ScriptExecPosition _pos; +}; + + +struct FunctionCallStack; class ScriptExecutor { using String = Common::String; public: - ScriptExecutor(); + ScriptExecutor() = default; - // Begin executing the function in the given script, passing an array of parameters - ScriptExecError Run(const RuntimeScript *script, const String &funcname, const RuntimeScriptValue *params, size_t param_count); + // Begin executing the function in the given script, passing an array of parameters; + // the script will be executed on the provided script thread. + ScriptExecError Run(ScriptThread *thread, const RuntimeScript *script, const String &funcname, const RuntimeScriptValue *params, size_t param_count); // Schedule abortion of the current script execution; // the actual stop will occur whenever control returns to the ScriptExecutor. void Abort(); @@ -74,20 +111,23 @@ class ScriptExecutor bool IsRunning() const { return (_flags & kScExecState_Running) != 0; } // Tells if the executor is busy running bytecode, and cannot start a nested run right now bool IsBusy() const { return (_flags & kScExecState_Busy) != 0; } - // Get the currently running script + // Get the currently used script thread + ScriptThread *GetRunningThread() const { return _thread; } + // Get the currently executed script const RuntimeScript *GetRunningScript() const { return _current; } // Get current program pointer (position in bytecode) int GetPC() const { return _pc; } // Get the script's execution position void GetScriptPosition(ScriptPosition &script_pos) const; - // Get the script's execution position and callstack as human-readable text - Common::String GetCallStack(int max_lines = INT_MAX) const; // Gets the top entry of this instance's stack const RuntimeScriptValue *GetCurrentStack() const { return _registers[SREG_SP].RValue; } // Get latest return value int GetReturnValue() const { return _returnValue; } // TODO: this is a hack, required for dialog script; redo this later! void SetReturnValue(int val) { _returnValue = val; } + + // Get the script's execution position and callstack as human-readable text + String FormatCallStack(uint32_t max_lines = UINT32_MAX) const; // Configures script executor timeout in case of a "hanging" script void SetExecTimeout(unsigned sys_poll_ms, unsigned abort_ms, unsigned abort_loops); @@ -104,6 +144,14 @@ class ScriptExecutor // Begin executing latest run script starting from the given bytecode index ScriptExecError Run(int32_t curpc); + // Switches to the new script thread; + // if there was any thread currently in running, then saves its state and + // pushes to the thread stack. + void PushThread(ScriptThread *thread); + // Pops out a script thread from the thread stack, + // if there was any, and makes it active. + void PopThread(); + // Sets current script and fast-access pointers void SetCurrentScript(const RuntimeScript *script); // For calling exported plugin functions old-style @@ -127,16 +175,17 @@ class ScriptExecutor void PushToFuncCallStack(FunctionCallStack &func_callstack, const RuntimeScriptValue &rval); void PopFromFuncCallStack(FunctionCallStack &func_callstack, int32_t num_entries); - // - // Virtual machine state - // Registers - RuntimeScriptValue _registers[CC_NUM_REGISTERS]; - // Data stack, contains function args, local variables, temporary values - std::vector _stack; - // An array for keeping stack data; stack entries reference data of variable size from here - std::vector _stackdata; - RuntimeScriptValue *_stackBegin = nullptr; // fast-access ptr to beginning of _stack - uint8_t *_stackdataBegin = nullptr; // fast-access ptr to beginning of _stackdata + // Current used thread + ScriptThread *_thread = nullptr; + // Thread stack holds a list of running or suspended script threads; + // In AGS currently only one script thread is running, others are waiting in the queue. + // An example situation is "repeatedly_execute_always" callback running while + // other thread(s) are waiting at the blocking action or Wait(). + std::deque _threadStack; + + // Thread stack pointers for faster access to the current thread + RuntimeScriptValue *_stackBegin = nullptr; // ptr to beginning of stack (or stack's section) + uint8_t *_stackdataBegin = nullptr; // ptr to beginning of stackdata (or stackdata's section) uint8_t *_stackdataPtr = nullptr; // points to the next unused byte in stack data array // Current executed script @@ -147,16 +196,22 @@ class ScriptExecutor const uint8_t *_code_fixups = nullptr; const char *_strings = nullptr; // pointer to ccScript's string data size_t _stringsize = 0u; - // Table pointers for simplicity + // Script table pointers const JointRTTI *_rtti = nullptr; const std::unordered_map *_typeidLocal2Global = nullptr; + // + // Virtual machine state + // Registers + RuntimeScriptValue _registers[CC_NUM_REGISTERS]; uint32_t _flags = kScExecState_None; // executor state flags ScriptExecState int _pc = 0; // program counter int _lineNumber = 0; // source code line number int _returnValue = 0; // last executed function's return value - // Executed script callstack, contains *previous* script positions - std::deque _callstack; // deque for easier iterating over + // Executed script callstack, contains *previous* script positions; + // this is copied from the ScriptThread on start, and copied back when done + std::deque _callstack; + // A value returned from plugin functions saved as RuntimeScriptValue. // This is a temporary solution (*sigh*, one of many) which allows to