Fix garbled glyphs: bounce buffer for AndroidBitmap_lockPixels

Shim_AndroidBitmap_lockPixels handed the guest a NULL pixel pointer
whenever the real buffer lived outside the guest region, on the
documented assumption that "texture decode FROM a Bitmap ... reads
pixels via other means, not by writing through this specific pointer".

Measurement disproved that assumption. The glyph atlas path is exactly
a read through this pointer:

  AndroidBitmap_lockPixels rc=0 hostAddr=0x... -> guest=0x0
  bitmap 1024x1024 stride=4096 format=1   (RGBA_8888)

1024x1024 is precisely the atlas size independently derived from the
glyph UVs (u1 = 35/1024 for a 35px glyph), which is why glyph
positions, sizes, pen advance and UVs all measured correct while the
pixels were coloured noise: BitmapGraphics rendered the glyphs fine,
the guest asked for them, got nothing, and uploaded guest address 0
as the font texture.

Fixed with a bounce buffer - real pixels copied into a guest-visible
buffer on lock, copied back and freed on unlock. The copy-back also
closes the write direction the original comment called a known gap.
Costs one height*stride copy each way, a handful of times per session.

Verified in both directions on the Pixel 6a: text renders correctly,
and the added "no bounce buffer was made" warning fired zero times,
so every lock genuinely got a buffer rather than silently falling
back to the old NULL path.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-19 13:59:53 +03:00
co-authored by Claude
parent 16f1125951
commit f8744f0392
+84 -10
View File
@@ -183,26 +183,100 @@ uint32_t Shim_AndroidBitmap_getInfo(GuestEngine& eng, uint32_t, uint32_t r1, uin
if (r2) memcpy(eng.G2H(r2), &info, sizeof(AndroidBitmapInfo));
return (uint32_t)rc;
}
// FIX (2026-09-19, task #41 - garbled glyphs). The version below used to
// hand the guest a NULL pixel pointer whenever the real buffer lived outside
// the guest region, on the documented assumption that "texture decode FROM a
// Bitmap ... reads pixels via other means, not by writing through this
// specific pointer". Measurement disproved that assumption: the glyph atlas
// path is exactly a read through this pointer. Live log -
// AndroidBitmap_lockPixels rc=0 hostAddr=0x... -> guest=0x0
// bitmap 1024x1024 stride=4096 format=1 (RGBA_8888)
// - 1024x1024 being precisely the atlas size independently derived from the
// glyph UVs (u1 = 35/1024 for a 35px glyph). So BitmapGraphics rendered the
// glyphs correctly, the guest asked for the pixels, got nothing, and
// uploaded whatever was at guest address 0 as the font texture. That is the
// coloured noise on screen.
//
// Fixed with a bounce buffer: the real pixels are copied into a guest-visible
// buffer on lock, and copied back on unlock (so a guest that WRITES pixels -
// the case the original comment worried about - works too). Costs one
// height*stride copy each way, but this fires a handful of times per session,
// not per frame.
namespace {
struct BouncedBitmap {
GuestAddr guestBuf = 0;
uint32_t bytes = 0;
void* hostPixels = nullptr;
};
std::mutex g_bouncedBitmapsMutex;
std::unordered_map<uint32_t, BouncedBitmap> g_bouncedBitmaps; // keyed by the guest bitmap handle
} // namespace
uint32_t Shim_AndroidBitmap_lockPixels(GuestEngine& eng, uint32_t, uint32_t r1, uint32_t r2, uint32_t, uint32_t) {
jobject bitmap = (jobject)JniShim::Instance().handles().Resolve(r1);
void* addr = nullptr;
int rc = AndroidBitmap_lockPixels(JniShim::Instance().RealEnv(), bitmap, &addr);
// Preferred path: give the guest a real, readable copy.
if (rc == 0 && addr && r2 && !eng.IsHostPointerInRegion(addr)) {
AndroidBitmapInfo info{};
if (AndroidBitmap_getInfo(JniShim::Instance().RealEnv(), bitmap, &info) == 0 &&
info.stride && info.height) {
uint32_t bytes = info.stride * info.height;
GuestAddr buf = eng.heap().Alloc(bytes);
if (buf) {
memcpy(eng.G2H(buf), addr, bytes);
memcpy(eng.G2H(r2), &buf, 4);
{
std::lock_guard<std::mutex> lock(g_bouncedBitmapsMutex);
g_bouncedBitmaps[r1] = BouncedBitmap{buf, bytes, addr};
}
return (uint32_t)rc;
}
// Allocation failed - fall through to the old NULL behaviour
// rather than handing out a host pointer the guest would
// dereference into its own region.
Log("gles_shim: AndroidBitmap_lockPixels: could not allocate a %u-byte guest bounce "
"buffer for a %ux%u bitmap - guest will see NULL pixels", bytes, info.width,
info.height);
}
}
if (r2) {
// Real pixel buffer lives in host (driver-owned) memory, not our
// guest region - H2G on it would silently produce a garbage/
// wraparound "guest address" (see IsHostPointerInRegion's own
// comment), so explicitly return NULL instead of a dangerous
// almost-valid-looking pointer. Guest code that wants to directly
// WRITE pixel data through this pointer can't do so under this
// design (documented gap, low priority - texture decode FROM a
// Bitmap is the main real use, which reads pixels via other means,
// not by writing through this specific pointer).
// Remaining cases: the lock itself failed, or the buffer already lives
// inside the guest region (then H2G is exact and no bounce is needed).
// A host pointer outside the region is never handed over as-is - H2G
// on it would silently produce a garbage/wraparound "guest address"
// (see IsHostPointerInRegion's own comment), so NULL is the honest
// answer.
uint32_t g = (addr && eng.IsHostPointerInRegion(addr)) ? eng.H2G(addr) : 0;
memcpy(eng.G2H(r2), &g, 4);
if (rc == 0 && !g) {
Log("gles_shim: AndroidBitmap_lockPixels succeeded (hostAddr=%p) but the guest gets "
"NULL - no bounce buffer was made, pixels read through this pointer will be "
"garbage", addr);
}
}
return (uint32_t)rc;
}
uint32_t Shim_AndroidBitmap_unlockPixels(GuestEngine&, uint32_t, uint32_t r1, uint32_t, uint32_t, uint32_t) {
uint32_t Shim_AndroidBitmap_unlockPixels(GuestEngine& eng, uint32_t, uint32_t r1, uint32_t, uint32_t, uint32_t) {
// Flush a bounce buffer back into the real bitmap before unlocking, so
// guest-side writes (the direction the old NULL-returning code could not
// support at all) land where Java will see them.
BouncedBitmap bounced{};
{
std::lock_guard<std::mutex> lock(g_bouncedBitmapsMutex);
auto it = g_bouncedBitmaps.find(r1);
if (it != g_bouncedBitmaps.end()) {
bounced = it->second;
g_bouncedBitmaps.erase(it);
}
}
if (bounced.guestBuf) {
memcpy(bounced.hostPixels, eng.G2H(bounced.guestBuf), bounced.bytes);
eng.heap().Free(bounced.guestBuf);
}
jobject bitmap = (jobject)JniShim::Instance().handles().Resolve(r1);
return (uint32_t)AndroidBitmap_unlockPixels(JniShim::Instance().RealEnv(), bitmap);
}