mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-15 15:30:29 +02:00
The libretro core is the frontend, and the headless tools are the test and debugging hosts. The Windows, UWP, Android, iOS, macOS and SDL apps, the UI framework and imgui go, along with what only they used: the Win32 debugger hooks (g_frameMutex, the shutdown lock), JNI thread attachment, the Android text drawer, RAIntegration, Discord, recent files, game installs, tilt input, the audio resamplers, the control mapper, memstick moving and the remote ISO and upload server modes. The web server keeps the debugger. The MSVC, Gradle and ndk-build projects go too.
399 lines
12 KiB
C++
399 lines
12 KiB
C++
#include "ppsspp_config.h"
|
|
|
|
#if defined(__linux__) || defined(__FreeBSD__) && !defined(__ANDROID__)
|
|
# include <dlfcn.h>
|
|
#elif defined(_WIN32)
|
|
# include <windows.h>
|
|
#endif
|
|
|
|
#include <algorithm>
|
|
#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/Render/Text/draw_text_cocoa.h"
|
|
#include "Common/Render/Text/draw_text_sdl.h"
|
|
#include "Common/Render/Text/draw_text_uwp.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Common/OSVersion.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__) || defined(__FreeBSD__)
|
|
// Returns a pointer if loaded, otherwise null
|
|
void* handle = dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD);
|
|
if (handle) {
|
|
dlclose(handle);
|
|
return true;
|
|
}
|
|
return false;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
TextDrawer::TextDrawer(Draw::DrawContext *draw) : draw_(draw) {
|
|
// These probably shouldn't be state.
|
|
dpiScale_ = CalculateDPIScale();
|
|
}
|
|
|
|
float TextDrawerWordWrapper::MeasureWidth(std::string_view str) {
|
|
float w, h;
|
|
drawer_->MeasureString(str, &w, &h);
|
|
return w;
|
|
}
|
|
|
|
void TextDrawer::WrapString(std::string &out, std::string_view str, float maxW, int flags) {
|
|
TextDrawerWordWrapper wrapper(this, str, maxW, flags);
|
|
out = wrapper.Wrapped();
|
|
}
|
|
|
|
void TextDrawer::SetFontScale(float xscale, float yscale) {
|
|
fontScaleX_ = xscale;
|
|
fontScaleY_ = yscale;
|
|
}
|
|
|
|
float TextDrawer::CalculateDPIScale() const {
|
|
if (ignoreGlobalDpi_)
|
|
return dpiScale_;
|
|
return g_display.dpi_scale_y;
|
|
}
|
|
|
|
void TextDrawer::DrawString(DrawBuffer &target, std::string_view str, float x, float y, uint32_t color, int align) {
|
|
using namespace Draw;
|
|
if (str.empty()) {
|
|
return;
|
|
}
|
|
|
|
const CacheKeyType key{ std::string(str), fontStyle_ };
|
|
target.Flush(true);
|
|
|
|
TextStringEntry *entry;
|
|
|
|
auto iter = cache_.find(key);
|
|
if (iter != cache_.end()) {
|
|
entry = iter->second.get();
|
|
entry->lastUsedFrame = frameCount_;
|
|
if (!entry->texture) {
|
|
return;
|
|
}
|
|
} else {
|
|
DataFormat texFormat;
|
|
// Pick between the supported formats, of which at least one is supported on each platform. Prefer R8 (but only if swizzle is supported)
|
|
bool emoji = SupportsColorEmoji() && AnyEmojiInString(str.data(), str.length());
|
|
if (emoji) {
|
|
texFormat = Draw::DataFormat::R8G8B8A8_UNORM;
|
|
} else if ((draw_->GetDataFormatSupport(Draw::DataFormat::R8_UNORM) & Draw::FMT_TEXTURE) != 0 && draw_->GetDeviceCaps().textureSwizzleSupported) {
|
|
texFormat = Draw::DataFormat::R8_UNORM;
|
|
} else if (draw_->GetDataFormatSupport(Draw::DataFormat::R4G4B4A4_UNORM_PACK16) & FMT_TEXTURE) {
|
|
texFormat = Draw::DataFormat::R4G4B4A4_UNORM_PACK16;
|
|
} else if (draw_->GetDataFormatSupport(Draw::DataFormat::A4R4G4B4_UNORM_PACK16) & FMT_TEXTURE) {
|
|
texFormat = Draw::DataFormat::A4R4G4B4_UNORM_PACK16;
|
|
} else if (draw_->GetDataFormatSupport(Draw::DataFormat::B4G4R4A4_UNORM_PACK16) & FMT_TEXTURE) {
|
|
texFormat = Draw::DataFormat::B4G4R4A4_UNORM_PACK16;
|
|
} else {
|
|
texFormat = Draw::DataFormat::R8G8B8A8_UNORM;
|
|
}
|
|
|
|
entry = new TextStringEntry(frameCount_);
|
|
|
|
// 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{};
|
|
std::vector<uint8_t> bitmapData;
|
|
if (!DrawStringBitmap(bitmapData, *entry, texFormat, str, align, emoji)) {
|
|
// Nothing drawn. Store that fact in the cache.
|
|
cache_[key] = std::unique_ptr<TextStringEntry>(entry);
|
|
return;
|
|
}
|
|
|
|
desc.initData.push_back(&bitmapData[0]);
|
|
|
|
desc.type = TextureType::LINEAR2D;
|
|
desc.format = texFormat;
|
|
desc.width = entry->bmWidth;
|
|
desc.height = entry->bmHeight;
|
|
desc.depth = 1;
|
|
desc.mipLevels = 1;
|
|
desc.tag = "TextDrawer";
|
|
desc.swizzle = texFormat == Draw::DataFormat::R8_UNORM ? Draw::TextureSwizzle::R8_AS_PREMUL_ALPHA : Draw::TextureSwizzle::DEFAULT,
|
|
entry->texture = draw_->CreateTexture(desc);
|
|
cache_[key] = std::unique_ptr<TextStringEntry>(entry);
|
|
}
|
|
|
|
_dbg_assert_(entry->texture);
|
|
draw_->BindTexture(0, entry->texture);
|
|
|
|
// Okay, the texture is bound, let's draw.
|
|
float w = (float)entry->width * (fontScaleX_ * dpiScale_);
|
|
float h = (float)entry->height * (fontScaleY_ * dpiScale_);
|
|
float u = (float)entry->width / (float)entry->bmWidth;
|
|
float v = (float)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);
|
|
}
|
|
|
|
void TextDrawer::MeasureString(std::string_view str, float *w, float *h) {
|
|
if (str.empty()) {
|
|
*w = 0.0f;
|
|
*h = 0.0f;
|
|
return;
|
|
}
|
|
|
|
// Clamp the size to something sane.
|
|
if (str.size() > MAX_TEXT_LENGTH) {
|
|
str = str.substr(0, MAX_TEXT_LENGTH);
|
|
}
|
|
|
|
const CacheKeyType key{std::string(str), fontStyle_};
|
|
|
|
TextMeasureEntry *entry;
|
|
auto iter = sizeCache_.find(key);
|
|
if (iter != sizeCache_.end()) {
|
|
entry = iter->second.get();
|
|
} else {
|
|
entry = new TextMeasureEntry();
|
|
float extW, extH;
|
|
MeasureStringInternal(str, &extW, &extH);
|
|
entry->width = extW;
|
|
entry->height = extH;
|
|
// Hm, use the old calculation?
|
|
// int h = i == lines.size() - 1 ? entry->height : metrics.tmHeight + metrics.tmExternalLeading;
|
|
sizeCache_[key] = std::unique_ptr<TextMeasureEntry>(entry);
|
|
}
|
|
|
|
entry->lastUsedFrame = frameCount_;
|
|
*w = entry->width * fontScaleX_ * dpiScale_;
|
|
*h = entry->height * fontScaleY_ * dpiScale_;
|
|
}
|
|
|
|
void TextDrawer::MeasureStringRect(std::string_view str, float maxWidth, float *w, float *h, int align) {
|
|
const int wrap = align & (FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT);
|
|
|
|
// Clamp the size to something sane.
|
|
if (str.size() > MAX_TEXT_LENGTH) {
|
|
str = str.substr(0, MAX_TEXT_LENGTH);
|
|
}
|
|
|
|
float plainW, plainH;
|
|
MeasureString(str, &plainW, &plainH);
|
|
|
|
if (wrap && plainW > maxWidth) {
|
|
std::string toMeasure = std::string(str);
|
|
WrapString(toMeasure, toMeasure, maxWidth, wrap);
|
|
MeasureString(toMeasure, w, h);
|
|
} else {
|
|
*w = plainW;
|
|
*h = plainH;
|
|
}
|
|
}
|
|
|
|
void TextDrawer::DrawStringRect(DrawBuffer &target, std::string_view str, const Bounds &bounds, uint32_t color, int align) {
|
|
if (bounds.w < 0.0f || bounds.h < 0.0f) {
|
|
return;
|
|
}
|
|
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();
|
|
}
|
|
|
|
// Clamp the size to something sane.
|
|
if (str.size() > MAX_TEXT_LENGTH) {
|
|
str = str.substr(0, MAX_TEXT_LENGTH);
|
|
}
|
|
|
|
std::string toDraw(str);
|
|
int wrap = align & (FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT);
|
|
if (wrap) {
|
|
WrapString(toDraw, str, bounds.w, wrap);
|
|
}
|
|
DrawString(target, toDraw, x, y, color, align);
|
|
}
|
|
|
|
bool TextDrawer::DrawStringBitmapRect(std::vector<uint8_t> &bitmapData, TextStringEntry &entry, Draw::DataFormat texFormat, std::string_view str, const Bounds &bounds, int align, bool fullColor) {
|
|
// Clamp the size to something sane.
|
|
if (str.size() > MAX_TEXT_LENGTH) {
|
|
str = str.substr(0, MAX_TEXT_LENGTH);
|
|
}
|
|
|
|
std::string toDraw(str);
|
|
int wrap = align & (FLAG_WRAP_TEXT | FLAG_ELLIPSIZE_TEXT);
|
|
if (wrap) {
|
|
WrapString(toDraw, str, bounds.w, wrap);
|
|
}
|
|
return DrawStringBitmap(bitmapData, entry, texFormat, toDraw, align, fullColor);
|
|
}
|
|
|
|
void TextDrawer::ClearCache() {
|
|
for (auto &iter : cache_) {
|
|
if (iter.second->texture)
|
|
iter.second->texture->Release();
|
|
}
|
|
cache_.clear();
|
|
sizeCache_.clear();
|
|
fontStyle_ = {};
|
|
}
|
|
|
|
void TextDrawer::OncePerFrame() {
|
|
frameCount_++;
|
|
// If DPI changed (small-mode, future proper monitor DPI support), drop everything.
|
|
float newDpiScale = CalculateDPIScale();
|
|
if (newDpiScale != dpiScale_) {
|
|
INFO_LOG(Log::G3D, "DPI Scale changed (%f to %f) - wiping font cache (%d items)", dpiScale_, newDpiScale, (int)cache_.size());
|
|
dpiScale_ = newDpiScale;
|
|
ClearCache();
|
|
ClearFonts();
|
|
}
|
|
|
|
// Drop old strings. Use a prime number to reduce clashing with other rhythms
|
|
if (frameCount_ % 63 == 0) {
|
|
for (auto iter = cache_.begin(); iter != cache_.end();) {
|
|
if (frameCount_ - iter->second->lastUsedFrame > 100) {
|
|
if (iter->second->texture)
|
|
iter->second->texture->Release();
|
|
iter = cache_.erase(iter);
|
|
} else {
|
|
iter++;
|
|
}
|
|
}
|
|
|
|
for (auto iter = sizeCache_.begin(); iter != sizeCache_.end(); ) {
|
|
if (frameCount_ - iter->second->lastUsedFrame > 100) {
|
|
iter = sizeCache_.erase(iter);
|
|
} else {
|
|
iter++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
size_t TextDrawer::GetCacheDataSize() const {
|
|
size_t sz = 0;
|
|
for (const auto &iter : cache_) {
|
|
sz += iter.second->texture->DataSize();
|
|
}
|
|
return sz;
|
|
}
|
|
|
|
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)
|
|
// 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.
|
|
// Also, this seems to cause failures under Windows 7, so let's gate on Win 10 to be safe.
|
|
if (IsRenderDocLoaded() || !IsWin10OrHigher()) {
|
|
drawer = new TextDrawerWin32(draw);
|
|
} else {
|
|
drawer = new TextDrawerUWP(draw);
|
|
}
|
|
#elif PPSSPP_PLATFORM(MAC) || PPSSPP_PLATFORM(IOS)
|
|
drawer = new TextDrawerCocoa(draw);
|
|
#elif USE_SDL3_TTF
|
|
drawer = new TextDrawerSDL(draw);
|
|
#endif
|
|
if (drawer && !drawer->IsReady()) {
|
|
delete drawer;
|
|
drawer = nullptr;
|
|
}
|
|
return drawer;
|
|
}
|
|
|
|
struct FontDesc {
|
|
FontFamily family;
|
|
FontStyleFlags flags;
|
|
std::string_view fontName;
|
|
std::string_view filename;
|
|
};
|
|
|
|
// Append ".ttf" to get the actual filenames.
|
|
static const FontDesc g_fontDescs[] = {
|
|
{FontFamily::SansSerif, FontStyleFlags::Default, "Roboto Condensed", "Roboto_Condensed-Regular"},
|
|
{FontFamily::SansSerif, FontStyleFlags::Bold, "Roboto Condensed", "Roboto_Condensed-Bold"},
|
|
{FontFamily::SansSerif, FontStyleFlags::Italic, "Roboto Condensed", "Roboto_Condensed-Italic"},
|
|
{FontFamily::SansSerif, FontStyleFlags::Light, "Roboto Condensed", "Roboto_Condensed-Light"},
|
|
{FontFamily::Fixed, FontStyleFlags::Default, "Inconsolata", "Inconsolata-Regular"},
|
|
};
|
|
|
|
std::map<FontFamily, std::string> g_fontOverrides;
|
|
|
|
void SetFontNameOverride(FontFamily family, std::string_view overrideFont) {
|
|
g_fontOverrides[family] = std::string(overrideFont);
|
|
}
|
|
|
|
std::vector<std::string> GetAllFontFilenames() {
|
|
std::vector<std::string> vec;
|
|
for (const auto &desc : g_fontDescs) {
|
|
vec.emplace_back(desc.filename);
|
|
}
|
|
// uniquify idiom
|
|
std::sort(vec.begin(), vec.end());
|
|
vec.erase(std::unique(vec.begin(), vec.end()), vec.end());
|
|
return vec;
|
|
}
|
|
|
|
std::string GetFilenameForFontStyle(const FontStyle &font) {
|
|
for (const auto &desc : g_fontDescs) {
|
|
if (desc.flags == font.flags && desc.family == font.family) {
|
|
return std::string(desc.filename);
|
|
}
|
|
}
|
|
|
|
// If nothing matched, just return the first font of the requested family.
|
|
for (const auto &desc : g_fontDescs) {
|
|
if (desc.family == font.family) {
|
|
return std::string(desc.filename);
|
|
}
|
|
}
|
|
|
|
_dbg_assert_(false);
|
|
return "";
|
|
}
|
|
|
|
std::string GetFontNameForFontStyle(const FontStyle &font, FontStyleFlags *outFlags) {
|
|
auto override = g_fontOverrides.find(font.family);
|
|
if (override != g_fontOverrides.end()) {
|
|
*outFlags = font.flags;
|
|
return override->second;
|
|
}
|
|
|
|
for (const auto &desc : g_fontDescs) {
|
|
if (desc.flags == font.flags && desc.family == font.family) {
|
|
*outFlags = font.flags;
|
|
return std::string(desc.fontName);
|
|
}
|
|
}
|
|
// If nothing matched, just return the first font of the requested family.
|
|
for (const auto &desc : g_fontDescs) {
|
|
if (desc.family == font.family) {
|
|
// Return the flags of the font we found.
|
|
// TODO: Some platforms can actually synthesize traits (bold, italic) if missing.
|
|
*outFlags = desc.flags;
|
|
return std::string(desc.fontName);
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|