C ABI 与宿主服务
模块与宿主之间的完整契约。用 script.hpp 的基类与注册宏时这些细节全部由 SDK 代劳——本页写给想理解边界、写测试驱动或排查加载问题的人。
模块导出
每个脚本库导出 7 个 C 函数(mtr_native.h;注册宏自动生成全部):
| 导出 | 说明 |
|---|---|
uint32_t mtrNativeAbiVersion() | 返回编译时的 ABI 版本(当前 13);宿主拒绝不兼容的版本——与 JCM 对 JS 脚本的版本失配重载策略一致 |
const char* mtrScriptType() | "vehicle" / "pids" / "eyecandy" / "lift" / "psd" |
const char* mtrScriptId() | 脚本 id(须与资源包声明一致) |
size_t mtrStateSize() | 逐实例状态块大小;宿主为每个实例分配一块 |
void mtrInit(const JcmFrameInput*) | 在宿主刚分配状态块后调用一次,placement-new 构造 State(ABI 6 起必需;宿主漏调时模块惰性构造兜底) |
int32_t mtrCreate / mtrRender / mtrDispose(const JcmFrameInput*, …) | 生命周期入口;返回 0 成功、-1 空入参、-2 ABI 不匹配、-3 资源类型不符。宿主在每模块单一后台线程上按实例串行调用 |
帧输入与帧输出
JcmFrameInput(宿主 → 模块)
typedef struct JcmFrameInput {
uint32_t abi_version; /* 协商后的 ABI 版本 */
int32_t resource_kind; /* MtrResourceKind:vehicle/pids/eyecandy/lift/psd */
const void* snapshot; /* 对应的 Jcm*Snapshot */
void* state; /* 宿主分配的逐实例状态块 */
size_t state_size;
const struct JcmHostServices* host;/* 渲染线程可调用的宿主服务 */
} JcmFrameInput;
快照内部约定:所有 *_offset 字段都是相对快照首字节的字节偏移;string_pool_offset/len 描述共享 UTF-8 字符串池的位置。SDK 包装器替你完成了全部寻址。
JcmFrameOutput(模块 → 宿主)
输出是扁平记录流 + 五个输出内存区——「内存区」(arena)指一大块预先申请的内存,帧内按顺序分配、帧末整体交还,mtrRender 返回后宿主立即读取并回收帧(模块间双缓冲由宿主完成):
| 内存区 | 内容 | 容量(SDK 默认) |
|---|---|---|
records | 顺序排列的绘制记录(变长,按 record_size 步进遍历) | 256 条 |
matrix_arena | 矩阵流(列主序 4×4,每矩阵 16 个 float) | 128 组 |
string_arena | UTF-8 字符串流(贴图 id、文本、声音 id…) | 32 KB |
pixel_arena | BGRA8 像素流(纹理上传) | 初始 1 MB,按需增长至 16 MB,闲置后衰减回落 |
float_arena | float 流(体素碰撞箱等) | 1024 个 |
ABI 9 起输出同时给出各内存区的完整容量——宿主为每个内存区只建一个 ByteBuffer、每帧改 limit 复用,不再逐帧分配五个包装对象。
绘制记录
每条记录以 JcmRecordHeader{kind, flags, record_size} 开头,变长顺序排列。flags 承载记录级开关:目前定义 MTR_RECORD_FLAG_NATURAL_LIGHT(Text / Texture / Rect 的 natural_light()——取环境光照而非最大渲染亮度)。该字节由原 padding 就地升格:旧 SDK 编译的模块恒写 0、旧宿主忽略之——双向二进制兼容,无 ABI 跳版。8 种类型:
| kind | 结构 | 用途 / 镜像的 JS 调用 |
|---|---|---|
JCM_DRAW_TEXT | JcmDrawText | PIDS / 通用文本(TextWrapper 字段逐一对齐:对齐、溢出模式、跑马灯、z 序、层、字体 id) |
JCM_DRAW_TEXTURE | JcmDrawTexture | 贴图面片(UV、染色、z 序) |
JCM_DRAW_MODEL | JcmDrawModel | 车辆车厢 / 转向架 / 装饰模型(宿主模型句柄 + 姿态矩阵组) |
JCM_DRAW_SOUND | JcmDrawSound | 定位音效(车厢局部坐标偏移) |
JCM_DRAW_LOCAL_SOUND | JcmDrawSound | 报站广播(仅乘坐玩家可闻) |
JCM_DRAW_TEXTURE_UPLOAD | JcmDrawTextureUpload | GraphicsTexture 像素上传(BGRA8 + 脏矩形) |
JCM_DRAW_OUTLINE_SHAPE | JcmDrawShape | 装饰轮廓 / 碰撞箱(每箱 6 个 float;碰撞箱上限 1.5 格) |
JCM_DRAW_BLOCK_EVENT | — | 装饰 onBlockUse 事件握手 |
宿主回放这些记录用的是与 JS 脚本完全相同的渲染代码路径(PIDSDrawCall / ScriptRenderManager)——画出来就是同一个东西。
宿主服务
JcmHostServices 是一张函数指针表(in->host),渲染线程内可调用。旧宿主上新增槽位为 null,调用前判空即可降级:
| 服务 | 起版 | 说明 |
|---|---|---|
acquire_model / release_model | ABI 1 | 模型 JSON 句柄生命周期(镜像 ModelJS 句柄) |
create_texture / release_texture | ABI 1 | GraphicsTexture 生命周期(镜像 Java 类) |
rasterize_text | ABI 1 | 宿主 TTF 栅格化回调(CJK 等),BGRA8 写入调用方缓冲;返回 -1 表示字体不覆盖该码点(应回退内置位图字体) |
log | ABI 1 | console.log / console.error 通道 |
acquire_quad_model | ABI 4 | 宿主构建贴图面片(4 顶点 + UV + 渲染阶段 + 纹理句柄)——JS DisplayHelper 槽位模型的原生等价物;顶点为模型空间、UV 以贴图左上为原点、y 向下。无头驱动返回 -1,此时跳过绘制 |
measure_font_text / draw_font_text | ABI 7 | 资源字体的度量与基线栅格化(与 JS Graphics2D 同一字体、同一压缩与裁剪语义;脏边界出参) |
invoke_api | ABI 8 | mtr::Api 的底层通道:UTF-8 JSON 请求进、结果出到线程局部缓冲(下次调用前有效) |
defer_free | ABI 10 | 把已死亡的大块内存交给宿主后台回收线程(详见 性能与调试) |
ABI 版本演进
宿主接受 ABI 6–13 模块(各自按其时代的布局喂数、回报其自己的 ABI 号)。用当前 SDK 编译即产出 ABI 13:
| ABI | 引入内容 |
|---|---|
| 6 | mtrInit():宿主必须构造状态对象(零填充块对非平凡类型是未定义行为) |
| 7 | 资源字体度量 + 基线文本栅格化加入宿主服务 |
| 8 | invoke_api(完整 Java API 通道)、电梯快照、换乘类型化模式;ABI 8 库要求 JCM 2.3.0-beta-2.yanyang.7 及以上宿主 |
| 9 | 换乘 / 车辆的规范交通类型 id(MAGIC MTR 扩展);宿主停止逐帧复制快照、内存区容量出参 |
| 10 | defer_free 延迟回收钩子(脚本自有大缓冲的离 JVM 线程释放) |
| 11 | 车辆快照 door_direction(开门方向 0..3 —— 由原 _pad0 字节就地升格,布局与尺寸不变,老模块照常工作) |
| 12 | MTR_RESOURCE_PSD(第 5 类脚本)与 JcmPsdSnapshot / JcmPsdArrival / JcmPsdRoute / JcmPsdDoor 快照——纯增量,既有结构零变化;ABI 12 库要求晏阳 hotfix.2 以上宿主 |
| 13 | JcmPsdSnapshot.station_offset(字节偏移 124;-1 = 无——该字段在 ABI 12 中是填充位)与其后 96 字节的 JcmPsdStation 记录,支撑 psd.stop() / mtr::Psd::stop():站名、颜色(ARGB + 十六进制)、票价区 1–3、停靠本站的全部线路、邻站线路分组、物理连通站名、出口及去向、站台编号——纯增量,既有结构零变化;ABI 6–12 模块照常加载 |
函数指针表随 ABI 增长:新模块会调用旧宿主不存在的槽位。宿主按模块报告的 ABI 号选择对应的结构布局与喂料方式,不匹配就拒载——这是刻意的失败快策略。
传输层(宿主内部,脚本不可见)
自 2.3.0-beta-2.yanyang.6(layout v2)起模组内完全无 JNI:两个调用平面承载全部跨语言流量——Panama FFM 平面(JDK 22+,libraryLookup + jcm_* downcall + upcall vtable)与共享内存轮询平面(任意 JDK ≥ 16,桥以构造器启动的后台 poller 线程经文件系统握手发现区域,一切经共享页服务)。脚本代码对此完全无感——模块只看到上述 C ABI。
接下来:性能与调试 —— 把脚本跑得又快又稳。