2412 lines
66 KiB
C++
2412 lines
66 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;
|
|
constexpr float TILE_ASPECT = 16.0f / 9.0f;
|
|
|
|
static std::atomic<bool> g_running{true};
|
|
|
|
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 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 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<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,
|
|
const std::vector<FeedConfig>& feedConfigs,
|
|
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 labelOverride =
|
|
i < feedConfigs.size()
|
|
? feedConfigs[i].label
|
|
: "";
|
|
const std::string labelText =
|
|
!labelOverride.empty()
|
|
? labelOverride
|
|
: sourceInfo.empty()
|
|
? feedRuntimeStatusDisplayName(status)
|
|
: sourceInfo;
|
|
const std::string label =
|
|
std::to_string(i + 1) +
|
|
": " +
|
|
labelText;
|
|
|
|
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)
|
|
);
|
|
const ImVec4 labelClip(
|
|
labelMin.x,
|
|
labelMin.y,
|
|
labelMax.x,
|
|
labelMax.y
|
|
);
|
|
drawList->AddText(
|
|
nullptr,
|
|
0.0f,
|
|
textPos,
|
|
tileLabelColor(status),
|
|
label.c_str(),
|
|
nullptr,
|
|
0.0f,
|
|
&labelClip
|
|
);
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
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;
|
|
}
|
|
|
|
if (!config.feeds[i].label.empty())
|
|
{
|
|
std::cout
|
|
<< " label=\"" << config.feeds[i].label << "\"";
|
|
}
|
|
|
|
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::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);
|
|
uint64_t lastUploadPaceTicks = SDL_GetPerformanceCounter();
|
|
|
|
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;
|
|
}
|
|
};
|
|
|
|
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<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(),
|
|
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)
|
|
{
|
|
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)
|
|
{
|
|
++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<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.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<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 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 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
|
|
// ----------------------------------------
|
|
|
|
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;
|
|
}
|