BBC BASIC for Windows
Programming >> Graphics and Games >> Julia sets in Assembler
http://bb4w.conforums.com/index.cgi?board=graphics&action=display&num=1460709250

Julia sets in Assembler
Post by DDRM on Apr 15th, 2016, 08:34am

Inspired by Ric's experiments with writing a real-time 3d renderer, I've been revisiting the assembler, and particularly the floating point instructions. It's actually surprisingly easy once you get your head around the way the stack of "registers" works, and once Richard had banged into my head that I had to leave the stack pointer at the top again when I bailed out! It's also blindingly fast.

I wanted to make a viewer that would allow you to explore the Julia sets associated with the area around the Mandelbrot set. This is quite a complex (in joke) area, which I won't attempt to explain, but basically points within the Mandelbrot set lead to a connected set of points which stay close to home, while points far away fly rapidly off to infinity. At the borders are some beautiful and complex patterns if we render the time to escape into colours.

If you run this code, it will open a window (initially) showing the area from (-2,-1.25i) to (2,1.25i). As you move the mouse around over it you will see the Julia set (for the function z(i+1)=z(i)^2+c) associated with that point. Pressing C will toggle between a colour and a grey scale palette.

If you scroll with the mouse wheel, two things will happen. (i) the image will magnify or shrink by 2 for each "click" of the wheel
(ii) the position of the image will be frozen. You can then use the mouse (left button) to drag the image around. It is re-rendering as you do this!

To unlock the image again (or to lock it initially), press F. Be careful what zoom you are at, and where your mouse is when you do this, or you may find yourself in the middle of a huge, boring area! I leave it to the user to add a "reset to the start" function... or you can always quit (Q) and re-rerun.

Hope that's interesting/amusing to someone...

D

PS Yes, I know it has a few naming infractions, but really only in the assembler code...

PPS If you change the two instructions near the start of the assembler code:
fld tbyte [^ci]
fld tbyte [^cr]

to

fld tbyte [^ni]
fld tbyte [^nr]

You will get the Mandelbrot set itself, and can explore that in the same way.

