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:
shenlei
2026-09-29 19:08:19 +09:00
co-authored by Claude Opus 5.5
parent 70710477cf
commit a46093104c
2817 changed files with 13728 additions and 13744 deletions
+5 -5
View File
@@ -1,17 +1,17 @@
# M0 工具。只依赖 libgr2。
add_executable(gr2dump gr2dump/main.cpp)
target_link_libraries(gr2dump PRIVATE xrender::libgr2)
target_link_libraries(gr2dump PRIVATE mt::libgr2)
add_executable(gr2fuzz gr2fuzz/main.cpp)
target_link_libraries(gr2fuzz PRIVATE xrender::libgr2)
target_link_libraries(gr2fuzz PRIVATE mt::libgr2)
# M2 门禁·主:libgr2 vs oracle 逐字段对拍(读 oracle/oracle.exe 产出的 .bin)
# M2 门禁·主:libgr2 vs oracle 逐字段对拍(读 tools/granny-oracle/oracle.exe 产出的 .bin)
add_executable(oracle_diff oracle_diff/main.cpp)
target_link_libraries(oracle_diff PRIVATE xrender::libgr2)
target_link_libraries(oracle_diff PRIVATE mt::libgr2)
# bisect:dump 每骨 world 3x3 的 det / shear / scale spread(查 dance_1 头塌陷)
add_executable(anim_probe anim_probe/main.cpp)
target_link_libraries(anim_probe PRIVATE xrender::libgr2)
target_link_libraries(anim_probe PRIVATE mt::libgr2)
# gr2dump --gltf 需要 tinygltf(M0 T8 辅助、可延后):
+46
View File
@@ -0,0 +1,46 @@
# oracle —— 独立 Windows 工程(不在主 CMake 树内)。见 ../docs/steps/00-oracle.md、RUNBOOK.md
#
# cmake -S oracle -B tools/granny-oracle/build -DGRANNY_SDK=<泄露 SDK 根>
# cmake --build tools/granny-oracle/build --config Release
#
# GRANNY_SDK 默认指向本仓库同级的 MobileSource 里的 Granny-3D-SDK-main。
# 链 granny2_x64.lib(import lib);运行时需要对应的 granny2_x64.dll 在 PATH / exe 目录。
# ⚠ 这个 DLL 是 SDK 2.9.12。资产由 2.4.0.7 导出(M0 T2 实测)。容器 + 组合公式在 2.4–2.11
# 稳定,但 B 样条边界 / 少数曲线细节可能有差异 —— 先跑 selfcheck(T7)确认坐标约定,
# 再决定是否要换成 2.4.x 的 runtime DLL(见 GRANNY-VERSION.md)。
cmake_minimum_required(VERSION 3.20)
project(xrender_oracle C)
set(CMAKE_C_STANDARD 11)
set(GRANNY_SDK "${CMAKE_SOURCE_DIR}/../../../MobileSource/Cross Platform/Granny-3D-SDK-main"
CACHE PATH "泄露 Granny SDK 根(含 include/ 和 lib/win64/)")
if(NOT EXISTS "${GRANNY_SDK}/include/granny.h")
message(FATAL_ERROR "找不到 ${GRANNY_SDK}/include/granny.h —— 传 -DGRANNY_SDK=<SDK 根>")
endif()
add_executable(oracle oracle.c)
target_include_directories(oracle PRIVATE "${GRANNY_SDK}/include")
if(WIN32)
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
target_link_libraries(oracle PRIVATE "${GRANNY_SDK}/lib/win64/granny2_x64.lib")
set(_dll "${GRANNY_SDK}/lib/win64/granny2_x64.dll")
else()
target_link_libraries(oracle PRIVATE "${GRANNY_SDK}/lib/win32/granny2.lib")
set(_dll "${GRANNY_SDK}/lib/win32/granny2.dll")
endif()
# 把 DLL 拷到 exe 旁边,方便直接跑 / Wine
add_custom_command(TARGET oracle POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_dll}" "$<TARGET_FILE_DIR:oracle>")
else()
message(WARNING
"非 Windows:SDK 只提供 win32/win64/wii/ps3/xenon 的 granny2 预编译库。\n"
" 选项 A:在 Windows 机器 / VM 上 build+run。\n"
" 选项 B:Wine(oracle 只做文件 IO + 数学 + 写二进制,不碰 GPU,Wine 够用)——\n"
" 用 MinGW 交叉编译或直接在 Wine 里跑 MSVC,链 granny2_x64.lib。\n"
" 选项 C(stretch):从 SDK source/ 移植 winxx/ 平台层出一个 POSIX libgranny。\n"
" 见 RUNBOOK.md。")
endif()
+45
View File
@@ -0,0 +1,45 @@
# oracle dump 二进制格式
> `oracle.c`(`oracle dump`)产出。libgr2 侧对拍读取器(`tools/oracle_diff/`)必须与本文件一致。
> `oracle dump` 把**层① 世界矩阵**和**mesh N 蒙皮顶点**写进**同一个文件**:先文件头,
> 再 `kind=1` 段,紧接着 `kind=3` 段(`--mesh` 未命中时 `vertex_count=0`)。全部小端。
## 文件头(所有 dump 共用)
```
magic "XORC" 4B
version u32 1
kind u32 1=层① 2=层② 3=蒙皮顶点 1
coord u32 坐标约定编码: 1
bit0 手系 (0=左手 1=右手)
bit1 单位 (0=原始 1=米)
bit2 根变换已 apply
granny_ver char[16] 选定 granny2.dll 版本串
model_path len-prefixed str
anim_path len-prefixed str
t f32 采样时刻
```
## kind=1/2 段(骨骼世界矩阵,紧跟文件头)
```
bone_count u32
matrices f32[bone_count][16] GrannyGetWorldPose4x4Array 直出(骨骼空间→世界),行主序 Granny 原生
```
> ⚠ 不是 `GrannyGetWorldPoseComposite4x4Array` —— 那个返回 `InverseWorld4x4·world` = **蒙皮矩阵**(bind pose 时 ≈ I)。
> `GrannyBuildWorldPose` 的 `Offset4x4` 传的是 model 的 `InitialPlacement`(`GrannyGetModelInitialPlacement4x4`),
> 与 libgr2 `skeleton.initial_placement` 一致;不传的话非单位 IP 的模型(如 assassin 双 root)会差一个平移。
## kind=3 段(mesh N 蒙皮顶点,紧跟 kind=1 段)
```
vertex_count u32 (--mesh 未命中 → 0,无后续)
positions f32[vertex_count][3] GrannyDeformVertices(PositionNormal) 后的位置
```
## 坐标约定
**M0/oracle 全程 raw**:`coord` = 手系按选定 DLL 实际输出、单位=原始、根变换未 apply。
libgr2 的 `gr2::Mat4` 声明同一约定(见 `docs/steps/M0-gr2-reader.md` 「约定」)。
basis fix / 单位缩放是 M1 渲染时才做的事,不进 dump。
+53
View File
@@ -0,0 +1,53 @@
# Granny 版本判定(oracle T1)
## 已知(M0 T2)
- 资产由 **Granny Standard Exporter, SDK version 2.4.0.7** 导出(从解压后的 gr2 内嵌串读到)。
- 9166 个 gr2 里 **9155 个文件格式 v6、11 个 v7**(magic:v6=`GRNFileMV_Old`,v7=`GRNFileMV_32Bit_LittleEndian`)。
- libgr2 自研,`bind_pose_self_check` 全量 2013/2017 多骨 skeleton < 1e-3(`warrior_cheongrin` 6.1e-5)。
## 候选 runtime
| DLL | 来源 | 状态 |
|---|---|---|
| `granny2_x64.dll` 2.9.12 | 泄露 SDK `lib/win64/` | **先用这个**(SDK 自带、能直接链)。容器 + 组合公式在 2.4–2.11 稳定。 |
| granny2.dll 2.4.x | 需另找(对应 2.4.0.7 exporter 的 runtime) | 若 2.9.12 在曲线 / B 样条边界上和 libgr2 分歧且 Blender 仲裁站 libgr2 这边,换这个。 |
| granny2.dll 2.11.x | m2dev-client-src 头文件写的版本 | 兜底候选。 |
## 判定步骤(跑 `oracle probe`)
对每个候选 DLL,跑:
```
oracle probe "assets/PC/ymir work/pc/warrior/warrior_cheongrin.gr2"
oracle probe "assets/PC/ymir work/pc/warrior/action/dance_1.gr2"
oracle probe "assets/Zone/ymir work/zone/.../<某静态件>.gr2"
```
AC(选定 DLL 下):
- `warrior_cheongrin.gr2`:`Skeletons=1`、`Meshes=5`、`Animations=0`、`Models=1`、
`Materials=4`、`Textures=2`、skeleton 0 = 75 骨、骨骼名以 `Bip01` 开头
—— 与 libgr2(`gr2dump`)**逐字段一致**。
- `dance_1.gr2`:`Animations=1`、`Duration≈28.333334`、`TrackGroups=1`。
- 静态件:`Skeletons=0` 或 1、`Meshes≥1`。
- 3+ 文件全过;其余候选 DLL 的失败表现记进本表。
## 选定结果 —— ✅ 已跑(2026-08-29)
- **选定:`granny2_x64.dll` 2.9.12.0**(泄露 SDK `lib/win64/`)。
- 环境:macOS 26.4 / Apple Silicon / wine-stable 11.0(Rosetta)/ MinGW-w64 交叉编译(`build-wine.sh`)。
- **`oracle probe` vs `gr2dump` 逐字段一致**:
| 文件 | Textures | Materials | Skeletons | Meshes | Models | Animations | skel0 骨骼数 | 骨骼名/父索引 |
|---|---|---|---|---|---|---|---|---|
| `warrior_cheongrin.gr2` | 2=2 | 4=4 | 1=1 | 5=5 | 1=1 | 0=0 | 75=75 | 逐项一致(Bip01…)|
| `assassin.gr2` | 3=3 | 6=6 | 1=1 | 17=17 | 1=1 | 0=0 | 95=95 | 逐项一致(含双 root:`assasin_low` + `Bip01`)|
| `dance_1.gr2` | — | — | 0 | 0 | 0 | 1=1 | — | Duration 28.333334,TrackGroups 1 |
- `FromFileName` 连非法编码的 Korean 路径都逐字节一致(libgr2 读原始字节 = Granny)。
**结论**:2.4 导出的资产在 2.9.12 runtime 下 `GrannyGetFileInfo` 完全正常,libgr2 与之逐字段对齐。
容器 + 组合公式在 2.4–2.9 稳定这一假设成立,**不需要换 2.4.x runtime**。
`oracle.exe` + `granny2_x64.dll` 不进仓库(体积 + 版权);`build-wine.sh` 一条命令重建。
+43
View File
@@ -0,0 +1,43 @@
# tools/granny-oracle/ —— Windows 真值源工具
**独立 Windows/MSVC 工程,不在主 CMake 树内。** 施工文档:[`../docs/steps/00-oracle.md`](../docs/steps/00-oracle.md)
## 两条轨
| 轨 | 产物 | 状态 |
|---|---|---|
| **Oracle-Lite**(只用 `granny2.dll` C API) | `granny_probe.exe` · `oracle dump` CLI · 最小 D3D9 渲染器 | 必做,关键路径 |
| **Oracle-Full**(+ 抽出的 `EterGrnLib`) | `oracle_etergrn` + 层② dump | 延后,demo 不需要 |
## 目录
```
tools/granny-oracle/
oracle.c ★ Oracle-Lite 全部:probe(T1)+ dump(T3 层① / T5 蒙皮顶点)
CMakeLists.txt Windows/MSVC 工程,链泄露 SDK 的 granny2_x64.lib
RUNBOOK.md 怎么 build+run(原生 Windows / Wine / stretch: POSIX 移植)
GRANNY-VERSION.md 版本判定 + 候选对比(待跑 probe 填实)
FORMAT.md dump 二进制布局 + 坐标约定
vendor/ 锁定的 granny2 DLL(T1 选定后 git-track)
render/ 独立最小 D3D9 → PNG(T6,不依赖 EterGrnLib;待写)
selfcheck.* T7 自检(Windows CI runner;逻辑见 RUNBOOK.md「selfcheck」)
full/ Oracle-Full:oracle_etergrn + 层② dump(T2/T4,延后)
```
## 状态
- [x] `oracle.c` —— probe + dump(层① 世界矩阵 + mesh N 蒙皮顶点)
- [x] **macOS + Wine 跑通**(`build-wine.sh` = MinGW-w64 交叉编译 + wine-stable)。Granny **2.9.12.0**。
- [x] **T1** DLL 选定 + probe vs `gr2dump` 逐字段一致(`GRANNY-VERSION.md`)
- [x] **T3/T5** 层① dump + 蒙皮顶点 dump 跑通;**M2 门禁·主对拍 PASS**:
- `warrior_cheongrin` bind + `dance_1` @ t={0, 4.7, 11.333, 20}:骨骼矩阵 `max|Δ| ≤ 4.1e-5`、蒙皮顶点 `max‖Δ‖ ≤ 6.5e-5`
- `assassin`(双 root,非单位 InitialPlacement)bind:`3.1e-5 / 4.6e-5`
- 对拍工具 `tools/oracle_diff`(libgr2 侧读 `.bin` 逐字段比)
- [ ] T8 noise_floor.json(跑同 dump 100 次 + 换相邻 DLL 一次,取 `max|Δ|`)
- [ ] T6 D3D9 确定态截图 harness(M1 视觉对拍用)
- [ ] Oracle-Full(层②,`EterGrnLib`)—— 延后
## 输出
- dump → `../test/oracle_dumps/`(格式见 `FORMAT.md`)
- 噪声地板 → `../test/noise_floor.json`(T8,M2 用它设 ε)
+88
View File
@@ -0,0 +1,88 @@
# oracle 运行手册(Oracle-Lite)
`oracle.c` 只用 `granny2` 的公开 C API(`granny.h`):文件 IO + 数学 + 写二进制,**不碰 GPU / D3D / EterGrnLib**。
所以除了原生 Windows,Wine 也能跑。
## 依赖
- `granny.h` + `granny2_x64.lib` + `granny2_x64.dll` —— 泄露 SDK 里就有:
`MobileSource/Cross Platform/Granny-3D-SDK-main/{include,lib/win64}/`
(32 位版在 `lib/win32/`。)
- 这套 DLL 是 **SDK 2.9.12**。资产由 **2.4.0.7** 导出(M0 T2 从解压后的 gr2 里读到)。
容器格式 + 组合公式(`BuildCompositeTransform4x4` / `ColumnMatrixMultiply4x3` / B 样条系数)
在 2.4–2.11 稳定,libgr2 的 self-check 6.1e-5 已印证。差异风险在 B 样条边界 / 少数曲线细节。
→ 先跑 `selfcheck`(下)确认坐标约定和 t=0 == bind pose,再决定是否换 2.4.x runtime(见 `GRANNY-VERSION.md`)。
## A. 原生 Windows(MSVC)
```bat
cmake -S oracle -B oracle\build -DGRANNY_SDK="C:\path\to\Granny-3D-SDK-main"
cmake --build oracle\build --config Release
oracle\build\Release\oracle.exe probe "assets\PC\ymir work\pc\warrior\warrior_cheongrin.gr2"
```
## B. macOS + Wine —— ✅ 已验证(macOS 26.4 / Apple Silicon)
```sh
brew install mingw-w64
# wine-stable cask 需要 sudo 装 gstreamer 依赖;无 sudo 时直接解包用:
tar -xf "$(ls ~/Library/Caches/Homebrew/downloads/*wine-stable*.tar.xz)" -C /tmp/wine
xattr -rc "/tmp/wine/Wine Stable.app"
WINE="/tmp/wine/Wine Stable.app/Contents/Resources/wine/bin/wine"
# 编译(脚本封装了 MinGW 的坑:granny.h 的 __int64 关键字)
tools/granny-oracle/build-wine.sh # → tools/granny-oracle/oracle.exe + tools/granny-oracle/granny2_x64.dll
# ⚠ WINEPREFIX 不能在 /tmp(Wine 拒绝在非属主目录建 prefix)
export WINEPREFIX="$HOME/.wine-oracle" WINEDEBUG=-all
"$WINE" wineboot --init # 首次,Rosetta 下 3–8 分钟
cd oracle
GR2='Z:\Users\me\...\assets\PC\ymir work\pc\warrior\warrior_cheongrin.gr2'
"$WINE" oracle.exe probe "$GR2"
"$WINE" oracle.exe dump "$GR2" - 0.0 \
-o 'Z:\Users\me\...\xrender-poc\test\oracle_dumps\cheongrin_bind_t0.bin' --mesh 4
"$WINE" oracle.exe dump "$GR2" 'Z:\...\action\dance_1.gr2' 11.333 \
-o 'Z:\...\test\oracle_dumps\cheongrin_dance1_t11.333.bin' --mesh 4
```
坑:① MinGW GCC 不认 `granny.h` 里的 `__int64` → `build-wine.sh` 用 `-D__int64="long long"`;
② Wine prefix 不能放 `/tmp`(非属主);③ wine-stable 首启会刷一大堆 MoltenVK 日志到 stderr,无害,`WINEDEBUG=-all` 挡不住它,grep 掉即可。
## C.(stretch)从 SDK source/ 移植 POSIX libgranny
`Granny-3D-SDK-main/source/` 有全套源码(62k 行)+ CMakeLists,但重度 Windows 化
(`winxx/win64/*.cpp`、`winxx_granny_windows.cpp`、`WINDOWS_IGNORE_PACKING_MISMATCH`)。
`wii/`、`wiiu/`、`ps3/` 目录里有 `ansi_*` 的通用实现可参考。真要做是几小时的平台层移植,
不在关键路径 —— 先走 A / B。
## 对拍(`tools/oracle_diff`)
```sh
build/tools/oracle_diff <model.gr2> <anim.gr2|-> <dump.bin> [--t <sec>] [--mesh N]
```
libgr2 侧用 `gr2::sample_pose` / `bind_pose` + 手写 LBS 算一遍,报:
- **gate 1**:骨骼世界矩阵 vs oracle 层① `max|Δ|`
- **gate 2**:mesh N 蒙皮顶点 vs oracle `max‖Δ‖`
- verdict:阈值 1e-3(noise_floor 就绪前)
**已跑结果**(Granny 2.9.12.0 vs libgr2):
| 用例 | mat max\|Δ\| | vtx max‖Δ‖ |
|---|---|---|
| `warrior_cheongrin` bind | 1.5e-5 | 4.6e-5 |
| `warrior_cheongrin` + `dance_1` @ t=0/4.7/11.333/20 | ≤ 4.1e-5 | ≤ 6.5e-5 |
| `assassin` bind(双 root + 非单位 InitialPlacement)| 3.1e-5 | 4.6e-5 |
全部 ≈ float32 累积误差 → **libgr2 的类型树 / 世界姿势 / OldCurve 求值 / CPU LBS 与真 Granny 数值一致**。
> 关键:`oracle.c` 的 `GrannyBuildWorldPose` 要把 model 的 `InitialPlacement` 当 `Offset4x4` 传
> (`GrannyGetModelInitialPlacement4x4`),否则非单位 InitialPlacement 的模型(如 assassin)会差一个平移。
> libgr2 的 `skeleton.initial_placement` 做同一件事。踩过一次。
## 产出
- dump → `test/oracle_dumps/`(布局见 `FORMAT.md`)
- 噪声地板 → `test/noise_floor.json`(`oracle dump` 跑同 `(model,anim,t)` 100 次 + 换一版
相邻 DLL 一次,取矩阵 / 顶点的 `max|Δ|`;M2 的 ε = 这个 ×10)
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# 在 macOS 上用 MinGW-w64 交叉编译 oracle.exe(链泄露 SDK 的 granny2_x64.lib),
# 用 Wine 跑。见 RUNBOOK.md。实测 macOS 26.4 / Apple Silicon / wine-stable 11.0 / Granny 2.9.12.
set -euo pipefail
cd "$(dirname "$0")"
SDK="${GRANNY_SDK:-../../../MobileSource/Cross Platform/Granny-3D-SDK-main}"
[ -f "$SDK/include/granny.h" ] || { echo "找不到 $SDK/include/granny.h(传 GRANNY_SDK=<SDK 根>)"; exit 1; }
CC="${MINGW_CC:-x86_64-w64-mingw32-gcc}"
command -v "$CC" >/dev/null || { echo "没装 MinGW:brew install mingw-w64"; exit 1; }
# 关键:granny.h 在 _WIN64 分支里 `#define GRANNY_64BIT_TYPE __int64`,
# MinGW GCC 默认不认 __int64 关键字 → 用 -D__int64="long long" 顶掉。
"$CC" -O2 -std=gnu11 -fms-extensions -D'__int64=long long' \
-I "$SDK/include" \
oracle.c "$SDK/lib/win64/granny2_x64.lib" \
-o oracle.exe
cp "$SDK/lib/win64/granny2_x64.dll" ./
echo "built: $(pwd)/oracle.exe (+ granny2_x64.dll)"
echo
echo "跑(Wine):"
echo " export WINEPREFIX=\$HOME/.wine-oracle # ⚠ 别放 /tmp:Wine 拒绝在非属主目录建 prefix"
echo " wine wineboot --init # 首次,Rosetta 下较慢"
echo " wine oracle.exe probe 'Z:\\path\\to\\warrior_cheongrin.gr2'"
echo " wine oracle.exe dump 'Z:\\...\\model.gr2' 'Z:\\...\\anim.gr2' <t> -o 'Z:\\...\\out.bin' --mesh N"
echo
echo "对拍:build/tools/oracle_diff <model.gr2> <anim.gr2|-> <out.bin> --mesh N"
+30
View File
@@ -0,0 +1,30 @@
{
"granny": "2.9.12.0",
"threshold": {"mat": 1e-3, "vtx": 1e-3},
"cases": [
{"model":"warrior_cheongrin.gr2","anim":"-","t":0,"mesh":4,"mat":1.5e-05,"vtx":4.6e-05,"status":"PASS"},
{"model":"warrior_cheongrin_lod_01.gr2","anim":"-","t":0,"mesh":4,"mat":1.5e-05,"vtx":4.6e-05,"status":"PASS"},
{"model":"warrior_cheongrin_lod_02.gr2","anim":"-","t":0,"mesh":4,"mat":1.5e-05,"vtx":4.6e-05,"status":"PASS"},
{"model":"warrior_cheongrin_lod_03.gr2","anim":"-","t":0,"mesh":4,"mat":1.5e-05,"vtx":4.6e-05,"status":"PASS"},
{"model":"warrior_lord.gr2","anim":"-","t":0,"mesh":1,"mat":1.5e-05,"vtx":1.5e-05,"status":"PASS"},
{"model":"warrior_lord_lod_01.gr2","anim":"-","t":0,"mesh":1,"mat":1.5e-05,"vtx":1.5e-05,"status":"PASS"},
{"model":"warrior_lord_lod_02.gr2","anim":"-","t":0,"mesh":1,"mat":1.5e-05,"vtx":1.5e-05,"status":"PASS"},
{"model":"warrior_lord_lod_03.gr2","anim":"-","t":0,"mesh":1,"mat":1.5e-05,"vtx":1.5e-05,"status":"PASS"},
{"model":"assassin.gr2","anim":"-","t":0,"mesh":16,"mat":3.1e-05,"vtx":4.6e-05,"status":"PASS"},
{"model":"shaman_lord.gr2","anim":"-","t":0,"mesh":0,"mat":7.6e-06,"vtx":3.8e-05,"status":"PASS"},
{"model":"redthief2_soldier2_lod_01.gr2","anim":"-","t":0,"mesh":4,"mat":0,"vtx":0,"status":"PASS"},
{"model":"snakeman_bow_lod_01.gr2","anim":"-","t":0,"mesh":0,"mat":3.1e-05,"vtx":6.1e-05,"status":"PASS"},
{"model":"snakeman_bow_lod_01.gr2","anim":"-","t":0,"mesh":1,"mat":3.1e-05,"vtx":3.2e-05,"status":"PASS"},
{"model":"warrior_rabbit1_backup.gr2","anim":"-","t":0,"mesh":0,"mat":1.5e-05,"vtx":3.9e-05,"status":"PASS"},
{"model":"ox_01.gr2","anim":"-","t":0,"mesh":0,"mat":0,"vtx":0,"status":"PASS"},
{"model":"warrior_cheongrin.gr2","anim":"dance_1.gr2","t":0,"mesh":4,"mat":2.3e-05,"vtx":3.9e-05,"status":"PASS"},
{"model":"warrior_cheongrin.gr2","anim":"dance_1.gr2","t":4.7,"mesh":4,"mat":3.1e-05,"vtx":5e-05,"status":"PASS"},
{"model":"warrior_cheongrin.gr2","anim":"dance_1.gr2","t":11.333,"mesh":4,"mat":4.1e-05,"vtx":5.8e-05,"status":"PASS"},
{"model":"warrior_cheongrin.gr2","anim":"dance_1.gr2","t":20,"mesh":4,"mat":3.1e-05,"vtx":6.5e-05,"status":"PASS"},
{"model":"warrior_cheongrin.gr2","anim":"attack.gr2","t":0.3,"mesh":4,"mat":4.5e-05,"vtx":5.9e-05,"status":"PASS"},
{"model":"warrior_cheongrin.gr2","anim":"run.gr2","t":0.5,"mesh":4,"mat":2.3e-05,"vtx":5e-05,"status":"PASS"},
{"model":"warrior_lord.gr2","anim":"dance_1.gr2","t":10,"mesh":1,"mat":4.6e-05,"vtx":6.4e-05,"status":"PASS"},
{"model":"warrior_lord.gr2","anim":"wait.gr2","t":1.0,"mesh":1,"mat":4.6e-05,"vtx":4.8e-05,"status":"PASS"}
],
"summary": {"total": 23, "passed": 23, "worst_mat": 4.6e-05, "worst_vtx": 6.5e-05}
}
+215
View File
@@ -0,0 +1,215 @@
/* oracle —— Windows 真值源(Oracle-Lite)。见 ../docs/steps/00-oracle.md
*
* oracle probe <file.gr2>
* → 打 GrannyGetFileInfo 的 count / FromFileName / 每 skeleton 骨骼数 + 前几个骨骼名
*
* oracle dump <model.gr2> <anim.gr2|-> <t> -o out.bin [--mesh N]
* → 层① 世界矩阵(GrannyGetWorldPoseComposite4x4Array 直出)+ mesh N 蒙皮顶点
* → 二进制布局见 ../FORMAT.md;libgr2 侧对拍读取器必须与之一致
*
* 只用 granny2 的公开 C API(granny.h)。链 granny2_x64.lib(见 CMakeLists.txt / RUNBOOK.md)。
* 不碰 D3D、不碰 EterGrnLib —— 那是 Oracle-Full(延后)。
*/
#include <granny.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#define ORC_MAGIC "XORC"
#define ORC_VERSION 1u
#define KIND_LAYER1 1u
#define KIND_SKINV 3u
static void die(const char *msg) { fprintf(stderr, "oracle: %s\n", msg); exit(2); }
static const char *granny_version_string(void) {
/* 运行期版本(链接到的 granny2.dll 实际报的)。编译期宏是 GrannyProduct*Version。*/
const char *rt = GrannyGetVersionString();
return rt ? rt : "unknown";
}
/* ── probe ─────────────────────────────────────────────────────────── */
static int cmd_probe(const char *path) {
granny_file *f = GrannyReadEntireFile(path);
if (!f) { fprintf(stderr, "probe: cannot read %s\n", path); return 1; }
granny_file_info *fi = GrannyGetFileInfo(f);
if (!fi) { fprintf(stderr, "probe: GrannyGetFileInfo NULL\n"); return 1; }
printf("file : %s\n", path);
printf("granny runtime : %s\n", granny_version_string());
printf("FromFileName : %s\n", fi->FromFileName ? fi->FromFileName : "(null)");
printf("Textures=%d Materials=%d Skeletons=%d VertexDatas=%d TriTopologies=%d "
"Meshes=%d Models=%d TrackGroups=%d Animations=%d\n",
fi->TextureCount, fi->MaterialCount, fi->SkeletonCount, fi->VertexDataCount,
fi->TriTopologyCount, fi->MeshCount, fi->ModelCount, fi->TrackGroupCount,
fi->AnimationCount);
for (int s = 0; s < fi->SkeletonCount; ++s) {
granny_skeleton *sk = fi->Skeletons[s];
printf(" skeleton %d: %d bones : ", s, sk->BoneCount);
for (int b = 0; b < sk->BoneCount && b < 6; ++b)
printf("[%d]%s(p=%d) ", b, sk->Bones[b].Name, sk->Bones[b].ParentIndex);
printf("%s\n", sk->BoneCount > 6 ? "..." : "");
}
for (int a = 0; a < fi->AnimationCount; ++a)
printf(" animation %d: '%s' Duration=%.6f TimeStep=%.6f TrackGroups=%d\n",
a, fi->Animations[a]->Name, fi->Animations[a]->Duration,
fi->Animations[a]->TimeStep, fi->Animations[a]->TrackGroupCount);
GrannyFreeFile(f);
return 0;
}
/* ── dump helpers ──────────────────────────────────────────────────── */
static void wr_u32(FILE *o, uint32_t v) { fwrite(&v, 4, 1, o); }
static void wr_f32(FILE *o, float v) { fwrite(&v, 4, 1, o); }
static void wr_str(FILE *o, const char *s) {
uint32_t n = (uint32_t)(s ? strlen(s) : 0);
wr_u32(o, n);
if (n) fwrite(s, 1, n, o);
}
/* 与 ../FORMAT.md「文件头」一致 */
static void write_header(FILE *o, uint32_t kind, uint32_t coord,
const char *model_path, const char *anim_path, float t) {
fwrite(ORC_MAGIC, 1, 4, o);
wr_u32(o, ORC_VERSION);
wr_u32(o, kind);
wr_u32(o, coord);
char gv[16]; memset(gv, 0, sizeof gv);
{ const char *v = granny_version_string(); size_t n = strlen(v);
memcpy(gv, v, n < 15 ? n : 15); }
fwrite(gv, 1, 16, o);
wr_str(o, model_path);
wr_str(o, anim_path ? anim_path : "");
wr_f32(o, t);
}
/* ── dump ──────────────────────────────────────────────────────────── */
static int cmd_dump(int argc, char **argv) {
if (argc < 4) die("dump <model.gr2> <anim.gr2|-> <t> -o out.bin [--mesh N]");
const char *model_path = argv[0];
const char *anim_path = strcmp(argv[1], "-") == 0 ? NULL : argv[1];
float t = (float)atof(argv[2]);
const char *out_path = NULL;
int mesh_index = 0;
for (int i = 3; i < argc; ++i) {
if (!strcmp(argv[i], "-o") && i + 1 < argc) out_path = argv[++i];
else if (!strcmp(argv[i], "--mesh") && i + 1 < argc) mesh_index = atoi(argv[++i]);
}
if (!out_path) die("dump: 缺 -o out.bin");
granny_file *mf = GrannyReadEntireFile(model_path);
if (!mf) die("dump: model 读失败");
granny_file_info *mfi = GrannyGetFileInfo(mf);
if (!mfi || mfi->ModelCount < 1) die("dump: model 无 Models[0]");
granny_model_instance *mi = GrannyInstantiateModel(mfi->Models[0]);
if (!mi) die("dump: GrannyInstantiateModel 失败");
granny_skeleton *skel = GrannyGetSourceSkeleton(mi);
int bone_count = skel->BoneCount;
granny_file *af = NULL;
if (anim_path) {
af = GrannyReadEntireFile(anim_path);
if (!af) die("dump: anim 读失败");
granny_file_info *afi = GrannyGetFileInfo(af);
if (!afi || afi->AnimationCount < 1) die("dump: anim 无 Animations[0]");
granny_control *ctrl = GrannyPlayControlledAnimation(0.0f, afi->Animations[0], mi);
if (!ctrl) die("dump: GrannyPlayControlledAnimation 失败");
}
GrannySetModelClock(mi, t);
/* Offset4x4 = model 的 InitialPlacement(客户端渲染时也这么给)。
* libgr2 侧 gr2::sample_pose 用 skeleton.initial_placement 做同一件事。 */
granny_real32 placement[16];
GrannyGetModelInitialPlacement4x4(mfi->Models[0], placement);
granny_local_pose *lp = GrannyNewLocalPose(bone_count);
granny_world_pose *wp = GrannyNewWorldPose(bone_count);
/* 先填 rest(无 track 的骨骼用它);有动画再让 SampleModelAnimations 覆盖动的那些。 */
GrannyBuildRestLocalPose(skel, 0, bone_count, lp);
if (af) GrannySampleModelAnimations(mi, 0, bone_count, lp);
GrannyBuildWorldPose(skel, 0, bone_count, lp, placement, wp);
/* 层① = 每骨「骨骼空间 → 世界」的原始世界矩阵(GrannyGetWorldPose4x4Array)。
* 注意:GrannyGetWorldPoseComposite4x4Array 返回的是 InverseWorld4x4·world = 蒙皮矩阵,
* 不是这里要的。 */
granny_matrix_4x4 *world = GrannyGetWorldPose4x4Array(wp);
/* coord:raw(手系按 DLL 输出、单位=原始、根变换未 apply)。见 FORMAT.md。 */
const uint32_t coord = 0u;
FILE *o = fopen(out_path, "wb");
if (!o) die("dump: 打不开输出文件");
write_header(o, KIND_LAYER1, coord, model_path, anim_path, t);
wr_u32(o, (uint32_t)bone_count);
for (int b = 0; b < bone_count; ++b)
fwrite(&world[b][0][0], 4, 16, o);
/* kind=3:mesh_index 的蒙皮顶点位置。照 SDK tutorial(wii_basic.cpp CreateMesh/Render):
* deformer(输入=mesh 自带顶点类型, 输出=PNT332)
* 矩阵缓冲 = GrannyGetWorldPoseComposite4x4Array(= InverseWorld·world = 蒙皮矩阵)
* 索引 = GrannyGetMeshBindingToBoneIndices(mesh 骨骼槽 → model 骨骼) */
if (mfi->MeshCount > mesh_index) {
granny_mesh *mesh = mfi->Meshes[mesh_index];
int vcount = GrannyGetMeshVertexCount(mesh);
typedef struct { float p[3], n[3], uv[2]; } pnt332;
pnt332 *dst = (pnt332 *)malloc(sizeof(pnt332) * vcount);
granny_matrix_4x4 *composite = GrannyGetWorldPoseComposite4x4Array(wp);
granny_mesh_binding *mb = GrannyNewMeshBinding(mesh, skel, skel);
granny_int32x const *toBone = GrannyGetMeshBindingToBoneIndices(mb);
granny_mesh_deformer *def = GrannyNewMeshDeformer(
GrannyGetMeshVertexType(mesh), GrannyPNT332VertexType,
GrannyDeformPositionNormal, GrannyDontAllowUncopiedTail);
if (def) {
GrannyDeformVertices(def, toBone, (const granny_real32 *)composite,
vcount, GrannyGetMeshVertices(mesh), dst);
} else {
fprintf(stderr, "dump: no deformer for mesh %d vertex type\n", mesh_index);
}
wr_u32(o, (uint32_t)vcount);
for (int v = 0; v < vcount; ++v)
fwrite(dst[v].p, 4, 3, o);
if (def) GrannyFreeMeshDeformer(def);
GrannyFreeMeshBinding(mb);
free(dst);
} else {
wr_u32(o, 0);
}
fclose(o);
printf("dump: wrote %s bones=%d t=%.6f granny=%s\n",
out_path, bone_count, t, granny_version_string());
GrannyFreeWorldPose(wp);
GrannyFreeLocalPose(lp);
GrannyFreeModelInstance(mi);
if (af) GrannyFreeFile(af);
GrannyFreeFile(mf);
return 0;
}
int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr,
"usage:\n"
" oracle probe <file.gr2>\n"
" oracle dump <model.gr2> <anim.gr2|-> <t> -o out.bin [--mesh N]\n");
return 2;
}
if (!strcmp(argv[1], "probe")) {
if (argc < 3) die("probe <file.gr2>");
return cmd_probe(argv[2]);
}
if (!strcmp(argv[1], "dump")) return cmd_dump(argc - 2, argv + 2);
die("未知子命令(probe / dump)");
return 2;
}
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env bash
# M2 门禁·主:对测试资产集跑 oracle dump + oracle_diff,汇总 tools/granny-oracle/m2-numeric.json。
# 前置:tools/granny-oracle/build-wine.sh 已产出 oracle.exe;WINEPREFIX 已 init(见 RUNBOOK.md);
# oracle_diff 已构建(cmake -B build/tools -DMT_BUILD_TOOLS=ON)。
set -uo pipefail
cd "$(dirname "$0")/../.."
REPO="$(pwd)"
ASSETS="$(cd "${MT_ASSETS:-$REPO/assets}" && pwd)"
DUMPS="$REPO/build/oracle_dumps"
DIFF=""
for c in "$REPO/build/tools/oracle_diff"; do
[ -x "$c" ] && DIFF="$c" && break
done
[ -n "$DIFF" ] || { echo "oracle_diff not built — run: cmake -B build/tools -DMT_BUILD_TOOLS=ON && cmake --build build/tools"; exit 2; }
mkdir -p "$DUMPS"
WINE="${WINE:-/tmp/wine/Wine Stable.app/Contents/Resources/wine/bin/wine}"
export WINEPREFIX="${WINEPREFIX:-$HOME/.wine-oracle}" WINEDEBUG="${WINEDEBUG:--all}"
# Z: 盘映射到 /
winpath() { printf 'Z:%s' "$(printf '%s' "$1" | sed 's#/#\\#g')"; }
# 用例:<model 相对 assets> | <anim 相对 assets,或 -> | <t> | <mesh idx>
CASES=$(cat <<'EOF'
PC/ymir work/pc/warrior/warrior_cheongrin.gr2|-|0|4
PC/ymir work/pc/warrior/warrior_cheongrin_lod_01.gr2|-|0|4
PC/ymir work/pc/warrior/warrior_cheongrin_lod_02.gr2|-|0|4
PC/ymir work/pc/warrior/warrior_cheongrin_lod_03.gr2|-|0|4
PC/ymir work/pc/warrior/warrior_lord.gr2|-|0|1
PC/ymir work/pc/warrior/warrior_lord_lod_01.gr2|-|0|1
PC/ymir work/pc/warrior/warrior_lord_lod_02.gr2|-|0|1
PC/ymir work/pc/warrior/warrior_lord_lod_03.gr2|-|0|1
PC/ymir work/pc/assassin/assassin.gr2|-|0|16
PC/ymir work/pc/shaman/shaman_lord.gr2|-|0|0
metin2_patch_dragon_rock_mobs/ymir work/monster2/redthief2_soldier2/redthief2_soldier2_lod_01.gr2|-|0|4
monster2/ymir work/monster2/snakeman_bow/snakeman_bow_lod_01.gr2|-|0|0
monster2/ymir work/monster2/snakeman_bow/snakeman_bow_lod_01.gr2|-|0|1
metin2_patch_easter1/ymir work/pc/warrior/warrior_rabbit1_backup.gr2|-|0|0
Zone/ymir work/zone/oxevent/ox_01.gr2|-|0|0
PC/ymir work/pc/warrior/warrior_cheongrin.gr2|PC/ymir work/pc/warrior/action/dance_1.gr2|0|4
PC/ymir work/pc/warrior/warrior_cheongrin.gr2|PC/ymir work/pc/warrior/action/dance_1.gr2|4.7|4
PC/ymir work/pc/warrior/warrior_cheongrin.gr2|PC/ymir work/pc/warrior/action/dance_1.gr2|11.333|4
PC/ymir work/pc/warrior/warrior_cheongrin.gr2|PC/ymir work/pc/warrior/action/dance_1.gr2|20|4
PC/ymir work/pc/warrior/warrior_cheongrin.gr2|PC/ymir work/pc/warrior/general/attack.gr2|0.3|4
PC/ymir work/pc/warrior/warrior_cheongrin.gr2|PC/ymir work/pc/warrior/general/run.gr2|0.5|4
PC/ymir work/pc/warrior/warrior_lord.gr2|PC/ymir work/pc/warrior/action/dance_1.gr2|10|1
PC/ymir work/pc/warrior/warrior_lord.gr2|PC/ymir work/pc/warrior/general/wait.gr2|1.0|1
EOF
)
key_for() { echo "$1|$2|$3|$4" | md5 -q | cut -c1-12; }
# ── 1. dump(并行,4 个一批)──────────────────────────────────────────
run_dump() {
local model="$1" anim="$2" t="$3" mesh="$4" out="$5"
local wm; wm="$(winpath "$ASSETS/$model")"
local wa="-"; [ "$anim" != "-" ] && wa="$(winpath "$ASSETS/$anim")"
local wo; wo="$(winpath "$out")"
"$WINE" "$REPO/tools/granny-oracle/oracle.exe" dump "$wm" "$wa" "$t" -o "$wo" --mesh "$mesh" >/dev/null 2>&1
}
echo "== oracle dump =="
i=0
while IFS='|' read -r model anim t mesh; do
[ -z "$model" ] && continue
k=$(key_for "$model" "$anim" "$t" "$mesh")
out="$DUMPS/suite_$k.bin"
run_dump "$model" "$anim" "$t" "$mesh" "$out" &
i=$((i+1)); [ $((i % 4)) -eq 0 ] && wait
done <<< "$CASES"
wait
# ── 2. diff + 汇总 ─────────────────────────────────────────────────────
echo "== oracle_diff =="
JSON="$REPO/tools/granny-oracle/m2-numeric.json"
{
echo '{'
echo ' "granny": "2.9.12.0",'
echo ' "threshold": {"mat": 1e-3, "vtx": 1e-3},'
echo ' "cases": ['
} > "$JSON"
total=0; passed=0; first=1
worst_mat=0; worst_vtx=0
while IFS='|' read -r model anim t mesh; do
[ -z "$model" ] && continue
k=$(key_for "$model" "$anim" "$t" "$mesh")
bin="$DUMPS/suite_$k.bin"
total=$((total+1))
if [ ! -s "$bin" ]; then
line=" {\"model\":\"$(basename "$model")\",\"anim\":\"$(basename "$anim")\",\"t\":$t,\"mesh\":$mesh,\"status\":\"NO_DUMP\"}"
else
amodel="$ASSETS/$model"; aanim="-"; [ "$anim" != "-" ] && aanim="$ASSETS/$anim"
v=$("$DIFF" "$amodel" "$aanim" "$bin" --mesh "$mesh" 2>/dev/null | tail -1)
m=$(echo "$v" | sed -n 's/.*mat \([0-9.e+-]*\) <.*/\1/p')
vt=$(echo "$v" | sed -n 's/.*vtx \([0-9.e+-]*\) <.*/\1/p')
st="FAIL"; echo "$v" | grep -q PASS && { st="PASS"; passed=$((passed+1)); }
awk -v m="$m" -v w="$worst_mat" 'BEGIN{exit !(m+0>w+0)}' && worst_mat=$m
awk -v m="$vt" -v w="$worst_vtx" 'BEGIN{exit !(m+0>w+0)}' && worst_vtx=$vt
printf ' [%-4s] %-40s %-14s mat=%-10s vtx=%s\n' "$st" "$(basename "$model")" "$(basename "$anim") @${t}" "$m" "$vt"
line=" {\"model\":\"$(basename "$model")\",\"anim\":\"$(basename "$anim")\",\"t\":$t,\"mesh\":$mesh,\"mat\":$m,\"vtx\":$vt,\"status\":\"$st\"}"
fi
[ $first -eq 1 ] && first=0 || echo ',' >> "$JSON"
printf '%s' "$line" >> "$JSON"
done <<< "$CASES"
{
echo ''
echo ' ],'
echo " \"summary\": {\"total\": $total, \"passed\": $passed, \"worst_mat\": $worst_mat, \"worst_vtx\": $worst_vtx}"
echo '}'
} >> "$JSON"
echo
echo "== $passed/$total PASS worst mat=$worst_mat vtx=$worst_vtx → $JSON =="
[ "$passed" -eq "$total" ]
+1 -1
View File
@@ -1,4 +1,4 @@
// oracle_diff —— libgr2 vs oracle 逐字段对拍(M2 门禁·主)。见 oracle/FORMAT.md、docs/steps/00-oracle.md
// oracle_diff —— libgr2 vs oracle 逐字段对拍(M2 门禁·主)。见 tools/granny-oracle/FORMAT.md、docs/steps/00-oracle.md
//
// oracle_diff <model.gr2> [<anim.gr2>|-] <oracle_dump.bin> [--t <sec>] [--mesh N]
//
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Summarises a native client --perf-log CSV (native_render/perf_log.h).
"""Summarises a native client --perf-log CSV (src/host/perf_log.h).
usage: perf_summary.py perf-YYYYMMDD-HHMMSS.csv [--all]
@@ -1,5 +1,5 @@
#!/bin/sh
# Copies the Android client's --perf-log files (native_render/perf_log.h) to ./perf-logs and prints a
# Copies the Android client's --perf-log files (src/host/perf_log.h) to ./perf-logs and prints a
# summary of the newest one. The phone must be connected with USB debugging; the files can also be
# taken off the phone without a PC from Android/data/org.metin2port.client/files/perf.
set -e
+2 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Generate one platform's pyconfig.h for the vendored CPython 2.7.18 (docs/PORT-PLAN.md 批次 2P step 1).
# pyconfig.h is the result of probing a platform's type sizes and system calls, so every platform keeps
# its own copy under extension/third_party/cpython-2.7.18/config/<platform>/. The source list and the
# its own copy under third_party/cpython-2.7.18/config/<platform>/. The source list and the
# built-in module table (config/config.c) are shared and only regenerated from the macos run.
#
# tools/py_embed/gen_pyconfig.sh macos|android|ios|linux
@@ -10,7 +10,7 @@
set -euo pipefail
cd "$(dirname "$0")/../.."
REPO="$PWD"
SRC="$REPO/extension/third_party/cpython-2.7.18"
SRC="$REPO/third_party/cpython-2.7.18"
PLAT="${1:?usage: gen_pyconfig.sh macos|android|ios|linux}"
# config.sub in 2.7.18 predates arm64; the build triple only names the machine running configure.
BUILD="$(uname -m | sed 's/^arm64$/aarch64/')-$(uname -s | tr 'A-Z' 'a-z')"
@@ -30,7 +30,7 @@ export MT40250_DUMP_ROOT=/usr/metin2_mysql_dump
```sh
pkg install gmake clang-devel mysql80-server mysql80-client
sh tools/40250/build_server.sh
sh tools/server/build_server.sh
```
源码 Makefile 是 40250 原版,使用 `clang++-devel`、`-m32`、静态 MySQL client、
@@ -44,7 +44,7 @@ src/server all`,失败时应保留完整编译输出修复 jail 的 32-bit/库
```sh
export MT40250_MYSQL_ROOT_PASSWORD='你的 root 密码' # 不设置则每次交互式 -p
export MT40250_DB_PASSWORD=password # 必须与 CONFIG 一致,默认 password
sh tools/40250/setup_mysql.sh
sh tools/server/setup_mysql.sh
```
脚本创建 `account/common/log/player`,并同时建立 `metin2@localhost` 与
@@ -54,9 +54,9 @@ sh tools/40250/setup_mysql.sh
## 启停 40250 服务
```sh
sh tools/40250/prepare_runtime.sh
sh tools/40250/start_server.sh
sh tools/40250/stop_server.sh
sh tools/server/prepare_runtime.sh
sh tools/server/start_server.sh
sh tools/server/stop_server.sh
```
`start_server.sh` 使用仓库自带 CONFIG:auth `11000`、channel1/first `13000`,
@@ -4,7 +4,7 @@
# REQUIRE: LOGIN mysql
# KEYWORD: shutdown
#
# Account registration service for the 40250 server (tools/40250/register_server.py).
# Account registration service for the 40250 server (tools/server/register_server.py).
# Install: /usr/local/etc/rc.d/mt_register, then `sysrc mt_register_enable=YES`.
. /etc/rc.subr