diff --git a/Common/CMakeLists.txt b/Common/CMakeLists.txt index 36880b070a..9603342672 100644 --- a/Common/CMakeLists.txt +++ b/Common/CMakeLists.txt @@ -272,6 +272,8 @@ add_library(Common STATIC Profiler/Profiler.h Render/AtlasGen.cpp Render/AtlasGen.h + Render/DropShadow.cpp + Render/DropShadow.h Render/TextureAtlas.cpp Render/TextureAtlas.h Render/DrawBuffer.cpp diff --git a/Common/Common.vcxproj b/Common/Common.vcxproj index 4db96c3d5d..4169ccbafc 100644 --- a/Common/Common.vcxproj +++ b/Common/Common.vcxproj @@ -495,6 +495,7 @@ + @@ -1019,6 +1020,7 @@ + diff --git a/Common/Common.vcxproj.filters b/Common/Common.vcxproj.filters index c5a2494a00..8780958f1d 100644 --- a/Common/Common.vcxproj.filters +++ b/Common/Common.vcxproj.filters @@ -698,6 +698,9 @@ Render + + Render + ext\nanosvg @@ -1417,6 +1420,9 @@ Render + + Render + UI diff --git a/Common/Render/AtlasGen.cpp b/Common/Render/AtlasGen.cpp index 9ebae1f570..0c805716b5 100644 --- a/Common/Render/AtlasGen.cpp +++ b/Common/Render/AtlasGen.cpp @@ -23,6 +23,7 @@ #include "Common/File/FileUtil.h" #include "Common/File/VFS/VFS.h" #include "Common/Render/AtlasGen.h" +#include "Common/Render/DropShadow.h" typedef unsigned short u16; @@ -227,78 +228,6 @@ AtlasImage ToAtlasImage(int id, std::string_view name, float tw, float th, const return img; } -// The below is ChatGPT-generated drop shadow code. Needs optimization! - -static std::vector makeGaussianKernel(int radius) { - const float sigma = radius / 2.0f; - std::vector kernel(2 * radius + 1); - float sum = 0.0f; - for (int i = -radius; i <= radius; i++) { - float val = std::exp(-(i * i) / (2 * sigma * sigma)); - kernel[i + radius] = val; - sum += val; - } - sum = 1.0f / sum; - for (float &v : kernel) - v *= sum; - return kernel; -} - -static void blurAlpha(const std::vector &src, std::vector &dst, int w, int h, int radius) { - auto kernel = makeGaussianKernel(radius); - int ksize = (int)kernel.size(); - int kr = radius; - - std::vector tmp(w * h, 0.0f); - - // horizontal - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - float sum = 0.0f; - for (int k = -kr; k <= kr; k++) { - int xx = std::clamp(x + k, 0, w - 1); - sum += src[y * w + xx] * kernel[k + kr]; - } - tmp[y * w + x] = sum; - } - } - - // vertical - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - float sum = 0.0f; - for (int k = -kr; k <= kr; k++) { - int yy = std::clamp(y + k, 0, h - 1); - sum += tmp[yy * w + x] * kernel[k + kr]; - } - dst[y * w + x] = sum; - } - } -} - -static inline uint32_t Over_ABGR(uint32_t front, uint32_t back) { - const uint32_t fr = front & 0xFFu; - const uint32_t fg = (front >> 8) & 0xFFu; // green - const uint32_t fb = (front >> 16) & 0xFFu; - const uint32_t fa = (front >> 24) & 0xFFu; - - const uint32_t br = back & 0xFFu; - const uint32_t bg = (back >> 8) & 0xFFu; // green - const uint32_t bb = (back >> 16) & 0xFFu; - const uint32_t ba = (back >> 24) & 0xFFu; - - const uint32_t invA = 255u - fa; - - // multiply then divide by 255 with rounding equivalent to (x*invA + 127)/255 - uint32_t outr = fr + (uint32_t)((br * invA + 127u) / 255u); - uint32_t outg = fg + (uint32_t)((bg * invA + 127u) / 255u); - uint32_t outb = fb + (uint32_t)((bb * invA + 127u) / 255u); - uint32_t outa = fa + (uint32_t)((ba * invA + 127u) / 255u); - - // pack back to ABGR - return (outa << 24) | (outb << 16) | (outg << 8) | outr; -} - void Add1PxTransparentBorder(Image &img) { std::vector newData((img.w + 2) * (img.h + 2), 0); for (int y = 0; y < img.h; y++) { @@ -313,51 +242,13 @@ void Add1PxTransparentBorder(Image &img) { } void AddDropShadow(Image &img, int shadowSize, float intensity) { - int radius = std::max(1, (int)(shadowSize * img.scale)); - - // Expand canvas so blur has space on all sides - int newW = img.w + radius * 2; - int newH = img.h + radius * 2; - - // Expanded alpha buffer - std::vector alpha(newW * newH, 0.0f); - for (int y = 0; y < img.h; y++) { - for (int x = 0; x < img.w; x++) { - float a = ((img.dat[y * img.w + x] >> 24) & 0xFF) * (1.0f / 255.0f); - alpha[(y + radius) * newW + (x + radius)] = a; - } - } - - // Blur the expanded alpha - std::vector blurred(newW * newH, 0.0f); - blurAlpha(alpha, blurred, newW, newH, radius); - - // Target buffer with transparent background - std::vector newData(newW * newH, 0); - - // Draw the computed shadow first (black, blurred alpha - automatically premultiplied). - for (int y = 0; y < newH; y++) { - for (int x = 0; x < newW; x++) { - float a = blurred[y * newW + x]; - if (a > 0.001f) { - newData[y * newW + x] = ((u32)(a * 255 * intensity) << 24); - } - } - } - - // Composite original image on top (centered in expanded buffer) - for (int y = 0; y < img.h; y++) { - for (int x = 0; x < img.w; x++) { - u32 c = img.dat[y * img.w + x]; - if ((c >> 24) & 0xFF) { - int nx = x + radius; - int ny = y + radius; - newData[ny * newW + nx] = Over_ABGR(c, newData[ny * newW + nx]); - } - } - } - - img.w = newW; - img.h = newH; + DropShadowParams params; + params.radius = std::max(1, (int)(shadowSize * img.scale)); + params.intensity = intensity; + const int pad = params.Padding(); + std::vector newData((img.w + pad * 2) * (img.h + pad * 2)); + CompositeDropShadow(img.dat.data(), img.w, img.h, newData.data(), img.w + pad * 2, params); + img.w += pad * 2; + img.h += pad * 2; img.dat = std::move(newData); } diff --git a/Common/Render/DropShadow.cpp b/Common/Render/DropShadow.cpp new file mode 100644 index 0000000000..51858d335e --- /dev/null +++ b/Common/Render/DropShadow.cpp @@ -0,0 +1,79 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#include +#include + +#include "Common/Render/DropShadow.h" + +void CompositeDropShadow(const u32 *src, int w, int h, u32 *dst, int dstStride, const DropShadowParams ¶ms) { + const int radius = std::max(1, params.radius); + const int pad = params.Padding(); + const int outW = w + pad * 2; + const int outH = h + pad * 2; + + const float sigma = radius * 0.5f; + std::vector kernel(radius * 2 + 1); + float kernelSum = 0.0f; + for (int i = -radius; i <= radius; i++) { + kernel[i + radius] = expf(-(float)(i * i) / (2.0f * sigma * sigma)); + kernelSum += kernel[i + radius]; + } + for (float &k : kernel) { + k /= kernelSum; + } + + // Blur the offset alpha, scattering each texel horizontally into rows, then the rows vertically. + // Scattering lets us skip the transparent texels, which are most of them. + std::vector rows(outW * h); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + const u32 a = src[y * w + x] >> 24; + if (!a) { + continue; + } + const float fa = a * (1.0f / 255.0f); + float *row = &rows[y * outW + x + pad + params.offsetX - radius]; + for (int i = 0; i <= radius * 2; i++) { + row[i] += fa * kernel[i]; + } + } + } + std::vector shadow(outW * outH); + for (int y = 0; y < h; y++) { + const float *row = &rows[y * outW]; + for (int i = 0; i <= radius * 2; i++) { + float *out = &shadow[(y + pad + params.offsetY - radius + i) * outW]; + const float k = kernel[i]; + for (int x = 0; x < outW; x++) { + out[x] += row[x] * k; + } + } + } + + for (int y = 0; y < outH; y++) { + for (int x = 0; x < outW; x++) { + const int sx = x - pad; + const int sy = y - pad; + const u32 color = (sx >= 0 && sx < w && sy >= 0 && sy < h) ? src[sy * w + sx] : 0; + const u32 alpha = color >> 24; + const float shadowAlpha = std::min(1.0f, shadow[y * outW + x] * params.intensity); + const u32 outAlpha = alpha + (u32)((255 - alpha) * shadowAlpha + 0.5f); + dst[y * dstStride + x] = (outAlpha << 24) | (color & 0x00FFFFFF); + } + } +} diff --git a/Common/Render/DropShadow.h b/Common/Render/DropShadow.h new file mode 100644 index 0000000000..a163a1c655 --- /dev/null +++ b/Common/Render/DropShadow.h @@ -0,0 +1,44 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#pragma once + +#include +#include + +#include "Common/CommonTypes.h" + +// A soft black shadow, shared by the UI atlas images and the text drawer. +struct DropShadowParams { + // Blur kernel half-width in pixels. The gaussian's sigma is half of it. + int radius = 1; + // Shadow offset in pixels, positive is right/down. + int offsetX = 0; + int offsetY = 0; + // Multiplies the shadow's alpha, which is then clamped to 1. + float intensity = 1.0f; + + // Room the shadow needs on each side of the image. + int Padding() const { + return radius + std::max(abs(offsetX), abs(offsetY)); + } +}; + +// src is premultiplied RGBA with alpha in the top byte. Writes it composited over its shadow to dst, +// (w + 2 * padding) x (h + 2 * padding) pixels with the image centered. Since the shadow is black, +// only alpha changes under the image. +void CompositeDropShadow(const u32 *src, int w, int h, u32 *dst, int dstStride, const DropShadowParams ¶ms); diff --git a/Common/Render/Text/draw_text.cpp b/Common/Render/Text/draw_text.cpp index be058abae2..3d6f9c0ad3 100644 --- a/Common/Render/Text/draw_text.cpp +++ b/Common/Render/Text/draw_text.cpp @@ -17,6 +17,7 @@ #include "Common/Data/Text/WrapText.h" #include "Common/Data/Encoding/Utf8.h" +#include "Common/Render/DropShadow.h" #include "Common/Render/Text/draw_text.h" #include "Common/Render/Text/draw_text_win.h" #include "Common/Render/Text/draw_text_cocoa.h" @@ -45,9 +46,9 @@ static bool IsRenderDocLoaded() { #endif } -// Drop shadow blur (sigma) and offset as fractions of the font size, which is in UI units. The bitmap +// Drop shadow blur radius and offset as fractions of the font size, which is in UI units. The bitmap // has 1/dpiScale_ pixels per UI unit, so at higher DPI the blur spans more pixels. -static constexpr float SHADOW_BLUR = 1.0f / 14.0f; +static constexpr float SHADOW_BLUR = 1.0f / 7.0f; static constexpr float SHADOW_OFFSET = 1.0f / 16.0f; // Blurring thin strokes spreads them thin, so boost the shadow's alpha a bit (clamped). static constexpr float SHADOW_STRENGTH = 1.5f; @@ -273,85 +274,31 @@ bool TextDrawer::DrawStringBitmapRect(std::vector &bitmapData, TextStri } void TextDrawer::AddDropShadow(std::vector &bitmapData, TextStringEntry &entry, Draw::DataFormat srcFormat) const { - const bool srcColor = srcFormat == Draw::DataFormat::R8G8B8A8_UNORM; const float pixelsPerUnit = 1.0f / dpiScale_; - const float sigma = std::max(0.5f, fontStyle_.sizePts * SHADOW_BLUR * pixelsPerUnit); - const int radius = (int)ceilf(sigma * 2.5f); - const int offset = std::max(1, (int)roundf(fontStyle_.sizePts * SHADOW_OFFSET * pixelsPerUnit)); - // Same padding on all sides, so the text stays centered in the bitmap. - const int pad = radius + offset; + DropShadowParams params; + params.radius = std::max(1, (int)roundf(fontStyle_.sizePts * SHADOW_BLUR * pixelsPerUnit)); + params.offsetX = params.offsetY = std::max(1, (int)roundf(fontStyle_.sizePts * SHADOW_OFFSET * pixelsPerUnit)); + params.intensity = SHADOW_STRENGTH; + const int pad = params.Padding(); const int srcW = entry.bmWidth; const int srcH = entry.bmHeight; - const int w = srcW + pad * 2; - const int h = srcH + pad * 2; - - std::vector kernel(radius * 2 + 1); - float kernelSum = 0.0f; - for (int i = -radius; i <= radius; i++) { - kernel[i + radius] = expf(-(float)(i * i) / (2.0f * sigma * sigma)); - kernelSum += kernel[i + radius]; - } - for (float &k : kernel) { - k /= kernelSum; - } - - auto srcAlpha = [&](int x, int y) -> uint8_t { - return srcColor ? bitmapData[(y * srcW + x) * 4 + 3] : bitmapData[y * srcW + x]; - }; - - // Blur the offset coverage, scattering each texel horizontally into rows, then the rows vertically. - std::vector rows(w * srcH); - for (int y = 0; y < srcH; y++) { - for (int x = 0; x < srcW; x++) { - const uint8_t a = srcAlpha(x, y); - if (!a) { - continue; - } - const float fa = a * (1.0f / 255.0f); - float *dst = &rows[y * w + x + pad + offset - radius]; - for (int i = 0; i <= radius * 2; i++) { - dst[i] += fa * kernel[i]; - } - } - } - std::vector shadow(w * h); - for (int y = 0; y < srcH; y++) { - const float *src = &rows[y * w]; - for (int i = 0; i <= radius * 2; i++) { - float *dst = &shadow[(y + pad + offset - radius + i) * w]; - const float k = kernel[i]; - for (int x = 0; x < w; x++) { - dst[x] += src[x] * k; - } + std::vector src; + if (srcFormat == Draw::DataFormat::R8G8B8A8_UNORM) { + src.resize(srcW * srcH); + memcpy(src.data(), bitmapData.data(), src.size() * sizeof(u32)); + } else { + src.reserve(srcW * srcH); + for (uint8_t a : bitmapData) { + src.push_back(AlphaToPremul8888(a)); } } - // Composite the (premultiplied) text over the black shadow. Only alpha changes, since the shadow adds no color. - const int bmWidth = (w + 3) & ~3; - const int bmHeight = (h + 3) & ~3; - std::vector out(bmWidth * bmHeight * sizeof(uint32_t)); - uint32_t *out32 = (uint32_t *)out.data(); - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - const int sx = x - pad; - const int sy = y - pad; - uint32_t textColor = 0; - if (sx >= 0 && sx < srcW && sy >= 0 && sy < srcH) { - if (srcColor) { - memcpy(&textColor, &bitmapData[(sy * srcW + sx) * 4], sizeof(textColor)); - } else { - textColor = AlphaToPremul8888(bitmapData[sy * srcW + sx]); - } - } - const uint32_t textAlpha = textColor >> 24; - const float shadowAlpha = std::min(1.0f, shadow[y * w + x] * SHADOW_STRENGTH); - const uint32_t alpha = textAlpha + (uint32_t)((255 - textAlpha) * shadowAlpha + 0.5f); - out32[y * bmWidth + x] = (alpha << 24) | (textColor & 0x00FFFFFF); - } - } + const int bmWidth = (srcW + pad * 2 + 3) & ~3; + const int bmHeight = (srcH + pad * 2 + 3) & ~3; + bitmapData.assign(bmWidth * bmHeight * sizeof(u32), 0); + CompositeDropShadow(src.data(), srcW, srcH, (u32 *)bitmapData.data(), bmWidth, params); - bitmapData = std::move(out); entry.bmWidth = bmWidth; entry.bmHeight = bmHeight; entry.shadowPad = pad; diff --git a/UWP/CommonUWP/CommonUWP.vcxproj b/UWP/CommonUWP/CommonUWP.vcxproj index 8781f9db62..65d8a97e43 100644 --- a/UWP/CommonUWP/CommonUWP.vcxproj +++ b/UWP/CommonUWP/CommonUWP.vcxproj @@ -163,6 +163,7 @@ + @@ -365,6 +366,7 @@ + diff --git a/UWP/CommonUWP/CommonUWP.vcxproj.filters b/UWP/CommonUWP/CommonUWP.vcxproj.filters index ad89d9bedb..2cb356e71c 100644 --- a/UWP/CommonUWP/CommonUWP.vcxproj.filters +++ b/UWP/CommonUWP/CommonUWP.vcxproj.filters @@ -524,6 +524,9 @@ Render + + Render + UI @@ -1077,6 +1080,9 @@ Render + + Render + UI diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 2089a1032d..ca7af5104b 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -422,6 +422,7 @@ EXEC_AND_LIB_FILES := \ $(SRC)/Common/Render/ManagedTexture.cpp \ $(SRC)/Common/Render/DrawBuffer.cpp \ $(SRC)/Common/Render/AtlasGen.cpp \ + $(SRC)/Common/Render/DropShadow.cpp \ $(SRC)/Common/Render/TextureAtlas.cpp \ $(SRC)/Common/Render/Text/draw_text.cpp \ $(SRC)/Common/Render/Text/draw_text_android.cpp \ diff --git a/libretro/Makefile.common b/libretro/Makefile.common index 5fe85b74f1..9b9e86a226 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -511,6 +511,7 @@ SOURCES_CXX += \ $(COMMONDIR)/Net/WebsocketServer.cpp \ $(COMMONDIR)/Render/ManagedTexture.cpp \ $(COMMONDIR)/Render/DrawBuffer.cpp \ + $(COMMONDIR)/Render/DropShadow.cpp \ $(COMMONDIR)/Render/TextureAtlas.cpp \ $(COMMONDIR)/Serialize/Serializer.cpp \ $(COMMONDIR)/Thread/ThreadUtil.cpp \ diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index a5e8590b61..678d2d9163 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -128,6 +128,7 @@ #include "Common/File/AndroidContentURI.h" #include "Common/System/Request.h" +#include "Common/Render/AtlasGen.h" #include "Common/Render/Text/draw_text.h" #include "Common/Net/Resolve.h" #include "Common/Net/SocketCompat.h" @@ -3707,6 +3708,32 @@ bool TestTextDrawerCache() { return true; } +// The atlas shadow has no offset, so it pads the image evenly and stays symmetric around it. +bool TestAtlasDropShadow() { + Image img; + img.scale = 2.0f; + img.resize(8, 8); + for (int y = 2; y < 6; y++) { + for (int x = 2; x < 6; x++) { + img.set1(x, y, 0xFFFFFFFF); + } + } + AddDropShadow(img, 3, 0.66f); + EXPECT_EQ_INT(img.w, 8 + 12); + EXPECT_EQ_INT(img.h, 8 + 12); + EXPECT_EQ_HEX(img.get1(6 + 3, 6 + 3), 0xFFFFFFFF); + EXPECT_EQ_HEX(img.get1(0, 0), 0); + for (int y = 0; y < img.h; y++) { + for (int x = 0; x < img.w; x++) { + EXPECT_EQ_HEX(img.get1(x, y), img.get1(img.w - 1 - x, img.h - 1 - y)); + } + } + const u32 edge = img.get1(5, 10); + EXPECT_EQ_INT(edge & 0xFFFFFF, 0); + EXPECT_TRUE((edge >> 24) > 0 && (edge >> 24) < 0xA8); + return true; +} + // Tab/Shift+Tab focus navigation walks the view hierarchy in declaration order rather than by // geometry, so what it does is entirely determined by CollectTabOrder - which is worth pinning // down, since the interesting cases (nesting, hidden tabs, disabled items) are all structural. @@ -4020,6 +4047,7 @@ TestItem availableTests[] = { TEST_ITEM(UITabOrder), TEST_ITEM(UIFocusNavigation), TEST_ITEM(TextDrawerCache), + TEST_ITEM(AtlasDropShadow), TEST_ITEM(FatShortNames), TEST_ITEM(GEDumpGameID), TEST_ITEM(QueueBuf),