// Chip.cpp: implementation of the CMotherBoard class.
//


#include "stdafx.h"
#include "Ini.h"
#include "Board.h"
#include "BKErrMsgSys.h"

#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#define new DEBUG_NEW
#endif

//////////////////////////////////////////////////////////////////////
// Construction/Destruction

CMotherBoard::CMotherBoard(BOARD_MODEL model)
	: m_pParent(nullptr)   // Указатель на окно, в которое посылаются оповещающие сообщения
	, m_pTimerThread(nullptr)
	, m_pSpeaker(nullptr)
	, m_pCovox(nullptr)
	, m_pAY8910(nullptr)
	, m_pDebugger(nullptr)
	, m_pSoundBuffer(nullptr)
	, m_bAskForBreak(false)
	, m_nGotoAddress(ADDRESS_NONE)
	, m_nCPUTicks(0)
	  //переменные,задающие конкретную модель
	, m_BoardModel(model)
	, m_nStartAddr(0100000)
	, m_nBKPortsIOArea(BK_PURE_PORTSIO_AREA)
{
	SetCPUBaseFreq(CPU_SPEED_BK10); //частота задаётся этой константой
	ZeroMemory(m_MemoryMap, sizeof(m_MemoryMap));
	m_cpu.AttachChip(this);
	m_fdd.AttachParent(this);
	m_fdd.init_A16M_10(&m_ConfBKModel, ALTPRO_A16M_STD10_MODE);
	m_nMediaTickCount = 0;
	m_nBufPos = 0;
	// Инициализация модуля памяти
	m_pMemory = new BYTE[0200000];
	InitMemoryValues(0200000);
	Reset();
}

CMotherBoard::~CMotherBoard()
{
	SAFE_DELETE_ARRAY(m_pSoundBuffer);
	SAFE_DELETE_ARRAY(m_pMemory);
}

DWORD CMotherBoard::GetConfiguration()
{
	return MSF_CONF_BK1001;
}

BOARD_MODEL CMotherBoard::GetBoardModel()
{
	return m_BoardModel;
}

void CMotherBoard::AttachWindow(CMainFrame *pParent)
{
	m_pParent = pParent;
}

void CMotherBoard::AttachSound(CBkSound *pSnd)
{
	m_pSound = pSnd;
}

void CMotherBoard::AttachSpeaker(CSpeaker *pDevice)
{
	m_pSpeaker = pDevice;
}

void CMotherBoard::AttachCovox(CCovox *pDevice)
{
	m_pCovox = pDevice;
}

void CMotherBoard::AttachAY8910(CEMU2149 *pDevice)
{
	m_pAY8910 = pDevice;
}

void CMotherBoard::AttachDebugger(CDebugger *pDevice)
{
	m_pDebugger = pDevice;
}

int CMotherBoard::GetMTC()
{
	//переменные для медиа системы
	m_nMediaTicksPerFrame = g_Config.m_nSoundSampleRate / CPU_FRAMERATE;
	m_nLowBound = m_nMediaTicksPerFrame * CPU_FRAMERATE * 10;
	m_nHighBound = m_nLowBound * 100;
	m_nSoundBufferLen = m_nMediaTicksPerFrame * SAMPLE_LENGTH;
	SAFE_DELETE_ARRAY(m_pSoundBuffer);
	m_pSoundBuffer = new BYTE[m_nSoundBufferLen];

	//установку данных буфера достаточно сделать один раз.
	// если осциллограф рисуется через opengl, то там же создаётся контекст.
	// поэтому, если установка буфера будет куда-нибудь перенесена, то контекст для opengl
	// нужно будет создавать отсюда вручную.
	m_pParent->GetOscillatorViewPtr()->SetBuffer(m_pSoundBuffer, m_nSoundBufferLen);

	return m_nMediaTicksPerFrame;
}

void CMotherBoard::InitMemoryValues(int nMemSize)
{
	WORD val = 0;
	BYTE flag = 0;
	auto pPtr = reinterpret_cast<WORD *>(m_pMemory);

	for (int i = 0; i < nMemSize/2; i++, flag--)
	{
		pPtr[i] = val;
		val = ~val;

		if (flag == 192)
		{
			val = ~val;
			flag = 0;
		}
	}
}

void CMotherBoard::OnReset()
{
	if (m_pParent)
	{
		m_pParent->GetScreen()->SetPalette(0);
		m_pParent->GetScreen()->SetRegister(m_reg177664);
	}

	m_cpu.ResetCPU();
	m_nCPUTicks = 0;
	m_fTickCount = 0.0;
	m_fMediaTicks = 0.0;
	m_fMemoryTicks = 0.0;
	m_fFDDTicks = 0.0;
	m_nVideoAddress = 0; //видео адрес, младшие 6 бит - счётчик строк внутри строки
	m_bHgate = false;
	m_bVgate = true;
	m_nVGateCounter = 64;
	m_nLineCounter = 0; //счётчик видео строк
}


/*
В функциях GetByte, GetWord, SetByte, SetWord доступ к регистрам 0177700..0177712
не обрабатывается, т.к. эти функции должны вызываться только из CPU, и доступ к
регистрам обрабатывается там.
*/

BYTE CMotherBoard::GetByte(WORD addr)
{
	register int nBank = (addr >> 12) & 0x0f;
	register BYTE v;

	// Сперва проверим, на системные регистры
	if (nBank == 15)
	{
		register BKFDD_MODEL nBKFddType = GetFDDType();

		if (addr >= m_nBKPortsIOArea)
		{
			switch (nBKFddType)
			{
				case DEV_FDD_SMK512:
					switch (GetAltProMode())
					{
						case ALTPRO_SMK_SYS_MODE:
						case ALTPRO_SMK_ALL_MODE:
							//в режиме SYS и ALL в этом диапазоне можно читать
							goto gblb1;

						default:
							if (OnGetSystemRegister(addr, &v, true))
							{
								return v;
							}
					}

					//иначе, в этом диапазоне действуют обычные правила
					break;

				case DEV_FDD_A16M:
					switch (GetAltProMode())
					{
						case ALTPRO_A16M_START_MODE:
							//в режиме Start в этом диапазоне можно читать
gblb1:
							if (OnGetSystemRegister(addr, &v, true))
							{
								return v | m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007777)];
							}
							else
							{
								return m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007777)];
							}

						case ALTPRO_A16M_BASIC_MODE:

							//ещё одно исключение. в режиме бейсика m_nBKPortsIOArea == BK_PURE_PORTSIO_AREA целых 0600 байтов ещё
							if ((addr < BK_PURE_PORTSIO_AREA) && (GetAltProExtCode() & 010)) //если бейсик подключен
							{
								goto gblb1;
							}
					}

				//тут break; не нужен!!!
				//иначе, как в обычном режиме, смотрим порты
				default:

					//в обычном режиме тут только регистры могут быть
					if (OnGetSystemRegister(addr, &v, true))
					{
						return v;
					}
					else
					{
						throw CExceptionHalt(addr, _T("Can't read this address."));
					}

					break;
			}
		}
		else
		{
			//по адресам 170000-177000 для SMK если режим не SYS - всегда ОЗУ
			if (nBKFddType == DEV_FDD_SMK512 && GetAltProMode() != ALTPRO_A16M_START_MODE)
			{
				return m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007777)];
			}
		}
	}

	//теперь проверим на чтение из несуществующего банка
	if (m_MemoryMap[nBank].bReadable)
	{
		//если читать можно, заодно и ремап сделаем, может не понадобится переопределять везде эту функцию
		return m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007777)];
	}
	else
	{
		throw CExceptionHalt(addr, _T("Can't read this address."));
	}
}


WORD CMotherBoard::GetWord(WORD addr)
{
	register int nBank = (addr >> 12) & 0x0f;
	register WORD v;

	// Сперва проверим, на системные регистры
	if (nBank == 15)
	{
		register BKFDD_MODEL nBKFddType = GetFDDType();

		if (addr >= m_nBKPortsIOArea)
		{
			switch (nBKFddType)
			{
				case DEV_FDD_SMK512:
					switch (GetAltProMode())
					{
						case ALTPRO_SMK_SYS_MODE:
						case ALTPRO_SMK_ALL_MODE:
							//в режиме SYS и ALL в этом диапазоне можно читать
							goto gwlb1;

						default:
							if (OnGetSystemRegister(addr, &v, false))
							{
								return v;
							}
					}

					//иначе, в этом диапазоне действуют обычные правила
					break;

				case DEV_FDD_A16M:
					switch (GetAltProMode())
					{
						case ALTPRO_A16M_START_MODE:
							//в режиме Start в этом диапазоне можно читать
gwlb1:
							if (OnGetSystemRegister(addr, &v, false))
							{
								return v | *(WORD *)&m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007776)];
							}
							else
							{
								return *(WORD *)&m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007776)];
							}

						case ALTPRO_A16M_BASIC_MODE:

							//ещё одно исключение. в режиме бейсика m_nBKPortsIOArea == BK_PURE_PORTSIO_AREA целых 0600 байтов ещё
							if ((addr < BK_PURE_PORTSIO_AREA) && (GetAltProExtCode() & 010)) //если бейсик подключен
							{
								goto gwlb1;
							}
					}

				//тут break; не нужен!!!
				//иначе, как в обычном режиме, смотрим порты
				default:

					//в обычном режиме тут только регистры могут быть
					if (OnGetSystemRegister(addr, &v, false))
					{
						return v;
					}
					else
					{
						throw CExceptionHalt(addr, _T("Can't read this address."));
					}

					break;
			}
		}
		else
		{
			//по адресам 170000-177000 для SMK если режим не SYS - всегда ОЗУ
			if (nBKFddType == DEV_FDD_SMK512 && GetAltProMode() != ALTPRO_A16M_START_MODE)
			{
				return *(WORD *)&m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007776)];
			}
		}
	}

	//теперь проверим на чтение из несуществующего банка
	if (m_MemoryMap[nBank].bReadable)
	{
		//если читать можно, заодно и ремап сделаем, может не понадобится переопределять везде эту функцию
		return *(WORD *)&m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007776)];
	}
	else
	{
		throw CExceptionHalt(addr, _T("Can't read this address."));
	}
}


