The MASM Forum

General => The Campus => Topic started by: LordAdef on January 29, 2018, 02:47:58 PM

Title: FPU question
Post by: LordAdef on January 29, 2018, 02:47:58 PM
Hi guys,
I wrote this procedure and tried to get away with integer division, using eax and ignoring edx. No, I´ll need FPU, which I am studying/reading... but clearly still incompetent.

I need to

->divide two values,
-> save the float value,  add and iterates the var
-> round it and move into ebx
-> float iteration continues as the loop progresses

QuoteLOCAL byteInc:DWORD   ; the interval of bytes to read. Affects img zoom
   ; LOCAL lineInc:DWORD    ; ==> USED IN THE DWORD VERSION
        ; FPU version:
   LOCAL lineInc:REAL4      ; the interval of lines to read. Affects img zoom
   LOCAL ySum:REAL4

Part of code with commented dword version, and fpu version.
Division of two values:
Quote; -----------------------------------------------------
         ; Fit to dimension: TRUE or FALSE
         ; -----------------------------------------------------
         cmp tFit, TRUE
         jnz fixedInc
         
         ; ------------------------------------------ Fit in img -----------------------------------------------------------
         ; -------- fit mapY minimap ----------
         
         ; ---------------  INTEGER version ------
         ; mov eax, map.height
         ; mov esi, MM_HEIGHT
         ; cdq
         ; div esi
         ; mov lineInc, eax   
         
         ; -------------- NEW ----------------------------
         fild map.height
         fdiv MM_HEIGHT
         fstp lineInc
         ; ---------------------------------------------------

fpu iteration, rounding and moving into ebx.:
Quote; ------ reached the end of the line, next line Ptr please --------------------------------
      
      ; -------- NEW ----------------------
      fld lineInc
      fadd ySum
      fstp ySum
      mov ebx, dword ptr [ySum]   
      ; --------------------------------------      
      
      ; ------ INTEGER version ----------
      ;add ebx,  lineInc      ; we need the Ptr for the next line
      ; ------------------------------------------
      mov cc, 0            ; resets line byte back to start of line      
      mov  xPos, 0         ; reset x coord
      add yPos, 1            ; inc y coord

These are the bits I need. The dword version was perfect but I need the FPU precision. But it´s not working..... Help would be mostly appreciated  :icon_redface: :icon_redface:

ps: I didn´t quote the whole procedure to save space. I´ll be glad to post it if asked to. Cheers
Title: Re: FPU question
Post by: jj2007 on January 29, 2018, 06:42:08 PM
Attention to the sizes:
      fld lineInc  ; ok if lineInc is REAL4 or REAL8
      fadd ySum  ; ok if REAL4
      fstp ySum  ; ok if REAL4
      mov ebx, dword ptr [ySum]  ; but it isn't REAL4, apparently :(

The assembler doesn't complain if you use float instructions for integer variables. But the result won't be correct 8)

Other than that, it's difficult to say where you could be wrong without complete code.

