diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 4a6b870..9726626 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -158,7 +158,7 @@ On Hyprland, also check that direct scanout is off (`hyprctl getoption render:di ## Hybrid graphics laptops (AMD/Intel iGPU + NVIDIA) -Metal renders with Vulkan, and each finished frame is handed to an OpenGL (EGL) context that presents it. The hand-off uses opaque file descriptors, which only work when Vulkan and OpenGL run on the same GPU. Indium takes the first Vulkan device the loader lists, and on a hybrid laptop both that device and the EGL context are normally the integrated GPU, so the discrete GPU stays idle. A report in [issue #3](https://github.com/lucannez64/darling-lol/issues/3) (Ryzen 9 5900HS, RTX 3060 Laptop) measured about 70 FPS in ARAM; which GPU rendered that match is not known. **Running on an NVIDIA GPU is not validated.** +Metal renders with Vulkan, and each finished frame is handed to an OpenGL (EGL) context that presents it. The hand-off uses opaque file descriptors, which only work when Vulkan and OpenGL run on the same GPU. Indium takes the first Vulkan device the loader lists, and on a hybrid laptop both that device and the EGL context are normally the integrated GPU, so the discrete GPU stays idle. A report in [issue #3](https://github.com/lucannez64/darling-lol/issues/3) (Ryzen 9 5900HS, RTX 3060 Laptop) measured 60-70 FPS in ARAM on the integrated GPU; on the NVIDIA GPU the game showed a black screen. **Running on an NVIDIA GPU is not validated.** ### Run on the discrete GPU @@ -185,6 +185,25 @@ Each process that uses Metal appends to guest `/tmp/metal-device.log` (host `~/. `MISMATCH` on the presenting line means Vulkan and OpenGL are on different GPUs. `match=unknown` means the OpenGL driver reports no device UUID. Include this file with performance reports. +The log also lists the selected device's memory heaps and types, and one line the first time each kind of resource (`buffer`, `texture`, `drawable`, `drawable-internal`) gets a storage mode and memory type: + +``` +[MetalDevice] pid=… memory heap=0 size=6144MiB deviceLocal=yes +[MetalDevice] pid=… memory type=0 flags=0x0(none) heap=1 +[MetalDevice] pid=… memory alloc kind=texture storage=private typeBits=0x… type=1 flags=0x1(DEVICE_LOCAL) heap=0 heapDeviceLocal=yes firstFit=0 flags=0x0(none) heap=1 heapDeviceLocal=no changed=yes +``` + +Resources only the GPU uses (`private`, `memoryless`, and `framebufferOnly` drawables) take the first allowed `DEVICE_LOCAL` type. Before 2026-10-16 they took the first allowed type (`firstFit`), which on NVIDIA can be system RAM; `changed=yes` marks where the two differ. `shared` and `managed` resources keep the first host-visible type. On the RX 6600 (RADV) type 0 is `DEVICE_LOCAL`, so nothing changes there. + +The first import of a drawable into the presenting OpenGL context, and the first semaphore import, are logged too; later ones only when they fail (at most five): + +``` +[MetalDevice] pid=… drawable GL import (first) dedicated=yes size=… 1920x1080 memoryType=1 tiling=linear glTiling=linear dedicated_error=0x0 import_error=0x0 setup_error=0x0 storage_error=0x0 query_error=0x0 ok +[MetalDevice] pid=… drawable GL semaphore (first) import_error=0x0 wait_error=0x0 ok (checked for the first 600 imports) +``` + +A non-zero error or `FAILED` means the frames cannot reach the window. Before 2026-10-16 the import did not mark the memory as dedicated, which the extension requires; Mesa ignores it, and it is the suspected cause of the NVIDIA black screen (not yet confirmed). If the game shows a black screen on NVIDIA, report it with this file. + ### Host-side checks These are host settings, not game settings. Run on AC power, and use a performance power profile (`powerprofilesctl set performance`, or `asusctl profile -P Performance` on ASUS laptops). On the tested desktop the game is limited mostly by its own main thread on the CPU, not by the GPU (see [status](STATUS.md#game-fps-2026-10-1314)), so laptop CPU power limits matter as much as the GPU choice. diff --git a/patches/cocotron.patch b/patches/cocotron.patch index 2f391a7..7212cc4 100644 --- a/patches/cocotron.patch +++ b/patches/cocotron.patch @@ -7105,13 +7105,14 @@ index 5ad375f8..c8e83814 100644 +- (BOOL) _treeHasAnimations; @end diff --git a/QuartzCore/CAMetalDrawable.mm b/QuartzCore/CAMetalDrawable.mm -index d6f86b31..4d08ce26 100644 +index d6f86b31..2bdc198d 100644 --- a/QuartzCore/CAMetalDrawable.mm +++ b/QuartzCore/CAMetalDrawable.mm -@@ -31,6 +31,12 @@ +@@ -31,6 +31,13 @@ #include #include ++#include +#include +#include +#include @@ -7121,7 +7122,7 @@ index d6f86b31..4d08ce26 100644 namespace DynamicVK = Indium::DynamicVK; static void reportGLErrors(void) { -@@ -50,8 +56,86 @@ static void reportGLErrors(void) { +@@ -50,8 +57,98 @@ static void reportGLErrors(void) { namespace Indium::DynamicVK { static DynamicFunction vkGetSemaphoreFdKHR("vkGetSemaphoreFdKHR"); static DynamicFunction vkGetMemoryFdKHR("vkGetMemoryFdKHR"); @@ -7138,6 +7139,21 @@ index d6f86b31..4d08ce26 100644 +// silently break. Record which GL renderer actually presents and whether its +// device UUID matches the Vulkan device's, once per process, in +// /tmp/metal-device.log (alongside the device list Indium logs at startup). ++// Clears pending GL errors (bounded: without a current context glGetError may never ++// report GL_NO_ERROR). ++static void drainGLErrors() { ++ for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) { ++ } ++} ++ ++// Same line format as Indium's device log. ++static void appendDeviceLog(const std::string& line) { ++ if (FILE* log = fopen("/tmp/metal-device.log", "a")) { ++ fprintf(log, "[MetalDevice] pid=%d %s\n", getpid(), line.c_str()); ++ fclose(log); ++ } ++} ++ +static std::string darlingUuidString(const GLubyte* uuid) { + static const char* digits = "0123456789abcdef"; + std::string out; @@ -7198,17 +7214,104 @@ index d6f86b31..4d08ce26 100644 + line += " match=unknown"; + } + -+ if (FILE* log = fopen("/tmp/metal-device.log", "a")) { -+ fprintf(log, "[MetalDevice] pid=%d %s\n", getpid(), line.c_str()); -+ fclose(log); -+ } ++ appendDeviceLog(line); + }); +} + // // helper classes // -@@ -161,6 +245,11 @@ CAMetalDrawableTexture::CAMetalDrawableTexture(CGSize size, Indium::PixelFormat +@@ -129,13 +226,39 @@ public: + // texture + // + +-static size_t findSharedMemory(const VkMemoryRequirements& reqs, std::shared_ptr device, bool framebufferOnly) { ++static std::string memoryFlagsString(VkMemoryPropertyFlags flags) { ++ char buffer[16]; ++ snprintf(buffer, sizeof(buffer), "0x%x(", (unsigned) flags); ++ std::string out = buffer; ++ if (flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) out += "DEVICE_LOCAL|"; ++ if (flags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) out += "HOST_VISIBLE|"; ++ if (flags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) out += "HOST_COHERENT|"; ++ if (flags & VK_MEMORY_PROPERTY_HOST_CACHED_BIT) out += "HOST_CACHED|"; ++ if (flags & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT) out += "LAZILY_ALLOCATED|"; ++ if (flags & VK_MEMORY_PROPERTY_PROTECTED_BIT) out += "PROTECTED|"; ++ if (flags & VK_MEMORY_PROPERTY_DEVICE_COHERENT_BIT_AMD) out += "DEVICE_COHERENT_AMD|"; ++ if (flags & VK_MEMORY_PROPERTY_DEVICE_UNCACHED_BIT_AMD) out += "DEVICE_UNCACHED_AMD|"; ++ if (out.back() == '|') out.back() = ')'; ++ else out += "none)"; ++ return out; ++} ++ ++// Vulkan orders memory types so a flag-less type (system RAM, which NVIDIA ++// exposes) precedes every DEVICE_LOCAL type, and first fit put framebufferOnly ++// drawables there. Both images are only touched by the GPU then -- the internal ++// one is imported by the presenting OpenGL context, which runs on the same GPU ++// (see reportGpuDeviceMatch) -- so they take the first allowed DEVICE_LOCAL type, ++// falling back to first fit. Without framebufferOnly they still need host-visible, ++// coherent memory and keep first fit. On RADV type 0 is DEVICE_LOCAL: unchanged. ++static size_t findSharedMemory(const VkMemoryRequirements& reqs, std::shared_ptr device, bool framebufferOnly, bool internal) { ++ const auto& memory = device->memoryProperties(); ++ size_t firstFit = SIZE_MAX; + size_t targetIndex = SIZE_MAX; + +- for (size_t i = 0; i < device->memoryProperties().memoryTypeCount; ++i) { +- const auto& type = device->memoryProperties().memoryTypes[i]; ++ for (size_t i = 0; i < memory.memoryTypeCount; ++i) { ++ const auto& type = memory.memoryTypes[i]; + +- if ((reqs.memoryTypeBits & (1 << i)) == 0) { ++ if ((reqs.memoryTypeBits & (1u << i)) == 0) { + continue; + } + +@@ -147,9 +270,42 @@ static size_t findSharedMemory(const VkMemoryRequirements& reqs, std::shared_ptr + continue; + } + +- // okay, this is good enough +- targetIndex = i; +- break; ++ if (firstFit == SIZE_MAX) { ++ firstFit = i; ++ } ++ ++ if (!framebufferOnly || (type.propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0) { ++ targetIndex = i; ++ break; ++ } ++ } ++ ++ if (targetIndex == SIZE_MAX) { ++ targetIndex = firstFit; ++ } ++ if (targetIndex == SIZE_MAX) { ++ return targetIndex; ++ } ++ ++ // Once per (image, framebufferOnly, type) in the process, in Indium's device log. ++ static std::atomic logged[2] {}; ++ const size_t bit = ((internal ? 2u : 0u) + (framebufferOnly ? 1u : 0u)) * 32 + targetIndex; ++ const uint64_t mask = uint64_t(1) << (bit & 63); ++ if ((logged[bit >> 6].load(std::memory_order_relaxed) & mask) == 0 && (logged[bit >> 6].fetch_or(mask) & mask) == 0) { ++ auto describe = [&](size_t index) { ++ const auto& type = memory.memoryTypes[index]; ++ return std::to_string(index) + " flags=" + memoryFlagsString(type.propertyFlags) ++ + " heap=" + std::to_string(type.heapIndex) ++ + " heapDeviceLocal=" + ((memory.memoryHeaps[type.heapIndex].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) ? "yes" : "no"); ++ }; ++ char typeBits[16]; ++ snprintf(typeBits, sizeof(typeBits), "0x%08x", (unsigned) reqs.memoryTypeBits); ++ appendDeviceLog(std::string("memory alloc kind=") + (internal ? "drawable-internal" : "drawable") ++ + " storage=" + (framebufferOnly ? "framebufferOnly" : "hostVisible") ++ + " typeBits=" + typeBits ++ + " type=" + describe(targetIndex) ++ + " firstFit=" + describe(firstFit) ++ + (targetIndex != firstFit ? " changed=yes" : " changed=no")); + } + + return targetIndex; +@@ -161,6 +317,11 @@ CAMetalDrawableTexture::CAMetalDrawableTexture(CGSize size, Indium::PixelFormat _pixelFormat(pixelFormat), _framebufferOnly(framebufferOnly) { @@ -7220,18 +7323,93 @@ index d6f86b31..4d08ce26 100644 // // create the images // -@@ -401,6 +490,10 @@ CAMetalDrawableTexture::CAMetalDrawableTexture(CGSize size, Indium::PixelFormat +@@ -219,7 +380,7 @@ CAMetalDrawableTexture::CAMetalDrawableTexture(CGSize size, Indium::PixelFormat + + DynamicVK::vkGetImageMemoryRequirements(_device->device(), _image, &reqs); + +- size_t targetIndex = findSharedMemory(reqs, _device, _framebufferOnly); ++ size_t targetIndex = findSharedMemory(reqs, _device, _framebufferOnly, false); + + if (targetIndex == SIZE_MAX) { + throw std::runtime_error("No suitable memory region found for image"); +@@ -242,7 +403,7 @@ CAMetalDrawableTexture::CAMetalDrawableTexture(CGSize size, Indium::PixelFormat + + DynamicVK::vkGetImageMemoryRequirements(_device->device(), _internalImage, &reqs); + +- targetIndex = findSharedMemory(reqs, _device, _framebufferOnly); ++ targetIndex = findSharedMemory(reqs, _device, _framebufferOnly, true); + + if (targetIndex == SIZE_MAX) { + throw std::runtime_error("No suitable memory region found for internal image"); +@@ -380,16 +541,26 @@ CAMetalDrawableTexture::CAMetalDrawableTexture(CGSize size, Indium::PixelFormat + abort(); + } + +- reportGLErrors(); ++ // reportGLErrors() is compiled out, so a failed import used to be silent (a black ++ // window). Clear errors left by earlier calls; the import is checked below. ++ drainGLErrors(); + + // import the memory into OpenGL + glCreateMemoryObjectsEXT(1, &_memoryObject); + reportGLErrors(); + ++ // _internalMemory is a dedicated allocation (VkMemoryDedicatedAllocateInfo above), ++ // and EXT_external_objects requires DEDICATED_MEMORY_OBJECT_EXT to be TRUE when the ++ // memory was created as one. Mesa ignores the flag; NVIDIA enforces it. It has to be ++ // set before the import, after which the memory object is immutable. ++ GLint dedicated = GL_TRUE; ++ glMemoryObjectParameterivEXT(_memoryObject, GL_DEDICATED_MEMORY_OBJECT_EXT, &dedicated); ++ const GLenum dedicatedError = glGetError(); ++ + // this transfers ownership of the FD to OpenGL + glImportMemoryFdEXT(_memoryObject, reqs.size, GL_HANDLE_TYPE_OPAQUE_FD_EXT, fd); + fd = -1; +- reportGLErrors(); ++ const GLenum importError = glGetError(); + + GLint prevTex = 0; + glGetIntegerv(GL_TEXTURE_BINDING_2D, &prevTex); +@@ -401,10 +572,36 @@ CAMetalDrawableTexture::CAMetalDrawableTexture(CGSize size, Indium::PixelFormat glTextureParameteri(_textureID, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTextureParameteri(_textureID, GL_TEXTURE_WRAP_S, GL_REPEAT); glTextureParameteri(_textureID, GL_TEXTURE_WRAP_T, GL_REPEAT); +- reportGLErrors(); + // A Metal layer is opaque: macOS ignores the drawable's alpha channel. The game's blended effects lower destination + // alpha as a side effect, and the layer compositor (CARenderer: blending plus an alpha test against 0, over a + // cleared-to-black window) then showed those regions as black quads. Always present alpha = 1. + glTextureParameteri(_textureID, GL_TEXTURE_SWIZZLE_A, GL_ONE); - reportGLErrors(); ++ const GLenum setupError = glGetError(); glTextureStorageMem2DEXT(_textureID, 1, GL_RGBA8, _size.width, _size.height, _memoryObject, 0); -@@ -605,35 +698,16 @@ void CAMetalDrawableActual::present() { +- reportGLErrors(); ++ const GLenum storageError = glGetError(); ++ ++ // The first import in the process is always logged, so a working run on a new ++ // driver shows up too; after that only failed imports, at most five. ++ static std::atomic importCount {0}; ++ static std::atomic importFailures {0}; ++ const bool failed = dedicatedError != GL_NO_ERROR || importError != GL_NO_ERROR || setupError != GL_NO_ERROR || storageError != GL_NO_ERROR; ++ const bool first = importCount.fetch_add(1, std::memory_order_relaxed) == 0; ++ if (first || (failed && importFailures.fetch_add(1, std::memory_order_relaxed) < 5)) { ++ GLint dedicatedReadBack = -1; ++ GLint tiling = 0; ++ glGetMemoryObjectParameterivEXT(_memoryObject, GL_DEDICATED_MEMORY_OBJECT_EXT, &dedicatedReadBack); ++ glGetTextureParameteriv(_textureID, GL_TEXTURE_TILING_EXT, &tiling); ++ const GLenum queryError = glGetError(); ++ char line[512]; ++ snprintf(line, sizeof(line), "drawable GL import%s dedicated=%s size=%llu %dx%d memoryType=%zu tiling=linear glTiling=%s" ++ " dedicated_error=0x%x import_error=0x%x setup_error=0x%x storage_error=0x%x query_error=0x%x%s", ++ first ? " (first)" : "", dedicatedReadBack == GL_TRUE ? "yes" : (dedicatedReadBack == -1 ? "unknown" : "no"), ++ (unsigned long long) reqs.size, (int) _size.width, (int) _size.height, targetIndex, ++ tiling == GL_LINEAR_TILING_EXT ? "linear" : (tiling == GL_OPTIMAL_TILING_EXT ? "optimal" : "unknown"), ++ dedicatedError, importError, setupError, storageError, queryError, failed ? " FAILED" : " ok"); ++ appendDeviceLog(line); ++ } + + glBindTexture(GL_TEXTURE_2D, prevTex); + }; +@@ -605,35 +802,16 @@ void CAMetalDrawableActual::present() { return; } @@ -7242,16 +7420,16 @@ index d6f86b31..4d08ce26 100644 - if (_semaphore) { - int fd = -1; - VkSemaphoreGetFdInfoKHR info {}; +- +- info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR; +- info.semaphore = _semaphore->semaphore; +- info.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT; + // present() may run on the deferred encoding worker. The window's + // EGL context remains current on the main thread, so attempting to + // import a semaphore here fails and leaks the exported syncobj FD. + // Import it in synchronizeRender(), with the compositor context + // current, immediately before the GPU-side wait. -- info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR; -- info.semaphore = _semaphore->semaphore; -- info.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT; -- - if (DynamicVK::vkGetSemaphoreFdKHR(_semaphore->device->device(), &info, &fd) != VK_SUCCESS) { - // TODO - abort(); @@ -7273,7 +7451,7 @@ index d6f86b31..4d08ce26 100644 } } -@@ -702,9 +776,24 @@ void CAMetalDrawableActual::reset() { +@@ -702,9 +880,46 @@ void CAMetalDrawableActual::reset() { }; void CAMetalDrawableActual::synchronizeRender() { @@ -7281,6 +7459,11 @@ index d6f86b31..4d08ce26 100644 - GLenum layout = GL_LAYOUT_GENERAL_EXT; - glWaitSemaphoreEXT(_glSemaphore, 0, NULL, 1, &tex, &layout); + if (!_semaphore) return; ++ // GL errors of the import and wait are checked for the first imports of the ++ // process only: glGetError can stall a threaded GL driver, and this runs every frame. ++ static std::atomic checkedImports {0}; ++ bool check = false; ++ GLenum importError = GL_NO_ERROR; + if (_glSemaphore == 0) { + // prepareRender() has made the layer's GL context current on this + // thread. Never try to steal it from the submission worker. @@ -7292,12 +7475,29 @@ index d6f86b31..4d08ce26 100644 + info.semaphore = _semaphore->semaphore; + info.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT; + if (DynamicVK::vkGetSemaphoreFdKHR(_semaphore->device->device(), &info, &fd) != VK_SUCCESS) abort(); ++ check = checkedImports.fetch_add(1, std::memory_order_relaxed) < 600; ++ if (check) drainGLErrors(); + glImportSemaphoreFdEXT(_glSemaphore, GL_HANDLE_TYPE_OPAQUE_FD_EXT, fd); + // A successful import transfers the native FD to the GL driver. ++ if (check) importError = glGetError(); + } + GLuint tex = _texture->glTexture(); + GLenum layout = GL_LAYOUT_GENERAL_EXT; + glWaitSemaphoreEXT(_glSemaphore, 0, NULL, 1, &tex, &layout); ++ if (check) { ++ // One line on first use, then only failures (at most five). ++ const GLenum waitError = glGetError(); ++ const bool failed = importError != GL_NO_ERROR || waitError != GL_NO_ERROR; ++ static std::atomic loggedFirst {false}; ++ static std::atomic failures {0}; ++ const bool first = !loggedFirst.exchange(true, std::memory_order_relaxed); ++ if (first || (failed && failures.fetch_add(1, std::memory_order_relaxed) < 5)) { ++ char line[192]; ++ snprintf(line, sizeof(line), "drawable GL semaphore%s import_error=0x%x wait_error=0x%x%s (checked for the first 600 imports)", ++ first ? " (first)" : "", importError, waitError, failed ? " FAILED" : " ok"); ++ appendDeviceLog(line); ++ } ++ } }; static void glDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* context) { diff --git a/patches/metal--deps--indium.patch b/patches/metal--deps--indium.patch index 7d92c48..dcfff97 100644 --- a/patches/metal--deps--indium.patch +++ b/patches/metal--deps--indium.patch @@ -781,7 +781,7 @@ index 8546daa..7e4993c 100644 void updateBindings(); diff --git a/private-include/indium/device.private.hpp b/private-include/indium/device.private.hpp -index 58f1794..75cdeb3 100644 +index 58f1794..be7dbdc 100644 --- a/private-include/indium/device.private.hpp +++ b/private-include/indium/device.private.hpp @@ -13,6 +13,8 @@ @@ -836,7 +836,7 @@ index 58f1794..75cdeb3 100644 std::shared_ptr getWrappedTimelineSemaphore(); BinarySemaphore getBinarySemaphore(bool exportable = false); -@@ -111,5 +128,31 @@ namespace Indium { +@@ -111,5 +128,34 @@ namespace Indium { INDIUM_PROPERTY(VkPhysicalDeviceMemoryProperties, m, M,emoryProperties); INDIUM_PROPERTY_READONLY(Feature, f, F,eatures); @@ -859,6 +859,9 @@ index 58f1794..75cdeb3 100644 + void releaseDescriptorPool(VkDescriptorPool pool); + // Cached handles belong to the device and remain alive through all GPU uses. + std::pair acquireRenderPass(const VkRenderPassCreateInfo& info); ++ // Memory type index for a buffer or image of the given storage mode, or SIZE_MAX. Non-virtual and without ++ // state in the class, so the layout above is unchanged. ++ size_t findMemoryType(uint32_t memoryTypeBits, StorageMode storageMode, bool image); + private: + std::mutex _descriptorPoolMutex; + std::vector _availableDescriptorPools; @@ -1789,7 +1792,7 @@ index e58f0ba..616d177 100644 + finishTrackedEncoder(_privateCommandBuffer.lock()->commandBuffer()); }; diff --git a/src/indium/buffer.cpp b/src/indium/buffer.cpp -index 23fd2d3..e73df2a 100644 +index 23fd2d3..1a48b81 100644 --- a/src/indium/buffer.cpp +++ b/src/indium/buffer.cpp @@ -1,6 +1,8 @@ @@ -1801,7 +1804,34 @@ index 23fd2d3..e73df2a 100644 #include -@@ -69,23 +71,11 @@ Indium::PrivateBuffer::PrivateBuffer(std::shared_ptr device, size +@@ -43,49 +45,17 @@ Indium::PrivateBuffer::PrivateBuffer(std::shared_ptr device, size + + DynamicVK::vkGetBufferMemoryRequirements(_privateDevice->device(), _buffer, &requirements); + +- size_t targetIndex = SIZE_MAX; +- +- for (size_t i = 0; i < _privateDevice->memoryProperties().memoryTypeCount; ++i) { +- const auto& type = _privateDevice->memoryProperties().memoryTypes[i]; +- +- if ((requirements.memoryTypeBits & (1 << i)) == 0) { +- continue; +- } +- +- if ((_storageMode == StorageMode::Managed || _storageMode == StorageMode::Shared) && (type.propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0) { +- continue; +- } +- +- if (_storageMode == StorageMode::Shared && (type.propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) == 0) { +- continue; +- } +- +- // okay, this is good enough +- targetIndex = i; +- break; +- } ++ size_t targetIndex = _privateDevice->findMemoryType(requirements.memoryTypeBits, _storageMode, false); + + if (targetIndex == SIZE_MAX) { throw std::runtime_error("No suitable memory region found for buffer with requested storage mode"); } @@ -1829,7 +1859,7 @@ index 23fd2d3..e73df2a 100644 }; Indium::PrivateBuffer::PrivateBuffer(std::shared_ptr device, const void* pointer, size_t length, ResourceOptions options): -@@ -103,9 +93,11 @@ Indium::PrivateBuffer::PrivateBuffer(std::shared_ptr device, cons +@@ -103,9 +73,11 @@ Indium::PrivateBuffer::PrivateBuffer(std::shared_ptr device, cons if (_storageMode == StorageMode::Managed) { VkMappedMemoryRange range {}; range.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE; @@ -1843,7 +1873,7 @@ index 23fd2d3..e73df2a 100644 if (DynamicVK::vkFlushMappedMemoryRanges(_privateDevice->device(), 1, &range) != VK_SUCCESS) { // TODO abort(); -@@ -114,12 +106,10 @@ Indium::PrivateBuffer::PrivateBuffer(std::shared_ptr device, cons +@@ -114,12 +86,10 @@ Indium::PrivateBuffer::PrivateBuffer(std::shared_ptr device, cons }; Indium::PrivateBuffer::~PrivateBuffer() { @@ -1859,7 +1889,7 @@ index 23fd2d3..e73df2a 100644 }; std::shared_ptr Indium::PrivateBuffer::device() { -@@ -136,7 +126,8 @@ void* Indium::PrivateBuffer::contents() { +@@ -136,7 +106,8 @@ void* Indium::PrivateBuffer::contents() { } if (!_mapped) { @@ -1869,7 +1899,7 @@ index 23fd2d3..e73df2a 100644 // TODO abort(); } -@@ -148,9 +139,16 @@ void* Indium::PrivateBuffer::contents() { +@@ -148,9 +119,16 @@ void* Indium::PrivateBuffer::contents() { void Indium::PrivateBuffer::didModifyRange(Range range) { VkMappedMemoryRange vulkanRange {}; vulkanRange.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE; @@ -1888,7 +1918,7 @@ index 23fd2d3..e73df2a 100644 if (DynamicVK::vkFlushMappedMemoryRanges(_privateDevice->device(), 1, &vulkanRange) != VK_SUCCESS) { // TODO abort(); -@@ -158,8 +156,10 @@ void Indium::PrivateBuffer::didModifyRange(Range range) { +@@ -158,8 +136,10 @@ void Indium::PrivateBuffer::didModifyRange(Range range) { }; uint64_t Indium::PrivateBuffer::gpuAddress() { @@ -2803,7 +2833,7 @@ index 5f62ad0..5957bec 100644 DynamicVK::vkCmdBindDescriptorSets(buf->commandBuffer(), VK_PIPELINE_BIND_POINT_COMPUTE, _pso->layout(), 0, descriptorSets.size(), descriptorSets.data(), 0, nullptr); }; diff --git a/src/indium/device.cpp b/src/indium/device.cpp -index 322e186..2003684 100644 +index 322e186..78b77ae 100644 --- a/src/indium/device.cpp +++ b/src/indium/device.cpp @@ -1,4 +1,6 @@ @@ -2813,7 +2843,7 @@ index 322e186..2003684 100644 #include #include #include -@@ -12,39 +14,167 @@ +@@ -12,39 +14,207 @@ #include @@ -2875,6 +2905,46 @@ index 322e186..2003684 100644 + } + } + ++ std::string memoryFlagsString(VkMemoryPropertyFlags flags) { ++ static const std::pair names[] = { ++ { VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, "DEVICE_LOCAL" }, ++ { VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, "HOST_VISIBLE" }, ++ { VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, "HOST_COHERENT" }, ++ { VK_MEMORY_PROPERTY_HOST_CACHED_BIT, "HOST_CACHED" }, ++ { VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT, "LAZILY_ALLOCATED" }, ++ { VK_MEMORY_PROPERTY_PROTECTED_BIT, "PROTECTED" }, ++ { VK_MEMORY_PROPERTY_DEVICE_COHERENT_BIT_AMD, "DEVICE_COHERENT_AMD" }, ++ { VK_MEMORY_PROPERTY_DEVICE_UNCACHED_BIT_AMD, "DEVICE_UNCACHED_AMD" }, ++ }; ++ std::string out = "0x" + hexValue(flags, 1) + "("; ++ bool first = true; ++ VkMemoryPropertyFlags rest = flags; ++ for (const auto& name: names) { ++ if ((flags & name.first) == 0) continue; ++ out += (first ? "" : "|") + std::string(name.second); ++ first = false; ++ rest &= ~name.first; ++ } ++ if (rest) out += (first ? "0x" : "|0x") + hexValue(rest, 1); ++ else if (first) out += "none"; ++ return out + ")"; ++ } ++ ++ // The memory types and heaps of the device Metal renders on, logged once when it ++ // is selected, so the per-allocation lines below can be read without vulkaninfo. ++ void appendMemoryTable(const VkPhysicalDeviceMemoryProperties& memory) { ++ for (uint32_t i = 0; i < memory.memoryHeapCount; ++i) { ++ const auto& heap = memory.memoryHeaps[i]; ++ appendDeviceLog("memory heap=" + std::to_string(i) + " size=" + std::to_string(heap.size >> 20) + "MiB" ++ + " deviceLocal=" + ((heap.flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) ? "yes" : "no")); ++ } ++ for (uint32_t i = 0; i < memory.memoryTypeCount; ++i) { ++ const auto& type = memory.memoryTypes[i]; ++ appendDeviceLog("memory type=" + std::to_string(i) + " flags=" + memoryFlagsString(type.propertyFlags) ++ + " heap=" + std::to_string(type.heapIndex)); ++ } ++ } ++ + struct GpuSelector { + std::string value; + std::string source; // empty when the loader's default order should decide @@ -2987,7 +3057,7 @@ index 322e186..2003684 100644 continue; } -@@ -67,12 +197,101 @@ void Indium::initGlobalDeviceList() { +@@ -67,12 +237,105 @@ void Indium::initGlobalDeviceList() { DynamicVK::vkGetPhysicalDeviceFeatures2(device, &features); if (!features12.timelineSemaphore) { @@ -3004,7 +3074,7 @@ index 322e186..2003684 100644 + memcpy(candidate.deviceUUID, idProperties.deviceUUID, 16); + memcpy(candidate.driverUUID, idProperties.driverUUID, 16); + candidates.push_back(candidate); - } ++ } + + // Resolve the explicit selection against the supported list. An explicit choice + // is honoured even when it names a software (CPU) device, because the user asked @@ -3061,6 +3131,10 @@ index 322e186..2003684 100644 + + " deviceUUID=" + uuidToString(selected.deviceUUID) + + " reason=" + (selectedReason.empty() ? std::string("loader-first") : selectedReason)); + ++ VkPhysicalDeviceMemoryProperties selectedMemory {}; ++ DynamicVK::vkGetPhysicalDeviceMemoryProperties(selected.device, &selectedMemory); ++ appendMemoryTable(selectedMemory); ++ + // A VkDevice is created for the selected device only: its driver is initialized + // and, on a hybrid laptop, the discrete GPU is not woken for a device the + // rendering GPU cannot share memory with anyway. The old behaviour -- a VkDevice @@ -3083,7 +3157,7 @@ index 322e186..2003684 100644 + + for (size_t index: creationOrder) { + globalDeviceList.push_back(std::make_shared(candidates[index].device)); -+ } + } + + appendDeviceLog("created VkDevice for " + std::to_string(globalDeviceList.size()) + " of " + + std::to_string(candidates.size()) + " supported device(s)" @@ -3091,7 +3165,7 @@ index 322e186..2003684 100644 }; void Indium::finitGlobalDeviceList() { -@@ -83,7 +302,8 @@ Indium::Device::~Device() {}; +@@ -83,7 +346,8 @@ Indium::Device::~Device() {}; Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): _physicalDevice(physicalDevice) @@ -3101,7 +3175,7 @@ index 322e186..2003684 100644 DynamicVK::vkGetPhysicalDeviceProperties(_physicalDevice, &_properties); std::vector queueFamilies; -@@ -233,6 +453,36 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): +@@ -233,6 +497,36 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): } } @@ -3138,7 +3212,7 @@ index 322e186..2003684 100644 VkDeviceCreateInfo deviceCreateInfo {}; deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; deviceCreateInfo.pQueueCreateInfos = queueCreateInfos.data(); -@@ -246,6 +496,7 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): +@@ -246,6 +540,7 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): } _features = indiumFeatures; @@ -3146,7 +3220,7 @@ index 322e186..2003684 100644 for (const auto& index: queueFamilyIndices) { VkQueue queue; -@@ -297,6 +548,9 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): +@@ -297,6 +592,9 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice): }; Indium::PrivateDevice::~PrivateDevice() { @@ -3156,7 +3230,7 @@ index 322e186..2003684 100644 if (_oneshotCommandPool) { DynamicVK::vkDestroyCommandPool(_device, _oneshotCommandPool, nullptr); } -@@ -308,6 +562,23 @@ std::string Indium::PrivateDevice::name() const { +@@ -308,6 +606,23 @@ std::string Indium::PrivateDevice::name() const { return _properties.deviceName; }; @@ -3180,7 +3254,7 @@ index 322e186..2003684 100644 std::shared_ptr Indium::PrivateDevice::newCommandQueue() { return std::make_shared(shared_from_this()); }; -@@ -317,9 +588,9 @@ std::shared_ptr Indium::PrivateDevice::newRenderPip +@@ -317,9 +632,9 @@ std::shared_ptr Indium::PrivateDevice::newRenderPip }; std::shared_ptr Indium::PrivateDevice::newComputePipelineState(const ComputePipelineDescriptor& descriptor, PipelineOption options, std::shared_ptr reflection) { @@ -3193,7 +3267,7 @@ index 322e186..2003684 100644 return std::make_shared(shared_from_this(), descriptor); }; -@@ -357,6 +628,7 @@ std::shared_ptr Indium::PrivateDevice::newLibrary(const void* d +@@ -357,6 +672,7 @@ std::shared_ptr Indium::PrivateDevice::newLibrary(const void* d break; } @@ -3201,7 +3275,7 @@ index 322e186..2003684 100644 funcInfo.bindings.insert(funcInfo.bindings.end(), info.bindings.begin(), info.bindings.end()); funcInfo.embeddedSamplers.insert(funcInfo.embeddedSamplers.end(), info.embeddedSamplers.begin(), info.embeddedSamplers.end()); } -@@ -480,9 +752,18 @@ void Indium::PrivateDevice::waitForSemaphore(VkSemaphore semaphore, uint64_t tar +@@ -480,9 +796,18 @@ void Indium::PrivateDevice::waitForSemaphore(VkSemaphore semaphore, uint64_t tar wakeupEventLoop(); }; @@ -3221,7 +3295,7 @@ index 322e186..2003684 100644 VkSemaphoreTypeCreateInfo typeInfo {}; typeInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO; typeInfo.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE; -@@ -505,7 +786,14 @@ Indium::TimelineSemaphore Indium::PrivateDevice::getTimelineSemaphore() { +@@ -505,7 +830,14 @@ Indium::TimelineSemaphore Indium::PrivateDevice::getTimelineSemaphore() { }; void Indium::PrivateDevice::putTimelineSemaphore(const TimelineSemaphore& semaphore) { @@ -3237,7 +3311,7 @@ index 322e186..2003684 100644 }; std::shared_ptr Indium::PrivateDevice::getWrappedTimelineSemaphore() { -@@ -551,3 +839,91 @@ std::shared_ptr Indium::PrivateDevice::getWrappedBinary +@@ -551,3 +883,160 @@ std::shared_ptr Indium::PrivateDevice::getWrappedBinary delete ptr; }); }; @@ -3329,6 +3403,75 @@ index 322e186..2003684 100644 + } + DynamicVK::vkDestroyDescriptorPool(_device, pool, nullptr); +} ++ ++// The Vulkan spec orders memory types so that a type whose flags are a subset of ++// another's comes first. A flag-less type (system RAM the GPU reads over PCIe, ++// which NVIDIA exposes) therefore precedes every DEVICE_LOCAL type, and the first ++// type allowed by memoryTypeBits put resources only the GPU touches in system RAM ++// there. Private and Memoryless resources now take the first allowed DEVICE_LOCAL ++// type and fall back to that first fit. Shared and Managed keep the first fit ++// that is host visible (and coherent for Shared): moving them to DEVICE_LOCAL and ++// HOST_VISIBLE BAR memory would make their CPU reads uncached. On RADV type 0 is ++// DEVICE_LOCAL, so nothing changes there. ++size_t Indium::PrivateDevice::findMemoryType(uint32_t memoryTypeBits, StorageMode storageMode, bool image) { ++ const auto& memory = _memoryProperties; ++ const bool gpuOnly = storageMode == StorageMode::Private || storageMode == StorageMode::Memoryless; ++ size_t firstFit = SIZE_MAX; ++ size_t deviceLocal = SIZE_MAX; ++ ++ for (size_t i = 0; i < memory.memoryTypeCount; ++i) { ++ const auto& type = memory.memoryTypes[i]; ++ ++ if ((memoryTypeBits & (1u << i)) == 0) { ++ continue; ++ } ++ ++ if ((storageMode == StorageMode::Managed || storageMode == StorageMode::Shared) && (type.propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0) { ++ continue; ++ } ++ ++ if (storageMode == StorageMode::Shared && (type.propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) == 0) { ++ continue; ++ } ++ ++ if (firstFit == SIZE_MAX) { ++ firstFit = i; ++ } ++ ++ if (!gpuOnly || (type.propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0) { ++ deviceLocal = i; ++ break; ++ } ++ } ++ ++ const size_t chosen = deviceLocal != SIZE_MAX ? deviceLocal : firstFit; ++ if (chosen == SIZE_MAX) { ++ return chosen; ++ } ++ ++ // Logged the first time each (kind, storage mode, type) occurs in the process; ++ // afterwards this is one relaxed load. 2 kinds x 4 modes x 32 types = 256 bits. ++ static std::atomic logged[4] {}; ++ const size_t bit = ((image ? 4u : 0u) + (static_cast(storageMode) & 3)) * 32 + chosen; ++ const uint64_t mask = uint64_t(1) << (bit & 63); ++ if ((logged[bit >> 6].load(std::memory_order_relaxed) & mask) == 0 && (logged[bit >> 6].fetch_or(mask) & mask) == 0) { ++ static const char* modeNames[] = { "shared", "managed", "private", "memoryless" }; ++ auto describe = [&](size_t index) { ++ const auto& type = memory.memoryTypes[index]; ++ return std::to_string(index) + " flags=" + memoryFlagsString(type.propertyFlags) ++ + " heap=" + std::to_string(type.heapIndex) ++ + " heapDeviceLocal=" + ((memory.memoryHeaps[type.heapIndex].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) ? "yes" : "no"); ++ }; ++ appendDeviceLog(std::string("memory alloc kind=") + (image ? "texture" : "buffer") ++ + " storage=" + modeNames[static_cast(storageMode) & 3] ++ + " typeBits=0x" + hexValue(memoryTypeBits, 8) ++ + " type=" + describe(chosen) ++ + " firstFit=" + describe(firstFit) ++ + (chosen != firstFit ? " changed=yes" : " changed=no")); ++ } ++ ++ return chosen; ++}; diff --git a/src/indium/dynamic-vk.cpp b/src/indium/dynamic-vk.cpp index a4677c3..69ef23f 100644 --- a/src/indium/dynamic-vk.cpp @@ -4499,7 +4642,7 @@ index 71392df..d82e5f0 100644 info.unnormalizedCoordinates = descriptor.normalizedCoordinates ? VK_FALSE : VK_TRUE; diff --git a/src/indium/texture.cpp b/src/indium/texture.cpp -index 1e3e971..4db87d4 100644 +index 1e3e971..cbdd7ff 100644 --- a/src/indium/texture.cpp +++ b/src/indium/texture.cpp @@ -1,8 +1,90 @@ @@ -4706,7 +4849,34 @@ index 1e3e971..4db87d4 100644 info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; // TODO: check if this should be "concurrent" instead. i think we're good, though. info.queueFamilyIndexCount = 0; info.pQueueFamilyIndices = nullptr; -@@ -373,19 +495,18 @@ Indium::ConcreteTexture::ConcreteTexture(std::shared_ptr device, +@@ -347,45 +469,24 @@ Indium::ConcreteTexture::ConcreteTexture(std::shared_ptr device, + + DynamicVK::vkGetImageMemoryRequirements(_device->device(), _image, &reqs); + +- size_t targetIndex = SIZE_MAX; +- +- for (size_t i = 0; i < _device->memoryProperties().memoryTypeCount; ++i) { +- const auto& type = _device->memoryProperties().memoryTypes[i]; +- +- if ((reqs.memoryTypeBits & (1 << i)) == 0) { +- continue; +- } +- +- if ((_storageMode == StorageMode::Managed || _storageMode == StorageMode::Shared) && (type.propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0) { +- continue; +- } +- +- if (_storageMode == StorageMode::Shared && (type.propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) == 0) { +- continue; +- } +- +- // okay, this is good enough +- targetIndex = i; +- break; +- } ++ size_t targetIndex = _device->findMemoryType(reqs.memoryTypeBits, _storageMode, true); + + if (targetIndex == SIZE_MAX) { throw std::runtime_error("No suitable memory region found for image with requested storage mode"); } @@ -4714,15 +4884,15 @@ index 1e3e971..4db87d4 100644 - allocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; - allocateInfo.allocationSize = reqs.size; - allocateInfo.memoryTypeIndex = targetIndex; -- -- if (DynamicVK::vkAllocateMemory(_device->device(), &allocateInfo, nullptr, &_memory) != VK_SUCCESS) { -- // TODO -- abort(); -- } + Indium::count("textureAlloc", reqs.size); + auto allocation = MemoryPool::allocate(_device->device(), reinterpret_cast(_image), targetIndex, reqs.size, reqs.alignment, true, false); + _memory = allocation.memory; +- if (DynamicVK::vkAllocateMemory(_device->device(), &allocateInfo, nullptr, &_memory) != VK_SUCCESS) { +- // TODO +- abort(); +- } +- - DynamicVK::vkBindImageMemory(_device->device(), _image, _memory, 0); + DynamicVK::vkBindImageMemory(_device->device(), _image, _memory, allocation.offset); @@ -4735,7 +4905,7 @@ index 1e3e971..4db87d4 100644 VkCommandBufferAllocateInfo cmdBufAllocInfo {}; cmdBufAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; cmdBufAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; -@@ -409,7 +530,7 @@ Indium::ConcreteTexture::ConcreteTexture(std::shared_ptr device, +@@ -409,7 +510,7 @@ Indium::ConcreteTexture::ConcreteTexture(std::shared_ptr device, barrier.srcAccessMask = VK_ACCESS_NONE; barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; @@ -4744,7 +4914,7 @@ index 1e3e971..4db87d4 100644 barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.image = _image; -@@ -438,15 +559,13 @@ Indium::ConcreteTexture::ConcreteTexture(std::shared_ptr device, +@@ -438,15 +539,13 @@ Indium::ConcreteTexture::ConcreteTexture(std::shared_ptr device, submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &cmdBuf; @@ -4766,7 +4936,7 @@ index 1e3e971..4db87d4 100644 VkImageViewCreateInfo info2 {}; info2.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; -@@ -463,12 +582,24 @@ Indium::ConcreteTexture::ConcreteTexture(std::shared_ptr device, +@@ -463,12 +562,24 @@ Indium::ConcreteTexture::ConcreteTexture(std::shared_ptr device, // TODO abort(); } @@ -4792,7 +4962,7 @@ index 1e3e971..4db87d4 100644 }; Indium::TextureType Indium::ConcreteTexture::textureType() const { -@@ -537,7 +668,20 @@ VkImage Indium::ConcreteTexture::image() { +@@ -537,7 +648,20 @@ VkImage Indium::ConcreteTexture::image() { return _image; }; @@ -4813,7 +4983,7 @@ index 1e3e971..4db87d4 100644 return VK_IMAGE_LAYOUT_GENERAL; }; -@@ -546,6 +690,16 @@ void Indium::ConcreteTexture::replaceRegion(Indium::Region region, size_t mipmap +@@ -546,6 +670,16 @@ void Indium::ConcreteTexture::replaceRegion(Indium::Region region, size_t mipmap }; void Indium::ConcreteTexture::replaceRegion(Indium::Region region, size_t mipmapLevel, size_t slice, const void* bytes, size_t bytesPerRow, size_t bytesPerImage) { @@ -4830,7 +5000,7 @@ index 1e3e971..4db87d4 100644 if (_storageMode != StorageMode::Managed && _storageMode != StorageMode::Shared) { throw std::runtime_error("Invalid usage of replaceRegion on non-managed and non-shared texture"); } -@@ -556,99 +710,109 @@ void Indium::ConcreteTexture::replaceRegion(Indium::Region region, size_t mipmap +@@ -556,99 +690,109 @@ void Indium::ConcreteTexture::replaceRegion(Indium::Region region, size_t mipmap auto aspect = pixelFormatToVkImageAspectFlags(_descriptor.pixelFormat);