mirror of
https://github.com/thinkonmay/sunshine-sdk.git
synced 2026-08-06 15:54:48 +00:00
debug test
This commit is contained in:
parent
71b561b95e
commit
1038743072
@ -42,7 +42,7 @@ nvenc::nvenc_two_pass twopass_from_view(const std::string_view &preset) {
|
||||
if (preset == "full_res")
|
||||
return nvenc::nvenc_two_pass::full_resolution;
|
||||
BOOST_LOG(warning) << "config: unknown nvenc_twopass value: " << preset;
|
||||
return nvenc::nvenc_two_pass::quarter_resolution;
|
||||
return nvenc::nvenc_two_pass::disabled;
|
||||
}
|
||||
|
||||
} // namespace nv
|
||||
|
||||
52
src/main.cpp
52
src/main.cpp
@ -21,6 +21,7 @@
|
||||
#include "globals.h"
|
||||
#include "interprocess.h"
|
||||
#include "logging.h"
|
||||
#include "output_debug.h"
|
||||
#include "platform/common.h"
|
||||
#include "video.h"
|
||||
|
||||
@ -33,7 +34,17 @@ enum QueueType {
|
||||
Video,
|
||||
Audio,
|
||||
};
|
||||
enum EventType { Pointer, Bitrate, Framerate, Idr, Hdr, Stop, BufferOverflow, Resolution, EventMax };
|
||||
enum EventType {
|
||||
Pointer,
|
||||
Bitrate,
|
||||
Framerate,
|
||||
Idr,
|
||||
Hdr,
|
||||
Stop,
|
||||
BufferOverflow,
|
||||
Resolution,
|
||||
EventMax
|
||||
};
|
||||
|
||||
using namespace std::literals;
|
||||
using namespace std::chrono_literals;
|
||||
@ -97,6 +108,7 @@ int main(int argc, char *argv[]) {
|
||||
MediaMemory *memory = NULL;
|
||||
IVSHMEM *ivshmem = NULL;
|
||||
SharedMemory *shm = NULL;
|
||||
bool debug_output_timing = false;
|
||||
|
||||
std::string ivshmem_path;
|
||||
std::string shm_name;
|
||||
@ -133,6 +145,8 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
if (memory == NULL) {
|
||||
BOOST_LOG(info) << "IPC shared memory not available, using mockup memory block"sv;
|
||||
BOOST_LOG(info) << "Output packet timing debug mode enabled"sv;
|
||||
debug_output_timing = true;
|
||||
memory = (MediaMemory *)calloc(1, sizeof(MediaMemory));
|
||||
}
|
||||
|
||||
@ -228,7 +242,7 @@ int main(int argc, char *argv[]) {
|
||||
if (buffer[1] == 0)
|
||||
break;
|
||||
|
||||
int new_bitrate = buffer[1] * 1000; // kbps
|
||||
int new_bitrate = buffer[1] * 1000; // kbps
|
||||
if (std::abs(new_bitrate - cached_bitrate) >= 1000) { // only change if delta >= 1 Mbps
|
||||
cached_bitrate = new_bitrate;
|
||||
bitrate->raise(new_bitrate);
|
||||
@ -264,19 +278,33 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
};
|
||||
|
||||
auto push_video = [process_shutdown_event](safe::mail_t mail, MediaQueue *queue) {
|
||||
auto push_video = [process_shutdown_event, debug_output_timing](safe::mail_t mail, MediaQueue *queue) {
|
||||
auto video_packets = mail->queue<video::packet_t>(mail::video_packets);
|
||||
auto audio_packets = mail->queue<audio::packet_t>(mail::audio_packets);
|
||||
auto local_shutdown = mail->event<bool>(mail::shutdown);
|
||||
|
||||
platf::adjust_thread_priority(platf::thread_priority_e::critical);
|
||||
|
||||
auto last_output_packet = steady_clock::now();
|
||||
output_debug::timing_t output_timing{debug_output_timing};
|
||||
auto check_output_timeout = [&]() {
|
||||
if (steady_clock::now() - last_output_packet < 10s) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOST_LOG(error) << "No video output packet written for 10 seconds; shutting down"sv;
|
||||
local_shutdown->raise(true);
|
||||
return true;
|
||||
};
|
||||
|
||||
while (!process_shutdown_event->peek() && !local_shutdown->peek()) {
|
||||
do {
|
||||
uint8_t flags = 0;
|
||||
auto packet = video_packets->pop();
|
||||
if (!packet)
|
||||
auto packet = video_packets->pop(100ms);
|
||||
if (!packet) {
|
||||
check_output_timeout();
|
||||
break;
|
||||
}
|
||||
|
||||
auto findex = packet->frame_index();
|
||||
std::string_view payload{(char *)packet->data(), packet->data_size()};
|
||||
@ -298,6 +326,17 @@ int main(int argc, char *argv[]) {
|
||||
if (packet->after_ref_frame_invalidation)
|
||||
flags |= (1 << 1);
|
||||
|
||||
constexpr auto header_size = sizeof(uint64_t) + sizeof(uint64_t) + sizeof(uint8_t);
|
||||
if (payload.size() > MEDIA_PACKET_SIZE - header_size) {
|
||||
BOOST_LOG(error) << "Dropping oversized video packet: " << payload.size()
|
||||
<< " bytes exceeds shared memory payload capacity "
|
||||
<< (MEDIA_PACKET_SIZE - header_size);
|
||||
if (check_output_timeout()) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
auto updated = queue->inindex + 1;
|
||||
if (updated >= IN_QUEUE_SIZE)
|
||||
updated = 0;
|
||||
@ -308,6 +347,9 @@ int main(int argc, char *argv[]) {
|
||||
copy_to_packet(&queue->incoming[queue->inindex], &flags, sizeof(uint8_t));
|
||||
copy_to_packet(&queue->incoming[queue->inindex], (void *)payload.data(), payload.size());
|
||||
queue->inindex = updated;
|
||||
last_output_packet = steady_clock::now();
|
||||
output_timing.record(findex, header_size + payload.size(), packet->is_idr(),
|
||||
packet->encode_duration_us.value_or(0));
|
||||
} while (video_packets->peek());
|
||||
}
|
||||
|
||||
|
||||
@ -107,7 +107,7 @@ bool nvenc_base::create_encoder(const nvenc_config &config, const video::config_
|
||||
encoder_params.height = client_config.height;
|
||||
encoder_params.buffer_format = buffer_format;
|
||||
encoder_params.rfi = true;
|
||||
encoder_params.intra_refresh = client_config.enableIntraRefresh;
|
||||
encoder_params.intra_refresh = false;
|
||||
|
||||
NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS session_params = {
|
||||
min_struct_version(NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER)};
|
||||
@ -215,8 +215,7 @@ bool nvenc_base::create_encoder(const nvenc_config &config, const video::config_
|
||||
init_params.tuningInfo = NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY;
|
||||
init_params.enablePTD = 1;
|
||||
init_params.enableEncodeAsync = async_event_handle ? 1 : 0;
|
||||
init_params.enableWeightedPrediction =
|
||||
config.weighted_prediction && get_encoder_cap(NV_ENC_CAPS_SUPPORT_WEIGHTED_PREDICTION);
|
||||
init_params.enableWeightedPrediction = 0;
|
||||
|
||||
init_params.encodeWidth = encoder_params.width;
|
||||
init_params.darWidth = encoder_params.width;
|
||||
@ -238,17 +237,13 @@ bool nvenc_base::create_encoder(const nvenc_config &config, const video::config_
|
||||
enc_config.profileGUID = NV_ENC_CODEC_PROFILE_AUTOSELECT_GUID;
|
||||
enc_config.gopLength = NVENC_INFINITE_GOPLENGTH;
|
||||
enc_config.frameIntervalP = 1;
|
||||
enc_config.rcParams.enableAQ = config.adaptive_quantization;
|
||||
enc_config.rcParams.enableAQ = 0;
|
||||
enc_config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_CBR;
|
||||
enc_config.rcParams.zeroReorderDelay = 1;
|
||||
enc_config.rcParams.enableLookahead = 0;
|
||||
enc_config.rcParams.lowDelayKeyFrameScale = 1;
|
||||
enc_config.rcParams.multiPass =
|
||||
config.two_pass == nvenc_two_pass::quarter_resolution ? NV_ENC_TWO_PASS_QUARTER_RESOLUTION
|
||||
: config.two_pass == nvenc_two_pass::full_resolution ? NV_ENC_TWO_PASS_FULL_RESOLUTION
|
||||
: NV_ENC_MULTI_PASS_DISABLED;
|
||||
enc_config.rcParams.multiPass = NV_ENC_MULTI_PASS_DISABLED;
|
||||
|
||||
enc_config.rcParams.enableAQ = config.adaptive_quantization;
|
||||
enc_config.rcParams.averageBitRate = client_config.bitrate * 1000;
|
||||
|
||||
if (get_encoder_cap(NV_ENC_CAPS_SUPPORT_CUSTOM_VBV_BUF_SIZE)) {
|
||||
@ -268,12 +263,6 @@ bool nvenc_base::create_encoder(const nvenc_config &config, const video::config_
|
||||
format_config.chromaFormatIDC = 3;
|
||||
}
|
||||
format_config.enableFillerDataInsertion = config.insert_filler_data;
|
||||
|
||||
if (client_config.enableIntraRefresh) {
|
||||
format_config.enableIntraRefresh = 1;
|
||||
format_config.intraRefreshPeriod = std::max(1, (client_config.framerate * 200) / 1000);
|
||||
format_config.intraRefreshCnt = format_config.intraRefreshPeriod - 1;
|
||||
}
|
||||
};
|
||||
|
||||
auto set_ref_frames = [&](uint32_t &ref_frames_option, NV_ENC_NUM_REF_FRAMES &L0_option,
|
||||
@ -353,12 +342,6 @@ bool nvenc_base::create_encoder(const nvenc_config &config, const video::config_
|
||||
format_config.chromaFormatIDC = 1; // YUV444 not supported by NVENC yet
|
||||
format_config.enableBitstreamPadding = config.insert_filler_data;
|
||||
|
||||
if (client_config.enableIntraRefresh) {
|
||||
format_config.enableIntraRefresh = 1;
|
||||
format_config.intraRefreshPeriod = std::max(1, (client_config.framerate * 200) / 1000);
|
||||
format_config.intraRefreshCnt = format_config.intraRefreshPeriod - 1;
|
||||
}
|
||||
|
||||
if (buffer_is_10bit()) {
|
||||
format_config.inputPixelBitDepthMinus8 = 2;
|
||||
format_config.pixelBitDepthMinus8 = 2;
|
||||
|
||||
@ -19,7 +19,7 @@ struct nvenc_config {
|
||||
|
||||
// Use optional preliminary pass for better motion vectors, bitrate distribution and stricter
|
||||
// VBV(HRD), uses CUDA cores
|
||||
nvenc_two_pass two_pass = nvenc_two_pass::quarter_resolution;
|
||||
nvenc_two_pass two_pass = nvenc_two_pass::disabled;
|
||||
|
||||
// Percentage increase of VBV/HRD from the default single frame, allows low-latency variable
|
||||
// bitrate
|
||||
|
||||
277
src/output_debug.cpp
Normal file
277
src/output_debug.cpp
Normal file
@ -0,0 +1,277 @@
|
||||
#include "output_debug.h"
|
||||
|
||||
#include "logging.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace output_debug {
|
||||
namespace {
|
||||
constexpr size_t graph_width = 60;
|
||||
constexpr int bar_width = 28;
|
||||
|
||||
const char *reset = "\x1b[0m";
|
||||
const char *dim = "\x1b[2m";
|
||||
const char *bold = "\x1b[1m";
|
||||
const char *blue = "\x1b[38;5;39m";
|
||||
const char *green = "\x1b[38;5;82m";
|
||||
const char *yellow = "\x1b[38;5;220m";
|
||||
const char *orange = "\x1b[38;5;208m";
|
||||
const char *red = "\x1b[38;5;196m";
|
||||
const char *cyan = "\x1b[38;5;51m";
|
||||
|
||||
template <class Value> double max_of(const std::vector<timing_t::sample_t> &history, Value value) {
|
||||
double result = 1;
|
||||
for (const auto &sample : history) {
|
||||
result = std::max(result, value(sample));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template <class Value>
|
||||
std::string sparkline(const std::vector<timing_t::sample_t> &history, Value value, double max_value) {
|
||||
static constexpr std::array<char, 8> levels{'.', ':', '-', '=', '+', '*', '#', '@'};
|
||||
const auto begin = history.size() > graph_width ? history.end() - graph_width : history.begin();
|
||||
std::ostringstream out;
|
||||
for (auto it = begin; it != history.end(); ++it) {
|
||||
auto normalized = std::clamp(value(*it) / max_value, 0.0, 1.0);
|
||||
auto index = std::min<size_t>(levels.size() - 1, static_cast<size_t>(normalized * levels.size()));
|
||||
out << levels[index];
|
||||
}
|
||||
return out.str();
|
||||
}
|
||||
|
||||
std::string bar(double value, double max_value, const char *color) {
|
||||
const auto filled = static_cast<int>(std::clamp(value / max_value, 0.0, 1.0) * bar_width);
|
||||
std::ostringstream out;
|
||||
out << color << std::string(filled, '=') << reset << dim << std::string(bar_width - filled, '-')
|
||||
<< reset;
|
||||
return out.str();
|
||||
}
|
||||
|
||||
std::string idr_markers(const std::vector<timing_t::sample_t> &history) {
|
||||
const auto begin = history.size() > graph_width ? history.end() - graph_width : history.begin();
|
||||
std::ostringstream out;
|
||||
for (auto it = begin; it != history.end(); ++it) {
|
||||
out << (it->last_idr ? '^' : ' ');
|
||||
}
|
||||
return out.str();
|
||||
}
|
||||
|
||||
const char *jitter_color(const timing_t::sample_t &sample) {
|
||||
if (sample.avg_interval_ms <= 0) {
|
||||
return dim;
|
||||
}
|
||||
const auto ratio = sample.jitter_ms / sample.avg_interval_ms;
|
||||
if (sample.jitter_ms < 1.0 || ratio < 0.05) {
|
||||
return green;
|
||||
}
|
||||
if (ratio < 0.15) {
|
||||
return yellow;
|
||||
}
|
||||
if (ratio < 0.35) {
|
||||
return orange;
|
||||
}
|
||||
return red;
|
||||
}
|
||||
|
||||
std::string jitter_insight(const timing_t::sample_t &sample) {
|
||||
if (sample.avg_interval_ms <= 0) {
|
||||
return "waiting for enough packet intervals to measure jitter";
|
||||
}
|
||||
|
||||
const auto ratio = sample.jitter_ms / sample.avg_interval_ms;
|
||||
if (sample.jitter_ms < 1.0 || ratio < 0.05) {
|
||||
return "stable pacing: jitter is low relative to the average packet interval";
|
||||
}
|
||||
if (ratio < 0.15) {
|
||||
return "minor pacing variance: watch max-interval spikes if stutter appears";
|
||||
}
|
||||
if (ratio < 0.35) {
|
||||
return "noticeable jitter: cadence is uneven enough to correlate with frame pacing issues";
|
||||
}
|
||||
return "high jitter: output timing is unstable and can explain FPS drops or stutter";
|
||||
}
|
||||
|
||||
std::string quality_label(const timing_t::sample_t &sample) {
|
||||
const auto color = jitter_color(sample);
|
||||
if (color == green) {
|
||||
return std::string{green} + "GOOD" + reset;
|
||||
}
|
||||
if (color == yellow) {
|
||||
return std::string{yellow} + "OK" + reset;
|
||||
}
|
||||
if (color == orange) {
|
||||
return std::string{orange} + "JITTERY" + reset;
|
||||
}
|
||||
if (color == red) {
|
||||
return std::string{red} + "BAD" + reset;
|
||||
}
|
||||
return std::string{dim} + "WARMING" + reset;
|
||||
}
|
||||
|
||||
std::string clear_previous_render(size_t lines) {
|
||||
if (lines == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ostringstream out;
|
||||
out << "\x1b[" << lines << "A";
|
||||
for (size_t i = 0; i < lines; ++i) {
|
||||
out << "\x1b[2K";
|
||||
if (i + 1 < lines) {
|
||||
out << "\x1b[1B";
|
||||
}
|
||||
}
|
||||
out << "\x1b[" << (lines - 1) << "A" << '\r';
|
||||
return out.str();
|
||||
}
|
||||
|
||||
void metric_row(std::ostringstream &out, const char *name, double value, const char *unit,
|
||||
double max_value, const char *color, int precision = 2) {
|
||||
out << " " << std::left << std::setw(15) << name << reset << std::right << std::setw(11)
|
||||
<< std::fixed << std::setprecision(precision) << value << ' ' << std::setw(4) << std::left
|
||||
<< unit << " " << bar(value, max_value, color) << reset << '\n';
|
||||
}
|
||||
} // namespace
|
||||
|
||||
timing_t::timing_t(bool enabled)
|
||||
: enabled{enabled}, start_time{std::chrono::steady_clock::now()}, window_start{start_time},
|
||||
last_packet{start_time} {
|
||||
if (this->enabled) {
|
||||
BOOST_LOG(info) << "Output packet timing terminal graph enabled";
|
||||
}
|
||||
}
|
||||
|
||||
void timing_t::record(int64_t frame_index, size_t packet_size, bool idr_frame, double encode_duration_us) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto interval_ms = std::chrono::duration<double, std::milli>(now - last_packet).count();
|
||||
last_packet = now;
|
||||
packets++;
|
||||
bytes += packet_size;
|
||||
encode_total_us += encode_duration_us;
|
||||
encode_max_us = std::max(encode_max_us, encode_duration_us);
|
||||
|
||||
if (packets > 1) {
|
||||
interval_total_ms += interval_ms;
|
||||
interval_squared_total_ms += interval_ms * interval_ms;
|
||||
if (interval_min_ms == 0 || interval_ms < interval_min_ms) {
|
||||
interval_min_ms = interval_ms;
|
||||
}
|
||||
if (interval_ms > interval_max_ms) {
|
||||
interval_max_ms = interval_ms;
|
||||
}
|
||||
}
|
||||
|
||||
auto elapsed = now - window_start;
|
||||
if (elapsed < 1s) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto elapsed_ms = std::chrono::duration<double, std::milli>(elapsed).count();
|
||||
auto interval_count = packets > 1 ? packets - 1 : 0;
|
||||
auto avg_interval_ms = interval_count > 0 ? interval_total_ms / interval_count : 0;
|
||||
auto jitter_ms = 0.0;
|
||||
if (interval_count > 0) {
|
||||
auto variance = interval_squared_total_ms / interval_count - avg_interval_ms * avg_interval_ms;
|
||||
jitter_ms = std::sqrt(std::max(0.0, variance));
|
||||
}
|
||||
sample_t sample{
|
||||
std::chrono::duration<double>(now - start_time).count(),
|
||||
packets * 1000.0 / elapsed_ms,
|
||||
avg_interval_ms,
|
||||
interval_min_ms,
|
||||
interval_max_ms,
|
||||
jitter_ms,
|
||||
packets > 0 ? encode_total_us / packets : 0,
|
||||
encode_max_us,
|
||||
packets,
|
||||
bytes,
|
||||
frame_index,
|
||||
packet_size,
|
||||
idr_frame,
|
||||
};
|
||||
|
||||
history.push_back(sample);
|
||||
if (history.size() > 300) {
|
||||
history.erase(history.begin(), history.begin() + (history.size() - 300));
|
||||
}
|
||||
|
||||
print_terminal(sample);
|
||||
|
||||
window_start = now;
|
||||
packets = 0;
|
||||
bytes = 0;
|
||||
interval_total_ms = 0;
|
||||
interval_squared_total_ms = 0;
|
||||
interval_min_ms = 0;
|
||||
interval_max_ms = 0;
|
||||
encode_total_us = 0;
|
||||
encode_max_us = 0;
|
||||
}
|
||||
|
||||
void timing_t::print_terminal(const sample_t &sample) {
|
||||
const auto fps_max = max_of(history, [](const auto &sample) { return sample.fps; });
|
||||
const auto avg_interval_max = max_of(history, [](const auto &sample) { return sample.avg_interval_ms; });
|
||||
const auto max_interval_max = max_of(history, [](const auto &sample) { return sample.max_interval_ms; });
|
||||
const auto jitter_max = max_of(history, [](const auto &sample) { return sample.jitter_ms; });
|
||||
const auto encode_max = max_of(history, [](const auto &sample) { return sample.max_encode_us; });
|
||||
const auto interval_scale = std::max({avg_interval_max, max_interval_max, jitter_max, 1.0});
|
||||
const auto interval_scale_us = interval_scale * 1000.0;
|
||||
|
||||
std::ostringstream out;
|
||||
out << bold << cyan << "Sunshine output packet timing" << reset << dim
|
||||
<< " last " << std::min(history.size(), graph_width) << "s" << reset << "\n";
|
||||
out << dim << std::string(96, '-') << reset << "\n";
|
||||
out << std::fixed << std::setprecision(3);
|
||||
out << " status " << quality_label(sample) << " time " << sample.elapsed_seconds << "s"
|
||||
<< " packets " << sample.packets << " bytes " << sample.bytes << " last_frame "
|
||||
<< sample.last_frame << " last_size " << sample.last_size << " IDR "
|
||||
<< (sample.last_idr ? "yes" : "no") << "\n\n";
|
||||
|
||||
metric_row(out, "FPS", sample.fps, "fps", fps_max, blue, 3);
|
||||
metric_row(out, "avg interval", sample.avg_interval_ms * 1000.0, "us", interval_scale_us, green, 1);
|
||||
metric_row(out, "max interval", sample.max_interval_ms * 1000.0, "us", interval_scale_us, orange, 1);
|
||||
metric_row(out, "jitter", sample.jitter_ms * 1000.0, "us", interval_scale_us, jitter_color(sample), 1);
|
||||
metric_row(out, "avg encode", sample.avg_encode_us, "us", encode_max, cyan, 1);
|
||||
metric_row(out, "max encode", sample.max_encode_us, "us", encode_max, yellow, 1);
|
||||
|
||||
out << "\n" << bold << "timeline" << reset << dim << " oldest -> newest" << reset << "\n";
|
||||
out << " " << blue << "fps " << reset
|
||||
<< sparkline(history, [](const auto &sample) { return sample.fps; }, fps_max) << " max "
|
||||
<< fps_max << "\n";
|
||||
out << " " << green << "avg us " << reset
|
||||
<< sparkline(history, [](const auto &sample) { return sample.avg_interval_ms; }, interval_scale)
|
||||
<< "\n";
|
||||
out << " " << orange << "max us " << reset
|
||||
<< sparkline(history, [](const auto &sample) { return sample.max_interval_ms; }, interval_scale)
|
||||
<< "\n";
|
||||
out << " " << jitter_color(sample) << "jitter us " << reset
|
||||
<< sparkline(history, [](const auto &sample) { return sample.jitter_ms; }, interval_scale)
|
||||
<< "\n";
|
||||
out << " " << cyan << "encode us " << reset
|
||||
<< sparkline(history, [](const auto &sample) { return sample.avg_encode_us; }, encode_max)
|
||||
<< "\n";
|
||||
out << " " << red << "IDR " << reset << idr_markers(history) << "\n\n";
|
||||
|
||||
out << bold << "jitter insight" << reset << " " << jitter_color(sample) << jitter_insight(sample)
|
||||
<< reset << "\n";
|
||||
out << dim << std::string(96, '-') << reset << "\n";
|
||||
|
||||
auto render = out.str();
|
||||
const auto lines = std::count(render.begin(), render.end(), '\n');
|
||||
std::cout << clear_previous_render(last_render_lines) << render << std::flush;
|
||||
last_render_lines = lines;
|
||||
}
|
||||
} // namespace output_debug
|
||||
49
src/output_debug.h
Normal file
49
src/output_debug.h
Normal file
@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace output_debug {
|
||||
class timing_t {
|
||||
public:
|
||||
struct sample_t {
|
||||
double elapsed_seconds;
|
||||
double fps;
|
||||
double avg_interval_ms;
|
||||
double min_interval_ms;
|
||||
double max_interval_ms;
|
||||
double jitter_ms;
|
||||
double avg_encode_us;
|
||||
double max_encode_us;
|
||||
uint64_t packets;
|
||||
uint64_t bytes;
|
||||
int64_t last_frame;
|
||||
size_t last_size;
|
||||
bool last_idr;
|
||||
};
|
||||
|
||||
explicit timing_t(bool enabled);
|
||||
|
||||
void record(int64_t frame_index, size_t packet_size, bool idr_frame, double encode_duration_us = 0);
|
||||
|
||||
private:
|
||||
void print_terminal(const sample_t &sample);
|
||||
|
||||
bool enabled;
|
||||
std::chrono::steady_clock::time_point start_time;
|
||||
std::chrono::steady_clock::time_point window_start;
|
||||
std::chrono::steady_clock::time_point last_packet;
|
||||
uint64_t packets{};
|
||||
uint64_t bytes{};
|
||||
double interval_total_ms{};
|
||||
double interval_squared_total_ms{};
|
||||
double interval_min_ms{};
|
||||
double interval_max_ms{};
|
||||
double encode_total_us{};
|
||||
double encode_max_us{};
|
||||
size_t last_render_lines{};
|
||||
std::vector<sample_t> history;
|
||||
};
|
||||
} // namespace output_debug
|
||||
@ -248,7 +248,9 @@ protected:
|
||||
|
||||
// DDUP holds an unfair D3D11 device lock during AcquireNextFrame, which starves
|
||||
// the encoder thread on timeout. WGC doesn't have this problem.
|
||||
virtual bool needs_timeout_yield() const { return true; }
|
||||
virtual bool needs_timeout_yield() const {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class display_ram_t : public display_base_t {
|
||||
@ -395,7 +397,9 @@ public:
|
||||
std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout,
|
||||
bool cursor_visible) override;
|
||||
capture_e release_snapshot() override;
|
||||
bool needs_timeout_yield() const override { return false; }
|
||||
bool needs_timeout_yield() const override {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -410,6 +414,8 @@ public:
|
||||
std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout,
|
||||
bool cursor_visible) override;
|
||||
capture_e release_snapshot() override;
|
||||
bool needs_timeout_yield() const override { return false; }
|
||||
bool needs_timeout_yield() const override {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
} // namespace platf::dxgi
|
||||
|
||||
@ -214,13 +214,19 @@ capture_e display_base_t::capture(const push_captured_image_cb_t &push_captured_
|
||||
platf::capture_e status = capture_e::ok;
|
||||
std::shared_ptr<img_t> img_out;
|
||||
|
||||
// Try to continue frame pacing group, snapshot() is called with zero timeout after waiting for client frame interval
|
||||
if (frame_pacing_group_start && client_frame_rate_adjusted.Numerator > 0 && client_frame_rate_adjusted.Denominator > 0) {
|
||||
const uint32_t seconds = (uint64_t) frame_pacing_group_frames * client_frame_rate_adjusted.Denominator / client_frame_rate_adjusted.Numerator;
|
||||
const uint32_t remainder = (uint64_t) frame_pacing_group_frames * client_frame_rate_adjusted.Denominator % client_frame_rate_adjusted.Numerator;
|
||||
const auto sleep_target = *frame_pacing_group_start +
|
||||
std::chrono::nanoseconds(1s) * seconds +
|
||||
std::chrono::nanoseconds(1s) * remainder / client_frame_rate_adjusted.Numerator;
|
||||
// Try to continue frame pacing group, snapshot() is called with zero timeout after waiting for
|
||||
// client frame interval
|
||||
if (frame_pacing_group_start && client_frame_rate_adjusted.Numerator > 0 &&
|
||||
client_frame_rate_adjusted.Denominator > 0) {
|
||||
const uint32_t seconds = (uint64_t)frame_pacing_group_frames *
|
||||
client_frame_rate_adjusted.Denominator /
|
||||
client_frame_rate_adjusted.Numerator;
|
||||
const uint32_t remainder = (uint64_t)frame_pacing_group_frames *
|
||||
client_frame_rate_adjusted.Denominator %
|
||||
client_frame_rate_adjusted.Numerator;
|
||||
const auto sleep_target =
|
||||
*frame_pacing_group_start + std::chrono::nanoseconds(1s) * seconds +
|
||||
std::chrono::nanoseconds(1s) * remainder / client_frame_rate_adjusted.Numerator;
|
||||
const auto sleep_period = sleep_target - std::chrono::steady_clock::now();
|
||||
|
||||
if (sleep_period <= 0ns) {
|
||||
@ -243,7 +249,10 @@ capture_e display_base_t::capture(const push_captured_image_cb_t &push_captured_
|
||||
}
|
||||
|
||||
// Start new frame pacing group if necessary, snapshot() is called with non-zero timeout
|
||||
if (status == capture_e::timeout || (status == capture_e::ok && (!frame_pacing_group_start || client_frame_rate_adjusted.Numerator == 0 || client_frame_rate_adjusted.Denominator == 0))) {
|
||||
if (status == capture_e::timeout ||
|
||||
(status == capture_e::ok &&
|
||||
(!frame_pacing_group_start || client_frame_rate_adjusted.Numerator == 0 ||
|
||||
client_frame_rate_adjusted.Denominator == 0))) {
|
||||
status = snapshot(pull_free_image_cb, img_out, 200ms, *cursor);
|
||||
|
||||
if (status == capture_e::ok && img_out) {
|
||||
|
||||
@ -6,12 +6,17 @@
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "logging.h"
|
||||
#include "utility.h"
|
||||
|
||||
namespace safe {
|
||||
@ -241,8 +246,20 @@ public:
|
||||
return;
|
||||
}
|
||||
|
||||
if (_queue.size() == _max_elements) {
|
||||
_queue.clear();
|
||||
if (_max_elements == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_queue.size() >= _max_elements) {
|
||||
_queue.erase(std::begin(_queue));
|
||||
++_overflow_count;
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (!_last_overflow_log || now - *_last_overflow_log >= std::chrono::seconds{1}) {
|
||||
BOOST_LOG(warning) << "Dropping oldest item from full queue; dropped " << _overflow_count
|
||||
<< " item(s) so far";
|
||||
_last_overflow_log = now;
|
||||
}
|
||||
}
|
||||
|
||||
_queue.emplace_back(std::forward<Args>(args)...);
|
||||
@ -318,6 +335,8 @@ private:
|
||||
std::condition_variable _cv;
|
||||
|
||||
std::vector<T> _queue;
|
||||
std::uint64_t _overflow_count{};
|
||||
std::optional<std::chrono::steady_clock::time_point> _last_overflow_log;
|
||||
};
|
||||
|
||||
template <class T> class shared_t {
|
||||
|
||||
@ -35,6 +35,37 @@ extern "C" {
|
||||
using namespace std::literals;
|
||||
namespace video {
|
||||
|
||||
namespace {
|
||||
void wait_until_frame_time(platf::high_precision_timer &timer,
|
||||
std::chrono::steady_clock::time_point target) {
|
||||
constexpr auto spin_threshold = 500us;
|
||||
constexpr auto yield_threshold = 50us;
|
||||
|
||||
while (true) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (now >= target) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto remaining = target - now;
|
||||
if (remaining > spin_threshold) {
|
||||
timer.sleep_for(remaining - spin_threshold);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (remaining > yield_threshold) {
|
||||
std::this_thread::yield();
|
||||
} else {
|
||||
#ifdef _WIN32
|
||||
YieldProcessor();
|
||||
#else
|
||||
std::this_thread::yield();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void free_ctx(AVCodecContext *ctx) {
|
||||
avcodec_free_context(&ctx);
|
||||
}
|
||||
@ -352,8 +383,7 @@ public:
|
||||
if (!device && (avcodec_ctx->slices > 1 || avcodec_ctx->codec_id == AV_CODEC_ID_H265)) {
|
||||
avcodec_ctx->rc_buffer_size = expected_bitrate / ((framerate * 10) / 15);
|
||||
} else {
|
||||
// Enforce strict VBV tuning by shrinking the hardware encoder's buffer to exactly 0.5s of data.
|
||||
avcodec_ctx->rc_buffer_size = expected_bitrate / (framerate * 2);
|
||||
avcodec_ctx->rc_buffer_size = expected_bitrate / framerate;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1068,6 +1098,7 @@ void captureThread(std::shared_ptr<safe::queue_t<capture_ctx_t>> capture_ctx_que
|
||||
int encode_avcodec(int64_t frame_nr, avcodec_encode_session_t &session,
|
||||
safe::mail_raw_t::queue_t<packet_t> &packets, void *channel_data,
|
||||
std::optional<std::chrono::steady_clock::time_point> frame_timestamp) {
|
||||
auto encode_start = std::chrono::steady_clock::now();
|
||||
auto &frame = session.device->frame;
|
||||
frame->pts = frame_nr;
|
||||
|
||||
@ -1130,6 +1161,9 @@ int encode_avcodec(int64_t frame_nr, avcodec_encode_session_t &session,
|
||||
|
||||
packet->replacements = &session.replacements;
|
||||
packet->channel_data = channel_data;
|
||||
packet->encode_duration_us =
|
||||
std::chrono::duration<double, std::micro>(std::chrono::steady_clock::now() - encode_start)
|
||||
.count();
|
||||
packets->raise(std::move(packet));
|
||||
}
|
||||
|
||||
@ -1139,7 +1173,11 @@ int encode_avcodec(int64_t frame_nr, avcodec_encode_session_t &session,
|
||||
int encode_nvenc(int64_t frame_nr, nvenc_encode_session_t &session,
|
||||
safe::mail_raw_t::queue_t<packet_t> &packets, void *channel_data,
|
||||
std::optional<std::chrono::steady_clock::time_point> frame_timestamp) {
|
||||
auto encode_start = std::chrono::steady_clock::now();
|
||||
auto encoded_frame = session.encode_frame(frame_nr);
|
||||
auto encode_duration_us =
|
||||
std::chrono::duration<double, std::micro>(std::chrono::steady_clock::now() - encode_start)
|
||||
.count();
|
||||
if (encoded_frame.data.empty()) {
|
||||
BOOST_LOG(error) << "NvENC returned empty packet";
|
||||
return -1;
|
||||
@ -1155,6 +1193,7 @@ int encode_nvenc(int64_t frame_nr, nvenc_encode_session_t &session,
|
||||
packet->channel_data = channel_data;
|
||||
packet->after_ref_frame_invalidation = encoded_frame.after_ref_frame_invalidation;
|
||||
packet->frame_timestamp = frame_timestamp;
|
||||
packet->encode_duration_us = encode_duration_us;
|
||||
packets->raise(std::move(packet));
|
||||
|
||||
return 0;
|
||||
@ -1429,8 +1468,7 @@ make_avcodec_encode_session(platf::display_t *disp, const encoder_t &encoder,
|
||||
// buffer by 1.5x for software HEVC encoding.
|
||||
ctx->rc_buffer_size = bitrate / ((config.framerate * 10) / 15);
|
||||
} else {
|
||||
// Enforce strict VBV tuning by shrinking the hardware encoder's buffer to exactly 0.5s of data.
|
||||
ctx->rc_buffer_size = bitrate / (config.framerate * 2);
|
||||
ctx->rc_buffer_size = bitrate / config.framerate;
|
||||
|
||||
if (encoder.name == "nvenc" && config::video.nv_legacy.vbv_percentage_increase > 0) {
|
||||
ctx->rc_buffer_size +=
|
||||
@ -1703,7 +1741,7 @@ void encode_run(int &frame_nr, // Store progress of the frame number
|
||||
next_frame_time = now + 100ms;
|
||||
}
|
||||
}
|
||||
timer->sleep_for(duration);
|
||||
wait_until_frame_time(*timer, now + duration);
|
||||
} else if (now - next_frame_time > 100ms) {
|
||||
// Reset target if we fall more than 100ms behind to avoid massive bursts
|
||||
next_frame_time = now;
|
||||
|
||||
@ -233,6 +233,7 @@ struct packet_raw_t {
|
||||
std::vector<replace_t> *replacements = nullptr;
|
||||
void *channel_data = nullptr;
|
||||
bool after_ref_frame_invalidation = false;
|
||||
std::optional<double> encode_duration_us;
|
||||
std::optional<std::chrono::steady_clock::time_point> frame_timestamp;
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user