Merge pull request #20392 from fpetrogalli:aarch64-semihosting

AArch64 semihosting

* [ts] Disable filesystem support in the TS module.

Because of this change, all the tests loading data will file, but tat
least the core module can be tested with the following line:

    opencv_test_core --gtest_filter=-"*Core_InputOutput*:*Core_globbing.accuracy*"

* [aarch64] Build OpenCV for AArch64 semihosting.

This patch provide a toolchain file that allows to build the library
for semihosting applications [1]. Minimal changes have been applied to
the code to be able to compile with a baremetal toolchain.

[1] https://developer.arm.com/documentation/100863/latest

The option `CV_SEMIHOSTING` is used to guard the bits in the code that
are specific to the target.

To build the code:

    cmake ../opencv/ \
        -DCMAKE_TOOLCHAIN_FILE=../opencv/platforms/semihosting/aarch64-semihosting.toolchain.cmake \
        -DSEMIHOSTING_TOOLCHAIN_PATH=/path/to/baremetal-toolchain/bin/ \
        -DBUILD_EXAMPLES=ON -GNinja

A barematel toolchain for targeting aarch64 semihosting can be found
at [2], under `aarch64-none-elf`.

[2] https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-a/downloads

The folder `samples/semihosting` provides two example semihosting
applications.

The two binaries can be executed on the host platform with:

    qemu-aarch64 ./bin/example_semihosting_histogram
    qemu-aarch64 ./bin/example_semihosting_norm

Similarly, the test and perf executables of the modules can be run
with:

    qemu-aarch64 ./bin/opecv_[test|perf]_<module>

Notice that filesystem support is disabled by the toolchain file,
hence some of the test that depend on filesystem support will fail.

* [semihosting] Remove blank like at the end of file. [NFC]

The spurious blankline was reported by
https://pullrequest.opencv.org/buildbot/builders/precommit_docs/builds/31158.

* [semihosting] Make the raw pixel file generation OS independent.

Use the facilities provided by Cmake to generate the header file
instead of a shell script, so that the build doesn't fail on systems
that do not have a unix shell.

* [semihosting] Rename variable for semihosting compilation.

* [semihosting] Move the cmake configuration to a variable file.

* [semihosting] Make the guard macro private for the core module.

* [semihosting] Remove space. [NFC]

* [semihosting] Improve comment with information about semihosting. [NFC]

* [semihosting] Update license statement on top of sourvce file. [NFC]

* [semihosting] Replace BM_SUFFIX with SEMIHOSTING_SUFFIX. [NFC]

* [semihosting] Remove double space. [NFC]

* [semihosting] Add some text output to the sample applications.

* [semihosting] Remove duplicate entry in cmake configuration. [NFCI]

* [semihosting] Replace `long` with `int` in sample apps. [NFCI]

* [semihosting] Use `configure_file` to create the random pixels. [NFCI]

* [semihosting][bugfix] Fix name of cmakedefine variable.

* [semihosting][samples] Use CV_8UC1 for grayscale images. [NFCI]

* [semihosting] Add readme file.

* [semihosting] Remove blank like at the end of README. [NFC]

This fixes the failure at
https://pullrequest.opencv.org/buildbot/builders/precommit_docs/builds/31272.
This commit is contained in:
Francesco Petrogalli 2021-07-21 16:46:05 +01:00 committed by GitHub
parent 755f4f324b
commit d29c7e7871
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 321 additions and 11 deletions

View File

