android: probe rgb10a2 cross-context sampling, force 8-bit when broken (#374) - #380
android: probe rgb10a2 cross-context sampling, force 8-bit when broken (#374)#380abdelaziz-mahdy wants to merge 2 commits into
Conversation
Some drivers accept a 10-bit EGLConfig and render into RGBA_1010102 window buffers without error, but corrupt them when a second GL context samples the result (PowerVR BXE / Realtek RTD2875P, wang-bin#374). Nothing surfaces through the EGL/GL API, so detection reproduces the handoff once per process: render quadrant colors into an RGBA_1010102 ImageReader surface (same gralloc usage as the real video path) from one context, import the buffer as an EGLImage in a second unshared context, sample and read back. On mismatch the GL render target is forced to depth=8 via setRenderAPI before updateNativeSurface, keeping 10-bit/HDR untouched on healthy drivers. mediandk is resolved via dlsym (AImageReader_newWithUsage is API 26+); probe-infrastructure failures never force 8-bit. Env overrides for testing: FVP_RGB10A2_PROBE=0 / force8.
There was a problem hiding this comment.
Code Review
This pull request introduces a cross-context RGBA_1010102 sampling probe to detect GPU driver corruption on 10-bit window buffers, forcing an 8-bit fallback when necessary. The review feedback highlights critical issues in the probe implementation, including potential EGL context and surface state corruption on the calling thread, a resource leak of the dynamically loaded libmediandk.so library, and a lack of thread safety when caching the probe results.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| struct ProbeCleanup { | ||
| EGLDisplay dpy = EGL_NO_DISPLAY; | ||
| EGLSurface winSurf = EGL_NO_SURFACE; | ||
| EGLSurface pbuf = EGL_NO_SURFACE; | ||
| EGLContext ctxA = EGL_NO_CONTEXT; | ||
| EGLContext ctxB = EGL_NO_CONTEXT; | ||
| EGLImageKHR image = EGL_NO_IMAGE_KHR; | ||
| AImageReader* reader = nullptr; | ||
| AImage* img = nullptr; | ||
| AImageReader_delete_t readerDelete = nullptr; | ||
| AImage_delete_t imageDelete = nullptr; | ||
| PFNEGLDESTROYIMAGEKHRPROC destroyImage = nullptr; | ||
|
|
||
| ~ProbeCleanup() { | ||
| if (dpy != EGL_NO_DISPLAY) { | ||
| eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); | ||
| if (image != EGL_NO_IMAGE_KHR && destroyImage) destroyImage(dpy, image); | ||
| if (winSurf != EGL_NO_SURFACE) eglDestroySurface(dpy, winSurf); | ||
| if (pbuf != EGL_NO_SURFACE) eglDestroySurface(dpy, pbuf); | ||
| if (ctxA != EGL_NO_CONTEXT) eglDestroyContext(dpy, ctxA); | ||
| if (ctxB != EGL_NO_CONTEXT) eglDestroyContext(dpy, ctxB); | ||
| } | ||
| if (img && imageDelete) imageDelete(img); | ||
| if (reader && readerDelete) readerDelete(reader); | ||
| } | ||
| }; |
There was a problem hiding this comment.
EGL Context and Surface State Leak / Corruption
The probe function probeShowsBroken() changes the current EGL context of the calling thread by calling eglMakeCurrent with its own internal contexts (ctxA and ctxB). When the function returns, the destructor of ProbeCleanup clears the current context of the thread by calling eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT).
If the calling thread (which is typically the Flutter UI/platform thread) already had a current EGL context before probeShowsBroken() was called, that context is lost. This will break subsequent OpenGL/EGL operations on that thread, leading to rendering issues or crashes.
Additionally, libmediandk.so opened via dlopen is currently leaked. We can manage its lifetime by storing the ndk handle in ProbeCleanup and calling dlclose(ndk) at the end of the destructor.
We should save the current EGL display, context, and surfaces at the start of the probe, and restore them in the destructor of ProbeCleanup.
struct ProbeCleanup {
void* ndk = nullptr;
EGLDisplay dpy = EGL_NO_DISPLAY;
EGLSurface winSurf = EGL_NO_SURFACE;
EGLSurface pbuf = EGL_NO_SURFACE;
EGLContext ctxA = EGL_NO_CONTEXT;
EGLContext ctxB = EGL_NO_CONTEXT;
EGLImageKHR image = EGL_NO_IMAGE_KHR;
AImageReader* reader = nullptr;
AImage* img = nullptr;
AImageReader_delete_t readerDelete = nullptr;
AImage_delete_t imageDelete = nullptr;
PFNEGLDESTROYIMAGEKHRPROC destroyImage = nullptr;
EGLDisplay oldDpy = EGL_NO_DISPLAY;
EGLContext oldCtx = EGL_NO_CONTEXT;
EGLSurface oldDraw = EGL_NO_SURFACE;
EGLSurface oldRead = EGL_NO_SURFACE;
~ProbeCleanup() {
if (dpy != EGL_NO_DISPLAY) {
if (oldCtx != EGL_NO_CONTEXT) {
eglMakeCurrent(oldDpy, oldDraw, oldRead, oldCtx);
} else {
eglMakeCurrent(dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
}
if (image != EGL_NO_IMAGE_KHR && destroyImage) destroyImage(dpy, image);
if (winSurf != EGL_NO_SURFACE) eglDestroySurface(dpy, winSurf);
if (pbuf != EGL_NO_SURFACE) eglDestroySurface(dpy, pbuf);
if (ctxA != EGL_NO_CONTEXT) eglDestroyContext(dpy, ctxA);
if (ctxB != EGL_NO_CONTEXT) eglDestroyContext(dpy, ctxB);
}
if (img && imageDelete) imageDelete(img);
if (reader && readerDelete) readerDelete(reader);
if (ndk) dlclose(ndk);
}
};There was a problem hiding this comment.
Fixed in d59add4 — the probe now saves the caller's display/context/surfaces before any EGL call and restores them on exit (clearing only when nothing was current).
| bool probeShowsBroken() { | ||
| void* ndk = dlopen("libmediandk.so", RTLD_NOW | RTLD_LOCAL); | ||
| if (!ndk) return false; | ||
| auto newWithUsage = (AImageReader_newWithUsage_t)dlsym(ndk, "AImageReader_newWithUsage"); | ||
| auto getWindow = (AImageReader_getWindow_t)dlsym(ndk, "AImageReader_getWindow"); | ||
| auto acquireNext = (AImageReader_acquireNextImage_t)dlsym(ndk, "AImageReader_acquireNextImage"); | ||
| auto readerDelete = (AImageReader_delete_t)dlsym(ndk, "AImageReader_delete"); | ||
| auto getHwBuffer = (AImage_getHardwareBuffer_t)dlsym(ndk, "AImage_getHardwareBuffer"); | ||
| auto imageDelete = (AImage_delete_t)dlsym(ndk, "AImage_delete"); | ||
| if (!newWithUsage || !getWindow || !acquireNext || !readerDelete || !getHwBuffer || !imageDelete) { | ||
| return false; // pre-26 device: can't probe (and 1010102 unlikely anyway) | ||
| } | ||
|
|
||
| auto getNativeClientBuffer = | ||
| (PFNEGLGETNATIVECLIENTBUFFERANDROIDPROC)eglGetProcAddress("eglGetNativeClientBufferANDROID"); | ||
| auto createImage = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR"); | ||
| auto destroyImage = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR"); | ||
| auto imageTargetTexture = | ||
| (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES"); | ||
| if (!getNativeClientBuffer || !createImage || !destroyImage || !imageTargetTexture) { | ||
| return false; | ||
| } | ||
|
|
||
| ProbeCleanup c; | ||
| c.readerDelete = readerDelete; | ||
| c.imageDelete = imageDelete; | ||
| c.destroyImage = destroyImage; |
There was a problem hiding this comment.
Prevent libmediandk.so Leaks and Save EGL State Early
To prevent leaking libmediandk.so on early returns and to correctly save the current EGL context/surfaces before any EGL operations, we should instantiate ProbeCleanup immediately after dlopen and populate its fields.
bool probeShowsBroken() {
void* ndk = dlopen("libmediandk.so", RTLD_NOW | RTLD_LOCAL);
if (!ndk) return false;
ProbeCleanup c;
c.ndk = ndk;
c.oldDpy = eglGetCurrentDisplay();
c.oldCtx = eglGetCurrentContext();
c.oldDraw = eglGetCurrentSurface(EGL_DRAW);
c.oldRead = eglGetCurrentSurface(EGL_READ);
auto newWithUsage = (AImageReader_newWithUsage_t)dlsym(ndk, "AImageReader_newWithUsage");
auto getWindow = (AImageReader_getWindow_t)dlsym(ndk, "AImageReader_getWindow");
auto acquireNext = (AImageReader_acquireNextImage_t)dlsym(ndk, "AImageReader_acquireNextImage");
auto readerDelete = (AImageReader_delete_t)dlsym(ndk, "AImageReader_delete");
auto getHwBuffer = (AImage_getHardwareBuffer_t)dlsym(ndk, "AImage_getHardwareBuffer");
auto imageDelete = (AImage_delete_t)dlsym(ndk, "AImage_delete");
if (!newWithUsage || !getWindow || !acquireNext || !readerDelete || !getHwBuffer || !imageDelete) {
return false; // pre-26 device: can't probe (and 1010102 unlikely anyway)
}
auto getNativeClientBuffer =
(PFNEGLGETNATIVECLIENTBUFFERANDROIDPROC)eglGetProcAddress("eglGetNativeClientBufferANDROID");
auto createImage = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
auto destroyImage = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
auto imageTargetTexture =
(PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
if (!getNativeClientBuffer || !createImage || !destroyImage || !imageTargetTexture) {
return false;
}
c.readerDelete = readerDelete;
c.imageDelete = imageDelete;
c.destroyImage = destroyImage;There was a problem hiding this comment.
Fixed in d59add4 — ProbeCleanup owns the dlopen handle and closes it on every exit path, after the AImage/AImageReader deleters that live in that library.
| bool fvpRgb10a2CrossContextOk() { | ||
| static int cached = -1; | ||
| if (cached >= 0) return cached != 0; | ||
| if (const char* env = getenv("FVP_RGB10A2_PROBE")) { | ||
| if (!strcmp(env, "0")) cached = 1; | ||
| else if (!strcmp(env, "force8")) cached = 0; | ||
| } | ||
| if (cached < 0) cached = probeShowsBroken() ? 0 : 1; | ||
| clog << "rgb10a2 cross-context sampling ok: " << cached << endl; | ||
| return cached != 0; | ||
| } |
There was a problem hiding this comment.
Thread-Safe Caching of Probe Result
The static variable cached is checked and modified without any synchronization. If multiple players are initialized concurrently, or if fvpRgb10a2CrossContextOk() is called from multiple threads, this results in a data race on cached and could cause probeShowsBroken() to run multiple times concurrently.
We can make this completely thread-safe and more idiomatic by using a C++11 thread-safe static local variable initialized with a lambda.
| bool fvpRgb10a2CrossContextOk() { | |
| static int cached = -1; | |
| if (cached >= 0) return cached != 0; | |
| if (const char* env = getenv("FVP_RGB10A2_PROBE")) { | |
| if (!strcmp(env, "0")) cached = 1; | |
| else if (!strcmp(env, "force8")) cached = 0; | |
| } | |
| if (cached < 0) cached = probeShowsBroken() ? 0 : 1; | |
| clog << "rgb10a2 cross-context sampling ok: " << cached << endl; | |
| return cached != 0; | |
| } | |
| bool fvpRgb10a2CrossContextOk() { | |
| static const bool ok = []() { | |
| if (const char* env = getenv("FVP_RGB10A2_PROBE")) { | |
| if (!strcmp(env, "0")) return true; | |
| if (!strcmp(env, "force8")) return false; | |
| } | |
| const bool broken = probeShowsBroken(); | |
| clog << "rgb10a2 cross-context sampling ok: " << !broken << endl; | |
| return !broken; | |
| }(); | |
| return ok; | |
| } |
There was a problem hiding this comment.
Fixed in d59add4 — cache is a magic static initialized by a lambda (env overrides included), so concurrent first calls are safe.
The probe ran eglMakeCurrent on the calling thread and cleared to EGL_NO_CONTEXT on exit, clobbering any context the caller had current. Save the current display/context/surfaces before touching EGL and restore them in the cleanup. Also dlclose the mediandk handle (after the AImage/AImageReader deleters that live in it) and switch the probe cache to a magic static so concurrent first calls are safe.
|
Tested on the affected device (TCL RTD2875P / PowerVR BXE-4-32, Android 12) — the probe as written does NOT detect the failure, and the reasons are informative, so posting them before any more engineering. Iterations tried (all on 1. 2. Flat content always survives. With solid color quadrants: 3. Even the faithful version passes. Also tried Conclusion: on this driver, in-process
So a faithful app-side probe would need a real Marking this draft accordingly; the branch stays available as a starting point. |
|
not elegant, I prefer GLRenderAPI.depth = 8. |
I agree, and even don't know if the other approaches will work But with 8 we lose the hdr right? |
hdr works for all depths, but lose some details in 8bit surfaces. i can change EGL_SDR_DEPTH default value to 8, then hdr will try rgb10a2 but still corrupt on your device. |
Oh I misunderstood then, so yeah 8 is good as a default |
Draft for your opinion — the detection approach discussed in #374. Full disclosure: the implementation was generated with Fable 5 (the same model that produced the #374 analysis) and reviewed by me.
UPDATE after on-device testing: this in-process probe does NOT detect the failure — see the findings comment below. In-process EGLImage import round-trips cleanly on this driver; the real failures live in SurfaceTexture's import path and in SurfaceFlinger's process. Kept as a draft for discussion of what detection should look like.
Idea
The driver accepts a 10-bit EGLConfig and renders RGBA_1010102 window buffers without any EGL/GL error — the corruption only appears when a second GL context samples the buffer. So a query can't detect it; the probe reproduces the handoff once per process, before the first surface attach:
AImageReader(512×512,RGBA_1010102,GPU_SAMPLED | GPU_COLOR_OUTPUT— same gralloc usage as the real video path, so vendor tiled/compressed layouts are exercised).AHardwareBufferis imported viaeglCreateImageKHRinto a second, unshared context and sampled asGL_TEXTURE_EXTERNAL_OES— the same import path the Flutter engine uses.Any corrupt sample →
GLRenderAPI.depth = 8viasetRenderAPI()beforeupdateNativeSurface()(your suggested fix from #374), so only broken drivers lose 10-bit; healthy devices keep rgb10a2/HDR untouched.Notes
dlsym(AImageReader_newWithUsageis API 26+; direct linking would break plugin load on older devices). Probe-infrastructure failures (no 10-bit config, EGL errors, pre-26) never force 8-bit.FVP_RGB10A2_PROBE=0(skip),FVP_RGB10A2_PROBE=force8.I'll test on the affected device and report the probe log output here. Happy to change the approach entirely if you have a different direction in mind.