using BizHawk.Common; namespace BizHawk.Emulation.Cores.Atari.Atari2600 { /* DPC (Pitfall 2) ----- Back in the day, this game was da shizzle (and IMO still is). It did its trick via a custom chip in the cartridge. Fortunately for us, there's a patent that describes lots of the internal workings of the chip (number 4644495, "video memory system"). Interestingly, the patent shows the DPC as a *separate* device. You plug a passthrough cartridge into your 2600, then plug the game cartridge into the passthrough. Apparently, Activision thought that people wouldn't like this, or there was some other reasoning behind it and they ditched that idea and went with the DPC inside the cartridge. Unfortunately for Activision, it was filed in January of 1984, during the height of the crash. The inventor is listed as David Crane. OK, enough background. Now onto the meat: The DPC chip is just 24 pins, and needs to pass through the chip enable to the game ROM on the cartridge, so it can only address 2K of memory. This means the DPC shows up twice in the address space, once at 1000-107F and again at 1800-18FF. There's been some discussion about the pitch of the music generated by this chip, and how different carts will play the music at different pitches. Turns out, on the cart, the frequency is determined by a resistor (560K ohms) and a capacitor integrated onto the die of the DPC chip itself. The resistor is a 5% tolerance part, and the process variations of the DPC itself will control the frequency of the music produced by it. If you touch the resistor on the cartridge board, the music pitch will drastically change, almost like you were playing it on a theremin! Lowering the resistance makes the music pitch increase, increasing the resistance makes the pitch lower. It's extremely high impedance so body effects of you touching the pin makes it vary wildly. Thus, I say there's really no "one true" pitch for the music. The patent, however, says that the frequency of this oscillator is 42KHz in the "preferred embodiment". The patent says that it can range from 15KHz to 80KHz depending on the application and the particular design of the sound generator. I chose 21KHz (half their preferred value) and it sounds fairly close to my actual cartridge. Address map: Read Only: 1000-1003 : random number generator 1004-1005 : sound value (and MOVAMT value ANDed with draw line carry, with draw line add) 1006-1007 : sound value (and MOVAMT value ANDed with draw line carry, no draw line add) 1008-100F : returned data value for fetcher 0-7 1010-1017 : returned data value for fetcher 0-7, masked 1018-101F : returned data value for fetcher 0-7, nybble swapped, masked 1020-1027 : returned data value for fetcher 0-7, byte reversed, masked 1028-102F : returned data value for fetcher 0-7, rotated right one bit, masked 1030-1037 : returned data value for fetcher 0-7, rotated left one bit, masked 1038-103F : fetcher 0-7 mask Write Only: 1040-1047 : fetcher 0-7 start count 1048-104F : fetcher 0-7 end count 1050-1057 : fetcher 0-7 pointer low 1058-105B : fetcher 0-3 pointer high 105C : fetcher 4 pointer high and draw line enable 105D-105F : fetcher 5-7 pointer high and music enable 1060-1067 : draw line movement value (MOVAMT) 1068-106F : not used 1070-1077 : random number generator reset 1078-107F : not used random number generator ----------------------- The random number generator is used on Pitfall 2 to make the eel flash between white and black, and nothing else. Failure to emulate this will result in the eel not flashing. It's an 8 bit LFSR which can be reset to the all 0's condition by accessing 1070-1077. Unlike a regular LFSR, this one uses three XOR gates and an inverter, so the illegal condition is the all 1's condition. There's 255 states and the following code emulates it: LFSR = ((LFSR << 1) | (~(((LFSR >> 7) ^ (LFSR >> 5)) ^ ((LFSR >> 4) ^ (LFSR >> 3))) & 1)) & 0xff; Bits 3, 4, 5, and 7 are XOR'd together and inverted and fed back into bit 0 each time the LFSR is clocked. The LFSR is clocked each time it is read. It wraps after it is read 255 times. (The 256th read returns the same value as the 1st). data fetchers ------------- Internal to the DPC is a 2K ROM containing the graphics and a few other bits and pieces (playfield values I think) of data that can be read via the auto-incrementing data fetchers. Each set of 8 addresses (1008-100F for example) return the data from one of the 8 data fetcher pointers, returning the data in a slightly different format for each. The format for the 6 possible register ranges is as follows: For the byte "ABCDEFGH" (bit 7 to bit 0) it is returned: 1008-100F: ABCDEFGH (never masked) 1010-1017: ABCDEFGH 1018-101F: EFGHABCD (nybble swap) 1020-1027: HGFEDCBA (bit reversed) 1028-102F: 0ABCDEFG (shifted right) 1030-1037: BCDEFGH0 (shifted left) Reading from each set of locations above returns the byte of data from the DPC's internal ROM. Reading from 1008 accesses data at DF (data fetcher) 0's pointer, then decrements the pointer. Reading from 1009 accesses data at DF1, and so on. There is no difference except how the data is returned when reading from 1008, 1010, 1018, 1020, etc. All of them return data pointed to by DF0's pointer. Only the order of the bits returned changes. I am not sure what purpose returning the data shifted left or right 1 bit serves, and it was not used on Pitfall 2, but that's what it does. I guess you could use it to make a sprite appear to "wiggle" left and right a bit, if it were 6 pixels wide. All of these read ports returns the data masked by an enable signal, except for 1008-100F. The data here is never masked. (more about this in a minute) To read data out of the chip, first you program in its start address into the pointer registers. These are at 1050-1057 for the lower 8 bits of the pointer value, and 1058-105F for the upper 4 bits of the pointer value. This forms the 12 bit address which can then be used to index the DPC's ROM. A few of the upper bits on 105C-105F are used for a few other purposes, which will be described later. Masking the data: ----------------- 1038-103F is the readback for the mask value 1040-1047 is the start count 1048-104F is the end count The mask value can be read via 1038-103F. It returns 0 when graphics are masked, and FFh when they are not masked. (0 = reset, 1 = set) The basic synopsis is thus: When the lower 8 bits of the pointer equals the start count, the mask register is set. When the lower 8 bits of the pointer equals the end count, the mask register is reset. Writing to the start count register also sets the register. This allows one to have the sprites only show up on specific scanlines, by programming the proper start and end counts, and the proper starting value into the pointer. This way, the sprite can be drawn from top to bottom of the screen, and have it only appear where it is desired without having to do anything else in the 2600 code. Making Music: ------------- The music is generated by repurposing three of the fetchers, the last three. Each fetcher can be individually selected for music or fetching. 7 0 --------- 105D-105F: xxSM PPPP S: Select clock input to fetching counter. 0 = read pulse when the proper returned data register is read (i.e. for fetcher 5, 1015 is being read) 1 = music oscillator. M: Music mode. 1 = enable music mode, 0 = disable music mode. P: upper 4 bits of the 12 bit data fetcher pointer. I am not sure why you can separately select the clock source and the music mode, but you can. Maybe they had some plans for externally clocking the chip via some logic to bump the pointers. Normally you set both the M and P bits to make music. When in music mode, the lower 8 bits of the fetcher pointer is used as an 8 bit down counter. Each time the lower 8 bits equals FFh, it is reloaded from the start count register. To turn the data fetcher into a square wave generator takes very little hardware. The start/end count registers are used as-is to toggle the flag register. This means that the duty cycle of the square waves produced can be varied by adjusting the end count register relative to the start count register. I suspect the game simply right shifts the start count by one and stuffs it into the end count to produce a 50% duty cycle waveform. The three flag outputs for fetchers 5 to 7 are fed into a cool little circuit composed of a 3 to 8 decoder and four 4 input NAND gates to produce the 4 bit audio output. The output is as follows: fetcher result 567 --------------------- 000 0h 001 4h 010 5h 011 9h 100 6h 101 Ah 110 Bh 111 Fh This is a somewhat nonlinear mixing of the three channels, so the apparent volume of them is different relative to each other. The final 4 bit output value from the above table is then available to read at address 1004-1007, in bits 0 to 3. Pitfall 2 just reads this location and stuffs it into the audio register every scanline or so. The value read at 1004-1007 is the instantanious value generated by the fetchers and mixing hardware. */ internal class mDPC : MapperBase { private ulong totalCycles; private ulong elapsedCycles; private double FractionalClocks; private int bank_4k; private IntBuffer Counters = new IntBuffer(8); private ByteBuffer Flags = new ByteBuffer(8); private IntBuffer Tops = new IntBuffer(8); private IntBuffer Bottoms = new IntBuffer(8); private ByteBuffer DisplayBank_2k = new ByteBuffer(2048); private byte RandomNumber; private bool[] MusicMode = new bool[3]; // TODO: savestates public override byte PeekMemory(ushort addr) { return base.PeekMemory(addr); //TODO } public override void ClockCpu() { totalCycles++; } public override byte ReadMemory(ushort addr) { ClockRandomNumberGenerator(); addr &= 0x0FFF; if (addr < 0x0040) { byte result = 0; int index = addr & 0x07; int function = (addr >> 3) & 0x07; // Update flag register for selected data fetcher if ((Counters[index] & 0x00ff) == Tops[index]) { Flags[index] = 0xff; } else if ((Counters[index] & 0x00ff) == Bottoms[index]) { Flags[index] = 0x00; } switch (function) { default: result = 0; break; case 0x00: if (index < 4) { result = RandomNumber; } else // it's a music read { byte[] MusicAmplitudes = { 0x00, 0x04, 0x05, 0x09, 0x06, 0x0a, 0x0b, 0x0f }; // Update the music data fetchers (counter & flag) UpdateMusicModeDataFetchers(); byte i = 0; if (MusicMode[0] && Flags[5] > 0) { i |= 0x01; } if (MusicMode[1] && Flags[6] > 0) { i |= 0x02; } if (MusicMode[2] && Flags[7] > 0) { i |= 0x04; } result = MusicAmplitudes[i]; } break; case 0x01: result = DisplayBank_2k[2047 - Counters[index]]; break; case 0x02: result = DisplayBank_2k[2047 - (Counters[index] & Flags[index])]; break; case 0x07: result = Flags[index]; break; } // Clock the selected data fetcher's counter if needed if ((index < 5) || ((index >= 5) && (!MusicMode[index - 5]))) { Counters[index] = (Counters[index] - 1) & 0x07ff; } return result; } Address(addr); return core.rom[(bank_4k << 12) + addr]; } public override void WriteMemory(ushort addr, byte value) { addr &= 0x0FFF; // Clock the random number generator. This should be done for every // cartridge access, however, we're only doing it for the DPC and // hot-spot accesses to save time. ClockRandomNumberGenerator(); if ((addr >= 0x0040) && (addr < 0x0080)) { // Get the index of the data fetcher that's being accessed int index = addr & 0x07; int function = (addr >> 3) & 0x07; switch (function) { case 0x00: // DFx top count Tops[index] = value; Flags[index] = 0x00; break; case 0x01: // DFx bottom count Bottoms[index] = value; break; case 0x02: // DFx counter low if ((index >= 5) && MusicMode[index - 5]) { Counters[index] = (Counters[index] & 0x0700) | Tops[index]; // Data fetcher is in music mode so its low counter value should be loaded from the top register not the poked value } else { // Data fetcher is either not a music mode data fetcher or it // isn't in music mode so it's low counter value should be loaded // with the poked value Counters[index] = (Counters[index] & 0x0700) | value; } break; case 0x03: // DFx counter high Counters[index] = ((value & 0x07) << 8) | (Counters[index] & 0x00ff); // Execute special code for music mode data fetchers if (index >= 5) { MusicMode[index - 5] = (value & 0x10) > 0; // NOTE: We are not handling the clock source input for // the music mode data fetchers. We're going to assume // they always use the OSC input. } break; case 0x06: // Random Number Generator Reset RandomNumber = 1; break; } } else { Address(addr); } return; } private void Address(ushort addr) { if (addr == 0x0FF8) { bank_4k = 0; } else if (addr == 0x0FF9) { bank_4k = 1; } } public override void Dispose() { DisplayBank_2k.Dispose(); Counters.Dispose(); Flags.Dispose(); base.Dispose(); } public override void SyncState(Serializer ser) { // TODO base.SyncState(ser); ser.Sync("bank_4k", ref bank_4k); ser.Sync("DisplayBank_2k", ref DisplayBank_2k); ser.Sync("Flags", ref Flags); ser.Sync("Counters", ref Counters); ser.Sync("RandomNumber", ref RandomNumber); } private void UpdateMusicModeDataFetchers() { // Calculate the number of cycles since the last update //int cycles = mySystem->cycles() - mySystemCycles; //mySystemCycles = mySystem->cycles(); ulong cycles = totalCycles - elapsedCycles; elapsedCycles = totalCycles; // Calculate the number of DPC OSC clocks since the last update double clocks = ((20000.0 * cycles) / 1193191.66666667) + FractionalClocks; int wholeClocks = (int)clocks; FractionalClocks = clocks - (double)wholeClocks; if (wholeClocks <= 0) { return; } // Let's update counters and flags of the music mode data fetchers for (int x = 5; x <= 7; ++x) { // Update only if the data fetcher is in music mode if (MusicMode[x - 5]) { int top = Tops[x] + 1; int newLow = Counters[x] & 0x00ff; if (Tops[x] != 0) { newLow -= (wholeClocks % top); if (newLow < 0) { newLow += top; } } else { newLow = 0; } // Update flag register for this data fetcher if (newLow <= Bottoms[x]) { Flags[x] = 0x00; } else if (newLow <= Tops[x]) { Flags[x] = 0xff; } Counters[x] = (Counters[x] & 0x0700) | newLow; } } } private void ClockRandomNumberGenerator() { // Table for computing the input bit of the random number generator's // shift register (it's the NOT of the EOR of four bits) byte[] f = { 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1 }; // Using bits 7, 5, 4, & 3 of the shift register compute the input // bit for the shift register byte bit = f[((RandomNumber >> 3) & 0x07) | ((RandomNumber & 0x80) > 0 ? 0x08 : 0x00)]; // Update the shift register RandomNumber = (byte)(RandomNumber << 1 | bit); } } }