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:
co-authored by
Claude Opus 5.5
parent
a46093104c
commit
75d2dd8545
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user