Compare commits

..
14 Commits
Author SHA1 Message Date
BZLZHH 8025745fa3 [Chore] (Version): bump version to 26.8 for android plugin 2026-08-01 23:46:58 +08:00
BZLZHH 4a533a215a [Chore] (MG_Backend): fix compiling error. 2026-08-01 23:21:35 +08:00
BZLZHH 0a5d7ceb6c [Feat] (DirectGLES): implement GL_ANY_SAMPLES_PASSED occlusion queries
DirectGLES never registered BeginOcclusionQuery/EndOcclusionQuery, so
the frontend rejected the occlusion query targets entirely; the CTS
tests that use them (e.g. packed_depth_stencil.verify_partial/mixed_
attachments) left a stray GL_INVALID_ENUM that a later, unrelated
glGetError() check would report as its own failure
("Uploading buffer data failed" at gluDrawUtil.cpp:363).

Occlusion queries are core ES3 (glGenQueries/glBeginQuery(GL_ANY_
SAMPLES_PASSED, ...)/glEndQuery/glGetQueryObjectuiv), unlike the timer
queries which need GL_EXT_disjoint_timer_query, so they're wired up
unconditionally (independent of MOBILEGL_DISABLE_TIMERQUERY) using the
same handle-based GetQueryResult64/DeleteBackendQuery plumbing already
shared with timer queries. GetQueryResult64 now reads the 0/1 result
through the core 32-bit glGetQueryObjectuiv getter for occlusion
handles instead of the timer-only 64-bit GL_EXT_disjoint_timer_query
getter, since a driver can fully support core occlusion queries while
lacking that extension entirely.
2026-08-01 11:20:38 -04:00
BZLZHH ac81185968 [Fix] (DirectGLES): implement GL_DEPTH_STENCIL readback for ReadPixels/GetTexImage
Neither ReadPixels nor GetTexImage recognized format=GL_DEPTH_STENCIL
(type GL_UNSIGNED_INT_24_8 / GL_FLOAT_32_UNSIGNED_INT_24_8_REV): it
matched none of the native-passthrough gates nor the color-channel
conversion mapping, so both silently no-op'd (logging a compiled-out
MGLOG_E) and left the caller's buffer untouched. Real GLES/GL drivers
already implement this readback natively, so widen the native-pair
gates to include it.

GetTexImage additionally always attached the source texture to its
scratch FBO as GL_COLOR_ATTACHMENT0, which a depth-stencil texture
cannot be (framebuffer-incomplete) - route it through the existing
EnsureDepthAttachment2D(..., withStencil=true) path instead and skip
the color-only glReadBuffer call for that format.

