C ABI and host services

Module exports

Every script library exports 7 C functions (mtr_native.h; the registration macros generate them all):

ExportDescription
uint32_t mtrNativeAbiVersion()Returns the ABI version compiled against (currently 13); the host rejects incompatible versions—matching JCM's version-mismatch reload policy for JS scripts
const char* mtrScriptType()"vehicle" / "pids" / "eyecandy" / "lift" / "psd"
const char* mtrScriptId()The script id (must match the resource pack declaration)
size_t mtrStateSize()Per-instance state block size; the host allocates one block per instance
void mtrInit(const JcmFrameInput*)Called once right after the host allocates the state block; placement-new constructs the State (required since ABI 6; if the host forgets to call it, the module falls back to lazy construction)
int32_t mtrCreate / mtrRender / mtrDispose(const JcmFrameInput*, …)Lifecycle entry points; return 0 for success, -1 for a null argument, -2 for an ABI mismatch, -3 for a resource kind mismatch. The host calls them serially per instance on a single background thread per module

Frame input and frame output

JcmFrameInput (host → module)

typedef struct JcmFrameInput {
    uint32_t abi_version;              /* The negotiated ABI version */
    int32_t  resource_kind;            /* MtrResourceKind: vehicle/pids/eyecandy/lift/psd */
    const void* snapshot;              /* The corresponding Jcm*Snapshot */
    void*    state;                    /* Host-allocated per-instance state block */
    size_t   state_size;
    const struct JcmHostServices* host;/* Host services callable on the render thread */
} JcmFrameInput;

Snapshot internals: every *_offset field is a byte offset relative to the first byte of the snapshot; string_pool_offset/len describes the location of the shared UTF-8 string pool. The SDK wrappers do all of this addressing for you.

JcmFrameOutput (module → host)

The output is a flat record stream + five arenas; once mtrRender returns, the host reads it immediately and reclaims the frame (double buffering between modules is handled by the host):

ArenaContentsCapacity (SDK default)
recordsDraw records laid out in sequence (variable length; walk them in record_size steps)256 records
matrix_arenaA stream of matrices (column-major 4×4, 16 floats each)128 sets
string_arenaA stream of UTF-8 strings (texture ids, text, sound ids, …)32 KB
pixel_arenaA stream of BGRA8 pixels (texture uploads)Starts at 1 MB, grows on demand up to 16 MB, decays back when idle
float_arenaA stream of floats (voxel collision boxes and so on)1024 floats

Since ABI 9 the output also carries each arena's full capacity—the host creates just one ByteBuffer per arena and reuses it by changing the limit each frame, instead of allocating five wrapper objects every frame.

Draw records

Every record starts with a JcmRecordHeader{kind, flags, record_size} and is laid out sequentially with variable length. flags carries record-level switches: currently MTR_RECORD_FLAG_NATURAL_LIGHT (the natural_light() option of Text / Texture / Rect — world light instead of max render light). The byte was promoted in place from padding: older-SDK modules always write 0 and older hosts ignore it — binary compatible in both directions, no ABI bump. The 8 kinds:

kindStructurePurpose / JS call mirrored
JCM_DRAW_TEXTJcmDrawTextPIDS / general text (the fields align one-to-one with TextWrapper: alignment, overflow mode, marquee, z-order, layer, font id)
JCM_DRAW_TEXTUREJcmDrawTextureTexture quads (UV, tint, z-order)
JCM_DRAW_MODELJcmDrawModelVehicle cars / bogies / eyecandy models (host model handle + pose matrix set)
JCM_DRAW_SOUNDJcmDrawSoundPositioned sound effects (car-local coordinate offset)
JCM_DRAW_LOCAL_SOUNDJcmDrawSoundIn-car announcements (audible only to riding players)
JCM_DRAW_TEXTURE_UPLOADJcmDrawTextureUploadGraphicsTexture pixel upload (BGRA8 + dirty rectangle)
JCM_DRAW_OUTLINE_SHAPEJcmDrawShapeEyecandy outline / collision boxes (6 floats per box; collision boxes are capped at 1.5 blocks)
JCM_DRAW_BLOCK_EVENT—Eyecandy onBlockUse event handshake

