Jump to content
IGNORED

cc7800, a C compiler dedicated to the Atari 7800


bsteux

Recommended Posts

29 minutes ago, karri said:

The SN cart has a 4k buffer at #d000 that can point anywhere in a 512k rom. So it is easy to copy graphics It also has a bit that can flip sprites left/right. But unfortunally not flip then up/down as required by R-Type.

 

One interesting feature of the SN cart is a 32k ram that can be toggled between two 16k banks. One bank could hold the 16k of graphics while drawing. The other bank could hold the music and sounds to be updated during blanking time.

 

The problem in the SN development is lack of emulators.

This is exactly what I'd need. Well, as I have "only" a Concerto, I'll have to do without... But sure, if we wanted to get the best out of Maria, the best would be to have more flexible bankswitching schemes than then commercial ones from the 80s... And support for it in the emulators !

Link to comment
Share on other sites

9 minutes ago, bsteux said:

This is exactly what I'd need. Well, as I have "only" a Concerto, I'll have to do without... But sure, if we wanted to get the best out of Maria, the best would be to have more flexible bankswitching schemes than then commercial ones from the 80s... And support for it in the emulators !

It may be of value to look at bankset bankswitching.

  • Like 2
Link to comment
Share on other sites

29 minutes ago, Trebor said:

It may be of value to look at bankset bankswitching.

I was thinking of adding support for this to cc7800, since it's supported by a7800 (and thus at least testable). In the future... There is still enough meat on the "standard" setting for a couple of months...

Link to comment
Share on other sites

Here's an intermediate progress on R-Type, where :

- I've added the 160B R-9 spaceship and missile firing (nothing special)

- I show how to redefine the screen layout so that we have 2 lines of 320A/C at the bottom of the screen to display the score (display_init() function and dli interrupt)

- I introduce the new function sparse_tiling_collision, which computes the collision of sprites with scrolling background (this one was a BIG headache. Not sure it's fully correct. At least it looks like it's working...).

- The horizontal sparse tiling routine is now way better balanced over frames. This leaves tons of GFlops for processing sprites and logic. In the end, it should look like R-Type Final on PS4.

 

