Files
mxl-multiviewer/main.cpp
T
2026-05-21 11:35:46 +03:00

2700 lines
74 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 <condition_variable>
#include <cstring>
#include <deque>
#include <csignal>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <limits>
#include <iostream>
#include <map>
#include <memory>
#include <mutex>
#include <sstream>
#include <thread>
#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;
constexpr float TILE_ASPECT = 16.0f / 9.0f;
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
{
uint64_t frames = 0;
uint64_t feedReadTicks = 0;
uint64_t uploadDecodeTicks = 0;
uint64_t stageCopyTicks = 0;
uint64_t directV210SourceReadNs = 0;
uint64_t directV210PayloadCopyNs = 0;
uint64_t workerV210SourceReadNs = 0;
uint64_t workerV210PayloadCopyNs = 0;
uint64_t uploadRecordTicks = 0;
uint64_t submitPresentTicks = 0;
uint64_t idleDelayTicks = 0;
uint64_t frameTicks = 0;
};
struct FramePerfStats
{
uint64_t eventTicks = 0;
uint64_t eventPumpTicks = 0;
uint64_t eventPollTicks = 0;
uint64_t eventProcessTicks = 0;
uint64_t emptyPollTicks = 0;
uint64_t maxEventPollTicks = 0;
uint64_t maxEventProcessTicks = 0;
uint64_t fenceWaitTicks = 0;
uint64_t feedReadTicks = 0;
uint64_t stageCopyTicks = 0;
uint64_t directV210SourceReadNs = 0;
uint64_t directV210PayloadCopyNs = 0;
uint64_t workerV210SourceReadNs = 0;
uint64_t workerV210PayloadCopyNs = 0;
uint64_t maxStageCopyTicks = 0;
uint64_t maxStageCopyBytes = 0;
uint32_t maxStageCopyFeed = 0;
uint32_t stageCopies = 0;
uint32_t v210StageCopies = 0;
uint64_t uploadRecordTicks = 0;
uint64_t layoutTicks = 0;
uint64_t imguiTicks = 0;
uint64_t drawRecordTicks = 0;
uint64_t acquireTicks = 0;
uint64_t submitPresentTicks = 0;
uint64_t idleDelayTicks = 0;
uint32_t events = 0;
uint32_t windowEvents = 0;
uint32_t resizeEvents = 0;
uint32_t mouseMotionEvents = 0;
uint32_t mouseButtonEvents = 0;
uint32_t mouseWheelEvents = 0;
uint32_t keyboardEvents = 0;
uint32_t textEvents = 0;
uint32_t quitEvents = 0;
uint32_t otherEvents = 0;
uint32_t maxPollEventType = 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
{
uint64_t updates = 0;
uint64_t uploads = 0;
uint64_t deferredUploads = 0;
uint64_t repeats = 0;
uint64_t skippedGrains = 0;
uint64_t lastGrain = 0;
uint64_t lastUploadTicks = 0;
uint64_t uploadIntervalTicks = 0;
uint64_t minUploadIntervalTicks = UINT64_MAX;
uint64_t maxUploadIntervalTicks = 0;
uint64_t uploadIntervals = 0;
bool hasLastGrain = false;
};
static void recordEventStats(
FramePerfStats& framePerf,
const SDL_Event& event)
{
++framePerf.events;
if (event.type >= SDL_EVENT_WINDOW_FIRST &&
event.type <= SDL_EVENT_WINDOW_LAST)
{
++framePerf.windowEvents;
if (event.type == SDL_EVENT_WINDOW_RESIZED ||
event.type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED)
{
++framePerf.resizeEvents;
}
return;
}
switch (event.type)
{
case SDL_EVENT_MOUSE_MOTION:
++framePerf.mouseMotionEvents;
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
++framePerf.mouseButtonEvents;
break;
case SDL_EVENT_MOUSE_WHEEL:
++framePerf.mouseWheelEvents;
break;
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
++framePerf.keyboardEvents;
break;
case SDL_EVENT_TEXT_EDITING:
case SDL_EVENT_TEXT_INPUT:
case SDL_EVENT_TEXT_EDITING_CANDIDATES:
++framePerf.textEvents;
break;
case SDL_EVENT_QUIT:
++framePerf.quitEvents;
break;
default:
++framePerf.otherEvents;
break;
}
}
static void disableUnusedSdlEvents()
{
// Keep SDL's internal state updates, but avoid queuing noisy events
// that the multiviewer does not consume. On Wayland these showed up
// around SDL_PumpEvents() stalls during compositor/window activity.
SDL_SetEventEnabled(SDL_EVENT_WINDOW_EXPOSED, false);
SDL_SetEventEnabled(SDL_EVENT_WINDOW_FOCUS_GAINED, false);
SDL_SetEventEnabled(SDL_EVENT_WINDOW_FOCUS_LOST, false);
SDL_SetEventEnabled(SDL_EVENT_WINDOW_MOUSE_ENTER, false);
SDL_SetEventEnabled(SDL_EVENT_WINDOW_MOUSE_LEAVE, false);
SDL_SetEventEnabled(SDL_EVENT_MOUSE_MOTION, false);
SDL_SetEventEnabled(SDL_EVENT_CLIPBOARD_UPDATE, 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 std::vector<TileRect> tileRects =
makeGridTileRects(
gridCols,
gridRows,
displaySize.x,
displaySize.y,
TILE_GAP_PIXELS,
TILE_ASPECT
);
const float labelH =
std::max(24.0f, ImGui::GetFontSize() + 10.0f);
for (uint32_t i = 0;
i < feeds.size() && i < tileRects.size();
++i)
{
const TileRect& tileRect = tileRects[i];
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(
tileRect.x0,
tileRect.y1 - labelH
);
const ImVec2 labelMax(tileRect.x1, tileRect.y1);
const ImVec2 textPos(
tileRect.x0 + 10.0f,
tileRect.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;
}
disableUnusedSdlEvents();
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);
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(
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)
// ----------------------------------------
std::vector<Vertex> quadVertices =
makeGridLayout(
config.gridCols,
config.gridRows,
static_cast<float>(winWidth),
static_cast<float>(winHeight),
TILE_GAP_PIXELS,
TILE_ASPECT);
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,
feedTextureWidth,
feedTextureHeight,
config.verbose
);
}
// ----------------------------------------
// Feed textures
// ----------------------------------------
VkDeviceSize imageSize =
static_cast<VkDeviceSize>(feedTextureWidth) *
feedTextureHeight * 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,
feedTextureWidth,
feedTextureHeight
);
std::vector<V210ComputeFeed> v210Feeds(feedCount);
std::vector<bool> v210FeedReady(feedCount);
std::vector<uint64_t> uploadedVersions(feedCount);
std::vector<bool> feedUploadNeeded(feedCount);
std::vector<const VideoFrame*> pendingFrames(feedCount);
std::vector<uint64_t> pendingVersions(feedCount);
std::vector<bool> pendingUploadNeeded(feedCount);
std::vector<bool> postFenceDirectV210Read(feedCount);
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();
if (config.v210UploadWorker)
{
v210UploadWorker = std::make_unique<V210UploadWorker>();
}
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();
FramePerfStats framePerf;
uint64_t perfSectionStartTicks = 0;
SDL_Event event;
perfSectionStartTicks =
SDL_GetPerformanceCounter();
{
const uint64_t pumpStartTicks =
SDL_GetPerformanceCounter();
SDL_PumpEvents();
if (config.logPerf)
{
framePerf.eventPumpTicks +=
SDL_GetPerformanceCounter() -
pumpStartTicks;
}
}
constexpr int EventBatchSize = 64;
SDL_Event eventBatch[EventBatchSize];
while (true)
{
const uint64_t pollStartTicks =
SDL_GetPerformanceCounter();
const int eventCount = SDL_PeepEvents(
eventBatch,
EventBatchSize,
SDL_GETEVENT,
SDL_EVENT_FIRST,
SDL_EVENT_LAST
);
if (eventCount <= 0)
{
if (config.logPerf)
{
framePerf.emptyPollTicks +=
SDL_GetPerformanceCounter() -
pollStartTicks;
}
break;
}
if (config.logPerf)
{
const uint64_t pollTicks =
SDL_GetPerformanceCounter() -
pollStartTicks;
framePerf.eventPollTicks += pollTicks;
if (pollTicks > framePerf.maxEventPollTicks)
{
framePerf.maxEventPollTicks = pollTicks;
framePerf.maxPollEventType =
static_cast<uint32_t>(
eventBatch[0].type);
}
}
for (int eventIndex = 0;
eventIndex < eventCount;
++eventIndex)
{
event = eventBatch[eventIndex];
if (config.logPerf)
{
recordEventStats(framePerf, event);
}
const uint64_t processStartTicks =
SDL_GetPerformanceCounter();
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;
}
if (config.logPerf)
{
const uint64_t processTicks =
SDL_GetPerformanceCounter() -
processStartTicks;
framePerf.eventProcessTicks += processTicks;
if (processTicks >
framePerf.maxEventProcessTicks)
{
framePerf.maxEventProcessTicks =
processTicks;
framePerf.maxProcessEventType =
static_cast<uint32_t>(event.type);
}
}
}
}
if (config.logPerf)
{
framePerf.eventTicks +=
SDL_GetPerformanceCounter() -
perfSectionStartTicks;
}
// ---- 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;
}
// ---- read feeds (overlap with GPU) ----
std::fill(
feedUploadNeeded.begin(),
feedUploadNeeded.end(),
false
);
std::fill(
pendingFrames.begin(),
pendingFrames.end(),
nullptr
);
std::fill(
pendingUploadNeeded.begin(),
pendingUploadNeeded.end(),
false
);
std::fill(
postFenceDirectV210Read.begin(),
postFenceDirectV210Read.end(),
false
);
const uint64_t uploadPaceTicks =
SDL_GetPerformanceCounter();
const double uploadPaceDeltaSeconds =
static_cast<double>(
uploadPaceTicks - lastUploadPaceTicks) /
static_cast<double>(perfFrequency);
lastUploadPaceTicks = uploadPaceTicks;
if (config.paceUploads)
{
for (uint32_t i = 0; i < feedCount; ++i)
{
if (!feeds[i]->hasFrameRate())
{
continue;
}
const double rate =
std::min(
feeds[i]->frameRate(),
static_cast<double>(config.fpsCap)
);
feedUploadCredits[i] =
std::min(
2.0,
feedUploadCredits[i] +
rate * uploadPaceDeltaSeconds
);
}
}
auto recordFeedVersionStats =
[&](uint32_t feedIndex, bool uploadNeeded)
{
if (!config.logPerf)
{
return;
}
if (uploadNeeded)
{
++feedPerfStats[feedIndex].updates;
if (feeds[feedIndex]->hasGrainIndex())
{
const uint64_t grain =
feeds[feedIndex]->grainIndex();
if (feedPerfStats[feedIndex].hasLastGrain &&
grain >
feedPerfStats[feedIndex].lastGrain + 1)
{
feedPerfStats[feedIndex].skippedGrains +=
grain -
feedPerfStats[feedIndex].lastGrain -
1;
}
feedPerfStats[feedIndex].lastGrain = grain;
feedPerfStats[feedIndex].hasLastGrain = true;
}
}
else
{
++feedPerfStats[feedIndex].repeats;
}
};
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)
{
if (v210FeedReady[i] &&
feeds[i]->supportsDirectV210Read())
{
postFenceDirectV210Read[i] = true;
continue;
}
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)
{
const uint64_t elapsed =
SDL_GetPerformanceCounter() -
perfSectionStartTicks;
perfStats.feedReadTicks += elapsed;
framePerf.feedReadTicks += elapsed;
recordFeedVersionStats(i, uploadNeeded);
}
if (!uploadNeeded)
{
continue;
}
pendingFrames[i] = &frame;
pendingVersions[i] = version;
pendingUploadNeeded[i] = true;
}
// ---- fence wait (now after CPU work) ----
perfSectionStartTicks =
SDL_GetPerformanceCounter();
vkWaitForFences(
ctx.device(),
1,
&inFlightFence,
VK_TRUE,
UINT64_MAX
);
vkResetFences(
ctx.device(),
1,
&inFlightFence
);
if (config.logPerf)
{
const uint64_t elapsed =
SDL_GetPerformanceCounter() -
perfSectionStartTicks;
perfStats.submitPresentTicks += elapsed;
framePerf.fenceWaitTicks += 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;
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;
}
}
// ---- stage uploads ----
const uint32_t maxV210Uploads =
config.maxV210UploadsPerFrame == 0
? feedCount
: config.maxV210UploadsPerFrame;
uint32_t v210UploadsThisFrame = 0;
const uint32_t uploadStart =
feedCount == 0 ? 0 : frameCounter % feedCount;
for (uint32_t offset = 0; offset < feedCount; ++offset)
{
const uint32_t i =
(uploadStart + offset) % feedCount;
const bool directV210Read =
postFenceDirectV210Read[i];
if (!directV210Read && !pendingUploadNeeded[i])
{
continue;
}
const bool isV210 =
directV210Read || feeds[i]->hasV210();
if (config.paceUploads &&
isV210 &&
feeds[i]->hasFrameRate() &&
feedUploadCredits[i] < 1.0)
{
if (config.logPerf)
{
++feedPerfStats[i].deferredUploads;
}
continue;
}
if (isV210 && v210UploadsThisFrame >= maxV210Uploads)
{
if (config.logPerf)
{
++feedPerfStats[i].deferredUploads;
}
continue;
}
perfSectionStartTicks =
SDL_GetPerformanceCounter();
if (isV210)
{
if (!directV210Read && !v210FeedReady[i])
{
v210Decoder.createFeedResources(
ctx.physicalDevice(),
ctx.commandPool(),
ctx.graphicsQueue(),
i,
feeds[i]->v210Width(),
feeds[i]->v210Height(),
feeds[i]->v210Stride(),
feedTextureWidth,
feedTextureHeight,
config.v210StagingMemoryMode,
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;
}
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{};
const size_t v210Bytes =
static_cast<size_t>(
feeds[i]->v210Stride()
) *
static_cast<size_t>(
feeds[i]->v210Height()
);
const bool copied =
feeds[i]->readV210FrameInto(
frameCounter,
v210Feeds[i].v210MappedData,
v210Bytes,
&directReadTiming
);
const uint64_t version =
feeds[i]->frameVersion();
const bool uploadNeeded =
copied &&
uploadedVersions[i] != version;
recordFeedVersionStats(i, uploadNeeded);
if (config.logPerf)
{
perfStats.directV210SourceReadNs +=
directReadTiming.sourceReadNs;
perfStats.directV210PayloadCopyNs +=
directReadTiming.payloadCopyNs;
framePerf.directV210SourceReadNs +=
directReadTiming.sourceReadNs;
framePerf.directV210PayloadCopyNs +=
directReadTiming.payloadCopyNs;
}
if (!uploadNeeded)
{
continue;
}
pendingVersions[i] = version;
}
else
{
std::memcpy(
v210Feeds[i].v210MappedData,
feeds[i]->v210Data(),
static_cast<size_t>(
feeds[i]->v210Stride()
) * feeds[i]->v210Height()
);
}
}
else
{
copyFrameToFeedTextureStaging(
*pendingFrames[i],
imageSize,
feedTextures[i]
);
}
if (config.logPerf)
{
const uint64_t elapsed =
SDL_GetPerformanceCounter() -
perfSectionStartTicks;
perfStats.stageCopyTicks += elapsed;
perfStats.uploadDecodeTicks += elapsed;
framePerf.stageCopyTicks += elapsed;
++framePerf.stageCopies;
if (isV210)
{
++framePerf.v210StageCopies;
}
if (elapsed > framePerf.maxStageCopyTicks)
{
framePerf.maxStageCopyTicks = elapsed;
framePerf.maxStageCopyFeed = i + 1;
framePerf.maxStageCopyBytes =
isV210
? static_cast<uint64_t>(
feeds[i]->v210Stride()) *
feeds[i]->v210Height()
: static_cast<uint64_t>(imageSize);
}
}
uploadedVersions[i] = pendingVersions[i];
if (isV210)
{
++v210UploadsThisFrame;
if (config.paceUploads &&
feeds[i]->hasFrameRate())
{
feedUploadCredits[i] =
std::max(
0.0,
feedUploadCredits[i] - 1.0
);
}
}
if (config.logPerf)
{
recordFeedUploadStats(i);
}
feedUploadNeeded[i] = true;
}
// ---- update vertex UVs for aspect ratio ----
perfSectionStartTicks =
SDL_GetPerformanceCounter();
VkExtent2D swapExtent = swapchain.extent();
updateGridLayout(
quadVertices.data(),
config.gridCols,
config.gridRows,
static_cast<float>(swapExtent.width),
static_cast<float>(swapExtent.height),
TILE_GAP_PIXELS,
TILE_ASPECT
);
for (uint32_t i = 0; i < feedCount; ++i)
{
float srcAspect = feeds[i]->srcAspectRatio();
updateUvForAspectRatio(
quadVertices.data(),
i,
srcAspect,
TILE_ASPECT);
}
std::memcpy(
vbData,
quadVertices.data(),
static_cast<size_t>(vbSize)
);
if (config.logPerf)
{
framePerf.layoutTicks +=
SDL_GetPerformanceCounter() -
perfSectionStartTicks;
}
// ---- ImGui frame ----
perfSectionStartTicks =
SDL_GetPerformanceCounter();
ImGui_ImplVulkan_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
drawTileLabels(
feeds,
config.gridCols,
config.gridRows
);
ImGui::Render();
ImDrawData* imguiDrawData = ImGui::GetDrawData();
if (config.logPerf)
{
framePerf.imguiTicks +=
SDL_GetPerformanceCounter() -
perfSectionStartTicks;
}
// ---- 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(),
feedTextureWidth,
feedTextureHeight
);
v210Decoder.recordOutputReadyForSampling(
uploadCmdBuf,
v210Feeds[i]
);
}
else
{
recordFeedTextureUpload(
uploadCmdBuf,
feedTextures[i],
feedTextureWidth,
feedTextureHeight
);
}
}
if (vkEndCommandBuffer(uploadCmdBuf) !=
VK_SUCCESS)
{
throw std::runtime_error(
"Failed to record upload cmd buf"
);
}
if (config.logPerf)
{
const uint64_t elapsed =
SDL_GetPerformanceCounter() -
perfSectionStartTicks;
perfStats.uploadRecordTicks += elapsed;
perfStats.uploadDecodeTicks += elapsed;
framePerf.uploadRecordTicks += elapsed;
}
// ---- acquire swapchain image ----
perfSectionStartTicks =
SDL_GetPerformanceCounter();
uint32_t imageIndex;
VkResult acquireResult =
vkAcquireNextImageKHR(
ctx.device(),
swapchain.handle(),
UINT64_MAX,
imageAvailableSem,
VK_NULL_HANDLE,
&imageIndex
);
if (config.logPerf)
{
framePerf.acquireTicks +=
SDL_GetPerformanceCounter() -
perfSectionStartTicks;
}
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"
);
}
perfSectionStartTicks =
SDL_GetPerformanceCounter();
recordDrawCmdBuf(
drawCmdBufs[imageIndex],
imageIndex,
imguiDrawData
);
if (config.logPerf)
{
framePerf.drawRecordTicks +=
SDL_GetPerformanceCounter() -
perfSectionStartTicks;
}
// ---- submit ----
perfSectionStartTicks =
SDL_GetPerformanceCounter();
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)
{
const uint64_t elapsed =
SDL_GetPerformanceCounter() -
perfSectionStartTicks;
perfStats.submitPresentTicks += elapsed;
framePerf.submitPresentTicks += elapsed;
}
const Uint64 frameElapsedTicks =
SDL_GetTicks() - frameStartTicks;
if (frameElapsedTicks < targetFrameMs)
{
perfSectionStartTicks =
SDL_GetPerformanceCounter();
SDL_Delay(
static_cast<Uint32>(
targetFrameMs - frameElapsedTicks
)
);
if (config.logPerf)
{
const uint64_t elapsed =
SDL_GetPerformanceCounter() -
perfSectionStartTicks;
perfStats.idleDelayTicks += elapsed;
framePerf.idleDelayTicks += elapsed;
}
}
if (config.logPerf)
{
++perfStats.frames;
const uint64_t now = SDL_GetPerformanceCounter();
const uint64_t currentFrameTicks =
now - perfFrameStartTicks;
perfStats.frameTicks += currentFrameTicks;
const double invMs =
1000.0 /
static_cast<double>(perfFrequency);
if (currentFrameTicks * invMs > 25.0)
{
const uint64_t accountedTicks =
framePerf.eventTicks +
framePerf.fenceWaitTicks +
framePerf.feedReadTicks +
framePerf.stageCopyTicks +
framePerf.uploadRecordTicks +
framePerf.layoutTicks +
framePerf.imguiTicks +
framePerf.drawRecordTicks +
framePerf.acquireTicks +
framePerf.submitPresentTicks +
framePerf.idleDelayTicks;
const uint64_t otherTicks =
currentFrameTicks > accountedTicks
? currentFrameTicks - accountedTicks
: 0;
std::cout
<< "SPIKE frame="
<< (currentFrameTicks * invMs)
<< "ms event="
<< (framePerf.eventTicks * invMs)
<< "ms events="
<< framePerf.events
<< " pump="
<< (framePerf.eventPumpTicks * invMs)
<< "ms peep="
<< (framePerf.eventPollTicks * invMs)
<< "ms process="
<< (framePerf.eventProcessTicks * invMs)
<< "ms emptyPoll="
<< (framePerf.emptyPollTicks * invMs)
<< "ms maxPoll="
<< (framePerf.maxEventPollTicks * invMs)
<< "ms/type="
<< framePerf.maxPollEventType
<< " maxProcess="
<< (framePerf.maxEventProcessTicks * invMs)
<< "ms/type="
<< framePerf.maxProcessEventType
<< " win="
<< framePerf.windowEvents
<< " resize="
<< framePerf.resizeEvents
<< " motion="
<< framePerf.mouseMotionEvents
<< " button="
<< framePerf.mouseButtonEvents
<< " wheel="
<< framePerf.mouseWheelEvents
<< " key="
<< framePerf.keyboardEvents
<< " text="
<< framePerf.textEvents
<< " quit="
<< framePerf.quitEvents
<< " otherEvents="
<< framePerf.otherEvents
<< " fence="
<< (framePerf.fenceWaitTicks * invMs)
<< "ms feedRead="
<< (framePerf.feedReadTicks * invMs)
<< "ms stageCopy="
<< (framePerf.stageCopyTicks * invMs)
<< "ms v210Read="
<< (static_cast<double>(
framePerf.directV210SourceReadNs) /
1000000.0)
<< "ms v210Copy="
<< (static_cast<double>(
framePerf.directV210PayloadCopyNs) /
1000000.0)
<< "ms v210WorkerRead="
<< (static_cast<double>(
framePerf.workerV210SourceReadNs) /
1000000.0)
<< "ms v210WorkerCopy="
<< (static_cast<double>(
framePerf.workerV210PayloadCopyNs) /
1000000.0)
<< "ms stageCopies="
<< framePerf.stageCopies
<< " v210Copies="
<< framePerf.v210StageCopies
<< " maxCopy=f"
<< framePerf.maxStageCopyFeed
<< ":"
<< (framePerf.maxStageCopyTicks * invMs)
<< "ms/"
<< (static_cast<double>(
framePerf.maxStageCopyBytes) /
(1024.0 * 1024.0))
<< "MiB"
<< " uploadRecord="
<< (framePerf.uploadRecordTicks * invMs)
<< "ms layout="
<< (framePerf.layoutTicks * invMs)
<< "ms imgui="
<< (framePerf.imguiTicks * invMs)
<< "ms drawRecord="
<< (framePerf.drawRecordTicks * invMs)
<< "ms acquire="
<< (framePerf.acquireTicks * invMs)
<< "ms submitPresent="
<< (framePerf.submitPresentTicks * invMs)
<< "ms idleDelay="
<< (framePerf.idleDelayTicks * invMs)
<< "ms other="
<< (otherTicks * invMs)
<< "ms"
<< std::endl;
}
const uint64_t elapsedTicks = now - lastPerfLogTicks;
if (elapsedTicks >= perfFrequency)
{
const double elapsedSeconds =
static_cast<double>(elapsedTicks) /
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 stageCopy="
<< (perfStats.stageCopyTicks * invMs / frames)
<< "ms v210Read="
<< (static_cast<double>(
perfStats.directV210SourceReadNs) /
1000000.0 / frames)
<< "ms v210Copy="
<< (static_cast<double>(
perfStats.directV210PayloadCopyNs) /
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="
<< (perfStats.uploadRecordTicks * 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
<< ",upl=" << feedPerfStats[i].uploads
<< ",def=" << feedPerfStats[i].deferredUploads
<< ",rep=" << feedPerfStats[i].repeats;
if (feedPerfStats[i].hasLastGrain)
{
std::cout
<< ",skip="
<< feedPerfStats[i].skippedGrains
<< ",grain="
<< feedPerfStats[i].lastGrain;
}
}
std::cout << std::endl;
std::cout << "JITTER";
for (uint32_t i = 0; i < feedCount; ++i)
{
std::cout << " f" << (i + 1)
<< ":upl=" << feedPerfStats[i].uploads;
if (feedPerfStats[i].uploadIntervals > 0)
{
const double avgMs =
feedPerfStats[i].uploadIntervalTicks *
invMs /
static_cast<double>(
feedPerfStats[i].uploadIntervals);
const double minMs =
feedPerfStats[i].minUploadIntervalTicks *
invMs;
const double maxMs =
feedPerfStats[i].maxUploadIntervalTicks *
invMs;
std::cout
<< ",avg=" << avgMs
<< "ms,min=" << minMs
<< "ms,max=" << maxMs
<< "ms";
}
if (feeds[i]->hasFrameRate() &&
feeds[i]->frameRate() > 0.0)
{
std::cout
<< ",exp="
<< (1000.0 / feeds[i]->frameRate())
<< "ms";
}
}
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
// ----------------------------------------
if (v210UploadWorker)
{
v210UploadWorker->stop();
}
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;
}