6 Commits

Author SHA1 Message Date
Johanness 2ff5f2f90b Probe RGBA16F v210 output 2026-05-24 21:45:38 +03:00
Johanness 102b1fb95f Allow feed labels in config 2026-05-24 21:28:11 +03:00
Johanness 58f9f7edaf Use MXL flow label in tile UI 2026-05-24 21:23:35 +03:00
Johanness f79eeadde6 Document BT.709 v210 conversion 2026-05-24 21:17:44 +03:00
Johanness 22335bb378 Initialize v210 descriptor writes 2026-05-24 20:25:25 +03:00
Johanness f99c6cd2c6 Add build README 2026-05-24 18:13:08 +03:00
9 changed files with 175 additions and 16 deletions
+6
View File
@@ -660,6 +660,12 @@ bool applyConfigFile(
feed.kind = FeedKind::MxlSdk;
}
if (const std::string* label =
jsonString(objectField(*feedObject, "label")))
{
feed.label = *label;
}
config.feeds[i] = std::move(feed);
}
+1
View File
@@ -35,6 +35,7 @@ struct FeedConfig
FeedKind kind = FeedKind::NoSignal;
std::string mxlDomain;
std::string mxlFlowId;
std::string label;
};
struct AppConfig
+3 -3
View File
@@ -160,10 +160,10 @@ namespace
{
const char* keys[] =
{
"name",
"flow_name",
"label",
"description",
"label"
"flow_name",
"name"
};
for (const char* key : keys)
+5 -1
View File
@@ -95,6 +95,8 @@ inline void decodeV210Line(
default: y10 = y0; cb10 = cb0; cr10 = cr0; break;
}
// BT.709 limited-range YCbCr to RGB. v210 stores 10-bit video-range
// samples: Y [64, 940], Cb/Cr centered at 512.
int32_t yp = (static_cast<int32_t>(y10) - 64) >> 2;
int32_t cbp = (static_cast<int32_t>(cb10) - 512) >> 2;
int32_t crp = (static_cast<int32_t>(cr10) - 512) >> 2;
@@ -175,6 +177,8 @@ inline void convertV210ToRgba(
default: y10 = y0; cb10 = cb0; cr10 = cr0; break;
}
// BT.709 limited-range YCbCr to RGB. v210 stores 10-bit video-range
// samples: Y [64, 940], Cb/Cr centered at 512.
int32_t yp = (static_cast<int32_t>(y10) - 64) >> 2;
int32_t cbp = (static_cast<int32_t>(cb10) - 512) >> 2;
int32_t crp = (static_cast<int32_t>(cr10) - 512) >> 2;
@@ -187,4 +191,4 @@ inline void convertV210ToRgba(
{clampUint8(r), clampUint8(g), clampUint8(b)});
}
}
}
}
+99
View File
@@ -0,0 +1,99 @@
# MXL Multiviewer
Small Vulkan/SDL3 multiviewer for test feeds and MXL SDK video flows.
## Dependencies
Install:
- CMake 3.20+
- C++20 compiler
- Vulkan loader and headers
- Vulkan driver for the target GPU
- SDL3 development package with `pkg-config` support
- `glslangValidator`
On Ubuntu/Debian-like systems the packages are usually similar to:
```bash
sudo apt install cmake g++ pkg-config libvulkan-dev vulkan-tools glslang-tools libsdl3-dev
```
Check that the discrete GPU is visible:
```bash
vulkaninfo --summary
```
## Build Without MXL SDK
This builds synthetic feeds, SMPTE bars, and no-signal mode:
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
```
Run a synthetic v210 test grid:
```bash
./build/mxl_multiviewer --perf --grid 4x4 v210 v210 v210 v210 v210 v210 v210 v210 v210
```
## Build With MXL SDK
Set `MXL_SDK_ROOT` to the SDK checkout/root that contains `lib/include` and `build/Linux-Clang-Release/lib`:
```bash
cmake -S . -B build-sdk \
-DCMAKE_BUILD_TYPE=Release \
-DMXL_MULTIVIEWER_ENABLE_MXL_SDK=ON \
-DMXL_SDK_ROOT=/path/to/mxl-sdk
cmake --build build-sdk -j
```
Run with an existing feed config:
```bash
./build-sdk/mxl_multiviewer --perf --grid 4x4 --config /tmp/feeds_config.json
```
Each feed object can include an optional `label` field to override the tile UI text:
```json
{
"domain": "/tmp/mxl",
"flow": "853a9cda-0f7a-4b48-b2df-b3f628440150",
"label": "Camera 1"
}
```
Useful probe flags:
```bash
--v210-upload-worker
--adaptive-feed-texture
--v210-staging-memory default|cached|device-local
```
Example:
```bash
./build-sdk/mxl_multiviewer \
--adaptive-feed-texture \
--v210-upload-worker \
--perf \
--grid 4x4 \
--config /tmp/feeds_config.json
```
## Notes
- Run from the repository root or keep the `shaders/` directory next to the executable path expected by the app.
- If SDL opens on the wrong GPU/session, try running from the desktop session attached to the discrete GPU and confirm with `vulkaninfo --summary`.
- `v210_test` is a CPU-side conversion sanity check:
```bash
./build/v210_test
```
+22 -4
View File
@@ -7,6 +7,9 @@
namespace
{
constexpr VkFormat V210_OUTPUT_FORMAT =
VK_FORMAT_R16G16B16A16_SFLOAT;
std::vector<char> readFile(const std::string& path)
{
std::ifstream file(path, std::ios::ate | std::ios::binary);
@@ -222,6 +225,21 @@ void V210ComputeDecoder::createFeedResources(
feed.srcHeight = srcHeight;
feed.srcStride = srcStride;
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(
physicalDevice,
V210_OUTPUT_FORMAT,
&formatProperties);
const VkFormatFeatureFlags requiredFeatures =
VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT |
VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT;
if ((formatProperties.optimalTilingFeatures & requiredFeatures) !=
requiredFeatures)
{
throw std::runtime_error(
"VK_FORMAT_R16G16B16A16_SFLOAT cannot be used for v210 output");
}
VkDeviceSize v210Size = static_cast<VkDeviceSize>(srcStride) * srcHeight;
createBuffer(
@@ -251,7 +269,7 @@ void V210ComputeDecoder::createFeedResources(
physicalDevice,
dstWidth,
dstHeight,
VK_FORMAT_R8G8B8A8_UNORM,
V210_OUTPUT_FORMAT,
VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_STORAGE_BIT |
VK_IMAGE_USAGE_SAMPLED_BIT,
@@ -264,7 +282,7 @@ void V210ComputeDecoder::createFeedResources(
commandPool,
graphicsQueue,
feed.image,
VK_FORMAT_R8G8B8A8_UNORM,
V210_OUTPUT_FORMAT,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_GENERAL);
@@ -272,7 +290,7 @@ void V210ComputeDecoder::createFeedResources(
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = feed.image;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
viewInfo.format = V210_OUTPUT_FORMAT;
viewInfo.subresourceRange.aspectMask =
VK_IMAGE_ASPECT_COLOR_BIT;
viewInfo.subresourceRange.baseMipLevel = 0;
@@ -330,7 +348,7 @@ void V210ComputeDecoder::createFeedResources(
imageInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL;
imageInfo.imageView = feed.imageView;
VkWriteDescriptorSet writes[2];
VkWriteDescriptorSet writes[2]{};
writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[0].dstSet = feed.descriptorSet;
+4 -2
View File
@@ -7,11 +7,13 @@
"feeds": [
{
"domain": "/tmp/mxl",
"flow": "853a9cda-0f7a-4b48-b2df-b3f628440150"
"flow": "853a9cda-0f7a-4b48-b2df-b3f628440150",
"label": "Camera 1"
},
{
"domain": "/tmp/mxl",
"flow": "fab60e17-f721-4e12-a7dd-312f5f6e502d"
"flow": "fab60e17-f721-4e12-a7dd-312f5f6e502d",
"label": "Camera 2"
},
{
"kind": "nosignal"
+31 -4
View File
@@ -464,6 +464,7 @@ static ImU32 tileLabelColor(
static void drawTileLabels(
const std::vector<std::unique_ptr<IVideoFeed>>& feeds,
const std::vector<FeedConfig>& feedConfigs,
uint32_t gridCols,
uint32_t gridRows)
{
@@ -492,12 +493,20 @@ static void drawTileLabels(
feeds[i]->status();
const std::string sourceInfo =
feeds[i]->sourceInfo();
const std::string labelOverride =
i < feedConfigs.size()
? feedConfigs[i].label
: "";
const std::string labelText =
!labelOverride.empty()
? labelOverride
: sourceInfo.empty()
? feedRuntimeStatusDisplayName(status)
: sourceInfo;
const std::string label =
std::to_string(i + 1) +
": " +
(sourceInfo.empty()
? feedRuntimeStatusDisplayName(status)
: sourceInfo);
labelText;
const ImVec2 labelMin(
tileRect.x0,
@@ -514,10 +523,21 @@ static void drawTileLabels(
labelMax,
IM_COL32(0, 0, 0, 175)
);
const ImVec4 labelClip(
labelMin.x,
labelMin.y,
labelMax.x,
labelMax.y
);
drawList->AddText(
nullptr,
0.0f,
textPos,
tileLabelColor(status),
label.c_str()
label.c_str(),
nullptr,
0.0f,
&labelClip
);
}
}
@@ -705,6 +725,12 @@ int main(int argc, char* argv[])
<< " flowId=" << config.feeds[i].mxlFlowId;
}
if (!config.feeds[i].label.empty())
{
std::cout
<< " label=\"" << config.feeds[i].label << "\"";
}
std::cout << std::endl;
feeds[i] = createFeed(
@@ -1798,6 +1824,7 @@ int main(int argc, char* argv[])
drawTileLabels(
feeds,
config.feeds,
config.gridCols,
config.gridRows
);
+4 -2
View File
@@ -6,7 +6,7 @@ layout(binding = 0) readonly buffer V210Data {
uint data[];
} v210;
layout(binding = 1, rgba8) uniform writeonly image2D dstImage;
layout(binding = 1, rgba16f) uniform writeonly image2D dstImage;
layout(push_constant) uniform PushConstants {
uint srcWidth;
@@ -62,6 +62,8 @@ void main()
default: y10 = y5; cb10 = cb4; cr10 = cr4; break;
}
// BT.709 limited-range YCbCr to RGB. v210 stores 10-bit video-range
// samples: Y [64, 940], Cb/Cr centered at 512.
int yp = int(y10) - 64;
int cbp = int(cb10) - 512;
int crp = int(cr10) - 512;
@@ -79,4 +81,4 @@ void main()
b = clamp(b, 0, 255);
imageStore(dstImage, dstPos, vec4(float(r) / 255.0, float(g) / 255.0, float(b) / 255.0, 1.0));
}
}