Note that when the R-9 flies at the bottom, I sometimes run out of DMA... (that wasn't expected. 160A background + 160A full tiling + 1 160B sprite ? )

Background: 10 + 20 * 3 + 8 + 24 * 3 = 150 cycles

Tiling: 23 * 9 = 207 cycles + sometimes 4 headers on the line = 247

9-B 160B sprite: 10 + 8 * 3 = 34 cycles

This sums to 150 + 247 + 34 = 431 cycles... Wheee, yes, I'm running out of DMA... Nervous breakdown ! Let's go to immediate mode quickly for the tiling ! (and hide a little bit on background at the bottom of the screen if we want to use 160B tiles...)

 

No video from the real Atari 7800 today : my Concerto is apparently sick (blue / green screen alternating).

 

Here is the code example_rtype.c :

 

#include "string.h"
#define _MS_DL_SIZE 96
#define HORIZONTAL_SCROLLING
#define _MS_BOTTOM_SCROLLING_ZONE 1
#include "sparse_tiling.h"
#include "joystick.h"

// Generated from sprites7800 RType_tiles.yaml
#include "example_RType_tiles.c"

// Generated from sprites7800 RType_sprites.yaml
#include "example_RType_sprites.c"

// Generated from tiles7800 --sparse RType_tiles.yaml --varname tilemap_level1 RType_level1.tmx 
#include "example_RType_level1.c"

// Generated from sprites7800 RType_font.yaml
#include "example_RType_font.c"

// DLI management
ramchip char save_acc, save_x, save_y;

void interrupt dli()
{
    store(save_acc);
    save_x = X;
    save_y = Y;
    multisprite_set_charbase(alphabet);
    *CTRL = 0x43; // DMA on, 320A/C mode, One (1) byte characters mode
    X = save_x;
    Y = save_y;
    load(save_acc);
}

// Game state management
#define MISSILES_SPEED 4 
#define MISSILES_NB_MAX 5
ramchip char missile_xpos[MISSILES_NB_MAX], missile_ypos[MISSILES_NB_MAX];
ramchip char missile_first, missile_last;

ramchip char button_pressed;
ramchip char R9_xpos, R9_ypos, R9_state, R9_state_counter; 

ramchip unsigned int score, high_score;
ramchip char update_score;
ramchip char display_score_str[5];
ramchip char display_high_score_str[5];

void game_init()
{
    score = 0;
    high_score = 0;
    update_score = 1;

    // Init game state variables
    missile_first = 0;
    missile_last = 0;

    // Initialize spaceship state
    R9_xpos = 20;
    R9_ypos = 80;
    R9_state = 1;
    R9_state_counter = 100;
}

void step()
{
    char x, y, i;
    // Draw missiles
    for (i = missile_first; i != missile_last; i++) {
        if (i == MISSILES_NB_MAX) {
            i = 0;
            if (missile_last == 0) break;
        }
        if (missile_xpos[X] != -1) {
            y = missile_ypos[X = i];
            x = missile_xpos[X] + MISSILES_SPEED;
            if (x >= 160 || sparse_tiling_collision(y + 1, x, x + 15) != -1 ) {
                // Out of screen
                missile_xpos[X = i] = -1; // Removed
                do {
                    X++;
                    if (X == MISSILES_NB_MAX) X = 0;
                } while (X != missile_last && missile_xpos[X] == -1);
                missile_first = X;
            } else {
                missile_xpos[X = i] = x;
                // Draw missile
                multisprite_display_small_sprite_ex(x, y, missile, 2, 6, 13, 0);
            }
        }
    }

    char draw_R9;
    if (R9_state == 0) {
        draw_R9 = 1;
        // Check collision with background
        char c = sparse_tiling_collision(R9_ypos + 6, R9_xpos, R9_xpos + 15);
        if (c != -1) {
            R9_state = 1;
            R9_state_counter = 50;
            score = c;
            update_score = 1;
        }
    } else if (R9_state == 1) {
        // Blinking returning R9
        draw_R9 = R9_state_counter & 8;
        R9_state_counter--;
        if (R9_state_counter == 0) {
            R9_state = 0;
        }
    } else if (R9_state == 2) {
        draw_R9 = 0;
    }

    if (draw_R9) {
        multisprite_display_small_sprite_ex(R9_xpos, R9_ypos, R9, 8, 4, 4, 1);
    }
}

void fire()
{
    X = missile_last++;
    if (missile_last == MISSILES_NB_MAX) missile_last = 0;
    if (missile_last != missile_first) {
        missile_xpos[X] = R9_xpos + 8;
        missile_ypos[X] = R9_ypos + 6;
    } else missile_last = X;
}

void joystick_input()
{
    joystick_update();
    if (joystick[0] & JOYSTICK_LEFT) {
        if (R9_xpos) R9_xpos--;
    } else if (joystick[0] & JOYSTICK_RIGHT) {
        if (R9_xpos < 160 - 16) R9_xpos++;
    }
    if (joystick[0] & JOYSTICK_UP) {
        if (R9_ypos > 2) R9_ypos -= 2;
    } else {
        if (joystick[0] & JOYSTICK_DOWN) {
            if (R9_ypos < 208 - 14) R9_ypos += 2;
        }
    }
    if (joystick[0] & JOYSTICK_BUTTON1) {
        if (!button_pressed) {
            button_pressed = 1;
            if (R9_state != 2) fire();
        }
    } else button_pressed = 0;
}

// Background scrolling
char scroll_background_counter;

void scroll_background()
{
    char c;
    signed char pos1, pos2, pos3, pos4;
    pos1 = -scroll_background_counter;
    pos2 = pos1 + 80;
    pos3 = pos1 - 8;
    if (pos3 < -16) pos3 += 16;
    pos4 = pos3 + 80;
    if (_ms_buffer) {
        X = _MS_DLL_ARRAY_SIZE + 1;
        scroll_background_counter++;
        if (scroll_background_counter == 16) scroll_background_counter = 0;
    } else X = 1;
    _ms_tmpptr = _ms_dls[X];
    for (c = 0; c != 3; c++) {
        // Modify bytes 4 and 8 of the DLL entries (x position of background sprites=
        _ms_tmpptr[Y = 4] = pos1;
        _ms_tmpptr[Y = 8] = pos2;
        _ms_tmpptr = _ms_dls[++X];
        _ms_tmpptr[Y = 4] = pos1;
        _ms_tmpptr[Y = 8] = pos2;
        _ms_tmpptr = _ms_dls[++X];
        _ms_tmpptr[Y = 4] = pos3;
        _ms_tmpptr[Y = 8] = pos4;
        _ms_tmpptr = _ms_dls[++X];
        _ms_tmpptr[Y = 4] = pos3;
        _ms_tmpptr[Y = 8] = pos4;
        _ms_tmpptr = _ms_dls[++X];
    }
}

void display_score_update(char *score_str)
{
    char display_score_ascii[6];
    itoa(score, display_score_ascii, 10);
    X = strlen(display_score_ascii); 
    for (Y = 0; Y != 5 - X; Y++) {
        score_str[Y] = 26; // '0'
    }
    Y = 4;
    do {
        score_str[Y--] = 26 + (display_score_ascii[--X] - '0');
    } while (X);
}

void display_init()
{
    *BACKGRND = 0x0;
    
    multisprite_get_tv();
    multisprite_clear();
    multisprite_save();

    _ms_tmpptr = _ms_b0_dll;
    for (X = 0, _ms_tmp = 0; _ms_tmp <= 1; _ms_tmp++) {
        // Build DLL
        // 69 blank lines for PAL
        // 19 blank lines for NTSC
        if (_ms_pal_detected) {
            // 16 blank lines
            _ms_tmpptr[Y = 0] = 0x0f;  // 16 lines
            _ms_tmpptr[++Y] = _ms_set_wm_dl >> 8;
            _ms_tmpptr[++Y] = _ms_set_wm_dl;
            // 16 blank lines
            _ms_tmpptr[++Y] = 0x0f;  // 16 lines
            _ms_tmpptr[++Y] = _ms_blank_dl >> 8;
            _ms_tmpptr[++Y] = _ms_blank_dl;
        } else {
            _ms_tmpptr[Y = 0] = 0x08; // 9 lines
            _ms_tmpptr[++Y] = _ms_set_wm_dl >> 8;
            _ms_tmpptr[++Y] = _ms_set_wm_dl;
        }
        // 16 pixel high regions
        for (_ms_tmp2 = 0; _ms_tmp2 != _MS_DLL_ARRAY_SIZE - 2; X++, _ms_tmp2++) {
            _ms_tmpptr[++Y] = 0x4f; // 16 lines
            _ms_tmpptr[++Y] = _ms_dls[X] >> 8; // High address
            _ms_tmpptr[++Y] = _ms_dls[X]; // Low address
        }
        // 1 pixel high region to separate from 320A scoreboard. This gives some little room for the DLI to execute
        _ms_tmpptr[++Y] = 0x00; // 1 line
        _ms_tmpptr[++Y] = _ms_blank_dl >> 8;
        _ms_tmpptr[++Y] = _ms_blank_dl;
        // 8 pixel high regions (320A)
        for (_ms_tmp2 = 0; _ms_tmp2 != 2; X++, _ms_tmp2++) {
            _ms_tmpptr[++Y] = 0x07; // 8 lines
            _ms_tmpptr[++Y] = _ms_dls[X] >> 8; // High address
            _ms_tmpptr[++Y] = _ms_dls[X]; // Low address
        }
        if (_ms_pal_detected) {
            // 16 blank lines
            _ms_tmpptr[++Y] = 0x0f;  // 16 lines
            _ms_tmpptr[++Y] = _ms_blank_dl >> 8;
            _ms_tmpptr[++Y] = _ms_blank_dl;
            // 16 blank lines
            _ms_tmpptr[++Y] = 0x0f;  // 16 lines
            _ms_tmpptr[++Y] = _ms_blank_dl >> 8;
            _ms_tmpptr[++Y] = _ms_blank_dl;
            // 4 blank lines
            _ms_tmpptr[++Y] = 0x03;  // 4 lines
            _ms_tmpptr[++Y] = _ms_blank_dl >> 8;
            _ms_tmpptr[++Y] = _ms_blank_dl;
        } else {
            _ms_tmpptr[++Y] = 0x08; // 9 lines
            _ms_tmpptr[++Y] = _ms_blank_dl >> 8;
            _ms_tmpptr[++Y] = _ms_blank_dl;
        }
        _ms_tmpptr = _ms_b1_dll;
    }
    multisprite_start();
}

const char oneup[3] = {27, 'U' - 'A', 'P' - 'A'};
const char high[4] = {'H' - 'A', 'I' - 'A', 'G' - 'A', 'H' - 'A'};
const char beam[4] = {'B' - 'A', 'E' - 'A', 'A' - 'A', 'M' - 'A'};
const char gauge_out[17] = {45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 47};
const char gauge_in[15] = { 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48};

void rtype_init()
{
    sparse_tiling_init(tilemap_level1_data_ptrs);
    multisprite_set_charbase(brown_tiles1);
   
    // Green (background) color 
    *P3C1 = multisprite_color(0xd0); 
    *P3C2 = multisprite_color(0xd2); 
    *P3C3 = multisprite_color(0xd1); 
    
    // Beige palette
    *P4C1 = multisprite_color(0x12); 
    *P4C2 = multisprite_color(0x14); 
    *P4C3 = multisprite_color(0x16); 
    
    // Blue palette
    *P5C1 = multisprite_color(0x84); // Dark blue 
    *P5C2 = multisprite_color(0x87); // Light blue
    *P5C3 = multisprite_color(0xac); // Turquoise 

    // Rose palette
    *P6C1 = multisprite_color(0x34); // Dark Rose
    *P6C2 = multisprite_color(0x38); // Rose 
    *P6C3 = multisprite_color(0x3c); // Light Rose 

    // Grey palette 
    *P7C1 = 0x04; // Dark gray
    *P7C2 = 0x08; // Medium gray
    *P7C3 = 0x0c; // Dark gray

    multisprite_display_tiles(3 * 4, 14, oneup, 3, 5);
    multisprite_display_tiles(7 * 4, 14, display_score_str, 5, 7);
    multisprite_display_tiles(16 * 4, 14, high, 4, 5);
    multisprite_display_tiles(21 * 4, 14, display_high_score_str, 5, 7);
    multisprite_display_tiles(8 * 4, 13, beam, 4, 5);
    multisprite_display_tiles(13 * 4, 13, gauge_out, 17, 7);
    multisprite_display_tiles(14 * 4, 13, gauge_in, 1, 5);
   
    // Background display
    char c, y = 0;
    for (c = 0; c != 3; c++) {
        y += 16;
        multisprite_display_sprite_ex(0, y, background_level1, 20, 3, 0);
        multisprite_display_sprite_fast(80, y, background_level1, 24, 3);
        y += 16;
        multisprite_display_sprite_ex(0, y, background_level1_1, 20, 3, 0);
        multisprite_display_sprite_fast(80, y, background_level1_1, 24, 3);
        y += 16;
        multisprite_display_sprite_ex(-8, y, background_level1, 20, 3, 0);
        multisprite_display_sprite_fast(72, y, background_level1, 24, 3);
        y += 16;
        multisprite_display_sprite_ex(-8, y, background_level1_1, 20, 3, 0);
        multisprite_display_sprite_fast(72, y, background_level1_1, 24, 3);
    }

    // Save it
    multisprite_save();

    multisprite_enable_dli(13);
    
    sparse_tiling_display();
    multisprite_flip();
    sparse_tiling_scroll(1); // Offset of 1 compared to previous screen
    sparse_tiling_display();
    multisprite_flip();
}

void main()
{
    scroll_background_counter = 0;
    button_pressed = 0;

    joystick_init();
    display_init();
    rtype_init();
    game_init();

    do {
        scroll_background();
        sparse_tiling_scroll(2); // Offset of 2 compared to previous same buffer (1 pixel scrolling due to double buffering)

        joystick_input();
        step();
        if (update_score) {
            display_score_update(display_score_str);
            if (score >= high_score) {
                high_score = score;
                display_score_update(display_high_score_str);
            }
            update_score = 0;
        }

        multisprite_flip();
        multisprite_set_charbase(brown_tiles1);
        *CTRL = 0x50; // DMA on, 160A/B mode, Two (2) byte characters mode
    } while (1);
}

 

I'll now work on using video memory (16Kb extra RAM) to put the tiles and use immediate mode.

If I succeed and survive, I will integrate the dobkeratops' code into this example and add explosions and gameover...

RType_sprites.png

 

RType_sprites.yaml example_RType.a78

  • Like 8
Link to comment
Share on other sites

I have some interesting results when I tried to replace the font in conio.h with my own. Obviously the keywords

 

reversed scattered(8,1) font[1024]

 

do a lot of interesting things.

 

It turned out that I only needed to add mode: 320A to my yaml file. Now I have my own coloured mono fonts working :) 

 

