#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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(__linux__) #include #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 g_running{true}; struct PerfStats { uint64_t frames = 0; uint64_t feedReadTicks = 0; uint64_t uploadDecodeTicks = 0; uint64_t stageCopyTicks = 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 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 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; 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 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(deltaTicks) * 100.0 / (static_cast(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(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>& feeds, uint32_t gridCols, uint32_t gridRows) { ImDrawList* drawList = ImGui::GetForegroundDrawList(); const ImVec2 displaySize = ImGui::GetIO().DisplaySize; const std::vector 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( 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); Swapchain swapchain( ctx.device(), ctx.physicalDevice(), ctx.surface(), pipeline.renderPass(), toVulkanPresentMode(config.presentMode), static_cast(winWidth), static_cast(winHeight) ); // ---------------------------------------- // Vertex buffer (quad tiles) // ---------------------------------------- std::vector quadVertices = makeGridLayout( config.gridCols, config.gridRows, static_cast(winWidth), static_cast(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(vbSize) ); // ---------------------------------------- // Feeds // ---------------------------------------- const uint32_t feedCount = static_cast(config.feeds.size()); std::vector> 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 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 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 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 v210Feeds(feedCount); std::vector v210FeedReady(feedCount); std::vector uploadedVersions(feedCount); std::vector feedUploadNeeded(feedCount); std::vector pendingFrames(feedCount); std::vector pendingVersions(feedCount); std::vector pendingUploadNeeded(feedCount); std::vector postFenceDirectV210Read(feedCount); std::vector feedUploadCredits(feedCount, 1.0); uint64_t lastUploadPaceTicks = SDL_GetPerformanceCounter(); std::fill( uploadedVersions.begin(), uploadedVersions.end(), std::numeric_limits::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( swapchain.extent().width ); vp.height = static_cast( 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 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(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(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( 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(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( drawCmdBufs.size() ), drawCmdBufs.data() ); swapchain.recreate( pipeline.renderPass(), static_cast(nw), static_cast(nh) ); ImGui_ImplVulkan_SetMinImageCount( 2 ); // Allocate and record new draw command buffers drawCmdBufs.resize( swapchain.imageCount() ); cmdAlloc.commandBufferCount = static_cast( 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( uploadPaceTicks - lastUploadPaceTicks) / static_cast(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(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; } }; 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; } ++frameCounter; if (config.logFps) { ++framesSinceLastLog; Uint64 now = SDL_GetTicks(); Uint64 elapsed = now - lastFpsLogTicks; if (elapsed >= 1000) { double fps = static_cast( framesSinceLastLog ) * 1000.0 / static_cast(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(), 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; } if (directV210Read) { const size_t v210Bytes = static_cast( feeds[i]->v210Stride() ) * static_cast( feeds[i]->v210Height() ); const bool copied = feeds[i]->readV210FrameInto( frameCounter, v210Feeds[i].v210MappedData, v210Bytes ); const uint64_t version = feeds[i]->frameVersion(); const bool uploadNeeded = copied && uploadedVersions[i] != version; recordFeedVersionStats(i, uploadNeeded); if (!uploadNeeded) { continue; } pendingVersions[i] = version; } else { std::memcpy( v210Feeds[i].v210MappedData, feeds[i]->v210Data(), static_cast( 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( feeds[i]->v210Stride()) * feeds[i]->v210Height() : static_cast(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) { ++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; } // ---- update vertex UVs for aspect ratio ---- perfSectionStartTicks = SDL_GetPerformanceCounter(); VkExtent2D swapExtent = swapchain.extent(); updateGridLayout( quadVertices.data(), config.gridCols, config.gridRows, static_cast(swapExtent.width), static_cast(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(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(), 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) { 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( 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(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 stageCopies=" << framePerf.stageCopies << " v210Copies=" << framePerf.v210StageCopies << " maxCopy=f" << framePerf.maxStageCopyFeed << ":" << (framePerf.maxStageCopyTicks * invMs) << "ms/" << (static_cast( 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(elapsedTicks) / static_cast(perfFrequency); const double frames = static_cast(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 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( 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 // ---------------------------------------- vkDeviceWaitIdle(ctx.device()); ImGui_ImplVulkan_Shutdown(); ImGui_ImplSDL3_Shutdown(); ImGui::DestroyContext(); vkFreeCommandBuffers( ctx.device(), ctx.commandPool(), static_cast(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; }