build-android-native.sh downloads SDL 3.4.16 (sha256-checked), builds libSDL3.so and libmain.so (native_render + port_platform + embedded Python) at API 24, and wraps them in the SDLActivity shell under android-native/ (package org.metin2port.client). push-android-client.sh copies the 40250 Client directory into the app's files/ with run-as (--locale XX selects locale_XX.cfg). port_logic links with --no-dependent-libraries on ELF targets so the verbatim `#pragma comment(lib, ...)` lines do not break lld. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
161 lines
10 KiB
Markdown
161 lines
10 KiB
Markdown
# Native Vulkan renderer prototype
|
||
|
||
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. The existing Godot client remains the default playable 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
|
||
|
||
# 1. Standalone replay build (without port_platform)
|
||
cmake -S . -B build-native-render -DMTGODOT_BUILD_EXTENSION=OFF \
|
||
-DMT_BUILD_NATIVE_RENDER_PROTOTYPE=ON -DCMAKE_PREFIX_PATH=/opt/homebrew
|
||
cmake --build build-native-render --target mt_native_render -j8
|
||
|
||
# 2. Full live-client build (links port_platform + mtpython + FakeLoginServer)
|
||
cmake -S . -B build -DMT_BUILD_NATIVE_RENDER_PROTOTYPE=ON -DCMAKE_PREFIX_PATH=/opt/homebrew
|
||
cmake --build build --target mtgodot mt_native_render port_fake_login_server -j8
|
||
|
||
# 3. Optimized Release (-O3) live-client build
|
||
cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release -DMT_BUILD_NATIVE_RENDER_PROTOTYPE=ON \
|
||
-DMTGODOT_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:
|
||
|
||
```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: `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):
|
||
|
||
```sh
|
||
# 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
|
||
|
||
```sh
|
||
./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:
|
||
|
||
```sh
|
||
# 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:
|
||
|
||
```sh
|
||
./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`:
|
||
|
||
```sh
|
||
./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)
|
||
|
||
```sh
|
||
./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.
|