Link to comment
Share on other sites

On 11/16/2023 at 6:55 PM, karri said:

I have some interesting results when I tried to replace the font in conio.h with my own. Obviously the keywords

 

reversed scattered(8,1) font[1024]

 

do a lot of interesting things.

 

It turned out that I only needed to add mode: 320A to my yaml file. Now I have my own coloured mono fonts working :) 

 

Yes, cc7800 uses a few keywords to map the data to ROM.

"reversed" automatically reverses the array line by line, as Maria displays everything upside down (the line counter in MARIA starts from OFFSET down to 0 for each Display List).

"Scattered" applies the right scattered memory layout. Again, Maria separates every line of gfx by 256 bytes (the most significant byte of an address if basically the line of the gfx, and the least significant byte is the gfx id or character number). So reversed and scattered takes a usual array (typically your font) and applies the right layout so that Maria can find its babies. "Holeydma" is another keyword that will select an holeydma-enabled ROM zone or not. Don't forget that  Holeydma-enabled memory is scarse (only 3 4kb memory zones : 0xa000, 0xc000 and 0e000).

And yes, in sprites7800, mode 160A is the default. You have to specify 320A mode somewhere if your gfx is a font.

Have fun !

  • Thanks 1
Link to comment
Share on other sites

Here is a first example of using 320A font with colours to make the text easier to understand. The font is the vga-font. I did a pull request to add it...

Nyttkuva2023-11-18142030.thumb.png.c5591f0cb2efe167da6099cdd05f910e.png

 

It is a nice feature in your conio.h implementation to be able to request the text colour and the system then allocates the actual display lists to match the request.

  • Like 3
Link to comment
Share on other sites

7 hours ago, karri said:

