Thursday 17 January 2019

Livening CPU speed, video mode and monitor in freeze menu

Continuing the work on the freeze menu, I have been able to make quite a bit of progress lately. In the last post, I had the freezer and freeze menu working in a minimalistic kind of way. However, there were a lot of things not there, and still plenty of bugs to cause trouble. 

For a start, I wasn't saving the state of the CIAs.  This turned out to be a bit more of a pain than I first expected.  Fortunately, I was able to read through the information on C64 freezing by Gideon, as well as get a bunch of useful tips from Groepaz and others.  This opened a whole can of worms, and basically reminded me of a fact I had either previously forgotten, or had failed to fully grasp:  Perfect freezing is practically impossible, and requires in the very least hardware support if you are to have any chance at getting it right. 

The CIAs are a big part of the reason behind this.  The problem is that the CIAs have timers that keep on running while you are trying to freeze them, and there is certain internal state that you can't read from the registers, but requires that you try to work out how far the timers have run down, wait for them to run out, and then read out the latched information.  Of course, one or more of the four timers of the 2 CIAs will probably run out before you spot it, and the accuracy you can achieve when trying to watch four timers with a single 1MHz CPU is rather limited -- probably of the order of 50 or 100 cycles.

My solution to deal with the CIAs was to make the CIAs effectively freeze themselves when in Hypervisor mode: The CIA timers stop ticking, and acknowledging interrupts via $DC0D etc has no effect.  I also added 16 extra registers in hypervisor mode of the CIA that allows us to directly read out the latched and current values of the timers, as well as the current and alarm times of the time-of-day clocks. It is still not perfect, but it pretty much works.

As I worked on the freeze menu, I wanted to provide an indication of which ROM you are running, since there are a variety of C65 ROMs available, and people will probably want to try some out.  So I wrote a little routine that makes an educated guess as to which ROM you are running. For the C65 ROMs, this is quite simple: they have a fairly reliable version string near the start of the ROM, and so if I find one of those, I indicate it is a C65 ROM and show the relevant version.  It can also detect a wide range of C64 and even PET 4064 ROMs. I did this by making a utility that reads in all the ROMs I could find, and looks for unique bytes in the KERNAL ROM part, and makes its decision on that basis. It turns out if you do the tests in the right order, a single PEEK is all you need to test for each known ROM:

  // Check for C65 ROM via version string
  if ((freeze_peek(0x20016L)=='V')
      &&(freeze_peek(0x20017L)=='9')) {
    c65_rom_name[0]=' ';
    c65_rom_name[1]='C';
    c65_rom_name[2]='6';
    c65_rom_name[3]='5';
    c65_rom_name[4]=' ';
    for(i=0;i<6;i++)
      c65_rom_name[5+i]=freeze_peek(0x20017L+i);
    c65_rom_name[11]=0;
    return c65_rom_name;
   
  }

  if (freeze_peek(0x2e47dL)=='J') {
    // Probably jiffy dos
    if (freeze_peek(0x2e535L)==0x06)
      return "sx64 jiffy ";
    else
      return "c64 jiffy  ";
  }
 
  // Else guess using detection routines from detect_roms.c
  // These were built using a combination of the ROMs from zimmers.net/pub/c64/firmware,
  // the RetroReplay ROM collection, and the JiffyDOS ROMs
  if (freeze_peek(0x2e449L)==0x2e) return "C64GS      ";
  if (freeze_peek(0x2e119L)==0xc9) return "C64 REV1   ";
  if (freeze_peek(0x2e67dL)==0xb0) return "C64 REV2 JP";
  if (freeze_peek(0x2ebaeL)==0x5b) return "C64 REV3 DK";
  if (freeze_peek(0x2e0efL)==0x28) return "C64 SCAND  ";
  if (freeze_peek(0x2ebf3L)==0x40) return "C64 SWEDEN ";
  if (freeze_peek(0x2e461L)==0x20) return "CYCLONE 1.0";
  if (freeze_peek(0x2e4a4L)==0x41) return "DOLPHIN 1.0";
  if (freeze_peek(0x2e47fL)==0x52) return "DOLPHIN 2AU";
  if (freeze_peek(0x2eed7L)==0x2c) return "DOLPHIN 2P1";
  if (freeze_peek(0x2e7d2L)==0x6b) return "DOLPHIN 2P2";
  if (freeze_peek(0x2e4a6L)==0x32) return "DOLPHIN 2P3";
  if (freeze_peek(0x2e0f9L)==0xaa) return "DOLPHIN 3.0";
  if (freeze_peek(0x2e462L)==0x45) return "DOSROM V1.2";
  if (freeze_peek(0x2e472L)==0x20) return "MERCRY3 PAL";
  if (freeze_peek(0x2e16dL)==0x84) return "MERCRY NTSC";
  if (freeze_peek(0x2e42dL)==0x4c) return "PET 4064   ";
  if (freeze_peek(0x2e1d9L)==0xa6) return "SX64 CROACH";
  if (freeze_peek(0x2eba9L)==0x2d) return "SX64 SCAND ";
  if (freeze_peek(0x2e476L)==0x2a) return "TRBOACS 2.6";
  if (freeze_peek(0x2e535L)==0x07) return "TRBOACS 3P1";
  if (freeze_peek(0x2e176L)==0x8d) return "TRBOASC 3P2";
  if (freeze_peek(0x2e42aL)==0x72) return "TRBOPROC US";
  if (freeze_peek(0x2e4acL)==0x81) return "C64C 251913";
  if (freeze_peek(0x2e479L)==0x2a) return "C64 REV2   ";
  if (freeze_peek(0x2e535L)==0x06) return "SX64 REV4  ";
  return "UNKNOWN ROM";

