From 0e933b8f2f16185cfd83749ab9e5072198f98d1f Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 26 Jul 2026 08:05:02 -0400 Subject: [PATCH 1/6] [Test] (CTS): run VK-GL-CTS KHR-GL33 against MobileGL on Android via a standalone glcts binary --- tools/cts/README.md | 85 ++++ tools/cts/platform/tcuMobileGLPlatform.cpp | 473 ++++++++++++++++++ tools/cts/platform/tcuMobileGLPlatform.hpp | 33 ++ tools/cts/probe/.gitignore | 2 + tools/cts/probe/mgprobe.c | 318 ++++++++++++ tools/cts/scripts/qpa_report.py | 204 ++++++++ tools/cts/scripts/run_cts.py | 266 ++++++++++ tools/cts/scripts/sync_to_cts.py | 53 ++ tools/cts/skills/README.md | 18 + tools/cts/skills/gl-cts-on-mobilegl/SKILL.md | 192 +++++++ .../gl-cts-on-mobilegl/agents/openai.yaml | 4 + tools/cts/targets/mobilegl.cmake | 36 ++ tools/cts/targets/ndk-modern.cmake | 61 +++ 13 files changed, 1745 insertions(+) create mode 100644 tools/cts/README.md create mode 100644 tools/cts/platform/tcuMobileGLPlatform.cpp create mode 100644 tools/cts/platform/tcuMobileGLPlatform.hpp create mode 100644 tools/cts/probe/.gitignore create mode 100644 tools/cts/probe/mgprobe.c create mode 100644 tools/cts/scripts/qpa_report.py create mode 100644 tools/cts/scripts/run_cts.py create mode 100644 tools/cts/scripts/sync_to_cts.py create mode 100644 tools/cts/skills/README.md create mode 100644 tools/cts/skills/gl-cts-on-mobilegl/SKILL.md create mode 100644 tools/cts/skills/gl-cts-on-mobilegl/agents/openai.yaml create mode 100644 tools/cts/targets/mobilegl.cmake create mode 100644 tools/cts/targets/ndk-modern.cmake diff --git a/tools/cts/README.md b/tools/cts/README.md new file mode 100644 index 00000000..b8d35b08 --- /dev/null +++ b/tools/cts/README.md @@ -0,0 +1,85 @@ +# Running the OpenGL CTS (VK-GL-CTS / KHR-GL33) against MobileGL on Android + +Goal: measure how much of the OpenGL 3.3 core-profile conformance suite MobileGL +passes, separately for each backend (`DirectGLES`, `DirectVulkan`). + +## How MobileGL is reached from a test binary + +MobileGL ships its own EGL implementation alongside its desktop-GL implementation +in a single `libMobileGL.so`. A plain arm64 ELF in `/data/local/tmp` can therefore +drive it with no APK and no Activity: + +1. `setenv("MOBILEGL_BACKEND_TYPE", "DirectGLES"|"DirectVulkan")` **before** the + library is mapped — MobileGL parses its configuration from an ELF constructor. +2. `dlopen("libMobileGL.so")`, then `dlsym` the `egl*` and `gl*` entry points. + MobileGL exports 45 EGL symbols and the desktop GL functions directly; + `eglGetProcAddress` resolves the same set. +3. `eglBindAPI(EGL_OPENGL_API)`, choose a config with `EGL_RENDERABLE_TYPE = + EGL_OPENGL_BIT`, then `eglCreateContext` with + `EGL_CONTEXT_OPENGL_PROFILE_MASK = EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT` and + major/minor `3`/`3`. + +This yields a genuine GL 3.3 core context (`GL_CONTEXT_PROFILE_MASK == 0x1`). + +## Surface type, per backend + +| backend | pbuffer (headless) | window | +|---|---|---| +| `DirectGLES` | works | works | +| `DirectVulkan` | **unusable** | works | + +`DirectVulkan`'s pbuffer path builds a headless `VkSurfaceKHR` and so requires the +`VK_EXT_headless_surface` instance extension, which Adreno's Android driver does +not expose. It fails inside `eglMakeCurrent`, not at surface creation. + +The workaround that keeps everything in a shell process: obtain a real +`ANativeWindow` from **`AImageReader`** (`AImageReader_newWithUsage` + +`AImageReader_getWindow`). It is an ordinary BufferQueue producer, so +`vkCreateAndroidSurfaceKHR` accepts it, and no Activity is involved. Register an +`onImageAvailable` listener that acquires and deletes each image — otherwise the +producer blocks once `maxImages` buffers are in flight and the next swap hangs. + +## Why the suite must render into an FBO + +On a window surface, `DirectVulkan`'s `glReadPixels` from the **default +framebuffer** returns all zeros, with no GL error, both before and after +`eglSwapBuffers`. `DirectGLES` on the identical window is correct, and readback +from a **user FBO is correct on both backends**. + +Verified on two SoCs and two drivers, so this is MobileGL's behaviour rather than +a driver quirk: + +| device | GPU | driver | default-FB | user FBO | +|---|---|---|---|---| +| Xiaomi 24129PN74C | Adreno 830 | Vulkan 1.3.284 / 512.800.46 | zeros | ok | +| Lenovo TB321FU | Adreno 750 | Vulkan 1.3.128 / 512.762.28 | zeros | ok | + +dEQP verifies nearly every case through `glReadPixels`, so running it against the +default framebuffer would score `DirectVulkan` near zero for a reason unrelated to +conformance. The runs therefore use `--deqp-surface-type=fbo`, uniformly for both +backends so the two numbers stay comparable. + +## Other constraints the harness must respect + +- `eglMakeCurrent` requires **draw == read** and rejects `EGL_NO_SURFACE` with + `EGL_BAD_MATCH`. dEQP's `surfaceless` platform is therefore unusable, which is + why this port supplies its own `tcu::Platform`. +- MobileGL aborts during static teardown (`FORTIFY: pthread_mutex_lock called on a + destroyed mutex`) *after* all work completes. Flush and `_exit()` so the exit + code and the `.qpa` log survive. + +## Contents + + probe/mgprobe.c preflight gate: one backend x one surface type, checks + context version/profile and both readback paths + scripts/qpa_report.py .qpa -> pass rate, status histogram, worst groups + +### Preflight + + aarch64-linux-android26-clang -O1 -o mgprobe mgprobe.c -ldl -llog -landroid -lmediandk + adb push mgprobe libMobileGL.so /data/local/tmp/mgcts/ + adb shell 'cd /data/local/tmp/mgcts && LD_LIBRARY_PATH=. ./mgprobe \ + --backend DirectVulkan --surface imagereader --lib ./libMobileGL.so' + +Exit status is 0 when a 3.3 core context came up and FBO readback is correct. +Default-framebuffer readback is reported but deliberately does not gate. diff --git a/tools/cts/platform/tcuMobileGLPlatform.cpp b/tools/cts/platform/tcuMobileGLPlatform.cpp new file mode 100644 index 00000000..190c1a8f --- /dev/null +++ b/tools/cts/platform/tcuMobileGLPlatform.cpp @@ -0,0 +1,473 @@ +/*------------------------------------------------------------------------- + * dEQP platform port for MobileGL on Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *//*! + * \file + * \brief MobileGL platform. + * + * Modelled on the surfaceless platform, but adapted to MobileGL, which ships + * its own EGL implementation inside libMobileGL.so: + * + * - Every EGL call goes through the dynamically loaded library. The + * surfaceless port mixes wrapper calls with globally linked egl* symbols; + * doing that here would silently reach Android's system EGL instead. + * - Desktop-GL configs are selected with EGL_OPENGL_BIT. The surfaceless port + * always asks for an ES bit, which cannot satisfy a GL 3.3 core context. + * - A real surface is always created. MobileGL rejects EGL_NO_SURFACE with + * EGL_BAD_MATCH, and --deqp-surface-type=fbo asks the platform for + * SURFACETYPE_DONT_CARE, so "no surface" is not an option. + * - Window surfaces are backed by an AImageReader rather than an Activity, + * which is what lets the suite run as a plain adb-shell binary. DirectVulkan + * needs this: its pbuffer path requires VK_EXT_headless_surface, which + * Adreno's Android driver does not expose. + * + * Environment: + * MOBILEGL_CTS_LIB path/soname of the MobileGL library (default libMobileGL.so) + * MOBILEGL_CTS_SURFACE "window" (default) or "pbuffer" + * MOBILEGL_BACKEND_TYPE read by MobileGL itself; set it before launching + *//*--------------------------------------------------------------------*/ + +#include "tcuMobileGLPlatform.hpp" + +#include +#include +#include + +#include "deDynamicLibrary.hpp" +#include "egluUtil.hpp" +#include "eglwEnums.hpp" +#include "eglwLibrary.hpp" +#include "gluPlatform.hpp" +#include "gluRenderConfig.hpp" +#include "gluRenderContext.hpp" +#include "glwInitFunctions.hpp" +#include "tcuCommandLine.hpp" +#include "tcuPixelFormat.hpp" +#include "tcuPlatform.hpp" +#include "tcuRenderTarget.hpp" + +#include +#include +#include + +using std::string; +using std::vector; + +#if !defined(EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR) +#define EGL_CONTEXT_FLAGS_KHR 0x30FC +#define EGL_CONTEXT_MAJOR_VERSION_KHR 0x3098 +#define EGL_CONTEXT_MINOR_VERSION_KHR 0x30FB +#define EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT_KHR 0x00000002 +#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR 0x00000001 +#define EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR 0x00000001 +#define EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR 0x00000002 +#define EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR 0x30FD +#define EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR 0x00000004 +#endif + +namespace tcu +{ +namespace mobilegl +{ + +static string getLibraryName(void) +{ + const char *env = std::getenv("MOBILEGL_CTS_LIB"); + return (env && env[0]) ? string(env) : string("libMobileGL.so"); +} + +//! Window surfaces default on: they are the only kind DirectVulkan can use. +static bool useWindowSurface(void) +{ + const char *env = std::getenv("MOBILEGL_CTS_SURFACE"); + return !(env && string(env) == "pbuffer"); +} + +/*--------------------------------------------------------------------*//*! + * \brief A real ANativeWindow with no Activity behind it. + * + * AImageReader's window is an ordinary BufferQueue producer, so both + * eglCreateWindowSurface and vkCreateAndroidSurfaceKHR accept it. The image + * listener must drain the queue: without it the producer blocks once maxImages + * buffers are in flight and the next swap deadlocks. + *//*--------------------------------------------------------------------*/ +class ImageReaderWindow +{ +public: + ImageReaderWindow(int width, int height) : m_reader(nullptr), m_window(nullptr) + { + const media_status_t status = + AImageReader_newWithUsage(width, height, AIMAGE_FORMAT_RGBA_8888, + AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | + AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT, + kMaxImages, &m_reader); + if (status != AMEDIA_OK || m_reader == nullptr) + throw tcu::ResourceError("AImageReader_newWithUsage() failed"); + + AImageReader_ImageListener listener = {this, onImageAvailable}; + AImageReader_setImageListener(m_reader, &listener); + + if (AImageReader_getWindow(m_reader, &m_window) != AMEDIA_OK || m_window == nullptr) + { + AImageReader_delete(m_reader); + m_reader = nullptr; + throw tcu::ResourceError("AImageReader_getWindow() failed"); + } + ANativeWindow_acquire(m_window); + } + + ~ImageReaderWindow(void) + { + if (m_window != nullptr) + ANativeWindow_release(m_window); + if (m_reader != nullptr) + { + AImageReader_setImageListener(m_reader, nullptr); + AImageReader_delete(m_reader); + } + } + + ANativeWindow *getWindow(void) const + { + return m_window; + } + +private: + static const int kMaxImages = 4; + + static void onImageAvailable(void *, AImageReader *reader) + { + AImage *image = nullptr; + if (AImageReader_acquireNextImage(reader, &image) == AMEDIA_OK && image != nullptr) + AImage_delete(image); + } + + ImageReaderWindow(const ImageReaderWindow &); + ImageReaderWindow &operator=(const ImageReaderWindow &); + + AImageReader *m_reader; + ANativeWindow *m_window; +}; + +class GetProcFuncLoader : public glw::FunctionLoader +{ +public: + GetProcFuncLoader(const eglw::Library &egl) : m_egl(egl) + { + } + + glw::GenericFuncType get(const char *name) const + { + return (glw::GenericFuncType)m_egl.getProcAddress(name); + } + +protected: + const eglw::Library &m_egl; +}; + +class EglRenderContext : public glu::RenderContext +{ +public: + EglRenderContext(const glu::RenderConfig &config, const tcu::CommandLine &cmdLine, + const glu::RenderContext *sharedContext); + ~EglRenderContext(void); + + glu::ContextType getType(void) const + { + return m_contextType; + } + eglw::EGLContext getEglContext(void) const + { + return m_eglContext; + } + const glw::Functions &getFunctions(void) const + { + return m_glFunctions; + } + const tcu::RenderTarget &getRenderTarget(void) const + { + return m_renderTarget; + } + void postIterate(void); + void makeCurrent(void); + + glw::GenericFuncType getProcAddress(const char *name) const + { + return (glw::GenericFuncType)m_egl.getProcAddress(name); + } + +private: + const eglw::DefaultLibrary m_egl; + const glu::ContextType m_contextType; + eglw::EGLDisplay m_eglDisplay; + eglw::EGLContext m_eglContext; + eglw::EGLSurface m_eglSurface; + ImageReaderWindow *m_window; + glw::Functions m_glFunctions; + tcu::RenderTarget m_renderTarget; + eglw::EGLContext m_sharedEglContext; +}; + +class ContextFactory : public glu::ContextFactory +{ +public: + ContextFactory(void) : glu::ContextFactory("default", "MobileGL EGL context") + { + } + + glu::RenderContext *createContext(const glu::RenderConfig &config, const tcu::CommandLine &cmdLine, + const glu::RenderContext *sharedContext) const + { + return new EglRenderContext(config, cmdLine, sharedContext); + } +}; + +class Platform : public tcu::Platform, public glu::Platform +{ +public: + Platform(void) + { + m_contextFactoryRegistry.registerFactory(new ContextFactory()); + } + + const glu::Platform &getGLPlatform(void) const + { + return *this; + } +}; + +EglRenderContext::EglRenderContext(const glu::RenderConfig &config, const tcu::CommandLine &cmdLine, + const glu::RenderContext *sharedContext) + : m_egl(getLibraryName().c_str()) + , m_contextType(config.type) + , m_eglDisplay(EGL_NO_DISPLAY) + , m_eglContext(EGL_NO_CONTEXT) + , m_eglSurface(EGL_NO_SURFACE) + , m_window(nullptr) + , m_renderTarget(config.width, config.height, + tcu::PixelFormat(config.redBits, config.greenBits, config.blueBits, config.alphaBits), + config.depthBits, config.stencilBits, config.numSamples) + , m_sharedEglContext(EGL_NO_CONTEXT) +{ + DE_UNREF(cmdLine); + + const glu::ContextType &contextType = config.type; + const bool isES = glu::isContextTypeES(contextType); + eglw::EGLint eglMajorVersion = 0; + eglw::EGLint eglMinorVersion = 0; + + m_eglDisplay = m_egl.getDisplay(EGL_DEFAULT_DISPLAY); + EGLU_CHECK_MSG(m_egl, "eglGetDisplay()"); + if (m_eglDisplay == EGL_NO_DISPLAY) + throw tcu::ResourceError("eglGetDisplay() failed"); + + EGLU_CHECK_CALL(m_egl, initialize(m_eglDisplay, &eglMajorVersion, &eglMinorVersion)); + + // MobileGL cannot make a context current without a surface, so + // SURFACETYPE_DONT_CARE (which is what --deqp-surface-type=fbo requests) + // still gets a real one. + bool wantWindow = false; + switch (config.surfaceType) + { + case glu::RenderConfig::SURFACETYPE_WINDOW: + wantWindow = true; + break; + case glu::RenderConfig::SURFACETYPE_OFFSCREEN_NATIVE: + case glu::RenderConfig::SURFACETYPE_OFFSCREEN_GENERIC: + wantWindow = false; + break; + case glu::RenderConfig::SURFACETYPE_DONT_CARE: + wantWindow = useWindowSurface(); + break; + default: + TCU_CHECK_INTERNAL(false); + } + + const int width = (config.width == glu::RenderConfig::DONT_CARE) ? 256 : config.width; + const int height = (config.height == glu::RenderConfig::DONT_CARE) ? 256 : config.height; + + vector cfgAttribs; + cfgAttribs.push_back(EGL_RENDERABLE_TYPE); + if (isES) + { + switch (contextType.getMajorVersion()) + { + case 3: + cfgAttribs.push_back(EGL_OPENGL_ES3_BIT); + break; + case 2: + cfgAttribs.push_back(EGL_OPENGL_ES2_BIT); + break; + default: + cfgAttribs.push_back(EGL_OPENGL_ES_BIT); + } + } + else + { + // Desktop GL, which is the whole point of this port. + cfgAttribs.push_back(EGL_OPENGL_BIT); + } + + cfgAttribs.push_back(EGL_SURFACE_TYPE); + cfgAttribs.push_back(wantWindow ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT); + + static const struct + { + eglw::EGLint attrib; + int glu::RenderConfig::*field; + } s_sizeAttribs[] = { + {EGL_RED_SIZE, &glu::RenderConfig::redBits}, {EGL_GREEN_SIZE, &glu::RenderConfig::greenBits}, + {EGL_BLUE_SIZE, &glu::RenderConfig::blueBits}, {EGL_ALPHA_SIZE, &glu::RenderConfig::alphaBits}, + {EGL_DEPTH_SIZE, &glu::RenderConfig::depthBits}, {EGL_STENCIL_SIZE, &glu::RenderConfig::stencilBits}, + {EGL_SAMPLES, &glu::RenderConfig::numSamples}, + }; + for (size_t ndx = 0; ndx < DE_LENGTH_OF_ARRAY(s_sizeAttribs); ndx++) + { + const int value = config.*(s_sizeAttribs[ndx].field); + if (value != glu::RenderConfig::DONT_CARE) + { + cfgAttribs.push_back(s_sizeAttribs[ndx].attrib); + cfgAttribs.push_back(value); + } + } + cfgAttribs.push_back(EGL_NONE); + + eglw::EGLConfig eglConfig = nullptr; + eglw::EGLint numConfigs = 0; + EGLU_CHECK_CALL(m_egl, chooseConfig(m_eglDisplay, &cfgAttribs[0], &eglConfig, 1, &numConfigs)); + if (numConfigs < 1) + throw tcu::NotSupportedError("No matching EGL config for the requested context"); + + if (wantWindow) + { + m_window = new ImageReaderWindow(width, height); + + eglw::EGLint visualId = 0; + if (m_egl.getConfigAttrib(m_eglDisplay, eglConfig, EGL_NATIVE_VISUAL_ID, &visualId) && visualId != 0) + ANativeWindow_setBuffersGeometry(m_window->getWindow(), width, height, visualId); + + m_eglSurface = m_egl.createWindowSurface(m_eglDisplay, eglConfig, + (eglw::EGLNativeWindowType)m_window->getWindow(), nullptr); + EGLU_CHECK_MSG(m_egl, "eglCreateWindowSurface()"); + } + else + { + const eglw::EGLint surfaceAttribs[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE}; + m_eglSurface = m_egl.createPbufferSurface(m_eglDisplay, eglConfig, surfaceAttribs); + EGLU_CHECK_MSG(m_egl, "eglCreatePbufferSurface()"); + } + + if (m_eglSurface == EGL_NO_SURFACE) + throw tcu::ResourceError("Failed to create EGL surface"); + + vector ctxAttribs; + ctxAttribs.push_back(EGL_CONTEXT_MAJOR_VERSION_KHR); + ctxAttribs.push_back(contextType.getMajorVersion()); + ctxAttribs.push_back(EGL_CONTEXT_MINOR_VERSION_KHR); + ctxAttribs.push_back(contextType.getMinorVersion()); + + switch (contextType.getProfile()) + { + case glu::PROFILE_ES: + EGLU_CHECK_CALL(m_egl, bindAPI(EGL_OPENGL_ES_API)); + break; + case glu::PROFILE_CORE: + EGLU_CHECK_CALL(m_egl, bindAPI(EGL_OPENGL_API)); + ctxAttribs.push_back(EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR); + ctxAttribs.push_back(EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR); + break; + case glu::PROFILE_COMPATIBILITY: + EGLU_CHECK_CALL(m_egl, bindAPI(EGL_OPENGL_API)); + ctxAttribs.push_back(EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR); + ctxAttribs.push_back(EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT_KHR); + break; + default: + TCU_CHECK_INTERNAL(false); + } + + eglw::EGLint flags = 0; + if ((contextType.getFlags() & glu::CONTEXT_DEBUG) != 0) + flags |= EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR; + if ((contextType.getFlags() & glu::CONTEXT_ROBUST) != 0) + flags |= EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR; + if ((contextType.getFlags() & glu::CONTEXT_FORWARD_COMPATIBLE) != 0) + flags |= EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR; + if (flags != 0) + { + ctxAttribs.push_back(EGL_CONTEXT_FLAGS_KHR); + ctxAttribs.push_back(flags); + } + ctxAttribs.push_back(EGL_NONE); + + const EglRenderContext *sharedEglRenderContext = dynamic_cast(sharedContext); + m_sharedEglContext = sharedEglRenderContext ? sharedEglRenderContext->getEglContext() : EGL_NO_CONTEXT; + + m_eglContext = m_egl.createContext(m_eglDisplay, eglConfig, m_sharedEglContext, &ctxAttribs[0]); + EGLU_CHECK_MSG(m_egl, "eglCreateContext()"); + if (!m_eglContext) + throw tcu::ResourceError("eglCreateContext() failed"); + + // MobileGL requires draw == read. + EGLU_CHECK_CALL(m_egl, makeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglContext)); + + // MobileGL advertises EGL 1.5, so eglGetProcAddress resolves core entry + // points too; there is no separate GL library to dlopen. + GetProcFuncLoader funcLoader(m_egl); + glu::initCoreFunctions(&m_glFunctions, &funcLoader, contextType.getAPI()); + glu::initExtensionFunctions(&m_glFunctions, &funcLoader, contextType.getAPI()); +} + +EglRenderContext::~EglRenderContext(void) +{ + try + { + if (m_eglDisplay != EGL_NO_DISPLAY) + { + m_egl.makeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + + if (m_eglContext != EGL_NO_CONTEXT) + m_egl.destroyContext(m_eglDisplay, m_eglContext); + + if (m_eglSurface != EGL_NO_SURFACE) + m_egl.destroySurface(m_eglDisplay, m_eglSurface); + + if (m_sharedEglContext == EGL_NO_CONTEXT) + m_egl.terminate(m_eglDisplay); + } + } + catch (...) + { + } + + delete m_window; +} + +void EglRenderContext::makeCurrent(void) +{ + EGLU_CHECK_CALL(m_egl, makeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglContext)); +} + +void EglRenderContext::postIterate(void) +{ + m_glFunctions.finish(); +} + +} // namespace mobilegl +} // namespace tcu + +tcu::Platform *createPlatform(void) +{ + return new tcu::mobilegl::Platform(); +} diff --git a/tools/cts/platform/tcuMobileGLPlatform.hpp b/tools/cts/platform/tcuMobileGLPlatform.hpp new file mode 100644 index 00000000..722fbe39 --- /dev/null +++ b/tools/cts/platform/tcuMobileGLPlatform.hpp @@ -0,0 +1,33 @@ +#ifndef _TCUMOBILEGLPLATFORM_HPP +#define _TCUMOBILEGLPLATFORM_HPP +/*------------------------------------------------------------------------- + * dEQP platform port for MobileGL on Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *//*! + * \file + * \brief MobileGL platform - drives libMobileGL.so's own EGL from a bare + * Android process, with no Activity and no system EGL involved. + *//*--------------------------------------------------------------------*/ + +#include "tcuDefs.hpp" + +namespace tcu +{ +class Platform; +} + +tcu::Platform *createPlatform(void); + +#endif // _TCUMOBILEGLPLATFORM_HPP diff --git a/tools/cts/probe/.gitignore b/tools/cts/probe/.gitignore new file mode 100644 index 00000000..b5eab991 --- /dev/null +++ b/tools/cts/probe/.gitignore @@ -0,0 +1,2 @@ +mgprobe +*.o diff --git a/tools/cts/probe/mgprobe.c b/tools/cts/probe/mgprobe.c new file mode 100644 index 00000000..c2bbe123 --- /dev/null +++ b/tools/cts/probe/mgprobe.c @@ -0,0 +1,318 @@ +/* mgprobe - preflight gate for running a GL conformance suite against MobileGL + * from a bare adb-shell process (no APK, no Activity). + * + * Verifies, for one backend and one surface type, that MobileGL can hand out a + * GL 3.3 core context and that pixels read back correctly - both from the + * default framebuffer and from a user FBO. Run this before burning hours on a + * CTS run; it catches a broken device/library pairing in about a second. + * + * mgprobe --backend DirectGLES|DirectVulkan --surface pbuffer|imagereader + * [--lib /path/to/libMobileGL.so] + * + * Exit status: 0 if a context came up and FBO readback is correct, non-zero + * otherwise. Default-framebuffer readback is reported but does NOT gate, because + * DirectVulkan is known to return zeros there while FBO readback is sound. + * + * Build (NDK, arm64): + * $NDK/toolchains/llvm/prebuilt//bin/aarch64-linux-android26-clang \ + * -O1 -o mgprobe mgprobe.c -ldl -llog -landroid -lmediandk + */ +#include +#include +#include +#include +#include +#include +#include + +typedef void *EGLDisplay; +typedef void *EGLConfig; +typedef void *EGLSurface; +typedef void *EGLContext; +typedef int EGLint; +typedef unsigned int EGLBoolean; +typedef unsigned int EGLenum; +typedef void *EGLNativeDisplayType; +typedef void *EGLNativeWindowType; + +#define EGL_DEFAULT_DISPLAY ((EGLNativeDisplayType)0) +#define EGL_NO_CONTEXT ((EGLContext)0) +#define EGL_NO_SURFACE ((EGLSurface)0) +#define EGL_NONE 0x3038 +#define EGL_WIDTH 0x3057 +#define EGL_HEIGHT 0x3056 +#define EGL_RENDERABLE_TYPE 0x3040 +#define EGL_SURFACE_TYPE 0x3033 +#define EGL_WINDOW_BIT 0x0004 +#define EGL_PBUFFER_BIT 0x0001 +#define EGL_OPENGL_BIT 0x0008 +#define EGL_OPENGL_API 0x30A2 +#define EGL_RED_SIZE 0x3024 +#define EGL_GREEN_SIZE 0x3023 +#define EGL_BLUE_SIZE 0x3022 +#define EGL_ALPHA_SIZE 0x3021 +#define EGL_DEPTH_SIZE 0x3025 +#define EGL_STENCIL_SIZE 0x3026 +#define EGL_NATIVE_VISUAL_ID 0x302E +#define EGL_CONTEXT_MAJOR_VERSION 0x3098 +#define EGL_CONTEXT_MINOR_VERSION 0x30FB +#define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD +#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001 + +#define GL_VENDOR 0x1F00 +#define GL_RENDERER 0x1F01 +#define GL_VERSION 0x1F02 +#define GL_SHADING_LANGUAGE_VERSION 0x8B8C +#define GL_CONTEXT_PROFILE_MASK 0x9126 +#define GL_MAJOR_VERSION 0x821B +#define GL_MINOR_VERSION 0x821C +#define GL_COLOR_BUFFER_BIT 0x00004000 +#define GL_RGBA 0x1908 +#define GL_RGBA8 0x8058 +#define GL_UNSIGNED_BYTE 0x1401 +#define GL_TEXTURE_2D 0x0DE1 +#define GL_FRAMEBUFFER 0x8D40 +#define GL_COLOR_ATTACHMENT0 0x8CE0 +#define GL_FRAMEBUFFER_COMPLETE 0x8CD5 +#define GL_TEXTURE_MIN_FILTER 0x2801 +#define GL_TEXTURE_MAG_FILTER 0x2800 +#define GL_NEAREST 0x2600 + +typedef EGLDisplay (*P_getDisplay)(EGLNativeDisplayType); +typedef EGLBoolean (*P_initialize)(EGLDisplay, EGLint *, EGLint *); +typedef EGLBoolean (*P_bindAPI)(EGLenum); +typedef EGLBoolean (*P_chooseConfig)(EGLDisplay, const EGLint *, EGLConfig *, EGLint, EGLint *); +typedef EGLBoolean (*P_getConfigAttrib)(EGLDisplay, EGLConfig, EGLint, EGLint *); +typedef EGLSurface (*P_createWindowSurface)(EGLDisplay, EGLConfig, EGLNativeWindowType, const EGLint *); +typedef EGLSurface (*P_createPbufferSurface)(EGLDisplay, EGLConfig, const EGLint *); +typedef EGLContext (*P_createContext)(EGLDisplay, EGLConfig, EGLContext, const EGLint *); +typedef EGLBoolean (*P_makeCurrent)(EGLDisplay, EGLSurface, EGLSurface, EGLContext); +typedef EGLint (*P_getError)(void); + +typedef const unsigned char *(*P_glGetString)(unsigned int); +typedef void (*P_glGetIntegerv)(unsigned int, int *); +typedef void (*P_glClearColor)(float, float, float, float); +typedef void (*P_glClear)(unsigned int); +typedef void (*P_glFinish)(void); +typedef void (*P_glReadPixels)(int, int, int, int, unsigned int, unsigned int, void *); +typedef unsigned int (*P_glGetError)(void); +typedef void (*P_glGenTextures)(int, unsigned int *); +typedef void (*P_glBindTexture)(unsigned int, unsigned int); +typedef void (*P_glTexImage2D)(unsigned int, int, int, int, int, int, unsigned int, unsigned int, const void *); +typedef void (*P_glTexParameteri)(unsigned int, unsigned int, int); +typedef void (*P_glGenFramebuffers)(int, unsigned int *); +typedef void (*P_glBindFramebuffer)(unsigned int, unsigned int); +typedef void (*P_glFramebufferTexture2D)(unsigned int, unsigned int, unsigned int, unsigned int, int); +typedef unsigned int (*P_glCheckFramebufferStatus)(unsigned int); +typedef void (*P_glViewport)(int, int, int, int); + +static void *g_lib; +static void *S(const char *n) { return dlsym(g_lib, n); } + +static void on_image(void *ctx, AImageReader *r) { + (void)ctx; + AImage *img = NULL; + /* Drain the queue, or the producer blocks once maxImages are in flight. */ + if (AImageReader_acquireNextImage(r, &img) == AMEDIA_OK && img) AImage_delete(img); +} + +#define DIM 256 + +static int near8(unsigned got, int want, int tol) { + int d = (int)got - want; + return d <= tol && d >= -tol; +} + +int main(int argc, char **argv) { + const char *backend = "DirectGLES"; + const char *surface = "pbuffer"; + const char *libpath = "libMobileGL.so"; + + for (int i = 1; i < argc; ++i) { + if (!strcmp(argv[i], "--backend") && i + 1 < argc) backend = argv[++i]; + else if (!strcmp(argv[i], "--surface") && i + 1 < argc) surface = argv[++i]; + else if (!strcmp(argv[i], "--lib") && i + 1 < argc) libpath = argv[++i]; + else { + fprintf(stderr, "usage: %s [--backend DirectGLES|DirectVulkan]" + " [--surface pbuffer|imagereader] [--lib path]\n", argv[0]); + return 2; + } + } + setvbuf(stdout, NULL, _IONBF, 0); + + /* MobileGL parses its config from an ELF constructor, so the backend must be + * selected before the library is mapped. */ + setenv("MOBILEGL_BACKEND_TYPE", backend, 1); + printf("mgprobe backend=%s surface=%s lib=%s\n", backend, surface, libpath); + + int useWindow = !strcmp(surface, "imagereader"); + ANativeWindow *win = NULL; + AImageReader *reader = NULL; + if (useWindow) { + if (AImageReader_newWithUsage(DIM, DIM, AIMAGE_FORMAT_RGBA_8888, + AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | + AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT, + 4, &reader) != AMEDIA_OK || !reader) { + printf("FAIL AImageReader_newWithUsage\n"); + return 3; + } + AImageReader_ImageListener l = {NULL, on_image}; + AImageReader_setImageListener(reader, &l); + if (AImageReader_getWindow(reader, &win) != AMEDIA_OK || !win) { + printf("FAIL AImageReader_getWindow\n"); + return 3; + } + } + + g_lib = dlopen(libpath, RTLD_NOW | RTLD_LOCAL); + if (!g_lib) { + printf("FAIL dlopen: %s\n", dlerror()); + return 4; + } + + P_getDisplay eglGetDisplay_ = (P_getDisplay)S("eglGetDisplay"); + P_initialize eglInitialize_ = (P_initialize)S("eglInitialize"); + P_bindAPI eglBindAPI_ = (P_bindAPI)S("eglBindAPI"); + P_chooseConfig eglChooseConfig_ = (P_chooseConfig)S("eglChooseConfig"); + P_getConfigAttrib eglGetConfigAttrib_ = (P_getConfigAttrib)S("eglGetConfigAttrib"); + P_createWindowSurface eglCreateWindowSurface_ = (P_createWindowSurface)S("eglCreateWindowSurface"); + P_createPbufferSurface eglCreatePbufferSurface_ = (P_createPbufferSurface)S("eglCreatePbufferSurface"); + P_createContext eglCreateContext_ = (P_createContext)S("eglCreateContext"); + P_makeCurrent eglMakeCurrent_ = (P_makeCurrent)S("eglMakeCurrent"); + P_getError eglGetError_ = (P_getError)S("eglGetError"); + + if (!eglGetDisplay_ || !eglInitialize_ || !eglChooseConfig_ || !eglCreateContext_ || !eglMakeCurrent_) { + printf("FAIL missing core EGL exports\n"); + return 5; + } + + EGLDisplay dpy = eglGetDisplay_(EGL_DEFAULT_DISPLAY); + EGLint vmaj = 0, vmin = 0; + if (!eglInitialize_(dpy, &vmaj, &vmin)) { + printf("FAIL eglInitialize err=0x%x\n", eglGetError_ ? eglGetError_() : 0); + return 6; + } + if (eglBindAPI_ && !eglBindAPI_(EGL_OPENGL_API)) { + printf("FAIL eglBindAPI(EGL_OPENGL_API) err=0x%x\n", eglGetError_ ? eglGetError_() : 0); + return 7; + } + + const EGLint cfgAttribs[] = { + EGL_SURFACE_TYPE, useWindow ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, + EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, + EGL_DEPTH_SIZE, 24, EGL_STENCIL_SIZE, 8, + EGL_NONE}; + EGLConfig cfg = 0; + EGLint ncfg = 0; + if (!eglChooseConfig_(dpy, cfgAttribs, &cfg, 1, &ncfg) || ncfg < 1) { + printf("FAIL eglChooseConfig n=%d err=0x%x\n", ncfg, eglGetError_ ? eglGetError_() : 0); + return 8; + } + + EGLSurface surf; + if (useWindow) { + EGLint vis = 0; + if (eglGetConfigAttrib_ && eglGetConfigAttrib_(dpy, cfg, EGL_NATIVE_VISUAL_ID, &vis) && vis) + ANativeWindow_setBuffersGeometry(win, DIM, DIM, vis); + surf = eglCreateWindowSurface_(dpy, cfg, (EGLNativeWindowType)win, NULL); + } else { + const EGLint sa[] = {EGL_WIDTH, DIM, EGL_HEIGHT, DIM, EGL_NONE}; + surf = eglCreatePbufferSurface_(dpy, cfg, sa); + } + if (surf == EGL_NO_SURFACE) { + printf("FAIL create%sSurface err=0x%x\n", useWindow ? "Window" : "Pbuffer", + eglGetError_ ? eglGetError_() : 0); + return 9; + } + + const EGLint ctxAttribs[] = { + EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, + EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, EGL_NONE}; + EGLContext ctx = eglCreateContext_(dpy, cfg, EGL_NO_CONTEXT, ctxAttribs); + if (ctx == EGL_NO_CONTEXT) { + printf("FAIL eglCreateContext(3.3 core) err=0x%x\n", eglGetError_ ? eglGetError_() : 0); + return 10; + } + /* MobileGL requires draw == read and rejects EGL_NO_SURFACE. */ + if (!eglMakeCurrent_(dpy, surf, surf, ctx)) { + printf("FAIL eglMakeCurrent err=0x%x\n", eglGetError_ ? eglGetError_() : 0); + return 11; + } + + P_glGetString glGetString_ = (P_glGetString)S("glGetString"); + P_glGetIntegerv glGetIntegerv_ = (P_glGetIntegerv)S("glGetIntegerv"); + P_glClearColor glClearColor_ = (P_glClearColor)S("glClearColor"); + P_glClear glClear_ = (P_glClear)S("glClear"); + P_glFinish glFinish_ = (P_glFinish)S("glFinish"); + P_glReadPixels glReadPixels_ = (P_glReadPixels)S("glReadPixels"); + P_glGetError glGetError_ = (P_glGetError)S("glGetError"); + P_glGenTextures glGenTextures_ = (P_glGenTextures)S("glGenTextures"); + P_glBindTexture glBindTexture_ = (P_glBindTexture)S("glBindTexture"); + P_glTexImage2D glTexImage2D_ = (P_glTexImage2D)S("glTexImage2D"); + P_glTexParameteri glTexParameteri_ = (P_glTexParameteri)S("glTexParameteri"); + P_glGenFramebuffers glGenFramebuffers_ = (P_glGenFramebuffers)S("glGenFramebuffers"); + P_glBindFramebuffer glBindFramebuffer_ = (P_glBindFramebuffer)S("glBindFramebuffer"); + P_glFramebufferTexture2D glFramebufferTexture2D_ = (P_glFramebufferTexture2D)S("glFramebufferTexture2D"); + P_glCheckFramebufferStatus glCheckFramebufferStatus_ = (P_glCheckFramebufferStatus)S("glCheckFramebufferStatus"); + P_glViewport glViewport_ = (P_glViewport)S("glViewport"); + + int major = -1, minor = -1, profile = -1; + glGetIntegerv_(GL_MAJOR_VERSION, &major); + glGetIntegerv_(GL_MINOR_VERSION, &minor); + glGetIntegerv_(GL_CONTEXT_PROFILE_MASK, &profile); + printf(" GL_VENDOR %s\n", (const char *)glGetString_(GL_VENDOR)); + printf(" GL_RENDERER %s\n", (const char *)glGetString_(GL_RENDERER)); + printf(" GL_VERSION %s\n", (const char *)glGetString_(GL_VERSION)); + printf(" GLSL %s\n", (const char *)glGetString_(GL_SHADING_LANGUAGE_VERSION)); + printf(" version %d.%d profile_mask 0x%x %s\n", major, minor, profile, + (profile & 1) ? "(core)" : "(NOT CORE)"); + + unsigned char px[4]; + + /* Default framebuffer. */ + glClearColor_(0.25f, 0.5f, 0.75f, 1.0f); + glClear_(GL_COLOR_BUFFER_BIT); + if (glFinish_) glFinish_(); + memset(px, 0, sizeof px); + glReadPixels_(DIM / 2, DIM / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px); + int defOk = near8(px[0], 64, 10) && near8(px[1], 128, 10) && near8(px[2], 191, 10); + printf(" default-FB readback (%u,%u,%u,%u) %s\n", px[0], px[1], px[2], px[3], + defOk ? "ok" : "BROKEN"); + + /* User FBO - this is what dEQP uses with --deqp-surface-type=fbo. */ + unsigned int tex = 0, fbo = 0; + glGenTextures_(1, &tex); + glBindTexture_(GL_TEXTURE_2D, tex); + glTexImage2D_(GL_TEXTURE_2D, 0, GL_RGBA8, DIM, DIM, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + glTexParameteri_(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri_(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glGenFramebuffers_(1, &fbo); + glBindFramebuffer_(GL_FRAMEBUFFER, fbo); + glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); + unsigned int fbst = glCheckFramebufferStatus_(GL_FRAMEBUFFER); + int fboOk = 0; + if (fbst == GL_FRAMEBUFFER_COMPLETE) { + glViewport_(0, 0, DIM, DIM); + glClearColor_(0.9f, 0.2f, 0.4f, 1.0f); + glClear_(GL_COLOR_BUFFER_BIT); + if (glFinish_) glFinish_(); + memset(px, 0, sizeof px); + glReadPixels_(DIM / 2, DIM / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px); + fboOk = near8(px[0], 230, 10) && near8(px[1], 51, 10) && near8(px[2], 102, 10); + printf(" user-FBO readback (%u,%u,%u,%u) %s\n", px[0], px[1], px[2], px[3], + fboOk ? "ok" : "BROKEN"); + } else { + printf(" user-FBO incomplete status=0x%x\n", fbst); + } + + unsigned glerr = glGetError_ ? glGetError_() : 0; + int ok = fboOk && (major > 3 || (major == 3 && minor >= 3)) && (profile & 1) && glerr == 0; + printf("%s backend=%s surface=%s default_fb=%s user_fbo=%s glerr=0x%x\n", + ok ? "PASS" : "FAIL", backend, surface, defOk ? "ok" : "broken", + fboOk ? "ok" : "broken", glerr); + + fflush(stdout); + /* MobileGL aborts in static teardown; leave before that runs. */ + _exit(ok ? 0 : 1); +} diff --git a/tools/cts/scripts/qpa_report.py b/tools/cts/scripts/qpa_report.py new file mode 100644 index 00000000..faa39091 --- /dev/null +++ b/tools/cts/scripts/qpa_report.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python +"""Summarise dEQP/glcts .qpa logs into a conformance pass rate. + +Handles the two ways a case can end in a .qpa: a normal +``#beginTestCaseResult``/``#endTestCaseResult`` pair carrying a +```` element, and ``#terminateTestCaseResult ``, +which is what the log contains when the process died partway through a case. +Cases that were started but never terminated (the run was killed) are reported +separately so a truncated chunk is never silently scored as a pass. + +Usage: + python qpa_report.py [ ...] [--json out.json] [--top N] +""" + +import argparse +import json +import os +import re +import sys +from collections import Counter, defaultdict + +# Khronos conformance treats these as non-failures: the test either passed or +# the implementation legitimately does not expose the feature under test. +NON_FAILURE = { + "Pass", + "NotSupported", + "QualityWarning", + "CompatibilityWarning", + "Waiver", +} + +# Statuses that indicate the case did not merely fail but destabilised the run. +HARD = {"Crash", "Timeout", "InternalError", "ResourceError", "DeviceHang"} + +CASE_START = re.compile(r"^#beginTestCaseResult\s+(\S+)") +CASE_END = re.compile(r"^#endTestCaseResult") +CASE_TERM = re.compile(r"^#terminateTestCaseResult\s+(.*)") +RESULT = re.compile(r' 1 else case + + +def load_sidecar(paths, name): + """Case names run_cts.py recorded in one of its sidecar lists.""" + out = set() + for p in paths: + d = p if os.path.isdir(p) else os.path.dirname(p) + f = os.path.join(d, name) + if os.path.isfile(f): + with open(f, "r", encoding="utf-8") as fh: + out.update(l.strip() for l in fh if l.strip() and not l.strip().startswith("#")) + return out + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("paths", nargs="+") + ap.add_argument("--json", dest="json_out") + ap.add_argument("--top", type=int, default=25) + ap.add_argument("--label", default="") + args = ap.parse_args() + + files = collect(args.paths) + if not files: + print("no .qpa files found", file=sys.stderr) + return 2 + + # Later chunks may re-run a case; last result wins. + results = {} + for f in files: + for case, status in parse_qpa(f): + results[case] = status + + # A case the runner saw take the process down is a Crash, not merely an + # unterminated log entry - but a real result from a later retry wins. + for case in load_sidecar(args.paths, "crashed.txt"): + if results.get(case, "Incomplete") == "Incomplete": + results[case] = "Crash" + # Worse than a crash: these rebooted the device. + for case in load_sidecar(args.paths, "hung.txt"): + if results.get(case, "Incomplete") in ("Incomplete", "Crash"): + results[case] = "DeviceHang" + + # Cases excluded up front, and cases the run never reached, are not results. + # Report them separately so a partial run is never read as a complete one. + skipped = load_sidecar(args.paths, "skipped.txt") + unrun = load_sidecar(args.paths, "unrun.txt") - set(results) + + counts = Counter(results.values()) + total = len(results) + non_fail = sum(counts[s] for s in NON_FAILURE) + strict_pass = counts["Pass"] + failures = total - non_fail + + by_group_fail = defaultdict(int) + by_group_total = defaultdict(int) + for case, status in results.items(): + g = group_of(case) + by_group_total[g] += 1 + if status not in NON_FAILURE: + by_group_fail[g] += 1 + + label = f" [{args.label}]" if args.label else "" + print(f"=== glcts conformance summary{label} ===") + print(f"files parsed : {len(files)}") + print(f"cases with result : {total}") + print() + for status, n in counts.most_common(): + mark = " " if status in NON_FAILURE else " ! " + print(f"{mark}{status:<22} {n:>7} {100.0 * n / total:6.2f}%") + print() + if total: + print(f"conformance pass rate (Pass+NotSupported+warnings) : {100.0 * non_fail / total:6.2f}% ({non_fail}/{total})") + print(f"strict pass rate (Pass only) : {100.0 * strict_pass / total:6.2f}% ({strict_pass}/{total})") + print(f"failures : {failures}") + + if skipped or unrun: + print("\n--- NOT MEASURED (excluded from the rates above) ---") + if skipped: + print(f" quarantined up front : {len(skipped)}") + if unrun: + print(f" never reached : {len(unrun)}") + print(" The rates above cover only cases that produced a result.") + + if failures: + print(f"\n--- worst groups (of {len(by_group_total)}) ---") + worst = sorted(by_group_fail.items(), key=lambda kv: -kv[1])[: args.top] + for g, nf in worst: + nt = by_group_total[g] + print(f" {g:<52} {nf:>6}/{nt:<6} fail ({100.0 * nf / nt:5.1f}%)") + + if args.json_out: + with open(args.json_out, "w", encoding="utf-8") as fh: + json.dump( + { + "label": args.label, + "files": len(files), + "total": total, + "counts": dict(counts), + "non_failure": non_fail, + "strict_pass": strict_pass, + "failures": failures, + "pass_rate": (non_fail / total) if total else 0.0, + "strict_pass_rate": (strict_pass / total) if total else 0.0, + "results": results, + }, + fh, + indent=1, + ) + print(f"\nwrote {args.json_out}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/cts/scripts/run_cts.py b/tools/cts/scripts/run_cts.py new file mode 100644 index 00000000..f827f9fb --- /dev/null +++ b/tools/cts/scripts/run_cts.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python +"""Drive a glcts run on a device, resuming across crashes. + +MobileGL crashes on some cases, and glcts takes the whole process down with it. +A single invocation would therefore stop at the first crash and leave most of +the suite unmeasured. This runner re-invokes glcts with only the cases that have +not produced a result yet, records each crashed case as "Crash", and repeats +until the list is exhausted, so one bad case costs one case rather than the run. + +Usage: + python run_cts.py --serial --backend DirectGLES|DirectVulkan \\ + --caselist --outdir [--device-dir /data/local/tmp/mgcts] +""" + +import argparse +import os +import re +import subprocess +import sys +import time + +CASE_START = re.compile(r"^#beginTestCaseResult\s+(\S+)") +CASE_END = re.compile(r"^#endTestCaseResult") +CASE_TERM = re.compile(r"^#terminateTestCaseResult\s+(.*)") + + +def adb(serial, *args, timeout=None): + try: + return subprocess.run(["adb", "-s", serial, *args], capture_output=True, text=True, timeout=timeout) + except subprocess.TimeoutExpired: + return subprocess.CompletedProcess(args, returncode=124, stdout="", stderr="adb timeout") + + +def device_alive(serial, timeout=30): + """True only if the device answers a trivial shell command. + + Distinguishes "glcts crashed" from "the device fell over". Without this a + dead device looks like every remaining case crashing, which silently turns a + broken run into a plausible-looking conformance number. + """ + r = adb(serial, "shell", "echo alive", timeout=timeout) + return r.returncode == 0 and "alive" in (r.stdout or "") + + +def wait_for_device(serial, attempts=20, delay=15): + for i in range(attempts): + if device_alive(serial): + return True + print(f"[run_cts] device {serial} unresponsive, waiting ({i + 1}/{attempts})") + time.sleep(delay) + return False + + +def mem_available_kb(serial): + r = adb(serial, "shell", "grep MemAvailable /proc/meminfo", timeout=30) + m = re.search(r"(\d+)", r.stdout or "") + return int(m.group(1)) if m else None + + +def completed_cases(qpa_path): + """Return (finished_case_names, last_started_case_or_None). + + A case that was started but never closed is the one the process died in. + """ + finished = [] + current = None + if not os.path.exists(qpa_path): + return finished, None + with open(qpa_path, "r", encoding="utf-8", errors="replace") as fh: + for line in fh: + m = CASE_START.match(line) + if m: + current = m.group(1) + continue + if current is not None and (CASE_END.match(line) or CASE_TERM.match(line)): + finished.append(current) + current = None + return finished, current + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--serial", required=True) + ap.add_argument("--backend", required=True, choices=["DirectGLES", "DirectVulkan"]) + ap.add_argument("--caselist", required=True) + ap.add_argument("--outdir", required=True) + ap.add_argument("--device-dir", default="/data/local/tmp/mgcts") + ap.add_argument("--surface", default="fbo", help="--deqp-surface-type value") + ap.add_argument("--max-rounds", type=int, default=4000) + ap.add_argument("--max-empty-streak", type=int, default=8, + help="abort after this many consecutive chunks that produce no log at all") + ap.add_argument("--min-mem-kb", type=int, default=400000, + help="pause when the device drops below this much available memory") + ap.add_argument("--chunk-timeout", type=int, default=900, + help="seconds before giving up on one glcts invocation (a GPU hang never returns)") + ap.add_argument("--skip-file", default=None, + help="file of case names to exclude, e.g. cases known to hang the device") + args = ap.parse_args() + + os.makedirs(args.outdir, exist_ok=True) + + with open(args.caselist, "r", encoding="utf-8") as fh: + remaining = [l.strip() for l in fh if l.strip() and not l.strip().startswith("#")] + + skipped = [] + if args.skip_file and os.path.isfile(args.skip_file): + with open(args.skip_file, "r", encoding="utf-8") as fh: + skip = {l.strip() for l in fh if l.strip() and not l.strip().startswith("#")} + skipped = [c for c in remaining if c in skip] + remaining = [c for c in remaining if c not in skip] + print(f"[run_cts] skipping {len(skipped)} case(s) from {args.skip_file}") + + total = len(remaining) + print(f"[run_cts] {args.backend} on {args.serial}: {total} cases") + + crashed = [] + hung = [] + done = set() + chunk = 0 + started = time.time() + empty_streak = 0 + + if not wait_for_device(args.serial): + print("[run_cts] device not responding before start; aborting", file=sys.stderr) + return 3 + + while remaining and chunk < args.max_rounds: + listfile = os.path.join(args.outdir, "remaining.txt") + with open(listfile, "w", encoding="utf-8", newline="\n") as fh: + fh.write("\n".join(remaining) + "\n") + + # Repeated process launches plus crash tombstones can drive the device + # into memory pressure; give it room rather than pushing it over. + mem = mem_available_kb(args.serial) + if mem is not None and mem < args.min_mem_kb: + print(f"[run_cts] low memory ({mem} kB available); pausing 30 s") + time.sleep(30) + + dev_list = f"{args.device_dir}/remaining.txt" + dev_qpa = f"{args.device_dir}/chunk.qpa" + push = adb(args.serial, "push", listfile, dev_list, timeout=120) + if push.returncode != 0: + print(f"[run_cts] push failed ({push.stderr.strip()}); treating as device trouble", + file=sys.stderr) + if not wait_for_device(args.serial): + print("[run_cts] ABORTING: device unreachable.", file=sys.stderr) + break + continue + adb(args.serial, "shell", f"rm -f {dev_qpa}", timeout=60) + + cmd = ( + f"cd {args.device_dir} && " + f"MOBILEGL_BACKEND_TYPE={args.backend} LD_LIBRARY_PATH=. " + f"./glcts --deqp-caselist-file={dev_list} " + f"--deqp-surface-type={args.surface} " + f"--deqp-terminate-on-device-lost=disable " + f"--deqp-log-images=disable --deqp-log-shader-sources=disable " + f"--deqp-log-filename={dev_qpa} > /dev/null 2>&1; echo RC=$?" + ) + run = adb(args.serial, "shell", cmd, timeout=args.chunk_timeout) + if run.returncode == 124: + print(f"[run_cts] chunk {chunk:04d} timed out after {args.chunk_timeout}s " + f"(likely a GPU hang)", file=sys.stderr) + + # Some cases hang the GPU hard enough to reboot the device. The log on + # /data/local/tmp survives that, so wait for the device to come back and + # pull it anyway rather than losing the whole chunk. + rebooted = False + if not device_alive(args.serial, timeout=30): + print(f"[run_cts] device went away during chunk {chunk:04d}; waiting for it", + file=sys.stderr) + if not wait_for_device(args.serial, attempts=40, delay=15): + print("[run_cts] ABORTING: device never came back. Results are incomplete; " + "do NOT treat the remaining cases as failures.", file=sys.stderr) + break + rebooted = True + print("[run_cts] device is back") + + local_qpa = os.path.join(args.outdir, f"chunk{chunk:04d}.qpa") + pull = adb(args.serial, "pull", dev_qpa, local_qpa, timeout=300) + if pull.returncode != 0 and rebooted: + time.sleep(10) + adb(args.serial, "pull", dev_qpa, local_qpa, timeout=300) + + finished, in_flight = completed_cases(local_qpa) + for c in finished: + done.add(c) + + progressed = len(finished) + if progressed > 0: + empty_streak = 0 + if in_flight is not None: + # The case that was open when the process (or the device) died. + if rebooted: + # It took the whole device down: quarantine it, or the next + # invocation walks straight back into it. + print(f"[run_cts] DEVICE HANG in {in_flight} - quarantining it") + hung.append(in_flight) + else: + crashed.append(in_flight) + done.add(in_flight) + progressed += 1 + elif progressed == 0: + # Nothing at all came back. Either the first remaining case takes + # the process down before the log is flushed, or the device died. + # Those look identical from here, so confirm the device is alive + # before blaming the test. + if not device_alive(args.serial): + print(f"[run_cts] device went away during chunk {chunk:04d}", file=sys.stderr) + if not wait_for_device(args.serial): + print("[run_cts] ABORTING: device never came back. Results are " + "incomplete; do NOT treat the remaining cases as crashes.", file=sys.stderr) + break + print("[run_cts] device recovered; retrying the same chunk") + continue + + empty_streak += 1 + if empty_streak >= args.max_empty_streak: + print(f"[run_cts] ABORTING: {empty_streak} consecutive chunks produced no output " + f"while the device stayed reachable. Something systemic is wrong; refusing " + f"to label the rest of the suite as crashes.", file=sys.stderr) + break + + victim = remaining[0] + print(f"[run_cts] no output at all; recording {victim} as Crash") + crashed.append(victim) + done.add(victim) + progressed = 1 + + remaining = [c for c in remaining if c not in done] + elapsed = time.time() - started + print( + f"[run_cts] chunk {chunk:04d}: +{progressed} (done {len(done)}/{total}, " + f"crashes {len(crashed)}, {elapsed / 60:.1f} min)" + ) + chunk += 1 + + with open(os.path.join(args.outdir, "crashed.txt"), "w", encoding="utf-8", newline="\n") as fh: + fh.write("\n".join(crashed) + ("\n" if crashed else "")) + + # Cases that rebooted the device. Feed this back in via --skip-file to avoid + # paying for the same reboot on the next run. + with open(os.path.join(args.outdir, "hung.txt"), "w", encoding="utf-8", newline="\n") as fh: + fh.write("\n".join(hung) + ("\n" if hung else "")) + if hung: + print(f"[run_cts] {len(hung)} case(s) hung the device (see hung.txt):") + for c in hung: + print(f" {c}") + + # Anything still in `remaining` was never measured. Record it so the report + # cannot quietly present a partial run as a complete one. + with open(os.path.join(args.outdir, "unrun.txt"), "w", encoding="utf-8", newline="\n") as fh: + fh.write("\n".join(remaining) + ("\n" if remaining else "")) + if skipped: + with open(os.path.join(args.outdir, "skipped.txt"), "w", encoding="utf-8", newline="\n") as fh: + fh.write("\n".join(skipped) + "\n") + + if remaining: + print(f"[run_cts] WARNING: {len(remaining)} cases were never run (see unrun.txt)", file=sys.stderr) + print(f"[run_cts] finished: {len(done)}/{total} cases, {len(crashed)} crashes, {chunk} invocations") + print(f"[run_cts] qpa chunks in {args.outdir}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/cts/scripts/sync_to_cts.py b/tools/cts/scripts/sync_to_cts.py new file mode 100644 index 00000000..de7d5db4 --- /dev/null +++ b/tools/cts/scripts/sync_to_cts.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +"""Copy the MobileGL dEQP platform port into a VK-GL-CTS checkout. + +The port is version-controlled here, in the MobileGL repo, so it survives a +throwaway CTS clone. This drops it into the places VK-GL-CTS expects: + + framework/platform/mobilegl/ <- platform sources + targets/mobilegl/mobilegl.cmake <- target definition (-DDEQP_TARGET=mobilegl) + +Usage: + python sync_to_cts.py +""" + +import os +import shutil +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +CTS_TOOLS = os.path.dirname(HERE) + +COPIES = [ + (os.path.join(CTS_TOOLS, "platform"), "framework/platform/mobilegl", None), + (os.path.join(CTS_TOOLS, "targets"), "targets/mobilegl", ["mobilegl.cmake", "ndk-modern.cmake"]), +] + + +def main(): + if len(sys.argv) != 2: + print(__doc__) + return 2 + cts = sys.argv[1] + if not os.path.isfile(os.path.join(cts, "CMakeLists.txt")): + print(f"error: {cts} does not look like a VK-GL-CTS checkout", file=sys.stderr) + return 1 + + for src, reldst, only in COPIES: + dst = os.path.join(cts, reldst) + os.makedirs(dst, exist_ok=True) + for name in sorted(os.listdir(src)): + if only is not None and name not in only: + continue + s = os.path.join(src, name) + if not os.path.isfile(s): + continue + shutil.copy2(s, os.path.join(dst, name)) + print(f" {reldst}/{name}") + + print("\nsynced. configure with -DDEQP_TARGET=mobilegl") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/cts/skills/README.md b/tools/cts/skills/README.md new file mode 100644 index 00000000..6b5831af --- /dev/null +++ b/tools/cts/skills/README.md @@ -0,0 +1,18 @@ +# MobileGL conformance-suite skills + +Task-focused skills for running Khronos conformance suites against MobileGL. +Each skill is a self-contained package, matching the layout used by +`tools/trace_replay/skills/`: + +- `SKILL.md` — the skill (frontmatter `name` + `description`, then the body). The + directory name equals the frontmatter `name`. +- `agents/openai.yaml` — OpenAI agent descriptor (`display_name`, + `short_description`, `default_prompt`). +- `scripts/` and/or `references/` — bundled tooling and supporting docs, when the + skill has them. + +## Skills + +| Skill | What it does | +| --- | --- | +| [gl-cts-on-mobilegl](gl-cts-on-mobilegl/SKILL.md) | Build VK-GL-CTS `glcts` as a standalone Android arm64 binary against MobileGL's own EGL, run KHR-GL33, and report a per-backend OpenGL 3.3 core conformance rate. | diff --git a/tools/cts/skills/gl-cts-on-mobilegl/SKILL.md b/tools/cts/skills/gl-cts-on-mobilegl/SKILL.md new file mode 100644 index 00000000..3a40b805 --- /dev/null +++ b/tools/cts/skills/gl-cts-on-mobilegl/SKILL.md @@ -0,0 +1,192 @@ +--- +name: gl-cts-on-mobilegl +description: Run the Khronos OpenGL CTS (VK-GL-CTS glcts, KHR-GL33) against MobileGL on an Android device and compute a per-backend conformance rate. Use when measuring OpenGL 3.3 core conformance for DirectGLES or DirectVulkan, building glcts for Android arm64, porting a dEQP tcu::Platform onto MobileGL, or triaging CTS failures, crashes, and cases that hang the device. +--- + +# OpenGL CTS on MobileGL (Android) + +## Overview + +`glcts` from VK-GL-CTS is built as a **standalone arm64 executable** and run from +`adb shell`. It reaches OpenGL only through `libMobileGL.so`, which supplies both +EGL and desktop GL, so a result is unambiguously MobileGL's and never the system +GL stack's. No APK and no Activity are involved. + +The port lives in this repository under `MobileGL/tools/cts/` and is copied into +a VK-GL-CTS checkout by `scripts/sync_to_cts.py`, so it survives a throwaway CTS +clone. + +Set up paths first: + +```sh +export MG= # do builds in a worktree, not the shared tree +export CTS= +export NDK="$ANDROID_HOME/ndk/27.3.13750724" +export SERIAL= +``` + +## Prerequisites + +- Android NDK r27 (the repo builds MobileGL with 27.3.13750724), CMake, Ninja, Python 3. +- A rooted-or-not Android device with `adb`; ~600 MB free under `/data/local/tmp`. +- **A device you can physically power-cycle.** Some cases hang the GPU hard + enough to reboot it — see "Cases that take the device down". +- On Windows, invoke `python`, not `python3`: the latter resolves to the + Microsoft Store alias stub and exits 49. + +## Step 1 — build libMobileGL.so + +Build in a git worktree (other agents share the main tree). A fresh worktree is +missing glslang's bundled SPIR-V Tools, which is a hard configure blocker +because `ENABLE_OPT` is forced on: + +```sh +cp -r /3rdparty/glslang/External/* "$MG/3rdparty/glslang/External/" +./gradlew -p "$MG/android-plugin" :app:assembleTraceRelease +``` + +The stripped library lands in +`android-plugin/app/build/intermediates/stripped_native_libs/traceRelease/.../arm64-v8a/libMobileGL.so`. + +## Step 2 — get VK-GL-CTS and its externals + +Use a **release tag**, not `main`, so the mustpass list — and therefore the +reported rate — is citable: + +```sh +git -C "$CTS" checkout opengl-cts-4.6.8.1 +cd "$CTS" && python external/fetch_sources.py +``` + +## Step 3 — build glcts for Android arm64 + +```sh +python "$MG/tools/cts/scripts/sync_to_cts.py" "$CTS" + +cmake -S "$CTS" -B build-cts-a64 -G Ninja \ + -DDEQP_TARGET=mobilegl -DDEQP_TARGET_TOOLCHAIN=ndk-modern \ + -DANDROID_NDK_PATH="$NDK" -DDE_ANDROID_API=26 -DANDROID_ABI=arm64-v8a \ + -DCMAKE_BUILD_TYPE=Release +ninja -C build-cts-a64 glcts +"$NDK"/toolchains/llvm/prebuilt/*/bin/llvm-strip build-cts-a64/external/openglcts/modules/glcts +``` + +Confirm the configure output says `DE_OS = DE_OS_ANDROID`, `DE_CPU = +DE_CPU_ARM_64` and `DEQP_ANDROID_BUILD = EXE`. Two things make that work and +both are easy to get wrong: + +- `DEQP_TARGET_TOOLCHAIN=ndk-modern` is required. dEQP includes `Defs.cmake` + *before* the target file, so a target cannot set `DE_OS` itself. Without the + toolchain hook the build mis-detects as `DE_OS_UNIX`/`x86_64` and dies on + `__assert_fail` (bionic has `__assert2`). +- The target sets `DEQP_ANDROID_EXE ON`. Otherwise dEQP builds the modules into + the `libdeqp.so` an APK would load and no `glcts` executable exists. + +`KHR-GL33` needs no ungating — the package registry registers it unconditionally; +only the `dEQP-*` packages are `#if DE_OS != DE_OS_ANDROID`. + +## Step 4 — deploy + +```sh +adb -s $SERIAL shell mkdir -p /data/local/tmp/mgcts +adb -s $SERIAL push build-cts-a64/external/openglcts/modules/glcts /data/local/tmp/mgcts/ +adb -s $SERIAL push build-cts-a64/external/openglcts/modules/gl_cts /data/local/tmp/mgcts/ +adb -s $SERIAL push /data/local/tmp/mgcts/ +adb -s $SERIAL shell chmod 755 /data/local/tmp/mgcts/glcts +``` + +## Step 5 — preflight + +Never start a multi-hour run without this. It proves the device/library pair +yields a 3.3 core context and that FBO readback is correct, in about a second: + +```sh +adb -s $SERIAL shell 'cd /data/local/tmp/mgcts && LD_LIBRARY_PATH=. ./mgprobe \ + --backend DirectVulkan --surface imagereader --lib ./libMobileGL.so' +``` + +Expect `PASS ... user_fbo=ok`. `default_fb=broken` on DirectVulkan is expected +and does not gate — see below. + +## Step 6 — run + +```sh +python "$MG/tools/cts/scripts/run_cts.py" \ + --serial $SERIAL --backend DirectGLES \ + --caselist .../mustpass/gl/khronos_mustpass/main/gl33-main.txt \ + --outdir runs/gles --skip-file runs/skip.txt +``` + +The runner re-invokes `glcts` with only the cases that have no result yet, so a +crash costs one case rather than the run. It distinguishes a crashed *case* from +a dead *device* by checking the device still answers a shell command — without +that check a dead device looks like every remaining case crashing, which yields +a completely bogus but plausible-looking conformance number. On a device reboot +it waits, re-pulls the partial `.qpa` (which survives on `/data/local/tmp`), +records the case that was open as `DeviceHang`, and quarantines it. + +## Step 7 — report + +```sh +python "$MG/tools/cts/scripts/qpa_report.py" runs/gles --label DirectGLES +``` + +Pass rate counts `Pass`, `NotSupported`, `QualityWarning`, `CompatibilityWarning` +and `Waiver` as non-failures, matching how Khronos scores a submission; the +strict rate counts only `Pass`. Quarantined and never-reached cases are reported +separately and excluded from the rates, so a partial run cannot read as a +complete one. + +## Required flags, and why + +| Flag | Why it is not optional | +| --- | --- | +| `--deqp-surface-type=fbo` | On DirectVulkan, `glReadPixels` from the **default framebuffer returns all zeros** with no GL error. dEQP verifies nearly everything through `glReadPixels`, so rendering to the surface scores DirectVulkan near zero for a reason unrelated to conformance. FBO readback is correct on both backends. Use it for **both** backends so the two numbers stay comparable. | +| `--deqp-terminate-on-device-lost=disable` | Defaults to *enable*, which calls `glGetGraphicsResetStatus()` after every case. That is GL 4.5 / `KHR_robustness`, absent from GL 3.3 core, so the pointer is null and the process segfaults on the first case. Desktop drivers expose the extension, which is why upstream never trips on it. | + +## Cases that take the device down + +Some cases hang the GPU hard enough that the device reboots or stops answering +adb entirely. Keep them in a `--skip-file`, and expect to find more: + +- `KHR-GL33.clip_distance.functional` — wedged an Adreno 750 tablet; it rebooted + and then stopped responding to adb altogether. +- `KHR-GL33.framebuffer_blit.multisampled_to_singlesampled_blit_color_config_test` + — rebooted an Adreno 830 phone after 862 cases, on DirectGLES. + +When a run dies, pull `/data/local/tmp/mgcts/chunk.qpa` — it survives the reboot, +and the last `#beginTestCaseResult` with no matching `#endTestCaseResult` names +the case that did it. + +## MobileGL constraints the port works around + +- **DirectVulkan cannot use an EGL pbuffer.** That path needs + `VK_EXT_headless_surface`, which Adreno's Android driver does not expose; it + fails inside `eglMakeCurrent`. The platform therefore gets a real + `ANativeWindow` from **`AImageReader`** — an ordinary BufferQueue producer that + `vkCreateAndroidSurfaceKHR` accepts, with no Activity. An `onImageAvailable` + listener must drain the queue or the producer blocks once `maxImages` buffers + are in flight and the next swap deadlocks. +- **`eglMakeCurrent` requires draw == read** and rejects `EGL_NO_SURFACE` with + `EGL_BAD_MATCH`, so dEQP's `surfaceless` platform cannot be used at all, and + `--deqp-surface-type=fbo` (which asks the platform for `SURFACETYPE_DONT_CARE`) + must still be given a real surface. +- **Every EGL call must go through the dynamically loaded library.** dEQP's + `surfaceless` platform mixes wrapper calls with globally linked `egl*` symbols; + copying that on Android silently reaches the system EGL and invalidates the + measurement. The `mobilegl` target links no `libEGL`/`libGLESv*` at all. +- **Desktop-GL configs need `EGL_OPENGL_BIT`.** The surfaceless port always asks + for an ES bit, which can never satisfy a GL 3.3 core context. +- MobileGL aborts during static teardown (`FORTIFY: pthread_mutex_lock called on + a destroyed mutex`) *after* the work is done; flush and `_exit()` in any small + tool, or its exit code and output are lost. + +## Contents + + platform/tcuMobileGLPlatform.{cpp,hpp} dEQP tcu::Platform for MobileGL + targets/mobilegl.cmake VK-GL-CTS target (-DDEQP_TARGET=mobilegl) + targets/ndk-modern.cmake NDK toolchain hook (sets DE_OS/DE_CPU) + probe/mgprobe.c preflight gate + scripts/sync_to_cts.py inject the port into a CTS checkout + scripts/run_cts.py crash- and reboot-resuming runner + scripts/qpa_report.py .qpa -> conformance rate diff --git a/tools/cts/skills/gl-cts-on-mobilegl/agents/openai.yaml b/tools/cts/skills/gl-cts-on-mobilegl/agents/openai.yaml new file mode 100644 index 00000000..c731da29 --- /dev/null +++ b/tools/cts/skills/gl-cts-on-mobilegl/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "OpenGL CTS on MobileGL (Android)" + short_description: "Build and run VK-GL-CTS KHR-GL33 against MobileGL and report per-backend conformance" + default_prompt: "Use $gl-cts-on-mobilegl to run the OpenGL 3.3 core CTS against MobileGL on my Android device and report the conformance rate for DirectGLES and DirectVulkan." diff --git a/tools/cts/targets/mobilegl.cmake b/tools/cts/targets/mobilegl.cmake new file mode 100644 index 00000000..021df7c1 --- /dev/null +++ b/tools/cts/targets/mobilegl.cmake @@ -0,0 +1,36 @@ +#------------------------------------------------------------------------- +# VK-GL-CTS target: MobileGL on Android +# +# Builds a standalone arm64 ELF that reaches OpenGL exclusively through +# libMobileGL.so, loaded at runtime. Nothing here links libEGL or libGLESv*: +# the whole point is that the system GL stack must not be reachable, so that a +# conformance result is unambiguously MobileGL's. +#------------------------------------------------------------------------- + +message("*** Using MobileGL target") + +set(DEQP_TARGET_NAME "MobileGL") + +# Build the modules as standalone executables instead of the libdeqp.so an APK +# would load. The suite runs from adb shell, with no Activity. +set(DEQP_ANDROID_EXE ON) + +# EGL comes from libMobileGL.so via the eglw dynamic wrapper, so the support +# flag is on but no import library is supplied. +set(DEQP_SUPPORT_EGL ON) +set(DEQP_EGL_LIBRARIES) +set(DEQP_GLES2_LIBRARIES) +set(DEQP_GLES3_LIBRARIES) + +set(TCUTIL_PLATFORM_SRCS + mobilegl/tcuMobileGLPlatform.cpp + mobilegl/tcuMobileGLPlatform.hpp + ) + +find_library(LOG_LIBRARY NAMES log) +find_library(ANDROID_LIBRARY NAMES android) +find_library(MEDIANDK_LIBRARY NAMES mediandk) + +# libmediandk supplies AImageReader, which is how a process with no Activity +# gets a real ANativeWindow. +list(APPEND TCUTIL_PLATFORM_LIBS ${ANDROID_LIBRARY} ${MEDIANDK_LIBRARY} ${LOG_LIBRARY}) diff --git a/tools/cts/targets/ndk-modern.cmake b/tools/cts/targets/ndk-modern.cmake new file mode 100644 index 00000000..cad1b3d1 --- /dev/null +++ b/tools/cts/targets/ndk-modern.cmake @@ -0,0 +1,61 @@ +#------------------------------------------------------------------------- +# drawElements CMake utilities +# ---------------------------- +# +# Copyright 2016 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +#------------------------------------------------------------------------- + +# Delegate most things to the NDK's cmake toolchain script + +if (NOT DEFINED ANDROID_NDK_PATH) + message(FATAL_ERROR "Please provide ANDROID_NDK_PATH") +endif () + +set(ANDROID_PLATFORM "android-${DE_ANDROID_API}") +set(ANDROID_STL c++_static) +set(ANDROID_CPP_FEATURES "rtti exceptions") + +include(${ANDROID_NDK_PATH}/build/cmake/android.toolchain.cmake) + +# The try_compile() used to verify the C/C++ compilers are sane tries to +# generate an executable, but doesn't seem to use the right compiler/linker +# options when cross-compiling, so it fails even when building an actual +# shared library or executable succeeds. +# +# I don't know why this doesn't affect simpler projects that use the NDK +# toolchain. +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) + +# Set variables used by other parts of dEQP's build scripts + +set(DE_OS "DE_OS_ANDROID") + +if (NOT DEFINED DE_COMPILER) + set(DE_COMPILER "DE_COMPILER_CLANG") +endif () + +if (ANDROID_ABI STREQUAL "x86") + set(DE_CPU "DE_CPU_X86") +elseif (ANDROID_ABI STREQUAL "armeabi" OR + ANDROID_ABI STREQUAL "armeabi-v7a") + set(DE_CPU "DE_CPU_ARM") +elseif (ANDROID_ABI STREQUAL "arm64-v8a") + set(DE_CPU "DE_CPU_ARM_64") +elseif (ANDROID_ABI STREQUAL "x86_64") + set(DE_CPU "DE_CPU_X86_64") +else () + message(FATAL_ERROR "Unknown ABI \"${ANDROID_ABI}\"") +endif () From 39f21e52ea678c97ea28f14fd48e650eabc6da36 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 26 Jul 2026 10:43:48 -0400 Subject: [PATCH 2/6] [Test] (CTS): isolate the DirectVulkan renderbuffer-FBO readback defect so the rest of KHR-GL33 can be measured --- .../0001-fbo-color-texture-attachment.patch | 109 ++++++++++++++++++ tools/cts/probe/mgprobe.c | 47 +++++++- tools/cts/scripts/run_cts.py | 5 +- tools/cts/skills/gl-cts-on-mobilegl/SKILL.md | 24 +++- 4 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 tools/cts/patches/0001-fbo-color-texture-attachment.patch diff --git a/tools/cts/patches/0001-fbo-color-texture-attachment.patch b/tools/cts/patches/0001-fbo-color-texture-attachment.patch new file mode 100644 index 00000000..b98c5ae3 --- /dev/null +++ b/tools/cts/patches/0001-fbo-color-texture-attachment.patch @@ -0,0 +1,109 @@ +diff --git a/framework/opengl/gluFboRenderContext.cpp b/framework/opengl/gluFboRenderContext.cpp +index 588cf7d2a..0721ffee7 100644 +--- a/framework/opengl/gluFboRenderContext.cpp ++++ b/framework/opengl/gluFboRenderContext.cpp +@@ -132,6 +132,7 @@ FboRenderContext::FboRenderContext(RenderContext *context, const RenderConfig &c + : m_context(context) + , m_framebuffer(0) + , m_colorBuffer(0) ++ , m_colorIsTexture(false) + , m_depthStencilBuffer(0) + , m_renderTarget() + { +@@ -151,6 +152,7 @@ FboRenderContext::FboRenderContext(const ContextFactory &factory, const RenderCo + : m_context(nullptr) + , m_framebuffer(0) + , m_colorBuffer(0) ++ , m_colorIsTexture(false) + , m_depthStencilBuffer(0) + , m_renderTarget() + { +@@ -215,19 +217,41 @@ void FboRenderContext::createFramebuffer(const RenderConfig &config) + height = (height == glu::RenderConfig::DONT_CARE) ? maxSize : height; + } + ++ // MOBILEGL: allow the colour attachment to be a texture instead of a ++ // renderbuffer. MobileGL's DirectVulkan backend returns zeros when reading ++ // back a renderbuffer-attached FBO, which makes every image comparison fail ++ // for one reason and hides everything else. Setting ++ // MOBILEGL_CTS_FBO_COLOR_TEXTURE=1 isolates that single defect so the rest ++ // of the suite can be measured. Off by default: stock behaviour. + { +- pixelFormat = getPixelFormat(colorFormat); ++ const char *useTexEnv = getenv("MOBILEGL_CTS_FBO_COLOR_TEXTURE"); ++ m_colorIsTexture = (useTexEnv && useTexEnv[0] == '1' && config.numSamples <= 0); + +- gl.genRenderbuffers(1, &m_colorBuffer); +- gl.bindRenderbuffer(GL_RENDERBUFFER, m_colorBuffer); ++ pixelFormat = getPixelFormat(colorFormat); + +- if (config.numSamples > 0) +- gl.renderbufferStorageMultisample(GL_RENDERBUFFER, config.numSamples, colorFormat, width, height); ++ if (m_colorIsTexture) ++ { ++ gl.genTextures(1, &m_colorBuffer); ++ gl.bindTexture(GL_TEXTURE_2D, m_colorBuffer); ++ gl.texStorage2D(GL_TEXTURE_2D, 1, colorFormat, width, height); ++ gl.texParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); ++ gl.texParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); ++ gl.bindTexture(GL_TEXTURE_2D, 0); ++ GLU_EXPECT_NO_ERROR(gl.getError(), "Creating color texture"); ++ } + else +- gl.renderbufferStorage(GL_RENDERBUFFER, colorFormat, width, height); +- +- gl.bindRenderbuffer(GL_RENDERBUFFER, 0); +- GLU_EXPECT_NO_ERROR(gl.getError(), "Creating color renderbuffer"); ++ { ++ gl.genRenderbuffers(1, &m_colorBuffer); ++ gl.bindRenderbuffer(GL_RENDERBUFFER, m_colorBuffer); ++ ++ if (config.numSamples > 0) ++ gl.renderbufferStorageMultisample(GL_RENDERBUFFER, config.numSamples, colorFormat, width, height); ++ else ++ gl.renderbufferStorage(GL_RENDERBUFFER, colorFormat, width, height); ++ ++ gl.bindRenderbuffer(GL_RENDERBUFFER, 0); ++ GLU_EXPECT_NO_ERROR(gl.getError(), "Creating color renderbuffer"); ++ } + } + + if (depthStencilFormat != GL_NONE) +@@ -250,7 +274,12 @@ void FboRenderContext::createFramebuffer(const RenderConfig &config) + gl.bindFramebuffer(GL_FRAMEBUFFER, m_framebuffer); + + if (m_colorBuffer) +- gl.framebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_colorBuffer); ++ { ++ if (m_colorIsTexture) ++ gl.framebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_colorBuffer, 0); ++ else ++ gl.framebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_colorBuffer); ++ } + + if (m_depthStencilBuffer) + { +@@ -290,7 +319,10 @@ void FboRenderContext::destroyFramebuffer(void) + + if (m_colorBuffer) + { +- gl.deleteRenderbuffers(1, &m_colorBuffer); ++ if (m_colorIsTexture) ++ gl.deleteTextures(1, &m_colorBuffer); ++ else ++ gl.deleteRenderbuffers(1, &m_colorBuffer); + m_colorBuffer = 0; + } + } +diff --git a/framework/opengl/gluFboRenderContext.hpp b/framework/opengl/gluFboRenderContext.hpp +index 75a0ff6b7..09ff1e7a9 100644 +--- a/framework/opengl/gluFboRenderContext.hpp ++++ b/framework/opengl/gluFboRenderContext.hpp +@@ -80,6 +80,7 @@ private: + RenderContext *m_context; + uint32_t m_framebuffer; + uint32_t m_colorBuffer; ++ bool m_colorIsTexture; + uint32_t m_depthStencilBuffer; + tcu::RenderTarget m_renderTarget; + }; diff --git a/tools/cts/probe/mgprobe.c b/tools/cts/probe/mgprobe.c index c2bbe123..915129ec 100644 --- a/tools/cts/probe/mgprobe.c +++ b/tools/cts/probe/mgprobe.c @@ -77,6 +77,7 @@ typedef void *EGLNativeWindowType; #define GL_TEXTURE_MIN_FILTER 0x2801 #define GL_TEXTURE_MAG_FILTER 0x2800 #define GL_NEAREST 0x2600 +#define GL_RENDERBUFFER 0x8D41 typedef EGLDisplay (*P_getDisplay)(EGLNativeDisplayType); typedef EGLBoolean (*P_initialize)(EGLDisplay, EGLint *, EGLint *); @@ -105,6 +106,10 @@ typedef void (*P_glBindFramebuffer)(unsigned int, unsigned int); typedef void (*P_glFramebufferTexture2D)(unsigned int, unsigned int, unsigned int, unsigned int, int); typedef unsigned int (*P_glCheckFramebufferStatus)(unsigned int); typedef void (*P_glViewport)(int, int, int, int); +typedef void (*P_glGenRenderbuffers)(int, unsigned int *); +typedef void (*P_glBindRenderbuffer)(unsigned int, unsigned int); +typedef void (*P_glRenderbufferStorage)(unsigned int, unsigned int, int, int); +typedef void (*P_glFramebufferRenderbuffer)(unsigned int, unsigned int, unsigned int, unsigned int); static void *g_lib; static void *S(const char *n) { return dlsym(g_lib, n); } @@ -256,6 +261,10 @@ int main(int argc, char **argv) { P_glFramebufferTexture2D glFramebufferTexture2D_ = (P_glFramebufferTexture2D)S("glFramebufferTexture2D"); P_glCheckFramebufferStatus glCheckFramebufferStatus_ = (P_glCheckFramebufferStatus)S("glCheckFramebufferStatus"); P_glViewport glViewport_ = (P_glViewport)S("glViewport"); + P_glGenRenderbuffers glGenRenderbuffers_ = (P_glGenRenderbuffers)S("glGenRenderbuffers"); + P_glBindRenderbuffer glBindRenderbuffer_ = (P_glBindRenderbuffer)S("glBindRenderbuffer"); + P_glRenderbufferStorage glRenderbufferStorage_ = (P_glRenderbufferStorage)S("glRenderbufferStorage"); + P_glFramebufferRenderbuffer glFramebufferRenderbuffer_ = (P_glFramebufferRenderbuffer)S("glFramebufferRenderbuffer"); int major = -1, minor = -1, profile = -1; glGetIntegerv_(GL_MAJOR_VERSION, &major); @@ -306,11 +315,43 @@ int main(int argc, char **argv) { printf(" user-FBO incomplete status=0x%x\n", fbst); } + /* FBO with a RENDERBUFFER colour attachment. This is what dEQP's + * FboRenderContext allocates for --deqp-surface-type=fbo, so it is the path + * that actually decides a conformance run - a texture-attached FBO working + * says nothing about it. */ + unsigned int rbo = 0, rfbo = 0; + int rboOk = 0; + if (glGenRenderbuffers_ && glBindRenderbuffer_ && glRenderbufferStorage_ && glFramebufferRenderbuffer_) { + glGenRenderbuffers_(1, &rbo); + glBindRenderbuffer_(GL_RENDERBUFFER, rbo); + glRenderbufferStorage_(GL_RENDERBUFFER, GL_RGBA8, DIM, DIM); + glBindRenderbuffer_(GL_RENDERBUFFER, 0); + glGenFramebuffers_(1, &rfbo); + glBindFramebuffer_(GL_FRAMEBUFFER, rfbo); + glFramebufferRenderbuffer_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rbo); + unsigned int rst = glCheckFramebufferStatus_(GL_FRAMEBUFFER); + if (rst == GL_FRAMEBUFFER_COMPLETE) { + glViewport_(0, 0, DIM, DIM); + glClearColor_(0.1f, 0.7f, 0.3f, 1.0f); + glClear_(GL_COLOR_BUFFER_BIT); + if (glFinish_) glFinish_(); + memset(px, 0, sizeof px); + glReadPixels_(DIM / 2, DIM / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px); + rboOk = near8(px[0], 26, 10) && near8(px[1], 179, 10) && near8(px[2], 77, 10); + printf(" rbo-FBO readback (%u,%u,%u,%u) %s\n", px[0], px[1], px[2], px[3], + rboOk ? "ok" : "BROKEN"); + } else { + printf(" rbo-FBO incomplete status=0x%x\n", rst); + } + } else { + printf(" rbo-FBO skipped (renderbuffer entry points unavailable)\n"); + } + unsigned glerr = glGetError_ ? glGetError_() : 0; - int ok = fboOk && (major > 3 || (major == 3 && minor >= 3)) && (profile & 1) && glerr == 0; - printf("%s backend=%s surface=%s default_fb=%s user_fbo=%s glerr=0x%x\n", + int ok = fboOk && rboOk && (major > 3 || (major == 3 && minor >= 3)) && (profile & 1) && glerr == 0; + printf("%s backend=%s surface=%s default_fb=%s user_fbo=%s rbo_fbo=%s glerr=0x%x\n", ok ? "PASS" : "FAIL", backend, surface, defOk ? "ok" : "broken", - fboOk ? "ok" : "broken", glerr); + fboOk ? "ok" : "broken", rboOk ? "ok" : "broken", glerr); fflush(stdout); /* MobileGL aborts in static teardown; leave before that runs. */ diff --git a/tools/cts/scripts/run_cts.py b/tools/cts/scripts/run_cts.py index f827f9fb..df10d1a7 100644 --- a/tools/cts/scripts/run_cts.py +++ b/tools/cts/scripts/run_cts.py @@ -95,6 +95,8 @@ def main(): help="seconds before giving up on one glcts invocation (a GPU hang never returns)") ap.add_argument("--skip-file", default=None, help="file of case names to exclude, e.g. cases known to hang the device") + ap.add_argument("--env", action="append", default=[], metavar="K=V", + help="extra environment variable for glcts (repeatable)") args = ap.parse_args() os.makedirs(args.outdir, exist_ok=True) @@ -148,9 +150,10 @@ def main(): continue adb(args.serial, "shell", f"rm -f {dev_qpa}", timeout=60) + extra_env = "".join(f"{kv} " for kv in args.env) cmd = ( f"cd {args.device_dir} && " - f"MOBILEGL_BACKEND_TYPE={args.backend} LD_LIBRARY_PATH=. " + f"MOBILEGL_BACKEND_TYPE={args.backend} LD_LIBRARY_PATH=. {extra_env}" f"./glcts --deqp-caselist-file={dev_list} " f"--deqp-surface-type={args.surface} " f"--deqp-terminate-on-device-lost=disable " diff --git a/tools/cts/skills/gl-cts-on-mobilegl/SKILL.md b/tools/cts/skills/gl-cts-on-mobilegl/SKILL.md index 3a40b805..7b829833 100644 --- a/tools/cts/skills/gl-cts-on-mobilegl/SKILL.md +++ b/tools/cts/skills/gl-cts-on-mobilegl/SKILL.md @@ -141,7 +141,8 @@ complete one. | Flag | Why it is not optional | | --- | --- | -| `--deqp-surface-type=fbo` | On DirectVulkan, `glReadPixels` from the **default framebuffer returns all zeros** with no GL error. dEQP verifies nearly everything through `glReadPixels`, so rendering to the surface scores DirectVulkan near zero for a reason unrelated to conformance. FBO readback is correct on both backends. Use it for **both** backends so the two numbers stay comparable. | +| `--deqp-surface-type=fbo` | On DirectVulkan, `glReadPixels` from the **default framebuffer returns all zeros** with no GL error. dEQP verifies nearly everything through `glReadPixels`, so rendering to the surface scores DirectVulkan near zero for a reason unrelated to conformance. Use it for **both** backends so the two numbers stay comparable. | +| `MOBILEGL_CTS_FBO_COLOR_TEXTURE=1` | **`--deqp-surface-type=fbo` alone is not enough.** dEQP's `FboRenderContext` allocates a *renderbuffer* colour attachment, and DirectVulkan returns zeros from a renderbuffer-attached FBO too — only a *texture*-attached FBO reads back correctly. This env var (a patch to `framework/opengl/gluFboRenderContext.cpp`, off by default) switches the attachment to a texture and isolates that single defect. Measured effect: `KHR-GL33.shaders.loops.for_constant_iterations.*` goes 0/62 → 62/62, and the whole-suite DirectVulkan conformance rate goes 46.15% → 72.74%. DirectGLES is bit-identical either way (93.05%), which is the control proving the switch is neutral where readback works. | | `--deqp-terminate-on-device-lost=disable` | Defaults to *enable*, which calls `glGetGraphicsResetStatus()` after every case. That is GL 4.5 / `KHR_robustness`, absent from GL 3.3 core, so the pointer is null and the process segfaults on the first case. Desktop drivers expose the extension, which is why upstream never trips on it. | ## Cases that take the device down @@ -153,11 +154,32 @@ adb entirely. Keep them in a `--skip-file`, and expect to find more: and then stopped responding to adb altogether. - `KHR-GL33.framebuffer_blit.multisampled_to_singlesampled_blit_color_config_test` — rebooted an Adreno 830 phone after 862 cases, on DirectGLES. +- `KHR-GL33.framebuffer_blit.multisampled_to_singlesampled_blit_depth_config_test` + — same, on both backends (found and quarantined automatically by the runner). +- `KHR-GL33.texture_repeat_mode.rgb565_11x131_0_clamp_to_edge` — on DirectVulkan. + +The whole `framebuffer_blit.multisampled_to_singlesampled_*` family is suspect; +treat a new variant as a device-hang candidate rather than a normal failure. When a run dies, pull `/data/local/tmp/mgcts/chunk.qpa` — it survives the reboot, and the last `#beginTestCaseResult` with no matching `#endTestCaseResult` names the case that did it. +## Reference results + +`opengl-cts-4.6.8.1`, KHR-GL33 mustpass (`gl33-main.txt`, 9886 cases), Adreno 830 +/ Android 15, MobileGL `dev`@199164c2, 9884 measured / 0 unrun / 2 quarantined. +Conformance rate = Pass + NotSupported, as Khronos scores a submission. + +| backend | conformance | strict Pass | Fail | Crash | InternalError | DeviceHang | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| DirectGLES | **93.05%** | 85.94% | 679 | 1 | 6 | 1 | +| DirectVulkan (texture FBO) | **72.74%** | 65.71% | 2394 | 294 | 5 | 1 | +| DirectVulkan (stock renderbuffer FBO) | 46.15% | 39.11% | 5024 | 292 | 5 | 2 | + +The third row is what stock dEQP reports; the gap to the second row is entirely +the renderbuffer-FBO readback defect. + ## MobileGL constraints the port works around - **DirectVulkan cannot use an EGL pbuffer.** That path needs From d9556ff041ce25c02b66aec4866208b8ce816bde Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 26 Jul 2026 11:30:54 -0400 Subject: [PATCH 3/6] [Fix] (DirectVulkan): implement color renderbuffer attachments - render pass/pipeline/blit/copy/readback/clear paths treated color renderbuffers as absent (writes masked to VK_ATTACHMENT_UNUSED, glClear dropped, readback zeros) --- .../Renderer/VkRenderPassManager.cpp | 103 ++++++++- .../Renderer/VkRenderPassManager.h | 13 +- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 217 ++++++++++++++++-- .../DirectVulkan/Renderer/VulkanRenderer.h | 3 + 4 files changed, 309 insertions(+), 27 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp index ac11c599..99b276cb 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp @@ -251,11 +251,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { const auto internalFormat = renderbuffer->GetInternalFormat(); const VkFormat format = MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat); const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format); - if ((aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) { - MGLOG_E("GetOrCreateRenderbufferResource: color renderbuffer %u is not supported by DirectVulkan render passes yet", - renderbuffer->GetExternalIndex()); - return nullptr; - } + // Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the + // usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer), + // BlitFramebuffer, CopyTexImage sources, and out-of-render-pass clear materialization. + const VkImageUsageFlags imageUsage = + ((aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT + : VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; auto& resource = m_renderbufferResources[renderbuffer.get()]; const Bool needsCreate = @@ -285,7 +287,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { imageInfo.format = format; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + imageInfo.usage = imageUsage; imageInfo.samples = sampleCount; imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; @@ -386,6 +388,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { void VkRenderPassManager::QueueRenderbufferClear( GLbitfield mask, const ClearFramebufferPayload& clearPayload, const MG_State::GLState::FramebufferObject& drawFbo) { + if ((mask & GL_COLOR_BUFFER_BIT) != 0) { + // Color renderbuffer draw buffers take the framebuffer-level clear too; texture + // attachments are skipped by the per-attachment overload's IsRenderbuffer guard. + for (const auto attachmentType : drawFbo.GetDrawBuffers()) { + if (attachmentType == FramebufferAttachmentType::None) { + continue; + } + QueueRenderbufferClear( + ClearAttachmentPayload{.mask = GL_COLOR_BUFFER_BIT, .color = clearPayload.color}, + drawFbo.GetAttachment(attachmentType)); + } + } if ((mask & GL_DEPTH_BUFFER_BIT) != 0) { QueueRenderbufferClear( ClearAttachmentPayload{.mask = GL_DEPTH_BUFFER_BIT, .depth = clearPayload.depth}, @@ -682,6 +696,83 @@ namespace MobileGL::MG_Backend::DirectVulkan { // assuming default FBO has the right param for (Uint32 i = 0; i < colorAttachmentSlotCount; ++i) { auto drawbuf = drawbufs[i]; + + // Renderbuffer color attachments mirror the texture path below, with the + // resource (image/view/format/layout) coming from the render-pass manager's + // renderbuffer store instead of the texture manager. + if (drawbuf != FramebufferAttachmentType::None && !isDefaultFbo) { + const auto& rbAtt = fbo.GetAttachment(drawbuf); + if (rbAtt.IsRenderbuffer() && rbAtt.IsComplete()) { + const auto& renderbuffer = rbAtt.GetRenderbuffer(); + auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer); + if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) { + MGLOG_E("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color " + "renderbuffer %u; using VK_ATTACHMENT_UNUSED", + i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex()); + continue; + } + + const Uint32 rbAttachmentIndex = static_cast(attachmentDescriptions.size()); + attachmentDescriptions.emplace_back(); + VkAttachmentDescription& rbDesc = attachmentDescriptions.back(); + + ClearAttachmentPayload rbClearPayload{}; + Bool rbHasClear = GetPendingRenderbufferClear(renderbuffer.get(), rbClearPayload) && + (rbClearPayload.mask & GL_COLOR_BUFFER_BIT) != 0; + if (rbHasClear && + MG_Util::GetBaseInternalFormatComponentCount(renderbuffer->GetInternalFormat()) == 3) { + // RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1. + rbClearPayload.color = + FloatVec4(rbClearPayload.color.x(), rbClearPayload.color.y(), + rbClearPayload.color.z(), 1.0f); + } + + const VkImageLayout trackedRbLayout = rbResource->layout; + rbDesc.flags = 0; + rbDesc.format = rbResource->format; + rbDesc.samples = rbResource->sampleCount; + rbDesc.loadOp = rbHasClear ? VK_ATTACHMENT_LOAD_OP_CLEAR : + (trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ATTACHMENT_LOAD_OP_DONT_CARE + : VK_ATTACHMENT_LOAD_OP_LOAD); + rbDesc.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + rbDesc.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + rbDesc.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + rbDesc.initialLayout = (rbHasClear || trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED) ? + VK_IMAGE_LAYOUT_UNDEFINED : trackedRbLayout; + rbDesc.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + adoptRenderPassSampleCount(rbResource->sampleCount, "color", + static_cast(renderbuffer->GetExternalIndex())); + + if (rbHasClear) { + pendingClearAttachments.emplace_back(PendingClearAttachmentInfo { + .attachmentIndex = rbAttachmentIndex, + .colorAttachmentSlot = i, + .renderbuffer = renderbuffer.get(), + .hasInlinePayload = true, + .inlinePayload = rbClearPayload, + }); + } + + if (width == 0) + width = static_cast(rbResource->extent.width); + if (height == 0) + height = static_cast(rbResource->extent.height); + + trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo { + .target = TrackedAttachmentTarget::Renderbuffer, + .renderbuffer = renderbuffer, + .finalLayout = rbDesc.finalLayout, + }); + textureResources.emplace_back(nullptr); + attachmentViews.emplace_back(rbResource->view); + MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE, + "GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i); + + colorAttachmentRefs[i].attachment = rbAttachmentIndex; + continue; + } + } + auto* texture = ResolveCompleteColorAttachmentTexture(fbo, drawbuf, i); if (texture == nullptr) continue; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h index 889065ca..71731435 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h @@ -211,6 +211,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint64 m_rpFastRbEpoch = 0; Uint64 m_rpFastRenderPassHash = 0; + public: struct RenderbufferResource { WeakPtr renderbuffer; VkImage image = VK_NULL_HANDLE; @@ -227,6 +228,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { void Destroy(VkDevice device, VmaAllocator allocator); }; + // Public so the renderer's blit/copy/readback bindings can source renderbuffer + // attachments the same way texture attachments go through the texture manager. + RenderbufferResource* GetOrCreateRenderbufferResource( + const SharedPtr& renderbuffer); + Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer, + ClearAttachmentPayload& outPayload) const; + + private: struct PendingRenderbufferClear { WeakPtr renderbuffer; ClearAttachmentPayload payload{}; @@ -235,10 +244,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { UnorderedMap m_renderbufferResources; UnorderedMap m_pendingRenderbufferClears; - RenderbufferResource* GetOrCreateRenderbufferResource( - const SharedPtr& renderbuffer); - Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer, - ClearAttachmentPayload& outPayload) const; Bool HasPendingRenderbufferClear( const MG_State::GLState::FramebufferAttachmentObject& attachment) const; void CollectRenderbufferGarbage(); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 38fd2167..acefe6fa 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -1274,7 +1274,8 @@ void main() { static Bool ResolveColorBlitBinding(MG_State::GLState::FramebufferObject& fbo, Bool isReadFramebuffer, Uint32 swapchainImageIndex, SwapchainObject& swapchainObject, - VkTextureManager& textureManager, BlitImageBinding& outBinding) { + VkTextureManager& textureManager, + VkRenderPassManager& renderPassManager, BlitImageBinding& outBinding) { const Bool isDefaultFbo = fbo.IsDefaultFramebuffer(); const FramebufferAttachmentType attachmentType = isReadFramebuffer ? fbo.GetReadBuffer() : fbo.GetDrawBuffers()[0]; @@ -1316,8 +1317,24 @@ void main() { return false; } if (attachment.IsRenderbuffer()) { - MGLOG_E("BlitFramebuffer skipped: renderbuffer attachments are not supported yet"); - return false; + const auto& renderbuffer = attachment.GetRenderbuffer(); + auto* rbResource = renderPassManager.GetOrCreateRenderbufferResource(renderbuffer); + if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) { + MGLOG_E("BlitFramebuffer skipped: %s framebuffer color renderbuffer %u is unsupported", + outBinding.label, renderbuffer->GetExternalIndex()); + return false; + } + outBinding.image = rbResource->image; + outBinding.trackedLayout = &rbResource->layout; + outBinding.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + outBinding.format = rbResource->format; + outBinding.extent = {static_cast(rbResource->extent.width), + static_cast(rbResource->extent.height)}; + outBinding.mipLevel = 0; + outBinding.mipLevelCount = 1; + outBinding.baseArrayLayer = 0; + outBinding.layerCount = 1; + return true; } if (!attachment.IsTexture()) { MGLOG_E("BlitFramebuffer skipped: unsupported framebuffer attachment type"); @@ -1355,6 +1372,7 @@ void main() { static Bool ResolveFramebufferBlitBinding(MG_State::GLState::FramebufferObject& fbo, Bool isReadFramebuffer, Uint32 swapchainImageIndex, SwapchainObject& swapchainObject, VkTextureManager& textureManager, + VkRenderPassManager& renderPassManager, VkImageAspectFlags requiredAspectMask, BlitImageBinding& outBinding) { const Bool isDefaultFbo = fbo.IsDefaultFramebuffer(); @@ -1398,8 +1416,29 @@ void main() { return false; } if (attachment.IsRenderbuffer()) { - MGLOG_E("BlitFramebuffer skipped: renderbuffer attachments are not supported yet"); - return false; + const auto& renderbuffer = attachment.GetRenderbuffer(); + auto* rbResource = renderPassManager.GetOrCreateRenderbufferResource(renderbuffer); + if (rbResource == nullptr) { + MGLOG_E("BlitFramebuffer skipped: %s framebuffer renderbuffer %u is unsupported", + outBinding.label, renderbuffer->GetExternalIndex()); + return false; + } + if ((rbResource->aspect & requiredAspectMask) != requiredAspectMask) { + MGLOG_E("BlitFramebuffer skipped: %s framebuffer renderbuffer %u is missing aspect mask=0x%x", + outBinding.label, renderbuffer->GetExternalIndex(), + static_cast(requiredAspectMask)); + return false; + } + outBinding.image = rbResource->image; + outBinding.trackedLayout = &rbResource->layout; + outBinding.aspectMask = requiredAspectMask; + outBinding.extent = {static_cast(rbResource->extent.width), + static_cast(rbResource->extent.height)}; + outBinding.mipLevel = 0; + outBinding.mipLevelCount = 1; + outBinding.baseArrayLayer = 0; + outBinding.layerCount = 1; + return true; } if (!attachment.IsTexture()) { MGLOG_E("BlitFramebuffer skipped: unsupported framebuffer attachment type"); @@ -1470,6 +1509,7 @@ void main() { static Bool ResolveTextureCopySourceBinding(MG_State::GLState::FramebufferObject& fbo, Uint32 swapchainImageIndex, SwapchainObject& swapchainObject, VkTextureManager& textureManager, + VkRenderPassManager& renderPassManager, VkImageAspectFlags requiredAspectMask, BlitImageBinding& outBinding) { const Bool isDefaultFbo = fbo.IsDefaultFramebuffer(); @@ -1514,8 +1554,30 @@ void main() { return false; } if (attachment.IsRenderbuffer()) { - MGLOG_E("CopyTexSubImage2D skipped: renderbuffer read attachments are not supported yet"); - return false; + const auto& renderbuffer = attachment.GetRenderbuffer(); + auto* rbResource = renderPassManager.GetOrCreateRenderbufferResource(renderbuffer); + if (rbResource == nullptr) { + MGLOG_E("CopyTexSubImage2D skipped: read framebuffer renderbuffer %u is unsupported", + renderbuffer->GetExternalIndex()); + return false; + } + if ((rbResource->aspect & requiredAspectMask) != requiredAspectMask) { + MGLOG_E("CopyTexSubImage2D skipped: read framebuffer renderbuffer %u aspect mask=0x%x " + "does not satisfy requested mask=0x%x", + renderbuffer->GetExternalIndex(), static_cast(rbResource->aspect), + static_cast(requiredAspectMask)); + return false; + } + outBinding.image = rbResource->image; + outBinding.trackedLayout = &rbResource->layout; + outBinding.aspectMask = requiredAspectMask; + outBinding.extent = {static_cast(rbResource->extent.width), + static_cast(rbResource->extent.height)}; + outBinding.mipLevel = 0; + outBinding.mipLevelCount = 1; + outBinding.baseArrayLayer = 0; + outBinding.layerCount = 1; + return true; } if (!attachment.IsTexture()) { MGLOG_E("CopyTexSubImage2D skipped: unsupported read framebuffer attachment type"); @@ -3552,6 +3614,7 @@ void main() { (bufferMask.a() ? VK_COLOR_COMPONENT_A_BIT : 0u)); Bool effectiveBlendEnabled = blendEnabled; MG_State::GLState::ITextureObject* colorAttachmentTexture = nullptr; + MG_State::GLState::RenderbufferObject* colorAttachmentRenderbuffer = nullptr; if (isDefaultDrawFbo && i < drawBuffers.size() && drawBuffers[i] == FramebufferAttachmentType::None) { // The default framebuffer spans the same MAX_DRAW_BUFFERS slots as an FBO @@ -3566,12 +3629,24 @@ void main() { if (!isDefaultDrawFbo && i < drawBuffers.size()) { const auto drawBuffer = drawBuffers[i]; colorAttachmentTexture = resolveCompleteColorAttachmentTexture(i); - if (drawBuffer == FramebufferAttachmentType::None || colorAttachmentTexture == nullptr) { + if (colorAttachmentTexture == nullptr && drawBuffer != FramebufferAttachmentType::None) { + const auto& attachment = drawFboBinding->GetAttachment(drawBuffer); + if (attachment.IsRenderbuffer() && attachment.IsComplete()) { + colorAttachmentRenderbuffer = attachment.GetRenderbuffer().get(); + } + } + if (drawBuffer == FramebufferAttachmentType::None || + (colorAttachmentTexture == nullptr && colorAttachmentRenderbuffer == nullptr)) { // GL ignores writes and per-target blend state for GL_NONE draw buffer slots. // Depth-only or otherwise unattached draw buffers should also discard color writes. attachmentColorWriteMask = 0; effectiveBlendEnabled = false; } + if (colorAttachmentRenderbuffer != nullptr) { + const SizeT componentCount = MG_Util::GetBaseInternalFormatComponentCount( + colorAttachmentRenderbuffer->GetInternalFormat()); + attachmentColorWriteMask &= GetSupportedColorWriteMaskForComponentCount(componentCount); + } if (colorAttachmentTexture != nullptr) { auto* texture = colorAttachmentTexture; #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG @@ -3648,10 +3723,14 @@ void main() { Int textureExternalIndex = -1; if (isDefaultDrawFbo) { colorAttachmentFormat = m_swapchainObject.GetSurfaceFormat().format; + } else if (colorAttachmentRenderbuffer != nullptr) { + textureExternalIndex = static_cast(colorAttachmentRenderbuffer->GetExternalIndex()); + colorAttachmentFormat = MG_Util::ConvertTextureInternalFormatToVkEnum( + colorAttachmentRenderbuffer->GetInternalFormat()); } else { auto* texture = colorAttachmentTexture; MOBILEGL_ASSERT(texture != nullptr, - "GetOrCreatePipeline: blend is enabled on draw buffer %u but no complete texture attachment is bound", + "GetOrCreatePipeline: blend is enabled on draw buffer %u but no complete color attachment is bound", i); textureExternalIndex = texture->GetExternalIndex(); auto* textureResource = m_textureManager->SyncTextureAndGetDescriptor(*texture); @@ -4802,6 +4881,98 @@ void main() { return true; } + Bool VulkanRenderer::MaterializePendingClearForRenderbuffer( + VkCommandBuffer commandBuffer, const SharedPtr& renderbuffer) { + if (renderbuffer == nullptr) { + return true; + } + ClearAttachmentPayload clearPayload{}; + if (!m_renderPassManager->GetPendingRenderbufferClear(renderbuffer.get(), clearPayload)) { + return true; + } + MOBILEGL_ASSERT(VkRenderPassManager::GetActiveRenderPass() == nullptr, + "MaterializePendingClearForRenderbuffer requires no active render pass"); + + auto* resource = m_renderPassManager->GetOrCreateRenderbufferResource(renderbuffer); + if (resource == nullptr) { + MGLOG_E("MaterializePendingClearForRenderbuffer: no resource for renderbuffer %u", + renderbuffer->GetExternalIndex()); + return false; + } + + VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags srcAccessMask = 0; + GetImageTransitionSourceState(resource->layout, srcStageMask, srcAccessMask); + + Bool ok = VkTextureManager::TransitionImageLayout( + commandBuffer, resource->image, resource->layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + srcStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_WRITE_BIT, + resource->aspect, 0, 1, 1); + MOBILEGL_ASSERT(ok, + "MaterializePendingClearForRenderbuffer: failed to transition renderbuffer %u to TRANSFER_DST", + renderbuffer->GetExternalIndex()); + + VkImageSubresourceRange subresourceRange{}; + subresourceRange.baseMipLevel = 0; + subresourceRange.levelCount = 1; + subresourceRange.baseArrayLayer = 0; + subresourceRange.layerCount = 1; + + VkImageLayout steadyLayout; + if ((resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) { + subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + VkClearColorValue clearValue{}; + clearValue.float32[0] = clearPayload.color.x(); + clearValue.float32[1] = clearPayload.color.y(); + clearValue.float32[2] = clearPayload.color.z(); + // RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1. + clearValue.float32[3] = + MG_Util::GetBaseInternalFormatComponentCount(renderbuffer->GetInternalFormat()) == 3 ? + 1.0f : clearPayload.color.w(); + vkCmdClearColorImage(commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + &clearValue, 1, &subresourceRange); + steadyLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + } else { + VkImageAspectFlags clearAspectMask = 0; + if ((resource->aspect & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 && + (clearPayload.mask & GL_DEPTH_BUFFER_BIT) != 0) { + clearAspectMask |= VK_IMAGE_ASPECT_DEPTH_BIT; + } + if ((resource->aspect & VK_IMAGE_ASPECT_STENCIL_BIT) != 0 && + (clearPayload.mask & GL_STENCIL_BUFFER_BIT) != 0) { + clearAspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT; + } + if (clearAspectMask != 0) { + subresourceRange.aspectMask = clearAspectMask; + VkClearDepthStencilValue clearValue{}; + clearValue.depth = clearPayload.depth; + clearValue.stencil = clearPayload.stencil; + vkCmdClearDepthStencilImage(commandBuffer, resource->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + &clearValue, 1, &subresourceRange); + } + steadyLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; + } + + VkImageLayout clearLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + ok = VkTextureManager::TransitionImageLayout( + commandBuffer, resource->image, clearLayout, steadyLayout, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT, + VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | + VK_ACCESS_TRANSFER_READ_BIT, + resource->aspect, 0, 1, 1); + MOBILEGL_ASSERT(ok, + "MaterializePendingClearForRenderbuffer: failed to transition renderbuffer %u to steady layout", + renderbuffer->GetExternalIndex()); + resource->layout = steadyLayout; + + m_renderPassManager->PopPendingRenderbufferClear(renderbuffer.get()); + MGLOG_D("MaterializePendingClearForRenderbuffer: renderbuffer %u pending clear materialized", + renderbuffer->GetExternalIndex()); + return true; + } + Bool VulkanRenderer::TryBlitToDefaultFramebufferWithShader(FrameContext::FrameData& frame, MG_State::GLState::FramebufferObject& readFbo, MG_State::GLState::FramebufferObject& drawFbo, @@ -4815,8 +4986,10 @@ void main() { BlitImageBinding srcBinding{}; BlitImageBinding dstBinding{}; - if (!ResolveColorBlitBinding(readFbo, true, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, srcBinding) || - !ResolveColorBlitBinding(drawFbo, false, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, dstBinding)) { + if (!ResolveColorBlitBinding(readFbo, true, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, + *m_renderPassManager, srcBinding) || + !ResolveColorBlitBinding(drawFbo, false, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, + *m_renderPassManager, dstBinding)) { return false; } if (srcBinding.trackedLayout == nullptr) { @@ -5002,9 +5175,11 @@ void main() { BlitImageBinding srcBinding{}; BlitImageBinding dstBinding{}; if (!ResolveFramebufferBlitBinding(*readFbo, true, m_imageIndexAcquired, m_swapchainObject, - *m_textureManager, VK_IMAGE_ASPECT_DEPTH_BIT, srcBinding) || + *m_textureManager, *m_renderPassManager, + VK_IMAGE_ASPECT_DEPTH_BIT, srcBinding) || !ResolveFramebufferBlitBinding(*drawFbo, false, m_imageIndexAcquired, m_swapchainObject, - *m_textureManager, VK_IMAGE_ASPECT_DEPTH_BIT, dstBinding)) { + *m_textureManager, *m_renderPassManager, + VK_IMAGE_ASPECT_DEPTH_BIT, dstBinding)) { return; } @@ -5169,8 +5344,10 @@ void main() { BlitImageBinding srcBinding{}; BlitImageBinding dstBinding{}; - if (!ResolveColorBlitBinding(*readFbo, true, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, srcBinding) || - !ResolveColorBlitBinding(*drawFbo, false, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, dstBinding)) { + if (!ResolveColorBlitBinding(*readFbo, true, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, + *m_renderPassManager, srcBinding) || + !ResolveColorBlitBinding(*drawFbo, false, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, + *m_renderPassManager, dstBinding)) { return; } @@ -5374,7 +5551,7 @@ void main() { BlitImageBinding srcBinding{}; if (!ResolveTextureCopySourceBinding(*readFbo, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, - dstBinding.aspectMask, srcBinding)) { + *m_renderPassManager, dstBinding.aspectMask, srcBinding)) { RecordTextureCopyError(__func__, ErrorCode::InvalidOperation, "CopyTexSubImage2D requires a complete read attachment compatible with the destination texture."); return; @@ -5715,7 +5892,7 @@ void main() { const Bool readIsDefaultFbo = readFbo->IsDefaultFramebuffer(); BlitImageBinding srcBinding{}; if (!ResolveColorBlitBinding(*readFbo, true, m_imageIndexAcquired, m_swapchainObject, *m_textureManager, - srcBinding)) { + *m_renderPassManager, srcBinding)) { return; } if (!readIsDefaultFbo) { @@ -5726,6 +5903,12 @@ void main() { MOBILEGL_ASSERT(clearReady, "ReadPixels: failed to materialize pending clear for source textureId=%d", sourceTexture->GetExternalIndex()); + } else if (sourceAttachment.IsRenderbuffer()) { + const Bool clearReady = + MaterializePendingClearForRenderbuffer(frame.commandBuffer, sourceAttachment.GetRenderbuffer()); + MOBILEGL_ASSERT(clearReady, + "ReadPixels: failed to materialize pending clear for source renderbuffer %u", + sourceAttachment.GetRenderbuffer()->GetExternalIndex()); } } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index ee3cd507..68c50c30 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -555,6 +555,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { GLenum filter); Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); + Bool MaterializePendingClearForRenderbuffer( + VkCommandBuffer commandBuffer, + const SharedPtr& renderbuffer); VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry); Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame, MG_State::GLState::ITextureObject& texture, From cc3dcfd80edc545bd6cb811ac9e862e0edb748da Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 26 Jul 2026 12:25:24 -0400 Subject: [PATCH 4/6] [Fix] (DirectVulkan): support UBO instance arrays as arrayed descriptors - uniform Block{...}b[N] reflected as one binding with descriptorCount=N, per-element GL block mapping, per-element buffer infos and dynamic offsets; non-UBO descriptor arrays now fail program creation cleanly instead of continuing corrupt --- .../DirectVulkan/Renderer/ProgramFactory.cpp | 116 +++++++++++++++--- .../DirectVulkan/Renderer/ProgramFactory.h | 10 ++ .../DirectVulkan/Renderer/UniformManager.cpp | 93 +++++++++----- .../DirectVulkan/Renderer/UniformManager.h | 2 +- 4 files changed, 173 insertions(+), 48 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index d6967c74..fb758973 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -1094,9 +1094,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { for (auto* binding : bindings) { MOBILEGL_ASSERT(binding != nullptr, "ProgramFactory: null descriptor binding reflection record"); const auto kind = ReflectDescriptorTypeToBindingKind(binding->descriptor_type); - MOBILEGL_ASSERT(binding->count == 1, - "ProgramFactory: descriptor arrays are unsupported (name='%s' count=%u)", - binding->name ? binding->name : "", binding->count); + // UBO instance arrays (uniform Block {...} b[N];) occupy one binding with + // descriptorCount = N; other descriptor arrays stay unsupported and must + // fail program creation cleanly rather than continue with corrupt state. + if (binding->count != 1 && kind != ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) { + MGLOG_E("ProgramFactory: descriptor arrays are unsupported for this descriptor " + "kind (name='%s' count=%u type=%d)", + binding->name ? binding->name : "", binding->count, + static_cast(binding->descriptor_type)); + destroyReflectModules(); + return false; + } DescriptorKey key{}; key.kind = kind; @@ -1613,6 +1621,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { entry.storageBlockIndexByBinding.assign(m_maxBindings, -1); entry.globalUboBinding = -1; entry.dynamicBindings.clear(); + entry.bindingDescriptorCounts.assign(m_maxBindings, 1); + entry.arrayedUniformBlockIndicesByBinding.clear(); // Use SpvcSession (Reflection mode) to reflect all SPIR-V modules in a single pass per module for (const auto& module : spirv) { @@ -1628,6 +1638,27 @@ namespace MobileGL::MG_Backend::DirectVulkan { "ProgramFactory::ReflectLayout: failed to create reflection module (result=%d)", static_cast(createReflectResult)); + // Descriptor counts per binding (UBO instance arrays reflect count > 1). + UnorderedMap descriptorCountByBinding; + { + uint32_t countProbe = 0; + if (spvReflectEnumerateDescriptorBindings(&reflectModule, &countProbe, nullptr) == + SPV_REFLECT_RESULT_SUCCESS && + countProbe > 0) { + Vector probeBindings(countProbe); + if (spvReflectEnumerateDescriptorBindings(&reflectModule, &countProbe, + probeBindings.data()) == + SPV_REFLECT_RESULT_SUCCESS) { + for (const auto* probeBinding : probeBindings) { + if (probeBinding != nullptr) { + descriptorCountByBinding[probeBinding->binding] = + std::max(1, probeBinding->count); + } + } + } + } + } + // Reflect uniform buffers auto ubos = session.GetShaderInterface(SPVC_RESOURCE_TYPE_UNIFORM_BUFFER); for (const auto& ubo : ubos) { @@ -1653,9 +1684,69 @@ namespace MobileGL::MG_Backend::DirectVulkan { continue; } - const Uint blockIndex = program.GetUniformBlockIndex(ubo.name.c_str()); - if (blockIndex == 0xFFFFFFFFu) { - MGLOG_D("ProgramFactory::ReflectLayout: skipping inactive UBO '%s' at binding %u", + const auto countIt = descriptorCountByBinding.find(binding); + const Uint32 descriptorCount = + countIt != descriptorCountByBinding.end() ? countIt->second : 1u; + + if (descriptorCount <= 1) { + const Uint blockIndex = program.GetUniformBlockIndex(ubo.name.c_str()); + if (blockIndex == 0xFFFFFFFFu) { + MGLOG_D("ProgramFactory::ReflectLayout: skipping inactive UBO '%s' at binding %u", + ubo.name.c_str(), binding); + continue; + } + + MOBILEGL_ASSERT(entry.bindingKinds[binding] == DescriptorBindingKind::None || + entry.bindingKinds[binding] == DescriptorBindingKind::UniformBufferDynamic, + "ProgramFactory::ReflectLayout: descriptor binding %u has conflicting kinds for UBO '%s'", + binding, ubo.name.c_str()); + entry.bindingKinds[binding] = DescriptorBindingKind::UniformBufferDynamic; + MOBILEGL_ASSERT(entry.globalUboBinding != static_cast(binding), + "ProgramFactory::ReflectLayout: regular UBO '%s' collides with global UBO binding %u", + ubo.name.c_str(), binding); + MOBILEGL_ASSERT(entry.uniformBlockIndexByBinding[binding] < 0 || + entry.uniformBlockIndexByBinding[binding] == static_cast(blockIndex), + "ProgramFactory::ReflectLayout: descriptor binding %u maps to conflicting UBO blocks (%d vs %u)", + binding, entry.uniformBlockIndexByBinding[binding], blockIndex); + entry.uniformBlockIndexByBinding[binding] = static_cast(blockIndex); + continue; + } + + // UBO instance array: one binding, descriptorCount elements. GL exposes each + // element as its own active block named "Name[i]"; map every element to its + // GL block index so the descriptor write can gather per-element buffer ranges. + if (descriptorCount > m_maxBindings) { + MGLOG_E("ProgramFactory::ReflectLayout: UBO array '%s' count %u exceeds maxBindings=%u; " + "leaving binding %u unmapped", + ubo.name.c_str(), descriptorCount, m_maxBindings, binding); + continue; + } + Vector elementBlockIndices; + elementBlockIndices.reserve(descriptorCount); + for (Uint32 element = 0; element < descriptorCount; ++element) { + String elementName = ubo.name + "[" + std::to_string(element) + "]"; + Uint elementBlockIndex = program.GetUniformBlockIndex(elementName.c_str()); + if (elementBlockIndex == 0xFFFFFFFFu && element == 0) { + // Some frontends report the first element under the bare block name. + elementBlockIndex = program.GetUniformBlockIndex(ubo.name.c_str()); + } + if (elementBlockIndex == 0xFFFFFFFFu) { + // Degrade rather than corrupt: reuse element 0's block if we have one, + // otherwise give up on the binding (same observable behavior as an + // inactive block: wrong values, but no crash). + MGLOG_E("ProgramFactory::ReflectLayout: UBO array '%s' element %u has no active " + "GL uniform block", + ubo.name.c_str(), element); + if (!elementBlockIndices.empty()) { + elementBlockIndex = static_cast(elementBlockIndices.front()); + } else { + break; + } + } + elementBlockIndices.push_back(static_cast(elementBlockIndex)); + } + if (elementBlockIndices.size() != descriptorCount) { + MGLOG_E("ProgramFactory::ReflectLayout: skipping unresolved UBO array '%s' at binding %u", ubo.name.c_str(), binding); continue; } @@ -1665,14 +1756,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { "ProgramFactory::ReflectLayout: descriptor binding %u has conflicting kinds for UBO '%s'", binding, ubo.name.c_str()); entry.bindingKinds[binding] = DescriptorBindingKind::UniformBufferDynamic; - MOBILEGL_ASSERT(entry.globalUboBinding != static_cast(binding), - "ProgramFactory::ReflectLayout: regular UBO '%s' collides with global UBO binding %u", - ubo.name.c_str(), binding); - MOBILEGL_ASSERT(entry.uniformBlockIndexByBinding[binding] < 0 || - entry.uniformBlockIndexByBinding[binding] == static_cast(blockIndex), - "ProgramFactory::ReflectLayout: descriptor binding %u maps to conflicting UBO blocks (%d vs %u)", - binding, entry.uniformBlockIndexByBinding[binding], blockIndex); - entry.uniformBlockIndexByBinding[binding] = static_cast(blockIndex); + entry.bindingDescriptorCounts[binding] = static_cast(descriptorCount); + entry.uniformBlockIndexByBinding[binding] = elementBlockIndices[0]; + entry.arrayedUniformBlockIndicesByBinding[binding] = Move(elementBlockIndices); } // Reflect sampled images, storage images, samplerBuffer uniforms, and SSBOs. @@ -1819,7 +1905,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkDescriptorSetLayoutBinding layoutBinding{}; layoutBinding.binding = binding; - layoutBinding.descriptorCount = 1; + layoutBinding.descriptorCount = entry.bindingDescriptorCounts[binding]; layoutBinding.stageFlags = VK_SHADER_STAGE_ALL; layoutBinding.pImmutableSamplers = nullptr; if (kind == DescriptorBindingKind::UniformBufferDynamic) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h index fa8f5cca..4d3d49b1 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h @@ -59,6 +59,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector bindingKinds; Vector dynamicBindings; Vector uniformBlockIndexByBinding; + // Descriptor count per binding (1 except for UBO instance arrays, which occupy one + // binding with descriptorCount = N). + Vector bindingDescriptorCounts; + // Per-element GL uniform block indices for arrayed UBO bindings (count > 1); + // element 0 of a non-arrayed binding stays in uniformBlockIndexByBinding. + UnorderedMap> arrayedUniformBlockIndicesByBinding; Vector samplerNameByBinding; Vector samplerUniformLocationByBinding; Vector samplerTextureTargetByBinding; @@ -97,6 +103,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { bindingKinds = std::move(other.bindingKinds); dynamicBindings = std::move(other.dynamicBindings); uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding); + bindingDescriptorCounts = std::move(other.bindingDescriptorCounts); + arrayedUniformBlockIndicesByBinding = std::move(other.arrayedUniformBlockIndicesByBinding); samplerNameByBinding = std::move(other.samplerNameByBinding); samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding); samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding); @@ -141,6 +149,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { bindingKinds = std::move(other.bindingKinds); dynamicBindings = std::move(other.dynamicBindings); uniformBlockIndexByBinding = std::move(other.uniformBlockIndexByBinding); + bindingDescriptorCounts = std::move(other.bindingDescriptorCounts); + arrayedUniformBlockIndicesByBinding = std::move(other.arrayedUniformBlockIndicesByBinding); samplerNameByBinding = std::move(other.samplerNameByBinding); samplerUniformLocationByBinding = std::move(other.samplerUniformLocationByBinding); samplerTextureTargetByBinding = std::move(other.samplerTextureTargetByBinding); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index f54aa155..50838e99 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -765,7 +765,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, - UboBindResult& out) const { + Uint32 arrayElement, UboBindResult& out) const { const void* outData = nullptr; VkDeviceSize outSize = 0; @@ -791,7 +791,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { MOBILEGL_ASSERT(binding < programObj.uniformBlockIndexByBinding.size(), "ResolveUniformBufferPayload: UBO mapping binding %u out of range", binding); - const Int blockIndex = programObj.uniformBlockIndexByBinding[binding]; + Int blockIndex = programObj.uniformBlockIndexByBinding[binding]; + if (arrayElement > 0) { + const auto arrayIt = programObj.arrayedUniformBlockIndicesByBinding.find(binding); + const Bool elementValid = arrayIt != programObj.arrayedUniformBlockIndicesByBinding.end() && + arrayElement < arrayIt->second.size(); + MOBILEGL_ASSERT(elementValid, + "ResolveUniformBufferPayload: UBO binding %u has no array element %u", binding, + arrayElement); + if (!elementValid) { + return false; + } + blockIndex = arrayIt->second[arrayElement]; + } MOBILEGL_ASSERT(blockIndex >= 0, "ResolveUniformBufferPayload: no uniform block mapped to descriptor binding %u", binding); @@ -1038,11 +1050,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { imageInfos.clear(); texelBufferViews.clear(); dynamicOffsets.clear(); + // Arrayed UBO bindings contribute extra buffer infos and dynamic offsets; reserve for + // the worst case so the pBufferInfo pointers taken below never dangle on reallocation. + Uint32 uboArrayExtra = 0; + for (const auto& arrayEntry : programObj.arrayedUniformBlockIndicesByBinding) { + uboArrayExtra += static_cast(arrayEntry.second.size()) - 1u; + } writes.reserve(m_maxBindings); - bufferInfos.reserve(m_maxBindings); + bufferInfos.reserve(m_maxBindings + uboArrayExtra); imageInfos.reserve(m_maxBindings); texelBufferViews.reserve(m_maxBindings); - dynamicOffsets.reserve(programObj.dynamicBindings.size()); + dynamicOffsets.reserve(programObj.dynamicBindings.size() + uboArrayExtra); const Uint32 bindingCount = std::min(m_maxBindings, static_cast(programObj.bindingKinds.size())); @@ -1060,40 +1078,51 @@ namespace MobileGL::MG_Backend::DirectVulkan { write.descriptorCount = 1; if (kind == ProgramFactory::DescriptorBindingKind::UniformBufferDynamic) { - UboBindResult ubo{}; - const Bool hasPayload = ResolveUniformBufferPayload(program, programObj, binding, ubo); - MOBILEGL_ASSERT(hasPayload && ubo.payload != nullptr && ubo.payloadSize > 0, - "UniformDescriptorBinder::BindProgramUniformBuffers failed: missing UBO payload on binding %u", - binding); + const Uint32 descriptorCount = + binding < programObj.bindingDescriptorCounts.size() + ? std::max(1, programObj.bindingDescriptorCounts[binding]) + : 1u; + const SizeT firstBufferInfoIndex = bufferInfos.size(); + for (Uint32 element = 0; element < descriptorCount; ++element) { + UboBindResult ubo{}; + const Bool hasPayload = + ResolveUniformBufferPayload(program, programObj, binding, element, ubo); + MOBILEGL_ASSERT(hasPayload && ubo.payload != nullptr && ubo.payloadSize > 0, + "UniformDescriptorBinder::BindProgramUniformBuffers failed: missing UBO payload on binding %u element %u", + binding, element); - VkDescriptorBufferInfo bufferInfo{}; - // Keep offset 0 (sub-range selected via the dynamic offset) so the hashed bufferInfo - // is stable across draws and the descriptor-set reuse cache keeps hitting. - bufferInfo.offset = 0; - Uint32 dynOffset; - if (ubo.directBindable) { - // Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy. - bufferInfo.buffer = ubo.buffer; - bufferInfo.range = ubo.range; - dynOffset = static_cast(ubo.dynamicOffset); - } else { - BufferSlice slice{}; - if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload, - ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) { - MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u", - binding); - return false; + VkDescriptorBufferInfo bufferInfo{}; + // Keep offset 0 (sub-range selected via the dynamic offset) so the hashed bufferInfo + // is stable across draws and the descriptor-set reuse cache keeps hitting. + bufferInfo.offset = 0; + Uint32 dynOffset; + if (ubo.directBindable) { + // Zero-copy: bind the app's resident VkBuffer directly, no per-draw memcpy. + bufferInfo.buffer = ubo.buffer; + bufferInfo.range = ubo.range; + dynOffset = static_cast(ubo.dynamicOffset); + } else { + BufferSlice slice{}; + if (!m_bufferManager->UploadTransient(BufferKind::Uniform, frameIndex, ubo.payload, + ubo.payloadSize, m_minDynamicOffsetAlignment, slice)) { + MOBILEGL_ASSERT(false, "UniformDescriptorBinder::BindProgramUniformBuffers failed: UBO upload failed on binding %u element %u", + binding, element); + return false; + } + bufferInfo.buffer = slice.buffer; + bufferInfo.range = ubo.payloadSize; + dynOffset = static_cast(slice.offset); } - bufferInfo.buffer = slice.buffer; - bufferInfo.range = ubo.payloadSize; - dynOffset = static_cast(slice.offset); + bufferInfos.push_back(bufferInfo); + // Dynamic offsets are consumed in binding order, then array element order, + // matching Vulkan's dynamic-offset consumption rules. + dynamicOffsets.push_back(dynOffset); } - bufferInfos.push_back(bufferInfo); write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC; - write.pBufferInfo = &bufferInfos.back(); + write.descriptorCount = descriptorCount; + write.pBufferInfo = &bufferInfos[firstBufferInfoIndex]; writes.push_back(write); - dynamicOffsets.push_back(dynOffset); } else if (kind == ProgramFactory::DescriptorBindingKind::UniformTexelBuffer) { VkBufferView bufferView = VK_NULL_HANDLE; if (!ResolveTexelBufferDescriptor(program, programObj, binding, frameIndex, bufferView) || diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index d7afe8c2..6f495f00 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -116,7 +116,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { }; Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, - UboBindResult& out) const; + Uint32 arrayElement, UboBindResult& out) const; Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const; Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex); VkResult AllocateDescriptorSetsFromActivePool( From db01bfa3e8c80496ebd00a427df6579a34df69e3 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 26 Jul 2026 13:28:30 -0400 Subject: [PATCH 5/6] [Fix] (DirectVulkan): general (format,type) readback conversion - hoist the CTS-verified StoreWideRowsToClient into shared ReadbackImpl and decode any color VkFormat to wide RGBA rows; readback previously supported only RGB/BGR/RGBA/BGRA x UNSIGNED_BYTE/FLOAT and silently returned zeros for everything else --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 88 +--- MobileGL/MG_Backend/DirectGLES/Utils.cpp | 90 ++++ MobileGL/MG_Backend/DirectGLES/Utils.h | 8 + .../DirectVulkan/Renderer/VulkanRenderer.cpp | 387 ++++++++++++++++-- 4 files changed, 450 insertions(+), 123 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 5c36ffe0..485e9c4d 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -3435,90 +3435,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return componentType != 0 ? static_cast(componentType) : GL_UNSIGNED_NORMALIZED; } - // Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the - // client-side PACK parameters and the bound pixel-pack buffer. `wide` holds - // `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked), - // 4 components x GetReadbackComponentSize(wideType) bytes each. - // applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage - // of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4). - // Per the GL addressing rules, slice k row j lands at - // SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes - // + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride. - static Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight, - GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type, - void* pixels, Bool applyPackImageParams) { - const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type); - if (dstPixelBytes == 0) { - return false; - } - ReadbackImpl::PackedReadbackLayout packedLayout{}; - const Bool isPackedType = ReadbackImpl::GetPackedReadbackLayout(type, packedLayout); - const SizeT dstComponentSize = GetReadbackComponentSize(type); - - const auto& pixelPackBufferObject = - MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); - - // Destination layout is computed from the client-side PACK parameters; only the actual pixel - // rows are written so skip regions of the destination stay untouched. - const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false); - const SizeT rowPixels = static_cast(packParams.RowLength > 0 ? packParams.RowLength : width); - const SizeT dstRowStride = AlignPixelRow(rowPixels * dstPixelBytes, packParams.Alignment); - const SizeT imageRows = - applyPackImageParams && packParams.ImageHeight > 0 - ? static_cast(packParams.ImageHeight) - : static_cast(sliceHeight); - const SizeT dstImageStride = imageRows * dstRowStride; - const SizeT skipImages = - applyPackImageParams ? static_cast(std::max(packParams.SkipImages, 0)) : SizeT{0}; - const SizeT dstSkipOffset = skipImages * dstImageStride + - static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + - static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; - const SizeT dstRowBytes = static_cast(width) * dstPixelBytes; - - const SizeT pboBaseOffset = reinterpret_cast(pixels); // with a PBO, `pixels` is an offset - if (pixelPackBufferObject) { - const SizeT requiredSize = pboBaseOffset + dstSkipOffset + - static_cast(sliceCount - 1) * dstImageStride + - static_cast(sliceHeight - 1) * dstRowStride + dstRowBytes; - if (requiredSize > pixelPackBufferObject->GetSize()) { - MGLOG_E("Readback conversion: pixel pack buffer is too small"); - return true; - } - } - - const SizeT srcComponentSize = GetReadbackComponentSize(wideType); - const SizeT srcPixelBytes = 4 * srcComponentSize; - Vector convertedRow(dstRowBytes); - - for (GLsizei slice = 0; slice < sliceCount; ++slice) { - for (GLsizei row = 0; row < sliceHeight; ++row) { - const SizeT flatRow = static_cast(slice) * static_cast(sliceHeight) + - static_cast(row); - const Uint8* srcRow = wide + flatRow * static_cast(width) * srcPixelBytes; - ReadbackImpl::ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast(width), wideType, - mapping, type); - - if (packParams.SwapBytes) { - const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize; - if (groupSize > 1) { - for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) { - std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize); - } - } - } - - const SizeT dstOffset = dstSkipOffset + static_cast(slice) * dstImageStride + - static_cast(row) * dstRowStride; - if (pixelPackBufferObject) { - pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes}, - pboBaseOffset + dstOffset); - } else { - Memcpy(static_cast(pixels) + dstOffset, convertedRow.data(), dstRowBytes); - } - } - } - return true; - } // Reads the current READ framebuffer as wide RGBA(_INTEGER) and repacks the pixels into the client's // (format, type) layout. Returns false when the combination is not convertible (the caller keeps its @@ -3651,7 +3567,7 @@ namespace MobileGL::MG_Backend::DirectGLES { ExpandNarrowWideRead(wide, static_cast(width) * static_cast(height), readChannels, wideType); } - if (!StoreWideRowsToClient(wide.data(), wideType, width, height, /*sliceCount=*/1, mapping, type, pixels, + if (!ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, height, /*sliceCount=*/1, mapping, type, pixels, honorPackImageParams)) { return false; } @@ -3705,7 +3621,7 @@ namespace MobileGL::MG_Backend::DirectGLES { return false; } const GLenum wideType = isInteger ? (isSigned ? GL_INT : GL_UNSIGNED_INT) : GL_FLOAT; - if (!StoreWideRowsToClient(wide.data(), wideType, width, sliceHeight, sliceCount, mapping, type, pixels, + if (!ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, sliceHeight, sliceCount, mapping, type, pixels, applyPackImageParams)) { return false; } diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index 17f33db9..0a3b7e1c 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -764,5 +764,95 @@ namespace MobileGL::MG_Backend::DirectGLES { } } } + + static SizeT AlignReadbackRow(SizeT rowBytes, Int alignment) { + const SizeT align = alignment > 0 ? static_cast(alignment) : 1; + return (rowBytes + align - 1) / align * align; + } + + // Repacks wide RGBA(_INTEGER) rows into the client's (format, type) layout, honoring the + // client-side PACK parameters and the bound pixel-pack buffer. `wide` holds + // `sliceHeight * sliceCount` rows of `width` texels (slice-major, tightly stacked), + // 4 components x GetReadbackComponentSize(wideType) bytes each. + // applyPackImageParams: GL_PACK_IMAGE_HEIGHT / GL_PACK_SKIP_IMAGES apply only to GetTexImage + // of 3D/array images; ReadPixels and 2D GetTexImage ignore them (GL 3.3 sections 4.3.1, 6.1.4). + // Per the GL addressing rules, slice k row j lands at + // SKIP_IMAGES*imageStride + SKIP_ROWS*rowStride + SKIP_PIXELS*pixelBytes + // + k*imageStride + j*rowStride, with imageStride = max(IMAGE_HEIGHT, sliceHeight)*rowStride. + Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight, + GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type, + void* pixels, Bool applyPackImageParams) { + const SizeT dstPixelBytes = GetReadbackDstPixelSize(mapping, type); + if (dstPixelBytes == 0) { + return false; + } + PackedReadbackLayout packedLayout{}; + const Bool isPackedType = GetPackedReadbackLayout(type, packedLayout); + const SizeT dstComponentSize = GetReadbackComponentSize(type); + + const auto& pixelPackBufferObject = + MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); + + // Destination layout is computed from the client-side PACK parameters; only the actual pixel + // rows are written so skip regions of the destination stay untouched. + const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false); + const SizeT rowPixels = static_cast(packParams.RowLength > 0 ? packParams.RowLength : width); + const SizeT dstRowStride = AlignReadbackRow(rowPixels * dstPixelBytes, packParams.Alignment); + const SizeT imageRows = + applyPackImageParams && packParams.ImageHeight > 0 + ? static_cast(packParams.ImageHeight) + : static_cast(sliceHeight); + const SizeT dstImageStride = imageRows * dstRowStride; + const SizeT skipImages = + applyPackImageParams ? static_cast(std::max(packParams.SkipImages, 0)) : SizeT{0}; + const SizeT dstSkipOffset = skipImages * dstImageStride + + static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + + static_cast(std::max(packParams.SkipPixels, 0)) * dstPixelBytes; + const SizeT dstRowBytes = static_cast(width) * dstPixelBytes; + + const SizeT pboBaseOffset = reinterpret_cast(pixels); // with a PBO, `pixels` is an offset + if (pixelPackBufferObject) { + const SizeT requiredSize = pboBaseOffset + dstSkipOffset + + static_cast(sliceCount - 1) * dstImageStride + + static_cast(sliceHeight - 1) * dstRowStride + dstRowBytes; + if (requiredSize > pixelPackBufferObject->GetSize()) { + MGLOG_E("Readback conversion: pixel pack buffer is too small"); + return true; + } + } + + const SizeT srcComponentSize = GetReadbackComponentSize(wideType); + const SizeT srcPixelBytes = 4 * srcComponentSize; + Vector convertedRow(dstRowBytes); + + for (GLsizei slice = 0; slice < sliceCount; ++slice) { + for (GLsizei row = 0; row < sliceHeight; ++row) { + const SizeT flatRow = static_cast(slice) * static_cast(sliceHeight) + + static_cast(row); + const Uint8* srcRow = wide + flatRow * static_cast(width) * srcPixelBytes; + ConvertWideReadbackRow(srcRow, convertedRow.data(), static_cast(width), wideType, + mapping, type); + + if (packParams.SwapBytes) { + const SizeT groupSize = isPackedType ? packedLayout.byteSize : dstComponentSize; + if (groupSize > 1) { + for (SizeT offset = 0; offset + groupSize <= dstRowBytes; offset += groupSize) { + std::reverse(convertedRow.data() + offset, convertedRow.data() + offset + groupSize); + } + } + } + + const SizeT dstOffset = dstSkipOffset + static_cast(slice) * dstImageStride + + static_cast(row) * dstRowStride; + if (pixelPackBufferObject) { + pixelPackBufferObject->WritebackFromBackend({convertedRow.data(), dstRowBytes}, + pboBaseOffset + dstOffset); + } else { + Memcpy(static_cast(pixels) + dstOffset, convertedRow.data(), dstRowBytes); + } + } + } + return true; + } } // namespace ReadbackImpl } // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.h b/MobileGL/MG_Backend/DirectGLES/Utils.h index 1068d0cd..f9d20c93 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.h +++ b/MobileGL/MG_Backend/DirectGLES/Utils.h @@ -88,6 +88,14 @@ namespace MobileGL::MG_Backend::DirectGLES { // bytes, dst receives width * GetReadbackDstPixelSize(mapping, type) bytes. void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType, const ReadbackChannelMapping& mapping, GLenum type); + + // Stores wide RGBA(_INTEGER) rows into the client pointer or the bound PACK pixel buffer, + // honoring the client-side PACK pixel-store parameters (row length, alignment, skips, + // swap-bytes, and - when applyPackImageParams - image height/skip images). Shared by the + // DirectGLES and DirectVulkan readback conversion paths. + Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight, + GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type, + void* pixels, Bool applyPackImageParams); } // namespace ReadbackImpl namespace PrgramImpl { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index acefe6fa..71a4d5ae 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -7,6 +7,8 @@ // End of Source File Header #include "VulkanRenderer.h" + +#include "MG_Backend/DirectGLES/Utils.h" #include "VertexInputStateFactory.h" #include "VertexInputStateBuilder.h" @@ -1881,58 +1883,369 @@ void main() { } } + // Generic VkFormat texel decode into the wide RGBA row layouts the shared readback + // store expects: GL_FLOAT rows for normalized/float sources, GL_INT / GL_UNSIGNED_INT + // rows for integer sources. Missing channels take GL defaults (0,0,0,1). + enum class ReadbackSourceClass : Uint8 { Unsupported, Float, SignedInt, UnsignedInt }; + + struct ReadbackSourceDesc { + ReadbackSourceClass sourceClass = ReadbackSourceClass::Unsupported; + Int channels = 0; // component count stored per texel + Int componentBits = 0; // per-component bits for regular formats; 0 for special packed + Bool isSnorm = false; + Bool isSrgb = false; + Bool bgraSwizzle = false; + VkFormat special = VK_FORMAT_UNDEFINED; // set for packed/special formats + }; + + static Bool GetReadbackSourceDesc(VkFormat format, ReadbackSourceDesc& out) { + out = ReadbackSourceDesc{}; + switch (format) { + // --- regular UNORM --- + case VK_FORMAT_R8_UNORM: out = {ReadbackSourceClass::Float, 1, 8}; return true; + case VK_FORMAT_R8G8_UNORM: out = {ReadbackSourceClass::Float, 2, 8}; return true; + case VK_FORMAT_R8G8B8A8_UNORM: out = {ReadbackSourceClass::Float, 4, 8}; return true; + case VK_FORMAT_B8G8R8A8_UNORM: out = {ReadbackSourceClass::Float, 4, 8, false, false, true}; return true; + case VK_FORMAT_R16_UNORM: out = {ReadbackSourceClass::Float, 1, 16}; return true; + case VK_FORMAT_R16G16_UNORM: out = {ReadbackSourceClass::Float, 2, 16}; return true; + case VK_FORMAT_R16G16B16A16_UNORM: out = {ReadbackSourceClass::Float, 4, 16}; return true; + // --- SRGB (decode to linear like GL readback of sRGB textures) --- + case VK_FORMAT_R8G8B8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8, false, true}; return true; + case VK_FORMAT_B8G8R8A8_SRGB: out = {ReadbackSourceClass::Float, 4, 8, false, true, true}; return true; + // --- SNORM --- + case VK_FORMAT_R8_SNORM: out = {ReadbackSourceClass::Float, 1, 8, true}; return true; + case VK_FORMAT_R8G8_SNORM: out = {ReadbackSourceClass::Float, 2, 8, true}; return true; + case VK_FORMAT_R8G8B8A8_SNORM: out = {ReadbackSourceClass::Float, 4, 8, true}; return true; + case VK_FORMAT_R16_SNORM: out = {ReadbackSourceClass::Float, 1, 16, true}; return true; + case VK_FORMAT_R16G16_SNORM: out = {ReadbackSourceClass::Float, 2, 16, true}; return true; + case VK_FORMAT_R16G16B16A16_SNORM: out = {ReadbackSourceClass::Float, 4, 16, true}; return true; + // --- SFLOAT --- + case VK_FORMAT_R16_SFLOAT: out = {ReadbackSourceClass::Float, 1, 16}; out.special = format; return true; + case VK_FORMAT_R16G16_SFLOAT: out = {ReadbackSourceClass::Float, 2, 16}; out.special = format; return true; + case VK_FORMAT_R16G16B16A16_SFLOAT: out = {ReadbackSourceClass::Float, 4, 16}; out.special = format; return true; + case VK_FORMAT_R32_SFLOAT: out = {ReadbackSourceClass::Float, 1, 32}; out.special = format; return true; + case VK_FORMAT_R32G32_SFLOAT: out = {ReadbackSourceClass::Float, 2, 32}; out.special = format; return true; + case VK_FORMAT_R32G32B32A32_SFLOAT: out = {ReadbackSourceClass::Float, 4, 32}; out.special = format; return true; + // --- UINT --- + case VK_FORMAT_R8_UINT: out = {ReadbackSourceClass::UnsignedInt, 1, 8}; return true; + case VK_FORMAT_R8G8_UINT: out = {ReadbackSourceClass::UnsignedInt, 2, 8}; return true; + case VK_FORMAT_R8G8B8A8_UINT: out = {ReadbackSourceClass::UnsignedInt, 4, 8}; return true; + case VK_FORMAT_R16_UINT: out = {ReadbackSourceClass::UnsignedInt, 1, 16}; return true; + case VK_FORMAT_R16G16_UINT: out = {ReadbackSourceClass::UnsignedInt, 2, 16}; return true; + case VK_FORMAT_R16G16B16A16_UINT: out = {ReadbackSourceClass::UnsignedInt, 4, 16}; return true; + case VK_FORMAT_R32_UINT: out = {ReadbackSourceClass::UnsignedInt, 1, 32}; return true; + case VK_FORMAT_R32G32_UINT: out = {ReadbackSourceClass::UnsignedInt, 2, 32}; return true; + case VK_FORMAT_R32G32B32A32_UINT: out = {ReadbackSourceClass::UnsignedInt, 4, 32}; return true; + // --- SINT --- + case VK_FORMAT_R8_SINT: out = {ReadbackSourceClass::SignedInt, 1, 8}; return true; + case VK_FORMAT_R8G8_SINT: out = {ReadbackSourceClass::SignedInt, 2, 8}; return true; + case VK_FORMAT_R8G8B8A8_SINT: out = {ReadbackSourceClass::SignedInt, 4, 8}; return true; + case VK_FORMAT_R16_SINT: out = {ReadbackSourceClass::SignedInt, 1, 16}; return true; + case VK_FORMAT_R16G16_SINT: out = {ReadbackSourceClass::SignedInt, 2, 16}; return true; + case VK_FORMAT_R16G16B16A16_SINT: out = {ReadbackSourceClass::SignedInt, 4, 16}; return true; + case VK_FORMAT_R32_SINT: out = {ReadbackSourceClass::SignedInt, 1, 32}; return true; + case VK_FORMAT_R32G32_SINT: out = {ReadbackSourceClass::SignedInt, 2, 32}; return true; + case VK_FORMAT_R32G32B32A32_SINT: out = {ReadbackSourceClass::SignedInt, 4, 32}; return true; + // --- packed / special --- + case VK_FORMAT_A2B10G10R10_UNORM_PACK32: + case VK_FORMAT_A2B10G10R10_UINT_PACK32: + case VK_FORMAT_B10G11R11_UFLOAT_PACK32: + case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32: + case VK_FORMAT_R5G6B5_UNORM_PACK16: + case VK_FORMAT_B5G6R5_UNORM_PACK16: + case VK_FORMAT_A1R5G5B5_UNORM_PACK16: + case VK_FORMAT_R5G5B5A1_UNORM_PACK16: + case VK_FORMAT_B5G5R5A1_UNORM_PACK16: + case VK_FORMAT_R4G4B4A4_UNORM_PACK16: + case VK_FORMAT_B4G4R4A4_UNORM_PACK16: + out.sourceClass = format == VK_FORMAT_A2B10G10R10_UINT_PACK32 ? + ReadbackSourceClass::UnsignedInt : ReadbackSourceClass::Float; + out.special = format; + return true; + default: + return false; + } + } + + static Float SrgbToLinear(Float value) { + if (value <= 0.04045f) { + return value / 12.92f; + } + return std::pow((value + 0.055f) / 1.055f, 2.4f); + } + + static Float DecodeUnsignedF11(Uint32 bits) { + const Uint32 exponent = (bits >> 6) & 0x1F; + const Uint32 mantissa = bits & 0x3F; + if (exponent == 0) { + return static_cast(mantissa) / 64.0f * std::pow(2.0f, -14.0f); + } + if (exponent == 31) { + return mantissa == 0 ? std::numeric_limits::infinity() + : std::numeric_limits::quiet_NaN(); + } + return (1.0f + static_cast(mantissa) / 64.0f) * + std::pow(2.0f, static_cast(static_cast(exponent)) - 15.0f); + } + + static Float DecodeUnsignedF10(Uint32 bits) { + const Uint32 exponent = (bits >> 5) & 0x1F; + const Uint32 mantissa = bits & 0x1F; + if (exponent == 0) { + return static_cast(mantissa) / 32.0f * std::pow(2.0f, -14.0f); + } + if (exponent == 31) { + return mantissa == 0 ? std::numeric_limits::infinity() + : std::numeric_limits::quiet_NaN(); + } + return (1.0f + static_cast(mantissa) / 32.0f) * + std::pow(2.0f, static_cast(static_cast(exponent)) - 15.0f); + } + + static void DecodeReadbackTexelSpecialFloat(const Uint8* source, VkFormat format, Float* rgba) { + rgba[0] = 0.0f; rgba[1] = 0.0f; rgba[2] = 0.0f; rgba[3] = 1.0f; + switch (format) { + case VK_FORMAT_R16_SFLOAT: + case VK_FORMAT_R16G16_SFLOAT: + case VK_FORMAT_R16G16B16A16_SFLOAT: { + const Int channels = format == VK_FORMAT_R16_SFLOAT ? 1 : + (format == VK_FORMAT_R16G16_SFLOAT ? 2 : 4); + for (Int c = 0; c < channels; ++c) { + Uint16 bits = 0; + Memcpy(&bits, source + static_cast(c) * sizeof(bits), sizeof(bits)); + rgba[c] = MG_Util::DecodeHalfBitsToFloat(bits); + } + return; + } + case VK_FORMAT_R32_SFLOAT: + case VK_FORMAT_R32G32_SFLOAT: + case VK_FORMAT_R32G32B32A32_SFLOAT: { + const Int channels = format == VK_FORMAT_R32_SFLOAT ? 1 : + (format == VK_FORMAT_R32G32_SFLOAT ? 2 : 4); + Memcpy(rgba, source, static_cast(channels) * sizeof(Float)); + return; + } + case VK_FORMAT_A2B10G10R10_UNORM_PACK32: { + Uint32 word = 0; + Memcpy(&word, source, sizeof(word)); + rgba[0] = static_cast(word & 0x3FFu) / 1023.0f; + rgba[1] = static_cast((word >> 10) & 0x3FFu) / 1023.0f; + rgba[2] = static_cast((word >> 20) & 0x3FFu) / 1023.0f; + rgba[3] = static_cast((word >> 30) & 0x3u) / 3.0f; + return; + } + case VK_FORMAT_B10G11R11_UFLOAT_PACK32: { + Uint32 word = 0; + Memcpy(&word, source, sizeof(word)); + rgba[0] = DecodeUnsignedF11(word & 0x7FFu); + rgba[1] = DecodeUnsignedF11((word >> 11) & 0x7FFu); + rgba[2] = DecodeUnsignedF10((word >> 22) & 0x3FFu); + return; + } + case VK_FORMAT_E5B9G9R9_UFLOAT_PACK32: { + Uint32 word = 0; + Memcpy(&word, source, sizeof(word)); + const Int exponent = static_cast((word >> 27) & 0x1Fu) - 15 - 9; + const Float scale = std::pow(2.0f, static_cast(exponent)); + rgba[0] = static_cast(word & 0x1FFu) * scale; + rgba[1] = static_cast((word >> 9) & 0x1FFu) * scale; + rgba[2] = static_cast((word >> 18) & 0x1FFu) * scale; + return; + } + case VK_FORMAT_R5G6B5_UNORM_PACK16: + case VK_FORMAT_B5G6R5_UNORM_PACK16: { + Uint16 word = 0; + Memcpy(&word, source, sizeof(word)); + const Float c0 = static_cast((word >> 11) & 0x1Fu) / 31.0f; + const Float c1 = static_cast((word >> 5) & 0x3Fu) / 63.0f; + const Float c2 = static_cast(word & 0x1Fu) / 31.0f; + const Bool bgr = format == VK_FORMAT_B5G6R5_UNORM_PACK16; + rgba[0] = bgr ? c2 : c0; + rgba[1] = c1; + rgba[2] = bgr ? c0 : c2; + return; + } + case VK_FORMAT_A1R5G5B5_UNORM_PACK16: { + Uint16 word = 0; + Memcpy(&word, source, sizeof(word)); + rgba[3] = static_cast((word >> 15) & 0x1u); + rgba[0] = static_cast((word >> 10) & 0x1Fu) / 31.0f; + rgba[1] = static_cast((word >> 5) & 0x1Fu) / 31.0f; + rgba[2] = static_cast(word & 0x1Fu) / 31.0f; + return; + } + case VK_FORMAT_R5G5B5A1_UNORM_PACK16: { + Uint16 word = 0; + Memcpy(&word, source, sizeof(word)); + rgba[0] = static_cast((word >> 11) & 0x1Fu) / 31.0f; + rgba[1] = static_cast((word >> 6) & 0x1Fu) / 31.0f; + rgba[2] = static_cast((word >> 1) & 0x1Fu) / 31.0f; + rgba[3] = static_cast(word & 0x1u); + return; + } + case VK_FORMAT_B5G5R5A1_UNORM_PACK16: { + Uint16 word = 0; + Memcpy(&word, source, sizeof(word)); + rgba[2] = static_cast((word >> 11) & 0x1Fu) / 31.0f; + rgba[1] = static_cast((word >> 6) & 0x1Fu) / 31.0f; + rgba[0] = static_cast((word >> 1) & 0x1Fu) / 31.0f; + rgba[3] = static_cast(word & 0x1u); + return; + } + case VK_FORMAT_R4G4B4A4_UNORM_PACK16: { + Uint16 word = 0; + Memcpy(&word, source, sizeof(word)); + rgba[0] = static_cast((word >> 12) & 0xFu) / 15.0f; + rgba[1] = static_cast((word >> 8) & 0xFu) / 15.0f; + rgba[2] = static_cast((word >> 4) & 0xFu) / 15.0f; + rgba[3] = static_cast(word & 0xFu) / 15.0f; + return; + } + case VK_FORMAT_B4G4R4A4_UNORM_PACK16: { + Uint16 word = 0; + Memcpy(&word, source, sizeof(word)); + rgba[2] = static_cast((word >> 12) & 0xFu) / 15.0f; + rgba[1] = static_cast((word >> 8) & 0xFu) / 15.0f; + rgba[0] = static_cast((word >> 4) & 0xFu) / 15.0f; + rgba[3] = static_cast(word & 0xFu) / 15.0f; + return; + } + default: + return; + } + } + + static Bool DecodeReadbackRowsToWide(const Uint8* srcPixels, VkFormat srcFormat, GLsizei width, + GLsizei height, Vector& outWide, GLenum& outWideType) { + ReadbackSourceDesc desc{}; + if (!GetReadbackSourceDesc(srcFormat, desc)) { + return false; + } + const SizeT texelSize = VulkanRenderer::GetReadbackTexelSize(srcFormat); + if (texelSize == 0) { + return false; + } + const SizeT pixelCount = static_cast(width) * static_cast(height); + outWide.assign(pixelCount * 4 * sizeof(Uint32), 0); + + if (desc.sourceClass == ReadbackSourceClass::Float) { + outWideType = GL_FLOAT; + Float* wide = reinterpret_cast(outWide.data()); + for (SizeT i = 0; i < pixelCount; ++i) { + const Uint8* source = srcPixels + i * texelSize; + Float rgba[4] = {0.0f, 0.0f, 0.0f, 1.0f}; + if (desc.special != VK_FORMAT_UNDEFINED) { + DecodeReadbackTexelSpecialFloat(source, desc.special, rgba); + } else { + for (Int c = 0; c < desc.channels; ++c) { + Float value = 0.0f; + if (desc.componentBits == 8) { + if (desc.isSnorm) { + Int8 raw = 0; + Memcpy(&raw, source + c, sizeof(raw)); + value = std::max(static_cast(raw) / 127.0f, -1.0f); + } else { + value = static_cast(source[c]) / 255.0f; + } + } else { // 16 + if (desc.isSnorm) { + Int16 raw = 0; + Memcpy(&raw, source + static_cast(c) * 2, sizeof(raw)); + value = std::max(static_cast(raw) / 32767.0f, -1.0f); + } else { + Uint16 raw = 0; + Memcpy(&raw, source + static_cast(c) * 2, sizeof(raw)); + value = static_cast(raw) / 65535.0f; + } + } + if (desc.isSrgb && c < 3) { + value = SrgbToLinear(value); + } + rgba[c] = value; + } + if (desc.bgraSwizzle) { + std::swap(rgba[0], rgba[2]); + } + } + Memcpy(wide + i * 4, rgba, sizeof(rgba)); + } + return true; + } + + // Integer classes: decode to 4 x (U)Int32 per texel; missing alpha reads 1. + outWideType = desc.sourceClass == ReadbackSourceClass::SignedInt ? GL_INT : GL_UNSIGNED_INT; + Uint32* wide = reinterpret_cast(outWide.data()); + for (SizeT i = 0; i < pixelCount; ++i) { + const Uint8* source = srcPixels + i * texelSize; + Uint32 rgba[4] = {0, 0, 0, 1}; + if (srcFormat == VK_FORMAT_A2B10G10R10_UINT_PACK32) { + Uint32 word = 0; + Memcpy(&word, source, sizeof(word)); + rgba[0] = word & 0x3FFu; + rgba[1] = (word >> 10) & 0x3FFu; + rgba[2] = (word >> 20) & 0x3FFu; + rgba[3] = (word >> 30) & 0x3u; + } else { + for (Int c = 0; c < desc.channels; ++c) { + if (desc.componentBits == 8) { + if (desc.sourceClass == ReadbackSourceClass::SignedInt) { + Int8 raw = 0; + Memcpy(&raw, source + c, sizeof(raw)); + rgba[c] = static_cast(static_cast(raw)); + } else { + rgba[c] = source[c]; + } + } else if (desc.componentBits == 16) { + if (desc.sourceClass == ReadbackSourceClass::SignedInt) { + Int16 raw = 0; + Memcpy(&raw, source + static_cast(c) * 2, sizeof(raw)); + rgba[c] = static_cast(static_cast(raw)); + } else { + Uint16 raw = 0; + Memcpy(&raw, source + static_cast(c) * 2, sizeof(raw)); + rgba[c] = raw; + } + } else { + Memcpy(&rgba[c], source + static_cast(c) * 4, sizeof(Uint32)); + } + } + } + Memcpy(wide + i * 4, rgba, sizeof(rgba)); + } + return true; + } + static Bool PackReadbackToClientOrPbo(const Uint8* srcPixels, VkFormat srcFormat, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) { if (width <= 0 || height <= 0) { return true; } - if (type != GL_UNSIGNED_BYTE && type != GL_FLOAT) { - MGLOG_E("DirectVulkan readback skipped: unsupported type=0x%x", type); + + DirectGLES::ReadbackImpl::ReadbackChannelMapping mapping{}; + if (!DirectGLES::ReadbackImpl::GetReadbackChannelMapping(format, mapping) || + DirectGLES::ReadbackImpl::GetReadbackDstPixelSize(mapping, type) == 0) { + MGLOG_E("DirectVulkan readback skipped: unsupported format=0x%x type=0x%x", format, type); return false; } - const Int dstChannels = GetReadbackChannelCount(format); - if (dstChannels == 0) { - MGLOG_E("DirectVulkan readback skipped: unsupported format=0x%x", format); - return false; - } - - const auto packParams = MG_State::pGLContext->GetPixelStoreParameters(false); - const SizeT dstComponentBytes = type == GL_FLOAT ? sizeof(Float) : sizeof(Uint8); - const SizeT rowPixels = static_cast(packParams.RowLength > 0 ? packParams.RowLength : width); - const SizeT dstRowStride = AlignPixelRow(rowPixels * static_cast(dstChannels) * dstComponentBytes, - packParams.Alignment); - const SizeT dstOffset = static_cast(std::max(packParams.SkipRows, 0)) * dstRowStride + - static_cast(std::max(packParams.SkipPixels, 0)) * - static_cast(dstChannels) * dstComponentBytes; - const SizeT packedSize = dstOffset + - (static_cast(height - 1) * dstRowStride) + - (static_cast(width) * static_cast(dstChannels) * dstComponentBytes); - Vector packed(packedSize, 0); - - if (!VulkanRenderer::ConvertReadbackPixels(srcPixels, srcFormat, width, height, format, type, - dstRowStride, packed.data() + dstOffset)) { + Vector wide; + GLenum wideType = GL_FLOAT; + if (!DecodeReadbackRowsToWide(srcPixels, srcFormat, width, height, wide, wideType)) { MGLOG_E("DirectVulkan readback skipped: unsupported source format=%d", static_cast(srcFormat)); return false; } - const auto& pixelPackBufferObject = - MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject(); - if (pixelPackBufferObject) { - const SizeT pboOffset = reinterpret_cast(pixels); - if (pboOffset + packed.size() > pixelPackBufferObject->GetSize()) { - MGLOG_E("DirectVulkan readback skipped: pixel pack buffer is too small"); - return false; - } - pixelPackBufferObject->WritebackFromBackend({packed.data(), packed.size()}, pboOffset); - return true; + const Bool sourceIsInteger = wideType == GL_INT || wideType == GL_UNSIGNED_INT; + if (sourceIsInteger != mapping.isInteger) { + MGLOG_E("DirectVulkan readback skipped: integerness mismatch (format=0x%x source=%d)", + format, static_cast(srcFormat)); + return false; } - if (pixels != nullptr && !packed.empty()) { - Memcpy(pixels, packed.data(), packed.size()); - } - return true; + return DirectGLES::ReadbackImpl::StoreWideRowsToClient(wide.data(), wideType, width, height, + /*sliceCount=*/1, mapping, type, pixels, + /*applyPackImageParams=*/false); } } // namespace From 5e676b338b8f5c343ce4f68fe48ad370cecda710 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 26 Jul 2026 13:28:30 -0400 Subject: [PATCH 6/6] [Fix] (DirectVulkan): back legacy low-bit formats (RGB565/RGB5A1/RGBA4/R3G3B2/RGB4/RGBA2/RGB10/12) with their UNorm8/16 canonical shadow layouts and add capability fallbacks - they mapped to VK_FORMAT_UNDEFINED and crashed or wedged the GPU on upload; also admit 2DMSArray/CubeMap/3D color attachment targets in the render pass --- .../DirectVulkan/BackendObject_DirectVulkan.cpp | 14 ++++++++++++++ .../Renderer/VkRenderPassManager.cpp | 4 ++++ .../DirectVulkan/Renderer/VkTextureManager.cpp | 16 ++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index a10ee89a..686ba19b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -140,6 +140,20 @@ namespace MobileGL::MG_Backend::DirectVulkan { case TextureInternalFormat::RGB: case TextureInternalFormat::RGB8: return TextureInternalFormat::RGBA8; + // Legacy low-bit-depth formats with no (or rarely supported) native Vulkan + // encoding; a wider normalized fallback keeps at least the required precision. + case TextureInternalFormat::R3G3B2: + case TextureInternalFormat::RGB4: + case TextureInternalFormat::RGB5: + case TextureInternalFormat::RGBA2: + case TextureInternalFormat::RGBA4: + case TextureInternalFormat::RGB5A1: + return TextureInternalFormat::RGBA8; + case TextureInternalFormat::RGB10: + return TextureInternalFormat::RGB10A2; + case TextureInternalFormat::RGB12: + case TextureInternalFormat::RGBA12: + return TextureInternalFormat::RGBA16; case TextureInternalFormat::SRGB8: return TextureInternalFormat::SRGB8Alpha8; case TextureInternalFormat::RGB8Snorm: diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp index 99b276cb..fcf09ddc 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp @@ -791,6 +791,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { case TextureTarget::Texture2D: case TextureTarget::Texture2DArray: case TextureTarget::Texture2DMultisample: + case TextureTarget::Texture2DMultisampleArray: + case TextureTarget::Texture3D: + case TextureTarget::TextureCubeMap: + case TextureTarget::TextureCubeMapArray: case TextureTarget::TextureRectangle: { desc.flags = 0; desc.format = isDefaultFbo ? diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 2b52e333..8cb2dad9 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -375,7 +375,23 @@ namespace MobileGL::MG_Backend::DirectVulkan { switch (format) { case TextureInternalFormat::RGB: case TextureInternalFormat::RGB8: + // Legacy low-bit RGB formats share the UNorm8 canonical shadow layout (see + // TextureFormatProcessor), so they upload exactly like RGB8 with an alpha expand. + case TextureInternalFormat::R3G3B2: + case TextureInternalFormat::RGB4: + case TextureInternalFormat::RGB5: return {VK_FORMAT_R8G8B8A8_UNORM, true, 1, {0xFF, 0x00, 0x00, 0x00}}; + // Low-bit RGBA formats: UNorm8x4 canonical shadow, no expansion needed. + case TextureInternalFormat::RGBA2: + case TextureInternalFormat::RGBA4: + case TextureInternalFormat::RGB5A1: + return {VK_FORMAT_R8G8B8A8_UNORM, false, 0, {0, 0, 0, 0}}; + // 10/12-bit RGB(A): UNorm16 canonical shadow. + case TextureInternalFormat::RGB10: + case TextureInternalFormat::RGB12: + return {VK_FORMAT_R16G16B16A16_UNORM, true, 2, {0xFF, 0xFF, 0x00, 0x00}}; + case TextureInternalFormat::RGBA12: + return {VK_FORMAT_R16G16B16A16_UNORM, false, 0, {0, 0, 0, 0}}; case TextureInternalFormat::SRGB8: return {VK_FORMAT_R8G8B8A8_SRGB, true, 1, {0xFF, 0x00, 0x00, 0x00}}; case TextureInternalFormat::RGB8Snorm: