Jump to content
IGNORED

vbcc optimizing C compiler for 6502 now supports Atari


Recommended Posts

Hello,

 

I have uploaded a new version of my C compiler for 6502 with experimental support for Atari 8bit computers to: http://www.compilers.de/vbcc.html

 

A few of the good things:

- compiler is under active development
- supports C99 (variable-length arrays, designated initializers etc.)
- generates optimized code (see dhrystones in sample directory)
- supports banked memory and far-pointers
- (limited) floating point support based on Steve Wozniaks code
- (pretty good) 32/64bit IEEE floating point support based on SANE
- support for writing interrupt handlers
- attributes for putting variables into zero page
- supports stack-frames > 256 bytes

 

On the bad side, the C library is currently only partially optimized for the 6502 and may be too big for many applications. Not all functionality is supported at the moment. Especially file I/O is missing (stdin/stdout to keyboard/screen is supported).

 

I have never used an Atari computer and the port is based on a bit of information I got from the internet. So, there are probably some things I did completely wrong. Feel free to tell me how to do it right.

 

So far I could only test with an emulator and do not know what works on real hardware. The samples directory contains a few examples compiled for the Atari. I would be interested to hear if they work on real hardware.

 

 

  • Like 10
  • Thanks 3
Link to comment
Share on other sites

Nice.

 

Minor issue in the __clock() code: RTCLOK ($12-14) is updated from the NMI-based VBI stage 1 handler, so SEI/CLI doesn't hold off updates. One fix is to read RTCLOK+1, RTCLOK, and RTCLOK+2 in that order, and then re-check RTCLOK+1 and repeat if it has changed.

 

  • Like 1
Link to comment
Share on other sites

Fantastic! I've been a VBCC user for Amiga development, for a LONG time. I can vouch for this guy making solid compilers. :)

 

(as an odd factoid, VBCC also is one of the few compilers with a preliminary back-end for the VideoCore IV GPU, the actual processor that comes up first on the Raspberry Pi).

 

-Thom

Link to comment
Share on other sites

Hi!

1 hour ago, vbc said:

I have uploaded a new version of my C compiler for 6502 with experimental support for Atari 8bit computers to: http://www.compilers.de/vbcc.html

 

A few of the good things:

- compiler is under active development
- supports C99 (variable-length arrays, designated initializers etc.)
- generates optimized code (see dhrystones in sample directory)
- supports banked memory and far-pointers
- (limited) floating point support based on Steve Wozniaks code
- (pretty good) 32/64bit IEEE floating point support based on SANE
- support for writing interrupt handlers
- attributes for putting variables into zero page
- supports stack-frames > 256 bytes

 

On the bad side, the C library is currently only partially optimized for the 6502 and may be too big for many applications. Not all functionality is supported at the moment. Especially file I/O is missing (stdin/stdout to keyboard/screen is supported).

 

This is great, thanks for doing it!

 

1 hour ago, vbc said:

I have never used an Atari computer and the port is based on a bit of information I got from the internet. So, there are probably some things I did completely wrong. Feel free to tell me how to do it right.

 

I know that the library is limited yet, but I have a few suggestions:

 

- The starting address of $700 makes the generated programs incompatible with any DOS. It is better to set the default starting address to $2000, this will make enough space available for most DOS.

- You are initializing the C stack to the top of available RAM (from MEMTOP, location $2E5/$2E6). This will make the program crash if you change the graphics mode, as most graphic modes use more ram than the default 40x24 text mode.

- The runtime ends with a JMP to itself. You should end with a RTS to return to DOS, or if the CPU stack was lost, you can end with a "JMP ($0A)".

 

1 hour ago, vbc said:

So far I could only test with an emulator and do not know what works on real hardware. The samples directory contains a few examples compiled for the Atari. I would be interested to hear if they work on real hardware.

 

Setting the start address to $2000 will make the code work in the real Atari.

 

Have Fun!

 

  • Like 1
Link to comment
Share on other sites

8 hours ago, phaeron said:

Minor issue in the __clock() code: RTCLOK ($12-14) is updated from the NMI-based VBI stage 1 handler, so SEI/CLI doesn't hold off updates. One fix is to read RTCLOK+1, RTCLOK, and RTCLOK+2 in that order, and then re-check RTCLOK+1 and repeat if it has changed.

