#include "vulkan_window.h" #include "host_util.h" #include #include #include #include #include #include #include #include namespace mt_host { VulkanWindow::VulkanWindow(bool vsync, int init_width, int init_height) : vsync_(vsync) { #ifdef __APPLE__ if (!std::getenv("VK_ICD_FILENAMES")) { for (const char* icd : { "/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json", "/usr/local/etc/vulkan/icd.d/MoltenVK_icd.json"}) { if (std::ifstream(icd).good()) { setenv("VK_ICD_FILENAMES", icd, 0); break; } } } #endif // Android's back key reaches the game as SDL_SCANCODE_AC_BACK (-> ESC) instead of finishing the activity. SDL_SetHint(SDL_HINT_ANDROID_TRAP_BACK_BUTTON, "1"); if (!SDL_Init(SDL_INIT_VIDEO)) throw std::runtime_error(SDL_GetError()); SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight"); window_ = SDL_CreateWindow( "Metin2 Native Vulkan Client", std::max(init_width, 320), std::max(init_height, 240), SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY); if (!window_) throw std::runtime_error(SDL_GetError()); SDL_StartTextInput(window_); Uint32 extension_count = 0; const char* const* sdl_extensions = SDL_Vulkan_GetInstanceExtensions(&extension_count); if (!sdl_extensions) throw std::runtime_error(SDL_GetError()); std::vector extensions(sdl_extensions, sdl_extensions + extension_count); std::uint32_t available_count = 0; check(vkEnumerateInstanceExtensionProperties(nullptr, &available_count, nullptr), "vkEnumerateInstanceExtensionProperties count"); std::vector available(available_count); check(vkEnumerateInstanceExtensionProperties(nullptr, &available_count, available.data()), "vkEnumerateInstanceExtensionProperties"); const bool portability = std::any_of(available.begin(), available.end(), [](const auto& extension) { return std::strcmp(extension.extensionName, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME) == 0; }); if (portability && std::find_if(extensions.begin(), extensions.end(), [](const char* name) { return std::strcmp(name, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME) == 0; }) == extensions.end()) extensions.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO}; app.pApplicationName = "Metin2 native renderer"; app.apiVersion = VK_API_VERSION_1_1; VkInstanceCreateInfo instance_info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; instance_info.flags = portability ? VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR : 0; instance_info.pApplicationInfo = &app; instance_info.enabledExtensionCount = static_cast(extensions.size()); instance_info.ppEnabledExtensionNames = extensions.data(); check(vkCreateInstance(&instance_info, nullptr, &instance_), "vkCreateInstance"); if (!SDL_Vulkan_CreateSurface(window_, instance_, nullptr, &surface_)) throw std::runtime_error(SDL_GetError()); select_device(); VkCommandPoolCreateInfo pool_info{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool_info.queueFamilyIndex = queue_family_; check(vkCreateCommandPool(device_, &pool_info, nullptr, &pool_), "vkCreateCommandPool"); VkQueryPoolCreateInfo query_info{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO}; query_info.queryType = VK_QUERY_TYPE_TIMESTAMP; query_info.queryCount = 2 * kMaxFramesInFlight; check(vkCreateQueryPool(device_, &query_info, nullptr, &query_pool_), "vkCreateQueryPool"); VkSemaphoreCreateInfo semaphore_info{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; VkFenceCreateInfo fence_info{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; fence_info.flags = VK_FENCE_CREATE_SIGNALED_BIT; for (std::uint32_t i = 0; i < kMaxFramesInFlight; ++i) { auto& slot = frames_[i]; VkCommandBufferAllocateInfo alloc{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; alloc.commandPool = pool_; alloc.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; alloc.commandBufferCount = 1; check(vkAllocateCommandBuffers(device_, &alloc, &slot.command), "vkAllocateCommandBuffers"); check(vkCreateSemaphore(device_, &semaphore_info, nullptr, &slot.acquire), "vkCreateSemaphore acquire"); check(vkCreateFence(device_, &fence_info, nullptr, &slot.fence), "vkCreateFence"); slot.query_base = 2 * i; } create_swapchain(); create_descriptors_and_buffers(); create_render_pass_and_layout(); touch_controller.update_screen_size(int(width()), int(height())); } VulkanWindow::~VulkanWindow() { if (hardware_cursor_enabled_) { SDL_SetCursor(nullptr); for (SDL_Cursor* cursor : game_cursors_) if (cursor) SDL_DestroyCursor(cursor); if (fallback_cursor_) SDL_DestroyCursor(fallback_cursor_); } if (device_) vkDeviceWaitIdle(device_); for (auto& slot : frames_) { release_retired_buffers(slot); if (slot.staging_mapped) vkUnmapMemory(device_, slot.staging_memory); if (slot.staging_buffer) vkDestroyBuffer(device_, slot.staging_buffer, nullptr); if (slot.staging_memory) vkFreeMemory(device_, slot.staging_memory, nullptr); if (slot.bone_mapped) vkUnmapMemory(device_, slot.bone_memory); if (slot.bone_buffer) vkDestroyBuffer(device_, slot.bone_buffer, nullptr); if (slot.bone_memory) vkFreeMemory(device_, slot.bone_memory, nullptr); if (slot.state_mapped) vkUnmapMemory(device_, slot.state_memory); if (slot.state_buffer) vkDestroyBuffer(device_, slot.state_buffer, nullptr); if (slot.state_memory) vkFreeMemory(device_, slot.state_memory, nullptr); if (slot.ui_mapped) vkUnmapMemory(device_, slot.ui_memory); if (slot.ui_buffer) vkDestroyBuffer(device_, slot.ui_buffer, nullptr); if (slot.ui_memory) vkFreeMemory(device_, slot.ui_memory, nullptr); if (slot.fence) vkDestroyFence(device_, slot.fence, nullptr); if (slot.acquire) vkDestroySemaphore(device_, slot.acquire, nullptr); } if (query_pool_) vkDestroyQueryPool(device_, query_pool_, nullptr); for (VkSemaphore semaphore : rendered_) vkDestroySemaphore(device_, semaphore, nullptr); for (auto& entry : geometries_) release_geometry(entry.second); for (auto& entry : textures_) release_texture(entry.second); for (auto& entry : render_targets_) release_render_target(entry.second); if (offscreen_pass_) vkDestroyRenderPass(device_, offscreen_pass_, nullptr); release_texture(fallback_texture_); for (auto& entry : pipelines_) vkDestroyPipeline(device_, entry.second, nullptr); if (vertex_module_) vkDestroyShaderModule(device_, vertex_module_, nullptr); if (fragment_module_) vkDestroyShaderModule(device_, fragment_module_, nullptr); if (layout_) vkDestroyPipelineLayout(device_, layout_, nullptr); if (descriptor_pool_) vkDestroyDescriptorPool(device_, descriptor_pool_, nullptr); if (descriptor_layout_) vkDestroyDescriptorSetLayout(device_, descriptor_layout_, nullptr); if (bone_descriptor_layout_) vkDestroyDescriptorSetLayout(device_, bone_descriptor_layout_, nullptr); if (sampler_) vkDestroySampler(device_, sampler_, nullptr); if (clamp_sampler_) vkDestroySampler(device_, clamp_sampler_, nullptr); if (ui_sampler_) vkDestroySampler(device_, ui_sampler_, nullptr); for (const auto& entry : state_samplers_) vkDestroySampler(device_, entry.second, nullptr); for (auto framebuffer : framebuffers_) vkDestroyFramebuffer(device_, framebuffer, nullptr); if (pass_) vkDestroyRenderPass(device_, pass_, nullptr); if (depth_view_) vkDestroyImageView(device_, depth_view_, nullptr); if (depth_image_) vkDestroyImage(device_, depth_image_, nullptr); if (depth_memory_) vkFreeMemory(device_, depth_memory_, nullptr); destroy_msaa_color(); for (auto view : image_views_) vkDestroyImageView(device_, view, nullptr); if (swapchain_) vkDestroySwapchainKHR(device_, swapchain_, nullptr); if (pool_) vkDestroyCommandPool(device_, pool_, nullptr); if (device_) vkDestroyDevice(device_, nullptr); if (surface_) vkDestroySurfaceKHR(instance_, surface_, nullptr); if (instance_) vkDestroyInstance(instance_, nullptr); if (window_) { SDL_StopTextInput(window_); SDL_DestroyWindow(window_); } SDL_Quit(); } void VulkanWindow::reset_timings() { finish_gpu_timings(); timings_ = {}; timed_frames_ = 0; upload_count_ = 0; uploaded_bytes_ = 0; texture_upload_count_ = 0; texture_uploaded_bytes_ = 0; } void VulkanWindow::finish_gpu_timings() { for (auto& slot : frames_) { if (!slot.has_pending_query) continue; check(vkWaitForFences(device_, 1, &slot.fence, VK_TRUE, UINT64_MAX), "vkWaitForFences finish"); collect_pending_gpu_timestamp(slot); } } void VulkanWindow::set_frames_in_flight(std::uint32_t count) { frames_in_flight_ = std::clamp(count, 1, kMaxFramesInFlight); } const char* VulkanWindow::present_mode_name() const { switch (present_mode_) { case VK_PRESENT_MODE_IMMEDIATE_KHR: return "IMMEDIATE"; case VK_PRESENT_MODE_MAILBOX_KHR: return "MAILBOX"; default: return "FIFO"; } } native_perf::RendererTotals VulkanWindow::perf_totals() const { native_perf::RendererTotals t; t.sync_ms = timings_.sync_ms; t.fence_ms = timings_.fence_ms; t.prepare_ms = timings_.prepare_ms; t.geometry_upload_ms = timings_.geometry_upload_ms; t.texture_upload_ms = timings_.texture_upload_ms; t.submit_ms = timings_.submit_ms; t.present_ms = timings_.present_ms; t.gpu_ms = timings_.gpu_ms; t.gpu_samples = timings_.gpu_samples; t.uploads = upload_count_; t.uploaded_bytes = uploaded_bytes_; t.texture_uploads = texture_upload_count_; t.texture_bytes = texture_uploaded_bytes_; t.draws = last_draw_count_; t.skinned_draws = last_skinned_draw_count_; t.ui_batches = last_ui_batch_count_; t.vertices = last_vertex_count_; t.last_texture = &last_texture_upload_name_; return t; } void VulkanWindow::collect_pending_gpu_timestamp(FrameSlot& slot) { if (!slot.has_pending_query) return; slot.has_pending_query = false; std::uint64_t timestamps[2] = {}; const VkResult res = vkGetQueryPoolResults( device_, query_pool_, slot.query_base, 2, sizeof(timestamps), timestamps, sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT); if (res == VK_SUCCESS && timestamps[1] >= timestamps[0]) { const double ns = double(timestamps[1] - timestamps[0]) * double(timestamp_period_ns_); timings_.gpu_ms += ns * 1e-6; ++timings_.gpu_samples; } } void print_summary(const VulkanWindow& renderer, int completed, double wall_ms, double update_ms, std::vector frame_ms) { const auto timing = renderer.timings(); std::sort(frame_ms.begin(), frame_ms.end()); const auto percentile = [&](double fraction) { if (frame_ms.empty()) return 0.0; const auto index = static_cast(std::ceil(fraction * frame_ms.size())) - 1; return frame_ms[std::min(index, frame_ms.size() - 1)]; }; std::cout << "device=" << renderer.device_name() << " present_mode=" << renderer.present_mode_name() << " msaa=" << renderer.msaa_samples() << " frames=" << completed << " draws=" << renderer.draw_count() << " skinned_draws=" << renderer.skinned_draw_count() << " ui_batches=" << renderer.ui_batch_count() << " ui_quads=" << renderer.ui_quad_count() << " vertices=" << renderer.vertex_count() << " indices=" << renderer.index_count() << " uploads=" << renderer.upload_count() << " uploaded_bytes=" << renderer.uploaded_bytes() << " texture_uploads=" << renderer.texture_upload_count() << " texture_bytes=" << renderer.texture_uploaded_bytes() << " mean_frame_ms=" << (completed ? wall_ms / completed : 0.0) << " p95_frame_ms=" << percentile(0.95) << " p99_frame_ms=" << percentile(0.99) << " max_frame_ms=" << percentile(1.0) << " game_update_ms=" << (completed ? update_ms / completed : 0.0) << " sync_ms=" << (completed ? timing.sync_ms / completed : 0.0) << " prepare_ms=" << (completed ? timing.prepare_ms / completed : 0.0) << " steady_prepare_ms=" << (completed > 1 ? timing.steady_prepare_ms / (completed - 1) : 0.0) << " submit_ms=" << (completed ? timing.submit_ms / completed : 0.0) << " present_ms=" << (completed ? timing.present_ms / completed : 0.0) << " gpu_ms=" << (timing.gpu_samples ? timing.gpu_ms / double(timing.gpu_samples) : 0.0) << '\n'; } } // namespace mt_host