diff --git a/Contralto/AltoSystem.cs b/Contralto/AltoSystem.cs index 4781963..989f06a 100644 --- a/Contralto/AltoSystem.cs +++ b/Contralto/AltoSystem.cs @@ -46,6 +46,7 @@ namespace Contralto _orbitController = new OrbitController(this); _audioDAC = new AudioDAC(this); _organKeyboard = new OrganKeyboard(this); + _tridentController = new TridentController(this); _cpu = new AltoCPU(this); @@ -73,7 +74,8 @@ namespace Contralto _mouse.Reset(); _cpu.Reset(); _ethernetController.Reset(); - _orbitController.Reset(); + _orbitController.Reset(); + _tridentController.Reset(); UCodeMemory.Reset(); } @@ -104,6 +106,17 @@ namespace Contralto // Allow the DAC to flush its output // _audioDAC.Shutdown(); + + // + // Save disk contents + // + _diskController.CommitDisk(0); + _diskController.CommitDisk(1); + + for (int i = 0; i < 8; i++) + { + _tridentController.CommitDisk(i); + } } public void SingleStep() @@ -116,73 +129,131 @@ namespace Contralto _scheduler.Clock(); } - public void LoadDrive(int drive, string path) + public void LoadDiabloDrive(int drive, string path, bool newImage) { if (drive < 0 || drive > 1) { throw new InvalidOperationException("drive must be 0 or 1."); } - DiabloDiskType type; + // + // Commit the current disk first + // + _diskController.CommitDisk(drive); + + DiskGeometry geometry; // // We select the disk type based on the file extension. Very elegant. // switch(Path.GetExtension(path).ToLowerInvariant()) { + case ".dsk": + geometry = DiskGeometry.Diablo31; + break; + case ".dsk44": - type = DiabloDiskType.Diablo44; + geometry = DiskGeometry.Diablo44; break; default: - type = DiabloDiskType.Diablo31; + geometry = DiskGeometry.Diablo31; break; - } + } - DiabloPack newPack = new DiabloPack(type); - using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read)) - { - newPack.Load(fs, path, false); - fs.Close(); + IDiskPack newPack; + + if (newImage) + { + newPack = InMemoryDiskPack.CreateEmpty(geometry, path); + } + else + { + newPack = InMemoryDiskPack.Load(geometry, path); } _diskController.Drives[drive].LoadPack(newPack); } - public void UnloadDrive(int drive) + public void UnloadDiabloDrive(int drive) { if (drive < 0 || drive > 1) { throw new InvalidOperationException("drive must be 0 or 1."); } + // + // Commit the current disk first + // + _diskController.CommitDisk(drive); + _diskController.Drives[drive].UnloadPack(); } - // - // Disk handling - // - public void CommitDiskPack(int driveId) + public void LoadTridentDrive(int drive, string path, bool newImage) { - DiabloDrive drive = _diskController.Drives[driveId]; - if (drive.IsLoaded) + if (drive < 0 || drive > 8) { - using (FileStream fs = new FileStream(drive.Pack.PackName, FileMode.Create, FileAccess.Write)) - { - try - { - drive.Pack.Save(fs); - } - catch (Exception e) - { - // TODO: this does not really belong here. - System.Windows.Forms.MessageBox.Show(String.Format("Unable to save disk {0}'s contents. Error {0}. Any changes have been lost.", e.Message), "Disk save error"); - } - } + throw new InvalidOperationException("drive must be between 0 and 7."); } + + // + // Commit the current disk first + // + _tridentController.CommitDisk(drive); + + DiskGeometry geometry; + + // + // We select the disk type based on the file extension. Very elegant. + // + switch (Path.GetExtension(path).ToLowerInvariant()) + { + case ".t80": + geometry = DiskGeometry.TridentT80; + break; + + case ".t300": + geometry = DiskGeometry.TridentT300; + break; + + default: + geometry = DiskGeometry.TridentT80; + break; + } + + + IDiskPack newPack; + + if (newImage) + { + newPack = FileBackedDiskPack.CreateEmpty(geometry, path); + } + else + { + newPack = FileBackedDiskPack.Load(geometry, path); + } + + _tridentController.Drives[drive].LoadPack(newPack); } + public void UnloadTridentDrive(int drive) + { + if (drive < 0 || drive > 7) + { + throw new InvalidOperationException("drive must be between 0 and 7."); + } + + // + // Commit the current disk first + // + _tridentController.CommitDisk(drive); + + _tridentController.Drives[drive].UnloadPack(); + } + + public void PressBootKeys(AlternateBootType bootType) { switch(bootType) @@ -237,6 +308,11 @@ namespace Contralto get { return _orbitController; } } + public TridentController TridentController + { + get { return _tridentController; } + } + public Scheduler Scheduler { get { return _scheduler; } @@ -253,6 +329,7 @@ namespace Contralto private OrbitController _orbitController; private AudioDAC _audioDAC; private OrganKeyboard _organKeyboard; + private TridentController _tridentController; private Scheduler _scheduler; } diff --git a/Contralto/App.config b/Contralto/App.config index 9fb50f4..4b0d123 100644 --- a/Contralto/App.config +++ b/Contralto/App.config @@ -67,6 +67,22 @@ True + + + + + + + + + + + + + + + diff --git a/Contralto/CPU/CPU.cs b/Contralto/CPU/CPU.cs index f6e0116..22201f8 100644 --- a/Contralto/CPU/CPU.cs +++ b/Contralto/CPU/CPU.cs @@ -26,6 +26,7 @@ namespace Contralto.CPU Invalid = -1, Emulator = 0, Orbit = 1, + TridentOutput = 3, DiskSector = 4, Ethernet = 7, MemoryRefresh = 8, @@ -35,6 +36,7 @@ namespace Contralto.CPU DisplayVertical = 12, Parity = 13, DiskWord = 14, + TridentInput = 15, } public partial class AltoCPU : IClockable @@ -54,6 +56,8 @@ namespace Contralto.CPU _tasks[(int)TaskType.Ethernet] = new EthernetTask(this); _tasks[(int)TaskType.Parity] = new ParityTask(this); _tasks[(int)TaskType.Orbit] = new OrbitTask(this); + _tasks[(int)TaskType.TridentInput] = new TridentTask(this, true); + _tasks[(int)TaskType.TridentOutput] = new TridentTask(this, false); Reset(); } @@ -182,7 +186,7 @@ namespace Contralto.CPU _tasks[i].SoftReset(); } } - + Log.Write(LogComponent.CPU, "Silent Boot; microcode banks initialized to {0}", Conversion.ToOctal(_rmr)); UCodeMemory.LoadBanksFromRMR(_rmr); @@ -202,6 +206,9 @@ namespace Contralto.CPU // WakeupTask(CPU.TaskType.DiskSector); BlockTask(CPU.TaskType.DiskWord); + + BlockTask(CPU.TaskType.TridentInput); + BlockTask(CPU.TaskType.TridentOutput); } /// diff --git a/Contralto/CPU/MicroInstruction.cs b/Contralto/CPU/MicroInstruction.cs index 9151942..c013d84 100644 --- a/Contralto/CPU/MicroInstruction.cs +++ b/Contralto/CPU/MicroInstruction.cs @@ -217,6 +217,21 @@ namespace Contralto.CPU OrbitROSCommand = 14, } + /// + /// Trident disk controller, from the microcode. + /// + enum TridentF2 + { + ReadKDTA = 6, + STATUS = 8, + KTAG = 10, + WriteKDTA = 11, + WAIT = 12, // These two are identical in function + WAIT2 = 13, + RESET = 14, + EMPTY = 15, + } + /// /// MicroInstruction encapsulates the decoding of a microinstruction word. /// It also caches precomputed metadata related to the microinstruction that diff --git a/Contralto/CPU/Tasks/EmulatorTask.cs b/Contralto/CPU/Tasks/EmulatorTask.cs index bb2f4f4..1b05edd 100644 --- a/Contralto/CPU/Tasks/EmulatorTask.cs +++ b/Contralto/CPU/Tasks/EmulatorTask.cs @@ -33,6 +33,9 @@ namespace Contralto.CPU // The Wakeup signal is always true for the Emulator task. _wakeup = true; + + // The Emulator is a RAM-related task. + _ramTask = true; } public override void Reset() @@ -138,20 +141,26 @@ namespace Contralto.CPU // switch(_busData) { - case 1: - case 2: - case 3: + case 0x01: + case 0x02: + case 0x03: // Ethernet _cpu._system.EthernetController.STARTF(_busData); break; - case 4: - // Orbit + case 0x04: + // Orbit _cpu._system.OrbitController.STARTF(_busData); break; + case 0x10: + case 0x20: + // Trident start + _cpu._system.TridentController.STARTF(_busData); + break; + default: - Log.Write(Logging.LogType.Warning, Logging.LogComponent.EmulatorTask, "STARTF for non-Ethernet device (code {0})", + Log.Write(Logging.LogType.Warning, Logging.LogComponent.EmulatorTask, "STARTF for unknown device (code {0})", Conversion.ToOctal(_busData)); break; } @@ -163,6 +172,8 @@ namespace Contralto.CPU break; case EmulatorF1.RDRAM: + // TODO: move RDRAM, WRTRAM and S-register BS stuff into the main Task implementation, + // guarded by a check of _ramTask. _rdRam = true; break; diff --git a/Contralto/CPU/Tasks/OrbitTask.cs b/Contralto/CPU/Tasks/OrbitTask.cs index e192a14..0de6245 100644 --- a/Contralto/CPU/Tasks/OrbitTask.cs +++ b/Contralto/CPU/Tasks/OrbitTask.cs @@ -31,6 +31,9 @@ namespace Contralto.CPU { _taskType = TaskType.Orbit; _wakeup = false; + + // The Orbit task is RAM-related. + _ramTask = true; } protected override void ExecuteBlock() diff --git a/Contralto/CPU/Tasks/Task.cs b/Contralto/CPU/Tasks/Task.cs index aa9ce98..3b01a14 100644 --- a/Contralto/CPU/Tasks/Task.cs +++ b/Contralto/CPU/Tasks/Task.cs @@ -139,13 +139,13 @@ namespace Contralto.CPU protected virtual InstructionCompletion ExecuteInstruction(MicroInstruction instruction) { InstructionCompletion completion = InstructionCompletion.Normal; - bool swMode = false; + bool swMode = false; ushort aluData; ushort nextModifier; bool softReset = _softReset; _loadR = false; _loadS = false; - _rSelect = 0; + _rSelect = 0; _busData = 0; _softReset = false; @@ -159,7 +159,7 @@ namespace Contralto.CPU { if (!_cpu._system.MemoryBus.Ready(instruction.MemoryOperation)) { - // Suspend operation for this cycle. + // Suspend operation for this cycle. return InstructionCompletion.MemoryWait; } } @@ -248,6 +248,12 @@ namespace Contralto.CPU _busData &= instruction.ConstantValue; } + // + // Let F2s that need to modify bus data before the ALU runs do their thing. + // (This is used by the Trident KDTA special functions) + // + ExecuteSpecialFunction2PostBusSource(instruction); + // // If there was a RDRAM operation last cycle, we AND in the uCode RAM data here. // @@ -487,7 +493,7 @@ namespace Contralto.CPU // address register... which is loaded from the ALU output whenver T is loaded // from its source." // - UCodeMemory.LoadControlRAMAddress(aluData); + UCodeMemory.LoadControlRAMAddress(aluData); } // Load L (and M) from ALU outputs. @@ -496,8 +502,7 @@ namespace Contralto.CPU _cpu._l = aluData; // Only RAM-related tasks can modify M. - if (_taskType == TaskType.Emulator || - _taskType == TaskType.Orbit) + if (_ramTask) { _cpu._m = aluData; } @@ -587,6 +592,16 @@ namespace Contralto.CPU // Nothing by default. } + /// + /// Executes after bus sources are selected but before the ALU. Used to allow Task-specific F2s that need to + /// modify bus data to do so. + /// + /// + protected virtual void ExecuteSpecialFunction2PostBusSource(MicroInstruction instruction) + { + // Nothing by default. + } + /// /// Executes after the ALU has run but before the shifter runs, provides task-specific implementations /// the opportunity to handle task-specific F2s. @@ -629,6 +644,14 @@ namespace Contralto.CPU /// protected SystemType _systemType; + // + // Task metadata: + // + + // Whether this task is RAM-enabled or not + // (can access S registers, etc.) + protected bool _ramTask; + // // Per uInstruction Task Data: // Modified by both the base Task implementation and any subclasses diff --git a/Contralto/CPU/Tasks/TridentTask.cs b/Contralto/CPU/Tasks/TridentTask.cs new file mode 100644 index 0000000..6a6015c --- /dev/null +++ b/Contralto/CPU/Tasks/TridentTask.cs @@ -0,0 +1,157 @@ +/* + This file is part of ContrAlto. + + ContrAlto is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + ContrAlto is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with ContrAlto. If not, see . +*/ + +using Contralto.IO; +using System; + +namespace Contralto.CPU +{ + public partial class AltoCPU + { + /// + /// TridentTask implements the microcode special functions for the Trident + /// disk controller (for both the read and write tasks). + /// + private sealed class TridentTask : Task + { + public TridentTask(AltoCPU cpu, bool input) : base(cpu) + { + _taskType = input ? TaskType.TridentInput : TaskType.TridentOutput; + _wakeup = false; + + _tridentController = cpu._system.TridentController; + + // Both Trident tasks are RAM-related + _ramTask = true; + } + + public override void SoftReset() + { + // + // Stop the controller. + // + _tridentController.Stop(); + + base.SoftReset(); + } + + protected override ushort GetBusSource(MicroInstruction instruction) + { + // + // The Trident tasks are wired to be RAM-enabled tasks so they can use + // S registers. + // This code is stolen from the Emulator task; we should REALLY refactor this + // since both this and the Orbit Task need it. + // + EmulatorBusSource ebs = (EmulatorBusSource)instruction.BS; + + switch (ebs) + { + case EmulatorBusSource.ReadSLocation: + if (instruction.RSELECT != 0) + { + return _cpu._s[_rb][instruction.RSELECT]; + } + else + { + // "...when reading data from the S registers onto the processor bus, + // the RSELECT value 0 causes the current value of the M register to + // appear on the bus..." + return _cpu._m; + } + + case EmulatorBusSource.LoadSLocation: + // "When an S register is being loaded from M, the processor bus receives an + // undefined value rather than being set to zero." + _loadS = true; + return 0xffff; // Technically this is an "undefined value," we're defining it as -1. + + default: + throw new InvalidOperationException(String.Format("Unhandled bus source {0}", instruction.BS)); + } + } + + protected override void ExecuteSpecialFunction2PostBusSource(MicroInstruction instruction) + { + TridentF2 tf2 = (TridentF2)instruction.F2; + + switch (tf2) + { + case TridentF2.ReadKDTA: + // + // <-KDTA is actually MD<- (SF 6), repurposed to gate disk data onto the bus + // iff BS is None. Otherwise it behaves like a normal MD<-. We'll let + // the normal Task implementation handle the actual MD<- operation. + // + if (instruction.BS == BusSource.None) + { + // _busData at this point should be 0xffff. We could technically + // just directly assign the bits... + _busData &= _tridentController.KDTA; + } + break; + + case TridentF2.STATUS: + _busData &= _tridentController.STATUS; + break; + + case TridentF2.EMPTY: + _tridentController.WaitForEmpty(); + break; + + } + } + + protected override void ExecuteSpecialFunction2(MicroInstruction instruction) + { + TridentF2 tf2 = (TridentF2)instruction.F2; + + switch (tf2) + { + case TridentF2.KTAG: + _tridentController.TagInstruction(_busData); + break; + + case TridentF2.WriteKDTA: + _tridentController.KDTA = _busData; + break; + + case TridentF2.WAIT: + case TridentF2.WAIT2: + // Identical to BLOCK + this.BlockTask(); + break; + + case TridentF2.RESET: + _tridentController.ControllerReset(); + break; + + case TridentF2.STATUS: + case TridentF2.EMPTY: + // Handled in PostBusSource override. + break; + + default: + throw new InvalidOperationException(String.Format("Unhandled trident special function 2 {0}", tf2)); + } + } + + private TridentController _tridentController; + + } + } +} diff --git a/Contralto/CPU/UCodeDisassembler.cs b/Contralto/CPU/UCodeDisassembler.cs index a460f54..e082ecf 100644 --- a/Contralto/CPU/UCodeDisassembler.cs +++ b/Contralto/CPU/UCodeDisassembler.cs @@ -229,7 +229,13 @@ namespace Contralto.CPU break; case SpecialFunction2.StoreMD: - if (instruction.F1 != SpecialFunction1.LoadMAR) + // Special case for Trident + if ((task == TaskType.TridentInput || task == TaskType.TridentOutput) && + instruction.BS == BusSource.None) + { + f2 = "MD← KDTA "; + } + else if (instruction.F1 != SpecialFunction1.LoadMAR) { f2 = "MD← "; } @@ -352,6 +358,8 @@ namespace Contralto.CPU { case TaskType.Emulator: case TaskType.Orbit: + case TaskType.TridentInput: + case TaskType.TridentOutput: return DisassembleEmulatorBusSource(instruction, out loadS); default: @@ -368,7 +376,7 @@ namespace Contralto.CPU return DisassembleEmulatorSpecialFunction1(instruction); case TaskType.Orbit: - return DisassembleOrbitSpecialFunction1(instruction); + return DisassembleOrbitSpecialFunction1(instruction); default: return String.Format("F1 {0}", Conversion.ToOctal((int)instruction.F1)); @@ -383,7 +391,11 @@ namespace Contralto.CPU return DisassembleEmulatorSpecialFunction2(instruction); case TaskType.Orbit: - return DisassembleOrbitSpecialFunction2(instruction); + return DisassembleOrbitSpecialFunction2(instruction); + + case TaskType.TridentInput: + case TaskType.TridentOutput: + return DisassembleTridentSpecialFunction2(instruction); default: return String.Format("F2 {0}", Conversion.ToOctal((int)instruction.F2)); @@ -539,5 +551,38 @@ namespace Contralto.CPU return String.Format("Orbit F2 {0}", Conversion.ToOctal((int)of2)); } } + + private static string DisassembleTridentSpecialFunction2(MicroInstruction instruction) + { + TridentF2 tf2 = (TridentF2)instruction.F2; + + switch (tf2) + { + case TridentF2.EMPTY: + return "EMPTY "; + + case TridentF2.KTAG: + return "KTAG← "; + + case TridentF2.ReadKDTA: + return "←KDTA "; + + case TridentF2.RESET: + return "RESET "; + + case TridentF2.STATUS: + return "STATUS "; + + case TridentF2.WAIT: + case TridentF2.WAIT2: + return "WAIT "; + + case TridentF2.WriteKDTA: + return "KDTA← "; + + default: + return String.Format("Trident F2 {0}", Conversion.ToOctal((int)tf2)); + } + } } } diff --git a/Contralto/Configuration.cs b/Contralto/Configuration.cs index bd6be45..7df5fbc 100644 --- a/Contralto/Configuration.cs +++ b/Contralto/Configuration.cs @@ -17,6 +17,7 @@ using Contralto.Logging; using System; +using System.Collections.Specialized; using System.IO; using System.Reflection; @@ -114,7 +115,14 @@ namespace Contralto // See if PCap is available. TestPCap(); - ReadConfiguration(); + try + { + ReadConfiguration(); + } + catch + { + Console.WriteLine("Warning: unable to load configuration. Assuming default settings."); + } // Special case: On first startup, AlternateBoot will come back as "None" which // is an invalid UI setting; default to Ethernet in this case. @@ -122,6 +130,15 @@ namespace Contralto { AlternateBootType = AlternateBootType.Ethernet; } + + // + // If configuration specifies fewer than 8 Trident disks, we need to pad the list out to 8 with empty entries. + // + int start = TridentImages.Count; + for (int i = start; i < 8; i++) + { + TridentImages.Add(String.Empty); + } } /// @@ -144,6 +161,11 @@ namespace Contralto /// public static string Drive1Image; + /// + /// The currently loaded images for the Trident controller. + /// + public static StringCollection TridentImages; + /// /// The Ethernet host address for this Alto /// @@ -294,6 +316,7 @@ namespace Contralto AudioDACCapturePath = Properties.Settings.Default.AudioDACCapturePath; Drive0Image = Properties.Settings.Default.Drive0Image; Drive1Image = Properties.Settings.Default.Drive1Image; + TridentImages = Properties.Settings.Default.TridentImages; SystemType = (SystemType)Properties.Settings.Default.SystemType; HostAddress = Properties.Settings.Default.HostAddress; HostPacketInterfaceName = Properties.Settings.Default.HostPacketInterfaceName; @@ -308,13 +331,14 @@ namespace Contralto AudioDACCapturePath = Properties.Settings.Default.AudioDACCapturePath; EnablePrinting = Properties.Settings.Default.EnablePrinting; PrintOutputPath = Properties.Settings.Default.PrintOutputPath; - ReversePageOrder = Properties.Settings.Default.ReversePageOrder; + ReversePageOrder = Properties.Settings.Default.ReversePageOrder; } private static void WriteConfigurationWindows() { Properties.Settings.Default.Drive0Image = Drive0Image; Properties.Settings.Default.Drive1Image = Drive1Image; + Properties.Settings.Default.TridentImages = TridentImages; Properties.Settings.Default.SystemType = (int)SystemType; Properties.Settings.Default.HostAddress = HostAddress; Properties.Settings.Default.HostPacketInterfaceName = HostPacketInterfaceName; @@ -478,6 +502,21 @@ namespace Contralto field.SetValue(null, Enum.Parse(typeof(LogComponent), value, true)); } break; + + case "StringCollection": + { + // value is expected to be a comma-delimited set. + StringCollection sc = new StringCollection(); + string[] strings = value.Split(','); + + foreach(string s in strings) + { + sc.Add(s); + } + + field.SetValue(null, sc); + } + break; } } catch diff --git a/Contralto/Contralto.cfg b/Contralto/Contralto.cfg index e0e177a..3217608 100644 --- a/Contralto/Contralto.cfg +++ b/Contralto/Contralto.cfg @@ -23,3 +23,15 @@ AlternateBootType = Ethernet EnablePrinting = true PrintOutputPath = . ReversePageOrder = true + +# Disk options +# + +# Diablo images: These specify a single image file for drive 0 and drive 1 +# +# Drive0Image = +# Drive1Image = + +# Trident images: This specifies up to eight images for Trident drives 0 through 8 +# in a comma-delimited list. Empty entries are allowed, as below: +# TridentImages = image0.dsk80, , image2.dsk300, image3.disk80 diff --git a/Contralto/Contralto.csproj b/Contralto/Contralto.csproj index 3798567..d02af50 100644 --- a/Contralto/Contralto.csproj +++ b/Contralto/Contralto.csproj @@ -125,7 +125,9 @@ + + @@ -133,6 +135,7 @@ + @@ -199,7 +202,7 @@ - + diff --git a/Contralto/IO/DiabloDrive.cs b/Contralto/IO/DiabloDrive.cs index f0b32ea..e737faf 100644 --- a/Contralto/IO/DiabloDrive.cs +++ b/Contralto/IO/DiabloDrive.cs @@ -75,7 +75,7 @@ namespace Contralto.IO public void Reset() { _sector = 0; - _cylinder = 0; + _cylinder = 0; _head = 0; _sectorModified = false; @@ -91,14 +91,24 @@ namespace Contralto.IO // Total "words" (really timeslices) per sector. public static readonly int SectorWordCount = 269 + HeaderOffset + 34; - public void LoadPack(DiabloPack pack) + public void LoadPack(IDiskPack pack) { + if (_pack != null) + { + _pack.Dispose(); + } + _pack = pack; Reset(); } public void UnloadPack() { + if (_pack != null) + { + _pack.Dispose(); + } + _pack = null; Reset(); } @@ -108,7 +118,7 @@ namespace Contralto.IO get { return _pack != null; } } - public DiabloPack Pack + public IDiskPack Pack { get { return _pack; } } @@ -218,13 +228,13 @@ namespace Contralto.IO // Note that this data is packed in in REVERSE ORDER because that's // how it gets written out and it's how the Alto expects it to be read back in. // - DiabloDiskSector sector = _pack.GetSector(_cylinder, _head, _sector); + DiskSector sector = _pack.GetSector(_cylinder, _head, _sector); // Header (2 words data, 1 word cksum) for (int i = HeaderOffset + 1, j = 1; i < HeaderOffset + 3; i++, j--) { // actual data to be loaded from disk / cksum calculated - _sectorData[i] = new DataCell(sector.Header[j], CellType.Data); + _sectorData[i] = new DataCell(sector.ReadHeader(j), CellType.Data); } ushort checksum = CalculateChecksum(_sectorData, HeaderOffset + 1, 2); @@ -235,7 +245,7 @@ namespace Contralto.IO for (int i = LabelOffset + 1, j = 7; i < LabelOffset + 9; i++, j--) { // actual data to be loaded from disk / cksum calculated - _sectorData[i] = new DataCell(sector.Label[j], CellType.Data); + _sectorData[i] = new DataCell(sector.ReadLabel(j), CellType.Data); } checksum = CalculateChecksum(_sectorData, LabelOffset + 1, 8); @@ -246,7 +256,7 @@ namespace Contralto.IO for (int i = DataOffset + 1, j = 255; i < DataOffset + 257; i++, j--) { // actual data to be loaded from disk / cksum calculated - _sectorData[i] = new DataCell(sector.Data[j], CellType.Data); + _sectorData[i] = new DataCell(sector.ReadData(j), CellType.Data); } checksum = CalculateChecksum(_sectorData, DataOffset + 1, 256); @@ -257,7 +267,7 @@ namespace Contralto.IO /// /// Commits modified sector data back to the emulated disk. - /// Intended to be called at the end of the sector / beginning of the next. + /// Intended to be called at the end of the sector / beginning of the next. /// private void CommitSector() { @@ -266,33 +276,38 @@ namespace Contralto.IO return; } - DiabloDiskSector sector = _pack.GetSector(_cylinder, _head, _sector); + DiskSector sector = _pack.GetSector(_cylinder, _head, _sector); // Header (2 words data, 1 word cksum) for (int i = HeaderOffset + 1, j = 1; i < HeaderOffset + 3; i++, j--) { // actual data to be loaded from disk / cksum calculated - sector.Header[j] = _sectorData[i].Data; + sector.WriteHeader(j, _sectorData[i].Data); } // Label (8 words data, 1 word cksum) for (int i = LabelOffset + 1, j = 7; i < LabelOffset + 9; i++, j--) { // actual data to be loaded from disk / cksum calculated - sector.Label[j] = _sectorData[i].Data; + sector.WriteLabel(j, _sectorData[i].Data); } // sector data (256 words data, 1 word cksum) for (int i = DataOffset + 1, j = 255; i < DataOffset + 257; i++, j--) { // actual data to be loaded from disk / cksum calculated - sector.Data[j] = _sectorData[i].Data; + sector.WriteData(j, _sectorData[i].Data); } + + // + // Commit it back to the pack. + // + _pack.CommitSector(sector); } private void InitSector() { - // Fill in sector with default data (basically, fill in non-data areas). + // Fill in sector with default data (basically, fill in non-data areas). // // header delay, 22 words @@ -362,6 +377,6 @@ namespace Contralto.IO // The pack loaded into the drive - DiabloPack _pack; + IDiskPack _pack; } } diff --git a/Contralto/IO/DiabloPack.cs b/Contralto/IO/DiabloPack.cs deleted file mode 100644 index 1fa7bc9..0000000 --- a/Contralto/IO/DiabloPack.cs +++ /dev/null @@ -1,230 +0,0 @@ -/* - This file is part of ContrAlto. - - ContrAlto is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - ContrAlto is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with ContrAlto. If not, see . -*/ - -using System; -using System.IO; - -namespace Contralto.IO -{ - - /// - /// DiskGeometry encapsulates the geometry of a disk. - /// - public struct DiskGeometry - { - public DiskGeometry(int cylinders, int tracks, int sectors) - { - Cylinders = cylinders; - Tracks = tracks; - Sectors = sectors; - } - - public int Cylinders; - public int Tracks; - public int Sectors; - } - - public enum DiabloDiskType - { - Diablo31, - Diablo44 - } - - /// - /// DiabloDiskSector encapsulates the records contained in a single Alto disk sector - /// on a Diablo disk. This includes the header, label, and data records. - /// - public class DiabloDiskSector - { - public DiabloDiskSector(byte[] header, byte[] label, byte[] data) - { - if (header.Length != 4 || - label.Length != 16 || - data.Length != 512) - { - throw new InvalidOperationException("Invalid sector header/label/data length."); - } - - Header = GetUShortArray(header); - Label = GetUShortArray(label); - Data = GetUShortArray(data); - } - - private ushort[] GetUShortArray(byte[] data) - { - if ((data.Length % 2) != 0) - { - throw new InvalidOperationException("Array length must be even."); - } - - ushort[] array = new ushort[data.Length / 2]; - - int offset = 0; - for(int i=0;i - /// Encapsulates disk image data for all disk packs used with the - /// standard Alto Disk Controller (i.e. the 31 and 44, which differ - /// only in the number of cylinders) - /// - public class DiabloPack - { - public DiabloPack(DiabloDiskType type) - { - _diskType = type; - _packName = null; - _geometry = new DiskGeometry(type == DiabloDiskType.Diablo31 ? 203 : 406, 2, 12); - _sectors = new DiabloDiskSector[_geometry.Cylinders, _geometry.Tracks, _geometry.Sectors]; - } - - public DiskGeometry Geometry - { - get { return _geometry; } - } - - public string PackName - { - get { return _packName; } - } - - public void Load(Stream imageStream, string path, bool reverseByteOrder) - { - _packName = path; - for(int cylinder = 0; cylinder < _geometry.Cylinders; cylinder++) - { - for(int track = 0; track < _geometry.Tracks; track++) - { - for(int sector = 0; sector < _geometry.Sectors; sector++) - { - byte[] header = new byte[4]; // 2 words - byte[] label = new byte[16]; // 8 words - byte[] data = new byte[512]; // 256 words - - // - // Bitsavers images have an extra word in the header for some reason. - // ignore it. - // TODO: should support different formats ("correct" raw, Alto CopyDisk format, etc.) - // - imageStream.Seek(2, SeekOrigin.Current); - - - if (imageStream.Read(header, 0, header.Length) != header.Length) - { - throw new InvalidOperationException("Short read while reading sector header."); - } - - if (imageStream.Read(label, 0, label.Length) != label.Length) - { - throw new InvalidOperationException("Short read while reading sector label."); - } - - if (imageStream.Read(data, 0, data.Length) != data.Length) - { - throw new InvalidOperationException("Short read while reading sector data."); - } - - if (reverseByteOrder) - { - SwapBytes(header); - SwapBytes(label); - SwapBytes(data); - } - - _sectors[cylinder, track, sector] = new DiabloDiskSector(header, label, data); - } - } - } - - if (imageStream.Position != imageStream.Length) - { - throw new InvalidOperationException("Extra data at end of image file."); - } - } - - public void Save(Stream imageStream) - { - for (int cylinder = 0; cylinder < _geometry.Cylinders; cylinder++) - { - for (int track = 0; track < _geometry.Tracks; track++) - { - for (int sector = 0; sector < _geometry.Sectors; sector++) - { - byte[] header = new byte[4]; // 2 words - byte[] label = new byte[16]; // 8 words - byte[] data = new byte[512]; // 256 words - - // - // Bitsavers images have an extra word in the header for some reason. - // We will follow this 'standard' when writing out. - // TODO: should support different formats ("correct" raw, Alto CopyDisk format, etc.) - // - byte[] dummy = new byte[2]; - imageStream.Write(dummy, 0, 2); - - DiabloDiskSector s = _sectors[cylinder, track, sector]; - - WriteWordBuffer(imageStream, s.Header); - WriteWordBuffer(imageStream, s.Label); - WriteWordBuffer(imageStream, s.Data); - } - } - } - } - - public DiabloDiskSector GetSector(int cylinder, int track, int sector) - { - return _sectors[cylinder, track, sector]; - } - - private void WriteWordBuffer(Stream imageStream, ushort[] buffer) - { - // TODO: this is beyond inefficient - for(int i=0;i> 8)); - } - } - - private void SwapBytes(byte[] data) - { - for(int i=0;i. +*/ + +using System; +using System.IO; + +namespace Contralto.IO +{ + + /// + /// DiskGeometry encapsulates the geometry of a disk. + /// + public struct DiskGeometry + { + public DiskGeometry(int cylinders, int heads, int sectors, SectorGeometry sectorGeometry) + { + Cylinders = cylinders; + Heads = heads; + Sectors = sectors; + SectorGeometry = sectorGeometry; + } + + public int Cylinders; + public int Heads; + public int Sectors; + public SectorGeometry SectorGeometry; + + /// + /// Returns the total size (in bytes) of a disk with this geometry. + /// This includes the extra word-per-sector of the Bitsavers images. + /// + /// + public int GetDiskSizeBytes() + { + return SectorGeometry.GetSectorSizeBytes() * (Sectors * Heads * Cylinders); + } + + // + // Standard Diablo geometries + // + public static readonly DiskGeometry Diablo31 = new DiskGeometry(203, 2, 12, SectorGeometry.Diablo); + public static readonly DiskGeometry Diablo44 = new DiskGeometry(406, 2, 12, SectorGeometry.Diablo); + + // + // Standard Trident geometries + // + public static readonly DiskGeometry TridentT80 = new DiskGeometry(815, 5, 9, SectorGeometry.Trident); + public static readonly DiskGeometry TridentT300 = new DiskGeometry(815, 19, 9, SectorGeometry.Trident); + public static readonly DiskGeometry Shugart4004 = new DiskGeometry(202, 4, 8, SectorGeometry.Trident); + public static readonly DiskGeometry Shugart4008 = new DiskGeometry(202, 8, 8, SectorGeometry.Trident); + } + + /// + /// Describes the geometry of an Alto disk sector in terms of the + /// size of the header, label, and data blocks. + /// + public struct SectorGeometry + { + /// + /// Specifies the layout of a sector on an Alto pack. Sizes are in bytes. + /// + public SectorGeometry(int headerSizeBytes, int labelSizeBytes, int dataSizeBytes) + { + HeaderSizeBytes = headerSizeBytes; + LabelSizeBytes = labelSizeBytes; + DataSizeBytes = dataSizeBytes; + + HeaderSize = HeaderSizeBytes / 2; + LabelSize = LabelSizeBytes / 2; + DataSize = DataSizeBytes / 2; + } + + public int HeaderSize; + public int LabelSize; + public int DataSize; + + public int HeaderSizeBytes; + public int LabelSizeBytes; + public int DataSizeBytes; + + /// + /// Returns the total size (in bytes) of a sector with this geometry. + /// This includes the extra word-per-sector of the Bitsavers images. + /// + /// + public int GetSectorSizeBytes() + { + return DataSizeBytes + + LabelSizeBytes + + HeaderSizeBytes + + 2; // Extra dummy word + } + + public static readonly SectorGeometry Diablo = new SectorGeometry(4, 16, 512); + public static readonly SectorGeometry Trident = new SectorGeometry(4, 20, 2048); + } + + /// + /// DiskSector encapsulates the records contained in a single Alto disk sector + /// on a disk. This includes the header, label, and data records. + /// + public class DiskSector + { + /// + /// Create a new DiskSector populated from the specified stream. + /// + /// + /// + /// + /// + /// + public DiskSector(SectorGeometry geometry, Stream inputStream, int cylinder, int head, int sector) + { + // + // Read in the sector from the input stream. + // + byte[] header = new byte[geometry.HeaderSizeBytes]; + byte[] label = new byte[geometry.LabelSizeBytes]; + byte[] data = new byte[geometry.DataSizeBytes]; + + // + // Bitsavers images have an extra word in the header for some reason. + // ignore it. + // TODO: should support different formats ("correct" raw, Alto CopyDisk format, etc.) + // + inputStream.Seek(2, SeekOrigin.Current); + + if (inputStream.Read(header, 0, header.Length) != header.Length) + { + throw new InvalidOperationException("Short read while reading sector header."); + } + + if (inputStream.Read(label, 0, label.Length) != label.Length) + { + throw new InvalidOperationException("Short read while reading sector label."); + } + + if (inputStream.Read(data, 0, data.Length) != data.Length) + { + throw new InvalidOperationException("Short read while reading sector data."); + } + + _header = GetUShortArray(header); + _label = GetUShortArray(label); + _data = GetUShortArray(data); + + _cylinder = cylinder; + _head = head; + _sector = sector; + + _modified = false; + } + + /// + /// Create a new, empty sector. + /// + /// + /// + /// + /// + public DiskSector(SectorGeometry geometry, int cylinder, int head, int sector) + { + _header = new ushort[geometry.HeaderSize]; + _label = new ushort[geometry.LabelSize]; + _data = new ushort[geometry.DataSize]; + + _cylinder = cylinder; + _head = head; + _sector = sector; + + _modified = true; + } + + public int Cylinder + { + get { return _cylinder; } + } + + public int Head + { + get { return _head; } + } + + public int Sector + { + get { return _sector; } + } + + public bool Modified + { + get { return _modified; } + } + + public ushort ReadHeader(int offset) + { + return _header[offset]; + } + + public void WriteHeader(int offset, ushort value) + { + _header[offset] = value; + _modified = true; + } + + public ushort ReadLabel(int offset) + { + return _label[offset]; + } + + public void WriteLabel(int offset, ushort value) + { + _label[offset] = value; + _modified = true; + } + + public ushort ReadData(int offset) + { + return _data[offset]; + } + + public void WriteData(int offset, ushort value) + { + _data[offset] = value; + _modified = true; + } + + public void WriteToStream(Stream s) + { + // + // Bitsavers images have an extra word in the header for some reason. + // We will follow this standard when writing out. + // TODO: should support different formats ("correct" raw, Alto CopyDisk format, etc.) + // + byte[] dummy = new byte[2]; + s.Write(dummy, 0, 2); + + WriteWordBuffer(s, _header); + WriteWordBuffer(s, _label); + WriteWordBuffer(s, _data); + + _modified = false; + } + + private ushort[] GetUShortArray(byte[] data) + { + if ((data.Length % 2) != 0) + { + throw new InvalidOperationException("Array length must be even."); + } + + ushort[] array = new ushort[data.Length / 2]; + + int offset = 0; + for(int i=0;i> 8)); + } + } + + private ushort[] _header; + private ushort[] _label; + private ushort[] _data; + + private int _cylinder; + private int _head; + private int _sector; + + private bool _modified; + } + + /// + /// The IDiskPack interface defines a generic mechanism for creating, loading, storing, + /// and accessing the sectors of a disk pack. + /// + public interface IDiskPack : IDisposable + { + /// + /// The geometry of this pack. + /// + DiskGeometry Geometry { get; } + + /// + /// The filename of this pack. + /// + string PackName { get; } + + /// + /// Commits the current in-memory image back to the file it came from. + /// + void Save(); + + /// + /// Retrieves the specified sector from storage. + /// + /// + /// + /// + /// + DiskSector GetSector(int cylinder, int head, int sector); + + /// + /// Commits this sector back to storage. + /// + /// + void CommitSector(DiskSector sector); + } + + + /// + /// In-memory implementation of IDiskPack. Useful for small disks (e.g. Diablo). + /// Changes to the in-memory copy are only committed back to the disk image + /// when Save is invoked. + /// + public class InMemoryDiskPack : IDiskPack + { + /// + /// Creates a new, empty disk pack with the specified geometry. + /// + /// + /// + public static InMemoryDiskPack CreateEmpty(DiskGeometry geometry, string path) + { + return new InMemoryDiskPack(geometry, path, false); + } + + /// + /// Loads an existing disk pack image. + /// + /// + /// + /// + public static InMemoryDiskPack Load(DiskGeometry geometry, string path) + { + return new InMemoryDiskPack(geometry, path, true); + } + + public void Dispose() + { + // + // Nothing to do here. + // + } + + private InMemoryDiskPack(DiskGeometry geometry, string path, bool load) + { + _packName = path; + _geometry = geometry; + _sectors = new DiskSector[_geometry.Cylinders, _geometry.Heads, _geometry.Sectors]; + + if (load) + { + // + // Attempt to load in the specified image file. + // + using (FileStream imageStream = new FileStream(_packName, FileMode.Open, FileAccess.Read)) + { + try + { + for (int cylinder = 0; cylinder < _geometry.Cylinders; cylinder++) + { + for (int head = 0; head < _geometry.Heads; head++) + { + for (int sector = 0; sector < _geometry.Sectors; sector++) + { + _sectors[cylinder, head, sector] = new DiskSector(_geometry.SectorGeometry, imageStream, cylinder, head, sector); + } + } + } + + if (imageStream.Position != imageStream.Length) + { + throw new InvalidOperationException("Extra data at end of image file."); + } + } + finally + { + imageStream.Close(); + } + } + } + else + { + // + // Just initialize a new, empty disk. + // + for (int cylinder = 0; cylinder < _geometry.Cylinders; cylinder++) + { + for (int head = 0; head < _geometry.Heads; head++) + { + for (int sector = 0; sector < _geometry.Sectors; sector++) + { + _sectors[cylinder, head, sector] = new DiskSector(_geometry.SectorGeometry, cylinder, head, sector); + } + } + } + } + } + + public DiskGeometry Geometry + { + get { return _geometry; } + } + + public string PackName + { + get { return _packName; } + } + + /// + /// Commits the current in-memory image back to the file from which it was loaded. + /// + public void Save() + { + using (FileStream imageStream = new FileStream(_packName, FileMode.Create, FileAccess.Write)) + { + try + { + for (int cylinder = 0; cylinder < _geometry.Cylinders; cylinder++) + { + for (int head = 0; head < _geometry.Heads; head++) + { + for (int sector = 0; sector < _geometry.Sectors; sector++) + { + _sectors[cylinder, head, sector].WriteToStream(imageStream); + } + } + } + } + finally + { + imageStream.Close(); + } + } + } + + public DiskSector GetSector(int cylinder, int head, int sector) + { + // + // Return the in-memory sector reference. + // + return _sectors[cylinder, head, sector]; + } + + public void CommitSector(DiskSector sector) + { + // + // Update the in-memory sector reference to point to this (possibly new) sector object. + // + if (sector.Modified) + { + _sectors[sector.Cylinder, sector.Head, sector.Sector] = sector; + } + } + + private string _packName; // The file from whence the data came + private DiskSector[,,] _sectors; // All of the sectors on disk + private DiskGeometry _geometry; // The geometry of this disk. + } + + /// + /// FileBackedDiskPack provides an implementation of IDiskPack where sectors are read into memory + /// only when requested, and changes are flushed to disk when use of the sector is complete. + /// + public class FileBackedDiskPack : IDiskPack + { + /// + /// Creates a new, empty disk pack with the specified geometry. + /// + /// + /// + public static FileBackedDiskPack CreateEmpty(DiskGeometry geometry, string path) + { + return new FileBackedDiskPack(geometry, path, false); + } + + /// + /// Loads an existing image. + /// + /// + /// + /// + public static FileBackedDiskPack Load(DiskGeometry geometry, string path) + { + return new FileBackedDiskPack(geometry, path, true); + } + + public void Dispose() + { + if (_diskStream != null) + { + _diskStream.Close(); + } + } + + private FileBackedDiskPack(DiskGeometry geometry, string path, bool load) + { + _packName = path; + _geometry = geometry; + + if (load) + { + // + // Attempt to open an existing stream for read/write access. + // + _diskStream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite); + + // + // Quick sanity check that the disk image is the right size. + // + if (_diskStream.Length != geometry.GetDiskSizeBytes()) + { + _diskStream.Close(); + _diskStream = null; + throw new InvalidOperationException("Image size is invalid."); + } + } + else + { + // + // Attempt to initialize a new stream with read/write access. + // + _diskStream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite); + + // + // And set the size of the stream. + // + _diskStream.SetLength(geometry.GetDiskSizeBytes()); + } + } + + public DiskGeometry Geometry + { + get { return _geometry; } + } + + public string PackName + { + get { return _packName; } + } + + /// + /// Commits pending changes back to disc. + /// + /// + public void Save() + { + // Nothing here, we expect CommitSector + // to be called by whoever has a sector checked out before shutdown. + } + + public DiskSector GetSector(int cylinder, int head, int sector) + { + // + // Retrieve the appropriate sector from disk. + // Seek to the appropriate position and read. + // + _diskStream.Position = GetOffsetForSector(cylinder, head, sector); + + return new DiskSector(_geometry.SectorGeometry, _diskStream, cylinder, head, sector); + } + + public void CommitSector(DiskSector sector) + { + if (sector.Modified) + { + // + // Commit this data back to disk. + // Seek to the appropriate position and flush. + // + _diskStream.Position = GetOffsetForSector(sector.Cylinder, sector.Head, sector.Sector); + sector.WriteToStream(_diskStream); + } + } + + private long GetOffsetForSector(int cylinder, int head, int sector) + { + int sectorNumber = (cylinder * _geometry.Heads * _geometry.Sectors) + + (head * _geometry.Sectors) + + sector; + + return sectorNumber * _geometry.SectorGeometry.GetSectorSizeBytes(); + } + + private string _packName; // The file from whence the data came + private FileStream _diskStream; // The disk image stream containing this disk's contents. + private DiskGeometry _geometry; // The geometry of this disk. + } +} diff --git a/Contralto/IO/TridentController.cs b/Contralto/IO/TridentController.cs new file mode 100644 index 0000000..f2f2504 --- /dev/null +++ b/Contralto/IO/TridentController.cs @@ -0,0 +1,1123 @@ +using Contralto.CPU; +using Contralto.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Contralto.IO +{ + /// + /// Implements the Alto's Trident disk controller ("Tricon"). + /// This can talk to up to 8 (technically 16, but the software doesn't + /// appear to support more than 8) T-80 (80mb) or T-300 (300mb) drives. + /// + /// TODO: This implementation is preliminary and still experimental. + /// There are a few major issues: + /// - The state machine for the Output FIFO is a tangled mess. + /// - TFU refuses to talk to more than drive 0. Unsure if this is + /// a bug in the Trident emulation, a bug elsewhere in Contralto + /// or user error. TriEx seems to be fine. + /// - TriEx gets many errors of status "00000" (meaning that TriEx + /// didn't get a response from the controller when it expected to.) + /// TFU works fine and can certify, erase, and exercise packs all day. + /// The TriEx issue seems to be related in some way to the timing of + /// the output FIFO. Probably a subtle issue with microcode wakeups. + /// Untangle the output FIFO state machine and revisit this. + /// - There is at this time no way to toggle the Read Only switch + /// on the emulated drive. (All drives are read/write). + /// - The Trident sector wakeup signal needs to go away when the TriCon + /// is put to sleep (for performance reasons, no sense keeping the + /// scheduler busy for no reason.) + /// + public class TridentController + { + public TridentController(AltoSystem system) + { + _system = system; + + _drives = new TridentDrive[16]; + + for(int i=0;i<_drives.Length;i++) + { + _drives[i] = new TridentDrive(system); + } + + Reset(); + } + + public void Reset() + { + _runEnable = false; + + _seekIncomplete = false; + _headOverflow = false; + _deviceCheck = false; + _notSelected = false; + _notOnline = false; + _notReady = false; + _sectorOverflow = false; + _outputLate = false; + _inputLate = false; + _compareError = false; + _readOnly = false; + _offset = false; + + _selectedDrive = 0; + + _sector = 0; + _checkDone = false; + _empty = false; + _pauseOutputProcessing = false; + _readState = ReadState.Idle; + _writeState = WriteState.Idle; + + _outputFifo = new Queue(); + _inputFifo = new Queue(); + + _sectorEvent = new Event(0, null, SectorCallback); + _outputFifoEvent = new Event(0, null, OutputFifoCallback); + _seekEvent = new Event(0, null, SeekCallback); + _readWordEvent = new Event(0, null, ReadWordCallback); + + // And schedule the first sector pulse. + _system.Scheduler.Schedule(_sectorEvent); + } + + /// + /// "RESET INPUT FIFO AND CLEAR ERRORS" + /// + public void ControllerReset() + { + _seekIncomplete = false; + _headOverflow = false; + _deviceCheck = false; + _notSelected = false; + _notOnline = false; + _notReady = false; + _sectorOverflow = false; + _outputLate = false; + _inputLate = false; + _compareError = false; + _readOnly = false; + _offset = false; + + _writeState = WriteState.Idle; + _readState = ReadState.Idle; + + _empty = false; + //_pauseOutputProcessing = false; + + ClearInputFIFO(); + } + + public void CommitDisk(int drive) + { + TridentDrive d = _drives[drive]; + if (d.IsLoaded) + { + try + { + d.Pack.Save(); + } + catch (Exception e) + { + // TODO: this does not really belong here. + System.Windows.Forms.MessageBox.Show(String.Format("Unable to save Trident disk {0}'s contents. Error {1}. Any changes have been lost.", drive, e.Message), "Disk save error"); + } + } + + } + + public TridentDrive[] Drives + { + get { return _drives; } + } + + public void STARTF(ushort value) + { + // + // "An SIO with bit 10 set will cause Run-Enable to be set. + // An SIO with bit 11 set to one will cause Run-Enable to be + // reset." + // + if ((value & 0x10) != 0) + { + _runEnable = false; + _system.CPU.BlockTask(TaskType.TridentInput); + _system.CPU.BlockTask(TaskType.TridentOutput); + + //ClearOutputFIFO(); + //ClearInputFIFO(); + } + + if ((value & 0x20) != 0) + { + _runEnable = true; + + // "Issuing an SIO with bit 10 set will wake up the microcode + // once and thus may report status in the absence of sector + // pulses from the disk." + // So do that now. + // Based on a reading of the microcode and schematics it looks like the + // Write (Output) task is woken up here. + + // + // Clear error flags. + // + _inputLate = false; + _outputLate = false; + _sectorOverflow = false; + + // _empty = false; + // _pauseOutputProcessing = false; + + // + // Clear the output FIFO + ClearOutputFIFO(); + ClearInputFIFO(); + + //_system.CPU.WakeupTask(TaskType.TridentInput); + _system.CPU.WakeupTask(TaskType.TridentOutput); + + //_system.CPU.BlockTask(TaskType.TridentInput); + //_system.CPU.BlockTask(TaskType.TridentOutput); + + } + + Log.Write(LogComponent.TridentController, "Trident STARTF {0}", Conversion.ToOctal(value)); + } + + public void Stop() + { + _runEnable = false; + _system.CPU.BlockTask(TaskType.TridentInput); + _system.CPU.BlockTask(TaskType.TridentOutput); + } + + public ushort KDTA + { + get + { + Log.Write(LogComponent.TridentController, "Trident KDTA read"); + return DequeueInputFIFO(); + } + + set + { + Log.Write(LogComponent.TridentController, "------> Trident KDTA queued {0}", Conversion.ToOctal(value)); + WriteOutputFifo(value); + } + } + + public ushort STATUS + { + get + { + // + // The status bits from the controller are: + // 0 - Seek Incomplete : drive was unable to correctly position the heads, a Rezero + // must be issued to clear this error. + // 1 - Head Overflow : head address given to the drive is invalid. + // 2 - Device check : One of the following errors occurred + // a) Head select or Cylinder select or Write commands and disk not ready + // b) An illegal cylinder address. + // c) Offset active and cylinder select command. + // d) Read-Only and write + // e) Certain errors during writing -- multiple heads selected, various read errors) + // 3 - Not selected : The selected drive is off-line or not powered up. + // 4 - Not Online : The drive is in test mode or the heads are not loaded. + // 5 - Not Ready : There is a cylinder seek in progress or the heads are not loaded + // 6 - Sector Overflow : The controller detected a write command was active when the next + // sector pulse occurred. + // 7 - Output Late : The 16 word output buffer became empty while a read or write command + // was in progress. + // 8 - Input Late : The 16 word input buffer became full. + // 9 - Compare Error : The data read during a "Read and Compare" operation did not match + // the data read off the disk. + // 10 - Read Only : The "Read-Only" switch on the disk drive is on. + // 11 - Offset : The cylinder position is currently offset. This is a mode used for + // recovery of bad data. (NOTE: Not emulated.) + // 12-15 Sector count : A value from 0 to count-1, where count is the number of sectors + // implemented in the disk drive. The value returned here is the sector + // count for the *next sector* on the disk. + + ushort status = (ushort)( + (_seekIncomplete ? 0x8000 : 0) | + (_headOverflow ? 0x4000 : 0) | + (_deviceCheck ? 0x2000 : 0) | + (_notSelected ? 0x1000 : 0) | + (_notOnline ? 0x0800 : 0) | + (NotReady() ? 0x0400 : 0) | + (_sectorOverflow ? 0x0200 : 0) | + (_outputLate ? 0x0100 : 0) | + (_inputLate ? 0x0080 : 0) | + (_compareError ? 0x0040 : 0) | + (SelectedDrive.ReadOnly ? 0x0020 : 0) | + (_offset ? 0x0010 : 0) | + ((_sector) & 0x000f)); + + Log.Write(LogComponent.TridentController, "STATUS word is {0}", Conversion.ToOctal(status)); + return status; + } + } + + public void TagInstruction(ushort tag) + { + Log.Write(LogComponent.TridentController, "------> Trident tag instruction queued {0}", Conversion.ToOctal(tag)); + + // + // Add tag to the output FIFO, with bit 16 set, identifying it as a tag. + // + WriteOutputFifo(0x10000 | tag); + } + + public void WaitForEmpty() + { + // Block the Output task; it will be awoken when the output FIFO is emptied. + if (_outputFifo.Count > 0) + { + _system.CPU.BlockTask(TaskType.TridentOutput); + _empty = true; + Log.Write(LogComponent.TridentController, "Trident output task blocked until Output FIFO emptied."); + } + } + + private bool NotReady() + { + return _notReady | !SelectedDrive.IsLoaded; + } + + private void WriteOutputFifo(int value) + { + if (_outputFifo.Count > 16) + { + Log.Write(LogType.Error, LogComponent.TridentController, "Output FIFO full, dropping word."); + return; + } + + EnqueueOutputFIFO(value); + + // + // If this is the first word written to an empty FIFO, and we aren't waiting for the next + // sector pulse, queue up the fifo callback to pull words and process them. + // + if (!_pauseOutputProcessing && _outputFifo.Count == 1) + { + _outputFifoEvent.TimestampNsec = _outputFifoDuration; + _system.Scheduler.Schedule(_outputFifoEvent); + } + } + + private void OutputFifoCallback(ulong timeNsec, ulong skewNsec, object context) + { + ProcessCommandOutput(); + } + + private void ProcessCommandOutput() + { + if (_outputFifo.Count == 0) + { + if (_writeState == WriteState.Writing) + { + // + // We have run out of data on a write. + // + _outputLate = true; + + Log.Write(LogComponent.TridentController, "Output FIFO underflowed."); + } + + return; + } + + if (_readState != ReadState.Reading) + { + int fifoWord = DequeueOutputFIFO(); + + if ((fifoWord & 0x10000) != 0) + { + // + // This is a Tag word; process it accordingly. + // + Log.Write(LogComponent.TridentController, "Tag word {0} pulled from Output FIFO.", Conversion.ToOctal((ushort)fifoWord)); + + // + // From the TRICON schematic (page 16): + // Bit 3 is the Enable bit, which enables selection of one of four commands + // to the drive, specified in bits 1 and 2: + // (and as usual, these are in the backwards Nova/Alto bit ordering scheme) + // Bits 1 and 2 decode to: + // 0 0 - Control + // 0 1 - Set Head + // 1 0 - Set Cylinder + // 1 1 - Set Drive + // + // Head, Cylinder, Drive are straightforward -- the lower bits of the tag + // word contain the data for the command. + // + // The Control bits are described in the Trident T300 Theory of Operations manual, + // Page 3-13 and are the lower 10 bits of the tag command: + // + // 0 - Strobe Late : Skews read data detection 4ns late for attempted read-error recovery. + // 1 - Strobe Early : Same as above, but early. + // 2 - Write : Turns on circuits to write data, + // 3 - Read : Turns on read circuits an resets Attention interrupts. + // 4 - Address Mark : Commands an address mark to be generated, if writing; or + // enables the address mark detector, if reading. + // 5 - Reset Head Register : Resets HAR to 0 + // 6 - Device Check Reset : Resets most types of Device Check errors unless an error + // condition is still present. + // 7 - Head Select : Tuns on the head-selection circuits. Head select must be active 5 + // or 15 microseconds before Write or Read is commanded, respectively. + // 8 - Rezero : Repositions the heads to cylinder 000, selects Head Address 0, and resets + // some types of Device Checks. + // 9 - Head Advance : Increases Head Address count by one. + // + // + // Bit 0 of the Tag word, if set, tells the controller to hold off Output FIFO processing + // until the next sector pulse. + // + if ((fifoWord & 0x8000) != 0) + { + _pauseOutputProcessing = true; + Log.Write(LogComponent.TridentController, "Output FIFO processing paused until next sector pulse."); + } + // See if the enable bit (3) is set, in which case this is a command to the drive + // + if ((fifoWord & 0x1000) != 0) + { + // + // Switch on the specific command + switch ((fifoWord & 0x6000) >> 13) + { + case 0: // Control + Log.Write(LogComponent.TridentController, "Control word."); + + if ((fifoWord & 0x0001) != 0) // 9 - Head advance + { + if (!SelectedDrive.IsLoaded) + { + _deviceCheck = true; + } + else + { + if (SelectedDrive.Head + 1 >= SelectedDrive.Pack.Geometry.Heads) + { + _headOverflow = true; + _deviceCheck = true; + + Log.Write(LogComponent.TridentController, "Head {0} is out of range on Head Advance.", SelectedDrive.Head + 1); + } + else + { + SelectedDrive.Head++; + Log.Write(LogComponent.TridentController, "Control: Head Advance. Head is now {0}", SelectedDrive.Head); + } + } + } + + if ((fifoWord & 0x0002) != 0) // 8 - Rezero + { + _deviceCheck = false; + SelectedDrive.Head = 0; + + InitSeek(0); + + Log.Write(LogComponent.TridentController, "Control: Rezero."); + } + + if ((fifoWord & 0x0004) != 0) // 7 - Head select + { + Log.Write(LogComponent.TridentController, "Control: Head Select."); + + if (!SelectedDrive.IsLoaded) + { + _deviceCheck = true; + } + + // TODO: technically this needs to be active before a write or read is selected. Do I care? + } + + if ((fifoWord & 0x0008) != 0) // 6 - Device Check Reset + { + Log.Write(LogComponent.TridentController, "Control: Device Check Reset."); + _deviceCheck = false; + } + + if ((fifoWord & 0x0010) != 0) // 5 - Reset Head Register + { + Log.Write(LogComponent.TridentController, "Control: Reset Head Register."); + SelectedDrive.Head = 0; + } + + if ((fifoWord & 0x0020) != 0) // 4 - Address Mark + { + Log.Write(LogComponent.TridentController, "Control: Address mark."); + + // Not much to do here, emulation-wise. + } + + if ((fifoWord & 0x0040) != 0) // 3 - Read + { + Log.Write(LogComponent.TridentController, "Control: Read."); + // + // Commence reading -- start reading a word at a time into the input FIFO, + // Waking up the Input task as necessary. + // + if (NotReady()) + { + _deviceCheck = true; + } + else + { + InitRead(); + } + } + + if ((fifoWord & 0x0080) != 0) // 2 - Write + { + Log.Write(LogComponent.TridentController, "Control: Write."); + + // + // Commence writing -- start pulling a word at a time out of the output FIFO, + // Waking up the Output task as necessary. + // + if (NotReady()) + { + _deviceCheck = true; + } + else + { + InitWrite(); + } + } + + if ((fifoWord & 0x0100) != 0) // 1 - Strobe Early + { + Log.Write(LogComponent.TridentController, "Control: Strobe Early."); + + // Not going to emulate this, as fun as it sounds. + } + + if ((fifoWord & 0x0200) != 0) // 0 - Strobe Late + { + Log.Write(LogComponent.TridentController, "Control: Strobe Late."); + + // Not going to emulate this either. + } + + break; + + case 1: // Set Head + int head = fifoWord & 0x1f; // low 5 bits + Log.Write(LogComponent.TridentController, "Command is Set Head {0}", head); + + if (!SelectedDrive.IsLoaded) + { + _deviceCheck = true; + } + else + { + if (head >= SelectedDrive.Pack.Geometry.Heads) + { + _headOverflow = true; + _deviceCheck = true; + + Log.Write(LogComponent.TridentController, "Head {0} is out of range.", head); + } + else + { + SelectedDrive.Head = head; + } + } + break; + + case 2: // Set Cylinder + int cyl = fifoWord & 0x3ff; // low 10 bits + Log.Write(LogComponent.TridentController, "Command is Set Cylinder {0}.", cyl); + + if (NotReady()) + { + _deviceCheck = true; + } + else + { + InitSeek(cyl); + } + break; + + case 3: // Set Drive + _selectedDrive = fifoWord & 0xf; + Log.Write(LogComponent.TridentController, "Command is Set Drive {0}", _selectedDrive); + break; + } + } + else + { + // + // There are two values we expect to see with the enable bit off: + // 0 is sent to reset the tag bus (to the drive) to zeros, we simply ignore it. + // 2 is sent because the drive expects the Rezero signal to be held for a time + // after the Control signal is dropped. We will commence a Rezero operation + // if we get one here. + // + if (fifoWord == 2) + { + Log.Write(LogComponent.TridentController, "Rezero started."); + } + else if (fifoWord != 0) + { + Log.Write(LogComponent.TridentController, "Unexpected Tag word without enable bit set: {0}", Conversion.ToOctal(fifoWord)); + } + } + } + else + { + // + // This is a data word. + // + Log.Write(LogComponent.TridentController, "Data word {0} pulled from Output FIFO.", Conversion.ToOctal((ushort)fifoWord)); + + if (_writeState != WriteState.Idle) + { + switch (_writeState) + { + case WriteState.Initialized: + // This word should be the MAX number of words to check. + _writeWordCount = fifoWord - 1; + _writeState = WriteState.WaitingForStartBit; + Log.Write(LogComponent.TridentController, "Write initializing {0} words (max) to write.", _writeWordCount); + break; + + case WriteState.WaitingForStartBit: + if (fifoWord == 1) + { + Log.Write(LogComponent.TridentController, "Start bit recognized, commencing write operation."); + _writeState = WriteState.Writing; + } + break; + + case WriteState.Writing: + ProcessDiskWrite(fifoWord); + break; + } + } + + if (_readState != ReadState.Idle) + { + switch (_readState) + { + case ReadState.Initialized: + _readWordCount = fifoWord - 1; + _readState = ReadState.Reading; + + Log.Write(LogType.Verbose, LogComponent.TridentController, "Read initializing {0} words (max) to read.", _readWordCount); + break; + + default: + throw new InvalidOperationException("Unexpected ReadState in ProcessCommandOutput."); + + } + } + } + } + + if (!_pauseOutputProcessing && _outputFifo.Count > 0) + { + // + // Schedule next FIFO wakeup. + _outputFifoEvent.TimestampNsec = _outputFifoDuration; + _system.Scheduler.Schedule(_outputFifoEvent); + } + } + + private void InitSeek(int destCylinder) + { + if (destCylinder > SelectedDrive.Pack.Geometry.Cylinders - 1) + { + Log.Write(LogType.Error, LogComponent.TridentController, "Specified cylinder {0} is out of range. Seek aborted.", destCylinder); + _deviceCheck = true; + return; + } + + int currentCylinder = SelectedDrive.Cylinder; + if (destCylinder != currentCylinder) + { + // Do a seek. + _notReady = true; + + _destCylinder = destCylinder; + + // + // I can't find a specific formula for seek timing; the Century manual says: + // "Positioning time for seeking to the next cylinder is normally 6ms, and + // for full seeks (814 cylinder differerence) it is 55ms." + // + // I'm just going to fudge this for now and assume a linear ramp; this is not + // accurate but it's not all that important. + // + _seekDuration = (ulong)((6.0 + 0.602 * Math.Abs(currentCylinder - destCylinder)) * Conversion.MsecToNsec); + + Log.Write(LogComponent.TridentController, "Commencing seek from {0} to {1}. Seek time is {2}ns", destCylinder, currentCylinder, _seekDuration); + + _seekEvent.TimestampNsec = _seekDuration; + _system.Scheduler.Schedule(_seekEvent); + } + else + { + Log.Write(LogComponent.TridentController, "Seek is a no-op."); + } + } + + private void SeekCallback(ulong timeNsec, ulong skewNsec, object context) + { + Log.Write(LogComponent.TridentDisk, "Seek to {0} complete.", _destCylinder); + + SelectedDrive.Cylinder = _destCylinder; + _notReady = false; + } + + private void InitRead() + { + if (_readState == ReadState.Idle) + { + _readState = ReadState.Initialized; + _checkedWordCount = 0; + _readIndex = 0; + _readWordCount = 0; + _checkDone = false; + + // Clear the FIFO: TODO check schematics. + // ClearInputFIFO(); + + _readWordEvent.TimestampNsec = _readWordDuration * 10; + _system.Scheduler.Schedule(_readWordEvent); + } + else + { + // Unexpected, throw for now. + throw new InvalidOperationException("Unexpected Read command while read active."); + } + } + + private void ReadWordCallback(ulong timeNsec, ulong skewNsec, object context) + { + if (_readWordCount > 0) + { + // Enqueue data from disk. + ushort dataWord; + + switch (_sectorBlock) + { + case SectorBlock.Header: + dataWord = SelectedDrive.ReadHeader(_readIndex); + break; + + case SectorBlock.Label: + dataWord = SelectedDrive.ReadLabel(_readIndex); + break; + + case SectorBlock.Data: + dataWord = SelectedDrive.ReadData(_readIndex); + break; + + default: + throw new InvalidOperationException(String.Format("Unexpected Sector Block of {0} on read.", _sectorBlock)); + } + + Log.Write(LogType.Verbose, LogComponent.TridentController, "Read word {0}:{1}", _readIndex, Conversion.ToOctal(dataWord)); + + EnqueueInputFIFO(dataWord); + + // + // Compare data with check data in output fifo, if any. + // From the microcode comments: + // "Note that the first two words of a block are always checked, + // followed by additional words until a zero word or the end of the block." + // + // If we hit a tag word, the check is also completed. + // TODO: verify this w/schematic & microcode. + + if (_outputFifo.Count > 0) + { + int checkWord = _outputFifo.Peek(); + if ((checkWord & 0x10000) == 0 && (!_checkDone || _checkedWordCount < 2)) + { + // Actually pull the word off + checkWord = (ushort)DequeueOutputFIFO(); + + Log.Write(LogType.Verbose, LogComponent.TridentController, "Pulled checkword {0} from output FIFO", Conversion.ToOctal(checkWord)); + + // A zero word indicates the check is complete (we will + // still read the minimum two words in) + if (checkWord == 0) + { + _checkDone = true; + } + // Compare didn't. + else if (checkWord != dataWord) + { + _compareError = true; + } + _checkedWordCount++; + } + } + } + else + { + // Last four words (0 through -3) are checksum and ECC words. + // The checksum words are ignored by the microcode, and + // since we're not simulating faulty disks, these are always zero. + EnqueueInputFIFO(0); + Log.Write(LogComponent.TridentController, "Read ECC/checksum word 0"); + } + + _readIndex++; + _readWordCount--; + + if (_readWordCount > -4) + { + // More words to read, queue up the next. + _readWordEvent.TimestampNsec = _readWordDuration; + _system.Scheduler.Schedule(_readWordEvent); + } + else + { + Log.Write(LogComponent.TridentController, "CHS {0}/{1}/{2} {3} read complete.", SelectedDrive.Cylinder, SelectedDrive.Head, _sector, _sectorBlock); + _readState = ReadState.Idle; + + _sectorBlock++; + } + } + + private void InitWrite() + { + if (_writeState == WriteState.Idle) + { + Log.Write(LogComponent.TridentController, "Write primed, waiting for start bit."); + _writeState = WriteState.Initialized; + _writeIndex = 0; + } + else + { + // Unexpected, throw for now. + throw new InvalidOperationException("Unexpected Write command while write active."); + } + } + + private void ProcessDiskWrite(int dataWord) + { + // + // Sanity check: if there's a tag bit set, we've picked up some + // invalid data... + // + if ((dataWord & 0x10000) != 0) + { + Log.Write(LogType.Error, LogComponent.TridentController, "Tag bit set in data word during Write ({0}).", Conversion.ToOctal(dataWord)); + } + + Log.Write(LogComponent.TridentController, "Write word {0}:{1}", _writeIndex, Conversion.ToOctal(dataWord)); + + // + // Commit to the proper block in the sector: + // + switch(_sectorBlock) + { + case SectorBlock.Header: + SelectedDrive.WriteHeader(_writeIndex, (ushort)dataWord); + break; + + case SectorBlock.Label: + SelectedDrive.WriteLabel(_writeIndex, (ushort)dataWord); + break; + + case SectorBlock.Data: + SelectedDrive.WriteData(_writeIndex, (ushort)dataWord); + break; + + default: + throw new InvalidOperationException(String.Format("Unexpected Sector Block of {0} on write.", _sectorBlock)); + } + + _writeIndex++; + + _writeWordCount--; + if (_writeWordCount <= 0) + { + Log.Write(LogComponent.TridentController, "CHS {0}/{1}/{2} {3} write complete. {4} words written.", SelectedDrive.Cylinder, SelectedDrive.Head, _sector, _sectorBlock, _writeIndex); + _writeState = WriteState.Idle; + _writeIndex = 0; + + // Move to the next block + _sectorBlock++; + } + } + + private void SectorCallback(ulong timeNsec, ulong skewNsec, object context) + { + // Move to the next sector. + _sector = (_sector + 1) % 9; + SelectedDrive.Sector = _sector; + + // Reset to the first block (header) in the sector. + _sectorBlock = SectorBlock.Header; + + // Wake up the Output task if RunEnable is true and the drive is ready. + if (_runEnable && !NotReady()) + { + _system.CPU.WakeupTask(TaskType.TridentOutput); + } + + // + // Schedule the next sector pulse + // + _sectorEvent.TimestampNsec = _sectorDuration - skewNsec; + _system.Scheduler.Schedule(_sectorEvent); + + // + // If output fifo processing was paused and there's data in the FIFO + // to be dealt with, wake up the callback now. + // + if (_pauseOutputProcessing && _outputFifo.Count > 0) + { + _pauseOutputProcessing = false; + + _outputFifoEvent.TimestampNsec = _outputFifoDuration; + _system.Scheduler.Schedule(_outputFifoEvent); + } + } + + private TridentDrive SelectedDrive + { + get { return _drives[_selectedDrive]; } + } + + // + // Input FIFO semantics + // + private void ClearInputFIFO() + { + _inputFifo.Clear(); + _system.CPU.BlockTask(TaskType.TridentInput); + } + + private void EnqueueInputFIFO(ushort word) + { + if (_inputFifo.Count == 16) + { + // We have overflowed the input FIFO. Set the requisite error + // flags and shut this thing down. + _inputLate = true; + + Log.Write(LogComponent.TridentController, "Input FIFO overflowed."); + } + else + { + _inputFifo.Enqueue(word); + + if (_inputFifo.Count >= 4) + { + _system.CPU.WakeupTask(TaskType.TridentInput); + } + else + { + _system.CPU.BlockTask(TaskType.TridentInput); + } + } + } + + private ushort DequeueInputFIFO() + { + ushort word = 0; + if (_inputFifo.Count == 0) + { + Log.Write(LogType.Warning, LogComponent.TridentController, "Input FIFO underflowed, returning 0."); + } + else + { + word = _inputFifo.Dequeue(); + } + + if (_inputFifo.Count < 4) + { + _system.CPU.BlockTask(TaskType.TridentInput); + } + else + { + _system.CPU.WakeupTask(TaskType.TridentInput); + } + return word; + } + + // + // Output FIFO semantics + // + private void ClearOutputFIFO() + { + _outputFifo.Clear(); + _system.CPU.WakeupTask(TaskType.TridentOutput); + } + + private void EnqueueOutputFIFO(int word) + { + if (_outputFifo.Count == 16) + { + Log.Write(LogComponent.TridentController, "Output FIFO overflowed, dropping word."); + } + else + { + _outputFifo.Enqueue(word); + + Log.Write(LogComponent.TridentController, "Output FIFO enqueued, queue depth is now {0}", _outputFifo.Count); + } + + if (_outputFifo.Count <= 12) + { + _system.CPU.WakeupTask(TaskType.TridentOutput); + } + else + { + _system.CPU.BlockTask(TaskType.TridentOutput); + } + + } + + private int DequeueOutputFIFO() + { + int word = 0; + if (_outputFifo.Count == 0) + { + Log.Write(LogType.Warning, LogComponent.TridentController, "Output FIFO underflowed, returning 0."); + } + else + { + word = _outputFifo.Dequeue(); + Log.Write(LogComponent.TridentController, "Output FIFO dequeued, queue depth is now {0}", _outputFifo.Count); + } + + if (_empty) + { + // Only wake up the Output task when the output FIFO goes completely empty. + if (_outputFifo.Count == 0) + { + _empty = false; + _system.CPU.WakeupTask(TaskType.TridentOutput); + Log.Write(LogComponent.TridentController, "Output FIFO emptied, waking Output task."); + } + else + { + _system.CPU.BlockTask(TaskType.TridentOutput); + } + } + else + { + if (_outputFifo.Count <= 12) + { + _system.CPU.WakeupTask(TaskType.TridentOutput); + } + else + { + _system.CPU.BlockTask(TaskType.TridentOutput); + } + } + return word; + } + + // Wakeup signals + private bool _runEnable; + + // Status bits + private bool _seekIncomplete; + private bool _headOverflow; + private bool _deviceCheck; + private bool _notSelected; + private bool _notOnline; + private bool _notReady; + private bool _sectorOverflow; + private bool _outputLate; + private bool _inputLate; + private bool _compareError; + private bool _readOnly; + private bool _offset; + + // Current sector + private int _sector; + + /// + /// Drive timings + /// + private static ulong _sectorDuration = (ulong)((16.66666 / 9.0) * Conversion.MsecToNsec); // time in nsec for one sector -- 9 sectors, 16.66ms per rotation + private Event _sectorEvent; + + // + // Seek timings + // + private static ulong _seekDuration = (6 * Conversion.MsecToNsec); + private Event _seekEvent; + private int _destCylinder; + + // Output FIFO. This is actually 17-bits wide (the 17th bit is the Tag bit). + private Queue _outputFifo; + private Event _outputFifoEvent; + private bool _pauseOutputProcessing; + private bool _empty; + private static ulong _outputFifoDuration = (ulong)(_sectorDuration / 2048.0); // time in nsec for one word -- 1120 words per sector. + + // Input FIFO. + private Queue _inputFifo; + + // + // Read timings + // + private static ulong _readWordDuration = (ulong)(_sectorDuration / 1120.0); // time in nsec for one word -- 1120 words per sector. + private Event _readWordEvent; + private int _readIndex; + private int _readWordCount; + private bool _checkDone; + private int _checkedWordCount; + + // + // Write timings + // + private static ulong _writeWordDuration = (ulong)(_sectorDuration / 1120.0); // time in nsec for one word -- 1120 words per sector. + private int _writeIndex; + private int _writeWordCount; + + private enum WriteState + { + Idle, + Initialized, + WaitingForStartBit, + Writing, + } + private WriteState _writeState; + + private enum ReadState + { + Idle, + Initialized, + WaitingForStartBit, + Reading, + } + private ReadState _readState; + + private enum SectorBlock + { + Header, + Label, + Data, + } + private SectorBlock _sectorBlock; + + // + // Attached drives + // + private TridentDrive[] _drives; + private int _selectedDrive; + + private AltoSystem _system; + } +} diff --git a/Contralto/IO/TridentDrive.cs b/Contralto/IO/TridentDrive.cs new file mode 100644 index 0000000..d256cac --- /dev/null +++ b/Contralto/IO/TridentDrive.cs @@ -0,0 +1,284 @@ +/* + This file is part of ContrAlto. + + ContrAlto is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + ContrAlto is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with ContrAlto. If not, see . +*/ + +using Contralto.Logging; +using System; + +namespace Contralto.IO +{ + /// + /// Encapsulates logic that belongs to a Trident drive, including loading/saving packs, + /// seeking, and parceling out sector data. + /// + public class TridentDrive + { + public TridentDrive(AltoSystem system) + { + _system = system; + Reset(); + } + + public void Reset() + { + _sector = 0; + _cylinder = 0; + _head = 0; + + UpdateTrackCache(); + } + + public void NewPack(string path, DiskGeometry geometry) + { + if (_pack != null) + { + UpdateTrackCache(); + _pack.Dispose(); + } + + _pack = FileBackedDiskPack.CreateEmpty(geometry, path); + Reset(); + } + + public void LoadPack(IDiskPack pack) + { + if (_pack != null) + { + UpdateTrackCache(); + _pack.Dispose(); + } + + _pack = pack; + Reset(); + } + + public void UnloadPack() + { + if (_pack != null) + { + UpdateTrackCache(); + _pack.Dispose(); + } + + _pack = null; + Reset(); + } + + public bool IsLoaded + { + get { return _pack != null; } + } + + public IDiskPack Pack + { + get { return _pack; } + } + + public int Sector + { + get { return _sector; } + set { _sector = value; } + } + + public int Head + { + get { return _head; } + set + { + if (_head != value) + { + _head = value; + UpdateTrackCache(); + } + } + } + + public int Cylinder + { + get { return _cylinder; } + set + { + if (_cylinder != value) + { + _cylinder = value; + UpdateTrackCache(); + } + } + } + + public bool ReadOnly + { + get { return false; } + } + + public ushort ReadHeader(int wordOffset) + { + if (wordOffset >= SectorGeometry.Trident.HeaderSize) + { + // + // We just ignore this; the microcode may read extra words + // and the controller was expected to ignore them. + // + Log.Write(LogType.Warning, LogComponent.TridentDisk, "Extra header word read, offset {0}", wordOffset); + return 0; + } + else + { + return CurrentSector.ReadHeader(wordOffset); + } + } + + public ushort ReadLabel(int wordOffset) + { + if (wordOffset >= SectorGeometry.Trident.LabelSize) + { + // + // We just ignore this; the microcode may read extra words + // and the controller was expected to ignore them. + // + Log.Write(LogType.Warning, LogComponent.TridentDisk, "Extra label word read, offset {0}", wordOffset); + return 0; + } + else + { + return CurrentSector.ReadLabel(wordOffset); + } + } + + public ushort ReadData(int wordOffset) + { + if (wordOffset >= SectorGeometry.Trident.DataSize) + { + // + // We just ignore this; the microcode may read extra words + // and the controller was expected to ignore them. + // + Log.Write(LogType.Warning, LogComponent.TridentDisk, "Extra data word read, offset {0 }", wordOffset); + return 0; + } + else + { + return CurrentSector.ReadData(wordOffset); + } + } + + public void WriteHeader(int wordOffset, ushort word) + { + if (wordOffset >= SectorGeometry.Trident.HeaderSize) + { + // + // We just ignore this; the microcode may send extra words + // and the controller was expected to ignore them. + // + Log.Write(LogType.Warning, LogComponent.TridentDisk, "Extra header word ({0}) written, offset {1}", Conversion.ToOctal(word), wordOffset); + } + else + { + CurrentSector.WriteHeader(wordOffset, word); + } + } + + public void WriteLabel(int wordOffset, ushort word) + { + if (wordOffset >= SectorGeometry.Trident.LabelSize) + { + // + // We just ignore this; the microcode may send extra words + // and the controller was expected to ignore them. + // + Log.Write(LogType.Warning, LogComponent.TridentDisk, "Extra label word ({0}) written, offset {1}", Conversion.ToOctal(word), wordOffset); + } + else + { + CurrentSector.WriteLabel(wordOffset, word); + } + } + + public void WriteData(int wordOffset, ushort word) + { + if (wordOffset >= SectorGeometry.Trident.DataSize) + { + // + // We just ignore this; the microcode may send extra words + // and the controller was expected to ignore them. + // + Log.Write(LogType.Warning, LogComponent.TridentDisk, "Extra data word ({0}) written, offset {1}", Conversion.ToOctal(word), wordOffset); + } + else + { + CurrentSector.WriteData(wordOffset, word); + } + } + + // + // Sector management. We load in an entire track's worth of sectors at a time. + // When the head or cylinder changes, UpdateTrackCache must be called. + // + private void UpdateTrackCache() + { + if (_pack != null) + { + if (_trackCache == null) + { + // + // First time through, initialize the cache. + // + _trackCache = new DiskSector[_pack.Geometry.Sectors]; + } + else + { + // + // Commit the sectors back to disk before loading in the new ones. + // + for (int i = 0; i < _trackCache.Length; i++) + { + _pack.CommitSector(_trackCache[i]); + } + } + + // + // Load the new sectors for this track. + // + for (int i = 0; i < _trackCache.Length; i++) + { + _trackCache[i] = _pack.GetSector(_cylinder, _head, i); + } + } + } + + private DiskSector CurrentSector + { + get { return _trackCache[_sector]; } + } + + private AltoSystem _system; + + // + // Current disk position + // + private int _cylinder; + private int _head; + private int _sector; + + // The pack loaded into the drive + IDiskPack _pack; + + // + // The track cache + // + private DiskSector[] _trackCache; + } +} diff --git a/Contralto/Logging/Log.cs b/Contralto/Logging/Log.cs index db43779..744d050 100644 --- a/Contralto/Logging/Log.cs +++ b/Contralto/Logging/Log.cs @@ -49,6 +49,9 @@ namespace Contralto.Logging Organ = 0x20000, Orbit = 0x40000, DoverROS = 0x80000, + TridentTask = 0x100000, + TridentController = 0x200000, + TridentDisk = 0x400000, Debug = 0x40000000, All = 0x7fffffff @@ -77,7 +80,7 @@ namespace Contralto.Logging { _components = Configuration.LogComponents; _type = Configuration.LogTypes; - + _logIndex = 0; } public static LogComponent LogComponents @@ -107,7 +110,8 @@ namespace Contralto.Logging // // My log has something to tell you... // TODO: color based on type, etc. - Console.WriteLine(component.ToString() + ": " + message, args); + Console.WriteLine(_logIndex.ToString() + ": " + component.ToString() + ": " + message, args); + _logIndex++; } } #else @@ -125,5 +129,6 @@ namespace Contralto.Logging private static LogComponent _components; private static LogType _type; + private static long _logIndex; } } diff --git a/Contralto/Program.cs b/Contralto/Program.cs index ca7d4d7..1e35f04 100644 --- a/Contralto/Program.cs +++ b/Contralto/Program.cs @@ -42,16 +42,17 @@ namespace Contralto _system = new AltoSystem(); + // Load disks specified by configuration if (!String.IsNullOrEmpty(Configuration.Drive0Image)) { try { - _system.LoadDrive(0, Configuration.Drive0Image); + _system.LoadDiabloDrive(0, Configuration.Drive0Image, false); } catch(Exception e) { - Console.WriteLine("Could not load image '{0}' for drive 0. Error '{1}'.", Configuration.Drive0Image, e.Message); - _system.UnloadDrive(0); + Console.WriteLine("Could not load image '{0}' for Diablo drive 0. Error '{1}'.", Configuration.Drive0Image, e.Message); + _system.UnloadDiabloDrive(0); } } @@ -59,12 +60,32 @@ namespace Contralto { try { - _system.LoadDrive(1, Configuration.Drive1Image); + _system.LoadDiabloDrive(1, Configuration.Drive1Image, false); } catch (Exception e) { - Console.WriteLine("Could not load image '{0}' for drive 1. Error '{1}'.", Configuration.Drive1Image, e.Message); - _system.UnloadDrive(1); + Console.WriteLine("Could not load image '{0}' for Diablo drive 1. Error '{1}'.", Configuration.Drive1Image, e.Message); + _system.UnloadDiabloDrive(1); + } + } + + + if (Configuration.TridentImages != null) + { + for (int i = 0; i < Math.Min(8, Configuration.TridentImages.Count); i++) + { + try + { + if (!String.IsNullOrWhiteSpace(Configuration.TridentImages[i])) + { + _system.LoadTridentDrive(i, Configuration.TridentImages[i], false); + } + } + catch (Exception e) + { + Console.WriteLine("Could not load image '{0}' for Trident drive {1}. Error '{2}'.", Configuration.TridentImages[i], i, e.Message); + _system.UnloadTridentDrive(i); + } } } @@ -106,13 +127,7 @@ namespace Contralto private static void OnProcessExit(object sender, EventArgs e) { Console.WriteLine("Exiting..."); - - // - // Save disk contents - // - _system.CommitDiskPack(0); - _system.CommitDiskPack(1); - + _system.Shutdown(); // diff --git a/Contralto/Properties/Settings.Designer.cs b/Contralto/Properties/Settings.Designer.cs index f0c06fd..79da709 100644 --- a/Contralto/Properties/Settings.Designer.cs +++ b/Contralto/Properties/Settings.Designer.cs @@ -238,5 +238,28 @@ namespace Contralto.Properties { this["ReversePageOrder"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute(@" + + + + + + + + + + +")] + public global::System.Collections.Specialized.StringCollection TridentImages { + get { + return ((global::System.Collections.Specialized.StringCollection)(this["TridentImages"])); + } + set { + this["TridentImages"] = value; + } + } } } diff --git a/Contralto/Properties/Settings.settings b/Contralto/Properties/Settings.settings index 6ce2097..642c663 100644 --- a/Contralto/Properties/Settings.settings +++ b/Contralto/Properties/Settings.settings @@ -56,5 +56,19 @@ True + + <?xml version="1.0" encoding="utf-16"?> +<ArrayOfString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <string /> + <string /> + <string /> + <string /> + <string /> + <string /> + <string /> + <string /> + <string /> +</ArrayOfString> + \ No newline at end of file diff --git a/Contralto/Scheduler.cs b/Contralto/Scheduler.cs index 568ede9..86f26e2 100644 --- a/Contralto/Scheduler.cs +++ b/Contralto/Scheduler.cs @@ -15,6 +15,7 @@ along with ContrAlto. If not, see . */ +using System; using System.Collections.Generic; @@ -120,6 +121,13 @@ namespace Contralto /// public Event Schedule(Event e) { +#if DEBUG + if (_schedule.Contains(e)) + { + throw new InvalidOperationException("Can't do that, time will bend."); + } +#endif + e.TimestampNsec += _currentTimeNsec; _schedule.Push(e); @@ -163,6 +171,11 @@ namespace Contralto } } + public bool Contains(Event e) + { + return _queue.Contains(e); + } + public void Push(Event e) { // Degenerate case: list is empty or new entry is earlier than the head of the list. diff --git a/Contralto/SdlUI/SdlConsole.cs b/Contralto/SdlUI/SdlConsole.cs index 3afc195..f1f7d85 100644 --- a/Contralto/SdlUI/SdlConsole.cs +++ b/Contralto/SdlUI/SdlConsole.cs @@ -187,11 +187,8 @@ namespace Contralto.SdlUI throw new InvalidOperationException("Drive specification out of range."); } - // Save current drive contents. - _system.CommitDiskPack(drive); - // Load the new pack. - _system.LoadDrive(drive, path); + _system.LoadDiabloDrive(drive, path, false); Console.WriteLine("Drive {0} loaded.", drive); return CommandResult.Normal; @@ -205,11 +202,8 @@ namespace Contralto.SdlUI throw new InvalidOperationException("Drive specification out of range."); } - // Save current drive contents. - _system.CommitDiskPack(drive); - // Unload the current pack. - _system.UnloadDrive(drive); + _system.UnloadDiabloDrive(drive); Console.WriteLine("Drive {0} unloaded.", drive); return CommandResult.Normal; @@ -236,7 +230,60 @@ namespace Contralto.SdlUI } return CommandResult.Normal; - } + } + + [DebuggerFunction("load trident", "Loads the specified trident drive with the requested disk image.", " ")] + private CommandResult LoadTrident(ushort drive, string path) + { + if (drive > 7) + { + throw new InvalidOperationException("Drive specification out of range."); + } + + // Load the new pack. + _system.LoadTridentDrive(drive, path, false); + Console.WriteLine("Trident {0} loaded.", drive); + + return CommandResult.Normal; + } + + [DebuggerFunction("unload trident", "Unloads the specified trident drive.", "")] + private CommandResult UnloadTrident(ushort drive) + { + if (drive > 7) + { + throw new InvalidOperationException("Drive specification out of range."); + } + + // Unload the current pack. + _system.UnloadTridentDrive(drive); + Console.WriteLine("Trident {0} unloaded.", drive); + + return CommandResult.Normal; + } + + [DebuggerFunction("show trident", "Displays the contents of the specified trident drive.", "")] + private CommandResult ShowTrident(ushort drive) + { + if (drive > 7) + { + throw new InvalidOperationException("Drive specification out of range."); + } + + // Save current drive contents. + if (_system.TridentController.Drives[drive].IsLoaded) + { + Console.WriteLine("Trident {0} contains image {1}", + drive, + _system.TridentController.Drives[drive].Pack.PackName); + } + else + { + Console.WriteLine("Trident {0} is not loaded.", drive); + } + + return CommandResult.Normal; + } [DebuggerFunction("show system type", "Displays the Alto system type.")] private CommandResult ShowSystemType() diff --git a/Contralto/UI/AltoWindow.Designer.cs b/Contralto/UI/AltoWindow.Designer.cs index fae007b..32078f5 100644 --- a/Contralto/UI/AltoWindow.Designer.cs +++ b/Contralto/UI/AltoWindow.Designer.cs @@ -41,13 +41,14 @@ this.unloadToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); this.Drive0ImageName = new System.Windows.Forms.ToolStripMenuItem(); this.drive1ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.loadToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.unloadToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.loadToolStripMenuItem2 = new System.Windows.Forms.ToolStripMenuItem(); + this.unloadToolStripMenuItem2 = new System.Windows.Forms.ToolStripMenuItem(); this.Drive1ImageName = new System.Windows.Forms.ToolStripMenuItem(); this.AlternateBootToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.SystemEthernetBootMenu = new System.Windows.Forms.ToolStripMenuItem(); this.optionsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.SystemShowDebuggerMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.fullScreenToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.helpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.StatusLine = new System.Windows.Forms.StatusStrip(); @@ -56,7 +57,9 @@ this.CaptureStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); this.SystemStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); this.DisplayBox = new System.Windows.Forms.PictureBox(); - this.fullScreenToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.newToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); + this.newToolStripMenuItem2 = new System.Windows.Forms.ToolStripMenuItem(); + this.TridentToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this._mainMenu.SuspendLayout(); this.StatusLine.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.DisplayBox)).BeginInit(); @@ -106,6 +109,7 @@ this.SystemResetMenuItem, this.drive0ToolStripMenuItem, this.drive1ToolStripMenuItem, + this.TridentToolStripMenuItem, this.AlternateBootToolStripMenuItem, this.SystemEthernetBootMenu, this.optionsToolStripMenuItem, @@ -139,6 +143,7 @@ this.drive0ToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.loadToolStripMenuItem1, this.unloadToolStripMenuItem1, + this.newToolStripMenuItem1, this.Drive0ImageName}); this.drive0ToolStripMenuItem.Name = "drive0ToolStripMenuItem"; this.drive0ToolStripMenuItem.Size = new System.Drawing.Size(223, 22); @@ -157,7 +162,7 @@ // this.unloadToolStripMenuItem1.Name = "unloadToolStripMenuItem1"; this.unloadToolStripMenuItem1.Size = new System.Drawing.Size(172, 22); - this.unloadToolStripMenuItem1.Text = "Unload..."; + this.unloadToolStripMenuItem1.Text = "Unload"; this.unloadToolStripMenuItem1.Click += new System.EventHandler(this.OnSystemDrive0UnloadClick); // // Drive0ImageName @@ -170,32 +175,33 @@ // drive1ToolStripMenuItem // this.drive1ToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.loadToolStripMenuItem, - this.unloadToolStripMenuItem, + this.loadToolStripMenuItem2, + this.unloadToolStripMenuItem2, + this.newToolStripMenuItem2, this.Drive1ImageName}); this.drive1ToolStripMenuItem.Name = "drive1ToolStripMenuItem"; this.drive1ToolStripMenuItem.Size = new System.Drawing.Size(223, 22); this.drive1ToolStripMenuItem.Text = "Drive 1"; // - // loadToolStripMenuItem + // loadToolStripMenuItem2 // - this.loadToolStripMenuItem.Name = "loadToolStripMenuItem"; - this.loadToolStripMenuItem.Size = new System.Drawing.Size(142, 22); - this.loadToolStripMenuItem.Text = "Load..."; - this.loadToolStripMenuItem.Click += new System.EventHandler(this.OnSystemDrive1LoadClick); + this.loadToolStripMenuItem2.Name = "loadToolStripMenuItem2"; + this.loadToolStripMenuItem2.Size = new System.Drawing.Size(152, 22); + this.loadToolStripMenuItem2.Text = "Load..."; + this.loadToolStripMenuItem2.Click += new System.EventHandler(this.OnSystemDrive1LoadClick); // - // unloadToolStripMenuItem + // unloadToolStripMenuItem2 // - this.unloadToolStripMenuItem.Name = "unloadToolStripMenuItem"; - this.unloadToolStripMenuItem.Size = new System.Drawing.Size(142, 22); - this.unloadToolStripMenuItem.Text = "Unload..."; - this.unloadToolStripMenuItem.Click += new System.EventHandler(this.OnSystemDrive1UnloadClick); + this.unloadToolStripMenuItem2.Name = "unloadToolStripMenuItem2"; + this.unloadToolStripMenuItem2.Size = new System.Drawing.Size(152, 22); + this.unloadToolStripMenuItem2.Text = "Unload"; + this.unloadToolStripMenuItem2.Click += new System.EventHandler(this.OnSystemDrive1UnloadClick); // // Drive1ImageName // this.Drive1ImageName.Enabled = false; this.Drive1ImageName.Name = "Drive1ImageName"; - this.Drive1ImageName.Size = new System.Drawing.Size(142, 22); + this.Drive1ImageName.Size = new System.Drawing.Size(152, 22); this.Drive1ImageName.Text = "Image Name"; // // AlternateBootToolStripMenuItem @@ -228,6 +234,15 @@ this.SystemShowDebuggerMenuItem.Text = "Show Debugger"; this.SystemShowDebuggerMenuItem.Click += new System.EventHandler(this.OnDebuggerShowClick); // + // fullScreenToolStripMenuItem + // + this.fullScreenToolStripMenuItem.Name = "fullScreenToolStripMenuItem"; + this.fullScreenToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)(((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.Alt) + | System.Windows.Forms.Keys.F))); + this.fullScreenToolStripMenuItem.Size = new System.Drawing.Size(223, 22); + this.fullScreenToolStripMenuItem.Text = "Full Screen"; + this.fullScreenToolStripMenuItem.Click += new System.EventHandler(this.OnFullScreenMenuClick); + // // helpToolStripMenuItem // this.helpToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { @@ -307,14 +322,25 @@ this.DisplayBox.MouseMove += new System.Windows.Forms.MouseEventHandler(this.OnDisplayMouseMove); this.DisplayBox.MouseUp += new System.Windows.Forms.MouseEventHandler(this.OnDisplayMouseUp); // - // fullScreenToolStripMenuItem + // newToolStripMenuItem1 // - this.fullScreenToolStripMenuItem.Name = "fullScreenToolStripMenuItem"; - this.fullScreenToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)(((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.Alt) - | System.Windows.Forms.Keys.F))); - this.fullScreenToolStripMenuItem.Size = new System.Drawing.Size(223, 22); - this.fullScreenToolStripMenuItem.Text = "Full Screen"; - this.fullScreenToolStripMenuItem.Click += new System.EventHandler(this.OnFullScreenMenuClick); + this.newToolStripMenuItem1.Name = "newToolStripMenuItem1"; + this.newToolStripMenuItem1.Size = new System.Drawing.Size(172, 22); + this.newToolStripMenuItem1.Text = "New..."; + this.newToolStripMenuItem1.Click += new System.EventHandler(this.OnSystemDrive0NewClick); + // + // newToolStripMenuItem2 + // + this.newToolStripMenuItem2.Name = "newToolStripMenuItem2"; + this.newToolStripMenuItem2.Size = new System.Drawing.Size(152, 22); + this.newToolStripMenuItem2.Text = "New..."; + this.newToolStripMenuItem2.Click += new System.EventHandler(this.OnSystemDrive1NewClick); + // + // TridentToolStripMenuItem + // + this.TridentToolStripMenuItem.Name = "TridentToolStripMenuItem"; + this.TridentToolStripMenuItem.Size = new System.Drawing.Size(223, 22); + this.TridentToolStripMenuItem.Text = "Trident Drives"; // // AltoWindow // @@ -363,8 +389,8 @@ private System.Windows.Forms.ToolStripMenuItem loadToolStripMenuItem1; private System.Windows.Forms.ToolStripMenuItem unloadToolStripMenuItem1; private System.Windows.Forms.ToolStripMenuItem drive1ToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem loadToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem unloadToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem loadToolStripMenuItem2; + private System.Windows.Forms.ToolStripMenuItem unloadToolStripMenuItem2; private System.Windows.Forms.ToolStripMenuItem optionsToolStripMenuItem; private System.Windows.Forms.StatusStrip StatusLine; private System.Windows.Forms.ToolStripStatusLabel CaptureStatusLabel; @@ -378,5 +404,8 @@ private System.Windows.Forms.ToolStripMenuItem saveScreenshotToolStripMenuItem; private System.Windows.Forms.ToolStripStatusLabel FPSLabel; private System.Windows.Forms.ToolStripMenuItem fullScreenToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem newToolStripMenuItem1; + private System.Windows.Forms.ToolStripMenuItem newToolStripMenuItem2; + private System.Windows.Forms.ToolStripMenuItem TridentToolStripMenuItem; } } \ No newline at end of file diff --git a/Contralto/UI/AltoWindow.cs b/Contralto/UI/AltoWindow.cs index 31dfe56..0a6b702 100644 --- a/Contralto/UI/AltoWindow.cs +++ b/Contralto/UI/AltoWindow.cs @@ -62,6 +62,8 @@ namespace Contralto ReleaseMouse(); + CreateTridentMenu(); + SystemStatusLabel.Text = _systemStoppedText; DiskStatusLabel.Text = String.Empty; @@ -95,8 +97,16 @@ namespace Contralto _controller.ErrorCallback += OnExecutionError; // Update disk image UI info + // Diablo disks: Drive0ImageName.Text = _system.DiskController.Drives[0].IsLoaded ? Path.GetFileName(_system.DiskController.Drives[0].Pack.PackName) : _noImageLoadedText; Drive1ImageName.Text = _system.DiskController.Drives[1].IsLoaded ? Path.GetFileName(_system.DiskController.Drives[1].Pack.PackName) : _noImageLoadedText; + + // Trident disks + for (int i = 0; i < _tridentImageNames.Count; i++) + { + TridentDrive drive = _system.TridentController.Drives[i]; + _tridentImageNames[i].Text = drive.IsLoaded ? Path.GetFileName(drive.Pack.PackName) : _noImageLoadedText; + } } /// @@ -129,7 +139,7 @@ namespace Contralto private void OnSystemDrive0LoadClick(object sender, EventArgs e) { - string path = ShowImageLoadDialog(0); + string path = ShowImageLoadDialog(0, false); if (String.IsNullOrEmpty(path)) { @@ -138,9 +148,7 @@ namespace Contralto try { - // Commit loaded pack back to disk - _system.CommitDiskPack(0); - _system.LoadDrive(0, path); + _system.LoadDiabloDrive(0, path, false); Drive0ImageName.Text = System.IO.Path.GetFileName(path); Configuration.Drive0Image = path; } @@ -154,15 +162,14 @@ namespace Contralto private void OnSystemDrive0UnloadClick(object sender, EventArgs e) { - _system.CommitDiskPack(0); - _system.UnloadDrive(0); + _system.UnloadDiabloDrive(0); Drive0ImageName.Text = _noImageLoadedText; Configuration.Drive0Image = String.Empty; } - private void OnSystemDrive1LoadClick(object sender, EventArgs e) + private void OnSystemDrive0NewClick(object sender, EventArgs e) { - string path = ShowImageLoadDialog(1); + string path = ShowImageNewDialog(0, false); if (String.IsNullOrEmpty(path)) { @@ -171,9 +178,30 @@ namespace Contralto try { - // Commit loaded pack back to disk - _system.CommitDiskPack(1); - _system.LoadDrive(1, path); + _system.LoadDiabloDrive(0, path, true); + Drive0ImageName.Text = System.IO.Path.GetFileName(path); + Configuration.Drive0Image = path; + } + catch (Exception ex) + { + MessageBox.Show( + String.Format("An error occurred while creating new disk image: {0}", ex.Message), + "Image creation error", MessageBoxButtons.OK); + } + } + + private void OnSystemDrive1LoadClick(object sender, EventArgs e) + { + string path = ShowImageLoadDialog(1, false); + + if (String.IsNullOrEmpty(path)) + { + return; + } + + try + { + _system.LoadDiabloDrive(1, path, false); Drive1ImageName.Text = System.IO.Path.GetFileName(path); Configuration.Drive1Image = path; } @@ -186,13 +214,92 @@ namespace Contralto } private void OnSystemDrive1UnloadClick(object sender, EventArgs e) - { - _system.CommitDiskPack(1); - _system.UnloadDrive(1); + { + _system.UnloadDiabloDrive(1); Drive1ImageName.Text = _noImageLoadedText; Configuration.Drive1Image = String.Empty; } + + private void OnSystemDrive1NewClick(object sender, EventArgs e) + { + string path = ShowImageNewDialog(1, false); + + if (String.IsNullOrEmpty(path)) + { + return; + } + + try + { + _system.LoadDiabloDrive(1, path, true); + Drive0ImageName.Text = System.IO.Path.GetFileName(path); + Configuration.Drive0Image = path; + } + catch (Exception ex) + { + MessageBox.Show( + String.Format("An error occurred while creating new disk image: {0}", ex.Message), + "Image creation error", MessageBoxButtons.OK); + } + } + + private void OnTridentLoadClick(object sender, EventArgs e) + { + int drive = (int)((ToolStripDropDownItem)sender).Tag; + string path = ShowImageLoadDialog(drive, true); + + if (String.IsNullOrEmpty(path)) + { + return; + } + + try + { + _system.LoadTridentDrive(drive, path, false); + _tridentImageNames[drive].Text = System.IO.Path.GetFileName(path); + Configuration.TridentImages[drive] = path; + } + catch (Exception ex) + { + MessageBox.Show( + String.Format("An error occurred while loading Trident image: {0}", ex.Message), + "Image load error", MessageBoxButtons.OK); + } + } + + private void OnTridentUnloadClick(object sender, EventArgs e) + { + int drive = (int)((ToolStripDropDownItem)sender).Tag; + _system.UnloadTridentDrive(drive); + _tridentImageNames[drive].Text = _noImageLoadedText; + Configuration.TridentImages[drive] = String.Empty; + } + + private void OnTridentNewClick(object sender, EventArgs e) + { + int drive = (int)((ToolStripDropDownItem)sender).Tag; + string path = ShowImageNewDialog(drive, true); + + if (String.IsNullOrEmpty(path)) + { + return; + } + + try + { + _system.LoadTridentDrive(drive, path, true); + _tridentImageNames[drive].Text = System.IO.Path.GetFileName(path); + Configuration.TridentImages[drive] = path; + } + catch (Exception ex) + { + MessageBox.Show( + String.Format("An error occurred while creating new Trident image: {0}", ex.Message), + "Image creation error", MessageBoxButtons.OK); + } + } + private void OnAlternateBootOptionsClicked(object sender, EventArgs e) { AlternateBootOptions bootWindow = new AlternateBootOptions(); @@ -311,16 +418,40 @@ namespace Contralto DialogResult = DialogResult.OK; } - private string ShowImageLoadDialog(int drive) + private string ShowImageLoadDialog(int drive, bool trident) { OpenFileDialog fileDialog = new OpenFileDialog(); - fileDialog.DefaultExt = "dsk"; - fileDialog.Filter = "Alto Disk Images (*.dsk, *.dsk44)|*.dsk;*.dsk44|Diablo 31 Disk Images (*.dsk)|*.dsk|Diablo 44 Disk Images (*.dsk44)|*.dsk44|All Files (*.*)|*.*"; + fileDialog.DefaultExt = trident ? "dsk80" : "dsk"; + fileDialog.Filter = trident ? _tridentFilter : _diabloFilter; fileDialog.Multiselect = false; fileDialog.CheckFileExists = true; fileDialog.CheckPathExists = true; - fileDialog.Title = String.Format("Select image to load into drive {0}", drive); + fileDialog.Title = String.Format("Select image to load into {0} drive {1}", trident ? "Trident" : "Diablo", drive); + + DialogResult res = fileDialog.ShowDialog(); + + if (res == DialogResult.OK) + { + return fileDialog.FileName; + } + else + { + return null; + } + } + + private string ShowImageNewDialog(int drive, bool trident) + { + SaveFileDialog fileDialog = new SaveFileDialog(); + + fileDialog.DefaultExt = trident ? "dsk80" : "dsk"; + fileDialog.Filter = trident ? _tridentFilter : _diabloFilter; + fileDialog.CheckFileExists = false; + fileDialog.CheckPathExists = true; + fileDialog.OverwritePrompt = true; + fileDialog.ValidateNames = true; + fileDialog.Title = String.Format("Select path for new {0} image for drive {1}", trident ? "Trident" : "Diablo", drive); DialogResult res = fileDialog.ShowDialog(); @@ -965,6 +1096,47 @@ namespace Contralto return null; } + private void CreateTridentMenu() + { + // + // Add eight sub-menus, one per drive. + // + _tridentImageNames = new List(8); + + for (int i=0;i<8;i++) + { + // Parent menu item + ToolStripMenuItem tridentMenu = new ToolStripMenuItem(String.Format("Drive {0}", i)); + + // Children: + // - Load + // - Unload + // - New + // - Pack Name (disabled) + // + ToolStripMenuItem loadMenu = new ToolStripMenuItem("Load...", null, OnTridentLoadClick); + loadMenu.Tag = i; + + ToolStripMenuItem unloadMenu = new ToolStripMenuItem("Unload", null, OnTridentUnloadClick); + unloadMenu.Tag = i; + + ToolStripMenuItem newMenu = new ToolStripMenuItem("New...", null, OnTridentNewClick); + newMenu.Tag = i; + + ToolStripMenuItem imageMenu = new ToolStripMenuItem(_noImageLoadedText); + imageMenu.Tag = i; + imageMenu.Enabled = false; + _tridentImageNames.Add(imageMenu); + + tridentMenu.DropDownItems.Add(loadMenu); + tridentMenu.DropDownItems.Add(unloadMenu); + tridentMenu.DropDownItems.Add(newMenu); + tridentMenu.DropDownItems.Add(imageMenu); + + TridentToolStripMenuItem.DropDownItems.Add(tridentMenu); + } + } + // Display related data. // Note: display is actually 606 pixels wide, but that's not an even multiple of 8, so we round up. @@ -1015,10 +1187,17 @@ namespace Contralto private Image _diskWriteImage; private Image _diskSeekImage; + // Trident menu items for disk names + private List _tridentImageNames; + // strings. TODO: move to resource private const string _noImageLoadedText = ""; private const string _systemStoppedText = "Alto Stopped."; private const string _systemRunningText = "Alto Running."; - private const string _systemErrorText = "Alto Stopped due to error. See Debugger."; + private const string _systemErrorText = "Alto Stopped due to error. See Debugger."; + private const string _diabloFilter = "Alto Diablo Disk Images (*.dsk, *.dsk44)|*.dsk;*.dsk44|Diablo 31 Disk Images (*.dsk)|*.dsk|Diablo 44 Disk Images (*.dsk44)|*.dsk44|All Files (*.*)|*.*"; + private const string _tridentFilter = "Alto Trident Disk Images (*.dsk80, *.dsk300)|*.dsk80;*.dsk300|Trident T80 Disk Images (*.dsk80)|*.dsk80|Trident T300 Disk Images (*.dsk300)|*.dsk300|All Files (*.*)|*.*"; + + } } diff --git a/Contralto/UI/Debugger.cs b/Contralto/UI/Debugger.cs index 68baf69..4821d56 100644 --- a/Contralto/UI/Debugger.cs +++ b/Contralto/UI/Debugger.cs @@ -598,7 +598,7 @@ namespace Contralto "EM", // 0 - emulator "OR", // 1 - orbit String.Empty, - String.Empty, + "TO", // 3 - trident output "KS", // 4 - disk sector String.Empty, String.Empty, @@ -610,7 +610,7 @@ namespace Contralto "DV", // 12 - display vertical "PA", // 13 - parity "KW", // 14 - disk word - String.Empty, + "TI", // 15 - trident input }; if (task == TaskType.Invalid) @@ -630,7 +630,7 @@ namespace Contralto Color.LightBlue, // 0 - emulator Color.LightGoldenrodYellow, // 1 - orbit Color.LightGray, // 2 - unused - Color.LightGray, // 3 - unused + Color.LightCoral, // 3 - trident output Color.LightGreen, // 4 - disk sector Color.LightGray, // 5 - unused Color.LightGray, // 6 - unused @@ -638,11 +638,11 @@ namespace Contralto Color.LightSeaGreen,// 8 - memory refresh Color.LightYellow, // 9 - display word Color.LightPink, // 10 - cursor - Color.Chartreuse, // 11 - display horizontal + Color.Chartreuse, // 11 - display horizontal Color.LightCoral, // 12 - display vertical Color.LightSteelBlue, // 13 - parity Color.Gray, // 14 - disk word - Color.LightGray, // 15 - unused + Color.LightSteelBlue, // 15 - trident output }; if (task == TaskType.Invalid) diff --git a/Contralto/UI/SystemOptions.Designer.cs b/Contralto/UI/SystemOptions.Designer.cs index 014156c..661c07a 100644 --- a/Contralto/UI/SystemOptions.Designer.cs +++ b/Contralto/UI/SystemOptions.Designer.cs @@ -55,15 +55,15 @@ this.label2 = new System.Windows.Forms.Label(); this.EnableDACCaptureCheckBox = new System.Windows.Forms.CheckBox(); this.EnableDACCheckBox = new System.Windows.Forms.CheckBox(); - this.DialogOKButton = new System.Windows.Forms.Button(); - this.DialogCancelButton = new System.Windows.Forms.Button(); this.PrintingTab = new System.Windows.Forms.TabPage(); this.PrintingOptionsGroupBox = new System.Windows.Forms.GroupBox(); + this.ReversePageOrderCheckBox = new System.Windows.Forms.CheckBox(); this.button1 = new System.Windows.Forms.Button(); this.PrintOutputPathTextBox = new System.Windows.Forms.TextBox(); this.label5 = new System.Windows.Forms.Label(); this.EnablePrintingCheckBox = new System.Windows.Forms.CheckBox(); - this.ReversePageOrderCheckBox = new System.Windows.Forms.CheckBox(); + this.DialogOKButton = new System.Windows.Forms.Button(); + this.DialogCancelButton = new System.Windows.Forms.Button(); this.OptionsTabs.SuspendLayout(); this.tabPage1.SuspendLayout(); this.tabPage2.SuspendLayout(); @@ -368,26 +368,6 @@ this.EnableDACCheckBox.UseVisualStyleBackColor = true; this.EnableDACCheckBox.CheckedChanged += new System.EventHandler(this.OnEnableDACCheckboxChanged); // - // DialogOKButton - // - this.DialogOKButton.Location = new System.Drawing.Point(211, 239); - this.DialogOKButton.Name = "DialogOKButton"; - this.DialogOKButton.Size = new System.Drawing.Size(75, 23); - this.DialogOKButton.TabIndex = 1; - this.DialogOKButton.Text = "OK"; - this.DialogOKButton.UseVisualStyleBackColor = true; - this.DialogOKButton.Click += new System.EventHandler(this.OKButton_Click); - // - // DialogCancelButton - // - this.DialogCancelButton.Location = new System.Drawing.Point(292, 239); - this.DialogCancelButton.Name = "DialogCancelButton"; - this.DialogCancelButton.Size = new System.Drawing.Size(75, 23); - this.DialogCancelButton.TabIndex = 2; - this.DialogCancelButton.Text = "Cancel"; - this.DialogCancelButton.UseVisualStyleBackColor = true; - this.DialogCancelButton.Click += new System.EventHandler(this.CancelButton_Click); - // // PrintingTab // this.PrintingTab.Controls.Add(this.PrintingOptionsGroupBox); @@ -413,6 +393,16 @@ this.PrintingOptionsGroupBox.TabStop = false; this.PrintingOptionsGroupBox.Text = "Printing options"; // + // ReversePageOrderCheckBox + // + this.ReversePageOrderCheckBox.AutoSize = true; + this.ReversePageOrderCheckBox.Location = new System.Drawing.Point(22, 51); + this.ReversePageOrderCheckBox.Name = "ReversePageOrderCheckBox"; + this.ReversePageOrderCheckBox.Size = new System.Drawing.Size(158, 17); + this.ReversePageOrderCheckBox.TabIndex = 4; + this.ReversePageOrderCheckBox.Text = "Reverse Output Page Order"; + this.ReversePageOrderCheckBox.UseVisualStyleBackColor = true; + // // button1 // this.button1.Location = new System.Drawing.Point(254, 24); @@ -421,6 +411,7 @@ this.button1.TabIndex = 3; this.button1.Text = "Browse..."; this.button1.UseVisualStyleBackColor = true; + this.button1.Click += new System.EventHandler(this.OnPrintOutputBrowseButtonClicked); // // PrintOutputPathTextBox // @@ -449,15 +440,25 @@ this.EnablePrintingCheckBox.UseVisualStyleBackColor = true; this.EnablePrintingCheckBox.CheckedChanged += new System.EventHandler(this.EnablePrintingCheckBox_CheckedChanged); // - // ReversePageOrderCheckBox + // DialogOKButton // - this.ReversePageOrderCheckBox.AutoSize = true; - this.ReversePageOrderCheckBox.Location = new System.Drawing.Point(22, 51); - this.ReversePageOrderCheckBox.Name = "ReversePageOrderCheckBox"; - this.ReversePageOrderCheckBox.Size = new System.Drawing.Size(158, 17); - this.ReversePageOrderCheckBox.TabIndex = 4; - this.ReversePageOrderCheckBox.Text = "Reverse Output Page Order"; - this.ReversePageOrderCheckBox.UseVisualStyleBackColor = true; + this.DialogOKButton.Location = new System.Drawing.Point(211, 239); + this.DialogOKButton.Name = "DialogOKButton"; + this.DialogOKButton.Size = new System.Drawing.Size(75, 23); + this.DialogOKButton.TabIndex = 1; + this.DialogOKButton.Text = "OK"; + this.DialogOKButton.UseVisualStyleBackColor = true; + this.DialogOKButton.Click += new System.EventHandler(this.OKButton_Click); + // + // DialogCancelButton + // + this.DialogCancelButton.Location = new System.Drawing.Point(292, 239); + this.DialogCancelButton.Name = "DialogCancelButton"; + this.DialogCancelButton.Size = new System.Drawing.Size(75, 23); + this.DialogCancelButton.TabIndex = 2; + this.DialogCancelButton.Text = "Cancel"; + this.DialogCancelButton.UseVisualStyleBackColor = true; + this.DialogCancelButton.Click += new System.EventHandler(this.CancelButton_Click); // // SystemOptions // diff --git a/Contralto/UI/SystemOptions.cs b/Contralto/UI/SystemOptions.cs index 23a599b..a1fe523 100644 --- a/Contralto/UI/SystemOptions.cs +++ b/Contralto/UI/SystemOptions.cs @@ -360,10 +360,15 @@ namespace Contralto.UI } else { - EnablePrintingCheckBox.Checked = false; + EnablePrintingCheckBox.Checked = string.IsNullOrEmpty(PrintOutputPathTextBox.Text) ? false : true; } } + private void OnPrintOutputBrowseButtonClicked(object sender, EventArgs e) + { + BrowseForPrintOutputFolder(); + } + private void EnablePrintingCheckBox_CheckedChanged(object sender, EventArgs e) { PrintingOptionsGroupBox.Enabled = EnablePrintingCheckBox.Checked; @@ -374,6 +379,6 @@ namespace Contralto.UI // BrowseForPrintOutputFolder(); } - } + } } } diff --git a/ContraltoSetup/Product.wxs b/ContraltoSetup/Product.wxs index db96d77..ba787cc 100644 --- a/ContraltoSetup/Product.wxs +++ b/ContraltoSetup/Product.wxs @@ -194,10 +194,10 @@ - - - - + + + +