/****************************************************************************** * The MIT License (MIT) * * Copyright (c) 2019-2025 Baldur Karlsson * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. ******************************************************************************/ #include #include "3rdparty/fmt/core.h" #include "vk_test.h" #if defined(SSE_TEST) #include #endif struct Alloc { VkDevice device; std::string name; VkDeviceMemory mem; VkMemoryPropertyFlags flags; VkBuffer buf; uint32_t type; VkDeviceSize size; byte *data = NULL; void map() { vkMapMemory(device, mem, 0, VK_WHOLE_SIZE, 0, (void **)&data); } void unmap() { vkUnmapMemory(device, mem); } }; byte *refData = NULL; size_t dummyStart, dummyEnd; namespace FindDiffRange_shipping { #if 0 static __m128 zero = {0}; #endif // assumes a and b both point to 16-byte aligned 16-byte chunks of memory. // Returns if they're equal or different bool Vec16NotEqual(void *a, void *b) { // disabled SSE version as it's acting dodgy #if 0 __m128 avec = _mm_load_ps(aflt); __m128 bvec = _mm_load_ps(bflt); __m128 diff = _mm_xor_ps(avec, bvec); __m128 eq = _mm_cmpeq_ps(diff, zero); int mask = _mm_movemask_ps(eq); int signMask = _mm_movemask_ps(diff); // first check ensures that diff is floatequal to zero (ie. avec bitwise equal to bvec). // HOWEVER -0 is floatequal to 0, so we ensure no sign bits are set on diff if((mask^0xf) || signMask != 0) { return true; } return false; #elif defined(__LP64__) || defined(_WIN64) || defined(__x86_64__) || defined(_M_X64) || \ defined(__ia64) || defined(_M_IA64) || defined(__aarch64__) || defined(__powerpc64__) || \ (defined(__riscv) && __riscv_xlen == 64) uint64_t *a64 = (uint64_t *)a; uint64_t *b64 = (uint64_t *)b; return a64[0] != b64[0] || a64[1] != b64[1]; #else uint32_t *a32 = (uint32_t *)a; uint32_t *b32 = (uint32_t *)b; return a32[0] != b32[0] || a32[1] != b32[1] || a32[2] != b32[2] || a32[3] != b32[3]; #endif } bool FindDiffRange(void *a, void *b, size_t bufSize, size_t &diffStart, size_t &diffEnd) { TEST_ASSERT(uintptr_t(a) % 16 == 0, "misaligned"); TEST_ASSERT(uintptr_t(b) % 16 == 0, "misaligned"); diffStart = bufSize + 1; diffEnd = 0; size_t alignedSize = bufSize & (~0xf); size_t numVecs = alignedSize / 16; size_t offs = 0; float *aflt = (float *)a; float *bflt = (float *)b; // sweep to find the start of differences for(size_t v = 0; v < numVecs; v++) { if(Vec16NotEqual(aflt, bflt)) { diffStart = offs; break; } aflt += 4; bflt += 4; offs += 4 * sizeof(float); } // make sure we're byte-accurate, to comply with WRITE_NO_OVERWRITE while(diffStart < bufSize && *((byte *)a + diffStart) == *((byte *)b + diffStart)) diffStart++; // do we have some unaligned bytes at the end of the buffer? if(bufSize > alignedSize) { size_t numBytes = bufSize - alignedSize; // if we haven't even found a start, check in these bytes if(diffStart > bufSize) { offs = alignedSize; for(size_t by = 0; by < numBytes; by++) { if(*((byte *)a + alignedSize + by) != *((byte *)b + alignedSize + by)) { diffStart = offs; break; } offs++; } } // sweep from the last byte to find the end for(size_t by = 0; by < numBytes; by++) { if(*((byte *)a + bufSize - 1 - by) != *((byte *)b + bufSize - 1 - by)) { diffEnd = bufSize - by; break; } } } // if we haven't found a start, or we've found a start AND and end, // then we're done. if(diffStart > bufSize || diffEnd > 0) return diffStart < bufSize; offs = alignedSize; // sweep from the last __m128 aflt = (float *)a + offs / sizeof(float) - 4; bflt = (float *)b + offs / sizeof(float) - 4; for(size_t v = 0; v < numVecs; v++) { if(Vec16NotEqual(aflt, bflt)) { diffEnd = offs; break; } aflt -= 4; bflt -= 4; offs -= 16; } // make sure we're byte-accurate, to comply with WRITE_NO_OVERWRITE while(diffEnd > 0 && *((byte *)a + diffEnd - 1) == *((byte *)b + diffEnd - 1)) diffEnd--; // if we found a start then we necessarily found an end return diffStart < bufSize; } }; void stream_memcpy(void *dst, void *src, size_t len) { char *d = (char *)dst; char *s = (char *)src; #if defined(SSE_TEST) /* If dst and src are not co-aligned, or if SSE4.1 is not present, fallback to memcpy(). */ if(((uintptr_t)d & 15) != ((uintptr_t)s & 15)) { memcpy(d, s, len); return; } /* memcpy() the misaligned header. At the end of this if block, and * are aligned to a 16-byte boundary or == 0. */ if((uintptr_t)d & 15) { uintptr_t bytes_before_alignment_boundary = 16 - ((uintptr_t)d & 15); TEST_ASSERT(bytes_before_alignment_boundary < 16, "!"); memcpy(d, s, std::min(bytes_before_alignment_boundary, len)); d = (char *)AlignUp((uintptr_t)d, (uintptr_t)16ULL); s = (char *)AlignUp((uintptr_t)s, (uintptr_t)16ULL); len -= std::min(bytes_before_alignment_boundary, len); } if(len >= 64) _mm_mfence(); while(len >= 64) { __m128i *dst_cacheline = (__m128i *)d; __m128i *src_cacheline = (__m128i *)s; __m128i temp1 = _mm_stream_load_si128(src_cacheline + 0); __m128i temp2 = _mm_stream_load_si128(src_cacheline + 1); __m128i temp3 = _mm_stream_load_si128(src_cacheline + 2); __m128i temp4 = _mm_stream_load_si128(src_cacheline + 3); _mm_store_si128(dst_cacheline + 0, temp1); _mm_store_si128(dst_cacheline + 1, temp2); _mm_store_si128(dst_cacheline + 2, temp3); _mm_store_si128(dst_cacheline + 3, temp4); d += 64; s += 64; len -= 64; } #endif /* memcpy() the tail. */ if(len) { memcpy(d, s, len); } } RD_TEST(VK_Mem_Bench, VulkanGraphicsTest) { static constexpr const char *Description = "Memory mapping benchmark"; bool bench = true; VkDeviceSize maxMemory = 500 * 1024 * 1024; uint32_t submits = 20; void Prepare(int argc, char **argv) { for(int i = 0; i < argc; i++) { if(!strcmp(argv[i], "--bench")) { bench = true; } if(!strcmp(argv[i], "--maxmem") && i + 1 < argc) { maxMemory = atoi(argv[i + 1]) * 1024 * 1024; } if(!strcmp(argv[i], "--submits") && i + 1 < argc) { submits = atoi(argv[i + 1]); } } forceComputeQueue = true; forceTransferQueue = true; VulkanGraphicsTest::Prepare(argc, argv); } int main() { // initialise, create window, create context, etc if(!Init()) return 3; VkPipelineLayout layout = createPipelineLayout(vkh::PipelineLayoutCreateInfo()); vkh::GraphicsPipelineCreateInfo pipeCreateInfo; pipeCreateInfo.layout = layout; pipeCreateInfo.renderPass = mainWindow->rp; pipeCreateInfo.vertexInputState.vertexBindingDescriptions = {vkh::vertexBind(0, DefaultA2V)}; pipeCreateInfo.vertexInputState.vertexAttributeDescriptions = { vkh::vertexAttr(0, 0, DefaultA2V, pos), vkh::vertexAttr(1, 0, DefaultA2V, col), vkh::vertexAttr(2, 0, DefaultA2V, uv), }; pipeCreateInfo.stages = { CompileShaderModule(VKDefaultVertex, ShaderLang::glsl, ShaderStage::vert, "main"), CompileShaderModule(VKDefaultPixel, ShaderLang::glsl, ShaderStage::frag, "main"), }; AllocatedImage img( this, vkh::ImageCreateInfo(mainWindow->scissor.extent.width, mainWindow->scissor.extent.height, 0, mainWindow->format, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT), VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_GPU_ONLY})); VkImageView imgview = createImageView( vkh::ImageViewCreateInfo(img.image, VK_IMAGE_VIEW_TYPE_2D, mainWindow->format)); vkh::RenderPassCreator renderPassCreateInfo; renderPassCreateInfo.attachments.push_back( vkh::AttachmentDescription(mainWindow->format, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, VK_ATTACHMENT_LOAD_OP_CLEAR)); renderPassCreateInfo.addSubpass({VkAttachmentReference({0, VK_IMAGE_LAYOUT_GENERAL})}); VkRenderPass renderPass = createRenderPass(renderPassCreateInfo); VkFramebuffer framebuffer = createFramebuffer( vkh::FramebufferCreateInfo(renderPass, {imgview}, mainWindow->scissor.extent)); pipeCreateInfo.renderPass = renderPass; VkPipeline pipe = createGraphicsPipeline(pipeCreateInfo); AllocatedBuffer vb( this, vkh::BufferCreateInfo(sizeof(DefaultTri) + 128 * 1024, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT), VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_CPU_TO_GPU})); vb.upload(DefaultTri); const VkPhysicalDeviceMemoryProperties *props = NULL; vmaGetMemoryProperties(allocator, &props); std::vector allocs; VkDeviceSize refDataSize = 0; uint32_t heapTypeCount[16] = {}; for(uint32_t m = 0; m < props->memoryTypeCount; m++) { const VkMemoryType &type = props->memoryTypes[m]; if((type.propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0) { heapTypeCount[type.heapIndex]++; } } for(uint32_t m = 0; m < props->memoryTypeCount; m++) { const VkMemoryType &type = props->memoryTypes[m]; const VkMemoryHeap &heap = props->memoryHeaps[type.heapIndex]; if((type.propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0) { Alloc alloc; alloc.device = device; alloc.type = m; alloc.flags = type.propertyFlags; VkMemoryAllocateInfo info = {}; info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; info.allocationSize = AlignUp(std::min(maxMemory, ((heap.size * 7) / 10) / heapTypeCount[type.heapIndex]), (VkDeviceSize)256ULL); info.memoryTypeIndex = m; vkAllocateMemory(device, &info, NULL, &alloc.mem); vkCreateBuffer( device, vkh::BufferCreateInfo(info.allocationSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT), NULL, &alloc.buf); vkBindBufferMemory(device, alloc.buf, alloc.mem, 0); alloc.size = info.allocationSize; if(bench && alloc.size > refDataSize) refDataSize = alloc.size; alloc.name = fmt::format("Mem {} ({:04}MB):", m, info.allocationSize >> 20); if((type.propertyFlags & VK_MEMORY_PROPERTY_HOST_CACHED_BIT) != 0) alloc.name += " CACHED"; if((type.propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) != 0) alloc.name += " COHERENT"; if((type.propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0) alloc.name += " DEVICE"; allocs.push_back(alloc); } } if(bench) refData = new byte[(uint32_t)refDataSize]; AllocatedBuffer readback; if(bench) readback = AllocatedBuffer(this, vkh::BufferCreateInfo(refDataSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT), VmaAllocationCreateInfo({0, VMA_MEMORY_USAGE_UNKNOWN, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT})); using ScanFunction = std::function; byte *scratch = new byte[65536]; std::vector scanners; auto blockScan = [scratch](VkDeviceSize blockSize, bool streamMemcpy, Alloc &, byte *data, VkDeviceSize size) { if(blockSize == 0) { FindDiffRange_shipping::FindDiffRange(data, refData, (size_t)size, dummyStart, dummyEnd); return; } for(VkDeviceSize i = 0; i < size; i += blockSize) { size_t chunkSize = (size_t)std::min(blockSize, size - i); if(streamMemcpy) stream_memcpy(scratch, data + i, chunkSize); else memcpy(scratch, data + i, chunkSize); FindDiffRange_shipping::FindDiffRange(scratch, refData, chunkSize, dummyStart, dummyEnd); } }; scanners.push_back([&blockScan](Alloc &a, byte *data, VkDeviceSize size) { blockScan(0, false, a, data, size); return "direct"; }); scanners.push_back([&blockScan](Alloc &a, byte *data, VkDeviceSize size) { blockScan(128, false, a, data, size); return "block_128"; }); scanners.push_back([&blockScan](Alloc &a, byte *data, VkDeviceSize size) { blockScan(1024, false, a, data, size); return "block_1024"; }); scanners.push_back([&blockScan](Alloc &a, byte *data, VkDeviceSize size) { blockScan(65536, false, a, data, size); return "block_65536"; }); scanners.push_back([&blockScan](Alloc &a, byte *data, VkDeviceSize size) { blockScan(128, true, a, data, size); return "block_128_stream"; }); scanners.push_back([&blockScan](Alloc &a, byte *data, VkDeviceSize size) { blockScan(1024, true, a, data, size); return "block_1024_stream"; }); scanners.push_back([&blockScan](Alloc &a, byte *data, VkDeviceSize size) { blockScan(65536, true, a, data, size); return "block_65536_stream"; }); auto gpuReadback = [this, &readback](VkQueue q, VkCommandBuffer cmd, Alloc &a) -> byte * { vkBeginCommandBuffer(cmd, vkh::CommandBufferBeginInfo()); VkBufferCopy region = {0, 0, a.size}; vkCmdCopyBuffer(cmd, a.buf, readback.buffer, 1, ®ion); vkh::cmdPipelineBarrier(cmd, {}, {vkh::BufferMemoryBarrier(VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT, readback.buffer)}); vkEndCommandBuffer(cmd); std::vector cmds = {cmd}; VkSubmitInfo submit = vkh::SubmitInfo(cmds); CHECK_VKR(vkQueueSubmit(q, 1, &submit, VK_NULL_HANDLE)); vkQueueWaitIdle(q); byte *ret = readback.map(); if((a.flags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) == 0) { VmaAllocationInfo info; vmaGetAllocationInfo(allocator, readback.alloc, &info); VkMappedMemoryRange range = { VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, NULL, info.deviceMemory, info.offset, info.size, }; vkInvalidateMappedMemoryRanges(device, 1, &range); } return ret; }; struct GPUReadbackFamily { uint32_t family; std::string name; VkQueue q; VkCommandPool pool; VkCommandBuffer cmd; }; GPUReadbackFamily readbackFamily[3] = { {queueFamilyIndex, "default"}, {computeQueueFamilyIndex, "compute"}, {transferQueueFamilyIndex, "transfer"}, }; for(size_t i = 0; i < ARRAY_COUNT(readbackFamily); i++) { GPUReadbackFamily &f = readbackFamily[i]; if(f.family == ~0U) continue; vkGetDeviceQueue(device, f.family, 0, &f.q); CHECK_VKR(vkCreateCommandPool( device, vkh::CommandPoolCreateInfo(VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, f.family), NULL, &f.pool)); CHECK_VKR(vkAllocateCommandBuffers(device, vkh::CommandBufferAllocateInfo(f.pool, 1), &f.cmd)); scanners.push_back( [this, &readback, f, &blockScan, &gpuReadback](Alloc &a, byte *, VkDeviceSize size) { vkResetCommandBuffer(f.cmd, 0); byte *data = gpuReadback(f.q, f.cmd, a); blockScan(0, false, a, data, size); readback.unmap(); return "gpu_" + f.name + "_direct"; }); scanners.push_back( [this, &readback, f, &blockScan, &gpuReadback](Alloc &a, byte *, VkDeviceSize size) { vkResetCommandBuffer(f.cmd, 0); byte *data = gpuReadback(f.q, f.cmd, a); blockScan(128, false, a, data, size); readback.unmap(); return "gpu_" + f.name + "_128"; }); scanners.push_back( [this, &readback, f, &blockScan, &gpuReadback](Alloc &a, byte *, VkDeviceSize size) { vkResetCommandBuffer(f.cmd, 0); byte *data = gpuReadback(f.q, f.cmd, a); blockScan(128, true, a, data, size); readback.unmap(); return "gpu_" + f.name + "_128_streaming"; }); scanners.push_back( [this, &readback, f, &blockScan, &gpuReadback](Alloc &a, byte *, VkDeviceSize size) { vkResetCommandBuffer(f.cmd, 0); byte *data = gpuReadback(f.q, f.cmd, a); blockScan(1024, false, a, data, size); readback.unmap(); return "gpu_" + f.name + "_1024"; }); scanners.push_back( [this, &readback, f, &blockScan, &gpuReadback](Alloc &a, byte *, VkDeviceSize size) { vkResetCommandBuffer(f.cmd, 0); byte *data = gpuReadback(f.q, f.cmd, a); blockScan(1024, true, a, data, size); readback.unmap(); return "gpu_" + f.name + "_1024_streaming"; }); } typedef std::chrono::high_resolution_clock Clock; typedef std::chrono::time_point Time; uint32_t seed = 0x31F10ca8; for(size_t i = 0; i < refDataSize; i++) { seed = (~seed) ^ (seed >> 5); refData[i] = seed & 0xff; } while(Running()) { for(Alloc &a : allocs) { a.map(); if(bench) { #if defined(WIN32) || defined(_WIN32) char OSName[] = "Windows"; #else char OSName[] = "Linux"; #endif TEST_LOG("-------- %s on %s", physProperties.deviceName, OSName); memcpy(a.data, refData, (size_t)a.size); } double bestSpeed = 0.0; std::string bestScannerName; for(size_t scan = 0; scan < scanners.size(); scan++) { Time prev = Clock::now(); std::string scannerName; int submitsCompleted = 0; for(uint32_t s = 0; s < submits; s++) { VkCommandBuffer cmd = GetCommandBuffer(); vkBeginCommandBuffer(cmd, vkh::CommandBufferBeginInfo()); VkBufferCopy region; region.size = 128; region.srcOffset = 0; region.dstOffset = 128; vkCmdCopyBuffer(cmd, a.buf, a.buf, 1, ®ion); vkCmdBeginRenderPass( cmd, vkh::RenderPassBeginInfo(renderPass, framebuffer, mainWindow->scissor, {vkh::ClearValue(0.2f, 0.2f, 0.2f, 1.0f)}), VK_SUBPASS_CONTENTS_INLINE); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe); vkCmdSetViewport(cmd, 0, 1, &mainWindow->viewport); vkCmdSetScissor(cmd, 0, 1, &mainWindow->scissor); vkh::cmdBindVertexBuffers(cmd, 0, {vb.buffer}, {0}); vkCmdDraw(cmd, 3, 1, 0, 0); vkCmdEndRenderPass(cmd); vkEndCommandBuffer(cmd); if(bench) scannerName = scanners[scan](a, a.data, a.size); Submit(99, 99, {cmd}); submitsCompleted++; if(bench) { Time cur = Clock::now(); double timeMS = double(std::chrono::duration_cast(cur - prev).count()) / 1000.0; if(timeMS > 10000) break; } } if(!bench) break; Time cur = Clock::now(); double timeMS = double(std::chrono::duration_cast(cur - prev).count()) / 1000.0; std::string data = a.name; data.resize(32, ' '); data += "scanned by "; data += scannerName; data.resize(70, ' '); double speed = double((submitsCompleted * a.size) >> 20) / double(timeMS / 1000.0); data += fmt::format("{:8.2f} MS for {} submits = {:8.2f} MB/s", timeMS, submitsCompleted, speed); TEST_LOG("%s", data.c_str()); if(speed > bestSpeed * 1.02) { bestScannerName = scannerName; bestSpeed = speed; } } if(bench) { TEST_LOG("--------"); TEST_LOG("%s's best scanner is %s", a.name.c_str(), bestScannerName.c_str()); TEST_LOG("--------"); } a.unmap(); } if(bench) TEST_LOG(""); { VkCommandBuffer cmd = GetCommandBuffer(); vkBeginCommandBuffer(cmd, vkh::CommandBufferBeginInfo()); VkImage swapimg = StartUsingBackbuffer(cmd, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_GENERAL); vkh::cmdPipelineBarrier( cmd, { vkh::ImageMemoryBarrier(VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, img.image), }); blitToSwap(cmd, img.image, VK_IMAGE_LAYOUT_GENERAL, swapimg, VK_IMAGE_LAYOUT_GENERAL); FinishUsingBackbuffer(cmd, VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_GENERAL); vkEndCommandBuffer(cmd); Submit(0, 1, {cmd}); } Present(); } for(size_t i = 0; i < ARRAY_COUNT(readbackFamily); i++) vkDestroyCommandPool(device, readbackFamily[i].pool, NULL); for(Alloc &a : allocs) { vkDestroyBuffer(device, a.buf, NULL); vkFreeMemory(device, a.mem, NULL); } return 0; } }; REGISTER_TEST();