VulkanWindow's 3000-line TU becomes sdl_window.cpp (window, events, cursor, screenshot BMP), vk_device.cpp (device, swapchain, MSAA, framebuffers), vk_pipelines.cpp (render passes, pipelines), vk_resources.cpp (buffers, render targets, geometry), vk_textures.cpp (textures, samplers, descriptors), vk_frame.cpp (render) and ui_batches.cpp; vulkan_window.cpp keeps construction and timing stats. Function bodies move verbatim. render() now reads fence/acquire -> prepare_draws -> prepare_ui -> write_draw_states -> record_passes -> record_readbacks -> submit -> finish_readbacks -> present, with FrameDraws / Readback carrying the locals that used to live across 500 lines. Verified: login-screen screenshot byte-identical to HEAD; game-world draw/vertex/index/UI counts identical; ctest, port_gate macos+android, Android build pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
257 lines
13 KiB
C++
257 lines
13 KiB
C++
#include "vulkan_window.h"
|
|
|
|
#include "host_util.h"
|
|
|
|
#include <SDL3/SDL_vulkan.h>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
|
|
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<const char*> extensions(sdl_extensions, sdl_extensions + extension_count);
|
|
std::uint32_t available_count = 0;
|
|
check(vkEnumerateInstanceExtensionProperties(nullptr, &available_count, nullptr), "vkEnumerateInstanceExtensionProperties count");
|
|
std::vector<VkExtensionProperties> 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<std::uint32_t>(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<std::uint32_t>(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<double> 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::size_t>(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
|