diff --git a/code/CMakeModules/FindFFmpeg.cmake b/code/CMakeModules/FindFFmpeg.cmake new file mode 100644 index 000000000..96cbb6ed0 --- /dev/null +++ b/code/CMakeModules/FindFFmpeg.cmake @@ -0,0 +1,173 @@ +# vim: ts=2 sw=2 +# - Try to find the required ffmpeg components(default: AVFORMAT, AVUTIL, AVCODEC) +# +# Once done this will define +# FFMPEG_FOUND - System has the all required components. +# FFMPEG_INCLUDE_DIRS - Include directory necessary for using the required components headers. +# FFMPEG_LIBRARIES - Link these to use the required ffmpeg components. +# FFMPEG_DEFINITIONS - Compiler switches required for using the required ffmpeg components. +# +# For each of the components it will additionaly set. +# - AVCODEC +# - AVDEVICE +# - AVFORMAT +# - AVUTIL +# - POSTPROC +# - SWSCALE +# - SWRESAMPLE +# the following variables will be defined +# _FOUND - System has +# _INCLUDE_DIRS - Include directory necessary for using the headers +# _LIBRARIES - Link these to use +# _DEFINITIONS - Compiler switches required for using +# _VERSION - The components version +# +# Copyright (c) 2006, Matthias Kretz, +# Copyright (c) 2008, Alexander Neundorf, +# Copyright (c) 2011, Michael Jansen, +# +# Redistribution and use is allowed according to the terms of the BSD license. + +include(FindPackageHandleStandardArgs) + +if(NOT FFmpeg_FIND_COMPONENTS) + set(FFmpeg_FIND_COMPONENTS AVFORMAT AVCODEC AVUTIL) +endif() + +# +### Macro: set_component_found +# +# Marks the given component as found if both *_LIBRARIES AND *_INCLUDE_DIRS is present. +# +macro(set_component_found _component) + if(${_component}_LIBRARIES AND ${_component}_INCLUDE_DIRS) + # message(STATUS " - ${_component} found.") + set(${_component}_FOUND TRUE) + else() + # message(STATUS " - ${_component} not found.") + endif() +endmacro() + +# +### Macro: find_component +# +# Checks for the given component by invoking pkgconfig and then looking up the libraries and +# include directories. +# +macro(find_component _component _pkgconfig _library _header) + if(NOT WIN32) + # use pkg-config to get the directories and then use these values + # in the FIND_PATH() and FIND_LIBRARY() calls + find_package(PkgConfig) + if(PKG_CONFIG_FOUND) + pkg_check_modules(PC_${_component} ${_pkgconfig}) + endif() + endif() + + find_path(${_component}_INCLUDE_DIRS ${_header} + HINTS + ${FFMPEGSDK_INC} + ${PC_LIB${_component}_INCLUDEDIR} + ${PC_LIB${_component}_INCLUDE_DIRS} + PATH_SUFFIXES + ffmpeg + ) + + find_library(${_component}_LIBRARIES NAMES ${_library} + HINTS + ${FFMPEGSDK_LIB} + ${PC_LIB${_component}_LIBDIR} + ${PC_LIB${_component}_LIBRARY_DIRS} + ) + + STRING(REGEX REPLACE "/.*" "/version.h" _ver_header ${_header}) + if(EXISTS "${${_component}_INCLUDE_DIRS}/${_ver_header}") + file(STRINGS "${${_component}_INCLUDE_DIRS}/${_ver_header}" version_str REGEX "^#define[\t ]+LIB${_component}_VERSION_M.*") + + foreach(_str "${version_str}") + if(NOT version_maj) + string(REGEX REPLACE "^.*LIB${_component}_VERSION_MAJOR[\t ]+([0-9]*).*$" "\\1" version_maj "${_str}") + endif() + if(NOT version_min) + string(REGEX REPLACE "^.*LIB${_component}_VERSION_MINOR[\t ]+([0-9]*).*$" "\\1" version_min "${_str}") + endif() + if(NOT version_mic) + string(REGEX REPLACE "^.*LIB${_component}_VERSION_MICRO[\t ]+([0-9]*).*$" "\\1" version_mic "${_str}") + endif() + endforeach() + unset(version_str) + + set(${_component}_VERSION "${version_maj}.${version_min}.${version_mic}" CACHE STRING "The ${_component} version number.") + unset(version_maj) + unset(version_min) + unset(version_mic) + endif(EXISTS "${${_component}_INCLUDE_DIRS}/${_ver_header}") + set(${_component}_VERSION ${PC_${_component}_VERSION} CACHE STRING "The ${_component} version number.") + set(${_component}_DEFINITIONS ${PC_${_component}_CFLAGS_OTHER} CACHE STRING "The ${_component} CFLAGS.") + + set_component_found(${_component}) + + mark_as_advanced( + ${_component}_INCLUDE_DIRS + ${_component}_LIBRARIES + ${_component}_DEFINITIONS + ${_component}_VERSION) +endmacro() + + +set(FFMPEGSDK $ENV{FFMPEG_HOME}) +if(FFMPEGSDK) + set(FFMPEGSDK_INC "${FFMPEGSDK}/include") + set(FFMPEGSDK_LIB "${FFMPEGSDK}/lib") +endif() + +# Check for all possible components. +find_component(AVCODEC libavcodec avcodec libavcodec/avcodec.h) +find_component(AVFORMAT libavformat avformat libavformat/avformat.h) +find_component(AVDEVICE libavdevice avdevice libavdevice/avdevice.h) +find_component(AVUTIL libavutil avutil libavutil/avutil.h) +find_component(SWSCALE libswscale swscale libswscale/swscale.h) +find_component(SWRESAMPLE libswresample swresample libswresample/swresample.h) +find_component(POSTPROC libpostproc postproc libpostproc/postprocess.h) + +# Check if the required components were found and add their stuff to the FFMPEG_* vars. +foreach(_component ${FFmpeg_FIND_COMPONENTS}) + if(${_component}_FOUND) + # message(STATUS "Required component ${_component} present.") + set(FFMPEG_LIBRARIES ${FFMPEG_LIBRARIES} ${${_component}_LIBRARIES}) + set(FFMPEG_DEFINITIONS ${FFMPEG_DEFINITIONS} ${${_component}_DEFINITIONS}) + list(APPEND FFMPEG_INCLUDE_DIRS ${${_component}_INCLUDE_DIRS}) + else() + # message(STATUS "Required component ${_component} missing.") + endif() +endforeach() + +# Build the include path and library list with duplicates removed. +if(FFMPEG_INCLUDE_DIRS) + list(REMOVE_DUPLICATES FFMPEG_INCLUDE_DIRS) +endif() + +if(FFMPEG_LIBRARIES) + list(REMOVE_DUPLICATES FFMPEG_LIBRARIES) +endif() + +# cache the vars. +set(FFMPEG_INCLUDE_DIRS ${FFMPEG_INCLUDE_DIRS} CACHE STRING "The FFmpeg include directories." FORCE) +set(FFMPEG_LIBRARIES ${FFMPEG_LIBRARIES} CACHE STRING "The FFmpeg libraries." FORCE) +set(FFMPEG_DEFINITIONS ${FFMPEG_DEFINITIONS} CACHE STRING "The FFmpeg cflags." FORCE) + +mark_as_advanced(FFMPEG_INCLUDE_DIRS FFMPEG_LIBRARIES FFMPEG_DEFINITIONS) + +# Now set the noncached _FOUND vars for the components. +foreach(_component AVCODEC AVDEVICE AVFORMAT AVUTIL POSTPROCESS SWRESAMPLE SWSCALE) + set_component_found(${_component}) +endforeach () + +# Compile the list of required vars +set(_FFmpeg_REQUIRED_VARS FFMPEG_LIBRARIES FFMPEG_INCLUDE_DIRS) +foreach(_component ${FFmpeg_FIND_COMPONENTS}) + list(APPEND _FFmpeg_REQUIRED_VARS ${_component}_LIBRARIES ${_component}_INCLUDE_DIRS) +endforeach() + +# Give a nice error message if some of the required vars are missing. +find_package_handle_standard_args(FFmpeg DEFAULT_MSG ${_FFmpeg_REQUIRED_VARS}) diff --git a/code/nel/CMakeLists.txt b/code/nel/CMakeLists.txt index 84b820d33..3470fcebb 100644 --- a/code/nel/CMakeLists.txt +++ b/code/nel/CMakeLists.txt @@ -20,6 +20,7 @@ ENDIF() IF(WITH_SOUND) FIND_PACKAGE(Ogg) FIND_PACKAGE(Vorbis) + FIND_PACKAGE(FFmpeg COMPONENTS AVCODEC AVFORMAT AVUTIL SWRESAMPLE) IF(WITH_DRIVER_OPENAL) FIND_PACKAGE(OpenAL) diff --git a/code/nel/include/nel/3d/computed_string.h b/code/nel/include/nel/3d/computed_string.h index 517200383..25d12a3ac 100644 --- a/code/nel/include/nel/3d/computed_string.h +++ b/code/nel/include/nel/3d/computed_string.h @@ -178,6 +178,10 @@ public: CVertexBuffer Vertices; CMaterial *Material; CRGBA Color; + ucstring Text; + + uint32 CacheVersion; + /// The width of the string, in pixels (eg: 30) float StringWidth; /// The height of the string, in pixels (eg: 10) @@ -223,6 +227,7 @@ public: */ CComputedString (bool bSetupVB=true) { + CacheVersion = 0; StringWidth = 0; StringHeight = 0; if (bSetupVB) diff --git a/code/nel/include/nel/3d/font_generator.h b/code/nel/include/nel/3d/font_generator.h index 5a07733a0..e71551c43 100644 --- a/code/nel/include/nel/3d/font_generator.h +++ b/code/nel/include/nel/3d/font_generator.h @@ -74,6 +74,8 @@ public: uint32 getUID() { return _UID; } + std::string getFontFileName() const; + private: static uint32 _FontGeneratorCounterUID; diff --git a/code/nel/include/nel/3d/font_manager.h b/code/nel/include/nel/3d/font_manager.h index 26ea02ce0..663e9d23a 100644 --- a/code/nel/include/nel/3d/font_manager.h +++ b/code/nel/include/nel/3d/font_manager.h @@ -59,6 +59,9 @@ class CFontManager CSmartPtr _MatFont; CSmartPtr _TexFont; + // Keep track number of textures created to properly report cache version + uint32 _TexCacheNr; + public: /** @@ -71,6 +74,7 @@ public: _NbChar = 0; _MatFont = NULL; _TexFont = NULL; + _TexCacheNr = 0; } @@ -94,7 +98,6 @@ public: */ CMaterial* getFontMaterial(); - /** * Compute primitive blocks and materials of each character of * the string. @@ -152,7 +155,8 @@ public: void dumpCache (const char *filename) { - _TexFont->dumpTextureFont (filename); + if (_TexFont) + _TexFont->dumpTextureFont (filename); } /** @@ -160,6 +164,15 @@ public: */ void invalidate(); + // get font atlas rebuild count + uint32 getCacheVersion() const + { + if (_TexFont) + return (_TexFont->getCacheVersion() << 16) + _TexCacheNr; + + return 0; + } + }; diff --git a/code/nel/include/nel/3d/text_context.h b/code/nel/include/nel/3d/text_context.h index a95916da4..1f75e1184 100644 --- a/code/nel/include/nel/3d/text_context.h +++ b/code/nel/include/nel/3d/text_context.h @@ -150,6 +150,10 @@ public: { nlassert (index < _CacheStrings.size()); CComputedString &rCS = _CacheStrings[index]; + if (rCS.CacheVersion != _FontManager->getCacheVersion()) + { + computeString(rCS.Text, rCS); + } if (_Shaded) { CRGBA bkup = rCS.Color; @@ -184,6 +188,10 @@ public: { nlassert (index < _CacheStrings.size()); CComputedString &rCS = _CacheStrings[index]; + if (rCS.CacheVersion != _FontManager->getCacheVersion()) + { + computeString(rCS.Text, rCS); + } if(_Shaded) { CRGBA bkup = rCS.Color; @@ -218,6 +226,11 @@ public: { nlassert (index < _CacheStrings.size()); CComputedString &rCS = _CacheStrings[index]; + if (rCS.CacheVersion != _FontManager->getCacheVersion()) + { + computeString(rCS.Text, rCS); + } + if (_Shaded) { CRGBA bkup = rCS.Color; diff --git a/code/nel/include/nel/3d/texture_font.h b/code/nel/include/nel/3d/texture_font.h index e743bb137..865615b63 100644 --- a/code/nel/include/nel/3d/texture_font.h +++ b/code/nel/include/nel/3d/texture_font.h @@ -18,6 +18,7 @@ #define NL_TEXTURE_FONT_H #include "nel/misc/types_nl.h" +#include "nel/misc/rect.h" #include "nel/3d/texture.h" namespace NL3D @@ -25,9 +26,6 @@ namespace NL3D class CFontGenerator; -#define TEXTUREFONT_NBCATEGORY 5 // Config 1 -//#define TEXTUREFONT_NBCATEGORY 4 - // **************************************************************************** /** * CTextureFont @@ -37,32 +35,59 @@ class CTextureFont : public ITexture public: - struct SLetterInfo + // Holds info for glyphs rendered on atlas + struct SGlyphInfo { - // To generate the letter - ucchar Char; - CFontGenerator *FontGenerator; + // font atlas info + uint32 CacheVersion; + + // atlas region with padding + uint32 X, Y, W, H; + + // rendered glyph size without padding + uint32 CharWidth; + uint32 CharHeight; + + // UV coords for rendered glyph without padding + float U0, V0, U1, V1; + + uint32 GlyphIndex; sint Size; bool Embolden; bool Oblique; + CFontGenerator *FontGenerator; + SGlyphInfo() + : CacheVersion(0), + U0(0.f), V0(0.f), U1(0.f), V1(0.f), + X(0), Y(0), W(0), H(0), CharWidth(0), CharHeight(0), + GlyphIndex(0), Size(0), Embolden(false), Oblique(false), FontGenerator(NULL) + { + } + }; - // The less recently used infos - SLetterInfo *Next, *Prev; + // Holds info for glyphs displayed on screen + struct SLetterInfo + { + ucchar Char; + sint Size; + bool Embolden; + bool Oblique; + CFontGenerator *FontGenerator; - uint Cat; // 8x8, 16x16, 24x24, 32x32 - - ////////////////////////////////////////////////////////////////////// - - float U ,V; - uint32 CharWidth; - uint32 CharHeight; - uint32 GlyphIndex; // number of the character in the this font + uint32 GlyphIndex; + uint32 CharWidth; // Displayed glyph height + uint32 CharHeight; // Displayed glyph height sint32 Top; // Distance between origin and top of the texture sint32 Left; // Distance between origin and left of the texture sint32 AdvX; // Advance to the next caracter - SLetterInfo():Char(0), FontGenerator(NULL), Size(0), Embolden(false), Oblique(false), Next(NULL), Prev(NULL), Cat(0), CharWidth(0), CharHeight(0), GlyphIndex(0), Top(0), Left(0), AdvX(0) + SGlyphInfo* glyph; + + SLetterInfo() + : Char(0), Size(0), Embolden(false), Oblique(false), FontGenerator(NULL), + GlyphIndex(0), CharWidth(0), CharHeight(0), Top(0), Left(0), AdvX(0), + glyph(NULL) { } }; @@ -70,14 +95,13 @@ public: struct SLetterKey { ucchar Char; - CFontGenerator *FontGenerator; sint Size; bool Embolden; bool Oblique; + CFontGenerator *FontGenerator; + // Does not use FontGenerator in return value uint32 getVal(); - //bool operator < (const SLetterKey&k) const; - //bool operator == (const SLetterKey&k) const; SLetterKey():Char(0), FontGenerator(NULL), Size(0), Embolden(false), Oblique(false) { @@ -96,19 +120,76 @@ public: void doGenerate (bool async = false); // This function manage the cache if the letter wanted does not exist - SLetterInfo* getLetterInfo (SLetterKey& k); + // \param render Set to true if letter is currently visible on screen + SLetterInfo* getLetterInfo (SLetterKey& k, bool render); void dumpTextureFont (const char *filename); + // Version is increased with each rebuild of font atlas + uint32 getCacheVersion() const { return _CacheVersion; } + private: + uint32 _CacheVersion; + + // current texture size + uint32 _TextureSizeX; + uint32 _TextureSizeY; + + // maximum texture size allowed + uint32 _TextureMaxW; + uint32 _TextureMaxH; + + // padding around glyphs + uint8 _PaddingL, _PaddingT; + uint8 _PaddingR, _PaddingB; // To find a letter in the texture - std::map Accel; - std::vector Letters[TEXTUREFONT_NBCATEGORY]; - SLetterInfo *Front[TEXTUREFONT_NBCATEGORY], *Back[TEXTUREFONT_NBCATEGORY]; + // Keep track of available space in main texture + std::vector _AtlasNodes; - void rebuildLetter (sint cat, sint x, sint y); + std::vector _Letters; + + // lookup letter from letter cache or create new + SLetterInfo* findLetter(SLetterKey& k, bool insert); + + // lower/upper bound of glyphs to render, sizes outside are scaled bitmaps + uint _MinGlyphSize; + uint _MaxGlyphSize; + // start using size stem from this font size + uint _GlyphSizeStepMin; + // every n'th font size is rendered, intermediates are using bitmap scaling + uint _GlyphSizeStep; + + // rendered glyph cache + std::list _GlyphCache; + SGlyphInfo* findLetterGlyph(SLetterInfo *letter, bool insert); + + // render letter glyph into glyph cache + SGlyphInfo* renderLetterGlyph(SLetterInfo *letter, uint32 bitmapFontSize); + + // copy glyph bitmap into texture and invalidate that region + void copyGlyphBitmap(uint8* bitmap, uint32 bitmapW, uint32 bitmapH, uint32 atlasX, uint32 atlasY); + + // Find best fit for WxH rect in atlas + uint fitRegion(uint index, uint width, uint height); + + // Return top/left from font texture or false if there is no more room + bool reserveAtlas(const uint32 width, const uint32 height, uint32 &x, uint32 &y); + + // repack glyphs, resize texture, and invalidate unused glyphs. + void repackAtlas(); + void repackAtlas(uint32 width, uint32 height); + + // resize texture, + bool resizeAtlas(); + + // remove all glyphs from atlas, clear glyph cache, letter info is kept + void clearAtlas(); + + // if return true: newW, newH contains next size font atlas should be resized + // if return false: _TextureMaxW and _TextureMaxH is reached + bool getNextTextureSize(uint32 &newW, uint32 &newH) const; /// Todo: serialize a font texture. public: diff --git a/code/nel/include/nel/gui/libwww.h b/code/nel/include/nel/gui/libwww.h index 64258608e..9c6579f1f 100644 --- a/code/nel/include/nel/gui/libwww.h +++ b/code/nel/include/nel/gui/libwww.h @@ -208,6 +208,7 @@ namespace NLGUI HTML_ATTR(P,QUICK_HELP_EVENTS), HTML_ATTR(P,QUICK_HELP_LINK), HTML_ATTR(P,NAME), + HTML_ATTR(P,STYLE), }; enum diff --git a/code/nel/include/nel/sound/audio_decoder_ffmpeg.h b/code/nel/include/nel/sound/audio_decoder_ffmpeg.h new file mode 100644 index 000000000..c12f52f58 --- /dev/null +++ b/code/nel/include/nel/sound/audio_decoder_ffmpeg.h @@ -0,0 +1,108 @@ +// NeL - MMORPG Framework +// Copyright (C) 2018 Winch Gate Property Limited +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +#ifndef NLSOUND_AUDIO_DECODER_FFMPEG_H +#define NLSOUND_AUDIO_DECODER_FFMPEG_H +#include + +#include + +struct AVCodecContext; +struct AVFormatContext; +struct AVIOContext; +struct AVPacket; +struct SwrContext; + +namespace NLSOUND { + +/** + * \brief CAudioDecoderFfmpeg + * \date 2018-10-21 08:08GMT + * \author Meelis Mägi (Nimetu) + * CAudioDecoderFfmpeg + * Create trough IAudioDecoder + */ +class CAudioDecoderFfmpeg : public IAudioDecoder +{ +protected: + NLMISC::IStream *_Stream; + + bool _IsSupported; + bool _Loop; + bool _IsMusicEnded; + sint32 _StreamOffset; + sint32 _StreamSize; + + AVIOContext *_AvioContext; + AVFormatContext *_FormatContext; + AVCodecContext *_AudioContext; + SwrContext *_SwrContext; + + // selected stream + sint32 _AudioStreamIndex; + + // output buffer for decoded frame + SwrContext *_ConvertContext; + +private: + // called from constructor if ffmpeg fails to initialize + // or from destructor to cleanup ffmpeg pointers + void release(); + +public: + CAudioDecoderFfmpeg(NLMISC::IStream *stream, bool loop); + virtual ~CAudioDecoderFfmpeg(); + + inline NLMISC::IStream *getStream() { return _Stream; } + inline sint32 getStreamSize() { return _StreamSize; } + inline sint32 getStreamOffset() { return _StreamOffset; } + + // Return true if ffmpeg is able to decode the stream + bool isFormatSupported() const; + + /// Get information on a music file (only artist and title at the moment). + static bool getInfo(NLMISC::IStream *stream, std::string &artist, std::string &title, float &length); + + /// Get how many bytes the music buffer requires for output minimum. + virtual uint32 getRequiredBytes(); + + /// Get an amount of bytes between minimum and maximum (can be lower than minimum if at end). + virtual uint32 getNextBytes(uint8 *buffer, uint32 minimum, uint32 maximum); + + /// Get the amount of channels (2 is stereo) in output. + virtual uint8 getChannels(); + + /// Get the samples per second (often 44100) in output. + virtual uint getSamplesPerSec(); + + /// Get the bits per sample (often 16) in output. + virtual uint8 getBitsPerSample(); + + /// Get if the music has ended playing (never true if loop). + virtual bool isMusicEnded(); + + /// Get the total time in seconds. + virtual float getLength(); + + /// Set looping + virtual void setLooping(bool loop); +}; /* class CAudioDecoderFfmpeg */ + +} /* namespace NLSOUND */ + +#endif // NLSOUND_AUDIO_DECODER_FFMPEG_H + +/* end of file */ diff --git a/code/nel/include/nel/sound/stream_file_source.h b/code/nel/include/nel/sound/stream_file_source.h index 79151f171..b7c48f96a 100644 --- a/code/nel/include/nel/sound/stream_file_source.h +++ b/code/nel/include/nel/sound/stream_file_source.h @@ -98,8 +98,9 @@ private: NLMISC::IThread *m_Thread; IAudioDecoder *m_AudioDecoder; - + bool m_Paused; + bool m_DecodingEnded; }; /* class CStreamFileSource */ diff --git a/code/nel/src/3d/font_generator.cpp b/code/nel/src/3d/font_generator.cpp index f50b61c5b..b0e917b6f 100644 --- a/code/nel/src/3d/font_generator.cpp +++ b/code/nel/src/3d/font_generator.cpp @@ -81,6 +81,11 @@ const char *CFontGenerator::getFT2Error(FT_Error fte) return ukn; } +std::string CFontGenerator::getFontFileName() const +{ + return _FontFileName; +} + CFontGenerator *newCFontGenerator(const std::string &fontFileName) { return new CFontGenerator(fontFileName); diff --git a/code/nel/src/3d/font_manager.cpp b/code/nel/src/3d/font_manager.cpp index 3b77a2100..d00ebb8f5 100644 --- a/code/nel/src/3d/font_manager.cpp +++ b/code/nel/src/3d/font_manager.cpp @@ -46,6 +46,7 @@ CMaterial* CFontManager::getFontMaterial() if (_TexFont == NULL) { _TexFont = new CTextureFont; + _TexCacheNr++; } if (_MatFont == NULL) @@ -142,11 +143,17 @@ void CFontManager::computeString (const ucstring &s, sint32 nMaxZ = -1000000, nMinZ = 1000000; output.StringHeight = 0; + // save string info for later rebuild as needed + output.Text = s; + output.CacheVersion = getCacheVersion(); + uint j = 0; { CVertexBufferReadWrite vba; output.Vertices.lock (vba); + hlfPixScrW = 0.f; + hlfPixScrH = 0.f; // For all chars for (uint i = 0; i < s.size(); i++) @@ -157,38 +164,43 @@ void CFontManager::computeString (const ucstring &s, k.Size = fontSize; k.Embolden = embolden; k.Oblique = oblique; - CTextureFont::SLetterInfo *pLI = pTexFont->getLetterInfo (k); + // render letter + CTextureFont::SLetterInfo *pLI = pTexFont->getLetterInfo (k, true); if(pLI != NULL) { - if ((pLI->CharWidth > 0) && (pLI->CharHeight > 0)) + if (pLI->glyph) { + // If letter is heavily upscaled, then there is noticeable clipping on edges + // fixing UV will make it bit better + if ((pLI->Size >> 1) > pLI->glyph->Size) + { + hlfPixTexW = 0.5f * TexRatioW; + hlfPixTexH = 0.5f * TexRatioH; + } + // Creating vertices dx = pLI->Left; - dz = -((sint32)pLI->CharHeight-(sint32)(pLI->Top)); - u1 = pLI->U - hlfPixTexW; - v1 = pLI->V - hlfPixTexH; - u2 = pLI->U + ((float)pLI->CharWidth) * TexRatioW + hlfPixTexW; - v2 = pLI->V + ((float)pLI->CharHeight) * TexRatioH + hlfPixTexH; + dz = -((sint32)pLI->CharHeight - (sint32)(pLI->Top)); x1 = (penx + dx) - hlfPixScrW; z1 = (penz + dz) - hlfPixScrH; - x2 = (penx + dx + (sint32)pLI->CharWidth) + hlfPixScrW; + x2 = (penx + dx + (sint32)pLI->CharWidth) + hlfPixScrW; z2 = (penz + dz + (sint32)pLI->CharHeight) + hlfPixScrH; vba.setVertexCoord (j, x1, 0, z1); - vba.setTexCoord (j, 0, u1, v2); + vba.setTexCoord (j, 0, pLI->glyph->U0-hlfPixTexW, pLI->glyph->V1+hlfPixTexH); ++j; vba.setVertexCoord (j, x2, 0, z1); - vba.setTexCoord (j, 0, u2, v2); + vba.setTexCoord (j, 0, pLI->glyph->U1+hlfPixTexW, pLI->glyph->V1+hlfPixTexH); ++j; vba.setVertexCoord (j, x2, 0, z2); - vba.setTexCoord (j, 0, u2, v1); + vba.setTexCoord (j, 0, pLI->glyph->U1+hlfPixTexW, pLI->glyph->V0-hlfPixTexH); ++j; vba.setVertexCoord (j, x1, 0, z2); - vba.setTexCoord (j, 0, u1, v1); + vba.setTexCoord (j, 0, pLI->glyph->U0-hlfPixTexW, pLI->glyph->V0-hlfPixTexH); ++j; // String Bound @@ -245,6 +257,19 @@ void CFontManager::computeStringInfo ( const ucstring &s, { output.Color = color; + // save string info for later rebuild as needed + output.Text = s; + output.CacheVersion = 0; + + if (s.empty()) + { + output.StringWidth = 0.f; + output.StringHeight = 0; + output.StringLine = 0; + + return; + } + // resize fontSize if window not of 800x600. if (keep800x600Ratio) { @@ -273,7 +298,7 @@ void CFontManager::computeStringInfo ( const ucstring &s, k.Size = fontSize; k.Embolden = embolden; k.Oblique = oblique; - pLI = pTexFont->getLetterInfo (k); + pLI = pTexFont->getLetterInfo (k, false); if(pLI != NULL) { if ((pLI->CharWidth > 0) && (pLI->CharHeight > 0)) @@ -318,7 +343,11 @@ void CFontManager::invalidate() { if (_TexFont) _TexFont = NULL; + _TexFont = new CTextureFont; + _TexCacheNr++; + + getFontMaterial()->setTexture(0, _TexFont); } diff --git a/code/nel/src/3d/text_context.cpp b/code/nel/src/3d/text_context.cpp index 5fcc43daa..235ea9fcb 100644 --- a/code/nel/src/3d/text_context.cpp +++ b/code/nel/src/3d/text_context.cpp @@ -74,25 +74,9 @@ uint32 CTextContext::textPush (const char *format, ...) char *str; NLMISC_CONVERT_VARGS (str, format, NLMISC::MaxCStringSize); - if (_CacheNbFreePlaces == 0) - { - CComputedString csTmp; - - _CacheStrings.push_back (csTmp); - if (_CacheFreePlaces.empty()) - _CacheFreePlaces.resize (1); - _CacheFreePlaces[0] = (uint32)_CacheStrings.size()-1; - _CacheNbFreePlaces = 1; - } - - // compute the string. - uint32 index = _CacheFreePlaces[_CacheNbFreePlaces-1]; - CComputedString &strToFill = _CacheStrings[index]; - _FontManager->computeString (str, _FontGen, _Color, _FontSize, _Embolden, _Oblique, _Driver, strToFill, _Keep800x600Ratio); - - _CacheNbFreePlaces--; - - return index; + ucstring uc; + uc.fromUtf8((const char *)str); + return textPush(uc); } // ------------------------------------------------------------------------------------------------ @@ -115,8 +99,10 @@ uint32 CTextContext::textPush (const ucstring &str) uint32 index = _CacheFreePlaces[_CacheNbFreePlaces-1]; nlassert (index < _CacheStrings.size()); CComputedString &strToFill = _CacheStrings[index]; - _FontManager->computeString (str, _FontGen, _Color - , _FontSize, _Embolden, _Oblique, _Driver, strToFill, _Keep800x600Ratio); + + _FontManager->computeString (str, _FontGen, _Color, _FontSize, _Embolden, _Oblique, _Driver, strToFill, _Keep800x600Ratio); + // just compute letters, glyphs are rendered on demand before first draw + //_FontManager->computeStringInfo(str, _FontGen, _Color, _FontSize, _Embolden, _Oblique, _Driver, strToFill, _Keep800x600Ratio); _CacheNbFreePlaces--; diff --git a/code/nel/src/3d/texture_font.cpp b/code/nel/src/3d/texture_font.cpp index cf46d025d..eac8cb545 100644 --- a/code/nel/src/3d/texture_font.cpp +++ b/code/nel/src/3d/texture_font.cpp @@ -23,7 +23,7 @@ #include "nel/misc/common.h" #include "nel/misc/rect.h" #include "nel/misc/file.h" - +#include "nel/misc/path.h" using namespace std; using namespace NLMISC; @@ -35,37 +35,14 @@ using namespace NLMISC; namespace NL3D { -// Config 1 -const int TextureSizeX = 1024; -const int TextureSizeY = 1024; // If change this value -> change NbLine too -const int Categories[TEXTUREFONT_NBCATEGORY] = { 8, 16, 24, 32, 64 }; -const int NbLine[TEXTUREFONT_NBCATEGORY] = { 8, 24, 16, 4, 1 }; // Based on textsize - -/* -const int TextureSizeX = 256; -const int TextureSizeY = 256; -const int Categories[TEXTUREFONT_NBCATEGORY] = { 8, 16, 24, 32 }; -const int NbLine[TEXTUREFONT_NBCATEGORY] = { 4, 6, 4, 1 }; // Based on textsize -*/ - -// --------------------------------------------------------------------------- -inline uint32 CTextureFont::SLetterKey::getVal() -{ - // this limits Size to 6bits - // Large sizes already render wrong when many - // different glyphs are used due to limited texture atlas - uint8 eb = ((uint)Embolden) + ((uint)Oblique << 1); - if (FontGenerator == NULL) - return Char + ((Size&255)<<16) + (eb << 22); - else - return Char + ((Size&255)<<16) + (eb << 22) + ((FontGenerator->getUID()&0xFF)<<24); -} - // --------------------------------------------------------------------------- CTextureFont::CTextureFont() + : _CacheVersion(1), + _TextureSizeX(512), _TextureSizeY(512), _TextureMaxW(4096), _TextureMaxH(4096), + _PaddingL(0), _PaddingT(0), _PaddingR(1), _PaddingB(1), + _MinGlyphSize(5), _MaxGlyphSize(200), + _GlyphSizeStepMin(50), _GlyphSizeStep(5) { - uint i; - setFilterMode (ITexture::Linear, ITexture::LinearMipMapOff); setWrapS (ITexture::Repeat); @@ -75,53 +52,9 @@ CTextureFont::CTextureFont() setReleasable (false); - resize (TextureSizeX, TextureSizeY, CBitmap::Alpha); - for(i = 0; i < TextureSizeX*TextureSizeY; ++i) - getPixels()[i] = 0; - // convertToType (CBitmap::Alpha); + resize (_TextureSizeX, _TextureSizeY, CBitmap::Alpha, true); - sint posY = 0; - - for(i = 0; i < TEXTUREFONT_NBCATEGORY; ++i) - { - // Number of chars per cache - Letters[i].resize ((TextureSizeX/Categories[i])*NbLine[i]); - - for(uint32 j = 0; j < Letters[i].size(); ++j) - { - SLetterInfo &rLetter = Letters[i][j]; - rLetter.Char = 0xffff; - rLetter.FontGenerator = NULL; - rLetter.Size= 0; - rLetter.Embolden = false; - rLetter.Oblique = false; - - // The less recently used infos - if (j < Letters[i].size()-1) - rLetter.Next = &Letters[i][j+1]; - else - rLetter.Next = NULL; - - if (j > 0) - rLetter.Prev = &Letters[i][j-1]; - else - rLetter.Prev = NULL; - - rLetter.Cat = i; - - sint sizeX = TextureSizeX/Categories[i]; - rLetter.U = (Categories[i]*(j%sizeX)) / ((float)TextureSizeX); - rLetter.V = (posY + Categories[i]*((sint)(j/sizeX))) / ((float)TextureSizeY); - - ///////////////////////////////////////////////// - - rLetter.CharWidth = rLetter.CharHeight = 0; - rLetter.GlyphIndex = rLetter.Top = rLetter.Left = rLetter.AdvX = 0; - } - Front[i] = &Letters[i][0]; - Back[i] = &Letters[i][Letters[i].size()-1]; - posY += NbLine[i] * Categories[i]; - } + _AtlasNodes.push_back(CRect(0, 0, _TextureSizeX, _TextureSizeY)); } @@ -129,17 +62,16 @@ CTextureFont::~CTextureFont() { } - // --------------------------------------------------------------------------- void CTextureFont::dumpTextureFont(const char *filename) { CBitmap b; COFile f( filename ); - b.resize (TextureSizeX, TextureSizeY, CBitmap::RGBA); + b.resize (_TextureSizeX, _TextureSizeY, CBitmap::RGBA); CObjectVector&bits = b.getPixels(); CObjectVector&src = getPixels(); - for (uint i = 0; i < (TextureSizeX*TextureSizeY); ++i) + for (uint i = 0; i < (_TextureSizeX*_TextureSizeY); ++i) { bits[i*4+0] = bits[i*4+1] = bits[i*4+2] = bits[i*4+3] = src[i]; } @@ -147,242 +79,468 @@ void CTextureFont::dumpTextureFont(const char *filename) b.writeTGA (f, 32); } +// --------------------------------------------------------------------------- +bool CTextureFont::getNextTextureSize(uint32 &newW, uint32 &newH) const +{ + // width will be resized first (256x256 -> 512x256) + if (_TextureSizeX <= _TextureSizeY) + { + newW = _TextureSizeX * 2; + newH = _TextureSizeY; + } + else + { + newW = _TextureSizeX; + newH = _TextureSizeY * 2; + } + + // no more room + return newW <= _TextureMaxW && newH <= _TextureMaxH; +} // --------------------------------------------------------------------------- -// cat : categories where the letter is -// x : pos x of the letter -// y : pos y of the letter -void CTextureFont::rebuildLetter (sint cat, sint x, sint y) +// out of room, clear everything and rebuild glyphs on demand +// note: text will display wrong until glyphs get rendered again +void CTextureFont::clearAtlas() { - sint sizex = TextureSizeX / Categories[cat]; - sint index = x + y*sizex; - SLetterInfo &rLetter = Letters[cat][index]; + nlwarning("Glyph cache will be cleared."); - if (rLetter.FontGenerator == NULL) - return; + _AtlasNodes.clear(); + _AtlasNodes.push_back(CRect(0, 0, _TextureSizeX, _TextureSizeY)); - sint catTopY = 0; - sint c = 0; - while (c < cat) + // clear texture + _Data[0].fill(0); + + // clear glyph cache + for(uint i = 0; i< _Letters.size(); ++i) { - catTopY += NbLine[c] * Categories[c]; - ++c; + _Letters[i].glyph = NULL; } - // Destination position in pixel of the letter - sint posx = x * Categories[cat]; - sint posy = catTopY + y * Categories[cat]; + _GlyphCache.clear(); - uint32 pitch = 0; - uint8 *bitmap = rLetter.FontGenerator->getBitmap ( rLetter.Char, rLetter.Size, rLetter.Embolden, rLetter.Oblique, - rLetter.CharWidth, rLetter.CharHeight, - pitch, rLetter.Left, rLetter.Top, - rLetter.AdvX, rLetter.GlyphIndex ); + _CacheVersion++; - // Copy FreeType buffer - uint i; - for (i = 0; i < rLetter.CharHeight; ++i) + touch(); +} + +// --------------------------------------------------------------------------- +void CTextureFont::repackAtlas() +{ + repackAtlas(_TextureSizeX, _TextureSizeY); +} + +// --------------------------------------------------------------------------- +// backup old glyphs and move them to newly resized texture +// new atlas will be sorted if _GlyphCache is +void CTextureFont::repackAtlas(uint32 newW, uint32 newH) +{ + uint32 newCacheVersion = _CacheVersion+1; + + CBitmap btm; + uint32 oldW, oldH; + + oldW = _TextureSizeX; + oldH = _TextureSizeY; + btm.resize(oldW, oldH, CBitmap::Alpha, true); + btm.blit(this, 0, 0); + + // resize texture + if (_TextureSizeX != newW || _TextureSizeY != newH) { - uint8 *pDst = &_Data[0][posx + (posy+i)*TextureSizeY]; - uint8 *pSrc = &bitmap[i*pitch]; - for (uint j = 0; j < rLetter.CharWidth; ++j) + _TextureSizeX = newW; + _TextureSizeY = newH; + resize (_TextureSizeX, _TextureSizeY, CBitmap::Alpha, true); + } + else + { + _Data[0].fill(0); + } + + // release atlas and rebuild + _AtlasNodes.clear(); + _AtlasNodes.push_back(CRect(0, 0, _TextureSizeX, _TextureSizeY)); + + CObjectVector&src = btm.getPixels(); + for(std::list::iterator it = _GlyphCache.begin(); it != _GlyphCache.end(); ++it) + { + if (it->CacheVersion != _CacheVersion) { - *pDst = *pSrc; - ++pDst; - ++pSrc; + // TODO: must remove glyph from all letters before removing glyph from cache + //continue; + } + + SGlyphInfo &glyph = *it; + + glyph.CacheVersion = newCacheVersion; + + uint32 atlasX, atlasY; + if (reserveAtlas(glyph.W, glyph.H, atlasX, atlasY)) + { + for (uint y = 0; y < glyph.H; ++y) + { + uint8 *pDst = &_Data[0][(atlasY + y) * _TextureSizeX + atlasX]; + for (uint x = 0; x < glyph.W; ++x) + { + *pDst = src[(glyph.Y + y) * oldW + glyph.X + x]; + ++pDst; + } + } + + // TODO: dup code with renderGlyph + glyph.U0 = (atlasX+_PaddingL) / (float)_TextureSizeX; + glyph.V0 = (atlasY+_PaddingT) / (float)_TextureSizeY; + glyph.U1 = (atlasX+_PaddingL+glyph.CharWidth) / (float)_TextureSizeX; + glyph.V1 = (atlasY+_PaddingT+glyph.CharHeight) / (float)_TextureSizeY; + + glyph.X = atlasX; + glyph.Y = atlasY; } } - // Black border bottom and right - for (i = 0; i < rLetter.CharHeight+1; ++i) + _CacheVersion = newCacheVersion; + + // invalidate full texture + touch(); +} + +// --------------------------------------------------------------------------- +bool CTextureFont::resizeAtlas() +{ + uint32 newW, newH; + if (!getNextTextureSize(newW, newH)) { - _Data[0][posx + rLetter.CharWidth + (posy+i)*TextureSizeY] = 0; + nlwarning("Font texture at maximum (%d,%d). Resize failed.", _TextureSizeX, _TextureSizeY); + return false; } - for (i = 0; i < rLetter.CharWidth+1; ++i) - { - _Data[0][posx + i + (posy+rLetter.CharHeight)*TextureSizeY] = 0; - } - - /* - dumpTextureFont (this); - int a = 5; - a++; - */ + // resize and redraw + repackAtlas(newW, newH); + return true; } // --------------------------------------------------------------------------- void CTextureFont::doGenerate(bool async) { - // Rectangle invalidate ? - if (_ListInvalidRect.begin()!=_ListInvalidRect.end()) - { - // Yes, rebuild only those rectangles. - - // For each rectangle to compute - std::list::iterator ite=_ListInvalidRect.begin(); - while (ite!=_ListInvalidRect.end()) - { - // Compute rectangle coordinates - sint x = ite->left(); - sint y = ite->bottom(); - - // Look in which category is the rectangle - sint cat = 0; - sint catTopY = 0; - sint catBotY = NbLine[cat] * Categories[cat]; - while (y > catBotY) - { - if (y < catBotY) - break; - ++cat; - nlassert (cat < TEXTUREFONT_NBCATEGORY); - catTopY = catBotY; - catBotY += NbLine[cat] * Categories[cat]; - } - - x = x / Categories[cat]; - y = ite->top(); - y = y - catTopY; - y = y / Categories[cat]; - - rebuildLetter (cat, x, y); - - // Next rectangle - ite++; - } - } - else - { - for(int cat = 0; cat < TEXTUREFONT_NBCATEGORY; ++cat) - { - sint sizex = TextureSizeX / Categories[cat]; - sint sizey = NbLine[cat]; - for (sint y = 0; y < sizey; y++) - for (sint x = 0; x < sizex; x++) - { - rebuildLetter (cat, x, y); - } - } - } -/* - dumpTextureFont (this); - int a = 5; -*/ + /* + nlinfo("doGenerate: Letters(%d/%d), Glyphs(%d/%d)\n", _Letters.size(), _Letters.size() * sizeof(SLetterInfo), + _GlyphCache.size(), _GlyphCache.size() * sizeof(SGlyphInfo)); + //std::string fname = CFile::findNewFile("/tmp/font-texture.tga"); + std::string fname = toString("/tmp/font-texture-%p-%03d.tga", this, _CacheVersion); + dumpTextureFont (fname.c_str()); + */ } // --------------------------------------------------------------------------- -CTextureFont::SLetterInfo* CTextureFont::getLetterInfo (SLetterKey& k) +uint CTextureFont::fitRegion(uint index, uint width, uint height) { - sint cat; - uint32 nTmp = k.getVal(); - map::iterator itAccel = Accel.find (nTmp); - if (itAccel != Accel.end()) + if (_AtlasNodes[index].X + width > _TextureSizeX - 1) { - // Put it in the first place - SLetterInfo *pLetterToMove = itAccel->second; - cat = pLetterToMove->Cat; - if (pLetterToMove != Front[cat]) + return -1; + } + + uint x = _AtlasNodes[index].X; + uint y = _AtlasNodes[index].Y; + sint widthLeft = width; + + while(widthLeft > 0) + { + if (_AtlasNodes[index].Y > y) { - // unlink - nlassert(pLetterToMove->Prev); - pLetterToMove->Prev->Next = pLetterToMove->Next; - if (pLetterToMove == Back[cat]) - { - Back[cat] = pLetterToMove->Prev; - } - else - { - pLetterToMove->Next->Prev = pLetterToMove->Prev; - } - - // link to front - pLetterToMove->Prev = NULL; - pLetterToMove->Next = Front[cat]; - Front[cat]->Prev = pLetterToMove; - Front[cat] = pLetterToMove; + y = _AtlasNodes[index].Y; } - return pLetterToMove; + + // _AtlasNodes[0] for margin is not used here + if (_AtlasNodes[index].Y + height > _TextureSizeY - 1) + { + return -1; + } + + widthLeft -= _AtlasNodes[index].Width; + index++; } - // The letter is not already present - // Found the category of the new letter - uint32 width, height; + return y; +} - //k.FontGenerator->getSizes (k.Char, k.Size, width, height); - // \todo mat : Temp !!! Try to use freetype cache - uint32 nPitch, nGlyphIndex; - sint32 nLeft, nTop, nAdvX; - k.FontGenerator->getBitmap (k.Char, k.Size, k.Embolden, k.Oblique, width, height, nPitch, nLeft, nTop, - nAdvX, nGlyphIndex ); +bool CTextureFont::reserveAtlas(const uint32 width, const uint32 height, uint32 &x, uint32 &y) +{ + if (_AtlasNodes.empty()) + { + nlwarning("No available space in texture atlas (_AtlasNodes.empty() == true)"); + return false; + } - // Add 1 pixel space for black border to get correct category - cat = 0; - if (((sint)width+1 > Categories[TEXTUREFONT_NBCATEGORY-1]) || - ((sint)height+1 > Categories[TEXTUREFONT_NBCATEGORY-1])) + x = 0; + y = 0; + + sint bestIndex = -1; + sint bestWidth = _TextureSizeX; + sint bestHeight = _TextureSizeY; + + sint selY=0; + + for (uint i = 0; i < _AtlasNodes.size(); ++i) + { + selY = fitRegion(i, width, height); + if (selY >=0) + { + if (((selY + height) < bestHeight) || ((selY + height) == bestHeight && _AtlasNodes[i].Width > 0 && _AtlasNodes[i].Width < bestWidth)) + { + bestHeight = selY + height; + bestIndex = i; + bestWidth = _AtlasNodes[i].Width; + x = _AtlasNodes[i].X; + y = selY; + } + } + } + + if (bestIndex == -1) + { + x = 0; + y = 0; + return false; + } + + CRect r(x, y + height, width, 0); + _AtlasNodes.insert(_AtlasNodes.begin() + bestIndex, r); + + // shrink or remove nodes overlaping with newly inserted node + for(uint i = bestIndex+1; i< _AtlasNodes.size(); i++) + { + if (_AtlasNodes[i].X < (_AtlasNodes[i-1].X + _AtlasNodes[i-1].Width)) + { + sint shrink = _AtlasNodes[i-1].X + _AtlasNodes[i-1].Width - _AtlasNodes[i].X; + _AtlasNodes[i].X += shrink; + if (_AtlasNodes[i].Width > shrink) + { + _AtlasNodes[i].Width -= shrink; + break; + } + _AtlasNodes.erase(_AtlasNodes.begin() + i); + i--; + } + else break; + } + + // merge nearby nodes from same row + for(uint i = 0; i < _AtlasNodes.size() - 1; i++) + { + if (_AtlasNodes[i].Y == _AtlasNodes[i+1].Y) + { + _AtlasNodes[i].Width += _AtlasNodes[i+1].Width; + _AtlasNodes.erase(_AtlasNodes.begin() + i + 1); + i--; + } + } + + return true; +} + +// --------------------------------------------------------------------------- +// bitmap : texture data +// bitmapW : bitmap width +// bitmapH : bitmap height +// atlasX : pos x in font texture +// atlasY : pos y in font texture +void CTextureFont::copyGlyphBitmap(uint8* bitmap, uint32 bitmapW, uint32 bitmapH, uint32 atlasX, uint32 atlasY) +{ + for (uint bY = 0; bY < bitmapH; ++bY) + { + uint8 *pDst = &_Data[0][(atlasY+_PaddingT+bY) * _TextureSizeX+atlasX+_PaddingL]; + for (uint bX = 0; bX < bitmapW; ++bX) + { + *pDst = bitmap[bY * bitmapW+bX]; + ++pDst; + } + } + + if (_PaddingR > 0 || _PaddingB > 0 || _PaddingL > 0 || _PaddingT > 0) + { + for(uint i = 0; i<(bitmapH+_PaddingT+_PaddingB); ++i) + { + if (_PaddingT > 0) _Data[0][(atlasY + i) * _TextureSizeX + atlasX ] = 0; + if (_PaddingB > 0) _Data[0][(atlasY + i) * _TextureSizeX + atlasX + _PaddingL + bitmapW] = 0; + } + + for (uint i = 0; i<(bitmapW+_PaddingL+_PaddingR); ++i) + { + if (_PaddingL > 0) _Data[0][atlasY * _TextureSizeX + atlasX + i] = 0; + if (_PaddingB > 0) _Data[0][(atlasY + _PaddingT + bitmapH) * _TextureSizeX + atlasX + i] = 0; + } + } + + CRect r(atlasX, atlasY, bitmapW + _PaddingL + _PaddingR, bitmapH + _PaddingT + _PaddingB); + touchRect(r); +} + + +// --------------------------------------------------------------------------- +CTextureFont::SGlyphInfo* CTextureFont::renderLetterGlyph(SLetterInfo *letter, uint bitmapFontSize) +{ + uint32 nPitch; + sint32 left; + sint32 top; + sint32 advx; + uint32 charWidth; + uint32 charHeight; + uint32 glyphIndex; + + uint8 *bitmap = letter->FontGenerator->getBitmap (letter->Char, bitmapFontSize, letter->Embolden, letter->Oblique, + charWidth, charHeight, + nPitch, left, top, + advx, glyphIndex ); + + uint32 atlasX, atlasY; + uint32 rectW, rectH; + rectW = charWidth + _PaddingL + _PaddingR; + rectH = charHeight + _PaddingT + _PaddingB; + + if (!reserveAtlas(rectW, rectH, atlasX, atlasY)) + { + // no room return NULL; + } + copyGlyphBitmap(bitmap, charWidth, charHeight, atlasX, atlasY); - while (((sint)width+1 > Categories[cat]) || ((sint)height+1 > Categories[cat])) + SGlyphInfo* glyphInfo = NULL; { - ++cat; - nlassert (cat != TEXTUREFONT_NBCATEGORY); + // keep cache sorted by height (smaller first) + std::list::iterator it = _GlyphCache.begin(); + while(it != _GlyphCache.end() && it->CharHeight < charHeight) + { + ++it; + } + + it = _GlyphCache.insert(it, SGlyphInfo()); + glyphInfo = &(*it); } - // And replace the less recently used letter - SLetterKey k2; - k2.Char = Back[cat]->Char; - k2.FontGenerator = Back[cat]->FontGenerator; - k2.Size = Back[cat]->Size; - k2.Embolden = Back[cat]->Embolden; - k2.Oblique = Back[cat]->Oblique; + glyphInfo->GlyphIndex = glyphIndex; + glyphInfo->Size = bitmapFontSize; + glyphInfo->Embolden = letter->Embolden; + glyphInfo->Oblique = letter->Oblique; + glyphInfo->FontGenerator = letter->FontGenerator; + glyphInfo->CacheVersion = _CacheVersion; - itAccel = Accel.find (k2.getVal()); - if (itAccel != Accel.end()) + glyphInfo->U0 = (atlasX+_PaddingL) / (float)_TextureSizeX; + glyphInfo->V0 = (atlasY+_PaddingT) / (float)_TextureSizeY; + glyphInfo->U1 = (atlasX+_PaddingL+charWidth) / (float)_TextureSizeX; + glyphInfo->V1 = (atlasY+_PaddingT+charHeight) / (float)_TextureSizeY; + + glyphInfo->CharWidth = charWidth; + glyphInfo->CharHeight = charHeight; + + glyphInfo->X = atlasX; + glyphInfo->Y = atlasY; + glyphInfo->W = rectW; + glyphInfo->H = rectH; + + return glyphInfo; +} + + +// --------------------------------------------------------------------------- +CTextureFont::SGlyphInfo* CTextureFont::findLetterGlyph(SLetterInfo *letter, bool insert) +{ + uint bitmapFontSize = max((sint)_MinGlyphSize, min((sint)_MaxGlyphSize, letter->Size)); + if (_GlyphSizeStep > 1 && bitmapFontSize > _GlyphSizeStepMin) { - Accel.erase (itAccel); + uint size = (bitmapFontSize / _GlyphSizeStep) * _GlyphSizeStep; } - SLetterInfo *NewBack = Back[cat]->Prev; - NewBack->Next = NULL; - Back[cat]->Cat = cat; - Back[cat]->Char = k.Char; - Back[cat]->FontGenerator = k.FontGenerator; - Back[cat]->Size = k.Size; - Back[cat]->Embolden = k.Embolden; - Back[cat]->Oblique = k.Oblique; - Back[cat]->CharWidth = width; - Back[cat]->CharHeight = height; - Back[cat]->Top = nTop; - Back[cat]->Left = nLeft; - Back[cat]->AdvX = nAdvX; - Back[cat]->Prev = NULL; - Back[cat]->Next = Front[cat]; - Front[cat]->Prev = Back[cat]; - Front[cat] = Back[cat]; - Back[cat] = NewBack; - - Accel.insert (map::value_type(k.getVal(),Front[cat])); - - // Invalidate the zone - sint index = (sint)(Front[cat] - &Letters[cat][0]);// / sizeof (SLetterInfo); - sint sizex = TextureSizeX / Categories[cat]; - sint x = index % sizex; - sint y = index / sizex; - x = x * Categories[cat]; - y = y * Categories[cat]; - - sint c = 0; - while (c < cat) + // CacheVersion not checked, all glyphs in cache must be rendered on texture + for(std::list::iterator it = _GlyphCache.begin(); it != _GlyphCache.end(); ++it) { - y = y + NbLine[c] * Categories[c]; - ++c; + if (it->GlyphIndex == letter->GlyphIndex && + it->Size == bitmapFontSize && + it->Embolden == letter->Embolden && + it->Oblique == letter->Oblique && + it->FontGenerator == letter->FontGenerator) + { + return &(*it); + } } - // must update the char, WITH the black borders - CRect r (x, y, width+1, height+1); + if (insert) + { + return renderLetterGlyph(letter, bitmapFontSize); + } - touchRect (r); + return NULL; +} - return Front[cat]; +// --------------------------------------------------------------------------- +CTextureFont::SLetterInfo* CTextureFont::findLetter(SLetterKey &k, bool insert) +{ + // TODO: use std::map + for(uint i = 0; i < _Letters.size(); ++i) + { + if (_Letters[i].Char == k.Char && _Letters[i].Size == k.Size && + _Letters[i].Embolden == k.Embolden && _Letters[i].Oblique == k.Oblique && + _Letters[i].FontGenerator == k.FontGenerator) + { + return &_Letters[i]; + } + } + + if (insert) + { + _Letters.push_back(SLetterInfo()); + SLetterInfo* letter = &_Letters.back(); + + // get metrics for requested size + letter->Char = k.Char; + letter->Size = k.Size; + letter->Embolden = k.Embolden; + letter->Oblique = k.Oblique; + letter->FontGenerator = k.FontGenerator; + + uint32 nPitch; + letter->FontGenerator->getBitmap(letter->Char, letter->Size, letter->Embolden, letter->Oblique, + letter->CharWidth, letter->CharHeight, + nPitch, letter->Left, letter->Top, + letter->AdvX, letter->GlyphIndex ); + + return letter; + } + + return NULL; +} + +// --------------------------------------------------------------------------- +CTextureFont::SLetterInfo* CTextureFont::getLetterInfo (SLetterKey& k, bool render) +{ + // find already cached letter or create new one + SLetterInfo* letter = findLetter(k, true); + // letter not found (=NULL) or render not requested + if (!letter || !render) return letter; + + if (!letter->glyph || letter->glyph->CacheVersion != _CacheVersion) + { + // render glyph + letter->glyph = findLetterGlyph(letter, true); + if (letter->glyph == NULL) + { + // resize/repack and try again + if (!resizeAtlas()) repackAtlas(); + + letter->glyph = findLetterGlyph(letter, true); + if (letter->glyph == NULL) + { + // make room by clearing all glyphs and reduce max size for glyphs + clearAtlas(); + if (_MaxGlyphSize > _MinGlyphSize) + { + _MaxGlyphSize = max(_MinGlyphSize, _MaxGlyphSize - 10); + } + + letter->glyph = findLetterGlyph(letter, true); + } + } + } + + return letter; } } // NL3D diff --git a/code/nel/src/gui/ctrl_text_button.cpp b/code/nel/src/gui/ctrl_text_button.cpp index 81dfcc953..6e1fab26e 100644 --- a/code/nel/src/gui/ctrl_text_button.cpp +++ b/code/nel/src/gui/ctrl_text_button.cpp @@ -899,7 +899,7 @@ namespace NLGUI } if (!(_SizeRef & 2)) { - _H= _BmpH; + _H= max(_BmpH, _ViewText->getH()); } CViewBase::updateCoords(); diff --git a/code/nel/src/gui/group_html.cpp b/code/nel/src/gui/group_html.cpp index b606a093d..efea31898 100644 --- a/code/nel/src/gui/group_html.cpp +++ b/code/nel/src/gui/group_html.cpp @@ -2281,8 +2281,8 @@ namespace NLGUI { newParagraph(PBeginSpace); pushStyle(); - if (present[HTML_BLOCK_STYLE] && value[HTML_BLOCK_STYLE]) - getStyleParams(value[HTML_BLOCK_STYLE], _Style); + if (present[MY_HTML_P_STYLE] && value[MY_HTML_P_STYLE]) + getStyleParams(value[MY_HTML_P_STYLE], _Style); } break; case HTML_PRE: diff --git a/code/nel/src/gui/interface_group.cpp b/code/nel/src/gui/interface_group.cpp index 5640da530..789030a4a 100644 --- a/code/nel/src/gui/interface_group.cpp +++ b/code/nel/src/gui/interface_group.cpp @@ -1407,6 +1407,9 @@ namespace NLGUI // ------------------------------------------------------------------------------------------------ void CInterfaceGroup::checkCoords() { + // Make XReal same as in updateCoords() as some elements (CViewText) depends on it + _XReal += _MarginLeft; + //update all children elements vector::const_iterator ite; for (ite = _EltOrder.begin() ; ite != _EltOrder.end(); ite++) @@ -1415,7 +1418,9 @@ namespace NLGUI if(pIE->getActive()) pIE->checkCoords(); } - executeLuaScriptOnDraw(); + + _XReal -= _MarginLeft; + executeLuaScriptOnDraw(); } // ------------------------------------------------------------------------------------------------ diff --git a/code/nel/src/gui/libwww.cpp b/code/nel/src/gui/libwww.cpp index 2e9b4451a..f00a92749 100644 --- a/code/nel/src/gui/libwww.cpp +++ b/code/nel/src/gui/libwww.cpp @@ -213,6 +213,7 @@ namespace NLGUI HTML_ATTR(P,QUICK_HELP_EVENTS), HTML_ATTR(P,QUICK_HELP_LINK), HTML_ATTR(P,NAME), + HTML_ATTR(P,STYLE), { 0 } }; diff --git a/code/nel/src/misc/common.cpp b/code/nel/src/misc/common.cpp index 599f3cfb2..48e6e9845 100644 --- a/code/nel/src/misc/common.cpp +++ b/code/nel/src/misc/common.cpp @@ -1720,12 +1720,12 @@ static bool openDocWithExtension (const std::string &document, const std::string const char *previousEnv = getenv("LD_LIBRARY_PATH"); // clear LD_LIBRARY_PATH to avoid problems with Steam Runtime - setenv("LD_LIBRARY_PATH", "", 1); + if (previousEnv) setenv("LD_LIBRARY_PATH", "", 1); bool res = launchProgram(command, document); // restore previous LD_LIBRARY_PATH - setenv("LD_LIBRARY_PATH", previousEnv, 1); + if (previousEnv) setenv("LD_LIBRARY_PATH", previousEnv, 1); return res; #endif // NL_OS_WINDOWS diff --git a/code/nel/src/misc/file.cpp b/code/nel/src/misc/file.cpp index 414a10e5a..54ffdfbc6 100644 --- a/code/nel/src/misc/file.cpp +++ b/code/nel/src/misc/file.cpp @@ -185,7 +185,7 @@ bool CIFile::open(const std::string &path, bool text) // Bigfile or xml pack access requested ? string::size_type pos; - if ((pos = path.find('@')) != string::npos) + if (!CFile::fileExists(path) && (pos = path.find('@')) != string::npos) { // check for a double @ to identify XML pack file if (pos+1 < path.size() && path[pos+1] == '@') diff --git a/code/nel/src/sound/CMakeLists.txt b/code/nel/src/sound/CMakeLists.txt index e4831c643..7b9eebc9f 100644 --- a/code/nel/src/sound/CMakeLists.txt +++ b/code/nel/src/sound/CMakeLists.txt @@ -1,6 +1,10 @@ FILE(GLOB SRC *.cpp *.h) FILE(GLOB HEADERS ../../include/nel/sound/*.h) +IF(NOT FFMPEG_FOUND) + LIST(REMOVE_ITEM SRC ${CMAKE_CURRENT_SOURCE_DIR}/audio_decoder_ffmpeg.cpp) + LIST(REMOVE_ITEM HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/../../include/nel/sound/audio_decoder_ffmpeg.h) +ENDIF() FILE(GLOB ANIMATION sound_anim_manager.cpp ../../include/nel/sound/sound_anim_manager.h @@ -58,6 +62,7 @@ FILE(GLOB STREAM FILE(GLOB STREAM_FILE audio_decoder.cpp ../../include/nel/sound/audio_decoder.h audio_decoder_vorbis.cpp ../../include/nel/sound/audio_decoder_vorbis.h + audio_decoder_ffmpeg.cpp ../../include/nel/sound/audio_decoder_ffmpeg.h stream_file_sound.cpp ../../include/nel/sound/stream_file_sound.h stream_file_source.cpp ../../include/nel/sound/stream_file_source.h ) @@ -95,6 +100,12 @@ IF(WITH_STATIC) TARGET_LINK_LIBRARIES(nelsound ${OGG_LIBRARY}) ENDIF() +IF(FFMPEG_FOUND) + ADD_DEFINITIONS(-DFFMPEG_ENABLED) + INCLUDE_DIRECTORIES(${FFMPEG_INCLUDE_DIRS}) + TARGET_LINK_LIBRARIES(nelsound ${FFMPEG_LIBRARIES}) +ENDIF() + INCLUDE_DIRECTORIES(${LIBXML2_INCLUDE_DIR}) diff --git a/code/nel/src/sound/audio_decoder.cpp b/code/nel/src/sound/audio_decoder.cpp index 6e9e42b61..f0eb80efd 100644 --- a/code/nel/src/sound/audio_decoder.cpp +++ b/code/nel/src/sound/audio_decoder.cpp @@ -37,6 +37,10 @@ // Project includes #include +#ifdef FFMPEG_ENABLED +#include +#endif + using namespace std; using namespace NLMISC; @@ -82,6 +86,17 @@ IAudioDecoder *IAudioDecoder::createAudioDecoder(const std::string &type, NLMISC nlwarning("Stream is NULL"); return NULL; } +#ifdef FFMPEG_ENABLED + try { + CAudioDecoderFfmpeg *decoder = new CAudioDecoderFfmpeg(stream, loop); + return static_cast(decoder); + } + catch(const Exception &e) + { + nlwarning("Exception %s during ffmpeg setup", e.what()); + return NULL; + } +#else std::string type_lower = toLower(type); if (type_lower == "ogg") { @@ -92,23 +107,32 @@ IAudioDecoder *IAudioDecoder::createAudioDecoder(const std::string &type, NLMISC nlwarning("Music file type unknown: '%s'", type_lower.c_str()); return NULL; } +#endif } bool IAudioDecoder::getInfo(const std::string &filepath, std::string &artist, std::string &title, float &length) { std::string lookup = CPath::lookup(filepath, false); if (lookup.empty()) - { + { nlwarning("Music file %s does not exist!", filepath.c_str()); - return false; + return false; } + +#ifdef FFMPEG_ENABLED + CIFile ifile; + ifile.setCacheFileOnOpen(false); + ifile.allowBNPCacheFileOnOpen(false); + if (ifile.open(lookup)) + return CAudioDecoderFfmpeg::getInfo(&ifile, artist, title, length); +#else std::string type = CFile::getExtension(filepath); std::string type_lower = NLMISC::toLower(type); if (type_lower == "ogg") { - CIFile ifile; - ifile.setCacheFileOnOpen(false); + CIFile ifile; + ifile.setCacheFileOnOpen(false); ifile.allowBNPCacheFileOnOpen(false); if (ifile.open(lookup)) return CAudioDecoderVorbis::getInfo(&ifile, artist, title, length); @@ -119,6 +143,7 @@ bool IAudioDecoder::getInfo(const std::string &filepath, std::string &artist, st { nlwarning("Music file type unknown: '%s'", type_lower.c_str()); } +#endif artist.clear(); title.clear(); return false; @@ -132,6 +157,11 @@ void IAudioDecoder::getMusicExtensions(std::vector &extensions) { extensions.push_back("ogg"); } +#ifdef FFMPEG_ENABLED + extensions.push_back("mp3"); + extensions.push_back("flac"); + extensions.push_back("aac"); +#endif // extensions.push_back("wav"); // TODO: Easy. } @@ -139,7 +169,11 @@ void IAudioDecoder::getMusicExtensions(std::vector &extensions) /// Return if a music extension is supported by the nel sound library. bool IAudioDecoder::isMusicExtensionSupported(const std::string &extension) { +#ifdef FFMPEG_ENABLED + return (extension == "ogg" || extension == "mp3" || extension == "flac" || extension == "aac"); +#else return (extension == "ogg"); +#endif } } /* namespace NLSOUND */ diff --git a/code/nel/src/sound/audio_decoder_ffmpeg.cpp b/code/nel/src/sound/audio_decoder_ffmpeg.cpp new file mode 100644 index 000000000..dea8368d2 --- /dev/null +++ b/code/nel/src/sound/audio_decoder_ffmpeg.cpp @@ -0,0 +1,435 @@ +// NeL - MMORPG Framework +// Copyright (C) 2018 Winch Gate Property Limited +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + + +#include "stdsound.h" + +#include + +#define __STDC_CONSTANT_MACROS +extern "C" +{ +#include +#include +#include +#include +} + +using namespace std; +using namespace NLMISC; +using namespace NLSOUND; + +// Visual Studio does not support AV_TIME_BASE_Q macro in C++ +#undef AV_TIME_BASE_Q +static const AVRational AV_TIME_BASE_Q = {1, AV_TIME_BASE}; + +namespace { + +const std::string av_err2string(sint err) +{ + char buf[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(err, buf, AV_ERROR_MAX_STRING_SIZE); + return (std::string)buf; +} + +void nel_logger(void *ptr, int level, const char *fmt, va_list vargs) +{ + static char msg[1024]; + + const char *module = NULL; + + // AV_LOG_DEBUG, AV_LOG_TRACE + if (level >= AV_LOG_DEBUG) return; + + if (ptr) + { + AVClass *avc = *(AVClass**) ptr; + module = avc->item_name(ptr); + } + + vsnprintf(msg, sizeof(msg), fmt, vargs); + msg[sizeof(msg)-1] = '\0'; + + switch(level) + { + case AV_LOG_PANIC: + // ffmpeg is about to crash so lets throw + nlerror("FFMPEG(P): (%s) %s", module, msg); + break; + case AV_LOG_FATAL: + // ffmpeg had unrecoverable error, corrupted stream or such + nlerrornoex("FFMPEG(F): (%s) %s", module, msg); + break; + case AV_LOG_ERROR: + nlwarning("FFMPEG(E): (%s) %s", module, msg); + break; + case AV_LOG_WARNING: + nlwarning("FFMPEG(W): (%s) %s", module, msg); + break; + case AV_LOG_INFO: + nlinfo("FFMPEG(I): (%s) %s", module, msg); + break; + case AV_LOG_VERBOSE: + nldebug("FFMPEG(V): (%s) %s", module, msg); + break; + case AV_LOG_DEBUG: + nldebug("FFMPEG(D): (%s) %s", module, msg); + break; + default: + nlinfo("FFMPEG: invalid log level:%d (%s) %s", level, module, msg); + break; + } +} + +class CFfmpegInstance +{ +public: + CFfmpegInstance() + { + av_log_set_level(AV_LOG_DEBUG); + av_log_set_callback(nel_logger); + + av_register_all(); + + //avformat_network_init(); + } + + virtual ~CFfmpegInstance() + { + //avformat_network_deinit(); + } +}; + +CFfmpegInstance ffmpeg; + +// Send bytes to ffmpeg +int avio_read_packet(void *opaque, uint8 *buf, int buf_size) +{ + NLSOUND::CAudioDecoderFfmpeg *decoder = static_cast(opaque); + NLMISC::IStream *stream = decoder->getStream(); + nlassert(stream->isReading()); + + uint32 available = decoder->getStreamSize() - stream->getPos(); + if (available == 0) return 0; + + buf_size = FFMIN(buf_size, available); + stream->serialBuffer((uint8 *)buf, buf_size); + return buf_size; +} + +sint64 avio_seek(void *opaque, sint64 offset, int whence) +{ + NLSOUND::CAudioDecoderFfmpeg *decoder = static_cast(opaque); + NLMISC::IStream *stream = decoder->getStream(); + nlassert(stream->isReading()); + + NLMISC::IStream::TSeekOrigin origin; + switch(whence) + { + case SEEK_SET: + origin = NLMISC::IStream::begin; + break; + case SEEK_CUR: + origin = NLMISC::IStream::current; + break; + case SEEK_END: + origin = NLMISC::IStream::end; + break; + case AVSEEK_SIZE: + return decoder->getStreamSize(); + default: + return -1; + } + + stream->seek((sint32) offset, origin); + return stream->getPos(); +} + +}//ns + +namespace NLSOUND { + +// swresample will convert audio to this format +#define FFMPEG_SAMPLE_RATE 44100 +#define FFMPEG_CHANNELS 2 +#define FFMPEG_CHANNEL_LAYOUT AV_CH_LAYOUT_STEREO +#define FFMPEG_BITS_PER_SAMPLE 16 +#define FFMPEG_SAMPLE_FORMAT AV_SAMPLE_FMT_S16 + +CAudioDecoderFfmpeg::CAudioDecoderFfmpeg(NLMISC::IStream *stream, bool loop) +: IAudioDecoder(), + _Stream(stream), _Loop(loop), _IsMusicEnded(false), _StreamSize(0), _IsSupported(false), + _AvioContext(NULL), _FormatContext(NULL), + _AudioContext(NULL), _AudioStreamIndex(0), + _SwrContext(NULL) +{ + _StreamOffset = stream->getPos(); + stream->seek(0, NLMISC::IStream::end); + _StreamSize = stream->getPos(); + stream->seek(_StreamOffset, NLMISC::IStream::begin); + + try { + _FormatContext = avformat_alloc_context(); + if (!_FormatContext) + throw Exception("Can't create AVFormatContext"); + + // avio_ctx_buffer can be reallocated by ffmpeg and assigned to avio_ctx->buffer + uint8 *avio_ctx_buffer = NULL; + size_t avio_ctx_buffer_size = 4096; + avio_ctx_buffer = static_cast(av_malloc(avio_ctx_buffer_size)); + if (!avio_ctx_buffer) + throw Exception("Can't allocate avio context buffer"); + + _AvioContext = avio_alloc_context(avio_ctx_buffer, avio_ctx_buffer_size, 0, this, &avio_read_packet, NULL, &avio_seek); + if (!_AvioContext) + throw Exception("Can't allocate avio context"); + + _FormatContext->pb = _AvioContext; + sint ret = avformat_open_input(&_FormatContext, NULL, NULL, NULL); + if (ret < 0) + throw Exception("avformat_open_input: %d", ret); + + // find stream and then audio codec to see if ffmpeg supports this + _IsSupported = false; + if (avformat_find_stream_info(_FormatContext, NULL) >= 0) + { + _AudioStreamIndex = av_find_best_stream(_FormatContext, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0); + if (_AudioStreamIndex >= 0) + { + _AudioContext = _FormatContext->streams[_AudioStreamIndex]->codec; + av_opt_set_int(_AudioContext, "refcounted_frames", 1, 0); + + AVCodec *codec = avcodec_find_decoder(_AudioContext->codec_id); + if (codec != NULL && avcodec_open2(_AudioContext, codec, NULL) >= 0) + { + _IsSupported = true; + } + } + } + } + catch(...) + { + release(); + + throw; + } + + if (!_IsSupported) + { + nlwarning("FFMPEG: Decoder created, unknown stream format / codec"); + } +} + +CAudioDecoderFfmpeg::~CAudioDecoderFfmpeg() +{ + release(); +} + +void CAudioDecoderFfmpeg::release() +{ + if (_SwrContext) + swr_free(&_SwrContext); + + if (_AudioContext) + avcodec_close(_AudioContext); + + if (_FormatContext) + avformat_close_input(&_FormatContext); + + if (_AvioContext && _AvioContext->buffer) + av_freep(&_AvioContext->buffer); + + if (_AvioContext) + av_freep(&_AvioContext); +} + +bool CAudioDecoderFfmpeg::isFormatSupported() const +{ + return _IsSupported; +} + +/// Get information on a music file. +bool CAudioDecoderFfmpeg::getInfo(NLMISC::IStream *stream, std::string &artist, std::string &title, float &length) +{ + CAudioDecoderFfmpeg ffmpeg(stream, false); + if (!ffmpeg.isFormatSupported()) + { + title.clear(); + artist.clear(); + length = 0.f; + + return false; + } + + AVDictionaryEntry *tag = NULL; + while((tag = av_dict_get(ffmpeg._FormatContext->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) + { + if (!strcmp(tag->key, "artist")) + { + artist = tag->value; + } + else if (!strcmp(tag->key, "title")) + { + title = tag->value; + } + } + + if (ffmpeg._FormatContext->duration != AV_NOPTS_VALUE) + { + length = ffmpeg._FormatContext->duration * av_q2d(AV_TIME_BASE_Q); + } + else if (ffmpeg._FormatContext->streams[ffmpeg._AudioStreamIndex]->duration != AV_NOPTS_VALUE) + { + length = ffmpeg._FormatContext->streams[ffmpeg._AudioStreamIndex]->duration * av_q2d(ffmpeg._FormatContext->streams[ffmpeg._AudioStreamIndex]->time_base); + } + else + { + length = 0.f; + } + + return true; +} + +uint32 CAudioDecoderFfmpeg::getRequiredBytes() +{ + return 0; // no minimum requirement of bytes to buffer out +} + +uint32 CAudioDecoderFfmpeg::getNextBytes(uint8 *buffer, uint32 minimum, uint32 maximum) +{ + if (_IsMusicEnded) return 0; + nlassert(minimum <= maximum); // can't have this.. + + // TODO: CStreamFileSource::play() will stall when there is no frames on warmup + // supported can be set false if there is an issue creating converter + if (!_IsSupported) + { + _IsMusicEnded = true; + return 1; + } + + uint32 bytes_read = 0; + + AVFrame frame = {0}; + AVPacket packet = {0}; + + if (!_SwrContext) + { + sint64 in_channel_layout = av_get_default_channel_layout(_AudioContext->channels); + _SwrContext = swr_alloc_set_opts(NULL, + // output + FFMPEG_CHANNEL_LAYOUT, FFMPEG_SAMPLE_FORMAT, FFMPEG_SAMPLE_RATE, + // input + in_channel_layout, _AudioContext->sample_fmt, _AudioContext->sample_rate, + 0, NULL); + swr_init(_SwrContext); + } + + sint ret; + while(bytes_read < minimum) + { + // read packet from stream + if ((ret = av_read_frame(_FormatContext, &packet)) < 0) + { + _IsMusicEnded = true; + // TODO: looping + break; + } + + if (packet.stream_index == _AudioStreamIndex) + { + // packet can contain multiple frames + AVPacket first = packet; + int got_frame = 0; + do { + got_frame = 0; + ret = avcodec_decode_audio4(_AudioContext, &frame, &got_frame, &packet); + if (ret < 0) + { + nlwarning("FFMPEG: error decoding audio frame: %s", av_err2string(ret).c_str()); + break; + } + packet.size -= ret; + packet.data += ret; + + if (got_frame) + { + uint32 out_bps = av_get_bytes_per_sample(FFMPEG_SAMPLE_FORMAT) * FFMPEG_CHANNELS; + uint32 max_samples = (maximum - bytes_read) / out_bps; + + uint32 out_samples = av_rescale_rnd(swr_get_delay(_SwrContext, _AudioContext->sample_rate) + frame.nb_samples, + FFMPEG_SAMPLE_RATE, _AudioContext->sample_rate, AV_ROUND_UP); + + if (max_samples > out_samples) + max_samples = out_samples; + + uint32 converted = swr_convert(_SwrContext, &buffer, max_samples, (const uint8 **)frame.extended_data, frame.nb_samples); + uint32 size = out_bps * converted; + + bytes_read += size; + buffer += size; + + av_frame_unref(&frame); + } + } while (got_frame && packet.size > 0); + + av_packet_unref(&first); + } + else + { + ret = 0; + av_packet_unref(&packet); + } + } + + return bytes_read; +} + +uint8 CAudioDecoderFfmpeg::getChannels() +{ + return FFMPEG_CHANNELS; +} + +uint CAudioDecoderFfmpeg::getSamplesPerSec() +{ + return FFMPEG_SAMPLE_RATE; +} + +uint8 CAudioDecoderFfmpeg::getBitsPerSample() +{ + return FFMPEG_BITS_PER_SAMPLE; +} + +bool CAudioDecoderFfmpeg::isMusicEnded() +{ + return _IsMusicEnded; +} + +float CAudioDecoderFfmpeg::getLength() +{ + printf(">> CAudioDecoderFfmpeg::getLength\n"); + // TODO: return (float)ov_time_total(&_OggVorbisFile, -1); + return 0.f; +} + +void CAudioDecoderFfmpeg::setLooping(bool loop) +{ + _Loop = loop; +} + +} /* namespace NLSOUND */ + +/* end of file */ diff --git a/code/nel/src/sound/stream_file_source.cpp b/code/nel/src/sound/stream_file_source.cpp index 185b7514f..9a03ba2d8 100644 --- a/code/nel/src/sound/stream_file_source.cpp +++ b/code/nel/src/sound/stream_file_source.cpp @@ -45,7 +45,7 @@ using namespace std; namespace NLSOUND { CStreamFileSource::CStreamFileSource(CStreamFileSound *streamFileSound, bool spawn, TSpawnEndCallback cb, void *cbUserParam, NL3D::CCluster *cluster, CGroupController *groupController) -: CStreamSource(streamFileSound, spawn, cb, cbUserParam, cluster, groupController), m_AudioDecoder(NULL), m_Paused(false) +: CStreamSource(streamFileSound, spawn, cb, cbUserParam, cluster, groupController), m_AudioDecoder(NULL), m_Paused(false), m_DecodingEnded(false) { m_Thread = NLMISC::IThread::create(this); } @@ -244,7 +244,7 @@ void CStreamFileSource::resume() bool CStreamFileSource::isEnded() { - return (!m_Thread->isRunning() && !_Playing && !m_WaitingForPlay && !m_Paused); + return m_DecodingEnded || (!m_Thread->isRunning() && !_Playing && !m_WaitingForPlay && !m_Paused); } float CStreamFileSource::getLength() @@ -319,6 +319,7 @@ void CStreamFileSource::run() this->getRecommendedBufferSize(samples, bytes); uint32 recSleep = 40; uint32 doSleep = 10; + m_DecodingEnded = false; while (_Playing || m_WaitingForPlay) { if (!m_AudioDecoder->isMusicEnded()) @@ -369,6 +370,9 @@ void CStreamFileSource::run() { delete m_AudioDecoder; m_AudioDecoder = NULL; + // _Playing cannot be used to detect play state because its required in cleanup + // Using m_AudioDecoder in isEnded() may result race condition (decoder is only created after thread is started) + m_DecodingEnded = true; } // drop buffers m_FreeBuffers = 3; diff --git a/code/ryzom/client/src/connection.cpp b/code/ryzom/client/src/connection.cpp index 92fce64f8..045822370 100644 --- a/code/ryzom/client/src/connection.cpp +++ b/code/ryzom/client/src/connection.cpp @@ -201,9 +201,9 @@ void connectionRestoreVideoMode () mode.Height = height; } - // don't allow sizes smaller than 800x600 - if (ClientCfg.Width < 800) ClientCfg.Width = 800; - if (ClientCfg.Height < 600) ClientCfg.Height = 600; + // don't allow sizes smaller than 1024x768 + if (ClientCfg.Width < 1024) ClientCfg.Width = 1024; + if (ClientCfg.Height < 768) ClientCfg.Height = 768; if (StereoDisplay) StereoDisplayAttached = StereoDisplay->attachToDisplay(); @@ -1252,6 +1252,16 @@ TInterfaceState globalMenu() // Restore video mode if (ClientCfg.SelectCharacter == -1) { + if (ClientCfg.Windowed) + { + // if used changed window resolution in char select + // if we don't update ClientCfg, then UI from icfg is restored wrong + uint32 width, height; + Driver->getWindowSize(width, height); + ClientCfg.Width = width; + ClientCfg.Height = height; + } + connectionRestoreVideoMode (); } diff --git a/code/ryzom/client/src/interface_v3/group_map.cpp b/code/ryzom/client/src/interface_v3/group_map.cpp index 37a0a8a57..e6cfcf824 100644 --- a/code/ryzom/client/src/interface_v3/group_map.cpp +++ b/code/ryzom/client/src/interface_v3/group_map.cpp @@ -2665,18 +2665,8 @@ CCtrlButton *CGroupMap::addUserLandMark(const NLMISC::CVector2f &pos, const ucst addLandMark(_UserLM, pos, title, getUserLandMarkOptions((uint32)_CurContinent->UserLandMarks.size() - 1)); // Save the config file each time a user landmark is created - CInterfaceManager *pIM = CInterfaceManager::getInstance(); - uint8 currMode = pIM->getMode(); - std::string filename = "save/interface_" + PlayerSelectedFileName + ".icfg"; - if (!CFile::fileExists(filename) && CFile::fileExists("save/shared_interface.icfg")) - { - filename = "save/shared_interface.icfg"; - } - pIM->saveConfig (filename); - if (currMode != pIM->getMode()) - { - pIM->setMode(currMode); - } + CInterfaceManager::getInstance()->saveConfig(); + return _UserLM.back(); } diff --git a/code/ryzom/client/src/interface_v3/interface_manager.cpp b/code/ryzom/client/src/interface_v3/interface_manager.cpp index ae6d0e538..7684accd8 100644 --- a/code/ryzom/client/src/interface_v3/interface_manager.cpp +++ b/code/ryzom/client/src/interface_v3/interface_manager.cpp @@ -1272,32 +1272,19 @@ void CInterfaceManager::loadInterfaceConfig() // ------------------------------------------------------------------------------------------------ void CInterfaceManager::uninitInGame0 () { - // Autosave of the keys if (_KeysLoaded) { - if (!ClientCfg.R2EDEnabled) - { - string filename = "save/keys_" + PlayerSelectedFileName + ".xml"; - if (!CFile::fileExists(filename) && CFile::fileExists("save/shared_keys.xml")) - filename = "save/shared_keys.xml"; + saveKeys(); - saveKeys(filename); - } _KeysLoaded = false; } // Autosave of the interface in interface.cfg if (_ConfigLoaded) { - if (!ClientCfg.R2EDEnabled) - { - string filename = "save/interface_" + PlayerSelectedFileName + ".icfg"; - if (!CFile::fileExists(filename) && CFile::fileExists("save/shared_interface.icfg")) - filename = "save/shared_interface.icfg"; + saveConfig(); - saveConfig(filename); - } _ConfigLoaded = false; } } @@ -1890,6 +1877,29 @@ public: } }; +// ------------------------------------------------------------------------------------------------ +// +bool CInterfaceManager::saveConfig (bool verbose) +{ + bool ret = true; + + if (!ClientCfg.R2EDEnabled) + { + uint8 currMode = getMode(); + + string filename = "save/interface_" + PlayerSelectedFileName + ".icfg"; + if (!CFile::fileExists(filename) && CFile::fileExists("save/shared_interface.icfg")) + filename = "save/shared_interface.icfg"; + + if (verbose) CInterfaceManager::getInstance()->displaySystemInfo("Saving " + filename); + ret = saveConfig(filename); + + if (currMode != getMode()) + setMode(currMode); + } + + return ret; +} // ------------------------------------------------------------------------------------------------ bool CInterfaceManager::saveConfig (const string &filename) @@ -2735,7 +2745,25 @@ void writeMacros (xmlNodePtr node) } // *************************************************************************** +bool CInterfaceManager::saveKeys(bool verbose) +{ + bool ret = true; + if (!ClientCfg.R2EDEnabled) + { + string filename = "save/keys_" + PlayerSelectedFileName + ".xml"; + if (!CFile::fileExists(filename) && CFile::fileExists("save/shared_keys.xml")) + filename = "save/shared_keys.xml"; + + if (verbose) CInterfaceManager::getInstance()->displaySystemInfo("Saving " + filename); + + ret = saveKeys(filename); + } + + return ret; +} + +// *************************************************************************** bool CInterfaceManager::saveKeys(const std::string &filename) { bool ret = false; @@ -2936,6 +2964,18 @@ void CInterfaceManager::displayWebWindow(const string & name, const string & url CAHManager::getInstance()->runActionHandler("browse", NULL, "name="+name+":content:html|url="+url); } + +// *************************************************************************** +class CAHSaveUI : public IActionHandler +{ + virtual void execute (CCtrlBase *pCaller, const string &Params) + { + CInterfaceManager::getInstance()->saveKeys(true); + CInterfaceManager::getInstance()->saveConfig(true); + } +}; +REGISTER_ACTION_HANDLER (CAHSaveUI, "save_ui"); + /* // *************************************************************************** class CHandlerDispWebOnQuit : public IActionHandler diff --git a/code/ryzom/client/src/interface_v3/interface_manager.h b/code/ryzom/client/src/interface_v3/interface_manager.h index 6dbe164a0..652d692b3 100644 --- a/code/ryzom/client/src/interface_v3/interface_manager.h +++ b/code/ryzom/client/src/interface_v3/interface_manager.h @@ -205,10 +205,14 @@ public: // Load/Save position, size, etc.. of windows bool loadConfig (const std::string &filename); + // Save config to default location, if verbose is true, display message in game sysinfo + bool saveConfig (bool verbose = false); bool saveConfig (const std::string &filename); // delete the user config (give the player ident fileName) bool deletePlayerConfig (const std::string &playerFileIdent); + // Save keys to default location, if verbose is true, display message in game sysinfo + bool saveKeys (bool verbose = false); // Save the keys config file bool saveKeys (const std::string &filename); // delete the user Keysconfig (give the player ident fileName) diff --git a/code/ryzom/client/src/interface_v3/music_player.cpp b/code/ryzom/client/src/interface_v3/music_player.cpp index b9cadad5c..0670c2935 100644 --- a/code/ryzom/client/src/interface_v3/music_player.cpp +++ b/code/ryzom/client/src/interface_v3/music_player.cpp @@ -39,6 +39,9 @@ extern UDriver *Driver; #define TEMPLATE_PLAYLIST_SONG "playlist_song" #define TEMPLATE_PLAYLIST_SONG_TITLE "title" #define TEMPLATE_PLAYLIST_SONG_DURATION "duration" +// ui state +#define MP3_SAVE_SHUFFLE "UI:SAVE:MP3_SHUFFLE" +#define MP3_SAVE_REPEAT "UI:SAVE:MP3_REPEAT" static const std::string MediaPlayerDirectory("music/"); @@ -48,8 +51,20 @@ CMusicPlayer MusicPlayer; CMusicPlayer::CMusicPlayer () { - _CurrentSong = 0; + _CurrentSongIndex = 0; _State = Stopped; + _PlayStart = 0; + _PauseTime = 0; +} + +bool CMusicPlayer::isRepeatEnabled() const +{ + return (NLGUI::CDBManager::getInstance()->getDbProp(MP3_SAVE_REPEAT)->getValue32() == 1); +} + +bool CMusicPlayer::isShuffleEnabled() const +{ + return (NLGUI::CDBManager::getInstance()->getDbProp(MP3_SAVE_SHUFFLE)->getValue32() == 1); } @@ -59,16 +74,61 @@ void CMusicPlayer::playSongs (const std::vector &songs) _Songs = songs; // reset song index if out of bounds - if (_CurrentSong > _Songs.size()) - _CurrentSong = 0; + if (_CurrentSongIndex > _Songs.size()) + _CurrentSongIndex = 0; + if (isShuffleEnabled()) + shuffleAndRebuildPlaylist(); + else + rebuildPlaylist(); + + // If pause, stop, else play will resume + if (_State == Paused) + _State = Stopped; +} + +// *************************************************************************** +void CMusicPlayer::updatePlaylist(sint prevIndex) +{ + CInterfaceElement *pIE; + std::string rowId; + + if (prevIndex >= 0 && prevIndex < _Songs.size()) + { + rowId = toString("%s:s%d:bg", MP3_PLAYER_PLAYLIST_LIST, prevIndex); + pIE = dynamic_cast(CWidgetManager::getInstance()->getElementFromId(rowId)); + if (pIE) pIE->setActive(false); + } + + rowId = toString("%s:s%d:bg", MP3_PLAYER_PLAYLIST_LIST, _CurrentSongIndex); + pIE = dynamic_cast(CWidgetManager::getInstance()->getElementFromId(rowId)); + if (pIE) pIE->setActive(true); +} + +// *************************************************************************** +void CMusicPlayer::shuffleAndRebuildPlaylist() +{ + std::random_shuffle(_Songs.begin(), _Songs.end()); + rebuildPlaylist(); +} + +// *************************************************************************** +void CMusicPlayer::rebuildPlaylist() +{ CGroupList *pList = dynamic_cast(CWidgetManager::getInstance()->getElementFromId(MP3_PLAYER_PLAYLIST_LIST)); if (pList) { pList->clearGroups(); pList->setDynamicDisplaySize(true); + bool found = _CurrentSong.Filename.empty(); for (uint i=0; i < _Songs.size(); ++i) { + if (!found && _CurrentSong.Filename == _Songs[i].Filename) + { + found = true; + _CurrentSongIndex = i; + } + uint min = (sint32)(_Songs[i].Length / 60) % 60; uint sec = (sint32)(_Songs[i].Length) % 60; uint hour = _Songs[i].Length / 3600; @@ -103,9 +163,7 @@ void CMusicPlayer::playSongs (const std::vector &songs) pList->invalidateCoords(); } - // If pause, stop, else play will resume - if (_State == Paused) - _State = Stopped; + updatePlaylist(); } @@ -116,31 +174,40 @@ void CMusicPlayer::play (sint index) if(!SoundMngr) return; + sint prevSongIndex = _CurrentSongIndex; + if (index >= 0 && index < (sint)_Songs.size()) { if (_State == Paused) + { stop(); + } - _CurrentSong = index; + _CurrentSongIndex = index; + _PauseTime = 0; } if (!_Songs.empty()) { - nlassert (_CurrentSong<_Songs.size()); + nlassert (_CurrentSongIndex<_Songs.size()); /* If the player is paused, resume, else, play the current song */ if (_State == Paused) + { SoundMngr->resumeMusic(); + } else - SoundMngr->playMusic(_Songs[_CurrentSong].Filename, 0, true, false, false); + { + SoundMngr->playMusic(_Songs[_CurrentSongIndex].Filename, 0, true, false, false); + _PauseTime = 0; + } _State = Playing; + _PlayStart = CTime::getLocalTime() - _PauseTime; - /* Show the song title */ - CInterfaceManager *pIM = CInterfaceManager::getInstance(); - CViewText *pVT = dynamic_cast(CWidgetManager::getInstance()->getElementFromId("ui:interface:mp3_player:screen:text")); - if (pVT) - pVT->setText (ucstring::makeFromUtf8(_Songs[_CurrentSong].Title)); + _CurrentSong = _Songs[_CurrentSongIndex]; + + updatePlaylist(prevSongIndex); } } @@ -155,6 +222,9 @@ void CMusicPlayer::pause () { SoundMngr->pauseMusic(); _State = Paused; + + if (_PlayStart > 0) + _PauseTime = CTime::getLocalTime() - _PlayStart; } } @@ -167,6 +237,8 @@ void CMusicPlayer::stop () // stop the music only if we are really playing (else risk to stop a background music!) SoundMngr->stopMusic(0); _State = Stopped; + _PlayStart = 0; + _PauseTime = 0; } // *************************************************************************** @@ -176,12 +248,13 @@ void CMusicPlayer::previous () if (!_Songs.empty()) { // Point the previous song - if (_CurrentSong == 0) - _CurrentSong = (uint)_Songs.size()-1; + sint index; + if (_CurrentSongIndex == 0) + index = (uint)_Songs.size()-1; else - _CurrentSong--; + index = _CurrentSongIndex-1; - play (); + play(index); } } @@ -191,9 +264,11 @@ void CMusicPlayer::next () { if (!_Songs.empty()) { - _CurrentSong++; - _CurrentSong%=_Songs.size(); - play (); + sint index = _CurrentSongIndex+1; + if (index == _Songs.size()) + index = 0; + + play(index); } } @@ -205,17 +280,35 @@ void CMusicPlayer::update () return; if (_State == Playing) { + CViewText *pVT = dynamic_cast(CWidgetManager::getInstance()->getElementFromId("ui:interface:mp3_player:screen:text")); + if (pVT) + { + TTime dur = (CTime::getLocalTime() - _PlayStart) / 1000; + uint min = (dur / 60) % 60; + uint sec = dur % 60; + uint hour = dur / 3600; + + std::string title(toString("%02d:%02d", min, sec)); + if (hour > 0) title = toString("%02d:", hour) + title; + title += " " + _CurrentSong.Title; + pVT->setText(ucstring::makeFromUtf8(title)); + } + if (SoundMngr->isMusicEnded ()) { - // Point the next song - _CurrentSong++; - _CurrentSong%=_Songs.size(); - - // End of the playlist ? - if (_CurrentSong != 0) + // select next song from playlist + sint index = _CurrentSongIndex + 1; + if (isRepeatEnabled() || index < _Songs.size()) { - // No, play the next song - play (); + if (index == _Songs.size()) + { + index = 0; + + if (isShuffleEnabled()) + shuffleAndRebuildPlaylist(); + } + + play(index); } else { @@ -284,22 +377,10 @@ public: // no format supported if (extensions.empty()) return; - bool oggSupported = false; - bool mp3Supported = false; - std::string message; for(uint i = 0; i < extensions.size(); ++i) { - if (extensions[i] == "ogg") - { - oggSupported = true; - message += " ogg"; - } - else if (extensions[i] == "mp3") - { - mp3Supported = true; - message += " mp3"; - } + message += " " + extensions[i]; } message += " m3u m3u8"; nlinfo("Media player supports: '%s'", message.substr(1).c_str()); @@ -316,15 +397,9 @@ public: for (i = 0; i < filesToProcess.size(); ++i) { std::string ext = toLower(CFile::getExtension(filesToProcess[i])); - if (ext == "ogg") + if (std::find(extensions.begin(), extensions.end(), ext) != extensions.end()) { - if (oggSupported) - filenames.push_back(filesToProcess[i]); - } - else if (ext == "mp3" || ext == "mp2" || ext == "mp1") - { - if (mp3Supported) - filenames.push_back(filesToProcess[i]); + filenames.push_back(filesToProcess[i]); } else if (ext == "m3u" || ext == "m3u8") { @@ -345,14 +420,6 @@ public: std::vector songs; for (i=0; igetMixer()->getSongTitle(filenames[i], song.Title, song.Length); - songs.push_back (song); + if (song.Length > 0) + songs.push_back (song); } MusicPlayer.playSongs(songs); } + else if (Params == "update_playlist") + { + if (MusicPlayer.isShuffleEnabled()) + MusicPlayer.shuffleAndRebuildPlaylist(); + + MusicPlayer.rebuildPlaylist(); + } else if (Params == "previous") MusicPlayer.previous(); else if (Params == "play") diff --git a/code/ryzom/client/src/interface_v3/music_player.h b/code/ryzom/client/src/interface_v3/music_player.h index 33de33c74..7170934d4 100644 --- a/code/ryzom/client/src/interface_v3/music_player.h +++ b/code/ryzom/client/src/interface_v3/music_player.h @@ -55,14 +55,28 @@ public: void update (); + bool isRepeatEnabled() const; + bool isShuffleEnabled() const; + + // Build playlist UI from songs + void rebuildPlaylist(); + // Randomize playlist and rebuild the ui + void shuffleAndRebuildPlaylist(); + // Update playlist active row + void updatePlaylist(sint prevIndex = -1); + private: // The playlist - uint _CurrentSong; // If (!_Songs.empty()) must always be <_Songs.size() + CSongs _CurrentSong; + uint _CurrentSongIndex; // If (!_Songs.empty()) must always be <_Songs.size() std::vector _Songs; // State enum TState { Stopped, Playing, Paused } _State; + + TTime _PlayStart; + TTime _PauseTime; }; extern CMusicPlayer MusicPlayer; diff --git a/code/ryzom/client/src/main_loop.cpp b/code/ryzom/client/src/main_loop.cpp index 4657f1dff..32b76391b 100644 --- a/code/ryzom/client/src/main_loop.cpp +++ b/code/ryzom/client/src/main_loop.cpp @@ -3419,6 +3419,22 @@ void displayDebugClusters() } +NLMISC_COMMAND(dumpFontTexture, "Write font texture to file", "") +{ + CInterfaceManager *im = CInterfaceManager::getInstance(); + if (TextContext) + { + std::string fname = CFile::findNewFile("font-texture.tga"); + TextContext->dumpCacheTexture(fname.c_str()); + im->displaySystemInfo(ucstring(fname + " created"), "SYS"); + } + else + { + im->displaySystemInfo(ucstring("Error: TextContext == NULL"), "SYS"); + } + return true; +} + // *************************************************************************** void inGamePatchUncompleteWarning() diff --git a/code/ryzom/client/src/misc.cpp b/code/ryzom/client/src/misc.cpp index 0f87b8037..97aaea128 100644 --- a/code/ryzom/client/src/misc.cpp +++ b/code/ryzom/client/src/misc.cpp @@ -1404,13 +1404,12 @@ bool getRyzomModes(std::vector &videoModes, std::vectorgetModes(videoModes); - // Remove modes under 800x600 and get the unique strings + // Remove modes under 1024x768 (outgame ui limitation) and get the unique strings sint i, j; for (i = 0; i < (sint)videoModes.size(); ++i) { - if ((videoModes[i].Width < 800) || (videoModes[i].Height < 600)) + if ((videoModes[i].Width < 1024) || (videoModes[i].Height < 768)) { - // discard modes under 800x600 videoModes.erase(videoModes.begin()+i); --i; }