505 lines
16 KiB
C#
505 lines
16 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|