Ah, good to know. I will fix this. Thanks.

Link to comment
Share on other sites

8 hours ago, dmsc said:

- The starting address of $700 makes the generated programs incompatible with any DOS. It is better to set the default starting address to $2000, this will make enough space available for most DOS.

$700 to $2000 are about 6.5KB. Seems pretty hefty to lose that RAM.

 

What is the DOS needed for after the program is loaded? I presume it is necessary when using file I/O. Using IOCBs for screen/keyboard does not require dos, does it?

 

Quote

- You are initializing the C stack to the top of available RAM (from MEMTOP, location $2E5/$2E6). This will make the program crash if you change the graphics mode, as most graphic modes use more ram than the default 40x24 text mode.

Ok. So this depends on what kind of modes the application uses? Is the graphics memory always located directly below MEMTOP? Is it sufficient to allow the application to specify an additional offset that is reserved below MEMTOP (so the stackpointer would start at MEMTOP-GRAPHSIZE or so)?

 

Quote

- The runtime ends with a JMP to itself. You should end with a RTS to return to DOS, or if the CPU stack was lost, you can end with a "JMP ($0A)".

Do certain memory areas have to be saved/restored before returning?

 

Is it possible ro run the program a second time without loading it again? In this case the init values for variables would have to be copied.

 

Thanks for your hints.

Link to comment
Share on other sites

48 minutes ago, vbc said:

Ok. So this depends on what kind of modes the application uses? Is the graphics memory always located directly below MEMTOP? Is it sufficient to allow the application to specify an additional offset that is reserved below MEMTOP (so the stackpointer would start at MEMTOP-GRAPHSIZE or so)?

The beginning of the screen memory and its size are specified in the Display List (ANTIC) programs (there may be more than one) and it doesn't have to be continuous memory, each line of the screen can start with a different memory address ;) and take more or less memory depending on the graphics mode of this line (row in text modes) :D

 

Of course there are some predefined Display Lists for various graphics modes.

 

https://playermissile.com/dli_tutorial/#display-lists-how-the-atari-generates-the-display

Edited by zbyti
display-lists-how-the-atari-generates-the-display
  • Sad 1
Link to comment
Share on other sites

@vbc take a look at this thread (and the link to the cc65 site within it) for details on the reserving of graphics memory

 

35 minutes ago, vbc said:

$700 to $2000 are about 6.5KB. Seems pretty hefty to lose that RAM.

the move to $2000 would be for the default, presumably this could be overridden in the command line if needed?

 

This is not only for DOSes, many of the multi-carts utilise their own binary executable bootstrap loader that is typically loaded in this lower memory area.

With both those and a DOS, the coder if free to trash that area and reuse it for anything else if the code/data there is not going to be used after.
 

Edited by Wrathchild
Link to comment
Share on other sites

Hi!

2 hours ago, vbc said:

$700 to $2000 are about 6.5KB. Seems pretty hefty to lose that RAM.

Yes, sadly DOSes are large.

2 hours ago, vbc said:

What is the DOS needed for after the program is loaded? I presume it is necessary when using file I/O. Using IOCBs for screen/keyboard does not require dos, does it?

No, DOS in only for file I/O. As @Wrathchild said, this is a good default, but most languages for the Atari allow you to change that value. To load an XEX (ataricom) executable from disk, you also need a disk loader, there are multiple of those but most will load from about $900 upwards, you need special bootloaders for lower starting addresses.

 

2 hours ago, vbc said:

Ok. So this depends on what kind of modes the application uses? Is the graphics memory always located directly below MEMTOP? Is it sufficient to allow the application to specify an additional offset that is reserved below MEMTOP (so the stackpointer would start at MEMTOP-GRAPHSIZE or so)?

That is the solution used in CC65. It works, but it is a constant source of questions in the forum, as novices tend to skip that part of the docs ;) 

 

As @zbyti said above, the hardware allows having the graphics data in any RAM address (subject to alignment restrictions, display-lists can't cross 1k boundaries and graphics data can't cross 4k boundaries), but the OS graphic modes are always setup from top of RAM downwards.

 

