Index: ps/trunk/source/soundmanager/items/CSoundBase.h =================================================================== --- ps/trunk/source/soundmanager/items/CSoundBase.h (revision 22343) +++ ps/trunk/source/soundmanager/items/CSoundBase.h (revision 22344) @@ -1,101 +1,102 @@ -/* Copyright (C) 2015 Wildfire Games. +/* 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 . */ #ifndef INCLUDED_CSOUNDBASE_H #define INCLUDED_CSOUNDBASE_H #include "lib/config2.h" #if CONFIG2_AUDIO #include "lib/external_libraries/openal.h" -#include "ps/ThreadUtil.h" #include "soundmanager/data/SoundData.h" #include "soundmanager/items/ISoundItem.h" +#include + class CSoundBase : public ISoundItem { protected: ALuint m_ALSource; CSoundData* m_SoundData; bool m_LastPlay; bool m_Looping; bool m_ShouldBePlaying; bool m_PauseAfterFade; bool m_IsPaused; double m_StartFadeTime; double m_EndFadeTime; ALfloat m_StartVolume; ALfloat m_EndVolume; - CMutex m_ItemMutex; + std::mutex m_ItemMutex; public: CSoundBase(); virtual ~CSoundBase(); bool InitOpenAL(); void ResetVars(); void EnsurePlay(); void SetGain(ALfloat gain); void SetRollOff(ALfloat gain); void SetPitch(ALfloat pitch); void SetDirection(const CVector3D& direction); void SetCone(ALfloat innerCone, ALfloat outerCone, ALfloat coneGain); void SetLastPlay(bool last); void ReleaseOpenAL(); bool IsFading(); bool Finished(); void Play(); void PlayAndDelete(); void PlayLoop(); void Stop(); void StopAndDelete(); void FadeToIn(ALfloat newVolume, double fadeDuration); bool GetLooping(); bool IsPlaying(); void SetLocation(const CVector3D& position); void FadeAndDelete(double fadeTime); void FadeAndPause(double fadeTime); void Pause(); void Resume(); const Path GetName(); virtual void SetLooping(bool loops); virtual bool IdleTask(); virtual void Attach(CSoundData* itemData); protected: void SetNameFromPath(VfsPath& itemPath); void ResetFade(); bool HandleFade(); }; #endif // CONFIG2_AUDIO #endif // INCLUDED_CSOUNDBASE_H Index: ps/trunk/source/tools/atlas/GameInterface/MessagePasserImpl.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/MessagePasserImpl.cpp (revision 22343) +++ ps/trunk/source/tools/atlas/GameInterface/MessagePasserImpl.cpp (revision 22344) @@ -1,195 +1,195 @@ -/* Copyright (C) 2013 Wildfire Games. +/* 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 "MessagePasserImpl.h" #include "Messages.h" #include "lib/timer.h" #include "lib/rand.h" #include "lib/posix/posix_filesystem.h" using namespace AtlasMessage; MessagePasserImpl::MessagePasserImpl() : m_Trace(false), m_Semaphore(NULL) { int tries = 0; while (tries++ < 16) // some arbitrary cut-off point to avoid infinite loops { static char name[64]; sprintf_s(name, ARRAY_SIZE(name), "/wfg-atlas-msgpass-%d-%d", (int)rand(1, 1000), (int)(time(0)%1000)); sem_t* sem = sem_open(name, O_CREAT | O_EXCL, 0700, 0); // This cast should not be necessary, but apparently SEM_FAILED is not // a value of a pointer type if (sem == (sem_t*)SEM_FAILED || !sem) { int err = errno; if (err == EEXIST) { // Semaphore already exists - try another one continue; } // Otherwise, it's a probably-fatal error debug_warn(L"sem_open failed"); break; } // Succeeded - use this semaphore m_Semaphore = sem; m_SemaphoreName = name; break; } if (! m_Semaphore) { debug_warn(L"Failed to create semaphore for Atlas - giving up"); // We will probably crash later - maybe we could fall back on sem_init, if this // ever fails in practice } } MessagePasserImpl::~MessagePasserImpl() { if (m_Semaphore) { // Clean up sem_close(m_Semaphore); sem_unlink(m_SemaphoreName.c_str()); } } void MessagePasserImpl::Add(IMessage* msg) { ENSURE(msg); ENSURE(msg->GetType() == IMessage::Message); if (m_Trace) debug_printf("%8.3f add message: %s\n", timer_Time(), msg->GetName()); { - CScopeLock lock(m_Mutex); + std::lock_guard lock(m_Mutex); m_Queue.push(msg); } } IMessage* MessagePasserImpl::Retrieve() { // (It should be fairly easy to use a more efficient thread-safe queue, // since there's only one thread adding items and one thread consuming; // but it's not worthwhile yet.) IMessage* msg = NULL; { - CScopeLock lock(m_Mutex); + std::lock_guard lock(m_Mutex); if (! m_Queue.empty()) { msg = m_Queue.front(); m_Queue.pop(); } } if (m_Trace && msg) debug_printf("%8.3f retrieved message: %s\n", timer_Time(), msg->GetName()); return msg; } void MessagePasserImpl::Query(QueryMessage* qry, void(* UNUSED(timeoutCallback) )()) { ENSURE(qry); ENSURE(qry->GetType() == IMessage::Query); if (m_Trace) debug_printf("%8.3f add query: %s\n", timer_Time(), qry->GetName()); // Set the semaphore, so we can block until the query has been handled qry->m_Semaphore = static_cast(m_Semaphore); { - CScopeLock lock(m_Mutex); + std::lock_guard lock(m_Mutex); m_Queue.push(qry); } // Wait until the query handler has handled the query and called sem_post: // The following code was necessary to avoid deadlock, but it still breaks // in some cases (e.g. when Atlas issues a query before its event loop starts // running) and doesn't seem to be the simplest possible solution. // So currently we're trying to not do anything like that at all, and // just stop the game making windows (which is what seems (from experience) to // deadlock things) by overriding ah_display_error. Hopefully it'll work like // that, and the redundant code below/elsewhere can be removed, but it's // left in here in case it needs to be reinserted in the future to make it // work. // (See http://www.wildfiregames.com/forum/index.php?s=&showtopic=10236&view=findpost&p=174617) // // At least on Win32, it is necessary for the UI thread to run its event // // loop to avoid deadlocking the system (particularly when the game // // tries to show a dialog box); so timeoutCallback is called whenever we // // think it's necessary for that to happen. // // #if OS_WIN // // On Win32, use MsgWaitForMultipleObjects, which waits on the semaphore // // but is also interrupted by incoming Windows-messages. // // while (0 != (err = sem_msgwait_np(psem))) // // while (0 != (err = sem_wait(psem))) // #else // // TODO: On non-Win32, I have no idea whether the same problem exists; but // // it might do, so call the callback every few seconds just in case it helps. // struct timespec abs_timeout; // clock_gettime(CLOCK_REALTIME, &abs_timeout); // abs_timeout.tv_sec += 2; // while (0 != (err = sem_timedwait(psem, &abs_timeout))) // #endif while (0 != sem_wait(m_Semaphore)) { // If timed out, call callback and try again // if (errno == ETIMEDOUT) // timeoutCallback(); // else // Keep retrying while EINTR, but other errors are probably fatal if (errno != EINTR) { debug_warn(L"Semaphore wait failed"); return; // (leaks the semaphore) } } // Clean up qry->m_Semaphore = NULL; } bool MessagePasserImpl::IsEmpty() { - CScopeLock lock(m_Mutex); + std::lock_guard lock(m_Mutex); return m_Queue.empty(); } void MessagePasserImpl::SetTrace(bool t) { m_Trace = t; } Index: ps/trunk/source/graphics/MapGenerator.cpp =================================================================== --- ps/trunk/source/graphics/MapGenerator.cpp (revision 22343) +++ ps/trunk/source/graphics/MapGenerator.cpp (revision 22344) @@ -1,410 +1,410 @@ -/* Copyright (C) 2018 Wildfire Games. +/* 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 "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/scripting/JSInterface_VFS.h" #include "scriptinterface/ScriptRuntime.h" #include "scriptinterface/ScriptConversions.h" #include "scriptinterface/ScriptInterface.h" #include #include // TODO: what's a good default? perhaps based on map size #define RMS_RUNTIME_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() { // If something happens before we initialize, that's a failure m_Progress = -1; } CMapGeneratorWorker::~CMapGeneratorWorker() { // Wait for thread to end pthread_join(m_WorkerThread, NULL); } void CMapGeneratorWorker::Initialize(const VfsPath& scriptFile, const std::string& settings) { - CScopeLock lock(m_WorkerMutex); + std::lock_guard lock(m_WorkerMutex); // Set progress to positive value m_Progress = 1; m_ScriptPath = scriptFile; m_Settings = settings; // Launch the worker thread int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this); ENSURE(ret == 0); } void* CMapGeneratorWorker::RunThread(void *data) { debug_SetThreadName("MapGenerator"); g_Profiler2.RegisterCurrentThread("MapGenerator"); CMapGeneratorWorker* self = static_cast(data); shared_ptr mapgenRuntime = ScriptInterface::CreateRuntime(g_ScriptRuntime, RMS_RUNTIME_SIZE); // Enable the script to be aborted JS_SetInterruptCallback(mapgenRuntime->m_rt, MapGeneratorInterruptCallback); self->m_ScriptInterface = new ScriptInterface("Engine", "MapGenerator", mapgenRuntime); // 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 - CScopeLock lock(self->m_WorkerMutex); + std::lock_guard lock(self->m_WorkerMutex); self->m_Progress = -1; } // 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() { // We must destroy the ScriptInterface in the same thread because the JSAPI requires that! // Also we must not be in a request when calling the ScriptInterface destructor, so the autoFree object // must be instantiated before the request (destructors are called in reverse order of instantiation) struct AutoFree { AutoFree(ScriptInterface* p) : m_p(p) {} ~AutoFree() { SAFE_DELETE(m_p); } ScriptInterface* m_p; } autoFree(m_ScriptInterface); JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); m_ScriptInterface->SetCallbackData(static_cast (this)); // Replace RNG with a seeded deterministic function m_ScriptInterface->ReplaceNondeterministicRNG(m_MapGenRNG); // Functions for RMS JSI_VFS::RegisterScriptFunctions_Maps(*m_ScriptInterface); m_ScriptInterface->RegisterFunction("LoadLibrary"); m_ScriptInterface->RegisterFunction("LoadHeightmapImage"); m_ScriptInterface->RegisterFunction("LoadMapTerrain"); m_ScriptInterface->RegisterFunction("ExportMap"); m_ScriptInterface->RegisterFunction("SetProgress"); 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"); m_ScriptInterface->RegisterFunction("GetTerrainTileSize"); // Globalscripts may use VFS script functions m_ScriptInterface->LoadGlobalScripts(); // Parse settings JS::RootedValue settingsVal(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"); m_MapGenRNG.seed(seed); // Copy settings to global variable JS::RootedValue global(cx, m_ScriptInterface->GetGlobalObject()); 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; } int CMapGeneratorWorker::GetProgress() { - CScopeLock lock(m_WorkerMutex); + std::lock_guard lock(m_WorkerMutex); return m_Progress; } shared_ptr CMapGeneratorWorker::GetResults() { - CScopeLock lock(m_WorkerMutex); + std::lock_guard lock(m_WorkerMutex); return m_MapData; } bool CMapGeneratorWorker::LoadLibrary(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); return self->LoadScripts(name); } void CMapGeneratorWorker::ExportMap(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue data) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); // Copy results - CScopeLock lock(self->m_WorkerMutex); + std::lock_guard lock(self->m_WorkerMutex); self->m_MapData = self->m_ScriptInterface->WriteStructuredClone(data); self->m_Progress = 0; } void CMapGeneratorWorker::SetProgress(ScriptInterface::CxPrivate* pCxPrivate, int progress) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); // Copy data - CScopeLock lock(self->m_WorkerMutex); + 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::CxPrivate* pCxPrivate, const std::string& templateName) { CMapGeneratorWorker* self = static_cast(pCxPrivate->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::CxPrivate* pCxPrivate, const std::string& templateName) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); return self->m_TemplateLoader.TemplateExists(templateName); } std::vector CMapGeneratorWorker::FindTemplates(ScriptInterface::CxPrivate* pCxPrivate, const std::string& path, bool includeSubdirectories) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); return self->m_TemplateLoader.FindTemplates(path, includeSubdirectories, SIMULATION_TEMPLATES); } std::vector CMapGeneratorWorker::FindActorTemplates(ScriptInterface::CxPrivate* pCxPrivate, const std::string& path, bool includeSubdirectories) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); return self->m_TemplateLoader.FindTemplates(path, includeSubdirectories, ACTOR_TEMPLATES); } int CMapGeneratorWorker::GetTerrainTileSize(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return TERRAIN_TILE_SIZE; } bool CMapGeneratorWorker::LoadScripts(const std::wstring& 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 = L"maps/random/" + libraryName + L"/"; 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::CxPrivate* pCxPrivate, const std::wstring& vfsPath) { std::vector heightmap; if (LoadHeightmapImageVfs(vfsPath, heightmap) != INFO::OK) { LOGERROR("Could not load heightmap file '%s'", utf8_from_wstring(vfsPath).c_str()); return JS::UndefinedValue(); } CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); JSContext* cx = self->m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue returnValue(cx); ToJSVal_vector(cx, &returnValue, heightmap); return returnValue; } // See CMapReader::UnpackTerrain, CMapReader::ParseTerrain for the reordering JS::Value CMapGeneratorWorker::LoadMapTerrain(ScriptInterface::CxPrivate* pCxPrivate, const std::string& filename) { if (!VfsFileExists(filename)) throw PSERROR_File_OpenFailed(); CFileUnpacker unpacker; unpacker.Read(filename, "PSMP"); if (unpacker.GetVersion() < CMapIO::FILE_READ_VERSION) throw PSERROR_File_InvalidVersion(); // 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); } } CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); JSContext* cx = self->m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue returnValue(cx); self->m_ScriptInterface->Eval("({})", &returnValue); self->m_ScriptInterface->SetProperty(returnValue, "height", heightmap); self->m_ScriptInterface->SetProperty(returnValue, "textureNames", textureNames); self->m_ScriptInterface->SetProperty(returnValue, "textureIDs", textureIDs); return returnValue; } ////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////// CMapGenerator::CMapGenerator() : m_Worker(new CMapGeneratorWorker()) { } 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(); } shared_ptr CMapGenerator::GetResults() { return m_Worker->GetResults(); } Index: ps/trunk/source/tools/atlas/GameInterface/MessagesSetup.h =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/MessagesSetup.h (revision 22343) +++ ps/trunk/source/tools/atlas/GameInterface/MessagesSetup.h (revision 22344) @@ -1,215 +1,213 @@ -/* Copyright (C) 2009 Wildfire Games. +/* 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 . */ // Used by Messages.h, so that file stays relatively clean. #ifndef MESSAGESSETUP_NOTFIRST #define MESSAGESSETUP_NOTFIRST #include "MessagePasser.h" #include "SharedTypes.h" #include "Shareable.h" // Structures in this file are passed over the DLL boundary, so some // carefulness and/or luck is required... -class CMutex; - namespace AtlasMessage { struct IMessage { virtual const char* GetName() const = 0; virtual ~IMessage() {} enum Type { Message, Query }; virtual Type GetType() const = 0; }; #define MESSAGESTRUCT(t) \ struct m##t : public IMessage { \ virtual const char* GetName() const { return #t; } \ virtual Type GetType() const { return IMessage::Message; } \ private: \ const m##t& operator=(const m##t&); \ public: // Messages for doing/undoing/etc world-altering commands MESSAGESTRUCT(WorldCommand) mWorldCommand() {} virtual void* CloneData() const = 0; virtual bool IsMergeable() const = 0; }; MESSAGESTRUCT(DoCommand) mDoCommand(mWorldCommand* c) : name(c->GetName()), data(c->CloneData()) {} const Shareable name; const Shareable data; // 'data' gets deallocated by ~cWhatever in the game thread }; MESSAGESTRUCT(UndoCommand) }; MESSAGESTRUCT(RedoCommand) }; MESSAGESTRUCT(MergeCommand) }; struct QueryMessage : public IMessage { Type GetType() const { return IMessage::Query; } void Post(); // defined in ScenarioEditor.cpp void* m_Semaphore; // for use by MessagePasser implementations (yay encapsulation) }; #define QUERYSTRUCT(t) \ struct q##t : public QueryMessage { \ const char* GetName() const { return #t; } \ private: \ const q##t& operator=(const q##t&); \ public: const bool MERGE = true; const bool NOMERGE = false; #define COMMANDDATASTRUCT(t) \ struct d##t { \ private: \ const d##t& operator=(const d##t&); \ public: #define COMMANDSTRUCT(t, merge) \ struct m##t : public mWorldCommand, public d##t { \ m##t(const d##t& d) : d##t(d) {} \ const char* GetName() const { return #t; } \ virtual bool IsMergeable() const { return merge; } \ void* CloneData() const { return SHAREABLE_NEW(d##t, (*this)); } \ private: \ const m##t& operator=(const m##t&);\ } #include #include #include #include #define B_TYPE(elem) BOOST_PP_TUPLE_ELEM(2, 0, elem) #define B_NAME(elem) BOOST_PP_TUPLE_ELEM(2, 1, elem) #define B_CONSTRUCTORARGS(r, data, n, elem) BOOST_PP_COMMA_IF(n) B_TYPE(elem) BOOST_PP_CAT(B_NAME(elem),_) #define B_CONSTRUCTORTYPES(r, data, n, elem) BOOST_PP_COMMA_IF(n) B_TYPE(elem) #define B_CONSTRUCTORINIT(r, data, n, elem) BOOST_PP_COMMA_IF(n) B_NAME(elem)(BOOST_PP_CAT(B_NAME(elem),_)) #define B_CONSTMEMBERS(r, data, n, elem) const Shareable< B_TYPE(elem) > B_NAME(elem); #define B_MEMBERS(r, data, n, elem) Shareable< B_TYPE(elem) > B_NAME(elem); /* For each message type, generate something roughly like: struct mBlah : public IMessage { const char* GetName() const { return "Blah"; } mBlah(int in0_, bool in1_) : in0(in0_), in1(in1_) {} static mBlah* CtorType (int, bool) { return NULL; } // This doesn't do anything useful - it's just to make template-writing easier const Shareable in0; const Shareable in1; } */ #define MESSAGE_WITH_INPUTS(name, vals) \ MESSAGESTRUCT(name) \ m##name( BOOST_PP_SEQ_FOR_EACH_I(B_CONSTRUCTORARGS, ~, vals) ) \ : BOOST_PP_SEQ_FOR_EACH_I(B_CONSTRUCTORINIT, ~, vals) {} \ static m##name* CtorType( BOOST_PP_SEQ_FOR_EACH_I(B_CONSTRUCTORTYPES, ~, vals) ) { return NULL; } \ BOOST_PP_SEQ_FOR_EACH_I(B_CONSTMEMBERS, ~, vals) \ } #define MESSAGE_WITHOUT_INPUTS(name, vals) \ MESSAGESTRUCT(name) \ m##name() {} \ static m##name* CtorType() { return NULL; } \ } #define MESSAGE(name, vals) \ BOOST_PP_IIF( \ BOOST_PP_EQUAL(BOOST_PP_SEQ_SIZE((~)vals), 1), \ MESSAGE_WITHOUT_INPUTS, \ MESSAGE_WITH_INPUTS) \ (name, vals) #define COMMAND(name, merge, vals) \ COMMANDDATASTRUCT(name) \ d##name( BOOST_PP_SEQ_FOR_EACH_I(B_CONSTRUCTORARGS, ~, vals) ) \ : BOOST_PP_SEQ_FOR_EACH_I(B_CONSTRUCTORINIT, ~, vals) {} \ BOOST_PP_SEQ_FOR_EACH_I(B_CONSTMEMBERS, ~, vals) \ }; \ COMMANDSTRUCT(name, merge) // Need different syntax depending on whether there are some input values in the query: #define QUERY_WITHOUT_INPUTS(name, in_vals, out_vals) \ QUERYSTRUCT(name) \ q##name() {} \ static q##name* CtorType() { return NULL; } \ BOOST_PP_SEQ_FOR_EACH_I(B_MEMBERS, ~, out_vals) /* other members */ \ } #define QUERY_WITH_INPUTS(name, in_vals, out_vals) \ QUERYSTRUCT(name) \ q##name( BOOST_PP_SEQ_FOR_EACH_I(B_CONSTRUCTORARGS, ~, in_vals) ) \ : BOOST_PP_SEQ_FOR_EACH_I(B_CONSTRUCTORINIT, ~, in_vals) {} \ static q##name* CtorType( BOOST_PP_SEQ_FOR_EACH_I(B_CONSTRUCTORTYPES, ~, in_vals) ) { return NULL; } \ BOOST_PP_SEQ_FOR_EACH_I(B_CONSTMEMBERS, ~, in_vals) \ BOOST_PP_SEQ_FOR_EACH_I(B_MEMBERS, ~, out_vals) \ } #define QUERY(name, in_vals, out_vals) \ BOOST_PP_IIF( \ BOOST_PP_EQUAL(BOOST_PP_SEQ_SIZE((~)in_vals), 1), \ QUERY_WITHOUT_INPUTS, \ QUERY_WITH_INPUTS) \ (name, in_vals, out_vals) ////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////// #else // MESSAGESSETUP_NOTFIRST => clean up the mess #undef MESSAGESTRUCT #undef QUERYSTRUCT #undef COMMANDDATASTRUCT #undef COMMANDSTRUCT #undef B_TYPE #undef B_NAME #undef B_CONSTRUCTORARGS #undef B_CONSTRUCTORTYPES #undef B_CONSTRUCTORINIT #undef B_CONSTMEMBERS #undef B_MEMBERS #undef MESSAGE #undef COMMAND #undef QUERY_WITHOUT_INPUTS #undef QUERY_WITH_INPUTS #undef QUERY } #endif Index: ps/trunk/source/graphics/MapGenerator.h =================================================================== --- ps/trunk/source/graphics/MapGenerator.h (revision 22343) +++ ps/trunk/source/graphics/MapGenerator.h (revision 22344) @@ -1,154 +1,155 @@ -/* Copyright (C) 2018 Wildfire Games. +/* 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 . */ #ifndef INCLUDED_MAPGENERATOR #define INCLUDED_MAPGENERATOR +#include "lib/posix/posix_pthread.h" #include "ps/FileIo.h" -#include "ps/ThreadUtil.h" #include "ps/TemplateLoader.h" #include "scriptinterface/ScriptInterface.h" #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 */ shared_ptr 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/runtimes. */ class CMapGeneratorWorker { public: CMapGeneratorWorker(); ~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 */ shared_ptr GetResults(); private: // Mapgen /** * Load all scripts of the given library * * @param libraryName String 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 std::wstring& libraryName); // callbacks for script functions static bool LoadLibrary(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name); static void ExportMap(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue data); static JS::Value LoadHeightmap(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& src); static JS::Value LoadMapTerrain(ScriptInterface::CxPrivate* pCxPrivate, const std::string& filename); static void SetProgress(ScriptInterface::CxPrivate* pCxPrivate, int progress); static CParamNode GetTemplate(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName); static bool TemplateExists(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName); static std::vector FindTemplates(ScriptInterface::CxPrivate* pCxPrivate, const std::string& path, bool includeSubdirectories); static std::vector FindActorTemplates(ScriptInterface::CxPrivate* pCxPrivate, const std::string& path, bool includeSubdirectories); static int GetTerrainTileSize(ScriptInterface::CxPrivate* pCxPrivate); std::set m_LoadedLibraries; shared_ptr m_MapData; boost::rand48 m_MapGenRNG; int m_Progress; ScriptInterface* m_ScriptInterface; VfsPath m_ScriptPath; std::string m_Settings; CTemplateLoader m_TemplateLoader; // Thread static void* RunThread(void* data); bool Run(); pthread_t m_WorkerThread; - CMutex m_WorkerMutex; + std::mutex m_WorkerMutex; }; #endif //INCLUDED_MAPGENERATOR Index: ps/trunk/source/network/NetServer.cpp =================================================================== --- ps/trunk/source/network/NetServer.cpp (revision 22343) +++ ps/trunk/source/network/NetServer.cpp (revision 22344) @@ -1,1626 +1,1625 @@ /* 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 "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/ThreadUtil.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptRuntime.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" static const 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 { - CScopeLock lock(m_WorkerMutex); + std::lock_guard lock(m_WorkerMutex); m_Shutdown = true; } // Wait for it to shut down cleanly pthread_join(m_WorkerThread, NULL); } // 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 int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this); ENSURE(ret == 0); #if CONFIG2_MINIUPNPC // Launch the UPnP thread ret = pthread_create(&m_UPnPThread, NULL, &SetupUPnP, NULL); ENSURE(ret == 0); #endif return true; } #if CONFIG2_MINIUPNPC void* CNetServerWorker::SetupUPnP(void*) { // 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. struct UPNPUrls urls; struct IGDdatas data; struct UPNPDev* devlist = NULL; // 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; bool allocatedUrls = false; // 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."); return NULL; } 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)); return NULL; } 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)); return NULL; } // 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)); return NULL; } 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); // Make sure everything is properly freed. if (allocatedUrls) FreeUPNPUrls(&urls); freeUPNPDevlist(devlist); return NULL; } #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(), session->GetCurrState()) != targetStates.end() && !session->SendMessage(message)) ok = false; return ok; } void* CNetServerWorker::RunThread(void* data) { debug_SetThreadName("NetServer"); static_cast(data)->Run(); return NULL; } void CNetServerWorker::Run() { // The script runtime uses the profiler and therefore the thread must be registered before the runtime is created g_Profiler2.RegisterCurrentThread("Net server"); // To avoid the need for JS_SetContextThread, we create and use and destroy // the script interface entirely within this network thread m_ScriptInterface = new ScriptInterface("Engine", "Net server", ScriptInterface::CreateRuntime(g_ScriptRuntime)); m_GameAttributes.init(m_ScriptInterface->GetJSRuntime(), 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->GetRuntime()->MaybeIncrementalGC(0.5f); JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); std::vector newStartGame; std::vector newGameAttributes; std::vector> newLobbyAuths; std::vector newTurnLength; { - CScopeLock lock(m_WorkerMutex); + 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(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_UNKNOWN); 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); session->SetLongTimeout(true); 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(); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue settings(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(); loadedSession->SetLongTimeout(false); // 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); } session->SetLongTimeout(false); 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* session : server.m_Sessions) { if (session->GetCurrState() == NSS_INGAME && message->m_GUID != session->GetGUID()) session->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 session->SetLongTimeout(true); } 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) { StunClient::SendHolePunchingMessages(m_Host, ipStr.c_str(), 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() { - CScopeLock lock(m_Worker->m_WorkerMutex); + 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); - CScopeLock lock(m_Worker->m_WorkerMutex); + std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->m_GameAttributesQueue.push_back(attrsJSON); } void CNetServer::OnLobbyAuth(const CStr& name, const CStr& token) { - CScopeLock lock(m_Worker->m_WorkerMutex); + std::lock_guard lock(m_Worker->m_WorkerMutex); m_Worker->m_LobbyAuthQueue.push_back(std::make_pair(name, token)); } void CNetServer::SetTurnLength(u32 msecs) { - CScopeLock lock(m_Worker->m_WorkerMutex); + 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/network/NetServer.h =================================================================== --- ps/trunk/source/network/NetServer.h (revision 22343) +++ ps/trunk/source/network/NetServer.h (revision 22344) @@ -1,386 +1,387 @@ /* 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 . */ #ifndef NETSERVER_H #define NETSERVER_H #include "NetFileTransfer.h" #include "NetHost.h" #include "lib/config2.h" +#include "lib/posix/posix_pthread.h" #include "lib/types.h" -#include "ps/ThreadUtil.h" #include "scriptinterface/ScriptTypes.h" +#include #include #include #include class CNetServerSession; class CNetServerTurnManager; class CFsmEvent; class ScriptInterface; class CPlayerAssignmentMessage; class CNetStatsTable; class CSimulationMessage; class CNetServerWorker; enum NetServerState { // We haven't opened the port yet, we're just setting some stuff up. // The worker thread has not been started. SERVER_STATE_UNCONNECTED, // The server is open and accepting connections. This is the screen where // rules are set up by the operator and where players join and select civs // and stuff. SERVER_STATE_PREGAME, // All the hosts are connected and are loading the game SERVER_STATE_LOADING, // The one with all the killing ;-) SERVER_STATE_INGAME, // The game is over and someone has won. Players might linger to chat or // download the replay log. SERVER_STATE_POSTGAME }; /** * Server session representation of client state */ enum NetServerSessionState { // The client has disconnected or been disconnected NSS_UNCONNECTED, // The client has just connected and we're waiting for its handshake message, // to agree on the protocol version NSS_HANDSHAKE, // The client has handshook and we're waiting for its lobby authentication message NSS_LOBBY_AUTHENTICATE, // The client has handshook and we're waiting for its authentication message, // to find its name and check its password etc NSS_AUTHENTICATE, // The client has fully joined, and is in the pregame setup stage // or is loading the game. // Server must be in SERVER_STATE_PREGAME or SERVER_STATE_LOADING. NSS_PREGAME, // The client has authenticated but the game was already started, // so it's synchronising with the game state from other clients NSS_JOIN_SYNCING, // The client is running the game. // Server must be in SERVER_STATE_LOADING or SERVER_STATE_INGAME. NSS_INGAME }; /** * Network server interface. Handles all the coordination between players. * One person runs this object, and every player (including the host) connects their CNetClient to it. * * The actual work is performed by CNetServerWorker in a separate thread. */ class CNetServer { NONCOPYABLE(CNetServer); public: /** * Construct a new network server. * @param autostartPlayers if positive then StartGame will be called automatically * once this many players are connected (intended for the command-line testing mode). */ CNetServer(bool useLobbyAuth = false, int autostartPlayers = -1); ~CNetServer(); /** * Begin listening for network connections. * This function is synchronous (it won't return until the connection is established). * @return true on success, false on error (e.g. port already in use) */ bool SetupConnection(const u16 port); /** * Call from the GUI to asynchronously notify all clients that they should start loading the game. */ void StartGame(); /** * Call from the GUI to update the game setup attributes. * This must be called at least once before starting the game. * The changes will be asynchronously propagated to all clients. * @param attrs game attributes, in the script context of scriptInterface */ void UpdateGameAttributes(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface); /** * Set the turn length to a fixed value. * TODO: we should replace this with some adapative lag-dependent computation. */ void SetTurnLength(u32 msecs); bool UseLobbyAuth() const; void OnLobbyAuth(const CStr& name, const CStr& token); void SendHolePunchingMessage(const CStr& ip, u16 port); private: CNetServerWorker* m_Worker; const bool m_LobbyAuth; }; /** * Network server worker thread. * (This is run in a thread so that client/server communication is not delayed * by the host player's framerate - the only delay should be the network latency.) * * Thread-safety: * - SetupConnection and constructor/destructor must be called from the main thread. * - The main thread may push commands onto the Queue members, * while holding the m_WorkerMutex lock. * - Public functions (SendMessage, Broadcast) must be called from the network * server thread. */ class CNetServerWorker { NONCOPYABLE(CNetServerWorker); public: // Public functions for CNetSession/CNetServerTurnManager to use: /** * Send a message to the given network peer. */ bool SendMessage(ENetPeer* peer, const CNetMessage* message); /** * Disconnects a player from gamesetup or session. */ void KickPlayer(const CStrW& playerName, const bool ban); /** * Send a message to all clients who match one of the given states. */ bool Broadcast(const CNetMessage* message, const std::vector& targetStates); private: friend class CNetServer; friend class CNetFileReceiveTask_ServerRejoin; CNetServerWorker(bool useLobbyAuth, int autostartPlayers); ~CNetServerWorker(); /** * Begin listening for network connections. * @return true on success, false on error (e.g. port already in use) */ bool SetupConnection(const u16 port); /** * Call from the GUI to update the player assignments. * The given GUID will be (re)assigned to the given player ID. * Any player currently using that ID will be unassigned. * The changes will be propagated to all clients. */ void AssignPlayer(int playerID, const CStr& guid); /** * Call from the GUI to notify all clients that they should start loading the game. */ void StartGame(); /** * Call from the GUI to update the game setup attributes. * This must be called at least once before starting the game. * The changes will be propagated to all clients. * @param attrs game attributes, in the script context of GetScriptInterface() */ void UpdateGameAttributes(JS::MutableHandleValue attrs); /** * Make a player name 'nicer' by limiting the length and removing forbidden characters etc. */ static CStrW SanitisePlayerName(const CStrW& original); /** * Make a player name unique, if it matches any existing session's name. */ CStrW DeduplicatePlayerName(const CStrW& original); /** * Get the script context used for game attributes. */ const ScriptInterface& GetScriptInterface(); /** * Set the turn length to a fixed value. * TODO: we should replace this with some adaptive lag-dependent computation. */ void SetTurnLength(u32 msecs); void ProcessLobbyAuth(const CStr& name, const CStr& token); void AddPlayer(const CStr& guid, const CStrW& name); void RemovePlayer(const CStr& guid); void SendPlayerAssignments(); void ClearAllPlayerReady(); void SetupSession(CNetServerSession* session); bool HandleConnect(CNetServerSession* session); void OnUserJoin(CNetServerSession* session); void OnUserLeave(CNetServerSession* session); static bool OnClientHandshake(void* context, CFsmEvent* event); static bool OnAuthenticate(void* context, CFsmEvent* event); static bool OnSimulationCommand(void* context, CFsmEvent* event); static bool OnSyncCheck(void* context, CFsmEvent* event); static bool OnEndCommandBatch(void* context, CFsmEvent* event); static bool OnChat(void* context, CFsmEvent* event); static bool OnReady(void* context, CFsmEvent* event); static bool OnClearAllReady(void* context, CFsmEvent* event); static bool OnGameSetup(void* context, CFsmEvent* event); static bool OnAssignPlayer(void* context, CFsmEvent* event); static bool OnStartGame(void* context, CFsmEvent* event); static bool OnLoadedGame(void* context, CFsmEvent* event); static bool OnJoinSyncingLoadedGame(void* context, CFsmEvent* event); static bool OnRejoined(void* context, CFsmEvent* event); static bool OnKickPlayer(void* context, CFsmEvent* event); static bool OnDisconnect(void* context, CFsmEvent* event); static bool OnClientPaused(void* context, CFsmEvent* event); /** * Checks if all clients have finished loading. * If so informs the clients about that and change the server state. * * Returns if all clients finished loading. */ bool CheckGameLoadStatus(CNetServerSession* changedSession); void ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message); void HandleMessageReceive(const CNetMessage* message, CNetServerSession* session); /** * Send a network warning if the connection to a client is being lost or has bad latency. */ void CheckClientConnections(); void SendHolePunchingMessage(const CStr& ip, u16 port); /** * Internal script context for (de)serializing script messages, * and for storing game attributes. * (TODO: we shouldn't bother deserializing (except for debug printing of messages), * we should just forward messages blindly and efficiently.) */ ScriptInterface* m_ScriptInterface; PlayerAssignmentMap m_PlayerAssignments; /** * Stores the most current game attributes. */ JS::PersistentRootedValue m_GameAttributes; int m_AutostartPlayers; /** * Whether this match requires lobby authentication. */ const bool m_LobbyAuth; ENetHost* m_Host; std::vector m_Sessions; CNetStatsTable* m_Stats; NetServerState m_State; CStrW m_ServerName; std::vector m_BannedIPs; std::vector m_BannedPlayers; /** * Holds the GUIDs of all currently paused players. */ std::vector m_PausingPlayers; u32 m_NextHostID; CNetServerTurnManager* m_ServerTurnManager; CStr m_HostGUID; /** * A copy of all simulation commands received so far, indexed by * turn number, to simplify support for rejoining etc. * TODO: verify this doesn't use too much RAM. */ std::vector> m_SavedCommands; /** * The latest copy of the simulation state, received from an existing * client when a new client has asked to rejoin the game. */ std::string m_JoinSyncFile; /** * Time when the clients connections were last checked for timeouts and latency. */ std::time_t m_LastConnectionCheck; private: // Thread-related stuff: #if CONFIG2_MINIUPNPC /** * Try to find a UPnP root on the network and setup port forwarding. */ static void* SetupUPnP(void*); pthread_t m_UPnPThread; #endif static void* RunThread(void* data); void Run(); bool RunStep(); pthread_t m_WorkerThread; - CMutex m_WorkerMutex; + std::mutex m_WorkerMutex; // protected by m_WorkerMutex bool m_Shutdown; // Queues for messages sent by the game thread (protected by m_WorkerMutex): std::vector m_StartGameQueue; std::vector m_GameAttributesQueue; std::vector> m_LobbyAuthQueue; std::vector m_TurnLengthQueue; }; /// Global network server for the standard game extern CNetServer *g_NetServer; #endif // NETSERVER_H Index: ps/trunk/source/network/NetStats.cpp =================================================================== --- ps/trunk/source/network/NetStats.cpp (revision 22343) +++ ps/trunk/source/network/NetStats.cpp (revision 22344) @@ -1,152 +1,152 @@ -/* Copyright (C) 2016 Wildfire Games. +/* 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 "NetStats.h" #include "lib/external_libraries/enet.h" enum { Row_InData, Row_OutData, Row_LastSendTime, Row_LastRecvTime, Row_NextTimeout, Row_PacketsSent, Row_PacketsLost, Row_LastRTT, Row_RTT, Row_MTU, Row_ReliableInTransit, NumberRows }; CNetStatsTable::CNetStatsTable(const ENetPeer* peer) : m_Peer(peer) { } CNetStatsTable::CNetStatsTable() : m_Peer(NULL) { } CStr CNetStatsTable::GetName() { return "net"; } CStr CNetStatsTable::GetTitle() { if (m_Peer) return "Network client statistics"; else return "Network host statistics"; } size_t CNetStatsTable::GetNumberRows() { return NumberRows; } const std::vector& CNetStatsTable::GetColumns() { m_ColumnDescriptions.clear(); m_ColumnDescriptions.push_back(ProfileColumn("Name", 200)); if (m_Peer) m_ColumnDescriptions.push_back(ProfileColumn("Value", 80)); else { - CScopeLock lock(m_Mutex); + std::lock_guard lock(m_Mutex); for (size_t i = 0; i < m_LatchedData.size(); ++i) m_ColumnDescriptions.push_back(ProfileColumn("Peer "+CStr::FromUInt(i), 80)); } return m_ColumnDescriptions; } CStr CNetStatsTable::GetCellText(size_t row, size_t col) { // Return latched data, if we have any { - CScopeLock lock(m_Mutex); + std::lock_guard lock(m_Mutex); if (col > 0 && m_LatchedData.size() > col-1 && m_LatchedData[col-1].size() > row) return m_LatchedData[col-1][row]; } #define ROW(id, title, member) \ case id: \ if (col == 0) return title; \ if (m_Peer) return CStr::FromUInt(m_Peer->member); \ return "???" switch(row) { ROW(Row_InData, "incoming bytes", incomingDataTotal); ROW(Row_OutData, "outgoing bytes", outgoingDataTotal); ROW(Row_LastSendTime, "last send time", lastSendTime); ROW(Row_LastRecvTime, "last receive time", lastReceiveTime); ROW(Row_NextTimeout, "next timeout", nextTimeout); ROW(Row_PacketsSent, "packets sent", packetsSent); ROW(Row_PacketsLost, "packets lost", packetsLost); ROW(Row_LastRTT, "last RTT", lastRoundTripTime); ROW(Row_RTT, "mean RTT", roundTripTime); ROW(Row_MTU, "MTU", mtu); ROW(Row_ReliableInTransit, "reliable data in transit", reliableDataInTransit); default: return "???"; } #undef ROW } AbstractProfileTable* CNetStatsTable::GetChild(size_t UNUSED(row)) { return 0; } void CNetStatsTable::LatchHostState(const ENetHost* host) { - CScopeLock lock(m_Mutex); + std::lock_guard lock(m_Mutex); #define ROW(id, title, member) \ m_LatchedData[i].push_back(CStr::FromUInt(host->peers[i].member)); m_LatchedData.clear(); m_LatchedData.resize(host->peerCount); for (size_t i = 0; i < host->peerCount; ++i) { ROW(Row_InData, "incoming bytes", incomingDataTotal); ROW(Row_OutData, "outgoing bytes", outgoingDataTotal); ROW(Row_LastSendTime, "last send time", lastSendTime); ROW(Row_LastRecvTime, "last receive time", lastReceiveTime); ROW(Row_NextTimeout, "next timeout", nextTimeout); ROW(Row_PacketsSent, "packets sent", packetsSent); ROW(Row_PacketsLost, "packets lost", packetsLost); ROW(Row_LastRTT, "last RTT", lastRoundTripTime); ROW(Row_RTT, "mean RTT", roundTripTime); ROW(Row_MTU, "MTU", mtu); ROW(Row_ReliableInTransit, "reliable data in transit", reliableDataInTransit); } #undef ROW } Index: ps/trunk/source/network/NetStats.h =================================================================== --- ps/trunk/source/network/NetStats.h (revision 22343) +++ ps/trunk/source/network/NetStats.h (revision 22344) @@ -1,60 +1,61 @@ -/* Copyright (C) 2016 Wildfire Games. +/* 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 . */ #ifndef INCLUDED_NETSTATS #define INCLUDED_NETSTATS #include "ps/ProfileViewer.h" -#include "ps/ThreadUtil.h" + +#include typedef struct _ENetPeer ENetPeer; typedef struct _ENetHost ENetHost; /** * ENet connection statistics profiler table. * * Thread-safety: * - Must be constructed in the main thread (to match the profiler). * - In host mode, the host can be running in a separate thread; * call LatchHostState from that thread periodically to safely * update our displayed copy of the data. */ class CNetStatsTable : public AbstractProfileTable { NONCOPYABLE(CNetStatsTable); public: CNetStatsTable(); CNetStatsTable(const ENetPeer* peer); 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); void LatchHostState(const ENetHost* host); private: const ENetPeer* m_Peer; std::vector m_ColumnDescriptions; - CMutex m_Mutex; + std::mutex m_Mutex; std::vector> m_LatchedData; // protected by m_Mutex }; #endif // INCLUDED_NETSTATS Index: ps/trunk/source/ps/CConsole.cpp =================================================================== --- ps/trunk/source/ps/CConsole.cpp (revision 22343) +++ ps/trunk/source/ps/CConsole.cpp (revision 22344) @@ -1,695 +1,695 @@ -/* Copyright (C) 2016 Wildfire Games. +/* 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 . */ /* * Implements the in-game console with scripting support. */ #include "precompiled.h" #include #include "CConsole.h" #include "graphics/FontMetrics.h" #include "graphics/ShaderManager.h" #include "graphics/TextRenderer.h" #include "gui/GUIutil.h" #include "gui/GUIManager.h" #include "lib/ogl.h" #include "lib/sysdep/clipboard.h" #include "lib/timer.h" #include "lib/utf8.h" #include "maths/MathUtil.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/GameSetup/Config.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "renderer/Renderer.h" #include "scriptinterface/ScriptInterface.h" CConsole* g_Console = 0; CConsole::CConsole() { m_bToggle = false; m_bVisible = false; m_fVisibleFrac = 0.0f; m_szBuffer = new wchar_t[CONSOLE_BUFFER_SIZE]; FlushBuffer(); m_iMsgHistPos = 1; m_charsPerPage = 0; m_prevTime = 0.0; m_bCursorVisState = true; m_cursorBlinkRate = 0.5; InsertMessage("[ 0 A.D. Console v0.14 ]"); InsertMessage(""); } CConsole::~CConsole() { delete[] m_szBuffer; } void CConsole::SetSize(float X, float Y, float W, float H) { m_fX = X; m_fY = Y; m_fWidth = W; m_fHeight = H; } void CConsole::UpdateScreenSize(int w, int h) { float height = h * 0.6f; SetSize(0, 0, w / g_GuiScale, height / g_GuiScale); } void CConsole::ToggleVisible() { m_bToggle = true; m_bVisible = !m_bVisible; // TODO: this should be based on input focus, not visibility if (m_bVisible) SDL_StartTextInput(); else SDL_StopTextInput(); } void CConsole::SetVisible(bool visible) { if (visible != m_bVisible) m_bToggle = true; m_bVisible = visible; if (visible) { m_prevTime = 0.0; m_bCursorVisState = false; } } void CConsole::SetCursorBlinkRate(double rate) { m_cursorBlinkRate = rate; } void CConsole::FlushBuffer() { // Clear the buffer and set the cursor and length to 0 memset(m_szBuffer, '\0', sizeof(wchar_t) * CONSOLE_BUFFER_SIZE); m_iBufferPos = m_iBufferLength = 0; } void CConsole::Update(const float deltaRealTime) { if(m_bToggle) { const float AnimateTime = .30f; const float Delta = deltaRealTime / AnimateTime; if(m_bVisible) { m_fVisibleFrac += Delta; if(m_fVisibleFrac > 1.0f) { m_fVisibleFrac = 1.0f; m_bToggle = false; } } else { m_fVisibleFrac -= Delta; if(m_fVisibleFrac < 0.0f) { m_fVisibleFrac = 0.0f; m_bToggle = false; } } } } //Render Manager. void CConsole::Render() { if (! (m_bVisible || m_bToggle) ) return; PROFILE3_GPU("console"); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); CShaderTechniquePtr solidTech = g_Renderer.GetShaderManager().LoadEffect(str_gui_solid); solidTech->BeginPass(); CShaderProgramPtr solidShader = solidTech->GetShader(); CMatrix3D transform = GetDefaultGuiMatrix(); // animation: slide in from top of screen const float DeltaY = (1.0f - m_fVisibleFrac) * m_fHeight; transform.PostTranslate(m_fX, m_fY - DeltaY, 0.0f); // move to window position solidShader->Uniform(str_transform, transform); DrawWindow(solidShader); solidTech->EndPass(); CShaderTechniquePtr textTech = g_Renderer.GetShaderManager().LoadEffect(str_gui_text); textTech->BeginPass(); CTextRenderer textRenderer(textTech->GetShader()); textRenderer.Font(CStrIntern(CONSOLE_FONT)); textRenderer.SetTransform(transform); DrawHistory(textRenderer); DrawBuffer(textRenderer); textRenderer.Render(); textTech->EndPass(); glDisable(GL_BLEND); } void CConsole::DrawWindow(CShaderProgramPtr& shader) { float boxVerts[] = { m_fWidth, 0.0f, 1.0f, 0.0f, 1.0f, m_fHeight-1.0f, m_fWidth, m_fHeight-1.0f }; shader->VertexPointer(2, GL_FLOAT, 0, boxVerts); // Draw Background // Set the color to a translucent blue shader->Uniform(str_color, 0.0f, 0.0f, 0.5f, 0.6f); shader->AssertPointersBound(); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); // Draw Border // Set the color to a translucent yellow shader->Uniform(str_color, 0.5f, 0.5f, 0.0f, 0.6f); shader->AssertPointersBound(); glDrawArrays(GL_LINE_LOOP, 0, 4); if (m_fHeight > m_iFontHeight + 4) { float lineVerts[] = { 0.0f, m_fHeight - (float)m_iFontHeight - 4.0f, m_fWidth, m_fHeight - (float)m_iFontHeight - 4.0f }; shader->VertexPointer(2, GL_FLOAT, 0, lineVerts); shader->AssertPointersBound(); glDrawArrays(GL_LINES, 0, 2); } } void CConsole::DrawHistory(CTextRenderer& textRenderer) { int i = 1; std::deque::iterator Iter; //History iterator - CScopeLock lock(m_Mutex); // needed for safe access to m_deqMsgHistory + std::lock_guard lock(m_Mutex); // needed for safe access to m_deqMsgHistory textRenderer.Color(1.0f, 1.0f, 1.0f); for (Iter = m_deqMsgHistory.begin(); Iter != m_deqMsgHistory.end() && (((i - m_iMsgHistPos + 1) * m_iFontHeight) < m_fHeight); ++Iter) { if (i >= m_iMsgHistPos) textRenderer.Put(9.0f, m_fHeight - (float)m_iFontOffset - (float)m_iFontHeight * (i - m_iMsgHistPos + 1), Iter->c_str()); i++; } } // Renders the buffer to the screen. void CConsole::DrawBuffer(CTextRenderer& textRenderer) { if (m_fHeight < m_iFontHeight) return; CMatrix3D savedTransform = textRenderer.GetTransform(); textRenderer.Translate(2.0f, m_fHeight - (float)m_iFontOffset + 1.0f, 0.0f); textRenderer.Color(1.0f, 1.0f, 0.0f); textRenderer.PutAdvance(L"]"); textRenderer.Color(1.0f, 1.0f, 1.0f); if (m_iBufferPos == 0) DrawCursor(textRenderer); for (int i = 0; i < m_iBufferLength; i++) { textRenderer.PrintfAdvance(L"%lc", m_szBuffer[i]); if (m_iBufferPos-1 == i) DrawCursor(textRenderer); } textRenderer.SetTransform(savedTransform); } void CConsole::DrawCursor(CTextRenderer& textRenderer) { if (m_cursorBlinkRate > 0.0) { // check if the cursor visibility state needs to be changed double currTime = timer_Time(); if ((currTime - m_prevTime) >= m_cursorBlinkRate) { m_bCursorVisState = !m_bCursorVisState; m_prevTime = currTime; } } else { // Should always be visible m_bCursorVisState = true; } if(m_bCursorVisState) { // Slightly translucent yellow textRenderer.Color(1.0f, 1.0f, 0.0f, 0.8f); // Cursor character is chosen to be an underscore textRenderer.Put(0.0f, 0.0f, L"_"); // Revert to the standard text color textRenderer.Color(1.0f, 1.0f, 1.0f); } } //Inserts a character into the buffer. void CConsole::InsertChar(const int szChar, const wchar_t cooked) { static int iHistoryPos = -1; if (!m_bVisible) return; switch (szChar) { case SDLK_RETURN: iHistoryPos = -1; m_iMsgHistPos = 1; ProcessBuffer(m_szBuffer); FlushBuffer(); return; case SDLK_TAB: // Auto Complete return; case SDLK_BACKSPACE: if (IsEmpty() || IsBOB()) return; if (m_iBufferPos == m_iBufferLength) m_szBuffer[m_iBufferPos - 1] = '\0'; else { for (int j = m_iBufferPos-1; j < m_iBufferLength-1; j++) m_szBuffer[j] = m_szBuffer[j+1]; // move chars to left m_szBuffer[m_iBufferLength-1] = '\0'; } m_iBufferPos--; m_iBufferLength--; return; case SDLK_DELETE: if (IsEmpty() || IsEOB()) return; if (m_iBufferPos == m_iBufferLength-1) { m_szBuffer[m_iBufferPos] = '\0'; m_iBufferLength--; } else { if (g_keys[SDLK_RCTRL] || g_keys[SDLK_LCTRL]) { // Make Ctrl-Delete delete up to end of line m_szBuffer[m_iBufferPos] = '\0'; m_iBufferLength = m_iBufferPos; } else { // Delete just one char and move the others left for(int j=m_iBufferPos; j lock(m_Mutex); // needed for safe access to m_deqMsgHistory int linesShown = (int)m_fHeight/m_iFontHeight - 4; m_iMsgHistPos = clamp((int)m_deqMsgHistory.size() - linesShown, 1, (int)m_deqMsgHistory.size()); } else { m_iBufferPos = 0; } return; case SDLK_END: if (g_keys[SDLK_RCTRL] || g_keys[SDLK_LCTRL]) { m_iMsgHistPos = 1; } else { m_iBufferPos = m_iBufferLength; } return; case SDLK_LEFT: if (m_iBufferPos) m_iBufferPos--; return; case SDLK_RIGHT: if (m_iBufferPos != m_iBufferLength) m_iBufferPos++; return; // BEGIN: Buffer History Lookup case SDLK_UP: if (m_deqBufHistory.size() && iHistoryPos != (int)m_deqBufHistory.size() - 1) { iHistoryPos++; SetBuffer(m_deqBufHistory.at(iHistoryPos).c_str()); m_iBufferPos = m_iBufferLength; } return; case SDLK_DOWN: if (m_deqBufHistory.size()) { if (iHistoryPos > 0) { iHistoryPos--; SetBuffer(m_deqBufHistory.at(iHistoryPos).c_str()); m_iBufferPos = m_iBufferLength; } else if (iHistoryPos == 0) { iHistoryPos--; FlushBuffer(); } } return; // END: Buffer History Lookup // BEGIN: Message History Lookup case SDLK_PAGEUP: { - CScopeLock lock(m_Mutex); // needed for safe access to m_deqMsgHistory + std::lock_guard lock(m_Mutex); // needed for safe access to m_deqMsgHistory if (m_iMsgHistPos != (int)m_deqMsgHistory.size()) m_iMsgHistPos++; return; } case SDLK_PAGEDOWN: if (m_iMsgHistPos != 1) m_iMsgHistPos--; return; // END: Message History Lookup default: //Insert a character if (IsFull()) return; if (cooked == 0) return; if (IsEOB()) //are we at the end of the buffer? m_szBuffer[m_iBufferPos] = cooked; //cat char onto end else { //we need to insert int i; for(i=m_iBufferLength; i>m_iBufferPos; i--) m_szBuffer[i] = m_szBuffer[i-1]; // move chars to right m_szBuffer[i] = cooked; } m_iBufferPos++; m_iBufferLength++; return; } } void CConsole::InsertMessage(const std::string& message) { // (TODO: this text-wrapping is rubbish since we now use variable-width fonts) //Insert newlines to wraparound text where needed std::wstring wrapAround = wstring_from_utf8(message.c_str()); std::wstring newline(L"\n"); size_t oldNewline=0; size_t distance; //make sure everything has been initialized if ( m_charsPerPage != 0 ) { while ( oldNewline+m_charsPerPage < wrapAround.length() ) { distance = wrapAround.find(newline, oldNewline) - oldNewline; if ( distance > m_charsPerPage ) { oldNewline += m_charsPerPage; wrapAround.insert( oldNewline++, newline ); } else oldNewline += distance+1; } } // Split into lines and add each one individually oldNewline = 0; { - CScopeLock lock(m_Mutex); // needed for safe access to m_deqMsgHistory + std::lock_guard lock(m_Mutex); // needed for safe access to m_deqMsgHistory while ( (distance = wrapAround.find(newline, oldNewline)) != wrapAround.npos) { distance -= oldNewline; m_deqMsgHistory.push_front(wrapAround.substr(oldNewline, distance)); oldNewline += distance+1; } m_deqMsgHistory.push_front(wrapAround.substr(oldNewline)); } } const wchar_t* CConsole::GetBuffer() { m_szBuffer[m_iBufferLength] = 0; return( m_szBuffer ); } void CConsole::SetBuffer(const wchar_t* szMessage) { int oldBufferPos = m_iBufferPos; // remember since FlushBuffer will set it to 0 FlushBuffer(); wcsncpy(m_szBuffer, szMessage, CONSOLE_BUFFER_SIZE); m_szBuffer[CONSOLE_BUFFER_SIZE-1] = 0; m_iBufferLength = (int)wcslen(m_szBuffer); m_iBufferPos = std::min(oldBufferPos, m_iBufferLength); } void CConsole::UseHistoryFile(const VfsPath& filename, int max_history_lines) { m_MaxHistoryLines = max_history_lines; m_sHistoryFile = filename; LoadHistory(); } void CConsole::ProcessBuffer(const wchar_t* szLine) { shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); JSContext* cx = pScriptInterface->GetContext(); JSAutoRequest rq(cx); if (szLine == NULL) return; if (wcslen(szLine) <= 0) return; ENSURE(wcslen(szLine) < CONSOLE_BUFFER_SIZE); m_deqBufHistory.push_front(szLine); SaveHistory(); // Do this each line for the moment; if a script causes // a crash it's a useful record. // Process it as JavaScript JS::RootedValue rval(cx); pScriptInterface->Eval(szLine, &rval); if (!rval.isUndefined()) InsertMessage(pScriptInterface->ToString(&rval)); } void CConsole::LoadHistory() { // note: we don't care if this file doesn't exist or can't be read; // just don't load anything in that case. // do this before LoadFile to avoid an error message if file not found. if (!VfsFileExists(m_sHistoryFile)) return; shared_ptr buf; size_t buflen; if (g_VFS->LoadFile(m_sHistoryFile, buf, buflen) < 0) return; CStr bytes ((char*)buf.get(), buflen); CStrW str (bytes.FromUTF8()); size_t pos = 0; while (pos != CStrW::npos) { pos = str.find('\n'); if (pos != CStrW::npos) { if (pos > 0) m_deqBufHistory.push_front(str.Left(str[pos-1] == '\r' ? pos - 1 : pos)); str = str.substr(pos + 1); } else if (str.length() > 0) m_deqBufHistory.push_front(str); } } void CConsole::SaveHistory() { WriteBuffer buffer; const int linesToSkip = (int)m_deqBufHistory.size() - m_MaxHistoryLines; std::deque::reverse_iterator it = m_deqBufHistory.rbegin(); if(linesToSkip > 0) std::advance(it, linesToSkip); for (; it != m_deqBufHistory.rend(); ++it) { CStr8 line = CStrW(*it).ToUTF8(); buffer.Append(line.data(), line.length()); static const char newline = '\n'; buffer.Append(&newline, 1); } g_VFS->CreateFile(m_sHistoryFile, buffer.Data(), buffer.Size()); } static bool isUnprintableChar(SDL_Keysym key) { switch (key.sym) { // We want to allow some, which are handled specially case SDLK_RETURN: case SDLK_TAB: case SDLK_BACKSPACE: case SDLK_DELETE: case SDLK_HOME: case SDLK_END: case SDLK_LEFT: case SDLK_RIGHT: case SDLK_UP: case SDLK_DOWN: case SDLK_PAGEUP: case SDLK_PAGEDOWN: return false; // Ignore the others default: return true; } } InReaction conInputHandler(const SDL_Event_* ev) { if ((int)ev->ev.type == SDL_HOTKEYDOWN) { std::string hotkey = static_cast(ev->ev.user.data1); if (hotkey == "console.toggle") { g_Console->ToggleVisible(); return IN_HANDLED; } else if (g_Console->IsActive() && hotkey == "copy") { sys_clipboard_set(g_Console->GetBuffer()); return IN_HANDLED; } else if (g_Console->IsActive() && hotkey == "paste") { wchar_t* text = sys_clipboard_get(); if (text) { for (wchar_t* c = text; *c; c++) g_Console->InsertChar(0, *c); sys_clipboard_free(text); } return IN_HANDLED; } } if (!g_Console->IsActive()) return IN_PASS; // In SDL2, we no longer get Unicode wchars via SDL_Keysym // we use text input events instead and they provide UTF-8 chars if (ev->ev.type == SDL_TEXTINPUT && !HotkeyIsPressed("console.toggle")) { // TODO: this could be more efficient with an interface to insert UTF-8 strings directly std::wstring wstr = wstring_from_utf8(ev->ev.text.text); for (size_t i = 0; i < wstr.length(); ++i) g_Console->InsertChar(0, wstr[i]); return IN_HANDLED; } // TODO: text editing events for IME support if (ev->ev.type != SDL_KEYDOWN) return IN_PASS; int sym = ev->ev.key.keysym.sym; // Stop unprintable characters (ctrl+, alt+ and escape), // also prevent ` and/or ~ appearing in console every time it's toggled. if (!isUnprintableChar(ev->ev.key.keysym) && !HotkeyIsPressed("console.toggle")) { g_Console->InsertChar(sym, 0); return IN_HANDLED; } return IN_PASS; } Index: ps/trunk/source/ps/CConsole.h =================================================================== --- ps/trunk/source/ps/CConsole.h (revision 22343) +++ ps/trunk/source/ps/CConsole.h (revision 22344) @@ -1,144 +1,144 @@ -/* Copyright (C) 2015 Wildfire Games. +/* 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 . */ /* * Implements the in-game console with scripting support. */ #ifndef INCLUDED_CCONSOLE #define INCLUDED_CCONSOLE -#include -#include #include #include +#include +#include +#include #include "graphics/ShaderProgramPtr.h" #include "lib/file/vfs/vfs_path.h" #include "lib/input.h" #include "ps/CStr.h" -#include "ps/ThreadUtil.h" class CTextRenderer; #define CONSOLE_BUFFER_SIZE 1024 // for text being typed into the console #define CONSOLE_MESSAGE_SIZE 1024 // for messages being printed into the console #define CONSOLE_FONT "mono-10" /** * In-game console. * * Thread-safety: * - Expected to be constructed/destructed in the main thread. * - InsertMessage may be called from any thread while the object is alive. */ class CConsole { NONCOPYABLE(CConsole); public: CConsole(); ~CConsole(); void SetSize(float X = 300, float Y = 0, float W = 800, float H = 600); void UpdateScreenSize(int w, int h); void ToggleVisible(); void SetVisible(bool visible); void SetCursorBlinkRate(double rate); /** * @param deltaRealTime Elapsed real time since the last frame. */ void Update(const float deltaRealTime); void Render(); void InsertChar(const int szChar, const wchar_t cooked); void InsertMessage(const std::string& message); void SetBuffer(const wchar_t* szMessage); void UseHistoryFile(const VfsPath& filename, int historysize); // Only returns a pointer to the buffer; copy out of here if you want to keep it. const wchar_t* GetBuffer(); void FlushBuffer(); bool IsActive() { return m_bVisible; } int m_iFontHeight; int m_iFontWidth; int m_iFontOffset; // distance to move up before drawing size_t m_charsPerPage; private: // Lock for all state modified by InsertMessage - CMutex m_Mutex; + std::mutex m_Mutex; float m_fX; float m_fY; float m_fHeight; float m_fWidth; // "position" in show/hide animation, how visible the console is (0..1). // allows implementing other animations than sliding, e.g. fading in/out. float m_fVisibleFrac; std::deque m_deqMsgHistory; // protected by m_Mutex std::deque m_deqBufHistory; int m_iMsgHistPos; wchar_t* m_szBuffer; int m_iBufferPos; int m_iBufferLength; VfsPath m_sHistoryFile; int m_MaxHistoryLines; bool m_bVisible; // console is to be drawn bool m_bToggle; // show/hide animation is currently active double m_prevTime; // the previous time the cursor draw state changed (used for blinking cursor) bool m_bCursorVisState; // if the cursor should be drawn or not double m_cursorBlinkRate; // cursor blink rate in seconds, if greater than 0.0 void DrawWindow(CShaderProgramPtr& shader); void DrawHistory(CTextRenderer& textRenderer); void DrawBuffer(CTextRenderer& textRenderer); void DrawCursor(CTextRenderer& textRenderer); bool IsEOB() { return (m_iBufferPos == m_iBufferLength); } // Is end of Buffer? bool IsBOB() { return (m_iBufferPos == 0); } // Is beginning of Buffer? bool IsFull() { return (m_iBufferLength == CONSOLE_BUFFER_SIZE); } bool IsEmpty() { return (m_iBufferLength == 0); } void ProcessBuffer(const wchar_t* szLine); void LoadHistory(); void SaveHistory(); }; extern CConsole* g_Console; extern InReaction conInputHandler(const SDL_Event_* ev); #endif Index: ps/trunk/source/ps/CLogger.cpp =================================================================== --- ps/trunk/source/ps/CLogger.cpp (revision 22343) +++ ps/trunk/source/ps/CLogger.cpp (revision 22344) @@ -1,341 +1,341 @@ -/* Copyright (C) 2017 Wildfire Games. +/* 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 "CLogger.h" #include "graphics/FontMetrics.h" #include "graphics/ShaderManager.h" #include "graphics/TextRenderer.h" #include "lib/ogl.h" #include "lib/timer.h" #include "lib/utf8.h" #include "ps/CConsole.h" #include "ps/Profile.h" #include "renderer/Renderer.h" #include #include #include CStrW g_UniqueLogPostfix; static const double RENDER_TIMEOUT = 10.0; // seconds before messages are deleted static const double RENDER_TIMEOUT_RATE = 10.0; // number of timed-out messages deleted per second static const size_t RENDER_LIMIT = 20; // maximum messages on screen at once // Set up a default logger that throws everything away, because that's // better than crashing. (This is particularly useful for unit tests which // don't care about any log output.) struct BlackHoleStreamBuf : public std::streambuf { } blackHoleStreamBuf; std::ostream blackHoleStream(&blackHoleStreamBuf); CLogger nullLogger(&blackHoleStream, &blackHoleStream, false, true); CLogger* g_Logger = &nullLogger; const char* html_header0 = "\n" "\n" "Pyrogenesis Log\n" "\n" "