Here is a first example of using 320A font with colours to make the text easier to understand. The font is the vga-font. I did a pull request to add it...

 

Pull request accepted ! Your screenshot is looking cool.

  • Thanks 1
Link to comment
Share on other sites

Alleluia !

The Atari 7800 is coming back in all its glory ! You can throw your PS5 through the window. It will look soon so 2020...

I've implemented horizontal sparse tiling scrolling using extra RAM video memory. It's a bit miracle that it's working, but it's working...

What exactly does it do ?

- It transfers in real time the tiles in ROM to RAM, so that all the display is done only using immediate (or direct, call it like you want) display mode, paving the way to 160B R-Type background

- This really saves a lot of CPU ! Roughly, transferring 16 tiles to RAM costs approx. 10000 6502 cycles, but this saves 160 pixels * 16 tiles * 16 lines * 1 CPU DMA cycle ~ 40000 cycles of CPU blocked by DMA. So the yielding is 4 to 1, for a scrolling of 1 pixel per frame. And 8 to 1 for a scrolling of 0.5 pixel per frame (like in the example), more realistic for R-Type.

- I've also implemented vertical tiles mirroring, both in the ROM to RAM transfer routine, but also in Tiles7800. This doubles the number of tiles actually usable.

- It opens the way to using 256 different 160B tiles, i.e. 16kb of tiles, since the tiles don't need to be packed into 4kb.

- And last be not least, the ROM to RAM routine uses a massive 105 bytes of zeropage memory ! But with cc7800 automatic zeropage registers allocation (which shares the zeropage registers among the routines according to the call tree), it just costs 0 zeropage memory in the end.

- It uses 12kb of RAM as video memory ring buffer. There are 4kb still available for some other funny stuff !

- ASM code free

- The bad news is that the transfer is not balanced among frames. It can consume up to 1/3 frame of CPU when a tileset of 16 tiles is transformed into a 32 bytes of immediate mode display... But 60fps game keeps being achievable (maybe with some rare frame skipping we can deal with).

 

Mirrored tiles are signaled in the YAML file (describing the tiles for C code generation) by the "mirror: !Vertical" YAML entry.

...
sprite_sheets:
  - image: RType_tiles_mirror.png
    bank: 1
    mirror: !Vertical
    sprites:
      - name: brown_tiles1 
        top: 0
        left: 0
...

With Tiled, vertically mirrored tiles can then be used just as normal tiles.

 

The routine itself that transfers from ROM to RAM, which looks ugly (extract from headers/sparse_tiling.h) :

