+TheBF Posted May 11, 2018 Author Share Posted May 11, 2018 I have developed a healthy respect for the authors of the Sprite control in X-BASIC. I found this old XB Demo program and wondered how I could do it in "GOLLY GEE WHIZ" Forth. Without Automotion I had to create a timer variable for each motion element and down count to zero, like the ROM code does. Was able to gain some efficiency by creating words that only play with X or Y in a sprite independently. Anyway for those studying Forth this might provide some ideas. \ EXTENDED BASIC DEMO ported to CAMEL99 V2 INCLUDE DSK1.TOOLS.F INCLUDE DSK1.CHARSET.F INCLUDE DSK1.DIRSPRIT.F \ This also loads GRAFIX.F HEX 0103 070F 1F3F 7FFF PATTERN: CH40 \ LEFT-TRIANGLE FFFF FFFF FFFF FFFF PATTERN: CH41 \ BLOCK1 80C0 E0F0 F8FC FEFF PATTERN: CH48 \ RIGHT-TRIANGLE FFFF FFFF FFFF FFFF PATTERN: CH49 \ BLOCK2 FFAA AAAA AAAA AAAA PATTERN: CH56 \ VERT LINES (FENCE) AAAA AAAA AAAA AAFF PATTERN: CH73 \ FENCE-BOTTOM 0000 0000 0000 0000 PATTERN: CH64 \ empty char FF80 BFA0 A0A0 A0A0 PATTERN: CH57 \ TOP-LEFT CORNER FF00 FF00 0000 0000 PATTERN: CH58 \ TOP-DOUBLE-LINE FF01 FD05 0505 0505 PATTERN: CH59 \ TOP-RIGHT CORNER 0505 0505 0505 0505 PATTERN: CH60 \ VERT-DBL-LINE 0505 0505 05FD 01FF PATTERN: CH61 \ BOT-RIGHT-CORN 0000 0000 00FF 00FF PATTERN: CH62 \ BOT-DOUBLE-LINE A0A0 A0A0 A0BF 80FF PATTERN: CH63 \ BOT-LEFT-CORN A0A0 A0A0 A0A0 A0A0 PATTERN: CH72 \ LEFT-DBL-LINE EEAA AAEE 2222 2222 PATTERN: CH80 \ '99' 8080 0077 5474 4474 PATTERN: CH81 \ 'er 7EFF FFFF FFFF FF7E PATTERN: CH88 \ ROCK 3C3C 3C3C 3C7E 7E99 PATTERN: CH104 \ STUMP 3038 7EFF FFFE 3E3C PATTERN: CH105 \ TOP-CLOUD 0000 0000 183C 3C18 PATTERN: CH106 \ TIRE 0003 0408 7FFF CF87 PATTERN: CH107 \ FRONT-CAR 00F0 8884 FEFF E7C3 PATTERN: CH108 \ BACK-CAR 0000 0000 3C5A 9981 PATTERN: CH109 \ WINGS-DOWN 0081 8142 3C18 1800 PATTERN: CH110 \ WINGS-UP 005C 7E7E FFFF 7600 PATTERN: CLOUD DECIMAL : DEF-CHARS CH40 40 CHARDEF CH41 41 CHARDEF CH48 48 CHARDEF CH49 49 CHARDEF CH56 56 CHARDEF CH73 73 CHARDEF CH57 57 CHARDEF CH58 58 CHARDEF CH59 59 CHARDEF CH60 60 CHARDEF CH61 61 CHARDEF CH62 62 CHARDEF CH63 63 CHARDEF CH72 72 CHARDEF CH80 80 CHARDEF CH81 81 CHARDEF CH88 88 CHARDEF CH41 96 CHARDEF CH104 104 CHARDEF CH105 105 CHARDEF CH106 106 CHARDEF CH107 107 CHARDEF CH108 108 CHARDEF CH109 109 CHARDEF CH110 110 CHARDEF CLOUD 111 CHARDEF ; : MAKE-SPRITES \ CHAR CLR COL ROW SPR# 104 9 104 130 1 SPRITE \ treetop 105 13 104 114 2 SPRITE \ tree trunk 106 2 101 116 3 SPRITE \ BACK TIRE 108 6 100 109 4 SPRITE \ BACK CAR 107 6 84 109 5 SPRITE \ FRONT CAR 106 2 82 116 6 SPRITE \ FRONT TIRE 109 2 100 35 11 SPRITE \ bird 111 16 10 31 8 SPRITE \ cloud1 111 16 50 13 9 SPRITE \ cloud2 111 16 200 50 10 SPRITE ; \ cloud3 \ motion support : --@ ( variable -- n ) -1 OVER +! @ ; : SP.X+! ( n spr# -- ) DUP >R SP.X@ + R> SP.X! ; : SP.Y+! ( n spr# -- ) DUP >R SP.Y@ + R> SP.Y! ; VARIABLE CAR-TMR : MOVE-CAR ( -- ) CAR-TMR --@ 0= IF -1 3 SP.X+! \ move 4 sprites together -1 4 SP.X+! -1 5 SP.X+! -1 6 SP.X+! 20 CAR-TMR ! THEN ; VARIABLE CLOUDTMR : MOVE-CLOUDS CLOUDTMR --@ 0= IF -1 8 SP.X+! -2 9 SP.X+! 1 10 SP.X+! 120 CLOUDTMR ! THEN ; VARIABLE BIRDTMR : MOVE-BIRD BIRDTMR --@ 0= IF 1 11 SP.X+! 25 BIRDTMR ! THEN ; VARIABLE FLAPTMR : FLAP ( -- ) FLAPTMR --@ 0= IF 11 ]SDT ->PAT VC@ \ read sprite pattern 109 = IF 110 11 PATTERN ELSE 109 11 PATTERN THEN 75 FLAPTMR ! \ reset timer THEN ; : SET-TIMERS 20 CAR-TMR ! 75 FLAPTMR ! 120 CLOUDTMR ! 25 BIRDTMR ! ; DECIMAL : DEFAULTS \ restore graphics to BASIC defaults 4 19 2 8 COLORS CHARSET 8 SCREEN DELALL ; : ROAD ( -- ) 96 SET# 16 16 COLOR 0 15 96 64 HCHAR ; : LOGRASS ( -- ) 0 17 41 32 7 * HCHAR ; : HIGRASS ( -- ) 0 13 49 32 2* HCHAR ; : FENCE 56 SET# 2 12 COLOR 73 SET# 2 12 COLOR 20 13 56 12 HCHAR 20 14 73 12 HCHAR ; : GREEN-COLORS ( -- ) 40 SET# 13 8 COLOR 48 SET# 4 8 COLOR ; : .MOUNTAINS ( col row -- ) AT-XY CR ." (0" CR ." (0 ()10 (0" CR ." ()10 (0 ())110 ()10" CR ." ())110 ()10 ()))1110 ())110" CR ." ()))1110())110())))11110()))1110" ; : ROCK ( -- ) 88 SET# 14 11 COLOR 7 14 88 1 HCHAR 9 14 88 1 HCHAR ; : SIGN-COLORS ( -- ) 7 9 2 12 COLORS ; : .SIGN ( col row -- ) [CHAR] P SET# 2 12 COLOR 2DUP AT-XY ." 9::;" 1+ 2DUP AT-XY ." HPQ<" 1+ AT-XY ." ?>>=" ; DECIMAL : RUN ( -- ) DELALL 9 SCREEN CLEAR DEF-CHARS GREEN-COLORS 32 SET# 8 8 COLOR 0 7 .MOUNTAINS ROAD LOGRASS HIGRASS SIGN-COLORS 17 11 .SIGN ROCK FENCE 1 MAGNIFY MAKE-SPRITES SET-TIMERS BEGIN MOVE-CAR MOVE-CLOUDS MOVE-BIRD FLAP ?TERMINAL UNTIL DEFAULTS ; xbdemo.mp4 1 Quote Link to comment Share on other sites More sharing options...
Tursi Posted May 15, 2018 Share Posted May 15, 2018 (edited) Hah, that looks familiar. I wrote that for the Summerland TI Users Group back when I was still in high school, based on a picture in 99er Magazine (was part of an explanation of sprites). (edit: the original XB version, that is ) Edited May 15, 2018 by Tursi 3 Quote Link to comment Share on other sites More sharing options...
+TheBF Posted May 16, 2018 Author Share Posted May 16, 2018 Hah, that looks familiar. I wrote that for the Summerland TI Users Group back when I was still in high school, based on a picture in 99er Magazine (was part of an explanation of sprites). (edit: the original XB version, that is ) You know you are history when... :-) That's really cool. Thanks for letting everybody know. B Quote Link to comment Share on other sites More sharing options...
D-Type Posted May 17, 2018 Share Posted May 17, 2018 So now there's 3 Forth systems for the TI? I assume this one is based on Brad's Camel Forth? But, by the look of it, you're not cross compiling from a PC? I'm also using Brad's Camel Forth, but the 6809 variant and using his Chromium cross compiler. Target is the Vectrex, probably a lot less work than you need to put it as it has the correct processor! Quote Link to comment Share on other sites More sharing options...
+TheBF Posted May 17, 2018 Author Share Posted May 17, 2018 (edited) So now there's 3 Forth systems for the TI? I assume this one is based on Brad's Camel Forth? But, by the look of it, you're not cross compiling from a PC? I'm also using Brad's Camel Forth, but the 6809 variant and using his Chromium cross compiler. Target is the Vectrex, probably a lot less work than you need to put it as it has the correct processor! Yes I am the last of the noble group. I am cross-compiling actually. On a "real" PC, the DOSBOX. :-) The cross-compiler is here. https://github.com/bfox9900/CAMEL99/tree/master/Compiler It actually began with the TI-Forth assembler that I modified to assemble into a different memory segment. It runs on an old DOS Forth from the '90s called HsForth which I upgraded to something a little closer to ANS Forth. I stripped the comments from Brad's MSP430 Camel Forth Assembly Language source , which are Forth and wrote the compiler to compile the comments. :-) It still had to write all the primitives in 9900 assembler however. Edit: The cross-compiler builds an 8K kernel that can extend itself with source code. The latest version now can compile TI DV80 source files and I have the beginnings of ANS file word set support. I had to do it the hard way. It's been on my bucket list for 35 years to make a cross-compiler so time was running out. Brian Edited May 17, 2018 by TheBF Quote Link to comment Share on other sites More sharing options...
+TheBF Posted May 18, 2018 Author Share Posted May 18, 2018 (edited) Improved Background Sound List Player Spent some time working on what I think can be my goto sound list player. I will test it a little harder and then put it up on GitHub. But I notice that even though it does not use interrupts it plays very solid when I exercise the screen heavily. The features are: It can Queue up to 16 sound lists Sound lists are stored in VDP RAM Code includes VDP byte directive so lists are as easy to code as Assembler Player task goes to sleep when sound Queue is empty Uses the TMS9901 timer for sound duration Task memory is allocated in Low RAM Size: Multi-tasker, VDP memory manager, VDP byte compiler, BG player adds 1218 bytes to the Forth dictionary >SNDQ command enqueues a sound list PLAYQ command plays whatever is the sound queue KILLQ command emptys the sound queue. (playing sounds will continue until finished) Sound lists look like this: VCREATE NOKIA VBYTE 01,9F,20 VBYTE 03,90,85,05,09 VBYTE 02,8F,05,09 VBYTE 02,87,09,12 VBYTE 02,87,08,12 VBYTE 02,85,06,09 VBYTE 02,81,07,09 VBYTE 02,8E,0B,12 VBYTE 02,8A,0A,12 VBYTE 02,81,07,09 VBYTE 02,8F,07,09 VBYTE 02,8A,0C,12 VBYTE 02,8A,0A,12 VBYTE 02,8F,07,24 VBYTE 01,9F,00 /VEND \ BACKGROUND VDP sound list player in CAMEL99 Forth V2 INCLUDE DSK1.MTASK99.F INCLUDE DSK1.VDPMEM.F \ ======================================================== \ sound list player HEX : SILENT ( --) 9F SND! BF SND! DF SND! FF SND! ; \ turn off all sounds : PLAY$ ( $addr -- ) \ play 1 sound string, no time-sharing VCOUNT ( -- addr len) 2DUP + VC@ >R \ get duration at end of string, to milli-secs & Rpush BOUNDS \ convert addr/len to end-addr. start-addr. DO I VC@ SND! LOOP \ feed bytes from VDP mem to sound chip R> JIFFS ; \ use the delay from Rstack 1 jiff = 1/60 sec \ play a TI sound list : PLAYLIST ( list-addr -- ) BEGIN DUP VC@ \ read the string length byte WHILE ( tos <> 0) PAUSE DUP PLAY$ \ play a single string VCOUNT + 1+ \ advance to the next sound string REPEAT SILENT DROP ; \ mom said always clean up after yourself \ ======================================================== \ create a fifo to feed the sound player HEX \ head tail data \ ---- ---- -------------- CREATE FIFO ( -- addr) 0 , 0 , 20 CELLS ALLOT \ name the address fields for maximum speed FIFO CONSTANT QHEAD FIFO CELL+ CONSTANT QTAIL FIFO 2 CELLS + CONSTANT QDATA \ circular indexer in Forth replaced with machine code version \ : []++ ( fifo head|tail -- addr) DUP @ 2+ 1F AND DUP ROT ! + ; \ CODE []++ ( fifo head|tail -- addr) \ compute next fifo location \ *TOS R0 MOV, \ DUP @ \ R0 INCT, \ 2+ \ R0 1F ANDI, \ 1F AND \ R0 *TOS MOV, \ DUP ROT ! \ R0 TOS MOV, \ \ *SP+ TOS A, \ + \ NEXT, \ ENDCODE \ machine code version of above assembler code CODE []++ ( fifo head|tail -- addr) C014 , 05C0 , 0240 , 001F , C500 , C100 , A136 , NEXT, ENDCODE : Q@ ( fifo -- n) QTAIL []++ @ ; \ bump tail and fetch data : Q! ( n fifo --) QHEAD []++ ! ; \ bump head and add to FIFO \ Background Player program : BGPLAYER ( -- ) \ play all lists in the Q then goto sleep BEGIN FIFO 2@ <> \ fetch,compare head & tail WHILE QDATA Q@ PLAYLIST REPEAT MYSELF SLEEP PAUSE ; \ hand-off to next task \ =============================================== \ CREATE player task INIT-MULTI USIZE MALLOC CONSTANT PLAYER \ get memory for task PLAYER FORK \ init the task ' BGPLAYER PLAYER ASSIGN \ assign something to do \ =============================================== \ end user commands \ Usage: MUNCHMAN >SNDQ PLAYQ : >SNDQ ( list -- ) QDATA Q! ; : PLAYQ ( list -- ) PLAYER RESTART ; : KILLQ ( -- ) QTAIL @ QHEAD ! ; \ =============================================== \ VDP list compiler \ Lets us make sound lists like in assember \ but the compile into VDP RAM : ?BYTE ( n -- ) FF00 AND ABORT" Not a byte" ; : VBYTE ( -- ) BEGIN [CHAR] , PARSE-WORD DUP WHILE EVALUATE DUP ?BYTE VC, REPEAT 2DROP ; : /VEND 0 VC, 0 VC, ; \ compile zeros Edited May 18, 2018 by TheBF 1 Quote Link to comment Share on other sites More sharing options...
+Lee Stewart Posted May 18, 2018 Share Posted May 18, 2018 Brian... I would like to be more involved, but my focus for a few weeks is getting my left knee back to normal function after successful surgery. I am not looking for sympathy—just explaining why I have not been more responsive. You would not, perchance, be coming to the Chicago Faire in November, would you? ...lee Quote Link to comment Share on other sites More sharing options...
+TheBF Posted May 18, 2018 Author Share Posted May 18, 2018 Brian... I would like to be more involved, but my focus for a few weeks is getting my left knee back to normal function after successful surgery. I am not looking for sympathy—just explaining why I have not been more responsive. You would not, perchance, be coming to the Chicago Faire in November, would you? ...lee Hi Lee, No worries. First things first is always a good strategy. I have been feeling off myself with a flu like thing. I have not given Chicago any thought. It would be a significant expense but would be fun I'm sure. I will give it some thought. Brian Quote Link to comment Share on other sites More sharing options...
D-Type Posted May 21, 2018 Share Posted May 21, 2018 (edited) Yes I am the last of the noble group. I am cross-compiling actually. On a "real" PC, the DOSBOX. :-) The cross-compiler is here. https://github.com/bfox9900/CAMEL99/tree/master/Compiler It actually began with the TI-Forth assembler that I modified to assemble into a different memory segment. <<CHOP>> Brian Ah yes...actually I read your release notes etc. perhaps 4 or 5 weeks back, then I went on holiday for a week and forgot everything about it :-) My memory now of what I read is that you compiled (assembled?) the primitives and put that onto the '99 and then loaded the rest of Forth and any extensions using the '99 to compile. When you've got access to disk/flash, I guess that gives you a nice usable system to do that. My CamelForth is running on 6809 and Brad R's "Chromium" cross-compiler is quite complete, so I'm compiling everything on the PC. As the target is the Vectrex with it's vector monitor and joystick, there is no keyboard or character-based display, and <1k of usable RAM, so compiling on the Vectrex is a non-starter without hardware mods. I do have a serial port/terminal added to mine for interactive testing, but a ~700 byte dictionary has some limitations! My aim is to allow creation new cartridge programs/games by other developers, so hardware changes can only be minimal, thus PC is the IDE. There are some great emulators for the Vectrex with build-in debuggers and one has a full IDE for assembly language development, but it still works quite nicely with the output from the Chromium compiler. With some help from the Forth community, I have Camel Forth 6809 Chromium compiler now running on Gforth instead of F83 and with plain text files instead of block files, I saw this as mandatory to get any new users. One day I'll learn how to use Github... I'm new to the TI processors,a quick look shows the 430 to be similar to the '99 processor, but with more capability, so it makes perfect sense to port 430 Camel Forth to the '99. It looks like you've put a lot of effort into your Forth, just like the other two Forths for the TI, they all look excellent. I've really enjoyed reading about them and the '99 in general, especially after learning so much about the machine on the Floppy Days series of '99 podcasts. I owe it to myself to download Classic99 and give'em a go! Edited May 21, 2018 by D-Type Quote Link to comment Share on other sites More sharing options...
+mizapf Posted May 21, 2018 Share Posted May 21, 2018 I'm new to the TI processors,a quick look shows the 430 to be similar to the '99 processor, but with more capability This is what I recently learned about as well, and indeed, there is quite some similarity. People who know TMS9900 should have a look here: https://en.wikipedia.org/wiki/TI_MSP430#MSP430_CPU 1 Quote Link to comment Share on other sites More sharing options...
D-Type Posted May 21, 2018 Share Posted May 21, 2018 (edited) Ah yes...actually I read your release notes etc. perhaps 4 or 5 weeks back, then I went on holiday for a week and forgot everything about it :-) My memory now of what I read is that you compiled (assembled?) the primitives and put that onto the '99 and then loaded the rest of Forth and any extensions using the '99 to compile. When you've got access to disk/flash, I guess that gives you a nice usable system to do that. <<CHOP>> Actually something I forgot to mention, I once owned a TI-99/4A. I can't remember where I got it from, but my eBay auction text from Jan 2002 when I listed it said "This is an auction for a Texas Instruments TI99/4A computer main unit only. I have no power supply or other leads so I do not know if it works." No wonder I can't really remember much about it, except it seemed really nicely made. I'm a bit sad now that I didn't stick it in a box in the garage for later! Edited May 21, 2018 by D-Type 1 Quote Link to comment Share on other sites More sharing options...
+mizapf Posted May 21, 2018 Share Posted May 21, 2018 I recently pondered over throwing away my old analog SLR camera. Better not. It would not have been the first time that I regret throwing away something later. Quote Link to comment Share on other sites More sharing options...
+TheBF Posted May 23, 2018 Author Share Posted May 23, 2018 Ah yes...actually I read your release notes etc. perhaps 4 or 5 weeks back, then I went on holiday for a week and forgot everything about it :-) My memory now of what I read is that you compiled (assembled?) the primitives and put that onto the '99 and then loaded the rest of Forth and any extensions using the '99 to compile. When you've got access to disk/flash, I guess that gives you a nice usable system to do that. My CamelForth is running on 6809 and Brad R's "Chromium" cross-compiler is quite complete, so I'm compiling everything on the PC. As the target is the Vectrex with it's vector monitor and joystick, there is no keyboard or character-based display, and <1k of usable RAM, so compiling on the Vectrex is a non-starter without hardware mods. I do have a serial port/terminal added to mine for interactive testing, but a ~700 byte dictionary has some limitations! My aim is to allow creation new cartridge programs/games by other developers, so hardware changes can only be minimal, thus PC is the IDE. There are some great emulators for the Vectrex with build-in debuggers and one has a full IDE for assembly language development, but it still works quite nicely with the output from the Chromium compiler. With some help from the Forth community, I have Camel Forth 6809 Chromium compiler now running on Gforth instead of F83 and with plain text files instead of block files, I saw this as mandatory to get any new users. One day I'll learn how to use Github... I'm new to the TI processors,a quick look shows the 430 to be similar to the '99 processor, but with more capability, so it makes perfect sense to port 430 Camel Forth to the '99. It looks like you've put a lot of effort into your Forth, just like the other two Forths for the TI, they all look excellent. I've really enjoyed reading about them and the '99 in general, especially after learning so much about the machine on the Floppy Days series of '99 podcasts. I owe it to myself to download Classic99 and give'em a go! For 100% clarity it cross-assembles about 100 primitives into the binary image and then using cross-compiler magic and those primitives you cross-compile the ANS/ISO Forth CORE word set and quite a bit of the extended word set. And the system includes simple file access and the word "INCLUDED". So with : ; and INCLUDED I can load and compile anything else that is needed. I can also use the XFC99 Cross-compiler to create stand alone 99 programs but currently they are limited to 8K in size. I don't know much about github either. I can get files up there but have not yet grokked version control. Not that interested to be honest, but I guess I should try harder. Well done on moving Chromium to gForth. I have dreams of porting my cross-compiler to gForth as well. So much code, so little time... There are things about MSP430 that are nice like a real stack pointer and instructions that execute in 2 or 3 clocks instead 14 to 28. The 9900 has the faster context switch, at least in terms of instructions. But yeah it's a pretty easy conversion for many primitives. B Quote Link to comment Share on other sites More sharing options...
+TheBF Posted May 25, 2018 Author Share Posted May 25, 2018 (edited) Performance Enhancements in ANS Forth I am going to release a pretty near final version of the instruction manual and it includes this chapter that might be interesting to the Forth experimenters here. Performance Enhancements As we have seen Forth operates like everything is a sub-routine. In many languages this would cause a big overhead but Forth was built to minimize sub-routine overhead. One way it does this is by using separate stacks for data and return addresses. Even so there are times when you need every bit of speed you can get. This can mean that calling a routine in a time critical loop makes things too slow. Text Macros The solution to removing some calling overhead is to use a feature that became part of Forth in the ANS Forth 94 Standard. The name of the feature is the “Text Macro” and it combines a literal text string and the word EVALUATE. Consider the following code: INCLUDE DSK1.ELAPSE.F \ elapsed timer utility \ Calculate the address of an array at index ‘n’ : [] ( ndx addr – addr[n]) SWAP CELLS + ; \ store n in array at ndx : []! ( n ndx addr -- ) [] ! ; \ fetch n from array at ndx : []@ ( ndx addr – n) [] @ ; DECIMAL 2000 CONSTANT SIZE CREATE Q SIZE CELLS ALLOT \ make some space called Q : FILL-Q SIZE 0 DO I I Q []! LOOP ; : SEE-Q SIZE 0 DO I Q []@ . LOOP ; : EMPTY-Q SIZE 0 DO I Q [] OFF LOOP ; This code will work as expected and you can fill the array, see-the-array and empty the array. If we run this we get the following results: (Example 1 jpeg) Now let’s replace the array index calculator with a TEXT Macro like this: : [] S” SWAP CELLS +” EVALUATE ; IMMEDIATE What’s the difference? When word [] runs now it will take the text string and interpret it. That’s normal, but now consider what happens when we do this: \ store n in array at ndx : []! ( n ndx addr -- ) [] ! ; Notice that now [] is an IMMEDIATE word. This means that the text string will be interpreted while we are compiling the definition of []! The effect of this we be that the compiler will compile SWAP CELLS + as separate words into the routine []!. This therefore means that there will no longer be a sub-routine call to [], but rather a call to each of the separate words. In other words we have compiled the definition of [] “inline”. Let’s see what it does to the speed of our loops. (Text macros jpeg) We get a 15% speed improvement in the first case and a 21% improvement in the second case. The downside of using text macros is that every time we use [] we consume 3 CELLS ( 6 bytes) of space rather than only 1 CELL with a colon definition. So use TEXT macros wisely and you can get some improvements in your code speed with very little effort. INLINE CODE WORDS CAMEL99 Forth is implemented as something called an "indirect threaded code". This is a very clever way to make a language that fits a lot of stuff in a small space. The secret to this code density is that every routine is identified by just one address. The secret to making it run quickly is creating a little routine to read those addresses and do the code associated with them as fast as possible. On the TMS9900 that little routine is 3 instructions of assembly language so it's pretty fast but there is a price penalty with threaded code that can range from 2.5 times to as much as ten times in the worst cases. It turns out that ITC Forth spends about 50% of the time running those three instructions that read the address which sometimes is called the [1]inner interpreter and is given the name NEXT. At the bottom of every Forth system are a pile of little routines written in assembler (CODE words) that do all the real work. They are simple things that read the contents of a memory address or add two numbers together. The code is all there and it is normally just called by the inner interpreter. Each routine ends with a call back to the inner interpreter. Wouldn't it be handy if we could use all that code the way a conventional compiler does and copy the routines into memory all in row and eliminate the inner interpreter between each CODE word? CAMEL99 has INLINE[ ] to do just that. It reads a string of Forth words that are assembly language words and strings the code together as one long routine. This is not as space efficient, but it can as much as 2.4 times faster! Below is an example of how we could use CODE words to improve the speed of our previous example but not write one instruction of Assembly language. Notice we have only changed the array indexing words to CODE words. The Magic is done with INLINE[ ] which compiles each code word but strips off the call to NEXT and lets each CODE word run one after the other. This is ONLY possible with system CODE words. If you attempt to INLINE[] Forth words the compiler will ABORT with an error message. INCLUDE DSK1.ELAPSE.F \ elapsed timer utility CODE [] ( i addr -- addr’) INLINE[ SWAP 2* + ] NEXT, ENDCODE CODE []! ( n i addr -- ) INLINE[ SWAP 2* + ! ] NEXT, ENDCODE CODE []@ ( i addr – n) INLINE[ SWAP 2* + @ ] NEXT, ENDCODE DECIMAL 2000 CONSTANT SIZE CREATE Q SIZE CELLS ALLOT \ make some space called Q : FILL-Q SIZE 0 DO I I Q []! LOOP ; : SEE-Q SIZE 0 DO I Q []@ . LOOP ; : EMPTY-Q SIZE 0 DO I Q [] OFF LOOP ; Now let’s see how our new programs perform. ( Inline jpeg) Performance with INLINE[] CODE Words As we can see our little programs now run 40% faster because the core routines inside the loop are much faster. But look at the last result. This uses the assembly language word called FILL. This line is doing the same thing as EMPTY-Q but using every trick available for the TMS9900 CPU. It is three times faster than using the DO/LOOP structure. So the lesson is that we can do many things to increase the speed of Forth and normally that’s fast enough. However when maximum speed is required Assembler is still fastest. [1] This is NOT the "outer" text interpreter that we communicate with from keyboard, but an internal routine that reads addresses and runs machine code Edited May 25, 2018 by TheBF 2 Quote Link to comment Share on other sites More sharing options...
D-Type Posted May 25, 2018 Share Posted May 25, 2018 At some point in my Vectrex 6809 Camel Forth adventure, I'm going to need these optimisations, thanks for posting. Originally, Vectrex carts were 4k, later ones were 8k. More recent homebrew carts have been up to 64k via simple 32k bank switching. Memory size isn't a problem any more, but program speed always is. The Vectrex runs at 1.5MHz and so to get a flicker-free 50Hz screen update you need to get each update done in 30,000 cycles. There are no frame buffers or separate vector generator hardware available, it's all got to be done in real time by the 6809! Quote Link to comment Share on other sites More sharing options...
+TheBF Posted May 26, 2018 Author Share Posted May 26, 2018 At some point in my Vectrex 6809 Camel Forth adventure, I'm going to need these optimisations, thanks for posting. Originally, Vectrex carts were 4k, later ones were 8k. More recent homebrew carts have been up to 64k via simple 32k bank switching. Memory size isn't a problem any more, but program speed always is. The Vectrex runs at 1.5MHz and so to get a flicker-free 50Hz screen update you need to get each update done in 30,000 cycles. There are no frame buffers or separate vector generator hardware available, it's all got to be done in real time by the 6809! Glad you found it helpful. The one thing you have going for you with 6809 I believe is that the 6809 does in-direct threading in 1 instruction. ( if memory serves me right) Contrast that to TMS9900 which takes 3 instructions and each instruction is sucking up 20 or so cycles. Some other things I have noticed in Camel Forth. There are quite a few stack operations code in Forth and many times they can be made much faster as CODE words. The word HERE is defined as DP @ and this can make the system snappier if written as a CODE word. The 99 really shows up slow code and dictionary searches were really slow. The test for that is type the numbers 1 to 10 and hit return. This causes 10 searches through the entire dictionary. Stock Camel Forth took about 3.5 seconds. I had to re-write FIND as a code primitive called (FIND) and then the final definition is : FIND LATEST @ (FIND) ; The same test now takes less than 1 second. These changes may be very 9900 specific however, so testing is essential. B Quote Link to comment Share on other sites More sharing options...
+Lee Stewart Posted May 26, 2018 Share Posted May 26, 2018 The 99 really shows up slow code and dictionary searches were really slow. The test for that is type the numbers 1 to 10 and hit return. This causes 10 searches through the entire dictionary. Stock Camel Forth took about 3.5 seconds. I had to re-write FIND as a code primitive called (FIND) and then the final definition is : FIND LATEST @ (FIND) ; The same test now takes less than 1 second. These changes may be very 9900 specific however, so testing is essential. B In fbForth 2.0, typing 0 1 2 3 4 5 6 7 8 9 happens in 0.67 s, while typing 4 5 6 7 8 9 8 7 6 5 takes 0.88 s. The reason for the difference is that 0 1 2 3 are defined as constants and are found in the dictionary, so are faster than number conversion. I am a little surprised that Camel99 Forth is so much slower because the dictionary search for words not found in fbForth 2.0 is 505 words! INTERPRET uses -FIND : : -FIND ( --- false | pfa len true ) ( IS:string ) BL WORD \ get next word in input stream to HERE HERE CONTEXT @ @ (FIND) \ search CONTEXT vocabulary for the word DUP 0= \ did we find it? IF \ no DROP HERE LATEST (FIND) \ search CURRENT vocabulary THEN ; (FIND) is indeed a code primitive. Because of the way I wrote the ROM code for fbForth 2.0, the code for (FIND) in the spoiler may not be terribly helpful, but here it is: _PFIND MOV *SP,R1 ; nfa to R1 JEQ PFIND4 ; top of dictionary? PFIND1 MOV R1,R0 ; no; copy nfa to R0 MOV @2(SP),R3 ; str addr ptr to R3 MOVB *R0+,W ; copy nfa count byte to W; inc R0 to 1st char ANDI W,>3F00 ; mask out non-count bits, but not smudge bit CB W,*R3+ ; compare char counts; inc R3 to 1st str char JNE PFIND3 ; counts the same? PFIND2 MOVB *R0+,W ; yes; next nfa char to W, incrementing nfa JLT PFIND5 ; char with terminator bit? CB W,*R3+ ; no; compare chars, incrementing str JEQ PFIND2 ; if =, compare next chars PFIND3 MOV @-2(R1),R1 ; not =; counts different; get prev word's nfa JNE PFIND1 ; if not top of dictionary, try again PFIND4 INCT SP ; no match; top of dictionary; pop top of stack CLR *SP ; return false on top of stack JMP PFINDX ; return to inner interpreter PFIND5 ANDI W,>7F00 ; mask out terminator bit from nfa char CB W,*R3 ; last nfa char same as last str char? * ...should match space at end of word with even count JNE PFIND3 ; no; head out INCT R0 ; yes; increment R0 to pfa or pfa ptr+2 * Change to accommodate headers not in bank 0 (pfa ptr in R0) BL @GETPFA ; check for ptr in ROM PFIND6 MOV R0,@2(SP) ; leave pfa on stack at same position as addr CLR *SP ; clear top stack position MOVB *R1,@1(SP) ; copy length byte to right byte of top of stack DECT SP ; make room on stack for 3rd cell SETO *SP ; put -1 on stack NEG *SP ; make it 1 (true) PFINDX B @RTNEXT ; return to inner interpreter ...lee Quote Link to comment Share on other sites More sharing options...
+TheBF Posted May 26, 2018 Author Share Posted May 26, 2018 (edited) In fbForth 2.0, typing 0 1 2 3 4 5 6 7 8 9 happens in 0.67 s, while typing 4 5 6 7 8 9 8 7 6 5 takes 0.88 s. The reason for the difference is that 0 1 2 3 are defined as constants and are found in the dictionary, so are faster than number conversion. I am a little surprised that Camel99 Forth is so much slower because the dictionary search for words not found in fbForth 2.0 is 505 words! INTERPRET uses -FIND : : -FIND ( --- false | pfa len true ) ( IS:string ) BL WORD \ get next word in input stream to HERE HERE CONTEXT @ @ (FIND) \ search CONTEXT vocabulary for the word DUP 0= \ did we find it? IF \ no DROP HERE LATEST (FIND) \ search CURRENT vocabulary THEN ; (FIND) is indeed a code primitive. Because of the way I wrote the ROM code for fbForth 2.0, the code for (FIND) in the spoiler may not be terribly helpful, but here it is: _PFIND MOV *SP,R1 ; nfa to R1 JEQ PFIND4 ; top of dictionary? PFIND1 MOV R1,R0 ; no; copy nfa to R0 MOV @2(SP),R3 ; str addr ptr to R3 MOVB *R0+,W ; copy nfa count byte to W; inc R0 to 1st char ANDI W,>3F00 ; mask out non-count bits, but not smudge bit CB W,*R3+ ; compare char counts; inc R3 to 1st str char JNE PFIND3 ; counts the same? PFIND2 MOVB *R0+,W ; yes; next nfa char to W, incrementing nfa JLT PFIND5 ; char with terminator bit? CB W,*R3+ ; no; compare chars, incrementing str JEQ PFIND2 ; if =, compare next chars PFIND3 MOV @-2(R1),R1 ; not =; counts different; get prev word's nfa JNE PFIND1 ; if not top of dictionary, try again PFIND4 INCT SP ; no match; top of dictionary; pop top of stack CLR *SP ; return false on top of stack JMP PFINDX ; return to inner interpreter PFIND5 ANDI W,>7F00 ; mask out terminator bit from nfa char CB W,*R3 ; last nfa char same as last str char? * ...should match space at end of word with even count JNE PFIND3 ; no; head out INCT R0 ; yes; increment R0 to pfa or pfa ptr+2 * Change to accommodate headers not in bank 0 (pfa ptr in R0) BL @GETPFA ; check for ptr in ROM PFIND6 MOV R0,@2(SP) ; leave pfa on stack at same position as addr CLR *SP ; clear top stack position MOVB *R1,@1(SP) ; copy length byte to right byte of top of stack DECT SP ; make room on stack for 3rd cell SETO *SP ; put -1 on stack NEG *SP ; make it 1 (true) PFINDX B @RTNEXT ; return to inner interpreter ...lee Brad wrote Camel Forth to use more Forth definitions and less CODE words so it would be easier to port to new machines. The poor old 9900 really struggles with that approach. Here is the original FIND word . N= is string comparison CODE word. Running the search loop in Forth is much slower than doing the whole thing in code as you have and CAMEL99 now has. : FIND ( c-addr -- c-addr 0 if not found ) \ xt 1 if immediate \ xt -1 if "normal" LATEST @ \ -- adr BEGIN \ -- adr nfa 2DUP OVER C@ CHAR+ \ -- adr nfa adr nfa n+1 N= \ -- adr nfa f DUP IF DROP NFA>LFA @ DUP \ -- adr link link THEN 0= UNTIL \ -- adr nfa OR adr 0 DUP IF NIP DUP NFA>CFA \ -- nfa xt SWAP IMMED? \ -- xt iflag 0= 1 OR \ -- xt 1/-1 THEN ; Here is my new (FIND) word. I made use of the large register set to feed the machine more efficiently. \ Register Usage \ Inputs: R3 = traverses NFAs in the Forth dictionary \ R8 = address of the counted string we are looking for \ R5 = length of the counted string in R8 + 1 \ string compare loop \ R0 = number of characters to compare(search string length+1) \ R1 = address of the 1st string to compare \ R2 = address of the second string to compare \ Outputs: R2 = address of found string -OR- address of search string \ R4 = Forth TOS register. Holds the true/false result flag CODE: (FIND) ( Caddr NFA -- XT ? ) TOS R3 MOV, \ R3 = NFA which is a counted string *SP R8 MOV, \ R8 = caddr which is a counted string TOS CLR, \ TOS is the output flag, init to zero \ get the length byte of Caddr *R8 R5 MOVB, \ caddr C@ -> R5 R5 8 SRL, \ get the byte on the correct side right @@7 JEQ, \ if count=0 jump back to Forth R5 INC, \ 1+ to account for length byte \ OUTER loop \ load char compare registers @@1: R5 R0 MOV, \ load R0 with length of caddr string R8 R1 MOV, \ load R1 with caddr string address R3 R2 MOV, \ load R2 with the NFA to compare \ inner character comparator loop @@3: *R1+ *R2+ CMPB, \ compare char by char including the length byte @@5 JNE, \ ANY mismatch found, goto @@5 R0 DEC, \ decr. loop counter @@3 JNE, \ loop while R0 > 0 @@6 JMP, \ WE FOUND IT!! exit the loop \ traverse link list to next NFA @@5: R3 -3 ADDI, \ convert nfa>lfa *R3 R3 MOV, \ do a fetch, R3 now has new NFA @@1 JNE, \ if <> 0 let's try the next word in the dictionary! NEXT, \ we got zero. End of the list! Go back to Forth \ end Outer loop \ convert NFA in R3 to CFA -> R2 @@6: R3 R2 MOV, \ if found R3 has a name field address (NFA), copy ro R2 *R3 R0 MOVB, \ get the length of the name to R0 R0 SWPB, \ fix the #$%!@$ byte order again R0 R2 ADD, \ add length to R2, gets past the string to the CFA R2 INCT, \ inc 1 for the count byte and 1 more for even address evaluation R2 -2 ANDI, \ align R2 to even address boundary \ test for immediate or normal word -> TOS TOS SETO, \ we found a word so set TOS to true R3 DEC, \ R3 still has the NFA. NFA-1 is the immediate field *R3 R0 MOVB, \ read contents of the immediate field @@9 JEQ, \ IF it's zero we're done TOS NEG, \ else if non zero negate the TOS from -1 to 1 \ and head for home \ bug in my forward jumps, needs multiple labels (assembler *TODO list) @@7: @@9: R2 *SP MOV, \ replace Caddr with the found XT in R2 NEXT, \ Return to Forth END-CODE \ 42 BYTES Edited May 26, 2018 by TheBF Quote Link to comment Share on other sites More sharing options...
D-Type Posted May 27, 2018 Share Posted May 27, 2018 FIND is only used when compiling, correct? If yes, I guess using a cross compiler on a PC almost exclusively, which compiles my 5k binary in a couple of seconds, means this doesn't really make a lot of difference to me. I'm still interested, though, to try the 1 2 3...9 10 Enter test the see how fast the Vectrex 6809 does it. When I'm back from holiday... Quote Link to comment Share on other sites More sharing options...
+TheBF Posted May 27, 2018 Author Share Posted May 27, 2018 FIND is only used when compiling, correct? If yes, I guess using a cross compiler on a PC almost exclusively, which compiles my 5k binary in a couple of seconds, means this doesn't really make a lot of difference to me. I'm still interested, though, to try the 1 2 3...9 10 Enter test the see how fast the Vectrex 6809 does it. When I'm back from holiday... It is used for compiling and can also be invoked by the word tick. ( ' interpreting mode version AND ['] compiling mode version ) So yes, using the cross-compiler means you are running the cross-compiler's FIND in the PC. But if you ever send text source code to the vectrex the speed of the downloads will be limited by FIND's lookup speed. Quote Link to comment Share on other sites More sharing options...
+Lee Stewart Posted May 27, 2018 Share Posted May 27, 2018 It is used for compiling and can also be invoked by the word tick. ( ' interpreting mode version AND ['] compiling mode version ) I want to be careful not to muddy the waters too much for readers, but I wish to point out that, in TI Forth and fbForth (both fig-Forth based), ' (tick) works in both interpreting and compiling modes because it is state smart. As of the Forth-83 standard, there began a move to minimize the use of state-smart words. Consequently, tick’s state-smart capacity was removed in favor of reducing ' to interpreting mode only and creating ['] strictly for compiling mode. And, now, back to your regularly scheduled program... ...lee 1 Quote Link to comment Share on other sites More sharing options...
+TheBF Posted May 27, 2018 Author Share Posted May 27, 2018 (edited) Indexed addressing mode in Forth Arrays After playing with arrays in Forth I realized the 9900 was designed to do this in the instruction set. Indexed addressing is the way to do it so here is one way to use it in Forth Arrays. It is not as general purpose because the base address of the array must be embedded in the assembler code. This means that you must write special words for each type of access but as you can see the performance is double the Forth only solutions shown earlier. Edit: removed some cut and paste mistakes \ fastest array access using indexed address mode INCLUDE DSK1.ASM9900.F INCLUDE DSK1.ELAPSE.F \ elapsed timer utility DECIMAL 2000 CONSTANT SIZE CREATE Q SIZE CELLS ALLOT \ make some space called Q \ fetch contents of any cell in ARRAY CODE ]Q@ ( i -- array[i]@) TOS 1 SLA, \ shift R1 1 bit left (mult. By 2) Q (TOS) TOS MOV, \ fetch contents of Q (TOS) to TOS NEXT, ENDCODE \ store 'n' on stack to any cell in ARRAY CODE ]Q! ( n index --) TOS 1 SLA, \ shift TOS 1 bit left (mult. By 2) *SP+ Q (TOS) MOV, \ POP 2nd stack item into address Q (TOS) TOS POP, \ refill TOS register NEXT, ENDCODE \ clear any cell in ARRAY CODE ]Q0! ( index --) TOS 1 SLA, \ shift TOS 1 bit left (mult. By 2) Q (TOS) CLR, \ Clear Q (TOS) TOS POP, \ refill TOS register NEXT, ENDCODE : FILL-Q SIZE 0 DO I I ]Q! LOOP ; : SEE-Q SIZE 0 DO I ]Q@ . LOOP ; : EMPTY-Q SIZE 0 DO I ]Q0! LOOP ; Edited May 28, 2018 by TheBF Quote Link to comment Share on other sites More sharing options...
+TheBF Posted June 1, 2018 Author Share Posted June 1, 2018 (edited) Adding LINPUT to CAMEL99 Forth As I write my instruction manual designed to assist the BASIC programmer who wants to try CAMEL99 Forth I got to the ANS Forth 94 file words. I know that new programmers sometimes struggle with using TI-BASIC's numerous file options so when I started explaining READ-FILE which takes 3 input parameters and returns 3 output parameters I wondered "What would it take to add LINPUT to the system?" Turns out not too much and I think it will be a nicer interface for a BASIC programmer to use. Here is the code that creates LINPUT to read a file record into a counted string. : LINPUT ( $ handle -- ) >R \ push handle onto return stack DUP CHAR+ ( -- $ $+1 ) \ skip past the string's length byte R> SELECT \ pop handle, select PAB for this file 2 FILEOP ?FILERR \ read operation, test error# [PAB FBUFF] V@ SWAP [PAB RECLEN] VC@ VREAD \ VDP data -> $ [PAB CHARS] VC@ SWAP C! \ update string length ; And a little DEMO file that shows how to use LINPUT went into the manual as well. Turns out LINPUT is a little faster than READ-FILE because it does not try to do so much. \ Print the contents of a DV80 file INCLUDE DSK1.ANSFILES.F \ ANS Forth file words INCLUDE DSK1.LINPUT.F DECIMAL VARIABLE #1 \ this variable will hold the file handle VARIABLE A$ 80 ALLOT \ variable with 80 bytes of space : PRINT$ ( $ -- ) CR COUNT TYPE ; : SEEFILE ( addr len -- ) \ Usage: S" DSK1.MYFILE" SEEFILE DISPLAY 80 VARI SEQUENTIAL R/O OPEN-FILE ?FILERR #1 ! BEGIN A$ #1 @ LINPUT A$ PRINT$ EOF UNTIL #1 @ CLOSE-FILE ?FILERR ; Edited June 1, 2018 by TheBF 2 Quote Link to comment Share on other sites More sharing options...
+TheBF Posted June 2, 2018 Author Share Posted June 2, 2018 (edited) I gotta stop playing with this thing! Sound List Assembler in Forth Now that I have a sound list player I wanted a better way to create sound lists. The Forth way of doing this is to create some new words that do the job, a meta-language as it is sometimes called. I had created a set of words to create sound bytes called HZ DB and NOISE to control the 9919 chip. There are also output selector words called GEN1, GEN2, GEN3, GEN4 I already had a VDP memory manager in the library so I made some different versions of HZ, DB, and NOISE, that put the bytes into VDP RAM sequentially rather than sending them to the chip. I created a way to count the VDP bytes that were used as they went into memory and fill in a count byte at the beginning of the string as required by the sound list format. These are called [[ and ]] Timing is put into the list by taking a millisecond value and dividing by 16 and compiling that number at the end of the string. The word is called MS, Then all we need is a way to give sound lists a name in the Forth dictionary and what you get is a simple way to make a sound list. For an example I took the PARSEC Explode sound list and translated it to SOUND assembler code. Here is the way it looked before, where I made some Forth words to emulate Assembler byte data, but for VDP RAM. HEX VCREATE EXPLODE VBYTE 7,9F,BF,DF,E7,F0,C0,07,5 VBYTE 1,F1,6 VBYTE 1,F2,7 VBYTE 1,F3,8 VBYTE 1,F4,9 VBYTE 1,F5,10 VBYTE 1,F6,11 VBYTE 1,F7,12 VBYTE 1,F8,13 VBYTE 1,F9,14 VBYTE 1,FA,15 VBYTE 1,FB,16 VBYTE 1,FC,17 VBYTE 1,FD,18 VBYTE 1,FE,30 VBYTE 1,FF,0 /VEND And here is Sound Assembler that generates the same data DECIMAL SOUND: EXPLODE2 \ GEN3 controls Noise Generator Frequency [[ GEN1 MUTE, GEN2 MUTE, GEN3 MUTE, 7 NOISE, 0 DB, GEN3 999 HZ, ]] 80 MS, \ Parsec used "7". Sounds same as 999 Hz. ?? GEN4 \ control noise generator volume [[ -2 DB, ]] 96 MS, [[ -4 DB, ]] 112 MS, [[ -6 DB, ]] 128 MS, [[ -8 DB, ]] 144 MS, [[ -10 DB, ]] 256 MS, [[ -12 DB, ]] 272 MS, [[ -14 DB, ]] 288 MS, [[ -16 DB, ]] 304 MS, [[ -18 DB, ]] 320 MS, [[ -20 DB, ]] 336 MS, [[ -22 DB, ]] 352 MS, [[ -24 DB, ]] 368 MS, [[ -26 DB, ]] 384 MS, [[ -28 DB, ]] 768 MS, [[ -30 DB, ]] 0 MS, ;SOUND I kind of like sound lists now. B For those who might be curious here is the code. I have to update GITHUB. All of this is not there yet. \ TI sound list assembler. \ Assembles TI sound lists in VDP RAM that are \ compatible with VDP Background sound player \ (VDPBGSND.F) INCLUDE DSK1.TOOLS.F INCLUDE DSK1.VDPBGSND.F INCLUDE DSK1.SOUND.F \ sound byte "assembler" commands compile values for the \ currently selected generator. (GEN1 GEN2 GEN3 GEN4) DECIMAL : HZ, ( f -- ) (HZ) SPLIT VC, VC, ; : DB, ( level -- ) (DB) VC, ; : MUTE, ( -- ) -30 DB, ; : MS, ( n -- ) 4 RSHIFT VC, ; \ ms/16 = 1/60 \ turn all sound off HEX : SILENT, ( -- ) 9F VC, BF VC, DF VC, FF VC, ; \ noise channel selects generator 4 by default : NOISE, ( n -- ) 0F AND GEN4 OSC @ OR VC, ; DECIMAL : SOUND: ( <text> -- ) VCREATE !CSP ; \ stack starting Vaddress & make space for string length : [[ ( -- vaddr) VHERE 0 VC, ; \ back-fill string length into vaddr : ]] ( vaddr -- ) VHERE OVER - 1- SWAP VC! ; \ mark end of sound list, check for clean stack : ;SOUND ( -- ) /VEND ?CSP ; Edited June 2, 2018 by TheBF 1 Quote Link to comment Share on other sites More sharing options...
+Lee Stewart Posted June 2, 2018 Share Posted June 2, 2018 You are moving right along! I see that I need to explore how I might similarly compose sound lists/tables in fbForth 2.0. I am still in slow motion with physical therapy and rest taking up far more time than I would like. ...lee Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.