0 A.D. ("; const char* html_header1 = "

\n"; CLogger::CLogger() { OsPath mainlogPath(psLogDir() / (L"mainlog" + g_UniqueLogPostfix + L".html")); m_MainLog = new std::ofstream(OsString(mainlogPath).c_str(), std::ofstream::out | std::ofstream::trunc); debug_printf("Writing the mainlog at %s\n", mainlogPath.string8().c_str()); OsPath interestinglogPath(psLogDir() / (L"interestinglog" + g_UniqueLogPostfix + L".html")); m_InterestingLog = new std::ofstream(OsString(interestinglogPath).c_str(), std::ofstream::out | std::ofstream::trunc); m_OwnsStreams = true; m_UseDebugPrintf = true; Init(); } CLogger::CLogger(std::ostream* mainLog, std::ostream* interestingLog, bool takeOwnership, bool useDebugPrintf) { m_MainLog = mainLog; m_InterestingLog = interestingLog; m_OwnsStreams = takeOwnership; m_UseDebugPrintf = useDebugPrintf; Init(); } void CLogger::Init() { m_RenderLastEraseTime = -1.0; // this is called too early to allow us to call timer_Time(), // so we'll fill in the initial value later m_NumberOfMessages = 0; m_NumberOfErrors = 0; m_NumberOfWarnings = 0; *m_MainLog << html_header0 << engine_version << ") Main log" << html_header1; *m_InterestingLog << html_header0 << engine_version << ") Main log (warnings and errors only)" << html_header1; } CLogger::~CLogger() { char buffer[128]; sprintf_s(buffer, ARRAY_SIZE(buffer), " with %d message(s), %d error(s) and %d warning(s).", m_NumberOfMessages,m_NumberOfErrors,m_NumberOfWarnings); time_t t = time(NULL); struct tm* now = localtime(&t); char currentDate[17]; sprintf_s(currentDate, ARRAY_SIZE(currentDate), "%04d-%02d-%02d", 1900+now->tm_year, 1+now->tm_mon, now->tm_mday); char currentTime[10]; sprintf_s(currentTime, ARRAY_SIZE(currentTime), "%02d:%02d:%02d", now->tm_hour, now->tm_min, now->tm_sec); //Write closing text *m_MainLog << "

