Drawing and canvases
Choosing between the two routes

Screen-coordinate content (PIDS text rows, texture tiles) → Text/Texture: one draw record, replayed by the host along exactly the same code path as JS. Pixel-level content (vehicle LCDs, dynamic dot-matrix screens, charts) → draw the pixels yourself on a GraphicsTexture, rasterise through Gfx2D primitives, and upload only the dirty rectangle to the GPU.

Text / Texture / Rect (PIDS)

<mtr/text.hpp> is a 1:1 port of the JS TextWrapper / TextureWrapper / Rect fluent APIs—method names follow the C++ snake_case convention, and calls are chained:

mtr::Text::create()
    .text("Tsim Sha Tsui")
    .scale(1.725)
    .size(100, 9)
    .stretch_xy()
    .font_mc()
    .color(0xFC9700)
    .pos(3, rowY)
    .draw(ctx);
Text methodJS equivalentDescription
.text(str[, len]).text()UTF-8 text
.scale(s).scale()Uniform scale
.left_align() / .center_align() / .right_align()Same nameAlignment (centred by default)
.shadowed() / .bold() / .italic()Same nameText styling
.stretch_xy() / .scale_xy() / .wrap_text() / .marquee(duration)Same nameOverflow modes (the marquee can be pinned to a fixed progress with .with_marquee_progress(p))
.font(id) / .font_mc()Same nameResource font (e.g. .font("jsblock:shanghai")) / the built-in MC font
.color(argb)Same nameColour (ARGB)
.pos(x, y) / .size(w, h)Same nameScreen coordinates and bounding box
.z_order(z) / .layer(l)Same nameLayering; with z=-1 the context assigns it automatically
.render_type("LIGHT_1").renderType()String render layer (equivalent to QueuedRenderLayer.valueOf; unknown names throw)
.natural_light().naturalLight()Take the world light value instead of the max render light
Texture methodDescription
.texture(id)Texture resource id (e.g. "mtr:textures/block/pids.png")
.uv(u1, v1, u2, v2)UV sub-rectangle
.color(argb)Tint
.pos(x, y) / .size(w, h)Position and size
.z_order(z) / .layer(l)Layering
.render_type("LIGHT_1") / .natural_light()String render layer / world light (as Text)

Rect (solid rectangles)

mtr::Rect mirrors the upstream JS Rect class (renamed from Rectangle as of upstream v2.3.0-prerelease.2; the Java class name is unchanged) — it draws a full-UV quad of mtr:white.png tinted with a colour: the lightest way to build background blocks, separator bars and status colour strips:

mtr::Rect::create()
    .color(0xFF00539F)          // opaque tint (default ARGB_WHITE)
    .pos(2, rowY + 18)
    .size(pids.width() - 4, 1)  // default 20x20
    .draw(ctx);
Rect methodDescription
.color(argb)Tint (default white ARGB_WHITE)
.pos(x, y) / .size(w, h)Position and size (default 20x20)
.render_type("LIGHT_2")String render layer (default LIGHT_2)
.natural_light() / .z_order(z)World light / z-order (z=-1 auto-assigned)

Companion context methods (PidsContext):

  • ctx.set_auto_z_ordering(bool) — automatic z-ordering (on by default)
  • ctx.set_z_order_step(step) — the automatic z-ordering step (0.0002 by default)

Render layers (the MtrRenderLayer enum of 2D layers, independent of mtr::Matrices): MTR_LAYER_TEXT (text), MTR_LAYER_LIGHT (light-emitting objects), MTR_LAYER_EXTERIOR / MTR_LAYER_INTERIOR (exterior / interior), matching MTR's QueuedRenderLayer.

Matrices and model drawing

<mtr/matrices.hpp> mirrors the JS global Matrices: transforms are pushed onto a stack, and compile() compiles the current stack into a matrix set written into the frame matrix arena:

mtr::Matrices m;
m.translate(0.5f, 1.0f, 0.0f);
m.rotate_y_degrees(90.0f);
m.scale(1.0f, 1.0f, 1.0f);
ctx.draw_car_model(model_handle, car_index, &m);   // Vehicle
ctx.draw_model(model_handle, &m);                   // Eyecandy / lift
MemberDescription
translate(x, y, z) / scale(x, y, z)Translation / scale
rotate_x / rotate_y / rotate_z(radians)Rotation (radians)
rotate_x_degrees / …_y… / …_z…(deg)Rotation (degrees)
compile()Compiles the current pose into the frame (returns the matrix set index; pass it to draw calls)

The vehicle context additionally offers draw_bogie_model(model, car, bogie, matrices) (bogies), play_car_sound(sound, car, x, y, z, volume, pitch) and play_ann_sound(sound, volume, pitch) (the in-car announcement, audible only to riding players).

Model handles come from the host services: ctx.host()->acquire_model(user, "namespace:path/model.json") acquires one, release_model(user, handle) releases it; acquire_quad_model(...) builds a host-owned textured quad from 4 model-space vertices + UVs (turning a GraphicsTexture into a screen you can stick onto a car). See C ABI and host services for the details.

