Files
mtgodot-poc/docs/CLIENT-PERFORMANCE-INVESTIGATION.md
T
2026-09-27 19:44:41 -07:00

32 KiB
Raw Blame History

客户端运行流畅度与性能瓶颈调查报告

调查日期:2026-09-24
调查目标:定位 macOS 客户端在运行、移动与战斗过程中出现不流畅、微卡顿(micro-stutter)与跳帧感的根本原因,提出针对性优化方案。
状态:已完成调查,尚未引入代码改动。


一、 核心结论概述

经链路排查,客户端运行不流畅的根本原因不在于 40250 原版游戏逻辑本身的性能,而在于当前 Godot 与 40250 C++ 桥接管线中存在的 6 大性能与同步瓶颈:

  1. 显存颠簸(致命瓶颈):每一帧都在销毁并重新创建数十到上百个 GPU 网格缓冲(ArrayMesh.new()),macOS Metal 驱动层发生每秒数千次的显存分配与释放(Memory Allocator Thrashing)。
  2. 跨语言数据巨量重复封包与无意义消耗:每帧将数万顶点、法线、矩阵打包为 Godot Dictionary,且在 UI 表面仅仅为了判断 has_3d 就把全量顶点数据封包一次并立即丢弃。
  3. Mac 120Hz ProMotion 与 40250 60 FPS 逻辑时钟失步:未设置帧率上限导致以 120Hz 刷新,每帧时间在 8ms 与 16ms 之间跳跃,不仅引发明显的视觉顿挫感,还导致所有 CPU/GPU 颠簸开销翻倍。
  4. GDScript 解释执行高频 CPU 计算:脚本层三重循环执行 4x4 矩阵乘法、逐 UI 图元无条件执行复杂多边形布尔裁剪。
  5. 双线程每帧 Ping-Pong 等待:Godot 主线程与 Python Script 线程通过互斥锁与条件变量每帧往返切换,受 macOS 大小核调度抖动影响。
  6. Retina 屏上的重型渲染器配置:默认启用了 Forward+ 与 MSAA 2x,在高 DPI 屏幕上带来了不必要的渲染计算与显存带宽压力。

二、 关键瓶颈深度剖析

1. 致命瓶颈:每帧反复创建并销毁 ArrayMesh(显存颠簸)

  • 代码位置:project/python_3d_surface.gd:121, 342-356
  • 实现逻辑:
    func update_frame() -> void:
        ...
        instance.mesh = _build_mesh(draw) # 每帧对每个 3D 绘制对象调用
    
    func _build_mesh(draw: Dictionary) -> ArrayMesh:
        var arrays := []
        arrays.resize(Mesh.ARRAY_MAX)
        arrays[Mesh.ARRAY_VERTEX] = draw["positions"]
        ...
        var mesh := ArrayMesh.new()
        mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
        mesh.surface_set_material(0, _material(draw))
        return mesh
    
  • 机制危害:
    • 在 Godot 架构中,ArrayMesh.new() 配合 add_surface_from_arrays 会在底层图形 API(macOS Metal / Vulkan)中新申请显存分配顶点缓冲区(VBO)和索引缓冲区(IBO)。
    • 场景中包含角色各部位(身体、武器、头发)、怪物、NPC、特效等数十至上百个 draw call。下一帧到来时,上一帧的所有 ArrayMesh 被丢弃进 GC 释放显存。
    • 以 60 FPS 计,每秒发生 3,000 ~ 6,000 次 GPU 显存分配和释放;若在 120Hz 屏幕上更达 6,000 ~ 12,000 次/秒。
    • 这会引发 macOS Metal 驱动内存分配器严重颠簸(Memory Allocator Thrashing)和驱动锁争用,是掉帧与微卡顿的最大源头。

2. C++ 到 GDScript 巨量跨语言数据封包与重复调用

  • 代码位置:
    1. project/python_ui_surface.gd:192-194
    2. project/python_3d_surface.gd:89, 136
    3. extension/src/python_host_node.cpp:169-220
  • 机制危害:
    • Metin2PythonHost::render3d_draws() 每次调用都会把所有 3D draw call 的 16 维矩阵、几万个顶点、法线、UV、颜色、索引逐个转换成 PackedVector3Array、PackedFloat32Array 并封装成带有 20~30 个键值对的庞大 Godot Dictionary 数组。
    • 严重重复调用: 在 python_ui_surface.gd:192-194 中:
      var draws_3d: Array = Metin2PythonHost.render3d_draws()
      var has_3d: bool = not draws_3d.is_empty()
      
      这里仅仅为了检查 3D 绘制队列是否为空,就把整个场景的几何数据完整跨语言序列化了一遍,然后立即全部丢弃进垃圾回收! 紧接着在 python_3d_surface.gd:89 又完整调用了一遍 render3d_draws() 真正用于绘制。
    • 此外,Metin2PythonHost.ui_render_commands() 每一帧也在 python_ui_surface.gd:195 与 python_3d_surface.gd:136(_update_background())中被调用了两次。

