Skip to content

Commit

Permalink
Engine: implemented ScriptThreads and thread stack in ScriptExecutor
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ivan-mogilko committed Jan 6, 2025
1 parent a8a1d5e commit 2beb19c
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 76 deletions.
1 change: 1 addition & 0 deletions Engine/ac/game.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@ void unload_game()
pl_stop_plugins();

FreeAllScripts();
ShutdownScriptExec();

charextra.clear();
mls.clear();
Expand Down
3 changes: 1 addition & 2 deletions Engine/game/game_init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<ScriptExecutor>();
ccSetScriptAliveTimer(1000 / 60u, 1000u, 150000u);
InitScriptExec();
setup_script_exports(base_api, compat_api);

//
Expand Down
39 changes: 31 additions & 8 deletions Engine/script/script.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -85,6 +87,8 @@ std::vector<RuntimeScriptValue> moduleRepExecAddr;
size_t numScriptModules = 0;

std::unique_ptr<ScriptExecutor> scriptExecutor;
std::unique_ptr<ScriptThread> scriptThreadMain;
std::unique_ptr<ScriptThread> scriptThreadNonBlocking;


static bool DoRunScriptFuncCantBlock(const RuntimeScript *script, NonBlockingScriptFunction* funcToRun, bool hasTheFunc);
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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>();
scriptExecutor = std::make_unique<ScriptExecutor>();
scriptThreadMain = std::make_unique<ScriptThread>("Main");
scriptThreadNonBlocking = std::make_unique<ScriptThread>("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);
Expand Down Expand Up @@ -566,8 +584,6 @@ void FreeAllScripts()
runDialogOptionTextInputHandlerFunc.ModuleHasFunction.clear();
runDialogOptionRepExecFunc.ModuleHasFunction.clear();
runDialogOptionCloseFunc.ModuleHasFunction.clear();

scriptExecutor = nullptr;
}

//=============================================================================
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions Engine/script/script.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
192 changes: 146 additions & 46 deletions Engine/script/scriptexecutor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,71 @@ extern new_line_hook_type new_line_hook;
extern std::unique_ptr<ScriptExecutor> 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<ScriptExecPosition> &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<ScriptExecPosition> &callstack)
{
_callstack = callstack;
_pos = pos;
}


// Function call stack is used to temporarily store
// values before passing them to script function
Expand All @@ -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();

Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
}
Expand All @@ -1593,7 +1694,6 @@ void ScriptExecutor::SetCurrentScript(const RuntimeScript *script)
_code_fixups = nullptr;
_strings = nullptr;
_stringsize = 0u;
// Table pointers
_rtti = nullptr;
_typeidLocal2Global = nullptr;
}
Expand Down
Loading

0 comments on commit 2beb19c

Please sign in to comment.