Engine exited successfully on " << currentDate; *m_MainLog << " at " << currentTime << buffer << "

\n"; *m_InterestingLog << "

Engine exited successfully on " << currentDate; *m_InterestingLog << " at " << currentTime << buffer << "

\n"; if (m_OwnsStreams) { SAFE_DELETE(m_InterestingLog); SAFE_DELETE(m_MainLog); } } static std::string ToHTML(const char* message) { std::string cmessage = message; boost::algorithm::replace_all(cmessage, "&", "&"); boost::algorithm::replace_all(cmessage, "<", "<"); return cmessage; } void CLogger::WriteMessage(const char* message, bool doRender = false) { std::string cmessage = ToHTML(message); - CScopeLock lock(m_Mutex); + std::lock_guard lock(m_Mutex); ++m_NumberOfMessages; // if (m_UseDebugPrintf) // debug_printf("MESSAGE: %s\n", message); *m_MainLog << "

" << cmessage << "

\n"; m_MainLog->flush(); if (doRender) { if (g_Console) g_Console->InsertMessage(std::string("INFO: ") + message); PushRenderMessage(Normal, message); } } void CLogger::WriteError(const char* message) { std::string cmessage = ToHTML(message); - CScopeLock lock(m_Mutex); + std::lock_guard lock(m_Mutex); ++m_NumberOfErrors; if (m_UseDebugPrintf) debug_printf("ERROR: %.16000s\n", message); if (g_Console) g_Console->InsertMessage(std::string("ERROR: ") + message); *m_InterestingLog << "

