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),