#ifdef ENABLE_VULKAN

#include <vk_peak.h>
#include <inventory.h>
#include <cstring>
#include <sstream>
#include <chrono>
#include <cfloat>
#if defined(_WIN32)
#include <windows.h>
#else
#include <time.h>
#endif

// ---------------------------------------------------------------------------
// VulkanDevice
// ---------------------------------------------------------------------------

VulkanDevice::VulkanDevice()
  : physicalDevice(VK_NULL_HANDLE), device(VK_NULL_HANDLE),
    computeQueue(VK_NULL_HANDLE), commandPool(VK_NULL_HANDLE)
{
}

VulkanDevice::~VulkanDevice()
{
  cleanup();
}

bool VulkanDevice::init(VkInstance inst, VkPhysicalDevice physDev)
{
  physicalDevice = physDev;

  // Query properties
  VkPhysicalDeviceProperties props;
  vkGetPhysicalDeviceProperties(physDev, &props);

  info.deviceName = props.deviceName;
  info.apiVersion = std::to_string(VK_VERSION_MAJOR(props.apiVersion)) + "." +
                    std::to_string(VK_VERSION_MINOR(props.apiVersion)) + "." +
                    std::to_string(VK_VERSION_PATCH(props.apiVersion));
  info.driverVersion = std::to_string(VK_VERSION_MAJOR(props.driverVersion)) + "." +
                       std::to_string(VK_VERSION_MINOR(props.driverVersion)) + "." +
                       std::to_string(VK_VERSION_PATCH(props.driverVersion));
  info.maxWGSize = std::min(props.limits.maxComputeWorkGroupSize[0], (uint32_t)MAX_WG_SIZE);
  info.maxAllocSize = props.limits.maxStorageBufferRange;
  info.deviceType = props.deviceType;

  // CU/SM count is filled in below from vendor property extensions if the
  // implementation advertises them.  When unavailable (Intel, MoltenVK, older
  // drivers) the dispatch helpers fall back to a fixed 32M-thread floor.
  info.numCUs = 0;

  // Get memory heap size (device-local)
  VkPhysicalDeviceMemoryProperties memProps;
  vkGetPhysicalDeviceMemoryProperties(physDev, &memProps);
  info.heapSize = 0;
  for (uint32_t i = 0; i < memProps.memoryHeapCount; i++)
  {
    if (memProps.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT)
    {
      info.heapSize = std::max(info.heapSize, (uint64_t)memProps.memoryHeaps[i].size);
    }
  }

  // Find compute queue family
  uint32_t queueFamilyCount = 0;
  vkGetPhysicalDeviceQueueFamilyProperties(physDev, &queueFamilyCount, nullptr);
  std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
  vkGetPhysicalDeviceQueueFamilyProperties(physDev, &queueFamilyCount, queueFamilies.data());

  info.computeQueueFamily = UINT32_MAX;
  for (uint32_t i = 0; i < queueFamilyCount; i++)
  {
    if (queueFamilies[i].queueFlags & VK_QUEUE_COMPUTE_BIT)
    {
      info.computeQueueFamily = i;
      break;
    }
  }
  if (info.computeQueueFamily == UINT32_MAX)
    return false;

  // Query supported extensions
  uint32_t extCount = 0;
  vkEnumerateDeviceExtensionProperties(physDev, nullptr, &extCount, nullptr);
  std::vector<VkExtensionProperties> extProps(extCount);
  vkEnumerateDeviceExtensionProperties(physDev, nullptr, &extCount, extProps.data());
  auto hasExt = [&](const char *name) {
    for (auto &e : extProps)
      if (strcmp(e.extensionName, name) == 0) return true;
    return false;
  };

  // Probe vendor property extensions for CU/SM count.  These are query-only
  // (no feature bits, no need to chain at vkCreateDevice -- the spec lets us
  // read them via vkGetPhysicalDeviceProperties2 as long as the extension is
  // advertised).  Order matters: prefer the most specific count.
  {
    VkPhysicalDeviceProperties2 p2 = {};
    p2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;

    if (hasExt("VK_NV_shader_sm_builtins"))
    {
      VkPhysicalDeviceShaderSMBuiltinsPropertiesNV smProps = {};
      smProps.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_SM_BUILTINS_PROPERTIES_NV;
      p2.pNext = &smProps;
      vkGetPhysicalDeviceProperties2(physDev, &p2);
      info.numCUs = smProps.shaderSMCount;
    }
    else if (hasExt("VK_AMD_shader_core_properties2"))
    {
      VkPhysicalDeviceShaderCoreProperties2AMD coreProps2 = {};
      coreProps2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_CORE_PROPERTIES_2_AMD;
      p2.pNext = &coreProps2;
      vkGetPhysicalDeviceProperties2(physDev, &p2);
      info.numCUs = coreProps2.activeComputeUnitCount;
    }
    else if (hasExt("VK_AMD_shader_core_properties"))
    {
      VkPhysicalDeviceShaderCorePropertiesAMD coreProps = {};
      coreProps.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_CORE_PROPERTIES_AMD;
      p2.pNext = &coreProps;
      vkGetPhysicalDeviceProperties2(physDev, &p2);
      info.numCUs = coreProps.shaderEngineCount *
                    coreProps.shaderArraysPerEngineCount *
                    coreProps.computeUnitsPerShaderArray;
    }
    else if (hasExt("VK_ARM_shader_core_builtins"))
    {
      VkPhysicalDeviceShaderCoreBuiltinsPropertiesARM armProps = {};
      armProps.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_CORE_BUILTINS_PROPERTIES_ARM;
      p2.pNext = &armProps;
      vkGetPhysicalDeviceProperties2(physDev, &p2);
      info.numCUs = armProps.shaderCoreCount;
    }
  }

  std::vector<const char *> enabledExts;
  info.int8DotProductSupported = false;
  info.float16Supported = false;
  info.float64Supported = false;
  info.bfloat16Supported = false;
  info.cooperativeMatrixSupported = false;
  info.fp8Supported = false;
  info.atomicFloat32Supported = false;
  info.atomicInt64Supported = false;
  info.coopmatFP16Supported = false;
  info.coopmatBF16Supported = false;
  info.coopmatINT8K = 0;
  info.coopmatFP8E4M3Supported = false;
  info.coopmatFP8E5M2Supported = false;
  info.coopmatFP32Supported = false;
  info.calibratedTimestampsSupported = false;

  // Feature query for optional shader types.  Uses vkGetPhysicalDeviceFeatures2
  // (Vulkan 1.1 core).  Each optional shader's feature struct gets chained
  // into pNext, queried, and then re-chained at vkCreateDevice time if the
  // driver advertises the capability we need.  Adding a new dtype reduces
  // to: declare the struct + macro + two conditional blocks here and one
  // runCompute* wrapper.
  //
  // On Android API < 28 Features2 isn't in the stub loader, so none of the
  // optional shader defines should be active unless the NDK provides a
  // modern-enough loader (see CompileShaders.cmake).
#if defined(CLPEAK_VK_HAS_COMPUTE_INT8_DP_V1) || defined(CLPEAK_VK_HAS_COMPUTE_MP_V1) || defined(CLPEAK_VK_HAS_COMPUTE_BF16_V1) || defined(CLPEAK_VK_HAS_ANY_COOPMAT) || defined(CLPEAK_VK_HAS_COMPUTE_DP_V1) || defined(CLPEAK_VK_HAS_ATOMIC_THROUGHPUT_GLOBAL_FLOAT) || defined(CLPEAK_VK_HAS_ATOMIC_THROUGHPUT_GLOBAL_UINT64)
  VkPhysicalDeviceShaderFloat16Int8FeaturesKHR f16i8Features = {};
  f16i8Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_FLOAT16_INT8_FEATURES_KHR;
#ifdef CLPEAK_VK_HAS_COMPUTE_INT8_DP_V1
  VkPhysicalDeviceShaderIntegerDotProductFeaturesKHR dpFeatures = {};
  dpFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_INTEGER_DOT_PRODUCT_FEATURES_KHR;
#endif
#if defined(CLPEAK_VK_HAS_COMPUTE_BF16_V1) || defined(CLPEAK_VK_HAS_COOPMAT_BF16)
  VkPhysicalDeviceShaderBfloat16FeaturesKHR bf16Features = {};
  bf16Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_BFLOAT16_FEATURES_KHR;
#endif
#ifdef CLPEAK_VK_HAS_ANY_COOPMAT
  VkPhysicalDeviceCooperativeMatrixFeaturesKHR coopmatFeatures = {};
  coopmatFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COOPERATIVE_MATRIX_FEATURES_KHR;
  // Cooperative-matrix shaders emitted by glslang use MemoryModel Vulkan via
  // GL_KHR_memory_scope_semantics, which requires the vulkanMemoryModel
  // feature bit (core in 1.2, otherwise VK_KHR_vulkan_memory_model).
  VkPhysicalDeviceVulkanMemoryModelFeatures vmmFeatures = {};
  vmmFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_MEMORY_MODEL_FEATURES;
#ifdef CLPEAK_VK_HAS_ANY_COOPMAT_FP8
  VkPhysicalDeviceShaderFloat8FeaturesEXT fp8Features = {};
  fp8Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_FLOAT8_FEATURES_EXT;
#endif
#endif
#ifdef CLPEAK_VK_HAS_ATOMIC_THROUGHPUT_GLOBAL_FLOAT
  VkPhysicalDeviceShaderAtomicFloatFeaturesEXT atomicFloatFeatures = {};
  atomicFloatFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_ATOMIC_FLOAT_FEATURES_EXT;
#endif
#ifdef CLPEAK_VK_HAS_ATOMIC_THROUGHPUT_GLOBAL_UINT64
  VkPhysicalDeviceShaderAtomicInt64Features atomicInt64Features = {};
  atomicInt64Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_ATOMIC_INT64_FEATURES;
#endif
  // Base features (shaderFloat64 etc.) come back inside Features2.features.
  VkPhysicalDeviceFeatures2 baseFeatures2 = {};
  baseFeatures2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;

  // Small helper: prepend a features struct onto a pNext chain.
  // All VkPhysicalDevice*Features* structs begin with the (sType, pNext)
  // pair described by VkBaseOutStructure, which is what we rely on here.
  auto chainPNext = [](void *head, void *newNode) -> void * {
    reinterpret_cast<VkBaseOutStructure *>(newNode)->pNext =
        reinterpret_cast<VkBaseOutStructure *>(head);
    return newNode;
  };

  bool f16i8ExtEnabled = false;
  bool hasF16I8Ext = hasExt(VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME);
#ifdef CLPEAK_VK_HAS_ANY_COOPMAT
  bool hasCoopmatExt = false;
#endif
#ifdef CLPEAK_VK_HAS_ANY_COOPMAT_FP8
  bool hasFP8Ext = false;
#endif
#ifdef CLPEAK_VK_HAS_ATOMIC_THROUGHPUT_GLOBAL_FLOAT
  bool hasAtomicFloatExt = false;
#endif
#ifdef CLPEAK_VK_HAS_ATOMIC_THROUGHPUT_GLOBAL_UINT64
  bool hasAtomicInt64Ext = false;
#endif
  {
    void *chain = nullptr;
    if (hasF16I8Ext)
      chain = chainPNext(chain, &f16i8Features);
#ifdef CLPEAK_VK_HAS_COMPUTE_INT8_DP_V1
    if (hasExt(VK_KHR_SHADER_INTEGER_DOT_PRODUCT_EXTENSION_NAME))
      chain = chainPNext(chain, &dpFeatures);
#endif
#if defined(CLPEAK_VK_HAS_COMPUTE_BF16_V1) || defined(CLPEAK_VK_HAS_COOPMAT_BF16)
    if (hasExt(VK_KHR_SHADER_BFLOAT16_EXTENSION_NAME))
      chain = chainPNext(chain, &bf16Features);
#endif
#ifdef CLPEAK_VK_HAS_ANY_COOPMAT
    // Cooperative-matrix requires vulkanMemoryModel.  Chain both feature
    // structs whenever the coopmat extension is present; the memory-model
    // struct is core-1.2 and is what drivers advertising coopmat expose.
    hasCoopmatExt = hasExt(VK_KHR_COOPERATIVE_MATRIX_EXTENSION_NAME);
    if (hasCoopmatExt)
    {
      chain = chainPNext(chain, &coopmatFeatures);
      chain = chainPNext(chain, &vmmFeatures);
    }
#ifdef CLPEAK_VK_HAS_ANY_COOPMAT_FP8
    hasFP8Ext = hasExt(VK_EXT_SHADER_FLOAT8_EXTENSION_NAME);
    if (hasFP8Ext)
      chain = chainPNext(chain, &fp8Features);
#endif
#endif
#ifdef CLPEAK_VK_HAS_ATOMIC_THROUGHPUT_GLOBAL_FLOAT
    hasAtomicFloatExt = hasExt("VK_EXT_shader_atomic_float");
    if (hasAtomicFloatExt)
      chain = chainPNext(chain, &atomicFloatFeatures);
#endif
#ifdef CLPEAK_VK_HAS_ATOMIC_THROUGHPUT_GLOBAL_UINT64
    hasAtomicInt64Ext = hasExt(VK_KHR_SHADER_ATOMIC_INT64_EXTENSION_NAME);
    if (hasAtomicInt64Ext)
      chain = chainPNext(chain, &atomicInt64Features);
#endif

    VkPhysicalDeviceFeatures2 features2 = {};
    features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
    features2.pNext = chain;
    // Resolve at runtime: on Android API 26 the NDK's libvulkan.so does not
    // export the 1.1-core symbol statically, but the 1.1 driver underneath
    // still answers the call.  Fall back to the KHR alias (VK_KHR_get_phys-
    // ical_device_properties2, a 1.0 extension) if the core entrypoint is
    // missing.  If neither resolves, leave all optional features disabled.
    auto pfnGetFeat2 = (PFN_vkGetPhysicalDeviceFeatures2)
        vkGetInstanceProcAddr(inst, "vkGetPhysicalDeviceFeatures2");
    if (!pfnGetFeat2)
      pfnGetFeat2 = (PFN_vkGetPhysicalDeviceFeatures2)
          vkGetInstanceProcAddr(inst, "vkGetPhysicalDeviceFeatures2KHR");
    if (pfnGetFeat2)
      pfnGetFeat2(physDev, &features2);
    baseFeatures2.features = features2.features;

#ifdef CLPEAK_VK_HAS_COMPUTE_DP_V1
    if (features2.features.shaderFloat64)
      info.float64Supported = true;
#endif
#ifdef CLPEAK_VK_HAS_ATOMIC_THROUGHPUT_GLOBAL_FLOAT
    if (hasAtomicFloatExt && atomicFloatFeatures.shaderBufferFloat32AtomicAdd)
    {
      enabledExts.push_back("VK_EXT_shader_atomic_float");
      info.atomicFloat32Supported = true;
    }
#endif
#ifdef CLPEAK_VK_HAS_ATOMIC_THROUGHPUT_GLOBAL_UINT64
    if (hasAtomicInt64Ext && atomicInt64Features.shaderBufferInt64Atomics)
    {
      enabledExts.push_back(VK_KHR_SHADER_ATOMIC_INT64_EXTENSION_NAME);
      info.atomicInt64Supported = true;
    }
#endif
#ifdef CLPEAK_VK_HAS_COMPUTE_MP_V1
    if (f16i8Features.shaderFloat16)
    {
      enabledExts.push_back(VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME);
      f16i8ExtEnabled = true;
      info.float16Supported = true;
    }
#endif
#ifdef CLPEAK_VK_HAS_COMPUTE_INT8_DP_V1
    if (hasExt(VK_KHR_SHADER_INTEGER_DOT_PRODUCT_EXTENSION_NAME) &&
        dpFeatures.shaderIntegerDotProduct && f16i8Features.shaderInt8)
    {
      enabledExts.push_back(VK_KHR_SHADER_INTEGER_DOT_PRODUCT_EXTENSION_NAME);
      if (!f16i8ExtEnabled)
      {
        enabledExts.push_back(VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME);
        f16i8ExtEnabled = true;
      }
      if (hasExt(VK_KHR_8BIT_STORAGE_EXTENSION_NAME))
        enabledExts.push_back(VK_KHR_8BIT_STORAGE_EXTENSION_NAME);
      info.int8DotProductSupported = true;
    }
#endif
#if defined(CLPEAK_VK_HAS_COMPUTE_BF16_V1) || defined(CLPEAK_VK_HAS_COOPMAT_BF16)
    if (hasExt(VK_KHR_SHADER_BFLOAT16_EXTENSION_NAME) && bf16Features.shaderBFloat16Type)
    {
      enabledExts.push_back(VK_KHR_SHADER_BFLOAT16_EXTENSION_NAME);
      info.bfloat16Supported = true;
    }
#endif
#ifdef CLPEAK_VK_HAS_ANY_COOPMAT
    if (hasCoopmatExt && coopmatFeatures.cooperativeMatrix && vmmFeatures.vulkanMemoryModel)
    {
      enabledExts.push_back(VK_KHR_COOPERATIVE_MATRIX_EXTENSION_NAME);
      info.cooperativeMatrixSupported = true;
      // FP16 coopmat inputs also need shaderFloat16.
#ifdef CLPEAK_VK_HAS_COOPMAT_FP16
      if (f16i8Features.shaderFloat16 && !f16i8ExtEnabled)
      {
        enabledExts.push_back(VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME);
        f16i8ExtEnabled = true;
        info.float16Supported = true;
      }
#endif
      // INT8 coopmat inputs also need shaderInt8 + 8-bit storage.
#ifdef CLPEAK_VK_HAS_COOPMAT_INT8
      if (f16i8Features.shaderInt8)
      {
        if (!f16i8ExtEnabled)
        {
          enabledExts.push_back(VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME);
          f16i8ExtEnabled = true;
        }
        if (hasExt(VK_KHR_8BIT_STORAGE_EXTENSION_NAME))
          enabledExts.push_back(VK_KHR_8BIT_STORAGE_EXTENSION_NAME);
      }
#endif
      // FP8 coopmat inputs additionally need VK_EXT_shader_float8 with
      // shaderFloat8 + shaderFloat8CooperativeMatrix both advertised.
#ifdef CLPEAK_VK_HAS_ANY_COOPMAT_FP8
      if (hasFP8Ext && fp8Features.shaderFloat8 &&
          fp8Features.shaderFloat8CooperativeMatrix)
      {
        enabledExts.push_back(VK_EXT_SHADER_FLOAT8_EXTENSION_NAME);
        info.fp8Supported = true;
      }
#endif
    }
#endif
  }
  (void)f16i8ExtEnabled;
#endif

#if defined(__APPLE__) || defined(__MACOSX)
  if (hasExt("VK_KHR_portability_subset"))
    enabledExts.push_back("VK_KHR_portability_subset");
#endif

  // VK_EXT_calibrated_timestamps lets us pin a host time into the GPU
  // timestamp domain, which is exactly what's needed to measure one-way
  // dispatch latency (host submit -> GPU kernel start) the same way
  // OpenCL does via CL_PROFILING_COMMAND_QUEUED.  Optional.
  if (hasExt("VK_EXT_calibrated_timestamps"))
  {
    enabledExts.push_back("VK_EXT_calibrated_timestamps");
    info.calibratedTimestampsSupported = true;
  }

  // Create logical device
  float queuePriority = 1.0f;
  VkDeviceQueueCreateInfo queueCI = {};
  queueCI.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
  queueCI.queueFamilyIndex = info.computeQueueFamily;
  queueCI.queueCount = 1;
  queueCI.pQueuePriorities = &queuePriority;

  VkDeviceCreateInfo deviceCI = {};
  deviceCI.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
  deviceCI.queueCreateInfoCount = 1;
  deviceCI.pQueueCreateInfos = &queueCI;
  deviceCI.enabledExtensionCount = (uint32_t)enabledExts.size();
  deviceCI.ppEnabledExtensionNames = enabledExts.empty() ? nullptr : enabledExts.data();

#if defined(CLPEAK_VK_HAS_COMPUTE_INT8_DP_V1) || defined(CLPEAK_VK_HAS_COMPUTE_MP_V1) || defined(CLPEAK_VK_HAS_COMPUTE_BF16_V1) || defined(CLPEAK_VK_HAS_ANY_COOPMAT) || defined(CLPEAK_VK_HAS_COMPUTE_DP_V1) || defined(CLPEAK_VK_HAS_ATOMIC_THROUGHPUT_GLOBAL_FLOAT) || defined(CLPEAK_VK_HAS_ATOMIC_THROUGHPUT_GLOBAL_UINT64)
  // Re-chain the feature structs we actually enabled for vkCreateDevice.
  // The query-phase chain is discarded; these are the features we ask the
  // driver to turn on.
  VkPhysicalDeviceFeatures2 enabledFeatures2 = {};
  enabledFeatures2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
  void *enabledChain = nullptr;
#ifdef CLPEAK_VK_HAS_COMPUTE_DP_V1
  if (info.float64Supported)
    enabledFeatures2.features.shaderFloat64 = VK_TRUE;
#endif
  if (info.float16Supported || info.int8DotProductSupported)
  {
    f16i8Features.pNext = nullptr;
    enabledChain = chainPNext(enabledChain, &f16i8Features);
  }
#ifdef CLPEAK_VK_HAS_COMPUTE_INT8_DP_V1
  if (info.int8DotProductSupported)
  {
    dpFeatures.pNext = nullptr;
    enabledChain = chainPNext(enabledChain, &dpFeatures);
  }
#endif
#if defined(CLPEAK_VK_HAS_COMPUTE_BF16_V1) || defined(CLPEAK_VK_HAS_COOPMAT_BF16)
  if (info.bfloat16Supported)
  {
    bf16Features.pNext = nullptr;
    enabledChain = chainPNext(enabledChain, &bf16Features);
  }
#endif
#ifdef CLPEAK_VK_HAS_ANY_COOPMAT
  if (info.cooperativeMatrixSupported)
  {
    coopmatFeatures.pNext = nullptr;
    enabledChain = chainPNext(enabledChain, &coopmatFeatures);
    vmmFeatures.pNext = nullptr;
    enabledChain = chainPNext(enabledChain, &vmmFeatures);
  }
#ifdef CLPEAK_VK_HAS_ANY_COOPMAT_FP8
  if (info.fp8Supported)
  {
    fp8Features.pNext = nullptr;
    enabledChain = chainPNext(enabledChain, &fp8Features);
  }
#endif
#endif
#ifdef CLPEAK_VK_HAS_ATOMIC_THROUGHPUT_GLOBAL_FLOAT
  if (info.atomicFloat32Supported)
  {
    atomicFloatFeatures.pNext = nullptr;
    enabledChain = chainPNext(enabledChain, &atomicFloatFeatures);
  }
#endif
#ifdef CLPEAK_VK_HAS_ATOMIC_THROUGHPUT_GLOBAL_UINT64
  if (info.atomicInt64Supported)
  {
    atomicInt64Features.pNext = nullptr;
    enabledChain = chainPNext(enabledChain, &atomicInt64Features);
  }
#endif
  // Always pass enabledFeatures2 if any enabled state present (chain or base feats).
  bool haveBaseFeats = enabledFeatures2.features.shaderFloat64 != VK_FALSE;
  if (enabledChain || haveBaseFeats)
  {
    enabledFeatures2.pNext = enabledChain;
    deviceCI.pNext = &enabledFeatures2;
  }
  (void)baseFeatures2;
#endif

  if (vkCreateDevice(physDev, &deviceCI, nullptr, &device) != VK_SUCCESS)
    return false;

  vkGetDeviceQueue(device, info.computeQueueFamily, 0, &computeQueue);

  // Create command pool
  VkCommandPoolCreateInfo poolCI = {};
  poolCI.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
  poolCI.queueFamilyIndex = info.computeQueueFamily;
  poolCI.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;

  if (vkCreateCommandPool(device, &poolCI, nullptr, &commandPool) != VK_SUCCESS)
    return false;

  return true;
}

