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; }