Not sure I understand. The main problem with multithreaded scripting is concurrent access to the world state, since every script needs the access.AnyOldName3 wrote: ↑08 Nov 2020, 01:05 Could we have something like a bunch of scripts that need to be run at some point during the frame, and the main thread sticks them in a queue of waiting scripts that get picked up by a pool of worker threads, then once it needs the results of things, it can either deal with what the worker threads have already processed, or pick a script from the front of the queue to run itself if no results are ready yet. This architecture would still leave the main thread in control, if OpenMW was run on a machine with as many cores as scripts it could scale, and we can reuse the worker threads for other async tasks like preloading for the part of the frame where we've got no pending scripts yet.
I sort of anticipate that we're going to have a bunch of work that absolutely needs doing before any scripts can be run and a bunch that needs doing after all scripts have finished and then a bunch of work that all arrives at once and needs to be finished as soon as possible, so we'll want a system that copes well with that.
Let me illustrate the approaches I see with code samples:
1. Current state (no separate thread)
It is how OpenMW function now.
Code: Select all
// Main thread
while (true) {
executeScripts();
updateMechanics();
updatePhysics();
updateWorld();
updateGUI();
mViewer->eventTraversal();
mViewer->updateTraversal();
mViewer->renderingTraversal();
}
2. Separate thread for scripting
Here we have one worker thread that evaluates scripts while the main thread does rendering.
We will need to verify that scripting and rendering never work with the same data at the same time.
Having several workers is more complicated due to the same concurrency problem. I.e. we can crash if one script reads an inventory at the same time as another script removes an item from it. Locking objects with critical sections may have too big overhead.
However even with a single worker separating scripting and rendering will help a lot.
Code: Select all
// Main thread
startScriptingThread();
while (true) {
// Update world and apply changes to OSG scene tree
updateMechanics();
updatePhysics();
updateWorld();
updateGUI();
mExecuteScriptsFlag = true; // start scripting
// Render OSG scene tree. Shouldn't use MWWorld.
mViewer->eventTraversal();
mViewer->updateTraversal();
mViewer->renderingTraversal();
while (mExecuteScriptsFlag) wait(); // wait scripting if it is not finished yet
}
// Scripting thread
initializeLuaAPI();
loadScripts();
while (true) {
while (!mExecuteScriptsFlag) wait();
executeScripts(); // Shouldn't use OSG scene tree.
mExecuteScriptsFlag = false;
}
3. Separate thread for scripting; copy data before use
Here we copy world data to a separate object (ScriptingWorld) before running scripts.
Benefits from ScriptingWorld:
- It is easy to verify that scripting and rendering work with different data.
- Possibility to have several worker threads for scripting (just create several instances of ScriptingWorld).
- Scripting rate can differ from frame rate (i.e. no need to drop frame rate if scripts are slow).
Code: Select all
// Main thread
startScriptingThread();
while (true) {
if (!mExecuteScriptsFlag) { // start scripting if the previous scripting iteration is already finished
// Apply results of previous scripting frame from ScriptingWorld to World
applyChangesToWorld(mScriptingWorld);
// Update data in ScriptingWorld
updateScriptingWorld(mScriptingWorld);
mExecuteScriptsFlag = true;
}
updateMechanics();
updatePhysics();
updateWorld();
updateGUI();
mViewer->eventTraversal();
mViewer->updateTraversal();
mViewer->renderingTraversal();
}
// Scripting thread
initializeLuaAPI();
loadScripts();
while (true) {
while (!mExecuteScriptsFlag) wait();
executeScripts(mScriptingWorld); // Only ScriptingWorld is used
mExecuteScriptsFlag = false;
}