Intro to DSTwo SDK

Discussion in 'Supercard SDK' started by BassAceGold, Feb 9, 2011.

Feb 9, 2011

Intro to DSTwo SDK by BassAceGold at 5:58 AM (7,511 Views / 1 Likes) 15 replies

  1. BassAceGold
    OP

    Member BassAceGold Testicles

    Joined:
    Aug 14, 2006
    Messages:
    494
    Country:
    Canada
    About this tutorial:
    This tutorial will not teach you how to program. It will however help to familiarize you with the SDK and hopefully get you started in programming with easy to read examples.
    All tutorial parts will be linked in this post here to its corresponding post in this thread where ever they may end up as they are completed.
    In fact I wouldn't even call this a tutorial but rather a collection of examples.
    Each part will focus on a core aspect of the SDK: Text, Input, Screen drawing, and audio. All of which are covered in example that comes with the SDK, but will be broken down into easy to understand chunks.

    *All examples provided by me will be written in C.

    Requirements:

    -A text editor of some sort

    -The SDK
    download: http://down.supercard.sc/download/dstwo/to...k_v0.13beta.zip
    installation instructions: http://gbatemp.net/t273186-installing-dstwo-sdk-on-ubuntu

    -Knowledge of C (The sdk doesn't fully support C++ yet):
    Tutorials:
    http://www.cplusplus.com/doc/tutorial/ - C++ tutorial. C users can use all the tutorials up to the "Object Oriented" section for reference
    http://www.iu.hio.no/~mark/CTutorial/CTutorial.html - standard C tutorial
    many more can be found on google easily.

    -Knowledge of Makefiles
    Tutorial: http://makepp.sourceforge.net/1.19/makepp_tutorial.html

    Tutorial Parts:

    Part 1: Hello World! and Setting up a project folder with a basic makefile
    Part 2: Input
    Part 3: Screen drawing
    Part 4: audio - coming soon


    FAQ:


    Q: Why is there a FAQ section when nothing has been frequently asked in this thread yet?
    A: I can see the future.

    Q: Why is there no content in this tutorial?
    A: Not enough planning involved and the need for action. It will get filled eventually.

    Q: Can I make emulators with this tutorial?
    A: No, but it can help you familiarize with the SDK to port over other emulators.

    Q: Is there an SDK for windows?
    A: No, but you can run linux in a virtual machine in windows.

    Q:Which language should I learn/use?
    A: C is prefered for imbedded development with its speed and slightly smaller binary sizes. C++ is not fully supported in the SDK so any advanced features of it will cause issues.

    Q:Are any other languages supported?
    A:With some tinkering to gcc, one may be able to get Objective C working. Other then that, no.

    Tips & tricks:


    -coming soon

    Feel free to ask questions.
     
    Margen67 likes this.
  2. Omega_2

    Member Omega_2 GBAtemp Advanced Fan

    Joined:
    Mar 14, 2009
    Messages:
    677
    Country:
    Antarctica
    Good to see you alive, Bass.
    Hope people find your guide useful.
     
  3. BassAceGold
    OP

    Member BassAceGold Testicles

    Joined:
    Aug 14, 2006
    Messages:
    494
    Country:
    Canada
    Part 1 - Setting up a project folder and Hello World!

    Starting a project
    So assuming you have the SDK correctly installed and compiled, you are probably wondering what to do next. To get started coding, a project folder is required to hold your source files and contain the building instructions.

    The structure of a basic project folder could look something like this:


    Code:
    ProjectFolder/
    
    
    
    -src/
    -main.c
    -makefile
    

    This folder can be located anywhere and ideally should not have any spaces in the folder path.

    The most important part of this setup is the makefile which contains the instructions for compiling the code. Here is a sample makefile that can be copied and modified. All that needs to be changed is the binary name, and the location of the DSTWOSDK to where the root of the sdk is found. I won't explain how the makefile works, there is a tutorial for them in the first post.



    Code:
    #name of output binary file
    
    
    BINARY := Template
    
    #folder of sdk root- CHANGE THIS TO YOUR SETUP
    DSTWOSDK :=/mnt/DSTwo
    
    #folders with sources
    SOURCES =src
    INCLUDES =include
    
    
    # CROSS :=#
    CROSS :=/opt/mipsel-4.1.2-nopic/bin/
    
    CC =$(CROSS)mipsel-linux-gcc
    AR =$(CROSS)mipsel-linux-ar rcsv
    LD	=$(CROSS)mipsel-linux-ld
    OBJCOPY	=$(CROSS)mipsel-linux-objcopy
    NM	=$(CROSS)mipsel-linux-nm
    OBJDUMP	=$(CROSS)mipsel-linux-objdump
    
    
    FS_DIR =$(DSTWOSDK)/libsrc/fs
    CONSOLE_DIR =$(DSTWOSDK)/libsrc/console
    KEY_DIR =$(DSTWOSDK)/libsrc/key
    
    
    
    CSRC :=$(foreach dir,$(SOURCES),$(wildcard $(dir)/*.c))
    CPPSRC :=$(foreach dir,$(SOURCES),$(wildcard $(dir)/*.cpp))
    SSRC :=$(foreach dir,$(SOURCES),$(wildcard $(dir)/*.S))
    
    
    LIBS :=$(DSTWOSDK)/lib/libds2b.a -lc -lm -lgcc
    EXTLIBS :=$(DSTWOSDK)/lib/libds2a.a
    
    INC :=-I$(DSTWOSDK)/include -I$(SOURCES) -I$(FS_DIR) -I$(CONSOLE_DIR) -I$(KEY_DIR) \
    $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \
    $(foreach dir,$(SOURCES),-I$(CURDIR)/$(dir)) \
    
    
    CFLAGS := -mips32 -O3 -mno-abicalls -fno-pic -fno-builtin \
    -fno-exceptions -ffunction-sections -mlong-calls\
    -fomit-frame-pointer -msoft-float -G 4  
    
    LINKS :=$(DSTWOSDK)/specs/link.xn
    STARTS :=$(DSTWOSDK)/specs/start.S
    STARTO :=start.o
    
    
    COBJS	:=$(CSRC:.c=.o)
    CPPOBJS :=$(CPPSRC:.cpp=.o)
    SOBJS	:=$(SSRC:.S=.o)
    
    
    
    APP	:=$(BINARY).elf
    
    
    all: $(APP)
    $(OBJCOPY) -O binary $(APP) $(BINARY).bin
    $(DSTWOSDK)/tools/makeplug $(BINARY).bin $(BINARY).plg
    
    $(APP):	depend $(SOBJS) $(COBJS) $(CPPOBJS) $(STARTO) $(LINKS) $(EXTLIBS)
    $(CC) -nostdlib -static -T $(LINKS) -o $@ $(STARTO) $(SOBJS) $(COBJS) $(CPPOBJS) $(EXTLIBS) $(LIBS)
    $(EXTLIBS):
    make -C ../source/
    
    $(STARTO):
    $(CC) $(CFLAGS) $(INC) -o $@ -c $(STARTS)
    
    .c.o:
    $(CC) $(CFLAGS) $(INC) -o $@ -c $<
    .cpp.o:
    $(CC) $(CFLAGS) $(INC) -fno-rtti -fvtable-gc -o $@ -c $<
    .S.o:
    $(CC) $(CFLAGS) $(INC) -D_ASSEMBLER_ -D__ASSEMBLY__ -o $@ -c $<
    
    #---------------------------------------------------------------------------------
    clean:
    rm -fr  $(COBJS) $(CPPOBJS) *.plg *.bin
    rm depend
    
    depend:	Makefile
    $(CC) -MM $(CFLAGS) $(INC) $(SSRC) $(CSRC) $(CPPSRC) > $@
    
    include depend
    
    

    Once your project folder is setup, you are ready to start coding! To compile your project, simply open your project folder in the terminal, and type "make" (no quotes) and watch the build process.

    If you are having trouble setting up a project folder, a download for the hello world examples are available to copy below.

    Hello World

    The DSTwo sdk comes with 2 stock methods of displaying text, there is the console and tile based font.

    Here is the main.c file for a basic hello world program using the console which is explained via the comments in it.


    Code:
    //Hello World using the console
    
    
    
    
    #include //standard C functions
    #include "ds2io.h"//required for input and output
    
    
    //The DSTwo launches ds2_main instead of the standard main
    void ds2_main(void)
    {
    
    //Initialize video, audio, input and output. This will be explained indepth in the audio portion of the tutorial
    if(!ds2io_init(1024))
    ds2_plug_exit();
    
    
    //Initialize console for printf
    int err = ConsoleInit(RGB15(31,31,31),//text color is set to white
    RGB15(0,0,0), //backdrop color is set to black
    UP_SCREEN, //output console to the top screen
    10);// hold 10 screens worth of text in its buffer
    
    if(err)
    ds2_plug_exit();
    
    //print text to the console
    printf("Hello World!\n");
    
    while(1);//required to keep the program alive
    
    }
    


    Download Here

    Everything there is fairly self explanatory, so I'll leave it at that.

    Here is an example using the tile based font



    Code:
    //Hello World using a tile based font
    
    
    
    #include //standard C functions
    #include "ds2io.h"//required for input and output
    
    //This tile based font is compiled into the portion of the SDK that is not accessable to us.
    //These functions were proveded in the SDK example
    extern const unsigned char font_map[128][8];
    
    static inline void drawfont(unsigned short *addr, unsigned short f_color, unsigned short b_color, unsigned char ch)
    {
    unsigned char *dot_map;
    unsigned int j, k;
    unsigned char dot;
    unsigned short *dst;
    
    dot_map = (unsigned char*)font_map[ch&0x7F];
    
    for(j= 0; j < 8; j++)
    {
    dot = *dot_map++;
    dst = addr + j*SCREEN_WIDTH;
    for(k = 0; k < 8; k++)
    *dst++ = (dot & (0x80>>k)) ? f_color : b_color;
    }
    }
    
    void drawstring(unsigned int x, unsigned int y, enum SCREEN_ID screen, char *string,
    unsigned short f_color, unsigned short b_color)
    {
    unsigned short *scr_addr, *dst;
    
    if(screen & UP_MASK)
    scr_addr = up_screen_addr;
    else
    scr_addr = down_screen_addr;
    
    if(x>= 32 || y>= 24) return;
    
    while(*string)
    {
    dst = scr_addr + (y*8)*SCREEN_WIDTH + x*8;
    drawfont(dst, f_color, b_color, *string++);
    
    x += 1;
    if(x>= 32)
    {
    x = 0;
    y+= 1;
    if(y >= 24) break;
    }
    }
    }
    
    //The DSTwo launches ds2_main instead of the standard main
    void ds2_main(void)
    {
    
    //Initialize video, audio, input and output. This will be explained indepth in the audio portion of the tutorial
    if(!ds2io_init(1024))
    ds2_plug_exit();
    
    //No console is needed for this method to work
    //so we can just draw the text
    drawstring(5,//the font is drawn in 8 x 8 tiles, so that gives a total of  32 possible x coordinates on the screen
    5, //on the y axis, there are only 24 possible coordinates
    UP_SCREEN, //draw to the top screen, DOWN_SCREEN can be used for the bottom screen
    "Hello World!",//your text
    RGB15(31,31,31),//font color of white
    RGB15(0,0,0));//back drop color of the tile
    
    //this function is what actually draws the screen buffers to the DS's screen
    ds2_flipScreen(UP_SCREEN, //this specifies which screen to update, the choices are UP_SCREEN, DOWN_SCREEN or even DUAL_SCREEN to update both
    1); //This determines how the screen is updated
    // setting it to 0 will update the screen and not wait to finish while using a double buffer
    //setting it to 1 will update the screen and wait while using a double buffer
    //setting it to 2 will update the screen and wait while only using a single buffer
    while(1);//required to keep the program alive
    }
    
    
    Download Here -check the example above, I missed a line in the code here

    The text functions can be changed to work in a non tile based way, however I won't be covering that.
    Other than that, the example is self explanatory. If there are any misunderstandings, please consult one of the tutorials in the first post.

    That concludes part 1... which may be subject to future revisions.
     
    Margen67 likes this.
  4. Terminator02

    Member Terminator02 ヽ( 。 ヮ゚)ノ

    Joined:
    Apr 10, 2010
    Messages:
    4,517
    Location:
    Somewhere near monkat
    Country:
    United States
    u probably should have written all of this down first so u could get 5 posts in a row, anyway, not that i can use this i'm sure it will help some people out there
     
  5. alekmaul

    Member alekmaul GBAtemp Regular

    Joined:
    Nov 5, 2002
    Messages:
    104
    Location:
    Blois
    Country:
    France
    Hi all,
    I have two problems with c++ compilation.
    First of all, the line
     
  6. BassAceGold
    OP

    Member BassAceGold Testicles

    Joined:
    Aug 14, 2006
    Messages:
    494
    Country:
    Canada
    Try using this instead
    CODE.cpp.o:
    ÂÂÂÂ$(CC) $(CFLAGS) $(INC) -fno-rtti -o $@ -c $
     
    Margen67 likes this.
  7. alekmaul

    Member alekmaul GBAtemp Regular

    Joined:
    Nov 5, 2002
    Messages:
    104
    Location:
    Blois
    Country:
    France
    Of course I used that BassAceGold ^^
    I also tried to use g++ instead of gcc, but same pb ...
    I think the big pb is c++ support, someone tested it ? it seems that we do not have c++ support with dstwo [​IMG]
     
  8. Normmatt

    Member Normmatt Former AKAIO Programmer

    Joined:
    Dec 14, 2004
    Messages:
    2,135
    Country:
    New Zealand
    (.text._ZNSt7codecvtIcc11__mbstate_tEC2Ej+0x5c): more undefined references to `_Unwind_Resume' follow

    looks more like your trying to call a C++ function from C which wont work because C++ mangles the function name.
     
  9. alekmaul

    Member alekmaul GBAtemp Regular

    Joined:
    Nov 5, 2002
    Messages:
    104
    Location:
    Blois
    Country:
    France
    I found the problem.
    It seems that we need to add c++ library BEFORE others, and it works [​IMG] !
    Now, i have a problem in the code with string objects. It seems to hang when we use them [​IMG]
    Perhaps we need to include a specific dstwo file as we did for malloc function ?
     
  10. alekmaul

    Member alekmaul GBAtemp Regular

    Joined:
    Nov 5, 2002
    Messages:
    104
    Location:
    Blois
    Country:
    France
    Double post to say that problem is in string management.
    Don't know exactly how to manage that but a
    hangs the ds ... [​IMG]
     
  11. loby

    Newcomer loby Newbie

    Joined:
    Dec 17, 2010
    Messages:
    9
    Country:
    China
    Because the compiler is not perfect, and Some C++ library is not included, so C++ support is not very good.
    For the function of new and delete, you can try it in this way:

    inline void* operator new ( size_t s ) { return malloc( s ); }
    inline void* operator new[] ( size_t s ) { return malloc( s ); }
    inline void operator delete ( void* p ) { free( p ); }
    inline void operator delete[] ( void* p ) { free( p ); }
     
  12. alekmaul

    Member alekmaul GBAtemp Regular

    Joined:
    Nov 5, 2002
    Messages:
    104
    Location:
    Blois
    Country:
    France
    We already got such thing in sdk, in file ds2_malloc.h
    I think the problem is more "inside" stdc++ regarding internal malloc for string objects. And I don't think that we can do something to change that if c++ support is minimal for sdk ...
    Such a shame, i will need to modify a lot StellaDS if I want a port for DSTWO.
     
  13. BassAceGold
    OP

    Member BassAceGold Testicles

    Joined:
    Aug 14, 2006
    Messages:
    494
    Country:
    Canada
    Sorry about the lack of updates but I have become very busy lately. If anyone would like to write a section or some information feel free to do so and I will link it in the first post when I can.
     
  14. BassAceGold
    OP

    Member BassAceGold Testicles

    Joined:
    Aug 14, 2006
    Messages:
    494
    Country:
    Canada
    Part 2 - Input

    There is not a whole lot to cover in this part, so it may seem somewhat lacking as a tutorial and is really more of an example.
    Like before, most of the explaining is covered via comments in the code.
    CODE
    //Basic input support

    #include //standard C functions
    #include "ds2io.h"//required for input and output


    //The DSTwo launches ds2_main instead of the standard main
    void ds2_main(void)
    {

    //Initialize video, audio, input and output. This will be explained in depth in the audio portion of the tutorial
    if(!ds2io_init(1024))
    ds2_plug_exit();


    //Initialize console for printf
    int err = ConsoleInit(RGB15(31,31,31), RGB15(0,0,0), UP_SCREEN, 10);

    if(err)
    ds2_plug_exit();


    printf("Press Anykey\n");

    while(1)//required to keep the program alive
    {

    struct key_buf input;//struct for holding input data
    unsigned int key = getInput(&input);//update input data

    //this portion was ripped from the SDK example
    if(key)
    {//checks for every key and the touch screen
    //works similar to libnds
    if(input.key & KEY_TOUCH)
    printf("x,y = (%d, %d)\n", input.x, input.y);
    else if(input.key & KEY_LID)
    printf("KEY_LID\n");
    else if(input.key & KEY_UP)
    printf("KEY_UP\n");
    else if(input.key & KEY_DOWN)
    printf("KEY_DOWN\n");
    else if(input.key & KEY_LEFT)
    printf("KEY_LEFT\n");
    else if(input.key & KEY_RIGHT)
    printf("KEY_RIGHT\n");
    else if(input.key & KEY_L)
    printf("KEY_L\n");
    else if(input.key & KEY_R)
    printf("KEY_R\n");
    else if(input.key & KEY_A)
    printf("KEY_A\n");
    else if(input.key & KEY_B)
    printf("KEY_B\n");
    else if(input.key & KEY_X)
    printf("KEY_X\n");
    else if(input.key & KEY_Y)
    printf("KEY_Y\n");
    else if(input.key & KEY_START)
    printf("KEY_START\n");
    else if(input.key & KEY_SELECT)
    printf("KEY_SELECT\n");
    }
    }
    }


    That is it! A very simple example of querying input. You can also easily modify it to get multiple key states such as held buttons, released buttons and newly pressed buttons.
     
    Margen67 likes this.
  15. BassAceGold
    OP

    Member BassAceGold Testicles

    Joined:
    Aug 14, 2006
    Messages:
    494
    Country:
    Canada
    Part 3: Screen drawing

    Wow it's been a while since I last added to this guide. However now that I happen to have some free time, here is part 3!

    Screen drawing is an essential part of output in the SDK, it is also the slowest function you can call within the SDK and will be the major source of bottleneck in your program unless properly managed.

    About the display setup:
    The image format that gets transferred to the DS is a raw 16 bit (r5 g5 b5 a1) buffer the size of the screen (2 * 256 * 192 bytes).
    In order to speed things up, the DSTwo sdk uses a double buffering system so that while one buffer is being transferred to the DS, you can still draw to the second buffer.


    Accessing the video buffers:
    To access the buffer, there are two pointers available - down_screen_addr and up_screen_addr. These pointers can simply be accessed as an array to add or get data to or from the video buffers. eg:
    down_screen_addr[ x + y * SCREEN_WIDTH] = RGB15(31, 31, 31);//place a white pixel at x,y on the bottom screen

    The same pointer is used for accessing both buffers on screen. This will be explained below.

    Updating the screens:
    ds2_flipScreen( screen, update_type) is the function which initiates the transfer of the screen to the DS. When this function is called, the pointer for the screen desired will be updated and point to the second free buffer. So you only have access to one buffer at a time. Due to this feature, everything will need to be re-drawn every frame to ensure that both buffers are kept updated with the newest image. There are ways around this by changing the update_type in the function.

    There are 3 different ways to update a screen (in order from fastest method to slowest):

    type 0: This is the standard double buffer method, however it will not halt the program while the image is being transferred. Using this function may cause screen tearing in that it will try to update the screen as fast as it can without actually waiting for the image to complete drawing before allowing another update.

    type 1: This also uses the standard double buffer method, but with wait for the image to completely send before allowing a new transfer to be initiated, thus it is slower.

    type 2: This type will update both video buffers on the screen at a time as if it were just a single video buffer while waiting for the image to completely draw, thus it is the slowest update type.


    So that is the basic gist of video output on the DSTwo. The example below is a short program that lets you draw on the bottom screen using the stylus.




    Code:
    //A simple drawing program using the stylus
    
    
    
    #include //standard C functions
    #include "ds2io.h"//required for input and output
    
    //data size of the screen
    #define SCREEN_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT)
    
    /*======================================
    Finer stylus controls (detects newpress, release and holds)
    ======================================*/
    typedef struct
    {
    unsigned char Held, Released, Newpress;
    short X, Y, OldX, OldY, downTime, downTimeOld;
    } _stylus_;
    _stylus_ Stylus;
    
    #define UPDATESTYLUS(type, var) do{\
    type = (var & KEY_TOUCH)>>12;\
    }while(0)
    
    unsigned short CompleteStylus, ExStylus, TempStylus;
    
    void UpdateStylus(void)
    {
    ExStylus = CompleteStylus;
    
    struct key_buf rawin;
    
    ds2_getrawInput(&rawin);
    CompleteStylus = rawin.key;
    
    UPDATESTYLUS(Stylus.Held, CompleteStylus);
    UPDATESTYLUS(Stylus.Released, (ExStylus & (~CompleteStylus)));
    UPDATESTYLUS(Stylus.Newpress, (CompleteStylus & (~ExStylus)));	
    Stylus.X = rawin.x;
    Stylus.Y = rawin.y;
    }
    
    
    
    
    /*=====================================
    Some drawing functions
    =====================================*/
    void Screen_Put_Pixel(unsigned short *Dest, short x, short y, unsigned short color)
    {
    if(x>=0 && x< SCREEN_WIDTH && y>=0 && yabs(longLen))
    {
    int swap=shortLen;
    shortLen=longLen;
    longLen=swap;				
    yLonger=1;
    }
    int decInc = 0;
    
    if (longLen==0) decInc=0;
    else decInc = (shortLen 0)
    {
    longLen+=y;
    for (j=0x8000+(x16, y, color);
    j-=decInc;
    }
    return;	
    }
    
    if (longLen>0)
    {
    longLen+=x;
    for (j=0x8000+(y16, color);
    j-=decInc;
    }
    
    }
    
    
    void Stylus_Draw(unsigned short *Dest, unsigned short color)
    {
    if(Stylus.Newpress)
    {
    Screen_Put_Pixel(Dest, Stylus.X, Stylus.Y, color);
    Stylus.downTimeOld = Stylus.downTime - 1;
    }
    else if(Stylus.Held)
    {
    if(Stylus.downTimeOld != Stylus.downTime - 1){
    Stylus.OldX = Stylus.X;
    Stylus.OldY = Stylus.Y;
    }
    Screen_Draw_Line(Dest, Stylus.X, Stylus.Y, Stylus.OldX, Stylus.OldY, color);
    }
    
    Stylus.OldX = Stylus.X;
    Stylus.OldY = Stylus.Y;
    
    Stylus.downTimeOld = Stylus.downTime++;
    }
    
    
    
    
    /*=======================================
    create a buffer to hold our drawing.
    Using the screen buffer is not recommeneded as update type 2
    would be needed which is very slow for our needs here
    ===========================================*/
    unsigned short Screen_Buffer[SCREEN_SIZE];
    
    //The DSTwo launches ds2_main instead of the standard main
    void ds2_main(void)
    {
    
    //Initialize video, audio, input and output. This will be explained indepth in the audio portion of the tutorial
    if(ds2io_init(1024))
    ds2_plug_exit();
    
    //Initialize console for printf
    int err = ConsoleInit(RGB15(31,31,31), RGB15(0,0,0), UP_SCREEN, 10);						
    if(err)
    ds2_plug_exit();
    
    printf("Use the Stylus to draw!\n");
    
    while(1)//required to keep the program alive
    {
    if(Stylus.Released)
    {	//clear screen on stylus release
    memset(Screen_Buffer, 0, sizeof(Screen_Buffer));
    ds2_clearScreen( DOWN_SCREEN, RGB15(0,0,0));
    ds2_flipScreen( DOWN_SCREEN, 1);// we want a full clear
    }
    else
    {
    Stylus_Draw(Screen_Buffer, RGB15(31,0,0));
    if(Stylus.Newpress || Stylus.Held)
    {
    memcpy(down_screen_addr, Screen_Buffer, SCREEN_SIZE * 2);
    ds2_flipScreen( DOWN_SCREEN, 0);
    mdelay(9);//very crude method to prevent over polling the key detection which causes input lag
    }
    }
    
    UpdateStylus();
    }
    }
    

    Example can be downloaded here


    Couple of tips to keep in mind when drawing:
    -Updating both screens at once cuts the fps in half
    -For long continuous screen updates, update type 0 works best, for single frame updates, the rest of the types will work just fine
     
    Margen67 and Terminator02 like this.
  16. BassAceGold
    OP

    Member BassAceGold Testicles

    Joined:
    Aug 14, 2006
    Messages:
    494
    Country:
    Canada
    Just re-uploaded examples and updated the links in each post.
     
    Margen67 likes this.

Share This Page