/* This file is part of sImlac. sImlac 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. sImlac 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 sImlac. If not, see . */ using System; using System.Collections.Generic; using imlac.IO; using imlac.Debugger; namespace imlac { /// /// DisplayProcessor implements the Display processor found in an Imlac PDS-1 with long vector hardware. /// public class PDS1DisplayProcessor : DisplayProcessorBase { public PDS1DisplayProcessor(ImlacSystem system) : base(system) { } public override void Reset() { base.Reset(); _sgrModeOn = false; _sgrBeamOn = false; _sgrDJRMOn = false; } public override void InitializeCache() { _instructionCache = new PDS1DisplayInstruction[Memory.Size]; } public override void InvalidateCache(ushort address) { _instructionCache[address & Memory.SizeMask] = null; } public override string Disassemble(ushort address, DisplayProcessorMode mode) { // // Return a precached instruction if we have it due to previous execution // otherwise disassemble it now in the requested mode; this disassembly // does not get added to the cache. // if (_instructionCache[address & Memory.SizeMask] != null) { return _instructionCache[address & Memory.SizeMask].Disassemble(mode); } else { return new PDS1DisplayInstruction((ushort)(address & Memory.SizeMask), mode).Disassemble(mode); } } public override void Clock() { _clocks++; if (_clocks > _frameClocks40Hz) { _clocks = 0; _frameLatch = true; _system.Display.FrameDone(); } if (_state == ProcessorState.Halted) { return; } switch (_mode) { case DisplayProcessorMode.Processor: ExecuteProcessor(); break; case DisplayProcessorMode.Increment: ExecuteIncrement(); break; } } public override int[] GetHandledIOTs() { return _handledIOTs; } public override void ExecuteIOT(int iotCode) { // // Dispatch the IOT instruction. // switch (iotCode) { case 0x03: // DLA: PC = _system.Processor.AC; // this is for debugging only, we keep track of the load address // to make it easy to see where the main Display List starts _dpcEntry = PC; break; case 0x0a: // Halt display processor HaltProcessor(); break; case 0x39: // Clear display 40Hz sync latch _frameLatch = false; break; case 0xc4: // clear halt state // TODO: what does this actually do? _halted = false; break; default: throw new NotImplementedException(String.Format("Unimplemented Display IOT instruction {0:x4}", iotCode)); } } private void ExecuteProcessor() { PDS1DisplayInstruction instruction = GetCachedInstruction(_pc, DisplayProcessorMode.Processor); instruction.UsageMode = DisplayProcessorMode.Processor; switch (instruction.Opcode) { case DisplayOpcode.DEIM: _mode = DisplayProcessorMode.Increment; _immediateWord = instruction.Data; _immediateHalf = ImmediateHalf.Second; if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "Enter increment mode"); break; case DisplayOpcode.DJMP: if (!_dadr) { // DADR off, use only 12 bits _pc = (ushort)((instruction.Data & 0xfff) | _block); } else { _pc = (ushort)(instruction.Data | _block); } break; case DisplayOpcode.DJMS: Push(); if (!_dadr) { // DADR off, use only 12 bits _pc = (ushort)((instruction.Data & 0xfff) | _block); } else { _pc = (ushort)(instruction.Data | _block); } break; case DisplayOpcode.DOPR: // Each of bits 4-11 can be combined in any fashion // to do a number of operations simultaneously; we walk the bits // and perform the operations as set. if ((instruction.Data & 0x800) == 0) { // DHLT -- halt the display processor. other micro-ops in this // instruction are still run. HaltProcessor(); } if ((instruction.Data & 0x400) != 0) { // HV Sync; this is currently a no-op, not much to do in emulation. if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "HV Sync"); _system.Display.MoveAbsolute(X, Y, DrawingMode.Off); } if ((instruction.Data & 0x200) != 0) { // DIXM -- increment X DAC MSB X += 0x20; _system.Display.MoveAbsolute(X, Y, DrawingMode.Off); if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "DIXM, X is now {0}", X); } if ((instruction.Data & 0x100) != 0) { // DIYM -- increment Y DAC MSB Y += 0x20; _system.Display.MoveAbsolute(X, Y, DrawingMode.Off); if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "DIYM, Y is now {0}", Y); } if ((instruction.Data & 0x80) != 0) { // DDXM - decrement X DAC MSB X -= 0x20; _system.Display.MoveAbsolute(X, Y, DrawingMode.Off); if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "DDXM, X is now {0}", X); } if ((instruction.Data & 0x40) != 0) { // DDYM - decrement y DAC MSB Y -= 0x20; _system.Display.MoveAbsolute(X, Y, DrawingMode.Off); if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "DDYM, Y is now {0}", Y); } if ((instruction.Data & 0x20) != 0) { // DRJM - return from display subroutine ReturnFromDisplaySubroutine(); _pc--; // hack (we add +1 at the end...) } if ((instruction.Data & 0x10) != 0) { // DDSP -- intensify point on screen for 1.8us (one instruction) // at the current position. _system.Display.DrawPoint(X, Y); if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "DDSP at {0},{1}", X, Y); } // F/C ops: int f = (instruction.Data & 0xc) >> 2; int c = instruction.Data & 0x3; switch (f) { case 0x0: // if bit 15 is set, the MIT mods flip the DADR bit. if (Configuration.MITMode && (c == 1)) { _dadr = !_dadr; } break; case 0x1: // Set scale based on C switch (c) { case 0: _scale = 1.0f; break; default: _scale = c; break; } if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "Scale set to {0}", _scale); break; case 0x2: if (!Configuration.MITMode) { _block = (ushort)(c << 12); } break; case 0x3: // TODO: light pen sensitize if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "Light pen, stub!"); break; } _pc++; break; case DisplayOpcode.DLXA: X = instruction.Data << 1; DrawingMode mode; if (_sgrModeOn && _sgrBeamOn) { if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "SGR-1 X set to {0}", X); mode = DrawingMode.SGR1; } else { mode = DrawingMode.Off; if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "X set to {0}", X); } _system.Display.MoveAbsolute(X, Y, mode); if (_sgrDJRMOn) { ReturnFromDisplaySubroutine(); } else { _pc++; } break; case DisplayOpcode.DLYA: Y = instruction.Data << 1; if (_sgrModeOn && _sgrBeamOn) { if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "SGR-1 Y set to {0}", Y); mode = DrawingMode.SGR1; } else { if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "Y set to {0}", Y); mode = DrawingMode.Off; } _system.Display.MoveAbsolute(X, Y, mode); if (_sgrDJRMOn) { ReturnFromDisplaySubroutine(); } else { _pc++; } break; case DisplayOpcode.DLVH: DrawLongVector(instruction.Data); break; case DisplayOpcode.SGR1: _sgrModeOn = (instruction.Data & 0x1) != 0; _sgrDJRMOn = (instruction.Data & 0x2) != 0; _sgrBeamOn = (instruction.Data & 0x4) != 0; if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "SGR-1 instruction: Enter {0} BeamOn {1} DRJM {2}", _sgrModeOn, _sgrBeamOn, _sgrDJRMOn); _pc++; break; default: throw new NotImplementedException(String.Format("Unimplemented Display Processor Opcode {0}, operands {1}", Helpers.ToOctal((ushort)instruction.Opcode), Helpers.ToOctal(instruction.Data))); } // If the next instruction has a breakpoint set we'll halt at this point, before executing it. if (BreakpointManager.TestBreakpoint(BreakpointType.Display, _pc)) { _state = ProcessorState.BreakpointHalt; } } private void ExecuteIncrement() { int halfWord = _immediateHalf == ImmediateHalf.First ? (_immediateWord & 0xff00) >> 8 : (_immediateWord & 0xff); int newX = (int)X; int newY = (int)Y; // translate the half word to vector movements or escapes if ((halfWord & 0x80) == 0) { if ((halfWord & 0x40) != 0) { // Escape code if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "Increment mode escape on halfword {0}", _immediateHalf); _mode = DisplayProcessorMode.Processor; _pc++; // move to next word // Moved this into this check (not sure it makes sense to do a DJMS when not escaped from Increment mode) if ((halfWord & 0x20) != 0) { if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "Increment mode return from subroutine."); ReturnFromDisplaySubroutine(); } } else { // Stay in increment mode. if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "Increment instruction, non-drawing."); MoveToNextHalfWord(); } if ((halfWord & 0x10) != 0) { newX += 0x20; if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "Increment X MSB, X is now {0}", X); } if ((halfWord & 0x08) != 0) { newX = newX & (0xffe0); if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "Reset X LSB, X is now {0}", X); } if ((halfWord & 0x02) != 0) { newY += 0x20; if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "Increment Y MSB, Y is now {0}", Y); } if ((halfWord & 0x01) != 0) { newY = newY & (0xffe0); if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "Reset Y LSB, Y is now {0}", Y); } _system.Display.MoveAbsolute(newX, newY, DrawingMode.Off); } else { int xSign = ((halfWord & 0x20) == 0) ? 1 : -1; int xMag = (int)(((halfWord & 0x18) >> 3) * _scale); int ySign = (int)(((halfWord & 0x04) == 0) ? 1 : -1); int yMag = (int)((halfWord & 0x03) * _scale); if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "Inc mode ({0}:{1}), x={2} y={3} dx={4} dy={5} beamon {6}", Helpers.ToOctal((ushort)_pc), Helpers.ToOctal((ushort)halfWord), newX, newY, xSign * xMag, ySign * yMag, (halfWord & 0x40) != 0); newX = (int)(newX + xSign * xMag * 2); newY = (int)(newY + ySign * yMag * 2); _system.Display.MoveAbsolute(newX, newY, (halfWord & 0x40) == 0 ? DrawingMode.Off : DrawingMode.Dotted); MoveToNextHalfWord(); } // Assign back to X, Y registers; clipping into range. X = newX; Y = newY; // If the next instruction has a breakpoint set we'll halt at this point, before executing it. if (_immediateHalf == ImmediateHalf.First && BreakpointManager.TestBreakpoint(BreakpointType.Display, _pc)) { _state = ProcessorState.BreakpointHalt; } } private void MoveToNextHalfWord() { if (_immediateHalf == ImmediateHalf.Second) { _pc++; _immediateWord = _mem.Fetch(_pc); _immediateHalf = ImmediateHalf.First; // Update the instruction cache with the type of instruction (to aid in debugging). PDS1DisplayInstruction instruction = GetCachedInstruction(_pc, DisplayProcessorMode.Increment); } else { _immediateHalf = ImmediateHalf.Second; } } private void DrawLongVector(ushort word0) { // // A Long Vector instruction is 3 words long: // Word 0: upper 4 bits indicate the opcode (4), lower 12 specify N-M // Word 1: upper 3 bits specify beam options (dotted, solid, etc) and the lower 12 specify the larger increment "M" // Word 2: upper 3 bits specify signs, lower 12 specify the smaller increment "N" // M is the larger absolute value between dX and dY // N is the smaller. // // Unsure at the moment what the N-M bits are for (I'm guessing they're there to help the processor figure things out). // Also unsure what bits are used in the 12 bits for N and M (the DACs are only 11-bits, but normally only 10 can be specified)... // ushort word1 = _mem.Fetch(++_pc); ushort word2 = _mem.Fetch(++_pc); uint M = (uint)(word1 & 0x3ff); uint N = (uint)(word2 & 0x3ff); bool beamOn = (word1 & 0x2000) != 0; bool dotted = (word1 & 0x4000) != 0; int dySign = (word2 & 0x2000) != 0 ? -1 : 1; int dxSign = (word2 & 0x4000) != 0 ? -1 : 1; bool dyGreater = (word2 & 0x1000) != 0; uint dx = 0; uint dy = 0; if (dyGreater) { dy = M; dx = N; } else { dx = M; dy = N; } if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "LongVector x={0} y={1} dx={2} dy={3} beamOn {4} dotted {5}", X, Y, dx * dxSign, dy * dySign, beamOn, dotted); // * 2 for translation to 11-bit space // The docs don't call this out, but the scale setting used in increment mode appears to apply // to the LVH vectors as well. (Maze appears to rely on this.) int newX = (int)(X + (dx * dxSign) * 2 * _scale); int newY = (int)(Y + (dy * dySign) * 2 * _scale); if (Trace.TraceOn) Trace.Log(LogType.DisplayProcessor, "LongVector, move complete - x={0} y={1}", newX, newY, dx * dxSign, dy * dySign, beamOn, dotted); _system.Display.MoveAbsolute(newX, newY, beamOn ? (dotted ? DrawingMode.Dotted : DrawingMode.Normal) : DrawingMode.Off); // Assign back to X, Y registers; clipping into range. X = newX; Y = newY; _pc++; } private void ReturnFromDisplaySubroutine() { Pop(); } private PDS1DisplayInstruction GetCachedInstruction(ushort address, DisplayProcessorMode mode) { if (_instructionCache[address & Memory.SizeMask] == null) { _instructionCache[address & Memory.SizeMask] = new PDS1DisplayInstruction(_mem.Fetch(address), mode); } return _instructionCache[address & Memory.SizeMask]; } // SGR-1 mode switches private bool _sgrModeOn; private bool _sgrDJRMOn; private bool _sgrBeamOn; private PDS1DisplayInstruction[] _instructionCache; private readonly int[] _handledIOTs = { 0x3, 0xa, 0x39, 0xc4 }; /// /// PDS-1 Display instruction decoder and disassembler. /// private class PDS1DisplayInstruction : DisplayInstructionBase { public PDS1DisplayInstruction(ushort word, DisplayProcessorMode mode) : base (word, mode) { } public override string Disassemble(DisplayProcessorMode mode) { if (mode == DisplayProcessorMode.Indeterminate) { mode = _usageMode; } switch (mode) { case DisplayProcessorMode.Increment: return DisassembleIncrement(); case DisplayProcessorMode.Processor: return DisassembleProcessor(); case DisplayProcessorMode.Indeterminate: return "Indeterminate"; default: throw new InvalidOperationException(String.Format("{0} is not a supported disassembly mode for this processor.", mode)); } } protected override void Decode() { if (_usageMode == DisplayProcessorMode.Processor) { DecodeProcessor(); } else { DecodeImmediate(); } } private void DecodeProcessor() { int op = (_word & 0x7000) >> 12; switch (op) { case 0x00: // opr code _opcode = DisplayOpcode.DOPR; _data = (ushort)(_word & 0xfff); break; case 0x01: _opcode = DisplayOpcode.DLXA; _data = (ushort)(_word & 0x3ff); break; case 0x02: _opcode = DisplayOpcode.DLYA; _data = (ushort)(_word & 0x3ff); break; case 0x03: _opcode = DisplayOpcode.DEIM; _data = (ushort)(_word & 0xff); if ((_word & 0xff00) == 0x3800) { Console.WriteLine("PPM-1 not implemented (instr {0})", Helpers.ToOctal(_word)); } break; case 0x04: _opcode = DisplayOpcode.DLVH; _data = (ushort)(_word & 0xfff); break; case 0x05: _opcode = DisplayOpcode.DJMS; _data = (ushort)(_word & 0xfff); if (Configuration.MITMode && (_word & 0x8000) != 0) { // MIT's mod takes the MSB of the address from the MSB of the instruction word _data |= 0x1000; } break; case 0x06: _opcode = DisplayOpcode.DJMP; _data = (ushort)(_word & 0xfff); if (Configuration.MITMode && (_word & 0x8000) != 0) { // MIT's mod takes the MSB of the address from the MSB of the instruction word _data |= 0x1000; } break; case 0x07: DecodeExtendedInstruction(_word); break; default: throw new NotImplementedException(String.Format("Unhandled Display Processor Mode instruction {0}", Helpers.ToOctal(_word))); } } void DecodeExtendedInstruction(ushort word) { int op = (word & 0x1f8) >> 3; switch (op) { case 0x36: case 0x37: _opcode = DisplayOpcode.ASG1; break; case 0x3a: case 0x3b: _opcode = DisplayOpcode.VIC1; break; case 0x3c: case 0x3d: _opcode = DisplayOpcode.MCI1; break; case 0x3e: _opcode = DisplayOpcode.STI1; break; case 0x3f: _opcode = DisplayOpcode.SGR1; break; default: throw new NotImplementedException(String.Format("Unhandled extended Display Processor Mode instruction {0}", Helpers.ToOctal(word))); } _data = (ushort)(word & 0x7); } private string DisassembleIncrement() { return DisassembleIncrementHalf(ImmediateHalf.First) + " | " + DisassembleIncrementHalf(ImmediateHalf.Second); } private string DisassembleIncrementHalf(ImmediateHalf half) { string ret = string.Empty; int halfWord = half == ImmediateHalf.First ? (_word & 0xff00) >> 8 : (_word & 0xff); // translate the half word to vector movements or escapes // special case for "Enter Immediate mode" halfword (030) in first half. if (half == ImmediateHalf.First && halfWord == 0x30) { ret += "E"; } else if ((halfWord & 0x80) == 0) { if ((halfWord & 0x10) != 0) { ret += "IX "; } if ((halfWord & 0x08) != 0) { ret += "ZX "; } if ((halfWord & 0x02) != 0) { ret += "IY "; } if ((halfWord & 0x01) != 0) { ret += "ZY "; } if ((halfWord & 0x40) != 0) { if ((halfWord & 0x20) != 0) { // escape and return ret += "F RJM"; } else { // Escape ret += "F"; } } } else { int xSign = ((halfWord & 0x20) == 0) ? 1 : -1; int xMag = (int)(((halfWord & 0x18) >> 3)); int ySign = (int)(((halfWord & 0x04) == 0) ? 1 : -1); int yMag = (int)((halfWord & 0x03)); ret += String.Format("{0},{1} {2}", xMag * xSign, yMag * ySign, (halfWord & 0x40) == 0 ? "OFF" : "ON"); } return ret; } private void DecodeImmediate() { // TODO: eventually actually precache movement calculations. } private string DisassembleProcessor() { string ret = String.Empty; if (_opcode == DisplayOpcode.DOPR) { string[] codes = { "INV0 ", "INV1 ", "INV2 ", "INV3 ", "DDSP ", "DRJM ", "DDYM ", "DDXM ", "DIYM ", "DIXM ", "DHVC ", "DHLT " }; for (int i = 4; i < 12; i++) { if ((_data & (0x01) << i) != 0) { if (!string.IsNullOrEmpty(ret)) { ret += ","; } ret += codes[i]; } } // F/C ops: int f = (_data & 0xc) >> 2; int c = _data & 0x3; switch (f) { case 0x0: // nothing if (c == 1) { ret += String.Format("DADR"); } break; case 0x1: ret += String.Format("DSTS {0}", c); break; case 0x2: ret += String.Format("DSTB {0}", c); break; case 0x3: ret += String.Format("DLPN {0}", c); break; } } else { // keep things simple -- should add special support for extended instructions at some point... ret = String.Format("{0} {1} ", _opcode, Helpers.ToOctal(_data)); } return ret; } } } }