32 KiB
客户端运行流畅度与性能瓶颈调查报告
调查日期:2026-09-24
调查目标:定位 macOS 客户端在运行、移动与战斗过程中出现不流畅、微卡顿(micro-stutter)与跳帧感的根本原因,提出针对性优化方案。
状态:已完成调查,尚未引入代码改动。
一、 核心结论概述
经链路排查,客户端运行不流畅的根本原因不在于 40250 原版游戏逻辑本身的性能,而在于当前 Godot 与 40250 C++ 桥接管线中存在的 6 大性能与同步瓶颈:
- 显存颠簸(致命瓶颈):每一帧都在销毁并重新创建数十到上百个 GPU 网格缓冲(
ArrayMesh.new()),macOS Metal 驱动层发生每秒数千次的显存分配与释放(Memory Allocator Thrashing)。 - 跨语言数据巨量重复封包与无意义消耗:每帧将数万顶点、法线、矩阵打包为 Godot Dictionary,且在 UI 表面仅仅为了判断
has_3d就把全量顶点数据封包一次并立即丢弃。 - Mac 120Hz ProMotion 与 40250 60 FPS 逻辑时钟失步:未设置帧率上限导致以 120Hz 刷新,每帧时间在 8ms 与 16ms 之间跳跃,不仅引发明显的视觉顿挫感,还导致所有 CPU/GPU 颠簸开销翻倍。
- GDScript 解释执行高频 CPU 计算:脚本层三重循环执行 4x4 矩阵乘法、逐 UI 图元无条件执行复杂多边形布尔裁剪。
- 双线程每帧 Ping-Pong 等待:Godot 主线程与 Python Script 线程通过互斥锁与条件变量每帧往返切换,受 macOS 大小核调度抖动影响。
- 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)和驱动锁争用,是掉帧与微卡顿的最大源头。
- 在 Godot 架构中,
2. C++ 到 GDScript 巨量跨语言数据封包与重复调用
- 代码位置:
project/python_ui_surface.gd:192-194project/python_3d_surface.gd:89, 136extension/src/python_host_node.cpp:169-220
- 机制危害:
Metin2PythonHost::render3d_draws()每次调用都会把所有 3D draw call 的 16 维矩阵、几万个顶点、法线、UV、颜色、索引逐个转换成PackedVector3Array、PackedFloat32Array并封装成带有 20~30 个键值对的庞大 GodotDictionary数组。- 严重重复调用:
在
python_ui_surface.gd:192-194中:这里仅仅为了检查 3D 绘制队列是否为空,就把整个场景的几何数据完整跨语言序列化了一遍,然后立即全部丢弃进垃圾回收! 紧接着在var draws_3d: Array = Metin2PythonHost.render3d_draws() var has_3d: bool = not draws_3d.is_empty()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 之间来回跳动,产生肉眼可见的“画面抽动 / 跳帧”感。
- MacBook 屏幕通常为 120Hz ProMotion。当前
4. GDScript 解释器高频数学与几何计算
- 代码位置:
project/python_3d_surface.gd:77-86:每一帧在 GDScript 解释器层面用三重循环对每一个 3D 模型进行 4x4 矩阵乘法,并频繁分配 16 个元素的数组。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 outproject/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-41renderer/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.cppproject/python_ui_surface.gdproject/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 专项优化
- C++ 端预分配与 StringName 优化:在
extension/src/python_host_node.cpp中预分配 Command Array 空间,使用静态StringName替换所有字典字符串键,避免每帧数百次重复字符串分配与动态哈希运算。 - 纹理元数据与 Atlas 缓存:在
project/python_ui_surface.gd建立_tex_info_cache,缓存纹理 RID、Atlas UV 缩放/偏移与全图默认 UV 数组,将 Atlas 区域与大小计算彻底移出每帧绘制主循环。 - 图元包围盒快速裁剪:基于预计算的
x1, y1, x2, y2与裁剪视口clip_x1, clip_y1, clip_x2, clip_y2执行 4 次浮点比较:- 完全在视口外的图元直接跳过;
- 完全在视口内的图元(占比 >95%)走无裁切快路径,直接组装多边形顶点,完全跳过多边形求交计算与中间数组构造。
- 颜色数组与画布清理池化:缓存高频单色数组,避免
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 怪场景的 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%)。
- 正确性完全保证:
- 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
- 1 怪:
独立 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 顶点/索引缓冲 + 着色器矩阵变换
- 持久化 GPU 缓冲与按修订号增量上传:以
GeometryId{geometry_key, signature}作为 GPU 缓冲缓存键(而非 draw 数组下标,避免视锥裁剪导致 draw 顺序变化时整表失效),仅在新几何出现或geometry_revision变化时上传顶点与索引缓冲。 - 着色器
world * view * proj变换:将每帧 CPU 顶点变换移入native.vertPush Constants,索引缓冲直接走vkCmdDrawIndexed(消除每帧 CPU 三角形展开)。
第三阶段:深度缓冲、DDS 贴图、法线/UV 与 D3D8 固定管线状态(Version 3 捕获)
- 深度与固定管线状态:新增
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裁剪。 - 自包含 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纹理与描述符集。 - 修复无
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 捕获)
- 分离真实 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中剥离。
- 在
- 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中支持 D3D8D3DTSS_TCI_CAMERASPACEPOSITION相机空间纹理矩阵生成地形基础层与 Alpha Splat 层 UV,并将m_textures[0]混入geometry_key实现跨帧持久化缓存。
- 在
- 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 批次)。
- 支持
- 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的BonePaletteSSBO 在顶点着色器中完成 4 骨骼线性混合蒙皮。
- 在
- 实时游戏主循环直接接入(
--live-client):- 当
mt_native_render在主构建目录build/下构建时,直接链接port_platform、mtpython与FakeLoginServer,通过--live-client <ClientDir>在原生 SDL3 + Vulkan 窗口内直接启动 40250system.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 |
关键验证结论:
- 真实 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垂直同步等待。 - 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% 通过):
- 交互启动与窗口/贴图扩展:
- 新增
--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时自动探测 HomebrewMoltenVK_icd.json。
- 新增
- 补齐剩余 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)高光叠加,并补齐完整 D3D8src_blend/dest_blend因子映射。 - 修复退出纯虚函数崩溃:移除基类析构函数
CSkyObject::~CSkyObject()对纯虚函数Destroy()的调用,以及CSpeedTreeForest::~CSpeedTreeForest()对纯虚函数Clear()的调用(移入派生类CSpeedTreeForestDirectX8::~CSpeedTreeForestDirectX8()),解决客户端退出时libc++abi: Pure virtual function called!崩溃。
- 水面(Water):移植
- 音频播放接入(SDL3 Audio + AudioToolbox):
- 在
native_render/main.cpp中实现NativeAudioEngine,每帧排空DrainAudioCommands(),通过 macOSAudioToolbox(AudioFileOpenWithCallbacks+ExtAudioFileRead)直接从内存解包.wav/.mp3为 44.1 kHz 双声道浮点 PCM,送入SDL_AudioStream完成 2D/3D 音效混音与 BGM 淡入淡出循环播放。
- 在
- 完整键盘/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 原生彩色游戏光标。
- 补齐
- 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 |