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);
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)
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()
{
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/Mod.cpp
===================================================================
--- ps/trunk/source/ps/Mod.cpp (revision 23320)
+++ ps/trunk/source/ps/Mod.cpp (revision 23321)
@@ -1,158 +1,159 @@
/* 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 "ps/Mod.h"
#include
#include "lib/file/file_system.h"
#include "lib/file/vfs/vfs.h"
#include "lib/utf8.h"
#include "ps/Filesystem.h"
#include "ps/GameSetup/GameSetup.h"
#include "ps/GameSetup/Paths.h"
+#include "ps/Pyrogenesis.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptRuntime.h"
std::vector g_modsLoaded;
std::vector> g_LoadedModVersions;
CmdLineArgs g_args;
JS::Value Mod::GetAvailableMods(const ScriptInterface& scriptInterface)
{
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedObject obj(cx, JS_NewPlainObject(cx));
const Paths paths(g_args);
// loop over all possible paths
OsPath modPath = paths.RData()/"mods";
OsPath modUserPath = paths.UserData()/"mods";
DirectoryNames modDirs;
DirectoryNames modDirsUser;
GetDirectoryEntries(modPath, NULL, &modDirs);
// Sort modDirs so that we can do a fast lookup below
std::sort(modDirs.begin(), modDirs.end());
PIVFS vfs = CreateVfs();
for (DirectoryNames::iterator iter = modDirs.begin(); iter != modDirs.end(); ++iter)
{
vfs->Clear();
if (vfs->Mount(L"", modPath / *iter, VFS_MOUNT_MUST_EXIST) < 0)
continue;
CVFSFile modinfo;
if (modinfo.Load(vfs, L"mod.json", false) != PSRETURN_OK)
continue;
JS::RootedValue json(cx);
if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json))
continue;
// Valid mod, add it to our structure
JS_SetProperty(cx, obj, utf8_from_wstring(iter->string()).c_str(), json);
}
GetDirectoryEntries(modUserPath, NULL, &modDirsUser);
bool dev = InDevelopmentCopy();
for (DirectoryNames::iterator iter = modDirsUser.begin(); iter != modDirsUser.end(); ++iter)
{
// If we are in a dev copy we do not mount mods in the user mod folder that
// are already present in the mod folder, thus we skip those here.
if (dev && std::binary_search(modDirs.begin(), modDirs.end(), *iter))
continue;
vfs->Clear();
if (vfs->Mount(L"", modUserPath / *iter, VFS_MOUNT_MUST_EXIST) < 0)
continue;
CVFSFile modinfo;
if (modinfo.Load(vfs, L"mod.json", false) != PSRETURN_OK)
continue;
JS::RootedValue json(cx);
if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json))
continue;
// Valid mod, add it to our structure
JS_SetProperty(cx, obj, utf8_from_wstring(iter->string()).c_str(), json);
}
return JS::ObjectValue(*obj);
}
void Mod::CacheEnabledModVersions(const shared_ptr& scriptRuntime)
{
ScriptInterface scriptInterface("Engine", "CacheEnabledModVersions", scriptRuntime);
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue availableMods(cx, GetAvailableMods(scriptInterface));
g_LoadedModVersions.clear();
for (const CStr& mod : g_modsLoaded)
{
// Ignore user and mod mod as they are irrelevant for compatibility checks
if (mod == "mod" || mod == "user")
continue;
CStr version;
JS::RootedValue modData(cx);
if (scriptInterface.GetProperty(availableMods, mod.c_str(), &modData))
scriptInterface.GetProperty(modData, "version", version);
g_LoadedModVersions.push_back({mod, version});
}
}
JS::Value Mod::GetLoadedModsWithVersions(const ScriptInterface& scriptInterface)
{
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue returnValue(cx);
scriptInterface.ToJSVal(cx, &returnValue, g_LoadedModVersions);
return returnValue;
}
JS::Value Mod::GetEngineInfo(const ScriptInterface& scriptInterface)
{
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue mods(cx, Mod::GetLoadedModsWithVersions(scriptInterface));
JS::RootedValue metainfo(cx);
ScriptInterface::CreateObject(
cx,
&metainfo,
"engine_version", engine_version,
"mods", mods);
scriptInterface.FreezeObject(metainfo, true);
return metainfo;
}
Index: ps/trunk/source/ps/ModIo.h
===================================================================
--- ps/trunk/source/ps/ModIo.h (revision 23320)
+++ ps/trunk/source/ps/ModIo.h (revision 23321)
@@ -1,208 +1,209 @@
-/* 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.
*/
#ifndef INCLUDED_MODIO
#define INCLUDED_MODIO
#include "lib/external_libraries/curl.h"
+#include "lib/os_path.h"
#include "scriptinterface/ScriptInterface.h"
#include
#include
// TODO: Allocate instance of the below two using sodium_malloc?
struct PKStruct
{
unsigned char sig_alg[2] = {}; // == "Ed"
unsigned char keynum[8] = {}; // should match the keynum in the sigstruct, else this is the wrong key
unsigned char pk[crypto_sign_PUBLICKEYBYTES] = {};
};
struct SigStruct
{
unsigned char sig_alg[2] = {}; // "ED" (since we only support the hashed mode)
unsigned char keynum[8] = {}; // should match the keynum in the PKStruct
unsigned char sig[crypto_sign_BYTES] = {};
};
struct ModIoModData
{
std::map properties;
std::vector dependencies;
SigStruct sig;
};
enum class DownloadProgressStatus {
NONE, // Default state
GAMEID, // The game ID is being downloaded
READY, // The game ID has been downloaded
LISTING, // The mod list is being downloaded
LISTED, // The mod list has been downloaded
DOWNLOADING, // A mod file is being downloaded
SUCCESS, // A mod file has been downloaded
FAILED_GAMEID, // Game ID couldn't be retrieved
FAILED_LISTING, // Mod list couldn't be retrieved
FAILED_DOWNLOADING, // File couldn't be retrieved
FAILED_FILECHECK // The file is corrupted
};
struct DownloadProgressData
{
DownloadProgressStatus status;
double progress;
std::string error;
};
struct DownloadCallbackData;
/**
* mod.io API interfacing code.
*
* Overview
*
* This class interfaces with a remote API provider that returns a list of mod files.
* These can then be downloaded after some cursory checking of well-formedness of the returned
* metadata.
* Downloaded files are checked for well formedness by validating that they fit the size and hash
* indicated by the API, then we check if the file is actually signed by a trusted key, and only
* if all of that is success the file is actually possible to be loaded as a mod.
*
* Security considerations
*
* This both distrusts the loaded JS mods, and the API as much as possible.
* We do not want a malicious mod to use this to download arbitrary files, nor do we want the API
* to make us download something we have not verified.
* Therefore we only allow mods to download one of the mods returned by this class (using indices).
*
* This (mostly) necessitates parsing the API responses here, as opposed to in JS.
* One could alternatively parse the responses in a locked down JS context, but that would require
* storing that code in here, or making sure nobody can overwrite it. Also this would possibly make
* some of the needed accesses for downloading and verifying files a bit more complicated.
*
* Everything downloaded from the API has its signature verified against our public key.
* This is a requirement, as otherwise a compromise of the API would result in users installing
* possibly malicious files.
* So a compromised API can just serve old files that we signed, so in that case there would need
* to be an issue in that old file that was missed.
*
* To limit the extend to how old those files could be the signing key should be rotated
* regularly (e.g. every release). To allow old versions of the engine to still use the API
* files can be signed by both the old and the new key for some amount of time, that however
* only makes sense in case a mod is compatible with both engine versions.
*
* Note that this does not prevent all possible attacks a package manager/update system should
* defend against. This is intentionally not an update system since proper package managers already
* exist. However there is some possible overlap in attack vectors and these should be evalutated
* whether they apply and to what extend we can fix that on our side (or how to get the API provider
* to help us do so). For a list of some possible issues see:
* https://github.com/theupdateframework/specification/blob/master/tuf-spec.md
*
* The mod.io settings are also locked down such that only mods that have been authorized by us
* show up in API queries. This is both done so that all required information (dependencies)
* are stored for the files, and that only mods that have been checked for being ok are actually
* shown to users.
*/
class ModIo
{
NONCOPYABLE(ModIo);
public:
ModIo();
~ModIo();
// Async requests
void StartGetGameId();
void StartListMods();
void StartDownloadMod(size_t idx);
/**
* Advance the current async request and perform final steps if the download is complete.
*
* @param scriptInterface used for parsing the data and possibly install the mod.
* @return true if the download is complete (successful or not), false otherwise.
*/
bool AdvanceRequest(const ScriptInterface& scriptInterface);
/**
* Cancel the current async request and clean things up
*/
void CancelRequest();
const std::vector& GetMods() const
{
return m_ModData;
}
const DownloadProgressData& GetDownloadProgress() const
{
return m_DownloadProgressData;
}
private:
static size_t ReceiveCallback(void* buffer, size_t size, size_t nmemb, void* userp);
static size_t DownloadCallback(void* buffer, size_t size, size_t nmemb, void* userp);
static int DownloadProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow);
CURLMcode SetupRequest(const std::string& url, bool fileDownload);
void TearDownRequest();
bool ParseGameId(const ScriptInterface& scriptInterface, std::string& err);
bool ParseMods(const ScriptInterface& scriptInterface, std::string& err);
void DeleteDownloadedFile();
bool VerifyDownloadedFile(std::string& err);
// Utility methods for parsing mod.io responses and metadata
static bool ParseGameIdResponse(const ScriptInterface& scriptInterface, const std::string& responseData, int& id, std::string& err);
static bool ParseModsResponse(const ScriptInterface& scriptInterface, const std::string& responseData, std::vector& modData, const PKStruct& pk, std::string& err);
static bool ParseSignature(const std::vector& minisigs, SigStruct& sig, const PKStruct& pk, std::string& err);
// Url parts
std::string m_BaseUrl;
std::string m_GamesRequest;
std::string m_GameId;
// Query parameters
std::string m_ApiKey;
std::string m_IdQuery;
CURL* m_Curl;
CURLM* m_CurlMulti;
curl_slist* m_Headers;
char m_ErrorBuffer[CURL_ERROR_SIZE];
std::string m_ResponseData;
// Current mod download
int m_DownloadModID;
OsPath m_DownloadFilePath;
DownloadCallbackData* m_CallbackData;
DownloadProgressData m_DownloadProgressData;
PKStruct m_pk;
std::vector m_ModData;
friend class TestModIo;
};
extern ModIo* g_ModIo;
#endif // INCLUDED_MODIO
Index: ps/trunk/source/ps/ProfileViewer.cpp
===================================================================
--- ps/trunk/source/ps/ProfileViewer.cpp (revision 23320)
+++ ps/trunk/source/ps/ProfileViewer.cpp (revision 23321)
@@ -1,624 +1,625 @@
/* 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 .
*/
/*
* Implementation of profile display (containing only display routines,
* the data model(s) are implemented elsewhere).
*/
#include "precompiled.h"
#include "ProfileViewer.h"
#include "graphics/FontMetrics.h"
#include "graphics/ShaderManager.h"
#include "graphics/TextRenderer.h"
#include "gui/GUIMatrix.h"
#include "lib/external_libraries/libsdl.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/Hotkey.h"
#include "ps/Profile.h"
+#include "ps/Pyrogenesis.h"
#include "renderer/Renderer.h"
#include "scriptinterface/ScriptInterface.h"
#include
#include
extern int g_xres, g_yres;
struct CProfileViewerInternals
{
NONCOPYABLE(CProfileViewerInternals); // because of the ofstream
public:
CProfileViewerInternals() {}
/// Whether the profiling display is currently visible
bool profileVisible;
/// List of root tables
std::vector rootTables;
/// Path from a root table (path[0]) to the currently visible table (path[size-1])
std::vector path;
/// Helper functions
void TableIsDeleted(AbstractProfileTable* table);
void NavigateTree(int id);
/// File for saved profile output (reset when the game is restarted)
std::ofstream outputStream;
};
///////////////////////////////////////////////////////////////////////////////////////////////
// AbstractProfileTable implementation
AbstractProfileTable::~AbstractProfileTable()
{
if (CProfileViewer::IsInitialised())
{
g_ProfileViewer.m->TableIsDeleted(this);
}
}
///////////////////////////////////////////////////////////////////////////////////////////////
// CProfileViewer implementation
// AbstractProfileTable got deleted, make sure we have no dangling pointers
void CProfileViewerInternals::TableIsDeleted(AbstractProfileTable* table)
{
for(int idx = (int)rootTables.size()-1; idx >= 0; --idx)
{
if (rootTables[idx] == table)
rootTables.erase(rootTables.begin() + idx);
}
for(size_t idx = 0; idx < path.size(); ++idx)
{
if (path[idx] != table)
continue;
path.erase(path.begin() + idx, path.end());
if (path.size() == 0)
profileVisible = false;
}
}
// Move into child tables or return to parent tables based on the given number
void CProfileViewerInternals::NavigateTree(int id)
{
if (id == 0)
{
if (path.size() > 1)
path.pop_back();
}
else
{
AbstractProfileTable* table = path[path.size() - 1];
size_t numrows = table->GetNumberRows();
for(size_t row = 0; row < numrows; ++row)
{
AbstractProfileTable* child = table->GetChild(row);
if (!child)
continue;
--id;
if (id == 0)
{
path.push_back(child);
break;
}
}
}
}
// Construction/Destruction
CProfileViewer::CProfileViewer()
{
m = new CProfileViewerInternals;
m->profileVisible = false;
}
CProfileViewer::~CProfileViewer()
{
delete m;
}
// Render
void CProfileViewer::RenderProfile()
{
if (!m->profileVisible)
return;
if (!m->path.size())
{
m->profileVisible = false;
return;
}
PROFILE3_GPU("profile viewer");
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
AbstractProfileTable* table = m->path[m->path.size() - 1];
const std::vector& columns = table->GetColumns();
size_t numrows = table->GetNumberRows();
CStrIntern font_name("mono-stroke-10");
CFontMetrics font(font_name);
int lineSpacing = font.GetLineSpacing();
// Render background
GLint estimate_height;
GLint estimate_width;
estimate_width = 50;
for(size_t i = 0; i < columns.size(); ++i)
estimate_width += (GLint)columns[i].width;
estimate_height = 3 + (GLint)numrows;
if (m->path.size() > 1)
estimate_height += 2;
estimate_height = lineSpacing*estimate_height;
CShaderTechniquePtr solidTech = g_Renderer.GetShaderManager().LoadEffect(str_gui_solid);
solidTech->BeginPass();
CShaderProgramPtr solidShader = solidTech->GetShader();
solidShader->Uniform(str_color, 0.0f, 0.0f, 0.0f, 0.5f);
CMatrix3D transform = GetDefaultGuiMatrix();
solidShader->Uniform(str_transform, transform);
float backgroundVerts[] = {
(float)estimate_width, 0.0f,
0.0f, 0.0f,
0.0f, (float)estimate_height,
0.0f, (float)estimate_height,
(float)estimate_width, (float)estimate_height,
(float)estimate_width, 0.0f
};
solidShader->VertexPointer(2, GL_FLOAT, 0, backgroundVerts);
solidShader->AssertPointersBound();
glDrawArrays(GL_TRIANGLES, 0, 6);
transform.PostTranslate(22.0f, lineSpacing*3.0f, 0.0f);
solidShader->Uniform(str_transform, transform);
// Draw row backgrounds
for (size_t row = 0; row < numrows; ++row)
{
if (row % 2)
solidShader->Uniform(str_color, 1.0f, 1.0f, 1.0f, 0.1f);
else
solidShader->Uniform(str_color, 0.0f, 0.0f, 0.0f, 0.1f);
float rowVerts[] = {
-22.f, 2.f,
estimate_width-22.f, 2.f,
estimate_width-22.f, 2.f-lineSpacing,
estimate_width-22.f, 2.f-lineSpacing,
-22.f, 2.f-lineSpacing,
-22.f, 2.f
};
solidShader->VertexPointer(2, GL_FLOAT, 0, rowVerts);
solidShader->AssertPointersBound();
glDrawArrays(GL_TRIANGLES, 0, 6);
transform.PostTranslate(0.0f, lineSpacing, 0.0f);
solidShader->Uniform(str_transform, transform);
}
solidTech->EndPass();
// Print table and column titles
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);
textRenderer.PrintfAt(2.0f, lineSpacing, L"%hs", table->GetTitle().c_str());
textRenderer.Translate(22.0f, lineSpacing*2.0f, 0.0f);
float colX = 0.0f;
for (size_t col = 0; col < columns.size(); ++col)
{
CStrW text = columns[col].title.FromUTF8();
int w, h;
font.CalculateStringSize(text.c_str(), w, h);
float x = colX;
if (col > 0) // right-align all but the first column
x += columns[col].width - w;
textRenderer.Put(x, 0.0f, text.c_str());
colX += columns[col].width;
}
textRenderer.Translate(0.0f, lineSpacing, 0.0f);
// Print rows
int currentExpandId = 1;
for (size_t row = 0; row < numrows; ++row)
{
if (table->IsHighlightRow(row))
textRenderer.Color(1.0f, 0.5f, 0.5f);
else
textRenderer.Color(1.0f, 1.0f, 1.0f);
if (table->GetChild(row))
{
textRenderer.PrintfAt(-15.0f, 0.0f, L"%d", currentExpandId);
currentExpandId++;
}
float colX = 0.0f;
for (size_t col = 0; col < columns.size(); ++col)
{
CStrW text = table->GetCellText(row, col).FromUTF8();
int w, h;
font.CalculateStringSize(text.c_str(), w, h);
float x = colX;
if (col > 0) // right-align all but the first column
x += columns[col].width - w;
textRenderer.Put(x, 0.0f, text.c_str());
colX += columns[col].width;
}
textRenderer.Translate(0.0f, lineSpacing, 0.0f);
}
textRenderer.Color(1.0f, 1.0f, 1.0f);
if (m->path.size() > 1)
{
textRenderer.Translate(0.0f, lineSpacing, 0.0f);
textRenderer.Put(-15.0f, 0.0f, L"0");
textRenderer.Put(0.0f, 0.0f, L"back to parent");
}
textRenderer.Render();
textTech->EndPass();
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
}
// Handle input
InReaction CProfileViewer::Input(const SDL_Event_* ev)
{
switch(ev->ev.type)
{
case SDL_KEYDOWN:
{
if (!m->profileVisible)
break;
int k = ev->ev.key.keysym.sym;
if (k >= SDLK_0 && k <= SDLK_9)
{
m->NavigateTree(k - SDLK_0);
return IN_HANDLED;
}
break;
}
case SDL_HOTKEYDOWN:
std::string hotkey = static_cast(ev->ev.user.data1);
if( hotkey == "profile.toggle" )
{
if (!m->profileVisible)
{
if (m->rootTables.size())
{
m->profileVisible = true;
m->path.push_back(m->rootTables[0]);
}
}
else
{
size_t i;
for(i = 0; i < m->rootTables.size(); ++i)
{
if (m->rootTables[i] == m->path[0])
break;
}
i++;
m->path.clear();
if (i < m->rootTables.size())
{
m->path.push_back(m->rootTables[i]);
}
else
{
m->profileVisible = false;
}
}
return( IN_HANDLED );
}
else if( hotkey == "profile.save" )
{
SaveToFile();
return( IN_HANDLED );
}
break;
}
return( IN_PASS );
}
InReaction CProfileViewer::InputThunk(const SDL_Event_* ev)
{
if (CProfileViewer::IsInitialised())
return g_ProfileViewer.Input(ev);
return IN_PASS;
}
// Add a table to the list of roots
void CProfileViewer::AddRootTable(AbstractProfileTable* table, bool front)
{
if (front)
m->rootTables.insert(m->rootTables.begin(), table);
else
m->rootTables.push_back(table);
}
namespace
{
struct WriteTable
{
std::ofstream& f;
WriteTable(std::ofstream& f) : f(f) {}
void operator() (AbstractProfileTable* table)
{
std::vector data; // 2d array of (rows+head)*columns elements
const std::vector& columns = table->GetColumns();
// Add column headers to 'data'
for (std::vector::const_iterator col_it = columns.begin();
col_it != columns.end(); ++col_it)
data.push_back(col_it->title);
// Recursively add all profile data to 'data'
WriteRows(1, table, data);
// Calculate the width of each column ( = the maximum width of
// any value in that column)
std::vector columnWidths;
size_t cols = columns.size();
for (size_t c = 0; c < cols; ++c)
{
size_t max = 0;
for (size_t i = c; i < data.size(); i += cols)
max = std::max(max, data[i].length());
columnWidths.push_back(max);
}
// Output data as a formatted table:
f << "\n\n" << table->GetTitle() << "\n";
if (cols == 0) // avoid divide-by-zero
return;
for (size_t r = 0; r < data.size()/cols; ++r)
{
for (size_t c = 0; c < cols; ++c)
f << (c ? " | " : "\n")
<< data[r*cols + c].Pad(PS_TRIM_RIGHT, columnWidths[c]);
// Add dividers under some rows. (Currently only the first, since
// that contains the column headers.)
if (r == 0)
for (size_t c = 0; c < cols; ++c)
f << (c ? "-|-" : "\n")
<< CStr::Repeat("-", columnWidths[c]);
}
}
void WriteRows(int indent, AbstractProfileTable* table, std::vector& data)
{
const std::vector& columns = table->GetColumns();
for (size_t r = 0; r < table->GetNumberRows(); ++r)
{
// Do pretty tree-structure indenting
CStr indentation = CStr::Repeat("| ", indent-1);
if (r+1 == table->GetNumberRows())
indentation += "'-";
else
indentation += "|-";
for (size_t c = 0; c < columns.size(); ++c)
if (c == 0)
data.push_back(indentation + table->GetCellText(r, c));
else
data.push_back(table->GetCellText(r, c));
if (table->GetChild(r))
WriteRows(indent+1, table->GetChild(r), data);
}
}
private:
const WriteTable& operator=(const WriteTable&);
};
struct DumpTable
{
const ScriptInterface& m_ScriptInterface;
JS::PersistentRooted m_Root;
DumpTable(const ScriptInterface& scriptInterface, JS::HandleValue root) :
m_ScriptInterface(scriptInterface), m_Root(scriptInterface.GetJSRuntime(), root)
{
}
// std::for_each requires a move constructor and the use of JS::PersistentRooted apparently breaks a requirement for an
// automatic move constructor
DumpTable(DumpTable && original) :
m_ScriptInterface(original.m_ScriptInterface),
m_Root(original.m_ScriptInterface.GetJSRuntime(), original.m_Root.get())
{
}
void operator() (AbstractProfileTable* table)
{
JSContext* cx = m_ScriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue t(cx);
ScriptInterface::CreateObject(
cx,
&t,
"cols", DumpCols(table),
"data", DumpRows(table));
m_ScriptInterface.SetProperty(m_Root, table->GetTitle().c_str(), t);
}
std::vector DumpCols(AbstractProfileTable* table)
{
std::vector titles;
const std::vector& columns = table->GetColumns();
for (size_t c = 0; c < columns.size(); ++c)
titles.push_back(columns[c].title);
return titles;
}
JS::Value DumpRows(AbstractProfileTable* table)
{
JSContext* cx = m_ScriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue data(cx);
ScriptInterface::CreateObject(cx, &data);
const std::vector& columns = table->GetColumns();
for (size_t r = 0; r < table->GetNumberRows(); ++r)
{
JS::RootedValue row(cx);
ScriptInterface::CreateArray(cx, &row);
m_ScriptInterface.SetProperty(data, table->GetCellText(r, 0).c_str(), row);
if (table->GetChild(r))
{
JS::RootedValue childRows(cx, DumpRows(table->GetChild(r)));
m_ScriptInterface.SetPropertyInt(row, 0, childRows);
}
for (size_t c = 1; c < columns.size(); ++c)
m_ScriptInterface.SetPropertyInt(row, c, table->GetCellText(r, c));
}
return data;
}
private:
const DumpTable& operator=(const DumpTable&);
};
bool SortByName(AbstractProfileTable* a, AbstractProfileTable* b)
{
return (a->GetName() < b->GetName());
}
}
void CProfileViewer::SaveToFile()
{
// Open the file, if necessary. If this method is called several times,
// the profile results will be appended to the previous ones from the same
// run.
if (! m->outputStream.is_open())
{
// Open the file. (It will be closed when the CProfileViewer
// destructor is called.)
OsPath path = psLogDir()/"profile.txt";
m->outputStream.open(OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc);
if (m->outputStream.fail())
{
LOGERROR("Failed to open profile log file");
return;
}
else
{
LOGMESSAGERENDER("Profiler snapshot saved to '%s'", path.string8());
}
}
time_t t;
time(&t);
m->outputStream << "================================================================\n\n";
m->outputStream << "PS profiler snapshot - " << asctime(localtime(&t));
std::vector tables = m->rootTables;
sort(tables.begin(), tables.end(), SortByName);
for_each(tables.begin(), tables.end(), WriteTable(m->outputStream));
m->outputStream << "\n\n================================================================\n";
m->outputStream.flush();
}
void CProfileViewer::ShowTable(const CStr& table)
{
m->path.clear();
if (table.length() > 0)
{
for (size_t i = 0; i < m->rootTables.size(); ++i)
{
if (m->rootTables[i]->GetName() == table)
{
m->path.push_back(m->rootTables[i]);
m->profileVisible = true;
return;
}
}
}
// No matching table found, so don't display anything
m->profileVisible = false;
}
Index: ps/trunk/source/ps/Profiler2.cpp
===================================================================
--- ps/trunk/source/ps/Profiler2.cpp (revision 23320)
+++ ps/trunk/source/ps/Profiler2.cpp (revision 23321)
@@ -1,981 +1,983 @@
/* 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 "lib/os_path.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
#include "ps/Profiler2GPU.h"
+#include "ps/Pyrogenesis.h"
#include "third_party/mongoose/mongoose.h"
#include
#include