/**************************************************************************** ** ** Copyright (C) 2008-2012 NVIDIA Corporation. ** Copyright (C) 2017 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt 3D Studio. ** ** $QT_BEGIN_LICENSE:GPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 or (at your option) any later version ** approved by the KDE Free Qt Foundation. The licenses are as published by ** the Free Software Foundation and appearing in the file LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "foundation/StringTable.h" #include "foundation/Qt3DSContainers.h" #include "foundation/Qt3DSAtomic.h" #include "foundation/Utils.h" #include "EASTL/string.h" #include "EASTL/sort.h" #include "foundation/StrConvertUTF.h" #include "foundation/PreAllocatedAllocator.h" #include "foundation/SerializationTypes.h" #include "foundation/Qt3DSMutex.h" using namespace qt3ds; using namespace qt3ds::foundation; using namespace eastl; namespace eastl { struct SCharAndHash { const char8_t *m_Data; size_t m_Hash; // Cache the generated hash code. SCharAndHash(const char8_t *inData) : m_Data(inData) , m_Hash(eastl::hash()(inData)) { } SCharAndHash() : m_Data(NULL) , m_Hash(0) { } operator const char *() const { return m_Data; } }; template <> struct hash { size_t operator()(const SCharAndHash &inItem) const { return inItem.m_Hash; } }; template <> struct equal_to { bool operator()(const SCharAndHash &inLhs, const SCharAndHash &inRhs) const { return char_equal_to()(inLhs.m_Data, inRhs.m_Data); } }; } void CRegisteredString::Remap(const IStringTable &inTable) { Remap(const_cast(inTable).GetRemapMap()); } void CRegisteredString::Remap(const SStrRemapMap &inMap) { if (IsValid()) { SStrRemapMap::const_iterator theIter = inMap.find((char8_t *)m_String); if (theIter != inMap.end()) m_String = reinterpret_cast(theIter->second); else { QT3DS_ASSERT(false); // Ensure a failure here doesn't *guarantee* a crash somewhere else. m_String = NULL; } } else { // Indicates an invalid string. m_String = reinterpret_cast(QT3DS_MAX_U32); } } void CRegisteredString::Remap(NVDataRef inDataPtr) { size_t theOffset = reinterpret_cast(m_String); if (theOffset >= QT3DS_MAX_U32) m_String = ""; else { if (theOffset < inDataPtr.size()) m_String = reinterpret_cast(inDataPtr.begin() + theOffset); else { QT3DS_ASSERT(false); m_String = ""; } } } namespace { const char16_t g_char16EmptyStr[] = { 0, 0, 0, 0 }; const char32_t g_char32EmptyStr[] = { 0, 0, 0, 0 }; typedef eastl::basic_string TNarrowStr; typedef eastl::basic_string TWideStr; inline bool isTrivialWide(const wchar_t *str) { return str == NULL || *str == 0; } QT3DSU8 *AlignPointer(QT3DSU8 *inStart, QT3DSU8 *inPtr) { QT3DSU32 alignment = sizeof(void *); size_t numBytes = (size_t)(inPtr - inStart); QT3DS_ASSERT(inStart < inPtr); if (numBytes % alignment) inPtr += alignment - (numBytes % alignment); return inPtr; } QT3DSU32 Align(QT3DSU32 inValue) { QT3DSU32 alignment = sizeof(void *); QT3DSU32 leftover = inValue % alignment; if (leftover) inValue += alignment - leftover; return inValue; } // Structure that is written out to the file. // Directly after this is the string character data. // TWideStr& inConvertStr struct SStringFileData { QT3DSU32 m_StartOffset; QT3DSU32 m_StrLen; QT3DSU32 m_Handle; char8_t *m_Str; SStringFileData(QT3DSU32 len, QT3DSU32 off, QT3DSU32 handle, char8_t *data) : m_StartOffset(off) , m_StrLen(len) , m_Handle(handle) , m_Str(data) { } SStringFileData() : m_StartOffset(0) , m_StrLen(0) , m_Handle(0) , m_Str(NULL) { } void Deallocate(NVAllocatorCallback &inAlloc) { inAlloc.deallocate(m_Str); m_Str = NULL; } const char8_t *GetNarrow() { return m_Str; } operator CRegisteredString() const { return CRegisteredString::ISwearThisHasBeenRegistered(m_Str); } QT3DSU32 EndOffset() const { return m_StartOffset + m_StrLen; } // Used only for the lower bound operation to find a given object by offset. bool operator<(const SStringFileData &inOther) const { return m_StartOffset < inOther.m_StartOffset; } }; struct SCharAndHandle { const char8_t *m_Data; QT3DSU32 m_Handle; SCharAndHandle() : m_Data(NULL) , m_Handle(0) { } SCharAndHandle(const char8_t *data, QT3DSU32 hdl) : m_Data(data) , m_Handle(hdl) { } operator const char *() const { return m_Data; } }; struct SStringFileHeader { QT3DSU32 m_NumStrings; QT3DSU32 m_NextHandleValue; // offset to the items of fixed size. Variable sized data is stored // up front. QT3DSU32 m_FixedBufferOffset; SStringFileHeader() : m_NumStrings(0) , m_NextHandleValue(0) , m_FixedBufferOffset(0) { } SStringFileHeader(QT3DSU32 ns, QT3DSU32 hv, QT3DSU32 fbo) : m_NumStrings(ns) , m_NextHandleValue(hv) , m_FixedBufferOffset(fbo) { } }; // This is the core of the string table. struct SStringFileDataList { typedef nvhash_map TMapType; typedef nvhash_map THandleMapType; mutable SPreAllocatedAllocator m_Allocator; // When we load from a file, we get a these items NVConstDataRef m_DataBlock; NVConstDataRef> m_HashDataBlockOffset; NVConstDataRef> m_HandleDataBlockOffset; // When we are running normally, this items get mangled nvvector m_AppendedStrings; protected: // Built roughly on demand SStrRemapMap m_StrRemapMap; TMapType m_HashToStrMap; THandleMapType m_HandleToStrMap; QT3DSU32 m_NextHandleValue; public: SStringFileDataList(NVAllocatorCallback &inAlloc) : m_Allocator(inAlloc) , m_AppendedStrings(inAlloc, "StringTable::m_AppendedStrings") , m_StrRemapMap(inAlloc, "StringTable::m_StrRemapMap") , m_HashToStrMap(inAlloc, "StringTable::m_HashToStrMap") , m_HandleToStrMap(inAlloc, "StringTable::m_HashToStrMap") , m_NextHandleValue(1) { } ~SStringFileDataList() { for (QT3DSU32 idx = 0, end = m_AppendedStrings.size(); idx < end; ++idx) m_AppendedStrings[idx].Deallocate(m_Allocator); } SStrRemapMap &GetRemapMap() { if (m_StrRemapMap.empty()) { for (QT3DSU32 idx = 0, end = m_DataBlock.size(); idx < end; ++idx) m_StrRemapMap.insert( eastl::make_pair(m_DataBlock[idx].m_Str, m_DataBlock[idx].m_StartOffset)); for (QT3DSU32 idx = 0, end = m_AppendedStrings.size(); idx < end; ++idx) m_StrRemapMap.insert(eastl::make_pair(m_AppendedStrings[idx].m_Str, m_AppendedStrings[idx].m_StartOffset)); } return m_StrRemapMap; } struct SHashPairComparator { bool operator()(const eastl::pair &lhs, const eastl::pair &rhs) const { return lhs.first < rhs.first; } }; struct SHandlePairComparator { bool operator()(const eastl::pair &lhs, const eastl::pair &rhs) const { return lhs.first < rhs.first; } }; SCharAndHandle DoFindStr(SCharAndHash hashCode) { if (isTrivial(hashCode.m_Data)) return SCharAndHandle(); TMapType::iterator theFind = m_HashToStrMap.find(hashCode); if (theFind == m_HashToStrMap.end()) { SCharAndHandle newStr = FindStrByHash(hashCode); if (!newStr.m_Data) newStr = Append(hashCode); // It may not be obvious why we have to reset the data member. // we have to do this so the hashtable keys don't change if the user isn't // passing in a persistent string. hashCode.m_Data = newStr; theFind = m_HashToStrMap.insert(eastl::make_pair(hashCode, newStr)).first; m_HandleToStrMap.insert(eastl::make_pair(theFind->second.m_Handle, newStr)); } return theFind->second; } const CRegisteredString FindStr(SCharAndHash hashCode) { SCharAndHandle result = DoFindStr(hashCode); if (result.m_Data) return CRegisteredString::ISwearThisHasBeenRegistered(result.m_Data); return CRegisteredString(); } const CStringHandle FindStrHandle(SCharAndHash hashCode) { SCharAndHandle result = DoFindStr(hashCode); return CStringHandle::ISwearThisHasBeenRegistered(result.m_Handle); } const CRegisteredString FindStrByHandle(QT3DSU32 handle) { if (handle == 0) return CRegisteredString(); THandleMapType::iterator theFind = m_HandleToStrMap.find(handle); if (theFind == m_HandleToStrMap.end()) { const char8_t *newStr = FindStrByHandleData(handle); if (!newStr) return CRegisteredString(); theFind = m_HandleToStrMap.insert(eastl::make_pair(handle, newStr)).first; } return CRegisteredString::ISwearThisHasBeenRegistered(theFind->second); } void Save(SWriteBuffer &ioBuffer) const { // Buffer should be aligned before we get to it. QT3DS_ASSERT(ioBuffer.size() % 4 == 0); QT3DSU32 numStrs = Size(); size_t bufferBegin = ioBuffer.size(); SStringFileHeader theHeader; ioBuffer.write(theHeader); QT3DSU32 startOffset = ioBuffer.size(); eastl::vector> hashToStringIndex; eastl::vector> handleToStringIndex; hashToStringIndex.reserve(numStrs); handleToStringIndex.reserve(numStrs); WriteStringBlockData(m_DataBlock.begin(), m_DataBlock.end(), ioBuffer, hashToStringIndex, handleToStringIndex); WriteStringBlockData(m_AppendedStrings.data(), m_AppendedStrings.data() + m_AppendedStrings.size(), ioBuffer, hashToStringIndex, handleToStringIndex); ioBuffer.align(4); QT3DSU32 fixedOffset = ioBuffer.size() - startOffset; WriteStringBlock(m_DataBlock.begin(), m_DataBlock.end(), ioBuffer); WriteStringBlock(m_AppendedStrings.data(), m_AppendedStrings.data() + m_AppendedStrings.size(), ioBuffer); // sort by hash code so we can do binary lookups later. eastl::sort(hashToStringIndex.begin(), hashToStringIndex.end(), SHashPairComparator()); ioBuffer.write(hashToStringIndex.data(), (QT3DSU32)hashToStringIndex.size()); eastl::sort(handleToStringIndex.begin(), handleToStringIndex.end(), SHandlePairComparator()); ioBuffer.write(handleToStringIndex.data(), (QT3DSU32)handleToStringIndex.size()); SStringFileHeader *fixedOffsetPtr = reinterpret_cast(ioBuffer.begin() + bufferBegin); *fixedOffsetPtr = SStringFileHeader(numStrs, m_NextHandleValue, fixedOffset); } void Load(NVDataRef inMemory) { QT3DS_ASSERT(m_AppendedStrings.empty()); m_Allocator.m_PreAllocatedBlock = inMemory; m_Allocator.m_OwnsMemory = false; SDataReader theReader(inMemory.begin(), inMemory.end()); SStringFileHeader theHeader = theReader.LoadRef(); QT3DSU32 numStrs = theHeader.m_NumStrings; m_NextHandleValue = theHeader.m_NextHandleValue; QT3DSU32 fixedOffset = theHeader.m_FixedBufferOffset; theReader.m_CurrentPtr += fixedOffset; m_DataBlock = NVConstDataRef( reinterpret_cast(theReader.m_CurrentPtr), numStrs); theReader.m_CurrentPtr += sizeof(SStringFileData) * numStrs; m_HashDataBlockOffset = NVConstDataRef>( reinterpret_cast *>(theReader.m_CurrentPtr), numStrs); theReader.m_CurrentPtr += sizeof(eastl::pair) * numStrs; m_HandleDataBlockOffset = NVConstDataRef>( reinterpret_cast *>(theReader.m_CurrentPtr), numStrs); for (QT3DSU32 idx = 0, end = m_DataBlock.size(); idx < end; ++idx) { SStringFileData &theData = const_cast(m_DataBlock[idx]); theData.m_Str = reinterpret_cast(inMemory.mData + theData.m_StartOffset); } } protected: // When we load from a file, we do not put every string into the hash because building // hashtables takes // time. We only put them into the hash on demand; this avoids building a giant hashtable upon // load. // and since in general we do not need to lookup all hash values again it avoids building the // hashtable // in general. SCharAndHandle FindStrByHash(SCharAndHash hashCode) const { const eastl::pair *iter = eastl::lower_bound(m_HashDataBlockOffset.begin(), m_HashDataBlockOffset.end(), eastl::make_pair(hashCode.m_Hash, (QT3DSU32)0), SHashPairComparator()); for (; iter != m_HashDataBlockOffset.end() && iter->first == hashCode.m_Hash; ++iter) { if (AreEqual(m_DataBlock[iter->second].m_Str, hashCode.m_Data)) return SCharAndHandle(m_DataBlock[iter->second].m_Str, m_DataBlock[iter->second].m_Handle); } return SCharAndHandle(); } const char8_t *FindStrByHandleData(QT3DSU32 handle) const { const eastl::pair *iter = eastl::lower_bound(m_HandleDataBlockOffset.begin(), m_HandleDataBlockOffset.end(), eastl::make_pair(handle, (QT3DSU32)0), SHandlePairComparator()); if (iter != m_HandleDataBlockOffset.end() && iter->first == handle && m_DataBlock[iter->second].m_Handle == handle) return m_DataBlock[iter->second].m_Str; return NULL; } void WriteStringBlockData(const SStringFileData *begin, const SStringFileData *end, SWriteBuffer &ioBuffer, eastl::vector> &ioHashToStringIndex, eastl::vector> &ioHandleToStringIndex) const { for (const SStringFileData *iter = begin; iter != end; ++iter) { size_t idx = ioHashToStringIndex.size(); SCharAndHash theHash(iter->m_Str); ioHashToStringIndex.push_back(eastl::make_pair(theHash.m_Hash, (QT3DSU32)idx)); ioHandleToStringIndex.push_back(eastl::make_pair(iter->m_Handle, (QT3DSU32)idx)); ioBuffer.write(iter->m_Str, iter->m_StrLen); } } void WriteStringBlock(const SStringFileData *begin, const SStringFileData *end, SWriteBuffer &ioBuffer) const { for (const SStringFileData *iter = begin; iter != end; ++iter) ioBuffer.write(*iter); } SCharAndHandle Append(SCharAndHash inStrHash) { if (isTrivial(inStrHash.m_Data)) return SCharAndHandle(); const char8_t *inStr(inStrHash.m_Data); QT3DSU32 len = (QT3DSU32)strlen(inStr) + 1; char8_t *newStr = (char8_t *)m_Allocator.allocate(len, "StringData", __FILE__, __LINE__); memCopy(newStr, inStr, len); // We write the number of strings to the file just after the header. QT3DSU32 theOffset = sizeof(SStringFileHeader); if (Size()) theOffset = Back().EndOffset(); QT3DSU32 handleValue = m_NextHandleValue; ++m_NextHandleValue; m_AppendedStrings.push_back(SStringFileData(len, theOffset, handleValue, newStr)); // Only add to the str map if necessary because building it could be expensive. if (m_StrRemapMap.empty() == false) m_StrRemapMap.insert(eastl::make_pair(newStr, theOffset)); return SCharAndHandle(newStr, handleValue); } // precondition is that size != 0 const SStringFileData &Back() const { QT3DS_ASSERT(Size()); if (m_AppendedStrings.size()) { return m_AppendedStrings.back(); } else { return m_DataBlock[m_DataBlock.size() - 1]; } } QT3DSU32 Size() const { return static_cast(m_AppendedStrings.size() + m_DataBlock.size()); } }; struct SStringTableMutexScope { Mutex *m_Mutex; SStringTableMutexScope(Mutex *inM) : m_Mutex(inM) { if (m_Mutex) m_Mutex->lock(); } ~SStringTableMutexScope() { if (m_Mutex) m_Mutex->unlock(); } }; #define STRING_TABLE_MULTITHREADED_METHOD SStringTableMutexScope __locker(m_MultithreadMutex) class StringTable : public IStringTable { typedef nvhash_map TMapType; typedef nvhash_map TNarrowToWideMapType; SStringFileDataList m_FileData; NVAllocatorCallback &m_Allocator; TNarrowToWideMapType m_StrWideMap; volatile QT3DSI32 mRefCount; // fnd's naming convention eastl::basic_string m_ConvertBuffer; TWideStr m_WideConvertBuffer; Mutex m_MultithreadMutexBacker; Mutex *m_MultithreadMutex; // Data that will be written out to the file. public: StringTable(NVAllocatorCallback &alloc) : m_FileData(alloc) , m_Allocator(m_FileData.m_Allocator) , m_StrWideMap(alloc, "StringTable::m_StrWideMap") , mRefCount(0) , m_ConvertBuffer(ForwardingAllocator(alloc, "StringTable::m_ConvertBuffer")) , m_WideConvertBuffer(ForwardingAllocator(alloc, "StringTable::m_WideConvertBuffer")) , m_MultithreadMutexBacker(alloc) , m_MultithreadMutex(NULL) { } virtual ~StringTable() {} QT3DS_IMPLEMENT_REF_COUNT_ADDREF_RELEASE_OVERRIDE(m_FileData.m_Allocator.m_Allocator) void EnableMultithreadedAccess() override { m_MultithreadMutex = &m_MultithreadMutexBacker; } void DisableMultithreadedAccess() override { m_MultithreadMutex = NULL; } CRegisteredString RegisterStr(Qt3DSBCharPtr str) override { STRING_TABLE_MULTITHREADED_METHOD; if (isTrivial(str)) return CRegisteredString(); return m_FileData.FindStr(str); } // utf-16->utf-8 CRegisteredString RegisterStr(const char16_t *str) override { STRING_TABLE_MULTITHREADED_METHOD; qt3ds::foundation::ConvertUTF(str, 0, m_ConvertBuffer); return RegisterStr(m_ConvertBuffer.c_str()); } // utf-32->utf-8 CRegisteredString RegisterStr(const char32_t *str) override { STRING_TABLE_MULTITHREADED_METHOD; qt3ds::foundation::ConvertUTF(str, 0, m_ConvertBuffer); return RegisterStr(m_ConvertBuffer.c_str()); } CRegisteredString RegisterStr(const wchar_t *str) override { STRING_TABLE_MULTITHREADED_METHOD; if (isTrivialWide(str)) return CRegisteredString(); qt3ds::foundation::ConvertUTF( reinterpret_cast(str), 0, m_ConvertBuffer); return RegisterStr(m_ConvertBuffer.c_str()); } CStringHandle GetHandle(Qt3DSBCharPtr str) override { STRING_TABLE_MULTITHREADED_METHOD; return m_FileData.FindStrHandle(str); } CRegisteredString HandleToStr(QT3DSU32 strHandle) override { STRING_TABLE_MULTITHREADED_METHOD; return m_FileData.FindStrByHandle(strHandle); } const wchar_t *GetWideStr(CRegisteredString theStr) { STRING_TABLE_MULTITHREADED_METHOD; eastl::pair pair_iter = m_StrWideMap.insert( eastl::make_pair(theStr, TWideStr(ForwardingAllocator(m_Allocator, "WideString")))); if (pair_iter.second) qt3ds::foundation::ConvertUTF(theStr.c_str(), 0, pair_iter.first->second); return reinterpret_cast(pair_iter.first->second.c_str()); } const wchar_t *GetWideStr(Qt3DSBCharPtr src) override { STRING_TABLE_MULTITHREADED_METHOD; if (isTrivial(src)) return L""; return GetWideStr(RegisterStr(src)); } const wchar_t *GetWideStr(const wchar_t *str) override { STRING_TABLE_MULTITHREADED_METHOD; if (isTrivialWide(str)) return L""; qt3ds::foundation::ConvertUTF( reinterpret_cast(str), 0, m_ConvertBuffer); return GetWideStr(RegisterStr(m_ConvertBuffer.c_str())); } const SStrRemapMap &GetRemapMap() override { return m_FileData.GetRemapMap(); } NVAllocatorCallback &GetAllocator() override { return m_FileData.m_Allocator.m_Allocator; } // Save to a block of memory. It is up the callers to deallocate using allocator // from GetAllocator(). Returns a remap map that takes existing strings and changes their // address such that they are offsets into the block of memory where this object // started saving. Returns a map that converts registered strings into offsets of the // written buffer. void Save(SWriteBuffer &ioBuffer) const override { STRING_TABLE_MULTITHREADED_METHOD; m_FileData.Save(ioBuffer); } // Load all the strings from inMemory. inMemory is not freed by this object // Finally, you will need to take inMemory and call remap on all strings // from offsets back into their correct address into inMemory. void Load(qt3ds::foundation::NVDataRef inMemory) override { STRING_TABLE_MULTITHREADED_METHOD; m_FileData.Load(inMemory); } }; } const char16_t *char16EmptyStr = g_char16EmptyStr; const char32_t *char32EmptyStr = g_char32EmptyStr; IStringTable &IStringTable::CreateStringTable(NVAllocatorCallback &alloc) { return *QT3DS_NEW(alloc, StringTable)(alloc); }