void VulkanDevice::cleanup()
{
  if (device != VK_NULL_HANDLE)
  {
    vkDeviceWaitIdle(device);
    if (commandPool != VK_NULL_HANDLE)
      vkDestroyCommandPool(device, commandPool, nullptr);
    vkDestroyDevice(device, nullptr);
    device = VK_NULL_HANDLE;
    commandPool = VK_NULL_HANDLE;
  }
}

bool VulkanDevice::createBuffer(VkDeviceSize size, VkBufferUsageFlags usage,
                                VkMemoryPropertyFlags memProps,
                                VkBuffer &buffer, VkDeviceMemory &memory)
{
  VkBufferCreateInfo bufCI = {};
  bufCI.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
  bufCI.size = size;
  bufCI.usage = usage;
  bufCI.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

  if (vkCreateBuffer(device, &bufCI, nullptr, &buffer) != VK_SUCCESS)
    return false;

  VkMemoryRequirements memReqs;
  vkGetBufferMemoryRequirements(device, buffer, &memReqs);

  VkMemoryAllocateInfo allocInfo = {};
  allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
  allocInfo.allocationSize = memReqs.size;
  allocInfo.memoryTypeIndex = findMemoryType(memReqs.memoryTypeBits, memProps);

  if (allocInfo.memoryTypeIndex == UINT32_MAX)
  {
    vkDestroyBuffer(device, buffer, nullptr);
    return false;
  }

  if (vkAllocateMemory(device, &allocInfo, nullptr, &memory) != VK_SUCCESS)
  {
    vkDestroyBuffer(device, buffer, nullptr);
    return false;
  }

  vkBindBufferMemory(device, buffer, memory, 0);
  return true;
}

bool VulkanDevice::createComputePipeline(const uint32_t *spirv, size_t spirvSize,
                                         VkDescriptorSetLayout dsLayout,
                                         VkPipelineLayout pipeLayout,
                                         VkPipeline &pipeline)
{
  VkShaderModuleCreateInfo smCI = {};
  smCI.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
  smCI.codeSize = spirvSize;
  smCI.pCode = spirv;

  VkShaderModule shaderModule;
  if (vkCreateShaderModule(device, &smCI, nullptr, &shaderModule) != VK_SUCCESS)
    return false;

  VkPipelineShaderStageCreateInfo stageCI = {};
  stageCI.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
  stageCI.stage = VK_SHADER_STAGE_COMPUTE_BIT;
  stageCI.module = shaderModule;
  stageCI.pName = "main";

  VkComputePipelineCreateInfo pipelineCI = {};
  pipelineCI.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO;
  pipelineCI.stage = stageCI;
  pipelineCI.layout = pipeLayout;

  VkResult result = vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &pipelineCI, nullptr, &pipeline);
  vkDestroyShaderModule(device, shaderModule, nullptr);

  return result == VK_SUCCESS;
}

VkResult VulkanDevice::submitAndWait(VkCommandBuffer cmdBuf)
{
  VkSubmitInfo submitInfo = {};
  submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
  submitInfo.commandBufferCount = 1;
  submitInfo.pCommandBuffers = &cmdBuf;

  VkResult sr = vkQueueSubmit(computeQueue, 1, &submitInfo, VK_NULL_HANDLE);
  VkResult wr = vkQueueWaitIdle(computeQueue);
  return sr != VK_SUCCESS ? sr : wr;
}

uint32_t VulkanDevice::findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties)
{
  VkPhysicalDeviceMemoryProperties memProps;
  vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProps);

  for (uint32_t i = 0; i < memProps.memoryTypeCount; i++)
  {
    if ((typeFilter & (1 << i)) &&
        (memProps.memoryTypes[i].propertyFlags & properties) == properties)
    {
      return i;
    }
  }
  return UINT32_MAX;
}

// ---------------------------------------------------------------------------
// vkPeak
// ---------------------------------------------------------------------------

vkPeak::vkPeak()
  : warmupCount(2), specifiedIters(0), forceIters(false),
    deviceIndex(-1),
    instance(VK_NULL_HANDLE)
{
  gating.enableAll();
}

vkPeak::~vkPeak()
{
  cleanup();
}

bool vkPeak::initInstance()
{
  VkApplicationInfo appInfo = {};
  appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
  appInfo.pApplicationName = "clpeak-vulkan";
  appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
  appInfo.pEngineName = "clpeak";
  // Only request 1.1 when we compiled in code that actually needs 1.1-core
  // symbols (e.g. vkGetPhysicalDeviceFeatures2 for INT8 DP feature query).
  // Otherwise request 1.0 so we work on older drivers / Android API levels
  // where libvulkan only exposes 1.0.
  // 1.1 gets us vkGetPhysicalDeviceFeatures2; cooperative matrix brings its
  // own extension + VK_KHR_vulkan_memory_model, so 1.1 is sufficient as the
  // instance version (MoltenVK 1.2 headers reject some older loaders).
#if defined(CLPEAK_VK_HAS_COMPUTE_INT8_DP_V1) || defined(CLPEAK_VK_HAS_COMPUTE_MP_V1) || defined(CLPEAK_VK_HAS_COMPUTE_BF16_V1) || defined(CLPEAK_VK_HAS_ANY_COOPMAT)
  appInfo.apiVersion = VK_API_VERSION_1_1;
#else
  appInfo.apiVersion = VK_API_VERSION_1_0;
#endif

  VkInstanceCreateInfo instCI = {};
  instCI.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
  instCI.pApplicationInfo = &appInfo;

#if defined(__APPLE__) || defined(__MACOSX)
  // MoltenVK portability
  instCI.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
  const char *extensions[] = {VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME};
  instCI.enabledExtensionCount = 1;
  instCI.ppEnabledExtensionNames = extensions;
#endif

  if (vkCreateInstance(&instCI, nullptr, &instance) != VK_SUCCESS)
    return false;

  uint32_t devCount = 0;
  vkEnumeratePhysicalDevices(instance, &devCount, nullptr);
  if (devCount == 0)
    return false;

  physicalDevices.resize(devCount);
  vkEnumeratePhysicalDevices(instance, &devCount, physicalDevices.data());

  return true;
}

void vkPeak::cleanup()
{
  if (instance != VK_NULL_HANDLE)
  {
    vkDestroyInstance(instance, nullptr);
    instance = VK_NULL_HANDLE;
  }
}

