Index: binaries/data/mods/public/simulation/ai/common-api/entity.js =================================================================== --- binaries/data/mods/public/simulation/ai/common-api/entity.js +++ binaries/data/mods/public/simulation/ai/common-api/entity.js @@ -679,24 +679,22 @@ }, "resourceSupplyAmount": function() { - return this._entity.resourceSupplyAmount; + return SimEngine.QueryInterface(this.id(), Sim.IID_ResourceSupply)?.GetCurrentAmount(); }, "resourceSupplyNumGatherers": function() { - return this._entity.resourceSupplyNumGatherers; + return SimEngine.QueryInterface(this.id(), Sim.IID_ResourceSupply)?.GetNumGatherers(); }, "isFull": function() { - if (this._entity.resourceSupplyNumGatherers !== undefined) - return this.maxGatherers() === this._entity.resourceSupplyNumGatherers; - - return undefined; + let numGatherers = this.resourceSupplyNumGatherers(); + return numGatherers ? this.maxGatherers() === numGatherers : undefined; }, "resourceCarrying": function() { - return this._entity.resourceCarrying; + return SimEngine.QueryInterface(this.id(), Sim.IID_ResourceGatherer)?.GetCarryingStatus(); }, "currentGatherRate": function() { Index: binaries/data/mods/public/simulation/components/AIProxy.js =================================================================== --- binaries/data/mods/public/simulation/components/AIProxy.js +++ binaries/data/mods/public/simulation/components/AIProxy.js @@ -179,27 +179,6 @@ this.cmpAIInterface.PushEvent("UnGarrison", { "entity": ent, "holder": this.entity }); }; -AIProxy.prototype.OnResourceSupplyChanged = function(msg) -{ - if (!this.NotifyChange()) - return; - this.changes.resourceSupplyAmount = msg.to; -}; - -AIProxy.prototype.OnResourceSupplyNumGatherersChanged = function(msg) -{ - if (!this.NotifyChange()) - return; - this.changes.resourceSupplyNumGatherers = msg.to; -}; - -AIProxy.prototype.OnResourceCarryingChanged = function(msg) -{ - if (!this.NotifyChange()) - return; - this.changes.resourceCarrying = msg.to; -}; - AIProxy.prototype.OnFoundationProgressChanged = function(msg) { if (!this.NotifyChange()) @@ -305,21 +284,6 @@ ret.foundationProgress = cmpFoundation.GetBuildPercentage(); } - let cmpResourceSupply = Engine.QueryInterface(this.entity, IID_ResourceSupply); - if (cmpResourceSupply) - { - // Updated by OnResourceSupplyChanged - ret.resourceSupplyAmount = cmpResourceSupply.GetCurrentAmount(); - ret.resourceSupplyNumGatherers = cmpResourceSupply.GetNumGatherers(); - } - - let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); - if (cmpResourceGatherer) - { - // Updated by OnResourceCarryingChanged - ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); - } - let cmpResourceDropsite = Engine.QueryInterface(this.entity, IID_ResourceDropsite); if (cmpResourceDropsite) { Index: binaries/data/mods/public/simulation/components/ResourceSupply.js =================================================================== --- binaries/data/mods/public/simulation/components/ResourceSupply.js +++ binaries/data/mods/public/simulation/components/ResourceSupply.js @@ -277,7 +277,6 @@ return true; this.gatherers.push(gathererID); - Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() }); return true; }; @@ -310,10 +309,7 @@ { let index = this.gatherers.indexOf(gathererID); if (index != -1) - { this.gatherers.splice(index, 1); - Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() }); - } index = this.activeGatherers.indexOf(gathererID); if (index == -1) Index: source/scriptinterface/ScriptInterface.h =================================================================== --- source/scriptinterface/ScriptInterface.h +++ source/scriptinterface/ScriptInterface.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -85,6 +85,16 @@ */ ScriptInterface(const char* nativeScopeName, const char* debugName, const std::shared_ptr& context); + /** + * Alternate constructor. This creates the new Realm in the same Compartment as the neighbor scriptInterface. + * This means that data can be freely exchanged between these two script interfaces without cloning. + * @param nativeScopeName Name of global object that functions (via ScriptFunction::Register) will + * be placed into, as a scoping mechanism; typically "Engine" + * @param debugName Name of this interface for CScriptStats purposes. + * @param scriptInterface 'Neighbor' scriptInterface to share a compartment with. + */ + ScriptInterface(const char* nativeScopeName, const char* debugName, const ScriptInterface& neighbor); + ~ScriptInterface(); struct CmptPrivate Index: source/scriptinterface/ScriptInterface.cpp =================================================================== --- source/scriptinterface/ScriptInterface.cpp +++ source/scriptinterface/ScriptInterface.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -31,6 +31,7 @@ #include "ps/Profile.h" #include +#include #include #define BOOST_MULTI_INDEX_DISABLE_SERIALIZATION @@ -51,7 +52,7 @@ struct ScriptInterface_impl { - ScriptInterface_impl(const char* nativeScopeName, const std::shared_ptr& context); + ScriptInterface_impl(const char* nativeScopeName, const std::shared_ptr& context, JS::Compartment* compartment); ~ScriptInterface_impl(); // Take care to keep this declaration before heap rooted members. Destructors of heap rooted @@ -299,7 +300,7 @@ return true; } -ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const std::shared_ptr& context) : +ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const std::shared_ptr& context, JS::Compartment* compartment) : m_context(context), m_cx(context->GetGeneralJSContext()), m_glob(context->GetGeneralJSContext()), m_nativeScope(context->GetGeneralJSContext()) { JS::RealmCreationOptions creationOpt; @@ -307,6 +308,13 @@ creationOpt.setPreserveJitCode(true); // Enable uneval creationOpt.setToSourceEnabled(true); + + if (compartment) + creationOpt.setExistingCompartment(compartment); + else + // This is the default behaviour. + creationOpt.setNewCompartmentAndZone(); + JS::RealmOptions opt(creationOpt, JS::RealmBehaviors{}); m_glob = JS_NewGlobalObject(m_cx, &global_class, nullptr, JS::OnNewGlobalHookOption::FireOnNewGlobalHook, opt); @@ -340,8 +348,26 @@ } ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const std::shared_ptr& context) : - m(std::make_unique(nativeScopeName, context)) + m(std::make_unique(nativeScopeName, context, nullptr)) +{ + // Profiler stats table isn't thread-safe, so only enable this on the main thread + if (Threading::IsMainThread()) + { + if (g_ScriptStatsTable) + g_ScriptStatsTable->Add(this, debugName); + } + + ScriptRequest rq(this); + m_CmptPrivate.pScriptInterface = this; + JS::SetRealmPrivate(JS::GetObjectRealmOrNull(rq.glob), (void*)&m_CmptPrivate); +} + +ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const ScriptInterface& neighbor) { + ScriptRequest nrq(neighbor); + JS::Compartment* comp = JS::GetCompartmentForRealm(JS::GetCurrentRealmOrNull(nrq.cx)); + m = std::make_unique(nativeScopeName, neighbor.GetContext(), comp); + // Profiler stats table isn't thread-safe, so only enable this on the main thread if (Threading::IsMainThread()) { Index: source/simulation2/components/CCmpAIManager.cpp =================================================================== --- source/simulation2/components/CCmpAIManager.cpp +++ source/simulation2/components/CCmpAIManager.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -57,27 +57,24 @@ * AI is primarily scripted, and the CCmpAIManager component defined here * takes care of managing all the scripts. * - * To avoid slow AI scripts causing jerky rendering, they are run in a background - * thread (maintained by CAIWorker) so that it's okay if they take a whole simulation - * turn before returning their results (though preferably they shouldn't use nearly - * that much CPU). + * The original idea was to run CAIWorker in a separate thread to prevent + * slow AIs from impacting framerate. However, copying the game-state every turn + * proved difficult and rather slow itself (and isn't threadable, obviously). + * For these reasons, the design was changed to a single-thread, same-compartment, different-realm design. + * The AI can therefore directly use the simulation data via the 'Sim' & 'SimEngine' globals. + * As a result, a lof of the code is still designed to be "thread-ready", but this no longer matters. * - * CCmpAIManager grabs the world state after each turn (making use of AIInterface.js - * and AIProxy.js to decide what data to include) then passes it to CAIWorker. - * The AI scripts will then run asynchronously and return a list of commands to execute. - * Any attempts to read the command list (including indirectly via serialization) - * will block until it's actually completed, so the rest of the engine should avoid - * reading it for as long as possible. + * TODO: despite the above, it would still be useful to allow the AI to run tasks asynchronously (and off-thread). + * This could be implemented by having a separate JS runtime in a different thread, + * that runs tasks and returns after a distinct # of simulation turns (to maintain determinism). * - * JS::Values are passed between the game and AI threads using Script::StructuredClone. - * - * TODO: actually the thread isn't implemented yet, because performance hasn't been - * sufficiently problematic to justify the complexity yet, but the CAIWorker interface - * is designed to hopefully support threading when we want it. + * Note also that the RL Interface, by default, uses the 'AI representation'. + * This representation, alimented by the JS AIInterface/AIProxy tandem, is likely to grow smaller over time + * as the AI uses more sim data directly. */ /** - * Implements worker thread for CCmpAIManager. + * AI computation orchestator for CCmpAIManager. */ class CAIWorker { @@ -206,27 +203,36 @@ std::shared_ptr m_ScriptInterface; JS::PersistentRootedValue m_Obj; - std::vector m_Commands; + std::vector m_Commands; }; public: struct SCommandSets { player_id_t player; - std::vector commands; + std::vector commands; }; CAIWorker() : - m_ScriptInterface(new ScriptInterface("Engine", "AI", g_ScriptContext)), m_TurnNum(0), m_CommandsComputed(true), m_HasLoadedEntityTemplates(false), - m_HasSharedComponent(false), - m_EntityTemplates(g_ScriptContext->GetGeneralJSContext()), - m_SharedAIObj(g_ScriptContext->GetGeneralJSContext()), - m_PassabilityMapVal(g_ScriptContext->GetGeneralJSContext()), - m_TerritoryMapVal(g_ScriptContext->GetGeneralJSContext()) + m_HasSharedComponent(false) + { + } + + void Init(const ScriptInterface& simInterface) { + // Create the script interface in the same compartment as the simulation interface. + // This will allow us to directly share data from the sim to the AI (and vice versa, should the need arise). + m_ScriptInterface = std::make_shared("Engine", "AI", simInterface); + + ScriptRequest rq(m_ScriptInterface); + + m_EntityTemplates.init(rq.cx); + m_SharedAIObj.init(rq.cx); + m_PassabilityMapVal.init(rq.cx); + m_TerritoryMapVal.init(rq.cx); m_ScriptInterface->ReplaceNondeterministicRNG(m_RNG); @@ -234,7 +240,15 @@ JS_AddExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), Trace, this); - ScriptRequest rq(m_ScriptInterface); + { + ScriptRequest simrq(simInterface); + // Register the sim globals for easy & explicit access. Mark it replaceable for hotloading. + JS::RootedValue global(rq.cx, simrq.globalValue()); + m_ScriptInterface->SetGlobal("Sim", global, true); + JS::RootedValue scope(rq.cx, JS::ObjectValue(*simrq.nativeScope.get())); + m_ScriptInterface->SetGlobal("SimEngine", scope, true); + } + #define REGISTER_FUNC_NAME(func, name) \ ScriptFunction::Register<&CAIWorker::func, ScriptInterface::ObjectFromCBData>(rq, name); @@ -814,10 +828,6 @@ } } - // Take care to keep this declaration before heap rooted members. Destructors of heap rooted - // members have to be called before the context destructor. - std::shared_ptr m_ScriptContext; - std::shared_ptr m_ScriptInterface; boost::rand48 m_RNG; u32 m_TurnNum; @@ -870,6 +880,8 @@ virtual void Init(const CParamNode& UNUSED(paramNode)) { + m_Worker.Init(GetSimContext().GetScriptInterface()); + m_TerritoriesDirtyID = 0; m_TerritoriesDirtyBlinkingID = 0; m_JustDeserialized = false;