This routine is written in C, because the whole freeze menu is written in C using the CC65 compiler.  This makes it quite easy to change, which is how we want it: If you want to make your own replacement or modified freeze menu, then it should be possible to do.   We are progressively building up a library of functions that are helpful, such as the freeze_peek() routine, which the freezer uses to retrieve a byte of memory from the frozen program.  To do this, it needs to know the layout of the freeze slots as they are saved on the SD card, because on the MEGA65 freezing always happens with the result written to the SD card, rather than being squished around in memory like with the original freeze cartridges.  The freeze_peek() routine itself is fairly simple, basically consisting of working out where the byte lives on the SD card, then reading the relevant sector, and returning the appropriate byte of data:

unsigned char freeze_peek(uint32_t addr)
{
  // Find sector
  uint32_t freeze_slot_offset=address_to_freeze_slot_offset(addr);
  unsigned short offset;

  offset=freeze_slot_offset&0x1ff;
  freeze_slot_offset=freeze_slot_offset>>9L;
 
  if (freeze_slot_offset==0xFFFFFFFFL) {
    // Invalid / unfrozen memory
    return 0x55;
  }
 

  // Read the sector
  sdcard_readsector(freeze_slot_start_sector+freeze_slot_offset);

  // Return the byte
  return sector_buffer[offset&0x1ff];

}

Working out where the byte lives is a little more complicated, as we have to ask the Hypervisor for the layout of the freeze regions, and then iterate through those regions to work out if the requested address falls in one of those regions.  It is possible that it doesn't, because the MEGA65 has a lot more address space (256MB) than it has populated with actual memory.

/* Convert a requested address to a location in the freeze slot,
   or to 0xFFFFFFFF if the address is not present.
*/
uint32_t address_to_freeze_slot_offset(uint32_t address)
{
  uint32_t freeze_slot_offset=1;  // Skip the initial saved SD sector at the beginning of each slot
  uint32_t relative_address=0;
  uint32_t region_length=0;
  char skip,i;

  for(i=0;i<freeze_region_count;i++) {
    skip=0;
    if (address<freeze_region_list[i].address_base) skip=1;
    relative_address=address-freeze_region_list[i].address_base;
    if (freeze_region_list[i].address_base==0x1000L) {
      // Thumbnail region: Treat specially so that we can examine it
      // We give the fictional mapping of $FF54xxx
      if ((address&0xFFFF000L)==0xFF54000L)
    { relative_address=address&0xFFF;
      freeze_slot_offset=freeze_slot_offset<<9;
      freeze_slot_offset+=(relative_address&0xFFF);
      return freeze_slot_offset;
    }
    }
    region_length=freeze_region_list[i].region_length&REGION_LENGTH_MASK;
    if (relative_address>=region_length) skip=1;
    if (skip) {
      // Skip this region if our address is not in it
      freeze_slot_offset+=region_length>>9;
      // If region is not an integer number of sectors long, don't forget to count the partial sector
      if (region_length&0x1ff) freeze_slot_offset++;
    } else {
      // The address is in this region.

      // Firsts add the number of sectors to get to the one with the content we want
      freeze_slot_offset+=relative_address>>9;

      // Now multiply it by the length of a sector (512 bytes), and add the offset in the sector
      // This gives us the absolute byte position in the slot of the address we want.
      freeze_slot_offset=freeze_slot_offset<<9;
      freeze_slot_offset+=(relative_address&0x1FF);
      return freeze_slot_offset;
    }
  }
  return 0xFFFFFFFFL;
}

Anyway, back to the ROM auto-detection: The result of the ROM auto-detection is displayed in the freeze menu, as shown in this screenshot:


While we are here, let's walk through the various elements, starting with most obvious part: the picture of the computer itself!  Here the results of the ROM auto-detection are used a second time, to pick the correct surround to show around the thumbnail image of the frozen program: If it is a C65 ROM, you see a C65 and 1084S monitor depicted, otherwise a C64 and 1702:


