diff --git a/Contralto/AltoSystem.cs b/Contralto/AltoSystem.cs index b2f0c85..fd6eb10 100644 --- a/Contralto/AltoSystem.cs +++ b/Contralto/AltoSystem.cs @@ -47,7 +47,7 @@ namespace Contralto t.AutoReset = true; t.Interval = 1000; t.Elapsed += T_Elapsed; - t.Start(); + //t.Start(); } public void Reset() @@ -80,6 +80,11 @@ namespace Contralto _displayController.AttachDisplay(d); } + public void DetachDisplay() + { + _displayController.DetachDisplay(); + } + public void SingleStep() { // Run every device that needs attention for a single clock cycle. @@ -128,7 +133,7 @@ namespace Contralto // if (Configuration.EthernetBootEnabled) { - _keyboard.PressBootKeys(Configuration.EthernetBootFile, true); + _keyboard.PressBootKeys(Configuration.BootAddress, true); } } @@ -172,20 +177,10 @@ namespace Contralto { get { return _scheduler; } } - - /// - /// Time (in msec) for one system clock - /// - /// - public static double ClockInterval - { - get { return 0.00017; } // appx 170nsec, TODO: more accurate value? - } - private void T_Elapsed(object sender, ElapsedEventArgs e) { - //System.Console.WriteLine("{0} CPU clocks/sec %{1}. {2} fields/sec", _clocks, ((double)_clocks / 5882353.0) * 100.0, _displayController.Fields); + System.Console.WriteLine("{0} CPU clocks/sec %{1}. {2} fields/sec", _clocks, ((double)_clocks / 5882353.0) * 100.0, _displayController.Fields); _clocks = 0; _displayController.Fields = 0; } diff --git a/Contralto/CPU/CPU.cs b/Contralto/CPU/CPU.cs index 13fdeb7..1710b30 100644 --- a/Contralto/CPU/CPU.cs +++ b/Contralto/CPU/CPU.cs @@ -172,7 +172,7 @@ namespace Contralto.CPU // WakeupTask(CPU.TaskType.DiskSector); - Logging.Log.Write(Logging.LogComponent.CPU, "Silent Boot; microcode banks initialized to {0}", Conversion.ToOctal(_rmr)); + Log.Write(Logging.LogComponent.CPU, "Silent Boot; microcode banks initialized to {0}", Conversion.ToOctal(_rmr)); } /// @@ -232,13 +232,15 @@ namespace Contralto.CPU if (_tasks[i] != null && _tasks[i].Wakeup) { _nextTask = _tasks[i]; + _nextTask.FirstInstructionAfterSwitch = true; + /* if (_nextTask != _currentTask && _currentTask != null) { Log.Write(LogComponent.TaskSwitch, "TASK: Next task will be {0} (pri {1}); current task {2} (pri {3})", (TaskType)_nextTask.Priority, _nextTask.Priority, (TaskType)_currentTask.Priority, _currentTask.Priority); - } + } */ break; } } diff --git a/Contralto/CPU/Tasks/Task.cs b/Contralto/CPU/Tasks/Task.cs index d4932a2..649c663 100644 --- a/Contralto/CPU/Tasks/Task.cs +++ b/Contralto/CPU/Tasks/Task.cs @@ -42,6 +42,17 @@ namespace Contralto.CPU get { return _mpc; } } + /// + /// Indicates whether a task switch just happened. TASK instructions behave differently on the + /// first instruction after a switch. This is not documented, but observed on the real hardware. + /// (See the implementation of the Task SF for more details.) + /// + public bool FirstInstructionAfterSwitch + { + get { return _firstInstructionAfterSwitch; } + set { _firstInstructionAfterSwitch = value; } + } + /// /// Indicates whether the current uInstruction asserts BLOCK. /// Used by hardware for various tasks. @@ -59,6 +70,7 @@ namespace Contralto.CPU _mpc = (ushort)_taskType; _rdRam = false; _rb = 0; + _firstInstructionAfterSwitch = false; } public virtual void SoftReset() @@ -273,7 +285,16 @@ namespace Contralto.CPU break; case SpecialFunction1.Task: - nextTask = true; // Yield to other more important tasks + // + // If the first uOp executed after a task switch contains a TASK F1, it does not take effect. + // This is observed on the real hardware, and does not appear to be documented. + // It also doensn't appear to affect the execution of the standard Alto uCode in any significant + // way, but is included here for correctness. + // + //if (!_firstInstructionAfterSwitch) + { + nextTask = true; // Yield to other more important tasks + } break; case SpecialFunction1.Block: @@ -430,7 +451,7 @@ namespace Contralto.CPU if (swMode) { UCodeMemory.SwitchMode(instruction.NEXT, _taskType); - Logging.Log.Write(Logging.LogComponent.Microcode, "SWMODE: uPC {0}, next uPC {1}", Conversion.ToOctal(_mpc), Conversion.ToOctal(instruction.NEXT | nextModifier)); + Logging.Log.Write(Logging.LogComponent.Microcode, "SWMODE: uPC {0}, next uPC {1}", Conversion.ToOctal(_mpc), Conversion.ToOctal(instruction.NEXT)); } // @@ -450,6 +471,7 @@ namespace Contralto.CPU _mpc = (ushort)(instruction.NEXT | nextModifier); } + _firstInstructionAfterSwitch = false; return nextTask; } @@ -523,6 +545,7 @@ namespace Contralto.CPU protected ushort _rb; // S register bank select protected TaskType _taskType; protected bool _wakeup; + protected bool _firstInstructionAfterSwitch; protected bool _block; diff --git a/Contralto/CPU/UCodeMemory.cs b/Contralto/CPU/UCodeMemory.cs index 744f9ef..84f4c23 100644 --- a/Contralto/CPU/UCodeMemory.cs +++ b/Contralto/CPU/UCodeMemory.cs @@ -40,17 +40,14 @@ namespace Contralto.CPU private static void Init() { - // - // TODO: this is currently configured for a 2K ROM machine - // (1K RAM, 2K ROM). This should be configurable. - // - // 1 bank of microcode RAM - _uCodeRam = new UInt32[1024]; + // + // Max 3 banks of microcode RAM + _uCodeRam = new UInt32[1024 * 3]; LoadMicrocode(_uCodeRoms); // - // Cache 3k of instructions: 2K ROM, 1K RAM. - _decodeCache = new MicroInstruction[1024 * 3]; + // Cache 5k of instructions: max 2K ROM, 3K RAM. + _decodeCache = new MicroInstruction[1024 * 5]; // Precache ROM CacheMicrocodeROM(); @@ -103,12 +100,22 @@ namespace Contralto.CPU _ramBank = (address & 0x3000) >> 12; _ramSelect = (address & 0x0800) == 0; _lowHalfsel = (address & 0x0400) == 0; - _ramAddr = (address & 0x3ff); - - if (_ramBank != 0) + _ramAddr = (address & 0x3ff); + + // Clip RAM bank into range, it's always 0 unless we have a 3K uCode RAM system + switch (Configuration.SystemType) { - //throw new NotImplementedException(String.Format("Unexpected RAM BANK select of {0}", _ramBank)); - _ramBank = 0; + case SystemType.OneKRom: + case SystemType.TwoKRom: + _ramBank = 0; + break; + case SystemType.ThreeKRam: + if (_ramBank > 3) + { + // TODO: clip or not. + throw new InvalidOperationException(String.Format("Unexpected RAM bank value of {0}.", _ramBank)); + } + break; } } @@ -121,26 +128,35 @@ namespace Contralto.CPU public static void SwitchMode(ushort nextAddress, TaskType task) { Logging.Log.Write(Logging.LogComponent.Microcode, "SWMODE: Current Bank {0}", _microcodeBank[(int)task]); - - // 2K ROM - switch(_microcodeBank[(int)task]) + + switch (Configuration.SystemType) { - case MicrocodeBank.ROM0: - _microcodeBank[(int)task] = (nextAddress & 0x100) == 0 ? MicrocodeBank.RAM0 : MicrocodeBank.ROM1; + case SystemType.OneKRom: + _microcodeBank[(int)task] = _microcodeBank[(int)task] == MicrocodeBank.ROM0 ? MicrocodeBank.RAM0 : MicrocodeBank.ROM0; break; - case MicrocodeBank.ROM1: - _microcodeBank[(int)task] = (nextAddress & 0x100) == 0 ? MicrocodeBank.ROM0 : MicrocodeBank.RAM0; + case SystemType.TwoKRom: + switch (_microcodeBank[(int)task]) + { + case MicrocodeBank.ROM0: + _microcodeBank[(int)task] = (nextAddress & 0x100) == 0 ? MicrocodeBank.RAM0 : MicrocodeBank.ROM1; + break; + + case MicrocodeBank.ROM1: + _microcodeBank[(int)task] = (nextAddress & 0x100) == 0 ? MicrocodeBank.ROM0 : MicrocodeBank.RAM0; + break; + + case MicrocodeBank.RAM0: + _microcodeBank[(int)task] = (nextAddress & 0x100) == 0 ? MicrocodeBank.ROM0 : MicrocodeBank.ROM1; + break; + } break; - case MicrocodeBank.RAM0: - _microcodeBank[(int)task] = (nextAddress & 0x100) == 0 ? MicrocodeBank.ROM0 : MicrocodeBank.ROM1; + case SystemType.ThreeKRam: + throw new NotImplementedException("3K uCode RAM not yet implemented."); break; } - // for 1K ROM -- todo: make configurable - //_microcodeBank[(int)task] = _microcodeBank[(int)task] == MicrocodeBank.ROM0 ? MicrocodeBank.RAM0 : MicrocodeBank.ROM0; - Logging.Log.Write(Logging.LogComponent.Microcode, "SWMODE: New Bank {0} for Task {1}", _microcodeBank[(int)task], task); } diff --git a/Contralto/Configuration.cs b/Contralto/Configuration.cs index 616dd23..ac99029 100644 --- a/Contralto/Configuration.cs +++ b/Contralto/Configuration.cs @@ -1,11 +1,48 @@ -using System; +using Contralto.Logging; +using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Contralto { + /// + /// The configuration of an Alto II to emulate + /// + public enum SystemType + { + /// + /// System with the standard 1K ROM, 1K RAM + /// + OneKRom, + + /// + /// System with 2K ROM, 1K RAM + /// + TwoKRom, + + /// + /// System with 3K RAM + /// + ThreeKRam, + } + + public enum PacketInterfaceType + { + /// + /// Encapsulate frames inside raw ethernet frames on the host interface. + /// Requires PCAP. + /// + EthernetEncapsulation, + + /// + /// Encapsulate frames inside UDP datagrams on the host interface. + /// + UDPEncapsulation, + } + /// /// Encapsulates user-configurable settings. To be enhanced. /// @@ -13,22 +50,218 @@ namespace Contralto { static Configuration() { - // Initialize things to defaults. - // TODO: Load from config file. + // Initialize things to defaults. HostAddress = 0x22; EthernetBootEnabled = false; - EthernetBootFile = 0; + BootAddress = 0; + + SystemType = SystemType.TwoKRom; + + InterlaceDisplay = false; + + ThrottleSpeed = true; + + ReadConfiguration(); } - public static string Drive0Image; - public static string Drive1Image; - public static byte HostAddress; - public static string HostEthernetInterfaceName; - public static bool HostEthernetAvailable; + /// + /// The type of Alto II to emulate + /// + public static SystemType SystemType; + /// + /// The currently loaded image for Drive 0 + /// + public static string Drive0Image; + + /// + /// The currently loaded image for Drive 1 + /// + public static string Drive1Image; + + /// + /// The Ethernet host address for this Alto + /// + public static byte HostAddress; + + /// + /// The name of the Ethernet adaptor on the emulator host to use for Ethernet emulation + /// + public static string HostPacketInterfaceName; + + /// + /// Whether any packet interfaces are available on the host + /// + public static bool HostRawEthernetInterfacesAvailable; + + /// + /// The type of interface to use to host networking. + /// + public static PacketInterfaceType HostPacketInterfaceType; + + /// + /// Whether to enable Ethernet boot at reset + /// public static bool EthernetBootEnabled; - public static ushort EthernetBootFile; + + /// + /// The address/file to boot at reset + /// + public static ushort BootAddress; + + /// + /// Whether to render the display "interlaced" or not. + /// + public static bool InterlaceDisplay; + + /// + /// Whether to cap execution speed at native execution speed or not. + /// + public static bool ThrottleSpeed; + + /// + /// Reads the current configuration file from disk. + /// + /// TODO: use reflection to do this. + /// + public static void ReadConfiguration() + { + try + { + using (StreamReader configStream = new StreamReader("contralto.cfg")) + { + // + // Config file consists of text lines containing name / value pairs: + // + // Whitespace is ignored + // + int lineNumber = 0; + while (!configStream.EndOfStream) + { + lineNumber++; + string line = configStream.ReadLine().ToLowerInvariant().Trim(); + + if (string.IsNullOrEmpty(line)) + { + continue; + } + + // Find whitespace separating tokens + int ws = line.IndexOfAny(new char[] { ' ', '\t' }); + + if (ws < 1) + { + Log.Write(LogType.Warning, LogComponent.Configuration, "Syntax error on line {0}. Ignoring.", lineNumber); + continue; + } + + string parameter = line.Substring(0, ws); + string value = line.Substring(ws + 1, line.Length - ws - 1); + + try + { + switch (parameter) + { + case "drive0image": + Drive0Image = value; + break; + + case "drive1image": + Drive1Image = value; + break; + + case "systemtype": + SystemType = (SystemType)Enum.Parse(typeof(SystemType), value, true); + break; + + case "hostaddress": + HostAddress = Convert.ToByte(value, 8); + break; + + case "hostpacketinterfacename": + HostPacketInterfaceName = value; + break; + + case "hostpacketinterfacetype": + HostPacketInterfaceType = (PacketInterfaceType)Enum.Parse(typeof(PacketInterfaceType), value, true); + break; + + case "ethernetbootenabled": + EthernetBootEnabled = bool.Parse(value); + break; + + case "bootaddress": + BootAddress = Convert.ToUInt16(value, 8); + break; + + case "interlacedisplay": + InterlaceDisplay = bool.Parse(value); + break; + + case "throttlespeed": + ThrottleSpeed = bool.Parse(value); + break; + + default: + Log.Write(LogType.Warning, LogComponent.Configuration, "Invalid parameter on line {0}. Ignoring.", lineNumber); + break; + } + } + catch + { + Log.Write(LogType.Warning, LogComponent.Configuration, "Invalid value on line {0}. Ignoring.", lineNumber); + continue; + } + } + } + } + catch (Exception) + { + Log.Write(LogType.Warning, LogComponent.Configuration, "Configuration file 'contralto.cfg' could not be read; assuming default settings."); + WriteConfiguration(); + } + } + + /// + /// Commits the current configuration to disk. + /// + public static void WriteConfiguration() + { + try + { + using (StreamWriter configStream = new StreamWriter("contralto.cfg")) + { + if (!string.IsNullOrEmpty(Drive0Image)) + { + configStream.WriteLine("Drive0Image {0}", Drive0Image); + } + + if (!string.IsNullOrEmpty(Drive1Image)) + { + configStream.WriteLine("Drive1Image {0}", Drive1Image); + } + + configStream.WriteLine("SystemType {0}", SystemType); + configStream.WriteLine("HostAddress {0}", Conversion.ToOctal(HostAddress)); + + if (!string.IsNullOrEmpty(HostPacketInterfaceName)) + { + configStream.WriteLine("HostPacketInterfaceName {0}", HostPacketInterfaceName); + } + + configStream.WriteLine("HostPacketInterfaceType {0}", HostPacketInterfaceType); + configStream.WriteLine("EthernetBootEnabled {0}", EthernetBootEnabled); + configStream.WriteLine("BootAddress {0}", Conversion.ToOctal(BootAddress)); + configStream.WriteLine("InterlaceDisplay {0}", InterlaceDisplay); + configStream.WriteLine("ThrottleSpeed {0}", ThrottleSpeed); + } + } + catch (Exception) + { + Log.Write(LogType.Warning, LogComponent.Configuration, "Configuration file 'contralto.cfg' could not be opened for writing."); + } + } } } diff --git a/Contralto/Contralto.csproj b/Contralto/Contralto.csproj index d47c7c7..ca432d1 100644 --- a/Contralto/Contralto.csproj +++ b/Contralto/Contralto.csproj @@ -99,6 +99,14 @@ + + + + + True + True + Resources.resx + Form @@ -140,18 +148,18 @@ - + Form - - EthernetBootWindow.cs + + AlternateBootWindow.cs - + @@ -163,6 +171,12 @@ + + Form + + + SystemOptions.cs + @@ -334,6 +348,10 @@ + + ResXFileCodeGenerator + Resources.Designer.cs + AboutBox.cs @@ -343,8 +361,11 @@ Debugger.cs - - EthernetBootWindow.cs + + AlternateBootWindow.cs + + + SystemOptions.cs diff --git a/Contralto/Disassembly/altoIIcode3.mu b/Contralto/Disassembly/altoIIcode3.mu index 9b8d381..0d45362 100644 --- a/Contralto/Disassembly/altoIIcode3.mu +++ b/Contralto/Disassembly/altoIIcode3.mu @@ -1102,7 +1102,7 @@ EM1036> AC1<- L, :LOADX; ;SIO - 61004 - Put AC0 on the bus, issue STARTF to get device attention, ;Read Host address from Ethernet interface into AC0. -EM104> SIO: L<- AC0, STARTF; +EM0104> SIO: L<- AC0, STARTF; EM1037> T<- 77777; ***X21 sets AC0[0] to 0 EM1040> L<- RSNF AND T; EM1041> LTOAC0: AC0<- L, TASK, :TOSTART; diff --git a/Contralto/Disk/Josh.dsk b/Contralto/Disk/Josh.dsk index cd6f8db..6d808bc 100644 Binary files a/Contralto/Disk/Josh.dsk and b/Contralto/Disk/Josh.dsk differ diff --git a/Contralto/Disk/allgames.dsk b/Contralto/Disk/allgames.dsk index 2412dce..b33f0be 100644 Binary files a/Contralto/Disk/allgames.dsk and b/Contralto/Disk/allgames.dsk differ diff --git a/Contralto/Disk/diag.dsk b/Contralto/Disk/diag.dsk index 0f0cbdb..a2750d3 100644 Binary files a/Contralto/Disk/diag.dsk and b/Contralto/Disk/diag.dsk differ diff --git a/Contralto/Disk/st76boot.dsk b/Contralto/Disk/st76boot.dsk index 335cbd3..390aa00 100644 Binary files a/Contralto/Disk/st76boot.dsk and b/Contralto/Disk/st76boot.dsk differ diff --git a/Contralto/Disk/tdisk8.dsk b/Contralto/Disk/tdisk8.dsk index 3b67268..f2b5132 100644 Binary files a/Contralto/Disk/tdisk8.dsk and b/Contralto/Disk/tdisk8.dsk differ diff --git a/Contralto/Display/DisplayController.cs b/Contralto/Display/DisplayController.cs index 803bffa..115d25e 100644 --- a/Contralto/Display/DisplayController.cs +++ b/Contralto/Display/DisplayController.cs @@ -21,6 +21,11 @@ namespace Contralto.Display _display = display; } + public void DetachDisplay() + { + _display = null; + } + public int Fields { get { return _fields; } @@ -160,6 +165,11 @@ namespace Contralto.Display private void WordCallback(ulong timeNsec, ulong skewNsec, object context) { + if (_display == null) + { + return; + } + // Dequeue a word (if available) and draw it to the screen. ushort displayWord = (ushort)(_whiteOnBlack ? 0 : 0xffff); if (_dataBuffer.Count > 0) diff --git a/Contralto/HighResTimer.cs b/Contralto/HighResTimer.cs new file mode 100644 index 0000000..12ca5b5 --- /dev/null +++ b/Contralto/HighResTimer.cs @@ -0,0 +1,191 @@ +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Contralto +{ + /// + /// HighResTimer gives us access to NT's very-high-resolution PerformanceCounters. + /// This gives us the precision we need to sync emulation to any speed we desire. + /// + public sealed class HighResTimer + { + [DllImport("Kernel32.dll")] + private static extern bool QueryPerformanceCounter( + out long lpPerformanceCount); + + [DllImport("Kernel32.dll")] + private static extern bool QueryPerformanceFrequency( + out long lpFrequency); + + public HighResTimer() + { + // What's the frequency, Kenneth? + if (QueryPerformanceFrequency(out _frequency) == false) + { + // high-performance counter not supported + throw new Win32Exception(); + } + } + + /// + /// Returns the current time in seconds. + /// + /// + public double GetCurrentTime() + { + long currentTime; + QueryPerformanceCounter(out currentTime); + + return (double)(currentTime) / (double)_frequency; + } + + private long _frequency; + } + + public sealed class FrameTimer + { + [DllImport("winmm.dll", EntryPoint = "timeGetDevCaps", SetLastError = true)] + static extern UInt32 TimeGetDevCaps(ref TimeCaps timeCaps, UInt32 sizeTimeCaps); + + [DllImport("winmm.dll", EntryPoint = "timeBeginPeriod")] + static extern UInt32 TimeBeginPeriod(UInt32 uPeriod); + + [DllImport("winmm.dll", EntryPoint = "timeEndPeriod")] + public static extern uint TimeEndPeriod(uint uMilliseconds); + + [DllImport("kernel32.dll", EntryPoint = "CreateTimerQueue")] + public static extern IntPtr CreateTimerQueue(); + + [DllImport("kernel32.dll", EntryPoint = "DeleteTimerQueueEx")] + public static extern bool DeleteTimerQueue(IntPtr hTimerQueue, IntPtr hCompletionEvent); + + [DllImport("kernel32.dll", EntryPoint = "CreateTimerQueueTimer")] + public static extern bool CreateTimerQueueTimer(out IntPtr phNewTimer, IntPtr hTimerQueue, IntPtr Callback, IntPtr Parameter, UInt32 DueTime, UInt32 Period, ulong Flags); + + [DllImport("kernel32.dll", EntryPoint = "ChangeTimerQueueTimer")] + public static extern bool ChangeTimerQueueTimer(IntPtr hTimerQueue, IntPtr hTimer, UInt32 DueTime, UInt32 Period); + + + [DllImport("kernel32.dll", EntryPoint = "DeleteTimerQueueTimer")] + public static extern bool DeleteTimerQueueTimer(IntPtr hTimerQueue, IntPtr hTimer, IntPtr hCompletionEvent); + + [StructLayout(LayoutKind.Sequential)] + public struct TimeCaps + { + public UInt32 wPeriodMin; + public UInt32 wPeriodMax; + }; + + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + public delegate void UnmanagedTimerCallback(IntPtr param, bool timerOrWait); + + /// + /// FrameTimer provides a simple method to synchronize execution to a given framerate. + /// Calling WaitForFrame() blocks until the start of the next frame. + /// + /// NOTE: This code uses the Win32 TimerQueue APIs instead of the System.Threading.Timer + /// APIs because the .NET APIs do not allow execution of the callback on the timer's thread -- + /// it queues up a new worker thread. This lowers the accuracy of the timer, and since we + /// need all the precision we can get they're not suitable here. + /// + /// The frame rate to sync to. + public FrameTimer(double framesPerSecond) + { + // + // Set the timer to the minimum value (1ms). This should be supported on any modern x86 system. + // If not, too bad... + // + UInt32 res = TimeBeginPeriod(1); + + if (res != 0) + { + throw new InvalidOperationException("Unable to set timer period."); + } + + // + // Create a new timer queue + // + _hTimerQueue = CreateTimerQueue(); + + if (_hTimerQueue == IntPtr.Zero) + { + throw new InvalidOperationException("Unable to create timer queue."); + } + + // + // Since we only have a resolution of 1ms, we have to do some hackery to get a slightly more accurate framerate. + // (60 fields/sec requires 16 2/3s ms frame delay.) + // We alternate between two rates at varying intervals and this gets us fairly close to the desired frame rate. + // + _callback = new UnmanagedTimerCallback(TimerCallbackFn); + + _highPeriod = (uint)Math.Ceiling(1000.0 * (1.0 / framesPerSecond)); + _lowPeriod = (uint)Math.Floor(1000.0 * (1.0 / framesPerSecond)); + _periodTenths = _periodSwitch = (uint)((1000.0 * (1.0 / framesPerSecond) - Math.Floor(1000.0 * (1.0 / framesPerSecond))) * 10.0); + + if (!CreateTimerQueueTimer(out _hTimer, _hTimerQueue, Marshal.GetFunctionPointerForDelegate(_callback), IntPtr.Zero, _lowPeriod, _lowPeriod, 0x00000020 /* execute in timer thread */)) + { + throw new InvalidOperationException("Unable to create timer queue timer."); + } + + _event = new AutoResetEvent(false); + + _lowTimer = 0; + } + + ~FrameTimer() + { + // + // Clean stuff up + // + DeleteTimerQueueTimer(_hTimerQueue, _hTimer, IntPtr.Zero); + DeleteTimerQueue(_hTimerQueue, IntPtr.Zero); + + // + // Fire off a final event to release any call that's waiting... + // + _event.Set(); + } + + /// + /// Waits for the timer to fire. + /// + public void WaitForFrame() + { + _event.WaitOne(); + } + + /// + /// Callback from timer queue. Work done here is executed on the timer's thread, so must be quick. + /// + /// + /// + private void TimerCallbackFn(IntPtr lpParameter, bool TimerOrWaitFired) + { + _event.Set(); + _lowTimer++; + + if (_lowTimer >= _periodSwitch) + { + _lowTimer = 0; + _period = !_period; + ChangeTimerQueueTimer(_hTimerQueue, _hTimer, _period ? _lowPeriod : _highPeriod, _period ? _lowPeriod : _highPeriod); + + _periodSwitch = !_period ? _periodTenths : 10 - _periodTenths; + } + } + + private IntPtr _hTimerQueue; + private IntPtr _hTimer; + private AutoResetEvent _event; + private UnmanagedTimerCallback _callback; + private uint _lowPeriod; + private uint _highPeriod; + private uint _periodSwitch; + private uint _periodTenths; + private int _lowTimer; + private bool _period; + } +} diff --git a/Contralto/IO/EthernetController.cs b/Contralto/IO/EthernetController.cs index 5aa8310..a697cc9 100644 --- a/Contralto/IO/EthernetController.cs +++ b/Contralto/IO/EthernetController.cs @@ -25,10 +25,21 @@ namespace Contralto.IO // Attach real Ethernet device if user has specified one, otherwise leave unattached; output data // will go into a bit-bucket. - if (!String.IsNullOrEmpty(Configuration.HostEthernetInterfaceName)) + switch(Configuration.HostPacketInterfaceType) { - _hostEthernet = new HostEthernet(Configuration.HostEthernetInterfaceName); - _hostEthernet.RegisterReceiveCallback(OnHostPacketReceived); + case PacketInterfaceType.UDPEncapsulation: + _hostInterface = new UDPEncapsulation(Configuration.HostPacketInterfaceName); + _hostInterface.RegisterReceiveCallback(OnHostPacketReceived); + break; + + case PacketInterfaceType.EthernetEncapsulation: + _hostInterface = new HostEthernetEncapsulation(Configuration.HostPacketInterfaceName); + _hostInterface.RegisterReceiveCallback(OnHostPacketReceived); + break; + + default: + _hostInterface = null; + break; } // More words than the Alto will ever send. @@ -285,9 +296,9 @@ namespace Contralto.IO // And actually tell the host ethernet interface to send the data. // NOTE: We do not append a checksum to the outgoing 3mbit packet. See comments on the // receiving end for an explanation. - if (_hostEthernet != null) + if (_hostInterface != null) { - _hostEthernet.Send(_outputData, _outputIndex); + _hostInterface.Send(_outputData, _outputIndex); } _outputIndex = 0; @@ -526,8 +537,8 @@ namespace Contralto.IO private const int _maxQueuedPackets = 32; - // The actual connection to a real Ethernet device on the host - HostEthernet _hostEthernet; + // The actual connection to a real network device of some sort on the host + IPacketEncapsulation _hostInterface; // Buffer to hold outgoing data to the host ethernet ushort[] _outputData; diff --git a/Contralto/IO/HostEthernet.cs b/Contralto/IO/HostEthernetEncapsulation.cs similarity index 88% rename from Contralto/IO/HostEthernet.cs rename to Contralto/IO/HostEthernetEncapsulation.cs index e169930..f045381 100644 --- a/Contralto/IO/HostEthernet.cs +++ b/Contralto/IO/HostEthernetEncapsulation.cs @@ -18,11 +18,10 @@ namespace Contralto.IO public struct EthernetInterface { - public EthernetInterface(string name, string description, MacAddress macAddress) + public EthernetInterface(string name, string description) { Name = name; - Description = description; - MacAddress = macAddress; + Description = description; } public static List EnumerateDevices() @@ -31,33 +30,35 @@ namespace Contralto.IO foreach (LivePacketDevice device in LivePacketDevice.AllLocalMachine) { - interfaces.Add(new EthernetInterface(device.Name, device.Description, device.GetMacAddress())); + interfaces.Add(new EthernetInterface(device.Name, device.Description)); } return interfaces; } - public string Name; - public string Description; - public MacAddress MacAddress; - } + public override string ToString() + { + return Description; + } - public delegate void ReceivePacketDelegate(MemoryStream data); + public string Name; + public string Description; + } /// /// Implements the logic for encapsulating a 3mbit ethernet packet into a 10mb packet and sending it over an actual - /// interface controlled by the host operating system. + /// ethernet interface controlled by the host operating system. /// /// This uses PCap.NET to do the dirty work. /// - public class HostEthernet + public class HostEthernetEncapsulation : IPacketEncapsulation { - public HostEthernet(EthernetInterface iface) + public HostEthernetEncapsulation(EthernetInterface iface) { AttachInterface(iface); } - public HostEthernet(string name) + public HostEthernetEncapsulation(string name) { // Find the specified device by name List interfaces = EthernetInterface.EnumerateDevices(); @@ -71,7 +72,7 @@ namespace Contralto.IO } } - throw new InvalidOperationException("Specified ethernet interface does not exist."); + throw new InvalidOperationException("Specified ethernet interface does not exist or is not compatible with WinPCAP."); } public void RegisterReceiveCallback(ReceivePacketDelegate callback) @@ -127,7 +128,7 @@ namespace Contralto.IO byte destinationHost = packetBytes[3]; byte sourceHost = packetBytes[2]; - Log.Write(LogComponent.HostEthernet, "Sending packet; source {0} destination {1}, length {2} words.", + Log.Write(LogComponent.HostNetworkInterface, "Sending packet; source {0} destination {1}, length {2} words.", Conversion.ToOctal(sourceHost), Conversion.ToOctal(destinationHost), length); @@ -153,7 +154,7 @@ namespace Contralto.IO // Send it over the 'net! _communicator.SendPacket(builder.Build(DateTime.Now)); - Log.Write(LogComponent.HostEthernet, "Encapsulated 3mbit packet sent."); + Log.Write(LogComponent.HostNetworkInterface, "Encapsulated 3mbit packet sent."); } private void ReceiveCallback(Packet p) @@ -166,7 +167,7 @@ namespace Contralto.IO p.Ethernet.Destination.ToValue() == _10mbitBroadcast) && // broadcast (p.Ethernet.Source.ToValue() != (UInt48)(_10mbitMACPrefix | Configuration.HostAddress))) // and not sent by this emulator { - Log.Write(LogComponent.HostEthernet, "Received encapsulated 3mbit packet."); + Log.Write(LogComponent.HostNetworkInterface, "Received encapsulated 3mbit packet."); _callback(p.Ethernet.Payload.ToMemoryStream()); } else @@ -182,7 +183,7 @@ namespace Contralto.IO // Find the specified device by name foreach (LivePacketDevice device in LivePacketDevice.AllLocalMachine) { - if (device.Name == iface.Name && device.GetMacAddress() == iface.MacAddress) + if (device.Description == iface.Name) { _interface = device; break; @@ -194,7 +195,7 @@ namespace Contralto.IO throw new InvalidOperationException("Requested interface not found."); } - Log.Write(LogComponent.HostEthernet, "Attached to host interface {0}", iface.Name); + Log.Write(LogComponent.HostNetworkInterface, "Attached to host interface {0}", iface.Name); } private void Open(bool promiscuous, int timeout) @@ -204,7 +205,7 @@ namespace Contralto.IO // Set this to 1 so we'll get packets as soon as they arrive, no buffering. _communicator.SetKernelMinimumBytesToCopy(1); - Log.Write(LogComponent.HostEthernet, "Host interface opened and receiving packets."); + Log.Write(LogComponent.HostNetworkInterface, "Host interface opened and receiving packets."); } /// @@ -222,7 +223,7 @@ namespace Contralto.IO // Just call ReceivePackets, that's it. This will never return. // (probably need to make this more elegant so we can tear down the thread // properly.) - Log.Write(LogComponent.HostEthernet, "Receiver thread started."); + Log.Write(LogComponent.HostNetworkInterface, "Receiver thread started."); _communicator.ReceivePackets(-1, ReceiveCallback); } diff --git a/Contralto/IO/IPacketEncapsulation.cs b/Contralto/IO/IPacketEncapsulation.cs new file mode 100644 index 0000000..8e862c6 --- /dev/null +++ b/Contralto/IO/IPacketEncapsulation.cs @@ -0,0 +1,23 @@ +using System.IO; + + +namespace Contralto.IO +{ + public delegate void ReceivePacketDelegate(MemoryStream data); + + public interface IPacketEncapsulation + { + /// + /// Registers a callback delegate to handle packets that are received. + /// + /// + void RegisterReceiveCallback(ReceivePacketDelegate callback); + + /// + /// Sends the specified word array + /// + /// + /// + void Send(ushort[] packet, int length); + } +} diff --git a/Contralto/IO/UDPEncapsulation.cs b/Contralto/IO/UDPEncapsulation.cs new file mode 100644 index 0000000..fc571e1 --- /dev/null +++ b/Contralto/IO/UDPEncapsulation.cs @@ -0,0 +1,208 @@ +using System; +using System.Net; +using System.Net.Sockets; + +using Contralto.Logging; +using System.Threading; +using System.Net.NetworkInformation; + +namespace Contralto.IO +{ + /// + /// Implements the logic for encapsulating a 3mbit ethernet packet into/out of UDP datagrams. + /// Sent packets are broadcast to the subnet. + /// + public class UDPEncapsulation : IPacketEncapsulation + { + public UDPEncapsulation(string interfaceName) + { + // Try to set up UDP client. + try + { + _udpClient = new UdpClient(_udpPort, AddressFamily.InterNetwork); + _udpClient.EnableBroadcast = true; + + // + // Grab the broadcast address for the interface so that we know what broadcast address to use + // for our UDP datagrams. + // + NetworkInterface[] nics = NetworkInterface.GetAllNetworkInterfaces(); + + IPInterfaceProperties props = null; + foreach(NetworkInterface nic in nics) + { + if (nic.Description.ToLowerInvariant() == interfaceName.ToLowerInvariant()) + { + props = nic.GetIPProperties(); + break; + } + } + + if (props == null) + { + throw new InvalidOperationException(String.Format("No interface matching description '{0}' was found.", interfaceName)); + } + + foreach(UnicastIPAddressInformation unicast in props.UnicastAddresses) + { + // Find the first InterNetwork address for this interface and + // go with it. + if (unicast.Address.AddressFamily == AddressFamily.InterNetwork) + { + _broadcastEndpoint = new IPEndPoint(GetBroadcastAddress(unicast.Address, unicast.IPv4Mask), _udpPort); + break; + } + } + + if (_broadcastEndpoint == null) + { + throw new InvalidOperationException(String.Format("No IPV4 network information was found for interface '{0}'.", interfaceName)); + } + + } + catch(Exception e) + { + Log.Write(LogType.Error, LogComponent.EthernetPacket, + "Error configuring UDP socket {0} for use with ContrAlto on interface {1}. Ensure that the selected network interface is valid, configured properly, and that nothing else is using this port.", + _udpPort, + interfaceName); + + Log.Write(LogType.Error, LogComponent.EthernetPacket, + "Error was '{0}'.", + e.Message); + + _udpClient = null; + } + } + + public void RegisterReceiveCallback(ReceivePacketDelegate callback) + { + // UDP connection could not be configured, can't receive anything. + if (_udpClient == null) + { + return; + } + + // Set up input + _callback = callback; + BeginReceive(); + } + + + /// + /// Sends an array of bytes over the ethernet as a 3mbit packet encapsulated in a 10mbit packet. + /// + /// + /// + public void Send(ushort[] packet, int length) + { + // UDP could not be configured, drop the packet. + if (_udpClient == null) + { + return; + } + + // Sanity check. + if (length < 1) + { + throw new InvalidOperationException("Raw packet data must contain at least two bytes for addressing."); + } + + // + // Outgoing packet contains 1 extra word (2 bytes) containing + // the prepended packet length (one word) + byte[] packetBytes = new byte[length * 2 + 2]; + + // + // First two bytes include the length of the 3mbit packet; since 10mbit packets have a minimum length of 46 + // bytes, and 3mbit packets have no minimum length this is necessary so the receiver can pull out the + // correct amount of data. + // + packetBytes[0] = (byte)(length); + packetBytes[1] = (byte)((length) >> 8); + + // + // Do this annoying dance to stuff the ushorts into bytes because this is C#. + // + for (int i = 0; i < length; i++) + { + packetBytes[i * 2 + 2] = (byte)(packet[i]); + packetBytes[i * 2 + 3] = (byte)(packet[i] >> 8); + } + + // + // Grab the source and destination host addresses from the packet we're sending + // and build 10mbit versions. + // + byte destinationHost = packetBytes[3]; + byte sourceHost = packetBytes[2]; + + Log.Write(LogComponent.HostNetworkInterface, "Sending packet; source {0} destination {1}, length {2} words.", + Conversion.ToOctal(sourceHost), + Conversion.ToOctal(destinationHost), + length); + + _udpClient.Send(packetBytes, packetBytes.Length, _broadcastEndpoint); + + Log.Write(LogComponent.HostNetworkInterface, "Encapsulated 3mbit packet sent via UDP."); + } + + /// + /// Begin receiving packets, forever. + /// + private void BeginReceive() + { + // Kick off receive thread. + _receiveThread = new Thread(ReceiveThread); + _receiveThread.Start(); + } + + private void ReceiveThread() + { + // Just call ReceivePackets, that's it. This will never return. + // (probably need to make this more elegant so we can tear down the thread + // properly.) + Log.Write(LogComponent.HostNetworkInterface, "UDP Receiver thread started."); + + IPEndPoint groupEndPoint = new IPEndPoint(IPAddress.Any, _udpPort); + //return; + + while (true) + { + byte[] data = _udpClient.Receive(ref groupEndPoint); + + // TODO: sanitize data before handing it off. + + Log.Write(LogComponent.HostNetworkInterface, "Received UDP-encapsulated 3mbit packet."); + _callback(new System.IO.MemoryStream(data)); + } + } + + + private IPAddress GetBroadcastAddress(IPAddress address, IPAddress subnetMask) + { + byte[] ipAdressBytes = address.GetAddressBytes(); + byte[] subnetMaskBytes = subnetMask.GetAddressBytes(); + + byte[] broadcastAddress = new byte[ipAdressBytes.Length]; + for (int i = 0; i < broadcastAddress.Length; i++) + { + broadcastAddress[i] = (byte)(ipAdressBytes[i] | (subnetMaskBytes[i] ^ 255)); + } + + return new IPAddress(broadcastAddress); + } + + // Callback delegate for received data. + private ReceivePacketDelegate _callback; + + // Thread used for receive + private Thread _receiveThread; + + // UDP port (TODO: make configurable?) + private const int _udpPort = 42424; + private UdpClient _udpClient; + private IPEndPoint _broadcastEndpoint; + + } +} diff --git a/Contralto/Logging/Log.cs b/Contralto/Logging/Log.cs index c7e2012..16ef657 100644 --- a/Contralto/Logging/Log.cs +++ b/Contralto/Logging/Log.cs @@ -25,8 +25,9 @@ namespace Contralto.Logging EthernetController = 0x400, EthernetTask = 0x800, TaskSwitch = 0x1000, - HostEthernet = 0x2000, + HostNetworkInterface = 0x2000, EthernetPacket = 0x4000, + Configuration = 0x8000, Debug = 0x40000000, All = 0x7fffffff diff --git a/Contralto/Program.cs b/Contralto/Program.cs index 76c3a85..b52c998 100644 --- a/Contralto/Program.cs +++ b/Contralto/Program.cs @@ -1,6 +1,7 @@ using Contralto.CPU; using Contralto.IO; using System; +using System.Net; using System.Collections.Generic; namespace Contralto @@ -12,11 +13,9 @@ namespace Contralto { // Handle command-line args PrintHerald(); - // See if WinPCap is installed and working - TestPCap(); - ParseCommandLine(args); - + // See if WinPCap is installed and working + TestPCap(); AltoSystem system = new AltoSystem(); @@ -44,99 +43,7 @@ namespace Contralto Console.WriteLine("ContrAlto v0.1 (c) 2015, 2016 Living Computer Museum."); Console.WriteLine("Bug reports to joshd@livingcomputermuseum.org"); Console.WriteLine(); - } - - private static void ParseCommandLine(string[] args) - { - // At the moment, options start with a "-" and are one of: - // "-hostaddress
" : specifies ethernet host address in octal (1-377) - // "-hostinterface " : specifies the name of the host ethernet interface to use - // "-listinterfaces" : lists ethernet interfaces known by pcap - // "-drive0 " : attaches disk image to drive 0 - // "-drive1 " : attaches disk image to drive 1 - - int index = 0; - - // TODO: this parsing needs to be made not terrible. - while(index < args.Length) - { - switch (args[index++].ToLower()) - { - case "-hostaddress": - if (index < args.Length) - { - Configuration.HostAddress = Convert.ToByte(args[index++], 8); - } - else - { - PrintUsage(); - } - break; - - case "-hostinterface": - if (index < args.Length) - { - if (!Configuration.HostEthernetAvailable) - { - Console.WriteLine("Ethernet functionality is disabled, host interface cannot be specified."); - } - else - { - Configuration.HostEthernetInterfaceName = args[index++]; - } - } - else - { - PrintUsage(); - } - break; - - case "-listinterfaces": - if (!Configuration.HostEthernetAvailable) - { - Console.WriteLine("Ethernet functionality is disabled, interfaces cannot be enumerated."); - } - else - { - List interfaces = EthernetInterface.EnumerateDevices(); - - foreach (EthernetInterface i in interfaces) - { - Console.WriteLine("Name: '{0}'\n Description: '{1}'\n MAC '{2}'", i.Name, i.Description, i.MacAddress); - } - } - break; - - case "-drive0": - if (index < args.Length) - { - Configuration.Drive0Image = args[index++]; - } - else - { - PrintUsage(); - } - break; - - case "-drive1": - if (index < args.Length) - { - Configuration.Drive1Image = args[index++]; - } - else - { - PrintUsage(); - } - break; - } - } - } - - private static void PrintUsage() - { - // TODO: make more useful. - Console.WriteLine("Something is wrong, try again."); - } + } private static void TestPCap() { @@ -145,7 +52,7 @@ namespace Contralto try { List interfaces = EthernetInterface.EnumerateDevices(); - Configuration.HostEthernetAvailable = true; + Configuration.HostRawEthernetInterfacesAvailable = true; } catch { @@ -153,7 +60,7 @@ namespace Contralto Console.WriteLine(" Ethernet functionality is disabled."); Console.WriteLine(" Please install WinPCAP from: http://www.winpcap.org/"); - Configuration.HostEthernetAvailable = false; + Configuration.HostRawEthernetInterfacesAvailable = false; } } } diff --git a/Contralto/Properties/Resources.Designer.cs b/Contralto/Properties/Resources.Designer.cs new file mode 100644 index 0000000..0235d95 --- /dev/null +++ b/Contralto/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Contralto.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Contralto.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/Contralto/UI/EthernetBootWindow.resx b/Contralto/Properties/Resources.resx similarity index 100% rename from Contralto/UI/EthernetBootWindow.resx rename to Contralto/Properties/Resources.resx diff --git a/Contralto/UI/EthernetBootWindow.Designer.cs b/Contralto/UI/AlternateBootWindow.Designer.cs similarity index 96% rename from Contralto/UI/EthernetBootWindow.Designer.cs rename to Contralto/UI/AlternateBootWindow.Designer.cs index 3850640..d4bf01e 100644 --- a/Contralto/UI/EthernetBootWindow.Designer.cs +++ b/Contralto/UI/AlternateBootWindow.Designer.cs @@ -1,6 +1,6 @@ namespace Contralto { - partial class EthernetBootWindow + partial class AlternateBootOptions { /// /// Required designer variable. @@ -99,16 +99,16 @@ this.CancelButton.UseVisualStyleBackColor = true; this.CancelButton.Click += new System.EventHandler(this.CancelButton_Click); // - // EthernetBootWindow + // AlternateBootOptions // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(413, 130); + this.ClientSize = new System.Drawing.Size(403, 128); this.Controls.Add(this.CancelButton); this.Controls.Add(this.OKButton); this.Controls.Add(this.BootFileGroup); this.Controls.Add(this.EthernetBootEnabled); - this.Name = "EthernetBootWindow"; + this.Name = "AlternateBootOptions"; this.ShowIcon = false; this.ShowInTaskbar = false; this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide; diff --git a/Contralto/UI/EthernetBootWindow.cs b/Contralto/UI/AlternateBootWindow.cs similarity index 95% rename from Contralto/UI/EthernetBootWindow.cs rename to Contralto/UI/AlternateBootWindow.cs index c7964a8..cf5bbc8 100644 --- a/Contralto/UI/EthernetBootWindow.cs +++ b/Contralto/UI/AlternateBootWindow.cs @@ -10,9 +10,9 @@ using System.Windows.Forms; namespace Contralto { - public partial class EthernetBootWindow : Form + public partial class AlternateBootOptions : Form { - public EthernetBootWindow() + public AlternateBootOptions() { InitializeComponent(); @@ -20,7 +20,7 @@ namespace Contralto BootFileGroup.Enabled = EthernetBootEnabled.Checked = Configuration.EthernetBootEnabled; - SelectBootFile(Configuration.EthernetBootFile); + SelectBootFile(Configuration.BootAddress); } @@ -79,7 +79,7 @@ namespace Contralto _selectedBoot = ((BootFileEntry)BootFileComboBox.SelectedItem).FileNumber; } - Configuration.EthernetBootFile = _selectedBoot; + Configuration.BootAddress = _selectedBoot; Configuration.EthernetBootEnabled = _bootEnabled; this.Close(); diff --git a/Contralto/UI/AlternateBootWindow.resx b/Contralto/UI/AlternateBootWindow.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/Contralto/UI/AlternateBootWindow.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Contralto/UI/AltoWindow.Designer.cs b/Contralto/UI/AltoWindow.Designer.cs index d60d8e0..1777900 100644 --- a/Contralto/UI/AltoWindow.Designer.cs +++ b/Contralto/UI/AltoWindow.Designer.cs @@ -44,6 +44,8 @@ this.loadToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.unloadToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.Drive1ImageName = new System.Windows.Forms.ToolStripMenuItem(); + this.enableAlternateBootToolStripMenuItem = 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.helpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -52,7 +54,6 @@ this.CaptureStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); this.SystemStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); this.DiskStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); - this.SystemEthernetBootMenu = new System.Windows.Forms.ToolStripMenuItem(); ((System.ComponentModel.ISupportInitialize)(this.DisplayBox)).BeginInit(); this.menuStrip1.SuspendLayout(); this.StatusLine.SuspendLayout(); @@ -93,7 +94,7 @@ // exitToolStripMenuItem // this.exitToolStripMenuItem.Name = "exitToolStripMenuItem"; - this.exitToolStripMenuItem.Size = new System.Drawing.Size(152, 22); + this.exitToolStripMenuItem.Size = new System.Drawing.Size(92, 22); this.exitToolStripMenuItem.Text = "Exit"; this.exitToolStripMenuItem.Click += new System.EventHandler(this.OnFileExitClick); // @@ -104,6 +105,7 @@ this.SystemResetMenuItem, this.drive0ToolStripMenuItem, this.drive1ToolStripMenuItem, + this.enableAlternateBootToolStripMenuItem, this.SystemEthernetBootMenu, this.optionsToolStripMenuItem, this.SystemShowDebuggerMenuItem}); @@ -176,14 +178,14 @@ // loadToolStripMenuItem // this.loadToolStripMenuItem.Name = "loadToolStripMenuItem"; - this.loadToolStripMenuItem.Size = new System.Drawing.Size(152, 22); + this.loadToolStripMenuItem.Size = new System.Drawing.Size(134, 22); this.loadToolStripMenuItem.Text = "Load..."; this.loadToolStripMenuItem.Click += new System.EventHandler(this.OnSystemDrive1LoadClick); // // unloadToolStripMenuItem // this.unloadToolStripMenuItem.Name = "unloadToolStripMenuItem"; - this.unloadToolStripMenuItem.Size = new System.Drawing.Size(152, 22); + this.unloadToolStripMenuItem.Size = new System.Drawing.Size(134, 22); this.unloadToolStripMenuItem.Text = "Unload..."; this.unloadToolStripMenuItem.Click += new System.EventHandler(this.OnSystemDrive1UnloadClick); // @@ -191,15 +193,28 @@ // this.Drive1ImageName.Enabled = false; this.Drive1ImageName.Name = "Drive1ImageName"; - this.Drive1ImageName.Size = new System.Drawing.Size(152, 22); + this.Drive1ImageName.Size = new System.Drawing.Size(134, 22); this.Drive1ImageName.Text = "Image Name"; // + // enableAlternateBootToolStripMenuItem + // + this.enableAlternateBootToolStripMenuItem.Name = "enableAlternateBootToolStripMenuItem"; + this.enableAlternateBootToolStripMenuItem.Size = new System.Drawing.Size(210, 22); + this.enableAlternateBootToolStripMenuItem.Text = "Enable Alternate Boot"; + // + // SystemEthernetBootMenu + // + this.SystemEthernetBootMenu.Name = "SystemEthernetBootMenu"; + this.SystemEthernetBootMenu.Size = new System.Drawing.Size(210, 22); + this.SystemEthernetBootMenu.Text = "Alternate Boot Options..."; + this.SystemEthernetBootMenu.Click += new System.EventHandler(this.OnAlternateBootOptionsClicked); + // // optionsToolStripMenuItem // - this.optionsToolStripMenuItem.Enabled = false; this.optionsToolStripMenuItem.Name = "optionsToolStripMenuItem"; this.optionsToolStripMenuItem.Size = new System.Drawing.Size(210, 22); this.optionsToolStripMenuItem.Text = "Options..."; + this.optionsToolStripMenuItem.Click += new System.EventHandler(this.OnSystemOptionsClick); // // SystemShowDebuggerMenuItem // @@ -221,7 +236,7 @@ // aboutToolStripMenuItem // this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem"; - this.aboutToolStripMenuItem.Size = new System.Drawing.Size(152, 22); + this.aboutToolStripMenuItem.Size = new System.Drawing.Size(103, 22); this.aboutToolStripMenuItem.Text = "About"; this.aboutToolStripMenuItem.Click += new System.EventHandler(this.OnHelpAboutClick); // @@ -256,13 +271,6 @@ this.DiskStatusLabel.Size = new System.Drawing.Size(82, 17); this.DiskStatusLabel.Text = "DiskStatusLabel"; // - // SystemEthernetBootMenu - // - this.SystemEthernetBootMenu.Name = "SystemEthernetBootMenu"; - this.SystemEthernetBootMenu.Size = new System.Drawing.Size(210, 22); - this.SystemEthernetBootMenu.Text = "Ethernet Boot..."; - this.SystemEthernetBootMenu.Click += new System.EventHandler(this.OnEthernetBootClicked); - // // AltoWindow // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); @@ -320,5 +328,6 @@ private System.Windows.Forms.ToolStripStatusLabel SystemStatusLabel; private System.Windows.Forms.ToolStripStatusLabel DiskStatusLabel; private System.Windows.Forms.ToolStripMenuItem SystemEthernetBootMenu; + private System.Windows.Forms.ToolStripMenuItem enableAlternateBootToolStripMenuItem; } } \ No newline at end of file diff --git a/Contralto/UI/AltoWindow.cs b/Contralto/UI/AltoWindow.cs index 72bd622..41e5597 100644 --- a/Contralto/UI/AltoWindow.cs +++ b/Contralto/UI/AltoWindow.cs @@ -1,17 +1,12 @@ using Contralto.CPU; using Contralto.Display; using Contralto.IO; +using Contralto.UI; using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Data; using System.Drawing; using System.Drawing.Imaging; using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; using System.Windows.Forms; namespace Contralto @@ -29,10 +24,17 @@ namespace Contralto _displayBuffer = new Bitmap(608, 808, PixelFormat.Format1bppIndexed); DisplayBox.Image = _displayBuffer; + _lastBuffer = _currentBuffer = _displayData0; + _frame = 0; + + _frameTimer = new FrameTimer(60.0); + ReleaseMouse(); SystemStatusLabel.Text = _systemStoppedText; DiskStatusLabel.Text = String.Empty; + + this.DoubleBuffered = true; } public void AttachSystem(AltoSystem system) @@ -135,9 +137,9 @@ namespace Contralto Drive1ImageName.Text = _noImageLoadedText; } - private void OnEthernetBootClicked(object sender, EventArgs e) + private void OnAlternateBootOptionsClicked(object sender, EventArgs e) { - EthernetBootWindow bootWindow = new EthernetBootWindow(); + AlternateBootOptions bootWindow = new AlternateBootOptions(); bootWindow.ShowDialog(); // @@ -146,6 +148,12 @@ namespace Contralto _system.PressBootKeys(); } + private void OnSystemOptionsClick(object sender, EventArgs e) + { + SystemOptions optionsWindow = new SystemOptions(); + optionsWindow.ShowDialog(); + } + private void OnHelpAboutClick(object sender, EventArgs e) { AboutBox about = new AboutBox(); @@ -185,14 +193,21 @@ namespace Contralto private void OnFileExitClick(object sender, EventArgs e) { _controller.StopExecution(); - this.Close(); + this.Close(); } private void OnAltoWindowClosed(object sender, FormClosedEventArgs e) { + // Halt the system and detach our display + _controller.StopExecution(); + _system.DetachDisplay(); + // Commit loaded packs back to disk CommitDiskPack(0); - CommitDiskPack(1); + CommitDiskPack(1); + + this.Dispose(); + Application.Exit(); } private string ShowImageLoadDialog(int drive) @@ -259,7 +274,28 @@ namespace Contralto public void Render() { - BeginInvoke(new DisplayDelegate(RefreshDisplayBox)); + _frame++; + + // Wait for the next frame + _frameTimer.WaitForFrame(); + + if (Configuration.InterlaceDisplay) + { + // Flip the back-buffer + if ((_frame % 2) == 0) + { + _currentBuffer = _displayData0; + _lastBuffer = _displayData1; + } + else + { + _currentBuffer = _displayData1; + _lastBuffer = _displayData0; + } + } + + // Asynchronously render this frame. + BeginInvoke(new DisplayDelegate(RefreshDisplayBox)); } private void RefreshDisplayBox() @@ -268,13 +304,16 @@ namespace Contralto BitmapData data = _displayBuffer.LockBits(_displayRect, ImageLockMode.WriteOnly, PixelFormat.Format1bppIndexed); IntPtr ptr = data.Scan0; - System.Runtime.InteropServices.Marshal.Copy(_displayData, 0, ptr, _displayData.Length - 4); + System.Runtime.InteropServices.Marshal.Copy(_lastBuffer, 0, ptr, _lastBuffer.Length - 4); _displayBuffer.UnlockBits(data); - DisplayBox.Refresh(); + DisplayBox.Refresh(); - // If you want interlacing to be more visible: - //Array.Clear(_displayData, 0, _displayData.Length); + // Clear the buffer if we're displaying in fakey-"interlaced" mode. + if (Configuration.InterlaceDisplay) + { + Array.Clear(_lastBuffer, 0, _lastBuffer.Length); + } } /// @@ -292,29 +331,29 @@ namespace Contralto // Low resolution; double up pixels. int address = scanline * 76 + wordOffset * 4; - if (address > _displayData.Length) + if (address > _currentBuffer.Length) { throw new InvalidOperationException("Display word address is out of bounds."); } UInt32 stretched = StretchWord(word); - _displayData[address] = (byte)(stretched >> 24); - _displayData[address + 1] = (byte)(stretched >> 16); - _displayData[address + 2] = (byte)(stretched >> 8); - _displayData[address + 3] = (byte)(stretched); + _currentBuffer[address] = (byte)(stretched >> 24); + _currentBuffer[address + 1] = (byte)(stretched >> 16); + _currentBuffer[address + 2] = (byte)(stretched >> 8); + _currentBuffer[address + 3] = (byte)(stretched); } else { int address = scanline * 76 + wordOffset * 2; - if (address > _displayData.Length) + if (address > _currentBuffer.Length) { throw new InvalidOperationException("Display word address is out of bounds."); } - _displayData[address] = (byte)(word >> 8); - _displayData[address + 1] = (byte)(word); + _currentBuffer[address] = (byte)(word >> 8); + _currentBuffer[address + 1] = (byte)(word); } } @@ -333,19 +372,19 @@ namespace Contralto // Grab the 32 bits straddling the cursor from the display buffer // so we can merge the 16 cursor bits in. // - UInt32 displayWord = (UInt32)((_displayData[address] << 24) | - (_displayData[address + 1] << 16) | - (_displayData[address + 2] << 8) | - _displayData[address + 3]); + UInt32 displayWord = (UInt32)((_currentBuffer[address] << 24) | + (_currentBuffer[address + 1] << 16) | + (_currentBuffer[address + 2] << 8) | + _currentBuffer[address + 3]); UInt32 longcursorWord = (UInt32)(cursorWord << 16); displayWord ^= (longcursorWord >> (xOffset % 8)); - _displayData[address] = (byte)(displayWord >> 24); - _displayData[address + 1] = (byte)(displayWord >> 16); - _displayData[address + 2] = (byte)(displayWord >> 8); - _displayData[address + 3] = (byte)(displayWord); + _currentBuffer[address] = (byte)(displayWord >> 24); + _currentBuffer[address + 1] = (byte)(displayWord >> 16); + _currentBuffer[address + 2] = (byte)(displayWord >> 8); + _currentBuffer[address + 3] = (byte)(displayWord); } /// @@ -696,11 +735,19 @@ namespace Contralto // Display related data. // Note: display is actually 606 pixels wide, but that's not an even multiple of 8, so we round up. - private byte[] _displayData = new byte[808 * 76 + 4]; // + 4 to make cursor display logic simpler. + // Two backbuffers and references to the current / last buffer for rendering + private byte[] _displayData0 = new byte[808 * 76 + 4]; // + 4 to make cursor display logic simpler. + private byte[] _displayData1 = new byte[808 * 76 + 4]; // + 4 to make cursor display logic simpler. + private byte[] _currentBuffer; + private byte[] _lastBuffer; + private int _frame; private Bitmap _displayBuffer; private Rectangle _displayRect = new Rectangle(0, 0, 608, 808); private delegate void DisplayDelegate(); + // Speed throttling + FrameTimer _frameTimer; + // Input related data // Keyboard mapping from windows vkeys to Alto keys @@ -725,6 +772,5 @@ namespace Contralto private const string _systemRunningText = "Alto Running."; private const string _systemErrorText = "Alto Stopped due to error. See Debugger."; - } } diff --git a/Contralto/UI/SystemOptions.Designer.cs b/Contralto/UI/SystemOptions.Designer.cs new file mode 100644 index 0000000..7df345f --- /dev/null +++ b/Contralto/UI/SystemOptions.Designer.cs @@ -0,0 +1,324 @@ +namespace Contralto.UI +{ + partial class SystemOptions + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.tabControl1 = new System.Windows.Forms.TabControl(); + this.tabPage1 = new System.Windows.Forms.TabPage(); + this.AltoII3KRAMRadioButton = new System.Windows.Forms.RadioButton(); + this.AltoII2KROMRadioButton = new System.Windows.Forms.RadioButton(); + this.label1 = new System.Windows.Forms.Label(); + this.AltoII1KROMRadioButton = new System.Windows.Forms.RadioButton(); + this.tabPage2 = new System.Windows.Forms.TabPage(); + this.groupBox1 = new System.Windows.Forms.GroupBox(); + this.RawEthernetRadioButton = new System.Windows.Forms.RadioButton(); + this.UDPRadioButton = new System.Windows.Forms.RadioButton(); + this.HostInterfaceGroupBox = new System.Windows.Forms.GroupBox(); + this.EthernetInterfaceListBox = new System.Windows.Forms.ListBox(); + this.label4 = new System.Windows.Forms.Label(); + this.label3 = new System.Windows.Forms.Label(); + this.AltoEthernetAddressTextBox = new System.Windows.Forms.TextBox(); + this.tabPage3 = new System.Windows.Forms.TabPage(); + this.ThrottleSpeedCheckBox = new System.Windows.Forms.CheckBox(); + this.InterlaceDisplayCheckBox = new System.Windows.Forms.CheckBox(); + this.OKButton = new System.Windows.Forms.Button(); + this.CancelButton = new System.Windows.Forms.Button(); + this.tabControl1.SuspendLayout(); + this.tabPage1.SuspendLayout(); + this.tabPage2.SuspendLayout(); + this.groupBox1.SuspendLayout(); + this.HostInterfaceGroupBox.SuspendLayout(); + this.tabPage3.SuspendLayout(); + this.SuspendLayout(); + // + // tabControl1 + // + this.tabControl1.Controls.Add(this.tabPage1); + this.tabControl1.Controls.Add(this.tabPage2); + this.tabControl1.Controls.Add(this.tabPage3); + this.tabControl1.Location = new System.Drawing.Point(3, 5); + this.tabControl1.Name = "tabControl1"; + this.tabControl1.SelectedIndex = 0; + this.tabControl1.Size = new System.Drawing.Size(368, 227); + this.tabControl1.TabIndex = 0; + // + // tabPage1 + // + this.tabPage1.Controls.Add(this.AltoII3KRAMRadioButton); + this.tabPage1.Controls.Add(this.AltoII2KROMRadioButton); + this.tabPage1.Controls.Add(this.label1); + this.tabPage1.Controls.Add(this.AltoII1KROMRadioButton); + this.tabPage1.Location = new System.Drawing.Point(4, 22); + this.tabPage1.Name = "tabPage1"; + this.tabPage1.Padding = new System.Windows.Forms.Padding(3); + this.tabPage1.Size = new System.Drawing.Size(360, 201); + this.tabPage1.TabIndex = 0; + this.tabPage1.Text = "CPU"; + this.tabPage1.UseVisualStyleBackColor = true; + // + // AltoII3KRAMRadioButton + // + this.AltoII3KRAMRadioButton.AutoSize = true; + this.AltoII3KRAMRadioButton.Location = new System.Drawing.Point(14, 75); + this.AltoII3KRAMRadioButton.Name = "AltoII3KRAMRadioButton"; + this.AltoII3KRAMRadioButton.Size = new System.Drawing.Size(217, 17); + this.AltoII3KRAMRadioButton.TabIndex = 3; + this.AltoII3KRAMRadioButton.TabStop = true; + this.AltoII3KRAMRadioButton.Text = "Alto II, 1K Control ROM, 3K Control RAM"; + this.AltoII3KRAMRadioButton.UseVisualStyleBackColor = true; + this.AltoII3KRAMRadioButton.CheckedChanged += new System.EventHandler(this.OnSystemTypeCheckChanged); + // + // AltoII2KROMRadioButton + // + this.AltoII2KROMRadioButton.AutoSize = true; + this.AltoII2KROMRadioButton.Location = new System.Drawing.Point(14, 52); + this.AltoII2KROMRadioButton.Name = "AltoII2KROMRadioButton"; + this.AltoII2KROMRadioButton.Size = new System.Drawing.Size(217, 17); + this.AltoII2KROMRadioButton.TabIndex = 2; + this.AltoII2KROMRadioButton.TabStop = true; + this.AltoII2KROMRadioButton.Text = "Alto II, 2K Control ROM, 1K Control RAM"; + this.AltoII2KROMRadioButton.UseVisualStyleBackColor = true; + this.AltoII2KROMRadioButton.CheckedChanged += new System.EventHandler(this.OnSystemTypeCheckChanged); + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(11, 13); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(108, 13); + this.label1.TabIndex = 1; + this.label1.Text = "System configuration:"; + // + // AltoII1KROMRadioButton + // + this.AltoII1KROMRadioButton.AutoSize = true; + this.AltoII1KROMRadioButton.Location = new System.Drawing.Point(14, 29); + this.AltoII1KROMRadioButton.Name = "AltoII1KROMRadioButton"; + this.AltoII1KROMRadioButton.Size = new System.Drawing.Size(217, 17); + this.AltoII1KROMRadioButton.TabIndex = 0; + this.AltoII1KROMRadioButton.TabStop = true; + this.AltoII1KROMRadioButton.Text = "Alto II, 1K Control ROM, 1K Control RAM"; + this.AltoII1KROMRadioButton.UseVisualStyleBackColor = true; + this.AltoII1KROMRadioButton.CheckedChanged += new System.EventHandler(this.OnSystemTypeCheckChanged); + // + // tabPage2 + // + this.tabPage2.Controls.Add(this.groupBox1); + this.tabPage2.Controls.Add(this.HostInterfaceGroupBox); + this.tabPage2.Controls.Add(this.label3); + this.tabPage2.Controls.Add(this.AltoEthernetAddressTextBox); + this.tabPage2.Location = new System.Drawing.Point(4, 22); + this.tabPage2.Name = "tabPage2"; + this.tabPage2.Padding = new System.Windows.Forms.Padding(3); + this.tabPage2.Size = new System.Drawing.Size(360, 201); + this.tabPage2.TabIndex = 1; + this.tabPage2.Text = "Ethernet"; + this.tabPage2.UseVisualStyleBackColor = true; + // + // groupBox1 + // + this.groupBox1.Controls.Add(this.RawEthernetRadioButton); + this.groupBox1.Controls.Add(this.UDPRadioButton); + this.groupBox1.Location = new System.Drawing.Point(10, 30); + this.groupBox1.Name = "groupBox1"; + this.groupBox1.Size = new System.Drawing.Size(335, 39); + this.groupBox1.TabIndex = 3; + this.groupBox1.TabStop = false; + this.groupBox1.Text = "Ethernet Encapsulation"; + // + // RawEthernetRadioButton + // + this.RawEthernetRadioButton.AutoSize = true; + this.RawEthernetRadioButton.Location = new System.Drawing.Point(77, 16); + this.RawEthernetRadioButton.Name = "RawEthernetRadioButton"; + this.RawEthernetRadioButton.Size = new System.Drawing.Size(186, 17); + this.RawEthernetRadioButton.TabIndex = 1; + this.RawEthernetRadioButton.TabStop = true; + this.RawEthernetRadioButton.Text = "Raw Ethernet (requires WinPCAP)"; + this.RawEthernetRadioButton.UseVisualStyleBackColor = true; + this.RawEthernetRadioButton.CheckedChanged += new System.EventHandler(this.OnEthernetTypeCheckedChanged); + // + // UDPRadioButton + // + this.UDPRadioButton.AutoSize = true; + this.UDPRadioButton.Location = new System.Drawing.Point(9, 16); + this.UDPRadioButton.Name = "UDPRadioButton"; + this.UDPRadioButton.Size = new System.Drawing.Size(48, 17); + this.UDPRadioButton.TabIndex = 0; + this.UDPRadioButton.TabStop = true; + this.UDPRadioButton.Text = "UDP"; + this.UDPRadioButton.UseVisualStyleBackColor = true; + this.UDPRadioButton.CheckedChanged += new System.EventHandler(this.OnEthernetTypeCheckedChanged); + // + // HostInterfaceGroupBox + // + this.HostInterfaceGroupBox.Controls.Add(this.EthernetInterfaceListBox); + this.HostInterfaceGroupBox.Controls.Add(this.label4); + this.HostInterfaceGroupBox.Location = new System.Drawing.Point(10, 75); + this.HostInterfaceGroupBox.Name = "HostInterfaceGroupBox"; + this.HostInterfaceGroupBox.Size = new System.Drawing.Size(335, 120); + this.HostInterfaceGroupBox.TabIndex = 2; + this.HostInterfaceGroupBox.TabStop = false; + this.HostInterfaceGroupBox.Text = "Host Interface"; + // + // EthernetInterfaceListBox + // + this.EthernetInterfaceListBox.FormattingEnabled = true; + this.EthernetInterfaceListBox.Location = new System.Drawing.Point(9, 41); + this.EthernetInterfaceListBox.Name = "EthernetInterfaceListBox"; + this.EthernetInterfaceListBox.Size = new System.Drawing.Size(326, 69); + this.EthernetInterfaceListBox.TabIndex = 1; + // + // label4 + // + this.label4.AutoSize = true; + this.label4.Location = new System.Drawing.Point(9, 20); + this.label4.Name = "label4"; + this.label4.Size = new System.Drawing.Size(245, 13); + this.label4.TabIndex = 0; + this.label4.Text = "Select the Network interface to use with ContrAlto:"; + // + // label3 + // + this.label3.AutoSize = true; + this.label3.Location = new System.Drawing.Point(7, 13); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(101, 13); + this.label3.TabIndex = 1; + this.label3.Text = "Alto Address (octal):"; + // + // AltoEthernetAddressTextBox + // + this.AltoEthernetAddressTextBox.Location = new System.Drawing.Point(114, 10); + this.AltoEthernetAddressTextBox.Name = "AltoEthernetAddressTextBox"; + this.AltoEthernetAddressTextBox.Size = new System.Drawing.Size(49, 20); + this.AltoEthernetAddressTextBox.TabIndex = 0; + // + // tabPage3 + // + this.tabPage3.Controls.Add(this.ThrottleSpeedCheckBox); + this.tabPage3.Controls.Add(this.InterlaceDisplayCheckBox); + this.tabPage3.Location = new System.Drawing.Point(4, 22); + this.tabPage3.Name = "tabPage3"; + this.tabPage3.Padding = new System.Windows.Forms.Padding(3); + this.tabPage3.Size = new System.Drawing.Size(360, 201); + this.tabPage3.TabIndex = 2; + this.tabPage3.Text = "Display"; + this.tabPage3.UseVisualStyleBackColor = true; + // + // ThrottleSpeedCheckBox + // + this.ThrottleSpeedCheckBox.AutoSize = true; + this.ThrottleSpeedCheckBox.Location = new System.Drawing.Point(19, 22); + this.ThrottleSpeedCheckBox.Name = "ThrottleSpeedCheckBox"; + this.ThrottleSpeedCheckBox.Size = new System.Drawing.Size(188, 17); + this.ThrottleSpeedCheckBox.TabIndex = 1; + this.ThrottleSpeedCheckBox.Text = "Throttle Framerate at 60 fields/sec"; + this.ThrottleSpeedCheckBox.UseVisualStyleBackColor = true; + // + // InterlaceDisplayCheckBox + // + this.InterlaceDisplayCheckBox.AutoSize = true; + this.InterlaceDisplayCheckBox.Location = new System.Drawing.Point(19, 45); + this.InterlaceDisplayCheckBox.Name = "InterlaceDisplayCheckBox"; + this.InterlaceDisplayCheckBox.Size = new System.Drawing.Size(196, 17); + this.InterlaceDisplayCheckBox.TabIndex = 0; + this.InterlaceDisplayCheckBox.Text = "Interlaced Display (headache mode)"; + this.InterlaceDisplayCheckBox.UseVisualStyleBackColor = true; + // + // OKButton + // + this.OKButton.Location = new System.Drawing.Point(211, 239); + this.OKButton.Name = "OKButton"; + this.OKButton.Size = new System.Drawing.Size(75, 23); + this.OKButton.TabIndex = 1; + this.OKButton.Text = "OK"; + this.OKButton.UseVisualStyleBackColor = true; + this.OKButton.Click += new System.EventHandler(this.OKButton_Click); + // + // CancelButton + // + this.CancelButton.Location = new System.Drawing.Point(292, 239); + this.CancelButton.Name = "CancelButton"; + this.CancelButton.Size = new System.Drawing.Size(75, 23); + this.CancelButton.TabIndex = 2; + this.CancelButton.Text = "Cancel"; + this.CancelButton.UseVisualStyleBackColor = true; + this.CancelButton.Click += new System.EventHandler(this.CancelButton_Click); + // + // SystemOptions + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(371, 271); + this.Controls.Add(this.CancelButton); + this.Controls.Add(this.OKButton); + this.Controls.Add(this.tabControl1); + this.Name = "SystemOptions"; + this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "System Options"; + this.tabControl1.ResumeLayout(false); + this.tabPage1.ResumeLayout(false); + this.tabPage1.PerformLayout(); + this.tabPage2.ResumeLayout(false); + this.tabPage2.PerformLayout(); + this.groupBox1.ResumeLayout(false); + this.groupBox1.PerformLayout(); + this.HostInterfaceGroupBox.ResumeLayout(false); + this.HostInterfaceGroupBox.PerformLayout(); + this.tabPage3.ResumeLayout(false); + this.tabPage3.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.TabControl tabControl1; + private System.Windows.Forms.TabPage tabPage1; + private System.Windows.Forms.RadioButton AltoII3KRAMRadioButton; + private System.Windows.Forms.RadioButton AltoII2KROMRadioButton; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.RadioButton AltoII1KROMRadioButton; + private System.Windows.Forms.TabPage tabPage2; + private System.Windows.Forms.GroupBox HostInterfaceGroupBox; + private System.Windows.Forms.ListBox EthernetInterfaceListBox; + private System.Windows.Forms.Label label4; + private System.Windows.Forms.Label label3; + private System.Windows.Forms.TextBox AltoEthernetAddressTextBox; + private System.Windows.Forms.Button OKButton; + private System.Windows.Forms.Button CancelButton; + private System.Windows.Forms.TabPage tabPage3; + private System.Windows.Forms.CheckBox ThrottleSpeedCheckBox; + private System.Windows.Forms.CheckBox InterlaceDisplayCheckBox; + private System.Windows.Forms.GroupBox groupBox1; + private System.Windows.Forms.RadioButton RawEthernetRadioButton; + private System.Windows.Forms.RadioButton UDPRadioButton; + } +} \ No newline at end of file diff --git a/Contralto/UI/SystemOptions.cs b/Contralto/UI/SystemOptions.cs new file mode 100644 index 0000000..2288b80 --- /dev/null +++ b/Contralto/UI/SystemOptions.cs @@ -0,0 +1,218 @@ +using Contralto.IO; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Net.NetworkInformation; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace Contralto.UI +{ + public partial class SystemOptions : Form + { + public SystemOptions() + { + InitializeComponent(); + + PopulateUI(); + } + + + /// + /// Populates the UI fields with data from the current configuration. + /// + private void PopulateUI() + { + _selectedSystemType = Configuration.SystemType; + _selectedInterfaceType = Configuration.HostPacketInterfaceType; + + switch(Configuration.SystemType) + { + case SystemType.OneKRom: + AltoII1KROMRadioButton.Checked = true; + break; + + case SystemType.TwoKRom: + AltoII2KROMRadioButton.Checked = true; + break; + + case SystemType.ThreeKRam: + AltoII3KRAMRadioButton.Checked = true; + break; + } + + InterlaceDisplayCheckBox.Checked = Configuration.InterlaceDisplay; + ThrottleSpeedCheckBox.Checked = Configuration.ThrottleSpeed; + + AltoEthernetAddressTextBox.Text = Conversion.ToOctal(Configuration.HostAddress); + + if (!Configuration.HostRawEthernetInterfacesAvailable) + { + // If PCAP isn't installed, the RAW Ethernet option is not available. + RawEthernetRadioButton.Enabled = false; + UDPRadioButton.Checked = true; + } + else + { + if (Configuration.HostPacketInterfaceType == PacketInterfaceType.UDPEncapsulation) + { + UDPRadioButton.Checked = true; + } + else + { + RawEthernetRadioButton.Checked = true; + } + } + + PopulateNetworkAdapterList(UDPRadioButton.Checked); + } + + private void PopulateNetworkAdapterList(bool udpEncapsulation) + { + // + // Populate the list with the interfaces available on the machine, for the + // type of encapsulation being used. + // + EthernetInterfaceListBox.Items.Clear(); + + NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces(); + + // Add the "Use no interface" option + EthernetInterfaceListBox.Items.Add( + new EthernetInterface("None", "No network adapter")); + + foreach (NetworkInterface iface in interfaces) + { + // For UDP we show all interfaces that support IPV4, for Raw Ethernet we show only Ethernet interfaces. + if (udpEncapsulation) + { + if (iface.Supports(NetworkInterfaceComponent.IPv4)) + { + EthernetInterfaceListBox.Items.Add(new EthernetInterface(iface.Name, iface.Description)); + } + } + else + { + if (iface.NetworkInterfaceType == NetworkInterfaceType.Ethernet) + { + EthernetInterfaceListBox.Items.Add(new EthernetInterface(iface.Name, iface.Description)); + } + } + } + + // + // Select the one that is already selected (if any) + // + EthernetInterfaceListBox.SelectedIndex = 0; + + if (!string.IsNullOrEmpty(Configuration.HostPacketInterfaceName)) + { + for (int i = 0; i < EthernetInterfaceListBox.Items.Count; i++) + { + EthernetInterface iface = (EthernetInterface)EthernetInterfaceListBox.Items[i]; + + if (iface.Description.ToLowerInvariant() == Configuration.HostPacketInterfaceName.ToLowerInvariant()) + { + EthernetInterfaceListBox.SelectedIndex = i; + break; + } + } + } + } + + private void OnSystemTypeCheckChanged(object sender, EventArgs e) + { + if (AltoII1KROMRadioButton.Checked) + { + _selectedSystemType = SystemType.OneKRom; + } + else if (AltoII2KROMRadioButton.Checked) + { + _selectedSystemType = SystemType.TwoKRom; + } + else if (AltoII3KRAMRadioButton.Checked) + { + _selectedSystemType = SystemType.ThreeKRam; + } + } + + private void OnEthernetTypeCheckedChanged(object sender, EventArgs e) + { + if (UDPRadioButton.Checked) + { + _selectedInterfaceType = PacketInterfaceType.UDPEncapsulation; + } + else + { + _selectedInterfaceType = PacketInterfaceType.EthernetEncapsulation; + } + + PopulateNetworkAdapterList(UDPRadioButton.Checked); + } + + private void OKButton_Click(object sender, EventArgs e) + { + try + { + int testValue = Convert.ToByte(AltoEthernetAddressTextBox.Text, 8); + + if (testValue < 1 || testValue > 254) + { + throw new ArgumentOutOfRangeException("Invalid host address."); + } + } + catch + { + MessageBox.Show("The Alto Ethernet address must be an octal value between 1 and 376."); + return; + } + + // + // Commit changes back to Configuration. + // + + EthernetInterface iface = (EthernetInterface)EthernetInterfaceListBox.SelectedItem; + + // + // First warn the user of changes that require a restart. + // + if (Configuration.HostPacketInterfaceName.ToLowerInvariant() != iface.Description.ToLowerInvariant() || + Configuration.HostPacketInterfaceType != _selectedInterfaceType || + Configuration.SystemType != _selectedSystemType) + { + MessageBox.Show("Changes to CPU or Ethernet configuration will not take effect until ContrAlto is restarted."); + } + + //System + Configuration.SystemType = _selectedSystemType; + + // Ethernet + Configuration.HostAddress = Convert.ToByte(AltoEthernetAddressTextBox.Text, 8); + Configuration.HostPacketInterfaceName = iface.Description; + Configuration.HostPacketInterfaceType = _selectedInterfaceType; + + // Display + Configuration.InterlaceDisplay = InterlaceDisplayCheckBox.Checked; + Configuration.ThrottleSpeed = ThrottleSpeedCheckBox.Checked; + + Configuration.WriteConfiguration(); + + this.Close(); + + } + + private void CancelButton_Click(object sender, EventArgs e) + { + this.Close(); + } + + private PacketInterfaceType _selectedInterfaceType; + private SystemType _selectedSystemType; + + + } +} diff --git a/Contralto/UI/SystemOptions.resx b/Contralto/UI/SystemOptions.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/Contralto/UI/SystemOptions.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file