#ifdef MULTISPRITE_USE_VIDEO_MEMORY
void _sparse_tiling_ROM_to_RAM(char *sptr, char w, char mode)
{
    char low, high, len, len2, tmp;

    len2 = (mode)?(w << 1):w; // Number of entries in chptr
    len = len2 << 1; // Number of actual bytes

    low = _sparse_tiling_vmem_ptr_low;
    high = _sparse_tiling_vmem_ptr_high;
    // Allocate n bytes in vmem
    tmp = low - len;
    if (tmp < 0) {
        low = -len;
        high += 16;
        if (high == 0x70) high = 0x40;
    } else low = tmp;

    _sparse_tiling_vmem_ptr_low = low;
    _sparse_tiling_vmem_ptr_high = high;

    char *vmemptr0, *vmemptr1, *vmemptr2, *vmemptr3, *vmemptr4, *vmemptr5, *vmemptr6, *vmemptr7, *vmemptr8, *vmemptr9, *vmemptr10, *vmemptr11, *vmemptr12, *vmemptr13, *vmemptr14, *vmemptr15;
    char *chptr0, *chptr1, *chptr2, *chptr3, *chptr4, *chptr5, *chptr6, *chptr7, *chptr8, *chptr9, *chptr10, *chptr11, *chptr12, *chptr13, *chptr14, *chptr15;
    char vtmp1[16], vtmp2[16];
   
    Y = low;
    X = high; 
    vmemptr0 = Y | (X++ << 8);
    vmemptr1 = Y | (X++ << 8);
    vmemptr2 = Y | (X++ << 8);
    vmemptr3 = Y | (X++ << 8);
    vmemptr4 = Y | (X++ << 8);
    vmemptr5 = Y | (X++ << 8);
    vmemptr6 = Y | (X++ << 8);
    vmemptr7 = Y | (X++ << 8);
    vmemptr8 = Y | (X++ << 8);
    vmemptr9 = Y | (X++ << 8);
    vmemptr10 = Y | (X++ << 8);
    vmemptr11 = Y | (X++ << 8);
    vmemptr12 = Y | (X++ << 8);
    vmemptr13 = Y | (X++ << 8);
    vmemptr14 = Y | (X++ << 8);
    vmemptr15 = Y | (X++ << 8);

    X = _sparse_tiling_charbase;
    chptr0 = X++ << 8;
    chptr1 = X++ << 8;
    chptr2 = X++ << 8;
    chptr3 = X++ << 8;
    chptr4 = X++ << 8;
    chptr5 = X++ << 8;
    chptr6 = X++ << 8;
    chptr7 = X++ << 8;
    chptr8 = X++ << 8;
    chptr9 = X++ << 8;
    chptr10 = X++ << 8;
    chptr11 = X++ << 8;
    chptr12 = X++ << 8;
    chptr13 = X++ << 8;
    chptr14 = X++ << 8;
    chptr15 = X++ << 8;

    for (Y = 0; Y != len2; Y++) {
        tmp = Y;
        Y = sptr[Y];
        X = Y & 1;
        if (X) { 
            // Vertical mirroring)
            Y--;
        }

        vtmp1[0] = chptr0[Y];
        vtmp1[1] = chptr1[Y];
        vtmp1[2] = chptr2[Y];
        vtmp1[3] = chptr3[Y];
        vtmp1[4] = chptr4[Y];
        vtmp1[5] = chptr5[Y];
        vtmp1[6] = chptr6[Y];
        vtmp1[7] = chptr7[Y];
        vtmp1[8] = chptr8[Y];
        vtmp1[9] = chptr9[Y];
        vtmp1[10] = chptr10[Y];
        vtmp1[11] = chptr11[Y];
        vtmp1[12] = chptr12[Y];
        vtmp1[13] = chptr13[Y];
        vtmp1[14] = chptr14[Y];
        vtmp1[15] = chptr15[Y];
        Y++;
        vtmp2[0] = chptr0[Y];
        vtmp2[1] = chptr1[Y];
        vtmp2[2] = chptr2[Y];
        vtmp2[3] = chptr3[Y];
        vtmp2[4] = chptr4[Y];
        vtmp2[5] = chptr5[Y];
        vtmp2[6] = chptr6[Y];
        vtmp2[7] = chptr7[Y];
        vtmp2[8] = chptr8[Y];
        vtmp2[9] = chptr9[Y];
        vtmp2[10] = chptr10[Y];
        vtmp2[11] = chptr11[Y];
        vtmp2[12] = chptr12[Y];
        vtmp2[13] = chptr13[Y];
        vtmp2[14] = chptr14[Y];
        vtmp2[15] = chptr15[Y];
        Y = tmp << 1;
        
        if (X) {
            vmemptr0[Y] = vtmp1[15];
            vmemptr1[Y] = vtmp1[14];
            vmemptr2[Y] = vtmp1[13];
            vmemptr3[Y] = vtmp1[12];
            vmemptr4[Y] = vtmp1[11];
            vmemptr5[Y] = vtmp1[10];
            vmemptr6[Y] = vtmp1[9];
            vmemptr7[Y] = vtmp1[8];
            vmemptr8[Y] = vtmp1[7];
            vmemptr9[Y] = vtmp1[6];
            vmemptr10[Y] = vtmp1[5];
            vmemptr11[Y] = vtmp1[4];
            vmemptr12[Y] = vtmp1[3];
            vmemptr13[Y] = vtmp1[2];
            vmemptr14[Y] = vtmp1[1];
            vmemptr15[Y] = vtmp1[0];
            Y++;
            vmemptr0[Y] = vtmp2[15];
            vmemptr1[Y] = vtmp2[14];
            vmemptr2[Y] = vtmp2[13];
            vmemptr3[Y] = vtmp2[12];
            vmemptr4[Y] = vtmp2[11];
            vmemptr5[Y] = vtmp2[10];
            vmemptr6[Y] = vtmp2[9];
            vmemptr7[Y] = vtmp2[8];
            vmemptr8[Y] = vtmp2[7];
            vmemptr9[Y] = vtmp2[6];
            vmemptr10[Y] = vtmp2[5];
            vmemptr11[Y] = vtmp2[4];
            vmemptr12[Y] = vtmp2[3];
            vmemptr13[Y] = vtmp2[2];
            vmemptr14[Y] = vtmp2[1];
            vmemptr15[Y] = vtmp2[0];
        } else {
            vmemptr0[Y] = vtmp1[0];
            vmemptr1[Y] = vtmp1[1];
            vmemptr2[Y] = vtmp1[2];
            vmemptr3[Y] = vtmp1[3];
            vmemptr4[Y] = vtmp1[4];
            vmemptr5[Y] = vtmp1[5];
            vmemptr6[Y] = vtmp1[6];
            vmemptr7[Y] = vtmp1[7];
            vmemptr8[Y] = vtmp1[8];
            vmemptr9[Y] = vtmp1[9];
            vmemptr10[Y] = vtmp1[10];
            vmemptr11[Y] = vtmp1[11];
            vmemptr12[Y] = vtmp1[12];
            vmemptr13[Y] = vtmp1[13];
            vmemptr14[Y] = vtmp1[14];
            vmemptr15[Y] = vtmp1[15];
            Y++;
            vmemptr0[Y] = vtmp2[0];
            vmemptr1[Y] = vtmp2[1];
            vmemptr2[Y] = vtmp2[2];
            vmemptr3[Y] = vtmp2[3];
            vmemptr4[Y] = vtmp2[4];
            vmemptr5[Y] = vtmp2[5];
            vmemptr6[Y] = vtmp2[6];
            vmemptr7[Y] = vtmp2[7];
            vmemptr8[Y] = vtmp2[8];
            vmemptr9[Y] = vtmp2[9];
            vmemptr10[Y] = vtmp2[10];
            vmemptr11[Y] = vtmp2[11];
            vmemptr12[Y] = vtmp2[12];
            vmemptr13[Y] = vtmp2[13];
            vmemptr14[Y] = vtmp2[14];
            vmemptr15[Y] = vtmp2[15];
        }
        Y = tmp;
    } // ~600 cycles x 16 (max) = ~10000 cycles = 5.6ms (out of 16ms per frame).
}
#endif

 

And the example that's working with vertical mirroring (examples/example_horizontal_scrolling.c) :

#include "string.h"
#define HORIZONTAL_SCROLLING
#define _MS_BOTTOM_SCROLLING_ZONE 1
#define MULTISPRITE_USE_VIDEO_MEMORY
#include "sparse_tiling.h"

#ifdef MULTISPRITE_USE_VIDEO_MEMORY
// Generated from sprites7800 RType_tiles_mirror.yaml
#include "example_RType_tiles_mirror.c"
// Generated from tiles7800 --sparse RType_tiles_mirror.yaml --varname tilemap_level1 RType_level1_mirror.tmx -m 16
#include "example_RType_level1_mirror.c"
#else
// Generated from sprites7800 RType_tiles.yaml
#include "example_RType_tiles.c"
// Generated from tiles7800 --sparse RType_tiles.yaml --varname tilemap_level1 RType_level1.tmx -m 16
#include "example_RType_level1.c"
#endif

char scroll_background_counter1, scroll_background_counter2;

void scroll_background(char speed)
{
    char c;
    signed char pos1, pos2, pos3, pos4;
    scroll_background_counter1++;
    if (scroll_background_counter1 == speed) {
        scroll_background_counter1 = 0;
        scroll_background_counter2++;
        if (scroll_background_counter2 == 16) scroll_background_counter2 = 0;
    }
    pos1 = -scroll_background_counter2;
    pos2 = pos1 + 80;
    pos3 = pos1 - 8;
    if (pos3 < -16) pos3 += 16;
    pos4 = pos3 + 80;
    if (_ms_buffer) {
        X = _MS_DLL_ARRAY_SIZE + 1;
    } else X = 1;
    
    _ms_tmpptr = _ms_dls[X];
    for (c = 0; c != 3; c++) {
        // Modify bytes 4 and 8 of the DLL entries (x position of background sprites=
        _ms_tmpptr[Y = 4] = pos1;
        _ms_tmpptr[Y = 8] = pos2;
        _ms_tmpptr = _ms_dls[++X];
        _ms_tmpptr[Y = 4] = pos1;
        _ms_tmpptr[Y = 8] = pos2;
        _ms_tmpptr = _ms_dls[++X];
        _ms_tmpptr[Y = 4] = pos3;
        _ms_tmpptr[Y = 8] = pos4;
        _ms_tmpptr = _ms_dls[++X];
        _ms_tmpptr[Y = 4] = pos3;
        _ms_tmpptr[Y = 8] = pos4;
        _ms_tmpptr = _ms_dls[++X];
    }
}

