144 lines
6.4 KiB
C++
144 lines
6.4 KiB
C++
|
|
/*
|
|||
|
|
Crafter®.Graphics
|
|||
|
|
Copyright (C) 2026 Catcrafts®
|
|||
|
|
catcrafts.net
|
|||
|
|
|
|||
|
|
This library is free software; you can redistribute it and/or
|
|||
|
|
modify it under the terms of the GNU Lesser General Public
|
|||
|
|
License version 3.0 as published by the Free Software Foundation;
|
|||
|
|
|
|||
|
|
This library 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
|
|||
|
|
Lesser General Public License for more details.
|
|||
|
|
|
|||
|
|
You should have received a copy of the GNU Lesser General Public
|
|||
|
|
License along with this library; if not, write to the Free Software
|
|||
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
// Issue #57: Font::GetLineWidth used to call stbtt_GetCodepointHMetrics for
|
|||
|
|
// every glyph on every call. Caret hit-testing (InputField_HitTestCursor)
|
|||
|
|
// rescans the whole field once per character, so that is O(n²) HMetrics calls.
|
|||
|
|
// Advances are now memoised per Font in *font units* (Font::AdvanceUnits):
|
|||
|
|
// ASCII in a flat array, the rest in a map, rescaled per call.
|
|||
|
|
//
|
|||
|
|
// The cache must be in unscaled units, NOT pixels — the FontAtlas's
|
|||
|
|
// Glyph::advance is baked at kBaseSize and is wrong at any other size, so it
|
|||
|
|
// cannot be reused here. What is asserted:
|
|||
|
|
//
|
|||
|
|
// - AdvanceUnits is size-independent: the value for a codepoint is identical
|
|||
|
|
// no matter which size was queried first (proves it stores units, not px).
|
|||
|
|
// - GetLineWidth equals the documented pipeline: sum of per-glyph
|
|||
|
|
// (int)(units * scaleForSize), with the truncation applied PER GLYPH.
|
|||
|
|
// - Repeated calls (cache hits) are byte-identical to the first (miss).
|
|||
|
|
// - Width scales with size — a 2× size is wider, ruling out a baked-at-one-
|
|||
|
|
// size advance that would make every size render the same width.
|
|||
|
|
// - The empty string is zero.
|
|||
|
|
// - Codepoints above ASCII (the map path) behave like the array path.
|
|||
|
|
//
|
|||
|
|
// Pure CPU: Font only touches stb_truetype, so no Vulkan device is needed.
|
|||
|
|
|
|||
|
|
#include <cstdlib>
|
|||
|
|
|
|||
|
|
import Crafter.Graphics;
|
|||
|
|
import std;
|
|||
|
|
|
|||
|
|
using namespace Crafter;
|
|||
|
|
|
|||
|
|
namespace {
|
|||
|
|
|
|||
|
|
int failures = 0;
|
|||
|
|
|
|||
|
|
void Check(bool ok, std::string_view what) {
|
|||
|
|
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
|||
|
|
if (!ok) ++failures;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
std::filesystem::path FindFont() {
|
|||
|
|
for (const char* cand : {
|
|||
|
|
"font.ttf",
|
|||
|
|
"tests/FontAdvanceCache/font.ttf",
|
|||
|
|
"../../examples/HelloUI/font.ttf" }) {
|
|||
|
|
if (std::filesystem::exists(cand)) return cand;
|
|||
|
|
}
|
|||
|
|
return "font.ttf";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// The exact contract GetLineWidth must honour: truncate each glyph's scaled
|
|||
|
|
// advance to int independently, then accumulate. Rescaling the cached unit
|
|||
|
|
// advance per call is what keeps it correct at sizes other than kBaseSize.
|
|||
|
|
std::uint32_t ExpectedWidth(Font& font, std::string_view text, float size) {
|
|||
|
|
float scale = font.ScaleForSize(size);
|
|||
|
|
std::uint32_t w = 0;
|
|||
|
|
std::size_t i = 0;
|
|||
|
|
while (i < text.size()) {
|
|||
|
|
std::uint32_t cp = DecodeUtf8(text, i);
|
|||
|
|
if (cp == 0) break;
|
|||
|
|
w += static_cast<int>(font.AdvanceUnits(cp) * scale);
|
|||
|
|
}
|
|||
|
|
return w;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} // namespace
|
|||
|
|
|
|||
|
|
int main() {
|
|||
|
|
Font font(FindFont());
|
|||
|
|
|
|||
|
|
const std::string_view text = "Hello, World!";
|
|||
|
|
|
|||
|
|
// ── 1. AdvanceUnits is size-independent (stored in font units). ─────────
|
|||
|
|
// Prime the cache via a large size, capture the unit advance, then prime
|
|||
|
|
// again via a tiny size. The unit value must not move — if the cache stored
|
|||
|
|
// pixels it would differ wildly between the two priming sizes.
|
|||
|
|
font.GetLineWidth(text, 96.0f);
|
|||
|
|
std::int32_t aBig = font.AdvanceUnits(static_cast<std::uint32_t>('A'));
|
|||
|
|
font.GetLineWidth(text, 6.0f);
|
|||
|
|
std::int32_t aSmall = font.AdvanceUnits(static_cast<std::uint32_t>('A'));
|
|||
|
|
Check(aBig == aSmall && aBig > 0,
|
|||
|
|
"AdvanceUnits('A') is a stable, positive font-unit value across sizes");
|
|||
|
|
|
|||
|
|
// ── 2. GetLineWidth matches the per-glyph-truncated, rescaled pipeline. ─
|
|||
|
|
bool pipelineOk = true;
|
|||
|
|
for (float size : {6.0f, 11.0f, 18.0f, 32.0f, 64.0f, 100.0f}) {
|
|||
|
|
std::uint32_t got = font.GetLineWidth(text, size);
|
|||
|
|
std::uint32_t exp = ExpectedWidth(font, text, size);
|
|||
|
|
if (got != exp) {
|
|||
|
|
pipelineOk = false;
|
|||
|
|
std::println(" size {}: got {} expected {}", size, got, exp);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
Check(pipelineOk, "GetLineWidth == sum of per-glyph (int)(units * scale) at every size");
|
|||
|
|
|
|||
|
|
// ── 3. Cache hits are byte-identical to the first (miss) call. ──────────
|
|||
|
|
std::uint32_t first = font.GetLineWidth(text, 18.0f);
|
|||
|
|
bool stable = true;
|
|||
|
|
for (int k = 0; k < 8; ++k) stable = stable && (font.GetLineWidth(text, 18.0f) == first);
|
|||
|
|
Check(stable && first > 0, "repeated GetLineWidth calls are identical (cache hit == miss)");
|
|||
|
|
|
|||
|
|
// ── 4. Width scales with size — not baked at a single size. ─────────────
|
|||
|
|
std::uint32_t w18 = font.GetLineWidth(text, 18.0f);
|
|||
|
|
std::uint32_t w36 = font.GetLineWidth(text, 36.0f);
|
|||
|
|
Check(w36 > w18 && w36 > w18 + (w18 / 2),
|
|||
|
|
"doubling the size roughly doubles the width (advance is rescaled, not baked)");
|
|||
|
|
|
|||
|
|
// ── 5. Empty string is zero. ────────────────────────────────────────────
|
|||
|
|
Check(font.GetLineWidth("", 18.0f) == 0, "empty string has zero width");
|
|||
|
|
|
|||
|
|
// ── 6. Non-ASCII codepoints (the map path) behave like the array path. ──
|
|||
|
|
// U+00E9 'é' (UTF-8 0xC3 0xA9) is > 127 so it lands in advanceUnits_, not
|
|||
|
|
// the flat ASCII array. Its advance must be stable and a prefix narrower
|
|||
|
|
// than the whole.
|
|||
|
|
const std::string_view accented = "café"; // 'é' is two UTF-8 bytes
|
|||
|
|
std::uint32_t accWidth = font.GetLineWidth(accented, 24.0f);
|
|||
|
|
std::uint32_t accExp = ExpectedWidth(font, accented, 24.0f);
|
|||
|
|
Check(accWidth == accExp && accWidth > font.GetLineWidth("caf", 24.0f),
|
|||
|
|
"non-ASCII codepoint goes through the map path and widens the line");
|
|||
|
|
std::uint32_t eFirst = static_cast<std::uint32_t>(font.AdvanceUnits(0x00E9));
|
|||
|
|
Check(static_cast<std::uint32_t>(font.AdvanceUnits(0x00E9)) == eFirst,
|
|||
|
|
"AdvanceUnits for a mapped codepoint is stable across calls");
|
|||
|
|
|
|||
|
|
if (failures == 0) std::println("\nAll font advance-cache checks passed.");
|
|||
|
|
else std::println("\n{} font advance-cache check(s) FAILED.", failures);
|
|||
|
|
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
|||
|
|
}
|