UI: Rounded rectangles from precomputed atlas corners

UIAtlas generates an antialiased white corner for each radius in
ROUND_RECT_RADII (8, 16, 24, 32 UI units) and each style (filled, and
thin/medium/thick outlines from ROUND_RECT_THICKNESS) at the atlas DPI.
DrawBuffer::DrawRoundRect mirrors the corner into all four corners and
stretches the corner's last texel row/column along the edges, so a
round rect is 9 quads (8 for outlines).

Other radii use the nearest corner, scaled. The LogoScreen temporarily
draws some for testing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-24 01:52:34 +02:00
1 parent 42bb9b134b
commit 110fb7fd6a
5 files changed
+182 -2

No files matched your search

+37 -1
View File
@@ -9,6 +9,7 @@
#include "Common/UI/Context.h"
#include "Common/Data/Format/PNGLoad.h"
#include "Common/Render/AtlasGen.h"
#include "Common/Render/DrawBuffer.h"
#include "Common/Render/ManagedTexture.h"
#include "Common/Common.h"
#include "Common/Thread/ParallelLoop.h"
@@ -40,6 +41,9 @@ Atlas *GetUIAtlas() {
struct ImageMeta {
std::string_view id;
bool addShadow = false;
// If set, the image is a generated round rect corner (see DrawBuffer::DrawRoundRect). In UI units.
float cornerRadius = 0.0f;
float cornerThickness = 0.0f; // 0 = filled
};
// We add shadows to all line-art images that are used for buttons, to improve visibility.
@@ -375,6 +379,28 @@ static bool RasterizeSVG(std::string_view filename, float dpiScale, int maxTextu
return true;
}
// White, premultiplied, antialiased by distance to the edge.
static void GenerateRoundRectCorner(Image &img, float radius, float thickness, float dpiScale) {
const float r = radius * dpiScale;
const float t = thickness * dpiScale;
const int center = RoundRectCornerCenter(r);
img.resize(center + 1, center + 1);
img.scale = dpiScale;
for (int y = 0; y <= center; y++) {
const float dy = std::max((float)center - (y + 0.5f), 0.0f);
for (int x = 0; x <= center; x++) {
const float dx = std::max((float)center - (x + 0.5f), 0.0f);
const float d = sqrtf(dx * dx + dy * dy);
float coverage = std::clamp(r - d + 0.5f, 0.0f, 1.0f);
if (t > 0.0f) {
coverage -= std::clamp(r - t - d + 0.5f, 0.0f, 1.0f);
}
const u32 a = (u32)(coverage * 255.0f + 0.5f);
img.set1(x, y, a * 0x01010101);
}
}
}
static bool GenerateUIAtlasImage(Atlas *atlas, float dpiScale, Image *dest, int maxTextureSize, const ImageMeta *imageIDs, size_t imageCount) {
Bucket bucket;
@@ -445,6 +471,8 @@ static bool GenerateUIAtlasImage(Atlas *atlas, float dpiScale, Image *dest, int
} else if (equals(imageIDs[i].id, "I_EMPTY")) {
img.resize(16, 16);
img.fill(0);
} else if (imageIDs[i].cornerRadius > 0.0f) {
GenerateRoundRectCorner(img, imageIDs[i].cornerRadius, imageIDs[i].cornerThickness, dpiScale);
} else {
std::string name = "ui_images/";
std::string pngName = PNGNameFromID(imageIDs[i].id);
@@ -491,6 +519,7 @@ static bool GenerateUIAtlasImage(Atlas *atlas, float dpiScale, Image *dest, int
atlas->images = new AtlasImage[genAtlasImages.size()];
std::copy(genAtlasImages.begin(), genAtlasImages.end(), atlas->images);
atlas->num_images = (int)genAtlasImages.size();
atlas->dpiScale = dpiScale;
// For debug, write out the atlas.
if (SAVE_DEBUG_ATLAS) {
@@ -510,8 +539,15 @@ Draw::Texture *GenerateUIAtlas(Draw::DrawContext *draw, Atlas *atlas, float dpiS
INFO_LOG(Log::G3D, "Regenerating atlas (empty: %s). Dpi scale (changed: %s): %0.2f (invalidate=%d)",
g_cachedUIAtlasImage.IsEmpty() ? "true" : "false", dpiScale != g_cachedDpiScale ? "true" : "false", dpiScale, invalidate);
std::vector<ImageMeta> imageIDs(std::begin(g_uiImageIDs), std::end(g_uiImageIDs));
for (int r = 0; r < ROUND_RECT_RADIUS_COUNT; r++) {
for (int style = 0; style < (int)RoundRectStyle::COUNT; style++) {
imageIDs.push_back({ RoundRectCornerImageName(r, (RoundRectStyle)style), false, ROUND_RECT_RADII[r], ROUND_RECT_THICKNESS[style] });
}
}
g_cachedUIAtlasImage.clear();
if (!GenerateUIAtlasImage(atlas, dpiScale, &g_cachedUIAtlasImage, draw->GetDeviceCaps().maxTextureSize, g_uiImageIDs, ARRAY_SIZE(g_uiImageIDs))) {
if (!GenerateUIAtlasImage(atlas, dpiScale, &g_cachedUIAtlasImage, draw->GetDeviceCaps().maxTextureSize, imageIDs.data(), imageIDs.size())) {
ERROR_LOG(Log::G3D, "Failed to generate UI atlas!");
return nullptr;
}