Index: ps/trunk/source/ps/ThreadUtil.cpp =================================================================== --- ps/trunk/source/ps/ThreadUtil.cpp (revision 24529) +++ ps/trunk/source/ps/ThreadUtil.cpp (nonexistent) @@ -1,41 +0,0 @@ -/* Copyright (C) 2019 Wildfire Games. - * This file is part of 0 A.D. - * - * 0 A.D. is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * 0 A.D. is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with 0 A.D. If not, see . - */ - -#include "precompiled.h" - -#include - -#include "ThreadUtil.h" - -static bool g_MainThreadSet; -static std::thread::id g_MainThread; - -bool ThreadUtil::IsMainThread() -{ - // If SetMainThread hasn't been called yet, this is probably being - // called at static initialisation time, so it must be the main thread - if (!g_MainThreadSet) - return true; - - return g_MainThread == std::this_thread::get_id(); -} - -void ThreadUtil::SetMainThread() -{ - g_MainThread = std::this_thread::get_id(); - g_MainThreadSet = true; -} Property changes on: ps/trunk/source/ps/ThreadUtil.cpp ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/source/graphics/MapGenerator.cpp =================================================================== --- ps/trunk/source/graphics/MapGenerator.cpp (revision 24529) +++ ps/trunk/source/graphics/MapGenerator.cpp (revision 24530) @@ -1,433 +1,432 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "MapGenerator.h" #include "graphics/MapIO.h" #include "graphics/Patch.h" #include "graphics/Terrain.h" #include "lib/external_libraries/libsdl.h" #include "lib/status.h" #include "lib/timer.h" #include "lib/file/vfs/vfs_path.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/FileIo.h" #include "ps/Profile.h" +#include "ps/Threading.h" #include "ps/scripting/JSInterface_VFS.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptConversions.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/helpers/MapEdgeTiles.h" #include #include // TODO: Maybe this should be optimized depending on the map size. constexpr int RMS_CONTEXT_SIZE = 96 * 1024 * 1024; extern bool IsQuitRequested(); static bool MapGeneratorInterruptCallback(JSContext* UNUSED(cx)) { // This may not use SDL_IsQuitRequested(), because it runs in a thread separate to SDL, see SDL_PumpEvents if (IsQuitRequested()) { LOGWARNING("Quit requested!"); return false; } return true; } CMapGeneratorWorker::CMapGeneratorWorker(ScriptInterface* scriptInterface) : m_ScriptInterface(scriptInterface) { // If something happens before we initialize, that's a failure m_Progress = -1; } CMapGeneratorWorker::~CMapGeneratorWorker() { // Wait for thread to end if (m_WorkerThread.joinable()) m_WorkerThread.join(); } void CMapGeneratorWorker::Initialize(const VfsPath& scriptFile, const std::string& settings) { std::lock_guard lock(m_WorkerMutex); // Set progress to positive value m_Progress = 1; m_ScriptPath = scriptFile; m_Settings = settings; // Launch the worker thread - m_WorkerThread = std::thread(RunThread, this); + m_WorkerThread = std::thread(Threading::HandleExceptions::Wrapper, this); } -void* CMapGeneratorWorker::RunThread(CMapGeneratorWorker* self) +void CMapGeneratorWorker::RunThread(CMapGeneratorWorker* self) { debug_SetThreadName("MapGenerator"); g_Profiler2.RegisterCurrentThread("MapGenerator"); shared_ptr mapgenContext = ScriptContext::CreateContext(RMS_CONTEXT_SIZE); // Enable the script to be aborted JS_AddInterruptCallback(mapgenContext->GetGeneralJSContext(), MapGeneratorInterruptCallback); self->m_ScriptInterface = new ScriptInterface("Engine", "MapGenerator", mapgenContext); // Run map generation scripts if (!self->Run() || self->m_Progress > 0) { // Don't leave progress in an unknown state, if generator failed, set it to -1 std::lock_guard lock(self->m_WorkerMutex); self->m_Progress = -1; } SAFE_DELETE(self->m_ScriptInterface); // At this point the random map scripts are done running, so the thread has no further purpose // and can die. The data will be stored in m_MapData already if successful, or m_Progress // will contain an error value on failure. - - return NULL; } bool CMapGeneratorWorker::Run() { ScriptRequest rq(m_ScriptInterface); // Parse settings JS::RootedValue settingsVal(rq.cx); if (!m_ScriptInterface->ParseJSON(m_Settings, &settingsVal) && settingsVal.isUndefined()) { LOGERROR("CMapGeneratorWorker::Run: Failed to parse settings"); return false; } // Prevent unintentional modifications to the settings object by random map scripts if (!m_ScriptInterface->FreezeObject(settingsVal, true)) { LOGERROR("CMapGeneratorWorker::Run: Failed to deepfreeze settings"); return false; } // Init RNG seed u32 seed = 0; if (!m_ScriptInterface->HasProperty(settingsVal, "Seed") || !m_ScriptInterface->GetProperty(settingsVal, "Seed", seed)) LOGWARNING("CMapGeneratorWorker::Run: No seed value specified - using 0"); InitScriptInterface(seed); RegisterScriptFunctions_MapGenerator(); // Copy settings to global variable JS::RootedValue global(rq.cx, rq.globalValue()); if (!m_ScriptInterface->SetProperty(global, "g_MapSettings", settingsVal, true, true)) { LOGERROR("CMapGeneratorWorker::Run: Failed to define g_MapSettings"); return false; } // Load RMS LOGMESSAGE("Loading RMS '%s'", m_ScriptPath.string8()); if (!m_ScriptInterface->LoadGlobalScriptFile(m_ScriptPath)) { LOGERROR("CMapGeneratorWorker::Run: Failed to load RMS '%s'", m_ScriptPath.string8()); return false; } return true; } void CMapGeneratorWorker::InitScriptInterface(const u32 seed) { m_ScriptInterface->SetCallbackData(static_cast(this)); m_ScriptInterface->ReplaceNondeterministicRNG(m_MapGenRNG); m_MapGenRNG.seed(seed); // VFS JSI_VFS::RegisterScriptFunctions_Maps(*m_ScriptInterface); // Globalscripts may use VFS script functions m_ScriptInterface->LoadGlobalScripts(); // File loading m_ScriptInterface->RegisterFunction("LoadLibrary"); m_ScriptInterface->RegisterFunction("LoadHeightmapImage"); m_ScriptInterface->RegisterFunction("LoadMapTerrain"); // Engine constants // Length of one tile of the terrain grid in metres. // Useful to transform footprint sizes to the tilegrid coordinate system. m_ScriptInterface->SetGlobal("TERRAIN_TILE_SIZE", static_cast(TERRAIN_TILE_SIZE)); // Number of impassable tiles at the map border m_ScriptInterface->SetGlobal("MAP_BORDER_WIDTH", static_cast(MAP_EDGE_TILES)); } void CMapGeneratorWorker::RegisterScriptFunctions_MapGenerator() { // Template functions m_ScriptInterface->RegisterFunction("GetTemplate"); m_ScriptInterface->RegisterFunction("TemplateExists"); m_ScriptInterface->RegisterFunction, std::string, bool, CMapGeneratorWorker::FindTemplates>("FindTemplates"); m_ScriptInterface->RegisterFunction, std::string, bool, CMapGeneratorWorker::FindActorTemplates>("FindActorTemplates"); // Progression and profiling m_ScriptInterface->RegisterFunction("SetProgress"); m_ScriptInterface->RegisterFunction("GetMicroseconds"); m_ScriptInterface->RegisterFunction("ExportMap"); } int CMapGeneratorWorker::GetProgress() { std::lock_guard lock(m_WorkerMutex); return m_Progress; } double CMapGeneratorWorker::GetMicroseconds(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) { return JS_Now(); } ScriptInterface::StructuredClone CMapGeneratorWorker::GetResults() { std::lock_guard lock(m_WorkerMutex); return m_MapData; } bool CMapGeneratorWorker::LoadLibrary(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& name) { CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); return self->LoadScripts(name); } void CMapGeneratorWorker::ExportMap(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue data) { CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); // Copy results std::lock_guard lock(self->m_WorkerMutex); self->m_MapData = self->m_ScriptInterface->WriteStructuredClone(data); self->m_Progress = 0; } void CMapGeneratorWorker::SetProgress(ScriptInterface::CmptPrivate* pCmptPrivate, int progress) { CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); // Copy data std::lock_guard lock(self->m_WorkerMutex); if (progress >= self->m_Progress) self->m_Progress = progress; else LOGWARNING("The random map script tried to reduce the loading progress from %d to %d", self->m_Progress, progress); } CParamNode CMapGeneratorWorker::GetTemplate(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName) { CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); const CParamNode& templateRoot = self->m_TemplateLoader.GetTemplateFileData(templateName).GetChild("Entity"); if (!templateRoot.IsOk()) LOGERROR("Invalid template found for '%s'", templateName.c_str()); return templateRoot; } bool CMapGeneratorWorker::TemplateExists(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName) { CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); return self->m_TemplateLoader.TemplateExists(templateName); } std::vector CMapGeneratorWorker::FindTemplates(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& path, bool includeSubdirectories) { CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); return self->m_TemplateLoader.FindTemplates(path, includeSubdirectories, SIMULATION_TEMPLATES); } std::vector CMapGeneratorWorker::FindActorTemplates(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& path, bool includeSubdirectories) { CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); return self->m_TemplateLoader.FindTemplates(path, includeSubdirectories, ACTOR_TEMPLATES); } bool CMapGeneratorWorker::LoadScripts(const VfsPath& libraryName) { // Ignore libraries that are already loaded if (m_LoadedLibraries.find(libraryName) != m_LoadedLibraries.end()) return true; // Mark this as loaded, to prevent it recursively loading itself m_LoadedLibraries.insert(libraryName); VfsPath path = VfsPath(L"maps/random/") / libraryName / VfsPath(); VfsPaths pathnames; // Load all scripts in mapgen directory Status ret = vfs::GetPathnames(g_VFS, path, L"*.js", pathnames); if (ret == INFO::OK) { for (const VfsPath& p : pathnames) { LOGMESSAGE("Loading map generator script '%s'", p.string8()); if (!m_ScriptInterface->LoadGlobalScriptFile(p)) { LOGERROR("CMapGeneratorWorker::LoadScripts: Failed to load script '%s'", p.string8()); return false; } } } else { // Some error reading directory wchar_t error[200]; LOGERROR("CMapGeneratorWorker::LoadScripts: Error reading scripts in directory '%s': %s", path.string8(), utf8_from_wstring(StatusDescription(ret, error, ARRAY_SIZE(error)))); return false; } return true; } JS::Value CMapGeneratorWorker::LoadHeightmap(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& filename) { std::vector heightmap; if (LoadHeightmapImageVfs(filename, heightmap) != INFO::OK) { LOGERROR("Could not load heightmap file '%s'", filename.string8()); return JS::UndefinedValue(); } CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); ScriptRequest rq(self->m_ScriptInterface); JS::RootedValue returnValue(rq.cx); ToJSVal_vector(rq, &returnValue, heightmap); return returnValue; } // See CMapReader::UnpackTerrain, CMapReader::ParseTerrain for the reordering JS::Value CMapGeneratorWorker::LoadMapTerrain(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& filename) { CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); ScriptRequest rq(self->m_ScriptInterface); if (!VfsFileExists(filename)) { ScriptException::Raise(rq, "Terrain file \"%s\" does not exist!", filename.string8().c_str()); return JS::UndefinedValue(); } CFileUnpacker unpacker; unpacker.Read(filename, "PSMP"); if (unpacker.GetVersion() < CMapIO::FILE_READ_VERSION) { ScriptException::Raise(rq, "Could not load terrain file \"%s\" too old version!", filename.string8().c_str()); return JS::UndefinedValue(); } // unpack size ssize_t patchesPerSide = (ssize_t)unpacker.UnpackSize(); size_t verticesPerSide = patchesPerSide * PATCH_SIZE + 1; // unpack heightmap std::vector heightmap; heightmap.resize(SQR(verticesPerSide)); unpacker.UnpackRaw(&heightmap[0], SQR(verticesPerSide) * sizeof(u16)); // unpack texture names size_t textureCount = unpacker.UnpackSize(); std::vector textureNames; textureNames.reserve(textureCount); for (size_t i = 0; i < textureCount; ++i) { CStr texturename; unpacker.UnpackString(texturename); textureNames.push_back(texturename); } // unpack texture IDs per tile ssize_t tilesPerSide = patchesPerSide * PATCH_SIZE; std::vector tiles; tiles.resize(size_t(SQR(tilesPerSide))); unpacker.UnpackRaw(&tiles[0], sizeof(CMapIO::STileDesc) * tiles.size()); // reorder by patches and store and save texture IDs per tile std::vector textureIDs; for (ssize_t x = 0; x < tilesPerSide; ++x) { size_t patchX = x / PATCH_SIZE; size_t offX = x % PATCH_SIZE; for (ssize_t y = 0; y < tilesPerSide; ++y) { size_t patchY = y / PATCH_SIZE; size_t offY = y % PATCH_SIZE; // m_Priority and m_Tex2Index unused textureIDs.push_back(tiles[(patchY * patchesPerSide + patchX) * SQR(PATCH_SIZE) + (offY * PATCH_SIZE + offX)].m_Tex1Index); } } JS::RootedValue returnValue(rq.cx); ScriptInterface::CreateObject( rq, &returnValue, "height", heightmap, "textureNames", textureNames, "textureIDs", textureIDs); return returnValue; } ////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////// CMapGenerator::CMapGenerator() : m_Worker(new CMapGeneratorWorker(nullptr)) { } CMapGenerator::~CMapGenerator() { delete m_Worker; } void CMapGenerator::GenerateMap(const VfsPath& scriptFile, const std::string& settings) { m_Worker->Initialize(scriptFile, settings); } int CMapGenerator::GetProgress() { return m_Worker->GetProgress(); } ScriptInterface::StructuredClone CMapGenerator::GetResults() { return m_Worker->GetResults(); } Index: ps/trunk/source/graphics/MapGenerator.h =================================================================== --- ps/trunk/source/graphics/MapGenerator.h (revision 24529) +++ ps/trunk/source/graphics/MapGenerator.h (revision 24530) @@ -1,248 +1,248 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_MAPGENERATOR #define INCLUDED_MAPGENERATOR #include "lib/posix/posix_pthread.h" #include "ps/FileIo.h" #include "ps/TemplateLoader.h" #include "scriptinterface/ScriptInterface.h" #include #include #include #include #include class CMapGeneratorWorker; /** * Random map generator interface. Initialized by CMapReader and then checked * periodically during loading, until it's finished (progress value is 0). * * The actual work is performed by CMapGeneratorWorker in a separate thread. */ class CMapGenerator { NONCOPYABLE(CMapGenerator); public: CMapGenerator(); ~CMapGenerator(); /** * Start the map generator thread * * @param scriptFile The VFS path for the script, e.g. "maps/random/latium.js" * @param settings JSON string containing settings for the map generator */ void GenerateMap(const VfsPath& scriptFile, const std::string& settings); /** * Get status of the map generator thread * * @return Progress percentage 1-100 if active, 0 when finished, or -1 on error */ int GetProgress(); /** * Get random map data, according to this format: * http://trac.wildfiregames.com/wiki/Random_Map_Generator_Internals#Dataformat * * @return StructuredClone containing map data */ ScriptInterface::StructuredClone GetResults(); private: CMapGeneratorWorker* m_Worker; }; /** * Random map generator worker thread. * (This is run in a thread so that the GUI remains responsive while loading) * * Thread-safety: * - Initialize and constructor/destructor must be called from the main thread. * - ScriptInterface created and destroyed by thread * - StructuredClone used to return JS map data - JS:Values can't be used across threads/contexts. */ class CMapGeneratorWorker { public: CMapGeneratorWorker(ScriptInterface* scriptInterface); ~CMapGeneratorWorker(); /** * Start the map generator thread * * @param scriptFile The VFS path for the script, e.g. "maps/random/latium.js" * @param settings JSON string containing settings for the map generator */ void Initialize(const VfsPath& scriptFile, const std::string& settings); /** * Get status of the map generator thread * * @return Progress percentage 1-100 if active, 0 when finished, or -1 on error */ int GetProgress(); /** * Get random map data, according to this format: * http://trac.wildfiregames.com/wiki/Random_Map_Generator_Internals#Dataformat * * @return StructuredClone containing map data */ ScriptInterface::StructuredClone GetResults(); /** * Set initial seed, callback data. * Expose functions, globals and classes defined in this class relevant to the map and test scripts. */ void InitScriptInterface(const u32 seed); private: /** * Expose functions defined in this class that are relevant to mapscripts but not the tests. */ void RegisterScriptFunctions_MapGenerator(); /** * Load all scripts of the given library * * @param libraryName VfsPath specifying name of the library (subfolder of ../maps/random/) * @return true if all scripts ran successfully, false if there's an error */ bool LoadScripts(const VfsPath& libraryName); /** * Recursively load all script files in the given folder. */ static bool LoadLibrary(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& name); /** * Finalize map generation and pass results from the script to the engine. */ static void ExportMap(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue data); /** * Load an image file and return it as a height array. */ static JS::Value LoadHeightmap(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& src); /** * Load an Atlas terrain file (PMP) returning textures and heightmap. */ static JS::Value LoadMapTerrain(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& filename); /** * Sets the map generation progress, which is one of multiple stages determining the loading screen progress. */ static void SetProgress(ScriptInterface::CmptPrivate* pCmptPrivate, int progress); /** * Microseconds since the epoch. */ static double GetMicroseconds(ScriptInterface::CmptPrivate* pCmptPrivate); /** * Return the template data of the given template name. */ static CParamNode GetTemplate(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName); /** * Check whether the given template exists. */ static bool TemplateExists(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName); /** * Returns all template names of simulation entity templates. */ static std::vector FindTemplates(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& path, bool includeSubdirectories); /** * Returns all template names of actors. */ static std::vector FindActorTemplates(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& path, bool includeSubdirectories); /** * Perform map generation in an independent thread. */ - static void* RunThread(CMapGeneratorWorker* self); + static void RunThread(CMapGeneratorWorker* self); /** * Perform the map generation. */ bool Run(); /** * Currently loaded script librarynames. */ std::set m_LoadedLibraries; /** * Result of the mapscript generation including terrain, entities and environment settings. */ ScriptInterface::StructuredClone m_MapData; /** * Deterministic random number generator. */ boost::rand48 m_MapGenRNG; /** * Current map generation progress. */ int m_Progress; /** * Provides the script context. */ ScriptInterface* m_ScriptInterface; /** * Map generation script to run. */ VfsPath m_ScriptPath; /** * Map and simulation settings chosen in the gamesetup stage. */ std::string m_Settings; /** * Backend to loading template data. */ CTemplateLoader m_TemplateLoader; /** * Holds the mapgeneration thread identifier. */ std::thread m_WorkerThread; /** * Avoids thread synchronization issues. */ std::mutex m_WorkerMutex; }; #endif //INCLUDED_MAPGENERATOR Index: ps/trunk/source/graphics/ShaderDefines.cpp =================================================================== --- ps/trunk/source/graphics/ShaderDefines.cpp (revision 24529) +++ ps/trunk/source/graphics/ShaderDefines.cpp (revision 24530) @@ -1,279 +1,279 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ShaderDefines.h" #include "graphics/ShaderProgram.h" #include "lib/hash.h" #include "maths/Vector4D.h" #include "ps/ThreadUtil.h" #include namespace std { template<> struct hash { std::size_t operator()(const CVector4D& v) const { size_t hash = 0; hash_combine(hash, v.X); hash_combine(hash, v.Y); hash_combine(hash, v.Z); hash_combine(hash, v.W); return hash; } }; } bool operator==(const CShaderParams::SItems& a, const CShaderParams::SItems& b) { return a.items == b.items; } bool operator==(const CShaderParams::SItems& a, const CShaderParams::SItems& b) { return a.items == b.items; } template struct ItemNameCmp { typedef typename CShaderParams::SItems::Item Item; typedef Item first_argument_type; typedef Item second_argument_type; bool operator()(const Item& a, const Item& b) const { return a.first < b.first; } }; template struct ItemNameGeq { typedef typename CShaderParams::SItems::Item Item; bool operator()(const Item& a, const Item& b) const { return !(b.first < a.first); } }; template typename CShaderParams::SItems* CShaderParams::GetInterned(const SItems& items) { - ENSURE(ThreadUtil::IsMainThread()); // s_InternedItems is not thread-safe + ENSURE(Threading::IsMainThread()); // s_InternedItems is not thread-safe typename InternedItems_t::iterator it = s_InternedItems.find(items); if (it != s_InternedItems.end()) return it->second.get(); // Sanity test: the items list is meant to be sorted by name. // This is a reasonable place to verify that, since this will be called once per distinct SItems. typedef ItemNameCmp Cmp; ENSURE(std::adjacent_find(items.items.begin(), items.items.end(), std::binary_negate(Cmp())) == items.items.end()); shared_ptr ptr(new SItems(items)); s_InternedItems.insert(std::make_pair(items, ptr)); return ptr.get(); } template CShaderParams::CShaderParams() { *this = s_Empty; } template CShaderParams::CShaderParams(SItems* items) : m_Items(items) { } template CShaderParams CShaderParams::CreateEmpty() { SItems items; items.RecalcHash(); return CShaderParams(GetInterned(items)); } template void CShaderParams::Set(CStrIntern name, const value_t& value) { SItems items = *m_Items; typename SItems::Item addedItem = std::make_pair(name, value); // Add the new item in a way that preserves the sortedness and uniqueness of item names for (typename std::vector::iterator it = items.items.begin(); ; ++it) { if (it == items.items.end() || addedItem.first < it->first) { items.items.insert(it, addedItem); break; } else if (addedItem.first == it->first) { it->second = addedItem.second; break; } } items.RecalcHash(); m_Items = GetInterned(items); } template void CShaderParams::SetMany(const CShaderParams& params) { SItems items; // set_union merges the two sorted lists into a new sorted list; // if two items are equivalent (i.e. equal names, possibly different values) // then the one from the first list is kept std::set_union( params.m_Items->items.begin(), params.m_Items->items.end(), m_Items->items.begin(), m_Items->items.end(), std::inserter(items.items, items.items.begin()), ItemNameCmp()); items.RecalcHash(); m_Items = GetInterned(items); } template std::map CShaderParams::GetMap() const { std::map ret; for (size_t i = 0; i < m_Items->items.size(); ++i) ret[m_Items->items[i].first] = m_Items->items[i].second; return ret; } template size_t CShaderParams::GetHash() const { return m_Items->hash; } template void CShaderParams::SItems::RecalcHash() { size_t h = 0; for (size_t i = 0; i < items.size(); ++i) { hash_combine(h, items[i].first); hash_combine(h, items[i].second); } hash = h; } void CShaderDefines::Add(CStrIntern name, CStrIntern value) { Set(name, value); } int CShaderDefines::GetInt(const char* name) const { CStrIntern nameIntern(name); for (size_t i = 0; i < m_Items->items.size(); ++i) { if (m_Items->items[i].first == nameIntern) { int ret; std::stringstream str(m_Items->items[i].second.c_str()); str >> ret; return ret; } } return 0; } void CShaderUniforms::Add(const char* name, const CVector4D& value) { Set(CStrIntern(name), value); } CVector4D CShaderUniforms::GetVector(const char* name) const { CStrIntern nameIntern(name); for (size_t i = 0; i < m_Items->items.size(); ++i) { if (m_Items->items[i].first == nameIntern) { return m_Items->items[i].second; } } return CVector4D(); } void CShaderUniforms::BindUniforms(const CShaderProgramPtr& shader) const { const std::vector& items = m_Items->items; for (size_t i = 0; i < items.size(); ++i) { CShaderProgram::Binding binding = shader->GetUniformBinding(items[i].first); if (binding.Active()) { CVector4D v = items[i].second; shader->Uniform(binding, v.X, v.Y, v.Z, v.W); } } } void CShaderRenderQueries::Add(const char* name) { if (name == CStr("sim_time")) { m_Items.emplace_back(RQUERY_TIME, CStrIntern(name)); } else if (name == CStr("water_tex")) { m_Items.emplace_back(RQUERY_WATER_TEX, CStrIntern(name)); } else if (name == CStr("sky_cube")) { m_Items.emplace_back(RQUERY_SKY_CUBE, CStrIntern(name)); } } void CShaderConditionalDefines::Add(const char* defname, const char* defvalue, int type, std::vector &args) { CondDefine cd; cd.m_DefName = CStrIntern(defname); cd.m_DefValue = CStrIntern(defvalue); cd.m_CondArgs = args; cd.m_CondType = type; m_Defines.push_back(cd); } // Explicit instantiations: template<> CShaderParams::InternedItems_t CShaderParams::s_InternedItems = CShaderParams::InternedItems_t(); template<> CShaderParams::InternedItems_t CShaderParams::s_InternedItems = CShaderParams::InternedItems_t(); template<> CShaderParams CShaderParams::s_Empty = CShaderParams::CreateEmpty(); template<> CShaderParams CShaderParams::s_Empty = CShaderParams::CreateEmpty(); template class CShaderParams; template class CShaderParams; Index: ps/trunk/source/graphics/TextureConverter.cpp =================================================================== --- ps/trunk/source/graphics/TextureConverter.cpp (revision 24529) +++ ps/trunk/source/graphics/TextureConverter.cpp (revision 24530) @@ -1,598 +1,599 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "TextureConverter.h" #include "lib/regex.h" #include "lib/timer.h" #include "lib/allocators/shared_ptr.h" #include "lib/tex/tex.h" #include "maths/MD5.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/Profiler2.h" +#include "ps/Threading.h" #include "ps/XML/Xeromyces.h" #if CONFIG2_NVTT #include "nvtt/nvtt.h" // We assume NVTT is recent enough to support the alpha flag in the DXT1a format. If users try to use an // old version of NVTT, the game will crash when trying to decode dds files generated by NVTT. // // The support was added upstream in https://github.com/castano/nvidia-texture-tools/commit/782a127071895f538c1ae49925a6e15687e3c966 // so, in theory, 2.0.7 and newer should be enough, but all 2.0.x releases define NVTT_VERSION as 200, so // we can't distinguish them. NVTT_VERSION is 201 in all development versions of the 2.0.x era, so we also // have to exclude that value. #if !defined NVTT_VERSION || NVTT_VERSION == 200 || NVTT_VERSION == 201 #error Please use NVTT 2.1.0 or newer. \ If your system does not provide it, you should use the bundled version by NOT passing --with-system-nvtt to premake. #endif /** * Output handler to collect NVTT's output into a simplistic buffer. * WARNING: Used in the worker thread - must be thread-safe. */ struct BufferOutputHandler : public nvtt::OutputHandler { std::vector buffer; virtual void beginImage(int UNUSED(size), int UNUSED(width), int UNUSED(height), int UNUSED(depth), int UNUSED(face), int UNUSED(miplevel)) { } virtual bool writeData(const void* data, int size) { size_t off = buffer.size(); buffer.resize(off + size); memcpy(&buffer[off], data, size); return true; } virtual void endImage() { } }; /** * Request for worker thread to process. */ struct CTextureConverter::ConversionRequest { VfsPath dest; CTexturePtr texture; nvtt::InputOptions inputOptions; nvtt::CompressionOptions compressionOptions; nvtt::OutputOptions outputOptions; }; /** * Result from worker thread. */ struct CTextureConverter::ConversionResult { VfsPath dest; CTexturePtr texture; BufferOutputHandler output; bool ret; // true if the conversion succeeded }; #endif // CONFIG2_NVTT void CTextureConverter::Settings::Hash(MD5& hash) { hash.Update((const u8*)&format, sizeof(format)); hash.Update((const u8*)&mipmap, sizeof(mipmap)); hash.Update((const u8*)&normal, sizeof(normal)); hash.Update((const u8*)&alpha, sizeof(alpha)); hash.Update((const u8*)&filter, sizeof(filter)); hash.Update((const u8*)&kaiserWidth, sizeof(kaiserWidth)); hash.Update((const u8*)&kaiserAlpha, sizeof(kaiserAlpha)); hash.Update((const u8*)&kaiserStretch, sizeof(kaiserStretch)); } CTextureConverter::SettingsFile* CTextureConverter::LoadSettings(const VfsPath& path) const { CXeromyces XeroFile; if (XeroFile.Load(m_VFS, path, "texture") != PSRETURN_OK) return NULL; // Define all the elements used in the XML file #define EL(x) int el_##x = XeroFile.GetElementID(#x) #define AT(x) int at_##x = XeroFile.GetAttributeID(#x) EL(textures); EL(file); AT(pattern); AT(format); AT(mipmap); AT(normal); AT(alpha); AT(filter); AT(kaiserwidth); AT(kaiseralpha); AT(kaiserstretch); #undef AT #undef EL XMBElement root = XeroFile.GetRoot(); if (root.GetNodeName() != el_textures) { LOGERROR("Invalid texture settings file \"%s\" (unrecognised root element)", path.string8()); return NULL; } std::unique_ptr settings(new SettingsFile()); XERO_ITER_EL(root, child) { if (child.GetNodeName() == el_file) { Match p; XERO_ITER_ATTR(child, attr) { if (attr.Name == at_pattern) { p.pattern = attr.Value.FromUTF8(); } else if (attr.Name == at_format) { CStr v(attr.Value); if (v == "dxt1") p.settings.format = FMT_DXT1; else if (v == "dxt3") p.settings.format = FMT_DXT3; else if (v == "dxt5") p.settings.format = FMT_DXT5; else if (v == "rgba") p.settings.format = FMT_RGBA; else if (v == "alpha") p.settings.format = FMT_ALPHA; else LOGERROR("Invalid attribute value ", v.c_str()); } else if (attr.Name == at_mipmap) { CStr v(attr.Value); if (v == "true") p.settings.mipmap = MIP_TRUE; else if (v == "false") p.settings.mipmap = MIP_FALSE; else LOGERROR("Invalid attribute value ", v.c_str()); } else if (attr.Name == at_normal) { CStr v(attr.Value); if (v == "true") p.settings.normal = NORMAL_TRUE; else if (v == "false") p.settings.normal = NORMAL_FALSE; else LOGERROR("Invalid attribute value ", v.c_str()); } else if (attr.Name == at_alpha) { CStr v(attr.Value); if (v == "none") p.settings.alpha = ALPHA_NONE; else if (v == "player") p.settings.alpha = ALPHA_PLAYER; else if (v == "transparency") p.settings.alpha = ALPHA_TRANSPARENCY; else LOGERROR("Invalid attribute value ", v.c_str()); } else if (attr.Name == at_filter) { CStr v(attr.Value); if (v == "box") p.settings.filter = FILTER_BOX; else if (v == "triangle") p.settings.filter = FILTER_TRIANGLE; else if (v == "kaiser") p.settings.filter = FILTER_KAISER; else LOGERROR("Invalid attribute value ", v.c_str()); } else if (attr.Name == at_kaiserwidth) { p.settings.kaiserWidth = CStr(attr.Value).ToFloat(); } else if (attr.Name == at_kaiseralpha) { p.settings.kaiserAlpha = CStr(attr.Value).ToFloat(); } else if (attr.Name == at_kaiserstretch) { p.settings.kaiserStretch = CStr(attr.Value).ToFloat(); } else { LOGERROR("Invalid attribute name ", XeroFile.GetAttributeString(attr.Name).c_str()); } } settings->patterns.push_back(p); } } return settings.release(); } CTextureConverter::Settings CTextureConverter::ComputeSettings(const std::wstring& filename, const std::vector& settingsFiles) const { // Set sensible defaults Settings settings; settings.format = FMT_DXT1; settings.mipmap = MIP_TRUE; settings.normal = NORMAL_FALSE; settings.alpha = ALPHA_NONE; settings.filter = FILTER_BOX; settings.kaiserWidth = 3.f; settings.kaiserAlpha = 4.f; settings.kaiserStretch = 1.f; for (size_t i = 0; i < settingsFiles.size(); ++i) { for (size_t j = 0; j < settingsFiles[i]->patterns.size(); ++j) { Match p = settingsFiles[i]->patterns[j]; // Check that the pattern matches the texture file if (!match_wildcard(filename.c_str(), p.pattern.c_str())) continue; if (p.settings.format != FMT_UNSPECIFIED) settings.format = p.settings.format; if (p.settings.mipmap != MIP_UNSPECIFIED) settings.mipmap = p.settings.mipmap; if (p.settings.normal != NORMAL_UNSPECIFIED) settings.normal = p.settings.normal; if (p.settings.alpha != ALPHA_UNSPECIFIED) settings.alpha = p.settings.alpha; if (p.settings.filter != FILTER_UNSPECIFIED) settings.filter = p.settings.filter; if (p.settings.kaiserWidth != -1.f) settings.kaiserWidth = p.settings.kaiserWidth; if (p.settings.kaiserAlpha != -1.f) settings.kaiserAlpha = p.settings.kaiserAlpha; if (p.settings.kaiserStretch != -1.f) settings.kaiserStretch = p.settings.kaiserStretch; } } return settings; } CTextureConverter::CTextureConverter(PIVFS vfs, bool highQuality) : m_VFS(vfs), m_HighQuality(highQuality), m_Shutdown(false) { // Verify that we are running with at least the version we were compiled with, // to avoid bugs caused by ABI changes #if CONFIG2_NVTT ENSURE(nvtt::version() >= NVTT_VERSION); #endif - m_WorkerThread = std::thread(RunThread, this); + m_WorkerThread = std::thread(Threading::HandleExceptions::Wrapper, this); // Maybe we should share some centralised pool of worker threads? // For now we'll just stick with a single thread for this specific use. } CTextureConverter::~CTextureConverter() { // Tell the thread to shut down { std::lock_guard lock(m_WorkerMutex); m_Shutdown = true; } while (true) { // Wake the thread up so that it shutdowns. // If we send the message once, there is a chance it will be missed, // so keep sending until shtudown becomes false again, indicating that the thread has shut down. std::lock_guard lock(m_WorkerMutex); m_WorkerCV.notify_all(); if (!m_Shutdown) break; } // Wait for it to shut down cleanly m_WorkerThread.join(); } bool CTextureConverter::ConvertTexture(const CTexturePtr& texture, const VfsPath& src, const VfsPath& dest, const Settings& settings) { shared_ptr file; size_t fileSize; if (m_VFS->LoadFile(src, file, fileSize) < 0) { LOGERROR("Failed to load texture \"%s\"", src.string8()); return false; } Tex tex; if (tex.decode(file, fileSize) < 0) { LOGERROR("Failed to decode texture \"%s\"", src.string8()); return false; } // Check whether there's any alpha channel bool hasAlpha = ((tex.m_Flags & TEX_ALPHA) != 0); if (settings.format == FMT_ALPHA) { // Convert to uncompressed 8-bit with no mipmaps if (tex.transform_to((tex.m_Flags | TEX_GREY) & ~(TEX_DXT | TEX_MIPMAPS | TEX_ALPHA)) < 0) { LOGERROR("Failed to transform texture \"%s\"", src.string8()); return false; } } else { // Convert to uncompressed BGRA with no mipmaps if (tex.transform_to((tex.m_Flags | TEX_BGR | TEX_ALPHA) & ~(TEX_DXT | TEX_MIPMAPS)) < 0) { LOGERROR("Failed to transform texture \"%s\"", src.string8()); return false; } } // Check if the texture has all alpha=255, so we can automatically // switch from DXT3/DXT5 to DXT1 with no loss if (hasAlpha) { hasAlpha = false; u8* data = tex.get_data(); for (size_t i = 0; i < tex.m_Width * tex.m_Height; ++i) { if (data[i*4+3] != 0xFF) { hasAlpha = true; break; } } } #if CONFIG2_NVTT shared_ptr request(new ConversionRequest); request->dest = dest; request->texture = texture; // Apply the chosen settings: request->inputOptions.setMipmapGeneration(settings.mipmap == MIP_TRUE); if (settings.alpha == ALPHA_TRANSPARENCY) request->inputOptions.setAlphaMode(nvtt::AlphaMode_Transparency); else request->inputOptions.setAlphaMode(nvtt::AlphaMode_None); if (settings.format == FMT_RGBA) { request->compressionOptions.setFormat(nvtt::Format_RGBA); // Change the default component order (see tex_dds.cpp decode_pf) request->compressionOptions.setPixelFormat(32, 0xFF, 0xFF00, 0xFF0000, 0xFF000000u); } else if (settings.format == FMT_ALPHA) { request->compressionOptions.setFormat(nvtt::Format_RGBA); request->compressionOptions.setPixelFormat(8, 0x00, 0x00, 0x00, 0xFF); } else if (!hasAlpha) { // if no alpha channel then there's no point using DXT3 or DXT5 request->compressionOptions.setFormat(nvtt::Format_DXT1); } else if (settings.format == FMT_DXT1) { request->compressionOptions.setFormat(nvtt::Format_DXT1a); } else if (settings.format == FMT_DXT3) { request->compressionOptions.setFormat(nvtt::Format_DXT3); } else if (settings.format == FMT_DXT5) { request->compressionOptions.setFormat(nvtt::Format_DXT5); } if (settings.filter == FILTER_BOX) request->inputOptions.setMipmapFilter(nvtt::MipmapFilter_Box); else if (settings.filter == FILTER_TRIANGLE) request->inputOptions.setMipmapFilter(nvtt::MipmapFilter_Triangle); else if (settings.filter == FILTER_KAISER) request->inputOptions.setMipmapFilter(nvtt::MipmapFilter_Kaiser); if (settings.normal == NORMAL_TRUE) request->inputOptions.setNormalMap(true); request->inputOptions.setKaiserParameters(settings.kaiserWidth, settings.kaiserAlpha, settings.kaiserStretch); request->inputOptions.setWrapMode(nvtt::WrapMode_Mirror); // TODO: should this be configurable? request->compressionOptions.setQuality(m_HighQuality ? nvtt::Quality_Production : nvtt::Quality_Fastest); // TODO: normal maps, gamma, etc // Load the texture data request->inputOptions.setTextureLayout(nvtt::TextureType_2D, tex.m_Width, tex.m_Height); if (tex.m_Bpp == 32) { request->inputOptions.setMipmapData(tex.get_data(), tex.m_Width, tex.m_Height); } else // bpp == 8 { // NVTT requires 32-bit input data, so convert const u8* input = tex.get_data(); u8* rgba = new u8[tex.m_Width * tex.m_Height * 4]; u8* p = rgba; for (size_t i = 0; i < tex.m_Width * tex.m_Height; i++) { p[0] = p[1] = p[2] = p[3] = *input++; p += 4; } request->inputOptions.setMipmapData(rgba, tex.m_Width, tex.m_Height); delete[] rgba; } { std::lock_guard lock(m_WorkerMutex); m_RequestQueue.push_back(request); } // Wake up the worker thread m_WorkerCV.notify_all(); return true; #else LOGERROR("Failed to convert texture \"%s\" (NVTT not available)", src.string8()); return false; #endif } bool CTextureConverter::Poll(CTexturePtr& texture, VfsPath& dest, bool& ok) { #if CONFIG2_NVTT shared_ptr result; // Grab the first result (if any) { std::lock_guard lock(m_WorkerMutex); if (!m_ResultQueue.empty()) { result = m_ResultQueue.front(); m_ResultQueue.pop_front(); } } if (!result) { // no work to do return false; } if (!result->ret) { // conversion had failed ok = false; return true; } // Move output into a correctly-aligned buffer size_t size = result->output.buffer.size(); shared_ptr file; AllocateAligned(file, size, maxSectorSize); memcpy(file.get(), &result->output.buffer[0], size); if (m_VFS->CreateFile(result->dest, file, size) < 0) { // error writing file ok = false; return true; } // Succeeded in converting texture texture = result->texture; dest = result->dest; ok = true; return true; #else // #if CONFIG2_NVTT return false; #endif } bool CTextureConverter::IsBusy() { std::lock_guard lock(m_WorkerMutex); return !m_RequestQueue.empty(); } void CTextureConverter::RunThread(CTextureConverter* textureConverter) { debug_SetThreadName("TextureConverter"); g_Profiler2.RegisterCurrentThread("texconv"); #if CONFIG2_NVTT // Wait until the main thread wakes us up while (true) { // We may have several textures in the incoming queue, process them all before going back to sleep. if (!textureConverter->IsBusy()) { std::unique_lock wait_lock(textureConverter->m_WorkerMutex); // Use the no-condition variant because spurious wake-ups don't matter that much here. textureConverter->m_WorkerCV.wait(wait_lock); } g_Profiler2.RecordSyncMarker(); PROFILE2_EVENT("wakeup"); shared_ptr request; { std::lock_guard wait_lock(textureConverter->m_WorkerMutex); if (textureConverter->m_Shutdown) break; // If we weren't woken up for shutdown, we must have been woken up for // a new request, so grab it from the queue request = textureConverter->m_RequestQueue.front(); textureConverter->m_RequestQueue.pop_front(); } // Set up the result object shared_ptr result(new ConversionResult()); result->dest = request->dest; result->texture = request->texture; request->outputOptions.setOutputHandler(&result->output); // TIMER(L"TextureConverter compress"); { PROFILE2("compress"); // Perform the compression nvtt::Compressor compressor; result->ret = compressor.process(request->inputOptions, request->compressionOptions, request->outputOptions); } // Push the result onto the queue std::lock_guard wait_lock(textureConverter->m_WorkerMutex); textureConverter->m_ResultQueue.push_back(result); } std::lock_guard wait_lock(textureConverter->m_WorkerMutex); textureConverter->m_Shutdown = false; #endif } Index: ps/trunk/source/lib/sysdep/os/win/wseh.h =================================================================== --- ps/trunk/source/lib/sysdep/os/win/wseh.h (revision 24529) +++ ps/trunk/source/lib/sysdep/os/win/wseh.h (revision 24530) @@ -1,35 +1,37 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * Win32 debug support code and exception handler. */ #ifndef INCLUDED_WSEH #define INCLUDED_WSEH +#include + struct _EXCEPTION_POINTERS; extern long __stdcall wseh_ExceptionFilter(_EXCEPTION_POINTERS* ep); EXTERN_C int wseh_EntryPoint(); #endif // #ifndef INCLUDED_WSEH Index: ps/trunk/source/lib/sysdep/os/win/wutil.cpp =================================================================== --- ps/trunk/source/lib/sysdep/os/win/wutil.cpp (revision 24529) +++ ps/trunk/source/lib/sysdep/os/win/wutil.cpp (revision 24530) @@ -1,580 +1,588 @@ -/* Copyright (C) 2015 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * various Windows-specific utilities */ #include "precompiled.h" #include "lib/sysdep/os/win/wutil.h" #include #include // __argc #include "lib/file/file.h" #include "lib/posix/posix.h" #include "lib/sysdep/sysdep.h" #include "lib/sysdep/os/win/win.h" #include "lib/sysdep/os/win/wdbg.h" // wdbg_assert #include "lib/sysdep/os/win/winit.h" #include // SHGetFolderPath WINIT_REGISTER_EARLY_INIT(wutil_Init); WINIT_REGISTER_LATE_SHUTDOWN(wutil_Shutdown); +// Defined in ps/Pyrogenesis.h +extern const char* main_window_name; + //----------------------------------------------------------------------------- // safe allocator // may be used independently of libc malloc // (in particular, before _cinit and while calling static dtors). // used by wpthread critical section code. void* wutil_Allocate(size_t size) { const DWORD flags = HEAP_ZERO_MEMORY; return HeapAlloc(GetProcessHeap(), flags, size); } void wutil_Free(void* p) { const DWORD flags = 0; HeapFree(GetProcessHeap(), flags, p); } //----------------------------------------------------------------------------- // locks // several init functions are before called before _cinit. // POSIX static mutex init may not have been done by then, // so we need our own lightweight functions. static CRITICAL_SECTION cs[NUM_CS]; static bool cs_valid; void wutil_Lock(WinLockId id) { if(!cs_valid) return; EnterCriticalSection(&cs[id]); } void wutil_Unlock(WinLockId id) { if(!cs_valid) return; LeaveCriticalSection(&cs[id]); } bool wutil_IsLocked(WinLockId id) { if(!cs_valid) return false; const BOOL successfullyEntered = TryEnterCriticalSection(&cs[id]); if(!successfullyEntered) return true; // still locked LeaveCriticalSection(&cs[id]); return false; // probably not locked } static void InitLocks() { for(int i = 0; i < NUM_CS; i++) InitializeCriticalSection(&cs[i]); cs_valid = true; } static void ShutdownLocks() { cs_valid = false; for(int i = 0; i < NUM_CS; i++) DeleteCriticalSection(&cs[i]); memset(cs, 0, sizeof(cs)); } //----------------------------------------------------------------------------- // error codes // only call after a Win32 function indicates failure. Status StatusFromWin() { switch(GetLastError()) { case ERROR_BUSY: case WAIT_TIMEOUT: return ERR::AGAIN; case ERROR_OPERATION_ABORTED: return ERR::ABORTED; case ERROR_INVALID_HANDLE: return ERR::INVALID_HANDLE; case ERROR_INSUFFICIENT_BUFFER: return ERR::INVALID_SIZE; case ERROR_INVALID_PARAMETER: case ERROR_BAD_ARGUMENTS: return ERR::INVALID_PARAM; case ERROR_OUTOFMEMORY: case ERROR_NOT_ENOUGH_MEMORY: return ERR::NO_MEM; case ERROR_NOT_SUPPORTED: case ERROR_CALL_NOT_IMPLEMENTED: case ERROR_PROC_NOT_FOUND: return ERR::NOT_SUPPORTED; case ERROR_FILE_NOT_FOUND: case ERROR_PATH_NOT_FOUND: return ERR::FILE_NOT_FOUND; case ERROR_ACCESS_DENIED: return ERR::FILE_ACCESS; default: return ERR::FAIL; } } //----------------------------------------------------------------------------- // command line // copy of GetCommandLine string. will be tokenized and then referenced by // the argv pointers. static wchar_t* argvContents; int s_argc = 0; wchar_t** s_argv = 0; static void ReadCommandLine() { const wchar_t* commandLine = GetCommandLineW(); // (this changes as quotation marks are removed) size_t numChars = wcslen(commandLine); argvContents = (wchar_t*)HeapAlloc(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, (numChars+1)*sizeof(wchar_t)); wcscpy_s(argvContents, numChars+1, commandLine); // first pass: tokenize string and count number of arguments bool ignoreSpace = false; for(size_t i = 0; i < numChars; i++) { switch(argvContents[i]) { case '"': ignoreSpace = !ignoreSpace; // strip the " character memmove(argvContents+i, argvContents+i+1, (numChars-i)*sizeof(wchar_t)); numChars--; i--; break; case ' ': if(!ignoreSpace) { argvContents[i] = '\0'; s_argc++; } break; } } s_argc++; // have argv entries point into the tokenized string s_argv = (wchar_t**)HeapAlloc(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, s_argc*sizeof(wchar_t*)); wchar_t* nextArg = argvContents; for(int i = 0; i < s_argc; i++) { s_argv[i] = nextArg; nextArg += wcslen(nextArg)+1; } } int wutil_argc() { return s_argc; } wchar_t** wutil_argv() { ENSURE(s_argv); return s_argv; } static void FreeCommandLine() { HeapFree(GetProcessHeap(), 0, s_argv); HeapFree(GetProcessHeap(), 0, argvContents); } bool wutil_HasCommandLineArgument(const wchar_t* arg) { for(int i = 0; i < s_argc; i++) { if(!wcscmp(s_argv[i], arg)) return true; } return false; } //----------------------------------------------------------------------------- // directories // (NB: wutil_Init is called before static ctors => use placement new) static OsPath* systemPath; static OsPath* executablePath; static OsPath* localAppdataPath; static OsPath* roamingAppdataPath; static OsPath* personalPath; const OsPath& wutil_SystemPath() { return *systemPath; } const OsPath& wutil_ExecutablePath() { return *executablePath; } const OsPath& wutil_LocalAppdataPath() { return *localAppdataPath; } const OsPath& wutil_RoamingAppdataPath() { return *roamingAppdataPath; } const OsPath& wutil_PersonalPath() { return *personalPath; } // Helper to avoid duplicating this setup static OsPath* GetFolderPath(int csidl) { HWND hwnd = 0; // ignored unless a dial-up connection is needed to access the folder HANDLE token = 0; wchar_t path[MAX_PATH]; // mandated by SHGetFolderPathW const HRESULT ret = SHGetFolderPathW(hwnd, csidl, token, 0, path); if (!SUCCEEDED(ret)) { debug_printf("SHGetFolderPathW failed with HRESULT = 0x%08lx for csidl = 0x%04x\n", ret, csidl); debug_warn("SHGetFolderPathW failed (see debug output)"); } if(GetLastError() == ERROR_NO_TOKEN) // avoid polluting last error SetLastError(0); return new(wutil_Allocate(sizeof(OsPath))) OsPath(path); } static void GetDirectories() { WinScopedPreserveLastError s; // system directory { const UINT length = GetSystemDirectoryW(0, 0); ENSURE(length != 0); std::wstring path(length, '\0'); const UINT charsWritten = GetSystemDirectoryW(&path[0], length); ENSURE(charsWritten == length-1); systemPath = new(wutil_Allocate(sizeof(OsPath))) OsPath(path); } // executable's directory executablePath = new(wutil_Allocate(sizeof(OsPath))) OsPath(sys_ExecutablePathname().Parent()); // roaming application data roamingAppdataPath = GetFolderPath(CSIDL_APPDATA); // local application data localAppdataPath = GetFolderPath(CSIDL_LOCAL_APPDATA); // my documents personalPath = GetFolderPath(CSIDL_PERSONAL); } static void FreeDirectories() { systemPath->~OsPath(); wutil_Free(systemPath); executablePath->~OsPath(); wutil_Free(executablePath); localAppdataPath->~OsPath(); wutil_Free(localAppdataPath); roamingAppdataPath->~OsPath(); wutil_Free(roamingAppdataPath); personalPath->~OsPath(); wutil_Free(personalPath); } //----------------------------------------------------------------------------- // user32 fix // HACK: make sure a reference to user32 is held, even if someone // decides to delay-load it. this fixes bug #66, which was the // Win32 mouse cursor (set via user32!SetCursor) appearing as a // black 32x32(?) rectangle. the underlying cause was as follows: // powrprof.dll was the first client of user32, causing it to be // loaded. after we were finished with powrprof, we freed it, in turn // causing user32 to unload. later code would then reload user32, // which apparently terminally confused the cursor implementation. // // since we hold a reference here, user32 will never unload. // of course, the benefits of delay-loading are lost for this DLL, // but that is unavoidable. it is safer to force loading it, rather // than documenting the problem and asking it not be delay-loaded. static HMODULE hUser32Dll; static void ForciblyLoadUser32Dll() { hUser32Dll = LoadLibraryW(L"user32.dll"); } // avoids Boundschecker warning static void FreeUser32Dll() { FreeLibrary(hUser32Dll); } //----------------------------------------------------------------------------- // memory static void EnableLowFragmentationHeap() { if(IsDebuggerPresent()) { // and the debug heap isn't explicitly disabled, char* var = getenv("_NO_DEBUG_HEAP"); if(!var || var[0] != '1') return; // we can't enable the LFH } #if WINVER >= 0x0501 WUTIL_FUNC(pHeapSetInformation, BOOL, (HANDLE, HEAP_INFORMATION_CLASS, void*, size_t)); WUTIL_IMPORT_KERNEL32(HeapSetInformation, pHeapSetInformation); if(pHeapSetInformation) { ULONG flags = 2; // enable LFH pHeapSetInformation(GetProcessHeap(), HeapCompatibilityInformation, &flags, sizeof(flags)); } #endif // #if WINVER >= 0x0501 } //----------------------------------------------------------------------------- // Wow64 // Wow64 'helpfully' redirects all 32-bit apps' accesses of // %windir%\\system32\\drivers to %windir%\\system32\\drivers\\SysWOW64. // that's bad, because the actual drivers are not in the subdirectory. to // work around this, provide for temporarily disabling redirection. static WUTIL_FUNC(pIsWow64Process, BOOL, (HANDLE, PBOOL)); static WUTIL_FUNC(pWow64DisableWow64FsRedirection, BOOL, (PVOID*)); static WUTIL_FUNC(pWow64RevertWow64FsRedirection, BOOL, (PVOID)); static bool isWow64; static void ImportWow64Functions() { WUTIL_IMPORT_KERNEL32(IsWow64Process, pIsWow64Process); WUTIL_IMPORT_KERNEL32(Wow64DisableWow64FsRedirection, pWow64DisableWow64FsRedirection); WUTIL_IMPORT_KERNEL32(Wow64RevertWow64FsRedirection, pWow64RevertWow64FsRedirection); } static void DetectWow64() { // function not found => running on 32-bit Windows if(!pIsWow64Process) { isWow64 = false; return; } BOOL isWow64Process = FALSE; const BOOL ok = pIsWow64Process(GetCurrentProcess(), &isWow64Process); WARN_IF_FALSE(ok); isWow64 = (isWow64Process == TRUE); } bool wutil_IsWow64() { return isWow64; } WinScopedDisableWow64Redirection::WinScopedDisableWow64Redirection() { // note: don't just check if the function pointers are valid. 32-bit // Vista includes them but isn't running Wow64, so calling the functions // would fail. since we have to check if actually on Wow64, there's no // more need to verify the pointers (their existence is implied). if(!wutil_IsWow64()) return; const BOOL ok = pWow64DisableWow64FsRedirection(&m_wasRedirectionEnabled); WARN_IF_FALSE(ok); } WinScopedDisableWow64Redirection::~WinScopedDisableWow64Redirection() { if(!wutil_IsWow64()) return; const BOOL ok = pWow64RevertWow64FsRedirection(m_wasRedirectionEnabled); WARN_IF_FALSE(ok); } //----------------------------------------------------------------------------- Status wutil_SetPrivilege(const wchar_t* privilege, bool enable) { WinScopedPreserveLastError s; HANDLE hToken; if(!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY, &hToken)) return ERR::_1; TOKEN_PRIVILEGES tp; if (!LookupPrivilegeValueW(NULL, privilege, &tp.Privileges[0].Luid)) return ERR::_2; tp.PrivilegeCount = 1; tp.Privileges[0].Attributes = enable? SE_PRIVILEGE_ENABLED : 0; SetLastError(0); const BOOL ok = AdjustTokenPrivileges(hToken, FALSE, &tp, 0, 0, 0); if(!ok || GetLastError() != 0) return ERR::_3; WARN_IF_FALSE(CloseHandle(hToken)); return INFO::OK; } //----------------------------------------------------------------------------- // module handle #ifndef LIB_STATIC_LINK #include "lib/sysdep/os/win/wdll_main.h" HMODULE wutil_LibModuleHandle() { HMODULE hModule; const DWORD flags = GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS|GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT; const BOOL ok = GetModuleHandleEx(flags, (LPCWSTR)&wutil_LibModuleHandle, &hModule); // (avoid ENSURE etc. because we're called from debug_DisplayError) wdbg_assert(ok); return hModule; } #else HMODULE wutil_LibModuleHandle() { return GetModuleHandle(0); } #endif //----------------------------------------------------------------------------- // find main window // this is required by the error dialog and clipboard code. // note that calling from wutil_Init won't work, because the app will not // have created its window by then. static HWND hAppWindow; static BOOL CALLBACK FindAppWindowByPid(HWND hWnd, LPARAM UNUSED(lParam)) { DWORD pid; DWORD tid = GetWindowThreadProcessId(hWnd, &pid); UNUSED2(tid); if(pid == GetCurrentProcessId()) - hAppWindow = hWnd; + { + char windowName[100]; + GetWindowTextA(hWnd, windowName, 99); + if (strcmp(windowName, main_window_name) == 0) + hAppWindow = hWnd; + } return TRUE; // keep calling } HWND wutil_AppWindow() { if(!hAppWindow) { WARN_IF_FALSE(EnumWindows(FindAppWindowByPid, 0)); // (hAppWindow may still be 0 if we haven't created a window yet) } return hAppWindow; } //----------------------------------------------------------------------------- static Status wutil_Init() { InitLocks(); ForciblyLoadUser32Dll(); EnableLowFragmentationHeap(); ReadCommandLine(); GetDirectories(); ImportWow64Functions(); DetectWow64(); return INFO::OK; } static Status wutil_Shutdown() { FreeCommandLine(); FreeUser32Dll(); ShutdownLocks(); FreeDirectories(); return INFO::OK; } Index: ps/trunk/source/network/NetClient.cpp =================================================================== --- ps/trunk/source/network/NetClient.cpp (revision 24529) +++ ps/trunk/source/network/NetClient.cpp (revision 24530) @@ -1,851 +1,852 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "NetClient.h" #include "NetClientTurnManager.h" #include "NetMessage.h" #include "NetSession.h" #include "lib/byte_order.h" #include "lib/external_libraries/enet.h" #include "lib/sysdep/sysdep.h" #include "lobby/IXmppClient.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/Compress.h" #include "ps/CStr.h" #include "ps/Game.h" #include "ps/Loader.h" #include "ps/Profile.h" +#include "ps/Threading.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" CNetClient *g_NetClient = NULL; /** * Async task for receiving the initial game state when rejoining an * in-progress network game. */ class CNetFileReceiveTask_ClientRejoin : public CNetFileReceiveTask { NONCOPYABLE(CNetFileReceiveTask_ClientRejoin); public: CNetFileReceiveTask_ClientRejoin(CNetClient& client) : m_Client(client) { } virtual void OnComplete() { // We've received the game state from the server // Save it so we can use it after the map has finished loading m_Client.m_JoinSyncBuffer = m_Buffer; // Pretend the server told us to start the game CGameStartMessage start; m_Client.HandleMessage(&start); } private: CNetClient& m_Client; }; CNetClient::CNetClient(CGame* game, bool isLocalClient) : m_Session(NULL), m_UserName(L"anonymous"), m_HostID((u32)-1), m_ClientTurnManager(NULL), m_Game(game), m_GameAttributes(game->GetSimulation2()->GetScriptInterface().GetGeneralJSContext()), m_IsLocalClient(isLocalClient), m_LastConnectionCheck(0), m_Rejoin(false) { m_Game->SetTurnManager(NULL); // delete the old local turn manager so we don't accidentally use it void* context = this; JS_AddExtraGCRootsTracer(GetScriptInterface().GetGeneralJSContext(), CNetClient::Trace, this); // Set up transitions for session AddTransition(NCS_UNCONNECTED, (uint)NMT_CONNECT_COMPLETE, NCS_CONNECT, (void*)&OnConnect, context); AddTransition(NCS_CONNECT, (uint)NMT_SERVER_HANDSHAKE, NCS_HANDSHAKE, (void*)&OnHandshake, context); AddTransition(NCS_HANDSHAKE, (uint)NMT_SERVER_HANDSHAKE_RESPONSE, NCS_AUTHENTICATE, (void*)&OnHandshakeResponse, context); AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NCS_AUTHENTICATE, (void*)&OnAuthenticateRequest, context); AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE_RESULT, NCS_INITIAL_GAMESETUP, (void*)&OnAuthenticate, context); AddTransition(NCS_INITIAL_GAMESETUP, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context); AddTransition(NCS_PREGAME, (uint)NMT_CHAT, NCS_PREGAME, (void*)&OnChat, context); AddTransition(NCS_PREGAME, (uint)NMT_READY, NCS_PREGAME, (void*)&OnReady, context); AddTransition(NCS_PREGAME, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context); AddTransition(NCS_PREGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_PREGAME, (void*)&OnPlayerAssignment, context); AddTransition(NCS_PREGAME, (uint)NMT_KICKED, NCS_PREGAME, (void*)&OnKicked, context); AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_PREGAME, (void*)&OnClientTimeout, context); AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_PREGAME, (void*)&OnClientPerformance, context); AddTransition(NCS_PREGAME, (uint)NMT_GAME_START, NCS_LOADING, (void*)&OnGameStart, context); AddTransition(NCS_PREGAME, (uint)NMT_JOIN_SYNC_START, NCS_JOIN_SYNCING, (void*)&OnJoinSyncStart, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CHAT, NCS_JOIN_SYNCING, (void*)&OnChat, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_SETUP, NCS_JOIN_SYNCING, (void*)&OnGameSetup, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_JOIN_SYNCING, (void*)&OnPlayerAssignment, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_KICKED, NCS_JOIN_SYNCING, (void*)&OnKicked, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_TIMEOUT, NCS_JOIN_SYNCING, (void*)&OnClientTimeout, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_PERFORMANCE, NCS_JOIN_SYNCING, (void*)&OnClientPerformance, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_START, NCS_JOIN_SYNCING, (void*)&OnGameStart, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_SIMULATION_COMMAND, NCS_JOIN_SYNCING, (void*)&OnInGame, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_END_COMMAND_BATCH, NCS_JOIN_SYNCING, (void*)&OnJoinSyncEndCommandBatch, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context); AddTransition(NCS_LOADING, (uint)NMT_CHAT, NCS_LOADING, (void*)&OnChat, context); AddTransition(NCS_LOADING, (uint)NMT_GAME_SETUP, NCS_LOADING, (void*)&OnGameSetup, context); AddTransition(NCS_LOADING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_LOADING, (void*)&OnPlayerAssignment, context); AddTransition(NCS_LOADING, (uint)NMT_KICKED, NCS_LOADING, (void*)&OnKicked, context); AddTransition(NCS_LOADING, (uint)NMT_CLIENT_TIMEOUT, NCS_LOADING, (void*)&OnClientTimeout, context); AddTransition(NCS_LOADING, (uint)NMT_CLIENT_PERFORMANCE, NCS_LOADING, (void*)&OnClientPerformance, context); AddTransition(NCS_LOADING, (uint)NMT_CLIENTS_LOADING, NCS_LOADING, (void*)&OnClientsLoading, context); AddTransition(NCS_LOADING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context); AddTransition(NCS_INGAME, (uint)NMT_REJOINED, NCS_INGAME, (void*)&OnRejoined, context); AddTransition(NCS_INGAME, (uint)NMT_KICKED, NCS_INGAME, (void*)&OnKicked, context); AddTransition(NCS_INGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_INGAME, (void*)&OnClientTimeout, context); AddTransition(NCS_INGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_INGAME, (void*)&OnClientPerformance, context); AddTransition(NCS_INGAME, (uint)NMT_CLIENTS_LOADING, NCS_INGAME, (void*)&OnClientsLoading, context); AddTransition(NCS_INGAME, (uint)NMT_CLIENT_PAUSED, NCS_INGAME, (void*)&OnClientPaused, context); AddTransition(NCS_INGAME, (uint)NMT_CHAT, NCS_INGAME, (void*)&OnChat, context); AddTransition(NCS_INGAME, (uint)NMT_GAME_SETUP, NCS_INGAME, (void*)&OnGameSetup, context); AddTransition(NCS_INGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_INGAME, (void*)&OnPlayerAssignment, context); AddTransition(NCS_INGAME, (uint)NMT_SIMULATION_COMMAND, NCS_INGAME, (void*)&OnInGame, context); AddTransition(NCS_INGAME, (uint)NMT_SYNC_ERROR, NCS_INGAME, (void*)&OnInGame, context); AddTransition(NCS_INGAME, (uint)NMT_END_COMMAND_BATCH, NCS_INGAME, (void*)&OnInGame, context); // Set first state SetFirstState(NCS_UNCONNECTED); } CNetClient::~CNetClient() { // Try to flush messages before dying (probably fails). if (m_ClientTurnManager) m_ClientTurnManager->OnDestroyConnection(); DestroyConnection(); JS_RemoveExtraGCRootsTracer(GetScriptInterface().GetGeneralJSContext(), CNetClient::Trace, this); } void CNetClient::TraceMember(JSTracer *trc) { for (JS::Heap& guiMessage : m_GuiMessageQueue) JS::TraceEdge(trc, &guiMessage, "m_GuiMessageQueue"); } void CNetClient::SetUserName(const CStrW& username) { ENSURE(!m_Session); // must be called before we start the connection m_UserName = username; } void CNetClient::SetHostingPlayerName(const CStr& hostingPlayerName) { m_HostingPlayerName = hostingPlayerName; } bool CNetClient::SetupConnection(const CStr& server, const u16 port, ENetHost* enetClient) { CNetClientSession* session = new CNetClientSession(*this); bool ok = session->Connect(server, port, m_IsLocalClient, enetClient); SetAndOwnSession(session); - m_PollingThread = std::thread(CNetClientSession::RunNetLoop, m_Session); + m_PollingThread = std::thread(Threading::HandleExceptions::Wrapper, m_Session); return ok; } void CNetClient::SetAndOwnSession(CNetClientSession* session) { delete m_Session; m_Session = session; } void CNetClient::DestroyConnection() { if (m_Session) m_Session->Shutdown(); if (m_PollingThread.joinable()) // Use detach() over join() because we don't want to wait for the session // (which may be polling or trying to send messages). m_PollingThread.detach(); // The polling thread will cleanup the session on its own, // mark it as nullptr here so we know we're done using it. m_Session = nullptr; } void CNetClient::Poll() { if (!m_Session) return; PROFILE3("NetClient::poll"); CheckServerConnection(); m_Session->ProcessPolledMessages(); } void CNetClient::CheckServerConnection() { // Trigger local warnings if the connection to the server is bad. // At most once per second. std::time_t now = std::time(nullptr); if (now <= m_LastConnectionCheck) return; m_LastConnectionCheck = now; // Report if we are losing the connection to the server u32 lastReceived = m_Session->GetLastReceivedTime(); if (lastReceived > NETWORK_WARNING_TIMEOUT) { PushGuiMessage( "type", "netwarn", "warntype", "server-timeout", "lastReceivedTime", lastReceived); return; } // Report if we have a bad ping to the server u32 meanRTT = m_Session->GetMeanRTT(); if (meanRTT > DEFAULT_TURN_LENGTH_MP) { PushGuiMessage( "type", "netwarn", "warntype", "server-latency", "meanRTT", meanRTT); } } void CNetClient::GuiPoll(JS::MutableHandleValue ret) { if (m_GuiMessageQueue.empty()) { ret.setUndefined(); return; } ret.set(m_GuiMessageQueue.front()); m_GuiMessageQueue.pop_front(); } std::string CNetClient::TestReadGuiMessages() { ScriptRequest rq(GetScriptInterface()); std::string r; JS::RootedValue msg(rq.cx); while (true) { GuiPoll(&msg); if (msg.isUndefined()) break; r += GetScriptInterface().ToString(&msg) + "\n"; } return r; } const ScriptInterface& CNetClient::GetScriptInterface() { return m_Game->GetSimulation2()->GetScriptInterface(); } void CNetClient::PostPlayerAssignmentsToScript() { ScriptRequest rq(GetScriptInterface()); JS::RootedValue newAssignments(rq.cx); ScriptInterface::CreateObject(rq, &newAssignments); for (const std::pair& p : m_PlayerAssignments) { JS::RootedValue assignment(rq.cx); ScriptInterface::CreateObject( rq, &assignment, "name", p.second.m_Name, "player", p.second.m_PlayerID, "status", p.second.m_Status); GetScriptInterface().SetProperty(newAssignments, p.first.c_str(), assignment); } PushGuiMessage( "type", "players", "newAssignments", newAssignments); } bool CNetClient::SendMessage(const CNetMessage* message) { if (!m_Session) return false; return m_Session->SendMessage(message); } void CNetClient::HandleConnect() { Update((uint)NMT_CONNECT_COMPLETE, NULL); } void CNetClient::HandleDisconnect(u32 reason) { PushGuiMessage( "type", "netstatus", "status", "disconnected", "reason", reason); DestroyConnection(); // Update the state immediately to UNCONNECTED (don't bother with FSM transitions since // we'd need one for every single state, and we don't need to use per-state actions) SetCurrState(NCS_UNCONNECTED); } void CNetClient::SendGameSetupMessage(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface) { CGameSetupMessage gameSetup(scriptInterface); gameSetup.m_Data = attrs; SendMessage(&gameSetup); } void CNetClient::SendAssignPlayerMessage(const int playerID, const CStr& guid) { CAssignPlayerMessage assignPlayer; assignPlayer.m_PlayerID = playerID; assignPlayer.m_GUID = guid; SendMessage(&assignPlayer); } void CNetClient::SendChatMessage(const std::wstring& text) { CChatMessage chat; chat.m_Message = text; SendMessage(&chat); } void CNetClient::SendReadyMessage(const int status) { CReadyMessage readyStatus; readyStatus.m_Status = status; SendMessage(&readyStatus); } void CNetClient::SendClearAllReadyMessage() { CClearAllReadyMessage clearAllReady; SendMessage(&clearAllReady); } void CNetClient::SendStartGameMessage() { CGameStartMessage gameStart; SendMessage(&gameStart); } void CNetClient::SendRejoinedMessage() { CRejoinedMessage rejoinedMessage; SendMessage(&rejoinedMessage); } void CNetClient::SendKickPlayerMessage(const CStrW& playerName, bool ban) { CKickedMessage kickPlayer; kickPlayer.m_Name = playerName; kickPlayer.m_Ban = ban; SendMessage(&kickPlayer); } void CNetClient::SendPausedMessage(bool pause) { CClientPausedMessage pausedMessage; pausedMessage.m_Pause = pause; SendMessage(&pausedMessage); } bool CNetClient::HandleMessage(CNetMessage* message) { // Handle non-FSM messages first Status status = m_Session->GetFileTransferer().HandleMessageReceive(*message); if (status == INFO::OK) return true; if (status != INFO::SKIPPED) return false; if (message->GetType() == NMT_FILE_TRANSFER_REQUEST) { CFileTransferRequestMessage* reqMessage = static_cast(message); // TODO: we should support different transfer request types, instead of assuming // it's always requesting the simulation state std::stringstream stream; LOGMESSAGERENDER("Serializing game at turn %u for rejoining player", m_ClientTurnManager->GetCurrentTurn()); u32 turn = to_le32(m_ClientTurnManager->GetCurrentTurn()); stream.write((char*)&turn, sizeof(turn)); bool ok = m_Game->GetSimulation2()->SerializeState(stream); ENSURE(ok); // Compress the content with zlib to save bandwidth // (TODO: if this is still too large, compressing with e.g. LZMA works much better) std::string compressed; CompressZLib(stream.str(), compressed, true); m_Session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, compressed); return true; } // Update FSM bool ok = Update(message->GetType(), message); if (!ok) LOGERROR("Net client: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)GetCurrState()); return ok; } void CNetClient::LoadFinished() { if (!m_JoinSyncBuffer.empty()) { // We're rejoining a game, and just finished loading the initial map, // so deserialize the saved game state now std::string state; DecompressZLib(m_JoinSyncBuffer, state, true); std::stringstream stream(state); u32 turn; stream.read((char*)&turn, sizeof(turn)); turn = to_le32(turn); LOGMESSAGE("Rejoining client deserializing state at turn %u\n", turn); bool ok = m_Game->GetSimulation2()->DeserializeState(stream); ENSURE(ok); m_ClientTurnManager->ResetState(turn, turn); PushGuiMessage( "type", "netstatus", "status", "join_syncing"); } else { // Connecting at the start of a game, so we'll wait for other players to finish loading PushGuiMessage( "type", "netstatus", "status", "waiting_for_players"); } CLoadedGameMessage loaded; loaded.m_CurrentTurn = m_ClientTurnManager->GetCurrentTurn(); SendMessage(&loaded); } void CNetClient::SendAuthenticateMessage() { CAuthenticateMessage authenticate; authenticate.m_Name = m_UserName; authenticate.m_Password = L""; // TODO authenticate.m_IsLocalClient = m_IsLocalClient; SendMessage(&authenticate); } bool CNetClient::OnConnect(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CONNECT_COMPLETE); CNetClient* client = static_cast(context); client->PushGuiMessage( "type", "netstatus", "status", "connected"); return true; } bool CNetClient::OnHandshake(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE); CNetClient* client = static_cast(context); CCliHandshakeMessage handshake; handshake.m_MagicResponse = PS_PROTOCOL_MAGIC_RESPONSE; handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION; handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION; client->SendMessage(&handshake); return true; } bool CNetClient::OnHandshakeResponse(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE_RESPONSE); CNetClient* client = static_cast(context); CSrvHandshakeResponseMessage* message = static_cast(event->GetParamRef()); client->m_GUID = message->m_GUID; if (message->m_Flags & PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH) { if (g_XmppClient && !client->m_HostingPlayerName.empty()) g_XmppClient->SendIqLobbyAuth(client->m_HostingPlayerName, client->m_GUID); else { client->PushGuiMessage( "type", "netstatus", "status", "disconnected", "reason", static_cast(NDR_LOBBY_AUTH_FAILED)); LOGMESSAGE("Net client: Couldn't send lobby auth xmpp message"); } return true; } client->SendAuthenticateMessage(); return true; } bool CNetClient::OnAuthenticateRequest(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE); CNetClient* client = static_cast(context); client->SendAuthenticateMessage(); return true; } bool CNetClient::OnAuthenticate(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE_RESULT); CNetClient* client = static_cast(context); CAuthenticateResultMessage* message = static_cast(event->GetParamRef()); LOGMESSAGE("Net: Authentication result: host=%u, %s", message->m_HostID, utf8_from_wstring(message->m_Message)); client->m_HostID = message->m_HostID; client->m_Rejoin = message->m_Code == ARC_OK_REJOINING; client->PushGuiMessage( "type", "netstatus", "status", "authenticated", "rejoining", client->m_Rejoin); return true; } bool CNetClient::OnChat(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CHAT); CNetClient* client = static_cast(context); CChatMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "type", "chat", "guid", message->m_GUID, "text", message->m_Message); return true; } bool CNetClient::OnReady(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_READY); CNetClient* client = static_cast(context); CReadyMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "type", "ready", "guid", message->m_GUID, "status", message->m_Status); return true; } bool CNetClient::OnGameSetup(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_SETUP); CNetClient* client = static_cast(context); CGameSetupMessage* message = static_cast(event->GetParamRef()); client->m_GameAttributes = message->m_Data; client->PushGuiMessage( "type", "gamesetup", "data", message->m_Data); return true; } bool CNetClient::OnPlayerAssignment(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_PLAYER_ASSIGNMENT); CNetClient* client = static_cast(context); CPlayerAssignmentMessage* message = static_cast(event->GetParamRef()); // Unpack the message PlayerAssignmentMap newPlayerAssignments; for (size_t i = 0; i < message->m_Hosts.size(); ++i) { PlayerAssignment assignment; assignment.m_Enabled = true; assignment.m_Name = message->m_Hosts[i].m_Name; assignment.m_PlayerID = message->m_Hosts[i].m_PlayerID; assignment.m_Status = message->m_Hosts[i].m_Status; newPlayerAssignments[message->m_Hosts[i].m_GUID] = assignment; } client->m_PlayerAssignments.swap(newPlayerAssignments); client->PostPlayerAssignmentsToScript(); return true; } // This is called either when the host clicks the StartGame button or // if this client rejoins and finishes the download of the simstate. bool CNetClient::OnGameStart(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_START); CNetClient* client = static_cast(context); // Find the player assigned to our GUID int player = -1; if (client->m_PlayerAssignments.find(client->m_GUID) != client->m_PlayerAssignments.end()) player = client->m_PlayerAssignments[client->m_GUID].m_PlayerID; client->m_ClientTurnManager = new CNetClientTurnManager( *client->m_Game->GetSimulation2(), *client, client->m_HostID, client->m_Game->GetReplayLogger()); client->m_Game->SetPlayerID(player); client->m_Game->StartGame(&client->m_GameAttributes, ""); client->PushGuiMessage("type", "start"); return true; } bool CNetClient::OnJoinSyncStart(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_JOIN_SYNC_START); CNetClient* client = static_cast(context); // The server wants us to start downloading the game state from it, so do so client->m_Session->GetFileTransferer().StartTask( shared_ptr(new CNetFileReceiveTask_ClientRejoin(*client)) ); return true; } bool CNetClient::OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH); CNetClient* client = static_cast(context); CEndCommandBatchMessage* endMessage = (CEndCommandBatchMessage*)event->GetParamRef(); client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength); // Execute all the received commands for the latest turn client->m_ClientTurnManager->UpdateFastForward(); return true; } bool CNetClient::OnRejoined(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_REJOINED); CNetClient* client = static_cast(context); CRejoinedMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "type", "rejoined", "guid", message->m_GUID); return true; } bool CNetClient::OnKicked(void *context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_KICKED); CNetClient* client = static_cast(context); CKickedMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "username", message->m_Name, "type", "kicked", "banned", message->m_Ban != 0); return true; } bool CNetClient::OnClientTimeout(void *context, CFsmEvent* event) { // Report the timeout of some other client ENSURE(event->GetType() == (uint)NMT_CLIENT_TIMEOUT); CNetClient* client = static_cast(context); CClientTimeoutMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "type", "netwarn", "warntype", "client-timeout", "guid", message->m_GUID, "lastReceivedTime", message->m_LastReceivedTime); return true; } bool CNetClient::OnClientPerformance(void *context, CFsmEvent* event) { // Performance statistics for one or multiple clients ENSURE(event->GetType() == (uint)NMT_CLIENT_PERFORMANCE); CNetClient* client = static_cast(context); CClientPerformanceMessage* message = static_cast(event->GetParamRef()); // Display warnings for other clients with bad ping for (size_t i = 0; i < message->m_Clients.size(); ++i) { if (message->m_Clients[i].m_MeanRTT < DEFAULT_TURN_LENGTH_MP || message->m_Clients[i].m_GUID == client->m_GUID) continue; client->PushGuiMessage( "type", "netwarn", "warntype", "client-latency", "guid", message->m_Clients[i].m_GUID, "meanRTT", message->m_Clients[i].m_MeanRTT); } return true; } bool CNetClient::OnClientsLoading(void *context, CFsmEvent *event) { ENSURE(event->GetType() == (uint)NMT_CLIENTS_LOADING); CNetClient* client = static_cast(context); CClientsLoadingMessage* message = static_cast(event->GetParamRef()); std::vector guids; guids.reserve(message->m_Clients.size()); for (const CClientsLoadingMessage::S_m_Clients& mClient : message->m_Clients) guids.push_back(mClient.m_GUID); client->PushGuiMessage( "type", "clients-loading", "guids", guids); return true; } bool CNetClient::OnClientPaused(void *context, CFsmEvent *event) { ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED); CNetClient* client = static_cast(context); CClientPausedMessage* message = static_cast(event->GetParamRef()); client->PushGuiMessage( "type", "paused", "pause", message->m_Pause != 0, "guid", message->m_GUID); return true; } bool CNetClient::OnLoadedGame(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); CNetClient* client = static_cast(context); // All players have loaded the game - start running the turn manager // so that the game begins client->m_Game->SetTurnManager(client->m_ClientTurnManager); client->PushGuiMessage( "type", "netstatus", "status", "active"); // If we have rejoined an in progress game, send the rejoined message to the server. if (client->m_Rejoin) client->SendRejoinedMessage(); return true; } bool CNetClient::OnInGame(void *context, CFsmEvent* event) { // TODO: should split each of these cases into a separate method CNetClient* client = static_cast(context); CNetMessage* message = static_cast(event->GetParamRef()); if (message) { if (message->GetType() == NMT_SIMULATION_COMMAND) { CSimulationMessage* simMessage = static_cast (message); client->m_ClientTurnManager->OnSimulationMessage(simMessage); } else if (message->GetType() == NMT_SYNC_ERROR) { CSyncErrorMessage* syncMessage = static_cast (message); client->m_ClientTurnManager->OnSyncError(syncMessage->m_Turn, syncMessage->m_HashExpected, syncMessage->m_PlayerNames); } else if (message->GetType() == NMT_END_COMMAND_BATCH) { CEndCommandBatchMessage* endMessage = static_cast (message); client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength); } } return true; } Index: ps/trunk/source/network/NetServer.cpp =================================================================== --- ps/trunk/source/network/NetServer.cpp (revision 24529) +++ ps/trunk/source/network/NetServer.cpp (revision 24530) @@ -1,1625 +1,1626 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "NetServer.h" #include "NetClient.h" #include "NetMessage.h" #include "NetSession.h" #include "NetServerTurnManager.h" #include "NetStats.h" #include "lib/external_libraries/enet.h" #include "lib/types.h" #include "network/StunClient.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/GUID.h" #include "ps/Profile.h" +#include "ps/Threading.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/system/TurnManager.h" #if CONFIG2_MINIUPNPC #include #include #include #include #endif #include /** * Number of peers to allocate for the enet host. * Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096). * * At most 8 players, 32 observers and 1 temporary connection to send the "server full" disconnect-reason. */ #define MAX_CLIENTS 41 #define DEFAULT_SERVER_NAME L"Unnamed Server" constexpr int CHANNEL_COUNT = 1; /** * enet_host_service timeout (msecs). * Smaller numbers may hurt performance; larger numbers will * hurt latency responding to messages from game thread. */ static const int HOST_SERVICE_TIMEOUT = 50; CNetServer* g_NetServer = NULL; static CStr DebugName(CNetServerSession* session) { if (session == NULL) return "[unknown host]"; if (session->GetGUID().empty()) return "[unauthed host]"; return "[" + session->GetGUID().substr(0, 8) + "...]"; } /** * Async task for receiving the initial game state to be forwarded to another * client that is rejoining an in-progress network game. */ class CNetFileReceiveTask_ServerRejoin : public CNetFileReceiveTask { NONCOPYABLE(CNetFileReceiveTask_ServerRejoin); public: CNetFileReceiveTask_ServerRejoin(CNetServerWorker& server, u32 hostID) : m_Server(server), m_RejoinerHostID(hostID) { } virtual void OnComplete() { // We've received the game state from an existing player - now // we need to send it onwards to the newly rejoining player // Find the session corresponding to the rejoining host (if any) CNetServerSession* session = NULL; for (CNetServerSession* serverSession : m_Server.m_Sessions) { if (serverSession->GetHostID() == m_RejoinerHostID) { session = serverSession; break; } } if (!session) { LOGMESSAGE("Net server: rejoining client disconnected before we sent to it"); return; } // Store the received state file, and tell the client to start downloading it from us // TODO: this will get kind of confused if there's multiple clients downloading in parallel; // they'll race and get whichever happens to be the latest received by the server, // which should still work but isn't great m_Server.m_JoinSyncFile = m_Buffer; CJoinSyncStartMessage message; session->SendMessage(&message); } private: CNetServerWorker& m_Server; u32 m_RejoinerHostID; }; /* * XXX: We use some non-threadsafe functions from the worker thread. * See http://trac.wildfiregames.com/ticket/654 */ CNetServerWorker::CNetServerWorker(bool useLobbyAuth, int autostartPlayers) : m_AutostartPlayers(autostartPlayers), m_LobbyAuth(useLobbyAuth), m_Shutdown(false), m_ScriptInterface(NULL), m_NextHostID(1), m_Host(NULL), m_HostGUID(), m_Stats(NULL), m_LastConnectionCheck(0) { m_State = SERVER_STATE_UNCONNECTED; m_ServerTurnManager = NULL; m_ServerName = DEFAULT_SERVER_NAME; } CNetServerWorker::~CNetServerWorker() { if (m_State != SERVER_STATE_UNCONNECTED) { // Tell the thread to shut down { std::lock_guard lock(m_WorkerMutex); m_Shutdown = true; } // Wait for it to shut down cleanly m_WorkerThread.join(); } #if CONFIG2_MINIUPNPC if (m_UPnPThread.joinable()) m_UPnPThread.detach(); #endif // Clean up resources delete m_Stats; for (CNetServerSession* session : m_Sessions) { session->DisconnectNow(NDR_SERVER_SHUTDOWN); delete session; } if (m_Host) enet_host_destroy(m_Host); delete m_ServerTurnManager; } bool CNetServerWorker::SetupConnection(const u16 port) { ENSURE(m_State == SERVER_STATE_UNCONNECTED); ENSURE(!m_Host); // Bind to default host ENetAddress addr; addr.host = ENET_HOST_ANY; addr.port = port; // Create ENet server m_Host = enet_host_create(&addr, MAX_CLIENTS, CHANNEL_COUNT, 0, 0); if (!m_Host) { LOGERROR("Net server: enet_host_create failed"); return false; } m_Stats = new CNetStatsTable(); if (CProfileViewer::IsInitialised()) g_ProfileViewer.AddRootTable(m_Stats); m_State = SERVER_STATE_PREGAME; // Launch the worker thread - m_WorkerThread = std::thread(RunThread, this); + m_WorkerThread = std::thread(Threading::HandleExceptions::Wrapper, this); #if CONFIG2_MINIUPNPC // Launch the UPnP thread - m_UPnPThread = std::thread(SetupUPnP); + m_UPnPThread = std::thread(Threading::HandleExceptions::Wrapper); #endif return true; } #if CONFIG2_MINIUPNPC void CNetServerWorker::SetupUPnP() { debug_SetThreadName("UPnP"); // Values we want to set. char psPort[6]; sprintf_s(psPort, ARRAY_SIZE(psPort), "%d", PS_DEFAULT_PORT); const char* leaseDuration = "0"; // Indefinite/permanent lease duration. const char* description = "0AD Multiplayer"; const char* protocall = "UDP"; char internalIPAddress[64]; char externalIPAddress[40]; // Variables to hold the values that actually get set. char intClient[40]; char intPort[6]; char duration[16]; // Intermediate variables. bool allocatedUrls = false; struct UPNPUrls urls; struct IGDdatas data; struct UPNPDev* devlist = NULL; // Make sure everything is properly freed. std::function freeUPnP = [&allocatedUrls, &urls, &devlist]() { if (allocatedUrls) FreeUPNPUrls(&urls); freeUPNPDevlist(devlist); // IGDdatas does not need to be freed according to UPNP_GetIGDFromUrl }; // Cached root descriptor URL. std::string rootDescURL; CFG_GET_VAL("network.upnprootdescurl", rootDescURL); if (!rootDescURL.empty()) LOGMESSAGE("Net server: attempting to use cached root descriptor URL: %s", rootDescURL.c_str()); int ret = 0; // Try a cached URL first if (!rootDescURL.empty() && UPNP_GetIGDFromUrl(rootDescURL.c_str(), &urls, &data, internalIPAddress, sizeof(internalIPAddress))) { LOGMESSAGE("Net server: using cached IGD = %s", urls.controlURL); ret = 1; } // No cached URL, or it did not respond. Try getting a valid UPnP device for 10 seconds. #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 14 else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 2, 0)) != NULL) #else else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 0)) != NULL) #endif { ret = UPNP_GetValidIGD(devlist, &urls, &data, internalIPAddress, sizeof(internalIPAddress)); allocatedUrls = ret != 0; // urls is allocated on non-zero return values } else { LOGMESSAGE("Net server: upnpDiscover failed and no working cached URL."); freeUPnP(); return; } switch (ret) { case 0: LOGMESSAGE("Net server: No IGD found"); break; case 1: LOGMESSAGE("Net server: found valid IGD = %s", urls.controlURL); break; case 2: LOGMESSAGE("Net server: found a valid, not connected IGD = %s, will try to continue anyway", urls.controlURL); break; case 3: LOGMESSAGE("Net server: found a UPnP device unrecognized as IGD = %s, will try to continue anyway", urls.controlURL); break; default: debug_warn(L"Unrecognized return value from UPNP_GetValidIGD"); } // Try getting our external/internet facing IP. TODO: Display this on the game-setup page for conviniance. ret = UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, externalIPAddress); if (ret != UPNPCOMMAND_SUCCESS) { LOGMESSAGE("Net server: GetExternalIPAddress failed with code %d (%s)", ret, strupnperror(ret)); freeUPnP(); return; } LOGMESSAGE("Net server: ExternalIPAddress = %s", externalIPAddress); // Try to setup port forwarding. ret = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, psPort, psPort, internalIPAddress, description, protocall, 0, leaseDuration); if (ret != UPNPCOMMAND_SUCCESS) { LOGMESSAGE("Net server: AddPortMapping(%s, %s, %s) failed with code %d (%s)", psPort, psPort, internalIPAddress, ret, strupnperror(ret)); freeUPnP(); return; } // Check that the port was actually forwarded. ret = UPNP_GetSpecificPortMappingEntry(urls.controlURL, data.first.servicetype, psPort, protocall, #if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 10 NULL/*remoteHost*/, #endif intClient, intPort, NULL/*desc*/, NULL/*enabled*/, duration); if (ret != UPNPCOMMAND_SUCCESS) { LOGMESSAGE("Net server: GetSpecificPortMappingEntry() failed with code %d (%s)", ret, strupnperror(ret)); freeUPnP(); return; } LOGMESSAGE("Net server: External %s:%s %s is redirected to internal %s:%s (duration=%s)", externalIPAddress, psPort, protocall, intClient, intPort, duration); // Cache root descriptor URL to try to avoid discovery next time. g_ConfigDB.SetValueString(CFG_USER, "network.upnprootdescurl", urls.controlURL); g_ConfigDB.WriteValueToFile(CFG_USER, "network.upnprootdescurl", urls.controlURL); LOGMESSAGE("Net server: cached UPnP root descriptor URL as %s", urls.controlURL); freeUPnP(); } #endif // CONFIG2_MINIUPNPC bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message) { ENSURE(m_Host); CNetServerSession* session = static_cast(peer->data); return CNetHost::SendMessage(message, peer, DebugName(session).c_str()); } bool CNetServerWorker::Broadcast(const CNetMessage* message, const std::vector& targetStates) { ENSURE(m_Host); bool ok = true; // TODO: this does lots of repeated message serialisation if we have lots // of remote peers; could do it more efficiently if that's a real problem for (CNetServerSession* session : m_Sessions) if (std::find(targetStates.begin(), targetStates.end(), static_cast(session->GetCurrState())) != targetStates.end() && !session->SendMessage(message)) ok = false; return ok; } void CNetServerWorker::RunThread(CNetServerWorker* data) { debug_SetThreadName("NetServer"); data->Run(); } void CNetServerWorker::Run() { // The script context uses the profiler and therefore the thread must be registered before the context is created g_Profiler2.RegisterCurrentThread("Net server"); // We create a new ScriptContext for this network thread, with a single ScriptInterface. shared_ptr netServerContext = ScriptContext::CreateContext(); m_ScriptInterface = new ScriptInterface("Engine", "Net server", netServerContext); m_GameAttributes.init(m_ScriptInterface->GetGeneralJSContext(), JS::UndefinedValue()); while (true) { if (!RunStep()) break; // Implement autostart mode if (m_State == SERVER_STATE_PREGAME && (int)m_PlayerAssignments.size() == m_AutostartPlayers) StartGame(); // Update profiler stats m_Stats->LatchHostState(m_Host); } // Clear roots before deleting their context m_SavedCommands.clear(); SAFE_DELETE(m_ScriptInterface); } bool CNetServerWorker::RunStep() { // Check for messages from the game thread. // (Do as little work as possible while the mutex is held open, // to avoid performance problems and deadlocks.) m_ScriptInterface->GetContext()->MaybeIncrementalGC(0.5f); ScriptRequest rq(m_ScriptInterface); std::vector newStartGame; std::vector newGameAttributes; std::vector> newLobbyAuths; std::vector newTurnLength; { std::lock_guard lock(m_WorkerMutex); if (m_Shutdown) return false; newStartGame.swap(m_StartGameQueue); newGameAttributes.swap(m_GameAttributesQueue); newLobbyAuths.swap(m_LobbyAuthQueue); newTurnLength.swap(m_TurnLengthQueue); } if (!newGameAttributes.empty()) { JS::RootedValue gameAttributesVal(rq.cx); GetScriptInterface().ParseJSON(newGameAttributes.back(), &gameAttributesVal); UpdateGameAttributes(&gameAttributesVal); } if (!newTurnLength.empty()) SetTurnLength(newTurnLength.back()); // Do StartGame last, so we have the most up-to-date game attributes when we start if (!newStartGame.empty()) StartGame(); while (!newLobbyAuths.empty()) { const std::pair& auth = newLobbyAuths.back(); ProcessLobbyAuth(auth.first, auth.second); newLobbyAuths.pop_back(); } // Perform file transfers for (CNetServerSession* session : m_Sessions) session->GetFileTransferer().Poll(); CheckClientConnections(); // Process network events: ENetEvent event; int status = enet_host_service(m_Host, &event, HOST_SERVICE_TIMEOUT); if (status < 0) { LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status); // TODO: notify game that the server has shut down return false; } if (status == 0) { // Reached timeout with no events - try again return true; } // Process the event: switch (event.type) { case ENET_EVENT_TYPE_CONNECT: { // Report the client address char hostname[256] = "(error)"; enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname)); LOGMESSAGE("Net server: Received connection from %s:%u", hostname, (unsigned int)event.peer->address.port); // Set up a session object for this peer CNetServerSession* session = new CNetServerSession(*this, event.peer); m_Sessions.push_back(session); SetupSession(session); ENSURE(event.peer->data == NULL); event.peer->data = session; HandleConnect(session); break; } case ENET_EVENT_TYPE_DISCONNECT: { // If there is an active session with this peer, then reset and delete it CNetServerSession* session = static_cast(event.peer->data); if (session) { LOGMESSAGE("Net server: Disconnected %s", DebugName(session).c_str()); // Remove the session first, so we won't send player-update messages to it // when updating the FSM m_Sessions.erase(remove(m_Sessions.begin(), m_Sessions.end(), session), m_Sessions.end()); session->Update((uint)NMT_CONNECTION_LOST, NULL); delete session; event.peer->data = NULL; } if (m_State == SERVER_STATE_LOADING) CheckGameLoadStatus(NULL); break; } case ENET_EVENT_TYPE_RECEIVE: { // If there is an active session with this peer, then process the message CNetServerSession* session = static_cast(event.peer->data); if (session) { // Create message from raw data CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface()); if (msg) { LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str()); HandleMessageReceive(msg, session); delete msg; } } // Done using the packet enet_packet_destroy(event.packet); break; } case ENET_EVENT_TYPE_NONE: break; } return true; } void CNetServerWorker::CheckClientConnections() { // Send messages at most once per second std::time_t now = std::time(nullptr); if (now <= m_LastConnectionCheck) return; m_LastConnectionCheck = now; for (size_t i = 0; i < m_Sessions.size(); ++i) { u32 lastReceived = m_Sessions[i]->GetLastReceivedTime(); u32 meanRTT = m_Sessions[i]->GetMeanRTT(); CNetMessage* message = nullptr; // Report if we didn't hear from the client since few seconds if (lastReceived > NETWORK_WARNING_TIMEOUT) { CClientTimeoutMessage* msg = new CClientTimeoutMessage(); msg->m_GUID = m_Sessions[i]->GetGUID(); msg->m_LastReceivedTime = lastReceived; message = msg; } // Report if the client has bad ping else if (meanRTT > DEFAULT_TURN_LENGTH_MP) { CClientPerformanceMessage* msg = new CClientPerformanceMessage(); CClientPerformanceMessage::S_m_Clients client; client.m_GUID = m_Sessions[i]->GetGUID(); client.m_MeanRTT = meanRTT; msg->m_Clients.push_back(client); message = msg; } // Send to all clients except the affected one // (since that will show the locally triggered warning instead). // Also send it to clients that finished the loading screen while // the game is still waiting for other clients to finish the loading screen. if (message) for (size_t j = 0; j < m_Sessions.size(); ++j) { if (i != j && ( (m_Sessions[j]->GetCurrState() == NSS_PREGAME && m_State == SERVER_STATE_PREGAME) || m_Sessions[j]->GetCurrState() == NSS_INGAME)) { m_Sessions[j]->SendMessage(message); } } SAFE_DELETE(message); } } void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session) { // Handle non-FSM messages first Status status = session->GetFileTransferer().HandleMessageReceive(*message); if (status != INFO::SKIPPED) return; if (message->GetType() == NMT_FILE_TRANSFER_REQUEST) { CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message; // Rejoining client got our JoinSyncStart after we received the state from // another client, and has now requested that we forward it to them ENSURE(!m_JoinSyncFile.empty()); session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, m_JoinSyncFile); return; } // Update FSM if (!session->Update(message->GetType(), (void*)message)) LOGERROR("Net server: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)session->GetCurrState()); } void CNetServerWorker::SetupSession(CNetServerSession* session) { void* context = session; // Set up transitions for session session->AddTransition(NSS_UNCONNECTED, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED); session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED); session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CLIENT_HANDSHAKE, NSS_AUTHENTICATE, (void*)&OnClientHandshake, context); session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED); session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context); session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED); session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context); session->AddTransition(NSS_PREGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context); session->AddTransition(NSS_PREGAME, (uint)NMT_CHAT, NSS_PREGAME, (void*)&OnChat, context); session->AddTransition(NSS_PREGAME, (uint)NMT_READY, NSS_PREGAME, (void*)&OnReady, context); session->AddTransition(NSS_PREGAME, (uint)NMT_CLEAR_ALL_READY, NSS_PREGAME, (void*)&OnClearAllReady, context); session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_SETUP, NSS_PREGAME, (void*)&OnGameSetup, context); session->AddTransition(NSS_PREGAME, (uint)NMT_ASSIGN_PLAYER, NSS_PREGAME, (void*)&OnAssignPlayer, context); session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, (void*)&OnKickPlayer, context); session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, (void*)&OnStartGame, context); session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnLoadedGame, context); session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, (void*)&OnKickPlayer, context); session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context); session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnJoinSyncingLoadedGame, context); session->AddTransition(NSS_INGAME, (uint)NMT_REJOINED, NSS_INGAME, (void*)&OnRejoined, context); session->AddTransition(NSS_INGAME, (uint)NMT_KICKED, NSS_INGAME, (void*)&OnKickPlayer, context); session->AddTransition(NSS_INGAME, (uint)NMT_CLIENT_PAUSED, NSS_INGAME, (void*)&OnClientPaused, context); session->AddTransition(NSS_INGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context); session->AddTransition(NSS_INGAME, (uint)NMT_CHAT, NSS_INGAME, (void*)&OnChat, context); session->AddTransition(NSS_INGAME, (uint)NMT_SIMULATION_COMMAND, NSS_INGAME, (void*)&OnSimulationCommand, context); session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, (void*)&OnSyncCheck, context); session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, (void*)&OnEndCommandBatch, context); // Set first state session->SetFirstState(NSS_HANDSHAKE); } bool CNetServerWorker::HandleConnect(CNetServerSession* session) { if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), session->GetIPAddress()) != m_BannedIPs.end()) { session->Disconnect(NDR_BANNED); return false; } CSrvHandshakeMessage handshake; handshake.m_Magic = PS_PROTOCOL_MAGIC; handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION; handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION; return session->SendMessage(&handshake); } void CNetServerWorker::OnUserJoin(CNetServerSession* session) { AddPlayer(session->GetGUID(), session->GetUserName()); if (m_HostGUID.empty() && session->IsLocalClient()) m_HostGUID = session->GetGUID(); CGameSetupMessage gameSetupMessage(GetScriptInterface()); gameSetupMessage.m_Data = m_GameAttributes; session->SendMessage(&gameSetupMessage); CPlayerAssignmentMessage assignMessage; ConstructPlayerAssignmentMessage(assignMessage); session->SendMessage(&assignMessage); } void CNetServerWorker::OnUserLeave(CNetServerSession* session) { std::vector::iterator pausing = std::find(m_PausingPlayers.begin(), m_PausingPlayers.end(), session->GetGUID()); if (pausing != m_PausingPlayers.end()) m_PausingPlayers.erase(pausing); RemovePlayer(session->GetGUID()); if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING) m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: only for non-observers // TODO: ought to switch the player controlled by that client // back to AI control, or something? } void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name) { // Find all player IDs in active use; we mustn't give them to a second player (excluding the unassigned ID: -1) std::set usedIDs; for (const std::pair& p : m_PlayerAssignments) if (p.second.m_Enabled && p.second.m_PlayerID != -1) usedIDs.insert(p.second.m_PlayerID); // If the player is rejoining after disconnecting, try to give them // back their old player ID i32 playerID = -1; // Try to match GUID first for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it) { if (!it->second.m_Enabled && it->first == guid && usedIDs.find(it->second.m_PlayerID) == usedIDs.end()) { playerID = it->second.m_PlayerID; m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now goto found; } } // Try to match username next for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it) { if (!it->second.m_Enabled && it->second.m_Name == name && usedIDs.find(it->second.m_PlayerID) == usedIDs.end()) { playerID = it->second.m_PlayerID; m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now goto found; } } // Otherwise leave the player ID as -1 (observer) and let gamesetup change it as needed. found: PlayerAssignment assignment; assignment.m_Enabled = true; assignment.m_Name = name; assignment.m_PlayerID = playerID; assignment.m_Status = 0; m_PlayerAssignments[guid] = assignment; // Send the new assignments to all currently active players // (which does not include the one that's just joining) SendPlayerAssignments(); } void CNetServerWorker::RemovePlayer(const CStr& guid) { m_PlayerAssignments[guid].m_Enabled = false; SendPlayerAssignments(); } void CNetServerWorker::ClearAllPlayerReady() { for (std::pair& p : m_PlayerAssignments) if (p.second.m_Status != 2) p.second.m_Status = 0; SendPlayerAssignments(); } void CNetServerWorker::KickPlayer(const CStrW& playerName, const bool ban) { // Find the user with that name std::vector::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(), [&](CNetServerSession* session) { return session->GetUserName() == playerName; }); // and return if no one or the host has that name if (it == m_Sessions.end() || (*it)->GetGUID() == m_HostGUID) return; if (ban) { // Remember name if (std::find(m_BannedPlayers.begin(), m_BannedPlayers.end(), playerName) == m_BannedPlayers.end()) m_BannedPlayers.push_back(m_LobbyAuth ? CStrW(playerName.substr(0, playerName.find(L" ("))) : playerName); // Remember IP address u32 ipAddress = (*it)->GetIPAddress(); if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), ipAddress) == m_BannedIPs.end()) m_BannedIPs.push_back(ipAddress); } // Disconnect that user (*it)->Disconnect(ban ? NDR_BANNED : NDR_KICKED); // Send message notifying other clients CKickedMessage kickedMessage; kickedMessage.m_Name = playerName; kickedMessage.m_Ban = ban; Broadcast(&kickedMessage, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME }); } void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid) { // Remove anyone who's already assigned to this player for (std::pair& p : m_PlayerAssignments) { if (p.second.m_PlayerID == playerID) p.second.m_PlayerID = -1; } // Update this host's assignment if it exists if (m_PlayerAssignments.find(guid) != m_PlayerAssignments.end()) m_PlayerAssignments[guid].m_PlayerID = playerID; SendPlayerAssignments(); } void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message) { for (const std::pair& p : m_PlayerAssignments) { if (!p.second.m_Enabled) continue; CPlayerAssignmentMessage::S_m_Hosts h; h.m_GUID = p.first; h.m_Name = p.second.m_Name; h.m_PlayerID = p.second.m_PlayerID; h.m_Status = p.second.m_Status; message.m_Hosts.push_back(h); } } void CNetServerWorker::SendPlayerAssignments() { CPlayerAssignmentMessage message; ConstructPlayerAssignmentMessage(message); Broadcast(&message, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME }); } const ScriptInterface& CNetServerWorker::GetScriptInterface() { return *m_ScriptInterface; } void CNetServerWorker::SetTurnLength(u32 msecs) { if (m_ServerTurnManager) m_ServerTurnManager->SetTurnLength(msecs); } void CNetServerWorker::ProcessLobbyAuth(const CStr& name, const CStr& token) { LOGMESSAGE("Net Server: Received lobby auth message from %s with %s", name, token); // Find the user with that guid std::vector::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(), [&](CNetServerSession* session) { return session->GetGUID() == token; }); if (it == m_Sessions.end()) return; (*it)->SetUserName(name.FromUTF8()); // Send an empty message to request the authentication message from the client // after its identity has been confirmed via the lobby CAuthenticateMessage emptyMessage; (*it)->SendMessage(&emptyMessage); } bool CNetServerWorker::OnClientHandshake(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CLIENT_HANDSHAKE); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CCliHandshakeMessage* message = (CCliHandshakeMessage*)event->GetParamRef(); if (message->m_ProtocolVersion != PS_PROTOCOL_VERSION) { session->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION); return false; } CStr guid = ps_generate_guid(); int count = 0; // Ensure unique GUID while(std::find_if( server.m_Sessions.begin(), server.m_Sessions.end(), [&guid] (const CNetServerSession* session) { return session->GetGUID() == guid; }) != server.m_Sessions.end()) { if (++count > 100) { session->Disconnect(NDR_GUID_FAILED); return true; } guid = ps_generate_guid(); } session->SetGUID(guid); CSrvHandshakeResponseMessage handshakeResponse; handshakeResponse.m_UseProtocolVersion = PS_PROTOCOL_VERSION; handshakeResponse.m_GUID = guid; handshakeResponse.m_Flags = 0; if (server.m_LobbyAuth) { handshakeResponse.m_Flags |= PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH; session->SetNextState(NSS_LOBBY_AUTHENTICATE); } session->SendMessage(&handshakeResponse); return true; } bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); // Prohibit joins while the game is loading if (server.m_State == SERVER_STATE_LOADING) { LOGMESSAGE("Refused connection while the game is loading"); session->Disconnect(NDR_SERVER_LOADING); return true; } CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef(); CStrW username = SanitisePlayerName(message->m_Name); CStrW usernameWithoutRating(username.substr(0, username.find(L" ("))); // Compare the lowercase names as specified by https://xmpp.org/extensions/xep-0029.html#sect-idm139493404168176 // "[...] comparisons will be made in case-normalized canonical form." if (server.m_LobbyAuth && usernameWithoutRating.LowerCase() != session->GetUserName().LowerCase()) { LOGERROR("Net server: lobby auth: %s tried joining as %s", session->GetUserName().ToUTF8(), usernameWithoutRating.ToUTF8()); session->Disconnect(NDR_LOBBY_AUTH_FAILED); return true; } // Either deduplicate or prohibit join if name is in use bool duplicatePlayernames = false; CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames); // If lobby authentication is enabled, the clients playername has already been registered. // There also can't be any duplicated names. if (!server.m_LobbyAuth && duplicatePlayernames) username = server.DeduplicatePlayerName(username); else { std::vector::iterator it = std::find_if( server.m_Sessions.begin(), server.m_Sessions.end(), [&username] (const CNetServerSession* session) { return session->GetUserName() == username; }); if (it != server.m_Sessions.end() && (*it) != session) { session->Disconnect(NDR_PLAYERNAME_IN_USE); return true; } } // Disconnect banned usernames if (std::find(server.m_BannedPlayers.begin(), server.m_BannedPlayers.end(), server.m_LobbyAuth ? usernameWithoutRating : username) != server.m_BannedPlayers.end()) { session->Disconnect(NDR_BANNED); return true; } int maxObservers = 0; CFG_GET_VAL("network.observerlimit", maxObservers); bool isRejoining = false; bool serverFull = false; if (server.m_State == SERVER_STATE_PREGAME) { // Don't check for maxObservers in the gamesetup, as we don't know yet who will be assigned serverFull = server.m_Sessions.size() >= MAX_CLIENTS; } else { bool isObserver = true; int disconnectedPlayers = 0; int connectedPlayers = 0; // (TODO: if GUIDs were stable, we should use them instead) for (const std::pair& p : server.m_PlayerAssignments) { const PlayerAssignment& assignment = p.second; if (!assignment.m_Enabled && assignment.m_Name == username) { isObserver = assignment.m_PlayerID == -1; isRejoining = true; } if (assignment.m_PlayerID == -1) continue; if (assignment.m_Enabled) ++connectedPlayers; else ++disconnectedPlayers; } // Optionally allow everyone or only buddies to join after the game has started if (!isRejoining) { CStr observerLateJoin; CFG_GET_VAL("network.lateobservers", observerLateJoin); if (observerLateJoin == "everyone") { isRejoining = true; } else if (observerLateJoin == "buddies") { CStr buddies; CFG_GET_VAL("lobby.buddies", buddies); std::wstringstream buddiesStream(wstring_from_utf8(buddies)); CStrW buddy; while (std::getline(buddiesStream, buddy, L',')) { if (buddy == usernameWithoutRating) { isRejoining = true; break; } } } } if (!isRejoining) { LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username)); session->Disconnect(NDR_SERVER_ALREADY_IN_GAME); return true; } // Ensure all players will be able to rejoin serverFull = isObserver && ( (int) server.m_Sessions.size() - connectedPlayers > maxObservers || (int) server.m_Sessions.size() + disconnectedPlayers >= MAX_CLIENTS); } if (serverFull) { session->Disconnect(NDR_SERVER_FULL); return true; } // TODO: check server password etc? u32 newHostID = server.m_NextHostID++; session->SetUserName(username); session->SetHostID(newHostID); session->SetLocalClient(message->m_IsLocalClient); CAuthenticateResultMessage authenticateResult; authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK; authenticateResult.m_HostID = newHostID; authenticateResult.m_Message = L"Logged in"; session->SendMessage(&authenticateResult); server.OnUserJoin(session); if (isRejoining) { // Request a copy of the current game state from an existing player, // so we can send it on to the new player // Assume session 0 is most likely the local player, so they're // the most efficient client to request a copy from CNetServerSession* sourceSession = server.m_Sessions.at(0); sourceSession->GetFileTransferer().StartTask( shared_ptr(new CNetFileReceiveTask_ServerRejoin(server, newHostID)) ); session->SetNextState(NSS_JOIN_SYNCING); } return true; } bool CNetServerWorker::OnSimulationCommand(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SIMULATION_COMMAND); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CSimulationMessage* message = (CSimulationMessage*)event->GetParamRef(); // Ignore messages sent by one player on behalf of another player // unless cheating is enabled bool cheatsEnabled = false; const ScriptInterface& scriptInterface = server.GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue settings(rq.cx); scriptInterface.GetProperty(server.m_GameAttributes, "settings", &settings); if (scriptInterface.HasProperty(settings, "CheatsEnabled")) scriptInterface.GetProperty(settings, "CheatsEnabled", cheatsEnabled); PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.find(session->GetGUID()); // When cheating is disabled, fail if the player the message claims to // represent does not exist or does not match the sender's player name if (!cheatsEnabled && (it == server.m_PlayerAssignments.end() || it->second.m_PlayerID != message->m_Player)) return true; // Send it back to all clients that have finished // the loading screen (and the synchronization when rejoining) server.Broadcast(message, { NSS_INGAME }); // Save all the received commands if (server.m_SavedCommands.size() < message->m_Turn + 1) server.m_SavedCommands.resize(message->m_Turn + 1); server.m_SavedCommands[message->m_Turn].push_back(*message); // TODO: we shouldn't send the message back to the client that first sent it return true; } bool CNetServerWorker::OnSyncCheck(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SYNC_CHECK); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CSyncCheckMessage* message = (CSyncCheckMessage*)event->GetParamRef(); server.m_ServerTurnManager->NotifyFinishedClientUpdate(*session, message->m_Turn, message->m_Hash); return true; } bool CNetServerWorker::OnEndCommandBatch(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CEndCommandBatchMessage* message = (CEndCommandBatchMessage*)event->GetParamRef(); // The turn-length field is ignored server.m_ServerTurnManager->NotifyFinishedClientCommands(*session, message->m_Turn); return true; } bool CNetServerWorker::OnChat(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CHAT); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CChatMessage* message = (CChatMessage*)event->GetParamRef(); message->m_GUID = session->GetGUID(); server.Broadcast(message, { NSS_PREGAME, NSS_INGAME }); return true; } bool CNetServerWorker::OnReady(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_READY); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); // Occurs if a client presses not-ready // in the very last moment before the hosts starts the game if (server.m_State == SERVER_STATE_LOADING) return true; CReadyMessage* message = (CReadyMessage*)event->GetParamRef(); message->m_GUID = session->GetGUID(); server.Broadcast(message, { NSS_PREGAME }); server.m_PlayerAssignments[message->m_GUID].m_Status = message->m_Status; return true; } bool CNetServerWorker::OnClearAllReady(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CLEAR_ALL_READY); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); if (session->GetGUID() == server.m_HostGUID) server.ClearAllPlayerReady(); return true; } bool CNetServerWorker::OnGameSetup(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_SETUP); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); // Changing the settings after gamestart is not implemented and would cause an Out-of-sync error. // This happened when doubleclicking on the startgame button. if (server.m_State != SERVER_STATE_PREGAME) return true; if (session->GetGUID() == server.m_HostGUID) { CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef(); server.UpdateGameAttributes(&(message->m_Data)); } return true; } bool CNetServerWorker::OnAssignPlayer(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_ASSIGN_PLAYER); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); if (session->GetGUID() == server.m_HostGUID) { CAssignPlayerMessage* message = (CAssignPlayerMessage*)event->GetParamRef(); server.AssignPlayer(message->m_PlayerID, message->m_GUID); } return true; } bool CNetServerWorker::OnStartGame(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_START); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); if (session->GetGUID() == server.m_HostGUID) server.StartGame(); return true; } bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); CNetServerSession* loadedSession = (CNetServerSession*)context; CNetServerWorker& server = loadedSession->GetServer(); // We're in the loading state, so wait until every client has loaded // before starting the game ENSURE(server.m_State == SERVER_STATE_LOADING); if (server.CheckGameLoadStatus(loadedSession)) return true; CClientsLoadingMessage message; // We always send all GUIDs of clients in the loading state // so that we don't have to bother about switching GUI pages for (CNetServerSession* session : server.m_Sessions) if (session->GetCurrState() != NSS_INGAME && loadedSession->GetGUID() != session->GetGUID()) { CClientsLoadingMessage::S_m_Clients client; client.m_GUID = session->GetGUID(); message.m_Clients.push_back(client); } // Send to the client who has loaded the game but did not reach the NSS_INGAME state yet loadedSession->SendMessage(&message); server.Broadcast(&message, { NSS_INGAME }); return true; } bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context, CFsmEvent* event) { // A client rejoining an in-progress game has now finished loading the // map and deserialized the initial state. // The simulation may have progressed since then, so send any subsequent // commands to them and set them as an active player so they can participate // in all future turns. // // (TODO: if it takes a long time for them to receive and execute all these // commands, the other players will get frozen for that time and may be unhappy; // we could try repeating this process a few times until the client converges // on the up-to-date state, before setting them as active.) ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef(); u32 turn = message->m_CurrentTurn; u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn(); // Send them all commands received since their saved state, // and turn-ended messages for any turns that have already been processed for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i) { if (i < server.m_SavedCommands.size()) for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j) session->SendMessage(&server.m_SavedCommands[i][j]); if (i <= readyTurn) { CEndCommandBatchMessage endMessage; endMessage.m_Turn = i; endMessage.m_TurnLength = server.m_ServerTurnManager->GetSavedTurnLength(i); session->SendMessage(&endMessage); } } // Tell the turn manager to expect commands from this new client server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn); // Tell the client that everything has finished loading and it should start now CLoadedGameMessage loaded; loaded.m_CurrentTurn = readyTurn; session->SendMessage(&loaded); return true; } bool CNetServerWorker::OnRejoined(void* context, CFsmEvent* event) { // A client has finished rejoining and the loading screen disappeared. ENSURE(event->GetType() == (uint)NMT_REJOINED); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); // Inform everyone of the client having rejoined CRejoinedMessage* message = (CRejoinedMessage*)event->GetParamRef(); message->m_GUID = session->GetGUID(); server.Broadcast(message, { NSS_INGAME }); // Send all pausing players to the rejoined client. for (const CStr& guid : server.m_PausingPlayers) { CClientPausedMessage pausedMessage; pausedMessage.m_GUID = guid; pausedMessage.m_Pause = true; session->SendMessage(&pausedMessage); } return true; } bool CNetServerWorker::OnKickPlayer(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_KICKED); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); if (session->GetGUID() == server.m_HostGUID) { CKickedMessage* message = (CKickedMessage*)event->GetParamRef(); server.KickPlayer(message->m_Name, message->m_Ban); } return true; } bool CNetServerWorker::OnDisconnect(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CONNECTION_LOST); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); server.OnUserLeave(session); return true; } bool CNetServerWorker::OnClientPaused(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED); CNetServerSession* session = (CNetServerSession*)context; CNetServerWorker& server = session->GetServer(); CClientPausedMessage* message = (CClientPausedMessage*)event->GetParamRef(); message->m_GUID = session->GetGUID(); // Update the list of pausing players. std::vector::iterator player = std::find(server.m_PausingPlayers.begin(), server.m_PausingPlayers.end(), session->GetGUID()); if (message->m_Pause) { if (player != server.m_PausingPlayers.end()) return true; server.m_PausingPlayers.push_back(session->GetGUID()); } else { if (player == server.m_PausingPlayers.end()) return true; server.m_PausingPlayers.erase(player); } // Send messages to clients that are in game, and are not the client who paused. for (CNetServerSession* netSession : server.m_Sessions) if (netSession->GetCurrState() == NSS_INGAME && message->m_GUID != netSession->GetGUID()) netSession->SendMessage(message); return true; } bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession) { for (const CNetServerSession* session : m_Sessions) if (session != changedSession && session->GetCurrState() != NSS_INGAME) return false; // Inform clients that everyone has loaded the map and that the game can start CLoadedGameMessage loaded; loaded.m_CurrentTurn = 0; // Notice the changedSession is still in the NSS_PREGAME state Broadcast(&loaded, { NSS_PREGAME, NSS_INGAME }); m_State = SERVER_STATE_INGAME; return true; } void CNetServerWorker::StartGame() { for (std::pair& player : m_PlayerAssignments) if (player.second.m_Enabled && player.second.m_PlayerID != -1 && player.second.m_Status == 0) { LOGERROR("Tried to start the game without player \"%s\" being ready!", utf8_from_wstring(player.second.m_Name).c_str()); return; } m_ServerTurnManager = new CNetServerTurnManager(*this); for (CNetServerSession* session : m_Sessions) m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0); // TODO: only for non-observers m_State = SERVER_STATE_LOADING; // Send the final setup state to all clients UpdateGameAttributes(&m_GameAttributes); // Remove players and observers that are not present when the game starts for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end();) if (it->second.m_Enabled) ++it; else it = m_PlayerAssignments.erase(it); SendPlayerAssignments(); CGameStartMessage gameStart; Broadcast(&gameStart, { NSS_PREGAME }); } void CNetServerWorker::UpdateGameAttributes(JS::MutableHandleValue attrs) { m_GameAttributes = attrs; if (!m_Host) return; CGameSetupMessage gameSetupMessage(GetScriptInterface()); gameSetupMessage.m_Data = m_GameAttributes; Broadcast(&gameSetupMessage, { NSS_PREGAME }); } CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original) { const size_t MAX_LENGTH = 32; CStrW name = original; name.Replace(L"[", L"{"); // remove GUI tags name.Replace(L"]", L"}"); // remove for symmetry // Restrict the length if (name.length() > MAX_LENGTH) name = name.Left(MAX_LENGTH); // Don't allow surrounding whitespace name.Trim(PS_TRIM_BOTH); // Don't allow empty name if (name.empty()) name = L"Anonymous"; return name; } CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original) { CStrW name = original; // Try names "Foo", "Foo (2)", "Foo (3)", etc size_t id = 2; while (true) { bool unique = true; for (const CNetServerSession* session : m_Sessions) { if (session->GetUserName() == name) { unique = false; break; } } if (unique) return name; name = original + L" (" + CStrW::FromUInt(id++) + L")"; } } void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port) { if (m_Host) StunClient::SendHolePunchingMessages(*m_Host, ipStr, port); } CNetServer::CNetServer(bool useLobbyAuth, int autostartPlayers) : m_Worker(new CNetServerWorker(useLobbyAuth, autostartPlayers)), m_LobbyAuth(useLobbyAuth) { } CNetServer::~CNetServer() { delete m_Worker; } bool CNetServer::UseLobbyAuth() const { return m_LobbyAuth; } bool CNetServer::SetupConnection(const u16 port) { return m_Worker->SetupConnection(port); } void CNetServer::StartGame() { std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->m_StartGameQueue.push_back(true); } void CNetServer::UpdateGameAttributes(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface) { // Pass the attributes as JSON, since that's the easiest safe // cross-thread way of passing script data std::string attrsJSON = scriptInterface.StringifyJSON(attrs, false); std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->m_GameAttributesQueue.push_back(attrsJSON); } void CNetServer::OnLobbyAuth(const CStr& name, const CStr& token) { std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->m_LobbyAuthQueue.push_back(std::make_pair(name, token)); } void CNetServer::SetTurnLength(u32 msecs) { std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->m_TurnLengthQueue.push_back(msecs); } void CNetServer::SendHolePunchingMessage(const CStr& ip, u16 port) { m_Worker->SendHolePunchingMessage(ip, port); } Index: ps/trunk/source/ps/CStrIntern.cpp =================================================================== --- ps/trunk/source/ps/CStrIntern.cpp (revision 24529) +++ ps/trunk/source/ps/CStrIntern.cpp (revision 24530) @@ -1,151 +1,151 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "CStrIntern.h" #include "lib/fnv_hash.h" #include "ps/CLogger.h" #include "ps/ThreadUtil.h" #include class CStrInternInternals { public: CStrInternInternals(const char* str, size_t len) : data(str, str+len), hash(fnv_hash(str, len)) { // LOGWARNING("New interned string '%s'", data.c_str()); } bool operator==(const CStrInternInternals& b) const { // Compare hash first for quick rejection of inequal strings return (hash == b.hash && data == b.data); } const std::string data; const u32 hash; // fnv_hash of data private: CStrInternInternals& operator=(const CStrInternInternals&); }; // Interned strings are stored in a hash table, indexed by string: using StringsKey = std::string; struct StringsKeyHash { size_t operator()(const StringsKey& key) const { return fnv_hash(key.c_str(), key.length()); } }; // To avoid std::string memory allocations when GetString does lookups in the // hash table of interned strings, we make use of std::unordered_map's ability // to do lookups with a functionally equivalent proxy object: struct StringsKeyProxy { const char* str; size_t len; }; struct StringsKeyProxyHash { size_t operator()(const StringsKeyProxy& key) const { return fnv_hash(key.str, key.len); } }; struct StringsKeyProxyEq { bool operator()(const StringsKeyProxy& proxy, const StringsKey& key) const { return (proxy.len == key.length() && memcmp(proxy.str, key.c_str(), proxy.len) == 0); } }; static std::unordered_map, StringsKeyHash> g_Strings; #define X(id) CStrIntern str_##id(#id); #define X2(id, str) CStrIntern str_##id(str); #include "CStrInternStatic.h" #undef X #undef X2 static CStrInternInternals* GetString(const char* str, size_t len) { // g_Strings is not thread-safe, so complain if anyone is using this // type in non-main threads. (If that's desired, g_Strings should be changed // to be thread-safe, preferably without sacrificing performance.) - ENSURE(ThreadUtil::IsMainThread()); + ENSURE(Threading::IsMainThread()); std::unordered_map >::iterator it = g_Strings.find(str); if (it != g_Strings.end()) return it->second.get(); shared_ptr internals(new CStrInternInternals(str, len)); g_Strings.insert(std::make_pair(internals->data, internals)); return internals.get(); } CStrIntern::CStrIntern() { *this = str__emptystring; } CStrIntern::CStrIntern(const char* str) { m = GetString(str, strlen(str)); } CStrIntern::CStrIntern(const std::string& str) { m = GetString(str.c_str(), str.length()); } u32 CStrIntern::GetHash() const { return m->hash; } const char* CStrIntern::c_str() const { return m->data.c_str(); } size_t CStrIntern::length() const { return m->data.length(); } bool CStrIntern::empty() const { return m->data.empty(); } const std::string& CStrIntern::string() const { return m->data; } Index: ps/trunk/source/ps/GameSetup/GameSetup.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 24529) +++ ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 24530) @@ -1,1641 +1,1641 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "lib/app_hooks.h" #include "lib/config2.h" #include "lib/input.h" #include "lib/ogl.h" #include "lib/timer.h" #include "lib/external_libraries/libsdl.h" #include "lib/file/common/file_stats.h" #include "lib/res/h_mgr.h" #include "lib/res/graphics/cursor.h" #include "graphics/CinemaManager.h" #include "graphics/Color.h" #include "graphics/FontMetrics.h" #include "graphics/GameView.h" #include "graphics/LightEnv.h" #include "graphics/MapReader.h" #include "graphics/ModelDef.h" #include "graphics/MaterialManager.h" #include "graphics/TerrainTextureManager.h" #include "gui/CGUI.h" #include "gui/GUIManager.h" #include "i18n/L10n.h" #include "maths/MathUtil.h" #include "network/NetServer.h" #include "network/NetClient.h" #include "network/NetMessage.h" #include "network/NetMessages.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Paths.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/GameSetup/HWDetect.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Joystick.h" #include "ps/Loader.h" #include "ps/Mod.h" #include "ps/ModIo.h" #include "ps/Profile.h" #include "ps/ProfileViewer.h" #include "ps/Profiler2.h" #include "ps/Pyrogenesis.h" // psSetLogDir #include "ps/scripting/JSInterface_Console.h" #include "ps/TouchInput.h" #include "ps/UserReport.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "ps/VisualReplay.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/VertexBufferManager.h" #include "renderer/ModelRenderer.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptStats.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptConversions.h" #include "simulation2/Simulation2.h" #include "lobby/IXmppClient.h" #include "soundmanager/scripting/JSInterface_Sound.h" #include "soundmanager/ISoundManager.h" #include "tools/atlas/GameInterface/GameLoop.h" #include "tools/atlas/GameInterface/View.h" #if !(OS_WIN || OS_MACOSX || OS_ANDROID) // assume all other platforms use X11 for wxWidgets #define MUST_INIT_X11 1 #include #else #define MUST_INIT_X11 0 #endif extern void RestartEngine(); #include #include #include ERROR_GROUP(System); ERROR_TYPE(System, SDLInitFailed); ERROR_TYPE(System, VmodeFailed); ERROR_TYPE(System, RequiredExtensionsMissing); bool g_DoRenderGui = true; bool g_DoRenderLogger = true; bool g_DoRenderCursor = true; thread_local shared_ptr g_ScriptContext; static const int SANE_TEX_QUALITY_DEFAULT = 5; // keep in sync with code static const CStr g_EventNameGameLoadProgress = "GameLoadProgress"; bool g_InDevelopmentCopy; bool g_CheckedIfInDevelopmentCopy = false; static void SetTextureQuality(int quality) { int q_flags; GLint filter; retry: // keep this in sync with SANE_TEX_QUALITY_DEFAULT switch(quality) { // worst quality case 0: q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP; filter = GL_NEAREST; break; // [perf] add bilinear filtering case 1: q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP; filter = GL_LINEAR; break; // [vmem] no longer reduce resolution case 2: q_flags = OGL_TEX_HALF_BPP; filter = GL_LINEAR; break; // [vmem] add mipmaps case 3: q_flags = OGL_TEX_HALF_BPP; filter = GL_NEAREST_MIPMAP_LINEAR; break; // [perf] better filtering case 4: q_flags = OGL_TEX_HALF_BPP; filter = GL_LINEAR_MIPMAP_LINEAR; break; // [vmem] no longer reduce bpp case SANE_TEX_QUALITY_DEFAULT: q_flags = OGL_TEX_FULL_QUALITY; filter = GL_LINEAR_MIPMAP_LINEAR; break; // [perf] add anisotropy case 6: // TODO: add anisotropic filtering q_flags = OGL_TEX_FULL_QUALITY; filter = GL_LINEAR_MIPMAP_LINEAR; break; // invalid default: debug_warn(L"SetTextureQuality: invalid quality"); quality = SANE_TEX_QUALITY_DEFAULT; // careful: recursion doesn't work and we don't want to duplicate // the "sane" default values. goto retry; } ogl_tex_set_defaults(q_flags, filter); } //---------------------------------------------------------------------------- // GUI integration //---------------------------------------------------------------------------- // display progress / description in loading screen void GUI_DisplayLoadProgress(int percent, const wchar_t* pending_task) { const ScriptInterface& scriptInterface = *(g_GUI->GetActiveGUI()->GetScriptInterface()); ScriptRequest rq(scriptInterface); JS::RootedValueVector paramData(rq.cx); ignore_result(paramData.append(JS::NumberValue(percent))); JS::RootedValue valPendingTask(rq.cx); scriptInterface.ToJSVal(rq, &valPendingTask, pending_task); ignore_result(paramData.append(valPendingTask)); g_GUI->SendEventToAll(g_EventNameGameLoadProgress, paramData); } bool ShouldRender() { return !g_app_minimized && (g_app_has_focus || !g_VideoMode.IsInFullscreen()); } void Render() { // Do not render if not focused while in fullscreen or minimised, // as that triggers a difficult-to-reproduce crash on some graphic cards. if (!ShouldRender()) return; PROFILE3("render"); ogl_WarnIfError(); g_Profiler2.RecordGPUFrameStart(); ogl_WarnIfError(); // prepare before starting the renderer frame if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->BeginFrame(); if (g_Game) g_Renderer.SetSimulation(g_Game->GetSimulation2()); // start new frame g_Renderer.BeginFrame(); ogl_WarnIfError(); if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->Render(); ogl_WarnIfError(); g_Renderer.RenderTextOverlays(); // If we're in Atlas game view, render special tools if (g_AtlasGameLoop && g_AtlasGameLoop->view) { g_AtlasGameLoop->view->DrawCinemaPathTool(); ogl_WarnIfError(); } if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->GetCinema()->Render(); ogl_WarnIfError(); if (g_DoRenderGui) g_GUI->Draw(); ogl_WarnIfError(); // If we're in Atlas game view, render special overlays (e.g. editor bandbox) if (g_AtlasGameLoop && g_AtlasGameLoop->view) { g_AtlasGameLoop->view->DrawOverlays(); ogl_WarnIfError(); } // Text: glDisable(GL_DEPTH_TEST); g_Console->Render(); ogl_WarnIfError(); if (g_DoRenderLogger) g_Logger->Render(); ogl_WarnIfError(); // Profile information g_ProfileViewer.RenderProfile(); ogl_WarnIfError(); // Draw the cursor (or set the Windows cursor, on Windows) if (g_DoRenderCursor) { PROFILE3_GPU("cursor"); CStrW cursorName = g_CursorName; if (cursorName.empty()) { cursor_draw(g_VFS, NULL, g_mouse_x, g_yres-g_mouse_y, g_GuiScale, false); } else { bool forceGL = false; CFG_GET_VAL("nohwcursor", forceGL); #if CONFIG2_GLES #warning TODO: implement cursors for GLES #else // set up transform for GL cursor glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); CMatrix3D transform; transform.SetOrtho(0.f, (float)g_xres, 0.f, (float)g_yres, -1.f, 1000.f); glLoadMatrixf(&transform._11); #endif #if OS_ANDROID #warning TODO: cursors for Android #else if (cursor_draw(g_VFS, cursorName.c_str(), g_mouse_x, g_yres-g_mouse_y, g_GuiScale, forceGL) < 0) LOGWARNING("Failed to draw cursor '%s'", utf8_from_wstring(cursorName)); #endif #if CONFIG2_GLES #warning TODO: implement cursors for GLES #else // restore transform glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); #endif } } glEnable(GL_DEPTH_TEST); g_Renderer.EndFrame(); PROFILE2_ATTR("draw calls: %d", (int)g_Renderer.GetStats().m_DrawCalls); PROFILE2_ATTR("terrain tris: %d", (int)g_Renderer.GetStats().m_TerrainTris); PROFILE2_ATTR("water tris: %d", (int)g_Renderer.GetStats().m_WaterTris); PROFILE2_ATTR("model tris: %d", (int)g_Renderer.GetStats().m_ModelTris); PROFILE2_ATTR("overlay tris: %d", (int)g_Renderer.GetStats().m_OverlayTris); PROFILE2_ATTR("blend splats: %d", (int)g_Renderer.GetStats().m_BlendSplats); PROFILE2_ATTR("particles: %d", (int)g_Renderer.GetStats().m_Particles); ogl_WarnIfError(); g_Profiler2.RecordGPUFrameEnd(); ogl_WarnIfError(); } ErrorReactionInternal psDisplayError(const wchar_t* UNUSED(text), size_t UNUSED(flags)) { // If we're fullscreen, then sometimes (at least on some particular drivers on Linux) // displaying the error dialog hangs the desktop since the dialog box is behind the // fullscreen window. So we just force the game to windowed mode before displaying the dialog. // (But only if we're in the main thread, and not if we're being reentrant.) - if (ThreadUtil::IsMainThread()) + if (Threading::IsMainThread()) { static bool reentering = false; if (!reentering) { reentering = true; g_VideoMode.SetFullscreen(false); reentering = false; } } // We don't actually implement the error display here, so return appropriately return ERI_NOT_IMPLEMENTED; } const std::vector& GetMods(const CmdLineArgs& args, int flags) { const bool init_mods = (flags & INIT_MODS) == INIT_MODS; const bool add_user = !InDevelopmentCopy() && !args.Has("noUserMod"); const bool add_public = (flags & INIT_MODS_PUBLIC) == INIT_MODS_PUBLIC; if (!init_mods) { // Add the user mod if it should be present if (add_user && (g_modsLoaded.empty() || g_modsLoaded.back() != "user")) g_modsLoaded.push_back("user"); return g_modsLoaded; } g_modsLoaded = args.GetMultiple("mod"); if (add_public) g_modsLoaded.insert(g_modsLoaded.begin(), "public"); g_modsLoaded.insert(g_modsLoaded.begin(), "mod"); // Add the user mod if not explicitly disabled or we have a dev copy so // that saved files end up in version control and not in the user mod. if (add_user) g_modsLoaded.push_back("user"); return g_modsLoaded; } void MountMods(const Paths& paths, const std::vector& mods) { OsPath modPath = paths.RData()/"mods"; OsPath modUserPath = paths.UserData()/"mods"; for (size_t i = 0; i < mods.size(); ++i) { size_t priority = (i+1)*2; // mods are higher priority than regular mountings, which default to priority 0 size_t userFlags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE|VFS_MOUNT_REPLACEABLE; size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST; OsPath modName(mods[i]); if (InDevelopmentCopy()) { // We are running a dev copy, so only mount mods in the user mod path // if the mod does not exist in the data path. if (DirectoryExists(modPath / modName/"")) g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority); else g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority); } else { g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority); // Ensure that user modified files are loaded, if they are present g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority+1); } } } static void InitVfs(const CmdLineArgs& args, int flags) { TIMER(L"InitVfs"); const bool setup_error = (flags & INIT_HAVE_DISPLAY_ERROR) == 0; const Paths paths(args); OsPath logs(paths.Logs()); CreateDirectories(logs, 0700); psSetLogDir(logs); // desired location for crashlog is now known. update AppHooks ASAP // (particularly before the following error-prone operations): AppHooks hooks = {0}; hooks.bundle_logs = psBundleLogs; hooks.get_log_dir = psLogDir; if (setup_error) hooks.display_error = psDisplayError; app_hooks_update(&hooks); g_VFS = CreateVfs(); const OsPath readonlyConfig = paths.RData()/"config"/""; g_VFS->Mount(L"config/", readonlyConfig); // Engine localization files. g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/""); MountMods(paths, GetMods(args, flags)); // We mount these dirs last as otherwise writing could result in files being placed in a mod's dir. g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/""); g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH); // Mounting with highest priority, so that a mod supplied user.cfg is harmless g_VFS->Mount(L"config/", readonlyConfig, 0, (size_t)-1); if(readonlyConfig != paths.Config()) g_VFS->Mount(L"config/", paths.Config(), 0, (size_t)-1); g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); // (adding XMBs to archive speeds up subsequent reads) // note: don't bother with g_VFS->TextRepresentation - directories // haven't yet been populated and are empty. } static void InitPs(bool setup_gui, const CStrW& gui_page, ScriptInterface* srcScriptInterface, JS::HandleValue initData) { { // console TIMER(L"ps_console"); g_Console->UpdateScreenSize(g_xres, g_yres); // Calculate and store the line spacing CFontMetrics font(CStrIntern(CONSOLE_FONT)); g_Console->m_iFontHeight = font.GetLineSpacing(); g_Console->m_iFontWidth = font.GetCharacterWidth(L'C'); g_Console->m_charsPerPage = (size_t)(g_xres / g_Console->m_iFontWidth); // Offset by an arbitrary amount, to make it fit more nicely g_Console->m_iFontOffset = 7; double blinkRate = 0.5; CFG_GET_VAL("gui.cursorblinkrate", blinkRate); g_Console->SetCursorBlinkRate(blinkRate); } // hotkeys { TIMER(L"ps_lang_hotkeys"); LoadHotkeys(); } if (!setup_gui) { // We do actually need *some* kind of GUI loaded, so use the // (currently empty) Atlas one g_GUI->SwitchPage(L"page_atlas.xml", srcScriptInterface, initData); return; } // GUI uses VFS, so this must come after VFS init. g_GUI->SwitchPage(gui_page, srcScriptInterface, initData); } void InitPsAutostart(bool networked, JS::HandleValue attrs) { // The GUI has not been initialized yet, so use the simulation scriptinterface for this variable ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue playerAssignments(rq.cx); ScriptInterface::CreateObject(rq, &playerAssignments); if (!networked) { JS::RootedValue localPlayer(rq.cx); ScriptInterface::CreateObject(rq, &localPlayer, "player", g_Game->GetPlayerID()); scriptInterface.SetProperty(playerAssignments, "local", localPlayer); } JS::RootedValue sessionInitData(rq.cx); ScriptInterface::CreateObject( rq, &sessionInitData, "attribs", attrs, "playerAssignments", playerAssignments); InitPs(true, L"page_loading.xml", &scriptInterface, sessionInitData); } void InitInput() { g_Joystick.Initialise(); // register input handlers // This stack is constructed so the first added, will be the last // one called. This is important, because each of the handlers // has the potential to block events to go further down // in the chain. I.e. the last one in the list added, is the // only handler that can block all messages before they are // processed. in_add_handler(game_view_handler); in_add_handler(CProfileViewer::InputThunk); in_add_handler(conInputHandler); in_add_handler(HotkeyInputHandler); // gui_handler needs to be registered after (i.e. called before!) the // hotkey handler so that input boxes can be typed in without // setting off hotkeys. in_add_handler(gui_handler); in_add_handler(touch_input_handler); // must be registered after (called before) the GUI which relies on these globals in_add_handler(GlobalsInputHandler); // Should be called first, this updates our hotkey press state // so that js calls to HotkeyIsPressed are synched with events. in_add_handler(HotkeyStateChange); } static void ShutdownPs() { SAFE_DELETE(g_GUI); UnloadHotkeys(); // disable the special Windows cursor, or free textures for OGL cursors cursor_draw(g_VFS, 0, g_mouse_x, g_yres-g_mouse_y, 1.0, false); } static void InitRenderer() { TIMER(L"InitRenderer"); // create renderer new CRenderer; // create terrain related stuff new CTerrainTextureManager; g_Renderer.Open(g_xres, g_yres); // Setup lighting environment. Since the Renderer accesses the // lighting environment through a pointer, this has to be done before // the first Frame. g_Renderer.SetLightEnv(&g_LightEnv); // I haven't seen the camera affecting GUI rendering and such, but the // viewport has to be updated according to the video mode SViewPort vp; vp.m_X = 0; vp.m_Y = 0; vp.m_Width = g_xres; vp.m_Height = g_yres; g_Renderer.SetViewport(vp); ModelDefActivateFastImpl(); ColorActivateFastImpl(); ModelRenderer::Init(); } static void InitSDL() { #if OS_LINUX // In fullscreen mode when SDL is compiled with DGA support, the mouse // sensitivity often appears to be unusably wrong (typically too low). // (This seems to be reported almost exclusively on Ubuntu, but can be // reproduced on Gentoo after explicitly enabling DGA.) // Disabling the DGA mouse appears to fix that problem, and doesn't // have any obvious negative effects. setenv("SDL_VIDEO_X11_DGAMOUSE", "0", 0); #endif if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_NOPARACHUTE) < 0) { LOGERROR("SDL library initialization failed: %s", SDL_GetError()); throw PSERROR_System_SDLInitFailed(); } atexit(SDL_Quit); // Text input is active by default, disable it until it is actually needed. SDL_StopTextInput(); #if OS_MACOSX // Some Mac mice only have one button, so they can't right-click // but SDL2 can emulate that with Ctrl+Click bool macMouse = false; CFG_GET_VAL("macmouse", macMouse); SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, macMouse ? "1" : "0"); #endif } static void ShutdownSDL() { SDL_Quit(); } void EndGame() { SAFE_DELETE(g_NetClient); SAFE_DELETE(g_NetServer); SAFE_DELETE(g_Game); if (CRenderer::IsInitialised()) { ISoundManager::CloseGame(); g_Renderer.ResetState(); } } void Shutdown(int flags) { const bool hasRenderer = CRenderer::IsInitialised(); if ((flags & SHUTDOWN_FROM_CONFIG)) goto from_config; EndGame(); SAFE_DELETE(g_XmppClient); SAFE_DELETE(g_ModIo); ShutdownPs(); TIMER_BEGIN(L"shutdown TexMan"); delete &g_TexMan; TIMER_END(L"shutdown TexMan"); if (hasRenderer) { TIMER_BEGIN(L"shutdown Renderer"); g_Renderer.~CRenderer(); g_VBMan.Shutdown(); TIMER_END(L"shutdown Renderer"); } g_RenderingOptions.ClearHooks(); g_Profiler2.ShutdownGPU(); // Free cursors before shutting down SDL, as they may depend on SDL. cursor_shutdown(); TIMER_BEGIN(L"shutdown SDL"); ShutdownSDL(); TIMER_END(L"shutdown SDL"); if (hasRenderer) g_VideoMode.Shutdown(); TIMER_BEGIN(L"shutdown UserReporter"); g_UserReporter.Deinitialize(); TIMER_END(L"shutdown UserReporter"); // Cleanup curl now that g_ModIo and g_UserReporter have been shutdown. curl_global_cleanup(); delete &g_L10n; from_config: TIMER_BEGIN(L"shutdown ConfigDB"); delete &g_ConfigDB; TIMER_END(L"shutdown ConfigDB"); SAFE_DELETE(g_Console); // This is needed to ensure that no callbacks from the JSAPI try to use // the profiler when it's already destructed g_ScriptContext.reset(); // resource // first shut down all resource owners, and then the handle manager. TIMER_BEGIN(L"resource modules"); ISoundManager::SetEnabled(false); g_VFS.reset(); // this forcibly frees all open handles (thus preventing real leaks), // and makes further access to h_mgr impossible. h_mgr_shutdown(); file_stats_dump(); TIMER_END(L"resource modules"); TIMER_BEGIN(L"shutdown misc"); timer_DisplayClientTotals(); CNetHost::Deinitialize(); // should be last, since the above use them SAFE_DELETE(g_Logger); delete &g_Profiler; delete &g_ProfileViewer; SAFE_DELETE(g_ScriptStatsTable); TIMER_END(L"shutdown misc"); } #if OS_UNIX static void FixLocales() { #if OS_MACOSX || OS_BSD // OS X requires a UTF-8 locale in LC_CTYPE so that *wprintf can handle // wide characters. Peculiarly the string "UTF-8" seems to be acceptable // despite not being a real locale, and it's conveniently language-agnostic, // so use that. setlocale(LC_CTYPE, "UTF-8"); #endif // On misconfigured systems with incorrect locale settings, we'll die // with a C++ exception when some code (e.g. Boost) tries to use locales. // To avoid death, we'll detect the problem here and warn the user and // reset to the default C locale. // For informing the user of the problem, use the list of env vars that // glibc setlocale looks at. (LC_ALL is checked first, and LANG last.) const char* const LocaleEnvVars[] = { "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LC_MESSAGES", "LANG" }; try { // this constructor is similar to setlocale(LC_ALL, ""), // but instead of returning NULL, it throws runtime_error // when the first locale env variable found contains an invalid value std::locale(""); } catch (std::runtime_error&) { LOGWARNING("Invalid locale settings"); for (size_t i = 0; i < ARRAY_SIZE(LocaleEnvVars); i++) { if (char* envval = getenv(LocaleEnvVars[i])) LOGWARNING(" %s=\"%s\"", LocaleEnvVars[i], envval); else LOGWARNING(" %s=\"(unset)\"", LocaleEnvVars[i]); } // We should set LC_ALL since it overrides LANG if (setenv("LC_ALL", std::locale::classic().name().c_str(), 1)) debug_warn(L"Invalid locale settings, and unable to set LC_ALL env variable."); else LOGWARNING("Setting LC_ALL env variable to: %s", getenv("LC_ALL")); } } #else static void FixLocales() { // Do nothing on Windows } #endif void EarlyInit() { // If you ever want to catch a particular allocation: //_CrtSetBreakAlloc(232647); - ThreadUtil::SetMainThread(); + Threading::SetMainThread(); debug_SetThreadName("main"); // add all debug_printf "tags" that we are interested in: debug_filter_add("TIMER"); timer_Init(); // initialise profiler early so it can profile startup, // but only after LatchStartTime g_Profiler2.Initialise(); FixLocales(); // Because we do GL calls from a secondary thread, Xlib needs to // be told to support multiple threads safely. // This is needed for Atlas, but we have to call it before any other // Xlib functions (e.g. the ones used when drawing the main menu // before launching Atlas) #if MUST_INIT_X11 int status = XInitThreads(); if (status == 0) debug_printf("Error enabling thread-safety via XInitThreads\n"); #endif // Initialise the low-quality rand function srand(time(NULL)); // NOTE: this rand should *not* be used for simulation! } bool Autostart(const CmdLineArgs& args); /** * Returns true if the user has intended to start a visual replay from command line. */ bool AutostartVisualReplay(const std::string& replayFile); bool Init(const CmdLineArgs& args, int flags) { h_mgr_init(); // Do this as soon as possible, because it chdirs // and will mess up the error reporting if anything // crashes before the working directory is set. InitVfs(args, flags); // This must come after VFS init, which sets the current directory // (required for finding our output log files). g_Logger = new CLogger; new CProfileViewer; new CProfileManager; // before any script code g_ScriptStatsTable = new CScriptStatsTable; g_ProfileViewer.AddRootTable(g_ScriptStatsTable); // Set up the console early, so that debugging // messages can be logged to it. (The console's size // and fonts are set later in InitPs()) g_Console = new CConsole(); // g_ConfigDB, command line args, globals CONFIG_Init(args); // Using a global object for the context is a workaround until Simulation and AI use // their own threads and also their own contexts. const int contextSize = 384 * 1024 * 1024; const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024; g_ScriptContext = ScriptContext::CreateContext(contextSize, heapGrowthBytesGCTrigger); Mod::CacheEnabledModVersions(g_ScriptContext); // Special command-line mode to dump the entity schemas instead of running the game. // (This must be done after loading VFS etc, but should be done before wasting time // on anything else.) if (args.Has("dumpSchema")) { CSimulation2 sim(NULL, g_ScriptContext, NULL); sim.LoadDefaultScripts(); std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc); f << sim.GenerateSchema(); std::cout << "Generated entity.rng\n"; exit(0); } CNetHost::Initialize(); #if CONFIG2_AUDIO if (!args.Has("autostart-nonvisual")) ISoundManager::CreateSoundManager(); #endif // Check if there are mods specified on the command line, // or if we already set the mods (~INIT_MODS), // else check if there are mods that should be loaded specified // in the config and load those (by aborting init and restarting // the engine). if (!args.Has("mod") && (flags & INIT_MODS) == INIT_MODS) { CStr modstring; CFG_GET_VAL("mod.enabledmods", modstring); if (!modstring.empty()) { std::vector mods; boost::split(mods, modstring, boost::is_any_of(" "), boost::token_compress_on); std::swap(g_modsLoaded, mods); // Abort init and restart RestartEngine(); return false; } } new L10n; // Optionally start profiler HTTP output automatically // (By default it's only enabled by a hotkey, for security/performance) bool profilerHTTPEnable = false; CFG_GET_VAL("profiler2.autoenable", profilerHTTPEnable); if (profilerHTTPEnable) g_Profiler2.EnableHTTP(); // Initialise everything except Win32 sockets (because our networking // system already inits those) curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32); if (!g_Quickstart) g_UserReporter.Initialize(); // after config PROFILE2_EVENT("Init finished"); return true; } void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& installedMods) { const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0; if(setup_vmode) { InitSDL(); if (!g_VideoMode.InitSDL()) throw PSERROR_System_VmodeFailed(); // abort startup } RunHardwareDetection(); const int quality = SANE_TEX_QUALITY_DEFAULT; // TODO: set value from config file SetTextureQuality(quality); ogl_WarnIfError(); // Optionally start profiler GPU timings automatically // (By default it's only enabled by a hotkey, for performance/compatibility) bool profilerGPUEnable = false; CFG_GET_VAL("profiler2.autoenable", profilerGPUEnable); if (profilerGPUEnable) g_Profiler2.EnableGPU(); if(!g_Quickstart) { WriteSystemInfo(); // note: no longer vfs_display here. it's dog-slow due to unbuffered // file output and very rarely needed. } if(g_DisableAudio) ISoundManager::SetEnabled(false); g_GUI = new CGUIManager(); // (must come after SetVideoMode, since it calls ogl_Init) CStr8 renderPath = "default"; CFG_GET_VAL("renderpath", renderPath); if ((ogl_HaveExtensions(0, "GL_ARB_vertex_program", "GL_ARB_fragment_program", NULL) != 0 // ARB && ogl_HaveExtensions(0, "GL_ARB_vertex_shader", "GL_ARB_fragment_shader", NULL) != 0) // GLSL || RenderPathEnum::FromString(renderPath) == FIXED) { // It doesn't make sense to continue working here, because we're not // able to display anything. DEBUG_DISPLAY_FATAL_ERROR( L"Your graphics card doesn't appear to be fully compatible with OpenGL shaders." L" The game does not support pre-shader graphics cards." L" You are advised to try installing newer drivers and/or upgrade your graphics card." L" For more information, please see http://www.wildfiregames.com/forum/index.php?showtopic=16734" ); } const char* missing = ogl_HaveExtensions(0, "GL_ARB_multitexture", "GL_EXT_draw_range_elements", "GL_ARB_texture_env_combine", "GL_ARB_texture_env_dot3", NULL); if(missing) { wchar_t buf[500]; swprintf_s(buf, ARRAY_SIZE(buf), L"The %hs extension doesn't appear to be available on your computer." L" The game may still work, though - you are welcome to try at your own risk." L" If not or it doesn't look right, upgrade your graphics card.", missing ); DEBUG_DISPLAY_ERROR(buf); // TODO: i18n } if (!ogl_HaveExtension("GL_ARB_texture_env_crossbar")) { DEBUG_DISPLAY_ERROR( L"The GL_ARB_texture_env_crossbar extension doesn't appear to be available on your computer." L" Shadows are not available and overall graphics quality might suffer." L" You are advised to try installing newer drivers and/or upgrade your graphics card."); g_ConfigDB.SetValueBool(CFG_HWDETECT, "shadows", false); } ogl_WarnIfError(); g_RenderingOptions.ReadConfigAndSetupHooks(); InitRenderer(); InitInput(); ogl_WarnIfError(); // TODO: Is this the best place for this? if (VfsDirectoryExists(L"maps/")) CXeromyces::AddValidator(g_VFS, "map", "maps/scenario.rng"); try { if (!AutostartVisualReplay(args.Get("replay-visual")) && !Autostart(args)) { const bool setup_gui = ((flags & INIT_NO_GUI) == 0); // We only want to display the splash screen at startup shared_ptr scriptInterface = g_GUI->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue data(rq.cx); if (g_GUI) { ScriptInterface::CreateObject(rq, &data, "isStartup", true); if (!installedMods.empty()) scriptInterface->SetProperty(data, "installedMods", installedMods); } InitPs(setup_gui, installedMods.empty() ? L"page_pregame.xml" : L"page_modmod.xml", g_GUI->GetScriptInterface().get(), data); } } catch (PSERROR_Game_World_MapLoadFailed& e) { // Map Loading failed // Start the engine so we have a GUI InitPs(true, L"page_pregame.xml", NULL, JS::UndefinedHandleValue); // Call script function to do the actual work // (delete game data, switch GUI page, show error, etc.) CancelLoad(CStr(e.what()).FromUTF8()); } } void InitNonVisual(const CmdLineArgs& args) { // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); Autostart(args); } void RenderGui(bool RenderingState) { g_DoRenderGui = RenderingState; } void RenderLogger(bool RenderingState) { g_DoRenderLogger = RenderingState; } void RenderCursor(bool RenderingState) { g_DoRenderCursor = RenderingState; } /** * Temporarily loads a scenario map and retrieves the "ScriptSettings" JSON * data from it. * The scenario map format is used for scenario and skirmish map types (random * games do not use a "map" (format) but a small JavaScript program which * creates a map on the fly). It contains a section to initialize the game * setup screen. * @param mapPath Absolute path (from VFS root) to the map file to peek in. * @return ScriptSettings in JSON format extracted from the map. */ CStr8 LoadSettingsOfScenarioMap(const VfsPath &mapPath) { CXeromyces mapFile; const char *pathToSettings[] = { "Scenario", "ScriptSettings", "" // Path to JSON data in map }; Status loadResult = mapFile.Load(g_VFS, mapPath); if (INFO::OK != loadResult) { LOGERROR("LoadSettingsOfScenarioMap: Unable to load map file '%s'", mapPath.string8()); throw PSERROR_Game_World_MapLoadFailed("Unable to load map file, check the path for typos."); } XMBElement mapElement = mapFile.GetRoot(); // Select the ScriptSettings node in the map file... for (int i = 0; pathToSettings[i][0]; ++i) { int childId = mapFile.GetElementID(pathToSettings[i]); XMBElementList nodes = mapElement.GetChildNodes(); auto it = std::find_if(nodes.begin(), nodes.end(), [&childId](const XMBElement& child) { return child.GetNodeName() == childId; }); if (it != nodes.end()) mapElement = *it; } // ... they contain a JSON document to initialize the game setup // screen return mapElement.GetText(); } /* * Command line options for autostart * (keep synchronized with binaries/system/readme.txt): * * -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME; * TYPEDIR is skirmishes, scenarios, or random * -autostart-seed=SEED sets randomization seed value (default 0, use -1 for random) * -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra) * -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI * (0: sandbox, 5: very hard) * -autostart-aiseed=AISEED sets the seed used for the AI random * generator (default 0, use -1 for random) * -autostart-player=NUMBER sets the playerID in non-networked games (default 1, use -1 for observer) * -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV * (skirmish and random maps only) * -autostart-team=PLAYER:TEAM sets the team for PLAYER (e.g. 2:2). * -autostart-ceasefire=NUM sets a ceasefire duration NUM * (default 0 minutes) * -autostart-nonvisual disable any graphics and sounds * -autostart-victory=SCRIPTNAME sets the victory conditions with SCRIPTNAME * located in simulation/data/settings/victory_conditions/ * (default conquest). When the first given SCRIPTNAME is * "endless", no victory conditions will apply. * -autostart-wonderduration=NUM sets the victory duration NUM for wonder victory condition * (default 10 minutes) * -autostart-relicduration=NUM sets the victory duration NUM for relic victory condition * (default 10 minutes) * -autostart-reliccount=NUM sets the number of relics for relic victory condition * (default 2 relics) * -autostart-disable-replay disable saving of replays * * Multiplayer: * -autostart-playername=NAME sets local player NAME (default 'anonymous') * -autostart-host sets multiplayer host mode * -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer * game (default 2) * -autostart-client=IP sets multiplayer client to join host at * given IP address * Random maps only: * -autostart-size=TILES sets random map size in TILES (default 192) * -autostart-players=NUMBER sets NUMBER of players on random map * (default 2) * * Examples: * 1) "Bob" will host a 2 player game on the Arcadia map: * -autostart="scenarios/Arcadia" -autostart-host -autostart-host-players=2 -autostart-playername="Bob" * "Alice" joins the match as player 2: * -autostart="scenarios/Arcadia" -autostart-client=127.0.0.1 -autostart-playername="Alice" * The players use the developer overlay to control players. * * 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot: * -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra * * 3) Observe the PetraBot on a triggerscript map: * -autostart="random/jebel_barkal" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=1:petra -autostart-ai=2:petra -autostart-player=-1 */ bool Autostart(const CmdLineArgs& args) { CStr autoStartName = args.Get("autostart"); if (autoStartName.empty()) return false; g_Game = new CGame(!args.Has("autostart-disable-replay")); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue attrs(rq.cx); JS::RootedValue settings(rq.cx); JS::RootedValue playerData(rq.cx); ScriptInterface::CreateObject(rq, &attrs); ScriptInterface::CreateObject(rq, &settings); ScriptInterface::CreateArray(rq, &playerData); // The directory in front of the actual map name indicates which type // of map is being loaded. Drawback of this approach is the association // of map types and folders is hard-coded, but benefits are: // - No need to pass the map type via command line separately // - Prevents mixing up of scenarios and skirmish maps to some degree Path mapPath = Path(autoStartName); std::wstring mapDirectory = mapPath.Parent().Filename().string(); std::string mapType; if (mapDirectory == L"random") { // Random map definition will be loaded from JSON file, so we need to parse it std::wstring scriptPath = L"maps/" + autoStartName.FromUTF8() + L".json"; JS::RootedValue scriptData(rq.cx); scriptInterface.ReadJSONFile(scriptPath, &scriptData); if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "settings", &settings)) { // JSON loaded ok - copy script name over to game attributes std::wstring scriptFile; scriptInterface.GetProperty(settings, "Script", scriptFile); scriptInterface.SetProperty(attrs, "script", scriptFile); // RMS filename } else { // Problem with JSON file LOGERROR("Autostart: Error reading random map script '%s'", utf8_from_wstring(scriptPath)); throw PSERROR_Game_World_MapLoadFailed("Error reading random map script.\nCheck application log for details."); } // Get optional map size argument (default 192) uint mapSize = 192; if (args.Has("autostart-size")) { CStr size = args.Get("autostart-size"); mapSize = size.ToUInt(); } scriptInterface.SetProperty(settings, "Size", mapSize); // Random map size (in patches) // Get optional number of players (default 2) size_t numPlayers = 2; if (args.Has("autostart-players")) { CStr num = args.Get("autostart-players"); numPlayers = num.ToUInt(); } // Set up player data for (size_t i = 0; i < numPlayers; ++i) { JS::RootedValue player(rq.cx); // We could load player_defaults.json here, but that would complicate the logic // even more and autostart is only intended for developers anyway ScriptInterface::CreateObject(rq, &player, "Civ", "athen"); scriptInterface.SetPropertyInt(playerData, i, player); } mapType = "random"; } else if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // Initialize general settings from the map data so some values // (e.g. name of map) are always present, even when autostart is // partially configured CStr8 mapSettingsJSON = LoadSettingsOfScenarioMap("maps/" + autoStartName + ".xml"); scriptInterface.ParseJSON(mapSettingsJSON, &settings); // Initialize the playerData array being modified by autostart // with the real map data, so sensible values are present: scriptInterface.GetProperty(settings, "PlayerData", &playerData); if (mapDirectory == L"scenarios") mapType = "scenario"; else mapType = "skirmish"; } else { LOGERROR("Autostart: Unrecognized map type '%s'", utf8_from_wstring(mapDirectory)); throw PSERROR_Game_World_MapLoadFailed("Unrecognized map type.\nConsult readme.txt for the currently supported types."); } scriptInterface.SetProperty(attrs, "mapType", mapType); scriptInterface.SetProperty(attrs, "map", "maps/" + autoStartName); scriptInterface.SetProperty(settings, "mapType", mapType); scriptInterface.SetProperty(settings, "CheatsEnabled", true); // The seed is used for both random map generation and simulation u32 seed = 0; if (args.Has("autostart-seed")) { CStr seedArg = args.Get("autostart-seed"); if (seedArg == "-1") seed = rand(); else seed = seedArg.ToULong(); } scriptInterface.SetProperty(settings, "Seed", seed); // Set seed for AIs u32 aiseed = 0; if (args.Has("autostart-aiseed")) { CStr seedArg = args.Get("autostart-aiseed"); if (seedArg == "-1") aiseed = rand(); else aiseed = seedArg.ToULong(); } scriptInterface.SetProperty(settings, "AISeed", aiseed); // Set player data for AIs // attrs.settings = { PlayerData: [ { AI: ... }, ... ] } // or = { PlayerData: [ null, { AI: ... }, ... ] } when gaia set int offset = 1; JS::RootedValue player(rq.cx); if (scriptInterface.GetPropertyInt(playerData, 0, &player) && player.isNull()) offset = 0; // Set teams if (args.Has("autostart-team")) { std::vector civArgs = args.GetMultiple("autostart-team"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-team option", playerID); continue; } ScriptInterface::CreateObject(rq, ¤tPlayer); } int teamID = civArgs[i].AfterFirst(":").ToInt() - 1; scriptInterface.SetProperty(currentPlayer, "Team", teamID); scriptInterface.SetPropertyInt(playerData, playerID-offset, currentPlayer); } } int ceasefire = 0; if (args.Has("autostart-ceasefire")) ceasefire = args.Get("autostart-ceasefire").ToInt(); scriptInterface.SetProperty(settings, "Ceasefire", ceasefire); if (args.Has("autostart-ai")) { std::vector aiArgs = args.GetMultiple("autostart-ai"); for (size_t i = 0; i < aiArgs.size(); ++i) { int playerID = aiArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-ai option", playerID); continue; } ScriptInterface::CreateObject(rq, ¤tPlayer); } scriptInterface.SetProperty(currentPlayer, "AI", aiArgs[i].AfterFirst(":")); scriptInterface.SetProperty(currentPlayer, "AIDiff", 3); scriptInterface.SetProperty(currentPlayer, "AIBehavior", "balanced"); scriptInterface.SetPropertyInt(playerData, playerID-offset, currentPlayer); } } // Set AI difficulty if (args.Has("autostart-aidiff")) { std::vector civArgs = args.GetMultiple("autostart-aidiff"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-aidiff option", playerID); continue; } ScriptInterface::CreateObject(rq, ¤tPlayer); } scriptInterface.SetProperty(currentPlayer, "AIDiff", civArgs[i].AfterFirst(":").ToInt()); scriptInterface.SetPropertyInt(playerData, playerID-offset, currentPlayer); } } // Set player data for Civs if (args.Has("autostart-civ")) { if (mapDirectory != L"scenarios") { std::vector civArgs = args.GetMultiple("autostart-civ"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-civ option", playerID); continue; } ScriptInterface::CreateObject(rq, ¤tPlayer); } scriptInterface.SetProperty(currentPlayer, "Civ", civArgs[i].AfterFirst(":")); scriptInterface.SetPropertyInt(playerData, playerID-offset, currentPlayer); } } else LOGWARNING("Autostart: Option 'autostart-civ' is invalid for scenarios"); } // Add player data to map settings scriptInterface.SetProperty(settings, "PlayerData", playerData); // Add map settings to game attributes scriptInterface.SetProperty(attrs, "settings", settings); // Get optional playername CStrW userName = L"anonymous"; if (args.Has("autostart-playername")) userName = args.Get("autostart-playername").FromUTF8(); // Add additional scripts to the TriggerScripts property std::vector triggerScriptsVector; JS::RootedValue triggerScripts(rq.cx); if (scriptInterface.HasProperty(settings, "TriggerScripts")) { scriptInterface.GetProperty(settings, "TriggerScripts", &triggerScripts); FromJSVal_vector(rq, triggerScripts, triggerScriptsVector); } if (!CRenderer::IsInitialised()) { CStr nonVisualScript = "scripts/NonVisualTrigger.js"; triggerScriptsVector.push_back(nonVisualScript.FromUTF8()); } std::vector victoryConditions(1, "conquest"); if (args.Has("autostart-victory")) victoryConditions = args.GetMultiple("autostart-victory"); if (victoryConditions.size() == 1 && victoryConditions[0] == "endless") victoryConditions.clear(); scriptInterface.SetProperty(settings, "VictoryConditions", victoryConditions); for (const CStr& victory : victoryConditions) { JS::RootedValue scriptData(rq.cx); JS::RootedValue data(rq.cx); JS::RootedValue victoryScripts(rq.cx); CStrW scriptPath = L"simulation/data/settings/victory_conditions/" + victory.FromUTF8() + L".json"; scriptInterface.ReadJSONFile(scriptPath, &scriptData); if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "Data", &data) && !data.isUndefined() && scriptInterface.GetProperty(data, "Scripts", &victoryScripts) && !victoryScripts.isUndefined()) { std::vector victoryScriptsVector; FromJSVal_vector(rq, victoryScripts, victoryScriptsVector); triggerScriptsVector.insert(triggerScriptsVector.end(), victoryScriptsVector.begin(), victoryScriptsVector.end()); } else { LOGERROR("Autostart: Error reading victory script '%s'", utf8_from_wstring(scriptPath)); throw PSERROR_Game_World_MapLoadFailed("Error reading victory script.\nCheck application log for details."); } } ToJSVal_vector(rq, &triggerScripts, triggerScriptsVector); scriptInterface.SetProperty(settings, "TriggerScripts", triggerScripts); int wonderDuration = 10; if (args.Has("autostart-wonderduration")) wonderDuration = args.Get("autostart-wonderduration").ToInt(); scriptInterface.SetProperty(settings, "WonderDuration", wonderDuration); int relicDuration = 10; if (args.Has("autostart-relicduration")) relicDuration = args.Get("autostart-relicduration").ToInt(); scriptInterface.SetProperty(settings, "RelicDuration", relicDuration); int relicCount = 2; if (args.Has("autostart-reliccount")) relicCount = args.Get("autostart-reliccount").ToInt(); scriptInterface.SetProperty(settings, "RelicCount", relicCount); if (args.Has("autostart-host")) { InitPsAutostart(true, attrs); size_t maxPlayers = 2; if (args.Has("autostart-host-players")) maxPlayers = args.Get("autostart-host-players").ToUInt(); g_NetServer = new CNetServer(false, maxPlayers); g_NetServer->UpdateGameAttributes(&attrs, scriptInterface); bool ok = g_NetServer->SetupConnection(PS_DEFAULT_PORT); ENSURE(ok); g_NetClient = new CNetClient(g_Game, true); g_NetClient->SetUserName(userName); g_NetClient->SetupConnection("127.0.0.1", PS_DEFAULT_PORT, nullptr); } else if (args.Has("autostart-client")) { InitPsAutostart(true, attrs); g_NetClient = new CNetClient(g_Game, false); g_NetClient->SetUserName(userName); CStr ip = args.Get("autostart-client"); if (ip.empty()) ip = "127.0.0.1"; bool ok = g_NetClient->SetupConnection(ip, PS_DEFAULT_PORT, nullptr); ENSURE(ok); } else { g_Game->SetPlayerID(args.Has("autostart-player") ? args.Get("autostart-player").ToInt() : 1); g_Game->StartGame(&attrs, ""); if (CRenderer::IsInitialised()) { InitPsAutostart(false, attrs); } else { // TODO: Non progressive load can fail - need a decent way to handle this LDR_NonprogressiveLoad(); ENSURE(g_Game->ReallyStartGame() == PSRETURN_OK); } } return true; } bool AutostartVisualReplay(const std::string& replayFile) { if (!FileExists(OsPath(replayFile))) return false; g_Game = new CGame(false); g_Game->SetPlayerID(-1); g_Game->StartVisualReplay(replayFile); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue attrs(rq.cx, g_Game->GetSimulation2()->GetInitAttributes()); InitPsAutostart(false, attrs); return true; } void CancelLoad(const CStrW& message) { shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); ScriptRequest rq(pScriptInterface); JS::RootedValue global(rq.cx, rq.globalValue()); LDR_Cancel(); if (g_GUI && g_GUI->GetPageCount() && pScriptInterface->HasProperty(global, "cancelOnLoadGameError")) pScriptInterface->CallFunctionVoid(global, "cancelOnLoadGameError", message); } bool InDevelopmentCopy() { if (!g_CheckedIfInDevelopmentCopy) { g_InDevelopmentCopy = (g_VFS->GetFileInfo(L"config/dev.cfg", NULL) == INFO::OK); g_CheckedIfInDevelopmentCopy = true; } return g_InDevelopmentCopy; } Index: ps/trunk/source/ps/Profile.cpp =================================================================== --- ps/trunk/source/ps/Profile.cpp (revision 24529) +++ ps/trunk/source/ps/Profile.cpp (revision 24530) @@ -1,773 +1,773 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* * GPG3-style hierarchical profiler */ #include "precompiled.h" #include "Profile.h" #include "ProfileViewer.h" #include "ThreadUtil.h" #include "lib/timer.h" #if OS_WIN && !defined(NDEBUG) # define USE_CRT_SET_ALLOC_HOOK #endif #if defined(__GLIBC__) && !defined(NDEBUG) //# define USE_GLIBC_MALLOC_HOOK # define USE_GLIBC_MALLOC_OVERRIDE # include # include # include "lib/sysdep/cpu.h" #endif #include /////////////////////////////////////////////////////////////////////////////////////////////// // CProfileNodeTable /** * Class CProfileNodeTable: Implement ProfileViewer's AbstractProfileTable * interface in order to display profiling data in-game. */ class CProfileNodeTable : public AbstractProfileTable { public: CProfileNodeTable(CProfileNode* n); virtual ~CProfileNodeTable(); // Implementation of AbstractProfileTable interface virtual CStr GetName(); virtual CStr GetTitle(); virtual size_t GetNumberRows(); virtual const std::vector& GetColumns(); virtual CStr GetCellText(size_t row, size_t col); virtual AbstractProfileTable* GetChild(size_t row); virtual bool IsHighlightRow(size_t row); private: /** * struct ColumnDescription: The only purpose of this helper structure * is to provide the global constructor that sets up the column * description. */ struct ColumnDescription { std::vector columns; ColumnDescription() { columns.push_back(ProfileColumn("Name", 230)); columns.push_back(ProfileColumn("calls/frame", 80)); columns.push_back(ProfileColumn("msec/frame", 80)); columns.push_back(ProfileColumn("mallocs/frame", 120)); columns.push_back(ProfileColumn("calls/turn", 80)); columns.push_back(ProfileColumn("msec/turn", 80)); columns.push_back(ProfileColumn("mallocs/turn", 80)); } }; /// The node represented by this table CProfileNode* node; /// Columns description (shared by all instances) static ColumnDescription columnDescription; }; CProfileNodeTable::ColumnDescription CProfileNodeTable::columnDescription; // Constructor/Destructor CProfileNodeTable::CProfileNodeTable(CProfileNode* n) { node = n; } CProfileNodeTable::~CProfileNodeTable() { } // Short name (= name of profile node) CStr CProfileNodeTable::GetName() { return node->GetName(); } // Title (= explanatory text plus time totals) CStr CProfileNodeTable::GetTitle() { char buf[512]; sprintf_s(buf, ARRAY_SIZE(buf), "Profiling Information for: %s (Time in node: %.3f msec/frame)", node->GetName(), node->GetFrameTime() * 1000.0f ); return buf; } // Total number of children size_t CProfileNodeTable::GetNumberRows() { return node->GetChildren()->size() + node->GetScriptChildren()->size() + 1; } // Column description const std::vector& CProfileNodeTable::GetColumns() { return columnDescription.columns; } // Retrieve cell text CStr CProfileNodeTable::GetCellText(size_t row, size_t col) { CProfileNode* child; size_t nrchildren = node->GetChildren()->size(); size_t nrscriptchildren = node->GetScriptChildren()->size(); char buf[256] = "?"; if (row < nrchildren) child = (*node->GetChildren())[row]; else if (row < nrchildren + nrscriptchildren) child = (*node->GetScriptChildren())[row - nrchildren]; else if (row > nrchildren + nrscriptchildren) return "!bad row!"; else { // "unlogged" row if (col == 0) return "unlogged"; else if (col == 1) return ""; else if (col == 4) return ""; double unlogged_time_frame = node->GetFrameTime(); double unlogged_time_turn = node->GetTurnTime(); double unlogged_mallocs_frame = node->GetFrameMallocs(); double unlogged_mallocs_turn = node->GetTurnMallocs(); CProfileNode::const_profile_iterator it; for (it = node->GetChildren()->begin(); it != node->GetChildren()->end(); ++it) { unlogged_time_frame -= (*it)->GetFrameTime(); unlogged_time_turn -= (*it)->GetTurnTime(); unlogged_mallocs_frame -= (*it)->GetFrameMallocs(); unlogged_mallocs_turn -= (*it)->GetTurnMallocs(); } for (it = node->GetScriptChildren()->begin(); it != node->GetScriptChildren()->end(); ++it) { unlogged_time_frame -= (*it)->GetFrameTime(); unlogged_time_turn -= (*it)->GetTurnTime(); unlogged_mallocs_frame -= (*it)->GetFrameMallocs(); unlogged_mallocs_turn -= (*it)->GetTurnMallocs(); } // The root node can't easily count per-turn values (since Turn isn't called until // halfway though a frame), so just reset them the zero to prevent weird displays if (!node->GetParent()) { unlogged_time_turn = 0.0; unlogged_mallocs_turn = 0.0; } if (col == 2) sprintf_s(buf, ARRAY_SIZE(buf), "%.3f", unlogged_time_frame * 1000.0f); else if (col == 3) sprintf_s(buf, ARRAY_SIZE(buf), "%.1f", unlogged_mallocs_frame); else if (col == 5) sprintf_s(buf, ARRAY_SIZE(buf), "%.3f", unlogged_time_turn * 1000.f); else if (col == 6) sprintf_s(buf, ARRAY_SIZE(buf), "%.1f", unlogged_mallocs_turn); return CStr(buf); } switch(col) { default: case 0: return child->GetName(); case 1: sprintf_s(buf, ARRAY_SIZE(buf), "%.1f", child->GetFrameCalls()); break; case 2: sprintf_s(buf, ARRAY_SIZE(buf), "%.3f", child->GetFrameTime() * 1000.0f); break; case 3: sprintf_s(buf, ARRAY_SIZE(buf), "%.1f", child->GetFrameMallocs()); break; case 4: sprintf_s(buf, ARRAY_SIZE(buf), "%.1f", child->GetTurnCalls()); break; case 5: sprintf_s(buf, ARRAY_SIZE(buf), "%.3f", child->GetTurnTime() * 1000.0f); break; case 6: sprintf_s(buf, ARRAY_SIZE(buf), "%.1f", child->GetTurnMallocs()); break; } return CStr(buf); } // Return a pointer to the child table if the child node is expandable AbstractProfileTable* CProfileNodeTable::GetChild(size_t row) { CProfileNode* child; size_t nrchildren = node->GetChildren()->size(); size_t nrscriptchildren = node->GetScriptChildren()->size(); if (row < nrchildren) child = (*node->GetChildren())[row]; else if (row < nrchildren + nrscriptchildren) child = (*node->GetScriptChildren())[row - nrchildren]; else return 0; if (child->CanExpand()) return child->display_table; return 0; } // Highlight all script nodes bool CProfileNodeTable::IsHighlightRow(size_t row) { size_t nrchildren = node->GetChildren()->size(); size_t nrscriptchildren = node->GetScriptChildren()->size(); return (row >= nrchildren && row < (nrchildren + nrscriptchildren)); } /////////////////////////////////////////////////////////////////////////////////////////////// // CProfileNode implementation // Note: As with the GPG profiler, name is assumed to be a pointer to a constant string; only pointer equality is checked. CProfileNode::CProfileNode( const char* _name, CProfileNode* _parent ) { name = _name; recursion = 0; Reset(); parent = _parent; display_table = new CProfileNodeTable(this); } CProfileNode::~CProfileNode() { profile_iterator it; for( it = children.begin(); it != children.end(); ++it ) delete( *it ); for( it = script_children.begin(); it != script_children.end(); ++it ) delete( *it ); delete display_table; } template static double average(const T& collection) { if (collection.empty()) return 0.0; return std::accumulate(collection.begin(), collection.end(), 0.0) / collection.size(); } double CProfileNode::GetFrameCalls() const { return average(calls_per_frame); } double CProfileNode::GetFrameTime() const { return average(time_per_frame); } double CProfileNode::GetTurnCalls() const { return average(calls_per_turn); } double CProfileNode::GetTurnTime() const { return average(time_per_turn); } double CProfileNode::GetFrameMallocs() const { return average(mallocs_per_frame); } double CProfileNode::GetTurnMallocs() const { return average(mallocs_per_turn); } const CProfileNode* CProfileNode::GetChild( const char* childName ) const { const_profile_iterator it; for( it = children.begin(); it != children.end(); ++it ) if( (*it)->name == childName ) return( *it ); return( NULL ); } const CProfileNode* CProfileNode::GetScriptChild( const char* childName ) const { const_profile_iterator it; for( it = script_children.begin(); it != script_children.end(); ++it ) if( (*it)->name == childName ) return( *it ); return( NULL ); } CProfileNode* CProfileNode::GetChild( const char* childName ) { profile_iterator it; for( it = children.begin(); it != children.end(); ++it ) if( (*it)->name == childName ) return( *it ); CProfileNode* newNode = new CProfileNode( childName, this ); children.push_back( newNode ); return( newNode ); } CProfileNode* CProfileNode::GetScriptChild( const char* childName ) { profile_iterator it; for( it = script_children.begin(); it != script_children.end(); ++it ) if( (*it)->name == childName ) return( *it ); CProfileNode* newNode = new CProfileNode( childName, this ); script_children.push_back( newNode ); return( newNode ); } bool CProfileNode::CanExpand() { return( !( children.empty() && script_children.empty() ) ); } void CProfileNode::Reset() { calls_per_frame.clear(); calls_per_turn.clear(); calls_frame_current = 0; calls_turn_current = 0; time_per_frame.clear(); time_per_turn.clear(); time_frame_current = 0.0; time_turn_current = 0.0; mallocs_per_frame.clear(); mallocs_per_turn.clear(); mallocs_frame_current = 0; mallocs_turn_current = 0; profile_iterator it; for (it = children.begin(); it != children.end(); ++it) (*it)->Reset(); for (it = script_children.begin(); it != script_children.end(); ++it) (*it)->Reset(); } void CProfileNode::Frame() { calls_per_frame.push_back(calls_frame_current); time_per_frame.push_back(time_frame_current); mallocs_per_frame.push_back(mallocs_frame_current); calls_frame_current = 0; time_frame_current = 0.0; mallocs_frame_current = 0; profile_iterator it; for (it = children.begin(); it != children.end(); ++it) (*it)->Frame(); for (it = script_children.begin(); it != script_children.end(); ++it) (*it)->Frame(); } void CProfileNode::Turn() { calls_per_turn.push_back(calls_turn_current); time_per_turn.push_back(time_turn_current); mallocs_per_turn.push_back(mallocs_turn_current); calls_turn_current = 0; time_turn_current = 0.0; mallocs_turn_current = 0; profile_iterator it; for (it = children.begin(); it != children.end(); ++it) (*it)->Turn(); for (it = script_children.begin(); it != script_children.end(); ++it) (*it)->Turn(); } // TODO: these should probably only count allocations that occur in the thread being profiled #if defined(USE_CRT_SET_ALLOC_HOOK) static long malloc_count = 0; static _CRT_ALLOC_HOOK prev_hook; static int crt_alloc_hook(int allocType, void* userData, size_t size, int blockType, long requestNumber, const unsigned char* filename, int lineNumber) { - if (allocType == _HOOK_ALLOC && ThreadUtil::IsMainThread()) + if (allocType == _HOOK_ALLOC && Threading::IsMainThread()) ++malloc_count; if (prev_hook) return prev_hook(allocType, userData, size, blockType, requestNumber, filename, lineNumber); else return 1; } static void alloc_hook_initialize() { prev_hook = _CrtSetAllocHook(crt_alloc_hook); } static long get_memory_alloc_count() { return malloc_count; } #elif defined(USE_GLIBC_MALLOC_HOOK) // Set up malloc hooks to count allocations - see // http://www.gnu.org/software/libc/manual/html_node/Hooks-for-Malloc.html static intptr_t malloc_count = 0; static void *(*old_malloc_hook) (size_t, const void*); static std::mutex alloc_hook_mutex; static void *malloc_hook(size_t size, const void* UNUSED(caller)) { // This doesn't really work across threads. The hooks are global variables, and // we have to temporarily unhook in order to call the real malloc, and during that // time period another thread may perform an unhooked (hence uncounted) allocation // which we will miss. // Two threads may execute the hook simultaneously, so we need to do the // temporary unhooking in a thread-safe way, so for simplicity we just use a mutex. std::lock_guard lock(alloc_hook_mutex); ++malloc_count; __malloc_hook = old_malloc_hook; void* result = malloc(size); old_malloc_hook = __malloc_hook; __malloc_hook = malloc_hook; return result; } static void alloc_hook_initialize() { std::lock_guard lock(alloc_hook_mutex); old_malloc_hook = __malloc_hook; __malloc_hook = malloc_hook; // (we don't want to bother hooking realloc and memalign, because if they allocate // new memory then they'll be caught by the malloc hook anyway) } /* It would be nice to do: __attribute__ ((visibility ("default"))) void (*__malloc_initialize_hook)() = malloc_initialize_hook; except that doesn't seem to work in practice, since something (?) resets the hook to NULL some time while loading the game, after we've set it here - so we just call malloc_initialize_hook once inside CProfileManager::Frame instead and hope nobody deletes our hook after that. */ static long get_memory_alloc_count() { return malloc_count; } #elif defined(USE_GLIBC_MALLOC_OVERRIDE) static intptr_t alloc_count = 0; // We override the malloc/realloc/calloc/free functions and then use dlsym to // defer the actual allocation to the real libc implementation. // The dlsym call will (in glibc 2.9/2.10) call calloc once (to allocate an error // message structure), so we have a bootstrapping problem when trying to // get the first called function via dlsym. So we kludge it by returning a statically-allocated // buffer for the very first call to calloc after we've called dlsym. // This is quite hacky but it seems to just about work in practice... // TODO: KNOWN ISSUE: Use after free and infinite recursion // We assign the glibc free function to libc_free in our malloc/calloc function (with dlsym). // We did that in the free function before, but had to change it to work around the first problem described below. // It's not a good solution because some of the problems described here can reappear when the first // call to malloc/calloc changes and enters the function with a different state. // // Dl* functions (dlsym, dlopen etc.) store an error message internally if something fails. // Calling dlerror returns a pointer to this error message. Calling dlerror a second time or calling dlsym // causes it to free the internal storage for this error message. // This behaviour can cause two types of problems: // // 1. Infinite recursion due to free call // Problem occurs if: We use any of the dl* functions in our free function and free gets called with an internal // error message buffer allocated. // What happens: Our call to the dl* function causes another free-call insdie glibc which calls our free function // and can cause infinite recursion. // // 2. Use after free // Problem occurs if: An external library (or any other function) calls a dl* function that stores an internal // error string, then calls dlerror to receive the message and then calls any of our malloc/calloc/realloc/free fuctions. // Our function uses one of the dl* functions too. After calling our function, it tries to use the error message pointer // it got with dlerror before. // What happens: Our call to the dl* function will free the storage of the message and the pointer in the external library // becomes invalid. We get undefined behaviour if the extern library uses the error message pointer after that. static bool alloc_bootstrapped = false; static char alloc_bootstrap_buffer[32]; // sufficient for x86_64 static bool alloc_has_called_dlsym = false; static void (*libc_free)(void*) = NULL; // (We'll only be running a single thread at this point so no need for locking these variables) //#define ALLOC_DEBUG void* malloc(size_t sz) { cpu_AtomicAdd(&alloc_count, 1); static void *(*libc_malloc)(size_t); if (libc_malloc == NULL) { alloc_has_called_dlsym = true; libc_malloc = (void *(*)(size_t)) dlsym(RTLD_NEXT, "malloc"); } void* ret = libc_malloc(sz); #ifdef ALLOC_DEBUG printf("### malloc(%d) = %p\n", sz, ret); #endif if (libc_free == NULL) libc_free = (void (*)(void*)) dlsym(RTLD_NEXT, "free"); return ret; } void* realloc(void* ptr, size_t sz) { cpu_AtomicAdd(&alloc_count, 1); static void *(*libc_realloc)(void*, size_t); if (libc_realloc == NULL) { alloc_has_called_dlsym = true; libc_realloc = (void *(*)(void*, size_t)) dlsym(RTLD_NEXT, "realloc"); } void* ret = libc_realloc(ptr, sz); #ifdef ALLOC_DEBUG printf("### realloc(%p, %d) = %p\n", ptr, sz, ret); #endif return ret; } void* calloc(size_t nm, size_t sz) { cpu_AtomicAdd(&alloc_count, 1); static void *(*libc_calloc)(size_t, size_t); if (libc_calloc == NULL) { if (alloc_has_called_dlsym && !alloc_bootstrapped) { ENSURE(nm*sz <= ARRAY_SIZE(alloc_bootstrap_buffer)); #ifdef ALLOC_DEBUG printf("### calloc-bs(%d, %d) = %p\n", nm, sz, alloc_bootstrap_buffer); #endif alloc_bootstrapped = true; return alloc_bootstrap_buffer; } alloc_has_called_dlsym = true; libc_calloc = (void *(*)(size_t, size_t)) dlsym(RTLD_NEXT, "calloc"); } void* ret = libc_calloc(nm, sz); #ifdef ALLOC_DEBUG printf("### calloc(%d, %d) = %p\n", nm, sz, ret); #endif if (libc_free == NULL) libc_free = (void (*)(void*)) dlsym(RTLD_NEXT, "free"); return ret; } void free(void* ptr) { // Might be triggered if free is called before any calloc/malloc calls or if the dlsym call inside // our calloc/malloc function causes a free call. Read the known issue comment block a few lines above. ENSURE (libc_free != NULL); libc_free(ptr); #ifdef ALLOC_DEBUG printf("### free(%p)\n", ptr); #endif } static void alloc_hook_initialize() { } static long get_memory_alloc_count() { return alloc_count; } #else static void alloc_hook_initialize() { } static long get_memory_alloc_count() { // TODO: don't show this column of data when we don't have sensible values // to display. return 0; } #endif void CProfileNode::Call() { calls_frame_current++; calls_turn_current++; if (recursion++ == 0) { start = timer_Time(); start_mallocs = get_memory_alloc_count(); } } bool CProfileNode::Return() { if (--recursion != 0) return false; double now = timer_Time(); long allocs = get_memory_alloc_count(); time_frame_current += (now - start); time_turn_current += (now - start); mallocs_frame_current += (allocs - start_mallocs); mallocs_turn_current += (allocs - start_mallocs); return true; } CProfileManager::CProfileManager() : root(NULL), current(NULL), needs_structural_reset(false) { PerformStructuralReset(); } CProfileManager::~CProfileManager() { delete root; } void CProfileManager::Start( const char* name ) { if( name != current->GetName() ) current = current->GetChild( name ); current->Call(); } void CProfileManager::StartScript( const char* name ) { if( name != current->GetName() ) current = current->GetScriptChild( name ); current->Call(); } void CProfileManager::Stop() { if (current->Return()) current = current->GetParent(); } void CProfileManager::Reset() { root->Reset(); } void CProfileManager::Frame() { ONCE(alloc_hook_initialize()); root->time_frame_current += (timer_Time() - root->start); root->mallocs_frame_current += (get_memory_alloc_count() - root->start_mallocs); root->Frame(); if (needs_structural_reset) { PerformStructuralReset(); needs_structural_reset = false; } root->start = timer_Time(); root->start_mallocs = get_memory_alloc_count(); } void CProfileManager::Turn() { root->Turn(); } void CProfileManager::StructuralReset() { // We can't immediately perform the reset, because we're probably already // nested inside the profile tree and it will get very confused if we delete // the tree when we're not currently at the root. // So just set a flag to perform the reset at the end of the frame. needs_structural_reset = true; } void CProfileManager::PerformStructuralReset() { delete root; root = new CProfileNode("root", NULL); root->Call(); current = root; g_ProfileViewer.AddRootTable(root->display_table, true); } CProfileSample::CProfileSample(const char* name) { if (CProfileManager::IsInitialised()) { // The profiler is only safe to use on the main thread - if(ThreadUtil::IsMainThread()) + if(Threading::IsMainThread()) g_Profiler.Start(name); } } CProfileSample::~CProfileSample() { if (CProfileManager::IsInitialised()) - if(ThreadUtil::IsMainThread()) + if(Threading::IsMainThread()) g_Profiler.Stop(); } Index: ps/trunk/source/ps/Profiler2.cpp =================================================================== --- ps/trunk/source/ps/Profiler2.cpp (revision 24529) +++ ps/trunk/source/ps/Profiler2.cpp (revision 24530) @@ -1,997 +1,997 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "precompiled.h" #include "Profiler2.h" #include "lib/allocators/shared_ptr.h" #include "lib/os_path.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/CStr.h" #include "ps/Profiler2GPU.h" #include "ps/Pyrogenesis.h" #include "third_party/mongoose/mongoose.h" #include #include #include CProfiler2 g_Profiler2; const size_t CProfiler2::MAX_ATTRIBUTE_LENGTH = 256; // TODO: what's a good size? const size_t CProfiler2::BUFFER_SIZE = 4 * 1024 * 1024; const size_t CProfiler2::HOLD_BUFFER_SIZE = 128 * 1024; // A human-recognisable pattern (for debugging) followed by random bytes (for uniqueness) const u8 CProfiler2::RESYNC_MAGIC[8] = {0x11, 0x22, 0x33, 0x44, 0xf4, 0x93, 0xbe, 0x15}; thread_local CProfiler2::ThreadStorage* CProfiler2::m_CurrentStorage = nullptr; CProfiler2::CProfiler2() : m_Initialised(false), m_FrameNumber(0), m_MgContext(NULL), m_GPU(NULL) { } CProfiler2::~CProfiler2() { if (m_Initialised) Shutdown(); } /** * Mongoose callback. Run in an arbitrary thread (possibly concurrently with other requests). */ static void* MgCallback(mg_event event, struct mg_connection *conn, const struct mg_request_info *request_info) { CProfiler2* profiler = (CProfiler2*)request_info->user_data; ENSURE(profiler); void* handled = (void*)""; // arbitrary non-NULL pointer to indicate successful handling const char* header200 = "HTTP/1.1 200 OK\r\n" "Access-Control-Allow-Origin: *\r\n" // TODO: not great for security "Content-Type: text/plain; charset=utf-8\r\n\r\n"; const char* header404 = "HTTP/1.1 404 Not Found\r\n" "Content-Type: text/plain; charset=utf-8\r\n\r\n" "Unrecognised URI"; const char* header400 = "HTTP/1.1 400 Bad Request\r\n" "Content-Type: text/plain; charset=utf-8\r\n\r\n" "Invalid request"; switch (event) { case MG_NEW_REQUEST: { std::stringstream stream; std::string uri = request_info->uri; if (uri == "/download") { profiler->SaveToFile(); } else if (uri == "/overview") { profiler->ConstructJSONOverview(stream); } else if (uri == "/query") { if (!request_info->query_string) { mg_printf(conn, "%s (no query string)", header400); return handled; } // Identify the requested thread char buf[256]; int len = mg_get_var(request_info->query_string, strlen(request_info->query_string), "thread", buf, ARRAY_SIZE(buf)); if (len < 0) { mg_printf(conn, "%s (no 'thread')", header400); return handled; } std::string thread(buf); const char* err = profiler->ConstructJSONResponse(stream, thread); if (err) { mg_printf(conn, "%s (%s)", header400, err); return handled; } } else { mg_printf(conn, "%s", header404); return handled; } mg_printf(conn, "%s", header200); std::string str = stream.str(); mg_write(conn, str.c_str(), str.length()); return handled; } case MG_HTTP_ERROR: return NULL; case MG_EVENT_LOG: // Called by Mongoose's cry() LOGERROR("Mongoose error: %s", request_info->log_message); return NULL; case MG_INIT_SSL: return NULL; default: debug_warn(L"Invalid Mongoose event type"); return NULL; } }; void CProfiler2::Initialise() { ENSURE(!m_Initialised); m_Initialised = true; RegisterCurrentThread("main"); } void CProfiler2::InitialiseGPU() { ENSURE(!m_GPU); m_GPU = new CProfiler2GPU(*this); } void CProfiler2::EnableHTTP() { ENSURE(m_Initialised); LOGMESSAGERENDER("Starting profiler2 HTTP server"); // Ignore multiple enablings if (m_MgContext) return; CStr listeningPort = "8000"; CStr listeningServer = "127.0.0.1"; CStr numThreads = "6"; if (CConfigDB::IsInitialised()) { CFG_GET_VAL("profiler2.server.port", listeningPort); CFG_GET_VAL("profiler2.server", listeningServer); CFG_GET_VAL("profiler2.server.threads", numThreads); } std::string listening_ports = fmt::format("{0}:{1}", listeningServer, listeningPort); const char* options[] = { "listening_ports", listening_ports.c_str(), "num_threads", numThreads.c_str(), nullptr }; m_MgContext = mg_start(MgCallback, this, options); ENSURE(m_MgContext); } void CProfiler2::EnableGPU() { ENSURE(m_Initialised); if (!m_GPU) { LOGMESSAGERENDER("Starting profiler2 GPU mode"); InitialiseGPU(); } } void CProfiler2::ShutdownGPU() { LOGMESSAGERENDER("Shutting down profiler2 GPU mode"); SAFE_DELETE(m_GPU); } void CProfiler2::ShutDownHTTP() { LOGMESSAGERENDER("Shutting down profiler2 HTTP server"); if (m_MgContext) { mg_stop(m_MgContext); m_MgContext = NULL; } } void CProfiler2::Toggle() { // TODO: Maybe we can open the browser to the profiler page automatically if (m_GPU && m_MgContext) { ShutdownGPU(); ShutDownHTTP(); } else if (!m_GPU && !m_MgContext) { EnableGPU(); EnableHTTP(); } } void CProfiler2::Shutdown() { ENSURE(m_Initialised); ENSURE(!m_GPU); // must shutdown GPU before profiler if (m_MgContext) { mg_stop(m_MgContext); m_MgContext = NULL; } // the destructor is not called for the main thread // we have to call it manually to avoid memory leaks - ENSURE(ThreadUtil::IsMainThread()); + ENSURE(Threading::IsMainThread()); m_Initialised = false; } void CProfiler2::RecordGPUFrameStart() { if (m_GPU) m_GPU->FrameStart(); } void CProfiler2::RecordGPUFrameEnd() { if (m_GPU) m_GPU->FrameEnd(); } void CProfiler2::RecordGPURegionEnter(const char* id) { if (m_GPU) m_GPU->RegionEnter(id); } void CProfiler2::RecordGPURegionLeave(const char* id) { if (m_GPU) m_GPU->RegionLeave(id); } void CProfiler2::RegisterCurrentThread(const std::string& name) { ENSURE(m_Initialised); // Must not register a thread more than once. ENSURE(m_CurrentStorage == nullptr); m_CurrentStorage = new ThreadStorage(*this, name); AddThreadStorage(m_CurrentStorage); RecordSyncMarker(); RecordEvent("thread start"); } void CProfiler2::AddThreadStorage(ThreadStorage* storage) { std::lock_guard lock(m_Mutex); m_Threads.push_back(std::unique_ptr(storage)); } void CProfiler2::RemoveThreadStorage(ThreadStorage* storage) { std::lock_guard lock(m_Mutex); m_Threads.erase(std::find_if(m_Threads.begin(), m_Threads.end(), [storage](const std::unique_ptr& s) { return s.get() == storage; })); } CProfiler2::ThreadStorage::ThreadStorage(CProfiler2& profiler, const std::string& name) : m_Profiler(profiler), m_Name(name), m_BufferPos0(0), m_BufferPos1(0), m_LastTime(timer_Time()), m_HeldDepth(0) { m_Buffer = new u8[BUFFER_SIZE]; memset(m_Buffer, ITEM_NOP, BUFFER_SIZE); } CProfiler2::ThreadStorage::~ThreadStorage() { delete[] m_Buffer; } void CProfiler2::ThreadStorage::Write(EItem type, const void* item, u32 itemSize) { if (m_HeldDepth > 0) { WriteHold(type, item, itemSize); return; } // See m_BufferPos0 etc for comments on synchronisation u32 size = 1 + itemSize; u32 start = m_BufferPos0; if (start + size > BUFFER_SIZE) { // The remainder of the buffer is too small - fill the rest // with NOPs then start from offset 0, so we don't have to // bother splitting the real item across the end of the buffer m_BufferPos0 = size; COMPILER_FENCE; // must write m_BufferPos0 before m_Buffer memset(m_Buffer + start, 0, BUFFER_SIZE - start); start = 0; } else { m_BufferPos0 = start + size; COMPILER_FENCE; // must write m_BufferPos0 before m_Buffer } m_Buffer[start] = (u8)type; memcpy(&m_Buffer[start + 1], item, itemSize); COMPILER_FENCE; // must write m_BufferPos1 after m_Buffer m_BufferPos1 = start + size; } void CProfiler2::ThreadStorage::WriteHold(EItem type, const void* item, u32 itemSize) { u32 size = 1 + itemSize; if (m_HoldBuffers[m_HeldDepth - 1].pos + size > CProfiler2::HOLD_BUFFER_SIZE) return; // we held on too much data, ignore the rest m_HoldBuffers[m_HeldDepth - 1].buffer[m_HoldBuffers[m_HeldDepth - 1].pos] = (u8)type; memcpy(&m_HoldBuffers[m_HeldDepth - 1].buffer[m_HoldBuffers[m_HeldDepth - 1].pos + 1], item, itemSize); m_HoldBuffers[m_HeldDepth - 1].pos += size; } std::string CProfiler2::ThreadStorage::GetBuffer() { // Called from an arbitrary thread (not the one writing to the buffer). // // See comments on m_BufferPos0 etc. shared_ptr buffer(new u8[BUFFER_SIZE], ArrayDeleter()); u32 pos1 = m_BufferPos1; COMPILER_FENCE; // must read m_BufferPos1 before m_Buffer memcpy(buffer.get(), m_Buffer, BUFFER_SIZE); COMPILER_FENCE; // must read m_BufferPos0 after m_Buffer u32 pos0 = m_BufferPos0; // The range [pos1, pos0) modulo BUFFER_SIZE is invalid, so concatenate the rest of the buffer if (pos1 <= pos0) // invalid range is in the middle of the buffer return std::string(buffer.get()+pos0, buffer.get()+BUFFER_SIZE) + std::string(buffer.get(), buffer.get()+pos1); else // invalid wrap is wrapped around the end/start buffer return std::string(buffer.get()+pos0, buffer.get()+pos1); } void CProfiler2::ThreadStorage::RecordAttribute(const char* fmt, va_list argp) { char buffer[MAX_ATTRIBUTE_LENGTH + 4] = {0}; // first 4 bytes are used for storing length int len = vsnprintf(buffer + 4, MAX_ATTRIBUTE_LENGTH - 1, fmt, argp); // subtract 1 from length to make MSVC vsnprintf safe // (Don't use vsprintf_s because it treats overflow as fatal) // Terminate the string if the printing was truncated if (len < 0 || len >= (int)MAX_ATTRIBUTE_LENGTH - 1) { strncpy(buffer + 4 + MAX_ATTRIBUTE_LENGTH - 4, "...", 4); len = MAX_ATTRIBUTE_LENGTH - 1; // excluding null terminator } // Store the length in the buffer memcpy(buffer, &len, sizeof(len)); Write(ITEM_ATTRIBUTE, buffer, 4 + len); } size_t CProfiler2::ThreadStorage::HoldLevel() { return m_HeldDepth; } u8 CProfiler2::ThreadStorage::HoldType() { return m_HoldBuffers[m_HeldDepth - 1].type; } void CProfiler2::ThreadStorage::PutOnHold(u8 newType) { m_HeldDepth++; m_HoldBuffers[m_HeldDepth - 1].clear(); m_HoldBuffers[m_HeldDepth - 1].setType(newType); } // this flattens the stack, use it sensibly void rewriteBuffer(u8* buffer, u32& bufferSize) { double startTime = timer_Time(); u32 size = bufferSize; u32 readPos = 0; double initialTime = -1; double total_time = -1; const char* regionName; std::set topLevelArgs; using infoPerType = std::tuple >; using timeByTypeMap = std::unordered_map; timeByTypeMap timeByType; std::vector last_time_stack; std::vector last_names; // never too many hacks std::string current_attribute = ""; std::map time_per_attribute; // Let's read the first event { u8 type = buffer[readPos]; ++readPos; if (type != CProfiler2::ITEM_ENTER) { debug_warn("Profiler2: Condensing a region should run into ITEM_ENTER first"); return; // do nothing } CProfiler2::SItem_dt_id item; memcpy(&item, buffer + readPos, sizeof(item)); readPos += sizeof(item); regionName = item.id; last_names.push_back(item.id); initialTime = (double)item.dt; } int enter = 1; int leaves = 0; // Read subsequent events. Flatten hierarchy because it would get too complicated otherwise. // To make sure time doesn't bloat, subtract time from nested events while (readPos < size) { u8 type = buffer[readPos]; ++readPos; switch (type) { case CProfiler2::ITEM_NOP: { // ignore break; } case CProfiler2::ITEM_SYNC: { debug_warn("Aggregated regions should not be used across frames"); // still try to act sane readPos += sizeof(double); readPos += sizeof(CProfiler2::RESYNC_MAGIC); break; } case CProfiler2::ITEM_EVENT: { // skip for now readPos += sizeof(CProfiler2::SItem_dt_id); break; } case CProfiler2::ITEM_ENTER: { enter++; CProfiler2::SItem_dt_id item; memcpy(&item, buffer + readPos, sizeof(item)); readPos += sizeof(item); last_time_stack.push_back((double)item.dt); last_names.push_back(item.id); current_attribute = ""; break; } case CProfiler2::ITEM_LEAVE: { float item_time; memcpy(&item_time, buffer + readPos, sizeof(float)); readPos += sizeof(float); leaves++; if (last_names.empty()) { // we somehow lost the first entry in the process debug_warn("Invalid buffer for condensing"); } const char* item_name = last_names.back(); last_names.pop_back(); if (last_time_stack.empty()) { // this is the leave for the whole scope total_time = (double)item_time; break; } double time = (double)item_time - last_time_stack.back(); std::string name = std::string(item_name); timeByTypeMap::iterator TimeForType = timeByType.find(name); if (TimeForType == timeByType.end()) { // keep reference to the original char pointer to make sure we don't break things down the line std::get<0>(timeByType[name]) = item_name; std::get<1>(timeByType[name]) = 0; } std::get<1>(timeByType[name]) += time; last_time_stack.pop_back(); // if we were nested, subtract our time from the below scope by making it look like it starts later if (!last_time_stack.empty()) last_time_stack.back() += time; if (!current_attribute.empty()) { time_per_attribute[current_attribute] += time; } break; } case CProfiler2::ITEM_ATTRIBUTE: { // skip for now u32 len; memcpy(&len, buffer + readPos, sizeof(len)); ENSURE(len <= CProfiler2::MAX_ATTRIBUTE_LENGTH); readPos += sizeof(len); char message[CProfiler2::MAX_ATTRIBUTE_LENGTH] = {0}; memcpy(&message[0], buffer + readPos, std::min(size_t(len), CProfiler2::MAX_ATTRIBUTE_LENGTH)); CStr mess = CStr((const char*)message, len); if (!last_names.empty()) { timeByTypeMap::iterator it = timeByType.find(std::string(last_names.back())); if (it == timeByType.end()) topLevelArgs.insert(mess); else std::get<2>(timeByType[std::string(last_names.back())]).insert(mess); } readPos += len; current_attribute = mess; break; } default: debug_warn(L"Invalid profiler item when condensing buffer"); continue; } } // rewrite the buffer // what we rewrite will always be smaller than the current buffer's size u32 writePos = 0; double curTime = initialTime; // the region enter { CProfiler2::SItem_dt_id item = { (float)curTime, regionName }; buffer[writePos] = (u8)CProfiler2::ITEM_ENTER; memcpy(buffer + writePos + 1, &item, sizeof(item)); writePos += sizeof(item) + 1; // add a nanosecond for sanity curTime += 0.000001; } // sub-events, aggregated for (const std::pair& type : timeByType) { CProfiler2::SItem_dt_id item = { (float)curTime, std::get<0>(type.second) }; buffer[writePos] = (u8)CProfiler2::ITEM_ENTER; memcpy(buffer + writePos + 1, &item, sizeof(item)); writePos += sizeof(item) + 1; // write relevant attributes if present for (const std::string& attrib : std::get<2>(type.second)) { buffer[writePos] = (u8)CProfiler2::ITEM_ATTRIBUTE; writePos++; std::string basic = attrib; std::map::iterator time_attrib = time_per_attribute.find(attrib); if (time_attrib != time_per_attribute.end()) basic += " " + CStr::FromInt(1000000*time_attrib->second) + "us"; u32 length = basic.size(); memcpy(buffer + writePos, &length, sizeof(length)); writePos += sizeof(length); memcpy(buffer + writePos, basic.c_str(), length); writePos += length; } curTime += std::get<1>(type.second); float leave_time = (float)curTime; buffer[writePos] = (u8)CProfiler2::ITEM_LEAVE; memcpy(buffer + writePos + 1, &leave_time, sizeof(float)); writePos += sizeof(float) + 1; } // Time of computation { CProfiler2::SItem_dt_id item = { (float)curTime, "CondenseBuffer" }; buffer[writePos] = (u8)CProfiler2::ITEM_ENTER; memcpy(buffer + writePos + 1, &item, sizeof(item)); writePos += sizeof(item) + 1; } { float time_out = (float)(curTime + timer_Time() - startTime); buffer[writePos] = (u8)CProfiler2::ITEM_LEAVE; memcpy(buffer + writePos + 1, &time_out, sizeof(float)); writePos += sizeof(float) + 1; // add a nanosecond for sanity curTime += 0.000001; } // the region leave { if (total_time < 0) { total_time = curTime + 0.000001; buffer[writePos] = (u8)CProfiler2::ITEM_ATTRIBUTE; writePos++; u32 length = sizeof("buffer overflow"); memcpy(buffer + writePos, &length, sizeof(length)); writePos += sizeof(length); memcpy(buffer + writePos, "buffer overflow", length); writePos += length; } else if (total_time < curTime) { // this seems to happen on rare occasions. curTime = total_time; } float leave_time = (float)total_time; buffer[writePos] = (u8)CProfiler2::ITEM_LEAVE; memcpy(buffer + writePos + 1, &leave_time, sizeof(float)); writePos += sizeof(float) + 1; } bufferSize = writePos; } void CProfiler2::ThreadStorage::HoldToBuffer(bool condensed) { ENSURE(m_HeldDepth); if (condensed) { // rewrite the buffer to show aggregated data rewriteBuffer(m_HoldBuffers[m_HeldDepth - 1].buffer, m_HoldBuffers[m_HeldDepth - 1].pos); } if (m_HeldDepth > 1) { // copy onto buffer below HoldBuffer& copied = m_HoldBuffers[m_HeldDepth - 1]; HoldBuffer& target = m_HoldBuffers[m_HeldDepth - 2]; if (target.pos + copied.pos > HOLD_BUFFER_SIZE) return; // too much data, too bad memcpy(&target.buffer[target.pos], copied.buffer, copied.pos); target.pos += copied.pos; } else { u32 size = m_HoldBuffers[m_HeldDepth - 1].pos; u32 start = m_BufferPos0; if (start + size > BUFFER_SIZE) { m_BufferPos0 = size; COMPILER_FENCE; memset(m_Buffer + start, 0, BUFFER_SIZE - start); start = 0; } else { m_BufferPos0 = start + size; COMPILER_FENCE; // must write m_BufferPos0 before m_Buffer } memcpy(&m_Buffer[start], m_HoldBuffers[m_HeldDepth - 1].buffer, size); COMPILER_FENCE; // must write m_BufferPos1 after m_Buffer m_BufferPos1 = start + size; } m_HeldDepth--; } void CProfiler2::ThreadStorage::ThrowawayHoldBuffer() { if (!m_HeldDepth) return; m_HeldDepth--; } void CProfiler2::ConstructJSONOverview(std::ostream& stream) { TIMER(L"profile2 overview"); std::lock_guard lock(m_Mutex); stream << "{\"threads\":["; bool first_time = true; for (std::unique_ptr& storage : m_Threads) { if (!first_time) stream << ","; stream << "{\"name\":\"" << CStr(storage->GetName()).EscapeToPrintableASCII() << "\"}"; first_time = false; } stream << "]}"; } /** * Given a buffer and a visitor class (with functions OnEvent, OnEnter, OnLeave, OnAttribute), * calls the visitor for every item in the buffer. */ template void RunBufferVisitor(const std::string& buffer, V& visitor) { TIMER(L"profile2 visitor"); // The buffer doesn't necessarily start at the beginning of an item // (we just grabbed it from some arbitrary point in the middle), // so scan forwards until we find a sync marker. // (This is probably pretty inefficient.) u32 realStart = (u32)-1; // the start point decided by the scan algorithm for (u32 start = 0; start + 1 + sizeof(CProfiler2::RESYNC_MAGIC) <= buffer.length(); ++start) { if (buffer[start] == CProfiler2::ITEM_SYNC && memcmp(buffer.c_str() + start + 1, &CProfiler2::RESYNC_MAGIC, sizeof(CProfiler2::RESYNC_MAGIC)) == 0) { realStart = start; break; } } ENSURE(realStart != (u32)-1); // we should have found a sync point somewhere in the buffer u32 pos = realStart; // the position as we step through the buffer double lastTime = -1; // set to non-negative by EVENT_SYNC; we ignore all items before that // since we can't compute their absolute times while (pos < buffer.length()) { u8 type = buffer[pos]; ++pos; switch (type) { case CProfiler2::ITEM_NOP: { // ignore break; } case CProfiler2::ITEM_SYNC: { u8 magic[sizeof(CProfiler2::RESYNC_MAGIC)]; double t; memcpy(magic, buffer.c_str()+pos, ARRAY_SIZE(magic)); ENSURE(memcmp(magic, &CProfiler2::RESYNC_MAGIC, sizeof(CProfiler2::RESYNC_MAGIC)) == 0); pos += sizeof(CProfiler2::RESYNC_MAGIC); memcpy(&t, buffer.c_str()+pos, sizeof(t)); pos += sizeof(t); lastTime = t; visitor.OnSync(lastTime); break; } case CProfiler2::ITEM_EVENT: { CProfiler2::SItem_dt_id item; memcpy(&item, buffer.c_str()+pos, sizeof(item)); pos += sizeof(item); if (lastTime >= 0) { visitor.OnEvent(lastTime + (double)item.dt, item.id); } break; } case CProfiler2::ITEM_ENTER: { CProfiler2::SItem_dt_id item; memcpy(&item, buffer.c_str()+pos, sizeof(item)); pos += sizeof(item); if (lastTime >= 0) { visitor.OnEnter(lastTime + (double)item.dt, item.id); } break; } case CProfiler2::ITEM_LEAVE: { float leave_time; memcpy(&leave_time, buffer.c_str() + pos, sizeof(float)); pos += sizeof(float); if (lastTime >= 0) { visitor.OnLeave(lastTime + (double)leave_time); } break; } case CProfiler2::ITEM_ATTRIBUTE: { u32 len; memcpy(&len, buffer.c_str()+pos, sizeof(len)); ENSURE(len <= CProfiler2::MAX_ATTRIBUTE_LENGTH); pos += sizeof(len); std::string attribute(buffer.c_str()+pos, buffer.c_str()+pos+len); pos += len; if (lastTime >= 0) { visitor.OnAttribute(attribute); } break; } default: debug_warn(L"Invalid profiler item when parsing buffer"); return; } } }; /** * Visitor class that dumps events as JSON. * TODO: this is pretty inefficient (in implementation and in output format). */ struct BufferVisitor_Dump { NONCOPYABLE(BufferVisitor_Dump); public: BufferVisitor_Dump(std::ostream& stream) : m_Stream(stream) { } void OnSync(double UNUSED(time)) { // Split the array of items into an array of array (arbitrarily splitting // around the sync points) to avoid array-too-large errors in JSON decoders m_Stream << "null], [\n"; } void OnEvent(double time, const char* id) { m_Stream << "[1," << std::fixed << std::setprecision(9) << time; m_Stream << ",\"" << CStr(id).EscapeToPrintableASCII() << "\"],\n"; } void OnEnter(double time, const char* id) { m_Stream << "[2," << std::fixed << std::setprecision(9) << time; m_Stream << ",\"" << CStr(id).EscapeToPrintableASCII() << "\"],\n"; } void OnLeave(double time) { m_Stream << "[3," << std::fixed << std::setprecision(9) << time << "],\n"; } void OnAttribute(const std::string& attr) { m_Stream << "[4,\"" << CStr(attr).EscapeToPrintableASCII() << "\"],\n"; } std::ostream& m_Stream; }; const char* CProfiler2::ConstructJSONResponse(std::ostream& stream, const std::string& thread) { TIMER(L"profile2 query"); std::string buffer; { TIMER(L"profile2 get buffer"); std::lock_guard lock(m_Mutex); // lock against changes to m_Threads or deletions of ThreadStorage std::vector>::iterator it = std::find_if(m_Threads.begin(), m_Threads.end(), [&thread](std::unique_ptr& storage) { return storage->GetName() == thread; }); if (it == m_Threads.end()) return "cannot find named thread"; stream << "{\"events\":[\n"; stream << "[\n"; buffer = (*it)->GetBuffer(); } BufferVisitor_Dump visitor(stream); RunBufferVisitor(buffer, visitor); stream << "null]\n]}"; return NULL; } void CProfiler2::SaveToFile() { OsPath path = psLogDir()/"profile2.jsonp"; debug_printf("Writing profile data to %s \n", path.string8().c_str()); LOGMESSAGERENDER("Writing profile data to %s \n", path.string8().c_str()); std::ofstream stream(OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); ENSURE(stream.good()); stream << "profileDataCB({\"threads\": [\n"; bool first_time = true; for (std::unique_ptr& storage : m_Threads) { if (!first_time) stream << ",\n"; stream << "{\"name\":\"" << CStr(storage->GetName()).EscapeToPrintableASCII() << "\",\n"; stream << "\"data\": "; ConstructJSONResponse(stream, storage->GetName()); stream << "\n}"; first_time = false; } stream << "\n]});\n"; } CProfile2SpikeRegion::CProfile2SpikeRegion(const char* name, double spikeLimit) : m_Name(name), m_Limit(spikeLimit), m_PushedHold(true) { if (g_Profiler2.HoldLevel() < 8 && g_Profiler2.HoldType() != CProfiler2::ThreadStorage::BUFFER_AGGREGATE) g_Profiler2.HoldMessages(CProfiler2::ThreadStorage::BUFFER_SPIKE); else m_PushedHold = false; COMPILER_FENCE; g_Profiler2.RecordRegionEnter(m_Name); m_StartTime = g_Profiler2.GetTime(); } CProfile2SpikeRegion::~CProfile2SpikeRegion() { double time = g_Profiler2.GetTime(); g_Profiler2.RecordRegionLeave(); bool shouldWrite = time - m_StartTime > m_Limit; if (m_PushedHold) g_Profiler2.StopHoldingMessages(shouldWrite); } CProfile2AggregatedRegion::CProfile2AggregatedRegion(const char* name, double spikeLimit) : m_Name(name), m_Limit(spikeLimit), m_PushedHold(true) { if (g_Profiler2.HoldLevel() < 8 && g_Profiler2.HoldType() != CProfiler2::ThreadStorage::BUFFER_AGGREGATE) g_Profiler2.HoldMessages(CProfiler2::ThreadStorage::BUFFER_AGGREGATE); else m_PushedHold = false; COMPILER_FENCE; g_Profiler2.RecordRegionEnter(m_Name); m_StartTime = g_Profiler2.GetTime(); } CProfile2AggregatedRegion::~CProfile2AggregatedRegion() { double time = g_Profiler2.GetTime(); g_Profiler2.RecordRegionLeave(); bool shouldWrite = time - m_StartTime > m_Limit; if (m_PushedHold) g_Profiler2.StopHoldingMessages(shouldWrite, true); } Index: ps/trunk/source/ps/Profiler2.h =================================================================== --- ps/trunk/source/ps/Profiler2.h (revision 24529) +++ ps/trunk/source/ps/Profiler2.h (revision 24530) @@ -1,558 +1,558 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /** * @file * New profiler (complementing the older CProfileManager) * * The profiler is designed for analysing framerate fluctuations or glitches, * and temporal relationships between threads. * This contrasts with CProfilerManager and most external profiling tools, * which are designed more for measuring average throughput over a number of * frames. * * To view the profiler output, press F11 to enable the HTTP output mode * and then open source/tools/profiler2/profiler2.html in a web browser. * * There is a single global CProfiler2 instance (g_Profiler2), * providing the API used by the rest of the game. * The game can record the entry/exit timings of a region of code * using the PROFILE2 macro, and can record other events using * PROFILE2_EVENT. * Regions and events can be annotated with arbitrary string attributes, * specified with printf-style format strings, using PROFILE2_ATTR * (e.g. PROFILE2_ATTR("frame: %d", m_FrameNum) ). * * This is designed for relatively coarse-grained profiling, or for rare events. * Don't use it for regions that are typically less than ~0.1msecs, or that are * called hundreds of times per frame. (The old CProfilerManager is better * for that.) * * New threads must call g_Profiler2.RegisterCurrentThread before any other * profiler functions. * * The main thread should call g_Profiler2.RecordFrameStart at the start of * each frame. * Other threads should call g_Profiler2.RecordSyncMarker occasionally, * especially if it's been a long time since their last call to the profiler, * or if they've made thousands of calls since the last sync marker. * * The profiler is implemented with thread-local fixed-size ring buffers, * which store a sequence of variable-length items indicating the time * of the event and associated data (pointers to names, attribute strings, etc). * An HTTP server provides access to the data: when requested, it will make * a copy of a thread's buffer, then parse the items and return them in JSON * format. The profiler2.html requests and processes and visualises this data. * * The RecordSyncMarker calls are necessary to correct for time drift and to * let the buffer parser accurately detect the start of an item in the byte stream. * * This design aims to minimise the performance overhead of recording data, * and to simplify the visualisation of the data by doing it externally in an * environment with better UI tools (i.e. HTML) instead of within the game engine. * * The initial setup of g_Profiler2 must happen in the game's main thread. * RegisterCurrentThread and the Record functions may be called from any thread. * The HTTP server runs its own threads, which may call the ConstructJSON functions. */ #ifndef INCLUDED_PROFILER2 #define INCLUDED_PROFILER2 #include "lib/timer.h" #include "ps/ThreadUtil.h" #include #include #include #include #include struct mg_context; // Note: Lots of functions are defined inline, to hypothetically // minimise performance overhead. class CProfiler2GPU; class CProfiler2 { friend class CProfiler2GPU_base; friend class CProfile2SpikeRegion; friend class CProfile2AggregatedRegion; public: // Items stored in the buffers: /// Item type identifiers enum EItem { ITEM_NOP = 0, ITEM_SYNC = 1, // magic value used for parse syncing ITEM_EVENT = 2, // single event ITEM_ENTER = 3, // entering a region ITEM_LEAVE = 4, // leaving a region (must be correctly nested) ITEM_ATTRIBUTE = 5, // arbitrary string associated with current region, or latest event (if the previous item was an event) }; static const size_t MAX_ATTRIBUTE_LENGTH; // includes null terminator, which isn't stored /// An arbitrary number to help resyncing with the item stream when parsing. static const u8 RESYNC_MAGIC[8]; /** * An item with a relative time and an ID string pointer. */ struct SItem_dt_id { float dt; // time relative to last event const char* id; }; private: // TODO: different threads might want different sizes static const size_t BUFFER_SIZE; static const size_t HOLD_BUFFER_SIZE; /** * Class instantiated in every registered thread. */ class ThreadStorage { NONCOPYABLE(ThreadStorage); public: ThreadStorage(CProfiler2& profiler, const std::string& name); ~ThreadStorage(); enum { BUFFER_NORMAL, BUFFER_SPIKE, BUFFER_AGGREGATE }; void RecordSyncMarker(double t) { // Store the magic string followed by the absolute time // (to correct for drift caused by the precision of relative // times stored in other items) u8 buffer[sizeof(RESYNC_MAGIC) + sizeof(t)]; memcpy(buffer, &RESYNC_MAGIC, sizeof(RESYNC_MAGIC)); memcpy(buffer + sizeof(RESYNC_MAGIC), &t, sizeof(t)); Write(ITEM_SYNC, buffer, ARRAY_SIZE(buffer)); m_LastTime = t; } void Record(EItem type, double t, const char* id) { // Store a relative time instead of absolute, so we can use floats // (to save memory) without suffering from precision problems SItem_dt_id item = { (float)(t - m_LastTime), id }; Write(type, &item, sizeof(item)); } void RecordFrameStart(double t) { RecordSyncMarker(t); Record(ITEM_EVENT, t, "__framestart"); // magic string recognised by the visualiser } void RecordLeave(double t) { float time = (float)(t - m_LastTime); Write(ITEM_LEAVE, &time, sizeof(float)); } void RecordAttribute(const char* fmt, va_list argp) VPRINTF_ARGS(2); void RecordAttributePrintf(const char* fmt, ...) PRINTF_ARGS(2) { va_list argp; va_start(argp, fmt); RecordAttribute(fmt, argp); va_end(argp); } size_t HoldLevel(); u8 HoldType(); void PutOnHold(u8 type); void HoldToBuffer(bool condensed); void ThrowawayHoldBuffer(); CProfiler2& GetProfiler() { return m_Profiler; } const std::string& GetName() { return m_Name; } /** * Returns a copy of a subset of the thread's buffer. * Not guaranteed to start on an item boundary. * May be called by any thread. */ std::string GetBuffer(); private: /** * Store an item into the buffer. */ void Write(EItem type, const void* item, u32 itemSize); void WriteHold(EItem type, const void* item, u32 itemSize); CProfiler2& m_Profiler; std::string m_Name; double m_LastTime; // used for computing relative times u8* m_Buffer; struct HoldBuffer { friend class ThreadStorage; public: HoldBuffer() { buffer = new u8[HOLD_BUFFER_SIZE]; memset(buffer, ITEM_NOP, HOLD_BUFFER_SIZE); pos = 0; } ~HoldBuffer() { delete[] buffer; } void clear() { pos = 0; } void setType(u8 newType) { type = newType; } u8* buffer; u32 pos; u8 type; }; HoldBuffer m_HoldBuffers[8]; size_t m_HeldDepth; // To allow hopefully-safe reading of the buffer from a separate thread, // without any expensive synchronisation in the recording thread, // two copies of the current buffer write position are stored. // BufferPos0 is updated before writing; BufferPos1 is updated after writing. // GetBuffer can read Pos1, copy the buffer, read Pos0, then assume any bytes // outside the range Pos1 <= x < Pos0 are safe to use. (Any in that range might // be half-written and corrupted.) (All ranges are modulo BUFFER_SIZE.) // Outside of Write(), these will always be equal. // // TODO: does this attempt at synchronisation (plus use of COMPILER_FENCE etc) // actually work in practice? u32 m_BufferPos0; u32 m_BufferPos1; }; public: CProfiler2(); ~CProfiler2(); /** * Call in main thread to set up the profiler, * before calling any other profiler functions. */ void Initialise(); /** * Call in main thread to enable the HTTP server. * (Disabled by default for security and performance * and to avoid annoying a firewall.) */ void EnableHTTP(); /** * Call in main thread to enable the GPU profiling support, * after OpenGL has been initialised. */ void EnableGPU(); /** * Call in main thread to shut down the GPU profiling support, * before shutting down OpenGL. */ void ShutdownGPU(); /** * Call in main thread to shut down the profiler's HTTP server */ void ShutDownHTTP(); /** * Call in main thread to enable/disable the profiler */ void Toggle(); /** * Call in main thread to shut everything down. * All other profiled threads should have been terminated already. */ void Shutdown(); /** * Call in any thread to enable the profiler in that thread. * @p name should be unique, and is used by the visualiser to identify * this thread. */ void RegisterCurrentThread(const std::string& name); /** * Non-main threads should call this occasionally, * especially if it's been a long time since their last call to the profiler, * or if they've made thousands of calls since the last sync marker. */ void RecordSyncMarker() { GetThreadStorage().RecordSyncMarker(GetTime()); } /** * Call in main thread at the start of a frame. */ void RecordFrameStart() { - ENSURE(ThreadUtil::IsMainThread()); + ENSURE(Threading::IsMainThread()); GetThreadStorage().RecordFrameStart(GetTime()); } void RecordEvent(const char* id) { GetThreadStorage().Record(ITEM_EVENT, GetTime(), id); } void RecordRegionEnter(const char* id) { GetThreadStorage().Record(ITEM_ENTER, GetTime(), id); } void RecordRegionEnter(const char* id, double time) { GetThreadStorage().Record(ITEM_ENTER, time, id); } void RecordRegionLeave() { GetThreadStorage().RecordLeave(GetTime()); } void RecordAttribute(const char* fmt, ...) PRINTF_ARGS(2) { va_list argp; va_start(argp, fmt); GetThreadStorage().RecordAttribute(fmt, argp); va_end(argp); } void RecordGPUFrameStart(); void RecordGPUFrameEnd(); void RecordGPURegionEnter(const char* id); void RecordGPURegionLeave(const char* id); /** * Hold onto messages until a call to release or write the held messages. */ size_t HoldLevel() { return GetThreadStorage().HoldLevel(); } u8 HoldType() { return GetThreadStorage().HoldType(); } void HoldMessages(u8 type) { GetThreadStorage().PutOnHold(type); } void StopHoldingMessages(bool writeToBuffer, bool condensed = false) { if (writeToBuffer) GetThreadStorage().HoldToBuffer(condensed); else GetThreadStorage().ThrowawayHoldBuffer(); } /** * Call in any thread to produce a JSON representation of the general * state of the application. */ void ConstructJSONOverview(std::ostream& stream); /** * Call in any thread to produce a JSON representation of the buffer * for a given thread. * Returns NULL on success, or an error string. */ const char* ConstructJSONResponse(std::ostream& stream, const std::string& thread); /** * Call in any thread to save a JSONP representation of the buffers * for all threads, to a file named profile2.jsonp in the logs directory. */ void SaveToFile(); double GetTime() { return timer_Time(); } int GetFrameNumber() { return m_FrameNumber; } void IncrementFrameNumber() { ++m_FrameNumber; } void AddThreadStorage(ThreadStorage* storage); void RemoveThreadStorage(ThreadStorage* storage); private: void InitialiseGPU(); ThreadStorage& GetThreadStorage() { ENSURE(m_CurrentStorage); return *m_CurrentStorage; } bool m_Initialised; int m_FrameNumber; mg_context* m_MgContext; CProfiler2GPU* m_GPU; std::mutex m_Mutex; static thread_local ThreadStorage* m_CurrentStorage; std::vector> m_Threads; // thread-safe; protected by m_Mutex }; extern CProfiler2 g_Profiler2; /** * Scope-based enter/leave helper. */ class CProfile2Region { public: CProfile2Region(const char* name) : m_Name(name) { g_Profiler2.RecordRegionEnter(m_Name); } ~CProfile2Region() { g_Profiler2.RecordRegionLeave(); } protected: const char* m_Name; }; /** * Scope-based enter/leave helper. */ class CProfile2SpikeRegion { public: CProfile2SpikeRegion(const char* name, double spikeLimit); ~CProfile2SpikeRegion(); private: const char* m_Name; double m_Limit; double m_StartTime; bool m_PushedHold; }; /** * Scope-based enter/leave helper. */ class CProfile2AggregatedRegion { public: CProfile2AggregatedRegion(const char* name, double spikeLimit); ~CProfile2AggregatedRegion(); private: const char* m_Name; double m_Limit; double m_StartTime; bool m_PushedHold; }; /** * Scope-based GPU enter/leave helper. */ class CProfile2GPURegion { public: CProfile2GPURegion(const char* name) : m_Name(name) { g_Profiler2.RecordGPURegionEnter(m_Name); } ~CProfile2GPURegion() { g_Profiler2.RecordGPURegionLeave(m_Name); } private: const char* m_Name; }; /** * Starts timing from now until the end of the current scope. * @p region is the name to associate with this region (should be * a constant string literal; the pointer must remain valid forever). * Regions may be nested, but preferably shouldn't be nested deeply since * it hurts the visualisation. */ #define PROFILE2(region) CProfile2Region profile2__(region) #define PROFILE2_IFSPIKE(region, limit) CProfile2SpikeRegion profile2__(region, limit) #define PROFILE2_AGGREGATED(region, limit) CProfile2AggregatedRegion profile2__(region, limit) #define PROFILE2_GPU(region) CProfile2GPURegion profile2gpu__(region) /** * Record the named event at the current time. */ #define PROFILE2_EVENT(name) g_Profiler2.RecordEvent(name) /** * Associates a string (with printf-style formatting) with the current * region or event. * (If the last profiler call was PROFILE2_EVENT, it associates with that * event; otherwise it associates with the currently-active region.) */ #define PROFILE2_ATTR g_Profiler2.RecordAttribute #endif // INCLUDED_PROFILER2 Index: ps/trunk/source/ps/Pyrogenesis.cpp =================================================================== --- ps/trunk/source/ps/Pyrogenesis.cpp (revision 24529) +++ ps/trunk/source/ps/Pyrogenesis.cpp (revision 24530) @@ -1,83 +1,84 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include #include "Pyrogenesis.h" #include "lib/sysdep/sysdep.h" #include "lib/svn_revision.h" const char* engine_version = "0.0.24"; +const char* main_window_name = "0 A.D."; // convert contents of file from char to wchar_t and // append to file. static void AppendAsciiFile(FILE* out, const OsPath& pathname) { FILE* in = sys_OpenFile(pathname, "rb"); if (!in) { fwprintf(out, L"(unavailable)"); return; } const size_t buf_size = 1024; char buf[buf_size+1]; // include space for trailing '\0' while (!feof(in)) { size_t bytes_read = fread(buf, 1, buf_size, in); if (!bytes_read) break; buf[bytes_read] = 0; // 0-terminate fwprintf(out, L"%hs", buf); } fclose(in); } // for user convenience, bundle all logs into this file: void psBundleLogs(FILE* f) { fwprintf(f, L"SVN Revision: %ls\n\n", svn_revision); fwprintf(f, L"Engine Version: %hs\n\n", engine_version); fwprintf(f, L"System info:\n\n"); OsPath path1 = psLogDir()/"system_info.txt"; AppendAsciiFile(f, path1); fwprintf(f, L"\n\n====================================\n\n"); fwprintf(f, L"Main log:\n\n"); OsPath path2 = psLogDir()/"mainlog.html"; AppendAsciiFile(f, path2); fwprintf(f, L"\n\n====================================\n\n"); } static OsPath logDir; void psSetLogDir(const OsPath& newLogDir) { logDir = newLogDir; } const OsPath& psLogDir() { return logDir; } Index: ps/trunk/source/ps/Pyrogenesis.h =================================================================== --- ps/trunk/source/ps/Pyrogenesis.h (revision 24529) +++ ps/trunk/source/ps/Pyrogenesis.h (revision 24530) @@ -1,36 +1,37 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* Pyrogenesis.h Standard declarations which are included in all projects. */ #ifndef INCLUDED_PYROGENESIS #define INCLUDED_PYROGENESIS class Path; using OsPath = Path; extern const char* engine_version; +extern const char* main_window_name; extern void psBundleLogs(FILE* f); // set during InitVfs extern void psSetLogDir(const OsPath& logDir); // set during InitVfs extern const OsPath& psLogDir(); // used by AppHooks and engine code when reporting errors #endif Index: ps/trunk/source/ps/ThreadUtil.h =================================================================== --- ps/trunk/source/ps/ThreadUtil.h (revision 24529) +++ ps/trunk/source/ps/ThreadUtil.h (revision 24530) @@ -1,38 +1,43 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_THREADUTIL #define INCLUDED_THREADUTIL -namespace ThreadUtil +/** + * Light-weight threading utilities. + * Implemented in Threading.cpp. + * Split from Threading because this is included (via profilers) in most files. + */ +namespace Threading { /** * Returns whether the current thread is the 'main' thread * (i.e. matches an earlier call to SetMainThread). */ bool IsMainThread(); /** * Set the current thread as the 'main' thread. * (This is called during engine initialisation.) */ void SetMainThread(); } #endif // INCLUDED_THREADUTIL Index: ps/trunk/source/ps/Threading.cpp =================================================================== --- ps/trunk/source/ps/Threading.cpp (nonexistent) +++ ps/trunk/source/ps/Threading.cpp (revision 24530) @@ -0,0 +1,41 @@ +/* Copyright (C) 2021 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "Threading.h" + +#include + +static bool g_MainThreadSet; +static std::thread::id g_MainThread; + +bool Threading::IsMainThread() +{ + // If SetMainThread hasn't been called yet, this is probably being + // called at static initialisation time, so it must be the main thread + if (!g_MainThreadSet) + return true; + + return g_MainThread == std::this_thread::get_id(); +} + +void Threading::SetMainThread() +{ + g_MainThread = std::this_thread::get_id(); + g_MainThreadSet = true; +} Property changes on: ps/trunk/source/ps/Threading.cpp ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/source/ps/Threading.h =================================================================== --- ps/trunk/source/ps/Threading.h (nonexistent) +++ ps/trunk/source/ps/Threading.h (revision 24530) @@ -0,0 +1,61 @@ +/* Copyright (C) 2021 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_THREADING +#define INCLUDED_THREADING + +#include "ThreadUtil.h" + +#if OS_WIN +#include "lib/sysdep/os/win/wseh.h" +#endif + +namespace Threading +{ + +/** + * Wrap a function to handle exceptions. + * This currently deals with Windows Structured Exception Handling (see wseh.cpp) + */ +template +struct HandleExceptionsBase {}; +template +struct HandleExceptions : public HandleExceptionsBase {}; + +template +struct HandleExceptionsBase +{ + static void Wrapper(Types... args) + { +#if OS_WIN + __try + { + functionPtr(args...); + } + __except(wseh_ExceptionFilter(GetExceptionInformation())) + { + // Nothing particular to do. + } +#else + functionPtr(args...); +#endif + } +}; + +} + +#endif // INCLUDED_THREADING Property changes on: ps/trunk/source/ps/Threading.h ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/source/ps/UserReport.cpp =================================================================== --- ps/trunk/source/ps/UserReport.cpp (revision 24529) +++ ps/trunk/source/ps/UserReport.cpp (revision 24530) @@ -1,638 +1,639 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "UserReport.h" #include "lib/timer.h" #include "lib/utf8.h" #include "lib/external_libraries/curl.h" #include "lib/external_libraries/zlib.h" #include "lib/file/archive/stream.h" #include "lib/os_path.h" #include "lib/sysdep/sysdep.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Profiler2.h" #include "ps/Pyrogenesis.h" +#include "ps/Threading.h" #include #include #include #include #include #include #define DEBUG_UPLOADS 0 /* * The basic idea is that the game submits reports to us, which we send over * HTTP to a server for storage and analysis. * * We can't use libcurl's asynchronous 'multi' API, because DNS resolution can * be synchronous and slow (which would make the game pause). * So we use the 'easy' API in a background thread. * The main thread submits reports, toggles whether uploading is enabled, * and polls for the current status (typically to display in the GUI); * the worker thread does all of the uploading. * * It'd be nice to extend this in the future to handle things like crash reports. * The game should store the crashlogs (suitably anonymised) in a directory, and * we should detect those files and upload them when we're restarted and online. */ /** * Version number stored in config file when the user agrees to the reporting. * Reporting will be disabled if the config value is missing or is less than * this value. If we start reporting a lot more data, we should increase this * value and get the user to re-confirm. */ static const int REPORTER_VERSION = 1; /** * Time interval (seconds) at which the worker thread will check its reconnection * timers. (This should be relatively high so the thread doesn't waste much time * continually waking up.) */ static const double TIMER_CHECK_INTERVAL = 10.0; /** * Seconds we should wait before reconnecting to the server after a failure. */ static const double RECONNECT_INVERVAL = 60.0; CUserReporter g_UserReporter; struct CUserReport { time_t m_Time; std::string m_Type; int m_Version; std::string m_Data; }; class CUserReporterWorker { public: CUserReporterWorker(const std::string& userID, const std::string& url) : m_URL(url), m_UserID(userID), m_Enabled(false), m_Shutdown(false), m_Status("disabled"), m_PauseUntilTime(timer_Time()), m_LastUpdateTime(timer_Time()) { // Set up libcurl: m_Curl = curl_easy_init(); ENSURE(m_Curl); #if DEBUG_UPLOADS curl_easy_setopt(m_Curl, CURLOPT_VERBOSE, 1L); #endif // Capture error messages curl_easy_setopt(m_Curl, CURLOPT_ERRORBUFFER, m_ErrorBuffer); // Disable signal handlers (required for multithreaded applications) curl_easy_setopt(m_Curl, CURLOPT_NOSIGNAL, 1L); // To minimise security risks, don't support redirects curl_easy_setopt(m_Curl, CURLOPT_FOLLOWLOCATION, 0L); // Prevent this thread from blocking the engine shutdown for 5 minutes in case the server is unavailable curl_easy_setopt(m_Curl, CURLOPT_CONNECTTIMEOUT, 10L); // Set IO callbacks curl_easy_setopt(m_Curl, CURLOPT_WRITEFUNCTION, ReceiveCallback); curl_easy_setopt(m_Curl, CURLOPT_WRITEDATA, this); curl_easy_setopt(m_Curl, CURLOPT_READFUNCTION, SendCallback); curl_easy_setopt(m_Curl, CURLOPT_READDATA, this); // Set URL to POST to curl_easy_setopt(m_Curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(m_Curl, CURLOPT_POST, 1L); // Set up HTTP headers m_Headers = NULL; // Set the UA string std::string ua = "User-Agent: 0ad "; ua += curl_version(); ua += " (http://play0ad.com/)"; m_Headers = curl_slist_append(m_Headers, ua.c_str()); // Override the default application/x-www-form-urlencoded type since we're not using that type m_Headers = curl_slist_append(m_Headers, "Content-Type: application/octet-stream"); // Disable the Accept header because it's a waste of a dozen bytes m_Headers = curl_slist_append(m_Headers, "Accept: "); curl_easy_setopt(m_Curl, CURLOPT_HTTPHEADER, m_Headers); - m_WorkerThread = std::thread(RunThread, this); + m_WorkerThread = std::thread(Threading::HandleExceptions::Wrapper, this); } ~CUserReporterWorker() { curl_slist_free_all(m_Headers); curl_easy_cleanup(m_Curl); } /** * Called by main thread, when the online reporting is enabled/disabled. */ void SetEnabled(bool enabled) { std::lock_guard lock(m_WorkerMutex); if (enabled != m_Enabled) { m_Enabled = enabled; // Wake up the worker thread m_WorkerCV.notify_all(); } } /** * Called by main thread to request shutdown. * Returns true if we've shut down successfully. * Returns false if shutdown is taking too long (we might be blocked on a * sync network operation) - you mustn't destroy this object, just leak it * and terminate. */ bool Shutdown() { { std::lock_guard lock(m_WorkerMutex); m_Shutdown = true; } // Wake up the worker thread m_WorkerCV.notify_all(); // Wait for it to shut down cleanly // TODO: should have a timeout in case of network hangs m_WorkerThread.join(); return true; } /** * Called by main thread to determine the current status of the uploader. */ std::string GetStatus() { std::lock_guard lock(m_WorkerMutex); return m_Status; } /** * Called by main thread to add a new report to the queue. */ void Submit(const shared_ptr& report) { { std::lock_guard lock(m_WorkerMutex); m_ReportQueue.push_back(report); } // Wake up the worker thread m_WorkerCV.notify_all(); } /** * Called by the main thread every frame, so we can check * retransmission timers. */ void Update() { double now = timer_Time(); if (now > m_LastUpdateTime + TIMER_CHECK_INTERVAL) { // Wake up the worker thread m_WorkerCV.notify_all(); m_LastUpdateTime = now; } } private: static void RunThread(CUserReporterWorker* data) { debug_SetThreadName("CUserReportWorker"); g_Profiler2.RegisterCurrentThread("userreport"); data->Run(); } void Run() { // Set libcurl's proxy configuration // (This has to be done in the thread because it's potentially very slow) SetStatus("proxy"); std::wstring proxy; { PROFILE2("get proxy config"); if (sys_get_proxy_config(wstring_from_utf8(m_URL), proxy) == INFO::OK) curl_easy_setopt(m_Curl, CURLOPT_PROXY, utf8_from_wstring(proxy).c_str()); } SetStatus("waiting"); /* * We use a condition_variable to let the thread be woken up when it has * work to do. Various actions from the main thread can wake it: * * SetEnabled() * * Shutdown() * * Submit() * * Retransmission timeouts, once every several seconds * * If multiple actions have triggered wakeups, we might respond to * all of those actions after the first wakeup, which is okay (we'll do * nothing during the subsequent wakeups). We should never hang due to * processing fewer actions than wakeups. * * Retransmission timeouts are triggered via the main thread. */ // Wait until the main thread wakes us up while (true) { g_Profiler2.RecordRegionEnter("condition_variable wait"); std::unique_lock lock(m_WorkerMutex); m_WorkerCV.wait(lock); lock.unlock(); g_Profiler2.RecordRegionLeave(); // Handle shutdown requests as soon as possible if (GetShutdown()) return; // If we're not enabled, ignore this wakeup if (!GetEnabled()) continue; // If we're still pausing due to a failed connection, // go back to sleep again if (timer_Time() < m_PauseUntilTime) continue; // We're enabled, so process as many reports as possible while (ProcessReport()) { // Handle shutdowns while we were sending the report if (GetShutdown()) return; } } } bool GetEnabled() { std::lock_guard lock(m_WorkerMutex); return m_Enabled; } bool GetShutdown() { std::lock_guard lock(m_WorkerMutex); return m_Shutdown; } void SetStatus(const std::string& status) { std::lock_guard lock(m_WorkerMutex); m_Status = status; #if DEBUG_UPLOADS debug_printf(">>> CUserReporterWorker status: %s\n", status.c_str()); #endif } bool ProcessReport() { PROFILE2("process report"); shared_ptr report; { std::lock_guard lock(m_WorkerMutex); if (m_ReportQueue.empty()) return false; report = m_ReportQueue.front(); m_ReportQueue.pop_front(); } ConstructRequestData(*report); m_RequestDataOffset = 0; m_ResponseData.clear(); m_ErrorBuffer[0] = '\0'; curl_easy_setopt(m_Curl, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)m_RequestData.size()); SetStatus("connecting"); #if DEBUG_UPLOADS TIMER(L"CUserReporterWorker request"); #endif CURLcode err = curl_easy_perform(m_Curl); #if DEBUG_UPLOADS printf(">>>\n%s\n<<<\n", m_ResponseData.c_str()); #endif if (err == CURLE_OK) { long code = -1; curl_easy_getinfo(m_Curl, CURLINFO_RESPONSE_CODE, &code); SetStatus("completed:" + CStr::FromInt(code)); // Check for success code if (code == 200) return true; // If the server returns the 410 Gone status, interpret that as meaning // it no longer supports uploads (at least from this version of the game), // so shut down and stop talking to it (to avoid wasting bandwidth) if (code == 410) { std::lock_guard lock(m_WorkerMutex); m_Shutdown = true; return false; } } else { std::string errorString(m_ErrorBuffer); if (errorString.empty()) errorString = curl_easy_strerror(err); SetStatus("failed:" + CStr::FromInt(err) + ":" + errorString); } // We got an unhandled return code or a connection failure; // push this report back onto the queue and try again after // a long interval { std::lock_guard lock(m_WorkerMutex); m_ReportQueue.push_front(report); } m_PauseUntilTime = timer_Time() + RECONNECT_INVERVAL; return false; } void ConstructRequestData(const CUserReport& report) { // Construct the POST request data in the application/x-www-form-urlencoded format std::string r; r += "user_id="; AppendEscaped(r, m_UserID); r += "&time=" + CStr::FromInt64(report.m_Time); r += "&type="; AppendEscaped(r, report.m_Type); r += "&version=" + CStr::FromInt(report.m_Version); r += "&data="; AppendEscaped(r, report.m_Data); // Compress the content with zlib to save bandwidth. // (Note that we send a request with unlabelled compressed data instead // of using Content-Encoding, because Content-Encoding is a mess and causes // problems with servers and breaks Content-Length and this is much easier.) std::string compressed; compressed.resize(compressBound(r.size())); uLongf destLen = compressed.size(); int ok = compress((Bytef*)compressed.c_str(), &destLen, (const Bytef*)r.c_str(), r.size()); ENSURE(ok == Z_OK); compressed.resize(destLen); m_RequestData.swap(compressed); } void AppendEscaped(std::string& buffer, const std::string& str) { char* escaped = curl_easy_escape(m_Curl, str.c_str(), str.size()); buffer += escaped; curl_free(escaped); } static size_t ReceiveCallback(void* buffer, size_t size, size_t nmemb, void* userp) { CUserReporterWorker* self = static_cast(userp); if (self->GetShutdown()) return 0; // signals an error self->m_ResponseData += std::string((char*)buffer, (char*)buffer+size*nmemb); return size*nmemb; } static size_t SendCallback(char* bufptr, size_t size, size_t nmemb, void* userp) { CUserReporterWorker* self = static_cast(userp); if (self->GetShutdown()) return CURL_READFUNC_ABORT; // signals an error // We can return as much data as available, up to the buffer size size_t amount = std::min(self->m_RequestData.size() - self->m_RequestDataOffset, size*nmemb); // ...But restrict to sending a small amount at once, so that we remain // responsive to shutdown requests even if the network is pretty slow amount = std::min((size_t)1024, amount); if(amount != 0) // (avoids invalid operator[] call where index=size) { memcpy(bufptr, &self->m_RequestData[self->m_RequestDataOffset], amount); self->m_RequestDataOffset += amount; } self->SetStatus("sending:" + CStr::FromDouble((double)self->m_RequestDataOffset / self->m_RequestData.size())); return amount; } private: // Thread-related members: std::thread m_WorkerThread; std::mutex m_WorkerMutex; std::condition_variable m_WorkerCV; // Shared by main thread and worker thread: // These variables are all protected by m_WorkerMutex std::deque > m_ReportQueue; bool m_Enabled; bool m_Shutdown; std::string m_Status; // Initialised in constructor by main thread; otherwise used only by worker thread: std::string m_URL; std::string m_UserID; CURL* m_Curl; curl_slist* m_Headers; double m_PauseUntilTime; // Only used by worker thread: std::string m_ResponseData; std::string m_RequestData; size_t m_RequestDataOffset; char m_ErrorBuffer[CURL_ERROR_SIZE]; // Only used by main thread: double m_LastUpdateTime; }; CUserReporter::CUserReporter() : m_Worker(NULL) { } CUserReporter::~CUserReporter() { ENSURE(!m_Worker); // Deinitialize should have been called before shutdown } std::string CUserReporter::LoadUserID() { std::string userID; // Read the user ID from user.cfg (if there is one) CFG_GET_VAL("userreport.id", userID); // If we don't have a validly-formatted user ID, generate a new one if (userID.length() != 16) { u8 bytes[8] = {0}; sys_generate_random_bytes(bytes, ARRAY_SIZE(bytes)); // ignore failures - there's not much we can do about it userID = ""; for (size_t i = 0; i < ARRAY_SIZE(bytes); ++i) { char hex[3]; sprintf_s(hex, ARRAY_SIZE(hex), "%02x", (unsigned int)bytes[i]); userID += hex; } g_ConfigDB.SetValueString(CFG_USER, "userreport.id", userID); g_ConfigDB.WriteValueToFile(CFG_USER, "userreport.id", userID); } return userID; } bool CUserReporter::IsReportingEnabled() { int version = -1; CFG_GET_VAL("userreport.enabledversion", version); return (version >= REPORTER_VERSION); } void CUserReporter::SetReportingEnabled(bool enabled) { CStr val = CStr::FromInt(enabled ? REPORTER_VERSION : 0); g_ConfigDB.SetValueString(CFG_USER, "userreport.enabledversion", val); g_ConfigDB.WriteValueToFile(CFG_USER, "userreport.enabledversion", val); if (m_Worker) m_Worker->SetEnabled(enabled); } std::string CUserReporter::GetStatus() { if (!m_Worker) return "disabled"; return m_Worker->GetStatus(); } void CUserReporter::Initialize() { ENSURE(!m_Worker); // must only be called once std::string userID = LoadUserID(); std::string url; CFG_GET_VAL("userreport.url_upload", url); m_Worker = new CUserReporterWorker(userID, url); m_Worker->SetEnabled(IsReportingEnabled()); } void CUserReporter::Deinitialize() { if (!m_Worker) return; if (m_Worker->Shutdown()) { // Worker was shut down cleanly SAFE_DELETE(m_Worker); } else { // Worker failed to shut down in a reasonable time // Leak the resources (since that's better than hanging or crashing) m_Worker = NULL; } } void CUserReporter::Update() { if (m_Worker) m_Worker->Update(); } void CUserReporter::SubmitReport(const std::string& type, int version, const std::string& data, const std::string& dataHumanReadable) { // Write to logfile, enabling users to assess privacy concerns before the data is submitted if (!dataHumanReadable.empty()) { OsPath path = psLogDir() / OsPath("userreport_" + type + ".txt"); std::ofstream stream(OsString(path), std::ofstream::trunc); if (stream) { debug_printf("UserReport written to %s\n", path.string8().c_str()); stream << dataHumanReadable << std::endl; stream.close(); } else debug_printf("Failed to write UserReport to %s\n", path.string8().c_str()); } // If not initialised, discard the report if (!m_Worker) return; // Actual submit shared_ptr report(new CUserReport); report->m_Time = time(NULL); report->m_Type = type; report->m_Version = version; report->m_Data = data; m_Worker->Submit(report); } Index: ps/trunk/source/ps/VideoMode.cpp =================================================================== --- ps/trunk/source/ps/VideoMode.cpp (revision 24529) +++ ps/trunk/source/ps/VideoMode.cpp (revision 24530) @@ -1,509 +1,510 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "VideoMode.h" #include "graphics/Camera.h" #include "graphics/GameView.h" #include "gui/GUIManager.h" #include "lib/config2.h" #include "lib/ogl.h" #include "lib/external_libraries/libsdl.h" #include "lib/sysdep/gfx.h" #include "lib/tex/tex.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" +#include "ps/Pyrogenesis.h" #include "ps/GameSetup/Config.h" #include "renderer/Renderer.h" #if OS_MACOSX # include "lib/sysdep/os/osx/osx_sys_version.h" #endif static int DEFAULT_WINDOW_W = 1024; static int DEFAULT_WINDOW_H = 768; static int DEFAULT_FULLSCREEN_W = 1024; static int DEFAULT_FULLSCREEN_H = 768; CVideoMode g_VideoMode; CVideoMode::CVideoMode() : m_IsFullscreen(false), m_IsInitialised(false), m_Window(NULL), m_PreferredW(0), m_PreferredH(0), m_PreferredBPP(0), m_PreferredFreq(0), m_ConfigW(0), m_ConfigH(0), m_ConfigBPP(0), m_ConfigFullscreen(false), m_WindowedW(DEFAULT_WINDOW_W), m_WindowedH(DEFAULT_WINDOW_H), m_WindowedX(0), m_WindowedY(0) { // (m_ConfigFullscreen defaults to false, so users don't get stuck if // e.g. half the filesystem is missing and the config files aren't loaded) } void CVideoMode::ReadConfig() { bool windowed = !m_ConfigFullscreen; CFG_GET_VAL("windowed", windowed); m_ConfigFullscreen = !windowed; CFG_GET_VAL("xres", m_ConfigW); CFG_GET_VAL("yres", m_ConfigH); CFG_GET_VAL("bpp", m_ConfigBPP); CFG_GET_VAL("display", m_ConfigDisplay); } bool CVideoMode::SetVideoMode(int w, int h, int bpp, bool fullscreen) { Uint32 flags = 0; if (fullscreen) flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; if (!m_Window) { // Note: these flags only take affect in SDL_CreateWindow flags |= SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE; m_WindowedX = m_WindowedY = SDL_WINDOWPOS_CENTERED_DISPLAY(m_ConfigDisplay); - m_Window = SDL_CreateWindow("0 A.D.", m_WindowedX, m_WindowedY, w, h, flags); + m_Window = SDL_CreateWindow(main_window_name, m_WindowedX, m_WindowedY, w, h, flags); if (!m_Window) { // If fullscreen fails, try windowed mode if (fullscreen) { LOGWARNING("Failed to set the video mode to fullscreen for the chosen resolution " "%dx%d:%d (\"%hs\"), falling back to windowed mode", w, h, bpp, SDL_GetError()); // Using default size for the window for now, as the attempted setting // could be as large, or larger than the screen size. return SetVideoMode(DEFAULT_WINDOW_W, DEFAULT_WINDOW_H, bpp, false); } else { LOGERROR("SetVideoMode failed in SDL_CreateWindow: %dx%d:%d %d (\"%s\")", w, h, bpp, fullscreen ? 1 : 0, SDL_GetError()); return false; } } if (SDL_SetWindowDisplayMode(m_Window, NULL) < 0) { LOGERROR("SetVideoMode failed in SDL_SetWindowDisplayMode: %dx%d:%d %d (\"%s\")", w, h, bpp, fullscreen ? 1 : 0, SDL_GetError()); return false; } SDL_GLContext context = SDL_GL_CreateContext(m_Window); if (!context) { LOGERROR("SetVideoMode failed in SDL_GL_CreateContext: %dx%d:%d %d (\"%s\")", w, h, bpp, fullscreen ? 1 : 0, SDL_GetError()); return false; } } else { if (m_IsFullscreen != fullscreen) { if (!fullscreen) { // For some reason, when switching from fullscreen to windowed mode, // we have to set the window size and position before and after switching SDL_SetWindowSize(m_Window, w, h); SDL_SetWindowPosition(m_Window, m_WindowedX, m_WindowedY); } if (SDL_SetWindowFullscreen(m_Window, flags) < 0) { LOGERROR("SetVideoMode failed in SDL_SetWindowFullscreen: %dx%d:%d %d (\"%s\")", w, h, bpp, fullscreen ? 1 : 0, SDL_GetError()); return false; } } if (!fullscreen) { SDL_SetWindowSize(m_Window, w, h); SDL_SetWindowPosition(m_Window, m_WindowedX, m_WindowedY); } } // Grab the current video settings SDL_GetWindowSize(m_Window, &m_CurrentW, &m_CurrentH); m_CurrentBPP = bpp; if (fullscreen) SDL_SetWindowGrab(m_Window, SDL_TRUE); else SDL_SetWindowGrab(m_Window, SDL_FALSE); m_IsFullscreen = fullscreen; g_xres = m_CurrentW; g_yres = m_CurrentH; return true; } bool CVideoMode::InitSDL() { ENSURE(!m_IsInitialised); ReadConfig(); // preferred video mode = current desktop settings // (command line params may override these) // TODO: handle multi-screen and HiDPI properly. SDL_DisplayMode mode; if (SDL_GetDesktopDisplayMode(0, &mode) == 0) { m_PreferredW = mode.w; m_PreferredH = mode.h; m_PreferredBPP = SDL_BITSPERPIXEL(mode.format); m_PreferredFreq = mode.refresh_rate; } int w = m_ConfigW; int h = m_ConfigH; if (m_ConfigFullscreen) { // If fullscreen and no explicit size set, default to the desktop resolution if (w == 0 || h == 0) { w = m_PreferredW; h = m_PreferredH; } } // If no size determined, default to something sensible if (w == 0 || h == 0) { w = DEFAULT_WINDOW_W; h = DEFAULT_WINDOW_H; } if (!m_ConfigFullscreen) { // Limit the window to the screen size (if known) if (m_PreferredW) w = std::min(w, m_PreferredW); if (m_PreferredH) h = std::min(h, m_PreferredH); } int bpp = GetBestBPP(); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); #if CONFIG2_GLES // Require GLES 2.0 SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); #endif if (!SetVideoMode(w, h, bpp, m_ConfigFullscreen)) { // Fall back to a smaller depth buffer // (The rendering may be ugly but this helps when running in VMware) SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16); if (!SetVideoMode(w, h, bpp, m_ConfigFullscreen)) return false; } SDL_GL_SetSwapInterval(g_VSync ? 1 : 0); // Work around a bug in the proprietary Linux ATI driver (at least versions 8.16.20 and 8.14.13). // The driver appears to register its own atexit hook on context creation. // If this atexit hook is called before SDL_Quit destroys the OpenGL context, // some kind of double-free problem causes a crash and lockup in the driver. // Calling SDL_Quit twice appears to be harmless, though, and avoids the problem // by destroying the context *before* the driver's atexit hook is called. // (Note that atexit hooks are guaranteed to be called in reverse order of their registration.) atexit(SDL_Quit); // End work around. ogl_Init(); // required after each mode change // (TODO: does that mean we need to call this when toggling fullscreen later?) m_IsInitialised = true; if (!m_ConfigFullscreen) { m_WindowedW = w; m_WindowedH = h; } SetWindowIcon(); return true; } bool CVideoMode::InitNonSDL() { ENSURE(!m_IsInitialised); ReadConfig(); m_IsInitialised = true; return true; } void CVideoMode::Shutdown() { ENSURE(m_IsInitialised); m_IsFullscreen = false; m_IsInitialised = false; if (m_Window) { SDL_DestroyWindow(m_Window); m_Window = NULL; } } bool CVideoMode::ResizeWindow(int w, int h) { ENSURE(m_IsInitialised); // Ignore if not windowed if (m_IsFullscreen) return true; // Ignore if the size hasn't changed if (w == m_WindowedW && h == m_WindowedH) return true; int bpp = GetBestBPP(); if (!SetVideoMode(w, h, bpp, false)) return false; m_WindowedW = w; m_WindowedH = h; UpdateRenderer(w, h); return true; } bool CVideoMode::SetFullscreen(bool fullscreen) { // This might get called before initialisation by psDisplayError; // if so then silently fail if (!m_IsInitialised) return false; // Check whether this is actually a change if (fullscreen == m_IsFullscreen) return true; if (!m_IsFullscreen) { // Windowed -> fullscreen: int w = 0, h = 0; // If a fullscreen size was configured, use that; else use the desktop size; else use a default if (m_ConfigFullscreen) { w = m_ConfigW; h = m_ConfigH; } if (w == 0 || h == 0) { w = m_PreferredW; h = m_PreferredH; } if (w == 0 || h == 0) { w = DEFAULT_FULLSCREEN_W; h = DEFAULT_FULLSCREEN_H; } int bpp = GetBestBPP(); if (!SetVideoMode(w, h, bpp, fullscreen)) return false; UpdateRenderer(m_CurrentW, m_CurrentH); return true; } else { // Fullscreen -> windowed: // Go back to whatever the previous window size was int w = m_WindowedW, h = m_WindowedH; int bpp = GetBestBPP(); if (!SetVideoMode(w, h, bpp, fullscreen)) return false; UpdateRenderer(w, h); return true; } } bool CVideoMode::ToggleFullscreen() { return SetFullscreen(!m_IsFullscreen); } bool CVideoMode::IsInFullscreen() const { return m_IsFullscreen; } void CVideoMode::UpdatePosition(int x, int y) { if (!m_IsFullscreen) { m_WindowedX = x; m_WindowedY = y; } } void CVideoMode::UpdateRenderer(int w, int h) { if (w < 2) w = 2; // avoid GL errors caused by invalid sizes if (h < 2) h = 2; g_xres = w; g_yres = h; SViewPort vp = { 0, 0, w, h }; if (CRenderer::IsInitialised()) { g_Renderer.SetViewport(vp); g_Renderer.Resize(w, h); } if (g_GUI) g_GUI->UpdateResolution(); if (g_Console) g_Console->UpdateScreenSize(w, h); if (g_Game) g_Game->GetView()->SetViewport(vp); } int CVideoMode::GetBestBPP() { if (m_ConfigBPP) return m_ConfigBPP; if (m_PreferredBPP) return m_PreferredBPP; return 32; } int CVideoMode::GetXRes() { ENSURE(m_IsInitialised); return m_CurrentW; } int CVideoMode::GetYRes() { ENSURE(m_IsInitialised); return m_CurrentH; } int CVideoMode::GetBPP() { ENSURE(m_IsInitialised); return m_CurrentBPP; } int CVideoMode::GetDesktopXRes() { ENSURE(m_IsInitialised); return m_PreferredW; } int CVideoMode::GetDesktopYRes() { ENSURE(m_IsInitialised); return m_PreferredH; } int CVideoMode::GetDesktopBPP() { ENSURE(m_IsInitialised); return m_PreferredBPP; } int CVideoMode::GetDesktopFreq() { ENSURE(m_IsInitialised); return m_PreferredFreq; } SDL_Window* CVideoMode::GetWindow() { ENSURE(m_IsInitialised); return m_Window; } void CVideoMode::SetWindowIcon() { // The window icon should be kept outside of art/textures/, or else it will be converted // to DDS by the archive builder and will become unusable here. Using DDS makes BGRA // conversion needlessly complicated. std::shared_ptr iconFile; size_t iconFileSize; if (g_VFS->LoadFile("art/icons/window.png", iconFile, iconFileSize) != INFO::OK) { LOGWARNING("Window icon not found."); return; } Tex iconTexture; if (iconTexture.decode(iconFile, iconFileSize) != INFO::OK) return; // Convert to required BGRA format. const size_t iconFlags = (iconTexture.m_Flags | TEX_BGR) & ~TEX_DXT; if (iconTexture.transform_to(iconFlags) != INFO::OK) return; void* bgra_img = iconTexture.get_data(); if (!bgra_img) return; SDL_Surface *iconSurface = SDL_CreateRGBSurfaceFrom(bgra_img, iconTexture.m_Width, iconTexture.m_Height, 32, iconTexture.m_Width * 4, 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000); if (!iconSurface) return; SDL_SetWindowIcon(m_Window, iconSurface); SDL_FreeSurface(iconSurface); } Index: ps/trunk/source/scriptinterface/ScriptContext.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptContext.cpp (revision 24529) +++ ps/trunk/source/scriptinterface/ScriptContext.cpp (revision 24530) @@ -1,255 +1,255 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ScriptContext.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "scriptinterface/ScriptExtraHeaders.h" #include "scriptinterface/ScriptEngine.h" #include "scriptinterface/ScriptInterface.h" void GCSliceCallbackHook(JSContext* UNUSED(cx), JS::GCProgress progress, const JS::GCDescription& UNUSED(desc)) { /** * From the GCAPI.h file: * > During GC, the GC is bracketed by GC_CYCLE_BEGIN/END callbacks. Each * > slice between those (whether an incremental or the sole non-incremental * > slice) is bracketed by GC_SLICE_BEGIN/GC_SLICE_END. * Thus, to safely monitor GCs, we need to profile SLICE_X calls. */ if (progress == JS::GC_SLICE_BEGIN) { - if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) + if (CProfileManager::IsInitialised() && Threading::IsMainThread()) g_Profiler.Start("GCSlice"); g_Profiler2.RecordRegionEnter("GCSlice"); } else if (progress == JS::GC_SLICE_END) { - if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) + if (CProfileManager::IsInitialised() && Threading::IsMainThread()) g_Profiler.Stop(); g_Profiler2.RecordRegionLeave(); } // The following code can be used to print some information aobut garbage collection // Search for "Nonincremental reason" if there are problems running GC incrementally. #if 0 if (progress == JS::GCProgress::GC_CYCLE_BEGIN) printf("starting cycle ===========================================\n"); const char16_t* str = desc.formatMessage(cx); int len = 0; for(int i = 0; i < 10000; i++) { len++; if(!str[i]) break; } wchar_t outstring[len]; for(int i = 0; i < len; i++) { outstring[i] = (wchar_t)str[i]; } printf("---------------------------------------\n: %ls \n---------------------------------------\n", outstring); #endif } shared_ptr ScriptContext::CreateContext(int contextSize, int heapGrowthBytesGCTrigger) { return shared_ptr(new ScriptContext(contextSize, heapGrowthBytesGCTrigger)); } ScriptContext::ScriptContext(int contextSize, int heapGrowthBytesGCTrigger): m_LastGCBytes(0), m_LastGCCheck(0.0f), m_HeapGrowthBytesGCTrigger(heapGrowthBytesGCTrigger), m_ContextSize(contextSize) { ENSURE(ScriptEngine::IsInitialised() && "The ScriptEngine must be initialized before constructing any ScriptContexts!"); m_cx = JS_NewContext(contextSize); ENSURE(m_cx); // TODO: error handling ENSURE(JS::InitSelfHostedCode(m_cx)); JS::SetGCSliceCallback(m_cx, GCSliceCallbackHook); JS_SetGCParameter(m_cx, JSGC_MAX_BYTES, m_ContextSize); JS_SetGCParameter(m_cx, JSGC_MODE, JSGC_MODE_INCREMENTAL); JS_SetOffthreadIonCompilationEnabled(m_cx, true); // For GC debugging: // JS_SetGCZeal(m_cx, 2, JS_DEFAULT_ZEAL_FREQ); JS_SetContextPrivate(m_cx, nullptr); JS_SetGlobalJitCompilerOption(m_cx, JSJITCOMPILER_ION_ENABLE, 1); JS_SetGlobalJitCompilerOption(m_cx, JSJITCOMPILER_BASELINE_ENABLE, 1); JS::ContextOptionsRef(m_cx).setStrictMode(true); ScriptEngine::GetSingleton().RegisterContext(m_cx); } ScriptContext::~ScriptContext() { ENSURE(ScriptEngine::IsInitialised() && "The ScriptEngine must be active (initialized and not yet shut down) when destroying a ScriptContext!"); JS_DestroyContext(m_cx); ScriptEngine::GetSingleton().UnRegisterContext(m_cx); } void ScriptContext::RegisterRealm(JS::Realm* realm) { ENSURE(realm); m_Realms.push_back(realm); } void ScriptContext::UnRegisterRealm(JS::Realm* realm) { // Schedule the zone for GC, which will destroy the realm. if (JS::IsIncrementalGCInProgress(m_cx)) JS::FinishIncrementalGC(m_cx, JS::GCReason::API); JS::PrepareZoneForGC(js::GetRealmZone(realm)); m_Realms.remove(realm); } #define GC_DEBUG_PRINT 0 void ScriptContext::MaybeIncrementalGC(double delay) { PROFILE2("MaybeIncrementalGC"); if (JS::IsIncrementalGCEnabled(m_cx)) { // The idea is to get the heap size after a completed GC and trigger the next GC when the heap size has // reached m_LastGCBytes + X. // In practice it doesn't quite work like that. When the incremental marking is completed, the sweeping kicks in. // The sweeping actually frees memory and it does this in a background thread (if JS_USE_HELPER_THREADS is set). // While the sweeping is happening we already run scripts again and produce new garbage. const int GCSliceTimeBudget = 30; // Milliseconds an incremental slice is allowed to run // Have a minimum time in seconds to wait between GC slices and before starting a new GC to distribute the GC // load and to hopefully make it unnoticeable for the player. This value should be high enough to distribute // the load well enough and low enough to make sure we don't run out of memory before we can start with the // sweeping. if (timer_Time() - m_LastGCCheck < delay) return; m_LastGCCheck = timer_Time(); int gcBytes = JS_GetGCParameter(m_cx, JSGC_BYTES); #if GC_DEBUG_PRINT std::cout << "gcBytes: " << gcBytes / 1024 << " KB" << std::endl; #endif if (m_LastGCBytes > gcBytes || m_LastGCBytes == 0) { #if GC_DEBUG_PRINT printf("Setting m_LastGCBytes: %d KB \n", gcBytes / 1024); #endif m_LastGCBytes = gcBytes; } // Run an additional incremental GC slice if the currently running incremental GC isn't over yet // ... or // start a new incremental GC if the JS heap size has grown enough for a GC to make sense if (JS::IsIncrementalGCInProgress(m_cx) || (gcBytes - m_LastGCBytes > m_HeapGrowthBytesGCTrigger)) { #if GC_DEBUG_PRINT if (JS::IsIncrementalGCInProgress(m_cx)) printf("An incremental GC cycle is in progress. \n"); else printf("GC needed because JSGC_BYTES - m_LastGCBytes > m_HeapGrowthBytesGCTrigger \n" " JSGC_BYTES: %d KB \n m_LastGCBytes: %d KB \n m_HeapGrowthBytesGCTrigger: %d KB \n", gcBytes / 1024, m_LastGCBytes / 1024, m_HeapGrowthBytesGCTrigger / 1024); #endif // A hack to make sure we never exceed the context size because we can't collect the memory // fast enough. if (gcBytes > m_ContextSize / 2) { if (JS::IsIncrementalGCInProgress(m_cx)) { #if GC_DEBUG_PRINT printf("Finishing incremental GC because gcBytes > m_ContextSize / 2. \n"); #endif PrepareZonesForIncrementalGC(); JS::FinishIncrementalGC(m_cx, JS::GCReason::API); } else { if (gcBytes > m_ContextSize * 0.75) { ShrinkingGC(); #if GC_DEBUG_PRINT printf("Running shrinking GC because gcBytes > m_ContextSize * 0.75. \n"); #endif } else { #if GC_DEBUG_PRINT printf("Running full GC because gcBytes > m_ContextSize / 2. \n"); #endif JS_GC(m_cx); } } } else { #if GC_DEBUG_PRINT if (!JS::IsIncrementalGCInProgress(m_cx)) printf("Starting incremental GC \n"); else printf("Running incremental GC slice \n"); #endif PrepareZonesForIncrementalGC(); if (!JS::IsIncrementalGCInProgress(m_cx)) JS::StartIncrementalGC(m_cx, GC_NORMAL, JS::GCReason::API, GCSliceTimeBudget); else JS::IncrementalGCSlice(m_cx, JS::GCReason::API, GCSliceTimeBudget); } m_LastGCBytes = gcBytes; } } } void ScriptContext::ShrinkingGC() { JS_SetGCParameter(m_cx, JSGC_MODE, JSGC_MODE_ZONE); JS::PrepareForFullGC(m_cx); JS::NonIncrementalGC(m_cx, GC_SHRINK, JS::GCReason::API); JS_SetGCParameter(m_cx, JSGC_MODE, JSGC_MODE_INCREMENTAL); } void ScriptContext::PrepareZonesForIncrementalGC() const { for (JS::Realm* const& realm : m_Realms) JS::PrepareZoneForGC(js::GetRealmZone(realm)); } Index: ps/trunk/source/scriptinterface/ScriptInterface.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 24529) +++ ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 24530) @@ -1,1056 +1,1056 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ScriptContext.h" #include "ScriptExtraHeaders.h" #include "ScriptInterface.h" #include "ScriptStats.h" #include "lib/debug.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/utf16string.h" #include #include #define BOOST_MULTI_INDEX_DISABLE_SERIALIZATION #include #include #include #include #include #include #include #include "valgrind.h" /** * @file * Abstractions of various SpiderMonkey features. * Engine code should be using functions of these interfaces rather than * directly accessing the underlying JS api. */ struct ScriptInterface_impl { ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& context); ~ScriptInterface_impl(); void Register(const char* name, JSNative fptr, uint nargs) const; // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the context destructor. shared_ptr m_context; friend ScriptRequest; private: JSContext* m_cx; JS::PersistentRootedObject m_glob; // global scope object public: boost::rand48* m_rng; JS::PersistentRootedObject m_nativeScope; // native function scope object }; ScriptRequest::ScriptRequest(const ScriptInterface& scriptInterface) : cx(scriptInterface.m->m_cx) { m_formerRealm = JS::EnterRealm(cx, scriptInterface.m->m_glob); glob = JS::CurrentGlobalOrNull(cx); } JS::Value ScriptRequest::globalValue() const { return JS::ObjectValue(*glob); } ScriptRequest::~ScriptRequest() { JS::LeaveRealm(cx, m_formerRealm); } namespace { JSClassOps global_classops = { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, JS_GlobalObjectTraceHook }; JSClass global_class = { "global", JSCLASS_GLOBAL_FLAGS, &global_classops }; // Functions in the global namespace: bool print(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ for (uint i = 0; i < args.length(); ++i) { std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[i], str)) return false; debug_printf("%s", utf8_from_wstring(str).c_str()); } fflush(stdout); args.rval().setUndefined(); return true; } bool logmsg(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; LOGMESSAGE("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool warn(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; LOGWARNING("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool error(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; LOGERROR("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool deepcopy(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ JS::RootedValue ret(cx); if (!JS_StructuredClone(rq.cx, args[0], &ret, NULL, NULL)) return false; args.rval().set(ret); return true; } bool deepfreeze(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ if (args.length() != 1 || !args.get(0).isObject()) { ScriptException::Raise(rq, "deepfreeze requires exactly one object as an argument."); return false; } ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface->FreezeObject(args.get(0), true); args.rval().set(args.get(0)); return true; } bool ProfileStart(JSContext* cx, uint argc, JS::Value* vp) { const char* name = "(ProfileStart)"; JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ if (args.length() >= 1) { std::string str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; name = StringFlyweight(str).get().c_str(); } - if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) + if (CProfileManager::IsInitialised() && Threading::IsMainThread()) g_Profiler.StartScript(name); g_Profiler2.RecordRegionEnter(name); args.rval().setUndefined(); return true; } bool ProfileStop(JSContext* UNUSED(cx), uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) + if (CProfileManager::IsInitialised() && Threading::IsMainThread()) g_Profiler.Stop(); g_Profiler2.RecordRegionLeave(); args.rval().setUndefined(); return true; } bool ProfileAttribute(JSContext* cx, uint argc, JS::Value* vp) { const char* name = "(ProfileAttribute)"; JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ if (args.length() >= 1) { std::string str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; name = StringFlyweight(str).get().c_str(); } g_Profiler2.RecordAttribute("%s", name); args.rval().setUndefined(); return true; } // Math override functions: // boost::uniform_real is apparently buggy in Boost pre-1.47 - for integer generators // it returns [min,max], not [min,max). The bug was fixed in 1.47. // We need consistent behaviour, so manually implement the correct version: static double generate_uniform_real(boost::rand48& rng, double min, double max) { while (true) { double n = (double)(rng() - rng.min()); double d = (double)(rng.max() - rng.min()) + 1.0; ENSURE(d > 0 && n >= 0 && n <= d); double r = n / d * (max - min) + min; if (r < max) return r; } } bool Math_random(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); double r; if (!ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface->MathRandom(r)) return false; args.rval().setNumber(r); return true; } } // anonymous namespace bool ScriptInterface::MathRandom(double& nbr) { if (m->m_rng == NULL) return false; nbr = generate_uniform_real(*(m->m_rng), 0.0, 1.0); return true; } ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& context) : m_context(context), m_cx(context->GetGeneralJSContext()), m_glob(context->GetGeneralJSContext()), m_nativeScope(context->GetGeneralJSContext()) { JS::RealmCreationOptions creationOpt; // Keep JIT code during non-shrinking GCs. This brings a quite big performance improvement. creationOpt.setPreserveJitCode(true); // Enable uneval creationOpt.setToSourceEnabled(true); JS::RealmOptions opt(creationOpt, JS::RealmBehaviors{}); m_glob = JS_NewGlobalObject(m_cx, &global_class, nullptr, JS::OnNewGlobalHookOption::FireOnNewGlobalHook, opt); JSAutoRealm autoRealm(m_cx, m_glob); ENSURE(JS::InitRealmStandardClasses(m_cx)); JS_DefineProperty(m_cx, m_glob, "global", m_glob, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); m_nativeScope = JS_DefineObject(m_cx, m_glob, nativeScopeName, nullptr, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "print", ::print, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "log", ::logmsg, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "warn", ::warn, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "error", ::error, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "clone", ::deepcopy, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "deepfreeze", ::deepfreeze, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); Register("ProfileStart", ::ProfileStart, 1); Register("ProfileStop", ::ProfileStop, 0); Register("ProfileAttribute", ::ProfileAttribute, 1); m_context->RegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } ScriptInterface_impl::~ScriptInterface_impl() { m_context->UnRegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } void ScriptInterface_impl::Register(const char* name, JSNative fptr, uint nargs) const { JSAutoRealm autoRealm(m_cx, m_glob); JS::RootedObject nativeScope(m_cx, m_nativeScope); JS::RootedFunction func(m_cx, JS_DefineFunction(m_cx, nativeScope, name, fptr, nargs, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)); } ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const shared_ptr& context) : m(new ScriptInterface_impl(nativeScopeName, context)) { // Profiler stats table isn't thread-safe, so only enable this on the main thread - if (ThreadUtil::IsMainThread()) + if (Threading::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Add(this, debugName); } ScriptRequest rq(this); m_CmptPrivate.pScriptInterface = this; JS_SetCompartmentPrivate(js::GetObjectCompartment(rq.glob), (void*)&m_CmptPrivate); } ScriptInterface::~ScriptInterface() { - if (ThreadUtil::IsMainThread()) + if (Threading::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Remove(this); } } void ScriptInterface::SetCallbackData(void* pCBData) { m_CmptPrivate.pCBData = pCBData; } ScriptInterface::CmptPrivate* ScriptInterface::GetScriptInterfaceAndCBData(JSContext* cx) { CmptPrivate* pCmptPrivate = (CmptPrivate*)JS_GetCompartmentPrivate(js::GetContextCompartment(cx)); return pCmptPrivate; } bool ScriptInterface::LoadGlobalScripts() { // Ignore this failure in tests if (!g_VFS) return false; // Load and execute *.js in the global scripts directory VfsPaths pathnames; vfs::GetPathnames(g_VFS, L"globalscripts/", L"*.js", pathnames); for (const VfsPath& path : pathnames) if (!LoadGlobalScriptFile(path)) { LOGERROR("LoadGlobalScripts: Failed to load script %s", path.string8()); return false; } return true; } bool ScriptInterface::ReplaceNondeterministicRNG(boost::rand48& rng) { ScriptRequest rq(this); JS::RootedValue math(rq.cx); JS::RootedObject global(rq.cx, rq.glob); if (JS_GetProperty(rq.cx, global, "Math", &math) && math.isObject()) { JS::RootedObject mathObj(rq.cx, &math.toObject()); JS::RootedFunction random(rq.cx, JS_DefineFunction(rq.cx, mathObj, "random", Math_random, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)); if (random) { m->m_rng = &rng; return true; } } ScriptException::CatchPending(rq); LOGERROR("ReplaceNondeterministicRNG: failed to replace Math.random"); return false; } void ScriptInterface::Register(const char* name, JSNative fptr, size_t nargs) const { m->Register(name, fptr, (uint)nargs); } JSContext* ScriptInterface::GetGeneralJSContext() const { return m->m_context->GetGeneralJSContext(); } shared_ptr ScriptInterface::GetContext() const { return m->m_context; } void ScriptInterface::CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const { ScriptRequest rq(this); if (!ctor.isObject()) { LOGERROR("CallConstructor: ctor is not an object"); out.setNull(); return; } JS::RootedObject ctorObj(rq.cx, &ctor.toObject()); out.setObjectOrNull(JS_New(rq.cx, ctorObj, argv)); } void ScriptInterface::DefineCustomObjectType(JSClass *clasp, JSNative constructor, uint minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs) { ScriptRequest rq(this); std::string typeName = clasp->name; if (m_CustomObjectTypes.find(typeName) != m_CustomObjectTypes.end()) { // This type already exists throw PSERROR_Scripting_DefineType_AlreadyExists(); } JS::RootedObject global(rq.cx, rq.glob); JS::RootedObject obj(rq.cx, JS_InitClass(rq.cx, global, nullptr, clasp, constructor, minArgs, // Constructor, min args ps, fs, // Properties, methods static_ps, static_fs)); // Constructor properties, methods if (obj == nullptr) { ScriptException::CatchPending(rq); throw PSERROR_Scripting_DefineType_CreationFailed(); } CustomType& type = m_CustomObjectTypes[typeName]; type.m_Prototype.init(rq.cx, obj); type.m_Class = clasp; type.m_Constructor = constructor; } JSObject* ScriptInterface::CreateCustomObject(const std::string& typeName) const { std::map::const_iterator it = m_CustomObjectTypes.find(typeName); if (it == m_CustomObjectTypes.end()) throw PSERROR_Scripting_TypeDoesNotExist(); ScriptRequest rq(this); JS::RootedObject prototype(rq.cx, it->second.m_Prototype.get()); return JS_NewObjectWithGivenProto(rq.cx, it->second.m_Class, prototype); } bool ScriptInterface::CallFunction_(JS::HandleValue val, const char* name, JS::HandleValueArray argv, JS::MutableHandleValue ret) const { ScriptRequest rq(this); JS::RootedObject obj(rq.cx); if (!JS_ValueToObject(rq.cx, val, &obj) || !obj) return false; // Check that the named function actually exists, to avoid ugly JS error reports // when calling an undefined value bool found; if (!JS_HasProperty(rq.cx, obj, name, &found) || !found) return false; if (JS_CallFunctionName(rq.cx, obj, name, argv, ret)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::CreateObject_(const ScriptRequest& rq, JS::MutableHandleObject object) { object.set(JS_NewPlainObject(rq.cx)); if (!object) throw PSERROR_Scripting_CreateObjectFailed(); return true; } void ScriptInterface::CreateArray(const ScriptRequest& rq, JS::MutableHandleValue objectValue, size_t length) { objectValue.setObjectOrNull(JS::NewArrayObject(rq.cx, length)); if (!objectValue.isObject()) throw PSERROR_Scripting_CreateObjectFailed(); } bool ScriptInterface::SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate) { ScriptRequest rq(this); JS::RootedObject global(rq.cx, rq.glob); bool found; if (!JS_HasProperty(rq.cx, global, name, &found)) return false; if (found) { JS::Rooted desc(rq.cx); if (!JS_GetOwnPropertyDescriptor(rq.cx, global, name, &desc)) return false; if (!desc.writable()) { if (!replace) { ScriptException::Raise(rq, "SetGlobal \"%s\" called multiple times", name); return false; } // This is not supposed to happen, unless the user has called SetProperty with constant = true on the global object // instead of using SetGlobal. if (!desc.configurable()) { ScriptException::Raise(rq, "The global \"%s\" is permanent and cannot be hotloaded", name); return false; } LOGMESSAGE("Hotloading new value for global \"%s\".", name); ENSURE(JS_DeleteProperty(rq.cx, global, name)); } } uint attrs = 0; if (constant) attrs |= JSPROP_READONLY; if (enumerate) attrs |= JSPROP_ENUMERATE; return JS_DefineProperty(rq.cx, global, name, value, attrs); } bool ScriptInterface::SetProperty_(JS::HandleValue obj, const char* name, JS::HandleValue value, bool constant, bool enumerate) const { ScriptRequest rq(this); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); return JS_DefineProperty(rq.cx, object, name, value, attrs); } bool ScriptInterface::SetProperty_(JS::HandleValue obj, const wchar_t* name, JS::HandleValue value, bool constant, bool enumerate) const { ScriptRequest rq(this); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); utf16string name16(name, name + wcslen(name)); return JS_DefineUCProperty(rq.cx, object, reinterpret_cast(name16.c_str()), name16.length(), value, attrs); } bool ScriptInterface::SetPropertyInt_(JS::HandleValue obj, int name, JS::HandleValue value, bool constant, bool enumerate) const { ScriptRequest rq(this); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); JS::RootedId id(rq.cx, INT_TO_JSID(name)); return JS_DefinePropertyById(rq.cx, object, id, value, attrs); } bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) const { ScriptRequest rq(this); return GetProperty(rq, obj, name, out); } bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleObject out) { JS::RootedValue val(rq.cx); if (!GetProperty(rq, obj, name, &val)) return false; if (!val.isObject()) { LOGERROR("GetProperty failed: trying to get an object, but the property is not an object!"); return false; } out.set(&val.toObject()); return true; } bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const { ScriptRequest rq(this); return GetProperty(rq, obj, name, out); } bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleValue out) { if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); return JS_GetProperty(rq.cx, object, name, out); } bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const { ScriptRequest rq(this); return GetPropertyInt(rq,obj, name, out); } bool ScriptInterface::GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, JS::MutableHandleValue out) { JS::RootedId nameId(rq.cx, INT_TO_JSID(name)); if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); return JS_GetPropertyById(rq.cx, object, nameId, out); } bool ScriptInterface::HasProperty(JS::HandleValue obj, const char* name) const { ScriptRequest rq(this); if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); bool found; if (!JS_HasProperty(rq.cx, object, name, &found)) return false; return found; } bool ScriptInterface::GetGlobalProperty(const ScriptRequest& rq, const std::string& name, JS::MutableHandleValue out) { // Try to get the object as a property of the global object. JS::RootedObject global(rq.cx, rq.glob); if (!JS_GetProperty(rq.cx, global, name.c_str(), out)) { out.set(JS::NullHandleValue); return false; } if (!out.isNullOrUndefined()) return true; // Some objects, such as const definitions, or Class definitions, are hidden inside closures. // We must fetch those from the correct lexical scope. //JS::RootedValue glob(cx); JS::RootedObject lexical_environment(rq.cx, JS_GlobalLexicalEnvironment(rq.glob)); if (!JS_GetProperty(rq.cx, lexical_environment, name.c_str(), out)) { out.set(JS::NullHandleValue); return false; } if (!out.isNullOrUndefined()) return true; out.set(JS::NullHandleValue); return false; } bool ScriptInterface::EnumeratePropertyNames(JS::HandleValue objVal, bool enumerableOnly, std::vector& out) const { ScriptRequest rq(this); if (!objVal.isObjectOrNull()) { LOGERROR("EnumeratePropertyNames expected object type!"); return false; } JS::RootedObject obj(rq.cx, &objVal.toObject()); JS::RootedIdVector props(rq.cx); // This recurses up the prototype chain on its own. if (!js::GetPropertyKeys(rq.cx, obj, enumerableOnly? 0 : JSITER_HIDDEN, &props)) return false; out.reserve(out.size() + props.length()); for (size_t i = 0; i < props.length(); ++i) { JS::RootedId id(rq.cx, props[i]); JS::RootedValue val(rq.cx); if (!JS_IdToValue(rq.cx, id, &val)) return false; // Ignore integer properties for now. // TODO: is this actually a thing in ECMAScript 6? if (!val.isString()) continue; std::string propName; if (!FromJSVal(rq, val, propName)) return false; out.emplace_back(std::move(propName)); } return true; } bool ScriptInterface::SetPrototype(JS::HandleValue objVal, JS::HandleValue protoVal) { ScriptRequest rq(this); if (!objVal.isObject() || !protoVal.isObject()) return false; JS::RootedObject obj(rq.cx, &objVal.toObject()); JS::RootedObject proto(rq.cx, &protoVal.toObject()); return JS_SetPrototype(rq.cx, obj, proto); } bool ScriptInterface::FreezeObject(JS::HandleValue objVal, bool deep) const { ScriptRequest rq(this); if (!objVal.isObject()) return false; JS::RootedObject obj(rq.cx, &objVal.toObject()); if (deep) return JS_DeepFreezeObject(rq.cx, obj); else return JS_FreezeObject(rq.cx, obj); } bool ScriptInterface::LoadScript(const VfsPath& filename, const std::string& code) const { ScriptRequest rq(this); JS::RootedObject global(rq.cx, rq.glob); // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = filename.string8(); JS::CompileOptions options(rq.cx); // Set the line to 0 because CompileFunction silently adds a `(function() {` as the first line, // and errors get misreported. // TODO: it would probably be better to not implicitly introduce JS scopes. options.setFileAndLine(filenameStr.c_str(), 0); options.setIsRunOnce(false); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); JS::RootedObjectVector emptyScopeChain(rq.cx); JS::RootedFunction func(rq.cx, JS::CompileFunction(rq.cx, emptyScopeChain, options, NULL, 0, NULL, src)); if (func == nullptr) { ScriptException::CatchPending(rq); return false; } JS::RootedValue rval(rq.cx); if (JS_CallFunction(rq.cx, nullptr, func, JS::HandleValueArray::empty(), &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::LoadGlobalScript(const VfsPath& filename, const std::string& code) const { ScriptRequest rq(this); // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = filename.string8(); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine(filenameStr.c_str(), 1); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::LoadGlobalScriptFile(const VfsPath& path) const { ScriptRequest rq(this); if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return false; } CVFSFile file; PSRETURN ret = file.Load(g_VFS, path); if (ret != PSRETURN_OK) { LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret)); return false; } CStr code = file.DecodeUTF8(); // assume it's UTF-8 uint lineNo = 1; // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = path.string8(); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine(filenameStr.c_str(), lineNo); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::Eval(const char* code) const { ScriptRequest rq(this); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine("(eval)", 1); JS::SourceText src; ENSURE(src.init(rq.cx, code, strlen(code), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::Eval(const char* code, JS::MutableHandleValue rval) const { ScriptRequest rq(this); JS::CompileOptions opts(rq.cx); opts.setFileAndLine("(eval)", 1); JS::SourceText src; ENSURE(src.init(rq.cx, code, strlen(code), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::ParseJSON(const std::string& string_utf8, JS::MutableHandleValue out) const { ScriptRequest rq(this); std::wstring attrsW = wstring_from_utf8(string_utf8); utf16string string(attrsW.begin(), attrsW.end()); if (JS_ParseJSON(rq.cx, reinterpret_cast(string.c_str()), (u32)string.size(), out)) return true; ScriptException::CatchPending(rq); return false; } void ScriptInterface::ReadJSONFile(const VfsPath& path, JS::MutableHandleValue out) const { if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return; } CVFSFile file; PSRETURN ret = file.Load(g_VFS, path); if (ret != PSRETURN_OK) { LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret)); return; } std::string content(file.DecodeUTF8()); // assume it's UTF-8 if (!ParseJSON(content, out)) LOGERROR("Failed to parse '%s'", path.string8()); } struct Stringifier { static bool callback(const char16_t* buf, u32 len, void* data) { utf16string str(buf, buf+len); std::wstring strw(str.begin(), str.end()); Status err; // ignore Unicode errors static_cast(data)->stream << utf8_from_wstring(strw, &err); return true; } std::stringstream stream; }; // TODO: It's not quite clear why JS_Stringify needs JS::MutableHandleValue. |obj| should not get modified. // It probably has historical reasons and could be changed by SpiderMonkey in the future. std::string ScriptInterface::StringifyJSON(JS::MutableHandleValue obj, bool indent) const { ScriptRequest rq(this); Stringifier str; JS::RootedValue indentVal(rq.cx, indent ? JS::Int32Value(2) : JS::UndefinedValue()); if (!JS_Stringify(rq.cx, obj, nullptr, indentVal, &Stringifier::callback, &str)) { ScriptException::CatchPending(rq); return std::string(); } return str.stream.str(); } std::string ScriptInterface::ToString(JS::MutableHandleValue obj, bool pretty) const { ScriptRequest rq(this); if (obj.isUndefined()) return "(void 0)"; // Try to stringify as JSON if possible // (TODO: this is maybe a bad idea since it'll drop 'undefined' values silently) if (pretty) { Stringifier str; JS::RootedValue indentVal(rq.cx, JS::Int32Value(2)); if (JS_Stringify(rq.cx, obj, nullptr, indentVal, &Stringifier::callback, &str)) return str.stream.str(); // Drop exceptions raised by cyclic values before trying something else JS_ClearPendingException(rq.cx); } // Caller didn't want pretty output, or JSON conversion failed (e.g. due to cycles), // so fall back to obj.toSource() std::wstring source = L"(error)"; CallFunction(obj, "toSource", source); return utf8_from_wstring(source); } JS::Value ScriptInterface::CloneValueFromOtherCompartment(const ScriptInterface& otherCompartment, JS::HandleValue val) const { PROFILE("CloneValueFromOtherCompartment"); ScriptRequest rq(this); JS::RootedValue out(rq.cx); ScriptInterface::StructuredClone structuredClone = otherCompartment.WriteStructuredClone(val); ReadStructuredClone(structuredClone, &out); return out.get(); } ScriptInterface::StructuredClone ScriptInterface::WriteStructuredClone(JS::HandleValue v) const { ScriptRequest rq(this); ScriptInterface::StructuredClone ret(new JSStructuredCloneData(JS::StructuredCloneScope::SameProcess)); JS::CloneDataPolicy policy; if (!JS_WriteStructuredClone(rq.cx, v, ret.get(), JS::StructuredCloneScope::SameProcess, policy, nullptr, nullptr, JS::UndefinedHandleValue)) { debug_warn(L"Writing a structured clone with JS_WriteStructuredClone failed!"); ScriptException::CatchPending(rq); return ScriptInterface::StructuredClone(); } return ret; } void ScriptInterface::ReadStructuredClone(const ScriptInterface::StructuredClone& ptr, JS::MutableHandleValue ret) const { ScriptRequest rq(this); JS::CloneDataPolicy policy; if (!JS_ReadStructuredClone(rq.cx, *ptr, JS_STRUCTURED_CLONE_VERSION, ptr->scope(), ret, policy, nullptr, nullptr)) ScriptException::CatchPending(rq); } Index: ps/trunk/source/soundmanager/SoundManager.cpp =================================================================== --- ps/trunk/source/soundmanager/SoundManager.cpp (revision 24529) +++ ps/trunk/source/soundmanager/SoundManager.cpp (revision 24530) @@ -1,866 +1,867 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ISoundManager.h" #include "SoundManager.h" #include "data/SoundData.h" #include "items/CBufferItem.h" #include "items/CSoundItem.h" #include "items/CStreamItem.h" #include "lib/external_libraries/libsdl.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Profiler2.h" +#include "ps/Threading.h" #include "ps/XML/Xeromyces.h" #include ISoundManager* g_SoundManager = NULL; #define SOURCE_NUM 64 #if CONFIG2_AUDIO class CSoundManagerWorker { NONCOPYABLE(CSoundManagerWorker); public: CSoundManagerWorker() { m_Items = new ItemsList; m_DeadItems = new ItemsList; m_Shutdown = false; - m_WorkerThread = std::thread(RunThread, this); + m_WorkerThread = std::thread(Threading::HandleExceptions::Wrapper, this); } ~CSoundManagerWorker() { delete m_Items; CleanupItems(); delete m_DeadItems; } bool Shutdown() { { std::lock_guard lock(m_WorkerMutex); m_Shutdown = true; ItemsList::iterator lstr = m_Items->begin(); while (lstr != m_Items->end()) { delete *lstr; ++lstr; } } m_WorkerThread.join(); return true; } void addItem(ISoundItem* anItem) { std::lock_guard lock(m_WorkerMutex); m_Items->push_back(anItem); } void CleanupItems() { std::lock_guard lock(m_DeadItemsMutex); AL_CHECK; ItemsList::iterator deadItems = m_DeadItems->begin(); while (deadItems != m_DeadItems->end()) { delete *deadItems; ++deadItems; AL_CHECK; } m_DeadItems->clear(); } private: static void RunThread(CSoundManagerWorker* data) { debug_SetThreadName("CSoundManagerWorker"); g_Profiler2.RegisterCurrentThread("soundmanager"); data->Run(); } void Run() { while (true) { // Handle shutdown requests as soon as possible if (GetShutdown()) return; int pauseTime = 500; if (g_SoundManager->InDistress()) pauseTime = 50; { std::lock_guard workerLock(m_WorkerMutex); ItemsList::iterator lstr = m_Items->begin(); ItemsList* nextItemList = new ItemsList; while (lstr != m_Items->end()) { AL_CHECK; if ((*lstr)->IdleTask()) { if ((pauseTime == 500) && (*lstr)->IsFading()) pauseTime = 100; nextItemList->push_back(*lstr); } else { std::lock_guard deadItemsLock(m_DeadItemsMutex); m_DeadItems->push_back(*lstr); } ++lstr; AL_CHECK; } delete m_Items; m_Items = nextItemList; AL_CHECK; } SDL_Delay(pauseTime); } } bool GetShutdown() { std::lock_guard lock(m_WorkerMutex); return m_Shutdown; } private: // Thread-related members: std::thread m_WorkerThread; std::mutex m_WorkerMutex; std::mutex m_DeadItemsMutex; // Shared by main thread and worker thread: // These variables are all protected by a mutexes ItemsList* m_Items; ItemsList* m_DeadItems; bool m_Shutdown; CSoundManagerWorker(ISoundManager* UNUSED(other)){}; }; void ISoundManager::CreateSoundManager() { if (!g_SoundManager) { g_SoundManager = new CSoundManager(); g_SoundManager->StartWorker(); } } void ISoundManager::SetEnabled(bool doEnable) { if (g_SoundManager && !doEnable) SAFE_DELETE(g_SoundManager); else if (!g_SoundManager && doEnable) ISoundManager::CreateSoundManager(); } void ISoundManager::CloseGame() { if (CSoundManager* aSndMgr = (CSoundManager*)g_SoundManager) aSndMgr->SetAmbientItem(NULL); } void CSoundManager::al_ReportError(ALenum err, const char* caller, int line) { LOGERROR("OpenAL error: %s; called from %s (line %d)\n", alGetString(err), caller, line); } void CSoundManager::al_check(const char* caller, int line) { ALenum err = alGetError(); if (err != AL_NO_ERROR) al_ReportError(err, caller, line); } Status CSoundManager::ReloadChangedFiles(const VfsPath& UNUSED(path)) { // TODO implement sound file hotloading return INFO::OK; } /*static*/ Status CSoundManager::ReloadChangedFileCB(void* param, const VfsPath& path) { return static_cast(param)->ReloadChangedFiles(path); } CSoundManager::CSoundManager() : m_Context(nullptr), m_Device(nullptr), m_ALSourceBuffer(nullptr), m_CurrentTune(nullptr), m_CurrentEnvirons(nullptr), m_Worker(nullptr), m_DistressMutex(), m_PlayListItems(nullptr), m_SoundGroups(), m_Gain(.5f), m_MusicGain(.5f), m_AmbientGain(.5f), m_ActionGain(.5f), m_UIGain(.5f), m_Enabled(false), m_BufferSize(98304), m_BufferCount(50), m_SoundEnabled(true), m_MusicEnabled(true), m_MusicPaused(false), m_AmbientPaused(false), m_ActionPaused(false), m_RunningPlaylist(false), m_PlayingPlaylist(false), m_LoopingPlaylist(false), m_PlaylistGap(0), m_DistressErrCount(0), m_DistressTime(0) { CFG_GET_VAL("sound.mastergain", m_Gain); CFG_GET_VAL("sound.musicgain", m_MusicGain); CFG_GET_VAL("sound.ambientgain", m_AmbientGain); CFG_GET_VAL("sound.actiongain", m_ActionGain); CFG_GET_VAL("sound.uigain", m_UIGain); AlcInit(); if (m_Enabled) { SetMasterGain(m_Gain); InitListener(); m_PlayListItems = new PlayList; } if (!CXeromyces::AddValidator(g_VFS, "sound_group", "audio/sound_group.rng")) LOGERROR("CSoundManager: failed to load grammar file 'audio/sound_group.rng'"); RegisterFileReloadFunc(ReloadChangedFileCB, this); RunHardwareDetection(); } CSoundManager::~CSoundManager() { UnregisterFileReloadFunc(ReloadChangedFileCB, this); if (m_Worker) { AL_CHECK; m_Worker->Shutdown(); AL_CHECK; m_Worker->CleanupItems(); AL_CHECK; delete m_Worker; } AL_CHECK; for (const std::pair& p : m_SoundGroups) delete p.second; m_SoundGroups.clear(); if (m_PlayListItems) delete m_PlayListItems; if (m_ALSourceBuffer != NULL) delete[] m_ALSourceBuffer; if (m_Context) alcDestroyContext(m_Context); if (m_Device) alcCloseDevice(m_Device); } void CSoundManager::StartWorker() { if (m_Enabled) m_Worker = new CSoundManagerWorker(); } Status CSoundManager::AlcInit() { Status ret = INFO::OK; m_Device = alcOpenDevice(NULL); if (m_Device) { ALCint attribs[] = {ALC_STEREO_SOURCES, 16, 0}; m_Context = alcCreateContext(m_Device, &attribs[0]); if (m_Context) { alcMakeContextCurrent(m_Context); m_ALSourceBuffer = new ALSourceHolder[SOURCE_NUM]; ALuint* sourceList = new ALuint[SOURCE_NUM]; alGenSources(SOURCE_NUM, sourceList); ALCenum err = alcGetError(m_Device); if (err == ALC_NO_ERROR) { for (int x = 0; x < SOURCE_NUM; x++) { m_ALSourceBuffer[x].ALSource = sourceList[x]; m_ALSourceBuffer[x].SourceItem = NULL; } m_Enabled = true; } else { LOGERROR("error in gensource = %d", err); } delete[] sourceList; } } // check if init succeeded. // some OpenAL implementations don't indicate failure here correctly; // we need to check if the device and context pointers are actually valid. ALCenum err = alcGetError(m_Device); const char* dev_name = (const char*)alcGetString(m_Device, ALC_DEVICE_SPECIFIER); if (err == ALC_NO_ERROR && m_Device && m_Context) debug_printf("Sound: AlcInit success, using %s\n", dev_name); else { LOGERROR("Sound: AlcInit failed, m_Device=%p m_Context=%p dev_name=%s err=%x\n", (void *)m_Device, (void *)m_Context, dev_name, err); // FIXME Hack to get around exclusive access to the sound device #if OS_UNIX ret = INFO::OK; #else ret = ERR::FAIL; #endif // !OS_UNIX } return ret; } bool CSoundManager::InDistress() { std::lock_guard lock(m_DistressMutex); if (m_DistressTime == 0) return false; else if ((timer_Time() - m_DistressTime) > 10) { m_DistressTime = 0; // Coming out of distress mode m_DistressErrCount = 0; return false; } return true; } void CSoundManager::SetDistressThroughShortage() { std::lock_guard lock(m_DistressMutex); // Going into distress for normal reasons m_DistressTime = timer_Time(); } void CSoundManager::SetDistressThroughError() { std::lock_guard lock(m_DistressMutex); // Going into distress due to unknown error m_DistressTime = timer_Time(); m_DistressErrCount++; } ALuint CSoundManager::GetALSource(ISoundItem* anItem) { for (int x = 0; x < SOURCE_NUM; x++) { if (!m_ALSourceBuffer[x].SourceItem) { m_ALSourceBuffer[x].SourceItem = anItem; return m_ALSourceBuffer[x].ALSource; } } SetDistressThroughShortage(); return 0; } void CSoundManager::ReleaseALSource(ALuint theSource) { for (int x = 0; x < SOURCE_NUM; x++) { if (m_ALSourceBuffer[x].ALSource == theSource) { m_ALSourceBuffer[x].SourceItem = NULL; return; } } } long CSoundManager::GetBufferCount() { return m_BufferCount; } long CSoundManager::GetBufferSize() { return m_BufferSize; } void CSoundManager::AddPlayListItem(const VfsPath& itemPath) { if (m_Enabled) m_PlayListItems->push_back(itemPath); } void CSoundManager::ClearPlayListItems() { if (m_Enabled) { if (m_PlayingPlaylist) SetMusicItem(NULL); m_PlayingPlaylist = false; m_LoopingPlaylist = false; m_RunningPlaylist = false; m_PlayListItems->clear(); } } void CSoundManager::StartPlayList(bool doLoop) { if (m_Enabled && m_MusicEnabled) { if (m_PlayListItems->size() > 0) { m_PlayingPlaylist = true; m_LoopingPlaylist = doLoop; m_RunningPlaylist = false; ISoundItem* aSnd = LoadItem((m_PlayListItems->at(0))); if (aSnd) SetMusicItem(aSnd); else SetMusicItem(NULL); } } } void CSoundManager::SetMasterGain(float gain) { if (m_Enabled) { m_Gain = gain; alListenerf(AL_GAIN, m_Gain); AL_CHECK; } } void CSoundManager::SetMusicGain(float gain) { m_MusicGain = gain; if (m_CurrentTune) m_CurrentTune->SetGain(m_MusicGain); } void CSoundManager::SetAmbientGain(float gain) { m_AmbientGain = gain; } void CSoundManager::SetActionGain(float gain) { m_ActionGain = gain; } void CSoundManager::SetUIGain(float gain) { m_UIGain = gain; } ISoundItem* CSoundManager::LoadItem(const VfsPath& itemPath) { AL_CHECK; if (m_Enabled) { CSoundData* itemData = CSoundData::SoundDataFromFile(itemPath); AL_CHECK; if (itemData) return CSoundManager::ItemForData(itemData); } return NULL; } ISoundItem* CSoundManager::ItemForData(CSoundData* itemData) { AL_CHECK; ISoundItem* answer = NULL; AL_CHECK; if (m_Enabled && (itemData != NULL)) { if (itemData->IsOneShot()) { if (itemData->GetBufferCount() == 1) answer = new CSoundItem(itemData); else answer = new CBufferItem(itemData); } else { answer = new CStreamItem(itemData); } if (answer && m_Worker) m_Worker->addItem(answer); } return answer; } void CSoundManager::IdleTask() { if (m_Enabled) { if (m_CurrentTune) { m_CurrentTune->EnsurePlay(); if (m_PlayingPlaylist && m_RunningPlaylist) { if (m_CurrentTune->Finished()) { if (m_PlaylistGap == 0) { m_PlaylistGap = timer_Time() + 15; } else if (m_PlaylistGap < timer_Time()) { m_PlaylistGap = 0; PlayList::iterator it = find(m_PlayListItems->begin(), m_PlayListItems->end(), m_CurrentTune->GetName()); if (it != m_PlayListItems->end()) { ++it; Path nextPath; if (it == m_PlayListItems->end()) nextPath = m_PlayListItems->at(0); else nextPath = *it; ISoundItem* aSnd = LoadItem(nextPath); if (aSnd) SetMusicItem(aSnd); } } } } } if (m_CurrentEnvirons) m_CurrentEnvirons->EnsurePlay(); if (m_Worker) m_Worker->CleanupItems(); } } ISoundItem* CSoundManager::ItemForEntity(entity_id_t UNUSED(source), CSoundData* sndData) { ISoundItem* currentItem = NULL; if (m_Enabled) currentItem = ItemForData(sndData); return currentItem; } void CSoundManager::InitListener() { ALfloat listenerPos[] = {0.0, 0.0, 0.0}; ALfloat listenerVel[] = {0.0, 0.0, 0.0}; ALfloat listenerOri[] = {0.0, 0.0, -1.0, 0.0, 1.0, 0.0}; alListenerfv(AL_POSITION, listenerPos); alListenerfv(AL_VELOCITY, listenerVel); alListenerfv(AL_ORIENTATION, listenerOri); alDistanceModel(AL_LINEAR_DISTANCE); } void CSoundManager::PlayGroupItem(ISoundItem* anItem, ALfloat groupGain) { if (anItem) { if (m_Enabled && (m_ActionGain > 0)) { anItem->SetGain(m_ActionGain * groupGain); anItem->PlayAndDelete(); AL_CHECK; } } } void CSoundManager::SetMusicEnabled(bool isEnabled) { if (m_CurrentTune && !isEnabled) { m_CurrentTune->FadeAndDelete(1.00); m_CurrentTune = NULL; } m_MusicEnabled = isEnabled; } void CSoundManager::PlayAsGroup(const VfsPath& groupPath, const CVector3D& sourcePos, entity_id_t source, bool ownedSound) { // Make sure the sound group is loaded CSoundGroup* group; if (m_SoundGroups.find(groupPath.string()) == m_SoundGroups.end()) { group = new CSoundGroup(); if (!group->LoadSoundGroup(L"audio/" + groupPath.string())) { LOGERROR("Failed to load sound group '%s'", groupPath.string8()); delete group; group = NULL; } // Cache the sound group (or the null, if it failed) m_SoundGroups[groupPath.string()] = group; } else { group = m_SoundGroups[groupPath.string()]; } // Failed to load group -> do nothing if (group && (ownedSound || !group->TestFlag(eOwnerOnly))) group->PlayNext(sourcePos, source); } void CSoundManager::PlayAsMusic(const VfsPath& itemPath, bool looping) { if (m_Enabled) { UNUSED2(looping); ISoundItem* aSnd = LoadItem(itemPath); if (aSnd != NULL) SetMusicItem(aSnd); } } void CSoundManager::PlayAsAmbient(const VfsPath& itemPath, bool looping) { if (m_Enabled) { UNUSED2(looping); ISoundItem* aSnd = LoadItem(itemPath); if (aSnd != NULL) SetAmbientItem(aSnd); } } void CSoundManager::PlayAsUI(const VfsPath& itemPath, bool looping) { if (m_Enabled) { IdleTask(); if (ISoundItem* anItem = LoadItem(itemPath)) { if (m_UIGain > 0) { anItem->SetGain(m_UIGain); anItem->SetLooping(looping); anItem->PlayAndDelete(); } } AL_CHECK; } } void CSoundManager::Pause(bool pauseIt) { PauseMusic(pauseIt); PauseAmbient(pauseIt); PauseAction(pauseIt); } void CSoundManager::PauseMusic(bool pauseIt) { if (m_CurrentTune && pauseIt && !m_MusicPaused) { m_CurrentTune->FadeAndPause(1.0); } else if (m_CurrentTune && m_MusicPaused && !pauseIt && m_MusicEnabled) { m_CurrentTune->SetGain(0); m_CurrentTune->Resume(); m_CurrentTune->FadeToIn(m_MusicGain, 1.0); } m_MusicPaused = pauseIt; } void CSoundManager::PauseAmbient(bool pauseIt) { if (m_CurrentEnvirons && pauseIt) m_CurrentEnvirons->Pause(); else if (m_CurrentEnvirons) m_CurrentEnvirons->Resume(); m_AmbientPaused = pauseIt; } void CSoundManager::PauseAction(bool pauseIt) { m_ActionPaused = pauseIt; } void CSoundManager::SetMusicItem(ISoundItem* anItem) { if (m_Enabled) { AL_CHECK; if (m_CurrentTune) { m_CurrentTune->FadeAndDelete(2.00); m_CurrentTune = NULL; } IdleTask(); if (anItem) { if (m_MusicEnabled) { m_CurrentTune = anItem; m_CurrentTune->SetGain(0); if (m_PlayingPlaylist) { m_RunningPlaylist = true; m_CurrentTune->Play(); } else m_CurrentTune->PlayLoop(); m_MusicPaused = false; m_CurrentTune->FadeToIn(m_MusicGain, 1.00); } else { anItem->StopAndDelete(); } } AL_CHECK; } } void CSoundManager::SetAmbientItem(ISoundItem* anItem) { if (m_Enabled) { if (m_CurrentEnvirons) { m_CurrentEnvirons->FadeAndDelete(3.00); m_CurrentEnvirons = NULL; } IdleTask(); if (anItem) { if (m_AmbientGain > 0) { m_CurrentEnvirons = anItem; m_CurrentEnvirons->SetGain(0); m_CurrentEnvirons->PlayLoop(); m_CurrentEnvirons->FadeToIn(m_AmbientGain, 2.00); } } AL_CHECK; } } void CSoundManager::RunHardwareDetection() { // OpenAL alGetString might not return anything interesting on certain platforms // (see https://stackoverflow.com/questions/28960638 for an example). // However our previous code supported only Windows, and alGetString does work on // Windows, so this is an improvement. // Sound cards const ALCchar* devices = nullptr; if (alcIsExtensionPresent(nullptr, "ALC_enumeration_EXT") == AL_TRUE) { if (alcIsExtensionPresent(nullptr, "ALC_enumerate_all_EXT") == AL_TRUE) devices = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER); else devices = alcGetString(nullptr, ALC_DEVICE_SPECIFIER); } WARN_IF_FALSE(devices); m_SoundCardNames.clear(); do { m_SoundCardNames += devices; devices += strlen(devices) + 1; m_SoundCardNames += "; "; } while (*devices); // Driver version const ALCchar* al_version = alGetString(AL_VERSION); if (al_version) m_OpenALVersion = al_version; } CStr8 CSoundManager::GetOpenALVersion() const { return m_OpenALVersion; } CStr8 CSoundManager::GetSoundCardNames() const { return m_SoundCardNames; } #else // CONFIG2_AUDIO void ISoundManager::CreateSoundManager(){} void ISoundManager::SetEnabled(bool UNUSED(doEnable)){} void ISoundManager::CloseGame(){} void ISoundManager::RunHardwareDetection() {} CStr8 ISoundManager::GetSoundCardNames() const { return CStr8(); }; CStr8 ISoundManager::GetOpenALVersion() const { return CStr8(); }; #endif // CONFIG2_AUDIO Index: ps/trunk/source/test_setup.cpp =================================================================== --- ps/trunk/source/test_setup.cpp (revision 24529) +++ ps/trunk/source/test_setup.cpp (revision 24530) @@ -1,158 +1,158 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ // Got to be consistent with what the rest of the source files do before // including precompiled.h, so that the PCH works correctly #ifndef CXXTEST_RUNNING #define CXXTEST_RUNNING #endif #define _CXXTEST_HAVE_STD #include "precompiled.h" #include #include "lib/self_test.h" #include #if OS_WIN #include "lib/sysdep/os/win/wdbg_heap.h" #endif #include "lib/timer.h" #include "lib/sysdep/sysdep.h" #include "ps/Profiler2.h" #include "scriptinterface/ScriptEngine.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" class LeakReporter : public CxxTest::GlobalFixture { virtual bool tearDownWorld() { // Enable leak reporting on exit. // (This is done in tearDownWorld so that it doesn't report 'leaks' // if the program is aborted before finishing cleanly.) #if OS_WIN wdbg_heap_Enable(true); #endif return true; } virtual bool setUpWorld() { #if MSC_VERSION // (Warning: the allocation numbers seem to differ by 3 when you // run in the build process vs the debugger) // _CrtSetBreakAlloc(1952); #endif return true; } }; class MiscSetup : public CxxTest::GlobalFixture { virtual bool setUpWorld() { // Timer must be initialised, else things will break when tests do IO timer_Init(); #if OS_MACOSX || OS_BSD // See comment in GameSetup.cpp FixLocales setlocale(LC_CTYPE, "UTF-8"); #endif - ThreadUtil::SetMainThread(); + Threading::SetMainThread(); g_Profiler2.Initialise(); m_ScriptEngine = new ScriptEngine; g_ScriptContext = ScriptContext::CreateContext(); return true; } virtual bool tearDownWorld() { g_ScriptContext.reset(); SAFE_DELETE(m_ScriptEngine); g_Profiler2.Shutdown(); return true; } virtual bool setUp() { // Clean up any JS leftover between tests. g_ScriptContext->ShrinkingGC(); return true; } private: // We're doing the initialization and shutdown of the ScriptEngine explicitly here // to make sure it's only initialized when setUpWorld is called. ScriptEngine* m_ScriptEngine; }; static LeakReporter leakReporter; static MiscSetup miscSetup; // Definition of functions from lib/self_test.h bool ts_str_contains(const std::string& str1, const std::string& str2) { return str1.find(str2) != str1.npos; } bool ts_str_contains(const std::wstring& str1, const std::wstring& str2) { return str1.find(str2) != str1.npos; } // we need the (version-controlled) binaries/data directory because it // contains input files (it is assumed that developer's machines have // write access to those directories). note that argv0 isn't // available, so we use sys_ExecutablePathname. OsPath DataDir() { return sys_ExecutablePathname().Parent()/".."/"data"; } // Script-based testing setup: namespace { void script_TS_FAIL(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& msg) { TS_FAIL(utf8_from_wstring(msg).c_str()); } } void ScriptTestSetup(const ScriptInterface& scriptinterface) { scriptinterface.RegisterFunction("TS_FAIL"); // Load the TS_* function definitions // (We don't use VFS because tests might not have the normal VFS paths loaded) OsPath path = DataDir()/"tests"/"test_setup.js"; std::ifstream ifs(OsString(path).c_str()); ENSURE(ifs.good()); std::string content((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); ENSURE(scriptinterface.LoadScript(L"test_setup.js", content)); } Index: ps/trunk/source/tools/atlas/GameInterface/GameLoop.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/GameLoop.cpp (revision 24529) +++ ps/trunk/source/tools/atlas/GameInterface/GameLoop.cpp (revision 24530) @@ -1,152 +1,152 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "GameLoop.h" #include "MessagePasserImpl.h" #include "Messages.h" #include "SharedMemory.h" #include "Handlers/MessageHandler.h" #include "ActorViewer.h" #include "View.h" #include "InputProcessor.h" #include "graphics/TextureManager.h" #include "lib/app_hooks.h" #include "lib/external_libraries/libsdl.h" #include "lib/timer.h" #include "ps/CLogger.h" #include "ps/DllLoader.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/ThreadUtil.h" #include "ps/GameSetup/Paths.h" #include "renderer/Renderer.h" using namespace AtlasMessage; #include namespace AtlasMessage { extern void RegisterHandlers(); } // Loaded from DLL: void (*Atlas_StartWindow)(const wchar_t* type); void (*Atlas_SetDataDirectory)(const wchar_t* path); void (*Atlas_SetConfigDirectory)(const wchar_t* path); void (*Atlas_SetMessagePasser)(MessagePasser*); void (*Atlas_GLSetCurrent)(void* cavas); void (*Atlas_GLSwapBuffers)(void* canvas); void (*Atlas_DisplayError)(const wchar_t* text, size_t flags); namespace AtlasMessage { void* (*ShareableMallocFptr)(size_t); void (*ShareableFreeFptr)(void*); } MessagePasser* AtlasMessage::g_MessagePasser = NULL; static GameLoopState state; GameLoopState* g_AtlasGameLoop = &state; void RendererIncrementalLoad() { // TODO: shouldn't duplicate this code from main.cpp if (!CRenderer::IsInitialised()) return; const double maxTime = 0.1f; double startTime = timer_Time(); bool more; do { more = g_Renderer.GetTextureManager().MakeProgress(); } while (more && timer_Time() - startTime < maxTime); } bool BeginAtlas(const CmdLineArgs& args, const DllLoader& dll) { // Load required symbols from the DLL try { dll.LoadSymbol("Atlas_StartWindow", Atlas_StartWindow); dll.LoadSymbol("Atlas_SetMessagePasser", Atlas_SetMessagePasser); dll.LoadSymbol("Atlas_SetDataDirectory", Atlas_SetDataDirectory); dll.LoadSymbol("Atlas_SetConfigDirectory", Atlas_SetConfigDirectory); dll.LoadSymbol("Atlas_GLSetCurrent", Atlas_GLSetCurrent); dll.LoadSymbol("Atlas_GLSwapBuffers", Atlas_GLSwapBuffers); dll.LoadSymbol("Atlas_DisplayError", Atlas_DisplayError); dll.LoadSymbol("ShareableMalloc", ShareableMallocFptr); dll.LoadSymbol("ShareableFree", ShareableFreeFptr); } catch (PSERROR_DllLoader&) { debug_warn(L"Failed to initialise DLL"); return false; } // Construct a message passer for communicating with Atlas // (here so that its scope lasts beyond the game thread) MessagePasserImpl msgPasser; AtlasMessage::g_MessagePasser = &msgPasser; // Pass our message handler to Atlas Atlas_SetMessagePasser(&msgPasser); // Tell Atlas the location of the data directory const Paths paths(args); Atlas_SetDataDirectory(paths.RData().string().c_str()); // Tell Atlas the location of the user config directory Atlas_SetConfigDirectory(paths.Config().string().c_str()); RegisterHandlers(); // Disable the game's cursor rendering extern CStrW g_CursorName; g_CursorName = L""; state.args = args; state.running = true; state.view = AtlasView::GetView_None(); state.glCanvas = NULL; // Start Atlas UI on main thread // (required for wxOSX/Cocoa compatibility - see http://trac.wildfiregames.com/ticket/500) Atlas_StartWindow(L"ScenarioEditor"); // TODO: delete all remaining messages, to avoid memory leak warnings // Restore main thread - ThreadUtil::SetMainThread(); + Threading::SetMainThread(); // Clean up AtlasView::DestroyViews(); AtlasMessage::g_MessagePasser = NULL; return true; }