Merge remote-tracking branch 'origin/cts-gl33' into dev

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