From alexcon314 at gmail.com Fri Jul 10 12:29:52 2026 From: alexcon314 at gmail.com (Alex Konovalov) Date: Fri, 10 Jul 2026 15:29:52 +0300 Subject: [x264-devel] [RFC PATCH 1/1] encoder: decouple VUI timing signaling from internal RC/HRD timebase In-Reply-To: <20260710123021.421936-1-alexcon314@gmail.com> References: <20260710123021.421936-1-alexcon314@gmail.com> Message-ID: <20260710123021.421936-2-alexcon314@gmail.com> From: alexcon --- encoder/ratecontrol.c | 38 +++++++++++++++++++------------------- encoder/set.c | 6 +++--- encoder/slicetype.c | 14 +++++++------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/encoder/ratecontrol.c b/encoder/ratecontrol.c index 109ba8ff..d1661cc5 100644 --- a/encoder/ratecontrol.c +++ b/encoder/ratecontrol.c @@ -732,7 +732,7 @@ void x264_ratecontrol_init_reconfigurable( x264_t *h, int b_init ) h->param.rc.f_vbv_buffer_init = x264_clip3f( h->param.rc.f_vbv_buffer_init / h->param.rc.i_vbv_buffer_size, 0, 1 ); h->param.rc.f_vbv_buffer_init = x264_clip3f( X264_MAX( h->param.rc.f_vbv_buffer_init, rc->buffer_rate / rc->buffer_size ), 0, 1); rc->buffer_fill_final = - rc->buffer_fill_final_min = rc->buffer_size * h->param.rc.f_vbv_buffer_init * h->sps->vui.i_time_scale; + rc->buffer_fill_final_min = rc->buffer_size * h->param.rc.f_vbv_buffer_init * (h->param.i_timebase_den * 2); rc->b_vbv = 1; rc->b_vbv_min_rate = !rc->b_2pass && h->param.rc.i_rc_method == X264_RC_ABR @@ -1458,7 +1458,7 @@ void x264_ratecontrol_start( x264_t *h, int i_force_qp, int overhead ) memset( h->fdec->f_row_qp, 0, h->mb.i_mb_height * sizeof(float) ); memset( h->fdec->f_row_qscale, 0, h->mb.i_mb_height * sizeof(float) ); rc->row_pred = rc->row_preds[h->sh.i_type]; - rc->buffer_rate = h->fenc->i_cpb_duration * rc->vbv_max_rate * h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale; + rc->buffer_rate = h->fenc->i_cpb_duration * rc->vbv_max_rate * h->param.i_timebase_num / (h->param.i_timebase_den * 2); update_vbv_plan( h, overhead ); const x264_level_t *l = x264_levels; @@ -1486,7 +1486,7 @@ void x264_ratecontrol_start( x264_t *h, int i_force_qp, int overhead ) else { //384 * MaxMBPS * ( tr( n ) - tr( n - 1 ) ) / MinCR - rc->frame_size_maximum = 384 * BIT_DEPTH * ((double)h->fenc->i_cpb_duration * h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale) * l->mbps / mincr; + rc->frame_size_maximum = 384 * BIT_DEPTH * ((double)h->fenc->i_cpb_duration * h->param.i_timebase_num / (h->param.i_timebase_den * 2)) * l->mbps / mincr; } } } @@ -1960,7 +1960,7 @@ int x264_ratecontrol_end( x264_t *h, int bits, int *filler ) else { h->fenc->hrd_timing.cpb_removal_time = rc->nrt_first_access_unit + (double)(h->fenc->i_cpb_delay - h->i_cpb_delay_pir_offset) * - h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale; + h->param.i_timebase_num / (h->param.i_timebase_den * 2); if( h->fenc->b_keyframe ) { @@ -1983,7 +1983,7 @@ int x264_ratecontrol_end( x264_t *h, int bits, int *filler ) h->fenc->hrd_timing.cpb_final_arrival_time = rc->previous_cpb_final_arrival_time = h->fenc->hrd_timing.cpb_initial_arrival_time + (double)(bits + filler_bits) / h->sps->vui.hrd.i_bit_rate_unscaled; - h->fenc->hrd_timing.dpb_output_time = (double)h->fenc->i_dpb_output_delay * h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale + + h->fenc->hrd_timing.dpb_output_time = (double)h->fenc->i_dpb_output_delay * h->param.i_timebase_num / (h->param.i_timebase_den * 2) + h->fenc->hrd_timing.cpb_removal_time; } @@ -2007,7 +2007,7 @@ static double get_qscale(x264_t *h, ratecontrol_entry_t *rce, double rate_factor double q; if( h->param.rc.b_mb_tree ) { - double timescale = (double)h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale; + double timescale = (double)h->param.i_timebase_num / (h->param.i_timebase_den * 2); q = pow( BASE_FRAME_DURATION / CLIP_DURATION(rce->i_duration * timescale), 1 - h->param.rc.f_qcompress ); } else @@ -2140,7 +2140,7 @@ static int update_vbv( x264_t *h, int bits ) int bitrate = h->sps->vui.hrd.i_bit_rate_unscaled; x264_ratecontrol_t *rcc = h->rc; x264_ratecontrol_t *rct = h->thread[0]->rc; - int64_t buffer_size = (int64_t)h->sps->vui.hrd.i_cpb_size_unscaled * h->sps->vui.i_time_scale; + int64_t buffer_size = (int64_t)h->sps->vui.hrd.i_cpb_size_unscaled * (h->param.i_timebase_den * 2); if( rcc->last_satd >= h->mb.i_mb_count ) update_predictor( &rct->pred[h->sh.i_type], qp2qscale( rcc->qpa_rc ), rcc->last_satd, bits ); @@ -2148,13 +2148,13 @@ static int update_vbv( x264_t *h, int bits ) if( !rcc->b_vbv ) return filler; - uint64_t buffer_diff = (uint64_t)bits * h->sps->vui.i_time_scale; + uint64_t buffer_diff = (uint64_t)bits * (h->param.i_timebase_den * 2); rct->buffer_fill_final -= buffer_diff; rct->buffer_fill_final_min -= buffer_diff; if( rct->buffer_fill_final_min < 0 ) { - double underflow = (double)rct->buffer_fill_final_min / h->sps->vui.i_time_scale; + double underflow = (double)rct->buffer_fill_final_min / (h->param.i_timebase_den * 2); if( rcc->rate_factor_max_increment && rcc->qpm >= rcc->qp_novbv + rcc->rate_factor_max_increment ) x264_log( h, X264_LOG_DEBUG, "VBV underflow due to CRF-max (frame %d, %.0f bits)\n", h->i_frame, underflow ); else @@ -2166,7 +2166,7 @@ static int update_vbv( x264_t *h, int bits ) if( h->param.i_avcintra_class ) buffer_diff = buffer_size; else - buffer_diff = (uint64_t)bitrate * h->sps->vui.i_num_units_in_tick * h->fenc->i_cpb_duration; + buffer_diff = (uint64_t)bitrate * h->param.i_timebase_num * h->fenc->i_cpb_duration; rct->buffer_fill_final += buffer_diff; rct->buffer_fill_final_min += buffer_diff; @@ -2174,10 +2174,10 @@ static int update_vbv( x264_t *h, int bits ) { if( h->param.rc.b_filler ) { - int64_t scale = (int64_t)h->sps->vui.i_time_scale * 8; + int64_t scale = (int64_t)(h->param.i_timebase_den * 2) * 8; filler = (rct->buffer_fill_final - buffer_size + scale - 1) / scale; bits = h->param.i_avcintra_class ? filler * 8 : X264_MAX( (FILLER_OVERHEAD - h->param.b_annexb), filler ) * 8; - buffer_diff = (uint64_t)bits * h->sps->vui.i_time_scale; + buffer_diff = (uint64_t)bits * (h->param.i_timebase_den * 2); rct->buffer_fill_final -= buffer_diff; rct->buffer_fill_final_min -= buffer_diff; } @@ -2194,16 +2194,16 @@ static int update_vbv( x264_t *h, int bits ) void x264_hrd_fullness( x264_t *h ) { x264_ratecontrol_t *rct = h->thread[0]->rc; - uint64_t denom = (uint64_t)h->sps->vui.hrd.i_bit_rate_unscaled * h->sps->vui.i_time_scale / rct->hrd_multiply_denom; + uint64_t denom = (uint64_t)h->sps->vui.hrd.i_bit_rate_unscaled * (h->param.i_timebase_den * 2) / rct->hrd_multiply_denom; uint64_t cpb_state = rct->buffer_fill_final; - uint64_t cpb_size = (uint64_t)h->sps->vui.hrd.i_cpb_size_unscaled * h->sps->vui.i_time_scale; + uint64_t cpb_size = (uint64_t)h->sps->vui.hrd.i_cpb_size_unscaled * (h->param.i_timebase_den * 2); uint64_t multiply_factor = 90000 / rct->hrd_multiply_denom; if( rct->buffer_fill_final < 0 || rct->buffer_fill_final > (int64_t)cpb_size ) { x264_log( h, X264_LOG_WARNING, "CPB %s: %.0f bits in a %.0f-bit buffer\n", rct->buffer_fill_final < 0 ? "underflow" : "overflow", - (double)rct->buffer_fill_final / h->sps->vui.i_time_scale, (double)cpb_size / h->sps->vui.i_time_scale ); + (double)rct->buffer_fill_final / (h->param.i_timebase_den * 2), (double)cpb_size / (h->param.i_timebase_den * 2) ); } h->initial_cpb_removal_delay = (multiply_factor * cpb_state) / denom; @@ -2217,7 +2217,7 @@ void x264_hrd_fullness( x264_t *h ) static void update_vbv_plan( x264_t *h, int overhead ) { x264_ratecontrol_t *rcc = h->rc; - rcc->buffer_fill = h->thread[0]->rc->buffer_fill_final_min / h->sps->vui.i_time_scale; + rcc->buffer_fill = h->thread[0]->rc->buffer_fill_final_min / (h->param.i_timebase_den * 2); if( h->i_thread_frames > 1 ) { int j = rcc - h->thread[0]->rc; @@ -2273,7 +2273,7 @@ static double vbv_pass1( x264_t *h, int pict_type, double q ) { double q0 = q; double fenc_cpb_duration = (double)h->fenc->i_cpb_duration * - h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale; + h->param.i_timebase_num / (h->param.i_timebase_den * 2); /* Lookahead VBV: raise the quantizer as necessary such that no frames in * the lookahead overflow and such that the buffer is in a reasonable state * by the end of the lookahead. */ @@ -2815,7 +2815,7 @@ static int find_underflow( x264_t *h, double *fills, int *t0, int *t1, int over int start = -1, end = -1; for( int i = *t0; i < rcc->num_entries; i++ ) { - fill += (rcc->entry_out[i]->i_cpb_duration * rcc->vbv_max_rate * h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale - + fill += (rcc->entry_out[i]->i_cpb_duration * rcc->vbv_max_rate * h->param.i_timebase_num / (h->param.i_timebase_den * 2) - qscale2bits( rcc->entry_out[i], rcc->entry_out[i]->new_qscale )) * parity; fill = x264_clip3f(fill, 0, rcc->buffer_size); fills[i] = fill; @@ -2933,7 +2933,7 @@ static int init_pass2( x264_t *h ) { x264_ratecontrol_t *rcc = h->rc; uint64_t all_const_bits = 0; - double timescale = (double)h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale; + double timescale = (double)h->param.i_timebase_num / (h->param.i_timebase_den * 2); double duration = 0; for( int i = 0; i < rcc->num_entries; i++ ) duration += rcc->entry[i].i_duration; diff --git a/encoder/set.c b/encoder/set.c index 70c67aee..e5909077 100644 --- a/encoder/set.c +++ b/encoder/set.c @@ -216,12 +216,12 @@ void x264_sps_init( x264_sps_t *sps, int i_id, x264_param_t *param ) sps->vui.i_chroma_loc_bottom = param->vui.i_chroma_loc; } - sps->vui.b_timing_info_present = param->i_timebase_num > 0 && param->i_timebase_den > 0; + sps->vui.b_timing_info_present = param->i_fps_num > 0 && param->i_fps_den > 0; if( sps->vui.b_timing_info_present ) { - sps->vui.i_num_units_in_tick = param->i_timebase_num; - sps->vui.i_time_scale = param->i_timebase_den * 2; + sps->vui.i_num_units_in_tick = param->i_fps_den; + sps->vui.i_time_scale = param->i_fps_num * 2; sps->vui.b_fixed_frame_rate = !param->b_vfr_input; } diff --git a/encoder/slicetype.c b/encoder/slicetype.c index 278a8fde..88b07c43 100644 --- a/encoder/slicetype.c +++ b/encoder/slicetype.c @@ -1251,10 +1251,10 @@ static void vbv_lookahead( x264_t *h, x264_mb_analysis_t *a, x264_frame_t **fram if( prev_frame ) { frames[next_nonb]->f_planned_cpb_duration[prev_frame_idx] = (double)prev_frame->i_cpb_duration * - h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale; + h->param.i_timebase_num / (h->param.i_timebase_den * 2); } frames[next_nonb]->f_planned_cpb_duration[idx] = (double)frames[cur_nonb]->i_cpb_duration * - h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale; + h->param.i_timebase_num / (h->param.i_timebase_den * 2); prev_frame = frames[cur_nonb]; prev_frame_idx = idx; idx++; @@ -1270,10 +1270,10 @@ static void vbv_lookahead( x264_t *h, x264_mb_analysis_t *a, x264_frame_t **fram if( prev_frame ) { frames[next_nonb]->f_planned_cpb_duration[prev_frame_idx] = (double)prev_frame->i_cpb_duration * - h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale; + h->param.i_timebase_num / (h->param.i_timebase_den * 2); } frames[next_nonb]->f_planned_cpb_duration[idx] = (double)frames[i]->i_cpb_duration * - h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale; + h->param.i_timebase_num / (h->param.i_timebase_den * 2); prev_frame = frames[i]; prev_frame_idx = idx; } @@ -1767,8 +1767,8 @@ void x264_slicetype_decide( x264_t *h ) h->lookahead->next.list[i]->i_duration = delta_tfi_divisor[h->lookahead->next.list[i]->i_pic_struct]; h->i_prev_duration = h->lookahead->next.list[i]->i_duration; h->lookahead->next.list[i]->f_duration = (double)h->lookahead->next.list[i]->i_duration - * h->sps->vui.i_num_units_in_tick - / h->sps->vui.i_time_scale; + * h->param.i_timebase_num / (h->param.i_timebase_den * 2); + if( h->lookahead->next.list[i]->i_frame > h->i_disp_fields_last_frame && lookahead_size > 0 ) { @@ -1966,7 +1966,7 @@ void x264_slicetype_decide( x264_t *h ) { calculate_durations( h, h->lookahead->next.list[i], h->lookahead->next.list[i-1], &h->i_cpb_delay, &h->i_coded_fields ); h->lookahead->next.list[0]->f_planned_cpb_duration[i-1] = (double)h->lookahead->next.list[i]->i_cpb_duration * - h->sps->vui.i_num_units_in_tick / h->sps->vui.i_time_scale; + h->param.i_timebase_num / (h->param.i_timebase_den * 2); } else calculate_durations( h, h->lookahead->next.list[i], NULL, &h->i_cpb_delay, &h->i_coded_fields ); -- 2.47.3 From alexcon314 at gmail.com Fri Jul 10 12:29:51 2026 From: alexcon314 at gmail.com (Alex Konovalov) Date: Fri, 10 Jul 2026 15:29:51 +0300 Subject: [x264-devel] [RFC PATCH 0/1] encoder: decouple VUI timing signaling from RC/HRD timebase Message-ID: <20260710123021.421936-1-alexcon314@gmail.com> From: alexcon Hi, I'm carrying this downstream in FFmpeg+x264 setups and would like feedback before proposing it as a non-RFC patch. This patch is based on x264 commit 4613ac3c15fd75cebc4b9f65b7fb95e70a3acce1. In pipelines where nominal FPS and encoder timebase diverge, x264 currently derives SPS/VUI timing from timebase and also uses the same timing scale in RC/HRD paths. In practice this can lead to inconsistent timing signaling (e.g. SPS-derived r_frame_rate becoming unrealistic in mismatch scenarios). Minimal repro (unpatched vs patched on same source): 1) Generate source: ffmpeg -hide_banner -nostats -loglevel error -y \ -f lavfi -i testsrc2=size=1280x720:rate=25 -t 2 -pix_fmt yuv420p /tmp/repro-src.y4m 2) Create timecode file: python3 - <<'PY' with open('/tmp/repro.tc','w') as f: f.write('# timecode format v2\n') for i in range(50): f.write(f'{i*40}\n') PY 3) Encode with x264: x264 --input-csp i420 --input-res 1280x720 \ --timebase 1/90000 --tcfile-in /tmp/repro.tc --frames 50 \ --vbv-maxrate 1000 --vbv-bufsize 1000 --nal-hrd vbr --bitrate 1000 \ -o /tmp/out.264 /tmp/repro-src.y4m 4) Inspect SPS timing: ffmpeg -hide_banner -i /tmp/out.264 -c copy -bsf:v trace_headers -f null - 2> /tmp/trace.log rg "num_units_in_tick|time_scale|fixed_frame_rate_flag" /tmp/trace.log Observed on my side: - unpatched: num_units_in_tick=1, time_scale=180000 - patched: num_units_in_tick=1, time_scale=50 5) Inspect SPS timing with ffprobe: /usr/bin/ffprobe -v error -show_streams /tmp/out.264 | rg "r_frame_rate|avg_frame_rate" ffprobe also differs accordingly: - unpatched: r_frame_rate=180000/1, avg_frame_rate=25/1 - patched: r_frame_rate=50/1, avg_frame_rate=25/1 This patch separates the two roles: - VUI timing signaling is derived from fps (i_fps_num/i_fps_den) - internal RC/HRD timing math is derived from encoder timebase (i_timebase_num/i_timebase_den) I used x265 behavior as a reference point for expected separation of signaling vs internal timing model. APIs differ, but the intent is similar. If useful, I can send v2 with any requested adjustments. Thanks. alexcon (1): encoder: decouple VUI timing signaling from internal RC/HRD timebase encoder/ratecontrol.c | 38 +++++++++++++++++++------------------- encoder/set.c | 6 +++--- encoder/slicetype.c | 14 +++++++------- 3 files changed, 29 insertions(+), 29 deletions(-) -- 2.47.3