Code:
      MODE 8
      xres=@vdu%!208
      yres=@vdu%!212

      minx=-2.0
      maxx=2.0
      xrange=maxx-minx
      xstep=xrange/xres
      miny=-1.25
      maxy=1.25
      yrange=maxy-miny
      ystep=yrange/yres

      REM Reserve space for the assembly routine itself, making sure it's in its own 2K block to avoid cache thrashing.
      size% = 2048
      DIM code% NOTEND AND 2047, code% size%-1
      REM These are just dummy variables, while setting up the assembler
      nr=3.5
      ni=1.5
      cr=0.29
      ci=0.01
      mag%=0
      temp=0.0
      col%=TRUE
      fixed%=FALSE

      PROCAssJulia

      REM Set up a DIBsection for screen output, so we can write data to it directly

      DIM BITMAPINFOHEADER{Size%, Width%, Height%, Planes{l&,h&}, BitCount{l&,h&}, \
      \                    Compression%, SizeImage%, XPelsPerMeter%, YPelsPerMeter%, \
      \                    ClrUsed%, ClrImportant%}

      DIM bmi{Header{} = BITMAPINFOHEADER{}, Palette%(255)}

      bmi.Header.Size% = DIM(BITMAPINFOHEADER{})
      bmi.Header.Width% = @vdu%!208
      bmi.Header.Height% = @vdu%!212
      bmi.Header.Planes.l& = 1
      bmi.Header.BitCount.l& = 8

      REM We've made an 8 bit per pixel bitmap definition: define a  palette to go with it

      SYS "CreateDIBSection", @memhdc%, bmi{}, 0, ^bits%, 0, 0 TO hbitmap%
      IF hbitmap% = 0 ERROR 100, "Couldn't create DIBSection"

      SYS "SelectObject", @memhdc%, hbitmap% TO oldhbm%
      SYS "DeleteObject", oldhbm%

      PROCSetColour(col%)

      CLS
      bytesperpixel% = bmi.Header.BitCount.l& DIV 8
      bytesperline% = ((bmi.Header.Width% * bytesperpixel%) + 3) AND -4

      REPEAT
        MOUSE x%,y%,z%
        IF NOT fixed% THEN
          cr=minx+x%*xstep/2
          ci=miny+y%*ystep/2
          PRINT TAB(0,0);cr,ci
        ELSE
          IF z%>0 THEN
            tx%=x%
            ty%=y%
            REPEAT MOUSE x%,y%,z% UNTIL z%=0
            minx+=(tx%-x%)*xstep/2
            maxx+=(tx%-x%)*xstep/2
            miny+=(ty%-y%)*ystep/2
            maxy+=(ty%-y%)*ystep/2
          ENDIF
        ENDIF
        CALL code%
        SYS "InvalidateRect", @hwnd%, 0, 0
        q$=INKEY$(0)
        CASE q$ OF
          WHEN "c","C":col%=NOT col%:PROCSetColour(col%)
          WHEN "f","F": fixed%=NOT fixed%
          WHEN CHR$(140):
            fixed%=TRUE
            minx+=xrange/4
            maxx-=xrange/4
            xrange=maxx-minx
            xstep=xrange/xres
            miny+=yrange/4
            maxy-=yrange/4
            yrange=maxy-miny
            ystep=yrange/yres
          WHEN CHR$(141):
            fixed%=TRUE
            minx-=xrange/4
            maxx+=xrange/4
            xrange=maxx-minx
            xstep=xrange/xres
            miny-=yrange/4
            maxy+=yrange/4
            yrange=maxy-miny
            ystep=yrange/yres
        ENDCASE
  
      UNTIL q$="q" OR q$="Q"
      QUIT
      :
      DEFPROCSetColour(col%)
      LOCAL i%,r%,g%,b%
      IF col% THEN
        FOR i% = 0 TO 255
          r% = (i% MOD 16)*16
          g% = (i% MOD 64)*4
          b% = 255-i%/2
          bmi.Palette%(i%) = b% + (g% << 8) + (r% << 16)
        NEXT
      ELSE
        FOR i% = 0 TO 255
          bmi.Palette%(i%) = i%  + (i% << 8) + (i% << 16)
        NEXT
      ENDIF
      SYS "SetDIBColorTable", @memhdc%, 0, 256, ^bmi.Palette%(0)

      ENDPROC
      :
      DEFPROCAssJulia
      LOCAL opt%
      FOR opt%=0 TO 2 STEP 2
        P%=code%
        [
        OPT opt%
        mov esi,[^yres]
        mov edx,[^bits%]
        fld tbyte [^miny]
        fstp tbyte [^ni]
        .yloop
        mov ecx,[^xres]
        fld tbyte [^minx]
        fstp tbyte [^nr]
        .xloop
        mov eax,0
        fld tbyte [^ci]
        fld tbyte [^cr]
        fld tbyte [^nr]
        fst st3
        fmul st0,st0
        fstp st4      ;After this we have cr,ci,nr and nr2 in ST0-3, and 2 pushes
        fld tbyte [^ni]
        fst st5
        fmul st0,st0
        fstp st6      ;After this we have cr,ci,nr, nr2,ni and ni2 in ST0-5, and 2 pushes
        .lpt
        ;OK now we should be able to calculate the new values of nr, ni
        fld st2
        fadd st0,st0
        fmul st0,st5
        fadd st0,st2
        fstp st5     ;we should be in the same place, but with the new value of ni
        fld st3
        fsub st0,st6
        fadd st0,st1
        fstp st3     ;After this we now have cr,ci, the new nr, the old nr2, the new ni, and the old ni2 in ST0-5, and 2 pushes
        fld st2
        fmul st0,st0
        fstp st4    ;now with new nr2, still 2 pushes
        fld st4
        fmul st0,st0
        fst st6    ;now with new ni2, now 3 pushes
        fadd st0,st4  ;gives magnitude squared
        fistp dword [^mag%]  ;store it as an integer, so it can be read into a standard register and compared, Back to 2 pushes
        inc eax
        mov ebx,[^mag%]
        cmp ebx,4
        ja esc
        cmp eax,255
        jb lpt
        .esc
        ;Now we have the problem of popping the stack twice
        fstp tbyte [^temp]     ;should pop cr into temp
        fstp tbyte [^temp]     ;should pop ci into temp
        mov byte [edx],al
        add edx,[^bytesperpixel%]
        fld tbyte [^nr]
        fld tbyte [^xstep]
        faddp st1,st0
        fstp tbyte [^nr]
        dec ecx
        jnz near xloop
        fld tbyte [^ni]
        fld tbyte [^ystep]
        faddp st1,st0
        fstp tbyte [^ni]
        dec esi
        jnz near yloop
        ret
        ]
      NEXT opt%
      ENDPROC

 


