1
0
forked from 0ad/0ad

Get a promise when starting a GUIpage

When calling `Engine.PushGuiPage` a promise is returned. The promise is
settled when the "child" page is closed. That allows to `await` it
inside `async` functions.
Previously the callback was run right inside the call to
`Engine.PopGuiPage`. Now the continuation of the promise is called at
the end of the "tick".

This won't help performance. It will more likely make things worse.
Since gui pages aren't opened or closed that frequently, it doesn't
matter that much.

Refs: 86c151ebaa

For the engine side:
The promise is stored in the `CGUIManager::SGUIPage` (like previously
the callback). When the promise is fulfilled it enqueues a callback in
the `JobQueue` of the `JSContext`.

Original patch by: @wraitii
Comments by: @wraitii, @Stan, @Polakrity, @lyv, @elexis, @vladislavbelov
Differential Revision: https://code.wildfiregames.com/D3807
This was SVN commit r28145.
This commit is contained in:
phosit 2024-07-08 19:07:04 +00:00
parent ae67a77bd9
commit f9114a87f2
22 changed files with 212 additions and 210 deletions

View File

@ -1 +1 @@
Engine.PushGuiPage("regainFocus/page_emptyPage.xml", {}, () => Engine.PopGuiPage());
Engine.PushGuiPage("regainFocus/page_emptyPage.xml").then(Engine.PopGuiPage);

View File