3. 屏幕刷新率(ProMotion 120Hz)与 40250 逻辑时钟(60 FPS)失步

  • 代码位置:
    • project/project.godot(未配置 max_fps)
    • extension/src/port/EterBase/Timer.cpp:109-116
  • 机制危害:
    • MacBook 屏幕通常为 120Hz ProMotion。当前 project.godot 没有设置帧率上限,Godot 默认以 120 FPS 运行 _process。
    • 这导致上述所有 C++ 内存封包、显存频繁重建、线程唤醒以每秒 120 次的频率翻倍运行。
    • 更关键的是,40250 原版内部逻辑是以 60 FPS(约 16.6ms)的时钟基准设计的。在 120Hz 下,传递给 40250 的单帧 delta 时间约为 8.3ms,导致游戏内部插值在 8ms 与 16ms 之间来回跳动,产生肉眼可见的“画面抽动 / 跳帧”感。

4. GDScript 解释器高频数学与几何计算

  • 代码位置:
    1. project/python_3d_surface.gd:77-86:
      static func multiply(a: PackedFloat32Array, b: PackedFloat32Array) -> PackedFloat32Array:
          var out := PackedFloat32Array()
          out.resize(16)
          for r in 4:
              for c in 4:
                  var sum := 0.0
                  for k in 4:
                      sum += a[r * 4 + k] * b[k * 4 + c]
                  out[r * 4 + c] = sum
          return out
      
      每一帧在 GDScript 解释器层面用三重循环对每一个 3D 模型进行 4x4 矩阵乘法,并频繁分配 16 个元素的数组。
    2. project/python_ui_surface.gd:341-355: _clip_quad 对界面上每个图元都无条件调用 Geometry2D.intersect_polygons(...) 进行多边形相交求值,即使 95% 以上的 UI 元素完全位于屏幕范围内未发生任何裁剪。

5. 双线程每帧条件变量往返切换(Ping-Pong Jitter)

  • 代码位置:extension/src/platform/ScriptLib/PythonBoot.cpp:597, 612-616
  • 机制危害: Godot 主线程与 Python Script 线程通过 g_fiber.cv.notify_all() 与 cv.wait() 互斥锁交替执行。主线程渲染完必须挂起等待 Script 线程完成 Process(),若 macOS 系统调度器将其中一个线程调度到了能效核(E-Core)或与其他系统后台任务竞争,会造成帧间隔不均匀(Frame Time Jitter)。

6. 渲染器配置过重(Forward+ & MSAA 2x)

  • 代码位置:project/project.godot:40-41
    renderer/rendering_method="forward_plus"
    anti_aliasing/quality/msaa_3d=2
    
  • 机制危害: Metin2 模型是典型的 2004 年代 Direct3D 8 单方向光、无复杂 PBR 的固定管线模型。在 Mac Retina 高分辨率(例如 3K/4K 物理分辨率)下,Godot 的 Forward+ 会执行完整的光照聚类裁剪(Clustered Shading Compute Shaders),且 MSAA 2x 在高分辨率下会产生巨大的 Resolve 显存带宽负担。

三、 优化实施方案与优先级建议

阶段 优化措施 涉及文件 预期成效
P0 锁定 60 FPS 帧率
在 project.godot 配置 application/run/max_fps=60
project/project.godot 彻底消除 120Hz/60FPS 帧率抖动与跳帧感,立竿见影减少一半 CPU/GPU 压力与发热
P0 消除跨语言重复调用
1. C++ 暴露轻量 Metin2PythonHost.has_3d_draws(),UI 表面不再调用 render3d_draws() 判空。
2. 缓存或单次提取 ui_render_commands(),避免同帧重复调用。
extension/src/python_host_node.cpp
project/python_ui_surface.gd
project/python_3d_surface.gd
消除每帧数万个无用 Variant 的内存分配与 GC 压力
P1 网格缓存与显存复用
对动态 mesh 进行池化或直接利用动态更新,避免每帧 ArrayMesh.new() 导致 GPU VBO/IBO 频繁申请与释放。
project/python_3d_surface.gd 彻底根治 Metal 显存分配器颠簸,使画面运行保持平滑稳定
P1 UI 裁剪短路优化
若图元 quad 的外接矩形完全在屏幕与 clip 矩形之内,直接跳过 Geometry2D.intersect_polygons 计算。
project/python_ui_surface.gd 显著减轻 GDScript 解释器的 CPU 计算负担
P2 渲染器与抗锯齿轻量化
评估切换为 mobile 渲染模式,在高 PPI Retina 屏上关闭 MSAA 2x 或改用开销更低的抗锯齿方案。
project/project.godot 大幅度降低 GPU 填充率与显存带宽消耗

