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>
This commit is contained in:
shenlei
2026-09-29 19:30:44 +09:00
co-authored by Claude Opus 5.5
parent a46093104c
commit 75d2dd8545
23 changed files with 4729 additions and 4271 deletions
+8
View File
@@ -14,6 +14,14 @@ add_custom_target(mt_native_shaders DEPENDS "${MT_NATIVE_VERT_SPV}" "${MT_NATIVE
set(MT_NATIVE_RENDER_SOURCES
main.cpp
frame_policy.cpp
host_util.cpp
input_keymap.cpp
live_client.cpp
native_audio.cpp
render_state.cpp
texture_decode.cpp
vulkan_window.cpp
stb_image_impl.cpp
dxt.cpp
)
+1 -1
View File
@@ -11,7 +11,7 @@
// frame budget, so the governor raises the clocks before a frame misses instead of idling the CPU at
// its lowest step between short bursts (the 556-748 MHz seen in perf-20260929-153205.csv).
// prefer_power_efficiency(): APerformanceHint_setPreferPowerEfficiency (API 35), set by the frame-rate
// policy (main.cpp FrameRatePolicy) except in the highest mode.
// policy (frame_policy.cpp FrameRatePolicy) except in the highest mode.
// - set_display_refresh_rate() / max_refresh_rate(): MainActivity switches the display mode at run time
// for the frame-rate setting (60 Hz unless the setting wants more).
// - battery_power(): BatteryManager current + voltage for the perf log's power columns; the power is only
+64
View File
@@ -0,0 +1,64 @@
#include "frame_policy.h"
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
#include "android_perf.h"
#include "platform/EterBase/FrameRateMode.h"
#include <SDL3/SDL.h>
#include <algorithm>
#include <cmath>
#include <fstream>
namespace mt_host {
// --fps-cap N: the loop starts a frame at most every 1/N s (0 = as fast as vsync allows).
int g_fps_cap = 0;
// --refresh-rate N: the surface's preferred frame rate (Android ANativeWindow_setFrameRate; MainActivity
// also picks the matching display mode). 0 leaves the system default.
int g_refresh_rate = 0;
// --fps-mode N: the frame-rate setting for this run (a test override: the saved setting stays);
// --idle-fps N: the rate after --idle-after S seconds without input (0 = never lower it).
int g_fps_mode = -1;
int g_idle_fps = 30;
double g_idle_after_s = 15.0;
void FrameRatePolicy::load() {
path_ = settings_path();
int mode = MtFrameRate::MODE_STANDARD;
if (std::ifstream in{path_}; in) in >> mode;
if (g_fps_mode >= 0) mode = g_fps_mode;
MtFrameRate::Set(mode);
saved_mode_ = MtFrameRate::Get();
max_hz_ = int(std::lround(android_perf::max_refresh_rate()));
if (max_hz_ < 30) max_hz_ = 60;
}
FrameRatePolicy::Decision FrameRatePolicy::decide(double idle_seconds, bool auto_hunt) {
Decision d;
d.mode = MtFrameRate::Get();
if (d.mode != saved_mode_) save(saved_mode_ = d.mode);
const int active = d.mode == MtFrameRate::MODE_SAVER ? 30
: d.mode == MtFrameRate::MODE_HIGH ? max_hz_ : std::min(60, max_hz_);
d.idle = g_idle_fps > 0 && g_idle_fps < active && idle_seconds >= g_idle_after_s && !auto_hunt;
d.fps = d.idle ? g_idle_fps : active;
d.display_hz = d.mode == MtFrameRate::MODE_HIGH && !d.idle ? max_hz_ : std::min(60, max_hz_);
return d;
}
std::string FrameRatePolicy::settings_path() {
std::string dir;
if (char* pref = SDL_GetPrefPath("metin2port", "client")) {
dir = pref;
SDL_free(pref);
}
return dir + "frame_rate.cfg";
}
void FrameRatePolicy::save(int mode) {
std::ofstream out{path_, std::ios::trunc};
out << mode << '\n';
}
} // namespace mt_host
#endif
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
#include <string>
namespace mt_host {
// Command-line overrides, defined in frame_policy.cpp.
extern int g_fps_cap;
extern int g_refresh_rate;
extern int g_fps_mode;
extern int g_idle_fps;
extern double g_idle_after_s;
// The frame-rate setting (system option dialog, platform/EterBase/FrameRateMode.h) turned into a frame
// rate, a display refresh rate and the ADPF budget; --fps-cap / --refresh-rate pin those for a test run.
// Frames the player does not watch closely cost the same power as the ones they do, so after a while
// without input (and no auto hunt running) the rate drops to --idle-fps until the next touch. The display
// is asked for 60 Hz unless the setting wants more: a 120 Hz panel scanning out a 60 fps game costs power
// for nothing.
class FrameRatePolicy {
public:
struct Decision {
int mode = -1;
int fps = 0; // frames per second the loop aims at
int display_hz = 0; // the refresh rate asked of the display
bool idle = false;
bool operator==(const Decision&) const = default;
};
bool pinned() const { return g_fps_cap > 0 || g_refresh_rate > 0; }
void load();
Decision decide(double idle_seconds, bool auto_hunt);
int max_hz() const { return max_hz_; }
private:
static std::string settings_path();
void save(int mode);
std::string path_;
int saved_mode_ = -1;
int max_hz_ = 60;
};
} // namespace mt_host
#endif
+36
View File
@@ -0,0 +1,36 @@
#include "host_util.h"
#include <SDL3/SDL.h>
#ifdef __ANDROID__
#include <jni.h>
#endif
#include <stdexcept>
namespace mt_host {
void check(VkResult result, const char* operation) {
if (result != VK_SUCCESS) throw std::runtime_error(std::string(operation) + ": VkResult " + std::to_string(result));
}
std::string bundled_file_path(const char* name) {
const char* base = SDL_GetBasePath();
return std::string(base ? base : "./") + name;
}
#ifdef __ANDROID__
// MainActivity.performHaptic(): the long-press vibration of the touch gestures.
void android_haptic() {
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
auto activity = static_cast<jobject>(SDL_GetAndroidActivity());
if (!env || !activity) return;
jclass cls = env->GetObjectClass(activity);
if (jmethodID method = env->GetMethodID(cls, "performHaptic", "()V"))
env->CallVoidMethod(activity, method);
if (env->ExceptionCheck()) env->ExceptionClear();
env->DeleteLocalRef(cls);
env->DeleteLocalRef(activity);
}
#endif
} // namespace mt_host
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <vulkan/vulkan.h>
#include <string>
namespace mt_host {
// Throws std::runtime_error when a Vulkan call fails.
void check(VkResult result, const char* operation);
// A file next to the executable (SDL base path).
std::string bundled_file_path(const char* name);
#ifdef __ANDROID__
void android_haptic();
#endif
} // namespace mt_host
+147
View File
@@ -0,0 +1,147 @@
#include "input_keymap.h"
namespace mt_host {
int sdl_scancode_to_dik(SDL_Scancode sc) {
switch (sc) {
case SDL_SCANCODE_ESCAPE: return 0x01;
case SDL_SCANCODE_1: return 0x02;
case SDL_SCANCODE_2: return 0x03;
case SDL_SCANCODE_3: return 0x04;
case SDL_SCANCODE_4: return 0x05;
case SDL_SCANCODE_5: return 0x06;
case SDL_SCANCODE_6: return 0x07;
case SDL_SCANCODE_7: return 0x08;
case SDL_SCANCODE_8: return 0x09;
case SDL_SCANCODE_9: return 0x0A;
case SDL_SCANCODE_0: return 0x0B;
case SDL_SCANCODE_MINUS: return 0x0C;
case SDL_SCANCODE_EQUALS: return 0x0D;
case SDL_SCANCODE_BACKSPACE: return 0x0E;
case SDL_SCANCODE_TAB: return 0x0F;
case SDL_SCANCODE_Q: return 0x10;
case SDL_SCANCODE_W: return 0x11;
case SDL_SCANCODE_E: return 0x12;
case SDL_SCANCODE_R: return 0x13;
case SDL_SCANCODE_T: return 0x14;
case SDL_SCANCODE_Y: return 0x15;
case SDL_SCANCODE_U: return 0x16;
case SDL_SCANCODE_I: return 0x17;
case SDL_SCANCODE_O: return 0x18;
case SDL_SCANCODE_P: return 0x19;
case SDL_SCANCODE_LEFTBRACKET: return 0x1A;
case SDL_SCANCODE_RIGHTBRACKET: return 0x1B;
case SDL_SCANCODE_RETURN: return 0x1C;
case SDL_SCANCODE_LCTRL: return 0x1D;
case SDL_SCANCODE_A: return 0x1E;
case SDL_SCANCODE_S: return 0x1F;
case SDL_SCANCODE_D: return 0x20;
case SDL_SCANCODE_F: return 0x21;
case SDL_SCANCODE_G: return 0x22;
case SDL_SCANCODE_H: return 0x23;
case SDL_SCANCODE_J: return 0x24;
case SDL_SCANCODE_K: return 0x25;
case SDL_SCANCODE_L: return 0x26;
case SDL_SCANCODE_SEMICOLON: return 0x27;
case SDL_SCANCODE_APOSTROPHE: return 0x28;
case SDL_SCANCODE_GRAVE: return 0x29;
case SDL_SCANCODE_LSHIFT: return 0x2A;
case SDL_SCANCODE_BACKSLASH: return 0x2B;
case SDL_SCANCODE_Z: return 0x2C;
case SDL_SCANCODE_X: return 0x2D;
case SDL_SCANCODE_C: return 0x2E;
case SDL_SCANCODE_V: return 0x2F;
case SDL_SCANCODE_B: return 0x30;
case SDL_SCANCODE_N: return 0x31;
case SDL_SCANCODE_M: return 0x32;
case SDL_SCANCODE_COMMA: return 0x33;
case SDL_SCANCODE_PERIOD: return 0x34;
case SDL_SCANCODE_SLASH: return 0x35;
case SDL_SCANCODE_RSHIFT: return 0x36;
case SDL_SCANCODE_KP_MULTIPLY: return 0x37;
case SDL_SCANCODE_LALT: return 0x38;
case SDL_SCANCODE_SPACE: return 0x39;
case SDL_SCANCODE_CAPSLOCK: return 0x3A;
case SDL_SCANCODE_F1: return 0x3B;
case SDL_SCANCODE_F2: return 0x3C;
case SDL_SCANCODE_F3: return 0x3D;
case SDL_SCANCODE_F4: return 0x3E;
case SDL_SCANCODE_F5: return 0x3F;
case SDL_SCANCODE_F6: return 0x40;
case SDL_SCANCODE_F7: return 0x41;
case SDL_SCANCODE_F8: return 0x42;
case SDL_SCANCODE_F9: return 0x43;
case SDL_SCANCODE_F10: return 0x44;
case SDL_SCANCODE_NUMLOCKCLEAR: return 0x45;
case SDL_SCANCODE_SCROLLLOCK: return 0x46;
case SDL_SCANCODE_KP_7: return 0x47;
case SDL_SCANCODE_KP_8: return 0x48;
case SDL_SCANCODE_KP_9: return 0x49;
case SDL_SCANCODE_KP_MINUS: return 0x4A;
case SDL_SCANCODE_KP_4: return 0x4B;
case SDL_SCANCODE_KP_5: return 0x4C;
case SDL_SCANCODE_KP_6: return 0x4D;
case SDL_SCANCODE_KP_PLUS: return 0x4E;
case SDL_SCANCODE_KP_1: return 0x4F;
case SDL_SCANCODE_KP_2: return 0x50;
case SDL_SCANCODE_KP_3: return 0x51;
case SDL_SCANCODE_KP_0: return 0x52;
case SDL_SCANCODE_KP_PERIOD: return 0x53;
case SDL_SCANCODE_F11: return 0x57;
case SDL_SCANCODE_F12: return 0x58;
case SDL_SCANCODE_KP_ENTER: return 0x9C;
case SDL_SCANCODE_RCTRL: return 0x9D;
case SDL_SCANCODE_KP_DIVIDE: return 0xB5;
case SDL_SCANCODE_RALT: return 0xB8;
case SDL_SCANCODE_HOME: return 0xC7;
case SDL_SCANCODE_UP: return 0xC8;
case SDL_SCANCODE_PAGEUP: return 0xC9;
case SDL_SCANCODE_LEFT: return 0xCB;
case SDL_SCANCODE_RIGHT: return 0xCD;
case SDL_SCANCODE_END: return 0xCF;
case SDL_SCANCODE_DOWN: return 0xD0;
case SDL_SCANCODE_PAGEDOWN: return 0xD1;
case SDL_SCANCODE_INSERT: return 0xD2;
case SDL_SCANCODE_DELETE: return 0xD3;
default: return 0;
}
}
int sdl_scancode_to_vk(SDL_Scancode sc) {
switch (sc) {
case SDL_SCANCODE_BACKSPACE: return 0x08;
case SDL_SCANCODE_TAB: return 0x09;
case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER: return 0x0D;
case SDL_SCANCODE_ESCAPE: return 0x1B;
case SDL_SCANCODE_SPACE: return 0x20;
case SDL_SCANCODE_PAGEUP: return 0x21;
case SDL_SCANCODE_PAGEDOWN: return 0x22;
case SDL_SCANCODE_END: return 0x23;
case SDL_SCANCODE_HOME: return 0x24;
case SDL_SCANCODE_LEFT: return 0x25;
case SDL_SCANCODE_UP: return 0x26;
case SDL_SCANCODE_RIGHT: return 0x27;
case SDL_SCANCODE_DOWN: return 0x28;
case SDL_SCANCODE_INSERT: return 0x2D;
case SDL_SCANCODE_DELETE: return 0x2E;
case SDL_SCANCODE_F1: return 0x70;
case SDL_SCANCODE_F2: return 0x71;
case SDL_SCANCODE_F3: return 0x72;
case SDL_SCANCODE_F4: return 0x73;
case SDL_SCANCODE_F5: return 0x74;
case SDL_SCANCODE_F6: return 0x75;
case SDL_SCANCODE_F7: return 0x76;
case SDL_SCANCODE_F8: return 0x77;
case SDL_SCANCODE_F9: return 0x78;
case SDL_SCANCODE_F10: return 0x79;
case SDL_SCANCODE_F11: return 0x7A;
case SDL_SCANCODE_F12: return 0x7B;
default: return 0;
}
}
} // namespace mt_host
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <SDL3/SDL_scancode.h>
namespace mt_host {
// SDL scancode -> DirectInput DIK_* code (CPythonApplication key events), 0 when unmapped.
int sdl_scancode_to_dik(SDL_Scancode sc);
// SDL scancode -> Win32 VK_* code (WM_KEYDOWN for EditLine / IME), 0 when unmapped.
int sdl_scancode_to_vk(SDL_Scancode sc);
} // namespace mt_host
+514
View File
@@ -0,0 +1,514 @@
#include "live_client.h"
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
#include "android_perf.h"
#include "draw_capture.h"
#include "frame_policy.h"
#include "host_util.h"
#include "native_audio.h"
#include "perf_log.h"
#include "texture_decode.h"
#include "vulkan_window.h"
#include "platform/MilesLib/AudioCommands.h"
#include "platform/PackBackend.h"
#include "platform/ScriptLib/PythonBoot.h"
#include "platform/UserInterface/ServerClock.h"
#include "platform/EterBase/TraceErrorObserver.h"
#include "platform/EterBase/FrameRateMode.h"
#include "../tests/port/port_login_flow_server.h"
#include <SDL3/SDL.h>
#ifdef __APPLE__
#include <TargetConditionals.h>
#endif
#include <unistd.h>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <fstream>
#include <iostream>
#include <thread>
#include <vector>
namespace mt_host {
// --py-exec: a test hook run once the auto-login run reaches the GameWindow (or, with --login-screen,
// once the LoginWindow is up).
std::string g_py_exec_after_login;
// --perf-log / MT_PERF_LOG=1: src/host/perf_log.h.
bool g_perf_log = false;
bool py_exec(const std::string& code) {
std::string err;
if (!PythonBoot::RunLine(code.c_str(), &err)) {
std::cerr << "PythonBoot::RunLine failed: " << err << '\n';
return false;
}
return true;
}
bool py_eval_true(const char* expr) {
std::string res, err;
return PythonBoot::Evaluate(expr, &res, &err) && (res == "True" || res == "1");
}
#ifdef __ANDROID__
bool hide_mobile_taskbar_quickslots() {
return py_exec(
"_mobile_taskbar = _stream.curPhaseWindow.interface.wndTaskBar\n"
"for _mobile_quickslot in _mobile_taskbar.quickslot:\n"
" _mobile_quickslot.Hide()\n"
"_mobile_taskbar.GetChild('QuickSlotBoard').Hide()\n");
}
#endif
#if defined(__APPLE__) && TARGET_OS_OSX
bool configure_macos_numeric_quickslots() {
return py_exec(
"_mac_game = _stream.curPhaseWindow\n"
"_mac_game.onPressKeyDict[app.DIK_5] = lambda: _mac_game._GameWindow__PressQuickSlot(4)\n"
"_mac_game.onPressKeyDict[app.DIK_6] = lambda: _mac_game._GameWindow__PressQuickSlot(5)\n"
"_mac_game.onPressKeyDict[app.DIK_7] = lambda: _mac_game._GameWindow__PressQuickSlot(6)\n"
"_mac_game.onPressKeyDict[app.DIK_8] = lambda: _mac_game._GameWindow__PressQuickSlot(7)\n"
"for _mac_key in (app.DIK_F1, app.DIK_F2, app.DIK_F3, app.DIK_F4):\n"
" del _mac_game.onPressKeyDict[_mac_key]\n"
"_mac_taskbar = _mac_game.interface.wndTaskBar\n"
"for _mac_slot_number in range(5, 9):\n"
" _mac_taskbar.GetChild('slot_%d' % _mac_slot_number).LoadImage('d:/ymir work/ui/game/taskbar/%d.sub' % _mac_slot_number)\n");
}
#endif
bool parse_live_server_spec(const std::string& spec, std::string& host, int& auth_port, int& game_port) {
const auto p1 = spec.find(':');
if (p1 == std::string::npos) return false;
const auto p2 = spec.find(':', p1 + 1);
if (p2 == std::string::npos) return false;
host = spec.substr(0, p1);
auth_port = std::stoi(spec.substr(p1 + 1, p2 - p1 - 1));
game_port = std::stoi(spec.substr(p2 + 1));
return !host.empty() && auth_port > 0 && game_port > 0;
}
int run_live_client(
VulkanWindow& renderer,
const std::string& client_dir,
int frames,
int fake_mobs,
bool gpu_skinning,
bool native_terrain,
bool login_screen,
bool selection_screen,
const std::string& live_server_spec,
const std::string& capture_out,
const std::string& screenshot_out) {
if (fake_mobs > 0) {
const std::string mobs_str = std::to_string(fake_mobs);
setenv("MT_FAKE_MOB_COUNT", mobs_str.c_str(), 1);
}
#ifdef __ANDROID__
SetTraceErrorObserver([](const char* line) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "40250 SYSERR: %s", line);
});
#endif
char resolved_client_dir[4096] = {};
const std::string abs_client_dir = realpath(client_dir.c_str(), resolved_client_dir) ? std::string(resolved_client_dir) : client_dir;
setenv("MT_40250_CLIENT", abs_client_dir.c_str(), 1);
#ifdef __ANDROID__
SDL_Log("live stage: pack init %s", abs_client_dir.c_str());
#endif
if (chdir(abs_client_dir.c_str()) != 0 || !mtpack40250::initialize(".")) {
throw std::runtime_error("cannot initialize 40250 pack at: " + abs_client_dir);
}
const bool host_hardware_cursor = !renderer.touch_controller.is_enabled();
PythonBoot::SetHostHardwareCursorEnabled(host_hardware_cursor);
if (host_hardware_cursor) renderer.enable_game_hardware_cursor();
SetNativeTerrainRenderEnabled(native_terrain);
SetGpuSkinningEnabled(gpu_skinning);
// The shadow map is drawn by the GPU (offscreen pass); MT_CPU_SHADOW=1 / --cpu-shadow keeps the
// CPU rasterizer (RecordingDevice::rasterize_shadow) for comparison.
const char* cpu_shadow = std::getenv("MT_CPU_SHADOW");
SetGpuRenderTargetsEnabled(!(cpu_shadow && *cpu_shadow == '1'));
NativeAudioEngine audio;
#ifdef __ANDROID__
SDL_Log("live stage: audio initialized");
#endif
std::string server_host = "127.0.0.1";
int auth_port = 0;
int game_port = 0;
const bool use_external_server = !live_server_spec.empty();
FakeLoginServer server;
if (use_external_server) {
if (!parse_live_server_spec(live_server_spec, server_host, auth_port, game_port))
throw std::runtime_error("invalid --live-server spec (expected HOST:AUTH_PORT:GAME_PORT): " + live_server_spec);
} else {
if (!server.Start()) throw std::runtime_error("FakeLoginServer failed to start on loopback");
auth_port = server.AuthPort();
game_port = server.GamePort();
}
{
// HiDPI text: rasterise glyph pages at the swapchain's pixels per UI pixel (before any font
// exists). Rounded up: a fractional density (Android 1760x800 UI on 2376x1080 = 1.35) then
// scales finer glyphs down instead of stretching 1x ones. MT_FONT_OVERSAMPLE=1 keeps the
// 40250 bilevel 1x glyphs.
const float density = float(renderer.width()) / float(std::max(1u, renderer.logical_width()));
int oversample = std::clamp(int(std::ceil(density - 0.05f)), 1, 4);
if (const char* env = std::getenv("MT_FONT_OVERSAMPLE")) oversample = std::max(1, std::atoi(env));
UISetFontOversample(oversample);
}
std::string error;
const char* env_stdlib = std::getenv("MT_PYTHON_STDLIB");
std::string stdlib = (env_stdlib && *env_stdlib) ? env_stdlib : bundled_file_path("python27.zip");
#ifdef __ANDROID__
if (!env_stdlib || !*env_stdlib) {
std::size_t zip_size = 0;
void* zip = SDL_LoadFile("assets://python27.zip", &zip_size);
if (!zip) zip = SDL_LoadFile("python27.zip", &zip_size);
if (!zip) throw std::runtime_error("cannot load bundled python27.zip");
char* pref = SDL_GetPrefPath("mtgodot", "native-render");
if (!pref) { SDL_free(zip); throw std::runtime_error("cannot locate app data directory"); }
stdlib = std::string(pref) + "python27.zip";
SDL_free(pref);
std::ofstream output(stdlib, std::ios::binary | std::ios::trunc);
output.write(static_cast<const char*>(zip), static_cast<std::streamsize>(zip_size));
SDL_free(zip);
if (!output) throw std::runtime_error("cannot extract bundled python27.zip");
}
#endif
if (!PythonBoot::Start(stdlib.c_str(), &error))
throw std::runtime_error("PythonBoot::Start: " + error);
#ifdef __ANDROID__
SDL_Log("live stage: PythonBoot started");
#endif
SetPlatformServerTime(123456789);
PythonBoot::SetUISafeInset(renderer.ui_safe_inset());
PythonBoot::SetUISize(static_cast<int>(renderer.logical_width()), static_cast<int>(renderer.logical_height()));
if (!PythonBoot::RunMainScript("", &error) || !PythonBoot::IsAppLooping())
throw std::runtime_error("PythonBoot::RunMainScript: " + error);
#ifdef __ANDROID__
SDL_Log("live stage: main script started");
#endif
const std::unordered_map<std::string, std::vector<std::uint8_t>> empty_textures;
auto pump_until = [&](double max_seconds, int sleep_ms, const auto& cond) -> bool {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::duration<double>(max_seconds);
while (std::chrono::steady_clock::now() < deadline && renderer.poll(true) && PythonBoot::IsAppLooping()) {
PythonBoot::UIUpdate();
renderer.sync_game_cursor();
PythonBoot::UIRender();
audio.pump();
unsigned ui_w = renderer.width(), ui_h = renderer.height();
UIRenderGetSize(&ui_w, &ui_h);
renderer.render(Render3DDraws(), empty_textures, ui_w, ui_h, UIRenderCommands());
if (cond()) return true;
if (sleep_ms > 0)
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
}
return cond();
};
if (!py_exec("import __main__, app, networkModule, introLogin, introSelect, introLoading, game\n"
"__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]"))
throw std::runtime_error("failed to locate networkModule.MainStream");
if (!pump_until(20.0, 5, [] { return py_eval_true("isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"); }))
throw std::runtime_error("timed out waiting for LoginWindow");
#ifdef __ANDROID__
SDL_Log("live stage: LoginWindow open");
#endif
if (login_screen) {
char conn_cmd[512];
std::snprintf(
conn_cmd,
sizeof(conn_cmd),
"_stream.SetConnectInfo('%s', %d, '%s', %d)\n"
"_w = _stream.curPhaseWindow\n"
"_w._LoginWindow__OpenServerBoard()",
server_host.c_str(),
game_port,
server_host.c_str(),
auth_port);
if (!py_exec(conn_cmd))
throw std::runtime_error("failed to configure LoginWindow connection info");
for (int i = 0; i < 15; ++i) {
PythonBoot::UIUpdate();
renderer.sync_game_cursor();
PythonBoot::UIRender();
}
if (!g_py_exec_after_login.empty() && !py_exec(g_py_exec_after_login))
throw std::runtime_error("--py-exec failed");
} else {
char login_cmd[512];
std::snprintf(
login_cmd,
sizeof(login_cmd),
"_stream.SetConnectInfo('%s', %d, '%s', %d)\n"
"_w = _stream.curPhaseWindow\n"
"_w._LoginWindow__OpenLoginBoard()\n"
"_w.idEditLine.SetText('%s')\n"
"_w.pwdEditLine.SetText('%s')\n"
"_w._LoginWindow__OnClickLoginButton()",
server_host.c_str(),
game_port,
server_host.c_str(),
auth_port,
FakeLoginServer::kLogin,
FakeLoginServer::kPassword);
#ifdef __ANDROID__
SDL_Log("live stage: submitting fake login");
#endif
if (!py_exec(login_cmd))
throw std::runtime_error("failed to submit login credentials");
#ifdef __ANDROID__
SDL_Log("live stage: fake login submitted");
#endif
if (!pump_until(20.0, 5, [&] {
return (use_external_server || server.Has("game1:login2")) &&
py_eval_true("isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)");
}))
throw std::runtime_error("timed out waiting for SelectCharacterWindow");
#ifdef __ANDROID__
SDL_Log("live stage: SelectCharacterWindow open");
#endif
if (!selection_screen) {
if (!py_exec("_stream.curPhaseWindow.SelectSlot(0)\n"
"_stream.curPhaseWindow.StartGame()"))
throw std::runtime_error("failed to start game from SelectCharacterWindow");
if (!use_external_server) {
if (!pump_until(30.0, 5, [&] { return server.Has("game2:client_version"); }))
throw std::runtime_error("timed out waiting for LoadingWindow (client_version)");
}
if (!pump_until(60.0, 0, [&] {
return (use_external_server || server.Has("game2:burst_pong")) &&
py_eval_true("isinstance(_stream.curPhaseWindow, game.GameWindow) and _stream.curPhaseWindow.IsShow()");
}))
throw std::runtime_error("timed out waiting for GameWindow (server error: " + server.Error() + ")");
#ifdef __ANDROID__
if (!hide_mobile_taskbar_quickslots())
throw std::runtime_error("failed to hide the desktop quick-slot bar on Android");
#endif
#if defined(__APPLE__) && TARGET_OS_OSX
if (!configure_macos_numeric_quickslots())
throw std::runtime_error("failed to configure numeric quick slots on macOS");
#endif
if (!g_py_exec_after_login.empty() && !py_exec(g_py_exec_after_login))
throw std::runtime_error("--py-exec failed");
const int warmup_frames = std::max(10, fake_mobs / 2 + 10);
for (int i = 0; i < warmup_frames; ++i) {
if (!renderer.poll(true) || !PythonBoot::IsAppLooping()) break;
PythonBoot::UIUpdate();
renderer.sync_game_cursor();
audio.pump();
}
}
}
// Render one warmup frame to upload initial scene & UI textures/geometries before timed benchmark.
{
unsigned ui_w = renderer.width(), ui_h = renderer.height();
UIRenderGetSize(&ui_w, &ui_h);
renderer.render(Render3DDraws(), empty_textures, ui_w, ui_h, UIRenderCommands());
}
if (!capture_out.empty()) {
std::unordered_map<std::string, std::vector<std::uint8_t>> cap_textures;
auto add_tex = [&](const std::string& name) {
if (name.empty() || cap_textures.count(name)) return;
if (name.rfind("mem:", 0) == 0) {
UIMemoryTexture mem_tex;
if (UIRenderMemoryTexture(name, &mem_tex) && mem_tex.width > 0 && mem_tex.height > 0) {
auto mtra = native_draw_capture::encode_raw_argb_as_mtra(
static_cast<std::uint32_t>(mem_tex.width),
static_cast<std::uint32_t>(mem_tex.height),
mem_tex.argb.data());
if (!mtra.empty()) cap_textures.emplace(name, std::move(mtra));
}
return;
}
std::vector<std::uint8_t> bytes;
if (read_live_pack_texture(name, bytes))
cap_textures.emplace(name, std::move(bytes));
};
for (const auto& d : Render3DDraws()) {
add_tex(d.texture0);
add_tex(d.texture1);
}
for (const auto& c : UIRenderCommands()) {
if (c.kind == UIRenderCommand::Image) {
add_tex(c.text);
add_tex(c.mask);
}
}
unsigned ui_w = renderer.width(), ui_h = renderer.height();
UIRenderGetSize(&ui_w, &ui_h);
std::vector<UIRenderCommand> cap_ui = UIRenderCommands();
if (renderer.touch_controller.is_enabled()) {
renderer.touch_controller.update_screen_size(int(ui_w), int(ui_h));
renderer.touch_controller.append_ui_commands(cap_ui);
}
native_draw_capture::write(capture_out, Render3DDraws(), cap_textures, ui_w, ui_h, cap_ui);
}
if (!screenshot_out.empty() && frames > 0) {
renderer.request_screenshot(screenshot_out, renderer.frame_number() + static_cast<std::uint64_t>(frames));
}
renderer.reset_timings();
native_perf::PerfLog perf_log;
if (g_perf_log) {
perf_log.open("device=" + renderer.device_name() + " present_mode=" + renderer.present_mode_name() +
" msaa=" + std::to_string(renderer.msaa_samples()) + " size=" + std::to_string(renderer.width()) +
"x" + std::to_string(renderer.height()) + " gpu_skinning=" + std::to_string(gpu_skinning) +
" terrain=" + std::to_string(native_terrain));
}
if (g_refresh_rate > 0) {
const bool ok = android_perf::request_frame_rate(renderer.sdl_window(), float(g_refresh_rate));
SDL_Log("frame rate request %d Hz: %s", g_refresh_rate, ok ? "accepted" : "unavailable");
}
perf_log.set_fps_cap(g_fps_cap);
perf_log.set_refresh_rate_source([] { return android_perf::display_refresh_rate(); });
perf_log.set_render_rate_source([] { return android_perf::render_rate(); });
perf_log.set_battery_source([] { return android_perf::battery_celsius(); });
perf_log.set_power_source([] {
const auto p = android_perf::battery_power();
return native_perf::PowerSample{p.current_ma, p.voltage_mv, p.power_mw, p.plugged};
});
// ADPF: each frame's CPU work (everything but the vsync/fence waits and the cap's sleep) against the
// frame budget. Opened once the script thread exists, i.e. here.
using steady = std::chrono::steady_clock;
// The target is 80% of the frame period: the governor settles the clocks so the reported work just
// meets the target, so a target equal to the period leaves every other frame late (cap 60 on the
// test phone: 54 fps against the period, 60 against 80% of it).
const auto budget_for = [](int fps) { return std::chrono::nanoseconds(800'000'000LL / std::max(1, fps)); };
FrameRatePolicy policy;
policy.load();
const auto frame_budget = budget_for(
g_fps_cap > 0 ? g_fps_cap : (g_refresh_rate > 0 ? g_refresh_rate : 60));
android_perf::PerformanceHint hint;
{
std::vector<int32_t> tids{android_perf::current_thread_id()};
if (const int script = PythonBoot::ScriptThreadId()) tids.push_back(script);
const bool ok = hint.open(tids, frame_budget.count());
SDL_Log("ADPF performance hint (%zu threads, %.2f ms): %s", tids.size(), frame_budget.count() / 1e6,
ok ? "on" : "unavailable");
}
const auto start = steady::now();
double update_ms = 0.0;
int completed = 0;
std::vector<double> frame_ms;
if (frames > 0) frame_ms.reserve(static_cast<std::size_t>(frames));
auto cap_period = g_fps_cap > 0 ? std::chrono::nanoseconds(1'000'000'000LL / g_fps_cap)
: std::chrono::nanoseconds(0);
auto next_frame_at = steady::now();
FrameRatePolicy::Decision decision;
auto display_checked_at = steady::time_point{};
// The loop sleeps to the policy's rate unless the display already runs at it (vsync then paces the
// loop, and a second clock beating against it would drop frames).
const auto apply_policy = [&] {
if (policy.pinned()) return;
const auto now = steady::now();
const auto next = policy.decide(renderer.input_idle_seconds(), PythonBoot::AutoHuntIsEnabled());
const bool changed = !(next == decision);
if (changed) {
if (next.display_hz != decision.display_hz) {
android_perf::set_display_refresh_rate(float(next.display_hz));
android_perf::request_frame_rate(renderer.sdl_window(), float(next.display_hz));
}
hint.set_target(budget_for(next.fps).count());
// Efficiency cores and lower clocks, except where the setting asks for the highest rate.
hint.prefer_power_efficiency(next.mode != MtFrameRate::MODE_HIGH || next.idle);
perf_log.set_fps_cap(next.fps);
perf_log.set_frame_policy(next.mode, next.idle);
SDL_Log("frame rate: mode %d -> %d fps, display %d Hz%s", next.mode, next.fps, next.display_hz,
next.idle ? " (idle)" : "");
decision = next;
}
if (changed || now - display_checked_at > std::chrono::seconds(1)) {
display_checked_at = now;
const double hz = android_perf::display_refresh_rate();
const bool vsync_paced = hz > 0 && std::fabs(hz - decision.fps) < 2.0;
const auto period = vsync_paced ? std::chrono::nanoseconds(0)
: std::chrono::nanoseconds(1'000'000'000LL / decision.fps);
if (period != cap_period) {
cap_period = period;
next_frame_at = now;
}
}
};
while ((frames == 0 || completed < frames) && renderer.poll(true) && PythonBoot::IsAppLooping()) {
apply_policy();
double pace_ms = 0.0;
if (cap_period.count()) {
// Fixed cadence: a late frame starts the next one at once, a frame more than one period
// late re-anchors instead of bursting to catch up.
const auto now = steady::now();
if (next_frame_at > now) {
std::this_thread::sleep_until(next_frame_at);
pace_ms = std::chrono::duration<double, std::milli>(steady::now() - now).count();
} else if (now - next_frame_at > cap_period) {
next_frame_at = now;
}
next_frame_at += cap_period;
}
const auto frame_start = steady::now();
const auto t0 = frame_start;
PythonBoot::UIUpdate();
const auto t_app = std::chrono::steady_clock::now();
renderer.sync_game_cursor();
PythonBoot::UIRender();
audio.pump();
const auto t1 = std::chrono::steady_clock::now();
update_ms += std::chrono::duration<double, std::milli>(t1 - t0).count();
unsigned ui_w = renderer.width(), ui_h = renderer.height();
UIRenderGetSize(&ui_w, &ui_h);
renderer.render(Render3DDraws(), empty_textures, ui_w, ui_h, UIRenderCommands());
const auto t2 = std::chrono::steady_clock::now();
const double elapsed_ms = std::chrono::duration<double, std::milli>(t2 - frame_start).count();
PythonBoot::SetNativeRenderFrameTime(static_cast<float>(elapsed_ms));
hint.report(std::int64_t((elapsed_ms - renderer.last_sync_ms()) * 1e6));
if (perf_log.enabled()) {
native_perf::FrameInput in;
in.frame_ms = elapsed_ms;
in.pace_ms = pace_ms;
in.app_ms = std::chrono::duration<double, std::milli>(t_app - t0).count();
in.host_ms = std::chrono::duration<double, std::milli>(t1 - t_app).count();
in.render_ms = std::chrono::duration<double, std::milli>(t2 - t1).count();
in.renderer = renderer.perf_totals();
perf_log.frame(in, [] { return PythonBoot::CurrentMapName(); });
}
frame_ms.push_back(elapsed_ms);
++completed;
}
renderer.finish_gpu_timings();
#ifdef __ANDROID__
SDL_Log("live stage: render loop complete (%d frames)", completed);
#endif
const auto wall_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
print_summary(renderer, completed, wall_ms, update_ms, std::move(frame_ms));
PythonBoot::Stop();
#ifdef __ANDROID__
SDL_Log("live stage: PythonBoot stopped");
#endif
if (!use_external_server)
server.Stop();
return (frames == 0 || completed == frames) ? 0 : 2;
}
} // namespace mt_host
#endif
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
#include <string>
namespace mt_host {
class VulkanWindow;
// --py-exec: a test hook run once the auto-login run reaches the GameWindow (or, with --login-screen,
// once the LoginWindow is up).
extern std::string g_py_exec_after_login;
// --perf-log / MT_PERF_LOG=1: src/host/perf_log.h.
extern bool g_perf_log;
// Boots the 40250 client (PythonBoot) from client_dir and runs it inside the renderer until it quits.
int run_live_client(
VulkanWindow& renderer,
const std::string& client_dir,
int frames,
int fake_mobs,
bool gpu_skinning,
bool native_terrain,
bool login_screen,
bool selection_screen,
const std::string& live_server_spec,
const std::string& capture_out,
const std::string& screenshot_out);
} // namespace mt_host
#endif
+6 -4260
View File
File diff suppressed because it is too large Load Diff
+267
View File
@@ -0,0 +1,267 @@
#include "native_audio.h"
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
#include "texture_decode.h"
#include "platform/MilesLib/AudioCommands.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <iostream>
namespace mt_host {
#ifdef __APPLE__
OSStatus NativeAudioEngine::mem_audio_read_proc(
void* inClientData, SInt64 inPosition, UInt32 requestCount, void* buffer, UInt32* actualCount) {
const auto* mem = static_cast<const MemoryAudioBuffer*>(inClientData);
if (inPosition < 0 || static_cast<std::size_t>(inPosition) >= mem->size) {
*actualCount = 0;
return noErr;
}
const std::size_t avail = mem->size - static_cast<std::size_t>(inPosition);
const std::size_t to_read = std::min<std::size_t>(requestCount, avail);
std::memcpy(buffer, mem->data + inPosition, to_read);
*actualCount = static_cast<UInt32>(to_read);
return noErr;
}
SInt64 NativeAudioEngine::mem_audio_get_size_proc(void* inClientData) {
return static_cast<SInt64>(static_cast<const MemoryAudioBuffer*>(inClientData)->size);
}
#endif
NativeAudioEngine::NativeAudioEngine() {
if (SDL_InitSubSystem(SDL_INIT_AUDIO)) {
SDL_AudioSpec spec{};
spec.format = SDL_AUDIO_F32;
spec.channels = 2;
spec.freq = 44100;
stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, nullptr);
if (stream_)
SDL_ResumeAudioStreamDevice(stream_);
}
// Always DrainAudioCommands() from cold boot so stale queued commands don't accumulate.
(void)DrainAudioCommands();
}
NativeAudioEngine::~NativeAudioEngine() {
if (stream_)
SDL_DestroyAudioStream(stream_);
SDL_QuitSubSystem(SDL_INIT_AUDIO);
}
void NativeAudioEngine::pump() {
const auto commands = DrainAudioCommands();
for (const auto& cmd : commands) {
++commands_drained_;
switch (cmd.type) {
case AudioCommand::PlaySound2D: {
const auto* clip = get_clip(cmd.filename);
if (clip && !clip->empty()) {
Voice v{};
v.samples = clip;
v.volume = sound_volume_;
v.target_volume = sound_volume_;
v.filename = cmd.filename;
voices_.push_back(std::move(v));
++played_2d_;
}
break;
}
case AudioCommand::PlaySound3D: {
const auto* clip = get_clip(cmd.filename);
if (clip && !clip->empty()) {
Voice v{};
v.samples = clip;
v.volume = sound_volume_ * std::clamp(cmd.volume, 0.0f, 1.0f);
v.target_volume = v.volume;
v.loop = cmd.play_count != 1;
v.is_3d = true;
v.handle = cmd.id;
v.filename = cmd.filename;
voices_.push_back(std::move(v));
++played_3d_;
}
break;
}
case AudioCommand::StopSound3D:
voices_.erase(
std::remove_if(voices_.begin(), voices_.end(), [&](const Voice& v) {
return v.is_3d && v.handle == cmd.id;
}),
voices_.end());
break;
case AudioCommand::StopAllSound3D:
voices_.erase(
std::remove_if(voices_.begin(), voices_.end(), [](const Voice& v) { return v.is_3d; }),
voices_.end());
break;
case AudioCommand::SetSoundVolume3D:
for (auto& v : voices_) {
if (v.is_3d && v.handle == cmd.id) {
v.volume = sound_volume_ * std::clamp(cmd.volume, 0.0f, 1.0f);
v.target_volume = v.volume;
}
}
break;
case AudioCommand::PlayMusic:
case AudioCommand::FadeInMusic: {
const auto* clip = get_clip(cmd.filename);
if (clip && !clip->empty()) {
bgm_.samples = clip;
bgm_.frame_cursor = 0;
bgm_.loop = true;
bgm_.filename = cmd.filename;
bgm_.stop_after_fade = false;
const float target = music_volume_ * std::clamp(cmd.volume, 0.0f, 1.0f);
bgm_.target_volume = target;
if (cmd.type == AudioCommand::FadeInMusic) {
bgm_.volume = 0.0f;
bgm_.fade_step_per_frame = target / (44100.0f * 1.5f);
} else {
bgm_.volume = target;
bgm_.fade_step_per_frame = 0.0f;
}
++played_music_;
}
break;
}
case AudioCommand::FadeOutMusic:
case AudioCommand::FadeOutAllMusic:
if (bgm_.samples) {
bgm_.target_volume = 0.0f;
bgm_.fade_step_per_frame = -std::max(bgm_.volume, 0.01f) / (44100.0f * 1.0f);
bgm_.stop_after_fade = true;
}
break;
case AudioCommand::FadeLimitOutMusic:
if (bgm_.samples) {
bgm_.target_volume = music_volume_ * std::clamp(cmd.volume, 0.0f, 1.0f);
bgm_.fade_step_per_frame = (bgm_.target_volume - bgm_.volume) / (44100.0f * 1.0f);
bgm_.stop_after_fade = false;
}
break;
case AudioCommand::SetMusicVolume:
music_volume_ = std::clamp(cmd.volume, 0.0f, 1.0f);
if (bgm_.samples && !bgm_.stop_after_fade) {
bgm_.volume = music_volume_;
bgm_.target_volume = music_volume_;
}
break;
case AudioCommand::SetSoundVolume:
sound_volume_ = std::clamp(cmd.volume, 0.0f, 1.0f);
break;
default:
break;
}
}
if (!stream_) return;
const int queued_bytes = SDL_GetAudioStreamAvailable(stream_);
if (queued_bytes < 0) return;
const std::size_t queued_frames = static_cast<std::size_t>(queued_bytes) / (2 * sizeof(float));
constexpr std::size_t kTargetQueuedFrames = 4410; // ~100 ms stereo @ 44.1 kHz
if (queued_frames >= kTargetQueuedFrames) return;
const std::size_t frames_to_mix = kTargetQueuedFrames - queued_frames;
mix_buffer_.assign(frames_to_mix * 2, 0.0f);
auto mix_voice = [&](Voice& v) -> bool {
if (!v.samples || v.samples->empty()) return false;
const std::size_t total_frames = v.samples->size() / 2;
if (total_frames == 0) return false;
const float* src = v.samples->data();
for (std::size_t f = 0; f < frames_to_mix; ++f) {
if (v.frame_cursor >= total_frames) {
if (v.loop) v.frame_cursor = 0;
else return false;
}
if (v.fade_step_per_frame != 0.0f) {
v.volume += v.fade_step_per_frame;
if ((v.fade_step_per_frame > 0.0f && v.volume >= v.target_volume) ||
(v.fade_step_per_frame < 0.0f && v.volume <= v.target_volume)) {
v.volume = v.target_volume;
v.fade_step_per_frame = 0.0f;
if (v.stop_after_fade && v.volume <= 0.0001f) {
v.samples = nullptr;
return false;
}
}
}
mix_buffer_[f * 2 + 0] += src[v.frame_cursor * 2 + 0] * v.volume;
mix_buffer_[f * 2 + 1] += src[v.frame_cursor * 2 + 1] * v.volume;
++v.frame_cursor;
}
return v.loop || v.frame_cursor < total_frames;
};
if (bgm_.samples) {
if (!mix_voice(bgm_)) bgm_.samples = nullptr;
}
for (auto it = voices_.begin(); it != voices_.end();) {
if (!mix_voice(*it)) it = voices_.erase(it);
else ++it;
}
for (float& sample : mix_buffer_)
sample = std::clamp(sample, -1.0f, 1.0f);
SDL_PutAudioStreamData(stream_, mix_buffer_.data(), static_cast<int>(mix_buffer_.size() * sizeof(float)));
}
const std::vector<float>* NativeAudioEngine::get_clip(const std::string& vpath) {
if (vpath.empty()) return nullptr;
auto [it, inserted] = clips_.try_emplace(vpath);
if (!inserted) return &it->second;
#ifdef __APPLE__
std::vector<std::uint8_t> bytes;
if (!read_live_pack_texture(vpath, bytes) || bytes.size() < 16)
return &it->second;
MemoryAudioBuffer mem{bytes.data(), bytes.size()};
AudioFileID audio_file = nullptr;
if (AudioFileOpenWithCallbacks(
&mem, mem_audio_read_proc, nullptr, mem_audio_get_size_proc, nullptr, 0, &audio_file) != noErr ||
!audio_file) {
return &it->second;
}
ExtAudioFileRef ext_file = nullptr;
if (ExtAudioFileWrapAudioFileID(audio_file, false, &ext_file) != noErr || !ext_file) {
AudioFileClose(audio_file);
return &it->second;
}
AudioStreamBasicDescription client_format{};
client_format.mSampleRate = 44100.0;
client_format.mFormatID = kAudioFormatLinearPCM;
client_format.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked;
client_format.mBytesPerPacket = 8;
client_format.mFramesPerPacket = 1;
client_format.mBytesPerFrame = 8;
client_format.mChannelsPerFrame = 2;
client_format.mBitsPerChannel = 32;
if (ExtAudioFileSetProperty(
ext_file,
kExtAudioFileProperty_ClientDataFormat,
sizeof(client_format),
&client_format) == noErr) {
std::vector<float> chunk(4096 * 2);
while (true) {
UInt32 frame_count = 4096;
AudioBufferList buf_list{};
buf_list.mNumberBuffers = 1;
buf_list.mBuffers[0].mNumberChannels = 2;
buf_list.mBuffers[0].mDataByteSize = static_cast<UInt32>(chunk.size() * sizeof(float));
buf_list.mBuffers[0].mData = chunk.data();
if (ExtAudioFileRead(ext_file, &frame_count, &buf_list) != noErr || frame_count == 0)
break;
it->second.insert(it->second.end(), chunk.begin(), chunk.begin() + std::size_t(frame_count) * 2);
if (it->second.size() > 44100 * 2 * 300) // cap single decoded track at 5 minutes
break;
}
}
ExtAudioFileDispose(ext_file);
AudioFileClose(audio_file);
#endif
return &it->second;
}
} // namespace mt_host
#endif
+67
View File
@@ -0,0 +1,67 @@
#pragma once
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
#include <SDL3/SDL.h>
#ifdef __APPLE__
#include <AudioToolbox/AudioToolbox.h>
#endif
#include <cstddef>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
namespace mt_host {
// Drains the MilesLib AudioCommand queue and mixes 2D/3D sounds and BGM into an SDL audio stream.
class NativeAudioEngine {
struct MemoryAudioBuffer {
const std::uint8_t* data = nullptr;
std::size_t size = 0;
};
struct Voice {
const std::vector<float>* samples = nullptr;
std::size_t frame_cursor = 0;
float volume = 1.0f;
float target_volume = 1.0f;
float fade_step_per_frame = 0.0f;
bool stop_after_fade = false;
bool loop = false;
bool is_3d = false;
int handle = 0;
std::string filename;
};
#ifdef __APPLE__
static OSStatus mem_audio_read_proc(
void* inClientData, SInt64 inPosition, UInt32 requestCount, void* buffer, UInt32* actualCount);
static SInt64 mem_audio_get_size_proc(void* inClientData);
#endif
public:
NativeAudioEngine();
~NativeAudioEngine();
void pump();
private:
const std::vector<float>* get_clip(const std::string& vpath);
SDL_AudioStream* stream_ = nullptr;
std::unordered_map<std::string, std::vector<float>> clips_;
std::vector<Voice> voices_;
Voice bgm_{};
std::vector<float> mix_buffer_;
float sound_volume_ = 0.8f;
float music_volume_ = 0.5f;
std::size_t commands_drained_ = 0;
std::size_t played_2d_ = 0;
std::size_t played_3d_ = 0;
std::size_t played_music_ = 0;
};
} // namespace mt_host
#endif
+130
View File
@@ -0,0 +1,130 @@
#include "render_state.h"
namespace mt_host {
std::array<float, 16> multiply(const float* a, const float* b) {
std::array<float, 16> result{};
for (int row = 0; row < 4; ++row)
for (int col = 0; col < 4; ++col)
for (int k = 0; k < 4; ++k)
result[row * 4 + col] += a[row * 4 + k] * b[k * 4 + col];
return result;
}
std::array<float, 16> draw_mvp(const Render3DDraw& draw) {
const auto world_view = multiply(draw.world, draw.view);
return multiply(world_view.data(), draw.proj);
}
std::array<float, 4> unpack_argb(std::uint32_t argb) {
return {
float((argb >> 16) & 255) / 255.0f,
float((argb >> 8) & 255) / 255.0f,
float(argb & 255) / 255.0f,
float((argb >> 24) & 255) / 255.0f};
}
// Inverse transpose of the upper 3x3 of a row-vector matrix, the D3D8 normal transform.
std::array<float, 16> normal_matrix(const std::array<float, 16>& m) {
const float a = m[0], b = m[1], c = m[2], d = m[4], e = m[5], f = m[6], g = m[8], h = m[9], i = m[10];
const float A = e * i - f * h, B = -(d * i - f * g), C = d * h - e * g;
const float D = -(b * i - c * h), E = a * i - c * g, F = -(a * h - b * g);
const float G = b * f - c * e, H = -(a * f - c * d), I = a * e - b * d;
const float det = a * A + b * B + c * C;
std::array<float, 16> out = kIdentityMatrix;
if (det == 0.0f) return out;
const float s = 1.0f / det;
// inverse = adjugate / det; its transpose is the cofactor matrix / det.
out[0] = A * s; out[1] = B * s; out[2] = C * s;
out[4] = D * s; out[5] = E * s; out[6] = F * s;
out[8] = G * s; out[9] = H * s; out[10] = I * s;
return out;
}
PushConstants make_push_constants(const Render3DDraw& draw, float skin_offset_encoded) {
PushConstants constants{};
constants.mvp = draw_mvp(draw);
constants.params = {skin_offset_encoded, draw.pretransformed ? 1.0f : 0.0f, 0.0f, 0.0f};
return constants;
}
FixedFunctionState make_fixed_function_state(const Render3DDraw& draw) {
FixedFunctionState state{};
for (int stage = 0; stage < 2; ++stage) {
state.stage_color[stage] = {draw.color_op[stage], draw.color_arg1[stage], draw.color_arg2[stage],
draw.texcoord_index[stage]};
state.stage_alpha[stage] = {draw.alpha_op[stage], draw.alpha_arg1[stage], draw.alpha_arg2[stage],
draw.texture_transform_flags[stage]};
std::copy(draw.texture_matrix[stage], draw.texture_matrix[stage] + 16, state.texture_matrix[stage].begin());
}
state.texture_factor = unpack_argb(draw.texture_factor);
state.world_view = multiply(draw.world, draw.view);
state.normal_matrix = normal_matrix(state.world_view);
state.fog_color = unpack_argb(draw.fog_color);
state.fog_params = {draw.fog_start, draw.fog_end, draw.fog_density,
draw.fog_range_enable ? 1.0f : 0.0f};
const std::uint32_t fog_mode = draw.fog_enable
? (draw.fog_table_mode ? draw.fog_table_mode : (draw.pretransformed ? 4u : draw.fog_vertex_mode)) : 0;
state.flags = {1u, !draw.texture0.empty() ? 1u : 0u, !draw.texture1.empty() ? 1u : 0u, fog_mode};
state.lighting_flags = {draw.lighting, draw.color_vertex, draw.diffuse.empty() ? 0u : 1u,
draw.normalize_normals};
state.material_sources = {draw.diffuse_material_source, draw.ambient_material_source,
draw.emissive_material_source, draw.local_viewer};
state.alpha_test = {draw.alpha_test, draw.alpha_func, draw.alpha_ref, 0};
std::copy(draw.material_diffuse, draw.material_diffuse + 4, state.material_diffuse.begin());
std::copy(draw.material_ambient, draw.material_ambient + 4, state.material_ambient.begin());
std::copy(draw.material_emissive, draw.material_emissive + 4, state.material_emissive.begin());
state.global_ambient = unpack_argb(draw.ambient);
const float* v = draw.view;
for (int i = 0; i < 8; ++i) {
const auto& light = draw.lights[i];
const float* p = light.position;
const float* d = light.direction;
std::array<float, 3> position{}, direction{};
for (int c = 0; c < 3; ++c) {
position[c] = p[0] * v[c] + p[1] * v[4 + c] + p[2] * v[8 + c] + v[12 + c];
direction[c] = d[0] * v[c] + d[1] * v[4 + c] + d[2] * v[8 + c];
}
const float length = std::sqrt(direction[0] * direction[0] + direction[1] * direction[1] +
direction[2] * direction[2]);
if (length > 0.0f)
for (float& c : direction) c /= length;
state.light_position_type[i] = {position[0], position[1], position[2], float(light.type)};
state.light_direction_range[i] = {direction[0], direction[1], direction[2], light.range};
std::copy(light.diffuse, light.diffuse + 4, state.light_diffuse[i].begin());
std::copy(light.ambient, light.ambient + 4, state.light_ambient[i].begin());
state.light_attenuation[i] = {light.attenuation[0], light.attenuation[1], light.attenuation[2], light.falloff};
state.light_spot[i] = {std::cos(light.theta * 0.5f), std::cos(light.phi * 0.5f), 0, 0};
}
return state;
}
std::uint64_t hash_bytes(std::uint64_t hash, const void* bytes, std::size_t size) {
const auto* data = static_cast<const std::uint8_t*>(bytes);
std::size_t i = 0;
for (; i + sizeof(std::uint64_t) <= size; i += sizeof(std::uint64_t)) {
std::uint64_t word = 0;
std::memcpy(&word, data + i, sizeof(word));
hash = (hash ^ word) * 1099511628211ull;
}
for (; i < size; ++i) hash = (hash ^ data[i]) * 1099511628211ull;
hash = (hash ^ size) * 1099511628211ull;
return hash;
}
// Immediate-mode draws have no source-buffer key. For them, verify content before reusing a slot.
std::uint64_t geometry_hash(const Render3DDraw& draw) {
std::uint64_t hash = 14695981039346656037ull;
const std::uint32_t flags = (draw.lines ? 1u : 0u) | (draw.pretransformed ? 2u : 0u);
hash = hash_bytes(hash, &flags, sizeof(flags));
hash = hash_bytes(hash, draw.positions.data(), draw.positions.size() * sizeof(float));
hash = hash_bytes(hash, draw.rhw.data(), draw.rhw.size() * sizeof(float));
hash = hash_bytes(hash, draw.vertex_fog.data(), draw.vertex_fog.size() * sizeof(float));
hash = hash_bytes(hash, draw.normals.data(), draw.normals.size() * sizeof(float));
hash = hash_bytes(hash, draw.uv0.data(), draw.uv0.size() * sizeof(float));
hash = hash_bytes(hash, draw.uv1.data(), draw.uv1.size() * sizeof(float));
hash = hash_bytes(hash, draw.diffuse.data(), draw.diffuse.size() * sizeof(std::uint32_t));
return hash_bytes(hash, draw.indices.data(), draw.indices.size() * sizeof(std::uint32_t));
}
} // namespace mt_host
+75
View File
@@ -0,0 +1,75 @@
#pragma once
#include "RenderCommands3D.h"
#include <array>
#include <cstddef>
#include <cstdint>
namespace mt_host {
struct Vertex {
float position[3];
float normal[3];
float uv[2];
float color[4];
std::uint8_t joints[4];
float weights[4];
float mask_uv[2];
float rhw = 1.0f;
float vertex_fog = 1.0f;
};
struct PushConstants {
std::array<float, 16> mvp{};
// x: bone palette base + 1 (0 = not skinned), y: D3DFVF_XYZRHW, z: UI mask batch, w: unused.
std::array<float, 4> params{};
};
static_assert(sizeof(PushConstants) <= 128, "PushConstants must fit within Vulkan's 128-byte minimum guarantee");
// std430 payload selected with a dynamic storage-buffer offset for every draw: the D3D8
// fixed-function state in effect for the draw (texture stages, texture coordinate processing,
// lighting, material, fog, alpha test). A zero flags.x marks a 2D UI batch.
struct alignas(16) FixedFunctionState {
std::array<std::array<std::uint32_t, 4>, 2> stage_color{}; // op, arg1, arg2, D3DTSS_TEXCOORDINDEX
std::array<std::array<std::uint32_t, 4>, 2> stage_alpha{}; // op, arg1, arg2, D3DTSS_TEXTURETRANSFORMFLAGS
std::array<float, 4> fog_color{};
std::array<float, 4> fog_params{}; // start, end, density, range enabled
std::array<float, 4> texture_factor{};
std::array<float, 16> world_view{};
std::array<std::uint32_t, 4> flags{}; // fixed function, texture0, texture1, fog mode
std::array<float, 16> normal_matrix{}; // inverse transpose of world * view
std::array<std::uint32_t, 4> lighting_flags{}; // LIGHTING, COLORVERTEX, vertex has diffuse, NORMALIZENORMALS
std::array<std::uint32_t, 4> material_sources{}; // DIFFUSE, AMBIENT, EMISSIVE source, LOCALVIEWER
std::array<std::uint32_t, 4> alpha_test{}; // ALPHATESTENABLE, ALPHAFUNC, ALPHAREF, unused
std::array<float, 4> material_diffuse{};
std::array<float, 4> material_ambient{};
std::array<float, 4> material_emissive{};
std::array<float, 4> global_ambient{};
std::array<std::array<float, 16>, 2> texture_matrix{};
// Lights in camera space. position.w = D3DLIGHTTYPE (0 = disabled).
std::array<std::array<float, 4>, 8> light_position_type{};
std::array<std::array<float, 4>, 8> light_direction_range{};
std::array<std::array<float, 4>, 8> light_diffuse{};
std::array<std::array<float, 4>, 8> light_ambient{};
std::array<std::array<float, 4>, 8> light_attenuation{}; // a0, a1, a2, falloff
std::array<std::array<float, 4>, 8> light_spot{}; // cos(theta / 2), cos(phi / 2)
};
static_assert(sizeof(FixedFunctionState) == 1264);
inline constexpr std::array<float, 16> kIdentityMatrix = {
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f};
std::array<float, 16> multiply(const float* a, const float* b);
std::array<float, 16> draw_mvp(const Render3DDraw& draw);
std::array<float, 4> unpack_argb(std::uint32_t argb);
std::array<float, 16> normal_matrix(const std::array<float, 16>& m);
PushConstants make_push_constants(const Render3DDraw& draw, float skin_offset_encoded);
FixedFunctionState make_fixed_function_state(const Render3DDraw& draw);
std::uint64_t hash_bytes(std::uint64_t hash, const void* bytes, std::size_t size);
std::uint64_t geometry_hash(const Render3DDraw& draw);
} // namespace mt_host
+1 -1
View File
@@ -23,7 +23,7 @@ layout(push_constant) uniform DrawConstants {
vec4 params; // bone base + 1 (0 = none), pretransformed, UI mask, unused
} draw;
// D3D8 fixed-function vertex state; see FixedFunctionState in main.cpp. Lights are in camera space.
// D3D8 fixed-function vertex state; see FixedFunctionState in render_state.h. Lights are in camera space.
layout(set = 1, binding = 1, std430) readonly buffer FixedFunctionState {
uvec4 stage_color[2]; // op, arg1, arg2, D3DTSS_TEXCOORDINDEX
uvec4 stage_alpha[2]; // op, arg1, arg2, D3DTSS_TEXTURETRANSFORMFLAGS
+154
View File
@@ -0,0 +1,154 @@
#include "texture_decode.h"
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
#include "platform/PackBackend.h"
#endif
#include "stb_image.h"
#include <algorithm>
#include <cstring>
#include <stdexcept>
namespace mt_host {
mtimage::Image decode_tga(const std::uint8_t* data, std::size_t size) {
mtimage::Image out;
if (size < 18) return out;
const std::uint8_t id_len = data[0];
const std::uint8_t cmap_type = data[1];
const std::uint8_t img_type = data[2];
const std::uint32_t w = std::uint32_t(data[12]) | (std::uint32_t(data[13]) << 8);
const std::uint32_t h = std::uint32_t(data[14]) | (std::uint32_t(data[15]) << 8);
const std::uint8_t bpp = data[16];
const std::uint8_t desc = data[17];
if (cmap_type != 0 || !w || !h || w > 4096 || h > 4096) return out;
if (img_type != 2 && img_type != 3 && img_type != 10) return out;
const std::size_t bytes_per_pixel = bpp / 8;
if (bytes_per_pixel != 1 && bytes_per_pixel != 3 && bytes_per_pixel != 4) return out;
std::size_t offset = 18 + std::size_t(id_len);
if (offset > size) return out;
const std::size_t pixel_count = std::size_t(w) * std::size_t(h);
std::vector<std::uint8_t> temp(pixel_count * 4);
auto write_pixel = [&](std::size_t idx, const std::uint8_t* src) {
std::uint8_t* dst = &temp[idx * 4];
if (bytes_per_pixel == 1) {
dst[0] = dst[1] = dst[2] = src[0];
dst[3] = 255;
} else if (bytes_per_pixel == 3) {
dst[0] = src[2];
dst[1] = src[1];
dst[2] = src[0];
dst[3] = 255;
} else {
dst[0] = src[2];
dst[1] = src[1];
dst[2] = src[0];
dst[3] = src[3];
}
};
if (img_type == 2 || img_type == 3) {
if (offset + pixel_count * bytes_per_pixel > size) return out;
for (std::size_t i = 0; i < pixel_count; ++i)
write_pixel(i, data + offset + i * bytes_per_pixel);
} else if (img_type == 10) {
std::size_t i = 0;
while (i < pixel_count && offset < size) {
const std::uint8_t header = data[offset++];
const std::size_t run = (header & 0x7fu) + 1u;
if (header & 0x80u) {
if (offset + bytes_per_pixel > size) return out;
for (std::size_t r = 0; r < run && i < pixel_count; ++r, ++i)
write_pixel(i, data + offset);
offset += bytes_per_pixel;
} else {
if (offset + run * bytes_per_pixel > size) return out;
for (std::size_t r = 0; r < run && i < pixel_count; ++r, ++i) {
write_pixel(i, data + offset);
offset += bytes_per_pixel;
}
}
}
if (i != pixel_count) return out;
}
out.w = static_cast<std::uint16_t>(w);
out.h = static_cast<std::uint16_t>(h);
const bool top_origin = (desc & 0x20u) != 0;
if (top_origin) {
out.rgba = std::move(temp);
} else {
out.rgba.resize(pixel_count * 4);
const std::size_t row_bytes = std::size_t(w) * 4;
for (std::uint32_t y = 0; y < h; ++y)
std::memcpy(&out.rgba[std::size_t(y) * row_bytes], &temp[std::size_t(h - 1 - y) * row_bytes], row_bytes);
}
return out;
}
mtimage::Image decode_texture_bytes(const std::uint8_t* data, std::size_t size) {
if (!data || size < 12) return {};
if (data[0] == 'M' && data[1] == 'T' && data[2] == 'R' && data[3] == 'A') {
std::uint32_t w = 0, h = 0;
std::memcpy(&w, data + 4, 4);
std::memcpy(&h, data + 8, 4);
const std::size_t bytes = std::size_t(w) * std::size_t(h) * 4;
if (w > 0 && h > 0 && w <= 4096 && h <= 4096 && size == 12 + bytes) {
mtimage::Image out;
out.w = static_cast<std::uint16_t>(w);
out.h = static_cast<std::uint16_t>(h);
out.rgba.assign(data + 12, data + 12 + bytes);
return out;
}
return {};
}
if (data[0] == 'D' && data[1] == 'D' && data[2] == 'S' && data[3] == ' ')
return mtimage::load_dds(data, size);
auto tga = decode_tga(data, size);
if (tga.ok()) return tga;
if (size > static_cast<std::size_t>(INT_MAX)) return {};
int w = 0, h = 0, channels = 0;
stbi_uc* pixels = stbi_load_from_memory(data, static_cast<int>(size), &w, &h, &channels, 4);
if (pixels && w > 0 && h > 0 && w <= 4096 && h <= 4096) {
mtimage::Image out;
out.w = static_cast<std::uint16_t>(w);
out.h = static_cast<std::uint16_t>(h);
out.rgba.assign(pixels, pixels + std::size_t(w) * std::size_t(h) * 4);
stbi_image_free(pixels);
return out;
}
stbi_image_free(pixels);
return {};
}
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
bool read_live_pack_texture(const std::string& vpath, std::vector<std::uint8_t>& bytes) {
if (vpath.empty() || !mtpack40250::ready())
return false;
std::string norm = vpath;
for (char& ch : norm)
if (ch == '\\') ch = '/';
std::string stripped = norm;
if (stripped.size() >= 2 && stripped[1] == ':')
stripped = stripped.substr(2);
while (!stripped.empty() && stripped.front() == '/')
stripped.erase(stripped.begin());
auto lower = [](std::string s) {
for (char& ch : s)
if (ch >= 'A' && ch <= 'Z')
ch = static_cast<char>(ch - 'A' + 'a');
return s;
};
for (const std::string& candidate : {
norm, stripped, "d:/" + stripped,
lower(norm), lower(stripped), lower("d:/" + stripped)}) {
if (mtpack40250::read(candidate, bytes) && !bytes.empty())
return true;
}
return false;
}
#endif
} // namespace mt_host
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include "dxt.h"
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace mt_host {
mtimage::Image decode_tga(const std::uint8_t* data, std::size_t size);
mtimage::Image decode_texture_bytes(const std::uint8_t* data, std::size_t size);
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
bool read_live_pack_texture(const std::string& vpath, std::vector<std::uint8_t>& bytes);
#endif
} // namespace mt_host
File diff suppressed because it is too large Load Diff
+437
View File
@@ -0,0 +1,437 @@
#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