Re: Julia sets in Assembler
Post by Zaphod on Apr 15th, 2016, 12:44pm


I have always been fascinated by fractals and this is magnificent. Thanks for sharing it. And boy is it fast!


Re: Julia sets in Assembler
Post by michael on Apr 15th, 2016, 9:56pm

That's fantastic.. I only played with ASM programming for a short bit in the early 80s on my TRS-80. At the time it just didn't have enough attention grab to lure me in. I think I had a ASM book and a cartridge of some sort for the interface for the programs.
( and limited.. I played with it for a very short time)

Its nice to see a post like that.. I would like to see more assembly language examples.. PLEASE!!
Maybe assembly snippets that do simple draws?
Re: Julia sets in Assembler
Post by Zaphod on Apr 16th, 2016, 7:17pm

Can I just comment that the program is heavy on CPU time as it rewrites the display even if the input variables haven't changed. So having ripped into you, Dave, I will leave you to do the decent thing. wink

I am having great fun with it.

How would you toggle between the Julia set and Mandlebrot set from the user interface? Is that something that could be added?
Re: Julia sets in Assembler
Post by DDRM on Apr 17th, 2016, 5:57pm

HI Zaphod,

I'm sure it is within your powers to save the mouse coordinates and only call the assembler if they have changed, so I'll leave it to you to do the decent thing yourself.
grin

With regard to making the switch from Julia sets to Mandelbrot: not straightforward (the value of nr and ni is different for every point in the Mandelbrot calculation, and that is done within the assembler code, while the value of cr and ci is fixed for a given Julia set). I could do it by setting a flag somewhere and testing it, then making the appropriate assignment, but of course that would slow down the assembler all the time - but that may not matter much in the grand scheme of things. I'll have a go. It's not in the innermost loop.

Best wishes,

D
Re: Julia sets in Assembler
Post by DDRM on Apr 17th, 2016, 7:02pm

Hi Zaphod (and anyone else who cares...)

OK, I have done your bidding. This version includes two new controls: you can press J to go to Julia sets (and reset to the starting field of view), or M to go to the Mandelbrot set (and reset the field of view).

I also implemented all sorts of tests to avoid running the assembler if nothing had changed - but still got heavy CPU usage. Of course, all it really needed was to put a WAIT 0 in the main loop of the BASIC bit... but I've left the other bits in, because I can't be bothered to take them out again...

Best wishes,

D

