Commit 48b282f9 authored by Ghislain MARY's avatar Ghislain MARY

Add arm64 support for Android.

parent b532b145
......@@ -30,6 +30,16 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include "TargetConditionals.h"
#endif
#if defined(__arm__) || defined(__arm64__) || defined(__aarch64__) || defined(_M_ARM)
#define MS_HAS_ARM 1
#endif
#if defined(__ARM_NEON__) || defined(__ARM_NEON)
#define MS_HAS_ARM_NEON 1
#endif
#if MS_HAS_ARM_NEON && !(defined(__arm64__) || defined(__aarch64__))
#define MS_HAS_ARM_NEON_32 1
#endif
#ifndef MS2_DEPRECATED
#if defined(_MSC_VER)
#define MS2_DEPRECATED __declspec(deprecated)
......
......@@ -23,11 +23,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#include <mediastreamer2/msfilter.h>
#if defined(__arm__) || defined(__arm64__) || defined(_M_ARM)
#define MS_HAS_ARM 1
#endif
/* some global constants for video MSFilter(s) */
#define MS_VIDEO_SIZE_UNKNOWN_W 0
#define MS_VIDEO_SIZE_UNKNOWN_H 0
......
......@@ -98,6 +98,14 @@ public class Version {
}
return cpuabis;
}
private static boolean isArm64() {
try {
return getCpuAbis().get(0).startsWith("arm64-v8a");
} catch (Throwable e) {
Log.e(e);
}
return false;
}
private static boolean isArmv7() {
try {
return getCpuAbis().get(0).startsWith("armeabi-v7");
......@@ -130,7 +138,7 @@ public class Version {
return !isArmv5();
}
public static boolean hasFastCpuWithAsmOptim() {
return (!isX86() && !isArmv5() && hasNeon()) || isX86();
return isX86() || isArm64() || (!isArmv5() && hasNeon());
}
public static boolean isVideoCapable() {
return !Version.sdkStrictlyBelow(5) && Version.hasFastCpu();
......
......@@ -170,7 +170,7 @@ public class AndroidVideoWindowImpl {
return mBitmap;
}
public void setOpenGLESDisplay(int ptr) {
public void setOpenGLESDisplay(long ptr) {
if (!useGLrendering)
Log.e("View class does not match Video display filter used (you must use a GL View)");
renderer.setOpenGLESDisplay(ptr);
......@@ -199,7 +199,7 @@ public class AndroidVideoWindowImpl {
}
private static class Renderer implements GLSurfaceView.Renderer {
int ptr;
long ptr;
boolean initPending;
int width, height;
......@@ -208,7 +208,7 @@ public class AndroidVideoWindowImpl {
initPending = false;
}
public void setOpenGLESDisplay(int ptr) {
public void setOpenGLESDisplay(long ptr) {
/*
* Synchronize this with onDrawFrame:
* - they are called from different threads (Rendering thread and Linphone's one)
......
......@@ -19,6 +19,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
package org.linphone.mediastream.video.display;
public class OpenGLESDisplay {
public static native void init(int ptr, int width, int height);
public static native void render(int ptr);
public static native void init(long ptr, int width, int height);
public static native void render(long ptr);
}
......@@ -255,7 +255,7 @@ void dumpMemory(void *obj, size_t size){
size_t i;
ms_message("Dumping memory at %p",obj);
for (i=0;i<size;i+=sizeof(long)){
ms_message("%4i\t%lx",i,*(long*)(((uint8_t*)obj)+i));
ms_message("%4i\t%lx",(int)i,*(long*)(((uint8_t*)obj)+i));
}
}
......
......@@ -216,7 +216,7 @@ static int android_display_set_window(MSFilter *f, void *arg){
ms_filter_lock(f);
oldsurf=ad->surf;
if (jsurface!=NULL) ad->surf=(Surface*)jenv->GetIntField(jsurface,ad->surface_id);
if (jsurface!=NULL) ad->surf=(Surface*)jenv->GetLongField(jsurface,ad->surface_id);
else ad->surf=NULL;
if (ad->surf)
sym_Android_RefBase_incStrong(ad->surf,NULL);
......
......@@ -47,7 +47,7 @@ static void android_display_init(MSFilter *f){
if (wc==0){
ms_fatal("Could not find org/linphone/mediastream/video/AndroidVideoWindowImpl class !");
}
ad->set_opengles_display_id=(*jenv)->GetMethodID(jenv,wc,"setOpenGLESDisplay","(I)V");
ad->set_opengles_display_id=(*jenv)->GetMethodID(jenv,wc,"setOpenGLESDisplay","(J)V");
ad->request_render_id=(*jenv)->GetMethodID(jenv,wc,"requestRender","()V");
if (ad->set_opengles_display_id == 0)
ms_error("Could not find 'setOpenGLESDisplay' method\n");
......@@ -136,10 +136,9 @@ static int android_display_set_window(MSFilter *f, void *arg){
}
if (window) {
unsigned int ptr = (unsigned int)ad->ogl;
ad->android_video_window=(*jenv)->NewGlobalRef(jenv, window);
ms_message("Sending opengles_display pointer as long: %p -> %u", ad->ogl, ptr);
(*jenv)->CallVoidMethod(jenv,window,ad->set_opengles_display_id, ptr);
ms_message("Sending opengles_display pointer (%p)", ad->ogl);
(*jenv)->CallVoidMethod(jenv,window,ad->set_opengles_display_id, (jlong)ad->ogl);
}else ad->android_video_window=NULL;
if (old_window)
......
......@@ -64,11 +64,11 @@ static void resample_init(MSFilter *obj){
ResampleData* data=resample_data_new();
#ifdef SPEEX_LIB_SET_CPU_FEATURES
#ifdef ANDROID
if (android_getCpuFamily() == ANDROID_CPU_FAMILY_ARM
&& (android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON) != 0) {
if (((android_getCpuFamily() == ANDROID_CPU_FAMILY_ARM) && ((android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON) != 0))
|| (android_getCpuFamily() == ANDROID_CPU_FAMILY_ARM64)) {
data->cpuFeatures = SPEEX_LIB_CPU_FEATURE_NEON;
}
#elif defined(__ARM_NEON__)
#elif MS_HAS_ARM_NEON
data->cpuFeatures = SPEEX_LIB_CPU_FEATURE_NEON;
#endif
ms_message("speex_lib_ctl init with neon ? %d", (data->cpuFeatures == SPEEX_LIB_CPU_FEATURE_NEON));
......@@ -108,14 +108,14 @@ static int resample_channel_adapt(int in_nchannels, int out_nchannels, mblk_t *i
static void resample_init_speex(ResampleData *dt){
int err=0;
int quality=SPEEX_RESAMPLER_QUALITY_VOIP; /*default value is voip*/
#if defined(__arm__) || defined(_M_ARM) /*on ARM, NEON optimization are mandatory to support this quality, else using basic mode*/
#if MS_HAS_ARM /*on ARM, NEON optimization are mandatory to support this quality, else using basic mode*/
#if SPEEX_LIB_SET_CPU_FEATURES
if (dt->cpuFeatures != SPEEX_LIB_CPU_FEATURE_NEON)
quality=SPEEX_RESAMPLER_QUALITY_MIN;
#elif !defined(__ARM_NEON__)
#elif !MS_HAS_ARM_NEON
quality=SPEEX_RESAMPLER_QUALITY_MIN;
#endif /*SPEEX_LIB_SET_CPU_FEATURES*/
#endif /*defined(__arm__) || defined(_M_ARM)*/
#endif /*MS_HAS_ARM*/
ms_message("Initializing speex resampler in mode [%s] ",(quality==SPEEX_RESAMPLER_QUALITY_VOIP?"voip":"min"));
dt->handle=speex_resampler_init(dt->in_nchannels, dt->input_rate, dt->output_rate, quality, &err);
}
......
......@@ -65,10 +65,10 @@ static void enc_init(MSFilter *f){
f->data=s;
#ifdef SPEEX_LIB_SET_CPU_FEATURES
#ifdef __ARM_NEON__
#if MS_HAS_ARM_NEON
#ifdef ANDROID
if (android_getCpuFamily() == ANDROID_CPU_FAMILY_ARM
&& (android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON) != 0) {
if (((android_getCpuFamily() == ANDROID_CPU_FAMILY_ARM) && ((android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON) != 0))
|| (android_getCpuFamily() == ANDROID_CPU_FAMILY_ARM64)) {
cpuFeatures = SPEEX_LIB_CPU_FEATURE_NEON;
}
#else
......
......@@ -586,12 +586,12 @@ void ogl_display_zoom(struct opengles_display* gldisp, float* params) {
}
#ifdef ANDROID
JNIEXPORT void JNICALL Java_org_linphone_mediastream_video_display_OpenGLESDisplay_init(JNIEnv * env, jobject obj, jint ptr, jint width, jint height) {
JNIEXPORT void JNICALL Java_org_linphone_mediastream_video_display_OpenGLESDisplay_init(JNIEnv * env, jobject obj, jlong ptr, jint width, jint height) {
struct opengles_display* d = (struct opengles_display*) ptr;
ogl_display_init(d, width, height);
}
JNIEXPORT void JNICALL Java_org_linphone_mediastream_video_display_OpenGLESDisplay_render(JNIEnv * env, jobject obj, jint ptr) {
JNIEXPORT void JNICALL Java_org_linphone_mediastream_video_display_OpenGLESDisplay_render(JNIEnv * env, jobject obj, jlong ptr) {
struct opengles_display* d = (struct opengles_display*) ptr;
ogl_display_render(d, 0);
}
......
......@@ -82,8 +82,8 @@ void ogl_display_zoom(struct opengles_display* gldisp, float* params);
#ifdef ANDROID
#include <jni.h>
JNIEXPORT void JNICALL Java_org_linphone_mediastream_video_display_OpenGLESDisplay_init(JNIEnv * env, jobject obj, jint ptr, jint width, jint height);
JNIEXPORT void JNICALL Java_org_linphone_mediastream_video_display_OpenGLESDisplay_render(JNIEnv * env, jobject obj, jint ptr);
JNIEXPORT void JNICALL Java_org_linphone_mediastream_video_display_OpenGLESDisplay_init(JNIEnv * env, jobject obj, jlong ptr, jint width, jint height);
JNIEXPORT void JNICALL Java_org_linphone_mediastream_video_display_OpenGLESDisplay_render(JNIEnv * env, jobject obj, jlong ptr);
#endif
......
......@@ -634,7 +634,7 @@ static MSScalerDesc android_scaler={
#include "cpu-features.h"
#endif
#if defined(ANDROID) && defined(MS_HAS_ARM)
#if defined(ANDROID) && defined(MS_HAS_ARM) && !defined(__aarch64__)
extern MSScalerDesc ms_android_scaler;
#endif
......@@ -644,7 +644,7 @@ static MSScalerDesc *scaler_impl=NULL;
MSScalerContext *ms_scaler_create_context(int src_w, int src_h, MSPixFmt src_fmt,
int dst_w, int dst_h, MSPixFmt dst_fmt, int flags){
if (!scaler_impl){
#if defined(ANDROID) && defined(MS_HAS_ARM)
#if defined(ANDROID) && defined(MS_HAS_ARM) && !defined(__aarch64__)
if (android_getCpuFamily() == ANDROID_CPU_FAMILY_ARM && (android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON) != 0){
scaler_impl = &ms_android_scaler;
}
......@@ -711,7 +711,7 @@ static void rotate_plane_down_scale_by_2(int wDest, int hDest, int full_width, c
#ifdef ANDROID
static int hasNeon = -1;
#elif defined (__ARM_NEON__)
#elif MS_HAS_ARM_NEON
static int hasNeon = 1;
#elif MS_HAS_ARM
static int hasNeon = 0;
......@@ -731,9 +731,10 @@ mblk_t *copy_ycbcrbiplanar_to_true_yuv_with_rotation_and_down_scale_by_2(MSYuvBu
#ifdef ANDROID
if (hasNeon == -1) {
hasNeon = (android_getCpuFamily() == ANDROID_CPU_FAMILY_ARM && (android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON) != 0);
#ifdef __arm64__
ms_warning("Warning: ARM64 NEON routines for video rotation are not yes implemented for Android: using SOFT version!");
hasNeon = (((android_getCpuFamily() == ANDROID_CPU_FAMILY_ARM) && ((android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON) != 0))
|| (android_getCpuFamily() == ANDROID_CPU_FAMILY_ARM64));
#ifdef __aarch64__
ms_warning("Warning: ARM64 NEON routines for video rotation are not yet implemented for Android: using SOFT version!");
#endif
}
#endif
......
......@@ -23,7 +23,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#if MS_HAS_ARM
#ifdef __ARM_NEON__
#if MS_HAS_ARM_NEON
#include <arm_neon.h>
#endif
......@@ -187,7 +187,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
static MS2_INLINE void rotate_block_8x8_clockwise(const unsigned char* src, int src_width, unsigned char* dest,int dest_width) {
#ifdef __ARM_NEON__
#if MS_HAS_ARM_NEON
__asm (MATRIX_LOAD_8X8
MATRIX_TRANSPOSE_8X8
VERTICAL_SYMETRIE_8x8
......@@ -201,7 +201,7 @@ static MS2_INLINE void rotate_block_8x8_clockwise(const unsigned char* src, int
/*rotate and scale down blocks of 16x16 into 8x8*/
static MS2_INLINE void rotate_and_scale_down_block_16x16_clockwise(const unsigned char* src, int src_width, unsigned char* dest,int dest_width) {
#ifdef __ARM_NEON__
#if MS_HAS_ARM_NEON
__asm (
LOAD_16x16_IN_8x8
MATRIX_TRANSPOSE_8X8
......@@ -217,7 +217,7 @@ static MS2_INLINE void rotate_and_scale_down_block_16x16_clockwise(const unsigne
/*rotate and scale down blocks of 16x16 into 8x8*/
static MS2_INLINE void rotate_and_scale_down_block_8x8_anticlockwise(const unsigned char* src, int src_width, unsigned char* dest,int dest_width) {
#ifdef __ARM_NEON__
#if MS_HAS_ARM_NEON
__asm (
LOAD_16x16_IN_8x8
MATRIX_TRANSPOSE_8X8
......@@ -230,7 +230,7 @@ static MS2_INLINE void rotate_and_scale_down_block_8x8_anticlockwise(const unsig
}
static MS2_INLINE void rotate_block_8x8_anticlockwise(const unsigned char* src, int src_width, unsigned char* dest,int dest_width) {
#ifdef __ARM_NEON__
#if MS_HAS_ARM_NEON
__asm (MATRIX_LOAD_8X8
MATRIX_TRANSPOSE_8X8
HORIZONTAL_SYM_AND_STORE_8X8
......@@ -242,7 +242,7 @@ static MS2_INLINE void rotate_block_8x8_anticlockwise(const unsigned char* src,
}
void rotate_down_scale_plane_neon_clockwise(int wDest, int hDest, int full_width, const uint8_t* src, uint8_t* dst, bool_t down_scale) {
#ifdef __ARM_NEON__
#if MS_HAS_ARM_NEON
char src_block_width=down_scale?16:8;
char dest_block_width=down_scale?src_block_width/2:src_block_width;
int hSrc = down_scale?wDest*2:wDest;
......@@ -271,7 +271,7 @@ void rotate_down_scale_plane_neon_clockwise(int wDest, int hDest, int full_width
}
void rotate_down_scale_plane_neon_anticlockwise(int wDest, int hDest, int full_width, const uint8_t* src, uint8_t* dst,bool_t down_scale) {
#ifdef __ARM_NEON__
#if MS_HAS_ARM_NEON
char src_block_width=down_scale?16:8;
char dest_block_width=down_scale?src_block_width/2:src_block_width;
int hSrc = down_scale?wDest*2:wDest;
......@@ -300,7 +300,7 @@ void rotate_down_scale_plane_neon_anticlockwise(int wDest, int hDest, int full_w
}
void rotate_down_scale_cbcr_to_cr_cb(int wDest, int hDest, int full_width, const uint8_t* cbcr_src, uint8_t* cr_dst, uint8_t* cb_dst,bool_t clockWise,bool_t down_scale) {
#ifdef __ARM_NEON__
#if MS_HAS_ARM_NEON
int hSrc = down_scale?wDest*2:wDest;
int wSrc = down_scale?hDest*2:hDest;
int src_stride = 2*full_width;
......@@ -380,7 +380,7 @@ void rotate_down_scale_cbcr_to_cr_cb(int wDest, int hDest, int full_width, const
#endif
}
#ifdef __ARM_NEON__
#if MS_HAS_ARM_NEON
static void reverse_and_down_scale_32bytes_neon(const unsigned char* src, unsigned char* dest) {
__asm (/*load 16x1 pixel
......@@ -452,11 +452,11 @@ static void deinterlace_down_scale_and_reverse_2x16bytes_neon(const unsigned cha
);
}
#endif // __ARM_NEON__
#endif // MS_HAS_ARM_NEON
void deinterlace_down_scale_and_rotate_180_neon(const uint8_t* ysrc, const uint8_t* cbcrsrc, uint8_t* ydst, uint8_t* udst, uint8_t* vdst, int w, int h, int y_byte_per_row,int cbcr_byte_per_row,bool_t down_scale) {
#ifdef __ARM_NEON__
#if MS_HAS_ARM_NEON
int y,x;
int src_h=down_scale?2*h:h;
int src_w=down_scale?2*w:w;
......@@ -515,7 +515,7 @@ void deinterlace_and_rotate_180_neon(const uint8_t* ysrc, const uint8_t* cbcrsrc
#endif /* defined(__arm__), the above functions are not used in iOS 64bits, so only the function below is implemented for __arm64__ */
void deinterlace_down_scale_neon(const uint8_t* ysrc, const uint8_t* cbcrsrc, uint8_t* ydst, uint8_t* u_dst, uint8_t* v_dst, int w, int h, int y_byte_per_row,int cbcr_byte_per_row,bool_t down_scale) {
#ifdef __ARM_NEON__
#if MS_HAS_ARM_NEON
char y_inc = down_scale?2:1;
char x_inc = down_scale?32:16;
int src_h = down_scale?2*h:h;
......
......@@ -18,7 +18,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "mediastreamer2/msvideo.h"
#ifdef __ARM_NEON__
#if MS_HAS_ARM_NEON
#include <arm_neon.h>
#endif
......@@ -66,7 +66,7 @@ static void init_premults(void){
}
#if !defined (__ARM_NEON__)
#if !MS_HAS_ARM_NEON
static inline void yuv2rgb_4x2(const uint8_t *y1, const uint8_t *y2, const uint8_t *u, const uint8_t *v, int16_t *r1, int16_t *g1, int16_t *b1, int16_t *r2, int16_t *g2, int16_t *b2){
int32_t py1[4];
......@@ -119,7 +119,7 @@ static inline void yuv2rgb_4x2(const uint8_t *y1, const uint8_t *y2, const uint8
}
#endif
#if defined (__ARM_NEON__)
#if MS_HAS_ARM_NEON
static int32_t yuvmax[4]={255<<13,255<<13,255<<13,255<<13};
static inline void yuv2rgb_4x2(const uint8_t *y1, const uint8_t *y2, const uint8_t *u, const uint8_t *v, int16_t *r1, int16_t *g1, int16_t *b1, int16_t *r2, int16_t *g2, int16_t *b2){
......@@ -247,7 +247,10 @@ static inline void line_yuv2rgb_2(const uint8_t *src_lines[], int src_strides[]
/*horizontal scaling of a single line (with 3 color planes)*/
static inline void line_horizontal_scale(AndroidScalerCtx * ctx, int16_t *src_lines[], int16_t *dst_lines[]){
#ifndef __ARM_NEON__
#if MS_HAS_ARM_NEON_32
//ms_line_scale_simple_8(ctx->hgrid,src_lines,dst_lines,ctx->dst_w_padded);
ms_line_scale_8(ctx->hgrid,(const int16_t * const*)src_lines,dst_lines,ctx->dst_w_padded,ctx->hcoeffs);
#else
int dst_w=ctx->dst_size.width;
int x=0;
int i,pos;
......@@ -260,9 +263,6 @@ static inline void line_horizontal_scale(AndroidScalerCtx * ctx, int16_t *src_li
dst_lines[1][i]=src_lines[1][pos];
dst_lines[2][i]=src_lines[2][pos];
}
#else
//ms_line_scale_simple_8(ctx->hgrid,src_lines,dst_lines,ctx->dst_w_padded);
ms_line_scale_8(ctx->hgrid,(const int16_t * const*)src_lines,dst_lines,ctx->dst_w_padded,ctx->hcoeffs);
#endif
}
......@@ -303,7 +303,7 @@ static void img_yuv2rgb_hscale(AndroidScalerCtx * ctx, uint8_t *src[], int src_s
}
}
#ifndef __ARM_NEON__
#if !MS_HAS_ARM_NEON_32
void ms_line_rgb2rgb565(const int16_t *r, const int16_t *g, const int16_t *b, uint16_t *dst, int width){
int i;
......@@ -332,10 +332,10 @@ static void img_yuv2rgb565_scale(AndroidScalerCtx *ctx, uint8_t *src[], int src_
p_src[0]=ctx->hscaled_img[0]+offset;
p_src[1]=ctx->hscaled_img[1]+offset;
p_src[2]=ctx->hscaled_img[2]+offset;
#ifndef __ARM_NEON__
ms_line_rgb2rgb565(p_src[0],p_src[1],p_src[2],(uint16_t*)p_dst,ctx->dst_size.width);
#else
#if MS_HAS_ARM_NEON_32
ms_line_rgb2rgb565_8(p_src[0],p_src[1],p_src[2],(uint16_t*)p_dst,ctx->dst_w_padded);
#else
ms_line_rgb2rgb565(p_src[0],p_src[1],p_src[2],(uint16_t*)p_dst,ctx->dst_size.width);
#endif
y+=ctx->h_inc;
p_dst+=dst_strides[0];
......
......@@ -19,10 +19,11 @@
#include <stdio.h>
#include <time.h>
#include <inttypes.h>
#include "mediastreamer2_tester.h"
#include <ortp/port.h>
#if defined(__ARM_NEON__) && defined(HAVE_SPEEXDSP)
#if MS_HAS_ARM_NEON && defined(HAVE_SPEEXDSP)
#include <arm_neon.h>
#include <speex/speex.h>
......@@ -255,7 +256,7 @@ static void inner_product_test(void) {
// we expect the result to be very similar and at least 5 times faster with NEON
BC_ASSERT(percent_off < 1.0);
BC_ASSERT(fast_enough);
ms_message("NEON = %llu ms, SOFT: %llu ms", neon_ms, soft_ms);
ms_message("NEON = %" PRIu64 " ms, SOFT: %" PRIu64 " ms", neon_ms, soft_ms);
if( !fast_enough ) {
ms_error("NEON not fast enough it seems");
}
......
......@@ -57,7 +57,7 @@ void mediastreamer2_tester_init(void(*ftester_printf)(int level, const char *fmt
#endif
bc_tester_add_suite(&framework_test_suite);
bc_tester_add_suite(&player_test_suite);
#ifdef __ARM_NEON__
#if MS_HAS_ARM_NEON
bc_tester_add_suite(&neon_test_suite);
#endif
bc_tester_add_suite(&text_stream_test_suite);
......
......@@ -44,7 +44,7 @@ extern test_suite_t text_stream_test_suite;
extern test_suite_t codec_impl_test_suite;
extern test_suite_t jitterbuffer_test_suite;
#endif
#ifdef __ARM_NEON__
#if MS_HAS_ARM_NEON
extern test_suite_t neon_test_suite;
#endif
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment