Files
metin2-client/src/host/vulkan_window.h
T
shenleiandClaude Opus 5.5 75d2dd8545 host: split src/host/main.cpp into translation units
main.cpp (4.5k lines) -> main.cpp (entry, args, synthetic benchmark) plus
vulkan_window, live_client, frame_policy, native_audio, texture_decode,
render_state, input_keymap, host_util. Bodies are moved verbatim; multi-line
VulkanWindow / NativeAudioEngine / FrameRatePolicy members are defined out of
line; one-line accessors stay inline. Anonymous namespace -> mt_host.

No behavior change: ctest 15/15, port_gate macos+android PASS, desktop
--live-client login screen and synthetic --draws runs unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:30:44 +09:00

438 lines
19 KiB
C++

#pragma once
#include "RenderCommands3D.h"
#include "UIRenderCommands.h"
#include "perf_log.h"
#include "render_state.h"
#include "touch_controller.h"
#include <SDL3/SDL.h>
#include <vulkan/vulkan.h>
#include <array>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
namespace mt_host {
// The SDL3 window, Vulkan device and swapchain, and the renderer that replays a frame's recorded
// D3D8 draws (Render3DDraw) and UI commands.
class VulkanWindow {
struct FrameSlot;
struct GeometryId {
std::uint64_t key = 0, signature = 0;
bool operator==(const GeometryId&) const = default;
};
struct GeometryIdHash {
std::size_t operator()(GeometryId id) const {
return std::size_t(id.key ^ (id.signature + 0x9e3779b97f4a7c15ull + (id.key << 6) + (id.key >> 2)));
}
};
struct Geometry {
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkDeviceSize index_offset = 0;
std::uint64_t last_used_frame = 0;
std::uint32_t vertex_count = 0, index_count = 0;
};
struct GpuTexture {
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
VkDescriptorSet descriptor = VK_NULL_HANDLE;
VkDescriptorSet ui_descriptor = VK_NULL_HANDLE;
std::uint64_t last_used_frame = 0;
std::uint64_t last_decode_attempt_frame = 0;
};
struct PairedDescriptor {
VkDescriptorSet set = VK_NULL_HANDLE;
std::uint64_t last_used_frame = 0;
};
// An offscreen D3D render-target texture (the 40250 character shadow map, "rt:<id>:<w>x<h>"):
// drawn by the frame's offscreen pass ahead of the back-buffer pass and sampled by that pass.
// Between frames the colour image stays SHADER_READ_ONLY_OPTIMAL and the depth image
// DEPTH_STENCIL_ATTACHMENT_OPTIMAL; the render pass keeps (LOAD/STORE) both, like a D3D surface.
struct RenderTarget {
GpuTexture texture; // colour image, view and descriptors used for sampling
VkImage depth_image = VK_NULL_HANDLE;
VkDeviceMemory depth_memory = VK_NULL_HANDLE;
VkImageView depth_view = VK_NULL_HANDLE;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
std::uint32_t width = 0, height = 0;
bool initialized = false; // cleared to white / depth 1 by the first frame that uses it
};
struct PreparedDraw {
Geometry* geometry = nullptr;
VkPipeline pipeline = VK_NULL_HANDLE;
VkDescriptorSet descriptor = VK_NULL_HANDLE;
PushConstants constants{};
FixedFunctionState fixed_state{};
std::uint32_t state_offset = 0;
VkViewport viewport{};
// Offscreen render target of the draw, or null for the back buffer.
RenderTarget* target = nullptr;
// IDirect3DDevice8::Clear recorded in draw order; geometry is null for these.
std::uint32_t clear_flags = 0;
VkClearValue clear_color{};
VkClearValue clear_depth{};
VkRect2D clear_rect{};
};
struct UiBatch {
std::uint32_t first_index = 0;
std::uint32_t index_count = 0;
VkPipeline pipeline = VK_NULL_HANDLE;
VkDescriptorSet descriptor = VK_NULL_HANDLE;
PushConstants constants{};
bool behind_3d = false;
std::uint32_t state_offset = 0;
};
static constexpr std::size_t kMaxBonesPerFrame = 65536;
static constexpr VkDeviceSize kBoneBufferBytes = kMaxBonesPerFrame * 16 * sizeof(float);
static constexpr std::size_t kMaxUiVerticesPerFrame = 65536;
static constexpr std::size_t kMaxUiIndicesPerFrame = 98304;
static constexpr VkDeviceSize kUiVertexBytes = kMaxUiVerticesPerFrame * sizeof(Vertex);
static constexpr VkDeviceSize kUiBufferBytes = kUiVertexBytes + kMaxUiIndicesPerFrame * sizeof(std::uint32_t);
static constexpr VkDeviceSize kStagingRingBytes = 4ull * 1024 * 1024;
static constexpr VkDeviceSize kStagingRingMaxBytes = 64ull * 1024 * 1024;
public:
struct Timings {
double sync_ms = 0;
double fence_ms = 0; // inside sync_ms: waiting for the frame slot's previous GPU work
double prepare_ms = 0;
double steady_prepare_ms = 0;
double submit_ms = 0;
double present_ms = 0;
double gpu_ms = 0;
std::uint64_t gpu_samples = 0;
// Inside prepare_ms: creating geometry buffers and the blocking texture uploads.
double geometry_upload_ms = 0;
double texture_upload_ms = 0;
};
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
void enable_game_hardware_cursor();
void sync_game_cursor();
#endif
explicit VulkanWindow(bool vsync = true, int init_width = 960, int init_height = 640);
~VulkanWindow();
// 32-bit bottom-up BMP from a B8G8R8A8 / R8G8B8A8 swapchain readback.
void write_bmp(const std::string& path, const std::uint8_t* pixels) const;
// Seconds since the last touch, mouse or key event; 0 while a finger or mouse button is held (a held
// joystick or attack button sends no events). The frame-rate policy's idle test.
double input_idle_seconds() const;
void request_screenshot(std::string path, std::uint64_t frame);
std::uint64_t frame_number() const { return frame_number_; }
int msaa_samples() const { return int(samples_); }
std::uint32_t width() const { return extent_.width; }
std::uint32_t height() const { return extent_.height; }
std::uint32_t logical_width() const;
std::uint32_t logical_height() const;
VkViewport full_viewport() const;
// Window pixels kept clear at the left and right screen edges (rounded corners, cutouts);
// MainActivity measures them and passes --safe-inset-px.
void set_safe_inset_px(int px) { safe_inset_px_ = std::max(0, px); }
// Test builds: frames per second in the top-left corner (--show-fps).
void set_show_fps(bool show) { show_fps_ = show; }
SDL_Window* sdl_window() const { return window_; }
// The last render()'s blocking time: slot fence + swapchain acquire.
double last_sync_ms() const { return last_sync_ms_; }
// The same inset in logical UI pixels: the 40250 window layers are shifted and narrowed by it.
int ui_safe_inset() const;
// D3DVIEWPORT8 (in the game's logical screen pixels) scaled to the swapchain extent.
VkViewport draw_viewport(const Render3DDraw& draw) const;
TouchController touch_controller;
// Touch hosts: SDL text input (and with it the system keyboard) is on only while the player
// types into a tapped EditLine; desktop keeps the always-on text input from window creation.
void sync_screen_keyboard();
bool poll(bool forward_to_live_client = false);
void reset_timings();
void finish_gpu_timings();
// 1 = the old serial loop (CPU waits for the previous frame's GPU work), 2 = CPU/GPU overlap.
void set_frames_in_flight(std::uint32_t count);
std::uint32_t frames_in_flight() const { return frames_in_flight_; }
void render(
const std::vector<Render3DDraw>& draws,
const std::unordered_map<std::string, std::vector<std::uint8_t>>& capture_textures,
std::uint32_t ui_width = 960,
std::uint32_t ui_height = 640,
const std::vector<UIRenderCommand>& ui_commands = {});
std::size_t draw_count() const { return last_draw_count_; }
std::size_t skinned_draw_count() const { return last_skinned_draw_count_; }
std::size_t ui_batch_count() const { return last_ui_batch_count_; }
std::size_t ui_quad_count() const { return last_ui_quad_count_; }
std::size_t vertex_count() const { return last_vertex_count_; }
std::size_t index_count() const { return last_index_count_; }
std::size_t upload_count() const { return upload_count_; }
std::size_t uploaded_bytes() const { return uploaded_bytes_; }
std::size_t texture_upload_count() const { return texture_upload_count_; }
std::size_t texture_uploaded_bytes() const { return texture_uploaded_bytes_; }
const std::string& device_name() const { return device_name_; }
const char* present_mode_name() const;
Timings timings() const { return timings_; }
native_perf::RendererTotals perf_totals() const;
private:
void recreate_swapchain();
void collect_pending_gpu_timestamp(FrameSlot& slot);
void build_ui_batches(
std::uint32_t ui_width,
std::uint32_t ui_height,
const std::vector<UIRenderCommand>& commands,
const std::unordered_map<std::string, std::vector<std::uint8_t>>& capture_textures,
std::vector<UiBatch>& batches,
std::size_t& out_quad_count);
std::uint32_t find_memory_type(std::uint32_t type_bits, VkMemoryPropertyFlags flags) const;
void select_device();
void create_swapchain();
// The multisampled colour target the subpass resolves into the swapchain image. It never
// leaves the render pass, so it is transient (lazily allocated tile memory where available).
void create_msaa_color();
void destroy_msaa_color();
void create_framebuffers();
void create_host_buffer(VkDeviceSize size, VkBufferUsageFlags usage, VkBuffer& buffer, VkDeviceMemory& memory, void** mapped);
void create_descriptors_and_buffers();
// The slot's fence has signalled: its oversize staging buffers are free, and the ring restarts
// (grown to the largest frame's uploads seen so far, so texture streaming stays in the ring).
void release_retired_buffers(FrameSlot& slot);
void reset_staging(FrameSlot& slot);
void ensure_bone_capacity(std::size_t required);
void ensure_state_capacity(std::size_t required);
void ensure_ui_capacity(std::size_t command_count);
void create_render_pass_and_layout();
// The render pass of offscreen render-target textures: colour (R5G6B5 like the D3D surface when
// the device can render to it) + D32 depth, both loaded and stored, colour left shader-readable.
void create_offscreen_pass();
// "rt:<id>:<w>x<h>" -> its render target, created on first use (by a draw into it or by a draw
// sampling it); null for a malformed name.
RenderTarget* get_render_target(const std::string& name);
void release_render_target(RenderTarget& target);
// A new render target starts white with depth 1 (what the CPU surface's first Clear leaves), in
// the layouts offscreen_pass_ expects. Recorded outside any render pass.
void initialize_render_target(RenderTarget& target);
// offscreen: for offscreen_pass_ (single-sampled render-target texture) instead of pass_.
VkPipeline get_pipeline(std::uint8_t cull, std::uint8_t depth, std::uint8_t blend,
bool lines = false, std::uint8_t z_func = 4, bool offscreen = false);
VkDescriptorSet allocate_texture_descriptor_set(VkImageView view0, VkImageView view1,
VkSampler sampler0 = VK_NULL_HANDLE,
VkSampler sampler1 = VK_NULL_HANDLE);
VkDescriptorSet allocate_ui_texture_descriptor_set(VkImageView view0, VkImageView view1);
GpuTexture& get_gpu_texture(
const std::string& texture_name,
const std::unordered_map<std::string, std::vector<std::uint8_t>>& capture_textures);
std::uint64_t sampler_state_key(const Render3DDraw& draw, int stage) const;
VkSampler sampler_for_draw(const Render3DDraw& draw, int stage);
VkDescriptorSet get_texture_descriptor(
const std::string& texture0_name,
const std::string& mask_name,
const std::unordered_map<std::string, std::vector<std::uint8_t>>& capture_textures,
const Render3DDraw& draw);
VkDescriptorSet get_ui_texture_descriptor(
const std::string& texture0_name,
const std::string& mask_name,
const std::unordered_map<std::string, std::vector<std::uint8_t>>& capture_textures);
void upload_texture(
std::uint32_t width,
std::uint32_t height,
const std::uint8_t* rgba,
GpuTexture& texture,
bool count_upload,
bool generate_mips = true,
const std::vector<std::vector<std::uint8_t>>* file_mips = nullptr);
void release_texture(GpuTexture& texture);
void prune_textures();
void release_geometry(Geometry& geometry);
void upload_geometry(const Render3DDraw& draw, Geometry& geometry);
bool vsync_ = true;
VkPresentModeKHR present_mode_ = VK_PRESENT_MODE_FIFO_KHR;
float timestamp_period_ns_ = 1.0f;
float max_anisotropy_ = 1.0f;
SDL_Window* window_ = nullptr;
unsigned screen_keyboard_serial_ = 0;
int safe_inset_px_ = 0;
bool show_fps_ = false;
int fps_ = -1;
int fps_frames_ = 0;
std::chrono::steady_clock::time_point fps_window_start_{};
// Adds the scope's wall time to a Timings field.
struct ScopedMs {
explicit ScopedMs(double& into) : into_(into), start_(std::chrono::steady_clock::now()) {}
~ScopedMs() { into_ += std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start_).count(); }
double& into_;
std::chrono::steady_clock::time_point start_;
};
// Presented frames over the last half second.
void update_fps_counter();
VkInstance instance_ = VK_NULL_HANDLE;
VkSurfaceKHR surface_ = VK_NULL_HANDLE;
VkPhysicalDevice physical_ = VK_NULL_HANDLE;
VkDevice device_ = VK_NULL_HANDLE;
VkQueue queue_ = VK_NULL_HANDLE;
std::uint32_t queue_family_ = 0;
std::string device_name_;
VkSwapchainKHR swapchain_ = VK_NULL_HANDLE;
VkFormat swapchain_format_ = VK_FORMAT_UNDEFINED;
VkExtent2D extent_{};
std::vector<VkImageView> image_views_;
std::vector<VkImage> swapchain_images_;
void note_input(const SDL_Event& event);
std::chrono::steady_clock::time_point last_input_ = std::chrono::steady_clock::now();
int fingers_down_ = 0, buttons_down_ = 0;
// --screenshot-out: read back the swapchain image of one frame into a BMP file.
std::string screenshot_path_;
std::uint64_t screenshot_frame_ = 0;
VkImage depth_image_ = VK_NULL_HANDLE;
VkDeviceMemory depth_memory_ = VK_NULL_HANDLE;
VkImageView depth_view_ = VK_NULL_HANDLE;
VkSampleCountFlagBits samples_ = VK_SAMPLE_COUNT_1_BIT;
VkImage msaa_image_ = VK_NULL_HANDLE;
VkDeviceMemory msaa_memory_ = VK_NULL_HANDLE;
VkImageView msaa_view_ = VK_NULL_HANDLE;
VkRenderPass pass_ = VK_NULL_HANDLE;
std::vector<VkFramebuffer> framebuffers_;
VkSampler sampler_ = VK_NULL_HANDLE;
VkSampler clamp_sampler_ = VK_NULL_HANDLE;
VkSampler ui_sampler_ = VK_NULL_HANDLE;
std::unordered_map<std::uint64_t, VkSampler> state_samplers_;
VkDescriptorSetLayout descriptor_layout_ = VK_NULL_HANDLE;
VkDescriptorSetLayout bone_descriptor_layout_ = VK_NULL_HANDLE;
VkDescriptorPool descriptor_pool_ = VK_NULL_HANDLE;
// Everything one frame writes while the GPU may still read the previous frame's copy: with
// kFramesInFlight slots the CPU prepares frame N while the GPU draws frame N-1. A slot is reused
// only after its fence (the submit of frame N - frames_in_flight_) has signalled.
struct FrameSlot {
VkCommandBuffer command = VK_NULL_HANDLE;
VkFence fence = VK_NULL_HANDLE;
VkSemaphore acquire = VK_NULL_HANDLE;
std::uint32_t query_base = 0;
bool has_pending_query = false;
bool recording = false;
VkDescriptorSet bone_descriptor_set = VK_NULL_HANDLE;
VkBuffer bone_buffer = VK_NULL_HANDLE;
VkDeviceMemory bone_memory = VK_NULL_HANDLE;
float* bone_mapped = nullptr;
std::size_t bone_capacity = 0;
VkBuffer state_buffer = VK_NULL_HANDLE;
VkDeviceMemory state_memory = VK_NULL_HANDLE;
std::uint8_t* state_mapped = nullptr;
std::size_t state_capacity = 0;
VkBuffer ui_buffer = VK_NULL_HANDLE;
VkDeviceMemory ui_memory = VK_NULL_HANDLE;
std::uint8_t* ui_mapped = nullptr;
std::size_t ui_vertex_capacity = 0, ui_index_capacity = 0;
VkDeviceSize ui_vertex_bytes = 0;
// Texture uploads recorded into this frame's command buffer copy from here.
VkBuffer staging_buffer = VK_NULL_HANDLE;
VkDeviceMemory staging_memory = VK_NULL_HANDLE;
std::uint8_t* staging_mapped = nullptr;
VkDeviceSize staging_capacity = 0, staging_used = 0, staging_wanted = 0;
// Uploads larger than the ring get their own staging buffer, freed when the slot comes back.
std::vector<std::pair<VkBuffer, VkDeviceMemory>> retired_buffers;
};
static constexpr std::uint32_t kMaxFramesInFlight = 2;
std::array<FrameSlot, kMaxFramesInFlight> frames_{};
FrameSlot* f_ = &frames_[0];
std::uint32_t frames_in_flight_ = kMaxFramesInFlight;
std::uint32_t frame_slot_ = 0;
// Signalled by a frame's submit, waited by the present of that swapchain image.
std::vector<VkSemaphore> rendered_;
VkDeviceSize state_stride_ = 0;
VkDeviceSize staging_alignment_ = 16;
GpuTexture fallback_texture_{};
std::unordered_map<std::string, GpuTexture> textures_;
std::unordered_map<std::string, PairedDescriptor> paired_descriptors_;
std::unordered_map<std::string, RenderTarget> render_targets_;
VkRenderPass offscreen_pass_ = VK_NULL_HANDLE;
VkFormat offscreen_format_ = VK_FORMAT_R5G6B5_UNORM_PACK16;
std::size_t last_offscreen_draw_count_ = 0;
VkShaderModule vertex_module_ = VK_NULL_HANDLE;
VkShaderModule fragment_module_ = VK_NULL_HANDLE;
VkPipelineLayout layout_ = VK_NULL_HANDLE;
std::unordered_map<std::uint32_t, VkPipeline> pipelines_;
std::unordered_map<GeometryId, Geometry, GeometryIdHash> geometries_;
std::uint64_t frame_number_ = 0;
std::uint64_t timed_frames_ = 0;
std::size_t upload_count_ = 0, uploaded_bytes_ = 0;
std::size_t texture_upload_count_ = 0, texture_uploaded_bytes_ = 0;
std::string last_texture_upload_name_;
VkCommandPool pool_ = VK_NULL_HANDLE;
VkQueryPool query_pool_ = VK_NULL_HANDLE;
bool os_cursor_hidden_ = false;
bool hardware_cursor_enabled_ = false;
std::array<SDL_Cursor*, 15> game_cursors_{};
SDL_Cursor* fallback_cursor_ = nullptr;
int current_cursor_shape_ = -1;
int last_mx_ = 0, last_my_ = 0;
std::size_t last_draw_count_ = 0, last_skinned_draw_count_ = 0;
std::size_t last_ui_batch_count_ = 0, last_ui_quad_count_ = 0;
std::size_t last_vertex_count_ = 0, last_index_count_ = 0;
Timings timings_{};
double last_sync_ms_ = 0;
};
// One summary line of frame and renderer counters on stdout.
void print_summary(const VulkanWindow& renderer, int completed, double wall_ms, double update_ms = 0.0,
std::vector<double> frame_ms = {});
} // namespace mt_host