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 it was compiled against (currently 13); the host rejects incompatible versions โ€” the same version-mismatch reload policy JCM applies to 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; constructs State with placement-new (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 success, -1 null input, -2 ABI mismatch, -3 wrong resource type. The host calls them per instance, serially, on a single background thread per module

Frame input and frame output

JcmFrameInput (host โ†’ module)

typedef struct JcmFrameInput {
    uint32_t abi_version;              /* Negotiated ABI version */
    int32_t  resource_kind;            /* MtrResourceKind: vehicle/pids/eyecandy/lift/psd */
    const void* snapshot;              /* The matching 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 convention: every *_offset field is a byte offset relative to the snapshot's first byte; string_pool_offset/len describe where the shared UTF-8 string pool lives. The SDK wrappers do all of this addressing for you.

JcmFrameOutput (module โ†’ host)

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

ArenaContentsCapacity (SDK default)
recordsDraw records in sequence (variable length; walk them in record_size steps)256 records
matrix_arenaMatrix stream (column-major 4ร—4, 16 floats per matrix)128 sets
string_arenaUTF-8 string stream (texture ids, text, sound ids, โ€ฆ)32 KB
pixel_arenaBGRA8 pixel stream (texture uploads)1 MB initially, grows on demand up to 16 MB, decays back when idle
float_arenaFloat stream (voxel collision boxes, etc.)1024 floats

Since ABI 9 the output also reports each arena's full capacity โ€” the host creates a single ByteBuffer per arena and reuses it every frame by moving the limit, instead of allocating five wrapper objects per frame.

Draw records

Each record starts with 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 โ€” take the world light value instead of the max render light). The byte was promoted in place from padding: modules compiled against older SDKs always write 0 and older hosts ignore it โ€” binary compatible in both directions, no ABI bump. Eight kinds:

kindStructPurpose / JS call mirrored
JCM_DRAW_TEXTJcmDrawTextPIDS / general text (field-by-field alignment 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 sounds (car-local coordinate offset)
JCM_DRAW_LOCAL_SOUNDJcmDrawSoundAnnouncements (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 capped at 1.5 blocks)
JCM_DRAW_BLOCK_EVENTโ€”Eyecandy onBlockUse event handshake

The host replays these records through 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, newer slots are null; null-check before calling to 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 rasterization callback (CJK etc.), writes BGRA8 into the caller's buffer; -1 means the font doesn't 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 to the texture's top-left, y points down. Headless drivers return -1; skip drawing then
measure_font_text / draw_font_textABI 7Resource-pack font metrics and baseline rasterization (same font as JS Graphics2D, same compression and clipping semantics; dirty bounds as out-parameters)
invoke_apiABI 8The transport under mtr::Api: UTF-8 JSON request in, result out into 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)

ABI version history

The host accepts ABI 6โ€“13 modules (each is fed with its era's layout 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 (zero-filled blocks are undefined behavior for non-trivial types)
7Resource-pack font metrics + baseline text rasterization joined 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 (MAGIC MTR extensions); the host stopped copying snapshots every frame, arena capacities became out-parameters
10The defer_free deferred-reclamation hook (freeing the script's 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, color (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 refused

The function pointer table grows with the ABI: a new module will call slots that an old host doesn't have. The host picks the struct layout and feeding scheme matching the ABI number the module reports, and refuses to load on mismatch โ€” a deliberate fail-fast strategy.

Transport layer (host-internal, 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; the bridge discovers regions through a filesystem handshake using a poller thread started from the constructor, and everything is served over shared pages). Script code is completely unaware of this โ€” modules only see the C ABI above.


Up next: Performance and debugging โ€” keeping your scripts fast and stable.