Files
mtgodot-poc/native_render
shenleiandClaude Opus 5.5 53d06c3c26 android-native: real-server login by default, readable CJK text, 16 KB pages
- FreeType: add the cff/psaux/pshinter modules (from the pinned 2.13.3
  tarball, same sha256). Android's only CJK font is the CFF2
  NotoSansCJK-Regular.ttc, which the TrueType-only build could not open,
  so every Chinese glyph drew as a box.
- Font oversample rounds the UI density up: Android's 1760x800 UI on a
  2376x1080 screen is 1.35, which rounded to 1x bilevel glyphs stretched
  1.35x. Now 2x anti-aliased glyphs are scaled down. macOS 2.0 unchanged.
- MainActivity: a launcher start (no intent args) opens the login screen
  against 192.168.21.203 instead of auto-logging into the in-process fake
  server; explicit args still select the fake-server smoke flow.
- build-android-native.sh: ANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON for SDL3
  and libmain (LOAD alignment 0x1000 -> 0x4000), with a stamp that
  rebuilds a cached pre-flag SDL3. Clears Android's 16 KB compat warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:16:38 +09:00
..

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:

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

./build-android-native.sh [Debug|Release] [--install] builds the arm64 APK (android-native/app/build/outputs/apk/debug/app-debug.apk, package org.metin2port.client): it downloads SDL 3.4.16 (sha256-checked) into build-android-native/_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-native/ (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. ./push-android-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:

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: extension/src/codepage carries Microsoft's WindowsBestFit tables for 874/932/936/949/950/1250-1258 (regenerate with python3 script/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):

# Interactive outdoor map session (auto-login via loopback FakeLoginServer)
./build-release/native_render/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/native_render/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/native_render/mt_native_render \
  --live-client "/path/to/40250/Server Client TMP4/Client" \
  --live-server 127.0.0.1:11002:13000 --login-screen

Synthetic benchmark

./build-release/native_render/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:

# First verify the runner and renderer using the local fake server.
node script/native_mac_acceptance.mjs --fake --frames 180

# Use the real server's shared host, auth port, and game channel port.
node script/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:

./build-release/native_render/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:

./build-release/native_render/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)

./build-release/native_render/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.