From b2faec05339b61a54696b74f63ed537158cd6d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 20 May 2026 10:11:46 +0200 Subject: [PATCH] Windows: Restore the GDI-based text drawer, use it if RenderDoc is attached The D2D-based text renderer requires a D3D11 context, which confuses RenderDoc (it doesn't permit multiple 3D APIs in the same process). So, let's restore the old GDI renderer and use it if RenderDoc is attached. See #21638 --- Common/Render/Text/draw_text.cpp | 35 ++- Common/Render/Text/draw_text_win.cpp | 260 +++++++++++++++++- Common/Render/Text/draw_text_win.h | 29 +- .../src/org/ppsspp/ppsspp/PpssppActivity.java | 1 + 4 files changed, 319 insertions(+), 6 deletions(-) diff --git a/Common/Render/Text/draw_text.cpp b/Common/Render/Text/draw_text.cpp index a20f8dedc8ad..607a389c00a3 100644 --- a/Common/Render/Text/draw_text.cpp +++ b/Common/Render/Text/draw_text.cpp @@ -1,5 +1,11 @@ #include "ppsspp_config.h" +#if defined(__linux__) && !defined(__ANDROID__) +# include +#elif defined(_WIN32) +# include +#endif + #include #include "Common/System/Display.h" #include "Common/GPU/thin3d.h" @@ -13,8 +19,27 @@ #include "Common/Render/Text/draw_text_qt.h" #include "Common/Render/Text/draw_text_android.h" #include "Common/Render/Text/draw_text_sdl.h" +#include "Common/Render/Text/draw_text_uwp.h" #include "Common/StringUtils.h" + +// TODO: Move this to somewhere more appropriate. +static bool IsRenderDocLoaded() { +#if PPSSPP_PLATFORM(UWP) || PPSSPP_PLATFORM(IOS) || PPSSPP_PLATFORM(MAC) || PPSSPP_PLATFORM(ANDROID) + return false; +#elif defined(_WIN32) + return (GetModuleHandleA("renderdoc.dll") != nullptr); +#elif defined(__linux__) + // Returns a pointer if loaded, otherwise null + void* handle = dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD); + if (handle) { + dlclose(handle); + return true; + } + return false; +#endif +} + TextDrawer::TextDrawer(Draw::DrawContext *draw) : draw_(draw) { // These probably shouldn't be state. dpiScale_ = CalculateDPIScale(); @@ -272,8 +297,16 @@ TextDrawer *TextDrawer::Create(Draw::DrawContext *draw) { TextDrawer *drawer = nullptr; #if defined(__LIBRETRO__) // No text drawer +#elif PPSSPP_PLATFORM(UWP) + drawer = new TextDrawerUWP(draw); #elif defined(_WIN32) && !defined(USING_QT_UI) - drawer = new TextDrawerWin32(draw); + // If RenderDoc is active, we can't use the UWP drawer since it uses D3D11 to initialize D2D, + // and RenderDoc doesn't permit multiple graphics contexts. + if (IsRenderDocLoaded()) { + drawer = new TextDrawerWin32(draw); + } else { + drawer = new TextDrawerUWP(draw); + } #elif PPSSPP_PLATFORM(MAC) || PPSSPP_PLATFORM(IOS) drawer = new TextDrawerCocoa(draw); #elif defined(USING_QT_UI) diff --git a/Common/Render/Text/draw_text_win.cpp b/Common/Render/Text/draw_text_win.cpp index dfb348c96d68..05bcf044bb50 100644 --- a/Common/Render/Text/draw_text_win.cpp +++ b/Common/Render/Text/draw_text_win.cpp @@ -1,5 +1,261 @@ #include "ppsspp_config.h" +#include "Common/System/Display.h" +#include "Common/GPU/thin3d.h" +#include "Common/Data/Hash/Hash.h" +#include "Common/Data/Text/WrapText.h" +#include "Common/Data/Encoding/Utf8.h" +#include "Common/Render/Text/draw_text.h" +#include "Common/Render/Text/draw_text_win.h" + +#include "Common/Log.h" +#include "Common/StringUtils.h" + +#if defined(_WIN32) && !defined(USING_QT_UI) && !PPSSPP_PLATFORM(UWP) + +#define WIN32_LEAN_AND_MEAN +#include + +enum { + MAX_TEXT_WIDTH = 4096, + MAX_TEXT_HEIGHT = 512 +}; + +class TextDrawerFontContext { +public: + explicit TextDrawerFontContext(const FontStyle &_style, float _dpiScale) : hFont(0), style(_style), dpiScale(_dpiScale) { + FontStyleFlags styleFlags{}; + std::string fontName = GetFontNameForFontStyle(style, &styleFlags); + if (fontName.empty()) { + // Shouldn't happen. + fontName = "Tahoma"; + } + + int weight = FW_NORMAL; + if (styleFlags & FontStyleFlags::Bold) { + weight = FW_BOLD; + } + if (styleFlags & FontStyleFlags::Light) { + weight = FW_LIGHT; + } + + bool italic = (styleFlags & FontStyleFlags::Italic); + int height = style.sizePts; + + if (hFont) { + Destroy(); + } + // We apparently specify all font sizes in pts (1pt = 1.33px), so divide by only 72 for pixels. + int nHeight = -MulDiv(height, (int)(96.0f * (1.0f / dpiScale)), 72); + hFont = CreateFont(nHeight, 0, 0, 0, weight, italic, + FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, + CLIP_DEFAULT_PRECIS, PROOF_QUALITY, + VARIABLE_PITCH, ConvertUTF8ToWString(fontName).c_str()); + } + + ~TextDrawerFontContext() { + Destroy(); + } + + void Destroy() { + DeleteObject(hFont); + hFont = 0; + } + + HFONT hFont = 0; + FontStyle style; + float dpiScale; +}; + +struct TextDrawerContext { + HDC hDC; + HBITMAP hbmBitmap; + int *pBitmapBits; +}; + +TextDrawerWin32::TextDrawerWin32(Draw::DrawContext *draw) : TextDrawer(draw), ctx_(nullptr) { + ctx_ = new TextDrawerContext(); + ctx_->hDC = CreateCompatibleDC(NULL); + + // Load the font files (pass the all flags so we get all filenames); + std::vector fonts = GetAllFontFilenames(); + for (const auto &iter : fonts) { + std::string fn = "assets/" + iter + ".ttf"; + int numFontsAdded = AddFontResourceEx(ConvertUTF8ToWString(fn).c_str(), FR_PRIVATE, NULL); + if (numFontsAdded == 0) { + ERROR_LOG(Log::G3D, "Failed to add font resource from %s", fn.c_str()); + } + } + + BITMAPINFO bmi{}; + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = MAX_TEXT_WIDTH; + bmi.bmiHeader.biHeight = -MAX_TEXT_HEIGHT; + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biCompression = BI_RGB; + bmi.bmiHeader.biBitCount = 32; + + ctx_->hbmBitmap = CreateDIBSection(ctx_->hDC, &bmi, DIB_RGB_COLORS, (VOID**)&ctx_->pBitmapBits, NULL, 0); + _assert_(ctx_->hbmBitmap != nullptr); + SetMapMode(ctx_->hDC, MM_TEXT); + + SelectObject(ctx_->hDC, ctx_->hbmBitmap); +} + +TextDrawerWin32::~TextDrawerWin32() { + ClearCache(); + ClearFonts(); + + // Unload the fonts. + std::vector fonts = GetAllFontFilenames(); + for (const auto &iter : fonts) { + std::string fn = "assets/" + iter + ".ttf"; + RemoveFontResourceEx(ConvertUTF8ToWString(fn).c_str(), FR_PRIVATE, NULL); + } + + DeleteObject(ctx_->hbmBitmap); + DeleteDC(ctx_->hDC); + delete ctx_; +} + +void TextDrawerWin32::SetOrCreateFont(const FontStyle &style) { + auto iter = fontMap_.find(style); + if (iter != fontMap_.end()) { + fontStyle_ = style; + return; + } + + fontMap_[style] = std::make_unique(style, dpiScale_); + fontStyle_ = style; +} + +void TextDrawerWin32::MeasureStringInternal(std::string_view str, float *w, float *h) { + auto iter = fontMap_.find(fontStyle_); + if (iter != fontMap_.end()) { + SelectObject(ctx_->hDC, iter->second->hFont); + } else { + ERROR_LOG(Log::G3D, "Failed to measure string"); + return; + } + + std::vector lines; + SplitString(str, '\n', lines); + + int extW = 0, extH = 0; + for (auto &line : lines) { + SIZE size; + std::wstring wstr = ConvertUTF8ToWString(line); + if (wstr.empty() && lines.size() > 1) { + // Measure empty lines as if it was a space. + wstr = L" "; + } + GetTextExtentPoint32(ctx_->hDC, wstr.c_str(), (int)wstr.size(), &size); + if (size.cx > extW) + extW = size.cx; + extH += size.cy; + } + + *w = extW; + *h = extH; +} + +bool TextDrawerWin32::DrawStringBitmap(std::vector &bitmapData, TextStringEntry &entry, Draw::DataFormat texFormat, std::string_view str, int align, bool fullColor) { + if (str.empty()) { + bitmapData.clear(); + return false; + } + std::wstring wstr = ConvertUTF8ToWString(ReplaceAll(str, "\n", "\r\n")); + + auto iter = fontMap_.find(fontStyle_); + 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_NOPREFIX | DT_TOP | dtAlign | DT_CALCRECT, 0); + SIZE size; + 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; + + if (size.cx == 0 || size.cy == 0) { + // Don't draw zero-sized textures. + WARN_LOG(Log::G3D, "Text '%.*s' caused a zero size image", (int)str.length(), str.data()); + return false; + } + + 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_NOPREFIX | 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] = AlphaToPremul8888(bAlpha); + } + } + } 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); + bitmapData16[entry.bmWidth * y + x] = AlphaToPremul4444(bAlpha); + } + } + } 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); + bitmapData16[entry.bmWidth * y + x] = AlphaToPremul4444(bAlpha); + } + } + } 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 { + _assert_msg_(false, "Bad TextDrawer format"); + } + return true; +} + +void TextDrawerWin32::ClearFonts() { + for (auto &iter : fontMap_) { + iter.second->Destroy(); + } + fontMap_.clear(); +} -#if defined(_WIN32) && !defined(USING_QT_UI) -// TextDrawerWin32 is an alias of TextDrawerUWP; implementation is in draw_text_uwp.cpp. #endif diff --git a/Common/Render/Text/draw_text_win.h b/Common/Render/Text/draw_text_win.h index 5f894d5be882..9b004a251c92 100644 --- a/Common/Render/Text/draw_text_win.h +++ b/Common/Render/Text/draw_text_win.h @@ -1,8 +1,31 @@ #pragma once -#include "Common/Render/Text/draw_text_uwp.h" +#include "ppsspp_config.h" -#if defined(_WIN32) && !defined(USING_QT_UI) -using TextDrawerWin32 = TextDrawerUWP; +#include +#include "Common/Render/Text/draw_text.h" + +#if defined(_WIN32) && !defined(USING_QT_UI) && !PPSSPP_PLATFORM(UWP) + +struct TextDrawerContext; +// Internal struct but all details in .cpp file (pimpl to avoid pulling in excessive headers here) +class TextDrawerFontContext; + +class TextDrawerWin32 : public TextDrawer { +public: + TextDrawerWin32(Draw::DrawContext *draw); + ~TextDrawerWin32(); + + void SetOrCreateFont(const FontStyle &style) override; + bool DrawStringBitmap(std::vector &bitmapData, TextStringEntry &entry, Draw::DataFormat texFormat, std::string_view str, int align, bool fullColor) override; + +protected: + void MeasureStringInternal(std::string_view str, float *w, float *h) override; + bool SupportsColorEmoji() const override { return false; } + void ClearFonts() override; + + TextDrawerContext *ctx_; + std::map> fontMap_; +}; #endif diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index 62caaa4f1ac2..55ada7d4245a 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -1650,6 +1650,7 @@ public boolean processCommand(String command, String params) { // Workaround for issue #13363 to fix Split/Second game start - it requires text input // but we don't support it on VR devices. if (isVRDevice()) { + Log.w(TAG, "VR: Missing UI, supplying default string for text input request"); NativeApp.sendRequestResult(requestID, false, defString, 0); return true; }