diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27d105be2..6233b9f3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -196,7 +196,7 @@ jobs: if: ${{ runner.os == 'Linux' && matrix.os != 'ubuntu-22.04' && !env['TEST_X86'] && !matrix.container }} run: | sudo apt update - sudo apt install cmake clang-19 llvm g++-12 valgrind zlib1g-dev libcurl4-openssl-dev + sudo apt install cmake clang-19 llvm g++-12 valgrind zlib1g-dev libcurl4-openssl-dev libvulkan-dev # Install kcov from source sudo apt-get install binutils-dev libssl-dev libelf-dev libstdc++-12-dev libdw-dev libiberty-dev git clone https://github.com/SimonKagstrom/kcov.git diff --git a/.gitmodules b/.gitmodules index d35218416..9d4b5d92e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,3 +14,6 @@ [submodule "external/benchmark"] path = external/benchmark url = https://github.com/google/benchmark.git +[submodule "external/vulkan-headers"] + path = external/vulkan-headers + url = https://github.com/KhronosGroup/Vulkan-Headers.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 528a1812f..47bf6886a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Use proper SDK name determination for structured logs `sdk.name` attribute. ([#1399](https://github.com/getsentry/sentry-native/pull/1399)) +**Features**: + +- Implement the GPU Info gathering within the Native SDK ([#1336](https://github.com/getsentry/sentry-native/pull/1336)) + ## 0.11.2 **Fixes**: diff --git a/CMakeLists.txt b/CMakeLists.txt index 35d4dd544..03b67ef6c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,6 +84,28 @@ option(SENTRY_PIC "Build sentry (and dependent) libraries as position independen option(SENTRY_TRANSPORT_COMPRESSION "Enable transport gzip compression" OFF) +# GPU information gathering support - enabled by default on supported platforms only +set(SENTRY_GPU_INFO_DEFAULT OFF) + +# Only enable GPU info on supported platforms +if(WIN32) + set(SENTRY_GPU_INFO_DEFAULT ON) +elseif(APPLE AND NOT IOS) + set(SENTRY_GPU_INFO_DEFAULT ON) +elseif(LINUX) + set(SENTRY_GPU_INFO_DEFAULT ON) +else() + # Disable GPU info on all other platforms (Android, iOS, AIX, etc.) + message(STATUS "GPU Info: Platform not supported, GPU information gathering disabled") +endif() + +option(SENTRY_WITH_GPU_INFO "Build with GPU information gathering support" ${SENTRY_GPU_INFO_DEFAULT}) + +# GPU info enabled - no longer requires Vulkan SDK (uses headers submodule + dynamic linking) +if(SENTRY_WITH_GPU_INFO) + message(STATUS "GPU information gathering enabled (using vulkan-headers submodule)") +endif() + option(SENTRY_BUILD_TESTS "Build sentry-native tests" "${SENTRY_MAIN_PROJECT}") option(SENTRY_BUILD_EXAMPLES "Build sentry-native example(s)" "${SENTRY_MAIN_PROJECT}") option(SENTRY_BUILD_BENCHMARKS "Build sentry-native benchmarks" OFF) @@ -563,6 +585,12 @@ if(NOT XBOX) endif() endif() +# handle Vulkan headers for GPU info +if(SENTRY_WITH_GPU_INFO) + target_include_directories(sentry PRIVATE + "$") +endif() + # apply platform libraries to sentry library target_link_libraries(sentry PRIVATE ${_SENTRY_PLATFORM_LIBS}) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d70145828..c62e644f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -184,7 +184,7 @@ The example currently supports the following commands: - `discarding-before-transaction`: Installs a `before_transaction()` callback that discards the transaction. - `traces-sampler`: Installs a traces sampler callback function when used alongside `capture-transaction`. - `attach-view-hierarchy`: Adds a `view-hierarchy.json` attachment file, giving it the proper `attachment_type` and `content_type`. - This file can be found in `./tests/fixtures/view-hierachy.json`. + This file can be found in `./tests/fixtures/view-hierachy.json`. - `set-trace`: Sets the scope `propagation_context`'s trace data to the given `trace_id="aaaabbbbccccddddeeeeffff00001111"` and `parent_span_id=""f0f0f0f0f0f0f0f0"`. - `capture-with-scope`: Captures an event with a local scope. - `attach-to-scope`: Same as `attachment` but attaches the file to the local scope. @@ -196,6 +196,7 @@ The example currently supports the following commands: - `test-logger-before-crash`: Outputs marker directly using printf for test parsing before crash. Only on Linux using crashpad: + - `crashpad-wait-for-upload`: Couples application shutdown to complete the upload in the `crashpad_handler`. Only on Windows using crashpad with its WER handler module: @@ -218,21 +219,25 @@ invoked directly. ## Handling locks -There are a couple of rules based on the current usage of mutexes in the Native SDK that should always be +There are a couple of rules based on the current usage of mutexes in the Native SDK that should always be applied in order not to have to fight boring concurrency bugs: -* we use recursive mutexes throughout the code-base -* these primarily allow us to call public interfaces from internal code instead of having a layer in-between -* but they come at the risk of less clarity whether a lock release still leaves a live lock -* they should not be considered as convenience: - * reduce the amount of recursive locking to an absolute minimum - * instead of retrieval via global locks, pass shared state like `options` or `scope` around in internal helpers - * or better yet: extract what you need into locals, then release the lock early -* we provide lexical scope macros `SENTRY_WITH_OPTIONS` and `SENTRY_WITH_SCOPE` (and variants) as convenience wrappers -* if you use them be aware of the following: - * as mentioned above, while the macros are convenience, their lexical scope should be as short as possible - * avoid nesting them unless strictly necessary - * if you nest them (directly or via callees), the `options` lock **must always be acquired before** the `scope` lock - * never early-return or jump (via `goto` or `return`) from within a `SENTRY_WITH_*` block: doing so skips the corresponding release or cleanup - * in particular, since `options` are readonly after `sentry_init()` the lock is only acquired to increment the refcount for the duration of `SENTRY_WITH_OPTIONS` - * however, `SENTRY_WITH_SCOPE` (and variants) always hold the lock for the entirety of their lexical scope \ No newline at end of file +- we use recursive mutexes throughout the code-base +- these primarily allow us to call public interfaces from internal code instead of having a layer in-between +- but they come at the risk of less clarity whether a lock release still leaves a live lock +- they should not be considered as convenience: + - reduce the amount of recursive locking to an absolute minimum + - instead of retrieval via global locks, pass shared state like `options` or `scope` around in internal helpers + - or better yet: extract what you need into locals, then release the lock early +- we provide lexical scope macros `SENTRY_WITH_OPTIONS` and `SENTRY_WITH_SCOPE` (and variants) as convenience wrappers +- if you use them be aware of the following: + - as mentioned above, while the macros are convenience, their lexical scope should be as short as possible + - avoid nesting them unless strictly necessary + - if you nest them (directly or via callees), the `options` lock **must always be acquired before** the `scope` lock + - never early-return or jump (via `goto` or `return`) from within a `SENTRY_WITH_*` block: doing so skips the corresponding release or cleanup + - in particular, since `options` are readonly after `sentry_init()` the lock is only acquired to increment the refcount for the duration of `SENTRY_WITH_OPTIONS` + - however, `SENTRY_WITH_SCOPE` (and variants) always hold the lock for the entirety of their lexical scope + +## Runtime Library Requirements + +**Vulkan**: libraries (e.g. libvulkan, vulkan-1, MoltenVK) are required for gathering GPU Context data. Native SDK provides vendored Headers of Vulkan for easier compilation and integration, however it relies on the libraries being installed in a known location. diff --git a/README.md b/README.md index 9858a50b1..14881b119 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![Conan Center](https://shields.io/conan/v/sentry-native)](https://conan.io/center/recipes/sentry-native) [![homebrew](https://img.shields.io/homebrew/v/sentry-native)](https://formulae.brew.sh/formula/sentry-native) [![nixpkgs unstable](https://repology.org/badge/version-for-repo/nix_unstable/sentry-native.svg)](https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/by-name/se/sentry-native/package.nix) [![vcpkg](https://shields.io/vcpkg/v/sentry-native)](https://vcpkg.link/ports/sentry-native) +

