Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js @@ -590,6 +590,8 @@ this._entityModif = sharedAI._entitiesModifications.get(entity.id); }, + "queryInterface": function(iid) { return SimEngine.QueryInterface(this.id(), iid) }, + "toString": function() { return "[Entity " + this.id() + " " + this.templateName() + "]"; }, "id": function() { return this._entity.id; }, @@ -679,24 +681,25 @@ }, "resourceSupplyAmount": function() { - return this._entity.resourceSupplyAmount; + return this.queryInterface(Sim.IID_ResourceSupply)?.GetCurrentAmount(); }, "resourceSupplyNumGatherers": function() { - return this._entity.resourceSupplyNumGatherers; + return this.queryInterface(Sim.IID_ResourceSupply)?.GetNumGatherers(); }, "isFull": function() { - if (this._entity.resourceSupplyNumGatherers !== undefined) - return this.maxGatherers() === this._entity.resourceSupplyNumGatherers; + let numGatherers = this.resourceSupplyNumGatherers(); + if (numGatherers) + return this.maxGatherers() === numGatherers; return undefined; }, "resourceCarrying": function() { - return this._entity.resourceCarrying; + return this.queryInterface(Sim.IID_ResourceGatherer)?.GetCarryingStatus(); }, "currentGatherRate": function() { Index: ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js +++ ps/trunk/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: ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js +++ ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js @@ -75,8 +75,6 @@ { for (let resource of resources) this.carrying[resource.type] = +resource.amount; - - Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() }); }; /** @@ -284,8 +282,6 @@ if (cmpStatisticsTracker) cmpStatisticsTracker.IncreaseResourceGatheredCounter(type.generic, status.amount, type.specific); - Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() }); - if (!this.CanCarryMore(type.generic)) this.StopGathering("InventoryFilled"); else if (status.exhausted) @@ -399,17 +395,12 @@ return; let change = cmpResourceDropsite.ReceiveResources(this.carrying, this.entity); - let changed = false; for (let type in change) { this.carrying[type] -= change[type]; if (this.carrying[type] == 0) delete this.carrying[type]; - changed = true; } - - if (changed) - Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() }); }; /** @@ -420,8 +411,6 @@ ResourceGatherer.prototype.DropResources = function() { this.carrying = {}; - - Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() }); }; /** Index: ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js +++ ps/trunk/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: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js @@ -1,7 +1 @@ Engine.RegisterInterface("ResourceGatherer"); - -/** - * Message of the form { "to": [{ "type": string, "amount": number, "max": number }] } - * sent from ResourceGatherer component whenever the amount of carried resources changes. - */ -Engine.RegisterMessageType("ResourceCarryingChanged"); Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js @@ -5,9 +5,3 @@ * sent from ResourceSupply component whenever the supply level changes. */ Engine.RegisterMessageType("ResourceSupplyChanged"); - -/** - * Message of the form { "to": number } - * sent from ResourceSupply component whenever the number of gatherer changes. - */ -Engine.RegisterMessageType("ResourceSupplyNumGatherersChanged"); Index: ps/trunk/source/scriptinterface/ScriptInterface.h =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.h +++ ps/trunk/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: ps/trunk/source/scriptinterface/ScriptInterface.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.cpp +++ ps/trunk/source/scriptinterface/ScriptInterface.cpp @@ -51,7 +51,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 +299,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 +307,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,7 +347,7 @@ } 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()) @@ -354,6 +361,24 @@ 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()) + { + 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() { if (Threading::IsMainThread()) Index: ps/trunk/source/simulation2/Simulation2.cpp =================================================================== --- ps/trunk/source/simulation2/Simulation2.cpp +++ ps/trunk/source/simulation2/Simulation2.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 @@ -510,11 +510,6 @@ if (m_EnableOOSLog) DumpState(); - // Start computing AI for the next turn - CmpPtr cmpAIManager(m_SimContext, SYSTEM_ENTITY); - if (cmpAIManager) - cmpAIManager->StartComputation(); - ++m_TurnNumber; } @@ -535,10 +530,6 @@ componentManager.BroadcastMessage(msgTurnStart); } - // Push AI commands onto the queue before we use them - CmpPtr cmpAIManager(simContext, SYSTEM_ENTITY); - if (cmpAIManager) - cmpAIManager->PushCommands(); CmpPtr cmpCommandQueue(simContext, SYSTEM_ENTITY); if (cmpCommandQueue) @@ -583,6 +574,14 @@ // Clean up any entities destroyed during the simulation update componentManager.FlushDestroyedComponents(); + // Compute AI immediately at turn's end. + CmpPtr cmpAIManager(simContext, SYSTEM_ENTITY); + if (cmpAIManager) + { + cmpAIManager->StartComputation(); + cmpAIManager->PushCommands(); + } + // Process all remaining moves if (cmpPathfinder) { Index: ps/trunk/source/simulation2/components/CCmpAIManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpAIManager.cpp +++ ps/trunk/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,43 @@ 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) + { + } + + ~CAIWorker() + { + // Init will always be called. + JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), Trace, this); + } + + 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 +247,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); @@ -253,11 +274,7 @@ // Globalscripts may use VFS script functions m_ScriptInterface->LoadGlobalScripts(); - } - ~CAIWorker() - { - JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), Trace, this); } bool HasLoadedEntityTemplates() const { return m_HasLoadedEntityTemplates; } @@ -814,10 +831,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 +883,8 @@ virtual void Init(const CParamNode& UNUSED(paramNode)) { + m_Worker.Init(GetSimContext().GetScriptInterface()); + m_TerritoriesDirtyID = 0; m_TerritoriesDirtyBlinkingID = 0; m_JustDeserialized = false;