@ -1,6 +1,7 @@
function messageBox(mbWidth, mbHeight, mbMessage, mbTitle, mbButtonCaptions, mbBtnCode, mbCallbackArgs)
async function messageBox(mbWidth, mbHeight, mbMessage, mbTitle, mbButtonCaptions, mbBtnCode,
mbCallbackArgs)
{
Engine.PushGuiPage(
const btnCode = await Engine.PushGuiPage(
"page_msgbox.xml",
{
"width": mbWidth,
@ -8,16 +9,16 @@ function messageBox(mbWidth, mbHeight, mbMessage, mbTitle, mbButtonCaptions, mbB
"message": mbMessage,
"title": mbTitle,
"buttonCaptions": mbButtonCaptions
},
btnCode => {
if (mbBtnCode !== undefined && mbBtnCode[btnCode])
mbBtnCode[btnCode](mbCallbackArgs ? mbCallbackArgs[btnCode] : undefined);
});
if (mbBtnCode !== undefined && mbBtnCode[btnCode])
mbBtnCode[btnCode](mbCallbackArgs ? mbCallbackArgs[btnCode] : undefined);
}
function timedConfirmation(width, height, message, timeParameter, timeout, title, buttonCaptions, btnCode, callbackArgs)
async function timedConfirmation(width, height, message, timeParameter, timeout, title, buttonCaptions,
btnCode, callbackArgs)
{
Engine.PushGuiPage(
const button = await Engine.PushGuiPage(
"page_timedconfirmation.xml",
{
"width": width,
@ -27,22 +28,10 @@ function timedConfirmation(width, height, message, timeParameter, timeout, title
"timeout": timeout,
"title": title,
"buttonCaptions": buttonCaptions
},
button => {
if (btnCode !== undefined && btnCode[button])
btnCode[button](callbackArgs ? callbackArgs[button] : undefined);
});
}
function colorMixer(color, callback)
{
Engine.PushGuiPage(
"page_colormixer.xml",
color,
result => {
callback(result);
}
);
if (btnCode !== undefined && btnCode[button])
btnCode[button](callbackArgs ? callbackArgs[button] : undefined);
}
function openURL(url)

View File

@ -5,9 +5,9 @@ function initTerms(terms)
g_Terms = terms;
}
function openTerms(page)
async function openTerms(page)
{
Engine.PushGuiPage(
const data = await Engine.PushGuiPage(
"page_termsdialog.xml",
{
"file": g_Terms[page].file,
@ -16,19 +16,17 @@ function openTerms(page)
"urlButtons": g_Terms[page].urlButtons || [],
"termsURL": g_Terms[page].termsURL || undefined,
"page": page
},
data => {
g_Terms[data.page].accepted = data.accepted;
});
Engine.ConfigDB_CreateAndSaveValue(
"user",
g_Terms[data.page].config,
data.accepted ? getTermsHash(data.page) : "0");
g_Terms[data.page].accepted = data.accepted;
if (g_Terms[data.page].callback)
g_Terms[data.page].callback(data);
}
);
Engine.ConfigDB_CreateAndSaveValue(
"user",
g_Terms[data.page].config,
data.accepted ? getTermsHash(data.page) : "0");
if (g_Terms[data.page].callback)
g_Terms[data.page].callback(data);
}
function checkTerms()

View File

@ -23,8 +23,11 @@ function downloadModsButton()
openTerms("Disclaimer");
}
function openModIo(data)
async function openModIo(data)
{
if (data.accepted)
Engine.PushGuiPage("page_modio.xml", {}, initMods);
if (!data.accepted)
return;
await Engine.PushGuiPage("page_modio.xml");
initMods();
}

View File

@ -19,16 +19,21 @@ class CampaignMenu extends AutoWatcher
this.levelSelection.onMouseLeftDoubleClickItem = () => this.startScenario();
Engine.GetGUIObjectByName('startButton').onPress = () => this.startScenario();
Engine.GetGUIObjectByName('backToMain').onPress = () => this.goBackToMainMenu();
Engine.GetGUIObjectByName('savedGamesButton').onPress = Engine.PushGuiPage.bind(Engine,
'page_loadgame.xml', { "campaignRun": this.run.filename }, this.loadSavegame.bind(this));
Engine.GetGUIObjectByName('savedGamesButton').onPress = this.loadSavegame.bind(this);
this.mapCache = new MapCache();
this._ready = true;
}
loadSavegame(gameId)
async loadSavegame()
{
const gameId = await Engine.PushGuiPage(
'page_loadgame.xml',
{
"campaignRun": this.run.filename
});
if (!gameId)
return;

View File

@ -288,19 +288,15 @@ function getBuildString()
* property that page is opened with the @a args property of that object.
* That continues untill there is no @a nextPage property in the completion
* value. If there is no @a nextPage in the completion value the
* @a continuation is called with the completion value.
* @a completionValue is returned.
* @param {String} page - The page first opened.
* @param args - passed to the first page opened.
* @param continuation {function | undefined} - Completion callback, called when
* there is no @a nextPage property in the completion value.
*/
function pageLoop(page, args, continuation)
async function pageLoop(page, args)
{
(function recursiveFunction(completionValue)
{
if (completionValue?.nextPage != null)
Engine.PushGuiPage(completionValue.nextPage, completionValue.args, recursiveFunction);
else
continuation?.(completionValue);
})({ "nextPage": page, "args": args });
let completionValue = { "nextPage": page, "args": args };
while (completionValue?.nextPage != null)
completionValue = await Engine.PushGuiPage(completionValue.nextPage, completionValue.args);
return completionValue;
}

View File

@ -23,9 +23,9 @@ class CivInfoButton
this.openPage(this.civInfo.page);
}
openPage(page)
async openPage(page)
{
pageLoop(page, this.civInfo.args, data => this.civInfo = data);
this.civInfo = await pageLoop(page, this.civInfo.args);
}
}

View File

@ -52,14 +52,11 @@ function languageSelectionChanged()
localeText.caption = locale;
}
function openAdvancedMenu()
async function openAdvancedMenu()
{
let localeText = Engine.GetGUIObjectByName("localeText");
Engine.PushGuiPage("page_locale_advanced.xml", { "locale": localeText.caption }, applyFromAdvancedMenu);
}
const locale = await Engine.PushGuiPage("page_locale_advanced.xml", { "locale": localeText.caption });
function applyFromAdvancedMenu(locale)
{
if (!locale)
return;
@ -72,6 +69,5 @@ function applyFromAdvancedMenu(locale)
if (index != -1)
languageList.selected = index;
var localeText = Engine.GetGUIObjectByName("localeText");
localeText.caption = locale;
}

View File