void CMotherBoard::GetByte(WORD addr, BYTE *pValue)
{
	*pValue = GetByte(addr);
}


void CMotherBoard::GetWord(WORD addr, WORD *pValue)
{
	*pValue = GetWord(addr);
}


void CMotherBoard::SetByte(WORD addr, BYTE value)
{
	register int nBank = (addr >> 12) & 0x0f;

	// Сперва проверим, на системные регистры
	if (nBank == 15)
	{
		register BKFDD_MODEL nBKFddType = GetFDDType();

		if (addr >= m_nBKPortsIOArea)
		{
			switch (nBKFddType)
			{
				case DEV_FDD_SMK512:
					switch (GetAltProMode())
					{
						case ALTPRO_SMK_HLT11_MODE:
						case ALTPRO_SMK_HLT10_MODE:
							//case ALTPRO_SMK_OZU10_MODE:
							//в режиме Hlt11 и Hlt10 в этом диапазоне можно писать, а для старой версии, ещё и в режиме ОЗУ10
							goto sblb1;

						default:
							if (OnSetSystemRegister(addr, value, true))
							{
								return;
							}
					}

					//иначе, в этом диапазоне действуют обычные правила
					break;

				case DEV_FDD_A16M:

					//в режиме Hlt11 в этом диапазоне можно писать
					if (GetAltProMode() == ALTPRO_A16M_HLT11_MODE)
					{
sblb1:
						OnSetSystemRegister(addr, value, true);
						m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007777)] = value;
						return;
					}

				//иначе, как в обычном режиме, смотрим порты
				default:

					//в обычном режиме тут только регистры могут быть
					if (OnSetSystemRegister(addr, value, true))
					{
						return;
					}
					else
					{
						throw CExceptionHalt(addr, _T("Can't write this address."));
					}

					break;
			}
		}
		else
		{
			if (nBKFddType == DEV_FDD_SMK512 && GetAltProMode() != ALTPRO_A16M_START_MODE)
			{
				m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007777)] = value;
				return;
			}
		}
	}

	if (m_MemoryMap[nBank].bWritable)
	{
		m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007777)] = value;
		//если это у нас экран, то передадим в эмулятор экрана данные
	}
	else
	{
		throw CExceptionHalt(addr, _T("Can't write this address."));
	}
}


void CMotherBoard::SetWord(WORD addr, WORD value)
{
	register int nBank = (addr >> 12) & 0x0f;

	// Сперва проверим, на системные регистры
	if (nBank == 15)
	{
		register BKFDD_MODEL nBKFddType = GetFDDType();

		if (addr >= m_nBKPortsIOArea)
		{
			switch (nBKFddType)
			{
				case DEV_FDD_SMK512:
					switch (GetAltProMode())
					{
						case ALTPRO_SMK_HLT11_MODE:
						case ALTPRO_SMK_HLT10_MODE:
							//case ALTPRO_SMK_OZU10_MODE:
							//в режиме Hlt11 и Hlt10 в этом диапазоне можно писать, а для старой версии, ещё и в режиме ОЗУ10
							goto swlb1;

						default:
							if (OnSetSystemRegister(addr, value, false))
							{
								return;
							}
					}

					//иначе, в этом диапазоне действуют обычные правила
					break;

				case DEV_FDD_A16M:

					//в режиме Hlt11 в этом диапазоне можно писать
					if (GetAltProMode() == ALTPRO_A16M_HLT11_MODE)
					{
swlb1:
						OnSetSystemRegister(addr, value, false);
						*(WORD *)&m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007776)] = value;
						return;
					}

				//иначе, как в обычном режиме, смотрим порты
				default:

					//в обычном режиме тут только регистры могут быть
					if (OnSetSystemRegister(addr, value, false))
					{
						return;
					}
					else
					{
						throw CExceptionHalt(addr, _T("Can't write this address."));
					}

					break;
			}
		}
		else
		{
			if (nBKFddType == DEV_FDD_SMK512 && GetAltProMode() != ALTPRO_A16M_START_MODE)
			{
				*(WORD *)&m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007776)] = value;
				return;
			}
		}
	}

	if (m_MemoryMap[nBank].bWritable)
	{
		*(WORD *)&m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007776)] = value;
	}
	else
	{
		throw CExceptionHalt(addr, _T("Can't write this address."));
	}
}


/*
GetByteIndirect,GetWordIndirect,SetByteIndirect,SetWordIndirect
этими функциями пользуются многие вспомогательные модули для того чтобы читать данные
напрямую из памяти, и как правило чтобы читать/писать что-то из/в регистров, а т.к.
внутренние системные регистры переехали в CPU, здесь делается перехват их адресов, чтобы
получить/записать значения из/в регистров в CPU
и да. тут не делаются проверки на невозможность, читать/писать в память, ибо эти функции
не CPU. и в экранную память вывод тоже не делается.
*/

BYTE CMotherBoard::GetByteIndirect(WORD addr)
{
	register int nBank = (addr >> 12) & 0x0f;
	register BYTE v;

	if (OnGetSystemRegister(addr, &v, true))
	{
		return (GetAltProMode() == ALTPRO_A16M_START_MODE) ? (v | m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007777)]) : v;
	}

	if ((0177700 <= addr) && (addr < 0177714))
	{
		register WORD w = m_cpu.GetSysRegs(addr);

		if (GetAltProMode() == ALTPRO_A16M_START_MODE)
		{
			w |= m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007777)];
		}

		return (addr & 1) ? HIBYTE(w) : LOBYTE(w);
	}

	return m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007777)];
}


WORD CMotherBoard::GetWordIndirect(WORD addr)
{
	register int nBank = (addr >> 12) & 0x0f;
	register WORD v;


	if (OnGetSystemRegister(addr, &v, false))
	{
		return (GetAltProMode() == ALTPRO_A16M_START_MODE) ? (v | *(WORD *)&m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007776)]) : v;
	}

	if ((0177700 <= addr) && (addr < 0177714))
	{
		register WORD w = m_cpu.GetSysRegs(addr);

		if (GetAltProMode() == ALTPRO_A16M_START_MODE)
		{
			w |= *(WORD *)&m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007776)];
		}

		return w;
	}

	return *(WORD *)&m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007776)];
}


void CMotherBoard::SetByteIndirect(WORD addr, BYTE value)
{
	register int nBank = (addr >> 12) & 0x0f;

	if (OnSetSystemRegister(addr, value, true))
	{
		if (GetAltProMode() == ALTPRO_A16M_HLT11_MODE || (GetAltProMode() == ALTPRO_SMK_HLT10_MODE && GetFDDType() == DEV_FDD_SMK512))
		{
			m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007777)] = value;
		}

		return;
	}

	if ((0177700 <= addr) && (addr < 0177714))
	{
		m_cpu.SetSysRegsInternal(addr, value);

		if (GetAltProMode() == ALTPRO_A16M_HLT11_MODE || (GetAltProMode() == ALTPRO_SMK_HLT10_MODE && GetFDDType() == DEV_FDD_SMK512))
		{
			m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007777)] = value;
		}
	}
	else
	{
		m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007777)] = value;
	}
}


void CMotherBoard::SetWordIndirect(WORD addr, WORD value)
{
	register int nBank = (addr >> 12) & 0x0f;

	if (OnSetSystemRegister(addr, value, false))
	{
		if (GetAltProMode() == ALTPRO_A16M_HLT11_MODE || (GetAltProMode() == ALTPRO_SMK_HLT10_MODE && GetFDDType() == DEV_FDD_SMK512))
		{
			*(WORD *)&m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007776)] = value;
		}

		return;
	}

	if ((0177700 <= addr) && (addr < 0177714))
	{
		m_cpu.SetSysRegsInternal(addr, value);

		if (GetAltProMode() == ALTPRO_A16M_HLT11_MODE || (GetAltProMode() == ALTPRO_SMK_HLT10_MODE && GetFDDType() == DEV_FDD_SMK512))
		{
			*(WORD *)&m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007776)] = value;
		}
	}
	else
	{
		*(WORD *)&m_pMemory[m_MemoryMap[nBank].nOffset + (addr & 007776)] = value;
	}
}


WORD CMotherBoard::GetRON(int reg) const
{
	return m_cpu.GetRON(reg);
}

void CMotherBoard::SetRON(int reg, WORD value)
{
	m_cpu.SetRON(reg, value);
}

WORD CMotherBoard::GetPrevPC()
{
	return m_nPreviousPC;
}

