Index: ps/trunk/source/graphics/MapGenerator.cpp
===================================================================
--- ps/trunk/source/graphics/MapGenerator.cpp (revision 22648)
+++ ps/trunk/source/graphics/MapGenerator.cpp (revision 22649)
@@ -1,437 +1,434 @@
/* 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 "simulation2/helpers/MapEdgeTiles.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);
+ m_WorkerThread.join();
}
void CMapGeneratorWorker::Initialize(const VfsPath& scriptFile, const std::string& settings)
{
std::lock_guard lock(m_WorkerMutex);
// Set progress to positive value
m_Progress = 1;
m_ScriptPath = scriptFile;
m_Settings = settings;
// Launch the worker thread
- int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this);
- ENSURE(ret == 0);
+ m_WorkerThread = std::thread(RunThread, this);
}
-void* CMapGeneratorWorker::RunThread(void *data)
+void* CMapGeneratorWorker::RunThread(CMapGeneratorWorker* self)
{
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
std::lock_guard lock(self->m_WorkerMutex);
self->m_Progress = -1;
}
SAFE_DELETE(self->m_ScriptInterface);
// At this point the random map scripts are done running, so the thread has no further purpose
// and can die. The data will be stored in m_MapData already if successful, or m_Progress
// will contain an error value on failure.
return NULL;
}
bool CMapGeneratorWorker::Run()
{
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);
RegisterScriptFunctions();
// 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;
}
void CMapGeneratorWorker::RegisterScriptFunctions()
{
// VFS
JSI_VFS::RegisterScriptFunctions_Maps(*m_ScriptInterface);
// Globalscripts may use VFS script functions
m_ScriptInterface->LoadGlobalScripts();
// File loading
m_ScriptInterface->RegisterFunction("LoadLibrary");
m_ScriptInterface->RegisterFunction("LoadHeightmapImage");
m_ScriptInterface->RegisterFunction("LoadMapTerrain");
// Progression and profiling
m_ScriptInterface->RegisterFunction("SetProgress");
m_ScriptInterface->RegisterFunction("GetMicroseconds");
m_ScriptInterface->RegisterFunction("ExportMap");
// Template functions
m_ScriptInterface->RegisterFunction("GetTemplate");
m_ScriptInterface->RegisterFunction("TemplateExists");
m_ScriptInterface->RegisterFunction, std::string, bool, CMapGeneratorWorker::FindTemplates>("FindTemplates");
m_ScriptInterface->RegisterFunction, std::string, bool, CMapGeneratorWorker::FindActorTemplates>("FindActorTemplates");
// Engine constants
// Length of one tile of the terrain grid in metres.
// Useful to transform footprint sizes to the tilegrid coordinate system.
m_ScriptInterface->SetGlobal("TERRAIN_TILE_SIZE", static_cast(TERRAIN_TILE_SIZE));
// Number of impassable tiles at the map border
m_ScriptInterface->SetGlobal("MAP_BORDER_WIDTH", static_cast(MAP_EDGE_TILES));
}
int CMapGeneratorWorker::GetProgress()
{
std::lock_guard lock(m_WorkerMutex);
return m_Progress;
}
double CMapGeneratorWorker::GetMicroseconds(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
{
return JS_Now();
}
shared_ptr CMapGeneratorWorker::GetResults()
{
std::lock_guard lock(m_WorkerMutex);
return m_MapData;
}
bool CMapGeneratorWorker::LoadLibrary(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& 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
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
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);
}
bool CMapGeneratorWorker::LoadScripts(const VfsPath& libraryName)
{
// Ignore libraries that are already loaded
if (m_LoadedLibraries.find(libraryName) != m_LoadedLibraries.end())
return true;
// Mark this as loaded, to prevent it recursively loading itself
m_LoadedLibraries.insert(libraryName);
VfsPath path = VfsPath(L"maps/random/") / libraryName / VfsPath();
VfsPaths pathnames;
// Load all scripts in mapgen directory
Status ret = vfs::GetPathnames(g_VFS, path, L"*.js", pathnames);
if (ret == INFO::OK)
{
for (const VfsPath& p : pathnames)
{
LOGMESSAGE("Loading map generator script '%s'", p.string8());
if (!m_ScriptInterface->LoadGlobalScriptFile(p))
{
LOGERROR("CMapGeneratorWorker::LoadScripts: Failed to load script '%s'", p.string8());
return false;
}
}
}
else
{
// Some error reading directory
wchar_t error[200];
LOGERROR("CMapGeneratorWorker::LoadScripts: Error reading scripts in directory '%s': %s", path.string8(), utf8_from_wstring(StatusDescription(ret, error, ARRAY_SIZE(error))));
return false;
}
return true;
}
JS::Value CMapGeneratorWorker::LoadHeightmap(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& filename)
{
std::vector heightmap;
if (LoadHeightmapImageVfs(filename, heightmap) != INFO::OK)
{
LOGERROR("Could not load heightmap file '%s'", filename.string8());
return JS::UndefinedValue();
}
CMapGeneratorWorker* self = static_cast(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 VfsPath& filename)
{
CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData);
JSContext* cx = self->m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
if (!VfsFileExists(filename))
{
self->m_ScriptInterface->ReportError(
("Terrain file \"" + filename.string8() + "\" does not exist!").c_str());
return JS::UndefinedValue();
}
CFileUnpacker unpacker;
unpacker.Read(filename, "PSMP");
if (unpacker.GetVersion() < CMapIO::FILE_READ_VERSION)
{
self->m_ScriptInterface->ReportError(
("Could not load terrain file \"" + filename.string8() + "\" too old version!").c_str());
return JS::UndefinedValue();
}
// unpack size
ssize_t patchesPerSide = (ssize_t)unpacker.UnpackSize();
size_t verticesPerSide = patchesPerSide * PATCH_SIZE + 1;
// unpack heightmap
std::vector heightmap;
heightmap.resize(SQR(verticesPerSide));
unpacker.UnpackRaw(&heightmap[0], SQR(verticesPerSide) * sizeof(u16));
// unpack texture names
size_t textureCount = unpacker.UnpackSize();
std::vector textureNames;
textureNames.reserve(textureCount);
for (size_t i = 0; i < textureCount; ++i)
{
CStr texturename;
unpacker.UnpackString(texturename);
textureNames.push_back(texturename);
}
// unpack texture IDs per tile
ssize_t tilesPerSide = patchesPerSide * PATCH_SIZE;
std::vector tiles;
tiles.resize(size_t(SQR(tilesPerSide)));
unpacker.UnpackRaw(&tiles[0], sizeof(CMapIO::STileDesc) * tiles.size());
// reorder by patches and store and save texture IDs per tile
std::vector textureIDs;
for (ssize_t x = 0; x < tilesPerSide; ++x)
{
size_t patchX = x / PATCH_SIZE;
size_t offX = x % PATCH_SIZE;
for (ssize_t y = 0; y < tilesPerSide; ++y)
{
size_t patchY = y / PATCH_SIZE;
size_t offY = y % PATCH_SIZE;
// m_Priority and m_Tex2Index unused
textureIDs.push_back(tiles[(patchY * patchesPerSide + patchX) * SQR(PATCH_SIZE) + (offY * PATCH_SIZE + offX)].m_Tex1Index);
}
}
JS::RootedValue returnValue(cx);
self->m_ScriptInterface->CreateObject(
&returnValue,
"height", heightmap,
"textureNames", textureNames,
"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/graphics/MapGenerator.h
===================================================================
--- ps/trunk/source/graphics/MapGenerator.h (revision 22648)
+++ ps/trunk/source/graphics/MapGenerator.h (revision 22649)
@@ -1,241 +1,242 @@
/* 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/TemplateLoader.h"
#include "scriptinterface/ScriptInterface.h"
#include
#include
#include
#include
+#include
class CMapGeneratorWorker;
/**
* Random map generator interface. Initialized by CMapReader and then checked
* periodically during loading, until it's finished (progress value is 0).
*
* The actual work is performed by CMapGeneratorWorker in a separate thread.
*/
class CMapGenerator
{
NONCOPYABLE(CMapGenerator);
public:
CMapGenerator();
~CMapGenerator();
/**
* Start the map generator thread
*
* @param scriptFile The VFS path for the script, e.g. "maps/random/latium.js"
* @param settings JSON string containing settings for the map generator
*/
void GenerateMap(const VfsPath& scriptFile, const std::string& settings);
/**
* Get status of the map generator thread
*
* @return Progress percentage 1-100 if active, 0 when finished, or -1 on error
*/
int GetProgress();
/**
* Get random map data, according to this format:
* http://trac.wildfiregames.com/wiki/Random_Map_Generator_Internals#Dataformat
*
* @return StructuredClone containing map data
*/
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:
/**
* Expose functions defined in this class to the script.
*/
void RegisterScriptFunctions();
/**
* Load all scripts of the given library
*
* @param libraryName VfsPath specifying name of the library (subfolder of ../maps/random/)
* @return true if all scripts ran successfully, false if there's an error
*/
bool LoadScripts(const VfsPath& libraryName);
/**
* Recursively load all script files in the given folder.
*/
static bool LoadLibrary(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& name);
/**
* Finalize map generation and pass results from the script to the engine.
*/
static void ExportMap(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue data);
/**
* Load an image file and return it as a height array.
*/
static JS::Value LoadHeightmap(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& src);
/**
* Load an Atlas terrain file (PMP) returning textures and heightmap.
*/
static JS::Value LoadMapTerrain(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& filename);
/**
* Sets the map generation progress, which is one of multiple stages determining the loading screen progress.
*/
static void SetProgress(ScriptInterface::CxPrivate* pCxPrivate, int progress);
/**
* Microseconds since the epoch.
*/
static double GetMicroseconds(ScriptInterface::CxPrivate* pCxPrivate);
/**
* Return the template data of the given template name.
*/
static CParamNode GetTemplate(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName);
/**
* Check whether the given template exists.
*/
static bool TemplateExists(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName);
/**
* Returns all template names of simulation entity templates.
*/
static std::vector FindTemplates(ScriptInterface::CxPrivate* pCxPrivate, const std::string& path, bool includeSubdirectories);
/**
* Returns all template names of actors.
*/
static std::vector FindActorTemplates(ScriptInterface::CxPrivate* pCxPrivate, const std::string& path, bool includeSubdirectories);
/**
* Perform map generation in an independent thread.
*/
- static void* RunThread(void* data);
+ static void* RunThread(CMapGeneratorWorker* self);
/**
* Perform the map generation.
*/
bool Run();
/**
* Currently loaded script librarynames.
*/
std::set m_LoadedLibraries;
/**
* Result of the mapscript generation including terrain, entities and environment settings.
*/
shared_ptr m_MapData;
/**
* Deterministic random number generator.
*/
boost::rand48 m_MapGenRNG;
/**
* Current map generation progress.
*/
int m_Progress;
/**
* Provides the script context.
*/
ScriptInterface* m_ScriptInterface;
/**
* Map generation script to run.
*/
VfsPath m_ScriptPath;
/**
* Map and simulation settings chosen in the gamesetup stage.
*/
std::string m_Settings;
/**
* Backend to loading template data.
*/
CTemplateLoader m_TemplateLoader;
/**
* Holds the mapgeneration thread identifier.
*/
- pthread_t m_WorkerThread;
+ std::thread m_WorkerThread;
/**
* Avoids thread synchronization issues.
*/
std::mutex m_WorkerMutex;
};
#endif //INCLUDED_MAPGENERATOR
Index: ps/trunk/source/graphics/TextureConverter.cpp
===================================================================
--- ps/trunk/source/graphics/TextureConverter.cpp (revision 22648)
+++ ps/trunk/source/graphics/TextureConverter.cpp (revision 22649)
@@ -1,606 +1,598 @@
-/* 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 "TextureConverter.h"
#include "lib/regex.h"
#include "lib/timer.h"
#include "lib/allocators/shared_ptr.h"
#include "lib/tex/tex.h"
#include "maths/MD5.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
#include "ps/Profiler2.h"
#include "ps/XML/Xeromyces.h"
#if CONFIG2_NVTT
#include "nvtt/nvtt.h"
/**
* Output handler to collect NVTT's output into a simplistic buffer.
* WARNING: Used in the worker thread - must be thread-safe.
*/
struct BufferOutputHandler : public nvtt::OutputHandler
{
std::vector buffer;
virtual void beginImage(int UNUSED(size), int UNUSED(width), int UNUSED(height), int UNUSED(depth), int UNUSED(face), int UNUSED(miplevel))
{
}
virtual bool writeData(const void* data, int size)
{
size_t off = buffer.size();
buffer.resize(off + size);
memcpy(&buffer[off], data, size);
return true;
}
};
/**
* Request for worker thread to process.
*/
struct CTextureConverter::ConversionRequest
{
VfsPath dest;
CTexturePtr texture;
nvtt::InputOptions inputOptions;
nvtt::CompressionOptions compressionOptions;
nvtt::OutputOptions outputOptions;
bool isDXT1a; // see comment in RunThread
bool is8bpp;
};
/**
* Result from worker thread.
*/
struct CTextureConverter::ConversionResult
{
VfsPath dest;
CTexturePtr texture;
BufferOutputHandler output;
bool ret; // true if the conversion succeeded
};
#endif // CONFIG2_NVTT
void CTextureConverter::Settings::Hash(MD5& hash)
{
hash.Update((const u8*)&format, sizeof(format));
hash.Update((const u8*)&mipmap, sizeof(mipmap));
hash.Update((const u8*)&normal, sizeof(normal));
hash.Update((const u8*)&alpha, sizeof(alpha));
hash.Update((const u8*)&filter, sizeof(filter));
hash.Update((const u8*)&kaiserWidth, sizeof(kaiserWidth));
hash.Update((const u8*)&kaiserAlpha, sizeof(kaiserAlpha));
hash.Update((const u8*)&kaiserStretch, sizeof(kaiserStretch));
}
CTextureConverter::SettingsFile* CTextureConverter::LoadSettings(const VfsPath& path) const
{
CXeromyces XeroFile;
if (XeroFile.Load(m_VFS, path, "texture") != PSRETURN_OK)
return NULL;
// Define all the elements used in the XML file
#define EL(x) int el_##x = XeroFile.GetElementID(#x)
#define AT(x) int at_##x = XeroFile.GetAttributeID(#x)
EL(textures);
EL(file);
AT(pattern);
AT(format);
AT(mipmap);
AT(normal);
AT(alpha);
AT(filter);
AT(kaiserwidth);
AT(kaiseralpha);
AT(kaiserstretch);
#undef AT
#undef EL
XMBElement root = XeroFile.GetRoot();
if (root.GetNodeName() != el_textures)
{
LOGERROR("Invalid texture settings file \"%s\" (unrecognised root element)", path.string8());
return NULL;
}
std::unique_ptr settings(new SettingsFile());
XERO_ITER_EL(root, child)
{
if (child.GetNodeName() == el_file)
{
Match p;
XERO_ITER_ATTR(child, attr)
{
if (attr.Name == at_pattern)
{
p.pattern = attr.Value.FromUTF8();
}
else if (attr.Name == at_format)
{
CStr v(attr.Value);
if (v == "dxt1")
p.settings.format = FMT_DXT1;
else if (v == "dxt3")
p.settings.format = FMT_DXT3;
else if (v == "dxt5")
p.settings.format = FMT_DXT5;
else if (v == "rgba")
p.settings.format = FMT_RGBA;
else if (v == "alpha")
p.settings.format = FMT_ALPHA;
else
LOGERROR("Invalid attribute value ", v.c_str());
}
else if (attr.Name == at_mipmap)
{
CStr v(attr.Value);
if (v == "true")
p.settings.mipmap = MIP_TRUE;
else if (v == "false")
p.settings.mipmap = MIP_FALSE;
else
LOGERROR("Invalid attribute value ", v.c_str());
}
else if (attr.Name == at_normal)
{
CStr v(attr.Value);
if (v == "true")
p.settings.normal = NORMAL_TRUE;
else if (v == "false")
p.settings.normal = NORMAL_FALSE;
else
LOGERROR("Invalid attribute value ", v.c_str());
}
else if (attr.Name == at_alpha)
{
CStr v(attr.Value);
if (v == "none")
p.settings.alpha = ALPHA_NONE;
else if (v == "player")
p.settings.alpha = ALPHA_PLAYER;
else if (v == "transparency")
p.settings.alpha = ALPHA_TRANSPARENCY;
else
LOGERROR("Invalid attribute value ", v.c_str());
}
else if (attr.Name == at_filter)
{
CStr v(attr.Value);
if (v == "box")
p.settings.filter = FILTER_BOX;
else if (v == "triangle")
p.settings.filter = FILTER_TRIANGLE;
else if (v == "kaiser")
p.settings.filter = FILTER_KAISER;
else
LOGERROR("Invalid attribute value ", v.c_str());
}
else if (attr.Name == at_kaiserwidth)
{
p.settings.kaiserWidth = CStr(attr.Value).ToFloat();
}
else if (attr.Name == at_kaiseralpha)
{
p.settings.kaiserAlpha = CStr(attr.Value).ToFloat();
}
else if (attr.Name == at_kaiserstretch)
{
p.settings.kaiserStretch = CStr(attr.Value).ToFloat();
}
else
{
LOGERROR("Invalid attribute name ", XeroFile.GetAttributeString(attr.Name).c_str());
}
}
settings->patterns.push_back(p);
}
}
return settings.release();
}
CTextureConverter::Settings CTextureConverter::ComputeSettings(const std::wstring& filename, const std::vector& settingsFiles) const
{
// Set sensible defaults
Settings settings;
settings.format = FMT_DXT1;
settings.mipmap = MIP_TRUE;
settings.normal = NORMAL_FALSE;
settings.alpha = ALPHA_NONE;
settings.filter = FILTER_BOX;
settings.kaiserWidth = 3.f;
settings.kaiserAlpha = 4.f;
settings.kaiserStretch = 1.f;
for (size_t i = 0; i < settingsFiles.size(); ++i)
{
for (size_t j = 0; j < settingsFiles[i]->patterns.size(); ++j)
{
Match p = settingsFiles[i]->patterns[j];
// Check that the pattern matches the texture file
if (!match_wildcard(filename.c_str(), p.pattern.c_str()))
continue;
if (p.settings.format != FMT_UNSPECIFIED)
settings.format = p.settings.format;
if (p.settings.mipmap != MIP_UNSPECIFIED)
settings.mipmap = p.settings.mipmap;
if (p.settings.normal != NORMAL_UNSPECIFIED)
settings.normal = p.settings.normal;
if (p.settings.alpha != ALPHA_UNSPECIFIED)
settings.alpha = p.settings.alpha;
if (p.settings.filter != FILTER_UNSPECIFIED)
settings.filter = p.settings.filter;
if (p.settings.kaiserWidth != -1.f)
settings.kaiserWidth = p.settings.kaiserWidth;
if (p.settings.kaiserAlpha != -1.f)
settings.kaiserAlpha = p.settings.kaiserAlpha;
if (p.settings.kaiserStretch != -1.f)
settings.kaiserStretch = p.settings.kaiserStretch;
}
}
return settings;
}
CTextureConverter::CTextureConverter(PIVFS vfs, bool highQuality) :
m_VFS(vfs), m_HighQuality(highQuality), m_Shutdown(false)
{
// Verify that we are running with at least the version we were compiled with,
// to avoid bugs caused by ABI changes
#if CONFIG2_NVTT
ENSURE(nvtt::version() >= NVTT_VERSION);
#endif
// Set up the worker thread:
int ret;
// Use SDL semaphores since OS X doesn't implement sem_init
m_WorkerSem = SDL_CreateSemaphore(0);
ENSURE(m_WorkerSem);
- ret = pthread_mutex_init(&m_WorkerMutex, NULL);
- ENSURE(ret == 0);
-
- ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this);
- ENSURE(ret == 0);
+ m_WorkerThread = std::thread(RunThread, this);
// Maybe we should share some centralised pool of worker threads?
// For now we'll just stick with a single thread for this specific use.
}
CTextureConverter::~CTextureConverter()
{
// Tell the thread to shut down
- pthread_mutex_lock(&m_WorkerMutex);
- m_Shutdown = true;
- pthread_mutex_unlock(&m_WorkerMutex);
+ {
+ std::lock_guard lock(m_WorkerMutex);
+ m_Shutdown = true;
+ }
// Wake it up so it sees the notification
SDL_SemPost(m_WorkerSem);
// Wait for it to shut down cleanly
- pthread_join(m_WorkerThread, NULL);
+ m_WorkerThread.join();
// Clean up resources
SDL_DestroySemaphore(m_WorkerSem);
- pthread_mutex_destroy(&m_WorkerMutex);
}
bool CTextureConverter::ConvertTexture(const CTexturePtr& texture, const VfsPath& src, const VfsPath& dest, const Settings& settings)
{
shared_ptr file;
size_t fileSize;
if (m_VFS->LoadFile(src, file, fileSize) < 0)
{
LOGERROR("Failed to load texture \"%s\"", src.string8());
return false;
}
Tex tex;
if (tex.decode(file, fileSize) < 0)
{
LOGERROR("Failed to decode texture \"%s\"", src.string8());
return false;
}
// Check whether there's any alpha channel
bool hasAlpha = ((tex.m_Flags & TEX_ALPHA) != 0);
if (settings.format == FMT_ALPHA)
{
// Convert to uncompressed 8-bit with no mipmaps
if (tex.transform_to((tex.m_Flags | TEX_GREY) & ~(TEX_DXT | TEX_MIPMAPS | TEX_ALPHA)) < 0)
{
LOGERROR("Failed to transform texture \"%s\"", src.string8());
return false;
}
}
else
{
// Convert to uncompressed BGRA with no mipmaps
if (tex.transform_to((tex.m_Flags | TEX_BGR | TEX_ALPHA) & ~(TEX_DXT | TEX_MIPMAPS)) < 0)
{
LOGERROR("Failed to transform texture \"%s\"", src.string8());
return false;
}
}
// Check if the texture has all alpha=255, so we can automatically
// switch from DXT3/DXT5 to DXT1 with no loss
if (hasAlpha)
{
hasAlpha = false;
u8* data = tex.get_data();
for (size_t i = 0; i < tex.m_Width * tex.m_Height; ++i)
{
if (data[i*4+3] != 0xFF)
{
hasAlpha = true;
break;
}
}
}
#if CONFIG2_NVTT
shared_ptr request(new ConversionRequest);
request->dest = dest;
request->texture = texture;
// Apply the chosen settings:
request->inputOptions.setMipmapGeneration(settings.mipmap == MIP_TRUE);
if (settings.alpha == ALPHA_TRANSPARENCY)
request->inputOptions.setAlphaMode(nvtt::AlphaMode_Transparency);
else
request->inputOptions.setAlphaMode(nvtt::AlphaMode_None);
request->isDXT1a = false;
request->is8bpp = false;
if (settings.format == FMT_RGBA)
{
request->compressionOptions.setFormat(nvtt::Format_RGBA);
// Change the default component order (see tex_dds.cpp decode_pf)
request->compressionOptions.setPixelFormat(32, 0xFF, 0xFF00, 0xFF0000, 0xFF000000u);
}
else if (settings.format == FMT_ALPHA)
{
request->compressionOptions.setFormat(nvtt::Format_RGBA);
request->compressionOptions.setPixelFormat(8, 0x00, 0x00, 0x00, 0xFF);
request->is8bpp = true;
}
else if (!hasAlpha)
{
// if no alpha channel then there's no point using DXT3 or DXT5
request->compressionOptions.setFormat(nvtt::Format_DXT1);
}
else if (settings.format == FMT_DXT1)
{
request->compressionOptions.setFormat(nvtt::Format_DXT1a);
request->isDXT1a = true;
}
else if (settings.format == FMT_DXT3)
{
request->compressionOptions.setFormat(nvtt::Format_DXT3);
}
else if (settings.format == FMT_DXT5)
{
request->compressionOptions.setFormat(nvtt::Format_DXT5);
}
if (settings.filter == FILTER_BOX)
request->inputOptions.setMipmapFilter(nvtt::MipmapFilter_Box);
else if (settings.filter == FILTER_TRIANGLE)
request->inputOptions.setMipmapFilter(nvtt::MipmapFilter_Triangle);
else if (settings.filter == FILTER_KAISER)
request->inputOptions.setMipmapFilter(nvtt::MipmapFilter_Kaiser);
if (settings.normal == NORMAL_TRUE)
request->inputOptions.setNormalMap(true);
request->inputOptions.setKaiserParameters(settings.kaiserWidth, settings.kaiserAlpha, settings.kaiserStretch);
request->inputOptions.setWrapMode(nvtt::WrapMode_Mirror); // TODO: should this be configurable?
request->compressionOptions.setQuality(m_HighQuality ? nvtt::Quality_Production : nvtt::Quality_Fastest);
// TODO: normal maps, gamma, etc
// Load the texture data
request->inputOptions.setTextureLayout(nvtt::TextureType_2D, tex.m_Width, tex.m_Height);
if (tex.m_Bpp == 32)
{
request->inputOptions.setMipmapData(tex.get_data(), tex.m_Width, tex.m_Height);
}
else // bpp == 8
{
// NVTT requires 32-bit input data, so convert
const u8* input = tex.get_data();
u8* rgba = new u8[tex.m_Width * tex.m_Height * 4];
u8* p = rgba;
for (size_t i = 0; i < tex.m_Width * tex.m_Height; i++)
{
p[0] = p[1] = p[2] = p[3] = *input++;
p += 4;
}
request->inputOptions.setMipmapData(rgba, tex.m_Width, tex.m_Height);
delete[] rgba;
}
- pthread_mutex_lock(&m_WorkerMutex);
- m_RequestQueue.push_back(request);
- pthread_mutex_unlock(&m_WorkerMutex);
+ {
+ std::lock_guard lock(m_WorkerMutex);
+ m_RequestQueue.push_back(request);
+ }
// Wake up the worker thread
SDL_SemPost(m_WorkerSem);
return true;
#else
LOGERROR("Failed to convert texture \"%s\" (NVTT not available)", src.string8());
return false;
#endif
}
bool CTextureConverter::Poll(CTexturePtr& texture, VfsPath& dest, bool& ok)
{
#if CONFIG2_NVTT
shared_ptr result;
// Grab the first result (if any)
- pthread_mutex_lock(&m_WorkerMutex);
- if (!m_ResultQueue.empty())
{
- result = m_ResultQueue.front();
- m_ResultQueue.pop_front();
+ std::lock_guard lock(m_WorkerMutex);
+ if (!m_ResultQueue.empty())
+ {
+ result = m_ResultQueue.front();
+ m_ResultQueue.pop_front();
+ }
}
- pthread_mutex_unlock(&m_WorkerMutex);
if (!result)
{
// no work to do
return false;
}
if (!result->ret)
{
// conversion had failed
ok = false;
return true;
}
// Move output into a correctly-aligned buffer
size_t size = result->output.buffer.size();
shared_ptr file;
AllocateAligned(file, size, maxSectorSize);
memcpy(file.get(), &result->output.buffer[0], size);
if (m_VFS->CreateFile(result->dest, file, size) < 0)
{
// error writing file
ok = false;
return true;
}
// Succeeded in converting texture
texture = result->texture;
dest = result->dest;
ok = true;
return true;
#else // #if CONFIG2_NVTT
return false;
#endif
}
bool CTextureConverter::IsBusy()
{
- pthread_mutex_lock(&m_WorkerMutex);
+ std::lock_guard lock(m_WorkerMutex);
bool busy = !m_RequestQueue.empty();
- pthread_mutex_unlock(&m_WorkerMutex);
return busy;
}
-void* CTextureConverter::RunThread(void* data)
+void CTextureConverter::RunThread(CTextureConverter* textureConverter)
{
debug_SetThreadName("TextureConverter");
g_Profiler2.RegisterCurrentThread("texconv");
- CTextureConverter* textureConverter = static_cast(data);
-
#if CONFIG2_NVTT
// Wait until the main thread wakes us up
while (SDL_SemWait(textureConverter->m_WorkerSem) == 0)
{
g_Profiler2.RecordSyncMarker();
PROFILE2_EVENT("wakeup");
- pthread_mutex_lock(&textureConverter->m_WorkerMutex);
- if (textureConverter->m_Shutdown)
+ shared_ptr request;
+
{
- pthread_mutex_unlock(&textureConverter->m_WorkerMutex);
- break;
+ std::lock_guard lock(textureConverter->m_WorkerMutex);
+ if (textureConverter->m_Shutdown)
+ break;
+ // If we weren't woken up for shutdown, we must have been woken up for
+ // a new request, so grab it from the queue
+ request = textureConverter->m_RequestQueue.front();
+ textureConverter->m_RequestQueue.pop_front();
}
- // If we weren't woken up for shutdown, we must have been woken up for
- // a new request, so grab it from the queue
- shared_ptr request = textureConverter->m_RequestQueue.front();
- textureConverter->m_RequestQueue.pop_front();
- pthread_mutex_unlock(&textureConverter->m_WorkerMutex);
// Set up the result object
shared_ptr result(new ConversionResult());
result->dest = request->dest;
result->texture = request->texture;
request->outputOptions.setOutputHandler(&result->output);
// TIMER(L"TextureConverter compress");
{
PROFILE2("compress");
// Perform the compression
nvtt::Compressor compressor;
result->ret = compressor.process(request->inputOptions, request->compressionOptions, request->outputOptions);
}
// Ugly hack: NVTT 2.0 doesn't set DDPF_ALPHAPIXELS for DXT1a, so we can't
// distinguish it from DXT1. (It's fixed in trunk by
// http://code.google.com/p/nvidia-texture-tools/source/detail?r=924&path=/trunk).
// Rather than using a trunk NVTT (unstable, makes packaging harder)
// or patching our copy (makes packaging harder), we'll just manually
// set the flag here.
if (request->isDXT1a && result->ret && result->output.buffer.size() > 80)
result->output.buffer[80] |= 1; // DDPF_ALPHAPIXELS in DDS_PIXELFORMAT.dwFlags
// Ugly hack: NVTT always sets DDPF_RGB, even if we're trying to output 8-bit
// alpha-only DDS with no RGB components. Unset that flag.
if (request->is8bpp)
result->output.buffer[80] &= ~0x40; // DDPF_RGB in DDS_PIXELFORMAT.dwFlags
// Push the result onto the queue
- pthread_mutex_lock(&textureConverter->m_WorkerMutex);
+ std::lock_guard lock(textureConverter->m_WorkerMutex);
textureConverter->m_ResultQueue.push_back(result);
- pthread_mutex_unlock(&textureConverter->m_WorkerMutex);
}
#endif
-
- return NULL;
}
Index: ps/trunk/source/graphics/TextureConverter.h
===================================================================
--- ps/trunk/source/graphics/TextureConverter.h (revision 22648)
+++ ps/trunk/source/graphics/TextureConverter.h (revision 22649)
@@ -1,219 +1,220 @@
-/* Copyright (C) 2012 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_TEXTURECONVERTER
#define INCLUDED_TEXTURECONVERTER
#include "lib/file/vfs/vfs.h"
-#include "lib/posix/posix_pthread.h"
#include "lib/external_libraries/libsdl.h"
#include "TextureManager.h"
+#include
+
class MD5;
/**
* Texture conversion helper class.
* Provides an asynchronous API to convert input image files into compressed DDS,
* given various conversion settings.
* (The (potentially very slow) compression is a performed in a background thread,
* so the game can remain responsive).
* Also provides an API to load conversion settings from XML files.
*
* XML files are of the form:
* @code
*
*
*
*
* @endcode
*
* 'pattern' is a wildcard expression, matching on filenames.
* All other attributes are optional. Later elements override attributes from
* earlier elements.
*
* 'format' is 'dxt1', 'dxt3', 'dxt5' or 'rgba'.
*
* 'mipmap' is 'true' or 'false'.
*
* 'normal' is 'true' or 'false'.
*
* 'alpha' is 'transparency' or 'player' (it determines whether the color value of
* 0-alpha pixels is significant or not).
*
* 'filter' is 'box', 'triangle' or 'kaiser'.
*
* 'kaiserwidth', 'kaiseralpha', 'kaiserstretch' are floats
* (see http://code.google.com/p/nvidia-texture-tools/wiki/ApiDocumentation#Mipmap_Generation)
*/
class CTextureConverter
{
public:
enum EFormat
{
FMT_UNSPECIFIED,
FMT_DXT1,
FMT_DXT3,
FMT_DXT5,
FMT_RGBA,
FMT_ALPHA
};
enum EMipmap
{
MIP_UNSPECIFIED,
MIP_TRUE,
MIP_FALSE
};
enum ENormalMap
{
NORMAL_UNSPECIFIED,
NORMAL_TRUE,
NORMAL_FALSE
};
enum EAlpha
{
ALPHA_UNSPECIFIED,
ALPHA_NONE,
ALPHA_PLAYER,
ALPHA_TRANSPARENCY
};
enum EFilter
{
FILTER_UNSPECIFIED,
FILTER_BOX,
FILTER_TRIANGLE,
FILTER_KAISER
};
/**
* Texture conversion settings.
*/
struct Settings
{
Settings() :
format(FMT_UNSPECIFIED), mipmap(MIP_UNSPECIFIED), normal(NORMAL_UNSPECIFIED),
alpha(ALPHA_UNSPECIFIED), filter(FILTER_UNSPECIFIED),
kaiserWidth(-1.f), kaiserAlpha(-1.f), kaiserStretch(-1.f)
{
}
/**
* Append this object's state to the given hash.
*/
void Hash(MD5& hash);
EFormat format;
EMipmap mipmap;
ENormalMap normal;
EAlpha alpha;
EFilter filter;
float kaiserWidth;
float kaiserAlpha;
float kaiserStretch;
};
/**
* Representation of \ line from settings XML file.
*/
struct Match
{
std::wstring pattern;
Settings settings;
};
/**
* Representation of settings XML file.
*/
struct SettingsFile
{
std::vector patterns;
};
/**
* Construct texture converter, for use with files in the given vfs.
*/
CTextureConverter(PIVFS vfs, bool highQuality);
/**
* Destroy texture converter and wait to shut down worker thread.
* This might take a long time (maybe seconds) if the worker is busy
* processing a texture.
*/
~CTextureConverter();
/**
* Load a texture conversion settings XML file.
* Returns NULL on failure.
*/
SettingsFile* LoadSettings(const VfsPath& path) const;
/**
* Match a sequence of settings files against a given texture filename,
* and return the resulting settings.
* Later entries in settingsFiles override earlier entries.
*/
Settings ComputeSettings(const std::wstring& filename, const std::vector& settingsFiles) const;
/**
* Begin converting a texture, using the given settings.
* This will load src and return false on failure.
* Otherwise it will return true and start an asynchronous conversion request,
* whose result will be returned from Poll() (with the texture and dest passed
* into this function).
*/
bool ConvertTexture(const CTexturePtr& texture, const VfsPath& src, const VfsPath& dest, const Settings& settings);
/**
* Returns the result of a successful ConvertTexture call.
* If no result is available yet, returns false.
* Otherwise, if the conversion succeeded, it sets ok to true and sets
* texture and dest to the corresponding values passed into ConvertTexture(),
* then returns true.
* If the conversion failed, it sets ok to false and doesn't touch the other arguments,
* then returns true.
*/
bool Poll(CTexturePtr& texture, VfsPath& dest, bool& ok);
/**
* Returns whether there is currently a queued request from ConvertTexture().
* (Note this may return false while the worker thread is still converting the last texture.)
*/
bool IsBusy();
private:
- static void* RunThread(void* data);
+ static void RunThread(CTextureConverter* data);
PIVFS m_VFS;
bool m_HighQuality;
- pthread_t m_WorkerThread;
- pthread_mutex_t m_WorkerMutex;
+ std::thread m_WorkerThread;
+ std::mutex m_WorkerMutex;
SDL_sem* m_WorkerSem;
struct ConversionRequest;
struct ConversionResult;
std::deque > m_RequestQueue; // protected by m_WorkerMutex
std::deque > m_ResultQueue; // protected by m_WorkerMutex
bool m_Shutdown; // protected by m_WorkerMutex
};
#endif // INCLUDED_TEXTURECONVERTER
Index: ps/trunk/source/lib/timer.cpp
===================================================================
--- ps/trunk/source/lib/timer.cpp (revision 22648)
+++ ps/trunk/source/lib/timer.cpp (revision 22649)
@@ -1,235 +1,234 @@
-/* Copyright (C) 2010 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.
*/
/*
* platform-independent high resolution timer
*/
#include "precompiled.h"
#include "lib/timer.h"
#include // std::stringstream
#include
#include
#include
#include
#include "lib/module_init.h"
#include "lib/posix/posix_pthread.h"
#include "lib/posix/posix_time.h"
# include "lib/sysdep/cpu.h"
#if OS_WIN
# include "lib/sysdep/os/win/whrt/whrt.h"
#endif
#if OS_UNIX
# include
#endif
#if OS_UNIX || OS_WIN
# define HAVE_GETTIMEOFDAY 1
#else
# define HAVE_GETTIMEOFDAY 0
#endif
#if (defined(_POSIX_TIMERS) && _POSIX_TIMERS > 0) || OS_WIN
# define HAVE_CLOCK_GETTIME 1
#else
# define HAVE_CLOCK_GETTIME 0
#endif
// rationale for wrapping gettimeofday and clock_gettime, instead of just
// emulating them where not available: allows returning higher-resolution
// timer values than their us / ns interface, via double [seconds].
// they're also not guaranteed to be monotonic.
#if HAVE_CLOCK_GETTIME
static struct timespec start;
#elif HAVE_GETTIMEOFDAY
static struct timeval start;
#endif
//-----------------------------------------------------------------------------
// timer API
void timer_LatchStartTime()
{
#if OS_WIN
// whrt_Time starts at zero, nothing needs to be done.
#elif HAVE_CLOCK_GETTIME
(void)clock_gettime(CLOCK_REALTIME, &start);
#elif HAVE_GETTIMEOFDAY
gettimeofday(&start, 0);
#endif
}
-static pthread_mutex_t ensure_monotonic_mutex = PTHREAD_MUTEX_INITIALIZER;
+static std::mutex ensure_monotonic_mutex;
// NB: does not guarantee strict monotonicity - callers must avoid
// dividing by the difference of two equal times.
static void EnsureMonotonic(double& newTime)
{
- pthread_mutex_lock(&ensure_monotonic_mutex);
+ std::lock_guard lock(ensure_monotonic_mutex);
static double maxTime;
maxTime = std::max(maxTime, newTime);
newTime = maxTime;
- pthread_mutex_unlock(&ensure_monotonic_mutex);
}
double timer_Time()
{
double t;
#if OS_WIN
t = whrt_Time();
#elif HAVE_CLOCK_GETTIME
ENSURE(start.tv_sec || start.tv_nsec); // must have called timer_LatchStartTime first
struct timespec cur;
(void)clock_gettime(CLOCK_REALTIME, &cur);
t = (cur.tv_sec - start.tv_sec) + (cur.tv_nsec - start.tv_nsec)*1e-9;
#elif HAVE_GETTIMEOFDAY
ENSURE(start.tv_sec || start.tv_usec); // must have called timer_LatchStartTime first
struct timeval cur;
gettimeofday(&cur, 0);
t = (cur.tv_sec - start.tv_sec) + (cur.tv_usec - start.tv_usec)*1e-6;
#else
# error "timer_Time: add timer implementation for this platform!"
#endif
EnsureMonotonic(t);
return t;
}
// cached because the default implementation may take several milliseconds
static double resolution;
static Status InitResolution()
{
#if OS_WIN
resolution = whrt_Resolution();
#elif HAVE_CLOCK_GETTIME
struct timespec ts;
if(clock_getres(CLOCK_REALTIME, &ts) == 0)
resolution = ts.tv_nsec * 1e-9;
#else
const double t0 = timer_Time();
double t1, t2;
do t1 = timer_Time(); while(t1 == t0);
do t2 = timer_Time(); while(t2 == t1);
resolution = t2-t1;
#endif
return INFO::OK;
}
double timer_Resolution()
{
static ModuleInitState initState;
ModuleInit(&initState, InitResolution);
return resolution;
}
//-----------------------------------------------------------------------------
// client API
// intrusive linked-list of all clients. a fixed-size limit would be
// acceptable (since timers are added manually), but the list is easy
// to implement and only has the drawback of exposing TimerClient to users.
//
// do not use std::list et al. for this! we must be callable at any time,
// especially before NLSO ctors run or before heap init.
static size_t numClients;
static TimerClient* clients;
TimerClient* timer_AddClient(TimerClient* tc, const wchar_t* description)
{
tc->sum.SetToZero();
tc->description = description;
// insert at front of list
tc->next = clients;
clients = tc;
numClients++;
return tc;
}
void timer_DisplayClientTotals()
{
debug_printf("TIMER TOTALS (%lu clients)\n", (unsigned long)numClients);
debug_printf("-----------------------------------------------------\n");
while(clients)
{
// (make sure list and count are consistent)
ENSURE(numClients != 0);
TimerClient* tc = clients;
clients = tc->next;
numClients--;
const std::string duration = tc->sum.ToString();
debug_printf(" %s: %s (%lux)\n", utf8_from_wstring(tc->description).c_str(), duration.c_str(), (unsigned long)tc->num_calls);
}
debug_printf("-----------------------------------------------------\n");
}
//-----------------------------------------------------------------------------
std::string StringForSeconds(double seconds)
{
double scale = 1e6;
const char* unit = " us";
if(seconds > 1.0)
scale = 1, unit = " s";
else if(seconds > 1e-3)
scale = 1e3, unit = " ms";
std::stringstream ss;
ss << seconds*scale;
ss << unit;
return ss.str();
}
std::string StringForCycles(Cycles cycles)
{
double scale = 1.0;
const char* unit = " c";
if(cycles > 10000000000LL) // 10 Gc
scale = 1e-9, unit = " Gc";
else if(cycles > 10000000) // 10 Mc
scale = 1e-6, unit = " Mc";
else if(cycles > 10000) // 10 kc
scale = 1e-3, unit = " kc";
std::stringstream ss;
ss << cycles*scale;
ss << unit;
return ss.str();
}
Index: ps/trunk/source/ps/UserReport.cpp
===================================================================
--- ps/trunk/source/ps/UserReport.cpp (revision 22648)
+++ ps/trunk/source/ps/UserReport.cpp (revision 22649)
@@ -1,652 +1,649 @@
/* 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
+#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);
+ m_WorkerThread = std::thread(RunThread, this);
}
~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)
{
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()
{
{
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);
+ m_WorkerThread.join();
return true;
}
/**
* Called by main thread to determine the current status of the uploader.
*/
std::string GetStatus()
{
std::lock_guard lock(m_WorkerMutex);
return m_Status;
}
/**
* Called by main thread to add a new report to the queue.
*/
void Submit(const shared_ptr& report)
{
{
std::lock_guard lock(m_WorkerMutex);
m_ReportQueue.push_back(report);
}
// Wake up the worker thread
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)
+ static void RunThread(CUserReporterWorker* data)
{
debug_SetThreadName("CUserReportWorker");
g_Profiler2.RegisterCurrentThread("userreport");
- static_cast(data)->Run();
-
- return NULL;
+ data->Run();
}
void Run()
{
// Set libcurl's proxy configuration
// (This has to be done in the thread because it's potentially very slow)
SetStatus("proxy");
std::wstring proxy;
{
PROFILE2("get proxy config");
if (sys_get_proxy_config(wstring_from_utf8(m_URL), proxy) == INFO::OK)
curl_easy_setopt(m_Curl, CURLOPT_PROXY, utf8_from_wstring(proxy).c_str());
}
SetStatus("waiting");
/*
* We use a 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()
{
std::lock_guard lock(m_WorkerMutex);
return m_Enabled;
}
bool GetShutdown()
{
std::lock_guard lock(m_WorkerMutex);
return m_Shutdown;
}
void SetStatus(const std::string& status)
{
std::lock_guard lock(m_WorkerMutex);
m_Status = status;
#if DEBUG_UPLOADS
debug_printf(">>> CUserReporterWorker status: %s\n", status.c_str());
#endif
}
bool ProcessReport()
{
PROFILE2("process report");
shared_ptr report;
{
std::lock_guard lock(m_WorkerMutex);
if (m_ReportQueue.empty())
return false;
report = m_ReportQueue.front();
m_ReportQueue.pop_front();
}
ConstructRequestData(*report);
m_RequestDataOffset = 0;
m_ResponseData.clear();
m_ErrorBuffer[0] = '\0';
curl_easy_setopt(m_Curl, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)m_RequestData.size());
SetStatus("connecting");
#if DEBUG_UPLOADS
TIMER(L"CUserReporterWorker request");
#endif
CURLcode err = curl_easy_perform(m_Curl);
#if DEBUG_UPLOADS
printf(">>>\n%s\n<<<\n", m_ResponseData.c_str());
#endif
if (err == CURLE_OK)
{
long code = -1;
curl_easy_getinfo(m_Curl, CURLINFO_RESPONSE_CODE, &code);
SetStatus("completed:" + CStr::FromInt(code));
// Check for success code
if (code == 200)
return true;
// If the server returns the 410 Gone status, interpret that as meaning
// it no longer supports uploads (at least from this version of the game),
// so shut down and stop talking to it (to avoid wasting bandwidth)
if (code == 410)
{
std::lock_guard lock(m_WorkerMutex);
m_Shutdown = true;
return false;
}
}
else
{
std::string errorString(m_ErrorBuffer);
if (errorString.empty())
errorString = curl_easy_strerror(err);
SetStatus("failed:" + CStr::FromInt(err) + ":" + errorString);
}
// We got an unhandled return code or a connection failure;
// push this report back onto the queue and try again after
// a long interval
{
std::lock_guard lock(m_WorkerMutex);
m_ReportQueue.push_front(report);
}
m_PauseUntilTime = timer_Time() + RECONNECT_INVERVAL;
return false;
}
void ConstructRequestData(const CUserReport& report)
{
// Construct the POST request data in the application/x-www-form-urlencoded format
std::string r;
r += "user_id=";
AppendEscaped(r, m_UserID);
r += "&time=" + CStr::FromInt64(report.m_Time);
r += "&type=";
AppendEscaped(r, report.m_Type);
r += "&version=" + CStr::FromInt(report.m_Version);
r += "&data=";
AppendEscaped(r, report.m_Data);
// Compress the content with zlib to save bandwidth.
// (Note that we send a request with unlabelled compressed data instead
// of using Content-Encoding, because Content-Encoding is a mess and causes
// problems with servers and breaks Content-Length and this is much easier.)
std::string compressed;
compressed.resize(compressBound(r.size()));
uLongf destLen = compressed.size();
int ok = compress((Bytef*)compressed.c_str(), &destLen, (const Bytef*)r.c_str(), r.size());
ENSURE(ok == Z_OK);
compressed.resize(destLen);
m_RequestData.swap(compressed);
}
void AppendEscaped(std::string& buffer, const std::string& str)
{
char* escaped = curl_easy_escape(m_Curl, str.c_str(), str.size());
buffer += escaped;
curl_free(escaped);
}
static size_t ReceiveCallback(void* buffer, size_t size, size_t nmemb, void* userp)
{
CUserReporterWorker* self = static_cast(userp);
if (self->GetShutdown())
return 0; // signals an error
self->m_ResponseData += std::string((char*)buffer, (char*)buffer+size*nmemb);
return size*nmemb;
}
static size_t SendCallback(char* bufptr, size_t size, size_t nmemb, void* userp)
{
CUserReporterWorker* self = static_cast(userp);
if (self->GetShutdown())
return CURL_READFUNC_ABORT; // signals an error
// We can return as much data as available, up to the buffer size
size_t amount = std::min(self->m_RequestData.size() - self->m_RequestDataOffset, size*nmemb);
// ...But restrict to sending a small amount at once, so that we remain
// responsive to shutdown requests even if the network is pretty slow
amount = std::min((size_t)1024, amount);
if(amount != 0) // (avoids invalid operator[] call where index=size)
{
memcpy(bufptr, &self->m_RequestData[self->m_RequestDataOffset], amount);
self->m_RequestDataOffset += amount;
}
self->SetStatus("sending:" + CStr::FromDouble((double)self->m_RequestDataOffset / self->m_RequestData.size()));
return amount;
}
private:
// Thread-related members:
- pthread_t m_WorkerThread;
+ std::thread m_WorkerThread;
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/soundmanager/SoundManager.cpp
===================================================================
--- ps/trunk/source/soundmanager/SoundManager.cpp (revision 22648)
+++ ps/trunk/source/soundmanager/SoundManager.cpp (revision 22649)
@@ -1,819 +1,818 @@
/* 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"
+#include
+
ISoundManager* g_SoundManager = NULL;
#define SOURCE_NUM 64
#if CONFIG2_AUDIO
class CSoundManagerWorker
{
NONCOPYABLE(CSoundManagerWorker);
public:
CSoundManagerWorker()
{
m_Items = new ItemsList;
m_DeadItems = new ItemsList;
m_Shutdown = false;
- int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this);
- ENSURE(ret == 0);
+ m_WorkerThread = std::thread(RunThread, this);
}
~CSoundManagerWorker()
{
delete m_Items;
CleanupItems();
delete m_DeadItems;
}
bool Shutdown()
{
{
std::lock_guard lock(m_WorkerMutex);
m_Shutdown = true;
ItemsList::iterator lstr = m_Items->begin();
while (lstr != m_Items->end())
{
delete *lstr;
++lstr;
}
}
- pthread_join(m_WorkerThread, NULL);
+ m_WorkerThread.join();
return true;
}
void addItem(ISoundItem* anItem)
{
std::lock_guard lock(m_WorkerMutex);
m_Items->push_back(anItem);
}
void CleanupItems()
{
std::lock_guard lock(m_DeadItemsMutex);
AL_CHECK;
ItemsList::iterator deadItems = m_DeadItems->begin();
while (deadItems != m_DeadItems->end())
{
delete *deadItems;
++deadItems;
AL_CHECK;
}
m_DeadItems->clear();
}
private:
- static void* RunThread(void* data)
+ static void RunThread(CSoundManagerWorker* data)
{
debug_SetThreadName("CSoundManagerWorker");
g_Profiler2.RegisterCurrentThread("soundmanager");
- static_cast(data)->Run();
-
- return NULL;
+ data->Run();
}
void Run()
{
while (true)
{
// Handle shutdown requests as soon as possible
if (GetShutdown())
return;
int pauseTime = 500;
if (g_SoundManager->InDistress())
pauseTime = 50;
{
std::lock_guard workerLock(m_WorkerMutex);
ItemsList::iterator lstr = m_Items->begin();
ItemsList* nextItemList = new ItemsList;
while (lstr != m_Items->end())
{
AL_CHECK;
if ((*lstr)->IdleTask())
{
if ((pauseTime == 500) && (*lstr)->IsFading())
pauseTime = 100;
nextItemList->push_back(*lstr);
}
else
{
std::lock_guard deadItemsLock(m_DeadItemsMutex);
m_DeadItems->push_back(*lstr);
}
++lstr;
AL_CHECK;
}
delete m_Items;
m_Items = nextItemList;
AL_CHECK;
}
SDL_Delay(pauseTime);
}
}
bool GetShutdown()
{
std::lock_guard lock(m_WorkerMutex);
return m_Shutdown;
}
private:
// Thread-related members:
- pthread_t m_WorkerThread;
+ std::thread m_WorkerThread;
std::mutex m_WorkerMutex;
std::mutex m_DeadItemsMutex;
// Shared by main thread and worker thread:
// These variables are all protected by a mutexes
ItemsList* m_Items;
ItemsList* m_DeadItems;
bool m_Shutdown;
CSoundManagerWorker(ISoundManager* UNUSED(other)){};
};
void ISoundManager::CreateSoundManager()
{
if (!g_SoundManager)
{
g_SoundManager = new CSoundManager();
g_SoundManager->StartWorker();
}
}
void ISoundManager::SetEnabled(bool doEnable)
{
if (g_SoundManager && !doEnable)
SAFE_DELETE(g_SoundManager);
else if (!g_SoundManager && doEnable)
ISoundManager::CreateSoundManager();
}
void ISoundManager::CloseGame()
{
if (CSoundManager* aSndMgr = (CSoundManager*)g_SoundManager)
aSndMgr->SetAmbientItem(NULL);
}
void CSoundManager::al_ReportError(ALenum err, const char* caller, int line)
{
LOGERROR("OpenAL error: %s; called from %s (line %d)\n", alGetString(err), caller, line);
}
void CSoundManager::al_check(const char* caller, int line)
{
ALenum err = alGetError();
if (err != AL_NO_ERROR)
al_ReportError(err, caller, line);
}
Status CSoundManager::ReloadChangedFiles(const VfsPath& UNUSED(path))
{
// TODO implement sound file hotloading
return INFO::OK;
}
/*static*/ Status CSoundManager::ReloadChangedFileCB(void* param, const VfsPath& path)
{
return static_cast(param)->ReloadChangedFiles(path);
}
CSoundManager::CSoundManager()
: m_Context(nullptr), m_Device(nullptr), m_ALSourceBuffer(nullptr),
m_CurrentTune(nullptr), m_CurrentEnvirons(nullptr),
m_Worker(nullptr), m_DistressMutex(), m_PlayListItems(nullptr), m_SoundGroups(),
m_Gain(.5f), m_MusicGain(.5f), m_AmbientGain(.5f), m_ActionGain(.5f), m_UIGain(.5f),
m_Enabled(false), m_BufferSize(98304), m_BufferCount(50),
m_SoundEnabled(true), m_MusicEnabled(true), m_MusicPaused(false),
m_AmbientPaused(false), m_ActionPaused(false),
m_RunningPlaylist(false), m_PlayingPlaylist(false), m_LoopingPlaylist(false),
m_PlaylistGap(0), m_DistressErrCount(0), m_DistressTime(0)
{
CFG_GET_VAL("sound.mastergain", m_Gain);
CFG_GET_VAL("sound.musicgain", m_MusicGain);
CFG_GET_VAL("sound.ambientgain", m_AmbientGain);
CFG_GET_VAL("sound.actiongain", m_ActionGain);
CFG_GET_VAL("sound.uigain", m_UIGain);
AlcInit();
if (m_Enabled)
{
SetMasterGain(m_Gain);
InitListener();
m_PlayListItems = new PlayList;
}
if (!CXeromyces::AddValidator(g_VFS, "sound_group", "audio/sound_group.rng"))
LOGERROR("CSoundManager: failed to load grammar file 'audio/sound_group.rng'");
RegisterFileReloadFunc(ReloadChangedFileCB, this);
}
CSoundManager::~CSoundManager()
{
UnregisterFileReloadFunc(ReloadChangedFileCB, this);
if (m_Worker)
{
AL_CHECK;
m_Worker->Shutdown();
AL_CHECK;
m_Worker->CleanupItems();
AL_CHECK;
delete m_Worker;
}
AL_CHECK;
for (const std::pair& p : m_SoundGroups)
delete p.second;
m_SoundGroups.clear();
if (m_PlayListItems)
delete m_PlayListItems;
if (m_ALSourceBuffer != NULL)
delete[] m_ALSourceBuffer;
if (m_Context)
alcDestroyContext(m_Context);
if (m_Device)
alcCloseDevice(m_Device);
}
void CSoundManager::StartWorker()
{
if (m_Enabled)
m_Worker = new CSoundManagerWorker();
}
Status CSoundManager::AlcInit()
{
Status ret = INFO::OK;
m_Device = alcOpenDevice(NULL);
if (m_Device)
{
ALCint attribs[] = {ALC_STEREO_SOURCES, 16, 0};
m_Context = alcCreateContext(m_Device, &attribs[0]);
if (m_Context)
{
alcMakeContextCurrent(m_Context);
m_ALSourceBuffer = new ALSourceHolder[SOURCE_NUM];
ALuint* sourceList = new ALuint[SOURCE_NUM];
alGenSources(SOURCE_NUM, sourceList);
ALCenum err = alcGetError(m_Device);
if (err == ALC_NO_ERROR)
{
for (int x = 0; x < SOURCE_NUM; x++)
{
m_ALSourceBuffer[x].ALSource = sourceList[x];
m_ALSourceBuffer[x].SourceItem = NULL;
}
m_Enabled = true;
}
else
{
LOGERROR("error in gensource = %d", err);
}
delete[] sourceList;
}
}
// check if init succeeded.
// some OpenAL implementations don't indicate failure here correctly;
// we need to check if the device and context pointers are actually valid.
ALCenum err = alcGetError(m_Device);
const char* dev_name = (const char*)alcGetString(m_Device, ALC_DEVICE_SPECIFIER);
if (err == ALC_NO_ERROR && m_Device && m_Context)
debug_printf("Sound: AlcInit success, using %s\n", dev_name);
else
{
LOGERROR("Sound: AlcInit failed, m_Device=%p m_Context=%p dev_name=%s err=%x\n", (void *)m_Device, (void *)m_Context, dev_name, err);
// FIXME Hack to get around exclusive access to the sound device
#if OS_UNIX
ret = INFO::OK;
#else
ret = ERR::FAIL;
#endif // !OS_UNIX
}
return ret;
}
bool CSoundManager::InDistress()
{
std::lock_guard lock(m_DistressMutex);
if (m_DistressTime == 0)
return false;
else if ((timer_Time() - m_DistressTime) > 10)
{
m_DistressTime = 0;
// Coming out of distress mode
m_DistressErrCount = 0;
return false;
}
return true;
}
void CSoundManager::SetDistressThroughShortage()
{
std::lock_guard lock(m_DistressMutex);
// Going into distress for normal reasons
m_DistressTime = timer_Time();
}
void CSoundManager::SetDistressThroughError()
{
std::lock_guard lock(m_DistressMutex);
// Going into distress due to unknown error
m_DistressTime = timer_Time();
m_DistressErrCount++;
}
ALuint CSoundManager::GetALSource(ISoundItem* anItem)
{
for (int x = 0; x < SOURCE_NUM; x++)
{
if (!m_ALSourceBuffer[x].SourceItem)
{
m_ALSourceBuffer[x].SourceItem = anItem;
return m_ALSourceBuffer[x].ALSource;
}
}
SetDistressThroughShortage();
return 0;
}
void CSoundManager::ReleaseALSource(ALuint theSource)
{
for (int x = 0; x < SOURCE_NUM; x++)
{
if (m_ALSourceBuffer[x].ALSource == theSource)
{
m_ALSourceBuffer[x].SourceItem = NULL;
return;
}
}
}
long CSoundManager::GetBufferCount()
{
return m_BufferCount;
}
long CSoundManager::GetBufferSize()
{
return m_BufferSize;
}
void CSoundManager::AddPlayListItem(const VfsPath& itemPath)
{
if (m_Enabled)
m_PlayListItems->push_back(itemPath);
}
void CSoundManager::ClearPlayListItems()
{
if (m_Enabled)
{
if (m_PlayingPlaylist)
SetMusicItem(NULL);
m_PlayingPlaylist = false;
m_LoopingPlaylist = false;
m_RunningPlaylist = false;
m_PlayListItems->clear();
}
}
void CSoundManager::StartPlayList(bool doLoop)
{
if (m_Enabled && m_MusicEnabled)
{
if (m_PlayListItems->size() > 0)
{
m_PlayingPlaylist = true;
m_LoopingPlaylist = doLoop;
m_RunningPlaylist = false;
ISoundItem* aSnd = LoadItem((m_PlayListItems->at(0)));
if (aSnd)
SetMusicItem(aSnd);
else
SetMusicItem(NULL);
}
}
}
void CSoundManager::SetMasterGain(float gain)
{
if (m_Enabled)
{
m_Gain = gain;
alListenerf(AL_GAIN, m_Gain);
AL_CHECK;
}
}
void CSoundManager::SetMusicGain(float gain)
{
m_MusicGain = gain;
if (m_CurrentTune)
m_CurrentTune->SetGain(m_MusicGain);
}
void CSoundManager::SetAmbientGain(float gain)
{
m_AmbientGain = gain;
}
void CSoundManager::SetActionGain(float gain)
{
m_ActionGain = gain;
}
void CSoundManager::SetUIGain(float gain)
{
m_UIGain = gain;
}
ISoundItem* CSoundManager::LoadItem(const VfsPath& itemPath)
{
AL_CHECK;
if (m_Enabled)
{
CSoundData* itemData = CSoundData::SoundDataFromFile(itemPath);
AL_CHECK;
if (itemData)
return CSoundManager::ItemForData(itemData);
}
return NULL;
}
ISoundItem* CSoundManager::ItemForData(CSoundData* itemData)
{
AL_CHECK;
ISoundItem* answer = NULL;
AL_CHECK;
if (m_Enabled && (itemData != NULL))
{
if (itemData->IsOneShot())
{
if (itemData->GetBufferCount() == 1)
answer = new CSoundItem(itemData);
else
answer = new CBufferItem(itemData);
}
else
{
answer = new CStreamItem(itemData);
}
if (answer && m_Worker)
m_Worker->addItem(answer);
}
return answer;
}
void CSoundManager::IdleTask()
{
if (m_Enabled)
{
if (m_CurrentTune)
{
m_CurrentTune->EnsurePlay();
if (m_PlayingPlaylist && m_RunningPlaylist)
{
if (m_CurrentTune->Finished())
{
if (m_PlaylistGap == 0)
{
m_PlaylistGap = timer_Time() + 15;
}
else if (m_PlaylistGap < timer_Time())
{
m_PlaylistGap = 0;
PlayList::iterator it = find(m_PlayListItems->begin(), m_PlayListItems->end(), m_CurrentTune->GetName());
if (it != m_PlayListItems->end())
{
++it;
Path nextPath;
if (it == m_PlayListItems->end())
nextPath = m_PlayListItems->at(0);
else
nextPath = *it;
ISoundItem* aSnd = LoadItem(nextPath);
if (aSnd)
SetMusicItem(aSnd);
}
}
}
}
}
if (m_CurrentEnvirons)
m_CurrentEnvirons->EnsurePlay();
if (m_Worker)
m_Worker->CleanupItems();
}
}
ISoundItem* CSoundManager::ItemForEntity(entity_id_t UNUSED(source), CSoundData* sndData)
{
ISoundItem* currentItem = NULL;
if (m_Enabled)
currentItem = ItemForData(sndData);
return currentItem;
}
void CSoundManager::InitListener()
{
ALfloat listenerPos[] = {0.0, 0.0, 0.0};
ALfloat listenerVel[] = {0.0, 0.0, 0.0};
ALfloat listenerOri[] = {0.0, 0.0, -1.0, 0.0, 1.0, 0.0};
alListenerfv(AL_POSITION, listenerPos);
alListenerfv(AL_VELOCITY, listenerVel);
alListenerfv(AL_ORIENTATION, listenerOri);
alDistanceModel(AL_LINEAR_DISTANCE);
}
void CSoundManager::PlayGroupItem(ISoundItem* anItem, ALfloat groupGain)
{
if (anItem)
{
if (m_Enabled && (m_ActionGain > 0))
{
anItem->SetGain(m_ActionGain * groupGain);
anItem->PlayAndDelete();
AL_CHECK;
}
}
}
void CSoundManager::SetMusicEnabled(bool isEnabled)
{
if (m_CurrentTune && !isEnabled)
{
m_CurrentTune->FadeAndDelete(1.00);
m_CurrentTune = NULL;
}
m_MusicEnabled = isEnabled;
}
void CSoundManager::PlayAsGroup(const VfsPath& groupPath, const CVector3D& sourcePos, entity_id_t source, bool ownedSound)
{
// Make sure the sound group is loaded
CSoundGroup* group;
if (m_SoundGroups.find(groupPath.string()) == m_SoundGroups.end())
{
group = new CSoundGroup();
if (!group->LoadSoundGroup(L"audio/" + groupPath.string()))
{
LOGERROR("Failed to load sound group '%s'", groupPath.string8());
delete group;
group = NULL;
}
// Cache the sound group (or the null, if it failed)
m_SoundGroups[groupPath.string()] = group;
}
else
{
group = m_SoundGroups[groupPath.string()];
}
// Failed to load group -> do nothing
if (group && (ownedSound || !group->TestFlag(eOwnerOnly)))
group->PlayNext(sourcePos, source);
}
void CSoundManager::PlayAsMusic(const VfsPath& itemPath, bool looping)
{
if (m_Enabled)
{
UNUSED2(looping);
ISoundItem* aSnd = LoadItem(itemPath);
if (aSnd != NULL)
SetMusicItem(aSnd);
}
}
void CSoundManager::PlayAsAmbient(const VfsPath& itemPath, bool looping)
{
if (m_Enabled)
{
UNUSED2(looping);
ISoundItem* aSnd = LoadItem(itemPath);
if (aSnd != NULL)
SetAmbientItem(aSnd);
}
}
void CSoundManager::PlayAsUI(const VfsPath& itemPath, bool looping)
{
if (m_Enabled)
{
IdleTask();
if (ISoundItem* anItem = LoadItem(itemPath))
{
if (m_UIGain > 0)
{
anItem->SetGain(m_UIGain);
anItem->SetLooping(looping);
anItem->PlayAndDelete();
}
}
AL_CHECK;
}
}
void CSoundManager::Pause(bool pauseIt)
{
PauseMusic(pauseIt);
PauseAmbient(pauseIt);
PauseAction(pauseIt);
}
void CSoundManager::PauseMusic(bool pauseIt)
{
if (m_CurrentTune && pauseIt && !m_MusicPaused)
{
m_CurrentTune->FadeAndPause(1.0);
}
else if (m_CurrentTune && m_MusicPaused && !pauseIt && m_MusicEnabled)
{
m_CurrentTune->SetGain(0);
m_CurrentTune->Resume();
m_CurrentTune->FadeToIn(m_MusicGain, 1.0);
}
m_MusicPaused = pauseIt;
}
void CSoundManager::PauseAmbient(bool pauseIt)
{
if (m_CurrentEnvirons && pauseIt)
m_CurrentEnvirons->Pause();
else if (m_CurrentEnvirons)
m_CurrentEnvirons->Resume();
m_AmbientPaused = pauseIt;
}
void CSoundManager::PauseAction(bool pauseIt)
{
m_ActionPaused = pauseIt;
}
void CSoundManager::SetMusicItem(ISoundItem* anItem)
{
if (m_Enabled)
{
AL_CHECK;
if (m_CurrentTune)
{
m_CurrentTune->FadeAndDelete(2.00);
m_CurrentTune = NULL;
}
IdleTask();
if (anItem)
{
if (m_MusicEnabled)
{
m_CurrentTune = anItem;
m_CurrentTune->SetGain(0);
if (m_PlayingPlaylist)
{
m_RunningPlaylist = true;
m_CurrentTune->Play();
}
else
m_CurrentTune->PlayLoop();
m_MusicPaused = false;
m_CurrentTune->FadeToIn(m_MusicGain, 1.00);
}
else
{
anItem->StopAndDelete();
}
}
AL_CHECK;
}
}
void CSoundManager::SetAmbientItem(ISoundItem* anItem)
{
if (m_Enabled)
{
if (m_CurrentEnvirons)
{
m_CurrentEnvirons->FadeAndDelete(3.00);
m_CurrentEnvirons = NULL;
}
IdleTask();
if (anItem)
{
if (m_AmbientGain > 0)
{
m_CurrentEnvirons = anItem;
m_CurrentEnvirons->SetGain(0);
m_CurrentEnvirons->PlayLoop();
m_CurrentEnvirons->FadeToIn(m_AmbientGain, 2.00);
}
}
AL_CHECK;
}
}
#else // CONFIG2_AUDIO
void ISoundManager::CreateSoundManager(){}
void ISoundManager::SetEnabled(bool UNUSED(doEnable)){}
void ISoundManager::CloseGame(){}
#endif // CONFIG2_AUDIO
Index: ps/trunk/source/tools/atlas/GameInterface/GameLoop.cpp
===================================================================
--- ps/trunk/source/tools/atlas/GameInterface/GameLoop.cpp (revision 22648)
+++ ps/trunk/source/tools/atlas/GameInterface/GameLoop.cpp (revision 22649)
@@ -1,332 +1,330 @@
-/* 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
+
#include "GameLoop.h"
#include "MessagePasserImpl.h"
#include "Messages.h"
#include "SharedMemory.h"
#include "Handlers/MessageHandler.h"
#include "ActorViewer.h"
#include "View.h"
#include "InputProcessor.h"
#include "graphics/TextureManager.h"
#include "gui/GUIManager.h"
#include "lib/app_hooks.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/timer.h"
#include "ps/CLogger.h"
#include "ps/DllLoader.h"
#include "ps/Filesystem.h"
#include "ps/Profile.h"
+#include "ps/ThreadUtil.h"
#include "ps/GameSetup/Paths.h"
#include "renderer/Renderer.h"
using namespace AtlasMessage;
namespace AtlasMessage
{
extern void RegisterHandlers();
}
// Loaded from DLL:
void (*Atlas_StartWindow)(const wchar_t* type);
void (*Atlas_SetDataDirectory)(const wchar_t* path);
void (*Atlas_SetConfigDirectory)(const wchar_t* path);
void (*Atlas_SetMessagePasser)(MessagePasser*);
void (*Atlas_GLSetCurrent)(void* cavas);
void (*Atlas_GLSwapBuffers)(void* canvas);
void (*Atlas_NotifyEndOfFrame)();
void (*Atlas_DisplayError)(const wchar_t* text, size_t flags);
namespace AtlasMessage
{
void* (*ShareableMallocFptr)(size_t);
void (*ShareableFreeFptr)(void*);
}
MessagePasser* AtlasMessage::g_MessagePasser = NULL;
static InputProcessor g_Input;
static GameLoopState state;
GameLoopState* g_AtlasGameLoop = &state;
static ErrorReactionInternal AtlasDisplayError(const wchar_t* text, size_t flags)
{
// TODO: after Atlas has been unloaded, don't do this
Atlas_DisplayError(text, flags);
return ERI_CONTINUE;
}
static void RendererIncrementalLoad()
{
// TODO: shouldn't duplicate this code from main.cpp
if (!CRenderer::IsInitialised())
return;
const double maxTime = 0.1f;
double startTime = timer_Time();
bool more;
do {
more = g_Renderer.GetTextureManager().MakeProgress();
}
while (more && timer_Time() - startTime < maxTime);
}
-static void* RunEngine(void* data)
+static void RunEngine(const CmdLineArgs& args)
{
debug_SetThreadName("engine_thread");
// Set new main thread so that all the thread-safety checks pass
ThreadUtil::SetMainThread();
g_Profiler2.RegisterCurrentThread("atlasmain");
- const CmdLineArgs args = *reinterpret_cast(data);
-
MessagePasserImpl* msgPasser = (MessagePasserImpl*)AtlasMessage::g_MessagePasser;
// Register all the handlers for message which might be passed back
RegisterHandlers();
// Override ah_display_error to pass all errors to the Atlas UI
// TODO: this doesn't work well because it doesn't pause the game thread
// and the error box is ugly, so only use it if we fix those issues
// (use INIT_HAVE_DISPLAY_ERROR init flag to test this)
AppHooks hooks = {0};
hooks.display_error = AtlasDisplayError;
app_hooks_update(&hooks);
// Disable the game's cursor rendering
extern CStrW g_CursorName;
g_CursorName = L"";
state.args = args;
state.running = true;
state.view = AtlasView::GetView_None();
state.glCanvas = NULL;
double last_activity = timer_Time();
while (state.running)
{
bool recent_activity = false;
//////////////////////////////////////////////////////////////////////////
// (TODO: Work out why these things have to be in this order (to avoid
// jumps when starting to move, etc))
// Calculate frame length
{
const double time = timer_Time();
static double last_time = time;
const double realFrameLength = time-last_time;
last_time = time;
ENSURE(realFrameLength >= 0.0);
// TODO: filter out big jumps, e.g. when having done a lot of slow
// processing in the last frame
state.realFrameLength = realFrameLength;
}
// Process the input that was received in the past
if (g_Input.ProcessInput(&state))
recent_activity = true;
//////////////////////////////////////////////////////////////////////////
{
IMessage* msg;
while ((msg = msgPasser->Retrieve()) != NULL)
{
recent_activity = true;
std::string name (msg->GetName());
msgHandlers::const_iterator it = GetMsgHandlers().find(name);
if (it != GetMsgHandlers().end())
{
it->second(msg);
}
else
{
debug_warn(L"Unrecognised message");
// CLogger might not be initialised, but this error will be sent
// to the debug output window anyway so people can still see it
LOGERROR("Unrecognised message (%s)", name.c_str());
}
if (msg->GetType() == IMessage::Query)
{
// For queries, we need to notify MessagePasserImpl::Query
// that the query has now been processed.
sem_post((sem_t*) static_cast(msg)->m_Semaphore);
// (msg may have been destructed at this point, so don't use it again)
// It's quite possible that the querier is going to do a tiny
// bit of processing on the query results and then issue another
// query, and repeat lots of times in a loop. To avoid slowing
// that down by rendering between every query, make this
// thread yield now.
SDL_Delay(0);
}
else
{
// For non-queries, we need to delete the object, since we
// took ownership of it.
AtlasMessage::ShareableDelete(msg);
}
}
}
// Exit, if desired
if (! state.running)
break;
//////////////////////////////////////////////////////////////////////////
// Do per-frame processing:
ReloadChangedFiles();
RendererIncrementalLoad();
// Pump SDL events (e.g. hotkeys)
SDL_Event_ ev;
while (in_poll_priority_event(&ev))
in_dispatch_event(&ev);
if (g_GUI)
g_GUI->TickObjects();
state.view->Update(state.realFrameLength);
state.view->Render();
if (CProfileManager::IsInitialised())
g_Profiler.Frame();
double time = timer_Time();
if (recent_activity)
last_activity = time;
// Be nice to the processor (by sleeping lots) if we're not doing anything
// useful, and nice to the user (by just yielding to other threads) if we are
bool yield = (time - last_activity > 0.5);
// But make sure we aren't doing anything interesting right now, where
// the user wants to see the screen updating even though they're not
// interacting with it
if (state.view->WantsHighFramerate())
yield = false;
if (yield) // if there was no recent activity...
{
double sleepUntil = time + 0.5; // only redraw at 2fps
while (time < sleepUntil)
{
// To minimise latency when the user starts doing stuff, only
// sleep for a short while, then check if anything's happened,
// then go back to sleep
// (TODO: This should probably be done with something like semaphores)
Atlas_NotifyEndOfFrame(); // (TODO: rename to NotifyEndOfQuiteShortProcessingPeriodSoPleaseSendMeNewMessages or something)
SDL_Delay(50);
if (!msgPasser->IsEmpty())
break;
time = timer_Time();
}
}
else
{
Atlas_NotifyEndOfFrame();
SDL_Delay(0);
}
}
-
- return NULL;
}
bool BeginAtlas(const CmdLineArgs& args, const DllLoader& dll)
{
// Load required symbols from the DLL
try
{
dll.LoadSymbol("Atlas_StartWindow", Atlas_StartWindow);
dll.LoadSymbol("Atlas_SetMessagePasser", Atlas_SetMessagePasser);
dll.LoadSymbol("Atlas_SetDataDirectory", Atlas_SetDataDirectory);
dll.LoadSymbol("Atlas_SetConfigDirectory", Atlas_SetConfigDirectory);
dll.LoadSymbol("Atlas_GLSetCurrent", Atlas_GLSetCurrent);
dll.LoadSymbol("Atlas_GLSwapBuffers", Atlas_GLSwapBuffers);
dll.LoadSymbol("Atlas_NotifyEndOfFrame", Atlas_NotifyEndOfFrame);
dll.LoadSymbol("Atlas_DisplayError", Atlas_DisplayError);
dll.LoadSymbol("ShareableMalloc", ShareableMallocFptr);
dll.LoadSymbol("ShareableFree", ShareableFreeFptr);
}
catch (PSERROR_DllLoader&)
{
debug_warn(L"Failed to initialise DLL");
return false;
}
// Construct a message passer for communicating with Atlas
// (here so that its scope lasts beyond the game thread)
MessagePasserImpl msgPasser;
AtlasMessage::g_MessagePasser = &msgPasser;
// Pass our message handler to Atlas
Atlas_SetMessagePasser(&msgPasser);
// Tell Atlas the location of the data directory
const Paths paths(args);
Atlas_SetDataDirectory(paths.RData().string().c_str());
// Tell Atlas the location of the user config directory
Atlas_SetConfigDirectory(paths.Config().string().c_str());
// Run the engine loop in a new thread
- pthread_t engineThread;
- pthread_create(&engineThread, NULL, RunEngine, reinterpret_cast(const_cast(&args)));
+ std::thread engineThread = std::thread(RunEngine, std::ref(args));
// Start Atlas UI on main thread
// (required for wxOSX/Cocoa compatibility - see http://trac.wildfiregames.com/ticket/500)
Atlas_StartWindow(L"ScenarioEditor");
// Wait for the engine to exit
- pthread_join(engineThread, NULL);
+ engineThread.join();
// TODO: delete all remaining messages, to avoid memory leak warnings
// Restore main thread
ThreadUtil::SetMainThread();
// Clean up
AtlasView::DestroyViews();
AtlasMessage::g_MessagePasser = NULL;
return true;
}