Index: ps/trunk/source/graphics/ShaderProgram.cpp =================================================================== --- ps/trunk/source/graphics/ShaderProgram.cpp (revision 19502) +++ ps/trunk/source/graphics/ShaderProgram.cpp (revision 19503) @@ -1,892 +1,892 @@ -/* Copyright (C) 2013 Wildfire Games. +/* Copyright (C) 2017 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 "ShaderProgram.h" #include "graphics/ShaderManager.h" #include "graphics/TextureManager.h" #include "lib/res/graphics/ogl_tex.h" #include "maths/Matrix3D.h" #include "maths/Vector3D.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/PreprocessorWrapper.h" #include "ps/Shapes.h" #if !CONFIG2_GLES class CShaderProgramARB : public CShaderProgram { public: CShaderProgramARB(const VfsPath& vertexFile, const VfsPath& fragmentFile, const CShaderDefines& defines, const std::map& vertexIndexes, const std::map& fragmentIndexes, int streamflags) : CShaderProgram(streamflags), m_VertexFile(vertexFile), m_FragmentFile(fragmentFile), m_Defines(defines), m_VertexIndexes(vertexIndexes), m_FragmentIndexes(fragmentIndexes) { pglGenProgramsARB(1, &m_VertexProgram); pglGenProgramsARB(1, &m_FragmentProgram); } ~CShaderProgramARB() { Unload(); pglDeleteProgramsARB(1, &m_VertexProgram); pglDeleteProgramsARB(1, &m_FragmentProgram); } bool Compile(GLuint target, const char* targetName, GLuint program, const VfsPath& file, const CStr& code) { ogl_WarnIfError(); pglBindProgramARB(target, program); ogl_WarnIfError(); pglProgramStringARB(target, GL_PROGRAM_FORMAT_ASCII_ARB, (GLsizei)code.length(), code.c_str()); if (ogl_SquelchError(GL_INVALID_OPERATION)) { GLint errPos = 0; glGetIntegerv(GL_PROGRAM_ERROR_POSITION_ARB, &errPos); int errLine = std::count(code.begin(), code.begin() + std::min((int)code.length(), errPos + 1), '\n') + 1; char* errStr = (char*)glGetString(GL_PROGRAM_ERROR_STRING_ARB); LOGERROR("Failed to compile %s program '%s' (line %d):\n%s", targetName, file.string8(), errLine, errStr); return false; } pglBindProgramARB(target, 0); ogl_WarnIfError(); return true; } virtual void Reload() { Unload(); CVFSFile vertexFile; if (vertexFile.Load(g_VFS, m_VertexFile) != PSRETURN_OK) return; CVFSFile fragmentFile; if (fragmentFile.Load(g_VFS, m_FragmentFile) != PSRETURN_OK) return; CPreprocessorWrapper preprocessor; preprocessor.AddDefines(m_Defines); CStr vertexCode = preprocessor.Preprocess(vertexFile.GetAsString()); CStr fragmentCode = preprocessor.Preprocess(fragmentFile.GetAsString()); // printf(">>>\n%s<<<\n", vertexCode.c_str()); // printf(">>>\n%s<<<\n", fragmentCode.c_str()); if (!Compile(GL_VERTEX_PROGRAM_ARB, "vertex", m_VertexProgram, m_VertexFile, vertexCode)) return; if (!Compile(GL_FRAGMENT_PROGRAM_ARB, "fragment", m_FragmentProgram, m_FragmentFile, fragmentCode)) return; m_IsValid = true; } void Unload() { m_IsValid = false; } virtual void Bind() { glEnable(GL_VERTEX_PROGRAM_ARB); glEnable(GL_FRAGMENT_PROGRAM_ARB); pglBindProgramARB(GL_VERTEX_PROGRAM_ARB, m_VertexProgram); pglBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, m_FragmentProgram); BindClientStates(); } virtual void Unbind() { glDisable(GL_VERTEX_PROGRAM_ARB); glDisable(GL_FRAGMENT_PROGRAM_ARB); pglBindProgramARB(GL_VERTEX_PROGRAM_ARB, 0); pglBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, 0); UnbindClientStates(); // TODO: should unbind textures, probably } int GetUniformVertexIndex(CStrIntern id) { std::map::iterator it = m_VertexIndexes.find(id); if (it == m_VertexIndexes.end()) return -1; return it->second; } frag_index_pair_t GetUniformFragmentIndex(CStrIntern id) { std::map::iterator it = m_FragmentIndexes.find(id); if (it == m_FragmentIndexes.end()) return std::make_pair(-1, 0); return it->second; } virtual Binding GetTextureBinding(texture_id_t id) { frag_index_pair_t fPair = GetUniformFragmentIndex(id); int index = fPair.first; if (index == -1) return Binding(); else return Binding((int)fPair.second, index); } virtual void BindTexture(texture_id_t id, Handle tex) { frag_index_pair_t fPair = GetUniformFragmentIndex(id); int index = fPair.first; if (index != -1) { GLuint h; ogl_tex_get_texture_id(tex, &h); pglActiveTextureARB(GL_TEXTURE0+index); glBindTexture(fPair.second, h); } } virtual void BindTexture(texture_id_t id, GLuint tex) { frag_index_pair_t fPair = GetUniformFragmentIndex(id); int index = fPair.first; if (index != -1) { pglActiveTextureARB(GL_TEXTURE0+index); glBindTexture(fPair.second, tex); } } virtual void BindTexture(Binding id, Handle tex) { int index = id.second; if (index != -1) ogl_tex_bind(tex, index); } virtual Binding GetUniformBinding(uniform_id_t id) { return Binding(GetUniformVertexIndex(id), GetUniformFragmentIndex(id).first); } virtual void Uniform(Binding id, float v0, float v1, float v2, float v3) { if (id.first != -1) pglProgramLocalParameter4fARB(GL_VERTEX_PROGRAM_ARB, (GLuint)id.first, v0, v1, v2, v3); if (id.second != -1) pglProgramLocalParameter4fARB(GL_FRAGMENT_PROGRAM_ARB, (GLuint)id.second, v0, v1, v2, v3); } virtual void Uniform(Binding id, const CMatrix3D& v) { if (id.first != -1) { pglProgramLocalParameter4fARB(GL_VERTEX_PROGRAM_ARB, (GLuint)id.first+0, v._11, v._12, v._13, v._14); pglProgramLocalParameter4fARB(GL_VERTEX_PROGRAM_ARB, (GLuint)id.first+1, v._21, v._22, v._23, v._24); pglProgramLocalParameter4fARB(GL_VERTEX_PROGRAM_ARB, (GLuint)id.first+2, v._31, v._32, v._33, v._34); pglProgramLocalParameter4fARB(GL_VERTEX_PROGRAM_ARB, (GLuint)id.first+3, v._41, v._42, v._43, v._44); } if (id.second != -1) { pglProgramLocalParameter4fARB(GL_FRAGMENT_PROGRAM_ARB, (GLuint)id.second+0, v._11, v._12, v._13, v._14); pglProgramLocalParameter4fARB(GL_FRAGMENT_PROGRAM_ARB, (GLuint)id.second+1, v._21, v._22, v._23, v._24); pglProgramLocalParameter4fARB(GL_FRAGMENT_PROGRAM_ARB, (GLuint)id.second+2, v._31, v._32, v._33, v._34); pglProgramLocalParameter4fARB(GL_FRAGMENT_PROGRAM_ARB, (GLuint)id.second+3, v._41, v._42, v._43, v._44); } } virtual void Uniform(Binding id, size_t count, const CMatrix3D* v) { ENSURE(count == 1); Uniform(id, v[0]); } private: VfsPath m_VertexFile; VfsPath m_FragmentFile; CShaderDefines m_Defines; GLuint m_VertexProgram; GLuint m_FragmentProgram; std::map m_VertexIndexes; // pair contains std::map m_FragmentIndexes; }; #endif // #if !CONFIG2_GLES ////////////////////////////////////////////////////////////////////////// TIMER_ADD_CLIENT(tc_ShaderGLSLCompile); TIMER_ADD_CLIENT(tc_ShaderGLSLLink); class CShaderProgramGLSL : public CShaderProgram { public: CShaderProgramGLSL(const VfsPath& vertexFile, const VfsPath& fragmentFile, const CShaderDefines& defines, const std::map& vertexAttribs, int streamflags) : CShaderProgram(streamflags), m_VertexFile(vertexFile), m_FragmentFile(fragmentFile), m_Defines(defines), m_VertexAttribs(vertexAttribs) { m_Program = 0; m_VertexShader = pglCreateShaderObjectARB(GL_VERTEX_SHADER); m_FragmentShader = pglCreateShaderObjectARB(GL_FRAGMENT_SHADER); } ~CShaderProgramGLSL() { Unload(); pglDeleteShader(m_VertexShader); pglDeleteShader(m_FragmentShader); } bool Compile(GLhandleARB shader, const VfsPath& file, const CStr& code) { TIMER_ACCRUE(tc_ShaderGLSLCompile); ogl_WarnIfError(); const char* code_string = code.c_str(); GLint code_length = code.length(); pglShaderSourceARB(shader, 1, &code_string, &code_length); pglCompileShaderARB(shader); GLint ok = 0; pglGetShaderiv(shader, GL_COMPILE_STATUS, &ok); GLint length = 0; pglGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length); // Apparently sometimes GL_INFO_LOG_LENGTH is incorrectly reported as 0 // (http://code.google.com/p/android/issues/detail?id=9953) if (!ok && length == 0) length = 4096; if (length > 1) { char* infolog = new char[length]; pglGetShaderInfoLog(shader, length, NULL, infolog); if (ok) LOGMESSAGE("Info when compiling shader '%s':\n%s", file.string8(), infolog); else LOGERROR("Failed to compile shader '%s':\n%s", file.string8(), infolog); delete[] infolog; } ogl_WarnIfError(); return (ok ? true : false); } bool Link() { TIMER_ACCRUE(tc_ShaderGLSLLink); ENSURE(!m_Program); m_Program = pglCreateProgramObjectARB(); pglAttachObjectARB(m_Program, m_VertexShader); ogl_WarnIfError(); pglAttachObjectARB(m_Program, m_FragmentShader); ogl_WarnIfError(); // Set up the attribute bindings explicitly, since apparently drivers // don't always pick the most efficient bindings automatically, // and also this lets us hardcode indexes into VertexPointer etc for (std::map::iterator it = m_VertexAttribs.begin(); it != m_VertexAttribs.end(); ++it) pglBindAttribLocationARB(m_Program, it->second, it->first.c_str()); pglLinkProgramARB(m_Program); GLint ok = 0; pglGetProgramiv(m_Program, GL_LINK_STATUS, &ok); GLint length = 0; pglGetProgramiv(m_Program, GL_INFO_LOG_LENGTH, &length); if (!ok && length == 0) length = 4096; if (length > 1) { char* infolog = new char[length]; pglGetProgramInfoLog(m_Program, length, NULL, infolog); if (ok) LOGMESSAGE("Info when linking program '%s'+'%s':\n%s", m_VertexFile.string8(), m_FragmentFile.string8(), infolog); else LOGERROR("Failed to link program '%s'+'%s':\n%s", m_VertexFile.string8(), m_FragmentFile.string8(), infolog); delete[] infolog; } ogl_WarnIfError(); if (!ok) return false; m_Uniforms.clear(); m_Samplers.clear(); Bind(); ogl_WarnIfError(); GLint numUniforms = 0; pglGetProgramiv(m_Program, GL_ACTIVE_UNIFORMS, &numUniforms); ogl_WarnIfError(); for (GLint i = 0; i < numUniforms; ++i) { char name[256] = {0}; GLsizei nameLength = 0; GLint size = 0; GLenum type = 0; pglGetActiveUniformARB(m_Program, i, ARRAY_SIZE(name), &nameLength, &size, &type, name); ogl_WarnIfError(); GLint loc = pglGetUniformLocationARB(m_Program, name); CStrIntern nameIntern(name); m_Uniforms[nameIntern] = std::make_pair(loc, type); // Assign sampler uniforms to sequential texture units if (type == GL_SAMPLER_2D || type == GL_SAMPLER_CUBE #if !CONFIG2_GLES || type == GL_SAMPLER_2D_SHADOW #endif ) { int unit = (int)m_Samplers.size(); m_Samplers[nameIntern].first = (type == GL_SAMPLER_CUBE ? GL_TEXTURE_CUBE_MAP : GL_TEXTURE_2D); m_Samplers[nameIntern].second = unit; pglUniform1iARB(loc, unit); // link uniform to unit ogl_WarnIfError(); } } // TODO: verify that we're not using more samplers than is supported Unbind(); ogl_WarnIfError(); return true; } virtual void Reload() { Unload(); CVFSFile vertexFile; if (vertexFile.Load(g_VFS, m_VertexFile) != PSRETURN_OK) return; CVFSFile fragmentFile; if (fragmentFile.Load(g_VFS, m_FragmentFile) != PSRETURN_OK) return; CPreprocessorWrapper preprocessor; preprocessor.AddDefines(m_Defines); #if CONFIG2_GLES // GLES defines the macro "GL_ES" in its GLSL preprocessor, // but since we run our own preprocessor first, we need to explicitly // define it here preprocessor.AddDefine("GL_ES", "1"); #endif CStr vertexCode = preprocessor.Preprocess(vertexFile.GetAsString()); CStr fragmentCode = preprocessor.Preprocess(fragmentFile.GetAsString()); #if CONFIG2_GLES // Ugly hack to replace desktop GLSL 1.10/1.20 with GLSL ES 1.00, // and also to set default float precision for fragment shaders vertexCode.Replace("#version 110\n", "#version 100\n"); vertexCode.Replace("#version 110\r\n", "#version 100\n"); vertexCode.Replace("#version 120\n", "#version 100\n"); vertexCode.Replace("#version 120\r\n", "#version 100\n"); fragmentCode.Replace("#version 110\n", "#version 100\nprecision mediump float;\n"); fragmentCode.Replace("#version 110\r\n", "#version 100\nprecision mediump float;\n"); fragmentCode.Replace("#version 120\n", "#version 100\nprecision mediump float;\n"); fragmentCode.Replace("#version 120\r\n", "#version 100\nprecision mediump float;\n"); #endif if (!Compile(m_VertexShader, m_VertexFile, vertexCode)) return; if (!Compile(m_FragmentShader, m_FragmentFile, fragmentCode)) return; if (!Link()) return; m_IsValid = true; } void Unload() { m_IsValid = false; if (m_Program) pglDeleteProgram(m_Program); m_Program = 0; // The shader objects can be reused and don't need to be deleted here } virtual void Bind() { pglUseProgramObjectARB(m_Program); for (std::map::iterator it = m_VertexAttribs.begin(); it != m_VertexAttribs.end(); ++it) pglEnableVertexAttribArrayARB(it->second); } virtual void Unbind() { pglUseProgramObjectARB(0); for (std::map::iterator it = m_VertexAttribs.begin(); it != m_VertexAttribs.end(); ++it) pglDisableVertexAttribArrayARB(it->second); // TODO: should unbind textures, probably } virtual Binding GetTextureBinding(texture_id_t id) { std::map >::iterator it = m_Samplers.find(CStrIntern(id)); if (it == m_Samplers.end()) return Binding(); else return Binding((int)it->second.first, it->second.second); } virtual void BindTexture(texture_id_t id, Handle tex) { std::map >::iterator it = m_Samplers.find(CStrIntern(id)); if (it == m_Samplers.end()) return; GLuint h; ogl_tex_get_texture_id(tex, &h); pglActiveTextureARB(GL_TEXTURE0 + it->second.second); glBindTexture(it->second.first, h); } virtual void BindTexture(texture_id_t id, GLuint tex) { std::map >::iterator it = m_Samplers.find(CStrIntern(id)); if (it == m_Samplers.end()) return; pglActiveTextureARB(GL_TEXTURE0 + it->second.second); glBindTexture(it->second.first, tex); } virtual void BindTexture(Binding id, Handle tex) { if (id.second == -1) return; GLuint h; ogl_tex_get_texture_id(tex, &h); pglActiveTextureARB(GL_TEXTURE0 + id.second); glBindTexture(id.first, h); } virtual Binding GetUniformBinding(uniform_id_t id) { std::map >::iterator it = m_Uniforms.find(id); if (it == m_Uniforms.end()) return Binding(); else return Binding(it->second.first, (int)it->second.second); } virtual void Uniform(Binding id, float v0, float v1, float v2, float v3) { if (id.first != -1) { if (id.second == GL_FLOAT) pglUniform1fARB(id.first, v0); else if (id.second == GL_FLOAT_VEC2) pglUniform2fARB(id.first, v0, v1); else if (id.second == GL_FLOAT_VEC3) pglUniform3fARB(id.first, v0, v1, v2); else if (id.second == GL_FLOAT_VEC4) pglUniform4fARB(id.first, v0, v1, v2, v3); else LOGERROR("CShaderProgramGLSL::Uniform(): Invalid uniform type (expected float, vec2, vec3, vec4)"); } } virtual void Uniform(Binding id, const CMatrix3D& v) { if (id.first != -1) { if (id.second == GL_FLOAT_MAT4) pglUniformMatrix4fvARB(id.first, 1, GL_FALSE, &v._11); else LOGERROR("CShaderProgramGLSL::Uniform(): Invalid uniform type (expected mat4)"); } } virtual void Uniform(Binding id, size_t count, const CMatrix3D* v) { if (id.first != -1) { if (id.second == GL_FLOAT_MAT4) pglUniformMatrix4fvARB(id.first, count, GL_FALSE, &v->_11); else LOGERROR("CShaderProgramGLSL::Uniform(): Invalid uniform type (expected mat4)"); } } // Map the various fixed-function Pointer functions onto generic vertex attributes // (matching the attribute indexes from ShaderManager's ParseAttribSemantics): virtual void VertexPointer(GLint size, GLenum type, GLsizei stride, const void* pointer) { pglVertexAttribPointerARB(0, size, type, GL_FALSE, stride, pointer); m_ValidStreams |= STREAM_POS; } virtual void NormalPointer(GLenum type, GLsizei stride, const void* pointer) { pglVertexAttribPointerARB(2, 3, type, GL_TRUE, stride, pointer); m_ValidStreams |= STREAM_NORMAL; } virtual void ColorPointer(GLint size, GLenum type, GLsizei stride, const void* pointer) { pglVertexAttribPointerARB(3, size, type, GL_TRUE, stride, pointer); m_ValidStreams |= STREAM_COLOR; } virtual void TexCoordPointer(GLenum texture, GLint size, GLenum type, GLsizei stride, const void* pointer) { pglVertexAttribPointerARB(8 + texture - GL_TEXTURE0, size, type, GL_FALSE, stride, pointer); m_ValidStreams |= STREAM_UV0 << (texture - GL_TEXTURE0); } virtual void VertexAttribPointer(attrib_id_t id, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void* pointer) { std::map::iterator it = m_VertexAttribs.find(id); if (it != m_VertexAttribs.end()) { pglVertexAttribPointerARB(it->second, size, type, normalized, stride, pointer); } } virtual void VertexAttribIPointer(attrib_id_t id, GLint size, GLenum type, GLsizei stride, const void* pointer) { std::map::iterator it = m_VertexAttribs.find(id); if (it != m_VertexAttribs.end()) { #if CONFIG2_GLES debug_warn(L"glVertexAttribIPointer not supported on GLES"); #else pglVertexAttribIPointerEXT(it->second, size, type, stride, pointer); #endif } } private: VfsPath m_VertexFile; VfsPath m_FragmentFile; CShaderDefines m_Defines; std::map m_VertexAttribs; GLhandleARB m_Program; GLhandleARB m_VertexShader; GLhandleARB m_FragmentShader; std::map > m_Uniforms; std::map > m_Samplers; // texture target & unit chosen for each uniform sampler }; ////////////////////////////////////////////////////////////////////////// CShaderProgram::CShaderProgram(int streamflags) : m_IsValid(false), m_StreamFlags(streamflags), m_ValidStreams(0) { } #if CONFIG2_GLES /*static*/ CShaderProgram* CShaderProgram::ConstructARB(const VfsPath& vertexFile, const VfsPath& fragmentFile, const CShaderDefines& UNUSED(defines), const std::map& UNUSED(vertexIndexes), const std::map& UNUSED(fragmentIndexes), int UNUSED(streamflags)) { LOGERROR("CShaderProgram::ConstructARB: '%s'+'%s': ARB shaders not supported on this device", vertexFile.string8(), fragmentFile.string8()); return NULL; } #else /*static*/ CShaderProgram* CShaderProgram::ConstructARB(const VfsPath& vertexFile, const VfsPath& fragmentFile, const CShaderDefines& defines, const std::map& vertexIndexes, const std::map& fragmentIndexes, int streamflags) { return new CShaderProgramARB(vertexFile, fragmentFile, defines, vertexIndexes, fragmentIndexes, streamflags); } #endif /*static*/ CShaderProgram* CShaderProgram::ConstructGLSL(const VfsPath& vertexFile, const VfsPath& fragmentFile, const CShaderDefines& defines, const std::map& vertexAttribs, int streamflags) { return new CShaderProgramGLSL(vertexFile, fragmentFile, defines, vertexAttribs, streamflags); } bool CShaderProgram::IsValid() const { return m_IsValid; } int CShaderProgram::GetStreamFlags() const { return m_StreamFlags; } void CShaderProgram::BindTexture(texture_id_t id, CTexturePtr tex) { BindTexture(id, tex->GetHandle()); } void CShaderProgram::Uniform(Binding id, int v) { Uniform(id, (float)v, (float)v, (float)v, (float)v); } void CShaderProgram::Uniform(Binding id, float v) { Uniform(id, v, v, v, v); } void CShaderProgram::Uniform(Binding id, float v0, float v1) { Uniform(id, v0, v1, 0.0f, 0.0f); } void CShaderProgram::Uniform(Binding id, const CVector3D& v) { Uniform(id, v.X, v.Y, v.Z, 0.0f); } void CShaderProgram::Uniform(Binding id, const CColor& v) { Uniform(id, v.r, v.g, v.b, v.a); } void CShaderProgram::Uniform(uniform_id_t id, int v) { Uniform(GetUniformBinding(id), (float)v, (float)v, (float)v, (float)v); } void CShaderProgram::Uniform(uniform_id_t id, float v) { Uniform(GetUniformBinding(id), v, v, v, v); } void CShaderProgram::Uniform(uniform_id_t id, float v0, float v1) { Uniform(GetUniformBinding(id), v0, v1, 0.0f, 0.0f); } void CShaderProgram::Uniform(uniform_id_t id, const CVector3D& v) { Uniform(GetUniformBinding(id), v.X, v.Y, v.Z, 0.0f); } void CShaderProgram::Uniform(uniform_id_t id, const CColor& v) { Uniform(GetUniformBinding(id), v.r, v.g, v.b, v.a); } void CShaderProgram::Uniform(uniform_id_t id, float v0, float v1, float v2, float v3) { Uniform(GetUniformBinding(id), v0, v1, v2, v3); } void CShaderProgram::Uniform(uniform_id_t id, const CMatrix3D& v) { Uniform(GetUniformBinding(id), v); } void CShaderProgram::Uniform(uniform_id_t id, size_t count, const CMatrix3D* v) { Uniform(GetUniformBinding(id), count, v); } // These should all be overridden by CShaderProgramGLSL, and not used // if a non-GLSL shader was loaded instead: void CShaderProgram::VertexAttribPointer(attrib_id_t UNUSED(id), GLint UNUSED(size), GLenum UNUSED(type), GLboolean UNUSED(normalized), GLsizei UNUSED(stride), const void* UNUSED(pointer)) { debug_warn("Shader type doesn't support VertexAttribPointer"); } void CShaderProgram::VertexAttribIPointer(attrib_id_t UNUSED(id), GLint UNUSED(size), GLenum UNUSED(type), GLsizei UNUSED(stride), const void* UNUSED(pointer)) { debug_warn("Shader type doesn't support VertexAttribIPointer"); } #if CONFIG2_GLES // These should all be overridden by CShaderProgramGLSL // (GLES doesn't support any other types of shader program): void CShaderProgram::VertexPointer(GLint UNUSED(size), GLenum UNUSED(type), GLsizei UNUSED(stride), const void* UNUSED(pointer)) { debug_warn("CShaderProgram::VertexPointer should be overridden"); } void CShaderProgram::NormalPointer(GLenum UNUSED(type), GLsizei UNUSED(stride), const void* UNUSED(pointer)) { debug_warn("CShaderProgram::NormalPointer should be overridden"); } void CShaderProgram::ColorPointer(GLint UNUSED(size), GLenum UNUSED(type), GLsizei UNUSED(stride), const void* UNUSED(pointer)) { debug_warn("CShaderProgram::ColorPointer should be overridden"); } void CShaderProgram::TexCoordPointer(GLenum UNUSED(texture), GLint UNUSED(size), GLenum UNUSED(type), GLsizei UNUSED(stride), const void* UNUSED(pointer)) { debug_warn("CShaderProgram::TexCoordPointer should be overridden"); } #else // These are overridden by CShaderProgramGLSL, but fixed-function and ARB shaders // both use the fixed-function vertex attribute pointers so we'll share their // definitions here: void CShaderProgram::VertexPointer(GLint size, GLenum type, GLsizei stride, const void* pointer) { glVertexPointer(size, type, stride, pointer); m_ValidStreams |= STREAM_POS; } void CShaderProgram::NormalPointer(GLenum type, GLsizei stride, const void* pointer) { glNormalPointer(type, stride, pointer); m_ValidStreams |= STREAM_NORMAL; } void CShaderProgram::ColorPointer(GLint size, GLenum type, GLsizei stride, const void* pointer) { glColorPointer(size, type, stride, pointer); m_ValidStreams |= STREAM_COLOR; } void CShaderProgram::TexCoordPointer(GLenum texture, GLint size, GLenum type, GLsizei stride, const void* pointer) { pglClientActiveTextureARB(texture); glTexCoordPointer(size, type, stride, pointer); pglClientActiveTextureARB(GL_TEXTURE0); m_ValidStreams |= STREAM_UV0 << (texture - GL_TEXTURE0); } void CShaderProgram::BindClientStates() { ENSURE(m_StreamFlags == (m_StreamFlags & (STREAM_POS|STREAM_NORMAL|STREAM_COLOR|STREAM_UV0|STREAM_UV1))); // Enable all the desired client states for non-GLSL rendering if (m_StreamFlags & STREAM_POS) glEnableClientState(GL_VERTEX_ARRAY); if (m_StreamFlags & STREAM_NORMAL) glEnableClientState(GL_NORMAL_ARRAY); if (m_StreamFlags & STREAM_COLOR) glEnableClientState(GL_COLOR_ARRAY); if (m_StreamFlags & STREAM_UV0) { pglClientActiveTextureARB(GL_TEXTURE0); glEnableClientState(GL_TEXTURE_COORD_ARRAY); } if (m_StreamFlags & STREAM_UV1) { pglClientActiveTextureARB(GL_TEXTURE1); glEnableClientState(GL_TEXTURE_COORD_ARRAY); pglClientActiveTextureARB(GL_TEXTURE0); } // Rendering code must subsequently call VertexPointer etc for all of the streams // that were activated in this function, else AssertPointersBound will complain // that some arrays were unspecified m_ValidStreams = 0; } void CShaderProgram::UnbindClientStates() { if (m_StreamFlags & STREAM_POS) glDisableClientState(GL_VERTEX_ARRAY); if (m_StreamFlags & STREAM_NORMAL) glDisableClientState(GL_NORMAL_ARRAY); if (m_StreamFlags & STREAM_COLOR) glDisableClientState(GL_COLOR_ARRAY); if (m_StreamFlags & STREAM_UV0) { pglClientActiveTextureARB(GL_TEXTURE0); glDisableClientState(GL_TEXTURE_COORD_ARRAY); } if (m_StreamFlags & STREAM_UV1) { pglClientActiveTextureARB(GL_TEXTURE1); glDisableClientState(GL_TEXTURE_COORD_ARRAY); pglClientActiveTextureARB(GL_TEXTURE0); } } #endif // !CONFIG2_GLES void CShaderProgram::AssertPointersBound() { ENSURE((m_StreamFlags & ~m_ValidStreams) == 0); } Index: ps/trunk/source/graphics/ShaderProgram.h =================================================================== --- ps/trunk/source/graphics/ShaderProgram.h (revision 19502) +++ ps/trunk/source/graphics/ShaderProgram.h (revision 19503) @@ -1,208 +1,208 @@ -/* Copyright (C) 2012 Wildfire Games. +/* Copyright (C) 2017 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_SHADERPROGRAM #define INCLUDED_SHADERPROGRAM #include "graphics/ShaderProgramPtr.h" #include "graphics/Texture.h" #include "lib/ogl.h" #include "lib/file/vfs/vfs_path.h" #include "lib/res/handle.h" #include struct CColor; class CMatrix3D; class CVector3D; class CShaderDefines; class CStrIntern; // Vertex data stream flags enum { STREAM_POS = (1 << 0), STREAM_NORMAL = (1 << 1), STREAM_COLOR = (1 << 2), STREAM_UV0 = (1 << 3), STREAM_UV1 = (1 << 4), STREAM_UV2 = (1 << 5), STREAM_UV3 = (1 << 6), STREAM_POSTOUV0 = (1 << 7), STREAM_POSTOUV1 = (1 << 8), STREAM_POSTOUV2 = (1 << 9), STREAM_POSTOUV3 = (1 << 10) }; /** * A compiled vertex+fragment shader program. * The implementation may use GL_ARB_{vertex,fragment}_program (ARB assembly syntax) * or GL_ARB_{vertex,fragment}_shader (GLSL), or may use hard-coded fixed-function * multitexturing setup code; the difference is hidden from the caller. * * Texture/uniform IDs are typically strings, corresponding to the names defined in * the shader .xml file. Alternatively (and more efficiently, if used very frequently), * call GetTextureBinding/GetUniformBinding and pass its return value as the ID. * Setting uniforms that the shader .xml doesn't support is harmless. * * For a high-level overview of shaders and materials, see * http://trac.wildfiregames.com/wiki/MaterialSystem */ class CShaderProgram { NONCOPYABLE(CShaderProgram); public: typedef CStrIntern attrib_id_t; typedef CStrIntern texture_id_t; typedef CStrIntern uniform_id_t; typedef std::pair frag_index_pair_t; /** * Construct based on ARB vertex/fragment program files. */ static CShaderProgram* ConstructARB(const VfsPath& vertexFile, const VfsPath& fragmentFile, const CShaderDefines& defines, const std::map& vertexIndexes, const std::map& fragmentIndexes, int streamflags); /** * Construct based on GLSL vertex/fragment shader files. */ static CShaderProgram* ConstructGLSL(const VfsPath& vertexFile, const VfsPath& fragmentFile, const CShaderDefines& defines, const std::map& vertexAttribs, int streamflags); /** * Construct an instance of a pre-defined fixed-function pipeline setup. */ static CShaderProgram* ConstructFFP(const std::string& id, const CShaderDefines& defines); /** * Represents a uniform attribute or texture binding. * For uniforms: * - ARB shaders store vertex location in 'first', fragment location in 'second'. * - GLSL shaders store uniform location in 'first', data type in 'second'. * - FFP shaders store -1 in 'first', index in 'second'. * For textures, all store texture target (e.g. GL_TEXTURE_2D) in 'first', texture unit in 'second'. * Non-existent bindings must store -1 in both. */ struct Binding { Binding(int a, int b) : first(a), second(b) { } Binding() : first(-1), second(-1) { } /** * Returns whether this uniform attribute is active in the shader. * If not then there's no point calling Uniform() to set its value. */ bool Active() { return first != -1 || second != -1; } int first; int second; }; virtual ~CShaderProgram() { } virtual void Reload() = 0; /** * Returns whether this shader was successfully loaded. */ bool IsValid() const; /** * Binds the shader into the GL context. Call this before calling Uniform() * or trying to render with it. */ virtual void Bind() = 0; /** * Unbinds the shader from the GL context. Call this after rendering with it. */ virtual void Unbind() = 0; /** * Returns bitset of STREAM_* value, indicating what vertex data streams the * vertex shader needs (e.g. position, color, UV, ...). */ int GetStreamFlags() const; virtual Binding GetTextureBinding(texture_id_t id) = 0; // Variants of texture binding: void BindTexture(texture_id_t id, CTexturePtr tex); virtual void BindTexture(texture_id_t id, Handle tex) = 0; virtual void BindTexture(texture_id_t id, GLuint tex) = 0; virtual void BindTexture(Binding id, Handle tex) = 0; virtual Binding GetUniformBinding(uniform_id_t id) = 0; // Uniform-setting methods that subclasses must define: virtual void Uniform(Binding id, float v0, float v1, float v2, float v3) = 0; virtual void Uniform(Binding id, const CMatrix3D& v) = 0; virtual void Uniform(Binding id, size_t count, const CMatrix3D* v) = 0; // Convenient uniform-setting wrappers: void Uniform(Binding id, int v); void Uniform(Binding id, float v); void Uniform(Binding id, float v0, float v1); void Uniform(Binding id, const CVector3D& v); void Uniform(Binding id, const CColor& v); void Uniform(uniform_id_t id, int v); void Uniform(uniform_id_t id, float v); void Uniform(uniform_id_t id, float v0, float v1); void Uniform(uniform_id_t id, const CVector3D& v); void Uniform(uniform_id_t id, const CColor& v); void Uniform(uniform_id_t id, float v0, float v1, float v2, float v3); void Uniform(uniform_id_t id, const CMatrix3D& v); void Uniform(uniform_id_t id, size_t count, const CMatrix3D* v); // Vertex attribute pointers (equivalent to glVertexPointer etc): virtual void VertexPointer(GLint size, GLenum type, GLsizei stride, const void* pointer); virtual void NormalPointer(GLenum type, GLsizei stride, const void* pointer); virtual void ColorPointer(GLint size, GLenum type, GLsizei stride, const void* pointer); virtual void TexCoordPointer(GLenum texture, GLint size, GLenum type, GLsizei stride, const void* pointer); virtual void VertexAttribPointer(attrib_id_t id, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void* pointer); virtual void VertexAttribIPointer(attrib_id_t id, GLint size, GLenum type, GLsizei stride, const void* pointer); /** * Checks that all the required vertex attributes have been set. * Call this before calling glDrawArrays/glDrawElements etc to avoid potential crashes. */ void AssertPointersBound(); protected: CShaderProgram(int streamflags); bool m_IsValid; int m_StreamFlags; // Non-GLSL client state handling: void BindClientStates(); void UnbindClientStates(); int m_ValidStreams; // which streams have been specified via VertexPointer etc since the last Bind }; #endif // INCLUDED_SHADERPROGRAM Index: ps/trunk/source/gui/CChart.h =================================================================== --- ps/trunk/source/gui/CChart.h (revision 19502) +++ ps/trunk/source/gui/CChart.h (revision 19503) @@ -1,70 +1,70 @@ -/* Copyright (C) 2016 Wildfire Games. +/* Copyright (C) 2017 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_CCHART #define INCLUDED_CCHART #include "GUI.h" #include "graphics/Color.h" #include "maths/Vector2D.h" #include struct CChartData { CColor m_Color; std::vector m_Points; }; /** * Chart for a data visualization as lines or points * * @see IGUIObject */ class CChart : public IGUIObject { GUI_OBJECT(CChart) public: CChart(); virtual ~CChart(); protected: /** * @see IGUIObject#HandleMessage() */ virtual void HandleMessage(SGUIMessage& Message); /** * Draws the Chart */ virtual void Draw(); virtual CRect GetChartRect() const; void UpdateSeries(); std::vector m_Series; private: /** * Helper function */ void DrawLine(const CShaderProgramPtr& shader, const CColor& color, const std::vector& vertices) const; }; #endif // INCLUDED_CCHART Index: ps/trunk/source/i18n/L10n.cpp =================================================================== --- ps/trunk/source/i18n/L10n.cpp (revision 19502) +++ ps/trunk/source/i18n/L10n.cpp (revision 19503) @@ -1,559 +1,559 @@ -/* Copyright (c) 2017 Wildfire Games +/* Copyright (C) 2017 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 "i18n/L10n.h" #include #include #include #include #include "gui/GUIManager.h" #include "lib/file/file_system.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/GameSetup/GameSetup.h" static Status ReloadChangedFileCB(void* param, const VfsPath& path) { return static_cast(param)->ReloadChangedFile(path); } L10n::L10n() : dictionary(new tinygettext::Dictionary()), currentLocaleIsOriginalGameLocale(false), useLongStrings(false) { // Determine whether or not to print tinygettext messages to the standard // error output, which it tinygettext’s default behavior, but not ours. bool tinygettext_debug = false; CFG_GET_VAL("tinygettext.debug", tinygettext_debug); if (!tinygettext_debug) { tinygettext::Log::log_info_callback = 0; tinygettext::Log::log_warning_callback = 0; tinygettext::Log::log_error_callback = 0; } LoadListOfAvailableLocales(); ReevaluateCurrentLocaleAndReload(); // Handle hotloading RegisterFileReloadFunc(ReloadChangedFileCB, this); } L10n::~L10n() { UnregisterFileReloadFunc(ReloadChangedFileCB, this); for (Locale* const& locale : availableLocales) delete locale; delete dictionary; } Locale L10n::GetCurrentLocale() const { return currentLocale; } bool L10n::SaveLocale(const std::string& localeCode) const { if (localeCode == "long" && InDevelopmentCopy()) { g_ConfigDB.SetValueString(CFG_USER, "locale", "long"); return true; } return SaveLocale(Locale(Locale::createCanonical(localeCode.c_str()))); } bool L10n::SaveLocale(const Locale& locale) const { if (!ValidateLocale(locale)) return false; g_ConfigDB.SetValueString(CFG_USER, "locale", locale.getName()); return g_ConfigDB.WriteValueToFile(CFG_USER, "locale", locale.getName()); } bool L10n::ValidateLocale(const std::string& localeCode) const { return ValidateLocale(Locale::createCanonical(localeCode.c_str())); } // Returns true if both of these conditions are true: // 1. ICU has resources for that locale (which also ensures it's a valid locale string) // 2. Either a dictionary for language_country or for language is available. bool L10n::ValidateLocale(const Locale& locale) const { if (locale.isBogus()) return false; return !GetFallbackToAvailableDictLocale(locale).empty(); } std::vector L10n::GetDictionariesForLocale(const std::string& locale) const { std::vector ret; VfsPaths filenames; std::wstring dictName = GetFallbackToAvailableDictLocale(Locale::createCanonical(locale.c_str())); vfs::GetPathnames(g_VFS, L"l10n/", dictName.append(L".*.po").c_str(), filenames); for (const VfsPath& path : filenames) ret.push_back(path.Filename().string()); return ret; } std::wstring L10n::GetFallbackToAvailableDictLocale(const std::string& locale) const { return GetFallbackToAvailableDictLocale(Locale::createCanonical(locale.c_str())); } std::wstring L10n::GetFallbackToAvailableDictLocale(const Locale& locale) const { std::wstringstream stream; std::function checkLangAndCountry = [&locale](const Locale* const& l) { return strcmp(locale.getLanguage(), l->getLanguage()) == 0 && strcmp(locale.getCountry(), l->getCountry()) == 0; }; if (strcmp(locale.getCountry(), "") != 0 && std::find_if(availableLocales.begin(), availableLocales.end(), checkLangAndCountry) != availableLocales.end()) { stream << locale.getLanguage() << L"_" << locale.getCountry(); return stream.str(); } std::function checkLang = [&locale](const Locale* const& l) { return strcmp(locale.getLanguage(), l->getLanguage()) == 0; }; if (std::find_if(availableLocales.begin(), availableLocales.end(), checkLang) != availableLocales.end()) { stream << locale.getLanguage(); return stream.str(); } return L""; } std::string L10n::GetDictionaryLocale(const std::string& configLocaleString) const { Locale out; GetDictionaryLocale(configLocaleString, out); return out.getName(); } // First, try to get a valid locale from the config, then check if the system locale can be used and otherwise fall back to en_US. void L10n::GetDictionaryLocale(const std::string& configLocaleString, Locale& outLocale) const { if (!configLocaleString.empty()) { Locale configLocale = Locale::createCanonical(configLocaleString.c_str()); if (ValidateLocale(configLocale)) { outLocale = configLocale; return; } else LOGWARNING("The configured locale is not valid or no translations are available. Falling back to another locale."); } Locale systemLocale = Locale::getDefault(); if (ValidateLocale(systemLocale)) outLocale = systemLocale; else outLocale = Locale::getUS(); } // Try to find the best dictionary locale based on user configuration and system locale, set the currentLocale and reload the dictionary. void L10n::ReevaluateCurrentLocaleAndReload() { std::string locale; CFG_GET_VAL("locale", locale); if (locale == "long") { // Set ICU to en_US to have a valid language for displaying dates currentLocale = Locale::getUS(); currentLocaleIsOriginalGameLocale = false; useLongStrings = true; } else { GetDictionaryLocale(locale, currentLocale); currentLocaleIsOriginalGameLocale = (currentLocale == Locale::getUS()) == TRUE; useLongStrings = false; } LoadDictionaryForCurrentLocale(); } // Get all locales supported by ICU. std::vector L10n::GetAllLocales() const { std::vector ret; int32_t count; const Locale* icuSupportedLocales = Locale::getAvailableLocales(count); for (int i=0; i L10n::GetSupportedLocaleBaseNames() const { std::vector supportedLocaleCodes; for (Locale* const& locale : availableLocales) { if (!InDevelopmentCopy() && strcmp(locale->getBaseName(), "long") == 0) continue; supportedLocaleCodes.push_back(locale->getBaseName()); } return supportedLocaleCodes; } std::vector L10n::GetSupportedLocaleDisplayNames() const { std::vector supportedLocaleDisplayNames; for (Locale* const& locale : availableLocales) { if (strcmp(locale->getBaseName(), "long") == 0) { if (InDevelopmentCopy()) supportedLocaleDisplayNames.push_back(wstring_from_utf8(Translate("Long strings"))); continue; } UnicodeString utf16LocaleDisplayName; locale->getDisplayName(*locale, utf16LocaleDisplayName); char localeDisplayName[512]; CheckedArrayByteSink sink(localeDisplayName, ARRAY_SIZE(localeDisplayName)); utf16LocaleDisplayName.toUTF8(sink); ENSURE(!sink.Overflowed()); supportedLocaleDisplayNames.push_back(wstring_from_utf8(std::string(localeDisplayName, sink.NumberOfBytesWritten()))); } return supportedLocaleDisplayNames; } std::string L10n::GetCurrentLocaleString() const { return currentLocale.getName(); } std::string L10n::GetLocaleLanguage(const std::string& locale) const { Locale loc = Locale::createCanonical(locale.c_str()); return loc.getLanguage(); } std::string L10n::GetLocaleBaseName(const std::string& locale) const { Locale loc = Locale::createCanonical(locale.c_str()); return loc.getBaseName(); } std::string L10n::GetLocaleCountry(const std::string& locale) const { Locale loc = Locale::createCanonical(locale.c_str()); return loc.getCountry(); } std::string L10n::GetLocaleScript(const std::string& locale) const { Locale loc = Locale::createCanonical(locale.c_str()); return loc.getScript(); } std::string L10n::Translate(const std::string& sourceString) const { if (!currentLocaleIsOriginalGameLocale) return dictionary->translate(sourceString); return sourceString; } std::string L10n::TranslateWithContext(const std::string& context, const std::string& sourceString) const { if (!currentLocaleIsOriginalGameLocale) return dictionary->translate_ctxt(context, sourceString); return sourceString; } std::string L10n::TranslatePlural(const std::string& singularSourceString, const std::string& pluralSourceString, int number) const { if (!currentLocaleIsOriginalGameLocale) return dictionary->translate_plural(singularSourceString, pluralSourceString, number); if (number == 1) return singularSourceString; return pluralSourceString; } std::string L10n::TranslatePluralWithContext(const std::string& context, const std::string& singularSourceString, const std::string& pluralSourceString, int number) const { if (!currentLocaleIsOriginalGameLocale) return dictionary->translate_ctxt_plural(context, singularSourceString, pluralSourceString, number); if (number == 1) return singularSourceString; return pluralSourceString; } std::string L10n::TranslateLines(const std::string& sourceString) const { std::string targetString; std::stringstream stringOfLines(sourceString); std::string line; while (std::getline(stringOfLines, line)) { if (!line.empty()) targetString.append(Translate(line)); targetString.append("\n"); } return targetString; } UDate L10n::ParseDateTime(const std::string& dateTimeString, const std::string& dateTimeFormat, const Locale& locale) const { UErrorCode success = U_ZERO_ERROR; UnicodeString utf16DateTimeString = UnicodeString::fromUTF8(dateTimeString.c_str()); UnicodeString utf16DateTimeFormat = UnicodeString::fromUTF8(dateTimeFormat.c_str()); DateFormat* dateFormatter = new SimpleDateFormat(utf16DateTimeFormat, locale, success); UDate date = dateFormatter->parse(utf16DateTimeString, success); delete dateFormatter; return date; } std::string L10n::LocalizeDateTime(const UDate dateTime, const DateTimeType& type, const DateFormat::EStyle& style) const { UnicodeString utf16Date; DateFormat* dateFormatter = CreateDateTimeInstance(type, style, currentLocale); dateFormatter->format(dateTime, utf16Date); char utf8Date[512]; CheckedArrayByteSink sink(utf8Date, ARRAY_SIZE(utf8Date)); utf16Date.toUTF8(sink); ENSURE(!sink.Overflowed()); delete dateFormatter; return std::string(utf8Date, sink.NumberOfBytesWritten()); } std::string L10n::FormatMillisecondsIntoDateString(const UDate milliseconds, const std::string& formatString, bool useLocalTimezone) const { UErrorCode status = U_ZERO_ERROR; UnicodeString dateString; std::string resultString; UnicodeString unicodeFormat = UnicodeString::fromUTF8(formatString.c_str()); SimpleDateFormat* dateFormat = new SimpleDateFormat(unicodeFormat, status); if (U_FAILURE(status)) LOGERROR("Error creating SimpleDateFormat: %s", u_errorName(status)); const TimeZone* timeZone = useLocalTimezone ? TimeZone::createDefault() : TimeZone::getGMT() ; status = U_ZERO_ERROR; Calendar* calendar = Calendar::createInstance(*timeZone, currentLocale, status); if (U_FAILURE(status)) LOGERROR("Error creating calendar: %s", u_errorName(status)); dateFormat->adoptCalendar(calendar); dateFormat->format(milliseconds, dateString); delete dateFormat; dateString.toUTF8String(resultString); return resultString; } std::string L10n::FormatDecimalNumberIntoString(double number) const { UErrorCode success = U_ZERO_ERROR; UnicodeString utf16Number; NumberFormat* numberFormatter = NumberFormat::createInstance(currentLocale, UNUM_DECIMAL, success); numberFormatter->format(number, utf16Number); char utf8Number[512]; CheckedArrayByteSink sink(utf8Number, ARRAY_SIZE(utf8Number)); utf16Number.toUTF8(sink); ENSURE(!sink.Overflowed()); return std::string(utf8Number, sink.NumberOfBytesWritten()); } VfsPath L10n::LocalizePath(const VfsPath& sourcePath) const { VfsPath localizedPath = sourcePath.Parent() / L"l10n" / wstring_from_utf8(currentLocale.getLanguage()) / sourcePath.Filename(); if (!VfsFileExists(localizedPath)) return sourcePath; return localizedPath; } Status L10n::ReloadChangedFile(const VfsPath& path) { if (!boost::algorithm::starts_with(path.string(), L"l10n/")) return INFO::OK; if (path.Extension() != L".po") return INFO::OK; // If the file was deleted, ignore it if (!VfsFileExists(path)) return INFO::OK; std::wstring dictName = GetFallbackToAvailableDictLocale(currentLocale); if (useLongStrings) dictName = L"long"; if (dictName.empty()) return INFO::OK; // Only the currently used language is loaded, so ignore all others if (path.string().rfind(dictName) == std::string::npos) return INFO::OK; LOGMESSAGE("Hotloading translations from '%s'", path.string8()); CVFSFile file; if (file.Load(g_VFS, path) != PSRETURN_OK) { LOGERROR("Failed to read translations from '%s'", path.string8()); return ERR::FAIL; } std::string content = file.DecodeUTF8(); ReadPoIntoDictionary(content, dictionary); if (g_GUI) g_GUI->ReloadAllPages(); return INFO::OK; } void L10n::LoadDictionaryForCurrentLocale() { delete dictionary; dictionary = new tinygettext::Dictionary(); VfsPaths filenames; if (useLongStrings) { if (vfs::GetPathnames(g_VFS, L"l10n/", L"long.*.po", filenames) < 0) return; } else { std::wstring dictName = GetFallbackToAvailableDictLocale(currentLocale); if (vfs::GetPathnames(g_VFS, L"l10n/", dictName.append(L".*.po").c_str(), filenames) < 0) { LOGERROR("No files for the dictionary found, but at this point the input should already be validated!"); return; } } for (const VfsPath& path : filenames) { CVFSFile file; file.Load(g_VFS, path); std::string content = file.DecodeUTF8(); ReadPoIntoDictionary(content, dictionary); } } void L10n::LoadListOfAvailableLocales() { for (Locale* const& locale : availableLocales) delete locale; availableLocales.clear(); Locale* defaultLocale = new Locale(Locale::getUS()); availableLocales.push_back(defaultLocale); // Always available. VfsPaths filenames; if (vfs::GetPathnames(g_VFS, L"l10n/", L"*.po", filenames) < 0) return; for (const VfsPath& path : filenames) { // Note: PO files follow this naming convention: “l10n/..po”. For example: “l10n/gl.public.po”. std::string filename = utf8_from_wstring(path.string()).substr(strlen("l10n/")); size_t lengthToFirstDot = filename.find('.'); std::string localeCode = filename.substr(0, lengthToFirstDot); Locale* locale = new Locale(Locale::createCanonical(localeCode.c_str())); auto it = std::find_if(availableLocales.begin(), availableLocales.end(), [&locale](Locale* const& l) { return *locale == *l; }); if (it != availableLocales.end()) { delete locale; continue; } availableLocales.push_back(locale); } } void L10n::ReadPoIntoDictionary(const std::string& poContent, tinygettext::Dictionary* dictionary) const { try { std::istringstream inputStream(poContent); tinygettext::POParser::parse("virtual PO file", inputStream, *dictionary); } catch(std::exception& e) { LOGERROR("[Localization] Exception while reading virtual PO file: %s", e.what()); } } DateFormat* L10n::CreateDateTimeInstance(const L10n::DateTimeType& type, const DateFormat::EStyle& style, const Locale& locale) const { switch(type) { case Date: return SimpleDateFormat::createDateInstance(style, locale); case Time: return SimpleDateFormat::createTimeInstance(style, locale); case DateTime: default: return SimpleDateFormat::createDateTimeInstance(style, style, locale); } } Index: ps/trunk/source/i18n/L10n.h =================================================================== --- ps/trunk/source/i18n/L10n.h (revision 19502) +++ ps/trunk/source/i18n/L10n.h (revision 19503) @@ -1,593 +1,593 @@ -/* Copyright (c) 2017 Wildfire Games +/* Copyright (C) 2017 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 L10N_H #define L10N_H #include #include #include "lib/code_annotation.h" #include "lib/external_libraries/icu.h" #include "lib/external_libraries/tinygettext.h" #include "lib/file/vfs/vfs_path.h" #include "ps/Singleton.h" #define g_L10n L10n::GetSingleton() /** * %Singleton for internationalization and localization. * * @sa http://trac.wildfiregames.com/wiki/Internationalization_and_Localization */ class L10n : public Singleton { /** * Marks the L10n class as ‘noncopyable’. * * This is required, as the class works as a singleton. * * @sa #NONCOPYABLE(className) */ NONCOPYABLE(L10n); public: /** * Creates an instance of L10n. * * L10n is a singleton. Use Instance() instead of creating you own instances * of L10n. */ L10n(); /** * Handles the descruction of L10n. * * Never destroy the L10n singleton manually. It must run as long as the * game runs, and it is destroyed automatically when you quit the game. */ ~L10n(); /** * Types of dates. * * @sa LocalizeDateTime() */ enum DateTimeType { DateTime, ///< Both date and time. Date, ///< Only date. Time ///< Only time. }; /** * Returns the current locale. * * @sa GetCurrentLocaleString() * @sa GetSupportedLocaleBaseNames() * @sa GetAllLocales() * @sa ReevaluateCurrentLocaleAndReload() */ Locale GetCurrentLocale() const; /** * Returns the code of the current locale. * * A locale code is a string such as "de" or "pt_BR". * * @sa GetCurrentLocale() * @sa GetSupportedLocaleBaseNames() * @sa GetAllLocales() * @sa ReevaluateCurrentLocaleAndReload() */ std::string GetCurrentLocaleString() const; /** * Returns a vector of locale codes supported by ICU. * * A locale code is a string such as "de" or "pt_BR". * * @return Vector of supported locale codes. * * @sa GetSupportedLocaleBaseNames() * @sa GetCurrentLocale() * * @sa http://www.icu-project.org/apiref/icu4c/classicu_1_1Locale.html#a073d70df8c9c8d119c0d42d70de24137 */ std::vector GetAllLocales() const; /** * Saves the specified locale in the game configuration file. * * The next time that the game starts, the game uses the locale in the * configuration file if there are translation files available for it. * * SaveLocale() checks the validity of the specified locale with * ValidateLocale(). If the specified locale is not valid, SaveLocale() * returns @c false and does not save the locale to the configuration file. * * @param localeCode Locale to save to the configuration file. * @return Whether the specified locale is valid (@c true) or not * (@c false). */ bool SaveLocale(const std::string& localeCode) const; ///@overload SaveLocale bool SaveLocale(const Locale& locale) const; /** * Returns an array of supported locale codes sorted alphabetically. * * A locale code is a string such as "de" or "pt_BR". * * If yours is a development copy (the ‘config/dev.cfg’ file is found in the * virtual filesystem), the output array may include the special “long” * locale code. * * @return Array of supported locale codes. * * @sa GetSupportedLocaleDisplayNames() * @sa GetAllLocales() * @sa GetCurrentLocale() * * @sa http://trac.wildfiregames.com/wiki/Implementation_of_Internationalization_and_Localization#LongStringsLocale */ std::vector GetSupportedLocaleBaseNames() const; /** * Returns an array of supported locale names sorted alphabetically by * locale code. * * A locale code is a string such as "de" or "pt_BR". * * If yours is a development copy (the ‘config/dev.cfg’ file is found in the * virtual filesystem), the output array may include the special “Long * Strings” locale name. * * @return Array of supported locale codes. * * @sa GetSupportedLocaleBaseNames() * * @sa http://trac.wildfiregames.com/wiki/Implementation_of_Internationalization_and_Localization#LongStringsLocale */ std::vector GetSupportedLocaleDisplayNames() const; /** * Returns the ISO-639 language code of the specified locale code. * * For example, if you specify the ‘en_US’ locate, it returns ‘en’. * * @param locale Locale code. * @return Language code. * * @sa http://www.icu-project.org/apiref/icu4c/classicu_1_1Locale.html#af36d821adced72a870d921ebadd0ca93 */ std::string GetLocaleLanguage(const std::string& locale) const; /** * Returns the programmatic code of the entire locale without keywords. * * @param locale Locale code. * @return Locale code without keywords. * * @sa http://www.icu-project.org/apiref/icu4c/classicu_1_1Locale.html#a4c1acbbdf95dc15599db5f322fa4c4d0 */ std::string GetLocaleBaseName(const std::string& locale) const; /** * Returns the ISO-3166 country code of the specified locale code. * * For example, if you specify the ‘en_US’ locate, it returns ‘US’. * * @param locale Locale code. * @return Country code. * * @sa http://www.icu-project.org/apiref/icu4c/classicu_1_1Locale.html#ae3f1fc415c00d4f0ab33288ceadccbf9 */ std::string GetLocaleCountry(const std::string& locale) const; /** * Returns the ISO-15924 abbreviation script code of the specified locale code. * * @param locale Locale code. * @return Script code. * * @sa http://www.icu-project.org/apiref/icu4c/classicu_1_1Locale.html#a5e0145a339d30794178a1412dcc55abe */ std::string GetLocaleScript(const std::string& locale) const; /** * Returns @c true if the current locale is the special “Long Strings” * locale. It returns @c false otherwise. * * @return Whether the current locale is the special “Long Strings” * (@c true) or not (@c false). */ bool UseLongStrings() const; /** * Returns an array of paths to files in the virtual filesystem that provide * translations for the specified locale code. * * @param locale Locale code. * @return Array of paths to files in the virtual filesystem that provide * translations for @p locale. */ std::vector GetDictionariesForLocale(const std::string& locale) const; std::wstring GetFallbackToAvailableDictLocale(const Locale& locale) const; std::wstring GetFallbackToAvailableDictLocale(const std::string& locale) const; /** * Returns the code of the recommended locale for the current user that the * game supports. * * “That the game supports” means both that a translation file is available * for that locale and that the locale code is either supported by ICU or * the special “long” locale code. * * The mechanism to select a recommended locale follows this logic: * 1. First see if the game supports the specified locale,\n * @p configLocale. * 2. Otherwise, check the system locale and see if the game supports\n * that locale. * 3. Else, return the default locale, ‘en_US’. * * @param configLocaleString Locale to check for support first. Pass an * empty string to check the system locale directly. * @return Code of a locale that the game supports. * * @sa http://trac.wildfiregames.com/wiki/Implementation_of_Internationalization_and_Localization#LongStringsLocale */ std::string GetDictionaryLocale(const std::string& configLocaleString) const; /** * Saves an instance of the recommended locale for the current user that the * game supports in the specified variable. * * “That the game supports” means both that a translation file is available * for that locale and that the locale code is either supported by ICU or * the special “long” locale code. * * The mechanism to select a recommended locale follows this logic: * 1. First see if the game supports the specified locale,\n * @p configLocale. * 2. Otherwise, check the system locale and see if the game supports\n * that locale. * 3. Else, return the default locale, ‘en_US’. * * @param configLocaleString Locale to check for support first. Pass an * empty string to check the system locale directly. * @param outLocale The recommended locale. * * @sa http://trac.wildfiregames.com/wiki/Implementation_of_Internationalization_and_Localization#LongStringsLocale */ void GetDictionaryLocale(const std::string& configLocaleString, Locale& outLocale) const; /** * Determines the best, supported locale for the current user, makes it the * current game locale and reloads the translations dictionary with * translations for that locale. * * To determine the best locale, ReevaluateCurrentLocaleAndReload(): * 1. Checks the user game configuration. * 2. If the locale is not defined there, it checks the system locale. * 3. If none of those locales are supported by the game, the default\n * locale, ‘en_US’, is used. * * @sa GetCurrentLocale() */ void ReevaluateCurrentLocaleAndReload(); /** * Returns @c true if the locale is supported by both ICU and the game. It * returns @c false otherwise. * * It returns @c true if both of these conditions are true: * 1. ICU has resources for that locale (which also ensures it’s a valid\n * locale string). * 2. Either a dictionary for language_country or for language is\n * available. * * @param locale Locale to check. * @return Whether @p locale is supported by both ICU and the game (@c true) * or not (@c false). */ bool ValidateLocale(const Locale& locale) const; ///@overload ValidateLocale bool ValidateLocale(const std::string& localeCode) const; /** * Returns the translation of the specified string to the * @link L10n::GetCurrentLocale() current locale@endlink. * * @param sourceString String to translate to the current locale. * @return Translation of @p sourceString to the current locale, or * @p sourceString if there is no translation available. */ std::string Translate(const std::string& sourceString) const; /** * Returns the translation of the specified string to the * @link L10n::GetCurrentLocale() current locale@endlink in the specified * context. * * @param context Context where the string is used. See * http://www.gnu.org/software/gettext/manual/html_node/Contexts.html * @param sourceString String to translate to the current locale. * @return Translation of @p sourceString to the current locale in the * specified @p context, or @p sourceString if there is no * translation available. */ std::string TranslateWithContext(const std::string& context, const std::string& sourceString) const; /** * Returns the translation of the specified string to the * @link L10n::GetCurrentLocale() current locale@endlink based on the * specified number. * * @param singularSourceString String to translate to the current locale, * in English’ singular form. * @param pluralSourceString String to translate to the current locale, in * English’ plural form. * @param number Number that determines the required form of the translation * (or the English string if no translation is available). * @return Translation of the source string to the current locale for the * specified @p number, or either @p singularSourceString (if * @p number is 1) or @p pluralSourceString (if @p number is not 1) * if there is no translation available. */ std::string TranslatePlural(const std::string& singularSourceString, const std::string& pluralSourceString, int number) const; /** * Returns the translation of the specified string to the * @link L10n::GetCurrentLocale() current locale@endlink in the specified * context, based on the specified number. * * @param context Context where the string is used. See * http://www.gnu.org/software/gettext/manual/html_node/Contexts.html * @param singularSourceString String to translate to the current locale, * in English’ singular form. * @param pluralSourceString String to translate to the current locale, in * English’ plural form. * @param number Number that determines the required form of the translation * (or the English string if no translation is available). * * @return Translation of the source string to the current locale in the * specified @p context and for the specified @p number, or either * @p singularSourceString (if @p number is 1) or * @p pluralSourceString (if @p number is not 1) if there is no * translation available. */ std::string TranslatePluralWithContext(const std::string& context, const std::string& singularSourceString, const std::string& pluralSourceString, int number) const; /** * Translates a text line by line to the * @link L10n::GetCurrentLocale() current locale@endlink. * * TranslateLines() is helpful when you need to translate a plain text file * after you load it. * * @param sourceString Text to translate to the current locale. * @return Line by line translation of @p sourceString to the current * locale. Some of the lines in the returned text may be in English * because there was not translation available for them. */ std::string TranslateLines(const std::string& sourceString) const; /** * Parses the date in the input string using the specified date format, and * returns the parsed date as a UNIX timestamp in milliseconds (not * seconds). * * @param dateTimeString String containing the date to parse. * @param dateTimeFormat Date format string to parse the input date, defined * using ICU date formatting symbols. * @param locale Locale to use when parsing the input date. * @return Specified date as a UNIX timestamp in milliseconds (not seconds). * * @sa GetCurrentLocale() * * @sa http://en.wikipedia.org/wiki/Unix_time * @sa https://sites.google.com/site/icuprojectuserguide/formatparse/datetime?pli=1#TOC-Date-Field-Symbol-Table */ UDate ParseDateTime(const std::string& dateTimeString, const std::string& dateTimeFormat, const Locale& locale) const; /** * Returns the specified date using the specified date format. * * @param dateTime Date specified as a UNIX timestamp in milliseconds * (not seconds). * @param type Whether the formatted date must show both the date and the * time, only the date or only the time. * @param style ICU style for the formatted date. * @return String containing the specified date with the specified date * format. * * @sa http://en.wikipedia.org/wiki/Unix_time * @sa http://icu-project.org/apiref/icu4c521/classicu_1_1DateFormat.html */ std::string LocalizeDateTime(const UDate dateTime, const DateTimeType& type, const DateFormat::EStyle& style) const; /** * Returns the specified date using the specified date format. * * @param milliseconds Date specified as a UNIX timestamp in milliseconds * (not seconds). * @param formatString Date format string defined using ICU date formatting * symbols. Usually, you internationalize the format string and * get it translated before you pass it to * FormatMillisecondsIntoDateString(). * @param useLocalTimezone Boolean useful for durations * @return String containing the specified date with the specified date * format. * * @sa http://en.wikipedia.org/wiki/Unix_time * @sa https://sites.google.com/site/icuprojectuserguide/formatparse/datetime?pli=1#TOC-Date-Field-Symbol-Table */ std::string FormatMillisecondsIntoDateString(const UDate milliseconds, const std::string& formatString, bool useLocalTimezone) const; /** * Returns the specified floating-point number as a string, with the number * formatted as a decimal number using the * @link L10n::GetCurrentLocale() current locale@endlink. * * @param number Number to format. * @return Decimal number formatted using the current locale. */ std::string FormatDecimalNumberIntoString(double number) const; /** * Returns the localized version of the specified path if there is one for * the @link L10n::GetCurrentLocale() current locale@endlink. * * If there is no localized version of the specified path, it returns the * specified path. * * For example, if the code of the current locale is ‘de_DE’, LocalizePath() * splits the input path into folder path and file name, and checks whether * the ‘/l10n/de/’ file exists. If it does, it returns that * path. Otherwise, it returns the input path, verbatim. * * This function is used for file localization (for example, image * localization). * * @param sourcePath %Path to localize. * @return Localized path if it exists, @c sourcePath otherwise. * * @sa http://trac.wildfiregames.com/wiki/Localization#LocalizingImages */ VfsPath LocalizePath(const VfsPath& sourcePath) const; /** * Loads @p path into the dictionary if it is a translation file of the * @link L10n::GetCurrentLocale() current locale@endlink. */ Status ReloadChangedFile(const VfsPath& path); private: /** * Dictionary that contains the translations for the * @link L10n::GetCurrentLocale() current locale@endlink and the matching * English strings, including contexts and plural forms. * * @sa LoadDictionaryForCurrentLocale() */ tinygettext::Dictionary* dictionary; /** * Locale that the game is currently using. * * To get the current locale, use its getter: GetCurrentLocale(). You can * also use GetCurrentLocaleString() to get the locale code of the current * locale. * * To change the value of this variable: * 1. Save a new locale to the game configuration file with SaveLocale(). * 2. Reload the translation dictionary with\n * ReevaluateCurrentLocaleAndReload(). */ Locale currentLocale; /** * Vector with the locales that the game supports. * * The list of available locales is calculated when the game starts. Call * LoadListOfAvailableLocales() to refresh the list. * * @sa GetSupportedLocaleBaseNames() * @sa GetSupportedLocaleDisplayNames() */ std::vector availableLocales; /** * Whether the game is using the default game locale (@c true), ‘en_US’, or * not (@c false). * * This variable is used in the L10n implementation for performance reasons. * Many localization steps can be skipped when this variable is @c true. */ bool currentLocaleIsOriginalGameLocale; /** * Whether the game is using the special game locale with the longest * strings of each translation (@c true) or not (@c false). * * @sa http://trac.wildfiregames.com/wiki/Implementation_of_Internationalization_and_Localization#LongStringsLocale */ bool useLongStrings; /** * Loads the translation files for the * @link L10n::GetCurrentLocale() current locale@endlink. * * This method loads every file in the ‘l10n’ folder of the game virtual * filesystem that is prefixed with the code of the current locale followed * by a dot. * * For example, if the code of the current locale code is ‘de’, * LoadDictionaryForCurrentLocale() loads the ‘l10n/de.engine.po’ and * ‘l10n/de.public.po’ translation files. * * @sa dictionary * @sa ReadPoIntoDictionary() */ void LoadDictionaryForCurrentLocale(); /** * Determines the list of locales that the game supports. * * LoadListOfAvailableLocales() checks the locale codes of the translation * files in the ‘l10n’ folder of the virtual filesystem. If it finds a * translation file prefixed with a locale code followed by a dot, it * determines that the game supports that locale. * * @sa availableLocales */ void LoadListOfAvailableLocales(); /** * Loads the specified content of a PO file into the specified dictionary. * * Used by LoadDictionaryForCurrentLocale() to add entries to the game * translations @link dictionary. * * @param poContent Content of a PO file as a string. * @param dictionary Dictionary where the entries from the PO file should be * stored. */ void ReadPoIntoDictionary(const std::string& poContent, tinygettext::Dictionary* dictionary) const; /** * Creates an ICU date formatted with the specified settings. * * @param type Whether formatted dates must show both the date and the time, * only the date or only the time. * @param style ICU style to format dates by default. * @param locale Locale that the date formatter should use to parse strings. * It has no relevance for date formatting, only matters for date * parsing. * @return ICU date formatter. */ DateFormat* CreateDateTimeInstance(const DateTimeType& type, const DateFormat::EStyle& style, const Locale& locale) const; }; #endif // L10N_H Index: ps/trunk/source/lib/input.cpp =================================================================== --- ps/trunk/source/lib/input.cpp (revision 19502) +++ ps/trunk/source/lib/input.cpp (revision 19503) @@ -1,93 +1,93 @@ -/* Copyright (c) 2012 Wildfire Games +/* Copyright (C) 2017 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. */ /* * SDL input redirector; dispatches to multiple handlers. */ #include "precompiled.h" #include "input.h" #include #include #include "lib/external_libraries/libsdl.h" const size_t MAX_HANDLERS = 9; static InHandler handler_stack[MAX_HANDLERS]; static size_t handler_stack_top = 0; static std::list priority_events; void in_add_handler(InHandler handler) { ENSURE(handler); if(handler_stack_top >= MAX_HANDLERS) WARN_IF_ERR(ERR::LIMIT); handler_stack[handler_stack_top++] = handler; } void in_reset_handlers() { handler_stack_top = 0; } // send ev to each handler until one returns IN_HANDLED void in_dispatch_event(const SDL_Event_* ev) { for(int i = (int)handler_stack_top-1; i >= 0; i--) { ENSURE(handler_stack[i] && ev); InReaction ret = handler_stack[i](ev); // .. done, return if(ret == IN_HANDLED) return; // .. next handler else if(ret == IN_PASS) continue; // .. invalid return value else DEBUG_WARN_ERR(ERR::LOGIC); // invalid handler return value } } void in_push_priority_event(const SDL_Event_* event) { priority_events.push_back(*event); } int in_poll_priority_event(SDL_Event_* event) { if (priority_events.empty()) return 0; *event = priority_events.front(); priority_events.pop_front(); return 1; } int in_poll_event(SDL_Event_* event) { return in_poll_priority_event(event) ? 1 : SDL_PollEvent(&event->ev); } Index: ps/trunk/source/lib/input.h =================================================================== --- ps/trunk/source/lib/input.h (revision 19502) +++ ps/trunk/source/lib/input.h (revision 19503) @@ -1,71 +1,71 @@ -/* Copyright (c) 2012 Wildfire Games +/* Copyright (C) 2017 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. */ /* * SDL input redirector; dispatches to multiple handlers. */ #ifndef INCLUDED_INPUT #define INCLUDED_INPUT #include "lib/external_libraries/libsdl_fwd.h" // input handler return values. enum InReaction { // (the handlers' return values are checked and these // 'strange' values might bring errors to light) // pass the event to the next handler in the chain IN_PASS = 4, // we've handled it; no other handlers will receive this event. IN_HANDLED = 2 }; typedef InReaction (*InHandler)(const SDL_Event_*); // register an input handler, which will receive all subsequent events first. // events are passed to other handlers if handler returns IN_PASS. extern void in_add_handler(InHandler handler); // remove all registered input handlers extern void in_reset_handlers(); // send event to each handler (newest first) until one returns true extern void in_dispatch_event(const SDL_Event_* event); // push an event onto the back of a high-priority queue - the new event will // be returned by in_poll_event before any standard SDL events extern void in_push_priority_event(const SDL_Event_* event); // reads events that were pushed by in_push_priority_event // returns 1 if an event was read, 0 otherwise. extern int in_poll_priority_event(SDL_Event_* event); // reads events that were pushed by in_push_priority_event, or, if there are // no high-priority events) reads from the SDL event queue with SDL_PollEvent. // returns 1 if an event was read, 0 otherwise. extern int in_poll_event(SDL_Event_* event); #endif // #ifndef INCLUDED_INPUT Index: ps/trunk/source/lib/ogl.cpp =================================================================== --- ps/trunk/source/lib/ogl.cpp (revision 19502) +++ ps/trunk/source/lib/ogl.cpp (revision 19503) @@ -1,495 +1,495 @@ -/* Copyright (c) 2013 Wildfire Games +/* Copyright (C) 2017 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. */ /* * OpenGL helper functions. */ #include "precompiled.h" #include "lib/ogl.h" #include #include #include #include "lib/external_libraries/libsdl.h" #include "lib/debug.h" #include "lib/sysdep/gfx.h" #include "lib/res/h_mgr.h" #if MSC_VERSION #pragma comment(lib, "opengl32.lib") #endif //---------------------------------------------------------------------------- // extensions //---------------------------------------------------------------------------- // define extension function pointers extern "C" { #define FUNC(ret, name, params) ret (GL_CALL_CONV *p##name) params; #define FUNC2(ret, nameARB, nameCore, version, params) ret (GL_CALL_CONV *p##nameARB) params; #define FUNC3(ret, nameARB, nameCore, version, params) ret (GL_CALL_CONV *p##nameCore) params; #include "lib/external_libraries/glext_funcs.h" #undef FUNC3 #undef FUNC2 #undef FUNC } static const char* exts = NULL; static bool have_30, have_21, have_20, have_15, have_14, have_13, have_12; // return a C string of unspecified length containing a space-separated // list of all extensions the OpenGL implementation advertises. // (useful for crash logs). const char* ogl_ExtensionString() { ENSURE(exts && "call ogl_Init before using this function"); return exts; } // paranoia: newer drivers may forget to advertise an extension // indicating support for something that has been folded into the core. // we therefore check for all extensions known to be offered by the // GL implementation present on the user's system; ogl_HaveExtension will // take this into account. // the app can therefore just ask for extensions and not worry about this. static bool isImplementedInCore(const char* ext) { #define MATCH(known_ext)\ if(!strcmp(ext, #known_ext))\ return true; if(have_30) { MATCH(GL_EXT_gpu_shader4); MATCH(GL_NV_conditional_render); MATCH(GL_ARB_color_buffer_float); MATCH(GL_ARB_depth_buffer_float); MATCH(GL_ARB_texture_float); MATCH(GL_EXT_packed_float); MATCH(GL_EXT_texture_shared_exponent); MATCH(GL_EXT_framebuffer_object); MATCH(GL_NV_half_float); MATCH(GL_ARB_half_float_pixel); MATCH(GL_EXT_framebuffer_multisample); MATCH(GL_EXT_framebuffer_blit); MATCH(GL_EXT_texture_integer); MATCH(GL_EXT_texture_array); MATCH(GL_EXT_packed_depth_stencil); MATCH(GL_EXT_draw_buffers2); MATCH(GL_EXT_texture_compression_rgtc); MATCH(GL_EXT_transform_feedback); MATCH(GL_APPLE_vertex_array_object); MATCH(GL_EXT_framebuffer_sRGB); } if(have_21) { MATCH(GL_ARB_pixel_buffer_object); MATCH(GL_EXT_texture_sRGB); } if(have_20) { MATCH(GL_ARB_shader_objects); MATCH(GL_ARB_vertex_shader); MATCH(GL_ARB_fragment_shader); MATCH(GL_ARB_shading_language_100); MATCH(GL_ARB_draw_buffers); MATCH(GL_ARB_texture_non_power_of_two); MATCH(GL_ARB_point_sprite); MATCH(GL_EXT_blend_equation_separate); } if(have_15) { MATCH(GL_ARB_vertex_buffer_object); MATCH(GL_ARB_occlusion_query); MATCH(GL_EXT_shadow_funcs); } if(have_14) { MATCH(GL_SGIS_generate_mipmap); MATCH(GL_NV_blend_square); MATCH(GL_ARB_depth_texture); MATCH(GL_ARB_shadow); MATCH(GL_EXT_fog_coord); MATCH(GL_EXT_multi_draw_arrays); MATCH(GL_ARB_point_parameters); MATCH(GL_EXT_secondary_color); MATCH(GL_EXT_blend_func_separate); MATCH(GL_EXT_stencil_wrap); MATCH(GL_ARB_texture_env_crossbar); MATCH(GL_EXT_texture_lod_bias); MATCH(GL_ARB_texture_mirrored_repeat); MATCH(GL_ARB_window_pos); // These extensions were added to GL 1.2, but as part of the optional // imaging subset; they're only guaranteed as of GL 1.4: MATCH(GL_EXT_blend_color); MATCH(GL_EXT_blend_minmax); MATCH(GL_EXT_blend_subtract); } if(have_13) { MATCH(GL_ARB_texture_compression); MATCH(GL_ARB_texture_cube_map); MATCH(GL_ARB_multisample); MATCH(GL_ARB_multitexture); MATCH(GL_ARB_transpose_matrix); MATCH(GL_ARB_texture_env_add); MATCH(GL_ARB_texture_env_combine); MATCH(GL_ARB_texture_env_dot3); MATCH(GL_ARB_texture_border_clamp); } if(have_12) { MATCH(GL_EXT_texture3D); MATCH(GL_EXT_bgra); MATCH(GL_EXT_packed_pixels); MATCH(GL_EXT_rescale_normal); MATCH(GL_EXT_separate_specular_color); MATCH(GL_SGIS_texture_edge_clamp); MATCH(GL_SGIS_texture_lod); MATCH(GL_EXT_draw_range_elements); // Skip the extensions that only affect the imaging subset } #undef MATCH return false; } // check if the extension is supported by the OpenGL implementation. // takes subsequently added core support for some extensions into account. bool ogl_HaveExtension(const char* ext) { ENSURE(exts && "call ogl_Init before using this function"); if(isImplementedInCore(ext)) return true; const char *p = exts, *end; // make sure ext is valid & doesn't contain spaces if(!ext || ext[0] == '\0' || strchr(ext, ' ')) return false; for(;;) { p = strstr(p, ext); if(!p) return false; // string not found - extension not supported end = p + strlen(ext); // end of current substring // make sure the substring found is an entire extension string, // i.e. it starts and ends with ' ' if((p == exts || p[-1] == ' ') && // valid start AND (*end == ' ' || *end == '\0')) // valid end return true; p = end; } } // check if the OpenGL implementation is at least at . // (format: "%d.%d" major minor) bool ogl_HaveVersion(const char* desired_version) { int desired_major, desired_minor; if(sscanf_s(desired_version, "%d.%d", &desired_major, &desired_minor) != 2) { DEBUG_WARN_ERR(ERR::LOGIC); // invalid version string return false; } // guaranteed to be of the form "major.minor[.release][ vendor-specific]" // or "OpenGL ES major.minor[.release][ vendor-specific]". // we won't distinguish GLES 2.0 from GL 2.0, but that's okay since // they're close enough. const char* version = (const char*)glGetString(GL_VERSION); int major, minor; if(!version || (sscanf_s(version, "%d.%d", &major, &minor) != 2 && sscanf_s(version, "OpenGL ES %d.%d", &major, &minor) != 2)) { DEBUG_WARN_ERR(ERR::LOGIC); // GL_VERSION invalid return false; } // note: don't just compare characters - major and minor may be >= 10. return (major > desired_major) || (major == desired_major && minor >= desired_minor); } // check if all given extension strings (passed as const char* parameters, // terminated by a 0 pointer) are supported by the OpenGL implementation, // as determined by ogl_HaveExtension. // returns 0 if all are present; otherwise, the first extension in the // list that's not supported (useful for reporting errors). // // note: dummy parameter is necessary to access parameter va_list. // // // rationale: this interface is more convenient than individual // ogl_HaveExtension calls and allows reporting which extension is missing. // // one disadvantage is that there is no way to indicate that either one // of 2 extensions would be acceptable, e.g. (ARB|EXT)_texture_env_dot3. // this is isn't so bad, since they wouldn't be named differently // if there weren't non-trivial changes between them. for that reason, // we refrain from equivalence checks (which would boil down to // string-matching known extensions to their equivalents). const char* ogl_HaveExtensions(int dummy, ...) { const char* ext; va_list args; va_start(args, dummy); for(;;) { ext = va_arg(args, const char*); // end of list reached; all were present => return 0. if(!ext) break; // not found => return name of missing extension. if(!ogl_HaveExtension(ext)) break; } va_end(args); return ext; } // to help when running with no hardware acceleration and only OpenGL 1.1 // (e.g. testing the game in virtual machines), we define dummy versions of // some extension functions which our graphics code assumes exist. // it will render incorrectly but at least it shouldn't crash. #if CONFIG2_GLES static void enableDummyFunctions() { } #else static void GL_CALL_CONV dummy_glDrawRangeElementsEXT(GLenum mode, GLuint, GLuint, GLsizei count, GLenum type, GLvoid* indices) { glDrawElements(mode, count, type, indices); } static void GL_CALL_CONV dummy_glActiveTextureARB(int) { } static void GL_CALL_CONV dummy_glClientActiveTextureARB(int) { } static void GL_CALL_CONV dummy_glMultiTexCoord2fARB(int, float s, float t) { glTexCoord2f(s, t); } static void GL_CALL_CONV dummy_glMultiTexCoord3fARB(int, float s, float t, float r) { glTexCoord3f(s, t, r); } static void enableDummyFunctions() { // fall back to the dummy functions when extensions (or equivalent core support) are missing if(!ogl_HaveExtension("GL_EXT_draw_range_elements")) { pglDrawRangeElementsEXT = &dummy_glDrawRangeElementsEXT; } if(!ogl_HaveExtension("GL_ARB_multitexture")) { pglActiveTextureARB = &dummy_glActiveTextureARB; pglClientActiveTextureARB = &dummy_glClientActiveTextureARB; pglMultiTexCoord2fARB = &dummy_glMultiTexCoord2fARB; pglMultiTexCoord3fARB = &dummy_glMultiTexCoord3fARB; } } #endif // #if CONFIG2_GLES static void importExtensionFunctions() { // It should be safe to load the ARB function pointers even if the // extension isn't advertised, since we won't actually use them without // checking for the extension. // (TODO: this calls ogl_HaveVersion far more times than is necessary - // we should probably use the have_* variables instead) // Note: the xorg-x11 implementation of glXGetProcAddress doesn't return NULL // if the function is unsupported (i.e. the rare case of a driver not reporting // its supported version correctly, see http://trac.wildfiregames.com/ticket/171) #define FUNC(ret, name, params) p##name = (ret (GL_CALL_CONV*) params)SDL_GL_GetProcAddress(#name); #define FUNC23(pname, ret, nameARB, nameCore, version, params) \ pname = NULL; \ if(ogl_HaveVersion(version)) \ pname = (ret (GL_CALL_CONV*) params)SDL_GL_GetProcAddress(#nameCore); \ if(!pname) /* use the ARB name if the driver lied about what version it supports */ \ pname = (ret (GL_CALL_CONV*) params)SDL_GL_GetProcAddress(#nameARB); #define FUNC2(ret, nameARB, nameCore, version, params) FUNC23(p##nameARB, ret, nameARB, nameCore, version, params) #define FUNC3(ret, nameARB, nameCore, version, params) FUNC23(p##nameCore, ret, nameARB, nameCore, version, params) #include "lib/external_libraries/glext_funcs.h" #undef FUNC3 #undef FUNC2 #undef FUNC23 #undef FUNC enableDummyFunctions(); } //---------------------------------------------------------------------------- const char* ogl_GetErrorName(GLenum err) { #define E(e) case e: return #e; switch (err) { E(GL_INVALID_ENUM) E(GL_INVALID_VALUE) E(GL_INVALID_OPERATION) #if !CONFIG2_GLES E(GL_STACK_OVERFLOW) E(GL_STACK_UNDERFLOW) #endif E(GL_OUT_OF_MEMORY) E(GL_INVALID_FRAMEBUFFER_OPERATION) default: return "Unknown GL error"; } #undef E } static void dump_gl_error(GLenum err) { debug_printf("OGL| %s (%04x)\n", ogl_GetErrorName(err), err); } void ogl_WarnIfErrorLoc(const char *file, int line) { // glGetError may return multiple errors, so we poll it in a loop. // the debug_printf should only happen once (if this is set), though. bool error_enountered = false; GLenum first_error = 0; for(;;) { GLenum err = glGetError(); if(err == GL_NO_ERROR) break; if(!error_enountered) first_error = err; error_enountered = true; dump_gl_error(err); } if(error_enountered) debug_printf("%s:%d: OpenGL error(s) occurred: %s (%04x)\n", file, line, ogl_GetErrorName(first_error), (unsigned int)first_error); } // ignore and reset the specified error (as returned by glGetError). // any other errors that have occurred are reported as ogl_WarnIfError would. // // this is useful for suppressing annoying error messages, e.g. // "invalid enum" for GL_CLAMP_TO_EDGE even though we've already // warned the user that their OpenGL implementation is too old. bool ogl_SquelchError(GLenum err_to_ignore) { // glGetError may return multiple errors, so we poll it in a loop. // the debug_printf should only happen once (if this is set), though. bool error_enountered = false; bool error_ignored = false; GLenum first_error = 0; for(;;) { GLenum err = glGetError(); if(err == GL_NO_ERROR) break; if(err == err_to_ignore) { error_ignored = true; continue; } if(!error_enountered) first_error = err; error_enountered = true; dump_gl_error(err); } if(error_enountered) debug_printf("OpenGL error(s) occurred: %04x\n", (unsigned int)first_error); return error_ignored; } //---------------------------------------------------------------------------- // feature and limit detect //---------------------------------------------------------------------------- GLint ogl_max_tex_size = -1; // [pixels] GLint ogl_max_tex_units = -1; // limit on GL_TEXTUREn // call after each video mode change, since thereafter extension functions // may have changed [address]. void ogl_Init() { // cache extension list and versions for oglHave*. // note: this is less about performance (since the above are not // time-critical) than centralizing the 'OpenGL is ready' check. exts = (const char*)glGetString(GL_EXTENSIONS); ENSURE(exts); // else: called before OpenGL is ready for use have_12 = ogl_HaveVersion("1.2"); have_13 = ogl_HaveVersion("1.3"); have_14 = ogl_HaveVersion("1.4"); have_15 = ogl_HaveVersion("1.5"); have_20 = ogl_HaveVersion("2.0"); have_21 = ogl_HaveVersion("2.1"); have_30 = ogl_HaveVersion("3.0"); importExtensionFunctions(); glGetIntegerv(GL_MAX_TEXTURE_SIZE, &ogl_max_tex_size); #if !CONFIG2_GLES glGetIntegerv(GL_MAX_TEXTURE_UNITS, &ogl_max_tex_units); #endif } Index: ps/trunk/source/lib/ogl.h =================================================================== --- ps/trunk/source/lib/ogl.h (revision 19502) +++ ps/trunk/source/lib/ogl.h (revision 19503) @@ -1,194 +1,194 @@ -/* Copyright (c) 2010 Wildfire Games +/* Copyright (C) 2017 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. */ /* * OpenGL helper functions. */ #ifndef INCLUDED_OGL #define INCLUDED_OGL #include "lib/external_libraries/opengl.h" /** * initialization: import extension function pointers and do feature detect. * call before using any other function, and after each video mode change. * fails if OpenGL not ready for use. **/ extern void ogl_Init(); //----------------------------------------------------------------------------- // extensions /** * check if an extension is supported by the OpenGL implementation. * * takes subsequently added core support for some extensions into account * (in case drivers forget to advertise extensions). * * @param ext extension string; exact case. * @return bool. **/ extern bool ogl_HaveExtension(const char* ext); /** * make sure the OpenGL implementation version matches or is newer than * the given version. * * @param version version string; format: ("%d.%d", major, minor). * example: "1.2". **/ extern bool ogl_HaveVersion(const char* version); /** * check if a list of extensions are all supported (as determined by * ogl_HaveExtension). * * @param dummy value ignored; varargs requires a placeholder. * follow it by a list of const char* extension string parameters, * terminated by a 0 pointer. * @return 0 if all are present; otherwise, the first extension in the * list that's not supported (useful for reporting errors). **/ extern const char* ogl_HaveExtensions(int dummy, ...) SENTINEL_ARG; /** * get a list of all supported extensions. * * useful for crash logs / system information. * * @return read-only C string of unspecified length containing all * advertised extension names, separated by space. **/ extern const char* ogl_ExtensionString(); // The game wants to use some extension constants that aren't provided by // glext.h on some old systems. // Manually define all the necessary ones that are missing from // GL_GLEXT_VERSION 39 (Mesa 7.0) since that's probably an old enough baseline: #ifndef GL_VERSION_3_0 # define GL_MIN_PROGRAM_TEXEL_OFFSET 0x8904 # define GL_MAX_PROGRAM_TEXEL_OFFSET 0x8905 #endif #ifndef GL_EXT_transform_feedback # define GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS_EXT 0x8C8A # define GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS_EXT 0x8C8B # define GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS_EXT 0x8C80 #endif #ifndef GL_ARB_geometry_shader4 # define GL_MAX_GEOMETRY_TEXTURE_IMAGE_UNITS_ARB 0x8C29 # define GL_MAX_GEOMETRY_VARYING_COMPONENTS_ARB 0x8DDD # define GL_MAX_VERTEX_VARYING_COMPONENTS_ARB 0x8DDE # define GL_MAX_GEOMETRY_UNIFORM_COMPONENTS_ARB 0x8DDF # define GL_MAX_GEOMETRY_OUTPUT_VERTICES_ARB 0x8DE0 # define GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS_ARB 0x8DE1 #endif #ifndef GL_ARB_timer_query # define GL_TIME_ELAPSED 0x88BF # define GL_TIMESTAMP 0x8E28 #endif #ifndef GL_ARB_framebuffer_object # define GL_INVALID_FRAMEBUFFER_OPERATION 0x0506 #endif // Also need some more for OS X 10.5: #ifndef GL_EXT_texture_array # define GL_MAX_ARRAY_TEXTURE_LAYERS_EXT 0x88FF #endif // Also need some types not in old glext.h: #ifndef GL_ARB_sync typedef int64_t GLint64; typedef uint64_t GLuint64; #endif // declare extension function pointers #if OS_WIN # define GL_CALL_CONV __stdcall #else # define GL_CALL_CONV #endif #define FUNC(ret, name, params) EXTERN_C ret (GL_CALL_CONV *p##name) params; #define FUNC2(ret, nameARB, nameCore, version, params) EXTERN_C ret (GL_CALL_CONV *p##nameARB) params; #define FUNC3(ret, nameARB, nameCore, version, params) EXTERN_C ret (GL_CALL_CONV *p##nameCore) params; #include "lib/external_libraries/glext_funcs.h" #undef FUNC3 #undef FUNC2 #undef FUNC // leave GL_CALL_CONV defined for ogl.cpp //----------------------------------------------------------------------------- // errors /** * raise a warning (break into the debugger) if an OpenGL error is pending. * resets the OpenGL error state afterwards. * * when an error is reported, insert calls to this in a binary-search scheme * to quickly narrow down the actual error location. * * reports a bogus invalid_operation error if called before OpenGL is * initialized, so don't! * * disabled in release mode for efficiency and to avoid annoying errors. **/ extern void ogl_WarnIfErrorLoc(const char *file, int line); #ifdef NDEBUG # define ogl_WarnIfError() #else # define ogl_WarnIfError() ogl_WarnIfErrorLoc(__FILE__, __LINE__) #endif /** * get a name of the error. * * useful for debug. * * @return read-only C string of unspecified length containing * the error's name. **/ extern const char* ogl_GetErrorName(GLenum err); /** * ignore and reset the specified OpenGL error. * * this is useful for suppressing annoying error messages, e.g. * "invalid enum" for GL_CLAMP_TO_EDGE even though we've already * warned the user that their OpenGL implementation is too old. * * call after the fact, i.e. the error has been raised. if another or * different error is pending, those are reported immediately. * * @param err_to_ignore: one of the glGetError enums. * @return true if the requested error was seen and ignored **/ extern bool ogl_SquelchError(GLenum err_to_ignore); //----------------------------------------------------------------------------- // implementation limits / feature detect extern GLint ogl_max_tex_size; /// [pixels] extern GLint ogl_max_tex_units; /// limit on GL_TEXTUREn #endif // #ifndef INCLUDED_OGL Index: ps/trunk/source/lib/tex/tex.cpp =================================================================== --- ps/trunk/source/lib/tex/tex.cpp (revision 19502) +++ ps/trunk/source/lib/tex/tex.cpp (revision 19503) @@ -1,777 +1,777 @@ -/* Copyright (c) 2017 Wildfire Games +/* Copyright (C) 2017 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. */ /* * support routines for 2d texture access/writing. */ #include "precompiled.h" #include "tex.h" #include #include #include #include "lib/timer.h" #include "lib/bits.h" #include "lib/allocators/shared_ptr.h" #include "lib/sysdep/cpu.h" #include "tex_codec.h" static const StatusDefinition texStatusDefinitions[] = { { ERR::TEX_FMT_INVALID, L"Invalid/unsupported texture format" }, { ERR::TEX_INVALID_COLOR_TYPE, L"Invalid color type" }, { ERR::TEX_NOT_8BIT_PRECISION, L"Not 8-bit channel precision" }, { ERR::TEX_INVALID_LAYOUT, L"Unsupported texel layout, e.g. right-to-left" }, { ERR::TEX_COMPRESSED, L"Unsupported texture compression" }, { WARN::TEX_INVALID_DATA, L"Warning: invalid texel data encountered" }, { ERR::TEX_INVALID_SIZE, L"Texture size is incorrect" }, { INFO::TEX_CODEC_CANNOT_HANDLE, L"Texture codec cannot handle the given format" } }; STATUS_ADD_DEFINITIONS(texStatusDefinitions); //----------------------------------------------------------------------------- // validation //----------------------------------------------------------------------------- // be careful not to use other tex_* APIs here because they call us. Status Tex::validate() const { if(m_Flags & TEX_UNDEFINED_FLAGS) WARN_RETURN(ERR::_1); // pixel data (only check validity if the image is still in memory; // ogl_tex frees the data after uploading to GL) if(m_Data) { // file size smaller than header+pixels. // possible causes: texture file header is invalid, // or file wasn't loaded completely. if(m_DataSize < m_Ofs + m_Width*m_Height*m_Bpp/8) WARN_RETURN(ERR::_2); } // bits per pixel // (we don't bother checking all values; a sanity check is enough) if(m_Bpp % 4 || m_Bpp > 32) WARN_RETURN(ERR::_3); // flags // .. DXT value const size_t dxt = m_Flags & TEX_DXT; if(dxt != 0 && dxt != 1 && dxt != DXT1A && dxt != 3 && dxt != 5) WARN_RETURN(ERR::_4); // .. orientation const size_t orientation = m_Flags & TEX_ORIENTATION; if(orientation == (TEX_BOTTOM_UP|TEX_TOP_DOWN)) WARN_RETURN(ERR::_5); return INFO::OK; } #define CHECK_TEX(t) RETURN_STATUS_IF_ERR((t->validate())) // check if the given texture format is acceptable: 8bpp grey, // 24bpp color or 32bpp color+alpha (BGR / upside down are permitted). // basically, this is the "plain" format understood by all codecs and // tex_codec_plain_transform. Status tex_validate_plain_format(size_t bpp, size_t flags) { const bool alpha = (flags & TEX_ALPHA ) != 0; const bool grey = (flags & TEX_GREY ) != 0; const bool dxt = (flags & TEX_DXT ) != 0; const bool mipmaps = (flags & TEX_MIPMAPS) != 0; if(dxt || mipmaps) WARN_RETURN(ERR::TEX_FMT_INVALID); // grey must be 8bpp without alpha, or it's invalid. if(grey) { if(bpp == 8 && !alpha) return INFO::OK; WARN_RETURN(ERR::TEX_FMT_INVALID); } if(bpp == 24 && !alpha) return INFO::OK; if(bpp == 32 && alpha) return INFO::OK; WARN_RETURN(ERR::TEX_FMT_INVALID); } //----------------------------------------------------------------------------- // mipmaps //----------------------------------------------------------------------------- void tex_util_foreach_mipmap(size_t w, size_t h, size_t bpp, const u8* pixels, int levels_to_skip, size_t data_padding, MipmapCB cb, void* RESTRICT cbData) { ENSURE(levels_to_skip >= 0 || levels_to_skip == TEX_BASE_LEVEL_ONLY); size_t level_w = w, level_h = h; const u8* level_data = pixels; // we iterate through the loop (necessary to skip over image data), // but do not actually call back until the requisite number of // levels have been skipped (i.e. level == 0). int level = (levels_to_skip == TEX_BASE_LEVEL_ONLY)? 0 : -levels_to_skip; // until at level 1x1: for(;;) { // used to skip past this mip level in const size_t level_dataSize = (size_t)(round_up(level_w, data_padding) * round_up(level_h, data_padding) * bpp/8); if(level >= 0) cb((size_t)level, level_w, level_h, level_data, level_dataSize, cbData); level_data += level_dataSize; // 1x1 reached - done if(level_w == 1 && level_h == 1) break; level_w /= 2; level_h /= 2; // if the texture is non-square, one of the dimensions will become // 0 before the other. to satisfy OpenGL's expectations, change it // back to 1. if(level_w == 0) level_w = 1; if(level_h == 0) level_h = 1; level++; // special case: no mipmaps, we were only supposed to call for // the base level if(levels_to_skip == TEX_BASE_LEVEL_ONLY) break; } } struct CreateLevelData { size_t num_components; size_t prev_level_w; size_t prev_level_h; const u8* prev_level_data; size_t prev_level_dataSize; }; // uses 2x2 box filter static void create_level(size_t level, size_t level_w, size_t level_h, const u8* RESTRICT level_data, size_t level_dataSize, void* RESTRICT cbData) { CreateLevelData* cld = (CreateLevelData*)cbData; const size_t src_w = cld->prev_level_w; const size_t src_h = cld->prev_level_h; const u8* src = cld->prev_level_data; u8* dst = (u8*)level_data; // base level - must be copied over from source buffer if(level == 0) { ENSURE(level_dataSize == cld->prev_level_dataSize); memcpy(dst, src, level_dataSize); } else { const size_t num_components = cld->num_components; const size_t dx = num_components, dy = dx*src_w; // special case: image is too small for 2x2 filter if(cld->prev_level_w == 1 || cld->prev_level_h == 1) { // image is either a horizontal or vertical line. // their memory layout is the same (packed pixels), so no special // handling is needed; just pick max dimension. for(size_t y = 0; y < std::max(src_w, src_h); y += 2) { for(size_t i = 0; i < num_components; i++) { *dst++ = (src[0]+src[dx]+1)/2; src += 1; } src += dx; // skip to next pixel (since box is 2x2) } } // normal else { for(size_t y = 0; y < src_h; y += 2) { for(size_t x = 0; x < src_w; x += 2) { for(size_t i = 0; i < num_components; i++) { *dst++ = (src[0]+src[dx]+src[dy]+src[dx+dy]+2)/4; src += 1; } src += dx; // skip to next pixel (since box is 2x2) } src += dy; // skip to next row (since box is 2x2) } } ENSURE(dst == level_data + level_dataSize); ENSURE(src == cld->prev_level_data + cld->prev_level_dataSize); } cld->prev_level_data = level_data; cld->prev_level_dataSize = level_dataSize; cld->prev_level_w = level_w; cld->prev_level_h = level_h; } static Status add_mipmaps(Tex* t, size_t w, size_t h, size_t bpp, void* newData, size_t dataSize) { // this code assumes the image is of POT dimension; we don't // go to the trouble of implementing image scaling because // the only place this is used (ogl_tex_upload) requires POT anyway. if(!is_pow2(w) || !is_pow2(h)) WARN_RETURN(ERR::TEX_INVALID_SIZE); t->m_Flags |= TEX_MIPMAPS; // must come before tex_img_size! const size_t mipmap_size = t->img_size(); shared_ptr mipmapData; AllocateAligned(mipmapData, mipmap_size); CreateLevelData cld = { bpp/8, w, h, (const u8*)newData, dataSize }; tex_util_foreach_mipmap(w, h, bpp, mipmapData.get(), 0, 1, create_level, &cld); t->m_Data = mipmapData; t->m_DataSize = mipmap_size; t->m_Ofs = 0; return INFO::OK; } //----------------------------------------------------------------------------- // pixel format conversion (transformation) //----------------------------------------------------------------------------- TIMER_ADD_CLIENT(tc_plain_transform); // handles BGR and row flipping in "plain" format (see below). // // called by codecs after they get their format-specific transforms out of // the way. note that this approach requires several passes over the image, // but is much easier to maintain than providing all<->all conversion paths. // // somewhat optimized (loops are hoisted, cache associativity accounted for) static Status plain_transform(Tex* t, size_t transforms) { TIMER_ACCRUE(tc_plain_transform); // (this is also called directly instead of through ogl_tex, so // we need to validate) CHECK_TEX(t); // extract texture info const size_t w = t->m_Width, h = t->m_Height, bpp = t->m_Bpp; const size_t flags = t->m_Flags; u8* const srcStorage = t->get_data(); // sanity checks (not errors, we just can't handle these cases) // .. unknown transform if(transforms & ~(TEX_BGR|TEX_ORIENTATION|TEX_MIPMAPS|TEX_ALPHA)) return INFO::TEX_CODEC_CANNOT_HANDLE; // .. data is not in "plain" format RETURN_STATUS_IF_ERR(tex_validate_plain_format(bpp, flags)); // .. nothing to do if(!transforms) return INFO::OK; const size_t srcSize = t->img_size(); size_t dstSize = srcSize; if(transforms & TEX_ALPHA) { // add alpha channel if(bpp == 24) { dstSize = (srcSize / 3) * 4; t->m_Bpp = 32; } // remove alpha channel else if(bpp == 32) { return INFO::TEX_CODEC_CANNOT_HANDLE; } // can't have alpha with grayscale else { return INFO::TEX_CODEC_CANNOT_HANDLE; } } // allocate copy of the image data. // rationale: L1 cache is typically A2 => swapping in-place with a // line buffer leads to thrashing. we'll assume the whole texture*2 // fits in cache, allocate a copy, and transfer directly from there. // // this is necessary even when not flipping because the initial data // is read-only. shared_ptr dstStorage; AllocateAligned(dstStorage, dstSize); // setup row source/destination pointers (simplifies outer loop) u8* dst = (u8*)dstStorage.get(); const u8* src; const size_t pitch = w * bpp/8; // source bpp (not necessarily dest bpp) // .. avoid y*pitch multiply in row loop; instead, add row_ofs. ssize_t row_ofs = (ssize_t)pitch; // flipping rows (0,1,2 -> 2,1,0) if(transforms & TEX_ORIENTATION) { src = (const u8*)srcStorage+srcSize-pitch; // last row row_ofs = -(ssize_t)pitch; } // adding/removing alpha channel (can't convert in-place) else if(transforms & TEX_ALPHA) { src = (const u8*)srcStorage; } // do other transforms in-place else { src = (const u8*)dstStorage.get(); memcpy(dstStorage.get(), srcStorage, srcSize); } // no conversion necessary if(!(transforms & (TEX_BGR | TEX_ALPHA))) { if(src != dst) // avoid overlapping memcpy if not flipping rows { for(size_t y = 0; y < h; y++) { memcpy(dst, src, pitch); dst += pitch; src += row_ofs; } } } // RGB -> BGRA, BGR -> RGBA else if(bpp == 24 && (transforms & TEX_ALPHA) && (transforms & TEX_BGR)) { for(size_t y = 0; y < h; y++) { for(size_t x = 0; x < w; x++) { // need temporaries in case src == dst (i.e. not flipping) const u8 b = src[0], g = src[1], r = src[2]; dst[0] = r; dst[1] = g; dst[2] = b; dst[3] = 0xFF; dst += 4; src += 3; } src += row_ofs - pitch; // flip? previous row : stay } } // RGB -> RGBA, BGR -> BGRA else if(bpp == 24 && (transforms & TEX_ALPHA) && !(transforms & TEX_BGR)) { for(size_t y = 0; y < h; y++) { for(size_t x = 0; x < w; x++) { // need temporaries in case src == dst (i.e. not flipping) const u8 r = src[0], g = src[1], b = src[2]; dst[0] = r; dst[1] = g; dst[2] = b; dst[3] = 0xFF; dst += 4; src += 3; } src += row_ofs - pitch; // flip? previous row : stay } } // RGB <-> BGR else if(bpp == 24 && !(transforms & TEX_ALPHA)) { for(size_t y = 0; y < h; y++) { for(size_t x = 0; x < w; x++) { // need temporaries in case src == dst (i.e. not flipping) const u8 b = src[0], g = src[1], r = src[2]; dst[0] = r; dst[1] = g; dst[2] = b; dst += 3; src += 3; } src += row_ofs - pitch; // flip? previous row : stay } } // RGBA <-> BGRA else if(bpp == 32 && !(transforms & TEX_ALPHA)) { for(size_t y = 0; y < h; y++) { for(size_t x = 0; x < w; x++) { // need temporaries in case src == dst (i.e. not flipping) const u8 b = src[0], g = src[1], r = src[2], a = src[3]; dst[0] = r; dst[1] = g; dst[2] = b; dst[3] = a; dst += 4; src += 4; } src += row_ofs - pitch; // flip? previous row : stay } } else { debug_warn(L"unsupported transform"); return INFO::TEX_CODEC_CANNOT_HANDLE; } t->m_Data = dstStorage; t->m_DataSize = dstSize; t->m_Ofs = 0; if(!(t->m_Flags & TEX_MIPMAPS) && transforms & TEX_MIPMAPS) RETURN_STATUS_IF_ERR(add_mipmaps(t, w, h, bpp, dstStorage.get(), dstSize)); CHECK_TEX(t); return INFO::OK; } TIMER_ADD_CLIENT(tc_transform); // change the pixel format by flipping the state of all TEX_* flags // that are set in transforms. Status Tex::transform(size_t transforms) { TIMER_ACCRUE(tc_transform); CHECK_TEX(this); const size_t target_flags = m_Flags ^ transforms; size_t remaining_transforms; for(;;) { remaining_transforms = target_flags ^ m_Flags; // we're finished (all required transforms have been done) if(remaining_transforms == 0) return INFO::OK; Status ret = tex_codec_transform(this, remaining_transforms); if(ret != INFO::OK) break; } // last chance RETURN_STATUS_IF_ERR(plain_transform(this, remaining_transforms)); return INFO::OK; } // change the pixel format to the new format specified by . // (note: this is equivalent to transform(t, t->flags^new_flags). Status Tex::transform_to(size_t new_flags) { // transform takes care of validating const size_t transforms = m_Flags ^ new_flags; return transform(transforms); } //----------------------------------------------------------------------------- // image orientation //----------------------------------------------------------------------------- // see "Default Orientation" in docs. static int global_orientation = TEX_TOP_DOWN; // set the orientation (either TEX_BOTTOM_UP or TEX_TOP_DOWN) to which // all loaded images will automatically be converted // (excepting file formats that don't specify their orientation, i.e. DDS). void tex_set_global_orientation(int o) { ENSURE(o == TEX_TOP_DOWN || o == TEX_BOTTOM_UP); global_orientation = o; } static void flip_to_global_orientation(Tex* t) { // (can't use normal CHECK_TEX due to void return) WARN_IF_ERR(t->validate()); size_t orientation = t->m_Flags & TEX_ORIENTATION; // if codec knows which way around the image is (i.e. not DDS): if(orientation) { // flip image if necessary size_t transforms = orientation ^ global_orientation; WARN_IF_ERR(plain_transform(t, transforms)); } // indicate image is at global orientation. this is still done even // if the codec doesn't know: the default orientation should be chosen // to make that work correctly (see "Default Orientation" in docs). t->m_Flags = (t->m_Flags & ~TEX_ORIENTATION) | global_orientation; // (can't use normal CHECK_TEX due to void return) WARN_IF_ERR(t->validate()); } // indicate if the orientation specified by matches // dst_orientation (if the latter is 0, then the global_orientation). // (we ask for src_flags instead of src_orientation so callers don't // have to mask off TEX_ORIENTATION) bool tex_orientations_match(size_t src_flags, size_t dst_orientation) { const size_t src_orientation = src_flags & TEX_ORIENTATION; if(dst_orientation == 0) dst_orientation = global_orientation; return (src_orientation == dst_orientation); } //----------------------------------------------------------------------------- // misc. API //----------------------------------------------------------------------------- // indicate if 's extension is that of a texture format // supported by Tex::load. case-insensitive. // // rationale: Tex::load complains if the given file is of an // unsupported type. this API allows users to preempt that warning // (by checking the filename themselves), and also provides for e.g. // enumerating only images in a file picker. // an alternative might be a flag to suppress warning about invalid files, // but this is open to misuse. bool tex_is_known_extension(const VfsPath& pathname) { const ITexCodec* dummy; // found codec for it => known extension const OsPath extension = pathname.Extension(); if(tex_codec_for_filename(extension, &dummy) == INFO::OK) return true; return false; } // store the given image data into a Tex object; this will be as if // it had been loaded via Tex::load. // // rationale: support for in-memory images is necessary for // emulation of glCompressedTexImage2D and useful overall. // however, we don't want to provide an alternate interface for each API; // these would have to be changed whenever fields are added to Tex. // instead, provide one entry point for specifying images. // // we need only add bookkeeping information and "wrap" it in // our Tex struct, hence the name. Status Tex::wrap(size_t w, size_t h, size_t bpp, size_t flags, const shared_ptr& data, size_t ofs) { m_Width = w; m_Height = h; m_Bpp = bpp; m_Flags = flags; m_Data = data; m_DataSize = ofs + w*h*bpp/8; m_Ofs = ofs; CHECK_TEX(this); return INFO::OK; } // free all resources associated with the image and make further // use of it impossible. void Tex::free() { // do not validate - this is called from Tex::load if loading // failed, so not all fields may be valid. m_Data.reset(); // do not zero out the fields! that could lead to trouble since // ogl_tex_upload followed by ogl_tex_free is legit, but would // cause OglTex_validate to fail (since its Tex.w is == 0). } //----------------------------------------------------------------------------- // getters //----------------------------------------------------------------------------- // returns a pointer to the image data (pixels), taking into account any // header(s) that may come before it. u8* Tex::get_data() { // (can't use normal CHECK_TEX due to u8* return value) WARN_IF_ERR(validate()); u8* p = m_Data.get(); if(!p) return 0; return p + m_Ofs; } // returns color of 1x1 mipmap level u32 Tex::get_average_color() const { // require mipmaps if(!(m_Flags & TEX_MIPMAPS)) return 0; // find the total size of image data size_t size = img_size(); // compute the size of the last (1x1) mipmap level const size_t data_padding = (m_Flags & TEX_DXT)? 4 : 1; size_t last_level_size = (size_t)(data_padding * data_padding * m_Bpp/8); // construct a new texture based on the current one, // but only include the last mipmap level // do this so that we can use the general conversion methods for the pixel data Tex basetex = *this; uint8_t *data = new uint8_t[last_level_size]; memcpy(data, m_Data.get() + m_Ofs + size - last_level_size, last_level_size); shared_ptr sdata(data, ArrayDeleter()); basetex.wrap(1, 1, m_Bpp, m_Flags, sdata, 0); // convert to BGRA WARN_IF_ERR(basetex.transform_to(TEX_BGR | TEX_ALPHA)); // extract components into u32 ENSURE(basetex.m_DataSize >= basetex.m_Ofs+4); u8 b = basetex.m_Data.get()[basetex.m_Ofs]; u8 g = basetex.m_Data.get()[basetex.m_Ofs+1]; u8 r = basetex.m_Data.get()[basetex.m_Ofs+2]; u8 a = basetex.m_Data.get()[basetex.m_Ofs+3]; return b + (g << 8) + (r << 16) + (a << 24); } static void add_level_size(size_t UNUSED(level), size_t UNUSED(level_w), size_t UNUSED(level_h), const u8* RESTRICT UNUSED(level_data), size_t level_dataSize, void* RESTRICT cbData) { size_t* ptotal_size = (size_t*)cbData; *ptotal_size += level_dataSize; } // return total byte size of the image pixels. (including mipmaps!) // this is preferable to calculating manually because it's // less error-prone (e.g. confusing bits_per_pixel with bytes). size_t Tex::img_size() const { // (can't use normal CHECK_TEX due to size_t return value) WARN_IF_ERR(validate()); const int levels_to_skip = (m_Flags & TEX_MIPMAPS)? 0 : TEX_BASE_LEVEL_ONLY; const size_t data_padding = (m_Flags & TEX_DXT)? 4 : 1; size_t out_size = 0; tex_util_foreach_mipmap(m_Width, m_Height, m_Bpp, 0, levels_to_skip, data_padding, add_level_size, &out_size); return out_size; } // return the minimum header size (i.e. offset to pixel data) of the // file format indicated by 's extension (that is all it need contain: // e.g. ".bmp"). returns 0 on error (i.e. no codec found). // this can be used to optimize calls to tex_write: when allocating the // buffer that will hold the image, allocate this much extra and // pass the pointer as base+hdr_size. this allows writing the header // directly into the output buffer and makes for zero-copy IO. size_t tex_hdr_size(const VfsPath& filename) { const ITexCodec* c; const OsPath extension = filename.Extension(); WARN_RETURN_STATUS_IF_ERR(tex_codec_for_filename(extension, &c)); return c->hdr_size(0); } //----------------------------------------------------------------------------- // read/write from memory and disk //----------------------------------------------------------------------------- Status Tex::decode(const shared_ptr& Data, size_t DataSize) { const ITexCodec* c; RETURN_STATUS_IF_ERR(tex_codec_for_header(Data.get(), DataSize, &c)); // make sure the entire header is available const size_t min_hdr_size = c->hdr_size(0); if(DataSize < min_hdr_size) WARN_RETURN(ERR::TEX_INCOMPLETE_HEADER); const size_t hdr_size = c->hdr_size(Data.get()); if(DataSize < hdr_size) WARN_RETURN(ERR::TEX_INCOMPLETE_HEADER); m_Data = Data; m_DataSize = DataSize; m_Ofs = hdr_size; RETURN_STATUS_IF_ERR(c->decode(Data.get(), DataSize, this)); // sanity checks if(!m_Width || !m_Height || m_Bpp > 32) WARN_RETURN(ERR::TEX_FMT_INVALID); if(m_DataSize < m_Ofs + img_size()) WARN_RETURN(ERR::TEX_INVALID_SIZE); flip_to_global_orientation(this); CHECK_TEX(this); return INFO::OK; } Status Tex::encode(const OsPath& extension, DynArray* da) { CHECK_TEX(this); WARN_RETURN_STATUS_IF_ERR(tex_validate_plain_format(m_Bpp, m_Flags)); // we could be clever here and avoid the extra alloc if our current // memory block ensued from the same kind of texture file. this is // most likely the case if in_img == get_data() + c->hdr_size(0). // this would make for zero-copy IO. const size_t max_out_size = img_size()*4 + 256*KiB; RETURN_STATUS_IF_ERR(da_alloc(da, max_out_size)); const ITexCodec* c; WARN_RETURN_STATUS_IF_ERR(tex_codec_for_filename(extension, &c)); // encode into Status err = c->encode(this, da); if(err < 0) { (void)da_free(da); WARN_RETURN(err); } return INFO::OK; } Index: ps/trunk/source/lib/tex/tex_bmp.cpp =================================================================== --- ps/trunk/source/lib/tex/tex_bmp.cpp (revision 19502) +++ ps/trunk/source/lib/tex/tex_bmp.cpp (revision 19503) @@ -1,157 +1,157 @@ -/* Copyright (c) 2017 Wildfire Games +/* Copyright (C) 2017 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. */ /* * Windows BMP codec */ #include "precompiled.h" #include "lib/byte_order.h" #include "tex_codec.h" #pragma pack(push, 1) struct BmpHeader { // BITMAPFILEHEADER u16 bfType; // "BM" u32 bfSize; // of file u16 bfReserved1; u16 bfReserved2; u32 bfOffBits; // offset to image data // BITMAPINFOHEADER u32 biSize; i32 biWidth; i32 biHeight; u16 biPlanes; u16 biBitCount; u32 biCompression; u32 biSizeImage; // the following are unused and zeroed when writing: i32 biXPelsPerMeter; i32 biYPelsPerMeter; u32 biClrUsed; u32 biClrImportant; }; #pragma pack(pop) #define BI_RGB 0 // biCompression Status TexCodecBmp::transform(Tex* UNUSED(t), size_t UNUSED(transforms)) const { return INFO::TEX_CODEC_CANNOT_HANDLE; } bool TexCodecBmp::is_hdr(const u8* file) const { // check header signature (bfType == "BM"?). // we compare single bytes to be endian-safe. return (file[0] == 'B' && file[1] == 'M'); } bool TexCodecBmp::is_ext(const OsPath& extension) const { return extension == L".bmp"; } size_t TexCodecBmp::hdr_size(const u8* file) const { const size_t hdr_size = sizeof(BmpHeader); if(file) { BmpHeader* hdr = (BmpHeader*)file; const u32 ofs = read_le32(&hdr->bfOffBits); ENSURE(ofs >= hdr_size && "bmp_hdr_size invalid"); return ofs; } return hdr_size; } // requirements: uncompressed, direct color, bottom up Status TexCodecBmp::decode(u8* RESTRICT data, size_t UNUSED(size), Tex* RESTRICT t) const { const BmpHeader* hdr = (const BmpHeader*)data; const long w = (long)read_le32(&hdr->biWidth); const long h_ = (long)read_le32(&hdr->biHeight); const u16 bpp = read_le16(&hdr->biBitCount); const u32 compress = read_le32(&hdr->biCompression); const long h = abs(h_); size_t flags = 0; flags |= (h_ < 0)? TEX_TOP_DOWN : TEX_BOTTOM_UP; if(bpp > 16) flags |= TEX_BGR; if(bpp == 32) flags |= TEX_ALPHA; // sanity checks if(compress != BI_RGB) WARN_RETURN(ERR::TEX_COMPRESSED); t->m_Width = w; t->m_Height = h; t->m_Bpp = bpp; t->m_Flags = flags; return INFO::OK; } Status TexCodecBmp::encode(Tex* RESTRICT t, DynArray* RESTRICT da) const { const size_t hdr_size = sizeof(BmpHeader); // needed for BITMAPFILEHEADER const size_t img_size = t->img_size(); const size_t file_size = hdr_size + img_size; const i32 h = (t->m_Flags & TEX_TOP_DOWN)? -(i32)t->m_Height : (i32)t->m_Height; size_t transforms = t->m_Flags; transforms &= ~TEX_ORIENTATION; // no flip needed - we can set top-down bit. transforms ^= TEX_BGR; // BMP is native BGR. const BmpHeader hdr = { // BITMAPFILEHEADER 0x4D42, // bfType = 'B','M' (u32)file_size, // bfSize 0, 0, // bfReserved1,2 hdr_size, // bfOffBits // BITMAPINFOHEADER 40, // biSize = sizeof(BITMAPINFOHEADER) (i32)t->m_Width, h, 1, // biPlanes (u16)t->m_Bpp, BI_RGB, // biCompression (u32)img_size, // biSizeImage 0, 0, 0, 0 // unused (bi?PelsPerMeter, biClr*) }; return tex_codec_write(t, transforms, &hdr, hdr_size, da); } Index: ps/trunk/source/lib/tex/tex_dds.cpp =================================================================== --- ps/trunk/source/lib/tex/tex_dds.cpp (revision 19502) +++ ps/trunk/source/lib/tex/tex_dds.cpp (revision 19503) @@ -1,653 +1,653 @@ -/* Copyright (c) 2017 Wildfire Games +/* Copyright (C) 2017 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. */ /* * DDS (DirectDraw Surface) codec. */ #include "precompiled.h" #include "lib/byte_order.h" #include "lib/bits.h" #include "lib/timer.h" #include "lib/allocators/shared_ptr.h" #include "tex_codec.h" // NOTE: the convention is bottom-up for DDS, but there's no way to tell. //----------------------------------------------------------------------------- // S3TC decompression //----------------------------------------------------------------------------- // note: this code may not be terribly efficient. it's only used to // emulate hardware S3TC support - if that isn't available, performance // will suffer anyway due to increased video memory usage. // for efficiency, we precalculate as much as possible about a block // and store it here. class S3tcBlock { public: S3tcBlock(size_t dxt, const u8* RESTRICT block) : dxt(dxt) { // (careful, 'dxt != 1' doesn't work - there's also DXT1a) const u8* a_block = block; const u8* c_block = (dxt == 3 || dxt == 5)? block+8 : block; PrecalculateAlpha(dxt, a_block); PrecalculateColor(dxt, c_block); } void WritePixel(size_t pixel_idx, u8* RESTRICT out) const { ENSURE(pixel_idx < 16); // pixel index -> color selector (2 bit) -> color const size_t c_selector = access_bit_tbl(c_selectors, pixel_idx, 2); for(int i = 0; i < 3; i++) out[i] = (u8)c[c_selector][i]; // if no alpha, done if(dxt == 1) return; size_t a; if(dxt == 3) { // table of 4-bit alpha entries a = access_bit_tbl(a_bits, pixel_idx, 4); a |= a << 4; // expand to 8 bits (replicate high into low!) } else if(dxt == 5) { // pixel index -> alpha selector (3 bit) -> alpha const size_t a_selector = access_bit_tbl(a_bits, pixel_idx, 3); a = dxt5_a_tbl[a_selector]; } // (dxt == DXT1A) else a = c[c_selector][A]; out[A] = (u8)(a & 0xFF); } private: // pixel colors are stored as size_t[4]. size_t rather than u8 protects from // overflow during calculations, and padding to an even size is a bit // more efficient (even though we don't need the alpha component). enum RGBA { R, G, B, A }; static inline void mix_2_3(size_t dst[4], size_t c0[4], size_t c1[4]) { for(int i = 0; i < 3; i++) dst[i] = (c0[i]*2 + c1[i] + 1)/3; } static inline void mix_avg(size_t dst[4], size_t c0[4], size_t c1[4]) { for(int i = 0; i < 3; i++) dst[i] = (c0[i]+c1[i])/2; } template static inline size_t access_bit_tbl(T tbl, size_t idx, size_t bit_width) { size_t val = (tbl >> (idx*bit_width)) & bit_mask(bit_width); return val; } // extract a range of bits and expand to 8 bits (by replicating // MS bits - see http://www.mindcontrol.org/~hplus/graphics/expand-bits.html ; // this is also the algorithm used by graphics cards when decompressing S3TC). // used to convert 565 to 32bpp RGB. static inline size_t unpack_to_8(u16 c, size_t bits_below, size_t num_bits) { const size_t num_filler_bits = 8-num_bits; const size_t field = (size_t)bits(c, bits_below, bits_below+num_bits-1); const size_t filler = field >> (num_bits-num_filler_bits); return (field << num_filler_bits) | filler; } void PrecalculateAlpha(size_t dxt, const u8* RESTRICT a_block) { // read block contents const u8 a0 = a_block[0], a1 = a_block[1]; a_bits = read_le64(a_block); // see below if(dxt == 5) { // skip a0,a1 bytes (data is little endian) a_bits >>= 16; const bool is_dxt5_special_combination = (a0 <= a1); u8* a = dxt5_a_tbl; // shorthand if(is_dxt5_special_combination) { a[0] = a0; a[1] = a1; a[2] = (4*a0 + 1*a1 + 2)/5; a[3] = (3*a0 + 2*a1 + 2)/5; a[4] = (2*a0 + 3*a1 + 2)/5; a[5] = (1*a0 + 4*a1 + 2)/5; a[6] = 0; a[7] = 255; } else { a[0] = a0; a[1] = a1; a[2] = (6*a0 + 1*a1 + 3)/7; a[3] = (5*a0 + 2*a1 + 3)/7; a[4] = (4*a0 + 3*a1 + 3)/7; a[5] = (3*a0 + 4*a1 + 3)/7; a[6] = (2*a0 + 5*a1 + 3)/7; a[7] = (1*a0 + 6*a1 + 3)/7; } } } void PrecalculateColor(size_t dxt, const u8* RESTRICT c_block) { // read block contents // .. S3TC reference colors (565 format). the color table is generated // from some combination of these, depending on their ordering. u16 rc[2]; for(int i = 0; i < 2; i++) rc[i] = read_le16(c_block + 2*i); // .. table of 2-bit color selectors c_selectors = read_le32(c_block+4); const bool is_dxt1_special_combination = (dxt == 1 || dxt == DXT1A) && rc[0] <= rc[1]; // c0 and c1 are the values of rc[], converted to 32bpp for(int i = 0; i < 2; i++) { c[i][R] = unpack_to_8(rc[i], 11, 5); c[i][G] = unpack_to_8(rc[i], 5, 6); c[i][B] = unpack_to_8(rc[i], 0, 5); } // c2 and c3 are combinations of c0 and c1: if(is_dxt1_special_combination) { mix_avg(c[2], c[0], c[1]); // c2 = (c0+c1)/2 for(int i = 0; i < 3; i++) c[3][i] = 0; // c3 = black c[3][A] = (dxt == DXT1A)? 0 : 255; // (transparent iff DXT1a) } else { mix_2_3(c[2], c[0], c[1]); // c2 = 2/3*c0 + 1/3*c1 mix_2_3(c[3], c[1], c[0]); // c3 = 1/3*c0 + 2/3*c1 } } // the 4 color choices for each pixel (RGBA) size_t c[4][4]; // c[i][RGBA_component] // (DXT5 only) the 8 alpha choices u8 dxt5_a_tbl[8]; // alpha block; interpretation depends on dxt. u64 a_bits; // table of 2-bit color selectors u32 c_selectors; size_t dxt; }; struct S3tcDecompressInfo { size_t dxt; size_t s3tc_block_size; size_t out_Bpp; u8* out; }; static void s3tc_decompress_level(size_t UNUSED(level), size_t level_w, size_t level_h, const u8* RESTRICT level_data, size_t level_data_size, void* RESTRICT cbData) { S3tcDecompressInfo* di = (S3tcDecompressInfo*)cbData; const size_t dxt = di->dxt; const size_t s3tc_block_size = di->s3tc_block_size; // note: 1x1 images are legitimate (e.g. in mipmaps). they report their // width as such for glTexImage, but the S3TC data is padded to // 4x4 pixel block boundaries. const size_t blocks_w = DivideRoundUp(level_w, size_t(4)); const size_t blocks_h = DivideRoundUp(level_h, size_t(4)); const u8* s3tc_data = level_data; ENSURE(level_data_size % s3tc_block_size == 0); for(size_t block_y = 0; block_y < blocks_h; block_y++) { for(size_t block_x = 0; block_x < blocks_w; block_x++) { S3tcBlock block(dxt, s3tc_data); s3tc_data += s3tc_block_size; size_t pixel_idx = 0; for(int y = 0; y < 4; y++) { // this is ugly, but advancing after x, y and block_y loops // is no better. u8* out = (u8*)di->out + ((block_y*4+y)*blocks_w*4 + block_x*4) * di->out_Bpp; for(int x = 0; x < 4; x++) { block.WritePixel(pixel_idx, out); out += di->out_Bpp; pixel_idx++; } } } } ENSURE(s3tc_data == level_data + level_data_size); di->out += blocks_w*blocks_h * 16 * di->out_Bpp; } // decompress the given image (which is known to be stored as DXTn) // effectively in-place. updates Tex fields. static Status s3tc_decompress(Tex* t) { // alloc new image memory // notes: // - dxt == 1 is the only non-alpha case. // - adding or stripping alpha channels during transform is not // our job; we merely output the same pixel format as given // (tex.cpp's plain transform could cover it, if ever needed). const size_t dxt = t->m_Flags & TEX_DXT; const size_t out_bpp = (dxt != 1)? 32 : 24; const size_t out_size = t->img_size() * out_bpp / t->m_Bpp; shared_ptr decompressedData; AllocateAligned(decompressedData, out_size, pageSize); const size_t s3tc_block_size = (dxt == 3 || dxt == 5)? 16 : 8; S3tcDecompressInfo di = { dxt, s3tc_block_size, out_bpp/8, decompressedData.get() }; const u8* s3tc_data = t->get_data(); const int levels_to_skip = (t->m_Flags & TEX_MIPMAPS)? 0 : TEX_BASE_LEVEL_ONLY; tex_util_foreach_mipmap(t->m_Width, t->m_Height, t->m_Bpp, s3tc_data, levels_to_skip, 4, s3tc_decompress_level, &di); t->m_Data = decompressedData; t->m_DataSize = out_size; t->m_Ofs = 0; t->m_Bpp = out_bpp; t->m_Flags &= ~TEX_DXT; return INFO::OK; } //----------------------------------------------------------------------------- // DDS file format //----------------------------------------------------------------------------- // bit values and structure definitions taken from // http://msdn.microsoft.com/en-us/library/ee417785(VS.85).aspx #pragma pack(push, 1) // DDS_PIXELFORMAT.dwFlags // we've seen some DXT3 files that don't have this set (which is nonsense; // any image lacking alpha should be stored as DXT1). it's authoritative // if fourcc is DXT1 (there's no other way to tell DXT1 and DXT1a apart) // and ignored otherwise. #define DDPF_ALPHAPIXELS 0x00000001 #define DDPF_FOURCC 0x00000004 #define DDPF_RGB 0x00000040 struct DDS_PIXELFORMAT { u32 dwSize; // size of structure (32) u32 dwFlags; // indicates which fields are valid u32 dwFourCC; // (DDPF_FOURCC) FOURCC code, "DXTn" u32 dwRGBBitCount; // (DDPF_RGB) bits per pixel u32 dwRBitMask; u32 dwGBitMask; u32 dwBBitMask; u32 dwABitMask; // (DDPF_ALPHAPIXELS) }; // DDS_HEADER.dwFlags (none are optional) #define DDSD_CAPS 0x00000001 #define DDSD_HEIGHT 0x00000002 #define DDSD_WIDTH 0x00000004 #define DDSD_PITCH 0x00000008 // used when texture is uncompressed #define DDSD_PIXELFORMAT 0x00001000 #define DDSD_MIPMAPCOUNT 0x00020000 #define DDSD_LINEARSIZE 0x00080000 // used when texture is compressed #define DDSD_DEPTH 0x00800000 // DDS_HEADER.dwCaps #define DDSCAPS_MIPMAP 0x00400000 // optional #define DDSCAPS_TEXTURE 0x00001000 // required struct DDS_HEADER { // (preceded by the FOURCC "DDS ") u32 dwSize; // size of structure (124) u32 dwFlags; // indicates which fields are valid u32 dwHeight; // (DDSD_HEIGHT) height of main image (pixels) u32 dwWidth; // (DDSD_WIDTH ) width of main image (pixels) u32 dwPitchOrLinearSize; // (DDSD_LINEARSIZE) size [bytes] of top level // (DDSD_PITCH) bytes per row (%4 = 0) u32 dwDepth; // (DDSD_DEPTH) vol. textures: vol. depth u32 dwMipMapCount; // (DDSD_MIPMAPCOUNT) total # levels u32 dwReserved1[11]; // reserved DDS_PIXELFORMAT ddpf; // (DDSD_PIXELFORMAT) surface description u32 dwCaps; // (DDSD_CAPS) misc. surface flags u32 dwCaps2; u32 dwCaps3; u32 dwCaps4; u32 dwReserved2; // reserved }; #pragma pack(pop) static bool is_valid_dxt(size_t dxt) { switch(dxt) { case 0: case 1: case DXT1A: case 3: case 5: return true; default: return false; } } // extract all information from DDS pixel format and store in bpp, flags. // pf points to the DDS file's header; all fields must be endian-converted // before use. // output parameters invalid on failure. static Status decode_pf(const DDS_PIXELFORMAT* pf, size_t& bpp, size_t& flags) { bpp = 0; flags = 0; // check struct size if(read_le32(&pf->dwSize) != sizeof(DDS_PIXELFORMAT)) WARN_RETURN(ERR::TEX_INVALID_SIZE); // determine type const size_t pf_flags = (size_t)read_le32(&pf->dwFlags); // .. uncompressed RGB/RGBA if(pf_flags & DDPF_RGB) { const size_t pf_bpp = (size_t)read_le32(&pf->dwRGBBitCount); const size_t pf_r_mask = (size_t)read_le32(&pf->dwRBitMask); const size_t pf_g_mask = (size_t)read_le32(&pf->dwGBitMask); const size_t pf_b_mask = (size_t)read_le32(&pf->dwBBitMask); const size_t pf_a_mask = (size_t)read_le32(&pf->dwABitMask); // (checked below; must be set in case below warning is to be // skipped) bpp = pf_bpp; if(pf_flags & DDPF_ALPHAPIXELS) { // something weird other than RGBA or BGRA if(pf_a_mask != 0xFF000000) WARN_RETURN(ERR::TEX_FMT_INVALID); flags |= TEX_ALPHA; } // make sure component ordering is 0xBBGGRR = RGB (see below) if(pf_r_mask != 0xFF || pf_g_mask != 0xFF00 || pf_b_mask != 0xFF0000) { // DDS_PIXELFORMAT in theory supports any ordering of R,G,B,A. // we need to upload to OpenGL, which can only receive BGR(A) or // RGB(A). the former still requires conversion (done by driver), // so it's slower. since the very purpose of supporting uncompressed // DDS is storing images in a format that requires no processing, // we do not allow any weird orderings that require runtime work. // instead, the artists must export with the correct settings. WARN_RETURN(ERR::TEX_FMT_INVALID); } RETURN_STATUS_IF_ERR(tex_validate_plain_format(bpp, (int)flags)); } // .. uncompressed 8bpp greyscale else if(pf_flags & DDPF_ALPHAPIXELS) { const size_t pf_bpp = (size_t)read_le32(&pf->dwRGBBitCount); const size_t pf_a_mask = (size_t)read_le32(&pf->dwABitMask); bpp = pf_bpp; if(pf_bpp != 8) WARN_RETURN(ERR::TEX_FMT_INVALID); if(pf_a_mask != 0xFF) WARN_RETURN(ERR::TEX_FMT_INVALID); flags |= TEX_GREY; RETURN_STATUS_IF_ERR(tex_validate_plain_format(bpp, (int)flags)); } // .. compressed else if(pf_flags & DDPF_FOURCC) { // set effective bpp and store DXT format in flags & TEX_DXT. // no endian conversion necessary - FOURCC() takes care of that. switch(pf->dwFourCC) { case FOURCC('D','X','T','1'): bpp = 4; if(pf_flags & DDPF_ALPHAPIXELS) flags |= DXT1A | TEX_ALPHA; else flags |= 1; break; case FOURCC('D','X','T','3'): bpp = 8; flags |= 3; flags |= TEX_ALPHA; // see DDPF_ALPHAPIXELS decl break; case FOURCC('D','X','T','5'): bpp = 8; flags |= 5; flags |= TEX_ALPHA; // see DDPF_ALPHAPIXELS decl break; default: WARN_RETURN(ERR::TEX_FMT_INVALID); } } // .. neither uncompressed nor compressed - invalid else WARN_RETURN(ERR::TEX_FMT_INVALID); return INFO::OK; } // extract all information from DDS header and store in w, h, bpp, flags. // sd points to the DDS file's header; all fields must be endian-converted // before use. // output parameters invalid on failure. static Status decode_sd(const DDS_HEADER* sd, size_t& w, size_t& h, size_t& bpp, size_t& flags) { // check header size if(read_le32(&sd->dwSize) != sizeof(*sd)) WARN_RETURN(ERR::CORRUPTED); // flags (indicate which fields are valid) const size_t sd_flags = (size_t)read_le32(&sd->dwFlags); // .. not all required fields are present // note: we can't guess dimensions - the image may not be square. const size_t sd_req_flags = DDSD_CAPS|DDSD_HEIGHT|DDSD_WIDTH|DDSD_PIXELFORMAT; if((sd_flags & sd_req_flags) != sd_req_flags) WARN_RETURN(ERR::TEX_INCOMPLETE_HEADER); // image dimensions h = (size_t)read_le32(&sd->dwHeight); w = (size_t)read_le32(&sd->dwWidth); // pixel format RETURN_STATUS_IF_ERR(decode_pf(&sd->ddpf, bpp, flags)); // if the image is not aligned with the S3TC block size, it is stored // with extra pixels on the bottom left to fill up the space, so we need // to account for those when calculating how big it should be size_t stored_h, stored_w; if(flags & TEX_DXT) { stored_h = Align<4>(h); stored_w = Align<4>(w); } else { stored_h = h; stored_w = w; } // verify pitch or linear size, if given const size_t pitch = stored_w*bpp/8; const size_t sd_pitch_or_size = (size_t)read_le32(&sd->dwPitchOrLinearSize); if(sd_flags & DDSD_PITCH) { if(sd_pitch_or_size != Align<4>(pitch)) DEBUG_WARN_ERR(ERR::CORRUPTED); } if(sd_flags & DDSD_LINEARSIZE) { // some DDS tools mistakenly store the total size of all levels, // so allow values close to that as well const ssize_t totalSize = ssize_t(pitch*stored_h*1.333333f); if(sd_pitch_or_size != pitch*stored_h && abs(ssize_t(sd_pitch_or_size)-totalSize) > 64) DEBUG_WARN_ERR(ERR::CORRUPTED); } // note: both flags set would be invalid; no need to check for that, // though, since one of the above tests would fail. // mipmaps if(sd_flags & DDSD_MIPMAPCOUNT) { const size_t mipmap_count = (size_t)read_le32(&sd->dwMipMapCount); if(mipmap_count) { // mipmap chain is incomplete // note: DDS includes the base level in its count, hence +1. if(mipmap_count != ceil_log2(std::max(w,h))+1) WARN_RETURN(ERR::TEX_FMT_INVALID); flags |= TEX_MIPMAPS; } } // check for volume textures if(sd_flags & DDSD_DEPTH) { const size_t depth = (size_t)read_le32(&sd->dwDepth); if(depth) WARN_RETURN(ERR::NOT_SUPPORTED); } // check caps // .. this is supposed to be set, but don't bail if not (pointless) ENSURE(sd->dwCaps & DDSCAPS_TEXTURE); // .. sanity check: warn if mipmap flag not set (don't bail if not // because we've already made the decision). const bool mipmap_cap = (sd->dwCaps & DDSCAPS_MIPMAP) != 0; const bool mipmap_flag = (flags & TEX_MIPMAPS) != 0; ENSURE(mipmap_cap == mipmap_flag); // note: we do not check for cubemaps and volume textures (not supported) // because the file may still have useful data we can read. return INFO::OK; } //----------------------------------------------------------------------------- bool TexCodecDds::is_hdr(const u8* file) const { return *(u32*)file == FOURCC('D','D','S',' '); } bool TexCodecDds::is_ext(const OsPath& extension) const { return extension == L".dds"; } size_t TexCodecDds::hdr_size(const u8* UNUSED(file)) const { return 4+sizeof(DDS_HEADER); } Status TexCodecDds::decode(u8* RESTRICT data, size_t UNUSED(size), Tex* RESTRICT t) const { const DDS_HEADER* sd = (const DDS_HEADER*)(data+4); RETURN_STATUS_IF_ERR(decode_sd(sd, t->m_Width, t->m_Height, t->m_Bpp, t->m_Flags)); return INFO::OK; } Status TexCodecDds::encode(Tex* RESTRICT UNUSED(t), DynArray* RESTRICT UNUSED(da)) const { // note: do not return ERR::NOT_SUPPORTED et al. because that would // break tex_write (which assumes either this, 0 or errors are returned). return INFO::TEX_CODEC_CANNOT_HANDLE; } TIMER_ADD_CLIENT(tc_dds_transform); Status TexCodecDds::transform(Tex* t, size_t transforms) const { TIMER_ACCRUE(tc_dds_transform); size_t mipmaps = t->m_Flags & TEX_MIPMAPS; size_t dxt = t->m_Flags & TEX_DXT; ENSURE(is_valid_dxt(dxt)); const size_t transform_mipmaps = transforms & TEX_MIPMAPS; const size_t transform_dxt = transforms & TEX_DXT; // requesting removal of mipmaps if(mipmaps && transform_mipmaps) { // we don't need to actually change anything except the flag - the // mipmap levels will just be treated as trailing junk t->m_Flags &= ~TEX_MIPMAPS; return INFO::OK; } // requesting decompression if(dxt && transform_dxt) { RETURN_STATUS_IF_ERR(s3tc_decompress(t)); return INFO::OK; } // both are DXT (unsupported; there are no flags we can change while // compressed) or requesting compression (not implemented) or // both not DXT (nothing we can do) - bail. return INFO::TEX_CODEC_CANNOT_HANDLE; } Index: ps/trunk/source/lib/tex/tex_internal.h =================================================================== --- ps/trunk/source/lib/tex/tex_internal.h (revision 19502) +++ ps/trunk/source/lib/tex/tex_internal.h (revision 19503) @@ -1,60 +1,60 @@ -/* Copyright (c) 2017 Wildfire Games +/* Copyright (C) 2017 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. */ /* * private texture loader helper functions */ #ifndef INCLUDED_TEX_INTERNAL #define INCLUDED_TEX_INTERNAL #include "lib/allocators/dynarray.h" #include "lib/file/io/io.h" // io::Allocate /** * check if the given texture format is acceptable: 8bpp grey, * 24bpp color or 32bpp color+alpha (BGR / upside down are permitted). * basically, this is the "plain" format understood by all codecs and * tex_codec_plain_transform. * @param bpp bits per pixel * @param flags TexFlags * @return Status **/ extern Status tex_validate_plain_format(size_t bpp, size_t flags); /** * indicate if the two vertical orientations match. * * used by tex_codec. * * @param src_flags TexFlags, used to extract the orientation. * we ask for this instead of src_orientation so callers don't have to * mask off TEX_ORIENTATION. * @param dst_orientation orientation to compare against. * can be one of TEX_BOTTOM_UP, TEX_TOP_DOWN, or 0 for the * "global orientation". * @return bool **/ extern bool tex_orientations_match(size_t src_flags, size_t dst_orientation); #endif // #ifndef INCLUDED_TEX_INTERNAL Index: ps/trunk/source/lib/tex/tex_png.cpp =================================================================== --- ps/trunk/source/lib/tex/tex_png.cpp (revision 19502) +++ ps/trunk/source/lib/tex/tex_png.cpp (revision 19503) @@ -1,334 +1,334 @@ -/* Copyright (c) 2017 Wildfire Games +/* Copyright (C) 2017 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. */ /* * PNG codec using libpng. */ #include "precompiled.h" #include "lib/external_libraries/png.h" #include "lib/byte_order.h" #include "tex_codec.h" #include "lib/allocators/shared_ptr.h" #include "lib/timer.h" #if MSC_VERSION // squelch "dtor / setjmp interaction" warnings. // all attempts to resolve the underlying problem failed; apparently // the warning is generated if setjmp is used at all in C++ mode. // (png_*_impl have no code that would trigger ctors/dtors, nor are any // called in their prolog/epilog code). # pragma warning(disable: 4611) #endif // MSC_VERSION //----------------------------------------------------------------------------- // //----------------------------------------------------------------------------- class MemoryStream { public: MemoryStream(u8* RESTRICT data, size_t size) : data(data), size(size), pos(0) { } size_t RemainingSize() const { ASSERT(pos <= size); return size-pos; } void CopyTo(u8* RESTRICT dst, size_t dstSize) { memcpy(dst, data+pos, dstSize); pos += dstSize; } private: u8* RESTRICT data; size_t size; size_t pos; }; // pass data from PNG file in memory to libpng static void io_read(png_struct* png_ptr, u8* RESTRICT data, png_size_t size) { MemoryStream* stream = (MemoryStream*)png_get_io_ptr(png_ptr); if(stream->RemainingSize() < size) { png_error(png_ptr, "PNG: not enough input"); return; } stream->CopyTo(data, size); } // write libpng output to PNG file static void io_write(png_struct* png_ptr, u8* data, png_size_t length) { DynArray* da = (DynArray*)png_get_io_ptr(png_ptr); if(da_append(da, data, length) != 0) png_error(png_ptr, "io_write failed"); } static void io_flush(png_structp UNUSED(png_ptr)) { } //----------------------------------------------------------------------------- Status TexCodecPng::transform(Tex* UNUSED(t), size_t UNUSED(transforms)) const { return INFO::TEX_CODEC_CANNOT_HANDLE; } // note: it's not worth combining png_encode and png_decode, due to // libpng read/write interface differences (grr). // split out of png_decode to simplify resource cleanup and avoid // "dtor / setjmp interaction" warning. static Status png_decode_impl(MemoryStream* stream, png_structp png_ptr, png_infop info_ptr, Tex* t) { png_set_read_fn(png_ptr, stream, io_read); // read header and determine format png_read_info(png_ptr, info_ptr); png_uint_32 w, h; int bit_depth, color_type, interlace_type; png_get_IHDR(png_ptr, info_ptr, &w, &h, &bit_depth, &color_type, &interlace_type, 0, 0); // (The following is based on GdkPixbuf's PNG image loader) // Convert the following images to 8-bit RGB/RGBA: // * indexed colors // * grayscale with alpha // * transparency header // * bit depth of 16 or less than 8 // * interlaced if (color_type == PNG_COLOR_TYPE_PALETTE && bit_depth <= 8) png_set_expand(png_ptr); else if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) png_set_expand(png_ptr); else if (bit_depth < 8) png_set_expand(png_ptr); if (bit_depth == 16) png_set_strip_16(png_ptr); if (color_type == PNG_COLOR_TYPE_GRAY_ALPHA) png_set_gray_to_rgb(png_ptr); if (interlace_type != PNG_INTERLACE_NONE) png_set_interlace_handling(png_ptr); // Update info after transformations png_read_update_info(png_ptr, info_ptr); png_get_IHDR(png_ptr, info_ptr, &w, &h, &bit_depth, &color_type, &interlace_type, 0, 0); // make sure format is acceptable: // * non-zero dimensions // * 8-bit depth // * RGB, RGBA, or grayscale // * 1, 3 or 4 channels if (w == 0 || h == 0) WARN_RETURN(ERR::TEX_INVALID_SIZE); if (bit_depth != 8) WARN_RETURN(ERR::TEX_NOT_8BIT_PRECISION); if (!(color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_RGB_ALPHA || color_type == PNG_COLOR_TYPE_GRAY)) WARN_RETURN(ERR::TEX_INVALID_COLOR_TYPE); const int channels = png_get_channels(png_ptr, info_ptr); if (!(channels == 3 || channels == 4 || channels == 1)) WARN_RETURN(ERR::TEX_FMT_INVALID); const size_t pitch = png_get_rowbytes(png_ptr, info_ptr); const u32 bpp = (u32)(pitch / w * 8); size_t flags = 0; if (color_type == PNG_COLOR_TYPE_RGB_ALPHA) flags |= TEX_ALPHA; if (color_type == PNG_COLOR_TYPE_GRAY) flags |= TEX_GREY; const size_t img_size = pitch * h; shared_ptr data; AllocateAligned(data, img_size, pageSize); std::vector rows = tex_codec_alloc_rows(data.get(), h, pitch, TEX_TOP_DOWN, 0); png_read_image(png_ptr, (png_bytepp)&rows[0]); png_read_end(png_ptr, info_ptr); // success; make sure all data was consumed. ENSURE(stream->RemainingSize() == 0); // store image info and validate return t->wrap(w,h,bpp,flags,data,0); } // split out of png_encode to simplify resource cleanup and avoid // "dtor / setjmp interaction" warning. static Status png_encode_impl(Tex* t, png_structp png_ptr, png_infop info_ptr, DynArray* da) { const png_uint_32 w = (png_uint_32)t->m_Width, h = (png_uint_32)t->m_Height; const size_t pitch = w * t->m_Bpp / 8; int color_type; switch(t->m_Flags & (TEX_GREY|TEX_ALPHA)) { case TEX_GREY|TEX_ALPHA: color_type = PNG_COLOR_TYPE_GRAY_ALPHA; break; case TEX_GREY: color_type = PNG_COLOR_TYPE_GRAY; break; case TEX_ALPHA: color_type = PNG_COLOR_TYPE_RGB_ALPHA; break; default: color_type = PNG_COLOR_TYPE_RGB; break; } png_set_write_fn(png_ptr, da, io_write, io_flush); png_set_IHDR(png_ptr, info_ptr, w, h, 8, color_type, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); u8* data = t->get_data(); std::vector rows = tex_codec_alloc_rows(data, h, pitch, t->m_Flags, TEX_TOP_DOWN); // PNG is native RGB. const int png_transforms = (t->m_Flags & TEX_BGR)? PNG_TRANSFORM_BGR : PNG_TRANSFORM_IDENTITY; png_set_rows(png_ptr, info_ptr, (png_bytepp)&rows[0]); png_write_png(png_ptr, info_ptr, png_transforms, 0); return INFO::OK; } bool TexCodecPng::is_hdr(const u8* file) const { // don't use png_sig_cmp, so we don't pull in libpng for // this check alone (it might not actually be used). return *(u32*)file == FOURCC('\x89','P','N','G'); } bool TexCodecPng::is_ext(const OsPath& extension) const { return extension == L".png"; } size_t TexCodecPng::hdr_size(const u8* UNUSED(file)) const { return 0; // libpng returns decoded image data; no header } static void user_warning_fn(png_structp UNUSED(png_ptr), png_const_charp warning_msg) { // Suppress this warning because it's useless and occurs on a large number of files // see http://trac.wildfiregames.com/ticket/2184 if (strcmp(warning_msg, "iCCP: known incorrect sRGB profile") == 0) return; debug_printf("libpng warning: %s\n", warning_msg); } TIMER_ADD_CLIENT(tc_png_decode); // limitation: palette images aren't supported Status TexCodecPng::decode(u8* RESTRICT data, size_t size, Tex* RESTRICT t) const { TIMER_ACCRUE(tc_png_decode); png_infop info_ptr = 0; // allocate PNG structures; use default stderr and longjmp error handler, use custom // warning handler to filter out useless messages png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, user_warning_fn); if(!png_ptr) WARN_RETURN(ERR::FAIL); info_ptr = png_create_info_struct(png_ptr); if(!info_ptr) { png_destroy_read_struct(&png_ptr, &info_ptr, 0); WARN_RETURN(ERR::NO_MEM); } // setup error handling if(setjmp(png_jmpbuf(png_ptr))) { // libpng longjmps here after an error png_destroy_read_struct(&png_ptr, &info_ptr, 0); WARN_RETURN(ERR::FAIL); } MemoryStream stream(data, size); Status ret = png_decode_impl(&stream, png_ptr, info_ptr, t); png_destroy_read_struct(&png_ptr, &info_ptr, 0); return ret; } // limitation: palette images aren't supported Status TexCodecPng::encode(Tex* RESTRICT t, DynArray* RESTRICT da) const { png_infop info_ptr = 0; // allocate PNG structures; use default stderr and longjmp error handlers png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0); if(!png_ptr) WARN_RETURN(ERR::FAIL); info_ptr = png_create_info_struct(png_ptr); if(!info_ptr) { png_destroy_write_struct(&png_ptr, &info_ptr); WARN_RETURN(ERR::NO_MEM); } // setup error handling if(setjmp(png_jmpbuf(png_ptr))) { // libpng longjmps here after an error png_destroy_write_struct(&png_ptr, &info_ptr); WARN_RETURN(ERR::FAIL); } Status ret = png_encode_impl(t, png_ptr, info_ptr, da); png_destroy_write_struct(&png_ptr, &info_ptr); return ret; } Index: ps/trunk/source/lib/tex/tex_tga.cpp =================================================================== --- ps/trunk/source/lib/tex/tex_tga.cpp (revision 19502) +++ ps/trunk/source/lib/tex/tex_tga.cpp (revision 19503) @@ -1,175 +1,175 @@ -/* Copyright (c) 2017 Wildfire Games +/* Copyright (C) 2017 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. */ /* * TGA codec. */ #include "precompiled.h" #include "lib/byte_order.h" #include "tex_codec.h" #include "lib/bits.h" #pragma pack(push, 1) enum TgaImgType { TGA_TRUE_COLOR = 2, // uncompressed 24 or 32 bit direct RGB TGA_GREY = 3 // uncompressed 8 bit direct greyscale }; enum TgaImgDesc { TGA_RIGHT_TO_LEFT = BIT(4), TGA_TOP_DOWN = BIT(5), }; typedef struct { u8 img_id_len; // 0 - no image identifier present u8 color_map_type; // 0 - no color map present u8 img_type; // see TgaImgType u8 color_map[5]; // unused u16 x_origin; // unused u16 y_origin; // unused u16 w; u16 h; u8 bpp; // bits per pixel u8 img_desc; } TgaHeader; // TGA file: header [img id] [color map] image data #pragma pack(pop) Status TexCodecTga::transform(Tex* UNUSED(t), size_t UNUSED(transforms)) const { return INFO::TEX_CODEC_CANNOT_HANDLE; } bool TexCodecTga::is_hdr(const u8* file) const { TgaHeader* hdr = (TgaHeader*)file; // the first TGA header doesn't have a magic field; // we can only check if the first 4 bytes are valid // .. not direct color if(hdr->color_map_type != 0) return false; // .. wrong color type (not uncompressed greyscale or RGB) if(hdr->img_type != TGA_TRUE_COLOR && hdr->img_type != TGA_GREY) return false; // note: we can't check img_id_len or color_map[0] - they are // undefined and may assume any value. return true; } bool TexCodecTga::is_ext(const OsPath& extension) const { return extension == L".tga"; } size_t TexCodecTga::hdr_size(const u8* file) const { size_t hdr_size = sizeof(TgaHeader); if(file) { TgaHeader* hdr = (TgaHeader*)file; hdr_size += hdr->img_id_len; } return hdr_size; } // requirements: uncompressed, direct color, bottom up Status TexCodecTga::decode(u8* RESTRICT data, size_t UNUSED(size), Tex* RESTRICT t) const { const TgaHeader* hdr = (const TgaHeader*)data; const u8 type = hdr->img_type; const size_t w = read_le16(&hdr->w); const size_t h = read_le16(&hdr->h); const size_t bpp = hdr->bpp; const u8 desc = hdr->img_desc; size_t flags = 0; flags |= (desc & TGA_TOP_DOWN)? TEX_TOP_DOWN : TEX_BOTTOM_UP; if(desc & 0x0F) // alpha bits flags |= TEX_ALPHA; if(bpp == 8) flags |= TEX_GREY; if(type == TGA_TRUE_COLOR) flags |= TEX_BGR; // sanity checks // .. storing right-to-left is just stupid; // we're not going to bother converting it. if(desc & TGA_RIGHT_TO_LEFT) WARN_RETURN(ERR::TEX_INVALID_LAYOUT); t->m_Width = w; t->m_Height = h; t->m_Bpp = bpp; t->m_Flags = flags; return INFO::OK; } Status TexCodecTga::encode(Tex* RESTRICT t, DynArray* RESTRICT da) const { u8 img_desc = 0; if(t->m_Flags & TEX_TOP_DOWN) img_desc |= TGA_TOP_DOWN; if(t->m_Bpp == 32) img_desc |= 8; // size of alpha channel TgaImgType img_type = (t->m_Flags & TEX_GREY)? TGA_GREY : TGA_TRUE_COLOR; size_t transforms = t->m_Flags; transforms &= ~TEX_ORIENTATION; // no flip needed - we can set top-down bit. transforms ^= TEX_BGR; // TGA is native BGR. const TgaHeader hdr = { 0, // no image identifier present 0, // no color map present (u8)img_type, {0,0,0,0,0}, // unused (color map) 0, 0, // unused (origin) (u16)t->m_Width, (u16)t->m_Height, (u8)t->m_Bpp, img_desc }; const size_t hdr_size = sizeof(hdr); return tex_codec_write(t, transforms, &hdr, hdr_size, da); } Index: ps/trunk/source/network/NetHost.h =================================================================== --- ps/trunk/source/network/NetHost.h (revision 19502) +++ ps/trunk/source/network/NetHost.h (revision 19503) @@ -1,106 +1,106 @@ -/* Copyright (C) 2016 Wildfire Games. +/* Copyright (C) 2017 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 NETHOST_H #define NETHOST_H #include "ps/CStr.h" #include /** * @file * Various declarations shared by networking code. */ typedef struct _ENetPeer ENetPeer; typedef struct _ENetPacket ENetPacket; typedef struct _ENetHost ENetHost; class CNetMessage; struct PlayerAssignment { /** * Whether the player is currently connected and active. * (We retain information on disconnected players to support rejoining, * but don't transmit these to other clients.) */ bool m_Enabled; /// Player name CStrW m_Name; /// The player that the given host controls, or -1 if none (observer) i32 m_PlayerID; /// Status - Ready or not: 0 for not ready, 1 for ready u8 m_Status; }; typedef std::map PlayerAssignmentMap; // map from GUID -> assignment /** * Reasons sent by server to clients in disconnection messages. * Must be kept in sync with binaries/data/mods/public/gui/common/network.js */ enum NetDisconnectReason { NDR_UNKNOWN = 0, NDR_SERVER_SHUTDOWN, NDR_INCORRECT_PROTOCOL_VERSION, NDR_SERVER_LOADING, NDR_SERVER_ALREADY_IN_GAME, NDR_KICKED, NDR_BANNED, NDR_PLAYERNAME_IN_USE, NDR_SERVER_FULL, NDR_PLAYERGUID_IN_USE }; class CNetHost { public: static const int DEFAULT_CHANNEL = 0; /** * Transmit a message to the given peer. * @param message message to send * @param peer peer to send to * @param peerName name of peer for debug logs * @return true on success, false on failure */ static bool SendMessage(const CNetMessage* message, ENetPeer* peer, const char* peerName); /** * Construct an ENet packet by serialising the given message. * @return NULL on failure */ static ENetPacket* CreatePacket(const CNetMessage* message); /** * Initialize ENet. * This must be called before any other networking code. */ static void Initialize(); /** * Deinitialize ENet. */ static void Deinitialize(); }; #endif // NETHOST_H Index: ps/trunk/source/simulation2/components/CCmpOwnership.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpOwnership.cpp (revision 19502) +++ ps/trunk/source/simulation2/components/CCmpOwnership.cpp (revision 19503) @@ -1,104 +1,104 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2017 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 "simulation2/system/Component.h" #include "ICmpOwnership.h" #include "simulation2/MessageTypes.h" /** * Basic ICmpOwnership implementation. */ class CCmpOwnership : public ICmpOwnership { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_Destroy); } DEFAULT_COMPONENT_ALLOCATOR(Ownership) player_id_t m_Owner; static std::string GetSchema() { return "" "Allows this entity to be owned by players." ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { m_Owner = INVALID_PLAYER; } virtual void Deinit() { } virtual void Serialize(ISerializer& serialize) { serialize.NumberI32_Unbounded("owner", m_Owner); } virtual void Deserialize(const CParamNode& UNUSED(paramNode), IDeserializer& deserialize) { deserialize.NumberI32_Unbounded("owner", m_Owner); } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_Destroy: { // Reset the owner so this entity is e.g. removed from population counts SetOwner(INVALID_PLAYER); break; } } } virtual player_id_t GetOwner() const { return m_Owner; } virtual void SetOwner(player_id_t playerID) { if (playerID == m_Owner) return; player_id_t old = m_Owner; m_Owner = playerID; CMessageOwnershipChanged msg(GetEntityId(), old, playerID); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } virtual void SetOwnerQuiet(player_id_t playerID) { if (playerID != m_Owner) m_Owner = playerID; } }; REGISTER_COMPONENT_TYPE(Ownership) Index: ps/trunk/source/simulation2/components/CCmpTest.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpTest.cpp (revision 19502) +++ ps/trunk/source/simulation2/components/CCmpTest.cpp (revision 19503) @@ -1,246 +1,246 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2017 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 "simulation2/system/Component.h" #include "ICmpTest.h" #include "simulation2/scripting/ScriptComponent.h" #include "simulation2/MessageTypes.h" #include "ps/Profile.h" class CCmpTest1A : public ICmpTest1 { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_TurnStart); componentManager.SubscribeToMessageType(MT_Interpolate); componentManager.SubscribeToMessageType(MT_Destroy); } DEFAULT_COMPONENT_ALLOCATOR(Test1A) int32_t m_x; static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& paramNode) { if (paramNode.GetChild("x").IsOk()) m_x = paramNode.GetChild("x").ToInt(); else m_x = 11000; } virtual void Deinit() { } virtual void Serialize(ISerializer& serialize) { serialize.NumberI32_Unbounded("x", m_x); } virtual void Deserialize(const CParamNode& UNUSED(paramNode), IDeserializer& deserialize) { deserialize.NumberI32_Unbounded("x", m_x); } virtual int GetX() { return m_x; } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_Destroy: GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, false); break; case MT_TurnStart: m_x += 1; break; case MT_Interpolate: m_x += 2; break; default: m_x = 0; break; } } }; REGISTER_COMPONENT_TYPE(Test1A) class CCmpTest1B : public ICmpTest1 { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_Update); componentManager.SubscribeGloballyToMessageType(MT_Interpolate); } DEFAULT_COMPONENT_ALLOCATOR(Test1B) int32_t m_x; static std::string GetSchema() { return ""; } virtual void Init(const CParamNode&) { m_x = 12000; } virtual void Deinit() { } virtual void Serialize(ISerializer& serialize) { serialize.NumberI32_Unbounded("x", m_x); } virtual void Deserialize(const CParamNode& UNUSED(paramNode), IDeserializer& deserialize) { deserialize.NumberI32_Unbounded("x", m_x); } virtual int GetX() { return m_x; } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_Update: m_x += 10; break; case MT_Interpolate: m_x += 20; break; default: m_x = 0; break; } } }; REGISTER_COMPONENT_TYPE(Test1B) class CCmpTest2A : public ICmpTest2 { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_TurnStart); componentManager.SubscribeToMessageType(MT_Update); } DEFAULT_COMPONENT_ALLOCATOR(Test2A) int32_t m_x; static std::string GetSchema() { return ""; } virtual void Init(const CParamNode&) { m_x = 21000; } virtual void Deinit() { } virtual void Serialize(ISerializer& serialize) { serialize.NumberI32_Unbounded("x", m_x); } virtual void Deserialize(const CParamNode& UNUSED(paramNode), IDeserializer& deserialize) { deserialize.NumberI32_Unbounded("x", m_x); } virtual int GetX() { return m_x; } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_TurnStart: m_x += 50; break; case MT_Update: m_x += static_cast (msg).turnLength.ToInt_RoundToZero(); break; default: m_x = 0; break; } } }; REGISTER_COMPONENT_TYPE(Test2A) //////////////////////////////////////////////////////////////// class CCmpTest1Scripted : public ICmpTest1 { public: DEFAULT_SCRIPT_WRAPPER(Test1Scripted) virtual int GetX() { return m_Script.Call ("GetX"); } }; REGISTER_COMPONENT_SCRIPT_WRAPPER(Test1Scripted) //////////////////////////////////////////////////////////////// class CCmpTest2Scripted : public ICmpTest2 { public: DEFAULT_SCRIPT_WRAPPER(Test2Scripted) virtual int GetX() { return m_Script.Call ("GetX"); } }; REGISTER_COMPONENT_SCRIPT_WRAPPER(Test2Scripted) Index: ps/trunk/source/simulation2/components/ICmpRangeManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpRangeManager.cpp (revision 19502) +++ ps/trunk/source/simulation2/components/ICmpRangeManager.cpp (revision 19503) @@ -1,64 +1,64 @@ -/* Copyright (C) 2015 Wildfire Games. +/* Copyright (C) 2017 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 "ICmpRangeManager.h" #include "simulation2/system/InterfaceScripted.h" std::string ICmpRangeManager::GetLosVisibility_wrapper(entity_id_t ent, int player) const { ELosVisibility visibility = GetLosVisibility(ent, player); switch (visibility) { case VIS_HIDDEN: return "hidden"; case VIS_FOGGED: return "fogged"; case VIS_VISIBLE: return "visible"; default: return "error"; // should never happen } } BEGIN_INTERFACE_WRAPPER(RangeManager) DEFINE_INTERFACE_METHOD_5("ExecuteQuery", std::vector, ICmpRangeManager, ExecuteQuery, entity_id_t, entity_pos_t, entity_pos_t, std::vector, int) DEFINE_INTERFACE_METHOD_5("ExecuteQueryAroundPos", std::vector, ICmpRangeManager, ExecuteQueryAroundPos, CFixedVector2D, entity_pos_t, entity_pos_t, std::vector, int) DEFINE_INTERFACE_METHOD_6("CreateActiveQuery", ICmpRangeManager::tag_t, ICmpRangeManager, CreateActiveQuery, entity_id_t, entity_pos_t, entity_pos_t, std::vector, int, u8) DEFINE_INTERFACE_METHOD_7("CreateActiveParabolicQuery", ICmpRangeManager::tag_t, ICmpRangeManager, CreateActiveParabolicQuery, entity_id_t, entity_pos_t, entity_pos_t, entity_pos_t, std::vector, int, u8) DEFINE_INTERFACE_METHOD_1("DestroyActiveQuery", void, ICmpRangeManager, DestroyActiveQuery, ICmpRangeManager::tag_t) DEFINE_INTERFACE_METHOD_1("EnableActiveQuery", void, ICmpRangeManager, EnableActiveQuery, ICmpRangeManager::tag_t) DEFINE_INTERFACE_METHOD_1("DisableActiveQuery", void, ICmpRangeManager, DisableActiveQuery, ICmpRangeManager::tag_t) DEFINE_INTERFACE_METHOD_CONST_1("IsActiveQueryEnabled", bool, ICmpRangeManager, IsActiveQueryEnabled, ICmpRangeManager::tag_t) DEFINE_INTERFACE_METHOD_1("ResetActiveQuery", std::vector, ICmpRangeManager, ResetActiveQuery, ICmpRangeManager::tag_t) DEFINE_INTERFACE_METHOD_3("SetEntityFlag", void, ICmpRangeManager, SetEntityFlag, entity_id_t, std::string, bool) DEFINE_INTERFACE_METHOD_CONST_1("GetEntityFlagMask", u8, ICmpRangeManager, GetEntityFlagMask, std::string) DEFINE_INTERFACE_METHOD_CONST_1("GetEntitiesByPlayer", std::vector, ICmpRangeManager, GetEntitiesByPlayer, player_id_t) DEFINE_INTERFACE_METHOD_CONST_0("GetNonGaiaEntities", std::vector, ICmpRangeManager, GetNonGaiaEntities) DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpRangeManager, SetDebugOverlay, bool) DEFINE_INTERFACE_METHOD_1("ExploreAllTiles", void, ICmpRangeManager, ExploreAllTiles, player_id_t) DEFINE_INTERFACE_METHOD_0("ExploreTerritories", void, ICmpRangeManager, ExploreTerritories) DEFINE_INTERFACE_METHOD_2("SetLosRevealAll", void, ICmpRangeManager, SetLosRevealAll, player_id_t, bool) DEFINE_INTERFACE_METHOD_CONST_1("GetLosRevealAll", bool, ICmpRangeManager, GetLosRevealAll, player_id_t) DEFINE_INTERFACE_METHOD_CONST_5("GetElevationAdaptedRange", entity_pos_t, ICmpRangeManager, GetElevationAdaptedRange, CFixedVector3D, CFixedVector3D, entity_pos_t, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_2("ActivateScriptedVisibility", void, ICmpRangeManager, ActivateScriptedVisibility, entity_id_t, bool) DEFINE_INTERFACE_METHOD_CONST_2("GetLosVisibility", std::string, ICmpRangeManager, GetLosVisibility_wrapper, entity_id_t, player_id_t) DEFINE_INTERFACE_METHOD_1("RequestVisibilityUpdate", void, ICmpRangeManager, RequestVisibilityUpdate, entity_id_t) DEFINE_INTERFACE_METHOD_1("SetLosCircular", void, ICmpRangeManager, SetLosCircular, bool) DEFINE_INTERFACE_METHOD_CONST_0("GetLosCircular", bool, ICmpRangeManager, GetLosCircular) DEFINE_INTERFACE_METHOD_2("SetSharedLos", void, ICmpRangeManager, SetSharedLos, player_id_t, std::vector) DEFINE_INTERFACE_METHOD_CONST_1("GetPercentMapExplored", u8, ICmpRangeManager, GetPercentMapExplored, player_id_t) DEFINE_INTERFACE_METHOD_CONST_1("GetUnionPercentMapExplored", u8, ICmpRangeManager, GetUnionPercentMapExplored, std::vector) END_INTERFACE_WRAPPER(RangeManager) Index: ps/trunk/source/simulation2/helpers/LongPathfinder.cpp =================================================================== --- ps/trunk/source/simulation2/helpers/LongPathfinder.cpp (revision 19502) +++ ps/trunk/source/simulation2/helpers/LongPathfinder.cpp (revision 19503) @@ -1,1115 +1,1115 @@ -/* Copyright (C) 2015 Wildfire Games. +/* Copyright (C) 2017 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 "LongPathfinder.h" #include "lib/bits.h" #include "ps/Profile.h" #include "Geometry.h" /** * Jump point cache. * * The JPS algorithm wants to efficiently either find the first jump point * in some direction from some cell (not counting the cell itself), * if it is reachable without crossing any impassable cells; * or know that there is no such reachable jump point. * The jump point is always on a passable cell. * We cache that data to allow fast lookups, which helps performance * significantly (especially on sparse maps). * Recalculation might be expensive but the underlying passability data * changes relatively rarely. * * To allow the algorithm to detect goal cells, we want to treat them as * jump points too. (That means the algorithm will push those cells onto * its open queue, and will eventually pop a goal cell and realise it's done.) * (Goals might be circles/squares/etc, not just a single cell.) * But the goal generally changes for every path request, so we can't cache * it like the normal jump points. * Instead, if there's no jump point from some cell then we'll cache the * first impassable cell as an 'obstruction jump point' * (with a flag to distinguish from a real jump point), and then the caller * can test whether the goal includes a cell that's closer than the first * (obstruction or real) jump point, * and treat the goal cell as a jump point in that case. * * We only ever need to find the jump point relative to a passable cell; * the cache is allowed to return bogus values for impassable cells. */ class JumpPointCache { /** * Simple space-inefficient row storage. */ struct RowRaw { std::vector data; size_t GetMemoryUsage() const { return data.capacity() * sizeof(u16); } RowRaw(int length) { data.resize(length); } /** * Set cells x0 <= x < x1 to have jump point x1. */ void SetRange(int x0, int x1, bool obstruction) { ENSURE(0 <= x0 && x0 <= x1 && x1 < (int)data.size()); for (int x = x0; x < x1; ++x) data[x] = (x1 << 1) | (obstruction ? 1 : 0); } /** * Returns the coordinate of the next jump point xp (where x < xp), * and whether it's an obstruction point or jump point. */ void Get(int x, int& xp, bool& obstruction) { ENSURE(0 <= x && x < (int)data.size()); xp = data[x] >> 1; obstruction = data[x] & 1; } void Finish() { } }; struct RowTree { /** * Represents an interval [u15 x0, u16 x1) * with a boolean obstruction flag, * packed into a single u32. */ struct Interval { Interval() : data(0) { } Interval(int x0, int x1, bool obstruction) { ENSURE(0 <= x0 && x0 < 0x8000); ENSURE(0 <= x1 && x1 < 0x10000); data = ((u32)x0 << 17) | (u32)(obstruction ? 0x10000 : 0) | (u32)x1; } int x0() { return data >> 17; } int x1() { return data & 0xFFFF; } bool obstruction() { return (data & 0x10000) != 0; } u32 data; }; std::vector data; size_t GetMemoryUsage() const { return data.capacity() * sizeof(Interval); } RowTree(int UNUSED(length)) { } void SetRange(int x0, int x1, bool obstruction) { ENSURE(0 <= x0 && x0 <= x1); data.emplace_back(x0, x1, obstruction); } /** * Recursive helper function for Finish(). * Given two ranges [x0, pivot) and [pivot, x1) in the sorted array 'data', * the pivot element is added onto the binary tree (stored flattened in an * array), and then each range is split into two sub-ranges with a pivot in * the middle (to ensure the tree remains balanced) and ConstructTree recurses. */ void ConstructTree(std::vector& tree, size_t x0, size_t pivot, size_t x1, size_t idx_tree) { ENSURE(x0 < data.size()); ENSURE(x1 <= data.size()); ENSURE(x0 <= pivot); ENSURE(pivot < x1); ENSURE(idx_tree < tree.size()); tree[idx_tree] = data[pivot]; if (x0 < pivot) ConstructTree(tree, x0, (x0 + pivot) / 2, pivot, (idx_tree << 1) + 1); if (pivot + 1 < x1) ConstructTree(tree, pivot + 1, (pivot + x1) / 2, x1, (idx_tree << 1) + 2); } void Finish() { // Convert the sorted interval list into a balanced binary tree std::vector tree; if (!data.empty()) { size_t depth = ceil_log2(data.size() + 1); tree.resize((1 << depth) - 1); ConstructTree(tree, 0, data.size() / 2, data.size(), 0); } data.swap(tree); } void Get(int x, int& xp, bool& obstruction) { // Search the binary tree for an interval which contains x int i = 0; while (true) { ENSURE(i < (int)data.size()); Interval interval = data[i]; if (x < interval.x0()) i = (i << 1) + 1; else if (x >= interval.x1()) i = (i << 1) + 2; else { ENSURE(interval.x0() <= x && x < interval.x1()); xp = interval.x1(); obstruction = interval.obstruction(); return; } } } }; // Pick one of the row implementations typedef RowRaw Row; public: int m_Width; int m_Height; std::vector m_JumpPointsRight; std::vector m_JumpPointsLeft; std::vector m_JumpPointsUp; std::vector m_JumpPointsDown; /** * Compute the cached obstruction/jump points for each cell, * in a single direction. By default the code assumes the rightwards * (+i) direction; set 'transpose' to switch to upwards (+j), * and/or set 'mirror' to reverse the direction. */ void ComputeRows(std::vector& rows, const Grid& terrain, pass_class_t passClass, bool transpose, bool mirror) { int w = terrain.m_W; int h = terrain.m_H; if (transpose) std::swap(w, h); // Check the terrain passability, adjusted for transpose/mirror #define TERRAIN_IS_PASSABLE(i, j) \ IS_PASSABLE( \ mirror \ ? (transpose ? terrain.get((j), w-1-(i)) : terrain.get(w-1-(i), (j))) \ : (transpose ? terrain.get((j), (i)) : terrain.get((i), (j))) \ , passClass) rows.reserve(h); for (int j = 0; j < h; ++j) rows.emplace_back(w); for (int j = 1; j < h - 1; ++j) { // Find the first passable cell. // Then, find the next jump/obstruction point after that cell, // and store that point for the passable range up to that cell, // then repeat. int i = 0; while (i < w) { // Restart the 'while' loop until we reach a passable cell if (!TERRAIN_IS_PASSABLE(i, j)) { ++i; continue; } // i is now a passable cell; find the next jump/obstruction point. // (We assume the map is surrounded by impassable cells, so we don't // need to explicitly check for world bounds here.) int i0 = i; while (true) { ++i; // Check if we hit an obstructed tile if (!TERRAIN_IS_PASSABLE(i, j)) { rows[j].SetRange(i0, i, true); break; } // Check if we reached a jump point #if ACCEPT_DIAGONAL_GAPS if ((!TERRAIN_IS_PASSABLE(i, j - 1) && TERRAIN_IS_PASSABLE(i + 1, j - 1)) || (!TERRAIN_IS_PASSABLE(i, j + 1) && TERRAIN_IS_PASSABLE(i + 1, j + 1))) #else if ((!TERRAIN_IS_PASSABLE(i - 1, j - 1) && TERRAIN_IS_PASSABLE(i, j - 1)) || (!TERRAIN_IS_PASSABLE(i - 1, j + 1) && TERRAIN_IS_PASSABLE(i, j + 1))) #endif { rows[j].SetRange(i0, i, false); break; } } } rows[j].Finish(); } #undef TERRAIN_IS_PASSABLE } void reset(const Grid* terrain, pass_class_t passClass) { PROFILE3("JumpPointCache reset"); TIMER(L"JumpPointCache reset"); m_Width = terrain->m_W; m_Height = terrain->m_H; ComputeRows(m_JumpPointsRight, *terrain, passClass, false, false); ComputeRows(m_JumpPointsLeft, *terrain, passClass, false, true); ComputeRows(m_JumpPointsUp, *terrain, passClass, true, false); ComputeRows(m_JumpPointsDown, *terrain, passClass, true, true); } size_t GetMemoryUsage() const { size_t bytes = 0; for (int i = 0; i < m_Width; ++i) { bytes += m_JumpPointsUp[i].GetMemoryUsage(); bytes += m_JumpPointsDown[i].GetMemoryUsage(); } for (int j = 0; j < m_Height; ++j) { bytes += m_JumpPointsRight[j].GetMemoryUsage(); bytes += m_JumpPointsLeft[j].GetMemoryUsage(); } return bytes; } /** * Returns the next jump point (or goal point) to explore, * at (ip, j) where i < ip. * Returns i if there is no such point. */ int GetJumpPointRight(int i, int j, const PathGoal& goal) { int ip; bool obstruction; m_JumpPointsRight[j].Get(i, ip, obstruction); // Adjust ip to be a goal cell, if there is one closer than the jump point; // and then return the new ip if there is a goal, // or the old ip if there is a (non-obstruction) jump point if (goal.NavcellRectContainsGoal(i + 1, j, ip - 1, j, &ip, NULL) || !obstruction) return ip; return i; } int GetJumpPointLeft(int i, int j, const PathGoal& goal) { int mip; // mirrored value, because m_JumpPointsLeft is generated from a mirrored map bool obstruction; m_JumpPointsLeft[j].Get(m_Width - 1 - i, mip, obstruction); int ip = m_Width - 1 - mip; if (goal.NavcellRectContainsGoal(i - 1, j, ip + 1, j, &ip, NULL) || !obstruction) return ip; return i; } int GetJumpPointUp(int i, int j, const PathGoal& goal) { int jp; bool obstruction; m_JumpPointsUp[i].Get(j, jp, obstruction); if (goal.NavcellRectContainsGoal(i, j + 1, i, jp - 1, NULL, &jp) || !obstruction) return jp; return j; } int GetJumpPointDown(int i, int j, const PathGoal& goal) { int mjp; // mirrored value bool obstruction; m_JumpPointsDown[i].Get(m_Height - 1 - j, mjp, obstruction); int jp = m_Height - 1 - mjp; if (goal.NavcellRectContainsGoal(i, j - 1, i, jp + 1, NULL, &jp) || !obstruction) return jp; return j; } }; ////////////////////////////////////////////////////////// LongPathfinder::LongPathfinder() : m_UseJPSCache(false), m_Grid(NULL), m_GridSize(0), m_DebugOverlay(NULL), m_DebugGrid(NULL), m_DebugPath(NULL) { } LongPathfinder::~LongPathfinder() { SAFE_DELETE(m_DebugOverlay); SAFE_DELETE(m_DebugGrid); SAFE_DELETE(m_DebugPath); } #define PASSABLE(i, j) IS_PASSABLE(state.terrain->get(i, j), state.passClass) // Calculate heuristic cost from tile i,j to goal // (This ought to be an underestimate for correctness) PathCost LongPathfinder::CalculateHeuristic(int i, int j, int iGoal, int jGoal) { int di = abs(i - iGoal); int dj = abs(j - jGoal); int diag = std::min(di, dj); return PathCost(di - diag + dj - diag, diag); } // Do the A* processing for a neighbour tile i,j. void LongPathfinder::ProcessNeighbour(int pi, int pj, int i, int j, PathCost pg, PathfinderState& state) { // Reject impassable tiles if (!PASSABLE(i, j)) return; PathfindTile& n = state.tiles->get(i, j); if (n.IsClosed()) return; PathCost dg; if (pi == i) dg = PathCost::horizvert(abs(pj - j)); else if (pj == j) dg = PathCost::horizvert(abs(pi - i)); else { ASSERT(abs((int)pi - (int)i) == abs((int)pj - (int)j)); // must be 45 degrees dg = PathCost::diag(abs((int)pi - (int)i)); } PathCost g = pg + dg; // cost to this tile = cost to predecessor + delta from predecessor PathCost h = CalculateHeuristic(i, j, state.iGoal, state.jGoal); // If this is a new tile, compute the heuristic distance if (n.IsUnexplored()) { // Remember the best tile we've seen so far, in case we never actually reach the target if (h < state.hBest) { state.hBest = h; state.iBest = i; state.jBest = j; } } else { // If we've already seen this tile, and the new path to this tile does not have a // better cost, then stop now if (g >= n.GetCost()) return; // Otherwise, we have a better path. // If we've already added this tile to the open list: if (n.IsOpen()) { // This is a better path, so replace the old one with the new cost/parent PathCost gprev = n.GetCost(); n.SetCost(g); n.SetPred(pi, pj, i, j); state.open.promote(TileID(i, j), gprev + h, g + h, h); return; } } // Add it to the open list: n.SetStatusOpen(); n.SetCost(g); n.SetPred(pi, pj, i, j); PriorityQueue::Item t = { TileID(i, j), g + h, h }; state.open.push(t); } /* * In the JPS algorithm, after a tile is taken off the open queue, * we don't process every adjacent neighbour (as in standard A*). * Instead we only move in a subset of directions (depending on the * direction from the predecessor); and instead of moving by a single * cell, we move up to the next jump point in that direction. * The AddJumped... functions do this by calling ProcessNeighbour * on the jump point (if any) in a certain direction. * The HasJumped... functions return whether there is any jump point * in that direction. */ // JPS functions scan navcells towards one direction // OnTheWay tests whether we are scanning towards the right direction, to avoid useless scans inline bool OnTheWay(int i, int j, int di, int dj, const PathGoal& goal) { entity_pos_t hw, hh; // half width/height of goal bounding box CFixedVector2D hbb = Geometry::GetHalfBoundingBox(goal.u, goal.v, CFixedVector2D(goal.hw, goal.hh)); switch (goal.type) { case PathGoal::POINT: hw = fixed::Zero(); hh = fixed::Zero(); break; case PathGoal::CIRCLE: case PathGoal::INVERTED_CIRCLE: hw = goal.hw; hh = goal.hw; break; case PathGoal::SQUARE: case PathGoal::INVERTED_SQUARE: hw = hbb.X.Absolute(); hh = hbb.Y.Absolute(); break; NODEFAULT; } if (dj != 0) { // Farthest goal point, z-direction int gj = ((goal.z + (dj > 0 ? hh : -hh)) / Pathfinding::NAVCELL_SIZE).ToInt_RoundToNegInfinity(); if ((gj - j)*dj < 0) // we're not moving towards the goal return false; } else { if (j < ((goal.z - hh) / Pathfinding::NAVCELL_SIZE).ToInt_RoundToNegInfinity() || j >((goal.z + hh) / Pathfinding::NAVCELL_SIZE).ToInt_RoundToNegInfinity()) return false; } if (di != 0) { // Farthest goal point, x-direction int gi = ((goal.x + (di > 0 ? hw : -hw)) / Pathfinding::NAVCELL_SIZE).ToInt_RoundToNegInfinity(); if ((gi - i)*di < 0) // we're not moving towards the goal return false; } else { if (i < ((goal.x - hw) / Pathfinding::NAVCELL_SIZE).ToInt_RoundToNegInfinity() || i >((goal.x + hh) / Pathfinding::NAVCELL_SIZE).ToInt_RoundToNegInfinity()) return false; } return true; } void LongPathfinder::AddJumpedHoriz(int i, int j, int di, PathCost g, PathfinderState& state, bool detectGoal) { if (m_UseJPSCache) { int jump; if (di > 0) jump = state.jpc->GetJumpPointRight(i, j, state.goal); else jump = state.jpc->GetJumpPointLeft(i, j, state.goal); if (jump != i) ProcessNeighbour(i, j, jump, j, g, state); } else { ASSERT(di == 1 || di == -1); int ni = i + di; while (true) { if (!PASSABLE(ni, j)) break; if (detectGoal && state.goal.NavcellContainsGoal(ni, j)) { state.open.clear(); ProcessNeighbour(i, j, ni, j, g, state); break; } #if ACCEPT_DIAGONAL_GAPS if ((!PASSABLE(ni, j - 1) && PASSABLE(ni + di, j - 1)) || (!PASSABLE(ni, j + 1) && PASSABLE(ni + di, j + 1))) #else if ((!PASSABLE(ni - di, j - 1) && PASSABLE(ni, j - 1)) || (!PASSABLE(ni - di, j + 1) && PASSABLE(ni, j + 1))) #endif { ProcessNeighbour(i, j, ni, j, g, state); break; } ni += di; } } } // Returns the i-coordinate of the jump point if it exists, else returns i int LongPathfinder::HasJumpedHoriz(int i, int j, int di, PathfinderState& state, bool detectGoal) { if (m_UseJPSCache) { int jump; if (di > 0) jump = state.jpc->GetJumpPointRight(i, j, state.goal); else jump = state.jpc->GetJumpPointLeft(i, j, state.goal); return jump; } else { ASSERT(di == 1 || di == -1); int ni = i + di; while (true) { if (!PASSABLE(ni, j)) return i; if (detectGoal && state.goal.NavcellContainsGoal(ni, j)) { state.open.clear(); return ni; } #if ACCEPT_DIAGONAL_GAPS if ((!PASSABLE(ni, j - 1) && PASSABLE(ni + di, j - 1)) || (!PASSABLE(ni, j + 1) && PASSABLE(ni + di, j + 1))) #else if ((!PASSABLE(ni - di, j - 1) && PASSABLE(ni, j - 1)) || (!PASSABLE(ni - di, j + 1) && PASSABLE(ni, j + 1))) #endif return ni; ni += di; } } } void LongPathfinder::AddJumpedVert(int i, int j, int dj, PathCost g, PathfinderState& state, bool detectGoal) { if (m_UseJPSCache) { int jump; if (dj > 0) jump = state.jpc->GetJumpPointUp(i, j, state.goal); else jump = state.jpc->GetJumpPointDown(i, j, state.goal); if (jump != j) ProcessNeighbour(i, j, i, jump, g, state); } else { ASSERT(dj == 1 || dj == -1); int nj = j + dj; while (true) { if (!PASSABLE(i, nj)) break; if (detectGoal && state.goal.NavcellContainsGoal(i, nj)) { state.open.clear(); ProcessNeighbour(i, j, i, nj, g, state); break; } #if ACCEPT_DIAGONAL_GAPS if ((!PASSABLE(i - 1, nj) && PASSABLE(i - 1, nj + dj)) || (!PASSABLE(i + 1, nj) && PASSABLE(i + 1, nj + dj))) #else if ((!PASSABLE(i - 1, nj - dj) && PASSABLE(i - 1, nj)) || (!PASSABLE(i + 1, nj - dj) && PASSABLE(i + 1, nj))) #endif { ProcessNeighbour(i, j, i, nj, g, state); break; } nj += dj; } } } // Returns the j-coordinate of the jump point if it exists, else returns j int LongPathfinder::HasJumpedVert(int i, int j, int dj, PathfinderState& state, bool detectGoal) { if (m_UseJPSCache) { int jump; if (dj > 0) jump = state.jpc->GetJumpPointUp(i, j, state.goal); else jump = state.jpc->GetJumpPointDown(i, j, state.goal); return jump; } else { ASSERT(dj == 1 || dj == -1); int nj = j + dj; while (true) { if (!PASSABLE(i, nj)) return j; if (detectGoal && state.goal.NavcellContainsGoal(i, nj)) { state.open.clear(); return nj; } #if ACCEPT_DIAGONAL_GAPS if ((!PASSABLE(i - 1, nj) && PASSABLE(i - 1, nj + dj)) || (!PASSABLE(i + 1, nj) && PASSABLE(i + 1, nj + dj))) #else if ((!PASSABLE(i - 1, nj - dj) && PASSABLE(i - 1, nj)) || (!PASSABLE(i + 1, nj - dj) && PASSABLE(i + 1, nj))) #endif return nj; nj += dj; } } } /* * We never cache diagonal jump points - they're usually so frequent that * a linear search is about as cheap and avoids the setup cost and memory cost. */ void LongPathfinder::AddJumpedDiag(int i, int j, int di, int dj, PathCost g, PathfinderState& state) { // ProcessNeighbour(i, j, i + di, j + dj, g, state); // return; ASSERT(di == 1 || di == -1); ASSERT(dj == 1 || dj == -1); int ni = i + di; int nj = j + dj; bool detectGoal = OnTheWay(i, j, di, dj, state.goal); while (true) { // Stop if we hit an obstructed cell if (!PASSABLE(ni, nj)) return; // Stop if moving onto this cell caused us to // touch the corner of an obstructed cell #if !ACCEPT_DIAGONAL_GAPS if (!PASSABLE(ni - di, nj) || !PASSABLE(ni, nj - dj)) return; #endif // Process this cell if it's at the goal if (detectGoal && state.goal.NavcellContainsGoal(ni, nj)) { state.open.clear(); ProcessNeighbour(i, j, ni, nj, g, state); return; } #if ACCEPT_DIAGONAL_GAPS if ((!PASSABLE(ni - di, nj) && PASSABLE(ni - di, nj + dj)) || (!PASSABLE(ni, nj - dj) && PASSABLE(ni + di, nj - dj))) { ProcessNeighbour(i, j, ni, nj, g, state); return; } #endif int fi = HasJumpedHoriz(ni, nj, di, state, detectGoal ? OnTheWay(ni, nj, di, 0, state.goal) : false); int fj = HasJumpedVert(ni, nj, dj, state, detectGoal ? OnTheWay(ni, nj, 0, dj, state.goal) : false); if (fi != ni || fj != nj) { ProcessNeighbour(i, j, ni, nj, g, state); g += PathCost::diag(abs(ni - i)); if (fi != ni) ProcessNeighbour(ni, nj, fi, nj, g, state); if (fj != nj) ProcessNeighbour(ni, nj, ni, fj, g, state); return; } ni += di; nj += dj; } } #undef PASSABLE void LongPathfinder::ComputeJPSPath(entity_pos_t x0, entity_pos_t z0, const PathGoal& origGoal, pass_class_t passClass, WaypointPath& path) { PROFILE("ComputePathJPS"); PROFILE2_IFSPIKE("ComputePathJPS", 0.0002); PathfinderState state = { 0 }; state.jpc = m_JumpPointCache[passClass].get(); if (m_UseJPSCache && !state.jpc) { state.jpc = new JumpPointCache; state.jpc->reset(m_Grid, passClass); debug_printf("PATHFINDER: JPC memory: %d kB\n", (int)state.jpc->GetMemoryUsage() / 1024); m_JumpPointCache[passClass] = shared_ptr(state.jpc); } // Convert the start coordinates to tile indexes u16 i0, j0; Pathfinding::NearestNavcell(x0, z0, i0, j0, m_GridSize, m_GridSize); if (!IS_PASSABLE(m_Grid->get(i0, j0), passClass)) { // The JPS pathfinder requires units to be on passable tiles // (otherwise it might crash), so handle the supposedly-invalid // state specially m_PathfinderHier.FindNearestPassableNavcell(i0, j0, passClass); } state.goal = origGoal; // Make the goal reachable. This includes shortening the path if the goal is in a non-passable // region, transforming non-point goals to reachable point goals, etc. m_PathfinderHier.MakeGoalReachable(i0, j0, state.goal, passClass); // If we're already at the goal tile, then move directly to the exact goal coordinates if (state.goal.NavcellContainsGoal(i0, j0)) { path.m_Waypoints.emplace_back(Waypoint{ state.goal.x, state.goal.z }); return; } Pathfinding::NearestNavcell(state.goal.x, state.goal.z, state.iGoal, state.jGoal, m_GridSize, m_GridSize); state.passClass = passClass; state.steps = 0; state.tiles = new PathfindTileGrid(m_Grid->m_W, m_Grid->m_H); state.terrain = m_Grid; state.iBest = i0; state.jBest = j0; state.hBest = CalculateHeuristic(i0, j0, state.iGoal, state.jGoal); PriorityQueue::Item start = { TileID(i0, j0), PathCost() }; state.open.push(start); state.tiles->get(i0, j0).SetStatusOpen(); state.tiles->get(i0, j0).SetPred(i0, j0, i0, j0); state.tiles->get(i0, j0).SetCost(PathCost()); while (true) { ++state.steps; // If we ran out of tiles to examine, give up if (state.open.empty()) break; // Move best tile from open to closed PriorityQueue::Item curr = state.open.pop(); u16 i = curr.id.i(); u16 j = curr.id.j(); state.tiles->get(i, j).SetStatusClosed(); // If we've reached the destination, stop if (state.goal.NavcellContainsGoal(i, j)) { state.iBest = i; state.jBest = j; state.hBest = PathCost(); break; } PathfindTile tile = state.tiles->get(i, j); PathCost g = tile.GetCost(); // Get the direction of the predecessor tile from this tile int dpi = tile.GetPredDI(); int dpj = tile.GetPredDJ(); dpi = (dpi < 0 ? -1 : dpi > 0 ? 1 : 0); dpj = (dpj < 0 ? -1 : dpj > 0 ? 1 : 0); if (dpi != 0 && dpj == 0) { // Moving horizontally from predecessor #if ACCEPT_DIAGONAL_GAPS if (!IS_PASSABLE(state.terrain->get(i, j-1), state.passClass)) AddJumpedDiag(i, j, -dpi, -1, g, state); if (!IS_PASSABLE(state.terrain->get(i, j+1), state.passClass)) AddJumpedDiag(i, j, -dpi, +1, g, state); #else if (!IS_PASSABLE(state.terrain->get(i + dpi, j-1), state.passClass)) { AddJumpedDiag(i, j, -dpi, -1, g, state); AddJumpedVert(i, j, -1, g, state, OnTheWay(i, j, 0, -1, state.goal)); } if (!IS_PASSABLE(state.terrain->get(i + dpi, j+1), state.passClass)) { AddJumpedDiag(i, j, -dpi, +1, g, state); AddJumpedVert(i, j, +1, g, state, OnTheWay(i, j, 0, +1, state.goal)); } #endif AddJumpedHoriz(i, j, -dpi, g, state, OnTheWay(i, j, -dpi, 0, state.goal)); } else if (dpi == 0 && dpj != 0) { // Moving vertically from predecessor #if ACCEPT_DIAGONAL_GAPS if (!IS_PASSABLE(state.terrain->get(i-1, j), state.passClass)) AddJumpedDiag(i, j, -1, -dpj, g, state); if (!IS_PASSABLE(state.terrain->get(i+1, j), state.passClass)) AddJumpedDiag(i, j, +1, -dpj, g, state); #else if (!IS_PASSABLE(state.terrain->get(i-1, j + dpj), state.passClass)) { AddJumpedDiag(i, j, -1, -dpj, g, state); AddJumpedHoriz(i, j, -1, g, state,OnTheWay(i, j, -1, 0, state.goal)); } if (!IS_PASSABLE(state.terrain->get(i+1, j + dpj), state.passClass)) { AddJumpedDiag(i, j, +1, -dpj, g, state); AddJumpedHoriz(i, j, +1, g, state,OnTheWay(i, j, +1, 0, state.goal)); } #endif AddJumpedVert(i, j, -dpj, g, state, OnTheWay(i, j, 0, -dpj, state.goal)); } else if (dpi != 0 && dpj != 0) { // Moving diagonally from predecessor #if ACCEPT_DIAGONAL_GAPS if (!IS_PASSABLE(state.terrain->get(i + dpi, j), state.passClass)) AddJumpedDiag(i, j, dpi, -dpj, g, state); if (!IS_PASSABLE(state.terrain->get(i, j + dpj), state.passClass)) AddJumpedDiag(i, j, -dpi, dpj, g, state); #endif AddJumpedHoriz(i, j, -dpi, g, state, OnTheWay(i, j, -dpi, 0, state.goal)); AddJumpedVert(i, j, -dpj, g, state, OnTheWay(i, j, 0, -dpj, state.goal)); AddJumpedDiag(i, j, -dpi, -dpj, g, state); } else { // No predecessor, i.e. the start tile // Start searching in every direction // XXX - check passability? bool passl = IS_PASSABLE(state.terrain->get(i-1, j), state.passClass); bool passr = IS_PASSABLE(state.terrain->get(i+1, j), state.passClass); bool passd = IS_PASSABLE(state.terrain->get(i, j-1), state.passClass); bool passu = IS_PASSABLE(state.terrain->get(i, j+1), state.passClass); if (passl && passd) ProcessNeighbour(i, j, i-1, j-1, g, state); if (passr && passd) ProcessNeighbour(i, j, i+1, j-1, g, state); if (passl && passu) ProcessNeighbour(i, j, i-1, j+1, g, state); if (passr && passu) ProcessNeighbour(i, j, i+1, j+1, g, state); if (passl) ProcessNeighbour(i, j, i-1, j, g, state); if (passr) ProcessNeighbour(i, j, i+1, j, g, state); if (passd) ProcessNeighbour(i, j, i, j-1, g, state); if (passu) ProcessNeighbour(i, j, i, j+1, g, state); } } // Reconstruct the path (in reverse) u16 ip = state.iBest, jp = state.jBest; while (ip != i0 || jp != j0) { PathfindTile& n = state.tiles->get(ip, jp); entity_pos_t x, z; Pathfinding::NavcellCenter(ip, jp, x, z); path.m_Waypoints.emplace_back(Waypoint{ x, z }); // Follow the predecessor link ip = n.GetPredI(ip); jp = n.GetPredJ(jp); } // The last waypoint is slightly incorrect (it's not the goal but the center // of the navcell of the goal), so replace it if (!path.m_Waypoints.empty()) path.m_Waypoints.front() = { state.goal.x, state.goal.z }; ImprovePathWaypoints(path, passClass, origGoal.maxdist, x0, z0); // Save this grid for debug display delete m_DebugGrid; m_DebugGrid = state.tiles; m_DebugSteps = state.steps; m_DebugGoal = state.goal; } void LongPathfinder::ImprovePathWaypoints(WaypointPath& path, pass_class_t passClass, entity_pos_t maxDist, entity_pos_t x0, entity_pos_t z0) { if (path.m_Waypoints.empty()) return; if (maxDist > fixed::Zero()) { CFixedVector2D start(x0, z0); CFixedVector2D first(path.m_Waypoints.back().x, path.m_Waypoints.back().z); CFixedVector2D offset = first - start; if (offset.CompareLength(maxDist) > 0) { offset.Normalize(maxDist); path.m_Waypoints.emplace_back(Waypoint{ (start + offset).X, (start + offset).Y }); } } if (path.m_Waypoints.size() < 2) return; std::vector& waypoints = path.m_Waypoints; std::vector newWaypoints; CFixedVector2D prev(waypoints.front().x, waypoints.front().z); newWaypoints.push_back(waypoints.front()); for (size_t k = 1; k < waypoints.size() - 1; ++k) { CFixedVector2D ahead(waypoints[k + 1].x, waypoints[k + 1].z); CFixedVector2D curr(waypoints[k].x, waypoints[k].z); if (maxDist > fixed::Zero() && (curr - prev).CompareLength(maxDist) > 0) { // We are too far away from the previous waypoint, so create one in // between and continue with the improvement of the path prev = prev + (curr - prev) / 2; newWaypoints.emplace_back(Waypoint{ prev.X, prev.Y }); } // If we're mostly straight, don't even bother. if ((ahead - curr).Perpendicular().Dot(curr - prev).Absolute() <= fixed::Epsilon() * 100) continue; if (!Pathfinding::CheckLineMovement(prev.X, prev.Y, ahead.X, ahead.Y, passClass, *m_Grid)) { prev = CFixedVector2D(waypoints[k].x, waypoints[k].z); newWaypoints.push_back(waypoints[k]); } } newWaypoints.push_back(waypoints.back()); path.m_Waypoints.swap(newWaypoints); } void LongPathfinder::GetDebugDataJPS(u32& steps, double& time, Grid& grid) const { steps = m_DebugSteps; time = m_DebugTime; if (!m_DebugGrid) return; u16 iGoal, jGoal; Pathfinding::NearestNavcell(m_DebugGoal.x, m_DebugGoal.z, iGoal, jGoal, m_GridSize, m_GridSize); grid = Grid(m_DebugGrid->m_W, m_DebugGrid->m_H); for (u16 j = 0; j < grid.m_H; ++j) { for (u16 i = 0; i < grid.m_W; ++i) { if (i == iGoal && j == jGoal) continue; PathfindTile t = m_DebugGrid->get(i, j); grid.set(i, j, (t.IsOpen() ? 1 : 0) | (t.IsClosed() ? 2 : 0)); } } } void LongPathfinder::SetDebugOverlay(bool enabled) { if (enabled && !m_DebugOverlay) m_DebugOverlay = new LongOverlay(*this); else if (!enabled && m_DebugOverlay) SAFE_DELETE(m_DebugOverlay); } void LongPathfinder::ComputePath(entity_pos_t x0, entity_pos_t z0, const PathGoal& origGoal, pass_class_t passClass, std::vector excludedRegions, WaypointPath& path) { GenerateSpecialMap(passClass, excludedRegions); ComputePath(x0, z0, origGoal, SPECIAL_PASS_CLASS, path); } inline bool InRegion(u16 i, u16 j, CircularRegion region) { fixed cellX = Pathfinding::NAVCELL_SIZE * i; fixed cellZ = Pathfinding::NAVCELL_SIZE * j; return CFixedVector2D(cellX - region.x, cellZ - region.z).CompareLength(region.r) <= 0; } void LongPathfinder::GenerateSpecialMap(pass_class_t passClass, std::vector excludedRegions) { for (u16 j = 0; j < m_Grid->m_H; ++j) { for (u16 i = 0; i < m_Grid->m_W; ++i) { NavcellData n = m_Grid->get(i, j); if (!IS_PASSABLE(n, passClass)) { n |= SPECIAL_PASS_CLASS; m_Grid->set(i, j, n); continue; } for (CircularRegion& region : excludedRegions) { if (!InRegion(i, j, region)) continue; n |= SPECIAL_PASS_CLASS; break; } m_Grid->set(i, j, n); } } } Index: ps/trunk/source/simulation2/helpers/LongPathfinder.h =================================================================== --- ps/trunk/source/simulation2/helpers/LongPathfinder.h (revision 19502) +++ ps/trunk/source/simulation2/helpers/LongPathfinder.h (revision 19503) @@ -1,398 +1,398 @@ -/* Copyright (C) 2015 Wildfire Games. +/* Copyright (C) 2017 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_LONGPATHFINDER #define INCLUDED_LONGPATHFINDER #include "Pathfinding.h" #include "HierarchicalPathfinder.h" #include "PriorityQueue.h" #include "graphics/Overlay.h" #include "renderer/Scene.h" #define ACCEPT_DIAGONAL_GAPS 0 /** * Represents the 2D coordinates of a tile. * The i/j components are packed into a single u32, since we usually use these * objects for equality comparisons and the VC2010 optimizer doesn't seem to automatically * compare two u16s in a single operation. * TODO: maybe VC2012 will? */ struct TileID { TileID() { } TileID(u16 i, u16 j) : data((i << 16) | j) { } bool operator==(const TileID& b) const { return data == b.data; } /// Returns lexicographic order over (i,j) bool operator<(const TileID& b) const { return data < b.data; } u16 i() const { return data >> 16; } u16 j() const { return data & 0xFFFF; } private: u32 data; }; /** * Tile data for A* computation. * (We store an array of one of these per terrain tile, so it ought to be optimised for size) */ struct PathfindTile { public: enum { STATUS_UNEXPLORED = 0, STATUS_OPEN = 1, STATUS_CLOSED = 2 }; bool IsUnexplored() const { return GetStatus() == STATUS_UNEXPLORED; } bool IsOpen() const { return GetStatus() == STATUS_OPEN; } bool IsClosed() const { return GetStatus() == STATUS_CLOSED; } void SetStatusOpen() { SetStatus(STATUS_OPEN); } void SetStatusClosed() { SetStatus(STATUS_CLOSED); } // Get pi,pj coords of predecessor to this tile on best path, given i,j coords of this tile inline int GetPredI(int i) const { return i + GetPredDI(); } inline int GetPredJ(int j) const { return j + GetPredDJ(); } inline PathCost GetCost() const { return g; } inline void SetCost(PathCost cost) { g = cost; } private: PathCost g; // cost to reach this tile u32 data; // 2-bit status; 15-bit PredI; 15-bit PredJ; packed for storage efficiency public: inline u8 GetStatus() const { return data & 3; } inline void SetStatus(u8 s) { ASSERT(s < 4); data &= ~3; data |= (s & 3); } int GetPredDI() const { return (i32)data >> 17; } int GetPredDJ() const { return ((i32)data << 15) >> 17; } // Set the pi,pj coords of predecessor, given i,j coords of this tile inline void SetPred(int pi, int pj, int i, int j) { int di = pi - i; int dj = pj - j; ASSERT(-16384 <= di && di < 16384); ASSERT(-16384 <= dj && dj < 16384); data &= 3; data |= (((u32)di & 0x7FFF) << 17) | (((u32)dj & 0x7FFF) << 2); } }; struct CircularRegion { CircularRegion(entity_pos_t x, entity_pos_t z, entity_pos_t r) : x(x), z(z), r(r) {} entity_pos_t x, z, r; }; typedef PriorityQueueHeap PriorityQueue; typedef SparseGrid PathfindTileGrid; class JumpPointCache; struct PathfinderState { u32 steps; // number of algorithm iterations PathGoal goal; u16 iGoal, jGoal; // goal tile pass_class_t passClass; PriorityQueue open; // (there's no explicit closed list; it's encoded in PathfindTile) PathfindTileGrid* tiles; Grid* terrain; PathCost hBest; // heuristic of closest discovered tile to goal u16 iBest, jBest; // closest tile JumpPointCache* jpc; }; class LongOverlay; class LongPathfinder { public: LongPathfinder(); ~LongPathfinder(); void SetDebugOverlay(bool enabled); void SetHierDebugOverlay(bool enabled, const CSimContext *simContext) { m_PathfinderHier.SetDebugOverlay(enabled, simContext); } void SetDebugPath(entity_pos_t x0, entity_pos_t z0, const PathGoal& goal, pass_class_t passClass) { if (!m_DebugOverlay) return; SAFE_DELETE(m_DebugGrid); delete m_DebugPath; m_DebugPath = new WaypointPath(); ComputePath(x0, z0, goal, passClass, *m_DebugPath); m_DebugPassClass = passClass; } void Reload(Grid* passabilityGrid, const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks) { m_Grid = passabilityGrid; ASSERT(passabilityGrid->m_H == passabilityGrid->m_W); m_GridSize = passabilityGrid->m_W; m_JumpPointCache.clear(); m_PathfinderHier.Recompute(passabilityGrid, nonPathfindingPassClassMasks, pathfindingPassClassMasks); } void Update(Grid* passabilityGrid, const Grid& dirtinessGrid) { m_Grid = passabilityGrid; ASSERT(passabilityGrid->m_H == passabilityGrid->m_W); ASSERT(m_GridSize == passabilityGrid->m_H); m_JumpPointCache.clear(); m_PathfinderHier.Update(passabilityGrid, dirtinessGrid); } void HierarchicalRenderSubmit(SceneCollector& collector) { for (size_t i = 0; i < m_PathfinderHier.m_DebugOverlayLines.size(); ++i) collector.Submit(&m_PathfinderHier.m_DebugOverlayLines[i]); } /** * Compute a tile-based path from the given point to the goal, and return the set of waypoints. * The waypoints correspond to the centers of horizontally/vertically adjacent tiles * along the path. */ void ComputePath(entity_pos_t x0, entity_pos_t z0, const PathGoal& origGoal, pass_class_t passClass, WaypointPath& path) { if (!m_Grid) { LOGERROR("The pathfinder grid hasn't been setup yet, aborting ComputePath"); return; } ComputeJPSPath(x0, z0, origGoal, passClass, path); } /** * Compute a tile-based path from the given point to the goal, excluding the regions * specified in excludedRegions (which are treated as impassable) and return the set of waypoints. * The waypoints correspond to the centers of horizontally/vertically adjacent tiles * along the path. */ void ComputePath(entity_pos_t x0, entity_pos_t z0, const PathGoal& origGoal, pass_class_t passClass, std::vector excludedRegions, WaypointPath& path); Grid GetConnectivityGrid(pass_class_t passClass) { return m_PathfinderHier.GetConnectivityGrid(passClass); } void GetDebugData(u32& steps, double& time, Grid& grid) const { GetDebugDataJPS(steps, time, grid); } Grid* m_Grid; u16 m_GridSize; // Debugging - output from last pathfind operation: LongOverlay* m_DebugOverlay; PathfindTileGrid* m_DebugGrid; u32 m_DebugSteps; double m_DebugTime; PathGoal m_DebugGoal; WaypointPath* m_DebugPath; pass_class_t m_DebugPassClass; private: PathCost CalculateHeuristic(int i, int j, int iGoal, int jGoal); void ProcessNeighbour(int pi, int pj, int i, int j, PathCost pg, PathfinderState& state); /** * JPS algorithm helper functions * @param detectGoal is not used if m_UseJPSCache is true */ void AddJumpedHoriz(int i, int j, int di, PathCost g, PathfinderState& state, bool detectGoal); int HasJumpedHoriz(int i, int j, int di, PathfinderState& state, bool detectGoal); void AddJumpedVert(int i, int j, int dj, PathCost g, PathfinderState& state, bool detectGoal); int HasJumpedVert(int i, int j, int dj, PathfinderState& state, bool detectGoal); void AddJumpedDiag(int i, int j, int di, int dj, PathCost g, PathfinderState& state); /** * See LongPathfinder.cpp for implementation details * TODO: cleanup documentation */ void ComputeJPSPath(entity_pos_t x0, entity_pos_t z0, const PathGoal& origGoal, pass_class_t passClass, WaypointPath& path); void GetDebugDataJPS(u32& steps, double& time, Grid& grid) const; // Helper functions for ComputePath /** * Given a path with an arbitrary collection of waypoints, updates the * waypoints to be nicer. Calls "Testline" between waypoints * so that bended paths can become straight if there's nothing in between * (this happens because A* is 8-direction, and the map isn't actually a grid). * If @param maxDist is non-zero, path waypoints will be espaced by at most @param maxDist. * In that case the distance between (x0, z0) and the first waypoint will also be made less than maxDist. */ void ImprovePathWaypoints(WaypointPath& path, pass_class_t passClass, entity_pos_t maxDist, entity_pos_t x0, entity_pos_t z0); /** * Generate a passability map, stored in the 16th bit of navcells, based on passClass, * but with a set of impassable circular regions. */ void GenerateSpecialMap(pass_class_t passClass, std::vector excludedRegions); bool m_UseJPSCache; std::map > m_JumpPointCache; HierarchicalPathfinder m_PathfinderHier; }; /** * Terrain overlay for pathfinder debugging. * Renders a representation of the most recent pathfinding operation. */ class LongOverlay : public TerrainTextureOverlay { public: LongPathfinder& m_Pathfinder; LongOverlay(LongPathfinder& pathfinder) : TerrainTextureOverlay(Pathfinding::NAVCELLS_PER_TILE), m_Pathfinder(pathfinder) { } virtual void BuildTextureRGBA(u8* data, size_t w, size_t h) { // Grab the debug data for the most recently generated path u32 steps; double time; Grid debugGrid; m_Pathfinder.GetDebugData(steps, time, debugGrid); // Render navcell passability u8* p = data; for (size_t j = 0; j < h; ++j) { for (size_t i = 0; i < w; ++i) { SColor4ub color(0, 0, 0, 0); if (!IS_PASSABLE(m_Pathfinder.m_Grid->get((int)i, (int)j), m_Pathfinder.m_DebugPassClass)) color = SColor4ub(255, 0, 0, 127); if (debugGrid.m_W && debugGrid.m_H) { u8 n = debugGrid.get((int)i, (int)j); if (n == 1) color = SColor4ub(255, 255, 0, 127); else if (n == 2) color = SColor4ub(0, 255, 0, 127); if (m_Pathfinder.m_DebugGoal.NavcellContainsGoal(i, j)) color = SColor4ub(0, 0, 255, 127); } *p++ = color.R; *p++ = color.G; *p++ = color.B; *p++ = color.A; } } // Render the most recently generated path if (m_Pathfinder.m_DebugPath && !m_Pathfinder.m_DebugPath->m_Waypoints.empty()) { std::vector& waypoints = m_Pathfinder.m_DebugPath->m_Waypoints; u16 ip = 0, jp = 0; for (size_t k = 0; k < waypoints.size(); ++k) { u16 i, j; Pathfinding::NearestNavcell(waypoints[k].x, waypoints[k].z, i, j, m_Pathfinder.m_GridSize, m_Pathfinder.m_GridSize); if (k == 0) { ip = i; jp = j; } else { bool firstCell = true; do { if (data[(jp*w + ip)*4+3] == 0) { data[(jp*w + ip)*4+0] = 0xFF; data[(jp*w + ip)*4+1] = 0xFF; data[(jp*w + ip)*4+2] = 0xFF; data[(jp*w + ip)*4+3] = firstCell ? 0xA0 : 0x60; } ip = ip < i ? ip+1 : ip > i ? ip-1 : ip; jp = jp < j ? jp+1 : jp > j ? jp-1 : jp; firstCell = false; } while (ip != i || jp != j); } } } } }; #endif // INCLUDED_LONGPATHFINDER Index: ps/trunk/source/tools/atlas/GameInterface/ActorViewer.h =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/ActorViewer.h (revision 19502) +++ ps/trunk/source/tools/atlas/GameInterface/ActorViewer.h (revision 19503) @@ -1,57 +1,57 @@ -/* Copyright (C) 2012 Wildfire Games. +/* Copyright (C) 2017 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_ACTORVIEWER #define INCLUDED_ACTORVIEWER #include "simulation2/system/Entity.h" #include "simulation2/helpers/Player.h" struct ActorViewerImpl; struct SColor4ub; class CSimulation2; class CStrW; class ActorViewer { NONCOPYABLE(ActorViewer); public: ActorViewer(); ~ActorViewer(); CSimulation2* GetSimulation2(); entity_id_t GetEntity(); void SetActor(const CStrW& id, const CStrW& animation, player_id_t playerID); void SetEnabled(bool enabled); void UnloadObjects(); void SetBackgroundColor(const SColor4ub& color); void SetWalkEnabled(bool enabled); void SetGroundEnabled(bool enabled); void SetWaterEnabled(bool enabled); void SetShadowsEnabled(bool enabled); void SetStatsEnabled(bool enabled); void SetBoundingBoxesEnabled(bool enabled); void SetAxesMarkerEnabled(bool enabled); void SetPropPointsMode(int mode); void Render(); void Update(float simFrameLength, float realFrameLength); private: ActorViewerImpl& m; }; #endif // INCLUDED_ACTORVIEWER Index: ps/trunk/source/tools/atlas/GameInterface/GameLoop.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/GameLoop.cpp (revision 19502) +++ ps/trunk/source/tools/atlas/GameInterface/GameLoop.cpp (revision 19503) @@ -1,334 +1,334 @@ -/* Copyright (C) 2013 Wildfire Games. +/* Copyright (C) 2017 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 "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/GameSetup/Paths.h" #include "renderer/Renderer.h" using namespace AtlasMessage; namespace AtlasMessage { extern void RegisterHandlers(); } // Loaded from DLL: void (*Atlas_StartWindow)(const wchar_t* type); void (*Atlas_SetDataDirectory)(const wchar_t* path); void (*Atlas_SetConfigDirectory)(const wchar_t* path); void (*Atlas_SetMessagePasser)(MessagePasser*); void (*Atlas_GLSetCurrent)(void* cavas); void (*Atlas_GLSwapBuffers)(void* canvas); void (*Atlas_NotifyEndOfFrame)(); void (*Atlas_DisplayError)(const wchar_t* text, size_t flags); void (*Atlas_ReportError)(); namespace AtlasMessage { void* (*ShareableMallocFptr)(size_t); void (*ShareableFreeFptr)(void*); } MessagePasser* AtlasMessage::g_MessagePasser = NULL; static InputProcessor g_Input; static GameLoopState state; GameLoopState* g_AtlasGameLoop = &state; static ErrorReactionInternal AtlasDisplayError(const wchar_t* text, size_t flags) { // TODO: after Atlas has been unloaded, don't do this Atlas_DisplayError(text, flags); return ERI_CONTINUE; } static void RendererIncrementalLoad() { // TODO: shouldn't duplicate this code from main.cpp if (!CRenderer::IsInitialised()) return; const double maxTime = 0.1f; double startTime = timer_Time(); bool more; do { more = g_Renderer.GetTextureManager().MakeProgress(); } while (more && timer_Time() - startTime < maxTime); } static void* RunEngine(void* data) { debug_SetThreadName("engine_thread"); // Set new main thread so that all the thread-safety checks pass ThreadUtil::SetMainThread(); g_Profiler2.RegisterCurrentThread("atlasmain"); const CmdLineArgs args = *reinterpret_cast(data); MessagePasserImpl* msgPasser = (MessagePasserImpl*)AtlasMessage::g_MessagePasser; // Register all the handlers for message which might be passed back RegisterHandlers(); // Override ah_display_error to pass all errors to the Atlas UI // TODO: this doesn't work well because it doesn't pause the game thread // and the error box is ugly, so only use it if we fix those issues // (use INIT_HAVE_DISPLAY_ERROR init flag to test this) AppHooks hooks = {0}; hooks.display_error = AtlasDisplayError; app_hooks_update(&hooks); // Disable the game's cursor rendering extern CStrW g_CursorName; g_CursorName = L""; state.args = args; state.running = true; state.view = AtlasView::GetView_None(); state.glCanvas = NULL; double last_activity = timer_Time(); while (state.running) { bool recent_activity = false; ////////////////////////////////////////////////////////////////////////// // (TODO: Work out why these things have to be in this order (to avoid // jumps when starting to move, etc)) // Calculate frame length { const double time = timer_Time(); static double last_time = time; const double realFrameLength = time-last_time; last_time = time; ENSURE(realFrameLength >= 0.0); // TODO: filter out big jumps, e.g. when having done a lot of slow // processing in the last frame state.realFrameLength = realFrameLength; } // Process the input that was received in the past if (g_Input.ProcessInput(&state)) recent_activity = true; ////////////////////////////////////////////////////////////////////////// { IMessage* msg; while ((msg = msgPasser->Retrieve()) != NULL) { recent_activity = true; std::string name (msg->GetName()); msgHandlers::const_iterator it = GetMsgHandlers().find(name); if (it != GetMsgHandlers().end()) { it->second(msg); } else { debug_warn(L"Unrecognised message"); // CLogger might not be initialised, but this error will be sent // to the debug output window anyway so people can still see it LOGERROR("Unrecognised message (%s)", name.c_str()); } if (msg->GetType() == IMessage::Query) { // For queries, we need to notify MessagePasserImpl::Query // that the query has now been processed. sem_post((sem_t*) static_cast(msg)->m_Semaphore); // (msg may have been destructed at this point, so don't use it again) // It's quite possible that the querier is going to do a tiny // bit of processing on the query results and then issue another // query, and repeat lots of times in a loop. To avoid slowing // that down by rendering between every query, make this // thread yield now. SDL_Delay(0); } else { // For non-queries, we need to delete the object, since we // took ownership of it. AtlasMessage::ShareableDelete(msg); } } } // Exit, if desired if (! state.running) break; ////////////////////////////////////////////////////////////////////////// // Do per-frame processing: ReloadChangedFiles(); RendererIncrementalLoad(); // Pump SDL events (e.g. hotkeys) SDL_Event_ ev; while (in_poll_priority_event(&ev)) in_dispatch_event(&ev); if (g_GUI) g_GUI->TickObjects(); state.view->Update(state.realFrameLength); state.view->Render(); if (CProfileManager::IsInitialised()) g_Profiler.Frame(); double time = timer_Time(); if (recent_activity) last_activity = time; // Be nice to the processor (by sleeping lots) if we're not doing anything // useful, and nice to the user (by just yielding to other threads) if we are bool yield = (time - last_activity > 0.5); // But make sure we aren't doing anything interesting right now, where // the user wants to see the screen updating even though they're not // interacting with it if (state.view->WantsHighFramerate()) yield = false; if (yield) // if there was no recent activity... { double sleepUntil = time + 0.5; // only redraw at 2fps while (time < sleepUntil) { // To minimise latency when the user starts doing stuff, only // sleep for a short while, then check if anything's happened, // then go back to sleep // (TODO: This should probably be done with something like semaphores) Atlas_NotifyEndOfFrame(); // (TODO: rename to NotifyEndOfQuiteShortProcessingPeriodSoPleaseSendMeNewMessages or something) SDL_Delay(50); if (!msgPasser->IsEmpty()) break; time = timer_Time(); } } else { Atlas_NotifyEndOfFrame(); SDL_Delay(0); } } return NULL; } bool BeginAtlas(const CmdLineArgs& args, const DllLoader& dll) { // Load required symbols from the DLL try { dll.LoadSymbol("Atlas_StartWindow", Atlas_StartWindow); dll.LoadSymbol("Atlas_SetMessagePasser", Atlas_SetMessagePasser); dll.LoadSymbol("Atlas_SetDataDirectory", Atlas_SetDataDirectory); dll.LoadSymbol("Atlas_SetConfigDirectory", Atlas_SetConfigDirectory); dll.LoadSymbol("Atlas_GLSetCurrent", Atlas_GLSetCurrent); dll.LoadSymbol("Atlas_GLSwapBuffers", Atlas_GLSwapBuffers); dll.LoadSymbol("Atlas_NotifyEndOfFrame", Atlas_NotifyEndOfFrame); dll.LoadSymbol("Atlas_DisplayError", Atlas_DisplayError); dll.LoadSymbol("Atlas_ReportError", Atlas_ReportError); dll.LoadSymbol("ShareableMalloc", ShareableMallocFptr); dll.LoadSymbol("ShareableFree", ShareableFreeFptr); } catch (PSERROR_DllLoader&) { debug_warn(L"Failed to initialise DLL"); return false; } // Construct a message passer for communicating with Atlas // (here so that its scope lasts beyond the game thread) MessagePasserImpl msgPasser; AtlasMessage::g_MessagePasser = &msgPasser; // Pass our message handler to Atlas Atlas_SetMessagePasser(&msgPasser); // Tell Atlas the location of the data directory const Paths paths(args); Atlas_SetDataDirectory(paths.RData().string().c_str()); // Tell Atlas the location of the user config directory Atlas_SetConfigDirectory(paths.Config().string().c_str()); // Run the engine loop in a new thread pthread_t engineThread; pthread_create(&engineThread, NULL, RunEngine, reinterpret_cast(const_cast(&args))); // Start Atlas UI on main thread // (required for wxOSX/Cocoa compatibility - see http://trac.wildfiregames.com/ticket/500) Atlas_StartWindow(L"ScenarioEditor"); // Wait for the engine to exit pthread_join(engineThread, NULL); // 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; } Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp (revision 19502) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp (revision 19503) @@ -1,204 +1,204 @@ -/* Copyright (C) 2015 Wildfire Games. +/* Copyright (C) 2017 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 "MessageHandler.h" #include "../GameLoop.h" #include "../CommandProc.h" #include "../ActorViewer.h" #include "../View.h" #include "graphics/GameView.h" #include "graphics/ObjectManager.h" #include "gui/GUIManager.h" #include "lib/external_libraries/libsdl.h" #include "lib/ogl.h" #include "maths/MathUtil.h" #include "ps/CConsole.h" #include "ps/Game.h" #include "ps/VideoMode.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/GameSetup.h" #include "renderer/Renderer.h" namespace AtlasMessage { // see comment in GameLoop.cpp about ah_display_error before using INIT_HAVE_DISPLAY_ERROR const int g_InitFlags = INIT_HAVE_VMODE|INIT_NO_GUI; MESSAGEHANDLER(Init) { UNUSED2(msg); g_Quickstart = true; // Mount mods if there are any specified as command line parameters if (!Init(g_AtlasGameLoop->args, g_InitFlags | INIT_MODS|INIT_MODS_PUBLIC)) { // There are no mods specified on the command line, // but there are in the config file, so mount those. Shutdown(SHUTDOWN_FROM_CONFIG); ENSURE(Init(g_AtlasGameLoop->args, g_InitFlags)); } // Initialise some graphics state for Atlas. // (This must be done after Init loads the config DB, // but before the UI constructs its GL canvases.) g_VideoMode.InitNonSDL(); } MESSAGEHANDLER(InitSDL) { UNUSED2(msg); // When using GLX (Linux), SDL has to load the GL library to find // glXGetProcAddressARB before it can load any extensions. // When running in Atlas, we skip the SDL video initialisation code // which loads the library, and so SDL_GL_GetProcAddress fails (in // ogl.cpp importExtensionFunctions). // (TODO: I think this is meant to be context-independent, i.e. it // doesn't matter that we're getting extensions from SDL-initialised // GL stuff instead of from the wxWidgets-initialised GL stuff, but that // should be checked.) // So, make sure it's loaded: SDL_InitSubSystem(SDL_INIT_VIDEO); SDL_GL_LoadLibrary(NULL); // NULL = use default // (it shouldn't hurt if this is called multiple times, I think) } MESSAGEHANDLER(InitGraphics) { UNUSED2(msg); ogl_Init(); InitGraphics(g_AtlasGameLoop->args, g_InitFlags); #if OS_WIN // HACK (to stop things looking very ugly when scrolling) - should // use proper config system. if(ogl_HaveExtension("WGL_EXT_swap_control")) pwglSwapIntervalEXT(1); #endif } MESSAGEHANDLER(Shutdown) { UNUSED2(msg); // Empty the CommandProc, to get rid of its references to entities before // we kill the EntityManager GetCommandProc().Destroy(); AtlasView::DestroyViews(); g_AtlasGameLoop->view = AtlasView::GetView_None(); int flags = 0; Shutdown(flags); } QUERYHANDLER(Exit) { UNUSED2(msg); g_AtlasGameLoop->running = false; } MESSAGEHANDLER(RenderEnable) { g_AtlasGameLoop->view->SetEnabled(false); g_AtlasGameLoop->view = AtlasView::GetView(msg->view); g_AtlasGameLoop->view->SetEnabled(true); } MESSAGEHANDLER(SetViewParamB) { AtlasView* view = AtlasView::GetView(msg->view); view->SetParam(*msg->name, msg->value); } MESSAGEHANDLER(SetViewParamI) { AtlasView* view = AtlasView::GetView(msg->view); view->SetParam(*msg->name, msg->value); } MESSAGEHANDLER(SetViewParamC) { AtlasView* view = AtlasView::GetView(msg->view); view->SetParam(*msg->name, msg->value); } MESSAGEHANDLER(SetViewParamS) { AtlasView* view = AtlasView::GetView(msg->view); view->SetParam(*msg->name, *msg->value); } MESSAGEHANDLER(SetActorViewer) { if (msg->flushcache) { // TODO EXTREME DANGER: this'll break horribly if any units remain // in existence and use their actors after we've deleted all the actors. // (The actor viewer currently only has one unit at a time, so it's // alright.) // Should replace this with proper actor hot-loading system, or something. AtlasView::GetView_Actor()->GetActorViewer().SetActor(L"", L"", -1); AtlasView::GetView_Actor()->GetActorViewer().UnloadObjects(); // vfs_reload_changed_files(); } AtlasView::GetView_Actor()->SetSpeedMultiplier(msg->speed); AtlasView::GetView_Actor()->GetActorViewer().SetActor(*msg->id, *msg->animation, msg->playerID); } ////////////////////////////////////////////////////////////////////////// MESSAGEHANDLER(SetCanvas) { // Need to set the canvas size before possibly doing any rendering, // else we'll get GL errors when trying to render to 0x0 CVideoMode::UpdateRenderer(msg->width, msg->height); g_AtlasGameLoop->glCanvas = msg->canvas; Atlas_GLSetCurrent(const_cast(g_AtlasGameLoop->glCanvas)); } MESSAGEHANDLER(ResizeScreen) { CVideoMode::UpdateRenderer(msg->width, msg->height); #if OS_MACOSX // OS X seems to require this to update the GL canvas Atlas_GLSetCurrent(const_cast(g_AtlasGameLoop->glCanvas)); #endif } ////////////////////////////////////////////////////////////////////////// MESSAGEHANDLER(RenderStyle) { g_Renderer.SetTerrainRenderMode(msg->wireframe ? EDGED_FACES : SOLID); g_Renderer.SetModelRenderMode(msg->wireframe ? EDGED_FACES : SOLID); } }