void main()
{
    scroll_background_counter1 = 0;
    scroll_background_counter2 = 0;

    multisprite_init();
#ifdef MULTISPRITE_USE_VIDEO_MEMORY
    sparse_tiling_init_vmem(tilemap_level1_data_ptrs, brown_tiles1);
#else
    sparse_tiling_init(tilemap_level1_data_ptrs);
#endif
    multisprite_set_charbase(brown_tiles1);
    
    // Green (background) color 
    *P3C1 = multisprite_color(0xd0); 
    *P3C3 = multisprite_color(0xd1); 
    *P3C2 = multisprite_color(0xd2); 
    
    // Beige palette
    *P4C1 = multisprite_color(0x12); 
    *P4C2 = multisprite_color(0x14); 
    *P4C3 = multisprite_color(0x16); 
    
    // Blue palette
    *P5C1 = multisprite_color(0x84); // Dark blue 
    *P5C2 = multisprite_color(0x87); // Light blue
    *P5C3 = multisprite_color(0xac); // Turquoise 

    // Rose palette
    *P6C1 = multisprite_color(0x34); // Dark Rose
    *P6C2 = multisprite_color(0x38); // Rose 
    *P6C3 = multisprite_color(0x3c); // Light Rose 

    // Grey palette 
    *P7C1 = 0x04; // Dark gray
    *P7C2 = 0x08; // Medium gray
    *P7C3 = 0x0c; // Dark gray

    // Background display
    char c, y = 0;
    for (c = 0; c != 3; c++) {
        y += 16;
        multisprite_display_sprite_ex(0, y, background_level1, 20, 3, 0);
        multisprite_display_sprite_fast(80, y, background_level1, 24, 3);
        y += 16;
        multisprite_display_sprite_ex(0, y, background_level1_1, 20, 3, 0);
        multisprite_display_sprite_fast(80, y, background_level1_1, 24, 3);
        y += 16;
        multisprite_display_sprite_ex(-8, y, background_level1, 20, 3, 0);
        multisprite_display_sprite_fast(72, y, background_level1, 24, 3);
        y += 16;
        multisprite_display_sprite_ex(-8, y, background_level1_1, 20, 3, 0);
        multisprite_display_sprite_fast(72, y, background_level1_1, 24, 3);
    }

    // Save it
    multisprite_save();

    sparse_tiling_display();
    multisprite_flip();
    sparse_tiling_scroll(1); // One pixel offset between the 2 buffers
    sparse_tiling_display();
    multisprite_flip();

    do {
        scroll_background(4);
        sparse_tiling_scroll(1); // Scroll 1 pixels to the right for this buffer (so 0.5 pixel from frame to frame due to double buffering)
        multisprite_flip();
    } while (1);
}

 

@Defender_2600, if you have some little time ahead, please send me some 160B tiles (with the color id, please, like you have done for the dobkeratops). I'll try to fit them in ASAP. And yes, the eyes and mouthes of Dokberatops are also welcome !

Meanwhile, I'll try to integrate some more features to make a R-Type mockup (the boss, explosions, gameover)

 

Btw, attached below are the latest versions of cc7800, sprites7800 and tiles7800 for Windows, as well as the resources I've used to build the scrolling example.

 

 

tiled_RTYpe_level1_mirror.png

RType_tiles_mirror.png

sprites7800.exe tiles7800.exe cc7800-0.2.17-x86_64.msi RType_level1_mirror.tmx RType_tiles_mirror.tsx RType_tiles_mirror.yaml example_horizontal_scrolling_vmem_mirroring.a78

  • Like 10
Link to comment
Share on other sites

Hey @bsteux, glad to see your great progress!

 

So, I looked in depth at the graphics of the first two levels in order to better organize my work in perspective and I think that, with the necessary attention, we can obtain a rather impressive result. I'm currently busy with two old 7800 projects but, if you're not in a hurry, I might be able to get you the complete graphics of the first level (sprites, tiles and palettes), progressing step by step.

 

I have almost all the tiles of the first level (85% complete), then we can continue with some sprites (player spaceship, red enemy spaceships, explosions...). The first palette 160B will be completely used for the tiles, the second palette 160B (and 160A *4) for all the sprites, the third palette 160B is already used for the Dobkeratops in the 11 horizontal zones, therefore leaving the first palette 160B (tiles) only for the upper and lower area, for a total of 36 colors + background color.

 

And yes, I saw that you cut some pixels in the lower area of the Dobkeratops so that it could fit into 11 zones (well done!), I made that graphic just as a demonstration, I didn't think anyone would bring it to life. I'll get back to you as soon as I can.

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

On 11/21/2023 at 9:47 PM, Defender_2600 said:

Hey @bsteux, glad to see your great progress!

 

So, I looked in depth at the graphics of the first two levels in order to better organize my work in perspective and I think that, with the necessary attention, we can obtain a rather impressive result. I'm currently busy with two old 7800 projects but, if you're not in a hurry, I might be able to get you the complete graphics of the first level (sprites, tiles and palettes), progressing step by step.

 

I have almost all the tiles of the first level (85% complete), then we can continue with some sprites (player spaceship, red enemy spaceships, explosions...). The first palette 160B will be completely used for the tiles, the second palette 160B (and 160A *4) for all the sprites, the third palette 160B is already used for the Dobkeratops in the 11 horizontal zones, therefore leaving the first palette 160B (tiles) only for the upper and lower area, for a total of 36 colors + background color.

 

And yes, I saw that you cut some pixels in the lower area of the Dobkeratops so that it could fit into 11 zones (well done!), I made that graphic just as a demonstration, I didn't think anyone would bring it to life. I'll get back to you as soon as I can.

Great. I'm eager to see the result in action. Just a few notes to guide your work :