//это ресет оборудования, выполняемый по команде RESET. вызывается из CPU
void CMotherBoard::ResetDevices()
{
	m_kprs.clear();

	m_reg177660 = 0100; //команда RESET запрещает прерывания от клавиатуры. проверено.
	m_reg177714in = 0;
	m_reg177714out = 0;
	m_reg177716out_tap = 0200;
	m_reg177716in |= 0300;

	//если у нас есть сопр. по команде RESET он тоже должен инициализироваться (в правильной схеме подключения)
	if (m_pAY8910)
	{
		m_pAY8910->Reset();
	}
}


bool CMotherBoard::ResetHot(WORD addrStart)
{
	m_kprs.clear();

	if (addrStart == 0)
	{
		addrStart = m_nStartAddr;    // Используем адрес по умолчанию
	}
	else
	{
		m_nStartAddr = addrStart;    // иначе используем адрес из конфигурации
	}

	//инициализация регистров
	m_reg177716in = addrStart & 0177400;
	m_reg177662out = 047400;
	m_reg177664 = 01330;
	m_reg177662in = 0;

	//инициализация карты памяти
	if (!InitMemoryModules())
	{
		return false;
	}

	//инициализация ОЗУ и CPU
	Reset();

	if (m_pSpeaker)
	{
		m_pSpeaker->Reset();
	}

	if (m_pCovox)
	{
		m_pCovox->Reset();
	}

	return true;
}


//////////////////////////////////////////////////////////////////////
//на входе - маска, по которой сбрасываем биты адреса.
void CMotherBoard::ResetCold(WORD addrMask)
{
	m_reg177716in = (m_nStartAddr & ~addrMask) & 0177400;
	m_reg177662out = 047400;
	m_reg177664 = 01330;
	Reset();
	m_reg177716in |= (m_nStartAddr & 0177400); //а в регистр всё-таки занесём стандартное значение, без сброшенных битов
}


void CMotherBoard::StopInterrupt()
{
	//нажали на кнопку стоп
	m_cpu.SetIRQ1();
}

void CMotherBoard::UnStopInterrupt()
{
	//отжали кнопку стоп
	m_cpu.UnsetIRQ1();
}


void CMotherBoard::KeyboardInterrupt(WORD interrupt)
{
	if ((m_reg177660 & 0100) == 0)
	{
		//если прерывания разрешены, делаем прерывание
		m_cpu.InterruptVIRQ(interrupt);
	}
}


void CMotherBoard::AccelerateCPU()
{
	if (m_nCPUFreq < m_nHighBound)
	{
		m_nCPUFreq += SPEED_STEP;
	}
}
void CMotherBoard::AccelerateCPUSmall()
{
	if (m_nCPUFreq < m_nHighBound)
	{
		m_nCPUFreq += SPEED_STEP_SMALL;
	}
}

void CMotherBoard::SlowdownCPU()
{
	if (m_nCPUFreq > m_nLowBound)
	{
		m_nCPUFreq -= SPEED_STEP;
	}
}
void CMotherBoard::SlowdownCPUSmall()
{
	if (m_nCPUFreq > m_nLowBound)
	{
		m_nCPUFreq -= SPEED_STEP_SMALL;
	}
}

void CMotherBoard::NormalizeCPU()
{
	m_nCPUFreq = m_nBaseCPUFreq;
}

bool CMotherBoard::CanAccelerate()
{
	return !(m_nCPUFreq >= m_nHighBound);
}

bool CMotherBoard::CanSlowDown()
{
	return !(m_nCPUFreq <= m_nLowBound);
}

int CMotherBoard::GetLowBound()
{
	return m_nLowBound;
}

int CMotherBoard::GetHighBound()
{
	return m_nHighBound;
}

LONGLONG CMotherBoard::GetCPUTick() const
{
	return m_tickCount;
}

void CMotherBoard::SetCPUBaseFreq(int frq) //установка базовой частоты
{
	m_nCPUFreq = m_nBaseCPUFreq = frq;
}

void CMotherBoard::SetCPUFreq(int frq) //установка текущей частоты
{
	m_nCPUFreq = frq;
}

int CMotherBoard::GetCPUFreq() const //выдача текущей частоты
{
	return m_nCPUFreq;
}

int CMotherBoard::GetCPUSpeed() //выдача текущей частоты для конфигурации
{
	return (m_nCPUFreq == m_nBaseCPUFreq) ? 0 : m_nCPUFreq;
}

void CMotherBoard::EnableAskForBreak(bool bEnable)
{
	m_bAskForBreak = bEnable;
}

bool CMotherBoard::IsAskForBreak()
{
	return m_bAskForBreak;
}

CFDDController *CMotherBoard::GetFDD()
{
	return &m_fdd;
}

void CMotherBoard::SetFDDType(BKFDD_MODEL model, bool bInit /*= true*/)
{
	m_fdd.SetFDDType(model);    //не забывать! bool bInit удалять нельзя, оно в других местах нужно.
}

BKFDD_MODEL CMotherBoard::GetFDDType()
{
	return m_fdd.GetFDDType();
}

WORD CMotherBoard::GetAltProMode()
{
	return LOWORD(m_ConfBKModel.nAltProMode);
}

void CMotherBoard::SetAltProMode(WORD w)
{
	m_ConfBKModel.nAltProMode = w;
	MemoryManager();
}

WORD CMotherBoard::GetAltProCode()
{
	return LOWORD(m_ConfBKModel.nAltProMemBank);
}

void CMotherBoard::SetAltProCode(WORD w)
{
	m_ConfBKModel.nAltProMemBank = w;
	MemoryManager();
}

WORD CMotherBoard::GetAltProExtCode()
{
	return LOWORD(m_ConfBKModel.nExtCodes);
}

void CMotherBoard::Set177716RegMem(WORD w)
{
	m_reg177716out_mem = w;
	MemoryManager();
}

BYTE *CMotherBoard::GetMainMemory()
{
	return m_pMemory;
}

BYTE *CMotherBoard::GetAddMemory()
{
	return nullptr;
}

/*
Чтение из системных регистров
вход: num - адрес регистра (177660, 177716 и т.п.)
pDst - адрес в массиве, куда сохраняется новое значение
bByteOperation - флаг операции true - байтовая, false - словная
*/
bool CMotherBoard::OnGetSystemRegister(WORD addr, void *pDst, bool bByteOperation)
{
	auto pDst_W = reinterpret_cast<WORD *>(pDst);
	auto pDst_B = reinterpret_cast<BYTE *>(pDst);
	register bool bRet = true;
	register WORD v = 0;

	switch (addr & 0177776)
	{
		case 0177660:
			v = m_reg177660;
			break;

		case 0177662:
			//если читаем из регистра данных клавиатуры,
			//сбросим бит готовности из регистра состояния клавиатуры
			m_reg177660 &= ~0200;
			v = m_reg177662in;
			break;

		case 0177664:
			v = m_reg177664;
			break;

		case 0177714:
			v = m_reg177714in;
			break;

		case 0177716:
			v = m_reg177716in;
			m_reg177716in &= ~4;
			break;

		default:
			bRet = false;
	}

	if (bByteOperation)
	{
		*pDst_B = (addr & 1) ? HIBYTE(v) : LOBYTE(v);
	}
	else
	{
		*pDst_W = v;
	}

	return bRet;
}

