From 755a893d192a48c70408f864f18dff46fd8aebb6 Mon Sep 17 00:00:00 2001 From: ABTOPEMOHT <163136224+ABTOPEMOHT@users.noreply.github.com> Date: Thu, 5 Nov 2026 16:17:26 +0100 Subject: [PATCH] mesa-ios: expose S3TC/RGTC/BPTC through Mesa's CPU fallback in OSMesa (#566) Metal on A-series iPhones and iPads has no BC texture sampling, so MoltenVK and KosmicKrisp report no BC formats to Zink. Mesa still exposes GL_EXT_texture_compression_s3tc, _s3tc_srgb, ARB_texture_compression_rgtc, EXT_texture_compression_latc and ARB_texture_compression_bptc, and decodes those uploads to RGBA8 on the CPU, but only with allow_compressed_fallback. The DRI frontends read it from driconf, where it defaults to true. OSMesa memsets its context attribs and never reads driconf, so it stayed false. Games that upload DXT data without checking (Mewgenics) then got GL_INVALID_ENUM and drew those textures as opaque black. Patch 0009 turns the option on in OSMesa (MADEIRA_GL_COMPRESSED_FALLBACK=0 turns it off). tests/x64/gls3tc-x64.c checks a DXT1 and a DXT5 upload. Confirmed on an iPhone 12 Pro Max (A14) by forcing the same extensions on with MESA_EXTENSION_OVERRIDE: Mewgenics' textures draw correctly. Co-Authored-By: Claude Opus 5.5 Signed-off-by: ABTOPEMOHT <163136224+ABTOPEMOHT@users.noreply.github.com> --- ...009-osmesa-allow-compressed-fallback.patch | 27 ++++ build/mesa-ios/patches/README.md | 9 ++ tests/x64/gls3tc-x64.c | 116 ++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 build/mesa-ios/patches/0009-osmesa-allow-compressed-fallback.patch create mode 100644 tests/x64/gls3tc-x64.c diff --git a/build/mesa-ios/patches/0009-osmesa-allow-compressed-fallback.patch b/build/mesa-ios/patches/0009-osmesa-allow-compressed-fallback.patch new file mode 100644 index 0000000000..b71e2f161e --- /dev/null +++ b/build/mesa-ios/patches/0009-osmesa-allow-compressed-fallback.patch @@ -0,0 +1,27 @@ +--- a/src/gallium/frontends/osmesa/osmesa.c ++++ b/src/gallium/frontends/osmesa/osmesa.c +@@ -671,6 +671,24 @@ + attribs.options.disable_blend_func_extended = false; + attribs.options.disable_glsl_line_continuations = false; + attribs.options.force_glsl_version = 0; ++ /* Madeira: the DRI frontends read allow_compressed_fallback from driconf, ++ * where it defaults to true (DRI_CONF_ALLOW_COMPRESSED_FALLBACK in ++ * driinfo_gallium.h). OSMesa never reads driconf, so the memset above left ++ * it false. On a GPU without BC sampling (Metal on A-series iPhones and ++ * iPads: supportsBCTextureCompression = NO, so neither MoltenVK nor ++ * KosmicKrisp has BC formats), GL_EXT_texture_compression_s3tc, _s3tc_srgb, ++ * ARB_texture_compression_rgtc, EXT_texture_compression_latc and ++ * ARB_texture_compression_bptc were then missing. A game that uploads DXT ++ * data without checking for them (every desktop GPU has them) got ++ * GL_INVALID_ENUM, and the incomplete texture sampled as opaque black ++ * (Mewgenics, willfaust/Madeira#566: forcing these extensions on with ++ * MESA_EXTENSION_OVERRIDE fixed it). With the option on, Mesa exposes ++ * them and decodes those uploads to RGBA8 on the CPU ++ * (st_compressed_format_fallback); a GPU with BC keeps the native formats. ++ * MADEIRA_GL_COMPRESSED_FALLBACK=0 turns it off. ++ */ ++ attribs.options.allow_compressed_fallback = ++ debug_get_bool_option("MADEIRA_GL_COMPRESSED_FALLBACK", true); + + osmesa_init_st_visual(&attribs.visual, + PIPE_FORMAT_NONE, diff --git a/build/mesa-ios/patches/README.md b/build/mesa-ios/patches/README.md index 2697393104..7e79e296cc 100644 --- a/build/mesa-ios/patches/README.md +++ b/build/mesa-ios/patches/README.md @@ -11,6 +11,15 @@ licence (MIT). (MoltenVK only), the vertex stage first stores zero into every varying component the fragment shader reads, and both stages get one variable per slot with the same shape. `MADEIRA_ZINK_LINK_FS_INPUTS=0` turns this off, and `=1` turns it on for other drivers. +- `0009-osmesa-allow-compressed-fallback.patch`: Metal on A-series iPhones and iPads has no BC + texture sampling (`supportsBCTextureCompression = NO`), so MoltenVK and KosmicKrisp report no BC + formats. Mesa can still expose S3TC (DXT1/3/5), RGTC, LATC and BPTC and decode those uploads to + RGBA8 on the CPU, but only when `allow_compressed_fallback` is on. The DRI frontends take it from + driconf, where it defaults to true. OSMesa never reads driconf and left it false, so those + extensions were missing, a game that uploads DXT data without checking got `GL_INVALID_ENUM`, and + the incomplete texture sampled as opaque black (Mewgenics, #566). OSMesa now turns the + option on; a GPU with BC keeps the native formats. `MADEIRA_GL_COMPRESSED_FALLBACK=0` turns it + off. - `kosmickrisp-ios-0001-wsi-metal-no-display-sync-on-ios.patch`: the Metal WSI sets `CAMetalLayer.displaySyncEnabled`, which iOS does not have. On iOS it is skipped, so an immediate-mode swapchain presents with vsync. diff --git a/tests/x64/gls3tc-x64.c b/tests/x64/gls3tc-x64.c new file mode 100644 index 0000000000..cb33faa34d --- /dev/null +++ b/tests/x64/gls3tc-x64.c @@ -0,0 +1,116 @@ +/* OpenGL: DXT (S3TC) textures must sample on a GPU without BC formats. + * + * Metal on A-series iPhones and iPads has no BC sampling, so Zink sees no + * DXT formats. Mesa then exposes GL_EXT_texture_compression_s3tc only with + * allow_compressed_fallback, which OSMesa left off until + * build/mesa-ios/patches/0009: glCompressedTexImage2D failed with + * GL_INVALID_ENUM, and a game's DXT textures drew black (Mewgenics, #566). + * + * Uploads one solid-green 4x4 block as DXT1 and as DXT5, draws each with + * GL_REPLACE over a red clear and a blue vertex colour, and reads one pixel + * back: green = the texture sampled; blue = the texture was incomplete, so + * fixed function drew without it (a shader samples such a texture as black). + * + * Writes gls3tc-result.txt next to the exe. Exit code 0 = pass. + * Build: tests/x64/build.sh gls3tc-x64 (link -lopengl32 -lgdi32 -luser32) + */ +#include +#include +#include +#include +#include + +#define GL_COMPRESSED_RGB_S3TC_DXT1_EXT 0x83F0 +#define GL_COMPRESSED_RGBA_S3TC_DXT5_EXT 0x83F3 +typedef void (APIENTRY *compressed_tex_image_2d_fn)(GLenum, GLint, GLenum, GLsizei, GLsizei, GLint, GLsizei, + const void *); + +static FILE *out; +static void say(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vprintf(fmt, ap); + va_end(ap); + if (out) { va_start(ap, fmt); vfprintf(out, fmt, ap); va_end(ap); fflush(out); } +} + +/* One 4x4 block, every texel colour0 = RGB565 0x07e0 (pure green). DXT5 adds + * an alpha block with alpha0 = alpha1 = 255 and every index 0. */ +static const unsigned char dxt1_green[8] = { 0xe0, 0x07, 0xe0, 0x07, 0, 0, 0, 0 }; +static const unsigned char dxt5_green[16] = { 0xff, 0xff, 0, 0, 0, 0, 0, 0, 0xe0, 0x07, 0x00, 0x00, 0, 0, 0, 0 }; + +static int draw_and_check(compressed_tex_image_2d_fn upload, GLenum format, const void *data, GLsizei size, + const char *name) +{ + GLuint tex = 0; + unsigned char px[4] = {0}; + GLenum err; + + while (glGetError() != GL_NO_ERROR) {} + glGenTextures(1, &tex); + glBindTexture(GL_TEXTURE_2D, tex); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + upload(GL_TEXTURE_2D, 0, format, 4, 4, 0, size, data); + err = glGetError(); + + glViewport(0, 0, 64, 64); + glClearColor(1, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + glEnable(GL_TEXTURE_2D); + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); + glColor3f(0, 0, 1); + glBegin(GL_QUADS); + glTexCoord2f(0, 0); glVertex2f(-1, -1); + glTexCoord2f(1, 0); glVertex2f(1, -1); + glTexCoord2f(1, 1); glVertex2f(1, 1); + glTexCoord2f(0, 1); glVertex2f(-1, 1); + glEnd(); + glDisable(GL_TEXTURE_2D); + glReadPixels(8, 8, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px); + glDeleteTextures(1, &tex); + + int green = px[0] < 32 && px[1] > 224 && px[2] < 32; + say("%s %s: upload error 0x%x, pixel %u,%u,%u (%s)\n", green ? "PASSED" : "FAILED", name, err, px[0], px[1], + px[2], green ? "the texture sampled" : "blue: the texture was incomplete"); + return green; +} + +int main(void) +{ + char path[MAX_PATH], *slash; + GetModuleFileNameA(NULL, path, sizeof(path)); + if ((slash = strrchr(path, '\\'))) strcpy(slash + 1, "gls3tc-result.txt"); + out = fopen(path, "w"); + + WNDCLASSA wc = {0}; + wc.lpfnWndProc = DefWindowProcA; wc.hInstance = GetModuleHandleA(NULL); wc.lpszClassName = "gls3tc"; + RegisterClassA(&wc); + HWND hwnd = CreateWindowA("gls3tc", "gls3tc", WS_OVERLAPPEDWINDOW | WS_VISIBLE, 0, 0, 256, 256, + NULL, NULL, wc.hInstance, NULL); + HDC dc = GetDC(hwnd); + PIXELFORMATDESCRIPTOR pfd = { sizeof(pfd), 1, PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, + PFD_TYPE_RGBA, 32 }; + SetPixelFormat(dc, ChoosePixelFormat(dc, &pfd), &pfd); + HGLRC rc = wglCreateContext(dc); + if (!rc || !wglMakeCurrent(dc, rc)) { say("FAIL: no OpenGL context\n"); return 2; } + say("GL_VERSION %s\n", (const char *)glGetString(GL_VERSION)); + say("GL_RENDERER %s\n", (const char *)glGetString(GL_RENDERER)); + + const char *ext = (const char *)glGetString(GL_EXTENSIONS); + int has_s3tc = ext && strstr(ext, "GL_EXT_texture_compression_s3tc") != NULL; + say("GL_EXT_texture_compression_s3tc %s\n", has_s3tc ? "listed" : "MISSING"); + + compressed_tex_image_2d_fn upload = (compressed_tex_image_2d_fn)wglGetProcAddress("glCompressedTexImage2D"); + if (!upload) { say("FAIL: glCompressedTexImage2D missing\n"); return 2; } + + int ok = draw_and_check(upload, GL_COMPRESSED_RGB_S3TC_DXT1_EXT, dxt1_green, sizeof(dxt1_green), "DXT1"); + ok &= draw_and_check(upload, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, dxt5_green, sizeof(dxt5_green), "DXT5"); + SwapBuffers(dc); + Sleep(1000); + + say("RESULT %s\n", ok && has_s3tc ? "pass" : "fail (exit 1)"); + if (out) fclose(out); + return ok && has_s3tc ? 0 : 1; +}