Index: ps/trunk/source/graphics/TextureConverter.cpp
===================================================================
--- ps/trunk/source/graphics/TextureConverter.cpp (revision 22651)
+++ ps/trunk/source/graphics/TextureConverter.cpp (revision 22652)
@@ -1,598 +1,596 @@
/* 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);
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
{
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
m_WorkerThread.join();
// Clean up resources
SDL_DestroySemaphore(m_WorkerSem);
}
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;
}
{
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)
{
std::lock_guard lock(m_WorkerMutex);
if (!m_ResultQueue.empty())
{
result = m_ResultQueue.front();
m_ResultQueue.pop_front();
}
}
if (!result)
{
// no work to do
return false;
}
if (!result->ret)
{
// conversion had failed
ok = false;
return true;
}
// Move output into a correctly-aligned buffer
size_t size = result->output.buffer.size();
shared_ptr file;
AllocateAligned(file, size, maxSectorSize);
memcpy(file.get(), &result->output.buffer[0], size);
if (m_VFS->CreateFile(result->dest, file, size) < 0)
{
// error writing file
ok = false;
return true;
}
// Succeeded in converting texture
texture = result->texture;
dest = result->dest;
ok = true;
return true;
#else // #if CONFIG2_NVTT
return false;
#endif
}
bool CTextureConverter::IsBusy()
{
std::lock_guard lock(m_WorkerMutex);
bool busy = !m_RequestQueue.empty();
return busy;
}
void CTextureConverter::RunThread(CTextureConverter* textureConverter)
{
debug_SetThreadName("TextureConverter");
g_Profiler2.RegisterCurrentThread("texconv");
#if CONFIG2_NVTT
// Wait until the main thread wakes us up
while (SDL_SemWait(textureConverter->m_WorkerSem) == 0)
{
g_Profiler2.RecordSyncMarker();
PROFILE2_EVENT("wakeup");
shared_ptr request;
{
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();
}
// 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
std::lock_guard lock(textureConverter->m_WorkerMutex);
textureConverter->m_ResultQueue.push_back(result);
}
#endif
}
Index: ps/trunk/source/graphics/TextureConverter.h
===================================================================
--- ps/trunk/source/graphics/TextureConverter.h (revision 22651)
+++ ps/trunk/source/graphics/TextureConverter.h (revision 22652)
@@ -1,220 +1,221 @@
/* 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/external_libraries/libsdl.h"
#include "TextureManager.h"
+#include
#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(CTextureConverter* data);
PIVFS m_VFS;
bool m_HighQuality;
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 22651)
+++ ps/trunk/source/lib/timer.cpp (revision 22652)
@@ -1,234 +1,236 @@
/* 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
+#include
+#include
+#include // std::stringstream
#include "lib/module_init.h"
#include "lib/posix/posix_pthread.h"
#include "lib/posix/posix_time.h"
-# include "lib/sysdep/cpu.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 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)
{
std::lock_guard lock(ensure_monotonic_mutex);
static double maxTime;
maxTime = std::max(maxTime, newTime);
newTime = maxTime;
}
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/tools/atlas/GameInterface/GameLoop.cpp
===================================================================
--- ps/trunk/source/tools/atlas/GameInterface/GameLoop.cpp (revision 22651)
+++ ps/trunk/source/tools/atlas/GameInterface/GameLoop.cpp (revision 22652)
@@ -1,330 +1,330 @@
/* 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;
+#include
+
namespace AtlasMessage
{
extern void RegisterHandlers();
}
// Loaded from DLL:
void (*Atlas_StartWindow)(const wchar_t* type);
void (*Atlas_SetDataDirectory)(const wchar_t* path);
void (*Atlas_SetConfigDirectory)(const wchar_t* path);
void (*Atlas_SetMessagePasser)(MessagePasser*);
void (*Atlas_GLSetCurrent)(void* cavas);
void (*Atlas_GLSwapBuffers)(void* canvas);
void (*Atlas_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(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");
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);
}
}
}
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
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
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;
}