@@ -10,6 +11,7 @@

# Official Sentry SDK for C/C++ + [![GH Workflow](https://img.shields.io/github/actions/workflow/status/getsentry/sentry-native/ci.yml?branch=master)](https://github.com/getsentry/sentry-native/actions) [![codecov](https://codecov.io/gh/getsentry/sentry-native/branch/master/graph/badge.svg)](https://codecov.io/gh/getsentry/sentry-native) @@ -98,7 +100,7 @@ per platform, and can also be configured for cross-compilation. System-wide installation of the resulting sentry library is also possible via CMake. -The prerequisites for building differ depending on the platform and backend. You will always need `CMake` to build the code. Additionally, when using the `crashpad` backend, `zlib` is required. On Linux and macOS, `libcurl` is a prerequisite. For more details, check out the [contribution guide](./CONTRIBUTING.md). +The prerequisites for building differ depending on the platform and backend. You will always need `CMake` to build the code. Additionally, when using the `crashpad` backend, `zlib` is required. On Linux and macOS, `libcurl` is a prerequisite. When GPU information gathering is enabled (`SENTRY_WITH_GPU_INFO=ON`), the **Vulkan** is required for cross-platform GPU detection. For more details, check out the [contribution guide](./CONTRIBUTING.md). Building the Breakpad and Crashpad backends requires a `C++17` compatible compiler. @@ -186,8 +188,8 @@ specifying the `SDKROOT`: $ export SDKROOT=$(xcrun --sdk macosx --show-sdk-path) ``` -If you build on macOS using _CMake 4_, then you _must_ specify the `SDKROOT`, because -[CMake 4 defaults to an empty `CMAKE_OSX_SYSROOT`](https://cmake.org/cmake/help/latest/variable/CMAKE_OSX_SYSROOT.html), +If you build on macOS using _CMake 4_, then you _must_ specify the `SDKROOT`, because +[CMake 4 defaults to an empty `CMAKE_OSX_SYSROOT`](https://cmake.org/cmake/help/latest/variable/CMAKE_OSX_SYSROOT.html), which could lead to inconsistent include paths when CMake tries to gather the `sysroot` later in the build. ### Compile-Time Options @@ -299,20 +301,27 @@ using `cmake -D BUILD_SHARED_LIBS=OFF ..`. tuning the thread stack guarantee parameters. Warnings and errors in the process of setting thread stack guarantees will always be logged. +- `SENTRY_WITH_GPU_INFO` (Default: `ON` on Windows, macOS, and Linux, otherwise `OFF`): + Enables GPU information collection and reporting. When enabled, the SDK will attempt to gather GPU details such as + GPU name, vendor, memory size, and driver version, which are included in event contexts. The implementation uses + the Vulkan API for cross-platform GPU detection. **Requires the Vulkan SDK to be installed** - if not found, + GPU information gathering will be automatically disabled during build. Setting this to `OFF` disables GPU + information collection entirely, which can reduce dependencies and binary size. + ### Support Matrix -| Feature | Windows | macOS | Linux | Android | iOS | -|------------|---------|-------|-------|---------|-------| -| Transports | | | | | | -| - curl | | ☑ | ☑ | (✓)*** | | -| - winhttp | ☑ | | | | | -| - none | ✓ | ✓ | ✓ | ☑ | ☑ | -| | | | | | | -| Backends | | | | | | -| - crashpad | ☑ | ☑ | ☑ | | | -| - breakpad | ✓ | ✓ | ✓ | (✓)** | (✓)** | -| - inproc | ✓ | (✓)* | ✓ | ☑ | | -| - none | ✓ | ✓ | ✓ | ✓ | | +| Feature | Windows | macOS | Linux | Android | iOS | +| ---------- | ------- | ----- | ----- | --------- | ------- | +| Transports | | | | | | +| - curl | | ☑ | ☑ | (✓)\*\*\* | | +| - winhttp | ☑ | | | | | +| - none | ✓ | ✓ | ✓ | ☑ | ☑ | +| | | | | | | +| Backends | | | | | | +| - crashpad | ☑ | ☑ | ☑ | | | +| - breakpad | ✓ | ✓ | ✓ | (✓)\*\* | (✓)\*\* | +| - inproc | ✓ | (✓)\* | ✓ | ☑ | | +| - none | ✓ | ✓ | ✓ | ✓ | | Legend: diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 4ccb3dd9a..367fd2da3 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -197,3 +197,4 @@ target_include_directories(breakpad_client PUBLIC "$" ) + diff --git a/external/vulkan-headers b/external/vulkan-headers new file mode 160000 index 000000000..2efaa559f --- /dev/null +++ b/external/vulkan-headers @@ -0,0 +1 @@ +Subproject commit 2efaa559ff41655ece68b2e904e2bb7e7d55d265 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5b8ccabc5..e66c0211a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -198,3 +198,30 @@ else() screenshot/sentry_screenshot_none.c ) endif() + +# gpu +if(SENTRY_WITH_GPU_INFO) + target_compile_definitions(sentry PRIVATE SENTRY_WITH_GPU_INFO) + sentry_target_sources_cwd(sentry + sentry_gpu.h + gpu/sentry_gpu_common.c + ) + + if(WIN32 OR (APPLE AND NOT IOS) OR LINUX) + sentry_target_sources_cwd(sentry + gpu/sentry_gpu_vulkan.h + gpu/sentry_gpu_vulkan.c + ) + else() + # For platforms that do not support GPU info gathering, we provide a no-op implementation + sentry_target_sources_cwd(sentry + gpu/sentry_gpu_none.c + ) + endif() +else() + sentry_target_sources_cwd(sentry + sentry_gpu.h + gpu/sentry_gpu_common.c + gpu/sentry_gpu_none.c + ) +endif() diff --git a/src/gpu/sentry_gpu_common.c b/src/gpu/sentry_gpu_common.c new file mode 100644 index 000000000..47dd05c58 --- /dev/null +++ b/src/gpu/sentry_gpu_common.c @@ -0,0 +1,161 @@ +#include "sentry_gpu.h" +#include "sentry_logger.h" +#include "sentry_string.h" + +char * +sentry__gpu_vendor_id_to_name(unsigned int vendor_id) +{ + switch (vendor_id) { + case 0x10DE: + return sentry__string_clone("NVIDIA Corporation"); + case 0x1002: + case 0x1022: + return sentry__string_clone("Advanced Micro Devices, Inc. [AMD/ATI]"); + case 0x8086: + return sentry__string_clone("Intel Corporation"); + case 0x106B: + return sentry__string_clone("Apple Inc."); + case 0x1414: + return sentry__string_clone("Microsoft Corporation"); + case 0x5143: + case 0x17CB: + return sentry__string_clone("Qualcomm"); + case 0x1AE0: + return sentry__string_clone("Google"); + case 0x1010: + return sentry__string_clone("VideoLogic"); + case 0x1023: + return sentry__string_clone("Trident Microsystems"); + case 0x102B: + return sentry__string_clone("Matrox Graphics"); + case 0x121A: + return sentry__string_clone("3dfx Interactive"); + case 0x18CA: + return sentry__string_clone("XGI Technology"); + case 0x1039: + return sentry__string_clone("Silicon Integrated Systems [SiS]"); + case 0x126F: + return sentry__string_clone("Silicon Motion"); + default: { + char unknown_vendor[64]; + snprintf(unknown_vendor, sizeof(unknown_vendor), "Unknown (0x%04X)", + vendor_id); + return sentry__string_clone(unknown_vendor); + } + } +} + +static sentry_value_t +create_gpu_context_from_info(sentry_gpu_info_t *gpu_info) +{ + if (!gpu_info) { + SENTRY_WARN("No GPU info provided. Skipping GPU context creation."); + return sentry_value_new_null(); + } + + sentry_value_t gpu_context = sentry_value_new_object(); + if (sentry_value_is_null(gpu_context)) { + return gpu_context; + } + + // Add type field for frontend recognition + sentry_value_set_by_key( + gpu_context, "type", sentry_value_new_string("gpu")); + + // Add GPU name + if (gpu_info->name) { + sentry_value_set_by_key( + gpu_context, "name", sentry_value_new_string(gpu_info->name)); + } + + // Add vendor information + if (gpu_info->vendor_name) { + sentry_value_set_by_key(gpu_context, "vendor_name", + sentry_value_new_string(gpu_info->vendor_name)); + } + + if (gpu_info->vendor_id != 0) { + char vendor_id_str[32]; + snprintf( + vendor_id_str, sizeof(vendor_id_str), "%u", gpu_info->vendor_id); + sentry_value_set_by_key( + gpu_context, "vendor_id", sentry_value_new_string(vendor_id_str)); + } + + // Add device ID + if (gpu_info->device_id != 0) { + char device_id_str[32]; + snprintf( + device_id_str, sizeof(device_id_str), "%u", gpu_info->device_id); + sentry_value_set_by_key( + gpu_context, "device_id", sentry_value_new_string(device_id_str)); + } + + // Add memory size + if (gpu_info->memory_size > 0) { + sentry_value_set_by_key(gpu_context, "memory_size", + sentry_value_new_uint64(gpu_info->memory_size)); + } + + // Add driver version + if (gpu_info->driver_version) { + sentry_value_set_by_key(gpu_context, "driver_version", + sentry_value_new_string(gpu_info->driver_version)); + } + + sentry_value_freeze(gpu_context); + return gpu_context; +} + +void +sentry__free_gpu_info(sentry_gpu_info_t *gpu_info) +{ + if (!gpu_info) { + return; + } + + sentry_free(gpu_info->name); + sentry_free(gpu_info->vendor_name); + sentry_free(gpu_info->driver_version); + sentry_free(gpu_info); +} + +void +sentry__free_gpu_list(sentry_gpu_list_t *gpu_list) +{ + if (!gpu_list) { + return; + } + + for (unsigned int i = 0; i < gpu_list->count; i++) { + sentry__free_gpu_info(gpu_list->gpus[i]); + } + + sentry_free(gpu_list->gpus); + sentry_free(gpu_list); +} + +void +sentry__add_gpu_contexts(sentry_value_t contexts) +{ + sentry_gpu_list_t *gpu_list = sentry__get_gpu_info(); + if (!gpu_list) { + return; + } + + for (unsigned int i = 0; i < gpu_list->count; i++) { + sentry_value_t gpu_context + = create_gpu_context_from_info(gpu_list->gpus[i]); + if (!sentry_value_is_null(gpu_context)) { + char context_key[16]; + if (i == 0) { + snprintf(context_key, sizeof(context_key), "gpu"); + } else { + snprintf(context_key, sizeof(context_key), "gpu%u", i + 1); + } + sentry_value_set_by_key(contexts, context_key, gpu_context); + } + } + + sentry__free_gpu_list(gpu_list); +} diff --git a/src/gpu/sentry_gpu_none.c b/src/gpu/sentry_gpu_none.c new file mode 100644 index 000000000..838d10cf2 --- /dev/null +++ b/src/gpu/sentry_gpu_none.c @@ -0,0 +1,7 @@ +#include "sentry_gpu.h" + +sentry_gpu_list_t * +sentry__get_gpu_info(void) +{ + return NULL; +} diff --git a/src/gpu/sentry_gpu_vulkan.c b/src/gpu/sentry_gpu_vulkan.c new file mode 100644 index 000000000..72bc776e5 --- /dev/null +++ b/src/gpu/sentry_gpu_vulkan.c @@ -0,0 +1,285 @@ +#include "sentry_alloc.h" +#include "sentry_gpu.h" +#include "sentry_logger.h" +#include "sentry_string.h" + +#include +#include + +#ifdef _WIN32 +# include +# define SENTRY_LIBRARY_HANDLE HMODULE +# define SENTRY_LOAD_LIBRARY(name) LoadLibraryA(name) +# define SENTRY_GET_PROC_ADDRESS(handle, name) GetProcAddress(handle, name) +# define SENTRY_FREE_LIBRARY(handle) FreeLibrary(handle) +#elif defined(__APPLE__) +# include +# define SENTRY_LIBRARY_HANDLE void * +# define SENTRY_LOAD_LIBRARY(name) dlopen(name, RTLD_LAZY) +# define SENTRY_GET_PROC_ADDRESS(handle, name) dlsym(handle, name) +# define SENTRY_FREE_LIBRARY(handle) dlclose(handle) +#else +# include +# define SENTRY_LIBRARY_HANDLE void * +# define SENTRY_LOAD_LIBRARY(name) dlopen(name, RTLD_LAZY) +# define SENTRY_GET_PROC_ADDRESS(handle, name) dlsym(handle, name) +# define SENTRY_FREE_LIBRARY(handle) dlclose(handle) +#endif + +// Define MoltenVK constants if not available +#ifndef VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR +# define VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR 0x00000001 +#endif + +// Dynamic function pointers +// Note: These are not thread-safe, but this is not a concern for our use case. +// We are only accessing these during scope initialization, which is explicitly +// locked, so it's fair to assume that only single-threadded access is happening +// here. +static PFN_vkCreateInstance pfn_vkCreateInstance = NULL; +static PFN_vkDestroyInstance pfn_vkDestroyInstance = NULL; +static PFN_vkEnumeratePhysicalDevices pfn_vkEnumeratePhysicalDevices = NULL; +static PFN_vkGetPhysicalDeviceProperties pfn_vkGetPhysicalDeviceProperties + = NULL; +static PFN_vkGetPhysicalDeviceMemoryProperties + pfn_vkGetPhysicalDeviceMemoryProperties + = NULL; + +static SENTRY_LIBRARY_HANDLE vulkan_library = NULL; + +static bool +load_vulkan_library(void) +{ + if (vulkan_library != NULL) { + return true; + } + +#ifdef _WIN32 + vulkan_library = SENTRY_LOAD_LIBRARY("vulkan-1.dll"); +#elif defined(__APPLE__) + vulkan_library = SENTRY_LOAD_LIBRARY("libvulkan.1.dylib"); + if (!vulkan_library) { + vulkan_library = SENTRY_LOAD_LIBRARY("libvulkan.dylib"); + } + if (!vulkan_library) { + vulkan_library + = SENTRY_LOAD_LIBRARY("/usr/local/lib/libvulkan.1.dylib"); + } +#else + vulkan_library = SENTRY_LOAD_LIBRARY("libvulkan.so.1"); + if (!vulkan_library) { + vulkan_library = SENTRY_LOAD_LIBRARY("libvulkan.so"); + } +#endif + + if (!vulkan_library) { + SENTRY_WARN("Failed to load Vulkan library"); + return false; + } + + // Load function pointers + pfn_vkCreateInstance = (PFN_vkCreateInstance)SENTRY_GET_PROC_ADDRESS( + vulkan_library, "vkCreateInstance"); + pfn_vkDestroyInstance = (PFN_vkDestroyInstance)SENTRY_GET_PROC_ADDRESS( + vulkan_library, "vkDestroyInstance"); + pfn_vkEnumeratePhysicalDevices + = (PFN_vkEnumeratePhysicalDevices)SENTRY_GET_PROC_ADDRESS( + vulkan_library, "vkEnumeratePhysicalDevices"); + pfn_vkGetPhysicalDeviceProperties + = (PFN_vkGetPhysicalDeviceProperties)SENTRY_GET_PROC_ADDRESS( + vulkan_library, "vkGetPhysicalDeviceProperties"); + pfn_vkGetPhysicalDeviceMemoryProperties + = (PFN_vkGetPhysicalDeviceMemoryProperties)SENTRY_GET_PROC_ADDRESS( + vulkan_library, "vkGetPhysicalDeviceMemoryProperties"); + + if (!pfn_vkCreateInstance || !pfn_vkDestroyInstance + || !pfn_vkEnumeratePhysicalDevices || !pfn_vkGetPhysicalDeviceProperties + || !pfn_vkGetPhysicalDeviceMemoryProperties) { + SENTRY_WARN("Failed to load required Vulkan functions"); + SENTRY_FREE_LIBRARY(vulkan_library); + vulkan_library = NULL; + return false; + } + + SENTRY_INFO("Successfully loaded Vulkan library and functions"); + return true; +} + +static void +unload_vulkan_library(void) +{ + if (vulkan_library != NULL) { + SENTRY_FREE_LIBRARY(vulkan_library); + vulkan_library = NULL; + pfn_vkCreateInstance = NULL; + pfn_vkDestroyInstance = NULL; + pfn_vkEnumeratePhysicalDevices = NULL; + pfn_vkGetPhysicalDeviceProperties = NULL; + pfn_vkGetPhysicalDeviceMemoryProperties = NULL; + } +} + +static VkInstance +create_vulkan_instance(void) +{ + VkApplicationInfo app_info = { 0 }; + app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + app_info.pApplicationName = "Sentry GPU Info"; + app_info.applicationVersion = VK_MAKE_VERSION(1, 0, 0); + app_info.pEngineName = "Sentry"; + app_info.engineVersion = VK_MAKE_VERSION(1, 0, 0); + app_info.apiVersion = VK_API_VERSION_1_0; + + VkInstanceCreateInfo create_info = { 0 }; + create_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + create_info.pApplicationInfo = &app_info; + +#ifdef __APPLE__ + // Required extensions for MoltenVK on macOS + const char *extensions[] = { "VK_KHR_portability_enumeration", + "VK_KHR_get_physical_device_properties2" }; + create_info.enabledExtensionCount = 2; + create_info.ppEnabledExtensionNames = extensions; + + // Required flag for MoltenVK on macOS + create_info.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + + // Disable validation layers on macOS as they may not be available + create_info.enabledLayerCount = 0; +#endif + + VkInstance instance = VK_NULL_HANDLE; + VkResult result = pfn_vkCreateInstance(&create_info, NULL, &instance); + if (result != VK_SUCCESS) { + SENTRY_DEBUGF("Failed to create Vulkan instance: %d", result); + return VK_NULL_HANDLE; + } + + return instance; +} + +static sentry_gpu_info_t * +create_gpu_info_from_device(VkPhysicalDevice device) +{ + VkPhysicalDeviceProperties properties; + VkPhysicalDeviceMemoryProperties memory_properties; + + pfn_vkGetPhysicalDeviceProperties(device, &properties); + pfn_vkGetPhysicalDeviceMemoryProperties(device, &memory_properties); + + sentry_gpu_info_t *gpu_info = SENTRY_MAKE(sentry_gpu_info_t); + if (!gpu_info) { + return NULL; + } + + memset(gpu_info, 0, sizeof(sentry_gpu_info_t)); + + gpu_info->name = sentry__string_clone(properties.deviceName); + gpu_info->vendor_id = properties.vendorID; + gpu_info->device_id = properties.deviceID; + gpu_info->vendor_name = sentry__gpu_vendor_id_to_name(properties.vendorID); + + char driver_version_str[64]; + uint32_t driver_version = properties.driverVersion; + snprintf(driver_version_str, sizeof(driver_version_str), "%u.%u.%u", + VK_VERSION_MAJOR(driver_version), VK_VERSION_MINOR(driver_version), + VK_VERSION_PATCH(driver_version)); + + gpu_info->driver_version = sentry__string_clone(driver_version_str); + + uint64_t total_memory = 0; + for (uint32_t i = 0; i < memory_properties.memoryHeapCount; i++) { + if (memory_properties.memoryHeaps[i].flags + & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) { + total_memory += memory_properties.memoryHeaps[i].size; + } + } + + // Sentry expects memory size in MB, and Vulkan reports in bytes + gpu_info->memory_size = total_memory / (1024 * 1024); + + return gpu_info; +} + +sentry_gpu_list_t * +sentry__get_gpu_info(void) +{ + if (!load_vulkan_library()) { + return NULL; + } + + VkInstance instance = create_vulkan_instance(); + if (instance == VK_NULL_HANDLE) { + unload_vulkan_library(); + return NULL; + } + + uint32_t device_count = 0; + VkResult result + = pfn_vkEnumeratePhysicalDevices(instance, &device_count, NULL); + if (result != VK_SUCCESS || device_count == 0) { + SENTRY_DEBUGF("Failed to enumerate Vulkan devices: %d", result); + pfn_vkDestroyInstance(instance, NULL); + unload_vulkan_library(); + return NULL; + } + + VkPhysicalDevice *devices + = sentry_malloc(sizeof(VkPhysicalDevice) * device_count); + if (!devices) { + pfn_vkDestroyInstance(instance, NULL); + unload_vulkan_library(); + return NULL; + } + + result = pfn_vkEnumeratePhysicalDevices(instance, &device_count, devices); + if (result != VK_SUCCESS) { + SENTRY_DEBUGF("Failed to get Vulkan physical devices: %d", result); + sentry_free(devices); + pfn_vkDestroyInstance(instance, NULL); + unload_vulkan_library(); + return NULL; + } + + sentry_gpu_list_t *gpu_list = SENTRY_MAKE(sentry_gpu_list_t); + if (!gpu_list) { + sentry_free(devices); + pfn_vkDestroyInstance(instance, NULL); + unload_vulkan_library(); + return NULL; + } + + gpu_list->gpus = sentry_malloc(sizeof(sentry_gpu_info_t *) * device_count); + if (!gpu_list->gpus) { + sentry_free(gpu_list); + sentry_free(devices); + pfn_vkDestroyInstance(instance, NULL); + unload_vulkan_library(); + return NULL; + } + + gpu_list->count = 0; + + for (uint32_t i = 0; i < device_count; i++) { + sentry_gpu_info_t *gpu_info = create_gpu_info_from_device(devices[i]); + if (gpu_info) { + gpu_list->gpus[gpu_list->count] = gpu_info; + gpu_list->count++; + } + } + + sentry_free(devices); + pfn_vkDestroyInstance(instance, NULL); + + if (gpu_list->count == 0) { + sentry_free(gpu_list->gpus); + sentry_free(gpu_list); + unload_vulkan_library(); + return NULL; + } + + // Clean up the dynamically loaded Vulkan library since we're done with it + unload_vulkan_library(); + + return gpu_list; +} diff --git a/src/gpu/sentry_gpu_vulkan.h b/src/gpu/sentry_gpu_vulkan.h new file mode 100644 index 000000000..9a28d58e7 --- /dev/null +++ b/src/gpu/sentry_gpu_vulkan.h @@ -0,0 +1,21 @@ +#ifndef SENTRY_GPU_VULKAN_H_INCLUDED +#define SENTRY_GPU_VULKAN_H_INCLUDED + +#include "sentry_gpu.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Retrieves GPU information using Vulkan API. + * Returns a sentry_gpu_list_t structure that must be freed with + * sentry__free_gpu_list, or NULL if no GPU information could be obtained. + */ +sentry_gpu_list_t *sentry__get_gpu_info(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/sentry_gpu.h b/src/sentry_gpu.h new file mode 100644 index 000000000..08e04b29b --- /dev/null +++ b/src/sentry_gpu.h @@ -0,0 +1,58 @@ +#ifndef SENTRY_GPU_H_INCLUDED +#define SENTRY_GPU_H_INCLUDED + +#include "sentry_boot.h" +#include "sentry_value.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct sentry_gpu_info_s { + char *name; + char *vendor_name; + char *driver_version; + unsigned int vendor_id; + unsigned int device_id; + uint64_t memory_size; +} sentry_gpu_info_t; + +typedef struct sentry_gpu_list_s { + sentry_gpu_info_t **gpus; + unsigned int count; +} sentry_gpu_list_t; + +/** + * Retrieves GPU information for the current system. + * Returns a sentry_gpu_info_t structure that must be freed with + * sentry__free_gpu_info, or NULL if no GPU information could be obtained. + */ +sentry_gpu_list_t *sentry__get_gpu_info(void); + +/** + * Frees the GPU information structure returned by sentry__get_gpu_info. + */ +void sentry__free_gpu_info(sentry_gpu_info_t *gpu_info); + +/** + * Frees the GPU list structure returned by sentry__get_all_gpu_info. + */ +void sentry__free_gpu_list(sentry_gpu_list_t *gpu_list); + +/** + * Maps a GPU vendor ID to a vendor name string. + * Returns a newly allocated string that must be freed, or NULL if unknown. + */ +char *sentry__gpu_vendor_id_to_name(unsigned int vendor_id); + +/** + * Adds GPU context information to the provided contexts object. + * Creates individual contexts named "gpu", "gpu2", "gpu3", etc. for each GPU. + */ +void sentry__add_gpu_contexts(sentry_value_t contexts); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 07bfeb4bb..a1912e2d2 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -4,6 +4,7 @@ #include "sentry_backend.h" #include "sentry_core.h" #include "sentry_database.h" +#include "sentry_gpu.h" #include "sentry_options.h" #include "sentry_os.h" #include "sentry_ringbuffer.h" @@ -95,6 +96,10 @@ get_scope(void) init_scope(&g_scope); sentry_value_set_by_key(g_scope.contexts, "os", sentry__get_os_context()); + + // Add GPU contexts if GPU info is enabled + sentry__add_gpu_contexts(g_scope.contexts); + g_scope.client_sdk = get_client_sdk(); g_scope_initialized = true; diff --git a/tests/assertions.py b/tests/assertions.py index ca9f7c902..1847e23f5 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -52,6 +52,107 @@ def assert_user_feedback(envelope): assert user_feedback["message"] == "some-message" +def assert_gpu_context(event, should_have_gpu=None): + """Assert GPU context in event, with optional expectation control. + + Args: + event: The event to check + should_have_gpu: If True, assert GPU context exists. If False, assert it doesn't. + If None, just validate structure if present. + + Usage: + # Test that GPU context is present and valid + assert_gpu_context(event, should_have_gpu=True) + + # Test that GPU context is absent (when disabled) + assert_gpu_context(event, should_have_gpu=False) + + # Just validate structure if present + assert_gpu_context(event) + """ + contexts = event.get("contexts", {}) + + # Find all GPU contexts (gpu, gpu2, gpu3, etc.) + gpu_contexts = {} + for key, value in contexts.items(): + if key == "gpu" or (key.startswith("gpu") and key[3:].isdigit()): + gpu_contexts[key] = value + + has_gpu = len(gpu_contexts) > 0 + + if should_have_gpu is True: + assert has_gpu, "Expected GPU context to be present" + elif should_have_gpu is False: + assert not has_gpu, "Expected GPU context to be absent" + + if has_gpu: + # Validate each GPU context + for context_key, gpu_context in gpu_contexts.items(): + assert isinstance(gpu_context, dict), f"{context_key} should be an object" + + # Check that type field is set to "gpu" + assert "type" in gpu_context, f"{context_key} should have a 'type' field" + assert gpu_context["type"] == "gpu", f"{context_key} type should be 'gpu'" + + # At least one identifying field should be present + identifying_fields = ["name", "vendor_name", "vendor_id", "device_id"] + assert any( + field in gpu_context for field in identifying_fields + ), f"{context_key} should contain at least one of: {identifying_fields}" + + _validate_single_gpu_context(gpu_context, context_key) + + +def _validate_single_gpu_context(gpu_context, gpu_name): + """Helper function to validate a single GPU context object.""" + # Validate field types and values + if "name" in gpu_context: + assert isinstance( + gpu_context["name"], str + ), f"{gpu_name} name should be a string" + assert len(gpu_context["name"]) > 0, f"{gpu_name} name should not be empty" + + if "vendor_name" in gpu_context: + assert isinstance( + gpu_context["vendor_name"], str + ), f"{gpu_name} vendor_name should be a string" + assert ( + len(gpu_context["vendor_name"]) > 0 + ), f"{gpu_name} vendor_name should not be empty" + + if "vendor_id" in gpu_context: + assert isinstance( + gpu_context["vendor_id"], str + ), f"{gpu_name} vendor_id should be a string" + assert ( + len(gpu_context["vendor_id"]) > 0 + ), f"{gpu_name} vendor_id should not be empty" + + if "device_id" in gpu_context: + assert isinstance( + gpu_context["device_id"], str + ), f"{gpu_name} device_id should be a string" + assert ( + len(gpu_context["device_id"]) > 0 + ), f"{gpu_name} device_id should not be empty" + + if "memory_size" in gpu_context: + assert isinstance( + gpu_context["memory_size"], int + ), f"{gpu_name} memory_size should be an integer" + assert ( + gpu_context["memory_size"] > 0 + ), f"{gpu_name} memory_size should be positive" + + if "driver_version" in gpu_context: + assert isinstance( + gpu_context["driver_version"], str + ), f"{gpu_name} driver_version should be a string" + assert ( + len(gpu_context["driver_version"]) > 0 + ), f"{gpu_name} driver_version should not be empty" + + def assert_user_report(envelope): user_report = None for item in envelope: diff --git a/tests/test_integration_gpu.py b/tests/test_integration_gpu.py new file mode 100644 index 000000000..cef00471c --- /dev/null +++ b/tests/test_integration_gpu.py @@ -0,0 +1,257 @@ +import sys + +import pytest + +from . import check_output, Envelope +from .assertions import ( + assert_meta, + assert_breadcrumb, + assert_event, + assert_gpu_context, +) + + +def test_gpu_context_present_when_enabled(cmake): + """Test that GPU context is present in events when GPU support is enabled.""" + tmp_path = cmake( + ["sentry_example"], + { + "SENTRY_BACKEND": "none", + "SENTRY_TRANSPORT": "none", + "SENTRY_WITH_GPU_INFO": "ON", + }, + ) + + output = check_output( + tmp_path, + "sentry_example", + ["stdout", "capture-event"], + ) + envelope = Envelope.deserialize(output) + + assert_meta(envelope) + assert_breadcrumb(envelope) + assert_event(envelope) + + # Test that GPU context is present and properly structured + event = envelope.get_event() + assert_gpu_context(event, should_have_gpu=None) # Allow either present or absent + + +def test_gpu_context_absent_when_disabled(cmake): + """Test that GPU context is absent in events when GPU support is disabled.""" + tmp_path = cmake( + ["sentry_example"], + { + "SENTRY_BACKEND": "none", + "SENTRY_TRANSPORT": "none", + "SENTRY_WITH_GPU_INFO": "OFF", + }, + ) + + output = check_output( + tmp_path, + "sentry_example", + ["stdout", "capture-event"], + ) + envelope = Envelope.deserialize(output) + + assert_meta(envelope) + assert_breadcrumb(envelope) + assert_event(envelope) + + # Test that GPU context is specifically absent + event = envelope.get_event() + assert_gpu_context(event, should_have_gpu=False) + + +def test_gpu_context_structure_validation(cmake): + """Test that GPU context contains expected fields when present.""" + tmp_path = cmake( + ["sentry_example"], + { + "SENTRY_BACKEND": "none", + "SENTRY_TRANSPORT": "none", + "SENTRY_WITH_GPU_INFO": "ON", + }, + ) + + output = check_output( + tmp_path, + "sentry_example", + ["stdout", "capture-event"], + ) + envelope = Envelope.deserialize(output) + event = envelope.get_event() + + # Check for GPU contexts (gpu, gpu2, gpu3, etc.) + contexts = event.get("contexts", {}) + gpu_contexts = {} + for key, value in contexts.items(): + if key == "gpu" or (key.startswith("gpu") and key[3:].isdigit()): + gpu_contexts[key] = value + + if gpu_contexts: + # Ensure we have at least one GPU + assert len(gpu_contexts) > 0, "Should have at least one GPU context" + + # Validate each GPU context + for context_key, gpu in gpu_contexts.items(): + # Check that type field is set to "gpu" + assert "type" in gpu, f"{context_key} should have a 'type' field" + assert gpu["type"] == "gpu", f"{context_key} type should be 'gpu'" + + # Validate that we have at least basic identifying information + identifying_fields = ["name", "vendor_name", "vendor_id", "device_id"] + assert any( + field in gpu for field in identifying_fields + ), f"{context_key} should contain at least one of: {identifying_fields}" + + # If name is present, it should be meaningful + if "name" in gpu: + name = gpu["name"] + assert isinstance(name, str), f"{context_key} name should be a string" + assert len(name) > 0, f"{context_key} name should not be empty" + # Should not be just a generic placeholder + assert ( + name != "Unknown" + ), f"{context_key} name should be meaningful, not 'Unknown'" + + # If vendor info is present, validate it + if "vendor_name" in gpu: + vendor_name = gpu["vendor_name"] + assert isinstance( + vendor_name, str + ), f"{context_key} vendor_name should be a string" + assert ( + len(vendor_name) > 0 + ), f"{context_key} vendor_name should not be empty" + + if "vendor_id" in gpu: + vendor_id = gpu["vendor_id"] + assert isinstance( + vendor_id, str + ), f"{context_key} vendor_id should be a string" + assert ( + len(vendor_id) > 0 + ), f"{context_key} vendor_id should not be empty" + # Should be a valid number when converted + assert ( + vendor_id.isdigit() + ), f"{context_key} vendor_id should be a numeric string" + + # Check device_id is now a string + if "device_id" in gpu: + device_id = gpu["device_id"] + assert isinstance( + device_id, str + ), f"{context_key} device_id should be a string" + assert ( + len(device_id) > 0 + ), f"{context_key} device_id should not be empty" + + # Memory size should be reasonable if present + if "memory_size" in gpu: + memory_size = gpu["memory_size"] + assert isinstance( + memory_size, int + ), f"{context_key} memory_size should be an integer" + assert memory_size > 0, f"{context_key} memory_size should be positive" + # Should be at least 1MB (very conservative) + assert memory_size >= 1, f"{context_key} memory size seems too small" + + +def test_gpu_context_cross_platform_compatibility(cmake): + """Test that GPU context works across different platforms without breaking.""" + tmp_path = cmake( + ["sentry_example"], + { + "SENTRY_BACKEND": "none", + "SENTRY_TRANSPORT": "none", + "SENTRY_WITH_GPU_INFO": "ON", + }, + ) + + # This should not crash regardless of platform + output = check_output( + tmp_path, + "sentry_example", + ["stdout", "capture-event"], + ) + envelope = Envelope.deserialize(output) + + assert_meta(envelope) + assert_event(envelope) + + # GPU context may or may not be present, but if it is, it should be valid + event = envelope.get_event() + assert_gpu_context(event) # No expectation, just validate if present + + +def test_gpu_context_multi_gpu_support(cmake): + """Test that multi-GPU systems are properly detected and reported.""" + tmp_path = cmake( + ["sentry_example"], + { + "SENTRY_BACKEND": "none", + "SENTRY_TRANSPORT": "none", + "SENTRY_WITH_GPU_INFO": "ON", + }, + ) + + output = check_output( + tmp_path, + "sentry_example", + ["stdout", "capture-event"], + ) + envelope = Envelope.deserialize(output) + + assert_meta(envelope) + assert_event(envelope) + + event = envelope.get_event() + + # Check for GPU contexts (gpu, gpu2, gpu3, etc.) + contexts = event.get("contexts", {}) + gpu_contexts = {} + for key, value in contexts.items(): + if key == "gpu" or (key.startswith("gpu") and key[3:].isdigit()): + gpu_contexts[key] = value + + if gpu_contexts: + print(f"Found {len(gpu_contexts)} GPU context(s) in the system") + + # Test for potential hybrid setups (NVIDIA + other vendors) + nvidia_count = 0 + other_vendors = set() + + for context_key, gpu in gpu_contexts.items(): + print(f"{context_key}: {gpu}") + + # Validate type field + assert "type" in gpu, f"{context_key} should have type field" + assert gpu["type"] == "gpu", f"{context_key} type should be 'gpu'" + + if "vendor_id" in gpu: + vendor_id = int(gpu["vendor_id"]) if gpu["vendor_id"].isdigit() else 0 + if vendor_id == 0x10DE or vendor_id == 4318: # NVIDIA + nvidia_count += 1 + else: + other_vendors.add(vendor_id) + + if nvidia_count > 0 and len(other_vendors) > 0: + print( + f"Hybrid GPU setup detected: {nvidia_count} NVIDIA + {len(other_vendors)} other vendor(s)" + ) + + # In hybrid setups, check for detailed info + for context_key, gpu in gpu_contexts.items(): + if "vendor_id" in gpu: + vendor_id = ( + int(gpu["vendor_id"]) if gpu["vendor_id"].isdigit() else 0 + ) + if vendor_id == 0x10DE or vendor_id == 4318: # NVIDIA + print(f"NVIDIA GPU details ({context_key}): {gpu}") + + # The main validation is handled by assert_gpu_context + assert_gpu_context(event) diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index b6c8dc0fc..df03e26d5 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -29,6 +29,7 @@ add_executable(sentry_test_unit test_envelopes.c test_failures.c test_fuzzfailures.c + test_gpu.c test_info.c test_logger.c test_logs.c diff --git a/tests/unit/test_gpu.c b/tests/unit/test_gpu.c new file mode 100644 index 000000000..b5840fe76 --- /dev/null +++ b/tests/unit/test_gpu.c @@ -0,0 +1,397 @@ +#include "sentry_gpu.h" +#include "sentry_scope.h" +#include "sentry_testsupport.h" + +#include + +SENTRY_TEST(gpu_info_basic) +{ + sentry_gpu_list_t *gpu_list = sentry__get_gpu_info(); + +#ifdef SENTRY_WITH_GPU_INFO + // When GPU support is enabled, we should get some GPU information (at least + // on most systems) + if (gpu_list && gpu_list->count > 0) { + printf("Found %u GPU(s):\n", gpu_list->count); + + // Check that at least one GPU has populated fields + bool has_info = false; + for (unsigned int i = 0; i < gpu_list->count; i++) { + sentry_gpu_info_t *gpu_info = gpu_list->gpus[i]; + TEST_ASSERT(!!gpu_info); + printf("GPU %u:\n", i); + + if (gpu_info->name && strlen(gpu_info->name) > 0) { + has_info = true; + printf(" Name: %s\n", gpu_info->name); + } + if (gpu_info->vendor_name && strlen(gpu_info->vendor_name) > 0) { + has_info = true; + printf(" Vendor: %s\n", gpu_info->vendor_name); + } + if (gpu_info->vendor_id != 0) { + has_info = true; + printf(" Vendor ID: 0x%04X\n", gpu_info->vendor_id); + } + if (gpu_info->device_id != 0) { + has_info = true; + printf(" Device ID: 0x%04X\n", gpu_info->device_id); + } + if (gpu_info->driver_version + && strlen(gpu_info->driver_version) > 0) { + has_info = true; + printf(" Driver Version: %s\n", gpu_info->driver_version); + } + if (gpu_info->memory_size > 0) { + has_info = true; + printf(" Memory Size: %" PRIu64 " bytes\n", + gpu_info->memory_size); + } + } + + TEST_CHECK(has_info); + TEST_MSG("At least one GPU info field should be populated"); + + sentry__free_gpu_list(gpu_list); + } else { + // It's okay if no GPU info is available on some systems (VMs, headless + // systems, etc.) + TEST_MSG("No GPU information available on this system"); + } +#else + // When GPU support is disabled, we should always get NULL + TEST_CHECK(gpu_list == NULL); + TEST_MSG("GPU support disabled - correctly returned NULL"); +#endif +} + +SENTRY_TEST(gpu_info_free_null) +{ + // Test that freeing NULL doesn't crash + sentry__free_gpu_info(NULL); + sentry__free_gpu_list(NULL); + TEST_CHECK(1); // If we get here, the test passed +} + +SENTRY_TEST(gpu_info_vendor_id_known) +{ + sentry_gpu_list_t *gpu_list = sentry__get_gpu_info(); + +#ifdef SENTRY_WITH_GPU_INFO + // Test the common vendor ID to name mapping function with all supported + // vendors + unsigned int test_vendor_ids[] + = { 0x10DE, 0x1002, 0x8086, 0x106B, 0x1414, 0x5143, 0x1AE0, 0x1010, + 0x1023, 0x102B, 0x121A, 0x18CA, 0x1039, 0x126F, 0x0000, 0xFFFF }; + + for (size_t i = 0; i < sizeof(test_vendor_ids) / sizeof(test_vendor_ids[0]); + i++) { + char *vendor_name = sentry__gpu_vendor_id_to_name(test_vendor_ids[i]); + TEST_ASSERT(vendor_name != NULL); + + switch (test_vendor_ids[i]) { + case 0x10DE: + TEST_CHECK(strstr(vendor_name, "NVIDIA") != NULL); + break; + case 0x1002: + TEST_CHECK(strstr(vendor_name, "AMD") != NULL + || strstr(vendor_name, "ATI") != NULL); + break; + case 0x8086: + TEST_CHECK(strstr(vendor_name, "Intel") != NULL); + break; + case 0x106B: + TEST_CHECK(strstr(vendor_name, "Apple") != NULL); + break; + case 0x1414: + TEST_CHECK(strstr(vendor_name, "Microsoft") != NULL); + break; + case 0x5143: + TEST_CHECK(strstr(vendor_name, "Qualcomm") != NULL); + break; + case 0x1AE0: + TEST_CHECK(strstr(vendor_name, "Google") != NULL); + break; + case 0x1010: + TEST_CHECK(strstr(vendor_name, "VideoLogic") != NULL); + break; + case 0x1023: + TEST_CHECK(strstr(vendor_name, "Trident") != NULL); + break; + case 0x102B: + TEST_CHECK(strstr(vendor_name, "Matrox") != NULL); + break; + case 0x121A: + TEST_CHECK(strstr(vendor_name, "3dfx") != NULL); + break; + case 0x18CA: + TEST_CHECK(strstr(vendor_name, "XGI") != NULL); + break; + case 0x1039: + TEST_CHECK(strstr(vendor_name, "SiS") != NULL + || strstr(vendor_name, "Silicon") != NULL); + break; + case 0x126F: + TEST_CHECK(strstr(vendor_name, "Silicon Motion") != NULL); + break; + case 0x0000: + case 0xFFFF: + TEST_CHECK(strstr(vendor_name, "Unknown") != NULL); + TEST_CHECK(strstr(vendor_name, "0x") != NULL); + break; + default: + TEST_CHECK(strstr(vendor_name, "Unknown") != NULL); + TEST_CHECK(strstr(vendor_name, "0x") != NULL); + break; + } + + sentry_free(vendor_name); + } + + // Test with actual GPU info if available + if (gpu_list && gpu_list->count > 0) { + for (unsigned int i = 0; i < gpu_list->count; i++) { + sentry_gpu_info_t *gpu_info = gpu_list->gpus[i]; + TEST_ASSERT(!!gpu_info); + + if (gpu_info->vendor_name) { + char *expected_vendor_name + = sentry__gpu_vendor_id_to_name(gpu_info->vendor_id); + TEST_CHECK(expected_vendor_name != NULL); + + if (expected_vendor_name) { + // Use strstr to check that the vendor name contains + // expected content rather than exact string comparison + // which may be fragile + switch (gpu_info->vendor_id) { + case 0x10DE: // NVIDIA + TEST_CHECK( + strstr(gpu_info->vendor_name, "NVIDIA") != NULL); + break; + case 0x1002: // AMD/ATI + TEST_CHECK(strstr(gpu_info->vendor_name, "AMD") != NULL + || strstr(gpu_info->vendor_name, "ATI") != NULL); + break; + case 0x8086: // Intel + TEST_CHECK( + strstr(gpu_info->vendor_name, "Intel") != NULL); + break; + case 0x106B: // Apple + TEST_CHECK( + strstr(gpu_info->vendor_name, "Apple") != NULL); + break; + case 0x1414: // Microsoft + TEST_CHECK( + strstr(gpu_info->vendor_name, "Microsoft") != NULL); + break; + default: + // For other or unknown vendors, just check it's not + // empty + TEST_CHECK(strlen(gpu_info->vendor_name) > 0); + break; + } + + sentry_free(expected_vendor_name); + } + } + } + + sentry__free_gpu_list(gpu_list); + } else { + TEST_MSG("No GPU vendor ID available for testing"); + } +#else + // When GPU support is disabled, should return NULL + TEST_CHECK(gpu_list == NULL); + TEST_MSG("GPU support disabled - correctly returned NULL"); +#endif +} + +SENTRY_TEST(gpu_info_memory_allocation) +{ + // Test multiple allocations and frees + for (int i = 0; i < 5; i++) { + sentry_gpu_list_t *gpu_list = sentry__get_gpu_info(); +#ifdef SENTRY_WITH_GPU_INFO + if (gpu_list) { + // Verify the structure is properly initialized + TEST_CHECK(gpu_list != NULL); + TEST_CHECK(gpu_list->count >= 0); + if (gpu_list->count > 0) { + TEST_CHECK(gpu_list->gpus != NULL); + } + sentry__free_gpu_list(gpu_list); + } +#else + // When GPU support is disabled, should always be NULL + TEST_CHECK(gpu_list == NULL); +#endif + } + TEST_CHECK(1); // If we get here without crashing, test passed +} + +SENTRY_TEST(gpu_context_scope_integration) +{ + // Test that GPU contexts are properly integrated into scope + sentry_value_t contexts = sentry_value_new_object(); + TEST_CHECK(!sentry_value_is_null(contexts)); + + sentry__add_gpu_contexts(contexts); + +#ifdef SENTRY_WITH_GPU_INFO + // When GPU support is enabled, check if we get valid contexts + sentry_value_t gpu_context = sentry_value_get_by_key(contexts, "gpu"); + + if (!sentry_value_is_null(gpu_context)) { + // GPU context should be an object with type "gpu" + TEST_CHECK( + sentry_value_get_type(gpu_context) == SENTRY_VALUE_TYPE_OBJECT); + + // Check that type field is set to "gpu" + sentry_value_t type_field + = sentry_value_get_by_key(gpu_context, "type"); + TEST_CHECK(!sentry_value_is_null(type_field)); + TEST_CHECK( + sentry_value_get_type(type_field) == SENTRY_VALUE_TYPE_STRING); + + const char *type_str = sentry_value_as_string(type_field); + TEST_CHECK(type_str != NULL); + TEST_CHECK(strcmp(type_str, "gpu") == 0); + + // Check that at least one GPU has valid fields + sentry_value_t name = sentry_value_get_by_key(gpu_context, "name"); + sentry_value_t vendor_name + = sentry_value_get_by_key(gpu_context, "vendor_name"); + sentry_value_t vendor_id + = sentry_value_get_by_key(gpu_context, "vendor_id"); + + bool has_field = !sentry_value_is_null(name) + || !sentry_value_is_null(vendor_name) + || !sentry_value_is_null(vendor_id); + TEST_CHECK(has_field); + TEST_MSG("Primary GPU should contain valid fields"); + + // Check for additional GPUs (gpu2, gpu3, etc.) + for (int i = 2; i <= 4; i++) { + char context_key[16]; + snprintf(context_key, sizeof(context_key), "gpu%d", i); + sentry_value_t additional_gpu + = sentry_value_get_by_key(contexts, context_key); + + if (!sentry_value_is_null(additional_gpu)) { + printf("Found additional GPU context: %s\n", context_key); + + // Check type field + sentry_value_t type_field + = sentry_value_get_by_key(additional_gpu, "type"); + TEST_CHECK(!sentry_value_is_null(type_field)); + const char *type_str = sentry_value_as_string(type_field); + TEST_CHECK(type_str != NULL); + TEST_CHECK(strcmp(type_str, "gpu") == 0); + } + } + } else { + TEST_MSG("No GPU context available on this system"); + } +#else + // When GPU support is disabled, should not have gpu context + sentry_value_t gpu_context = sentry_value_get_by_key(contexts, "gpu"); + TEST_CHECK(sentry_value_is_null(gpu_context)); + TEST_MSG("GPU support disabled - correctly no GPU context"); +#endif + + sentry_value_decref(contexts); +} + +SENTRY_TEST(gpu_info_multi_gpu_support) +{ + sentry_gpu_list_t *gpu_list = sentry__get_gpu_info(); + +#ifdef SENTRY_WITH_GPU_INFO + if (gpu_list && gpu_list->count > 0) { + printf("Testing multi-GPU support with %u GPU(s)\n", gpu_list->count); + + // Test that all GPUs in the list are properly initialized + for (unsigned int i = 0; i < gpu_list->count; i++) { + sentry_gpu_info_t *gpu_info = gpu_list->gpus[i]; + TEST_CHECK(gpu_info != NULL); + + // At least vendor_id should be set for each GPU + if (gpu_info->vendor_id == 0 + && (!gpu_info->name || strlen(gpu_info->name) == 0)) { + TEST_MSG("GPU entry has no identifying information"); + } + + printf("GPU %u: vendor_id=0x%04X, name=%s\n", i, + gpu_info->vendor_id, + gpu_info->name ? gpu_info->name : "(null)"); + } + + // Test that we don't have duplicate pointers in the array + if (gpu_list->count > 1) { + for (unsigned int i = 0; i < gpu_list->count - 1; i++) { + for (unsigned int j = i + 1; j < gpu_list->count; j++) { + TEST_CHECK(gpu_list->gpus[i] != gpu_list->gpus[j]); + } + } + } + + sentry__free_gpu_list(gpu_list); + } else { + TEST_MSG("No multi-GPU setup detected - this is normal"); + } +#else + TEST_CHECK(gpu_list == NULL); + TEST_MSG("GPU support disabled - correctly returned NULL"); +#endif +} + +SENTRY_TEST(gpu_info_hybrid_setup_simulation) +{ + // This test simulates what should happen in a hybrid GPU setup + sentry_gpu_list_t *gpu_list = sentry__get_gpu_info(); + +#ifdef SENTRY_WITH_GPU_INFO + if (gpu_list && gpu_list->count > 1) { + printf("Hybrid GPU setup detected with %u GPUs\n", gpu_list->count); + + bool has_nvidia = false; + bool has_other = false; + + for (unsigned int i = 0; i < gpu_list->count; i++) { + sentry_gpu_info_t *gpu_info = gpu_list->gpus[i]; + + if (gpu_info->vendor_id == 0x10DE) { // NVIDIA + has_nvidia = true; + printf("Found NVIDIA GPU: %s\n", + gpu_info->name ? gpu_info->name : "Unknown"); + + // NVIDIA GPUs should have more detailed info if NVML worked + if (gpu_info->driver_version) { + printf(" Driver: %s\n", gpu_info->driver_version); + } + if (gpu_info->memory_size > 0) { + printf( + " Memory: %" PRIu64 " bytes\n", gpu_info->memory_size); + } + } else { + has_other = true; + printf("Found other GPU: vendor=0x%04X, name=%s\n", + gpu_info->vendor_id, + gpu_info->name ? gpu_info->name : "Unknown"); + } + } + + if (has_nvidia && has_other) { + TEST_MSG("Successfully detected hybrid NVIDIA + other GPU setup"); + } + + sentry__free_gpu_list(gpu_list); + } else { + TEST_MSG("No hybrid GPU setup detected - this is normal"); + } +#else + TEST_CHECK(gpu_list == NULL); + TEST_MSG("GPU support disabled"); +#endif +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 4d2407cf9..f6399e99c 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -77,6 +77,13 @@ XX(event_with_id) XX(exception_without_type_or_value_still_valid) XX(formatted_log_messages) XX(fuzz_json) +XX(gpu_context_scope_integration) +XX(gpu_info_basic) +XX(gpu_info_free_null) +XX(gpu_info_hybrid_setup_simulation) +XX(gpu_info_memory_allocation) +XX(gpu_info_multi_gpu_support) +XX(gpu_info_vendor_id_known) XX(init_failure) XX(internal_uuid_api) XX(invalid_dsn)