mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-01 20:18:15 +01:00
Measuring took the current font and scale, so UIContext switched to the style being measured and back. On first draw the font to switch back to could be an empty one, which the UWP drawer asserts on. MeasureString and MeasureStringRect now take the style and scale, and the backends' MeasureStringInternal measures in the style it's given. SetOrCreateFont, which sets the font drawing uses, is shared, and the backends only implement EnsureFont, which creates a font if needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
228 lines
7.9 KiB
C++
228 lines
7.9 KiB
C++
#include "ppsspp_config.h"
|
|
|
|
#include "Common/Log.h"
|
|
#include "Common/StringUtils.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_android.h"
|
|
|
|
#include "android/jni/app-android.h"
|
|
|
|
#if PPSSPP_PLATFORM(ANDROID) && !defined(__LIBRETRO__)
|
|
|
|
#include <jni.h>
|
|
|
|
// A new TextDrawer is created every time graphics are brought up, which on Android is every resume.
|
|
// The class stays valid for the life of the process, so hold one global ref to it, forever.
|
|
static jclass GetTextRendererClass(JNIEnv *env) {
|
|
static jclass cls;
|
|
if (!cls) {
|
|
const char *textRendererClassName = "org/ppsspp/ppsspp/TextRenderer";
|
|
jclass localClass = findClass(textRendererClassName);
|
|
if (localClass) {
|
|
cls = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
|
|
env->DeleteLocalRef(localClass);
|
|
} else {
|
|
env->ExceptionClear(); // ClassNotFoundException
|
|
ERROR_LOG(Log::G3D, "Failed to find class: '%s'", textRendererClassName);
|
|
}
|
|
}
|
|
return cls;
|
|
}
|
|
|
|
TextDrawerAndroid::TextDrawerAndroid(Draw::DrawContext *draw) : TextDrawer(draw) {
|
|
auto env = getEnv();
|
|
cls_textRenderer = GetTextRendererClass(env);
|
|
if (cls_textRenderer) {
|
|
method_allocFont = env->GetStaticMethodID(cls_textRenderer, "allocFont", "(Landroid/content/Context;Ljava/lang/String;)I");
|
|
method_freeAllFonts = env->GetStaticMethodID(cls_textRenderer, "freeAllFonts", "()V");
|
|
method_measureText = env->GetStaticMethodID(cls_textRenderer, "measureText", "(Ljava/lang/String;ID)I");
|
|
method_renderText = env->GetStaticMethodID(cls_textRenderer, "renderText", "(Ljava/lang/String;IDII)[I");
|
|
}
|
|
dpiScale_ = CalculateDPIScale();
|
|
|
|
INFO_LOG(Log::G3D, "Initializing TextDrawerAndroid with DPI scale %f", dpiScale_);
|
|
}
|
|
|
|
TextDrawerAndroid::~TextDrawerAndroid() {
|
|
// cls_textRenderer is shared and never released, see GetTextRendererClass.
|
|
ClearCache();
|
|
fontMap_.clear(); // size is precomputed using dpiScale_.
|
|
}
|
|
|
|
bool TextDrawerAndroid::IsReady() const {
|
|
return cls_textRenderer != nullptr && method_measureText != nullptr && method_renderText != nullptr;
|
|
}
|
|
|
|
void TextDrawerAndroid::EnsureFont(const FontStyle &style) {
|
|
// We will only use the default font but just for consistency let's still involve
|
|
// the font name.
|
|
if (fontMap_.find(style) != fontMap_.end()) {
|
|
return;
|
|
}
|
|
|
|
std::string filename = GetFilenameForFontStyle(style) + ".ttf";
|
|
// TextRenderer has no font -1, so it draws that with the default typeface.
|
|
int fontId = -1;
|
|
auto fontIter = allocatedFonts_.find(filename);
|
|
if (fontIter != allocatedFonts_.end()) {
|
|
fontId = fontIter->second;
|
|
} else {
|
|
auto env = getEnv();
|
|
JNILocalFrame frame(env);
|
|
jobject activity = Android_GetActivity(env);
|
|
if (activity) {
|
|
jstring jstr = env->NewStringUTF(filename.c_str());
|
|
fontId = env->CallStaticIntMethod(cls_textRenderer, method_allocFont, activity, jstr);
|
|
if (CheckJavaException(env, __FUNCTION__) || fontId < 0) {
|
|
ERROR_LOG(Log::G3D, "Failed to load font '%s', using the default", filename.c_str());
|
|
fontId = -1;
|
|
}
|
|
// Remembered even if it failed, that won't change.
|
|
allocatedFonts_[filename] = fontId;
|
|
} else {
|
|
// Not remembered, so it's tried again when the fonts are next recreated.
|
|
WARN_LOG(Log::G3D, "No activity to load font '%s' with, using the default", filename.c_str());
|
|
}
|
|
}
|
|
|
|
AndroidFontEntry entry{};
|
|
entry.font = fontId;
|
|
entry.size = 1.25f * style.sizePts / dpiScale_; // Not sure why this formula works.
|
|
|
|
// Just chose a factor that looks good, don't know what unit size is in anyway.
|
|
fontMap_[style] = entry;
|
|
}
|
|
|
|
void TextDrawerAndroid::MeasureStringInternal(const FontStyle &style, std::string_view str, float *w, float *h) {
|
|
auto iter = fontMap_.find(style);
|
|
if (iter == fontMap_.end()) {
|
|
ERROR_LOG(Log::G3D, "Missing font");
|
|
*w = 1.0f;
|
|
*h = 1.0f;
|
|
return;
|
|
}
|
|
|
|
std::string text;
|
|
ConvertUTF8ToJavaModifiedUTF8(&text, str);
|
|
|
|
auto env = getEnv();
|
|
jstring jstr = env->NewStringUTF(text.c_str());
|
|
uint32_t size = env->CallStaticIntMethod(cls_textRenderer, method_measureText, jstr, iter->second.font, iter->second.size);
|
|
env->DeleteLocalRef(jstr);
|
|
if (CheckJavaException(env, __FUNCTION__)) {
|
|
*w = 1.0f;
|
|
*h = 1.0f;
|
|
return;
|
|
}
|
|
|
|
*w = (float)(size >> 16);
|
|
*h = (float)(size & 0xFFFF);
|
|
}
|
|
|
|
bool TextDrawerAndroid::DrawStringBitmap(std::vector<uint8_t> &bitmapData, TextStringEntry &entry, Draw::DataFormat texFormat, std::string_view str, int align, bool fullColor) {
|
|
if (str.empty()) {
|
|
bitmapData.clear();
|
|
return false;
|
|
}
|
|
|
|
auto iter = fontMap_.find(fontStyle_);
|
|
if (iter == fontMap_.end()) {
|
|
ERROR_LOG(Log::G3D, "Missing font");
|
|
return false;
|
|
}
|
|
|
|
auto env = getEnv();
|
|
|
|
std::string text;
|
|
ConvertUTF8ToJavaModifiedUTF8(&text, str);
|
|
jstring jstr = env->NewStringUTF(text.c_str());
|
|
|
|
uint32_t textSize = env->CallStaticIntMethod(cls_textRenderer, method_measureText, jstr, iter->second.font, iter->second.size);
|
|
if (CheckJavaException(env, __FUNCTION__)) {
|
|
env->DeleteLocalRef(jstr);
|
|
return false;
|
|
}
|
|
int imageWidth = (short)(textSize >> 16);
|
|
int imageHeight = (short)(textSize & 0xFFFF);
|
|
if (imageWidth <= 0)
|
|
imageWidth = 1;
|
|
if (imageHeight <= 0)
|
|
imageHeight = 1;
|
|
// WARN_LOG(Log::G3D, "Text: '%.*s' (%02x)", (int)str.length(), str.data(), str[0]);
|
|
|
|
// TODO: Handle bold/italic
|
|
jintArray imageData = (jintArray)env->CallStaticObjectMethod(cls_textRenderer, method_renderText, jstr, iter->second.font, iter->second.size, imageWidth, imageHeight);
|
|
env->DeleteLocalRef(jstr);
|
|
// Most likely an OutOfMemoryError allocating the bitmap.
|
|
if (CheckJavaException(env, __FUNCTION__) || !imageData) {
|
|
ERROR_LOG(Log::G3D, "TextRenderer failed to render text");
|
|
return false;
|
|
}
|
|
if (env->GetArrayLength(imageData) != imageWidth * imageHeight) {
|
|
ERROR_LOG(Log::G3D, "TextRenderer returned bad image size");
|
|
env->DeleteLocalRef(imageData);
|
|
return false;
|
|
}
|
|
jint *jimage = env->GetIntArrayElements(imageData, nullptr);
|
|
if (!jimage) {
|
|
env->ExceptionClear(); // OutOfMemoryError
|
|
env->DeleteLocalRef(imageData);
|
|
return false;
|
|
}
|
|
|
|
entry.texture = nullptr;
|
|
entry.bmWidth = imageWidth;
|
|
entry.width = imageWidth;
|
|
entry.bmHeight = imageHeight;
|
|
entry.height = imageHeight;
|
|
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 y = 0; y < entry.bmHeight; y++) {
|
|
for (int x = 0; x < entry.bmWidth; x++) {
|
|
uint32_t v = jimage[imageWidth * y + x];
|
|
// Premultiplied alpha: Put alpha in all four channels.
|
|
bitmapData16[entry.bmWidth * y + x] = AlphaToPremul4444(v >> 24);
|
|
}
|
|
}
|
|
} else if (texFormat == Draw::DataFormat::R8_UNORM) {
|
|
// We only get here if swizzle is possible, so we can duplicate the value to all four channels.
|
|
bitmapData.resize(entry.bmWidth * entry.bmHeight);
|
|
for (int y = 0; y < entry.bmHeight; y++) {
|
|
for (int x = 0; x < entry.bmWidth; x++) {
|
|
uint32_t v = jimage[imageWidth * y + x];
|
|
bitmapData[entry.bmWidth * y + x] = (uint8_t)(v >> 24);
|
|
}
|
|
}
|
|
} else if (texFormat == Draw::DataFormat::R8G8B8A8_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++) {
|
|
uint32_t v = jimage[imageWidth * y + x];
|
|
// Swap R and B, for some reason.
|
|
v = (v & 0xFF00FF00) | ((v >> 16) & 0xFF) | ((v << 16) & 0xFF0000);
|
|
bitmapData32[entry.bmWidth * y + x] = RGBAToPremul8888(v);
|
|
}
|
|
}
|
|
} else {
|
|
_assert_msg_(false, "Bad TextDrawer format");
|
|
}
|
|
env->ReleaseIntArrayElements(imageData, jimage, JNI_ABORT);
|
|
env->DeleteLocalRef(imageData);
|
|
return true;
|
|
}
|
|
|
|
void TextDrawerAndroid::ClearFonts() {
|
|
fontMap_.clear(); // size is precomputed using dpiScale_.
|
|
}
|
|
|
|
#endif
|