@ -80,17 +80,14 @@ var g_OptionType = {
control.caption = value;
},
"initGUI": (option, control) => {
control.children[2].onPress = () => {
colorMixer(
control.caption,
(color) => {
if (color != control.caption)
{
control.caption = color;
control.onTextEdit();
}
}
);
control.children[2].onPress = async() => {
const color = await Engine.PushGuiPage("page_colormixer.xml", control.caption);
if (color != control.caption)
{
control.caption = color;
control.onTextEdit();
}
};
},
"guiToValue": control => control.caption,

View File

@ -90,31 +90,32 @@ var g_MainMenuItems = [
{
"caption": translate("Load Game"),
"tooltip": translate("Load a saved game."),
"onPress": Engine.PushGuiPage.bind(Engine, "page_loadgame.xml", {}, (gameId) =>
"onPress": async() => {
const gameId = await Engine.PushGuiPage("page_loadgame.xml");
if (!gameId)
return;
const metadata = Engine.StartSavedGame(gameId);
if (!metadata)
{
if (!gameId)
return;
error("Could not load saved game: " + gameId);
return;
}
const metadata = Engine.StartSavedGame(gameId);
if (!metadata)
{
error("Could not load saved game: " + gameId);
return;
}
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": metadata.initAttributes,
"playerAssignments": {
"local": {
"name": metadata.initAttributes.settings.
PlayerData[metadata.playerID]?.Name ??
singleplayerName(),
"player": metadata.playerID
}
},
"savedGUIData": metadata.gui
});
})
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": metadata.initAttributes,
"playerAssignments": {
"local": {
"name": metadata.initAttributes.settings.
PlayerData[metadata.playerID]?.Name ??
singleplayerName(),
"player": metadata.playerID
}
},
"savedGUIData": metadata.gui
});
}
},
{
"caption": translate("Continue Campaign"),
@ -221,11 +222,8 @@ var g_MainMenuItems = [
{
"caption": translate("Options"),
"tooltip": translate("Adjust game settings."),
"onPress": () => {
Engine.PushGuiPage(
"page_options.xml",
{},
fireConfigChangeHandlers);
"onPress": async() => {
fireConfigChangeHandlers(await Engine.PushGuiPage("page_options.xml"));
}
},
{

View File

@ -16,11 +16,12 @@ MenuButtons.prototype.Manual = class
this.pauseControl = pauseControl;
}
onPress()
async onPress()
{
closeOpenDialogs();
this.pauseControl.implicitPause();
Engine.PushGuiPage("page_manual.xml", {}, resumeGame);
await Engine.PushGuiPage("page_manual.xml");
resumeGame();
}
};
@ -54,18 +55,18 @@ MenuButtons.prototype.Save = class
this.pauseControl = pauseControl;
}
onPress()
async onPress()
{
closeOpenDialogs();
this.pauseControl.implicitPause();
Engine.PushGuiPage(
await Engine.PushGuiPage(
"page_loadgame.xml",
{
"savedGameData": getSavedGameData(),
"campaignRun": g_CampaignSession ? g_CampaignSession.run.filename : null
},
resumeGame);
});
resumeGame();
}
};
@ -92,7 +93,7 @@ MenuButtons.prototype.Summary = class
});
}
onPress()
async onPress()
{
if (Engine.IsAtlasRunning())
return;
@ -102,7 +103,7 @@ MenuButtons.prototype.Summary = class
// Allows players to see their own summary.
// If they have shared ally vision researched, they are able to see the summary of there allies too.
let simState = Engine.GuiInterfaceCall("GetExtendedSimulationState");
Engine.PushGuiPage(
const data = await Engine.PushGuiPage(
"page_summary.xml",
{
"sim": {
@ -117,11 +118,10 @@ MenuButtons.prototype.Summary = class
"isInGame": true,
"summarySelection": this.summarySelection
},
},
data => {
this.summarySelection = data.summarySelection;
this.pauseControl.implicitResume();
});
this.summarySelection = data.summarySelection;
this.pauseControl.implicitResume();
}
};
@ -162,18 +162,13 @@ MenuButtons.prototype.Options = class
this.pauseControl = pauseControl;
}
onPress()
async onPress()
{
closeOpenDialogs();
this.pauseControl.implicitPause();
Engine.PushGuiPage(
"page_options.xml",
{},
changes => {
fireConfigChangeHandlers(changes);
resumeGame();
});
fireConfigChangeHandlers(await Engine.PushGuiPage("page_options.xml"));
resumeGame();
}
};
@ -186,15 +181,13 @@ MenuButtons.prototype.Hotkeys = class
this.pauseControl = pauseControl;
}
onPress()
async onPress()
{
closeOpenDialogs();
this.pauseControl.implicitPause();
Engine.PushGuiPage(
"hotkeys/page_hotkeys.xml",
{},
() => { resumeGame(); });
await Engine.PushGuiPage("hotkeys/page_hotkeys.xml");
resumeGame();
}
};