The host replays these records along exactly the same rendering code path as JS scripts (PIDSDrawCall / ScriptRenderManager)—what gets drawn is the same thing.

Host services

JcmHostServices is a table of function pointers (in->host) callable on the render thread. On older hosts the newer slots are null; test for null before calling and degrade gracefully:

ServiceSinceDescription
acquire_model / release_modelABI 1Model JSON handle lifecycle (mirrors ModelJS handles)
create_texture / release_textureABI 1GraphicsTexture lifecycle (mirrors the Java class)
rasterize_textABI 1Host TTF rasterisation callback (CJK and more), writing BGRA8 into the caller's buffer; -1 means the font does not cover that code point (fall back to the built-in bitmap font)
logABI 1The console.log / console.error channel
acquire_quad_modelABI 4The host builds a textured quad (4 vertices + UV + render stage + texture handle)—the native equivalent of the JS DisplayHelper slot model; vertices are in model space, UVs are anchored at the texture's top-left corner, and y points down. Headless drivers return -1; skip drawing then
measure_font_text / draw_font_textABI 7Metric measurement and baseline rasterisation for resource fonts (the same font as JS Graphics2D, the same compression and clipping semantics; dirty bounds as out-parameters)
invoke_apiABI 8The channel underneath mtr::Api: a UTF-8 JSON request goes in, the result lands in a thread-local buffer (valid until the next call)
defer_freeABI 10Hands already-dead large memory blocks to the host's background reclamation thread (see Performance and debugging for details)

ABI version history

The host accepts ABI 6–13 modules (each is fed the layout of its era and reports its own ABI number). Compiling with the current SDK produces ABI 13:

ABIWhat it introduced
6mtrInit(): the host must construct the state object (a zero-filled block is undefined behaviour for non-trivial types)
7Resource-font metrics + baseline text rasterisation join the host services
8invoke_api (the full Java API channel), the lift snapshot, typed interchange modes; ABI 8 libraries require a JCM Yanyang.7 or newer host
9Canonical transport type ids for interchanges / vehicles (the MAGIC MTR extensions); the host stops copying the snapshot every frame, arena capacities become out-parameters
10The defer_free deferred-reclamation hook (freeing scripts' own large buffers off the JVM threads)
11door_direction on the vehicle snapshot (door-opening side 0..3 — the former _pad0 byte promoted in place; layout and size unchanged, older modules keep working)
12MTR_RESOURCE_PSD (the 5th script kind) and the JcmPsdSnapshot / JcmPsdArrival / JcmPsdRoute / JcmPsdDoor snapshots — purely additive, every pre-existing struct unchanged; ABI 12 modules need a Yanyang hotfix.2+ host
13JcmPsdSnapshot.station_offset (byte offset 124; -1 = none — the field was padding in ABI 12) and the 96-byte JcmPsdStation record behind psd.stop() / mtr::Psd::stop(): station name, colour (ARGB + hex), fare zones 1–3, every route calling at the station, connecting-station route groups, physically connected station names, exits with destinations, platform numbers — purely additive, every pre-existing struct unchanged; ABI 6–12 modules keep loading
Why old hosts must be rejected

The function pointer table grows with the ABI: a new module would call slots that an old host does not have. The host picks the matching structure layout and feeding strategy from the ABI number the module reports and refuses to load on mismatch—this fail-fast policy is deliberate.

Transport layer (internal to the host, invisible to scripts)

Since 2.3.0-beta-2.yanyang.6 (layout v2) the mod contains no JNI at all: two calling planes carry all cross-language traffic—the Panama FFM plane (JDK 22+, libraryLookup + jcm_* downcalls + an upcall vtable) and the shared-memory polling plane (any JDK ≥ 16, where the background poller thread started by the bridge's constructor discovers regions via a file-system handshake and everything is served through shared pages). Script code is completely unaware of this—modules only ever see the C ABI above.


Up next: Performance and debugging — keeping your scripts fast and stable.