From 27ca0f80fc8eac3237338aa4384d602ce03fc2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 3 Sep 2026 12:48:18 -0600 Subject: [PATCH] sceFont: let glyphs draw past bytesPerLine, like the hardware does SetFontPixel refused to write any pixel whose x fell outside bytesPerLine, so a glyph drawn into a buffer with a bytesPerLine narrower than its rows came out mostly blank. The hardware doesn't bound it that way - it works out an address and writes, so the rows overlap and the glyph smears across them. The declared bufWidth and bufHeight, plus the address check, are what keep it in bounds. Cache invalidation now covers the wider of bytesPerLine * bufHeight and where the last row actually ends, since those are no longer the same thing when the rows overlap. Fixes font/charglyphimage and font/charglyphimageclip, moved from tests_next to tests_good. The other font tests are unaffected, so whatever ails them is something else. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq --- Core/Font/PGF.cpp | 30 +++++++++++++++++++++++------- test.py | 4 ++-- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/Core/Font/PGF.cpp b/Core/Font/PGF.cpp index 26a577459d..4c31ded955 100644 --- a/Core/Font/PGF.cpp +++ b/Core/Font/PGF.cpp @@ -596,6 +596,23 @@ bool PGF::GetCharGlyph(int charCode, int glyphType, Glyph &glyph) const { return true; } +static const u8 fontPixelSizeInBytes[] = { 0, 0, 1, 3, 4 }; // 0 means 2 pixels per byte + +// How far into the glyph buffer a draw can reach. Usually bytesPerLine covers a whole row and this +// is just bytesPerLine * bufHeight, but nothing makes a game set it that way - with a smaller +// bytesPerLine the rows overlap and the last one runs past that, so the cache invalidation has to +// cover the wider of the two. +static u32 GlyphBufferExtent(const GlyphImage *image) { + const u32 rows = (u32)image->bytesPerLine * image->bufHeight; + if (image->pixelFormat < 0 || image->pixelFormat > PSP_FONT_PIXELFORMAT_32 || image->bufHeight == 0) { + return rows; + } + const int pixelBytes = fontPixelSizeInBytes[image->pixelFormat]; + const u32 rowBytes = pixelBytes == 0 ? ((u32)image->bufWidth + 1) / 2 : (u32)image->bufWidth * pixelBytes; + const u32 lastRow = (u32)(image->bufHeight - 1) * image->bytesPerLine + rowBytes; + return std::max(rows, lastRow); +} + void PGF::DrawCharacter(const GlyphImage *image, int clipX, int clipY, int clipWidth, int clipHeight, int charCode, int altCharCode, int glyphType) const { Glyph glyph; if (!GetCharGlyph(charCode, glyphType, glyph)) { @@ -734,7 +751,7 @@ void PGF::DrawCharacter(const GlyphImage *image, int clipX, int clipY, int clipW } } - gpu->InvalidateCache(image->bufferPtr, image->bytesPerLine * image->bufHeight, GPU_INVALIDATE_SAFE); + gpu->InvalidateCache(image->bufferPtr, GlyphBufferExtent(image), GPU_INVALIDATE_SAFE); } // pixelColor arrives already scaled to `pixelformat`'s range, and is *added* to what is in the buffer with saturation; it does not replace it. @@ -746,17 +763,16 @@ void PGF::SetFontPixel(u32 base, int bpl, int bufWidth, int bufHeight, int x, in return; } - static const u8 fontPixelSizeInBytes[] = { 0, 0, 1, 3, 4 }; // 0 means 2 pixels per byte if (pixelformat < 0 || pixelformat > PSP_FONT_PIXELFORMAT_32) { ERROR_LOG_REPORT_ONCE(pfgbadformat, Log::sceFont, "Invalid image format in image: %d", (int)pixelformat); return; } - int pixelBytes = fontPixelSizeInBytes[pixelformat]; - int bufMaxWidth = (pixelBytes == 0 ? bpl * 2 : bpl / pixelBytes); - if (x >= bufMaxWidth) { - return; - } + const int pixelBytes = fontPixelSizeInBytes[pixelformat]; + // Deliberately no check that x fits within bytesPerLine. The hardware just works out an + // address and writes, so with a bytesPerLine smaller than the row needs, rows overlap and the + // glyph smears across them - see the "Linesize = 1" case in pspautotests font/charglyphimage. + // The bufWidth/bufHeight rectangle above and the address check below are what keep this sane. int framebufferAddr = base + (y * bpl) + (pixelBytes == 0 ? x / 2 : x * pixelBytes); if (!Memory::IsValidAddress(framebufferAddr)) { return; diff --git a/test.py b/test.py index 8029de327c..4af839e44c 100755 --- a/test.py +++ b/test.py @@ -138,6 +138,8 @@ tests_good = [ "display/setmode", "dmac/dmactest", "font/altcharcode", + "font/charglyphimage", + "font/charglyphimageclip", "font/charimagerect", "font/find", "font/fontinfo", @@ -409,8 +411,6 @@ tests_next = [ "ccc/convertstring", "display/hcount", "font/fonttest", - "font/charglyphimage", - "font/charglyphimageclip", "font/charinfo", "font/newlib", "font/open",