ERROR: " << cmessage << "

\n"; m_InterestingLog->flush(); *m_MainLog << "

ERROR: " << cmessage << "

\n"; m_MainLog->flush(); PushRenderMessage(Error, message); } void CLogger::WriteWarning(const char* message) { std::string cmessage = ToHTML(message); - CScopeLock lock(m_Mutex); + std::lock_guard lock(m_Mutex); ++m_NumberOfWarnings; if (m_UseDebugPrintf) debug_printf("WARNING: %s\n", message); if (g_Console) g_Console->InsertMessage(std::string("WARNING: ") + message); *m_InterestingLog << "

WARNING: " << cmessage << "

\n"; m_InterestingLog->flush(); *m_MainLog << "

WARNING: " << cmessage << "

\n"; m_MainLog->flush(); PushRenderMessage(Warning, message); } void CLogger::Render() { PROFILE3_GPU("logger"); CleanupRenderQueue(); CStrIntern font_name("mono-stroke-10"); CFontMetrics font(font_name); int lineSpacing = font.GetLineSpacing(); CShaderTechniquePtr textTech = g_Renderer.GetShaderManager().LoadEffect(str_gui_text); textTech->BeginPass(); CTextRenderer textRenderer(textTech->GetShader()); textRenderer.Font(font_name); textRenderer.Color(1.0f, 1.0f, 1.0f); // Offset by an extra 35px vertically to avoid the top bar. textRenderer.Translate(4.0f, 35.0f + lineSpacing, 0.0f); // (Lock must come after loading the CFont, since that might log error messages // and attempt to lock the mutex recursively which is forbidden) - CScopeLock lock(m_Mutex); + std::lock_guard lock(m_Mutex); for (const RenderedMessage& msg : m_RenderMessages) { const char* type; if (msg.method == Normal) { type = "info"; textRenderer.Color(0.0f, 0.8f, 0.0f); } else if (msg.method == Warning) { type = "warning"; textRenderer.Color(1.0f, 1.0f, 0.0f); } else { type = "error"; textRenderer.Color(1.0f, 0.0f, 0.0f); } CMatrix3D savedTransform = textRenderer.GetTransform(); textRenderer.PrintfAdvance(L"[%8.3f] %hs: ", msg.time, type); // Display the actual message in white so it's more readable textRenderer.Color(1.0f, 1.0f, 1.0f); textRenderer.Put(0.0f, 0.0f, msg.message.c_str()); textRenderer.SetTransform(savedTransform); textRenderer.Translate(0.0f, (float)lineSpacing, 0.0f); } textRenderer.Render(); textTech->EndPass(); } void CLogger::PushRenderMessage(ELogMethod method, const char* message) { double now = timer_Time(); // Add each message line separately const char* pos = message; const char* eol; while ((eol = strchr(pos, '\n')) != NULL) { if (eol != pos) { RenderedMessage r = { method, now, std::string(pos, eol) }; m_RenderMessages.push_back(r); } pos = eol + 1; } // Add the last line, if we didn't end on a \n if (*pos != '\0') { RenderedMessage r = { method, now, std::string(pos) }; m_RenderMessages.push_back(r); } } void CLogger::CleanupRenderQueue() { - CScopeLock lock(m_Mutex); + std::lock_guard lock(m_Mutex); if (m_RenderMessages.empty()) return; double now = timer_Time(); // Initialise the timer on the first call (since we can't do it in the ctor) if (m_RenderLastEraseTime == -1.0) m_RenderLastEraseTime = now; // Delete old messages, approximately at the given rate limit (and at most one per frame) if (now - m_RenderLastEraseTime > 1.0/RENDER_TIMEOUT_RATE) { if (m_RenderMessages[0].time + RENDER_TIMEOUT < now) { m_RenderMessages.pop_front(); m_RenderLastEraseTime = now; } } // If there's still too many then delete the oldest if (m_RenderMessages.size() > RENDER_LIMIT) m_RenderMessages.erase(m_RenderMessages.begin(), m_RenderMessages.end() - RENDER_LIMIT); } TestLogger::TestLogger() { m_OldLogger = g_Logger; g_Logger = new CLogger(&m_Stream, &blackHoleStream, false, false); } TestLogger::~TestLogger() { delete g_Logger; g_Logger = m_OldLogger; } std::string TestLogger::GetOutput() { return m_Stream.str(); } TestStdoutLogger::TestStdoutLogger() { m_OldLogger = g_Logger; g_Logger = new CLogger(&std::cout, &blackHoleStream, false, false); } TestStdoutLogger::~TestStdoutLogger() { delete g_Logger; g_Logger = m_OldLogger; } Index: ps/trunk/source/ps/CLogger.h =================================================================== --- ps/trunk/source/ps/CLogger.h (revision 22343) +++ ps/trunk/source/ps/CLogger.h (revision 22344) @@ -1,140 +1,140 @@ -/* Copyright (C) 2015 Wildfire Games. +/* 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 . */ #ifndef INCLUDED_CLOGGER #define INCLUDED_CLOGGER #include +#include #include #include #include -#include "ps/ThreadUtil.h" #include "third_party/cppformat/format.h" class CLogger; extern CLogger* g_Logger; #define LOGMESSAGE(...) g_Logger->WriteMessage(fmt::sprintf(__VA_ARGS__).c_str(), false) #define LOGMESSAGERENDER(...) g_Logger->WriteMessage(fmt::sprintf(__VA_ARGS__).c_str(), true) #define LOGWARNING(...) g_Logger->WriteWarning(fmt::sprintf(__VA_ARGS__).c_str()) #define LOGERROR(...) g_Logger->WriteError (fmt::sprintf(__VA_ARGS__).c_str()) /** * Error/warning/message logging class. * * Thread-safety: * - Expected to be constructed/destructed in the main thread. * - The message logging functions may be called from any thread * while the object is alive. */ class CLogger { NONCOPYABLE(CLogger); public: enum ELogMethod { Normal, Error, Warning }; // Default constructor - outputs to normal log files CLogger(); // Special constructor (mostly for testing) - outputs to provided streams. // Can take ownership of streams and delete them in the destructor. CLogger(std::ostream* mainLog, std::ostream* interestingLog, bool takeOwnership, bool useDebugPrintf); ~CLogger(); // Functions to write different message types (Errors and warnings are placed // both in mainLog and intrestingLog.) void WriteMessage(const char* message, bool doRender); void WriteError (const char* message); void WriteWarning(const char* message); // Render recent log messages onto the screen void Render(); private: void Init(); void PushRenderMessage(ELogMethod method, const char* message); // Delete old timed-out entries from the list of text to render void CleanupRenderQueue(); // the output streams std::ostream* m_MainLog; std::ostream* m_InterestingLog; bool m_OwnsStreams; // whether errors should be reported via debug_printf (default) // or suppressed (for tests that intentionally trigger errors) bool m_UseDebugPrintf; // vars to hold message counts int m_NumberOfMessages; int m_NumberOfErrors; int m_NumberOfWarnings; // Used for Render() struct RenderedMessage { ELogMethod method; double time; std::string message; }; std::deque m_RenderMessages; double m_RenderLastEraseTime; // Lock for all state modified by logging commands - CMutex m_Mutex; + std::mutex m_Mutex; }; /** * Helper class for unit tests - captures all log output while it is in scope, * and returns it as a single string. */ class TestLogger { NONCOPYABLE(TestLogger); public: TestLogger(); ~TestLogger(); std::string GetOutput(); private: CLogger* m_OldLogger; std::stringstream m_Stream; }; /** * Helper class for unit tests - redirects all log output to stdout. */ class TestStdoutLogger { NONCOPYABLE(TestStdoutLogger); public: TestStdoutLogger(); ~TestStdoutLogger(); private: CLogger* m_OldLogger; }; #endif Index: ps/trunk/source/ps/ConfigDB.cpp =================================================================== --- ps/trunk/source/ps/ConfigDB.cpp (revision 22343) +++ ps/trunk/source/ps/ConfigDB.cpp (revision 22344) @@ -1,469 +1,455 @@ -/* Copyright (C) 2018 Wildfire Games. +/* 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 "ConfigDB.h" #include #include "lib/allocators/shared_ptr.h" #include "lib/file/vfs/vfs_path.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/Filesystem.h" -#include "ps/ThreadUtil.h" +#include #include + typedef std::map TConfigMap; TConfigMap CConfigDB::m_Map[CFG_LAST]; VfsPath CConfigDB::m_ConfigFile[CFG_LAST]; bool CConfigDB::m_HasChanges[CFG_LAST]; -static pthread_mutex_t cfgdb_mutex = PTHREAD_MUTEX_INITIALIZER; +static std::recursive_mutex cfgdb_mutex; // These entries will not be printed to logfiles, so that logfiles can be shared without leaking personal or sensitive data static const std::unordered_set g_UnloggedEntries = { "lobby.password", "lobby.buddies", "userreport.id" // authentication token for GDPR personal data requests }; -CConfigDB::CConfigDB() -{ - // Recursive mutex needed for WriteFile - pthread_mutexattr_t attr; - int err; - err = pthread_mutexattr_init(&attr); - ENSURE(err == 0); - err = pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); - ENSURE(err == 0); - err = pthread_mutex_init(&cfgdb_mutex, &attr); - ENSURE(err == 0); - err = pthread_mutexattr_destroy(&attr); - ENSURE(err == 0); -} - #define CHECK_NS(rval)\ do {\ if (ns < 0 || ns >= CFG_LAST)\ {\ debug_warn(L"CConfigDB: Invalid ns value");\ return rval;\ }\ } while (false) namespace { template void Get(const CStr& value, T& ret) { std::stringstream ss(value); ss >> ret; } template<> void Get<>(const CStr& value, bool& ret) { ret = value == "true"; } template<> void Get<>(const CStr& value, std::string& ret) { ret = value; } std::string EscapeString(const CStr& str) { std::string ret; for (size_t i = 0; i < str.length(); ++i) { if (str[i] == '\\') ret += "\\\\"; else if (str[i] == '"') ret += "\\\""; else ret += str[i]; } return ret; } } // namespace #define GETVAL(type)\ void CConfigDB::GetValue(EConfigNamespace ns, const CStr& name, type& value)\ {\ CHECK_NS(;);\ - CScopeLock s(&cfgdb_mutex);\ + std::lock_guard s(cfgdb_mutex);\ TConfigMap::iterator it = m_Map[CFG_COMMAND].find(name);\ if (it != m_Map[CFG_COMMAND].end())\ {\ Get(it->second[0], value);\ return;\ }\ for (int search_ns = ns; search_ns >= 0; --search_ns)\ {\ it = m_Map[search_ns].find(name);\ if (it != m_Map[search_ns].end())\ {\ Get(it->second[0], value);\ return;\ }\ }\ } GETVAL(bool) GETVAL(int) GETVAL(u32) GETVAL(float) GETVAL(double) GETVAL(std::string) #undef GETVAL bool CConfigDB::HasChanges(EConfigNamespace ns) const { CHECK_NS(false); - CScopeLock s(&cfgdb_mutex); + std::lock_guard s(cfgdb_mutex); return m_HasChanges[ns]; } void CConfigDB::SetChanges(EConfigNamespace ns, bool value) { CHECK_NS(;); - CScopeLock s(&cfgdb_mutex); + std::lock_guard s(cfgdb_mutex); m_HasChanges[ns] = value; } void CConfigDB::GetValues(EConfigNamespace ns, const CStr& name, CConfigValueSet& values) const { CHECK_NS(;); - CScopeLock s(&cfgdb_mutex); + std::lock_guard s(cfgdb_mutex); TConfigMap::iterator it = m_Map[CFG_COMMAND].find(name); if (it != m_Map[CFG_COMMAND].end()) { values = it->second; return; } for (int search_ns = ns; search_ns >= 0; --search_ns) { it = m_Map[search_ns].find(name); if (it != m_Map[search_ns].end()) { values = it->second; return; } } } EConfigNamespace CConfigDB::GetValueNamespace(EConfigNamespace ns, const CStr& name) const { CHECK_NS(CFG_LAST); - CScopeLock s(&cfgdb_mutex); + std::lock_guard s(cfgdb_mutex); TConfigMap::iterator it = m_Map[CFG_COMMAND].find(name); if (it != m_Map[CFG_COMMAND].end()) return CFG_COMMAND; for (int search_ns = ns; search_ns >= 0; --search_ns) { it = m_Map[search_ns].find(name); if (it != m_Map[search_ns].end()) return (EConfigNamespace)search_ns; } return CFG_LAST; } std::map CConfigDB::GetValuesWithPrefix(EConfigNamespace ns, const CStr& prefix) const { - CScopeLock s(&cfgdb_mutex); + std::lock_guard s(cfgdb_mutex); std::map ret; CHECK_NS(ret); // Loop upwards so that values in later namespaces can override // values in earlier namespaces for (int search_ns = 0; search_ns <= ns; ++search_ns) for (const std::pair& p : m_Map[search_ns]) if (boost::algorithm::starts_with(p.first, prefix)) ret[p.first] = p.second; for (const std::pair& p : m_Map[CFG_COMMAND]) if (boost::algorithm::starts_with(p.first, prefix)) ret[p.first] = p.second; return ret; } void CConfigDB::SetValueString(EConfigNamespace ns, const CStr& name, const CStr& value) { CHECK_NS(;); - CScopeLock s(&cfgdb_mutex); + std::lock_guard s(cfgdb_mutex); TConfigMap::iterator it = m_Map[ns].find(name); if (it == m_Map[ns].end()) it = m_Map[ns].insert(m_Map[ns].begin(), make_pair(name, CConfigValueSet(1))); it->second[0] = value; } void CConfigDB::SetValueBool(EConfigNamespace ns, const CStr& name, const bool value) { CStr valueString = value ? "true" : "false"; SetValueString(ns, name, valueString); } void CConfigDB::RemoveValue(EConfigNamespace ns, const CStr& name) { CHECK_NS(;); - CScopeLock s(&cfgdb_mutex); + std::lock_guard s(cfgdb_mutex); TConfigMap::iterator it = m_Map[ns].find(name); if (it == m_Map[ns].end()) return; m_Map[ns].erase(it); } void CConfigDB::SetConfigFile(EConfigNamespace ns, const VfsPath& path) { CHECK_NS(;); - CScopeLock s(&cfgdb_mutex); + std::lock_guard s(cfgdb_mutex); m_ConfigFile[ns] = path; } bool CConfigDB::Reload(EConfigNamespace ns) { CHECK_NS(false); - CScopeLock s(&cfgdb_mutex); + std::lock_guard s(cfgdb_mutex); shared_ptr buffer; size_t buflen; { // Handle missing files quietly if (g_VFS->GetFileInfo(m_ConfigFile[ns], NULL) < 0) { LOGMESSAGE("Cannot find config file \"%s\" - ignoring", m_ConfigFile[ns].string8()); return false; } LOGMESSAGE("Loading config file \"%s\"", m_ConfigFile[ns].string8()); Status ret = g_VFS->LoadFile(m_ConfigFile[ns], buffer, buflen); if (ret != INFO::OK) { LOGERROR("CConfigDB::Reload(): vfs_load for \"%s\" failed: return was %lld", m_ConfigFile[ns].string8(), (long long)ret); return false; } } TConfigMap newMap; char *filebuf = (char*)buffer.get(); char *filebufend = filebuf+buflen; bool quoted = false; CStr header; CStr name; CStr value; int line = 1; std::vector values; for (char* pos = filebuf; pos < filebufend; ++pos) { switch (*pos) { case '\n': case ';': break; // We finished parsing this line case ' ': case '\r': case '\t': continue; // ignore case '[': header.clear(); for (++pos; pos < filebufend && *pos != '\n' && *pos != ']'; ++pos) header.push_back(*pos); if (pos == filebufend || *pos == '\n') { LOGERROR("Config header with missing close tag encountered on line %d in '%s'", line, m_ConfigFile[ns].string8()); header.clear(); ++line; continue; } LOGMESSAGE("Found config header '%s'", header.c_str()); header.push_back('.'); while (++pos < filebufend && *pos != '\n' && *pos != ';') if (*pos != ' ' && *pos != '\r') { LOGERROR("Config settings on the same line as a header on line %d in '%s'", line, m_ConfigFile[ns].string8()); break; } while (pos < filebufend && *pos != '\n') ++pos; ++line; continue; case '=': // Parse parameters (comma separated, possibly quoted) for (++pos; pos < filebufend && *pos != '\n' && *pos != ';'; ++pos) { switch (*pos) { case '"': quoted = true; // parse until not quoted anymore for (++pos; pos < filebufend && *pos != '\n' && *pos != '"'; ++pos) { if (*pos == '\\' && ++pos == filebufend) { LOGERROR("Escape character at end of input (line %d in '%s')", line, m_ConfigFile[ns].string8()); break; } value.push_back(*pos); } if (pos < filebufend && *pos == '"') quoted = false; else --pos; // We should terminate the outer loop too break; case ' ': case '\r': case '\t': break; // ignore case ',': if (!value.empty()) values.push_back(value); value.clear(); break; default: value.push_back(*pos); break; } } if (quoted) // We ignore the invalid parameter LOGERROR("Unmatched quote while parsing config file '%s' on line %d", m_ConfigFile[ns].string8(), line); else if (!value.empty()) values.push_back(value); value.clear(); quoted = false; break; // We are either at the end of the line, or we still have a comment to parse default: name.push_back(*pos); continue; } // Consume the rest of the line while (pos < filebufend && *pos != '\n') ++pos; // Store the setting if (!name.empty() && !values.empty()) { CStr key(header + name); newMap[key] = values; if (g_UnloggedEntries.find(key) != g_UnloggedEntries.end()) LOGMESSAGE("Loaded config string \"%s\"", key); else { std::string vals; for (size_t i = 0; i < newMap[key].size() - 1; ++i) vals += "\"" + EscapeString(newMap[key][i]) + "\", "; vals += "\"" + EscapeString(newMap[key][values.size()-1]) + "\""; LOGMESSAGE("Loaded config string \"%s\" = %s", key, vals); } } else if (!name.empty()) LOGERROR("Encountered config setting '%s' without value while parsing '%s' on line %d", name, m_ConfigFile[ns].string8(), line); name.clear(); values.clear(); ++line; } if (!name.empty()) LOGERROR("Config file does not have a new line after the last config setting '%s'", name); m_Map[ns].swap(newMap); return true; } bool CConfigDB::WriteFile(EConfigNamespace ns) const { CHECK_NS(false); - CScopeLock s(&cfgdb_mutex); + std::lock_guard s(cfgdb_mutex); return WriteFile(ns, m_ConfigFile[ns]); } bool CConfigDB::WriteFile(EConfigNamespace ns, const VfsPath& path) const { CHECK_NS(false); - CScopeLock s(&cfgdb_mutex); + std::lock_guard s(cfgdb_mutex); shared_ptr buf; AllocateAligned(buf, 1*MiB, maxSectorSize); char* pos = (char*)buf.get(); for (const std::pair& p : m_Map[ns]) { size_t i; pos += sprintf(pos, "%s = ", p.first.c_str()); for (i = 0; i < p.second.size() - 1; ++i) pos += sprintf(pos, "\"%s\", ", EscapeString(p.second[i]).c_str()); pos += sprintf(pos, "\"%s\"\n", EscapeString(p.second[i]).c_str()); } const size_t len = pos - (char*)buf.get(); Status ret = g_VFS->CreateFile(path, buf, len); if (ret < 0) { LOGERROR("CConfigDB::WriteFile(): CreateFile \"%s\" failed (error: %d)", path.string8(), (int)ret); return false; } return true; } bool CConfigDB::WriteValueToFile(EConfigNamespace ns, const CStr& name, const CStr& value) { CHECK_NS(false); - CScopeLock s(&cfgdb_mutex); + std::lock_guard s(cfgdb_mutex); return WriteValueToFile(ns, name, value, m_ConfigFile[ns]); } bool CConfigDB::WriteValueToFile(EConfigNamespace ns, const CStr& name, const CStr& value, const VfsPath& path) { CHECK_NS(false); - CScopeLock s(&cfgdb_mutex); + std::lock_guard s(cfgdb_mutex); TConfigMap newMap; m_Map[ns].swap(newMap); Reload(ns); SetValueString(ns, name, value); bool ret = WriteFile(ns, path); m_Map[ns].swap(newMap); return ret; } #undef CHECK_NS Index: ps/trunk/source/ps/ConfigDB.h =================================================================== --- ps/trunk/source/ps/ConfigDB.h (revision 22343) +++ ps/trunk/source/ps/ConfigDB.h (revision 22344) @@ -1,177 +1,175 @@ /* 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 . */ /* CConfigDB - Load, access and store configuration variables TDD : http://www.wildfiregames.com/forum/index.php?showtopic=1125 OVERVIEW: JavaScript: Check this documentation: http://trac.wildfiregames.com/wiki/Exposed_ConfigDB_Functions */ #ifndef INCLUDED_CONFIGDB #define INCLUDED_CONFIGDB #include "lib/file/vfs/vfs_path.h" #include "ps/CStr.h" #include "ps/Singleton.h" #include // Namespace priorities: User supersedes mod supersedes system. // Command-line arguments override everything. enum EConfigNamespace { CFG_DEFAULT, CFG_SYSTEM, CFG_MOD, CFG_USER, CFG_COMMAND, CFG_LAST }; typedef std::vector CConfigValueSet; #define g_ConfigDB CConfigDB::GetSingleton() class CConfigDB : public Singleton { public: - CConfigDB(); - /** * Attempt to retrieve the value of a config variable with the given name; * will search CFG_COMMAND first, and then all namespaces from the specified * namespace down. */ void GetValue(EConfigNamespace ns, const CStr& name, bool& value); ///@copydoc CConfigDB::GetValue void GetValue(EConfigNamespace ns, const CStr& name, int& value); ///@copydoc CConfigDB::GetValue void GetValue(EConfigNamespace ns, const CStr& name, u32& value); ///@copydoc CConfigDB::GetValue void GetValue(EConfigNamespace ns, const CStr& name, float& value); ///@copydoc CConfigDB::GetValue void GetValue(EConfigNamespace ns, const CStr& name, double& value); ///@copydoc CConfigDB::GetValue void GetValue(EConfigNamespace ns, const CStr& name, std::string& value); /** * Returns true if changed with respect to last write on file */ bool HasChanges(EConfigNamespace ns) const; void SetChanges(EConfigNamespace ns, bool value); /** * Attempt to retrieve a vector of values corresponding to the given setting; * will search CFG_COMMAND first, and then all namespaces from the specified * namespace down. */ void GetValues(EConfigNamespace ns, const CStr& name, CConfigValueSet& values) const; /** * Returns the namespace that the value returned by GetValues was defined in, * or CFG_LAST if it wasn't defined at all. */ EConfigNamespace GetValueNamespace(EConfigNamespace ns, const CStr& name) const; /** * Retrieve a map of values corresponding to settings whose names begin * with the given prefix; * will search all namespaces from default up to the specified namespace. */ std::map GetValuesWithPrefix(EConfigNamespace ns, const CStr& prefix) const; /** * Save a config value in the specified namespace. If the config variable * existed the value is replaced. */ void SetValueString(EConfigNamespace ns, const CStr& name, const CStr& value); void SetValueBool(EConfigNamespace ns, const CStr& name, const bool value); /** * Remove a config value in the specified namespace. */ void RemoveValue(EConfigNamespace ns, const CStr& name); /** * Set the path to the config file used to populate the specified namespace * Note that this function does not actually load the config file. Use * the Reload() method if you want to read the config file at the same time. * * 'path': The path to the config file. */ void SetConfigFile(EConfigNamespace ns, const VfsPath& path); /** * Reload the config file associated with the specified config namespace * (the last config file path set with SetConfigFile) * * Returns: * true: if the reload succeeded, * false: if the reload failed */ bool Reload(EConfigNamespace); /** * Write the current state of the specified config namespace to the file * specified by 'path' * * Returns: * true: if the config namespace was successfully written to the file * false: if an error occurred */ bool WriteFile(EConfigNamespace ns, const VfsPath& path) const; /** * Write the current state of the specified config namespace to the file * it was originally loaded from. * * Returns: * true: if the config namespace was successfully written to the file * false: if an error occurred */ bool WriteFile(EConfigNamespace ns) const; /** * Write a config value to the file specified by 'path' * * Returns: * true: if the config value was successfully saved and written to the file * false: if an error occurred */ bool WriteValueToFile(EConfigNamespace ns, const CStr& name, const CStr& value, const VfsPath& path); bool WriteValueToFile(EConfigNamespace ns, const CStr& name, const CStr& value); private: static std::map m_Map[]; static VfsPath m_ConfigFile[]; static bool m_HasChanges[]; }; // stores the value of the given key into . this quasi-template // convenience wrapper on top of GetValue simplifies user code #define CFG_GET_VAL(name, destination)\ g_ConfigDB.GetValue(CFG_USER, name, destination) #endif // INCLUDED_CONFIGDB Index: ps/trunk/source/ps/Profile.cpp =================================================================== --- ps/trunk/source/ps/Profile.cpp (revision 22343) +++ ps/trunk/source/ps/Profile.cpp (revision 22344) @@ -1,756 +1,775 @@ -/* Copyright (C) 2011 Wildfire Games. +/* 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 . */ /* * 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()) ++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 pthread_mutex_t alloc_hook_mutex = PTHREAD_MUTEX_INITIALIZER; 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. pthread_mutex_lock(&alloc_hook_mutex); ++malloc_count; __malloc_hook = old_malloc_hook; void* result = malloc(size); old_malloc_hook = __malloc_hook; __malloc_hook = malloc_hook; pthread_mutex_unlock(&alloc_hook_mutex); return result; } static void alloc_hook_initialize() { pthread_mutex_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) pthread_mutex_unlock(&alloc_hook_mutex); } /* 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()) + g_Profiler.Start(name); + } +} + +CProfileSample::~CProfileSample() +{ + if (CProfileManager::IsInitialised()) + if(ThreadUtil::IsMainThread()) + g_Profiler.Stop(); +} Index: ps/trunk/source/ps/Profile.h =================================================================== --- ps/trunk/source/ps/Profile.h (revision 22343) +++ ps/trunk/source/ps/Profile.h (revision 22344) @@ -1,232 +1,194 @@ /* 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 . */ /* * GPG3-style hierarchical profiler */ #ifndef INCLUDED_PROFILE #define INCLUDED_PROFILE #include #include "lib/adts/ring_buf.h" +#include "lib/posix/posix_pthread.h" #include "ps/Profiler2.h" #include "ps/Singleton.h" -#include "ps/ThreadUtil.h" #include #include #include #include #define PROFILE_AMORTIZE_FRAMES 30 #define PROFILE_AMORTIZE_TURNS 1 class CProfileManager; class CProfileNodeTable; class CStr8; class CStrW; // To profile scripts usefully, we use a call hook that's called on every enter/exit, // and need to find the function name. But most functions are anonymous so we make do // with filename plus line number instead. // Computing the names is fairly expensive, and we need to return an interned char* // for the profiler to hold a copy of, so we use boost::flyweight to construct interned // strings per call location. // // TODO: Check again how much the overhead for getting filename and line really is and if // it has increased with the new approach after the SpiderMonkey 31 upgrade. // // Flyweight types (with no_locking because the call hooks are only used in the // main thread, and no_tracking because we mustn't delete values the profiler is // using and it's not going to waste much memory) typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; class CProfileNode { NONCOPYABLE(CProfileNode); public: typedef std::vector::iterator profile_iterator; typedef std::vector::const_iterator const_profile_iterator; CProfileNode( const char* name, CProfileNode* parent ); ~CProfileNode(); const char* GetName() const { return name; } double GetFrameCalls() const; double GetFrameTime() const; double GetTurnCalls() const; double GetTurnTime() const; double GetFrameMallocs() const; double GetTurnMallocs() const; const CProfileNode* GetChild( const char* name ) const; const CProfileNode* GetScriptChild( const char* name ) const; const std::vector* GetChildren() const { return( &children ); } const std::vector* GetScriptChildren() const { return( &script_children ); } bool CanExpand(); CProfileNode* GetChild( const char* name ); CProfileNode* GetScriptChild( const char* name ); CProfileNode* GetParent() const { return( parent ); } // Resets timing information for this node and all its children void Reset(); // Resets frame timings for this node and all its children void Frame(); // Resets turn timings for this node and all its children void Turn(); // Enters the node void Call(); // Leaves the node. Returns true if the node has actually been left bool Return(); private: friend class CProfileManager; friend class CProfileNodeTable; const char* name; int calls_frame_current; int calls_turn_current; RingBuf calls_per_frame; RingBuf calls_per_turn; double time_frame_current; double time_turn_current; RingBuf time_per_frame; RingBuf time_per_turn; long mallocs_frame_current; long mallocs_turn_current; RingBuf mallocs_per_frame; RingBuf mallocs_per_turn; double start; long start_mallocs; int recursion; CProfileNode* parent; std::vector children; std::vector script_children; CProfileNodeTable* display_table; }; class CProfileManager : public Singleton { public: CProfileManager(); ~CProfileManager(); // Begins timing for a named subsection void Start( const char* name ); void StartScript( const char* name ); // Ends timing for the current subsection void Stop(); // Resets all timing information void Reset(); // Resets frame timing information void Frame(); // Resets turn timing information // (Must not be called before Frame) void Turn(); // Resets absolutely everything, at the end of this frame void StructuralReset(); inline const CProfileNode* GetCurrent() { return( current ); } inline const CProfileNode* GetRoot() { return( root ); } private: CProfileNode* root; CProfileNode* current; bool needs_structural_reset; void PerformStructuralReset(); }; #define g_Profiler CProfileManager::GetSingleton() class CProfileSample { public: - CProfileSample(const char* name) - { - if (CProfileManager::IsInitialised()) - { - // The profiler is only safe to use on the main thread - - if(ThreadUtil::IsMainThread()) - g_Profiler.Start(name); - } - } - ~CProfileSample() - { - if (CProfileManager::IsInitialised()) - if(ThreadUtil::IsMainThread()) - g_Profiler.Stop(); - } -}; - -class CProfileSampleScript -{ -public: - CProfileSampleScript( const char* name ) - { - if (CProfileManager::IsInitialised()) - { - // The profiler is only safe to use on the main thread, - // but scripts get run on other threads too so we need to - // conditionally enable the profiler. - // (This usually only gets used in debug mode so performance - // doesn't matter much.) - if (ThreadUtil::IsMainThread()) - g_Profiler.StartScript( name ); - } - } - ~CProfileSampleScript() - { - if (CProfileManager::IsInitialised()) - if (ThreadUtil::IsMainThread()) - g_Profiler.Stop(); - } + CProfileSample(const char* name); + ~CProfileSample(); }; // Put a PROFILE("xyz") block at the start of all code to be profiled. // Profile blocks last until the end of the containing scope. #define PROFILE(name) CProfileSample __profile(name) // Cheat a bit to make things slightly easier on the user #define PROFILE_START(name) { CProfileSample __profile(name) #define PROFILE_END(name) } // Do both old and new profilers simultaneously (1+2=3), for convenience. #define PROFILE3(name) PROFILE(name); PROFILE2(name) // Also do GPU #define PROFILE3_GPU(name) PROFILE(name); PROFILE2(name); PROFILE2_GPU(name) #endif // INCLUDED_PROFILE Index: ps/trunk/source/ps/Profiler2.cpp =================================================================== --- ps/trunk/source/ps/Profiler2.cpp (revision 22343) +++ ps/trunk/source/ps/Profiler2.cpp (revision 22344) @@ -1,1000 +1,1000 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2019 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 "ps/CLogger.h" #include "ps/CStr.h" #include "ps/Profiler2GPU.h" #include "third_party/mongoose/mongoose.h" #include #include #include CProfiler2 g_Profiler2; // 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}; 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); int err = pthread_key_create(&m_TLS, &CProfiler2::TLSDtor); ENSURE(err == 0); 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; const char *options[] = { "listening_ports", "127.0.0.1:8000", // bind to localhost for security "num_threads", "6", // enough for the browser's parallel connection limit NULL }; 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()); void * dataptr = pthread_getspecific(m_TLS); TLSDtor(dataptr); int err = pthread_key_delete(m_TLS); ENSURE(err == 0); 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); } /** * Called by pthreads when a registered thread is destroyed. */ void CProfiler2::TLSDtor(void* data) { ThreadStorage* storage = (ThreadStorage*)data; storage->GetProfiler().RemoveThreadStorage(storage); delete (ThreadStorage*)data; } void CProfiler2::RegisterCurrentThread(const std::string& name) { ENSURE(m_Initialised); ENSURE(pthread_getspecific(m_TLS) == NULL); // mustn't register a thread more than once ThreadStorage* storage = new ThreadStorage(*this, name); int err = pthread_setspecific(m_TLS, storage); ENSURE(err == 0); RecordSyncMarker(); RecordEvent("thread start"); AddThreadStorage(storage); } void CProfiler2::AddThreadStorage(ThreadStorage* storage) { - CScopeLock lock(m_Mutex); + std::lock_guard lock(m_Mutex); m_Threads.push_back(storage); } void CProfiler2::RemoveThreadStorage(ThreadStorage* storage) { - CScopeLock lock(m_Mutex); + std::lock_guard lock(m_Mutex); m_Threads.erase(std::find(m_Threads.begin(), m_Threads.end(), 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; typedef std::tuple > infoPerType; std::unordered_map 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); auto 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, len); CStr mess = CStr((const char*)message, len); if (!last_names.empty()) { auto 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 (auto& 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 auto& attrib : std::get<2>(type.second)) { buffer[writePos] = (u8)CProfiler2::ITEM_ATTRIBUTE; writePos++; std::string basic = attrib; auto 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"); - CScopeLock lock(m_Mutex); + std::lock_guard lock(m_Mutex); stream << "{\"threads\":["; for (size_t i = 0; i < m_Threads.size(); ++i) { if (i != 0) stream << ","; stream << "{\"name\":\"" << CStr(m_Threads[i]->GetName()).EscapeToPrintableASCII() << "\"}"; } 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"); - CScopeLock lock(m_Mutex); // lock against changes to m_Threads or deletions of ThreadStorage + std::lock_guard lock(m_Mutex); // lock against changes to m_Threads or deletions of ThreadStorage ThreadStorage* storage = NULL; for (size_t i = 0; i < m_Threads.size(); ++i) { if (m_Threads[i]->GetName() == thread) { storage = m_Threads[i]; break; } } if (!storage) return "cannot find named thread"; stream << "{\"events\":[\n"; stream << "[\n"; buffer = storage->GetBuffer(); } BufferVisitor_Dump visitor(stream); RunBufferVisitor(buffer, visitor); stream << "null]\n]}"; return NULL; } void CProfiler2::SaveToFile() { OsPath path = psLogDir()/"profile2.jsonp"; std::ofstream stream(OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); ENSURE(stream.good()); std::vector threads; { - CScopeLock lock(m_Mutex); + std::lock_guard lock(m_Mutex); threads = m_Threads; } stream << "profileDataCB({\"threads\": [\n"; for (size_t i = 0; i < threads.size(); ++i) { if (i != 0) stream << ",\n"; stream << "{\"name\":\"" << CStr(threads[i]->GetName()).EscapeToPrintableASCII() << "\",\n"; stream << "\"data\": "; ConstructJSONResponse(stream, threads[i]->GetName()); stream << "\n}"; } 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 22343) +++ ps/trunk/source/ps/Profiler2.h (revision 22344) @@ -1,556 +1,558 @@ -/* Copyright (C) 2016 Wildfire Games. +/* Copyright (C) 2019 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 + 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 = 256; // 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: what's a good size? // TODO: different threads might want different sizes static const size_t BUFFER_SIZE = 4*1024*1024; static const size_t HOLD_BUFFER_SIZE = 128 * 1024; /** * 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()); 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(); static void TLSDtor(void* data); ThreadStorage& GetThreadStorage() { ThreadStorage* storage = (ThreadStorage*)pthread_getspecific(m_TLS); ASSERT(storage); return *storage; } bool m_Initialised; int m_FrameNumber; mg_context* m_MgContext; pthread_key_t m_TLS; CProfiler2GPU* m_GPU; - CMutex m_Mutex; + std::mutex m_Mutex; 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/ThreadUtil.cpp =================================================================== --- ps/trunk/source/ps/ThreadUtil.cpp (revision 22343) +++ ps/trunk/source/ps/ThreadUtil.cpp (revision 22344) @@ -1,39 +1,41 @@ -/* Copyright (C) 2010 Wildfire Games. +/* 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 pthread_t g_MainThread; +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 pthread_equal(pthread_self(), g_MainThread) ? true : false; + return g_MainThread == std::this_thread::get_id(); } void ThreadUtil::SetMainThread() { - g_MainThread = pthread_self(); + g_MainThread = std::this_thread::get_id(); g_MainThreadSet = true; } Index: ps/trunk/source/ps/ThreadUtil.h =================================================================== --- ps/trunk/source/ps/ThreadUtil.h (revision 22343) +++ ps/trunk/source/ps/ThreadUtil.h (revision 22344) @@ -1,117 +1,38 @@ -/* Copyright (C) 2010 Wildfire Games. +/* 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 . */ #ifndef INCLUDED_THREADUTIL #define INCLUDED_THREADUTIL -#include "lib/posix/posix_pthread.h" - -#ifdef DEBUG_LOCKS - -#define LOCK_MUTEX(_mutex) STMT( \ - printf("pthread_mutex_lock: 1 %p [pid:%d]\n", _mutex, pthread_self()); \ - pthread_mutex_lock(_mutex); \ - printf("pthread_mutex_lock: 2 %p [pid:%d]\n", _mutex, pthread_self()) \ -) -#define UNLOCK_MUTEX(_mutex) STMT( \ - pthread_mutex_unlock(_mutex); \ - printf("pthread_mutex_unlock: %p [pid:%d]\n", _mutex, pthread_self()) \ -) - -#else - -#define LOCK_MUTEX(_mutex) pthread_mutex_lock(_mutex) -#define UNLOCK_MUTEX(_mutex) pthread_mutex_unlock(_mutex) - -#endif - -/** - * A non-recursive mutual exclusion lock. - */ -class CMutex -{ - NONCOPYABLE(CMutex); - - friend class CScopeLock; - -public: - CMutex() - { - int ret = pthread_mutex_init(&m_Mutex, NULL); - ENSURE(ret == 0); - } - - ~CMutex() - { - int ret = pthread_mutex_destroy(&m_Mutex); - ENSURE(ret == 0); - } - -private: - pthread_mutex_t m_Mutex; -}; - -/** - * Locks a CMutex over this object's lifetime. - * The mutexes are non-recursive - a single thread locking a mutex more than once - * results in undefined behaviour. - */ -class CScopeLock -{ - NONCOPYABLE(CScopeLock); - -public: - CScopeLock(pthread_mutex_t* mutex) : - m_Mutex(mutex) - { - LOCK_MUTEX(m_Mutex); - } - - CScopeLock(CMutex& mutex) : - m_Mutex(&mutex.m_Mutex) - { - LOCK_MUTEX(m_Mutex); - } - - ~CScopeLock() - { - UNLOCK_MUTEX(m_Mutex); - } - -private: - pthread_mutex_t* m_Mutex; -}; - - namespace ThreadUtil { /** * 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/UserReport.cpp =================================================================== --- ps/trunk/source/ps/UserReport.cpp (revision 22343) +++ ps/trunk/source/ps/UserReport.cpp (revision 22344) @@ -1,651 +1,652 @@ -/* Copyright (C) 2018 Wildfire Games. +/* 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 "UserReport.h" #include "lib/timer.h" #include "lib/utf8.h" #include "lib/external_libraries/curl.h" #include "lib/external_libraries/libsdl.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/ThreadUtil.h" #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); // Set up the worker thread: // Use SDL semaphores since OS X doesn't implement sem_init m_WorkerSem = SDL_CreateSemaphore(0); ENSURE(m_WorkerSem); int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this); ENSURE(ret == 0); } ~CUserReporterWorker() { // Clean up resources SDL_DestroySemaphore(m_WorkerSem); 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) { - CScopeLock lock(m_WorkerMutex); + std::lock_guard lock(m_WorkerMutex); if (enabled != m_Enabled) { m_Enabled = enabled; // Wake up the worker thread SDL_SemPost(m_WorkerSem); } } /** * 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() { { - CScopeLock lock(m_WorkerMutex); + std::lock_guard lock(m_WorkerMutex); m_Shutdown = true; } // Wake up the worker thread SDL_SemPost(m_WorkerSem); // Wait for it to shut down cleanly // TODO: should have a timeout in case of network hangs pthread_join(m_WorkerThread, NULL); return true; } /** * Called by main thread to determine the current status of the uploader. */ std::string GetStatus() { - CScopeLock lock(m_WorkerMutex); + 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) { { - CScopeLock lock(m_WorkerMutex); + std::lock_guard lock(m_WorkerMutex); m_ReportQueue.push_back(report); } // Wake up the worker thread SDL_SemPost(m_WorkerSem); } /** * 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 SDL_SemPost(m_WorkerSem); m_LastUpdateTime = now; } } private: static void* RunThread(void* data) { debug_SetThreadName("CUserReportWorker"); g_Profiler2.RegisterCurrentThread("userreport"); static_cast(data)->Run(); return NULL; } 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 semaphore 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 - we can't simply * use SDL_SemWaitTimeout because on Linux it's implemented as an inefficient * busy-wait loop, and we can't use a manual busy-wait with a long delay time * because we'd lose responsiveness. So the main thread pings the worker * occasionally so it can check its timer. */ // Wait until the main thread wakes us up while (true) { g_Profiler2.RecordRegionEnter("semaphore wait"); ENSURE(SDL_SemWait(m_WorkerSem) == 0); 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() { - CScopeLock lock(m_WorkerMutex); + std::lock_guard lock(m_WorkerMutex); return m_Enabled; } bool GetShutdown() { - CScopeLock lock(m_WorkerMutex); + std::lock_guard lock(m_WorkerMutex); return m_Shutdown; } void SetStatus(const std::string& status) { - CScopeLock lock(m_WorkerMutex); + 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; { - CScopeLock lock(m_WorkerMutex); + 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) { - CScopeLock lock(m_WorkerMutex); + 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 { - CScopeLock lock(m_WorkerMutex); + 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: pthread_t m_WorkerThread; - CMutex m_WorkerMutex; + std::mutex m_WorkerMutex; SDL_sem* m_WorkerSem; // 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/XML/RelaxNG.cpp =================================================================== --- ps/trunk/source/ps/XML/RelaxNG.cpp (revision 22343) +++ ps/trunk/source/ps/XML/RelaxNG.cpp (revision 22344) @@ -1,183 +1,184 @@ -/* Copyright (C) 2015 Wildfire Games. +/* 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 "RelaxNG.h" #include "lib/timer.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include #include +#include TIMER_ADD_CLIENT(xml_validation); /* * libxml2 leaks memory when parsing schemas: https://bugzilla.gnome.org/show_bug.cgi?id=615767 * To minimise that problem, keep a global cache of parsed schemas, so we don't * leak an indefinitely large amount of memory when repeatedly restarting the simulation. */ class RelaxNGSchema; static std::map > g_SchemaCache; -static CMutex g_SchemaCacheLock; +static std::mutex g_SchemaCacheLock; void ClearSchemaCache() { - CScopeLock lock(g_SchemaCacheLock); + std::lock_guard lock(g_SchemaCacheLock); g_SchemaCache.clear(); } static void relaxNGErrorHandler(void* UNUSED(userData), xmlErrorPtr error) { // Strip a trailing newline std::string message = error->message; if (message.length() > 0 && message[message.length()-1] == '\n') message.erase(message.length()-1); LOGERROR("RelaxNGValidator: Validation %s: %s:%d: %s", error->level == XML_ERR_WARNING ? "warning" : "error", error->file, error->line, message.c_str()); } class RelaxNGSchema { public: xmlRelaxNGPtr m_Schema; RelaxNGSchema(const std::string& grammar) { xmlRelaxNGParserCtxtPtr ctxt = xmlRelaxNGNewMemParserCtxt(grammar.c_str(), (int)grammar.size()); m_Schema = xmlRelaxNGParse(ctxt); xmlRelaxNGFreeParserCtxt(ctxt); if (m_Schema == NULL) LOGERROR("RelaxNGValidator: Failed to compile schema"); } ~RelaxNGSchema() { if (m_Schema) xmlRelaxNGFree(m_Schema); } }; RelaxNGValidator::RelaxNGValidator() : m_Schema(NULL) { } RelaxNGValidator::~RelaxNGValidator() { } bool RelaxNGValidator::LoadGrammar(const std::string& grammar) { shared_ptr schema; { - CScopeLock lock(g_SchemaCacheLock); + std::lock_guard lock(g_SchemaCacheLock); std::map >::iterator it = g_SchemaCache.find(grammar); if (it == g_SchemaCache.end()) { schema = shared_ptr(new RelaxNGSchema(grammar)); g_SchemaCache[grammar] = schema; } else { schema = it->second; } } m_Schema = schema->m_Schema; if (!m_Schema) return false; MD5 hash; hash.Update((const u8*)grammar.c_str(), grammar.length()); m_Hash = hash; return true; } bool RelaxNGValidator::LoadGrammarFile(const PIVFS& vfs, const VfsPath& grammarPath) { CVFSFile file; if (file.Load(vfs, grammarPath) != PSRETURN_OK) return false; return LoadGrammar(file.DecodeUTF8()); } bool RelaxNGValidator::Validate(const std::wstring& filename, const std::wstring& document) const { std::string docutf8 = "" + utf8_from_wstring(document); return ValidateEncoded(filename, docutf8); } bool RelaxNGValidator::ValidateEncoded(const std::wstring& filename, const std::string& document) const { TIMER_ACCRUE(xml_validation); if (!m_Schema) { LOGERROR("RelaxNGValidator: No grammar loaded"); return false; } xmlDocPtr doc = xmlReadMemory(document.c_str(), (int)document.size(), utf8_from_wstring(filename).c_str(), NULL, XML_PARSE_NONET); if (doc == NULL) { LOGERROR("RelaxNGValidator: Failed to parse document '%s'", utf8_from_wstring(filename).c_str()); return false; } bool ret = ValidateEncoded(doc); xmlFreeDoc(doc); return ret; } bool RelaxNGValidator::ValidateEncoded(xmlDocPtr doc) const { xmlRelaxNGValidCtxtPtr ctxt = xmlRelaxNGNewValidCtxt(m_Schema); xmlRelaxNGSetValidStructuredErrors(ctxt, &relaxNGErrorHandler, NULL); int ret = xmlRelaxNGValidateDoc(ctxt, doc); xmlRelaxNGFreeValidCtxt(ctxt); if (ret == 0) { return true; } else if (ret > 0) { LOGERROR("RelaxNGValidator: Validation failed for '%s'", doc->name); return false; } else { LOGERROR("RelaxNGValidator: Internal error %d", ret); return false; } } bool RelaxNGValidator::CanValidate() const { return m_Schema != NULL; } Index: ps/trunk/source/ps/XML/Xeromyces.cpp =================================================================== --- ps/trunk/source/ps/XML/Xeromyces.cpp (revision 22343) +++ ps/trunk/source/ps/XML/Xeromyces.cpp (revision 22344) @@ -1,420 +1,421 @@ /* 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 #include +#include #include #include #include "maths/MD5.h" #include "ps/CacheLoader.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "RelaxNG.h" #include "Xeromyces.h" #include -static CMutex g_ValidatorCacheLock; +static std::mutex g_ValidatorCacheLock; static std::map g_ValidatorCache; static bool g_XeromycesStarted = false; static void errorHandler(void* UNUSED(userData), xmlErrorPtr error) { // Strip a trailing newline std::string message = error->message; if (message.length() > 0 && message[message.length()-1] == '\n') message.erase(message.length()-1); LOGERROR("CXeromyces: Parse %s: %s:%d: %s", error->level == XML_ERR_WARNING ? "warning" : "error", error->file, error->line, message); // TODO: The (non-fatal) warnings and errors don't get stored in the XMB, // so the caching is less transparent than it should be } void CXeromyces::Startup() { ENSURE(!g_XeromycesStarted); xmlInitParser(); xmlSetStructuredErrorFunc(NULL, &errorHandler); - CScopeLock lock(g_ValidatorCacheLock); + std::lock_guard lock(g_ValidatorCacheLock); g_ValidatorCache.insert(std::make_pair(std::string(), RelaxNGValidator())); g_XeromycesStarted = true; } void CXeromyces::Terminate() { ENSURE(g_XeromycesStarted); g_XeromycesStarted = false; ClearSchemaCache(); - CScopeLock lock(g_ValidatorCacheLock); + std::lock_guard lock(g_ValidatorCacheLock); g_ValidatorCache.clear(); xmlSetStructuredErrorFunc(NULL, NULL); xmlCleanupParser(); } bool CXeromyces::AddValidator(const PIVFS& vfs, const std::string& name, const VfsPath& grammarPath) { ENSURE(g_XeromycesStarted); RelaxNGValidator validator; if (!validator.LoadGrammarFile(vfs, grammarPath)) { LOGERROR("CXeromyces: failed adding validator for '%s'", grammarPath.string8()); return false; } { - CScopeLock lock(g_ValidatorCacheLock); + std::lock_guard lock(g_ValidatorCacheLock); std::map::iterator it = g_ValidatorCache.find(name); if (it != g_ValidatorCache.end()) g_ValidatorCache.erase(it); g_ValidatorCache.insert(std::make_pair(name, validator)); } return true; } bool CXeromyces::ValidateEncoded(const std::string& name, const std::wstring& filename, const std::string& document) { - CScopeLock lock(g_ValidatorCacheLock); + std::lock_guard lock(g_ValidatorCacheLock); return GetValidator(name).ValidateEncoded(filename, document); } /** * NOTE: Callers MUST acquire the g_ValidatorCacheLock before calling this. */ RelaxNGValidator& CXeromyces::GetValidator(const std::string& name) { if (g_ValidatorCache.find(name) == g_ValidatorCache.end()) return g_ValidatorCache.find("")->second; return g_ValidatorCache.find(name)->second; } PSRETURN CXeromyces::Load(const PIVFS& vfs, const VfsPath& filename, const std::string& validatorName /* = "" */) { ENSURE(g_XeromycesStarted); CCacheLoader cacheLoader(vfs, L".xmb"); MD5 validatorGrammarHash; { - CScopeLock lock(g_ValidatorCacheLock); + std::lock_guard lock(g_ValidatorCacheLock); validatorGrammarHash = GetValidator(validatorName).GetGrammarHash(); } VfsPath xmbPath; Status ret = cacheLoader.TryLoadingCached(filename, validatorGrammarHash, XMBVersion, xmbPath); if (ret == INFO::OK) { // Found a cached XMB - load it if (ReadXMBFile(vfs, xmbPath)) return PSRETURN_OK; // If this fails then we'll continue and (re)create the loose cache - // this failure legitimately happens due to partially-written XMB files. } else if (ret == INFO::SKIPPED) { // No cached version was found - we'll need to create it } else { ENSURE(ret < 0); // No source file or archive cache was found, so we can't load the // XML file at all LOGERROR("CCacheLoader failed to find archived or source file for: \"%s\"", filename.string8()); return PSRETURN_Xeromyces_XMLOpenFailed; } // XMB isn't up to date with the XML, so rebuild it return ConvertFile(vfs, filename, xmbPath, validatorName); } bool CXeromyces::GenerateCachedXMB(const PIVFS& vfs, const VfsPath& sourcePath, VfsPath& archiveCachePath, const std::string& validatorName /* = "" */) { CCacheLoader cacheLoader(vfs, L".xmb"); archiveCachePath = cacheLoader.ArchiveCachePath(sourcePath); return (ConvertFile(vfs, sourcePath, VfsPath("cache") / archiveCachePath, validatorName) == PSRETURN_OK); } PSRETURN CXeromyces::ConvertFile(const PIVFS& vfs, const VfsPath& filename, const VfsPath& xmbPath, const std::string& validatorName) { CVFSFile input; if (input.Load(vfs, filename)) { LOGERROR("CXeromyces: Failed to open XML file %s", filename.string8()); return PSRETURN_Xeromyces_XMLOpenFailed; } xmlDocPtr doc = xmlReadMemory((const char*)input.GetBuffer(), input.GetBufferSize(), CStrW(filename.string()).ToUTF8().c_str(), NULL, XML_PARSE_NONET|XML_PARSE_NOCDATA); if (!doc) { LOGERROR("CXeromyces: Failed to parse XML file %s", filename.string8()); return PSRETURN_Xeromyces_XMLParseError; } { - CScopeLock lock(g_ValidatorCacheLock); + std::lock_guard lock(g_ValidatorCacheLock); RelaxNGValidator& validator = GetValidator(validatorName); if (validator.CanValidate() && !validator.ValidateEncoded(doc)) // For now, log the error and continue, in the future we might fail LOGERROR("CXeromyces: failed to validate XML file %s", filename.string8()); } WriteBuffer writeBuffer; CreateXMB(doc, writeBuffer); xmlFreeDoc(doc); // Save the file to disk, so it can be loaded quickly next time vfs->CreateFile(xmbPath, writeBuffer.Data(), writeBuffer.Size()); m_XMBBuffer = writeBuffer.Data(); // add a reference // Set up the XMBFile const bool ok = Initialise((const char*)m_XMBBuffer.get()); ENSURE(ok); return PSRETURN_OK; } bool CXeromyces::ReadXMBFile(const PIVFS& vfs, const VfsPath& filename) { size_t size; if(vfs->LoadFile(filename, m_XMBBuffer, size) < 0) return false; // if the game crashes during loading, (e.g. due to driver bugs), // it sometimes leaves empty XMB files in the cache. // reporting failure will cause our caller to re-generate the XMB. if(size == 0) return false; ENSURE(size >= 4); // make sure it's at least got the initial header // Set up the XMBFile if(!Initialise((const char*)m_XMBBuffer.get())) return false; return true; } PSRETURN CXeromyces::LoadString(const char* xml, const std::string& validatorName /* = "" */) { ENSURE(g_XeromycesStarted); xmlDocPtr doc = xmlReadMemory(xml, (int)strlen(xml), "(no file)", NULL, XML_PARSE_NONET|XML_PARSE_NOCDATA); if (!doc) { LOGERROR("CXeromyces: Failed to parse XML string"); return PSRETURN_Xeromyces_XMLParseError; } { - CScopeLock lock(g_ValidatorCacheLock); + std::lock_guard lock(g_ValidatorCacheLock); RelaxNGValidator& validator = GetValidator(validatorName); if (validator.CanValidate() && !validator.ValidateEncoded(doc)) // For now, log the error and continue, in the future we might fail LOGERROR("CXeromyces: failed to validate XML string"); } WriteBuffer writeBuffer; CreateXMB(doc, writeBuffer); xmlFreeDoc(doc); m_XMBBuffer = writeBuffer.Data(); // add a reference // Set up the XMBFile const bool ok = Initialise((const char*)m_XMBBuffer.get()); ENSURE(ok); return PSRETURN_OK; } static void FindNames(const xmlNodePtr node, std::set& elementNames, std::set& attributeNames) { elementNames.insert((const char*)node->name); for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) attributeNames.insert((const char*)attr->name); for (xmlNodePtr child = node->children; child; child = child->next) if (child->type == XML_ELEMENT_NODE) FindNames(child, elementNames, attributeNames); } static void OutputElement(const xmlNodePtr node, WriteBuffer& writeBuffer, std::map& elementIDs, std::map& attributeIDs ) { // Filled in later with the length of the element size_t posLength = writeBuffer.Size(); writeBuffer.Append("????", 4); writeBuffer.Append(&elementIDs[(const char*)node->name], 4); u32 attrCount = 0; for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) ++attrCount; writeBuffer.Append(&attrCount, 4); u32 childCount = 0; for (xmlNodePtr child = node->children; child; child = child->next) if (child->type == XML_ELEMENT_NODE) ++childCount; writeBuffer.Append(&childCount, 4); // Filled in later with the offset to the list of child elements size_t posChildrenOffset = writeBuffer.Size(); writeBuffer.Append("????", 4); // Trim excess whitespace in the entity's text, while counting // the number of newlines trimmed (so that JS error reporting // can give the correct line number within the script) std::string whitespace = " \t\r\n"; std::string text; for (xmlNodePtr child = node->children; child; child = child->next) { if (child->type == XML_TEXT_NODE) { xmlChar* content = xmlNodeGetContent(child); text += std::string((const char*)content); xmlFree(content); } } u32 linenum = xmlGetLineNo(node); // Find the start of the non-whitespace section size_t first = text.find_first_not_of(whitespace); if (first == text.npos) // Entirely whitespace - easy to handle text = ""; else { // Count the number of \n being cut off, // and add them to the line number std::string trimmed (text.begin(), text.begin()+first); linenum += std::count(trimmed.begin(), trimmed.end(), '\n'); // Find the end of the non-whitespace section, // and trim off everything else size_t last = text.find_last_not_of(whitespace); text = text.substr(first, 1+last-first); } // Output text, prefixed by length in bytes if (text.length() == 0) { // No text; don't write much writeBuffer.Append("\0\0\0\0", 4); } else { // Write length and line number and null-terminated text u32 nodeLen = u32(4 + text.length()+1); writeBuffer.Append(&nodeLen, 4); writeBuffer.Append(&linenum, 4); writeBuffer.Append((void*)text.c_str(), nodeLen-4); } // Output attributes for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) { writeBuffer.Append(&attributeIDs[(const char*)attr->name], 4); xmlChar* value = xmlNodeGetContent(attr->children); u32 attrLen = u32(xmlStrlen(value)+1); writeBuffer.Append(&attrLen, 4); writeBuffer.Append((void*)value, attrLen); xmlFree(value); } // Go back and fill in the child-element offset u32 childrenOffset = (u32)(writeBuffer.Size() - (posChildrenOffset+4)); writeBuffer.Overwrite(&childrenOffset, 4, posChildrenOffset); // Output all child elements for (xmlNodePtr child = node->children; child; child = child->next) if (child->type == XML_ELEMENT_NODE) OutputElement(child, writeBuffer, elementIDs, attributeIDs); // Go back and fill in the length u32 length = (u32)(writeBuffer.Size() - posLength); writeBuffer.Overwrite(&length, 4, posLength); } PSRETURN CXeromyces::CreateXMB(const xmlDocPtr doc, WriteBuffer& writeBuffer) { // Header writeBuffer.Append(UnfinishedHeaderMagicStr, 4); // Version writeBuffer.Append(&XMBVersion, 4); u32 i; // Find the unique element/attribute names std::set elementNames; std::set attributeNames; FindNames(xmlDocGetRootElement(doc), elementNames, attributeNames); std::map elementIDs; std::map attributeIDs; // Output element names i = 0; u32 elementCount = (u32)elementNames.size(); writeBuffer.Append(&elementCount, 4); for (const std::string& n : elementNames) { u32 textLen = (u32)n.length()+1; writeBuffer.Append(&textLen, 4); writeBuffer.Append((void*)n.c_str(), textLen); elementIDs[n] = i++; } // Output attribute names i = 0; u32 attributeCount = (u32)attributeNames.size(); writeBuffer.Append(&attributeCount, 4); for (const std::string& n : attributeNames) { u32 textLen = (u32)n.length()+1; writeBuffer.Append(&textLen, 4); writeBuffer.Append((void*)n.c_str(), textLen); attributeIDs[n] = i++; } OutputElement(xmlDocGetRootElement(doc), writeBuffer, elementIDs, attributeIDs); // file is now valid, so insert correct magic string writeBuffer.Overwrite(HeaderMagicStr, 4, 0); return PSRETURN_OK; } Index: ps/trunk/source/soundmanager/SoundManager.cpp =================================================================== --- ps/trunk/source/soundmanager/SoundManager.cpp (revision 22343) +++ ps/trunk/source/soundmanager/SoundManager.cpp (revision 22344) @@ -1,819 +1,819 @@ -/* Copyright (C) 2018 Wildfire Games. +/* 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 "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/XML/Xeromyces.h" 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; int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this); ENSURE(ret == 0); } ~CSoundManagerWorker() { delete m_Items; CleanupItems(); delete m_DeadItems; } bool Shutdown() { { - CScopeLock lock(m_WorkerMutex); + std::lock_guard lock(m_WorkerMutex); m_Shutdown = true; ItemsList::iterator lstr = m_Items->begin(); while (lstr != m_Items->end()) { delete *lstr; ++lstr; } } pthread_join(m_WorkerThread, NULL); return true; } void addItem(ISoundItem* anItem) { - CScopeLock lock(m_WorkerMutex); + std::lock_guard lock(m_WorkerMutex); m_Items->push_back(anItem); } void CleanupItems() { - CScopeLock lock(m_DeadItemsMutex); + 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(void* data) { debug_SetThreadName("CSoundManagerWorker"); g_Profiler2.RegisterCurrentThread("soundmanager"); static_cast(data)->Run(); return NULL; } void Run() { while (true) { // Handle shutdown requests as soon as possible if (GetShutdown()) return; int pauseTime = 500; if (g_SoundManager->InDistress()) pauseTime = 50; { - CScopeLock workerLock(m_WorkerMutex); + 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 { - CScopeLock deadItemsLock(m_DeadItemsMutex); + 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() { - CScopeLock lock(m_WorkerMutex); + std::lock_guard lock(m_WorkerMutex); return m_Shutdown; } private: // Thread-related members: pthread_t m_WorkerThread; - CMutex m_WorkerMutex; - CMutex m_DeadItemsMutex; + 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); } 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() { - CScopeLock lock(m_DistressMutex); + 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() { - CScopeLock lock(m_DistressMutex); + std::lock_guard lock(m_DistressMutex); // Going into distress for normal reasons m_DistressTime = timer_Time(); } void CSoundManager::SetDistressThroughError() { - CScopeLock lock(m_DistressMutex); + 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; } } #else // CONFIG2_AUDIO void ISoundManager::CreateSoundManager(){} void ISoundManager::SetEnabled(bool UNUSED(doEnable)){} void ISoundManager::CloseGame(){} #endif // CONFIG2_AUDIO Index: ps/trunk/source/soundmanager/SoundManager.h =================================================================== --- ps/trunk/source/soundmanager/SoundManager.h (revision 22343) +++ ps/trunk/source/soundmanager/SoundManager.h (revision 22344) @@ -1,176 +1,177 @@ -/* Copyright (C) 2017 Wildfire Games. +/* 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 . */ #ifndef INCLUDED_SOUNDMANAGER_H #define INCLUDED_SOUNDMANAGER_H #include "lib/config2.h" #if CONFIG2_AUDIO #include "ISoundManager.h" #include "data/SoundData.h" #include "items/ISoundItem.h" #include "scripting/SoundGroup.h" #include "lib/external_libraries/openal.h" #include "lib/file/vfs/vfs_path.h" #include "ps/Profiler2.h" #include "simulation2/system/Entity.h" -#include #include +#include +#include #define AL_CHECK CSoundManager::al_check(__func__, __LINE__) struct ALSourceHolder { /// Title of the column ALuint ALSource; ISoundItem* SourceItem; }; typedef std::vector PlayList; typedef std::vector ItemsList; typedef std::map ItemsMap; typedef std::map SoundGroupMap; class CSoundManagerWorker; class CSoundManager : public ISoundManager { NONCOPYABLE(CSoundManager); protected: ALCcontext* m_Context; ALCdevice* m_Device; ALSourceHolder* m_ALSourceBuffer; ISoundItem* m_CurrentTune; ISoundItem* m_CurrentEnvirons; CSoundManagerWorker* m_Worker; - CMutex m_DistressMutex; + std::mutex m_DistressMutex; PlayList* m_PlayListItems; SoundGroupMap m_SoundGroups; float m_Gain; float m_MusicGain; float m_AmbientGain; float m_ActionGain; float m_UIGain; bool m_Enabled; long m_BufferSize; int m_BufferCount; bool m_SoundEnabled; bool m_MusicEnabled; bool m_MusicPaused; bool m_AmbientPaused; bool m_ActionPaused; bool m_RunningPlaylist; bool m_PlayingPlaylist; bool m_LoopingPlaylist; long m_PlaylistGap; long m_DistressErrCount; long m_DistressTime; public: CSoundManager(); virtual ~CSoundManager(); void StartWorker(); ISoundItem* LoadItem(const VfsPath& itemPath); ISoundItem* ItemForData(CSoundData* itemData); ISoundItem* ItemForEntity(entity_id_t source, CSoundData* sndData); Status ReloadChangedFiles(const VfsPath& path); void ClearPlayListItems(); void StartPlayList(bool doLoop); void AddPlayListItem(const VfsPath& itemPath); static void CreateSoundManager(); static void SetEnabled(bool doEnable); static Status ReloadChangedFileCB(void* param, const VfsPath& path); static void CloseGame(); static void al_ReportError(ALenum err, const char* caller, int line); static void al_check(const char* caller, int line); void SetMusicEnabled(bool isEnabled); void SetSoundEnabled(bool enabled); ALuint GetALSource(ISoundItem* anItem); void ReleaseALSource(ALuint theSource); ISoundItem* ItemFromData(CSoundData* itemData); ISoundItem* ItemFromWAV(VfsPath& fname); ISoundItem* ItemFromOgg(VfsPath& fname); ISoundItem* GetSoundItem(unsigned long itemRow); unsigned long Count(); void IdleTask(); void SetMemoryUsage(long bufferSize, int bufferCount); long GetBufferCount(); long GetBufferSize(); void PlayAsMusic(const VfsPath& itemPath, bool looping); void PlayAsAmbient(const VfsPath& itemPath, bool looping); void PlayAsUI(const VfsPath& itemPath, bool looping); void PlayAsGroup(const VfsPath& groupPath, const CVector3D& sourcePos, entity_id_t source, bool ownedSound); void PlayGroupItem(ISoundItem* anItem, ALfloat groupGain); bool InDistress(); void SetDistressThroughShortage(); void SetDistressThroughError(); void Pause(bool pauseIt); void PauseMusic(bool pauseIt); void PauseAmbient(bool pauseIt); void PauseAction(bool pauseIt); void SetAmbientItem(ISoundItem* anItem); void SetMasterGain(float gain); void SetMusicGain(float gain); void SetAmbientGain(float gain); void SetActionGain(float gain); void SetUIGain(float gain); protected: void InitListener(); Status AlcInit(); void SetMusicItem(ISoundItem* anItem); private: CSoundManager(CSoundManager* UNUSED(other)){}; }; #else // !CONFIG2_AUDIO #define AL_CHECK #endif // !CONFIG2_AUDIO #endif // INCLUDED_SOUNDMANAGER_H Index: ps/trunk/source/soundmanager/items/CBufferItem.cpp =================================================================== --- ps/trunk/source/soundmanager/items/CBufferItem.cpp (revision 22343) +++ ps/trunk/source/soundmanager/items/CBufferItem.cpp (revision 22344) @@ -1,129 +1,131 @@ -/* Copyright (C) 2015 Wildfire Games. +/* 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 "CBufferItem.h" #if CONFIG2_AUDIO #include "soundmanager/SoundManager.h" #include "soundmanager/data/SoundData.h" +#include + CBufferItem::CBufferItem(CSoundData* sndData) { ResetVars(); if (InitOpenAL()) Attach(sndData); } CBufferItem::~CBufferItem() { Stop(); ReleaseOpenALBuffer(); } void CBufferItem::ReleaseOpenALBuffer() { if ( m_ALSource == 0 ) return; int num_processed; AL_CHECK; alGetSourcei(m_ALSource, AL_BUFFERS_PROCESSED, &num_processed); AL_CHECK; if (num_processed > 0) { ALuint* al_buf = new ALuint[num_processed]; alSourceUnqueueBuffers(m_ALSource, num_processed, al_buf); AL_CHECK; delete[] al_buf; } alSourcei(m_ALSource, AL_BUFFER, 0); ((CSoundManager*)g_SoundManager)->ReleaseALSource(m_ALSource); AL_CHECK; m_ALSource = 0; } bool CBufferItem::IdleTask() { if ( m_ALSource == 0 ) return false; HandleFade(); if (m_LastPlay) { - CScopeLock lock(m_ItemMutex); + std::lock_guard lock(m_ItemMutex); int proc_state; alGetSourcei(m_ALSource, AL_SOURCE_STATE, &proc_state); AL_CHECK; m_ShouldBePlaying = (proc_state != AL_STOPPED); return (proc_state != AL_STOPPED); } if (GetLooping()) { int num_processed; AL_CHECK; alGetSourcei(m_ALSource, AL_BUFFERS_PROCESSED, &num_processed); AL_CHECK; for (int i = 0; i < num_processed; i++) { ALuint al_buf; alSourceUnqueueBuffers(m_ALSource, 1, &al_buf); AL_CHECK; alSourceQueueBuffers(m_ALSource, 1, &al_buf); AL_CHECK; } } return true; } void CBufferItem::Attach(CSoundData* itemData) { if ( m_ALSource == 0 ) return; AL_CHECK; if (m_SoundData != NULL) { CSoundData::ReleaseSoundData(m_SoundData); m_SoundData = 0; } AL_CHECK; if (itemData != NULL) { m_SoundData = itemData->IncrementCount(); alSourceQueueBuffers(m_ALSource, m_SoundData->GetBufferCount(),(const ALuint *) m_SoundData->GetBufferPtr()); AL_CHECK; } } void CBufferItem::SetLooping(bool loops) { m_Looping = loops; } #endif // CONFIG2_AUDIO Index: ps/trunk/source/soundmanager/items/CSoundBase.cpp =================================================================== --- ps/trunk/source/soundmanager/items/CSoundBase.cpp (revision 22343) +++ ps/trunk/source/soundmanager/items/CSoundBase.cpp (revision 22344) @@ -1,390 +1,390 @@ -/* Copyright (C) 2015 Wildfire Games. +/* 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 "CSoundBase.h" #if CONFIG2_AUDIO #include "lib/timer.h" #include "soundmanager/SoundManager.h" #include "soundmanager/data/SoundData.h" #include "ps/CLogger.h" CSoundBase::CSoundBase() { ResetVars(); } CSoundBase::~CSoundBase() { Stop(); ReleaseOpenAL(); } void CSoundBase::ReleaseOpenAL() { if (m_ALSource != 0) { AL_CHECK; alSourcei(m_ALSource, AL_BUFFER, 0L); AL_CHECK; ((CSoundManager*)g_SoundManager)->ReleaseALSource(m_ALSource); AL_CHECK; m_ALSource = 0; } if (m_SoundData != 0) { AL_CHECK; CSoundData::ReleaseSoundData(m_SoundData); AL_CHECK; m_SoundData = 0; } } void CSoundBase::Attach(CSoundData* itemData) { UNUSED2(itemData); } void CSoundBase::ResetVars() { m_ALSource = 0; m_SoundData = 0; m_LastPlay = false; m_Looping = false; m_StartFadeTime = 0; m_EndFadeTime = 0; m_StartVolume = 0; m_EndVolume = 0; m_ShouldBePlaying = false; m_IsPaused = false; ResetFade(); } void CSoundBase::ResetFade() { m_StartFadeTime = 0; m_EndFadeTime = 0; m_StartVolume = 0; m_EndVolume = 0; m_PauseAfterFade = false; } bool CSoundBase::Finished() { return !m_ShouldBePlaying && !IsPlaying(); } bool CSoundBase::InitOpenAL() { alGetError(); /* clear error */ m_ALSource = ((CSoundManager*)g_SoundManager)->GetALSource( this ); AL_CHECK; if ( m_ALSource ) { alSourcef(m_ALSource,AL_PITCH,1.0f); AL_CHECK; alSourcef(m_ALSource,AL_GAIN,1.0f); AL_CHECK; alSourcei(m_ALSource,AL_LOOPING,AL_FALSE); AL_CHECK; return true; } else { // LOGERROR("Source not allocated by SoundManager\n", 0); } return false; } void CSoundBase::SetGain(ALfloat gain) { AL_CHECK; if ( m_ALSource ) { - CScopeLock lock(m_ItemMutex); + std::lock_guard lock(m_ItemMutex); alSourcef(m_ALSource, AL_GAIN, gain); AL_CHECK; } } void CSoundBase::SetRollOff(ALfloat rolls) { if ( m_ALSource ) { - CScopeLock lock(m_ItemMutex); + std::lock_guard lock(m_ItemMutex); alSourcef(m_ALSource, AL_REFERENCE_DISTANCE, 70.0f); AL_CHECK; alSourcef(m_ALSource, AL_MAX_DISTANCE, 200.0); AL_CHECK; alSourcef(m_ALSource, AL_ROLLOFF_FACTOR, rolls); AL_CHECK; } } void CSoundBase::EnsurePlay() { if (m_ShouldBePlaying && !m_IsPaused && !IsPlaying()) Play(); } void CSoundBase::SetCone(ALfloat innerCone, ALfloat outerCone, ALfloat coneGain) { if ( m_ALSource ) { - CScopeLock lock(m_ItemMutex); + std::lock_guard lock(m_ItemMutex); AL_CHECK; alSourcef(m_ALSource, AL_CONE_INNER_ANGLE, innerCone); AL_CHECK; alSourcef(m_ALSource, AL_CONE_OUTER_ANGLE, outerCone); AL_CHECK; alSourcef(m_ALSource, AL_CONE_OUTER_GAIN, coneGain); AL_CHECK; } } void CSoundBase::SetPitch(ALfloat pitch) { if ( m_ALSource ) { - CScopeLock lock(m_ItemMutex); + std::lock_guard lock(m_ItemMutex); alSourcef(m_ALSource, AL_PITCH, pitch); AL_CHECK; } } void CSoundBase::SetDirection(const CVector3D& direction) { if ( m_ALSource ) { - CScopeLock lock(m_ItemMutex); + std::lock_guard lock(m_ItemMutex); alSourcefv(m_ALSource, AL_DIRECTION, direction.GetFloatArray()); AL_CHECK; } } bool CSoundBase::IsPlaying() { if ( m_ALSource ) { - CScopeLock lock(m_ItemMutex); + std::lock_guard lock(m_ItemMutex); int proc_state; alGetSourcei(m_ALSource, AL_SOURCE_STATE, &proc_state); AL_CHECK; return (proc_state == AL_PLAYING); } return false; } void CSoundBase::SetLastPlay(bool last) { m_LastPlay = last; } bool CSoundBase::IdleTask() { return true; } void CSoundBase::SetLocation (const CVector3D& position) { if ( m_ALSource != 0 ) { - CScopeLock lock(m_ItemMutex); + std::lock_guard lock(m_ItemMutex); alSourcefv(m_ALSource,AL_POSITION, position.GetFloatArray()); AL_CHECK; } } bool CSoundBase::HandleFade() { AL_CHECK; if (m_ALSource == 0) return true; if (m_StartFadeTime != 0) { double currTime = timer_Time(); double pctDone = std::min(1.0, (currTime - m_StartFadeTime) / (m_EndFadeTime - m_StartFadeTime)); pctDone = std::max(0.0, pctDone); ALfloat curGain = ((m_EndVolume - m_StartVolume) * pctDone) + m_StartVolume; if (curGain == 0) { if ( m_PauseAfterFade ) Pause(); else Stop(); } else if (curGain == m_EndVolume) { if (m_ALSource != 0) alSourcef(m_ALSource, AL_GAIN, curGain); ResetFade(); } else if (m_ALSource != 0) alSourcef(m_ALSource, AL_GAIN, curGain); AL_CHECK; } return true; } bool CSoundBase::IsFading() { return ((m_ALSource != 0) && (m_StartFadeTime != 0)); } bool CSoundBase::GetLooping() { return m_Looping; } void CSoundBase::SetLooping(bool loops) { m_Looping = loops; if (m_ALSource != 0) { alSourcei(m_ALSource, AL_LOOPING, loops ? AL_TRUE : AL_FALSE); AL_CHECK; } } void CSoundBase::Play() { - CScopeLock lock(m_ItemMutex); + std::lock_guard lock(m_ItemMutex); m_ShouldBePlaying = true; m_IsPaused = false; AL_CHECK; if (m_ALSource != 0) { alSourcePlay(m_ALSource); ALenum err = alGetError(); if (err != AL_NO_ERROR) { if (err == AL_INVALID) ((CSoundManager*)g_SoundManager)->SetDistressThroughError(); else ((CSoundManager*)g_SoundManager)->al_ReportError(err, __func__, __LINE__); } } } void CSoundBase::PlayAndDelete() { SetLastPlay(true); Play(); } void CSoundBase::FadeAndPause(double fadeTime) { m_PauseAfterFade = true; FadeToIn(0, fadeTime); } void CSoundBase::FadeAndDelete(double fadeTime) { SetLastPlay(true); FadeToIn(0, fadeTime); } void CSoundBase::StopAndDelete() { SetLastPlay(true); Stop(); } void CSoundBase::PlayLoop() { if (m_ALSource != 0) { SetLooping(true); Play(); AL_CHECK; } } void CSoundBase::FadeToIn(ALfloat newVolume, double fadeDuration) { if (m_ALSource != 0) { ALenum proc_state; alGetSourcei(m_ALSource, AL_SOURCE_STATE, &proc_state); if (proc_state == AL_PLAYING) { m_StartFadeTime = timer_Time(); m_EndFadeTime = m_StartFadeTime + fadeDuration; alGetSourcef(m_ALSource, AL_GAIN, &m_StartVolume); m_EndVolume = newVolume; } AL_CHECK; } } void CSoundBase::Stop() { m_ShouldBePlaying = false; if (m_ALSource != 0) { - CScopeLock lock(m_ItemMutex); + std::lock_guard lock(m_ItemMutex); AL_CHECK; alSourcei(m_ALSource, AL_LOOPING, AL_FALSE); AL_CHECK; alSourceStop(m_ALSource); AL_CHECK; } } const Path CSoundBase::GetName() { if (m_SoundData) return m_SoundData->GetFileName(); return Path(); } void CSoundBase::Pause() { if (m_ALSource != 0) { m_IsPaused = true; alSourcePause(m_ALSource); AL_CHECK; } } void CSoundBase::Resume() { if (m_ALSource != 0) { alSourcePlay(m_ALSource); AL_CHECK; } } #endif // CONFIG2_AUDIO Index: ps/trunk/source/soundmanager/items/CSoundItem.cpp =================================================================== --- ps/trunk/source/soundmanager/items/CSoundItem.cpp (revision 22343) +++ ps/trunk/source/soundmanager/items/CSoundItem.cpp (revision 22344) @@ -1,84 +1,86 @@ -/* Copyright (C) 2015 Wildfire Games. +/* 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 "CSoundItem.h" #if CONFIG2_AUDIO #include "soundmanager/SoundManager.h" #include "soundmanager/data/SoundData.h" +#include + CSoundItem::CSoundItem() { ResetVars(); } CSoundItem::CSoundItem(CSoundData* sndData) { ResetVars(); if (InitOpenAL()) Attach(sndData); } CSoundItem::~CSoundItem() { Stop(); ReleaseOpenAL(); } bool CSoundItem::IdleTask() { if ( m_ALSource == 0 ) return false; HandleFade(); if (m_LastPlay && m_ALSource) { - CScopeLock lock(m_ItemMutex); + std::lock_guard lock(m_ItemMutex); int proc_state; alGetSourcei(m_ALSource, AL_SOURCE_STATE, &proc_state); AL_CHECK; m_ShouldBePlaying = (proc_state != AL_STOPPED); return (proc_state != AL_STOPPED); } return true; } void CSoundItem::Attach(CSoundData* itemData) { if (m_SoundData != NULL) { CSoundData::ReleaseSoundData(m_SoundData); m_SoundData = 0; } if (itemData != NULL) { AL_CHECK; alSourcei(m_ALSource, AL_BUFFER, 0); AL_CHECK; m_SoundData = itemData->IncrementCount(); alSourcei(m_ALSource, AL_BUFFER, m_SoundData->GetBuffer()); AL_CHECK; } } #endif // CONFIG2_AUDIO Index: ps/trunk/source/tools/atlas/GameInterface/MessagePasserImpl.h =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/MessagePasserImpl.h (revision 22343) +++ ps/trunk/source/tools/atlas/GameInterface/MessagePasserImpl.h (revision 22344) @@ -1,49 +1,51 @@ -/* Copyright (C) 2009 Wildfire Games. +/* 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 . */ #ifndef INCLUDED_MESSAGEPASSERIMPL #define INCLUDED_MESSAGEPASSERIMPL #include "MessagePasser.h" -#include "ps/ThreadUtil.h" +#include "lib/posix/posix_pthread.h" #include "ps/CStr.h" #include +#include + class MessagePasserImpl : public AtlasMessage::MessagePasser { NONCOPYABLE(MessagePasserImpl); public: MessagePasserImpl(); ~MessagePasserImpl(); virtual void Add(AtlasMessage::IMessage* msg); virtual AtlasMessage::IMessage* Retrieve(); virtual void Query(AtlasMessage::QueryMessage* qry, void(*timeoutCallback)()); bool IsEmpty(); void SetTrace(bool t); private: - CMutex m_Mutex; + std::mutex m_Mutex; CStr m_SemaphoreName; sem_t* m_Semaphore; std::queue m_Queue; bool m_Trace; }; #endif // INCLUDED_MESSAGEPASSERIMPL