Do you expect C programs to need a lot of stack space? If not, a better solution could be to pu the stack bellow the application code, this way you have more stack if using smaller DOSs. Something like: from MEMLO to $2400 stack, from $2400 to $XXX code and data, from $XXX to MEMTOP heap. MEMLO is automatically set by DOS and other loadable drivers to the first byte of available memory.

 

2 hours ago, vbc said:

Do certain memory areas have to be saved/restored before returning?

You need to save the low part of the ZP (up to $7F), the OS area from $200 to $3C0 and the DOS area from $700 to MEMLO.

 

From $3C0 up to $5FF there are a lot of OS buffers: the printer buffer, the cassette buffer, the line-input buffer and floating-point scratch space (used by the math-pack and BASIC), those areas will be overwritten by the OS when needed.

 

Parts of the ZP area can be modified - for example there are a few bytes used by the OS plot and line drawing that are overwritten on each call, and locations $0 and $1 are normally available. And if you use the OS math-pack, locations from $D4 to $FF are used as registers and temporary floating-point areas.

 

2 hours ago, vbc said:

Is it possible ro run the program a second time without loading it again? In this case the init values for variables would have to be copied.

It is possible, but not common - you need to "run at address" and know the starting address of the program. Most users would never do that.

 

2 hours ago, vbc said:

Thanks for your hints.

 

Thank you again for the great compiler!

 

Have Fun!

  • Like 1
Link to comment
Share on other sites

3 hours ago, dmsc said:

Hi!

Yes, sadly DOSes are large.

No, DOS in only for file I/O. As @Wrathchild said, this is a good default, but most languages for the Atari allow you to change that value. To load an XEX (ataricom) executable from disk, you also need a disk loader, there are multiple of those but most will load from about $900 upwards, you need special bootloaders for lower starting addresses.

 

The start address can be changed by adjusting MEMSTART in targets/6502-atari/vlink.cmd.

So I should set it to e.g. $2000 as default and users who need more memory have to think about adjusting it and possibly restricting their programs to using smaller DOS/loader?

 

Quote

That is the solution used in CC65. It works, but it is a constant source of questions in the forum, as novices tend to skip that part of the docs ;) 

 

As @zbyti said above, the hardware allows having the graphics data in any RAM address (subject to alignment restrictions, display-lists can't cross 1k boundaries and graphics data can't cross 4k boundaries), but the OS graphic modes are always setup from top of RAM downwards.

So that means the usable end address has to be specified/adjusted by the user, because the compiler cannot know. But many users don't, because, well, users. ?

 

Quote

Do you expect C programs to need a lot of stack space? If not, a better solution could be to pu the stack bellow the application code, this way you have more stack if using smaller DOSs. Something like: from MEMLO to $2400 stack, from $2400 to $XXX code and data, from $XXX to MEMTOP heap. MEMLO is automatically set by DOS and other loadable drivers to the first byte of available memory.

This does depend a lot on the code. Developers who have used cc65 in the past will probably try to avoid using stack like the plague. Generic C code or code ported from other systems may use it more. The advantage of the current layout is that it allows the stack to grow into free heap area. Of course that is moot if the stack is overwritten by graphics memory. ?

 

 

So, basically, either the user has to specify the graphics memory in the current layout or the maximum stack size (+/- DOS size) if the stack is put at MEMLO?

 

Is the size of graphics memory something that a developer should know or is it difficult to specify it?

 

Quote

ou need to save the low part of the ZP (up to $7F), the OS area from $200 to $3C0 and the DOS area from $700 to MEMLO.

 

From $3C0 up to $5FF there are a lot of OS buffers: the printer buffer, the cassette buffer, the line-input buffer and floating-point scratch space (used by the math-pack and BASIC), those areas will be overwritten by the OS when needed.

 

Parts of the ZP area can be modified - for example there are a few bytes used by the OS plot and line drawing that are overwritten on each call, and locations $0 and $1 are normally available. And if you use the OS math-pack, locations from $D4 to $FF are used as registers and temporary floating-point areas.

I assume only memory that is changed by the program has to be saved?

 

You mentioned the option of jmp ($a) rather than rts. Is this some kind of soft-reset? Does memory also have to saved for this variant? Is there any benefit of one solution of the other?

 

Quote

It is possible, but not common - you need to "run at address" and know the starting address of the program. Most users would never do that.

Ok. So no need to support this.

  • Like 1
Link to comment
Share on other sites

Hi!