四、 已实施优化与复测对比(2026-09-25)

优化轮次:ui_draw 专项优化

  1. C++ 端预分配与 StringName 优化:在 extension/src/python_host_node.cpp 中预分配 Command Array 空间,使用静态 StringName 替换所有字典字符串键,避免每帧数百次重复字符串分配与动态哈希运算。
  2. 纹理元数据与 Atlas 缓存:在 project/python_ui_surface.gd 建立 _tex_info_cache,缓存纹理 RID、Atlas UV 缩放/偏移与全图默认 UV 数组,将 Atlas 区域与大小计算彻底移出每帧绘制主循环。
  3. 图元包围盒快速裁剪:基于预计算的 x1, y1, x2, y2 与裁剪视口 clip_x1, clip_y1, clip_x2, clip_y2 执行 4 次浮点比较:
    • 完全在视口外的图元直接跳过;
    • 完全在视口内的图元(占比 >95%)走无裁切快路径,直接组装多边形顶点,完全跳过多边形求交计算与中间数组构造。
  4. 颜色数组与画布清理池化:缓存高频单色数组,避免 PackedColorArray([color]) 的动态堆分配;仅清除当帧实际使用过的 CanvasItem 节点。

复测性能对比(稳态场景 240 帧采样)

性能指标 优化前 优化后 改善幅度
ui_draw 耗时 (P50) 9.267 ms 1.798 ms -80.6%(减少 7.47 ms)
单帧平均耗时 (Mean) 21.126 ms (47.3 FPS) 16.666 ms (60.0 FPS) -21.1%(稳定跑满 60 帧)
单帧中位数耗时 (P50) 20.964 ms 16.667 ms -20.5%
95分位耗时 (P95) 22.391 ms 16.920 ms -24.4%(抖动几乎消除)
测试门禁状态 PASS PASS (12/12 全绿) 画面与逻辑 100% 保持一致

多怪战斗复测与字形批处理(2026-09-25)

用户已在 40250 原版客户端实测相同怪物量运行流畅。因此,多怪场景的卡顿应优先排查移植层增加的开销,不能仅归因于原版逐字绘制或怪物数量。

MT_FAKE_MOB_COUNT=24 场景中,原始 UI 命令约 1000 条,其中约 900 条是字体图集 mem:1@51 的字形。Godot 端逐条将这些字形转换为画布多边形,是移植后额外的每帧开销。现在 C++ 在不改变命令顺序的前提下,把同纹理、无遮挡裁剪、无蒙版的相邻字形合并为一次三角形数组提交;其他 UI 图元沿用原路径。3D 背景提取也复用同帧批处理命令。

24 怪场景指标 批处理前 批处理后
空闲帧 P50 27.424 ms 17.790 ms
空闲帧 P95 28.788 ms 19.221 ms
空闲帧 UI 绘制 P50 7.253 ms 1.086 ms
战斗帧 P50 22.429 ms 16.632 ms
战斗帧 UI 绘制 P50 6.470 ms 1.063 ms

批处理后的一帧诊断中,约 1000 条原始 UI 命令缩减为 345 条提交命令,其中 7 个批次容纳 666 个字形。单怪场景的空闲与战斗帧 P50 均约 16.66 ms。24 怪场景的登录、进图、贴地、贴图、光照、截图及击杀检查通过。空闲帧 P50 仍约 17.8 ms,ui_update P50 约 11.6 ms,是后续分析重点。以上数字来自本地自动化场景,不能当作原版与移植版在同一机器上的直接性能对比。

原始和复测数据分别在 build/rendering/python-game-20260925-064436-59271/report.json 与 build/rendering/python-game-20260925-072043-60686/report.json。

64 怪压力测试(2026-09-25)

MT_FAKE_MOB_COUNT=64 的本地测试通过登录、进图、画面及击杀检查;截图检查时视野内记录到 38 个角色。无额外原生计时器时,空闲 240 帧的帧间隔 P50 为 27.028 ms、P95 为 28.785 ms(约 37 FPS);战斗采样 P50 为 16.732 ms、P95 为 21.449 ms。战斗段的可见内容和角色数量会变化,因此不能凭其 P50 宣称 64 怪稳定 60 FPS。数据见 build/rendering/python-game-20260925-072444-60992/report.json。

