diff --git a/app/oapv_app_enc.c b/app/oapv_app_enc.c index 710ccce..2ab7dda 100644 --- a/app/oapv_app_enc.c +++ b/app/oapv_app_enc.c @@ -35,7 +35,23 @@ #include "oapv_app_y4m.h" #define MAX_BS_BUF (128 * 1024 * 1024) -#define MAX_NUM_FRMS (1) // supports only 1-frame in an access unit +/* Worst-case AU bitstream size as a function of frame dimensions and the + * number of color components. Allows roughly 4 bytes per coefficient plus a + * fixed allowance for AU/PBU headers and metadata. Result is clamped to + * INT_MAX-1 because oapv_bitb_t.bsize is `int`. */ +#define BS_BUF_FLOOR MAX_BS_BUF +#define BS_BUF_HEADER_ALLOW (16 * 1024 * 1024) +static int compute_bs_buf_size(int w, int h, int num_comp) +{ + unsigned long long need = + (unsigned long long)w * (unsigned long long)h * + (unsigned long long)num_comp * 4ULL + (unsigned long long)BS_BUF_HEADER_ALLOW; + if(need < (unsigned long long)BS_BUF_FLOOR) need = BS_BUF_FLOOR; + if(need > (unsigned long long)0x7FFFFFF0ULL) need = 0x7FFFFFF0ULL; + return (int)need; +} +#define MAX_NUM_FRMS (OAPV_MAX_NUM_FRAMES) // TMV: supports for mips as non-primary frames in access unit +#define NUM_PRI_FRMS (1) // Supports only 1 primary frame in access unit #define FRM_IDX (0) // supports only 1-frame in an access unit #define MAX_NUM_CC (OAPV_MAX_CC) // Max number of color components (upto 4:4:4:4) @@ -284,6 +300,10 @@ static const args_opt_t enc_args_opts[] = { ARGS_NO_KEY, "max-cll", ARGS_VAL_TYPE_STRING, 0, NULL, 0, "content light level information metadata" }, + { + ARGS_NO_KEY, "tmv-mips", ARGS_VAL_TYPE_NONE, 0, NULL, 0, + "TMV - Generate mipmaps as non-primary frames in each access units." + }, {ARGS_END_KEY, "", ARGS_VAL_TYPE_NONE, 0, NULL, 0, ""} /* termination */ }; @@ -298,6 +318,7 @@ typedef struct args_var { char fname_rec[256]; int max_au; int hash; + int tmv_mips; int input_depth; int input_csp; int seek; @@ -356,6 +377,7 @@ static args_var_t *args_init_vars(args_parser_t *args, oapve_param_t *param) args_set_variable_by_key_long(opts, "recon", vars->fname_rec, sizeof(vars->fname_rec)); args_set_variable_by_key_long(opts, "max-au", &vars->max_au, 0); args_set_variable_by_key_long(opts, "hash", &vars->hash, 0); + args_set_variable_by_key_long(opts, "tmv-mips", &vars->tmv_mips, 0); args_set_variable_by_key_long(opts, "verbose", &op_verbose, 0); op_verbose = VERBOSE_SIMPLE; /* default */ args_set_variable_by_key_long(opts, "input-depth", &vars->input_depth, 0); @@ -555,7 +577,7 @@ static void print_commandline(int argc, const char **argv) static void add_thousands_comma_to_number(char *in, char *out) { - int len, left = 0; + size_t len, left = 0; len = strlen(in); left = len % 3; @@ -770,6 +792,12 @@ static int update_param(args_var_t *vars, oapve_param_t *param) UPDATE_A_PARAM_W_KEY_VAL(param, "tile-w", vars->tile_w); UPDATE_A_PARAM_W_KEY_VAL(param, "tile-h", vars->tile_h); + + /* The TMV mip/tiled workflow (--tmv-mips) produces per-AU tiled output + * meant for selective / tiled decoding, which needs per-tile sizes in the + * frame header so the decoder can index tiles. Enable the flag for that + * case; otherwise leave it off (base bitstream). */ + param->tile_size_present_in_fh_flag = vars->tmv_mips ? 1 : 0; return 0; } @@ -895,7 +923,10 @@ int main(int argc, const char **argv) int is_out = 0, is_rec = 0; char *errstr = NULL; int cfmt; // color format - const int num_frames = MAX_NUM_FRMS; // number of frames in an access unit + const int num_frames = NUM_PRI_FRMS; // number of primary frames in an access unit + int num_mips = 0; // [TMV] number of mipmaps + int start_mip_idx = 0; + char fname_out_au[256+128]; // filename for given AU when outputting one AU per file. // print logo logv2(" ____ ___ ___ _ __\n"); @@ -1016,8 +1047,20 @@ int main(int argc, const char **argv) goto ERR; } - cdesc.max_bs_buf_size = MAX_BS_BUF; /* maximum bitstream buffer size */ - cdesc.max_num_frms = MAX_NUM_FRMS; + /* Size the bitstream buffer to the worst-case AU for the chosen + * dimensions and color format, so high-resolution inputs (e.g. 16k) + * don't trigger OAPV_ERR_OUT_OF_BS_BUF. */ + { + int num_comp; + switch(cfmt) { + case OAPV_CF_YCBCR400: num_comp = 1; break; + case OAPV_CF_YCBCR4444: num_comp = 4; break; + case OAPV_CF_PLANAR2: num_comp = 3; break; + default: num_comp = 3; break; + } + cdesc.max_bs_buf_size = compute_bs_buf_size(param->w, param->h, num_comp); + } + cdesc.max_num_frms = NUM_PRI_FRMS; if(!strcmp(args_var->threads, "auto")){ cdesc.threads = OAPV_CDESC_THREADS_AUTO; } @@ -1059,14 +1102,56 @@ int main(int argc, const char **argv) is_rec = 1; } - /* allocate bitstream buffer */ - bs_buf = (unsigned char *)malloc(MAX_BS_BUF); + /* allocate bitstream buffer (sized in cdesc.max_bs_buf_size above) */ + bs_buf = (unsigned char *)malloc((size_t)cdesc.max_bs_buf_size); if(bs_buf == NULL) { - logerr("ERR: cannot allocate bitstream buffer, size=%d", MAX_BS_BUF); + logerr("ERR: cannot allocate bitstream buffer, size=%d", cdesc.max_bs_buf_size); ret = -1; goto ERR; } + // TMV -- Prep mip encoding parameters -- + num_mips = 0; + + if(args_var->tmv_mips) { + for(int mip_idx = 0, w = param->w / 2, h = param->h / 2;; mip_idx++) { + + if((cfmt == OAPV_CF_YCBCR422 || cfmt == OAPV_CF_YCBCR420) && (w & 0x1)) { + logerr("ERR: Can't generate mip of width %d. Not multiple of two (YUV 422/420 constraint).\n", w); + break; + } + + if(cfmt == OAPV_CF_YCBCR420 && (h & 0x1)) { + logerr("ERR: Can't generate mip of heigth %d. Not multiple of two (YUV 420 constraint).\n", h); + break; + } + + int frame_idx = 1 + mip_idx; // assumes 1 primary frame + cdesc.param[frame_idx] = *param; + cdesc.param[frame_idx].w = w; + cdesc.param[frame_idx].h = h; + + if(w == 1 && h == 1) { + break; + } + + num_mips++; + + w = MAX_VAL(w / 2, 1); + h = MAX_VAL(h / 2, 1); + } + + // Log mipmap levels that will be generated + logv2("Generating %d mipmap levels:\n", num_mips); + logv2(" Mip 0: %dx%d (primary frame)\n", param->w, param->h); + for(int i = 0; i < num_mips; i++) { + logv2(" Mip %d: %dx%d\n", i + 1, + cdesc.param[i + 1].w, cdesc.param[i + 1].h); + } + } + + cdesc.max_num_frms = 1 + num_mips; // TMV + /* create encoder */ id = oapve_create(&cdesc, &ret); if(id == NULL) { @@ -1093,7 +1178,7 @@ int main(int argc, const char **argv) bitrate_tot = 0; bitb.addr = bs_buf; - bitb.bsize = MAX_BS_BUF; + bitb.bsize = cdesc.max_bs_buf_size; if(args_var->seek > 0) { state = STATE_SKIPPING; @@ -1166,6 +1251,29 @@ int main(int argc, const char **argv) goto ERR; } + // --- TMV Begin + // prepare frames for the mips + start_mip_idx = ifrms.num_frms; + + if (num_mips + ifrms.num_frms >= OAPV_MAX_NUM_FRAMES) + { + num_mips = OAPV_MAX_NUM_FRAMES - ifrms.num_frms - 1; + logerr("ERR: Too many mips, clamping to %d mips.", num_mips); + } + + for (int mip_idx = 0, w = param->w/2, h = param->h/2; mip_idx < num_mips; mip_idx++) + { + int frame_idx = start_mip_idx + mip_idx; + // Allocate the mip with codec format and bitdepth directly, mips are calculated from already converted imgb. + ifrms.frm[frame_idx].imgb = imgb_create(w, h, OAPV_CS_SET(cfmt, codec_depth, 0)); + w = MAX_VAL(w / 2, 1); + h = MAX_VAL(h / 2, 1); + } + + ifrms.num_frms += num_mips; + // --- TMV End + + /* encode pictures *******************************************************/ while(args_var->max_au == 0 || (au_cnt < args_var->max_au)) { for(int i = 0; i < num_frames; i++) { @@ -1189,6 +1297,26 @@ int main(int argc, const char **argv) ifrms.frm[i].pbu_type = OAPV_PBU_TYPE_PRIMARY_FRAME; } + // TMV -- Calculcate the mipmaps and store in non-primary frame. + for (int mip_idx = 0; mip_idx < num_mips; mip_idx++) + { + int src_frm_idx = 0; + int dst_frm_idx = mip_idx + start_mip_idx; + if (mip_idx > 0) + { + src_frm_idx = start_mip_idx + mip_idx - 1; + } + + logv3("Encoding mip level %d (%dx%d)...\n", + mip_idx + 1, + ifrms.frm[dst_frm_idx].imgb->w[0], + ifrms.frm[dst_frm_idx].imgb->h[0]); + + imgb_calc_mip(ifrms.frm[dst_frm_idx].imgb, ifrms.frm[src_frm_idx].imgb); + ifrms.frm[dst_frm_idx].group_id = 2 + mip_idx; // non primary frame must have different group id. + ifrms.frm[dst_frm_idx].pbu_type = OAPV_PBU_TYPE_NON_PRIMARY_FRAME; + } + if(state == STATE_ENCODING) { /* encoding */ clk_beg = oapv_clk_get(); @@ -1205,7 +1333,7 @@ int main(int argc, const char **argv) bitrate_tot += stat.frm_size[FRM_IDX]; - print_stat_au(&stat, au_cnt, param, args_var->max_au, bitrate_tot, clk_end, clk_tot); + print_stat_au(&stat, (int)au_cnt, param, args_var->max_au, bitrate_tot, clk_end, clk_tot); for(int fidx = 0; fidx < num_frames; fidx++) { if(is_rec) { @@ -1221,10 +1349,23 @@ int main(int argc, const char **argv) /* store bitstream */ if(OAPV_SUCCEEDED(ret)) { if(is_out && stat.write > 0) { - if(write_data(args_var->fname_out, bs_buf, stat.write)) { - logerr("ERR: cannot write bitstream\n"); - ret = -1; - goto ERR; + /* If an APV extension (.apv or .oapv) is specified, all AU are appended to that file. */ + if (strstr(args_var->fname_out, ".apv") || strstr(args_var->fname_out, ".APV") || + strstr(args_var->fname_out, ".oapv") || strstr(args_var->fname_out, ".OAPV")) { + if (write_data(args_var->fname_out, bs_buf, stat.write)) { + logerr("ERR: cannot write bitstream\n"); + ret = -1; + goto ERR; + } + } + else { + /* Separate each AU in an individual file. */ + sprintf(fname_out_au, "%s_%04d.apv1", args_var->fname_out, (int)au_cnt); + if(overwrite_data(fname_out_au, bs_buf, stat.write)) { + logerr("ERR: cannot write bitstream\n"); + ret = -1; + goto ERR; + } } } } diff --git a/app/oapv_app_util.h b/app/oapv_app_util.h index 2d2192f..998eb96 100644 --- a/app/oapv_app_util.h +++ b/app/oapv_app_util.h @@ -238,6 +238,7 @@ static __inline oapv_clk_t oapv_clk_sec(oapv_clk_t clk) #define CLIP_VAL(n, min, max) (((n) > (max)) ? (max) : (((n) < (min)) ? (min) : (n))) #define ALIGN_VAL(val, align) ((((val) + (align) - 1) / (align)) * (align)) +#define MAX_VAL(a,b) (((a) > (b)) ? (a) : (b)) /* Function for atomic increment: This function might need to modify according to O/S or CPU platform @@ -493,7 +494,7 @@ static int imgb_write(char *fname, oapv_imgb_t *imgb) { unsigned char *p8; int i, j, bd; - int chroma_format, bit_depth; + int color_format, bit_depth; FILE *fp; if(imgb == NULL) { @@ -501,7 +502,7 @@ static int imgb_write(char *fname, oapv_imgb_t *imgb) return -1; } - chroma_format = OAPV_CS_GET_FORMAT(imgb->cs); + color_format = OAPV_CS_GET_FORMAT(imgb->cs); bit_depth = OAPV_CS_GET_BIT_DEPTH(imgb->cs); fp = fopen(fname, "ab"); @@ -509,14 +510,14 @@ static int imgb_write(char *fname, oapv_imgb_t *imgb) logerr("ERR: cannot open file = %s\n", fname); return -1; } - if(bit_depth == 8 && (chroma_format == OAPV_CF_YCBCR400 || chroma_format == OAPV_CF_YCBCR420 || chroma_format == OAPV_CF_YCBCR422 || - chroma_format == OAPV_CF_YCBCR444 || chroma_format == OAPV_CF_YCBCR4444)) { + if(bit_depth == 8 && (color_format == OAPV_CF_YCBCR400 || color_format == OAPV_CF_YCBCR420 || color_format == OAPV_CF_YCBCR422 || + color_format == OAPV_CF_YCBCR444 || color_format == OAPV_CF_YCBCR4444)) { bd = 1; } - else if(bit_depth >= 10 && bit_depth <= 16 && (chroma_format == OAPV_CF_YCBCR400 || chroma_format == OAPV_CF_YCBCR420 || chroma_format == OAPV_CF_YCBCR422 || chroma_format == OAPV_CF_YCBCR444 || chroma_format == OAPV_CF_YCBCR4444)) { + else if(bit_depth >= 10 && bit_depth <= 16 && (color_format == OAPV_CF_YCBCR400 || color_format == OAPV_CF_YCBCR420 || color_format == OAPV_CF_YCBCR422 || color_format == OAPV_CF_YCBCR444 || color_format == OAPV_CF_YCBCR4444)) { bd = 2; } - else if(bit_depth >= 10 && chroma_format == OAPV_CF_PLANAR2) { + else if(bit_depth >= 10 && color_format == OAPV_CF_PLANAR2) { bd = 2; } else { @@ -525,7 +526,10 @@ static int imgb_write(char *fname, oapv_imgb_t *imgb) return -1; } - for(i = 0; i < imgb->np; i++) { + // Note: because of 4444, we only save up to 3 components because y4m doesn't support 4. + int num_components = imgb->np > 3 ? 3 : imgb->np; + + for(i = 0; i < num_components; i++) { p8 = (unsigned char *)imgb->a[i] + (imgb->s[i] * imgb->y[i]) + (imgb->x[i] * bd); for(j = 0; j < imgb->h[i]; j++) { @@ -736,6 +740,128 @@ void imgb_clear(oapv_imgb_t *imgb) } } +// todo: doesn't work with planar2. +static void imgb_calc_mip_8(oapv_imgb_t *dst_img, oapv_imgb_t *src_img) +{ + unsigned char *src_buff; + unsigned char *dst_buff; + int accum = 0; + int dst_w, dst_h, src_w, src_h; + int plane_idx, pixel_y, pixel_x; + + for(plane_idx = 0; plane_idx < dst_img->np; plane_idx++) { + src_buff = (unsigned char *)src_img->a[plane_idx]; + dst_buff = (unsigned char *)dst_img->a[plane_idx]; + dst_w = dst_img->w[plane_idx]; + dst_h = dst_img->h[plane_idx]; + src_w = src_img->w[plane_idx]; + src_h = src_img->h[plane_idx]; + + if(src_h > dst_h && src_w > dst_w) { + for(pixel_y = 0; pixel_y < dst_h; pixel_y++) { + for(pixel_x = 0; pixel_x < dst_w; pixel_x++) { + accum = src_buff[pixel_x * 2]; + accum += src_buff[pixel_x * 2 + 1]; + accum += src_buff[pixel_x * 2 + src_img->s[plane_idx]]; + accum += src_buff[pixel_x * 2 + 1 + src_img->s[plane_idx]]; + dst_buff[pixel_x] = (unsigned char)(accum / 4); + } + src_buff += src_img->s[plane_idx] * 2; + dst_buff += dst_img->s[plane_idx]; + } + } + else if(src_h > dst_h) { + for(pixel_y = 0; pixel_y < dst_h; pixel_y++) { + for(pixel_x = 0; pixel_x < dst_w; pixel_x++) { + accum = src_buff[pixel_x]; + accum += src_buff[pixel_x + src_img->s[plane_idx]]; + dst_buff[pixel_x] = (unsigned char)(accum / 2); + } + src_buff += src_img->s[plane_idx] * 2; + dst_buff += dst_img->s[plane_idx]; + } + } + else if(src_w > dst_w) { + for(pixel_y = 0; pixel_y < dst_h; pixel_y++) { + for(pixel_x = 0; pixel_x < dst_w; pixel_x++) { + accum = src_buff[pixel_x * 2]; + accum += src_buff[pixel_x * 2 + 1]; + dst_buff[pixel_x] = (unsigned char)(accum / 2); + } + src_buff += src_img->s[plane_idx]; + dst_buff += dst_img->s[plane_idx]; + } + } + } +} + +static void imgb_calc_mip_16(oapv_imgb_t* dst_img, oapv_imgb_t* src_img) +{ + unsigned short *src_buff; + unsigned short *dst_buff; + int accum = 0; + int dst_w, dst_h, src_w, src_h; + int plane_idx, pixel_y, pixel_x; + + for(plane_idx = 0; plane_idx < dst_img->np; plane_idx++) + { + src_buff = (unsigned short *)src_img->a[plane_idx]; + dst_buff = (unsigned short *)dst_img->a[plane_idx]; + dst_w = dst_img->w[plane_idx]; + dst_h = dst_img->h[plane_idx]; + src_w = src_img->w[plane_idx]; + src_h = src_img->h[plane_idx]; + + if(src_h > dst_h && src_w > dst_w) { + for(pixel_y = 0; pixel_y < dst_h; pixel_y++) { + for(pixel_x = 0; pixel_x < dst_w; pixel_x++) { + accum = src_buff[pixel_x * 2]; + accum += src_buff[pixel_x * 2 + 1]; + accum += src_buff[pixel_x * 2 + src_img->s[plane_idx]/2]; + accum += src_buff[pixel_x * 2 + 1 + src_img->s[plane_idx]/2]; + dst_buff[pixel_x] = (unsigned short)(accum / 4); + } + src_buff += src_img->s[plane_idx]; // must divide stride by 2 because of ushort ptrs. + dst_buff += dst_img->s[plane_idx] / 2; // must divide stride by 2 because of ushort ptrs. + } + } + else if (src_h > dst_h) { + for(pixel_y = 0; pixel_y < dst_h; pixel_y++) { + for(pixel_x = 0; pixel_x < dst_w; pixel_x++) { + accum = src_buff[pixel_x]; + accum += src_buff[pixel_x + src_img->s[plane_idx]/2]; + dst_buff[pixel_x] = (unsigned short)(accum / 2); + } + src_buff += src_img->s[plane_idx]; // must divide stride by 2 because of ushort ptrs. + dst_buff += dst_img->s[plane_idx] / 2; // must divide stride by 2 because of ushort ptrs. + } + } + else if(src_w > dst_w) { + for(pixel_y = 0; pixel_y < dst_h; pixel_y++) { + for(pixel_x = 0; pixel_x < dst_w; pixel_x++) { + accum = src_buff[pixel_x * 2]; + accum += src_buff[pixel_x * 2 + 1]; + dst_buff[pixel_x] = (unsigned short)(accum / 2); + } + src_buff += src_img->s[plane_idx] / 2; // must divide stride by 2 because of ushort ptrs. + dst_buff += dst_img->s[plane_idx] / 2; // must divide stride by 2 because of ushort ptrs. + } + } + } +} + +static void imgb_calc_mip(oapv_imgb_t *dst_img, oapv_imgb_t *src_img) +{ + int dst_bit_depth = OAPV_CS_GET_BIT_DEPTH(dst_img->cs); + if(dst_bit_depth > 8) { + imgb_calc_mip_16(dst_img, src_img); // might have to mask bits. + } + else + { + imgb_calc_mip_8(dst_img, src_img); + } +} + static void measure_psnr(oapv_imgb_t *org, oapv_imgb_t *rec, double psnr[4], int bit_depth) { double sum[4], mse[4]; @@ -789,6 +915,7 @@ static void measure_psnr(oapv_imgb_t *org, oapv_imgb_t *rec, double psnr[4], int } } +/* Note: this should be called append_data */ static int write_data(char *fname, unsigned char *data, int size) { FILE *fp; @@ -803,9 +930,32 @@ static int write_data(char *fname, unsigned char *data, int size) return 0; } +/* Opens a file and write the whole buffer to file. File is overwriten. */ +static int overwrite_data(char *fname, unsigned char *data, int size) +{ + FILE *fp; + + fp = fopen(fname, "wb"); + if(fp == NULL) { + logerr("cannot open the output file=%s\n", fname); + return -1; + } + fwrite(data, 1, size, fp); + fclose(fp); + return 0; +} + static int clear_data(char *fname) { FILE *fp; + + /* don't create an empty file, if it didn't exist before. */ + fp = fopen(fname, "rb"); + if(fp == NULL) { + return 0; + } + fclose(fp); + fp = fopen(fname, "wb"); if(fp == NULL) { logerr("ERR: cannot remove file (%s)\n", fname); diff --git a/app/oapv_app_y4m.h b/app/oapv_app_y4m.h index 07835b3..17fe1cb 100644 --- a/app/oapv_app_y4m.h +++ b/app/oapv_app_y4m.h @@ -273,7 +273,7 @@ static int write_y4m_header(char *fname, oapv_imgb_t *imgb) else if(bit_depth == 12) strcpy(c_buf, "422p12"); } - else if(color_format == OAPV_CF_YCBCR444) { + else if(color_format == OAPV_CF_YCBCR444 || color_format == OAPV_CF_YCBCR4444) { // for testing 4444 is considered 444. if(bit_depth == 8) strcpy(c_buf, "444"); else if(bit_depth == 10) diff --git a/inc/oapv.h b/inc/oapv.h index bc4c271..50fa377 100644 --- a/inc/oapv.h +++ b/inc/oapv.h @@ -42,6 +42,8 @@ extern "C" { #include #endif +#include + /***************************************************************************** * version and related macro * the version string follows the rule of API_SET.MAJOR.MINOR.PATCH @@ -81,8 +83,8 @@ extern "C" { #define OAPV_BLK_D (OAPV_BLK_W * OAPV_BLK_H) /* size of tile */ -#define OAPV_MAX_TILE_ROWS (20) // max number of tiles in row -#define OAPV_MAX_TILE_COLS (20) // max number of tiles in column +#define OAPV_MAX_TILE_ROWS (64) // max number of tiles in row (supports 16K with 256x256 pixel tiles) +#define OAPV_MAX_TILE_COLS (64) // max number of tiles in column #define OAPV_MAX_TILES (OAPV_MAX_TILE_ROWS * OAPV_MAX_TILE_COLS) #define OAPV_MIN_TILE_W_MB (16) #define OAPV_MIN_TILE_H_MB (8) @@ -280,6 +282,40 @@ extern "C" { #define OAPV_RC_CQP (0) #define OAPV_RC_ABR (1) +/***************************************************************************** + * logging verbosities + *****************************************************************************/ + +#define OAPV_LOG_ERROR 0 +#define OAPV_LOG_WARNING 1 +#define OAPV_LOG_INFO 2 +#define OAPV_LOG_DEBUG 3 + +/***************************************************************************** + * logging callback (note: handlers must be thread safe) + *****************************************************************************/ +typedef void (*oapv_log_callback_t)(const char *message, int verbosity, void *userdata); + +/***************************************************************************** + * memory callbacks + *****************************************************************************/ +typedef struct oapv_memory_callbacks oapv_memory_callbacks_t; +struct oapv_memory_callbacks { + void *(*malloc)(size_t size); + void *(*calloc)(size_t count, size_t size); + void *(*realloc)(void *block, size_t size); + void (*free)(void *block); +}; + +/***************************************************************************** + * cpu event tracing callbacks + *****************************************************************************/ +typedef struct oapv_cputrace_callbacks oapv_cputrace_callbacks_t; +struct oapv_cputrace_callbacks { + void (*begin_event)(const char *name, const char *file, int line); + void (*end_event)(void); +}; + /***************************************************************************** * type and macro for media time *****************************************************************************/ @@ -372,6 +408,29 @@ struct oapv_imgb { int (*addref)(oapv_imgb_t *imgb); int (*getref)(oapv_imgb_t *imgb); int (*release)(oapv_imgb_t *imgb); + + /* Optional tiled-layout output. When tiled_layout == 0 (default), `a[c]` + * points at a scanline-strided plane and `s[c]` is the picture stride + * in bytes — the historical behavior. When tiled_layout != 0, the + * output buffer is laid out tile-major with planes interleaved within + * each tile (i.e. tile k occupies one contiguous `tile_size`-byte + * block, and all of plane c's `tile_h[c]*tile_stride[c]` bytes live + * within that block at the same intra-tile offset for every tile). + * + * In tiled mode `a[c]` is the buffer base plus the intra-tile byte + * offset of plane c, so tile (tx, ty) for component c starts at: + * a[c] + (ty * num_tile_cols + tx) * tile_size + * and the decoder writes pixels at tile-local coordinates using + * `tile_stride[c]` as the per-row byte advance. + * + * Zero-initialised structs continue to use the scanline path. */ + int tiled_layout; + int num_tile_cols; /* number of tile columns in the picture */ + int num_tile_rows; /* number of tile rows in the picture */ + int tile_size; /* total bytes per tile (sum of plane sub-tiles, incl. padding) */ + int tile_w[OAPV_MAX_CC]; /* tile width per component, in samples */ + int tile_h[OAPV_MAX_CC]; /* tile height per component, in samples */ + int tile_stride[OAPV_MAX_CC]; /* tile row stride in bytes (= tile_w[c] * bytes_per_sample) */ }; typedef struct oapv_frm oapv_frm_t; @@ -625,6 +684,11 @@ struct oapve_param { int tile_w; // width of tile MUST be N * MB width int tile_h; // height of tile MUST be N * MB height + /* when non-zero, write per-tile sizes in the frame header so a decoder can + index individual tiles (e.g. for selective / tiled decoding) without + parsing the whole access unit. 0 (default) matches the base bitstream. */ + int tile_size_present_in_fh_flag; + /* preset for setting trade-off between complexity and coding gain */ int preset; /* color description values */ @@ -690,6 +754,63 @@ struct oapvd_stat { int frm_size[OAPV_MAX_NUM_FRAMES]; }; +/***************************************************************************** + * multi-mip selective decode structures + *****************************************************************************/ +typedef struct oapv_mip_request oapv_mip_request_t; +struct oapv_mip_request { + int mip_level; // Which mip level to decode (0=full, 1=half, etc.) + int num_tiles; // Number of tiles to decode for this mip + int tile_coords[2*OAPV_MAX_TILES]; // Pairs of [col, row] for each tile + oapv_imgb_t *output_buffer; // Output buffer for this mip level + + // Status (filled by decoder) + int status; // OAPV_OK if successful, error code otherwise + + // Frame metadata (filled by decoder) + int frame_width_mb_aligned; // Frame width in pixels aligned to macroblock boundaries from mip level metadata + int frame_height_mb_aligned; // Frame height in pixels aligned to macroblock boundaries from mip level metadata + int tile_width_mb_aligned; // Tile width in pixels aligned to macroblock boundaries (converted from MBs) + int tile_height_mb_aligned; // Tile height in pixels aligned to macroblock boundaries (converted from MBs) + int bit_depth; // Bit depth from frame metadata + int chroma_format_idc; // Chroma format from frame metadata + + /* Optional per-tile destination slot mapping for virtualized output. + * + * When NULL (default after Memzero), the decoder routes each tile to its + * natural offset within output_buffer using (row * num_tile_cols + col). + * This is the legacy behavior and is fully ABI-compatible with callers + * that don't know about this field. + * + * When non-NULL, must point to a caller-owned array of at least num_tiles + * ints, where tile_dst_slots[i] is the destination slot index for the + * tile at tile_coords[i*2..i*2+1]. The decoder writes that tile at + * (tile_dst_slots[i] * tile_size) within output_buffer. Used by callers + * implementing a bounded resident-tile cache where output_buffer is sized + * to a tile-budget (much smaller than the worst-case full-tile-count). + * + * Only honoured when output_buffer->tiled_layout != 0. Caller is + * responsible for keeping the array alive across the decode call. */ + const int *tile_dst_slots; +}; + +typedef struct oapv_multi_mip_decode oapv_multi_mip_decode_t; +struct oapv_multi_mip_decode { + int num_mips; // Number of mip levels to decode + oapv_mip_request_t *mip_requests; // Array of mip requests +}; + +/***************************************************************************** + * selective decode input stream + *****************************************************************************/ +typedef struct oapvd_istream oapvd_istream_t; +struct oapvd_istream { + void *data; + long long (*tell)(oapvd_istream_t *bitr); + int (*seek)(oapvd_istream_t *bitr, long long offset, int origin); + size_t (*read)(oapvd_istream_t *bitr, void *buffer, size_t size, size_t count); +}; + /***************************************************************************** * metadata payload *****************************************************************************/ @@ -781,10 +902,43 @@ OAPV_EXPORT oapvd_t oapvd_create(oapvd_cdesc_t *cdesc, int *err); OAPV_EXPORT void oapvd_delete(oapvd_t did); OAPV_EXPORT int oapvd_config(oapvd_t did, int cfg, void *buf, int *size); OAPV_EXPORT int oapvd_decode(oapvd_t did, oapv_bitb_t *bitb, oapv_frms_t *ofrms, oapvm_t mid, oapvd_stat_t *stat); +OAPV_EXPORT int oapvd_decode_selective_multi_mips(oapvd_t did, oapvd_istream_t *istream, oapv_multi_mip_decode_t *multi_mip_decode, oapvm_t mid, oapvd_stat_t *stat); /* utility APIs **************************************************************/ OAPV_EXPORT int oapvd_info(void *au, int au_size, oapv_au_info_t *aui); +/***************************************************************************** + * openapv logging + *****************************************************************************/ +OAPV_EXPORT void oapv_set_logging_callback(oapv_log_callback_t callback, void *userdata); +OAPV_EXPORT void oapv_set_logging_verbosity(int verbosity); + +/***************************************************************************** + * openapv memory callbacks + * + * Installs custom allocators. All four callbacks (malloc/calloc/realloc/free) + * must be non-NULL, or the call fails with OAPV_ERR_INVALID_ARGUMENT. Passing + * a NULL 'callbacks' pointer resets to the default (libc) allocators. + * + * Not thread-safe and not safe to call while any codec instance is live: set + * this once during process initialization, before creating any encoder or + * decoder, and do not change it afterward. + *****************************************************************************/ +OAPV_EXPORT int oapv_set_memory_callbacks(const oapv_memory_callbacks_t* callbacks); + +/***************************************************************************** + * cpu event tracing callbacks + * + * Installs CPU trace callbacks. Both callbacks (begin_event/end_event) must be + * non-NULL, or the call fails with OAPV_ERR_INVALID_ARGUMENT. Passing a NULL + * 'callbacks' pointer resets to the default (no-op) trace callbacks. + * + * Not thread-safe and not safe to call while any codec instance is live: set + * this once during process initialization, before creating any encoder or + * decoder, and do not change it afterward. + *****************************************************************************/ +OAPV_EXPORT int oapv_set_cputrace_callbacks(const oapv_cputrace_callbacks_t *callbacks); + /***************************************************************************** * openapv version *****************************************************************************/ diff --git a/src/oapv.c b/src/oapv.c index 61bde19..99ee9be 100644 --- a/src/oapv.c +++ b/src/oapv.c @@ -31,6 +31,67 @@ #include "oapv_def.h" +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +static oapv_log_callback_t current_log_callback = NULL; +static void* current_log_user_data = NULL; +static int current_log_verbosity = OAPV_LOG_WARNING; + +static oapv_cputrace_callbacks_t cputrace_callbacks = { NULL, NULL }; + +#define BEGIN_CPU_TRACE(name) \ + if(cputrace_callbacks.begin_event) { \ + cputrace_callbacks.begin_event(name, __FILE__, __LINE__); \ + } + +#define END_CPU_TRACE() \ + if(cputrace_callbacks.end_event) { \ + cputrace_callbacks.end_event(); \ + } + +/* Simple log message for trouble shooting. */ +static void log_msg(int verbosity, const char *fmt, ...) +{ + if (verbosity > current_log_verbosity) + { + return; + } + + char str[1024] = { '\0' }; + va_list args; + va_start(args, fmt); + vsnprintf(str, sizeof(str), fmt, args); + va_end(args); + + if (current_log_callback != NULL) { + current_log_callback(str, verbosity, current_log_user_data); + } + else { + switch(verbosity) { + case OAPV_LOG_ERROR: + fprintf(stderr, "[ERROR] %s", str); + break; + case OAPV_LOG_WARNING: + fprintf(stderr, "[WARNING] %s", str); + break; + case OAPV_LOG_INFO: + printf("[INFO] %s", str); + break; + case OAPV_LOG_DEBUG: + printf("[DEBUG] %s", str); + break; + default: + printf("[UNKNOWN] %s", str); + break; + } + } +} + static void imgb_pad(oapv_imgb_t *imgb, int aw, int ah, int comp_sft[N_C][2]) { int imgb_w = imgb->w[0]; @@ -557,7 +618,23 @@ static void enc_flush(oapve_ctx_t *ctx) ctx->core[i] = NULL; } - oapv_mfree_fast(ctx->tile[0].bs_buf); + // Free per-tile bitstream buffers (each tile owns its own buffer) + if(ctx->tile != NULL) { + for(int i = 0; i < OAPV_MAX_TILES; i++) { + if(ctx->tile[i].bs_buf != NULL) { + oapv_mfree_fast(ctx->tile[i].bs_buf); + ctx->tile[i].bs_buf = NULL; + } + } + oapv_mfree_fast(ctx->tile); + ctx->tile = NULL; + } + + // Free frame header tile_size array + if(ctx->fh.tile_size != NULL) { + oapv_mfree_fast(ctx->fh.tile_size); + ctx->fh.tile_size = NULL; + } } static int enc_ready(oapve_ctx_t *ctx) @@ -566,6 +643,20 @@ static int enc_ready(oapve_ctx_t *ctx) int ret = OAPV_OK; oapv_assert(ctx->core[0] == NULL); + // Allocate tile array for maximum possible tiles + if(ctx->tile == NULL) { + ctx->tile = (oapve_tile_t *)oapv_malloc_fast(OAPV_MAX_TILES * sizeof(oapve_tile_t)); + oapv_assert_gv(ctx->tile != NULL, ret, OAPV_ERR_OUT_OF_MEMORY, ERR); + oapv_mset_x64a(ctx->tile, 0, OAPV_MAX_TILES * sizeof(oapve_tile_t)); + } + + // Allocate frame header tile_size array + if(ctx->fh.tile_size == NULL) { + ctx->fh.tile_size = (u32 *)oapv_malloc_fast(OAPV_MAX_TILES * sizeof(u32)); + oapv_assert_gv(ctx->fh.tile_size != NULL, ret, OAPV_ERR_OUT_OF_MEMORY, ERR); + oapv_mset_x64a(ctx->fh.tile_size, 0, OAPV_MAX_TILES * sizeof(u32)); + } + ret = oapve_param_update(ctx); oapv_assert_g(ret == OAPV_OK, ERR); @@ -594,14 +685,23 @@ static int enc_ready(oapve_ctx_t *ctx) } } + // Initialize all allocated tiles. Per-tile bitstream buffers are + // allocated lazily in enc_frm_prepare() once tile dimensions are known. for(int i = 0; i < OAPV_MAX_TILES; i++) { ctx->tile[i].stat = ENC_TILE_STAT_NOT_ENCODED; + ctx->tile[i].bs_buf = NULL; + ctx->tile[i].bs_buf_max = 0; } - ctx->tile[0].bs_buf = (u8 *)oapv_malloc(ctx->cdesc.max_bs_buf_size); - oapv_assert_gv(ctx->tile[0].bs_buf, ret, OAPV_ERR_UNKNOWN, ERR); ctx->rc_param.alpha = OAPV_RC_ALPHA; ctx->rc_param.beta = OAPV_RC_BETA; + /* Per-frame-index RC state: each mip slot keeps its own alpha/beta so the + * controller adapts within a resolution rather than across mips. */ + for(int i = 0; i < OAPV_MAX_NUM_FRAMES; i++) { + oapv_mset(&ctx->rc_param_frm[i], 0, sizeof(oapve_rc_param_t)); + ctx->rc_param_frm[i].alpha = OAPV_RC_ALPHA; + ctx->rc_param_frm[i].beta = OAPV_RC_BETA; + } ctx->au_bs_fmt = OAPV_CFG_VAL_AU_BS_FMT_RBAU; // default: enable raw bitstream format return OAPV_OK; @@ -747,6 +847,10 @@ static int enc_tile(oapve_ctx_t *ctx, oapve_core_t *core, oapve_tile_t *tile) tile->th.tile_data_size[c] = enc_tile_comp(&bs, tile, ctx, core, c, org_s, org, rec_s, rec); } + if(bs.ndata[0] != 0) { + /* one of the inner-loop coefficient writes detected a buffer overrun */ + return OAPV_ERR_OUT_OF_BS_BUF; + } u32 remained_bs_size = (int)((u8*)oapv_bsw_sink(&bs) - bs.beg); if(remained_bs_size > tile->bs_buf_max) { return OAPV_ERR_OUT_OF_BS_BUF; @@ -950,12 +1054,31 @@ static int enc_frm_prepare(oapve_ctx_t *ctx, oapve_param_t *param, oapv_imgb_t * ret = enc_set_tile_info(ctx->tile, ctx->w, ctx->h, param->tile_w, param->tile_h, &ctx->num_tile_cols, &ctx->num_tile_rows, &ctx->num_tiles); oapv_assert_rv(OAPV_SUCCEEDED(ret), ret); - // set bitstream buffer for each tile - int buf_size = ctx->cdesc.max_bs_buf_size / ctx->num_tiles; - ctx->tile[0].bs_buf_max = buf_size; - for(i = 1; i < ctx->num_tiles; i++) { - ctx->tile[i].bs_buf = ctx->tile[i - 1].bs_buf + buf_size; - ctx->tile[i].bs_buf_max = buf_size; + // Allocate a per-tile bitstream buffer sized for the tile geometry. + // Worst case: ~4 bytes per coefficient (16-bit value + run/sign overhead), + // plus a fixed allowance for the tile header. + { + u64 worst = (u64)param->tile_w * (u64)param->tile_h * (u64)ctx->num_c * 4ULL + 4096ULL; + // also honor the user's max_bs_buf_size hint as a lower bound + u64 hinted = (ctx->num_tiles > 0) ? ((u64)ctx->cdesc.max_bs_buf_size / (u64)ctx->num_tiles) : 0; + if(hinted > worst) worst = hinted; + /* Clamp to INT_MAX-16: oapv_bsw_init takes `int size`, so anything + * above INT_MAX would wrap negative and produce bs->end < bs->beg. */ + if(worst > 0x7FFFFFF0ULL) worst = 0x7FFFFFF0ULL; + u32 per_tile = (u32)worst; + + for(i = 0; i < ctx->num_tiles; i++) { + if(ctx->tile[i].bs_buf == NULL || ctx->tile[i].bs_buf_max < per_tile) { + if(ctx->tile[i].bs_buf != NULL) { + oapv_mfree_fast(ctx->tile[i].bs_buf); + ctx->tile[i].bs_buf = NULL; + ctx->tile[i].bs_buf_max = 0; + } + ctx->tile[i].bs_buf = (u8 *)oapv_malloc(per_tile); + oapv_assert_rv(ctx->tile[i].bs_buf != NULL, OAPV_ERR_OUT_OF_MEMORY); + ctx->tile[i].bs_buf_max = per_tile; + } + } } // set cores for(i = 0; i < ctx->threads; i++) { @@ -1076,6 +1199,10 @@ static int enc_frame(oapve_ctx_t *ctx, oapv_bs_t *bs) oapve_vlc_frame_header(&bs_fh, ctx, &ctx->fh); oapv_bsw_sink(&bs_fh); // make sure write bits to bs buffer } + if(bs->ndata[0] != 0) { + /* AU bitstream overflowed during frame-header write or tile merge */ + return OAPV_ERR_OUT_OF_BS_BUF; + } if(ctx->param->rc_type != 0) { oapve_rc_update_after_pic(ctx, cost_sum); } @@ -1225,6 +1352,11 @@ int oapve_encode(oapve_t eid, oapv_frms_t *ifrms, oapvm_t mid, oapv_bitb_t *bitb ret = enc_frm_prepare(ctx, &ctx->cdesc.param[i], frm->imgb, (rfrms != NULL) ? rfrms->frm[i].imgb : NULL); oapv_assert_rv(OAPV_SUCCEEDED(ret), ret); + /* Load this frame slot's RC state into the working ctx->rc_param so + * enc_frame and oapve_rc_update_after_pic operate on per-slot alpha/beta. */ + int rc_slot = (i < OAPV_MAX_NUM_FRAMES) ? i : (OAPV_MAX_NUM_FRAMES - 1); + ctx->rc_param = ctx->rc_param_frm[rc_slot]; + // write headers bs_pos_pbu_beg = oapv_bsw_sink(bs); /* store pbu pos to calculate size */ DUMP_SAVE(0); @@ -1234,6 +1366,9 @@ int oapve_encode(oapve_t eid, oapv_frms_t *ifrms, oapvm_t mid, oapv_bitb_t *bitb ret = enc_frame(ctx, bs); oapv_assert_rv(OAPV_SUCCEEDED(ret), ret); + /* Save the updated RC state back into this slot for the next AU. */ + ctx->rc_param_frm[rc_slot] = ctx->rc_param; + // rewrite pbu_size int pbu_size = ((u8 *)oapv_bsw_sink(bs)) - bs_pos_pbu_beg - 4; DUMP_SAVE(1); @@ -1295,6 +1430,10 @@ int oapve_encode(oapve_t eid, oapv_frms_t *ifrms, oapvm_t mid, oapv_bitb_t *bitb oapv_bsw_deinit(bs); /* de-init BSW */ stat->write = bsw_get_write_byte(bs); + if(bs->ndata[0] != 0) { + /* a write hit the end of the caller-supplied bitb buffer */ + return OAPV_ERR_OUT_OF_BS_BUF; + } return OAPV_OK; } @@ -1445,10 +1584,13 @@ static int dec_block(oapvd_ctx_t *ctx, oapvd_core_t *core, int log2_w, int log2_ // DC prediction core->coef[0] = core->dc_diff + core->prev_dc[c]; core->prev_dc[c] = core->coef[0]; + // Inverse quantization ctx->fn_dquant[0](core->coef, core->q_mat[c], log2_w, log2_h, core->dq_shift[c]); + // Inverse transform ctx->fn_itx[0](core->coef, ITX_SHIFT1, ITX_SHIFT2(bit_depth), 1 << log2_w); + return OAPV_OK; } @@ -1467,6 +1609,7 @@ static int dec_set_tile_info(oapvd_tile_t* tile, int w_pel, int h_pel, int tile_ return OAPV_OK; } +// bs is assumed to be at the memory location of the first tile. For the tile-mip selection, part may be NULL. static int dec_frm_prepare(oapvd_ctx_t *ctx, oapv_tile_info_t * part, oapv_imgb_t *imgb) { int i, ret; @@ -1558,6 +1701,13 @@ static int dec_frm_prepare(oapvd_ctx_t *ctx, oapv_tile_info_t * part, oapv_imgb_ ret = oapv_validate_tile_topology(ctx->num_tile_cols, ctx->num_tile_rows, &ctx->num_tiles); oapv_assert_rv(OAPV_SUCCEEDED(ret), ret); + // Allocate tile array if not already allocated + if(ctx->tile == NULL) { + ctx->tile = (oapvd_tile_t *)oapv_malloc_fast(OAPV_MAX_TILES * sizeof(oapvd_tile_t)); + oapv_assert_rv(ctx->tile != NULL, OAPV_ERR_OUT_OF_MEMORY); + oapv_mset_x64a(ctx->tile, 0, OAPV_MAX_TILES * sizeof(oapvd_tile_t)); + } + dec_set_tile_info(ctx->tile, ctx->w, ctx->h, tile_w, tile_h, ctx->num_tile_cols, ctx->num_tiles); for(i = 0; i < ctx->num_tiles; i++) { @@ -1584,6 +1734,76 @@ static int dec_frm_prepare(oapvd_ctx_t *ctx, oapv_tile_info_t * part, oapv_imgb_ return OAPV_OK; } +/* Lightweight frame-context setup for the TMV selective / multi-mip decode + * paths. Unlike dec_frm_prepare it does NOT validate imgb against the frame + * dimensions/format/capacity: those decoders pass a dummy imgb (used only to + * carry the destination color space) and decode tiles into their own work + * buffers rather than a full-frame imgb. It populates the same ctx metadata + * and tile geometry that the shared dec_tile_comp path relies on. */ +static int dec_frm_prepare_selective(oapvd_ctx_t *ctx, oapv_imgb_t *imgb) +{ + int i, ret; + + oapv_assert_rv(imgb != NULL, OAPV_ERR_MALFORMED_BITSTREAM); + + ctx->imgb = imgb; + imgb_addref(ctx->imgb); // increase reference count + + ctx->bit_depth = ctx->fh.fi.bit_depth; + ctx->cfi = ctx->fh.fi.chroma_format_idc; + ctx->num_c = get_num_comp(ctx->cfi); + ctx->c_sft[Y_C][0] = 0; + ctx->c_sft[Y_C][1] = 0; + + for(int c = 1; c < ctx->num_c; c++) { + ctx->c_sft[c][0] = get_chroma_sft_w(color_format_to_chroma_format_idc(OAPV_CS_GET_FORMAT(imgb->cs))); + ctx->c_sft[c][1] = get_chroma_sft_h(color_format_to_chroma_format_idc(OAPV_CS_GET_FORMAT(imgb->cs))); + } + + ctx->w = oapv_align_value(ctx->fh.fi.frame_width, OAPV_MB_W); + ctx->h = oapv_align_value(ctx->fh.fi.frame_height, OAPV_MB_H); + + if(OAPV_CS_GET_FORMAT(imgb->cs) == OAPV_CF_PLANAR2) { + ctx->fn_blk_to_pic[Y_C] = oapv_blk_to_pic_p21x_y; + ctx->fn_blk_to_pic[U_C] = oapv_blk_to_pic_p21x_uv; + ctx->fn_blk_to_pic[V_C] = oapv_blk_to_pic_p21x_uv; + } + else if(ctx->fh.fi.profile_idc == OAPV_PROFILE_444_16C12 || ctx->fh.fi.profile_idc == OAPV_PROFILE_4444_16C12) { + for(i = 0; i < ctx->num_c; i++) { + ctx->fn_blk_to_pic[i] = ctx->disable_companding ? oapv_blk_to_pic_16 : oapv_blk_to_pic_12E16; + } + } + else { + for(i = 0; i < ctx->num_c; i++) { + ctx->fn_blk_to_pic[i] = oapv_blk_to_pic_16; + } + } + + int tile_w = ctx->fh.tile_width_in_mbs * OAPV_MB_W; + int tile_h = ctx->fh.tile_height_in_mbs * OAPV_MB_H; + + ctx->num_tile_cols = (ctx->w + (tile_w - 1)) / tile_w; + ctx->num_tile_rows = (ctx->h + (tile_h - 1)) / tile_h; + + ret = oapv_validate_tile_topology(ctx->num_tile_cols, ctx->num_tile_rows, &ctx->num_tiles); + oapv_assert_rv(OAPV_SUCCEEDED(ret), ret); + + if(ctx->tile == NULL) { + ctx->tile = (oapvd_tile_t *)oapv_malloc_fast(OAPV_MAX_TILES * sizeof(oapvd_tile_t)); + oapv_assert_rv(ctx->tile != NULL, OAPV_ERR_OUT_OF_MEMORY); + oapv_mset_x64a(ctx->tile, 0, OAPV_MAX_TILES * sizeof(oapvd_tile_t)); + } + + dec_set_tile_info(ctx->tile, ctx->w, ctx->h, tile_w, tile_h, ctx->num_tile_cols, ctx->num_tiles); + + for(i = 0; i < ctx->num_tiles; i++) { + ctx->tile[i].bs_beg = NULL; + } + ctx->tile[0].bs_beg = oapv_bsr_sink(&ctx->bs); + + return OAPV_OK; +} + static void dec_frm_finish(oapvd_ctx_t *ctx) { imgb_release(ctx->imgb); // decrease reference count @@ -1614,7 +1834,9 @@ static int dec_tile_comp(oapvd_tile_t *tile, oapvd_ctx_t *ctx, oapvd_core_t *cor // parse DC coefficient ret = oapvd_vlc_dc_coef(bs, &core->dc_diff, &core->kparam_dc[c]); - oapv_assert_rv(OAPV_SUCCEEDED(ret), ret); + if(OAPV_FAILED(ret)) { + return ret; + } // parse AC coefficient ret = oapvd_vlc_ac_coef(bs, core->coef, &core->kparam_ac[c]); @@ -1673,7 +1895,8 @@ static int dec_tile(oapvd_core_t *core, oapvd_tile_t *tile) s16 *pic; oapv_bs_t bsc; // bs for 'tile_data()' syntax - oapv_bsr_init(&bsc, BSR_GET_CUR(&bs), tile->th.tile_data_size[c], NULL); + u8 *comp_start = BSR_GET_CUR(&bs); + oapv_bsr_init(&bsc, comp_start, tile->th.tile_data_size[c], NULL); if(OAPV_CS_GET_FORMAT(ctx->imgb->cs) == OAPV_CF_PLANAR2) { tc = c > 0 ? 1 : 0; @@ -1697,6 +1920,7 @@ static int dec_tile(oapvd_core_t *core, oapvd_tile_t *tile) return OAPV_OK; } + static int dec_thread_tile(void *arg) { oapv_bs_t bs; @@ -1777,6 +2001,18 @@ static int dec_thread_tile(void *arg) static void dec_flush(oapvd_ctx_t *ctx) { + // Free dynamically allocated tile array + if(ctx->tile != NULL) { + oapv_mfree_fast(ctx->tile); + ctx->tile = NULL; + } + + + // Free frame header tile_size array + if(ctx->fh.tile_size != NULL) { + oapv_mfree_fast(ctx->fh.tile_size); + ctx->fh.tile_size = NULL; + } if(ctx->threads >= 2) { if(ctx->tpool) { // thread controller instance is present @@ -2350,8 +2586,1216 @@ int oapvd_decode_metadata(oapvd_t did, oapv_bitb_t *bitb, oapvm_payload_t *pld) return OAPV_OK; } -/////////////////////////////////////////////////////////////////////////////// -// end of decoder code + +// Data structures for multi-tile decoding + +/* Shared context for all tiles belonging to the same mip level. + * This data is read-only after initialization and shared among all worker threads + * processing tiles from this mip, reducing memory footprint and improving cache locality. + */ +typedef struct { + int mip_level; // Mip level identifier + + // Frame dimensions and format + int bit_depth; + int chroma_format_idc; + int frame_width; // Actual frame dimensions from mip header + int frame_height; + int padded_frame_width; // Padded dimensions for buffer allocation + int padded_frame_height; + + // Quantization matrices for this mip (256 bytes - largest shared data) + u8 q_matrix[N_C][OAPV_BLK_H][OAPV_BLK_W]; + + // Output buffer for this mip + oapv_imgb_t *output_buffer; + + // Tile configuration from mip's frame header + int tile_width_in_mbs; + int tile_height_in_mbs; + + // Decode context fields (to avoid shared ctx issues) + int num_comp; // Number of components (derived from chroma_format_idc) + int comp_sft[N_C][2]; // Component shift values for chroma subsampling +} mip_context_t; + +/* Per-tile work item. Points to shared mip_context_t to avoid duplicating + * mip-level data across all tiles (saves ~336 bytes per tile). + */ +typedef struct { + int tile_idx; // Tile index in frame + int col, row; // Tile coordinates + u32 size; // Tile data size + u64 file_offset; // Position in file + u8 *data; // Pointer to tile data + volatile int status; // 0=NOT_DECODED, 1=ON_DECODING, 2=DECODED + oapvd_core_t *core; // Assigned decoder core + + /* Per-tile destination slot override. >= 0 means "write tile to + * dst_slot * tile_size" in tiled output (caller-virtualized routing). + * -1 means "use default (row * num_tile_cols + col) * tile_size routing". + * Populated from oapv_mip_request::tile_dst_slots when that field is set. */ + int dst_slot; + + // Pointer to shared mip-level context (read-only, shared among all tiles of this mip) + const mip_context_t *mip_ctx; +} tile_work_t; + +typedef struct { + u64 start_offset; // Start position in file + u64 end_offset; // End position + u32 total_size; // Total bytes to read + int first_tile_idx; // First tile in this block + int num_tiles; // Number of tiles in block + u8 *buffer; // Allocated memory for block + int read_failed; // Set if the block's read came up short +} tile_read_block_t; + +// Performance metrics structure +typedef struct { + u64 io_start_ns; + u64 io_end_ns; + u64 decode_start_ns; + u64 decode_end_ns; + u32 bytes_read; + u32 tiles_decoded; +} perf_metrics_t; + +// Helper function to get current time in nanoseconds +static u64 get_time_ns() { +#ifdef _WIN32 + LARGE_INTEGER freq, counter; + QueryPerformanceFrequency(&freq); + QueryPerformanceCounter(&counter); + return (u64)((counter.QuadPart * 1000000000LL) / freq.QuadPart); +#else + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (u64)ts.tv_sec * 1000000000LL + ts.tv_nsec; +#endif +} + +/***************************************************************************** + * Modular selective decode utilities - reusable for multi and multi-mip + *****************************************************************************/ + +// Stream information structure +typedef struct { + u32 au_size; + int64_t au_start_pos; + u32 signature; +} oapv_stream_info_t; + +// Mip location information +typedef struct { + int64_t frame_file_pos; + u32 pbu_size; + int64_t frame_data_offset; + int found; +} oapv_mip_location_t; + +// Stream validation and setup +static int oapvd_validate_stream(oapvd_istream_t *istream, oapv_stream_info_t *stream_info, + perf_metrics_t *metrics) +{ + oapv_assert_rv(istream && stream_info, OAPV_ERR_INVALID_ARGUMENT); + + // Reset stream position and read AU size + istream->seek(istream, 0, SEEK_SET); + + u8 size_buf[4]; + if(istream->read(istream, size_buf, 4, 1) != 1) { + return OAPV_ERR_MALFORMED_BITSTREAM; + } + stream_info->au_size = ((u32)size_buf[0] << 24) | ((u32)size_buf[1] << 16) | + ((u32)size_buf[2] << 8) | (u32)size_buf[3]; + if(metrics) metrics->bytes_read += 4; + + stream_info->au_start_pos = istream->tell(istream); + + // Verify APV signature + u8 sig_buf[4]; + if(istream->read(istream, sig_buf, 4, 1) != 1) { + return OAPV_ERR_MALFORMED_BITSTREAM; + } + stream_info->signature = ((u32)sig_buf[0] << 24) | ((u32)sig_buf[1] << 16) | + ((u32)sig_buf[2] << 8) | (u32)sig_buf[3]; + if(stream_info->signature != 0x61507631) { + return OAPV_ERR_MALFORMED_BITSTREAM; + } + if(metrics) metrics->bytes_read += 4; + + return OAPV_OK; +} + + +// Batch mip level discovery - locates multiple mip frames in a single traversal +static int oapvd_locate_all_mips(oapvd_istream_t *istream, oapv_stream_info_t *stream_info, + const int *requested_mips, int num_mips, + oapv_mip_location_t *locations, + perf_metrics_t *metrics) +{ + oapv_assert_rv(istream && stream_info && requested_mips && locations, OAPV_ERR_INVALID_ARGUMENT); + oapv_assert_rv(num_mips > 0, OAPV_ERR_INVALID_ARGUMENT); + + // Initialize all locations as not found + for(int i = 0; i < num_mips; i++) { + locations[i].found = 0; + } + + // Find the highest requested mip level for early termination + int max_mip = requested_mips[0]; + for(int i = 1; i < num_mips; i++) { + if(requested_mips[i] > max_mip) { + max_mip = requested_mips[i]; + } + } + + // Create a lookup map for fast mip-to-index resolution + // Map mip level -> index in locations array (-1 if not requested) + int mip_to_idx[256]; // Support up to 256 mip levels + for(int i = 0; i < 256; i++) { + mip_to_idx[i] = -1; + } + for(int i = 0; i < num_mips; i++) { + if(requested_mips[i] < 256) { + mip_to_idx[requested_mips[i]] = i; + } + } + + // Single traversal through PBU stream + int current_frame = 0; + int64_t current_pos = stream_info->au_start_pos + 4; // Skip signature + int found_count = 0; + + while(current_pos < stream_info->au_start_pos + stream_info->au_size && + current_frame <= max_mip) { + + istream->seek(istream, current_pos, SEEK_SET); + + // Read PBU size + u8 pbu_size_buf[4]; + if(istream->read(istream, pbu_size_buf, 4, 1) != 1) { + return OAPV_ERR_MALFORMED_BITSTREAM; + } + u32 pbu_size = ((u32)pbu_size_buf[0] << 24) | ((u32)pbu_size_buf[1] << 16) | + ((u32)pbu_size_buf[2] << 8) | (u32)pbu_size_buf[3]; + + if(pbu_size == 0 || pbu_size > stream_info->au_size) { + return OAPV_ERR_MALFORMED_BITSTREAM; + } + + if(metrics) { + metrics->bytes_read += 4; + } + + // Read and parse PBU header + u8 pbu_header_buf[4]; + if(istream->read(istream, pbu_header_buf, 4, 1) != 1) { + return OAPV_ERR_MALFORMED_BITSTREAM; + } + + if(metrics) { + metrics->bytes_read += 4; + } + + oapv_bs_t pbu_header_bs; + oapv_bsr_init(&pbu_header_bs, pbu_header_buf, 4, NULL); + + oapv_pbuh_t pbuh; + int ret = oapvd_vlc_pbu_header(&pbu_header_bs, &pbuh); + + if(OAPV_FAILED(ret)) { + return ret; + } + + // Check if this is a frame PBU + if(pbuh.pbu_type == OAPV_PBU_TYPE_PRIMARY_FRAME || + pbuh.pbu_type == OAPV_PBU_TYPE_NON_PRIMARY_FRAME) { + + // Check if this mip level is requested + if(current_frame < 256) { + int idx = mip_to_idx[current_frame]; + if(idx >= 0) { + // Found a requested mip + locations[idx].frame_file_pos = current_pos; + locations[idx].pbu_size = pbu_size; + locations[idx].found = 1; + found_count++; + + // Early termination if all mips found + if(found_count == num_mips) { + return OAPV_OK; + } + } + } + current_frame++; + } + + // Skip to next PBU + current_pos += 4 + pbu_size; + } + + // Return success even if not all mips found (caller checks found flags) + return OAPV_OK; +} + +// Frame header parsing with progressive buffer expansion +static int oapvd_parse_frame_headers(oapvd_istream_t *istream, oapv_mip_location_t *location, + oapvd_ctx_t *ctx, oapv_fh_t *frame_header, + perf_metrics_t *metrics) +{ + oapv_assert_rv(istream && location && ctx && frame_header, OAPV_ERR_INVALID_ARGUMENT); + oapv_assert_rv(location->found, OAPV_ERR_INVALID_ARGUMENT); + + // I/O optimization: progressive header reading + const u32 INITIAL_HEADER_CHUNK = 8192; // Start with 8KB + const u32 MAX_HEADER_CHUNK = 65536; // Maximum 64KB + u32 header_buffer_size = (location->pbu_size < INITIAL_HEADER_CHUNK) ? + location->pbu_size : INITIAL_HEADER_CHUNK; + + // Allocate initial header buffer + u8 *frame_buffer = (u8 *)oapv_malloc(header_buffer_size); + if(!frame_buffer) { + return OAPV_ERR_OUT_OF_MEMORY; + } + + // Read PBU header and initial frame data + istream->seek(istream, location->frame_file_pos + 4, SEEK_SET); // Skip PBU size + u8 pbu_header_buf[4]; + if(istream->read(istream, pbu_header_buf, 4, 1) != 1) { + oapv_mfree(frame_buffer); + return OAPV_ERR_MALFORMED_BITSTREAM; + } + if(metrics) metrics->bytes_read += 4; + + // Copy PBU header and read initial frame data + memcpy(frame_buffer, pbu_header_buf, 4); + u32 remaining_to_read = header_buffer_size - 4; + if(remaining_to_read > location->pbu_size - 4) { + remaining_to_read = location->pbu_size - 4; + } + if(remaining_to_read > 0 && + istream->read(istream, frame_buffer + 4, remaining_to_read, 1) != 1) { + oapv_mfree(frame_buffer); + return OAPV_ERR_MALFORMED_BITSTREAM; + } + if(metrics) metrics->bytes_read += remaining_to_read; + + // Progressive parsing with buffer expansion + oapv_bs_t pbu_bs; + oapv_pbuh_t pbuh_check; + int parse_success = 0; + + while(!parse_success && header_buffer_size <= location->pbu_size) { + // Initialize bitstream for VLC parsing + oapv_bsr_init(&pbu_bs, frame_buffer, header_buffer_size, NULL); + + // Try parsing PBU header, then frame header. Both use the same + // grow-and-retry policy, so share it via this loop body. + int ret = oapvd_vlc_pbu_header(&pbu_bs, &pbuh_check); + if(OAPV_SUCCEEDED(ret)) { + ret = oapvd_vlc_frame_header(&pbu_bs, frame_header); + } + + if(OAPV_SUCCEEDED(ret)) { + parse_success = 1; + break; + } + + // Parse failed. Grow the buffer and retry only if we consumed almost + // all of it (i.e. we likely ran out of data, not hit a real error) AND + // the buffer can actually grow. Growth is capped at MAX_HEADER_CHUNK and + // pbu_size; if new_size == header_buffer_size we cannot make progress, + // so we must stop instead of spinning forever (headers over 64 KB or a + // truncated/malformed stream both land here). + long bytes_consumed = BSR_GET_CUR(&pbu_bs) - pbu_bs.beg; + u32 new_size = header_buffer_size * 2; + if(new_size > location->pbu_size) new_size = location->pbu_size; + if(new_size > MAX_HEADER_CHUNK) new_size = MAX_HEADER_CHUNK; + + int can_grow = (new_size > header_buffer_size); + if(bytes_consumed < (long)(header_buffer_size * 0.9) || !can_grow) { + oapv_mfree(frame_buffer); + return ret; + } + + u8 *new_buffer = (u8 *)oapv_realloc(frame_buffer, new_size); + if(!new_buffer) { + oapv_mfree(frame_buffer); + return OAPV_ERR_OUT_OF_MEMORY; + } + frame_buffer = new_buffer; + + // Read additional data + u32 additional_bytes = new_size - header_buffer_size; + if(istream->read(istream, frame_buffer + header_buffer_size, additional_bytes, 1) != 1) { + oapv_mfree(frame_buffer); + return OAPV_ERR_MALFORMED_BITSTREAM; + } + if(metrics) metrics->bytes_read += additional_bytes; + header_buffer_size = new_size; + } + + if(!parse_success) { + oapv_mfree(frame_buffer); + return OAPV_ERR_MALFORMED_BITSTREAM; + } + + // Calculate frame data offset for later use + long header_consumed = BSR_GET_CUR(&pbu_bs) - pbu_bs.beg; + location->frame_data_offset = location->frame_file_pos + 4 + header_consumed; + + oapv_mfree(frame_buffer); + return OAPV_OK; +} + + +// Multi-mip worker structure for thread parallelism +typedef struct { + oapvd_ctx_t *ctx; + oapvd_core_t *core; + tile_work_t *work_queue; + int num_tiles; + oapv_sync_obj_t sync_obj; + volatile int *tiles_completed; + perf_metrics_t *metrics; + + // Pipeline synchronization for batched I/O + volatile int *next_tile_idx; // Atomic counter for next tile to claim (eliminates O(n²) scanning) +} multi_mip_worker_t; + +/* + * Worker thread function for multi-mip tile decoding. + * Processes tiles from a work queue, handling tiles from different mip levels + * with thread-safe access to shared resources. + */ +static int dec_thread_tile_selective_multi_mip(void *arg) +{ + multi_mip_worker_t *worker = (multi_mip_worker_t*)arg; + oapvd_ctx_t *ctx = worker->ctx; + oapvd_core_t *core = worker->core; + tile_work_t *work_queue = worker->work_queue; + int num_tiles = worker->num_tiles; + + while(1) { + // Atomically claim next tile index + int tile_idx = oapv_tpool_atomic_inc(worker->sync_obj, worker->next_tile_idx) - 1; + + // Check if we've exceeded total tile count + if(tile_idx >= num_tiles) { + break; + } + + tile_work_t *work = &work_queue[tile_idx]; + + // Wait for tile data to be loaded (batched I/O) + int current_status; + while(1) { + oapv_tpool_enter_cs(worker->sync_obj); + current_status = work->status; + oapv_tpool_leave_cs(worker->sync_obj); + + if(current_status != DEC_TILE_STAT_NOT_READY) { + break; + } + + // Wait for I/O batch to load this tile + oapv_tpool_yield(); + } + + // Skip tiles whose coalesced block failed to load (short/failed read): + // their buffer is not fully populated, so they must not be decoded. + if(current_status == DEC_TILE_STAT_ERROR) { + continue; + } + + // Tile ownership is guaranteed via atomic counter. + + BEGIN_CPU_TRACE("DecodeTile"); + + oapv_bs_t tile_bs; + oapv_bsr_init(&tile_bs, work->data, work->size, NULL); + + oapvd_tile_t tile; + memset(&tile, 0, sizeof(tile)); + + /* Access shared mip context (read-only, safe for concurrent access) */ + const mip_context_t *mip_ctx = work->mip_ctx; + + int tile_w_config = mip_ctx->tile_width_in_mbs * OAPV_MB_W; + int tile_h_config = mip_ctx->tile_height_in_mbs * OAPV_MB_H; + + int tile_x_start = work->col * tile_w_config; + int tile_y_start = work->row * tile_h_config; + int tile_x_end = (tile_x_start + tile_w_config < mip_ctx->frame_width) ? + tile_x_start + tile_w_config : mip_ctx->frame_width; + int tile_y_end = (tile_y_start + tile_h_config < mip_ctx->frame_height) ? + tile_y_start + tile_h_config : mip_ctx->frame_height; + + tile.w = tile_x_end - tile_x_start; + tile.h = tile_y_end - tile_y_start; + + /* Create thread-local context copy to avoid race conditions when + * multiple threads decode tiles from different mip levels */ + oapvd_ctx_t local_ctx = *ctx; + local_ctx.num_c = mip_ctx->num_comp; + memcpy(local_ctx.c_sft, mip_ctx->comp_sft, sizeof(local_ctx.c_sft)); + + int ret = oapvd_vlc_tile_header(&tile_bs, local_ctx.num_c, &tile.th, work->size, local_ctx.bit_depth); + + if(OAPV_FAILED(ret)) { + work->status = DEC_TILE_STAT_ERROR; + continue; + } + + // Keep track of the tile header size to be able to calculate the start of each components + // from the start of the work buffer (see component loop below). + size_t tile_header_size = (BSR_GET_CUR(&tile_bs))-tile_bs.beg; + + int num_comp = get_num_comp(mip_ctx->chroma_format_idc); + oapv_assert_rv(mip_ctx->num_comp == num_comp, OAPV_ERR_INVALID_ARGUMENT); + + for(int c = 0; c < num_comp; c++) { + core->qp[c] = tile.th.tile_qp[c]; + u8 dq_scale = oapv_tbl_dq_scale[core->qp[c] % 6]; + core->dq_shift[c] = mip_ctx->bit_depth - 2 - (core->qp[c] / 6); + + core->kparam_dc[c] = OAPV_KPARAM_DC_MAX; + core->kparam_ac[c] = OAPV_KPARAM_AC_MIN; + core->prev_dc[c] = 0; + + int midx = 0; + for(int y = 0; y < OAPV_BLK_H; y++) { + for(int x = 0; x < OAPV_BLK_W; x++) { + core->q_mat[c][midx++] = dq_scale * mip_ctx->q_matrix[c][y][x]; + } + } + } + + for(int c = 0; c < num_comp; c++) { + if(!mip_ctx->output_buffer) continue; + + int comp_stride_bytes; + u16 *tile_dst; + + if(mip_ctx->output_buffer->tiled_layout) { + /* Tiled output: tile-major layout with planes interleaved within + * each tile. Each tile occupies `tile_size` bytes; plane c's data + * lives at the same intra-tile offset for every tile, and `a[c]` + * is pre-biased to point at that offset in tile 0. + * + * Default routing: tile (col,row) for component c starts at: + * a[c] + (row * num_tile_cols + col) * tile_size + * + * Virtualized routing (work->dst_slot >= 0): caller has assigned + * this tile to a specific physical slot in a bounded output + * buffer. The dest offset becomes (dst_slot * tile_size). + * + * With tile_for_comp.x/y reset to 0 below, dec_tile_comp's inner + * loop writes blocks at tile-local coords [0..tile_h_c) x + * [0..tile_w_c), so we just pass the per-tile row stride. */ + const int tile_stride_c = mip_ctx->output_buffer->tile_stride[c]; + const size_t tile_bytes = (size_t)mip_ctx->output_buffer->tile_size; + size_t tile_offset; + if(work->dst_slot >= 0) { + tile_offset = (size_t)work->dst_slot * tile_bytes; + } + else { + const int ntc = mip_ctx->output_buffer->num_tile_cols; + tile_offset = ((size_t)work->row * (size_t)ntc + (size_t)work->col) * tile_bytes; + } + tile_dst = (u16*)((u8*)mip_ctx->output_buffer->a[c] + tile_offset); + comp_stride_bytes = tile_stride_c; + } + else { + comp_stride_bytes = mip_ctx->output_buffer->s[c]; + u8 *comp_base_bytes = (u8*)mip_ctx->output_buffer->a[c]; + + int comp_tile_x_pixels = (c > 0 && mip_ctx->chroma_format_idc == 2) ? + tile_x_start / 2 : tile_x_start; + int comp_tile_y_pixels = tile_y_start; + + u8 *tile_dst_bytes = comp_base_bytes + (comp_tile_y_pixels * comp_stride_bytes) + + (comp_tile_x_pixels * 2); + tile_dst = (u16*)tile_dst_bytes; + } + + oapv_bs_t comp_bs; + + // Calculate the start of the component in the work data: start_of_work_buffer + tile_header_size + prev_tile_comp_sizes + size_t comp_data_offset = 0; + for(int prev_c = 0; prev_c < c; prev_c++) { + comp_data_offset += tile.th.tile_data_size[prev_c]; + } + + oapv_bsr_init(&comp_bs, work->data + tile_header_size + comp_data_offset, + tile.th.tile_data_size[c], NULL); + + oapvd_tile_t tile_for_comp = tile; + tile_for_comp.x = 0; + tile_for_comp.y = 0; + + ret = dec_tile_comp(&tile_for_comp, &local_ctx, core, &comp_bs, c, comp_stride_bytes, tile_dst); + + if(OAPV_FAILED(ret)) { + work->status = DEC_TILE_STAT_ERROR; + break; + } + } + + if(ret == OAPV_OK) { + oapv_tpool_enter_cs(worker->sync_obj); + work->status = DEC_TILE_STAT_DECODED; + (*worker->tiles_completed)++; + oapv_tpool_leave_cs(worker->sync_obj); + } + + END_CPU_TRACE(); + } + + return OAPV_OK; +} + +/* + * Decodes selective tiles from multiple mip levels in a single operation. + * Performs coalesced I/O for efficient random access and uses multi-threading + * for parallel tile decoding across mip levels. + * + * Algorithm: + * 1. Locate each requested mip level in the bitstream + * 2. Parse frame headers and prepare decode context for each mip + * 3. Build work queue of all requested tiles across all mips + * 4. Coalesce I/O requests to minimize seeks + * 5. Decode tiles in parallel using worker threads + */ +int oapvd_decode_selective_multi_mips(oapvd_t did, oapvd_istream_t *istream, + oapv_multi_mip_decode_t *multi_mip_decode, + oapvm_t mid, oapvd_stat_t *stat) +{ + oapvd_ctx_t *ctx; + int ret = OAPV_OK; + perf_metrics_t metrics = {0}; + + ctx = dec_id_to_ctx(did); + oapv_assert_rv(ctx, OAPV_ERR_INVALID_ARGUMENT); + oapv_assert_rv(multi_mip_decode && multi_mip_decode->num_mips > 0, OAPV_ERR_INVALID_ARGUMENT); + + metrics.io_start_ns = get_time_ns(); + + oapv_stream_info_t stream_info; + ret = oapvd_validate_stream(istream, &stream_info, &metrics); + if(OAPV_FAILED(ret)) { + return ret; + } + + int total_tiles = 0; + for(int m = 0; m < multi_mip_decode->num_mips; m++) { + total_tiles += multi_mip_decode->mip_requests[m].num_tiles; + } + + tile_work_t *work_queue = (tile_work_t *)oapv_calloc(total_tiles, sizeof(tile_work_t)); + if(!work_queue) { + return OAPV_ERR_OUT_OF_MEMORY; + } + + /* Allocate shared mip contexts (one per mip level) */ + mip_context_t *mip_contexts = (mip_context_t *)oapv_calloc(multi_mip_decode->num_mips, sizeof(mip_context_t)); + if(!mip_contexts) { + oapv_mfree(mip_contexts); + oapv_mfree(work_queue); + return OAPV_ERR_OUT_OF_MEMORY; + } + + typedef struct { + int num_comp; + int comp_sft[N_C][2]; + } mip_decode_ctx_t; + + typedef struct { + int mip_level; + oapv_mip_request_t *mip_req; + int64_t frame_file_offset; + int64_t frame_data_offset; + u32 pbu_size; + oapv_fh_t frame_header; + int num_tiles_in_frame; + mip_decode_ctx_t decode_ctx; + int found; + } mip_info_t; + + mip_info_t *mip_infos = (mip_info_t*)oapv_calloc(multi_mip_decode->num_mips, sizeof(mip_info_t)); + if(!mip_infos) { + oapv_mfree(mip_contexts); + oapv_mfree(work_queue); + return OAPV_ERR_OUT_OF_MEMORY; + } + + for(int m = 0; m < multi_mip_decode->num_mips; m++) { + mip_infos[m].mip_level = multi_mip_decode->mip_requests[m].mip_level; + mip_infos[m].mip_req = &multi_mip_decode->mip_requests[m]; + mip_infos[m].found = 0; + mip_infos[m].num_tiles_in_frame = 0; + mip_infos[m].frame_header.tile_size = NULL; + } + + BEGIN_CPU_TRACE("Locate Mips"); + + /* Build array of requested mip levels for batch lookup */ + + int *requested_mips = (int *)oapv_malloc(multi_mip_decode->num_mips * sizeof(int)); + + if(!requested_mips) { + oapv_mfree(mip_infos); + oapv_mfree(mip_contexts); + oapv_mfree(work_queue); + return OAPV_ERR_OUT_OF_MEMORY; + } + + for(int m = 0; m < multi_mip_decode->num_mips; m++) { + requested_mips[m] = mip_infos[m].mip_level; + } + + /* Allocate locations array for batch results */ + oapv_mip_location_t *locations = (oapv_mip_location_t *)oapv_malloc( + multi_mip_decode->num_mips * sizeof(oapv_mip_location_t)); + if(!locations) { + oapv_mfree(requested_mips); + oapv_mfree(mip_infos); + oapv_mfree(mip_contexts); + oapv_mfree(work_queue); + return OAPV_ERR_OUT_OF_MEMORY; + } + + /* Locate all requested mip levels in a single traversal */ + ret = oapvd_locate_all_mips(istream, &stream_info, requested_mips, + multi_mip_decode->num_mips, locations, &metrics); + + if(OAPV_FAILED(ret)) { + oapv_mfree(locations); + oapv_mfree(requested_mips); + oapv_mfree(mip_infos); + oapv_mfree(mip_contexts); + oapv_mfree(work_queue); + return ret; + } + + /* Copy results back to mip_infos */ + for(int m = 0; m < multi_mip_decode->num_mips; m++) { + if(locations[m].found) { + mip_infos[m].frame_file_offset = locations[m].frame_file_pos; + mip_infos[m].pbu_size = locations[m].pbu_size; + mip_infos[m].found = 1; + } else { + mip_infos[m].found = 0; + } + } + + /* Clean up temporary arrays */ + oapv_mfree(locations); + oapv_mfree(requested_mips); + + END_CPU_TRACE(); + + BEGIN_CPU_TRACE("Parse headers"); + + /* Parse frame headers and initialize decode context for each mip. + * Per-mip failures are recorded in mip_req->status (the designed per-request + * error channel); the function itself still returns OAPV_OK unless a fatal + * (shared) error occurs. Use a local status so `ret` is not clobbered. */ + for(int m = 0; m < multi_mip_decode->num_mips; m++) { + mip_info_t *mip_info = &mip_infos[m]; + if(!mip_info->found) continue; + + oapv_mip_location_t location; + location.frame_file_pos = mip_info->frame_file_offset; + location.pbu_size = mip_info->pbu_size; + location.found = 1; + + int hret = oapvd_parse_frame_headers(istream, &location, ctx, &ctx->fh, &metrics); + if(OAPV_FAILED(hret)) { + mip_info->mip_req->status = hret; + continue; + } + + /* This feature requires per-tile sizes carried in the frame header so + * the decoder can index individual tiles. Without the flag the tile + * offsets below are undefined; fail loudly rather than decode garbage. */ + if(!ctx->fh.tile_size_present_in_fh_flag) { + log_msg(OAPV_LOG_ERROR, + "Multi-mip decode requires tile_size_present_in_fh_flag=1 " + "(encode with --tmv-mips); mip %d rejected\n", mip_info->mip_level); + mip_info->mip_req->status = OAPV_ERR_UNSUPPORTED; + continue; + } + + mip_info->frame_header = ctx->fh; + mip_info->frame_data_offset = location.frame_data_offset; + + oapv_imgb_t dummy_imgb; + memset(&dummy_imgb, 0, sizeof(dummy_imgb)); + dummy_imgb.cs = OAPV_CS_SET(OAPV_CF_YCBCR422, 10, 0); + if(mip_info->mip_req->output_buffer) { + dummy_imgb.cs = mip_info->mip_req->output_buffer->cs; + } + dummy_imgb.refcnt = 1; + + u8 dummy_tile_data = 0; + oapv_bsr_init(&ctx->bs, &dummy_tile_data, 0, NULL); + + hret = dec_frm_prepare_selective(ctx, &dummy_imgb); + /* dec_frm_prepare_selective stored the stack dummy_imgb in ctx->imgb; + * clear it now so nothing dereferences it after this scope. */ + ctx->imgb = NULL; + if(OAPV_FAILED(hret)) { + mip_info->mip_req->status = hret; + continue; + } + + mip_info->decode_ctx.num_comp = ctx->num_c; + memcpy(mip_info->decode_ctx.comp_sft, ctx->c_sft, sizeof(ctx->c_sft)); + + /* Transfer tile_size array ownership from ctx to mip_info */ + mip_info->num_tiles_in_frame = ctx->num_tiles; + ctx->fh.tile_size = NULL; + + /* Validate tile sizes against the PBU bounds before they are used to + * compute file offsets and allocate/read coalesced blocks. A truncated + * or malicious stream must not drive reads past the frame's payload. */ + if(mip_info->num_tiles_in_frame > OAPV_MAX_TILES || + mip_info->frame_header.tile_size == NULL) { + log_msg(OAPV_LOG_ERROR, "Invalid tile count %d for mip %d\n", + mip_info->num_tiles_in_frame, mip_info->mip_level); + mip_info->mip_req->status = OAPV_ERR_MALFORMED_BITSTREAM; + continue; + } + { + /* Bytes available for tile payload = end-of-PBU - start-of-tile-data. */ + int64_t pbu_end = mip_info->frame_file_offset + 4 + (int64_t)mip_info->pbu_size; + int64_t avail = pbu_end - mip_info->frame_data_offset; + u64 required = 0; + for(int t = 0; t < mip_info->num_tiles_in_frame; t++) { + /* +4 for each per-tile size prefix (see work-queue builder). */ + required += (u64)mip_info->frame_header.tile_size[t] + 4; + } + if(avail < 0 || required > (u64)avail) { + log_msg(OAPV_LOG_ERROR, + "Tile sizes for mip %d exceed frame payload (need %llu, have %lld)\n", + mip_info->mip_level, (unsigned long long)required, (long long)avail); + mip_info->mip_req->status = OAPV_ERR_MALFORMED_BITSTREAM; + continue; + } + } + + mip_info->mip_req->frame_width_mb_aligned = oapv_align_value(ctx->fh.fi.frame_width, OAPV_MB_W); + mip_info->mip_req->frame_height_mb_aligned = oapv_align_value(ctx->fh.fi.frame_height, OAPV_MB_H); + mip_info->mip_req->tile_width_mb_aligned = ctx->fh.tile_width_in_mbs * OAPV_MB_W; + mip_info->mip_req->tile_height_mb_aligned = ctx->fh.tile_height_in_mbs * OAPV_MB_H; + mip_info->mip_req->bit_depth = ctx->fh.fi.bit_depth; + mip_info->mip_req->chroma_format_idc = ctx->fh.fi.chroma_format_idc; + mip_info->mip_req->status = OAPV_OK; + + } + + END_CPU_TRACE(); + + BEGIN_CPU_TRACE("Build Work Queue"); + + /* Build work queue from all requested tiles across mips */ + int work_queue_idx = 0; + for(int m = 0; m < multi_mip_decode->num_mips; m++) { + mip_info_t *mip_info = &mip_infos[m]; + if(!mip_info->found) { + mip_info->mip_req->status = OAPV_ERR_NOT_FOUND; + continue; + } + + /* Skip mips that failed header parse/validation above: their + * frame_header is unpopulated (division-by-zero / garbage offsets). */ + if(OAPV_FAILED(mip_info->mip_req->status)) { + continue; + } + + if(mip_info->mip_req->output_buffer == NULL) { + continue; + } + + /* Populate shared mip context for this mip level */ + mip_context_t *mip_ctx = &mip_contexts[m]; + oapv_fh_t *fh = &mip_info->frame_header; + + mip_ctx->mip_level = mip_info->mip_level; + mip_ctx->bit_depth = fh->fi.bit_depth; + mip_ctx->chroma_format_idc = fh->fi.chroma_format_idc; + mip_ctx->frame_width = fh->fi.frame_width; + mip_ctx->frame_height = fh->fi.frame_height; + mip_ctx->padded_frame_width = mip_info->mip_req->frame_width_mb_aligned; + mip_ctx->padded_frame_height = mip_info->mip_req->frame_height_mb_aligned; + mip_ctx->tile_width_in_mbs = fh->tile_width_in_mbs; + mip_ctx->tile_height_in_mbs = fh->tile_height_in_mbs; + mip_ctx->output_buffer = mip_info->mip_req->output_buffer; + mip_ctx->num_comp = mip_info->decode_ctx.num_comp; + memcpy(mip_ctx->q_matrix, fh->q_matrix, sizeof(mip_ctx->q_matrix)); + memcpy(mip_ctx->comp_sft, mip_info->decode_ctx.comp_sft, sizeof(mip_ctx->comp_sft)); + + int frame_width_in_mbs = (fh->fi.frame_width + OAPV_MB_W - 1) / OAPV_MB_W; + int frame_height_in_mbs = (fh->fi.frame_height + OAPV_MB_H - 1) / OAPV_MB_H; + int tiles_per_row = (frame_width_in_mbs + fh->tile_width_in_mbs - 1) / fh->tile_width_in_mbs; + int tiles_per_col = (frame_height_in_mbs + fh->tile_height_in_mbs - 1) / fh->tile_height_in_mbs; + + /* Validate all tile coordinates for this mip before creating work items */ + int has_invalid_tiles = 0; + for(int t = 0; t < mip_info->mip_req->num_tiles; t++) { + int col = mip_info->mip_req->tile_coords[t * 2]; + int row = mip_info->mip_req->tile_coords[t * 2 + 1]; + + if(col < 0 || col >= tiles_per_row || row < 0 || row >= tiles_per_col) { + log_msg(OAPV_LOG_ERROR, "Invalid tile coordinates (%d,%d) for mip %d (valid range: 0-%d, 0-%d)\n", + col, row, mip_info->mip_level, tiles_per_row - 1, tiles_per_col - 1); + has_invalid_tiles = 1; + } + } + + if(has_invalid_tiles) { + mip_info->mip_req->status = OAPV_ERR_INVALID_ARGUMENT; + continue; /* Skip entire mip if any tile coordinate is invalid */ + } + + /* Create work items for each requested tile, pointing to shared mip context */ + for(int t = 0; t < mip_info->mip_req->num_tiles; t++) { + int col = mip_info->mip_req->tile_coords[t * 2]; + int row = mip_info->mip_req->tile_coords[t * 2 + 1]; + int tile_idx = row * tiles_per_row + col; + + tile_work_t *work = &work_queue[work_queue_idx++]; + work->tile_idx = tile_idx; + work->col = col; + work->row = row; + work->dst_slot = (mip_info->mip_req->tile_dst_slots != NULL) + ? mip_info->mip_req->tile_dst_slots[t] + : -1; + work->status = DEC_TILE_STAT_NOT_READY; /* Tile data not yet loaded */ + work->mip_ctx = mip_ctx; /* Point to shared mip context */ + + if(fh->tile_size != NULL && tile_idx < OAPV_MAX_TILES) { + work->size = fh->tile_size[tile_idx]; + } else { + work->size = 65536; + } + + work->file_offset = mip_info->frame_data_offset; + for(int prev = 0; prev < tile_idx; prev++) { + if(fh->tile_size != NULL && prev < OAPV_MAX_TILES) { + work->file_offset += fh->tile_size[prev]; + work->file_offset += 4; + } + } + work->file_offset += 4; + + } + } + + END_CPU_TRACE(); + + for(int m = 0; m < multi_mip_decode->num_mips; m++) { + if(mip_infos[m].frame_header.tile_size) { + oapv_mfree(mip_infos[m].frame_header.tile_size); + mip_infos[m].frame_header.tile_size = NULL; + } + } + oapv_mfree(mip_infos); + + /* Early return if no tiles to decode (metadata-only call) */ + if(work_queue_idx == 0) { + oapv_mfree(work_queue); + oapv_mfree(mip_contexts); + return OAPV_OK; + } + + /* Sort work queue by file offset to enable coalesced I/O */ + for(int i = 0; i < work_queue_idx - 1; i++) { + for(int j = i + 1; j < work_queue_idx; j++) { + if(work_queue[j].file_offset < work_queue[i].file_offset) { + tile_work_t temp = work_queue[i]; + work_queue[i] = work_queue[j]; + work_queue[j] = temp; + } + } + } + + /* Coalesce adjacent tiles into larger read blocks */ + const u64 COALESCE_THRESHOLD = 64 * 1024; + tile_read_block_t *read_blocks = (tile_read_block_t*)oapv_calloc(work_queue_idx, sizeof(tile_read_block_t)); + int num_blocks = 0; + + if(work_queue_idx > 0) { + read_blocks[0].start_offset = work_queue[0].file_offset; + read_blocks[0].end_offset = work_queue[0].file_offset + work_queue[0].size; + read_blocks[0].first_tile_idx = 0; + read_blocks[0].num_tiles = 1; + num_blocks = 1; + + for(int i = 1; i < work_queue_idx; i++) { + u64 gap = work_queue[i].file_offset - read_blocks[num_blocks-1].end_offset; + + if(gap <= COALESCE_THRESHOLD) { + read_blocks[num_blocks-1].end_offset = work_queue[i].file_offset + work_queue[i].size; + read_blocks[num_blocks-1].num_tiles++; + } else { + read_blocks[num_blocks].start_offset = work_queue[i].file_offset; + read_blocks[num_blocks].end_offset = work_queue[i].file_offset + work_queue[i].size; + read_blocks[num_blocks].first_tile_idx = i; + read_blocks[num_blocks].num_tiles = 1; + num_blocks++; + } + } + } + + /* Pre-allocate all read block buffers (done once for simplicity) */ + for(int b = 0; b < num_blocks; b++) { + read_blocks[b].total_size = (u32)(read_blocks[b].end_offset - read_blocks[b].start_offset); + read_blocks[b].buffer = (u8*)oapv_malloc(read_blocks[b].total_size); + if(!read_blocks[b].buffer) { + for(int j = 0; j < b; j++) { + oapv_mfree(read_blocks[j].buffer); + } + oapv_mfree(read_blocks); + oapv_mfree(mip_contexts); + oapv_mfree(work_queue); + return OAPV_ERR_OUT_OF_MEMORY; + } + } + + int num_threads = ctx->threads; + if(num_threads <= 0) num_threads = 1; + + /* Pipeline synchronization variables */ + oapv_sync_obj_t sync_obj = oapv_tpool_sync_obj_create(); + volatile int tiles_completed = 0; + volatile int next_tile_idx = 0; // Atomic counter for tile claiming + + /* Load first batch BEFORE starting worker threads to avoid spinning */ + BEGIN_CPU_TRACE("Batched I/O"); + + int batch_size = num_threads; // Tiles per batch + int tiles_loaded = 0; + int block_idx = 0; // Track current read block + int io_error = 0; // Set if any coalesced block read comes up short + + /* Load first batch synchronously */ + int tile_idx_in_block = 0; // Track position within current block + while(tiles_loaded < batch_size && block_idx < num_blocks) { + int b = block_idx; + + /* Read coalesced block if we're starting a new block. On a short read + * the buffer is only partly filled, so its tiles are flagged ERROR and + * skipped by the workers rather than decoded from stale memory. */ + if(tile_idx_in_block == 0) { + istream->seek(istream, (int64_t)read_blocks[b].start_offset, SEEK_SET); + if(istream->read(istream, read_blocks[b].buffer, read_blocks[b].total_size, 1) != 1) { + log_msg(OAPV_LOG_ERROR, "Short read of tile block (offset %lld, %u bytes)\n", + (long long)read_blocks[b].start_offset, read_blocks[b].total_size); + read_blocks[b].read_failed = 1; + io_error = 1; + } + metrics.bytes_read += read_blocks[b].total_size; + } + + /* Assign data pointers for tiles in this block */ + for(int t = tile_idx_in_block; t < read_blocks[b].num_tiles; t++) { + int tile_idx = read_blocks[b].first_tile_idx + t; + u64 tile_offset_in_block = work_queue[tile_idx].file_offset - read_blocks[b].start_offset; + work_queue[tile_idx].data = read_blocks[b].buffer + tile_offset_in_block; + work_queue[tile_idx].status = read_blocks[b].read_failed + ? DEC_TILE_STAT_ERROR : DEC_TILE_STAT_NOT_DECODED; /* Mark ready */ + tiles_loaded++; + + if(tiles_loaded >= batch_size) { + tile_idx_in_block = t + 1; /* Remember where we stopped */ + goto first_batch_done; /* Exit both loops */ + } + } + + /* Finished this block, move to next */ + block_idx++; + tile_idx_in_block = 0; + } + +first_batch_done: + + metrics.decode_start_ns = get_time_ns(); + + /* Now start worker threads - they immediately find work ready */ + multi_mip_worker_t worker; + worker.ctx = ctx; + worker.core = ctx->core[num_threads - 1]; /* Main thread will use the last one */ + worker.work_queue = work_queue; + worker.num_tiles = work_queue_idx; + worker.sync_obj = sync_obj; + worker.tiles_completed = &tiles_completed; + worker.next_tile_idx = &next_tile_idx; + worker.metrics = &metrics; + + oapv_tpool_t *tpool = ctx->tpool; + multi_mip_worker_t **thread_workers = NULL; + /* Spawn one fewer worker than num_threads: the main thread participates in + * decoding using ctx->core[num_threads-1], so workers must use cores + * 0..num_threads-2. Sharing a core between two threads corrupts its + * coef/prev_dc/q_mat state (matches the regular decode path). */ + int num_worker_threads = num_threads - 1; + + if(num_threads > 1) { + thread_workers = (multi_mip_worker_t **)oapv_malloc(num_worker_threads * sizeof(multi_mip_worker_t *)); + if(!thread_workers) { + oapv_tpool_sync_obj_delete(&sync_obj); + for(int b = 0; b < num_blocks; b++) { + oapv_mfree(read_blocks[b].buffer); + } + oapv_mfree(read_blocks); + oapv_mfree(mip_contexts); + oapv_mfree(work_queue); + return OAPV_ERR_OUT_OF_MEMORY; + } + + for(int t = 0; t < num_worker_threads; t++) { + thread_workers[t] = (multi_mip_worker_t *)oapv_malloc(sizeof(multi_mip_worker_t)); + if(!thread_workers[t]) { + for(int j = 0; j < t; j++) { + oapv_mfree(thread_workers[j]); + } + oapv_mfree(thread_workers); + oapv_tpool_sync_obj_delete(&sync_obj); + for(int b = 0; b < num_blocks; b++) { + oapv_mfree(read_blocks[b].buffer); + } + oapv_mfree(read_blocks); + oapv_mfree(mip_contexts); + oapv_mfree(work_queue); + return OAPV_ERR_OUT_OF_MEMORY; + } + *thread_workers[t] = worker; + thread_workers[t]->core = ctx->core[t]; + + tpool->run(ctx->thread_id[t], dec_thread_tile_selective_multi_mip, thread_workers[t]); + } + } + + /* Main thread continues loading remaining batches while workers decode */ + for(int b = block_idx; b < num_blocks; b++) { + /* Read coalesced block if not already read (first block may be partially processed) */ + int start_tile = (b == block_idx) ? tile_idx_in_block : 0; + + if(start_tile == 0) { + istream->seek(istream, (int64_t)read_blocks[b].start_offset, SEEK_SET); + if(istream->read(istream, read_blocks[b].buffer, read_blocks[b].total_size, 1) != 1) { + log_msg(OAPV_LOG_ERROR, "Short read of tile block (offset %lld, %u bytes)\n", + (long long)read_blocks[b].start_offset, read_blocks[b].total_size); + read_blocks[b].read_failed = 1; + io_error = 1; + /* Flag this block's tiles ERROR under the CS so the promotion + * loop below skips them and workers never decode stale data. */ + oapv_tpool_enter_cs(sync_obj); + for(int t = 0; t < read_blocks[b].num_tiles; t++) { + work_queue[read_blocks[b].first_tile_idx + t].status = DEC_TILE_STAT_ERROR; + } + oapv_tpool_leave_cs(sync_obj); + } + metrics.bytes_read += read_blocks[b].total_size; + } + + /* Assign data pointers and mark tiles ready in batches */ + for(int t = start_tile; t < read_blocks[b].num_tiles; t++) { + int tile_idx = read_blocks[b].first_tile_idx + t; + u64 tile_offset_in_block = work_queue[tile_idx].file_offset - read_blocks[b].start_offset; + work_queue[tile_idx].data = read_blocks[b].buffer + tile_offset_in_block; + tiles_loaded++; + + /* Batch boundary: make tiles available to workers */ + if(tiles_loaded % batch_size == 0 || tile_idx == work_queue_idx - 1) { + + int batch_start = tiles_loaded - (tiles_loaded % batch_size); + + if(tiles_loaded % batch_size == 0) { + batch_start = tiles_loaded - batch_size; + } else { + batch_start = (tiles_loaded / batch_size) * batch_size; + } + int batch_end = tiles_loaded; + + oapv_tpool_enter_cs(sync_obj); + + /* Tag batch as read but not decoded yet (NOT_READY -> NOT_DECODED) */ + + for(int i = batch_start; i < batch_end; i++) { + if(work_queue[i].status == DEC_TILE_STAT_NOT_READY) { + work_queue[i].status = DEC_TILE_STAT_NOT_DECODED; + } + } + + oapv_tpool_leave_cs(sync_obj); + } + } + } + + metrics.io_end_ns = get_time_ns(); + + END_CPU_TRACE(); + + /* Main thread helps decode after I/O completes */ + if(num_threads > 1) { + /* Main thread participates in decoding */ + dec_thread_tile_selective_multi_mip(&worker); + + /* Wait for worker threads to complete */ + for(int t = 0; t < num_worker_threads; t++) { + int thread_ret; + tpool->join(ctx->thread_id[t], &thread_ret); + oapv_mfree(thread_workers[t]); + } + oapv_mfree(thread_workers); + } else { + /* Single-threaded: main thread does all work */ + dec_thread_tile_selective_multi_mip(&worker); + } + + oapv_tpool_sync_obj_delete(&sync_obj); + for(int b = 0; b < num_blocks; b++) { + oapv_mfree(read_blocks[b].buffer); + } + oapv_mfree(read_blocks); + oapv_mfree(mip_contexts); + oapv_mfree(work_queue); + + metrics.decode_end_ns = get_time_ns(); + metrics.tiles_decoded = tiles_completed; + + double io_time_ms = (metrics.io_end_ns - metrics.io_start_ns) / 1000000.0; + double decode_time_ms = (metrics.decode_end_ns - metrics.decode_start_ns) / 1000000.0; + double total_time_ms = (metrics.decode_end_ns - metrics.io_start_ns) / 1000000.0; + + log_msg(OAPV_LOG_INFO, "\nMulti-Mip Performance:\n"); + log_msg(OAPV_LOG_INFO, " Mips decoded: %d\n", multi_mip_decode->num_mips); + log_msg(OAPV_LOG_INFO, " Total tiles: %d\n", work_queue_idx); + log_msg(OAPV_LOG_INFO, " I/O time: %.2f ms\n", io_time_ms); + log_msg(OAPV_LOG_INFO, " Decode time: %.2f ms\n", decode_time_ms); + log_msg(OAPV_LOG_INFO, " Total time: %.2f ms\n", total_time_ms); + log_msg(OAPV_LOG_INFO, " Bytes read: %u\n", metrics.bytes_read); + log_msg(OAPV_LOG_INFO, " Throughput: %.2f tiles/sec\n", work_queue_idx * 1000.0 / total_time_ms); + + if(stat) { + stat->read = metrics.bytes_read; + } + + /* A short read on any coalesced block means the stream was truncated + * mid-payload: report it as a fatal (shared) error. Otherwise the operation + * completed and per-mip outcomes are reported through + * multi_mip_decode->mip_requests[m].status. */ + if(io_error) { + return OAPV_ERR_MALFORMED_BITSTREAM; + } + return OAPV_OK; +} + #endif // ENABLE_DECODER /////////////////////////////////////////////////////////////////////////////// @@ -2366,3 +3810,32 @@ const char *oapv_version(unsigned int *ver_num) return (char*)oapv_version_string; } + +void oapv_set_logging_callback(oapv_log_callback_t callback, void* user_data) +{ + current_log_callback = callback; + current_log_user_data = user_data; +} + +void oapv_set_logging_verbosity(int verbosity) +{ + current_log_verbosity = verbosity; +} + +/* See oapv.h for usage contract (NULL resets to defaults; not safe to call + * while codec instances are live). */ +int oapv_set_cputrace_callbacks(const oapv_cputrace_callbacks_t *callbacks) +{ + // Passing NULL resets to the default (no-op) trace callbacks. + if(callbacks == NULL) { + oapv_cputrace_callbacks_t defaults = { NULL, NULL }; + cputrace_callbacks = defaults; + return OAPV_OK; + } + // Make sure all the callbacks are properly set. + if(callbacks->begin_event && callbacks->end_event) { + cputrace_callbacks = *callbacks; + return OAPV_OK; + } + return OAPV_ERR_INVALID_ARGUMENT; +} \ No newline at end of file diff --git a/src/oapv_bs.c b/src/oapv_bs.c index 5614e21..f22bcaf 100644 --- a/src/oapv_bs.c +++ b/src/oapv_bs.c @@ -44,7 +44,17 @@ static int bsw_flush(oapv_bs_t *bs, int bytes) if(bytes == 0) bytes = BSW_GET_SINK_BYTE(bs); - oapv_assert_rv(bs->cur + bytes <= bs->end, -1); + /* Root bounds check: bsw_flush is the single point that advances + * bs->cur. Callers' pre-checks have under-counted in the past (e.g. + * checking for 1 byte when up to 4 are flushed), so guard here too. + * On overflow, set the sticky error flag and drop the pending bytes + * instead of writing past bs->end. */ + if(bs->cur + bytes > bs->end) { + bs->ndata[0] = -1; + bs->code = 0; + bs->leftbits = 64; + return -1; + } while(bytes--) { *bs->cur++ = (bs->code >> 56) & 0xFF; @@ -65,6 +75,8 @@ void oapv_bsw_init(oapv_bs_t *bs, u8 *buf, int size, oapv_bs_fn_flush_t fn_flush bs->code = 0; bs->leftbits = 64; bs->fn_flush = (fn_flush == NULL ? bsw_flush : fn_flush); + /* ndata[0] is used as an overflow flag: 0 = ok, -1 = buffer exhausted */ + bs->ndata[0] = 0; } void oapv_bsw_deinit(oapv_bs_t *bs) @@ -74,7 +86,14 @@ void oapv_bsw_deinit(oapv_bs_t *bs) void *oapv_bsw_sink(oapv_bs_t *bs) { - oapv_assert_rv(bs->cur + BSW_GET_SINK_BYTE(bs) < bs->end, NULL); + /* If the outstanding bytes plus current cursor would exceed the buffer, + * record the overflow on bs (ndata[0]) and return the current cursor + * unchanged. Returning NULL here was unsafe: callers do pointer + * arithmetic on the result and would compute garbage sizes. */ + if(bs->cur + BSW_GET_SINK_BYTE(bs) > bs->end) { + bs->ndata[0] = -1; + return (void *)bs->cur; + } bs->fn_flush(bs, 0); bs->code = 0; bs->leftbits = 64; @@ -104,7 +123,12 @@ int oapv_bsw_write1(oapv_bs_t *bs, int val) bs->code |= ((u64)(val & 0x1) << bs->leftbits); if(bs->leftbits == 0) { - oapv_assert_rv(bs->cur < bs->end, -1); + if(bs->cur >= bs->end) { + bs->ndata[0] = -1; + bs->code = 0; + bs->leftbits = 64; + return -1; + } bs->fn_flush(bs, 0); bs->code = 0; diff --git a/src/oapv_def.h b/src/oapv_def.h index b834526..0d5ff86 100644 --- a/src/oapv_def.h +++ b/src/oapv_def.h @@ -132,7 +132,7 @@ struct oapv_fh { int tile_width_in_mbs; /* u(20) */ int tile_height_in_mbs; /* u(20) */ int tile_size_present_in_fh_flag; /* u( 1) */ - u32 tile_size[OAPV_MAX_TILES]; /* u(32) */ + u32 *tile_size; /* u(32) - dynamically allocated */ /* ( end ) tile_info */ // int reserved_zero_8bits_4; /* u( 8) */ }; @@ -185,6 +185,9 @@ typedef void (*oapv_fn_dquant_t)(s16 *coef, s16 q_matrix[OAPV_BLK_D], int log2_w typedef int (*oapv_fn_sad_t)(int w, int h, void *src1, void *src2, int s_src1, int s_src2); typedef s64 (*oapv_fn_ssd_t)(int w, int h, void *src1, void *src2, int s_src1, int s_src2); typedef void (*oapv_fn_diff_t)(int w, int h, void *src1, void *src2, int s_src1, int s_src2, int s_diff, s16 *diff); +/* RC-specific 8x8 sampler (TMV): reads a block straight from the input imgb + * for a given component and luma-space coordinate, used by rate control. */ +typedef void (*oapv_fn_imgb_to_blk_rc_t)(oapv_imgb_t *imgb, int c, int x_l, int y_l, int w_l, int h_l, s16 *block, int bit_depth); typedef double (*oapv_fn_enc_blk_cost_t)(oapve_ctx_t *ctx, oapve_core_t *core, int log2_w, int log2_h, int c); typedef void (*oapv_fn_blk_from_pic_t)(int w, int h, void *pic, int pic_x, int pic_s, void *blk, int blk_s, int bd, int mid_val); @@ -277,8 +280,13 @@ struct oapve_ctx { oapv_imgb_t *imgb_r; oapve_param_t *param; oapv_fh_t fh; - oapve_tile_t tile[OAPV_MAX_TILES]; + oapve_tile_t *tile; /* dynamically allocated based on num_tiles */ oapve_rc_param_t rc_param; + /* per-frame RC working slots: rc_param is loaded from rc_param_frm[i] at + * the start of each frame and saved back after oapve_rc_update_after_pic, + * so alpha/beta drift from a small mip doesn't pollute the next AU's large + * mip. Keyed by frame index in the AU (0 = primary, 1..N = mips). */ + oapve_rc_param_t rc_param_frm[OAPV_MAX_NUM_FRAMES]; oapv_tpool_t *tpool; oapv_thread_t thread_id[OAPV_MAX_THREADS]; oapv_sync_obj_t sync_obj; @@ -310,6 +318,10 @@ struct oapve_ctx { const oapv_fn_ssd_t *fn_ssd; const oapv_fn_diff_t *fn_diff; + /* RC-specific 8x8 sampler used by oapve_rc; keeps its own signature + * (imgb + component + luma coords) distinct from the block<->picture + * transfer functions below. */ + oapv_fn_imgb_to_blk_rc_t fn_imgb_to_blk_rc; oapv_fn_blk_from_pic_t fn_blk_from_pic[N_C]; oapv_fn_blk_to_pic_t fn_blk_to_pic[N_C]; oapv_fn_imgb_pad_t fn_imgb_pad; @@ -349,6 +361,15 @@ struct oapve_ctx { #define DEC_TILE_STAT_IS_ON(stat) ((stat) & DEC_TILE_STAT_FLAG_ON) #define DEC_TILE_STAT_IS_DONE(stat) ((stat) & DEC_TILE_STAT_FLAG_DONE) +/* TMV selective/disk-I/O decoder tile status (distinct scheme from the + * bitfield macros above; used by the multi-mip/selective decode paths). */ +#define DEC_TILE_STAT_NOT_READY -2 // Tile data not yet loaded from disk +#define DEC_TILE_STAT_NOT_DECODED 0 // Data loaded, ready to decode +#define DEC_TILE_STAT_ON_DECODING 1 +#define DEC_TILE_STAT_DECODED 2 +#define DEC_TILE_STAT_ERROR 3 +#define DEC_TILE_STAT_SIZE_ERROR -1 + typedef struct oapvd_tile oapvd_tile_t; struct oapvd_tile { @@ -394,7 +415,7 @@ struct oapvd_ctx { oapv_bs_t bs; oapv_imgb_t *imgb; oapv_fh_t fh; - oapvd_tile_t tile[OAPV_MAX_TILES]; + oapvd_tile_t *tile; /* dynamically allocated based on num_tiles */ oapv_tpool_t *tpool; oapv_thread_t thread_id[OAPV_MAX_THREADS]; oapv_sync_obj_t sync_obj; diff --git a/src/oapv_param.c b/src/oapv_param.c index 148eb99..0a095cd 100644 --- a/src/oapv_param.c +++ b/src/oapv_param.c @@ -45,6 +45,8 @@ int oapve_param_default(oapve_param_t *param) param->tile_w = 16 * OAPV_MB_W; // default: 256 param->tile_h = 16 * OAPV_MB_H; // default: 256 + param->tile_size_present_in_fh_flag = 0; // default: off (base bitstream) + param->profile_idc = OAPV_PROFILE_422_10; param->level_idc = OAPVE_PARAM_LEVEL_IDC_AUTO; param->band_idc = OAPVE_PARAM_BAND_IDC_AUTO; diff --git a/src/oapv_port.c b/src/oapv_port.c index ed9c2de..5d82144 100644 --- a/src/oapv_port.c +++ b/src/oapv_port.c @@ -31,6 +31,64 @@ #include #include "oapv_port.h" +#include "oapv.h" + +static oapv_memory_callbacks_t memory_callbacks = { NULL, NULL, NULL, NULL }; + +/* See oapv.h for usage contract (NULL resets to defaults; not safe to call + * while codec instances are live). */ +int oapv_set_memory_callbacks(const oapv_memory_callbacks_t *callbacks) +{ + // Passing NULL resets to the default (libc) allocators. + if(callbacks == NULL) { + oapv_memory_callbacks_t defaults = { NULL, NULL, NULL, NULL }; + memory_callbacks = defaults; + return OAPV_OK; + } + // Make sure all the callbacks are properly set. + if(callbacks->malloc && callbacks->calloc && callbacks->realloc && callbacks->free) { + memory_callbacks = *callbacks; + return OAPV_OK; + } + return OAPV_ERR_INVALID_ARGUMENT; +} + +void* oapv_internal_malloc(size_t size) +{ + if(memory_callbacks.malloc) { + return memory_callbacks.malloc(size); + } + + return malloc(size); +} + +void* oapv_internal_calloc(size_t count, size_t size) +{ + if(memory_callbacks.calloc) { + return memory_callbacks.calloc(count, size); + } + + return calloc(count, size); +} + +void* oapv_internal_realloc(void* block, size_t size) +{ + if(memory_callbacks.realloc) { + return memory_callbacks.realloc(block, size); + } + + return realloc(block, size); +} + +void oapv_internal_free(void* block) +{ + if(memory_callbacks.free) { + memory_callbacks.free(block); + } + else { + free(block); + } +} void *oapv_malloc_align32(int size) { diff --git a/src/oapv_port.h b/src/oapv_port.h index 1cdd576..2143c33 100644 --- a/src/oapv_port.h +++ b/src/oapv_port.h @@ -154,16 +154,35 @@ void oapv_trace_line(char *pre); #include #endif +/***************************************************************************** + * file operations + *****************************************************************************/ +#ifdef _WIN32 +#define oapv_ftell _ftelli64 +#define oapv_fseek _fseeki64 +#else +#define oapv_ftell ftell +#define oapv_fseek fseek +#endif + /***************************************************************************** * memory operations *****************************************************************************/ -#define oapv_malloc(size) malloc((size)) +void *oapv_internal_malloc(size_t size); +void *oapv_internal_calloc(size_t count, size_t size); +void *oapv_internal_realloc(void* block, size_t size); +void oapv_internal_free(void* block); + +#define oapv_malloc(size) oapv_internal_malloc((size)) #define oapv_malloc_fast(size) oapv_malloc((size)) +#define oapv_calloc(count, size) oapv_internal_calloc((count), (size)) +#define oapv_realloc(block, size) oapv_internal_realloc((block), (size)) + #define oapv_mfree(m) \ { \ if(m) { \ - free(m); \ + oapv_internal_free(m); \ } \ } #define oapv_mfree_fast(m) \ diff --git a/src/oapv_rc.c b/src/oapv_rc.c index 25d6e3a..2ded6a5 100644 --- a/src/oapv_rc.c +++ b/src/oapv_rc.c @@ -214,9 +214,12 @@ void oapve_rc_update_after_pic(oapve_ctx_t* ctx, double cost) num_pixel += (ctx->w * ctx->h) >> (ctx->c_sft[c][0] + ctx->c_sft[c][1]); } - int total_bits = 0; + /* total_bits must be u64: a 16k 10-bit 4:2:2 frame at high quality can + * exceed INT_MAX bits (~33 MB encoded), which previously wrapped negative + * and turned the subsequent log() into NaN, corrupting alpha/beta. */ + u64 total_bits = 0; for (int i = 0; i < ctx->num_tiles; i++) { - total_bits += ctx->fh.tile_size[i] * 8; + total_bits += (u64)ctx->fh.tile_size[i] * 8; } double ln_bpp = log(pow(cost / (double)num_pixel, OAPV_RC_BETA)); diff --git a/src/oapv_tpool.c b/src/oapv_tpool.c index 8af2049..62fdc75 100644 --- a/src/oapv_tpool.c +++ b/src/oapv_tpool.c @@ -41,6 +41,16 @@ #define WINDOWS_MUTEX_SYNC 0 +void oapv_tpool_yield() +{ + #if !defined(WIN32) && !defined(WIN64) + sched_yield(); + #else + Sleep(0); + #endif +} + + #if !defined(WIN32) && !defined(WIN64) typedef struct thread_ctx { @@ -338,6 +348,20 @@ void oapv_tpool_leave_cs(oapv_sync_obj_t sobj) pthread_mutex_unlock(&imutex->lmutex); } +int oapv_tpool_atomic_inc(oapv_sync_obj_t sobj, volatile int *pcnt) +{ + thread_mutex_t *imutex = (thread_mutex_t *)(sobj); + int temp = 0; + + // lock the mutex, increment the count and release the mutex + pthread_mutex_lock(&imutex->lmutex); + temp = *pcnt; + *pcnt = ++temp; + pthread_mutex_unlock(&imutex->lmutex); + + return temp; +} + #else typedef struct thread_ctx { // synchronization members @@ -647,6 +671,39 @@ void oapv_tpool_leave_cs(oapv_sync_obj_t sobj) LeaveCriticalSection(&imutex->c_section); } +int oapv_tpool_atomic_inc(oapv_sync_obj_t sobj, volatile int *pcnt) +{ + thread_mutex_t *imutex = (thread_mutex_t *)(sobj); + int temp = 0; + +#if WINDOWS_MUTEX_SYNC + // let's lock the mutex + DWORD dw_wait_result = WaitForSingleObject(imutex->lmutex, INFINITE); // wait for infinite time + + switch(dw_wait_result) { + // The thread got ownership of the mutex + case WAIT_OBJECT_0: + temp = *pcnt; + *pcnt = ++temp; + // Release ownership of the mutex object + ReleaseMutex(imutex->lmutex); + break; + // The thread got ownership of an abandoned mutex + case WAIT_ABANDONED: + temp = *pcnt; + temp++; + *pcnt = temp; + break; + } +#else + EnterCriticalSection(&imutex->c_section); + temp = *pcnt; + *pcnt = ++temp; + LeaveCriticalSection(&imutex->c_section); +#endif + return temp; +} + #endif tpool_result_t oapv_tpool_init(oapv_tpool_t *tp, int maxtask) diff --git a/src/oapv_tpool.h b/src/oapv_tpool.h index 2529883..367a7b8 100644 --- a/src/oapv_tpool.h +++ b/src/oapv_tpool.h @@ -80,5 +80,8 @@ int oapv_tpool_spinlock_wait(volatile int *addr, int val); void oapv_tpool_enter_cs(oapv_sync_obj_t sobj); void oapv_tpool_leave_cs(oapv_sync_obj_t sobj); +int oapv_tpool_atomic_inc(oapv_sync_obj_t sobj, volatile int *pcnt); + +void oapv_tpool_yield(); #endif // __OAPV_TPOOL_H__ diff --git a/src/oapv_vlc.c b/src/oapv_vlc.c index ce7937f..a68207e 100644 --- a/src/oapv_vlc.c +++ b/src/oapv_vlc.c @@ -284,7 +284,13 @@ void oapve_set_frame_header(oapve_ctx_t *ctx, oapv_fh_t *fh) { oapve_param_t *param = ctx->param; + // Preserve dynamically allocated tile_size pointer + u32 *tile_size_backup = fh->tile_size; + oapv_mset(fh, 0, sizeof(oapv_fh_t)); + + // Restore tile_size pointer + fh->tile_size = tile_size_backup; fh->fi.profile_idc = param->profile_idc; fh->fi.level_idc = param->level_idc; fh->fi.band_idc = param->band_idc; @@ -320,7 +326,10 @@ void oapve_set_frame_header(oapve_ctx_t *ctx, oapv_fh_t *fh) } } } - fh->tile_size_present_in_fh_flag = 0; + /* When enabled, tile sizes are written in the frame header so a decoder + * can index individual tiles (e.g. for selective / tiled decoding) + * without parsing the whole access unit. Off by default. */ + fh->tile_size_present_in_fh_flag = param->tile_size_present_in_fh_flag; } void oapve_set_tile_header(oapve_ctx_t *ctx, oapv_th_t *th, int tile_idx, int qp) @@ -805,6 +814,7 @@ static int dec_vlc_tile_info(oapv_bs_t *bs, oapv_fh_t *fh) { int ret; int pic_w, pic_h, tile_w, tile_h, tile_cols, tile_rows; + int num_tiles; fh->tile_width_in_mbs = oapv_bsr_read(bs, 20); DUMP_HLS(fh->tile_width_in_mbs, fh->tile_width_in_mbs); @@ -823,15 +833,22 @@ static int dec_vlc_tile_info(oapv_bs_t *bs, oapv_fh_t *fh) tile_cols = (pic_w + (tile_w - 1)) / tile_w; tile_rows = (pic_h + (tile_h - 1)) / tile_h; + num_tiles = tile_cols * tile_rows; ret = oapv_validate_tile_topology(tile_cols, tile_rows, NULL); oapv_assert_rv(OAPV_SUCCEEDED(ret), ret); + // Allocate tile_size array if needed + if(fh->tile_size == NULL) { + fh->tile_size = (u32 *)oapv_malloc_fast(OAPV_MAX_TILES * sizeof(u32)); + oapv_assert_rv(fh->tile_size != NULL, OAPV_ERR_OUT_OF_MEMORY); + } + fh->tile_size_present_in_fh_flag = oapv_bsr_read1(bs); DUMP_HLS(fh->tile_size_present_in_fh_flag, fh->tile_size_present_in_fh_flag); if(fh->tile_size_present_in_fh_flag) { - for(int i = 0; i < tile_cols * tile_rows; i++) { + for(int i = 0; i < num_tiles; i++) { fh->tile_size[i] = oapv_bsr_read(bs, 32); DUMP_HLS(fh->tile_size, fh->tile_size[i]); oapv_assert_rv(fh->tile_size[i] > 0, OAPV_ERR_MALFORMED_BITSTREAM); @@ -865,7 +882,7 @@ int oapvd_vlc_dc_coef(oapv_bs_t *bs, int *dc_diff, int *kparam_dc) } int oapvd_vlc_ac_coef(oapv_bs_t *bs, s16 *coef, int *kparam_ac) -{ +{ int level, run, k_ac, k_run, flag; int scan_pos_offset; const u8 *scanp;