Files
mxl-multiviewer/main.cpp
T
2026-05-18 00:03:06 +03:00

1748 lines
44 KiB
C++

#include "AppConfig.hpp"
#include "FeedCreate.hpp"
#include "FeedTexture.hpp"
#include "RenderPipeline.hpp"
#include "Swapchain.hpp"
#include "TileLayout.hpp"
#include "V210ComputeDecoder.hpp"
#include "VulkanContext.hpp"
#include "VulkanUtils.hpp"
#include <imgui.h>
#include <imgui_impl_sdl3.h>
#include <imgui_impl_vulkan.h>
#include <SDL3/SDL.h>
#include <vulkan/vulkan.h>
#include <algorithm>
#include <atomic>
#include <cstring>
#include <csignal>
#include <filesystem>
#include <fstream>
#include <limits>
#include <iostream>
#include <map>
#include <memory>
#include <sstream>
#include <vector>
#if defined(__linux__)
#include <unistd.h>
#endif
constexpr uint32_t TEXTURE_WIDTH = 512;
constexpr uint32_t TEXTURE_HEIGHT = 288;
constexpr uint32_t VERTICES_PER_QUAD = 6;
constexpr float TILE_GAP_PIXELS = 8.0f;
static std::atomic<bool> g_running{true};
struct PerfStats
{
uint64_t frames = 0;
uint64_t feedReadTicks = 0;
uint64_t uploadDecodeTicks = 0;
uint64_t submitPresentTicks = 0;
uint64_t idleDelayTicks = 0;
uint64_t frameTicks = 0;
};
struct FeedPerfStats
{
uint64_t updates = 0;
uint64_t repeats = 0;
uint64_t skippedGrains = 0;
uint64_t lastGrain = 0;
bool hasLastGrain = false;
};
struct ThreadCpuSample
{
uint64_t ticks = 0;
std::string name;
};
using ThreadCpuSamples = std::map<int, ThreadCpuSample>;
static bool readThreadCpuStat(
const std::filesystem::path& statPath,
int tid,
ThreadCpuSample& sample)
{
std::ifstream file(statPath);
if (!file)
{
return false;
}
std::string line;
std::getline(file, line);
const size_t lparen = line.find('(');
const size_t rparen = line.rfind(") ");
if (lparen == std::string::npos ||
rparen == std::string::npos ||
rparen <= lparen)
{
return false;
}
sample.name = line.substr(lparen + 1, rparen - lparen - 1);
std::istringstream fields(line.substr(rparen + 2));
char state = '\0';
fields >> state;
std::string ignored;
for (int i = 0; i < 10; ++i)
{
fields >> ignored;
}
uint64_t utime = 0;
uint64_t stime = 0;
fields >> utime >> stime;
if (!fields)
{
return false;
}
sample.ticks = utime + stime;
(void)tid;
return true;
}
static ThreadCpuSamples collectThreadCpuSamples()
{
ThreadCpuSamples samples;
#if defined(__linux__)
const std::filesystem::path taskDir("/proc/self/task");
std::error_code ec;
for (const auto& entry :
std::filesystem::directory_iterator(taskDir, ec))
{
if (ec)
{
break;
}
const std::string tidText =
entry.path().filename().string();
int tid = 0;
try
{
tid = std::stoi(tidText);
}
catch (...)
{
continue;
}
ThreadCpuSample sample;
if (readThreadCpuStat(entry.path() / "stat", tid, sample))
{
samples[tid] = sample;
}
}
#endif
return samples;
}
static void printThreadCpuPerf(
const ThreadCpuSamples& previous,
const ThreadCpuSamples& current,
double elapsedSeconds)
{
#if defined(__linux__)
const long ticksPerSecond = sysconf(_SC_CLK_TCK);
if (ticksPerSecond <= 0 || elapsedSeconds <= 0.0)
{
return;
}
struct ThreadCpuReport
{
double percent = 0.0;
int tid = 0;
std::string name;
};
std::vector<ThreadCpuReport> reports;
double totalPercent = 0.0;
for (const auto& [tid, sample] : current)
{
auto prevIt = previous.find(tid);
if (prevIt == previous.end() ||
sample.ticks < prevIt->second.ticks)
{
continue;
}
const uint64_t deltaTicks =
sample.ticks - prevIt->second.ticks;
const double percent =
static_cast<double>(deltaTicks) * 100.0 /
(static_cast<double>(ticksPerSecond) *
elapsedSeconds);
totalPercent += percent;
if (percent >= 0.1)
{
reports.push_back({percent, tid, sample.name});
}
}
std::sort(
reports.begin(),
reports.end(),
[](const ThreadCpuReport& a, const ThreadCpuReport& b)
{
return a.percent > b.percent;
});
std::cout << "THREADCPU total=" << totalPercent << "%";
const size_t count = std::min<size_t>(reports.size(), 6);
for (size_t i = 0; i < count; ++i)
{
std::cout
<< " tid=" << reports[i].tid
<< "(" << reports[i].name << ")="
<< reports[i].percent << "%";
}
std::cout << std::endl;
#else
(void)previous;
(void)current;
(void)elapsedSeconds;
#endif
}
static void signalHandler(int)
{
g_running = false;
}
static VkPresentModeKHR toVulkanPresentMode(
PresentModeConfig mode)
{
switch (mode)
{
case PresentModeConfig::Mailbox:
return VK_PRESENT_MODE_MAILBOX_KHR;
case PresentModeConfig::Immediate:
return VK_PRESENT_MODE_IMMEDIATE_KHR;
case PresentModeConfig::Fifo:
default:
return VK_PRESENT_MODE_FIFO_KHR;
}
}
static const char* feedRuntimeStatusName(
FeedRuntimeStatus status)
{
switch (status)
{
case FeedRuntimeStatus::Live:
return "live";
case FeedRuntimeStatus::Bars:
return "bars";
case FeedRuntimeStatus::Placeholder:
return "placeholder";
case FeedRuntimeStatus::Connecting:
return "connecting";
case FeedRuntimeStatus::Reconnecting:
return "reconnecting";
case FeedRuntimeStatus::Error:
return "error";
case FeedRuntimeStatus::NoSignal:
default:
return "nosignal";
}
}
static const char* feedRuntimeStatusDisplayName(
FeedRuntimeStatus status)
{
switch (status)
{
case FeedRuntimeStatus::Live:
return "LIVE";
case FeedRuntimeStatus::Bars:
return "BARS";
case FeedRuntimeStatus::Placeholder:
return "PLACEHOLDER";
case FeedRuntimeStatus::Connecting:
return "CONNECTING";
case FeedRuntimeStatus::Reconnecting:
return "RECONNECTING";
case FeedRuntimeStatus::Error:
return "ERROR";
case FeedRuntimeStatus::NoSignal:
default:
return "NO SIGNAL";
}
}
static ImU32 tileLabelColor(
FeedRuntimeStatus status)
{
switch (status)
{
case FeedRuntimeStatus::Live:
return IM_COL32(70, 235, 115, 255);
case FeedRuntimeStatus::Bars:
return IM_COL32(245, 205, 70, 255);
case FeedRuntimeStatus::Placeholder:
return IM_COL32(115, 165, 255, 255);
case FeedRuntimeStatus::Connecting:
case FeedRuntimeStatus::Reconnecting:
return IM_COL32(255, 165, 70, 255);
case FeedRuntimeStatus::Error:
return IM_COL32(255, 80, 80, 255);
case FeedRuntimeStatus::NoSignal:
default:
return IM_COL32(225, 230, 240, 255);
}
}
static void drawTileLabels(
const std::vector<std::unique_ptr<IVideoFeed>>& feeds,
uint32_t gridCols,
uint32_t gridRows)
{
ImDrawList* drawList = ImGui::GetForegroundDrawList();
const ImVec2 displaySize = ImGui::GetIO().DisplaySize;
const float gapX =
std::min(TILE_GAP_PIXELS, displaySize.x * 0.1f);
const float gapY =
std::min(TILE_GAP_PIXELS, displaySize.y * 0.1f);
const float tileW =
(displaySize.x -
gapX * static_cast<float>(gridCols - 1)) /
static_cast<float>(gridCols);
const float tileH =
(displaySize.y -
gapY * static_cast<float>(gridRows - 1)) /
static_cast<float>(gridRows);
const float labelH =
std::max(24.0f, ImGui::GetFontSize() + 10.0f);
for (uint32_t i = 0; i < feeds.size(); ++i)
{
const uint32_t col = i % gridCols;
const uint32_t row = i / gridCols;
const float x0 =
static_cast<float>(col) * (tileW + gapX);
const float y0 =
static_cast<float>(row) * (tileH + gapY);
const float x1 = x0 + tileW;
const float y1 = y0 + tileH;
const FeedRuntimeStatus status =
feeds[i]->status();
const std::string sourceInfo =
feeds[i]->sourceInfo();
const std::string label =
std::to_string(i + 1) +
": " +
(sourceInfo.empty()
? feedRuntimeStatusDisplayName(status)
: sourceInfo);
const ImVec2 labelMin(x0, y1 - labelH);
const ImVec2 labelMax(x1, y1);
const ImVec2 textPos(x0 + 10.0f, y1 - labelH + 5.0f);
drawList->AddRectFilled(
labelMin,
labelMax,
IM_COL32(0, 0, 0, 175)
);
drawList->AddText(
textPos,
tileLabelColor(status),
label.c_str()
);
}
}
int main(int argc, char* argv[])
{
const ConfigParseResult configResult =
parseAppConfig(argc, argv);
if (configResult.shouldExit)
{
return configResult.exitCode;
}
const AppConfig& config = configResult.config;
const Uint64 targetFrameMs =
std::max<Uint64>(
1,
(1000 + config.fpsCap - 1) / config.fpsCap
);
// ----------------------------------------
// SDL window
// ----------------------------------------
if (!SDL_Init(SDL_INIT_VIDEO))
{
std::cerr << "SDL_Init failed" << std::endl;
return 1;
}
SDL_Window* window = SDL_CreateWindow(
AppName,
1280,
720,
SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE
);
if (!window)
{
std::cerr << "SDL_CreateWindow failed"
<< std::endl;
SDL_Quit();
return 1;
}
// ----------------------------------------
// Vulkan context
// ----------------------------------------
VulkanContext ctx(
window,
AppName,
AppVersionMajor,
AppVersionMinor,
AppVersionPatch
);
// ----------------------------------------
// Choose surface format
// ----------------------------------------
SwapchainSupportDetails support =
querySwapchainSupport(
ctx.physicalDevice(),
ctx.surface()
);
VkSurfaceFormatKHR surfaceFormat =
support.formats[0];
for (const auto& f : support.formats)
{
if (f.format == VK_FORMAT_B8G8R8A8_SRGB &&
f.colorSpace ==
VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
{
surfaceFormat = f;
break;
}
}
// ----------------------------------------
// Render pipeline
// ----------------------------------------
RenderPipeline pipeline(
ctx.device(),
surfaceFormat.format
);
// ----------------------------------------
// Swapchain
// ----------------------------------------
int winWidth, winHeight;
SDL_GetWindowSize(window, &winWidth, &winHeight);
Swapchain swapchain(
ctx.device(),
ctx.physicalDevice(),
ctx.surface(),
pipeline.renderPass(),
toVulkanPresentMode(config.presentMode),
static_cast<uint32_t>(winWidth),
static_cast<uint32_t>(winHeight)
);
// ----------------------------------------
// Vertex buffer (quad tiles)
// ----------------------------------------
const float initialGapX =
2.0f * TILE_GAP_PIXELS /
static_cast<float>(winWidth);
const float initialGapY =
2.0f * TILE_GAP_PIXELS /
static_cast<float>(winHeight);
std::vector<Vertex> quadVertices =
makeGridLayout(
config.gridCols,
config.gridRows,
initialGapX,
initialGapY);
VkDeviceSize vbSize =
sizeof(quadVertices[0]) * quadVertices.size();
VkBuffer vertexBuffer;
VkDeviceMemory vertexBufferMemory;
createBuffer(
ctx.device(),
ctx.physicalDevice(),
vbSize,
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
vertexBuffer,
vertexBufferMemory
);
void* vbData = nullptr;
vkMapMemory(
ctx.device(),
vertexBufferMemory,
0,
vbSize,
0,
&vbData
);
std::memcpy(
vbData,
quadVertices.data(),
static_cast<size_t>(vbSize)
);
// ----------------------------------------
// Feeds
// ----------------------------------------
const uint32_t feedCount =
static_cast<uint32_t>(config.feeds.size());
std::vector<std::unique_ptr<IVideoFeed>> feeds(
feedCount);
std::cout << "Grid: " << config.gridCols
<< "x" << config.gridRows << std::endl;
std::cout << "Feeds:" << std::endl;
for (uint32_t i = 0; i < feedCount; ++i)
{
std::cout
<< " Feed " << (i + 1)
<< ": " << feedKindName(config.feeds[i].kind);
if (!config.feeds[i].mxlDomain.empty())
{
std::cout
<< " domain=" << config.feeds[i].mxlDomain;
}
if (!config.feeds[i].mxlFlowId.empty())
{
std::cout
<< " flowId=" << config.feeds[i].mxlFlowId;
}
std::cout << std::endl;
feeds[i] = createFeed(
config.feeds[i],
i,
TEXTURE_WIDTH,
TEXTURE_HEIGHT,
config.verbose
);
}
// ----------------------------------------
// Feed textures
// ----------------------------------------
VkDeviceSize imageSize =
TEXTURE_WIDTH * TEXTURE_HEIGHT * 4;
std::vector<FeedTexture> feedTextures(feedCount);
for (uint32_t i = 0; i < feedCount; ++i)
{
const VideoFrame& initialFrame =
feeds[i]->getFrame(0);
createFeedTexture(
ctx.device(),
ctx.physicalDevice(),
ctx.commandPool(),
ctx.graphicsQueue(),
initialFrame,
imageSize,
feedTextures[i]
);
}
if (config.verbose)
{
std::cout << "Feed textures created" << std::endl;
}
// ----------------------------------------
// Descriptor pool & sets
// ----------------------------------------
VkDescriptorPoolSize poolSize{};
poolSize.type =
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
poolSize.descriptorCount = feedCount;
VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType =
VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = 1;
poolInfo.pPoolSizes = &poolSize;
poolInfo.maxSets = feedCount;
VkDescriptorPool descriptorPool;
if (vkCreateDescriptorPool(
ctx.device(),
&poolInfo,
nullptr,
&descriptorPool) != VK_SUCCESS)
{
throw std::runtime_error(
"Failed to create descriptor pool"
);
}
std::vector<VkDescriptorSetLayout> dsl(feedCount);
std::fill(
dsl.begin(),
dsl.end(),
pipeline.descriptorSetLayout()
);
VkDescriptorSetAllocateInfo descAlloc{};
descAlloc.sType =
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
descAlloc.descriptorPool = descriptorPool;
descAlloc.descriptorSetCount = feedCount;
descAlloc.pSetLayouts = dsl.data();
std::vector<VkDescriptorSet> descriptorSets(feedCount);
if (vkAllocateDescriptorSets(
ctx.device(),
&descAlloc,
descriptorSets.data()) != VK_SUCCESS)
{
throw std::runtime_error(
"Failed to allocate descriptor sets"
);
}
for (uint32_t i = 0; i < feedCount; ++i)
{
feedTextures[i].descriptorSet =
descriptorSets[i];
}
for (uint32_t i = 0; i < feedCount; ++i)
{
updateFeedDescriptorSet(
ctx.device(),
feedTextures[i]
);
}
if (config.verbose)
{
std::cout << "Descriptor sets created"
<< std::endl;
}
// ----------------------------------------
// v210 compute decoder
// ----------------------------------------
V210ComputeDecoder v210Decoder;
v210Decoder.init(
ctx.device(),
feedCount,
TEXTURE_WIDTH,
TEXTURE_HEIGHT
);
std::vector<V210ComputeFeed> v210Feeds(feedCount);
std::vector<bool> v210FeedReady(feedCount);
std::vector<uint64_t> uploadedVersions(feedCount);
std::vector<bool> feedUploadNeeded(feedCount);
std::fill(
uploadedVersions.begin(),
uploadedVersions.end(),
std::numeric_limits<uint64_t>::max()
);
// ----------------------------------------
// Draw command buffers
// ----------------------------------------
auto recordDrawCmdBuf =
[&](VkCommandBuffer cmd,
uint32_t swapchainImageIndex,
ImDrawData* imguiDrawData)
{
vkResetCommandBuffer(cmd, 0);
VkCommandBufferBeginInfo bi{};
bi.sType =
VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
vkBeginCommandBuffer(cmd, &bi);
VkClearValue cc =
{{{0.0f, 0.0f, 0.0f, 1.0f}}};
VkRenderPassBeginInfo rp{};
rp.sType =
VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
rp.renderPass = pipeline.renderPass();
rp.framebuffer =
swapchain.framebuffers()[swapchainImageIndex];
rp.renderArea.offset = {0, 0};
rp.renderArea.extent =
swapchain.extent();
rp.clearValueCount = 1;
rp.pClearValues = &cc;
vkCmdBeginRenderPass(
cmd,
&rp,
VK_SUBPASS_CONTENTS_INLINE
);
vkCmdBindPipeline(
cmd,
VK_PIPELINE_BIND_POINT_GRAPHICS,
pipeline.graphicsPipeline()
);
VkViewport vp{};
vp.x = 0.0f;
vp.y = 0.0f;
vp.width =
static_cast<float>(
swapchain.extent().width
);
vp.height =
static_cast<float>(
swapchain.extent().height
);
vp.minDepth = 0.0f;
vp.maxDepth = 1.0f;
vkCmdSetViewport(cmd, 0, 1, &vp);
VkRect2D sc{};
sc.offset = {0, 0};
sc.extent = swapchain.extent();
vkCmdSetScissor(cmd, 0, 1, &sc);
VkBuffer vb[] = { vertexBuffer };
VkDeviceSize vo[] = { 0 };
vkCmdBindVertexBuffers(
cmd, 0, 1, vb, vo
);
for (uint32_t f = 0; f < feedCount; ++f)
{
vkCmdBindDescriptorSets(
cmd,
VK_PIPELINE_BIND_POINT_GRAPHICS,
pipeline.pipelineLayout(),
0,
1,
&descriptorSets[f],
0,
nullptr
);
vkCmdDraw(
cmd,
VERTICES_PER_QUAD,
1,
f * VERTICES_PER_QUAD,
0
);
}
ImGui_ImplVulkan_RenderDrawData(
imguiDrawData,
cmd
);
vkCmdEndRenderPass(cmd);
if (vkEndCommandBuffer(cmd) !=
VK_SUCCESS)
{
throw std::runtime_error(
"Failed to record command buffer"
);
}
};
std::vector<VkCommandBuffer> drawCmdBufs(
swapchain.imageCount()
);
VkCommandBufferAllocateInfo cmdAlloc{};
cmdAlloc.sType =
VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
cmdAlloc.commandPool = ctx.commandPool();
cmdAlloc.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
cmdAlloc.commandBufferCount =
static_cast<uint32_t>(drawCmdBufs.size());
if (vkAllocateCommandBuffers(
ctx.device(),
&cmdAlloc,
drawCmdBufs.data()) != VK_SUCCESS)
{
throw std::runtime_error(
"Failed to allocate draw command buffers"
);
}
if (config.verbose)
{
std::cout << "Draw command buffers allocated"
<< std::endl;
}
// ----------------------------------------
// Upload command buffer
// ----------------------------------------
VkCommandBufferAllocateInfo uploadAlloc{};
uploadAlloc.sType =
VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
uploadAlloc.commandPool = ctx.commandPool();
uploadAlloc.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
uploadAlloc.commandBufferCount = 1;
VkCommandBuffer uploadCmdBuf;
if (vkAllocateCommandBuffers(
ctx.device(),
&uploadAlloc,
&uploadCmdBuf) != VK_SUCCESS)
{
throw std::runtime_error(
"Failed to allocate upload command buffer"
);
}
// ----------------------------------------
// Sync objects
// ----------------------------------------
VkSemaphoreCreateInfo semInfo{};
semInfo.sType =
VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
VkSemaphore imageAvailableSem;
VkSemaphore renderFinishedSem;
vkCreateSemaphore(
ctx.device(),
&semInfo,
nullptr,
&imageAvailableSem
);
vkCreateSemaphore(
ctx.device(),
&semInfo,
nullptr,
&renderFinishedSem
);
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType =
VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
VkFence inFlightFence;
vkCreateFence(
ctx.device(),
&fenceInfo,
nullptr,
&inFlightFence
);
if (config.verbose)
{
std::cout << "Sync objects created" << std::endl;
}
// ----------------------------------------
// Dear ImGui
// ----------------------------------------
VkDescriptorPoolSize imguiPoolSizes[] =
{
{
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
64
}
};
VkDescriptorPoolCreateInfo imguiPoolInfo{};
imguiPoolInfo.sType =
VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
imguiPoolInfo.flags =
VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
imguiPoolInfo.maxSets = 64;
imguiPoolInfo.poolSizeCount = 1;
imguiPoolInfo.pPoolSizes = imguiPoolSizes;
VkDescriptorPool imguiDescriptorPool =
VK_NULL_HANDLE;
if (vkCreateDescriptorPool(
ctx.device(),
&imguiPoolInfo,
nullptr,
&imguiDescriptorPool) != VK_SUCCESS)
{
throw std::runtime_error(
"Failed to create ImGui descriptor pool"
);
}
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& imguiIo = ImGui::GetIO();
imguiIo.ConfigFlags |=
ImGuiConfigFlags_NavEnableKeyboard;
ImGui::StyleColorsDark();
ImGui_ImplSDL3_InitForVulkan(window);
ImGui_ImplVulkan_InitInfo imguiInitInfo{};
imguiInitInfo.ApiVersion = VK_API_VERSION_1_2;
imguiInitInfo.Instance = ctx.instance();
imguiInitInfo.PhysicalDevice = ctx.physicalDevice();
imguiInitInfo.Device = ctx.device();
imguiInitInfo.QueueFamily =
ctx.queueIndices().graphicsFamily.value();
imguiInitInfo.Queue = ctx.graphicsQueue();
imguiInitInfo.DescriptorPool =
imguiDescriptorPool;
imguiInitInfo.MinImageCount = 2;
imguiInitInfo.ImageCount = swapchain.imageCount();
imguiInitInfo.PipelineInfoMain.RenderPass =
pipeline.renderPass();
imguiInitInfo.PipelineInfoMain.Subpass = 0;
imguiInitInfo.PipelineInfoMain.MSAASamples =
VK_SAMPLE_COUNT_1_BIT;
if (!ImGui_ImplVulkan_Init(&imguiInitInfo))
{
throw std::runtime_error(
"Failed to initialize ImGui Vulkan backend"
);
}
if (config.verbose)
{
std::cout << "Dear ImGui initialized" << std::endl;
}
// ----------------------------------------
// Main loop
// ----------------------------------------
uint32_t frameCounter = 0;
uint32_t framesSinceLastLog = 0;
Uint64 lastFpsLogTicks = SDL_GetTicks();
const uint64_t perfFrequency = SDL_GetPerformanceFrequency();
uint64_t lastPerfLogTicks = SDL_GetPerformanceCounter();
PerfStats perfStats;
std::vector<FeedPerfStats> feedPerfStats(feedCount);
ThreadCpuSamples previousThreadCpuSamples =
collectThreadCpuSamples();
bool framebufferResized = false;
std::signal(SIGINT, signalHandler);
std::signal(SIGTERM, signalHandler);
while (g_running)
{
const Uint64 frameStartTicks = SDL_GetTicks();
const uint64_t perfFrameStartTicks =
SDL_GetPerformanceCounter();
SDL_Event event;
while (SDL_PollEvent(&event))
{
ImGui_ImplSDL3_ProcessEvent(&event);
switch (event.type)
{
case SDL_EVENT_QUIT:
g_running = false;
break;
case SDL_EVENT_WINDOW_RESIZED:
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
framebufferResized = true;
break;
case SDL_EVENT_KEY_DOWN:
if (event.key.key == SDLK_ESCAPE)
{
g_running = false;
}
break;
}
}
// ---- resize handling ----
if (framebufferResized)
{
if (config.verbose)
{
std::cout
<< "Window resized, recreating swapchain"
<< std::endl;
}
vkDeviceWaitIdle(ctx.device());
int nw, nh;
SDL_GetWindowSize(window, &nw, &nh);
while (nw == 0 || nh == 0)
{
SDL_GetWindowSize(
window, &nw, &nh
);
SDL_WaitEvent(nullptr);
}
// Free old draw command buffers
vkFreeCommandBuffers(
ctx.device(),
ctx.commandPool(),
static_cast<uint32_t>(
drawCmdBufs.size()
),
drawCmdBufs.data()
);
swapchain.recreate(
pipeline.renderPass(),
static_cast<uint32_t>(nw),
static_cast<uint32_t>(nh)
);
ImGui_ImplVulkan_SetMinImageCount(
2
);
// Allocate and record new draw command buffers
drawCmdBufs.resize(
swapchain.imageCount()
);
cmdAlloc.commandBufferCount =
static_cast<uint32_t>(
drawCmdBufs.size()
);
if (vkAllocateCommandBuffers(
ctx.device(),
&cmdAlloc,
drawCmdBufs.data()) != VK_SUCCESS)
{
throw std::runtime_error(
"Failed to allocate cmd bufs "
"during resize"
);
}
framebufferResized = false;
continue;
}
// ---- frame timing ----
uint64_t perfSectionStartTicks =
SDL_GetPerformanceCounter();
vkWaitForFences(
ctx.device(),
1,
&inFlightFence,
VK_TRUE,
UINT64_MAX
);
vkResetFences(
ctx.device(),
1,
&inFlightFence
);
if (config.logPerf)
{
perfStats.submitPresentTicks +=
SDL_GetPerformanceCounter() -
perfSectionStartTicks;
}
++frameCounter;
if (config.logFps)
{
++framesSinceLastLog;
Uint64 now = SDL_GetTicks();
Uint64 elapsed = now - lastFpsLogTicks;
if (elapsed >= 1000)
{
double fps =
static_cast<double>(
framesSinceLastLog
) * 1000.0 /
static_cast<double>(elapsed);
std::cout << "FPS: " << fps
<< std::endl;
framesSinceLastLog = 0;
lastFpsLogTicks = now;
}
}
// ---- update feed pixels ----
std::fill(
feedUploadNeeded.begin(),
feedUploadNeeded.end(),
false
);
for (uint32_t i = 0; i < feedCount; ++i)
{
perfSectionStartTicks =
SDL_GetPerformanceCounter();
const VideoFrame& frame =
feeds[i]->getFrame(frameCounter);
const uint64_t version =
feeds[i]->frameVersion();
const bool uploadNeeded =
uploadedVersions[i] != version;
if (config.logPerf)
{
perfStats.feedReadTicks +=
SDL_GetPerformanceCounter() -
perfSectionStartTicks;
if (uploadNeeded)
{
++feedPerfStats[i].updates;
if (feeds[i]->hasGrainIndex())
{
const uint64_t grain =
feeds[i]->grainIndex();
if (feedPerfStats[i].hasLastGrain &&
grain >
feedPerfStats[i].lastGrain + 1)
{
feedPerfStats[i].skippedGrains +=
grain -
feedPerfStats[i].lastGrain -
1;
}
feedPerfStats[i].lastGrain = grain;
feedPerfStats[i].hasLastGrain = true;
}
}
else
{
++feedPerfStats[i].repeats;
}
}
if (!uploadNeeded)
{
continue;
}
perfSectionStartTicks =
SDL_GetPerformanceCounter();
if (feeds[i]->hasV210())
{
if (!v210FeedReady[i])
{
v210Decoder.createFeedResources(
ctx.physicalDevice(),
ctx.commandPool(),
ctx.graphicsQueue(),
i,
feeds[i]->v210Width(),
feeds[i]->v210Height(),
feeds[i]->v210Stride(),
TEXTURE_WIDTH,
TEXTURE_HEIGHT,
v210Feeds[i]
);
updateSampledImageDescriptorSet(
ctx.device(),
descriptorSets[i],
v210Feeds[i].imageView,
v210Feeds[i].sampler,
VK_IMAGE_LAYOUT_GENERAL
);
v210FeedReady[i] = true;
std::cout
<< "Feed "
<< (i + 1)
<< ": enabled GPU v210 decode "
<< feeds[i]->v210Width()
<< "x"
<< feeds[i]->v210Height()
<< " stride="
<< feeds[i]->v210Stride()
<< std::endl;
}
std::memcpy(
v210Feeds[i].v210MappedData,
feeds[i]->v210Data(),
static_cast<size_t>(
feeds[i]->v210Stride()
) * feeds[i]->v210Height()
);
}
else
{
copyFrameToFeedTextureStaging(
frame,
imageSize,
feedTextures[i]
);
}
if (config.logPerf)
{
perfStats.uploadDecodeTicks +=
SDL_GetPerformanceCounter() -
perfSectionStartTicks;
}
uploadedVersions[i] = version;
feedUploadNeeded[i] = true;
}
// ---- update vertex UVs for aspect ratio ----
VkExtent2D swapExtent = swapchain.extent();
const float gapX =
2.0f * TILE_GAP_PIXELS /
static_cast<float>(swapExtent.width);
const float gapY =
2.0f * TILE_GAP_PIXELS /
static_cast<float>(swapExtent.height);
updateGridLayout(
quadVertices.data(),
config.gridCols,
config.gridRows,
gapX,
gapY
);
const float tileWidthPx =
(static_cast<float>(swapExtent.width) -
TILE_GAP_PIXELS *
static_cast<float>(config.gridCols - 1)) /
static_cast<float>(config.gridCols);
const float tileHeightPx =
(static_cast<float>(swapExtent.height) -
TILE_GAP_PIXELS *
static_cast<float>(config.gridRows - 1)) /
static_cast<float>(config.gridRows);
const float tileAspect =
tileWidthPx / tileHeightPx;
for (uint32_t i = 0; i < feedCount; ++i)
{
float srcAspect = feeds[i]->srcAspectRatio();
updateUvForAspectRatio(
quadVertices.data(),
i,
srcAspect,
tileAspect);
}
std::memcpy(
vbData,
quadVertices.data(),
static_cast<size_t>(vbSize)
);
// ---- ImGui frame ----
ImGui_ImplVulkan_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
drawTileLabels(
feeds,
config.gridCols,
config.gridRows
);
ImGui::Render();
ImDrawData* imguiDrawData = ImGui::GetDrawData();
// ---- record upload commands ----
perfSectionStartTicks =
SDL_GetPerformanceCounter();
vkResetCommandBuffer(uploadCmdBuf, 0);
VkCommandBufferBeginInfo upBegin{};
upBegin.sType =
VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
upBegin.flags =
VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
if (vkBeginCommandBuffer(
uploadCmdBuf,
&upBegin) != VK_SUCCESS)
{
throw std::runtime_error(
"Failed to begin upload cmd buf"
);
}
for (uint32_t i = 0; i < feedCount; ++i)
{
if (!feedUploadNeeded[i])
{
continue;
}
if (v210FeedReady[i] && feeds[i]->hasV210())
{
v210Decoder.recordDecode(
uploadCmdBuf,
i,
feeds[i]->v210Width(),
feeds[i]->v210Height(),
feeds[i]->v210Stride(),
TEXTURE_WIDTH,
TEXTURE_HEIGHT
);
v210Decoder.recordOutputReadyForSampling(
uploadCmdBuf,
v210Feeds[i]
);
}
else
{
recordFeedTextureUpload(
uploadCmdBuf,
feedTextures[i],
TEXTURE_WIDTH,
TEXTURE_HEIGHT
);
}
}
if (vkEndCommandBuffer(uploadCmdBuf) !=
VK_SUCCESS)
{
throw std::runtime_error(
"Failed to record upload cmd buf"
);
}
if (config.logPerf)
{
perfStats.uploadDecodeTicks +=
SDL_GetPerformanceCounter() -
perfSectionStartTicks;
}
// ---- acquire swapchain image ----
perfSectionStartTicks =
SDL_GetPerformanceCounter();
uint32_t imageIndex;
VkResult acquireResult =
vkAcquireNextImageKHR(
ctx.device(),
swapchain.handle(),
UINT64_MAX,
imageAvailableSem,
VK_NULL_HANDLE,
&imageIndex
);
if (acquireResult == VK_ERROR_OUT_OF_DATE_KHR)
{
std::cout
<< "Swapchain out of date "
"during acquire"
<< std::endl;
framebufferResized = true;
continue;
}
if (acquireResult != VK_SUCCESS &&
acquireResult != VK_SUBOPTIMAL_KHR)
{
throw std::runtime_error(
"Failed to acquire swapchain image"
);
}
recordDrawCmdBuf(
drawCmdBufs[imageIndex],
imageIndex,
imguiDrawData
);
// ---- submit ----
VkPipelineStageFlags waitStage =
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
VkSubmitInfo submitInfo{};
submitInfo.sType =
VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores =
&imageAvailableSem;
submitInfo.pWaitDstStageMask = &waitStage;
VkCommandBuffer submitCmds[] =
{
uploadCmdBuf,
drawCmdBufs[imageIndex]
};
submitInfo.commandBufferCount = 2;
submitInfo.pCommandBuffers = submitCmds;
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores =
&renderFinishedSem;
if (vkQueueSubmit(
ctx.graphicsQueue(),
1,
&submitInfo,
inFlightFence) != VK_SUCCESS)
{
throw std::runtime_error(
"Failed to submit frame"
);
}
// ---- present ----
VkSwapchainKHR presentSwapchain =
swapchain.handle();
VkPresentInfoKHR presentInfo{};
presentInfo.sType =
VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores =
&renderFinishedSem;
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains =
&presentSwapchain;
presentInfo.pImageIndices = &imageIndex;
VkResult presentResult =
vkQueuePresentKHR(
ctx.presentQueue(),
&presentInfo
);
if (presentResult ==
VK_ERROR_OUT_OF_DATE_KHR ||
presentResult == VK_SUBOPTIMAL_KHR)
{
std::cout
<< "Swapchain out of date "
"during present"
<< std::endl;
framebufferResized = true;
}
else if (presentResult != VK_SUCCESS)
{
throw std::runtime_error(
"Failed to present swapchain image"
);
}
if (config.logPerf)
{
perfStats.submitPresentTicks +=
SDL_GetPerformanceCounter() -
perfSectionStartTicks;
}
const Uint64 frameElapsedTicks =
SDL_GetTicks() - frameStartTicks;
if (frameElapsedTicks < targetFrameMs)
{
perfSectionStartTicks =
SDL_GetPerformanceCounter();
SDL_Delay(
static_cast<Uint32>(
targetFrameMs - frameElapsedTicks
)
);
if (config.logPerf)
{
perfStats.idleDelayTicks +=
SDL_GetPerformanceCounter() -
perfSectionStartTicks;
}
}
if (config.logPerf)
{
++perfStats.frames;
const uint64_t now = SDL_GetPerformanceCounter();
perfStats.frameTicks += now - perfFrameStartTicks;
const uint64_t elapsedTicks = now - lastPerfLogTicks;
if (elapsedTicks >= perfFrequency)
{
const double elapsedSeconds =
static_cast<double>(elapsedTicks) /
static_cast<double>(perfFrequency);
const double invMs =
1000.0 /
static_cast<double>(perfFrequency);
const double frames =
static_cast<double>(perfStats.frames);
std::cout
<< "PERF frames=" << perfStats.frames
<< " feedRead="
<< (perfStats.feedReadTicks * invMs / frames)
<< "ms uploadDecode="
<< (perfStats.uploadDecodeTicks * invMs / frames)
<< "ms submitPresent="
<< (perfStats.submitPresentTicks * invMs / frames)
<< "ms idleDelay="
<< (perfStats.idleDelayTicks * invMs / frames)
<< "ms frame="
<< (perfStats.frameTicks * invMs / frames)
<< "ms"
<< std::endl;
std::cout << "FEEDPERF";
for (uint32_t i = 0; i < feedCount; ++i)
{
std::cout
<< " f" << (i + 1)
<< ":status="
<< feedRuntimeStatusName(
feeds[i]->status())
<< ":upd=" << feedPerfStats[i].updates
<< ",rep=" << feedPerfStats[i].repeats;
if (feedPerfStats[i].hasLastGrain)
{
std::cout
<< ",skip="
<< feedPerfStats[i].skippedGrains
<< ",grain="
<< feedPerfStats[i].lastGrain;
}
}
std::cout << std::endl;
ThreadCpuSamples currentThreadCpuSamples =
collectThreadCpuSamples();
printThreadCpuPerf(
previousThreadCpuSamples,
currentThreadCpuSamples,
elapsedSeconds
);
previousThreadCpuSamples =
std::move(currentThreadCpuSamples);
perfStats = {};
std::fill(
feedPerfStats.begin(),
feedPerfStats.end(),
FeedPerfStats{}
);
lastPerfLogTicks = now;
}
}
}
// ----------------------------------------
// Cleanup
// ----------------------------------------
vkDeviceWaitIdle(ctx.device());
ImGui_ImplVulkan_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext();
vkFreeCommandBuffers(
ctx.device(),
ctx.commandPool(),
static_cast<uint32_t>(drawCmdBufs.size()),
drawCmdBufs.data()
);
vkFreeCommandBuffers(
ctx.device(),
ctx.commandPool(),
1,
&uploadCmdBuf
);
vkDestroyFence(ctx.device(), inFlightFence, nullptr);
vkDestroySemaphore(
ctx.device(),
renderFinishedSem,
nullptr
);
vkDestroySemaphore(
ctx.device(),
imageAvailableSem,
nullptr
);
vkUnmapMemory(ctx.device(), vertexBufferMemory);
vkDestroyBuffer(
ctx.device(),
vertexBuffer,
nullptr
);
vkFreeMemory(
ctx.device(),
vertexBufferMemory,
nullptr
);
vkDestroyDescriptorPool(
ctx.device(),
descriptorPool,
nullptr
);
vkDestroyDescriptorPool(
ctx.device(),
imguiDescriptorPool,
nullptr
);
for (uint32_t i = 0; i < feedCount; ++i)
{
if (v210FeedReady[i])
{
v210Decoder.destroyFeedResources(
v210Feeds[i]
);
}
destroyFeedTexture(
ctx.device(),
feedTextures[i]
);
}
v210Decoder.destroy();
swapchain.release();
pipeline.release();
ctx.release();
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}