As a user it doesn't really matter to me if loading a tape takes 2 seconds or 5 seconds - either way it is nothing compared to the loading times back in the day. Still, I always want to improve my emulator if I can, and tape flash loading has not been as fast as it apparently can be, when you compare it to other emulators. Also, as in the case of the Bad Apple 2 demo, some tape files won't even load correctly without efficient flash loading. So I decided to have one more look at the flash loading routines and found that I had made things much more complicated than I had to, and that a simpler solution was much faster!
Previously, the emulator used the header information to flash load the following data block, but the header itself was never flash loaded. If speed load and edge detection is activated, loading a header is fast anyway, but there is still a noticeable delay. Also, this method required separate routines for loading program blocks, data blocks and headerless blocks. After analyzing the ROM routines (using this excellent site) and doing some experimenting I found that by intercepting the LD BYTES ROM routine the emulator could flash load every tape block, regardless of type, so everything could be handled by the same routine - and much faster.
Below is the new flash loading routine in a static class, which is called by the Controller class after each instruction, when tape loading is active. The actual flash load routine is very simple, and the bulk of the code is needed to handle different errors that can occur. In short, the process consists of the following steps:
namespace SP48 { /// <summary> /// Loads tape data directly into RAM, bypassing the ROM loading routines. /// </summary> public static class TapeFlashLoader { /// <summary> /// Checks if the program counter is at an entry point in the ROM tape loading routines /// and attempts to flash load the next tape block. /// </summary> /// <returns> /// An integer representing the index of the <see cref="TapeBlock"/> that was flash loaded. A return /// value of -1 means that flash loading could not be performed. /// </returns> public static int FlashLoad(Z80 z80, Memory memory, TapeManager tapeManager) { // The return value, representing the index of the TapeBlock which was flash loaded. int lastBlockIndex = -1; // The LD BYTES ROM routine is intercepted at an early stage just before the edge detection is started. // Check that the tape position is at the end of a block and that there is a following block to flash load. if (z80.PC == 0x056A && tapeManager.NextBlock != null && tapeManager.CurrentTapePosition > tapeManager.NextBlock.StartPosition - 10) { // The target address for the data is stored in IX. int dataTargetParameter = 256 * z80.I1 + z80.X; // The block length (number of bytes) is stored in DE. int dataLengthParameter = 256 * z80.D + z80.E; // The flag byte is stored in A'. int flagByte = z80.APrime; // Check for various errors: // Is there a mismatch between the block type and the flag byte in the A' register? if (tapeManager.NextBlock.BlockTypeNum != flagByte && dataLengthParameter > 0) { // Don't load the block, but reset all flags. z80.CarryFlag = 0; z80.SignFlag = 0; z80.ZeroFlag = 0; z80.HalfCarryFlag = 0; z80.Parity_OverflowFlag = 0; z80.SubstractFlag = 0; z80.F3Flag = 0; z80.F5Flag = 0; // The A register is updated by XOR:ing the flag byte with the block byte read from the file. z80.A = flagByte ^ tapeManager.NextBlock.BlockTypeNum; } else // Is the expected number of bytes larger than the actual length of the block? if (dataLengthParameter > tapeManager.NextBlock.BlockContent.Length) { // If the DE register indicates a too long data length, the loader will fail after // loading the block and it expects one more byte. memory.WriteDataBlock(tapeManager.NextBlock.BlockContent, dataTargetParameter); // When a new edge is not found, flags carry = 0 and zero = 1. z80.CarryFlag = 0; z80.ZeroFlag = 1; // The other flags are set by the last INC B (from 0xFF) at 0x05ED. z80.HalfCarryFlag = 1; z80.SignFlag = 0; z80.Parity_OverflowFlag = 0; z80.SubstractFlag = 0; z80.F3Flag = 0; z80.F5Flag = 0; // Check that we're not dealing with a data fragment (in which case IX and DE are intact). if (tapeManager.NextBlock.BlockContent.Length >= 2) { z80.I1 = (dataTargetParameter + tapeManager.NextBlock.BlockContent.Length + 1) / 256; z80.X = (dataTargetParameter + tapeManager.NextBlock.BlockContent.Length + 1) - 256 * z80.I1; z80.D = (dataLengthParameter - (tapeManager.NextBlock.BlockContent.Length + 1)) / 256; z80.E = (dataLengthParameter - (tapeManager.NextBlock.BlockContent.Length + 1)) - 256 * z80.D; } z80.A = 0; } else // Is the expected number of bytes smaller than the length of the block? if (dataLengthParameter < tapeManager.NextBlock.BlockContent.Length) { memory.WriteDataBlock(tapeManager.NextBlock.BlockContent, dataTargetParameter); // When calculating the checksum for the loaded data, there are two different cases, // either the block length parameter equals zero, in which case there is no parity // calculated and the checksum contains the flag byte. // Otherwise, the checksum is calculated in the usual way but only for the number // of bytes specified in the data length parameter + 1. int calculatedCheckSum; if (dataLengthParameter == 0) calculatedCheckSum = flagByte; else { calculatedCheckSum = flagByte; // The checksum is calculated by XOR:ing each byte of data with the flag byte. for (int i = 0; i < dataLengthParameter + 1; i++) calculatedCheckSum ^= tapeManager.NextBlock.BlockContent[i]; } // The flags are set by the CP 0x01 operation at 0x05E0, where A = the current checksum. int flagTest = calculatedCheckSum - 1; z80.CarryFlag = BitOps.GetBit(flagTest, 8); z80.SignFlag = Flags.SignFlag(flagTest); z80.ZeroFlag = Flags.ZeroFlag(flagTest); z80.HalfCarryFlag = Flags.HalfCarryFlagSub8(calculatedCheckSum, 1, z80.CarryFlag); z80.Parity_OverflowFlag = Flags.OverflowFlagSub8(calculatedCheckSum, 1, flagTest); z80.SubstractFlag = 1; z80.F3Flag = 0; z80.F5Flag = 0; // Update the A, IX and DE registers. z80.A = calculatedCheckSum; z80.I1 = (dataTargetParameter + dataLengthParameter) / 256; z80.X = (dataTargetParameter + dataLengthParameter) - 256 * z80.I1; z80.D = 0; z80.E = 0; } else // Is the expected number of bytes equal to zero? if (dataLengthParameter == 0) { // There is no parity check, so the checksum contains the block type value. int calculatedCheckSum = 0xFF; // The flags are set by the CP 0x01 operation at 0x05E0, where A = the current checksum. int flagTest = calculatedCheckSum - 1; z80.CarryFlag = BitOps.GetBit(flagTest, 8); z80.SignFlag = Flags.SignFlag(flagTest); z80.ZeroFlag = Flags.ZeroFlag(flagTest); z80.HalfCarryFlag = Flags.HalfCarryFlagSub8(calculatedCheckSum, 1, z80.CarryFlag); z80.Parity_OverflowFlag = Flags.OverflowFlagSub8(calculatedCheckSum, 1, flagTest); z80.SubstractFlag = 1; z80.F3Flag = 0; z80.F5Flag = 0; z80.A = calculatedCheckSum; } else // Flash load the block and update IX, DE and AF. { int lastBytePos = memory.WriteDataBlock(tapeManager.NextBlock.BlockContent, dataTargetParameter); int calculatedCheckSum = 0; // The flags are set by the CP 0x01 operation at 0x05E0, where A = the current checksum. int flagTest = calculatedCheckSum - 1; z80.CarryFlag = BitOps.GetBit(flagTest, 8); z80.SignFlag = Flags.SignFlag(flagTest); z80.ZeroFlag = Flags.ZeroFlag(flagTest); z80.HalfCarryFlag = Flags.HalfCarryFlagSub8(calculatedCheckSum, 1, z80.CarryFlag); z80.Parity_OverflowFlag = Flags.OverflowFlagSub8(calculatedCheckSum, 1, flagTest); z80.SubstractFlag = 1; z80.F3Flag = 0; z80.F5Flag = 0; z80.A = calculatedCheckSum; // Set IX to the same value as if the block had been loaded by the ROM routine. z80.I1 = lastBytePos / 256; z80.X = lastBytePos - 256 * z80.I1; // Set DE to 0. z80.D = 0; z80.E = 0; } // Keep track of the index of the last loaded tape block. This information // can be used to rewind the tape to the start position of the next block // after an auto pause. lastBlockIndex = tapeManager.NextBlock.Index; // Skip forward to the end of the block which was just flash loaded into RAM. tapeManager.GoToEndOfBlock(tapeManager.NextBlock.Index); // Skip to the end of the LD BYTES ROM routine (actually a RET, so it doesn't really matter which RET instruction we point to here). z80.PC = 0x05E2; } return lastBlockIndex; } } }
I'm sure there is still room for improvement here, but so far flash loading is much faster and more reliable than before!
0 Comments
Leave a Reply. |
Archives
November 2020
Categories
All
|