GraphicsTexture (pixel canvas)

The GraphicsTexture from <mtr/gfx.hpp> keeps the pixel buffer inside your module; the game side receives only dirty-rectangle uploads—pixel data is not carried across the boundary frame after frame:

struct LcdState {
    mtr::GraphicsTexture tex;      // Best kept in State: same lifetime as the instance
};

void create(mtr::VehicleContext& ctx, LcdState& s, const mtr::Vehicle&) {
    s.tex.create(ctx.input(), 512, 128);   // Also registers the GPU texture with the host
    // Stick the texture onto a car: host->acquire_quad_model(...) → draw_car_model(...)
}

void render(mtr::VehicleContext& ctx, LcdState& s, const mtr::Vehicle& vehicle) {
    // 1. Draw pixels (Gfx2D or plain set_pixel)
    // 2. Upload only the changed region:
    s.tex.upload(ctx.frame());
}
MemberDescription
create(input, w, h)Allocates the pixel buffer (BGRA8, row-major) and creates the GPU texture on the host side; the whole texture is treated as dirty afterwards
pixels() / pixels32()Direct pixel pointers (uint8_t* / uint32_t*)
set_pixel(x, y, argb)Writes a single pixel and accumulates the dirty region automatically
upload(frame)Writes the current dirty rectangle into the frame pixel arena (one JCM_DRAW_TEXTURE_UPLOAD record); the dirty region resets afterwards
last_upload_bytes()Bytes uploaded last time—a great performance observable
close(input) / destructorReleases the host-side GPU texture
handle() / width() / height() / texture_bytes()Host texture handle and size information
Performance essentials

upload() carries only the bounding box that has changed since the last upload. Paint a static background once; each frame, touch only the strips that need to move (marquees, blinking segments, progress bars). If a vehicle LCD's blinking/cycling changes only a thin strip, the upload is only that thin strip.

The same header also provides LcdCanvas—an LCD paintbrush built on the built-in 5×7 dot-matrix font (lit/unlit colours configurable), well suited to classic dot-matrix screens.

Gfx2D software rasteriser

<mtr/gfx2d.hpp> implements the commonly used subset of java.awt.Graphics2D as a deterministic software rasteriser that draws straight onto a GraphicsTexture. fillRect / fillOval / drawLine / GeneralPath / clip / drawString logic from JS resource packs can be ported over mechanically:

mtr::Gfx2D g(s.tex, ctx.host(), "jsblock:shanghai");  // Canvas + host font

g.set_color(mtr::clr(12, 28, 44));                    // Dark background
g.fill_rect(0, 0, W, H);

g.push_clip(8, 8, W - 16, 40);                       // Clip stack
g.set_color(mtr::clra(255, 205, 66, 255));
g.draw_text_centered(W / 2, 30, 18.0, "下一站 Next");
g.pop_clip();
PrimitiveDescription
fill_rect / fill_round_rect / draw_round_rectRectangles (rounded-corner radius parameter)
fill_oval / draw_ovalEllipse fill / stroke (line width)
draw_line(x1, y1, x2, y2, lw)Line segments
fill_polygon(xs, ys, n)Polygon fill (the GeneralPath equivalent)
push_clip / pop_clipClip-rectangle stack; every primitive is clipped before rasterising, and the dirty region is constrained by the clip as well
draw_text / draw_text_centered / draw_text_compressed / draw_mixed_textText (see the font section below)
draw_logo_badge(x, y, w, h, bg, fg)Rounded badge backing
full_aa() / no_aa()One-pixel-coverage antialiasing toggle (off by default, mirroring upstream fullAA() / noAA()): edges of oval / rounded-rect / stroke / polygon fills get an integer src-over blend; text remains host-TTF antialiased throughout

Colour helpers: mtr::clr(r, g, b) / mtr::clra(r, g, b, a) (opaque / with alpha), matching the same-named functions in the JS config.js. Semi-transparent colours composite with src-over; opaque colours take a fast path.

Font stack

Three tiers of fonts, degrading automatically by availability:

TierCoverageDescription
Resource font (preferred)All of UTF-8Pass a font resource id to Text::font("jsblock:shanghai") or as the third Gfx2D constructor argument: this uses the host's real FontMetrics and drawString, preserving letter spacing, baseline, compression, alpha and device clipping—the same font pipeline as the JS side
Host TTF callbackCJK and moreWithout a resource font, CJK glyphs are rasterised through the rasterize_text host service (mirroring the JCM FontManager's TTF set)
Built-in 5×7 bitmap fontASCII / LatinScales to the requested size and steps proportionally to glyph width (narrow i/l/., wide m/W)—text_width() and the actual drawing use the same numbers, so JS's adaptive font size / 5% compression layout algorithms port over unchanged; without a host, CJK degrades to placeholder boxes

Metric conventions of the built-in font: ascent = 0.78 em, descent = 0.22 em (≈ Arial / HanSans); full-width CJK advances 1.0 em. Resource-font metrics (the measure_font_text host service) are computed from the real font.


Up next: mtr::Api and Java interop — everything beyond the snapshot.