Code:
      MODE 8
      xres=@vdu%!208
      yres=@vdu%!212
      julia%=1:PROCSetfieldJ


      REM Reserve space for the assembly routine itself, making sure it's in its own 2K block to avoid cache thrashing.
      size% = 2048
      DIM code% NOTEND AND 2047, code% size%-1
      REM These are just dummy variables, while setting up the assembler
      nr=3.5
      ni=1.5
      cr=0.29
      ci=0.01
      mag%=0
      temp=0.0
      col%=TRUE
      fixed%=FALSE

      PROCAssJulia

      REM Set up a DIBsection for screen output, so we can write data to it directly

      DIM BITMAPINFOHEADER{Size%, Width%, Height%, Planes{l&,h&}, BitCount{l&,h&}, \
      \                    Compression%, SizeImage%, XPelsPerMeter%, YPelsPerMeter%, \
      \                    ClrUsed%, ClrImportant%}

      DIM bmi{Header{} = BITMAPINFOHEADER{}, Palette%(255)}

      bmi.Header.Size% = DIM(BITMAPINFOHEADER{})
      bmi.Header.Width% = @vdu%!208
      bmi.Header.Height% = @vdu%!212
      bmi.Header.Planes.l& = 1
      bmi.Header.BitCount.l& = 8

      REM We've made an 8 bit per pixel bitmap definition: define a  palette to go with it

      SYS "CreateDIBSection", @memhdc%, bmi{}, 0, ^bits%, 0, 0 TO hbitmap%
      IF hbitmap% = 0 ERROR 100, "Couldn't create DIBSection"

      SYS "SelectObject", @memhdc%, hbitmap% TO oldhbm%
      SYS "DeleteObject", oldhbm%

      PROCSetColour(col%)
      CLS
      OFF
      bytesperpixel% = bmi.Header.BitCount.l& DIV 8
      bytesperline% = ((bmi.Header.Width% * bytesperpixel%) + 3) AND -4
      tcr=-5
      tci=-5
      REPEAT
        WAIT 0
        MOUSE x%,y%,z%
        IF (NOT fixed%) AND julia% THEN
          cr=minx+x%*xstep/2
          ci=miny+y%*ystep/2
          IF cr<>tcr OR ci<>tci THEN changed%=TRUE
          PRINT TAB(0,0);cr,ci
        ELSE
          IF z%>0 THEN
            tx%=x%
            ty%=y%
            REPEAT MOUSE x%,y%,z% UNTIL z%=0
            minx+=(tx%-x%)*xstep/2
            maxx+=(tx%-x%)*xstep/2
            miny+=(ty%-y%)*ystep/2
            maxy+=(ty%-y%)*ystep/2
            changed%=TRUE
          ENDIF
        ENDIF
        IF changed% THEN
          CALL code%
          changed%=FALSE
          tcr=cr
          tci=ci
          SYS "InvalidateRect", @hwnd%, 0, 0
        ENDIF
        q$=INKEY$(0)
        CASE q$ OF
          WHEN "c","C":col%=NOT col%:PROCSetColour(col%):changed%=TRUE
          WHEN "f","F": fixed%=NOT fixed%
          WHEN "j","J": julia%=1:PROCSetfieldJ
          WHEN "m","M": julia%=0:PROCSetfieldM
      
          WHEN CHR$(140):
            fixed%=TRUE
            minx+=xrange/4
            maxx-=xrange/4
            xrange=maxx-minx
            xstep=xrange/xres
            miny+=yrange/4
            maxy-=yrange/4
            yrange=maxy-miny
            ystep=yrange/yres
            changed%=TRUE
          WHEN CHR$(141):
            fixed%=TRUE
            minx-=xrange/4
            maxx+=xrange/4
            xrange=maxx-minx
            xstep=xrange/xres
            miny-=yrange/4
            maxy+=yrange/4
            yrange=maxy-miny
            ystep=yrange/yres
            changed%=TRUE
        ENDCASE
  
      UNTIL q$="q" OR q$="Q"
      QUIT
      :
      DEFPROCSetColour(col%)
      LOCAL i%,r%,g%,b%
      IF col% THEN
        FOR i% = 0 TO 255
          r% = (i% MOD 16)*16
          g% = (i% MOD 64)*4
          b% = 255-i%/2
          bmi.Palette%(i%) = b% + (g% << 8) + (r% << 16)
        NEXT
      ELSE
        FOR i% = 0 TO 255
          bmi.Palette%(i%) = i%  + (i% << 8) + (i% << 16)
        NEXT
      ENDIF
      SYS "SetDIBColorTable", @memhdc%, 0, 256, ^bmi.Palette%(0)

      ENDPROC
      :
      DEFPROCSetfieldJ
      minx=-2.0
      maxx=2.0
      xrange=maxx-minx
      xstep=xrange/xres
      miny=-1.25
      maxy=1.25
      yrange=maxy-miny
      ystep=yrange/yres
      changed%=TRUE
      fixed%=FALSE
      ENDPROC
      :
      DEFPROCSetfieldM
      minx=-2.0
      maxx=0.5
      xrange=maxx-minx
      xstep=xrange/xres
      miny=-1.25
      maxy=1.25
      yrange=maxy-miny
      ystep=yrange/yres
      changed%=TRUE
      fixed%=FALSE
      ENDPROC
      :
      DEFPROCAssJulia
      LOCAL opt%
      FOR opt%=0 TO 2 STEP 2
        P%=code%
        [
        OPT opt%
        mov esi,[^yres]
        mov edx,[^bits%]
        fld tbyte [^miny]
        fstp tbyte [^ni]
        .yloop
        mov ecx,[^xres]
        fld tbyte [^minx]
        fstp tbyte [^nr]
        .xloop
        mov eax,[^julia%]
        cmp eax,0
        jz mand
        fld tbyte [^ci]
        fld tbyte [^cr]
        jmp inloop
        .mand
        fld tbyte [^ni]
        fld tbyte [^nr]
        .inloop
        mov eax,0
        fld tbyte [^nr]
        fst st3
        fmul st0,st0
        fstp st4      ;After this we have cr,ci,nr and nr2 in ST0-3, and 2 pushes
        fld tbyte [^ni]
        fst st5
        fmul st0,st0
        fstp st6      ;After this we have cr,ci,nr, nr2,ni and ni2 in ST0-5, and 2 pushes
        .lpt
        ;OK now we should be able to calculate the new values of nr, ni
        fld st2
        fadd st0,st0
        fmul st0,st5
        fadd st0,st2
        fstp st5     ;we should be in the same place, but with the new value of ni
        fld st3
        fsub st0,st6
        fadd st0,st1
        fstp st3     ;After this we now have cr,ci, the new nr, the old nr2, the new ni, and the old ni2 in ST0-5, and 2 pushes
        fld st2
        fmul st0,st0
        fstp st4    ;now with new nr2, still 2 pushes
        fld st4
        fmul st0,st0
        fst st6    ;now with new ni2, now 3 pushes
        fadd st0,st4  ;gives magnitude squared
        fistp dword [^mag%]  ;store it as an integer, so it can be read into a standard register and compared, Back to 2 pushes
        inc eax
        mov ebx,[^mag%]
        cmp ebx,4
        ja esc
        cmp eax,255
        jb lpt
        .esc
        ;Now we have the problem of popping the stack twice
        fstp tbyte [^temp]     ;should pop cr into temp
        fstp tbyte [^temp]     ;should pop ci into temp
        mov byte [edx],al
        add edx,[^bytesperpixel%]
        fld tbyte [^nr]
        fld tbyte [^xstep]
        faddp st1,st0
        fstp tbyte [^nr]
        dec ecx
        jnz near xloop
        fld tbyte [^ni]
        fld tbyte [^ystep]
        faddp st1,st0
        fstp tbyte [^ni]
        dec esi
        jnz near yloop
        ret
        ]
      NEXT opt%
      ENDPROC
 