6 hours ago, vbc said:

The start address can be changed by adjusting MEMSTART in targets/6502-atari/vlink.cmd.

So I should set it to e.g. $2000 as default and users who need more memory have to think about adjusting it and possibly restricting their programs to using smaller DOS/loader?

IMHO, that should be OK, this is what I do in FastBasic, and most users don't need to adjust it.

 

6 hours ago, vbc said:

So that means the usable end address has to be specified/adjusted by the user, because the compiler cannot know. But many users don't, because, well, users. ?

 

This does depend a lot on the code. Developers who have used cc65 in the past will probably try to avoid using stack like the plague. Generic C code or code ported from other systems may use it more. The advantage of the current layout is that it allows the stack to grow into free heap area. Of course that is moot if the stack is overwritten by graphics memory. ?

Yes, I know, not easy. Most languages in the Atari don't have big stacks, for example in Action local variables are statically allocated and no recursion is allowed. In FastBasic the stack is really limited - it uses the 6502 stack for subroutine calling and the cassette buffer for the interpreter stack (128 bytes) - also variables are static and recursive procedures are not supported.

 

6 hours ago, vbc said:

So, basically, either the user has to specify the graphics memory in the current layout or the maximum stack size (+/- DOS size) if the stack is put at MEMLO?

Yes, I think this is it. Note that, by using the layout I proposed, the user would only need to adjust MEMSTART, as the stack would be from that address downward. This also allows using more memory, as the $2000 value is only a "big enough value".

 

