613 lines · c
1/*2 * Copyright 2020 Advanced Micro Devices, Inc.3 *4 * Permission is hereby granted, free of charge, to any person obtaining a5 * copy of this software and associated documentation files (the "Software"),6 * to deal in the Software without restriction, including without limitation7 * the rights to use, copy, modify, merge, publish, distribute, sublicense,8 * and/or sell copies of the Software, and to permit persons to whom the9 * Software is furnished to do so, subject to the following conditions:10 *11 * The above copyright notice and this permission notice shall be included in12 * all copies or substantial portions of the Software.13 *14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL17 * THE COPYRIGHT HOLDER(S) OR AUTHOR(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR18 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,19 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR20 * OTHER DEALINGS IN THE SOFTWARE.21 *22 * Authors: AMD23 *24 */25 26#include "dm_services.h"27#include "core_types.h"28#include "reg_helper.h"29#include "dcn30/dcn30_dpp.h"30#include "basics/conversion.h"31#include "dcn30/dcn30_cm_common.h"32#include "custom_float.h"33 34#define REG(reg) reg35 36#define CTX \37 ctx //dpp->base.ctx38 39#undef FN40#define FN(reg_name, field_name) \41 reg->shifts.field_name, reg->masks.field_name42 43void cm_helper_program_gamcor_xfer_func(44 struct dc_context *ctx,45 const struct pwl_params *params,46 const struct dcn3_xfer_func_reg *reg)47{48 uint32_t reg_region_cur;49 unsigned int i = 0;50 51 REG_SET_2(reg->start_cntl_b, 0,52 exp_region_start, params->corner_points[0].blue.custom_float_x,53 exp_resion_start_segment, 0);54 REG_SET_2(reg->start_cntl_g, 0,55 exp_region_start, params->corner_points[0].green.custom_float_x,56 exp_resion_start_segment, 0);57 REG_SET_2(reg->start_cntl_r, 0,58 exp_region_start, params->corner_points[0].red.custom_float_x,59 exp_resion_start_segment, 0);60 61 REG_SET(reg->start_slope_cntl_b, 0, //linear slope at start of curve62 field_region_linear_slope, params->corner_points[0].blue.custom_float_slope);63 REG_SET(reg->start_slope_cntl_g, 0,64 field_region_linear_slope, params->corner_points[0].green.custom_float_slope);65 REG_SET(reg->start_slope_cntl_r, 0,66 field_region_linear_slope, params->corner_points[0].red.custom_float_slope);67 68 REG_SET(reg->start_end_cntl1_b, 0,69 field_region_end_base, params->corner_points[1].blue.custom_float_y);70 REG_SET(reg->start_end_cntl1_g, 0,71 field_region_end_base, params->corner_points[1].green.custom_float_y);72 REG_SET(reg->start_end_cntl1_r, 0,73 field_region_end_base, params->corner_points[1].red.custom_float_y);74 75 REG_SET_2(reg->start_end_cntl2_b, 0,76 field_region_end_slope, params->corner_points[1].blue.custom_float_slope,77 field_region_end, params->corner_points[1].blue.custom_float_x);78 REG_SET_2(reg->start_end_cntl2_g, 0,79 field_region_end_slope, params->corner_points[1].green.custom_float_slope,80 field_region_end, params->corner_points[1].green.custom_float_x);81 REG_SET_2(reg->start_end_cntl2_r, 0,82 field_region_end_slope, params->corner_points[1].red.custom_float_slope,83 field_region_end, params->corner_points[1].red.custom_float_x);84 85 for (reg_region_cur = reg->region_start;86 reg_region_cur <= reg->region_end;87 reg_region_cur++) {88 89 const struct gamma_curve *curve0 = &(params->arr_curve_points[2 * i]);90 const struct gamma_curve *curve1 = &(params->arr_curve_points[(2 * i) + 1]);91 92 REG_SET_4(reg_region_cur, 0,93 exp_region0_lut_offset, curve0->offset,94 exp_region0_num_segments, curve0->segments_num,95 exp_region1_lut_offset, curve1->offset,96 exp_region1_num_segments, curve1->segments_num);97 98 i++;99 }100}101 102/* driver uses 32 regions or less, but DCN HW has 34, extra 2 are set to 0 */103#define MAX_REGIONS_NUMBER 34104#define MAX_LOW_POINT 25105#define NUMBER_REGIONS 32106#define NUMBER_SW_SEGMENTS 16107 108bool cm3_helper_translate_curve_to_hw_format(109 const struct dc_transfer_func *output_tf,110 struct pwl_params *lut_params, bool fixpoint)111{112 struct curve_points3 *corner_points;113 struct pwl_result_data *rgb_resulted;114 struct pwl_result_data *rgb;115 struct pwl_result_data *rgb_plus_1;116 struct pwl_result_data *rgb_minus_1;117 118 int32_t region_start, region_end;119 int32_t i;120 uint32_t j, k, seg_distr[MAX_REGIONS_NUMBER], increment, start_index, hw_points;121 122 if (output_tf == NULL || lut_params == NULL || output_tf->type == TF_TYPE_BYPASS)123 return false;124 125 corner_points = lut_params->corner_points;126 rgb_resulted = lut_params->rgb_resulted;127 hw_points = 0;128 129 memset(lut_params, 0, sizeof(struct pwl_params));130 memset(seg_distr, 0, sizeof(seg_distr));131 132 if (output_tf->tf == TRANSFER_FUNCTION_PQ || output_tf->tf == TRANSFER_FUNCTION_GAMMA22 ||133 output_tf->tf == TRANSFER_FUNCTION_HLG) {134 /* 32 segments135 * segments are from 2^-25 to 2^7136 */137 for (i = 0; i < NUMBER_REGIONS ; i++)138 seg_distr[i] = 3;139 140 region_start = -MAX_LOW_POINT;141 region_end = NUMBER_REGIONS - MAX_LOW_POINT;142 } else {143 /* 11 segments144 * segment is from 2^-10 to 2^0145 * There are less than 256 points, for optimization146 */147 seg_distr[0] = 3;148 seg_distr[1] = 4;149 seg_distr[2] = 4;150 seg_distr[3] = 4;151 seg_distr[4] = 4;152 seg_distr[5] = 4;153 seg_distr[6] = 4;154 seg_distr[7] = 4;155 seg_distr[8] = 4;156 seg_distr[9] = 4;157 seg_distr[10] = 1;158 159 region_start = -10;160 region_end = 1;161 }162 163 for (i = region_end - region_start; i < MAX_REGIONS_NUMBER ; i++)164 seg_distr[i] = -1;165 166 for (k = 0; k < MAX_REGIONS_NUMBER; k++) {167 if (seg_distr[k] != -1)168 hw_points += (1 << seg_distr[k]);169 }170 171 j = 0;172 for (k = 0; k < (region_end - region_start); k++) {173 increment = NUMBER_SW_SEGMENTS / (1 << seg_distr[k]);174 start_index = (region_start + k + MAX_LOW_POINT) *175 NUMBER_SW_SEGMENTS;176 for (i = start_index; i < start_index + NUMBER_SW_SEGMENTS;177 i += increment) {178 if (j == hw_points)179 break;180 if (i >= TRANSFER_FUNC_POINTS)181 return false;182 rgb_resulted[j].red = output_tf->tf_pts.red[i];183 rgb_resulted[j].green = output_tf->tf_pts.green[i];184 rgb_resulted[j].blue = output_tf->tf_pts.blue[i];185 j++;186 }187 }188 189 /* last point */190 start_index = (region_end + MAX_LOW_POINT) * NUMBER_SW_SEGMENTS;191 rgb_resulted[hw_points].red = output_tf->tf_pts.red[start_index];192 rgb_resulted[hw_points].green = output_tf->tf_pts.green[start_index];193 rgb_resulted[hw_points].blue = output_tf->tf_pts.blue[start_index];194 195 rgb_resulted[hw_points+1].red = rgb_resulted[hw_points].red;196 rgb_resulted[hw_points+1].green = rgb_resulted[hw_points].green;197 rgb_resulted[hw_points+1].blue = rgb_resulted[hw_points].blue;198 199 // All 3 color channels have same x200 corner_points[0].red.x = dc_fixpt_pow(dc_fixpt_from_int(2),201 dc_fixpt_from_int(region_start));202 corner_points[0].green.x = corner_points[0].red.x;203 corner_points[0].blue.x = corner_points[0].red.x;204 205 corner_points[1].red.x = dc_fixpt_pow(dc_fixpt_from_int(2),206 dc_fixpt_from_int(region_end));207 corner_points[1].green.x = corner_points[1].red.x;208 corner_points[1].blue.x = corner_points[1].red.x;209 210 corner_points[0].red.y = rgb_resulted[0].red;211 corner_points[0].green.y = rgb_resulted[0].green;212 corner_points[0].blue.y = rgb_resulted[0].blue;213 214 corner_points[0].red.slope = dc_fixpt_div(corner_points[0].red.y,215 corner_points[0].red.x);216 corner_points[0].green.slope = dc_fixpt_div(corner_points[0].green.y,217 corner_points[0].green.x);218 corner_points[0].blue.slope = dc_fixpt_div(corner_points[0].blue.y,219 corner_points[0].blue.x);220 221 /* see comment above, m_arrPoints[1].y should be the Y value for the222 * region end (m_numOfHwPoints), not last HW point(m_numOfHwPoints - 1)223 */224 corner_points[1].red.y = rgb_resulted[hw_points].red;225 corner_points[1].green.y = rgb_resulted[hw_points].green;226 corner_points[1].blue.y = rgb_resulted[hw_points].blue;227 corner_points[1].red.slope = dc_fixpt_zero;228 corner_points[1].green.slope = dc_fixpt_zero;229 corner_points[1].blue.slope = dc_fixpt_zero;230 231 // DCN3+ have 257 pts in lieu of no separate slope registers232 // Prior HW had 256 base+slope pairs233 lut_params->hw_points_num = hw_points + 1;234 235 k = 0;236 for (i = 1; i < MAX_REGIONS_NUMBER; i++) {237 if (seg_distr[k] != -1) {238 lut_params->arr_curve_points[k].segments_num =239 seg_distr[k];240 lut_params->arr_curve_points[i].offset =241 lut_params->arr_curve_points[k].offset + (1 << seg_distr[k]);242 }243 k++;244 }245 246 if (seg_distr[k] != -1)247 lut_params->arr_curve_points[k].segments_num = seg_distr[k];248 249 rgb = rgb_resulted;250 rgb_plus_1 = rgb_resulted + 1;251 rgb_minus_1 = rgb;252 253 if (fixpoint == true) {254 i = 1;255 while (i != hw_points + 2) {256 if (i >= hw_points) {257 if (dc_fixpt_lt(rgb_plus_1->red, rgb->red))258 rgb_plus_1->red = dc_fixpt_add(rgb->red,259 rgb_minus_1->delta_red);260 if (dc_fixpt_lt(rgb_plus_1->green, rgb->green))261 rgb_plus_1->green = dc_fixpt_add(rgb->green,262 rgb_minus_1->delta_green);263 if (dc_fixpt_lt(rgb_plus_1->blue, rgb->blue))264 rgb_plus_1->blue = dc_fixpt_add(rgb->blue,265 rgb_minus_1->delta_blue);266 }267 268 rgb->delta_red_reg = dc_fixpt_clamp_u0d10(rgb->delta_red);269 rgb->delta_green_reg = dc_fixpt_clamp_u0d10(rgb->delta_green);270 rgb->delta_blue_reg = dc_fixpt_clamp_u0d10(rgb->delta_blue);271 rgb->red_reg = dc_fixpt_clamp_u0d14(rgb->red);272 rgb->green_reg = dc_fixpt_clamp_u0d14(rgb->green);273 rgb->blue_reg = dc_fixpt_clamp_u0d14(rgb->blue);274 275 ++rgb_plus_1;276 rgb_minus_1 = rgb;277 ++rgb;278 ++i;279 }280 }281 cm3_helper_convert_to_custom_float(rgb_resulted,282 lut_params->corner_points,283 hw_points+1, fixpoint);284 285 return true;286}287 288#define NUM_DEGAMMA_REGIONS 12289 290 291bool cm3_helper_translate_curve_to_degamma_hw_format(292 const struct dc_transfer_func *output_tf,293 struct pwl_params *lut_params)294{295 struct curve_points3 *corner_points;296 struct pwl_result_data *rgb_resulted;297 struct pwl_result_data *rgb;298 struct pwl_result_data *rgb_plus_1;299 300 int32_t region_start, region_end;301 int32_t i;302 uint32_t j, k, seg_distr[MAX_REGIONS_NUMBER], increment, start_index, hw_points;303 304 if (output_tf == NULL || lut_params == NULL || output_tf->type == TF_TYPE_BYPASS)305 return false;306 307 corner_points = lut_params->corner_points;308 rgb_resulted = lut_params->rgb_resulted;309 hw_points = 0;310 311 memset(lut_params, 0, sizeof(struct pwl_params));312 memset(seg_distr, 0, sizeof(seg_distr));313 314 region_start = -NUM_DEGAMMA_REGIONS;315 region_end = 0;316 317 318 for (i = region_end - region_start; i < MAX_REGIONS_NUMBER ; i++)319 seg_distr[i] = -1;320 /* 12 segments321 * segments are from 2^-12 to 0322 */323 for (i = 0; i < NUM_DEGAMMA_REGIONS ; i++)324 seg_distr[i] = 4;325 326 for (k = 0; k < MAX_REGIONS_NUMBER; k++) {327 if (seg_distr[k] != -1)328 hw_points += (1 << seg_distr[k]);329 }330 331 j = 0;332 for (k = 0; k < (region_end - region_start); k++) {333 increment = NUMBER_SW_SEGMENTS / (1 << seg_distr[k]);334 start_index = (region_start + k + MAX_LOW_POINT) *335 NUMBER_SW_SEGMENTS;336 for (i = start_index; i < start_index + NUMBER_SW_SEGMENTS;337 i += increment) {338 if (j == hw_points - 1)339 break;340 if (i >= TRANSFER_FUNC_POINTS)341 return false;342 rgb_resulted[j].red = output_tf->tf_pts.red[i];343 rgb_resulted[j].green = output_tf->tf_pts.green[i];344 rgb_resulted[j].blue = output_tf->tf_pts.blue[i];345 j++;346 }347 }348 349 /* last point */350 start_index = (region_end + MAX_LOW_POINT) * NUMBER_SW_SEGMENTS;351 rgb_resulted[hw_points - 1].red = output_tf->tf_pts.red[start_index];352 rgb_resulted[hw_points - 1].green = output_tf->tf_pts.green[start_index];353 rgb_resulted[hw_points - 1].blue = output_tf->tf_pts.blue[start_index];354 355 corner_points[0].red.x = dc_fixpt_pow(dc_fixpt_from_int(2),356 dc_fixpt_from_int(region_start));357 corner_points[0].green.x = corner_points[0].red.x;358 corner_points[0].blue.x = corner_points[0].red.x;359 corner_points[1].red.x = dc_fixpt_pow(dc_fixpt_from_int(2),360 dc_fixpt_from_int(region_end));361 corner_points[1].green.x = corner_points[1].red.x;362 corner_points[1].blue.x = corner_points[1].red.x;363 364 corner_points[0].red.y = rgb_resulted[0].red;365 corner_points[0].green.y = rgb_resulted[0].green;366 corner_points[0].blue.y = rgb_resulted[0].blue;367 368 /* see comment above, m_arrPoints[1].y should be the Y value for the369 * region end (m_numOfHwPoints), not last HW point(m_numOfHwPoints - 1)370 */371 corner_points[1].red.y = rgb_resulted[hw_points - 1].red;372 corner_points[1].green.y = rgb_resulted[hw_points - 1].green;373 corner_points[1].blue.y = rgb_resulted[hw_points - 1].blue;374 corner_points[1].red.slope = dc_fixpt_zero;375 corner_points[1].green.slope = dc_fixpt_zero;376 corner_points[1].blue.slope = dc_fixpt_zero;377 378 if (output_tf->tf == TRANSFER_FUNCTION_PQ) {379 /* for PQ, we want to have a straight line from last HW X point,380 * and the slope to be such that we hit 1.0 at 10000 nits.381 */382 const struct fixed31_32 end_value =383 dc_fixpt_from_int(125);384 385 corner_points[1].red.slope = dc_fixpt_div(386 dc_fixpt_sub(dc_fixpt_one, corner_points[1].red.y),387 dc_fixpt_sub(end_value, corner_points[1].red.x));388 corner_points[1].green.slope = dc_fixpt_div(389 dc_fixpt_sub(dc_fixpt_one, corner_points[1].green.y),390 dc_fixpt_sub(end_value, corner_points[1].green.x));391 corner_points[1].blue.slope = dc_fixpt_div(392 dc_fixpt_sub(dc_fixpt_one, corner_points[1].blue.y),393 dc_fixpt_sub(end_value, corner_points[1].blue.x));394 }395 396 lut_params->hw_points_num = hw_points;397 398 k = 0;399 for (i = 1; i < MAX_REGIONS_NUMBER; i++) {400 if (seg_distr[k] != -1) {401 lut_params->arr_curve_points[k].segments_num =402 seg_distr[k];403 lut_params->arr_curve_points[i].offset =404 lut_params->arr_curve_points[k].offset + (1 << seg_distr[k]);405 }406 k++;407 }408 409 if (seg_distr[k] != -1)410 lut_params->arr_curve_points[k].segments_num = seg_distr[k];411 412 rgb = rgb_resulted;413 rgb_plus_1 = rgb_resulted + 1;414 415 i = 1;416 while (i != hw_points + 1) {417 if (dc_fixpt_lt(rgb_plus_1->red, rgb->red))418 rgb_plus_1->red = rgb->red;419 if (dc_fixpt_lt(rgb_plus_1->green, rgb->green))420 rgb_plus_1->green = rgb->green;421 if (dc_fixpt_lt(rgb_plus_1->blue, rgb->blue))422 rgb_plus_1->blue = rgb->blue;423 424 rgb->delta_red = dc_fixpt_sub(rgb_plus_1->red, rgb->red);425 rgb->delta_green = dc_fixpt_sub(rgb_plus_1->green, rgb->green);426 rgb->delta_blue = dc_fixpt_sub(rgb_plus_1->blue, rgb->blue);427 428 ++rgb_plus_1;429 ++rgb;430 ++i;431 }432 cm3_helper_convert_to_custom_float(rgb_resulted,433 lut_params->corner_points,434 hw_points, false);435 436 return true;437}438 439bool cm3_helper_convert_to_custom_float(440 struct pwl_result_data *rgb_resulted,441 struct curve_points3 *corner_points,442 uint32_t hw_points_num,443 bool fixpoint)444{445 struct custom_float_format fmt;446 447 struct pwl_result_data *rgb = rgb_resulted;448 449 uint32_t i = 0;450 451 fmt.exponenta_bits = 6;452 fmt.mantissa_bits = 12;453 fmt.sign = false;454 455 /* corner_points[0] - beginning base, slope offset for R,G,B456 * corner_points[1] - end base, slope offset for R,G,B457 */458 if (!convert_to_custom_float_format(corner_points[0].red.x, &fmt,459 &corner_points[0].red.custom_float_x)) {460 BREAK_TO_DEBUGGER();461 return false;462 }463 if (!convert_to_custom_float_format(corner_points[0].green.x, &fmt,464 &corner_points[0].green.custom_float_x)) {465 BREAK_TO_DEBUGGER();466 return false;467 }468 if (!convert_to_custom_float_format(corner_points[0].blue.x, &fmt,469 &corner_points[0].blue.custom_float_x)) {470 BREAK_TO_DEBUGGER();471 return false;472 }473 474 if (!convert_to_custom_float_format(corner_points[0].red.offset, &fmt,475 &corner_points[0].red.custom_float_offset)) {476 BREAK_TO_DEBUGGER();477 return false;478 }479 if (!convert_to_custom_float_format(corner_points[0].green.offset, &fmt,480 &corner_points[0].green.custom_float_offset)) {481 BREAK_TO_DEBUGGER();482 return false;483 }484 if (!convert_to_custom_float_format(corner_points[0].blue.offset, &fmt,485 &corner_points[0].blue.custom_float_offset)) {486 BREAK_TO_DEBUGGER();487 return false;488 }489 490 if (!convert_to_custom_float_format(corner_points[0].red.slope, &fmt,491 &corner_points[0].red.custom_float_slope)) {492 BREAK_TO_DEBUGGER();493 return false;494 }495 if (!convert_to_custom_float_format(corner_points[0].green.slope, &fmt,496 &corner_points[0].green.custom_float_slope)) {497 BREAK_TO_DEBUGGER();498 return false;499 }500 if (!convert_to_custom_float_format(corner_points[0].blue.slope, &fmt,501 &corner_points[0].blue.custom_float_slope)) {502 BREAK_TO_DEBUGGER();503 return false;504 }505 506 if (fixpoint == true) {507 corner_points[1].red.custom_float_y =508 dc_fixpt_clamp_u0d14(corner_points[1].red.y);509 corner_points[1].green.custom_float_y =510 dc_fixpt_clamp_u0d14(corner_points[1].green.y);511 corner_points[1].blue.custom_float_y =512 dc_fixpt_clamp_u0d14(corner_points[1].blue.y);513 } else {514 if (!convert_to_custom_float_format(corner_points[1].red.y,515 &fmt, &corner_points[1].red.custom_float_y)) {516 BREAK_TO_DEBUGGER();517 return false;518 }519 if (!convert_to_custom_float_format(corner_points[1].green.y,520 &fmt, &corner_points[1].green.custom_float_y)) {521 BREAK_TO_DEBUGGER();522 return false;523 }524 if (!convert_to_custom_float_format(corner_points[1].blue.y,525 &fmt, &corner_points[1].blue.custom_float_y)) {526 BREAK_TO_DEBUGGER();527 return false;528 }529 }530 531 fmt.mantissa_bits = 10;532 fmt.sign = false;533 534 if (!convert_to_custom_float_format(corner_points[1].red.x, &fmt,535 &corner_points[1].red.custom_float_x)) {536 BREAK_TO_DEBUGGER();537 return false;538 }539 if (!convert_to_custom_float_format(corner_points[1].green.x, &fmt,540 &corner_points[1].green.custom_float_x)) {541 BREAK_TO_DEBUGGER();542 return false;543 }544 if (!convert_to_custom_float_format(corner_points[1].blue.x, &fmt,545 &corner_points[1].blue.custom_float_x)) {546 BREAK_TO_DEBUGGER();547 return false;548 }549 550 if (!convert_to_custom_float_format(corner_points[1].red.slope, &fmt,551 &corner_points[1].red.custom_float_slope)) {552 BREAK_TO_DEBUGGER();553 return false;554 }555 if (!convert_to_custom_float_format(corner_points[1].green.slope, &fmt,556 &corner_points[1].green.custom_float_slope)) {557 BREAK_TO_DEBUGGER();558 return false;559 }560 if (!convert_to_custom_float_format(corner_points[1].blue.slope, &fmt,561 &corner_points[1].blue.custom_float_slope)) {562 BREAK_TO_DEBUGGER();563 return false;564 }565 566 if (hw_points_num == 0 || rgb_resulted == NULL || fixpoint == true)567 return true;568 569 fmt.mantissa_bits = 12;570 571 while (i != hw_points_num) {572 if (!convert_to_custom_float_format(rgb->red, &fmt,573 &rgb->red_reg)) {574 BREAK_TO_DEBUGGER();575 return false;576 }577 578 if (!convert_to_custom_float_format(rgb->green, &fmt,579 &rgb->green_reg)) {580 BREAK_TO_DEBUGGER();581 return false;582 }583 584 if (!convert_to_custom_float_format(rgb->blue, &fmt,585 &rgb->blue_reg)) {586 BREAK_TO_DEBUGGER();587 return false;588 }589 590 ++rgb;591 ++i;592 }593 594 return true;595}596 597bool is_rgb_equal(const struct pwl_result_data *rgb, uint32_t num)598{599 uint32_t i;600 bool ret = true;601 602 for (i = 0 ; i < num; i++) {603 if (rgb[i].red_reg != rgb[i].green_reg ||604 rgb[i].blue_reg != rgb[i].red_reg ||605 rgb[i].blue_reg != rgb[i].green_reg) {606 ret = false;607 break;608 }609 }610 return ret;611}612 613