空闲帧里,Godot 记录的 ui_update P50 为 17.146 ms、3D 表面更新 P50 为 6.292 ms、UI 画布绘制 P50 为 1.737 ms。ui_update 这个字段包含脚本线程执行完整的原生 CPythonApplication::Process(),并不等于纯界面更新。临时细分计时表明原生窗口树 OnUIUpdate() 只有约 0.3 ms,原生渲染录制约 16 ms;其中 RenderGame() 约 11 ms,角色 Deform() 约 5 ms,角色 Render() 约 5 ms,背景约 0.8 ms。这些是连续 120 帧的平均值,计时探针本身会增加少量开销;探针已移除,日志保存在 build/rendering/python-game-20260925-072854-61643/godot.log。

这一场景的原始 UI 图像命令为 1747 条,其中字体图集字形 1619 条;批处理后的命令总量 538 条,13 个字形批次覆盖 1229 个字形。后续优先分析角色变形和原生绘制录制的逐怪线性开销,再分析 Godot 3D 几何提交;继续优化 Godot UI 画布预计收益较小。任何角色动画降频、剔除或 LOD 都要先与 40250 原版可见行为对照。

渲染转换第一轮(2026-09-25)

先试过对动画 ArrayMesh 原地更新;单怪测试中 3D 更新 P50 升至约 12 ms。临时细分计时发现每帧顶点打包和拓扑哈希使跨语言命令提取耗时约 14 ms,而且网格原地更新没有命中。该试验已撤回,没有保留在运行路径。

随后改为在 Godot 单精度构建中,把原生连续 float 顶点、法线和 UV 批量复制到对应 Packed 数组;非单精度构建保留逐元素转换。64 怪场景在相同临时计时下,命令提取 P50 从 2.583 ms 降至 1.571 ms,3D 表面更新 P50 从 6.857 ms 降至 5.280 ms,空闲帧 P50 从 28.109 ms 降至 25.603 ms;战斗 P50 仍约 16.7 ms。数据见 build/rendering/python-game-20260925-075500-62940/report.json 和 build/rendering/python-game-20260925-075707-63151/report.json。单怪测试空闲帧 P50 为 16.667 ms,24 怪两次复测 P50 为 18.639/18.547 ms;三档测试的画面和击杀检查均通过。24 怪整体帧时间比更早的 17.790 ms 测量略高,而本次测得的 3D 表面更新时间更低,差异主要出现在原生帧阶段,仍需重复采样判断。

移除临时细分计时器后,最终 64 怪复测的空闲帧 P50/P95 为 25.591/27.372 ms,3D 表面更新 P50 为 5.240 ms,战斗 P50/P95 为 16.718/21.065 ms,画面和击杀检查通过;见 build/rendering/python-game-20260925-080029-63326/report.json。这仍未达到 64 怪稳定 60 FPS。下一步若改用 C++ RenderingServer 或 GPU 蒙皮,应分别验证稳定网格身份与拓扑复用、骨骼权重和绑定姿态的导出,以及逐帧顶点/GPU 同步成本;不能把底层 API 本身当作零拷贝或固定收益保证。

批量转换与几何缓存完整验证(2026-09-25 最新复测)

对当前未提交代码(含 C++ 连续内存批量 memcpy 复制、静态网格 geometry_key 缓存复用、UI 批处理、以及输入与拾取射线同步)进行了完整的 1 怪、24 怪、64 怪多密度验证:

测试场景 空闲帧 P50 空闲帧 P95 3D 表面更新 P50 UI 绘制 P50 战斗帧 P50 战斗 3D 更新 P50 门禁与击杀检查
1 怪 (单体基准) 16.679 ms (60 FPS) 16.825 ms 1.928 ms 0.653 ms 16.670 ms 1.664 ms PASS (已击杀)
24 怪 (中等密度) 17.611 ms 18.078 ms 3.346 ms 1.061 ms 16.655 ms 2.499 ms PASS (已击杀)
64 怪 (极限压力) 25.619 ms (约 39 FPS) 26.493 ms 5.252 ms 1.664 ms 16.710 ms 2.926 ms PASS (已击杀)

结论与评估:

  1. 优化切实有效且无低密度回退:
    • 1 怪场景的 3D 表面更新由最初的 2.3 ms 降至 1.93 ms,空闲与战斗均稳定在 60 FPS(16.67 ms),证明没有像第一版那样出现小场景负优化(之前第一版原地更新曾劣化到 11.9 ms)。
    • 24 怪场景空闲帧 P50 取得 17.611 ms、P95 取得 18.078 ms,创下历史最佳稳定性;3D 表面更新仅 3.35 ms,战斗全程维持 60 FPS(16.66 ms)。
    • 64 怪极端场景下,3D 表面更新由优化前基准的 6.857 ms 降至 5.252 ms(减少 1.605 ms / -23.4%);空闲帧由 28.109 ms 降至 25.619 ms(减少 2.49 ms / -8.9%)。
  2. 正确性完全保证:
    • 1 怪、24 怪、64 怪所有用例的地面贴地、材质贴图、光照、屏幕对照度分析、目标锁定与鼠标左键普攻击杀(HP 递减为 [100, 66, 33, 0])均 100% 通过。
    • 报告数据分别保存在:
      • 1 怪:build/rendering/python-game-20260925-090440-78310/report.json
      • 24 怪:build/rendering/python-game-20260925-090521-78357/report.json
      • 64 怪:build/rendering/python-game-20260925-090617-78378/report.json

