Initial commit: testpattern → fakesink pipeline over MXL shared memory

- NodeBase, Signal, FlowDef, V210 shared headers
- testpattern node: SMPTE 75% color bars writer at 25fps
- fakesink node: non-blocking MXL reader with per-second stats
- studio-manager: FlowGraph data model, graph-driven fork/exec launcher
- mxl pinned as submodule at 0ae1dc5

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
JohannesItten
2026-06-23 13:35:01 +03:00
commit ca682eaf0a
17 changed files with 1269 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
namespace dmf::v210 {
// SMPTE 75% color bars — 10-bit limited range
// Y: 64 (black) to 940 (white)
// Cb/Cr: 64 to 960, 512 = neutral grey
struct Color { uint16_t y, cb, cr; };
constexpr std::array<Color, 7> SMPTE_BARS = {{
{721, 512, 512}, // white
{674, 176, 543}, // yellow
{581, 589, 176}, // cyan
{534, 253, 207}, // green
{251, 771, 817}, // magenta
{204, 435, 848}, // red
{111, 848, 481}, // blue
}};
// Pack 6 pixels into 4 x 32-bit V210 words (16 bytes total).
//
// V210 is 4:2:2 — each pair of pixels shares one Cb and one Cr sample.
// The three pairs in a block map to words like this (bits [9:0],[19:10],[29:20]):
// word 0: Cb(pair0) | Y(px0) | Cr(pair0)
// word 1: Y(px1) | Cb(pair1) | Y(px2)
// word 2: Cr(pair1) | Y(px3) | Cb(pair2)
// word 3: Y(px4) | Cr(pair2) | Y(px5)
inline void pack_block(
uint8_t* out,
Color p01, uint16_t y0, uint16_t y1, // pair 01
Color p23, uint16_t y2, uint16_t y3, // pair 23
Color p45, uint16_t y4, uint16_t y5) // pair 45
{
auto* w = reinterpret_cast<uint32_t*>(out);
w[0] = (p01.cb & 0x3FFu) | ((y0 & 0x3FFu) << 10) | ((p01.cr & 0x3FFu) << 20);
w[1] = (y1 & 0x3FFu) | ((p23.cb & 0x3FFu) << 10) | ((y2 & 0x3FFu) << 20);
w[2] = (p23.cr & 0x3FFu) | ((y3 & 0x3FFu) << 10) | ((p45.cb & 0x3FFu) << 20);
w[3] = (y4 & 0x3FFu) | ((p45.cr & 0x3FFu) << 10) | ((y5 & 0x3FFu) << 20);
}
// Write one horizontal line of SMPTE 75% bars.
// `stride` is the line size in bytes as returned by MXL (configInfo.discrete.sliceSizes[0]).
// Bytes beyond the active pixels are already zeroed by the mmap, so no explicit padding needed.
inline void write_bar_line(uint8_t* line, int width, uint32_t /*stride*/)
{
const int n = static_cast<int>(SMPTE_BARS.size());
const int blocks = width / 6; // one V210 block = 6 pixels = 16 bytes
for (int b = 0; b < blocks; b++) {
int x = b * 6;
auto color = [&](int px) -> const Color& {
return SMPTE_BARS[static_cast<size_t>(px * n / width)];
};
const Color& c01 = color(x);
const Color& c23 = color(x + 2);
const Color& c45 = color(x + 4);
pack_block(line + b * 16,
c01, c01.y, color(x+1).y,
c23, c23.y, color(x+3).y,
c45, c45.y, color(x+5).y);
}
}
// Fill an entire frame buffer with SMPTE 75% color bars.
inline void fill_frame(uint8_t* buf, int width, int height, uint32_t stride)
{
for (int y = 0; y < height; y++) {
write_bar_line(buf + static_cast<ptrdiff_t>(y) * stride, width, stride);
}
}
} // namespace dmf::v210