These surrounding images can be drawn as a 152x96 pixel PNG file, and converted to the correct format using the thumbnail-surround-formatter from thte MEGA65 Freeze Menu repository, so if you want to make it look different, perhaps like Gus the snail, you are totally free to do so! The only limitation is that the screen position for the thumbnail is fixed, and the colour palette is a 256 colour colour cube, with the first 16 entries replaced by the C64's standard palette. I might do something to improve the palette at some point, because it is a bit annoying.  It might be possible, for example, to have a separate palette for the surrounding image versus the thumbnail by selecting the alternate palette bit in the thumbnail graphics tiles. But that will have to wait for another day.

The thumbnails are 80x50 images generated automatically by the MEGA65 hardware, exactly for the freeze menu.  I wrote about the thumbnail generator hardware a long time ago, and it has finally taken until now before it got to be used for its intended purpose.  The only real change to the thumbnail generator was to move it to $D640 instead of $D630 to avoid a recurring address resolution problem in the VHDL, which I think was due to glitching of the chip select line.

While on the one hand just a bit of a fun cosmetic touch, the thumbnails serve the very useful purpose of making it easier for you to find previously frozen programs.  From in the freeze menu you can use the cursor keys to navigate through the set of freeze slots configured in the MEGA65 system partition.  The MEGA65 FDISK+FORMAT utility will normally allocate upto 2GB for the systesm partition, with about half of that being freeze slots, giving a total of just over 2,000 freeze slots under the current design. 

That should be more than enough to have games and programs you like to use pre-frozen ready for an instant meal whenever you want! It also means if you need to interrupt a favourite game at a critical point, it should be easy enough to find a free slot and save it for reheating later.  In fact, with ~2,000 slots, searching through them linearly will be sure to become a chore, so I will add some kind of search facility in the future.  This is a great thing about writing the freeze menu as a separate program in C, is that it is pretty easy to work on and extend.

Going further through the freeze menu, we find the bank of six quite configuration settings: CPU mode, ROM, CPU Frequency (speed), write-protection of the ROM area, cartridge enable/disable and PAL/NTSC select.  With each off these, the setting can be changed by pressing the indicated letter on the screen.  The only exception is R for ROM selection, which is not yet implemented.

Below that we have a set of typical Freeze utilities: A monitor to inspect and modify the memory of a frozen program, a mechanism to enter poke codes, e.g., for game cheats, a disk image chooser to let you switch disks while running a program (or to get ready to run one), a sprite viewer, poke finder and sprite collision killer (also for cheating in gamems).  At the moment only the monitor and disk select options are implemented. 

The monitor uses 80-column mode, and works like your typical machine code monitor on the C64.  At the moment only M to inspect memory and S to set contents of memory are supported. The syntax of these mirrors that of the MEGA65's hardware monitor, except that with the S command you can use " and ' to indicate either ASCII strings or screen poke codes.  So for example S400 'HELLO would put the word HELLO at the top left of the screen in C64 mode, by writing $08 $05 $0C $0C $0F to locations $0400 - $0404.

The disk selector scans the SD card for D81 files, and then displays a list of up to 1,000 of them, and lets you choose one to use.  Again, at some point I will add some kind of sorting and/or searching function to make browsing through long lists of disk images easier.  But what I have already implemented, is that if you highlight a disk image, and then don't press any keys for a second or more, it will retrieve the directory from the disk image.  In this way you can more easily find the disk you are looking for.  Here is an example of this in action:

This menu will also refuse to let you select a disk image if it can't be mounted for some reason. The most common cause at the moment is if the .D81 file is fragmented on the SD card.  The reason this isn't allowed is that the hardware support for D81 image access requires the D81 file to exist as a single linear 800KB block, so as to avoid the hardware needing to know about FAT file systems.  What I will likely do at some point is add support for automatically de-fragmenting image files, so that the user doesn't need to think about it.

Talking about disk images, whenever you load a frozen program, it goes through the disk image attachment process again, so if you have changed the disk image, e.g., by creating a new file in a different session, then they will show up.  There are some hazards with this, for example, if the frozen program isn't clever enough to notice the disk-change line, but this will hopefully not be a big problem. Also, of course you could have deleted the disk image.  In that case, when you resume the frozen program there will be no disk image attached.

That's the bulk of the function of the freeze menu for now.  I have skipped over a whole pile of little niggly problems I have had to solve along the way, and there are still a number of bugs and missing features to be solved, and the freeze/unfreeze process still sometimes messes up the frozen program in some way causing it to crash, but it is already functional enough to be very useful.  In short, you can now easily use the MEGA65, including switching disks, CPU speed and other things, without having to do anything awkward.  The following video shows me using the freeze menu to do various things.  I particularly like how cute the little thumbnails of the frozen programs look :)


No comments:

Post a Comment