// VulkanWindow: texture decode/upload, samplers per D3D8 sampler state, texture descriptor sets and eviction. #include "vulkan_window.h" #include "draw_capture.h" #include "host_util.h" #include "texture_decode.h" #include #include #include namespace mt_host { VkDescriptorSet VulkanWindow::allocate_texture_descriptor_set(VkImageView view0, VkImageView view1, VkSampler sampler0, VkSampler sampler1) { VkDescriptorSet set = VK_NULL_HANDLE; VkDescriptorSetAllocateInfo desc_alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; desc_alloc.descriptorPool = descriptor_pool_; desc_alloc.descriptorSetCount = 1; desc_alloc.pSetLayouts = &descriptor_layout_; check(vkAllocateDescriptorSets(device_, &desc_alloc, &set), "vkAllocateDescriptorSets"); VkDescriptorImageInfo image_infos[2]{}; image_infos[0].sampler = sampler0 ? sampler0 : sampler_; image_infos[0].imageView = view0; image_infos[0].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; image_infos[1].sampler = sampler1 ? sampler1 : (clamp_sampler_ ? clamp_sampler_ : sampler_); image_infos[1].imageView = view1; image_infos[1].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; VkWriteDescriptorSet writes[2]{}; for (int b = 0; b < 2; ++b) { writes[b].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[b].dstSet = set; writes[b].dstBinding = static_cast(b); writes[b].descriptorCount = 1; writes[b].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[b].pImageInfo = &image_infos[b]; } vkUpdateDescriptorSets(device_, 2, writes, 0, nullptr); return set; } VkDescriptorSet VulkanWindow::allocate_ui_texture_descriptor_set(VkImageView view0, VkImageView view1, bool point) { VkDescriptorSet set = VK_NULL_HANDLE; VkDescriptorSetAllocateInfo desc_alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; desc_alloc.descriptorPool = descriptor_pool_; desc_alloc.descriptorSetCount = 1; desc_alloc.pSetLayouts = &descriptor_layout_; check(vkAllocateDescriptorSets(device_, &desc_alloc, &set), "vkAllocateDescriptorSets ui"); VkDescriptorImageInfo image_infos[2]{}; image_infos[0].sampler = point ? ui_point_sampler_ : (ui_sampler_ ? ui_sampler_ : sampler_); image_infos[0].imageView = view0; image_infos[0].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; image_infos[1].sampler = point ? ui_point_sampler_ : (ui_sampler_ ? ui_sampler_ : sampler_); image_infos[1].imageView = view1; image_infos[1].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; VkWriteDescriptorSet writes[2]{}; for (int b = 0; b < 2; ++b) { writes[b].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[b].dstSet = set; writes[b].dstBinding = static_cast(b); writes[b].descriptorCount = 1; writes[b].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writes[b].pImageInfo = &image_infos[b]; } vkUpdateDescriptorSets(device_, 2, writes, 0, nullptr); return set; } VulkanWindow::GpuTexture& VulkanWindow::get_gpu_texture( const std::string& texture_name, const std::unordered_map>& capture_textures) { if (texture_name.empty()) return fallback_texture_; if (texture_name.rfind("rt:", 0) == 0) { RenderTarget* target = get_render_target(texture_name); return target ? target->texture : fallback_texture_; } auto [it, inserted] = textures_.try_emplace(texture_name); it->second.last_used_frame = frame_number_; if (!inserted && (it->second.view || frame_number_ - it->second.last_decode_attempt_frame < 60)) return it->second; if (inserted) { it->second.descriptor = fallback_texture_.descriptor; it->second.ui_descriptor = fallback_texture_.ui_descriptor; } it->second.last_decode_attempt_frame = frame_number_; mtimage::Image decoded; if (auto raw = capture_textures.find(texture_name); raw != capture_textures.end() && !raw->second.empty()) { decoded = decode_texture_bytes(raw->second.data(), raw->second.size()); } #ifdef MT_NATIVE_HAS_LIVE_CLIENT if (!decoded.ok()) { if (texture_name.rfind("mem:", 0) == 0) { UIMemoryTexture mem_tex; if (UIRenderMemoryTexture(texture_name, &mem_tex) && mem_tex.width > 0 && mem_tex.height > 0) { const auto mtra = native_draw_capture::encode_raw_argb_as_mtra( static_cast(mem_tex.width), static_cast(mem_tex.height), mem_tex.argb.data()); decoded = decode_texture_bytes(mtra.data(), mtra.size()); } } else { // PORT: the phone magnifies the 600-high 40250 UI about 1.8x, so the 1x window art and icons // look soft. A UI texture found in hd_ui.pak (scripts/build_hd_ui.py) is uploaded at 2x in // place of the pack file. The engine keeps the 1x size from its own decode for layout and // quads, and the UVs are normalized, so only the texel density changes. std::vector pack_bytes; if (read_hd_ui_texture(texture_name, pack_bytes)) decoded = decode_texture_bytes(pack_bytes.data(), pack_bytes.size()); if (!decoded.ok() && read_live_pack_texture(texture_name, pack_bytes)) decoded = decode_texture_bytes(pack_bytes.data(), pack_bytes.size()); } } #endif if (!decoded.ok()) return it->second; // 40250 CGraphicImageTexture: DXT DDS -> CreateDDSTexture with the file's levels; // other files -> D3DXCreateTextureFromFileInMemoryEx(D3DX_DEFAULT) = full chain; // memory images (mem:, CreateTexture(w, h, 1, ...)) -> one level. const bool is_memory_tex = texture_name.rfind("mem:", 0) == 0; last_texture_upload_name_ = texture_name; upload_texture( decoded.w, decoded.h, decoded.rgba.data(), it->second, true, !is_memory_tex && decoded.file_mip_levels == 0, &decoded.mips); it->second.descriptor = allocate_texture_descriptor_set(it->second.view, fallback_texture_.view); it->second.ui_descriptor = allocate_ui_texture_descriptor_set(it->second.view, fallback_texture_.view); return it->second; } std::uint64_t VulkanWindow::sampler_state_key(const Render3DDraw& draw, int stage) const { return (std::uint64_t(draw.address_u[stage] & 15u)) | (std::uint64_t(draw.address_v[stage] & 15u) << 4) | (std::uint64_t(draw.min_filter[stage] & 15u) << 8) | (std::uint64_t(draw.mag_filter[stage] & 15u) << 12) | (std::uint64_t(draw.mip_filter[stage] & 15u) << 16); } VkSampler VulkanWindow::sampler_for_draw(const Render3DDraw& draw, int stage) { const auto key = sampler_state_key(draw, stage); if (key == 0) return stage == 0 ? sampler_ : clamp_sampler_; if (auto it = state_samplers_.find(key); it != state_samplers_.end()) return it->second; auto address_mode = [](std::uint32_t mode) { switch (mode) { case 2: return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT; case 3: return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; case 4: return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; default: return VK_SAMPLER_ADDRESS_MODE_REPEAT; } }; VkSamplerCreateInfo info{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; info.addressModeU = address_mode(draw.address_u[stage]); info.addressModeV = address_mode(draw.address_v[stage]); info.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; info.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; info.minFilter = draw.min_filter[stage] == 1 ? VK_FILTER_NEAREST : VK_FILTER_LINEAR; info.magFilter = draw.mag_filter[stage] == 1 ? VK_FILTER_NEAREST : VK_FILTER_LINEAR; info.mipmapMode = draw.mip_filter[stage] == 1 ? VK_SAMPLER_MIPMAP_MODE_NEAREST : VK_SAMPLER_MIPMAP_MODE_LINEAR; info.minLod = 0.0f; info.maxLod = draw.mip_filter[stage] == 0 ? 0.0f : VK_LOD_CLAMP_NONE; if ((draw.min_filter[stage] == 3 || draw.mag_filter[stage] == 3) && max_anisotropy_ > 1.0f) { info.anisotropyEnable = VK_TRUE; info.maxAnisotropy = max_anisotropy_; } VkSampler sampler = VK_NULL_HANDLE; check(vkCreateSampler(device_, &info, nullptr, &sampler), "vkCreateSampler D3D state"); state_samplers_.emplace(key, sampler); return sampler; } VkDescriptorSet VulkanWindow::get_texture_descriptor( const std::string& texture0_name, const std::string& mask_name, const std::unordered_map>& capture_textures, const Render3DDraw& draw) { const auto& tex0 = get_gpu_texture(texture0_name, capture_textures); const auto& tex1 = mask_name.empty() ? fallback_texture_ : get_gpu_texture(mask_name, capture_textures); const std::string pair_key = "3d\n" + texture0_name + "\n" + mask_name + "\n" + std::to_string(sampler_state_key(draw, 0)) + "\n" + std::to_string(sampler_state_key(draw, 1)); if (auto it = paired_descriptors_.find(pair_key); it != paired_descriptors_.end()) { it->second.last_used_frame = frame_number_; return it->second.set; } const VkDescriptorSet set = allocate_texture_descriptor_set( tex0.view ? tex0.view : fallback_texture_.view, tex1.view ? tex1.view : fallback_texture_.view, sampler_for_draw(draw, 0), sampler_for_draw(draw, 1)); paired_descriptors_.emplace(pair_key, PairedDescriptor{set, frame_number_}); return set; } VkDescriptorSet VulkanWindow::get_ui_texture_descriptor( const std::string& texture0_name, const std::string& mask_name, const std::unordered_map>& capture_textures, bool point) { auto& tex0 = get_gpu_texture(texture0_name, capture_textures); if (point && mask_name.empty() && tex0.view) { if (!tex0.ui_point_descriptor) tex0.ui_point_descriptor = allocate_ui_texture_descriptor_set(tex0.view, fallback_texture_.view, true); return tex0.ui_point_descriptor; } if (mask_name.empty()) return tex0.ui_descriptor ? tex0.ui_descriptor : tex0.descriptor; const auto& tex1 = get_gpu_texture(mask_name, capture_textures); if (!tex0.view || !tex1.view) return tex0.ui_descriptor ? tex0.ui_descriptor : tex0.descriptor; const std::string pair_key = "ui\n" + texture0_name + "\n" + mask_name; if (auto it = paired_descriptors_.find(pair_key); it != paired_descriptors_.end()) { it->second.last_used_frame = frame_number_; return it->second.set; } const VkDescriptorSet set = allocate_ui_texture_descriptor_set( tex0.view ? tex0.view : fallback_texture_.view, tex1.view ? tex1.view : fallback_texture_.view); paired_descriptors_.emplace(pair_key, PairedDescriptor{set, frame_number_}); return set; } void VulkanWindow::upload_texture( std::uint32_t width, std::uint32_t height, const std::uint8_t* rgba, GpuTexture& texture, bool count_upload, bool generate_mips, const std::vector>* file_mips) { const ScopedMs timer(timings_.texture_upload_ms); texture.width = width; texture.height = height; const bool has_file_mips = !generate_mips && file_mips && !file_mips->empty(); VkDeviceSize byte_size = VkDeviceSize(width) * height * 4; if (has_file_mips) for (const auto& level : *file_mips) byte_size += level.size(); const std::uint32_t mip_levels = generate_mips ? (static_cast(std::floor(std::log2(std::max(width, height)))) + 1u) : has_file_mips ? 1u + static_cast(file_mips->size()) : 1u; // Inside a frame the copy is recorded ahead of that frame's render pass and reads the slot's // staging ring; the GPU runs it with the frame, no queue wait. Outside a frame (the fallback // texture at startup) it is a one-off submit that waits. const bool in_frame = f_->recording; VkBuffer staging_buffer = VK_NULL_HANDLE; VkDeviceMemory staging_memory = VK_NULL_HANDLE; VkDeviceSize staging_offset = 0; std::uint8_t* mapped = nullptr; const VkDeviceSize aligned_offset = (f_->staging_used + staging_alignment_ - 1) / staging_alignment_ * staging_alignment_; if (in_frame && f_->staging_mapped && aligned_offset + byte_size <= f_->staging_capacity) { staging_buffer = f_->staging_buffer; staging_offset = aligned_offset; mapped = f_->staging_mapped + staging_offset; f_->staging_used = aligned_offset + byte_size; } else { if (in_frame) f_->staging_wanted = std::max(f_->staging_wanted, aligned_offset + byte_size); void* raw = nullptr; create_host_buffer(byte_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, staging_buffer, staging_memory, &raw); mapped = static_cast(raw); } std::memcpy(mapped, rgba, VkDeviceSize(width) * height * 4); if (has_file_mips) { auto* dst = mapped + VkDeviceSize(width) * height * 4; for (const auto& level : *file_mips) { std::memcpy(dst, level.data(), level.size()); dst += level.size(); } } if (staging_memory) vkUnmapMemory(device_, staging_memory); VkImageCreateInfo image_info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; image_info.imageType = VK_IMAGE_TYPE_2D; image_info.format = VK_FORMAT_R8G8B8A8_UNORM; image_info.extent = {width, height, 1}; image_info.mipLevels = mip_levels; image_info.arrayLayers = 1; image_info.samples = VK_SAMPLE_COUNT_1_BIT; image_info.tiling = VK_IMAGE_TILING_OPTIMAL; image_info.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; check(vkCreateImage(device_, &image_info, nullptr, &texture.image), "vkCreateImage texture"); VkMemoryRequirements image_reqs{}; vkGetImageMemoryRequirements(device_, texture.image, &image_reqs); VkMemoryAllocateInfo image_alloc{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; image_alloc.allocationSize = image_reqs.size; image_alloc.memoryTypeIndex = find_memory_type(image_reqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); check(vkAllocateMemory(device_, &image_alloc, nullptr, &texture.memory), "vkAllocateMemory texture"); check(vkBindImageMemory(device_, texture.image, texture.memory, 0), "vkBindImageMemory texture"); VkCommandBuffer upload_cmd = f_->command; if (!in_frame) { VkCommandBufferAllocateInfo cmd_alloc{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; cmd_alloc.commandPool = pool_; cmd_alloc.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cmd_alloc.commandBufferCount = 1; check(vkAllocateCommandBuffers(device_, &cmd_alloc, &upload_cmd), "vkAllocateCommandBuffers texture"); VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; check(vkBeginCommandBuffer(upload_cmd, &begin), "vkBeginCommandBuffer texture"); } VkImageMemoryBarrier to_transfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; to_transfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; to_transfer.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; to_transfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; to_transfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; to_transfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; to_transfer.image = texture.image; to_transfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, mip_levels, 0, 1}; vkCmdPipelineBarrier( upload_cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_transfer); std::vector regions(has_file_mips ? mip_levels : 1u); VkDeviceSize region_offset = 0; for (std::uint32_t i = 0; i < regions.size(); ++i) { const std::uint32_t lw = std::max(1u, width >> i), lh = std::max(1u, height >> i); regions[i].bufferOffset = staging_offset + region_offset; regions[i].imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, i, 0, 1}; regions[i].imageExtent = {lw, lh, 1}; region_offset += VkDeviceSize(lw) * lh * 4; } vkCmdCopyBufferToImage( upload_cmd, staging_buffer, texture.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data()); std::int32_t mip_w = static_cast(width); std::int32_t mip_h = static_cast(height); for (std::uint32_t i = 1; generate_mips && i < mip_levels; ++i) { VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; barrier.image = texture.image; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, i - 1, 1, 0, 1}; barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; vkCmdPipelineBarrier( upload_cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); const std::int32_t next_w = std::max(1, mip_w / 2); const std::int32_t next_h = std::max(1, mip_h / 2); VkImageBlit blit{}; blit.srcOffsets[0] = {0, 0, 0}; blit.srcOffsets[1] = {mip_w, mip_h, 1}; blit.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, i - 1, 0, 1}; blit.dstOffsets[0] = {0, 0, 0}; blit.dstOffsets[1] = {next_w, next_h, 1}; blit.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, i, 0, 1}; vkCmdBlitImage( upload_cmd, texture.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, texture.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR); barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; vkCmdPipelineBarrier( upload_cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); mip_w = next_w; mip_h = next_h; } VkImageMemoryBarrier to_shader{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; to_shader.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; to_shader.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; to_shader.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; to_shader.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; to_shader.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; to_shader.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; to_shader.image = texture.image; to_shader.subresourceRange = generate_mips ? VkImageSubresourceRange{VK_IMAGE_ASPECT_COLOR_BIT, mip_levels - 1, 1, 0, 1} : VkImageSubresourceRange{VK_IMAGE_ASPECT_COLOR_BIT, 0, mip_levels, 0, 1}; vkCmdPipelineBarrier( upload_cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &to_shader); if (in_frame) { if (staging_memory) f_->retired_buffers.emplace_back(staging_buffer, staging_memory); } else { check(vkEndCommandBuffer(upload_cmd), "vkEndCommandBuffer texture"); VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; submit.commandBufferCount = 1; submit.pCommandBuffers = &upload_cmd; check(vkQueueSubmit(queue_, 1, &submit, VK_NULL_HANDLE), "vkQueueSubmit texture"); check(vkQueueWaitIdle(queue_), "vkQueueWaitIdle texture"); vkFreeCommandBuffers(device_, pool_, 1, &upload_cmd); vkDestroyBuffer(device_, staging_buffer, nullptr); vkFreeMemory(device_, staging_memory, nullptr); } VkImageViewCreateInfo view_info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; view_info.image = texture.image; view_info.viewType = VK_IMAGE_VIEW_TYPE_2D; view_info.format = VK_FORMAT_R8G8B8A8_UNORM; view_info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, mip_levels, 0, 1}; check(vkCreateImageView(device_, &view_info, nullptr, &texture.view), "vkCreateImageView texture"); if (count_upload) { ++texture_upload_count_; texture_uploaded_bytes_ += byte_size; } } void VulkanWindow::release_texture(GpuTexture& texture) { for (VkDescriptorSet set : {texture.descriptor, texture.ui_descriptor, texture.ui_point_descriptor}) if (set && set != fallback_texture_.descriptor && set != fallback_texture_.ui_descriptor) vkFreeDescriptorSets(device_, descriptor_pool_, 1, &set); if (texture.view) vkDestroyImageView(device_, texture.view, nullptr); if (texture.image) vkDestroyImage(device_, texture.image, nullptr); if (texture.memory) vkFreeMemory(device_, texture.memory, nullptr); texture = {}; } void VulkanWindow::prune_textures() { constexpr std::uint64_t static_retention_frames = 120; constexpr std::uint64_t memory_retention_frames = 2; for (auto it = paired_descriptors_.begin(); it != paired_descriptors_.end();) { const auto retention = it->first.find("mem:") != std::string::npos ? memory_retention_frames : static_retention_frames; if (frame_number_ - it->second.last_used_frame > retention) { vkFreeDescriptorSets(device_, descriptor_pool_, 1, &it->second.set); it = paired_descriptors_.erase(it); } else ++it; } for (auto it = textures_.begin(); it != textures_.end();) { const auto retention = it->first.rfind("mem:", 0) == 0 ? memory_retention_frames : static_retention_frames; if (frame_number_ - it->second.last_used_frame > retention) { release_texture(it->second); it = textures_.erase(it); } else ++it; } // PORT: render targets too (a closed model preview); a later use recreates and clears one. for (auto it = render_targets_.begin(); it != render_targets_.end();) { if (frame_number_ - it->second.last_used_frame > static_retention_frames) { for (auto pair = paired_descriptors_.begin(); pair != paired_descriptors_.end();) { if (pair->first.find("\n" + it->first + "\n") != std::string::npos) { vkFreeDescriptorSets(device_, descriptor_pool_, 1, &pair->second.set); pair = paired_descriptors_.erase(pair); } else ++pair; } release_render_target(it->second); it = render_targets_.erase(it); } else ++it; } } } // namespace mt_host