diff --git a/README.md b/README.md index 0dfa068c16..7b1d2e782a 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,24 @@ For example, you could capture the video within [OBS]. [OBS]: https://obsproject.com/fr +#### Buffering + +It is possible to add buffering. This increases latency but reduces jitter (see +#2464). + +The option is available for display buffering: + +```bash +scrcpy --display-buffer=50 # add 50 ms buffering for display +``` + +and V4L2 sink: + +```bash +scrcpy --v4l2-buffer=500 # add 500 ms buffering for v4l2 sink +``` + + ### Connection #### Wireless diff --git a/app/meson.build b/app/meson.build index 0663c641db..f5345803cc 100644 --- a/app/meson.build +++ b/app/meson.build @@ -2,6 +2,7 @@ src = [ 'src/main.c', 'src/adb.c', 'src/cli.c', + 'src/clock.c', 'src/compat.c', 'src/control_msg.c', 'src/controller.c', @@ -10,6 +11,7 @@ src = [ 'src/event_converter.c', 'src/file_handler.c', 'src/fps_counter.c', + 'src/frame_buffer.c', 'src/input_manager.c', 'src/opengl.c', 'src/receiver.c', @@ -25,6 +27,7 @@ src = [ 'src/util/process.c', 'src/util/str_util.c', 'src/util/thread.c', + 'src/util/tick.c', ] if host_machine.system() == 'windows' @@ -165,6 +168,10 @@ if get_option('buildtype') == 'debug' 'src/cli.c', 'src/util/str_util.c', ]], + ['test_clock', [ + 'tests/test_clock.c', + 'src/clock.c', + ]], ['test_control_msg_serialize', [ 'tests/test_control_msg_serialize.c', 'src/control_msg.c', diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 253dd04ff7..00589a43d9 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -56,6 +56,12 @@ The list of possible display ids can be listed by "adb shell dumpsys display" Default is 0. +.TP +.BI "\-\-display\-buffer ms +Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter. + +Default is 0 (no buffering). + .TP .BI "\-\-encoder " name Use a specific MediaCodec encoder (must be a H.264 encoder). @@ -191,6 +197,14 @@ Output to v4l2loopback device. It requires to lock the video orientation (see \fB\-\-lock\-video\-orientation\fR). +.TP +.BI "\-\-v4l2-buffer " ms +Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter. + +This option is similar to \fB\-\-display\-buffer\fR, but specific to V4L2 sink. + +Default is 0 (no buffering). + .TP .BI "\-V, \-\-verbosity " value Set the log level ("verbose", "debug", "info", "warn" or "error"). diff --git a/app/src/cli.c b/app/src/cli.c index ab35745d85..d22096cafa 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -55,6 +55,12 @@ scrcpy_print_usage(const char *arg0) { "\n" " Default is 0.\n" "\n" + " --display-buffer ms\n" + " Add a buffering delay (in milliseconds) before displaying.\n" + " This increases latency to compensate for jitter.\n" + "\n" + " Default is 0 (no buffering).\n" + "\n" " --encoder name\n" " Use a specific MediaCodec encoder (must be a H.264 encoder).\n" "\n" @@ -182,6 +188,15 @@ scrcpy_print_usage(const char *arg0) { " It requires to lock the video orientation (see\n" " --lock-video-orientation).\n" "\n" + " --v4l2-buffer ms\n" + " Add a buffering delay (in milliseconds) before pushing\n" + " frames. This increases latency to compensate for jitter.\n" + "\n" + " This option is similar to --display-buffer, but specific to\n" + " V4L2 sink.\n" + "\n" + " Default is 0 (no buffering).\n" + "\n" #endif " -V, --verbosity value\n" " Set the log level (verbose, debug, info, warn or error).\n" @@ -392,6 +407,19 @@ parse_max_fps(const char *s, uint16_t *max_fps) { return true; } +static bool +parse_buffering_time(const char *s, sc_tick *tick) { + long value; + bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, + "buffering time"); + if (!ok) { + return false; + } + + *tick = SC_TICK_FROM_MS(value); + return true; +} + static bool parse_lock_video_orientation(const char *s, enum sc_lock_video_orientation *lock_mode) { @@ -689,6 +717,8 @@ guess_record_format(const char *filename) { #define OPT_ENCODER_NAME 1025 #define OPT_POWER_OFF_ON_CLOSE 1026 #define OPT_V4L2_SINK 1027 +#define OPT_DISPLAY_BUFFER 1028 +#define OPT_V4L2_BUFFER 1029 bool scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { @@ -700,6 +730,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"disable-screensaver", no_argument, NULL, OPT_DISABLE_SCREENSAVER}, {"display", required_argument, NULL, OPT_DISPLAY_ID}, + {"display-buffer", required_argument, NULL, OPT_DISPLAY_BUFFER}, {"encoder", required_argument, NULL, OPT_ENCODER_NAME}, {"force-adb-forward", no_argument, NULL, OPT_FORCE_ADB_FORWARD}, @@ -732,6 +763,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { {"turn-screen-off", no_argument, NULL, 'S'}, #ifdef HAVE_V4L2 {"v4l2-sink", required_argument, NULL, OPT_V4L2_SINK}, + {"v4l2-buffer", required_argument, NULL, OPT_V4L2_BUFFER}, #endif {"verbosity", required_argument, NULL, 'V'}, {"version", no_argument, NULL, 'v'}, @@ -917,10 +949,20 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { case OPT_POWER_OFF_ON_CLOSE: opts->power_off_on_close = true; break; + case OPT_DISPLAY_BUFFER: + if (!parse_buffering_time(optarg, &opts->display_buffer)) { + return false; + } + break; #ifdef HAVE_V4L2 case OPT_V4L2_SINK: opts->v4l2_device = optarg; break; + case OPT_V4L2_BUFFER: + if (!parse_buffering_time(optarg, &opts->v4l2_buffer)) { + return false; + } + break; #endif default: // getopt prints the error message on stderr @@ -941,6 +983,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { "See --lock-video-orientation."); opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL; } + + if (opts->v4l2_buffer && !opts->v4l2_device) { + LOGE("V4L2 buffer value without V4L2 sink\n"); + return false; + } #else if (!opts->display && !opts->record_filename) { LOGE("-N/--no-display requires screen recording (-r/--record)"); diff --git a/app/src/clock.c b/app/src/clock.c new file mode 100644 index 0000000000..fe072f0156 --- /dev/null +++ b/app/src/clock.c @@ -0,0 +1,111 @@ +#include "clock.h" + +#include "util/log.h" + +#define SC_CLOCK_NDEBUG // comment to debug + +void +sc_clock_init(struct sc_clock *clock) { + clock->count = 0; + clock->head = 0; + clock->left_sum.system = 0; + clock->left_sum.stream = 0; + clock->right_sum.system = 0; + clock->right_sum.stream = 0; +} + +// Estimate the affine function f(stream) = slope * stream + offset +static void +sc_clock_estimate(struct sc_clock *clock, + double *out_slope, sc_tick *out_offset) { + assert(clock->count > 1); // two points are necessary + + struct sc_clock_point left_avg = { + .system = clock->left_sum.system / (clock->count / 2), + .stream = clock->left_sum.stream / (clock->count / 2), + }; + struct sc_clock_point right_avg = { + .system = clock->right_sum.system / ((clock->count + 1) / 2), + .stream = clock->right_sum.stream / ((clock->count + 1) / 2), + }; + + double slope = (double) (right_avg.system - left_avg.system) + / (right_avg.stream - left_avg.stream); + + if (clock->count < SC_CLOCK_RANGE) { + /* The first frames are typically received and decoded with more delay + * than the others, causing a wrong slope estimation on start. To + * compensate, assume an initial slope of 1, then progressively use the + * estimated slope. */ + slope = (clock->count * slope + (SC_CLOCK_RANGE - clock->count)) + / SC_CLOCK_RANGE; + } + + struct sc_clock_point global_avg = { + .system = (clock->left_sum.system + clock->right_sum.system) + / clock->count, + .stream = (clock->left_sum.stream + clock->right_sum.stream) + / clock->count, + }; + + sc_tick offset = global_avg.system - (sc_tick) (global_avg.stream * slope); + + *out_slope = slope; + *out_offset = offset; +} + +void +sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) { + struct sc_clock_point *point = &clock->points[clock->head]; + + if (clock->count == SC_CLOCK_RANGE || clock->count & 1) { + // One point passes from the right sum to the left sum + + unsigned mid; + if (clock->count == SC_CLOCK_RANGE) { + mid = (clock->head + SC_CLOCK_RANGE / 2) % SC_CLOCK_RANGE; + } else { + // Only for the first frames + mid = clock->count / 2; + } + + struct sc_clock_point *mid_point = &clock->points[mid]; + clock->left_sum.system += mid_point->system; + clock->left_sum.stream += mid_point->stream; + clock->right_sum.system -= mid_point->system; + clock->right_sum.stream -= mid_point->stream; + } + + if (clock->count == SC_CLOCK_RANGE) { + // The current point overwrites the previous value in the circular + // array, update the left sum accordingly + clock->left_sum.system -= point->system; + clock->left_sum.stream -= point->stream; + } else { + ++clock->count; + } + + point->system = system; + point->stream = stream; + + clock->right_sum.system += system; + clock->right_sum.stream += stream; + + clock->head = (clock->head + 1) % SC_CLOCK_RANGE; + + if (clock->count > 1) { + // Update estimation + sc_clock_estimate(clock, &clock->slope, &clock->offset); + +#ifndef SC_CLOCK_NDEBUG + LOGD("Clock estimation: %g * pts + %" PRItick, + clock->slope, clock->offset); +#endif + } +} + +sc_tick +sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) { + assert(clock->count > 1); // sc_clock_update() must have been called + return (sc_tick) (stream * clock->slope) + clock->offset; +} diff --git a/app/src/clock.h b/app/src/clock.h new file mode 100644 index 0000000000..eb7fa59411 --- /dev/null +++ b/app/src/clock.h @@ -0,0 +1,70 @@ +#ifndef SC_CLOCK_H +#define SC_CLOCK_H + +#include "common.h" + +#include + +#include "util/tick.h" + +#define SC_CLOCK_RANGE 32 +static_assert(!(SC_CLOCK_RANGE & 1), "SC_CLOCK_RANGE must be even"); + +struct sc_clock_point { + sc_tick system; + sc_tick stream; +}; + +/** + * The clock aims to estimate the affine relation between the stream (device) + * time and the system time: + * + * f(stream) = slope * stream + offset + * + * To that end, it stores the SC_CLOCK_RANGE last clock points (the timestamps + * of a frame expressed both in stream time and system time) in a circular + * array. + * + * To estimate the slope, it splits the last SC_CLOCK_RANGE points into two + * sets of SC_CLOCK_RANGE/2 points, and compute their centroid ("average + * point"). The slope of the estimated affine function is that of the line + * passing through these two points. + * + * To estimate the offset, it computes the centroid of all the SC_CLOCK_RANGE + * points. The resulting affine function passes by this centroid. + * + * With a circular array, the rolling sums (and average) are quick to compute. + * In practice, the estimation is stable and the evolution is smooth. + */ +struct sc_clock { + // Circular array + struct sc_clock_point points[SC_CLOCK_RANGE]; + + // Number of points in the array (count <= SC_CLOCK_RANGE) + unsigned count; + + // Index of the next point to write + unsigned head; + + // Sum of the first count/2 points + struct sc_clock_point left_sum; + + // Sum of the last (count+1)/2 points + struct sc_clock_point right_sum; + + // Estimated slope and offset + // (computed on sc_clock_update(), used by sc_clock_to_system_time()) + double slope; + sc_tick offset; +}; + +void +sc_clock_init(struct sc_clock *clock); + +void +sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream); + +sc_tick +sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream); + +#endif diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index bbf7188729..c92d414053 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -1,11 +1,10 @@ #include "fps_counter.h" #include -#include #include "util/log.h" -#define FPS_COUNTER_INTERVAL_MS 1000 +#define FPS_COUNTER_INTERVAL SC_TICK_FROM_SEC(1) bool fps_counter_init(struct fps_counter *counter) { @@ -47,7 +46,7 @@ set_started(struct fps_counter *counter, bool started) { static void display_fps(struct fps_counter *counter) { unsigned rendered_per_second = - counter->nr_rendered * 1000 / FPS_COUNTER_INTERVAL_MS; + counter->nr_rendered * SC_TICK_FREQ / FPS_COUNTER_INTERVAL; if (counter->nr_skipped) { LOGI("%u fps (+%u frames skipped)", rendered_per_second, counter->nr_skipped); @@ -68,8 +67,8 @@ check_interval_expired(struct fps_counter *counter, uint32_t now) { counter->nr_skipped = 0; // add a multiple of the interval uint32_t elapsed_slices = - (now - counter->next_timestamp) / FPS_COUNTER_INTERVAL_MS + 1; - counter->next_timestamp += FPS_COUNTER_INTERVAL_MS * elapsed_slices; + (now - counter->next_timestamp) / FPS_COUNTER_INTERVAL + 1; + counter->next_timestamp += FPS_COUNTER_INTERVAL * elapsed_slices; } static int @@ -82,14 +81,12 @@ run_fps_counter(void *data) { sc_cond_wait(&counter->state_cond, &counter->mutex); } while (!counter->interrupted && is_started(counter)) { - uint32_t now = SDL_GetTicks(); + sc_tick now = sc_tick_now(); check_interval_expired(counter, now); - assert(counter->next_timestamp > now); - uint32_t remaining = counter->next_timestamp - now; - // ignore the reason (timeout or signaled), we just loop anyway - sc_cond_timedwait(&counter->state_cond, &counter->mutex, remaining); + sc_cond_timedwait(&counter->state_cond, &counter->mutex, + counter->next_timestamp); } } sc_mutex_unlock(&counter->mutex); @@ -99,7 +96,7 @@ run_fps_counter(void *data) { bool fps_counter_start(struct fps_counter *counter) { sc_mutex_lock(&counter->mutex); - counter->next_timestamp = SDL_GetTicks() + FPS_COUNTER_INTERVAL_MS; + counter->next_timestamp = sc_tick_now() + FPS_COUNTER_INTERVAL; counter->nr_rendered = 0; counter->nr_skipped = 0; sc_mutex_unlock(&counter->mutex); @@ -165,7 +162,7 @@ fps_counter_add_rendered_frame(struct fps_counter *counter) { } sc_mutex_lock(&counter->mutex); - uint32_t now = SDL_GetTicks(); + sc_tick now = sc_tick_now(); check_interval_expired(counter, now); ++counter->nr_rendered; sc_mutex_unlock(&counter->mutex); @@ -178,7 +175,7 @@ fps_counter_add_skipped_frame(struct fps_counter *counter) { } sc_mutex_lock(&counter->mutex); - uint32_t now = SDL_GetTicks(); + sc_tick now = sc_tick_now(); check_interval_expired(counter, now); ++counter->nr_skipped; sc_mutex_unlock(&counter->mutex); diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index de2525866c..9609c81489 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -24,7 +24,7 @@ struct fps_counter { bool interrupted; unsigned nr_rendered; unsigned nr_skipped; - uint32_t next_timestamp; + sc_tick next_timestamp; }; bool diff --git a/app/src/frame_buffer.c b/app/src/frame_buffer.c new file mode 100644 index 0000000000..33ca622721 --- /dev/null +++ b/app/src/frame_buffer.c @@ -0,0 +1,88 @@ +#include "frame_buffer.h" + +#include +#include +#include + +#include "util/log.h" + +bool +sc_frame_buffer_init(struct sc_frame_buffer *fb) { + fb->pending_frame = av_frame_alloc(); + if (!fb->pending_frame) { + return false; + } + + fb->tmp_frame = av_frame_alloc(); + if (!fb->tmp_frame) { + av_frame_free(&fb->pending_frame); + return false; + } + + bool ok = sc_mutex_init(&fb->mutex); + if (!ok) { + av_frame_free(&fb->pending_frame); + av_frame_free(&fb->tmp_frame); + return false; + } + + // there is initially no frame, so consider it has already been consumed + fb->pending_frame_consumed = true; + + return true; +} + +void +sc_frame_buffer_destroy(struct sc_frame_buffer *fb) { + sc_mutex_destroy(&fb->mutex); + av_frame_free(&fb->pending_frame); + av_frame_free(&fb->tmp_frame); +} + +static inline void +swap_frames(AVFrame **lhs, AVFrame **rhs) { + AVFrame *tmp = *lhs; + *lhs = *rhs; + *rhs = tmp; +} + +bool +sc_frame_buffer_push(struct sc_frame_buffer *fb, const AVFrame *frame, + bool *previous_frame_skipped) { + sc_mutex_lock(&fb->mutex); + + // Use a temporary frame to preserve pending_frame in case of error. + // tmp_frame is an empty frame, no need to call av_frame_unref() beforehand. + int r = av_frame_ref(fb->tmp_frame, frame); + if (r) { + LOGE("Could not ref frame: %d", r); + return false; + } + + // Now that av_frame_ref() succeeded, we can replace the previous + // pending_frame + swap_frames(&fb->pending_frame, &fb->tmp_frame); + av_frame_unref(fb->tmp_frame); + + if (previous_frame_skipped) { + *previous_frame_skipped = !fb->pending_frame_consumed; + } + fb->pending_frame_consumed = false; + + sc_mutex_unlock(&fb->mutex); + + return true; +} + +void +sc_frame_buffer_consume(struct sc_frame_buffer *fb, AVFrame *dst) { + sc_mutex_lock(&fb->mutex); + assert(!fb->pending_frame_consumed); + fb->pending_frame_consumed = true; + + av_frame_move_ref(dst, fb->pending_frame); + // av_frame_move_ref() resets its source frame, so no need to call + // av_frame_unref() + + sc_mutex_unlock(&fb->mutex); +} diff --git a/app/src/frame_buffer.h b/app/src/frame_buffer.h new file mode 100644 index 0000000000..f97261cd31 --- /dev/null +++ b/app/src/frame_buffer.h @@ -0,0 +1,44 @@ +#ifndef SC_FRAME_BUFFER_H +#define SC_FRAME_BUFFER_H + +#include "common.h" + +#include + +#include "util/thread.h" + +// forward declarations +typedef struct AVFrame AVFrame; + +/** + * A frame buffer holds 1 pending frame, which is the last frame received from + * the producer (typically, the decoder). + * + * If a pending frame has not been consumed when the producer pushes a new + * frame, then it is lost. The intent is to always provide access to the very + * last frame to minimize latency. + */ + +struct sc_frame_buffer { + AVFrame *pending_frame; + AVFrame *tmp_frame; // To preserve the pending frame on error + + sc_mutex mutex; + + bool pending_frame_consumed; +}; + +bool +sc_frame_buffer_init(struct sc_frame_buffer *fb); + +void +sc_frame_buffer_destroy(struct sc_frame_buffer *fb); + +bool +sc_frame_buffer_push(struct sc_frame_buffer *fb, const AVFrame *frame, + bool *skipped); + +void +sc_frame_buffer_consume(struct sc_frame_buffer *fb, AVFrame *dst); + +#endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index d0a22e77b8..25822526ab 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -381,6 +381,7 @@ scrcpy(const struct scrcpy_options *options) { .rotation = options->rotation, .mipmaps = options->mipmaps, .fullscreen = options->fullscreen, + .buffering_time = options->display_buffer, }; if (!screen_init(&s->screen, &screen_params)) { @@ -393,7 +394,8 @@ scrcpy(const struct scrcpy_options *options) { #ifdef HAVE_V4L2 if (options->v4l2_device) { - if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device, frame_size)) { + if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device, frame_size, + options->v4l2_buffer)) { goto end; } diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index 0a2deb71e2..8b76fb25a2 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -7,6 +7,8 @@ #include #include +#include "util/tick.h" + enum sc_log_level { SC_LOG_LEVEL_VERBOSE, SC_LOG_LEVEL_DEBUG, @@ -78,6 +80,8 @@ struct scrcpy_options { uint16_t window_width; uint16_t window_height; uint32_t display_id; + sc_tick display_buffer; + sc_tick v4l2_buffer; bool show_touches; bool fullscreen; bool always_on_top; @@ -126,6 +130,8 @@ struct scrcpy_options { .window_width = 0, \ .window_height = 0, \ .display_id = 0, \ + .display_buffer = 0, \ + .v4l2_buffer = 0, \ .show_touches = false, \ .fullscreen = false, \ .always_on_top = false, \ diff --git a/app/src/screen.c b/app/src/screen.c index 99327b3b04..3cd4329ff2 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -274,14 +274,16 @@ screen_frame_sink_close(struct sc_frame_sink *sink) { static bool screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { struct screen *screen = DOWNCAST(sink); + return sc_video_buffer_push(&screen->vb, frame); +} - bool previous_frame_skipped; - bool ok = video_buffer_push(&screen->vb, frame, &previous_frame_skipped); - if (!ok) { - return false; - } +static void +sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, + void *userdata) { + (void) vb; + struct screen *screen = userdata; - if (previous_frame_skipped) { + if (previous_skipped) { fps_counter_add_skipped_frame(&screen->fps_counter); // The EVENT_NEW_FRAME triggered for the previous frame will consume // this new frame instead @@ -293,8 +295,6 @@ screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { // Post the event on the UI thread SDL_PushEvent(&new_frame_event); } - - return true; } bool @@ -304,15 +304,26 @@ screen_init(struct screen *screen, const struct screen_params *params) { screen->fullscreen = false; screen->maximized = false; - bool ok = video_buffer_init(&screen->vb); + static const struct sc_video_buffer_callbacks cbs = { + .on_new_frame = sc_video_buffer_on_new_frame, + }; + + bool ok = sc_video_buffer_init(&screen->vb, params->buffering_time, &cbs, + screen); if (!ok) { LOGE("Could not initialize video buffer"); return false; } + ok = sc_video_buffer_start(&screen->vb); + if (!ok) { + LOGE("Could not start video_buffer"); + goto error_destroy_video_buffer; + } + if (!fps_counter_init(&screen->fps_counter)) { LOGE("Could not initialize FPS counter"); - goto error_destroy_video_buffer; + goto error_stop_and_join_video_buffer; } screen->frame_size = params->frame_size; @@ -453,8 +464,11 @@ screen_init(struct screen *screen, const struct screen_params *params) { SDL_DestroyWindow(screen->window); error_destroy_fps_counter: fps_counter_destroy(&screen->fps_counter); +error_stop_and_join_video_buffer: + sc_video_buffer_stop(&screen->vb); + sc_video_buffer_join(&screen->vb); error_destroy_video_buffer: - video_buffer_destroy(&screen->vb); + sc_video_buffer_destroy(&screen->vb); return false; } @@ -471,11 +485,13 @@ screen_hide_window(struct screen *screen) { void screen_interrupt(struct screen *screen) { + sc_video_buffer_stop(&screen->vb); fps_counter_interrupt(&screen->fps_counter); } void screen_join(struct screen *screen) { + sc_video_buffer_join(&screen->vb); fps_counter_join(&screen->fps_counter); } @@ -489,7 +505,7 @@ screen_destroy(struct screen *screen) { SDL_DestroyRenderer(screen->renderer); SDL_DestroyWindow(screen->window); fps_counter_destroy(&screen->fps_counter); - video_buffer_destroy(&screen->vb); + sc_video_buffer_destroy(&screen->vb); } static void @@ -595,7 +611,7 @@ update_texture(struct screen *screen, const AVFrame *frame) { static bool screen_update_frame(struct screen *screen) { av_frame_unref(screen->frame); - video_buffer_consume(&screen->vb, screen->frame); + sc_video_buffer_consume(&screen->vb, screen->frame); AVFrame *frame = screen->frame; fps_counter_add_rendered_frame(&screen->fps_counter); diff --git a/app/src/screen.h b/app/src/screen.h index 8acabebec4..86aa1183e5 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -20,7 +20,7 @@ struct screen { bool open; // track the open/close state to assert correct behavior #endif - struct video_buffer vb; + struct sc_video_buffer vb; struct fps_counter fps_counter; SDL_Window *window; @@ -63,6 +63,8 @@ struct screen_params { bool mipmaps; bool fullscreen; + + sc_tick buffering_time; }; // initialize screen, create window, renderer and texture (window is hidden) diff --git a/app/src/server.c b/app/src/server.c index a4cdb0c970..4e0f2b2347 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -554,10 +554,10 @@ server_stop(struct server *server) { sc_mutex_lock(&server->mutex); bool signaled = false; if (!server->process_terminated) { -#define WATCHDOG_DELAY_MS 1000 +#define WATCHDOG_DELAY SC_TICK_FROM_SEC(1) signaled = sc_cond_timedwait(&server->process_terminated_cond, &server->mutex, - WATCHDOG_DELAY_MS); + sc_tick_now() + WATCHDOG_DELAY); } sc_mutex_unlock(&server->mutex); diff --git a/app/src/util/thread.c b/app/src/util/thread.c index de05365d96..2c376e978d 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -123,7 +123,13 @@ sc_cond_wait(sc_cond *cond, sc_mutex *mutex) { } bool -sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, uint32_t ms) { +sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick deadline) { + sc_tick now = sc_tick_now(); + if (deadline <= now) { + return false; // timeout + } + + uint32_t ms = SC_TICK_TO_MS(deadline - now); int r = SDL_CondWaitTimeout(cond->cond, mutex->mutex, ms); #ifndef NDEBUG if (r < 0) { diff --git a/app/src/util/thread.h b/app/src/util/thread.h index dd3a630e91..7add6f1c2a 100644 --- a/app/src/util/thread.h +++ b/app/src/util/thread.h @@ -5,7 +5,8 @@ #include #include -#include + +#include "tick.h" /* Forward declarations */ typedef struct SDL_Thread SDL_Thread; @@ -72,7 +73,7 @@ sc_cond_wait(sc_cond *cond, sc_mutex *mutex); // return true on signaled, false on timeout bool -sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, uint32_t ms); +sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick deadline); void sc_cond_signal(sc_cond *cond); diff --git a/app/src/util/tick.c b/app/src/util/tick.c new file mode 100644 index 0000000000..b85ce9711b --- /dev/null +++ b/app/src/util/tick.c @@ -0,0 +1,16 @@ +#include "tick.h" + +#include + +sc_tick +sc_tick_now(void) { + // SDL_GetTicks() resolution is in milliseconds, but sc_tick are expressed + // in microseconds to store PTS without precision loss. + // + // As an alternative, SDL_GetPerformanceCounter() and + // SDL_GetPerformanceFrequency() could be used, but: + // - the conversions (avoiding overflow) are expansive, since the + // frequency is not known at compile time; + // - in practice, we don't need more precision for now. + return (sc_tick) SDL_GetTicks() * 1000; +} diff --git a/app/src/util/tick.h b/app/src/util/tick.h new file mode 100644 index 0000000000..472a18a7ac --- /dev/null +++ b/app/src/util/tick.h @@ -0,0 +1,21 @@ +#ifndef SC_TICK_H +#define SC_TICK_H + +#include + +typedef int64_t sc_tick; +#define PRItick PRIi64 +#define SC_TICK_FREQ 1000000 // microsecond + +// To be adapted if SC_TICK_FREQ changes +#define SC_TICK_TO_US(tick) (tick) +#define SC_TICK_TO_MS(tick) ((tick) / 1000) +#define SC_TICK_TO_SEC(tick) ((tick) / 1000000) +#define SC_TICK_FROM_US(us) (us) +#define SC_TICK_FROM_MS(ms) ((ms) * 1000) +#define SC_TICK_FROM_SEC(sec) ((sec) * 1000000) + +sc_tick +sc_tick_now(void); + +#endif diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 5ee9c8eb9a..cae3eee9e0 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -121,11 +121,11 @@ run_v4l2_sink(void *data) { break; } - video_buffer_consume(&vs->vb, vs->frame); vs->has_frame = false; - sc_mutex_unlock(&vs->mutex); + sc_video_buffer_consume(&vs->vb, vs->frame); + bool ok = encode_and_write_frame(vs, vs->frame); av_frame_unref(vs->frame); if (!ok) { @@ -139,18 +139,42 @@ run_v4l2_sink(void *data) { return 0; } +static void +sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped, + void *userdata) { + (void) vb; + struct sc_v4l2_sink *vs = userdata; + + if (!previous_skipped) { + sc_mutex_lock(&vs->mutex); + vs->has_frame = true; + sc_cond_signal(&vs->cond); + sc_mutex_unlock(&vs->mutex); + } +} + static bool sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { - bool ok = video_buffer_init(&vs->vb); + static const struct sc_video_buffer_callbacks cbs = { + .on_new_frame = sc_video_buffer_on_new_frame, + }; + + bool ok = sc_video_buffer_init(&vs->vb, vs->buffering_time, &cbs, vs); if (!ok) { LOGE("Could not initialize video buffer"); return false; } + ok = sc_video_buffer_start(&vs->vb); + if (!ok) { + LOGE("Could not start video buffer"); + goto error_video_buffer_destroy; + } + ok = sc_mutex_init(&vs->mutex); if (!ok) { LOGC("Could not create mutex"); - goto error_video_buffer_destroy; + goto error_video_buffer_stop_and_join; } ok = sc_cond_init(&vs->cond); @@ -275,8 +299,11 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) { sc_cond_destroy(&vs->cond); error_mutex_destroy: sc_mutex_destroy(&vs->mutex); +error_video_buffer_stop_and_join: + sc_video_buffer_stop(&vs->vb); + sc_video_buffer_join(&vs->vb); error_video_buffer_destroy: - video_buffer_destroy(&vs->vb); + sc_video_buffer_destroy(&vs->vb); return false; } @@ -288,7 +315,10 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) { sc_cond_signal(&vs->cond); sc_mutex_unlock(&vs->mutex); + sc_video_buffer_stop(&vs->vb); + sc_thread_join(&vs->thread, NULL); + sc_video_buffer_join(&vs->vb); av_packet_free(&vs->packet); av_frame_free(&vs->frame); @@ -298,24 +328,12 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) { avformat_free_context(vs->format_ctx); sc_cond_destroy(&vs->cond); sc_mutex_destroy(&vs->mutex); - video_buffer_destroy(&vs->vb); + sc_video_buffer_destroy(&vs->vb); } static bool sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) { - sc_mutex_lock(&vs->mutex); - - bool ok = video_buffer_push(&vs->vb, frame, NULL); - if (!ok) { - return false; - } - - vs->has_frame = true; - sc_cond_signal(&vs->cond); - - sc_mutex_unlock(&vs->mutex); - - return true; + return sc_video_buffer_push(&vs->vb, frame); } static bool @@ -338,7 +356,7 @@ sc_v4l2_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { bool sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, - struct size frame_size) { + struct size frame_size, sc_tick buffering_time) { vs->device_name = strdup(device_name); if (!vs->device_name) { LOGE("Could not strdup v4l2 device name"); @@ -346,6 +364,7 @@ sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, } vs->frame_size = frame_size; + vs->buffering_time = buffering_time; static const struct sc_frame_sink_ops ops = { .open = sc_v4l2_frame_sink_open, diff --git a/app/src/v4l2_sink.h b/app/src/v4l2_sink.h index aad2592671..6773cd26e2 100644 --- a/app/src/v4l2_sink.h +++ b/app/src/v4l2_sink.h @@ -6,18 +6,20 @@ #include "coords.h" #include "trait/frame_sink.h" #include "video_buffer.h" +#include "util/tick.h" #include struct sc_v4l2_sink { struct sc_frame_sink frame_sink; // frame sink trait - struct video_buffer vb; + struct sc_video_buffer vb; AVFormatContext *format_ctx; AVCodecContext *encoder_ctx; char *device_name; struct size frame_size; + sc_tick buffering_time; sc_thread thread; sc_mutex mutex; @@ -32,7 +34,7 @@ struct sc_v4l2_sink { bool sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name, - struct size frame_size); + struct size frame_size, sc_tick buffering_time); void sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs); diff --git a/app/src/video_buffer.c b/app/src/video_buffer.c index 7adf098bc6..f71a4e7882 100644 --- a/app/src/video_buffer.c +++ b/app/src/video_buffer.c @@ -1,88 +1,255 @@ #include "video_buffer.h" #include +#include + #include #include #include "util/log.h" -bool -video_buffer_init(struct video_buffer *vb) { - vb->pending_frame = av_frame_alloc(); - if (!vb->pending_frame) { - return false; +#define SC_BUFFERING_NDEBUG // comment to debug + +static struct sc_video_buffer_frame * +sc_video_buffer_frame_new(const AVFrame *frame) { + struct sc_video_buffer_frame *vb_frame = malloc(sizeof(*vb_frame)); + if (!vb_frame) { + return NULL; + } + + vb_frame->frame = av_frame_alloc(); + if (!vb_frame->frame) { + free(vb_frame); + return NULL; + } + + if (av_frame_ref(vb_frame->frame, frame)) { + av_frame_free(&vb_frame->frame); + free(vb_frame); + return NULL; } - vb->tmp_frame = av_frame_alloc(); - if (!vb->tmp_frame) { - av_frame_free(&vb->pending_frame); + return vb_frame; +} + +static void +sc_video_buffer_frame_delete(struct sc_video_buffer_frame *vb_frame) { + av_frame_unref(vb_frame->frame); + av_frame_free(&vb_frame->frame); + free(vb_frame); +} + +static bool +sc_video_buffer_offer(struct sc_video_buffer *vb, const AVFrame *frame) { + bool previous_skipped; + bool ok = sc_frame_buffer_push(&vb->fb, frame, &previous_skipped); + if (!ok) { return false; } - bool ok = sc_mutex_init(&vb->mutex); + vb->cbs->on_new_frame(vb, previous_skipped, vb->cbs_userdata); + return true; +} + +static int +run_buffering(void *data) { + struct sc_video_buffer *vb = data; + + assert(vb->buffering_time > 0); + + for (;;) { + sc_mutex_lock(&vb->b.mutex); + + while (!vb->b.stopped && sc_queue_is_empty(&vb->b.queue)) { + sc_cond_wait(&vb->b.queue_cond, &vb->b.mutex); + } + + if (vb->b.stopped) { + sc_mutex_unlock(&vb->b.mutex); + goto stopped; + } + + struct sc_video_buffer_frame *vb_frame; + sc_queue_take(&vb->b.queue, next, &vb_frame); + + sc_tick max_deadline = sc_tick_now() + vb->buffering_time; + // PTS (written by the server) are expressed in microseconds + sc_tick pts = SC_TICK_TO_US(vb_frame->frame->pts); + + bool timed_out = false; + while (!vb->b.stopped && !timed_out) { + sc_tick deadline = sc_clock_to_system_time(&vb->b.clock, pts) + + vb->buffering_time; + if (deadline > max_deadline) { + deadline = max_deadline; + } + + timed_out = + !sc_cond_timedwait(&vb->b.wait_cond, &vb->b.mutex, deadline); + } + + if (vb->b.stopped) { + sc_video_buffer_frame_delete(vb_frame); + sc_mutex_unlock(&vb->b.mutex); + goto stopped; + } + + sc_mutex_unlock(&vb->b.mutex); + +#ifndef SC_BUFFERING_NDEBUG + LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick, + pts, vb_frame->push_date, sc_tick_now()); +#endif + + sc_video_buffer_offer(vb, vb_frame->frame); + + sc_video_buffer_frame_delete(vb_frame); + } + +stopped: + // Flush queue + while (!sc_queue_is_empty(&vb->b.queue)) { + struct sc_video_buffer_frame *vb_frame; + sc_queue_take(&vb->b.queue, next, &vb_frame); + sc_video_buffer_frame_delete(vb_frame); + } + + LOGD("Buffering thread ended"); + + return 0; +} + +bool +sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time, + const struct sc_video_buffer_callbacks *cbs, + void *cbs_userdata) { + bool ok = sc_frame_buffer_init(&vb->fb); if (!ok) { - av_frame_free(&vb->pending_frame); - av_frame_free(&vb->tmp_frame); return false; } - // there is initially no frame, so consider it has already been consumed - vb->pending_frame_consumed = true; + assert(buffering_time >= 0); + if (buffering_time) { + ok = sc_mutex_init(&vb->b.mutex); + if (!ok) { + LOGC("Could not create mutex"); + sc_frame_buffer_destroy(&vb->fb); + return false; + } + + ok = sc_cond_init(&vb->b.queue_cond); + if (!ok) { + LOGC("Could not create cond"); + sc_mutex_destroy(&vb->b.mutex); + sc_frame_buffer_destroy(&vb->fb); + return false; + } + + ok = sc_cond_init(&vb->b.wait_cond); + if (!ok) { + LOGC("Could not create wait cond"); + sc_cond_destroy(&vb->b.queue_cond); + sc_mutex_destroy(&vb->b.mutex); + sc_frame_buffer_destroy(&vb->fb); + return false; + } + + sc_clock_init(&vb->b.clock); + sc_queue_init(&vb->b.queue); + } + + assert(cbs); + assert(cbs->on_new_frame); + + vb->buffering_time = buffering_time; + vb->cbs = cbs; + vb->cbs_userdata = cbs_userdata; + return true; +} + +bool +sc_video_buffer_start(struct sc_video_buffer *vb) { + if (vb->buffering_time) { + bool ok = + sc_thread_create(&vb->b.thread, run_buffering, "buffering", vb); + if (!ok) { + LOGE("Could not start buffering thread"); + return false; + } + } return true; } void -video_buffer_destroy(struct video_buffer *vb) { - sc_mutex_destroy(&vb->mutex); - av_frame_free(&vb->pending_frame); - av_frame_free(&vb->tmp_frame); +sc_video_buffer_stop(struct sc_video_buffer *vb) { + if (vb->buffering_time) { + sc_mutex_lock(&vb->b.mutex); + vb->b.stopped = true; + sc_cond_signal(&vb->b.queue_cond); + sc_cond_signal(&vb->b.wait_cond); + sc_mutex_unlock(&vb->b.mutex); + } +} + +void +sc_video_buffer_join(struct sc_video_buffer *vb) { + if (vb->buffering_time) { + sc_thread_join(&vb->b.thread, NULL); + } } -static inline void -swap_frames(AVFrame **lhs, AVFrame **rhs) { - AVFrame *tmp = *lhs; - *lhs = *rhs; - *rhs = tmp; +void +sc_video_buffer_destroy(struct sc_video_buffer *vb) { + sc_frame_buffer_destroy(&vb->fb); + if (vb->buffering_time) { + sc_cond_destroy(&vb->b.wait_cond); + sc_cond_destroy(&vb->b.queue_cond); + sc_mutex_destroy(&vb->b.mutex); + } } bool -video_buffer_push(struct video_buffer *vb, const AVFrame *frame, - bool *previous_frame_skipped) { - sc_mutex_lock(&vb->mutex); - - // Use a temporary frame to preserve pending_frame in case of error. - // tmp_frame is an empty frame, no need to call av_frame_unref() beforehand. - int r = av_frame_ref(vb->tmp_frame, frame); - if (r) { - LOGE("Could not ref frame: %d", r); - return false; +sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame) { + if (!vb->buffering_time) { + // No buffering + return sc_video_buffer_offer(vb, frame); } - // Now that av_frame_ref() succeeded, we can replace the previous - // pending_frame - swap_frames(&vb->pending_frame, &vb->tmp_frame); - av_frame_unref(vb->tmp_frame); + sc_mutex_lock(&vb->b.mutex); - if (previous_frame_skipped) { - *previous_frame_skipped = !vb->pending_frame_consumed; + sc_tick pts = SC_TICK_FROM_US(frame->pts); + sc_clock_update(&vb->b.clock, sc_tick_now(), pts); + sc_cond_signal(&vb->b.wait_cond); + + if (vb->b.clock.count == 1) { + sc_mutex_unlock(&vb->b.mutex); + // First frame, offer it immediately, for two reasons: + // - not to delay the opening of the scrcpy window + // - the buffering estimation needs at least two clock points, so it + // could not handle the first frame + return sc_video_buffer_offer(vb, frame); + } + + struct sc_video_buffer_frame *vb_frame = sc_video_buffer_frame_new(frame); + if (!vb_frame) { + sc_mutex_unlock(&vb->b.mutex); + LOGE("Could not allocate frame"); + return false; } - vb->pending_frame_consumed = false; - sc_mutex_unlock(&vb->mutex); +#ifndef SC_BUFFERING_NDEBUG + vb_frame->push_date = sc_tick_now(); +#endif + sc_queue_push(&vb->b.queue, next, vb_frame); + sc_cond_signal(&vb->b.queue_cond); + + sc_mutex_unlock(&vb->b.mutex); return true; } void -video_buffer_consume(struct video_buffer *vb, AVFrame *dst) { - sc_mutex_lock(&vb->mutex); - assert(!vb->pending_frame_consumed); - vb->pending_frame_consumed = true; - - av_frame_move_ref(dst, vb->pending_frame); - // av_frame_move_ref() resets its source frame, so no need to call - // av_frame_unref() - - sc_mutex_unlock(&vb->mutex); +sc_video_buffer_consume(struct sc_video_buffer *vb, AVFrame *dst) { + sc_frame_buffer_consume(&vb->fb, dst); } diff --git a/app/src/video_buffer.h b/app/src/video_buffer.h index c4d1d1a488..4877770328 100644 --- a/app/src/video_buffer.h +++ b/app/src/video_buffer.h @@ -1,43 +1,76 @@ -#ifndef VIDEO_BUFFER_H -#define VIDEO_BUFFER_H +#ifndef SC_VIDEO_BUFFER_H +#define SC_VIDEO_BUFFER_H #include "common.h" #include +#include "clock.h" +#include "frame_buffer.h" +#include "util/queue.h" #include "util/thread.h" +#include "util/tick.h" // forward declarations typedef struct AVFrame AVFrame; -/** - * A video buffer holds 1 pending frame, which is the last frame received from - * the producer (typically, the decoder). - * - * If a pending frame has not been consumed when the producer pushes a new - * frame, then it is lost. The intent is to always provide access to the very - * last frame to minimize latency. - */ +struct sc_video_buffer_frame { + AVFrame *frame; + struct sc_video_buffer_frame *next; +#ifndef NDEBUG + sc_tick push_date; +#endif +}; + +struct sc_video_buffer_frame_queue SC_QUEUE(struct sc_video_buffer_frame); + +struct sc_video_buffer { + struct sc_frame_buffer fb; -struct video_buffer { - AVFrame *pending_frame; - AVFrame *tmp_frame; // To preserve the pending frame on error + sc_tick buffering_time; - sc_mutex mutex; + // only if buffering_time > 0 + struct { + sc_thread thread; + sc_mutex mutex; + sc_cond queue_cond; + sc_cond wait_cond; + + struct sc_clock clock; + struct sc_video_buffer_frame_queue queue; + bool stopped; + } b; // buffering + + const struct sc_video_buffer_callbacks *cbs; + void *cbs_userdata; +}; - bool pending_frame_consumed; +struct sc_video_buffer_callbacks { + void (*on_new_frame)(struct sc_video_buffer *vb, bool previous_skipped, + void *userdata); }; bool -video_buffer_init(struct video_buffer *vb); +sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time, + const struct sc_video_buffer_callbacks *cbs, + void *cbs_userdata); + +bool +sc_video_buffer_start(struct sc_video_buffer *vb); + +void +sc_video_buffer_stop(struct sc_video_buffer *vb); + +void +sc_video_buffer_join(struct sc_video_buffer *vb); void -video_buffer_destroy(struct video_buffer *vb); +sc_video_buffer_destroy(struct sc_video_buffer *vb); bool -video_buffer_push(struct video_buffer *vb, const AVFrame *frame, bool *skipped); +sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame); void -video_buffer_consume(struct video_buffer *vb, AVFrame *dst); +sc_video_buffer_consume(struct sc_video_buffer *vb, AVFrame *dst); #endif diff --git a/app/tests/test_clock.c b/app/tests/test_clock.c new file mode 100644 index 0000000000..a88d580006 --- /dev/null +++ b/app/tests/test_clock.c @@ -0,0 +1,79 @@ +#include "common.h" + +#include + +#include "clock.h" + +void test_small_rolling_sum(void) { + struct sc_clock clock; + sc_clock_init(&clock); + + assert(clock.count == 0); + assert(clock.left_sum.system == 0); + assert(clock.left_sum.stream == 0); + assert(clock.right_sum.system == 0); + assert(clock.right_sum.stream == 0); + + sc_clock_update(&clock, 2, 3); + assert(clock.count == 1); + assert(clock.left_sum.system == 0); + assert(clock.left_sum.stream == 0); + assert(clock.right_sum.system == 2); + assert(clock.right_sum.stream == 3); + + sc_clock_update(&clock, 10, 20); + assert(clock.count == 2); + assert(clock.left_sum.system == 2); + assert(clock.left_sum.stream == 3); + assert(clock.right_sum.system == 10); + assert(clock.right_sum.stream == 20); + + sc_clock_update(&clock, 40, 80); + assert(clock.count == 3); + assert(clock.left_sum.system == 2); + assert(clock.left_sum.stream == 3); + assert(clock.right_sum.system == 50); + assert(clock.right_sum.stream == 100); + + sc_clock_update(&clock, 400, 800); + assert(clock.count == 4); + assert(clock.left_sum.system == 12); + assert(clock.left_sum.stream == 23); + assert(clock.right_sum.system == 440); + assert(clock.right_sum.stream == 880); +} + +void test_large_rolling_sum(void) { + const unsigned half_range = SC_CLOCK_RANGE / 2; + + struct sc_clock clock1; + sc_clock_init(&clock1); + for (unsigned i = 0; i < 5 * half_range; ++i) { + sc_clock_update(&clock1, i, 2 * i + 1); + } + + struct sc_clock clock2; + sc_clock_init(&clock2); + for (unsigned i = 3 * half_range; i < 5 * half_range; ++i) { + sc_clock_update(&clock2, i, 2 * i + 1); + } + + assert(clock1.count == SC_CLOCK_RANGE); + assert(clock2.count == SC_CLOCK_RANGE); + + // The values before the last SC_CLOCK_RANGE points in clock1 should have + // no impact + assert(clock1.left_sum.system == clock2.left_sum.system); + assert(clock1.left_sum.stream == clock2.left_sum.stream); + assert(clock1.right_sum.system == clock2.right_sum.system); + assert(clock1.right_sum.stream == clock2.right_sum.stream); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_small_rolling_sum(); + test_large_rolling_sum(); + return 0; +};