float vkPeak::runKernel(VulkanDevice &dev, VkPipeline pipeline,
                        VkPipelineLayout pipeLayout,
                        VkDescriptorSet descriptorSet,
                        uint32_t groupCountX, unsigned int iters,
                        const void *pushData, uint32_t pushSize)
{
  // Allocate command buffer
  VkCommandBufferAllocateInfo allocInfo = {};
  allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
  allocInfo.commandPool = dev.commandPool;
  allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
  allocInfo.commandBufferCount = 1;

  VkCommandBuffer cmdBuf;
  vkAllocateCommandBuffers(dev.device, &allocInfo, &cmdBuf);

  // Warmup
  for (unsigned int w = 0; w < warmupCount; w++)
  {
    VkCommandBufferBeginInfo warmBeginInfo = {};
    warmBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    warmBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
    vkBeginCommandBuffer(cmdBuf, &warmBeginInfo);
    vkCmdBindPipeline(cmdBuf, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
    vkCmdBindDescriptorSets(cmdBuf, VK_PIPELINE_BIND_POINT_COMPUTE, pipeLayout, 0, 1, &descriptorSet, 0, nullptr);
    if (pushData && pushSize > 0)
      vkCmdPushConstants(cmdBuf, pipeLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, pushSize, pushData);
    vkCmdDispatch(cmdBuf, groupCountX, 1, 1);
    vkEndCommandBuffer(cmdBuf);
    dev.submitAndWait(cmdBuf);
    vkResetCommandBuffer(cmdBuf, 0);
  }

  // Record all dispatches into one command buffer, submit once, and measure
  // the total GPU execution time.  This gives steady-state throughput by
  // amortizing submit / driver overhead across all iterations.
  VkCommandBufferBeginInfo beginInfo = {};
  beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
  beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
  vkBeginCommandBuffer(cmdBuf, &beginInfo);
  vkCmdBindPipeline(cmdBuf, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
  vkCmdBindDescriptorSets(cmdBuf, VK_PIPELINE_BIND_POINT_COMPUTE, pipeLayout, 0, 1, &descriptorSet, 0, nullptr);
  if (pushData && pushSize > 0)
    vkCmdPushConstants(cmdBuf, pipeLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, pushSize, pushData);
  for (unsigned int i = 0; i < iters; i++)
    vkCmdDispatch(cmdBuf, groupCountX, 1, 1);
  vkEndCommandBuffer(cmdBuf);

  auto start = std::chrono::high_resolution_clock::now();
  VkResult submitRes = dev.submitAndWait(cmdBuf);
  auto end = std::chrono::high_resolution_clock::now();
  float timed = (float)std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();

  vkFreeCommandBuffers(dev.device, dev.commandPool, 1, &cmdBuf);

  // Driver/device failure (e.g. Adreno+Turnip losing the device on a
  // shader that uses an advertised-but-unsupported feature): vkQueueWaitIdle
  // returns instantly with VK_ERROR_DEVICE_LOST, leaving timed ~= 0 which
  // would otherwise propagate as "inf" GFLOPS/GBPS into the report.
  if (submitRes != VK_SUCCESS)
    return -1.0f;
  if (timed <= 0.0f)
    return -1.0f;
  return timed / static_cast<float>(iters);
}

int vkPeak::runAll()
{
  log->print(NEWLINE "=== Vulkan backend ===" NEWLINE);
  if (!initInstance())
  {
    log->print("Vulkan: failed to create instance or no devices found" NEWLINE);
    return -1;
  }

  // Mirror the OpenCL context stack so logger_android recordMetric() can
  // reach contextStack depth 4 (clpeak > platform > device > test_group).
  auto clpeakScope = log->resultScope("clpeak");
  log->resultScopeAttribute("os", OS_NAME);
  auto platformScope = log->resultScope("platform");
  log->resultScopeAttribute("name", "Vulkan");
  log->resultScopeAttribute("backend", "Vulkan");

  for (size_t d = 0; d < physicalDevices.size(); d++)
  {
    if (deviceIndex >= 0 && static_cast<size_t>(deviceIndex) != d)
      continue;

    VulkanDevice dev;
    if (!dev.init(instance, physicalDevices[d]))
    {
      log->print(NEWLINE "Vulkan: failed to init device " + std::to_string(d) + NEWLINE);
      continue;
    }

#ifdef CLPEAK_VK_HAS_ANY_COOPMAT
    // Enumerate cooperative-matrix properties to decide which dtype
    // combinations are advertised at the canonical 16x16x16 subgroup-scope
    // tile.  Done here (not in VulkanDevice::init) because the entry point
    // is an instance-level extension function that must be resolved via
    // vkGetInstanceProcAddr against the real VkInstance.
    if (dev.info.cooperativeMatrixSupported)
    {
      auto pfn = (PFN_vkGetPhysicalDeviceCooperativeMatrixPropertiesKHR)
          vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceCooperativeMatrixPropertiesKHR");
      uint32_t propCount = 0;
      if (pfn && pfn(physicalDevices[d], &propCount, nullptr) == VK_SUCCESS && propCount > 0)
      {
        std::vector<VkCooperativeMatrixPropertiesKHR> props(propCount);
        for (auto &p : props) p.sType = VK_STRUCTURE_TYPE_COOPERATIVE_MATRIX_PROPERTIES_KHR;
        if (pfn(physicalDevices[d], &propCount, props.data()) == VK_SUCCESS)
        {
          // Tile matcher: subgroup-scope property at the requested MxNxK
          // with matching A/B input type and C/Result accumulator type.
          auto matches = [](const VkCooperativeMatrixPropertiesKHR &p,
                            uint32_t m, uint32_t n, uint32_t k,
                            VkComponentTypeKHR ab, VkComponentTypeKHR c) {
            return p.MSize == m && p.NSize == n && p.KSize == k &&
                   p.scope == VK_SCOPE_SUBGROUP_KHR &&
                   p.AType == ab && p.BType == ab &&
                   p.CType == c && p.ResultType == c;
          };
          for (auto &p : props)
          {
            if (matches(p, 16,16,16, VK_COMPONENT_TYPE_FLOAT16_KHR, VK_COMPONENT_TYPE_FLOAT32_KHR))
              dev.info.coopmatFP16Supported = true;
            if (matches(p, 16,16,16, VK_COMPONENT_TYPE_FLOAT32_KHR, VK_COMPONENT_TYPE_FLOAT32_KHR))
              dev.info.coopmatFP32Supported = true;
            if (matches(p, 16,16,16, VK_COMPONENT_TYPE_BFLOAT16_KHR, VK_COMPONENT_TYPE_FLOAT32_KHR))
              dev.info.coopmatBF16Supported = true;
            if (matches(p, 16,16,16, VK_COMPONENT_TYPE_FLOAT8_E4M3_EXT, VK_COMPONENT_TYPE_FLOAT32_KHR))
              dev.info.coopmatFP8E4M3Supported = true;
            if (matches(p, 16,16,16, VK_COMPONENT_TYPE_FLOAT8_E5M2_EXT, VK_COMPONENT_TYPE_FLOAT32_KHR))
              dev.info.coopmatFP8E5M2Supported = true;
            // INT8: prefer K=16 (AMD/Intel) but fall back to K=32 (NVIDIA
            // Turing+ advertises INT8 tensor-core tiles only with K=32).
            if (dev.info.coopmatINT8K == 0 &&
                matches(p, 16,16,16, VK_COMPONENT_TYPE_SINT8_KHR, VK_COMPONENT_TYPE_SINT32_KHR))
              dev.info.coopmatINT8K = 16;
            if (dev.info.coopmatINT8K != 16 &&
                matches(p, 16,16,32, VK_COMPONENT_TYPE_SINT8_KHR, VK_COMPONENT_TYPE_SINT32_KHR))
              dev.info.coopmatINT8K = 32;
          }
        }
      }
    }
#endif

    benchmark_config_t cfg = benchmark_config_t::forDevice(
        (dev.info.deviceType == VK_PHYSICAL_DEVICE_TYPE_CPU) ? CL_DEVICE_TYPE_CPU : CL_DEVICE_TYPE_GPU);

    if (forceIters)
    {
      cfg.computeIters = specifiedIters;
      cfg.globalBWIters = specifiedIters;
      cfg.transferBWIters = specifiedIters;
      cfg.kernelLatencyIters = specifiedIters;
    }

    log->print(NEWLINE "Vulkan Device: " + dev.info.deviceName + NEWLINE);
    log->print(TAB "API version   : " + dev.info.apiVersion + NEWLINE);
    log->print(TAB "Driver version: " + dev.info.driverVersion + NEWLINE);
    log->print(TAB "VRAM          : ");
    log->print((unsigned int)(dev.info.heapSize / (1024 * 1024)));
    log->print(" MB" NEWLINE);
    if (dev.info.numCUs > 0)
    {
      log->print(TAB "Compute units : ");
      log->print(dev.info.numCUs);
      log->print(NEWLINE);
    }

    auto deviceScope = log->resultScope("device");
    log->resultScopeAttribute("name", dev.info.deviceName);
    log->resultScopeAttribute("api", "vulkan");
    log->resultScopeAttribute("driver_version", dev.info.driverVersion);

    // ---- Phase 1: floating-point compute (GFLOPS / TFLOPS) ---------
    if (gating.isAllowed(Benchmark::ComputeSP))       runComputeSP(dev, cfg);
#ifdef CLPEAK_VK_HAS_COMPUTE_HP_V1
    if (gating.isAllowed(Benchmark::ComputeHP))       runComputeHP(dev, cfg);
#endif
#ifdef CLPEAK_VK_HAS_COMPUTE_DP_V1
    if (gating.isAllowed(Benchmark::ComputeDP))       runComputeDP(dev, cfg);
#endif
#ifdef CLPEAK_VK_HAS_COMPUTE_MP_V1
    if (gating.isAllowed(Benchmark::ComputeMP))       runComputeMP(dev, cfg);
#endif
#ifdef CLPEAK_VK_HAS_COMPUTE_BF16_V1
    if (gating.isAllowed(Benchmark::ComputeBF16))     runComputeBF16(dev, cfg);
#endif
#ifdef CLPEAK_VK_HAS_ANY_COOPMAT
    if (gating.isAllowedAs(Benchmark::CoopMatrix, Category::FpCompute))
        runCoopMatrix(dev, cfg, /*intPart=*/false);
#endif
#ifdef CLPEAK_VK_HAS_ATOMIC_THROUGHPUT_GLOBAL_FLOAT
    // Float atomic add is reported in the fp-compute phase (mirrors Metal).
    if (gating.isAllowedAs(Benchmark::AtomicThroughput, Category::FpCompute))
        runAtomicThroughputFp(dev, cfg);
#endif

    // ---- Phase 2: integer compute (GOPS / TOPS) --------------------
#ifdef CLPEAK_VK_HAS_COMPUTE_INT32_V1
    if (gating.isAllowed(Benchmark::ComputeInt))        runComputeInt32(dev, cfg);
#endif
#ifdef CLPEAK_VK_HAS_COMPUTE_INT8_DP_V1
    if (gating.isAllowed(Benchmark::ComputeInt8DP))     runComputeInt8DP(dev, cfg);
#endif
#ifdef CLPEAK_VK_HAS_COMPUTE_INT4_PACKED_V1
    if (gating.isAllowed(Benchmark::ComputeInt4Packed)) runComputeInt4Packed(dev, cfg);
#endif
#ifdef CLPEAK_VK_HAS_ANY_COOPMAT
    if (gating.isAllowedAs(Benchmark::CoopMatrix, Category::IntCompute))
        runCoopMatrix(dev, cfg, /*intPart=*/true);
#endif
    if (gating.isAllowedAs(Benchmark::AtomicThroughput, Category::IntCompute))
        runAtomicThroughput(dev, cfg);

    // ---- Phase 3: bandwidth (GBPS) ---------------------------------
    if (gating.isAllowed(Benchmark::GlobalBW))        runGlobalBandwidth(dev, cfg);
    if (gating.isAllowed(Benchmark::LocalBW))         runLocalBandwidth(dev, cfg);
    if (gating.isAllowed(Benchmark::ImageBW))         runImageBandwidth(dev, cfg);
    if (gating.isAllowed(Benchmark::TransferBW))      runTransferBandwidth(dev, cfg);

    // ---- Phase 4: latency (us) -------------------------------------
    if (gating.isAllowed(Benchmark::KernelLatency))   runKernelLatency(dev, cfg);

    log->print(NEWLINE);
    // device
  }

  // platform
  // clpeak
  return 0;
}

// ---------------------------------------------------------------------------
// Shared compute-peak driver.
//
// Every compute-peak benchmark (runComputeSP / MP / INT8-DP / INT4-packed /
// coop-matrix / ...) shares the same Vulkan scaffolding: allocate a single
// device-local output buffer, build a one-binding descriptor set, create a
// pipeline from the shader's SPIR-V, dispatch repeatedly with a push
// constant, and report work-per-WI / elapsed time.  The only differences
// are the shader, the buffer-element size, the push-constant payload, and
// the strings used for display / result output.  All of those are bundled into
// vk_compute_desc_t so each concrete benchmark becomes a few-line wrapper.
// ---------------------------------------------------------------------------

int vkPeak::runComputeKernel(VulkanDevice &dev, benchmark_config_t &cfg,
                             const vk_compute_desc_t &d)
{
  log->print(NEWLINE TAB);
  log->print(d.title);
  log->print(NEWLINE);
  auto scope = log->resultScope(d.resultTag);
  log->resultScopeAttribute("unit", d.unit);
  if (d.extraAttribKey && d.extraAttribVal)
    log->resultScopeAttribute(d.extraAttribKey, d.extraAttribVal);

  if (d.skip)
  {
    log->print(TAB TAB);
    log->print(d.skipMsg ? d.skipMsg : "Skipped");
    log->print(NEWLINE);
    struct SkipVariant { const char *label; };
    std::vector<SkipVariant> skipVariants;
    if (d.variants && d.numVariants > 0)
    {
      for (uint32_t i = 0; i < d.numVariants; i++)
        skipVariants.push_back({d.variants[i].label});
    }
    else
    {
      skipVariants.push_back({d.metricLabel});
    }
    for (const auto &sv : skipVariants)
      log->recordSkip(sv.label, ResultStatus::Unsupported,
                       d.skipMsg ? d.skipMsg : "Skipped");
    return 0;
  }

  // Collect variants.  Multi-variant path (e.g. fp16 v1/v2/v4) shares one
  // buffer + descriptor set and swaps only the pipeline between dispatches;
  // single-variant benchmarks materialize a one-entry list.
  struct Variant { const char *label; const uint32_t *spirv; size_t spirvSize; };
  std::vector<Variant> variants;
  if (d.variants && d.numVariants > 0)
  {
    for (uint32_t i = 0; i < d.numVariants; i++)
      variants.push_back({d.variants[i].label, d.variants[i].spirv, d.variants[i].spirvSize});
  }
  else
  {
    variants.push_back({d.metricLabel, d.spirv, d.spirvSize});
  }

  // Size the dispatch to saturate the device and amortize submit overhead.
  // Vulkan doesn't expose a CU count, so target 32M work-items (matches
  // OpenCL's numCUs*2048*maxWGSize on most GPUs once clamped), bounded
  // by maxStorageBufferRange.  Cooperative-matrix shaders run one subgroup
  // per work-group (32 threads on NVIDIA / AMD RDNA3+ / Intel Arc); other
  // compute kernels use the classic 256.
  const uint32_t wgSize = d.wgSize ? d.wgSize : 256;
  const uint32_t outPerWG = d.outElemsPerWG ? d.outElemsPerWG : wgSize;
  uint64_t globalWIs = targetGlobalThreads(dev.info.numCUs);
  // Buffer footprint = numGroups * outPerWG * elemSize.  Bound by allocation.
  uint64_t bytesPerWG = (uint64_t)outPerWG * d.elemSize;
  uint64_t maxWGs = dev.info.maxAllocSize / bytesPerWG;
  uint64_t wantWGs = globalWIs / wgSize;
  uint32_t numGroups = (uint32_t)std::min(wantWGs, maxWGs);
  globalWIs = (uint64_t)numGroups * wgSize;
  uint64_t bufferBytes = (uint64_t)numGroups * bytesPerWG;

  VkBuffer outputBuf;
  VkDeviceMemory outputMem;
  if (!dev.createBuffer(bufferBytes,
                        VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
                        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
                        outputBuf, outputMem))
  {
    log->print(TAB TAB "Failed to allocate buffer" NEWLINE);
    return -1;
  }

  VkDescriptorSetLayoutBinding binding = {};
  binding.binding = 0;
  binding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
  binding.descriptorCount = 1;
  binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;

  VkDescriptorSetLayoutCreateInfo dsLayoutCI = {};
  dsLayoutCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
  dsLayoutCI.bindingCount = 1;
  dsLayoutCI.pBindings = &binding;

  VkDescriptorSetLayout dsLayout;
  vkCreateDescriptorSetLayout(dev.device, &dsLayoutCI, nullptr, &dsLayout);

  VkPushConstantRange pushRange = {};
  pushRange.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
  pushRange.offset = 0;
  pushRange.size = d.pushSize;

  VkPipelineLayoutCreateInfo pipeLayoutCI = {};
  pipeLayoutCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
  pipeLayoutCI.setLayoutCount = 1;
  pipeLayoutCI.pSetLayouts = &dsLayout;
  if (d.pushSize > 0)
  {
    pipeLayoutCI.pushConstantRangeCount = 1;
    pipeLayoutCI.pPushConstantRanges = &pushRange;
  }

  VkPipelineLayout pipeLayout;
  vkCreatePipelineLayout(dev.device, &pipeLayoutCI, nullptr, &pipeLayout);

  VkDescriptorPoolSize poolSize = {};
  poolSize.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
  poolSize.descriptorCount = 1;

  VkDescriptorPoolCreateInfo dpCI = {};
  dpCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
  dpCI.maxSets = 1;
  dpCI.poolSizeCount = 1;
  dpCI.pPoolSizes = &poolSize;

  VkDescriptorPool descPool;
  vkCreateDescriptorPool(dev.device, &dpCI, nullptr, &descPool);

  VkDescriptorSetAllocateInfo dsAI = {};
  dsAI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
  dsAI.descriptorPool = descPool;
  dsAI.descriptorSetCount = 1;
  dsAI.pSetLayouts = &dsLayout;

  VkDescriptorSet descSet;
  vkAllocateDescriptorSets(dev.device, &dsAI, &descSet);

  VkDescriptorBufferInfo bufInfo = {};
  bufInfo.buffer = outputBuf;
  bufInfo.offset = 0;
  bufInfo.range = bufferBytes;

  VkWriteDescriptorSet write = {};
  write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
  write.dstSet = descSet;
  write.dstBinding = 0;
  write.descriptorCount = 1;
  write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
  write.pBufferInfo = &bufInfo;

  vkUpdateDescriptorSets(dev.device, 1, &write, 0, nullptr);

  // Build + dispatch each variant's pipeline.  Variants failing pipeline
  // creation are printed as skipped but don't abort the group -- some
  // drivers accept the v1 shader but choke on a wider packed variant.
  for (const auto &v : variants)
  {
    log->print(TAB TAB);
    log->print(v.label);
    log->print(" : ");

    VkPipeline pipeline;
    if (!dev.createComputePipeline(v.spirv, v.spirvSize, dsLayout, pipeLayout, pipeline))
    {
      log->print("pipeline creation failed (driver may not honor extension)" NEWLINE);
      log->recordSkip(v.label, ResultStatus::Error,
                       "Pipeline creation failed");
      continue;
    }

    float timed = runKernel(dev, pipeline, pipeLayout, descSet, numGroups,
                            cfg.computeIters, d.pushData, d.pushSize);
    if (timed <= 0.0f)
    {
      log->print("submit failed (driver lost the device or rejected the dispatch)" NEWLINE);
      log->recordSkip(v.label, ResultStatus::Error,
                       "vkQueueSubmit/WaitIdle failed");
      vkDestroyPipeline(dev.device, pipeline, nullptr);
      continue;
    }
    double divider = d.unitDivider > 0.0 ? d.unitDivider : 1e9;
    float value = (float)((double)globalWIs * (double)d.workPerWI * 1e6 / timed / divider);

    log->print(value);
    log->print(NEWLINE);
    log->resultRecord(v.label, value);

    vkDestroyPipeline(dev.device, pipeline, nullptr);
  }

  vkDestroyDescriptorPool(dev.device, descPool, nullptr);
  vkDestroyPipelineLayout(dev.device, pipeLayout, nullptr);
  vkDestroyDescriptorSetLayout(dev.device, dsLayout, nullptr);
  vkDestroyBuffer(dev.device, outputBuf, nullptr);
  vkFreeMemory(dev.device, outputMem, nullptr);

  return 0;
}

// ---------------------------------------------------------------------------
// Concrete compute benchmarks.  Each is a thin wrapper that fills in a
// vk_compute_desc_t and delegates to runComputeKernel above.
// ---------------------------------------------------------------------------

int vkPeak::runComputeSP(VulkanDevice &dev, benchmark_config_t &cfg)
{
  static const vk_compute_variant_t variants[] = {
    { "float",  vk_shaders::compute_sp_v1, vk_shaders::compute_sp_v1_size },
#ifdef CLPEAK_VK_HAS_COMPUTE_SP_V2
    { "float2", vk_shaders::compute_sp_v2, vk_shaders::compute_sp_v2_size },
#endif
#ifdef CLPEAK_VK_HAS_COMPUTE_SP_V4
    { "float4", vk_shaders::compute_sp_v4, vk_shaders::compute_sp_v4_size },
#endif
  };
  float A = 1.3f;
  vk_compute_desc_t d = {};
  d.title       = "Single-precision compute (GFLOPS)";
  d.resultTag   = "single_precision_compute";
  d.unit        = "gflops";
  d.variants    = variants;
  d.numVariants = sizeof(variants) / sizeof(variants[0]);
  d.workPerWI   = COMPUTE_FP_WORK_PER_WI;
  d.elemSize    = sizeof(float);
  d.pushData    = &A;
  d.pushSize    = sizeof(A);
  return runComputeKernel(dev, cfg, d);
}

#ifdef CLPEAK_VK_HAS_COMPUTE_HP_V1
int vkPeak::runComputeHP(VulkanDevice &dev, benchmark_config_t &cfg)
{
  static const vk_compute_variant_t variants[] = {
    { "half",   vk_shaders::compute_hp_v1, vk_shaders::compute_hp_v1_size },
#ifdef CLPEAK_VK_HAS_COMPUTE_HP_V2
    { "half2",  vk_shaders::compute_hp_v2, vk_shaders::compute_hp_v2_size },
#endif
#ifdef CLPEAK_VK_HAS_COMPUTE_HP_V4
    { "half4",  vk_shaders::compute_hp_v4, vk_shaders::compute_hp_v4_size },
#endif
  };
  float A = 1.3f;
  vk_compute_desc_t d = {};
  d.title       = "Half-precision compute fp16xfp16+fp16 (GFLOPS)";
  d.resultTag   = "half_precision_compute";
  d.unit        = "gflops";
  d.variants    = variants;
  d.numVariants = sizeof(variants) / sizeof(variants[0]);
  d.workPerWI   = COMPUTE_FP_WORK_PER_WI;
  d.elemSize    = sizeof(float);
  d.pushData    = &A;
  d.pushSize    = sizeof(A);
  d.skip        = !dev.info.float16Supported;
  d.skipMsg     = "shaderFloat16 not supported! Skipped";
  return runComputeKernel(dev, cfg, d);
}
#endif

#ifdef CLPEAK_VK_HAS_COMPUTE_DP_V1
int vkPeak::runComputeDP(VulkanDevice &dev, benchmark_config_t &cfg)
{
  static const vk_compute_variant_t variants[] = {
    { "double",  vk_shaders::compute_dp_v1, vk_shaders::compute_dp_v1_size },
#ifdef CLPEAK_VK_HAS_COMPUTE_DP_V2
    { "double2", vk_shaders::compute_dp_v2, vk_shaders::compute_dp_v2_size },
#endif
#ifdef CLPEAK_VK_HAS_COMPUTE_DP_V4
    { "double4", vk_shaders::compute_dp_v4, vk_shaders::compute_dp_v4_size },
#endif
  };
  double A = 1.3;
  vk_compute_desc_t d = {};
  d.title       = "Double-precision compute (GFLOPS)";
  d.resultTag   = "double_precision_compute";
  d.unit        = "gflops";
  d.variants    = variants;
  d.numVariants = sizeof(variants) / sizeof(variants[0]);
  d.workPerWI   = COMPUTE_FP_WORK_PER_WI;
  d.elemSize    = sizeof(double);
  d.pushData    = &A;
  d.pushSize    = sizeof(A);
  d.skip        = !dev.info.float64Supported;
  d.skipMsg     = "shaderFloat64 not supported! Skipped";
  return runComputeKernel(dev, cfg, d);
}
#endif

#ifdef CLPEAK_VK_HAS_COMPUTE_INT32_V1
int vkPeak::runComputeInt32(VulkanDevice &dev, benchmark_config_t &cfg)
{
  static const vk_compute_variant_t variants[] = {
    { "int",   vk_shaders::compute_int32_v1, vk_shaders::compute_int32_v1_size },
#ifdef CLPEAK_VK_HAS_COMPUTE_INT32_V2
    { "int2",  vk_shaders::compute_int32_v2, vk_shaders::compute_int32_v2_size },
#endif
#ifdef CLPEAK_VK_HAS_COMPUTE_INT32_V4
    { "int4",  vk_shaders::compute_int32_v4, vk_shaders::compute_int32_v4_size },
#endif
  };
  int32_t A = 4;
  vk_compute_desc_t d = {};
  d.title       = "Integer compute int32 (GOPS)";
  d.resultTag   = "integer_compute";
  d.unit        = "gops";
  d.variants    = variants;
  d.numVariants = sizeof(variants) / sizeof(variants[0]);
  d.workPerWI   = COMPUTE_INT_WORK_PER_WI;
  d.elemSize    = sizeof(int32_t);
  d.pushData    = &A;
  d.pushSize    = sizeof(A);
  return runComputeKernel(dev, cfg, d);
}
#endif

#ifdef CLPEAK_VK_HAS_COMPUTE_MP_V1
int vkPeak::runComputeMP(VulkanDevice &dev, benchmark_config_t &cfg)
{
  // v1 = scalar fp16 (baseline; no HFMA2 packing).
  // v2 = f16vec2  (unlocks NVIDIA HFMA2 at 2x FP32 rate on shader cores).
  // v4 = f16vec4  (wider packing; informs AMD/Intel where issue rate
  //                exceeds two lanes per slot).
  static const vk_compute_variant_t variants[] = {
    { "mp",  vk_shaders::compute_mp_v1, vk_shaders::compute_mp_v1_size },
#ifdef CLPEAK_VK_HAS_COMPUTE_MP_V2
    { "mp2", vk_shaders::compute_mp_v2, vk_shaders::compute_mp_v2_size },
#endif
#ifdef CLPEAK_VK_HAS_COMPUTE_MP_V4
    { "mp4", vk_shaders::compute_mp_v4, vk_shaders::compute_mp_v4_size },
#endif
  };
  float A = 1.3f;
  vk_compute_desc_t d = {};
  d.title       = "Mixed-precision compute fp16xfp16+fp32 (GFLOPS)";
  d.resultTag      = "mixed_precision_compute";
  d.unit        = "gflops";
  d.variants    = variants;
  d.numVariants = sizeof(variants) / sizeof(variants[0]);
  d.workPerWI   = COMPUTE_FP_WORK_PER_WI;
  d.elemSize    = sizeof(float);
  d.pushData    = &A;
  d.pushSize    = sizeof(A);
  d.skip        = !dev.info.float16Supported;
  d.skipMsg     = "shaderFloat16 not supported! Skipped";
  return runComputeKernel(dev, cfg, d);
}
#endif

#ifdef CLPEAK_VK_HAS_COMPUTE_INT4_PACKED_V1
int vkPeak::runComputeInt4Packed(VulkanDevice &dev, benchmark_config_t &cfg)
{
  int32_t A = 3;
  vk_compute_desc_t d = {};
  d.title           = "Packed INT4 compute (emulated) (GOPS)";
  d.resultTag          = "int4_packed_compute";
  d.metricLabel     = "int4_packed";
  d.unit            = "gops";
  d.spirv           = vk_shaders::compute_int4_packed_v1;
  d.spirvSize       = vk_shaders::compute_int4_packed_v1_size;
  d.workPerWI       = COMPUTE_INT4_PACKED_WORK_PER_WI;
  d.elemSize        = sizeof(int32_t);
  d.pushData        = &A;
  d.pushSize        = sizeof(A);
  d.extraAttribKey  = "emulated";
  d.extraAttribVal  = "true";
  return runComputeKernel(dev, cfg, d);
}
#endif

#ifdef CLPEAK_VK_HAS_COMPUTE_INT8_DP_V1
int vkPeak::runComputeInt8DP(VulkanDevice &dev, benchmark_config_t &cfg)
{
  // v1 = single dp4a chain (serial through REPACK; dep-stall bound).
  // v2 = two parallel dp4a chains (double the independent work for the
  //       instruction issue queue to pipeline).
  // v4 = four parallel chains (enough to saturate dp4a issue rate on
  //       NVIDIA Turing+ / AMD RDNA2+ / Intel Xe+).
  static const vk_compute_variant_t variants[] = {
    { "int8_dp",  vk_shaders::compute_int8_dp_v1, vk_shaders::compute_int8_dp_v1_size },
#ifdef CLPEAK_VK_HAS_COMPUTE_INT8_DP_V2
    { "int8_dp2", vk_shaders::compute_int8_dp_v2, vk_shaders::compute_int8_dp_v2_size },
#endif
#ifdef CLPEAK_VK_HAS_COMPUTE_INT8_DP_V4
    { "int8_dp4", vk_shaders::compute_int8_dp_v4, vk_shaders::compute_int8_dp_v4_size },
#endif
  };
  int32_t A = 4;
  vk_compute_desc_t d = {};
  d.title       = "INT8 dot-product compute (GOPS)";
  d.resultTag      = "integer_compute_int8_dp";
  d.unit        = "gops";
  d.variants    = variants;
  d.numVariants = sizeof(variants) / sizeof(variants[0]);
  d.workPerWI   = COMPUTE_INT8_DP_WORK_PER_WI;
  d.elemSize    = sizeof(int32_t);
  d.pushData    = &A;
  d.pushSize    = sizeof(A);
  d.skip        = !dev.info.int8DotProductSupported;
  d.skipMsg     = "VK_KHR_shader_integer_dot_product / shaderInt8 not supported! Skipped";
  return runComputeKernel(dev, cfg, d);
}
#endif

#ifdef CLPEAK_VK_HAS_COMPUTE_BF16_V1
int vkPeak::runComputeBF16(VulkanDevice &dev, benchmark_config_t &cfg)
{
  // v1 / v2 / v4: same packing story as MP.  NVIDIA shader-core BF16
  // peaks at bf16vec2 via BMMA2-style packed multiply.
  static const vk_compute_variant_t variants[] = {
    { "bf16",  vk_shaders::compute_bf16_v1, vk_shaders::compute_bf16_v1_size },
#ifdef CLPEAK_VK_HAS_COMPUTE_BF16_V2
    { "bf16_2", vk_shaders::compute_bf16_v2, vk_shaders::compute_bf16_v2_size },
#endif
#ifdef CLPEAK_VK_HAS_COMPUTE_BF16_V4
    { "bf16_4", vk_shaders::compute_bf16_v4, vk_shaders::compute_bf16_v4_size },
#endif
  };
  float A = 1.3f;
  vk_compute_desc_t d = {};
  d.title       = "BF16 compute bf16xbf16+fp32 (GFLOPS)";
  d.resultTag      = "bfloat16_compute";
  d.unit        = "gflops";
  d.variants    = variants;
  d.numVariants = sizeof(variants) / sizeof(variants[0]);
  d.workPerWI   = COMPUTE_FP_WORK_PER_WI;
  d.elemSize    = sizeof(float);
  d.pushData    = &A;
  d.pushSize    = sizeof(A);
  d.skip        = !dev.info.bfloat16Supported;
  d.skipMsg     = "VK_KHR_shader_bfloat16 / shaderBFloat16Type not supported! Skipped";
  return runComputeKernel(dev, cfg, d);
}
#endif

// ---------------------------------------------------------------------------
// Cooperative matrix (tensor-core) umbrella.
//
// Runs every dtype combination the driver advertises at the canonical
// 16x16x16 subgroup-scope tile (queried once in VulkanDevice::init).  Each
// dtype shares the same scaffolding via runComputeKernel -- only the shader,
// buffer element type, and label strings differ.  Adding FP8 / INT4 in
// Phase 2 reduces to: compile a coopmat_fp8.comp, query the matching
// component-type enums, and add one more entry here.
// ---------------------------------------------------------------------------

int vkPeak::runCoopMatrix(VulkanDevice &dev, benchmark_config_t &cfg, bool intPart)
{
  // Coopmat shape constants: shaders hard-code 16x16x16 with 256 iters and
  // local_size_x=32 (one subgroup per work-group).  See COOPMAT_WORK_PER_WI.
  const uint32_t coopWGSize  = 32;
  const uint32_t coopOutElems = 16 * 16;  // M*N tile written per WG
  const uint32_t coopWork    = COOPMAT_WORK_PER_WI;

  if (!intPart) {
#ifdef CLPEAK_VK_HAS_COOPMAT_FP32
    {
      float A = 1.3f;
      vk_compute_desc_t d = {};
      d.title          = "Cooperative-matrix fp32xfp32+fp32 16x16x16 (TFLOPS)";
      d.resultTag      = "coopmat_fp32";
      d.metricLabel    = "coopmat_fp32";
      d.unit           = "tflops";
      d.unitDivider    = 1e12;
      d.spirv          = vk_shaders::coopmat_fp32;
      d.spirvSize      = vk_shaders::coopmat_fp32_size;
      d.workPerWI      = coopWork;
      d.elemSize       = sizeof(float);
      d.wgSize         = coopWGSize;
      d.outElemsPerWG  = coopOutElems;
      d.pushData       = &A;
      d.pushSize       = sizeof(A);
      d.skip           = !dev.info.coopmatFP32Supported;
      d.skipMsg        = "No 16x16x16 fp32xfp32+fp32 coopmat property! Skipped";
      d.extraAttribKey = "tile";
      d.extraAttribVal = "16x16x16";
      runComputeKernel(dev, cfg, d);
    }
#endif
#ifdef CLPEAK_VK_HAS_COOPMAT_FP16
    {
      float A = 1.3f;
      vk_compute_desc_t d = {};
      d.title          = "Cooperative-matrix fp16xfp16+fp32 16x16x16 (TFLOPS)";
      d.resultTag      = "coopmat_fp16";
      d.metricLabel    = "coopmat_fp16";
      d.unit           = "tflops";
      d.unitDivider    = 1e12;
      d.spirv          = vk_shaders::coopmat_fp16;
      d.spirvSize      = vk_shaders::coopmat_fp16_size;
      d.workPerWI      = coopWork;
      d.elemSize       = sizeof(float);
      d.wgSize         = coopWGSize;
      d.outElemsPerWG  = coopOutElems;
      d.pushData       = &A;
      d.pushSize       = sizeof(A);
      d.skip           = !dev.info.coopmatFP16Supported;
      d.skipMsg        = "No 16x16x16 fp16xfp16+fp32 coopmat property! Skipped";
      d.extraAttribKey = "tile";
      d.extraAttribVal = "16x16x16";
      runComputeKernel(dev, cfg, d);
    }
#endif
#ifdef CLPEAK_VK_HAS_COOPMAT_BF16
    {
      float A = 1.3f;
      vk_compute_desc_t d = {};
      d.title          = "Cooperative-matrix bf16xbf16+fp32 16x16x16 (TFLOPS)";
      d.resultTag      = "coopmat_bf16";
      d.metricLabel    = "coopmat_bf16";
      d.unit           = "tflops";
      d.unitDivider    = 1e12;
      d.spirv          = vk_shaders::coopmat_bf16;
      d.spirvSize      = vk_shaders::coopmat_bf16_size;
      d.workPerWI      = coopWork;
      d.elemSize       = sizeof(float);
      d.wgSize         = coopWGSize;
      d.outElemsPerWG  = coopOutElems;
      d.pushData       = &A;
      d.pushSize       = sizeof(A);
      d.skip           = !dev.info.coopmatBF16Supported;
      d.skipMsg        = "No 16x16x16 bf16xbf16+fp32 coopmat property! Skipped";
      d.extraAttribKey = "tile";
      d.extraAttribVal = "16x16x16";
      runComputeKernel(dev, cfg, d);
    }
#endif
#ifdef CLPEAK_VK_HAS_COOPMAT_FP8_E4M3
    {
      float A = 1.3f;
      vk_compute_desc_t d = {};
      d.title          = "Cooperative-matrix fp8(E4M3)xfp8(E4M3)+fp32 16x16x16 (TFLOPS)";
      d.resultTag      = "coopmat_fp8_e4m3";
      d.metricLabel    = "coopmat_fp8_e4m3";
      d.unit           = "tflops";
      d.unitDivider    = 1e12;
      d.spirv          = vk_shaders::coopmat_fp8_e4m3;
      d.spirvSize      = vk_shaders::coopmat_fp8_e4m3_size;
      d.workPerWI      = coopWork;
      d.elemSize       = sizeof(float);
      d.wgSize         = coopWGSize;
      d.outElemsPerWG  = coopOutElems;
      d.pushData       = &A;
      d.pushSize       = sizeof(A);
      d.skip           = !(dev.info.fp8Supported && dev.info.coopmatFP8E4M3Supported);
      d.skipMsg        = "No fp8-E4M3 coopmat support (VK_EXT_shader_float8 or property)! Skipped";
      d.extraAttribKey = "tile";
      d.extraAttribVal = "16x16x16";
      runComputeKernel(dev, cfg, d);
    }
#endif
#ifdef CLPEAK_VK_HAS_COOPMAT_FP8_E5M2
    {
      float A = 1.3f;
      vk_compute_desc_t d = {};
      d.title          = "Cooperative-matrix fp8(E5M2)xfp8(E5M2)+fp32 16x16x16 (TFLOPS)";
      d.resultTag      = "coopmat_fp8_e5m2";
      d.metricLabel    = "coopmat_fp8_e5m2";
      d.unit           = "tflops";
      d.unitDivider    = 1e12;
      d.spirv          = vk_shaders::coopmat_fp8_e5m2;
      d.spirvSize      = vk_shaders::coopmat_fp8_e5m2_size;
      d.workPerWI      = coopWork;
      d.elemSize       = sizeof(float);
      d.wgSize         = coopWGSize;
      d.outElemsPerWG  = coopOutElems;
      d.pushData       = &A;
      d.pushSize       = sizeof(A);
      d.skip           = !(dev.info.fp8Supported && dev.info.coopmatFP8E5M2Supported);
      d.skipMsg        = "No fp8-E5M2 coopmat support (VK_EXT_shader_float8 or property)! Skipped";
      d.extraAttribKey = "tile";
      d.extraAttribVal = "16x16x16";
      runComputeKernel(dev, cfg, d);
    }
#endif
  } // !intPart

#if defined(CLPEAK_VK_HAS_COOPMAT_INT8) || defined(CLPEAK_VK_HAS_COOPMAT_INT8_K32)
  if (intPart) {
    // Select the shader variant matching whichever INT8 tile the driver
    // advertised.  K=16 is the generic path; NVIDIA tensor cores need K=32.
    int32_t A = 3;
    vk_compute_desc_t d = {};
    d.resultTag         = "coopmat_int8";
    d.metricLabel    = "coopmat_int8";
    d.unit           = "tops";
    d.unitDivider    = 1e12;
    d.workPerWI      = coopWork;
    d.elemSize       = sizeof(int32_t);
    d.wgSize         = coopWGSize;
    d.outElemsPerWG  = coopOutElems;
    d.pushData       = &A;
    d.pushSize       = sizeof(A);
    d.extraAttribKey = "tile";

    const char *titleK16 = "Cooperative-matrix int8xint8+int32 16x16x16 (TOPS)";
    const char *titleK32 = "Cooperative-matrix int8xint8+int32 16x16x32 (TOPS)";
    bool haveShaderK16 = false, haveShaderK32 = false;
#ifdef CLPEAK_VK_HAS_COOPMAT_INT8
    haveShaderK16 = true;
#endif
#ifdef CLPEAK_VK_HAS_COOPMAT_INT8_K32
    haveShaderK32 = true;
#endif

    if (dev.info.coopmatINT8K == 16 && haveShaderK16)
    {
#ifdef CLPEAK_VK_HAS_COOPMAT_INT8
      d.title          = titleK16;
      d.spirv          = vk_shaders::coopmat_int8;
      d.spirvSize      = vk_shaders::coopmat_int8_size;
      d.extraAttribVal = "16x16x16";
#endif
    }
    else if (dev.info.coopmatINT8K == 32 && haveShaderK32)
    {
#ifdef CLPEAK_VK_HAS_COOPMAT_INT8_K32
      d.title          = titleK32;
      d.spirv          = vk_shaders::coopmat_int8_k32;
      d.spirvSize      = vk_shaders::coopmat_int8_k32_size;
      d.extraAttribVal = "16x16x32";
#endif
    }
    else
    {
      // Driver advertised neither 16x16x16 nor 16x16x32 INT8 -- or the
      // corresponding shader didn't compile in this build.  Skip with a
      // label that names the probed tiles so the reason is obvious.
      d.title          = titleK16;
      d.skip           = true;
      d.skipMsg        = "No 16x16x{16,32} int8xint8+int32 coopmat property! Skipped";
      d.extraAttribVal = "16x16x16";
      d.spirv          = nullptr;
      d.spirvSize      = 0;
    }
    runComputeKernel(dev, cfg, d);
  } // if (intPart)
#endif
  return 0;
}

// ---------------------------------------------------------------------------
// Global bandwidth benchmark (Vulkan)
// ---------------------------------------------------------------------------

int vkPeak::runGlobalBandwidth(VulkanDevice &dev, benchmark_config_t &cfg)
{
  unsigned int iters = cfg.globalBWIters;
  const uint32_t wgSize = 256;

  uint64_t maxItems = dev.info.maxAllocSize / sizeof(float) / 2;
  uint64_t numItems = (maxItems / (wgSize * FETCH_PER_WI)) * (wgSize * FETCH_PER_WI);
  if (numItems > cfg.globalBWMaxSize / sizeof(float))
    numItems = (cfg.globalBWMaxSize / sizeof(float) / (wgSize * FETCH_PER_WI)) * (wgSize * FETCH_PER_WI);

  uint32_t numGroups = (uint32_t)(numItems / FETCH_PER_WI / wgSize);
  if (numGroups == 0) numGroups = 1;

  log->print(NEWLINE TAB "Global memory bandwidth (GBPS)" NEWLINE);
  auto scope_1 = log->resultScope("global_memory_bandwidth");
  log->resultScopeAttribute("unit", "gbps");

  // Create input + output buffers
  VkBuffer inputBuf, outputBuf;
  VkDeviceMemory inputMem, outputMem;

  if (!dev.createBuffer(numItems * sizeof(float),
                        VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
                        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
                        inputBuf, inputMem) ||
      !dev.createBuffer(numItems * sizeof(float),
                        VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
                        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
                        outputBuf, outputMem))
  {
    log->print(TAB TAB "Failed to allocate buffers" NEWLINE);
    return -1;
  }

  // Descriptor set layout (2 bindings: input + output)
  VkDescriptorSetLayoutBinding bindings[2] = {};
  bindings[0].binding = 0;
  bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
  bindings[0].descriptorCount = 1;
  bindings[0].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
  bindings[1].binding = 1;
  bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
  bindings[1].descriptorCount = 1;
  bindings[1].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;

  VkDescriptorSetLayoutCreateInfo dsLayoutCI = {};
  dsLayoutCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
  dsLayoutCI.bindingCount = 2;
  dsLayoutCI.pBindings = bindings;

  VkDescriptorSetLayout dsLayout;
  vkCreateDescriptorSetLayout(dev.device, &dsLayoutCI, nullptr, &dsLayout);

  VkPipelineLayoutCreateInfo pipeLayoutCI = {};
  pipeLayoutCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
  pipeLayoutCI.setLayoutCount = 1;
  pipeLayoutCI.pSetLayouts = &dsLayout;

  VkPipelineLayout pipeLayout;
  vkCreatePipelineLayout(dev.device, &pipeLayoutCI, nullptr, &pipeLayout);

  struct GBVar { const char *label; const uint32_t *spv; size_t sz; uint32_t width; };
  const GBVar variants[] = {
    {"float ",  vk_shaders::global_bandwidth_v1, vk_shaders::global_bandwidth_v1_size, 1},
#ifdef CLPEAK_VK_HAS_GLOBAL_BANDWIDTH_V2
    {"float2",  vk_shaders::global_bandwidth_v2, vk_shaders::global_bandwidth_v2_size, 2},
#endif
#ifdef CLPEAK_VK_HAS_GLOBAL_BANDWIDTH_V4
    {"float4",  vk_shaders::global_bandwidth_v4, vk_shaders::global_bandwidth_v4_size, 4},
#endif
  };

  // Descriptor pool + set
  VkDescriptorPoolSize poolSize = {};
  poolSize.type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
  poolSize.descriptorCount = 2;

  VkDescriptorPoolCreateInfo dpCI = {};
  dpCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
  dpCI.maxSets = 1;
  dpCI.poolSizeCount = 1;
  dpCI.pPoolSizes = &poolSize;

  VkDescriptorPool descPool;
  vkCreateDescriptorPool(dev.device, &dpCI, nullptr, &descPool);

  VkDescriptorSetAllocateInfo dsAI = {};
  dsAI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
  dsAI.descriptorPool = descPool;
  dsAI.descriptorSetCount = 1;
  dsAI.pSetLayouts = &dsLayout;

  VkDescriptorSet descSet;
  vkAllocateDescriptorSets(dev.device, &dsAI, &descSet);

  VkDescriptorBufferInfo bufInfos[2] = {};
  bufInfos[0].buffer = inputBuf;
  bufInfos[0].offset = 0;
  bufInfos[0].range = numItems * sizeof(float);
  bufInfos[1].buffer = outputBuf;
  bufInfos[1].offset = 0;
  bufInfos[1].range = numItems * sizeof(float);

  VkWriteDescriptorSet writes[2] = {};
  for (int i = 0; i < 2; i++)
  {
    writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
    writes[i].dstSet = descSet;
    writes[i].dstBinding = i;
    writes[i].descriptorCount = 1;
    writes[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
    writes[i].pBufferInfo = &bufInfos[i];
  }
  vkUpdateDescriptorSets(dev.device, 2, writes, 0, nullptr);

  // Run each variant.  Same buffer + descriptor set; only the pipeline
  // (i.e. the loaded shader) changes per row.
  for (const auto &v : variants)
  {
    log->print(TAB TAB);
    log->print(v.label);
    log->print("  : ");
    VkPipeline pipe;
    if (!dev.createComputePipeline(v.spv, v.sz, dsLayout, pipeLayout, pipe))
    {
      log->print("pipeline creation failed" NEWLINE);
      continue;
    }
    // Match OpenCL: reduce work-group count by vector width so total bytes
    // touched stays at numItems * sizeof(float) across all variants.
    uint32_t variantGroups = numGroups / v.width;
    if (variantGroups == 0) variantGroups = 1;
    float timed = runKernel(dev, pipe, pipeLayout, descSet, variantGroups, iters);
    std::string key(v.label);
    while (!key.empty() && key.back() == ' ') key.pop_back();
    if (timed <= 0.0f)
    {
      log->print("submit failed" NEWLINE);
      log->recordSkip(key, ResultStatus::Error, "vkQueueSubmit/WaitIdle failed");
      vkDestroyPipeline(dev.device, pipe, nullptr);
      continue;
    }
    float gbps = ((float)numItems * sizeof(float)) / timed / 1e3f;
    log->print(gbps);
    log->print(NEWLINE);
    log->resultRecord(key, gbps);
    vkDestroyPipeline(dev.device, pipe, nullptr);
  }

  // Cleanup
  vkDestroyDescriptorPool(dev.device, descPool, nullptr);
  vkDestroyPipelineLayout(dev.device, pipeLayout, nullptr);
  vkDestroyDescriptorSetLayout(dev.device, dsLayout, nullptr);
  vkDestroyBuffer(dev.device, inputBuf, nullptr);
  vkFreeMemory(dev.device, inputMem, nullptr);
  vkDestroyBuffer(dev.device, outputBuf, nullptr);
  vkFreeMemory(dev.device, outputMem, nullptr);

  // global_memory_bandwidth
  return 0;
}

// ---------------------------------------------------------------------------
// Host<->device transfer bandwidth (Vulkan)
// ---------------------------------------------------------------------------

int vkPeak::runTransferBandwidth(VulkanDevice &dev, benchmark_config_t &cfg)
{
  VkPhysicalDeviceProperties props;
  vkGetPhysicalDeviceProperties(dev.physicalDevice, &props);

  uint32_t queueFamilyCount = 0;
  vkGetPhysicalDeviceQueueFamilyProperties(dev.physicalDevice, &queueFamilyCount, nullptr);
  std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
  vkGetPhysicalDeviceQueueFamilyProperties(dev.physicalDevice, &queueFamilyCount, queueFamilies.data());

  const bool canUseTimestamps =
      dev.info.computeQueueFamily < queueFamilies.size() &&
      queueFamilies[dev.info.computeQueueFamily].timestampValidBits > 0 &&
      props.limits.timestampPeriod > 0.0f;

  uint64_t bytes = cfg.transferBWMaxSize ? cfg.transferBWMaxSize : (1ull << 27);
  if (dev.info.maxAllocSize > 0)
    bytes = std::min(bytes, dev.info.maxAllocSize);
  bytes &= ~255ull;
  if (bytes == 0)
    bytes = 256;

  unsigned int iters = cfg.transferBWIters ? cfg.transferBWIters : 10;

  log->print(NEWLINE TAB "Transfer bandwidth (GBPS)" NEWLINE);
  auto scope_2 = log->resultScope("transfer_bandwidth");
  log->resultScopeAttribute("unit", "gbps");
  log->resultScopeAttribute("memory", "host_visible_staging");
  log->resultScopeAttribute("timer", canUseTimestamps ? "gpu_timestamp" : "host_wall");

  VkBuffer hostBuf = VK_NULL_HANDLE;
  VkBuffer devBuf = VK_NULL_HANDLE;
  VkDeviceMemory hostMem = VK_NULL_HANDLE;
  VkDeviceMemory devMem = VK_NULL_HANDLE;

  if (!dev.createBuffer(bytes,
                        VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
                        VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
                        hostBuf, hostMem) ||
      !dev.createBuffer(bytes,
                        VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
                        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
                        devBuf, devMem))
  {
    log->print(TAB TAB "Failed to allocate transfer buffers" NEWLINE);
    if (hostBuf != VK_NULL_HANDLE) vkDestroyBuffer(dev.device, hostBuf, nullptr);
    if (hostMem != VK_NULL_HANDLE) vkFreeMemory(dev.device, hostMem, nullptr);
    if (devBuf != VK_NULL_HANDLE) vkDestroyBuffer(dev.device, devBuf, nullptr);
    if (devMem != VK_NULL_HANDLE) vkFreeMemory(dev.device, devMem, nullptr);
    return -1;
  }

  VkCommandBufferAllocateInfo allocInfo = {};
  allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
  allocInfo.commandPool = dev.commandPool;
  allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
  allocInfo.commandBufferCount = 1;

  VkCommandBuffer cmdBuf = VK_NULL_HANDLE;
  vkAllocateCommandBuffers(dev.device, &allocInfo, &cmdBuf);

  VkQueryPool queryPool = VK_NULL_HANDLE;
  if (canUseTimestamps)
  {
    VkQueryPoolCreateInfo qpCI = {};
    qpCI.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
    qpCI.queryType = VK_QUERY_TYPE_TIMESTAMP;
    qpCI.queryCount = 2;
    vkCreateQueryPool(dev.device, &qpCI, nullptr, &queryPool);
  }

  auto runCopy = [&](VkBuffer src, VkBuffer dst) -> float
  {
    VkBufferCopy region = {};
    region.size = bytes;

    auto recordCopy = [&](bool timed) {
      VkCommandBufferBeginInfo beginInfo = {};
      beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
      beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
      vkBeginCommandBuffer(cmdBuf, &beginInfo);
      if (timed && queryPool != VK_NULL_HANDLE)
      {
        vkCmdResetQueryPool(cmdBuf, queryPool, 0, 2);
        vkCmdWriteTimestamp(cmdBuf, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, queryPool, 0);
      }
      vkCmdCopyBuffer(cmdBuf, src, dst, 1, &region);
      if (timed && queryPool != VK_NULL_HANDLE)
        vkCmdWriteTimestamp(cmdBuf, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queryPool, 1);
      vkEndCommandBuffer(cmdBuf);
    };

    for (unsigned int w = 0; w < warmupCount; w++)
    {
      recordCopy(false);
      dev.submitAndWait(cmdBuf);
      vkResetCommandBuffer(cmdBuf, 0);
    }

    float totalUs = 0.0f;
    bool   submitFailed = false;
    for (unsigned int i = 0; i < iters; i++)
    {
      recordCopy(queryPool != VK_NULL_HANDLE);
      auto start = std::chrono::high_resolution_clock::now();
      VkResult sr = dev.submitAndWait(cmdBuf);
      auto end = std::chrono::high_resolution_clock::now();
      if (sr != VK_SUCCESS)
      {
        submitFailed = true;
        vkResetCommandBuffer(cmdBuf, 0);
        break;
      }

      double iterUs = (double)std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
      if (queryPool != VK_NULL_HANDLE)
      {
        uint64_t ts[2] = {};
        VkResult qr = vkGetQueryPoolResults(dev.device, queryPool, 0, 2, sizeof(ts), ts,
                                            sizeof(uint64_t),
                                            VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT);
        // Strict > (not >=): some drivers (e.g. Adreno+Turnip) return both
        // timestamps with the same value for buffer-copy ranges instead of
        // failing the query.  Treat equality as "timer didn't tick" and
        // fall back to the host wall clock.
        if (qr == VK_SUCCESS && ts[1] > ts[0])
          totalUs += (float)((double)(ts[1] - ts[0]) * props.limits.timestampPeriod / 1000.0);
        else
          totalUs += (float)iterUs;
      }
      else
      {
        totalUs += (float)iterUs;
      }

      vkResetCommandBuffer(cmdBuf, 0);
    }

    if (submitFailed)
      return -1.0f;
    // A genuine zero reading (no measurable wall-clock time AND no
    // timestamp delta) is the timer-broken case -- don't paper over it
    // with FLT_EPSILON, since (bytes / FLT_EPSILON / 1e3) prints 1.13e12
    // GBPS and looks like a real result.
    if (totalUs <= 0.0f)
      return -1.0f;
    return totalUs / static_cast<float>(iters);
  };

  auto reportCopy = [&](const char *label, const char *metric, float us)
  {
    log->print(TAB TAB);
    log->print(label);
    if (us <= 0.0f)
    {
      log->print("submit failed (timer/device unreliable)" NEWLINE);
      log->recordSkip(metric, ResultStatus::Error,
                       "vkQueueSubmit/WaitIdle failed or timer returned zero");
      return;
    }
    float gbps = (float)bytes / us / 1e3f;
    log->print(gbps);
    log->print(NEWLINE);
    log->resultRecord(metric, gbps);
  };

  reportCopy("Host->Dev : ", "h2d", runCopy(hostBuf, devBuf));
  reportCopy("Dev->Host : ", "d2h", runCopy(devBuf, hostBuf));

  if (queryPool != VK_NULL_HANDLE)
    vkDestroyQueryPool(dev.device, queryPool, nullptr);
  vkFreeCommandBuffers(dev.device, dev.commandPool, 1, &cmdBuf);
  vkDestroyBuffer(dev.device, hostBuf, nullptr);
  vkFreeMemory(dev.device, hostMem, nullptr);
  vkDestroyBuffer(dev.device, devBuf, nullptr);
  vkFreeMemory(dev.device, devMem, nullptr);

  // transfer_bandwidth
  return 0;
}

// ---------------------------------------------------------------------------
// Local memory bandwidth (Vulkan -- shared memory)
// ---------------------------------------------------------------------------
//
// Same single-output-buffer scaffolding as runComputeKernel; only the bytes-
// per-WI calculation differs per variant.  Width = 1/2/4/8 floats per slot.

int vkPeak::runLocalBandwidth(VulkanDevice &dev, benchmark_config_t &cfg)
{
  unsigned int iters = cfg.computeIters;

  log->print(NEWLINE TAB "Local memory bandwidth (GBPS)" NEWLINE);
  auto scope_3 = log->resultScope("local_memory_bandwidth");
  log->resultScopeAttribute("unit", "gbps");

  const uint32_t wgSize = 256;
  uint64_t globalWIs = targetGlobalThreads(dev.info.numCUs);
  uint32_t numGroups = (uint32_t)(globalWIs / wgSize);
  uint64_t bufferBytes = (uint64_t)globalWIs * sizeof(float);

  VkBuffer outBuf;
  VkDeviceMemory outMem;
  if (!dev.createBuffer(bufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
                        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, outBuf, outMem))
  {
    log->print(TAB TAB "Failed to allocate buffer" NEWLINE);
    return -1;
  }

  // Descriptor set layout: 1 storage buffer.
  VkDescriptorSetLayoutBinding binding = {};
  binding.binding = 0;
  binding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
  binding.descriptorCount = 1;
  binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;

  VkDescriptorSetLayoutCreateInfo dslCI = {};
  dslCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
  dslCI.bindingCount = 1; dslCI.pBindings = &binding;
  VkDescriptorSetLayout dsLayout;
  vkCreateDescriptorSetLayout(dev.device, &dslCI, nullptr, &dsLayout);

  VkPipelineLayoutCreateInfo plCI = {};
  plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
  plCI.setLayoutCount = 1; plCI.pSetLayouts = &dsLayout;
  VkPipelineLayout pipeLayout;
  vkCreatePipelineLayout(dev.device, &plCI, nullptr, &pipeLayout);

  VkDescriptorPoolSize ps = {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1};
  VkDescriptorPoolCreateInfo dpCI = {};
  dpCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
  dpCI.maxSets = 1; dpCI.poolSizeCount = 1; dpCI.pPoolSizes = &ps;
  VkDescriptorPool descPool;
  vkCreateDescriptorPool(dev.device, &dpCI, nullptr, &descPool);

  VkDescriptorSetAllocateInfo dsAI = {};
  dsAI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
  dsAI.descriptorPool = descPool; dsAI.descriptorSetCount = 1; dsAI.pSetLayouts = &dsLayout;
  VkDescriptorSet descSet;
  vkAllocateDescriptorSets(dev.device, &dsAI, &descSet);

  VkDescriptorBufferInfo bi = {outBuf, 0, bufferBytes};
  VkWriteDescriptorSet w = {};
  w.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
  w.dstSet = descSet; w.dstBinding = 0; w.descriptorCount = 1;
  w.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; w.pBufferInfo = &bi;
  vkUpdateDescriptorSets(dev.device, 1, &w, 0, nullptr);

  struct V { const char *label; const uint32_t *spv; size_t sz; uint32_t width; };
  const V variants[] = {
    {"float  ", vk_shaders::local_bandwidth_v1, vk_shaders::local_bandwidth_v1_size, 1},
    {"float2 ", vk_shaders::local_bandwidth_v2, vk_shaders::local_bandwidth_v2_size, 2},
    {"float4 ", vk_shaders::local_bandwidth_v4, vk_shaders::local_bandwidth_v4_size, 4},
    {"float8 ", vk_shaders::local_bandwidth_v8, vk_shaders::local_bandwidth_v8_size, 8},
  };
  for (const auto &v : variants)
  {
    log->print(TAB TAB);
    log->print(v.label);
    log->print(": ");
    VkPipeline pipe;
    if (!dev.createComputePipeline(v.spv, v.sz, dsLayout, pipeLayout, pipe))
    {
      log->print("pipeline failed" NEWLINE);
      continue;
    }
    float us = runKernel(dev, pipe, pipeLayout, descSet, numGroups, iters);
    // Strip padding from the display label for the result metric key.
    std::string key(v.label);
    while (!key.empty() && key.back() == ' ') key.pop_back();
    if (us <= 0.0f)
    {
      log->print("submit failed" NEWLINE);
      log->recordSkip(key, ResultStatus::Error, "vkQueueSubmit/WaitIdle failed");
      vkDestroyPipeline(dev.device, pipe, nullptr);
      continue;
    }
    // Each rep: 1 write + 1 read per WI = 2 * width * sizeof(float) bytes.
    uint64_t bytes = (uint64_t)LMEM_REPS * 2 * v.width * sizeof(float) * globalWIs;
    float gbps = (float)bytes / us / 1e3f;
    log->print(gbps);
    log->print(NEWLINE);
    log->resultRecord(key, gbps);
    vkDestroyPipeline(dev.device, pipe, nullptr);
  }

  vkDestroyDescriptorPool(dev.device, descPool, nullptr);
  vkDestroyPipelineLayout(dev.device, pipeLayout, nullptr);
  vkDestroyDescriptorSetLayout(dev.device, dsLayout, nullptr);
  vkDestroyBuffer(dev.device, outBuf, nullptr);
  vkFreeMemory(dev.device, outMem, nullptr);
  return 0;
}

// ---------------------------------------------------------------------------
// Image (texture) bandwidth (Vulkan)
// ---------------------------------------------------------------------------
//
// Combined image-sampler descriptor + storage-buffer output.  Image is
// VK_FORMAT_R32G32B32A32_SFLOAT, sampled with NEAREST + CLAMP_TO_EDGE.

int vkPeak::runImageBandwidth(VulkanDevice &dev, benchmark_config_t &cfg)
{
  unsigned int iters = cfg.globalBWIters;

  log->print(NEWLINE TAB "Image memory bandwidth (GBPS)" NEWLINE);
  auto scope_4 = log->resultScope("image_memory_bandwidth");
  log->resultScopeAttribute("unit", "gbps");

  const uint32_t imgW = 4096, imgH = 4096;
  const uint32_t wgSize = 256;
  uint64_t globalWIs = targetGlobalThreads(dev.info.numCUs);
  uint32_t numGroups = (uint32_t)(globalWIs / wgSize);
  uint64_t outBytes  = globalWIs * sizeof(float);

  // Create image (RGBA32F, sampled, transfer-dst so we can clear it).
  VkImageCreateInfo imgCI = {};
  imgCI.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
  imgCI.imageType = VK_IMAGE_TYPE_2D;
  imgCI.format    = VK_FORMAT_R32G32B32A32_SFLOAT;
  imgCI.extent    = {imgW, imgH, 1};
  imgCI.mipLevels = 1; imgCI.arrayLayers = 1;
  imgCI.samples   = VK_SAMPLE_COUNT_1_BIT;
  imgCI.tiling    = VK_IMAGE_TILING_OPTIMAL;
  imgCI.usage     = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
  imgCI.sharingMode   = VK_SHARING_MODE_EXCLUSIVE;
  imgCI.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;

  VkImage img;
  if (vkCreateImage(dev.device, &imgCI, nullptr, &img) != VK_SUCCESS)
  {
    log->print(TAB TAB "Image create failed" NEWLINE);
    return -1;
  }

  // Allocate device-local memory for the image.
  VkMemoryRequirements imReq;
  vkGetImageMemoryRequirements(dev.device, img, &imReq);
  VkPhysicalDeviceMemoryProperties memProps;
  vkGetPhysicalDeviceMemoryProperties(dev.physicalDevice, &memProps);
  uint32_t typeIdx = UINT32_MAX;
  for (uint32_t i = 0; i < memProps.memoryTypeCount; i++)
    if ((imReq.memoryTypeBits & (1u << i)) &&
        (memProps.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT))
    { typeIdx = i; break; }

  VkMemoryAllocateInfo aI = {};
  aI.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
  aI.allocationSize = imReq.size;
  aI.memoryTypeIndex = typeIdx;
  VkDeviceMemory imgMem;
  vkAllocateMemory(dev.device, &aI, nullptr, &imgMem);
  vkBindImageMemory(dev.device, img, imgMem, 0);

  // Transition UNDEFINED -> SHADER_READ_ONLY_OPTIMAL.  We don't need to
  // upload any data; the image contents being unspecified is fine for a
  // bandwidth measurement (the cache lines still get fetched).
  VkCommandBufferAllocateInfo cbAI = {};
  cbAI.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
  cbAI.commandPool = dev.commandPool;
  cbAI.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
  cbAI.commandBufferCount = 1;
  VkCommandBuffer transCmd;
  vkAllocateCommandBuffers(dev.device, &cbAI, &transCmd);
  VkCommandBufferBeginInfo cbBI = {};
  cbBI.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
  cbBI.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
  vkBeginCommandBuffer(transCmd, &cbBI);
  VkImageMemoryBarrier b = {};
  b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
  b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
  b.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
  b.image = img;
  b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
  b.srcAccessMask = 0;
  b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
  vkCmdPipelineBarrier(transCmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
                       VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0,
                       0, nullptr, 0, nullptr, 1, &b);
  vkEndCommandBuffer(transCmd);
  dev.submitAndWait(transCmd);
  vkFreeCommandBuffers(dev.device, dev.commandPool, 1, &transCmd);

  VkImageViewCreateInfo ivCI = {};
  ivCI.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
  ivCI.image = img; ivCI.viewType = VK_IMAGE_VIEW_TYPE_2D;
  ivCI.format = VK_FORMAT_R32G32B32A32_SFLOAT;
  ivCI.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
  VkImageView imgView;
  vkCreateImageView(dev.device, &ivCI, nullptr, &imgView);

  VkSamplerCreateInfo smCI = {};
  smCI.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
  smCI.magFilter = VK_FILTER_NEAREST;
  smCI.minFilter = VK_FILTER_NEAREST;
  smCI.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
  smCI.addressModeU = smCI.addressModeV = smCI.addressModeW =
      VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
  smCI.unnormalizedCoordinates = VK_FALSE;
  VkSampler sampler;
  vkCreateSampler(dev.device, &smCI, nullptr, &sampler);

  VkBuffer outBuf; VkDeviceMemory outMem;
  if (!dev.createBuffer(outBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
                        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, outBuf, outMem))
  {
    log->print(TAB TAB "Output buffer alloc failed" NEWLINE);
    vkDestroySampler(dev.device, sampler, nullptr);
    vkDestroyImageView(dev.device, imgView, nullptr);
    vkDestroyImage(dev.device, img, nullptr);
    vkFreeMemory(dev.device, imgMem, nullptr);
    return -1;
  }

  VkDescriptorSetLayoutBinding bs[2] = {};
  bs[0].binding = 0; bs[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
  bs[0].descriptorCount = 1; bs[0].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
  bs[1].binding = 1; bs[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
  bs[1].descriptorCount = 1; bs[1].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
  VkDescriptorSetLayoutCreateInfo dslCI = {};
  dslCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
  dslCI.bindingCount = 2; dslCI.pBindings = bs;
  VkDescriptorSetLayout dsLayout;
  vkCreateDescriptorSetLayout(dev.device, &dslCI, nullptr, &dsLayout);

  VkPipelineLayoutCreateInfo plCI = {};
  plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
  plCI.setLayoutCount = 1; plCI.pSetLayouts = &dsLayout;
  VkPipelineLayout pipeLayout;
  vkCreatePipelineLayout(dev.device, &plCI, nullptr, &pipeLayout);

  VkDescriptorPoolSize ps[2] = {
    {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1},
    {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1},
  };
  VkDescriptorPoolCreateInfo dpCI = {};
  dpCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
  dpCI.maxSets = 1; dpCI.poolSizeCount = 2; dpCI.pPoolSizes = ps;
  VkDescriptorPool descPool;
  vkCreateDescriptorPool(dev.device, &dpCI, nullptr, &descPool);

  VkDescriptorSetAllocateInfo dsAI = {};
  dsAI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
  dsAI.descriptorPool = descPool; dsAI.descriptorSetCount = 1; dsAI.pSetLayouts = &dsLayout;
  VkDescriptorSet descSet;
  vkAllocateDescriptorSets(dev.device, &dsAI, &descSet);

  VkDescriptorImageInfo ii = {};
  ii.imageView = imgView; ii.sampler = sampler;
  ii.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
  VkDescriptorBufferInfo bi = {outBuf, 0, outBytes};
  VkWriteDescriptorSet ws[2] = {};
  ws[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
  ws[0].dstSet = descSet; ws[0].dstBinding = 0; ws[0].descriptorCount = 1;
  ws[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
  ws[0].pImageInfo = &ii;
  ws[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
  ws[1].dstSet = descSet; ws[1].dstBinding = 1; ws[1].descriptorCount = 1;
  ws[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
  ws[1].pBufferInfo = &bi;
  vkUpdateDescriptorSets(dev.device, 2, ws, 0, nullptr);

  VkPipeline pipe;
  bool ok = dev.createComputePipeline(vk_shaders::image_bandwidth_v1,
      vk_shaders::image_bandwidth_v1_size, dsLayout, pipeLayout, pipe);
  log->print(TAB TAB "float4 : ");
  if (!ok)
  {
    log->print("pipeline failed" NEWLINE);
  }
  else
  {
    float us = runKernel(dev, pipe, pipeLayout, descSet, numGroups, iters);
    if (us <= 0.0f)
    {
      log->print("submit failed" NEWLINE);
      log->recordSkip("float4", ResultStatus::Error, "vkQueueSubmit/WaitIdle failed");
    }
    else
    {
      uint64_t bytes = (uint64_t)IMAGE_FETCH_PER_WI * 4 * sizeof(float) * globalWIs;
      float gbps = (float)bytes / us / 1e3f;
      log->print(gbps);
      log->print(NEWLINE);
      log->resultRecord("float4", gbps);
    }
    vkDestroyPipeline(dev.device, pipe, nullptr);
  }

  vkDestroyDescriptorPool(dev.device, descPool, nullptr);
  vkDestroyPipelineLayout(dev.device, pipeLayout, nullptr);
  vkDestroyDescriptorSetLayout(dev.device, dsLayout, nullptr);
  vkDestroyBuffer(dev.device, outBuf, nullptr);
  vkFreeMemory(dev.device, outMem, nullptr);
  vkDestroySampler(dev.device, sampler, nullptr);
  vkDestroyImageView(dev.device, imgView, nullptr);
  vkDestroyImage(dev.device, img, nullptr);
  vkFreeMemory(dev.device, imgMem, nullptr);
  return 0;
}

// ---------------------------------------------------------------------------
// Atomic throughput (Vulkan -- global + local atomics)
// ---------------------------------------------------------------------------

int vkPeak::runAtomicThroughput(VulkanDevice &dev, benchmark_config_t &cfg)
{
  unsigned int iters = cfg.atomicIters;

  log->print(NEWLINE TAB "Atomic throughput (GOPS)" NEWLINE);
  auto scope_5 = log->resultScope("atomic_throughput");
  log->resultScopeAttribute("unit", "gops");

  const uint32_t wgSize = 256;
  uint64_t globalWIs = targetGlobalThreads(dev.info.numCUs);
  uint32_t numGroups = (uint32_t)(globalWIs / wgSize);

  // Helper: allocate a single-storage-buffer descriptor + dispatch + time.
  auto runOne = [&](const char *label, const uint32_t *spv, size_t spvSize,
                    uint64_t bufBytes) -> float
  {
    VkBuffer buf; VkDeviceMemory mem;
    if (!dev.createBuffer(bufBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
                          VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, buf, mem))
      return -1.0f;
    VkDescriptorSetLayoutBinding b = {};
    b.binding = 0; b.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
    b.descriptorCount = 1; b.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
    VkDescriptorSetLayoutCreateInfo dslCI = {};
    dslCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
    dslCI.bindingCount = 1; dslCI.pBindings = &b;
    VkDescriptorSetLayout dsl; vkCreateDescriptorSetLayout(dev.device, &dslCI, nullptr, &dsl);
    VkPipelineLayoutCreateInfo plCI = {};
    plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
    plCI.setLayoutCount = 1; plCI.pSetLayouts = &dsl;
    VkPipelineLayout pl; vkCreatePipelineLayout(dev.device, &plCI, nullptr, &pl);
    VkDescriptorPoolSize ps = {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1};
    VkDescriptorPoolCreateInfo dpCI = {};
    dpCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
    dpCI.maxSets = 1; dpCI.poolSizeCount = 1; dpCI.pPoolSizes = &ps;
    VkDescriptorPool dp; vkCreateDescriptorPool(dev.device, &dpCI, nullptr, &dp);
    VkDescriptorSetAllocateInfo dsAI = {};
    dsAI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
    dsAI.descriptorPool = dp; dsAI.descriptorSetCount = 1; dsAI.pSetLayouts = &dsl;
    VkDescriptorSet ds; vkAllocateDescriptorSets(dev.device, &dsAI, &ds);
    VkDescriptorBufferInfo bi = {buf, 0, bufBytes};
    VkWriteDescriptorSet w = {};
    w.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
    w.dstSet = ds; w.dstBinding = 0; w.descriptorCount = 1;
    w.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; w.pBufferInfo = &bi;
    vkUpdateDescriptorSets(dev.device, 1, &w, 0, nullptr);

    VkPipeline pipe;
    if (!dev.createComputePipeline(spv, spvSize, dsl, pl, pipe))
    {
      log->print("pipeline failed" NEWLINE);
      vkDestroyDescriptorPool(dev.device, dp, nullptr);
      vkDestroyPipelineLayout(dev.device, pl, nullptr);
      vkDestroyDescriptorSetLayout(dev.device, dsl, nullptr);
      vkDestroyBuffer(dev.device, buf, nullptr);
      vkFreeMemory(dev.device, mem, nullptr);
      return -1.0f;
    }
    float us = runKernel(dev, pipe, pl, ds, numGroups, iters);
    vkDestroyPipeline(dev.device, pipe, nullptr);
    vkDestroyDescriptorPool(dev.device, dp, nullptr);
    vkDestroyPipelineLayout(dev.device, pl, nullptr);
    vkDestroyDescriptorSetLayout(dev.device, dsl, nullptr);
    vkDestroyBuffer(dev.device, buf, nullptr);
    vkFreeMemory(dev.device, mem, nullptr);
    (void)label;
    return us;
  };

  // Global atomics: one int counter per WI -> 128 MB output.
  log->print(TAB TAB "int_global   : ");
  float us_g = runOne("int_global", vk_shaders::atomic_throughput_global,
      vk_shaders::atomic_throughput_global_size, globalWIs * sizeof(int32_t));
  if (us_g > 0)
  {
    float gops = ((float)globalWIs * (float)ATOMIC_REPS) / us_g / 1e3f;
    log->print(gops); log->print(NEWLINE);
    log->resultRecord("int_global", gops);
  }
  else
  {
    log->print("submit failed" NEWLINE);
    log->recordSkip("int_global", ResultStatus::Error, "vkQueueSubmit/WaitIdle failed");
  }

#ifdef CLPEAK_VK_HAS_ATOMIC_THROUGHPUT_GLOBAL_UINT64
  if (dev.info.atomicInt64Supported)
  {
    log->print(TAB TAB "ulong_global : ");
    float us = runOne("ulong_global", vk_shaders::atomic_throughput_global_uint64,
        vk_shaders::atomic_throughput_global_uint64_size, globalWIs * sizeof(uint64_t));
    if (us > 0)
    {
      float gops = ((float)globalWIs * (float)ATOMIC_REPS) / us / 1e3f;
      log->print(gops); log->print(NEWLINE);
      log->resultRecord("ulong_global", gops);
    }
    else
    {
      log->print("submit failed" NEWLINE);
      log->recordSkip("ulong_global", ResultStatus::Error, "vkQueueSubmit/WaitIdle failed");
    }
  }
#endif

  // Local atomics: one int counter per workgroup.
  log->print(TAB TAB "int_local    : ");
  float us_l = runOne("int_local", vk_shaders::atomic_throughput_local,
      vk_shaders::atomic_throughput_local_size, (uint64_t)numGroups * sizeof(int32_t));
  if (us_l > 0)
  {
    float gops = ((float)globalWIs * (float)ATOMIC_REPS) / us_l / 1e3f;
    log->print(gops); log->print(NEWLINE);
    log->resultRecord("int_local", gops);
  }
  else
  {
    log->print("submit failed" NEWLINE);
    log->recordSkip("int_local", ResultStatus::Error, "vkQueueSubmit/WaitIdle failed");
  }

  return 0;
}

// ---------------------------------------------------------------------------
// FP atomic throughput (Vulkan) -- emitted in the FpCompute phase, mirroring
// Metal's runAtomicThroughputFp.  Currently a single global_float row, gated
// on VK_EXT_shader_atomic_float + shaderBufferFloat32AtomicAdd.
// ---------------------------------------------------------------------------

#ifdef CLPEAK_VK_HAS_ATOMIC_THROUGHPUT_GLOBAL_FLOAT
int vkPeak::runAtomicThroughputFp(VulkanDevice &dev, benchmark_config_t &cfg)
{
  unsigned int iters = cfg.atomicIters;

  log->print(NEWLINE TAB "Atomic throughput (GFLOPS)" NEWLINE);
  auto scope = log->resultScope("atomic_throughput");
  log->resultScopeAttribute("unit", "gflops");

  if (!dev.info.atomicFloat32Supported)
  {
    log->print(TAB TAB "VK_EXT_shader_atomic_float / shaderBufferFloat32AtomicAdd not supported! Skipped" NEWLINE);
    log->recordSkip("float_global", ResultStatus::Unsupported,
                    "VK_EXT_shader_atomic_float not supported");
    return 0;
  }

  const uint32_t wgSize = 256;
  uint64_t globalWIs = targetGlobalThreads(dev.info.numCUs);
  uint32_t numGroups = (uint32_t)(globalWIs / wgSize);
  uint64_t bufBytes = globalWIs * sizeof(float);

  VkBuffer buf; VkDeviceMemory mem;
  if (!dev.createBuffer(bufBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
                        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, buf, mem))
  {
    log->print(TAB TAB "Failed to allocate buffer" NEWLINE);
    return -1;
  }
  VkDescriptorSetLayoutBinding b = {};
  b.binding = 0; b.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
  b.descriptorCount = 1; b.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
  VkDescriptorSetLayoutCreateInfo dslCI = {};
  dslCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
  dslCI.bindingCount = 1; dslCI.pBindings = &b;
  VkDescriptorSetLayout dsl; vkCreateDescriptorSetLayout(dev.device, &dslCI, nullptr, &dsl);
  VkPipelineLayoutCreateInfo plCI = {};
  plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
  plCI.setLayoutCount = 1; plCI.pSetLayouts = &dsl;
  VkPipelineLayout pl; vkCreatePipelineLayout(dev.device, &plCI, nullptr, &pl);
  VkDescriptorPoolSize ps = {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1};
  VkDescriptorPoolCreateInfo dpCI = {};
  dpCI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
  dpCI.maxSets = 1; dpCI.poolSizeCount = 1; dpCI.pPoolSizes = &ps;
  VkDescriptorPool dp; vkCreateDescriptorPool(dev.device, &dpCI, nullptr, &dp);
  VkDescriptorSetAllocateInfo dsAI = {};
  dsAI.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
  dsAI.descriptorPool = dp; dsAI.descriptorSetCount = 1; dsAI.pSetLayouts = &dsl;
  VkDescriptorSet ds; vkAllocateDescriptorSets(dev.device, &dsAI, &ds);
  VkDescriptorBufferInfo bi = {buf, 0, bufBytes};
  VkWriteDescriptorSet w = {};
  w.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
  w.dstSet = ds; w.dstBinding = 0; w.descriptorCount = 1;
  w.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; w.pBufferInfo = &bi;
  vkUpdateDescriptorSets(dev.device, 1, &w, 0, nullptr);

  log->print(TAB TAB "float_global : ");
  VkPipeline pipe;
  if (!dev.createComputePipeline(vk_shaders::atomic_throughput_global_float,
                                  vk_shaders::atomic_throughput_global_float_size,
                                  dsl, pl, pipe))
  {
    log->print("pipeline failed" NEWLINE);
  }
  else
  {
    float us = runKernel(dev, pipe, pl, ds, numGroups, iters);
    if (us <= 0.0f)
    {
      log->print("submit failed" NEWLINE);
      log->recordSkip("float_global", ResultStatus::Error, "vkQueueSubmit/WaitIdle failed");
    }
    else
    {
      float gflops = ((float)globalWIs * (float)ATOMIC_REPS) / us / 1e3f;
      log->print(gflops); log->print(NEWLINE);
      log->resultRecord("float_global", gflops);
    }
    vkDestroyPipeline(dev.device, pipe, nullptr);
  }

  vkDestroyDescriptorPool(dev.device, dp, nullptr);
  vkDestroyPipelineLayout(dev.device, pl, nullptr);
  vkDestroyDescriptorSetLayout(dev.device, dsl, nullptr);
  vkDestroyBuffer(dev.device, buf, nullptr);
  vkFreeMemory(dev.device, mem, nullptr);
  return 0;
}
#endif

// ---------------------------------------------------------------------------
// Kernel launch latency (Vulkan) -- two metrics:
//
//   dispatch  : one-way host-submit -> GPU-kernel-start.  Requires
//               VK_EXT_calibrated_timestamps to map host time into the GPU
//               clock domain.  Skipped (with note) when the extension is
//               unavailable.
//   roundtrip : std::chrono around vkQueueSubmit + vkQueueWaitIdle.  Always
//               reported, directly comparable to the same metric on
//               OpenCL/CUDA/Metal.
// ---------------------------------------------------------------------------

int vkPeak::runKernelLatency(VulkanDevice &dev, benchmark_config_t &cfg)
{
  unsigned int iters = cfg.kernelLatencyIters ? cfg.kernelLatencyIters : 1000;

  log->print(NEWLINE TAB "Kernel launch latency (us)" NEWLINE);
  auto scope_6 = log->resultScope("kernel_launch_latency");
  log->resultScopeAttribute("unit", "us");

  // Pipeline layout with no descriptor sets and no push constants.
  VkPipelineLayoutCreateInfo plCI = {};
  plCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
  VkPipelineLayout pipeLayout = VK_NULL_HANDLE;
  if (vkCreatePipelineLayout(dev.device, &plCI, nullptr, &pipeLayout) != VK_SUCCESS)
  {
    log->print(TAB TAB "pipeline layout creation failed" NEWLINE);
    return -1;
  }

  // Shader module from the embedded no-op SPIR-V.
  VkShaderModuleCreateInfo smCI = {};
  smCI.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
  smCI.codeSize = vk_shaders::kernel_latency_size;
  smCI.pCode    = vk_shaders::kernel_latency;
  VkShaderModule shaderModule = VK_NULL_HANDLE;
  if (vkCreateShaderModule(dev.device, &smCI, nullptr, &shaderModule) != VK_SUCCESS)
  {
    vkDestroyPipelineLayout(dev.device, pipeLayout, nullptr);
    log->print(TAB TAB "shader module creation failed" NEWLINE);
    return -1;
  }

  VkComputePipelineCreateInfo pCI = {};
  pCI.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO;
  pCI.layout = pipeLayout;
  pCI.stage.sType  = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
  pCI.stage.stage  = VK_SHADER_STAGE_COMPUTE_BIT;
  pCI.stage.module = shaderModule;
  pCI.stage.pName  = "main";
  VkPipeline pipeline = VK_NULL_HANDLE;
  if (vkCreateComputePipelines(dev.device, VK_NULL_HANDLE, 1, &pCI, nullptr, &pipeline) != VK_SUCCESS)
  {
    vkDestroyShaderModule(dev.device, shaderModule, nullptr);
    vkDestroyPipelineLayout(dev.device, pipeLayout, nullptr);
    log->print(TAB TAB "compute pipeline creation failed" NEWLINE);
    return -1;
  }

  // GPU timestamp query pool used by the dispatch-latency path: a single
  // TOP_OF_PIPE timestamp tells us when the GPU started executing.
  VkPhysicalDeviceProperties props;
  vkGetPhysicalDeviceProperties(dev.physicalDevice, &props);
  float timestampPeriodNs = props.limits.timestampPeriod;
  bool haveTimestamps = (timestampPeriodNs > 0.0f) && dev.info.calibratedTimestampsSupported;

  // Resolve VK_EXT_calibrated_timestamps function pointers.  These let us
  // capture a (host_t, gpu_t) pair "at the same instant" so we can convert
  // GPU timestamps into the host clock domain and measure exact one-way
  // dispatch latency, the same way OpenCL profiling reports QUEUED -> START.
  PFN_vkGetCalibratedTimestampsEXT pfnGetCalTs = nullptr;
  VkTimeDomainEXT hostDomain = VK_TIME_DOMAIN_DEVICE_EXT; // sentinel: unset
  if (haveTimestamps)
  {
    pfnGetCalTs = (PFN_vkGetCalibratedTimestampsEXT)
        vkGetDeviceProcAddr(dev.device, "vkGetCalibratedTimestampsEXT");
    auto pfnGetDomains = (PFN_vkGetPhysicalDeviceCalibrateableTimeDomainsEXT)
        vkGetInstanceProcAddr(instance, "vkGetPhysicalDeviceCalibrateableTimeDomainsEXT");
    if (pfnGetCalTs && pfnGetDomains)
    {
      uint32_t n = 0;
      pfnGetDomains(dev.physicalDevice, &n, nullptr);
      std::vector<VkTimeDomainEXT> domains(n);
      pfnGetDomains(dev.physicalDevice, &n, domains.data());
      // Prefer a host monotonic clock so we can read it cheaply with
      // std::chrono::steady_clock (CLOCK_MONOTONIC on POSIX, QPC on Windows).
      VkTimeDomainEXT preferred[] = {
#if defined(_WIN32)
        VK_TIME_DOMAIN_QUERY_PERFORMANCE_COUNTER_EXT,
#else
        VK_TIME_DOMAIN_CLOCK_MONOTONIC_EXT,
        VK_TIME_DOMAIN_CLOCK_MONOTONIC_RAW_EXT,
#endif
      };
      for (auto p : preferred)
      {
        for (auto d : domains) if (d == p) { hostDomain = p; break; }
        if (hostDomain != VK_TIME_DOMAIN_DEVICE_EXT) break;
      }
    }
    if (hostDomain == VK_TIME_DOMAIN_DEVICE_EXT)
      haveTimestamps = false; // can't pin host<->device clocks; skip dispatch
  }

  VkQueryPool queryPool = VK_NULL_HANDLE;
  if (haveTimestamps)
  {
    VkQueryPoolCreateInfo qpCI = {};
    qpCI.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
    qpCI.queryType = VK_QUERY_TYPE_TIMESTAMP;
    qpCI.queryCount = 1;
    if (vkCreateQueryPool(dev.device, &qpCI, nullptr, &queryPool) != VK_SUCCESS)
      haveTimestamps = false;
  }

  // Pre-record a 1x1x1 dispatch.  Reusing one command buffer across iters
  // keeps record overhead out of the timing loop.
  VkCommandBufferAllocateInfo allocInfo = {};
  allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
  allocInfo.commandPool = dev.commandPool;
  allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
  allocInfo.commandBufferCount = 1;
  VkCommandBuffer cmdBuf = VK_NULL_HANDLE;
  vkAllocateCommandBuffers(dev.device, &allocInfo, &cmdBuf);

  VkCommandBufferBeginInfo beginInfo = {};
  beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
  beginInfo.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;
  vkBeginCommandBuffer(cmdBuf, &beginInfo);
  if (haveTimestamps)
  {
    vkCmdResetQueryPool(cmdBuf, queryPool, 0, 1);
    vkCmdWriteTimestamp(cmdBuf, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, queryPool, 0);
  }
  vkCmdBindPipeline(cmdBuf, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
  vkCmdDispatch(cmdBuf, 1, 1, 1);
  vkEndCommandBuffer(cmdBuf);

  // Get a calibrated (host, gpu) pair.  Stored in driver-domain units:
  //   ns since CLOCK_MONOTONIC epoch (POSIX) or QPC ticks (Windows).
  // We re-read the matching host timestamp in the SAME domain inside the
  // hot loop, so the units always match across the subtraction.
  auto getCalibrated = [&](uint64_t &hostOut, uint64_t &gpuOut) -> bool {
    VkCalibratedTimestampInfoEXT infos[2] = {};
    infos[0].sType = VK_STRUCTURE_TYPE_CALIBRATED_TIMESTAMP_INFO_EXT;
    infos[0].timeDomain = hostDomain;
    infos[1].sType = VK_STRUCTURE_TYPE_CALIBRATED_TIMESTAMP_INFO_EXT;
    infos[1].timeDomain = VK_TIME_DOMAIN_DEVICE_EXT;
    uint64_t ts[2] = {0, 0};
    uint64_t maxDev = 0;
    if (pfnGetCalTs(dev.device, 2, infos, ts, &maxDev) != VK_SUCCESS)
      return false;
    hostOut = ts[0];
    gpuOut  = ts[1];
    return true;
  };

  // Read the host clock in the same domain as `hostDomain`.  POSIX:
  // CLOCK_MONOTONIC nanoseconds; Windows: QPC ticks.  Matches what the
  // driver returns for VK_TIME_DOMAIN_CLOCK_MONOTONIC{,_RAW}_EXT and
  // VK_TIME_DOMAIN_QUERY_PERFORMANCE_COUNTER_EXT respectively, so the
  // raw uint64 can be subtracted directly from `hostCalib`.
  auto hostNowInDriverDomain = [&]() -> uint64_t {
#if defined(_WIN32)
    LARGE_INTEGER c;
    QueryPerformanceCounter(&c);
    return (uint64_t)c.QuadPart;
#else
    struct timespec ts;
    clock_gettime((hostDomain == VK_TIME_DOMAIN_CLOCK_MONOTONIC_RAW_EXT)
                    ? CLOCK_MONOTONIC_RAW : CLOCK_MONOTONIC, &ts);
    return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
#endif
  };

  // Conversion factor from host driver-domain "ticks" to nanoseconds.
  // POSIX: ticks ARE nanoseconds, factor = 1.  Windows: ticks are QPC,
  // factor = 1e9 / QPC_frequency.
  double hostTickNs = 1.0;
#if defined(_WIN32)
  {
    LARGE_INTEGER freq;
    QueryPerformanceFrequency(&freq);
    hostTickNs = 1e9 / (double)freq.QuadPart;
  }
#endif

  // Calibrate ONCE before the loop.  vkGetCalibratedTimestampsEXT itself
  // costs a few microseconds; doing it per-iter pollutes the dispatch
  // measurement by exactly that cost.  Clock drift over our short
  // measurement window is negligible (<<1us across thousands of iters
  // on every modern OS clock).
  uint64_t hostCalib = 0, gpuCalib = 0;
  if (haveTimestamps && !getCalibrated(hostCalib, gpuCalib))
    haveTimestamps = false;

  // Warmup
  for (unsigned int w = 0; w < warmupCount; w++)
    dev.submitAndWait(cmdBuf);

  double totalDispatchUs  = 0;
  double totalRoundtripUs = 0;
  unsigned int dispatchSamples = 0;
  for (unsigned int i = 0; i < iters; i++)
  {
    uint64_t hostSubmit = haveTimestamps ? hostNowInDriverDomain() : 0;
    auto chronoStart = std::chrono::high_resolution_clock::now();
    dev.submitAndWait(cmdBuf);
    auto chronoEnd = std::chrono::high_resolution_clock::now();
    totalRoundtripUs += (double)std::chrono::duration_cast<std::chrono::nanoseconds>(chronoEnd - chronoStart).count() / 1000.0;

    if (haveTimestamps)
    {
      uint64_t gpuStart = 0;
      if (vkGetQueryPoolResults(dev.device, queryPool, 0, 1,
                                sizeof(gpuStart), &gpuStart, sizeof(uint64_t),
                                VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT) == VK_SUCCESS)
      {
        // Convert everything to nanoseconds, then to microseconds.
        double hostSubmitNs = (double)hostSubmit * hostTickNs;
        double hostCalibNs  = (double)hostCalib  * hostTickNs;
        double gpuDeltaNs   = (double)((int64_t)gpuStart - (int64_t)gpuCalib) * (double)timestampPeriodNs;
        double hostStartNs  = hostCalibNs + gpuDeltaNs;
        double dispatchUs   = (hostStartNs - hostSubmitNs) / 1000.0;
        if (dispatchUs > 0)
        {
          totalDispatchUs += dispatchUs;
          dispatchSamples++;
        }
      }
    }
  }

  double avgRoundtripUs = totalRoundtripUs / static_cast<double>(iters);
  log->print(TAB TAB "dispatch  : ");
  if (dispatchSamples > 0)
  {
    float dispatchUs = (float)(totalDispatchUs / dispatchSamples);
    log->print(dispatchUs);
    log->print(NEWLINE);
    log->resultRecord("dispatch", dispatchUs);
  }
  else
  {
    log->print("skipped (needs VK_EXT_calibrated_timestamps)" NEWLINE);
    log->recordSkip("dispatch", ResultStatus::Unsupported,
                     "Needs VK_EXT_calibrated_timestamps");
  }
  log->print(TAB TAB "roundtrip : ");
  log->print((float)avgRoundtripUs);
  log->print(NEWLINE);
  log->resultRecord("roundtrip", (float)avgRoundtripUs);

  vkFreeCommandBuffers(dev.device, dev.commandPool, 1, &cmdBuf);
  if (queryPool != VK_NULL_HANDLE)
    vkDestroyQueryPool(dev.device, queryPool, nullptr);
  vkDestroyPipeline(dev.device, pipeline, nullptr);
  vkDestroyShaderModule(dev.device, shaderModule, nullptr);
  vkDestroyPipelineLayout(dev.device, pipeLayout, nullptr);

  return 0;
}

// Free-function enumeration used by --list-devices (desktop) and the Android
// JNI surface. Spins up a throwaway VkInstance just long enough to read
// physical-device properties, then tears it down.
BackendInventory enumerateVulkan()
{
  BackendInventory inv;
  inv.backend = "Vulkan";

  vkPeak vk;
  if (!vk.initInstance())
    return inv;  // available stays false

  inv.available = !vk.physicalDevices.empty();

  InventoryPlatform plat;
  plat.index = 0;
  plat.name  = "Vulkan";

  for (size_t i = 0; i < vk.physicalDevices.size(); i++)
  {
    VkPhysicalDeviceProperties props;
    vkGetPhysicalDeviceProperties(vk.physicalDevices[i], &props);

    InventoryDevice dev;
    dev.index   = static_cast<int>(i);
    dev.name    = props.deviceName;
    dev.typeStr = (props.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU)   ? "Discrete GPU"
                : (props.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU) ? "Integrated GPU"
                : (props.deviceType == VK_PHYSICAL_DEVICE_TYPE_CPU)            ? "CPU"
                                                                               : "Other";
    {
      std::ostringstream api;
      api << VK_VERSION_MAJOR(props.apiVersion) << "."
          << VK_VERSION_MINOR(props.apiVersion) << "."
          << VK_VERSION_PATCH(props.apiVersion);
      dev.apiVersion = api.str();
    }
    plat.devices.push_back(std::move(dev));
  }

  inv.platforms.push_back(std::move(plat));
  return inv;
}

#endif // ENABLE_VULKAN
