diff --git a/Core/HLE/sceDisplay.cpp b/Core/HLE/sceDisplay.cpp index 883cc7a587..2871be8140 100644 --- a/Core/HLE/sceDisplay.cpp +++ b/Core/HLE/sceDisplay.cpp @@ -49,6 +49,7 @@ #include "Core/HLE/sceKernel.h" #include "Core/HLE/sceKernelThread.h" #include "Core/HLE/sceKernelInterrupt.h" +#include "Core/Util/PPGeDraw.h" #include "GPU/GPU.h" #include "GPU/GPUState.h" @@ -832,6 +833,7 @@ void __DisplayFlip(int cyclesLate) { void hleAfterFlip(u64 userdata, int cyclesLate) { gpu->BeginFrame(); // doesn't really matter if begin or end of frame. + PPGeNotifyFrame(); // This seems like as good a time as any to check if the config changed. if (lagSyncScheduled != g_Config.bForceLagSync) { diff --git a/Core/Util/PPGeDraw.cpp b/Core/Util/PPGeDraw.cpp index 421959b223..bf64fb1725 100644 --- a/Core/Util/PPGeDraw.cpp +++ b/Core/Util/PPGeDraw.cpp @@ -20,6 +20,7 @@ #include "base/stringutil.h" #include "file/vfs.h" #include "gfx/texture_atlas.h" +#include "gfx_es2/draw_text.h" #include "image/zim_load.h" #include "image/png_load.h" #include "util/text/utf8.h" @@ -103,6 +104,41 @@ static AtlasCharLine char_one_line; static AtlasLineArray char_lines; static AtlasTextMetrics char_lines_metrics; +static bool textDrawerInited = false; +static TextDrawer *textDrawer = nullptr; +struct PPGeTextDrawerCacheKey { + bool operator < (const PPGeTextDrawerCacheKey &other) const { + if (align != other.align) + return align < other.align; + if (wrapWidth != other.wrapWidth) + return wrapWidth < other.wrapWidth; + return text < other.text; + } + std::string text; + int align; + float wrapWidth; +}; +struct PPGeTextDrawerImage { + TextStringEntry entry; + u32 ptr; +}; +std::map textDrawerImages; + +// Overwrite the current text lines buffer so it can be drawn later. +void PPGePrepareText(const char *text, float x, float y, int align, float scale, float lineHeightScale, + int WrapType = PPGE_LINE_NONE, int wrapWidth = 0); + +// Get the metrics of the bounding box of the currently stated text. +void PPGeMeasureCurrentText(float *x, float *y, float *w, float *h, int *n); + +// These functions must be called between PPGeBegin and PPGeEnd. + +// Draw currently buffered text using the state from PPGeGetTextBoundingBox() call. +// Clears the buffer and state when done. +void PPGeDrawCurrentText(u32 color = 0xFFFFFFFF); + +void PPGeSetTexture(u32 dataAddr, int width, int height); + //only 0xFFFFFF of data is used static void WriteCmd(u8 cmd, u32 data) { Memory::Write_U32((cmd << 24) | (data & 0xFFFFFF), dlWritePtr); @@ -129,18 +165,18 @@ static void BeginVertexData() { vertexStart = dataWritePtr; } -static void Vertex(float x, float y, float u, float v, int tw, int th, u32 color = 0xFFFFFFFF) { +static void Vertex(float x, float y, float u, float v, int tw, int th, u32 color = 0xFFFFFFFF, float off = -0.5f) { if (g_RemasterMode) { PPGeRemasterVertex vtx; - vtx.x = x - 0.5f; vtx.y = y - 0.5f; vtx.z = 0; - vtx.u = u * tw - 0.5f; vtx.v = v * th - 0.5f; + vtx.x = x + off; vtx.y = y + off; vtx.z = 0; + vtx.u = u * tw + off; vtx.v = v * th + off; vtx.color = color; Memory::WriteStruct(dataWritePtr, &vtx); dataWritePtr += sizeof(vtx); } else { PPGeVertex vtx; - vtx.x = x - 0.5f; vtx.y = y - 0.5f; vtx.z = 0; - vtx.u = u * tw - 0.5f; vtx.v = v * th - 0.5f; + vtx.x = x + off; vtx.y = y + off; vtx.z = 0; + vtx.u = u * tw + off; vtx.v = v * th + off; vtx.color = color; Memory::WriteStruct(dataWritePtr, &vtx); dataWritePtr += sizeof(vtx); @@ -232,13 +268,18 @@ void __PPGeInit() free(imageData[0]); + // We can't create it here, because Android needs it on the right thread. + textDrawerInited = false; + textDrawer = nullptr; + textDrawerImages.clear(); + DEBUG_LOG(SCEGE, "PPGe drawing library initialized. DL: %08x Data: %08x Atlas: %08x (%i) Args: %08x", dlPtr, dataPtr, atlasPtr, atlasSize, listArgs.ptr); } void __PPGeDoState(PointerWrap &p) { - auto s = p.Section("PPGeDraw", 1, 2); + auto s = p.Section("PPGeDraw", 1, 3); if (!s) return; @@ -256,6 +297,30 @@ void __PPGeDoState(PointerWrap &p) p.Do(listArgs); } + if (s >= 3) { + uint32_t sz = (uint32_t)textDrawerImages.size(); + p.Do(sz); + + switch (p.mode) { + case PointerWrap::MODE_READ: + textDrawerImages.clear(); + for (uint32_t i = 0; i < sz; ++i) { + // We only care about the pointers, so we can free them. We'll decimate right away. + PPGeTextDrawerCacheKey key{ StringFromFormat("__savestate__%d", i), -1, -1 }; + textDrawerImages[key] = PPGeTextDrawerImage{}; + p.Do(textDrawerImages[key].ptr); + } + break; + default: + for (const auto &im : textDrawerImages) { + p.Do(im.second.ptr); + } + break; + } + } else { + textDrawerImages.clear(); + } + p.Do(dlPtr); p.Do(dlWritePtr); p.Do(dlSize); @@ -291,6 +356,9 @@ void __PPGeShutdown() dlPtr = 0; savedContextPtr = 0; listArgs = 0; + + delete textDrawer; + textDrawer = nullptr; } void PPGeBegin() @@ -625,9 +693,51 @@ static AtlasTextMetrics BreakLines(const char *text, const AtlasFont &atlasfont, return metrics; } +static bool HasTextDrawer() { + // We create this on first use so it's on the correct thread. + if (textDrawerInited) { + return textDrawer != nullptr; + } + + // TODO: Should we pass a draw_? + textDrawer = TextDrawer::Create(nullptr); + if (textDrawer) { + textDrawer->SetFontScale(1.0f, 1.0f); + textDrawer->SetForcedDPIScale(1.0f); + textDrawer->SetFont(g_Config.sFont.c_str(), 20, 0); + } + textDrawerInited = true; + return textDrawer != nullptr; +} + void PPGeMeasureText(float *w, float *h, int *n, const char *text, float scale, int WrapType, int wrapWidth) { + if (HasTextDrawer()) { + float mw, mh; + textDrawer->SetFontScale(scale, scale); + int dtalign = (WrapType & PPGE_LINE_WRAP_WORD) ? FLAG_WRAP_TEXT : 0; + if (WrapType & PPGE_LINE_USE_ELLIPSIS) + dtalign |= FLAG_ELLIPSIZE_TEXT; + Bounds b(0, 0, wrapWidth <= 0 ? 480.0f : wrapWidth, 272.0f); + textDrawer->MeasureStringRect(text, strlen(text), b, &mw, &mh, dtalign); + + if (w) + *w = mw; + if (h) + *h = mh; + if (n) { + // Cheap way to get the n. + float oneLine, twoLines; + textDrawer->MeasureString("|", 1, &mw, &oneLine); + textDrawer->MeasureStringRect("|\n|", 3, Bounds(0, 0, 480, 272), &mw, &twoLines); + + float lineHeight = twoLines - oneLine; + *n = (int)((mh + (lineHeight - 1)) / lineHeight); + } + return; + } + const AtlasFont &atlasfont = g_ppge_atlas.fonts[0]; AtlasTextMetrics metrics = BreakLines(text, atlasfont, 0, 0, 0, scale, scale, WrapType, wrapWidth, true); if (w) *w = metrics.maxWidth; @@ -683,8 +793,110 @@ void PPGeDrawCurrentText(u32 color) PPGeResetCurrentText(); } -void PPGeDrawText(const char *text, float x, float y, int align, float scale, u32 color) -{ +// Return a value such that (1 << value) >= x +int GetPow2(int x) { +#ifdef __GNUC__ + int ret = 31 - __builtin_clz(x | 1); + if ((1 << ret) < x) +#else + int ret = 0; + while ((1 << ret) < x) +#endif + ret++; + return ret; +} + +static PPGeTextDrawerImage PPGeGetTextImage(const char *text, int align, float scale, float maxWidth, bool wrap) { + int tdalign = (align & PPGE_ALIGN_HCENTER) ? ALIGN_HCENTER : 0; + tdalign |= FLAG_ELLIPSIZE_TEXT; + if (wrap) { + tdalign |= FLAG_WRAP_TEXT; + } + + PPGeTextDrawerCacheKey key{ text, tdalign, maxWidth / scale }; + PPGeTextDrawerImage im; + + auto cacheItem = textDrawerImages.find(key); + if (cacheItem != textDrawerImages.end()) { + im = cacheItem->second; + cacheItem->second.entry.lastUsedFrame = gpuStats.numFlips; + } else { + std::vector bitmapData; + textDrawer->SetFontScale(scale, scale); + Bounds b(0, 0, maxWidth, 272.0f); + std::string cleaned = ReplaceAll(text, "\r", ""); + textDrawer->DrawStringBitmapRect(bitmapData, im.entry, Draw::DataFormat::R8_UNORM, cleaned.c_str(), b, tdalign); + + int bufwBytes = ((im.entry.bmWidth + 31) / 32) * 16; + u32 sz = bufwBytes * (im.entry.bmHeight + 1); + u32 origSz = sz; + im.ptr = __PPGeDoAlloc(sz, true, "PPGeText"); + + if (bitmapData.size() & 1) + bitmapData.resize(bitmapData.size() + 1); + + if (im.ptr) { + int wBytes = (im.entry.bmWidth + 1) / 2; + u8 *ramPtr = (u8 *)Memory::GetPointer(im.ptr); + for (int y = 0; y < im.entry.bmHeight; ++y) { + for (int x = 0; x < wBytes; ++x) { + uint8_t c1 = bitmapData[y * im.entry.bmWidth + x * 2]; + uint8_t c2 = bitmapData[y * im.entry.bmWidth + x * 2 + 1]; + // Convert this to 4-bit palette values. + ramPtr[y * bufwBytes + x] = (c2 & 0xF0) | (c1 >> 4); + } + if (bufwBytes != wBytes) { + memset(ramPtr + y * bufwBytes + wBytes, 0, bufwBytes - wBytes); + } + } + memset(ramPtr + im.entry.bmHeight * bufwBytes, 0, bufwBytes + sz - origSz); + } + + im.entry.lastUsedFrame = gpuStats.numFlips; + textDrawerImages[key] = im; + } + + return im; +} + +static void PPGeDrawTextImage(PPGeTextDrawerImage im, float x, float y, int align, float scale, u32 color) { + int bufw = ((im.entry.bmWidth + 31) / 32) * 32; + int wp2 = GetPow2(im.entry.bmWidth); + int hp2 = GetPow2(im.entry.bmHeight); + WriteCmd(GE_CMD_TEXADDR0, im.ptr & 0xFFFFF0); + WriteCmd(GE_CMD_TEXBUFWIDTH0, bufw | ((im.ptr & 0xFF000000) >> 8)); + WriteCmd(GE_CMD_TEXSIZE0, wp2 | (hp2 << 8)); + WriteCmd(GE_CMD_TEXFLUSH, 0); + + float w = im.entry.width * scale; + float h = im.entry.height * scale; + + if (align & PPGE_ALIGN_HCENTER) + x -= w / 2.0f; + else if (align & PPGE_ALIGN_RIGHT) + x -= w; + if (align & PPGE_ALIGN_VCENTER) + y -= h / 2.0f; + else if (align & PPGE_ALIGN_BOTTOM) + y -= h; + + BeginVertexData(); + float u1 = (float)im.entry.width / (1 << wp2); + float v1 = (float)im.entry.height / (1 << hp2); + Vertex(x, y, 0, 0, 1 << wp2, 1 << hp2, color, 0.0f); + Vertex(x + w, y + h, u1, v1, 1 << wp2, 1 << hp2, color, 0.0f); + EndVertexDataAndDraw(GE_PRIM_RECTANGLES); + + PPGeSetDefaultTexture(); +} + +void PPGeDrawText(const char *text, float x, float y, int align, float scale, u32 color) { + if (HasTextDrawer()) { + PPGeTextDrawerImage im = PPGeGetTextImage(text, align, scale, 480.0f - x, false); + PPGeDrawTextImage(im, x, y, align, scale, color); + return; + } + PPGePrepareText(text, x, y, align, scale, scale, PPGE_LINE_USE_ELLIPSIS); PPGeDrawCurrentText(color); } @@ -718,10 +930,40 @@ void PPGeDrawTextWrapped(const char *text, float x, float y, float wrapWidth, fl s = StripTrailingWhite(s); } - PPGePrepareText(s.c_str(), x, y, align, scale, scale, PPGE_LINE_USE_ELLIPSIS | PPGE_LINE_WRAP_WORD, wrapWidth); - int zoom = (PSP_CoreParameter().pixelHeight + 479) / 480; float maxScaleDown = zoom == 1 ? 1.3f : 2.0f; + + if (HasTextDrawer()) { + float actualWidth, actualHeight; + Bounds b(0, 0, wrapWidth <= 0 ? 480.0f - x : wrapWidth, wrapHeight); + int tdalign = (align & PPGE_ALIGN_HCENTER) ? ALIGN_HCENTER : 0; + textDrawer->SetFontScale(scale, scale); + textDrawer->MeasureStringRect(s.c_str(), s.size(), b, &actualWidth, &actualHeight, tdalign | FLAG_WRAP_TEXT); + if (wrapHeight != 0.0f && actualHeight > wrapHeight) { + // Cheap way to get the line height. + float oneLine, twoLines; + textDrawer->MeasureString("|", 1, &actualWidth, &oneLine); + textDrawer->MeasureStringRect("|\n|", 3, Bounds(0, 0, 480, 272), &actualWidth, &twoLines); + + float lineHeight = twoLines - oneLine; + if (actualHeight > wrapHeight * maxScaleDown) { + float maxLines = floor(wrapHeight * maxScaleDown / lineHeight); + actualHeight = (maxLines + 1) * lineHeight; + // Add an ellipsis if it's just too long to be readable. + // On a PSP, it does this without scaling it down. + s = StripTrailingWhite(CropLinesToCount(s, (int)maxLines)) + "\n..."; + } + + scale *= wrapHeight / actualHeight; + } + + PPGeTextDrawerImage im = PPGeGetTextImage(s.c_str(), align, scale, wrapWidth <= 0 ? 480.0f - x : wrapWidth, true); + PPGeDrawTextImage(im, x, y, align, scale, color); + return; + } + + PPGePrepareText(s.c_str(), x, y, align, scale, scale, PPGE_LINE_USE_ELLIPSIS | PPGE_LINE_WRAP_WORD, wrapWidth); + float actualHeight = char_lines_metrics.lineHeight * char_lines_metrics.numLines; if (wrapHeight != 0.0f && actualHeight > wrapHeight) { if (actualHeight > wrapHeight * maxScaleDown) { @@ -837,20 +1079,6 @@ void PPGeDrawImage(float x, float y, float w, float h, float u1, float v1, float EndVertexDataAndDraw(GE_PRIM_RECTANGLES); } -// Return a value such that (1 << value) >= x -int GetPow2(int x) -{ -#ifdef __GNUC__ - int ret = 31 - __builtin_clz(x|1); - if ((1 << ret) < x) -#else - int ret = 0; - while ((1 << ret) < x) -#endif - ret++; - return ret; -} - void PPGeSetDefaultTexture() { WriteCmd(GE_CMD_TEXTUREMAPENABLE, 1); @@ -1004,3 +1232,20 @@ void PPGeImage::SetTexture() { } } +void PPGeNotifyFrame() { + if (textDrawer) { + textDrawer->OncePerFrame(); + } + + // Do this always, in case the platform has no TextDrawer but save state did. + for (auto it = textDrawerImages.begin(); it != textDrawerImages.end(); ) { + if (it->second.entry.lastUsedFrame - gpuStats.numFlips >= 97) { + kernelMemory.Free(it->second.ptr); + it = textDrawerImages.erase(it); + } else { + ++it; + } + } + + PPGeImage::Decimate(); +} diff --git a/Core/Util/PPGeDraw.h b/Core/Util/PPGeDraw.h index a599c45e37..8962fa7f99 100644 --- a/Core/Util/PPGeDraw.h +++ b/Core/Util/PPGeDraw.h @@ -45,10 +45,6 @@ void __PPGeShutdown(); void PPGeBegin(); void PPGeEnd(); -// If you want to draw using this texture but not go through the PSP GE emulation, -// jsut call this. Will bind the texture to unit 0. -void PPGeBindTexture(); - enum { PPGE_ALIGN_LEFT = 0, PPGE_ALIGN_RIGHT = 16, @@ -65,11 +61,6 @@ enum { PPGE_ALIGN_BOTTOMRIGHT = PPGE_ALIGN_BOTTOM | PPGE_ALIGN_RIGHT, }; -enum { - PPGE_ESCAPE_NONE, - PPGE_ESCAPE_BACKSLASHED, -}; - enum { PPGE_LINE_NONE = 0, PPGE_LINE_USE_ELLIPSIS = 1, // use ellipses in too long words @@ -81,19 +72,6 @@ enum { void PPGeMeasureText(float *w, float *h, int *n, const char *text, float scale, int WrapType = PPGE_LINE_NONE, int wrapWidth = 0); -// Overwrite the current text lines buffer so it can be drawn later. -void PPGePrepareText(const char *text, float x, float y, int align, float scale, float lineHeightScale, - int WrapType = PPGE_LINE_NONE, int wrapWidth = 0); - -// Get the metrics of the bounding box of the currently stated text. -void PPGeMeasureCurrentText(float *x, float *y, float *w, float *h, int *n); - -// These functions must be called between PPGeBegin and PPGeEnd. - -// Draw currently buffered text using the state from PPGeGetTextBoundingBox() call. -// Clears the buffer and state when done. -void PPGeDrawCurrentText(u32 color = 0xFFFFFFFF); - // Draws some text using the one font we have. // Clears the text buffer when done. void PPGeDrawText(const char *text, float x, float y, int align, float scale = 1.0f, u32 color = 0xFFFFFFFF); @@ -107,6 +85,8 @@ void PPGeDrawImage(ImageID atlasImage, float x, float y, int align, u32 color = void PPGeDrawImage(ImageID atlasImage, float x, float y, float w, float h, int align, u32 color = 0xFFFFFFFF); void PPGeDrawImage(float x, float y, float w, float h, float u1, float v1, float u2, float v2, int tw, int th, u32 color); +void PPGeNotifyFrame(); + class PPGeImage { public: PPGeImage(const std::string &pspFilename); @@ -132,8 +112,9 @@ public: return height_; } -private: static void Decimate(); + +private: static std::vector loadedTextures_; std::string filename_; @@ -152,6 +133,5 @@ private: void PPGeDrawRect(float x1, float y1, float x2, float y2, u32 color); void PPGeSetDefaultTexture(); -void PPGeSetTexture(u32 dataAddr, int width, int height); void PPGeDisableTexture(); diff --git a/ext/native/gfx_es2/draw_buffer.cpp b/ext/native/gfx_es2/draw_buffer.cpp index 78eb6733ab..dbe024fe24 100644 --- a/ext/native/gfx_es2/draw_buffer.cpp +++ b/ext/native/gfx_es2/draw_buffer.cpp @@ -365,7 +365,7 @@ void DrawBuffer::DrawImage2GridH(ImageID atlas_image, float x1, float y1, float class AtlasWordWrapper : public WordWrapper { public: // Note: maxW may be height if rotated. - AtlasWordWrapper(const AtlasFont &atlasfont, float scale, const char *str, float maxW) : WordWrapper(str, maxW), atlasfont_(atlasfont), scale_(scale) { + AtlasWordWrapper(const AtlasFont &atlasfont, float scale, const char *str, float maxW, int flags) : WordWrapper(str, maxW, flags), atlasfont_(atlasfont), scale_(scale) { } protected: @@ -442,14 +442,15 @@ void DrawBuffer::MeasureTextRect(FontID font_id, const char *text, int count, co } std::string toMeasure = std::string(text, count); - if (align & FLAG_WRAP_TEXT) { + int wrap = align & (FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT); + if (wrap) { const AtlasFont *font = atlas->getFont(font_id); if (!font) { *w = 0.0f; *h = 0.0f; return; } - AtlasWordWrapper wrapper(*font, fontscalex, toMeasure.c_str(), bounds.w); + AtlasWordWrapper wrapper(*font, fontscalex, toMeasure.c_str(), bounds.w, wrap); toMeasure = wrapper.Wrapped(); } MeasureTextCount(font_id, toMeasure.c_str(), (int)toMeasure.length(), w, h); @@ -491,8 +492,9 @@ void DrawBuffer::DrawTextRect(FontID font, const char *text, float x, float y, f } std::string toDraw = text; - if (align & FLAG_WRAP_TEXT) { - AtlasWordWrapper wrapper(*atlas->getFont(font), fontscalex, toDraw.c_str(), w); + int wrap = align & (FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT); + if (wrap) { + AtlasWordWrapper wrapper(*atlas->getFont(font), fontscalex, toDraw.c_str(), w, wrap); toDraw = wrapper.Wrapped(); } diff --git a/ext/native/gfx_es2/draw_buffer.h b/ext/native/gfx_es2/draw_buffer.h index 211fc4fb95..a702fd6693 100644 --- a/ext/native/gfx_es2/draw_buffer.h +++ b/ext/native/gfx_es2/draw_buffer.h @@ -39,6 +39,7 @@ enum { FLAG_DYNAMIC_ASCII = 2048, FLAG_NO_PREFIX = 4096, // means to not process ampersands FLAG_WRAP_TEXT = 8192, + FLAG_ELLIPSIZE_TEXT = 16384, }; namespace Draw { diff --git a/ext/native/gfx_es2/draw_text.cpp b/ext/native/gfx_es2/draw_text.cpp index 168f84f151..ce9b51b379 100644 --- a/ext/native/gfx_es2/draw_text.cpp +++ b/ext/native/gfx_es2/draw_text.cpp @@ -24,8 +24,8 @@ float TextDrawerWordWrapper::MeasureWidth(const char *str, size_t bytes) { return w; } -void TextDrawer::WrapString(std::string &out, const char *str, float maxW) { - TextDrawerWordWrapper wrapper(this, str, maxW); +void TextDrawer::WrapString(std::string &out, const char *str, float maxW, int flags) { + TextDrawerWordWrapper wrapper(this, str, maxW, flags); out = wrapper.Wrapped(); } @@ -35,6 +35,8 @@ void TextDrawer::SetFontScale(float xscale, float yscale) { } float TextDrawer::CalculateDPIScale() { + if (ignoreGlobalDpi_) + return dpiScale_; float scale = g_dpi_scale_y; if (scale >= 1.0f) { scale = 1.0f; @@ -42,6 +44,41 @@ float TextDrawer::CalculateDPIScale() { return scale; } +void TextDrawer::DrawStringRect(DrawBuffer &target, const char *str, const Bounds &bounds, uint32_t color, int align) { + float x = bounds.x; + float y = bounds.y; + if (align & ALIGN_HCENTER) { + x = bounds.centerX(); + } else if (align & ALIGN_RIGHT) { + x = bounds.x2(); + } + if (align & ALIGN_VCENTER) { + y = bounds.centerY(); + } else if (align & ALIGN_BOTTOM) { + y = bounds.y2(); + } + + std::string toDraw = str; + int wrap = align & (FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT); + if (wrap) { + bool rotated = (align & (ROTATE_90DEG_LEFT | ROTATE_90DEG_RIGHT)) != 0; + WrapString(toDraw, str, rotated ? bounds.h : bounds.w, wrap); + } + + DrawString(target, toDraw.c_str(), x, y, color, align); +} + +void TextDrawer::DrawStringBitmapRect(std::vector &bitmapData, TextStringEntry &entry, Draw::DataFormat texFormat, const char *str, const Bounds &bounds, int align) { + std::string toDraw = str; + int wrap = align & (FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT); + if (wrap) { + bool rotated = (align & (ROTATE_90DEG_LEFT | ROTATE_90DEG_RIGHT)) != 0; + WrapString(toDraw, str, rotated ? bounds.h : bounds.w, wrap); + } + + DrawStringBitmap(bitmapData, entry, texFormat, toDraw.c_str(), align); +} + TextDrawer *TextDrawer::Create(Draw::DrawContext *draw) { TextDrawer *drawer = nullptr; #if defined(_WIN32) && !PPSSPP_PLATFORM(UWP) diff --git a/ext/native/gfx_es2/draw_text.h b/ext/native/gfx_es2/draw_text.h index a36db98ad2..f6bf8e3e34 100644 --- a/ext/native/gfx_es2/draw_text.h +++ b/ext/native/gfx_es2/draw_text.h @@ -58,11 +58,17 @@ public: virtual void MeasureString(const char *str, size_t len, float *w, float *h) = 0; virtual void MeasureStringRect(const char *str, size_t len, const Bounds &bounds, float *w, float *h, int align = ALIGN_TOPLEFT) = 0; virtual void DrawString(DrawBuffer &target, const char *str, float x, float y, uint32_t color, int align = ALIGN_TOPLEFT) = 0; - virtual void DrawStringRect(DrawBuffer &target, const char *str, const Bounds &bounds, uint32_t color, int align) = 0; + void DrawStringRect(DrawBuffer &target, const char *str, const Bounds &bounds, uint32_t color, int align); + virtual void DrawStringBitmap(std::vector &bitmapData, TextStringEntry &entry, Draw::DataFormat texFormat, const char *str, int align = ALIGN_TOPLEFT) = 0; + void DrawStringBitmapRect(std::vector &bitmapData, TextStringEntry &entry, Draw::DataFormat texFormat, const char *str, const Bounds &bounds, int align); // Use for housekeeping like throwing out old strings. virtual void OncePerFrame() = 0; float CalculateDPIScale(); + void SetForcedDPIScale(float dpi) { + dpiScale_ = dpi; + ignoreGlobalDpi_ = true; + } // Factory function that selects implementation. static TextDrawer *Create(Draw::DrawContext *draw); @@ -72,7 +78,7 @@ protected: Draw::DrawContext *draw_; virtual void ClearCache() = 0; - void WrapString(std::string &out, const char *str, float maxWidth); + void WrapString(std::string &out, const char *str, float maxWidth, int flags); struct CacheKey { bool operator < (const CacheKey &other) const { @@ -86,20 +92,21 @@ protected: uint32_t fontHash; }; - int frameCount_; - float fontScaleX_; - float fontScaleY_; - float dpiScale_; + int frameCount_ = 0; + float fontScaleX_ = 1.0f; + float fontScaleY_ = 1.0f; + float dpiScale_ = 1.0f; + bool ignoreGlobalDpi_ = false; }; class TextDrawerWordWrapper : public WordWrapper { public: - TextDrawerWordWrapper(TextDrawer *drawer, const char *str, float maxW) : WordWrapper(str, maxW), drawer_(drawer) { + TextDrawerWordWrapper(TextDrawer *drawer, const char *str, float maxW, int flags) : WordWrapper(str, maxW, flags), drawer_(drawer) { } protected: float MeasureWidth(const char *str, size_t bytes) override; TextDrawer *drawer_; -}; \ No newline at end of file +}; diff --git a/ext/native/gfx_es2/draw_text_android.cpp b/ext/native/gfx_es2/draw_text_android.cpp index b2e15e86e3..f2513b99e3 100644 --- a/ext/native/gfx_es2/draw_text_android.cpp +++ b/ext/native/gfx_es2/draw_text_android.cpp @@ -115,9 +115,10 @@ void TextDrawerAndroid::MeasureStringRect(const char *str, size_t len, const Bou } std::string toMeasure = std::string(str, len); - if (align & FLAG_WRAP_TEXT) { + int wrap = align & (FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT); + if (wrap) { bool rotated = (align & (ROTATE_90DEG_LEFT | ROTATE_90DEG_RIGHT)) != 0; - WrapString(toMeasure, toMeasure.c_str(), rotated ? bounds.h : bounds.w); + WrapString(toMeasure, toMeasure.c_str(), rotated ? bounds.h : bounds.w, wrap); } std::vector lines; @@ -154,6 +155,68 @@ void TextDrawerAndroid::MeasureStringRect(const char *str, size_t len, const Bou *h = total_h * dpiScale_; } +void TextDrawerAndroid::DrawStringBitmap(std::vector &bitmapData, TextStringEntry &entry, Draw::DataFormat texFormat, const char *str, int align) { + if (!strlen(str)) { + bitmapData.clear(); + return; + } + + double size = 0.0; + auto iter = fontMap_.find(fontHash_); + if (iter != fontMap_.end()) { + size = iter->second.size; + } else { + ELOG("Missing font"); + } + + jstring jstr = env_->NewStringUTF(str); + uint32_t textSize = env_->CallStaticIntMethod(cls_textRenderer, method_measureText, jstr, size); + int imageWidth = (short)(textSize >> 16); + int imageHeight = (short)(textSize & 0xFFFF); + if (imageWidth <= 0) + imageWidth = 1; + if (imageHeight <= 0) + imageHeight = 1; + + jintArray imageData = (jintArray)env_->CallStaticObjectMethod(cls_textRenderer, method_renderText, jstr, size); + env_->DeleteLocalRef(jstr); + + entry.texture = nullptr; + entry.bmWidth = imageWidth; + entry.width = imageWidth; + entry.bmHeight = imageHeight; + entry.height = imageHeight; + entry.lastUsedFrame = frameCount_; + + jint *jimage = env_->GetIntArrayElements(imageData, nullptr); + assert(env_->GetArrayLength(imageData) == imageWidth * imageHeight); + if (texFormat == Draw::DataFormat::B4G4R4A4_UNORM_PACK16 || texFormat == Draw::DataFormat::R4G4B4A4_UNORM_PACK16) { + bitmapData.resize(entry.bmWidth * entry.bmHeight * sizeof(uint16_t)); + uint16_t *bitmapData16 = (uint16_t *)&bitmapData[0]; + for (int x = 0; x < entry.bmWidth; x++) { + for (int y = 0; y < entry.bmHeight; y++) { + uint32_t v = jimage[imageWidth * y + x]; + v = 0xFFF0 | ((v >> 12) & 0xF); // Just grab some bits from the green channel. + bitmapData16[entry.bmWidth * y + x] = (uint16_t)v; + } + } + } else if (texFormat == Draw::DataFormat::R8_UNORM) { + bitmapData.resize(entry.bmWidth * entry.bmHeight); + for (int x = 0; x < entry.bmWidth; x++) { + for (int y = 0; y < entry.bmHeight; y++) { + uint32_t v = jimage[imageWidth * y + x]; + v = (v >> 12) & 0xF; // Just grab some bits from the green channel. + bitmapData[entry.bmWidth * y + x] = (uint8_t)(v | (v << 4)); + } + } + } else { + ELOG("Bad TextDrawer format"); + assert(false); + } + env_->ReleaseIntArrayElements(imageData, jimage, 0); + env_->DeleteLocalRef(imageData); +} + void TextDrawerAndroid::DrawString(DrawBuffer &target, const char *str, float x, float y, uint32_t color, int align) { using namespace Draw; std::string text(NormalizeString(std::string(str))); @@ -169,66 +232,32 @@ void TextDrawerAndroid::DrawString(DrawBuffer &target, const char *str, float x, if (iter != cache_.end()) { entry = iter->second.get(); entry->lastUsedFrame = frameCount_; - if (entry->texture) - draw_->BindTexture(0, entry->texture); } else { - double size = 0.0; - auto iter = fontMap_.find(fontHash_); - if (iter != fontMap_.end()) { - size = iter->second.size; - } else { - ELOG("Missing font"); - } - - jstring jstr = env_->NewStringUTF(text.c_str()); - uint32_t textSize = env_->CallStaticIntMethod(cls_textRenderer, method_measureText, jstr, size); - int imageWidth = (short)(textSize >> 16); - int imageHeight = (short)(textSize & 0xFFFF); - if (imageWidth <= 0) - imageWidth = 1; - if (imageHeight <= 0) - imageHeight = 1; - - jintArray imageData = (jintArray)env_->CallStaticObjectMethod(cls_textRenderer, method_renderText, jstr, size); - env_->DeleteLocalRef(jstr); + DataFormat texFormat = Draw::DataFormat::R4G4B4A4_UNORM_PACK16; entry = new TextStringEntry(); - entry->bmWidth = imageWidth; - entry->width = imageWidth; - entry->bmHeight = imageHeight; - entry->height = imageHeight; - entry->lastUsedFrame = frameCount_; TextureDesc desc{}; + std::vector bitmapData; + DrawStringBitmap(bitmapData, *entry, texFormat, text.c_str(), align); + desc.initData.push_back(&bitmapData[0]); + desc.type = TextureType::LINEAR2D; - desc.format = Draw::DataFormat::R4G4B4A4_UNORM_PACK16; + desc.format = texFormat; desc.width = entry->bmWidth; desc.height = entry->bmHeight; desc.depth = 1; desc.mipLevels = 1; desc.generateMips = false; desc.tag = "TextDrawer"; - - uint16_t *bitmapData = new uint16_t[entry->bmWidth * entry->bmHeight]; - jint* jimage = env_->GetIntArrayElements(imageData, nullptr); - assert(env_->GetArrayLength(imageData) == imageWidth * imageHeight); - for (int x = 0; x < entry->bmWidth; x++) { - for (int y = 0; y < entry->bmHeight; y++) { - uint32_t v = jimage[imageWidth * y + x]; - v = 0xFFF0 | ((v >> 12) & 0xF); // Just grab some bits from the green channel. - bitmapData[entry->bmWidth * y + x] = (uint16_t)v; - } - } - env_->ReleaseIntArrayElements(imageData, jimage, 0); - env_->DeleteLocalRef(imageData); - desc.initData.push_back((uint8_t *)bitmapData); entry->texture = draw_->CreateTexture(desc); - delete[] bitmapData; cache_[key] = std::unique_ptr(entry); - if (entry->texture) { - draw_->BindTexture(0, entry->texture); - } } + + if (entry->texture) { + draw_->BindTexture(0, entry->texture); + } + float w = entry->bmWidth * fontScaleX_ * dpiScale_; float h = entry->bmHeight * fontScaleY_ * dpiScale_; DrawBuffer::DoAlign(align, &x, &y, &w, &h); @@ -247,29 +276,6 @@ void TextDrawerAndroid::ClearCache() { sizeCache_.clear(); } -void TextDrawerAndroid::DrawStringRect(DrawBuffer &target, const char *str, const Bounds &bounds, uint32_t color, int align) { - float x = bounds.x; - float y = bounds.y; - if (align & ALIGN_HCENTER) { - x = bounds.centerX(); - } else if (align & ALIGN_RIGHT) { - x = bounds.x2(); - } - if (align & ALIGN_VCENTER) { - y = bounds.centerY(); - } else if (align & ALIGN_BOTTOM) { - y = bounds.y2(); - } - - std::string toDraw = str; - if (align & FLAG_WRAP_TEXT) { - bool rotated = (align & (ROTATE_90DEG_LEFT | ROTATE_90DEG_RIGHT)) != 0; - WrapString(toDraw, str, rotated ? bounds.h : bounds.w); - } - - DrawString(target, toDraw.c_str(), x, y, color, align); -} - void TextDrawerAndroid::OncePerFrame() { frameCount_++; // If DPI changed (small-mode, future proper monitor DPI support), drop everything. diff --git a/ext/native/gfx_es2/draw_text_android.h b/ext/native/gfx_es2/draw_text_android.h index 9ed3fdbd22..dff54dade4 100644 --- a/ext/native/gfx_es2/draw_text_android.h +++ b/ext/native/gfx_es2/draw_text_android.h @@ -24,7 +24,7 @@ public: void MeasureString(const char *str, size_t len, float *w, float *h) override; void MeasureStringRect(const char *str, size_t len, const Bounds &bounds, float *w, float *h, int align = ALIGN_TOPLEFT) override; void DrawString(DrawBuffer &target, const char *str, float x, float y, uint32_t color, int align = ALIGN_TOPLEFT) override; - void DrawStringRect(DrawBuffer &target, const char *str, const Bounds &bounds, uint32_t color, int align) override; + void DrawStringBitmap(std::vector &bitmapData, TextStringEntry &entry, Draw::DataFormat texFormat, const char *str, int align = ALIGN_TOPLEFT) override; // Use for housekeeping like throwing out old strings. void OncePerFrame() override; diff --git a/ext/native/gfx_es2/draw_text_qt.cpp b/ext/native/gfx_es2/draw_text_qt.cpp index 41d4009d3a..d792f656d8 100644 --- a/ext/native/gfx_es2/draw_text_qt.cpp +++ b/ext/native/gfx_es2/draw_text_qt.cpp @@ -1,3 +1,4 @@ +#include #include "base/display.h" #include "base/logging.h" #include "base/stringutil.h" @@ -33,7 +34,7 @@ uint32_t TextDrawerQt::SetFont(const char *fontName, int size, int flags) { return fontHash; } - QFont* font = fontName ? new QFont(fontName) : new QFont(); + QFont *font = fontName ? new QFont(fontName) : new QFont(); font->setPixelSize(size + 6); fontMap_[fontHash] = font; fontHash_ = fontHash; @@ -41,22 +42,44 @@ uint32_t TextDrawerQt::SetFont(const char *fontName, int size, int flags) { } void TextDrawerQt::SetFont(uint32_t fontHandle) { - + uint32_t fontHash = fontHandle; + auto iter = fontMap_.find(fontHash); + if (iter != fontMap_.end()) { + fontHash_ = fontHandle; + } else { + ELOG("Invalid font handle %08x", fontHandle); + } } void TextDrawerQt::MeasureString(const char *str, size_t len, float *w, float *h) { - QFont* font = fontMap_.find(fontHash_)->second; - QFontMetrics fm(*font); - QSize size = fm.size(0, QString::fromUtf8(str, (int)len)); - *w = (float)size.width() * fontScaleX_; - *h = (float)size.height() * fontScaleY_; + CacheKey key{ std::string(str, len), fontHash_ }; + + TextMeasureEntry *entry; + auto iter = sizeCache_.find(key); + if (iter != sizeCache_.end()) { + entry = iter->second.get(); + } else { + QFont* font = fontMap_.find(fontHash_)->second; + QFontMetrics fm(*font); + QSize size = fm.size(0, QString::fromUtf8(str, (int)len)); + + entry = new TextMeasureEntry(); + entry->width = size.width(); + entry->height = size.height(); + sizeCache_[key] = std::unique_ptr(entry); + } + + entry->lastUsedFrame = frameCount_; + *w = entry->width * fontScaleX_ * dpiScale_; + *h = entry->height * fontScaleY_ * dpiScale_; } void TextDrawerQt::MeasureStringRect(const char *str, size_t len, const Bounds &bounds, float *w, float *h, int align) { std::string toMeasure = std::string(str, len); - if (align & FLAG_WRAP_TEXT) { + int wrap = align & (FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT); + if (wrap) { bool rotated = (align & (ROTATE_90DEG_LEFT | ROTATE_90DEG_RIGHT)) != 0; - WrapString(toMeasure, toMeasure.c_str(), rotated ? bounds.h : bounds.w); + WrapString(toMeasure, toMeasure.c_str(), rotated ? bounds.h : bounds.w, wrap); } QFont* font = fontMap_.find(fontHash_)->second; @@ -66,71 +89,102 @@ void TextDrawerQt::MeasureStringRect(const char *str, size_t len, const Bounds & *h = (float)size.height() * fontScaleY_; } +void TextDrawerQt::DrawStringBitmap(std::vector &bitmapData, TextStringEntry &entry, Draw::DataFormat texFormat, const char *str, int align) { + if (!strlen(str)) { + bitmapData.clear(); + return; + } + + QFont *font = fontMap_.find(fontHash_)->second; + QFontMetrics fm(*font); + QSize size = fm.size(0, QString::fromUtf8(str)); + QImage image((size.width() + 3) & ~3, (size.height() + 3) & ~3, QImage::Format_ARGB32_Premultiplied); + if (image.isNull()) { + bitmapData.clear(); + return; + } + image.fill(0); + + QPainter painter; + painter.begin(&image); + painter.setFont(*font); + painter.setPen(0xFFFFFFFF); + // TODO: Involve ALIGN_HCENTER (bounds etc.) + painter.drawText(image.rect(), Qt::AlignTop | Qt::AlignLeft, QString::fromUtf8(str).replace("&&", "&")); + painter.end(); + + entry.texture = nullptr; + entry.bmWidth = entry.width = image.width(); + entry.bmHeight = entry.height = image.height(); + entry.lastUsedFrame = frameCount_; + + if (texFormat == Draw::DataFormat::B4G4R4A4_UNORM_PACK16 || texFormat == Draw::DataFormat::R4G4B4A4_UNORM_PACK16) { + bitmapData.resize(entry.bmWidth * entry.bmHeight * sizeof(uint16_t)); + uint16_t *bitmapData16 = (uint16_t *)&bitmapData[0]; + for (int x = 0; x < entry.bmWidth; x++) { + for (int y = 0; y < entry.bmHeight; y++) { + bitmapData16[entry.bmWidth * y + x] = 0xfff0 | (image.pixel(x, y) >> 28); + } + } + } else if (texFormat == Draw::DataFormat::R8_UNORM) { + bitmapData.resize(entry.bmWidth * entry.bmHeight); + for (int x = 0; x < entry.bmWidth; x++) { + for (int y = 0; y < entry.bmHeight; y++) { + bitmapData[entry.bmWidth * y + x] = image.pixel(x, y) >> 24; + } + } + } else { + ELOG("Bad TextDrawer format"); + assert(false); + } +} + void TextDrawerQt::DrawString(DrawBuffer &target, const char *str, float x, float y, uint32_t color, int align) { using namespace Draw; if (!strlen(str)) return; - uint32_t stringHash = hash::Adler32((const uint8_t *)str, strlen(str)); - uint32_t entryHash = stringHash ^ fontHash_ ^ (align << 24); - + CacheKey key{ std::string(str), fontHash_ }; target.Flush(true); TextStringEntry *entry; - auto iter = cache_.find(entryHash); + auto iter = cache_.find(key); if (iter != cache_.end()) { entry = iter->second.get(); + entry->lastUsedFrame = frameCount_; } else { - QFont *font = fontMap_.find(fontHash_)->second; - QFontMetrics fm(*font); - QSize size = fm.size(0, QString::fromUtf8(str)); - QImage image((size.width() + 3) & ~3, (size.height() + 3) & ~3, QImage::Format_ARGB32_Premultiplied); - if (image.isNull()) { - return; - } - image.fill(0); - - QPainter painter; - painter.begin(&image); - painter.setFont(*font); - painter.setPen(color); - // TODO: Involve ALIGN_HCENTER (bounds etc.) - painter.drawText(image.rect(), Qt::AlignTop | Qt::AlignLeft, QString::fromUtf8(str).replace("&&", "&")); - painter.end(); + DataFormat texFormat = Draw::DataFormat::R4G4B4A4_UNORM_PACK16; entry = new TextStringEntry(); - entry->bmWidth = entry->width = image.width(); - entry->bmHeight = entry->height = image.height(); TextureDesc desc{}; + std::vector bitmapData; + DrawStringBitmap(bitmapData, *entry, texFormat, str, align); + desc.initData.push_back(&bitmapData[0]); + desc.type = TextureType::LINEAR2D; - desc.format = Draw::DataFormat::R4G4B4A4_UNORM_PACK16; + desc.format = texFormat; desc.width = entry->bmWidth; desc.height = entry->bmHeight; desc.depth = 1; desc.mipLevels = 1; desc.tag = "TextDrawer"; - - uint16_t *bitmapData = new uint16_t[entry->bmWidth * entry->bmHeight]; - for (int x = 0; x < entry->bmWidth; x++) { - for (int y = 0; y < entry->bmHeight; y++) { - bitmapData[entry->bmWidth * y + x] = 0xfff0 | image.pixel(x, y) >> 28; - } - } - desc.initData.push_back((uint8_t *)bitmapData); entry->texture = draw_->CreateTexture(desc); - delete[] bitmapData; - cache_[entryHash] = std::unique_ptr(entry); + cache_[key] = std::unique_ptr(entry); } - float w = entry->bmWidth * fontScaleX_; - float h = entry->bmHeight * fontScaleY_; - entry->lastUsedFrame = frameCount_; - draw_->BindTexture(0, entry->texture); + if (entry->texture) { + draw_->BindTexture(0, entry->texture); + } + + float w = entry->bmWidth * fontScaleX_ * dpiScale_; + float h = entry->bmHeight * fontScaleY_ * dpiScale_; DrawBuffer::DoAlign(align, &x, &y, &w, &h); - target.DrawTexRect(x, y, x + w, y + h, 0.0f, 0.0f, 1.0f, 1.0f, color); - target.Flush(true); + if (entry->texture) { + target.DrawTexRect(x, y, x + w, y + h, 0.0f, 0.0f, 1.0f, 1.0f, color); + target.Flush(true); + } } void TextDrawerQt::ClearCache() { @@ -148,29 +202,6 @@ void TextDrawerQt::ClearCache() { fontHash_ = 0; } -void TextDrawerQt::DrawStringRect(DrawBuffer &target, const char *str, const Bounds &bounds, uint32_t color, int align) { - float x = bounds.x; - float y = bounds.y; - if (align & ALIGN_HCENTER) { - x = bounds.centerX(); - } else if (align & ALIGN_RIGHT) { - x = bounds.x2(); - } - if (align & ALIGN_VCENTER) { - y = bounds.centerY(); - } else if (align & ALIGN_BOTTOM) { - y = bounds.y2(); - } - - std::string toDraw = str; - if (align & FLAG_WRAP_TEXT) { - bool rotated = (align & (ROTATE_90DEG_LEFT | ROTATE_90DEG_RIGHT)) != 0; - WrapString(toDraw, str, rotated ? bounds.h : bounds.w); - } - - DrawString(target, toDraw.c_str(), x, y, color, align); -} - void TextDrawerQt::OncePerFrame() { frameCount_++; // If DPI changed (small-mode, future proper monitor DPI support), drop everything. diff --git a/ext/native/gfx_es2/draw_text_qt.h b/ext/native/gfx_es2/draw_text_qt.h index 815a76a13a..c75cd51ca0 100644 --- a/ext/native/gfx_es2/draw_text_qt.h +++ b/ext/native/gfx_es2/draw_text_qt.h @@ -17,7 +17,7 @@ public: void MeasureString(const char *str, size_t len, float *w, float *h) override; void MeasureStringRect(const char *str, size_t len, const Bounds &bounds, float *w, float *h, int align = ALIGN_TOPLEFT) override; void DrawString(DrawBuffer &target, const char *str, float x, float y, uint32_t color, int align = ALIGN_TOPLEFT) override; - void DrawStringRect(DrawBuffer &target, const char *str, const Bounds &bounds, uint32_t color, int align) override; + void DrawStringBitmap(std::vector &bitmapData, TextStringEntry &entry, Draw::DataFormat texFormat, const char *str, int align = ALIGN_TOPLEFT) override; // Use for housekeeping like throwing out old strings. void OncePerFrame() override; @@ -27,9 +27,8 @@ protected: uint32_t fontHash_; std::map fontMap_; - // The key is the CityHash of the string xor the fontHash_. - std::map> cache_; - std::map> sizeCache_; + std::map> cache_; + std::map> sizeCache_; }; -#endif \ No newline at end of file +#endif diff --git a/ext/native/gfx_es2/draw_text_win.cpp b/ext/native/gfx_es2/draw_text_win.cpp index f6c1113d5f..2570236feb 100644 --- a/ext/native/gfx_es2/draw_text_win.cpp +++ b/ext/native/gfx_es2/draw_text_win.cpp @@ -1,3 +1,4 @@ +#include #include "base/display.h" #include "base/logging.h" #include "base/stringutil.h" @@ -153,11 +154,15 @@ void TextDrawerWin32::MeasureStringRect(const char *str, size_t len, const Bound } std::string toMeasure = std::string(str, len); - if (align & FLAG_WRAP_TEXT) { + int wrap = align & (FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT); + if (wrap) { bool rotated = (align & (ROTATE_90DEG_LEFT | ROTATE_90DEG_RIGHT)) != 0; - WrapString(toMeasure, toMeasure.c_str(), rotated ? bounds.h : bounds.w); + WrapString(toMeasure, toMeasure.c_str(), rotated ? bounds.h : bounds.w, wrap); } + TEXTMETRIC metrics{}; + GetTextMetrics(ctx_->hDC, &metrics); + std::vector lines; SplitString(toMeasure, '\n', lines); float total_w = 0.0f; @@ -184,19 +189,114 @@ void TextDrawerWin32::MeasureStringRect(const char *str, size_t len, const Bound if (total_w < entry->width * fontScaleX_) { total_w = entry->width * fontScaleX_; } - total_h += entry->height * fontScaleY_; + int h = i == lines.size() - 1 ? entry->height : metrics.tmHeight + metrics.tmExternalLeading; + total_h += h * fontScaleY_; } + *w = total_w * dpiScale_; *h = total_h * dpiScale_; } +void TextDrawerWin32::DrawStringBitmap(std::vector &bitmapData, TextStringEntry &entry, Draw::DataFormat texFormat, const char *str, int align) { + if (!strlen(str)) { + bitmapData.clear(); + return; + } + + std::wstring wstr = ConvertUTF8ToWString(ReplaceAll(str, "\n", "\r\n")); + SIZE size; + + auto iter = fontMap_.find(fontHash_); + if (iter != fontMap_.end()) { + SelectObject(ctx_->hDC, iter->second->hFont); + } + // Set text properties + SetTextColor(ctx_->hDC, 0xFFFFFF); + SetBkColor(ctx_->hDC, 0); + SetTextAlign(ctx_->hDC, TA_TOP); + + // This matters for multi-line text - DT_CENTER is horizontal only. + UINT dtAlign = (align & ALIGN_HCENTER) == 0 ? DT_LEFT : DT_CENTER; + + RECT textRect = { 0 }; + DrawTextExW(ctx_->hDC, (LPWSTR)wstr.c_str(), (int)wstr.size(), &textRect, DT_HIDEPREFIX | DT_TOP | dtAlign | DT_CALCRECT, 0); + size.cx = textRect.right; + size.cy = textRect.bottom; + + if (size.cx > MAX_TEXT_WIDTH) + size.cx = MAX_TEXT_WIDTH; + if (size.cy > MAX_TEXT_HEIGHT) + size.cy = MAX_TEXT_HEIGHT; + // Prevent zero-sized textures, which can occur. Not worth to avoid + // creating the texture altogether in this case. One example is a string + // containing only '\r\n', see issue #10764. + if (size.cx == 0) + size.cx = 1; + if (size.cy == 0) + size.cy = 1; + + entry.texture = nullptr; + entry.width = size.cx; + entry.height = size.cy; + entry.bmWidth = (size.cx + 3) & ~3; + entry.bmHeight = (size.cy + 3) & ~3; + entry.lastUsedFrame = frameCount_; + + RECT rc = { 0 }; + rc.right = entry.bmWidth; + rc.bottom = entry.bmHeight; + FillRect(ctx_->hDC, &rc, (HBRUSH)GetStockObject(BLACK_BRUSH)); + DrawTextExW(ctx_->hDC, (LPWSTR)wstr.c_str(), (int)wstr.size(), &rc, DT_HIDEPREFIX | DT_TOP | dtAlign, 0); + + // Convert the bitmap to a Thin3D compatible array of 16-bit pixels. Can't use a single channel format + // because we need white. Well, we could using swizzle, but not all our backends support that. + if (texFormat == Draw::DataFormat::R8G8B8A8_UNORM || texFormat == Draw::DataFormat::B8G8R8A8_UNORM) { + bitmapData.resize(entry.bmWidth * entry.bmHeight * sizeof(uint32_t)); + uint32_t *bitmapData32 = (uint32_t *)&bitmapData[0]; + for (int y = 0; y < entry.bmHeight; y++) { + for (int x = 0; x < entry.bmWidth; x++) { + uint8_t bAlpha = (uint8_t)(ctx_->pBitmapBits[MAX_TEXT_WIDTH * y + x] & 0xff); + bitmapData32[entry.bmWidth * y + x] = (bAlpha << 24) | 0x00ffffff; + } + } + } else if (texFormat == Draw::DataFormat::B4G4R4A4_UNORM_PACK16 || texFormat == Draw::DataFormat::R4G4B4A4_UNORM_PACK16) { + bitmapData.resize(entry.bmWidth * entry.bmHeight * sizeof(uint16_t)); + uint16_t *bitmapData16 = (uint16_t *)&bitmapData[0]; + for (int y = 0; y < entry.bmHeight; y++) { + for (int x = 0; x < entry.bmWidth; x++) { + uint8_t bAlpha = (uint8_t)((ctx_->pBitmapBits[MAX_TEXT_WIDTH * y + x] & 0xff) >> 4); + bitmapData16[entry.bmWidth * y + x] = (bAlpha) | 0xfff0; + } + } + } else if (texFormat == Draw::DataFormat::A4R4G4B4_UNORM_PACK16) { + bitmapData.resize(entry.bmWidth * entry.bmHeight * sizeof(uint16_t)); + uint16_t *bitmapData16 = (uint16_t *)&bitmapData[0]; + for (int y = 0; y < entry.bmHeight; y++) { + for (int x = 0; x < entry.bmWidth; x++) { + uint8_t bAlpha = (uint8_t)((ctx_->pBitmapBits[MAX_TEXT_WIDTH * y + x] & 0xff) >> 4); + bitmapData16[entry.bmWidth * y + x] = (bAlpha << 12) | 0x0fff; + } + } + } else if (texFormat == Draw::DataFormat::R8_UNORM) { + bitmapData.resize(entry.bmWidth * entry.bmHeight); + for (int y = 0; y < entry.bmHeight; y++) { + for (int x = 0; x < entry.bmWidth; x++) { + uint8_t bAlpha = ctx_->pBitmapBits[MAX_TEXT_WIDTH * y + x] & 0xff; + bitmapData[entry.bmWidth * y + x] = bAlpha; + } + } + } else { + ELOG("Bad TextDrawer format"); + assert(false); + } +} + void TextDrawerWin32::DrawString(DrawBuffer &target, const char *str, float x, float y, uint32_t color, int align) { using namespace Draw; if (!strlen(str)) return; CacheKey key{ std::string(str), fontHash_ }; - target.Flush(true); TextStringEntry *entry; @@ -206,52 +306,6 @@ void TextDrawerWin32::DrawString(DrawBuffer &target, const char *str, float x, f entry = iter->second.get(); entry->lastUsedFrame = frameCount_; } else { - // Render the string to our bitmap and save to a GL texture. - std::wstring wstr = ConvertUTF8ToWString(ReplaceAll(str, "\n", "\r\n")); - SIZE size; - - auto iter = fontMap_.find(fontHash_); - if (iter != fontMap_.end()) { - SelectObject(ctx_->hDC, iter->second->hFont); - } - // Set text properties - SetTextColor(ctx_->hDC, 0xFFFFFF); - SetBkColor(ctx_->hDC, 0); - SetTextAlign(ctx_->hDC, TA_TOP); - - // This matters for multi-line text - DT_CENTER is horizontal only. - UINT dtAlign = (align & ALIGN_HCENTER) == 0 ? DT_LEFT : DT_CENTER; - - RECT textRect = { 0 }; - DrawTextExW(ctx_->hDC, (LPWSTR)wstr.c_str(), (int)wstr.size(), &textRect, DT_HIDEPREFIX | DT_TOP | dtAlign | DT_CALCRECT, 0); - size.cx = textRect.right; - size.cy = textRect.bottom; - - if (size.cx > MAX_TEXT_WIDTH) - size.cx = MAX_TEXT_WIDTH; - if (size.cy > MAX_TEXT_HEIGHT) - size.cy = MAX_TEXT_HEIGHT; - // Prevent zero-sized textures, which can occur. Not worth to avoid - // creating the texture altogether in this case. One example is a string - // containing only '\r\n', see issue #10764. - if (size.cx == 0) - size.cx = 1; - if (size.cy == 0) - size.cy = 1; - - entry = new TextStringEntry(); - entry->width = size.cx; - entry->height = size.cy; - entry->bmWidth = (size.cx + 3) & ~3; - entry->bmHeight = (size.cy + 3) & ~3; - entry->lastUsedFrame = frameCount_; - - RECT rc = { 0 }; - rc.right = entry->bmWidth; - rc.bottom = entry->bmHeight; - FillRect(ctx_->hDC, &rc, (HBRUSH)GetStockObject(BLACK_BRUSH)); - DrawTextExW(ctx_->hDC, (LPWSTR)wstr.c_str(), (int)wstr.size(), &rc, DT_HIDEPREFIX | DT_TOP | dtAlign, 0); - DataFormat texFormat; // For our purposes these are equivalent, so just choose the supported one. D3D can emulate them. if (draw_->GetDataFormatSupport(Draw::DataFormat::A4R4G4B4_UNORM_PACK16) & FMT_TEXTURE) @@ -263,39 +317,14 @@ void TextDrawerWin32::DrawString(DrawBuffer &target, const char *str, float x, f else texFormat = Draw::DataFormat::R8G8B8A8_UNORM; + entry = new TextStringEntry(); + // Convert the bitmap to a Thin3D compatible array of 16-bit pixels. Can't use a single channel format // because we need white. Well, we could using swizzle, but not all our backends support that. TextureDesc desc{}; - uint32_t *bitmapData32 = nullptr; - uint16_t *bitmapData16 = nullptr; - if (texFormat == Draw::DataFormat::R8G8B8A8_UNORM || texFormat == Draw::DataFormat::B8G8R8A8_UNORM) { - bitmapData32 = new uint32_t[entry->bmWidth * entry->bmHeight]; - for (int y = 0; y < entry->bmHeight; y++) { - for (int x = 0; x < entry->bmWidth; x++) { - uint8_t bAlpha = (uint8_t)(ctx_->pBitmapBits[MAX_TEXT_WIDTH * y + x] & 0xff); - bitmapData32[entry->bmWidth * y + x] = (bAlpha << 24) | 0x00ffffff; - } - } - desc.initData.push_back((uint8_t *)bitmapData32); - } else if (texFormat == Draw::DataFormat::B4G4R4A4_UNORM_PACK16 || texFormat == Draw::DataFormat::R4G4B4A4_UNORM_PACK16) { - bitmapData16 = new uint16_t[entry->bmWidth * entry->bmHeight]; - for (int y = 0; y < entry->bmHeight; y++) { - for (int x = 0; x < entry->bmWidth; x++) { - uint8_t bAlpha = (uint8_t)((ctx_->pBitmapBits[MAX_TEXT_WIDTH * y + x] & 0xff) >> 4); - bitmapData16[entry->bmWidth * y + x] = (bAlpha) | 0xfff0; - } - } - desc.initData.push_back((uint8_t *)bitmapData16); - } else if (texFormat == Draw::DataFormat::A4R4G4B4_UNORM_PACK16) { - bitmapData16 = new uint16_t[entry->bmWidth * entry->bmHeight]; - for (int y = 0; y < entry->bmHeight; y++) { - for (int x = 0; x < entry->bmWidth; x++) { - uint8_t bAlpha = (uint8_t)((ctx_->pBitmapBits[MAX_TEXT_WIDTH * y + x] & 0xff) >> 4); - bitmapData16[entry->bmWidth * y + x] = (bAlpha << 12) | 0x0fff; - } - } - desc.initData.push_back((uint8_t *)bitmapData16); - } + std::vector bitmapData; + DrawStringBitmap(bitmapData, *entry, texFormat, str, align); + desc.initData.push_back(&bitmapData[0]); desc.type = TextureType::LINEAR2D; desc.format = texFormat; @@ -305,14 +334,12 @@ void TextDrawerWin32::DrawString(DrawBuffer &target, const char *str, float x, f desc.mipLevels = 1; desc.tag = "TextDrawer"; entry->texture = draw_->CreateTexture(desc); - if (bitmapData16) - delete[] bitmapData16; - if (bitmapData32) - delete[] bitmapData32; cache_[key] = std::unique_ptr(entry); } - draw_->BindTexture(0, entry->texture); + if (entry->texture) { + draw_->BindTexture(0, entry->texture); + } // Okay, the texture is bound, let's draw. float w = entry->width * fontScaleX_ * dpiScale_; @@ -320,8 +347,10 @@ void TextDrawerWin32::DrawString(DrawBuffer &target, const char *str, float x, f float u = entry->width / (float)entry->bmWidth; float v = entry->height / (float)entry->bmHeight; DrawBuffer::DoAlign(align, &x, &y, &w, &h); - target.DrawTexRect(x, y, x + w, y + h, 0.0f, 0.0f, u, v, color); - target.Flush(true); + if (entry->texture) { + target.DrawTexRect(x, y, x + w, y + h, 0.0f, 0.0f, u, v, color); + target.Flush(true); + } } void TextDrawerWin32::RecreateFonts() { @@ -340,29 +369,6 @@ void TextDrawerWin32::ClearCache() { sizeCache_.clear(); } -void TextDrawerWin32::DrawStringRect(DrawBuffer &target, const char *str, const Bounds &bounds, uint32_t color, int align) { - float x = bounds.x; - float y = bounds.y; - if (align & ALIGN_HCENTER) { - x = bounds.centerX(); - } else if (align & ALIGN_RIGHT) { - x = bounds.x2(); - } - if (align & ALIGN_VCENTER) { - y = bounds.centerY(); - } else if (align & ALIGN_BOTTOM) { - y = bounds.y2(); - } - - std::string toDraw = str; - if (align & FLAG_WRAP_TEXT) { - bool rotated = (align & (ROTATE_90DEG_LEFT | ROTATE_90DEG_RIGHT)) != 0; - WrapString(toDraw, str, rotated ? bounds.h : bounds.w); - } - - DrawString(target, toDraw.c_str(), x, y, color, align); -} - void TextDrawerWin32::OncePerFrame() { frameCount_++; // If DPI changed (small-mode, future proper monitor DPI support), drop everything. diff --git a/ext/native/gfx_es2/draw_text_win.h b/ext/native/gfx_es2/draw_text_win.h index ec76d3ef2b..41489eefbb 100644 --- a/ext/native/gfx_es2/draw_text_win.h +++ b/ext/native/gfx_es2/draw_text_win.h @@ -24,7 +24,7 @@ public: void MeasureString(const char *str, size_t len, float *w, float *h) override; void MeasureStringRect(const char *str, size_t len, const Bounds &bounds, float *w, float *h, int align = ALIGN_TOPLEFT) override; void DrawString(DrawBuffer &target, const char *str, float x, float y, uint32_t color, int align = ALIGN_TOPLEFT) override; - void DrawStringRect(DrawBuffer &target, const char *str, const Bounds &bounds, uint32_t color, int align) override; + void DrawStringBitmap(std::vector &bitmapData, TextStringEntry &entry, Draw::DataFormat texFormat, const char *str, int align = ALIGN_TOPLEFT) override; // Use for housekeeping like throwing out old strings. void OncePerFrame() override; @@ -40,4 +40,4 @@ protected: std::map> sizeCache_; }; -#endif \ No newline at end of file +#endif diff --git a/ext/native/util/text/wrap_text.cpp b/ext/native/util/text/wrap_text.cpp index edf3354673..c052056034 100644 --- a/ext/native/util/text/wrap_text.cpp +++ b/ext/native/util/text/wrap_text.cpp @@ -1,4 +1,5 @@ #include +#include "gfx_es2/draw_buffer.h" #include "util/text/utf8.h" #include "util/text/wrap_text.h" @@ -75,39 +76,58 @@ std::string WordWrapper::Wrapped() { } bool WordWrapper::WrapBeforeWord() { - if (x_ + wordWidth_ > maxW_ && out_.size() > 0) { - if (IsShy(out_[out_.size() - 1])) { - // Soft hyphen, replace it with a real hyphen since we wrapped at it. - // TODO: There's an edge case here where the hyphen might not fit. - out_[out_.size() - 1] = '-'; + if (flags_ & FLAG_WRAP_TEXT) { + if (x_ + wordWidth_ > maxW_ && !out_.empty()) { + if (IsShy(out_[out_.size() - 1])) { + // Soft hyphen, replace it with a real hyphen since we wrapped at it. + // TODO: There's an edge case here where the hyphen might not fit. + out_[out_.size() - 1] = '-'; + } + out_ += "\n"; + lastLineStart_ = out_.size(); + x_ = 0.0f; + forceEarlyWrap_ = false; + return true; + } + } + if (flags_ & FLAG_ELLIPSIZE_TEXT) { + if (x_ + wordWidth_ > maxW_) { + if (!out_.empty() && IsSpace(out_[out_.size() - 1])) { + out_[out_.size() - 1] = '.'; + out_ += ".."; + } else { + out_ += "..."; + } + x_ = maxW_; } - out_ += "\n"; - lastLineStart_ = out_.size(); - x_ = 0.0f; - forceEarlyWrap_ = false; - return true; } return false; } void WordWrapper::AppendWord(int endIndex, bool addNewline) { - int nextWordIndex = lastIndex_; + int lastWordStartIndex = lastIndex_; if (WrapBeforeWord()) { // Advance to the first non-whitespace UTF-8 character in the following word (if any) to prevent starting the new line with a whitespace - UTF8 utf8Word(str_, nextWordIndex); - while (nextWordIndex < endIndex) { + UTF8 utf8Word(str_, lastWordStartIndex); + while (lastWordStartIndex < endIndex) { const uint32_t c = utf8Word.next(); if (!IsSpace(c)) { break; } - nextWordIndex = utf8Word.byteIndex(); + lastWordStartIndex = utf8Word.byteIndex(); } } + // This will include the newline. - out_.append(str_ + nextWordIndex, str_ + endIndex); - if (addNewline) { + if (x_ < maxW_) { + out_.append(str_ + lastWordStartIndex, str_ + endIndex); + } else { + scanForNewline_ = true; + } + if (addNewline && (flags_ & FLAG_WRAP_TEXT)) { out_ += "\n"; lastLineStart_ = out_.size(); + scanForNewline_ = false; } else { // We may have appended a newline - check. size_t pos = out_.substr(lastLineStart_).find_last_of("\n"); @@ -129,6 +149,10 @@ void WordWrapper::Wrap() { return; } + if (flags_ & FLAG_ELLIPSIZE_TEXT) { + ellipsisWidth_ = MeasureWidth("...", 3); + } + for (UTF8 utf(str_); !utf.end(); ) { int beforeIndex = utf.byteIndex(); uint32_t c = utf.next(); @@ -142,6 +166,13 @@ void WordWrapper::Wrap() { wordWidth_ = 0.0f; // We wrapped once, so stop forcing. forceEarlyWrap_ = false; + scanForNewline_ = false; + continue; + } + + if (scanForNewline_) { + // We're discarding the rest of the characters until a newline (no wrapping.) + lastIndex_ = afterIndex; continue; } @@ -175,13 +206,33 @@ void WordWrapper::Wrap() { } // Now, add the word so far (without this latest character) and break. AppendWord(beforeIndex, true); - x_ = 0.0f; + if (lastLineStart_ != out_.size()) { + x_ = MeasureWidth(out_.c_str() + lastLineStart_, out_.size() - lastLineStart_); + } else { + x_ = 0.0f; + } wordWidth_ = 0.0f; forceEarlyWrap_ = false; // The current character will be handled as part of the next word. continue; } + if ((flags_ & FLAG_ELLIPSIZE_TEXT) && wordWidth_ > 0.0f && x_ + newWordWidth + ellipsisWidth_ > maxW_) { + if ((flags_ & FLAG_WRAP_TEXT) == 0) { + // Now, add the word so far (without this latest character) and show the ellipsis. + AppendWord(beforeIndex, true); + if (lastLineStart_ != out_.size()) { + x_ = MeasureWidth(out_.c_str() + lastLineStart_, out_.size() - lastLineStart_); + } else { + x_ = 0.0f; + } + wordWidth_ = 0.0f; + forceEarlyWrap_ = false; + // The current character will be handled as part of the next word. + continue; + } + } + wordWidth_ = newWordWidth; // Is this the end of a word via punctuation / CJK? diff --git a/ext/native/util/text/wrap_text.h b/ext/native/util/text/wrap_text.h index 61049bccef..554210e3cf 100644 --- a/ext/native/util/text/wrap_text.h +++ b/ext/native/util/text/wrap_text.h @@ -4,8 +4,8 @@ class WordWrapper { public: - WordWrapper(const char *str, float maxW) - : str_(str), maxW_(maxW) { + WordWrapper(const char *str, float maxW, int flags) + : str_(str), maxW_(maxW), flags_(flags) { } std::string Wrapped(); @@ -23,7 +23,9 @@ protected: const char *const str_; const float maxW_; + const int flags_; std::string out_; + // Index of last output / start of current word. int lastIndex_ = 0; // Index of last line start. @@ -32,6 +34,10 @@ protected: float x_ = 0.0f; // Most recent width of word since last index. float wordWidth_ = 0.0f; + // Width of "..." when flag is set, zero otherwise. + float ellipsisWidth_ = 0.0f; // Force the next word to cut partially and wrap. bool forceEarlyWrap_ = false; + // Skip all characters until the next newline. + bool scanForNewline_ = false; };