Jump to content

Adventures in File IO and Commander X16


Recommended Posts

I have to say that working with the filesystem is one of the most challenging aspects of the Commander X16 that I've personally run across. There are tons of permutations of "Host FS vs SD image", emulator/ROM version, bugs, compiler/system library behavior, etc.

One thing that's been updated since R39 is that the hyper load/save hooks have changed a bit, if my understanding is correct - and now instead of just intercepting LOAD and SAVE, the emulator(s) now intercept calls to MACPTR and ACPTR (or some other similar choke point) in order to allow more operations to work with the host FS.

It appears that whatever the current methodology is, it doesn't catch everything, because I've recently started working with sequential access to files instead of just bulk loading them into (V)RAM/HIRAM. I've found that this doesn't work unless you're using an SD image. I find SD images to be very inconvenient to work with but hey, I guess that's just the nature of working with something like the X16 in pre-release state (life on the frontier - there's no Dominos and no running water).

One thing I've run across is cc65's cbm.h library, which provides what it calls "BASIC-like" interfaces to the low-level kernal file handling routines:

  • cbm_open(LFN, DEVICE, SA, filename)
  • cbm_close(LFN)
  • cbm_read(LFN, buffer* , num_bytes)
  • cbm_write(LFN, buffer*, num_bytes)

Where LFN = logical file number, DEVICE = 8 (for disk drive), and SA = that bizarre, multi-purpose byte that is essentially an "argument" for various low-level routines like LOAD or OPEN....

So here we are in cc65 space. The include file also provides three defines to use for the SA in cbm_open():

Quote

/* Constants to use with cbm_open() for openning a file for reading or
** writing without the need to append ",r" or ",w" to the filename.
**
** e.g., cbm_open(2, 8, CBM_READ, "0:data,s");
*/
#define CBM_READ        0       /* default is ",p" */
#define CBM_WRITE       1       /* ditto */
#define CBM_SEQ         2       /* default is ",r" -- or ",s" when writing */

 

This leads me to believe that cbm_open() might be doing a little bit of spying on the SA value before calling the underlying Kernal routines.

In BASIC, you can use any combo of LFN and SA as long as LFN >= 2 as 0 and 1 have special meaning. I'm going to dig a little deeper into the cc65 sources to see if it's intercepting and interpreting the SA and LFN or not.

But the interesting thing I've discovered is that in order for sequential access to work properly, you must use the same value for LFN and SA - and furthermore, these must be 2, 6, or 10 in order for file seeking to work. (I notice that these result in bit 1 of the SA/LFN value being set - hmm)

Oh, and seeking - stdio.h implements fopen() fseek() and fclose() - but fseek() won't build on X16 as there's no underlying code for it in cx16.lib - and there's no cbm_seek() function because CBM machines don't naturally support seek (well, without add-ons like SD2IEC or DOS extenders) so the library doesn't do it.

To do a seek, I wrote my own function with some advice from @TomXP411 - if you open the command channel to device 8 and send a "P" command (position) while the file is open, Commander-DOS will do a seek.
The P command epects binary values to follow it, so here's how to do it in cc65:

int8_t cx16_fseek(uint8_t channel, uint32_t offset) {
  #define SETNAM 0xFFBD
  static struct cmd {
    char p;
    uint8_t lfn;
    uint32_t offset;
  } cmd;

  // open command channel to DOS and send P command.
  // P u8 u32 (no spaces) u8 is LFN to seek(), and u32 = offset.
  cmd.p='p';
  cmd.lfn=channel;
  cmd.offset=offset;
  // can't call cbm_open because the P command is binary and may
  // contain zeros, which C would interpret as null terminator.
  //
  // Roll-your-own inline asm call to SETNAM:
  __asm__ ("lda #6");
  __asm__ ("ldx #<%v",cmd);
  __asm__ ("ldy #>%v",cmd);
  __asm__ ("jsr %w",SETNAM);
  cbm_k_setlfs(15,8,15);
  cbm_k_open(); // this sends the CMD bytes..
  cbm_k_close(15); // close the command channel
  return 0;
  // TODO: ERROR HANDLING!!!!!
}

Note that this code uses cbm_k_open() and not cbm_open() - the k version is a direct Kernal call, so it's not monkeying around with anything like cbm_open might be.

 

Anyway, if anyone has any tips, insights, or whatever regarding file IO, I thought that would be a useful topic for people to be able to hit upon.

  • Thanks 2
Link to comment
Share on other sites

Thank you!

I've shied away from sequential files so far because of the not-knowing.  Your function will be helpful when I finally have the time and inclination to wrap these sorts of functions myself.

 

BUT ALSO... even after the X16 is released, I will still be writing cc65 code on my Mac.  This means SD card access will STILL be inconvenient.  Which makes me think....

 

Edited by rje
Link to comment
Share on other sites

  • Super Administrators

This is also a good lesson in dealing with the quirks of C programming. This custom code was only necessary because of C's use of null-terminated strings and the fact that the Position command uses zeros as part of its data. ZeroByte is getting around this problem by forcing the data length to 6 (lda #6) and then pointing to a binary version of his Position parameters. 

Once this has been thoroughly tested and has something in there to trap any errors, this definitely needs to end up as part of the CX16 library for both cc65 and KickC. 

 

Link to comment
Share on other sites

The saga continues:

I fully expected this result, but let it be known that cbm_read() has no concept of banked RAM at this time.
If you perform a read that runs off the end of a bank, then it will happily throw your data at the ROM region after running off the end of the bank window, and I presume then wrap back around to 0000 after passing FFFF 🙂

Fortunately, my current experiment is going to involve streaming music from a data file on the fly (chip music, not PCM - no WAY to keep up with that!!!) and so it's not going to be an issue for me down the road with this particular program, but I thought folks should be made aware that while LOAD works in BASIC, and Kernal's LOAD routine works for HiRam, cbm_read() just runs off the cliff. (Goofy yell: Yaaaa-hoo-hoo-hoooey!)

One of the items I'm going to experiment with is a cx16_macptr() frontend routine similar to the cx16_seek() routine above. Kernal MACPTR will be bank safe, and run much faster than cbm_read() {which doesn't know MACPTR exists - it's a CX16 exclusive!

For those who don't know, MACPTR is the Kernal routine to perform a block read from disk as opposed to a byte-by-byte read using ACPTR.

Also for those who don't know: VLOAD only uses ACPTR, which is hella-slow compared to MACPTR, so if your VLOADs are taking too much time, try LOAD-ing them into HiRam and then blitting them into VRAM yourself. You'll do that before VLOAD gets to the halfway marker with any "large" amounts of data.

 

Link to comment
Share on other sites

This is my sequential file read routine ...

/**
 * @brief Load a file to banked ram located between address 0xA000 and 0xBFFF incrementing the banks.
 *
 * @param channel Input channel.
 * @param device Input device.
 * @param secondary Secondary channel.
 * @param filename Name of the file to be loaded.
 * @param bank The bank in banked ram to where the data of the file needs to be loaded.
 * @param sptr The pointer between 0xA000 and 0xBFFF in banked ram.
 * @return bram_ptr_t
 *  - 0x0000: Something is wrong! Kernal Error Code (https://commodore.ca/manuals/pdfs/commodore_error_messages.pdf)
 *  - other: OK! The last pointer between 0xA000 and 0xBFFF is returned. Note that the last pointer is indicating the first free byte.
 */
unsigned int load_file_bram(char channel, char device, char secondary, bram_bank_t dbank, bram_ptr_t dptr, size_t size) 
{
    #ifdef __FILE
        printf("load file, c=%u, d=%u, s=%u, b=%x, p=%p, si=%u", channel, device, secondary, dbank, dptr, size);
    #endif

    byte status = 0;
    char ch = 0;
    unsigned int read = 0;

    byte bank_old = bank_get_bram();
    bank_set_bram(dbank);

    status = cbm_k_chkin(channel);
    status = cbm_k_readst();
    if(status) return 0;

    byte* ptr = dptr;

    // gotoxy(0,10);
    // printf("load: ptr = %p, bank = %x", ptr, bank);
    while ((size && read < size) || !size) {

        ch = cbm_k_chrin();

        // OK, character read and not end of input
        read++;

        // check if pointer is between bram boundaries
        if(ptr == 0xC000) {
            dbank++;
            // printf(", %x", (word)bank);
            bank_set_bram(dbank); // select the bank
            ptr = (byte*)0xA000;
        }

        // put the read character into the pointer location
        *ptr = ch;

        // increase the pointer
        ptr++;

        status = cbm_k_readst();
        if(status) break;
    }

    #ifdef __FILE
        printf(", r=%u, status=%u\n", read, status);
    #endif

    bank_set_bram(bank_old);

    #ifdef __FILE
        // cbm_k_chkin(0);
        // while(!getin());
    #endif

    return read;
}

Note that the routine and the description still requires some love from my side ... it's just a version at the moment to get my files loaded into bram in a sequential manner, in chunks as I use a bram heap manager that allocates blocks of memory for me. 

Edited by svenvandevelde
Link to comment
Share on other sites

I just had some initial success using MACPTR in cc65. It’s TONS faster and already bank-aware. I’m going to play with it some more but I think it could be even faster if it really will blit in 512 bytes per pass.

CHRIN has tons of overhead per byte.

will post example code later.

  • Like 1
Link to comment
Share on other sites

No example code tonight, as it's late and I've just hit a good stopping point.

So here is a screenshot of my Wolf3d music demo program, but running in a test mode playing with SEQ access. I post it here because the debug output gives a very good view of how MACPTR does its thing, and the screen includes a ticks counter benchmark to show load speeds. It loads 8K in about 12 frames' time (and would be faster if not having to stop and write all of this debug output to the screen on every pass) 😉

image.thumb.png.97b124e1cfd8a839e20d57e63806d6cc.png

Link to comment
Share on other sites

As promised, here's the code that I'm using to harness MACPTR. It's incomplete in that it doesn't fall back on ACPTR (cbm_load() in this case)  if MACPTR returns -1. I'm not happy with the return type being signed, and will probably fix this by either making it a signed log, or else just returning zero on failure and requiring that the calling code check status byte to know if it's zero because EOF or because of unsupported device.

Most likely, though, this is a case that will never happen with files, as the OS supports block reads from disk - but if you pass it an LFN that's connected to the screen or keyboard or whatever, then that would fail....

extern int __fastcall__ macptr(unsigned char numbytes, void* buffer);

int cx16_read(unsigned char lfn, void* buffer, unsigned int size) {
  int error = 0;
  char* buf = (char*)buffer;
  static unsigned int bytesread;
  static int tmp;

  /* if we can't change to the inputchannel #lfn then return an error */
  if (_oserror = cbm_k_chkin(lfn)) return -1;

  bytesread = 0;
  printf("\n");
  while (size > 0 && !cbm_k_readst()) {
    if (size>=512)
      tmp = macptr(0,buf);  // let MACPTR read as much as it wants
    else if (size>=256)
      tmp = macptr(255,buf); // If size 256..512, unlimited MACPTR would read past desired stopping point.
    else
      tmp = macptr((size), buf);
    if (tmp == -1) return -1;
    bytesread += tmp;
    size -= tmp;
    // wrap the buffer pointer back into the bank window
    // if it advances above 0xbfff. Note that MACPTR did
    // wrap banks correctly, but our calculation must match.
    // also note that MACPTR incremented the active bank,
    // so there is no need to do RAM_BANK++ here.
    if (buf >= (char*)0xc000) buf -= 0x2000;
    buf += tmp;
    if (cbm_k_readst() & 0xBF) break;
    if (tmp == 0) break;
  }

And the assembly is short:

MACPTR := $FF44

.import popa
.export _macptr ; the _ is important - C symbols all start with _

.proc _macptr: near
    phx
    tax
    jsr popa
    ply
    jsr MACPTR
    bcs macptr_unsupported
macptr_supported:
    txa ; C wants int in .AX not .XY
    phy
    plx
    rts
macptr_unsupported: ; return -1 on error
    ldx #$ff
    lda #$ff
    rts
.endproc

Note that the MACPTR function itself returning a signed int is fine, since MACPTR will return 512 bytes at most.

Link to comment
Share on other sites

On 6/21/2022 at 2:10 PM, ZeroByte said:

if (buf >= (char*)0xc000) buf -= 0x2000;
buf += tmp;

buf should be increased before the test.  Otherwise, the current wrap-around won't be seen.  But, if tmp is zero, then there's no point in doing any increases/decreases.

Edited by Greg King
Link to comment
Share on other sites

On 6/21/2022 at 8:55 PM, Greg King said:

buf should be increased before the test.  Otherwise, the current wrap-around won't be seen.  But, if tmp is zero, then there's no point in doing any increases/decreases.

Good catch - that is a bug indeed.

Right now, I'm wrestling with why VIA IRQs aren't behaving as I expect.... will fix that line right now tho.

Link to comment
Share on other sites

I've uploaded a new version of my OPL music on X16 demo (VOPLDEMO) to the downloads section.

Now, you can play _all_ the songs from the shareware version, and it loads each one into memory dynamically as you switch between them (using the left/right arrow keys)

  • Thanks 1
Link to comment
Share on other sites

On 6/21/2022 at 8:55 PM, Greg King said:

But, if tmp is zero, then there's no point in doing any increases/decreases.

Actually, there is a reason - I made my routine report results via a global struct containing the next-available memory location just like the Kernal's LOAD routine does. (only it's written into a struct and not passed back directly)

Also, since all the values are right there in the routine's hot little hands, I decided to save the caller the effort of calculating offsets, etc., and the struct contains total bytes transferred and a success/fail boolean. (which I suppose could be the final status byte, come to think of it)

Link to comment
Share on other sites

  • 4 weeks later...
On 6/21/2022 at 1:10 PM, ZeroByte said:

As promised, here's the code that I'm using to harness MACPTR. It's incomplete in that it doesn't fall back on ACPTR (cbm_load() in this case)  if MACPTR returns -1. I'm not happy with the return type being signed, and will probably fix this by either making it a signed log, or else just returning zero on failure and requiring that the calling code check status byte to know if it's zero because EOF or because of unsupported device.

Most likely, though, this is a case that will never happen with files, as the OS supports block reads from disk - but if you pass it an LFN that's connected to the screen or keyboard or whatever, then that would fail....

extern int __fastcall__ macptr(unsigned char numbytes, void* buffer);

int cx16_read(unsigned char lfn, void* buffer, unsigned int size) {
  int error = 0;
  char* buf = (char*)buffer;
  static unsigned int bytesread;
  static int tmp;

  /* if we can't change to the inputchannel #lfn then return an error */
  if (_oserror = cbm_k_chkin(lfn)) return -1;

  bytesread = 0;
  printf("\n");
  while (size > 0 && !cbm_k_readst()) {
    if (size>=512)
      tmp = macptr(0,buf);  // let MACPTR read as much as it wants
    else if (size>=256)
      tmp = macptr(255,buf); // If size 256..512, unlimited MACPTR would read past desired stopping point.
    else
      tmp = macptr((size), buf);
    if (tmp == -1) return -1;
    bytesread += tmp;
    size -= tmp;
    // wrap the buffer pointer back into the bank window
    // if it advances above 0xbfff. Note that MACPTR did
    // wrap banks correctly, but our calculation must match.
    // also note that MACPTR incremented the active bank,
    // so there is no need to do RAM_BANK++ here.
    if (buf >= (char*)0xc000) buf -= 0x2000;
    buf += tmp;
    if (cbm_k_readst() & 0xBF) break;
    if (tmp == 0) break;
  }

And the assembly is short:

MACPTR := $FF44

.import popa
.export _macptr ; the _ is important - C symbols all start with _

.proc _macptr: near
    phx
    tax
    jsr popa
    ply
    jsr MACPTR
    bcs macptr_unsupported
macptr_supported:
    txa ; C wants int in .AX not .XY
    phy
    plx
    rts
macptr_unsupported: ; return -1 on error
    ldx #$ff
    lda #$ff
    rts
.endproc

Note that the MACPTR function itself returning a signed int is fine, since MACPTR will return 512 bytes at most.

Good work ZeroByte!

This maybe a silly question, but how does it know when it reaches EOF? 

Is it one of these lines?

  if (cbm_k_readst() & 0xBF) break;
  if (tmp == 0) break;

What would be the proper way of calling cx16_read?

Maybe something like:

char buffer[255];

int filesize;

char lfn=?

filesize=cx16_read(lfn, buffer, 255);

lfn would be the number we used to open the file with cbm_open, correct?  Are there lfn numbers we should not use?

 

 

Edited by bce
Link to comment
Share on other sites

LFN needs to be 2..14 for sequential read. (which is what this is)

0 and 1 have special meanings that've been explained to me, and which I've read about, but I really haven't grasped what's truly going on - I just know use 0 for LOAD, use >=2 for sequential access. If you use 0 or 1, the call to send P: command to DOS will fail and you will not have seeked anywhere in the file.

And yes, tmp==0 is typically the case that will catch EOF. MACPTR returns a number 0-512, being the number of bytes read. EOF is something a little odd that I also haven't truly spent the time to dig in and completely understand. It's one of the bits in the status byte, and I believe (someone correct me if I'm wrong) that it isn't set after hitting EOF initially - only if you try to read when the starting point is EOF. So if you were 1 byte away from EOF and asked for 2 bytes, you'd get 1 and no errors, followed by a zero + EOF status the next time you tried to read.

And to call cx16_read(), you must have previously performed an open() on the file. You can use the cbm_k_X() kernal call wrappers for setnam, setlfs, and open, or else use the cbm.h convenience function.

e.g.:
cbm_open(LFN,DEVICE,SA,FILENAME);
cx16_read(LFN,buffer,numbytes);

or:
cbm_k_setnam("myfile.bin");
cbm_k_setlfs(2,8,0);
cbm_k_open();

Link to comment
Share on other sites

On 7/18/2022 at 5:16 PM, ZeroByte said:

LFN needs to be 2..14 for sequential read. (which is what this is)

0 and 1 have special meanings that've been explained to me, and which I've read about, but I really haven't grasped what's truly going on - I just know use 0 for LOAD, use >=2 for sequential access. If you use 0 or 1, the call to send P: command to DOS will fail and you will not have seeked anywhere in the file.

Secondary address 0 for binary load, 1 for binary save, 2-14 for sequential read or write and 15 for the command channel of the device. Remember that the KERNAL was originally the underlying Basic Input/Output subroutines to support Microsoft 6502 Basic, and then it was generalized a bit by Commodore to better support assembly language programs. The binary save has to know its starting address and ending address, with the starting address saved at the start of the binary file, and then load either goes to the default Basic load location or to the location the binary file was saved from. Keyboard and screen printing output requires sequential byte at a time input and output respectively, so that is extended to sequential byte at a time read or write access to sequential files.

Since program load and save each do their thing then return, without staying open, only one secondary address is needed for each. Then, device command channel is a unique channel for each device, so only one command channel secondary address is needed. Since the KERNAL can maintain up to 10 open channels, that means 16 secondary addresses is enough for load, 0, save, 1, command channel, 15, and then "enough" secondary addresses in between, 2-14.

Edited by BruceMcF
Link to comment
Share on other sites

oh, and one other thing I mentioned earlier in the thread but forgot about here - for whatever reason, only values 2, 6, and 10 allow seeking in my experience. So if you want to just do sequential reads from start of file - no big deal I guess, but if you want to use the P command, you have to use 2 6 or 10, and the LFN needs to match the SA for some odd reason.

  • Like 1
Link to comment
Share on other sites

On 7/19/2022 at 1:07 PM, ZeroByte said:

oh, and one other thing I mentioned earlier in the thread but forgot about here - for whatever reason, only values 2, 6, and 10 allow seeking in my experience. So if you want to just do sequential reads from start of file - no big deal I guess, but if you want to use the P command, you have to use 2 6 or 10, and the LFN needs to match the SA for some odd reason.

AFAICT, the P command only works on Relative files. I haven't ever seen any limitation on the LFN or SA being 2, 6 or 10, but on the other hand, the relative files I used were used to implement Block files for a Commodore 64 port of fig-Forth, so back in the day, while I USED relative files heavily, I didn't do any manipulation of REL files in either Basic or Assembler -- they were always hiding behind the BLOCK word.

As far as LFN and SA matching, I recall everyone doing that anyway, except for normally opening the Command Channel with an LFN of 1. For one thing, that makes it easier to keep track, and for another, when you use a variable in the OPEN command, you can re-use the same variable in both the LFN and SA positions.

Edited by BruceMcF
Link to comment
Share on other sites

  • Super Administrators
On 7/19/2022 at 1:58 PM, BruceMcF said:

AFAICT, the P command only works on Relative files. I haven't ever seen any limitation on the LFN or SA being 2, 6 or 10, but on the other hand, the relative files I used were used to implement Block files for a Commodore 64 port of fig-Forth, so back in the day, while I USED relative files heavily, I didn't do any manipulation of REL files in either Basic or Assembler -- they were always hiding behind the BLOCK word.

Let's be clear. In CMDR-DOS, the P file only works on Sequential files, because the system does not support REL files. 

In CBM-DOS, the P command only works in REL files, because that's what REL files are for.

On 7/19/2022 at 1:58 PM, BruceMcF said:

As far as LFN and SA matching, I recall everyone doing that anyway, except for normally opening the Command Channel with an LFN of 1. For one thing, that makes it easier to keep track, and for another, when you use a variable in the OPEN command, you can re-use the same variable in both the LFN and SA positions.

The LFN and SA don't need to match. I tested this while helping ZeroByte troubleshoot his C code. Whatever's going on there is a bug in the C runtime, not CMDR-DOS. 

I actually tested explicitly using different Logical File Number and Secondary Address, and P works just fine. (ie: OPEN 3,8,5). 

Link to comment
Share on other sites

On 7/19/2022 at 6:43 PM, TomXP411 said:

The LFN and SA don't need to match. I tested this while helping ZeroByte troubleshoot his C code. Whatever's going on there is a bug in the C runtime, not CMDR-DOS. 

I actually tested explicitly using different Logical File Number and Secondary Address, and P works just fine. (ie: OPEN 3,8,5). 

I guess I should've been more clear that this was in C. Yes, as Tom mentions, he fired off a quick demo in BASIC that works using any LFN/SA.

Do you recall whether it worked for values not having bit 1 set?

Link to comment
Share on other sites

On 7/20/2022 at 9:43 AM, ZeroByte said:

I guess I should've been more clear that this was in C.

Ah, I don't program in C for the CX16 (C64, etc.) ... at one time, a quarter of a century ago, I programmed a brute force maximum entropy estimator in C, but that was on a two 3.5" disk drive MS-DOS machine (with hard drive via a parallel port adapter).

Link to comment
Share on other sites

So here's something that I'm not 100% on: what exactly IS the SA?

My current understanding is that SETNAM and SETLFS are basically glorified "accessor functions" - they literally do nothing but store the values in some ZP locations. So essentially, those two functions are "arguments" for whatever upcoming channel-related command is about to be issued (open, load, save, chkin, etc)
Thus - SA is essentially a general-purpose "argument slot" whose function/encoding depend upon whatever command is actually about to get called. (e.g. LOAD uses it to determine relocate (bit 0) and headerless (bit 1))

Somehow in practice, its functionality seems to transcend this. It's also interesting how the channel number has significance as well.

What a mess.

Link to comment
Share on other sites

  • Super Administrators
On 7/21/2022 at 7:49 AM, ZeroByte said:

So here's something that I'm not 100% on: what exactly IS the SA?

It's entirely implementation dependent: it will be different for a disk drive, a printer, the serial port, and whatever other devices you can imagine. 

Specifically, with the disk drive, there are 3 reserved values.

15 is the command channel. Sending data to this channel issues DOS commands, rather than writing to disk. 
0 is used for LOAD
1 is used for SAVE

I'm going to circle back around to my earlier statement, I think either your code or the C library you're using is either mixing up or swapping the channel number and secondary address. You should probably audit the code and confirm the correct usage of the carry flag, and the correct values in .A, .X, and .Y in all of the KERNAL calls. I don't see any of the problems you're describing when using BASIC, so your confusion is likely due to bad code, not the KERNAL API.

Link to comment
Share on other sites

okay - so having followed the code in the kernal source, it appears that there's a concept I was not yet aware of...

I think LOAD is an exception to the rule, but in general, channel-related commands such as open/close/chkin/etc.... those all consider SA to be broken into two subcomponents:

high nybble = command, low nybble = channel.

The commands like open seem to manipulate the command portion of this.... having to do with talk / untalk / listen / unlisten - commands for the serial bus (or the CMDR-DOS pretending to be a serial device in the case of X16 on device 8.)

Man, this onion keeps having more layers. That's why I wasn't understanding what you were saying, @TomXP411... I was thinking LFN was the channel, essentially. I know it means "logical file number" - but now I have a new question - can LFN be literally any byte the program wants to use? Could you use LFN = 72 if you want?

 

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

×
×
  • Create New...

Important Information

Please review our Terms of Use