/*
Запись данных в системные регистры БК0010
    вход: num - адрес регистра(177660, 177716 и т.п.)
          src - записываемое значение.
          bByteOperation - флаг операции true - байтовая, false - словная
*/
bool CMotherBoard::OnSetSystemRegister(WORD addr, WORD src, bool bByteOperation)
{
	register WORD mask = 0177777; //это какие биты в регистре используются

	switch (addr & 0177776)
	{
		case 0177660:
			/*
			177660
			Регистр состояния клавиатуры.
			(0100)бит 6 -- маскирование прерывание от клавиатуры,
			    "0" -- разрешено прерывание по вектору 060 либо 0274.
			    Прерывание вызывается при появлении "1" в бите 7. Начальное состояние: "0".
			    Доступен по чтению и по записи
			(0200)бит 7 -- готовность: "1" -- в регистре данных клавиатуры(177662) готов код нажатой клавиши.
			    Устанавливается при нажатии на клавишу, сбрасывается при чтении регистра данных клавиатуры.
			    Начальное состояние: "1".
			    Доступен только по чтению.
			другие биты: "0". Доступны только по чтению.
			*/
			mask = 0100;

			if (bByteOperation)
			{
				src &= 0377;

				if (addr & 1)
				{
					src = (src << 8) | (m_reg177660 & 0377); // работаем со старшим байтом, младший оставляем неизменным
				}
				else
				{
					src = src | (m_reg177660 & 0177400); // работаем с младшим байтом, старший оставляем неизменным
				}
			}

			//сбрасываем используемые биты и устанавливаем их значения из слова, которое записываем.
			//остальные биты - которые не используются - остаются нетронутыми.
			m_reg177660 = (~mask & m_reg177660) | (mask & src);
			return true;

		case 0177662:
			/*
			177662
			Регистр данных клавиатуры.
			    биты 0-6: код клавиши. Доступ только по чтению.
			(040000)бит 14: разрешение прерывания по(внешнему) таймеру(50 Гц),
			    "0" -- прерывание разрешено,
			    "1" -- таймер отключен.
			    Доступен только по записи.
			    Бит 14 на БК0010 не документирован, и таймер работал только на некоторых экземплярах, можно считать, что его нет
			*/
			return false; //на бк10 запись запрещена. это был один из способов определения бк10 или бк11

		case 0177664:
			/*
			177664
			Регистр скроллинга. Доступен по записи и чтению.
			    биты 0-7: смещение скроллинга, строк. Начальное значение -- 0330.
			(01000)бит 9: сокращённый режим экрана, "0" -- сокращённый(1/4 экрана, старшие адреса),
			    "1" -- полный экран 256 строк.
			*/
			mask = 01377;

			if (bByteOperation)
			{
				src &= 0377;
				/*
				оказалось, что в этот регистр не работает байтовая запись.
				чтение байта работает, но записывается всегда слово.
				а при байтовой операции, старший байт просто теряется.
				*/
			}

			m_reg177664 = (~mask & m_reg177664) | (mask & src);
			return true;

		case 0177714:

			/*177714
			Регистр параллельного программируемого порта ввода вывода - два регистра, входной по чтению и выходной по записи.
			из выходного нельзя ничего прочитать т.к., читается оно из входного,
			во входной невозможно ничего записать, т.к. записывается оно в выходной.
			*/
			if (bByteOperation)
			{
				src &= 0377; // работаем с младшим байтом

				if (addr & 1)
				{
					src <<= 8; // работаем со старшим байтом
				}
			}

			m_reg177714out = src;
			m_pCovox->SetSample(m_reg177714out);

			if (g_Config.m_bICLBlock) //если включен блок нагрузок
			{
				//данные записанные в выходной порт передаются во входной
				m_reg177714in = m_reg177714out;
			}

			return true;

		case 0177716:

			/*
			177716
			Системный регистр. Внешний регистр 1(ВР1, SEL1) процессора ВМ1, регистр начального пуска.
			как и 177714 состоит из двух регистров, раздельных по чтению и по записи
			По чтению:
			(004)бит 2: признак записи в системный регистр. Устанавливается в "1" при
			    любой записи в регистр, сбрасывается в "0" после чтения из регистра.
			(020)бит 4: данные с ТЛГ-линии, "0" - логический 0, "1" - логическая 1
			(040)бит 5: данные с магнитофона, "0" - логический 0, "1" - логическая 1
			(100)бит 6: индикатор нажатия клавиши, установлен в "0" если нажата клавиша клавиатуры, "1" если нет нажатых клавиш.
			(200)бит 7: сигнал готовности с ТЛГ-линии, "0" - логический 0, "1" - логическая 1
			        Однако по заводской документации на БК0010 этот бит - константа "1", указывающая на отсутствие
			        в системе команд расширенной арифметики
			    биты 8-15: адрес начального пуска, 100000(БК-0010), младший байт при этом игнорируется
			    биты 0,1,3, не используются, "0".
			По записи:
			(020)бит 4: данные для передачи на ТЛГ-линию, "0" - логический 0, "1" - логическая 1
			(040)бит 5: данные на магнитофон(либо сигнал готовности на ТЛГ-линию - этого в тех документации нету.). Начальное состояние "0".
			(100)бит 6: данные на магнитофон и на пьезодинамик. Начальное состояние "0".
			(200)бит 7: включение двигателя магнитофона, "1" -- стоп, "0" -- пуск. Начальное состояние "1".
			    биты 0,1,3, не используются, "0".
			*/
			if (bByteOperation)
			{
				src &= 0377; // работаем с младшим байтом

				if (addr & 1)
				{
					src <<= 8; // работаем со старшим байтом
				}
			}

			m_reg177716out_tap = src;
			m_pSpeaker->SetSample(m_reg177716out_tap);
			//В БК 2й разряд SEL1 фиксирует любую запись в этот регистр, взводя триггер D9.1 на неограниченное время, сбрасывается который любым чтением этого регистра.
			m_reg177716in |= 4;
			return true;
	}

	return false;
}

void CMotherBoard::RunToAddr(WORD addr)
{
	// Run all commands to address
	UnbreakCPU(addr);
}


void CMotherBoard::RunOver()
{
	// Run one command with go over command
	register WORD pc = GetRON(CCPU::R_PC);
	register WORD instr = GetWordIndirect(pc);
	register WORD NextAddr = 0;

	if (m_pDebugger->IsInstructionOver(instr))
	{
		RunToAddr(pc + m_pDebugger->CalcInstructionLength(instr));
	}
	else if (m_pDebugger->CalcNextAddr(pc, NextAddr))
	{
		RunToAddr(NextAddr);
	}
	else
	{
		RunInto();
	}
}

void CMotherBoard::RunOut()
{
	// Run all commands to function end
	CDebugger::InitOutMode();
	UnbreakCPU(GO_OUT);
}


void CMotherBoard::RunInto()
{
	// Run one command with go into command
	UnbreakCPU(GO_INTO);
}



void CMotherBoard::BreakCPU()
{
	m_lockBreaked.Lock(); //включаем отладочную приостановку
	CDebugger::InitOutMode();

	if (m_pParent)
	{
		m_pParent->PostMessage(WM_CPU_DEBUGBREAK);
	}
}

void CMotherBoard::UnbreakCPU(int nGoto)
{
	m_nGotoAddress = nGoto;
	m_lockBreaked.UnLock(); //отменяем отладочную приостановку
}


bool CMotherBoard::IsCPUBreaked()
{
	return m_lockBreaked.IsLocked() != 0;
}

void CMotherBoard::RunCPU(bool bUnbreak)
{
	if (bUnbreak)
	{
		UnbreakCPU(ADDRESS_NONE);
	}

	// Set running flag
	m_lockRunning.Lock();
}


void CMotherBoard::StopCPU(bool bUnbreak)
{
	m_nGotoAddress = ADDRESS_NONE;
	m_lockRunning.UnLock();

	if (bUnbreak)
	{
		m_lockBreaked.UnLock(); //отменяем отладочную приостановку, для случая, когда мы останавливаем
		//проц чтобы изменить конфигурацию, загрузить состояние или выйти из программы
	}

	MSG msg;

	//тут надо дождаться, пока фрейм не завершит работу.
	//и чтобы UI не зависло - берём на себя трансляцию сообщений
	while (IsFrameExecuted())
	{
		if (GetMessage(&msg, m_pParent->GetSafeHwnd(), 0, 0))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}

		Sleep(10);
	}
}

bool CMotherBoard::IsCPURun()
{
	return m_lockRunning.IsLocked() != 0;
}

bool CMotherBoard::IsFrameExecuted()
{
	return m_lockExecuteFrame.IsLocked() != 0;
}

void CMotherBoard::NextTick()
{
	ASSERT(m_pSpeaker != nullptr);

	//обработка медиатактов
	if (m_nMediaTickCount == 0)
	{
		// Обнуляем звуковые буферы
		m_pSpeaker->Reset();
		// Получаем сохранённые 1/CPU_FRAMERATE секунды звуковые данные
		ZeroMemory(m_pSoundBuffer, m_nSoundBufferLen);
		// Берём данные с ленты -> m_pSoundBuffer
		m_pParent->GetTapePtr()->PlayWaveGetBuffer(m_pSoundBuffer, m_nSoundBufferLen);
		// Посылаем данные с ленты в 177716 <- m_pSoundBuffer
		m_pSpeaker->ReceiveSamples(m_pSoundBuffer, m_nSoundBufferLen);
		m_nBufPos = 0;
	}

	auto pSTBuffer = reinterpret_cast<SAMPLE_TYPE *>(m_pSoundBuffer);
	register double fL = 0.0, fR = 0.0;
	m_pSound->SoundGen_ResetSample(pSTBuffer[m_nBufPos], pSTBuffer[m_nBufPos + 1]); //задаём начальные уровни для микширования

	// Берём звуки со спикера BK -> m_pSoundBuffer
	if (m_pSpeaker->IsSoundEnabled())
	{
		if (m_pSpeaker->GetSample(fL, fR))
		{
			m_reg177716in |= 040;
		}
		else
		{
			m_reg177716in &= ~040;
		}

		m_pSound->SoundGen_MixSample(fL, fR); //микшируем сэмплы внутри m_pSound
	}

	// Берём данные с ковокса -> m_pSoundBuffer
	if (m_pCovox->IsSoundEnabled())
	{
		m_pCovox->GetSample(fL, fR);
		m_pSound->SoundGen_MixSample(fL, fR);
	}

	// Берём данные с ay8910 -> m_pSoundBuffer
	if (m_pAY8910->IsSoundEnabled())
	{
		m_pAY8910->GetSample(fL, fR);
		m_pSound->SoundGen_MixSample(fL, fR);
	}

	register SAMPLE_TYPE L, R;
	m_pSound->SoundGen_FeedDAC_Mixer(L, R); //получаем сюда смикшированное, а там - осуществляем звучание
	pSTBuffer[m_nBufPos++] = L; //это нужно в основном для осциллографа
	pSTBuffer[m_nBufPos++] = R;

	if (++m_nMediaTickCount >= m_nMediaTicksPerFrame)
	{
		m_nMediaTickCount = 0;
		// Отправляем их на ленту <- m_pSoundBuffer
		m_pParent->GetTapePtr()->RecordWaveGetBuffer(m_pSoundBuffer, m_nSoundBufferLen);
		// Отображаем буфер на осциллографе
		m_pParent->GetOscillatorViewPtr()->GetobjPtr()->RedrawWindow();
	}

	CDevice::NextTick();
}

