Files
mtgodot-poc/src/host/vulkan_window.cpp
T
shenleiandClaude Opus 5.5 22d534c5eb Split vulkan_window.cpp by responsibility and render() into phases
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>
2026-09-29 21:54:36 +09:00

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