5 Commits

Author SHA1 Message Date
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
12 changed files with 198 additions and 558 deletions
+6 -108
View File
@@ -15,9 +15,6 @@
std::optional<PresentModeConfig> parsePresentMode( std::optional<PresentModeConfig> parsePresentMode(
std::string_view text); std::string_view text);
std::optional<V210StagingMemoryMode> parseV210StagingMemoryMode(
std::string_view text);
namespace namespace
{ {
struct JsonValue struct JsonValue
@@ -577,14 +574,6 @@ bool applyConfigFile(
{ {
config.paceUploads = boolValue; config.paceUploads = boolValue;
} }
if (jsonBool(objectField(*root, "v210UploadWorker"), boolValue))
{
config.v210UploadWorker = boolValue;
}
if (jsonBool(objectField(*root, "adaptiveFeedTexture"), boolValue))
{
config.adaptiveFeedTexture = boolValue;
}
if (const std::string* present = if (const std::string* present =
jsonString(objectField(*root, "present"))) jsonString(objectField(*root, "present")))
@@ -599,20 +588,6 @@ bool applyConfigFile(
config.presentMode = mode.value(); config.presentMode = mode.value();
} }
if (const std::string* memoryMode =
jsonString(objectField(*root, "v210StagingMemory")))
{
const std::optional<V210StagingMemoryMode> mode =
parseV210StagingMemoryMode(*memoryMode);
if (!mode.has_value())
{
error = "v210StagingMemory must be default, cached, "
"or device-local";
return false;
}
config.v210StagingMemoryMode = mode.value();
}
const JsonValue::Array* feeds = const JsonValue::Array* feeds =
jsonArray(objectField(*root, "feeds")); jsonArray(objectField(*root, "feeds"));
if (feeds == nullptr) if (feeds == nullptr)
@@ -685,6 +660,12 @@ bool applyConfigFile(
feed.kind = FeedKind::MxlSdk; feed.kind = FeedKind::MxlSdk;
} }
if (const std::string* label =
jsonString(objectField(*feedObject, "label")))
{
feed.label = *label;
}
config.feeds[i] = std::move(feed); config.feeds[i] = std::move(feed);
} }
@@ -746,27 +727,6 @@ std::optional<PresentModeConfig> parsePresentMode(std::string_view text)
return std::nullopt; return std::nullopt;
} }
std::optional<V210StagingMemoryMode> parseV210StagingMemoryMode(
std::string_view text)
{
if (text == "default")
{
return V210StagingMemoryMode::Default;
}
if (text == "cached")
{
return V210StagingMemoryMode::Cached;
}
if (text == "device-local" || text == "device_local")
{
return V210StagingMemoryMode::DeviceLocal;
}
return std::nullopt;
}
std::string_view feedKindName(FeedKind kind) std::string_view feedKindName(FeedKind kind)
{ {
switch (kind) switch (kind)
@@ -789,23 +749,6 @@ std::string_view feedKindName(FeedKind kind)
} }
} }
std::string_view v210StagingMemoryModeName(
V210StagingMemoryMode mode)
{
switch (mode)
{
case V210StagingMemoryMode::Cached:
return "cached";
case V210StagingMemoryMode::DeviceLocal:
return "device-local";
case V210StagingMemoryMode::Default:
default:
return "default";
}
}
void printUsage(const char* executableName) void printUsage(const char* executableName)
{ {
std::cout std::cout
@@ -817,13 +760,10 @@ void printUsage(const char* executableName)
<< " --perf Log per-frame CPU timing breakdown\n" << " --perf Log per-frame CPU timing breakdown\n"
<< " --verbose Enable detailed startup and SDK logs\n" << " --verbose Enable detailed startup and SDK logs\n"
<< " --pace-uploads Pace v210 uploads by feed frame rate\n" << " --pace-uploads Pace v210 uploads by feed frame rate\n"
<< " --v210-upload-worker Copy direct v210 payloads on a worker thread\n"
<< " --adaptive-feed-texture Size feed textures to grid tile size\n"
<< " --config <path> Load JSON feed configuration\n" << " --config <path> Load JSON feed configuration\n"
<< " --grid <cols>x<rows> Set multiview grid (default 2x2, max 16 feeds)\n" << " --grid <cols>x<rows> Set multiview grid (default 2x2, max 16 feeds)\n"
<< " --fps-cap <fps> Limit render loop FPS (default 60)\n" << " --fps-cap <fps> Limit render loop FPS (default 60)\n"
<< " --max-v210-uploads <n> Limit v210 uploads per frame (0 = unlimited)\n" << " --max-v210-uploads <n> Limit v210 uploads per frame (0 = unlimited)\n"
<< " --v210-staging-memory <mode> Memory: default, cached, device-local\n"
<< " --present <mode> Present mode: fifo, mailbox, immediate (default mailbox)\n" << " --present <mode> Present mode: fifo, mailbox, immediate (default mailbox)\n"
<< " --feed <idx> <kind> Set feed slot kind explicitly\n" << " --feed <idx> <kind> Set feed slot kind explicitly\n"
<< " --domain <idx> <path> Set MXL domain for feed slot\n" << " --domain <idx> <path> Set MXL domain for feed slot\n"
@@ -965,48 +905,6 @@ ConfigParseResult parseAppConfig(
continue; continue;
} }
if (arg == "--v210-upload-worker")
{
result.config.v210UploadWorker = true;
continue;
}
if (arg == "--adaptive-feed-texture")
{
result.config.adaptiveFeedTexture = true;
continue;
}
if (arg == "--v210-staging-memory")
{
if (i + 1 >= argc)
{
std::cerr << "--v210-staging-memory requires <mode>"
<< std::endl;
result.shouldExit = true;
result.exitCode = 1;
return result;
}
const std::optional<V210StagingMemoryMode> mode =
parseV210StagingMemoryMode(argv[i + 1]);
if (!mode.has_value())
{
std::cerr
<< "--v210-staging-memory must be default, cached, "
<< "or device-local"
<< std::endl;
result.shouldExit = true;
result.exitCode = 1;
return result;
}
result.config.v210StagingMemoryMode = mode.value();
++i;
continue;
}
if (arg == "--grid") if (arg == "--grid")
{ {
++i; ++i;
+1 -13
View File
@@ -30,18 +30,12 @@ enum class PresentModeConfig
Immediate Immediate
}; };
enum class V210StagingMemoryMode
{
Default,
Cached,
DeviceLocal
};
struct FeedConfig struct FeedConfig
{ {
FeedKind kind = FeedKind::NoSignal; FeedKind kind = FeedKind::NoSignal;
std::string mxlDomain; std::string mxlDomain;
std::string mxlFlowId; std::string mxlFlowId;
std::string label;
}; };
struct AppConfig struct AppConfig
@@ -50,22 +44,16 @@ struct AppConfig
bool logPerf = false; bool logPerf = false;
bool verbose = false; bool verbose = false;
bool paceUploads = false; bool paceUploads = false;
bool v210UploadWorker = false;
bool adaptiveFeedTexture = false;
uint32_t fpsCap = 60; uint32_t fpsCap = 60;
uint32_t maxV210UploadsPerFrame = 0; uint32_t maxV210UploadsPerFrame = 0;
uint32_t gridCols = DefaultGridCols; uint32_t gridCols = DefaultGridCols;
uint32_t gridRows = DefaultGridRows; uint32_t gridRows = DefaultGridRows;
PresentModeConfig presentMode = PresentModeConfig::Mailbox; PresentModeConfig presentMode = PresentModeConfig::Mailbox;
V210StagingMemoryMode v210StagingMemoryMode =
V210StagingMemoryMode::Default;
std::vector<FeedConfig> feeds; std::vector<FeedConfig> feeds;
}; };
std::optional<FeedKind> parseFeedKind(std::string_view text); std::optional<FeedKind> parseFeedKind(std::string_view text);
std::string_view feedKindName(FeedKind kind); std::string_view feedKindName(FeedKind kind);
std::string_view v210StagingMemoryModeName(
V210StagingMemoryMode mode);
void printUsage(const char* executableName); void printUsage(const char* executableName);
+3 -3
View File
@@ -160,10 +160,10 @@ namespace
{ {
const char* keys[] = const char* keys[] =
{ {
"name", "label",
"flow_name",
"description", "description",
"label" "flow_name",
"name"
}; };
for (const char* key : keys) for (const char* key : keys)
+5 -1
View File
@@ -95,6 +95,8 @@ inline void decodeV210Line(
default: y10 = y0; cb10 = cb0; cr10 = cr0; break; 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 yp = (static_cast<int32_t>(y10) - 64) >> 2;
int32_t cbp = (static_cast<int32_t>(cb10) - 512) >> 2; int32_t cbp = (static_cast<int32_t>(cb10) - 512) >> 2;
int32_t crp = (static_cast<int32_t>(cr10) - 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; 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 yp = (static_cast<int32_t>(y10) - 64) >> 2;
int32_t cbp = (static_cast<int32_t>(cb10) - 512) >> 2; int32_t cbp = (static_cast<int32_t>(cb10) - 512) >> 2;
int32_t crp = (static_cast<int32_t>(cr10) - 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)}); {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
```
+3 -39
View File
@@ -216,7 +216,6 @@ void V210ComputeDecoder::createFeedResources(
uint32_t srcStride, uint32_t srcStride,
uint32_t dstWidth, uint32_t dstWidth,
uint32_t dstHeight, uint32_t dstHeight,
V210StagingMemoryMode stagingMemoryMode,
V210ComputeFeed& feed) V210ComputeFeed& feed)
{ {
feed.srcWidth = srcWidth; feed.srcWidth = srcWidth;
@@ -225,27 +224,15 @@ void V210ComputeDecoder::createFeedResources(
VkDeviceSize v210Size = static_cast<VkDeviceSize>(srcStride) * srcHeight; VkDeviceSize v210Size = static_cast<VkDeviceSize>(srcStride) * srcHeight;
VkMemoryPropertyFlags preferredProperties = 0; createBuffer(
if (stagingMemoryMode == V210StagingMemoryMode::Cached)
{
preferredProperties = VK_MEMORY_PROPERTY_HOST_CACHED_BIT;
}
else if (stagingMemoryMode == V210StagingMemoryMode::DeviceLocal)
{
preferredProperties = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
}
createBufferWithPreferredMemory(
m_device, m_device,
physicalDevice, physicalDevice,
v210Size, v210Size,
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
preferredProperties,
feed.v210Buffer, feed.v210Buffer,
feed.v210BufferMemory, feed.v210BufferMemory);
feed.v210MemoryProperties);
if (vkMapMemory( if (vkMapMemory(
m_device, m_device,
@@ -259,29 +246,6 @@ void V210ComputeDecoder::createFeedResources(
"Failed to map v210 buffer memory"); "Failed to map v210 buffer memory");
} }
std::cout
<< "Feed " << (feedIndex + 1)
<< ": v210 staging memory mode="
<< v210StagingMemoryModeName(stagingMemoryMode)
<< " flags=0x" << std::hex << feed.v210MemoryProperties
<< std::dec << std::endl;
VkFormatProperties outputFormatProperties{};
vkGetPhysicalDeviceFormatProperties(
physicalDevice,
VK_FORMAT_R8G8B8A8_UNORM,
&outputFormatProperties);
const VkFormatFeatureFlags requiredOutputFeatures =
VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT |
VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT;
if ((outputFormatProperties.optimalTilingFeatures &
requiredOutputFeatures) != requiredOutputFeatures)
{
throw std::runtime_error(
"VK_FORMAT_R8G8B8A8_UNORM does not support sampled "
"storage images on this GPU");
}
createImage( createImage(
m_device, m_device,
physicalDevice, physicalDevice,
@@ -366,7 +330,7 @@ void V210ComputeDecoder::createFeedResources(
imageInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL; imageInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL;
imageInfo.imageView = feed.imageView; imageInfo.imageView = feed.imageView;
VkWriteDescriptorSet writes[2]; VkWriteDescriptorSet writes[2]{};
writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[0].dstSet = feed.descriptorSet; writes[0].dstSet = feed.descriptorSet;
-4
View File
@@ -1,7 +1,5 @@
#pragma once #pragma once
#include "AppConfig.hpp"
#include <vulkan/vulkan.h> #include <vulkan/vulkan.h>
#include <cstdint> #include <cstdint>
@@ -12,7 +10,6 @@ struct V210ComputeFeed
VkBuffer v210Buffer = VK_NULL_HANDLE; VkBuffer v210Buffer = VK_NULL_HANDLE;
VkDeviceMemory v210BufferMemory = VK_NULL_HANDLE; VkDeviceMemory v210BufferMemory = VK_NULL_HANDLE;
void* v210MappedData = nullptr; void* v210MappedData = nullptr;
VkMemoryPropertyFlags v210MemoryProperties = 0;
VkImage image = VK_NULL_HANDLE; VkImage image = VK_NULL_HANDLE;
VkDeviceMemory imageMemory = VK_NULL_HANDLE; VkDeviceMemory imageMemory = VK_NULL_HANDLE;
@@ -47,7 +44,6 @@ public:
uint32_t srcStride, uint32_t srcStride,
uint32_t dstWidth, uint32_t dstWidth,
uint32_t dstHeight, uint32_t dstHeight,
V210StagingMemoryMode stagingMemoryMode,
V210ComputeFeed& feed); V210ComputeFeed& feed);
void destroyFeedResources(V210ComputeFeed& feed); void destroyFeedResources(V210ComputeFeed& feed);
+2 -4
View File
@@ -30,10 +30,8 @@ QueueFamilyIndices findQueueFamilies(
for (uint32_t i = 0; i < queueFamilyCount; ++i) for (uint32_t i = 0; i < queueFamilyCount; ++i)
{ {
if ((queueFamilies[i].queueFlags & if (queueFamilies[i].queueFlags &
VK_QUEUE_GRAPHICS_BIT) && VK_QUEUE_GRAPHICS_BIT)
(queueFamilies[i].queueFlags &
VK_QUEUE_COMPUTE_BIT))
{ {
indices.graphicsFamily = i; indices.graphicsFamily = i;
} }
+6 -29
View File
@@ -117,10 +117,7 @@ void createBuffer(
throw std::runtime_error("Failed to allocate buffer memory"); throw std::runtime_error("Failed to allocate buffer memory");
} }
if (vkBindBufferMemory(device, buffer, bufferMemory, 0) != VK_SUCCESS) vkBindBufferMemory(device, buffer, bufferMemory, 0);
{
throw std::runtime_error("Failed to bind buffer memory");
}
} }
void createBufferWithPreferredMemory( void createBufferWithPreferredMemory(
@@ -164,10 +161,7 @@ void createBufferWithPreferredMemory(
throw std::runtime_error("Failed to allocate buffer memory"); throw std::runtime_error("Failed to allocate buffer memory");
} }
if (vkBindBufferMemory(device, buffer, bufferMemory, 0) != VK_SUCCESS) vkBindBufferMemory(device, buffer, bufferMemory, 0);
{
throw std::runtime_error("Failed to bind buffer memory");
}
} }
VkCommandBuffer beginSingleTimeCommands( VkCommandBuffer beginSingleTimeCommands(
@@ -198,29 +192,15 @@ void endSingleTimeCommands(
VkQueue graphicsQueue, VkQueue graphicsQueue,
VkCommandBuffer commandBuffer) VkCommandBuffer commandBuffer)
{ {
if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) vkEndCommandBuffer(commandBuffer);
{
throw std::runtime_error("Failed to end one-time command buffer");
}
VkSubmitInfo submitInfo{}; VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1; submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer; submitInfo.pCommandBuffers = &commandBuffer;
if (vkQueueSubmit( vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
graphicsQueue, vkQueueWaitIdle(graphicsQueue);
1,
&submitInfo,
VK_NULL_HANDLE) != VK_SUCCESS)
{
throw std::runtime_error("Failed to submit one-time command buffer");
}
if (vkQueueWaitIdle(graphicsQueue) != VK_SUCCESS)
{
throw std::runtime_error("Failed to wait for one-time command buffer");
}
vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer); vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
} }
@@ -274,10 +254,7 @@ void createImage(
throw std::runtime_error("Failed to allocate image memory"); throw std::runtime_error("Failed to allocate image memory");
} }
if (vkBindImageMemory(device, image, imageMemory, 0) != VK_SUCCESS) vkBindImageMemory(device, image, imageMemory, 0);
{
throw std::runtime_error("Failed to bind image memory");
}
} }
void recordImageLayoutTransition( void recordImageLayoutTransition(
+4 -2
View File
@@ -7,11 +7,13 @@
"feeds": [ "feeds": [
{ {
"domain": "/tmp/mxl", "domain": "/tmp/mxl",
"flow": "853a9cda-0f7a-4b48-b2df-b3f628440150" "flow": "853a9cda-0f7a-4b48-b2df-b3f628440150",
"label": "Camera 1"
}, },
{ {
"domain": "/tmp/mxl", "domain": "/tmp/mxl",
"flow": "fab60e17-f721-4e12-a7dd-312f5f6e502d" "flow": "fab60e17-f721-4e12-a7dd-312f5f6e502d",
"label": "Camera 2"
}, },
{ {
"kind": "nosignal" "kind": "nosignal"
+66 -354
View File
@@ -18,20 +18,15 @@
#include <algorithm> #include <algorithm>
#include <atomic> #include <atomic>
#include <condition_variable>
#include <cstring> #include <cstring>
#include <deque>
#include <csignal> #include <csignal>
#include <cmath>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <limits> #include <limits>
#include <iostream> #include <iostream>
#include <map> #include <map>
#include <memory> #include <memory>
#include <mutex>
#include <sstream> #include <sstream>
#include <thread>
#include <vector> #include <vector>
#if defined(__linux__) #if defined(__linux__)
@@ -46,50 +41,6 @@ constexpr float TILE_ASPECT = 16.0f / 9.0f;
static std::atomic<bool> g_running{true}; static std::atomic<bool> g_running{true};
struct FeedTextureSize
{
uint32_t width = TEXTURE_WIDTH;
uint32_t height = TEXTURE_HEIGHT;
};
FeedTextureSize chooseFeedTextureSize(
const AppConfig& config,
int windowWidth,
int windowHeight)
{
if (!config.adaptiveFeedTexture)
{
return {};
}
const std::vector<TileRect> rects =
makeGridTileRects(
config.gridCols,
config.gridRows,
static_cast<float>(windowWidth),
static_cast<float>(windowHeight),
TILE_GAP_PIXELS,
TILE_ASPECT);
if (rects.empty())
{
return {};
}
const TileRect& rect = rects.front();
const uint32_t tileWidth =
static_cast<uint32_t>(
std::max(1.0f, std::ceil(rect.x1 - rect.x0)));
const uint32_t tileHeight =
static_cast<uint32_t>(
std::max(1.0f, std::ceil(rect.y1 - rect.y0)));
return {
std::min(TEXTURE_WIDTH, tileWidth),
std::min(TEXTURE_HEIGHT, tileHeight)
};
}
struct PerfStats struct PerfStats
{ {
uint64_t frames = 0; uint64_t frames = 0;
@@ -98,8 +49,6 @@ struct PerfStats
uint64_t stageCopyTicks = 0; uint64_t stageCopyTicks = 0;
uint64_t directV210SourceReadNs = 0; uint64_t directV210SourceReadNs = 0;
uint64_t directV210PayloadCopyNs = 0; uint64_t directV210PayloadCopyNs = 0;
uint64_t workerV210SourceReadNs = 0;
uint64_t workerV210PayloadCopyNs = 0;
uint64_t uploadRecordTicks = 0; uint64_t uploadRecordTicks = 0;
uint64_t submitPresentTicks = 0; uint64_t submitPresentTicks = 0;
uint64_t idleDelayTicks = 0; uint64_t idleDelayTicks = 0;
@@ -120,8 +69,6 @@ struct FramePerfStats
uint64_t stageCopyTicks = 0; uint64_t stageCopyTicks = 0;
uint64_t directV210SourceReadNs = 0; uint64_t directV210SourceReadNs = 0;
uint64_t directV210PayloadCopyNs = 0; uint64_t directV210PayloadCopyNs = 0;
uint64_t workerV210SourceReadNs = 0;
uint64_t workerV210PayloadCopyNs = 0;
uint64_t maxStageCopyTicks = 0; uint64_t maxStageCopyTicks = 0;
uint64_t maxStageCopyBytes = 0; uint64_t maxStageCopyBytes = 0;
uint32_t maxStageCopyFeed = 0; uint32_t maxStageCopyFeed = 0;
@@ -148,129 +95,6 @@ struct FramePerfStats
uint32_t maxProcessEventType = 0; uint32_t maxProcessEventType = 0;
}; };
struct V210UploadJob
{
uint32_t feedIndex = 0;
uint32_t frameCounter = 0;
IVideoFeed* feed = nullptr;
void* destination = nullptr;
size_t destinationSize = 0;
};
struct V210UploadResult
{
uint32_t feedIndex = 0;
bool copied = false;
uint64_t version = 0;
V210ReadTiming timing{};
};
class V210UploadWorker
{
public:
V210UploadWorker()
: mThread(&V210UploadWorker::run, this)
{
}
~V210UploadWorker()
{
stop();
}
V210UploadWorker(const V210UploadWorker&) = delete;
V210UploadWorker& operator=(const V210UploadWorker&) = delete;
void enqueue(V210UploadJob job)
{
{
std::lock_guard<std::mutex> lock(mMutex);
mJobs.push_back(job);
}
mCondition.notify_one();
}
void drainResults(std::vector<V210UploadResult>& results)
{
std::lock_guard<std::mutex> lock(mMutex);
while (!mResults.empty())
{
results.push_back(mResults.front());
mResults.pop_front();
}
}
void stop()
{
{
std::lock_guard<std::mutex> lock(mMutex);
if (mStopping)
{
return;
}
mStopping = true;
}
mCondition.notify_one();
if (mThread.joinable())
{
mThread.join();
}
}
private:
void run()
{
while (true)
{
V210UploadJob job{};
{
std::unique_lock<std::mutex> lock(mMutex);
mCondition.wait(
lock,
[&]
{
return mStopping || !mJobs.empty();
});
if (mStopping && mJobs.empty())
{
return;
}
job = mJobs.front();
mJobs.pop_front();
}
V210UploadResult result{};
result.feedIndex = job.feedIndex;
if (job.feed != nullptr && job.destination != nullptr)
{
result.copied = job.feed->readV210FrameInto(
job.frameCounter,
job.destination,
job.destinationSize,
&result.timing);
if (result.copied)
{
result.version = job.feed->frameVersion();
}
}
{
std::lock_guard<std::mutex> lock(mMutex);
mResults.push_back(result);
}
}
}
std::mutex mMutex;
std::condition_variable mCondition;
std::deque<V210UploadJob> mJobs;
std::deque<V210UploadResult> mResults;
bool mStopping = false;
std::thread mThread;
};
struct FeedPerfStats struct FeedPerfStats
{ {
uint64_t updates = 0; uint64_t updates = 0;
@@ -640,6 +464,7 @@ static ImU32 tileLabelColor(
static void drawTileLabels( static void drawTileLabels(
const std::vector<std::unique_ptr<IVideoFeed>>& feeds, const std::vector<std::unique_ptr<IVideoFeed>>& feeds,
const std::vector<FeedConfig>& feedConfigs,
uint32_t gridCols, uint32_t gridCols,
uint32_t gridRows) uint32_t gridRows)
{ {
@@ -668,12 +493,20 @@ static void drawTileLabels(
feeds[i]->status(); feeds[i]->status();
const std::string sourceInfo = const std::string sourceInfo =
feeds[i]->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 = const std::string label =
std::to_string(i + 1) + std::to_string(i + 1) +
": " + ": " +
(sourceInfo.empty() labelText;
? feedRuntimeStatusDisplayName(status)
: sourceInfo);
const ImVec2 labelMin( const ImVec2 labelMin(
tileRect.x0, tileRect.x0,
@@ -690,10 +523,21 @@ static void drawTileLabels(
labelMax, labelMax,
IM_COL32(0, 0, 0, 175) IM_COL32(0, 0, 0, 175)
); );
const ImVec4 labelClip(
labelMin.x,
labelMin.y,
labelMax.x,
labelMax.y
);
drawList->AddText( drawList->AddText(
nullptr,
0.0f,
textPos, textPos,
tileLabelColor(status), tileLabelColor(status),
label.c_str() label.c_str(),
nullptr,
0.0f,
&labelClip
); );
} }
} }
@@ -794,19 +638,6 @@ int main(int argc, char* argv[])
int winWidth, winHeight; int winWidth, winHeight;
SDL_GetWindowSize(window, &winWidth, &winHeight); SDL_GetWindowSize(window, &winWidth, &winHeight);
const FeedTextureSize feedTextureSize =
chooseFeedTextureSize(config, winWidth, winHeight);
const uint32_t feedTextureWidth = feedTextureSize.width;
const uint32_t feedTextureHeight = feedTextureSize.height;
std::cout
<< "Feed texture size: "
<< feedTextureWidth
<< "x"
<< feedTextureHeight
<< (config.adaptiveFeedTexture ? " adaptive" : " fixed")
<< std::endl;
Swapchain swapchain( Swapchain swapchain(
ctx.device(), ctx.device(),
ctx.physicalDevice(), ctx.physicalDevice(),
@@ -894,13 +725,19 @@ int main(int argc, char* argv[])
<< " flowId=" << config.feeds[i].mxlFlowId; << " flowId=" << config.feeds[i].mxlFlowId;
} }
if (!config.feeds[i].label.empty())
{
std::cout
<< " label=\"" << config.feeds[i].label << "\"";
}
std::cout << std::endl; std::cout << std::endl;
feeds[i] = createFeed( feeds[i] = createFeed(
config.feeds[i], config.feeds[i],
i, i,
feedTextureWidth, TEXTURE_WIDTH,
feedTextureHeight, TEXTURE_HEIGHT,
config.verbose config.verbose
); );
} }
@@ -910,8 +747,7 @@ int main(int argc, char* argv[])
// ---------------------------------------- // ----------------------------------------
VkDeviceSize imageSize = VkDeviceSize imageSize =
static_cast<VkDeviceSize>(feedTextureWidth) * TEXTURE_WIDTH * TEXTURE_HEIGHT * 4;
feedTextureHeight * 4;
std::vector<FeedTexture> feedTextures(feedCount); std::vector<FeedTexture> feedTextures(feedCount);
@@ -1019,8 +855,8 @@ int main(int argc, char* argv[])
v210Decoder.init( v210Decoder.init(
ctx.device(), ctx.device(),
feedCount, feedCount,
feedTextureWidth, TEXTURE_WIDTH,
feedTextureHeight TEXTURE_HEIGHT
); );
std::vector<V210ComputeFeed> v210Feeds(feedCount); std::vector<V210ComputeFeed> v210Feeds(feedCount);
@@ -1032,16 +868,8 @@ int main(int argc, char* argv[])
std::vector<bool> pendingUploadNeeded(feedCount); std::vector<bool> pendingUploadNeeded(feedCount);
std::vector<bool> postFenceDirectV210Read(feedCount); std::vector<bool> postFenceDirectV210Read(feedCount);
std::vector<double> feedUploadCredits(feedCount, 1.0); std::vector<double> feedUploadCredits(feedCount, 1.0);
std::vector<bool> v210WorkerInFlight(feedCount);
std::unique_ptr<V210UploadWorker> v210UploadWorker;
std::vector<V210UploadResult> v210WorkerResults;
uint64_t lastUploadPaceTicks = SDL_GetPerformanceCounter(); uint64_t lastUploadPaceTicks = SDL_GetPerformanceCounter();
if (config.v210UploadWorker)
{
v210UploadWorker = std::make_unique<V210UploadWorker>();
}
std::fill( std::fill(
uploadedVersions.begin(), uploadedVersions.begin(),
uploadedVersions.end(), uploadedVersions.end(),
@@ -1630,40 +1458,6 @@ int main(int argc, char* argv[])
} }
}; };
auto recordFeedUploadStats =
[&](uint32_t feedIndex)
{
if (!config.logPerf)
{
return;
}
++feedPerfStats[feedIndex].uploads;
const uint64_t uploadTicks =
SDL_GetPerformanceCounter();
if (feedPerfStats[feedIndex].lastUploadTicks != 0)
{
const uint64_t interval =
uploadTicks -
feedPerfStats[feedIndex].lastUploadTicks;
feedPerfStats[feedIndex].uploadIntervalTicks +=
interval;
feedPerfStats[feedIndex].minUploadIntervalTicks =
std::min(
feedPerfStats[feedIndex].minUploadIntervalTicks,
interval
);
feedPerfStats[feedIndex].maxUploadIntervalTicks =
std::max(
feedPerfStats[feedIndex].maxUploadIntervalTicks,
interval
);
++feedPerfStats[feedIndex].uploadIntervals;
}
feedPerfStats[feedIndex].lastUploadTicks = uploadTicks;
};
for (uint32_t i = 0; i < feedCount; ++i) for (uint32_t i = 0; i < feedCount; ++i)
{ {
if (v210FeedReady[i] && if (v210FeedReady[i] &&
@@ -1732,54 +1526,6 @@ int main(int argc, char* argv[])
framePerf.submitPresentTicks += elapsed; framePerf.submitPresentTicks += elapsed;
} }
if (config.v210UploadWorker)
{
v210WorkerResults.clear();
v210UploadWorker->drainResults(v210WorkerResults);
for (const V210UploadResult& result : v210WorkerResults)
{
const uint32_t i = result.feedIndex;
if (i >= feedCount)
{
continue;
}
v210WorkerInFlight[i] = false;
if (!result.copied)
{
continue;
}
const bool uploadNeeded =
uploadedVersions[i] != result.version;
recordFeedVersionStats(i, uploadNeeded);
if (!uploadNeeded)
{
continue;
}
pendingVersions[i] = result.version;
uploadedVersions[i] = result.version;
feedUploadNeeded[i] = true;
recordFeedUploadStats(i);
if (config.logPerf)
{
perfStats.workerV210SourceReadNs +=
result.timing.sourceReadNs;
perfStats.workerV210PayloadCopyNs +=
result.timing.payloadCopyNs;
framePerf.workerV210SourceReadNs +=
result.timing.sourceReadNs;
framePerf.workerV210PayloadCopyNs +=
result.timing.payloadCopyNs;
}
}
}
++frameCounter; ++frameCounter;
if (config.logFps) if (config.logFps)
@@ -1865,9 +1611,8 @@ int main(int argc, char* argv[])
feeds[i]->v210Width(), feeds[i]->v210Width(),
feeds[i]->v210Height(), feeds[i]->v210Height(),
feeds[i]->v210Stride(), feeds[i]->v210Stride(),
feedTextureWidth, TEXTURE_WIDTH,
feedTextureHeight, TEXTURE_HEIGHT,
config.v210StagingMemoryMode,
v210Feeds[i] v210Feeds[i]
); );
@@ -1895,42 +1640,6 @@ int main(int argc, char* argv[])
if (directV210Read) if (directV210Read)
{ {
if (config.v210UploadWorker)
{
if (!v210WorkerInFlight[i])
{
const size_t v210Bytes =
static_cast<size_t>(
feeds[i]->v210Stride()
) *
static_cast<size_t>(
feeds[i]->v210Height()
);
v210UploadWorker->enqueue(
V210UploadJob{
i,
frameCounter,
feeds[i].get(),
v210Feeds[i].v210MappedData,
v210Bytes
});
v210WorkerInFlight[i] = true;
++v210UploadsThisFrame;
if (config.paceUploads &&
feeds[i]->hasFrameRate())
{
feedUploadCredits[i] =
std::max(
0.0,
feedUploadCredits[i] - 1.0
);
}
}
continue;
}
V210ReadTiming directReadTiming{}; V210ReadTiming directReadTiming{};
const size_t v210Bytes = const size_t v210Bytes =
static_cast<size_t>( static_cast<size_t>(
@@ -2036,7 +1745,30 @@ int main(int argc, char* argv[])
} }
if (config.logPerf) if (config.logPerf)
{ {
recordFeedUploadStats(i); ++feedPerfStats[i].uploads;
const uint64_t uploadTicks =
SDL_GetPerformanceCounter();
if (feedPerfStats[i].lastUploadTicks != 0)
{
const uint64_t interval =
uploadTicks -
feedPerfStats[i].lastUploadTicks;
feedPerfStats[i].uploadIntervalTicks +=
interval;
feedPerfStats[i].minUploadIntervalTicks =
std::min(
feedPerfStats[i].minUploadIntervalTicks,
interval
);
feedPerfStats[i].maxUploadIntervalTicks =
std::max(
feedPerfStats[i].maxUploadIntervalTicks,
interval
);
++feedPerfStats[i].uploadIntervals;
}
feedPerfStats[i].lastUploadTicks = uploadTicks;
} }
feedUploadNeeded[i] = true; feedUploadNeeded[i] = true;
} }
@@ -2092,6 +1824,7 @@ int main(int argc, char* argv[])
drawTileLabels( drawTileLabels(
feeds, feeds,
config.feeds,
config.gridCols, config.gridCols,
config.gridRows config.gridRows
); );
@@ -2143,8 +1876,8 @@ int main(int argc, char* argv[])
feeds[i]->v210Width(), feeds[i]->v210Width(),
feeds[i]->v210Height(), feeds[i]->v210Height(),
feeds[i]->v210Stride(), feeds[i]->v210Stride(),
feedTextureWidth, TEXTURE_WIDTH,
feedTextureHeight TEXTURE_HEIGHT
); );
v210Decoder.recordOutputReadyForSampling( v210Decoder.recordOutputReadyForSampling(
@@ -2157,8 +1890,8 @@ int main(int argc, char* argv[])
recordFeedTextureUpload( recordFeedTextureUpload(
uploadCmdBuf, uploadCmdBuf,
feedTextures[i], feedTextures[i],
feedTextureWidth, TEXTURE_WIDTH,
feedTextureHeight TEXTURE_HEIGHT
); );
} }
} }
@@ -2437,14 +2170,6 @@ int main(int argc, char* argv[])
<< (static_cast<double>( << (static_cast<double>(
framePerf.directV210PayloadCopyNs) / framePerf.directV210PayloadCopyNs) /
1000000.0) 1000000.0)
<< "ms v210WorkerRead="
<< (static_cast<double>(
framePerf.workerV210SourceReadNs) /
1000000.0)
<< "ms v210WorkerCopy="
<< (static_cast<double>(
framePerf.workerV210PayloadCopyNs) /
1000000.0)
<< "ms stageCopies=" << "ms stageCopies="
<< framePerf.stageCopies << framePerf.stageCopies
<< " v210Copies=" << " v210Copies="
@@ -2504,14 +2229,6 @@ int main(int argc, char* argv[])
<< (static_cast<double>( << (static_cast<double>(
perfStats.directV210PayloadCopyNs) / perfStats.directV210PayloadCopyNs) /
1000000.0 / frames) 1000000.0 / frames)
<< "ms v210WorkerRead="
<< (static_cast<double>(
perfStats.workerV210SourceReadNs) /
1000000.0 / frames)
<< "ms v210WorkerCopy="
<< (static_cast<double>(
perfStats.workerV210PayloadCopyNs) /
1000000.0 / frames)
<< "ms uploadRecord=" << "ms uploadRecord="
<< (perfStats.uploadRecordTicks * invMs / frames) << (perfStats.uploadRecordTicks * invMs / frames)
<< "ms submitPresent=" << "ms submitPresent="
@@ -2610,11 +2327,6 @@ int main(int argc, char* argv[])
// Cleanup // Cleanup
// ---------------------------------------- // ----------------------------------------
if (v210UploadWorker)
{
v210UploadWorker->stop();
}
vkDeviceWaitIdle(ctx.device()); vkDeviceWaitIdle(ctx.device());
ImGui_ImplVulkan_Shutdown(); ImGui_ImplVulkan_Shutdown();
+3 -1
View File
@@ -62,6 +62,8 @@ void main()
default: y10 = y5; cb10 = cb4; cr10 = cr4; break; 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 yp = int(y10) - 64;
int cbp = int(cb10) - 512; int cbp = int(cb10) - 512;
int crp = int(cr10) - 512; int crp = int(cr10) - 512;
@@ -79,4 +81,4 @@ void main()
b = clamp(b, 0, 255); b = clamp(b, 0, 255);
imageStore(dstImage, dstPos, vec4(float(r) / 255.0, float(g) / 255.0, float(b) / 255.0, 1.0)); imageStore(dstImage, dstPos, vec4(float(r) / 255.0, float(g) / 255.0, float(b) / 255.0, 1.0));
} }