void CMotherBoard::ExecuteFrame()
{
	m_lockExecuteFrame.Lock();

	if (!IsCPUBreaked())
	{
		m_fMemory_Mod = double(m_nCPUFreq) / 750000.0;      //коэффициент пересчёта ТЧ проца в количество обращений к видеопамяти за фрейм
		m_fTickCount += double(m_nCPUFreq) / CPU_FRAMERATE; //количество тактов процессора в текущем фрейме. При нестандартной частоте
		//может быть совсем не целое число.
		m_fFDD_Mod = m_fTickCount / (15625.0 / CPU_FRAMERATE);  //15625 = 5 оборотов в сек, в одном обороте 3125 слов,
		//норма 15625.0, больше - быстрее вращение, меньше - медленнее
		m_fMedia_Mod = m_fTickCount / double(m_nMediaTicksPerFrame); //коэффициент пересчёта тактов процессора в медиа такты
		//означает - через сколько тактов процессора надо вставлять 1 медиа такт, т.к. число не целое, то
		//получается приблизительно.
	}

	double fCpuTickTime = 1.0 / m_nCPUFreq; //длительность одного такта в секундах

	while (--m_fTickCount > 0.0)  //пока такты укладываются в фрейм
	{
		// Если время пришло, выполняем текущую инструкцию
		if (--m_nCPUTicks <= 0)
		{
			// Если останов на адресе где стоит отладочный курсор
			if (m_nGotoAddress == m_nPreviousPC)
			{
				BreakCPU();
			}

			//тут для отладочных целей приостанавливаем фрейм.
			//это хорошо для отладки, и крайне хреново с точки зрения вообще алгоритма
			//но другого способа я просто не вижу.
			while (IsCPUBreaked()) //пока процессор приостановлен - ждём
			{
				if (!IsCPURun())    //если решили всё остановить,
				{
					++m_nCPUTicks;  //инкрементируем счётчики обратно
					++m_fTickCount;
					m_lockExecuteFrame.UnLock();
					return;         //и выходим из функции
				}

				DrawDebugScreen();  //но продолжаем обновлять экран
				Sleep(19);          //со стандартной частотой примерно 50Гц
			}

			try
			{
				m_nCPUTicks = m_cpu.TranslateInstruction(); // Выполняем одну инструкцию, возвращаем время выполнения одной инструкции в тактах.
				Interception(); //перехват разных подпрограмм монитора, для их эмуляции
			}
			catch (CExceptionHalt &halt)
			{
				// BK Violation exception. Например запись в ПЗУ или чтение из несуществующей памяти
				if (m_bAskForBreak)
				{
					BreakCPU(); // Останавливаем CPU для отладки
					register WORD pc = GetPrevPC();
					CString strMessage;
					strMessage.Format(IDS_ERRMSG_ROM, halt.m_addr, pc, halt.m_info);
					CBKMessageBox message;
					register int answer = message.Show(strMessage, MB_ABORTRETRYIGNORE | MB_DEFBUTTON2 | MB_ICONSTOP);

					switch (answer)
					{
						case IDIGNORE:  //если выбрали "игнорировать"
							m_bAskForBreak = false; //то снимаем галку, и больше не вызываем диалог

						//и выполним всё, что должно выполняться для "повтор"
						case IDRETRY: //если выбрали "повтор" - то просто продолжить выполнение и посмотреть, что будет дальше
							UnbreakCPU(ADDRESS_NONE); //обратно запускаем CPU
							m_cpu.ReplyError(); // Делаем прер. по вектору 4(halt) в следующем цикле
							break;

						case IDABORT: //если выбрали "прервать" - остаёмся в отладочном останове
						default:
							break;
					}
				}
				else
				{
					m_cpu.ReplyError(); // Делаем прер. по вектору 4(halt) в следующем цикле
				}
			}
			catch (...)
			{
				// Любое другое исключение. Неизвестное исключение или ошибка доступа к памяти WINDOWS.
				// Эта ошибка может быть неизвестной ошибкой BK или ошибкой эмулятора
				register WORD pc = GetPrevPC();
				CString strMessage;
				strMessage.LoadString(IDS_ERRMSG_INTERNAL);
				CBKMessageBox(strMessage, MB_OK | MB_ICONSTOP);
				BreakCPU();
				StopCPU(false);
				break; // Прекращаем выполнять инструкции
			}

			// Сохраняем текущее значение PC для отладки
			m_nPreviousPC = m_cpu.GetRON(CCPU::R_PC);

			if (
			    m_pDebugger->GetDebugPCBreak(m_nPreviousPC) || // Спросим отладчик на счёт условий остановки. Если ВОИСТИНУ останов
			    (m_nGotoAddress == GO_INTO) || // Отладочный останов если только одиночный шаг
			    (m_nGotoAddress == GO_OUT && CDebugger::IsInstructionOut(m_cpu.GetCurrentInstruction())) // Отладочный останов если команда выхода из п/п
			)
			{
				BreakCPU();
			}
		}

		m_pSpeaker->RCSHIM(fCpuTickTime); //эмуляция конденсатора на выходе линейного входа.

		if (--m_fMediaTicks <= 0.0)
		{
			m_fMediaTicks += m_fMedia_Mod;
			NextTick(); //тут делается звучание всех устройств и обработка прочих устройств
		}

		if (--m_fMemoryTicks <= 0.0)
		{
			m_fMemoryTicks += m_fMemory_Mod;
			Make_One_Screen_Cycle(); //тут выполняются циклы экрана
		}

		if (--m_fFDDTicks <= 0.0)
		{
			m_fFDDTicks += m_fFDD_Mod;
			m_fdd.Periodic();    // Вращаем диск на одно слово на дорожке
		}
	} //while

	m_lockExecuteFrame.UnLock();
}


//////////////////////////////////////////////////////////////////////
//перехват разных подпрограмм монитора, для их эмуляции
bool CMotherBoard::Interception()
{
	switch (GetRON(CCPU::R_PC) & 0177776)
	{
		case 0116076:

			//если по этому адресу действительно ПЗУ монитора БК10
			if ((GetWordIndirect(0116076) == 04467) && (GetWordIndirect(0116100) == 0172244))
			{
				EmulateTapeOperation();
			}

			return true; //если выполнилось
	}

	return false; //если ничего не выполнилось
}


int CMotherBoard::GetScreenPage()
{
	return 1;    //для БК10 всегда 1
}

bool CMotherBoard::LoadRomModule(int iniRomNameIndex, int bank)
{
	CString strPath = g_Config.m_strROMPath +
	                  g_Config.GetIniObj()->GetValueString(IDS_INI_SECTIONNAME_ROMMODULES, iniRomNameIndex, _T(""));
	CFile file;

	if (file.Open(strPath, CFile::modeRead))
	{
		register auto len = (UINT)file.GetLength();

		if (len > 020000) //размер ПЗУ не должен быть больше 8кб
		{
			len = 020000;
		}

		UINT readed = file.Read(&m_pMemory[bank << 12], len);
		file.Close();

		if (readed == len)
		{
			return true;
		}
	}
	else
	{
		CString strError;
		strError.LoadString(IDS_ERROR_CANTOPENFILE);
		CBKMessageBox(strError + _T('\'') + strPath + _T('\''), MB_OK | MB_ICONSTOP);
	}

	return false;
}


bool CMotherBoard::InitMemoryModules()
{
	register bool bRes = true;
	bRes = bRes && LoadRomModule(IDS_INI_BK10_RE2_017_MONITOR, BRD_10_MON10_BNK);
	bRes = bRes && LoadRomModule(IDS_INI_BK10_RE2_106_BASIC1, BRD_10_BASIC10_1_BNK);
	bRes = bRes && LoadRomModule(IDS_INI_BK10_RE2_107_BASIC2, BRD_10_BASIC10_2_BNK);
	bRes = bRes && LoadRomModule(IDS_INI_BK10_RE2_108_BASIC3, BRD_10_REGISTERS_BNK);

	//и проинициализируем карту памяти
	if (bRes)
	{
		MemoryManager();
	}

	return bRes;
}

void CMotherBoard::MemoryManager()
{
	for (int i = 0; i <= 017; ++i)
	{
		m_MemoryMap[i].bReadable = true;
		m_MemoryMap[i].bWritable = true;
		m_MemoryMap[i].nBank = i;
		m_MemoryMap[i].nPage = i >> 2;
		m_MemoryMap[i].nOffset = i << 12;
	}

	for (int i = 010; i <= 017; ++i)
	{
		m_MemoryMap[i].bWritable = false;
	}

	//m_MemoryMap[017].bReadable = false; //170000-177600 - всё ещё бейсик, а для МСТД там ПЗУ тестов
}


bool CMotherBoard::RestoreState(CMSFManager &msf, HBITMAP hScreenshot)
{
	if (RestorePreview(msf, hScreenshot))
	{
		if (RestoreConfig(msf))
		{
			if (RestoreRegisters(msf))
			{
				if (RestoreMemoryMap(msf)) //необходимо делать строго перед работой с памятью.
				{
					if (RestoreMemory(msf))
					{
						return true;
					}
				}
			}
		}
	}

	return false;
}

