diff --git a/src/config.cpp b/src/config.cpp index 8e551e2a..8c518df4 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -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 diff --git a/src/main.cpp b/src/main.cpp index 2ab7189c..2d9a1070 100644 --- a/src/main.cpp +++ b/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(mail::video_packets); auto audio_packets = mail->queue(mail::audio_packets); auto local_shutdown = mail->event(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()); } diff --git a/src/nvenc/nvenc_base.cpp b/src/nvenc/nvenc_base.cpp index 72860f72..fc96c6c4 100644 --- a/src/nvenc/nvenc_base.cpp +++ b/src/nvenc/nvenc_base.cpp @@ -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; diff --git a/src/nvenc/nvenc_config.h b/src/nvenc/nvenc_config.h index def856fe..a94e8e06 100644 --- a/src/nvenc/nvenc_config.h +++ b/src/nvenc/nvenc_config.h @@ -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 diff --git a/src/output_debug.cpp b/src/output_debug.cpp new file mode 100644 index 00000000..acab8775 --- /dev/null +++ b/src/output_debug.cpp @@ -0,0 +1,277 @@ +#include "output_debug.h" + +#include "logging.h" + +#include +#include +#include +#include +#include +#include +#include + +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 double max_of(const std::vector &history, Value value) { + double result = 1; + for (const auto &sample : history) { + result = std::max(result, value(sample)); + } + return result; +} + +template +std::string sparkline(const std::vector &history, Value value, double max_value) { + static constexpr std::array 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(levels.size() - 1, static_cast(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(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 &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(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(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(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 diff --git a/src/output_debug.h b/src/output_debug.h new file mode 100644 index 00000000..b3c71a60 --- /dev/null +++ b/src/output_debug.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include + +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 history; +}; +} // namespace output_debug diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h index d1334ae5..22279c77 100644 --- a/src/platform/windows/display.h +++ b/src/platform/windows/display.h @@ -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 &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 &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 diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index 6dc0b439..4a809a42 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -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_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) { diff --git a/src/thread_safe.h b/src/thread_safe.h index 7378a1a8..8c0be93f 100644 --- a/src/thread_safe.h +++ b/src/thread_safe.h @@ -6,12 +6,17 @@ #include #include +#include #include +#include #include #include +#include #include +#include #include +#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)...); @@ -318,6 +335,8 @@ private: std::condition_variable _cv; std::vector _queue; + std::uint64_t _overflow_count{}; + std::optional _last_overflow_log; }; template class shared_t { diff --git a/src/video.cpp b/src/video.cpp index 203c9ef4..7690b4ae 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -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> capture_ctx_que int encode_avcodec(int64_t frame_nr, avcodec_encode_session_t &session, safe::mail_raw_t::queue_t &packets, void *channel_data, std::optional 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(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 &packets, void *channel_data, std::optional 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(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; diff --git a/src/video.h b/src/video.h index 082b6c17..7b68a2cd 100644 --- a/src/video.h +++ b/src/video.h @@ -233,6 +233,7 @@ struct packet_raw_t { std::vector *replacements = nullptr; void *channel_data = nullptr; bool after_ref_frame_invalidation = false; + std::optional encode_duration_us; std::optional frame_timestamp; };