Re: Julia sets in Assembler
Post by Zaphod on Apr 18th, 2016, 12:33am

Why, thank you!

The BASIC stuff yes, I fixed that to my satisfaction and added key movement and zoom controls and higher precision starting point selection via the keys, and put in a refresh flag etc. I did not want to post that as it looks like one up man ship which others have done in the past. Thanks for redoing that.

But the assembler, not within my capabilities so thanks again for that part. I found that I could go 98 levels of zoom on one set, truly remarkable. Very well done.

And I very much hope others are interested as it is a great example of what BB4W can do in the right hands.

Re: Julia sets in Assembler
Post by KenDown on Jun 1st, 2016, 07:56am

Am I doing something wrong? I get the Mandelbrot set, but I can't dig down into it. It just sits there, no matter how I move the mouse or which button I press.
Re: Julia sets in Assembler
Post by DDRM on Jun 6th, 2016, 10:57am

Hi Kendall,

Sorry for the delay, I've been away, without net access. Fantastic!

Umm, don't know, it works for me!

Pressing M switches to the Mandelbrot set - I guess that is working?

Dragging (with left or right button) moves it about, and scrolling the scroll wheel magnifies or zooms out.

Unlike for the Julia sets just moving the mouse doesn't do anything - there's no logical equivalent to implement.

The scroll wheel handling is the WHEN CHR$(140) (and 141) sections: if you don't have, or don't like, a scroll wheel you could amend this to 138/139 to use the arrow keys instead.

Best wishes,

D
Re: Julia sets in Assembler
Post by KenDown on Jun 6th, 2016, 7:36pm

Ah, thanks. I wasn't using the wheel. Perhaps a few instructions wouldn't go astray, as it does work once you know how.
Re: Julia sets in Assembler
Post by Ric on Jun 15th, 2016, 08:41am

Fantastic DDRM,

Nice to see you have dabbled in fpasm again. Although this is more than dabbled. The results are great and in real time too. (not sure what julias or mandlebrots are tho.)

Keep dabbling grin grin

Ric