bool CMotherBoard::RestoreConfig(CMSFManager &msf)
{
	if (msf.IsLoad())
	{
		msf.GetBlockConfig();
		MSF_FRAMEDATA framedata;

		if (msf.GetBlockFrameData(&framedata))
		{
			m_nCPUTicks = framedata.nCPUTicks;
			m_fTickCount = framedata.fTickCount;
			m_fMediaTicks = framedata.fMediaTicks;
			m_fMedia_Mod = framedata.fMedia_Mod;
			m_fMemoryTicks = framedata.fMemoryTicks;
			m_fMemory_Mod = framedata.fMemory_Mod;
			m_fFDDTicks = framedata.fFDDTicks;
			m_fFDD_Mod = framedata.fFDD_Mod;
			m_cpu.SetTimerSpeedInternal(framedata.nTimerSpeed);
			m_nVideoAddress = framedata.nVideoAddress & 0xffff; //видео адрес, младшие 6 бит - счётчик строк внутри строки
			m_bHgate = (framedata.bHgate != 0); //флаг отсчёта служебных видеоциклов в строке
			m_bVgate = (framedata.bVgate != 0); //флаг отсчёта служебных строк
			m_nVGateCounter = framedata.nVGateCounter; //дополнительный счётчик служебных строк
			m_nLineCounter = framedata.nLineCounter & 0377; //счётчик видео строк
		}
		else
		{
			m_nCPUTicks = 0;
			m_fTickCount = 0.0;
			m_fMediaTicks = 0.0;
			m_fMemoryTicks = 0.0;
			m_fFDDTicks = 0.0;
			m_nVideoAddress = 0; //видео адрес, младшие 6 бит - счётчик строк внутри строки
			m_bHgate = false;
			m_bVgate = true;
			m_nVGateCounter = 64;
			m_nLineCounter = 0; //счётчик видео строк
		}

		return true; //если конфиг не прочёлся, значит этого блока просто нет. ничего страшного
	}
	else
	{
		bool bRet = msf.SetBlockConfig();
		MSF_FRAMEDATA framedata;
		framedata.nCPUTicks = m_nCPUTicks;
		framedata.fTickCount = m_fTickCount;
		framedata.fMediaTicks = m_fMediaTicks;
		framedata.fMedia_Mod = m_fMedia_Mod;
		framedata.fMemoryTicks = m_fMemoryTicks;
		framedata.fMemory_Mod = m_fMemory_Mod;
		framedata.fFDDTicks = m_fFDDTicks;
		framedata.fFDD_Mod = m_fFDD_Mod;
		framedata.nTimerSpeed = m_cpu.GetTimerSpeedInternal();
		framedata.nVideoAddress = m_nVideoAddress; //видео адрес, младшие 6 бит - счётчик строк внутри строки
		framedata.bHgate = m_bHgate ? 1 : 0; //флаг отсчёта служебных видеоциклов в строке
		framedata.bVgate = m_bVgate ? 1 : 0; //флаг отсчёта служебных строк
		framedata.nVGateCounter = m_nVGateCounter; //дополнительный счётчик служебных строк
		framedata.nLineCounter = m_nLineCounter; //счётчик видео строк
		return  bRet && msf.SetBlockFrameData(&framedata);
	}
}


bool CMotherBoard::RestoreRegisters(CMSFManager &msf)
{
	MSF_CPU_REGISTERS cpu_reg;
	MSF_PORT_REGS port_regs;

	if (msf.IsLoad())
	{
		if (!msf.GetBlockCPURegisters(&cpu_reg))
		{
			return false;
		}

		SetRON(CCPU::R_R0, cpu_reg.r0);
		SetRON(CCPU::R_R1, cpu_reg.r1);
		SetRON(CCPU::R_R2, cpu_reg.r2);
		SetRON(CCPU::R_R3, cpu_reg.r3);
		SetRON(CCPU::R_R4, cpu_reg.r4);
		SetRON(CCPU::R_R5, cpu_reg.r5);
		SetRON(CCPU::R_SP, cpu_reg.sp);
		SetRON(CCPU::R_PC, cpu_reg.pc);
		SetPSW(cpu_reg.psw);

		if (msf.GetVersion() >= MSF_VERSION_MINIMAL)
		{
			if (!msf.GetBlockPortRegs(&port_regs))
			{
				return false;
			}

			m_reg177660 = port_regs.p0177660;
			m_reg177662in = port_regs.p0177662_in;
			m_reg177662out = port_regs.p0177662_out;
			m_reg177664 = port_regs.p0177664;
			m_reg177714in = port_regs.p0177714_in;
			m_reg177714out = port_regs.p0177714_out;
			m_reg177716in = port_regs.p0177716_in;
			m_reg177716out_tap = port_regs.p0177716_out_tap;
			m_reg177716out_mem = port_regs.p0177716_out_mem;
			m_cpu.SetSysRegsInternal(0177700, port_regs.p0177700);
			m_cpu.SetSysRegsInternal(0177702, port_regs.p0177702);
			m_cpu.SetSysRegsInternal(0177704, port_regs.p0177704);
			m_cpu.SetSysRegsInternal(0177706, port_regs.p0177706);
			m_cpu.SetSysRegsInternal(0177710, port_regs.p0177710);
			m_cpu.SetSysRegsInternal(0177712, port_regs.p0177712);
		}
		else
		{
			return false;
		}
	}
	else
	{
		cpu_reg.r0 = GetRON(CCPU::R_R0);
		cpu_reg.r1 = GetRON(CCPU::R_R1);
		cpu_reg.r2 = GetRON(CCPU::R_R2);
		cpu_reg.r3 = GetRON(CCPU::R_R3);
		cpu_reg.r4 = GetRON(CCPU::R_R4);
		cpu_reg.r5 = GetRON(CCPU::R_R5);
		cpu_reg.sp = GetRON(CCPU::R_SP);
		cpu_reg.pc = GetRON(CCPU::R_PC);
		cpu_reg.psw = GetPSW();

		if (!msf.SetBlockCPURegisters(&cpu_reg))
		{
			return false;
		}

		port_regs.p0177660 = m_reg177660;
		port_regs.p0177662_in = m_reg177662in;
		port_regs.p0177662_out = m_reg177662out;
		port_regs.p0177664 = m_reg177664;
		port_regs.p0177714_in = m_reg177714in;
		port_regs.p0177714_out = m_reg177714out;
		port_regs.p0177716_in = m_reg177716in;
		port_regs.p0177716_out_tap = m_reg177716out_tap;
		port_regs.p0177716_out_mem = m_reg177716out_mem;
		port_regs.p0177700 = m_cpu.GetSysRegs(0177700);
		port_regs.p0177702 = m_cpu.GetSysRegs(0177702);
		port_regs.p0177704 = m_cpu.GetSysRegs(0177704);
		port_regs.p0177706 = m_cpu.GetSysRegs(0177706);
		port_regs.p0177710 = m_cpu.GetSysRegs(0177710);
		port_regs.p0177712 = m_cpu.GetSysRegs(0177712);

		if (!msf.SetBlockPortRegs(&port_regs))
		{
			return false;
		}
	}

	return true;
}


bool CMotherBoard::RestoreMemory(CMSFManager &msf)
{
	if (msf.IsLoad())
	{
		return msf.GetBlockBaseMemory(m_pMemory);
	}
	else
	{
		return msf.SetBlockBaseMemory(m_pMemory);
	}
}

bool CMotherBoard::RestoreMemoryMap(CMSFManager &msf)
{
	if (msf.IsLoad())
	{
		return msf.GetBlockMemMap(m_MemoryMap, &m_ConfBKModel);
	}
	else
	{
		return msf.SetBlockMemMap(m_MemoryMap, &m_ConfBKModel);
	}
}


bool CMotherBoard::RestorePreview(CMSFManager &msf, HBITMAP hScreenshot)
{
	bool bRet = true;

	if (msf.IsSave())
	{
		ASSERT(hScreenshot);

		if (hScreenshot)
		{
			auto hBm = (HBITMAP)CopyImage(hScreenshot, IMAGE_BITMAP, 200, 200, LR_CREATEDIBSECTION);

			if (hBm)
			{
				bRet = msf.SetBlockPreview(hBm);
				DeleteObject(hBm);
			}
		}
	}

	return bRet;
}


