//-------------------------------------------------------------------------------------- // File: SpriteFont.cpp // // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. // // http://go.microsoft.com/fwlink/?LinkId=248929 //-------------------------------------------------------------------------------------- #include "pch.h" #include #include #include "SpriteFont.h" #include "DirectXHelpers.h" #include "BinaryReader.h" #include "LoaderHelpers.h" using namespace DirectX; using Microsoft::WRL::ComPtr; // Internal SpriteFont implementation class. class SpriteFont::Impl { public: Impl(_In_ ID3D11Device* device, _In_ BinaryReader* reader, bool forceSRGB) noexcept(false); Impl(_In_ ID3D11ShaderResourceView* texture, _In_reads_(glyphCount) Glyph const* glyphs, size_t glyphCount, float lineSpacing) noexcept(false); Glyph const* FindGlyph(wchar_t character) const; void SetDefaultCharacter(wchar_t character); template void ForEachGlyph(_In_z_ wchar_t const* text, TAction action, bool ignoreWhitespace) const; void CreateTextureResource(_In_ ID3D11Device* device, uint32_t width, uint32_t height, DXGI_FORMAT format, uint32_t stride, uint32_t rows, _In_reads_(stride * rows) const uint8_t* data) noexcept(false); const wchar_t* ConvertUTF8(_In_z_ const char *text) noexcept(false); // Fields. ComPtr texture; std::vector glyphs; std::vector glyphsIndex; Glyph const* defaultGlyph; float lineSpacing; private: size_t utfBufferSize; std::unique_ptr utfBuffer; }; // Constants. const XMFLOAT2 SpriteFont::Float2Zero(0, 0); static const char spriteFontMagic[] = "DXTKfont"; // Comparison operators make our sorted glyph vector work with std::binary_search and lower_bound. namespace DirectX { static inline bool operator< (SpriteFont::Glyph const& left, SpriteFont::Glyph const& right) noexcept { return left.Character < right.Character; } static inline bool operator< (wchar_t left, SpriteFont::Glyph const& right) noexcept { return left < right.Character; } static inline bool operator< (SpriteFont::Glyph const& left, wchar_t right) noexcept { return left.Character < right; } } // Reads a SpriteFont from the binary format created by the MakeSpriteFont utility. _Use_decl_annotations_ SpriteFont::Impl::Impl( ID3D11Device* device, BinaryReader* reader, bool forceSRGB) noexcept(false) : defaultGlyph(nullptr), lineSpacing(0), utfBufferSize(0) { // Validate the header. for (char const* magic = spriteFontMagic; *magic; magic++) { if (reader->Read() != *magic) { DebugTrace("ERROR: SpriteFont provided with an invalid .spritefont file\n"); throw std::exception("Not a MakeSpriteFont output binary"); } } // Read the glyph data. auto glyphCount = reader->Read(); auto glyphData = reader->ReadArray(glyphCount); glyphs.assign(glyphData, glyphData + glyphCount); glyphsIndex.reserve(glyphs.size()); for (auto& glyph : glyphs) { glyphsIndex.emplace_back(glyph.Character); } // Read font properties. lineSpacing = reader->Read(); SetDefaultCharacter(static_cast(reader->Read())); // Read the texture data. auto textureWidth = reader->Read(); auto textureHeight = reader->Read(); auto textureFormat = reader->Read(); auto textureStride = reader->Read(); auto textureRows = reader->Read(); uint64_t dataSize = uint64_t(textureStride) * uint64_t(textureRows); if (dataSize > UINT32_MAX) { DebugTrace("ERROR: SpriteFont provided with an invalid .spritefont file\n"); throw std::overflow_error("Invalid .spritefont file"); } auto textureData = reader->ReadArray(static_cast(dataSize)); if (forceSRGB) { textureFormat = LoaderHelpers::MakeSRGB(textureFormat); } // Create the D3D texture. CreateTextureResource( device, textureWidth, textureHeight, textureFormat, textureStride, textureRows, textureData); } // Constructs a SpriteFont from arbitrary user specified glyph data. _Use_decl_annotations_ SpriteFont::Impl::Impl( ID3D11ShaderResourceView* itexture, Glyph const* iglyphs, size_t glyphCount, float ilineSpacing) noexcept(false) : texture(itexture), glyphs(iglyphs, iglyphs + glyphCount), defaultGlyph(nullptr), lineSpacing(ilineSpacing), utfBufferSize(0) { if (!std::is_sorted(iglyphs, iglyphs + glyphCount)) { throw std::exception("Glyphs must be in ascending codepoint order"); } glyphsIndex.reserve(glyphs.size()); for (auto& glyph : glyphs) { glyphsIndex.emplace_back(glyph.Character); } } // Looks up the requested glyph, falling back to the default character if it is not in the font. SpriteFont::Glyph const* SpriteFont::Impl::FindGlyph(wchar_t character) const { // Rather than use std::lower_bound (which includes a slow debug path when built for _DEBUG), // we implement a binary search inline to ensure sufficient Debug build performance to be useful // for text-heavy applications. size_t lower = 0; size_t higher = glyphs.size() - 1; size_t index = higher / 2; const size_t size = glyphs.size(); while (index < size) { const auto curChar = glyphsIndex[index]; if (curChar == character) { return &glyphs[index]; } if (curChar < character) { lower = index + 1; } else { higher = index - 1; } if (higher < lower) { break; } else if (higher - lower <= 4) { for (index = lower; index <= higher; index++) { if (glyphsIndex[index] == character) { return &glyphs[index]; } } } index = lower + ((higher - lower) / 2); } if (defaultGlyph) { return defaultGlyph; } DebugTrace("ERROR: SpriteFont encountered a character not in the font (%u, %C), and no default glyph was provided\n", character, character); throw std::exception("Character not in font"); } // Sets the missing-character fallback glyph. void SpriteFont::Impl::SetDefaultCharacter(wchar_t character) { defaultGlyph = nullptr; if (character) { defaultGlyph = FindGlyph(character); } } // The core glyph layout algorithm, shared between DrawString and MeasureString. template void SpriteFont::Impl::ForEachGlyph(_In_z_ wchar_t const* text, TAction action, bool ignoreWhitespace) const { float x = 0; float y = 0; for (; *text; text++) { wchar_t character = *text; switch (character) { case '\r': // Skip carriage returns. continue; case '\n': // New line. x = 0; y += lineSpacing; break; default: // Output this character. auto glyph = FindGlyph(character); x += glyph->XOffset; if (x < 0) x = 0; float advance = float(glyph->Subrect.right) - float(glyph->Subrect.left) + glyph->XAdvance; if (!ignoreWhitespace || !iswspace(character) || ((glyph->Subrect.right - glyph->Subrect.left) > 1) || ((glyph->Subrect.bottom - glyph->Subrect.top) > 1)) { action(glyph, x, y, advance); } x += advance; break; } } } _Use_decl_annotations_ void SpriteFont::Impl::CreateTextureResource( ID3D11Device* device, uint32_t width, uint32_t height, DXGI_FORMAT format, uint32_t stride, uint32_t rows, const uint8_t* data) noexcept(false) { uint64_t sliceBytes = uint64_t(stride) * uint64_t(rows); if (sliceBytes > UINT32_MAX) { DebugTrace("ERROR: SpriteFont provided with an invalid .spritefont file\n"); throw std::overflow_error("Invalid .spritefont file"); } D3D11_TEXTURE2D_DESC desc = {}; desc.Width = width; desc.Height = height; desc.MipLevels = 1; desc.ArraySize = 1; desc.Format = format; desc.SampleDesc.Count = 1; desc.Usage = D3D11_USAGE_IMMUTABLE; desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; D3D11_SUBRESOURCE_DATA initData = { data, stride, static_cast(sliceBytes) }; ComPtr texture2D; ThrowIfFailed( device->CreateTexture2D(&desc, &initData, &texture2D) ); CD3D11_SHADER_RESOURCE_VIEW_DESC viewDesc(D3D11_SRV_DIMENSION_TEXTURE2D, format); ThrowIfFailed( device->CreateShaderResourceView(texture2D.Get(), &viewDesc, texture.ReleaseAndGetAddressOf()) ); SetDebugObjectName(texture.Get(), "DirectXTK:SpriteFont"); SetDebugObjectName(texture2D.Get(), "DirectXTK:SpriteFont"); } const wchar_t* SpriteFont::Impl::ConvertUTF8(_In_z_ const char *text) noexcept(false) { if (!utfBuffer) { utfBufferSize = 1024; utfBuffer.reset(new wchar_t[1024]); } int result = MultiByteToWideChar(CP_UTF8, 0, text, -1, utfBuffer.get(), static_cast(utfBufferSize)); if (!result && (GetLastError() == ERROR_INSUFFICIENT_BUFFER)) { // Compute required buffer size result = MultiByteToWideChar(CP_UTF8, 0, text, -1, nullptr, 0); utfBufferSize = AlignUp(static_cast(result), 1024u); utfBuffer.reset(new wchar_t[utfBufferSize]); // Retry conversion result = MultiByteToWideChar(CP_UTF8, 0, text, -1, utfBuffer.get(), static_cast(utfBufferSize)); } if (!result) { DebugTrace("ERROR: MultiByteToWideChar failed with error %u.\n", GetLastError()); throw std::exception("MultiByteToWideChar"); } return utfBuffer.get(); } // Construct from a binary file created by the MakeSpriteFont utility. _Use_decl_annotations_ SpriteFont::SpriteFont(ID3D11Device* device, wchar_t const* fileName, bool forceSRGB) { BinaryReader reader(fileName); pImpl = std::make_unique(device, &reader, forceSRGB); } // Construct from a binary blob created by the MakeSpriteFont utility and already loaded into memory. _Use_decl_annotations_ SpriteFont::SpriteFont(ID3D11Device* device, uint8_t const* dataBlob, size_t dataSize, bool forceSRGB) { BinaryReader reader(dataBlob, dataSize); pImpl = std::make_unique(device, &reader, forceSRGB); } // Construct from arbitrary user specified glyph data (for those not using the MakeSpriteFont utility). _Use_decl_annotations_ SpriteFont::SpriteFont(ID3D11ShaderResourceView* texture, Glyph const* glyphs, size_t glyphCount, float lineSpacing) : pImpl(std::make_unique(texture, glyphs, glyphCount, lineSpacing)) { } // Move constructor. SpriteFont::SpriteFont(SpriteFont&& moveFrom) noexcept : pImpl(std::move(moveFrom.pImpl)) { } // Move assignment. SpriteFont& SpriteFont::operator= (SpriteFont&& moveFrom) noexcept { pImpl = std::move(moveFrom.pImpl); return *this; } // Public destructor. SpriteFont::~SpriteFont() { } // Wide-character / UTF-16LE void XM_CALLCONV SpriteFont::DrawString(_In_ SpriteBatch* spriteBatch, _In_z_ wchar_t const* text, XMFLOAT2 const& position, FXMVECTOR color, float rotation, XMFLOAT2 const& origin, float scale, SpriteEffects effects, float layerDepth) const { DrawString(spriteBatch, text, XMLoadFloat2(&position), color, rotation, XMLoadFloat2(&origin), XMVectorReplicate(scale), effects, layerDepth); } void XM_CALLCONV SpriteFont::DrawString(_In_ SpriteBatch* spriteBatch, _In_z_ wchar_t const* text, XMFLOAT2 const& position, FXMVECTOR color, float rotation, XMFLOAT2 const& origin, XMFLOAT2 const& scale, SpriteEffects effects, float layerDepth) const { DrawString(spriteBatch, text, XMLoadFloat2(&position), color, rotation, XMLoadFloat2(&origin), XMLoadFloat2(&scale), effects, layerDepth); } void XM_CALLCONV SpriteFont::DrawString(_In_ SpriteBatch* spriteBatch, _In_z_ wchar_t const* text, FXMVECTOR position, FXMVECTOR color, float rotation, FXMVECTOR origin, float scale, SpriteEffects effects, float layerDepth) const { DrawString(spriteBatch, text, position, color, rotation, origin, XMVectorReplicate(scale), effects, layerDepth); } void XM_CALLCONV SpriteFont::DrawString(_In_ SpriteBatch* spriteBatch, _In_z_ wchar_t const* text, FXMVECTOR position, FXMVECTOR color, float rotation, FXMVECTOR origin, GXMVECTOR scale, SpriteEffects effects, float layerDepth) const { static_assert(SpriteEffects_FlipHorizontally == 1 && SpriteEffects_FlipVertically == 2, "If you change these enum values, the following tables must be updated to match"); // Lookup table indicates which way to move along each axis per SpriteEffects enum value. static XMVECTORF32 axisDirectionTable[4] = { { { { -1, -1, 0, 0 } } }, { { { 1, -1, 0, 0 } } }, { { { -1, 1, 0, 0 } } }, { { { 1, 1, 0, 0 } } }, }; // Lookup table indicates which axes are mirrored for each SpriteEffects enum value. static XMVECTORF32 axisIsMirroredTable[4] = { { { { 0, 0, 0, 0 } } }, { { { 1, 0, 0, 0 } } }, { { { 0, 1, 0, 0 } } }, { { { 1, 1, 0, 0 } } }, }; XMVECTOR baseOffset = origin; // If the text is mirrored, offset the start position accordingly. if (effects) { baseOffset = XMVectorNegativeMultiplySubtract( MeasureString(text), axisIsMirroredTable[effects & 3], baseOffset); } // Draw each character in turn. pImpl->ForEachGlyph(text, [&](Glyph const* glyph, float x, float y, float advance) { UNREFERENCED_PARAMETER(advance); XMVECTOR offset = XMVectorMultiplyAdd(XMVectorSet(x, y + glyph->YOffset, 0, 0), axisDirectionTable[effects & 3], baseOffset); if (effects) { // For mirrored characters, specify bottom and/or right instead of top left. XMVECTOR glyphRect = XMConvertVectorIntToFloat(XMLoadInt4(reinterpret_cast(&glyph->Subrect)), 0); // xy = glyph width/height. glyphRect = XMVectorSubtract(XMVectorSwizzle<2, 3, 0, 1>(glyphRect), glyphRect); offset = XMVectorMultiplyAdd(glyphRect, axisIsMirroredTable[effects & 3], offset); } spriteBatch->Draw(pImpl->texture.Get(), position, &glyph->Subrect, color, rotation, offset, scale, effects, layerDepth); }, true); } XMVECTOR XM_CALLCONV SpriteFont::MeasureString(_In_z_ wchar_t const* text, bool ignoreWhitespace) const { XMVECTOR result = XMVectorZero(); pImpl->ForEachGlyph(text, [&](Glyph const* glyph, float x, float y, float advance) { UNREFERENCED_PARAMETER(advance); auto w = static_cast(glyph->Subrect.right - glyph->Subrect.left); auto h = static_cast(glyph->Subrect.bottom - glyph->Subrect.top) + glyph->YOffset; h = iswspace(wchar_t(glyph->Character)) ? pImpl->lineSpacing : std::max(h, pImpl->lineSpacing); result = XMVectorMax(result, XMVectorSet(x + w, y + h, 0, 0)); }, ignoreWhitespace); return result; } RECT SpriteFont::MeasureDrawBounds(_In_z_ wchar_t const* text, XMFLOAT2 const& position, bool ignoreWhitespace) const { RECT result = { LONG_MAX, LONG_MAX, 0, 0 }; pImpl->ForEachGlyph(text, [&](Glyph const* glyph, float x, float y, float advance) noexcept { auto isWhitespace = iswspace(wchar_t(glyph->Character)); auto w = static_cast(glyph->Subrect.right - glyph->Subrect.left); auto h = isWhitespace ? pImpl->lineSpacing : static_cast(glyph->Subrect.bottom - glyph->Subrect.top); float minX = position.x + x; float minY = position.y + y + (isWhitespace ? 0.0f : glyph->YOffset); float maxX = std::max(minX + advance, minX + w); float maxY = minY + h; if (minX < float(result.left)) result.left = long(minX); if (minY < float(result.top)) result.top = long(minY); if (float(result.right) < maxX) result.right = long(maxX); if (float(result.bottom) < maxY) result.bottom = long(maxY); }, ignoreWhitespace); if (result.left == LONG_MAX) { result.left = 0; result.top = 0; } return result; } RECT XM_CALLCONV SpriteFont::MeasureDrawBounds(_In_z_ wchar_t const* text, FXMVECTOR position, bool ignoreWhitespace) const { XMFLOAT2 pos; XMStoreFloat2(&pos, position); return MeasureDrawBounds(text, pos, ignoreWhitespace); } // UTF-8 void XM_CALLCONV SpriteFont::DrawString(_In_ SpriteBatch* spriteBatch, _In_z_ char const* text, XMFLOAT2 const& position, FXMVECTOR color, float rotation, XMFLOAT2 const& origin, float scale, SpriteEffects effects, float layerDepth) const { DrawString(spriteBatch, pImpl->ConvertUTF8(text), XMLoadFloat2(&position), color, rotation, XMLoadFloat2(&origin), XMVectorReplicate(scale), effects, layerDepth); } void XM_CALLCONV SpriteFont::DrawString(_In_ SpriteBatch* spriteBatch, _In_z_ char const* text, XMFLOAT2 const& position, FXMVECTOR color, float rotation, XMFLOAT2 const& origin, XMFLOAT2 const& scale, SpriteEffects effects, float layerDepth) const { DrawString(spriteBatch, pImpl->ConvertUTF8(text), XMLoadFloat2(&position), color, rotation, XMLoadFloat2(&origin), XMLoadFloat2(&scale), effects, layerDepth); } void XM_CALLCONV SpriteFont::DrawString(_In_ SpriteBatch* spriteBatch, _In_z_ char const* text, FXMVECTOR position, FXMVECTOR color, float rotation, FXMVECTOR origin, float scale, SpriteEffects effects, float layerDepth) const { DrawString(spriteBatch, pImpl->ConvertUTF8(text), position, color, rotation, origin, XMVectorReplicate(scale), effects, layerDepth); } void XM_CALLCONV SpriteFont::DrawString(_In_ SpriteBatch* spriteBatch, _In_z_ char const* text, FXMVECTOR position, FXMVECTOR color, float rotation, FXMVECTOR origin, GXMVECTOR scale, SpriteEffects effects, float layerDepth) const { DrawString(spriteBatch, pImpl->ConvertUTF8(text), position, color, rotation, origin, scale, effects, layerDepth); } XMVECTOR XM_CALLCONV SpriteFont::MeasureString(_In_z_ char const* text, bool ignoreWhitespace) const { return MeasureString(pImpl->ConvertUTF8(text), ignoreWhitespace); } RECT SpriteFont::MeasureDrawBounds(_In_z_ char const* text, XMFLOAT2 const& position, bool ignoreWhitespace) const { return MeasureDrawBounds(pImpl->ConvertUTF8(text), position, ignoreWhitespace); } RECT XM_CALLCONV SpriteFont::MeasureDrawBounds(_In_z_ char const* text, FXMVECTOR position, bool ignoreWhitespace) const { XMFLOAT2 pos; XMStoreFloat2(&pos, position); return MeasureDrawBounds(pImpl->ConvertUTF8(text), pos, ignoreWhitespace); } // Spacing properties float SpriteFont::GetLineSpacing() const noexcept { return pImpl->lineSpacing; } void SpriteFont::SetLineSpacing(float spacing) { pImpl->lineSpacing = spacing; } // Font properties wchar_t SpriteFont::GetDefaultCharacter() const noexcept { return static_cast(pImpl->defaultGlyph ? pImpl->defaultGlyph->Character : 0); } void SpriteFont::SetDefaultCharacter(wchar_t character) { pImpl->SetDefaultCharacter(character); } bool SpriteFont::ContainsCharacter(wchar_t character) const { return std::binary_search(pImpl->glyphs.begin(), pImpl->glyphs.end(), character); } // Custom layout/rendering SpriteFont::Glyph const* SpriteFont::FindGlyph(wchar_t character) const { return pImpl->FindGlyph(character); } void SpriteFont::GetSpriteSheet(ID3D11ShaderResourceView** texture) const { if (!texture) return; ThrowIfFailed(pImpl->texture.CopyTo(texture)); }