独立 Vulkan/MoltenVK 原型(2026-09-25)

新增 native_render/:SDL3 创建独立窗口,Vulkan 创建交换链、顶点缓冲、管线与逐 draw 提交;macOS 通过 MoltenVK 映射到 Metal。它直接读取录制设备的 Render3DDraw 结构,也能回放 Godot 客户端显式捕获的一帧。构建和回放命令见 native_render/README.md。现有 Godot 客户端运行路径未切换。

第一阶段:基础提交与每帧 CPU 展开基线

Apple M4 上,合成的 64 draw × 333 三角形(63,936 个展开顶点)运行 60 帧,平均帧间隔 16.64 ms,CPU 几何准备 6.80 ms。真实 64 怪测试捕获的一帧(64-mob.mtdr,v1)为 51 draw、51,231 个索引;回放 60 帧,平均帧间隔 16.81 ms,CPU 几何准备 5.96 ms。

第二阶段:按 geometry_key 持久化 GPU 顶点/索引缓冲 + 着色器矩阵变换

  1. 持久化 GPU 缓冲与按修订号增量上传:以 GeometryId{geometry_key, signature} 作为 GPU 缓冲缓存键(而非 draw 数组下标,避免视锥裁剪导致 draw 顺序变化时整表失效),仅在新几何出现或 geometry_revision 变化时上传顶点与索引缓冲。
  2. 着色器 world * view * proj 变换:将每帧 CPU 顶点变换移入 native.vert Push Constants,索引缓冲直接走 vkCmdDrawIndexed(消除每帧 CPU 三角形展开)。