//это вариант точного алгоритма из VP1-037
void CMotherBoard::Make_One_Screen_Cycle()
{
	register WORD dww = m_nVideoAddress & 076; // счётчик слов внутри строки
	register WORD dwa = m_nVideoAddress & 037700; //адрес строки

	if (!(m_bVgate || m_bHgate))
	{
		m_pParent->GetScreen()->SetExtendedMode((m_reg177664 & 01000) == 0);
		register DWORD_PTR nScrAddr = ((DWORD_PTR)GetScreenPage()) << 14;
		register BYTE *nScr = m_pMemory + nScrAddr + m_nVideoAddress;
		m_pParent->GetScreen()->PrepareScreenLineWordRGB32(m_nLineCounter, dww, *(WORD *)nScr);
	}

	dww += 2; //переходим к следующему слову

	if (m_bHgate) //считаем служебное поле в строке
	{
		if (dww == 040) //отсчитали 16. служебных слов
		{
			dww = 0;
			m_bHgate = false;
		}
	}
	else if (dww == 0100)
	{
		//если отсчитали 32. слова, нужно теперь считать служебные
		dww = 0;
		m_bHgate = true;
		register WORD dwl = (m_nLineCounter + 1) & 0377; // Счётчик строк

		if (m_bVgate) //считаем служебные
		{
			if (--m_nVGateCounter <= 0) //отсчитали 64 служебные строки
			{
				dwl = 0;  //снова начнём считать информационные
				m_bVgate = false;
			}

			//если считаем служебные строки, выбираем из них 353..350
			if (050 <= m_nVGateCounter && m_nVGateCounter <= 053)   //if ((nLineCounter & 074) == 050)
			{
				if (050 == m_nVGateCounter || m_nVGateCounter == 051)   //if (!(nLineCounter & (1 << 1))) //если строки 351, 350
				{
					//применяем новое смещение
					dwa = LOBYTE(m_reg177664) << 6; //VA[13:6] <= RA[7:0];
				}

				//при этом на новую строку не переходим. в одной крутимся.
			}
			else
			{
				//переход на следующую строку
				dwa += 0100; //VA[13:6] <= VA[13:6] + 8'b00000001;
			}
		}
		else
		{
			if (dwl == 0)
			{
				//если отсчитали 256 информационных строк, нужно теперь считать служебные.
				m_bVgate = true;
				m_nVGateCounter = 64;

				if ((m_reg177662out & 040000) == 0) //если бит 14 установлен, таймер не работает.
				{
					m_cpu.TickIRQ2();
				}

				register auto view = m_pParent->GetActiveView();

				if (view)
				{
					view->Invalidate(FALSE);
				}
			}

			//переход на следующую строку
			dwa += 0100; //VA[13:6] <= VA[13:6] + 8'b00000001;
		}

		m_nLineCounter = dwl;
	}

	m_nVideoAddress = (dwa | dww) & 037776;
}


//специально для отладки, рисуем экран старым методом.
//при этом новый метод не отключается и поэтому возможны разного рода казусы.
void CMotherBoard::DrawDebugScreen()
{
	if (m_pParent)
	{
		//чтобы видеть как происходит движение луча в процессе прорисовки экрана
		//эти три строки нужно закомментировать.
		//а иногда наоборот, очень неудобно когда экран по частям перерисовывается.
		//тогда эти три строки надо раскомментировать
		//!!!TODO: может сделать чекбокс, с выбором способа отладочной перерисовки
//      m_pParent->GetScreen()->SetRegister(m_reg177664);
//      register DWORD_PTR nScrAddr = ((DWORD_PTR)GetScreenPage()) << 14;
//      m_pParent->GetScreen()->PrepareScreenRGB32(m_pMemory + nScrAddr);
		register auto view = m_pParent->GetActiveView();

		if (view)
		{
			view->Invalidate(FALSE);
		}
	}
}


void CMotherBoard::StopTimerThread()
{
	if (!m_lockKillTimerEvent.IsLocked())
	{
		m_lockKillTimerEvent.Lock();
		MSG msg;

		while (m_lockKillTimerEvent.IsLocked()) //ждём завершения потока
		{
			//берём на себя трансляцию сообщений, чтобы UI не зависало
			//это возможно уже лишнее. Но на всякий случай, пусть останется.
			//вдруг придётся останавливать поток при работающем фрейме.
			if (GetMessage(&msg, m_pParent->GetSafeHwnd(), 0, 0))
			{
				TranslateMessage(&msg);
				DispatchMessage(&msg);
			}

			Sleep(10);
		}

		m_lockKillTimerEvent.UnLock(); //выставляем событие, что таймер остановился, и можно продолжать остальные действия по остановке
	}
}

/*
Тут реализуется таймер CPU_FRAMERATE Гц. С помощью высокоточного waitable timerа
*/
bool CMotherBoard::StartTimerThread()
{
	if ((m_pTimerThread = AfxBeginThread(
	                          TimerThreadWrapper,
	                          (void *)this,
	                          THREAD_PRIORITY_NORMAL,
	                          0,
	                          CREATE_SUSPENDED)
	    ) == nullptr)
	{
		CBKMessageBox(g_pstrErrors[BK_ERROR_NOTENMEMR], MB_OK);
		return false;
	}

	m_pTimerThread->m_bAutoDelete = true;
	m_pTimerThread->ResumeThread();
	return true;
}

UINT AFX_CDECL CMotherBoard::TimerThreadWrapper(LPVOID lpParam)
{
	auto pThis = reinterpret_cast<CMotherBoard *>(lpParam);
	return pThis->TimerThreadFunc();
}


#define ONE_SECOND_MS 990
//старый проверенный способ.
UINT CMotherBoard::TimerThreadFunc()
{

//тут выяснилось, что синхронизация идёт по звуку.
//и этот таймер нужен лишь тогда, когда нет звуковой подсистемы.
//главное условие, чтобы он выставлял задержку хоть примерно равную фрейму.
//а если задержка будет равна длине фрейма - таймер станет вносить
//незапланированные задержки. Почему так - наверное мы просто опаздываем с
//переходом к следующему фрейму из-за длительности операции
//вот поэтому, у меня тут секунда == ONE_SECOND_MS милисекунд.
	LARGE_INTEGER liDueTime;    //интервал таймера
	// создаём waitable timer.
	HANDLE hWaitableTimer = CreateWaitableTimer(nullptr, FALSE, nullptr); //самосбрасывающийся

	if (hWaitableTimer == nullptr)
	{
		return -1;
	}

	liDueTime.QuadPart = -10000LL * ONE_SECOND_MS / CPU_FRAMERATE;
	SetWaitableTimer(hWaitableTimer, &liDueTime, (ONE_SECOND_MS / CPU_FRAMERATE), nullptr, nullptr, FALSE); //периодический

	do
	{
		//SetWaitableTimer(hWaitableTimer, &liDueTime, 0, nullptr, nullptr, FALSE); //одноразовый, каждый раз переустанавливать надо

		//если процессор остановлен для каких-то целей, значит ничего выполнять не надо
		//фрейм всегда выполняется полностью. Остановка делается только на границах фреймов.
		if (IsCPURun())
		{
			if (m_kprs.nScanCode == -1) //сканкод пуст, значит все нажатые клавиши отпущены
			{
				// Установим в регистре 177716 флаг отпускания клавиши
				m_reg177716in |= 0100;
			}
			else //если не пуст, то что-то остаётся нажатым ещё
			{
				// Установим в регистре 177716 флаг нажатия клавиши
				m_reg177716in &= ~0100;
			}
			// Выполняем набор инструкций
			ExecuteFrame();
			// Парсим скрипт, если он есть
			m_pParent->GetScriptRunnerPtr()->RunScript();

			if (m_kprs.bProcessed) //если последнюю из нажатых клавишу отпустили
			{
				TRACE1("pop char %d\n", m_kprs.nScanCode);
				m_kprs.clear(); //то всё очищаем
			}
		}

		// ждём срабатывания таймера.
		WaitForSingleObject(hWaitableTimer, INFINITE);
		//это вот ещё так можно, якобы ожидание можно остановить, когда события разные происходят
		//но проверять не будем, просто на всякий случай тут оставлю, вдруг ещё где-нибудь пригодится
		//MsgWaitForMultipleObjects(1, &hWaitableTimer, FALSE, INFINITE, QS_ALLEVENTS);
	}
	while (!m_lockKillTimerEvent.IsLocked());       //пока не придёт событие остановки

	CancelWaitableTimer(hWaitableTimer);
	CloseHandle(hWaitableTimer);
	m_lockKillTimerEvent.UnLock();
	return 0;
}