- Tiles in the same tileset (horizontal set of continuous tiles) must be in the same 4KB zone, i.e. 64 tiles group (due to 6502 8-bit addressing limit).

- For the R-Type "Circle Fire" I've already implemented (see the video attached), Red and Blue must be in the same 160A palette, because "Circle Fire" make some big sprites. I've tested it in 160A, and it still works with the Dobkeratops (by removing the background).

 

 

- I must switch from 160B tiles palettes to 160B dobkeratops palette (and keep the second 12 colors palette for the sprites), as well as switch from Bank1 where I put the tiles to Bank2 where I put the Dobkeratops... The transition is not nice at the moment (I fade out the background and clear the screen...). I need to find a way to display some tiles along with the dobkeratops, but at the moment, I have no idea how to proceed... Not the same bank, not the same palette...

This is work in progress. See examples/example_RType2.c on github for the code.

  • Like 5
Link to comment
Share on other sites

Hi,

Some news about the progress on the R-Type mockup (example_RType2.c) :

- Finally, I've used the remaining 4Kb of RAM to store the dobkeratops - which was a little stripped down to fit (12kb were already use as video buffer for spare tiling scrolling). The big dobkeratops sprites is moved from bank2 to RAM so that it can now be used from bank0 and 1.

- I switch back and forth from tiles 160B palette to dobkeratops 160B palette in the interrupt routine (DLI). This effectively makes a screen with 37 different colors displayed at once (or not far from it)... All this smoothess the transition to the boss, which looks more natural than before.

I've also refactored a little bit headers/multisprite.h (now v0.4) to allow for partially off-screen sprites display (at no cost, but only for 16 or less pixels high sprites, not for big sprites). It doesn't work when vertical scrolling is on (sprites have to be in bounds in that case).

Attached is a video of the current state, filmed by my 9 years old son Paul (so don't be too critic. It's his first official shooting). My Concerto is back ! It had lost its firmware during holidays... Flashed it with a USB cable, and it's back as new !

See you later. Still have to add explosions and gameover, and also the beam meter...

Ah yes, @Defender_2600, would it be possible to fit the tiles in 8kb i.e. 128 8x16 tiles (+ free mirroring) ? I'm afraid it will be complicated to use more memory (I have to put the enemy sprites + the dobkeratops tail animation + the level definition in the same bank... These 8 bits machine are so tight !)

example_RType.a78

  • Like 10
Link to comment
Share on other sites

Hi,

Here is a new update on R-Type. Still no explosions nor gameover, but I've added some sound !

I've grabbed a song from Atari SAP archive by @em_kay : R-Type Deep Mix, and I've tried to fit it in...

Well, it was not so easy... The song is big (6kb), and the RMT player code also (4kb). So I moved all this (along with the sfx) to bank7, in order to relax a little on bank0 (the main bank in cc7800, the one that stays at 0xc000 whatever the weather is). The problem was how to switch from bank1 (which stores R-Type level 1 data and gfx) to bank7 at every frame ? Well, the scoreboard !

I've put all the sound machinery in the scoreboard display DLI, and moved all the scoreboard gfx (the R-Type font and the beam meter) to bank7, so that everything is OK. Then I've used the RIOT timer for the first time to check that I stay long enough in bank7 for the scoreboard to display entirely... Whoo. My bank0 is now available for more code.

A glimpse at the DLI function (extract from example_RType2.c) :

void interrupt dli()
{
    if (dli_counter != -1) {
        scoreboard_and_music = 1;
        if (level_progress_high >= DOBKERATOPS_GETS_IN) {
            if (dli_counter == 0) {
                // Switch to dobkeratops palette    
                if (_ms_pal_detected) {
                    *P4C1 = 0x4c; 
                  ...
                    *P7C3 = 0x53; // Red (unused)
                } else {
                    *P4C1 = 0x3c; 
                    ...
                    *P7C3 = 0x43; // Red (unused)
                }
                *P5C3 = 0x0e; 
                *P6C1 = 0x0a; 
                *P6C2 = 0x04; 
                *P6C3 = 0x02; 
                scoreboard_and_music = 0;
                dli_counter = 1;
            } else if (dli_counter == 1) {
                // Switch to level1 palette
                if (_ms_pal_detected) {
                    // Beige palette
                    *P4C1 = 0x22; 
                    ...
                    *P6C3 = 0x4c; // Light Rose 
                } else {
                    // Beige palette
                    *P4C1 = 0x12; 
                    ...
                    *P6C3 = 0x3c; // Light Rose 
                }
                // Grey palette 
                *P7C1 = 0x04; // Dark gray
                *P7C2 = 0x08; // Medium gray
                *P7C3 = 0x0c; // Dark gray
                scoreboard_and_music = 0;
                dli_counter = 2;
            }
        }
        if (scoreboard_and_music) {
            // Set the RIOT timer to 32
            do {
                *TIM64T = 32 + 10;
            } while (*INTIM != 31 + 10);
            
            *CTRL = 0x43; // DMA on, 320A/C mode, One (1) byte characters mode

            // Play the music
            if (sfx_to_play) {
                sfx_schedule(sfx_to_play);
                sfx_to_play = NULL;
            }
            sfx_play();
#ifdef POKEY_MUSIC
            // This will switch to bank7
            pokey_play();
#endif
            // Wait for the end of timer
            while (*INTIM >= 10); // We may miss the 0 due to DMA. Take a 10 margin

            // Go back to the right rombank
            *ROM_SELECT = rom_bank;
            dli_counter = -1;
        }
    }
}

 

Second novelty this week, a new utility in tools7800 : rmt2cc7800, which generates the C code (well, the C array) for a given RMT file (instead of going through assembly). rmt2cc7800 is written in Rust (obviously) and the windows binary is provided in attachment.

 

Third novelty is the support for display lists of different max size depending on the zone, through the _MS_DL_MALLOC macro. For instance, in the R-Type example, _MS_DL_MALLOC is defined as :

#define _MS_DL_MALLOC(line) ((line < 8)?64:(line < 13)?128:32) 

which allocates 64 bytes for DLs in the upper part of the screen, and 128 bytes for the DLs in the lower part of screen (where the dobkeratops tail lies), and 32 for the scoreboard display. This allows to adjust memory usage to the game, as RAM resources are limited. Note that _MS_DL_MALLOC is resolved at compiled time and used to reserve static arrays.

 

