# 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) ```cpp 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 ```cpp 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()` ```cpp static int on_frame_ready(void* priv, void* frame, st20_rx_frame_meta* meta) { auto* self = static_cast(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 ```cpp 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`: ```cpp while (dmf::g_running.load(std::memory_order_relaxed)) mxlSleepForNs(10'000'000); ``` MTL drives the pacing, exactly like DeckLink hardware does.