bool CMotherBoard::EmulateTapeOperation()
{
	bool bRet = true;
	// пока ошибок нет
	bool bError = false;
	// получим адрес блока параметров из R1 (BK emt 36)
	WORD pParam = GetRON(CCPU::R_R1);
	// Первый байт - команда: загрузка сохранение и т.п.
	WORD command = GetByteIndirect(pParam);
	// Установим код ошибки во втором байте (позже его поменяем)
	SetByteIndirect(pParam + 1, 2);
	WORD fileAddr = GetWordIndirect(pParam + 2);  // второе слово - адрес загрузки/сохранения
	WORD fileSize = GetWordIndirect(pParam + 4);  // третье слово - длина файла (для загрузки может быть 0)
	// Внутренняя загрузка на БК
	BYTE bkName[16];   // Максимальная длина имени файла на БК - 16 байтов

	// Подбираем 16 байтовое имя файла из блока параметров
	for (WORD c = 0; c < 16; ++c)
	{
		bkName[c] = GetByteIndirect(pParam + 6 + c);
	}

	CString strFileName = BKToUNICODE(bkName, 16); //тут надо перекодировать  имя файла из кои8 в unicode
	strFileName.Trim(); //удаляем пробелы в конце файла, а в середине - оставляем

	////////////////////////////////////////////////////////////////////////////
	// Файловые операции
	////////////////////////////////////////////////////////////////////////////
	// если "Загрузка"
	if (command == 3)
	{
		////////////////////////////////////////////////////////////////////////////
		// Загрузка файла
		////////////////////////////////////////////////////////////////////////////
		if (!g_Config.m_bEmulateLoadTape)
		{
			return true;
		}

		if (!m_pParent->GetStrBinFileName()->IsEmpty())
		{
			strFileName = *m_pParent->GetStrBinFileName();
			*m_pParent->GetStrBinFileName() = _T("");
		}

		if (strFileName.SpanExcluding(_T(" ")) == "")
		{
			// Если имя пустое - то покажем диалог выбора файла.
			// Запомним текущую директорию
			CString strCurDir;
			::GetCurrentDirectory(1024, strCurDir.GetBufferSetLength(1024));
			strCurDir.ReleaseBuffer();
			::SetCurrentDirectory(g_Config.m_strBinPath);
			CString strFilterBin(MAKEINTRESOURCE(IDS_FILEFILTER_BIN));
			CFileDialog dlg(true, nullptr, nullptr,
			                OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT | OFN_EXPLORER,
			                strFilterBin, m_pParent->GetScreen()->GetBackgroundWindow());
			// Зададим начальной директорией директорию с Bin файлами
			dlg.GetOFN().lpstrInitialDir = g_Config.m_strBinPath;

			if (dlg.DoModal() == IDCANCEL)
			{
				// Если нажали Отмену, установим ошибку во втором байте блока параметров
				SetByteIndirect(pParam + 1, 4);
				bError = true; // случилась ошибка
			}

			g_Config.m_strBinPath = g_Config.GetFilePath(dlg.GetPathName());
			strFileName = dlg.GetPathName(); //вот выбранный файл
			//имя файла надо бы как-то поместить в 0352..0372 иначе некоторые глюки наблюдаются
			UNICODEtoBK(strFileName, bkName, 16, true); //вот из этого массива будем потом помещать
			// восстановим текущую директорию
			::SetCurrentDirectory(strCurDir);
		}
		else    // Если имя не пустое
		{
			CString strCurrentPath;

			if (g_Config.m_bSavesDefault)
			{
				// If Saves Default flag is set loading from User directory
				strCurrentPath = g_Config.m_strSavesPath;
			}
			else
			{
				// If not Load from Binary files directory
				strCurrentPath = g_Config.m_strBinPath;
			}

			CFileStatus fs; //получим информацию о файле - таким образом проверяется существует он или нет

			//сейчас узнаем, нужно ли нам добавлять расширение .bin, или наоборот, удалять
			if (g_Config.GetFileExt(strFileName).CompareNoCase(_T(".bin")) == 0)
			{
				//у файла уже есть расширение бин
				if (!CFile::GetStatus(strCurrentPath + strFileName, fs)) //если нету файла с расширением.
				{
					CString str = g_Config.GetFileName(strFileName); //удаляем расширение

					if (CFile::GetStatus(strCurrentPath + str, fs)) //если есть файл без расширения
					{
						strFileName = str; //оставим файл без расширения
					}
					else
					{
						//нет файла ни с расширением, ни без расширения
						bError = true;
					}
				}
			}
			else
			{
				//у файла нету расширения бин
				if (!CFile::GetStatus(strCurrentPath + strFileName, fs)) //если нету файла без расширения
				{
					CString str = strFileName + _T(".bin"); //добавляем стандартное расширение для бин файлов.

					if (CFile::GetStatus(strCurrentPath + str, fs))  //если есть файл с расширением
					{
						strFileName = str;
					}
					else
					{
						//нет файла ни с расширением, ни без расширения
						bError = true;
					}
				}
			}

			strFileName = strCurrentPath + strFileName;
		}

		CFile file;
		WORD readAddr = 0;
		WORD readSize = 0;

		// Загрузим файл, если ошибок не было
		if (file.Open(strFileName, CFile::modeRead) && !bError)
		{
			file.Read(&readAddr, sizeof(readAddr));   // Первое слово в файле - адрес загрузки

			if (fileAddr) // Если адрес загрузки в блоке параметров не 0
			{
				readAddr = fileAddr;    // то загружать надо по указанному адресу
			}

			file.Read(&readSize, sizeof(readSize));   // Второе слово в файле - длина
			// сплошь и рядом встречаются .bin файлы. у которых во втором слове указана длина
			// меньше, чем длина файла - 4. Это другой формат бин, у которого в начале указывается
			// адрес, длина, имя файла[16], массив[длина], КС - контрольная сумма в конце
			auto filesz = (WORD)file.GetLength();

			if (readSize != filesz - 4)
			{
				if (readSize == filesz - 22)
				{
					file.Seek(20, SEEK_SET); //надо пропустить ненужные данные.
				}
				else
				{
					//то ошибка, непонятный формат бин, или нет конца файла
					bError = true;
				}
			}

			fileAddr = readAddr; //сохраним адрес начала загрузки, он нам ещё пригодится

			// Загрузка по адресу readAddr
			for (int i = 0; i < readSize; ++i)
			{
				BYTE val;
				file.Read(&val, sizeof(val));
				SetByteIndirect(readAddr++, val);
			}

			file.Close();

			// Исправляем код ошибки в блоке параметров
			if (!bError)
			{
				SetByteIndirect(pParam + 1, 0);
			}

			// Заполняем системные ячейки, как это делает emt 36
			SetWordIndirect(0264, fileAddr);
			SetWordIndirect(0266, readSize);
			SetWordIndirect(pParam + 026, fileAddr);
			SetWordIndirect(pParam + 030, readSize);

			//копируем прочитанное имя файла
			for (WORD i = 0; i < 16; ++i)
			{
				SetByteIndirect(pParam + 032 + i, bkName[i]);
			}
		}
		else
		{
			// При ошибке покажем сообщение
			CString strError;
			strError.Format(IDS_CANT_OPEN_FILE_S, strFileName);
			CBKMessageBox message;

			if (message.Show(strError, MB_ICONWARNING | MB_YESNO | MB_DEFBUTTON2) == IDYES)
			{
				BreakCPU();
				bRet = false;
			}
		}

		// Помещаем в R5 последний адрес, куда производилось чтение, как в emt 36
		SetRON(CCPU::R_R5, readAddr);
		// делаем RTS PC
		SetRON(CCPU::R_PC, GetWordIndirect(GetRON(CCPU::R_SP)));
		SetRON(CCPU::R_SP, GetRON(CCPU::R_SP) + 2);
	}
	else if (command == 2)
	{
		////////////////////////////////////////////////////////////////////////////
		// Сохранение файла
		////////////////////////////////////////////////////////////////////////////
		if (!g_Config.m_bEmulateSaveTape)
		{
			return true;
		}

		// Если имя пустое
		if (strFileName.SpanExcluding(_T(" ")) == "")
		{
			// Покажем диалог сохранения
			CFileDialog dlg(false, nullptr, nullptr,
			                OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT | OFN_EXPLORER,
			                nullptr, m_pParent->GetScreen()->GetBackgroundWindow());
			dlg.GetOFN().lpstrInitialDir = g_Config.m_strBinPath;

			if (dlg.DoModal() != IDOK)
			{
				// Если отмена - установим флаг ошибки
				SetByteIndirect(pParam + 1, 4);
				bError = true;
			}

			g_Config.m_strBinPath = g_Config.GetFilePath(dlg.GetPathName());
			// Получим имя
			strFileName = dlg.GetPathName();
		}
		else
		{
			// Если имя не пустое
			strFileName += _T(".bin"); //добавляем стандартное расширение для бин файлов.

			// If internal file loading
			if (g_Config.m_bSavesDefault)
			{
				// If Saves Default flag is set loading from User directory
				strFileName = g_Config.m_strSavesPath + strFileName;
			}
			else
			{
				// If not Load from Binary files directory
				strFileName = g_Config.m_strBinPath + strFileName;
			}
		}

		CFile file;

		// Save file array if no errors
		if ((file.Open(strFileName, CFile::modeCreate | CFile::modeWrite)) && (!bError))
		{
			// Write file address and length
			file.Write(&fileAddr, sizeof(fileAddr));
			file.Write(&fileSize, sizeof(fileSize));
			WORD writeAddr = fileAddr;

			for (int i = 0; i < fileSize; ++i)
			{
				BYTE val = GetByteIndirect(writeAddr++);
				file.Write(&val, sizeof(val));
			}

			file.Close();
			// Set no error in parameter block byte
			SetByteIndirect(pParam + 1, 0);
			// Set last reading address like in normal emt 36
			SetWordIndirect(0264, fileAddr);
			SetWordIndirect(0266, fileSize);
			SetWordIndirect(pParam + 026, fileAddr);
			SetWordIndirect(pParam + 030, fileSize);
			WORD wp = 0; //копируем записанное имя файла

			for (int i = 0; i < 8; ++i)
			{
				WORD w = GetWordIndirect(pParam + 06 + wp);
				SetWordIndirect(pParam + 032 + wp, w);
				wp += 2;
			}
		}

		// Make RTS PC
		SetRON(CCPU::R_PC, GetWordIndirect(GetRON(CCPU::R_SP)));
		SetRON(CCPU::R_SP, GetRON(CCPU::R_SP) + 2);
	}

	// Refresh keyboard
	m_kprs.clear(); //очистим всё, что было нажато - отменяем.
	m_reg177660 &= ~0200;  // ещё удалим флаг готовности нового кода
	m_pParent->SendMessage(WM_TAPE_FILE_LOAD);
	return bRet;
}