Fixes KHR-GL3{2,3}.packed_depth_stencil.verify_read_pixels,
verify_get_tex_image, and verify_copy_tex_image (which depends on
GetTexImage internally) for both depth24_stencil8 and
depth32f_stencil8.
2026-08-01 11:14:33 -04:00
BZLZHH 9e861f3f7a [Test] (tools/cts): extend the fbo-harness waiver to the Espryt renderer
Same test-methodology artifact as Magma (dEQP's fbo-surface-type
wrapper FBO being mistaken for the true default framebuffer by
ApiCoverageTestCase's ReadBuffer coverage sub-test) - the waiver's
renderer_list only matched "Magma*", so KHR-GL3{0,1,2}.api.coverage
still reported Fail under the DirectGLES (Espryt) backend. Add
"Espryt*" to the same waiver entry.
2026-08-01 10:48:11 -04:00
BZLZHH da6f75dbd1 [Fix] (DirectGLES): cap advertised GL_MAX_SAMPLE_MASK_WORDS to 1
MobileGL's sample-mask state is a single 32-bit word (RenderState::
SampleMaskValue) and SampleMaski_State() hard-rejects any maskNumber
other than 0. DirectGLES forwarded the real underlying driver's
GL_MAX_SAMPLE_MASK_WORDS unmodified (NVIDIA's GLES driver reports 2),
so dEQP's per-test-case gluStateReset - which always calls
glSampleMaski up to that reported word count - hit GL_INVALID_VALUE on
word 1 after every single case and aborted the whole glcts process.
Each restart only got through one more case before repeating, which
run_cts_local.py recorded as a wall of per-case crashes (63 in
packed_pixels.rectangle alone) and tripped its "many empty chunks"
abort heuristic partway through the GL32 suite. 1 is the spec-required
minimum and is what MobileGL actually implements, so cap to it instead
of forwarding the raw driver limit.
2026-08-01 09:59:26 -04:00
BZLZHH 951da362f7 [Fix] (submodules): point include/FastSTL at the fork's already-pushed fix
Our local-only commit f8567f6 for the erase(iterator) bug was never
pushed to MobileGL-Dev/FastSTL and broke CI's submodule checkout
("not our ref"). The fork's own main branch already carries an
equivalent fix (022211c, same root cause) plus a perf improvement on
erase(key) (34f55f9), so switch to that instead of pushing a redundant
duplicate that would diverge from it.
2026-08-01 09:25:01 -04:00
BZLZHH 4c929b9b3f Merge branch 'dev' of github.com:MobileGL-Dev/MobileGL into dev 2026-08-01 21:03:14 +08:00
BZLZHH 8d5072543a [Test] (tools/cts): waive api.coverage's fbo-surface-type wrapper-FBO artifact
KHR-GL3{0,1,2,3}.api.coverage's ReadBuffer coverage sub-test captures
GL_READ_BUFFER while dEQP's own fbo-surface-type wrapper FBO is bound
(a real, non-zero-named FBO, not framebuffer 0), then later deletes an
unrelated FBO of its own. Per the GL spec, deleting a bound FBO
implicitly rebinds framebuffer target 0 - the true default framebuffer
this time, not the wrapper - and restoring the captured
GL_COLOR_ATTACHMENTn value against it correctly raises GL_INVALID_ENUM
(only FRONT/BACK-style tokens are valid there). This is a spec-correct
response to a --deqp-surface-type=fbo-only test-methodology artifact,
not a MobileGL conformance defect, and cannot occur on a real
window/pbuffer-backed run where framebuffer 0 is genuinely bound
throughout. Add a waiver (dEQP's own mechanism for exactly this kind of
known non-defect) instead of weakening the (correct) validation, and
wire --waiver-file through run_cts_local.py.
2026-08-01 08:53:35 -04:00
BZLZHH 5de2b9e3e9 [Chore] (Version): bump version to 26.8 2026-08-01 20:35:31 +08:00
BZLZHH 9192d156d1 [Fix] (DirectVulkan): ReadPixels materializes pending clears before resolving the blit binding
ResolveColorBlitBinding cached a RenderbufferResource*/TextureResource*
(trackedLayout) before the pending-clear materialization step ran. For an
attachment that had never been part of any render pass yet (e.g. a
GL_NONE draw buffer slot read back via an explicit glReadBuffer), the
materialize call was the first thing to touch its resource, and creating
that entry in the UnorderedMap (FastSTL, open-addressing) can rehash and
invalidate every previously-taken pointer into the map - including the
one just cached. The read then saw a stale VK_IMAGE_LAYOUT_UNDEFINED and
silently bailed (via a compiled-out MGLOG_E in release builds), leaving
the client buffer untouched. Reordering so the clear is materialized
first, then the binding resolved, guarantees the pointer reflects the
final resource state. Fixes KHR-GL3{0,1,2,3}.draw_buffers.draw_buffers_1.
2026-08-01 08:09:49 -04:00
BZLZHH 27cdfbc0ca [Fix] (DirectVulkan): advertise GL_ARB_explicit_attrib_location unconditionally
layout(location=N) out qualifiers are fully supported (glslang parses them,
SPIR-V expresses them natively), but the extension string was never
advertised. KHR-GL3{0,1,2}.draw_buffers.draw_buffers_1 builds its MRT
fragment shader with per-attachment layout(location=i) outputs only when
GL_ARB_explicit_attrib_location is reported or the context is >=3.3; below
that it fell back to a single non-indexed `out vec4`, which only ever
targets location 0, leaving every draw buffer past slot 0 unwritten.
2026-08-01 08:09:35 -04:00
BZLZHH 4567c3b468 [Fix] (DirectVulkan): advertise GL_ARB_texture_multisample unconditionally
GL_ARB_texture_multisample was implemented (glTexImage2D/3DMultisample,
GL_TEXTURE_2D_MULTISAMPLE) but never listed in BuildAdvertisedExtensions.
dEQP's GL 3.1 context loader only binds non-core-until-3.2 entry points
when the extension string is present, so glTexImage2DMultisample stayed
a null function pointer and KHR-GL31.texture_size_promotion.functional
crashed on the null call. GL 3.2+ contexts treat it as core and were
unaffected.
2026-08-01 08:09:20 -04:00
BZLZHH d18c6a1bae [Fix] (DirectVulkan): Xlib surface fallback for ICDs without VK_EXT_headless_surface
Real drivers (NVIDIA proprietary Linux) don't implement VK_EXT_headless_surface,
which the pbuffer path required unconditionally, hard-aborting at CreateInstance.
CreateInstance now detects instance-extension support and requests
VK_KHR_xlib_surface instead when headless is unavailable; CreateSurface creates
an unmapped Xlib window purely to obtain a VkSurfaceKHR, then proceeds through
the existing swapchain path unchanged. Shutdown destroys the window it owns.
Lavapipe and other headless-capable ICDs are unaffected.
2026-08-01 06:38:30 -04:00
12 changed files with 241 additions and 21 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ namespace MobileGL::MG_Config {
inline const String ProjectName = "MobileGL";
inline const String CoreName = "MobileGL Core";
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
inline const Version CoreVersion = {26, 7, 0, "-dev", VersionType::Development};
inline const Version CoreVersion = {26, 8, 0, "-dev", VersionType::Development};
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
inline const Uint64 CacheVersion = 0;
@@ -935,11 +935,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
funcsTable.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
funcsTable.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
funcsTable.GL.QueryCounterTimestamp = QueryCounterTimestamp;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTable.GL.GetGpuTimestampNs = GetGpuTimestampNs;
}
// Occlusion queries are core ES3 (independent of MOBILEGL_DISABLE_TIMERQUERY)
// and share the handle-based result/delete entries, which must exist even
// when the timer-query group above is disabled.
funcsTable.GL.BeginOcclusionQuery = BeginOcclusionQuery;
funcsTable.GL.EndOcclusionQuery = EndOcclusionQuery;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
funcsTableInitialized = true;
}
return funcsTable;
+66 -6
View File
@@ -3632,12 +3632,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Bool IsLegacyNativeReadPixelsFormat(GLenum format) {
return format == GL_RGBA || format == GL_RGBA_INTEGER || format == GL_RED || format == GL_RED_INTEGER ||
format == GL_DEPTH_COMPONENT || format == GL_STENCIL_INDEX;
format == GL_DEPTH_COMPONENT || format == GL_STENCIL_INDEX || format == GL_DEPTH_STENCIL;
}
static Bool IsLegacyNativeReadPixelsType(GLenum type) {
// GL_UNSIGNED_INT_24_8 / GL_FLOAT_32_UNSIGNED_INT_24_8_REV are only ever valid
// paired with GL_DEPTH_STENCIL (packed_depth_stencil.verify_read_pixels); the real
// driver already implements this readback natively.
return type == GL_UNSIGNED_BYTE || type == GL_UNSIGNED_INT || type == GL_UNSIGNED_INT_2_10_10_10_REV ||
type == GL_INT || type == GL_FLOAT;
type == GL_INT || type == GL_FLOAT || type == GL_UNSIGNED_INT_24_8 ||
type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV;
}
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
@@ -3782,6 +3786,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (format == GL_RGBA_INTEGER) {
return type == GL_INT || type == GL_UNSIGNED_INT || type == GL_UNSIGNED_INT_2_10_10_10_REV;
}
if (format == GL_DEPTH_STENCIL) {
return type == GL_UNSIGNED_INT_24_8 || type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV;
}
return false;
}
@@ -3850,7 +3857,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("GetTexImage: attaching level %d to the scratch FBO", level);
const GLenum backendAttachTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(
MG_Util::ConvertGLEnumToTextureUploadTarget(target));
if (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY) {
// GL_DEPTH_STENCIL can't be attached as a color attachment (glCheckFramebufferStatus
// would report it incomplete); it has its own combined depth+stencil attachment point.
// glReadBuffer only selects among color attachments, so it does not apply here.
if (format == GL_DEPTH_STENCIL) {
ScratchFBOImpl::EnsureDepthAttachment2D(
tempFB, GL_READ_FRAMEBUFFER, backendTexId,
backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, level, /*withStencil=*/true);
} else if (backendAttachTarget == GL_TEXTURE_3D || backendAttachTarget == GL_TEXTURE_2D_ARRAY) {
// ES cannot attach 3D/array textures through glFramebufferTexture2D; read layer 0. Reads
// of deeper slices are served from the CPU shadow instead (see the shadow-first branch).
ScratchFBOImpl::EnsureColorAttachmentLayer(tempFB, GL_READ_FRAMEBUFFER, backendTexId, level, 0);
@@ -3859,8 +3873,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
tempFB, GL_READ_FRAMEBUFFER, backendTexId,
backendAttachTarget == GL_UNKNOWN_MGL ? target : backendAttachTarget, level);
}
MGLOG_D("GetTexImage: glReadBuffer(GL_COLOR_ATTACHMENT0)");
ScratchFBOImpl::EnsureReadBuffer(tempFB, GL_COLOR_ATTACHMENT0);
if (format != GL_DEPTH_STENCIL) {
MGLOG_D("GetTexImage: glReadBuffer(GL_COLOR_ATTACHMENT0)");
ScratchFBOImpl::EnsureReadBuffer(tempFB, GL_COLOR_ATTACHMENT0);
}
GLenum fbStatus = g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER);
MGLOG_D("GetTexImage: GL_READ_FRAMEBUFFER status = %s", MG_Util::ConvertGLEnumToString(fbStatus).c_str());
@@ -4344,6 +4360,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
struct GLESQueryObject {
GLuint queryId = 0;
Uint contextGeneration = 0;
// GL_ANY_SAMPLES_PASSED result is 0/1 and only ever reachable through the
// core (non-extension) glGetQueryObjectuiv getter - GL_EXT_disjoint_timer_query's
// 64-bit glGetQueryObjectui64vEXT is timer-specific and may be entirely absent
// on drivers that otherwise fully support core ES3 occlusion queries.
Bool isOcclusion = false;
};
}
@@ -4536,6 +4557,38 @@ namespace MobileGL::MG_Backend::DirectGLES {
return new GLESQueryObject{queryId, g_syncContextGeneration};
}
// GL_SAMPLES_PASSED/GL_ANY_SAMPLES_PASSED(_CONSERVATIVE) occlusion queries. Unlike the
// timer queries above, these are core ES 3.0 (no GL_EXT_disjoint_timer_query needed).
Bool AreOcclusionQueriesSupported() {
return g_GLESFuncs.glGenQueries && g_GLESFuncs.glDeleteQueries && g_GLESFuncs.glBeginQuery &&
g_GLESFuncs.glEndQuery && g_GLESFuncs.glGetQueryObjectuiv;
}
BackendQueryHandle BeginOcclusionQuery() {
if (!IsBackendContextCurrentOnThisThread() || !AreOcclusionQueriesSupported()) {
return nullptr;
}
GLuint queryId = 0;
g_GLESFuncs.glGenQueries(1, &queryId);
if (queryId == 0) {
return nullptr;
}
// ES only implements the boolean ANY_SAMPLES_PASSED variant, not an exact
// GL_SAMPLES_PASSED count; the frontend already coerces ANY_SAMPLES_PASSED*
// targets to boolean, and desktop GL_SAMPLES_PASSED reads a 0/1 approximation.
g_GLESFuncs.glBeginQuery(GL_ANY_SAMPLES_PASSED, queryId);
return new GLESQueryObject{queryId, g_syncContextGeneration, /*isOcclusion=*/true};
}
void EndOcclusionQuery(BackendQueryHandle handle) {
const auto* query = static_cast<GLESQueryObject*>(handle);
if (query == nullptr || query->contextGeneration != g_syncContextGeneration ||
!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glEndQuery) {
return;
}
g_GLESFuncs.glEndQuery(GL_ANY_SAMPLES_PASSED);
}
Bool IsQueryResultAvailable(BackendQueryHandle handle) {
const auto* query = static_cast<GLESQueryObject*>(handle);
// Null/stale handles report available so the frontend proceeds to
@@ -4562,7 +4615,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// report it as produced and let the frontend cache it and release
// the handle.
if (query == nullptr || query->contextGeneration != g_syncContextGeneration ||
!g_GLESFuncs.glGetQueryObjectuiv || !g_GLESFuncs.glGetQueryObjectui64vEXT) {
!g_GLESFuncs.glGetQueryObjectuiv ||
(!query->isOcclusion && !g_GLESFuncs.glGetQueryObjectui64vEXT)) {
return true;
}
// A thread that does not own the ES context cannot issue GL calls,
@@ -4600,6 +4654,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// context switch) the result may be garbage, which is tolerable for
// an F3 GPU% readout, and consuming the latched flag here could hide
// the event from another observer.
if (query->isOcclusion) {
GLuint result32 = 0;
g_GLESFuncs.glGetQueryObjectuiv(query->queryId, GL_QUERY_RESULT, &result32);
*outNanoseconds = static_cast<Uint64>(result32);
return true;
}
GLuint64 result = 0;
g_GLESFuncs.glGetQueryObjectui64vEXT(query->queryId, GL_QUERY_RESULT, &result);
*outNanoseconds = static_cast<Uint64>(result);
@@ -130,6 +130,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendQueryHandle BeginTimeElapsedQuery();
void EndTimeElapsedQuery(BackendQueryHandle query);
BackendQueryHandle QueryCounterTimestamp();
// GL_ANY_SAMPLES_PASSED(_CONSERVATIVE) occlusion queries: core ES3, independent of
// GL_EXT_disjoint_timer_query and of MOBILEGL_DISABLE_TIMERQUERY. Results/deletion
// flow through GetQueryResult64/DeleteBackendQuery like the timer queries above.
BackendQueryHandle BeginOcclusionQuery();
void EndOcclusionQuery(BackendQueryHandle query);
Bool IsQueryResultAvailable(BackendQueryHandle query);
// Returns true when a final value landed in *outNanoseconds (a zero for
// null or stale-generation handles IS final: the frontend may cache it
@@ -526,10 +526,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
E_GL_ARB_multi_draw_indirect, E_GL_ARB_indirect_parameters,
E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample,
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
E_GL_ARB_texture_multisample, E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access,
E_GL_ARB_shader_draw_parameters, E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug,
E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack,
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size};
E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
E_GL_ARB_explicit_attrib_location};
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
extensions.push_back(E_GL_KHR_shader_subgroup);
}
@@ -2866,6 +2866,15 @@ void main() {
#if defined(VK_USE_PLATFORM_XLIB_KHR)
if (m_platformDisplay != nullptr) {
if (m_ownsFallbackXlibWindow && m_window != 0 && m_platformLibrary != nullptr) {
using XDestroyWindowFn = int (*)(Display*, Window);
auto* destroyWindow = reinterpret_cast<XDestroyWindowFn>(dlsym(m_platformLibrary, "XDestroyWindow"));
if (destroyWindow) {
destroyWindow(static_cast<Display*>(m_platformDisplay), static_cast<Window>(m_window));
}
m_window = 0;
m_ownsFallbackXlibWindow = false;
}
using XCloseDisplayFn = int (*)(Display*);
auto* closeDisplay = reinterpret_cast<XCloseDisplayFn>(m_platformCloseDisplay);
if (closeDisplay) {
@@ -6973,11 +6982,14 @@ void main() {
}
const Bool readIsDefaultFbo = readFbo->IsDefaultFramebuffer();
BlitImageBinding srcBinding{};
if (!ResolveColorBlitBinding(*readFbo, true, m_imageIndexAcquired, m_swapchainObject, *m_textureManager,
*m_renderPassManager, srcBinding)) {
return;
}
// Materialize any pending clear on the read-buffer attachment BEFORE resolving the
// blit binding below: for a renderbuffer/texture that has never been part of any
// render pass yet (e.g. a GL_NONE draw buffer slot whose attachment is only ever
// touched via an explicit glReadBuffer), materializing lazily creates its backing
// Vulkan resource for the first time. UnorderedMap (FastSTL, open-addressing) may
// rehash on that insertion, invalidating any RenderbufferResource*/TextureResource*
// obtained beforehand - so ResolveColorBlitBinding's cached `trackedLayout` pointer
// must be taken AFTER this, never before it.
if (!readIsDefaultFbo) {
const auto& sourceAttachment = readFbo->GetAttachment(readFbo->GetReadBuffer());
auto sourceTexture = sourceAttachment.GetTexture();
@@ -6995,6 +7007,12 @@ void main() {
}
}
BlitImageBinding srcBinding{};
if (!ResolveColorBlitBinding(*readFbo, true, m_imageIndexAcquired, m_swapchainObject, *m_textureManager,
*m_renderPassManager, srcBinding)) {
return;
}
const VkImageLayout srcOriginalLayout = readIsDefaultFbo
? m_swapchainObject.GetImageLayout(m_imageIndexAcquired)
: *srcBinding.trackedLayout;
@@ -9284,6 +9302,18 @@ void main() {
if (!m_window) {
#ifdef VK_USE_PLATFORM_METAL_EXT
exts.push_back(VK_EXT_METAL_SURFACE_EXTENSION_NAME);
#elif defined VK_USE_PLATFORM_XLIB_KHR
m_headlessSurfaceSupported = IsExtensionSupported(m_extensions, VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
if (m_headlessSurfaceSupported) {
exts.push_back(VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
} else {
// Real ICDs (e.g. NVIDIA's proprietary Linux driver) may not implement
// VK_EXT_headless_surface at all. CreateSurface() falls back to a
// hidden Xlib window in that case, so request that extension instead.
MGLOG_I("%s not available; falling back to a hidden %s surface for the pbuffer context.",
VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME, VK_KHR_XLIB_SURFACE_EXTENSION_NAME);
exts.push_back(VK_KHR_XLIB_SURFACE_EXTENSION_NAME);
}
#else
exts.push_back(VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME);
#endif
@@ -9903,7 +9933,17 @@ void main() {
VkPhysicalDeviceProperties2 properties2{};
properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
properties2.pNext = &xfbProperties;
vkGetPhysicalDeviceProperties2(m_physicalDevice.handle, &properties2);
// Resolved via proc addr: vkGetPhysicalDeviceProperties2 is Vulkan 1.1, and Android's
// libvulkan.so only exports it from API 28 while minSdk is 26.
auto getPhysicalDeviceProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2"));
if (getPhysicalDeviceProperties2 == nullptr) {
getPhysicalDeviceProperties2 = reinterpret_cast<PFN_vkGetPhysicalDeviceProperties2>(
vkGetInstanceProcAddr(m_instance, "vkGetPhysicalDeviceProperties2KHR"));
}
if (getPhysicalDeviceProperties2 != nullptr) {
getPhysicalDeviceProperties2(m_physicalDevice.handle, &properties2);
}
m_xfbQueriesSupported = xfbProperties.transformFeedbackQueries == VK_TRUE &&
s_vkCmdBeginQueryIndexedEXT != nullptr && s_vkCmdEndQueryIndexedEXT != nullptr;
}
@@ -9985,6 +10025,21 @@ void main() {
m_window = reinterpret_cast<NativeWindowType>(
CreateInternalMetalLayer(m_config.SurfaceWidth, m_config.SurfaceHeight, &m_platformDisplay));
m_platformLibrary = reinterpret_cast<void*>(m_window);
#elif defined VK_USE_PLATFORM_XLIB_KHR
if (m_headlessSurfaceSupported) {
auto* createHeadlessSurface =
reinterpret_cast<PFN_vkCreateHeadlessSurfaceEXT>(
vkGetInstanceProcAddr(m_instance, "vkCreateHeadlessSurfaceEXT"));
MOBILEGL_ASSERT(createHeadlessSurface != nullptr,
"VK_EXT_headless_surface is not available for DirectVulkan pbuffer surface");
VkHeadlessSurfaceCreateInfoEXT sci{VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT};
VK_VERIFY(createHeadlessSurface(m_instance, &sci, nullptr, &m_surface),
"vkCreateHeadlessSurfaceEXT failed");
return;
}
// VK_EXT_headless_surface unavailable on this ICD: fall through to the
// Xlib branch below, which creates a hidden window since m_window is
// still null here.
#else
auto* createHeadlessSurface =
reinterpret_cast<PFN_vkCreateHeadlessSurfaceEXT>(
@@ -10019,7 +10074,8 @@ void main() {
sci.pLayer = reinterpret_cast<const void*>(m_window);
VK_VERIFY(vkCreateMetalSurfaceEXT(m_instance, &sci, nullptr, &m_surface), "vkCreateMetalSurfaceEXT failed");
#elif defined VK_USE_PLATFORM_XLIB_KHR
MOBILEGL_ASSERT(m_window, "X11 Window is null");
// m_window may legitimately still be null here: the pbuffer/headless-fallback
// path below creates its own window when the caller didn't provide one.
void* x11Lib = dlopen("libX11.so.6", RTLD_LOCAL | RTLD_NOW);
if (!x11Lib) {
@@ -10039,6 +10095,38 @@ void main() {
m_platformLibrary = x11Lib;
m_platformCloseDisplay = reinterpret_cast<void*>(xCloseDisplay);
if (!m_window) {
// Pbuffer/headless fallback: the ICD didn't implement
// VK_EXT_headless_surface (e.g. NVIDIA's proprietary Linux driver), so
// CreateInstance() requested VK_KHR_xlib_surface instead and left
// m_window null for us to fill in here. This window is never mapped -
// it exists only to give the WSI a valid drawable - so nothing is ever
// shown on screen; the swapchain image backs the GL default framebuffer
// exactly like the headless-surface path does.
using XDefaultRootWindowFn = Window (*)(Display*);
using XDefaultScreenFn = int (*)(Display*);
using XBlackPixelFn = unsigned long (*)(Display*, int);
using XCreateSimpleWindowFn = Window (*)(Display*, Window, int, int, unsigned int, unsigned int,
unsigned int, unsigned long, unsigned long);
auto* xDefaultRootWindow = reinterpret_cast<XDefaultRootWindowFn>(dlsym(x11Lib, "XDefaultRootWindow"));
auto* xDefaultScreen = reinterpret_cast<XDefaultScreenFn>(dlsym(x11Lib, "XDefaultScreen"));
auto* xBlackPixel = reinterpret_cast<XBlackPixelFn>(dlsym(x11Lib, "XBlackPixel"));
auto* xCreateSimpleWindow =
reinterpret_cast<XCreateSimpleWindowFn>(dlsym(x11Lib, "XCreateSimpleWindow"));
MOBILEGL_ASSERT(xDefaultRootWindow && xDefaultScreen && xBlackPixel && xCreateSimpleWindow,
"Failed to resolve XCreateSimpleWindow dependencies for the Xlib pbuffer fallback");
const int screen = xDefaultScreen(display);
const Uint32 width = std::max<Uint32>(m_config.SurfaceWidth, 1);
const Uint32 height = std::max<Uint32>(m_config.SurfaceHeight, 1);
const Window fallbackWindow = xCreateSimpleWindow(display, xDefaultRootWindow(display), 0, 0, width,
height, 0, xBlackPixel(display, screen),
xBlackPixel(display, screen));
MOBILEGL_ASSERT(fallbackWindow != 0, "XCreateSimpleWindow failed for the Xlib pbuffer fallback");
m_window = static_cast<NativeWindowType>(fallbackWindow);
m_ownsFallbackXlibWindow = true;
}
VkXlibSurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR};
sci.dpy = display;
sci.window = static_cast<Window>(m_window);
@@ -431,6 +431,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void* m_platformDisplay = nullptr;
void* m_platformLibrary = nullptr;
void* m_platformCloseDisplay = nullptr;
// Some real ICDs (e.g. NVIDIA's proprietary Linux driver) don't implement
// VK_EXT_headless_surface at all. Detected once in CreateInstance() from the
// enumerated instance extensions; when false, CreateSurface() falls back to a
// hidden Xlib window instead of vkCreateHeadlessSurfaceEXT.
Bool m_headlessSurfaceSupported = true;
// Set when CreateSurface() had to create its own Xlib window for the fallback
// above (rather than being handed one by the caller), so Shutdown() knows it
// owns that window and must destroy it.
Bool m_ownsFallbackXlibWindow = false;
VulkanRendererConfig m_config;
Bool m_swapchainResizeRequested = false;
// Presentation is suspended while the window is zero-area (minimized): the
@@ -929,6 +929,15 @@ namespace MobileGL::MG_Util::BackendLoader {
glesFuncs.glGetIntegerv(GL_MAX_INTEGER_SAMPLES, &maxIntegerSamples);
glesFuncs.glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
glesFuncs.glGetIntegerv(GL_MAX_SAMPLE_MASK_WORDS, &maxSampleMaskWords);
// MobileGL's sample-mask state only stores a single 32-bit word (see
// RenderState::SampleMaskValue) and SampleMaski_State() rejects any
// maskNumber other than 0. Advertising the real driver's value here (e.g.
// NVIDIA's GLES driver reports 2) makes glSampleMaski(1, ...) - which
// dEQP's per-case gluStateReset always issues up to GL_MAX_SAMPLE_MASK_WORDS -
// raise GL_INVALID_VALUE and aborts the whole glcts process after every
// single test case. 1 is a spec-legal value (the minimum required), so cap
// to what is actually implemented instead of forwarding the raw driver limit.
maxSampleMaskWords = std::min(maxSampleMaskWords, 1);
glesFuncs.glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTextureImageUnits);
glesFuncs.glGetIntegerv(GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, &maxVertexTextureImageUnits);
glesFuncs.glGetIntegerv(GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS, &maxComputeTextureImageUnits);
+1 -1
View File
@@ -55,7 +55,7 @@ val releaseSigningReady = signingStoreFile.exists()
val debuggableRelease = (findProperty("mobilegl.debuggableRelease") ?: "false").toString().toBoolean()
val mobileGlVersionMajor = 26
val mobileGlVersionMinor = 7
val mobileGlVersionMinor = 8
val mobileGlGitShortHash = runGit("rev-parse", "--short=7", "HEAD") ?: "nogit"
val mobileGlMonthlyRevision = runGit(
"rev-list",
+4
View File
@@ -65,6 +65,8 @@ def main():
help="seconds before killing one glcts invocation (a wedged case never returns)")
ap.add_argument("--skip-file", default=None,
help="file of case names to exclude, e.g. cases known to wedge the host")
ap.add_argument("--waiver-file", default=None,
help="--deqp-waiver-file value, e.g. tools/cts/waivers/mobilegl-fbo-harness.xml")
ap.add_argument("--env", action="append", default=[], metavar="K=V",
help="extra environment variable for glcts (repeatable)")
args = ap.parse_args()
@@ -135,6 +137,8 @@ def main():
"--deqp-log-shader-sources=disable",
f"--deqp-log-filename={qpa}",
]
if args.waiver_file:
cmd.append(f"--deqp-waiver-file={os.path.abspath(args.waiver_file)}")
timed_out = False
try:
# RLIMIT_CORE=0: MobileGL asserts abort with a core dump, and writing
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<waiver_list>
<!--
Waivers for known false failures caused by this local test harness's
fbo-surface-type mode (deqp-surface-type=fbo), not by MobileGL itself.
-->
<waiver vendor="MobileGL-Dev*" url="local-only: fbo-surface-type wrapper-FBO artifact, not a MobileGL conformance bug">
<description>
dEQP's fbo surface-type mode wraps rendering in its own
application level framebuffer object (a real, non-zero-named FBO,
not framebuffer 0). ApiCoverageTestCase's ReadBuffer coverage
sub-test captures GL_READ_BUFFER while that wrapper FBO is bound
(a valid GL_COLOR_ATTACHMENTn value there), then later deletes an
unrelated FBO of its own; per the GL spec, deleting a bound FBO
implicitly rebinds framebuffer target 0, the TRUE default
framebuffer this time, not the wrapper. Restoring the captured
GL_COLOR_ATTACHMENTn value against the true default framebuffer
correctly raises GL_INVALID_ENUM per spec (only FRONT/BACK style
tokens are valid there), so the test fails with a fully
spec conformant driver. This can only happen when the harness's
"default framebuffer" is a real dEQP created FBO instead of a
genuine window/pbuffer backed framebuffer 0, which is unique to
this local fbo surface-type setup. Confirmed by tracing
framebuffer bindings/external indices across the test's
execution (2026-08-01).
</description>
<renderer_list>
<r>Magma*</r>
<r>Espryt*</r>
</renderer_list>
<t>KHR-GL30.api.coverage</t>
<t>KHR-GL31.api.coverage</t>
<t>KHR-GL32.api.coverage</t>
<t>KHR-GL33.api.coverage</t>
</waiver>
</waiver_list>