layout: restructure into src/ tests/ android/ scripts/ tools/
- extension/src/{port,platform,codepage} -> src/; native_render -> src/host
(+ dxt, shaders/); libgr2 -> src/gr2; extension/third_party -> third_party
- extension/tests -> tests/port, libgr2/tests -> tests/gr2
- android-native -> android (build.sh, push-client.sh moved in)
- script -> scripts; tools/40250 -> tools/server; oracle -> tools/granny-oracle;
perf tools -> tools/perf
- all build trees under build/ (native, release, android, port-gate)
- xrender:: CMake aliases -> mt::; port-map ledger impl paths rewritten
No code changes. ctest 15/15, port_gate macos+android PASS, port_map check 0 errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
70710477cf
commit
a46093104c
@@ -0,0 +1,84 @@
|
||||
find_package(Vulkan REQUIRED)
|
||||
find_package(SDL3 CONFIG REQUIRED)
|
||||
find_program(GLSLC glslc REQUIRED)
|
||||
|
||||
set(MT_NATIVE_VERT_SPV "${CMAKE_CURRENT_BINARY_DIR}/native.vert.spv")
|
||||
set(MT_NATIVE_FRAG_SPV "${CMAKE_CURRENT_BINARY_DIR}/native.frag.spv")
|
||||
add_custom_command(OUTPUT "${MT_NATIVE_VERT_SPV}"
|
||||
COMMAND "${GLSLC}" -fshader-stage=vert "${CMAKE_CURRENT_SOURCE_DIR}/shaders/native.vert" -o "${MT_NATIVE_VERT_SPV}"
|
||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/shaders/native.vert")
|
||||
add_custom_command(OUTPUT "${MT_NATIVE_FRAG_SPV}"
|
||||
COMMAND "${GLSLC}" -fshader-stage=frag "${CMAKE_CURRENT_SOURCE_DIR}/shaders/native.frag" -o "${MT_NATIVE_FRAG_SPV}"
|
||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/shaders/native.frag")
|
||||
add_custom_target(mt_native_shaders DEPENDS "${MT_NATIVE_VERT_SPV}" "${MT_NATIVE_FRAG_SPV}")
|
||||
|
||||
set(MT_NATIVE_RENDER_SOURCES
|
||||
main.cpp
|
||||
stb_image_impl.cpp
|
||||
dxt.cpp
|
||||
)
|
||||
if(TARGET port_platform AND TARGET mtpython)
|
||||
list(APPEND MT_NATIVE_RENDER_SOURCES
|
||||
"${PROJECT_SOURCE_DIR}/tests/port/port_login_flow_server.cpp"
|
||||
"${PROJECT_SOURCE_DIR}/tests/port/classic/classic_cipher.cpp"
|
||||
)
|
||||
endif()
|
||||
|
||||
if(ANDROID)
|
||||
# SDLActivity loads libmain.so; Android does not launch a native executable.
|
||||
set(MT_NATIVE_TARGET main)
|
||||
add_library(${MT_NATIVE_TARGET} SHARED ${MT_NATIVE_RENDER_SOURCES})
|
||||
else()
|
||||
set(MT_NATIVE_TARGET mt_native_render)
|
||||
add_executable(${MT_NATIVE_TARGET} ${MT_NATIVE_RENDER_SOURCES})
|
||||
endif()
|
||||
if(APPLE)
|
||||
set_target_properties(${MT_NATIVE_TARGET} PROPERTIES
|
||||
OSX_ARCHITECTURES "${CMAKE_HOST_SYSTEM_PROCESSOR}"
|
||||
)
|
||||
endif()
|
||||
add_dependencies(${MT_NATIVE_TARGET} mt_native_shaders)
|
||||
target_include_directories(${MT_NATIVE_TARGET} PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/third_party"
|
||||
"${PROJECT_SOURCE_DIR}/src"
|
||||
"${PROJECT_SOURCE_DIR}/src/platform/EterLib")
|
||||
if(NOT ANDROID)
|
||||
add_custom_command(TARGET ${MT_NATIVE_TARGET} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MT_NATIVE_VERT_SPV}" "$<TARGET_FILE_DIR:${MT_NATIVE_TARGET}>/native.vert.spv"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MT_NATIVE_FRAG_SPV}" "$<TARGET_FILE_DIR:${MT_NATIVE_TARGET}>/native.frag.spv")
|
||||
endif()
|
||||
target_link_libraries(${MT_NATIVE_TARGET} PRIVATE Vulkan::Vulkan SDL3::SDL3)
|
||||
if(APPLE)
|
||||
target_link_libraries(${MT_NATIVE_TARGET} PRIVATE
|
||||
"-framework AudioToolbox")
|
||||
endif()
|
||||
if(TARGET port_platform AND TARGET mtpython)
|
||||
target_compile_definitions(${MT_NATIVE_TARGET} PRIVATE MT_NATIVE_HAS_LIVE_CLIENT=1)
|
||||
target_link_libraries(${MT_NATIVE_TARGET} PRIVATE port_platform)
|
||||
if(TARGET mtpython_stdlib)
|
||||
add_dependencies(${MT_NATIVE_TARGET} mtpython_stdlib)
|
||||
if(NOT ANDROID)
|
||||
add_custom_command(TARGET ${MT_NATIVE_TARGET} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MT_PYTHON_STDLIB_ZIP}" "$<TARGET_FILE_DIR:${MT_NATIVE_TARGET}>/python27.zip")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(ANDROID)
|
||||
# Add this directory to the SDL Android app's assets.srcDirs.
|
||||
set(MT_NATIVE_ANDROID_ASSETS "${CMAKE_CURRENT_BINARY_DIR}/android-assets")
|
||||
add_custom_target(mt_native_android_assets ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${MT_NATIVE_ANDROID_ASSETS}"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${MT_NATIVE_VERT_SPV}" "${MT_NATIVE_ANDROID_ASSETS}/native.vert.spv"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${MT_NATIVE_FRAG_SPV}" "${MT_NATIVE_ANDROID_ASSETS}/native.frag.spv"
|
||||
DEPENDS mt_native_shaders)
|
||||
if(TARGET mtpython_stdlib)
|
||||
add_dependencies(mt_native_android_assets mtpython_stdlib)
|
||||
add_custom_command(TARGET mt_native_android_assets POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${MT_PYTHON_STDLIB_ZIP}" "${MT_NATIVE_ANDROID_ASSETS}/python27.zip")
|
||||
endif()
|
||||
endif()
|
||||
@@ -0,0 +1,151 @@
|
||||
# Native Vulkan client
|
||||
|
||||
This is the standalone renderer path for the 40250 `Render3DDraw` and `UIRenderCommand` command streams, plus direct `--live-client` execution of the ported 40250 client (`PythonBoot`). SDL3 owns the window and input forwarding; Vulkan owns the swapchain (`FIFO` or `IMMEDIATE`/`MAILBOX`), `VK_FORMAT_D32_SFLOAT` depth buffer, `VK_QUERY_TYPE_TIMESTAMP` hardware GPU timer, key-indexed persistent vertex/index buffers, GPU skeletal skinning bone palette SSBO (`set = 1, binding = 0`), DDS/TGA/`mem:` glyph-page texture sampler descriptors, fixed-function 3D + 2D UI state pipelines, and draw submission. On macOS, the Vulkan loader uses MoltenVK over Metal. This is the active macOS and Android client path.
|
||||
|
||||
## Build and run on macOS
|
||||
|
||||
Install Vulkan headers/loader, MoltenVK, SDL3 and `glslc` (shaderc). With Homebrew:
|
||||
|
||||
```sh
|
||||
brew install vulkan-headers vulkan-loader molten-vk sdl3 shaderc
|
||||
|
||||
# Optimized Release build (includes port_platform, embedded Python and FakeLoginServer)
|
||||
cmake -S . -B build/release -DCMAKE_BUILD_TYPE=Release -DMT_BUILD_NATIVE_RENDER=ON \
|
||||
-DMT_EMBED_PYTHON=ON -DCMAKE_PREFIX_PATH=/opt/homebrew
|
||||
cmake --build build/release --target mt_native_render -j8
|
||||
```
|
||||
|
||||
On macOS, `mt_native_render` automatically detects `/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json` (or `/usr/local/etc/vulkan/icd.d/MoltenVK_icd.json`) when `VK_ICD_FILENAMES` is not set in the environment.
|
||||
The build copies `native.vert.spv`, `native.frag.spv`, and, for the live client,
|
||||
`python27.zip` beside the executable. Keep these files together when moving the
|
||||
binary. In a macOS `.app`, put them in `Contents/Resources` (the directory
|
||||
returned by SDL's `SDL_GetBasePath`). `MT_PYTHON_STDLIB` can override the zip path.
|
||||
|
||||
## Android integration
|
||||
|
||||
`./android/build.sh [Debug|Release] [--install]` builds the arm64 APK
|
||||
(`android/app/build/outputs/apk/debug/app-debug.apk`, package
|
||||
`org.metin2port.client`): it downloads SDL 3.4.16 (sha256-checked) into
|
||||
`build/android/_deps`, builds `libSDL3.so`, cross-builds `libmain.so`
|
||||
(this target + `port_platform` + embedded Python) at API 24, and wraps both in the
|
||||
SDLActivity shell under `android/` (`MainActivity` passes the launch intent's
|
||||
`args` extra as argv). `mt_native_android_assets` supplies `native.vert.spv`,
|
||||
`native.frag.spv` and `python27.zip` as APK assets.
|
||||
|
||||
The 40250 `Client` directory is not packaged. `./android/push-client.sh` copies it
|
||||
to the app's `files/Client` (`SDL_GetPrefPath`) with `run-as`, so it needs the
|
||||
debug-signed APK installed first. Without arguments the app runs against the
|
||||
in-process fake server; for a real server:
|
||||
|
||||
```sh
|
||||
adb shell am start -n org.metin2port.client/.MainActivity \
|
||||
--es args "--live-server HOST:AUTH_PORT:GAME_PORT --login-screen"
|
||||
```
|
||||
|
||||
Code page conversion does not use iconv: `src/codepage` carries Microsoft's
|
||||
WindowsBestFit tables for 874/932/936/949/950/1250-1258 (regenerate with
|
||||
`python3 scripts/gen_codepage_tables.py`), so Android converts the same as macOS.
|
||||
|
||||
The Vulkan portability enumeration extension is selected only when advertised
|
||||
by the loader. DDS, TGA, and memory textures retain their existing decoders;
|
||||
JPEG, PNG, and BMP use the same portable decoder on macOS and Android. The
|
||||
vendored `stb_image.h` is upstream v2.30 (SHA-256
|
||||
`594c2fe35d49488b4382dbfaec8f98366defca819d916ac95becf3e75f4200b3`).
|
||||
|
||||
### Interactive Playable Modes
|
||||
|
||||
Run the full 40250 client interactively (infinite frame loop until window close, resizable SDL3 window with automatic Vulkan swapchain recreation and `PythonBoot::SetUISize` sync, full keyboard/IME text input, SDL hardware cursor built from the original cursor images, and SDL3 + `AudioToolbox` `.wav`/`.mp3` audio):
|
||||
|
||||
```sh
|
||||
# Interactive outdoor map session (auto-login via loopback FakeLoginServer)
|
||||
./build/release/src/host/mt_native_render \
|
||||
--live-client "/path/to/40250/Server Client TMP4/Client" \
|
||||
--interactive --fake-mobs 24 --width 1280 --height 800
|
||||
|
||||
# Interactive login screen (stops at introLogin.LoginWindow for manual typing/login)
|
||||
./build/release/src/host/mt_native_render \
|
||||
--live-client "/path/to/40250/Server Client TMP4/Client" \
|
||||
--login-screen --width 1024 --height 768
|
||||
|
||||
# Connect to an external 40250 Auth + Game server
|
||||
./build/release/src/host/mt_native_render \
|
||||
--live-client "/path/to/40250/Server Client TMP4/Client" \
|
||||
--live-server 127.0.0.1:11002:13000 --login-screen
|
||||
```
|
||||
|
||||
### Synthetic benchmark
|
||||
|
||||
```sh
|
||||
./build/release/src/host/mt_native_render --frames 60 --draws 64 --triangles-per-draw 333 --no-vsync
|
||||
```
|
||||
|
||||
Timing metrics printed by `mt_native_render`:
|
||||
- `p95_frame_ms`, `p99_frame_ms`, `max_frame_ms`: wall-clock time for each measured update/render iteration, excluding startup and final GPU drain. These include vsync wait when enabled; use them alongside `gpu_ms` and the CPU breakdown.
|
||||
- `game_update_ms`: mean CPU time spent in `PythonBoot::UIUpdate()`, `PythonBoot::UIRender()`, and audio command draining per frame in `--live-client` mode.
|
||||
- `prepare_ms`: mean CPU draw-preparation time across all frames (including frame 0 cold-start geometry/texture uploads and pipeline creation).
|
||||
- `steady_prepare_ms`: mean CPU draw-preparation time on frames after frame 0 (bone palette copy, UI quad batching, command recording).
|
||||
- `submit_ms`: host CPU time around `vkQueueSubmit` (in `FIFO` mode this includes swapchain backpressure; pass `--no-vsync` to switch to `IMMEDIATE`/`MAILBOX`).
|
||||
- `gpu_ms`: true hardware GPU execution time between top-of-pipe `vkCmdBeginRenderPass` and bottom-of-pipe `vkCmdEndRenderPass` measured via `VK_QUERY_TYPE_TIMESTAMP`.
|
||||
|
||||
### macOS real-server acceptance
|
||||
|
||||
Build the Release live-client target above, then start the evidence runner from the repository root:
|
||||
|
||||
```sh
|
||||
# First verify the runner and renderer using the local fake server.
|
||||
node scripts/native_mac_acceptance.mjs --fake --frames 180
|
||||
|
||||
# Use the real server's shared host, auth port, and game channel port.
|
||||
node scripts/native_mac_acceptance.mjs --server HOST:AUTH_PORT:GAME_PORT
|
||||
```
|
||||
|
||||
Real-server mode checks both ports before starting, opens the native login screen,
|
||||
and collects a redacted client log, one-second process RSS samples, and a JSON
|
||||
report under `build/native-acceptance/`. Enter credentials in the app, then
|
||||
exercise login, character selection, movement, combat, map changes, inventory,
|
||||
chat, window resize/focus, and visual comparison with the current client. Close
|
||||
the window after at least 30 minutes. The report records whether that minimum
|
||||
was met, but remains `NEEDS_MANUAL_REVIEW` until those actions and visual results
|
||||
are checked by a person. It does not assert that RSS alone proves no GPU leak.
|
||||
|
||||
`--live-server` currently accepts one shared host for the auth and game ports.
|
||||
If those endpoints use different hosts, update the native connection setup
|
||||
before claiming a real-server pass. Credentials are entered in the client UI;
|
||||
the runner never puts them in arguments or the report.
|
||||
|
||||
## Run the real 40250 client benchmark in native Vulkan (`--live-client`)
|
||||
|
||||
When built with `port_platform`, `mt_native_render` boots `system.py`, logs in via loopback `FakeLoginServer`, enters the outdoor map with 1..64 monsters, renders the complete 3D scene (40250 hardware-transform terrain splats, animated water patches, gradient skybox & scrolling clouds, SpeedTree forest bark/leaf geometry, GPU-skinned characters with stage-1 specular sphere-maps, and 2D UI/minimap/text-tails/software cursor), and optionally writes a Version 5 `.mtdr` capture:
|
||||
|
||||
```sh
|
||||
./build/release/src/host/mt_native_render \
|
||||
--live-client "/path/to/40250/Server Client TMP4/Client" \
|
||||
--fake-mobs 64 --frames 180 --gpu-skinning --no-vsync \
|
||||
--capture-out /tmp/mt_full_64mobs.mtdr
|
||||
```
|
||||
|
||||
Compare against CPU skinning (`GrannyDeformVertices` on CPU + per-frame vertex buffer re-uploads) with `--no-gpu-skinning`:
|
||||
|
||||
```sh
|
||||
./build/release/src/host/mt_native_render \
|
||||
--live-client "/path/to/40250/Server Client TMP4/Client" \
|
||||
--fake-mobs 64 --frames 180 --no-gpu-skinning --no-vsync
|
||||
```
|
||||
|
||||
## Replay a captured frame (`.mtdr` v1 / v2 / v3 / v4 / v5)
|
||||
|
||||
```sh
|
||||
./build/release/src/host/mt_native_render \
|
||||
--capture /tmp/mt_full_64mobs.mtdr --frames 180 --animate-bones --no-vsync
|
||||
```
|
||||
|
||||
- Pass `--animate-bones` to animate the bone palette SSBO each frame without re-uploading any vertex buffers (`uploads` stays equal to unique static geometries uploaded on frame 0).
|
||||
- Pass `--animate-first-draw` to increment the first draw's `geometry_revision` each frame after frame 0 and verify incremental GPU buffer re-uploads.
|
||||
|
||||
Capture format Version 5 (backward-compatible with Versions 1–4) stores:
|
||||
1. 3D draws (`Render3DDraw`): `geometry_key`, `geometry_revision`, matrices, positions, normals, UVs, diffuse colors, indices, D3D8 fixed-function states, `texture0` / `texture1` names, and GPU skinning data (`bone_indices`, `bone_weights`, `bone_matrices`).
|
||||
2. Self-contained textures: `.dds`, `.tga`, `.jpg`, `.png`, and `.bmp` pack bytes plus `"MTRA"` raw RGBA memory textures (`mem:<id>@<revision>` font glyph pages).
|
||||
3. 2D UI stream (`UIRenderCommand`): canvas size (`ui_width`, `ui_height`) and all `Bar`, `GradientBar`, `Line`, and `Image` commands (including `behind_3d`, clip rects, minimap mask UV coordinates, and software mouse cursor quads).
|
||||
4. Per-draw fog color, vertex/table mode, range flag, start/end distance and density. Older captures omit these fields and replay without fog.
|
||||
|
||||
The native shader now evaluates recorded D3D8 stage 0/1 color and alpha operations, including the original cloud operation (`D3DTOP_MODULATEINVALPHA_ADDCOLOR = 20`), and applies the captured linear or exponential fog. Expanded UI image modes use the 40250 blend factors for screen/color-dodge and modulate. These state fixes do not by themselves establish pixel parity with a Windows 40250 screenshot; compare the same map, time, camera and UI state before treating a color difference as resolved.
|
||||
@@ -0,0 +1,206 @@
|
||||
#pragma once
|
||||
// Android frame-rate and CPU-clock plumbing for the live client's render loop.
|
||||
//
|
||||
// - display_refresh_rate(): the panel's refresh rate now (MainActivity.currentRefreshRate), and
|
||||
// render_rate(): the app-vsync rate the system actually gives the app (MainActivity.currentRenderRate),
|
||||
// for the perf log's refresh_hz / render_hz columns.
|
||||
// - request_frame_rate(): ANativeWindow_setFrameRate (API 30), the surface's preferred rate. The display
|
||||
// mode itself is chosen by MainActivity (--refresh-rate -> preferredDisplayModeId); the vendor
|
||||
// (ColorOS etc.) can still cap an app it does not know.
|
||||
// - PerformanceHint: ADPF APerformanceHint (API 33). The loop reports each frame's CPU work against the
|
||||
// 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.
|
||||
// - 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
|
||||
// meaningful off the charger (USB adb counts as a charger: test over wireless adb).
|
||||
//
|
||||
// Every NDK entry point is looked up at run time: minSdk is 24. On other platforms all of this is a no-op.
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#ifdef __ANDROID__
|
||||
#include <android/native_window.h>
|
||||
#include <dlfcn.h>
|
||||
#include <jni.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace android_perf {
|
||||
|
||||
#ifdef __ANDROID__
|
||||
inline void* libandroid() {
|
||||
static void* lib = dlopen("libandroid.so", RTLD_NOW);
|
||||
return lib;
|
||||
}
|
||||
|
||||
inline double call_activity_float(const char* name) {
|
||||
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
|
||||
auto activity = static_cast<jobject>(SDL_GetAndroidActivity());
|
||||
if (!env || !activity) return -1.0;
|
||||
double hz = -1.0;
|
||||
jclass cls = env->GetObjectClass(activity);
|
||||
if (jmethodID method = env->GetMethodID(cls, name, "()F"))
|
||||
hz = env->CallFloatMethod(activity, method);
|
||||
if (env->ExceptionCheck()) {
|
||||
env->ExceptionClear();
|
||||
hz = -1.0;
|
||||
}
|
||||
env->DeleteLocalRef(cls);
|
||||
env->DeleteLocalRef(activity);
|
||||
return hz;
|
||||
}
|
||||
|
||||
inline void call_activity_set_float(const char* name, float value) {
|
||||
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, name, "(F)V"))
|
||||
env->CallVoidMethod(activity, method, jfloat(value));
|
||||
if (env->ExceptionCheck()) env->ExceptionClear();
|
||||
env->DeleteLocalRef(cls);
|
||||
env->DeleteLocalRef(activity);
|
||||
}
|
||||
|
||||
inline double display_refresh_rate() { return call_activity_float("currentRefreshRate"); }
|
||||
// The highest refresh rate the display offers at its current resolution; -1 if unknown.
|
||||
inline double max_refresh_rate() { return call_activity_float("maxRefreshRate"); }
|
||||
// Switches the display mode to the refresh rate closest to `hz` (MainActivity.preferredDisplayModeId).
|
||||
inline void set_display_refresh_rate(float hz) { call_activity_set_float("setDisplayRefreshRate", hz); }
|
||||
// The app-vsync rate (Choreographer); -1 until the first measurement.
|
||||
inline double render_rate() { return call_activity_float("currentRenderRate"); }
|
||||
inline double battery_celsius() { return call_activity_float("batteryTemperature"); }
|
||||
|
||||
struct BatteryPower {
|
||||
double current_ma = 0; // drawn from the battery, positive while discharging
|
||||
double voltage_mv = -1;
|
||||
double power_mw = -1; // current x voltage; -1 while plugged in or unknown
|
||||
bool plugged = false;
|
||||
};
|
||||
|
||||
// BatteryManager's CURRENT_NOW is uA by the docs but mA on some vendors, and its sign convention varies:
|
||||
// a game draws well over 20 mA, so a magnitude above 20000 can only be uA; unplugged, the whole current is
|
||||
// the draw. Plugged in, the charger supplies part of it and the reading says nothing about the game.
|
||||
inline BatteryPower battery_power() {
|
||||
BatteryPower out;
|
||||
const double raw = call_activity_float("batteryCurrentRaw");
|
||||
out.current_ma = std::fabs(raw) > 20000.0 ? std::fabs(raw) / 1000.0 : std::fabs(raw);
|
||||
out.voltage_mv = call_activity_float("batteryVoltage");
|
||||
out.plugged = call_activity_float("batteryPlugged") > 0.5;
|
||||
if (!out.plugged && out.current_ma > 0 && out.voltage_mv > 0)
|
||||
out.power_mw = out.current_ma * out.voltage_mv / 1000.0;
|
||||
return out;
|
||||
}
|
||||
|
||||
// false when the call is unavailable (API < 30) or rejected.
|
||||
inline bool request_frame_rate(SDL_Window* window, float fps) {
|
||||
using SetFrameRate = int32_t (*)(ANativeWindow*, float, int8_t);
|
||||
static const auto set_frame_rate =
|
||||
libandroid() ? reinterpret_cast<SetFrameRate>(dlsym(libandroid(), "ANativeWindow_setFrameRate")) : nullptr;
|
||||
if (!set_frame_rate || !window) return false;
|
||||
auto* native = static_cast<ANativeWindow*>(SDL_GetPointerProperty(
|
||||
SDL_GetWindowProperties(window), SDL_PROP_WINDOW_ANDROID_WINDOW_POINTER, nullptr));
|
||||
if (!native) return false;
|
||||
// ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_DEFAULT: a game, not fixed-rate video.
|
||||
return set_frame_rate(native, fps, 0) == 0;
|
||||
}
|
||||
|
||||
class PerformanceHint {
|
||||
public:
|
||||
// `thread_ids`: the threads that do each frame's work (render thread + script thread).
|
||||
bool open(const std::vector<int32_t>& thread_ids, std::int64_t target_ns) {
|
||||
void* lib = libandroid();
|
||||
if (!lib || thread_ids.empty()) return false;
|
||||
get_manager_ = reinterpret_cast<GetManager>(dlsym(lib, "APerformanceHint_getManager"));
|
||||
create_ = reinterpret_cast<CreateSession>(dlsym(lib, "APerformanceHint_createSession"));
|
||||
update_target_ = reinterpret_cast<UpdateTarget>(dlsym(lib, "APerformanceHint_updateTargetWorkDuration"));
|
||||
report_ = reinterpret_cast<Report>(dlsym(lib, "APerformanceHint_reportActualWorkDuration"));
|
||||
close_ = reinterpret_cast<Close>(dlsym(lib, "APerformanceHint_closeSession"));
|
||||
prefer_efficiency_ = reinterpret_cast<PreferEfficiency>(
|
||||
dlsym(lib, "APerformanceHint_setPreferPowerEfficiency"));
|
||||
if (!get_manager_ || !create_ || !report_ || !close_) return false;
|
||||
void* manager = get_manager_();
|
||||
if (!manager) return false;
|
||||
session_ = create_(manager, thread_ids.data(), thread_ids.size(), target_ns);
|
||||
target_ns_ = target_ns;
|
||||
return session_ != nullptr;
|
||||
}
|
||||
|
||||
~PerformanceHint() {
|
||||
if (session_ && close_) close_(session_);
|
||||
}
|
||||
|
||||
bool active() const { return session_ != nullptr; }
|
||||
|
||||
void set_target(std::int64_t target_ns) {
|
||||
if (!session_ || !update_target_ || target_ns == target_ns_ || target_ns <= 0) return;
|
||||
update_target_(session_, target_ns);
|
||||
target_ns_ = target_ns;
|
||||
}
|
||||
|
||||
void report(std::int64_t actual_ns) {
|
||||
if (session_ && actual_ns > 0) report_(session_, actual_ns);
|
||||
}
|
||||
|
||||
// API 35: the scheduler may favour efficiency cores and lower clocks for the session's threads.
|
||||
// false when unavailable.
|
||||
bool prefer_power_efficiency(bool enabled) {
|
||||
if (!session_ || !prefer_efficiency_) return false;
|
||||
if (enabled == prefer_efficiency_on_) return true;
|
||||
prefer_efficiency_on_ = enabled;
|
||||
return prefer_efficiency_(session_, enabled) == 0;
|
||||
}
|
||||
|
||||
private:
|
||||
using GetManager = void* (*)();
|
||||
using CreateSession = void* (*)(void*, const int32_t*, size_t, int64_t);
|
||||
using UpdateTarget = int (*)(void*, int64_t);
|
||||
using Report = int (*)(void*, int64_t);
|
||||
using Close = void (*)(void*);
|
||||
using PreferEfficiency = int (*)(void*, bool);
|
||||
GetManager get_manager_ = nullptr;
|
||||
CreateSession create_ = nullptr;
|
||||
UpdateTarget update_target_ = nullptr;
|
||||
Report report_ = nullptr;
|
||||
Close close_ = nullptr;
|
||||
PreferEfficiency prefer_efficiency_ = nullptr;
|
||||
bool prefer_efficiency_on_ = false;
|
||||
void* session_ = nullptr;
|
||||
std::int64_t target_ns_ = 0;
|
||||
};
|
||||
|
||||
inline int32_t current_thread_id() { return int32_t(gettid()); }
|
||||
#else
|
||||
inline double display_refresh_rate() {
|
||||
const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(SDL_GetPrimaryDisplay());
|
||||
return mode ? double(mode->refresh_rate) : -1.0;
|
||||
}
|
||||
inline double render_rate() { return display_refresh_rate(); }
|
||||
inline double max_refresh_rate() { return display_refresh_rate(); }
|
||||
inline void set_display_refresh_rate(float) {}
|
||||
inline double battery_celsius() { return -1.0; }
|
||||
struct BatteryPower {
|
||||
double current_ma = 0, voltage_mv = -1, power_mw = -1;
|
||||
bool plugged = false;
|
||||
};
|
||||
inline BatteryPower battery_power() { return {}; }
|
||||
inline bool request_frame_rate(SDL_Window*, float) { return false; }
|
||||
class PerformanceHint {
|
||||
public:
|
||||
bool open(const std::vector<int32_t>&, std::int64_t) { return false; }
|
||||
bool active() const { return false; }
|
||||
void set_target(std::int64_t) {}
|
||||
void report(std::int64_t) {}
|
||||
bool prefer_power_efficiency(bool) { return false; }
|
||||
};
|
||||
inline int32_t current_thread_id() { return 0; }
|
||||
#endif
|
||||
|
||||
} // namespace android_perf
|
||||
@@ -0,0 +1,417 @@
|
||||
#pragma once
|
||||
|
||||
#include "../platform/EterLib/RenderCommands3D.h"
|
||||
#include "../platform/EterLib/UIRenderCommands.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
// Diagnostic, little-endian arm64 format:
|
||||
// - Version 1: matrices, positions, diffuse, indices.
|
||||
// - Version 2: adds stable geometry_key and geometry_revision.
|
||||
// - Version 3: adds normals, UVs, D3D8 fixed-function states, and embedded pack texture bytes.
|
||||
// - Version 4: adds GPU skeletal skinning streams (bone_indices, bone_weights, bone_matrices)
|
||||
// and 2D UIRenderCommand stream + memory textures ("MTRA").
|
||||
// - Version 5: adds per-draw fog color, mode, range and distance/density state.
|
||||
// - Version 6: adds texture address and filtering state for both stages.
|
||||
// - Version 7: adds two fixed-function lights and material color sources.
|
||||
// - Version 8: eight lights, back-buffer clear entries, viewport MinZ/MaxZ, border color,
|
||||
// TEXCOORDINDEX / TEXTURETRANSFORMFLAGS / D3DTS_TEXTUREn per stage, emissive
|
||||
// material source, NORMALIZENORMALS and LOCALVIEWER; UVs are raw vertex sets.
|
||||
// - Version 9: adds XYZRHW vertex fog (specular alpha).
|
||||
// Capture is opt-in and never runs in the normal game path.
|
||||
namespace native_draw_capture {
|
||||
|
||||
struct Capture {
|
||||
std::uint32_t version = 10;
|
||||
std::vector<Render3DDraw> draws;
|
||||
std::unordered_map<std::string, std::vector<std::uint8_t>> textures;
|
||||
std::uint32_t ui_width = 960;
|
||||
std::uint32_t ui_height = 640;
|
||||
std::vector<UIRenderCommand> ui_commands;
|
||||
};
|
||||
|
||||
inline std::vector<std::uint8_t> encode_raw_argb_as_mtra(
|
||||
std::uint32_t width,
|
||||
std::uint32_t height,
|
||||
const std::uint32_t* argb) {
|
||||
std::vector<std::uint8_t> out;
|
||||
if (!width || !height || !argb)
|
||||
return out;
|
||||
const std::size_t pixel_count = std::size_t(width) * std::size_t(height);
|
||||
out.resize(12 + pixel_count * 4);
|
||||
out[0] = 'M'; out[1] = 'T'; out[2] = 'R'; out[3] = 'A';
|
||||
std::memcpy(out.data() + 4, &width, 4);
|
||||
std::memcpy(out.data() + 8, &height, 4);
|
||||
std::uint8_t* dst = out.data() + 12;
|
||||
for (std::size_t i = 0; i < pixel_count; ++i) {
|
||||
const std::uint32_t c = argb[i];
|
||||
dst[i * 4 + 0] = static_cast<std::uint8_t>((c >> 16) & 0xffu);
|
||||
dst[i * 4 + 1] = static_cast<std::uint8_t>((c >> 8) & 0xffu);
|
||||
dst[i * 4 + 2] = static_cast<std::uint8_t>(c & 0xffu);
|
||||
dst[i * 4 + 3] = static_cast<std::uint8_t>((c >> 24) & 0xffu);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
template <typename T> void write_scalar(std::ofstream& file, const T& value) {
|
||||
file.write(reinterpret_cast<const char*>(&value), sizeof(value));
|
||||
}
|
||||
|
||||
template <typename T> void read_scalar(std::ifstream& file, T& value) {
|
||||
file.read(reinterpret_cast<char*>(&value), sizeof(value));
|
||||
if (!file) throw std::runtime_error("truncated native draw capture");
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void write_vector(std::ofstream& file, const std::vector<T>& values, std::size_t max_count = 4'000'000) {
|
||||
if (values.size() > max_count) throw std::runtime_error("native draw capture array too large");
|
||||
const auto count = static_cast<std::uint32_t>(values.size());
|
||||
write_scalar(file, count);
|
||||
if (count) file.write(reinterpret_cast<const char*>(values.data()), count * sizeof(T));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void read_vector(std::ifstream& file, std::vector<T>& values, std::size_t max_count = 4'000'000) {
|
||||
std::uint32_t count = 0;
|
||||
read_scalar(file, count);
|
||||
if (count > max_count) throw std::runtime_error("native draw capture array too large");
|
||||
values.resize(count);
|
||||
if (count) file.read(reinterpret_cast<char*>(values.data()), count * sizeof(T));
|
||||
if (!file) throw std::runtime_error("truncated native draw capture array");
|
||||
}
|
||||
|
||||
inline void write_string(std::ofstream& file, const std::string& value) {
|
||||
if (value.size() > 4096) throw std::runtime_error("native draw capture string too large");
|
||||
const auto size = static_cast<std::uint32_t>(value.size());
|
||||
write_scalar(file, size);
|
||||
if (size) file.write(value.data(), size);
|
||||
}
|
||||
|
||||
inline void read_string(std::ifstream& file, std::string& value) {
|
||||
std::uint32_t size = 0;
|
||||
read_scalar(file, size);
|
||||
if (size > 4096) throw std::runtime_error("native draw capture string too large");
|
||||
value.resize(size);
|
||||
if (size) file.read(value.data(), size);
|
||||
if (!file) throw std::runtime_error("truncated native draw capture string");
|
||||
}
|
||||
|
||||
inline void write(
|
||||
const std::string& path,
|
||||
const std::vector<Render3DDraw>& draws,
|
||||
const std::unordered_map<std::string, std::vector<std::uint8_t>>& textures = {},
|
||||
std::uint32_t ui_width = 960,
|
||||
std::uint32_t ui_height = 640,
|
||||
const std::vector<UIRenderCommand>& ui_commands = {}) {
|
||||
if (draws.size() > 10'000 || textures.size() > 8'192 || ui_commands.size() > 100'000)
|
||||
throw std::runtime_error("native draw capture has too many draws, textures, or UI commands");
|
||||
std::ofstream file(path, std::ios::binary | std::ios::trunc);
|
||||
if (!file) throw std::runtime_error("cannot create native draw capture: " + path);
|
||||
const std::uint32_t magic = 0x4d544452; // MTDR
|
||||
const std::uint32_t version = 10;
|
||||
const auto count = static_cast<std::uint32_t>(draws.size());
|
||||
write_scalar(file, magic); write_scalar(file, version); write_scalar(file, count);
|
||||
for (const auto& draw : draws) {
|
||||
file.write(reinterpret_cast<const char*>(draw.world), sizeof(draw.world));
|
||||
file.write(reinterpret_cast<const char*>(draw.view), sizeof(draw.view));
|
||||
file.write(reinterpret_cast<const char*>(draw.proj), sizeof(draw.proj));
|
||||
write_scalar(file, draw.geometry_key);
|
||||
write_scalar(file, draw.geometry_revision);
|
||||
const std::uint32_t flags =
|
||||
(draw.lines ? 1u : 0u) | (draw.pretransformed ? 2u : 0u) | (draw.light0 ? 4u : 0u);
|
||||
write_scalar(file, flags);
|
||||
write_vector(file, draw.positions);
|
||||
write_vector(file, draw.diffuse);
|
||||
write_vector(file, draw.indices);
|
||||
file.write(reinterpret_cast<const char*>(draw.viewport), sizeof(draw.viewport));
|
||||
write_string(file, draw.texture0);
|
||||
write_string(file, draw.texture1);
|
||||
write_vector(file, draw.rhw);
|
||||
write_vector(file, draw.normals);
|
||||
write_vector(file, draw.uv0);
|
||||
write_vector(file, draw.uv1);
|
||||
write_scalar(file, draw.alpha_blend);
|
||||
write_scalar(file, draw.src_blend);
|
||||
write_scalar(file, draw.dest_blend);
|
||||
write_scalar(file, draw.alpha_test);
|
||||
write_scalar(file, draw.alpha_ref);
|
||||
write_scalar(file, draw.alpha_func);
|
||||
write_scalar(file, draw.cull_mode);
|
||||
write_scalar(file, draw.z_enable);
|
||||
write_scalar(file, draw.z_write);
|
||||
write_scalar(file, draw.z_func);
|
||||
write_scalar(file, draw.lighting);
|
||||
write_scalar(file, draw.texture_factor);
|
||||
write_scalar(file, draw.fog_enable);
|
||||
file.write(reinterpret_cast<const char*>(draw.color_op), sizeof(draw.color_op));
|
||||
file.write(reinterpret_cast<const char*>(draw.color_arg1), sizeof(draw.color_arg1));
|
||||
file.write(reinterpret_cast<const char*>(draw.color_arg2), sizeof(draw.color_arg2));
|
||||
file.write(reinterpret_cast<const char*>(draw.alpha_op), sizeof(draw.alpha_op));
|
||||
file.write(reinterpret_cast<const char*>(draw.alpha_arg1), sizeof(draw.alpha_arg1));
|
||||
file.write(reinterpret_cast<const char*>(draw.alpha_arg2), sizeof(draw.alpha_arg2));
|
||||
file.write(reinterpret_cast<const char*>(draw.material_diffuse), sizeof(draw.material_diffuse));
|
||||
file.write(reinterpret_cast<const char*>(draw.material_ambient), sizeof(draw.material_ambient));
|
||||
file.write(reinterpret_cast<const char*>(draw.material_emissive), sizeof(draw.material_emissive));
|
||||
file.write(reinterpret_cast<const char*>(draw.light0_direction), sizeof(draw.light0_direction));
|
||||
file.write(reinterpret_cast<const char*>(draw.light0_diffuse), sizeof(draw.light0_diffuse));
|
||||
file.write(reinterpret_cast<const char*>(draw.light0_ambient), sizeof(draw.light0_ambient));
|
||||
write_scalar(file, draw.ambient);
|
||||
write_scalar(file, draw.fog_color);
|
||||
write_scalar(file, draw.fog_vertex_mode);
|
||||
write_scalar(file, draw.fog_table_mode);
|
||||
write_scalar(file, draw.fog_range_enable);
|
||||
write_scalar(file, draw.fog_start);
|
||||
write_scalar(file, draw.fog_end);
|
||||
write_scalar(file, draw.fog_density);
|
||||
file.write(reinterpret_cast<const char*>(draw.address_u), sizeof(draw.address_u));
|
||||
file.write(reinterpret_cast<const char*>(draw.address_v), sizeof(draw.address_v));
|
||||
file.write(reinterpret_cast<const char*>(draw.min_filter), sizeof(draw.min_filter));
|
||||
file.write(reinterpret_cast<const char*>(draw.mag_filter), sizeof(draw.mag_filter));
|
||||
file.write(reinterpret_cast<const char*>(draw.mip_filter), sizeof(draw.mip_filter));
|
||||
file.write(reinterpret_cast<const char*>(draw.lights), sizeof(draw.lights));
|
||||
write_scalar(file, draw.diffuse_material_source);
|
||||
write_scalar(file, draw.ambient_material_source);
|
||||
write_scalar(file, draw.color_vertex);
|
||||
file.write(reinterpret_cast<const char*>(draw.viewport_z), sizeof(draw.viewport_z));
|
||||
write_scalar(file, draw.clear_flags);
|
||||
write_scalar(file, draw.clear_color);
|
||||
write_scalar(file, draw.clear_z);
|
||||
file.write(reinterpret_cast<const char*>(draw.border_color), sizeof(draw.border_color));
|
||||
file.write(reinterpret_cast<const char*>(draw.texcoord_index), sizeof(draw.texcoord_index));
|
||||
file.write(reinterpret_cast<const char*>(draw.texture_transform_flags), sizeof(draw.texture_transform_flags));
|
||||
file.write(reinterpret_cast<const char*>(draw.texture_matrix), sizeof(draw.texture_matrix));
|
||||
write_scalar(file, draw.emissive_material_source);
|
||||
write_scalar(file, draw.normalize_normals);
|
||||
write_scalar(file, draw.local_viewer);
|
||||
write_vector(file, draw.vertex_fog);
|
||||
write_vector(file, draw.bone_indices);
|
||||
write_vector(file, draw.bone_weights);
|
||||
write_vector(file, draw.bone_matrices);
|
||||
write_scalar(file, draw.render_target);
|
||||
write_scalar(file, draw.target_width);
|
||||
write_scalar(file, draw.target_height);
|
||||
}
|
||||
const auto texture_count = static_cast<std::uint32_t>(textures.size());
|
||||
write_scalar(file, texture_count);
|
||||
for (const auto& [name, bytes] : textures) {
|
||||
write_string(file, name);
|
||||
write_vector(file, bytes, 16'777'216);
|
||||
}
|
||||
write_scalar(file, ui_width);
|
||||
write_scalar(file, ui_height);
|
||||
const auto ui_count = static_cast<std::uint32_t>(ui_commands.size());
|
||||
write_scalar(file, ui_count);
|
||||
for (const auto& cmd : ui_commands) {
|
||||
const auto kind = static_cast<std::uint32_t>(cmd.kind);
|
||||
write_scalar(file, kind);
|
||||
write_scalar(file, cmd.x1);
|
||||
write_scalar(file, cmd.y1);
|
||||
write_scalar(file, cmd.x2);
|
||||
write_scalar(file, cmd.y2);
|
||||
write_scalar(file, cmd.argb);
|
||||
write_scalar(file, cmd.end_argb);
|
||||
write_scalar(file, cmd.clip_x1);
|
||||
write_scalar(file, cmd.clip_y1);
|
||||
write_scalar(file, cmd.clip_x2);
|
||||
write_scalar(file, cmd.clip_y2);
|
||||
write_string(file, cmd.text);
|
||||
const std::uint32_t uiflags = (cmd.quad ? 1u : 0u) | (cmd.behind_3d ? 2u : 0u);
|
||||
write_scalar(file, uiflags);
|
||||
file.write(reinterpret_cast<const char*>(cmd.qx), sizeof(cmd.qx));
|
||||
file.write(reinterpret_cast<const char*>(cmd.qy), sizeof(cmd.qy));
|
||||
write_scalar(file, cmd.su);
|
||||
write_scalar(file, cmd.sv);
|
||||
write_scalar(file, cmd.eu);
|
||||
write_scalar(file, cmd.ev);
|
||||
write_scalar(file, cmd.blend);
|
||||
write_string(file, cmd.mask);
|
||||
file.write(reinterpret_cast<const char*>(cmd.mu), sizeof(cmd.mu));
|
||||
file.write(reinterpret_cast<const char*>(cmd.mv), sizeof(cmd.mv));
|
||||
}
|
||||
if (!file) throw std::runtime_error("failed to write native draw capture: " + path);
|
||||
}
|
||||
|
||||
inline Capture read_capture(const std::string& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) throw std::runtime_error("cannot open native draw capture: " + path);
|
||||
std::uint32_t magic = 0, version = 0, count = 0;
|
||||
read_scalar(file, magic); read_scalar(file, version); read_scalar(file, count);
|
||||
if (magic != 0x4d544452 || (version < 1 || version > 10) || count > 10'000)
|
||||
throw std::runtime_error("unsupported native draw capture format");
|
||||
Capture capture;
|
||||
capture.version = version;
|
||||
capture.draws.resize(count);
|
||||
for (auto& draw : capture.draws) {
|
||||
file.read(reinterpret_cast<char*>(draw.world), sizeof(draw.world));
|
||||
file.read(reinterpret_cast<char*>(draw.view), sizeof(draw.view));
|
||||
file.read(reinterpret_cast<char*>(draw.proj), sizeof(draw.proj));
|
||||
if (!file) throw std::runtime_error("truncated native draw capture matrices");
|
||||
if (version >= 2) {
|
||||
read_scalar(file, draw.geometry_key);
|
||||
read_scalar(file, draw.geometry_revision);
|
||||
}
|
||||
std::uint32_t flags = 0;
|
||||
read_scalar(file, flags);
|
||||
draw.lines = (flags & 1u) != 0;
|
||||
draw.pretransformed = (flags & 2u) != 0;
|
||||
draw.light0 = (flags & 4u) != 0;
|
||||
read_vector(file, draw.positions);
|
||||
read_vector(file, draw.diffuse);
|
||||
read_vector(file, draw.indices);
|
||||
if (version >= 3) {
|
||||
file.read(reinterpret_cast<char*>(draw.viewport), sizeof(draw.viewport));
|
||||
read_string(file, draw.texture0);
|
||||
read_string(file, draw.texture1);
|
||||
read_vector(file, draw.rhw);
|
||||
read_vector(file, draw.normals);
|
||||
read_vector(file, draw.uv0);
|
||||
read_vector(file, draw.uv1);
|
||||
read_scalar(file, draw.alpha_blend);
|
||||
read_scalar(file, draw.src_blend);
|
||||
read_scalar(file, draw.dest_blend);
|
||||
read_scalar(file, draw.alpha_test);
|
||||
read_scalar(file, draw.alpha_ref);
|
||||
read_scalar(file, draw.alpha_func);
|
||||
read_scalar(file, draw.cull_mode);
|
||||
read_scalar(file, draw.z_enable);
|
||||
read_scalar(file, draw.z_write);
|
||||
read_scalar(file, draw.z_func);
|
||||
read_scalar(file, draw.lighting);
|
||||
read_scalar(file, draw.texture_factor);
|
||||
read_scalar(file, draw.fog_enable);
|
||||
file.read(reinterpret_cast<char*>(draw.color_op), sizeof(draw.color_op));
|
||||
file.read(reinterpret_cast<char*>(draw.color_arg1), sizeof(draw.color_arg1));
|
||||
file.read(reinterpret_cast<char*>(draw.color_arg2), sizeof(draw.color_arg2));
|
||||
file.read(reinterpret_cast<char*>(draw.alpha_op), sizeof(draw.alpha_op));
|
||||
file.read(reinterpret_cast<char*>(draw.alpha_arg1), sizeof(draw.alpha_arg1));
|
||||
file.read(reinterpret_cast<char*>(draw.alpha_arg2), sizeof(draw.alpha_arg2));
|
||||
file.read(reinterpret_cast<char*>(draw.material_diffuse), sizeof(draw.material_diffuse));
|
||||
file.read(reinterpret_cast<char*>(draw.material_ambient), sizeof(draw.material_ambient));
|
||||
file.read(reinterpret_cast<char*>(draw.material_emissive), sizeof(draw.material_emissive));
|
||||
file.read(reinterpret_cast<char*>(draw.light0_direction), sizeof(draw.light0_direction));
|
||||
file.read(reinterpret_cast<char*>(draw.light0_diffuse), sizeof(draw.light0_diffuse));
|
||||
file.read(reinterpret_cast<char*>(draw.light0_ambient), sizeof(draw.light0_ambient));
|
||||
read_scalar(file, draw.ambient);
|
||||
if (version >= 5) {
|
||||
read_scalar(file, draw.fog_color);
|
||||
read_scalar(file, draw.fog_vertex_mode);
|
||||
read_scalar(file, draw.fog_table_mode);
|
||||
read_scalar(file, draw.fog_range_enable);
|
||||
read_scalar(file, draw.fog_start);
|
||||
read_scalar(file, draw.fog_end);
|
||||
read_scalar(file, draw.fog_density);
|
||||
}
|
||||
if (version >= 6) {
|
||||
file.read(reinterpret_cast<char*>(draw.address_u), sizeof(draw.address_u));
|
||||
file.read(reinterpret_cast<char*>(draw.address_v), sizeof(draw.address_v));
|
||||
file.read(reinterpret_cast<char*>(draw.min_filter), sizeof(draw.min_filter));
|
||||
file.read(reinterpret_cast<char*>(draw.mag_filter), sizeof(draw.mag_filter));
|
||||
file.read(reinterpret_cast<char*>(draw.mip_filter), sizeof(draw.mip_filter));
|
||||
}
|
||||
if (version >= 7) {
|
||||
file.read(reinterpret_cast<char*>(draw.lights),
|
||||
(version >= 8 ? 8 : 2) * sizeof(draw.lights[0]));
|
||||
read_scalar(file, draw.diffuse_material_source);
|
||||
read_scalar(file, draw.ambient_material_source);
|
||||
read_scalar(file, draw.color_vertex);
|
||||
}
|
||||
if (version >= 8) {
|
||||
file.read(reinterpret_cast<char*>(draw.viewport_z), sizeof(draw.viewport_z));
|
||||
read_scalar(file, draw.clear_flags);
|
||||
read_scalar(file, draw.clear_color);
|
||||
read_scalar(file, draw.clear_z);
|
||||
file.read(reinterpret_cast<char*>(draw.border_color), sizeof(draw.border_color));
|
||||
file.read(reinterpret_cast<char*>(draw.texcoord_index), sizeof(draw.texcoord_index));
|
||||
file.read(reinterpret_cast<char*>(draw.texture_transform_flags), sizeof(draw.texture_transform_flags));
|
||||
file.read(reinterpret_cast<char*>(draw.texture_matrix), sizeof(draw.texture_matrix));
|
||||
read_scalar(file, draw.emissive_material_source);
|
||||
read_scalar(file, draw.normalize_normals);
|
||||
read_scalar(file, draw.local_viewer);
|
||||
if (version >= 9) read_vector(file, draw.vertex_fog);
|
||||
} else if (draw.light0) {
|
||||
auto& light = draw.lights[0];
|
||||
light.type = 3;
|
||||
std::memcpy(light.direction, draw.light0_direction, sizeof(light.direction));
|
||||
std::memcpy(light.diffuse, draw.light0_diffuse, sizeof(light.diffuse));
|
||||
std::memcpy(light.ambient, draw.light0_ambient, sizeof(light.ambient));
|
||||
}
|
||||
if (!file) throw std::runtime_error("truncated native draw capture state");
|
||||
} else {
|
||||
draw.z_enable = 1;
|
||||
draw.z_write = 1;
|
||||
}
|
||||
if (version >= 4) {
|
||||
read_vector(file, draw.bone_indices);
|
||||
read_vector(file, draw.bone_weights);
|
||||
read_vector(file, draw.bone_matrices);
|
||||
}
|
||||
if (version >= 10) {
|
||||
read_scalar(file, draw.render_target);
|
||||
read_scalar(file, draw.target_width);
|
||||
read_scalar(file, draw.target_height);
|
||||
}
|
||||
}
|
||||
if (version >= 3) {
|
||||
std::uint32_t texture_count = 0;
|
||||
read_scalar(file, texture_count);
|
||||
if (texture_count > 8'192) throw std::runtime_error("native draw capture has too many textures");
|
||||
for (std::uint32_t i = 0; i < texture_count; ++i) {
|
||||
std::string name;
|
||||
std::vector<std::uint8_t> bytes;
|
||||
read_string(file, name);
|
||||
read_vector(file, bytes, 16'777'216);
|
||||
capture.textures.emplace(std::move(name), std::move(bytes));
|
||||
}
|
||||
}
|
||||
if (version >= 4) {
|
||||
read_scalar(file, capture.ui_width);
|
||||
read_scalar(file, capture.ui_height);
|
||||
std::uint32_t ui_count = 0;
|
||||
read_scalar(file, ui_count);
|
||||
if (ui_count > 100'000) throw std::runtime_error("native draw capture has too many UI commands");
|
||||
capture.ui_commands.resize(ui_count);
|
||||
for (auto& cmd : capture.ui_commands) {
|
||||
std::uint32_t kind = 0, uiflags = 0;
|
||||
read_scalar(file, kind);
|
||||
cmd.kind = static_cast<UIRenderCommand::Kind>(kind);
|
||||
read_scalar(file, cmd.x1);
|
||||
read_scalar(file, cmd.y1);
|
||||
read_scalar(file, cmd.x2);
|
||||
read_scalar(file, cmd.y2);
|
||||
read_scalar(file, cmd.argb);
|
||||
read_scalar(file, cmd.end_argb);
|
||||
read_scalar(file, cmd.clip_x1);
|
||||
read_scalar(file, cmd.clip_y1);
|
||||
read_scalar(file, cmd.clip_x2);
|
||||
read_scalar(file, cmd.clip_y2);
|
||||
read_string(file, cmd.text);
|
||||
read_scalar(file, uiflags);
|
||||
cmd.quad = (uiflags & 1u) != 0;
|
||||
cmd.behind_3d = (uiflags & 2u) != 0;
|
||||
file.read(reinterpret_cast<char*>(cmd.qx), sizeof(cmd.qx));
|
||||
file.read(reinterpret_cast<char*>(cmd.qy), sizeof(cmd.qy));
|
||||
read_scalar(file, cmd.su);
|
||||
read_scalar(file, cmd.sv);
|
||||
read_scalar(file, cmd.eu);
|
||||
read_scalar(file, cmd.ev);
|
||||
read_scalar(file, cmd.blend);
|
||||
read_string(file, cmd.mask);
|
||||
file.read(reinterpret_cast<char*>(cmd.mu), sizeof(cmd.mu));
|
||||
file.read(reinterpret_cast<char*>(cmd.mv), sizeof(cmd.mv));
|
||||
if (!file) throw std::runtime_error("truncated native draw capture UI command");
|
||||
}
|
||||
}
|
||||
return capture;
|
||||
}
|
||||
|
||||
inline std::vector<Render3DDraw> read(const std::string& path) {
|
||||
return read_capture(path).draws;
|
||||
}
|
||||
|
||||
} // namespace native_draw_capture
|
||||
@@ -0,0 +1,212 @@
|
||||
// engine/dxt —— DDS(DXT1/3/5) + 未压缩 BGRA8 → RGBA8 软解。M1 T3。
|
||||
#include "dxt.h"
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace mtimage {
|
||||
namespace {
|
||||
|
||||
inline uint32_t rd_le32(const uint8_t* p) {
|
||||
return uint32_t(p[0]) | (uint32_t(p[1]) << 8) | (uint32_t(p[2]) << 16) | (uint32_t(p[3]) << 24);
|
||||
}
|
||||
|
||||
// 5:6:5 → r,g,b (0..255)
|
||||
inline void unpack565(uint16_t c, int& r, int& g, int& b) {
|
||||
r = ((c >> 11) & 0x1f); r = (r << 3) | (r >> 2);
|
||||
g = ((c >> 5) & 0x3f); g = (g << 2) | (g >> 4);
|
||||
b = (c & 0x1f); b = (b << 3) | (b >> 2);
|
||||
}
|
||||
|
||||
// 解一个 DXT1 颜色块(8B)→ 16 像素 RGB(不写 alpha;dxt1_alpha=true 时按 1-bit alpha 写)
|
||||
void decode_color_block(const uint8_t* blk, uint8_t out[16][4], bool dxt1_alpha) {
|
||||
uint16_t c0 = uint16_t(blk[0] | (blk[1] << 8));
|
||||
uint16_t c1 = uint16_t(blk[2] | (blk[3] << 8));
|
||||
int r[4], g[4], b[4], a[4] = {255, 255, 255, 255};
|
||||
unpack565(c0, r[0], g[0], b[0]);
|
||||
unpack565(c1, r[1], g[1], b[1]);
|
||||
if (c0 > c1 || !dxt1_alpha) {
|
||||
r[2] = (2 * r[0] + r[1]) / 3; g[2] = (2 * g[0] + g[1]) / 3; b[2] = (2 * b[0] + b[1]) / 3;
|
||||
r[3] = (r[0] + 2 * r[1]) / 3; g[3] = (g[0] + 2 * g[1]) / 3; b[3] = (b[0] + 2 * b[1]) / 3;
|
||||
} else {
|
||||
r[2] = (r[0] + r[1]) / 2; g[2] = (g[0] + g[1]) / 2; b[2] = (b[0] + b[1]) / 2;
|
||||
r[3] = g[3] = b[3] = 0; a[3] = 0; // 透明
|
||||
}
|
||||
uint32_t bits = rd_le32(blk + 4);
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
int idx = (bits >> (i * 2)) & 3;
|
||||
out[i][0] = uint8_t(r[idx]); out[i][1] = uint8_t(g[idx]);
|
||||
out[i][2] = uint8_t(b[idx]); out[i][3] = uint8_t(a[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
// DXT3:alpha 块(8B)= 16 个 4-bit alpha,直接展开
|
||||
void decode_dxt3_alpha(const uint8_t* blk, uint8_t out[16][4]) {
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
int a0 = blk[i] & 0x0f, a1 = (blk[i] >> 4) & 0x0f;
|
||||
out[i * 2 + 0][3] = uint8_t(a0 * 17); // 0..15 → 0..255
|
||||
out[i * 2 + 1][3] = uint8_t(a1 * 17);
|
||||
}
|
||||
}
|
||||
|
||||
// DXT5:alpha 块(8B)= 2 端点 + 16×3-bit 索引
|
||||
void decode_dxt5_alpha(const uint8_t* blk, uint8_t out[16][4]) {
|
||||
int a0 = blk[0], a1 = blk[1];
|
||||
int a[8];
|
||||
a[0] = a0; a[1] = a1;
|
||||
if (a0 > a1) {
|
||||
for (int i = 1; i < 7; ++i) a[i + 1] = ((7 - i) * a0 + i * a1) / 7;
|
||||
} else {
|
||||
for (int i = 1; i < 5; ++i) a[i + 1] = ((5 - i) * a0 + i * a1) / 5;
|
||||
a[6] = 0; a[7] = 255;
|
||||
}
|
||||
uint64_t bits = 0;
|
||||
for (int i = 0; i < 6; ++i) bits |= uint64_t(blk[2 + i]) << (8 * i);
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
int idx = int((bits >> (i * 3)) & 7);
|
||||
out[i][3] = uint8_t(a[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
enum Fmt { F_NONE, F_DXT1, F_DXT3, F_DXT5, F_RGB };
|
||||
|
||||
bool valid_mask(uint32_t mask, uint32_t bits) {
|
||||
if (!mask || (bits < 32 && (mask >> bits))) return false;
|
||||
while (!(mask & 1)) mask >>= 1;
|
||||
return (mask & (mask + 1)) == 0; // contiguous channel bits
|
||||
}
|
||||
|
||||
uint8_t channel(uint32_t pixel, uint32_t mask) {
|
||||
if (!mask) return 255;
|
||||
while (!(mask & 1)) { mask >>= 1; pixel >>= 1; }
|
||||
return uint8_t((uint64_t(pixel & mask) * 255 + mask / 2) / mask);
|
||||
}
|
||||
|
||||
void decode_dxt_level(Fmt fmt, const uint8_t* src, uint32_t w, uint32_t h, uint8_t* rgba) {
|
||||
const int block_bytes = (fmt == F_DXT1) ? 8 : 16;
|
||||
const size_t bx = (w + 3) / 4, by = (h + 3) / 4;
|
||||
for (size_t byi = 0; byi < by; ++byi) {
|
||||
for (size_t bxi = 0; bxi < bx; ++bxi) {
|
||||
const uint8_t* blk = src + (byi * bx + bxi) * block_bytes;
|
||||
uint8_t px[16][4];
|
||||
if (fmt == F_DXT1) {
|
||||
decode_color_block(blk, px, /*dxt1_alpha=*/true);
|
||||
} else {
|
||||
decode_color_block(blk + 8, px, /*dxt1_alpha=*/false);
|
||||
if (fmt == F_DXT3) decode_dxt3_alpha(blk, px);
|
||||
else decode_dxt5_alpha(blk, px);
|
||||
}
|
||||
for (int py = 0; py < 4; ++py) {
|
||||
size_t y = byi * 4 + py;
|
||||
if (y >= h) break;
|
||||
for (int pxx = 0; pxx < 4; ++pxx) {
|
||||
size_t x = bxi * 4 + pxx;
|
||||
if (x >= w) break;
|
||||
uint8_t* o = &rgba[(y * w + x) * 4];
|
||||
const uint8_t* s = px[py * 4 + pxx];
|
||||
o[0] = s[0]; o[1] = s[1]; o[2] = s[2]; o[3] = s[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image decode(const uint8_t* d, size_t len) {
|
||||
Image img;
|
||||
if (len < 128 || std::memcmp(d, "DDS ", 4) != 0) return img;
|
||||
uint32_t hsize = rd_le32(d + 4);
|
||||
if (hsize != 124) return img;
|
||||
uint32_t h = rd_le32(d + 12);
|
||||
uint32_t w = rd_le32(d + 16);
|
||||
uint32_t pf_flags = rd_le32(d + 80);
|
||||
const uint8_t* fourcc = d + 84;
|
||||
uint32_t rgb_bitcount = rd_le32(d + 88);
|
||||
|
||||
Fmt fmt = F_NONE;
|
||||
if (pf_flags & 0x4) { // DDPF_FOURCC
|
||||
if (!std::memcmp(fourcc, "DXT1", 4)) fmt = F_DXT1;
|
||||
else if (!std::memcmp(fourcc, "DXT3", 4)) fmt = F_DXT3;
|
||||
else if (!std::memcmp(fourcc, "DXT5", 4)) fmt = F_DXT5;
|
||||
} else if ((pf_flags & 0x40) && (rgb_bitcount == 16 || rgb_bitcount == 24 || rgb_bitcount == 32)) {
|
||||
fmt = F_RGB;
|
||||
}
|
||||
if (fmt == F_NONE || w == 0 || h == 0 || w > 8192 || h > 8192) return img;
|
||||
|
||||
const uint8_t* src = d + 128;
|
||||
size_t avail = len - 128;
|
||||
img.w = uint16_t(w);
|
||||
img.h = uint16_t(h);
|
||||
img.rgba.assign(size_t(w) * h * 4, 0);
|
||||
|
||||
if (fmt == F_RGB) {
|
||||
const uint32_t r = rd_le32(d + 92), g = rd_le32(d + 96), b = rd_le32(d + 100);
|
||||
const uint32_t a = (pf_flags & 1) ? rd_le32(d + 104) : 0;
|
||||
if (!valid_mask(r, rgb_bitcount) || !valid_mask(g, rgb_bitcount) || !valid_mask(b, rgb_bitcount) ||
|
||||
(a && !valid_mask(a, rgb_bitcount)) || ((pf_flags & 1) && !a) ||
|
||||
(r & g) || (r & b) || (g & b) || (a & (r | g | b))) return {};
|
||||
const size_t bytes = rgb_bitcount / 8, row_bytes = size_t(w) * bytes;
|
||||
const size_t pitch = (rd_le32(d + 8) & 8) ? rd_le32(d + 20) : row_bytes;
|
||||
if (pitch < row_bytes || avail < row_bytes || pitch > avail || (h - 1) > (avail - row_bytes) / pitch) return {};
|
||||
for (size_t y = 0; y < h; ++y) {
|
||||
for (size_t x = 0; x < w; ++x) {
|
||||
uint32_t pixel = 0;
|
||||
for (size_t k = 0; k < bytes; ++k) pixel |= uint32_t(src[y * pitch + x * bytes + k]) << (8 * k);
|
||||
auto* out = &img.rgba[(y * w + x) * 4];
|
||||
out[0] = channel(pixel, r); out[1] = channel(pixel, g);
|
||||
out[2] = channel(pixel, b); out[3] = channel(pixel, a);
|
||||
}
|
||||
}
|
||||
img.format = rgb_bitcount == 32 && r == 0xff0000 && b == 0xff ? "BGRA8" : "RGB_MASKED";
|
||||
return img;
|
||||
}
|
||||
|
||||
const int block_bytes = (fmt == F_DXT1) ? 8 : 16;
|
||||
const size_t bx = (w + 3) / 4, by = (h + 3) / 4;
|
||||
if (avail < bx * by * block_bytes) { img = Image{}; return img; }
|
||||
decode_dxt_level(fmt, src, uint32_t(w), uint32_t(h), img.rgba.data());
|
||||
|
||||
// 40250 CDXTCImage::LoadHeaderFromMemory + CGraphicImageTexture::CreateDDSTexture:
|
||||
// mipmapCount = max(1, min(dwMipMapCount, MAX_MIPLEVELS)); level i starts at
|
||||
// sum(dwLinearSize >> 2k) and Copy() memcpy's dwLinearSize >> 2i bytes of it. Only
|
||||
// with DDSD_MIPMAPCOUNT and without DDSD_PITCH are levels > 0 filled — otherwise 40250
|
||||
// creates them uninitialized; here that case is one level.
|
||||
const uint32_t dds_flags = rd_le32(d + 8);
|
||||
const uint32_t linear_size = rd_le32(d + 20);
|
||||
uint32_t mip_count = std::min<uint32_t>(rd_le32(d + 28), 12u);
|
||||
if (mip_count == 0) mip_count = 1;
|
||||
if (!(dds_flags & 0x20000u) || (dds_flags & 0x8u) || linear_size == 0) mip_count = 1;
|
||||
size_t offset = linear_size;
|
||||
for (uint32_t i = 1; i < mip_count; ++i) {
|
||||
const uint32_t lw = std::max<uint32_t>(1, w >> i), lh = std::max<uint32_t>(1, h >> i);
|
||||
const size_t level_bytes = ((lw + 3) / 4) * ((lh + 3) / 4) * size_t(block_bytes);
|
||||
const size_t copy_bytes = std::min<size_t>(size_t(linear_size) >> (2 * i), level_bytes);
|
||||
std::vector<uint8_t> blocks(level_bytes, 0);
|
||||
if (offset < avail) std::memcpy(blocks.data(), src + offset, std::min(copy_bytes, avail - offset));
|
||||
offset += size_t(linear_size) >> (2 * i);
|
||||
std::vector<uint8_t> level(size_t(lw) * lh * 4, 0);
|
||||
decode_dxt_level(fmt, blocks.data(), lw, lh, level.data());
|
||||
img.mips.push_back(std::move(level));
|
||||
}
|
||||
img.file_mip_levels = uint8_t(mip_count);
|
||||
img.format = (fmt == F_DXT1) ? "DXT1" : (fmt == F_DXT3) ? "DXT3" : "DXT5";
|
||||
return img;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Image load_dds(const uint8_t* bytes, size_t len) { return decode(bytes, len); }
|
||||
|
||||
Image load_dds_path(const char* path) {
|
||||
FILE* f = std::fopen(path, "rb");
|
||||
if (!f) return {};
|
||||
std::fseek(f, 0, SEEK_END);
|
||||
long n = std::ftell(f);
|
||||
std::fseek(f, 0, SEEK_SET);
|
||||
std::vector<uint8_t> buf(n > 0 ? size_t(n) : 0);
|
||||
size_t rd = buf.empty() ? 0 : std::fread(buf.data(), 1, buf.size(), f);
|
||||
std::fclose(f);
|
||||
if (rd != buf.size()) return {};
|
||||
return decode(buf.data(), buf.size());
|
||||
}
|
||||
|
||||
} // namespace mtimage
|
||||
@@ -0,0 +1,27 @@
|
||||
// dxt.{h,cpp} — ported verbatim from xrender-poc/engine/dxt.{h,cpp}
|
||||
// (namespace engine -> mtimage). DDS DXT1/3/5 + masked 16/24/32-bit RGB -> RGBA8, level 0.
|
||||
// engine/dxt —— DDS(DXT1/3/5) → RGBA8 软解。见 docs/steps/M1-static-render.md T3
|
||||
// 自己写 ~150 行绕开 reuse/EterImageLib 的 windows.h 依赖(M1 风险表允许)。
|
||||
// 对拍时两侧都喂软解 RGBA,不被 GPU S3TC 的 bit 级差异污染。
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace mtimage {
|
||||
|
||||
struct Image {
|
||||
uint16_t w = 0, h = 0;
|
||||
std::vector<uint8_t> rgba; // w*h*4,level 0
|
||||
// DXT DDS only: level count 40250 CreateDDSTexture gives the texture (0 = not a DXT
|
||||
// DDS, caller follows the D3DX_DEFAULT full-chain path) and levels 1.. decoded to RGBA8.
|
||||
uint8_t file_mip_levels = 0;
|
||||
std::vector<std::vector<uint8_t>> mips;
|
||||
const char* format = ""; // "DXT1" / "DXT3" / "DXT5" / "BGRA8" / ""
|
||||
bool ok() const { return w && h && rgba.size() == size_t(w) * h * 4; }
|
||||
};
|
||||
|
||||
// 读整个 .dds 文件字节,解 level 0 到 RGBA8(DXT 另解文件内 mip 到 mips)。失败返回 !ok()。
|
||||
Image load_dds(const uint8_t* bytes, size_t len);
|
||||
Image load_dds_path(const char* path);
|
||||
|
||||
} // namespace mtimage
|
||||
+4528
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,461 @@
|
||||
#pragma once
|
||||
// Performance telemetry for the live client (--perf-log or MT_PERF_LOG=1).
|
||||
//
|
||||
// Every frame the host hands PerfLog the frame's wall times and the renderer's running totals; the
|
||||
// script thread's section times come from platform/EterBase/PerfCounters.h. Every interval (2 s) one CSV
|
||||
// row with the window's per-frame averages is appended to perf-YYYYMMDD-HHMMSS.csv and a short summary
|
||||
// goes to logcat (stderr on desktop). A frame over the hitch threshold additionally writes a "# hitch"
|
||||
// comment line with that frame's own breakdown.
|
||||
//
|
||||
// Files: Android <external files dir>/perf (/sdcard/Android/data/<package>/files/perf, pulled with
|
||||
// tools/perf/pull_perf_logs.sh), desktop $MT_PERF_DIR or ./perf. The newest kKeepFiles files are kept.
|
||||
|
||||
#include "platform/EterBase/PerfCounters.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__linux__) || defined(__ANDROID__)
|
||||
#include <sched.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#ifdef __ANDROID__
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
namespace native_perf {
|
||||
|
||||
// Running totals the renderer keeps (VulkanWindow::Timings and its upload counters); PerfLog differences
|
||||
// them itself, so they never need resetting.
|
||||
struct RendererTotals {
|
||||
double sync_ms = 0, fence_ms = 0, prepare_ms = 0, geometry_upload_ms = 0, texture_upload_ms = 0;
|
||||
double submit_ms = 0, present_ms = 0, gpu_ms = 0;
|
||||
std::uint64_t gpu_samples = 0;
|
||||
std::uint64_t uploads = 0, uploaded_bytes = 0, texture_uploads = 0, texture_bytes = 0;
|
||||
// The last frame's counts.
|
||||
std::uint64_t draws = 0, skinned_draws = 0, ui_batches = 0, vertices = 0;
|
||||
const std::string* last_texture = nullptr; // name of the texture uploaded last
|
||||
};
|
||||
|
||||
// One battery reading (android_perf::battery_power). power_mw < 0: unknown or on a charger.
|
||||
struct PowerSample {
|
||||
double current_ma = 0, voltage_mv = -1, power_mw = -1;
|
||||
bool plugged = false;
|
||||
};
|
||||
|
||||
struct FrameInput {
|
||||
double frame_ms = 0; // whole loop iteration
|
||||
double app_ms = 0; // PythonBoot::UIUpdate: the script thread's Process() plus the baton handoff
|
||||
double host_ms = 0; // cursor sync, UIRender, audio pump
|
||||
double render_ms = 0; // VulkanWindow::render
|
||||
double pace_ms = 0; // sleep of the frame-rate cap (--fps-cap), outside frame_ms
|
||||
RendererTotals renderer;
|
||||
};
|
||||
|
||||
class PerfLog {
|
||||
public:
|
||||
static constexpr double kIntervalS = 2.0;
|
||||
static constexpr double kHitchMs = 100.0;
|
||||
static constexpr int kKeepFiles = 10;
|
||||
static constexpr double kPowerSampleS = 0.25;
|
||||
|
||||
static bool requested_by_env() {
|
||||
const char* env = std::getenv("MT_PERF_LOG");
|
||||
return env && *env && std::string(env) != "0";
|
||||
}
|
||||
|
||||
bool enabled() const { return file_ != nullptr; }
|
||||
// Display refresh rate in Hz as the OS reports it now (Android Display.getRefreshRate), sampled
|
||||
// once per row; -1 when unknown.
|
||||
void set_refresh_rate_source(std::function<double()> source) { refresh_rate_ = std::move(source); }
|
||||
// The rate the system actually drives the app at (Android Choreographer); can be a divisor of refresh_hz.
|
||||
void set_render_rate_source(std::function<double()> source) { render_rate_ = std::move(source); }
|
||||
// Battery temperature in C (Android: the ACTION_BATTERY_CHANGED sticky intent; sysfs is SELinux-denied).
|
||||
void set_battery_source(std::function<double()> source) { battery_ = std::move(source); }
|
||||
// Sampled every kPowerSampleS and averaged over the row: one reading swings with each frame's burst.
|
||||
void set_power_source(std::function<PowerSample()> source) { power_ = std::move(source); }
|
||||
void set_fps_cap(int cap) { fps_cap_ = cap; }
|
||||
// The frame-rate setting (FrameRateMode.h) and whether the idle rate is in effect, as of the row's end.
|
||||
void set_frame_policy(int mode, bool idle) { fps_mode_ = mode; idle_ = idle; }
|
||||
const std::string& path() const { return path_; }
|
||||
|
||||
// `context` goes into the file header (device, present mode, MSAA, size...).
|
||||
bool open(const std::string& context) {
|
||||
const std::string dir = directory();
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(dir, ec);
|
||||
prune(dir);
|
||||
char stamp[32];
|
||||
const std::time_t now = std::time(nullptr);
|
||||
std::tm local{};
|
||||
localtime_r(&now, &local);
|
||||
std::strftime(stamp, sizeof(stamp), "%Y%m%d-%H%M%S", &local);
|
||||
path_ = dir + "/perf-" + stamp + ".csv";
|
||||
file_ = std::fopen(path_.c_str(), "w");
|
||||
if (!file_) {
|
||||
log("perf log: cannot write %s", path_.c_str());
|
||||
return false;
|
||||
}
|
||||
std::fprintf(file_, "# mt_native_render perf log %s\n# %s\n", stamp, context.c_str());
|
||||
std::fprintf(file_, "# per-frame averages over each %.0f s window; *_ms game sections run on the script thread "
|
||||
"inside app_ms; py_ms includes update_game/render_game\n", kIntervalS);
|
||||
std::fprintf(file_,
|
||||
"t_s,map,actors,frames,fps,refresh_hz,render_hz,fps_cap,frame_ms,frame_p95_ms,frame_max_ms,jank33,jank50,"
|
||||
"app_ms,process_ms,handoff_ms,net_ms,camera_ms,ui_update_ms,update_game_ms,render_begin_ms,ui_render_ms,"
|
||||
"render_game_ms,shadow_raster_ms,py_ms,py_calls,py_missing,host_ms,render_ms,pace_ms,sync_ms,fence_ms,"
|
||||
"prepare_ms,geo_upload_ms,tex_upload_ms,"
|
||||
"submit_ms,present_ms,gpu_ms,draws,skinned_draws,ui_batches,vertices,geo_uploads,geo_kb,tex_uploads,tex_kb,"
|
||||
"script_cpu,main_cpu,cpu_mhz,cpu_max_mhz,thermal,batt_c,rss_mb,batt_ma,batt_mv,power_mw,plugged,fps_mode,idle,"
|
||||
"last_texture\n");
|
||||
std::fflush(file_);
|
||||
MtPerf::Enabled() = true;
|
||||
MtPerf::Take();
|
||||
start_ = window_start_ = std::chrono::steady_clock::now();
|
||||
log("perf log: %s", path_.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
~PerfLog() {
|
||||
if (file_) std::fclose(file_);
|
||||
MtPerf::Enabled() = false;
|
||||
}
|
||||
|
||||
// Call once per frame. `map_name` is only asked for when a row is written.
|
||||
template <typename MapName>
|
||||
void frame(const FrameInput& in, MapName&& map_name) {
|
||||
if (!file_) return;
|
||||
const MtPerf::SCounters game = MtPerf::Take();
|
||||
const RendererTotals& r = in.renderer;
|
||||
const RendererTotals& p = have_prev_ ? prev_ : r;
|
||||
Window f;
|
||||
f.frames = 1;
|
||||
f.frame_ms = in.frame_ms;
|
||||
f.app_ms = in.app_ms;
|
||||
f.host_ms = in.host_ms;
|
||||
f.render_ms = in.render_ms;
|
||||
f.pace_ms = in.pace_ms;
|
||||
for (int i = 0; i < MtPerf::SECTION_COUNT; ++i) f.game_ms[i] = game.ms[i];
|
||||
f.py_calls = game.calls[MtPerf::SECTION_PYTHON];
|
||||
f.py_missing = game.pyMissing;
|
||||
f.sync_ms = r.sync_ms - p.sync_ms;
|
||||
f.fence_ms = r.fence_ms - p.fence_ms;
|
||||
f.prepare_ms = r.prepare_ms - p.prepare_ms;
|
||||
f.geo_upload_ms = r.geometry_upload_ms - p.geometry_upload_ms;
|
||||
f.tex_upload_ms = r.texture_upload_ms - p.texture_upload_ms;
|
||||
f.submit_ms = r.submit_ms - p.submit_ms;
|
||||
f.present_ms = r.present_ms - p.present_ms;
|
||||
f.gpu_ms = r.gpu_ms - p.gpu_ms;
|
||||
f.gpu_samples = r.gpu_samples - p.gpu_samples;
|
||||
f.geo_uploads = r.uploads - p.uploads;
|
||||
f.geo_bytes = r.uploaded_bytes - p.uploaded_bytes;
|
||||
f.tex_uploads = r.texture_uploads - p.texture_uploads;
|
||||
f.tex_bytes = r.texture_bytes - p.texture_bytes;
|
||||
prev_ = r;
|
||||
have_prev_ = true;
|
||||
|
||||
if (in.frame_ms >= kHitchMs) write_hitch(f, game);
|
||||
window_.add(f);
|
||||
frame_times_.push_back(in.frame_ms);
|
||||
draws_ = r.draws;
|
||||
skinned_draws_ = r.skinned_draws;
|
||||
ui_batches_ = r.ui_batches;
|
||||
vertices_ = r.vertices;
|
||||
if (f.tex_uploads && r.last_texture) last_texture_ = *r.last_texture;
|
||||
actors_ = game.actorCount;
|
||||
script_cpu_ = game.scriptCpu;
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (power_ && std::chrono::duration<double>(now - power_sampled_at_).count() >= kPowerSampleS) {
|
||||
power_sampled_at_ = now;
|
||||
const PowerSample sample = power_();
|
||||
power_sum_.current_ma += sample.current_ma;
|
||||
power_sum_.voltage_mv += sample.voltage_mv;
|
||||
if (sample.power_mw >= 0) {
|
||||
power_sum_.power_mw += sample.power_mw;
|
||||
++power_valid_;
|
||||
}
|
||||
power_sum_.plugged = power_sum_.plugged || sample.plugged;
|
||||
++power_samples_;
|
||||
}
|
||||
const double elapsed = std::chrono::duration<double>(now - window_start_).count();
|
||||
if (elapsed < kIntervalS) return;
|
||||
write_row(elapsed, map_name());
|
||||
power_sum_ = {};
|
||||
power_sum_.voltage_mv = 0;
|
||||
power_sum_.power_mw = 0;
|
||||
power_samples_ = power_valid_ = 0;
|
||||
window_ = {};
|
||||
frame_times_.clear();
|
||||
window_start_ = now;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Window {
|
||||
std::uint64_t frames = 0;
|
||||
double frame_ms = 0, app_ms = 0, host_ms = 0, render_ms = 0, pace_ms = 0;
|
||||
double game_ms[MtPerf::SECTION_COUNT] = {};
|
||||
std::uint64_t py_calls = 0, py_missing = 0;
|
||||
double sync_ms = 0, fence_ms = 0, prepare_ms = 0, geo_upload_ms = 0, tex_upload_ms = 0, submit_ms = 0;
|
||||
double present_ms = 0, gpu_ms = 0;
|
||||
std::uint64_t gpu_samples = 0;
|
||||
std::uint64_t geo_uploads = 0, geo_bytes = 0, tex_uploads = 0, tex_bytes = 0;
|
||||
|
||||
void add(const Window& o) {
|
||||
frames += o.frames;
|
||||
frame_ms += o.frame_ms; app_ms += o.app_ms; host_ms += o.host_ms; render_ms += o.render_ms;
|
||||
pace_ms += o.pace_ms; fence_ms += o.fence_ms;
|
||||
for (int i = 0; i < MtPerf::SECTION_COUNT; ++i) game_ms[i] += o.game_ms[i];
|
||||
py_calls += o.py_calls; py_missing += o.py_missing;
|
||||
sync_ms += o.sync_ms; prepare_ms += o.prepare_ms; geo_upload_ms += o.geo_upload_ms;
|
||||
tex_upload_ms += o.tex_upload_ms; submit_ms += o.submit_ms; present_ms += o.present_ms;
|
||||
gpu_ms += o.gpu_ms; gpu_samples += o.gpu_samples;
|
||||
geo_uploads += o.geo_uploads; geo_bytes += o.geo_bytes;
|
||||
tex_uploads += o.tex_uploads; tex_bytes += o.tex_bytes;
|
||||
}
|
||||
};
|
||||
|
||||
static std::string directory() {
|
||||
if (const char* env = std::getenv("MT_PERF_DIR"); env && *env) return env;
|
||||
#ifdef __ANDROID__
|
||||
if (const char* ext = SDL_GetAndroidExternalStoragePath(); ext && *ext) return std::string(ext) + "/perf";
|
||||
if (const char* internal = SDL_GetAndroidInternalStoragePath(); internal && *internal)
|
||||
return std::string(internal) + "/perf";
|
||||
#endif
|
||||
return "perf";
|
||||
}
|
||||
|
||||
static void prune(const std::string& dir) {
|
||||
std::error_code ec;
|
||||
std::vector<std::filesystem::path> files;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(dir, ec)) {
|
||||
const std::string name = entry.path().filename().string();
|
||||
if (name.rfind("perf-", 0) == 0 && entry.path().extension() == ".csv") files.push_back(entry.path());
|
||||
}
|
||||
std::sort(files.begin(), files.end());
|
||||
// Leave room for the file about to be opened.
|
||||
for (std::size_t i = 0; i + kKeepFiles <= files.size(); ++i) std::filesystem::remove(files[i], ec);
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
static void log(const char* format, Args... args) {
|
||||
#ifdef __ANDROID__
|
||||
SDL_Log(format, args...);
|
||||
#else
|
||||
std::fprintf(stderr, format, args...);
|
||||
std::fputc('\n', stderr);
|
||||
#endif
|
||||
}
|
||||
|
||||
static int current_cpu() {
|
||||
#if defined(__linux__) || defined(__ANDROID__)
|
||||
return sched_getcpu();
|
||||
#else
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
static long read_long(const char* path) {
|
||||
std::FILE* f = std::fopen(path, "r");
|
||||
if (!f) return -1;
|
||||
long value = -1;
|
||||
if (std::fscanf(f, "%ld", &value) != 1) value = -1;
|
||||
std::fclose(f);
|
||||
return value;
|
||||
}
|
||||
|
||||
// Current clock of every CPU in MHz, "cpu0/cpu1/...", or "" where sysfs is closed.
|
||||
static std::string cpu_mhz() {
|
||||
std::string out;
|
||||
#if defined(__linux__) || defined(__ANDROID__)
|
||||
for (int cpu = 0; cpu < 16; ++cpu) {
|
||||
char path[96];
|
||||
std::snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/cpufreq/scaling_cur_freq", cpu);
|
||||
const long khz = read_long(path);
|
||||
if (khz < 0) {
|
||||
if (cpu >= 8 || access(path, F_OK) != 0) break;
|
||||
continue;
|
||||
}
|
||||
if (!out.empty()) out += '/';
|
||||
out += std::to_string(khz / 1000);
|
||||
}
|
||||
#endif
|
||||
return out;
|
||||
}
|
||||
|
||||
// Each cpufreq policy's current ceiling (scaling_max_freq): the vendor's thermal throttling shows up
|
||||
// here long before AThermal reports anything.
|
||||
static std::string cpu_max_mhz() {
|
||||
std::string out;
|
||||
#if defined(__linux__) || defined(__ANDROID__)
|
||||
for (int policy = 0; policy < 16; ++policy) {
|
||||
char path[96];
|
||||
std::snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpufreq/policy%d/scaling_max_freq", policy);
|
||||
const long khz = read_long(path);
|
||||
if (khz < 0) continue;
|
||||
if (!out.empty()) out += '/';
|
||||
out += std::to_string(khz / 1000);
|
||||
}
|
||||
#endif
|
||||
return out;
|
||||
}
|
||||
|
||||
// AThermal_getCurrentThermalStatus (API 30+, looked up at run time since minSdk is lower):
|
||||
// 0 none, 1 light, 2 moderate, 3 severe, 4 critical, 5 emergency, 6 shutdown; -1 unknown.
|
||||
static int thermal_status() {
|
||||
#ifdef __ANDROID__
|
||||
using Acquire = void* (*)();
|
||||
using Status = int (*)(void*);
|
||||
static void* manager = nullptr;
|
||||
static Status status = nullptr;
|
||||
static bool looked_up = false;
|
||||
if (!looked_up) {
|
||||
looked_up = true;
|
||||
if (void* lib = dlopen("libandroid.so", RTLD_NOW)) {
|
||||
const auto acquire = reinterpret_cast<Acquire>(dlsym(lib, "AThermal_acquireManager"));
|
||||
status = reinterpret_cast<Status>(dlsym(lib, "AThermal_getCurrentThermalStatus"));
|
||||
if (acquire && status) manager = acquire();
|
||||
}
|
||||
}
|
||||
if (manager && status) return status(manager);
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
double battery_celsius() const {
|
||||
if (battery_) return battery_();
|
||||
#if defined(__linux__) || defined(__ANDROID__)
|
||||
const long tenths = read_long("/sys/class/power_supply/battery/temp");
|
||||
if (tenths > 0) return double(tenths) / 10.0; // -1: unreadable (SELinux on most phones)
|
||||
#endif
|
||||
return -1.0;
|
||||
}
|
||||
|
||||
static double rss_mb() {
|
||||
#if defined(__linux__) || defined(__ANDROID__)
|
||||
std::FILE* f = std::fopen("/proc/self/statm", "r");
|
||||
if (!f) return -1.0;
|
||||
long pages = 0, resident = 0;
|
||||
const int read = std::fscanf(f, "%ld %ld", &pages, &resident);
|
||||
std::fclose(f);
|
||||
if (read == 2) return double(resident) * double(sysconf(_SC_PAGESIZE)) / (1024.0 * 1024.0);
|
||||
#endif
|
||||
return -1.0;
|
||||
}
|
||||
|
||||
static std::string csv_field(std::string text) {
|
||||
for (char& c : text)
|
||||
if (c == ',' || c == '\n' || c == '\r') c = ' ';
|
||||
return text;
|
||||
}
|
||||
|
||||
void write_row(double elapsed_s, const std::string& map) {
|
||||
const Window& w = window_;
|
||||
if (!w.frames) return;
|
||||
const double n = double(w.frames);
|
||||
std::vector<double> sorted = frame_times_;
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
const double p95 = sorted[std::min(sorted.size() - 1, std::size_t(0.95 * double(sorted.size())))];
|
||||
int jank33 = 0, jank50 = 0;
|
||||
for (double ms : frame_times_) {
|
||||
jank33 += ms > 33.4;
|
||||
jank50 += ms > 50.0;
|
||||
}
|
||||
const auto g = [&](MtPerf::ESection s) { return w.game_ms[s] / n; };
|
||||
const double fps = n / elapsed_s;
|
||||
const double gpu = w.gpu_samples ? w.gpu_ms / double(w.gpu_samples) : 0.0;
|
||||
const double t = std::chrono::duration<double>(std::chrono::steady_clock::now() - start_).count();
|
||||
const std::string mhz = cpu_mhz();
|
||||
const std::string max_mhz = cpu_max_mhz();
|
||||
const int thermal = thermal_status();
|
||||
const double batt = battery_celsius();
|
||||
const double rss = rss_mb();
|
||||
const int main_cpu = current_cpu();
|
||||
const double refresh = refresh_rate_ ? refresh_rate_() : -1.0;
|
||||
const double render_rate = render_rate_ ? render_rate_() : -1.0;
|
||||
const double ns = double(power_samples_);
|
||||
const double batt_ma = power_samples_ ? power_sum_.current_ma / ns : -1.0;
|
||||
const double batt_mv = power_samples_ ? power_sum_.voltage_mv / ns : -1.0;
|
||||
// Only when every sample of the row was off the charger.
|
||||
const double power_mw = power_valid_ && power_valid_ == power_samples_ ? power_sum_.power_mw / ns : -1.0;
|
||||
std::fprintf(file_,
|
||||
"%.1f,%s,%d,%llu,%.1f,%.1f,%.1f,%d,%.2f,%.2f,%.2f,%d,%d,"
|
||||
"%.2f,%.2f,%.2f,%.3f,%.3f,%.3f,%.3f,%.3f,%.3f,"
|
||||
"%.3f,%.3f,%.3f,%.1f,%.1f,%.3f,%.2f,%.2f,%.2f,%.2f,"
|
||||
"%.2f,%.3f,%.3f,"
|
||||
"%.3f,%.3f,%.2f,%llu,%llu,%llu,%llu,%.2f,%.1f,%.2f,%.1f,"
|
||||
"%d,%d,%s,%s,%d,%.1f,%.0f,%.0f,%.0f,%.0f,%d,%d,%d,%s\n",
|
||||
t, csv_field(map).c_str(), actors_, (unsigned long long)w.frames, fps, refresh, render_rate, fps_cap_, w.frame_ms / n,
|
||||
p95, sorted.back(), jank33, jank50,
|
||||
w.app_ms / n, g(MtPerf::SECTION_PROCESS), (w.app_ms - w.game_ms[MtPerf::SECTION_PROCESS]) / n,
|
||||
g(MtPerf::SECTION_NETWORK), g(MtPerf::SECTION_CAMERA), g(MtPerf::SECTION_UI_UPDATE),
|
||||
g(MtPerf::SECTION_UPDATE_GAME), g(MtPerf::SECTION_RENDER_BEGIN), g(MtPerf::SECTION_UI_RENDER),
|
||||
g(MtPerf::SECTION_RENDER_GAME), g(MtPerf::SECTION_SHADOW_RASTER), g(MtPerf::SECTION_PYTHON),
|
||||
double(w.py_calls) / n,
|
||||
double(w.py_missing) / n, w.host_ms / n, w.render_ms / n, w.pace_ms / n, w.sync_ms / n, w.fence_ms / n,
|
||||
w.prepare_ms / n, w.geo_upload_ms / n, w.tex_upload_ms / n,
|
||||
w.submit_ms / n, w.present_ms / n, gpu, (unsigned long long)draws_, (unsigned long long)skinned_draws_,
|
||||
(unsigned long long)ui_batches_, (unsigned long long)vertices_, double(w.geo_uploads) / n,
|
||||
double(w.geo_bytes) / 1024.0 / n, double(w.tex_uploads) / n, double(w.tex_bytes) / 1024.0 / n,
|
||||
script_cpu_, main_cpu, mhz.c_str(), max_mhz.c_str(), thermal, batt, rss, batt_ma, batt_mv, power_mw,
|
||||
int(power_sum_.plugged), fps_mode_, int(idle_), csv_field(last_texture_).c_str());
|
||||
std::fflush(file_);
|
||||
if (!w.tex_uploads) last_texture_.clear();
|
||||
log("perf fps=%.1f @%.0fHz/%.0f cap=%d frame=%.1f/p95 %.1f/max %.1f jank33=%d | app=%.1f (upd=%.1f rnd=%.1f "
|
||||
"shadow=%.1f py=%.1f) | render=%.1f (sync=%.1f fence=%.1f prep=%.1f up=%.1f/%.1f) pace=%.1f gpu=%.1f | "
|
||||
"draws=%llu actors=%d map=%s thermal=%d batt=%.1f cpu=%s max=%s tex_uploads=%.2f | pwr=%.0fmW %.0fmA%s mode=%d%s "
|
||||
"last_tex=%s",
|
||||
fps, refresh, render_rate, fps_cap_, w.frame_ms / n, p95, sorted.back(), jank33, w.app_ms / n,
|
||||
g(MtPerf::SECTION_UPDATE_GAME), g(MtPerf::SECTION_RENDER_GAME), g(MtPerf::SECTION_SHADOW_RASTER),
|
||||
g(MtPerf::SECTION_PYTHON), w.render_ms / n, w.sync_ms / n, w.fence_ms / n, w.prepare_ms / n,
|
||||
w.geo_upload_ms / n, w.tex_upload_ms / n, w.pace_ms / n, gpu, (unsigned long long)draws_, actors_,
|
||||
map.c_str(), thermal, batt, mhz.c_str(), max_mhz.c_str(), double(w.tex_uploads) / n, power_mw, batt_ma,
|
||||
power_sum_.plugged ? " plugged" : "", fps_mode_, idle_ ? " idle" : "", last_texture_.c_str());
|
||||
}
|
||||
|
||||
void write_hitch(const Window& f, const MtPerf::SCounters& game) {
|
||||
const double t = std::chrono::duration<double>(std::chrono::steady_clock::now() - start_).count();
|
||||
std::fprintf(file_,
|
||||
"# hitch t=%.2f frame=%.1f app=%.1f process=%.1f net=%.1f camera=%.1f ui_update=%.1f update_game=%.1f "
|
||||
"ui_render=%.1f render_game=%.1f shadow=%.1f py=%.1f host=%.1f render=%.1f sync=%.1f prepare=%.1f geo_upload=%.1f(%llu) "
|
||||
"tex_upload=%.1f(%llu, %.0f KB) submit=%.1f present=%.1f\n",
|
||||
t, f.frame_ms, f.app_ms, game.ms[MtPerf::SECTION_PROCESS], game.ms[MtPerf::SECTION_NETWORK],
|
||||
game.ms[MtPerf::SECTION_CAMERA], game.ms[MtPerf::SECTION_UI_UPDATE], game.ms[MtPerf::SECTION_UPDATE_GAME],
|
||||
game.ms[MtPerf::SECTION_UI_RENDER], game.ms[MtPerf::SECTION_RENDER_GAME],
|
||||
game.ms[MtPerf::SECTION_SHADOW_RASTER], game.ms[MtPerf::SECTION_PYTHON],
|
||||
f.host_ms, f.render_ms, f.sync_ms, f.prepare_ms, f.geo_upload_ms, (unsigned long long)f.geo_uploads,
|
||||
f.tex_upload_ms, (unsigned long long)f.tex_uploads, double(f.tex_bytes) / 1024.0, f.submit_ms, f.present_ms);
|
||||
}
|
||||
|
||||
std::FILE* file_ = nullptr;
|
||||
std::string path_;
|
||||
std::chrono::steady_clock::time_point start_{}, window_start_{};
|
||||
Window window_;
|
||||
std::vector<double> frame_times_;
|
||||
RendererTotals prev_;
|
||||
bool have_prev_ = false;
|
||||
std::uint64_t draws_ = 0, skinned_draws_ = 0, ui_batches_ = 0, vertices_ = 0;
|
||||
int actors_ = 0, script_cpu_ = -1;
|
||||
int fps_cap_ = 0;
|
||||
std::function<double()> refresh_rate_;
|
||||
std::function<double()> render_rate_;
|
||||
std::function<double()> battery_;
|
||||
std::function<PowerSample()> power_;
|
||||
std::chrono::steady_clock::time_point power_sampled_at_{};
|
||||
PowerSample power_sum_{0, 0, 0, false};
|
||||
int power_samples_ = 0, power_valid_ = 0;
|
||||
int fps_mode_ = -1;
|
||||
bool idle_ = false;
|
||||
std::string last_texture_;
|
||||
};
|
||||
|
||||
} // namespace native_perf
|
||||
@@ -0,0 +1,130 @@
|
||||
#version 450
|
||||
layout(location = 0) in vec4 in_color;
|
||||
layout(location = 1) in vec2 in_uv;
|
||||
layout(location = 2) in vec2 in_mask_uv;
|
||||
layout(location = 3) in float in_fog;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
|
||||
layout(set = 0, binding = 0) uniform sampler2D tex_sampler;
|
||||
layout(set = 0, binding = 1) uniform sampler2D mask_sampler;
|
||||
|
||||
layout(push_constant) uniform DrawConstants {
|
||||
mat4 mvp;
|
||||
vec4 params; // bone base + 1, pretransformed, UI mask, unused
|
||||
} draw;
|
||||
|
||||
layout(set = 1, binding = 1, std430) readonly buffer FixedFunctionState {
|
||||
uvec4 stage_color[2];
|
||||
uvec4 stage_alpha[2];
|
||||
vec4 fog_color;
|
||||
vec4 fog_params;
|
||||
vec4 texture_factor;
|
||||
mat4 world_view;
|
||||
uvec4 flags;
|
||||
mat4 normal_matrix;
|
||||
uvec4 lighting_flags;
|
||||
uvec4 material_sources;
|
||||
uvec4 alpha_test; // ALPHATESTENABLE, ALPHAFUNC, ALPHAREF, unused
|
||||
} fixed_state;
|
||||
|
||||
vec4 stage_arg(uint selector, vec4 diffuse, vec4 current, vec4 texel) {
|
||||
uint source = selector & 15u;
|
||||
vec4 value = source == 0u ? diffuse :
|
||||
source == 1u ? current :
|
||||
source == 2u ? texel :
|
||||
source == 3u ? fixed_state.texture_factor : vec4(1.0);
|
||||
if ((selector & 16u) != 0u) value = vec4(1.0) - value;
|
||||
if ((selector & 32u) != 0u) value.rgb = vec3(value.a);
|
||||
return value;
|
||||
}
|
||||
|
||||
vec4 apply_op(uint op, vec4 a, vec4 b, vec4 current, vec4 diffuse, vec4 texel) {
|
||||
if (op == 2u) return a; // SELECTARG1
|
||||
if (op == 3u) return b; // SELECTARG2
|
||||
if (op == 4u) return a * b; // MODULATE
|
||||
if (op == 5u) return 2.0 * a * b; // MODULATE2X
|
||||
if (op == 6u) return 4.0 * a * b; // MODULATE4X
|
||||
if (op == 7u) return a + b; // ADD
|
||||
if (op == 8u) return a + b - vec4(0.5); // ADDSIGNED
|
||||
if (op == 9u) return 2.0 * (a + b - vec4(0.5)); // ADDSIGNED2X
|
||||
if (op == 10u) return a - b; // SUBTRACT
|
||||
if (op == 11u) return a + b - a * b; // ADDSMOOTH
|
||||
if (op == 12u) return mix(b, a, diffuse.a);
|
||||
if (op == 13u) return mix(b, a, texel.a);
|
||||
if (op == 14u) return mix(b, a, fixed_state.texture_factor.a);
|
||||
if (op == 15u) return a + b * (1.0 - texel.a);
|
||||
if (op == 16u) return mix(b, a, current.a);
|
||||
if (op == 18u) return vec4(a.rgb + a.a * b.rgb, a.a);
|
||||
if (op == 19u) return vec4(a.rgb * b.rgb + vec3(a.a), a.a);
|
||||
if (op == 20u) return vec4((1.0 - a.a) * b.rgb + a.rgb, a.a);
|
||||
if (op == 21u) return vec4((vec3(1.0) - a.rgb) * b.rgb + vec3(a.a), a.a);
|
||||
return current;
|
||||
}
|
||||
|
||||
vec4 apply_stage(uvec4 color_state, uvec4 alpha_state,
|
||||
vec4 diffuse, vec4 current, vec4 texel) {
|
||||
if (color_state.x <= 1u) return current;
|
||||
vec4 color_arg1 = stage_arg(color_state.y, diffuse, current, texel);
|
||||
vec4 color_arg2 = stage_arg(color_state.z, diffuse, current, texel);
|
||||
vec4 result = apply_op(color_state.x, color_arg1, color_arg2, current, diffuse, texel);
|
||||
if (alpha_state.x > 1u) {
|
||||
vec4 alpha_arg1 = stage_arg(alpha_state.y, diffuse, current, texel);
|
||||
vec4 alpha_arg2 = stage_arg(alpha_state.z, diffuse, current, texel);
|
||||
result.a = apply_op(alpha_state.x, alpha_arg1, alpha_arg2, current, diffuse, texel).a;
|
||||
} else if (alpha_state.x == 1u) {
|
||||
result.a = current.a;
|
||||
}
|
||||
return clamp(result, 0.0, 1.0);
|
||||
}
|
||||
|
||||
void main() {
|
||||
if (fixed_state.flags.x != 0u) {
|
||||
// D3D8 texture stage cascade: stage 1 runs unless its COLOROP is D3DTOP_DISABLE.
|
||||
vec4 diffuse = in_color;
|
||||
vec4 color = apply_stage(fixed_state.stage_color[0], fixed_state.stage_alpha[0],
|
||||
diffuse, diffuse, texture(tex_sampler, in_uv));
|
||||
if (fixed_state.stage_color[0].x > 1u && fixed_state.stage_color[1].x > 1u)
|
||||
color = apply_stage(fixed_state.stage_color[1], fixed_state.stage_alpha[1],
|
||||
diffuse, color, texture(mask_sampler, in_mask_uv));
|
||||
if (fixed_state.alpha_test.x != 0u) {
|
||||
// D3DRS_ALPHAREF is an 8-bit reference compared against the 8-bit pixel alpha.
|
||||
uint a = uint(clamp(color.a, 0.0, 1.0) * 255.0 + 0.5);
|
||||
uint ref = fixed_state.alpha_test.z & 255u;
|
||||
uint func = fixed_state.alpha_test.y;
|
||||
bool passes = func == 1u ? false :
|
||||
func == 2u ? a < ref :
|
||||
func == 3u ? a == ref :
|
||||
func == 4u ? a <= ref :
|
||||
func == 5u ? a > ref :
|
||||
func == 6u ? a != ref :
|
||||
func == 7u ? a >= ref : true;
|
||||
if (!passes) discard;
|
||||
}
|
||||
color.rgb = mix(fixed_state.fog_color.rgb, color.rgb, in_fog);
|
||||
out_color = color;
|
||||
return;
|
||||
}
|
||||
// 2D UI batch (UIRenderCommand): vertex color times texture, optionally clipped by a mask.
|
||||
// The UI is laid out in logical pixels and magnified onto a HiDPI swapchain. Plain bilinear
|
||||
// smears each texel over the scale factor; "sharp bilinear" keeps texels square and only blends
|
||||
// across a one-pixel seam. Minified or 1:1 texels keep the original bilinear lookup.
|
||||
vec2 uv = in_uv;
|
||||
vec2 tex_size = vec2(textureSize(tex_sampler, 0));
|
||||
vec2 texel = uv * tex_size;
|
||||
vec2 texels_per_pixel = fwidth(texel);
|
||||
if (texels_per_pixel.x > 0.0 && texels_per_pixel.y > 0.0 &&
|
||||
texels_per_pixel.x < 0.99 && texels_per_pixel.y < 0.99) {
|
||||
vec2 seam = floor(texel + 0.5);
|
||||
texel = seam + clamp((texel - seam) / texels_per_pixel, -0.5, 0.5);
|
||||
uv = texel / tex_size;
|
||||
}
|
||||
vec4 color = in_color * texture(tex_sampler, uv);
|
||||
if (draw.params.z > 0.5) {
|
||||
if (in_mask_uv.x < 0.0 || in_mask_uv.x > 1.0 || in_mask_uv.y < 0.0 || in_mask_uv.y > 1.0)
|
||||
discard;
|
||||
vec4 mask_val = texture(mask_sampler, in_mask_uv);
|
||||
color.rgb *= mask_val.rgb;
|
||||
color.a = mask_val.a * in_color.a;
|
||||
}
|
||||
out_color = color;
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
#version 450
|
||||
layout(location = 0) in vec3 in_position;
|
||||
layout(location = 1) in vec3 in_normal;
|
||||
layout(location = 2) in vec2 in_uv;
|
||||
layout(location = 3) in vec4 in_color;
|
||||
layout(location = 4) in uvec4 in_joints;
|
||||
layout(location = 5) in vec4 in_weights;
|
||||
layout(location = 6) in vec2 in_mask_uv;
|
||||
layout(location = 7) in float in_rhw;
|
||||
layout(location = 8) in float in_vertex_fog;
|
||||
|
||||
layout(location = 0) out vec4 out_color;
|
||||
layout(location = 1) out vec2 out_uv;
|
||||
layout(location = 2) out vec2 out_mask_uv;
|
||||
layout(location = 3) out float out_fog;
|
||||
|
||||
layout(set = 1, binding = 0, std430) readonly buffer BonePalette {
|
||||
mat4 bones[];
|
||||
} bone_palette;
|
||||
|
||||
layout(push_constant) uniform DrawConstants {
|
||||
mat4 mvp;
|
||||
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.
|
||||
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
|
||||
vec4 fog_color;
|
||||
vec4 fog_params;
|
||||
vec4 texture_factor;
|
||||
mat4 world_view;
|
||||
uvec4 flags;
|
||||
mat4 normal_matrix;
|
||||
uvec4 lighting_flags; // LIGHTING, COLORVERTEX, vertex has diffuse, NORMALIZENORMALS
|
||||
uvec4 material_sources; // DIFFUSE, AMBIENT, EMISSIVE material source, LOCALVIEWER
|
||||
uvec4 alpha_test;
|
||||
vec4 material_diffuse;
|
||||
vec4 material_ambient;
|
||||
vec4 material_emissive;
|
||||
vec4 global_ambient;
|
||||
mat4 texture_matrix[2];
|
||||
vec4 light_position_type[8];
|
||||
vec4 light_direction_range[8];
|
||||
vec4 light_diffuse[8];
|
||||
vec4 light_ambient[8];
|
||||
vec4 light_attenuation[8];
|
||||
vec4 light_spot[8];
|
||||
} fixed_state;
|
||||
|
||||
// D3DMCS_COLOR1 picks the vertex diffuse color when COLORVERTEX is on and the vertex has one.
|
||||
vec4 material_color(uint source, vec4 material) {
|
||||
if (fixed_state.lighting_flags.y != 0u && fixed_state.lighting_flags.z != 0u && source == 1u)
|
||||
return in_color;
|
||||
return material;
|
||||
}
|
||||
|
||||
// D3D8 texture coordinate processing: D3DTSS_TEXCOORDINDEX generation, then D3DTS_TEXTUREn.
|
||||
vec2 stage_texcoord(int stage, bool pretransformed, vec3 eye, vec3 n) {
|
||||
uint tci = fixed_state.stage_color[stage].w;
|
||||
uint gen = tci & 0xFFFF0000u;
|
||||
uint set_index = tci & 0xFFFFu;
|
||||
vec4 coord = vec4(0.0, 0.0, 1.0, 0.0);
|
||||
if (gen == 0u || pretransformed) {
|
||||
if (set_index == 0u) coord.xy = in_uv;
|
||||
else if (set_index == 1u) coord.xy = in_mask_uv;
|
||||
} else if (gen == 0x10000u) { // CAMERASPACENORMAL
|
||||
coord = vec4(n, 1.0);
|
||||
} else if (gen == 0x20000u) { // CAMERASPACEPOSITION
|
||||
coord = vec4(eye, 1.0);
|
||||
} else if (gen == 0x30000u) { // CAMERASPACEREFLECTIONVECTOR
|
||||
vec3 e = fixed_state.material_sources.w != 0u
|
||||
? (length(eye) > 0.0 ? -normalize(eye) : vec3(0.0)) : vec3(0.0, 0.0, -1.0);
|
||||
coord = vec4(2.0 * dot(e, n) * n - e, 1.0);
|
||||
}
|
||||
// Pretransformed (XYZRHW) vertices bypass D3D8 T&L, texture transforms included
|
||||
// (RecordingDevice's CPU texcoord path does the same).
|
||||
uint transform_flags = pretransformed ? 0u : fixed_state.stage_alpha[stage].w;
|
||||
uint elements = transform_flags & 0xFFu;
|
||||
vec4 r = coord;
|
||||
if (elements != 0u)
|
||||
r = fixed_state.texture_matrix[stage] * coord;
|
||||
if ((transform_flags & 0x100u) != 0u && elements >= 2u && elements <= 4u && r[elements - 1u] != 0.0)
|
||||
r.xy /= r[elements - 1u];
|
||||
return r.xy;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec3 pos = in_position;
|
||||
vec3 nrm = in_normal;
|
||||
if (draw.params.x > 0.5) {
|
||||
uint bone_base = uint(draw.params.x - 0.5);
|
||||
vec3 skinned_pos = vec3(0.0);
|
||||
vec3 skinned_nrm = vec3(0.0);
|
||||
float total_w = 0.0;
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
float w = in_weights[k];
|
||||
if (w > 0.0) {
|
||||
mat4 B = bone_palette.bones[bone_base + in_joints[k]];
|
||||
skinned_pos += w * (B * vec4(pos, 1.0)).xyz;
|
||||
skinned_nrm += w * (mat3(B) * nrm);
|
||||
total_w += w;
|
||||
}
|
||||
}
|
||||
if (total_w > 0.0) {
|
||||
pos = skinned_pos;
|
||||
nrm = skinned_nrm;
|
||||
}
|
||||
}
|
||||
bool pretransformed = draw.params.y > 0.5;
|
||||
// D3D row-vector matrices are uploaded row-major. GLSL reads the bytes as their transpose.
|
||||
gl_Position = draw.mvp * vec4(pos, 1.0);
|
||||
if (pretransformed) {
|
||||
// Vertices behind the eye carry a negative rhw (STP terrain): rebuilding clip space
|
||||
// with w = 1/rhw < 0 lets the clipper drop them instead of drawing w = 1 spikes.
|
||||
float clip_w = in_rhw != 0.0 ? 1.0 / in_rhw : 1.0;
|
||||
gl_Position.xyz *= clip_w;
|
||||
gl_Position.w = clip_w;
|
||||
}
|
||||
gl_Position.y = -gl_Position.y;
|
||||
|
||||
// A vertex without D3DFVF_DIFFUSE carries opaque white (upload_geometry).
|
||||
out_color = in_color;
|
||||
if (fixed_state.flags.x == 0u) {
|
||||
// 2D UI batch.
|
||||
out_uv = in_uv;
|
||||
out_mask_uv = in_mask_uv;
|
||||
out_fog = 1.0;
|
||||
return;
|
||||
}
|
||||
|
||||
vec3 eye = (fixed_state.world_view * vec4(pos, 1.0)).xyz;
|
||||
vec3 n = mat3(fixed_state.normal_matrix) * nrm;
|
||||
if (fixed_state.lighting_flags.w != 0u && length(n) > 0.0)
|
||||
n = normalize(n);
|
||||
|
||||
if (fixed_state.lighting_flags.x != 0u && !pretransformed) {
|
||||
vec4 mat_diffuse = material_color(fixed_state.material_sources.x, fixed_state.material_diffuse);
|
||||
vec4 mat_ambient = material_color(fixed_state.material_sources.y, fixed_state.material_ambient);
|
||||
vec4 mat_emissive = material_color(fixed_state.material_sources.z, fixed_state.material_emissive);
|
||||
vec3 ambient_sum = fixed_state.global_ambient.rgb;
|
||||
vec3 diffuse_sum = vec3(0.0);
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
int type = int(fixed_state.light_position_type[i].w + 0.5);
|
||||
if (type == 0) continue;
|
||||
vec3 L;
|
||||
float strength = 1.0;
|
||||
if (type == 3) { // D3DLIGHT_DIRECTIONAL
|
||||
L = -fixed_state.light_direction_range[i].xyz;
|
||||
} else { // D3DLIGHT_POINT / D3DLIGHT_SPOT
|
||||
vec3 to_light = fixed_state.light_position_type[i].xyz - eye;
|
||||
float d = length(to_light);
|
||||
if (d > fixed_state.light_direction_range[i].w)
|
||||
continue;
|
||||
L = d > 0.0 ? to_light / d : vec3(0.0);
|
||||
vec4 attenuation = fixed_state.light_attenuation[i];
|
||||
float denominator = attenuation.x + attenuation.y * d + attenuation.z * d * d;
|
||||
strength = denominator != 0.0 ? 1.0 / denominator : 1.0;
|
||||
if (type == 2) {
|
||||
float rho = dot(-L, fixed_state.light_direction_range[i].xyz);
|
||||
float cos_theta = fixed_state.light_spot[i].x;
|
||||
float cos_phi = fixed_state.light_spot[i].y;
|
||||
float spot = rho > cos_theta ? 1.0 : (rho <= cos_phi ? 0.0 :
|
||||
pow((rho - cos_phi) / (cos_theta - cos_phi), attenuation.w));
|
||||
strength *= spot;
|
||||
}
|
||||
}
|
||||
ambient_sum += fixed_state.light_ambient[i].rgb * strength;
|
||||
diffuse_sum += fixed_state.light_diffuse[i].rgb * max(dot(n, L), 0.0) * strength;
|
||||
}
|
||||
vec3 lit = mat_emissive.rgb + mat_ambient.rgb * ambient_sum + mat_diffuse.rgb * diffuse_sum;
|
||||
out_color = vec4(clamp(lit, 0.0, 1.0), clamp(mat_diffuse.a, 0.0, 1.0));
|
||||
}
|
||||
|
||||
out_uv = stage_texcoord(0, pretransformed, eye, n);
|
||||
out_mask_uv = stage_texcoord(1, pretransformed, eye, n);
|
||||
|
||||
out_fog = 1.0;
|
||||
if (fixed_state.flags.w == 4u) {
|
||||
// XYZRHW with FOGTABLEMODE NONE: D3D8 takes the vertex fog from the specular alpha.
|
||||
out_fog = in_vertex_fog;
|
||||
} else if (fixed_state.flags.w != 0u) {
|
||||
// Table fog on XYZRHW vertices runs on eye depth w = 1/rhw.
|
||||
float distance_to_eye = pretransformed ? gl_Position.w
|
||||
: (fixed_state.fog_params.w > 0.5 ? length(eye) : abs(eye.z));
|
||||
if (fixed_state.flags.w == 3u) {
|
||||
float span = fixed_state.fog_params.y - fixed_state.fog_params.x;
|
||||
if (span > 0.0001)
|
||||
out_fog = clamp((fixed_state.fog_params.y - distance_to_eye) / span, 0.0, 1.0);
|
||||
} else if (fixed_state.flags.w == 1u) {
|
||||
out_fog = clamp(exp(-fixed_state.fog_params.z * distance_to_eye), 0.0, 1.0);
|
||||
} else if (fixed_state.flags.w == 2u) {
|
||||
float fog_distance = fixed_state.fog_params.z * distance_to_eye;
|
||||
out_fog = clamp(exp(-fog_distance * fog_distance), 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#define STBI_NO_STDIO
|
||||
#define STBI_ONLY_JPEG
|
||||
#define STBI_ONLY_PNG
|
||||
#define STBI_ONLY_BMP
|
||||
#define STBI_MAX_DIMENSIONS 4096
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "stb_image.h"
|
||||
Vendored
+7988
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,892 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "UIRenderCommands.h"
|
||||
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
#include "platform/ScriptLib/PythonBoot.h"
|
||||
#endif
|
||||
|
||||
class TouchController {
|
||||
public:
|
||||
struct ButtonDef {
|
||||
int id; // 1 = attack, 2..7 = quick slots 1..6, 8 = auto hunt
|
||||
int dik;
|
||||
float x, y, radius;
|
||||
const char* label;
|
||||
uint32_t color_idle;
|
||||
uint32_t color_pressed;
|
||||
bool pressed = false;
|
||||
int64_t finger_id = -1;
|
||||
};
|
||||
|
||||
TouchController() {
|
||||
init_buttons();
|
||||
}
|
||||
|
||||
void set_enabled(bool enabled) {
|
||||
enabled_ = enabled;
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::SetTouchInput(enabled);
|
||||
#endif
|
||||
}
|
||||
bool is_enabled() const { return enabled_; }
|
||||
// Short vibration when a long press fires (the host wires it to the platform).
|
||||
void set_haptic(void (*haptic)()) { haptic_ = haptic; }
|
||||
|
||||
// The system keyboard is up only after the player taps the focused EditLine, not when a
|
||||
// script focuses one on its own (intrologin.py focuses the ID field as the board opens).
|
||||
// The serial changes on every such tap so the host can re-show a keyboard the user dismissed.
|
||||
bool wants_screen_keyboard() const { return keyboard_requested_; }
|
||||
unsigned screen_keyboard_serial() const { return keyboard_serial_; }
|
||||
|
||||
// safe_inset: logical pixels kept clear at the left and right edges (rounded corners, cutouts).
|
||||
void update_screen_size(int width, int height, int safe_inset = 0) {
|
||||
if (width <= 0 || height <= 0) return;
|
||||
screen_w_ = width;
|
||||
screen_h_ = height;
|
||||
safe_inset_ = float(std::max(0, safe_inset));
|
||||
// Position joystick on lower-left
|
||||
joystick_base_x_ = safe_inset_ + std::max(90.0f, float(width) * 0.12f);
|
||||
joystick_base_y_ = float(height) - std::max(90.0f, float(height) * 0.22f);
|
||||
if (!joystick_active_) {
|
||||
joystick_knob_x_ = joystick_base_x_;
|
||||
joystick_knob_y_ = joystick_base_y_;
|
||||
}
|
||||
layout_buttons();
|
||||
}
|
||||
|
||||
// Called once per frame to maintain continuous analog movement
|
||||
void update() {
|
||||
if (!enabled_) return;
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
if (keyboard_requested_ && !PythonBoot::TextInputFocused())
|
||||
keyboard_requested_ = false;
|
||||
// A world tap presses and releases on later frames than its mouse move so
|
||||
// 40250's per-frame actor picking has already hit the target under the finger.
|
||||
if (tap_stage_ == 1) {
|
||||
PythonBoot::UIMouseMove(int(tap_x_), int(tap_y_));
|
||||
PythonBoot::UIMouseButton(1, true, int(tap_x_), int(tap_y_));
|
||||
tap_stage_ = 2;
|
||||
} else if (tap_stage_ == 2) {
|
||||
PythonBoot::UIMouseButton(1, false, int(tap_x_), int(tap_y_));
|
||||
tap_stage_ = 0;
|
||||
}
|
||||
// A UI finger held still turns into a right click (use item, equip, skill up...).
|
||||
if (ui_touch_finger_id_ >= 0 && ui_gesture_ == UiGesture::Pending &&
|
||||
get_time_sec() - ui_down_time_ >= kLongPressSec) {
|
||||
ui_gesture_ = UiGesture::LongPressed;
|
||||
last_tap_time_ = -1.0;
|
||||
PythonBoot::UIMouseButton(2, true, int(ui_start_x_), int(ui_start_y_));
|
||||
PythonBoot::UIMouseButton(2, false, int(ui_start_x_), int(ui_start_y_));
|
||||
if (haptic_) haptic_();
|
||||
}
|
||||
for (auto& btn : buttons_) {
|
||||
if (btn.id == kAutoHuntButton && btn.pressed && !auto_hunt_long_pressed_ &&
|
||||
get_time_sec() - auto_hunt_down_time_ >= kLongPressSec) {
|
||||
auto_hunt_long_pressed_ = true;
|
||||
PythonBoot::AutoHuntOpenSettings();
|
||||
if (haptic_) haptic_();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
const bool controls_visible = gameplay_controls_visible();
|
||||
if (!controls_visible) {
|
||||
if (controls_were_visible_)
|
||||
release_gameplay_controls();
|
||||
controls_were_visible_ = false;
|
||||
return;
|
||||
}
|
||||
controls_were_visible_ = true;
|
||||
if (joystick_active_) {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::SetMoveDirection(move_angle_, true);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// Handles finger touch down (norm_x, norm_y in 0.0 .. 1.0)
|
||||
bool on_finger_down(int64_t finger_id, float norm_x, float norm_y) {
|
||||
if (!enabled_) return false;
|
||||
const float px = norm_x * float(screen_w_);
|
||||
const float py = norm_y * float(screen_h_);
|
||||
const bool controls_visible = gameplay_controls_visible();
|
||||
|
||||
// The mobile overlay owns only the movement and combat control regions.
|
||||
if (controls_visible) {
|
||||
const int btn_idx = find_button(px, py);
|
||||
if (btn_idx >= 0) {
|
||||
auto& btn = buttons_[btn_idx];
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
if (is_quick_slot_button(btn.id) && PythonBoot::TryAssignAttachedObjectToLocalQuickSlot(btn.id - 2))
|
||||
return true;
|
||||
#endif
|
||||
btn.pressed = true;
|
||||
btn.finger_id = finger_id;
|
||||
if (btn.id == kAutoHuntButton) {
|
||||
// Tap toggles; holding opens the settings window (update()).
|
||||
auto_hunt_down_time_ = get_time_sec();
|
||||
auto_hunt_long_pressed_ = false;
|
||||
return true;
|
||||
}
|
||||
trigger_button(btn.dik, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Everything else uses the unchanged 40250 UI and its normal mouse path.
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
// Only hover here (the tooltip shows while the finger rests); what the finger does next picks
|
||||
// the gesture: lift = click, slide = press + drag, hold = right click, second tap = double click.
|
||||
if (PythonBoot::IsPointInsideActiveUI(int(px), int(py))) {
|
||||
ui_touch_finger_id_ = finger_id;
|
||||
ui_gesture_ = UiGesture::Pending;
|
||||
ui_start_x_ = px;
|
||||
ui_start_y_ = py;
|
||||
ui_down_time_ = get_time_sec();
|
||||
PythonBoot::UIMouseMove(int(px), int(py));
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Mobile-game layout: the left half moves (floating stick), the right half looks.
|
||||
if (controls_visible && norm_x < kMoveZoneRight && norm_y > kMoveZoneTop && !joystick_active_) {
|
||||
joystick_active_ = true;
|
||||
joystick_finger_id_ = finger_id;
|
||||
joystick_base_x_ = px;
|
||||
joystick_base_y_ = py;
|
||||
joystick_knob_x_ = px;
|
||||
joystick_knob_y_ = py;
|
||||
update_joystick_motion(px, py);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!controls_visible)
|
||||
return false;
|
||||
|
||||
const bool in_look_zone = norm_x >= kMoveZoneRight;
|
||||
if (look_finger_id_ < 0) {
|
||||
// Outside the look zone the finger can only tap-select.
|
||||
look_finger_id_ = finger_id;
|
||||
look_can_rotate_ = in_look_zone;
|
||||
look_rotating_ = false;
|
||||
look_moved_ = false;
|
||||
look_start_x_ = look_last_x_ = px;
|
||||
look_start_y_ = look_last_y_ = py;
|
||||
look_down_time_ = get_time_sec();
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::UIMouseMove(int(px), int(py)); // start picking under the finger
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
if (pinch_finger_id_ < 0 && look_can_rotate_ && in_look_zone) {
|
||||
pinch_finger_id_ = finger_id;
|
||||
pinch_x_ = px;
|
||||
pinch_y_ = py;
|
||||
pinch_last_dist_ = std::hypot(px - look_last_x_, py - look_last_y_);
|
||||
look_rotating_ = false;
|
||||
look_moved_ = true; // a pinch never ends as a tap
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handles finger motion
|
||||
bool on_finger_motion(int64_t finger_id, float norm_x, float norm_y) {
|
||||
if (!enabled_) return false;
|
||||
const float px = norm_x * float(screen_w_);
|
||||
const float py = norm_y * float(screen_h_);
|
||||
|
||||
// 1. UI Touch dragging (e.g. dragging item in inventory or scrollbar)
|
||||
if (ui_touch_finger_id_ == finger_id) {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
if (ui_gesture_ == UiGesture::Pending &&
|
||||
std::hypot(px - ui_start_x_, py - ui_start_y_) > kUiDragSlop) {
|
||||
// 40250 drag: press where the finger landed (a slot attaches its icon, a title bar
|
||||
// or scroll bar captures), then follow the finger.
|
||||
ui_gesture_ = UiGesture::Dragging;
|
||||
last_tap_time_ = -1.0;
|
||||
PythonBoot::UIMouseButton(1, true, int(ui_start_x_), int(ui_start_y_));
|
||||
}
|
||||
PythonBoot::UIMouseMove(int(px), int(py));
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Virtual Joystick finger
|
||||
if (joystick_active_ && finger_id == joystick_finger_id_) {
|
||||
update_joystick_motion(px, py);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. Button drag tracking (check if finger slid off)
|
||||
for (auto& btn : buttons_) {
|
||||
if (btn.finger_id == finger_id) {
|
||||
const float dist = std::hypot(px - btn.x, py - btn.y);
|
||||
if (dist > btn.radius * 1.5f && btn.pressed) {
|
||||
btn.pressed = false;
|
||||
trigger_button(btn.dik, false);
|
||||
} else if (dist <= btn.radius * 1.5f && !btn.pressed) {
|
||||
btn.pressed = true;
|
||||
trigger_button(btn.dik, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Two-finger pinch zoom (look finger + second right-half finger)
|
||||
if (pinch_finger_id_ >= 0 && (finger_id == pinch_finger_id_ || finger_id == look_finger_id_)) {
|
||||
if (finger_id == pinch_finger_id_) {
|
||||
pinch_x_ = px;
|
||||
pinch_y_ = py;
|
||||
} else {
|
||||
look_last_x_ = px;
|
||||
look_last_y_ = py;
|
||||
}
|
||||
const float cur_dist = std::hypot(pinch_x_ - look_last_x_, pinch_y_ - look_last_y_);
|
||||
const float delta_d = cur_dist - pinch_last_dist_;
|
||||
if (std::abs(delta_d) > 2.0f) {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::UIMouseWheel(int(delta_d * 8.0f));
|
||||
#endif
|
||||
pinch_last_dist_ = cur_dist;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 5. Single-finger look drag: yaw by dx, pitch by dy, stops the moment the finger stops.
|
||||
if (finger_id == look_finger_id_) {
|
||||
if (!look_moved_ && std::hypot(px - look_start_x_, py - look_start_y_) > kLookDeadZone) {
|
||||
look_moved_ = true;
|
||||
look_rotating_ = look_can_rotate_;
|
||||
look_last_x_ = px; // start from here so the dead zone does not jump the camera
|
||||
look_last_y_ = py;
|
||||
}
|
||||
if (look_rotating_) {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::CameraRotateBy((px - look_last_x_) * kLookYawDegPerPx,
|
||||
(py - look_last_y_) * kLookPitchDegPerPx);
|
||||
#endif
|
||||
}
|
||||
look_last_x_ = px;
|
||||
look_last_y_ = py;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handles finger touch up
|
||||
bool on_finger_up(int64_t finger_id, float norm_x, float norm_y) {
|
||||
if (!enabled_) return false;
|
||||
const float px = norm_x * float(screen_w_);
|
||||
const float py = norm_y * float(screen_h_);
|
||||
|
||||
// 1. UI Touch release (e.g. dropped item in inventory or clicked button)
|
||||
if (ui_touch_finger_id_ == finger_id) {
|
||||
ui_touch_finger_id_ = -1;
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
if (ui_gesture_ == UiGesture::Dragging) {
|
||||
// Dropped on a skill button: that quick slot takes the dragged icon.
|
||||
const int btn_idx = gameplay_controls_visible() ? find_button(px, py) : -1;
|
||||
if (btn_idx >= 0 && is_quick_slot_button(buttons_[btn_idx].id) && PythonBoot::UIIsAttaching()) {
|
||||
PythonBoot::TryAssignAttachedObjectToLocalQuickSlot(buttons_[btn_idx].id - 2);
|
||||
PythonBoot::UIMouseButton(1, false, int(ui_start_x_), int(ui_start_y_));
|
||||
} else {
|
||||
PythonBoot::UIMouseButton(1, false, int(px), int(py));
|
||||
}
|
||||
} else if (ui_gesture_ == UiGesture::Pending) {
|
||||
const double now = get_time_sec();
|
||||
const int x = int(ui_start_x_), y = int(ui_start_y_);
|
||||
if (last_tap_time_ >= 0.0 && now - last_tap_time_ < kDoubleTapSec &&
|
||||
std::hypot(ui_start_x_ - last_tap_x_, ui_start_y_ - last_tap_y_) < kDoubleTapSlop) {
|
||||
// Win32 sends DOWN, UP, DBLCLK, UP; the first click picked the icon up, which a
|
||||
// mouse user would have seen and a finger hides, so put it back first.
|
||||
if (last_tap_attached_ && PythonBoot::UIIsAttaching())
|
||||
PythonBoot::UIDeattachObject();
|
||||
PythonBoot::UIMouseDoubleClick(x, y);
|
||||
PythonBoot::UIMouseButton(1, false, x, y);
|
||||
last_tap_time_ = -1.0;
|
||||
} else {
|
||||
const bool was_attaching = PythonBoot::UIIsAttaching();
|
||||
PythonBoot::UIMouseButton(1, true, x, y);
|
||||
PythonBoot::UIMouseButton(1, false, x, y);
|
||||
last_tap_time_ = now;
|
||||
last_tap_x_ = ui_start_x_;
|
||||
last_tap_y_ = ui_start_y_;
|
||||
last_tap_attached_ = !was_attaching && PythonBoot::UIIsAttaching();
|
||||
}
|
||||
keyboard_requested_ = PythonBoot::TextInputFocused() &&
|
||||
PythonBoot::IsPointInsideFocusedWindow(x, y);
|
||||
if (keyboard_requested_) ++keyboard_serial_;
|
||||
}
|
||||
ui_gesture_ = UiGesture::None;
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Joystick release
|
||||
if (joystick_active_ && finger_id == joystick_finger_id_) {
|
||||
joystick_active_ = false;
|
||||
joystick_finger_id_ = -1;
|
||||
joystick_knob_x_ = joystick_base_x_;
|
||||
joystick_knob_y_ = joystick_base_y_;
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::SetMoveDirection(0.0f, false);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. Button release
|
||||
for (auto& btn : buttons_) {
|
||||
if (btn.finger_id == finger_id) {
|
||||
if (btn.pressed) {
|
||||
btn.pressed = false;
|
||||
trigger_button(btn.dik, false);
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
if (btn.id == kAutoHuntButton && !auto_hunt_long_pressed_)
|
||||
PythonBoot::AutoHuntToggle();
|
||||
#endif
|
||||
}
|
||||
btn.finger_id = -1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Pinch end: the finger left down keeps looking (never a tap).
|
||||
if (pinch_finger_id_ >= 0 && (finger_id == pinch_finger_id_ || finger_id == look_finger_id_)) {
|
||||
if (finger_id == look_finger_id_) {
|
||||
look_finger_id_ = pinch_finger_id_;
|
||||
look_last_x_ = pinch_x_;
|
||||
look_last_y_ = pinch_y_;
|
||||
}
|
||||
pinch_finger_id_ = -1;
|
||||
pinch_last_dist_ = 0.0f;
|
||||
look_rotating_ = look_can_rotate_;
|
||||
look_moved_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 5. Look finger release; a short still touch selects a target (mob, NPC, ground).
|
||||
if (finger_id == look_finger_id_) {
|
||||
look_finger_id_ = -1;
|
||||
look_rotating_ = false;
|
||||
keyboard_requested_ = false;
|
||||
if (!look_moved_ && (get_time_sec() - look_down_time_) < kTapMaxSec && tap_stage_ == 0) {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::UIMouseMove(int(px), int(py));
|
||||
tap_x_ = px;
|
||||
tap_y_ = py;
|
||||
tap_stage_ = 1;
|
||||
#endif
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Overlay only the controls that have no practical desktop-UI touch equivalent.
|
||||
void append_ui_commands(std::vector<UIRenderCommand>& commands) const {
|
||||
if (!enabled_ || !gameplay_controls_visible()) return;
|
||||
|
||||
// Right-side attack and quick-slot buttons.
|
||||
for (const auto& btn : buttons_) {
|
||||
const uint32_t col = btn.pressed ? btn.color_pressed : btn.color_idle;
|
||||
draw_filled_disc(commands, btn.x, btn.y, btn.radius, col);
|
||||
draw_circle(commands, btn.x, btn.y, btn.radius * 0.85f, btn.pressed ? 0xFFFFFFFF : 0x70FFFFFF, 16);
|
||||
|
||||
const uint32_t icon_col = btn.pressed ? 0xFFFFFFFF : 0xDDFFFFFF;
|
||||
const float r = btn.radius;
|
||||
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
if (btn.id == kAutoHuntButton) {
|
||||
draw_auto_hunt_button(commands, btn);
|
||||
continue;
|
||||
}
|
||||
if (is_quick_slot_button(btn.id)) {
|
||||
std::string skill_icon;
|
||||
float uv[4];
|
||||
int count = 0;
|
||||
if (PythonBoot::LocalQuickSlotIcon(btn.id - 2, &skill_icon, uv, &count)) {
|
||||
UIRenderCommand icon{};
|
||||
icon.kind = UIRenderCommand::Image;
|
||||
icon.x1 = btn.x - r * 0.68f;
|
||||
icon.y1 = btn.y - r * 0.68f;
|
||||
icon.x2 = btn.x + r * 0.68f;
|
||||
icon.y2 = btn.y + r * 0.68f;
|
||||
icon.argb = icon_col;
|
||||
icon.text = skill_icon;
|
||||
icon.su = uv[0];
|
||||
icon.sv = uv[1];
|
||||
icon.eu = uv[2];
|
||||
icon.ev = uv[3];
|
||||
commands.push_back(std::move(icon));
|
||||
if (count > 0) {
|
||||
const std::string digits = std::to_string(std::min(count, 9999));
|
||||
const float h = r * 0.26f;
|
||||
draw_segment_text(commands, digits, btn.x + r * 0.62f - segment_text_width(digits, h),
|
||||
btn.y + r * 0.62f - h, h);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
switch (btn.id) {
|
||||
case 1: { // ATK: crossed swords
|
||||
const float s = r * 0.35f;
|
||||
draw_line(commands, btn.x - s, btn.y - s, btn.x + s, btn.y + s, icon_col);
|
||||
draw_line(commands, btn.x + s, btn.y - s, btn.x - s, btn.y + s, icon_col);
|
||||
const float g = s * 0.35f;
|
||||
draw_line(commands, btn.x - s*0.4f - g, btn.y - s*0.4f + g, btn.x - s*0.4f + g, btn.y - s*0.4f - g, icon_col);
|
||||
draw_line(commands, btn.x + s*0.4f - g, btn.y - s*0.4f - g, btn.x + s*0.4f + g, btn.y - s*0.4f + g, icon_col);
|
||||
break;
|
||||
}
|
||||
case 2: { // S1: I
|
||||
const float h = r * 0.35f;
|
||||
draw_line(commands, btn.x, btn.y - h, btn.x, btn.y + h, icon_col);
|
||||
draw_line(commands, btn.x - 4.0f, btn.y - h, btn.x + 4.0f, btn.y - h, icon_col);
|
||||
draw_line(commands, btn.x - 4.0f, btn.y + h, btn.x + 4.0f, btn.y + h, icon_col);
|
||||
break;
|
||||
}
|
||||
case 3: { // S2: II
|
||||
const float h = r * 0.35f;
|
||||
draw_line(commands, btn.x - 4.0f, btn.y - h, btn.x - 4.0f, btn.y + h, icon_col);
|
||||
draw_line(commands, btn.x + 4.0f, btn.y - h, btn.x + 4.0f, btn.y + h, icon_col);
|
||||
break;
|
||||
}
|
||||
case 4: { // S3: III
|
||||
const float h = r * 0.35f;
|
||||
draw_line(commands, btn.x - 6.0f, btn.y - h, btn.x - 6.0f, btn.y + h, icon_col);
|
||||
draw_line(commands, btn.x, btn.y - h, btn.x, btn.y + h, icon_col);
|
||||
draw_line(commands, btn.x + 6.0f, btn.y - h, btn.x + 6.0f, btn.y + h, icon_col);
|
||||
break;
|
||||
}
|
||||
case 5: { // S4: IV
|
||||
const float h = r * 0.35f;
|
||||
draw_line(commands, btn.x - 7.0f, btn.y - h, btn.x - 7.0f, btn.y + h, icon_col);
|
||||
draw_line(commands, btn.x, btn.y - h, btn.x + 7.0f, btn.y + h, icon_col);
|
||||
draw_line(commands, btn.x + 14.0f, btn.y - h, btn.x + 7.0f, btn.y + h, icon_col);
|
||||
break;
|
||||
}
|
||||
case 6: { // S5: V
|
||||
const float h = r * 0.35f;
|
||||
draw_line(commands, btn.x - 8.0f, btn.y - h, btn.x, btn.y + h, icon_col);
|
||||
draw_line(commands, btn.x + 8.0f, btn.y - h, btn.x, btn.y + h, icon_col);
|
||||
break;
|
||||
}
|
||||
case 7: { // S6: VI
|
||||
const float h = r * 0.35f;
|
||||
draw_line(commands, btn.x - 11.0f, btn.y - h, btn.x - 3.0f, btn.y + h, icon_col);
|
||||
draw_line(commands, btn.x + 5.0f, btn.y - h, btn.x - 3.0f, btn.y + h, icon_col);
|
||||
draw_line(commands, btn.x + 11.0f, btn.y - h, btn.x + 11.0f, btn.y + h, icon_col);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Left-side movement joystick.
|
||||
draw_circle(commands, joystick_base_x_, joystick_base_y_, joystick_radius_, 0x8080C0FF, 24);
|
||||
draw_circle(commands, joystick_base_x_, joystick_base_y_, joystick_radius_ * 0.45f, 0x4080C0FF, 16);
|
||||
draw_line(commands, joystick_base_x_ - joystick_radius_, joystick_base_y_,
|
||||
joystick_base_x_ - joystick_radius_ + 8.0f, joystick_base_y_, 0x90FFFFFF);
|
||||
draw_line(commands, joystick_base_x_ + joystick_radius_ - 8.0f, joystick_base_y_,
|
||||
joystick_base_x_ + joystick_radius_, joystick_base_y_, 0x90FFFFFF);
|
||||
draw_line(commands, joystick_base_x_, joystick_base_y_ - joystick_radius_,
|
||||
joystick_base_x_, joystick_base_y_ - joystick_radius_ + 8.0f, 0x90FFFFFF);
|
||||
draw_line(commands, joystick_base_x_, joystick_base_y_ + joystick_radius_ - 8.0f,
|
||||
joystick_base_x_, joystick_base_y_ + joystick_radius_, 0x90FFFFFF);
|
||||
|
||||
if (joystick_active_) {
|
||||
draw_line(commands, joystick_base_x_, joystick_base_y_, joystick_knob_x_, joystick_knob_y_, 0xB000FFFF);
|
||||
}
|
||||
const uint32_t knob_color = joystick_active_ ? 0xB040A0FF : 0x6040A0FF;
|
||||
draw_filled_disc(commands, joystick_knob_x_, joystick_knob_y_, joystick_knob_radius_, knob_color);
|
||||
draw_circle(commands, joystick_knob_x_, joystick_knob_y_, joystick_knob_radius_ * 0.5f, 0x80FFFFFF, 12);
|
||||
}
|
||||
|
||||
// Desktop mouse testing simulation
|
||||
bool on_mouse_button(int button, bool pressed, int x, int y) {
|
||||
if (!enabled_) return false;
|
||||
const float norm_x = float(x) / float(screen_w_);
|
||||
const float norm_y = float(y) / float(screen_h_);
|
||||
if (pressed) {
|
||||
if (button == 1) {
|
||||
return on_finger_down(101, norm_x, norm_y);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
if (button == 1) {
|
||||
return on_finger_up(101, norm_x, norm_y);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool on_mouse_motion(int x, int y) {
|
||||
if (!enabled_) return false;
|
||||
const float norm_x = float(x) / float(screen_w_);
|
||||
const float norm_y = float(y) / float(screen_h_);
|
||||
bool handled = false;
|
||||
if (ui_touch_finger_id_ == 101) {
|
||||
handled |= on_finger_motion(101, norm_x, norm_y);
|
||||
}
|
||||
if (joystick_active_ && joystick_finger_id_ == 101) {
|
||||
handled |= on_finger_motion(101, norm_x, norm_y);
|
||||
}
|
||||
if (look_finger_id_ == 101) {
|
||||
handled |= on_finger_motion(101, norm_x, norm_y);
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
|
||||
// Seven-segment text (digits, 'F', 'P', 'S', ' ') with its top-left at (left, top), each glyph h
|
||||
// tall, on a dark backing. Used for quick-slot counts and the test-build FPS readout.
|
||||
static void draw_segment_text(std::vector<UIRenderCommand>& commands, const std::string& text,
|
||||
float left, float top, float h, std::uint32_t color = 0xFFFFFFFF) {
|
||||
static constexpr std::uint8_t kDigits[10] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};
|
||||
const float w = h * 0.55f, gap = h * 0.3f;
|
||||
UIRenderCommand back{};
|
||||
back.kind = UIRenderCommand::Bar;
|
||||
back.x1 = left - 3.0f;
|
||||
back.y1 = top - 3.0f;
|
||||
back.x2 = left + segment_text_width(text, h) + 3.0f;
|
||||
back.y2 = top + h + 3.0f;
|
||||
back.argb = 0xA0000000;
|
||||
commands.push_back(back);
|
||||
float x = left;
|
||||
for (char c : text) {
|
||||
std::uint8_t seg = 0;
|
||||
if (c >= '0' && c <= '9') seg = kDigits[c - '0'];
|
||||
else if (c == 'F') seg = 0x71;
|
||||
else if (c == 'P') seg = 0x73;
|
||||
else if (c == 'S') seg = 0x6D;
|
||||
const float t = top, m = top + h * 0.5f, b = top + h;
|
||||
if (seg & 0x01) draw_line(commands, x, t, x + w, t, color);
|
||||
if (seg & 0x02) draw_line(commands, x + w, t, x + w, m, color);
|
||||
if (seg & 0x04) draw_line(commands, x + w, m, x + w, b, color);
|
||||
if (seg & 0x08) draw_line(commands, x, b, x + w, b, color);
|
||||
if (seg & 0x10) draw_line(commands, x, m, x, b, color);
|
||||
if (seg & 0x20) draw_line(commands, x, t, x, m, color);
|
||||
if (seg & 0x40) draw_line(commands, x, m, x + w, m, color);
|
||||
x += w + gap;
|
||||
}
|
||||
}
|
||||
static float segment_text_width(const std::string& text, float h) {
|
||||
return text.empty() ? 0.0f : float(text.size()) * h * 0.85f - h * 0.3f;
|
||||
}
|
||||
|
||||
private:
|
||||
bool gameplay_controls_visible() const {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
return !PythonBoot::CurrentMapName().empty();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void release_gameplay_controls() {
|
||||
if (joystick_active_) {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::SetMoveDirection(0.0f, false);
|
||||
#endif
|
||||
}
|
||||
joystick_active_ = false;
|
||||
joystick_finger_id_ = -1;
|
||||
joystick_knob_x_ = joystick_base_x_;
|
||||
joystick_knob_y_ = joystick_base_y_;
|
||||
for (auto& btn : buttons_) {
|
||||
if (btn.pressed)
|
||||
trigger_button(btn.dik, false);
|
||||
btn.pressed = false;
|
||||
btn.finger_id = -1;
|
||||
}
|
||||
look_finger_id_ = -1;
|
||||
look_rotating_ = false;
|
||||
pinch_finger_id_ = -1;
|
||||
pinch_last_dist_ = 0.0f;
|
||||
}
|
||||
|
||||
static constexpr int kAutoHuntButton = 8;
|
||||
static bool is_quick_slot_button(int id) { return id >= 2 && id <= 7; }
|
||||
|
||||
// A circular arrow; green while the hunt runs.
|
||||
static void draw_auto_hunt_button(std::vector<UIRenderCommand>& commands, const ButtonDef& btn) {
|
||||
bool on = false;
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
on = PythonBoot::AutoHuntIsEnabled();
|
||||
#endif
|
||||
const uint32_t disc = btn.pressed ? btn.color_pressed : (on ? 0xC040C040 : btn.color_idle);
|
||||
draw_filled_disc(commands, btn.x, btn.y, btn.radius, disc);
|
||||
const uint32_t col = on ? 0xFFB0FFB0 : 0xDDFFFFFF;
|
||||
const float r = btn.radius * 0.45f;
|
||||
const float kPi = 3.14159265f;
|
||||
const int segments = 12;
|
||||
const float a0 = -kPi * 0.35f, a1 = a0 + kPi * 1.6f;
|
||||
for (int i = 0; i < segments; ++i) {
|
||||
const float t0 = a0 + (a1 - a0) * float(i) / float(segments);
|
||||
const float t1 = a0 + (a1 - a0) * float(i + 1) / float(segments);
|
||||
draw_line(commands, btn.x + std::cos(t0) * r, btn.y + std::sin(t0) * r,
|
||||
btn.x + std::cos(t1) * r, btn.y + std::sin(t1) * r, col);
|
||||
}
|
||||
// Arrowhead at the arc's end, pointing along the direction of travel.
|
||||
const float ex = btn.x + std::cos(a1) * r, ey = btn.y + std::sin(a1) * r;
|
||||
const float tx = -std::sin(a1), ty = std::cos(a1);
|
||||
const float nx = std::cos(a1), ny = std::sin(a1);
|
||||
const float h = r * 0.55f;
|
||||
draw_line(commands, ex, ey, ex - tx * h + nx * h * 0.6f, ey - ty * h + ny * h * 0.6f, col);
|
||||
draw_line(commands, ex, ey, ex - tx * h - nx * h * 0.6f, ey - ty * h - ny * h * 0.6f, col);
|
||||
if (on) {
|
||||
// A play dot in the middle while running.
|
||||
draw_rect_bar(commands, btn.x - 2.0f, btn.y - 2.0f, btn.x + 2.0f, btn.y + 2.0f, col);
|
||||
}
|
||||
}
|
||||
|
||||
void init_buttons() {
|
||||
buttons_.clear();
|
||||
// Lower-right combat controls. All menus and status UI remain the original 40250 UI.
|
||||
buttons_.push_back({1, 0x39, 0, 0, 42.0f, "ATK", 0x80D48820, 0xD0FFB040});
|
||||
buttons_.push_back({2, 0x02, 0, 0, 26.0f, "S1", 0x803060C0, 0xD05080FF});
|
||||
buttons_.push_back({3, 0x03, 0, 0, 26.0f, "S2", 0x80903090, 0xD0D050D0});
|
||||
buttons_.push_back({4, 0x04, 0, 0, 26.0f, "S3", 0x80309060, 0xD050D080});
|
||||
buttons_.push_back({5, 0x05, 0, 0, 26.0f, "S4", 0x80906030, 0xD0D08050});
|
||||
buttons_.push_back({6, 0x06, 0, 0, 26.0f, "S5", 0x80603090, 0xD08050D0});
|
||||
buttons_.push_back({7, 0x07, 0, 0, 26.0f, "S6", 0x80308090, 0xD050C0D0});
|
||||
buttons_.push_back({kAutoHuntButton, 0, 0, 0, 24.0f, "AUTO", 0x80707070, 0xD0FFFFFF});
|
||||
}
|
||||
|
||||
void layout_buttons() {
|
||||
const float W = float(screen_w_) - safe_inset_;
|
||||
const float H = float(screen_h_);
|
||||
for (auto& btn : buttons_) {
|
||||
switch (btn.id) {
|
||||
case 1:
|
||||
btn.x = W - 75.0f;
|
||||
btn.y = H - 85.0f;
|
||||
btn.radius = std::min(46.0f, H * 0.12f);
|
||||
break;
|
||||
case 2:
|
||||
btn.x = W - 360.0f;
|
||||
btn.y = H - 75.0f;
|
||||
btn.radius = std::min(28.0f, H * 0.08f);
|
||||
break;
|
||||
case 3:
|
||||
btn.x = W - 295.0f;
|
||||
btn.y = H - 75.0f;
|
||||
btn.radius = std::min(28.0f, H * 0.08f);
|
||||
break;
|
||||
case 4:
|
||||
btn.x = W - 230.0f;
|
||||
btn.y = H - 75.0f;
|
||||
btn.radius = std::min(28.0f, H * 0.08f);
|
||||
break;
|
||||
case 5:
|
||||
btn.x = W - 165.0f;
|
||||
btn.y = H - 85.0f;
|
||||
btn.radius = std::min(28.0f, H * 0.08f);
|
||||
break;
|
||||
case 6:
|
||||
btn.x = W - 125.0f;
|
||||
btn.y = H - 155.0f;
|
||||
btn.radius = std::min(28.0f, H * 0.08f);
|
||||
break;
|
||||
case 7:
|
||||
btn.x = W - 75.0f;
|
||||
btn.y = H - 225.0f;
|
||||
btn.radius = std::min(28.0f, H * 0.08f);
|
||||
break;
|
||||
case kAutoHuntButton:
|
||||
btn.x = W - 75.0f;
|
||||
btn.y = H - 295.0f;
|
||||
btn.radius = std::min(24.0f, H * 0.07f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int find_button(float x, float y) {
|
||||
for (size_t i = 0; i < buttons_.size(); ++i) {
|
||||
const auto& btn = buttons_[i];
|
||||
const float dist = std::hypot(x - btn.x, y - btn.y);
|
||||
if (dist <= btn.radius * 1.25f) {
|
||||
return int(i);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void trigger_button(int dik, bool pressed) {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
if (dik == 0x39) {
|
||||
PythonBoot::SetAttackKey(pressed);
|
||||
}
|
||||
if (dik >= 0x02 && dik <= 0x07) {
|
||||
if (pressed)
|
||||
PythonBoot::UseLocalQuickSlot(dik - 0x02);
|
||||
return;
|
||||
}
|
||||
if (dik != 0) {
|
||||
PythonBoot::UIKey(dik, pressed);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void update_joystick_motion(float px, float py) {
|
||||
const float dx = px - joystick_base_x_;
|
||||
const float dy = py - joystick_base_y_;
|
||||
const float dist = std::hypot(dx, dy);
|
||||
|
||||
if (dist <= joystick_radius_) {
|
||||
joystick_knob_x_ = px;
|
||||
joystick_knob_y_ = py;
|
||||
} else if (dist > 0.0f) {
|
||||
joystick_knob_x_ = joystick_base_x_ + (dx / dist) * joystick_radius_;
|
||||
joystick_knob_y_ = joystick_base_y_ + (dy / dist) * joystick_radius_;
|
||||
}
|
||||
|
||||
if (dist > 8.0f) {
|
||||
const float rad = std::atan2(-dx, -dy);
|
||||
move_angle_ = rad * 180.0f / 3.14159265358979323846f;
|
||||
if (move_angle_ < 0.0f) move_angle_ += 360.0f;
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::SetMoveDirection(move_angle_, true);
|
||||
#endif
|
||||
} else {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::SetMoveDirection(0.0f, false);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_line(std::vector<UIRenderCommand>& commands,
|
||||
float x1, float y1, float x2, float y2, uint32_t argb) {
|
||||
UIRenderCommand cmd{};
|
||||
cmd.kind = UIRenderCommand::Line;
|
||||
cmd.x1 = x1; cmd.y1 = y1;
|
||||
cmd.x2 = x2; cmd.y2 = y2;
|
||||
cmd.argb = argb;
|
||||
commands.push_back(cmd);
|
||||
}
|
||||
|
||||
static void draw_rect_bar(std::vector<UIRenderCommand>& commands,
|
||||
float x1, float y1, float x2, float y2, uint32_t argb) {
|
||||
UIRenderCommand bar{};
|
||||
bar.kind = UIRenderCommand::Bar;
|
||||
bar.x1 = x1; bar.y1 = y1;
|
||||
bar.x2 = x2; bar.y2 = y2;
|
||||
bar.argb = argb;
|
||||
commands.push_back(bar);
|
||||
}
|
||||
|
||||
static void draw_rect_lines(std::vector<UIRenderCommand>& commands,
|
||||
float x1, float y1, float x2, float y2, uint32_t argb) {
|
||||
draw_line(commands, x1, y1, x2, y1, argb);
|
||||
draw_line(commands, x2, y1, x2, y2, argb);
|
||||
draw_line(commands, x2, y2, x1, y2, argb);
|
||||
draw_line(commands, x1, y2, x1, y1, argb);
|
||||
}
|
||||
|
||||
static void draw_circle(std::vector<UIRenderCommand>& commands,
|
||||
float cx, float cy, float radius, uint32_t argb, int segments = 16) {
|
||||
const float step = 2.0f * 3.14159265f / float(segments);
|
||||
for (int i = 0; i < segments; ++i) {
|
||||
const float a1 = float(i) * step;
|
||||
const float a2 = float(i + 1) * step;
|
||||
draw_line(commands,
|
||||
cx + std::cos(a1) * radius, cy + std::sin(a1) * radius,
|
||||
cx + std::cos(a2) * radius, cy + std::sin(a2) * radius,
|
||||
argb);
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_filled_disc(std::vector<UIRenderCommand>& commands,
|
||||
float cx, float cy, float radius, uint32_t argb) {
|
||||
// Base rectangular fill
|
||||
draw_rect_bar(commands, cx - radius * 0.65f, cy - radius * 0.65f, cx + radius * 0.65f, cy + radius * 0.65f, (argb & 0x00FFFFFF) | 0x55000000);
|
||||
// Cross fills for roundness
|
||||
draw_rect_bar(commands, cx - radius * 0.85f, cy - radius * 0.35f, cx + radius * 0.85f, cy + radius * 0.35f, (argb & 0x00FFFFFF) | 0x55000000);
|
||||
draw_rect_bar(commands, cx - radius * 0.35f, cy - radius * 0.85f, cx + radius * 0.35f, cy + radius * 0.85f, (argb & 0x00FFFFFF) | 0x55000000);
|
||||
// Border rings
|
||||
draw_circle(commands, cx, cy, radius, argb, 20);
|
||||
draw_circle(commands, cx, cy, radius - 1.0f, (argb & 0x00FFFFFF) | 0x40000000, 20);
|
||||
}
|
||||
|
||||
static double get_time_sec() {
|
||||
using namespace std::chrono;
|
||||
return duration_cast<duration<double>>(steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
bool enabled_ = false;
|
||||
int screen_w_ = 1280;
|
||||
int screen_h_ = 720;
|
||||
float safe_inset_ = 0.0f;
|
||||
|
||||
bool controls_were_visible_ = false;
|
||||
int64_t ui_touch_finger_id_ = -1;
|
||||
enum class UiGesture { None, Pending, Dragging, LongPressed };
|
||||
UiGesture ui_gesture_ = UiGesture::None;
|
||||
float ui_start_x_ = 0.0f;
|
||||
float ui_start_y_ = 0.0f;
|
||||
double ui_down_time_ = 0.0;
|
||||
double last_tap_time_ = -1.0;
|
||||
float last_tap_x_ = 0.0f;
|
||||
float last_tap_y_ = 0.0f;
|
||||
bool last_tap_attached_ = false;
|
||||
void (*haptic_)() = nullptr;
|
||||
static constexpr float kUiDragSlop = 10.0f;
|
||||
static constexpr double kLongPressSec = 0.5;
|
||||
static constexpr double kDoubleTapSec = 0.35;
|
||||
static constexpr float kDoubleTapSlop = 24.0f;
|
||||
bool keyboard_requested_ = false;
|
||||
unsigned keyboard_serial_ = 0;
|
||||
|
||||
bool joystick_active_ = false;
|
||||
int64_t joystick_finger_id_ = -1;
|
||||
float joystick_base_x_ = 140.0f;
|
||||
float joystick_base_y_ = 580.0f;
|
||||
float joystick_knob_x_ = 140.0f;
|
||||
float joystick_knob_y_ = 580.0f;
|
||||
float joystick_radius_ = 65.0f;
|
||||
float joystick_knob_radius_ = 28.0f;
|
||||
float move_angle_ = 0.0f;
|
||||
|
||||
// Zones are fractions of the screen; distances are logical UI pixels.
|
||||
static constexpr float kMoveZoneRight = 0.5f;
|
||||
static constexpr float kMoveZoneTop = 0.2f;
|
||||
static constexpr float kLookDeadZone = 8.0f;
|
||||
static constexpr float kLookYawDegPerPx = 0.25f;
|
||||
static constexpr float kLookPitchDegPerPx = 0.15f;
|
||||
static constexpr double kTapMaxSec = 0.3;
|
||||
|
||||
int64_t look_finger_id_ = -1;
|
||||
bool look_can_rotate_ = false;
|
||||
bool look_rotating_ = false;
|
||||
bool look_moved_ = false;
|
||||
float look_start_x_ = 0.0f;
|
||||
float look_start_y_ = 0.0f;
|
||||
float look_last_x_ = 0.0f;
|
||||
float look_last_y_ = 0.0f;
|
||||
double look_down_time_ = 0.0;
|
||||
|
||||
int64_t pinch_finger_id_ = -1;
|
||||
float pinch_x_ = 0.0f;
|
||||
float pinch_y_ = 0.0f;
|
||||
float pinch_last_dist_ = 0.0f;
|
||||
|
||||
int tap_stage_ = 0; // 1: press next frame, 2: release next frame
|
||||
float tap_x_ = 0.0f;
|
||||
float tap_y_ = 0.0f;
|
||||
|
||||
std::vector<ButtonDef> buttons_;
|
||||
double auto_hunt_down_time_ = 0.0;
|
||||
bool auto_hunt_long_pressed_ = false;
|
||||
};
|
||||
Reference in New Issue
Block a user