// basisu_resampler.cpp // Copyright (C) 2019 Binomial LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "basisu_resampler.h" #include "basisu_resampler_filters.h" #define RESAMPLER_DEBUG 0 namespace basisu { static inline int resampler_range_check(int v, int h) { BASISU_NOTE_UNUSED(h); assert((v >= 0) && (v < h)); return v; } // Float to int cast with truncation. static inline int cast_to_int(Resample_Real i) { return (int)i; } // Ensure that the contributing source sample is within bounds. If not, reflect, clamp, or wrap. int Resampler::reflect(const int j, const int src_x, const Boundary_Op boundary_op) { int n; if (j < 0) { if (boundary_op == BOUNDARY_REFLECT) { n = -j; if (n >= src_x) n = src_x - 1; } else if (boundary_op == BOUNDARY_WRAP) n = posmod(j, src_x); else n = 0; } else if (j >= src_x) { if (boundary_op == BOUNDARY_REFLECT) { n = (src_x - j) + (src_x - 1); if (n < 0) n = 0; } else if (boundary_op == BOUNDARY_WRAP) n = posmod(j, src_x); else n = src_x - 1; } else n = j; return n; } // The make_clist() method generates, for all destination samples, // the list of all source samples with non-zero weighted contributions. Resampler::Contrib_List * Resampler::make_clist( int src_x, int dst_x, Boundary_Op boundary_op, Resample_Real(*Pfilter)(Resample_Real), Resample_Real filter_support, Resample_Real filter_scale, Resample_Real src_ofs) { struct Contrib_Bounds { // The center of the range in DISCRETE coordinates (pixel center = 0.0f). Resample_Real center; int left, right; }; int i, j, k, n, left, right; Resample_Real total_weight; Resample_Real xscale, center, half_width, weight; Contrib_List* Pcontrib; Contrib* Pcpool; Contrib* Pcpool_next; Contrib_Bounds* Pcontrib_bounds; if ((Pcontrib = (Contrib_List*)calloc(dst_x, sizeof(Contrib_List))) == NULL) return NULL; Pcontrib_bounds = (Contrib_Bounds*)calloc(dst_x, sizeof(Contrib_Bounds)); if (!Pcontrib_bounds) { free(Pcontrib); return (NULL); } const Resample_Real oo_filter_scale = 1.0f / filter_scale; const Resample_Real NUDGE = 0.5f; xscale = dst_x / (Resample_Real)src_x; if (xscale < 1.0f) { int total; (void)total; // Handle case when there are fewer destination samples than source samples (downsampling/minification). // stretched half width of filter half_width = (filter_support / xscale) * filter_scale; // Find the range of source sample(s) that will contribute to each destination sample. for (i = 0, n = 0; i < dst_x; i++) { // Convert from discrete to continuous coordinates, scale, then convert back to discrete. center = ((Resample_Real)i + NUDGE) / xscale; center -= NUDGE; center += src_ofs; left = cast_to_int((Resample_Real)floor(center - half_width)); right = cast_to_int((Resample_Real)ceil(center + half_width)); Pcontrib_bounds[i].center = center; Pcontrib_bounds[i].left = left; Pcontrib_bounds[i].right = right; n += (right - left + 1); } // Allocate memory for contributors. if ((n == 0) || ((Pcpool = (Contrib*)calloc(n, sizeof(Contrib))) == NULL)) { free(Pcontrib); free(Pcontrib_bounds); return NULL; } total = n; Pcpool_next = Pcpool; // Create the list of source samples which contribute to each destination sample. for (i = 0; i < dst_x; i++) { int max_k = -1; Resample_Real max_w = -1e+20f; center = Pcontrib_bounds[i].center; left = Pcontrib_bounds[i].left; right = Pcontrib_bounds[i].right; Pcontrib[i].n = 0; Pcontrib[i].p = Pcpool_next; Pcpool_next += (right - left + 1); assert((Pcpool_next - Pcpool) <= total); total_weight = 0; for (j = left; j <= right; j++) total_weight += (*Pfilter)((center - (Resample_Real)j) * xscale * oo_filter_scale); const Resample_Real norm = static_cast(1.0f / total_weight); total_weight = 0; #if RESAMPLER_DEBUG printf("%i: ", i); #endif for (j = left; j <= right; j++) { weight = (*Pfilter)((center - (Resample_Real)j) * xscale * oo_filter_scale) * norm; if (weight == 0.0f) continue; n = reflect(j, src_x, boundary_op); #if RESAMPLER_DEBUG printf("%i(%f), ", n, weight); #endif // Increment the number of source samples which contribute to the current destination sample. k = Pcontrib[i].n++; Pcontrib[i].p[k].pixel = (unsigned short)n; /* store src sample number */ Pcontrib[i].p[k].weight = weight; /* store src sample weight */ total_weight += weight; /* total weight of all contributors */ if (weight > max_w) { max_w = weight; max_k = k; } } #if RESAMPLER_DEBUG printf("\n\n"); #endif //assert(Pcontrib[i].n); //assert(max_k != -1); if ((max_k == -1) || (Pcontrib[i].n == 0)) { free(Pcpool); free(Pcontrib); free(Pcontrib_bounds); return NULL; } if (total_weight != 1.0f) Pcontrib[i].p[max_k].weight += 1.0f - total_weight; } } else { // Handle case when there are more destination samples than source samples (upsampling). half_width = filter_support * filter_scale; // Find the source sample(s) that contribute to each destination sample. for (i = 0, n = 0; i < dst_x; i++) { // Convert from discrete to continuous coordinates, scale, then convert back to discrete. center = ((Resample_Real)i + NUDGE) / xscale; center -= NUDGE; center += src_ofs; left = cast_to_int((Resample_Real)floor(center - half_width)); right = cast_to_int((Resample_Real)ceil(center + half_width)); Pcontrib_bounds[i].center = center; Pcontrib_bounds[i].left = left; Pcontrib_bounds[i].right = right; n += (right - left + 1); } /* Allocate memory for contributors. */ int total = n; if ((total == 0) || ((Pcpool = (Contrib*)calloc(total, sizeof(Contrib))) == NULL)) { free(Pcontrib); free(Pcontrib_bounds); return NULL; } Pcpool_next = Pcpool; // Create the list of source samples which contribute to each destination sample. for (i = 0; i < dst_x; i++) { int max_k = -1; Resample_Real max_w = -1e+20f; center = Pcontrib_bounds[i].center; left = Pcontrib_bounds[i].left; right = Pcontrib_bounds[i].right; Pcontrib[i].n = 0; Pcontrib[i].p = Pcpool_next; Pcpool_next += (right - left + 1); assert((Pcpool_next - Pcpool) <= total); total_weight = 0; for (j = left; j <= right; j++) total_weight += (*Pfilter)((center - (Resample_Real)j) * oo_filter_scale); const Resample_Real norm = static_cast(1.0f / total_weight); total_weight = 0; #if RESAMPLER_DEBUG printf("%i: ", i); #endif for (j = left; j <= right; j++) { weight = (*Pfilter)((center - (Resample_Real)j) * oo_filter_scale) * norm; if (weight == 0.0f) continue; n = reflect(j, src_x, boundary_op); #if RESAMPLER_DEBUG printf("%i(%f), ", n, weight); #endif // Increment the number of source samples which contribute to the current destination sample. k = Pcontrib[i].n++; Pcontrib[i].p[k].pixel = (unsigned short)n; /* store src sample number */ Pcontrib[i].p[k].weight = weight; /* store src sample weight */ total_weight += weight; /* total weight of all contributors */ if (weight > max_w) { max_w = weight; max_k = k; } } #if RESAMPLER_DEBUG printf("\n\n"); #endif //assert(Pcontrib[i].n); //assert(max_k != -1); if ((max_k == -1) || (Pcontrib[i].n == 0)) { free(Pcpool); free(Pcontrib); free(Pcontrib_bounds); return NULL; } if (total_weight != 1.0f) Pcontrib[i].p[max_k].weight += 1.0f - total_weight; } } #if RESAMPLER_DEBUG printf("*******\n"); #endif free(Pcontrib_bounds); return Pcontrib; } void Resampler::resample_x(Sample * Pdst, const Sample * Psrc) { assert(Pdst); assert(Psrc); int i, j; Sample total; Contrib_List* Pclist = m_Pclist_x; Contrib* p; for (i = m_resample_dst_x; i > 0; i--, Pclist++) { #if BASISU_RESAMPLER_DEBUG_OPS total_ops += Pclist->n; #endif for (j = Pclist->n, p = Pclist->p, total = 0; j > 0; j--, p++) total += Psrc[p->pixel] * p->weight; *Pdst++ = total; } } void Resampler::scale_y_mov(Sample * Ptmp, const Sample * Psrc, Resample_Real weight, int dst_x) { int i; #if BASISU_RESAMPLER_DEBUG_OPS total_ops += dst_x; #endif // Not += because temp buf wasn't cleared. for (i = dst_x; i > 0; i--) * Ptmp++ = *Psrc++ * weight; } void Resampler::scale_y_add(Sample * Ptmp, const Sample * Psrc, Resample_Real weight, int dst_x) { #if BASISU_RESAMPLER_DEBUG_OPS total_ops += dst_x; #endif for (int i = dst_x; i > 0; i--) (*Ptmp++) += *Psrc++ * weight; } void Resampler::clamp(Sample * Pdst, int n) { while (n > 0) { Sample x = *Pdst; *Pdst++ = clamp_sample(x); n--; } } void Resampler::resample_y(Sample * Pdst) { int i, j; Sample* Psrc; Contrib_List* Pclist = &m_Pclist_y[m_cur_dst_y]; Sample* Ptmp = m_delay_x_resample ? m_Ptmp_buf : Pdst; assert(Ptmp); /* Process each contributor. */ for (i = 0; i < Pclist->n; i++) { // locate the contributor's location in the scan buffer -- the contributor must always be found! for (j = 0; j < MAX_SCAN_BUF_SIZE; j++) if (m_Pscan_buf->scan_buf_y[j] == Pclist->p[i].pixel) break; assert(j < MAX_SCAN_BUF_SIZE); Psrc = m_Pscan_buf->scan_buf_l[j]; if (!i) scale_y_mov(Ptmp, Psrc, Pclist->p[i].weight, m_intermediate_x); else scale_y_add(Ptmp, Psrc, Pclist->p[i].weight, m_intermediate_x); /* If this source line doesn't contribute to any * more destination lines then mark the scanline buffer slot * which holds this source line as free. * (The max. number of slots used depends on the Y * axis sampling factor and the scaled filter width.) */ if (--m_Psrc_y_count[resampler_range_check(Pclist->p[i].pixel, m_resample_src_y)] == 0) { m_Psrc_y_flag[resampler_range_check(Pclist->p[i].pixel, m_resample_src_y)] = false; m_Pscan_buf->scan_buf_y[j] = -1; } } /* Now generate the destination line */ if (m_delay_x_resample) // Was X resampling delayed until after Y resampling? { assert(Pdst != Ptmp); resample_x(Pdst, Ptmp); } else { assert(Pdst == Ptmp); } if (m_lo < m_hi) clamp(Pdst, m_resample_dst_x); } bool Resampler::put_line(const Sample * Psrc) { int i; if (m_cur_src_y >= m_resample_src_y) return false; /* Does this source line contribute * to any destination line? if not, * exit now. */ if (!m_Psrc_y_count[resampler_range_check(m_cur_src_y, m_resample_src_y)]) { m_cur_src_y++; return true; } /* Find an empty slot in the scanline buffer. (FIXME: Perf. is terrible here with extreme scaling ratios.) */ for (i = 0; i < MAX_SCAN_BUF_SIZE; i++) if (m_Pscan_buf->scan_buf_y[i] == -1) break; /* If the buffer is full, exit with an error. */ if (i == MAX_SCAN_BUF_SIZE) { m_status = STATUS_SCAN_BUFFER_FULL; return false; } m_Psrc_y_flag[resampler_range_check(m_cur_src_y, m_resample_src_y)] = true; m_Pscan_buf->scan_buf_y[i] = m_cur_src_y; /* Does this slot have any memory allocated to it? */ if (!m_Pscan_buf->scan_buf_l[i]) { if ((m_Pscan_buf->scan_buf_l[i] = (Sample*)malloc(m_intermediate_x * sizeof(Sample))) == NULL) { m_status = STATUS_OUT_OF_MEMORY; return false; } } // Resampling on the X axis first? if (m_delay_x_resample) { assert(m_intermediate_x == m_resample_src_x); // Y-X resampling order memcpy(m_Pscan_buf->scan_buf_l[i], Psrc, m_intermediate_x * sizeof(Sample)); } else { assert(m_intermediate_x == m_resample_dst_x); // X-Y resampling order resample_x(m_Pscan_buf->scan_buf_l[i], Psrc); } m_cur_src_y++; return true; } const Resampler::Sample* Resampler::get_line() { int i; /* If all the destination lines have been * generated, then always return NULL. */ if (m_cur_dst_y == m_resample_dst_y) return NULL; /* Check to see if all the required * contributors are present, if not, * return NULL. */ for (i = 0; i < m_Pclist_y[m_cur_dst_y].n; i++) if (!m_Psrc_y_flag[resampler_range_check(m_Pclist_y[m_cur_dst_y].p[i].pixel, m_resample_src_y)]) return NULL; resample_y(m_Pdst_buf); m_cur_dst_y++; return m_Pdst_buf; } Resampler::~Resampler() { int i; #if BASISU_RESAMPLER_DEBUG_OPS printf("actual ops: %i\n", total_ops); #endif free(m_Pdst_buf); m_Pdst_buf = NULL; if (m_Ptmp_buf) { free(m_Ptmp_buf); m_Ptmp_buf = NULL; } /* Don't deallocate a contibutor list * if the user passed us one of their own. */ if ((m_Pclist_x) && (!m_clist_x_forced)) { free(m_Pclist_x->p); free(m_Pclist_x); m_Pclist_x = NULL; } if ((m_Pclist_y) && (!m_clist_y_forced)) { free(m_Pclist_y->p); free(m_Pclist_y); m_Pclist_y = NULL; } free(m_Psrc_y_count); m_Psrc_y_count = NULL; free(m_Psrc_y_flag); m_Psrc_y_flag = NULL; if (m_Pscan_buf) { for (i = 0; i < MAX_SCAN_BUF_SIZE; i++) free(m_Pscan_buf->scan_buf_l[i]); free(m_Pscan_buf); m_Pscan_buf = NULL; } } void Resampler::restart() { if (STATUS_OKAY != m_status) return; m_cur_src_y = m_cur_dst_y = 0; int i, j; for (i = 0; i < m_resample_src_y; i++) { m_Psrc_y_count[i] = 0; m_Psrc_y_flag[i] = false; } for (i = 0; i < m_resample_dst_y; i++) { for (j = 0; j < m_Pclist_y[i].n; j++) m_Psrc_y_count[resampler_range_check(m_Pclist_y[i].p[j].pixel, m_resample_src_y)]++; } for (i = 0; i < MAX_SCAN_BUF_SIZE; i++) { m_Pscan_buf->scan_buf_y[i] = -1; free(m_Pscan_buf->scan_buf_l[i]); m_Pscan_buf->scan_buf_l[i] = NULL; } } Resampler::Resampler(int src_x, int src_y, int dst_x, int dst_y, Boundary_Op boundary_op, Resample_Real sample_low, Resample_Real sample_high, const char* Pfilter_name, Contrib_List * Pclist_x, Contrib_List * Pclist_y, Resample_Real filter_x_scale, Resample_Real filter_y_scale, Resample_Real src_x_ofs, Resample_Real src_y_ofs) { int i, j; Resample_Real support, (*func)(Resample_Real); assert(src_x > 0); assert(src_y > 0); assert(dst_x > 0); assert(dst_y > 0); #if BASISU_RESAMPLER_DEBUG_OPS total_ops = 0; #endif m_lo = sample_low; m_hi = sample_high; m_delay_x_resample = false; m_intermediate_x = 0; m_Pdst_buf = NULL; m_Ptmp_buf = NULL; m_clist_x_forced = false; m_Pclist_x = NULL; m_clist_y_forced = false; m_Pclist_y = NULL; m_Psrc_y_count = NULL; m_Psrc_y_flag = NULL; m_Pscan_buf = NULL; m_status = STATUS_OKAY; m_resample_src_x = src_x; m_resample_src_y = src_y; m_resample_dst_x = dst_x; m_resample_dst_y = dst_y; m_boundary_op = boundary_op; if ((m_Pdst_buf = (Sample*)malloc(m_resample_dst_x * sizeof(Sample))) == NULL) { m_status = STATUS_OUT_OF_MEMORY; return; } // Find the specified filter. if (Pfilter_name == NULL) Pfilter_name = BASISU_RESAMPLER_DEFAULT_FILTER; for (i = 0; i < g_num_resample_filters; i++) if (strcmp(Pfilter_name, g_resample_filters[i].name) == 0) break; if (i == g_num_resample_filters) { m_status = STATUS_BAD_FILTER_NAME; return; } func = g_resample_filters[i].func; support = g_resample_filters[i].support; /* Create contributor lists, unless the user supplied custom lists. */ if (!Pclist_x) { m_Pclist_x = make_clist(m_resample_src_x, m_resample_dst_x, m_boundary_op, func, support, filter_x_scale, src_x_ofs); if (!m_Pclist_x) { m_status = STATUS_OUT_OF_MEMORY; return; } } else { m_Pclist_x = Pclist_x; m_clist_x_forced = true; } if (!Pclist_y) { m_Pclist_y = make_clist(m_resample_src_y, m_resample_dst_y, m_boundary_op, func, support, filter_y_scale, src_y_ofs); if (!m_Pclist_y) { m_status = STATUS_OUT_OF_MEMORY; return; } } else { m_Pclist_y = Pclist_y; m_clist_y_forced = true; } if ((m_Psrc_y_count = (int*)calloc(m_resample_src_y, sizeof(int))) == NULL) { m_status = STATUS_OUT_OF_MEMORY; return; } if ((m_Psrc_y_flag = (unsigned char*)calloc(m_resample_src_y, sizeof(unsigned char))) == NULL) { m_status = STATUS_OUT_OF_MEMORY; return; } // Count how many times each source line contributes to a destination line. for (i = 0; i < m_resample_dst_y; i++) for (j = 0; j < m_Pclist_y[i].n; j++) m_Psrc_y_count[resampler_range_check(m_Pclist_y[i].p[j].pixel, m_resample_src_y)]++; if ((m_Pscan_buf = (Scan_Buf*)malloc(sizeof(Scan_Buf))) == NULL) { m_status = STATUS_OUT_OF_MEMORY; return; } for (i = 0; i < MAX_SCAN_BUF_SIZE; i++) { m_Pscan_buf->scan_buf_y[i] = -1; m_Pscan_buf->scan_buf_l[i] = NULL; } m_cur_src_y = m_cur_dst_y = 0; { // Determine which axis to resample first by comparing the number of multiplies required // for each possibility. int x_ops = count_ops(m_Pclist_x, m_resample_dst_x); int y_ops = count_ops(m_Pclist_y, m_resample_dst_y); // Hack 10/2000: Weight Y axis ops a little more than X axis ops. // (Y axis ops use more cache resources.) int xy_ops = x_ops * m_resample_src_y + (4 * y_ops * m_resample_dst_x) / 3; int yx_ops = (4 * y_ops * m_resample_src_x) / 3 + x_ops * m_resample_dst_y; #if BASISU_RESAMPLER_DEBUG_OPS printf("src: %i %i\n", m_resample_src_x, m_resample_src_y); printf("dst: %i %i\n", m_resample_dst_x, m_resample_dst_y); printf("x_ops: %i\n", x_ops); printf("y_ops: %i\n", y_ops); printf("xy_ops: %i\n", xy_ops); printf("yx_ops: %i\n", yx_ops); #endif // Now check which resample order is better. In case of a tie, choose the order // which buffers the least amount of data. if ((xy_ops > yx_ops) || ((xy_ops == yx_ops) && (m_resample_src_x < m_resample_dst_x))) { m_delay_x_resample = true; m_intermediate_x = m_resample_src_x; } else { m_delay_x_resample = false; m_intermediate_x = m_resample_dst_x; } #if BASISU_RESAMPLER_DEBUG_OPS printf("delaying: %i\n", m_delay_x_resample); #endif } if (m_delay_x_resample) { if ((m_Ptmp_buf = (Sample*)malloc(m_intermediate_x * sizeof(Sample))) == NULL) { m_status = STATUS_OUT_OF_MEMORY; return; } } } void Resampler::get_clists(Contrib_List * *ptr_clist_x, Contrib_List * *ptr_clist_y) { if (ptr_clist_x) * ptr_clist_x = m_Pclist_x; if (ptr_clist_y) * ptr_clist_y = m_Pclist_y; } int Resampler::get_filter_num() { return g_num_resample_filters; } const char* Resampler::get_filter_name(int filter_num) { if ((filter_num < 0) || (filter_num >= g_num_resample_filters)) return NULL; else return g_resample_filters[filter_num].name; } } // namespace basisu