第三阶段:深度缓冲、DDS 贴图、法线/UV 与 D3D8 固定管线状态(Version 3 捕获)

  1. 深度与固定管线状态:新增 VK_FORMAT_D32_SFLOAT 深度附件,按 (cull_mode, z_enable/z_write, alpha_blend/dest_blend) 缓存 VkPipeline,在 native.vert / native.frag(128 字节 Push Constants,满足桌面与 Android Vulkan 最小保证)中实现 D3D8 方向光(light0 + 环境光 + 自发光)、texture_factor 调制与 alpha_test 裁剪。
  2. 自包含 DDS 贴图回放:捕获格式升级为 Version 3(向下兼容 v1/v2),在 MT_NATIVE_CAPTURE_PATH 触发时自动从 40250 pack 提取被引用的 .dds 原始字节写入 .mtdr;mt_native_render 复用 extension/src/dxt.cpp 在首帧软解 DXT1/3/5 并上传至 VK_IMAGE_TILING_OPTIMAL 纹理与描述符集。
  3. 修复无 MT_PROFILE_FRAME 时 64 怪测试 dog_screen=(-1,-1) 失败的根因:
    • 根因一:CPythonNetworkStream::GamePhase() 每帧最多消费 3 个网络包,64 怪场景的进图突发包(约 147 个包,含末尾 GC_PING)需要约 49 帧排空;此前未开启 MT_PROFILE_FRAME 时仅等待 30 帧就点击地面移动,导致客户端在回复 CG_PONG(254)前先发出了 CG_CHARACTER_MOVE(7),伪造服务端报 expected CG header 254, got 7 并主动断开连接清空角色。
    • 根因二:settled 闭包未检查 now.x >= 0.0,连续两帧 (-1, -1) 距离为 0.0 < 0.5 会提前退出等待。
    • 修复后,MT_FAKE_MOB_COUNT=64 在不开启 MT_PROFILE_FRAME 的捕获运行中也 100% 通过所有贴图、光照、投影与普攻击杀检查(build/native-render/capture-64mob-v3/report.json���。

Apple M4 原生 Vulkan 60 帧回放实测对比

测试用例(60 帧,FIFO 垂直同步) Draw 数 顶点数 索引数 60 帧总几何上传 贴图上传 全帧平均 prepare_ms 稳态 steady_prepare_ms(第 2~60 帧) submit_ms mean_frame_ms
合成 64 draw(阶段一:每帧 CPU 展开) 64 63,936 — 3,840 次 0 6.80 ms 6.80 ms 8.21 ms 16.64 ms
真实 64 怪 v1(阶段一:每帧 CPU 展开) 51 51,231 — 3,060 次 0 5.96 ms 5.96 ms 9.33 ms 16.81 ms
合成 64 draw(阶段二/三:持久 GPU 缓冲) 64 63,936 63,936 64 次 (3.32 MB) 0 0.17 ms 0.13 ms 13.88 ms 16.60 ms
真实 64 怪 v2(51 draw,持久 GPU 缓冲) 51 216,384 51,231 51 次 (10.59 MB) 0 0.21 ms 0.11 ms 14.13 ms 16.35 ms
真实 64 怪 v3(90 draw,含 17 张 DDS 贴图 + 光照 + 深度) 90 244,520 157,647 90 次 (12.37 MB) 17 次 (18.37 MB) 2.48 ms(含首帧冷启动解纹理) 0.23 ms 13.57 ms 18.75 ms
真实 64 怪 v3 + 首 draw 每帧动画修订(--animate-first-draw) 90 244,520 157,647 149 次 (90 + 59) 17 次 (18.37 MB) 2.66 ms(含首帧冷启动解纹理) 0.43 ms 13.45 ms 18.82 ms

第四阶段:硬件 GPU 时间戳、40250 地形 HTP、2D UI/字形/小地图遮罩、GPU 骨骼蒙皮与实时游戏主循环接入(Version 4 捕获)

  1. 分离真实 GPU 执行耗时(gpu_ms)与垂直同步等待(submit_ms):
    • 在 native_render/main.cpp 中接入 VkQueryPool(VK_QUERY_TYPE_TIMESTAMP),在 vkCmdBeginRenderPass(TOP_OF_PIPE)与 vkCmdEndRenderPass(BOTTOM_OF_PIPE)记录硬件时间戳并换算为毫秒级 gpu_ms。
    • 新增 --no-vsync(优先选用 VK_PRESENT_MODE_IMMEDIATE_KHR / MAILBOX_KHR),将交换链垂直同步等待从 submit_ms 中剥离。
  2. 40250 原生硬件变换地形块(HTP)渲染:
    • 在 extension/src/platform/GameLib/MapOutdoorRenderHTP.cpp 中实现 CMapOutdoor::__RenderTerrain_RenderHardwareTransformPatch() 与 __HardwareTransformPatch_RenderPatchSplat(...)(由 SetNativeTerrainRenderEnabled(true) 或 MT_NATIVE_TERRAIN=1 启用,不影响 Godot 默认测试路径)。
    • 在 extension/src/platform/EterLib/RecordingDevice.cpp 中支持 D3D8 D3DTSS_TCI_CAMERASPACEPOSITION 相机空间纹理矩阵生成地形基础层与 Alpha Splat 层 UV,并将 m_textures[0] 混入 geometry_key 实现跨帧持久化缓存。
  3. 2D UIRenderCommand 全量渲染(含字形图集与小地图圆角遮罩):
    • 支持 behind_3d 与前景两趟正交 2D 渲染,涵盖 Bar、GradientBar、Line、Image、裁剪矩形(clip_x1..y2)、mem:<id>@<revision> 动态字形图集("MTRA" 原始 RGBA 编码)、.tga(含未压缩与 RLE)解码,以及 CPythonMiniMap 的双纹理(tex0 + mask_tex)圆角遮罩混合。
    • 相邻同状态、同纹理的 UI 图元与字形自动合并为单次 vkCmdDrawIndexed 批次提交(64 怪场景 1,597 个 UI/字形四边形成批为 31 个 Vulkan 批次)。
  4. GPU 线性混合骨骼蒙皮(GPU Skeletal Skinning):
    • 在 extension/src/platform/EterGrnLib/GrannyRuntime.cpp 中,当启用 GPU 蒙皮(--gpu-skinning 或 MT_GPU_SKINNING=1)时,GrannyDeformVertices 不再逐帧在 CPU 上对每个顶点做矩阵加权变形,而是按不可变的源网格指针(SourceVertices)提取一次静态绑定姿态(Bind-Pose)顶点、法线、4 骨骼索引(bone_indices)与 4 骨骼权重(bone_weights),仅将当前实例的骨骼矩阵调色板(bone_count * 16 个 float)写入切片表。
    • 在 RecordingDevice.cpp 中,所有共享同一 .gr2 源网格的怪物/角色实例共享同一个 geometry_key 且 geometry_revision 恒为 1;native.vert 通过 set = 1, binding = 0 的 BonePalette SSBO 在顶点着色器中完成 4 骨骼线性混合蒙皮。
  5. 实时游戏主循环直接接入(--live-client):
    • 当 mt_native_render 在主构建目录 build/ 下构建时,直接链接 port_platform、mtpython 与 FakeLoginServer,通过 --live-client <ClientDir> 在原生 SDL3 + Vulkan 窗口内直接启动 40250 system.py,完成登录、选人、进图、刷出 1~64 只怪物、转发 SDL3 键鼠事件,并可导出包含 3D 骨骼权重、地形、2D UI 与字形页的 Version 4 .mtdr 捕获文件。

Apple M4 第四阶段实测数据(实时客户端 --live-client 与 v4 回放,60 帧)

运行模式(Apple M4,60 帧) 呈现模式 3D Draw (含地形) GPU 蒙皮 Draw UI 批次 / 四边形 顶点数 / 索引数 60 帧动态几何重传 game_update_ms (40250 逻辑) 稳态 steady_prepare_ms submit_ms 硬件 gpu_ms mean_frame_ms
实时 1 怪 (--live-client --gpu-skinning --no-vsync) IMMEDIATE 74 10 31 / 220 225,355 / 91,119 30 次 (9.8 KB) 5.55 ms 0.62 ms 2.22 ms 0.36 ms 8.48 ms (~118 FPS)
实时 64 怪・CPU 蒙皮 (--live-client --no-gpu-skinning --no-vsync) IMMEDIATE 116 0 31 / 1,597 253,999 / 207,543 3,178 次 (205.37 MB) 19.93 ms 3.42 ms 0.16 ms 0.83 ms 23.62 ms (~42 FPS)
实时 64 怪・GPU 蒙皮 (--live-client --gpu-skinning --no-vsync) IMMEDIATE 117 52 31 / 1,597 254,003 / 207,549 28 次 (9.18 KB) 10.51 ms 0.78 ms 0.10 ms 0.79 ms 11.46 ms (~87 FPS)
实时 64 怪・GPU 蒙皮 (--live-client --gpu-skinning,默认 FIFO) FIFO 116 52 31 / 1,597 253,999 / 207,543 19 次 (6.23 KB) 10.42 ms 0.79 ms 0.10 ms 0.88 ms 11.37 ms
v4 捕获回放 + 骨骼动画 (--capture 64-mob-v4.mtdr --animate-bones --no-vsync) IMMEDIATE 116 52 31 / 1,597 253,999 / 207,543 首帧 74 次,第 2~60 帧 0 次 — 1.11 ms 5.19 ms 1.08 ms 12.25 ms

关键验证结论:

  1. 真实 GPU 耗时(gpu_ms)极低:通过 Vulkan 硬件时间戳确认,Apple M4 执行完整 64 怪场景(117 个 3D Draw 含地形多级 Splat、52 个 GPU 骨骼蒙皮网格、31 个 UI 批次共 1,597 个 UI/字形四边形、约 25.4 万顶点)的单帧真实 GPU 耗时仅为 0.79 ~ 0.88 ms,此前 submit_ms 的 ~13.5 ms 完全来自 FIFO 垂直同步等待。
  2. GPU 骨骼蒙皮消除 99.995% 动态顶点带宽并减半 CPU 帧耗时:在 64 怪实时客户端中,开启 --gpu-skinning 后:
    • 60 帧动态几何上传量从 CPU 蒙皮的 3,178 次 / 205.37 MB 降至 28 次 / 9.18 KB(所有同模型怪物实例共享同一份静态绑定姿态 GPU 缓冲);
    • 40250 游戏帧更新 game_update_ms 从 19.93 ms 降至 10.51 ms(每帧节省 9.42 ms);
    • 渲染准备 steady_prepare_ms 从 3.42 ms 降至 0.78 ms(每帧节省 2.64 ms);
    • 端到端单帧总耗时 mean_frame_ms 从 23.62 ms(~42 FPS) 降至 11.46 ms(~87 FPS),在 Debug 构建下即已稳定跑进 60 FPS(16.67 ms)预算以内。

第五阶段:完整可玩原生客户端补齐(水面/天空盒/云层/SpeedTree/球面环境高光、音频、全键鼠/IME/软件光标、交互启动与 Release -O3 构建)

按路线图顺序完成全部 5 项剩余原生特性(通过 IsNativeTerrainRenderEnabled() 门控新 3D 要素,保持 Godot 默认路径 32/32 回归检查 100% 通过):

  1. 交互启动与窗口/贴图扩展:
    • 新增 --interactive(无限帧循环直至关闭窗口)、--login-screen(停留在 introLogin.LoginWindow 供手动输入账号密码)、--live-server HOST:AUTH_PORT:GAME_PORT(直连外部 40250 服务端)与 --width W --height H。
    • 支持 SDL_WINDOW_RESIZABLE 与 VK_ERROR_OUT_OF_DATE_KHR / VK_SUBOPTIMAL_KHR 交换链自动重建,并同步调用 PythonBoot::SetUISize。
    • 接入 macOS ImageIO 解码器,支持从 40250 pack 直接解码 .jpg / .png / .bmp 登录背景与 UI 贴图;并在未显式设置 VK_ICD_FILENAMES 时自动探测 Homebrew MoltenVK_icd.json。
  2. 补齐剩余 3D 场景视觉要素:
    • 水面(Water):移植 extension/src/platform/GameLib/MapOutdoorWater.cpp(CMapOutdoor::RenderWater 与 DrawWater),支持 30 帧循环水面贴图、相机空间水面变换矩阵与基于高度差的顶点 Alpha 混合。
    • 天空盒与动态云层(Skybox & Clouds):移植 extension/src/platform/EterLib/SkyBox.cpp(CSkyObjectQuad、CSkyObject、CSkyBox::Render、CSkyBox::RenderCloud),并在 RecordingDevice.cpp 中支持 Stage 0 二维纹理矩阵变换(D3DTTFF_COUNT2)与 D3DTOP_MODULATEINVALPHA_ADDCOLOR(19)云层着色模式。
    • SpeedTree 森林与树木(SpeedTreeLib):实现 SpeedTreeForest.cpp、SpeedTreeForestDirectX8.cpp 与 SpeedTreeWrapper.cpp,读取 40250 .spt 属性与 "TreeSize" 参数生成带树皮贴图与树叶交叉十字网格的 Z-up 顶点/索引缓冲并参与渲染。
    • 多级纹理高光(Specular Sphere-Map)与完整混合因子:在 PythonApplication.cpp 中实现 CGrannyMaterial::CreateSphereMap 与 TranslateSpecularMatrix,在 native.vert / native.frag 中实现球面环境映射 UV 生成与 D3DTOP_MODULATEALPHA_ADDCOLOR(18)高光叠加,并补齐完整 D3D8 src_blend / dest_blend 因子映射。
    • 修复退出纯虚函数崩溃:移除基类析构函数 CSkyObject::~CSkyObject() 对纯虚函数 Destroy() 的调用,以及 CSpeedTreeForest::~CSpeedTreeForest() 对纯虚函数 Clear() 的调用(移入派生类 CSpeedTreeForestDirectX8::~CSpeedTreeForestDirectX8()),解决客户端退出时 libc++abi: Pure virtual function called! 崩溃。
  3. 音频播放接入(SDL3 Audio + AudioToolbox):
    • 在 native_render/main.cpp 中实现 NativeAudioEngine,每帧排空 DrainAudioCommands(),通过 macOS AudioToolbox(AudioFileOpenWithCallbacks + ExtAudioFileRead)直接从内存解包 .wav / .mp3 为 44.1 kHz 双声道浮点 PCM,送入 SDL_AudioStream 完成 2D/3D 音效混音与 BGM 淡入淡出循环播放。
  4. 完整键盘/IME 文本输入与原生软件光标:
    • 补齐 F1–F12、Tab、Backspace、Return、Delete、Shift/Ctrl/Alt、方向键、数字小键盘及符号键的 SDL_Scancode -> DIK_* 与 VK_*(PythonBoot::UIIMEKeyDown)映射,接入 SDL_EVENT_TEXT_INPUT 转发 UTF-8 字符至 PythonBoot::UIChar。
    • 启用 40250 CURSOR_MODE_SOFTWARE(OnMouseUpdate / OnMouseRender),当游戏软件光标激活时自动隐藏系统光标并渲染 40250 原生彩色游戏光标。
  5. Release (-O3) 构建与实测对比(Apple M4,180 帧,--no-vsync):
场景与构建配置(Apple M4,180 帧) 3D Draw GPU 蒙皮 Draw UI 批次 / 四边形 顶点数 / 索引数 game_update_ms (逻辑+UI+音频) 稳态 steady_prepare_ms 硬件 gpu_ms mean_frame_ms
完整场景 1 怪 · Debug 构建 97 10 32 / 221 225,447 / 91,257 5.82 ms 0.62 ms 0.44 ms 8.27 ms (~121 FPS)
完整场景 1 怪 · Release (-O3) 构建 96 10 32 / 221 225,443 / 91,251 1.36 ms (4.3x 加速) 0.79 ms 0.67 ms 8.33 ms (~120 FPS)
完整场景 64 怪 · Debug 构建 140 52 32 / 1,598 254,095 / 207,687 9.89 ms 0.87 ms 0.77 ms 13.45 ms (~74 FPS)
完整场景 64 怪 · Release (-O3) 构建 140 52 32 / 1,598 254,095 / 207,687 1.62 ms (6.1x 加速) 0.77 ms 1.00 ms 8.24 ms (~121 FPS)
完整场景 64 怪 .mtdr 回放 · Release (-O3) 140 52 31 / 1,597 254,095 / 207,687 — 0.19 ms 1.02 ms 8.62 ms