Drawing and canvases
Two routes: PIDS screens go straight out through the Text / Texture / Rect fluent APIs; pixel-level content (LCDs, dot-matrix screens) uses GraphicsTexture + Gfx2D native rasterisation.
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 method | JS equivalent | Description |
|---|---|---|
.text(str[, len]) | .text() | UTF-8 text |
.scale(s) | .scale() | Uniform scale |
.left_align() / .center_align() / .right_align() | Same name | Alignment (centred by default) |
.shadowed() / .bold() / .italic() | Same name | Text styling |
.stretch_xy() / .scale_xy() / .wrap_text() / .marquee(duration) | Same name | Overflow modes (the marquee can be pinned to a fixed progress with .with_marquee_progress(p)) |
.font(id) / .font_mc() | Same name | Resource font (e.g. .font("jsblock:shanghai")) / the built-in MC font |
.color(argb) | Same name | Colour (ARGB) |
.pos(x, y) / .size(w, h) | Same name | Screen coordinates and bounding box |
.z_order(z) / .layer(l) | Same name | Layering; 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 method | Description |
|---|---|
.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 method | Description |
|---|---|
.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
| Member | Description |
|---|---|
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());
}
| Member | Description |
|---|---|
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) / destructor | Releases the host-side GPU texture |
handle() / width() / height() / texture_bytes() | Host texture handle and size information |
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();
| Primitive | Description |
|---|---|
fill_rect / fill_round_rect / draw_round_rect | Rectangles (rounded-corner radius parameter) |
fill_oval / draw_oval | Ellipse 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_clip | Clip-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_text | Text (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:
| Tier | Coverage | Description |
|---|---|---|
| Resource font (preferred) | All of UTF-8 | Pass 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 callback | CJK and more | Without 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 font | ASCII / Latin | Scales 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.