@ -513,6 +513,7 @@ OCV_OPTION(ENABLE_CONFIG_VERIFICATION "Fail build if actual configuration doesn'
OCV_OPTION(OPENCV_ENABLE_MEMALIGN "Enable posix_memalign or memalign usage" ON)
OCV_OPTION(OPENCV_DISABLE_FILESYSTEM_SUPPORT "Disable filesystem support" OFF)
OCV_OPTION(OPENCV_DISABLE_THREAD_SUPPORT "Build the library without multi-threaded code." OFF)
OCV_OPTION(OPENCV_SEMIHOSTING "Build the library for semihosting target (Arm). See https://developer.arm.com/documentation/100863/latest." OFF)
OCV_OPTION(ENABLE_PYLINT "Add target with Pylint checks" (BUILD_DOCS OR BUILD_EXAMPLES) IF (NOT CMAKE_CROSSCOMPILING AND NOT APPLE_FRAMEWORK) )
OCV_OPTION(ENABLE_FLAKE8 "Add target with Python flake8 checker" (BUILD_DOCS OR BUILD_EXAMPLES) IF (NOT CMAKE_CROSSCOMPILING AND NOT APPLE_FRAMEWORK) )

View File

@ -16,3 +16,6 @@ endmacro()
variable_watch(OPENCV_DISABLE_THREAD_SUPPORT ocv_change_mode_var)
set(OPENCV_DISABLE_THREAD_SUPPORT "${OPENCV_DISABLE_THREAD_SUPPORT}")
variable_watch(OPENCV_SEMIHOSTING ocv_change_mode_var)
set(OPENCV_SEMIHOSTING "${OPENCV_SEMIHOSTING}")

View File

@ -0,0 +1,10 @@
set(CV_TRACE OFF)
# These third parties libraries are incompatible with the semihosting
# toolchain.
set(WITH_JPEG OFF)
set(WITH_OPENEXR OFF)
set(WITH_TIFF OFF)
# Turn off `libpng` for some linking issues.
set(WITH_PNG OFF)

View File

@ -7,8 +7,6 @@
static inline double cbrt(double x) { return (double)cv::cubeRoot((float)x); };
#endif
using namespace std;
namespace {
void solveQuartic(const double *factors, double *realRoots) {
const double &a4 = factors[0];
@ -30,29 +28,29 @@ void solveQuartic(const double *factors, double *realRoots) {
double q3 = (72 * r4 * p4 - 2 * p4 * p4 * p4 - 27 * q4 * q4) / 432; // /=2
double t; // *=2
complex<double> w;
std::complex<double> w;
if (q3 >= 0)
w = -sqrt(static_cast<complex<double> >(q3 * q3 - p3 * p3 * p3)) - q3;
w = -std::sqrt(static_cast<std::complex<double> >(q3 * q3 - p3 * p3 * p3)) - q3;
else
w = sqrt(static_cast<complex<double> >(q3 * q3 - p3 * p3 * p3)) - q3;
w = std::sqrt(static_cast<std::complex<double> >(q3 * q3 - p3 * p3 * p3)) - q3;
if (w.imag() == 0.0) {
w.real(cbrt(w.real()));
w.real(std::cbrt(w.real()));
t = 2.0 * (w.real() + p3 / w.real());
} else {
w = pow(w, 1.0 / 3);
t = 4.0 * w.real();
}
complex<double> sqrt_2m = sqrt(static_cast<complex<double> >(-2 * p4 / 3 + t));
std::complex<double> sqrt_2m = sqrt(static_cast<std::complex<double> >(-2 * p4 / 3 + t));
double B_4A = -a3 / (4 * a4);
double complex1 = 4 * p4 / 3 + t;
#if defined(__clang__) && defined(__arm__) && (__clang_major__ == 3 || __clang_major__ == 4) && !defined(__ANDROID__)
// details: https://github.com/opencv/opencv/issues/11135
// details: https://github.com/opencv/opencv/issues/11056
complex<double> complex2 = 2 * q4;
complex2 = complex<double>(complex2.real() / sqrt_2m.real(), 0);
std::complex<double> complex2 = 2 * q4;
complex2 = std::complex<double>(complex2.real() / sqrt_2m.real(), 0);
#else
complex<double> complex2 = 2 * q4 / sqrt_2m;
std::complex<double> complex2 = 2 * q4 / sqrt_2m;
#endif
double sqrt_2m_rh = sqrt_2m.real() / 2;
double sqrt1 = sqrt(-(complex1 + complex2)).real() / 2;

View File

@ -157,6 +157,10 @@ if(OPENCV_DISABLE_THREAD_SUPPORT)
ocv_target_compile_definitions(${the_module} PUBLIC "OPENCV_DISABLE_THREAD_SUPPORT=1")
endif()
if(OPENCV_SEMIHOSTING)
ocv_target_compile_definitions(${the_module} PRIVATE "-DOPENCV_SEMIHOSTING")
endif(OPENCV_SEMIHOSTING)
if(HAVE_HPX)
ocv_target_link_libraries(${the_module} LINK_PRIVATE "${HPX_LIBRARIES}")
endif()

View File

@ -888,6 +888,7 @@ T minNonZero(const T& val_1, const T& val_2)
static
int getNumberOfCPUs_()
{
#ifndef OPENCV_SEMIHOSTING
/*
* Logic here is to try different methods of getting CPU counts and return
* the minimum most value as it has high probablity of being right and safe.
@ -979,6 +980,9 @@ int getNumberOfCPUs_()
#endif
return ncpus != 0 ? ncpus : 1;
#else // OPENCV_SEMIHOSTING
return 1;
#endif //OPENCV_SEMIHOSTING
}
int getNumberOfCPUs()

View File

@ -72,7 +72,9 @@
#if defined _WIN32 || defined WINCE
# include <windows.h>
#else
#if OPENCV_HAVE_FILESYSTEM_SUPPORT
# include <dirent.h>
#endif
# include <sys/stat.h>
#endif

View File

@ -1067,6 +1067,7 @@ class GTEST_API_ UnitTestImpl {
void AddTestInfo(Test::SetUpTestCaseFunc set_up_tc,
Test::TearDownTestCaseFunc tear_down_tc,
TestInfo* test_info) {
#if OPENCV_HAVE_FILESYSTEM_SUPPORT
// In order to support thread-safe death tests, we need to
// remember the original working directory when the test program
// was first invoked. We cannot do this in RUN_ALL_TESTS(), as
@ -1079,6 +1080,7 @@ class GTEST_API_ UnitTestImpl {
GTEST_CHECK_(!original_working_dir_.IsEmpty())
<< "Failed to get the current working directory.";
}
#endif
GetTestCase(test_info->test_case_name(),
test_info->type_param(),
@ -9165,6 +9167,7 @@ static bool IsPathSeparator(char c) {
// Returns the current working directory, or "" if unsuccessful.
FilePath FilePath::GetCurrentDir() {
#if OPENCV_HAVE_FILESYSTEM_SUPPORT
#if GTEST_OS_WINDOWS_MOBILE || GTEST_OS_WINDOWS_PHONE || GTEST_OS_WINDOWS_RT
// Windows CE doesn't have a current directory, so we just return
// something reasonable.
@ -9183,6 +9186,9 @@ FilePath FilePath::GetCurrentDir() {
# endif // GTEST_OS_NACL
return FilePath(result == NULL ? "" : cwd);
#endif // GTEST_OS_WINDOWS_MOBILE
#else // OPENCV_HAVE_FILESYSTEM_SUPPORT
return FilePath("");
#endif // OPENCV_HAVE_FILESYSTEM_SUPPORT
}
// Returns a copy of the FilePath with the case-insensitive extension removed.
@ -9391,6 +9397,7 @@ bool FilePath::CreateDirectoriesRecursively() const {
// directory for any reason, including if the parent directory does not
// exist. Not named "CreateDirectory" because that's a macro on Windows.
bool FilePath::CreateFolder() const {
#if OPENCV_HAVE_FILESYSTEM_SUPPORT
#if GTEST_OS_WINDOWS_MOBILE
FilePath removed_sep(this->RemoveTrailingPathSeparator());
LPCWSTR unicode = String::AnsiToUtf16(removed_sep.c_str());
@ -9406,6 +9413,9 @@ bool FilePath::CreateFolder() const {
return this->DirectoryExists(); // An error is OK if the directory exists.
}
return true; // No error.
#else // OPENCV_HAVE_FILESYSTEM_SUPPORT
return false;
#endif // OPENCV_HAVE_FILESYSTEM_SUPPORT
}
// If input name has a trailing separator character, remove it and return the

View File

@ -0,0 +1,40 @@
# This file is part of OpenCV project.
# It is subject to the license terms in the LICENSE file found in the top-level directory
# of this distribution and at http://opencv.org/license.html
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR AArch64)
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
set(PORT_FILE ${CMAKE_SOURCE_DIR}/platforms/semihosting/include/aarch64_semihosting_port.hpp)
set(COMMON_FLAGS "--specs=rdimon.specs -DOPENCV_INCLUDE_PORT_FILE=\\\"${PORT_FILE}\\\"")
set(CMAKE_AR ${SEMIHOSTING_TOOLCHAIN_PATH}aarch64-none-elf-ar${CMAKE_EXECUTABLE_SUFFIX})
set(CMAKE_ASM_COMPILER ${SEMIHOSTING_TOOLCHAIN_PATH}aarch64-none-elf-gcc${CMAKE_EXECUTABLE_SUFFIX})
set(CMAKE_C_COMPILER ${SEMIHOSTING_TOOLCHAIN_PATH}aarch64-none-elf-gcc${CMAKE_EXECUTABLE_SUFFIX})
set(CMAKE_CXX_COMPILER ${SEMIHOSTING_TOOLCHAIN_PATH}aarch64-none-elf-g++${CMAKE_EXECUTABLE_SUFFIX})
set(CMAKE_LINKER ${SEMIHOSTING_TOOLCHAIN_PATH}aarch64-none-elf-ld${CMAKE_EXECUTABLE_SUFFIX})
set(CMAKE_OBJCOPY ${SEMIHOSTING_TOOLCHAIN_PATH}aarch64-none-elf-objcopy${CMAKE_EXECUTABLE_SUFFIX} CACHE INTERNAL "")
set(CMAKE_RANLIB ${SEMIHOSTING_TOOLCHAIN_PATH}aarch64-none-elf-ranlib${CMAKE_EXECUTABLE_SUFFIX} CACHE INTERNAL "")
set(CMAKE_SIZE ${SEMIHOSTING_TOOLCHAIN_PATH}aarch64-none-elf-size${CMAKE_EXECUTABLE_SUFFIX} CACHE INTERNAL "")
set(CMAKE_STRIP ${SEMIHOSTING_TOOLCHAIN_PATH}aarch64-none-elf-strip${CMAKE_EXECUTABLE_SUFFIX} CACHE INTERNAL "")
set(CMAKE_C_FLAGS ${COMMON_FLAGS} CACHE INTERNAL "")
set(CMAKE_CXX_FLAGS ${COMMON_FLAGS} CACHE INTERNAL "")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(OPENCV_SEMIHOSTING ON)
set(OPENCV_DISABLE_THREAD_SUPPORT ON)
set(OPENCV_DISABLE_FILESYSTEM_SUPPORT ON)
set(BUILD_SHARED_LIBS OFF)
set(OPENCV_FORCE_3RDPARTY_BUILD OFF)
# Enable newlib.
add_definitions(-D_GNU_SOURCE)
add_definitions(-D_POSIX_PATH_MAX=0)

View File

@ -0,0 +1,42 @@
// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html.
#ifndef AARCH64_BAREMETAL_PORT_HPP
#define AARCH64_BAREMETAL_PORT_HPP
#include <malloc.h> // Needed for `memalign`.
#include <sys/errno.h> // Needed for `ENOMEM`.
// -std=c++11 is missing the following definitions when targeting
// semihosting on aarch64.
#if __cplusplus == 201103L
#include <cmath>
#define M_PI 3.14159265358979323846
#define M_SQRT2 1.41421356237309504880
namespace std {
inline double cbrt(double x) {
return ::cbrt(x);
}
inline double copysign(double mag, double sgn) {
return ::copysign(mag, sgn);
}
} //namespace std
#endif // __cplusplus == 201103L
extern "C" {
// Redirect the implementation of `posix_memalign` to `memalign`
// as the former is
// missing at link time. https://pubs.opengroup.org/onlinepubs/9699919799/functions/posix_memalign.html
__attribute__((weak)) int posix_memalign(void **memptr, size_t alignment, size_t size) {
void * ptr = memalign(alignment, size);
if (ptr != NULL) {
*memptr = ptr;
return 0;
}
return ENOMEM;
}
} // extern "C"
#endif

View File

@ -45,7 +45,12 @@ endif()
if(INSTALL_PYTHON_EXAMPLES)
add_subdirectory(python)
endif()
# The examples in this folder will work with a semihosting version of
# OpenCV. For more information about semihosting, see
# https://developer.arm.com/documentation/100863/latest
if(OPENCV_SEMIHOSTING)
add_subdirectory(semihosting)
endif()
ocv_install_example_src("." CMakeLists.txt samples_utils.cmake)
if(INSTALL_C_EXAMPLES)
install(DIRECTORY data DESTINATION "${OPENCV_SAMPLES_SRC_INSTALL_PATH}" COMPONENT samples_data)

View File

@ -0,0 +1,10 @@
# This file is part of OpenCV project.
# It is subject to the license terms in the LICENSE file found in the top-level directory
# of this distribution and at http://opencv.org/license.html
set(SEMIHOSTING_SUFFIX semihosting)
add_subdirectory(include)
set(RAW_PIXEL_INCLUDE ${CMAKE_CURRENT_BINARY_DIR}/include)
add_subdirectory(histogram)
add_subdirectory(norm)

View File

@ -0,0 +1,27 @@
# Arm semihosting
This folder contain a toolchain file and a couple of examples for
building OpenCV based applications that can run in an [Arm
semihosting](https://developer.arm.com/documentation/100863/latest)
setup.
OpenCV can be compiled to target a semihosting platform as follows:
```
cmake ../opencv/ \
-DCMAKE_TOOLCHAIN_FILE=../opencv/platforms/semihosting/aarch64-semihosting.toolchain.cmake \
-DSEMIHOSTING_TOOLCHAIN_PATH=/path/to/baremetal-toolchain/bin/ \
-DBUILD_EXAMPLES=ON -GNinja
```
A barematel toolchain for targeting aarch64 semihosting can be found
[here](https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-a/downloads),
under `aarch64-none-elf`.
The code of the examples in the `norm` and `histogram` folders can be
executed with qemu in Linux userspace:
```
qemu-aarch64 ./bin/example_semihosting_histogram
qemu-aarch64 ./bin/example_semihosting_norm
```

View File

@ -0,0 +1,26 @@
# This file is part of OpenCV project.
# It is subject to the license terms in the LICENSE file found in the top-level directory
# of this distribution and at http://opencv.org/license.html
set(PROJECT_NAME histogram)
project(${PROJECT_NAME})
ocv_install_example_src(histogram *.cpp *.hpp CMakeLists.txt)
set(LOCAL_DEPS
opencv_core
opencv_imgproc
${OPENCV_MODULES_PUBLIC}
${OpenCV_LIB_COMPONENTS})
ocv_check_dependencies(${LOCAL_DEPS})
if(NOT OCV_DEPENDENCIES_FOUND)
return()
endif()
ocv_define_sample(histogram histogram.cpp ${SEMIHOSTING_SUFFIX})
ocv_include_modules_recurse(${LOCAL_DEPS})
target_include_directories(${histogram} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(${histogram} PRIVATE ${RAW_PIXEL_INCLUDE})
ocv_target_link_libraries(${histogram} PRIVATE ${OPENCV_LINKER_LIBS}
${LOCAL_DEPS})

View File

@ -0,0 +1,43 @@
// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html
#include <opencv2/imgproc.hpp>
#include <opencv2/imgcodecs.hpp>
#include <cstdint>
#include <array>
#include <iostream>
#include "raw_pixels.hpp"
#define IMG_ROWS 100
#define IMG_COLS 100
static_assert(IMG_ROWS * IMG_COLS <= RAW_PIXELS_SIZE, "Incompatible size");
int main(void)
{
// Number of experiment runs
int no_runs = 2;
// https://docs.opencv.org/master/d3/d63/classcv_1_1Mat.html
cv::Mat src_new(IMG_ROWS, IMG_COLS, CV_8UC1, (void *)raw_pixels);
// Set parameters
int imgCount = 1;
const int channels[] = {0};
cv::Mat mask = cv::Mat();
cv::Mat hist;
int dims = 1;
const int hist_sizes[] = {256};
float Range[] = {0,256};
const float *ranges[] = {Range};
// Run calc Hist
for(int i=0; i < no_runs; i++){
std::cout << "Running iteration # "<< i << std::endl;
cv::calcHist(&src_new, imgCount, channels, mask, hist, dims, hist_sizes, ranges);
}
return 0;
}

View File

@ -0,0 +1,16 @@
# Populate a C array with random data.
set(RAW_PIXELS_SIZE 102400)
set(RAW_PIXELS_HEADER ${CMAKE_CURRENT_BINARY_DIR}/raw_pixels.hpp)
set(RAW_PIXELS_HEADER_IN ${CMAKE_CURRENT_SOURCE_DIR}/raw_pixels.hpp.in)
set(RAW_PIXEL_VALUES "")
# Seed the random number generator.
string(RANDOM LENGTH 8 ALPHABET 0123456789abcdf RANDOM_SEED 314 number)
math(EXPR LOOP_RANGE "${RAW_PIXELS_SIZE} - 1")
foreach(i RANGE ${LOOP_RANGE})
string(RANDOM LENGTH 8 ALPHABET 0123456789abcdf number)
string(CONCAT RAW_PIXEL_VALUES ${RAW_PIXEL_VALUES} "0x${number}, \\\n")
endforeach()
configure_file(${RAW_PIXELS_HEADER_IN} ${RAW_PIXELS_HEADER})

View File

@ -0,0 +1,11 @@
#ifndef RAW_PIXELS_HPP
#define RAW_PIXELS_HP
#include <cstdint>
#cmakedefine RAW_PIXEL_VALUES @RAW_PIXEL_VALUES@
#cmakedefine RAW_PIXELS_SIZE @RAW_PIXELS_SIZE@
static std::uint32_t raw_pixels[RAW_PIXELS_SIZE] = {
RAW_PIXEL_VALUES
};
#endif //RAW_PIXELS_HPP

View File

@ -0,0 +1,25 @@
# This file is part of OpenCV project.
# It is subject to the license terms in the LICENSE file found in the top-level directory
# of this distribution and at http://opencv.org/license.html
set(PROJECT_NAME norm)
project(${PROJECT_NAME})
ocv_install_example_src(norm *.cpp *.hpp CMakeLists.txt)
set(LOCAL_DEPS
opencv_core
${OPENCV_MODULES_PUBLIC}
${OpenCV_LIB_COMPONENTS})
ocv_check_dependencies(${LOCAL_DEPS})
if(NOT OCV_DEPENDENCIES_FOUND)
return()
endif()
ocv_define_sample(norm norm.cpp ${SEMIHOSTING_SUFFIX})
ocv_include_modules_recurse(${LOCAL_DEPS})
target_include_directories(${norm} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(${norm} PRIVATE ${RAW_PIXEL_INCLUDE})
ocv_target_link_libraries(${norm} PRIVATE ${OPENCV_LINKER_LIBS}
${LOCAL_DEPS})

View File

@ -0,0 +1,33 @@
// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html
#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>
#include <cstdint>
#include <array>
#include <iostream>
#include "raw_pixels.hpp"
#define IMG_ROWS 100
#define IMG_COLS 100
static_assert(IMG_ROWS * IMG_COLS <= RAW_PIXELS_SIZE, "Incompatible size");
int main(void)
{
// Number of experiment runs
int no_runs = 2;
// https://docs.opencv.org/master/d3/d63/classcv_1_1Mat.html
cv::Mat src(IMG_ROWS, IMG_COLS, CV_8UC1, (void *)raw_pixels);
// Run calc Hist
for(int i=0; i < no_runs; i++){
std::cout << "Running iteration # "<< i << std::endl;
cv::norm(src);
}
return 0;
}