diff --git a/host/features/include/gfxstream/host/features.h b/host/features/include/gfxstream/host/features.h index 98d00e589..b54911f04 100644 --- a/host/features/include/gfxstream/host/features.h +++ b/host/features/include/gfxstream/host/features.h @@ -438,6 +438,21 @@ struct FeatureSet { "and the available Vulkan driver extensions.", &map, }; + StringFeatureInfo VulkanValidation = { + "VulkanValidation", + "If set, configures Vulkan validation behavior (off|print|fail|crash).", + &map, + }; + StringFeatureInfo VulkanValidationIncludeFilter = { + "VulkanValidationIncludeFilter", + "Comma-separated, case-insensitive list of exact string filters to include for Vulkan validation. Matches against guest application name or engine name. If empty, all applications are included by default.", + &map, + }; + StringFeatureInfo VulkanValidationExcludeFilter = { + "VulkanValidationExcludeFilter", + "Comma-separated, case-insensitive list of exact string filters to exclude from Vulkan validation. Matches against guest application name or engine name.", + &map, + }; BoolFeatureInfo VulkanProtectedMemoryEmulation = { "VulkanProtectedMemoryEmulation", "If enabled, enables protected memory emulation for the guest.", diff --git a/host/frame_buffer.cpp b/host/frame_buffer.cpp index a3979d09a..473fcea7e 100644 --- a/host/frame_buffer.cpp +++ b/host/frame_buffer.cpp @@ -1207,6 +1207,12 @@ std::unique_ptr FrameBuffer::Impl::Create(FrameBuffer* frameb // used by underlying EGL driver might become invalid, // preventing new contexts from being created that share // against those contexts. + if (impl->m_features.VulkanValidation.getValue().has_value() || + impl->m_features.VulkanValidationIncludeFilter.getValue().has_value() || + impl->m_features.VulkanValidationExcludeFilter.getValue().has_value()) { + vk::ensureVulkanValidationLayersEnabled(); + } + vk::VulkanDispatch* vkDispatch = nullptr; if (impl->m_features.Vulkan.enabled()) { vkDispatch = vk::vkDispatch(false /* not for testing */); diff --git a/host/virtio_gpu_gfxstream_renderer.cpp b/host/virtio_gpu_gfxstream_renderer.cpp index 250f106d6..f974dad12 100644 --- a/host/virtio_gpu_gfxstream_renderer.cpp +++ b/host/virtio_gpu_gfxstream_renderer.cpp @@ -212,6 +212,12 @@ RendererPtr InitRenderer(uint32_t displayWidth, GFXSTREAM_DEBUG("Initializing renderer with width:%u height:%u renderer-flags:0x%x", displayWidth, displayHeight, rendererFlags); + if (features.VulkanValidation.getValue().has_value() || + features.VulkanValidationIncludeFilter.getValue().has_value() || + features.VulkanValidationExcludeFilter.getValue().has_value()) { + gfxstream::host::vk::ensureVulkanValidationLayersEnabled(); + } + gfxstream::host::vk::vkDispatch(false /* don't use test ICD */); static gfxstream::RenderLibPtr sRendererLibrary = gfxstream::initLibrary(); diff --git a/host/vulkan/Android.bp b/host/vulkan/Android.bp index f15a39de2..2daa4acc3 100644 --- a/host/vulkan/Android.bp +++ b/host/vulkan/Android.bp @@ -92,6 +92,7 @@ cc_library_static { "vk_format_utils.cpp", "vk_reconstruction.cpp", "vk_utils.cpp", + "vk_vvl_configuration.cpp", "vulkan_boxed_handles.cpp", "vulkan_dispatch.cpp", "vulkan_handle_mapping.cpp", diff --git a/host/vulkan/BUILD.bazel b/host/vulkan/BUILD.bazel index dc2c8a7a2..113f3ea71 100644 --- a/host/vulkan/BUILD.bazel +++ b/host/vulkan/BUILD.bazel @@ -33,6 +33,7 @@ cc_library( "vk_qsri_timeline.h", "vk_snapshot_handles.h", "vk_utils.h", + "vk_vvl_configuration.h", "vulkan_boxed_handles.h", "vulkan_dispatch.h", "vulkan_handle_mapping.h", @@ -86,6 +87,7 @@ cc_library( "vk_format_utils.cpp", "vk_reconstruction.cpp", "vk_utils.cpp", + "vk_vvl_configuration.cpp", "vulkan_boxed_handles.cpp", "vulkan_dispatch.cpp", "vulkan_handle_mapping.cpp", @@ -126,6 +128,7 @@ cc_library( "vk_reconstruction.h", "vk_snapshot_handles.h", "vk_utils.h", + "vk_vvl_configuration.h", "vulkan_boxed_handles.h", "vulkan_dispatch.h", "vulkan_handle_mapping.h", diff --git a/host/vulkan/CMakeLists.txt b/host/vulkan/CMakeLists.txt index bb73f00c7..d98f3db50 100644 --- a/host/vulkan/CMakeLists.txt +++ b/host/vulkan/CMakeLists.txt @@ -46,6 +46,7 @@ add_library(gfxstream-vulkan-server vk_format_utils.cpp vk_reconstruction.cpp vk_utils.cpp + vk_vvl_configuration.cpp vulkan_boxed_handles.cpp vulkan_dispatch.cpp vulkan_handle_mapping.cpp diff --git a/host/vulkan/meson.build b/host/vulkan/meson.build index 2ea154a65..602458e5d 100644 --- a/host/vulkan/meson.build +++ b/host/vulkan/meson.build @@ -34,6 +34,7 @@ files_lib_vulkan_server = files( 'vk_format_utils.cpp', 'vk_reconstruction.cpp', 'vk_utils.cpp', + 'vk_vvl_configuration.cpp', 'vulkan_boxed_handles.cpp', 'vulkan_dispatch.cpp', 'vulkan_handle_mapping.cpp', diff --git a/host/vulkan/vk_common_operations.cpp b/host/vulkan/vk_common_operations.cpp index 7ad518a3a..bab1cf31d 100644 --- a/host/vulkan/vk_common_operations.cpp +++ b/host/vulkan/vk_common_operations.cpp @@ -784,6 +784,10 @@ std::unique_ptr VkEmulation::create(VulkanDispatch* gvk, emulation->m_globalState = globalState; emulation->mGvk = gvk; emulation->setFeatures(features); + auto vvlConfig = VVLConfiguration::parse(features); + if (vvlConfig.getBehavior() != VVLBehavior::None) { + emulation->mVVLConfig.emplace(std::move(vvlConfig)); + } std::vector getPhysicalDeviceProperties2InstanceExtNames = { VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, @@ -1818,6 +1822,15 @@ bool VkEmulation::createResourcesWithRequirementsEnabled() const { return mUseCreateResourcesWithRequirements; } +std::unique_ptr VkEmulation::createVVLContext( + const std::string& appName, const std::string& engineName, + VkDebugUtilsMessengerCreateInfoEXT* outCreateInfo) const { + if (!mVVLConfig.has_value()) { + return nullptr; + } + return mVVLConfig->createDebugContext(appName, engineName, outCreateInfo); +} + bool VkEmulation::supportsGetPhysicalDeviceProperties2() const { return mInstanceSupportsGetPhysicalDeviceProperties2; } diff --git a/host/vulkan/vk_common_operations.h b/host/vulkan/vk_common_operations.h index 99e14cbcb..6b60ac1a9 100644 --- a/host/vulkan/vk_common_operations.h +++ b/host/vulkan/vk_common_operations.h @@ -27,6 +27,8 @@ #include #include +#include "vk_vvl_configuration.h" + #include "color_buffer_vk.h" #include "compositor_vk.h" #include "debug_utils_helper.h" @@ -187,6 +189,10 @@ class VkEmulation { bool isGuestVulkanOnly() const; + std::unique_ptr createVVLContext( + const std::string& appName, const std::string& engineName, + VkDebugUtilsMessengerCreateInfoEXT* outCreateInfo = nullptr) const; + bool commandBufferCheckpointsEnabled() const; bool supportsSamplerYcbcrConversion() const; @@ -801,6 +807,8 @@ class VkEmulation { // UdmabufCreator std::unique_ptr mUdmabufCreator; + + std::optional mVVLConfig; }; #ifdef __ANDROID__ diff --git a/host/vulkan/vk_decoder_global_state.cpp b/host/vulkan/vk_decoder_global_state.cpp index e1fac243a..5f91b4e07 100644 --- a/host/vulkan/vk_decoder_global_state.cpp +++ b/host/vulkan/vk_decoder_global_state.cpp @@ -1164,6 +1164,38 @@ class VkDecoderGlobalState::Impl { deepcopy_VkInstanceCreateInfo(pool, VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, pCreateInfo, &createInfoFiltered); + std::string vvlAppName = ""; + std::string vvlEngineName = ""; + if (pCreateInfo->pApplicationInfo) { + if (pCreateInfo->pApplicationInfo->pApplicationName) { + vvlAppName = pCreateInfo->pApplicationInfo->pApplicationName; + } + if (pCreateInfo->pApplicationInfo->pEngineName) { + vvlEngineName = pCreateInfo->pApplicationInfo->pEngineName; + } + } + + VkDebugUtilsMessengerCreateInfoEXT debugInfo = {}; + std::unique_ptr debugContext = + m_vkEmulation->createVVLContext(vvlAppName, vvlEngineName, &debugInfo); + + if (debugContext) { + GFXSTREAM_INFO("Enabling VVL for %s %s", vvlAppName.c_str(), vvlEngineName.c_str()); + + bool hasDebugUtils = false; + for (const char* ext : finalExts) { + if (ext && strcmp(ext, VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0) { + hasDebugUtils = true; + break; + } + } + if (!hasDebugUtils) { + finalExts.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + } + } else { + GFXSTREAM_VERBOSE("Not enabling VVL for %s %s", vvlAppName.c_str(), vvlEngineName.c_str()); + } + createInfoFiltered.enabledExtensionCount = static_cast(finalExts.size()); createInfoFiltered.ppEnabledExtensionNames = finalExts.data(); if (createInfoFiltered.pApplicationInfo != nullptr) { @@ -1175,6 +1207,11 @@ class VkDecoderGlobalState::Impl { vk_struct_chain_filter(&createInfoFiltered); vk_struct_chain_filter(&createInfoFiltered); + if (debugContext) { + auto chainIter = vk_make_chain_iterator(&createInfoFiltered); + vk_append_struct(&chainIter, &debugInfo); + } + #if defined(__APPLE__) if (m_vkEmulation->supportsPortabilityEnumeration()) { createInfoFiltered.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; @@ -1234,14 +1271,30 @@ class VkDecoderGlobalState::Impl { info.contextId = renderThreadInfo->ctx_id; } + if (debugContext) { + auto vk = dispatch_VkInstance(boxed); + if (vk && vk->vkCreateDebugUtilsMessengerEXT && vk->vkDestroyDebugUtilsMessengerEXT) { + VkDebugUtilsMessengerEXT messenger = VK_NULL_HANDLE; + VkResult messengerRes = vk->vkCreateDebugUtilsMessengerEXT(*pInstance, &debugInfo, nullptr, &messenger); + if (messengerRes == VK_SUCCESS) { + info.debugMessenger = messenger; + } else { + GFXSTREAM_WARNING("Failed to create Vulkan debug utils messenger: %s", string_VkResult(messengerRes)); + } + } + } + + uint64_t contextId = info.contextId; + info.debugContext = std::move(debugContext); + VALIDATE_NEW_HANDLE_INFO_ENTRY(mInstanceInfo, *pInstance); - mInstanceInfo[*pInstance] = info; + mInstanceInfo[*pInstance] = std::move(info); - *pInstance = (VkInstance)info.boxed; + *pInstance = (VkInstance)boxed; if (vkCleanupEnabled()) { m_vkEmulation->getGlobalState()->registerProcessCleanupCallback( - unbox_VkInstance(boxed), info.contextId, [this, boxed] { + unbox_VkInstance(boxed), contextId, [this, boxed] { if (snapshotsEnabled()) { snapshot()->vkDestroyInstance(nullptr, kInvalidSnapshotApiCallHandle, nullptr, 0, boxed, nullptr); } @@ -11055,6 +11108,13 @@ class VkDecoderGlobalState::Impl { destroyDeviceObjects(deviceObjects); } + if (instanceInfo.debugMessenger != VK_NULL_HANDLE) { + auto vk = dispatch_VkInstance(instanceInfo.boxed); + if (vk && vk->vkDestroyDebugUtilsMessengerEXT) { + vk->vkDestroyDebugUtilsMessengerEXT(instance, instanceInfo.debugMessenger, nullptr); + } + } + m_vk->vkDestroyInstance(instance, nullptr); GFXSTREAM_INFO("Destroyed VkInstance:%p for application:'%s' engine:'%s'.", instance, instanceInfo.applicationName.c_str(), instanceInfo.engineName.c_str()); diff --git a/host/vulkan/vk_decoder_internal_structs.h b/host/vulkan/vk_decoder_internal_structs.h index 472b25a1a..711d31af0 100644 --- a/host/vulkan/vk_decoder_internal_structs.h +++ b/host/vulkan/vk_decoder_internal_structs.h @@ -34,6 +34,8 @@ #include #include +#include "vk_vvl_configuration.h" + #include "common/goldfish_vk_deepcopy.h" #include "debug_utils_helper.h" #include "device_op_tracker.h" @@ -216,6 +218,8 @@ struct InstanceInfo { std::string applicationName; std::string engineName; uint32_t contextId = 0; + std::unique_ptr debugContext; + VkDebugUtilsMessengerEXT debugMessenger = VK_NULL_HANDLE; }; struct PhysicalDeviceInfo { diff --git a/host/vulkan/vk_vvl_configuration.cpp b/host/vulkan/vk_vvl_configuration.cpp new file mode 100644 index 000000000..98b7cb2e9 --- /dev/null +++ b/host/vulkan/vk_vvl_configuration.cpp @@ -0,0 +1,188 @@ +#include "vk_vvl_configuration.h" +#include "vulkan_dispatch.h" + +#include +#include +#include +#include + +#include "gfxstream/common/logging.h" +#include "gfxstream/host/features.h" +#include "gfxstream/strings.h" + +namespace gfxstream { +namespace host { +namespace vk { + +namespace { + +std::string toLowerString(const std::string& str) { + std::string res = str; + std::transform(res.begin(), res.end(), res.begin(), + [](unsigned char c) { return std::tolower(c); }); + return res; +} + +std::unordered_set parseFilterList(const std::string& filterList) { + std::unordered_set patterns; + for (const auto& token : gfxstream::Split(filterList, ", \t\r\n")) { + if (token.empty()) { + continue; + } + patterns.insert(toLowerString(token)); + } + return patterns; +} + +VVLBehavior parseVVLBehaviorString(const std::string& modeStr) { + if (modeStr.empty()) { + return VVLBehavior::None; + } + if (modeStr == "print") { + return VVLBehavior::PrintOnly; + } else if (modeStr == "fail") { + return VVLBehavior::Fail; + } else if (modeStr == "crash") { + return VVLBehavior::Crash; + } else if (modeStr == "off") { + return VVLBehavior::None; + } else { + GFXSTREAM_WARNING("Unknown Vulkan validation mode '%s', defaulting to off.", + modeStr.c_str()); + return VVLBehavior::None; + } +} + +VKAPI_ATTR VkBool32 VKAPI_CALL +vk_debug_callback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, + VkDebugUtilsMessageTypeFlagsEXT messageTypes, + const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) { + auto* context = static_cast(pUserData); + if (!context) { + GFXSTREAM_ERROR("VVL debug callback invoked without a valid VVLContext!"); + return VK_FALSE; + } + + gfxstream::host::LogLevel logSeverity = gfxstream::host::LogLevel::kInfo; + if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) { + logSeverity = gfxstream::host::LogLevel::kError; + } else if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { + logSeverity = gfxstream::host::LogLevel::kWarning; + } else if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT) { + logSeverity = gfxstream::host::LogLevel::kInfo; + } else if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT) { + logSeverity = gfxstream::host::LogLevel::kVerbose; + } + + GFXSTREAM_LOG_INNER(logSeverity, "VVL%s: %s", context->appInfo.c_str(), pCallbackData->pMessage); + + if (messageSeverity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) { + if (context->behavior == VVLBehavior::Crash) { + GFXSTREAM_FATAL("VVL requested CRASH on error: %s", pCallbackData->pMessage); + } else if (context->behavior == VVLBehavior::Fail) { + GFXSTREAM_ERROR("VVL requested FAIL on error: %s", pCallbackData->pMessage); + return VK_TRUE; + } + } + + return VK_FALSE; +} + +} // namespace + +VVLConfiguration VVLConfiguration::parse(const gfxstream::host::FeatureSet& features) { + auto vvlValOpt = features.VulkanValidation.getValue(); + std::string valMode = vvlValOpt ? *vvlValOpt : ""; + + if (valMode.empty()) { + if (const char* env = getenv("ANDROID_EMU_VVL_BEHAVIOR")) { + valMode = env; + GFXSTREAM_INFO("VVL behavior set via ANDROID_EMU_VVL_BEHAVIOR envvar: '%s'", valMode.c_str()); + } + } + + std::unordered_set includeFilters; + auto includeOpt = features.VulkanValidationIncludeFilter.getValue(); + if (includeOpt && !includeOpt->empty()) { + includeFilters = parseFilterList(*includeOpt); + } else if (const char* env = getenv("ANDROID_EMU_VVL_INCLUDE_FILTER")) { + includeFilters = parseFilterList(env); + } + + std::unordered_set excludeFilters; + auto excludeOpt = features.VulkanValidationExcludeFilter.getValue(); + if (excludeOpt && !excludeOpt->empty()) { + excludeFilters = parseFilterList(*excludeOpt); + } else if (const char* env = getenv("ANDROID_EMU_VVL_EXCLUDE_FILTER")) { + excludeFilters = parseFilterList(env); + } + + if (valMode.empty() && (!includeFilters.empty() || !excludeFilters.empty())) { + valMode = "print"; + GFXSTREAM_INFO("VVL filters specified without behavior mode, defaulting behavior to '%s'", valMode.c_str()); + } + + VVLBehavior behavior = parseVVLBehaviorString(valMode); + if (behavior != VVLBehavior::None) { + ensureVulkanValidationLayersEnabled(); + } + + return VVLConfiguration(behavior, std::move(includeFilters), std::move(excludeFilters)); +} + +bool VVLConfiguration::matchesApp(const std::string& appName, const std::string& engineName) const { + if (mBehavior == VVLBehavior::None) { + return false; + } + + std::string app = toLowerString(appName); + std::string engine = toLowerString(engineName); + + return (mIncludeFilters.empty() || mIncludeFilters.find(app) != mIncludeFilters.end() || + mIncludeFilters.find(engine) != mIncludeFilters.end()) && + (mExcludeFilters.find(app) == mExcludeFilters.end() && + mExcludeFilters.find(engine) == mExcludeFilters.end()); +} + +std::unique_ptr VVLConfiguration::createDebugContext( + const std::string& appName, const std::string& engineName, + VkDebugUtilsMessengerCreateInfoEXT* outCreateInfo) const { + if (!matchesApp(appName, engineName)) { + return nullptr; + } + + auto debugContext = std::make_unique(); + debugContext->appName = appName; + debugContext->engineName = engineName; + debugContext->behavior = mBehavior; + + if (!appName.empty()) { + debugContext->appInfo = " [App: " + appName; + if (!engineName.empty() && engineName != "No Engine") { + debugContext->appInfo += ", Engine: " + engineName; + } + debugContext->appInfo += "]"; + } + + if (outCreateInfo) { + *outCreateInfo = { + .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT, + .pNext = nullptr, + .flags = 0, + .messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT, + .messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | + VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT, + .pfnUserCallback = vk_debug_callback, + .pUserData = debugContext.get(), + }; + } + + return debugContext; +} + +} // namespace vk +} // namespace host +} // namespace gfxstream \ No newline at end of file diff --git a/host/vulkan/vk_vvl_configuration.h b/host/vulkan/vk_vvl_configuration.h new file mode 100644 index 000000000..6fae7b419 --- /dev/null +++ b/host/vulkan/vk_vvl_configuration.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include + +#include "gfxstream/host/features.h" +#include "vulkan/vulkan_core.h" + +namespace gfxstream { +namespace host { +namespace vk { + +// X11 headers (via -DUSE_X11) define `#define None 0L`, which conflicts with VVLBehavior::None. +#ifdef None +#undef None +#endif + +enum class VVLBehavior { + None, // Do not register callback / ignore validation + PrintOnly, + Fail, // Returns an error to the Vulkan API call + Crash // Forces a hard crash (e.g., abort()) +}; + +// The payload we pass via pUserData to the debug callback +struct VVLContext { + std::string appName; + std::string engineName; + VVLBehavior behavior; + std::string appInfo; +}; + +class VVLConfiguration { + public: + static VVLConfiguration parse(const gfxstream::host::FeatureSet& features); + std::unique_ptr createDebugContext( + const std::string& appName, + const std::string& engineName, + VkDebugUtilsMessengerCreateInfoEXT* outCreateInfo = nullptr) const; + + VVLBehavior getBehavior() const { return mBehavior; } + const std::unordered_set& getIncludeFilters() const { return mIncludeFilters; } + const std::unordered_set& getExcludeFilters() const { return mExcludeFilters; } + + private: + VVLConfiguration(VVLBehavior behavior, + std::unordered_set includeFilters, + std::unordered_set excludeFilters) + : mBehavior(behavior), + mIncludeFilters(std::move(includeFilters)), + mExcludeFilters(std::move(excludeFilters)) {} + + bool matchesApp(const std::string& appName, const std::string& engineName) const; + + const VVLBehavior mBehavior; + const std::unordered_set mIncludeFilters; + const std::unordered_set mExcludeFilters; +}; + +} // namespace vk +} // namespace host +} // namespace gfxstream \ No newline at end of file diff --git a/host/vulkan/vulkan_dispatch.cpp b/host/vulkan/vulkan_dispatch.cpp index 4a64b48df..2485b7a3f 100644 --- a/host/vulkan/vulkan_dispatch.cpp +++ b/host/vulkan/vulkan_dispatch.cpp @@ -391,6 +391,63 @@ static void* sVulkanDispatchDlSym(void* lib, const char* sym) { return sVulkanDispatchImpl()->dlsym(lib, sym); } +void ensureVulkanValidationLayersEnabled() { + GFXSTREAM_INFO("Enabling Vulkan validation layers."); + + // Our VkLayer_khronos_validation.json expects the VVL .so file to be present in the same directory. + std::vector candidateDirs; + const std::string testdataPath = + gfxstream::base::getEnvironmentVariable("GFXSTREAM_TESTDATA_PATH"); + if (!testdataPath.empty()) { + candidateDirs.push_back(testdataPath); + } + candidateDirs.push_back(pj({gfxstream::base::getProgramDirectory(), "lib64", "vulkan", "layers"})); + candidateDirs.push_back(pj({gfxstream::base::getLauncherDirectory(), "lib64", "vulkan", "layers"})); + candidateDirs.push_back(pj({gfxstream::base::getProgramDirectory(), "testlib64", "layers"})); + candidateDirs.push_back(pj({gfxstream::base::getLauncherDirectory(), "testlib64", "layers"})); + candidateDirs.push_back(gfxstream::base::getProgramDirectory()); + candidateDirs.push_back(gfxstream::base::getLauncherDirectory()); + + std::string vvlPath; + for (const auto& dir : candidateDirs) { + if (pathExists(pj({dir, "VkLayer_khronos_validation.json"}).c_str())) { + vvlPath = dir; + break; + } + } + + if (!vvlPath.empty()) { +#ifdef _WIN32 + const char kPathSeparator = ';'; +#else + const char kPathSeparator = ':'; +#endif + const char* const kVkAddLayerPathEnvVar = "VK_ADD_LAYER_PATH"; + const char* const kVkLayerPathEnvVar = "VK_LAYER_PATH"; + const char* const kVkInstanceLayersEnvVar = "VK_INSTANCE_LAYERS"; + const char* const kEnableVVLEnvVar = "VK_LAYER_KHRONOS_validation"; + + if (gfxstream::base::getEnvironmentVariable(kVkAddLayerPathEnvVar).empty()) { + gfxstream::base::setEnvironmentVariable(kVkAddLayerPathEnvVar, vvlPath); + } + if (gfxstream::base::getEnvironmentVariable(kVkLayerPathEnvVar).empty()) { + gfxstream::base::setEnvironmentVariable(kVkLayerPathEnvVar, vvlPath); + } + auto layersEnvVar = gfxstream::base::getEnvironmentVariable(kVkInstanceLayersEnvVar); + if (layersEnvVar.empty()) { + gfxstream::base::setEnvironmentVariable(kVkInstanceLayersEnvVar, kEnableVVLEnvVar); + } else { + if (layersEnvVar.find(kEnableVVLEnvVar) == std::string::npos) { + std::stringstream ss; + ss << layersEnvVar << kPathSeparator << kEnableVVLEnvVar; + gfxstream::base::setEnvironmentVariable(kVkInstanceLayersEnvVar, ss.str()); + } + } + } else { + GFXSTREAM_WARNING("Vulkan validation layer library path not found in %s. Skipping validation layer setup.", vvlPath.c_str()); + } +} + void VulkanDispatchImpl::initialize(bool forTesting) { AutoLock lock(mLock); @@ -438,44 +495,14 @@ void VulkanDispatchImpl::initialize(bool forTesting) { } } - if (!gfxstream::base::getEnvironmentVariable("GFXSTREAM_USE_TESTING_VALIDATION_LAYERS").empty()) { - GFXSTREAM_INFO("GFXSTREAM_USE_TESTING_VALIDATION_LAYERS set. Enabling Vulkan validation layers."); - - // Our VkLayer_khronos_validation.json expects the VVL .so file to be present in the same directory. - std::string vvlPath = - pj({gfxstream::base::getProgramDirectory(), "testlib64", "layers"}); - if (!pathExists(vvlPath.c_str())) { - vvlPath = - pj({gfxstream::base::getLauncherDirectory(), "testlib64", "layers"}); - } + bool vvlRequested = + !gfxstream::base::getEnvironmentVariable("GFXSTREAM_USE_TESTING_VALIDATION_LAYERS").empty() || + !gfxstream::base::getEnvironmentVariable("ANDROID_EMU_VVL_BEHAVIOR").empty() || + !gfxstream::base::getEnvironmentVariable("ANDROID_EMU_VVL_INCLUDE_FILTER").empty() || + !gfxstream::base::getEnvironmentVariable("ANDROID_EMU_VVL_EXCLUDE_FILTER").empty(); - if (pathExists(vvlPath.c_str())) { -#ifdef _WIN32 - const char kPathSeparator = ';'; -#else - const char kPathSeparator = ':'; -#endif - const char* const kVkAddLayerPathEnvVar = "VK_ADD_LAYER_PATH"; - const char* const kVkInstanceLayersEnvVar = "VK_INSTANCE_LAYERS"; - const char* const kEnableVVLEnvVar = "VK_LAYER_KHRONOS_validation"; - - if (!gfxstream::base::getEnvironmentVariable(kVkAddLayerPathEnvVar).empty()) { - GFXSTREAM_WARNING("Overriding %s", kVkAddLayerPathEnvVar); - } - gfxstream::base::setEnvironmentVariable(kVkAddLayerPathEnvVar, vvlPath); - auto layersEnvVar = gfxstream::base::getEnvironmentVariable(kVkInstanceLayersEnvVar); - if (layersEnvVar.empty()) { - gfxstream::base::setEnvironmentVariable(kVkInstanceLayersEnvVar, kEnableVVLEnvVar); - } else { - if (layersEnvVar.find(kEnableVVLEnvVar) == std::string::npos) { - std::stringstream ss; - ss << layersEnvVar << kPathSeparator << kEnableVVLEnvVar; - gfxstream::base::setEnvironmentVariable(kVkInstanceLayersEnvVar, ss.str()); - } - } - } else { - GFXSTREAM_WARNING("Vulkan validation layer library path not found in %s. Skipping validation layer setup.", vvlPath.c_str()); - } + if (vvlRequested) { + ensureVulkanValidationLayersEnabled(); } init_vulkan_dispatch_from_system_loader(sVulkanDispatchDlOpen, sVulkanDispatchDlSym, diff --git a/host/vulkan/vulkan_dispatch.h b/host/vulkan/vulkan_dispatch.h index 81db1b2a4..8ee1ee689 100644 --- a/host/vulkan/vulkan_dispatch.h +++ b/host/vulkan/vulkan_dispatch.h @@ -19,6 +19,7 @@ namespace gfxstream { namespace host { namespace vk { +void ensureVulkanValidationLayersEnabled(); VulkanDispatch* vkDispatch(bool forTesting = false); bool vkDispatchValid(const VulkanDispatch* vk); diff --git a/host/vulkan/vulkan_unittest.cpp b/host/vulkan/vulkan_unittest.cpp index b153ee307..b00a1d2b7 100644 --- a/host/vulkan/vulkan_unittest.cpp +++ b/host/vulkan/vulkan_unittest.cpp @@ -25,6 +25,7 @@ #include "gfxstream/host/testing/VkTestUtils.h" #include "gfxstream/system/System.h" #include "vk_common_operations.h" +#include "vk_vvl_configuration.h" #include "vulkan_dispatch.h" #ifdef _WIN32 @@ -430,6 +431,55 @@ TEST_F(VulkanTest, StagingMemoryQuery) { EXPECT_TRUE(getStagingMemoryTypeIndex(&mVk, mDevice, &memProps, memReqs, &typeIndex)); } +TEST(VVLConfigurationTest, ExactMatchingCaseInsensitive) { + gfxstream::host::FeatureSet features; + features.VulkanValidation.parseValue("print"); + features.VulkanValidationIncludeFilter.parseValue("com.example.game,Angle"); + VVLConfiguration config = VVLConfiguration::parse(features); + + EXPECT_TRUE(config.createDebugContext("com.example.game", "No Engine") != nullptr); + EXPECT_TRUE(config.createDebugContext("COM.EXAMPLE.GAME", "No Engine") != nullptr); + EXPECT_TRUE(config.createDebugContext("angle", "No Engine") != nullptr); + EXPECT_FALSE(config.createDebugContext("com.example.game.sub", "No Engine") != nullptr); + EXPECT_FALSE(config.createDebugContext("angle_driver", "No Engine") != nullptr); +} + +TEST(VVLConfigurationTest, ExcludeFilterPrecedence) { + gfxstream::host::FeatureSet features; + features.VulkanValidation.parseValue("print"); + features.VulkanValidationIncludeFilter.parseValue("com.example.game,TestApp"); + features.VulkanValidationExcludeFilter.parseValue("com.example.game"); + VVLConfiguration config = VVLConfiguration::parse(features); + + EXPECT_FALSE(config.createDebugContext("com.example.game", "No Engine") != nullptr); + EXPECT_TRUE(config.createDebugContext("TestApp", "No Engine") != nullptr); +} + +TEST(VVLConfigurationTest, FilterWithoutExplicitBehaviorDefaultsToPrint) { + gfxstream::host::FeatureSet features; + features.VulkanValidationIncludeFilter.parseValue("com.example.game"); + VVLConfiguration config = VVLConfiguration::parse(features); + + EXPECT_EQ(config.getBehavior(), VVLBehavior::PrintOnly); + EXPECT_TRUE(config.createDebugContext("com.example.game", "No Engine") != nullptr); + EXPECT_FALSE(config.createDebugContext("other.app", "No Engine") != nullptr); +} + +TEST(VVLConfigurationTest, FilterEnvironmentVariables) { + gfxstream::base::setEnvironmentVariable("ANDROID_EMU_VVL_INCLUDE_FILTER", "com.env.include"); + gfxstream::base::setEnvironmentVariable("ANDROID_EMU_VVL_EXCLUDE_FILTER", "com.env.exclude"); + + gfxstream::host::FeatureSet features; + VVLConfiguration config = VVLConfiguration::parse(features); + + EXPECT_EQ(config.getBehavior(), VVLBehavior::PrintOnly); + EXPECT_TRUE(config.createDebugContext("com.env.include", "No Engine") != nullptr); + EXPECT_FALSE(config.createDebugContext("com.env.exclude", "No Engine") != nullptr); + + gfxstream::base::setEnvironmentVariable("ANDROID_EMU_VVL_INCLUDE_FILTER", ""); + gfxstream::base::setEnvironmentVariable("ANDROID_EMU_VVL_EXCLUDE_FILTER", ""); +} + } // namespace } // namespace vk } // namespace host diff --git a/tests/end2end/Android.bp b/tests/end2end/Android.bp index 331a7207e..b1c54144a 100644 --- a/tests/end2end/Android.bp +++ b/tests/end2end/Android.bp @@ -27,6 +27,7 @@ cc_test_host { "gfxstream_end2end_gl_tests.cpp", "gfxstream_end2end_gralloc_tests.cpp", "gfxstream_end2end_vk_tests.cpp", + "gfxstream_end2end_vvl_tests.cpp", "kumquat_instance.cpp", "test_data_utils.cpp", ], diff --git a/tests/end2end/BUILD.bazel b/tests/end2end/BUILD.bazel index a132089a0..77361d20f 100644 --- a/tests/end2end/BUILD.bazel +++ b/tests/end2end/BUILD.bazel @@ -58,6 +58,7 @@ cc_test( "gfxstream_end2end_tests.cpp", "gfxstream_end2end_tests.h", "gfxstream_end2end_vk_tests.cpp", + "gfxstream_end2end_vvl_tests.cpp", "kumquat_instance.cpp", "kumquat_instance.h", "test_data_utils.cpp", diff --git a/tests/end2end/gfxstream_end2end_tests.cpp b/tests/end2end/gfxstream_end2end_tests.cpp index 740afd67a..ba7278732 100644 --- a/tests/end2end/gfxstream_end2end_tests.cpp +++ b/tests/end2end/gfxstream_end2end_tests.cpp @@ -93,7 +93,9 @@ std::string TestParams::ToString() const { ret += "SampleCount" + std::to_string(samples); if (!with_features.empty()) { ret += "WithFeatures_"; - ret += Join(with_features, "_"); + std::string featuresStr = Join(with_features, "_"); + std::replace(featuresStr.begin(), featuresStr.end(), ':', '_'); + ret += featuresStr; ret += "_"; } ret += "Over"; @@ -241,12 +243,19 @@ void GfxstreamEnd2EndTest::SetUp() { const std::string transportValue = GfxstreamTransportToEnvVar(params.with_transport); std::vector featureEnables; for (const std::string& feature : params.with_features) { - featureEnables.push_back(feature + ":enabled"); + if (feature.find(':') != std::string::npos) { + featureEnables.push_back(feature); + } else { + featureEnables.push_back(feature + ":enabled"); + } } ASSERT_THAT(gfxstream::testing::SetupGraphicsTestEnvironment(), IsTrue()) << "Failed to configured graphics test environment!"; + const std::string testdataDirectory = GetTestDataPath("kumquat_virtio").parent_path().string(); + gfxstream::base::setEnvironmentVariable("GFXSTREAM_TESTDATA_PATH", testdataDirectory.c_str()); + ASSERT_THAT(setenv("GFXSTREAM_TRANSPORT", transportValue.c_str(), /*overwrite=*/1), Eq(0)); ASSERT_THAT(setenv("VIRTGPU_KUMQUAT", "1", /*overwrite=*/1), Eq(0)); diff --git a/tests/end2end/gfxstream_end2end_vvl_tests.cpp b/tests/end2end/gfxstream_end2end_vvl_tests.cpp new file mode 100644 index 000000000..5e0f9a63e --- /dev/null +++ b/tests/end2end/gfxstream_end2end_vvl_tests.cpp @@ -0,0 +1,175 @@ +// Copyright (C) 2026 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include "gfxstream_end2end_test_utils.h" +#include "gfxstream_end2end_tests.h" +#include "gfxstream/common/logging.h" +#include "gfxstream/Expected.h" + +namespace gfxstream { +namespace tests { +namespace { + +using testing::Eq; +using testing::NotNull; + +class GfxstreamEnd2EndVvlTest : public GfxstreamEnd2EndTest { + protected: + void SetUp() override { + testing::internal::CaptureStdout(); + testing::internal::CaptureStderr(); + mOutputCaptured = true; + GfxstreamEnd2EndTest::SetUp(); + } + + void TearDown() override { + GfxstreamEnd2EndTest::TearDown(); + if (mOutputCaptured) { + (void)testing::internal::GetCapturedStdout(); + (void)testing::internal::GetCapturedStderr(); + mOutputCaptured = false; + } + } + + std::string GetCapturedLogs() { + if (mOutputCaptured) { + mOutputCaptured = false; + return testing::internal::GetCapturedStdout() + testing::internal::GetCapturedStderr(); + } + return ""; + } + + void DoValidVkOperations() { + auto [instance, physicalDevice, device, queue, queueFamilyIndex] = + GFXSTREAM_ASSERT(SetUpTypicalVkTestEnvironment()); + + const uint32_t width = 32; + const uint32_t height = 32; + auto ahb = GFXSTREAM_ASSERT(ScopedAHardwareBuffer::Allocate( + *mGralloc, width, height, GFXSTREAM_AHB_FORMAT_R8G8B8A8_UNORM)); + + const VkNativeBufferANDROID imageNativeBufferInfo = { + .sType = VK_STRUCTURE_TYPE_NATIVE_BUFFER_ANDROID, + .handle = mGralloc->getNativeHandle(ahb), + }; + + const vkhpp::ImageCreateInfo imageCreateInfo = { + .pNext = &imageNativeBufferInfo, + .imageType = vkhpp::ImageType::e2D, + .format = vkhpp::Format::eR8G8B8A8Unorm, + .extent = + { + .width = width, + .height = height, + .depth = 1, + }, + .mipLevels = 1, + .arrayLayers = 1, + .samples = vkhpp::SampleCountFlagBits::e1, + .tiling = vkhpp::ImageTiling::eOptimal, + .usage = vkhpp::ImageUsageFlagBits::eSampled | vkhpp::ImageUsageFlagBits::eTransferDst | + vkhpp::ImageUsageFlagBits::eTransferSrc, + .sharingMode = vkhpp::SharingMode::eExclusive, + .initialLayout = vkhpp::ImageLayout::eUndefined, + }; + auto image = device->createImageUnique(imageCreateInfo).value; + ASSERT_TRUE(image.get() != VK_NULL_HANDLE); + } + + private: + bool mOutputCaptured = false; +}; + +TEST_P(GfxstreamEnd2EndVvlTest, BasicValidOperations) { + DoValidVkOperations(); +} + +TEST_P(GfxstreamEnd2EndVvlTest, InvalidVkOperationTriggersVvlLog) { + bool vvlEnabled = false; + for (const auto& f : GetParam().with_features) { + if (f.find("VulkanValidation") != std::string::npos) { + vvlEnabled = true; + break; + } + } + + { + auto [instance, physicalDevice, device, queue, queueFamilyIndex] = + GFXSTREAM_ASSERT(SetUpTypicalVkTestEnvironment()); + + // Perform invalid operations that trigger VVL validation without hitting Mesa assertions: + // 1. createBuffer with usage = 0 (VUID-VkBufferCreateInfo-usage-requiredbitmask) + const vkhpp::BufferCreateInfo invalidBufferInfo = { + .size = 64, + .usage = {}, + }; + auto bufferRes = device->createBufferUnique(invalidBufferInfo); + (void)bufferRes; + + // 2. createSampler with maxLod < minLod (VUID-VkSamplerCreateInfo-maxLod-01973) + const vkhpp::SamplerCreateInfo invalidSamplerInfo = { + .minLod = 2.0f, + .maxLod = 1.0f, + }; + auto samplerRes = device->createSamplerUnique(invalidSamplerInfo); + (void)samplerRes; + } + + // Tear down guest connection so kumquat_virtio processes and flushes all pending commands/logs + TearDownGuest(); + + const std::string logs = GetCapturedLogs(); + + if (vvlEnabled) { + EXPECT_NE(logs.find("VVL"), std::string::npos) + << "Expected VVL prefix in logs when VulkanValidation feature is enabled. Logs:\n" + << logs; + EXPECT_NE(logs.find("VUID-"), std::string::npos) + << "Expected VVL VUID validation error in logs when VulkanValidation feature is enabled. Logs:\n" + << logs; + } else { + EXPECT_EQ(logs.find("VVL"), std::string::npos) + << "Expected no VVL validation output in logs when VulkanValidation feature is disabled. Logs:\n" + << logs; + EXPECT_EQ(logs.find("VUID-"), std::string::npos) + << "Expected no VVL VUID validation error in logs when VulkanValidation feature is disabled. Logs:\n" + << logs; + } +} + +INSTANTIATE_TEST_SUITE_P(GfxstreamEnd2EndTests, GfxstreamEnd2EndVvlTest, ::testing::ValuesIn([]() { + std::vector cases = { + TestParams{ + .with_gl = false, + .with_vk = true, + .with_features = {"MinimalLogging"}, + .with_transport = GfxstreamTransport::kVirtioGpuAsg, + }, + TestParams{ + .with_gl = false, + .with_vk = true, + .with_features = {"MinimalLogging", "VulkanValidation:print"}, + .with_transport = GfxstreamTransport::kVirtioGpuAsg, + }, + }; + return cases; + }()), + &GetTestName); + +} // namespace +} // namespace tests +} // namespace gfxstream diff --git a/third_party/vvl/BUILD.vvl.bazel b/third_party/vvl/BUILD.vvl.bazel index 3cd61430f..719759dca 100644 --- a/third_party/vvl/BUILD.vvl.bazel +++ b/third_party/vvl/BUILD.vvl.bazel @@ -45,6 +45,11 @@ COPTS = select({ "-Wno-implicit-fallthrough", "-Wno-sign-compare", "-Wno-missing-field-initializers", + # Workaround for an ASAN bug in VVL's bundled parallel_hashmap (phmap.h), + # which constructs elements into poisoned slots before unpoisoning when + # ADDRESS_SANITIZER is defined. Undefine it to disable phmap's manual + # slot poisoning while keeping compiler ASAN (-fsanitize=address) enabled. + "-UADDRESS_SANITIZER", ], })