Porting to a new display

Connect the portable renderer to your own display, operating system, and input.

The portable package does not depend on the Waveshare adapter. Keep the core and supply rendering, timing and input integration for your platform.

For integration into an existing application or GUI, start with the integration guide. It covers the high-level Mascot API, temporary reactions, multiple instances, and commands from other tasks.

1. Allocate a framebuffer

Provide writable native-endian uint16_t RGB565 pixels. For a tightly packed buffer, capacity is width * height pixels, or twice that many bytes. A stride larger than width is supported; capacity must cover (height - 1) * stride + width. Keep the allocation alive for every renderer that borrows it. On embedded targets, large buffers usually belong in PSRAM or another suitable long-lived allocation.

mochi::Renderer renderer({pixels, capacityInPixels, width, height, strideInPixels});
if (!renderer.valid()) {
    // Handle an invalid allocation or layout before rendering.
}

The renderer performs no allocation. An adapter may allocate a framebuffer at startup; the included Waveshare adapter does so in PSRAM.

An existing framebuffer can be shared through sub_surface(): each view clips to its widget rectangle while keeping the parent stride. If a full framebuffer does not fit, use the stripe example. Advance the pose once per complete frame, render it into each stripe with an offset center, and finish each display transfer before reusing the buffer. Measure the additional rasterization cost and task stack usage on your target.

2. Drive animation and present pixels

Use actual elapsed seconds for Animator::update(), then clear/restore the old background, draw the current pose, and send the changed pixels to the display. Choose a center and scale that fit all poses, including raised hands, leaves and ears. Selecting a character on the renderer does not change the animator’s state.

Keep pixel byte swapping at the display-driver boundary. The SDK always treats its surface as native-endian RGB565, regardless of the panel’s wire format.

Start with complete-frame updates. If optimizing to a smaller rectangle, verify every character and state remains inside it. The existing demo’s rows 48–347 are specific to its 480 × 480 layout and cannot be reused blindly. Respect the panel’s alignment rules; the CO5300 adapter expands updates to even rows.

3. Feed calibrated touch reports

Apply display rotation and touch mirroring in the adapter, so touch coordinates match the rendered screen. Pass Pressed, Released, or None reports into GestureRecognizer::update() with a wrapping uint32_t millisecond timestamp. Keep polling during None so missing releases can expire safely. The application decides what taps and swipes mean; the core does not own menus.

For non-touch targets, change states from physical buttons or application events. Use one task for input, animation and rendering, or queue commands to that task.

4. Keep the adapter separate

Put pin maps, reset sequences, bus ownership and driver dependencies in their own package. Document the exact board revision and supported toolchain. Preserve upstream licenses. Do not introduce driver headers or GPIO constants into the core.

5. Validate the port

Run the native SDK tests, then check display orientation, RGB565 colors, clipping, all 40 character/state combinations, pause and pace, touch coordinates and reconnects on the real device. Record framebuffer placement, memory use, frame rate and any unsupported hardware. Use the contribution checklist.

Improve this page on GitHub ↗