Files
dmf-studio-rnd/mtl.md
T
2026-07-13 19:06:11 +03:00

2.7 KiB

ST 2110-20 receiver with Intel MTL

The MTL pattern is structurally identical to DeckLinkReceiver — callback-based, MTL manages frame buffers, you drain them into MXL.

MTL initialization (once per process)

mtl_init_params p{};
p.ports[MTL_PORT_P] = "0000:31:00.0"; // PCI address or netdev for AF_XDP
p.num_ports = 1;
p.flags = MTL_FLAG_BIND_NUMA;
// For AF_XDP mode (no DPDK hugepages needed):
p.transport = MTL_TRANSPORT_AF_XDP;

mtl_handle dev = mtl_init(&p);

Create RX session

st20_rx_ops ops{};
ops.port.num_port = 1;
memcpy(ops.port.sip_addr[MTL_PORT_P], multicast_ip, 4); // join this group
ops.port.udp_port[MTL_PORT_P] = 20000;
ops.width  = 1280;
ops.height = 720;
ops.fps    = ST_FPS_P25;
ops.fmt    = ST20_FMT_YUV_422_10BIT; // RFC 4175 packed — see note below
ops.framebuff_cnt = 3;
ops.notify_frame_ready = on_frame_ready; // your callback
ops.priv = this;

st20_rx_handle rx = st20_rx_create(dev, &ops);

Callback — replaces wait_for_frame()

static int on_frame_ready(void* priv, void* frame, st20_rx_frame_meta* meta) {
    auto* self = static_cast<St2110InNode*>(priv);
    // frame points to a complete assembled frame — MTL did the reassembly
    const uint64_t index = mxlGetCurrentIndex(&self->video_rate);
    mxlGrainInfo grain{};
    uint8_t* buf = nullptr;
    if (mxlFlowWriterOpenGrain(self->writer, index, &grain, &buf) == MXL_STATUS_OK) {
        memcpy(buf, frame, self->frame_size); // ← see format note
        grain.validSlices = grain.totalSlices;
        mxlFlowWriterCommitGrain(self->writer, &grain);
    }
    st20_rx_put_framebuff(self->rx, frame); // return buffer to MTL pool
    return 0;
}

Format conversion: RFC 4175 vs V210

ST 2110-20 wire format is RFC 4175 packed 10-bit — not V210. They encode the same YCbCr 4:2:2 10-bit data differently:

  • RFC 4175: 5 bytes per 2 pixels, big-endian packed
  • V210: 4 bytes per 3 luma + 2 chroma, little-endian with padding bits

MXL in this project uses V210. Check MTL's output_fmt option — newer MTL versions support ST_FRAME_FMT_V210 as the output format, which means MTL does the conversion internally. If your version doesn't have it, you'll need a small RFC4175→V210 conversion step before the memcpy.

Check st_frame_fmt enum in MTL headers for what's available.

Teardown

st20_rx_free(rx);
mtl_uninit(dev);

Main loop

The callback is called from MTL's internal thread (like DeckLink's), so the MXL write happens inside the callback rather than in the main loop. The main loop just blocks on g_running:

while (dmf::g_running.load(std::memory_order_relaxed))
    mxlSleepForNs(10'000'000);

MTL drives the pacing, exactly like DeckLink hardware does.