diff --git a/CMakeLists.txt b/CMakeLists.txt index 6985fa0f12..4d79c4444a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -867,6 +867,8 @@ add_library(native STATIC ext/native/gfx_es2/draw_text_win.h ext/native/gfx_es2/draw_text_qt.cpp ext/native/gfx_es2/draw_text_qt.h + ext/native/gfx_es2/draw_text_android.cpp + ext/native/gfx_es2/draw_text_android.h ext/native/gfx_es2/gpu_features.cpp ext/native/gfx_es2/gpu_features.h ext/native/gfx_es2/glsl_program.cpp diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 64f2892074..24ba1eaf33 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -28,6 +28,7 @@ #include "thread/threadutil.h" #include "file/zip_read.h" #include "input/input_state.h" +#include "input/keycodes.h" #include "profiler/profiler.h" #include "math/math_util.h" #include "net/resolve.h" @@ -49,7 +50,7 @@ #include "app-android.h" -static JNIEnv *jniEnvUI; +JNIEnv *jniEnvUI; enum { ANDROID_VERSION_GINGERBREAD = 9, diff --git a/android/jni/app-android.h b/android/jni/app-android.h index bf842ac54a..a17be4b8b0 100644 --- a/android/jni/app-android.h +++ b/android/jni/app-android.h @@ -1,8 +1,11 @@ #pragma once -#include "input/keycodes.h" +#include "ppsspp_config.h" -// Compatability we alias the keycodes -// since native's keycodes are based on -// android keycodes. -typedef enum _keycode_t AndroidKeyCodes; +#if PPSSPP_PLATFORM(ANDROID) + +#include + +extern JNIEnv *jniEnvUI; + +#endif \ No newline at end of file diff --git a/android/src/org/ppsspp/ppsspp/TextRenderer.java b/android/src/org/ppsspp/ppsspp/TextRenderer.java new file mode 100644 index 0000000000..78873535c8 --- /dev/null +++ b/android/src/org/ppsspp/ppsspp/TextRenderer.java @@ -0,0 +1,48 @@ +package org.ppsspp.ppsspp; +import android.graphics.*; +import android.graphics.drawable.*; + +import java.nio.ByteBuffer; + +public class TextRenderer { + static int measureText(String string, float textSize) { + Paint p; + p = new Paint(Paint.ANTI_ALIAS_FLAG); + Rect bound = new Rect(); + p.setTextSize(textSize); + p.getTextBounds(string, 0, string.length(), bound); + int packedBounds = (bound.width() << 16) | bound.height(); + return packedBounds; + } + static short[] renderText(String string, float textSize) { + Paint p; + p = new Paint(Paint.ANTI_ALIAS_FLAG); + Rect bound = new Rect(); + p.setTextSize(textSize); + p.getTextBounds(string, 0, string.length(), bound); + Bitmap bmp = Bitmap.createBitmap(bound.width(), bound.height(), Bitmap.Config.ARGB_4444); + Canvas canvas = new Canvas(bmp); + p.setColor(Color.WHITE); + canvas.drawText(string, 0, 0, p); + + int bufSize = bmp.getRowBytes() * bmp.getHeight() * 2; // 2 = sizeof(ARGB_4444) + ByteBuffer buf = ByteBuffer.allocate(bufSize); + bmp.copyPixelsFromBuffer(buf); + byte[] bytes = buf.array(); + + // Output array size must match return value of measureText + short[] output = new short[bound.width() * bound.height()]; + + // 16-bit pixels but stored as bytes. + for (int y = 0; y < bound.height(); y++) { + int srcOffset = y * bmp.getRowBytes(); + int dstOffset = y * bound.width(); + for (int x = 0; x < bound.width(); x++) { + int val = bytes[srcOffset + x * 2]; + val = (val << 12) | 0xFFF; + output[dstOffset + x] = (short)val; + } + } + return output; + } +} diff --git a/ext/native/gfx_es2/draw_text.cpp b/ext/native/gfx_es2/draw_text.cpp index 1a49c2f0ab..9cde47ec47 100644 --- a/ext/native/gfx_es2/draw_text.cpp +++ b/ext/native/gfx_es2/draw_text.cpp @@ -8,6 +8,7 @@ #include "gfx_es2/draw_text.h" #include "gfx_es2/draw_text_win.h" #include "gfx_es2/draw_text_qt.h" +#include "gfx_es2/draw_text_android.h" TextDrawer::TextDrawer(Draw::DrawContext *draw) : draw_(draw) { // These probably shouldn't be state. @@ -45,6 +46,8 @@ TextDrawer *TextDrawer::Create(Draw::DrawContext *draw) { return new TextDrawerWin32(draw); #elif defined(USING_QT_UI) return new TextDrawerQt(draw); +#elif PPSSPP_PLATFORM(ANDROID) + return new TextDrawerAndroid(draw); #else return nullptr; #endif diff --git a/ext/native/gfx_es2/draw_text_android.cpp b/ext/native/gfx_es2/draw_text_android.cpp new file mode 100644 index 0000000000..5ab16a3583 --- /dev/null +++ b/ext/native/gfx_es2/draw_text_android.cpp @@ -0,0 +1,208 @@ +#include "base/display.h" +#include "base/logging.h" +#include "base/stringutil.h" +#include "thin3d/thin3d.h" +#include "util/hash/hash.h" +#include "util/text/wrap_text.h" +#include "util/text/utf8.h" +#include "gfx_es2/draw_text.h" +#include "gfx_es2/draw_text_android.h" + +#include "android/jni/app-android.h" + +#if PPSSPP_PLATFORM(ANDROID) + +#include + +TextDrawerAndroid::TextDrawerAndroid(Draw::DrawContext *draw) : TextDrawer(draw) { + env_ = jniEnvUI; + cls_textRenderer = env_->FindClass("org/ppsspp/ppsspp/TextRenderer"); + method_measureText = env_->GetStaticMethodID(cls_textRenderer, "measureText", "(ILjava/lang/String;F"); + method_renderText = env_->GetStaticMethodID(cls_textRenderer, "renderText", "([SLjava/lang/String;F"); + ILOG("method_measureText: %p", method_measureText); + ILOG("method_renderText: %p", method_renderText); + curSize_ = 12; +} + +TextDrawerAndroid::~TextDrawerAndroid() { + ClearCache(); +} + +uint32_t TextDrawerAndroid::SetFont(const char *fontName, int size, int flags) { + // We will only use the default font + uint32_t fontHash = 0; //hash::Fletcher((const uint8_t *)fontName, strlen(fontName)); + fontHash ^= size; + fontHash ^= flags << 10; + + auto iter = fontMap_.find(fontHash); + if (iter != fontMap_.end()) { + fontHash_ = fontHash; + return fontHash; + } + + curSize_ = size; + AndroidFontEntry entry; + entry.size = curSize_; + fontMap_[fontHash] = entry; + fontHash_ = fontHash; + return fontHash; +} + +void TextDrawerAndroid::SetFont(uint32_t fontHandle) { + uint32_t fontHash = fontHandle; + auto iter = fontMap_.find(fontHash); + if (iter != fontMap_.end()) { + curSize_ = iter->second.size; + } +} + +void TextDrawerAndroid::RecreateFonts() { + +} + +void TextDrawerAndroid::MeasureString(const char *str, size_t len, float *w, float *h) { + std::string stdstring(str, len); + jstring jstr = env_->NewStringUTF(stdstring.c_str()); + uint32_t size = env_->CallStaticIntMethod(cls_textRenderer, method_measureText, jstr, curSize_); + *w = (size >> 16) * fontScaleX_; + *h = (size & 0xFFFF) * fontScaleY_; + env_->DeleteLocalRef(jstr); +} + +void TextDrawerAndroid::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) { + bool rotated = (align & (ROTATE_90DEG_LEFT | ROTATE_90DEG_RIGHT)) != 0; + WrapString(toMeasure, toMeasure.c_str(), rotated ? bounds.h : bounds.w); + } + + jstring jstr = env_->NewStringUTF(toMeasure.c_str()); + uint32_t size = env_->CallStaticIntMethod(cls_textRenderer, method_measureText, jstr, curSize_); + *w = (size >> 16) * fontScaleX_; + *h = (size & 0xFFFF) * fontScaleY_; + env_->DeleteLocalRef(jstr); +} + +void TextDrawerAndroid::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::Fletcher((const uint8_t *)str, strlen(str)); + uint32_t entryHash = stringHash ^ fontHash_ ^ (align << 24); + + target.Flush(true); + + TextStringEntry *entry; + + auto iter = cache_.find(entryHash); + if (iter != cache_.end()) { + entry = iter->second.get(); + entry->lastUsedFrame = frameCount_; + draw_->BindTexture(0, entry->texture); + } else { + jstring jstr = env_->NewStringUTF(str); + uint32_t size = env_->CallStaticIntMethod(cls_textRenderer, method_measureText, jstr, curSize_); + int imageWidth = (size >> 16) * fontScaleX_; + int imageHeight = (size & 0xFFFF) * fontScaleY_; + jshortArray imageData = (jshortArray)env_->CallObjectMethod(cls_textRenderer, method_renderText, jstr, curSize_); + env_->DeleteLocalRef(jstr); + + entry = new TextStringEntry(); + entry->bmWidth = entry->width = imageWidth; + entry->bmHeight = entry->height = imageHeight; + entry->lastUsedFrame = frameCount_; + + TextureDesc desc{}; + desc.type = TextureType::LINEAR2D; + desc.format = Draw::DataFormat::B4G4R4A4_UNORM_PACK16; + desc.width = entry->bmWidth; + desc.height = entry->bmHeight; + desc.depth = 1; + desc.mipLevels = 1; + + uint16_t *bitmapData = new uint16_t[entry->bmWidth * entry->bmHeight]; + jshort* jimage = env_->GetShortArrayElements(imageData, nullptr); + for (int x = 0; x < entry->bmWidth; x++) { + for (int y = 0; y < entry->bmHeight; y++) { + bitmapData[entry->bmWidth * y + x] = jimage[imageWidth * y + x]; + } + } + env_->ReleaseShortArrayElements(imageData, jimage, 0); + desc.initData.push_back((uint8_t *)bitmapData); + entry->texture = draw_->CreateTexture(desc); + delete[] bitmapData; + cache_[entryHash] = std::unique_ptr(entry); + } + float w = entry->bmWidth * fontScaleX_; + float h = entry->bmHeight * fontScaleY_; + 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); +} + +void TextDrawerAndroid::ClearCache() { + for (auto &iter : cache_) { + if (iter.second->texture) + iter.second->texture->Release(); + } + cache_.clear(); + 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. + float newDpiScale = CalculateDPIScale(); + if (newDpiScale != dpiScale_) { + dpiScale_ = newDpiScale; + ClearCache(); + RecreateFonts(); + } + + // Drop old strings. Use a prime number to reduce clashing with other rhythms + if (frameCount_ % 23 == 0) { + for (auto iter = cache_.begin(); iter != cache_.end();) { + if (frameCount_ - iter->second->lastUsedFrame > 100) { + if (iter->second->texture) + iter->second->texture->Release(); + cache_.erase(iter++); + } else { + iter++; + } + } + + for (auto iter = sizeCache_.begin(); iter != sizeCache_.end(); ) { + if (frameCount_ - iter->second->lastUsedFrame > 100) { + sizeCache_.erase(iter++); + } else { + iter++; + } + } + } +} + +#endif diff --git a/ext/native/gfx_es2/draw_text_android.h b/ext/native/gfx_es2/draw_text_android.h new file mode 100644 index 0000000000..3a51b2f34e --- /dev/null +++ b/ext/native/gfx_es2/draw_text_android.h @@ -0,0 +1,50 @@ +#pragma once + +#include "ppsspp_config.h" + +#include +#include "gfx_es2/draw_text.h" + +#if PPSSPP_PLATFORM(ANDROID) + +#include + +struct AndroidFontEntry { + float size; +}; + +class TextDrawerAndroid : public TextDrawer { +public: + TextDrawerAndroid(Draw::DrawContext *draw); + ~TextDrawerAndroid(); + + uint32_t SetFont(const char *fontName, int size, int flags) override; + void SetFont(uint32_t fontHandle) override; // Shortcut once you've set the font once. + 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; + // Use for housekeeping like throwing out old strings. + void OncePerFrame() override; + +protected: + void ClearCache() override; + void RecreateFonts() override; // On DPI change + + // JNI functions + JNIEnv *env_; + jclass cls_textRenderer; + jmethodID method_measureText; + jmethodID method_renderText; + float curSize_; + + uint32_t fontHash_; + + std::map fontMap_; + + // The key is the CityHash of the string xor the fontHash_. + std::map> cache_; + std::map> sizeCache_; +}; + +#endif \ No newline at end of file diff --git a/ext/native/native.vcxproj b/ext/native/native.vcxproj index 12cd0e4b3d..34f5e2aeb8 100644 --- a/ext/native/native.vcxproj +++ b/ext/native/native.vcxproj @@ -237,6 +237,7 @@ + @@ -692,6 +693,7 @@ + diff --git a/ext/native/native.vcxproj.filters b/ext/native/native.vcxproj.filters index b1718027c9..ee8f08f807 100644 --- a/ext/native/native.vcxproj.filters +++ b/ext/native/native.vcxproj.filters @@ -320,6 +320,9 @@ gfx + + gfx + @@ -778,6 +781,9 @@ gfx + + gfx +