Btw deb (http://www.webalice.it/jj2006/MasmBasicQuickReference.htm#Mb1019) is perfectly able to show you the content of the FPU.
Title: Re: FPU question
Post by: HSE on January 29, 2018, 10:27:00 PM
Just replace "fstp ySum" with "fistp dSum" where dSum is dword, then "mov ebx, dword ptr[dSum]'
(you can make only "fistp ySum" and will work, but is error prone)

Later: you need to store the new ySum:

replace "fstp ySum" with
       " fst  ySum
         fistp dSum"
Title: Re: FPU question
Post by: LordAdef on January 30, 2018, 10:33:54 AM
The problem was here:
         fild map.height
         fdiv sHeight
         fstp lineInc

I changed to "fld" and it works. My problem is... map.height is a dword, shouldn´t "fild" be the right choice???
         


QuotepCreateMiniMap proc, tFit:DWORD
   ; ---------------------------------------------------------------------------------------------------------
   ; SetPixels in a pre created img, of a minuature map of the main map
   ; tFit == TRUE  makes the map fit into img dimension
   ; tFit == FALSE x /y scales are given by constants xInc and yInc   
   ; ---------------------------------------------------------------------------------------------------------

   LOCAL cc:DWORD         ; increment the line Ptr, searching the line for the target valeu
   LOCAL xPos:DWORD      ; x in miniMap img
   LOCAL yPos:DWORD      ; y in miniMap img
   LOCAL byteInc:DWORD   ; the interval of bytes to read. Affects img zoom

   LOCAL lineInc:REAL4      ; the interval of lines to read. Affects img zoom
   LOCAL sHeight:SDWORD   
   LOCAL ySum:REAL4
   LOCAL dSum:DWORD
      
   PUSH ebx
   PUSH edi
   PUSH esi
         ; ------ CREATE IMG FIRST -----------------------------------------------------------------
         ; First, it creates a compatible bmp
         ; I´m cheating: made beginPaint hdc handle (from CREATE) a global
         ; only to deal with the miniMap here
         ; --------------------------------------------------------------------------------------------------------
         invoke    CreateCompatibleBitmap, gHDC, MM_WIDTH, MM_HEIGHT
           mov     hMiniMapBmp, eax
          invoke    SelectObject, hMiniMapDC, hMiniMapBmp

   ; ----------------- Constants -------------------------------   
   byteVal       EQU    32               ; the target byte we want to setPixel. 32 is space (" ")
   xInc         EQU   8               ; Fixed value for scale x. The bigger, the smaller the Minimap x view is
   yInc      EQU   8               ; Fixed value for scale y. The bigger, the smaller the Minimap y view is   
   ; ------------------------------------------------------------------

   mov cc, 0
   mov xPos, 0
   mov yPos, 0
   mov sHeight, MM_HEIGHT         ; converts the const into a sdword

         ; -----------------------------------------------------
         ; Fit to dimension: TRUE or FALSE
         ; -----------------------------------------------------
         cmp tFit, TRUE
         jnz fixedInc
         ; ------------------------------------------ Fit in img -----------------------------------------------------------
         ; -------- fit mapY minimap ----------
         fld map.height
         fdiv sHeight
         fstp lineInc
         
         ; --------- fit mapX minimap ---------
         mov eax, cWIDTH
         mov esi, MM_WIDTH
         cdq
         div esi
         mov byteInc, eax
         jmp drawMap
         
         fixedInc:
         ; ----------------------------------------------------------------------
         ; It´s fixed.
         ; So, use the values set in xInc and yInc above
         ; ----------------------------------------------------------------------
         mov byteInc,  xInc         ; The bigger, the smaller the Minimap x view is
         mov lineInc, yInc   
   
   drawMap:   
         
   fldz            ; st(0) == 0
   xor ebx, ebx      
   newLinePtr:
      mov edi, [map.mapLinePtr+ ebx * 4]      ; Get line Ptr (map.mapLinePtr is an array of Ptr)
      
   fillLine:   
      cmp byte Ptr [edi], byteVal             ; 32 == space, it´s what we want
      jnz nextPix                           ; it´s not, skip SetPixel
      
      invoke SetPixel, hMiniMapDC, xPos, yPos, 0FFFF0Bh   
         
   nextPix:
      ; Byte is NOT 32, so we don´t setpixel and just keep going
      mov eax, byteInc
      add cc, eax            ; next byte within line
      add edi, eax            ; next byte Ptr      
      add xPos, 1            ; next x coord
      
      cmp cc, cWIDTH      ;  cWIDTH is the number of bytes/line (currently 159)
      jbe fillLine            ; it´s within line
      
      ; ------ reached the end of the line, next line Ptr please --------------------------------
;       fld ySum
;       fadd lineInc
;       fst ySum            ; sum in real4
;       fistp dSum            ; sum in dword
      
      fadd lineInc
      fist dSum
      mov ebx, dword ptr [dSum]   
      
      ;; printf ("ebx: %d \n", ebx)
      mov cc, 0            ; resets line byte back to start of line      
      mov  xPos, 0         ; reset x coord
      add yPos, 1            ; inc y coord
      
      cmp ebx, map.height    ; map.height is the num of lines of map (after is changed, according to map.style)
      jb newLinePtr   
      
   done:
      POP esi
      POP edi
      POP ebx
      ret
pCreateMiniMap endp
Title: Re: FPU question
Post by: LordAdef on January 30, 2018, 10:35:42 AM
Quote from: HSE on January 29, 2018, 10:27:00 PM
Just replace "fstp ySum" with "fistp dSum" where dSum is dword, then "mov ebx, dword ptr[dSum]'
(you can make only "fistp ySum" and will work, but is error prone)

Later: you need to store the new ySum:

replace "fstp ySum" with
       " fst  ySum
         fistp dSum"


Woking version in action. Press 1, and then when you press 4 you move to the next level, and the new miniMap is redrawn (There`re 3 maps)
Thanks HSE. It works! In fact I don´t even need to save ySum, I can iterate straight in st(0)
Title: Re: FPU question
Post by: jj2007 on January 30, 2018, 04:23:56 PM
Works fine but I can't see any effect of pressing 4 :(
Title: Re: FPU question
Post by: LordAdef on January 30, 2018, 05:18:06 PM
the 4, not the side pad four, right?

Just skip the current level, and take you to the start of the next level, with the miniMap being redrawn with the new map.
Title: Re: FPU question
Post by: HSE on January 31, 2018, 01:59:41 AM
Before the proc end, "fstp st" will empty FPU       :t
Title: Re: FPU question
Post by: LordAdef on January 31, 2018, 04:28:27 AM
Quote from: HSE on January 31, 2018, 01:59:41 AM
Before the proc end, "fstp st" will empty FPU       :t

Sure, is it a good practice to empty FPU after using it?

concerning my question above, map.height is a dword, but the code only worked with  "fld map.height". Wouldn´t "fild" be the right one??
Title: Re: FPU question
Post by: jj2007 on January 31, 2018, 04:51:53 AM
Quote from: LordAdef on January 31, 2018, 04:28:27 AMSure, is it a good practice to empty FPU after using it?
Yes, otherwise you run into trouble when you need it next time, especially in a loop.


Quoteconcerning my question above, map.height is a dword, but the code only worked with  "fld map.height". Wouldn´t "fild" be the right one??

Yes, probably, but check all your types. As mentioned above, unfortunately the assemblers we use do not check the type for FPU instructions. Thus, you can write fdiv someinteger (instead of fidiv), and it will erroneously treat someinteger as a REAL4.
Title: Re: FPU question
Post by: LordAdef on January 31, 2018, 05:01:10 AM
QuoteYes, probably, but check all your types. As mentioned above, unfortunately the assemblers we use do not check the type for FPU instructions. Thus, you can write fdiv someinteger (instead of fidiv), and it will erroneously treat someinteger as a REAL4.

Yep, but that´s the problem... map.height is definitely a "dword", but the code will only work with fld, not with fild

here:
         ; -------- fit mapY minimap ----------
         fld map.height    <=== this should be fild right?? (map.height == dword)
         fdiv sHeight
         fstp lineInc
Title: Re: FPU question
Post by: jj2007 on January 31, 2018, 05:10:34 AM
So, when you insert
  deb 4, "fpu in", map.height, sHeight
         ; -------- fit mapY minimap ----------
         fld map.height    <=== this should be fild right?? (map.height == dword)
         fdiv sHeight
         fstp lineInc
  deb 4, "fpu out", lineInc

... does it look right? In particular since LOCAL sHeight:SDWORD?
Title: Re: FPU question
Post by: daydreamer on January 31, 2018, 05:13:39 AM
Quote from: LordAdef on January 31, 2018, 05:01:10 AM
QuoteYes, probably, but check all your types. As mentioned above, unfortunately the assemblers we use do not check the type for FPU instructions. Thus, you can write fdiv someinteger (instead of fidiv), and it will erroneously treat someinteger as a REAL4.

Yep, but that´s the problem... map.height is definitely a "dword", but the code will only work with fld, not with fild

here:
         ; -------- fit mapY minimap ----------
         fld map.height    <=== this should be fild right?? (map.height == dword)
         fdiv sHeight
         fstp lineInc

Use int3 and use debugger to singlestep your fpu code and there you can watch what real4 value endsup in your fpu, its also good singlestep any fpu code to learn how fpu regs behave
I think it 23bits number, 1bit sign and rest of bits is info if number is very big or very small, like scientific number format

Or find right print to invoke for real4 printout
Title: Re: FPU question
Post by: LordAdef on January 31, 2018, 06:18:25 AM
Hey DDreamer,

Thanks for the tip, I´ll try the debugger. Unfortunately I´m working on a laptop that doesn´t have Olly. 
Title: Re: FPU question
Post by: HSE on January 31, 2018, 10:47:50 AM
lea eax,map
fild [eax].MapStructure.height

Or
Assume eax: ptr MapStructure
fild [eax].height
Title: Re: FPU question
Post by: LordAdef on January 31, 2018, 04:48:35 PM
thanks HSE,

I'll try that!
Title: Re: FPU question
Post by: LordAdef on February 01, 2018, 04:09:11 PM
Quote from: jj2007 on January 31, 2018, 05:10:34 AM
So, when you insert
  deb 4, "fpu in", map.height, sHeight
         ; -------- fit mapY minimap ----------
         fld map.height    <=== this should be fild right?? (map.height == dword)
         fdiv sHeight
         fstp lineInc
  deb 4, "fpu out", lineInc

... does it look right? In particular since LOCAL sHeight:SDWORD?

Yep, you´re...... cheers!

But let me ask:

MM_HEIGHT EQU 200

I´m having to load MM_HEIGHT in a dword LOCAL in order to make the code world:
fild map.height
fidiv mm_Height   <--- HERE
fstp lineInc


Are constants forbidden for FPU?
Title: Re: FPU question
Post by: Siekmanski on February 01, 2018, 05:43:24 PM
.const
MM_HEIGHT dd 200 ; constant

.data
lineInc dd 0

.code
fild map.height
fidiv mm_Height   <--- HERE
fistp lineInc ; assume the output is integer?
Title: Re: FPU question
Post by: LordAdef on February 01, 2018, 05:50:34 PM
Quote
Quotefild map.height
   fidiv mm_Height   <--- HERE
   fistp lineInc ; assume the output is integer?   

lineInc must be REAL4.

But would it be a problem?  I don´t see the difference between working with a dword, and working with a int const.
Obviously I´m wrong, but why?
Title: Re: FPU question
Post by: Siekmanski on February 01, 2018, 05:58:25 PM
No, it's up to you, but a line number is normally a natural number.
Of course you can use a real number, then use fstp instead of fistp.   
Title: Re: FPU question
Post by: jj2007 on February 01, 2018, 06:41:26 PM
Quote from: LordAdef on February 01, 2018, 04:09:11 PMAre constants forbidden for FPU?

Ask your assembler :P

Simple solution:
MM_HEIGHT EQU 200
push MM_HEIGHT
fild dword ptr [esp]  ; there it is...
pop edx
Title: Re: FPU question
Post by: raymond on February 02, 2018, 08:21:48 AM
QuoteAre constants forbidden for FPU?
Questions like this are covered in the "Simply FPU" tutorial which you can find at http://www.ray.masmcode.com/tutorial/index.html. In this case, it would be in Chapter 2.

Another detail you must remember is that MASM treats "dword" and "real4" in a strictly identical manner as plainly referring to a 32-bit value (or "qword" and "real8" as a 64-bit value). Accompanying instructions are then coded accordingly.

And, one of the most common source of error made by programmers starting to use the FPU without sufficient knowledge is "A source parameter is identified as being a REAL number type or an integer type at a specified memory address but a different type resides at that address (while still being considered as acceptable data by the FPU)". The above tutorial describes what each instruction expects from the source. For example, the "fld" instruction expects a value in the floating point format, while the "fild" instruction assumes a value supplied as a signed integer format.
Title: Re: FPU question
Post by: LordAdef on February 02, 2018, 02:16:57 PM
Dear Raymond,

Thanks for dropping by!

Indeed, I must say I already start reading your wonderful tutorial (Hutch has previously suggested it).

True, FPU can be a bit of a hassle when you start with it. It looks easy though... fld for float, fild otherwise... easy but not..

I mentioned about constants on my post above. Yes, I understand I cannot deal with it directly, so the push esp. In my code above I used a LOCAL instead.

My question resides, as an educational point-of-view, on the architectural reason why I cannot fild a declare int const. 
Title: Re: FPU question
Post by: raymond on February 02, 2018, 03:38:44 PM
QuoteMy question resides, as an educational point-of-view, on the architectural reason why I cannot fild a declare int const.

You could consider a declared constant as a macro replacing its 'lable' with the given declared constant when the code gets assembled. I haven't tested it but I would suspect that an error would be generated when trying to use a declared constant with those ALU  integer instructions which don't allow the use of constants (such as the mul and div instructions for example). As for the FPU, none of the related instructions allow for the direct use of constants. (Only seven hard-coded constants can be loaded into the top FPU register with specific instructions, as described in Chap. 4).
Title: Re: FPU question
Post by: LordAdef on February 02, 2018, 04:11:51 PM
Thanks for that Raymond, you were very kind.

It´s incredible to see how much I learned from all of you in this single thread. It pushed me to research and so on. I´ll keep studying Raymond´s tutorial and improve my FPU skills. I hope this serves for the next guys looking for similar help (I always think of this when I post something). You are all wonderful people.

The code is working now! Here it´s the whole procedure. Now I´ll try and do some optmization(unecessary locals and so forth).
Attached a working version.
Start the thing, and
press 1 (top numbers, not numpad)
press 4
press 1
press 4 etc.. The miniMap should redraw new maps in blue
pCreateMiniMap proc, tFit:DWORD
; --------------------------------------------------------------------------------------------------------------
; SetPixels in a pre created img, of a minuature map of the main map
; tFit == TRUE  makes the map fit into img dimension
; tFit == FALSE x /y scales are given by constants xInc and yInc
; --------------------------------------------------------------------------------------------------------------
LOCAL  cc:DWORD ; float byte counter
LOCAL  ccFloat:REAL4 ; round byte counter
LOCAL xPos:DWORD ; x in miniMap img
LOCAL yPos:DWORD ; y in miniMap img
LOCAL ySum:REAL4 ; float y value
LOCAL dSum:DWORD ; y value
LOCAL nAdd:DWORD ; incremented address for byte value within line
LOCAL byteInc:REAL4 ; increment the line Ptr, searching the line for the target valeu
LOCAL lineInc:REAL4 ; the interval of lines to read. Affects img zoom

PUSH ebx
PUSH edi
PUSH esi

and cc, 0
and ccFloat, 0
and xPos, 0
and yPos, 0
and ySum, 0
and dSum, 0
; ----------------- Constants ---------------------------------------------------------------------------------------
byteVal EQU 32 ; the target byte we want to setPixel. 32 is space (" ")
xInc EQU 8 ; Fixed value for scale x. The bigger, the smaller
yInc EQU 8 ; Fixed value for scale y. The bigger, the smaller
; --------------------------------------------------------------------------------------------------------------------------

; --------------------------------- CREATE IMG FIRST ------------------------------------------
; First, it creates a compatible bmp
; I´m cheating: made beginPaint hdc handle (from CREATE) a global
; only to deal with the miniMap here
; ------------------------------------------------------------------------------------------------------------
invoke CreateCompatibleBitmap, gHDC, MM_WIDTH, MM_HEIGHT
  mov  hMiniMapBmp, eax
invoke SelectObject, hMiniMapDC, hMiniMapBmp

; ----------------------------------------------------------------------------
; Fit Map to MiniMap dimensions: TRUE / FALSE
; ----------------------------------------------------------------------------
cmp tFit, TRUE
jnz fixedInc
; ------------------------------------------ Fit in img -----------------------------------------------------------
; -------- fit mapY minimap ----------

fild map.height
push MM_HEIGHT
fidiv dword ptr [esp] ; map height / MiniMap height
fstp lineInc

; --------- fit mapX minimap ---------
push cWIDTH
fild dword ptr [esp]
push MM_WIDTH
fidiv dword ptr [esp] ; map width / MiniMap width
fstp byteInc

;deb 4, "fpu", cWIDTH, MM_WIDTH, byteInc ;debug
jmp drawMap

fixedInc:
; --------------------------------------------------------------------------------
; It´s fixed. Use the values set in xInc and yInc above
; --------------------------------------------------------------------------------
mov byteInc,  xInc ; the bigger value, the smaller the Minimap x view is
mov lineInc, yInc
drawMap:

xor ebx, ebx
newLinePtr:
mov edi, [map.mapLinePtr+ ebx * 4] ; Get line Ptr (map.mapLinePtr is an array of Ptr)
fillLine:
cmp byte Ptr [edi], byteVal ; 32 == space, it´s what we want
jnz nextPix ; it´s not, skip SetPixel

invoke SetPixel, hMiniMapDC, xPos, yPos, 0FFFF0Bh
nextPix:
; ----------------------------------------------------------------------------------------
; Byte is NOT 32, so we don´t setpixel and just keep going
; next byte within line
; ----------------------------------------------------------------------------------------

; ---------------- inc  byte ---------------------------------------------------------
push edi ; byte address
fild dword ptr [esp]
fadd byteInc ; add float offset to address
fistp nAdd ; round new address

; ---------------- inc counter -----------------------------------------------------
fld ccFloat ; ccFloat is the float counter
fadd byteInc ; add float offset
fst ccFloat ; save it
fistp cc ; round next count

; ---------------- udpade address and xPos ------------------------------
mov edi, nAdd ; update address
add xPos, 1 ; next x coord
cmp xPos, MM_WIDTH
jb fillLine

; ----------------------------------------------------------------------------------------
;                        End of line, next line Ptr please
; ----------------------------------------------------------------------------------------
fld ySum ; float Y val
fadd lineInc ; add float Y offset
fst ySum ; save it to float
fistp dSum ; round next line value

mov ebx, dword ptr [dSum]

and ccFloat, 0
and  xPos, 0
add yPos, 1

cmp ebx, map.height ; map.height is the num of lines of map
jb newLinePtr
done:
POP esi
POP edi
POP ebx
ret
pCreateMiniMap endp
Title: Re: FPU question
Post by: jj2007 on February 02, 2018, 04:42:06 PM
While there are no FPU instructions for loading constants, there are the Masm32 SDK FPxx macros doing that:

include \masm32\MasmBasic\MasmBasic.inc
  Init
  fld FP4(1234567890.1234567890)
  fld FP8(1234567890.1234567890)
  fld FP10(1234567890.1234567890)
  deb 1, "On the FPU:", ST(0), ST(1), ST(2)
EndOfCode


On the FPU:
ST(0)           1234567890.123456789
ST(1)           1234567890.123456716
ST(2)           1234567936.000000000


Note the low precision of the last one, and the order: FP4 gets loaded first, and is finally in ST(2)
Title: Re: FPU question
Post by: raymond on February 03, 2018, 04:37:58 AM
Once you learn how to use the FPU, you probably should shy away from using macros such as the FPxx series. For one, writing the actual code yourself is just as fast (and sometimes faster) and you know the exact output unless you also become very familiar with the purpose and output of the macro.

For example, the FPxx macro would initialize a float value as a 'local variable' on the stack within a procedure. If you want to use that same 'constant' elsewhere outside the procedure where it was initialized, you would have to use a separate macro to reinitialize it once more. You might as well initialize it once in your .data section and use it throughout your program.
Title: Re: FPU question
Post by: jj2007 on February 03, 2018, 05:23:38 AM
Quote from: raymond on February 03, 2018, 04:37:58 AMthe FPxx macro would initialize a float value as a 'local variable' on the stack within a procedure.
Sure?
Title: Re: FPU question
Post by: raymond on February 03, 2018, 05:36:14 AM
Quote from: jj2007 on February 03, 2018, 05:23:38 AM
Quote from: raymond on February 03, 2018, 04:37:58 AMthe FPxx macro would initialize a float value as a 'local variable' on the stack within a procedure.
Sure?

I may be wrong but it looks like it to me according to its description.

Quote; **********************************************************
    ; function style macros for direct insertion of data types *
    ; **********************************************************

      FP4 MACRO value
        LOCAL vname
        .data
        align 4
          vname REAL4 value
        .code
        EXITM <vname>
      ENDM
Title: Re: FPU question
Post by: jj2007 on February 03, 2018, 05:46:42 AM
FPxx allocates its value in the .data section, so it's always a global variable. But you are right that re-using the same memory location would be desirable. That can be done with certain macro techniques, though.

Main argument for macros is that
  fld FP4(12.3)  ; you see the value
is clearer than
  fld somefloat  ; you must consult the .data section to know the value
Title: Re: FPU question
Post by: HSE on February 03, 2018, 06:33:09 AM
With SmplMath you can write:fSlv8  = 12.3 That store de value in st0 and the constant is located in data section only the first time.
Title: Re: FPU question
Post by: LordAdef on February 03, 2018, 01:25:35 PM
Good to know about these macros. But since I´m new to all of this, the best practice is to stay in the asm world until I fell confortable with FPU, as Raymond has also said.

But I wouldn´t know about the macros and now I know!
Cheers