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 through exactly the same code path as JS. Pixel-level content (vehicle LCDs, dynamic dot-matrix screens, charts) β†’ GraphicsTexture draws its own pixels, rasterized through Gfx2D primitives, and only the dirty rectangles are uploaded 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 counterpartDescription
.text(str[, len]).text()UTF-8 text
.scale(s).scale()Uniform scale
.left_align() / .center_align() / .right_align()Same nameAlignment (centered by default)
.shadowed() / .bold() / .italic()Same nameText styling
.stretch_xy() / .scale_xy() / .wrap_text() / .marquee(duration)Same nameOverflow modes (the marquee can pin its progress with .with_marquee_progress(p))
.font(id) / .font_mc()Same nameResource-pack font (e.g. .font("jsblock:shanghai")) / the built-in MC font
.color(argb)Same nameColor (ARGB)
.pos(x, y) / .size(w, h)Same nameScreen coordinates and bounding box
.z_order(z) / .layer(l)Same nameLayering; z=-1 lets the context assign 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 asset 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 (same 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 color: the lightest way to build background blocks, separator bars and status color 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) β€” automatic z-ordering step (0.0002 by default)

Render layers (the 2D layer enum MtrRenderLayer, 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 push onto a stack, and compile() compiles the current stack into a matrix set written into the frame's 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 a 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) (announcements audible only to riding players).

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

GraphicsTexture (pixel canvas)

GraphicsTexture from <mtr/gfx.hpp> keeps the pixel buffer inside your module; the game side only receives dirty-rectangle uploads β€” pixel data is not hauled across the boundary every 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
    // Mount the texture on 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 set_pixel directly)
    // 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; everything is treated as dirty after it returns
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's pixel arena (one JCM_DRAW_TEXTURE_UPLOAD record); the dirty region then resets
last_upload_bytes()Bytes uploaded last time β€” a great performance probe
close(input) / destructorReleases the host GPU texture
handle() / width() / height() / texture_bytes()Host texture handle and size information
Performance note

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

The same header also provides LcdCanvas β€” an LCD paintbrush on the built-in 5Γ—7 dot-matrix font (on/off colors configurable), a good fit for classic dot-matrix screens.

Gfx2D software rasterizer

<mtr/gfx2d.hpp> implements the commonly used subset of java.awt.Graphics2D as a deterministic software rasterizer that draws straight onto a GraphicsTexture. The fillRect / fillOval / drawLine / GeneralPath / clip / drawString logic from JS resource packs ports 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 (with a rounded-corner arc-radius parameter)
fill_oval / draw_ovalOval fill / stroke (line width)
draw_line(x1, y1, x2, y2, lw)Line segment
fill_polygon(xs, ys, n)Polygon fill (GeneralPath equivalent)
push_clip / pop_clipClip-rectangle stack; every primitive is clipped before rasterizing, and the dirty region is clip-constrained too
draw_text / draw_text_centered / draw_text_compressed / draw_mixed_textText (fonts in the next section)
draw_logo_badge(x, y, w, h, bg, fg)Rounded badge background
full_aa() / no_aa()One-pixel-coverage antialiasing toggle (default off, mirroring upstream fullAA() / noAA()): edges of oval / rounded-rect / stroke / polygon fills get an integer src-over blend; text is always host-TTF antialiased and unaffected

Color 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 colors composite with src-over; opaque colors take a fast path.

Font stack

Three font tiers, falling back automatically by availability:

TierCoverageDescription
Resource-pack font (preferred)Full UTF-8Pass a font asset id to Text::font("jsblock:shanghai") or as the third Gfx2D constructor argument: it uses the host's real FontMetrics and drawString, preserving letter spacing, baselines, compression, alpha, and device clipping β€” the same font pipeline as the JS side
Host TTF callbackCJK and moreWithout a resource-pack font, CJK glyphs are rasterized through the rasterize_text host service (mirroring the JCM FontManager's TTF set)
Built-in 5Γ—7 bitmap fontASCII / LatinScaled to the requested size, with proportional advances per glyph shape (narrow i/l/., wide m/W) β€” text_width() and the actual drawing share the same numbers, so JS adaptive font sizes / the 5% compression layout algorithm port over as-is; CJK degrades to placeholder boxes when there is no host

Metric conventions of the built-in font: ascent = 0.78 em, descent = 0.22 em (β‰ˆ Arial / HanSans); CJK full-width glyphs advance 1.0 em. Resource-pack fonts are measured (measure_font_text host service) with their real font metrics.


Up next: mtr::Api and Java interop β€” everything beyond the snapshots.