More OpCodes - working towards ZEXALL perfection

This commit is contained in:
2026-04-22 22:38:53 +01:00
parent b50f7a79da
commit 02680cb92d
6 changed files with 364 additions and 38 deletions

View File

@@ -5,37 +5,47 @@ using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Core.Io
{
public class IO_Bus
{
{
public byte BorderColorIndex { get; private set; } = 7;
public bool BeeperState { get; private set; } = false;
public byte[] KeyboardRows = new byte[8] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
TapManager _tapManager = new TapManager();
public byte ReadPort(ushort portAddress)
{
// The Spectrum ULA responds to any even port address (where the lowest bit is 0)
if ((portAddress & 0x01) == 0)
public IO_Bus(TapManager tapManager)
{
byte highByte = (byte)(portAddress >> 8); // The B register!
byte result = 0xFF; // Start assuming no keys are pressed
// The ROM pulls a specific bit low (0) in the high byte to request a row.
// Sometimes it pulls multiple bits low to scan multiple rows at once, so we AND the results.
if ((highByte & 0x01) == 0) result &= KeyboardRows[0]; // 0xFE: CAPS, Z, X, C, V
if ((highByte & 0x02) == 0) result &= KeyboardRows[1]; // 0xFD: A, S, D, F, G
if ((highByte & 0x04) == 0) result &= KeyboardRows[2]; // 0xFB: Q, W, E, R, T
if ((highByte & 0x08) == 0) result &= KeyboardRows[3]; // 0xF7: 1, 2, 3, 4, 5
if ((highByte & 0x10) == 0) result &= KeyboardRows[4]; // 0xEF: 0, 9, 8, 7, 6
if ((highByte & 0x20) == 0) result &= KeyboardRows[5]; // 0xDF: P, O, I, U, Y
if ((highByte & 0x40) == 0) result &= KeyboardRows[6]; // 0xBF: ENTER, L, K, J, H
if ((highByte & 0x80) == 0) result &= KeyboardRows[7]; // 0x7F: SPACE, SYM, M, N, B
// The top 3 bits (5, 6, 7) are unused by the keyboard and usually return 1 on a real Spectrum
return (byte)(result | 0xE0);
_tapManager = tapManager;
}
// Return 0xFF for unhandled ports
return 0xFF;
}
public byte ReadPort(ushort portAddress)
{
// The Spectrum ULA responds to any even port address (where the lowest bit is 0)
if ((portAddress & 0x01) == 0)
{
byte highByte = (byte)(portAddress >> 8); // The B register!
byte result = 0xFF; // Start assuming no keys are pressed
// The ROM pulls a specific bit low (0) in the high byte to request a row.
// Sometimes it pulls multiple bits low to scan multiple rows at once, so we AND the results.
if ((highByte & 0x01) == 0) result &= KeyboardRows[0]; // 0xFE: CAPS, Z, X, C, V
if ((highByte & 0x02) == 0) result &= KeyboardRows[1]; // 0xFD: A, S, D, F, G
if ((highByte & 0x04) == 0) result &= KeyboardRows[2]; // 0xFB: Q, W, E, R, T
if ((highByte & 0x08) == 0) result &= KeyboardRows[3]; // 0xF7: 1, 2, 3, 4, 5
if ((highByte & 0x10) == 0) result &= KeyboardRows[4]; // 0xEF: 0, 9, 8, 7, 6
if ((highByte & 0x20) == 0) result &= KeyboardRows[5]; // 0xDF: P, O, I, U, Y
if ((highByte & 0x40) == 0) result &= KeyboardRows[6]; // 0xBF: ENTER, L, K, J, H
if ((highByte & 0x80) == 0) result &= KeyboardRows[7]; // 0x7F: SPACE, SYM, M, N, B
if (_tapManager.EarBit) result |= 0x40; // Set Bit 6 high
else result &= 0xBF; // Set Bit 6 low
//return result;
// The top 3 bits (5, 6, 7) are unused by the keyboard and usually return 1 on a real Spectrum
return (byte)(result | 0xE0);
}
// Return 0xFF for unhandled ports
return 0xFF;
}
public void WritePort(ushort portAddress, byte portValue)
{

View File

@@ -6,8 +6,20 @@ namespace Core.Io
public class TapManager
{
private Queue<byte[]> _blocks = new Queue<byte[]>();
private byte[] _currentBlock;
// State Machine Tracking
private enum TapeState { Idle, Pilot, Sync1, Sync2, Data, Pause }
private TapeState _state = TapeState.Idle;
public bool EarBit { get; private set; } = false; // The physical audio signal sent to the CPU
private int _pulseTimer = 0;
private int _pilotPulsesRemaining = 0;
private int _byteIndex = 0;
private int _bitIndex = 0;
private int _pulseCountForCurrentBit = 0;
public void LoadTapData(byte[] fileData)
{
_blocks.Clear();
@@ -15,20 +27,136 @@ namespace Core.Io
while (position < fileData.Length)
{
// 1. Read the 16-bit block length (Little Endian)
int blockLength = fileData[position] | (fileData[position + 1] << 8);
position += 2;
// 2. Extract the block payload
byte[] blockData = new byte[blockLength];
Array.Copy(fileData, position, blockData, 0, blockLength);
position += blockLength;
// 3. Queue it up
_blocks.Enqueue(blockData);
}
}
public void Play()
{
if (_blocks.Count > 0 && _state == TapeState.Idle)
{
LoadNextBlock();
}
}
private void LoadNextBlock()
{
if (_blocks.Count == 0)
{
_state = TapeState.Idle;
return;
}
_currentBlock = _blocks.Dequeue();
_byteIndex = 0;
_bitIndex = 7; // MSB first
_pulseCountForCurrentBit = 0;
// Header blocks (flag 0x00) have longer pilot tones than Data blocks
bool isHeader = _currentBlock.Length > 0 && _currentBlock[0] == 0;
_pilotPulsesRemaining = isHeader ? 8063 : 3223;
_state = TapeState.Pilot;
_pulseTimer = 2168; // Length of a pilot pulse
EarBit = false;
}
// --- THE ENGINE SYNC ---
// Call this every time the CPU steps, passing in how many T-States elapsed
public void Update(int tStatesElapsed)
{
if (_state == TapeState.Idle) return;
_pulseTimer -= tStatesElapsed;
if (_pulseTimer <= 0)
{
EarBit = !EarBit; // Flip the audio square wave
AdvanceState();
}
}
private void AdvanceState()
{
switch (_state)
{
case TapeState.Pilot:
_pilotPulsesRemaining--;
if (_pilotPulsesRemaining > 0)
{
_pulseTimer += 2168;
}
else
{
_state = TapeState.Sync1;
_pulseTimer += 667;
}
break;
case TapeState.Sync1:
_state = TapeState.Sync2;
_pulseTimer += 735;
break;
case TapeState.Sync2:
_state = TapeState.Data;
CalculateNextDataPulse();
break;
case TapeState.Data:
_pulseCountForCurrentBit++;
if (_pulseCountForCurrentBit < 2)
{
// A bit requires 2 pulses (one high, one low)
CalculateNextDataPulse();
}
else
{
// Move to next bit
_pulseCountForCurrentBit = 0;
_bitIndex--;
if (_bitIndex < 0)
{
// Move to next byte
_bitIndex = 7;
_byteIndex++;
if (_byteIndex >= _currentBlock.Length)
{
// Block finished! 1 second pause before the next block
_state = TapeState.Pause;
_pulseTimer += 3500000;
return;
}
}
CalculateNextDataPulse();
}
break;
case TapeState.Pause:
LoadNextBlock();
break;
}
}
private void CalculateNextDataPulse()
{
// Extract the current bit from the current byte
bool isBitOne = (_currentBlock[_byteIndex] & (1 << _bitIndex)) != 0;
// Bit 0 = 855 T-States, Bit 1 = 1710 T-States
_pulseTimer += isBitOne ? 1710 : 855;
}
// --- FAST LOAD METHODS (For ROM Hijack) ---
public byte[] GetNextBlock()
{
return _blocks.Count > 0 ? _blocks.Dequeue() : null;
@@ -37,3 +165,46 @@ namespace Core.Io
public bool HasBlocks => _blocks.Count > 0;
}
}
//using System;
//using System.Collections.Generic;
//namespace Core.Io
//{
// public class TapManager
// {
// private Queue<byte[]> _blocks = new Queue<byte[]>();
// public void LoadTapData(byte[] fileData)
// {
// _blocks.Clear();
// int position = 0;
// while (position < fileData.Length)
// {
// // 1. Read the 16-bit block length (Little Endian)
// int blockLength = fileData[position] | (fileData[position + 1] << 8);
// position += 2;
// // 2. Extract the block payload
// byte[] blockData = new byte[blockLength];
// Array.Copy(fileData, position, blockData, 0, blockLength);
// position += blockLength;
// // 3. Queue it up
// _blocks.Enqueue(blockData);
// }
// }
// public byte[] GetNextBlock()
// {
// return _blocks.Count > 0 ? _blocks.Dequeue() : null;
// }
// public bool HasBlocks => _blocks.Count > 0;
// }
//}