They actually may do that (sign extension). And look into integral promotion
rules and unsigned contagion. The rules are slightly hairy, but every C(++)
programmer doing this sort of stuff must know them.
- dR = (((Uint16)(((Sint16)sR)-((Sint16)dR))*(((Uint16)Adiv)+1-A))>>Abits)+dR; \
If sR, dR, A are of type int, how is this different from
dR = ((sR - dR) * (Adiv + 1 - A) >> Abits) + dR
? Don’t cast to integers of exact sizes without a good reason. The name of
the fastest type is “int”, almost by definition.
Hmm… I actually hadn’t thought of that… Mostly because
everything came in as a Uint8…
Okay, anyway, I did some more testing after my email last
night… Turns out that my integer rounding trick only
works with positive numbers. Doh!!! If you run the
test/testalpha demo, and click the mouse repeatedly in
the same spot you’ll start to see a dark rectangle aound
the circle it’s drawing… I had to add a conditional to
detect the sign of what was being multiplied.
I know that this is a slight performance hit. I did get
the bright idea that if I do the conditionals, and made
everything so that it was positive arithmetic, I would
have 3 8-bit numbers multiplied by one of two 8-bit numbers
( or more correctly one of two number in the range of 0-256
… 9-bits really… but even the maximum 8-bit number, 255,
times 256 won’t overflow 16-bits) So I know I have at least
2 numbers multiplied by the same value, each needing 16
bits, so I can do 2 multiplies at once in a 32-bit int,
and on a processor with a 64-bit int if I am multiplying
all three numbers by the same value (1 in 4 chance) I
can actually do all three multiplies with one 64-bit
multiply.
…So…
I added 3 conditionals, but eliminated 1 multiply, and
sometimes, on a 64-bit processor, elliminated 2.
UNFORTUNATELY… I’ve now replaced a 6 line macro with a
180+ line macro (Ouch!) I have tested this. It looks fine
and doesn’t have the darkening bug I saw last night, and is
AT LEAST as fast as the original code, although I would like
to see some benchmarks, and I had no means to test the
64-bit code… (Anyone have an alpha handy?)
Anyway, this is the last patch I plan on making to the
alpha-blitting code for a while, although I welcome any
questions or comments
Thanks again for all the feedback.
Regards,
-Loren
P.S. This patch is from the original SDL-1.1.2 code-
base… not from the patch I set out yesterday.
======BEGIN======
diff -ruN src/SDL-1.1.2/src/video/SDL_blit.h src/SDL-1.1.2-new/src/video/SDL_blit.h
— src/SDL-1.1.2/src/video/SDL_blit.h Thu Mar 16 07:20:38 2000
+++ src/SDL-1.1.2-new/src/video/SDL_blit.h Thu May 4 11:35:11 2000
@@ -221,15 +221,211 @@
/* Blend the RGB values of two pixels based on a source alpha value */
#define FAST_ALPHA_BLEND
+#define CONDENSE_MULTIPLIES_ALPHA_BLEND
#ifdef FAST_ALPHA_BLEND
-#define ALPHA_BLEND(sR, sG, sB, A, Adiv, dR, dG, dB)
+#ifdef CONDENSE_MULTIPLIES_ALPHA_BLEND
+#define ALPHA_BLEND(sR, sG, sB, A, Adiv, Abits, dR, dG, dB) \ { \
- dR = (((Uint16)(sR-dR)*(Adiv-A))>>8)+dR; \
- dG = (((Uint16)(sG-dG)*(Adiv-A))>>8)+dG; \
- dB = (((Uint16)(sB-dB)*(Adiv-A))>>8)+dB; \
- /* We have 3 8-bit numbers that we are multiplying by one of 2 \
-
numbers... at least 2 will be the same. */ \
- int multiplier = 0; \
- if ( sizeof(int) >= 6 ) { /* Optimize for 64-bit processors */ \
-
if ( sR<dR ) { \
-
if ( sG<dG ) { \
-
if ( sB<dB ) { \
-
/* Same sign... use one multiply */ \+ multiplier = ((int)(dR-sR)) | (((int)(dG-sG))<<16) | \
-
(((int)(dB-sB))<<32) ; \
-
multiplier *= (((int)A)+1) ; \
-
multiplier >>= Abits ; \
-
multiplier &= 0x00ff00ff00ff ; \
-
dR = ((multiplier & 0xff) + sR) & 0xff ; \
-
dG = (((multiplier & 0xff0000)>>16) + sG) & 0xff ; \
-
dB = (((multiplier & 0xff00000000)>>32) + sB) & 0xff ; \
-
} else { \
-
/* Same sign */ \
-
multiplier = ((int)(dR-sR)) | (((int)(dG-sG))<<16) ; \
-
multiplier *= (((int)A)+1) ; \
-
multiplier >>= Abits ; \
-
multiplier &= 0x00ff00ff ; \
-
dR = ((multiplier & 0xff) + sR) & 0xff ; \
-
dG = (((multiplier & 0xff0000)>>16) + sG) & 0xff ; \
-
/* Odd man out */ \
-
dB = 0xff & ((((int)(sB-dB)*(((int)Adiv)+1-A))>>Abits)+dB) ; \
-
} \
-
} else { \
-
if ( sB<dB ) { \
-
/* Same sign */ \
-
multiplier = ((int)(dR-sR)) | (((int)(dB-sB))<<16) ; \
-
multiplier *= (((int)A)+1) ; \
-
multiplier >>= Abits ; \
-
multiplier &= 0x00ff00ff ; \
-
dR = ((multiplier & 0xff) + sR) & 0xff ; \
-
dB = (((multiplier & 0xff0000)>>16) + sB) & 0xff ; \
-
/* Odd man out */ \
-
dG = 0xff & ((((int)(sG-dG)*(((int)Adiv)+1-A))>>Abits)+dG) ; \
-
} else { \
-
/* Same sign */ \
-
multiplier = ((int)(sG-dG)) | (((int)(sB-dB))<<16) ; \
-
multiplier *= (((int)Adiv)+1-A) ; \+ multiplier >>= Abits ; \
-
multiplier &= 0x00ff00ff ; \
-
dG = ((multiplier & 0xff) + dG) & 0xff ; \
-
dB = (((multiplier & 0xff0000)>>16) + dB) & 0xff ; \
-
/* Odd man out */ \
-
dR = 0xff & ((((int)(dR-sR)*(((int)A)+1))>>Abits)+sR) ; \
-
} \
-
} \
-
} else { \
-
if ( sG<dG ) { \
-
if ( sB<dB ) { \
-
/* Same sign */ \
-
multiplier = ((int)(dG-sG)) | (((int)(dB-sB))<<16) ; \
-
multiplier *= (((int)A)+1) ; \
-
multiplier >>= Abits ; \
-
multiplier &= 0x00ff00ff ; \
-
dG = ((multiplier & 0xff) + sG) & 0xff ; \
-
dB = (((multiplier & 0xff0000)>>16) + sB) & 0xff ; \
-
/* Odd man out */ \
-
dR = 0xff & ((((int)(sR-dR)*(((int)Adiv)+1-A))>>Abits)+dR) ; \
-
} else { \
-
/* Same sign */ \
-
multiplier = ((int)(sR-dR)) | (((int)(sB-dB))<<16) ; \
-
multiplier *= (((int)Adiv)+1-A) ; \+ multiplier >>= Abits ; \
-
multiplier &= 0x00ff00ff ; \
-
dR = ((multiplier & 0xff) + dR) & 0xff ; \
-
dB = (((multiplier & 0xff0000)>>16) + dB) & 0xff ; \
-
/* Odd man out */ \
-
dG = 0xff & ((((int)(dG-sG)*(((int)A)+1))>>Abits)+sG) ; \
-
} \
-
} else { \
-
if ( sB<dB ) { \
-
/* Same sign */ \
-
multiplier = ((int)(sR-dR)) | (((int)(sG-dG))<<16) ; \
-
multiplier *= (((int)Adiv)+1-A) ; \+ multiplier >>= Abits ; \
-
multiplier &= 0x00ff00ff ; \
-
dR = ((multiplier & 0xff) + dR) & 0xff ; \
-
dG = (((multiplier & 0xff0000)>>16) + dG) & 0xff ; \
-
/* Odd man out */ \
-
dB = 0xff & ((((int)(dB-sB)*(((int)A)+1))>>Abits)+sB) ; \
-
} else { \
-
/* Same sign... use one multiply */ \+ multiplier = ((int)(sR-dR)) | (((int)(sG-dG))<<16) | \
-
(((int)(sB-dB))<<32) ; \
-
multiplier *= (((int)Adiv)+1-A) ; \+ multiplier >>= Abits ; \
-
multiplier &= 0x00ff00ff00ff ; \
-
dR = ((multiplier & 0xff) + dR) & 0xff ; \
-
dG = (((multiplier & 0xff0000)>>16) + dG) & 0xff ; \
-
dB = (((multiplier & 0xff00000000)>>32) + dB) & 0xff ; \
-
} \
-
} \
-
} \
- } else if ( sizeof(int) >= 4 ) { \
-
if ( sR<dR ) { \
-
if ( sG<dG ) { \
-
/* Same sign */ \
-
multiplier = ((int)(dR-sR)) | (((int)(dG-sG))<<16) ; \
-
multiplier *= (((int)A)+1) ; \
-
multiplier >>= Abits ; \
-
multiplier &= 0x00ff00ff ; \
-
dR = ((multiplier & 0xff) + sR) & 0xff ; \
-
dG = (((multiplier & 0xff0000)>>16) + sG) & 0xff ; \
-
/* Odd man out */ \
-
dB = 0xff & ((sB<dB)? \
-
((((int)(dB-sB)*(((int)A)+1))>>Abits)+sB): \
-
((((int)(sB-dB)*(((int)Adiv)+1-A))>>Abits)+dB) ); \
-
} else { \
-
if ( sB<dB ) { \
-
/* Same sign */ \
-
multiplier = ((int)(dR-sR)) | (((int)(dB-sB))<<16) ; \
-
multiplier *= (((int)A)+1) ; \
-
multiplier >>= Abits ; \
-
multiplier &= 0x00ff00ff ; \
-
dR = ((multiplier & 0xff) + sR) & 0xff ; \
-
dB = (((multiplier & 0xff0000)>>16) + sB) & 0xff ; \
-
/* Odd man out */ \
-
dG = 0xff & ((((int)(sG-dG)*(((int)Adiv)+1-A))>>Abits)+dG) ; \
-
} else { \
-
/* Same sign */ \
-
multiplier = ((int)(sG-dG)) | (((int)(sB-dB))<<16) ; \
-
multiplier *= (((int)Adiv)+1-A) ; \+ multiplier >>= Abits ; \
-
multiplier &= 0x00ff00ff ; \
-
dG = ((multiplier & 0xff) + dG) & 0xff ; \
-
dB = (((multiplier & 0xff0000)>>16) + dB) & 0xff ; \
-
/* Odd man out */ \
-
dR = 0xff & ((((int)(dR-sR)*(((int)A)+1))>>Abits)+sR) ; \
-
} \
-
} \
-
} else { \
-
if ( sG<dG ) { \
-
if ( sB<dB ) { \
-
/* Same sign */ \
-
multiplier = ((int)(dG-sG)) | (((int)(dB-sB))<<16) ; \
-
multiplier *= (((int)A)+1) ; \
-
multiplier >>= Abits ; \
-
multiplier &= 0x00ff00ff ; \
-
dG = ((multiplier & 0xff) + sG) & 0xff ; \
-
dB = (((multiplier & 0xff0000)>>16) + sB) & 0xff ; \
-
/* Odd man out */ \
-
dR = 0xff & ((((int)(sR-dR)*(((int)Adiv)+1-A))>>Abits)+dR) ; \
-
} else { \
-
/* Same sign */ \
-
multiplier = ((int)(sR-dR)) | (((int)(sB-dB))<<16) ; \
-
multiplier *= (((int)Adiv)+1-A) ; \+ multiplier >>= Abits ; \
-
multiplier &= 0x00ff00ff ; \
-
dR = ((multiplier & 0xff) + dR) & 0xff ; \
-
dB = (((multiplier & 0xff0000)>>16) + dB) & 0xff ; \
-
/* Odd man out */ \
-
dG = 0xff & ((((int)(dG-sG)*(((int)A)+1))>>Abits)+sG) ; \
-
} \
-
} else { \
-
/* Same sign */ \
-
multiplier = ((int)(sR-dR)) | (((int)(sG-dG))<<16) ; \
-
multiplier *= (((int)Adiv)+1-A) ; \
-
multiplier >>= Abits ; \
-
multiplier &= 0x00ff00ff ; \
-
dR = ((multiplier & 0xff) + dR) & 0xff ; \
-
dG = (((multiplier & 0xff0000)>>16) + dG) & 0xff ; \
-
/* Odd man out */ \
-
dB = 0xff & ((sB<dB)? \
-
((((int)(dB-sB)*(((int)A)+1))>>Abits)+sB): \
-
((((int)(sB-dB)*(((int)Adiv)+1-A))>>Abits)+dB) ); \
-
} \
-
} \
- } else { /* need a 32-bit int to do any special multiplication \
-
condensing... otherwise fall-back */ \
-
dR = 0xff & ((sR<dR)? \
-
((((int)(dR-sR)*(((int)A)+1))>>Abits)+sR): \
-
((((int)(sR-dR)*(((int)Adiv)+1-A))>>Abits)+dR) ); \
-
dG = 0xff & ((sG<dG)? \
-
((((int)(dG-sG)*(((int)A)+1))>>Abits)+sG): \
-
((((int)(sG-dG)*(((int)Adiv)+1-A))>>Abits)+dG) ); \
-
dB = 0xff & ((sB<dB)? \
-
((((int)(dB-sB)*(((int)A)+1))>>Abits)+sB): \
-
((((int)(sB-dB)*(((int)Adiv)+1-A))>>Abits)+dB) ); \
- }
}
-#else
-#define ALPHA_BLEND(sR, sG, sB, A, Adiv, dR, dG, dB)
+#else /* CONDENSE_MULTIPLIES_ALPHA_BLEND */
+#define ALPHA_BLEND(sR, sG, sB, A, Adiv, Abits, dR, dG, dB) +{ \
- dR = 0xff & ((sR<dR)? \
-
((((int)(dR-sR)*(((int)A)+1))>>Abits)+sR): \
-
((((int)(sR-dR)*(((int)Adiv)+1-A))>>Abits)+dR) ); \
- dG = 0xff & ((sG<dG)? \
-
((((int)(dG-sG)*(((int)A)+1))>>Abits)+sG): \
-
((((int)(sG-dG)*(((int)Adiv)+1-A))>>Abits)+dG) ); \
- dB = 0xff & ((sB<dB)? \
-
((((int)(dB-sB)*(((int)A)+1))>>Abits)+sB): \
-
((((int)(sB-dB)*(((int)Adiv)+1-A))>>Abits)+dB) ); \
+}
+#endif /* CONDENSE_MULTIPLIES_ALPHA_BLEND /
+#else / FAST_ALPHA_BLEND /
+#define ALPHA_BLEND(sR, sG, sB, A, Adiv, Abits, dR, dG, dB) \ {
dR = ((Uint16)sR(Adiv-A) + (Uint16)dRA) / Adiv;
dG = ((Uint16)sG(Adiv-A) + (Uint16)dG*A) / Adiv;
diff -ruN src/SDL-1.1.2/src/video/SDL_blit_A.c src/SDL-1.1.2-new/src/video/SDL_blit_A.c
— src/SDL-1.1.2/src/video/SDL_blit_A.c Thu Mar 16 07:20:38 2000
+++ src/SDL-1.1.2-new/src/video/SDL_blit_A.c Wed May 3 23:10:04 2000
@@ -64,7 +64,7 @@
sB = srcpal[bit].b;
DISEMBLE_RGB(dst, dstbpp, dstfmt,
pixel, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, A, 255, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, A, 255, 8, dR, dG, dB);
ASSEMBLE_RGB(dst, dstbpp, dstfmt, dR, dG, dB);
}
byte <<= 1;
@@ -96,7 +96,7 @@
sB = srcpal[*src].b;
DISEMBLE_RGB(dst, dstbpp, dstfmt,
pixel, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, A, 255, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, A, 255, 8, dR, dG, dB);
ASSEMBLE_RGB(dst, dstbpp, dstfmt, dR, dG, dB);
}
src++;
@@ -129,7 +129,7 @@
dR = dstfmt->palette->colors[*dst].r;
dG = dstfmt->palette->colors[*dst].g;
dB = dstfmt->palette->colors[*dst].b;
-
ALPHA_BLEND(sR, sG, sB, A, 255, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, A, 255, 8, dR, dG, dB);
/* Pack RGB into 8bit pixel */
if ( palmap == NULL ) {
*dst =((dR>>5)<<(3+2))|
@@ -151,6 +151,7 @@
Uint32 pixel;
Uint8 sA;
const Uint8 Adiv = (srcfmt->Amask>>srcfmt->Ashift);
@@ -159,7 +160,7 @@
dR = dstfmt->palette->colors[*dst].r;
dG = dstfmt->palette->colors[*dst].g;
dB = dstfmt->palette->colors[*dst].b;
-
ALPHA_BLEND(sR, sG, sB, sA, Adiv, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, sA, Adiv, Abits, dR, dG, dB);
/* Pack RGB into 8bit pixel */
if ( palmap == NULL ) {
*dst =((dR>>5)<<(3+2))|
@@ -206,7 +207,7 @@
if ( 1 ) {
pixel16 = *dstp;
RGB_FROM_PIXEL(pixel16, dstfmt, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, A, 255, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, A, 255, 8, dR, dG, dB);
PIXEL_FROM_RGB(*dstp, dstfmt, dR, dG, dB);
}
++srcp;
@@ -218,6 +219,7 @@
} else {
Uint8 sA;
const Uint8 Adiv = (srcfmt->Amask>>srcfmt->Ashift);
@@ -226,7 +228,7 @@
if ( 1 ) {
pixel16 = *dstp;
RGB_FROM_PIXEL(pixel16, dstfmt, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, sA, Adiv, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, sA, Adiv, Abits, dR, dG, dB);
PIXEL_FROM_RGB(*dstp, dstfmt, dR, dG, dB);
}
++srcp;
@@ -260,7 +262,7 @@
if ( 1 ) {
DISEMBLE_RGB(dst, dstbpp, dstfmt,
pixel, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, A, 255, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, A, 255, 8, dR, dG, dB);
ASSEMBLE_RGB(dst, dstbpp, dstfmt, dR, dG, dB);
}
src += srcbpp;
@@ -272,6 +274,7 @@
} else {
Uint8 sA;
const Uint8 Adiv = (srcfmt->Amask>>srcfmt->Ashift);
@@ -279,7 +282,7 @@
if ( 1 ) {
DISEMBLE_RGB(dst, dstbpp, dstfmt,
pixel, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, sA, Adiv, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, sA, Adiv, Abits, dR, dG, dB);
ASSEMBLE_RGB(dst, dstbpp, dstfmt, dR, dG, dB);
}
src += srcbpp;
diff -ruN src/SDL-1.1.2/src/video/SDL_blit_AK.c src/SDL-1.1.2-new/src/video/SDL_blit_AK.c
— src/SDL-1.1.2/src/video/SDL_blit_AK.c Thu Mar 16 07:20:38 2000
+++ src/SDL-1.1.2-new/src/video/SDL_blit_AK.c Wed May 3 23:10:04 2000
@@ -64,7 +64,7 @@
sB = srcpal[bit].b;
DISEMBLE_RGB(dst, dstbpp, dstfmt,
pixel, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, A, 255, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, A, 255, 8, dR, dG, dB);
ASSEMBLE_RGB(dst, dstbpp, dstfmt, dR, dG, dB);
}
byte <<= 1;
@@ -96,7 +96,7 @@
sB = srcpal[*src].b;
DISEMBLE_RGB(dst, dstbpp, dstfmt,
pixel, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, A, 255, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, A, 255, 8, dR, dG, dB);
ASSEMBLE_RGB(dst, dstbpp, dstfmt, dR, dG, dB);
}
src++;
@@ -129,7 +129,7 @@
dR = dstfmt->palette->colors[*dst].r;
dG = dstfmt->palette->colors[*dst].g;
dB = dstfmt->palette->colors[*dst].b;
-
ALPHA_BLEND(sR, sG, sB, A, 255, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, A, 255, 8, dR, dG, dB);
/* Pack RGB into 8bit pixel */
if ( palmap == NULL ) {
*dst =((dR>>5)<<(3+2))|
@@ -151,6 +151,7 @@
Uint32 pixel;
Uint8 sA;
const Uint8 Adiv = (srcfmt->Amask>>srcfmt->Ashift);
@@ -159,7 +160,7 @@
dR = dstfmt->palette->colors[*dst].r;
dG = dstfmt->palette->colors[*dst].g;
dB = dstfmt->palette->colors[*dst].b;
-
ALPHA_BLEND(sR, sG, sB, sA, Adiv, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, sA, Adiv, Abits, dR, dG, dB);
/* Pack RGB into 8bit pixel */
if ( palmap == NULL ) {
*dst =((dR>>5)<<(3+2))|
@@ -206,7 +207,7 @@
if ( pixel32 != srcfmt->colorkey ) {
pixel16 = *dstp;
RGB_FROM_PIXEL(pixel16, dstfmt, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, A, 255, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, A, 255, 8, dR, dG, dB);
PIXEL_FROM_RGB(*dstp, dstfmt, dR, dG, dB);
}
++srcp;
@@ -218,6 +219,7 @@
} else {
Uint8 sA;
const Uint8 Adiv = (srcfmt->Amask>>srcfmt->Ashift);
@@ -226,7 +228,7 @@
if ( pixel32 != srcfmt->colorkey ) {
pixel16 = *dstp;
RGB_FROM_PIXEL(pixel16, dstfmt, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, sA, Adiv, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, sA, Adiv, Abits, dR, dG, dB);
PIXEL_FROM_RGB(*dstp, dstfmt, dR, dG, dB);
}
++srcp;
@@ -260,7 +262,7 @@
if ( pixel != srcfmt->colorkey ) {
DISEMBLE_RGB(dst, dstbpp, dstfmt,
pixel, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, A, 255, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, A, 255, 8, dR, dG, dB);
ASSEMBLE_RGB(dst, dstbpp, dstfmt, dR, dG, dB);
}
src += srcbpp;
@@ -272,6 +274,7 @@
} else {
Uint8 sA;
const Uint8 Adiv = (srcfmt->Amask>>srcfmt->Ashift);
@@ -279,7 +282,7 @@
if ( pixel != srcfmt->colorkey ) {
DISEMBLE_RGB(dst, dstbpp, dstfmt,
pixel, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, sA, Adiv, dR, dG, dB);
-
ALPHA_BLEND(sR, sG, sB, sA, Adiv, Abits, dR, dG, dB);
ASSEMBLE_RGB(dst, dstbpp, dstfmt, dR, dG, dB);
}
src += srcbpp;
=======END=======
…
Great news! Get free KNXmail here!
http://www.knx1070.com