There are reduced functionality DOSs (see LiteDOS http://www.mr-atari.com/Mr.Atari/LiteDOS/ ) with values as low as $1000, giving you 4k extra RAM. And also there is SDX, a cartridge based DOS, that can set MEMLO less than $900 on expanded memory systems. I personally use BW-DOS, a full-featured DOS with support for directories, command line and hard-disks, it sets MEMLO $1EE4, giving you 284 bytes for loadable drivers before reaching $2000.

 

6 hours ago, vbc said:

Is the size of graphics memory something that a developer should know or is it difficult to specify it?

Well, a developer should know... but, IMHO, it is a little more difficult, because people likes to copy BASIC samples from old magazines, or from old ASM listings, porting those to C, before really understanding the memory needs.

 

Most BASIC programs using "advanced" graphics reserve memory from the top by adjusting down the location 106 (RAMTOP), then issuing a graphics command so that the OS lowers the display memory. For example, you lower RAMTOP by 8, reserving 1K for a new charset and 1K for P/M graphics. This had the advantage of working on any 8-bit Atari regardless of the amount of RAM.

 

In that case, a good memory layout for a C program would be to lower the stack by the graphics memory used (for example, 8K for high-res graphic modes) plus P/M memory (another 2K for high res P/M).

 

6 hours ago, vbc said:

I assume only memory that is changed by the program has to be saved?

IMHO, it is better that you don't change OS memory ($02 to $7F, $200 to $3C0, $700 to MEMLO), so you don't need to save anything. If you use OS facilities, then the DOS would restore the graphics mode, close open files and work ok after return. For example, if you change the left margin and background color (by writing locations $52 and $2C8), after returning to DOS the new values would be respected and all would work. If you want to restore the colors before exit, just reopen IOCB#0 with "E:" after exit.

 

What you need to restore before exit to DOS are interrupt handlers, changes to MEMLO and TOPMEM, etc. But I expect that to be handled by the user program, not by the library.

 

6 hours ago, vbc said:

You mentioned the option of jmp ($a) rather than rts. Is this some kind of soft-reset? Does memory also have to saved for this variant? Is there any benefit of one solution of the other?

The JMP ($A) would be the same as an RTS. The only advantage is allowing to exit from inside nested routines, so you don't need to rollback the stack.

 

6 hours ago, vbc said:

Ok. So no need to support this.

Yes!

 

Have Fun!

 

  • Like 1
Link to comment
Share on other sites

The other wrinkle in allocating screen memory is that you must be mindful of 4K blocks; should the video memory cross a 4K boundary, the display will be distorted.  Allocating memory to the display without considering the 4K issue can lead to problems.

 

Link to comment
Share on other sites

18 hours ago, vbc said:

Is the size of graphics memory something that a developer should know or is it difficult to specify it?

Depends how well they know the Atari hardware.   A lot of developers I encounter seem to know shockingly little about the underlying hardware because they are just making API calls that do all the magic.   However, it is hard to code this way on an 8-bit machine because memory is so limited, memory managers are virtually non-existent, and API calls are a luxury.

 

Still, if I wrote an Atari 8-bit program in C, I would be aware of these things because I learned them back then,  but if I was to port my code to a C64 or Apple II, I'm sure I would make all sorts of rookie mistakes because I don't know exactly how their memory is laid out

  • Like 1
Link to comment
Share on other sites

On 7/31/2020 at 2:50 AM, dmsc said:

IMHO, that should be OK, this is what I do in FastBasic, and most users don't need to adjust it.

Ok, so I will try start with a mostly foolproof configuration and offer the option to change it for advanced users.

 

Quote

Yes, I think this is it. Note that, by using the layout I proposed, the user would only need to adjust MEMSTART, as the stack would be from that address downward. This also allows using more memory, as the $2000 value is only a "big enough value".

 

There are reduced functionality DOSs (see LiteDOS http://www.mr-atari.com/Mr.Atari/LiteDOS/ ) with values as low as $1000, giving you 4k extra RAM. And also there is SDX, a cartridge based DOS, that can set MEMLO less than $900 on expanded memory systems. I personally use BW-DOS, a full-featured DOS with support for directories, command line and hard-disks, it sets MEMLO $1EE4, giving you 284 bytes for loadable drivers before reaching $2000.

 

Well, a developer should know... but, IMHO, it is a little more difficult, because people likes to copy BASIC samples from old magazines, or from old ASM listings, porting those to C, before really understanding the memory needs.

The general consensus seems to be that relying on correctly specified graphics memory size is likely to cause trouble. In this case the layout you proposed is probably indeed better suited.

 

Quote

What you need to restore before exit to DOS are interrupt handlers, changes to MEMLO and TOPMEM, etc. But I expect that to be handled by the user program, not by the library.

 

The JMP ($A) would be the same as an RTS. The only advantage is allowing to exit from inside nested routines, so you don't need to rollback the stack.

Understood. However, when I add the jmp ($a), the screen contents seem to get lost when the system is returning to e.g. Basic. The benchmarks from several threads on this forum all seem to contain an infinite loop at the end for this reason. To me it seems to make more sense to have standard C programs behave as expected without having to add a loop at the end. Atari-aware programs that want to return can then call that vector.

 

Thanks again to you and the other posters for sharing some light on those topics.

  • Like 1
Link to comment
Share on other sites

I'd expect that returning from main would transfer control back to DOS or some other control program.

 

Also, it looks like anonymous unions are not supported, I use anonymous unions to access hardware registers, it works as expected in cc65. Maybe it should't, who knows? But it does work and is extremely usefull.

Edited by antrykot
Link to comment
Share on other sites

7 hours ago, vbc said:

Understood. However, when I add the jmp ($a), the screen contents seem to get lost when the system is returning to e.g. Basic. The benchmarks from several threads on this forum all seem to contain an infinite loop at the end for this reason. To me it seems to make more sense to have standard C programs behave as expected without having to add a loop at the end. Atari-aware programs that want to return can then call that vector.

More needs to be said on memory configuration, allocation and returning. There is still a lot of confusion.

 

Free memory for applications starts at MEMLO, the contents of the pointers at $2e5,$2e6. MEMLO is dynamic, and will depend on the DOS. For some DOS versions (e.g. Dos++) it can be as low as $800, but generally, a value of $1f00 is generally assumed safe. Ideally, binaries should be able to relocate themselves, even though this was rather uncommon at its time.

 

The highest address that can be used by an application is MEMTOP, $2e7,$2e8. Above that the Os uses screen memory. The highest used memory used by the application should be placed in APPMEMHI, $e,$f. Then, when changing the grapics mode, the Os will use between RAMTOP ($6c) and  APPMEMHI, and upon successful allocation, adjust MEMTOP.

 

Thus, MEMLO and MEMTOP are adjusted by the Os, APPMEMHI by the application, and memory allocation increments APPMEMHI, releasing memory shrinks APPMEMHI. Memory between MEMLO and APPMEMHI is memory allocated by the application, memory between  APPMEMHI and MEMTOP is free and avaliable for the graphics. Graphics memory starts at MEMTOP and exceeds  to RAMTOP.

 

Finally, JMP ($0a) does not return from the program. It rather calls to the DOS. This may not exactly what the user wants as it may require loading DUP.SYS from disk, or inserting a disk. In general, an RTS should be preferred.

Edited by thorfdbg
  • Like 2
Link to comment
Share on other sites

On 8/1/2020 at 4:24 PM, antrykot said:

I'd expect that returning from main would transfer control back to DOS or some other control program.

Perhaps I will add a destructor that waits for a key press before returning if stdout was used.

 

Quote

Also, it looks like anonymous unions are not supported, I use anonymous unions to access hardware registers, it works as expected in cc65. Maybe it should't, who knows? But it does work and is extremely usefull.

Anonymous unions are not allowed in C89 and C99. I think they have been added to C11. Maybe I will add it when I add support for C11. However I do not know if the cc65 implementation is compatible with the C11 specification.

Link to comment
Share on other sites

On 8/1/2020 at 9:46 PM, thorfdbg said:

More needs to be said on memory configuration, allocation and returning. There is still a lot of confusion.

 

Free memory for applications starts at MEMLO, the contents of the pointers at $2e5,$2e6. MEMLO is dynamic, and will depend on the DOS. For some DOS versions (e.g. Dos++) it can be as low as $800, but generally, a value of $1f00 is generally assumed safe. Ideally, binaries should be able to relocate themselves, even though this was rather uncommon at its time.

How would a binary relocate itself? As I understand it, a binary has to specify a fixed address each segment is loaded into. So a viable memory address is needed to load into anyway. There is not much benefit in relocating later on, is there? It would make more sense if the loader did the relocation. To be honest the memory handling on those machines does not seem to have been really thought through.

 

Quote

The highest address that can be used by an application is MEMTOP, $2e7,$2e8. Above that the Os uses screen memory. The highest used memory used by the application should be placed in APPMEMHI, $e,$f. Then, when changing the grapics mode, the Os will use between RAMTOP ($6c) and  APPMEMHI, and upon successful allocation, adjust MEMTOP.

 

Thus, MEMLO and MEMTOP are adjusted by the Os, APPMEMHI by the application, and memory allocation increments APPMEMHI, releasing memory shrinks APPMEMHI. Memory between MEMLO and APPMEMHI is memory allocated by the application, memory between  APPMEMHI and MEMTOP is free and avaliable for the graphics. Graphics memory starts at MEMTOP and exceeds  to RAMTOP.

 

Finally, JMP ($0a) does not return from the program. It rather calls to the DOS. This may not exactly what the user wants as it may require loading DUP.SYS from disk, or inserting a disk. In general, an RTS should be preferred.

Thanks for the explanation.

Link to comment
Share on other sites

On 8/5/2020 at 2:19 AM, vbc said:

How would a binary relocate itself? As I understand it, a binary has to specify a fixed address each segment is loaded into. So a viable memory address is needed to load into anyway. There is not much benefit in relocating later on, is there? It would make more sense if the loader did the relocation. To be honest the memory handling on those machines does not seem to have been really thought through.

The way how I do that is to first load a small loader into a "known to be available" space, e.g. page 6, and then the loader figures out where the binary would need to go (e.g. above MemLo), pull in the data from the file, and then relocate through data also in the file.

Technically, such a binary consists of a regular "binary load file" in the sense of the regular Atari DOS syntax, whose first chunk sets an init vector, that is, fills the vector at 2E2,2E3. Then, after finding the init vector filled, the Dos loader will jump through the init vector, thus to your program, and from there, you can continue loading. Relocation information is in the simplest case just a bitmask, one bit for each byte in the file, whether it needs to be relocated. In such a simple layout, one would need to round up to entire pages as only the high-bytes would need relocation. In more complicated setups, one would need a table.

 

Yes, of course, memory allocation on such old machines is shady, and there is not much service the operating system has to offer. Atari is in a sense both better and worse than the C64. Better, because there are at least some known pointers the Os uses to allocate and reserve memory (those I mentioned above), worse, because it is necessary to use them as the screen will be relocated, and the size of the DOS will vary, so you better use them.

 

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.
Note: Your post will require moderator approval before it will be visible.

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.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...