Jump to content
IGNORED

Smelly Adventures


cubanismo

Recommended Posts

OK, I figured that mysterious failure out. It was my error. Osh is off the hook ?

 

When I got more parts in, I finished off the last board and ran into the same problem. This made me mad, and also made me doubt the board manufacturing error theory even more (Osh is not particularly cheap, and their main selling point is their quality, so I was doubting it to begin with). I desoldered the CPLD and reasoned that if that chip was causing a short between VCC/GND, there must be a short across two of the VCC/GND pins on the chip as well, and there was on exactly one VCC pin on all the broken ones that I'd saved. Always the same pin. One out of the four VCC pins on the chip was shorted through to its ground lines. It also seemed unlikely that 3 different Xilinx chips had failed in exactly the same way when all the previous ones from the batch had worked fine. It very much looked like what had happened was something blew out while I was handling them.

 

Recall that my first encounter with the failure was just after flashing the CPLD, while I was removing the power connectors (alligator clips) that I use to power the boards on my bench for initial testing and programming. If you look way back at post #17 on this thread, you can also see how I prevent this from shorting the non-power pins on the back of the edge connector, since alligator clips would contact both sides of it by default: I just wrapped a bit of cheap masking tape around one side of the alligator clips.

 

After building ~7 boards this way, it appears that tape had worn through on the +5V alligator clip just as I was removing it. On very close visual inspection, this was readily apparent. I'd sent a relatively large amount of 5V power through the ED4 and ED11 lines on side A of the connector, and the CPLD did not respond well to that. As a temporary fix, I doubled up the tape, soldered a fresh CPLD on the last board, and it worked great. Problem solved. Like I say, I haven't gotten through a single batch of builds yet without running into something dumb like this, but each one is a lesson learned. To prevent this in the future, I came up with a more reliable way to ensure the clips don't short anything:

 

skunk_bracket_1.thumb.jpg.19726ff14fa094fead29da6d9b9a28ca.jpg

 

This is a little bracket I 3D printed which has grooves I can slide the boards into:

 

skunk_bracket_2.thumb.jpg.e70515323044d01c90bf42695d35a76c.jpg

 

Then flip it over and it has another pair of grooves I can slide the alligator clips into, leaving them perfectly aligned with the VCC and GND pins on side B of the edge connector:

 

skunk_bracket_3.thumb.jpg.7f0099c67cf6bec770706c295ecd4160.jpg

 

skunk_bracket_4.thumb.jpg.841a203e5b780f05f2c616319e728b42.jpg

 

skunk_bracket_5.thumb.jpg.5ef728b14af57c75d71aa6229f82d456.jpg

 

Managed to line it up just about right on the first go here (Thanks calipers!), but I got a little expansion on the printer, so the clips fit pretty snug:

 

skunk_bracket_6.thumb.jpg.5a663836ce1456f45553691909ab3ad0.jpg

 

The one pictured here worked, but in the end I enlarged the grooves a bit and printed a second one that fit much better. I need the practice on the printing side anyway. With the second version, I wedged the clips on as sloppily as I could, then checked for shorts to all the neighboring pins and couldn't find any. Seems like a good enough solution, and much more robust than the masking tape.

 

Of course, I could have just broken down and bought or built a proper bench power supply to feed 3.3v directly to the JTAG header, but where's the fun in that? Also, this way I still get to test the actual board power delivery circuitry a bit before I drop it into my Jaguar, which is comforting.

  • Like 4
Link to comment
Share on other sites

There's something I hadn't noticed before:

image.png.ed81bba50ced125f1da44710831175b4.png

 

I wonder if there's a risk that those thin traces could be damaged by repeated insertions, or shorted together by the cartridge connector's contacts.

 

Just to be sure, I'd recommend moving them of that area, or maybe applying a bit of kapton tape to protect them if it's too late to change the PCB layout.

 

Edited by Zerosquare
Link to comment
Share on other sites

Correct. While they're quite visible, these traces are actually in the two inner layers (most of which are occupied by the ground and VCC3 planes) and aren't actually exposed as long as you don't seriously over-bevel the PCB. I borrowed this idea from the Rev.4 boards, as it would have been a nightmare to route these nets otherwise.

Link to comment
Share on other sites

  • 2 months later...

I haven't spent much time working on things relevant to this thread, so I've been updating the general interest thread instead, but now that all the boring logistics and art stuff are done, I've returned my focus to to the manual labor of manufacturing these things.

 

Thankfully, I've gotten much better at soldering them overall. I haven't had to scrap any boards since the above note on shorting out the CPLDs on the last round of prototype boards. Even though I repaired one of those as noted above, I accidentally lifted a bunch of traces trying to smooth out the solder residue for repair using some solder wick on the other affected board, so it was off to the trash bin for that one. I like to think the soldering looks much smoother and consistent at this point as well.

 

The remaining obstacle I face is getting the components down that I use solder paste + hot air reflow for, namely U4, the power regulator chip, and U7, the tiny AND gate used to select between the EEPROMs. When I ordered the final boards, it was super cheap to add on a solder paste stencil, so had one made. The only issue here was generating the solder paste gerber file correct. The gold finger component in FreePCB neglected to opt its pads (The cartridge edge connector gold surfaces) out of the solder paste layer, so by default they would have shown up as solder paste areas in the stencil. Upon correcting this and re-placing the component, pretty much every trace connected to the finger became invalid and had to be rerouted, and after attempting to fix a few, they mysteriously failed design-rule checks afterwards, wouldn't show up as routed for unclear reasons, etc. Basically threw the whole board layout into disarray, and I just wasn't willing to deal with that this late in the process, so I went with a work-around: I opened the solder paste gerber file and deleted the lines corresponding to the edge connector part. Luckily FreePCB, while it's obviously a bit finicky in the UI, generates clearly commented gerber files. You can find the hand-edited gerber file in the PCB git repo:

 

https://github.com/cubanismo/skunk_pcb/blob/master/releases/top_paste_mask_v5-J0_removed.gbr

 

And while I'm linking things, very detailed notes on the work I did to get to get from Rev.3 (the last public PCB layout) to Rev.5:

 

https://github.com/cubanismo/skunk_pcb/blob/master/skunk_v5.txt

 

Back to the matter at hand, I have a solder paste stencil now:

 