Fourth novelty is the support by cc7800 0.2.18 of bank bracketing, i.e. you can specify quickly the destination bank by simple brackets, as in the example :

bank7 {
#include "sfx.h"
#ifdef POKEY_MUSIC
#define POKEY_AT_450
#include "rmtplayer.h"
#include "RMT_RType.c"
#endif
}

This puts all the sfx and RMT code, as well as the music in bank7. You don't have to specify the bank for each statement. This is non standard C, but frankly I don't care : bankswitching is a the hearth of Atari 7800 development, and it should be quickly setup.

 

No video this time, as my concerto doesn't support Pokey at 0x450 (0x4000 is used for video RAM). The demo is only working in a7800. Listen to this music, it's so cool !

Next time, no way to avoid it: explosions and gameover. Before moving probably back to cc2600 for a while, as it needs its multisprite.h header.

 

R-Type.a78 cc7800-0.2.18-x86_64.msi rmt2cc7800.exe

  • Like 7
Link to comment
Share on other sites

This is insane! I love it. Great work...

 

All I have to say is... look at this port of R-Type on the NES for a comparison. I don't think this is an official port, but it appears to be a recent port. With the modern tools we have, this should have looked better.

 

But, @bsteux this should make you feel even better about your work.

 

image.thumb.png.ea46ac74e8a58fa09998f3f08e6298c7.png

Link to comment
Share on other sites

  • 2 weeks later...
On 12/4/2023 at 9:44 PM, saxmeister said:

This is insane! I love it. Great work...

 

All I have to say is... look at this port of R-Type on the NES for a comparison. I don't think this is an official port, but it appears to be a recent port. With the modern tools we have, this should have looked better.

 

But, @bsteux this should make you feel even better about your work.

 

image.thumb.png.ea46ac74e8a58fa09998f3f08e6298c7.png

Yes, compared to the 1986 NES Gradius port which is quite good, this one is surprisingly very ugly... Should be called R-Puke.

Link to comment
Share on other sites

Hi,

Here's my last cc7800 post for 2023 : "R-Type Christmas" POC (Proof of Concept).

The code for a R-Type proto is quite complete now : you have the enemy spawning, the explosions, the music, the powerup, the boss, the gameover, and a "Merry Christmas" to all AtariAge members !

There's still a lot of room for adding graphics (160B background by @Defender_2600) and code (1.5Kb free in bank 0). Don't hesitate to have a look at the source code on Github and enhance it.

For 2024 (after a little bit of cc2600 workout), I plan to add the following features :

- Support for TIATracker songs (in parallel to cc2600)

- Support for paddles (Revenge of Doh POC ?)

- Support for 160A & 160B bitmaps (Qix or Tempest POC ?)

- Support for 3d projections (Rotating cube?)

- And some documentation for all of this...

The Atari 7800 is really a fantastic machine and I've had lot of pleasure writing and sharing this code along this 2023 year.

I wish to all of you a Merry Christmas, and see you very soon !

 

P.S: I've attached the latest version of sprites7800, which had a bug with 320C sprites (the lives on the bottom left of R-Type are 320Csprites... you can't compile the code if you don't use this latest version of sprites7800)

RType_sprites.png

christmas.png

example_RType_christmas.a78

sprites7800.exe

  • Like 11
  • Thanks 4
Link to comment
Share on other sites

Wow. Just... wow. This is what I knew the little machine could do. This is beautiful. There are so many great arcade conversions on this system and this one looks better than any other home conversion I've seen until you get to the TG-16/PC Engine.

 

A great present for us all... Thanks!

  • Thanks 1
Link to comment
Share on other sites

9 hours ago, bsteux said:

That's amazing!

  • Thanks 1
Link to comment
Share on other sites

13 hours ago, bsteux said:

Hi,

Here's my last cc7800 post for 2023 : "R-Type Christmas" POC (Proof of Concept).

The code for a R-Type proto is quite complete now : you have the enemy spawning, the explosions, the music, the powerup, the boss, the gameover, and a "Merry Christmas" to all AtariAge members !

There's still a lot of room for adding graphics (160B background by @Defender_2600) and code (1.5Kb free in bank 0). Don't hesitate to have a look at the source code on Github and enhance it.

For 2024 (after a little bit of cc2600 workout), I plan to add the following features :

- Support for TIATracker songs (in parallel to cc2600)

- Support for paddles (Revenge of Doh POC ?)

- Support for 160A & 160B bitmaps (Qix or Tempest POC ?)

- Support for 3d projections (Rotating cube?)

- And some documentation for all of this...

The Atari 7800 is really a fantastic machine and I've had lot of pleasure writing and sharing this code along this 2023 year.

I wish to all of you a Merry Christmas, and see you very soon !

 

P.S: I've attached the latest version of sprites7800, which had a bug with 320C sprites (the lives on the bottom left of R-Type are 320Csprites... you can't compile the code if you don't use this latest version of sprites7800)

 

RType_sprites.png

christmas.png

example_RType_christmas.a78 128.13 kB · 11 downloads

sprites7800.exe 2.6 MB · 1 download

Oh man, this combined with defender_2600's graphics would be EPIC!

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

My adventure gets close to the 32K cart setup. Would it be possible to change the allowed size to 48K?

 

This is closer to the cart designs I have today...

 

I patched the cc7800 compiler for my own needs. But it conflicts with other allocations at $4000 so I won't make a pull request.

build.rs.48kdiff

  • Thanks 1
Link to comment
Share on other sites

  • 2 weeks later...
On 12/23/2023 at 11:44 AM, karri said:

My adventure gets close to the 32K cart setup. Would it be possible to change the allowed size to 48K?

 

This is closer to the cart designs I have today...

 

I patched the cc7800 compiler for my own needs. But it conflicts with other allocations at $4000 so I won't make a pull request.

build.rs.48kdiff 2.07 kB · 3 downloads

OK. I will add 48ko ROM support to the upcoming version of cc7800 (ASAP).

Happy new year to all Atarians !

  • Thanks 1
Link to comment
Share on other sites

  • 2 weeks later...

I really think that a full 7800 port of R-Type is sorely needed. R-Type did not get ported to the NES, and was mostly ported to other consoles that were not super popular, like the TG-16. I think a good port of R-Type would give the 7800 some legitimacy.

Edited by KrunchyTC
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...