using BizHawk.Common; using BizHawk.Common.NumberExtensions; using BizHawk.Emulation.Common; using BizHawk.Emulation.Cores.Components.Z80A; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BizHawk.Emulation.Cores.Computers.AmstradCPC { /// /// * Amstrad Gate Array * /// http://www.cpcwiki.eu/index.php/Gate_Array /// https://web.archive.org/web/20170612081209/http://www.grimware.org/doku.php/documentations/devices/gatearray /// public class AmstradGateArray : IPortIODevice, IVideoProvider { #region Devices private CPCBase _machine; private Z80A CPU => _machine.CPU; private CRCT_6845 CRCT => _machine.CRCT; //private CRTDevice CRT => _machine.CRT; private IPSG PSG => _machine.AYDevice; private NECUPD765 FDC => _machine.UPDDiskDevice; private DatacorderDevice DATACORDER => _machine.TapeDevice; private ushort BUSRQ => CPU.MEMRQ[CPU.bus_pntr]; public const ushort PCh = 1; private GateArrayType ChipType; #endregion #region Palettes /// /// The standard CPC Pallete (ordered by firmware #) /// http://www.cpcwiki.eu/index.php/CPC_Palette /// public static readonly int[] CPCFirmwarePalette = { Colors.ARGB(0x00, 0x00, 0x00), // Black Colors.ARGB(0x00, 0x00, 0x80), // Blue Colors.ARGB(0x00, 0x00, 0xFF), // Bright Blue Colors.ARGB(0x80, 0x00, 0x00), // Red Colors.ARGB(0x80, 0x00, 0x80), // Magenta Colors.ARGB(0x80, 0x00, 0xFF), // Mauve Colors.ARGB(0xFF, 0x00, 0x00), // Bright Red Colors.ARGB(0xFF, 0x00, 0x80), // Purple Colors.ARGB(0xFF, 0x00, 0xFF), // Bright Magenta Colors.ARGB(0x00, 0x80, 0x00), // Green Colors.ARGB(0x00, 0x80, 0x80), // Cyan Colors.ARGB(0x00, 0x80, 0xFF), // Sky Blue Colors.ARGB(0x80, 0x80, 0x00), // Yellow Colors.ARGB(0x80, 0x80, 0x80), // White Colors.ARGB(0x80, 0x80, 0xFF), // Pastel Blue Colors.ARGB(0xFF, 0x80, 0x00), // Orange Colors.ARGB(0xFF, 0x80, 0x80), // Pink Colors.ARGB(0xFF, 0x80, 0xFF), // Pastel Magenta Colors.ARGB(0x00, 0xFF, 0x00), // Bright Green Colors.ARGB(0x00, 0xFF, 0x80), // Sea Green Colors.ARGB(0x00, 0xFF, 0xFF), // Bright Cyan Colors.ARGB(0x80, 0xFF, 0x00), // Lime Colors.ARGB(0x80, 0xFF, 0x80), // Pastel Green Colors.ARGB(0x80, 0xFF, 0xFF), // Pastel Cyan Colors.ARGB(0xFF, 0xFF, 0x00), // Bright Yellow Colors.ARGB(0xFF, 0xFF, 0x80), // Pastel Yellow Colors.ARGB(0xFF, 0xFF, 0xFF), // Bright White }; /// /// The standard CPC Pallete (ordered by hardware #) /// http://www.cpcwiki.eu/index.php/CPC_Palette /// public static readonly int[] CPCHardwarePalette = { Colors.ARGB(0x80, 0x80, 0x80), // White Colors.ARGB(0x80, 0x80, 0x80), // White (duplicate) Colors.ARGB(0x00, 0xFF, 0x80), // Sea Green Colors.ARGB(0xFF, 0xFF, 0x80), // Pastel Yellow Colors.ARGB(0x00, 0x00, 0x80), // Blue Colors.ARGB(0xFF, 0x00, 0x80), // Purple Colors.ARGB(0x00, 0x80, 0x80), // Cyan Colors.ARGB(0xFF, 0x80, 0x80), // Pink Colors.ARGB(0xFF, 0x00, 0x80), // Purple (duplicate) Colors.ARGB(0xFF, 0xFF, 0x80), // Pastel Yellow (duplicate) Colors.ARGB(0xFF, 0xFF, 0x00), // Bright Yellow Colors.ARGB(0xFF, 0xFF, 0xFF), // Bright White Colors.ARGB(0xFF, 0x00, 0x00), // Bright Red Colors.ARGB(0xFF, 0x00, 0xFF), // Bright Magenta Colors.ARGB(0xFF, 0x80, 0x00), // Orange Colors.ARGB(0xFF, 0x80, 0xFF), // Pastel Magenta Colors.ARGB(0x00, 0x00, 0x80), // Blue (duplicate) Colors.ARGB(0x00, 0xFF, 0x80), // Sea Green (duplicate) Colors.ARGB(0x00, 0xFF, 0x00), // Bright Green Colors.ARGB(0x00, 0xFF, 0xFF), // Bright Cyan Colors.ARGB(0x00, 0x00, 0x00), // Black Colors.ARGB(0x00, 0x00, 0xFF), // Bright Blue Colors.ARGB(0x00, 0x80, 0x00), // Green Colors.ARGB(0x00, 0x80, 0xFF), // Sky Blue Colors.ARGB(0x80, 0x00, 0x80), // Magenta Colors.ARGB(0x80, 0xFF, 0x80), // Pastel Green Colors.ARGB(0x80, 0xFF, 0x00), // Lime Colors.ARGB(0x80, 0xFF, 0xFF), // Pastel Cyan Colors.ARGB(0x80, 0x00, 0x00), // Red Colors.ARGB(0x80, 0x00, 0xFF), // Mauve Colors.ARGB(0x80, 0x80, 0x00), // Yellow Colors.ARGB(0x80, 0x80, 0xFF), // Pastel Blue }; #endregion #region Clocks and Timing /// /// The Gate Array Clock Speed /// public int GAClockSpeed = 16000000; /// /// The CPU Clock Speed /// public int Z80ClockSpeed = 4000000; /// /// CRCT Clock Speed /// public int CRCTClockSpeed = 1000000; /// /// AY-3-8912 Clock Speed /// public int PSGClockSpeed = 1000000; /// /// Number of CPU cycles in one frame /// public int FrameLength = 79872; /// /// Number of Gate Array cycles within one frame /// public int GAFrameLength = 319488; #endregion #region Construction public AmstradGateArray(CPCBase machine, GateArrayType chipType) { _machine = machine; ChipType = chipType; //PenColours = new int[17]; borderType = _machine.CPC.SyncSettings.BorderType; SetupScreenSize(); //Reset(); CRCT.AttachHSYNCCallback(OnHSYNC); CRCT.AttachVSYNCCallback(OnVSYNC); CurrentLine = new CharacterLine(); InitByteLookup(); CalculateNextScreenMemory(); } #endregion #region Registers and Internal State /// /// PENR (register 0) - Pen Selection /// This register can be used to select one of the 17 color-registers (pen 0 to 15 or the border). /// It will remain selected until another PENR command is executed. /// PENR Index /// 7 6 5 4 3 2 1 0 color register selected /// 0 0 0 0 n n n n pen n from 0 to 15 (4bits) /// 0 0 0 1 x x x x border /// /// x can be 0 or 1, it doesn't matter /// private byte _PENR; public byte PENR { get { return _PENR; } set { _PENR = value; if (_PENR.Bit(4)) { // border select CurrentPen = 16; } else { // pen select CurrentPen = _PENR & 0x0f; } } } /// /// 0-15: Pen Registers /// 16: Border Colour /// public int[] ColourRegisters = new int[17]; /// /// The currently selected Pen /// private int CurrentPen; /// /// INKR (register 1) - Colour Selection /// This register takes a 5bits parameter which is a color-code. This color-code range from 0 to 31 but there's only 27 differents colors /// (because the Gate Array use a 3-states logic on the R,G and B signals, thus 3x3x3=27). /// INKR Color /// 7 6 5 4 3 2 1 0 /// 0 1 0 n n n n n where n is a color code (5 bits) /// /// The PEN affected by the INKR command is updated (almost) immediatly /// private byte _INKR; public byte INKR { get { return _INKR; } set { _INKR = value; ColourRegisters[CurrentPen] = _INKR & 0x1f; } } /// /// RMR (register 2) - Select screen mode and ROM configuration /// This register control the interrupt counter (reset), the upper and lower ROM paging and the video mode. /// RMR Commands /// 7 6 5 4 3 2 1 0 /// 1 0 0 I UR LR VM--> /// /// I : if set (1), this will reset the interrupt counter /// UR : Enable (0) or Disable (1) the upper ROM paging (&C000 to &FFFF). You can select which upper ROM with the I/O address &DF00 /// LR : Enable (0) or Disable (1) the lower ROM paging /// VM : Select the video mode 0,1,2 or 3 (it will take effect after the next HSync) /// private byte _RMR; public byte RMR { get { return _RMR; } set { _RMR = value; //ScreenMode = _RMR & 0x03; var sm = _RMR & 0x03; if (sm != 1) { } if ((_RMR & 0x08) != 0) _machine.UpperROMPaged = false; else _machine.UpperROMPaged = true; if ((_RMR & 0x04) != 0) _machine.LowerROMPaged = false; else _machine.LowerROMPaged = true; if (_RMR.Bit(4)) { // reset interrupt counter InterruptCounter = 0; } } } /// /// RAMR (register 3) - RAM Banking /// This register exists only in CPCs with 128K RAM (like the CPC 6128, or CPCs with Standard Memory Expansions) /// Note: In the CPC 6128, the register is a separate PAL that assists the Gate Array chip /// /// Bit Value Function /// 7 1 Gate Array function 3 /// 6 1 /// 5 b 64K bank number(0..7); always 0 on an unexpanded CPC6128, 0-7 on Standard Memory Expansions /// 4 b /// 3 b /// 2 x RAM Config(0..7) /// 1 x "" /// 0 x "" /// /// The 3bit RAM Config value is used to access the second 64K of the total 128K RAM that is built into the CPC 6128 or the additional 64K-512K of standard memory expansions. /// These contain up to eight 64K ram banks, which are selected with bit 3-5. A standard CPC 6128 only contains bank 0. Normally the register is set to 0, so that only the /// first 64K RAM are used (identical to the CPC 464 and 664 models). The register can be used to select between the following eight predefined configurations only: /// /// -Address- 0 1 2 3 4 5 6 7 /// 0000-3FFF RAM_0 RAM_0 RAM_4 RAM_0 RAM_0 RAM_0 RAM_0 RAM_0 /// 4000-7FFF RAM_1 RAM_1 RAM_5 RAM_3 RAM_4 RAM_5 RAM_6 RAM_7 /// 8000-BFFF RAM_2 RAM_2 RAM_6 RAM_2 RAM_2 RAM_2 RAM_2 RAM_2 /// C000-FFFF RAM_3 RAM_7 RAM_7 RAM_7 RAM_3 RAM_3 RAM_3 RAM_3 /// /// The Video RAM is always located in the first 64K, VRAM is in no way affected by this register /// private byte _RAMR; /// /// This is actually implemented outside of here. These values do nothing. /// public byte RAMR { get { return _RAMR; } set { _RAMR = value; } } /// /// The selected screen mode (updated after every HSYNC) /// private int ScreenMode; /// /// Simulates the internal 6bit INT counter /// private int _interruptCounter; public int InterruptCounter { get { return _interruptCounter; } set { _interruptCounter = value; } } /// /// Interrupts are delayed when a VSYNC occurs /// private int VSYNCDelay; /// /// Signals that the frame end has been reached /// public bool FrameEnd; /// /// Internal phase clock /// private int ClockCounter; /// /// Master frame clock counter /// public int FrameClock; /// /// Simulates the gate array memory /WAIT line /// private bool WaitLine; /// /// 16-bit address - read from the CRCT /// private short _MA; private short MA { get { _MA = CRCT.MA; return _MA; } } /// /// Set when the HSYNC signal is detected from the CRCT /// private bool HSYNC; // /// // /// Is set when an initial HSYNC is seen from the CRCT // /// On real hardware interrupt generation is based on the falling edge of the HSYNC signal // /// So in this emulation, once the falling edge is detected, interrupt processing happens // /// // private bool HSYNC_falling; /// /// Used to count HSYNCs during a VSYNC /// private int HSYNC_counter; /// /// Set when the VSYNC signal is detected from the CRCT /// private bool VSYNC; /// /// TRUE when the /INT pin is held low /// private bool InterruptRaised; /// /// Counts the GA cycles that the /INT pin should be held low /// private int InterruptHoldCounter; /// /// Set at the start of a new frame /// public bool IsNewFrame; /// /// Set when a new line is beginning /// public bool IsNewLine; /// /// Horizontal Character Counter /// private int HCC; /// /// Vertical Line Counter /// private int VLC; /// /// The first video byte fetched /// private byte VideoByte1; /// /// The second video byte fetched /// private byte VideoByte2; #endregion #region Clock Business /// /// Called every CPU cycle /// In reality the GA is clocked at 16Mhz (4 times the frequency of the CPU) /// Therefore this method has to take care of: /// 4 GA cycles /// 1 CRCT cycle every 4 calls /// 1 PSG cycle every 4 calls /// 1 CPU cycle (uncontended) /// public void ClockCycle() { // gatearray uses 4-phase clock to supply clocks to other devices switch (ClockCounter) { case 0: CRCT.ClockCycle(); WaitLine = false; break; case 1: WaitLine = true; // detect new scanline and upcoming new frame on next render cycle //FrameDetector(); break; case 2: // video fetch WaitLine = true; //FetchByte(1); break; case 3: // video fetch and render WaitLine = true; //FetchByte(2); GACharacterCycle(); //PixelGenerator(); break; } if (!HSYNC && CRCT.HSYNC) { HSYNC = true; } // run the interrupt generator routine InterruptGenerator(); if (!CRCT.HSYNC) { HSYNC = false; } // conditional CPU cycle DoConditionalCPUCycle(); AdvanceClock(); } /// /// Increments the internal clock counters by one /// private void AdvanceClock() { FrameClock++; ClockCounter++; if (ClockCounter == 4) ClockCounter = 0; // check for frame end if (FrameClock == FrameLength) { FrameEnd = true; } } /// /// Runs a 4 Mhz CPU cycle if neccessary /// /WAIT line status is a factor here /// private void DoConditionalCPUCycle() { if (!WaitLine) { // /WAIT line is NOT active CPU.ExecuteOne(); return; } // /WAIT line is active switch (ClockCounter) { case 2: case 3: // gate array video fetch is occuring // check for memory access if (BUSRQ > 0) { // memory action upcoming - CPU clock is halted CPU.TotalExecutedCycles++; } break; case 1: // CPU accesses RAM if it's performing a non-opcode read or write // assume for now that an opcode fetch is always looking at PC if (BUSRQ == PCh) { // opcode fetch memory action upcoming - CPU clock is halted CPU.TotalExecutedCycles++; } else { // no fetch, or non-opcode fetch CPU.ExecuteOne(); } break; } } #endregion #region Frame & Interrupt Handling /// /// The CRCT builds the picture in a strange way, so that the top left of the display area is the first pixel from /// video RAM. The borders come either side of the HSYNC and VSYNCs later on: /// https://web.archive.org/web/20170501112330im_/http://www.grimware.org/lib/exe/fetch.php/documentations/devices/crtc.6845/crtc.standard.video.frame.png?w=800&h=500 /// Therefore when the gate array initialises, we will attempt end the frame early in order to /// sync up at the point where VSYNC is active and HSYNC just begins. This is roughly how a CRT monitor would display the picture. /// The CRT would start a new line at the point where an HSYNC is detected. /// private void FrameDetector() { if (CRCT.HSYNC && !IsNewLine) { // start of a new line on the next render cycle IsNewLine = true; // process scanline //CRT.CurrentLine.CommitScanline(); // check for end of frame if (CRCT.VSYNC && !IsNewFrame) { // start of a new frame on the next render cycle IsNewFrame = true; //FrameEnd = true; VLC = 0; } else if (!CRCT.VSYNC) { // increment line counter VLC++; IsNewFrame = false; } HCC = 0; // update screenmode //ScreenMode = RMR & 0x03; //CRT.CurrentLine.InitScanline(ScreenMode, VLC); } else if (!CRCT.HSYNC) { // reset the flags IsNewLine = false; IsNewFrame = false; } } /// /// Handles interrupt generation /// private void InterruptGenerator() { if (HSYNC && !CRCT.HSYNC) { // falling edge of the HSYNC detected InterruptCounter++; if (CRCT.VSYNC) { if (HSYNC_counter >= 2) { // x2 HSYNC have happened during VSYNC if (InterruptCounter >= 32) { // no interrupt InterruptCounter = 0; } else if (InterruptCounter < 32) { // interrupt InterruptRaised = true; InterruptCounter = 0; } HSYNC_counter = 0; } else { HSYNC_counter++; } } if (InterruptCounter == 52) { // gatearray should raise an interrupt InterruptRaised = true; InterruptCounter = 0; } } if (InterruptRaised) { // interrupt should been raised CPU.FlagI = true; InterruptHoldCounter++; // the INT signal should be held low for 1.4us. // in gatearray cycles, this equates to 22.4 // we will round down to 22 for emulation purposes if (InterruptHoldCounter >= 22) { CPU.FlagI = false; InterruptRaised = false; InterruptHoldCounter = 0; } } } #endregion #region Rendering Business /// /// Builds up current scanline character information /// Ther GA modifies HSYNC and VSYNC signals before they are sent to the monitor /// This is handled here /// Runs at 1Mhz /// private void GACharacterCycle() { if (CRCT.VSYNC && CRCT.HSYNC) { // both hsync and vsync active CurrentLine.AddCharacter(Phase.HSYNCandVSYNC); } else if (CRCT.VSYNC) { // vsync is active but hsync is not CurrentLine.AddCharacter(Phase.VSYNC); } else if (CRCT.HSYNC) { // hsync is active but vsync is not CurrentLine.AddCharacter(Phase.HSYNC); } else if (!CRCT.DISPTMG) { // border generation CurrentLine.AddCharacter(Phase.BORDER); } else if (CRCT.DISPTMG) { // pixels generated from video RAM CurrentLine.AddCharacter(Phase.DISPLAY); } } /// /// Holds the upcoming video RAM addresses for the next scanline /// Firmware default size is 80 (40 characters - 2 bytes per character) /// private ushort[] NextVidRamLine = new ushort[40 * 2]; /// /// The current character line we are working from /// private CharacterLine CurrentLine; /// /// List of screen lines as they are built up /// private List ScreenLines = new List(); /// /// Pixel value lookups for every scanline byte value /// Based on the lookup at https://github.com/gavinpugh/xnacpc /// private int[][] ByteLookup = new int[4][]; private void InitByteLookup() { int pix; for (int m = 0; m < 4; m++) { int pos = 0; ByteLookup[m] = new int[256 * 8]; for (int b = 0; b < 256; b++) { switch (m) { case 0: pix = b & 0xaa; pix = (((pix & 0x80) >> 7) | ((pix & 0x08) >> 2) | ((pix & 0x20) >> 3) | ((pix & 0x02) << 2)); for (int c = 0; c < 4; c++) ByteLookup[m][pos++] = pix; pix = b & 0x55; pix = (((pix & 0x40) >> 6) | ((pix & 0x04) >> 1) | ((pix & 0x10) >> 2) | ((pix & 0x01) << 3)); for (int c = 0; c < 4; c++) ByteLookup[m][pos++] = pix; break; case 1: pix = (((b & 0x80) >> 7) | ((b & 0x08) >> 2)); ByteLookup[m][pos++] = pix; ByteLookup[m][pos++] = pix; pix = (((b & 0x40) >> 6) | ((b & 0x04) >> 1)); ByteLookup[m][pos++] = pix; ByteLookup[m][pos++] = pix; pix = (((b & 0x20) >> 5) | (b & 0x02)); ByteLookup[m][pos++] = pix; ByteLookup[m][pos++] = pix; pix = (((b & 0x10) >> 4) | ((b & 0x01) << 1)); ByteLookup[m][pos++] = pix; ByteLookup[m][pos++] = pix; break; case 2: for (int i = 7; i >= 0; i--) ByteLookup[m][pos++] = ((b & (1 << i)) != 0) ? 1 : 0; break; case 3: pix = b & 0xaa; pix = (((pix & 0x80) >> 7) | ((pix & 0x08) >> 2) | ((pix & 0x20) >> 3) | ((pix & 0x02) << 2)); for (int c = 0; c < 4; c++) ByteLookup[m][pos++] = pix; pix = b & 0x55; pix = (((pix & 0x40) >> 6) | ((pix & 0x04) >> 1) | ((pix & 0x10) >> 2) | ((pix & 0x01) << 3)); for (int c = 0; c < 4; c++) ByteLookup[m][pos++] = pix; break; } } } } /// /// Runs at HSYNC *AFTER* the scanline has been commmitted /// Sets up the upcoming memory addresses for the next scanline /// private void CalculateNextScreenMemory() { var verCharCount = CRCT.VCC; var verRasCount = CRCT.VLC; var screenWidthByteCount = CRCT.DisplayWidth * 2; NextVidRamLine = new ushort[screenWidthByteCount * 2]; var screenHeightCharCount = CRCT.DisplayHeightInChars; var screenAddress = CRCT.MA; int baseAddress = ((screenAddress << 2) & 0xf000); int offset = (screenAddress * 2) & 0x7ff; int x = offset + ((verCharCount * screenWidthByteCount) & 0x7ff); int y = baseAddress + (verRasCount * 0x800); for (int b = 0; b < screenWidthByteCount; b++) { NextVidRamLine[b] = (ushort)(y + x); x++; x &= 0x7ff; } } /// /// Called at the start of HSYNC, this renders the currently built-up scanline /// private void RenderScanline() { // memory addresses int cRow = CRCT.VCC; int cRas = CRCT.VLC; int screenByteWidth = CRCT.DisplayWidth * 2; var screenHeightCharCount = CRCT.DisplayHeightInChars; //CalculateNextScreenMemory(); var crctAddr = CRCT.DStartHigh << 8; crctAddr |= CRCT.DStartLow; var baseAddr = ((crctAddr << 2) & (0xF000)); //CRCT.VideoPageBase;// var baseOffset = (crctAddr * 2) & 0x7FF; //CRCT.VideoRAMOffset * 2; // var xA = baseOffset + ((cRow * screenByteWidth) & 0x7ff); var yA = baseAddr + (cRas * 2048); // border and display int pix = 0; int scrByte = 0; for (int i = 0; i < CurrentLine.PhaseCount; i++) { // every character renders 8 pixels switch (CurrentLine.Phases[i]) { case Phase.NONE: break; case Phase.HSYNC: break; case Phase.HSYNCandVSYNC: break; case Phase.VSYNC: break; case Phase.BORDER: // output current border colour for (pix = 0; pix < 16; pix++) { CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[16]]); } break; case Phase.DISPLAY: // each character references 2 bytes in video RAM byte data; for (int by = 0; by < 2; by++) { ushort addr = (ushort)(yA + xA); data = _machine.FetchScreenMemory(addr); scrByte++; switch (CurrentLine.ScreenMode) { case 0: pix = data & 0xaa; pix = (((pix & 0x80) >> 7) | ((pix & 0x08) >> 2) | ((pix & 0x20) >> 3) | ((pix & 0x02) << 2)); for (int c = 0; c < 4; c++) CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); pix = data & 0x55; pix = (((pix & 0x40) >> 6) | ((pix & 0x04) >> 1) | ((pix & 0x10) >> 2) | ((pix & 0x01) << 3)); for (int c = 0; c < 4; c++) CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); break; case 1: pix = (((data & 0x80) >> 7) | ((data & 0x08) >> 2)); CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); pix = (((data & 0x40) >> 6) | ((data & 0x04) >> 1)); CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); pix = (((data & 0x20) >> 5) | (data & 0x02)); CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); pix = (((data & 0x10) >> 4) | ((data & 0x01) << 1)); CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); break; case 2: pix = data; CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(7) ? 1 : 0]]); CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(6) ? 1 : 0]]); CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(5) ? 1 : 0]]); CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(4) ? 1 : 0]]); CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(3) ? 1 : 0]]); CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(2) ? 1 : 0]]); CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(1) ? 1 : 0]]); CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(0) ? 1 : 0]]); break; case 3: pix = data & 0xaa; pix = (((pix & 0x80) >> 7) | ((pix & 0x08) >> 2) | ((pix & 0x20) >> 3) | ((pix & 0x02) << 2)); for (int c = 0; c < 4; c++) CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); pix = data & 0x55; pix = (((pix & 0x40) >> 6) | ((pix & 0x04) >> 1) | ((pix & 0x10) >> 2) | ((pix & 0x01) << 3)); for (int c = 0; c < 4; c++) CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); break; } xA++; xA &= 0x7ff; } break; } } // add to the list ScreenLines.Add(new CharacterLine { ScreenMode = CurrentLine.ScreenMode, Phases = CurrentLine.Phases.ToList(), Pixels = CurrentLine.Pixels.ToList() }); } #endregion #region Public Methods /// /// Called when the Z80 acknowledges an interrupt /// public void IORQA() { // bit 5 of the interrupt counter is reset InterruptCounter &= ~(1 << 5); } private int slCounter = 0; private int slBackup = 0; /// /// Fired when the CRCT flags HSYNC /// public void OnHSYNC() { HSYNC = true; slCounter++; // commit the scanline RenderScanline(); // setup vid memory for next scanline CalculateNextScreenMemory(); if (CRCT.VLC == 0) { // update screenmode ScreenMode = _RMR & 0x03; } // setup scanline for next CurrentLine.Clear(ScreenMode); } /// /// Fired when the CRCT flags VSYNC /// public void OnVSYNC() { FrameEnd = true; slBackup = slCounter; slCounter = 0; } #endregion #region IVideoProvider public int[] ScreenBuffer; private int _virtualWidth; private int _virtualHeight; private int _bufferWidth; private int _bufferHeight; public int BackgroundColor { get { return CPCHardwarePalette[0]; } } public int VirtualWidth { get { return _virtualWidth; } set { _virtualWidth = value; } } public int VirtualHeight { get { return _virtualHeight; } set { _virtualHeight = value; } } public int BufferWidth { get { return _bufferWidth; } set { _bufferWidth = value; } } public int BufferHeight { get { return _bufferHeight; } set { _bufferHeight = value; } } public int SysBufferWidth; public int SysBufferHeight; public int VsyncNumerator { get { return 200000000; } set { } } public int VsyncDenominator { get { return Z80ClockSpeed; } } public int[] GetVideoBuffer() { // get only lines that have pixel data var lines = ScreenLines.Where(a => a.Pixels.Count > 0).ToList(); var height = lines.Count(); int pos = 0; int lCount = 0; foreach (var l in lines) { var lCop = l.Pixels.ToList(); var len = l.Pixels.Count; if (l.Phases.Contains(Phase.VSYNC) && l.Phases.Contains(Phase.BORDER)) continue; if (len < 320) continue; var pad = BufferWidth - len; if (pad < 0) { // trim the left and right var padPos = pad * -1; var excessL = padPos / 2; var excessR = excessL + (padPos % 2); for (int i = 0; i < excessL; i++) { var lThing = lCop.First(); lCop.Remove(lThing); } for (int i = 0; i < excessL; i++) { var lThing = lCop.Last(); lCop.Remove(lThing); } } var lPad = pad / 2; var rPad = lPad + (pad % 2); for (int i = 0; i < 2; i++) { lCount++; for (int pL = 0; pL < lPad; pL++) { ScreenBuffer[pos++] = 0; } for (int pix = 0; pix < lCop.Count; pix++) { ScreenBuffer[pos++] = lCop[pix]; } for (int pR = 0; pR < rPad; pR++) { ScreenBuffer[pos++] = 0; } } if (lCount >= BufferHeight - 2) { break; } } ScreenLines.Clear(); return ScreenBuffer; /* switch (borderType) { // crop to 768x272 (544) // borders 64px - 64 scanlines case AmstradCPC.BorderType.Uniform: /* var slSize = 64; var dispLines = (24 * 8) * 2; var origTopBorder = (7 * 8) * 2; var origBotBorder = (5 * 8) * 2; var lR = 16; var rR = 16; var trimTop = origTopBorder - slSize; var startP = SysBufferWidth * (origTopBorder - 64); var index1 = 0; // line by line int cnt = 0; for (int line = startP; line < ScreenBuffer.Length; line += SysBufferWidth) { cnt++; // pixels in line for (int p = lR; p < SysBufferWidth - rR; p++) { if (index1 == croppedBuffer.Length) break; croppedBuffer[index1++] = ScreenBuffer[line + p]; } } return croppedBuffer; */ /* var slWidth = BufferWidth; return ScreenBuffer; break; } */ //return ScreenBuffer; } public void SetupScreenSize() { SysBufferWidth = 800; SysBufferHeight = 600; BufferHeight = SysBufferHeight; BufferWidth = SysBufferWidth; VirtualHeight = BufferHeight; VirtualWidth = BufferWidth; ScreenBuffer = new int[BufferWidth * BufferHeight]; croppedBuffer = ScreenBuffer; switch (borderType) { case AmstradCPC.BorderType.Uncropped: break; case AmstradCPC.BorderType.Uniform: BufferWidth = 800; BufferHeight = 600; VirtualHeight = BufferHeight; VirtualWidth = BufferWidth; croppedBuffer = new int[BufferWidth * BufferHeight]; break; case AmstradCPC.BorderType.Widescreen: break; } } protected int[] croppedBuffer; private AmstradCPC.BorderType _borderType; public AmstradCPC.BorderType borderType { get { return _borderType; } set { _borderType = value; } } #endregion #region IPortIODevice /// /// Device responds to an IN instruction /// public bool ReadPort(ushort port, ref int result) { // gate array is OUT only return false; } /// /// Device responds to an OUT instruction /// public bool WritePort(ushort port, int result) { BitArray portBits = new BitArray(BitConverter.GetBytes(port)); BitArray dataBits = new BitArray(BitConverter.GetBytes((byte)result)); byte portUpper = (byte)(port >> 8); byte portLower = (byte)(port & 0xff); // The gate array is selected when bit 15 of the I/O port address is set to "0" and bit 14 of the I/O port address is set to "1" bool accessed = false; if (!portUpper.Bit(7) && portUpper.Bit(6)) accessed = true; if (!accessed) return accessed; // Bit 9 and 8 of the data byte define the function to access if (!dataBits[6] && !dataBits[7]) { // select pen PENR = (byte)result; } if (dataBits[6] && !dataBits[7]) { // select colour for selected pen INKR = (byte)result; } if (!dataBits[6] && dataBits[7]) { // select screen mode, ROM configuration and interrupt control RMR = (byte)result; } if (dataBits[6] && dataBits[7]) { // RAM memory management RAMR = (byte)result; } return true; } #endregion #region Serialization public void SyncState(Serializer ser) { ser.BeginSection("GateArray"); ser.SyncEnum(nameof(ChipType), ref ChipType); ser.Sync(nameof(_PENR), ref _PENR); ser.Sync(nameof(_INKR), ref _INKR); ser.Sync(nameof(_RMR), ref _RMR); ser.Sync(nameof(_RAMR), ref _RAMR); ser.Sync(nameof(ColourRegisters), ref ColourRegisters, false); ser.Sync(nameof(CurrentPen), ref CurrentPen); ser.Sync(nameof(ClockCounter), ref ClockCounter); ser.Sync(nameof(FrameClock), ref FrameClock); ser.Sync(nameof(FrameEnd), ref FrameEnd); ser.Sync(nameof(WaitLine), ref WaitLine); ser.Sync(nameof(_interruptCounter), ref _interruptCounter); ser.Sync(nameof(VSYNCDelay), ref VSYNCDelay); ser.Sync(nameof(ScreenMode), ref ScreenMode); ser.Sync(nameof(HSYNC), ref HSYNC); //ser.Sync(nameof(HSYNC_falling), ref HSYNC_falling); ser.Sync(nameof(HSYNC_counter), ref HSYNC_counter); ser.Sync(nameof(VSYNC), ref VSYNC); ser.Sync(nameof(InterruptRaised), ref InterruptRaised); ser.Sync(nameof(InterruptHoldCounter), ref InterruptHoldCounter); ser.Sync(nameof(_MA), ref _MA); ser.Sync(nameof(IsNewFrame), ref IsNewFrame); ser.Sync(nameof(IsNewLine), ref IsNewLine); ser.Sync(nameof(HCC), ref HCC); ser.Sync(nameof(VLC), ref VLC); ser.Sync(nameof(VideoByte1), ref VideoByte1); ser.Sync(nameof(VideoByte2), ref VideoByte2); ser.Sync(nameof(NextVidRamLine), ref NextVidRamLine, false); ser.EndSection(); } #endregion #region Enums, Classes & Lookups /// /// Represents a single scanline (in characters) /// public class CharacterLine { /// /// Screenmode is defined at each HSYNC (start of a new character line) /// Therefore we pass the mode in via constructor /// //public CharacterLine(int screenMode) //{ //ScreenMode = screenMode; //} public int ScreenMode = 1; public List Phases = new List(); public List Pixels = new List(); /// /// Adds a new horizontal character to the list /// public void AddCharacter(Phase phase) { Phases.Add(phase); } public int PhaseCount { get { return Phases.Count(); } } public void Clear(int screenMode) { ScreenMode = screenMode; Phases.Clear(); Pixels.Clear(); } } [Flags] public enum Phase : int { /// /// Nothing /// NONE = 0, /// /// Border is being rendered /// BORDER = 1, /// /// Display rendered from video RAM /// DISPLAY = 2, /// /// HSYNC in progress /// HSYNC = 3, /// /// VSYNC in process /// VSYNC = 4, /// /// HSYNC occurs within a VSYNC /// HSYNCandVSYNC = 5 } public enum GateArrayType { /// /// CPC 464 /// The first version of the Gate Array is the 40007 and was released with the CPC 464 /// Amstrad40007, /// /// CPC 664 /// Later, the CPC 664 came out fitted with the 40008 version (and at the same time, the CPC 464 was also upgraded with this version). /// This version is pinout incompatible with the 40007 (that's why the upgraded 464 of this period have two Gate Array slots on the motherboard, /// one for a 40007 and one for a 40008) /// Amstrad40008, /// /// CPC 6128 /// The CPC 6128 was released with the 40010 version (and the CPC 464 and 664 manufactured at that time were also upgraded to this version). /// The 40010 is pinout compatible with the previous 40008 /// Amstrad40010, /// /// Costdown CPC /// In the last serie of CPC 464 and 6128 produced by Amstrad in 1988, a small ASIC chip have been used to reduce the manufacturing costs. /// This ASIC emulates the Gate Array, the PAL and the CRTC 6845. And no, there is no extra features like on the Amstrad Plus. /// The only noticeable difference seems to be about the RGB output levels which are not exactly the same than those produced with a real Gate Array /// Amstrad40226, /// /// Plus & GX-4000 /// All the Plus range is built upon a bigger ASIC chip which is integrating many features of the classic CPC (FDC, CRTC, PPI, Gate Array/PAL) and all /// the new Plus specific features. The Gate Array on the Plus have a new register, named RMR2, to expand the ROM mapping functionnalities of the machine. /// This register requires to be unlocked first to be available. And finally, the RGB levels produced by the ASIC on the Plus are noticeably differents /// Amstrad40489, } #endregion } }