stencil.thumb.jpg.4e0c26e911764ebf6053449692032402.jpg

 

Though since I opted not to panelize my boards to ensure they had very clean routed edges that would fit nicely in cartridge cases if desired, the stencil is rather large and cumbersome to work with compared to the size of the tiny boards. In particular, it doesn't fit on my usual workspace on my desk/workbench unless I remove a bunch of other stuff that's hard to move. I also don't have a jig or anything to align the boards with it, but I rigged up a sort-of solution for that at least:

 

stencil_back.thumb.jpg.506d45a358e317db4371142279980c6d.jpg

 

My plan was to try doing a board with reflow just to compare the difficulty and resulting quality, and worst case, just dab some paste on the stencil holes corresponding to the U4 and U7 pads I use reflow for already to make that part of the process faster than my current "Use the tiniest solder paste syringe tip I can get paste to flow through, still gob way too much of the stuff over the pads, reflow it, then touch it up with some flux and regular soldering iron." Neither effort went well.

 

Like most things, solder paste stencils seem to be harder to work with than any of the youtube videos discussing the subject let on, and I've yet to get a nice, clean application using the stencil after 4 or 5 attempts. Part of the problem is the thing isn't rigid, so when it sits over the board, it warps and creates a bit of a dome over the board rather than sitting completely flat against the PCB all the way across. I can alleviate this a bit by putting some scrap chunks of FR4 under it, but it's still not great. I also may not have the optimum paste for the job (I have some MG Chemicals stuff that is supposed to be pretty good in general, but with specs I don't recall at the moment), and I have no idea if the thickness of the stencil is ideal. I just picked the default and hoped for the best I think. I apply the paste by dabbing some out of the syringe onto an expired credit card and wiping it across the stencil, then wiping away excess  with a clean edge. The result is always either way too much paste on the pads, or the paste sticking to the stencil instead of the pads when I lift it away. It's also *very* hard to lift that massive stencil away without jiggling it just enough such that the paste gets pushed off the tiny pads and ends up looking like a mess. 0.5mm pitch pads,while not the tiniest things to work with these days, are none the less small enough it doesn't take more then a momentary tiny tremor of the hand to throw everything off.

 

So once again, any advice from any resident pros/old-hands here? Cheap home-made solutions to securing the stencil? Guidelines to pick the thickness in case I want to re-order, or recommendations on solder pastes and/or how to pick one for a given project's parameters? I'm nearly out of my current paste anyway. In the meantime, after needing to heavily rework all the components I managed to get down using the stencil anyway, I've gone back to the tried-and-true line of paste from the syringe tip + reflow + touch up. I'm getting a little better at it, and the results look and work fine, but I can't help thinking I could be doing this all much faster.

Link to comment
Share on other sites

  • 6 months later...

I've been hacking around on the USB code on and off, and recently tackled a big chunk of functionality now that I've built all the boards that were ordered. As @grips03 guessed, the goal was always of course to flash games from the USB drive, but I was never sure I'd have the time and motivation to get it working, so I didn't want to explicitly announce that given I was also selling boards. Didn't want to get accused of false advertising. Well, I still don't have it working in a way that's useful to end users, but it's beyond obvious from the current progress at this point that that's what I'm doing, so there. That's what I'm doing.

 

Behold: usbffs.cof (Skunk USB + FatFS). Binary attached, source code here.

 

