mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d1b280375 | ||
|
|
8acd885594 | ||
|
|
10ff5e2b18 | ||
|
|
a6e52476f3 | ||
|
|
0deff52a1b | ||
|
|
50fefca959 |
@@ -182,6 +182,7 @@ set(ENABLE_SPVREMAPPER OFF CACHE BOOL "Enable SPVRemapper" FORCE)
|
||||
set(ENABLE_OPT ON CACHE BOOL "Enable SPIRV-Tools opt usage in glslang" FORCE)
|
||||
set(BUILD_EXTERNAL ON CACHE BOOL "Build external deps in External/" FORCE)
|
||||
set(ENABLE_GLSLANG_INSTALL OFF CACHE BOOL "Install glslang targets" FORCE)
|
||||
set(SPIRV_SKIP_EXECUTABLES ON CACHE BOOL "Skip building SPIRV-Tools executables" FORCE)
|
||||
|
||||
set(SPIRV_CROSS_C_API ON CACHE BOOL "Enable C API" FORCE)
|
||||
set(SPIRV_CROSS_ENABLE_GLSL ON CACHE BOOL "Enable GLSL backend" FORCE)
|
||||
|
||||
@@ -712,9 +712,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
{
|
||||
.TargetGLVersion = {4, 0, 0}, // GL target version
|
||||
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
|
||||
// Baseline advertisement (no timer queries / anisotropy yet); reconciled
|
||||
// once the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false),
|
||||
// Baseline advertisement (no runtime capabilities yet); reconciled once
|
||||
// the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||
.IsCompatibilityProfile = false // Is Compatibility Profile
|
||||
},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
|
||||
@@ -734,9 +734,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// thread can only observe the extension string after the
|
||||
// advertisement for its context has settled; rebuilding the whole
|
||||
// list keeps the re-run after a context recreation idempotent.
|
||||
void UpdateAdvertisedCapabilityExtensions(Bool anisotropicFilteringSupported) {
|
||||
MutableRendererInfo().RendererGLInfo.Extensions =
|
||||
BuildAdvertisedExtensions(AreTimerQueriesSupported(), anisotropicFilteringSupported);
|
||||
void UpdateAdvertisedCapabilityExtensions(const MG_External::GLESCapabilities& capabilities) {
|
||||
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
|
||||
capabilities.SupportsDrawIndirect,
|
||||
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -779,11 +781,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return false;
|
||||
}
|
||||
DirectGLES::SetGLESCapabilities(m_GLESCapabilities);
|
||||
// Now that g_GLESCapabilities knows about GL_EXT_disjoint_timer_query and
|
||||
// GL_EXT_texture_filter_anisotropic, reconcile the advertisement (see the comment on
|
||||
// UpdateAdvertisedCapabilityExtensions for why it cannot happen when the extension
|
||||
// list is first built).
|
||||
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities.SupportsTextureFilterAnisotropy);
|
||||
// Now that g_GLESCapabilities knows the host extensions, entry points, and ES version,
|
||||
// reconcile every runtime-gated advertisement (see the comment on
|
||||
// UpdateAdvertisedCapabilityExtensions for why this cannot happen when the list is first
|
||||
// built).
|
||||
UpdateAdvertisedCapabilityExtensions(m_GLESCapabilities);
|
||||
UpdateDynamicBackendParameters();
|
||||
PopulateFormatCapabilities(m_GLESFunctions, m_GLESCapabilities, MutableFormatCapabilities());
|
||||
PrintFormatCapabilities(GetFormatCapabilities());
|
||||
@@ -924,7 +926,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return MutableRendererInfo();
|
||||
}
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported) {
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||
Vector<GLExtension> extensions = {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
@@ -955,6 +959,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
E_GL_ARB_get_program_binary};
|
||||
// Minecraft 26.3 checks this prerequisite before it even considers
|
||||
// GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader
|
||||
// folds the version and pointer checks into SupportsDrawIndirect.
|
||||
if (drawIndirectSupported) {
|
||||
extensions.push_back(E_GL_ARB_draw_indirect);
|
||||
}
|
||||
// ARB_base_instance also defines the last word of an indirect command. Direct calls are
|
||||
// emulated on every Espryt device, but without host GL_EXT_base_instance a native indirect
|
||||
// draw cannot shift divisor attributes by a GPU-authored non-zero value, so do not promise
|
||||
// that incomplete case.
|
||||
if (drawIndirectSupported && nonZeroIndirectBaseInstanceSupported) {
|
||||
extensions.push_back(E_GL_ARB_base_instance);
|
||||
}
|
||||
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the host ES
|
||||
// driver's: the compiler threads are MobileGL's, and glCompileShader/glLinkProgram
|
||||
// are serviced entirely inside the frontend. Whether the device driver advertises
|
||||
|
||||
@@ -67,9 +67,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const RendererInfo& GetRendererIdentity();
|
||||
|
||||
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
|
||||
// for a device whose timer queries / anisotropic filtering are (or are not) usable.
|
||||
// for a device whose timer queries / anisotropic filtering / native indirect draws /
|
||||
// non-zero indirect baseInstance semantics are (or are not) usable.
|
||||
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported);
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported);
|
||||
|
||||
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
|
||||
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
||||
|
||||
@@ -3061,10 +3061,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
static Bool SupportsNativeIndirectDraws() {
|
||||
const auto& version = g_GLESCapabilities.GLESVersion;
|
||||
const Bool esVersionOk = version.Major > 3 || (version.Major == 3 && version.Minor >= 1);
|
||||
return esVersionOk && g_GLESFuncs.glDrawElementsIndirect != nullptr &&
|
||||
g_GLESFuncs.glDrawArraysIndirect != nullptr;
|
||||
return g_GLESCapabilities.SupportsDrawIndirect;
|
||||
}
|
||||
|
||||
// Runs an (indexed) indirect multi-draw. When a GL_DRAW_INDIRECT_BUFFER is bound the draws
|
||||
|
||||
@@ -497,20 +497,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.ExtraVendor = Nullopt,
|
||||
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
|
||||
.TargetGLSLVersion = {4, 6, 0},
|
||||
// Baseline advertisement (no shader subgroup, no timer queries); a
|
||||
// live backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false),
|
||||
// Baseline advertisement (no runtime-gated capabilities); a live
|
||||
// backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
|
||||
.IsCompatibilityProfile = false},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
||||
return rendererInfo;
|
||||
}
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported) {
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported) {
|
||||
Vector<GLExtension> extensions = {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_multi_draw_indirect,
|
||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_draw_indirect,
|
||||
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_texture_multisample,
|
||||
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_shader_draw_parameters,
|
||||
@@ -530,6 +532,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
E_GL_ARB_get_program_binary};
|
||||
// Vulkan's drawIndirectFirstInstance feature is optional. Direct base-instance calls work
|
||||
// without it, but ARB_base_instance also promises non-zero firstInstance in GPU indirect
|
||||
// commands; the renderer supplies true only when that word is legal and gl_InstanceID can
|
||||
// be rebased to OpenGL's zero-based semantics.
|
||||
if (nonZeroIndirectBaseInstanceSupported) {
|
||||
extensions.push_back(E_GL_ARB_base_instance);
|
||||
}
|
||||
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||
}
|
||||
@@ -690,7 +699,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// the whole list keeps re-runs idempotent.
|
||||
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported());
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
||||
|
||||
@@ -62,8 +62,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// POST screen shows.
|
||||
|
||||
// Static identity of the Magma renderer (renderer/backend names, target GL/GLSL
|
||||
// versions, ExtraVendor) with the baseline extension advertisement (no shader
|
||||
// subgroup, no timer queries). A live backend copies this in its constructor and
|
||||
// versions, ExtraVendor) with the baseline extension advertisement (no runtime-gated
|
||||
// capabilities). A live backend copies this in its constructor and
|
||||
// reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities
|
||||
// exist; callers that need the advertised list for a known capability set must
|
||||
// use BuildAdvertisedExtensions instead.
|
||||
@@ -74,7 +74,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
|
||||
// the detected device support (passing an already-gated value is harmless).
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported);
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported);
|
||||
|
||||
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
||||
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
||||
|
||||
@@ -224,6 +224,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
@@ -240,6 +241,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.readsBaseVertexBuiltin = false;
|
||||
other.writesViewportIndexBuiltin = false;
|
||||
other.needsPassthroughTessControl = false;
|
||||
other.passthroughTessControlEmulatable = false;
|
||||
other.lastUsedFrame = 0;
|
||||
@@ -282,6 +284,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
@@ -298,6 +301,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.readsBaseVertexBuiltin = false;
|
||||
other.writesViewportIndexBuiltin = false;
|
||||
other.needsPassthroughTessControl = false;
|
||||
other.passthroughTessControlEmulatable = false;
|
||||
other.lastUsedFrame = 0;
|
||||
|
||||
@@ -305,7 +305,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// texture/sampler resolution, completeness probe, sync, layout handling, sampler
|
||||
// and view lookups - would recompute the identical descriptor.
|
||||
if (trustUnchangedHint && descriptorMemoUsable && binding < m_samplerResolveMemo.size() &&
|
||||
m_samplerResolveMemo[binding].infoValid) {
|
||||
m_samplerResolveMemo[binding].infoValid &&
|
||||
m_samplerResolveMemo[binding].infoProgramLifetimeId == program.GetLifetimeId()) {
|
||||
outImageInfo = m_samplerResolveMemo[binding].info;
|
||||
return true;
|
||||
}
|
||||
@@ -504,6 +505,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (binding < m_samplerResolveMemo.size()) {
|
||||
if (descriptorMemoUsable) {
|
||||
m_samplerResolveMemo[binding].info = outImageInfo;
|
||||
m_samplerResolveMemo[binding].infoProgramLifetimeId = program.GetLifetimeId();
|
||||
m_samplerResolveMemo[binding].infoValid = true;
|
||||
} else {
|
||||
// An arrayed binding publishes nothing here, and clears what a previous program
|
||||
|
||||
@@ -341,8 +341,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// lifetime id, so a freed-and-reallocated sampler or texture at the same heap address
|
||||
// always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a
|
||||
// stale guess can only miss and fall through to the hash, never resolve wrong. Still
|
||||
// reset each frame alongside the descriptor-set cache. Indexed by binding.
|
||||
// reset each frame alongside the descriptor-set cache. Indexed by binding, but the
|
||||
// whole-descriptor entry is additionally keyed by program lifetime: Vulkan binding
|
||||
// numbers are layout-local and unrelated programs routinely reuse binding 0/1.
|
||||
struct SamplerResolveMemo {
|
||||
Uint64 infoProgramLifetimeId = 0;
|
||||
Uint64 samplerLifetimeId = 0;
|
||||
Uint64 textureLifetimeId = 0;
|
||||
VkSampler sampler = VK_NULL_HANDLE;
|
||||
|
||||
@@ -238,11 +238,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// nothing else across all of gl33.
|
||||
//
|
||||
// The mapping below is derived from - and at full extent exactly reproduces - the pixel
|
||||
// mapping RemapDefaultFboReadbackToGLOrientation has always used:
|
||||
// mapping VulkanRenderer::RemapDefaultFramebufferReadback uses:
|
||||
// identity : image(x, H-1-y) -> flip Y
|
||||
// 180 : image(W-1-x, y) -> mirror X (the rotation already flips the rows)
|
||||
// Quarter turns swap the axes; nothing in this renderer models that (the readback declines to
|
||||
// remap them and the viewport path only rescales), so they are left exactly as they were.
|
||||
// Quarter turns swap the axes and are handled by MapDefaultFramebufferReadbackRect rather than
|
||||
// this same-axis helper.
|
||||
struct DefaultFramebufferRectMapping {
|
||||
Bool flipY = false;
|
||||
Bool mirrorX = false;
|
||||
@@ -2151,47 +2151,6 @@ void main() {
|
||||
return static_cast<Uint8>(value * 255.0f + 0.5f);
|
||||
}
|
||||
|
||||
// Re-order the copied BLOCK - not the whole image - from the default framebuffer's stored
|
||||
// orientation into GL's. The caller has already aimed the copy at the right place with
|
||||
// MapDefaultFramebufferRectAxis, so what arrives here is exactly the requested
|
||||
// rectWidth x rectHeight rect, and all that is left is the order of rows (identity) or of
|
||||
// columns (180) WITHIN it.
|
||||
//
|
||||
// This used to iterate the full swapchain extent and index both sides with that stride,
|
||||
// which is why its caller could only use it on an exact full-extent read - and why every
|
||||
// partial glReadPixels of the default framebuffer came back in Vulkan row order. Only
|
||||
// identity/180 share the swapchain extent with the default framebuffer; 90/270 swap
|
||||
// extents and are still declined.
|
||||
static Bool RemapDefaultFboReadbackToGLOrientation(const Uint8* rawPixels,
|
||||
Uint32 rectWidth,
|
||||
Uint32 rectHeight,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||
SizeT texelSize,
|
||||
Uint8* outPixels) {
|
||||
if (IsQuarterTurnPreTransform(preTransform)) {
|
||||
return false;
|
||||
}
|
||||
if (rectWidth == 0 || rectHeight == 0 || texelSize == 0) {
|
||||
return false;
|
||||
}
|
||||
const DefaultFramebufferRectMapping mapping = GetDefaultFramebufferRectMapping(preTransform);
|
||||
const SizeT rowBytes = static_cast<SizeT>(rectWidth) * texelSize;
|
||||
for (Uint32 outY = 0; outY < rectHeight; ++outY) {
|
||||
const Uint32 srcY = mapping.flipY ? (rectHeight - 1 - outY) : outY;
|
||||
const Uint8* srcRow = rawPixels + static_cast<SizeT>(srcY) * rowBytes;
|
||||
Uint8* dstRow = outPixels + static_cast<SizeT>(outY) * rowBytes;
|
||||
if (!mapping.mirrorX) {
|
||||
Memcpy(dstRow, srcRow, rowBytes);
|
||||
continue;
|
||||
}
|
||||
for (Uint32 outX = 0; outX < rectWidth; ++outX) {
|
||||
Memcpy(dstRow + static_cast<SizeT>(outX) * texelSize,
|
||||
srcRow + static_cast<SizeT>(rectWidth - 1 - outX) * texelSize, texelSize);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static SizeT AlignPixelRow(SizeT rowBytes, Int alignment) {
|
||||
const SizeT resolvedAlignment = static_cast<SizeT>(std::max(alignment, 1));
|
||||
return (rowBytes + resolvedAlignment - 1) & ~(resolvedAlignment - 1);
|
||||
@@ -2738,6 +2697,95 @@ void main() {
|
||||
return formatInfo.texel_block_size;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::MapDefaultFramebufferReadbackRect(
|
||||
GLint x, GLint y, GLsizei width, GLsizei height, VkExtent2D imageExtent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform, VkOffset2D* imageOffset,
|
||||
VkExtent2D* imageCopyExtent) {
|
||||
if (width <= 0 || height <= 0 || imageOffset == nullptr || imageCopyExtent == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Int imageWidth = static_cast<Int>(imageExtent.width);
|
||||
const Int imageHeight = static_cast<Int>(imageExtent.height);
|
||||
Int mappedX = x;
|
||||
Int mappedY = y;
|
||||
Uint32 mappedWidth = static_cast<Uint32>(width);
|
||||
Uint32 mappedHeight = static_cast<Uint32>(height);
|
||||
|
||||
// InsertPositionFixup first flips GL Y and then applies the surface transform. In pixel
|
||||
// coordinates that gives these half-open rectangle mappings into the stored image:
|
||||
// identity: (x, H-y-h), 90: (y, x), 180: (W-x-w, y), 270: (H-y-h, W-x-w).
|
||||
// Quarter turns also transpose the copied block's extent.
|
||||
switch (preTransform) {
|
||||
case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR:
|
||||
mappedX = y;
|
||||
mappedY = x;
|
||||
mappedWidth = static_cast<Uint32>(height);
|
||||
mappedHeight = static_cast<Uint32>(width);
|
||||
break;
|
||||
case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR:
|
||||
mappedX = imageWidth - x - width;
|
||||
mappedY = y;
|
||||
break;
|
||||
case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR:
|
||||
mappedX = imageWidth - y - height;
|
||||
mappedY = imageHeight - x - width;
|
||||
mappedWidth = static_cast<Uint32>(height);
|
||||
mappedHeight = static_cast<Uint32>(width);
|
||||
break;
|
||||
default:
|
||||
mappedY = imageHeight - y - height;
|
||||
break;
|
||||
}
|
||||
|
||||
if (mappedX < 0 || mappedY < 0 || mappedWidth > imageExtent.width ||
|
||||
mappedHeight > imageExtent.height ||
|
||||
static_cast<Uint64>(mappedX) + mappedWidth > imageExtent.width ||
|
||||
static_cast<Uint64>(mappedY) + mappedHeight > imageExtent.height) {
|
||||
return false;
|
||||
}
|
||||
*imageOffset = {mappedX, mappedY};
|
||||
*imageCopyExtent = {mappedWidth, mappedHeight};
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::RemapDefaultFramebufferReadback(
|
||||
const Uint8* rawPixels, Uint32 logicalWidth, Uint32 logicalHeight,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform, SizeT texelSize, Uint8* outPixels) {
|
||||
if (rawPixels == nullptr || outPixels == nullptr || logicalWidth == 0 || logicalHeight == 0 ||
|
||||
texelSize == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Uint32 rawWidth = IsQuarterTurnPreTransform(preTransform) ? logicalHeight : logicalWidth;
|
||||
for (Uint32 outY = 0; outY < logicalHeight; ++outY) {
|
||||
for (Uint32 outX = 0; outX < logicalWidth; ++outX) {
|
||||
Uint32 srcX = outX;
|
||||
Uint32 srcY = outY;
|
||||
switch (preTransform) {
|
||||
case VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR:
|
||||
srcX = outY;
|
||||
srcY = outX;
|
||||
break;
|
||||
case VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR:
|
||||
srcX = logicalWidth - 1 - outX;
|
||||
break;
|
||||
case VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR:
|
||||
srcX = logicalHeight - 1 - outY;
|
||||
srcY = logicalWidth - 1 - outX;
|
||||
break;
|
||||
default:
|
||||
srcY = logicalHeight - 1 - outY;
|
||||
break;
|
||||
}
|
||||
Memcpy(outPixels + (static_cast<SizeT>(outY) * logicalWidth + outX) * texelSize,
|
||||
rawPixels + (static_cast<SizeT>(srcY) * rawWidth + srcX) * texelSize,
|
||||
texelSize);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
||||
GLsizei width, GLsizei height, GLenum destinationFormat,
|
||||
GLenum destinationType, SizeT destinationRowStride,
|
||||
@@ -9138,19 +9186,18 @@ void main() {
|
||||
// The GL rect, aimed at the default framebuffer's stored orientation. Using the GL y
|
||||
// verbatim copied rows [y, y+h) counted from the TOP of the image, i.e. the wrong band for
|
||||
// every read that was not full-height.
|
||||
Int32 copyOffsetX = x;
|
||||
Int32 copyOffsetY = y;
|
||||
VkOffset2D copyOffset{x, y};
|
||||
VkExtent2D copyExtent{static_cast<Uint32>(width), static_cast<Uint32>(height)};
|
||||
if (readIsDefaultFbo) {
|
||||
const VkExtent2D defaultFboExtent = m_swapchainObject.GetExtent();
|
||||
const DefaultFramebufferRectMapping mapping =
|
||||
GetDefaultFramebufferRectMapping(m_swapchainObject.GetPreTransform());
|
||||
copyOffsetX = MapDefaultFramebufferRectAxis(x, width, static_cast<Int>(defaultFboExtent.width),
|
||||
mapping.mirrorX);
|
||||
copyOffsetY = MapDefaultFramebufferRectAxis(y, height, static_cast<Int>(defaultFboExtent.height),
|
||||
mapping.flipY);
|
||||
const Bool mapped = MapDefaultFramebufferReadbackRect(
|
||||
x, y, width, height, defaultFboExtent, m_swapchainObject.GetPreTransform(), ©Offset,
|
||||
©Extent);
|
||||
MOBILEGL_ASSERT(mapped, "ReadPixels: default framebuffer read rectangle is out of bounds");
|
||||
if (!mapped) return;
|
||||
}
|
||||
copyRegion.imageOffset = {copyOffsetX, copyOffsetY, static_cast<Int32>(srcBinding.depthOffset)};
|
||||
copyRegion.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
||||
copyRegion.imageOffset = {copyOffset.x, copyOffset.y, static_cast<Int32>(srcBinding.depthOffset)};
|
||||
copyRegion.imageExtent = {copyExtent.width, copyExtent.height, 1};
|
||||
vkCmdCopyImageToBuffer(frame.commandBuffer, srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
readback.GetHandle(), 1, ©Region);
|
||||
|
||||
@@ -9192,16 +9239,14 @@ void main() {
|
||||
// already aimed with the same mapping. The gate is exactly what made every partial
|
||||
// read of the default framebuffer come back in Vulkan row order.
|
||||
Vector<Uint8> remapped(static_cast<SizeT>(width) * static_cast<SizeT>(height) * sourceTexelSize);
|
||||
if (RemapDefaultFboReadbackToGLOrientation(mapped, static_cast<Uint32>(width),
|
||||
static_cast<Uint32>(height), preTransform, sourceTexelSize,
|
||||
remapped.data())) {
|
||||
if (RemapDefaultFramebufferReadback(mapped, static_cast<Uint32>(width),
|
||||
static_cast<Uint32>(height), preTransform, sourceTexelSize,
|
||||
remapped.data())) {
|
||||
PackReadbackToClientOrPbo(remapped.data(), srcFormat, width, height, 1, format, type, pixels,
|
||||
/*applyPackImageParams=*/false, /*applyReadColorClamp=*/true);
|
||||
return;
|
||||
}
|
||||
// Only a quarter-turn pre-transform reaches this, and nothing in this renderer models
|
||||
// one. MGLOG_I because the INFO builds are the ones that run conformance.
|
||||
MGLOG_D("DirectVulkan::ReadPixels: default-FBO remap declined (w=%d h=%d preTransform=%d); falling back "
|
||||
MGLOG_D("DirectVulkan::ReadPixels: default-FBO remap failed (w=%d h=%d preTransform=%d); falling back "
|
||||
"to raw readback",
|
||||
width, height, static_cast<Int>(preTransform));
|
||||
}
|
||||
@@ -9582,16 +9627,15 @@ void main() {
|
||||
// The swapchain's depth/stencil image is stored display-side-up like its colour twin, so
|
||||
// the GL rect has to be mapped into that space before the copy and the copied rows
|
||||
// re-oriented afterwards - the same two halves the colour ReadPixels path applies.
|
||||
Int32 copyOffsetX = x;
|
||||
Int32 copyOffsetY = y;
|
||||
VkOffset2D copyOffset{x, y};
|
||||
VkExtent2D copyExtent{static_cast<Uint32>(width), static_cast<Uint32>(height)};
|
||||
if (defaultFramebufferOrientation) {
|
||||
const VkExtent2D defaultFboExtent = m_swapchainObject.GetExtent();
|
||||
const DefaultFramebufferRectMapping mapping =
|
||||
GetDefaultFramebufferRectMapping(m_swapchainObject.GetPreTransform());
|
||||
copyOffsetX = MapDefaultFramebufferRectAxis(x, width, static_cast<Int>(defaultFboExtent.width),
|
||||
mapping.mirrorX);
|
||||
copyOffsetY = MapDefaultFramebufferRectAxis(y, height, static_cast<Int>(defaultFboExtent.height),
|
||||
mapping.flipY);
|
||||
const Bool mapped = MapDefaultFramebufferReadbackRect(
|
||||
x, y, width, height, defaultFboExtent, m_swapchainObject.GetPreTransform(), ©Offset,
|
||||
©Extent);
|
||||
MOBILEGL_ASSERT(mapped, "ReadDepthStencilPixels: default framebuffer read rectangle is out of bounds");
|
||||
if (!mapped) return;
|
||||
}
|
||||
|
||||
VkBufferImageCopy regions[2]{};
|
||||
@@ -9603,8 +9647,8 @@ void main() {
|
||||
region.imageSubresource.mipLevel = mipLevel;
|
||||
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
||||
region.imageSubresource.layerCount = 1;
|
||||
region.imageOffset = {copyOffsetX, copyOffsetY, 0};
|
||||
region.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
||||
region.imageOffset = {copyOffset.x, copyOffset.y, 0};
|
||||
region.imageExtent = {copyExtent.width, copyExtent.height, 1};
|
||||
}
|
||||
if (wantStencil) {
|
||||
auto& region = regions[regionCount++];
|
||||
@@ -9613,8 +9657,8 @@ void main() {
|
||||
region.imageSubresource.mipLevel = mipLevel;
|
||||
region.imageSubresource.baseArrayLayer = baseArrayLayer;
|
||||
region.imageSubresource.layerCount = 1;
|
||||
region.imageOffset = {copyOffsetX, copyOffsetY, 0};
|
||||
region.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
|
||||
region.imageOffset = {copyOffset.x, copyOffset.y, 0};
|
||||
region.imageExtent = {copyExtent.width, copyExtent.height, 1};
|
||||
}
|
||||
vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(),
|
||||
regionCount, regions);
|
||||
@@ -9648,23 +9692,21 @@ void main() {
|
||||
Bool remapped = true;
|
||||
if (wantDepth && depthCopyBytes > 0) {
|
||||
remappedDepth.resize(pixelCount * depthCopyBytes);
|
||||
remapped = RemapDefaultFboReadbackToGLOrientation(depthSrc, static_cast<Uint32>(width),
|
||||
static_cast<Uint32>(height), preTransform,
|
||||
depthCopyBytes, remappedDepth.data());
|
||||
remapped = RemapDefaultFramebufferReadback(depthSrc, static_cast<Uint32>(width),
|
||||
static_cast<Uint32>(height), preTransform,
|
||||
depthCopyBytes, remappedDepth.data());
|
||||
}
|
||||
if (remapped && wantStencil) {
|
||||
remappedStencil.resize(pixelCount);
|
||||
remapped = RemapDefaultFboReadbackToGLOrientation(stencilSrc, static_cast<Uint32>(width),
|
||||
static_cast<Uint32>(height), preTransform, 1,
|
||||
remappedStencil.data());
|
||||
remapped = RemapDefaultFramebufferReadback(stencilSrc, static_cast<Uint32>(width),
|
||||
static_cast<Uint32>(height), preTransform, 1,
|
||||
remappedStencil.data());
|
||||
}
|
||||
if (remapped) {
|
||||
if (!remappedDepth.empty()) depthSrc = remappedDepth.data();
|
||||
if (!remappedStencil.empty()) stencilSrc = remappedStencil.data();
|
||||
} else {
|
||||
// Only a quarter-turn pre-transform reaches this, and nothing in this renderer
|
||||
// models one. MGLOG_I because the INFO builds are the ones that run conformance.
|
||||
MGLOG_D("DirectVulkan::ReadDepthStencilPixels: default-FBO remap declined (w=%d h=%d "
|
||||
MGLOG_D("DirectVulkan::ReadDepthStencilPixels: default-FBO remap failed (w=%d h=%d "
|
||||
"preTransform=%d); falling back to raw readback",
|
||||
width, height, static_cast<Int>(preTransform));
|
||||
}
|
||||
|
||||
@@ -229,6 +229,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
|
||||
VkImageLayout dstRestoreLayout, Bool stencilAspect);
|
||||
static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
|
||||
// Map a GL bottom-left-origin rectangle into the display-oriented swapchain image.
|
||||
// Quarter-turn surface transforms swap the copy extent's axes.
|
||||
static Bool MapDefaultFramebufferReadbackRect(GLint x, GLint y, GLsizei width, GLsizei height,
|
||||
VkExtent2D imageExtent,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||
VkOffset2D* imageOffset, VkExtent2D* imageCopyExtent);
|
||||
// Reorder a tightly packed block copied with MapDefaultFramebufferReadbackRect back into
|
||||
// GL row order. The input block has swapped dimensions for 90/270 degree transforms.
|
||||
static Bool RemapDefaultFramebufferReadback(const Uint8* rawPixels, Uint32 logicalWidth,
|
||||
Uint32 logicalHeight,
|
||||
VkSurfaceTransformFlagBitsKHR preTransform,
|
||||
SizeT texelSize, Uint8* outPixels);
|
||||
static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
|
||||
GLsizei width, GLsizei height, GLenum destinationFormat,
|
||||
GLenum destinationType, SizeT destinationRowStride,
|
||||
@@ -298,6 +310,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
|
||||
// honored rather than accepted-and-ignored.
|
||||
Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
|
||||
// ARB_base_instance extends indirect command records with a non-zero firstInstance and
|
||||
// requires gl_InstanceID to remain zero-based. Vulkan needs both features to honor that
|
||||
// complete contract: one legalizes the command word, the other enables the shader rebase.
|
||||
Bool IsNonZeroIndirectBaseInstanceSupported() const {
|
||||
return m_drawIndirectFirstInstanceFeatureEnabled && m_shaderDrawParametersFeatureEnabled;
|
||||
}
|
||||
// Ensures the frame command buffer is recording (same lazy pattern as
|
||||
// SetupDraw) and writes a bottom-of-pipe timestamp into the current
|
||||
// frame's pool. Null when unsupported or the pool is exhausted.
|
||||
|
||||
@@ -8,9 +8,28 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <iostream>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
|
||||
|
||||
TEST(DirectVulkanSanity, ProgramMovePreservesViewportIndexUsage) {
|
||||
using VkProgramObject = MobileGL::MG_Backend::DirectVulkan::ProgramFactory::VkProgramObject;
|
||||
|
||||
VkProgramObject moveConstructedSource;
|
||||
moveConstructedSource.writesViewportIndexBuiltin = true;
|
||||
VkProgramObject moveConstructed(std::move(moveConstructedSource));
|
||||
EXPECT_TRUE(moveConstructed.writesViewportIndexBuiltin);
|
||||
EXPECT_FALSE(moveConstructedSource.writesViewportIndexBuiltin);
|
||||
|
||||
VkProgramObject moveAssignedSource;
|
||||
moveAssignedSource.writesViewportIndexBuiltin = true;
|
||||
VkProgramObject moveAssigned;
|
||||
moveAssigned = std::move(moveAssignedSource);
|
||||
EXPECT_TRUE(moveAssigned.writesViewportIndexBuiltin);
|
||||
EXPECT_FALSE(moveAssignedSource.writesViewportIndexBuiltin);
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, ExtensionEnumeration) {
|
||||
uint32_t extensionCount = 0;
|
||||
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
|
||||
|
||||
@@ -725,22 +725,57 @@ TEST(TextureAnisotropyCapabilities, ExtensionIsAdvertisedOnlyWhenTheHostDriverSu
|
||||
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
|
||||
};
|
||||
|
||||
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false);
|
||||
const auto without = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false);
|
||||
EXPECT_FALSE(contains(without, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_FALSE(contains(without, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
|
||||
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true);
|
||||
const auto with = MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, true, false, false);
|
||||
EXPECT_TRUE(contains(with, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_TRUE(contains(with, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
|
||||
// Same rule on the Vulkan backend, where the gate is the samplerAnisotropy device feature.
|
||||
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false);
|
||||
const auto vkWithout = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false);
|
||||
EXPECT_FALSE(contains(vkWithout, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true);
|
||||
const auto vkWith = MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, true, false);
|
||||
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_EXT_texture_filter_anisotropic));
|
||||
EXPECT_TRUE(contains(vkWith, MobileGL::E_GL_ARB_texture_filter_anisotropic));
|
||||
}
|
||||
|
||||
// Minecraft 26.3 checks ARB_draw_indirect before it considers the already-advertised
|
||||
// ARB_multi_draw_indirect, then separately requires ARB_base_instance before enabling its terrain
|
||||
// indirect path. Pin both strings and, just as importantly, the non-zero firstInstance gate.
|
||||
TEST(IndirectDrawAdvertisement, MatchesEachBackendsUsableCommandSemantics) {
|
||||
const auto contains = [](const MobileGL::Vector<MobileGL::GLExtension>& extensions,
|
||||
MobileGL::GLExtension wanted) {
|
||||
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
|
||||
};
|
||||
|
||||
const auto esWithoutIndirect =
|
||||
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false);
|
||||
EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_FALSE(contains(esWithoutIndirect, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
const auto esWithoutBaseInstance =
|
||||
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, false);
|
||||
EXPECT_TRUE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_FALSE(contains(esWithoutBaseInstance, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
const auto esWithBoth =
|
||||
MobileGL::MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, true, true);
|
||||
EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_TRUE(contains(esWithBoth, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
const auto vkWithoutBaseInstance =
|
||||
MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false);
|
||||
EXPECT_TRUE(contains(vkWithoutBaseInstance, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_FALSE(contains(vkWithoutBaseInstance, MobileGL::E_GL_ARB_base_instance));
|
||||
|
||||
const auto vkWithBoth =
|
||||
MobileGL::MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, true);
|
||||
EXPECT_TRUE(contains(vkWithBoth, MobileGL::E_GL_ARB_draw_indirect));
|
||||
EXPECT_TRUE(contains(vkWithBoth, MobileGL::E_GL_ARB_base_instance));
|
||||
}
|
||||
|
||||
TEST(TextureAnisotropyCapabilities, MaxAnisotropyIsQueriedOnlyWhenTheExtensionIsPresent) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
@@ -836,3 +871,63 @@ TEST(MultiDrawCapabilities, ExtensionWithoutResolvedPointerIsNotSupport) {
|
||||
EXPECT_FALSE(caps.SupportsMultiDrawIndirect);
|
||||
EXPECT_FALSE(caps.SupportsMultiDrawElementsBaseVertex);
|
||||
}
|
||||
|
||||
TEST(DrawIndirectCapabilities, RequiresEs31AndBothCoreEntryPoints) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
auto funcs = MakeFakeGLESFunctions();
|
||||
funcs.glDrawElementsIndirect = [](GLenum, GLenum, const void*) {};
|
||||
|
||||
MobileGL::MG_External::GLESCapabilities supportedCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(supportedCaps, funcs));
|
||||
EXPECT_TRUE(supportedCaps.SupportsDrawIndirect);
|
||||
|
||||
// The same pointers on an ES 3.0 context are not core entry points and cannot back the
|
||||
// desktop extension contract.
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.glesMinorVersion = 0;
|
||||
MobileGL::MG_External::GLESCapabilities es30Caps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(es30Caps, funcs));
|
||||
EXPECT_FALSE(es30Caps.SupportsDrawIndirect);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
const auto missingElements = MakeFakeGLESFunctions();
|
||||
MobileGL::MG_External::GLESCapabilities missingEntryPointCaps;
|
||||
ASSERT_TRUE(
|
||||
MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(missingEntryPointCaps, missingElements));
|
||||
EXPECT_FALSE(missingEntryPointCaps.SupportsDrawIndirect);
|
||||
}
|
||||
|
||||
TEST(BaseInstanceCapabilities, RequiresTheExtensionAndAllThreeEntryPoints) {
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
auto funcs = MakeFakeGLESFunctions();
|
||||
funcs.glDrawArraysInstancedBaseInstanceEXT = [](GLenum, GLint, GLsizei, GLsizei, GLuint) {};
|
||||
funcs.glDrawElementsInstancedBaseInstanceEXT =
|
||||
[](GLenum, GLsizei, GLenum, const void*, GLsizei, GLuint) {};
|
||||
funcs.glDrawElementsInstancedBaseVertexBaseInstanceEXT =
|
||||
[](GLenum, GLsizei, GLenum, const void*, GLsizei, GLint, GLuint) {};
|
||||
|
||||
// Resolved stubs alone must never make the capability true.
|
||||
MobileGL::MG_External::GLESCapabilities pointersOnlyCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(pointersOnlyCaps, funcs));
|
||||
EXPECT_FALSE(pointersOnlyCaps.SupportsBaseInstance);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.extensions.emplace_back("GL_EXT_base_instance");
|
||||
MobileGL::MG_External::GLESCapabilities supportedCaps;
|
||||
ASSERT_TRUE(MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(supportedCaps, funcs));
|
||||
EXPECT_TRUE(supportedCaps.SupportsBaseInstance);
|
||||
|
||||
ResetFakeDriver();
|
||||
g_fake.maxVertexSsboBlocks = 0;
|
||||
g_fake.extensions.emplace_back("GL_EXT_base_instance");
|
||||
funcs.glDrawElementsInstancedBaseInstanceEXT = nullptr;
|
||||
MobileGL::MG_External::GLESCapabilities missingEntryPointCaps;
|
||||
ASSERT_TRUE(
|
||||
MobileGL::MG_Util::BackendLoader::FillInGLESCapabilities(missingEntryPointCaps, funcs));
|
||||
EXPECT_FALSE(missingEntryPointCaps.SupportsBaseInstance);
|
||||
}
|
||||
|
||||
@@ -516,17 +516,17 @@ TEST_F(ParallelShaderCompileTest, MaxShaderCompilerThreadsIgnoresTheCurrentBudge
|
||||
TEST_F(ParallelShaderCompileTest, BothBackendsAdvertiseTheExtensionIffAsyncIsEnabled) {
|
||||
{
|
||||
const AsyncModeScope async(true);
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false),
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile));
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false),
|
||||
EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile));
|
||||
}
|
||||
{
|
||||
const AsyncModeScope async(false);
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false),
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile))
|
||||
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false),
|
||||
EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
|
||||
E_GL_KHR_parallel_shader_compile))
|
||||
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
|
||||
}
|
||||
|
||||
@@ -2692,11 +2692,13 @@ out vec4 fragColor;
|
||||
float fma
|
||||
(float a, float b, float c) { return a * b + c; }
|
||||
float sinh(float x, float y) { return x * y; }
|
||||
float length_squared(vec3 value) { return dot(value, value); }
|
||||
float round(float x) { return floor(x + 0.5); }
|
||||
float min3(float a, float b, float c) { return min(min(a, b), c); }
|
||||
|
||||
void main() {
|
||||
fragColor = vec4(fma(0.1, 0.2, 0.3), sinh(0.4, 2.0), round(1.25), min3(0.1, 0.2, 0.3));
|
||||
fragColor = vec4(fma(0.1, 0.2, 0.3), sinh(0.4, 2.0), round(1.25),
|
||||
min3(0.1, 0.2, 0.3) + length_squared(vec3(0.1, 0.2, 0.3)));
|
||||
}
|
||||
)";
|
||||
GLuint vs = CompileShaderChecked(GL_VERTEX_SHADER, vsSource);
|
||||
@@ -2707,6 +2709,7 @@ void main() {
|
||||
if (essl.find("fragColor") == String::npos) continue; // fragment module only
|
||||
EXPECT_NE(essl.find("mg_fma("), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("mg_sinh("), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("mg_length_squared("), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("mg_round("), String::npos) << essl;
|
||||
EXPECT_NE(essl.find("mg_min3("), String::npos) << essl;
|
||||
EXPECT_EQ(essl.find("float fma("), String::npos) << essl;
|
||||
|
||||
@@ -52,20 +52,24 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
|
||||
OpEntryPoint Fragment %main "main" %outColor
|
||||
OpExecutionMode %main OriginUpperLeft
|
||||
OpName %globalSampler "sampler"
|
||||
OpName %globalNew "new"
|
||||
OpName %paramSampler "sampler"
|
||||
OpName %paramNew "new"
|
||||
OpName %main "main"
|
||||
OpDecorate %outColor Location 0
|
||||
%void = OpTypeVoid
|
||||
%float = OpTypeFloat 32
|
||||
%v4float = OpTypeVector %float 4
|
||||
%mainFn = OpTypeFunction %void
|
||||
%paramFn = OpTypeFunction %void %float
|
||||
%paramFn = OpTypeFunction %void %float %float
|
||||
%outV4Ptr = OpTypePointer Output %v4float
|
||||
%privatePtr = OpTypePointer Private %float
|
||||
%outColor = OpVariable %outV4Ptr Output
|
||||
%globalSampler = OpVariable %privatePtr Private
|
||||
%globalNew = OpVariable %privatePtr Private
|
||||
%helper = OpFunction %void None %paramFn
|
||||
%paramSampler = OpFunctionParameter %float
|
||||
%paramNew = OpFunctionParameter %float
|
||||
%helperBody = OpLabel
|
||||
OpReturn
|
||||
OpFunctionEnd
|
||||
@@ -91,6 +95,7 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
|
||||
ASSERT_TRUE(tools.Disassemble(outputBinary, &outputText));
|
||||
|
||||
EXPECT_NE(outputText.find("\"MGL_COMPAT_sampler\""), String::npos);
|
||||
EXPECT_NE(outputText.find("\"MGL_COMPAT_new\""), String::npos);
|
||||
|
||||
SizeT exactSamplerNameCount = 0;
|
||||
SizeT searchOffset = 0;
|
||||
@@ -99,6 +104,14 @@ TEST_F(ProgramUtilTest, RenameSamplerFunctionParameterInSpirvPass) {
|
||||
searchOffset += std::strlen("\"sampler\"");
|
||||
}
|
||||
EXPECT_EQ(exactSamplerNameCount, 1u);
|
||||
|
||||
SizeT exactNewNameCount = 0;
|
||||
searchOffset = 0;
|
||||
while ((searchOffset = outputText.find("\"new\"", searchOffset)) != String::npos) {
|
||||
++exactNewNameCount;
|
||||
searchOffset += std::strlen("\"new\"");
|
||||
}
|
||||
EXPECT_EQ(exactNewNameCount, 1u);
|
||||
}
|
||||
|
||||
TEST_F(ProgramUtilTest, UnformattedFloatStorageImagesKeepIntegerAtomicImagesTyped) {
|
||||
|
||||
@@ -936,6 +936,45 @@ TEST(DirectVulkanSanity, ReadbackUsesTheSourceFormatTexelSize) {
|
||||
EXPECT_EQ(VulkanRenderer::GetReadbackTexelSize(VK_FORMAT_R32G32B32A32_SFLOAT), 16u);
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, DefaultFramebufferQuarterTurnReadbackMapsRectAndPixels) {
|
||||
using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer;
|
||||
using MobileGL::Uint8;
|
||||
|
||||
VkOffset2D offset{};
|
||||
VkExtent2D copyExtent{};
|
||||
ASSERT_TRUE(VulkanRenderer::MapDefaultFramebufferReadbackRect(
|
||||
1, 0, 2, 1, VkExtent2D{2, 3}, VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR,
|
||||
&offset, ©Extent));
|
||||
EXPECT_EQ(offset.x, 0);
|
||||
EXPECT_EQ(offset.y, 1);
|
||||
EXPECT_EQ(copyExtent.width, 1u);
|
||||
EXPECT_EQ(copyExtent.height, 2u);
|
||||
|
||||
ASSERT_TRUE(VulkanRenderer::MapDefaultFramebufferReadbackRect(
|
||||
1, 0, 2, 1, VkExtent2D{2, 3}, VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR,
|
||||
&offset, ©Extent));
|
||||
EXPECT_EQ(offset.x, 1);
|
||||
EXPECT_EQ(offset.y, 0);
|
||||
EXPECT_EQ(copyExtent.width, 1u);
|
||||
EXPECT_EQ(copyExtent.height, 2u);
|
||||
|
||||
// Logical GL rows, bottom to top, are abc / def. The display-oriented swapchain blocks are
|
||||
// transposed in opposite directions for 90 and 270 degrees.
|
||||
const Uint8 raw90[] = {'a', 'd', 'b', 'e', 'c', 'f'};
|
||||
const Uint8 raw270[] = {'f', 'c', 'e', 'b', 'd', 'a'};
|
||||
const Uint8 expected[] = {'a', 'b', 'c', 'd', 'e', 'f'};
|
||||
Uint8 result[sizeof(expected)]{};
|
||||
|
||||
ASSERT_TRUE(VulkanRenderer::RemapDefaultFramebufferReadback(
|
||||
raw90, 3, 2, VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR, 1, result));
|
||||
EXPECT_TRUE(std::equal(std::begin(expected), std::end(expected), std::begin(result)));
|
||||
|
||||
std::fill(std::begin(result), std::end(result), 0);
|
||||
ASSERT_TRUE(VulkanRenderer::RemapDefaultFramebufferReadback(
|
||||
raw270, 3, 2, VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR, 1, result));
|
||||
EXPECT_TRUE(std::equal(std::begin(expected), std::end(expected), std::begin(result)));
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, ReadbackConvertsRgba8AndRgba16fPixels) {
|
||||
using MobileGL::MG_Backend::DirectVulkan::VulkanRenderer;
|
||||
using MobileGL::MG_Util::EncodeFloatToHalfBits;
|
||||
|
||||
@@ -955,6 +955,8 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2);
|
||||
const Bool esAtLeast31 = caps.GLESVersion.Major > 3 ||
|
||||
(caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 1);
|
||||
caps.SupportsDrawIndirect = esAtLeast31 && glesFuncs.glDrawArraysIndirect != nullptr &&
|
||||
glesFuncs.glDrawElementsIndirect != nullptr;
|
||||
caps.SupportsDrawElementsBaseVertex = (esAtLeast32 || hasDrawElementsBaseVertexExtension) &&
|
||||
glesFuncs.glDrawElementsBaseVertex != nullptr;
|
||||
caps.SupportsComputeShader = esAtLeast31 && glesFuncs.glDispatchCompute != nullptr &&
|
||||
@@ -976,6 +978,7 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
MGLOG_I(" indexed glColorMaski: %s", caps.SupportsIndexedColorMask ? "yes" : "no");
|
||||
MGLOG_I(" dual-source blend (EXT_blend_func_extended): %s",
|
||||
caps.SupportsDualSourceBlend ? "yes" : "no");
|
||||
MGLOG_I(" draw indirect (ES 3.1 core): %s", caps.SupportsDrawIndirect ? "yes" : "no");
|
||||
MGLOG_I(" multi-draw indirect (EXT_multi_draw_indirect): %s",
|
||||
caps.SupportsMultiDrawIndirect ? "yes" : "no");
|
||||
MGLOG_I(" multi-draw base vertex (EXT/OES_draw_elements_base_vertex + EXT_multi_draw_arrays): %s",
|
||||
|
||||
@@ -1149,6 +1149,10 @@ namespace MobileGL {
|
||||
// GLES 3.2 core or GL_OES_shader_multisample_interpolation exposes
|
||||
// interpolateAtOffset and the three fragment-offset limit queries.
|
||||
Bool SupportsShaderMultisampleInterpolation = false;
|
||||
// ES 3.1+ exposes glDrawArraysIndirect / glDrawElementsIndirect in core. Keep the
|
||||
// version and both entry-point checks together so extension advertisement and the
|
||||
// DirectGLES dispatch path cannot disagree on whether native indirect draws exist.
|
||||
Bool SupportsDrawIndirect = false;
|
||||
// GL_EXT_multi_draw_indirect is present AND glMultiDrawArraysIndirectEXT /
|
||||
// glMultiDrawElementsIndirectEXT both resolved. Multi-draw is not core in any ES
|
||||
// version, and eglGetProcAddress may return a live-looking stub on drivers without
|
||||
|
||||
@@ -1168,7 +1168,9 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
backendApiVersionString = MG_Backend::DirectGLES::FormatBackendAPIVersionString(
|
||||
summary.caps.GLESRendererString, summary.caps.GLESVersion.Major, summary.caps.GLESVersion.Minor);
|
||||
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectGLES::BuildAdvertisedExtensions(
|
||||
summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy));
|
||||
summary.caps.SupportsDisjointTimerQuery, summary.caps.SupportsTextureFilterAnisotropy,
|
||||
summary.caps.SupportsDrawIndirect,
|
||||
summary.caps.SupportsDrawIndirect && summary.caps.SupportsBaseInstance));
|
||||
}
|
||||
AppendMobileGLReportedRows(builder, MG_Backend::DirectGLES::GetRendererIdentity(), backendApiVersionString,
|
||||
advertisedExtensions);
|
||||
@@ -1461,6 +1463,8 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
Bool shaderSubgroupUsable = false;
|
||||
Bool timerQueriesSupported = false;
|
||||
Bool samplerAnisotropySupported = false;
|
||||
Bool drawIndirectFirstInstanceSupported = false;
|
||||
Bool shaderDrawParametersSupported = false;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
@@ -1744,6 +1748,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
VkPhysicalDeviceFeatures features{};
|
||||
vkGetPhysicalDeviceFeaturesFn(physicalDevice, &features);
|
||||
summary.samplerAnisotropySupported = features.samplerAnisotropy == VK_TRUE;
|
||||
summary.drawIndirectFirstInstanceSupported = features.drawIndirectFirstInstance == VK_TRUE;
|
||||
if (features.multiDrawIndirect == VK_TRUE) {
|
||||
builder.Pass("multiDrawIndirect", "indirect multi-draw batches run as single native commands");
|
||||
} else {
|
||||
@@ -1910,6 +1915,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
builder.Warn("shaderDrawParameters",
|
||||
"unavailable; shaders using gl_DrawID/gl_BaseInstance will not work");
|
||||
}
|
||||
summary.shaderDrawParametersSupported = shaderDrawParameters;
|
||||
|
||||
Bool provokingVertexLast = false;
|
||||
Bool transformFeedbackPreservesProvokingVertex = false;
|
||||
@@ -2108,7 +2114,8 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
backendApiVersionString = MG_Backend::DirectVulkan::FormatBackendAPIVersionString(
|
||||
summary.deviceName, summary.apiVersionString, summary.driverVersionString);
|
||||
advertisedExtensions = JoinAdvertisedExtensions(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(
|
||||
summary.shaderSubgroupUsable, summary.timerQueriesSupported, summary.samplerAnisotropySupported));
|
||||
summary.shaderSubgroupUsable, summary.timerQueriesSupported, summary.samplerAnisotropySupported,
|
||||
summary.drawIndirectFirstInstanceSupported && summary.shaderDrawParametersSupported));
|
||||
}
|
||||
AppendMobileGLReportedRows(builder, MG_Backend::DirectVulkan::GetRendererIdentity(), backendApiVersionString,
|
||||
advertisedExtensions);
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace MobileGL {
|
||||
"imageAtomicXor", "imageLoad", "imageSize", "imageStore", "imulExtended",
|
||||
"intBitsToFloat", "interpolateAtCentroid", "interpolateAtOffset",
|
||||
"interpolateAtSample", "inverse", "inversesqrt", "isinf", "isnan",
|
||||
"ldexp", "length", "lessThan", "lessThanEqual", "log", "log2",
|
||||
"ldexp", "length", "length_squared", "lessThan", "lessThanEqual", "log", "log2",
|
||||
"matrixCompMult", "max", "max3", "memoryBarrier",
|
||||
"memoryBarrierAtomicCounter", "memoryBarrierBuffer", "memoryBarrierImage",
|
||||
"memoryBarrierShared", "mid3", "min", "min3", "mix", "mod", "modf",
|
||||
|
||||
+15
-10
@@ -19,23 +19,27 @@ namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
constexpr const char* kConflictingName = "sampler";
|
||||
constexpr const char* kCompatName = "MGL_COMPAT_sampler";
|
||||
const char* GetCompatName(StringView name) {
|
||||
if (name == "sampler") return "MGL_COMPAT_sampler";
|
||||
if (name == "new") return "MGL_COMPAT_new";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Bool IsNamedSamplerFunctionParameter(spvtools::opt::IRContext* context,
|
||||
spvtools::opt::Instruction& nameInst) {
|
||||
const char* GetConflictingFunctionParameterCompatName(spvtools::opt::IRContext* context,
|
||||
spvtools::opt::Instruction& nameInst) {
|
||||
if (nameInst.opcode() != spv::Op::OpName || nameInst.NumInOperands() < 2) {
|
||||
return false;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (nameInst.GetInOperand(1).AsString() != kConflictingName) {
|
||||
return false;
|
||||
const char* compatName = GetCompatName(nameInst.GetInOperand(1).AsString());
|
||||
if (compatName == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* defUseMgr = context->get_def_use_mgr();
|
||||
const Uint32 targetId = nameInst.GetSingleWordInOperand(0);
|
||||
const auto* target = defUseMgr->GetDef(targetId);
|
||||
return target != nullptr && target->opcode() == spv::Op::OpFunctionParameter;
|
||||
return target != nullptr && target->opcode() == spv::Op::OpFunctionParameter ? compatName : nullptr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -44,12 +48,13 @@ namespace MobileGL {
|
||||
auto* irContext = context();
|
||||
|
||||
for (auto& debugInst : irContext->debugs2()) {
|
||||
if (!IsNamedSamplerFunctionParameter(irContext, debugInst)) {
|
||||
const char* compatName = GetConflictingFunctionParameterCompatName(irContext, debugInst);
|
||||
if (compatName == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
debugInst.SetInOperand(
|
||||
1, spvtools::utils::MakeVector<spvtools::opt::Operand::OperandData>(kCompatName));
|
||||
1, spvtools::utils::MakeVector<spvtools::opt::Operand::OperandData>(compatName));
|
||||
modified = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ The bundled fixtures cover:
|
||||

|
||||
- minecraft-1.21.4-main-menu: captured from Minecraft 1.21.4's main menu.
|
||||

|
||||
- minecraft-1.21.11-main-menu: captured from Minecraft 1.21.11's main menu on a Pixel 8 Pro through FCL MobileGL.
|
||||

|
||||
- minecraft-1.17-main-menu-854: captured from Minecraft 1.17's 854x480 main menu through FCL MobileGL capture.
|
||||

|
||||
- minecraft-1.21.4-in-world: captured from Minecraft 1.21.4 after entering a singleplayer world.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -40,6 +40,13 @@
|
||||
"target_call": 481787,
|
||||
"timeout_seconds": 180
|
||||
},
|
||||
{
|
||||
"name": "minecraft-1.21.11-main-menu",
|
||||
"trace_archive": "minecraft-1.21.11-main-menu.tgz",
|
||||
"golden": "minecraft-1.21.11-main-menu.0000205347.png",
|
||||
"target_call": 205347,
|
||||
"timeout_seconds": 180
|
||||
},
|
||||
{
|
||||
"name": "minecraft-1.17-main-menu-854",
|
||||
"trace_archive": "minecraft-1.17-main-menu-854.tgz",
|
||||
|
||||
Reference in New Issue
Block a user