View File

@ -4,11 +4,12 @@
*/
class SessionMessageBox
{
display()
async display()
{
this.onPageOpening();
closeOpenDialogs();
g_PauseControl.implicitPause();
Engine.PushGuiPage(
const buttonId = await Engine.PushGuiPage(
"page_msgbox.xml",
{
"width": this.Width,
@ -16,18 +17,8 @@ class SessionMessageBox
"title": this.Title,
"message": this.Caption,
"buttonCaptions": this.Buttons ? this.Buttons.map(button => button.caption) : undefined,
},
this.onPageClosed.bind(this));
}
});
onPageOpening()
{
closeOpenDialogs();
g_PauseControl.implicitPause();
}
onPageClosed(buttonId)
{
if (this.Buttons && this.Buttons[buttonId].onPress)
this.Buttons[buttonId].onPress.call(this);

View File

@ -1262,19 +1262,19 @@ function initSelectionPanels()
*
* @param {string} [civCode] - The template name of the entity that researches the selected technology.
*/
function showTemplateDetails(templateName, civCode)
async function showTemplateDetails(templateName, civCode)
{
if (inputState != INPUT_NORMAL)
return;
g_PauseControl.implicitPause();
Engine.PushGuiPage(
await Engine.PushGuiPage(
"page_viewer.xml",
{
"templateName": templateName,
"civ": civCode
},
resumeGame);
});
resumeGame();
}
/**

View File

@ -29,12 +29,12 @@ class CivIcon
this.openPage(this.dialogSelection.page);
}
openPage(page)
async openPage(page)
{
closeOpenDialogs();
g_PauseControl.implicitPause();
pageLoop(
this.dialogSelection = await pageLoop(
page,
{
// If an Observer triggers `openPage()` via hotkey, g_ViewedPlayer could be -1 or 0
@ -42,12 +42,8 @@ class CivIcon
"civ": this.dialogSelection.args.civ ?? g_Players[Math.max(g_ViewedPlayer, 1)].civ
// TODO add info about researched techs and unlocked entities
},
data =>
{
this.dialogSelection = data;
resumeGame();
});
resumeGame();
}
rebuild()

View File

@ -30,6 +30,7 @@
#include "ps/XML/Xeromyces.h"
#include "scriptinterface/FunctionWrapper.h"
#include "scriptinterface/Object.h"
#include "scriptinterface/Promises.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/StructuredClone.h"
@ -112,24 +113,28 @@ void CGUIManager::SwitchPage(const CStrW& pageName, const ScriptInterface* srcSc
m_PageStack.clear();
}
PushPage(pageName, initDataClone, JS::UndefinedHandleValue);
PushPage(pageName, initDataClone);
}
void CGUIManager::PushPage(const CStrW& pageName, Script::StructuredClone initData, JS::HandleValue callbackFunction)
JS::Value CGUIManager::PushPage(const CStrW& pageName, Script::StructuredClone initData)
{
// Store the callback handler in the current GUI page before opening the new one
if (!m_PageStack.empty() && !callbackFunction.isUndefined())
{
m_PageStack.back().SetCallbackFunction(m_ScriptInterface, callbackFunction);
JS::RootedValue promise{m_ScriptInterface.GetGeneralJSContext(), [&]
{
if (m_PageStack.empty())
return JS::UndefinedValue();
// Make sure we unfocus anything on the current page.
m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS);
}
// Make sure we unfocus anything on the current page.
m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS);
return m_PageStack.back().ReplacePromise(m_ScriptInterface);
}()};
// Push the page prior to loading its contents, because that may push
// another GUI page on init which should be pushed on top of this new page.
m_PageStack.emplace_back(pageName, initData);
m_PageStack.back().LoadPage(m_ScriptContext);
return promise;
}
void CGUIManager::PopPage(Script::StructuredClone args)
@ -144,7 +149,7 @@ void CGUIManager::PopPage(Script::StructuredClone args)
m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS);
m_PageStack.pop_back();
m_PageStack.back().PerformCallbackFunction(args);
m_PageStack.back().ResolvePromise(args);
// We return to a page where some object might have been focused.
m_PageStack.back().gui->SendFocusMessage(GUIM_GOT_FOCUS);
@ -245,26 +250,15 @@ void CGUIManager::SGUIPage::LoadPage(ScriptContext& scriptContext)
LOGERROR("GUI page '%s': Failed to call init() function", utf8_from_wstring(m_Name));
}
void CGUIManager::SGUIPage::SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc)
JS::Value CGUIManager::SGUIPage::ReplacePromise(ScriptInterface& scriptInterface)
{
if (!callbackFunc.isObject())
{
LOGERROR("Given callback handler is not an object!");
return;
}
ScriptRequest rq(scriptInterface);
if (!JS_ObjectIsFunction(&callbackFunc.toObject()))
{
LOGERROR("Given callback handler is not a function!");
return;
}
callbackFunction = std::make_shared<JS::PersistentRootedValue>(scriptInterface.GetGeneralJSContext(), callbackFunc);
JSContext* generalContext{scriptInterface.GetGeneralJSContext()};
callbackFunction = std::make_shared<JS::PersistentRootedObject>(generalContext,
JS::NewPromiseObject(generalContext, nullptr));
return JS::ObjectValue(**callbackFunction);
}
void CGUIManager::SGUIPage::PerformCallbackFunction(Script::StructuredClone args)
void CGUIManager::SGUIPage::ResolvePromise(Script::StructuredClone args)
{
if (!callbackFunction)
return;
@ -274,7 +268,7 @@ void CGUIManager::SGUIPage::PerformCallbackFunction(Script::StructuredClone args
JS::RootedObject globalObj(rq.cx, rq.glob);
JS::RootedValue funcVal(rq.cx, *callbackFunction);
JS::RootedObject funcVal(rq.cx, *callbackFunction);
// Delete the callback function, so that it is not called again
callbackFunction.reset();
@ -283,13 +277,8 @@ void CGUIManager::SGUIPage::PerformCallbackFunction(Script::StructuredClone args
if (args)
Script::ReadStructuredClone(rq, args, &argVal);
JS::RootedValueVector paramData(rq.cx);
ignore_result(paramData.append(argVal));
JS::RootedValue result(rq.cx);
if(!JS_CallFunctionValue(rq.cx, globalObj, funcVal, paramData, &result))
ScriptException::CatchPending(rq);
// This only resolves the promise, it doesn't call the continuation.
JS::ResolvePromise(rq.cx, funcVal, argVal);
}
Status CGUIManager::ReloadChangedFile(const VfsPath& path)
@ -388,6 +377,8 @@ void CGUIManager::TickObjects()
for (const SGUIPage& p : pageStack)
p.gui->TickObjects();
m_ScriptContext.RunJobs();
}
void CGUIManager::Draw(CCanvas2D& canvas) const

View File

@ -68,9 +68,9 @@ public:
* Load a new GUI page and make it active. All current pages will be retained,
* and will still be drawn and receive tick events, but will not receive
* user inputs.
* If given, the callbackHandler function will be executed once this page is closed.
* The returned promise will be fulfilled once the pushed page is closed.
*/
void PushPage(const CStrW& pageName, Script::StructuredClone initData, JS::HandleValue callbackFunc);
JS::Value PushPage(const CStrW& pageName, Script::StructuredClone initData);
/**
* Unload the currently active GUI page, and make the previous page active.
@ -146,16 +146,17 @@ private:
void LoadPage(ScriptContext& scriptContext);
/**
* Sets the callback handler when a new page is opened that will be performed when the page is closed.
* A new promise gets set. A reference to that promise is returned. The promise will settle when
* the page is closed.
*/
void SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc);
JS::Value ReplacePromise(ScriptInterface& scriptInterface);
/**
* Execute the stored callback function with the given arguments.
*/
void PerformCallbackFunction(Script::StructuredClone args);
void ResolvePromise(Script::StructuredClone args);
CStrW m_Name;
std::wstring m_Name;
std::unordered_set<VfsPath> inputs; // for hotloading
Script::StructuredClone initData; // data to be passed to the init() function
std::shared_ptr<CGUI> gui; // the actual GUI page
@ -164,7 +165,7 @@ private:
* Function executed by this parent GUI page when the child GUI page it pushed is popped.
* Notice that storing it in the SGUIPage instead of CGUI means that it will survive the hotloading CGUI reset.
*/
std::shared_ptr<JS::PersistentRootedValue> callbackFunction;
std::shared_ptr<JS::PersistentRootedObject> callbackFunction;
};
std::shared_ptr<CGUI> top() const;
@ -176,8 +177,22 @@ private:
* The page stack must not move pointers on push/pop, or pushing a page in a page's init method
* may crash (as the pusher page will suddenly have moved, and the stack will be confused).
* Therefore use std::deque over std::vector.
* Also the elements have to be destructed back to front.
*/
using PageStackType = std::deque<SGUIPage>;
class PageStackType : public std::deque<SGUIPage>
{
public:
~PageStackType()
{
clear();
}
void clear()
{
while (!std::deque<SGUIPage>::empty())
std::deque<SGUIPage>::pop_back();
}
};
PageStackType m_PageStack;
CTemplateLoader m_TemplateLoader;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2021 Wildfire Games.
/* Copyright (C) 2024 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -32,9 +32,10 @@ namespace JSI_GUIManager
{
// Note that the initData argument may only contain clonable data.
// Functions aren't supported for example!
void PushGuiPage(const ScriptRequest& rq, const std::wstring& name, JS::HandleValue initData, JS::HandleValue callbackFunction)
// It returns a promise.
JS::Value PushGuiPage(const ScriptRequest& rq, const std::wstring& name, JS::HandleValue initData)
{
g_GUI->PushPage(name, Script::WriteStructuredClone(rq, initData), callbackFunction);
return g_GUI->PushPage(name, Script::WriteStructuredClone(rq, initData));
}
void SwitchGuiPage(const ScriptInterface& scriptInterface, const std::wstring& name, JS::HandleValue initData)

View File

@ -26,6 +26,7 @@
#include "ps/GameSetup/GameSetup.h"
#include "ps/Hotkey.h"
#include "ps/XML/Xeromyces.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptRequest.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/StructuredClone.h"
@ -72,7 +73,7 @@ public:
Script::CreateObject(rq, &val);
Script::StructuredClone data = Script::WriteStructuredClone(rq, JS::NullHandleValue);
g_GUI->PushPage(L"event/page_event.xml", data, JS::UndefinedHandleValue);
g_GUI->PushPage(L"event/page_event.xml", data);
const ScriptInterface& pageScriptInterface = *(g_GUI->GetActiveGUI()->GetScriptInterface());
ScriptRequest prq(pageScriptInterface);
@ -135,7 +136,7 @@ public:
Script::CreateObject(rq, &val);
Script::StructuredClone data = Script::WriteStructuredClone(rq, JS::NullHandleValue);
g_GUI->PushPage(L"hotkey/page_hotkey.xml", data, JS::UndefinedHandleValue);
g_GUI->PushPage(L"hotkey/page_hotkey.xml", data);
// Press 'a'.
SDL_Event_ hotkeyNotification;
@ -208,20 +209,28 @@ public:
JS::RootedValue val(rq.cx);
Script::CreateObject(rq, &val);
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 0);
Script::StructuredClone data = Script::WriteStructuredClone(rq, JS::NullHandleValue);
g_GUI->PushPage(L"regainFocus/page_emptyPage.xml", data, JS::UndefinedHandleValue);
g_GUI->PushPage(L"regainFocus/page_emptyPage.xml", data);
const ScriptInterface& pageScriptInterface = *(g_GUI->GetActiveGUI()->GetScriptInterface());
ScriptRequest prq(pageScriptInterface);
JS::RootedValue global(prq.cx, prq.globalValue());
g_GUI->PushPage(L"regainFocus/page_emptyPage.xml", data, JS::UndefinedHandleValue);
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 1);
g_GUI->PushPage(L"regainFocus/page_emptyPage.xml", data);
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 2);
g_GUI->PopPage(data);
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 1);
// This page instantly pushes an empty page with a callback that pops another page again.
g_GUI->PushPage(L"regainFocus/page_pushWithPopOnInit.xml", data, JS::UndefinedHandleValue);
g_GUI->PushPage(L"regainFocus/page_pushWithPopOnInit.xml", data);
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 3);
// Pop the empty page and trigger the callback (effectively pops twice).
// Pop the empty page
g_GUI->PopPage(data);
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 2);
scriptInterface->GetContext().RunJobs();
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 1);
}
};

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2023 Wildfire Games.
/* Copyright (C) 2024 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -22,6 +22,7 @@
#include "lib/alignment.h"
#include "ps/GameSetup/Config.h"
#include "ps/Profile.h"
#include "scriptinterface/Promises.h"
#include "scriptinterface/ScriptExtraHeaders.h"
#include "scriptinterface/ScriptEngine.h"
#include "scriptinterface/ScriptInterface.h"
@ -83,10 +84,9 @@ std::shared_ptr<ScriptContext> ScriptContext::CreateContext(int contextSize, int
}
ScriptContext::ScriptContext(int contextSize, int heapGrowthBytesGCTrigger):
m_LastGCBytes(0),
m_LastGCCheck(0.0f),
m_HeapGrowthBytesGCTrigger(heapGrowthBytesGCTrigger),
m_ContextSize(contextSize)
m_JobQueue{std::make_unique<Script::JobQueue>()},
m_ContextSize{contextSize},
m_HeapGrowthBytesGCTrigger{heapGrowthBytesGCTrigger}
{
ENSURE(ScriptEngine::IsInitialised() && "The ScriptEngine must be initialized before constructing any ScriptContexts!");
@ -130,6 +130,8 @@ ScriptContext::ScriptContext(int contextSize, int heapGrowthBytesGCTrigger):
JS::ContextOptionsRef(m_cx).setStrictMode(true);
ScriptEngine::GetSingleton().RegisterContext(m_cx);
JS::SetJobQueue(m_cx, m_JobQueue.get());
}
ScriptContext::~ScriptContext()
@ -268,6 +270,11 @@ void ScriptContext::ShrinkingGC()
JS_SetGCParameter(m_cx, JSGC_PER_ZONE_GC_ENABLED, false);
}
void ScriptContext::RunJobs()
{
m_JobQueue->runJobs(m_cx);
}
void ScriptContext::PrepareZonesForIncrementalGC() const
{
for (JS::Realm* const& realm : m_Realms)

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2023 Wildfire Games.
/* Copyright (C) 2024 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -27,6 +27,11 @@
constexpr int DEFAULT_CONTEXT_SIZE = 16 * 1024 * 1024;
constexpr int DEFAULT_HEAP_GROWTH_BYTES_GCTRIGGER = 2 * 1024 * 1024;
namespace Script
{
class JobQueue;
}
/**
* Abstraction around a SpiderMonkey JSContext.
*
@ -74,6 +79,14 @@ public:
void RegisterRealm(JS::Realm* realm);
void UnRegisterRealm(JS::Realm* realm);
/**
* Runs the promise continuation.
* On contexts where promises can be used this function has to be
* called.
* This function has to be called frequently.
*/
void RunJobs();
/**
* GetGeneralJSContext returns the context without starting a GC request and without
* entering any compartment. It should only be used in specific situations, such as
@ -86,14 +99,15 @@ public:
private:
JSContext* m_cx;
const std::unique_ptr<Script::JobQueue> m_JobQueue;
void PrepareZonesForIncrementalGC() const;
std::list<JS::Realm*> m_Realms;
int m_ContextSize;
int m_HeapGrowthBytesGCTrigger;
int m_LastGCBytes;
double m_LastGCCheck;
int m_LastGCBytes{0};
double m_LastGCCheck{0.0};
};
// Using a global object for the context is a workaround until Simulation, AI, etc,

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2023 Wildfire Games.
/* Copyright (C) 2024 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -384,6 +384,7 @@ ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugN
ScriptInterface::~ScriptInterface()
{
m->m_context.RunJobs();
if (Threading::IsMainThread())
{
if (g_ScriptStatsTable)

View File

@ -585,6 +585,8 @@ void CSimulation2Impl::UpdateComponents(CSimContext& simContext, fixed turnLengt
cmpPathfinder->UpdateGrid();
cmpPathfinder->StartProcessingMoves(false);
}
componentManager.GetScriptInterface().GetContext().RunJobs();
}
void CSimulation2Impl::Interpolate(float simFrameLength, float frameOffset, float realFrameLength)