update hooks to exceptions

This commit is contained in:
PancakeTAS 2025-07-16 23:53:59 +02:00 committed by Pancake
parent cfdf4e1d1b
commit 3526dde26c
4 changed files with 168 additions and 164 deletions

View file

@ -14,7 +14,6 @@ namespace Hooks {
VkDevice device; VkDevice device;
VkPhysicalDevice physicalDevice; VkPhysicalDevice physicalDevice;
std::pair<uint32_t, VkQueue> queue; // graphics family std::pair<uint32_t, VkQueue> queue; // graphics family
uint64_t frameGen; // amount of frames to generate
}; };
/// Map of hooked Vulkan functions. /// Map of hooked Vulkan functions.

View file

@ -4,8 +4,9 @@
#include <cstdint> #include <cstdint>
#include <cstddef> #include <cstddef>
#include <vector>
#include <utility> #include <utility>
#include <string>
#include <vector>
namespace Utils { namespace Utils {
@ -68,4 +69,20 @@ namespace Utils {
VkPipelineStageFlags pre, VkPipelineStageFlags post, VkPipelineStageFlags pre, VkPipelineStageFlags post,
bool makeSrcPresentable, bool makeDstPresentable); bool makeSrcPresentable, bool makeDstPresentable);
///
/// Log a message at most n times.
///
/// @param id The identifier for the log message.
/// @param n The maximum number of times to log the message.
/// @param message The message to log.
///
void logLimitN(const std::string& id, size_t n, const std::string& message);
///
/// Reset the log limit for a given identifier.
///
/// @param id The identifier for the log message.
///
void resetLimitN(const std::string& id) noexcept;
} }

View file

