| /* |
| * Copyright (c) 2018 Georg Ottinger. All Rights Reserved. |
| * |
| * Permission is hereby granted, free of charge, to any person obtaining a |
| * copy of this software and associated documentation files (the |
| * "Software"), to deal in the Software without restriction, including |
| * without limitation the rights to use, copy, modify, merge, publish, |
| * distribute, sub license, and/or sell copies of the Software, and to |
| * permit persons to whom the Software is furnished to do so, subject to |
| * the following conditions: |
| * |
| * The above copyright notice and this permission notice (including the |
| * next paragraph) shall be included in all copies or substantial portions |
| * of the Software. |
| * |
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
| * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
| * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. |
| * IN NO EVENT SHALL PRECISION INSIGHT AND/OR ITS SUPPLIERS BE LIABLE FOR |
| * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, |
| * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE |
| * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| */ |
| |
| /* This file includes code taken from vp9enc.c (libva-utils) copyright 2017 |
| * by Intel Cooperation and licensed under same conditions. |
| * This file includes code ported from vaapiencoder_vp8.cpp (libyami) copyright |
| * by 2014-2016 Intel Corporation. https://github.com/intel/libyami |
| * The original copyright and licence statement as below. |
| */ |
| |
| /* |
| * Copyright (C) 2014-2016 Intel Corporation. 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 <stdbool.h> |
| #include <stdio.h> |
| #include <string.h> |
| #include <stdlib.h> |
| #include <getopt.h> |
| #include <unistd.h> |
| #include <inttypes.h> |
| |
| #include <sys/time.h> |
| #include <sys/types.h> |
| #include <sys/stat.h> |
| #include <sys/mman.h> |
| #include <fcntl.h> |
| #include <assert.h> |
| #include <time.h> |
| #include <stdlib.h> |
| #include <pthread.h> |
| |
| #include <va/va.h> |
| #include <va/va_enc_vp8.h> |
| #include "va_display.h" |
| |
| #define MAX_XY_RESOLUTION 16364 |
| |
| #define KEY_FRAME 0 |
| #define INTER_FRAME 1 |
| |
| #define NUM_REF_SURFACES 4 |
| #define NUM_INPUT_SURFACES 2 |
| #define NUM_SURFACES_TOTAL (NUM_REF_SURFACES+NUM_INPUT_SURFACES) |
| #define SID_INPUT_PICTURE_0 (NUM_REF_SURFACES) |
| #define SID_INPUT_PICTURE_1 (NUM_REF_SURFACES+1) |
| #define NUM_BUFFERS 10 |
| |
| #define VP8ENC_OK 0 |
| #define VP8ENC_FAIL -1 |
| #define PARSE_OPTIONS_OK 0 |
| #define PARSE_OPTIONS_FAIL -1 |
| |
| #ifndef N_ELEMENTS |
| #define N_ELEMENTS(array) (sizeof(array)/sizeof(array[0])) |
| #endif |
| |
| #ifndef CHECK_VASTATUS |
| #define CHECK_VASTATUS(va_status,func) \ |
| if (va_status != VA_STATUS_SUCCESS) { \ |
| fprintf(stderr,"%s:%s (%d) failed,exit\n", __func__, func, __LINE__); \ |
| exit(1); \ |
| } |
| #endif |
| |
| |
| |
| static const struct option long_opts[] = { |
| {"help", no_argument, NULL, 0 }, |
| {"rcmode", required_argument, NULL, 1 }, |
| {"qp", required_argument, NULL, 2 }, |
| {"intra_period", required_argument, NULL, 3 }, |
| {"fb", required_argument, NULL, 4 }, |
| {"lf_level", required_argument, NULL, 5 }, |
| {"hrd_win", required_argument, NULL, 6}, |
| {"vbr_max", required_argument, NULL, 7}, |
| {"fn_num", required_argument, NULL, 8}, |
| {"error_resilient", no_argument, NULL, 9}, |
| {"debug", no_argument, NULL, 10}, |
| {"temp_svc", required_argument, NULL, 11}, |
| {NULL, no_argument, NULL, 0 } |
| }; |
| |
| |
| static const int default_rc_modes[4] = { |
| VA_RC_CQP, // 0 |
| VA_RC_CBR, // 1 |
| VA_RC_VBR, // 2 |
| VA_RC_NONE |
| }; |
| |
| struct vp8enc_settings { |
| int width; |
| int height; |
| int frame_rate; |
| int frame_size; |
| int loop_filter_level; |
| int clamp_qindex_low; |
| int clamp_qindex_high; |
| int intra_period; |
| int quantization_parameter; |
| int frame_bitrate; |
| int max_variable_bitrate; |
| int rc_mode; |
| int num_frames; |
| VAEntrypoint vaapi_entry_point; |
| int codedbuf_size; |
| int hrd_window; |
| int error_resilient; |
| int debug; |
| int temporal_svc_layers; |
| }; |
| |
| |
| static struct vp8enc_settings settings = |
| { |
| //Default Values - unless otherwise specified with command line options |
| .frame_rate = 30, |
| .loop_filter_level = 19, |
| .clamp_qindex_low = 9, |
| .clamp_qindex_high = 127, |
| .intra_period = 30, |
| .quantization_parameter = 60, |
| .frame_bitrate = -1, |
| .max_variable_bitrate = -1, |
| .num_frames = 0, |
| .rc_mode = VA_RC_CQP, |
| .vaapi_entry_point = VAEntrypointEncSlice, //VAEntrypointEncSliceLP would be LowPower Mode - but not supported with VP8Encoder |
| .hrd_window = 1500, |
| .error_resilient = 0, |
| .debug = 0, |
| .temporal_svc_layers = 1, |
| }; |
| |
| struct vp8enc_vaapi_context { |
| VADisplay display; |
| VAProfile profile; |
| VAContextID context_id; |
| VAConfigID config_id; |
| VAEncSequenceParameterBufferVP8 seq_param; |
| VAEncPictureParameterBufferVP8 pic_param; |
| VAQMatrixBufferVP8 q_matrix; |
| VASurfaceID surfaces[NUM_SURFACES_TOTAL]; |
| VASurfaceID recon_surface, last_ref_surface, golden_ref_surface, alt_ref_surface; |
| VASurfaceID input_surface; |
| VABufferID codedbuf_buf_id; |
| VABufferID va_buffers[NUM_BUFFERS]; |
| int num_va_buffers; |
| int is_golden_refreshed; |
| struct { |
| VAEncMiscParameterBuffer header; |
| VAEncMiscParameterHRD data; |
| } hrd_param; |
| struct { |
| VAEncMiscParameterBuffer header; |
| VAEncMiscParameterFrameRate data; |
| } frame_rate_param; |
| struct { |
| VAEncMiscParameterBuffer header; |
| VAEncMiscParameterRateControl data; |
| } rate_control_param; |
| struct { |
| pthread_t id; |
| int value; |
| FILE *input_fp; |
| int input_surface_num; |
| VASurfaceID input_surface; |
| int processed_frame; |
| } upload_thread; |
| }; |
| |
| static struct vp8enc_vaapi_context vaapi_context; |
| |
| |
| /******************************************** |
| * |
| * START: IVF Container Releated Stuff |
| * |
| ********************************************/ |
| static void |
| vp8enc_write_word(char *ptr, uint32_t value) |
| { |
| uint8_t *tmp; |
| |
| tmp = (uint8_t *)ptr; |
| *(tmp) = (value >> 0) & 0XFF; |
| *(tmp + 1) = (value >> 8) & 0XFF; |
| } |
| |
| static void |
| vp8enc_write_dword(char *ptr, uint32_t value) |
| { |
| uint8_t *tmp; |
| |
| tmp = (uint8_t *)ptr; |
| *(tmp) = (value >> 0) & 0XFF; |
| *(tmp + 1) = (value >> 8) & 0XFF; |
| *(tmp + 2) = (value >> 16) & 0XFF; |
| *(tmp + 3) = (value >> 24) & 0XFF; |
| } |
| |
| static void |
| vp8enc_write_qword(char *ptr, uint64_t value) |
| { |
| uint8_t *tmp; |
| |
| tmp = (uint8_t *)ptr; |
| *(tmp) = (value >> 0) & 0XFF; |
| *(tmp + 1) = (value >> 8) & 0XFF; |
| *(tmp + 2) = (value >> 16) & 0XFF; |
| *(tmp + 3) = (value >> 24) & 0XFF; |
| *(tmp + 4) = (value >> 32) & 0XFF; |
| *(tmp + 5) = (value >> 40) & 0XFF; |
| *(tmp + 6) = (value >> 48) & 0XFF; |
| *(tmp + 7) = (value >> 56) & 0XFF; |
| } |
| |
| static void |
| vp8enc_write_frame_header(FILE *vp8_output,uint32_t data_length, uint64_t timestamp) |
| { |
| char header[12]; |
| |
| vp8enc_write_dword(header, data_length); |
| vp8enc_write_qword(header + 4, timestamp ); |
| |
| fwrite(header, 1, 12, vp8_output); |
| } |
| |
| static void |
| vp8enc_write_ivf_header(FILE *vp8_file) |
| { |
| |
| |
| #define VP8_FOURCC 0x30385056 |
| |
| char header[32]; |
| |
| header[0] = 'D'; |
| header[1] = 'K'; |
| header[2] = 'I'; |
| header[3] = 'F'; |
| |
| vp8enc_write_word(header + 4, 0); |
| vp8enc_write_word(header + 6, 32); |
| vp8enc_write_dword(header + 8, VP8_FOURCC); |
| vp8enc_write_word(header + 12, settings.width); |
| vp8enc_write_word(header + 14, settings.height); |
| vp8enc_write_dword(header + 16, settings.frame_rate); |
| vp8enc_write_dword(header + 20, 1); |
| vp8enc_write_dword(header + 24, settings.num_frames); |
| vp8enc_write_dword(header + 28, 0); |
| |
| fwrite(header, 1, 32, vp8_file); |
| } |
| |
| /******************************************** |
| * |
| * END: IVF Container Releated Stuff |
| * |
| ********************************************/ |
| |
| |
| /******************************************** |
| * |
| * START: Read YUV Input File Releated Stuff |
| * |
| ********************************************/ |
| static void |
| vp8enc_upload_yuv_to_surface(FILE *yuv_fp, VASurfaceID surface_id, int current_frame) |
| { |
| VAImage surface_image; |
| VAStatus va_status; |
| void *surface_p = NULL; |
| uint8_t *y_src, *u_src, *v_src; |
| uint8_t *y_dst, *u_dst, *v_dst; |
| int y_size = settings.width * settings.height; |
| int u_size = (settings.width >> 1) * (settings.height >> 1); |
| int row, col; |
| char *yuv_mmap_ptr = NULL; |
| unsigned long long frame_start_pos, mmap_start; |
| int mmap_size; |
| |
| frame_start_pos = (unsigned long long)current_frame * settings.frame_size; |
| |
| mmap_start = frame_start_pos & (~0xfff); |
| mmap_size = (settings.frame_size + (frame_start_pos & 0xfff) + 0xfff) & (~0xfff); |
| yuv_mmap_ptr = mmap(0, mmap_size, PROT_READ, MAP_SHARED, |
| fileno(yuv_fp), mmap_start); |
| |
| if (yuv_mmap_ptr == MAP_FAILED) { |
| fprintf(stderr,"Error: Failed to mmap YUV file.\n"); |
| assert(0); |
| } |
| |
| y_src = (uint8_t*)yuv_mmap_ptr+(frame_start_pos & 0xfff); |
| u_src = y_src + y_size; /* UV offset for NV12 */ |
| v_src = y_src + y_size + u_size; |
| |
| |
| va_status = vaDeriveImage(vaapi_context.display, surface_id, &surface_image); |
| CHECK_VASTATUS(va_status,"vaDeriveImage"); |
| |
| vaMapBuffer(vaapi_context.display, surface_image.buf, &surface_p); |
| assert(VA_STATUS_SUCCESS == va_status); |
| |
| |
| y_dst = surface_p + surface_image.offsets[0]; |
| u_dst = surface_p + surface_image.offsets[1]; /* UV offset for NV12 */ |
| v_dst = surface_p + surface_image.offsets[2]; |
| |
| /* Y plane */ |
| for (row = 0; row < surface_image.height; row++) { |
| memcpy(y_dst, y_src, surface_image.width); |
| y_dst += surface_image.pitches[0]; |
| y_src += settings.width; |
| } |
| |
| if (surface_image.format.fourcc == VA_FOURCC_NV12) { /* UV plane */ |
| for (row = 0; row < surface_image.height / 2; row++) { |
| for (col = 0; col < surface_image.width / 2; col++) { |
| u_dst[col * 2] = u_src[col]; |
| u_dst[col * 2 + 1] = v_src[col]; |
| } |
| |
| u_dst += surface_image.pitches[1]; |
| u_src += (settings.width / 2); |
| v_src += (settings.width / 2); |
| } |
| } else if (surface_image.format.fourcc == VA_FOURCC_YV12 || |
| surface_image.format.fourcc == VA_FOURCC_I420) { |
| const int U = surface_image.format.fourcc == VA_FOURCC_I420 ? 1 : 2; |
| const int V = surface_image.format.fourcc == VA_FOURCC_I420 ? 2 : 1; |
| |
| u_dst = surface_p + surface_image.offsets[U]; |
| v_dst = surface_p + surface_image.offsets[V]; |
| |
| for (row = 0; row < surface_image.height / 2; row++) { |
| memcpy(u_dst, u_src, surface_image.width / 2); |
| memcpy(v_dst, v_src, surface_image.width / 2); |
| u_dst += surface_image.pitches[U]; |
| v_dst += surface_image.pitches[V]; |
| u_src += (settings.width / 2); |
| v_src += (settings.width / 2); |
| } |
| } |
| |
| vaUnmapBuffer(vaapi_context.display, surface_image.buf); |
| vaDestroyImage(vaapi_context.display, surface_image.image_id); |
| |
| if (yuv_mmap_ptr) |
| munmap(yuv_mmap_ptr, mmap_size); |
| } |
| |
| static void * |
| vp8enc_upload_thread_function(void *data) |
| { |
| vp8enc_upload_yuv_to_surface(vaapi_context.upload_thread.input_fp, vaapi_context.upload_thread.input_surface,vaapi_context.upload_thread.processed_frame); |
| |
| return NULL; |
| } |
| /******************************************** |
| * |
| * END: Read YUV Input File Releated Stuff |
| * |
| ********************************************/ |
| |
| void vp8enc_init_QMatrix(VAQMatrixBufferVP8 *qMatrix) |
| { |
| // When segmentation is disabled, only quantization_index[0] will be used |
| for (size_t i = 0; i < N_ELEMENTS(qMatrix->quantization_index); i++) { |
| qMatrix->quantization_index[i] = settings.quantization_parameter; |
| } |
| |
| for (size_t i = 0; i < N_ELEMENTS(qMatrix->quantization_index_delta); i++) { |
| qMatrix->quantization_index_delta[i] = 0; |
| } |
| |
| } |
| |
| void vp8enc_init_SequenceParameterBuffer(VAEncSequenceParameterBufferVP8* seqParam) |
| { |
| |
| memset(seqParam, 0, sizeof(VAEncSequenceParameterBufferVP8)); |
| |
| seqParam->frame_width = settings.width; |
| seqParam->frame_height = settings.height; |
| |
| if(settings.frame_bitrate > 0) |
| seqParam->bits_per_second = settings.frame_bitrate * 1000; |
| else |
| seqParam->bits_per_second = 0; |
| |
| seqParam->intra_period = settings.intra_period; |
| seqParam->error_resilient = settings.error_resilient; |
| |
| for (size_t i = 0; i < N_ELEMENTS(seqParam->reference_frames); i++) |
| seqParam->reference_frames[i] = VA_INVALID_ID; |
| } |
| |
| void vp8enc_init_PictureParameterBuffer(VAEncPictureParameterBufferVP8 *picParam) |
| { |
| memset(picParam, 0, sizeof(VAEncPictureParameterBufferVP8)); |
| |
| picParam->ref_last_frame = VA_INVALID_SURFACE; |
| picParam->ref_gf_frame = VA_INVALID_SURFACE; |
| picParam->ref_arf_frame = VA_INVALID_SURFACE; |
| |
| /* always show it */ |
| picParam->pic_flags.bits.show_frame = 1; |
| |
| for (size_t i = 0; i < N_ELEMENTS(picParam->loop_filter_level); i++) { |
| picParam->loop_filter_level[i] = settings.loop_filter_level; |
| } |
| |
| picParam->clamp_qindex_low = settings.clamp_qindex_low; |
| picParam->clamp_qindex_high = settings.clamp_qindex_high; |
| |
| } |
| |
| void vp8enc_set_refreshparameter_for_svct_2layers(VAEncPictureParameterBufferVP8 *picParam, int current_frame, int *is_golden_refreshed) |
| { |
| //Pattern taken from libyami |
| |
| picParam->ref_flags.bits.no_ref_arf = 1; |
| |
| if(! *is_golden_refreshed) |
| picParam->ref_flags.bits.no_ref_gf = 1; |
| |
| switch(current_frame % 2) { |
| case 0: |
| //Layer 0 |
| picParam->pic_flags.bits.refresh_last = 1; |
| picParam->ref_flags.bits.no_ref_gf = 1; |
| picParam->ref_flags.bits.temporal_id = 0; |
| break; |
| case 1: |
| //Layer 1 |
| picParam->pic_flags.bits.refresh_golden_frame = 1; |
| *is_golden_refreshed = 1; |
| picParam->ref_flags.bits.temporal_id = 1; |
| break; |
| } |
| } |
| |
| void vp8enc_set_refreshparameter_for_svct_3layers(VAEncPictureParameterBufferVP8 *picParam, int current_frame,int *is_golden_refreshed) |
| { |
| //Pattern taken from libyami - Note that the alternate frame is never referenced, |
| //this is because, libyami implementation suggests to be able to drop individual |
| //frames from Layer 2 on bad network connections |
| picParam->ref_flags.bits.no_ref_arf = 1; |
| |
| if(! *is_golden_refreshed) |
| picParam->ref_flags.bits.no_ref_gf = 1; |
| |
| switch(current_frame % 4) { |
| case 0: |
| //Layer 0 |
| picParam->pic_flags.bits.refresh_last = 1; |
| picParam->ref_flags.bits.no_ref_gf = 1; |
| picParam->ref_flags.bits.temporal_id = 0; |
| break; |
| case 1: |
| case 3: |
| //Layer 2 |
| picParam->pic_flags.bits.refresh_alternate_frame = 1; |
| picParam->ref_flags.bits.temporal_id = 2; |
| break; |
| case 2: |
| //Layer 1 |
| picParam->pic_flags.bits.refresh_golden_frame = 1; |
| *is_golden_refreshed = 1; |
| picParam->ref_flags.bits.temporal_id = 1; |
| break; |
| } |
| } |
| |
| void vp8enc_reset_picture_parameter_references(VAEncPictureParameterBufferVP8 *picParam) |
| { |
| picParam->ref_last_frame = VA_INVALID_SURFACE; |
| picParam->ref_gf_frame = VA_INVALID_SURFACE; |
| picParam->ref_arf_frame = VA_INVALID_SURFACE; |
| picParam->pic_flags.bits.refresh_last = 0; |
| picParam->pic_flags.bits.refresh_golden_frame = 0; |
| picParam->pic_flags.bits.refresh_alternate_frame = 0; |
| picParam->pic_flags.bits.copy_buffer_to_golden = 0; |
| picParam->pic_flags.bits.copy_buffer_to_alternate = 0; |
| picParam->ref_flags.bits.no_ref_last = 0; |
| picParam->ref_flags.bits.no_ref_gf = 0; |
| picParam->ref_flags.bits.no_ref_arf = 0; |
| } |
| |
| void vp8enc_update_picture_parameter(int frame_type, int current_frame) |
| { |
| VAEncPictureParameterBufferVP8 *picParam = &vaapi_context.pic_param; |
| |
| picParam->reconstructed_frame = vaapi_context.recon_surface; |
| |
| vp8enc_reset_picture_parameter_references(picParam); |
| |
| if (frame_type == KEY_FRAME) |
| { |
| picParam->ref_flags.bits.force_kf = 1; |
| picParam->pic_flags.bits.frame_type = KEY_FRAME; |
| vaapi_context.is_golden_refreshed = 0; |
| return; |
| } |
| |
| // INTER_FRAME |
| picParam->ref_flags.bits.force_kf = 0; |
| picParam->pic_flags.bits.frame_type = INTER_FRAME; |
| |
| switch(settings.temporal_svc_layers) |
| { |
| case 1: |
| //Standard behavoir only 1 Temporal Layer |
| picParam->pic_flags.bits.refresh_last = 1; |
| picParam->pic_flags.bits.copy_buffer_to_golden = 1; |
| picParam->pic_flags.bits.copy_buffer_to_alternate = 2; |
| picParam->ref_flags.bits.temporal_id = 0; |
| break; |
| case 2: |
| //2 Temporal Layers |
| vp8enc_set_refreshparameter_for_svct_2layers(picParam,current_frame,&vaapi_context.is_golden_refreshed); |
| break; |
| case 3: |
| //3 Temporal Layers |
| vp8enc_set_refreshparameter_for_svct_3layers(picParam,current_frame,&vaapi_context.is_golden_refreshed); |
| break; |
| default: |
| //should never happen |
| fprintf(stderr,"Error: Only 1,2 or 3 TemporalLayers supported.\n"); |
| assert(0); |
| break; |
| } |
| |
| if(!picParam->ref_flags.bits.no_ref_last) |
| picParam->ref_last_frame = vaapi_context.last_ref_surface; |
| if(!picParam->ref_flags.bits.no_ref_gf) |
| picParam->ref_gf_frame = vaapi_context.golden_ref_surface; |
| if(!picParam->ref_flags.bits.no_ref_arf) |
| picParam->ref_arf_frame = vaapi_context.alt_ref_surface; |
| |
| } |
| |
| VASurfaceID vp8enc_get_unused_surface() |
| { |
| VASurfaceID current_surface; |
| |
| for (size_t i = 0; i < NUM_REF_SURFACES; i++) { |
| current_surface = vaapi_context.surfaces[i]; |
| |
| if(current_surface != vaapi_context.last_ref_surface && current_surface != vaapi_context.golden_ref_surface && current_surface != vaapi_context.alt_ref_surface) |
| return current_surface; |
| } |
| |
| //No unused surface found - should never happen. |
| fprintf(stderr, "Error: No unused surface found!\n"); |
| assert(0); |
| |
| } |
| |
| VASurfaceID vp8enc_update_reference(VASurfaceID current_surface, VASurfaceID second_copy_surface, bool refresh_with_recon, int copy_flag) |
| { |
| if(refresh_with_recon) |
| return vaapi_context.recon_surface; |
| switch(copy_flag) |
| { |
| case 0: |
| return current_surface; |
| case 1: |
| return vaapi_context.last_ref_surface; |
| case 2: |
| return second_copy_surface; |
| default: // should never happen |
| fprintf(stderr, "Error: Invalid copy_buffer_to_X flag\n"); |
| assert(0); |
| } |
| |
| return VA_INVALID_ID; // should never happen |
| } |
| |
| |
| void vp8enc_update_reference_list(int frame_type) |
| { |
| |
| VAEncPictureParameterBufferVP8 *picParam = &vaapi_context.pic_param; |
| |
| if (frame_type == KEY_FRAME) |
| { |
| vaapi_context.last_ref_surface = vaapi_context.recon_surface; |
| vaapi_context.golden_ref_surface = vaapi_context.recon_surface; |
| vaapi_context.alt_ref_surface = vaapi_context.recon_surface; |
| } else { // INTER_FRAME |
| //check refresh_X and copy_buffer_to_golden_X and update references accordingly |
| if(picParam->pic_flags.bits.refresh_last) |
| vaapi_context.last_ref_surface = vaapi_context.recon_surface; |
| vaapi_context.golden_ref_surface = vp8enc_update_reference(vaapi_context.golden_ref_surface,vaapi_context.alt_ref_surface,picParam->pic_flags.bits.refresh_golden_frame, picParam->pic_flags.bits.copy_buffer_to_golden ); |
| vaapi_context.alt_ref_surface = vp8enc_update_reference(vaapi_context.alt_ref_surface,vaapi_context.golden_ref_surface,picParam->pic_flags.bits.refresh_alternate_frame, picParam->pic_flags.bits.copy_buffer_to_alternate ); |
| } |
| |
| vaapi_context.recon_surface = vp8enc_get_unused_surface(); |
| } |
| |
| void vp8enc_init_MiscParameterBuffers(VAEncMiscParameterHRD *hrd, VAEncMiscParameterFrameRate *frame_rate, VAEncMiscParameterRateControl *rate_control) |
| { |
| if(hrd != NULL) |
| { |
| if(settings.frame_bitrate) |
| { |
| hrd->initial_buffer_fullness = settings.frame_bitrate * settings.hrd_window / 2; |
| hrd->buffer_size = settings.frame_bitrate * settings.hrd_window; |
| } else { |
| hrd->initial_buffer_fullness = 0; |
| hrd->buffer_size = 0; |
| } |
| } |
| |
| if(frame_rate != NULL) |
| { |
| frame_rate->framerate = settings.frame_rate; |
| } |
| |
| if (rate_control != NULL) |
| { |
| rate_control->window_size = settings.hrd_window; |
| rate_control->initial_qp = settings.quantization_parameter; |
| rate_control->min_qp = settings.clamp_qindex_low; |
| //rate_control->rc_flags.bits.disable_bit_stuffing = 1; |
| |
| if(settings.rc_mode == VA_RC_VBR) |
| { |
| rate_control->bits_per_second = settings.max_variable_bitrate * 1000; |
| rate_control->target_percentage = (settings.frame_bitrate * 100) / settings.max_variable_bitrate; |
| } else { |
| rate_control->bits_per_second = settings.frame_bitrate * 1000; |
| rate_control->target_percentage = 95; |
| |
| } |
| } |
| } |
| |
| void vp8enc_create_EncoderPipe() |
| { |
| VAEntrypoint entrypoints[5]; |
| int num_entrypoints; |
| bool entrypoint_found; |
| VAConfigAttrib conf_attrib[2]; |
| VASurfaceAttrib surface_attrib; |
| int major_ver, minor_ver; |
| VAStatus va_status; |
| |
| vaapi_context.display = va_open_display(); |
| va_status = vaInitialize(vaapi_context.display, &major_ver, &minor_ver); |
| CHECK_VASTATUS(va_status, "vaInitialize"); |
| |
| vaQueryConfigEntrypoints(vaapi_context.display, vaapi_context.profile, entrypoints, |
| &num_entrypoints); |
| |
| entrypoint_found = true; |
| for(int i = 0; i < num_entrypoints;i++) |
| { |
| if (entrypoints[i] == settings.vaapi_entry_point) |
| entrypoint_found = true; |
| } |
| |
| if(entrypoint_found == false) |
| { |
| fprintf(stderr,"Error: VAEntrypoint not found!\n"); |
| assert(0); |
| } |
| |
| /* find out the format for the render target, and rate control mode */ |
| conf_attrib[0].type = VAConfigAttribRTFormat; |
| conf_attrib[1].type = VAConfigAttribRateControl; |
| vaGetConfigAttributes(vaapi_context.display, vaapi_context.profile, settings.vaapi_entry_point, |
| &conf_attrib[0], 2); |
| |
| if ((conf_attrib[0].value & VA_RT_FORMAT_YUV420) == 0) { |
| fprintf(stderr, "Error: Input colorspace YUV420 not supported, exit\n"); |
| assert(0); |
| } |
| |
| if ((conf_attrib[1].value & settings.rc_mode) == 0) { |
| /* Can't find matched RC mode */ |
| fprintf(stderr, "Error: Can't find the desired RC mode, exit\n"); |
| assert(0); |
| } |
| |
| conf_attrib[0].value = VA_RT_FORMAT_YUV420; /* set to desired RT format */ |
| conf_attrib[1].value = settings.rc_mode; /* set to desired RC mode */ |
| |
| va_status = vaCreateConfig(vaapi_context.display, vaapi_context.profile, settings.vaapi_entry_point, |
| &conf_attrib[0], 2,&vaapi_context.config_id); |
| CHECK_VASTATUS(va_status, "vaCreateConfig"); |
| |
| surface_attrib.type = VASurfaceAttribPixelFormat; |
| surface_attrib.flags = VA_SURFACE_ATTRIB_SETTABLE; |
| surface_attrib.value.type = VAGenericValueTypeInteger; |
| surface_attrib.value.value.i = VA_FOURCC_NV12; |
| |
| // Create surface (Reference Surfaces + Input Surfaces) |
| va_status = vaCreateSurfaces( |
| vaapi_context.display, |
| VA_RT_FORMAT_YUV420, settings.width, settings.height, |
| vaapi_context.surfaces, NUM_SURFACES_TOTAL, |
| &surface_attrib, 1 |
| ); |
| |
| CHECK_VASTATUS(va_status, "vaCreateSurfaces"); |
| |
| vaapi_context.recon_surface = vaapi_context.surfaces[0]; |
| vaapi_context.last_ref_surface = VA_INVALID_SURFACE; |
| vaapi_context.golden_ref_surface = VA_INVALID_SURFACE; |
| vaapi_context.alt_ref_surface = VA_INVALID_SURFACE; |
| vaapi_context.input_surface = vaapi_context.surfaces[NUM_REF_SURFACES]; // input surfaces trail the reference surfaces |
| |
| /* Create a context for this Encoder pipe */ |
| /* the surface is added to the render_target list when creating the context */ |
| va_status = vaCreateContext(vaapi_context.display, vaapi_context.config_id, |
| settings.width, settings.height, |
| VA_PROGRESSIVE, |
| vaapi_context.surfaces, NUM_SURFACES_TOTAL, |
| &vaapi_context.context_id); |
| |
| CHECK_VASTATUS(va_status, "vaCreateContext"); |
| |
| |
| } |
| |
| void vp8enc_destory_EncoderPipe() |
| { |
| pthread_join(vaapi_context.upload_thread.id,NULL); |
| vaDestroySurfaces(vaapi_context.display, vaapi_context.surfaces, NUM_SURFACES_TOTAL); |
| vaDestroyContext(vaapi_context.display,vaapi_context.context_id); |
| vaDestroyConfig(vaapi_context.display,vaapi_context.config_id); |
| vaTerminate(vaapi_context.display); |
| va_close_display(vaapi_context.display); |
| } |
| |
| |
| void vp8enc_init_VaapiContext() |
| { |
| vaapi_context.profile = VAProfileVP8Version0_3; |
| |
| vp8enc_init_SequenceParameterBuffer(&vaapi_context.seq_param); |
| vp8enc_init_PictureParameterBuffer(&vaapi_context.pic_param); |
| vp8enc_init_QMatrix(&vaapi_context.q_matrix); |
| |
| vaapi_context.hrd_param.header.type = VAEncMiscParameterTypeHRD; |
| vaapi_context.frame_rate_param.header.type = VAEncMiscParameterTypeFrameRate; |
| vaapi_context.rate_control_param.header.type = VAEncMiscParameterTypeRateControl; |
| vp8enc_init_MiscParameterBuffers(&vaapi_context.hrd_param.data, &vaapi_context.frame_rate_param.data,&vaapi_context.rate_control_param.data); |
| |
| for(size_t i = 0; i < N_ELEMENTS(vaapi_context.va_buffers);i++) |
| vaapi_context.va_buffers[i] = VA_INVALID_ID; |
| vaapi_context.num_va_buffers = 0; |
| |
| vaapi_context.is_golden_refreshed = 0; |
| } |
| |
| |
| |
| static int |
| vp8enc_store_coded_buffer(FILE *vp8_fp,uint64_t timestamp) |
| { |
| VACodedBufferSegment *coded_buffer_segment; |
| uint8_t *coded_mem; |
| int data_length; |
| VAStatus va_status; |
| VASurfaceStatus surface_status; |
| size_t w_items; |
| |
| va_status = vaSyncSurface(vaapi_context.display, vaapi_context.recon_surface); |
| CHECK_VASTATUS(va_status,"vaSyncSurface"); |
| |
| surface_status = 0; |
| va_status = vaQuerySurfaceStatus(vaapi_context.display, vaapi_context.recon_surface, &surface_status); |
| CHECK_VASTATUS(va_status,"vaQuerySurfaceStatus"); |
| |
| va_status = vaMapBuffer(vaapi_context.display, vaapi_context.codedbuf_buf_id, (void **)(&coded_buffer_segment)); |
| CHECK_VASTATUS(va_status,"vaMapBuffer"); |
| coded_mem = coded_buffer_segment->buf; |
| |
| if (coded_buffer_segment->status & VA_CODED_BUF_STATUS_SLICE_OVERFLOW_MASK) { |
| fprintf(stderr,"Error: CodeBuffer Size too small\n"); |
| vaUnmapBuffer(vaapi_context.display, vaapi_context.codedbuf_buf_id); |
| assert(0); |
| } |
| |
| data_length = coded_buffer_segment->size; |
| |
| vp8enc_write_frame_header(vp8_fp, data_length,timestamp); |
| |
| do { |
| w_items = fwrite(coded_mem, data_length, 1, vp8_fp); |
| } while (w_items != 1); |
| |
| if(settings.debug) |
| fprintf(stderr,"Timestamp: %ld Bytes written %d\n",timestamp,data_length); |
| |
| vaUnmapBuffer(vaapi_context.display, vaapi_context.codedbuf_buf_id); |
| |
| return 0; |
| } |
| |
| size_t vp8enc_get_FileSize(FILE *fp) |
| { |
| struct stat st; |
| fstat(fileno(fp), &st); |
| return st.st_size; |
| } |
| |
| int vp8enc_prepare_buffers(int frame_type) |
| { |
| int num_buffers = 0; |
| VABufferID *va_buffers; |
| VAStatus va_status; |
| VAEncPictureParameterBufferVP8 *picParam = &vaapi_context.pic_param; |
| |
| |
| va_buffers = vaapi_context.va_buffers; |
| /* coded buffer */ |
| va_status = vaCreateBuffer(vaapi_context.display, |
| vaapi_context.context_id, |
| VAEncCodedBufferType, |
| settings.codedbuf_size, 1, NULL, |
| &vaapi_context.codedbuf_buf_id); |
| CHECK_VASTATUS(va_status,"vaCreateBuffer"); |
| |
| /* sequence parameter set */ |
| va_status = vaCreateBuffer(vaapi_context.display, |
| vaapi_context.context_id, |
| VAEncSequenceParameterBufferType, |
| sizeof(vaapi_context.seq_param), 1, &vaapi_context.seq_param, |
| va_buffers); |
| CHECK_VASTATUS(va_status,"vaCreateBuffer"); |
| |
| va_buffers ++; num_buffers++; |
| |
| /* picture parameter set */ |
| picParam->coded_buf = vaapi_context.codedbuf_buf_id; |
| |
| va_status = vaCreateBuffer(vaapi_context.display, |
| vaapi_context.context_id, |
| VAEncPictureParameterBufferType, |
| sizeof(vaapi_context.pic_param), 1, &vaapi_context.pic_param, |
| va_buffers); |
| CHECK_VASTATUS(va_status,"vaCreateBuffer"); |
| va_buffers ++; num_buffers++; |
| |
| |
| |
| /* hrd parameter */ |
| vaCreateBuffer(vaapi_context.display, |
| vaapi_context.context_id, |
| VAEncMiscParameterBufferType, |
| sizeof(vaapi_context.hrd_param), 1, &vaapi_context.hrd_param, |
| va_buffers); |
| CHECK_VASTATUS(va_status, "vaCreateBuffer"); |
| |
| va_buffers ++; num_buffers++; |
| |
| /* QMatrix */ |
| va_status = vaCreateBuffer(vaapi_context.display, |
| vaapi_context.context_id, |
| VAQMatrixBufferType, |
| sizeof(vaapi_context.q_matrix), 1, &vaapi_context.q_matrix, |
| va_buffers); |
| CHECK_VASTATUS(va_status,"vaCreateBuffer"); |
| |
| |
| va_buffers ++; num_buffers++; |
| /* Create the Misc FR/RC buffer under non-CQP mode */ |
| if (settings.rc_mode != VA_RC_CQP && frame_type == KEY_FRAME) { |
| vaCreateBuffer(vaapi_context.display, |
| vaapi_context.context_id, |
| VAEncMiscParameterBufferType, |
| sizeof(vaapi_context.frame_rate_param),1,&vaapi_context.frame_rate_param, |
| va_buffers); |
| CHECK_VASTATUS(va_status, "vaCreateBuffer"); |
| |
| va_buffers ++; num_buffers++; |
| |
| vaCreateBuffer(vaapi_context.display, |
| vaapi_context.context_id, |
| VAEncMiscParameterBufferType, |
| sizeof(vaapi_context.rate_control_param),1,&vaapi_context.rate_control_param, |
| va_buffers); |
| CHECK_VASTATUS(va_status, "vaCreateBuffer"); |
| |
| va_buffers ++; num_buffers++; |
| } |
| |
| vaapi_context.num_va_buffers = num_buffers; |
| |
| return num_buffers; |
| } |
| |
| |
| |
| |
| static void |
| vp8enc_render_picture() |
| { |
| VAStatus va_status; |
| |
| va_status = vaBeginPicture(vaapi_context.display, |
| vaapi_context.context_id, |
| vaapi_context.input_surface); |
| CHECK_VASTATUS(va_status,"vaBeginPicture"); |
| |
| |
| va_status = vaRenderPicture(vaapi_context.display, |
| vaapi_context.context_id, |
| vaapi_context.va_buffers, |
| vaapi_context.num_va_buffers); |
| CHECK_VASTATUS(va_status,"vaRenderPicture"); |
| |
| va_status = vaEndPicture(vaapi_context.display, vaapi_context.context_id); |
| CHECK_VASTATUS(va_status,"vaEndPicture"); |
| |
| } |
| |
| void vp8enc_destroy_buffers() |
| { |
| VAStatus va_status; |
| |
| for(int i = 0; i < vaapi_context.num_va_buffers; i++) { |
| if (vaapi_context.va_buffers[i] != VA_INVALID_ID) { |
| va_status = vaDestroyBuffer(vaapi_context.display, vaapi_context.va_buffers[i]); |
| CHECK_VASTATUS(va_status,"vaDestroyBuffer"); |
| vaapi_context.va_buffers[i] = VA_INVALID_ID; |
| } |
| } |
| |
| if (vaapi_context.codedbuf_buf_id != VA_INVALID_ID) { |
| va_status = vaDestroyBuffer(vaapi_context.display, vaapi_context.codedbuf_buf_id); |
| CHECK_VASTATUS(va_status,"vaDestroyBuffer"); |
| vaapi_context.codedbuf_buf_id = VA_INVALID_ID; |
| } |
| |
| } |
| void vp8enc_show_help () |
| { |
| printf("Usage: vp8enc <width> <height> <input_yuvfile> <output_vp8> additional_option\n"); |
| printf("output_vp8 should use *.ivf\n"); |
| printf("The additional option is listed\n"); |
| printf("-f <frame rate> \n"); |
| printf("--intra_period <key_frame interval>\n"); |
| printf("--qp <quantization parameter> \n"); |
| printf("--rcmode <rate control mode> 0: CQP, 1: CBR, 2: VBR\n"); |
| printf("--fb <bitrate> (kbps unit)\n"); |
| printf("--lf_level <loop filter level> [0-63]\n"); |
| printf("--hrd_win <num> [1000-8000]\n"); |
| printf("--vbr_max <num> (kbps unit. It should be greater than fb)\n"); |
| printf("--fn_num <num>\n how many frames to be encoded\n"); |
| printf("--error_resilient Turn on Error resilient mode\n"); |
| printf("--debug Turn debug info on\n"); |
| printf("--temp_svc <num> number of temporal layers 2 or 3\n"); |
| } |
| |
| void parameter_check(const char *param, int val, int min, int max) |
| { |
| if(val < min || val > max) |
| { |
| fprintf(stderr,"Error: %s out of range (%d..%d) \n",param,min,max); |
| exit(VP8ENC_FAIL); |
| } |
| } |
| |
| void parameter_check_positive(const char *param, int val, int min) |
| { |
| if(val < 1) |
| { |
| fprintf(stderr,"Error: %s demands a positive value greater than %d \n",param,min); |
| exit(VP8ENC_FAIL); |
| } |
| } |
| |
| int parse_options(int ac,char *av[]) |
| { |
| int c, long_index, tmp_input; |
| while (1) { |
| c = getopt_long_only(ac,av,"hf:?",long_opts,&long_index); |
| |
| if (c == -1) |
| break; |
| |
| switch(c) { |
| case 'f': |
| tmp_input = atoi(optarg); |
| parameter_check_positive("-f",tmp_input,1); |
| settings.frame_rate = tmp_input; |
| break; |
| case 1: |
| tmp_input = atoi(optarg); |
| parameter_check("--rcmode", tmp_input, 0, 2); |
| settings.rc_mode = default_rc_modes[tmp_input]; |
| break; |
| case 2: |
| tmp_input = atoi(optarg); |
| parameter_check("--qp", tmp_input, 0, 255); |
| settings.quantization_parameter = tmp_input; |
| break; |
| case 3: |
| tmp_input = atoi(optarg); |
| parameter_check_positive("--intra_period",tmp_input,1); |
| settings.intra_period = tmp_input; |
| break; |
| case 4: |
| tmp_input = atoi(optarg); |
| parameter_check_positive("--fb",tmp_input,1); |
| settings.frame_bitrate = tmp_input; |
| break; |
| case 5: |
| tmp_input = atoi(optarg); |
| parameter_check("--lf_level",tmp_input, 0, 63); |
| settings.loop_filter_level = tmp_input; |
| break; |
| case 6: |
| tmp_input = atoi(optarg); |
| parameter_check("--hrd_win",tmp_input,1000,8000); |
| settings.hrd_window = tmp_input; |
| break; |
| case 7: |
| tmp_input = atoi(optarg); |
| parameter_check_positive("--vbr_max",tmp_input,1); |
| settings.max_variable_bitrate = tmp_input; |
| break; |
| case 8: |
| tmp_input = atoi(optarg); |
| parameter_check_positive("--fn_num" ,tmp_input,1); |
| settings.num_frames = tmp_input; |
| break; |
| case 9: |
| settings.error_resilient = 1; |
| break; |
| case 10: |
| settings.debug = 1; |
| break; |
| case 11: |
| tmp_input = atoi(optarg); |
| parameter_check("--temp_svc",tmp_input,2,3); |
| settings.temporal_svc_layers = tmp_input; |
| break; |
| case 'h': |
| case 0: |
| default: |
| return PARSE_OPTIONS_FAIL; |
| break; |
| } |
| } |
| return PARSE_OPTIONS_OK; |
| } |
| |
| int main(int argc, char *argv[]) |
| { |
| int current_frame, frame_type; |
| FILE *fp_vp8_output, *fp_yuv_input; |
| uint64_t timestamp; |
| struct timeval t1,t2; |
| double fps, elapsed_time; |
| |
| |
| if(argc < 5) { |
| vp8enc_show_help(); |
| return VP8ENC_FAIL; |
| } |
| |
| if(parse_options(argc-4, &argv[4]) != PARSE_OPTIONS_OK) |
| { |
| vp8enc_show_help(); |
| return VP8ENC_FAIL; |
| } |
| |
| settings.width = atoi(argv[1]); |
| parameter_check("Width", settings.width, 16, MAX_XY_RESOLUTION); |
| |
| settings.height = atoi(argv[2]); |
| parameter_check("Height", settings.height, 16, MAX_XY_RESOLUTION); |
| |
| fp_yuv_input = fopen(argv[3],"rb"); |
| if(fp_yuv_input == NULL) |
| { |
| fprintf(stderr,"Error: Couldn't open input file.\n"); |
| return VP8ENC_FAIL; |
| } |
| vaapi_context.upload_thread.input_fp = fp_yuv_input; |
| |
| fp_vp8_output = fopen(argv[4],"wb"); |
| if(fp_vp8_output == NULL) |
| { |
| fprintf(stderr,"Error: Couldn't open output file.\n"); |
| return VP8ENC_FAIL; |
| } |
| |
| if( settings.rc_mode == VA_RC_VBR && settings.max_variable_bitrate < settings.frame_bitrate) |
| { |
| fprintf(stderr,"Error: max. variable bitrate should be greater than frame bitrate (--vbr_max >= --fb)\n"); |
| return VP8ENC_FAIL; |
| } |
| |
| if( settings.temporal_svc_layers == 2 && settings.intra_period % 2) |
| fprintf(stderr,"Warning: Choose Key-Frame interval (--intra_period) to be integer mutliply of 2 to match temporal layer pattern"); |
| |
| if ( settings.temporal_svc_layers == 3 && settings.intra_period % 4 ) |
| fprintf(stderr,"Warning: Choose Key-Frame interval (--intra_period) to be integer mutliply of 4 to match temporal layer pattern"); |
| |
| |
| settings.frame_size = settings.width * settings.height * 3 / 2; //NV12 Colorspace - For a 2x2 group of pixels, you have 4 Y samples and 1 U and 1 V sample. |
| if(!settings.num_frames) |
| settings.num_frames = vp8enc_get_FileSize(fp_yuv_input)/(size_t)settings.frame_size; |
| settings.codedbuf_size = settings.width * settings.height; //just a generous assumptions |
| |
| fprintf(stderr,"Info: Encoding total of %d frames.\n",settings.num_frames); |
| |
| gettimeofday(&t1,0); //Measure Runtime |
| |
| vp8enc_init_VaapiContext(); |
| vp8enc_create_EncoderPipe(); |
| |
| vp8enc_write_ivf_header(fp_vp8_output); |
| |
| current_frame = 0; |
| timestamp = 0; |
| vaapi_context.input_surface = vaapi_context.surfaces[SID_INPUT_PICTURE_0]; |
| vaapi_context.upload_thread.input_surface_num = SID_INPUT_PICTURE_0; |
| |
| while (current_frame < settings.num_frames) |
| { |
| fprintf(stderr,"\rProcessing frame: %d",current_frame); |
| |
| if ( (current_frame%settings.intra_period) == 0) |
| frame_type = KEY_FRAME; |
| else |
| frame_type = INTER_FRAME; |
| |
| if(current_frame == 0) { |
| // Preload first input_surface |
| vp8enc_upload_yuv_to_surface(fp_yuv_input, vaapi_context.input_surface, current_frame); //prefill |
| } else { |
| // wait for input processing thread to finish |
| pthread_join(vaapi_context.upload_thread.id, NULL); |
| vaapi_context.input_surface = vaapi_context.upload_thread.input_surface; |
| } |
| |
| // Start Upload thread |
| if((current_frame + 1) < settings.num_frames) { |
| vaapi_context.upload_thread.processed_frame = current_frame + 1; |
| |
| if(vaapi_context.upload_thread.input_surface_num == SID_INPUT_PICTURE_0) |
| vaapi_context.upload_thread.input_surface_num = SID_INPUT_PICTURE_1; |
| else |
| vaapi_context.upload_thread.input_surface_num = SID_INPUT_PICTURE_0; |
| |
| vaapi_context.upload_thread.input_surface = vaapi_context.surfaces[vaapi_context.upload_thread.input_surface_num]; |
| vaapi_context.upload_thread.value = pthread_create(&vaapi_context.upload_thread.id,NULL,vp8enc_upload_thread_function,NULL); |
| } |
| |
| |
| vp8enc_update_picture_parameter(frame_type, current_frame); |
| vp8enc_prepare_buffers(frame_type); |
| |
| vp8enc_render_picture(); |
| |
| vp8enc_store_coded_buffer(fp_vp8_output, timestamp); |
| vp8enc_destroy_buffers(); |
| |
| vp8enc_update_reference_list(frame_type); |
| |
| current_frame ++; |
| timestamp ++; |
| } |
| |
| vp8enc_destory_EncoderPipe(); |
| fclose(fp_vp8_output); |
| fclose(fp_yuv_input); |
| |
| gettimeofday(&t2,0); |
| elapsed_time = (double)(t2.tv_sec-t1.tv_sec)+(double)(t2.tv_usec-t1.tv_usec)/1000000.0; |
| fps = (double)current_frame/elapsed_time; |
| |
| fprintf(stderr, "\nProcessed %d frames in %.0f ms (%.2f FPS)\n",current_frame,elapsed_time*1000.0,fps); |
| |
| return VP8ENC_OK; |
| } |