From 12c94111b51cc27134d5dec287475aa1ca1c7e7c Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 18 Aug 2026 03:04:09 -0400 Subject: [PATCH] [Fix, Test] (DirectVulkan, SelfTest, MG_IntegrationTest, TraceReplay): snapshot sampler/image feedback and add Program 203 diagnostics Co-Authored-By: Claude Fable 5 --- 3rdparty/apitrace | 2 +- CMakeLists.txt | 1 + .../DirectVulkan/Renderer/UniformManager.cpp | 116 ++- .../DirectVulkan/Renderer/UniformManager.h | 20 +- .../Renderer/VkTextureManager.cpp | 152 +++ .../DirectVulkan/Renderer/VkTextureManager.h | 12 + .../DirectVulkan/Renderer/VulkanRenderer.cpp | 99 +- .../DirectVulkan/Renderer/VulkanRenderer.h | 10 + MobileGL/MG_IntegrationTest/CMakeLists.txt | 1 + .../MG_IntegrationTest/Harness/HeadlessGL.cpp | 68 +- .../Program203FirstReductionScenario.cpp | 878 ++++++++++++++++++ .../Scenarios/ViewportArrayScenario.cpp | 8 +- MobileGL/MG_Test/CMakeLists.txt | 1 + MobileGL/MG_Test/SanityTest.cpp | 10 + MobileGL/MG_Test/SelfTest/CMakeLists.txt | 19 + .../DriverPostProgram203WitnessTest.cpp | 188 ++++ MobileGL/MG_Util/SelfTest/DriverPost.cpp | 450 ++++++++- .../SelfTest/DriverPostProgram203Witness.comp | 164 ++++ .../SelfTest/DriverPostProgram203Witness.cpp | 277 ++++++ .../SelfTest/DriverPostProgram203Witness.h | 151 +++ .../SelfTest/DriverPostProgram203WitnessSpv.h | 291 ++++++ .../app/src/trace/cpp/CMakeLists.txt | 9 +- .../src/trace/cpp/apitrace_glws_android.cpp | 2 + .../app/src/trace/cpp/trace_replay_core.cpp | 19 + .../app/src/trace/cpp/trace_replay_core.hpp | 3 + .../app/src/trace/cpp/trace_replay_jni.cpp | 22 +- .../plugin/trace/TraceReplayActivity.java | 14 +- android-plugin/trace-replay-ci.sh | 28 + tools/trace_replay/apitrace_fbo_dump.cpp | 245 ++++- tools/trace_replay/apitrace_fbo_dump.hpp | 7 +- 30 files changed, 3221 insertions(+), 46 deletions(-) create mode 100644 MobileGL/MG_IntegrationTest/Scenarios/Program203FirstReductionScenario.cpp create mode 100644 MobileGL/MG_Test/SelfTest/CMakeLists.txt create mode 100644 MobileGL/MG_Test/SelfTest/DriverPostProgram203WitnessTest.cpp create mode 100644 MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.comp create mode 100644 MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.cpp create mode 100644 MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.h create mode 100644 MobileGL/MG_Util/SelfTest/DriverPostProgram203WitnessSpv.h diff --git a/3rdparty/apitrace b/3rdparty/apitrace index 10935bb5..c8036190 160000 --- a/3rdparty/apitrace +++ b/3rdparty/apitrace @@ -1 +1 @@ -Subproject commit 10935bb5e466c84ff81141badcbe50b5745ea200 +Subproject commit c8036190fcb79ce0d64cf44ab419c54c3307cd71 diff --git a/CMakeLists.txt b/CMakeLists.txt index f3265ee4..483c6fed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -298,6 +298,7 @@ set(SOURCE_FILES MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp MobileGL/MG_Util/SelfTest/DriverPost.cpp + MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.cpp MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index e6d4bd5d..4e86572e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -542,11 +542,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { outImageInfo = { .sampler = m_samplerManager->GetOrCreateSampler(*samplerBindingOverride.sampler, - *samplerBindingOverride.texture), + *samplerBindingOverride.texture, + samplerBindingOverride.forceNearestFiltering, + resource->sampledLevelCount), .imageView = samplerBindingOverride.imageView != VK_NULL_HANDLE ? samplerBindingOverride.imageView : (resource->sampledView != VK_NULL_HANDLE ? resource->sampledView : resource->fullView), - .imageLayout = resource->layout, + .imageLayout = samplerBindingOverride.imageLayout != VK_IMAGE_LAYOUT_UNDEFINED ? + samplerBindingOverride.imageLayout : resource->layout, }; return outImageInfo.sampler != VK_NULL_HANDLE; } @@ -1269,6 +1272,87 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } + Bool UniformManager::SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel, + GLint imageLevel, GLenum imageAccess) { + return imageAccess != GL_READ_ONLY && imageLevel >= samplerBaseLevel && imageLevel <= samplerMaxLevel; + } + + Bool UniformManager::CollectSamplerImageFeedback( + const MG_State::GLState::ProgramObject& program, + const ProgramFactory::VkProgramObject& programObj, + Vector& outBindings) const { + outBindings.clear(); + MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, + "CollectSamplerImageFeedback: GL context is null"); + if (programObj.declinedDescriptors) return true; + + for (const Uint32 samplerBinding : programObj.activeBindings) { + if (samplerBinding >= m_maxBindings || + programObj.bindingKinds[samplerBinding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) { + continue; + } + const Uint32 samplerCount = BindingDescriptorCount(programObj, samplerBinding); + for (Uint32 samplerElement = 0; samplerElement < samplerCount; ++samplerElement) { + MG_State::GLState::ITextureObject* sampledTexture = nullptr; + const MG_State::GLState::SamplerObject* sampledSampler = nullptr; + if (!ResolveSampledBinding(program, programObj, samplerBinding, samplerElement, + sampledTexture, sampledSampler) || + sampledTexture == nullptr || sampledSampler == nullptr || + MG_State::GLState::SamplesAsIncompleteTexture(sampledTexture, sampledSampler)) { + // ResolveSamplerDescriptor uses a fallback in these cases, which cannot + // alias the image-unit binding of the original texture. + continue; + } + // Multisample source images intentionally omit TRANSFER_SRC usage. Keep their existing + // direct binding instead of turning otherwise valid sampler2DMS/image2DMS dispatches + // into failed dispatches; a correct snapshot for them needs a same-sample-count path. + const TextureTarget sampledTarget = sampledTexture->GetTarget(); + if (sampledTarget == TextureTarget::Texture2DMultisample || + sampledTarget == TextureTarget::Texture2DMultisampleArray) { + continue; + } + const auto& levelRange = sampledTexture->GetLevelRange(); + Bool aliasesWritableImage = false; + for (const Uint32 imageBinding : programObj.activeBindings) { + if (imageBinding >= m_maxBindings || + programObj.bindingKinds[imageBinding] != ProgramFactory::DescriptorBindingKind::StorageImage) { + continue; + } + if (imageBinding >= programObj.samplerUniformLocationByBinding.size()) return false; + const Int baseLocation = programObj.samplerUniformLocationByBinding[imageBinding]; + if (baseLocation < 0) return false; + const Uint32 imageCount = BindingDescriptorCount(programObj, imageBinding); + for (Uint32 imageElement = 0; imageElement < imageCount; ++imageElement) { + const Int location = ResolveDescriptorElementLocation(program, baseLocation, imageElement); + if (location < 0) return false; + const Int imageUnit = program.GetUniformSamplerOrImageUnitIndex(static_cast(location)); + if (imageUnit < 0 || imageUnit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) { + return false; + } + const auto& image = MG_State::pGLContext->GetImageTextureBinding(imageUnit); + // A sampler view exposes all layers of its target; equal texture plus an + // overlapping mip therefore aliases the writable image subresource. + if (image.Texture.get() == sampledTexture && + SamplerOverlapsWritableImageSubresource(levelRange.x(), levelRange.y(), + image.Level, image.Access)) { + aliasesWritableImage = true; + break; + } + } + if (aliasesWritableImage) break; + } + if (aliasesWritableImage) { + outBindings.push_back({.samplerBinding = samplerBinding, + .samplerElement = samplerElement, + .texture = sampledTexture, + .sampler = sampledSampler, + .numericDomain = programObj.samplerNumericDomainByBinding[samplerBinding]}); + } + } + } + return true; + } + Bool UniformManager::ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 arrayElement, UboBindResult& out) const { @@ -1642,7 +1726,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 frameIndex, VkPipelineBindPoint bindPoint, const SamplerBindingOverride* samplerBindingOverride, - Bool samplerDescriptorsUnchangedHint) { + Bool samplerDescriptorsUnchangedHint, + const Vector* samplerBindingOverrides) { // This program has a descriptor MobileGL could not resolve (see // VkProgramObject::declinedDescriptors). Refusing here is the whole of the decline: the // binding is still declared in the layout, so the pipeline is consistent with the shader @@ -1669,7 +1754,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { // sampler binding, and an unchanged (buffer, range) for the single // dynamic UBO covers the rest - except the dynamic offset, which rebinding // the SAME set delivers without any descriptor write. - const Bool cacheable = (samplerBindingOverride == nullptr); + const Bool cacheable = samplerBindingOverride == nullptr && + (samplerBindingOverrides == nullptr || samplerBindingOverrides->empty()); if (cacheable && samplerDescriptorsUnchangedHint && m_fastRebindMemo.valid && m_fastRebindMemo.frameIndex == frameIndex && m_fastRebindMemo.programLifetimeId == program.GetLifetimeId() && @@ -1893,13 +1979,23 @@ namespace MobileGL::MG_Backend::DirectVulkan { const SizeT firstImageInfoIndex = imageInfos.size(); for (Uint32 element = 0; element < descriptorCount; ++element) { VkDescriptorImageInfo imageInfo{}; - Bool hasImage = false; - if (overrideThisBinding && element == 0) { - hasImage = ResolveSamplerDescriptorOverride(*samplerBindingOverride, imageInfo); - } else { - hasImage = ResolveSamplerDescriptor(commandBuffer, program, programObj, binding, element, - imageInfo, samplerDescriptorsUnchangedHint); + const SamplerBindingOverride* overrideForElement = + overrideThisBinding && element == 0 ? samplerBindingOverride : nullptr; + if (overrideForElement == nullptr && samplerBindingOverrides != nullptr) { + const auto overrideIt = std::find_if( + samplerBindingOverrides->begin(), samplerBindingOverrides->end(), + [binding, element](const SamplerBindingOverride& candidate) { + return candidate.binding == binding && candidate.element == element; + }); + if (overrideIt != samplerBindingOverrides->end()) { + overrideForElement = &*overrideIt; + } } + const Bool hasImage = overrideForElement != nullptr + ? ResolveSamplerDescriptorOverride(*overrideForElement, imageInfo) + : ResolveSamplerDescriptor(commandBuffer, program, programObj, binding, + element, imageInfo, + samplerDescriptorsUnchangedHint); if (!hasImage) { MGLOG_E_ONCE( "UniformDescriptorBinder::BindProgramUniformBuffers failed: sampler binding %u element %u " diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index 3ac6b87c..d83c4e4f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -26,9 +26,20 @@ namespace MobileGL::MG_Backend::DirectVulkan { public: struct SamplerBindingOverride { Uint32 binding = 0; + Uint32 element = 0; MG_State::GLState::ITextureObject* texture = nullptr; const MG_State::GLState::SamplerObject* sampler = nullptr; VkImageView imageView = VK_NULL_HANDLE; + VkImageLayout imageLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Bool forceNearestFiltering = false; + }; + + struct SamplerImageFeedbackBinding { + Uint32 samplerBinding = 0; + Uint32 samplerElement = 0; + MG_State::GLState::ITextureObject* texture = nullptr; + const MG_State::GLState::SamplerObject* sampler = nullptr; + SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown; }; Bool Initialize(VkDevice device, VkBufferManager* bufferManager, @@ -79,6 +90,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Vector& outTextures) const; + Bool CollectSamplerImageFeedback( + const MG_State::GLState::ProgramObject& program, + const ProgramFactory::VkProgramObject& programObj, + Vector& outBindings) const; + static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel, + GLint imageLevel, GLenum imageAccess); // samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that // every input of every combined-image-sampler resolution is unchanged since the // previous draw's resolve - same (texture, sampler) per binding, texture params @@ -91,7 +108,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 frameIndex, VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, const SamplerBindingOverride* samplerBindingOverride = nullptr, - Bool samplerDescriptorsUnchangedHint = false); + Bool samplerDescriptorsUnchangedHint = false, + const Vector* samplerBindingOverrides = nullptr); // Pure format-policy helper kept public for host regression tests. Formatted storage // images use their shader qualifier; transformed float images use glBindImageTexture's diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index c1a008fe..c068f076 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -1291,6 +1291,158 @@ namespace MobileGL::MG_Backend::DirectVulkan { return ok; } + Bool VkTextureManager::SnapshotTextureForSampling(VkCommandBuffer commandBuffer, + MG_State::GLState::ITextureObject& texture, + SamplerNumericDomain numericDomain, + VkPipelineStageFlags consumerShaderStageMask, + SampledTextureSnapshot& outSnapshot) { + outSnapshot = {}; + TextureResource* source = SyncTextureAndGetDescriptor(texture); + if (source == nullptr || source->image == VK_NULL_HANDLE || source->sampleCount != VK_SAMPLE_COUNT_1_BIT || + source->sampledLevelCount == 0) { + return false; + } + + const VkFormat sampledFormat = ResolveSampledImageViewFormat(source->format, numericDomain); + if (sampledFormat == VK_FORMAT_UNDEFINED || + !AreSampledImageViewFormatsCompatible(source->format, sampledFormat)) { + MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d cannot create sampled view format=%d from image format=%d", + texture.GetExternalIndex(), static_cast(sampledFormat), static_cast(source->format)); + return false; + } + if (sampledFormat != source->format && + (source->imageCreateFlags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) == 0) { + MGLOG_E_ONCE("SnapshotTextureForSampling: textureId=%d needs unavailable mutable image format=%d for sampled view=%d", + texture.GetExternalIndex(), static_cast(source->format), static_cast(sampledFormat)); + return false; + } + + VkImageType imageType = VK_IMAGE_TYPE_2D; + switch (source->viewType) { + case VK_IMAGE_VIEW_TYPE_1D: + case VK_IMAGE_VIEW_TYPE_1D_ARRAY: + imageType = VK_IMAGE_TYPE_1D; + break; + case VK_IMAGE_VIEW_TYPE_3D: + imageType = VK_IMAGE_TYPE_3D; + break; + default: + break; + } + + TextureResource snapshot{}; + VkImageCreateInfo imageInfo{}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.flags = source->imageCreateFlags; + imageInfo.imageType = imageType; + imageInfo.extent = {source->extent.width, source->extent.height, source->depth}; + imageInfo.mipLevels = source->mipLevels; + imageInfo.arrayLayers = source->arrayLayers; + imageInfo.format = source->format; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + // Keep the temporary's view-format list just as narrow as the source's sampler use. This + // has no storage-image usage, so unlike an app image binding the exact list is knowable. + Vector viewFormats; + VkImageFormatListCreateInfo formatListInfo{}; + if (m_imageFormatListSupported && (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) { + viewFormats.push_back(source->format); + if (sampledFormat != source->format) { + viewFormats.push_back(sampledFormat); + } + formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO; + formatListInfo.viewFormatCount = static_cast(viewFormats.size()); + formatListInfo.pViewFormats = viewFormats.data(); + imageInfo.pNext = &formatListInfo; + } + + VmaAllocationCreateInfo allocationInfo{}; + allocationInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; + allocationInfo.requiredFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + const VkResult createResult = + vmaCreateImage(m_allocator, &imageInfo, &allocationInfo, &snapshot.image, &snapshot.allocation, nullptr); + if (createResult != VK_SUCCESS) { + MGLOG_E_ONCE("SnapshotTextureForSampling: vmaCreateImage failed result=%d textureId=%d", createResult, + texture.GetExternalIndex()); + return false; + } + + snapshot.extent = source->extent; + snapshot.depth = source->depth; + snapshot.arrayLayers = source->arrayLayers; + snapshot.mipLevels = source->mipLevels; + snapshot.sampledBaseMipLevel = source->sampledBaseMipLevel; + snapshot.sampledLevelCount = source->sampledLevelCount; + snapshot.format = source->format; + snapshot.aspect = source->aspect; + snapshot.viewType = source->viewType; + snapshot.sampleCount = VK_SAMPLE_COUNT_1_BIT; + snapshot.imageCreateFlags = imageInfo.flags; + snapshot.usageFlags = imageInfo.usage; + + const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat()); + const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo); + const VkImageAspectFlags sampledAspect = + ResolveSampledImageViewAspectMask(snapshot.aspect, texture.GetDepthStencilTextureMode()); + snapshot.sampledView = CreateImageView(snapshot.image, sampledFormat, sampledAspect, snapshot.viewType, + snapshot.sampledBaseMipLevel, snapshot.sampledLevelCount, 0, + snapshot.arrayLayers, &sampledComponents); + if (snapshot.sampledView == VK_NULL_HANDLE) { + MGLOG_E_ONCE("SnapshotTextureForSampling: failed to create sampled view textureId=%d", texture.GetExternalIndex()); + return false; + } + + VkPipelineStageFlags sourceStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags sourceAccessMask = 0; + const VkImageLayout sourceLayout = source->layout; + GetImageTransitionSourceState(sourceLayout, sourceStageMask, sourceAccessMask); + if (!TransitionImageLayout(commandBuffer, source->image, source->layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + sourceStageMask, VK_PIPELINE_STAGE_TRANSFER_BIT, sourceAccessMask, + VK_ACCESS_TRANSFER_READ_BIT, source->aspect, 0, source->mipLevels) || + !TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, + VK_ACCESS_TRANSFER_WRITE_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel, + snapshot.sampledLevelCount)) { + return false; + } + + Vector copyRegions; + copyRegions.reserve(snapshot.sampledLevelCount); + for (Uint32 level = snapshot.sampledBaseMipLevel; + level < snapshot.sampledBaseMipLevel + snapshot.sampledLevelCount; ++level) { + VkImageCopy copy{}; + copy.srcSubresource = {source->aspect, level, 0, source->arrayLayers}; + copy.dstSubresource = {snapshot.aspect, level, 0, snapshot.arrayLayers}; + copy.extent = {std::max(source->extent.width >> level, 1u), + std::max(source->extent.height >> level, 1u), + std::max(source->depth >> level, 1u)}; + copyRegions.push_back(copy); + } + vkCmdCopyImage(commandBuffer, source->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, snapshot.image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(copyRegions.size()), copyRegions.data()); + + if (!TransitionImageLayout(commandBuffer, snapshot.image, snapshot.layout, + ResolveSampledReadOnlyLayout(snapshot.aspect), VK_PIPELINE_STAGE_TRANSFER_BIT, + consumerShaderStageMask, VK_ACCESS_TRANSFER_WRITE_BIT, + VK_ACCESS_SHADER_READ_BIT, snapshot.aspect, snapshot.sampledBaseMipLevel, + snapshot.sampledLevelCount) || + !TransitionImageLayout(commandBuffer, source->image, source->layout, sourceLayout, + VK_PIPELINE_STAGE_TRANSFER_BIT, consumerShaderStageMask, + VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, + source->aspect, 0, source->mipLevels)) { + return false; + } + + StampResourceRecordingUse(*source); + outSnapshot = {.imageView = snapshot.sampledView, .layout = snapshot.layout}; + DeferResourceRelease(Move(snapshot)); + return true; + } + void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) { m_storageImageTextures.insert(MakeTextureIdentity(&texture)); } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 5ad1771c..40f23d0d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -310,6 +310,11 @@ public: static inline VmaAllocator s_allocator = VK_NULL_HANDLE; }; + struct SampledTextureSnapshot { + VkImageView imageView = VK_NULL_HANDLE; + VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED; + }; + Bool Initialize(const InitInfo& initInfo); void Shutdown(); void BeginFrame(Uint32 frameIndex); @@ -343,6 +348,13 @@ public: VkImageLayout newLayout); Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); + // Copies the complete sampler-visible mip range into a transient sampled image. The source is + // restored to its prior layout, so image-store descriptors continue to name the original image. + // The transient ownership is tied to the current frame slot and is safe through its submission. + Bool SnapshotTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture, + SamplerNumericDomain numericDomain, + VkPipelineStageFlags consumerShaderStageMask, + SampledTextureSnapshot& outSnapshot); // Recording-generation bookkeeping for the pre-pass command stream. The // generation advances every time the frame command buffer (re)begins diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 4cf9d052..85ec4a46 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -5412,7 +5412,81 @@ void main() { } return true; } + Bool VulkanRenderer::PrepareSamplerImageFeedbackSnapshots( + FrameContext::FrameData& frame, + const MG_State::GLState::ProgramObject& program, + const ProgramFactory::VkProgramObject& programObj, + VkPipelineStageFlags consumerShaderStageMask) { + auto& feedbackBindings = m_samplerImageFeedbackScratch; + auto& overrides = m_samplerImageBindingOverridesScratch; + overrides.clear(); + if (!programObj.hasStorageImages) { + feedbackBindings.clear(); + return true; + } + if (!m_uniformManager->CollectSamplerImageFeedback(program, programObj, feedbackBindings)) { + MGLOG_E_ONCE("%s: failed to collect sampler/image feedback for program=%u", __func__, + program.GetExternalIndex()); + return false; + } + if (feedbackBindings.empty()) { + return true; + } + // Copy and layout barriers cannot be recorded inside a render pass. A graphics draw only + // gets here after an actual sampled/writable-image mip overlap was found, so ordinary + // graphics draws retain the active pass. + if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { + VkRenderPassManager::EndRenderPass(frame.commandBuffer); + } + + struct SnapshotCacheEntry { + MG_State::GLState::ITextureObject* texture = nullptr; + SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown; + VkTextureManager::SampledTextureSnapshot snapshot{}; + }; + Vector snapshotCache; + snapshotCache.reserve(feedbackBindings.size()); + overrides.reserve(feedbackBindings.size()); + for (const auto& feedback : feedbackBindings) { + VkTextureManager::SampledTextureSnapshot snapshot{}; + const auto existing = std::find_if( + snapshotCache.begin(), snapshotCache.end(), [&feedback](const SnapshotCacheEntry& candidate) { + return candidate.texture == feedback.texture && candidate.numericDomain == feedback.numericDomain; + }); + if (existing != snapshotCache.end()) { + snapshot = existing->snapshot; + } else { + if (!m_textureManager->SnapshotTextureForSampling(frame.commandBuffer, *feedback.texture, + feedback.numericDomain, consumerShaderStageMask, + snapshot) || + snapshot.imageView == VK_NULL_HANDLE) { + MGLOG_E_ONCE("%s: failed to snapshot textureId=%d for sampler binding=%u element=%u", __func__, + feedback.texture != nullptr ? feedback.texture->GetExternalIndex() : 0, + feedback.samplerBinding, feedback.samplerElement); + return false; + } + snapshotCache.push_back({.texture = feedback.texture, + .numericDomain = feedback.numericDomain, + .snapshot = snapshot}); + } + overrides.push_back({ + .binding = feedback.samplerBinding, + .element = feedback.samplerElement, + .texture = feedback.texture, + .sampler = feedback.sampler, + .imageView = snapshot.imageView, + .imageLayout = snapshot.layout, + .forceNearestFiltering = feedback.numericDomain == SamplerNumericDomain::SignedInteger || + feedback.numericDomain == SamplerNumericDomain::UnsignedInteger, + }); + if (program.GetExternalIndex() == 194 && feedback.texture->GetExternalIndex() == 75) { + MGLOG_D_ONCE("sampler/image feedback snapshot: program=194 texture=75 binding=%u element=%u view=%p", + feedback.samplerBinding, feedback.samplerElement, snapshot.imageView); + } + } + return true; + } // The scissor rectangle Vulkan needs for ARB_viewport_array index `index`. Vulkan has no // per-viewport scissor-test TOGGLE - a scissor rectangle always applies - so an index whose @@ -6094,6 +6168,11 @@ void main() { MGLOG_E_ONCE("SetupDraw skipped: storage image preparation failed"); return false; } + if (!PrepareSamplerImageFeedbackSnapshots(frame, program, programObj, + VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT)) { + MGLOG_E_ONCE("SetupDraw skipped: sampler/image feedback snapshot failed"); + return false; + } auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); @@ -6338,7 +6417,9 @@ void main() { } const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers( - frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex()); + frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(), + VK_PIPELINE_BIND_POINT_GRAPHICS, nullptr, false, + m_samplerImageBindingOverridesScratch.empty() ? nullptr : &m_samplerImageBindingOverridesScratch); if (!boundUniforms) { MGLOG_E_ONCE("SetupDraw skipped: BindProgramUniformBuffers failed"); return false; @@ -6461,6 +6542,11 @@ void main() { MGLOG_E_ONCE("DispatchCompute skipped: storage image preparation failed"); return; } + if (!PrepareSamplerImageFeedbackSnapshots(frame, program, programObj, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT)) { + MGLOG_E_ONCE("DispatchCompute skipped: sampler/image feedback snapshot failed"); + return; + } const VkPipeline pipeline = GetOrCreateComputePipeline(programObj); if (pipeline == VK_NULL_HANDLE) { @@ -6472,7 +6558,8 @@ void main() { vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers( frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(), - VK_PIPELINE_BIND_POINT_COMPUTE); + VK_PIPELINE_BIND_POINT_COMPUTE, nullptr, false, + m_samplerImageBindingOverridesScratch.empty() ? nullptr : &m_samplerImageBindingOverridesScratch); if (!boundUniforms) { MGLOG_E_ONCE("DispatchCompute skipped: BindProgramUniformBuffers failed"); return; @@ -6507,6 +6594,11 @@ void main() { MGLOG_E_ONCE("DispatchComputeIndirect skipped: storage image preparation failed"); return; } + if (!PrepareSamplerImageFeedbackSnapshots(frame, program, programObj, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT)) { + MGLOG_E_ONCE("DispatchComputeIndirect skipped: sampler/image feedback snapshot failed"); + return; + } const VkPipeline pipeline = GetOrCreateComputePipeline(programObj); if (pipeline == VK_NULL_HANDLE) { @@ -6518,7 +6610,8 @@ void main() { vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); const Bool boundUniforms = m_uniformManager->BindProgramUniformBuffers( frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(), - VK_PIPELINE_BIND_POINT_COMPUTE); + VK_PIPELINE_BIND_POINT_COMPUTE, nullptr, false, + m_samplerImageBindingOverridesScratch.empty() ? nullptr : &m_samplerImageBindingOverridesScratch); if (!boundUniforms) { MGLOG_E_ONCE("DispatchComputeIndirect skipped: BindProgramUniformBuffers failed"); return; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 5aac2886..06b23605 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -929,6 +929,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { // already sampleable. Vector m_sampledResourcesScratch; Vector m_storageImageTexturesScratch; + Vector m_samplerImageFeedbackScratch; + Vector m_samplerImageBindingOverridesScratch; Vector m_vertexBuffersScratch; Vector m_vertexOffsetsScratch; Vector m_patchedAttributesScratch; @@ -1148,6 +1150,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { FrameContext::FrameData& frame, const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj); + // Vulkan forbids a sampled descriptor and writable storage descriptor from naming the + // same image subresource in one shader operation. Snapshot only the sampler side; the + // storage descriptor continues to name the application texture. + Bool PrepareSamplerImageFeedbackSnapshots( + FrameContext::FrameData& frame, + const MG_State::GLState::ProgramObject& program, + const ProgramFactory::VkProgramObject& programObj, + VkPipelineStageFlags consumerShaderStageMask); // The per-draw dynamic-state tail (viewport, scissor, blend constants, depth // bias, line width, stencil), gated behind one render-state-parameters-version diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 9d7c8d7b..1c234208 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -68,6 +68,7 @@ add_executable(MobileGLIntegrationTest Scenarios/DoublePrecisionScenario.cpp Scenarios/UniformInitializerScenario.cpp Scenarios/SwizzleAccessRoutineScenario.cpp + Scenarios/Program203FirstReductionScenario.cpp Scenarios/ProgramPipelineScenario.cpp Scenarios/ImageLoadStoreSsoScenario.cpp Scenarios/ImageTargetKindScenario.cpp diff --git a/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp b/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp index 7526028f..a745faea 100644 --- a/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp +++ b/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp @@ -15,6 +15,11 @@ #include #include +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#include +#endif + // MobileGL's own headers, in the order MobileGL/Includes.h uses them: GL/gl.h // first, then glcorearb.h for the 3.x+ entry points. This binary links // MobileGL_s, so every gl*/egl* below binds to MobileGL's implementation, not @@ -53,6 +58,37 @@ namespace MGITest { constexpr int kSurfaceWidth = 128; constexpr int kSurfaceHeight = 96; +#if defined(_WIN32) + HWND g_testWindow = nullptr; + + HWND CreateTestWindow() { + static const wchar_t* const kClassName = L"MobileGLIntegrationTestWindow"; + static bool registered = false; + if (!registered) { + WNDCLASSW windowClass{}; + windowClass.lpfnWndProc = DefWindowProcW; + windowClass.hInstance = GetModuleHandleW(nullptr); + windowClass.lpszClassName = kClassName; + if (RegisterClassW(&windowClass) == 0 && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) { + return nullptr; + } + registered = true; + } + return CreateWindowExW(0, kClassName, L"MobileGL Integration Test", WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, CW_USEDEFAULT, kSurfaceWidth, kSurfaceHeight, nullptr, nullptr, + GetModuleHandleW(nullptr), nullptr); + } +#endif + + bool UseWindowSurface() { +#if defined(_WIN32) + const char* value = std::getenv("MOBILEGL_ITEST_WINDOW_SURFACE"); + return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0; +#else + return false; +#endif + } + std::string EnvOr(const char* name, const char* fallback) { const char* value = std::getenv(name); return (value != nullptr && value[0] != '\0') ? std::string(value) : std::string(fallback); @@ -134,8 +170,9 @@ namespace MGITest { return 3; } + const bool useWindowSurface = UseWindowSurface(); const EGLint configAttribs[] = {EGL_SURFACE_TYPE, - EGL_PBUFFER_BIT, + useWindowSurface ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, @@ -152,7 +189,9 @@ namespace MGITest { EGLConfig config = nullptr; EGLint configCount = 0; if (eglChooseConfig(display, configAttribs, &config, 1, &configCount) != EGL_TRUE || configCount < 1) { - outReason = WithEglError("eglChooseConfig found no pbuffer-capable RGBA8/D24 config"); + outReason = WithEglError(useWindowSurface + ? "eglChooseConfig found no window-capable RGBA8/D24 config" + : "eglChooseConfig found no pbuffer-capable RGBA8/D24 config"); return 4; } @@ -166,10 +205,23 @@ namespace MGITest { return 5; } - const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE}; - EGLSurface surface = eglCreatePbufferSurface(display, config, pbufferAttribs); + EGLSurface surface = EGL_NO_SURFACE; + if (useWindowSurface) { +#if defined(_WIN32) + if (g_testWindow == nullptr) g_testWindow = CreateTestWindow(); + if (g_testWindow == nullptr) { + outReason = "failed to create the Windows integration-test window"; + return 6; + } + surface = eglCreateWindowSurface(display, config, g_testWindow, nullptr); +#endif + } else { + const EGLint pbufferAttribs[] = {EGL_WIDTH, kSurfaceWidth, EGL_HEIGHT, kSurfaceHeight, EGL_NONE}; + surface = eglCreatePbufferSurface(display, config, pbufferAttribs); + } if (surface == EGL_NO_SURFACE) { - outReason = WithEglError("eglCreatePbufferSurface failed"); + outReason = WithEglError(useWindowSurface ? "eglCreateWindowSurface failed" + : "eglCreatePbufferSurface failed"); return 6; } // The step that brings the whole backend up (DirectVulkan creates its @@ -491,6 +543,12 @@ namespace MGITest { if (m_context != nullptr) eglDestroyContext(display, static_cast(m_context)); if (m_surface != nullptr) eglDestroySurface(display, static_cast(m_surface)); eglTerminate(display); +#if defined(_WIN32) + if (g_testWindow != nullptr) { + DestroyWindow(g_testWindow); + g_testWindow = nullptr; + } +#endif m_context = nullptr; m_surface = nullptr; m_display = nullptr; diff --git a/MobileGL/MG_IntegrationTest/Scenarios/Program203FirstReductionScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/Program203FirstReductionScenario.cpp new file mode 100644 index 00000000..32539f0d --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/Program203FirstReductionScenario.cpp @@ -0,0 +1,878 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/Program203FirstReductionScenario.cpp +// Copyright (c) 2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// Scenario - PROGRAM 203'S FIRST SUBGROUP REDUCTION. +// +// Program 203 reduces a 32 x 16 exposure tile with a vector subgroup inclusive add, +// then a shared-memory scan of subgroup totals. The source assumes that every +// subgroup has a last lane, that there are 2..32 subgroups, and that local index +// 511 belongs to the last subgroup and its last lane. Those are source assumptions, +// not API contracts. This probe intentionally does not repair them: it records the +// observed topology and makes each handoff independently observable. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { + namespace { + + constexpr std::size_t kInvocationCount = 512; + constexpr std::size_t kScanStageCount = 6; + constexpr std::uint32_t kQuietNanBits = 0x7fc00000u; + constexpr std::size_t kNoSlot = std::numeric_limits::max(); + + struct UVec4 { + std::uint32_t x; + std::uint32_t y; + std::uint32_t z; + std::uint32_t w; + }; + + struct Vec4 { + float x; + float y; + float z; + float w; + }; + + // Matches the std430 block exactly. uvec4/vec4 arrays have a 16-byte + // stride, floats are a dense scalar array, and the outer scan array is + // stage-major in both GLSL and C++. + struct ProbeOutput { + std::array invocation; + std::array subgroup; + std::array reduction; + std::array finalAverage; + std::array, kScanStageCount> scanAfter; + }; + + static_assert(sizeof(UVec4) == 16); + static_assert(sizeof(Vec4) == 16); + static_assert(std::is_standard_layout_v); + static_assert(offsetof(ProbeOutput, invocation) == 0); + static_assert(offsetof(ProbeOutput, subgroup) == 8192); + static_assert(offsetof(ProbeOutput, reduction) == 16384); + static_assert(offsetof(ProbeOutput, finalAverage) == 24576); + static_assert(offsetof(ProbeOutput, scanAfter) == 26624); + static_assert(sizeof(ProbeOutput) == 38912); + + enum class InputMode { + SampledRgba32f, + IndexedSsbo, + }; + + const char* InputModeName(InputMode mode) { + return mode == InputMode::SampledRgba32f ? "sampled RGBA32F" : "indexed SSBO"; + } + + std::uint32_t FloatBits(float value) { + return std::bit_cast(value); + } + + bool SameBits(float lhs, float rhs) { + return FloatBits(lhs) == FloatBits(rhs); + } + + bool IsQuietNanSentinel(float value) { + return FloatBits(value) == kQuietNanBits; + } + + bool DrainGlErrors() { + bool hadError = false; + while (glGetError() != GL_NO_ERROR) hadError = true; + return hadError; + } + + bool HasExtension(const char* wanted) { + GLint extensionCount = 0; + glGetIntegerv(GL_NUM_EXTENSIONS, &extensionCount); + for (GLint i = 0; i < extensionCount; ++i) { + const auto* extension = reinterpret_cast(glGetStringi(GL_EXTENSIONS, static_cast(i))); + if (extension != nullptr && std::string(extension) == wanted) return true; + } + return false; + } + + struct CapabilityInfo { + bool subgroupExtension = false; + GLint subgroupSize = 0; + GLint supportedStages = 0; + GLint supportedFeatures = 0; + GLint maxComputeStorageBlocks = 0; + GLint maxStorageBindings = 0; + GLint maxWorkGroupInvocations = 0; + std::array maxWorkGroupSize{}; + bool queryHadError = false; + + bool SupportsProbe() const { + const auto stages = static_cast(supportedStages); + const auto features = static_cast(supportedFeatures); + return !queryHadError && subgroupExtension && + (stages & GL_COMPUTE_SHADER_BIT) != 0 && + (features & (GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR)) == + (GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR) && + maxComputeStorageBlocks >= 2 && maxStorageBindings >= 2 && + maxWorkGroupInvocations >= static_cast(kInvocationCount) && maxWorkGroupSize[0] >= 32 && + maxWorkGroupSize[1] >= 16 && maxWorkGroupSize[2] >= 1; + } + + std::string MissingRequirements() const { + std::vector missing; + const auto stages = static_cast(supportedStages); + const auto features = static_cast(supportedFeatures); + if (queryHadError) missing.emplace_back("a subgroup/compute capability query generated GL error"); + if (!subgroupExtension) missing.emplace_back("GL_KHR_shader_subgroup"); + if ((stages & GL_COMPUTE_SHADER_BIT) == 0) { + missing.emplace_back("GL_COMPUTE_SHADER_BIT in GL_SUBGROUP_SUPPORTED_STAGES_KHR"); + } + const auto requiredFeatures = + GL_SUBGROUP_FEATURE_BASIC_BIT_KHR | GL_SUBGROUP_FEATURE_ARITHMETIC_BIT_KHR; + if ((features & requiredFeatures) != requiredFeatures) { + missing.emplace_back("basic|arithmetic in GL_SUBGROUP_SUPPORTED_FEATURES_KHR"); + } + if (maxComputeStorageBlocks < 2 || maxStorageBindings < 2) { + missing.emplace_back("two compute SSBO bindings"); + } + if (maxWorkGroupInvocations < static_cast(kInvocationCount) || maxWorkGroupSize[0] < 32 || + maxWorkGroupSize[1] < 16 || maxWorkGroupSize[2] < 1) { + missing.emplace_back("a 32x16x1 / 512-invocation compute workgroup"); + } + + std::ostringstream message; + for (std::size_t i = 0; i < missing.size(); ++i) { + if (i != 0) message << ", "; + message << missing[i]; + } + return message.str(); + } + }; + + CapabilityInfo QueryCapabilities() { + CapabilityInfo info; + DrainGlErrors(); + info.subgroupExtension = HasExtension("GL_KHR_shader_subgroup"); + glGetIntegerv(GL_SUBGROUP_SIZE_KHR, &info.subgroupSize); + glGetIntegerv(GL_SUBGROUP_SUPPORTED_STAGES_KHR, &info.supportedStages); + glGetIntegerv(GL_SUBGROUP_SUPPORTED_FEATURES_KHR, &info.supportedFeatures); + glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &info.maxComputeStorageBlocks); + glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, &info.maxStorageBindings); + glGetIntegerv(GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, &info.maxWorkGroupInvocations); + for (GLuint axis = 0; axis < info.maxWorkGroupSize.size(); ++axis) { + glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, axis, &info.maxWorkGroupSize[axis]); + } + info.queryHadError = DrainGlErrors(); + return info; + } + + void PrintMetadata(const CapabilityInfo& info, std::ostream& output) { + output << "Program203FirstReductionScenario metadata: " + << "GL_SUBGROUP_SIZE_KHR=" << info.subgroupSize + << ", GL_SUBGROUP_SUPPORTED_STAGES_KHR=0x" << std::hex + << static_cast(info.supportedStages) + << ", GL_SUBGROUP_SUPPORTED_FEATURES_KHR=0x" + << static_cast(info.supportedFeatures) << std::dec + << ", subgroupExtension=" << info.subgroupExtension + << ", GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS=" << info.maxComputeStorageBlocks + << ", GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS=" << info.maxStorageBindings + << ", GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS=" << info.maxWorkGroupInvocations + << ", GL_MAX_COMPUTE_WORK_GROUP_SIZE=" << info.maxWorkGroupSize[0] << 'x' + << info.maxWorkGroupSize[1] << 'x' << info.maxWorkGroupSize[2] + << ", queryHadError=" << info.queryHadError << '\n'; + } + + bool DumpRequested() { + const char* value = std::getenv("MOBILEGL_ITEST_SUBGROUP_PROBE_DUMP"); + return value != nullptr && std::string(value) == "1"; + } + + constexpr const char* kShaderPreamble = R"(#version 430 core +#extension GL_KHR_shader_subgroup_basic : require +#extension GL_KHR_shader_subgroup_arithmetic : require + +layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in; + +layout(std430, binding = 1) buffer SubgroupProbeOutput { + uvec4 invocation[512]; + uvec4 subgroup[512]; + vec4 reduction[512]; + float finalAverage[512]; + float scanAfter[6][512]; +} outProbe; + +shared vec2 prefixSumCache[32]; +)"; + + constexpr const char* kSampledInput = R"( +uniform sampler2D colortex2; +uniform vec2 pixelSize; +)"; + + constexpr const char* kIndexedInput = R"( +layout(std430, binding = 0) readonly buffer Input { + float value[512]; +} inputData; +)"; + + // Only the expression producing tileExposure differs between the two + // tests. The remainder is the program-203 first reduction, with stores + // placed after its existing barriers to expose each handoff. + constexpr const char* kSampledTileExposure = R"( + vec2 texCoord = (vec2(gl_GlobalInvocationID.xy) + 0.5) * + vec2(1.0 / 32.0, 1.0 / 16.0); + vec2 sampleCoord = texCoord * (1.0 / 64.0); + sampleCoord.x += (15.0 / 32.0) + pixelSize.x * 12.0; + + float tileExposure = dot( + textureLod(colortex2, sampleCoord, 0.0).rgb, + vec3(0.2125, 0.7154, 0.0721)); +)"; + + constexpr const char* kIndexedTileExposure = R"( + float tileExposure = inputData.value[gl_LocalInvocationIndex]; +)"; + + constexpr const char* kReductionBody = R"( + vec2 sampleLuminance = vec2(tileExposure, 0.0); + sampleLuminance = subgroupInclusiveAdd(sampleLuminance); + float nativeInclusive = sampleLuminance.x; + + // This is a uniform, safety-only branch: it leaves an invalid source + // contract visible without indexing past the 32-entry cache or underflowing + // loopLength - 1. It is deliberately a failure on the CPU, not a skip. + bool sourceDomain = gl_NumSubgroups >= 2u && gl_NumSubgroups <= 32u; + if (!sourceDomain) { + float qNaN = uintBitsToFloat(0x7fc00000u); + uint localIndex = gl_LocalInvocationIndex; + outProbe.invocation[localIndex] = uvec4(localIndex, gl_LocalInvocationID); + outProbe.subgroup[localIndex] = uvec4(gl_SubgroupSize, gl_NumSubgroups, gl_SubgroupID, + gl_SubgroupInvocationID); + outProbe.reduction[localIndex] = vec4(tileExposure, nativeInclusive, qNaN, qNaN); + outProbe.finalAverage[localIndex] = qNaN; + for (uint stage = 0u; stage < 6u; ++stage) + outProbe.scanAfter[stage][localIndex] = qNaN; + return; + } + + if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) + prefixSumCache[gl_SubgroupID] = sampleLuminance; + barrier(); + + float sourceRawSubtotal = prefixSumCache[gl_SubgroupID].x; + + uint loopLength = uint(findMSB(gl_NumSubgroups)); + loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u); + + for (uint scanStage = 0u; scanStage < loopLength; ++scanStage) { + if ((gl_SubgroupID & (1u << scanStage)) > 0u) { + sampleLuminance += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u]; + if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) + prefixSumCache[gl_SubgroupID] = sampleLuminance; + } + barrier(); + outProbe.scanAfter[scanStage][gl_LocalInvocationIndex] = sampleLuminance.x; + } + + float sourceMergedPrefix = sampleLuminance.x; + + if (gl_LocalInvocationIndex == 511u) + prefixSumCache[0] = sampleLuminance / 512.0; + barrier(); + + float avg = prefixSumCache[0].x; + + uint localIndex = gl_LocalInvocationIndex; + outProbe.invocation[localIndex] = uvec4(localIndex, gl_LocalInvocationID); + outProbe.subgroup[localIndex] = uvec4(gl_SubgroupSize, gl_NumSubgroups, gl_SubgroupID, + gl_SubgroupInvocationID); + outProbe.reduction[localIndex] = vec4(tileExposure, nativeInclusive, sourceRawSubtotal, sourceMergedPrefix); + outProbe.finalAverage[localIndex] = avg; +} +)"; + + std::string BuildProbeShader(InputMode mode) { + std::string source = kShaderPreamble; + source += mode == InputMode::SampledRgba32f ? kSampledInput : kIndexedInput; + source += "\nvoid main() {\n"; + source += mode == InputMode::SampledRgba32f ? kSampledTileExposure : kIndexedTileExposure; + source += kReductionBody; + return source; + } + + std::string FormatFloat(float value) { + std::ostringstream text; + text << std::hexfloat << value; + return text.str(); + } + + struct ValidationResult { + bool ok = true; + std::string phase; + std::string message; + bool scanStageMismatch = false; + int scanStage = -1; + bool ownerEvaluated = false; + bool index511IsSourceLastLaneWriter = false; + bool index511IsHighestSubgroupMember = false; + std::uint32_t highestObservedSubgroup = 0; + }; + + ValidationResult Failure(std::string phase, std::string message) { + ValidationResult result; + result.ok = false; + result.phase = std::move(phase); + result.message = std::move(message); + return result; + } + + constexpr float kSampledLuminance = 0.2125f + 0.7154f + 0.0721f; + + float ExpectedInput(InputMode mode, std::uint32_t localIndex) { + return mode == InputMode::SampledRgba32f ? kSampledLuminance : static_cast(localIndex + 1u); + } + + ValidationResult ValidateProbe(const ProbeOutput& output, InputMode mode) { + std::array slotForLocal{}; + slotForLocal.fill(kNoSlot); + + // 1. Record identity. Slots are only used to locate each reported + // local index; all subgroup behavior below groups recorded IDs/lanes. + for (std::size_t slot = 0; slot < kInvocationCount; ++slot) { + const std::uint32_t localIndex = output.invocation[slot].x; + if (localIndex >= kInvocationCount) { + std::ostringstream message; + message << "output slot " << slot << " reports localIndex " << localIndex << " outside [0, 511]"; + return Failure("record identity", message.str()); + } + if (slotForLocal[localIndex] != kNoSlot) { + std::ostringstream message; + message << "localIndex " << localIndex << " appears in output slots " << slotForLocal[localIndex] + << " and " << slot; + return Failure("record identity", message.str()); + } + slotForLocal[localIndex] = slot; + } + for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) { + if (slotForLocal[localIndex] == kNoSlot) { + std::ostringstream message; + message << "localIndex " << localIndex << " is missing from all 512 records"; + return Failure("record identity", message.str()); + } + } + for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) { + const std::size_t slot = slotForLocal[localIndex]; + const UVec4& invocation = output.invocation[slot]; + const std::uint32_t expectedX = static_cast(localIndex % 32u); + const std::uint32_t expectedY = static_cast(localIndex / 32u); + if (invocation.y != expectedX || invocation.z != expectedY || invocation.w != 0u) { + std::ostringstream message; + message << "localIndex " << localIndex << " reports local invocation (" << invocation.y << ',' + << invocation.z << ',' << invocation.w << "), expected (" << expectedX << ',' << expectedY + << ",0)"; + return Failure("record identity", message.str()); + } + const float expectedInput = ExpectedInput(mode, static_cast(localIndex)); + const float actualInput = output.reduction[slot].x; + if (!SameBits(actualInput, expectedInput)) { + std::ostringstream message; + message << "localIndex " << localIndex << " input was " << FormatFloat(actualInput) << ", expected " + << FormatFloat(expectedInput); + return Failure("input", message.str()); + } + } + + // 2. Observed topology. Do not derive lanes or subgroup membership + // from local invocation indices: only the values the shader recorded + // participate in grouping. + const std::uint32_t reportedNumSubgroups = output.subgroup[slotForLocal[0]].y; + if (reportedNumSubgroups == 0u) { + return Failure("observed topology", "localIndex 0 reported gl_NumSubgroups == 0"); + } + if (reportedNumSubgroups > kInvocationCount) { + std::ostringstream message; + message << "reported gl_NumSubgroups=" << reportedNumSubgroups + << " exceeds the 512 recorded invocations, so at least one subgroup ID is missing"; + return Failure("observed topology", message.str()); + } + std::vector> subgroupSlots(reportedNumSubgroups); + for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) { + const std::size_t slot = slotForLocal[localIndex]; + const UVec4& subgroup = output.subgroup[slot]; + if (subgroup.x == 0u || subgroup.y == 0u) { + std::ostringstream message; + message << "localIndex " << localIndex << " reported subgroupSize=" << subgroup.x + << ", numSubgroups=" << subgroup.y; + return Failure("observed topology", message.str()); + } + if (subgroup.y != reportedNumSubgroups) { + std::ostringstream message; + message << "localIndex " << localIndex << " reported numSubgroups=" << subgroup.y + << ", while localIndex 0 reported " << reportedNumSubgroups; + return Failure("observed topology", message.str()); + } + if (subgroup.z >= reportedNumSubgroups) { + std::ostringstream message; + message << "localIndex " << localIndex << " reported subgroupID=" << subgroup.z + << " outside [0, " << (reportedNumSubgroups - 1u) << ']'; + return Failure("observed topology", message.str()); + } + if (subgroup.w >= subgroup.x) { + std::ostringstream message; + message << "localIndex " << localIndex << " reported laneID=" << subgroup.w + << " outside its subgroupSize=" << subgroup.x; + return Failure("observed topology", message.str()); + } + subgroupSlots[subgroup.z].push_back(slot); + } + for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) { + if (subgroupSlots[subgroupID].empty()) { + std::ostringstream message; + message << "reported gl_NumSubgroups=" << reportedNumSubgroups + << " but subgroupID " << subgroupID << " has no recorded members"; + return Failure("observed topology", message.str()); + } + auto& members = subgroupSlots[subgroupID]; + std::sort(members.begin(), members.end(), [&output](std::size_t lhs, std::size_t rhs) { + return output.subgroup[lhs].w < output.subgroup[rhs].w; + }); + for (std::size_t i = 1; i < members.size(); ++i) { + if (output.subgroup[members[i - 1]].w == output.subgroup[members[i]].w) { + std::ostringstream message; + message << "subgroupID " << subgroupID << " contains duplicate laneID " + << output.subgroup[members[i]].w; + return Failure("observed topology", message.str()); + } + } + } + + // 3. Native subgroup arithmetic, in the actual lane ordering emitted + // by the driver. The fixture values and all partial sums are exactly + // representable binary32 values, so compare representation, not epsilon. + std::array nativePrefix{}; + std::vector nativeSubtotal(reportedNumSubgroups, 0.0f); + for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) { + float inclusive = 0.0f; + for (const std::size_t slot : subgroupSlots[subgroupID]) { + const std::uint32_t localIndex = output.invocation[slot].x; + inclusive += ExpectedInput(mode, localIndex); + nativePrefix[slot] = inclusive; + const float actualNative = output.reduction[slot].y; + if (!SameBits(actualNative, inclusive)) { + std::ostringstream message; + message << "subgroupID " << subgroupID << ", laneID " << output.subgroup[slot].w + << ", localIndex " << localIndex << " nativeInclusive was " << FormatFloat(actualNative) + << ", expected " << FormatFloat(inclusive); + return Failure("native subgroup arithmetic", message.str()); + } + } + nativeSubtotal[subgroupID] = inclusive; + } + + // sourceDomain is the narrow source-side safety branch. It is checked + // after native arithmetic so an unsupported source topology still + // reports native subgroup behavior before failing explicitly. + if (reportedNumSubgroups < 2u || reportedNumSubgroups > 32u) { + for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) { + const std::size_t slot = slotForLocal[localIndex]; + const Vec4& reduction = output.reduction[slot]; + if (!IsQuietNanSentinel(reduction.z) || !IsQuietNanSentinel(reduction.w) || + !IsQuietNanSentinel(output.finalAverage[slot])) { + std::ostringstream message; + message << "program 203 source reduction has no valid contract for gl_NumSubgroups=" + << reportedNumSubgroups << "; localIndex " << localIndex + << " did not preserve its qNaN source-reduction sentinel"; + return Failure("source domain", message.str()); + } + for (std::size_t stage = 0; stage < kScanStageCount; ++stage) { + if (!IsQuietNanSentinel(output.scanAfter[stage][slot])) { + std::ostringstream message; + message << "program 203 source reduction has no valid contract for gl_NumSubgroups=" + << reportedNumSubgroups << "; localIndex " << localIndex << ", scan stage " << stage + << " did not preserve its qNaN source-reduction sentinel"; + return Failure("source domain", message.str()); + } + } + } + std::ostringstream message; + message << "program 203 source reduction has no valid contract for observed gl_NumSubgroups=" + << reportedNumSubgroups << " (requires 2..32); native subgroup results were recorded"; + return Failure("source domain", message.str()); + } + + // 4. Program-203 source writer and first shared-memory handoff. + std::vector sourceWriter(reportedNumSubgroups, kNoSlot); + for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) { + std::size_t writerCount = 0; + for (const std::size_t slot : subgroupSlots[subgroupID]) { + const UVec4& subgroup = output.subgroup[slot]; + if (subgroup.w == subgroup.x - 1u) { + sourceWriter[subgroupID] = slot; + ++writerCount; + } + } + if (writerCount != 1u) { + std::ostringstream message; + message << "subgroupID " << subgroupID << " has " << writerCount + << " recorded lane(s) where laneID == subgroupSize - 1; program 203 leaves that " + "shared-cache entry unwritten"; + return Failure("source writer", message.str()); + } + for (const std::size_t slot : subgroupSlots[subgroupID]) { + const float actualRawSubtotal = output.reduction[slot].z; + if (!SameBits(actualRawSubtotal, nativeSubtotal[subgroupID])) { + std::ostringstream message; + message << "subgroupID " << subgroupID << ", localIndex " << output.invocation[slot].x + << " sourceRawSubtotal was " << FormatFloat(actualRawSubtotal) << ", expected " + << FormatFloat(nativeSubtotal[subgroupID]); + return Failure("source raw subtotal", message.str()); + } + } + } + + // 5. Reproduce the source loop exactly, including the redundant final + // scan iteration on power-of-two subgroup counts. Reads and writes in + // one iteration target disjoint cache entries, so update the cache at + // the CPU equivalent of the source barrier. + std::array mergedPrefix = nativePrefix; + std::vector cache = nativeSubtotal; + std::uint32_t loopLength = std::bit_width(reportedNumSubgroups) - 1u; + loopLength += + static_cast(reportedNumSubgroups - (1u << (loopLength - 1u)) > 0u); + for (std::uint32_t scanStage = 0u; scanStage < loopLength; ++scanStage) { + std::vector cacheAfterStage = cache; + for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) { + if ((subgroupID & (1u << scanStage)) == 0u) continue; + const std::uint32_t sourceCacheIndex = (subgroupID >> scanStage << scanStage) - 1u; + const float sourcePrefix = cache[sourceCacheIndex]; + for (const std::size_t slot : subgroupSlots[subgroupID]) { + mergedPrefix[slot] += sourcePrefix; + } + cacheAfterStage[subgroupID] = mergedPrefix[sourceWriter[subgroupID]]; + } + cache.swap(cacheAfterStage); + for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) { + const std::size_t slot = slotForLocal[localIndex]; + const float actualAfterStage = output.scanAfter[scanStage][slot]; + if (!SameBits(actualAfterStage, mergedPrefix[slot])) { + std::ostringstream message; + message << "scanStage " << scanStage << ", subgroupID " << output.subgroup[slot].z + << ", laneID " << output.subgroup[slot].w << ", localIndex " << localIndex + << " scanAfter was " << FormatFloat(actualAfterStage) << ", expected " + << FormatFloat(mergedPrefix[slot]); + ValidationResult result = Failure("source scan", message.str()); + result.scanStageMismatch = true; + result.scanStage = static_cast(scanStage); + return result; + } + } + } + for (std::size_t localIndex = 0; localIndex < kInvocationCount; ++localIndex) { + const std::size_t slot = slotForLocal[localIndex]; + const float actualMergedPrefix = output.reduction[slot].w; + if (!SameBits(actualMergedPrefix, mergedPrefix[slot])) { + std::ostringstream message; + message << "localIndex " << localIndex << " sourceMergedPrefix was " + << FormatFloat(actualMergedPrefix) << ", expected " << FormatFloat(mergedPrefix[slot]); + return Failure("source scan", message.str()); + } + } + + // 6. Final owner and average. The uniformity check is intentionally + // separate from the source's topology contract at local index 511. + const float firstAverage = output.finalAverage[slotForLocal[0]]; + for (std::size_t localIndex = 1; localIndex < kInvocationCount; ++localIndex) { + const float actualAverage = output.finalAverage[slotForLocal[localIndex]]; + if (!SameBits(actualAverage, firstAverage)) { + std::ostringstream message; + message << "finalAverage differs: localIndex 0 has " << FormatFloat(firstAverage) + << ", localIndex " << localIndex << " has " << FormatFloat(actualAverage); + return Failure("final average", message.str()); + } + } + + ValidationResult ownerResult; + ownerResult.ownerEvaluated = true; + for (std::uint32_t subgroupID = 0; subgroupID < reportedNumSubgroups; ++subgroupID) { + if (!subgroupSlots[subgroupID].empty()) { + ownerResult.highestObservedSubgroup = std::max(ownerResult.highestObservedSubgroup, subgroupID); + } + } + const std::size_t index511Slot = slotForLocal[kInvocationCount - 1u]; + const UVec4& index511Subgroup = output.subgroup[index511Slot]; + ownerResult.index511IsSourceLastLaneWriter = + index511Subgroup.w == index511Subgroup.x - 1u; + ownerResult.index511IsHighestSubgroupMember = + index511Subgroup.z == ownerResult.highestObservedSubgroup; + if (!ownerResult.index511IsSourceLastLaneWriter || !ownerResult.index511IsHighestSubgroupMember) { + std::ostringstream message; + message << "program 203 topology incompatibility: localIndex 511 is sourceLastLaneWriter=" + << ownerResult.index511IsSourceLastLaneWriter << ", highestSubgroupMember=" + << ownerResult.index511IsHighestSubgroupMember << " (subgroupID=" << index511Subgroup.z + << ", highest observed subgroupID=" << ownerResult.highestObservedSubgroup << ')'; + ownerResult.ok = false; + ownerResult.phase = "final average"; + ownerResult.message = message.str(); + return ownerResult; + } + + float total = 0.0f; + for (const float subtotal : nativeSubtotal) total += subtotal; + float sampledExpectedTotal = 0.0f; + for (std::size_t i = 0; i < kInvocationCount; ++i) sampledExpectedTotal += kSampledLuminance; + const float expectedTotal = mode == InputMode::IndexedSsbo ? 131328.0f : sampledExpectedTotal; + if (!SameBits(total, expectedTotal) || !SameBits(mergedPrefix[index511Slot], expectedTotal)) { + std::ostringstream message; + message << "program 203 source total was " << FormatFloat(mergedPrefix[index511Slot]) + << " (native total " << FormatFloat(total) << "), expected " << FormatFloat(expectedTotal); + ownerResult.ok = false; + ownerResult.phase = "final average"; + ownerResult.message = message.str(); + return ownerResult; + } + + const float expectedAverage = mode == InputMode::IndexedSsbo ? 256.5f : sampledExpectedTotal / 512.0f; + if (!SameBits(firstAverage, expectedAverage)) { + std::ostringstream message; + message << "finalAverage was " << FormatFloat(firstAverage) << ", expected " + << FormatFloat(expectedAverage); + ownerResult.ok = false; + ownerResult.phase = "final average"; + ownerResult.message = message.str(); + return ownerResult; + } + return ownerResult; + } + + void DumpProbe(const ProbeOutput& output, const CapabilityInfo& capabilities, const ValidationResult& validation, + bool includeScanStages) { + PrintMetadata(capabilities, std::cout); + if (validation.ok) { + std::cout << "Program203FirstReductionScenario firstFailure=none\n"; + } else { + std::cout << "Program203FirstReductionScenario firstFailure=" << validation.phase << ": " + << validation.message << '\n'; + } + std::cout << "localIndex,localX,localY,localZ,subgroupSize,numSubgroups,subgroupID,laneID,input," + "nativeInclusive,subgroupSubtotal,mergedPrefix,finalAverage\n"; + for (std::size_t slot = 0; slot < kInvocationCount; ++slot) { + const UVec4& invocation = output.invocation[slot]; + const UVec4& subgroup = output.subgroup[slot]; + const Vec4& reduction = output.reduction[slot]; + std::cout << invocation.x << ',' << invocation.y << ',' << invocation.z << ',' << invocation.w << ',' + << subgroup.x << ',' << subgroup.y << ',' << subgroup.z << ',' << subgroup.w << ',' + << std::hexfloat << reduction.x << ',' << reduction.y << ',' << reduction.z << ',' + << reduction.w << ',' << output.finalAverage[slot] << std::defaultfloat << '\n'; + } + if (includeScanStages) { + std::cout << "scanStage,localIndex,scanAfter\n"; + for (std::size_t scanStage = 0; scanStage < kScanStageCount; ++scanStage) { + for (std::size_t slot = 0; slot < kInvocationCount; ++slot) { + std::cout << scanStage << ',' << output.invocation[slot].x << ',' << std::hexfloat + << output.scanAfter[scanStage][slot] << std::defaultfloat << '\n'; + } + } + } + } + + class Program203FirstReductionScenario : public ScenarioTest { + protected: + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + + m_capabilities = QueryCapabilities(); + // GL_SUBGROUP_SIZE_KHR is diagnostic only. It is deliberately + // never used to infer lane placement or an expected group count. + PrintMetadata(m_capabilities, std::cout); + RecordProperty("program203_gl_subgroup_size_khr", std::to_string(m_capabilities.subgroupSize)); + if (!m_capabilities.SupportsProbe()) { + GTEST_SKIP() << "subgroup probe requires " << m_capabilities.MissingRequirements(); + } + } + + void TearDown() override { + if (!Ready()) return; + glUseProgram(0); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + glActiveTexture(GL_TEXTURE3); + glBindTexture(GL_TEXTURE_2D, 0); + glActiveTexture(GL_TEXTURE0); + if (m_texture != 0) glDeleteTextures(1, &m_texture); + if (m_inputBuffer != 0) glDeleteBuffers(1, &m_inputBuffer); + if (m_outputBuffer != 0) glDeleteBuffers(1, &m_outputBuffer); + if (m_program != 0) glDeleteProgram(m_program); + m_texture = 0; + m_inputBuffer = 0; + m_outputBuffer = 0; + m_program = 0; + } + + GLuint CompileComputeProgram(const std::string& source, std::string* outError) { + const char* text = source.c_str(); + const GLuint shader = glCreateShader(GL_COMPUTE_SHADER); + if (shader == 0) { + *outError = "glCreateShader(GL_COMPUTE_SHADER) returned 0"; + return 0; + } + glShaderSource(shader, 1, &text, nullptr); + glCompileShader(shader); + GLint compiled = GL_FALSE; + glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + if (compiled == GL_FALSE) { + char log[8192] = {}; + glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log); + *outError = std::string("the subgroup probe compute shader did not compile: ") + log; + glDeleteShader(shader); + return 0; + } + const GLuint program = glCreateProgram(); + glAttachShader(program, shader); + glLinkProgram(program); + glDeleteShader(shader); + GLint linked = GL_FALSE; + glGetProgramiv(program, GL_LINK_STATUS, &linked); + if (linked == GL_FALSE) { + char log[8192] = {}; + glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log); + *outError = std::string("the subgroup probe compute program did not link: ") + log; + glDeleteProgram(program); + return 0; + } + return program; + } + + bool RunProbe(InputMode mode, ProbeOutput* output, std::string* outError) { + m_program = CompileComputeProgram(BuildProbeShader(mode), outError); + if (m_program == 0) return false; + + ProbeOutput poison{}; + std::memset(&poison, 0xa5, sizeof(poison)); + glGenBuffers(1, &m_outputBuffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_outputBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(ProbeOutput), &poison, GL_DYNAMIC_COPY); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_outputBuffer); + + if (mode == InputMode::IndexedSsbo) { + std::array values{}; + for (std::size_t i = 0; i < values.size(); ++i) values[i] = static_cast(i + 1u); + glGenBuffers(1, &m_inputBuffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_inputBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(values), values.data(), GL_STATIC_DRAW); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_inputBuffer); + } else { + constexpr std::array kOneTexel = {1.0f, 1.0f, 1.0f, 1.0f}; + glGenTextures(1, &m_texture); + glActiveTexture(GL_TEXTURE3); + glBindTexture(GL_TEXTURE_2D, m_texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, 1, 1, 0, GL_RGBA, GL_FLOAT, kOneTexel.data()); + } + + if (const GLenum error = FirstGLError(); error != GL_NO_ERROR) { + std::ostringstream message; + message << "subgroup probe resource setup left " << GLErrorName(error); + *outError = message.str(); + return false; + } + + glUseProgram(m_program); + if (mode == InputMode::SampledRgba32f) { + const GLint sampler = glGetUniformLocation(m_program, "colortex2"); + const GLint pixelSize = glGetUniformLocation(m_program, "pixelSize"); + if (sampler == -1 || pixelSize == -1) { + *outError = "the sampled probe uniforms were optimized away or not reflected"; + return false; + } + glUniform1i(sampler, 3); + glUniform2f(pixelSize, 1.0f / 854.0f, 1.0f / 480.0f); + } + glDispatchCompute(1, 1, 1); + glMemoryBarrier(GL_ALL_BARRIER_BITS); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_outputBuffer); + glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(ProbeOutput), output); + if (const GLenum error = FirstGLError(); error != GL_NO_ERROR) { + std::ostringstream message; + message << "subgroup probe dispatch/readback left " << GLErrorName(error); + *outError = message.str(); + return false; + } + return true; + } + + void RunAndValidate(InputMode mode) { + ProbeOutput output{}; + std::string error; + ASSERT_TRUE(RunProbe(mode, &output, &error)) << InputModeName(mode) << ": " << error; + + const ValidationResult validation = ValidateProbe(output, mode); + if (validation.ownerEvaluated) { + RecordProperty("program203_index511_source_last_lane_writer", + validation.index511IsSourceLastLaneWriter ? "true" : "false"); + RecordProperty("program203_index511_highest_subgroup_member", + validation.index511IsHighestSubgroupMember ? "true" : "false"); + RecordProperty("program203_highest_observed_subgroup", + std::to_string(validation.highestObservedSubgroup)); + std::cout << "Program203FirstReductionScenario owner: localIndex511 sourceLastLaneWriter=" + << validation.index511IsSourceLastLaneWriter << ", highestSubgroupMember=" + << validation.index511IsHighestSubgroupMember << ", highestObservedSubgroup=" + << validation.highestObservedSubgroup << '\n'; + } + if (!validation.ok || DumpRequested()) { + DumpProbe(output, m_capabilities, validation, validation.scanStageMismatch || DumpRequested()); + } + EXPECT_TRUE(validation.ok) << validation.phase << ": " << validation.message; + } + + CapabilityInfo m_capabilities; + GLuint m_program = 0; + GLuint m_inputBuffer = 0; + GLuint m_outputBuffer = 0; + GLuint m_texture = 0; + }; + + } // namespace + + TEST_F(Program203FirstReductionScenario, SampledRgba32fFirstAverage) { + if (!Ready() || IsSkipped()) return; + RunAndValidate(InputMode::SampledRgba32f); + } + + TEST_F(Program203FirstReductionScenario, IndexedInputTopologyAndReduction) { + if (!Ready() || IsSkipped()) return; + RunAndValidate(InputMode::IndexedSsbo); + } + +} // namespace MGITest diff --git a/MobileGL/MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp index 11b79957..c53c3105 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp @@ -428,15 +428,15 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); } std::vector pixels(static_cast(kWidth) * kHeight, 0.0f); glReadPixels(0, 0, kWidth, kHeight, GL_RED, GL_FLOAT, pixels.data()); for (int i = 0; i < kViewportCount; ++i) { - const float near = static_cast(i) / 16.0f; - const float far = 1.0f - static_cast(i) / 16.0f; + const float nearDepth = static_cast(i) / 16.0f; + const float farDepth = 1.0f - static_cast(i) / 16.0f; // The tolerance covers depth-buffer-free rasterization of gl_FragCoord.z on a // software rasterizer; the per-index values are 1/16 apart, so it cannot let a // neighbouring viewport's range through, and viewport 0's range (0, 1) differs // from every other index by at least 1/16. - EXPECT_NEAR(pixels[i], near, 1.0e-3f) + EXPECT_NEAR(pixels[i], nearDepth, 1.0e-3f) << "viewport " << i << " near-plane depth; got viewport 0's range if this is 0"; - EXPECT_NEAR(pixels[static_cast(kWidth) + i], far, 1.0e-3f) + EXPECT_NEAR(pixels[static_cast(kWidth) + i], farDepth, 1.0e-3f) << "viewport " << i << " far-plane depth; got viewport 0's range if this is 1"; } diff --git a/MobileGL/MG_Test/CMakeLists.txt b/MobileGL/MG_Test/CMakeLists.txt index 7f513507..a981087d 100644 --- a/MobileGL/MG_Test/CMakeLists.txt +++ b/MobileGL/MG_Test/CMakeLists.txt @@ -78,6 +78,7 @@ add_subdirectory(Query) add_subdirectory(Pipeline) add_subdirectory(ShaderTranspiler) add_subdirectory(Util) +add_subdirectory(SelfTest) # The DirectGLES post-transpile ESSL passes are pure String -> String, so unlike the # DirectVulkan suite below this one needs no device and always builds. add_subdirectory(Backend/DirectGLES) diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index dd9bee54..a426f7fc 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -2387,3 +2387,13 @@ TEST(DirectGLESTextureSync, UnitMemoRefusesToDriveATwinFromAnotherTexture) { MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); } + +TEST(DirectVulkanSanity, GraphicsSamplerFeedbackOnlyAliasesWritableOverlappingMip) { + using MobileGL::MG_Backend::DirectVulkan::UniformManager; + + EXPECT_TRUE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 2, GL_WRITE_ONLY)); + EXPECT_TRUE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 3, GL_READ_WRITE)); + EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 2, GL_READ_ONLY)); + EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 0, GL_WRITE_ONLY)); + EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 4, GL_WRITE_ONLY)); +} diff --git a/MobileGL/MG_Test/SelfTest/CMakeLists.txt b/MobileGL/MG_Test/SelfTest/CMakeLists.txt new file mode 100644 index 00000000..9beb3e27 --- /dev/null +++ b/MobileGL/MG_Test/SelfTest/CMakeLists.txt @@ -0,0 +1,19 @@ +# MobileGL - MobileGL/MG_Test/SelfTest/CMakeLists.txt + +add_executable( + DriverPostProgram203WitnessTest + DriverPostProgram203WitnessTest.cpp +) + +target_include_directories(DriverPostProgram203WitnessTest PRIVATE + ${MGL_ROOT}/include + ${MGL_ROOT}/MobileGL +) + +target_link_libraries(DriverPostProgram203WitnessTest PRIVATE + GTest::gtest_main + ${LINK_LIBRARIES} +) + +include(GoogleTest) +gtest_discover_tests(DriverPostProgram203WitnessTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) diff --git a/MobileGL/MG_Test/SelfTest/DriverPostProgram203WitnessTest.cpp b/MobileGL/MG_Test/SelfTest/DriverPostProgram203WitnessTest.cpp new file mode 100644 index 00000000..888228a1 --- /dev/null +++ b/MobileGL/MG_Test/SelfTest/DriverPostProgram203WitnessTest.cpp @@ -0,0 +1,188 @@ +// MobileGL - MobileGL/MG_Test/SelfTest/DriverPostProgram203WitnessTest.cpp +// Copyright (c) 2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include + +#include + +#include "MG_Util/SelfTest/DriverPostProgram203Witness.h" + +namespace MobileGL::MG_Util::SelfTest { + namespace { + Program203WitnessOutput MakeValidWitness(std::uint32_t numSubgroups) { + Program203WitnessOutput output{}; + output.magic = kProgram203WitnessMagic; + output.numSubgroups = numSubgroups; + output.loopLength = ComputeProgram203WitnessLoopLength(numSubgroups); + output.seenSubgroupMask = + numSubgroups == kProgram203WitnessMaxSubgroups ? 0xffffffffu : (1u << numSubgroups) - 1u; + + // Valid test layouts use equal contiguous groups of the indexed + // 1..512 input. The compact witness only needs their independent sums. + const std::uint32_t subgroupSize = kProgram203WitnessInvocationCount / numSubgroups; + for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) { + const std::uint32_t first = subgroup * subgroupSize + 1u; + const std::uint32_t last = first + subgroupSize - 1u; + output.lastLaneWriterCount[subgroup] = 1u; + output.indexedInputTotal[subgroup] = subgroupSize * (first + last) / 2u; + output.rawPrefix[subgroup] = {static_cast(output.indexedInputTotal[subgroup]), 0.0f}; + } + output.owner511 = {subgroupSize, numSubgroups, numSubgroups - 1u, subgroupSize - 1u}; + + auto cache = output.rawPrefix; + for (std::uint32_t scanStage = 0u; scanStage < output.loopLength; ++scanStage) { + auto cacheAfterStage = cache; + for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) { + if ((subgroup & (1u << scanStage)) == 0u) continue; + const std::uint32_t sourceCacheIndex = (subgroup >> scanStage << scanStage) - 1u; + cacheAfterStage[subgroup].x += cache[sourceCacheIndex].x; + cacheAfterStage[subgroup].y += cache[sourceCacheIndex].y; + } + cache = cacheAfterStage; + output.scanCache[scanStage] = cache; + } + output.finalAverage = {256.5f, 0.0f}; + return output; + } + + Program203WitnessLimits MakeSufficientLimits() { + Program203WitnessLimits limits; + limits.computeStageSupported = true; + limits.basicSubgroupSupported = true; + limits.arithmeticSubgroupSupported = true; + limits.subgroupSize = 32u; + limits.maxComputeWorkGroupInvocations = kProgram203WitnessInvocationCount; + limits.maxComputeWorkGroupSize = {32u, 16u, 1u}; + limits.maxComputeSharedMemorySize = kProgram203WitnessSharedMemoryBytes; + limits.maxPerStageDescriptorStorageBuffers = 1u; + limits.maxDescriptorSetStorageBuffers = 1u; + limits.maxBoundDescriptorSets = 1u; + limits.maxStorageBufferRange = sizeof(Program203WitnessOutput); + return limits; + } + } // namespace + + TEST(DriverPostProgram203WitnessTest, ValidTwoSubgroupWitness) { + const Program203WitnessValidationResult validation = ValidateProgram203Witness(MakeValidWitness(2u)); + ASSERT_TRUE(validation.ok) << validation.detail; + EXPECT_EQ(validation.detail, "N=2, owner511=id1/lane255, 2 scan stages, average=(256.5,0)"); + } + + TEST(DriverPostProgram203WitnessTest, ValidThirtyTwoSubgroupWitness) { + const Program203WitnessValidationResult validation = ValidateProgram203Witness(MakeValidWitness(32u)); + ASSERT_TRUE(validation.ok) << validation.detail; + EXPECT_EQ(validation.detail, "N=32, owner511=id31/lane15, 6 scan stages, average=(256.5,0)"); + } + + TEST(DriverPostProgram203WitnessTest, RejectsNonuniformNumSubgroups) { + Program203WitnessOutput output = MakeValidWitness(16u); + output.topologyFlags |= Program203WitnessNonuniformNumSubgroups; + const Program203WitnessValidationResult validation = ValidateProgram203Witness(output); + EXPECT_FALSE(validation.ok); + EXPECT_EQ(validation.failure, Program203WitnessValidationFailure::Topology); + EXPECT_NE(validation.detail.find("gl_NumSubgroups differed"), std::string::npos); + } + + TEST(DriverPostProgram203WitnessTest, RejectsMissingAndOutOfRangeSubgroupIds) { + Program203WitnessOutput missing = MakeValidWitness(16u); + missing.seenSubgroupMask &= ~(1u << 7u); + Program203WitnessValidationResult validation = ValidateProgram203Witness(missing); + EXPECT_FALSE(validation.ok); + EXPECT_NE(validation.detail.find("seen subgroup-ID mask"), std::string::npos); + + Program203WitnessOutput outOfRange = MakeValidWitness(16u); + outOfRange.topologyFlags |= Program203WitnessInvalidSubgroupId; + validation = ValidateProgram203Witness(outOfRange); + EXPECT_FALSE(validation.ok); + EXPECT_NE(validation.detail.find("invalid gl_SubgroupID"), std::string::npos); + } + + TEST(DriverPostProgram203WitnessTest, RejectsInvalidMultipleAndMissingLastLaneWriters) { + Program203WitnessOutput invalidLane = MakeValidWitness(16u); + invalidLane.topologyFlags |= Program203WitnessInvalidSubgroupLane; + Program203WitnessValidationResult validation = ValidateProgram203Witness(invalidLane); + EXPECT_FALSE(validation.ok); + EXPECT_NE(validation.detail.find("invalid subgroup lane"), std::string::npos); + + Program203WitnessOutput multiple = MakeValidWitness(16u); + multiple.lastLaneWriterCount[4] = 2u; + validation = ValidateProgram203Witness(multiple); + EXPECT_FALSE(validation.ok); + EXPECT_NE(validation.detail.find("subgroup 4 has 2 source last-lane writers"), std::string::npos); + + Program203WitnessOutput missing = MakeValidWitness(16u); + missing.lastLaneWriterCount[6] = 0u; + validation = ValidateProgram203Witness(missing); + EXPECT_FALSE(validation.ok); + EXPECT_NE(validation.detail.find("subgroup 6 has 0 source last-lane writers"), std::string::npos); + } + + TEST(DriverPostProgram203WitnessTest, ReportsEarliestCorruptSourceScanStage) { + Program203WitnessOutput output = MakeValidWitness(32u); + output.scanCache[0][1].x += 1.0f; + output.scanCache[3][5].x += 1.0f; + Program203WitnessValidationResult validation = ValidateProgram203Witness(output); + EXPECT_FALSE(validation.ok); + EXPECT_EQ(validation.failure, Program203WitnessValidationFailure::SourceScan); + EXPECT_EQ(validation.scanStage, 0u); + EXPECT_NE(validation.detail.find("source scan stage 0, subgroup 1"), std::string::npos); + + output = MakeValidWitness(32u); + output.scanCache[3][5].x += 1.0f; + validation = ValidateProgram203Witness(output); + EXPECT_FALSE(validation.ok); + EXPECT_EQ(validation.failure, Program203WitnessValidationFailure::SourceScan); + EXPECT_EQ(validation.scanStage, 3u); + EXPECT_NE(validation.detail.find("source scan stage 3, subgroup 5"), std::string::npos); + } + + TEST(DriverPostProgram203WitnessTest, RejectsOwner511OutsideHighestFinalLane) { + Program203WitnessOutput output = MakeValidWitness(16u); + output.owner511.z = 14u; + const Program203WitnessValidationResult validation = ValidateProgram203Witness(output); + EXPECT_FALSE(validation.ok); + EXPECT_EQ(validation.failure, Program203WitnessValidationFailure::FinalOwner); + EXPECT_NE(validation.detail.find("not in the highest subgroup"), std::string::npos); + } + + TEST(DriverPostProgram203WitnessTest, RejectsIncorrectVectorFinalAverage) { + Program203WitnessOutput output = MakeValidWitness(16u); + output.finalAverage.y = 1.0f; + const Program203WitnessValidationResult validation = ValidateProgram203Witness(output); + EXPECT_FALSE(validation.ok); + EXPECT_EQ(validation.failure, Program203WitnessValidationFailure::FinalAverage); + EXPECT_NE(validation.detail.find("final average"), std::string::npos); + } + + TEST(DriverPostProgram203WitnessTest, MissingNativeFeatureIsTheOnlySkipCondition) { + for (const auto toggleMissingFeature : {0u, 1u, 2u}) { + Program203WitnessLimits limits = MakeSufficientLimits(); + if (toggleMissingFeature == 0u) limits.computeStageSupported = false; + if (toggleMissingFeature == 1u) limits.basicSubgroupSupported = false; + if (toggleMissingFeature == 2u) limits.arithmeticSubgroupSupported = false; + const Program203WitnessEligibilityResult eligibility = EvaluateProgram203WitnessEligibility(limits); + EXPECT_EQ(eligibility.eligibility, Program203WitnessEligibility::SkipUnsupportedNativeFeatureSet) + << eligibility.detail; + } + + Program203WitnessLimits zeroSubgroupSize = MakeSufficientLimits(); + zeroSubgroupSize.subgroupSize = 0u; + Program203WitnessEligibilityResult eligibility = EvaluateProgram203WitnessEligibility(zeroSubgroupSize); + EXPECT_EQ(eligibility.eligibility, Program203WitnessEligibility::FailInadequateLimits) << eligibility.detail; + + Program203WitnessLimits limits = MakeSufficientLimits(); + limits.maxComputeWorkGroupInvocations = 511u; + eligibility = EvaluateProgram203WitnessEligibility(limits); + EXPECT_EQ(eligibility.eligibility, Program203WitnessEligibility::FailInadequateLimits) << eligibility.detail; + + limits = MakeSufficientLimits(); + limits.maxStorageBufferRange = sizeof(Program203WitnessOutput) - 1u; + eligibility = EvaluateProgram203WitnessEligibility(limits); + EXPECT_EQ(eligibility.eligibility, Program203WitnessEligibility::FailInadequateLimits) << eligibility.detail; + } +} // namespace MobileGL::MG_Util::SelfTest diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index 4aeeb2c0..8b1070ed 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -7,6 +7,8 @@ // End of Source File Header #include "DriverPost.h" +#include "DriverPostProgram203Witness.h" +#include "DriverPostProgram203WitnessSpv.h" #include "MG_Util/BackendLoaders/OpenGL/Loader.h" #include #include @@ -24,6 +26,8 @@ #include #include #include +#include +#include #include #if !defined(_WIN32) @@ -1454,6 +1458,437 @@ namespace MobileGL::MG_Util::SelfTest { disabledNote); } + // Native Program-203 compute witness. This deliberately uses a separate + // throwaway Vulkan device rather than the real renderer's queues, and it + // treats MOBILEGL_DISABLE_SUBGROUP as irrelevant: the row reports what the + // driver does, not what MobileGL elects to advertise to applications. + void ProbeVulkanProgram203Witness(ReportBuilder& builder, PFN_vkGetInstanceProcAddr getInstanceProcAddr, + VkInstance instance, VkPhysicalDevice physicalDevice, + Uint32 computeQueueFamilyIndex, + const VkPhysicalDeviceProperties& properties, + Bool subgroupPropertiesAvailable, + const VkPhysicalDeviceSubgroupProperties& subgroupProperties) { + constexpr const char* RowName = "Subgroup first-reduction witness"; + const auto fail = [&](String detail) { builder.Fail(RowName, Move(detail)); }; + + if (!subgroupPropertiesAvailable) { + fail("vkGetPhysicalDeviceProperties2 could not provide raw Vulkan subgroup properties"); + return; + } + + Program203WitnessLimits limits{}; + limits.computeStageSupported = + (subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0; + limits.basicSubgroupSupported = + (subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0; + limits.arithmeticSubgroupSupported = + (subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_ARITHMETIC_BIT) != 0; + limits.subgroupSize = subgroupProperties.subgroupSize; + limits.maxComputeWorkGroupInvocations = properties.limits.maxComputeWorkGroupInvocations; + limits.maxComputeWorkGroupSize = {properties.limits.maxComputeWorkGroupSize[0], + properties.limits.maxComputeWorkGroupSize[1], + properties.limits.maxComputeWorkGroupSize[2]}; + limits.maxComputeSharedMemorySize = properties.limits.maxComputeSharedMemorySize; + limits.maxPerStageDescriptorStorageBuffers = properties.limits.maxPerStageDescriptorStorageBuffers; + limits.maxDescriptorSetStorageBuffers = properties.limits.maxDescriptorSetStorageBuffers; + limits.maxBoundDescriptorSets = properties.limits.maxBoundDescriptorSets; + limits.maxStorageBufferRange = properties.limits.maxStorageBufferRange; + + const Program203WitnessEligibilityResult eligibility = EvaluateProgram203WitnessEligibility(limits); + if (eligibility.eligibility == Program203WitnessEligibility::SkipUnsupportedNativeFeatureSet) { + builder.Info(RowName, eligibility.detail); + return; + } + if (eligibility.eligibility == Program203WitnessEligibility::FailInadequateLimits) { + fail(eligibility.detail); + return; + } + if (computeQueueFamilyIndex == std::numeric_limits::max()) { + fail("no compute queue family is available for the native Vulkan witness"); + return; + } + + const auto vkGetPhysicalDeviceMemoryPropertiesFn = + reinterpret_cast( + getInstanceProcAddr(instance, "vkGetPhysicalDeviceMemoryProperties")); + const auto vkCreateDeviceFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkCreateDevice")); + const auto vkDestroyDeviceFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkDestroyDevice")); + const auto vkGetDeviceQueueFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkGetDeviceQueue")); + const auto vkCreateBufferFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkCreateBuffer")); + const auto vkDestroyBufferFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkDestroyBuffer")); + const auto vkGetBufferMemoryRequirementsFn = reinterpret_cast( + getInstanceProcAddr(instance, "vkGetBufferMemoryRequirements")); + const auto vkAllocateMemoryFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkAllocateMemory")); + const auto vkFreeMemoryFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkFreeMemory")); + const auto vkBindBufferMemoryFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkBindBufferMemory")); + const auto vkMapMemoryFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkMapMemory")); + const auto vkUnmapMemoryFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkUnmapMemory")); + const auto vkCreateDescriptorSetLayoutFn = reinterpret_cast( + getInstanceProcAddr(instance, "vkCreateDescriptorSetLayout")); + const auto vkDestroyDescriptorSetLayoutFn = reinterpret_cast( + getInstanceProcAddr(instance, "vkDestroyDescriptorSetLayout")); + const auto vkCreateDescriptorPoolFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkCreateDescriptorPool")); + const auto vkDestroyDescriptorPoolFn = reinterpret_cast( + getInstanceProcAddr(instance, "vkDestroyDescriptorPool")); + const auto vkAllocateDescriptorSetsFn = reinterpret_cast( + getInstanceProcAddr(instance, "vkAllocateDescriptorSets")); + const auto vkUpdateDescriptorSetsFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkUpdateDescriptorSets")); + const auto vkCreateShaderModuleFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkCreateShaderModule")); + const auto vkDestroyShaderModuleFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkDestroyShaderModule")); + const auto vkCreatePipelineLayoutFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkCreatePipelineLayout")); + const auto vkDestroyPipelineLayoutFn = reinterpret_cast( + getInstanceProcAddr(instance, "vkDestroyPipelineLayout")); + const auto vkCreateComputePipelinesFn = reinterpret_cast( + getInstanceProcAddr(instance, "vkCreateComputePipelines")); + const auto vkDestroyPipelineFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkDestroyPipeline")); + const auto vkCreateCommandPoolFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkCreateCommandPool")); + const auto vkDestroyCommandPoolFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkDestroyCommandPool")); + const auto vkAllocateCommandBuffersFn = reinterpret_cast( + getInstanceProcAddr(instance, "vkAllocateCommandBuffers")); + const auto vkBeginCommandBufferFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkBeginCommandBuffer")); + const auto vkEndCommandBufferFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkEndCommandBuffer")); + const auto vkCmdBindPipelineFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkCmdBindPipeline")); + const auto vkCmdBindDescriptorSetsFn = reinterpret_cast( + getInstanceProcAddr(instance, "vkCmdBindDescriptorSets")); + const auto vkCmdDispatchFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkCmdDispatch")); + const auto vkCmdPipelineBarrierFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkCmdPipelineBarrier")); + const auto vkCreateFenceFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkCreateFence")); + const auto vkDestroyFenceFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkDestroyFence")); + const auto vkQueueSubmitFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkQueueSubmit")); + const auto vkWaitForFencesFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkWaitForFences")); + const auto vkDeviceWaitIdleFn = + reinterpret_cast(getInstanceProcAddr(instance, "vkDeviceWaitIdle")); + + if (vkGetPhysicalDeviceMemoryPropertiesFn == nullptr || vkCreateDeviceFn == nullptr || + vkDestroyDeviceFn == nullptr || vkGetDeviceQueueFn == nullptr || vkCreateBufferFn == nullptr || + vkDestroyBufferFn == nullptr || vkGetBufferMemoryRequirementsFn == nullptr || + vkAllocateMemoryFn == nullptr || vkFreeMemoryFn == nullptr || vkBindBufferMemoryFn == nullptr || + vkMapMemoryFn == nullptr || vkUnmapMemoryFn == nullptr || vkCreateDescriptorSetLayoutFn == nullptr || + vkDestroyDescriptorSetLayoutFn == nullptr || vkCreateDescriptorPoolFn == nullptr || + vkDestroyDescriptorPoolFn == nullptr || vkAllocateDescriptorSetsFn == nullptr || + vkUpdateDescriptorSetsFn == nullptr || vkCreateShaderModuleFn == nullptr || + vkDestroyShaderModuleFn == nullptr || vkCreatePipelineLayoutFn == nullptr || + vkDestroyPipelineLayoutFn == nullptr || vkCreateComputePipelinesFn == nullptr || + vkDestroyPipelineFn == nullptr || vkCreateCommandPoolFn == nullptr || vkDestroyCommandPoolFn == nullptr || + vkAllocateCommandBuffersFn == nullptr || vkBeginCommandBufferFn == nullptr || + vkEndCommandBufferFn == nullptr || vkCmdBindPipelineFn == nullptr || + vkCmdBindDescriptorSetsFn == nullptr || vkCmdDispatchFn == nullptr || + vkCmdPipelineBarrierFn == nullptr || vkCreateFenceFn == nullptr || vkDestroyFenceFn == nullptr || + vkQueueSubmitFn == nullptr || vkWaitForFencesFn == nullptr || vkDeviceWaitIdleFn == nullptr) { + fail("vkGetInstanceProcAddr could not resolve the Vulkan entry points required for the witness"); + return; + } + + const Float queuePriority = 1.0f; + VkDeviceQueueCreateInfo queueInfo{}; + queueInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queueInfo.queueFamilyIndex = computeQueueFamilyIndex; + queueInfo.queueCount = 1; + queueInfo.pQueuePriorities = &queuePriority; + + VkDeviceCreateInfo deviceInfo{}; + deviceInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + deviceInfo.queueCreateInfoCount = 1; + deviceInfo.pQueueCreateInfos = &queueInfo; + + VkDevice device = VK_NULL_HANDLE; + VkResult result = vkCreateDeviceFn(physicalDevice, &deviceInfo, nullptr, &device); + if (result != VK_SUCCESS || device == VK_NULL_HANDLE) { + fail(format("vkCreateDevice failed (VkResult = {})", static_cast(result))); + return; + } + + VkBuffer outputBuffer = VK_NULL_HANDLE; + VkDeviceMemory outputMemory = VK_NULL_HANDLE; + VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE; + VkDescriptorPool descriptorPool = VK_NULL_HANDLE; + VkShaderModule shaderModule = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; + VkCommandPool commandPool = VK_NULL_HANDLE; + VkFence fence = VK_NULL_HANDLE; + void* mappedOutput = nullptr; + Bool fenceWaitTimedOut = false; + const ScopeGuard destroyDeviceObjects([&]() { + if (fenceWaitTimedOut) { + // Match ProbeVulkanTimerQuery: the command may still execute + // after a timeout, so intentionally retain every device-owned + // resource rather than risking a forever wait or UAF in the ICD. + return; + } + vkDeviceWaitIdleFn(device); + if (fence != VK_NULL_HANDLE) vkDestroyFenceFn(device, fence, nullptr); + if (commandPool != VK_NULL_HANDLE) vkDestroyCommandPoolFn(device, commandPool, nullptr); + if (pipeline != VK_NULL_HANDLE) vkDestroyPipelineFn(device, pipeline, nullptr); + if (pipelineLayout != VK_NULL_HANDLE) vkDestroyPipelineLayoutFn(device, pipelineLayout, nullptr); + if (shaderModule != VK_NULL_HANDLE) vkDestroyShaderModuleFn(device, shaderModule, nullptr); + if (descriptorPool != VK_NULL_HANDLE) vkDestroyDescriptorPoolFn(device, descriptorPool, nullptr); + if (descriptorSetLayout != VK_NULL_HANDLE) { + vkDestroyDescriptorSetLayoutFn(device, descriptorSetLayout, nullptr); + } + if (mappedOutput != nullptr) vkUnmapMemoryFn(device, outputMemory); + if (outputBuffer != VK_NULL_HANDLE) vkDestroyBufferFn(device, outputBuffer, nullptr); + if (outputMemory != VK_NULL_HANDLE) vkFreeMemoryFn(device, outputMemory, nullptr); + vkDestroyDeviceFn(device, nullptr); + }); + + VkQueue queue = VK_NULL_HANDLE; + vkGetDeviceQueueFn(device, computeQueueFamilyIndex, 0, &queue); + if (queue == VK_NULL_HANDLE) { + fail("vkGetDeviceQueue returned a null compute queue"); + return; + } + + VkBufferCreateInfo bufferInfo{}; + bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufferInfo.size = sizeof(Program203WitnessOutput); + bufferInfo.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + result = vkCreateBufferFn(device, &bufferInfo, nullptr, &outputBuffer); + if (result != VK_SUCCESS) { + fail(format("vkCreateBuffer(output SSBO) failed (VkResult = {})", static_cast(result))); + return; + } + + VkMemoryRequirements memoryRequirements{}; + vkGetBufferMemoryRequirementsFn(device, outputBuffer, &memoryRequirements); + VkPhysicalDeviceMemoryProperties memoryProperties{}; + vkGetPhysicalDeviceMemoryPropertiesFn(physicalDevice, &memoryProperties); + Uint32 memoryTypeIndex = std::numeric_limits::max(); + for (Uint32 index = 0; index < memoryProperties.memoryTypeCount; ++index) { + const Bool compatible = (memoryRequirements.memoryTypeBits & (1u << index)) != 0u; + const VkMemoryPropertyFlags required = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + if (compatible && (memoryProperties.memoryTypes[index].propertyFlags & required) == required) { + memoryTypeIndex = index; + break; + } + } + if (memoryTypeIndex == std::numeric_limits::max()) { + fail("no host-visible/coherent memory type is compatible with the output SSBO"); + return; + } + + VkMemoryAllocateInfo memoryInfo{}; + memoryInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + memoryInfo.allocationSize = memoryRequirements.size; + memoryInfo.memoryTypeIndex = memoryTypeIndex; + result = vkAllocateMemoryFn(device, &memoryInfo, nullptr, &outputMemory); + if (result != VK_SUCCESS) { + fail(format("vkAllocateMemory(output SSBO) failed (VkResult = {})", static_cast(result))); + return; + } + result = vkBindBufferMemoryFn(device, outputBuffer, outputMemory, 0); + if (result != VK_SUCCESS) { + fail(format("vkBindBufferMemory(output SSBO) failed (VkResult = {})", static_cast(result))); + return; + } + result = vkMapMemoryFn(device, outputMemory, 0, sizeof(Program203WitnessOutput), 0, &mappedOutput); + if (result != VK_SUCCESS || mappedOutput == nullptr) { + fail(format("vkMapMemory(output SSBO) failed (VkResult = {})", static_cast(result))); + return; + } + std::memset(mappedOutput, 0xa5, sizeof(Program203WitnessOutput)); + + VkDescriptorSetLayoutBinding outputBinding{}; + outputBinding.binding = 0; + outputBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + outputBinding.descriptorCount = 1; + outputBinding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + VkDescriptorSetLayoutCreateInfo descriptorSetLayoutInfo{}; + descriptorSetLayoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + descriptorSetLayoutInfo.bindingCount = 1; + descriptorSetLayoutInfo.pBindings = &outputBinding; + result = vkCreateDescriptorSetLayoutFn(device, &descriptorSetLayoutInfo, nullptr, &descriptorSetLayout); + if (result != VK_SUCCESS) { + fail(format("vkCreateDescriptorSetLayout failed (VkResult = {})", static_cast(result))); + return; + } + + VkDescriptorPoolSize poolSize{}; + poolSize.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + poolSize.descriptorCount = 1; + VkDescriptorPoolCreateInfo descriptorPoolInfo{}; + descriptorPoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + descriptorPoolInfo.maxSets = 1; + descriptorPoolInfo.poolSizeCount = 1; + descriptorPoolInfo.pPoolSizes = &poolSize; + result = vkCreateDescriptorPoolFn(device, &descriptorPoolInfo, nullptr, &descriptorPool); + if (result != VK_SUCCESS) { + fail(format("vkCreateDescriptorPool failed (VkResult = {})", static_cast(result))); + return; + } + + VkDescriptorSet descriptorSet = VK_NULL_HANDLE; + VkDescriptorSetAllocateInfo descriptorSetInfo{}; + descriptorSetInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + descriptorSetInfo.descriptorPool = descriptorPool; + descriptorSetInfo.descriptorSetCount = 1; + descriptorSetInfo.pSetLayouts = &descriptorSetLayout; + result = vkAllocateDescriptorSetsFn(device, &descriptorSetInfo, &descriptorSet); + if (result != VK_SUCCESS) { + fail(format("vkAllocateDescriptorSets failed (VkResult = {})", static_cast(result))); + return; + } + VkDescriptorBufferInfo outputDescriptor{}; + outputDescriptor.buffer = outputBuffer; + outputDescriptor.offset = 0; + outputDescriptor.range = sizeof(Program203WitnessOutput); + VkWriteDescriptorSet descriptorWrite{}; + descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrite.dstSet = descriptorSet; + descriptorWrite.dstBinding = 0; + descriptorWrite.descriptorCount = 1; + descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrite.pBufferInfo = &outputDescriptor; + vkUpdateDescriptorSetsFn(device, 1, &descriptorWrite, 0, nullptr); + + VkShaderModuleCreateInfo shaderModuleInfo{}; + shaderModuleInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + shaderModuleInfo.codeSize = sizeof(kDriverPostProgram203WitnessSpv); + shaderModuleInfo.pCode = kDriverPostProgram203WitnessSpv; + result = vkCreateShaderModuleFn(device, &shaderModuleInfo, nullptr, &shaderModule); + if (result != VK_SUCCESS) { + fail(format("vkCreateShaderModule failed (VkResult = {})", static_cast(result))); + return; + } + + VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; + pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + pipelineLayoutInfo.setLayoutCount = 1; + pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout; + result = vkCreatePipelineLayoutFn(device, &pipelineLayoutInfo, nullptr, &pipelineLayout); + if (result != VK_SUCCESS) { + fail(format("vkCreatePipelineLayout failed (VkResult = {})", static_cast(result))); + return; + } + + VkPipelineShaderStageCreateInfo shaderStage{}; + shaderStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + shaderStage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + shaderStage.module = shaderModule; + shaderStage.pName = "main"; + VkComputePipelineCreateInfo pipelineInfo{}; + pipelineInfo.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; + pipelineInfo.stage = shaderStage; + pipelineInfo.layout = pipelineLayout; + result = vkCreateComputePipelinesFn(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline); + if (result != VK_SUCCESS) { + fail(format("vkCreateComputePipelines failed (VkResult = {})", static_cast(result))); + return; + } + + VkCommandPoolCreateInfo commandPoolInfo{}; + commandPoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + commandPoolInfo.queueFamilyIndex = computeQueueFamilyIndex; + result = vkCreateCommandPoolFn(device, &commandPoolInfo, nullptr, &commandPool); + if (result != VK_SUCCESS) { + fail(format("vkCreateCommandPool failed (VkResult = {})", static_cast(result))); + return; + } + VkCommandBufferAllocateInfo commandBufferInfo{}; + commandBufferInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + commandBufferInfo.commandPool = commandPool; + commandBufferInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + commandBufferInfo.commandBufferCount = 1; + VkCommandBuffer commandBuffer = VK_NULL_HANDLE; + result = vkAllocateCommandBuffersFn(device, &commandBufferInfo, &commandBuffer); + if (result != VK_SUCCESS) { + fail(format("vkAllocateCommandBuffers failed (VkResult = {})", static_cast(result))); + return; + } + + VkCommandBufferBeginInfo commandBufferBeginInfo{}; + commandBufferBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + commandBufferBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + result = vkBeginCommandBufferFn(commandBuffer, &commandBufferBeginInfo); + if (result != VK_SUCCESS) { + fail(format("vkBeginCommandBuffer failed (VkResult = {})", static_cast(result))); + return; + } + vkCmdBindPipelineFn(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); + vkCmdBindDescriptorSetsFn(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipelineLayout, 0, 1, + &descriptorSet, 0, nullptr); + vkCmdDispatchFn(commandBuffer, 1, 1, 1); + VkBufferMemoryBarrier hostReadBarrier{}; + hostReadBarrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; + hostReadBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + hostReadBarrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT; + hostReadBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + hostReadBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + hostReadBarrier.buffer = outputBuffer; + hostReadBarrier.offset = 0; + hostReadBarrier.size = sizeof(Program203WitnessOutput); + vkCmdPipelineBarrierFn(commandBuffer, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, + 0, nullptr, 1, &hostReadBarrier, 0, nullptr); + result = vkEndCommandBufferFn(commandBuffer); + if (result != VK_SUCCESS) { + fail(format("vkEndCommandBuffer failed (VkResult = {})", static_cast(result))); + return; + } + + VkFenceCreateInfo fenceInfo{}; + fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + result = vkCreateFenceFn(device, &fenceInfo, nullptr, &fence); + if (result != VK_SUCCESS) { + fail(format("vkCreateFence failed (VkResult = {})", static_cast(result))); + return; + } + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &commandBuffer; + result = vkQueueSubmitFn(queue, 1, &submitInfo, fence); + if (result != VK_SUCCESS) { + fail(format("vkQueueSubmit failed (VkResult = {})", static_cast(result))); + return; + } + + constexpr Uint64 FenceTimeoutNs = 5'000'000'000ull; + result = vkWaitForFencesFn(device, 1, &fence, VK_TRUE, FenceTimeoutNs); + if (result != VK_SUCCESS) { + fenceWaitTimedOut = true; + fail(format("vkWaitForFences did not signal within 5 s (VkResult = {})", static_cast(result))); + return; + } + + Program203WitnessOutput output{}; + std::memcpy(&output, mappedOutput, sizeof(output)); + const Program203WitnessValidationResult validation = ValidateProgram203Witness(output); + if (!validation.ok) { + fail(validation.detail); + return; + } + builder.Pass(RowName, validation.detail); + } + // Everything the "MobileGL reported ..." rows need from the Vulkan device probe. struct VulkanProbeSummary { Bool devicePropsValid = false; @@ -1669,6 +2104,7 @@ namespace MobileGL::MG_Util::SelfTest { VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; Uint32 graphicsQueueFamilyIndex = 0; Uint32 graphicsQueueTimestampValidBits = 0; + Uint32 computeQueueFamilyIndex = std::numeric_limits::max(); for (VkPhysicalDevice candidate : devices) { Uint32 queueFamilyCount = 0; vkGetPhysicalDeviceQueueFamilyPropertiesFn(candidate, &queueFamilyCount, nullptr); @@ -1684,6 +2120,13 @@ namespace MobileGL::MG_Util::SelfTest { } } if (physicalDevice != VK_NULL_HANDLE) { + for (Uint32 familyIndex = 0; familyIndex < queueFamilyCount; ++familyIndex) { + const VkQueueFamilyProperties& family = queueFamilies[familyIndex]; + if (family.queueCount > 0 && (family.queueFlags & VK_QUEUE_COMPUTE_BIT) != 0) { + computeQueueFamilyIndex = familyIndex; + break; + } + } break; } } @@ -2020,13 +2463,15 @@ namespace MobileGL::MG_Util::SelfTest { "change every N instances change every one"); } + VkPhysicalDeviceSubgroupProperties subgroupProperties{}; + Bool subgroupPropertiesAvailable = false; if (vkGetPhysicalDeviceProperties2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) { - VkPhysicalDeviceSubgroupProperties subgroupProperties{}; subgroupProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES; VkPhysicalDeviceProperties2 properties2{}; properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; properties2.pNext = &subgroupProperties; vkGetPhysicalDeviceProperties2Fn(physicalDevice, &properties2); + subgroupPropertiesAvailable = true; const Bool subgroupUsable = subgroupProperties.subgroupSize > 0 && (subgroupProperties.supportedStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0 && (subgroupProperties.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0; @@ -2046,6 +2491,9 @@ namespace MobileGL::MG_Util::SelfTest { builder.Warn("Compute shader subgroup", "subgroup properties could not be queried"); } + ProbeVulkanProgram203Witness(builder, getInstanceProcAddr, instance, physicalDevice, computeQueueFamilyIndex, + properties, subgroupPropertiesAvailable, subgroupProperties); + if (HasVkExtension(deviceExtensions, VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME)) { builder.Pass("VK_KHR_draw_indirect_count", "supported (count-buffer indirect draws run as single native " diff --git a/MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.comp b/MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.comp new file mode 100644 index 00000000..2576228c --- /dev/null +++ b/MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.comp @@ -0,0 +1,164 @@ +// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.comp +// Copyright (c) 2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// Native Vulkan GLSL 450 witness for Program 203's first subgroup reduction. +// It is intentionally independent of the GL 430 integration scenario. The body +// below preserves Program 203's source reduction; the surrounding diagnostics +// only observe its topology and cache handoffs. + +#version 450 +#extension GL_KHR_shader_subgroup_basic : require +#extension GL_KHR_shader_subgroup_arithmetic : require + +layout(local_size_x = 32, local_size_y = 16, local_size_z = 1) in; + +const uint kTopologyNonuniformNumSubgroups = 1u << 0u; +const uint kTopologyInvalidNumSubgroups = 1u << 1u; +const uint kTopologyInvalidSubgroupId = 1u << 2u; +const uint kTopologyInvalidSubgroupLane = 1u << 3u; +const uint kWitnessMagic = 0x50323033u; + +layout(std430, set = 0, binding = 0) buffer Program203WitnessOutput { + uint magic; + uint topologyFlags; + uint numSubgroups; + uint loopLength; + uint seenSubgroupMask; + + uvec4 owner511; + + uint lastLaneWriterCount[32]; + uint indexedInputTotal[32]; + + vec2 rawPrefix[32]; + vec2 scanCache[6][32]; + vec2 finalAverage; +} outWitness; + +// Program 203's cache stays separate from all diagnostic shared state. In +// particular, no instrumentation stores through prefixSumCache except source +// writes retained below. +shared vec2 prefixSumCache[32]; +shared uint canonicalNumSubgroups; +shared uint topologyFlagsShared; +shared uint seenSubgroupMaskShared; +shared uint lastLaneWriterCountShared[32]; +shared uint indexedInputTotalShared[32]; + +void main() { + const uint localInvocationIndex = gl_LocalInvocationIndex; + + // Host memory is deliberately poisoned before dispatch. Initialize only + // shared atomic diagnostic state; owner, average, and magic remain poisoned + // until their required post-source-barrier writes below. + if (localInvocationIndex == 0u) { + canonicalNumSubgroups = 0u; + topologyFlagsShared = 0u; + seenSubgroupMaskShared = 0u; + } + if (localInvocationIndex < 32u) { + lastLaneWriterCountShared[localInvocationIndex] = 0u; + indexedInputTotalShared[localInvocationIndex] = 0u; + } + memoryBarrierShared(); + barrier(); + + // Invocation zero defines the canonical domain. It is broadcast through + // shared memory before every invocation records its own raw observation. + if (localInvocationIndex == 0u) { + canonicalNumSubgroups = gl_NumSubgroups; + outWitness.numSubgroups = gl_NumSubgroups; + } + barrier(); + + const uint canonicalN = canonicalNumSubgroups; + if (gl_NumSubgroups != canonicalN) + atomicOr(topologyFlagsShared, kTopologyNonuniformNumSubgroups); + if (gl_NumSubgroups < 2u || gl_NumSubgroups > 32u) + atomicOr(topologyFlagsShared, kTopologyInvalidNumSubgroups); + if (gl_SubgroupID >= canonicalN || gl_SubgroupID >= 32u) + atomicOr(topologyFlagsShared, kTopologyInvalidSubgroupId); + if (gl_SubgroupSize == 0u || gl_SubgroupInvocationID >= gl_SubgroupSize) + atomicOr(topologyFlagsShared, kTopologyInvalidSubgroupLane); + + // Keep all atomic collection bounded by the canonical valid domain. A + // nonuniform/broken report reaches the uniform safety branch below instead + // of making some lanes return before a barrier. + const bool canonicalDomain = canonicalN >= 2u && canonicalN <= 32u; + const bool idInCanonicalDomain = canonicalDomain && gl_SubgroupID < canonicalN; + if (idInCanonicalDomain) { + atomicOr(seenSubgroupMaskShared, 1u << gl_SubgroupID); + atomicAdd(indexedInputTotalShared[gl_SubgroupID], localInvocationIndex + 1u); + if (gl_SubgroupSize != 0u && gl_SubgroupInvocationID == gl_SubgroupSize - 1u) + atomicAdd(lastLaneWriterCountShared[gl_SubgroupID], 1u); + } + memoryBarrierShared(); + barrier(); + + if (localInvocationIndex == 0u) + outWitness.seenSubgroupMask = seenSubgroupMaskShared; + if (localInvocationIndex < 32u) { + outWitness.lastLaneWriterCount[localInvocationIndex] = lastLaneWriterCountShared[localInvocationIndex]; + outWitness.indexedInputTotal[localInvocationIndex] = indexedInputTotalShared[localInvocationIndex]; + } + + // This branch is uniform after collection and is solely a safety guard for + // broken topology reports. The valid side retains Program 203 verbatim. + const bool sourceDomain = canonicalDomain && topologyFlagsShared == 0u; + if (sourceDomain) { + vec2 sampleLuminance = vec2(float(gl_LocalInvocationIndex + 1u), 0.0); + sampleLuminance = subgroupInclusiveAdd(sampleLuminance); + + if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) + prefixSumCache[gl_SubgroupID] = sampleLuminance; + barrier(); + + if (gl_LocalInvocationIndex < gl_NumSubgroups) + outWitness.rawPrefix[gl_LocalInvocationIndex] = prefixSumCache[gl_LocalInvocationIndex]; + barrier(); + + uint loopLength = uint(findMSB(gl_NumSubgroups)); + loopLength += uint(gl_NumSubgroups - (1u << (loopLength - 1u)) > 0u); + if (gl_LocalInvocationIndex == 0u) + outWitness.loopLength = loopLength; + + for (uint scanStage = 0u; scanStage < loopLength; ++scanStage) { + if ((gl_SubgroupID & (1u << scanStage)) > 0u) { + sampleLuminance += prefixSumCache[(gl_SubgroupID >> scanStage << scanStage) - 1u]; + if (gl_SubgroupInvocationID == gl_SubgroupSize - 1u) + prefixSumCache[gl_SubgroupID] = sampleLuminance; + } + barrier(); + + if (gl_LocalInvocationIndex < gl_NumSubgroups) + outWitness.scanCache[scanStage][gl_LocalInvocationIndex] = + prefixSumCache[gl_LocalInvocationIndex]; + // A second, diagnostic-only barrier prevents a faster invocation + // from entering the next source stage while another reads this cache. + barrier(); + } + + if (gl_LocalInvocationIndex == 511u) + prefixSumCache[0] = sampleLuminance / 512.0; + barrier(); + + if (gl_LocalInvocationIndex == 511u) { + outWitness.owner511 = uvec4(gl_SubgroupSize, gl_NumSubgroups, gl_SubgroupID, + gl_SubgroupInvocationID); + outWitness.finalAverage = prefixSumCache[0]; + } + } + + // Both sides of the uniform branch reach this barrier. The magic is the + // completion latch and therefore cannot be written before the final barrier. + barrier(); + if (localInvocationIndex == 0u) { + outWitness.topologyFlags = topologyFlagsShared; + outWitness.magic = kWitnessMagic; + } +} diff --git a/MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.cpp b/MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.cpp new file mode 100644 index 00000000..74a0078f --- /dev/null +++ b/MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.cpp @@ -0,0 +1,277 @@ +// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.cpp +// Copyright (c) 2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "DriverPostProgram203Witness.h" + +#include +#include +#include +#include + +namespace MobileGL::MG_Util::SelfTest { + namespace { + [[nodiscard]] Program203WitnessValidationResult Failure(Program203WitnessValidationFailure failure, + std::string detail, + std::uint32_t scanStage = 0u, + std::uint32_t subgroup = 0u) { + Program203WitnessValidationResult result; + result.ok = false; + result.failure = failure; + result.scanStage = scanStage; + result.subgroup = subgroup; + result.detail = std::move(detail); + return result; + } + + [[nodiscard]] std::uint32_t FloatBits(float value) { + return std::bit_cast(value); + } + + [[nodiscard]] bool SameBits(float lhs, float rhs) { + return FloatBits(lhs) == FloatBits(rhs); + } + + [[nodiscard]] bool SameBits(const Program203WitnessVec2& lhs, const Program203WitnessVec2& rhs) { + return SameBits(lhs.x, rhs.x) && SameBits(lhs.y, rhs.y); + } + + [[nodiscard]] std::string Vec2String(const Program203WitnessVec2& value) { + std::ostringstream output; + output << '(' << value.x << ',' << value.y << ')'; + return output.str(); + } + + [[nodiscard]] std::uint32_t ExpectedSeenSubgroupMask(std::uint32_t numSubgroups) { + return numSubgroups == kProgram203WitnessMaxSubgroups ? 0xffffffffu : (1u << numSubgroups) - 1u; + } + + [[nodiscard]] std::string JoinRequirements(const std::vector& requirements) { + std::ostringstream output; + for (std::size_t i = 0; i < requirements.size(); ++i) { + if (i != 0u) output << "; "; + output << requirements[i]; + } + return output.str(); + } + } // namespace + + Program203WitnessEligibilityResult + EvaluateProgram203WitnessEligibility(const Program203WitnessLimits& limits) { + // This classification deliberately precedes numeric limits. An absent native + // compute/basic/arithmetic subgroup contract means there is nothing to witness, + // whereas every resource/entry-point failure on a capable device is a POST FAIL. + if (!limits.computeStageSupported || !limits.basicSubgroupSupported || !limits.arithmeticSubgroupSupported) { + std::vector missing; + if (!limits.computeStageSupported) missing.emplace_back("VK_SHADER_STAGE_COMPUTE_BIT"); + if (!limits.basicSubgroupSupported) missing.emplace_back("VK_SUBGROUP_FEATURE_BASIC_BIT"); + if (!limits.arithmeticSubgroupSupported) missing.emplace_back("VK_SUBGROUP_FEATURE_ARITHMETIC_BIT"); + return {Program203WitnessEligibility::SkipUnsupportedNativeFeatureSet, + "skipped because the native compute/basic/arithmetic subgroup feature set is unsupported (missing " + + JoinRequirements(missing) + ')'}; + } + + std::vector inadequate; + if (limits.subgroupSize == 0u) { + inadequate.emplace_back("subgroupSize == 0"); + } + if (limits.maxComputeWorkGroupInvocations < kProgram203WitnessInvocationCount) { + inadequate.emplace_back("maxComputeWorkGroupInvocations < 512"); + } + if (limits.maxComputeWorkGroupSize[0] < 32u || limits.maxComputeWorkGroupSize[1] < 16u || + limits.maxComputeWorkGroupSize[2] < 1u) { + inadequate.emplace_back("maxComputeWorkGroupSize does not cover 32x16x1"); + } + if (limits.maxComputeSharedMemorySize < kProgram203WitnessSharedMemoryBytes) { + inadequate.emplace_back("maxComputeSharedMemorySize < " + + std::to_string(kProgram203WitnessSharedMemoryBytes)); + } + if (limits.maxPerStageDescriptorStorageBuffers < 1u) { + inadequate.emplace_back("maxPerStageDescriptorStorageBuffers < 1"); + } + if (limits.maxDescriptorSetStorageBuffers < 1u) { + inadequate.emplace_back("maxDescriptorSetStorageBuffers < 1"); + } + if (limits.maxBoundDescriptorSets < 1u) { + inadequate.emplace_back("maxBoundDescriptorSets < 1"); + } + if (limits.maxStorageBufferRange < sizeof(Program203WitnessOutput)) { + inadequate.emplace_back("maxStorageBufferRange < " + + std::to_string(sizeof(Program203WitnessOutput))); + } + if (!inadequate.empty()) { + return {Program203WitnessEligibility::FailInadequateLimits, + "insufficient Vulkan limits for a 32x16x1 workgroup, one output SSBO, and " + + std::to_string(kProgram203WitnessSharedMemoryBytes) + " bytes of shared memory: " + + JoinRequirements(inadequate)}; + } + return {Program203WitnessEligibility::Execute, {}}; + } + + std::uint32_t ComputeProgram203WitnessLoopLength(std::uint32_t numSubgroups) { + if (numSubgroups < 2u || numSubgroups > kProgram203WitnessMaxSubgroups) return 0u; + + // Exact C++ spelling of the source's findMSB-based calculation. In + // particular, its final iteration for powers of two is intentional. + std::uint32_t loopLength = 0u; + for (std::uint32_t value = numSubgroups; value > 1u; value >>= 1u) { + ++loopLength; + } + loopLength += static_cast(numSubgroups - (1u << (loopLength - 1u)) > 0u); + return loopLength; + } + + Program203WitnessValidationResult ValidateProgram203Witness(const Program203WitnessOutput& output) { + // 1. Completion. A poisoned or unwritten result must never turn into a + // topology diagnosis, because it says nothing about execution. + if (output.magic != kProgram203WitnessMagic) { + std::ostringstream detail; + detail << "completion: magic was 0x" << std::hex << output.magic << ", expected 0x" + << kProgram203WitnessMagic; + return Failure(Program203WitnessValidationFailure::Completion, detail.str()); + } + + // 2. Observed topology. All checks consume observations written by the + // shader, rather than inferring subgroup layout from invocation indices. + const std::uint32_t numSubgroups = output.numSubgroups; + if (numSubgroups < 2u || numSubgroups > kProgram203WitnessMaxSubgroups) { + std::ostringstream detail; + detail << "topology: canonical gl_NumSubgroups=" << numSubgroups << " is outside [2, 32]"; + return Failure(Program203WitnessValidationFailure::Topology, detail.str()); + } + if ((output.topologyFlags & Program203WitnessNonuniformNumSubgroups) != 0u) { + return Failure(Program203WitnessValidationFailure::Topology, + "topology: gl_NumSubgroups differed across workgroup"); + } + if ((output.topologyFlags & Program203WitnessInvalidNumSubgroups) != 0u) { + return Failure(Program203WitnessValidationFailure::Topology, + "topology: an invocation reported gl_NumSubgroups outside [2, 32]"); + } + if ((output.topologyFlags & Program203WitnessInvalidSubgroupId) != 0u) { + return Failure(Program203WitnessValidationFailure::Topology, + "topology: an invocation reported an invalid gl_SubgroupID"); + } + if ((output.topologyFlags & Program203WitnessInvalidSubgroupLane) != 0u) { + return Failure(Program203WitnessValidationFailure::Topology, + "topology: an invocation reported an invalid subgroup lane"); + } + if ((output.topologyFlags & ~(Program203WitnessNonuniformNumSubgroups | + Program203WitnessInvalidNumSubgroups | + Program203WitnessInvalidSubgroupId | + Program203WitnessInvalidSubgroupLane)) != 0u) { + std::ostringstream detail; + detail << "topology: unknown topology flags 0x" << std::hex << output.topologyFlags; + return Failure(Program203WitnessValidationFailure::Topology, detail.str()); + } + const std::uint32_t expectedMask = ExpectedSeenSubgroupMask(numSubgroups); + if (output.seenSubgroupMask != expectedMask) { + std::ostringstream detail; + detail << "topology: seen subgroup-ID mask was 0x" << std::hex << output.seenSubgroupMask + << ", expected 0x" << expectedMask; + return Failure(Program203WitnessValidationFailure::Topology, detail.str()); + } + const std::uint32_t expectedLoopLength = ComputeProgram203WitnessLoopLength(numSubgroups); + if (output.loopLength != expectedLoopLength) { + std::ostringstream detail; + detail << "topology: loopLength was " << std::dec << output.loopLength << ", expected " + << expectedLoopLength; + return Failure(Program203WitnessValidationFailure::Topology, detail.str()); + } + for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) { + if (output.lastLaneWriterCount[subgroup] != 1u) { + std::ostringstream detail; + detail << "topology: subgroup " << subgroup << " has " + << output.lastLaneWriterCount[subgroup] << " source last-lane writers, expected exactly 1"; + return Failure(Program203WitnessValidationFailure::Topology, detail.str(), 0u, subgroup); + } + } + if (output.owner511.y != numSubgroups) { + std::ostringstream detail; + detail << "final owner: invocation 511 reported gl_NumSubgroups=" << output.owner511.y << ", expected " + << numSubgroups; + return Failure(Program203WitnessValidationFailure::FinalOwner, detail.str()); + } + if (output.owner511.z != numSubgroups - 1u) { + std::ostringstream detail; + detail << "final owner: invocation 511 is not in the highest subgroup (id" << output.owner511.z + << ", expected id" << (numSubgroups - 1u) << ')'; + return Failure(Program203WitnessValidationFailure::FinalOwner, detail.str()); + } + if (output.owner511.x == 0u || output.owner511.w != output.owner511.x - 1u) { + std::ostringstream detail; + detail << "final owner: invocation 511 is not the last lane of highest subgroup (size " + << output.owner511.x << ", lane " << output.owner511.w << ')'; + return Failure(Program203WitnessValidationFailure::FinalOwner, detail.str()); + } + + // 3. Initial subgroup handoff. The atomic scalar totals are independent + // of subgroupInclusiveAdd; their sum and the cache values establish that + // the final lanes handed off the native vector inclusive-add results. + std::uint64_t indexedTotal = 0u; + for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) { + indexedTotal += output.indexedInputTotal[subgroup]; + } + if (indexedTotal != 131328u) { + std::ostringstream detail; + detail << "initial subgroup handoff: indexed input total was " << indexedTotal << ", expected 131328"; + return Failure(Program203WitnessValidationFailure::InitialSubgroupHandoff, detail.str()); + } + for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) { + const Program203WitnessVec2 expected = {static_cast(output.indexedInputTotal[subgroup]), 0.0f}; + if (!SameBits(output.rawPrefix[subgroup], expected)) { + std::ostringstream detail; + detail << "initial subgroup handoff: subgroup " << subgroup << " rawPrefix observed " + << Vec2String(output.rawPrefix[subgroup]) << ", expected " << Vec2String(expected); + return Failure(Program203WitnessValidationFailure::InitialSubgroupHandoff, detail.str(), 0u, + subgroup); + } + } + + // 4. Source scan. Do not substitute a conventional scan: this reproduces + // the source cache index expression and stage ordering word for word. + std::array expectedCache = output.rawPrefix; + for (std::uint32_t scanStage = 0u; scanStage < expectedLoopLength; ++scanStage) { + auto cacheAfterStage = expectedCache; + for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) { + if ((subgroup & (1u << scanStage)) > 0u) { + const std::uint32_t sourceCacheIndex = (subgroup >> scanStage << scanStage) - 1u; + cacheAfterStage[subgroup].x += expectedCache[sourceCacheIndex].x; + cacheAfterStage[subgroup].y += expectedCache[sourceCacheIndex].y; + } + } + expectedCache = cacheAfterStage; + for (std::uint32_t subgroup = 0u; subgroup < numSubgroups; ++subgroup) { + if (!SameBits(output.scanCache[scanStage][subgroup], expectedCache[subgroup])) { + std::ostringstream detail; + detail << "source scan stage " << scanStage << ", subgroup " << subgroup << ": observed " + << Vec2String(output.scanCache[scanStage][subgroup]) << ", expected " + << Vec2String(expectedCache[subgroup]); + return Failure(Program203WitnessValidationFailure::SourceScan, detail.str(), scanStage, subgroup); + } + } + } + + // 5. The owner contract was checked above with the other topology facts; + // this final result remains a separate exact-vector check. + const Program203WitnessVec2 expectedAverage = {256.5f, 0.0f}; + if (!SameBits(output.finalAverage, expectedAverage)) { + std::ostringstream detail; + detail << "final average: observed " << Vec2String(output.finalAverage) << ", expected " + << Vec2String(expectedAverage); + return Failure(Program203WitnessValidationFailure::FinalAverage, detail.str()); + } + + std::ostringstream detail; + detail << "N=" << numSubgroups << ", owner511=id" << output.owner511.z << "/lane" << output.owner511.w + << ", " << expectedLoopLength << " scan stages, average=" << Vec2String(output.finalAverage); + Program203WitnessValidationResult result; + result.ok = true; + result.failure = Program203WitnessValidationFailure::None; + result.detail = detail.str(); + return result; + } +} // namespace MobileGL::MG_Util::SelfTest diff --git a/MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.h b/MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.h new file mode 100644 index 00000000..be7af86c --- /dev/null +++ b/MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.h @@ -0,0 +1,151 @@ +// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostProgram203Witness.h +// Copyright (c) 2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// Compact, native-Vulkan Program-203 first-reduction witness ABI and its pure +// validator. The types below deliberately mirror DriverPostProgram203Witness.comp's +// single std430 storage block; changing either side requires updating the static +// layout assertions here. + +#pragma once + +#include +#include +#include +#include +#include + +namespace MobileGL::MG_Util::SelfTest { + constexpr std::uint32_t kProgram203WitnessMagic = 0x50323033u; // "P203" + constexpr std::uint32_t kProgram203WitnessInvocationCount = 512u; + constexpr std::uint32_t kProgram203WitnessMaxSubgroups = 32u; + constexpr std::uint32_t kProgram203WitnessMaxScanStages = 6u; + + // These bit values are shared with the GLSL source. They document failures in + // topology observations rather than guessing a topology from local IDs on the host. + enum Program203WitnessTopologyFlag : std::uint32_t { + Program203WitnessNonuniformNumSubgroups = 1u << 0u, + Program203WitnessInvalidNumSubgroups = 1u << 1u, + Program203WitnessInvalidSubgroupId = 1u << 2u, + Program203WitnessInvalidSubgroupLane = 1u << 3u, + }; + + struct alignas(8) Program203WitnessVec2 { + float x; + float y; + }; + + struct alignas(16) Program203WitnessUVec4 { + std::uint32_t x; + std::uint32_t y; + std::uint32_t z; + std::uint32_t w; + }; + + // std430 layout of DriverPostProgram203Witness.comp's Program203WitnessOutput block. + struct alignas(16) Program203WitnessOutput { + std::uint32_t magic; + std::uint32_t topologyFlags; + std::uint32_t numSubgroups; + std::uint32_t loopLength; + std::uint32_t seenSubgroupMask; + + Program203WitnessUVec4 owner511; + + std::array lastLaneWriterCount; + std::array indexedInputTotal; + + std::array rawPrefix; + std::array, + kProgram203WitnessMaxScanStages> + scanCache; + Program203WitnessVec2 finalAverage; + }; + + static_assert(std::is_standard_layout_v); + static_assert(std::is_standard_layout_v); + static_assert(std::is_standard_layout_v); + static_assert(sizeof(Program203WitnessVec2) == 8u); + static_assert(alignof(Program203WitnessVec2) == 8u); + static_assert(sizeof(Program203WitnessUVec4) == 16u); + static_assert(alignof(Program203WitnessUVec4) == 16u); + static_assert(offsetof(Program203WitnessOutput, magic) == 0u); + static_assert(offsetof(Program203WitnessOutput, topologyFlags) == 4u); + static_assert(offsetof(Program203WitnessOutput, numSubgroups) == 8u); + static_assert(offsetof(Program203WitnessOutput, loopLength) == 12u); + static_assert(offsetof(Program203WitnessOutput, seenSubgroupMask) == 16u); + static_assert(offsetof(Program203WitnessOutput, owner511) == 32u); + static_assert(offsetof(Program203WitnessOutput, lastLaneWriterCount) == 48u); + static_assert(offsetof(Program203WitnessOutput, indexedInputTotal) == 176u); + static_assert(offsetof(Program203WitnessOutput, rawPrefix) == 304u); + static_assert(offsetof(Program203WitnessOutput, scanCache) == 560u); + static_assert(offsetof(Program203WitnessOutput, finalAverage) == 2096u); + static_assert(sizeof(Program203WitnessOutput) == 2112u); + + // The witness uses prefixSumCache[32], three scalar shared diagnostics, and + // two 32-entry scalar diagnostic arrays in the GLSL source. Keep this + // independent of the output SSBO size. + constexpr std::uint32_t kProgram203WitnessSharedMemoryBytes = + kProgram203WitnessMaxSubgroups * sizeof(Program203WitnessVec2) + + 3u * sizeof(std::uint32_t) + + 2u * kProgram203WitnessMaxSubgroups * sizeof(std::uint32_t); + + enum class Program203WitnessEligibility { + Execute, + SkipUnsupportedNativeFeatureSet, + FailInadequateLimits, + }; + + // The raw physical-device conditions needed by the native witness. This is + // intentionally distinct from MobileGL's advertised-extension policy. + struct Program203WitnessLimits { + bool computeStageSupported = false; + bool basicSubgroupSupported = false; + bool arithmeticSubgroupSupported = false; + std::uint32_t subgroupSize = 0u; + + std::uint32_t maxComputeWorkGroupInvocations = 0u; + std::array maxComputeWorkGroupSize{}; + std::uint32_t maxComputeSharedMemorySize = 0u; + std::uint32_t maxPerStageDescriptorStorageBuffers = 0u; + std::uint32_t maxDescriptorSetStorageBuffers = 0u; + std::uint32_t maxBoundDescriptorSets = 0u; + std::uint64_t maxStorageBufferRange = 0u; + }; + + struct Program203WitnessEligibilityResult { + Program203WitnessEligibility eligibility = Program203WitnessEligibility::FailInadequateLimits; + std::string detail; + }; + + enum class Program203WitnessValidationFailure { + None, + Completion, + Topology, + InitialSubgroupHandoff, + SourceScan, + FinalOwner, + FinalAverage, + }; + + struct Program203WitnessValidationResult { + bool ok = false; + Program203WitnessValidationFailure failure = Program203WitnessValidationFailure::Completion; + std::uint32_t scanStage = 0u; + std::uint32_t subgroup = 0u; + std::string detail; + }; + + [[nodiscard]] Program203WitnessEligibilityResult + EvaluateProgram203WitnessEligibility(const Program203WitnessLimits& limits); + + // Mirrors the source's findMSB expression for valid N in [2, 32]. + [[nodiscard]] std::uint32_t ComputeProgram203WitnessLoopLength(std::uint32_t numSubgroups); + + [[nodiscard]] Program203WitnessValidationResult + ValidateProgram203Witness(const Program203WitnessOutput& output); +} // namespace MobileGL::MG_Util::SelfTest diff --git a/MobileGL/MG_Util/SelfTest/DriverPostProgram203WitnessSpv.h b/MobileGL/MG_Util/SelfTest/DriverPostProgram203WitnessSpv.h new file mode 100644 index 00000000..c5bf98eb --- /dev/null +++ b/MobileGL/MG_Util/SelfTest/DriverPostProgram203WitnessSpv.h @@ -0,0 +1,291 @@ +// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostProgram203WitnessSpv.h +// Copyright (c) 2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// Generated from DriverPostProgram203Witness.comp with: +// glslangValidator --target-env vulkan1.1 -V DriverPostProgram203Witness.comp +// Validated with spirv-val --target-env vulkan1.1. Do not edit words by hand. + +#pragma once + +#include +#include + +namespace MobileGL::MG_Util::SelfTest { + inline constexpr std::uint32_t kDriverPostProgram203WitnessSpv[] = { + 0x07230203u, 0x00010300u, 0x0008000bu, 0x00000145u, 0x00000000u, 0x00020011u, 0x00000001u, 0x00020011u, + 0x0000003du, 0x00020011u, 0x0000003fu, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, + 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x000a000fu, 0x00000005u, 0x00000004u, 0x6e69616du, + 0x00000000u, 0x0000000au, 0x0000002au, 0x00000050u, 0x0000005eu, 0x00000064u, 0x00060010u, 0x00000004u, + 0x00000011u, 0x00000020u, 0x00000010u, 0x00000001u, 0x00030003u, 0x00000002u, 0x000001c2u, 0x000a0004u, + 0x4b5f4c47u, 0x735f5248u, 0x65646168u, 0x75735f72u, 0x6f726762u, 0x615f7075u, 0x68746972u, 0x6974656du, + 0x00000063u, 0x00090004u, 0x4b5f4c47u, 0x735f5248u, 0x65646168u, 0x75735f72u, 0x6f726762u, 0x625f7075u, + 0x63697361u, 0x00000000u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00080005u, 0x00000008u, + 0x61636f6cu, 0x766e496cu, 0x7461636fu, 0x496e6f69u, 0x7865646eu, 0x00000000u, 0x00080005u, 0x0000000au, + 0x4c5f6c67u, 0x6c61636fu, 0x6f766e49u, 0x69746163u, 0x6e496e6fu, 0x00786564u, 0x00080005u, 0x00000013u, + 0x6f6e6163u, 0x6163696eu, 0x6d754e6cu, 0x67627553u, 0x70756f72u, 0x00000073u, 0x00070005u, 0x00000014u, + 0x6f706f74u, 0x79676f6cu, 0x67616c46u, 0x61685373u, 0x00646572u, 0x00080005u, 0x00000015u, 0x6e656573u, + 0x67627553u, 0x70756f72u, 0x6b73614du, 0x72616853u, 0x00006465u, 0x00090005u, 0x0000001du, 0x7473616cu, + 0x656e614cu, 0x74697257u, 0x6f437265u, 0x53746e75u, 0x65726168u, 0x00000064u, 0x00080005u, 0x00000020u, + 0x65646e69u, 0x49646578u, 0x7475706eu, 0x61746f54u, 0x6168536cu, 0x00646572u, 0x00060005u, 0x0000002au, + 0x4e5f6c67u, 0x75536d75u, 0x6f726762u, 0x00737075u, 0x00080005u, 0x00000035u, 0x676f7250u, 0x326d6172u, + 0x69573330u, 0x73656e74u, 0x74754f73u, 0x00747570u, 0x00050006u, 0x00000035u, 0x00000000u, 0x6967616du, + 0x00000063u, 0x00070006u, 0x00000035u, 0x00000001u, 0x6f706f74u, 0x79676f6cu, 0x67616c46u, 0x00000073u, + 0x00070006u, 0x00000035u, 0x00000002u, 0x536d756eu, 0x72676275u, 0x7370756fu, 0x00000000u, 0x00060006u, + 0x00000035u, 0x00000003u, 0x706f6f6cu, 0x676e654cu, 0x00006874u, 0x00080006u, 0x00000035u, 0x00000004u, + 0x6e656573u, 0x67627553u, 0x70756f72u, 0x6b73614du, 0x00000000u, 0x00060006u, 0x00000035u, 0x00000005u, + 0x656e776fu, 0x31313572u, 0x00000000u, 0x00080006u, 0x00000035u, 0x00000006u, 0x7473616cu, 0x656e614cu, + 0x74697257u, 0x6f437265u, 0x00746e75u, 0x00080006u, 0x00000035u, 0x00000007u, 0x65646e69u, 0x49646578u, + 0x7475706eu, 0x61746f54u, 0x0000006cu, 0x00060006u, 0x00000035u, 0x00000008u, 0x50776172u, 0x69666572u, + 0x00000078u, 0x00060006u, 0x00000035u, 0x00000009u, 0x6e616373u, 0x68636143u, 0x00000065u, 0x00070006u, + 0x00000035u, 0x0000000au, 0x616e6966u, 0x6576416cu, 0x65676172u, 0x00000000u, 0x00050005u, 0x00000037u, + 0x5774756fu, 0x656e7469u, 0x00007373u, 0x00050005u, 0x0000003du, 0x6f6e6163u, 0x6163696eu, 0x00004e6cu, + 0x00060005u, 0x00000050u, 0x535f6c67u, 0x72676275u, 0x4970756fu, 0x00000044u, 0x00060005u, 0x0000005eu, + 0x535f6c67u, 0x72676275u, 0x5370756fu, 0x00657a69u, 0x00080005u, 0x00000064u, 0x535f6c67u, 0x72676275u, + 0x4970756fu, 0x636f766eu, 0x6f697461u, 0x0044496eu, 0x00060005u, 0x0000006eu, 0x6f6e6163u, 0x6163696eu, + 0x6d6f446cu, 0x006e6961u, 0x00070005u, 0x00000074u, 0x6e496469u, 0x6f6e6143u, 0x6163696eu, 0x6d6f446cu, + 0x006e6961u, 0x00060005u, 0x000000acu, 0x72756f73u, 0x6f446563u, 0x6e69616du, 0x00000000u, 0x00060005u, + 0x000000b7u, 0x706d6173u, 0x754c656cu, 0x616e696du, 0x0065636eu, 0x00060005u, 0x000000c8u, 0x66657270u, + 0x75537869u, 0x6361436du, 0x00006568u, 0x00050005u, 0x000000d9u, 0x706f6f6cu, 0x676e654cu, 0x00006874u, + 0x00050005u, 0x000000edu, 0x6e616373u, 0x67617453u, 0x00000065u, 0x00040047u, 0x0000000au, 0x0000000bu, + 0x0000001du, 0x00040047u, 0x0000002au, 0x0000000bu, 0x00000026u, 0x00040047u, 0x0000002du, 0x00000006u, + 0x00000004u, 0x00040047u, 0x0000002eu, 0x00000006u, 0x00000004u, 0x00040047u, 0x00000031u, 0x00000006u, + 0x00000008u, 0x00040047u, 0x00000032u, 0x00000006u, 0x00000008u, 0x00040047u, 0x00000034u, 0x00000006u, + 0x00000100u, 0x00030047u, 0x00000035u, 0x00000002u, 0x00050048u, 0x00000035u, 0x00000000u, 0x00000023u, + 0x00000000u, 0x00050048u, 0x00000035u, 0x00000001u, 0x00000023u, 0x00000004u, 0x00050048u, 0x00000035u, + 0x00000002u, 0x00000023u, 0x00000008u, 0x00050048u, 0x00000035u, 0x00000003u, 0x00000023u, 0x0000000cu, + 0x00050048u, 0x00000035u, 0x00000004u, 0x00000023u, 0x00000010u, 0x00050048u, 0x00000035u, 0x00000005u, + 0x00000023u, 0x00000020u, 0x00050048u, 0x00000035u, 0x00000006u, 0x00000023u, 0x00000030u, 0x00050048u, + 0x00000035u, 0x00000007u, 0x00000023u, 0x000000b0u, 0x00050048u, 0x00000035u, 0x00000008u, 0x00000023u, + 0x00000130u, 0x00050048u, 0x00000035u, 0x00000009u, 0x00000023u, 0x00000230u, 0x00050048u, 0x00000035u, + 0x0000000au, 0x00000023u, 0x00000830u, 0x00040047u, 0x00000037u, 0x00000021u, 0x00000000u, 0x00040047u, + 0x00000037u, 0x00000022u, 0x00000000u, 0x00040047u, 0x00000050u, 0x0000000bu, 0x00000028u, 0x00030047u, + 0x0000005eu, 0x00000000u, 0x00040047u, 0x0000005eu, 0x0000000bu, 0x00000024u, 0x00030047u, 0x0000005fu, + 0x00000000u, 0x00030047u, 0x00000064u, 0x00000000u, 0x00040047u, 0x00000064u, 0x0000000bu, 0x00000029u, + 0x00030047u, 0x00000065u, 0x00000000u, 0x00030047u, 0x00000066u, 0x00000000u, 0x00030047u, 0x00000087u, + 0x00000000u, 0x00030047u, 0x0000008bu, 0x00000000u, 0x00030047u, 0x0000008cu, 0x00000000u, 0x00030047u, + 0x0000008du, 0x00000000u, 0x00030047u, 0x000000c0u, 0x00000000u, 0x00030047u, 0x000000c1u, 0x00000000u, + 0x00030047u, 0x000000c2u, 0x00000000u, 0x00030047u, 0x00000107u, 0x00000000u, 0x00030047u, 0x00000108u, + 0x00000000u, 0x00030047u, 0x00000109u, 0x00000000u, 0x00030047u, 0x0000012fu, 0x00000000u, 0x00030047u, + 0x00000132u, 0x00000000u, 0x00040047u, 0x00000144u, 0x0000000bu, 0x00000019u, 0x00020013u, 0x00000002u, + 0x00030021u, 0x00000003u, 0x00000002u, 0x00040015u, 0x00000006u, 0x00000020u, 0x00000000u, 0x00040020u, + 0x00000007u, 0x00000007u, 0x00000006u, 0x00040020u, 0x00000009u, 0x00000001u, 0x00000006u, 0x0004003bu, + 0x00000009u, 0x0000000au, 0x00000001u, 0x0004002bu, 0x00000006u, 0x0000000du, 0x00000000u, 0x00020014u, + 0x0000000eu, 0x00040020u, 0x00000012u, 0x00000004u, 0x00000006u, 0x0004003bu, 0x00000012u, 0x00000013u, + 0x00000004u, 0x0004003bu, 0x00000012u, 0x00000014u, 0x00000004u, 0x0004003bu, 0x00000012u, 0x00000015u, + 0x00000004u, 0x0004002bu, 0x00000006u, 0x00000017u, 0x00000020u, 0x0004001cu, 0x0000001bu, 0x00000006u, + 0x00000017u, 0x00040020u, 0x0000001cu, 0x00000004u, 0x0000001bu, 0x0004003bu, 0x0000001cu, 0x0000001du, + 0x00000004u, 0x0004003bu, 0x0000001cu, 0x00000020u, 0x00000004u, 0x0004002bu, 0x00000006u, 0x00000023u, + 0x00000001u, 0x0004002bu, 0x00000006u, 0x00000024u, 0x00000108u, 0x0004002bu, 0x00000006u, 0x00000025u, + 0x00000002u, 0x0004003bu, 0x00000009u, 0x0000002au, 0x00000001u, 0x00040017u, 0x0000002cu, 0x00000006u, + 0x00000004u, 0x0004001cu, 0x0000002du, 0x00000006u, 0x00000017u, 0x0004001cu, 0x0000002eu, 0x00000006u, + 0x00000017u, 0x00030016u, 0x0000002fu, 0x00000020u, 0x00040017u, 0x00000030u, 0x0000002fu, 0x00000002u, + 0x0004001cu, 0x00000031u, 0x00000030u, 0x00000017u, 0x0004001cu, 0x00000032u, 0x00000030u, 0x00000017u, + 0x0004002bu, 0x00000006u, 0x00000033u, 0x00000006u, 0x0004001cu, 0x00000034u, 0x00000032u, 0x00000033u, + 0x000d001eu, 0x00000035u, 0x00000006u, 0x00000006u, 0x00000006u, 0x00000006u, 0x00000006u, 0x0000002cu, + 0x0000002du, 0x0000002eu, 0x00000031u, 0x00000034u, 0x00000030u, 0x00040020u, 0x00000036u, 0x0000000cu, + 0x00000035u, 0x0004003bu, 0x00000036u, 0x00000037u, 0x0000000cu, 0x00040015u, 0x00000038u, 0x00000020u, + 0x00000001u, 0x0004002bu, 0x00000038u, 0x00000039u, 0x00000002u, 0x00040020u, 0x0000003bu, 0x0000000cu, + 0x00000006u, 0x0004003bu, 0x00000009u, 0x00000050u, 0x00000001u, 0x0004002bu, 0x00000006u, 0x0000005cu, + 0x00000004u, 0x0004003bu, 0x00000009u, 0x0000005eu, 0x00000001u, 0x0004003bu, 0x00000009u, 0x00000064u, + 0x00000001u, 0x0004002bu, 0x00000006u, 0x0000006bu, 0x00000008u, 0x00040020u, 0x0000006du, 0x00000007u, + 0x0000000eu, 0x0004002bu, 0x00000038u, 0x00000099u, 0x00000004u, 0x0004002bu, 0x00000038u, 0x000000a0u, + 0x00000006u, 0x0004002bu, 0x00000038u, 0x000000a6u, 0x00000007u, 0x00040020u, 0x000000b6u, 0x00000007u, + 0x00000030u, 0x0004002bu, 0x0000002fu, 0x000000bbu, 0x00000000u, 0x0004002bu, 0x00000006u, 0x000000beu, + 0x00000003u, 0x0004001cu, 0x000000c6u, 0x00000030u, 0x00000017u, 0x00040020u, 0x000000c7u, 0x00000004u, + 0x000000c6u, 0x0004003bu, 0x000000c7u, 0x000000c8u, 0x00000004u, 0x00040020u, 0x000000cbu, 0x00000004u, + 0x00000030u, 0x0004002bu, 0x00000038u, 0x000000d2u, 0x00000008u, 0x00040020u, 0x000000d7u, 0x0000000cu, + 0x00000030u, 0x0004002bu, 0x00000038u, 0x000000eau, 0x00000003u, 0x0004002bu, 0x00000038u, 0x00000115u, + 0x00000009u, 0x0004002bu, 0x00000038u, 0x0000011du, 0x00000001u, 0x0004002bu, 0x00000006u, 0x00000120u, + 0x000001ffu, 0x0004002bu, 0x00000038u, 0x00000124u, 0x00000000u, 0x0004002bu, 0x0000002fu, 0x00000126u, + 0x44000000u, 0x0004002bu, 0x00000038u, 0x0000012eu, 0x00000005u, 0x00040020u, 0x00000134u, 0x0000000cu, + 0x0000002cu, 0x0004002bu, 0x00000038u, 0x00000136u, 0x0000000au, 0x0004002bu, 0x00000006u, 0x00000140u, + 0x50323033u, 0x00040017u, 0x00000142u, 0x00000006u, 0x00000003u, 0x0004002bu, 0x00000006u, 0x00000143u, + 0x00000010u, 0x0006002cu, 0x00000142u, 0x00000144u, 0x00000017u, 0x00000143u, 0x00000023u, 0x00050036u, + 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003bu, 0x00000007u, + 0x00000008u, 0x00000007u, 0x0004003bu, 0x00000007u, 0x0000003du, 0x00000007u, 0x0004003bu, 0x0000006du, + 0x0000006eu, 0x00000007u, 0x0004003bu, 0x0000006du, 0x00000074u, 0x00000007u, 0x0004003bu, 0x0000006du, + 0x000000acu, 0x00000007u, 0x0004003bu, 0x000000b6u, 0x000000b7u, 0x00000007u, 0x0004003bu, 0x00000007u, + 0x000000d9u, 0x00000007u, 0x0004003bu, 0x00000007u, 0x000000edu, 0x00000007u, 0x0004003du, 0x00000006u, + 0x0000000bu, 0x0000000au, 0x0003003eu, 0x00000008u, 0x0000000bu, 0x0004003du, 0x00000006u, 0x0000000cu, + 0x00000008u, 0x000500aau, 0x0000000eu, 0x0000000fu, 0x0000000cu, 0x0000000du, 0x000300f7u, 0x00000011u, + 0x00000000u, 0x000400fau, 0x0000000fu, 0x00000010u, 0x00000011u, 0x000200f8u, 0x00000010u, 0x0003003eu, + 0x00000013u, 0x0000000du, 0x0003003eu, 0x00000014u, 0x0000000du, 0x0003003eu, 0x00000015u, 0x0000000du, + 0x000200f9u, 0x00000011u, 0x000200f8u, 0x00000011u, 0x0004003du, 0x00000006u, 0x00000016u, 0x00000008u, + 0x000500b0u, 0x0000000eu, 0x00000018u, 0x00000016u, 0x00000017u, 0x000300f7u, 0x0000001au, 0x00000000u, + 0x000400fau, 0x00000018u, 0x00000019u, 0x0000001au, 0x000200f8u, 0x00000019u, 0x0004003du, 0x00000006u, + 0x0000001eu, 0x00000008u, 0x00050041u, 0x00000012u, 0x0000001fu, 0x0000001du, 0x0000001eu, 0x0003003eu, + 0x0000001fu, 0x0000000du, 0x0004003du, 0x00000006u, 0x00000021u, 0x00000008u, 0x00050041u, 0x00000012u, + 0x00000022u, 0x00000020u, 0x00000021u, 0x0003003eu, 0x00000022u, 0x0000000du, 0x000200f9u, 0x0000001au, + 0x000200f8u, 0x0000001au, 0x000300e1u, 0x00000023u, 0x00000024u, 0x000400e0u, 0x00000025u, 0x00000025u, + 0x00000024u, 0x0004003du, 0x00000006u, 0x00000026u, 0x00000008u, 0x000500aau, 0x0000000eu, 0x00000027u, + 0x00000026u, 0x0000000du, 0x000300f7u, 0x00000029u, 0x00000000u, 0x000400fau, 0x00000027u, 0x00000028u, + 0x00000029u, 0x000200f8u, 0x00000028u, 0x0004003du, 0x00000006u, 0x0000002bu, 0x0000002au, 0x0003003eu, + 0x00000013u, 0x0000002bu, 0x0004003du, 0x00000006u, 0x0000003au, 0x0000002au, 0x00050041u, 0x0000003bu, + 0x0000003cu, 0x00000037u, 0x00000039u, 0x0003003eu, 0x0000003cu, 0x0000003au, 0x000200f9u, 0x00000029u, + 0x000200f8u, 0x00000029u, 0x000400e0u, 0x00000025u, 0x00000025u, 0x00000024u, 0x0004003du, 0x00000006u, + 0x0000003eu, 0x00000013u, 0x0003003eu, 0x0000003du, 0x0000003eu, 0x0004003du, 0x00000006u, 0x0000003fu, + 0x0000002au, 0x0004003du, 0x00000006u, 0x00000040u, 0x0000003du, 0x000500abu, 0x0000000eu, 0x00000041u, + 0x0000003fu, 0x00000040u, 0x000300f7u, 0x00000043u, 0x00000000u, 0x000400fau, 0x00000041u, 0x00000042u, + 0x00000043u, 0x000200f8u, 0x00000042u, 0x000700f1u, 0x00000006u, 0x00000044u, 0x00000014u, 0x00000023u, + 0x0000000du, 0x00000023u, 0x000200f9u, 0x00000043u, 0x000200f8u, 0x00000043u, 0x0004003du, 0x00000006u, + 0x00000045u, 0x0000002au, 0x000500b0u, 0x0000000eu, 0x00000046u, 0x00000045u, 0x00000025u, 0x000400a8u, + 0x0000000eu, 0x00000047u, 0x00000046u, 0x000300f7u, 0x00000049u, 0x00000000u, 0x000400fau, 0x00000047u, + 0x00000048u, 0x00000049u, 0x000200f8u, 0x00000048u, 0x0004003du, 0x00000006u, 0x0000004au, 0x0000002au, + 0x000500acu, 0x0000000eu, 0x0000004bu, 0x0000004au, 0x00000017u, 0x000200f9u, 0x00000049u, 0x000200f8u, + 0x00000049u, 0x000700f5u, 0x0000000eu, 0x0000004cu, 0x00000046u, 0x00000043u, 0x0000004bu, 0x00000048u, + 0x000300f7u, 0x0000004eu, 0x00000000u, 0x000400fau, 0x0000004cu, 0x0000004du, 0x0000004eu, 0x000200f8u, + 0x0000004du, 0x000700f1u, 0x00000006u, 0x0000004fu, 0x00000014u, 0x00000023u, 0x0000000du, 0x00000025u, + 0x000200f9u, 0x0000004eu, 0x000200f8u, 0x0000004eu, 0x0004003du, 0x00000006u, 0x00000051u, 0x00000050u, + 0x0004003du, 0x00000006u, 0x00000052u, 0x0000003du, 0x000500aeu, 0x0000000eu, 0x00000053u, 0x00000051u, + 0x00000052u, 0x000400a8u, 0x0000000eu, 0x00000054u, 0x00000053u, 0x000300f7u, 0x00000056u, 0x00000000u, + 0x000400fau, 0x00000054u, 0x00000055u, 0x00000056u, 0x000200f8u, 0x00000055u, 0x0004003du, 0x00000006u, + 0x00000057u, 0x00000050u, 0x000500aeu, 0x0000000eu, 0x00000058u, 0x00000057u, 0x00000017u, 0x000200f9u, + 0x00000056u, 0x000200f8u, 0x00000056u, 0x000700f5u, 0x0000000eu, 0x00000059u, 0x00000053u, 0x0000004eu, + 0x00000058u, 0x00000055u, 0x000300f7u, 0x0000005bu, 0x00000000u, 0x000400fau, 0x00000059u, 0x0000005au, + 0x0000005bu, 0x000200f8u, 0x0000005au, 0x000700f1u, 0x00000006u, 0x0000005du, 0x00000014u, 0x00000023u, + 0x0000000du, 0x0000005cu, 0x000200f9u, 0x0000005bu, 0x000200f8u, 0x0000005bu, 0x0004003du, 0x00000006u, + 0x0000005fu, 0x0000005eu, 0x000500aau, 0x0000000eu, 0x00000060u, 0x0000005fu, 0x0000000du, 0x000400a8u, + 0x0000000eu, 0x00000061u, 0x00000060u, 0x000300f7u, 0x00000063u, 0x00000000u, 0x000400fau, 0x00000061u, + 0x00000062u, 0x00000063u, 0x000200f8u, 0x00000062u, 0x0004003du, 0x00000006u, 0x00000065u, 0x00000064u, + 0x0004003du, 0x00000006u, 0x00000066u, 0x0000005eu, 0x000500aeu, 0x0000000eu, 0x00000067u, 0x00000065u, + 0x00000066u, 0x000200f9u, 0x00000063u, 0x000200f8u, 0x00000063u, 0x000700f5u, 0x0000000eu, 0x00000068u, + 0x00000060u, 0x0000005bu, 0x00000067u, 0x00000062u, 0x000300f7u, 0x0000006au, 0x00000000u, 0x000400fau, + 0x00000068u, 0x00000069u, 0x0000006au, 0x000200f8u, 0x00000069u, 0x000700f1u, 0x00000006u, 0x0000006cu, + 0x00000014u, 0x00000023u, 0x0000000du, 0x0000006bu, 0x000200f9u, 0x0000006au, 0x000200f8u, 0x0000006au, + 0x0004003du, 0x00000006u, 0x0000006fu, 0x0000003du, 0x000500aeu, 0x0000000eu, 0x00000070u, 0x0000006fu, + 0x00000025u, 0x0004003du, 0x00000006u, 0x00000071u, 0x0000003du, 0x000500b2u, 0x0000000eu, 0x00000072u, + 0x00000071u, 0x00000017u, 0x000500a7u, 0x0000000eu, 0x00000073u, 0x00000070u, 0x00000072u, 0x0003003eu, + 0x0000006eu, 0x00000073u, 0x0004003du, 0x0000000eu, 0x00000075u, 0x0000006eu, 0x000300f7u, 0x00000077u, + 0x00000000u, 0x000400fau, 0x00000075u, 0x00000076u, 0x00000077u, 0x000200f8u, 0x00000076u, 0x0004003du, + 0x00000006u, 0x00000078u, 0x00000050u, 0x0004003du, 0x00000006u, 0x00000079u, 0x0000003du, 0x000500b0u, + 0x0000000eu, 0x0000007au, 0x00000078u, 0x00000079u, 0x000200f9u, 0x00000077u, 0x000200f8u, 0x00000077u, + 0x000700f5u, 0x0000000eu, 0x0000007bu, 0x00000075u, 0x0000006au, 0x0000007au, 0x00000076u, 0x0003003eu, + 0x00000074u, 0x0000007bu, 0x0004003du, 0x0000000eu, 0x0000007cu, 0x00000074u, 0x000300f7u, 0x0000007eu, + 0x00000000u, 0x000400fau, 0x0000007cu, 0x0000007du, 0x0000007eu, 0x000200f8u, 0x0000007du, 0x0004003du, + 0x00000006u, 0x0000007fu, 0x00000050u, 0x000500c4u, 0x00000006u, 0x00000080u, 0x00000023u, 0x0000007fu, + 0x000700f1u, 0x00000006u, 0x00000081u, 0x00000015u, 0x00000023u, 0x0000000du, 0x00000080u, 0x0004003du, + 0x00000006u, 0x00000082u, 0x00000050u, 0x00050041u, 0x00000012u, 0x00000083u, 0x00000020u, 0x00000082u, + 0x0004003du, 0x00000006u, 0x00000084u, 0x00000008u, 0x00050080u, 0x00000006u, 0x00000085u, 0x00000084u, + 0x00000023u, 0x000700eau, 0x00000006u, 0x00000086u, 0x00000083u, 0x00000023u, 0x0000000du, 0x00000085u, + 0x0004003du, 0x00000006u, 0x00000087u, 0x0000005eu, 0x000500abu, 0x0000000eu, 0x00000088u, 0x00000087u, + 0x0000000du, 0x000300f7u, 0x0000008au, 0x00000000u, 0x000400fau, 0x00000088u, 0x00000089u, 0x0000008au, + 0x000200f8u, 0x00000089u, 0x0004003du, 0x00000006u, 0x0000008bu, 0x00000064u, 0x0004003du, 0x00000006u, + 0x0000008cu, 0x0000005eu, 0x00050082u, 0x00000006u, 0x0000008du, 0x0000008cu, 0x00000023u, 0x000500aau, + 0x0000000eu, 0x0000008eu, 0x0000008bu, 0x0000008du, 0x000200f9u, 0x0000008au, 0x000200f8u, 0x0000008au, + 0x000700f5u, 0x0000000eu, 0x0000008fu, 0x00000088u, 0x0000007du, 0x0000008eu, 0x00000089u, 0x000300f7u, + 0x00000091u, 0x00000000u, 0x000400fau, 0x0000008fu, 0x00000090u, 0x00000091u, 0x000200f8u, 0x00000090u, + 0x0004003du, 0x00000006u, 0x00000092u, 0x00000050u, 0x00050041u, 0x00000012u, 0x00000093u, 0x0000001du, + 0x00000092u, 0x000700eau, 0x00000006u, 0x00000094u, 0x00000093u, 0x00000023u, 0x0000000du, 0x00000023u, + 0x000200f9u, 0x00000091u, 0x000200f8u, 0x00000091u, 0x000200f9u, 0x0000007eu, 0x000200f8u, 0x0000007eu, + 0x000300e1u, 0x00000023u, 0x00000024u, 0x000400e0u, 0x00000025u, 0x00000025u, 0x00000024u, 0x0004003du, + 0x00000006u, 0x00000095u, 0x00000008u, 0x000500aau, 0x0000000eu, 0x00000096u, 0x00000095u, 0x0000000du, + 0x000300f7u, 0x00000098u, 0x00000000u, 0x000400fau, 0x00000096u, 0x00000097u, 0x00000098u, 0x000200f8u, + 0x00000097u, 0x0004003du, 0x00000006u, 0x0000009au, 0x00000015u, 0x00050041u, 0x0000003bu, 0x0000009bu, + 0x00000037u, 0x00000099u, 0x0003003eu, 0x0000009bu, 0x0000009au, 0x000200f9u, 0x00000098u, 0x000200f8u, + 0x00000098u, 0x0004003du, 0x00000006u, 0x0000009cu, 0x00000008u, 0x000500b0u, 0x0000000eu, 0x0000009du, + 0x0000009cu, 0x00000017u, 0x000300f7u, 0x0000009fu, 0x00000000u, 0x000400fau, 0x0000009du, 0x0000009eu, + 0x0000009fu, 0x000200f8u, 0x0000009eu, 0x0004003du, 0x00000006u, 0x000000a1u, 0x00000008u, 0x0004003du, + 0x00000006u, 0x000000a2u, 0x00000008u, 0x00050041u, 0x00000012u, 0x000000a3u, 0x0000001du, 0x000000a2u, + 0x0004003du, 0x00000006u, 0x000000a4u, 0x000000a3u, 0x00060041u, 0x0000003bu, 0x000000a5u, 0x00000037u, + 0x000000a0u, 0x000000a1u, 0x0003003eu, 0x000000a5u, 0x000000a4u, 0x0004003du, 0x00000006u, 0x000000a7u, + 0x00000008u, 0x0004003du, 0x00000006u, 0x000000a8u, 0x00000008u, 0x00050041u, 0x00000012u, 0x000000a9u, + 0x00000020u, 0x000000a8u, 0x0004003du, 0x00000006u, 0x000000aau, 0x000000a9u, 0x00060041u, 0x0000003bu, + 0x000000abu, 0x00000037u, 0x000000a6u, 0x000000a7u, 0x0003003eu, 0x000000abu, 0x000000aau, 0x000200f9u, + 0x0000009fu, 0x000200f8u, 0x0000009fu, 0x0004003du, 0x0000000eu, 0x000000adu, 0x0000006eu, 0x000300f7u, + 0x000000afu, 0x00000000u, 0x000400fau, 0x000000adu, 0x000000aeu, 0x000000afu, 0x000200f8u, 0x000000aeu, + 0x0004003du, 0x00000006u, 0x000000b0u, 0x00000014u, 0x000500aau, 0x0000000eu, 0x000000b1u, 0x000000b0u, + 0x0000000du, 0x000200f9u, 0x000000afu, 0x000200f8u, 0x000000afu, 0x000700f5u, 0x0000000eu, 0x000000b2u, + 0x000000adu, 0x0000009fu, 0x000000b1u, 0x000000aeu, 0x0003003eu, 0x000000acu, 0x000000b2u, 0x0004003du, + 0x0000000eu, 0x000000b3u, 0x000000acu, 0x000300f7u, 0x000000b5u, 0x00000000u, 0x000400fau, 0x000000b3u, + 0x000000b4u, 0x000000b5u, 0x000200f8u, 0x000000b4u, 0x0004003du, 0x00000006u, 0x000000b8u, 0x0000000au, + 0x00050080u, 0x00000006u, 0x000000b9u, 0x000000b8u, 0x00000023u, 0x00040070u, 0x0000002fu, 0x000000bau, + 0x000000b9u, 0x00050050u, 0x00000030u, 0x000000bcu, 0x000000bau, 0x000000bbu, 0x0003003eu, 0x000000b7u, + 0x000000bcu, 0x0004003du, 0x00000030u, 0x000000bdu, 0x000000b7u, 0x0006015eu, 0x00000030u, 0x000000bfu, + 0x000000beu, 0x00000001u, 0x000000bdu, 0x0003003eu, 0x000000b7u, 0x000000bfu, 0x0004003du, 0x00000006u, + 0x000000c0u, 0x00000064u, 0x0004003du, 0x00000006u, 0x000000c1u, 0x0000005eu, 0x00050082u, 0x00000006u, + 0x000000c2u, 0x000000c1u, 0x00000023u, 0x000500aau, 0x0000000eu, 0x000000c3u, 0x000000c0u, 0x000000c2u, + 0x000300f7u, 0x000000c5u, 0x00000000u, 0x000400fau, 0x000000c3u, 0x000000c4u, 0x000000c5u, 0x000200f8u, + 0x000000c4u, 0x0004003du, 0x00000006u, 0x000000c9u, 0x00000050u, 0x0004003du, 0x00000030u, 0x000000cau, + 0x000000b7u, 0x00050041u, 0x000000cbu, 0x000000ccu, 0x000000c8u, 0x000000c9u, 0x0003003eu, 0x000000ccu, + 0x000000cau, 0x000200f9u, 0x000000c5u, 0x000200f8u, 0x000000c5u, 0x000400e0u, 0x00000025u, 0x00000025u, + 0x00000024u, 0x0004003du, 0x00000006u, 0x000000cdu, 0x0000000au, 0x0004003du, 0x00000006u, 0x000000ceu, + 0x0000002au, 0x000500b0u, 0x0000000eu, 0x000000cfu, 0x000000cdu, 0x000000ceu, 0x000300f7u, 0x000000d1u, + 0x00000000u, 0x000400fau, 0x000000cfu, 0x000000d0u, 0x000000d1u, 0x000200f8u, 0x000000d0u, 0x0004003du, + 0x00000006u, 0x000000d3u, 0x0000000au, 0x0004003du, 0x00000006u, 0x000000d4u, 0x0000000au, 0x00050041u, + 0x000000cbu, 0x000000d5u, 0x000000c8u, 0x000000d4u, 0x0004003du, 0x00000030u, 0x000000d6u, 0x000000d5u, + 0x00060041u, 0x000000d7u, 0x000000d8u, 0x00000037u, 0x000000d2u, 0x000000d3u, 0x0003003eu, 0x000000d8u, + 0x000000d6u, 0x000200f9u, 0x000000d1u, 0x000200f8u, 0x000000d1u, 0x000400e0u, 0x00000025u, 0x00000025u, + 0x00000024u, 0x0004003du, 0x00000006u, 0x000000dau, 0x0000002au, 0x0006000cu, 0x00000038u, 0x000000dbu, + 0x00000001u, 0x0000004bu, 0x000000dau, 0x0004007cu, 0x00000006u, 0x000000dcu, 0x000000dbu, 0x0003003eu, + 0x000000d9u, 0x000000dcu, 0x0004003du, 0x00000006u, 0x000000ddu, 0x0000002au, 0x0004003du, 0x00000006u, + 0x000000deu, 0x000000d9u, 0x00050082u, 0x00000006u, 0x000000dfu, 0x000000deu, 0x00000023u, 0x000500c4u, + 0x00000006u, 0x000000e0u, 0x00000023u, 0x000000dfu, 0x00050082u, 0x00000006u, 0x000000e1u, 0x000000ddu, + 0x000000e0u, 0x000500acu, 0x0000000eu, 0x000000e2u, 0x000000e1u, 0x0000000du, 0x000600a9u, 0x00000006u, + 0x000000e3u, 0x000000e2u, 0x00000023u, 0x0000000du, 0x0004003du, 0x00000006u, 0x000000e4u, 0x000000d9u, + 0x00050080u, 0x00000006u, 0x000000e5u, 0x000000e4u, 0x000000e3u, 0x0003003eu, 0x000000d9u, 0x000000e5u, + 0x0004003du, 0x00000006u, 0x000000e6u, 0x0000000au, 0x000500aau, 0x0000000eu, 0x000000e7u, 0x000000e6u, + 0x0000000du, 0x000300f7u, 0x000000e9u, 0x00000000u, 0x000400fau, 0x000000e7u, 0x000000e8u, 0x000000e9u, + 0x000200f8u, 0x000000e8u, 0x0004003du, 0x00000006u, 0x000000ebu, 0x000000d9u, 0x00050041u, 0x0000003bu, + 0x000000ecu, 0x00000037u, 0x000000eau, 0x0003003eu, 0x000000ecu, 0x000000ebu, 0x000200f9u, 0x000000e9u, + 0x000200f8u, 0x000000e9u, 0x0003003eu, 0x000000edu, 0x0000000du, 0x000200f9u, 0x000000eeu, 0x000200f8u, + 0x000000eeu, 0x000400f6u, 0x000000f0u, 0x000000f1u, 0x00000000u, 0x000200f9u, 0x000000f2u, 0x000200f8u, + 0x000000f2u, 0x0004003du, 0x00000006u, 0x000000f3u, 0x000000edu, 0x0004003du, 0x00000006u, 0x000000f4u, + 0x000000d9u, 0x000500b0u, 0x0000000eu, 0x000000f5u, 0x000000f3u, 0x000000f4u, 0x000400fau, 0x000000f5u, + 0x000000efu, 0x000000f0u, 0x000200f8u, 0x000000efu, 0x0004003du, 0x00000006u, 0x000000f6u, 0x00000050u, + 0x0004003du, 0x00000006u, 0x000000f7u, 0x000000edu, 0x000500c4u, 0x00000006u, 0x000000f8u, 0x00000023u, + 0x000000f7u, 0x000500c7u, 0x00000006u, 0x000000f9u, 0x000000f6u, 0x000000f8u, 0x000500acu, 0x0000000eu, + 0x000000fau, 0x000000f9u, 0x0000000du, 0x000300f7u, 0x000000fcu, 0x00000000u, 0x000400fau, 0x000000fau, + 0x000000fbu, 0x000000fcu, 0x000200f8u, 0x000000fbu, 0x0004003du, 0x00000006u, 0x000000fdu, 0x00000050u, + 0x0004003du, 0x00000006u, 0x000000feu, 0x000000edu, 0x000500c2u, 0x00000006u, 0x000000ffu, 0x000000fdu, + 0x000000feu, 0x0004003du, 0x00000006u, 0x00000100u, 0x000000edu, 0x000500c4u, 0x00000006u, 0x00000101u, + 0x000000ffu, 0x00000100u, 0x00050082u, 0x00000006u, 0x00000102u, 0x00000101u, 0x00000023u, 0x00050041u, + 0x000000cbu, 0x00000103u, 0x000000c8u, 0x00000102u, 0x0004003du, 0x00000030u, 0x00000104u, 0x00000103u, + 0x0004003du, 0x00000030u, 0x00000105u, 0x000000b7u, 0x00050081u, 0x00000030u, 0x00000106u, 0x00000105u, + 0x00000104u, 0x0003003eu, 0x000000b7u, 0x00000106u, 0x0004003du, 0x00000006u, 0x00000107u, 0x00000064u, + 0x0004003du, 0x00000006u, 0x00000108u, 0x0000005eu, 0x00050082u, 0x00000006u, 0x00000109u, 0x00000108u, + 0x00000023u, 0x000500aau, 0x0000000eu, 0x0000010au, 0x00000107u, 0x00000109u, 0x000300f7u, 0x0000010cu, + 0x00000000u, 0x000400fau, 0x0000010au, 0x0000010bu, 0x0000010cu, 0x000200f8u, 0x0000010bu, 0x0004003du, + 0x00000006u, 0x0000010du, 0x00000050u, 0x0004003du, 0x00000030u, 0x0000010eu, 0x000000b7u, 0x00050041u, + 0x000000cbu, 0x0000010fu, 0x000000c8u, 0x0000010du, 0x0003003eu, 0x0000010fu, 0x0000010eu, 0x000200f9u, + 0x0000010cu, 0x000200f8u, 0x0000010cu, 0x000200f9u, 0x000000fcu, 0x000200f8u, 0x000000fcu, 0x000400e0u, + 0x00000025u, 0x00000025u, 0x00000024u, 0x0004003du, 0x00000006u, 0x00000110u, 0x0000000au, 0x0004003du, + 0x00000006u, 0x00000111u, 0x0000002au, 0x000500b0u, 0x0000000eu, 0x00000112u, 0x00000110u, 0x00000111u, + 0x000300f7u, 0x00000114u, 0x00000000u, 0x000400fau, 0x00000112u, 0x00000113u, 0x00000114u, 0x000200f8u, + 0x00000113u, 0x0004003du, 0x00000006u, 0x00000116u, 0x000000edu, 0x0004003du, 0x00000006u, 0x00000117u, + 0x0000000au, 0x0004003du, 0x00000006u, 0x00000118u, 0x0000000au, 0x00050041u, 0x000000cbu, 0x00000119u, + 0x000000c8u, 0x00000118u, 0x0004003du, 0x00000030u, 0x0000011au, 0x00000119u, 0x00070041u, 0x000000d7u, + 0x0000011bu, 0x00000037u, 0x00000115u, 0x00000116u, 0x00000117u, 0x0003003eu, 0x0000011bu, 0x0000011au, + 0x000200f9u, 0x00000114u, 0x000200f8u, 0x00000114u, 0x000400e0u, 0x00000025u, 0x00000025u, 0x00000024u, + 0x000200f9u, 0x000000f1u, 0x000200f8u, 0x000000f1u, 0x0004003du, 0x00000006u, 0x0000011cu, 0x000000edu, + 0x00050080u, 0x00000006u, 0x0000011eu, 0x0000011cu, 0x0000011du, 0x0003003eu, 0x000000edu, 0x0000011eu, + 0x000200f9u, 0x000000eeu, 0x000200f8u, 0x000000f0u, 0x0004003du, 0x00000006u, 0x0000011fu, 0x0000000au, + 0x000500aau, 0x0000000eu, 0x00000121u, 0x0000011fu, 0x00000120u, 0x000300f7u, 0x00000123u, 0x00000000u, + 0x000400fau, 0x00000121u, 0x00000122u, 0x00000123u, 0x000200f8u, 0x00000122u, 0x0004003du, 0x00000030u, + 0x00000125u, 0x000000b7u, 0x00050050u, 0x00000030u, 0x00000127u, 0x00000126u, 0x00000126u, 0x00050088u, + 0x00000030u, 0x00000128u, 0x00000125u, 0x00000127u, 0x00050041u, 0x000000cbu, 0x00000129u, 0x000000c8u, + 0x00000124u, 0x0003003eu, 0x00000129u, 0x00000128u, 0x000200f9u, 0x00000123u, 0x000200f8u, 0x00000123u, + 0x000400e0u, 0x00000025u, 0x00000025u, 0x00000024u, 0x0004003du, 0x00000006u, 0x0000012au, 0x0000000au, + 0x000500aau, 0x0000000eu, 0x0000012bu, 0x0000012au, 0x00000120u, 0x000300f7u, 0x0000012du, 0x00000000u, + 0x000400fau, 0x0000012bu, 0x0000012cu, 0x0000012du, 0x000200f8u, 0x0000012cu, 0x0004003du, 0x00000006u, + 0x0000012fu, 0x0000005eu, 0x0004003du, 0x00000006u, 0x00000130u, 0x0000002au, 0x0004003du, 0x00000006u, + 0x00000131u, 0x00000050u, 0x0004003du, 0x00000006u, 0x00000132u, 0x00000064u, 0x00070050u, 0x0000002cu, + 0x00000133u, 0x0000012fu, 0x00000130u, 0x00000131u, 0x00000132u, 0x00050041u, 0x00000134u, 0x00000135u, + 0x00000037u, 0x0000012eu, 0x0003003eu, 0x00000135u, 0x00000133u, 0x00050041u, 0x000000cbu, 0x00000137u, + 0x000000c8u, 0x00000124u, 0x0004003du, 0x00000030u, 0x00000138u, 0x00000137u, 0x00050041u, 0x000000d7u, + 0x00000139u, 0x00000037u, 0x00000136u, 0x0003003eu, 0x00000139u, 0x00000138u, 0x000200f9u, 0x0000012du, + 0x000200f8u, 0x0000012du, 0x000200f9u, 0x000000b5u, 0x000200f8u, 0x000000b5u, 0x000400e0u, 0x00000025u, + 0x00000025u, 0x00000024u, 0x0004003du, 0x00000006u, 0x0000013au, 0x00000008u, 0x000500aau, 0x0000000eu, + 0x0000013bu, 0x0000013au, 0x0000000du, 0x000300f7u, 0x0000013du, 0x00000000u, 0x000400fau, 0x0000013bu, + 0x0000013cu, 0x0000013du, 0x000200f8u, 0x0000013cu, 0x0004003du, 0x00000006u, 0x0000013eu, 0x00000014u, + 0x00050041u, 0x0000003bu, 0x0000013fu, 0x00000037u, 0x0000011du, 0x0003003eu, 0x0000013fu, 0x0000013eu, + 0x00050041u, 0x0000003bu, 0x00000141u, 0x00000037u, 0x00000124u, 0x0003003eu, 0x00000141u, 0x00000140u, + 0x000200f9u, 0x0000013du, 0x000200f8u, 0x0000013du, 0x000100fdu, 0x00010038u, + }; + inline constexpr std::size_t kDriverPostProgram203WitnessSpvWordCount = + sizeof(kDriverPostProgram203WitnessSpv) / sizeof(kDriverPostProgram203WitnessSpv[0]); +} // namespace MobileGL::MG_Util::SelfTest diff --git a/android-plugin/app/src/trace/cpp/CMakeLists.txt b/android-plugin/app/src/trace/cpp/CMakeLists.txt index 5effef40..fa97470a 100644 --- a/android-plugin/app/src/trace/cpp/CMakeLists.txt +++ b/android-plugin/app/src/trace/cpp/CMakeLists.txt @@ -223,7 +223,8 @@ target_include_directories(glretrace_common PUBLIC "${APITRACE_GENERATED_DIR}" "${APITRACE_ROOT}/dispatch" "${APITRACE_ROOT}/helpers" - "${APITRACE_ROOT}/retrace") + "${APITRACE_ROOT}/retrace" + "${CMAKE_CURRENT_LIST_DIR}/../../../../../tools/trace_replay") target_compile_definitions(glretrace_common PRIVATE main=mobilegl_apitrace_main) target_redirect_exit(glretrace_common) @@ -231,14 +232,16 @@ target_link_libraries(glretrace_common PUBLIC retrace_common glhelpers glproc) add_library(trace_replay_runner SHARED trace_replay_core.cpp - trace_replay_jni.cpp) + trace_replay_jni.cpp + "${CMAKE_CURRENT_LIST_DIR}/../../../../../tools/trace_replay/apitrace_fbo_dump.cpp") target_compile_features(trace_replay_runner PRIVATE cxx_std_17) target_compile_definitions(trace_replay_runner PRIVATE MOBILEGL_APITRACE_RETRACE_MAIN=mobilegl_apitrace_main) target_include_directories(trace_replay_runner PRIVATE - "${APITRACE_ROOT}/lib/image") + "${APITRACE_ROOT}/lib/image" + "${CMAKE_CURRENT_LIST_DIR}/../../../../../tools/trace_replay") target_link_libraries(trace_replay_runner glretrace_common diff --git a/android-plugin/app/src/trace/cpp/apitrace_glws_android.cpp b/android-plugin/app/src/trace/cpp/apitrace_glws_android.cpp index 625e18ef..b02b7605 100644 --- a/android-plugin/app/src/trace/cpp/apitrace_glws_android.cpp +++ b/android-plugin/app/src/trace/cpp/apitrace_glws_android.cpp @@ -1,3 +1,4 @@ +#include "apitrace_fbo_dump.hpp" #include "glws.hpp" #include "retrace.hpp" @@ -375,6 +376,7 @@ bool makeCurrentInternal(Drawable *drawable, Drawable *readable, Context *contex } gCurrentDrawable = drawable; gCurrentContext = eglContext; + mobilegl_trace_dump::InstallIfRequested(); return true; } diff --git a/android-plugin/app/src/trace/cpp/trace_replay_core.cpp b/android-plugin/app/src/trace/cpp/trace_replay_core.cpp index b67d8b35..151fa98c 100644 --- a/android-plugin/app/src/trace/cpp/trace_replay_core.cpp +++ b/android-plugin/app/src/trace/cpp/trace_replay_core.cpp @@ -176,6 +176,18 @@ bool LoadMobileGL(const Request& request, std::string& error) { } setenv("MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS", dumpPoints.c_str(), 1); } + if (request.texture2dDumps.empty()) { + unsetenv("MOBILEGL_TRACE_DUMP_TEXTURE_2D"); + } else { + std::string dumpPoints; + for (const std::string& dumpPoint : request.texture2dDumps) { + if (!dumpPoints.empty()) { + dumpPoints += ';'; + } + dumpPoints += dumpPoint; + } + setenv("MOBILEGL_TRACE_DUMP_TEXTURE_2D", dumpPoints.c_str(), 1); + } void* handle = dlopen(request.mobileGlLibrary.c_str(), RTLD_NOW | RTLD_GLOBAL); if (handle == nullptr) { @@ -383,6 +395,13 @@ std::string SnapshotCallSet(const Request& request) { callSet += "," + call; } } + for (const std::string& dumpPoint : request.texture2dDumps) { + const std::size_t separator = dumpPoint.find(','); + const std::string call = dumpPoint.substr(0, separator); + if (!call.empty() && call != std::to_string(request.targetCall)) { + callSet += "," + call; + } + } return callSet; } diff --git a/android-plugin/app/src/trace/cpp/trace_replay_core.hpp b/android-plugin/app/src/trace/cpp/trace_replay_core.hpp index 9c81c254..cc550378 100644 --- a/android-plugin/app/src/trace/cpp/trace_replay_core.hpp +++ b/android-plugin/app/src/trace/cpp/trace_replay_core.hpp @@ -27,6 +27,9 @@ struct Request { // Framebuffer-attachment dump points, each `CALL:DIR[:FBO,FBO,...]`. Debug-only; the // replay behaves exactly as before when this is empty. std::vector fboAttachmentDumps; + // Named GL_TEXTURE_2D dump points, each `CALL,TEXTURE,LEVEL,DIR`. Debug-only; the replay + // behaves exactly as before when this is empty. + std::vector texture2dDumps; int targetFrame = -1; long long targetCall = -1; int width = 0; diff --git a/android-plugin/app/src/trace/cpp/trace_replay_jni.cpp b/android-plugin/app/src/trace/cpp/trace_replay_jni.cpp index 88479748..8c6e1a6e 100644 --- a/android-plugin/app/src/trace/cpp/trace_replay_jni.cpp +++ b/android-plugin/app/src/trace/cpp/trace_replay_jni.cpp @@ -6,6 +6,7 @@ #include #include +#include #include extern "C" void mobilegl_trace_set_native_window(ANativeWindow *window); @@ -25,6 +26,23 @@ std::string ToString(JNIEnv* env, jstring value) { return out; } +std::vector SplitSemicolonList(const std::string& value) { + std::vector values; + std::size_t begin = 0; + while (begin < value.size()) { + const std::size_t end = value.find(';', begin); + const std::string entry = value.substr(begin, end - begin); + if (!entry.empty()) { + values.push_back(entry); + } + if (end == std::string::npos) { + break; + } + begin = end + 1; + } + return values; +} + jobject MakeResult(JNIEnv* env, const mobilegl_trace::Result& result) { jclass clazz = env->FindClass("top/mobilegl/plugin/trace/TraceReplayActivity$TraceReplayResult"); if (clazz == nullptr) { @@ -103,7 +121,8 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv* jboolean usePbuffer, jboolean avoidAngleLlvmpipeSamplerMipmapMinFilter, jboolean avoidAngleLlvmpipeExplicitLodBias, - jboolean coherentAsFlush) { + jboolean coherentAsFlush, + jstring texture2dDumps) { mobilegl_trace::Request request; request.tracePath = ToString(env, tracePath); request.goldenPath = ToString(env, goldenPath); @@ -115,6 +134,7 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv* request.diffPath = ToString(env, diffPath); request.backend = ToString(env, backend); request.angleVariant = ToString(env, angleVariant); + request.texture2dDumps = SplitSemicolonList(ToString(env, texture2dDumps)); request.targetFrame = targetFrame; request.targetCall = targetCall; request.width = width; diff --git a/android-plugin/app/src/trace/java/top/mobilegl/plugin/trace/TraceReplayActivity.java b/android-plugin/app/src/trace/java/top/mobilegl/plugin/trace/TraceReplayActivity.java index 0ab48d67..33e81d28 100644 --- a/android-plugin/app/src/trace/java/top/mobilegl/plugin/trace/TraceReplayActivity.java +++ b/android-plugin/app/src/trace/java/top/mobilegl/plugin/trace/TraceReplayActivity.java @@ -115,7 +115,8 @@ public final class TraceReplayActivity extends Activity { request.usePbuffer, request.avoidAngleLlvmpipeSamplerMipmapMinFilter, request.avoidAngleLlvmpipeExplicitLodBias, - request.coherentAsFlush + request.coherentAsFlush, + request.texture2dDumps ); Log.i(TAG, result.toString()); TraceReplayResult finalResult = result; @@ -147,7 +148,8 @@ public final class TraceReplayActivity extends Activity { boolean usePbuffer, boolean avoidAngleLlvmpipeSamplerMipmapMinFilter, boolean avoidAngleLlvmpipeExplicitLodBias, - boolean coherentAsFlush + boolean coherentAsFlush, + String texture2dDumps ); private static final class TraceReplayRequest { @@ -172,6 +174,7 @@ public final class TraceReplayActivity extends Activity { final boolean avoidAngleLlvmpipeSamplerMipmapMinFilter; final boolean avoidAngleLlvmpipeExplicitLodBias; final boolean coherentAsFlush; + final String texture2dDumps; private TraceReplayRequest( String tracePath, @@ -194,7 +197,8 @@ public final class TraceReplayActivity extends Activity { boolean usePbuffer, boolean avoidAngleLlvmpipeSamplerMipmapMinFilter, boolean avoidAngleLlvmpipeExplicitLodBias, - boolean coherentAsFlush + boolean coherentAsFlush, + String texture2dDumps ) { this.tracePath = tracePath; this.goldenPath = goldenPath; @@ -217,6 +221,7 @@ public final class TraceReplayActivity extends Activity { this.avoidAngleLlvmpipeSamplerMipmapMinFilter = avoidAngleLlvmpipeSamplerMipmapMinFilter; this.avoidAngleLlvmpipeExplicitLodBias = avoidAngleLlvmpipeExplicitLodBias; this.coherentAsFlush = coherentAsFlush; + this.texture2dDumps = texture2dDumps; } static TraceReplayRequest from(Intent intent, File filesDir, String defaultBackend) { @@ -243,7 +248,8 @@ public final class TraceReplayActivity extends Activity { intent.getBooleanExtra("use_pbuffer", false), intent.getBooleanExtra("avoid_angle_llvmpipe_sampler_mipmap_min_filter", false), intent.getBooleanExtra("avoid_angle_llvmpipe_explicit_lod_bias", false), - intent.getBooleanExtra("coherent_as_flush", false) + intent.getBooleanExtra("coherent_as_flush", false), + readString(intent, "texture_2d_dumps", "") ); } diff --git a/android-plugin/trace-replay-ci.sh b/android-plugin/trace-replay-ci.sh index 07ca235d..93a9c923 100644 --- a/android-plugin/trace-replay-ci.sh +++ b/android-plugin/trace-replay-ci.sh @@ -31,6 +31,7 @@ Usage: [--avoid-angle-llvmpipe-sampler-mipmap-min-filter] \ [--avoid-angle-llvmpipe-explicit-lod-bias] \ [--coherent-as-flush] \ + [--dump-texture-2d CALL,TEXTURE,LEVEL,DIR] \ --timeout-seconds N Set MOBILEGL_USE_ANGLE=1 to run DirectGLES replay with packaged ANGLE @@ -104,6 +105,7 @@ use_pbuffer=0 avoid_angle_llvmpipe_sampler_mipmap_min_filter=0 avoid_angle_llvmpipe_explicit_lod_bias=0 coherent_as_flush=0 +texture_2d_dumps="" timeout_seconds="" while [ "$#" -gt 0 ]; do @@ -144,6 +146,7 @@ while [ "$#" -gt 0 ]; do shift 1 ;; --coherent-as-flush) coherent_as_flush=1; shift 1 ;; + --dump-texture-2d) texture_2d_dumps="$(next_arg "$@")"; shift 2 ;; --timeout-seconds) timeout_seconds="$(next_arg "$@")"; shift 2 ;; -h|--help) usage; exit 0 ;; *) die "unknown argument: $1" ;; @@ -262,6 +265,27 @@ copy_app_artifact() { fi } +copy_texture_2d_dumps() { + [ -n "${texture_2d_dumps}" ] || return + saved_ifs="${IFS}" + IFS=';' + set -- ${texture_2d_dumps} + IFS="${saved_ifs}" + for dump_point in "$@"; do + dump_dir="${dump_point#*,}" + dump_dir="${dump_dir#*,}" + dump_dir="${dump_dir#*,}" + [ -n "${dump_dir}" ] || continue + dump_name="$(basename "${dump_dir}")" + destination_dir="${result_dir}/${dump_name}" + mkdir -p "${destination_dir}" + if ! adb_device_path exec-out run-as "${package_name}" tar -C "${dump_dir}" -cf - . | tar -xf - -C "${destination_dir}"; then + echo "trace-replay-ci.sh: warning: failed to copy texture dump ${dump_dir}" >&2 + rm -rf "${destination_dir}" + fi + done +} + prepare_fixture() { fixture_dir="${fixture_root}/${safe_case}" rm -rf "${fixture_dir}" @@ -339,6 +363,9 @@ run_retrace() { if [ "${coherent_as_flush}" -eq 1 ]; then set -- "$@" --ez coherent_as_flush true fi + if [ -n "${texture_2d_dumps}" ]; then + set -- "$@" --es texture_2d_dumps "${texture_2d_dumps}" + fi set -- "$@" \ --es output_dir "${app_dir}/output" \ --es diff_path "${app_dir}/output/${safe_case}-diff.png" \ @@ -399,6 +426,7 @@ run_retrace() { copy_app_artifact "${app_dir}/output/${safe_case}-diff.png" "${result_dir}/${safe_case}-${backend}-diff.png" copy_app_artifact "${app_dir}/output/retrace.log" "${result_dir}/retrace.log" copy_app_artifact "${app_dir}/output/mobilegl.log" "${result_dir}/mobilegl.log" + copy_texture_2d_dumps # A replay that wrote result.json but did not pass used to print nothing but # the JSON, which for a non-zero statusCode says only "retrace failed with diff --git a/tools/trace_replay/apitrace_fbo_dump.cpp b/tools/trace_replay/apitrace_fbo_dump.cpp index b0be05e3..b08aa302 100644 --- a/tools/trace_replay/apitrace_fbo_dump.cpp +++ b/tools/trace_replay/apitrace_fbo_dump.cpp @@ -13,8 +13,13 @@ #include #include #include +#include #include +#if defined(_WIN32) +#include +#else #include +#endif #include // Dumps every colour attachment (and the depth attachment) of every live framebuffer @@ -34,6 +39,7 @@ using PfnGetIntegerv = void (*)(GLenum, GLint *); using PfnGetError = GLenum (*)(void); constexpr const char *kDumpPointsEnv = "MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS"; +constexpr const char *kTexture2dDumpPointsEnv = "MOBILEGL_TRACE_DUMP_TEXTURE_2D"; constexpr const char *kScanLimitEnv = "MOBILEGL_TRACE_DUMP_FBO_SCAN_LIMIT"; constexpr unsigned kDefaultScanLimit = 1024; @@ -45,6 +51,16 @@ struct DumpPoint { bool done = false; }; +// CALL,TEXTURE,LEVEL,DIR entries, separated by ';'. This deliberately does not share the +// framebuffer dump grammar: an absolute Windows directory contains ':' but not ','. +struct Texture2dDumpPoint { + unsigned call = 0; + unsigned texture = 0; + unsigned level = 0; + std::string directory; + bool done = false; +}; + struct AttachmentDesc { GLint objectType = GL_NONE; GLint objectName = 0; @@ -56,6 +72,7 @@ struct AttachmentDesc { }; std::vector gDumpPoints; +std::vector gTexture2dDumpPoints; bool gInstalled = false; bool gConfigured = false; retrace::Dumper *gInnerDumper = nullptr; @@ -105,13 +122,27 @@ bool MakeDirectories(const std::string &path) { for (std::size_t i = 0; i < path.size(); ++i) { partial.push_back(path[i]); const bool last = i + 1 == path.size(); - if (path[i] != '/' && !last) { +#if defined(_WIN32) + const bool separator = path[i] == '/' || path[i] == '\\'; +#else + const bool separator = path[i] == '/'; +#endif + if (!separator && !last) { continue; } if (partial == "/") { continue; } +#if defined(_WIN32) + if (separator && partial.size() == 3 && partial[1] == ':') { + continue; + } +#endif +#if defined(_WIN32) + if (_mkdir(partial.c_str()) != 0 && errno != EEXIST) { +#else if (mkdir(partial.c_str(), 0755) != 0 && errno != EEXIST) { +#endif return false; } } @@ -161,6 +192,52 @@ void ParseDumpPoints(const char *spec) { } } +bool IsDecimal(const std::string &value) { + return !value.empty() && value.find_first_not_of("0123456789") == std::string::npos; +} + +void ParseTexture2dDumpPoints(const char *spec) { + for (const std::string &entry : Split(spec, ';')) { + if (entry.empty()) { + continue; + } + const std::vector fields = Split(entry, ','); + if (fields.size() != 4 || !IsDecimal(fields[0]) || !IsDecimal(fields[1]) || + !IsDecimal(fields[2]) || fields[3].empty()) { + std::cerr << "warning: ignoring malformed " << kTexture2dDumpPointsEnv + << " entry: " << entry << "\n"; + continue; + } + + char *end = nullptr; + const unsigned long call = std::strtoul(fields[0].c_str(), &end, 10); + if (*end != '\0' || call > std::numeric_limits::max()) { + std::cerr << "warning: ignoring malformed " << kTexture2dDumpPointsEnv + << " call: " << entry << "\n"; + continue; + } + const unsigned long texture = std::strtoul(fields[1].c_str(), &end, 10); + if (*end != '\0' || texture == 0 || texture > std::numeric_limits::max()) { + std::cerr << "warning: ignoring malformed " << kTexture2dDumpPointsEnv + << " texture: " << entry << "\n"; + continue; + } + const unsigned long level = std::strtoul(fields[2].c_str(), &end, 10); + if (*end != '\0' || level > static_cast(std::numeric_limits::max())) { + std::cerr << "warning: ignoring malformed " << kTexture2dDumpPointsEnv + << " level: " << entry << "\n"; + continue; + } + + Texture2dDumpPoint point; + point.call = static_cast(call); + point.texture = static_cast(texture); + point.level = static_cast(level); + point.directory = fields[3]; + gTexture2dDumpPoints.push_back(point); + } +} + unsigned ScanLimit() { const char *value = std::getenv(kScanLimitEnv); if (value == nullptr || value[0] == '\0') { @@ -235,6 +312,45 @@ bool DescribeAttachment(GLenum attachment, AttachmentDesc &desc) { return desc.width > 0 && desc.height > 0; } +bool DescribeTexture2D(GLuint texture, GLint level, AttachmentDesc &desc) { + const GLint savedTexture = GetInteger(GL_TEXTURE_BINDING_2D); + glBindTexture(GL_TEXTURE_2D, texture); + desc.objectType = GL_TEXTURE; + desc.objectName = static_cast(texture); + desc.level = level; + glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &desc.width); + glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_HEIGHT, &desc.height); + glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_INTERNAL_FORMAT, &desc.internalFormat); + glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_RED_TYPE, &desc.componentType); + glBindTexture(GL_TEXTURE_2D, static_cast(savedTexture)); + return DrainErrors() == 0 && desc.width > 0 && desc.height > 0; +} + +bool ReadTexture2DFloats(const AttachmentDesc &desc, std::vector &pixels) { + const std::size_t count = static_cast(desc.width) * desc.height * 4; + pixels.assign(count, 0.0f); + const GLint savedTexture = GetInteger(GL_TEXTURE_BINDING_2D); + glBindTexture(GL_TEXTURE_2D, static_cast(desc.objectName)); + if (desc.componentType == GL_INT || desc.componentType == GL_UNSIGNED_INT) { + std::vector raw(count, 0); + const GLenum type = desc.componentType == GL_INT ? GL_INT : GL_UNSIGNED_INT; + glGetTexImage(GL_TEXTURE_2D, desc.level, GL_RGBA_INTEGER, type, raw.data()); + glBindTexture(GL_TEXTURE_2D, static_cast(savedTexture)); + if (DrainErrors() != 0) { + return false; + } + for (std::size_t i = 0; i < count; ++i) { + pixels[i] = desc.componentType == GL_INT + ? static_cast(raw[i]) + : static_cast(static_cast(raw[i])); + } + return true; + } + glGetTexImage(GL_TEXTURE_2D, desc.level, GL_RGBA, GL_FLOAT, pixels.data()); + glBindTexture(GL_TEXTURE_2D, static_cast(savedTexture)); + return DrainErrors() == 0; +} + // Reads the attachment as floats regardless of its storage: normalised and float targets // convert on the way out, integer targets are read as integers and widened. The float view // keeps out-of-[0,1] accumulation buffers legible in the statistics even though the PNG @@ -308,6 +424,17 @@ std::string FormatStatistics(const std::vector &pixels, unsigned channels static_cast(minimum[c]), static_cast(maximum[c]), mean); text += buffer; } + if (pixelCount > 0) { + text += " first=("; + for (unsigned c = 0; c < channels; ++c) { + if (c > 0) { + text += ","; + } + std::snprintf(buffer, sizeof(buffer), "%.9g", static_cast(pixels[c])); + text += buffer; + } + text += ")"; + } std::snprintf(buffer, sizeof(buffer), " nonfinite=%zu hash=%016llx", nonFinite, static_cast(hash)); text += buffer; @@ -325,8 +452,8 @@ bool WriteFloatPng(const std::string &path, const AttachmentDesc &desc, unsigned return snapshot.writePNG(path.c_str()); } -void DumpOneAttachment(std::ofstream &manifest, const std::string &directory, unsigned framebuffer, - GLenum attachment, const char *label, bool depth) { +void DumpOneAttachment(std::ofstream &manifest, const std::string &directory, + const std::string &identity, GLenum attachment, const char *label, bool depth) { AttachmentDesc desc; if (!DescribeAttachment(attachment, desc)) { return; @@ -343,14 +470,13 @@ void DumpOneAttachment(std::ofstream &manifest, const std::string &directory, un std::vector pixels; const bool read = ReadAttachmentFloats(desc, depth, channels, pixels); - const std::string path = - directory + "/fbo" + std::to_string(framebuffer) + "-" + label + ".png"; + const std::string path = directory + "/" + identity + "-" + label + ".png"; const bool wrote = read && WriteFloatPng(path, desc, channels, pixels); char header[512]; std::snprintf(header, sizeof(header), - "fbo %u %s object=%s name=%d level=%d size=%dx%d internalformat=0x%04x component=%s", - framebuffer, label, + "%s %s object=%s name=%d level=%d size=%dx%d internalformat=0x%04x component=%s", + identity.c_str(), label, desc.objectType == GL_RENDERBUFFER ? "renderbuffer" : "texture", desc.objectName, desc.level, desc.width, desc.height, static_cast(desc.internalFormat), ComponentTypeName(desc.componentType)); @@ -381,13 +507,14 @@ void DumpFramebuffer(std::ofstream &manifest, const std::string &directory, unsi } const GLint savedReadBuffer = GetInteger(GL_READ_BUFFER); + const std::string identity = "fbo" + std::to_string(framebuffer); for (GLint index = 0; index < maxColorAttachments; ++index) { char label[32]; std::snprintf(label, sizeof(label), "att%d", index); - DumpOneAttachment(manifest, directory, framebuffer, + DumpOneAttachment(manifest, directory, identity, static_cast(GL_COLOR_ATTACHMENT0 + index), label, false); } - DumpOneAttachment(manifest, directory, framebuffer, GL_DEPTH_ATTACHMENT, "depth", true); + DumpOneAttachment(manifest, directory, identity, GL_DEPTH_ATTACHMENT, "depth", true); // The read buffer is per-framebuffer state the trace goes on using; put it back. if (framebuffer != 0 && savedReadBuffer != 0) { @@ -416,6 +543,7 @@ void RunDumpPoint(DumpPoint &point) { const GLint savedPackSkipRows = GetInteger(GL_PACK_SKIP_ROWS); const GLint savedPackImageHeight = GetInteger(GL_PACK_IMAGE_HEIGHT); const GLint savedPackSkipImages = GetInteger(GL_PACK_SKIP_IMAGES); + const GLint savedPackSwapBytes = GetInteger(GL_PACK_SWAP_BYTES); DrainErrors(); if (savedPackBuffer != 0) { @@ -427,6 +555,7 @@ void RunDumpPoint(DumpPoint &point) { glPixelStorei(GL_PACK_SKIP_ROWS, 0); glPixelStorei(GL_PACK_IMAGE_HEIGHT, 0); glPixelStorei(GL_PACK_SKIP_IMAGES, 0); + glPixelStorei(GL_PACK_SWAP_BYTES, GL_FALSE); DrainErrors(); const GLint maxColorAttachments = GetInteger(GL_MAX_COLOR_ATTACHMENTS); @@ -462,6 +591,7 @@ void RunDumpPoint(DumpPoint &point) { glPixelStorei(GL_PACK_SKIP_ROWS, savedPackSkipRows); glPixelStorei(GL_PACK_IMAGE_HEIGHT, savedPackImageHeight); glPixelStorei(GL_PACK_SKIP_IMAGES, savedPackSkipImages); + glPixelStorei(GL_PACK_SWAP_BYTES, savedPackSwapBytes); DrainErrors(); std::cerr << "MOBILEGL_TRACE_FBO_DUMP: call " << retrace::callNo << " -> " << manifestPath @@ -469,12 +599,98 @@ void RunDumpPoint(DumpPoint &point) { point.done = true; } +void RunTexture2dDumpPoint(Texture2dDumpPoint &point) { + if (!MakeDirectories(point.directory)) { + std::cerr << "warning: failed to create texture dump directory " << point.directory << "\n"; + point.done = true; + return; + } + + // glGetError is destructive. This debug-only snapshot hook deliberately starts from a + // clean error state so diagnostics below identify the dump rather than an earlier trace call. + DrainErrors(); + const GLint savedReadFramebuffer = GetInteger(GL_READ_FRAMEBUFFER_BINDING); + const GLint savedPackBuffer = GetInteger(GL_PIXEL_PACK_BUFFER_BINDING); + const GLint savedPackAlignment = GetInteger(GL_PACK_ALIGNMENT); + const GLint savedPackRowLength = GetInteger(GL_PACK_ROW_LENGTH); + const GLint savedPackSkipPixels = GetInteger(GL_PACK_SKIP_PIXELS); + const GLint savedPackSkipRows = GetInteger(GL_PACK_SKIP_ROWS); + const GLint savedPackImageHeight = GetInteger(GL_PACK_IMAGE_HEIGHT); + const GLint savedPackSkipImages = GetInteger(GL_PACK_SKIP_IMAGES); + const GLint savedPackSwapBytes = GetInteger(GL_PACK_SWAP_BYTES); + DrainErrors(); + + if (savedPackBuffer != 0) { + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + } + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glPixelStorei(GL_PACK_ROW_LENGTH, 0); + glPixelStorei(GL_PACK_SKIP_PIXELS, 0); + glPixelStorei(GL_PACK_SKIP_ROWS, 0); + glPixelStorei(GL_PACK_IMAGE_HEIGHT, 0); + glPixelStorei(GL_PACK_SKIP_IMAGES, 0); + glPixelStorei(GL_PACK_SWAP_BYTES, GL_FALSE); + DrainErrors(); + + const std::string identity = "texture" + std::to_string(point.texture) + + "-level" + std::to_string(point.level); + const std::string manifestPath = point.directory + "/manifest.txt"; + std::ofstream manifest(manifestPath, std::ios::trunc); + manifest << "call " << retrace::callNo << " texture " << point.texture << " level " + << point.level << "\n"; + AttachmentDesc desc; + if (glIsTexture(point.texture) == GL_FALSE || + !DescribeTexture2D(point.texture, static_cast(point.level), desc)) { + manifest << identity << " skipped=not-live-2d-texture\n"; + } else { + std::vector pixels; + const bool read = ReadTexture2DFloats(desc, pixels); + const bool wrote = read && WriteFloatPng(point.directory + "/" + identity + ".png", desc, 4, pixels); + manifest << identity << " object=texture name=" << desc.objectName << " level=" << desc.level + << " size=" << desc.width << "x" << desc.height << " internalformat=0x" << std::hex + << static_cast(desc.internalFormat) << std::dec + << " component=" << ComponentTypeName(desc.componentType); + if (read) { + manifest << FormatStatistics(pixels, 4); + } else { + manifest << " read=failed"; + } + if (!wrote) { + manifest << " png=failed"; + } + manifest << "\n"; + } + manifest.flush(); + + glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast(savedReadFramebuffer)); + if (savedPackBuffer != 0) { + glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast(savedPackBuffer)); + } + glPixelStorei(GL_PACK_ALIGNMENT, savedPackAlignment); + glPixelStorei(GL_PACK_ROW_LENGTH, savedPackRowLength); + glPixelStorei(GL_PACK_SKIP_PIXELS, savedPackSkipPixels); + glPixelStorei(GL_PACK_SKIP_ROWS, savedPackSkipRows); + glPixelStorei(GL_PACK_IMAGE_HEIGHT, savedPackImageHeight); + glPixelStorei(GL_PACK_SKIP_IMAGES, savedPackSkipImages); + glPixelStorei(GL_PACK_SWAP_BYTES, savedPackSwapBytes); + DrainErrors(); + + std::cerr << "MOBILEGL_TRACE_TEXTURE_2D_DUMP: call " << retrace::callNo << " texture " + << point.texture << " level " << point.level << " -> " << manifestPath << "\n"; + point.done = true; +} + void RunPendingDumps() { for (DumpPoint &point : gDumpPoints) { if (!point.done && point.call == retrace::callNo) { RunDumpPoint(point); } } + for (Texture2dDumpPoint &point : gTexture2dDumpPoints) { + if (!point.done && point.call == retrace::callNo) { + RunTexture2dDumpPoint(point); + } + } } class DumpingDumper final : public retrace::Dumper { @@ -511,8 +727,12 @@ void InstallIfRequested() { if (spec != nullptr && spec[0] != '\0') { ParseDumpPoints(spec); } + const char *textureSpec = std::getenv(kTexture2dDumpPointsEnv); + if (textureSpec != nullptr && textureSpec[0] != '\0') { + ParseTexture2dDumpPoints(textureSpec); + } } - if (gDumpPoints.empty()) { + if (gDumpPoints.empty() && gTexture2dDumpPoints.empty()) { gInstalled = true; return; } @@ -528,6 +748,11 @@ void InstallIfRequested() { std::cerr << "MOBILEGL_TRACE_FBO_DUMP: armed for call " << point.call << " -> " << point.directory << "\n"; } + for (const Texture2dDumpPoint &point : gTexture2dDumpPoints) { + std::cerr << "MOBILEGL_TRACE_TEXTURE_2D_DUMP: armed for call " << point.call + << " texture " << point.texture << " level " << point.level << " -> " + << point.directory << "\n"; + } } } // namespace mobilegl_trace_dump diff --git a/tools/trace_replay/apitrace_fbo_dump.hpp b/tools/trace_replay/apitrace_fbo_dump.hpp index 68ebfc0c..5b963884 100644 --- a/tools/trace_replay/apitrace_fbo_dump.hpp +++ b/tools/trace_replay/apitrace_fbo_dump.hpp @@ -2,9 +2,10 @@ namespace mobilegl_trace_dump { -// Installs the framebuffer-attachment dump hook when MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS -// describes at least one dump point. Safe and cheap to call on every makeCurrent: the -// environment is consulted once and the hook is installed at most once. +// Installs the opt-in framebuffer-attachment and named-2D-texture dump hooks. The respective +// environments are MOBILEGL_TRACE_DUMP_FBO_ATTACHMENTS and MOBILEGL_TRACE_DUMP_TEXTURE_2D. +// Safe and cheap to call on every makeCurrent: the environment is consulted once and the hook is +// installed at most once. void InstallIfRequested(); } // namespace mobilegl_trace_dump