What does it do? It's the world's worst command line-based USB file browser and SkunkBoard flasher. Here's how you can use it if you're savvy with JCP and curious:

 

  • Stick a Fat32 or Fat16-formatted USB drive in the left (If you're facing the Jaguar from the front) USB-A port of your SkunkBoard. Put some Jaguar ROM files on it first of course. Don't have USB-A ports on your SkunkBoard? Too bad for you. I told you you'd probably want them! It's really very easy to solder these on if they're missing though.
  • Load usbffs.cof with the Skunk console enabled: jcp -c usbffs.cof
  • If it works, you'll see a bunch of info describing properties of the USB drive followed by a directory listing of the top-level directory on the USB stick followed by a command prompt. Enter one of these commands:
    • ls - List files and folders (Shown with a trailing '/') in the current directory
    • cd <directory name> - Change to a directory
    • cd .. - Move up one directory
    • pwd - Print the current directory's full path
    • quit - Exit the console. The Jaguar probably just crashes executing random code after this. Reset it.
    • flash <file name> - The good one. This flashes the specified ROM to bank one of the Skunkboard.
  • Use the above commands to browse around and flash your ROM. You'll see some status messages. Like, a lot of status messages.
  • When you see "Flashing complete" it's finally done. Type "quit" and reset or power cycle your Jaguar.
  • Launch the ROM in bank 0 using any of the usual methods (E.g., press Up on the Jaguar's D-Pad).

 

I'm aware it's not that useful as-is. If you still have to use JCP to run the commands, you might as well just flash using JCP too, and JCP will even launch the ROM for you after flashing it, unlike current usbffs. It's just a proof of concept. If I find myself with time again, I'll work on polishing things up and add a UI so you can do all this from the Jaguar using the D-Pad.

 

Also, you'll quickly notice how terrible the command line processor is. Don't put any extra spaces between "cd" or "flash" and the file/directory name: It wants one space there and only one. Don't try to get fancy and quote your directory/file names: You don't need to and it won't work if you do. There are probably more things you could add to this list.

 

For those who code: Take a look at the XXX comment near the top of usbffs.c. WTF is going on here? I haven't debugged this yet. It's weird. Things seem to sometimes go wrong in mysterious ways when I call the skunklib functions from C code, though it generally works fine. My C->m68k stubs are here if you're curious.

usbffs-prealpha-20210905.cof

  • Like 7
  • Thanks 2
Link to comment
Share on other sites

  • 1 month later...

I started looking into adding a GUI to this code, and it was going pretty smoothly. I made a slick logo graphic. I got the OP to display it and scale it. I made the background color match the purple Skunkboards. Basically done, right? At that point, I went a little crazy. A perfectly usable GUI could have been written entirely in C code running on the 68k, but where's the fun in that? This is the 64-bit Jaguar! I promptly moved all my object list handling code to the GPU, decided I ought to write all the text and other UI rendering as GPU routines, and I felt I needed to add what is likely the most responsive joystick handling ever written for the Jaguar by building a ridiculously high-frequency joystick event polling loop that builds a timestamped event queue using the DSP (managing the circular buffer of the queue using the DSP's special addqmod instruction of course) for my simple ROM list menu. I invested way more late nights in this than any reasonable person should have, but it was fun, and I again know a lot more about the JRISC processors than I did before. An especially fun one-character bug that took me ~6 hours over 3 nights to track down:

 

https://github.com/cubanismo/skunk_usb/commit/ee927fe1a

 

I also debugged that mysterious skunklib failure I mentioned above. It was an equally frustrating bug in the skunkCONSOLEREAD function:

 

https://github.com/cubanismo/jaguar-sdk/commit/67c10f0a5d

 

Anyway, getting to the point: In the end, I had added just enough GUI functionality to make this project interesting to regular users. I also packaged it up into an auto-boot ROM file. See the attached usbffs-alpha-20211028.j64. The way that works is you flash it to the 2nd bank of your Skunkboard, and suddenly your Skunkboard boots to a graphical ROM loading menu instead of a blank green screen. So without further ado, the instructions:

 

  1. Put a handful of cartridge dumps on a FAT32/FAT16-formatted USB flash drive. There are some funny rules to follow here currently:
    1. No more than 32 character file names, including the period and extension. Longer names will likely corrupt the list window and/or crash the program.
    2. No more than 15 files, including other directories, in a given directory. There's no scrolling yet, and this is how many fit in the list window. If you have more, it will probably crash.
    3. Cartridge dumps only. BJL/.cof/.abs/.jag(jag server)/.bin/headerless ROM files are not yet supported. If you try to use any other files, yup, you guessed it, it will probably crash.
    4. Only 1MiB, 2MiB, and 4MiB ROMs are currently supported. 6MiB ROMs are not yet supported. Guess what happens if you try to flash a 6MiB ROM? Yup, you're probably right.
  2. Put the USB flash drive in the 1st USB-A port (Left one looking at the logo side) of your Skunkboard.
  3. For the first run, connect your skunk to a computer with JCP installed and get the usbffs rom on that computer.
  4. Turn on your Jaguar
  5. On the host computer, run:
jcp -2f usbffs-alpha-20211028.j64

And when flashing completes, you should be in the SkunkUSB menu; something like this:

 

skunkusb.thumb.jpg.ad8628ad12f194ce1aa6bc88fc1b9165.jpg

 

If you only see a pulsing purple background and the SkunkUSB logo, check your USB drive: This situation means the SkunkUSB code couldn't read the drive. If you're certain you have a correctly formatted drive, let me know. Your USB stick might not get along with the lower level USB driver code, and I'd like to collect some diagnostic info from your system if you have the time.

 

Assuming you are seeing a black box with a list of files in it as shown above, you're in business. Feel free to disconnect the Skunkboard from the computer and power cycle your Jaguar to enjoy the new, fully untethered experience. Here's what you can do:

  • Up/Down - Select a file
  • C - Launch whatever game is currently flashed to bank 0 of the Skunkboard
  • B - Flash the currently selected ROM or change to the currently selected directory. When flashing, the game list is cleared and used as a top-to-bottom status bar for the erase+flash operation instead.
  • Start - Show/Hide the file list (E.g., to get a better look at that sweet logo)
  • Option - Refresh the directory listing (Really no reason to do this. I just left it wired up from some earlier testing)

Here's what you can't do:

  • Go up a directory. Once you descend into the directory tree, you're stuck there until you reset. This is because reasons, not because it's hard to handle more buttons. This is probably the first thing I'll fix up.
  • Flash the first ROM in the file list apparently (Just found this bug while writing this up)
  • Launch certain ROMs. Things the Skunk didn't support before still don't work. SuperCross 3D has issues when launched from the GUI, but you can reset to the Skunk green screen (See below) after flashing it, then press 'Up' to launch it from there. If you find others that don't work, please let me know. I haven't done that much testing yet.
  • Use a controller on a TeamTap or the 2nd port on the Jaguar. Player 1 only for now.
  • Mess up your USB drive content. The filesystem code currently only has read access to it.

OK, so you tried it out, it's spiffy but obviously incomplete, or it didn't work at all for you. Now you want it off your Skunkboard, but you're stuck with a Skunkboard that only boots to this weird purple program that doesn't respond to JCP anymore. How do you get rid of it? You can bypass the Skunkboard's autoboot feature by holding down 'A' while resetting or powering on your Jaguar. That will get you back to the regular Skunk BIOS green boot screen temporarily. If you want to remove it permanently, get to that screen then flash any other 4MiB ROM to the 2nd bank of your Skunkboard using JCP (Or your favorite Skunkboard host-computer GUI) from a host computer again, overwriting the SkunkUSB ROM and autoboot info. Your Skunkboard will then be back to normal again.

 

For those who want to check out the code, it's available here:

 

https://github.com/cubanismo/skunk_usb

 

By default, it builds a console-enabled version that requires the JCP console to be running and connected. To build the stand-alone version, edit the Makefile and set SKUNKLIB to 0 instead of 1, then make clean && make to rebuild everything. You'll also need the latest version of my Jaguar SDK, as I added some functionality to skunklib to allow joypad and skunk console input simultaneously to some extent:

 

https://github.com/cubanismo/jaguar-sdk

usbffs-alpha-20211028.j64

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

Man... When I saw this thread pop up last year, I thought Smelly Adventures was a new game.

 

That title paints a picture in my head. There's a big hairy sasquatch running around, dodging obstacles, and really working up a sweat.

 

I mean, this is really cool and all. It's always motivating to see the results when someone keeps working on a project like this. An hour or two of work here and there doesn't sound like much in writing, but it's more than most are willing to put towards any one thing... and fewer would bother to document it like this. I didn't know what I was reading half the time but I enjoyed the ride. Kinda makes me want to do something productive myself.

 

But what I really want to do is play Smelly Adventures...

  • Haha 4
Link to comment
Share on other sites

  • 5 months later...

I came across this thread recently:

 

 

And noticed @Matthias's flash cartridge could emulate a memory track cartridge. I thought that was pretty cool. Ever since I got the serial EEPROM stuff working on the Skunkboards, I tend to prefer playing my games on a Skunkboard even when I have the originals. That way I can just upload all my save games to my home server and pick up where I left off regardless of where I'm playing, even if it means pushing them onto a flash card for use on the Game Drive (I'm like the scrooge McDuck of Skunkboards at this point, but I still only have one Game Drive). However, I've been playing a lot of Hover Strike: Unconquered Lands and Battle Morph lately, and that means taking my one Memory Track cartridge with me wherever I go if I want to continue on the same campaign. It seemed like the Skunkboard should be able to act like a MemoryTrack too. It's just a cartridge with a lot of flash memory after all. So I located the memory track BIOS sources and set about reading through them and getting them building. And so the nightmare began...

 

The Memory Track is a simple cartridge that has both an 8bit ROM (I don't know the exact size, but it appears to be the same chip used for the Jaguar system BIOS) containing its BIOS and a 128K 8bit parallel flash chip for storing the game data. The PCB and BIOS are wired up cleverly such that the 8-bit flash chip can be addressed by the Jaguar while running in the 32-bit ROM memory mode that the CD uses. You just have to multiply the address you want by 4, read/write a DWORD and look in bits 16-23 for the one byte of data. Clever. The Skunkboard also has a bit deficit Vs. the standard operating mode of the Jaguar CD (Skunk is 16, CD is 32). Can I use a similar trick? No, it turns out, because the Skunkboard's addressing and data lines aren't wired up in a way that would allow this. The address lines run straight from the cartridge port to the flash chip, without going through the FPGA, so I can't swizzle them around, and the data lines are limited to the same set of 16 lines on the Jaguar no matter how you swizzle them around in the FPGA. The Jaguar memory controller appears to align all accesses to 32-bit words in 32-bit ROM mode, so you can't move around the bits by reading address + 2, for example. It just goes ahead and reads address, then address + 4, and stitches them together itself from what I can tell in my limited experimenting. The end result being, you can read every other WORD from the Skunk while in 32-bit ROM mode, but not the other ones. That would actually be fine (We don't need 8MB of memory for a memory track cartridge to be useful) if you never had to write to the cartridge. Unfortunately, the addresses used to send the special write and erase commands to the Skunk flash chip are not all odd or even, but a mixture of the two. Obviously, no writes is a deal-breaker, so right away I was a bit worried the project wasn't going to work at all. You can fiddle MEMCON1 and change the ROM width whenever you want, but my experience messing with this to do skunklib console printf debugging while working on the cinepak player gave mixed results: If anyone tries to touch the Jaguar CD Butch chip while the ROM width is set to 16-bit, the whole system becomes unstable fast. Anecdotally, the Jaguar CD BIOS code seems to do this from time to time if you leave the CD spinning and reading data while you're doing stuff on the 68k, because no matter how careful I was, I always seemed to introduce at least some instability by messing with MEMCON1 while using the CD. Or, maybe I needed to give it more time to settle or something. Regardless, it didn't seem like a recipe for success, so I put the project on the back burner for a while.

 

Off and on though, I kept reading through the memory track BIOS source, and tinkered away at getting it building. This should have been easy, but it was not. I had the Makefiles cleaned up and building *something* pretty quickly. Helpfully, they build both a ROM suitable for burning to the memory track BIOS chip, and a stand-alone build of the memory track manager program that I presume was generally used for testing on an Alpine or similar system. However, I couldn't build anything that had a chance of working until I had a working version of the filefix utility. Hence, my sidetrack effort at writing an updated version of that. Sure, I could have used the slow-as-molasses DOS version in dosemu and/or rolled something myself out of dd+cat+objdump, or just built headerless files in the first place, but where's the fun in that? After I had properly built binaries, I stubbed out all the actual flash access commands and loaded this stand-alone program up on a Skunkboard... and it did nothing. It was stuck on a black screen. Throughout this project, I ran into various compiler bugs that were all utter hell to track down. This first one was because if I didn't build The C code with -fno-builtins, the compiler inexplicably decided my memset() and memcpy() functions used word-sized size_t parameters, so they were either running forever or writing to/reading from some garbage address and hanging. I eventually figured that out with BG-toggling debugging and disassembling the object files. Now I had something on the screen, but there was a giant smear instead of the cool embossed Jaguar logo in the menu, and all the text was totally garbled. The fonts were easy: they used Atari's standard font rendering library, which just needed the same tiny fix I had to apply to fix the same issue on the 3D demo. The logo was a little tougher. By this time I had skunklib console printf()'s wired up, and I printed out all the blitter variables, and they all looked sane. Keeping in mind this was presumably the same source that worked fine in the memory track cartridge, I knew it must be another weird interaction with modern compilers. I looked for uninitialized stack variables for quite some time and tried casting everything to long, then everything to short, even though the printf()s and basic logic told me that was all fine. Turns out the problem was a line like this:

  A2_STEP = A1_STEP = 0x00010000L | ((-224L) & 0x0000ffff);

Figure it out yet? I'll give you another minute...

 

Most of the blitter registers, including these, are write-only. The C compiler decided the correct way to interpret that line was to write the value to A1_STEP, read A1_STEP, and write the result (Some undefined value I didn't bother to print out) to A2_STEP. Storing the step value into a temp variable and assigning it to both registers individually gave me a pretty, empty memory track manager screen. Basically done at this point, right? Well, maybe we want it to actually be able to read and write some flash memory first. At least all the C compiler bugs and corner cases were behind me though. Time to move on to assemblers and linkers...

 

The memory track source is actually pretty well organized, and contains multiple flash "drivers" already: One for an AMD flash chip, one for an Atmel flash chip (Hey, the Skunkboard uses an Atmel chip!), and one that emulates flash using the RAM on an Alpine for development/testing. Turns out the AMD one is actually a closer match to the commands used in the Skunkboard flash, so I copied that one and started hacking on it. I cut/pasted some code from another project that I initially cut/pasted from the Skunkboard BIOS updater/flasher to erase sectors and write single words to the Atmel 16-bit flash chip. I'd noted before that the commands look nothing like those in the datasheet, but it works on both the other projects. We'll come back to that...

 

It didn't take long to have something that compiled and seemed like it might work. I loaded it up, tried to create a file, and it did nothing of course. How to debug? Well except the actual flash code itself that is fiddling with the Skunkboard's read/write/HPI modes, I could use jserve to step through it in a gdb session! gdb can't actually get symbols out of the files aln or rln generate, but those cool little tools symval and allsyms that came along with that filefix rewrite I did were very useful here: Just run symval stand.cof <some label> and it dumps out the address to set a breakpoint on. Don't even have to start disassembling linked binaries or anything. Something was wrong though. GDB was making the code unstable. The register values I was reading back didn't make sense. I figured out what was going on here pretty quickly. This is the second time I've battled dueling BIOSes on the Jaguar. The memory track "BIOS" consists of both the actual BIOS ROM (Or a .cof/.abs equivalent for the stand-alone build), and various "drivers" or what this codebase refers to as the BIOSes embedded inside of it. When the ROM boots, it determines the type of flash chip in the system (I stripped all the others out and hard-coded the Skunk one), and copies it to a fixed location in the Jaguar's reserved memory area: 0x2400. Well, there's not a ton of space down below 0x4000. We have the Jaguar BIOS (I assume, I've never actually looked at whether it installs anything here itself), the Jaguar CD BIOS, the Skunkboard BIOS, this Memory Track BIOS, and also, of course, the GDB server stub code, which shares the location 0x2c00 with the Alpine debugging stub code I believe. The Memory Track BIOSes get 0x2400-0x2BFF for all their code, data, and BSS areas, and my code was two long-words over, so my BSS section was clobbering the gdb stub's shadow copy of the 68k registers or vice-versa. This first time, it was easy to shave a few bytes off here and there, but this was to be a recurring problem.

 

Somewhere between these problems and the next ones, I ran into the next two compiler issues. First, ALN was randomly just completely clobbering a move instruction in my BIOS driver code. I didn't have any explanation. No big deal, ALN is made of crust right? I'll just use RLN. Well, not so fast. RLN didn't handle the syntax the Makefile was using to build the BIOS driver files embedded in the larger BIOS ROM. These files all have their text section placed at 0x2400 as described above, their data section in the ROM they're loaded from, and then their BSS section immediately following their text section. ALN and RLN have syntax for specifying this when linking non-relocatable executables: "a <text_addr> <data_addr> <bss_addr>" where data_addr and text_addr can be the special value x meaning the come immediately after the prior section, and bss_addr can further use the special values xd or xt, meaning it explicitly follows the data section (same as just x) or it instead follows the text section. RLN accepts all these, but doesn't even attempt to handle "xt" correctly, so my BSS section was getting placed up in ROM space, which didn't work at all. It took me a minute to notice this. Well, that was an easy patch to RLN at least. I'll get that cleaned and sent out shortly.

 

Now back to the actual problem: My flash code didn't work at all. I'd modified it a bit to try to make it flow with the surrounding code, but the flash chip was having none of it. After fiddling with it for a while, I ended up building an even thinner test harness for the low-level driver functions in the BIOS code and using that to debug it. In the end, I had to duplicate all the little idiosyncratic nuances of the Skunk BIOS code exactly to get it to work. Get an error while writing? Don't go retry the write like the manuals suggest! Maybe try reading the value one more time instead. Generally, it actually succeeded and just didn't feel like telling you at first. All hardware is the worst.

 

So now my flash code is working flawlessly... the first time I ran it on a freshly erased Skunkboard. ruh-roh. Remember how I said all those flash commands looked like nonsense compared to the values in the manual? Turns out this isn't a case of the flash chip being bizarre, and I actually had suspected this was coming after spending some time looking at the Sunkboard wiring in more detail while trying to find a way around the 16-bit-in-32-bit-mode issue above: The flash chip isn't wired up to the Jaguar address and data lines as you'd intuitively expect. E.g., Jaguar address bit 2 doesn't go to flash address bit 1 (Jag uses byte addressing, flash is word-addressed), it goes to bit 11 instead. The others are similarly all over the place. How does this ever work? Well with a few exceptions it doesn't actually matter. Both reads and writes travel through these same crossed lines, so you get consistent results and that's all that matters. Does it matter if the bits 1001 0001 1000 1100 are actually stored as 1111 0000 1100 0000? No, as long as when you read them back, they get swizzled back around the inverse of how they were swizzled upon writing. Same with addresses for the most part: Do you really care if a byte you wanted store at address 4 ends up at address 512? No, not really, as long as when you read address 4 it goes and retrieves it from 512 again. No one cares. They're just zeros and ones and the flash chip couldn't care less where or in what order they are in... except in a few cases, both of which we care about here.

 

First of all, sometimes the bits *do* have a special meaning to the flash chip. Besides basic read and write operations, the chip accepts certain commands. These do things like erase sectors (We'll get to those), prepare the chip for a write, reset the chip, etc. Those commands have specific values that need to be written to specific addresses for them to be interpreted correctly by the chip's controller logic. This is precisely why all the flash driver stuff for the Skunkboard looks so wonky: You have to manually perform the inverse swizzle of both the address and data bits of the commands before writing them to the board such that when they make their way through the Skunkboard's crossed wiring, they end up looking like the correct command and value to the flash chip. This is a seriously tedious process, but at least for the special commands I could just cut/paste them.

 

Now about those sectors: Flash chips don't actually let you just write arbitrary values any time you want to a given address. You can only write to erased locations. Writing a given address twice without erasing it in between results in garbage or an error. Most people don't realize this, because drivers, storage controllers, and operating systems all conspire to hide it from the user. And it's even worse than this actually: You can't just go and erase single bytes, words, or dwords here and there. That would be too easy. Rather, the storage is arranged as relatively large "sectors", and you have to erase an entire sector at a time. What if you only want to modify one byte in that sector though? Tough. Read it all back, erase the entire sector, modify the one byte in your local copy, and write the entire sector back. That's how flash works.

 

OK, so no big deal, the memory track common code takes care of caching sectors and all that junk for us. All we have to do is implement the erase and write command logic, which I did. However, recall the swizzled address lines... If your sectors are 64 bytes like they are on the Skunkboard, and you want to write all 64 bytes in that sector, you'd better hope all the lower 15 address lines on the Jaguar that correspond to the 64 byte area of a sector map to the 14 address lines on the flash that correspond to a sector. They can be all crossed up any way they like in that range, but they all need to be within that range. For the special first sectors on the flash chip used for the BIOS that are only 8k, this was done correctly, so you can erase just the BIOS of the Skunkboard without erasing some data using a logical address outside of that sector. Surprise, for the 64k sector, this is not done correctly. Two address lines are out of range, so you can't address bytes within individual sectors correctly. You get something like the first 8k of bytes landing in the naively expected sector, then the next 256 bytes don't, then some more do, or something along those lines. You can work out the exact details by examining the Skunk PCB layout, or check out this diagram I made while working it out the other day:

 

   Flash Chip        Jaguar Bus
(word addresses)  (byte addresses)
----------------  ----------------
     A0                 EA1
     A1                 EA7
     A2                 EA6
     A3                 EA12
     A4                 EA8
     A5                 EA11
     A6                 EA9
     A7                 EA10
     A8                 EA5
     A9                 EA4
     A10                EA3
     A11                EA2
     A12                EA20
     A13                EA13
     A14                EA19
     A15                EA17
     A16                EA16
     A17                EA14
     A18                EA21
     A19                EA15
     A20                EA18
     A21                CPLD toggles this for bank switching

Address ranges by bits (byte addresses):

21 20 19    16 15    12 11    8  7     4  3     0
 x x  x x x x  x x x x  x x x x  x x x x  x x x x
   |  |        |     |                          |
   |  |        |     +---------0-8191(8k)-------+
   |  |        |                                |
   |  |        +-----------0-65535(64k)---------+
   |  |                                         |
   |  +-----------------0-1048575(1M)-----------+
   |                                            |
   +--------------------0-2097151(2M)-----------+

How does this work normally? Well, it doesn't really, but you can't tell. All the existing Skunkboard software wipes very large chunks of the flash at a time. You either erase 2MB or a full 4MB bank at a time, so it doesn't matter if sectors overlap here or there. So what are we going to do? Well, fortunately all accesses, including reads, in memory track mode go through two functions in the low-level drivers. Hence, we have the opportunity to munge them a bit. After probably an hour pouring over the Skunkboard PCB building the above diagram, it became clear the necessary operation was simply to swap the Jaguar-side bits 14 and 15 with bits 19 and 20 before sending the write on to the Skunkboard. That keeps all the <64k address bits in the same sector as required.

 

With that change, my low-level test harness was working flawlessly, so I went to sleep. I'd stayed up waaaaay to late figuring that part out. The next morning, I spent a good deal of time further size-optimizing the skunk flash driver code, as the logic to shift the address bits around and fix the flashing commands in general had put it waaaay over budget again. I had to trim another 16 words out, which was getting harder and harder. Eventually I had it down to size, so I started it up, and it still did almost nothing. I could occasionally get it to create a file, but the name was always wrong. It'd be only one or two of the letters I entered, and often I couldn't delete the files. For the longest time, I thought I'd messed up the flash code integration somehow, but I looked at it over and over again, and it all looked flawless. The only other suspect was these weird byte-packing functions that compress 3 characters down to one word variable, but those were trivial. It couldn't be them, could it? Surprise again, it was. After writing and debugging yet more harness code to exercise these functions directly from the memory track manager startup code and dump their results over the skunk console, it was clear they were the culprit, but it was not at all obvious why. Everything looked perfect, but the results just didn't match what I got when I ran the same calculations manually. On a whim, I tried compiling my code with MAC instead of RMAC and comparing the two, since presumably the MAC-built version of these functions had worked in the past. Of course, that didn't work. Another MAC bug prevented it from accepting some of the clever addressing tricks I'd done to reduce size without entirely sacrificing clarity. Nevermind, let's just zero that address out for testing... and... WTF? Why is the MAC version some 30+ bytes larger? Clearly MAC is buggy as hell... but wait, let's compare them anyway. Well, that 30+ bytes was the bug of course, and it was a bug in RMAC, not MAC this time. This byte-packing code relies on a table of valid characters to perform its encoding and decoding, defined thusly:

charset:
        dc.s    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        dc.s    "0123456789"            
        dc.s    ":'. "

On the MAC version, it's all there. On the RMAC version, there's just some gibberish. I'd looked at this several times in the disassembler while debugging, and it would just politely decline to show this range and put a "..." there instead. I assumed that just meant it couldn't disassemble this data into sensible instructions, so it punted, but I should have looked closer at the addresses. It was only a few words of gibberish, not the 40 bytes it should have been. The functions were trying to encode and decode characters using a table that consisted mostly of the instructions surrounding this listing, resulting in utter garbage. I wasn't too keen on trying to fix this particular bug, so I started looking for workarounds. Interestingly changing "dc.s" to "dc.b" produces correct results. They're supposed to be equivalent according to the manual, but one works and one doesn't.

 

Finally, with this fixed, I could create as many glorious place-holder files as I wanted using the debugging features of the stand-alone memory track manager! I could also create a ROM from the same code and flash it to the Skunkboard, booting it like any other game. However... that again didn't work so well after the first run. Turns out, the cool address swizzling was clobbering the ROM contents no matter where in the Skunk address space I placed the emulated memory track memory. Address lines! My only weakness! That was easily solved though by placing the emulated memory track memory in the second flash bank, leaving the ROM safely hidden away behind the FPGA's bank switching logic.

 

Still though, this didn't actually do anything useful. The way the memory track provides functionality to games is that it is run by the Jaguar CD BIOS like a normal cartridge plugged into the Jaguar CD. The Memory Track startup code then detects that it's running on the Jaguar CD by examining a marker the Jaguar CD leaves behind at... address 0x2400 of course, copies its low-level driver BIOS over that marker, leaving one of its own for the CD games to find, then simply returning to the Jaguar CD BIOS code, which then continues its process of booting a CD game. It's like those old terminate-and-stay-resident (TOS) DOS programs from back in the day. Load up some functionality and leave it there for someone to find later. In my case, the Skunk BIOS was sitting there in between the CD BIOS and the memory track BIOS, keeping them from playing nice together. The Skunkboard has code to detect the CD unit just like the memory track and return to it if the user is holding 'C' down while booting, just like the memory track does. However, that's not what we want: We want the Skunkboard to detect the Jaguar CD, and, if the user is holding down C, first launch the memory track BIOS, if it's present, so it can install its low-level driver BIOS before the Skunkboard returns to the CD BIOS. Fortunately, I've meddled in the Skunk BIOS code before. This wasn't that complicated of a modification, but it took a while to get it right because you have to make sure to leave everything in a somewhat pristine state for the whole trek through all 3 BIOSes and back to work without someone getting miffed and hanging. For that reason, the existing Skunk BIOS code is careful to detect the CD unit *very* early on and bail out immediately before it touches anything. That works if all you want to do is get back to the CD BIOS, but we want a functioning Skunkboard as well. It turns out if you skip all that init code, you can't write to the Skunk flash for whatever reason. Hence, I took the opposite approach, coaxing the code flow through the existing init path as much as possible and skipping only those things that definitely wouldn't work, which ended up being the video init code and the code to switch over to Atari's standard stack location (What good is an rts, if you've lost the stack your return address is on?). After fiddling with that for a few hours, I had a scheme worked out similar to the existing under-utilized auto-boot/Skunk extended BIOS code: The Skunkboard detects the Jaguar CD unit, and rather than bailing out immediately, skips video init and sets a flag. After performing the rest of init, if that flag is set, the Skunkboard looks for a marker in bank one at address 0x802004. If it finds the marker, it extracts a load address from 0x80200C and uses a JMP instruction to transfer control flow to it rather than the usual JSR. Hence, when that code returns, it will bypass the Skunk BIOS code and return straight to the Jaguar CD BIOS, just like we want.

 

After all that work, it still wasn't clear if it was going to work at all. There was still the 16-bit/32-bit memory controller disparity to worry about. Well luckily, it did work with the first game I tested. Battlemorph gets by just fine, and I'm able to save/restore games there just fine. So far, of the games I've tested, Battlemorph, Iron Soldier 2, and Primal Rage work perfectly. Vid Grid kind of works, but somehow using the Skunkboard Memory Track causes the videos to be switched around. Trying to play "Give it Away Now" launches straight into the hidden "Smells Like Teen Spirit", which was fun, but still gives me credit for completing "Give it Away Now" when I'm done, including saving and restoring that progress. Hover Strike did not work at all though, which broke my heart, because that's what I've been playing the most lately. It hangs before it even starts playing the intro video, bolstering its reputation as the most finicky of all the finicky Jaguar CD games.

 

Those are all the Jaguar CD games I have handy at the moment. Anyone know other Memory Track titles off-hand, or want to suggest one for testing? I did some preliminary testing with Blue Lightning before I had all the bugs above worked out (Though I've presented it as a linear narrative, in reality I jumped back and forth more than a Skunkboard address line while working through these, as they were all incredibly frustrating, and I had to continually build hacky prototypes of various parts to convince myself the project was still feasible overall), so I think it's probably going to work as well, which would put me at 2/3 for my JagCD favorites, which is pretty good. I couldn't even find a list of all the games that support the memory track though, and I'm not too keen on having a coaster-making party trying to figure that out through trial and error, so I'd appreciate recommendations from those who already know.

 

I'm putting the final polish on some of the code, but I'll release this all in more or less its current state soon: A new Skunkboard BIOS (with a corresponding new version of JCP for Win/Mac/Linux to apply the update), and a ROM file of the memory track ROM you can flash with JCP. I'd like to add functions to upload/download individual files and full dump of the flash so you can more easily transport them between machines, but I don't want to let feature creep hold things up. It's already useful as a memory track stand-in. Before I release the ROM, I'll push the code to github. When that RMAC bug was worked around and the character table subsequently jumped up to its correct size, it put me another 16 words over the size limit again, meaning I have to shave at least another ~12 instructions off somehow. I can probably find a few more bytes to shave off here or there, but it's getting pretty hard with my abilities. Perhaps @42bs will be able to apply his size coding abilities and help me out there. There's probably still plenty of low-hanging fruit for the real experts.


In the meantime, pics or it didn't happen!

 

skunk_memtrk.thumb.jpg.bd41e1350add19efde1b328fca10a60b.jpg

  • Like 7
  • Thanks 2
  • Confused 1
Link to comment
Share on other sites

6 hours ago, JagChris said:

Off the top of my head MYST used the memory track.

Baldies and Braindead 13 definitely used the cart.

 

Now I'm itching to play Baldies and watch those ridiculous FMVs that wowed me as teen. I'm remembering a buzzard on a sign.

  • Like 2
Link to comment
Share on other sites

23 hours ago, cubanismo said:

Vid Grid kind of works, but somehow using the Skunkboard Memory Track causes the videos to be switched around.

I’ve had this issue before with a normal memory track while playing Space Ace, some scenes are mirrored when I’m playing with a memory track inserted, and seem fine when I play without it. Very odd. 

  • Confused 1
Link to comment
Share on other sites

On 4/8/2022 at 9:12 AM, cubanismo said:

erhaps @42bs will be able to apply his size coding abilities and help me out there. There's probably still plenty of low-hanging fruit for the real experts.

Does it contain GPU/DSP code? If so a good way to save space is to do this:

; large:
  movei	#yloop,r29
yloop:
  ...
  jump ne,(r29)
  nop

; smaller:
  move pc,r29
  addq #4,r29
yloop:
 ...
  jump ne,(r29)
  nop

; smallest (with cycle penalty)
yloop:
  move pc,r29
 ...
  jump ne,(r29)
  nop

Also check the 68k code if it does

bsr subroutine

and not

jsr subroutine

 

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

Well, I went over to the internet archive and had myself a coaster fest. I verified Blue Lightning, Myst and Baldies also work. Highlander does not: It creates a "settings" file on the Skunk Memory Track, but then hangs on a black screen. Braindead 13 does indeed seem bugged, but it works just as well with the Skunk Memory Track as the real one: It creates a save game, but can't load it (Bummer, that game would have been entirely playable compared to Space Ace and Dragon's Lair if you could actually save progress). As far as I can tell the other Readysoft games don't use the memory track.

On 4/8/2022 at 5:44 PM, CyranoJ said:

The .CDI images for the early reboot games use MemoryTrack.

 

You can grab them at: https://reboot-games.com

These won't work. I tested them, then looked at the source code, and for some reason they include the Memory Track BIOS code in the game itself, overwriting the new BIOS the Skunk Memory Track code loads when it boots, so there's no way to support these without rebuilding the games to use the Memory Track like they're supposed to. I tried fiddling with the source to get it to work, but it doesn't seem to find a memory track (real or skunk) if it doesn't load all the BIOS stuff itself, and oddly hangs if some of the other stuff in its memory track init routine isn't run, even if no memory track is present, and only if run from ULS. Works fine direct-loaded via JCP when built like that. From the code you posted here @CyranoJ, I wonder if this manual memory track BIOS loading is because ULS is mucking about down in those lower memory ranges the various BIOSes use and needs to fix up the memory track BIOS area after it does its stuff?

 

In summary, for original releases, I the current stats are 6 working, 2 kind of working (VidGrid and Braindead), 2 not working (Hover Strike, Highlander), and 3 that don't use the memory track (World Tour Racing, Dragon's Lair, and Space Ace).

 

I don't currently have access to any of Orion's CD games to test, nor am I aware of any other homebrew using the memory track, but I would still love to hear if others know of some.

 

The Reboot games, and likely anything based on the same code, or possibly ULS, isn't going to work unfortunately.

 

On the plus side, I got an excuse to try playing Downfall long enough to rack up a few decent high scores. Good times.

 

I also finished testing the requisite new Skunkboard BIOS (Built separate versions for Rev2/Rev3/Rev4 boards and my newer Rev5 boards. Don't want people to get left behind!) and new build of JCP that includes it and a few other very minor fixes I've had in my tree waiting for a reason to publish. I'll get some installers built and signed for that and post them soon.

 

I tried a few things to get Hoverstrike working (Disabling interrupts during the flash access, switching to 16-bit mode for only the handful of instructions that actually touch the Skunk rather than whole functions, forcing the GPU into single-step mode during the the flash access and resuming it after), but couldn't get it working yet. I did notice as I was finishing up for the day that the CD BIOS is going to be loading the CD TOC at 0x2C00. Sound familiar? Right, that's the address I'm currently overflowing into by a few bytes with my memory track BIOS code. As mentioned above, the overflowing bytes end up being the BSS section, so all kinds of interesting data is going to get written over the TOC when the memory track init routines are called, which is probably not helping things, and could easily explain why VidGrid is wonky, if not more, but it could also end up being a red herring in practice given so many games work already. Still, I needed to revisit shrinking that code anyway, so this is just extra motivation. Oh, and thanks for the suggestions there @42bs, the memory track BIOS is all 68k code, using bsr everywhere AFAICT, and in fact is already pretty good about using bsr.s explicitly when possible. If I can't get it down to size after another pass or two, I'll post it as-is and try to crowd source some cleverness. It's a relatively large chunk of code (2k), so it has to be possible to shave off 32 more bytes somehow. Probably just needs fresh eyes.

Link to comment
Share on other sites

On 4/9/2022 at 12:16 AM, Wilco said:

I’ve had this issue before with a normal memory track while playing Space Ace, some scenes are mirrored when I’m playing with a memory track inserted, and seem fine when I play without it. Very odd. 

Googling a bit, it appears that this game randomly mirrors the scenes and/or makes you play through scenes in both directions. It's probably a coincidence that it seemed to correlate with the memory track being present. I don't think it accesses the memory track at all.

  • Thanks 1
Link to comment
Share on other sites

40 minutes ago, cubanismo said:

Works fine direct-loaded via JCP when built like that. From the code you posted here @CyranoJ, I wonder if this manual memory track BIOS loading is because ULS is mucking about down in those lower memory ranges the various BIOSes use and needs to fix up the memory track BIOS area after it does its stuff?

It's probably MEMCON changes, from what I recall its forced to 8bit mode for the CD-BIOS load. It's been a while.

 

In any case, nearly all the games are on reBOOTed and fixed up to use EEPROM :)

 

Quote

On the plus side, I got an excuse to try playing Downfall long enough to rack up a few decent high scores. Good times.

Cool, always fun to hear stuff like this :)

Link to comment
Share on other sites

1 hour ago, cubanismo said:

Googling a bit, it appears that this game randomly mirrors the scenes and/or makes you play through scenes in both directions. It's probably a coincidence that it seemed to correlate with the memory track being present. I don't think it accesses the memory track at all.

That does make a lot more sense, thank you for clearing that up! 

Link to comment
Share on other sites

I managed to squeeze the necessary 32 bytes out by optimizing the common code portion of the BIOS, which had clearly already been size optimized (Pats self on back). I actually saved a word or two more, but it still pads up to exactly 2k. I found the easiest way to do this was walking through the disassembly of the linked file. Not only do you get to see how big the instructions are right inline if you have the raw encoded hex version next to it, but it's easier to spot potential optimization opportunities too. For example, it isn't immediately obvious this is wasteful:

lea	gl_appname(a5),a0	; 2 word instruction

Because the variable obscures the specialization opportunity. It's much more obvious when it's written as this:

lea	0(a5),a0		; 2 word instruction

That zero is doing what, exactly? Clearly all we need is this, along with a build-time assert to make sure gl_appname really is zero:

movea.l	a5,a0			; 1 word instruction

That was actually the single biggest optimization I found, given the idiom was replicated about 5 times in the file. There were also a handful of things of the forms:

; Big
lea	TINYVALUE(a0),a0	; 2 word instruction to increment a0
lea	0,a0			; 2 word instruction to clear a0

; Smaller version
addq.l	#TINYVALUE,a0		; 1 word instruction to increment a0
suba.l	a0,a0			; 1 word instruction to clear a0

However, I pulled a n00b move here and spent a half hour debugging why only Vid Grid and sometimes the self test corrupted the FAT block after this last optimization. Remember folks: "addq #val,a0" defaults to "addq.w #val,a0" and same for suba, even though it's unlikely you ever want to increment/decrement only a word of an address register. I went back and changed everything to the explicit addq.l and suba.l and all was well. Then there were several one-off opportunities here and there. I was sort of disappointed I only managed to find one structural/algorithm-level size optimization here (and I think it only saved 2 words), but I'll take whatever gets the job done.

 

Enough technical details though, let me tell you about the sweet results: I mentioned Vid Grid above; does it work 100% now? Yes. How about Hoverstrike? Yes! Highlander? yes, Yes, YES! Skunk Memory Track has reached 100% support for the original run titles that work with the original Memory Track cartridge. Unfortunately, that took all my time for the night thanks to that stupid word-sized add/sub mistake. The optimizations still have "XXX add assert here" notes and commented out code fragments here and there. It'll take another free night or two to clean up the code, tag version numbers, write instructions, package up a ROM release, and get those signed JCP builds done for the BIOS, but I'm personally much happier with the project now that Hoverstrike works.

  • Like 3
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...