@ -1,248 +1,227 @@
#include "hooks.hpp" #include "hooks.hpp"
#include "common/exception.hpp"
#include "utils/utils.hpp"
#include "context.hpp" #include "context.hpp"
#include "layer.hpp" #include "layer.hpp"
#include "utils/log.hpp"
#include "utils/utils.hpp"
#include "common/exception.hpp"
#include <vulkan/vulkan_core.h> #include <vulkan/vulkan_core.h>
#include <cstdint> #include <unordered_map>
#include <cstdlib> #include <stdexcept>
#include <algorithm> #include <algorithm>
#include <exception> #include <exception>
#include <cstdint>
#include <cstdlib>
#include <string> #include <string>
#include <unordered_map>
#include <vector> #include <vector>
using namespace Hooks; using namespace Hooks;
namespace { namespace {
// instance hooks ///
/// Add extensions to the instance create info.
///
VkResult myvkCreateInstance( VkResult myvkCreateInstance(
const VkInstanceCreateInfo* pCreateInfo, const VkInstanceCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator, const VkAllocationCallbacks* pAllocator,
VkInstance* pInstance) { VkInstance* pInstance) {
// add extensions auto extensions = Utils::addExtensions(
auto extensions = Utils::addExtensions(pCreateInfo->ppEnabledExtensionNames, pCreateInfo->ppEnabledExtensionNames,
pCreateInfo->enabledExtensionCount, { pCreateInfo->enabledExtensionCount,
{
"VK_KHR_get_physical_device_properties2", "VK_KHR_get_physical_device_properties2",
"VK_KHR_external_memory_capabilities", "VK_KHR_external_memory_capabilities",
"VK_KHR_external_semaphore_capabilities" "VK_KHR_external_semaphore_capabilities"
}); }
);
VkInstanceCreateInfo createInfo = *pCreateInfo; VkInstanceCreateInfo createInfo = *pCreateInfo;
createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size()); createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
createInfo.ppEnabledExtensionNames = extensions.data(); createInfo.ppEnabledExtensionNames = extensions.data();
auto res = Layer::ovkCreateInstance(&createInfo, pAllocator, pInstance); auto res = Layer::ovkCreateInstance(&createInfo, pAllocator, pInstance);
if (res != VK_SUCCESS) { if (res == VK_ERROR_EXTENSION_NOT_PRESENT)
Log::error("hooks", "Failed to create Vulkan instance: {:x}", throw std::runtime_error(
static_cast<uint32_t>(res)); "Required Vulkan instance extensions are not present."
"Your GPU driver is not supported.");
return res; return res;
} }
Log::info("hooks", "Instance created successfully: {:x}", /// Map of devices to related information.
reinterpret_cast<uintptr_t>(*pInstance)); std::unordered_map<VkDevice, DeviceInfo> deviceToInfo;
return res;
}
void myvkDestroyInstance(
VkInstance instance,
const VkAllocationCallbacks* pAllocator) {
Log::info("hooks", "Instance destroyed successfully: {:x}",
reinterpret_cast<uintptr_t>(instance));
Layer::ovkDestroyInstance(instance, pAllocator);
}
// device hooks
std::unordered_map<VkDevice, DeviceInfo> devices;
///
/// Add extensions to the device create info.
/// (function pointers are not initialized yet)
///
VkResult myvkCreateDevicePre( VkResult myvkCreateDevicePre(
VkPhysicalDevice physicalDevice, VkPhysicalDevice physicalDevice,
const VkDeviceCreateInfo* pCreateInfo, const VkDeviceCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator, const VkAllocationCallbacks* pAllocator,
VkDevice* pDevice) { VkDevice* pDevice) {
// add extensions // add extensions
auto extensions = Utils::addExtensions(pCreateInfo->ppEnabledExtensionNames, auto extensions = Utils::addExtensions(
pCreateInfo->enabledExtensionCount, { pCreateInfo->ppEnabledExtensionNames,
pCreateInfo->enabledExtensionCount,
{
"VK_KHR_external_memory", "VK_KHR_external_memory",
"VK_KHR_external_memory_fd", "VK_KHR_external_memory_fd",
"VK_KHR_external_semaphore", "VK_KHR_external_semaphore",
"VK_KHR_external_semaphore_fd" "VK_KHR_external_semaphore_fd"
}); }
);
VkDeviceCreateInfo createInfo = *pCreateInfo; VkDeviceCreateInfo createInfo = *pCreateInfo;
createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size()); createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
createInfo.ppEnabledExtensionNames = extensions.data(); createInfo.ppEnabledExtensionNames = extensions.data();
auto res = Layer::ovkCreateDevice(physicalDevice, &createInfo, pAllocator, pDevice); auto res = Layer::ovkCreateDevice(physicalDevice, &createInfo, pAllocator, pDevice);
if (res != VK_SUCCESS) { if (res == VK_ERROR_EXTENSION_NOT_PRESENT)
Log::error("hooks", "Failed to create Vulkan device: {:x}", throw std::runtime_error(
static_cast<uint32_t>(res)); "Required Vulkan device extensions are not present."
return res; "Your GPU driver is not supported.");
}
Log::info("hooks", "Device created successfully: {:x}",
reinterpret_cast<uintptr_t>(*pDevice));
return res; return res;
} }
///
/// Add related device information after the device is created.
///
VkResult myvkCreateDevicePost( VkResult myvkCreateDevicePost(
VkPhysicalDevice physicalDevice, VkPhysicalDevice physicalDevice,
VkDeviceCreateInfo* pCreateInfo, VkDeviceCreateInfo* pCreateInfo,
const VkAllocationCallbacks*, const VkAllocationCallbacks*,
VkDevice* pDevice) { VkDevice* pDevice) {
// store device info deviceToInfo.emplace(*pDevice, DeviceInfo {
Log::debug("hooks", "Creating device info for device: {:x}",
reinterpret_cast<uintptr_t>(*pDevice));
try {
const char* frameGenEnv = std::getenv("LSFG_MULTIPLIER");
const uint64_t frameGen = static_cast<uint64_t>(
std::max<int64_t>(1, std::stol(frameGenEnv ? frameGenEnv : "2") - 1));
Log::debug("hooks", "Using {}x frame generation",
frameGen + 1);
auto queue = Utils::findQueue(*pDevice, physicalDevice, pCreateInfo,
VK_QUEUE_GRAPHICS_BIT);
Log::debug("hooks", "Found queue at index {}: {:x}",
queue.first, reinterpret_cast<uintptr_t>(queue.second));
devices.emplace(*pDevice, DeviceInfo {
.device = *pDevice, .device = *pDevice,
.physicalDevice = physicalDevice, .physicalDevice = physicalDevice,
.queue = queue, .queue = Utils::findQueue(*pDevice, physicalDevice, pCreateInfo, VK_QUEUE_GRAPHICS_BIT)
.frameGen = frameGen,
}); });
} catch (const std::exception& e) {
Log::error("hooks", "Failed to create device info: {}", e.what());
return VK_ERROR_INITIALIZATION_FAILED;
}
Log::info("hooks", "Device info created successfully for: {:x}",
reinterpret_cast<uintptr_t>(*pDevice));
return VK_SUCCESS; return VK_SUCCESS;
} }
void myvkDestroyDevice(VkDevice device, const VkAllocationCallbacks* pAllocator) { /// Erase the device information when the device is destroyed.
devices.erase(device); // erase device info void myvkDestroyDevice(VkDevice device, const VkAllocationCallbacks* pAllocator) noexcept {
deviceToInfo.erase(device);
Log::info("hooks", "Device & Device info destroyed successfully: {:x}",
reinterpret_cast<uintptr_t>(device));
Layer::ovkDestroyDevice(device, pAllocator); Layer::ovkDestroyDevice(device, pAllocator);
} }
// swapchain hooks
std::unordered_map<VkSwapchainKHR, LsContext> swapchains; std::unordered_map<VkSwapchainKHR, LsContext> swapchains;
std::unordered_map<VkSwapchainKHR, VkDevice> swapchainToDeviceTable; std::unordered_map<VkSwapchainKHR, VkDevice> swapchainToDeviceTable;
///
/// Adjust swapchain creation parameters and create a swapchain context.
///
VkResult myvkCreateSwapchainKHR( VkResult myvkCreateSwapchainKHR(
VkDevice device, VkDevice device,
const VkSwapchainCreateInfoKHR* pCreateInfo, const VkSwapchainCreateInfoKHR* pCreateInfo,
const VkAllocationCallbacks* pAllocator, const VkAllocationCallbacks* pAllocator,
VkSwapchainKHR* pSwapchain) { VkSwapchainKHR* pSwapchain) noexcept {
auto it = devices.find(device); // find device
if (it == devices.end()) { auto it = deviceToInfo.find(device);
Log::warn("hooks", "Created swapchain without device info present"); if (it == deviceToInfo.end()) {
Utils::logLimitN("swapMap", 5, "Device not found in map");
return Layer::ovkCreateSwapchainKHR(device, pCreateInfo, pAllocator, pSwapchain); return Layer::ovkCreateSwapchainKHR(device, pCreateInfo, pAllocator, pSwapchain);
} }
Utils::resetLimitN("swapMap");
auto& deviceInfo = it->second; auto& deviceInfo = it->second;
// update swapchain create info // increase amount of images in swapchain
VkSwapchainCreateInfoKHR createInfo = *pCreateInfo; VkSwapchainCreateInfoKHR createInfo = *pCreateInfo;
const uint32_t maxImageCount = Utils::getMaxImageCount(deviceInfo.physicalDevice, pCreateInfo->surface); const auto maxImages = Utils::getMaxImageCount(
const uint32_t imageCount = createInfo.minImageCount + 1 + static_cast<uint32_t>(deviceInfo.frameGen); deviceInfo.physicalDevice, pCreateInfo->surface);
Log::debug("hooks", "Creating swapchain with max image count: {}/{}", imageCount, maxImageCount); createInfo.minImageCount = createInfo.minImageCount + 1
if (imageCount > maxImageCount) { + static_cast<uint32_t>(deviceInfo.queue.first);
Log::warn("hooks", "LSFG_MULTIPLIER is set very high. This might lead to performance degradation"); if (createInfo.minImageCount > maxImages) {
createInfo.minImageCount = maxImageCount; // limit to max possible createInfo.minImageCount = maxImages;
Utils::logLimitN("swapCount", 10,
"Requested image count (" +
std::to_string(pCreateInfo->minImageCount) + ") "
"exceeds maximum allowed (" +
std::to_string(maxImages) + "). "
"Continuing with maximum allowed image count. "
"This might lead to performance degradation.");
} else { } else {
createInfo.minImageCount = imageCount; // set to frameGen + 1 Utils::resetLimitN("swapCount");
} }
createInfo.imageUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; // allow copy from/to images
createInfo.imageUsage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
createInfo.presentMode = VK_PRESENT_MODE_FIFO_KHR; // force vsync
auto res = Layer::ovkCreateSwapchainKHR(device, &createInfo, pAllocator, pSwapchain);
if (res != VK_SUCCESS) {
Log::error("hooks", "Failed to create swapchain: {}", static_cast<uint32_t>(res));
return res;
}
Log::info("hooks", "Swapchain created successfully: {:x}",
reinterpret_cast<uintptr_t>(*pSwapchain));
// retire previous swapchain if it exists // allow copy operations on swapchain images
createInfo.imageUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT;
createInfo.imageUsage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
// enforce vsync
createInfo.presentMode = VK_PRESENT_MODE_FIFO_KHR;
// retire potential old swapchain
if (pCreateInfo->oldSwapchain) { if (pCreateInfo->oldSwapchain) {
Log::debug("hooks", "Retiring previous swapchain context: {:x}",
reinterpret_cast<uintptr_t>(pCreateInfo->oldSwapchain));
swapchains.erase(pCreateInfo->oldSwapchain); swapchains.erase(pCreateInfo->oldSwapchain);
swapchainToDeviceTable.erase(pCreateInfo->oldSwapchain); swapchainToDeviceTable.erase(pCreateInfo->oldSwapchain);
Log::info("hooks", "Previous swapchain context retired successfully: {:x}",
reinterpret_cast<uintptr_t>(pCreateInfo->oldSwapchain));
} }
// create swapchain context // create swapchain
Log::debug("hooks", "Creating swapchain context for device: {:x}", auto res = Layer::ovkCreateSwapchainKHR(device, &createInfo, pAllocator, pSwapchain);
reinterpret_cast<uintptr_t>(device)); if (res != VK_SUCCESS)
return res; // can't be caused by lsfg-vk (yet)
try { try {
// get swapchain images // get all swapchain images
uint32_t imageCount{}; uint32_t imageCount{};
res = Layer::ovkGetSwapchainImagesKHR(device, *pSwapchain, &imageCount, nullptr); res = Layer::ovkGetSwapchainImagesKHR(device, *pSwapchain, &imageCount, nullptr);
if (res != VK_SUCCESS || imageCount == 0) if (res != VK_SUCCESS || imageCount == 0)
throw LSFG::vulkan_error(res, "Failed to get swapchain images count"); throw LSFG::vulkan_error(res, "Failed to get swapchain image count");
std::vector<VkImage> swapchainImages(imageCount); std::vector<VkImage> swapchainImages(imageCount);
res = Layer::ovkGetSwapchainImagesKHR(device, *pSwapchain, &imageCount, swapchainImages.data()); res = Layer::ovkGetSwapchainImagesKHR(device, *pSwapchain,
&imageCount, swapchainImages.data());
if (res != VK_SUCCESS) if (res != VK_SUCCESS)
throw LSFG::vulkan_error(res, "Failed to get swapchain images"); throw LSFG::vulkan_error(res, "Failed to get swapchain images");
Log::debug("hooks", "Swapchain has {} images", swapchainImages.size());
// create swapchain context // create swapchain context
swapchainToDeviceTable.emplace(*pSwapchain, device);
swapchains.emplace(*pSwapchain, LsContext( swapchains.emplace(*pSwapchain, LsContext(
deviceInfo, *pSwapchain, pCreateInfo->imageExtent, deviceInfo, *pSwapchain, pCreateInfo->imageExtent,
swapchainImages swapchainImages
)); ));
swapchainToDeviceTable.emplace(*pSwapchain, device);
} catch (const LSFG::vulkan_error& e) { Utils::resetLimitN("swapCtxCreate");
Log::error("hooks", "Encountered Vulkan error {:x} while creating swapchain context: {}",
static_cast<uint32_t>(e.error()), e.what());
return e.error();
} catch (const std::exception& e) { } catch (const std::exception& e) {
Log::error("hooks", "Encountered error while creating swapchain context: {}", e.what()); Utils::logLimitN("swapCtxCreate", 5,
"An error occurred while creating the swapchain wrapper:\n"
"- " + std::string(e.what()));
return VK_ERROR_INITIALIZATION_FAILED; return VK_ERROR_INITIALIZATION_FAILED;
} }
return VK_SUCCESS;
Log::info("hooks", "Swapchain context created successfully for: {:x}",
reinterpret_cast<uintptr_t>(*pSwapchain));
return res;
} }
///
/// Update presentation parameters and present the next frame(s).
///
VkResult myvkQueuePresentKHR( VkResult myvkQueuePresentKHR(
VkQueue queue, VkQueue queue,
const VkPresentInfoKHR* pPresentInfo) { const VkPresentInfoKHR* pPresentInfo) noexcept {
// find swapchain device
auto it = swapchainToDeviceTable.find(*pPresentInfo->pSwapchains); auto it = swapchainToDeviceTable.find(*pPresentInfo->pSwapchains);
if (it == swapchainToDeviceTable.end()) { if (it == swapchainToDeviceTable.end()) {
Log::warn("hooks2", "Swapchain {:x} not found in swapchainToDeviceTable", Utils::logLimitN("swapMap", 5,
reinterpret_cast<uintptr_t>(*pPresentInfo->pSwapchains)); "Swapchain not found in map");
return Layer::ovkQueuePresentKHR(queue, pPresentInfo); return Layer::ovkQueuePresentKHR(queue, pPresentInfo);
} }
auto it2 = devices.find(it->second);
if (it2 == devices.end()) { // find device info
Log::warn("hooks2", "Device {:x} not found in devices", auto it2 = deviceToInfo.find(it->second);
reinterpret_cast<uintptr_t>(it->second)); if (it2 == deviceToInfo.end()) {
Utils::logLimitN("swapMap", 5,
"Device not found in map");
return Layer::ovkQueuePresentKHR(queue, pPresentInfo); return Layer::ovkQueuePresentKHR(queue, pPresentInfo);
} }
auto& deviceInfo = it2->second;
// find swapchain context
auto it3 = swapchains.find(*pPresentInfo->pSwapchains); auto it3 = swapchains.find(*pPresentInfo->pSwapchains);
if (it3 == swapchains.end()) { if (it3 == swapchains.end()) {
Log::warn("hooks2", "Swapchain {:x} not found in swapchains", Utils::logLimitN("swapMap", 5,
reinterpret_cast<uintptr_t>(*pPresentInfo->pSwapchains)); "Swapchain context not found in map");
return Layer::ovkQueuePresentKHR(queue, pPresentInfo); return Layer::ovkQueuePresentKHR(queue, pPresentInfo);
} }
auto& deviceInfo = it2->second;
auto& swapchain = it3->second; auto& swapchain = it3->second;
// patch vsync NOLINTBEGIN // enforce vsync | NOLINTBEGIN
#pragma clang diagnostic push #pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" #pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
const VkSwapchainPresentModeInfoEXT* presentModeInfo = const VkSwapchainPresentModeInfoEXT* presentModeInfo =
@ -255,48 +234,35 @@ namespace {
} }
presentModeInfo = presentModeInfo =
reinterpret_cast<const VkSwapchainPresentModeInfoEXT*>(presentModeInfo->pNext); reinterpret_cast<const VkSwapchainPresentModeInfoEXT*>(presentModeInfo->pNext);
} // NOLINTEND }
#pragma clang diagnostic pop #pragma clang diagnostic pop
// present the next frame // NOLINTEND | present the next frame
Log::debug("hooks2", "Presenting swapchain: {:x} on queue: {:x}", VkResult res{}; // might return VK_SUBOPTIMAL_KHR
reinterpret_cast<uintptr_t>(*pPresentInfo->pSwapchains),
reinterpret_cast<uintptr_t>(queue));
VkResult res{};
try { try {
std::vector<VkSemaphore> semaphores(pPresentInfo->waitSemaphoreCount); std::vector<VkSemaphore> semaphores(pPresentInfo->waitSemaphoreCount);
std::copy_n(pPresentInfo->pWaitSemaphores, semaphores.size(), semaphores.data()); std::copy_n(pPresentInfo->pWaitSemaphores, semaphores.size(), semaphores.data());
Log::debug("hooks2", "Waiting on {} semaphores", semaphores.size());
// present the next frame
res = swapchain.present(deviceInfo, pPresentInfo->pNext, res = swapchain.present(deviceInfo, pPresentInfo->pNext,
queue, semaphores, *pPresentInfo->pImageIndices); queue, semaphores, *pPresentInfo->pImageIndices);
} catch (const LSFG::vulkan_error& e) {
Log::error("hooks2", "Encountered Vulkan error {:x} while presenting: {}", Utils::resetLimitN("swapPresent");
static_cast<uint32_t>(e.error()), e.what());
return e.error();
} catch (const std::exception& e) { } catch (const std::exception& e) {
Log::error("hooks2", "Encountered error while creating presenting: {}", Utils::logLimitN("swapPresent", 5,
e.what()); "An error occurred while presenting the swapchain:\n"
"- " + std::string(e.what()));
return VK_ERROR_INITIALIZATION_FAILED; return VK_ERROR_INITIALIZATION_FAILED;
} }
// non VK_SUCCESS or VK_SUBOPTIMAL_KHR doesn't reach here
Log::debug("hooks2", "Presented swapchain {:x} on queue {:x} successfully",
reinterpret_cast<uintptr_t>(*pPresentInfo->pSwapchains),
reinterpret_cast<uintptr_t>(queue));
return res; return res;
} }
/// Erase the swapchain context and mapping when the swapchain is destroyed.
void myvkDestroySwapchainKHR( void myvkDestroySwapchainKHR(
VkDevice device, VkDevice device,
VkSwapchainKHR swapchain, VkSwapchainKHR swapchain,
const VkAllocationCallbacks* pAllocator) { const VkAllocationCallbacks* pAllocator) noexcept {
swapchains.erase(swapchain); // erase swapchain context swapchains.erase(swapchain);
swapchainToDeviceTable.erase(swapchain); swapchainToDeviceTable.erase(swapchain);
Log::info("hooks", "Swapchain & Swapchain context destroyed successfully: {:x}",
reinterpret_cast<uintptr_t>(swapchain));
Layer::ovkDestroySwapchainKHR(device, swapchain, pAllocator); Layer::ovkDestroySwapchainKHR(device, swapchain, pAllocator);
} }
} }
@ -304,7 +270,6 @@ namespace {
std::unordered_map<std::string, PFN_vkVoidFunction> Hooks::hooks = { std::unordered_map<std::string, PFN_vkVoidFunction> Hooks::hooks = {
// instance hooks // instance hooks
{"vkCreateInstance", reinterpret_cast<PFN_vkVoidFunction>(myvkCreateInstance)}, {"vkCreateInstance", reinterpret_cast<PFN_vkVoidFunction>(myvkCreateInstance)},
{"vkDestroyInstance", reinterpret_cast<PFN_vkVoidFunction>(myvkDestroyInstance)},
// device hooks // device hooks
{"vkCreateDevicePre", reinterpret_cast<PFN_vkVoidFunction>(myvkCreateDevicePre)}, {"vkCreateDevicePre", reinterpret_cast<PFN_vkVoidFunction>(myvkCreateDevicePre)},

View file

@ -5,11 +5,14 @@
#include <vulkan/vulkan_core.h> #include <vulkan/vulkan_core.h>
#include <cstdint> #include <unordered_map>
#include <cstring>
#include <algorithm> #include <algorithm>
#include <optional> #include <optional>
#include <iostream>
#include <cstdint>
#include <cstring>
#include <utility> #include <utility>
#include <string>
#include <vector> #include <vector>
using namespace Utils; using namespace Utils;
@ -183,3 +186,23 @@ void Utils::copyImage(VkCommandBuffer buf,
1, &presentBarrier); 1, &presentBarrier);
} }
} }
namespace {
auto& logCounts() {
static std::unordered_map<std::string, size_t> map;
return map;
}
}
void Utils::logLimitN(const std::string& id, size_t n, const std::string& message) {
auto& count = logCounts()[id];
if (count <= n)
std::cerr << "lsfg-vk: " << message << '\n';
if (count == n)
std::cerr << "(above message has been repeated " << n << " times, suppressing further)\n";
count++;
}
void Utils::resetLimitN(const std::string& id) noexcept {
logCounts().erase(id);
}