/* * Copyright 2020 faddenSoft * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.ObjectModel; using PluginCommon; namespace RuntimeData.Atari { /// /// Visualizer for Atari Analog Vector Generator commands (Battlezone, etc). /// /// Currently ignores beam intensity, except as on/off. /// /// References: /// http://www.ionpool.net/arcade/atari_docs/avg.pdf /// https://www.jmargolin.com/vgens/vgcmd.pdf /// http://www.brouhaha.com/~eric/software/vecsim/ /// /// https://github.com/mamedev/mame/blob/master/src/mame/video/avgdvg.cpp is the /// definitive description, but it's harder to parse than the above (it's emulating /// the hardware at a lower level). /// public class VisAVG : MarshalByRefObject, IPlugin, IPlugin_Visualizer_v2 { // IPlugin public string Identifier { get { return "Atari AVG Visualizer"; } } private IApplication mAppRef; private byte[] mFileData; private AddressTranslate mAddrTrans; // Visualization identifiers; DO NOT change or projects that use them will break. private const string VIS_GEN_AVG = "atari-avg-bz"; // Battlezone, Red Baron private const string P_OFFSET = "offset"; private const string P_BASE_ADDR = "baseAddr"; // Visualization descriptors. private VisDescr[] mDescriptors = new VisDescr[] { new VisDescr(VIS_GEN_AVG, "Atari AVG Commands", VisDescr.VisType.Wireframe, new VisParamDescr[] { new VisParamDescr("File offset (hex)", P_OFFSET, typeof(int), 0, 0x00ffffff, VisParamDescr.SpecialMode.Offset, 0), new VisParamDescr("Base address", P_BASE_ADDR, typeof(int), 0x0000, 0xffff, 0, 0x2000), VisWireframe.Param_IsRecentered("Centered", true), }), }; // IPlugin public void Prepare(IApplication appRef, byte[] fileData, AddressTranslate addrTrans) { mAppRef = appRef; mFileData = fileData; mAddrTrans = addrTrans; } // IPlugin public void Unprepare() { mAppRef = null; mFileData = null; mAddrTrans = null; } // IPlugin_Visualizer public VisDescr[] GetVisGenDescrs() { if (mFileData == null) { return null; } return mDescriptors; } // IPlugin_Visualizer public IVisualization2d Generate2d(VisDescr descr, ReadOnlyDictionary parms) { mAppRef.ReportError("2d not supported"); return null; } // IPlugin_Visualizer_v2 public IVisualizationWireframe GenerateWireframe(VisDescr descr, ReadOnlyDictionary parms) { switch (descr.Ident) { case VIS_GEN_AVG: return GenerateWireframe(parms); default: mAppRef.ReportError("Unknown ident " + descr.Ident); return null; } } private enum Opcode { Unknown = 0, VCTR, HALT, SVEC, STAT, SCAL, CNTR, JSR, RTS, JMP } private IVisualizationWireframe GenerateWireframe(ReadOnlyDictionary parms) { int offset = Util.GetFromObjDict(parms, P_OFFSET, 0); int baseAddr = Util.GetFromObjDict(parms, P_BASE_ADDR, 0); if (offset < 0 || offset >= mFileData.Length) { // should be caught by editor mAppRef.ReportError("Invalid parameter"); return null; } VisWireframe vw = new VisWireframe(); vw.Is2d = true; try { int[] stack = new int[4]; int stackPtr = 0; int beamX = 0; int beamY = 0; double scale = 1.0; bool done = false; int centerVertex = vw.AddVertex(0, 0, 0); int curVertex = centerVertex; while (!done && offset < mFileData.Length) { ushort code0 = (ushort)Util.GetWord(mFileData, offset, 2, false); offset += 2; Opcode opc = GetOpcode(code0); int dx, dy, ii, vaddr; switch (opc) { case Opcode.VCTR: // 000YYYYY YYYYYYYY IIIXXXXX XXXXXXXX ushort code1 = (ushort)Util.GetWord(mFileData, offset, 2, false); offset += 2; dy = sign13(code0 & 0x1fff); dx = sign13(code1 & 0x1fff); ii = code1 >> 13; beamX += (int)Math.Round(dx * scale); beamY += (int)Math.Round(dy * scale); if (ii == 0) { // move only curVertex = vw.AddVertex(beamX, beamY, 0); } else if (dx == 0 && dy == 0) { // plot point vw.AddPoint(curVertex); //mAppRef.DebugLog("PLOT v" + curVertex + ": " // + beamX + "," + beamY); } else { // draw line from previous vertex int newVertex = vw.AddVertex(beamX, beamY, 0); vw.AddEdge(curVertex, newVertex); curVertex = newVertex; } break; case Opcode.HALT: // 00100000 00100000 if (stackPtr != 0) { mAppRef.DebugLog("NOTE: encountered HALT with nonzero stack"); } done = true; break; case Opcode.SVEC: // 010YYYYY IIIXXXXX dy = sign5((code0 >> 8) & 0x1f) * 2; dx = sign5(code0 & 0x1f) * 2; ii = (code0 >> 5) & 0x07; if (ii != 1) { ii *= 2; } // note dx/dy==0 (i.e. draw point) is not supported for SVEC beamX += (int)Math.Round(dx * scale); beamY += (int)Math.Round(dy * scale); if (ii == 0) { // move only curVertex = vw.AddVertex(beamX, beamY, 0); } else { // draw line from previous vertex int newVertex = vw.AddVertex(beamX, beamY, 0); vw.AddEdge(curVertex, newVertex); //mAppRef.DebugLog("SVEC edge " + curVertex + " - " + newVertex); curVertex = newVertex; } break; case Opcode.STAT: // 0110-EHO IIIICCCC // Note this is different for e.g. Star Wars: 0110-RGB ZZZZZZZZ ii = (code0 >> 4) & 0x0f; break; case Opcode.SCAL: // 0111-BBB LLLLLLLL // Binary scaling adjusts vector drawing time, linear scaling does // not. (Does this affect the intensity?) int bs = (code0 >> 8) & 0x07; int ls = code0 & 0xff; scale = (16384 - (ls << 6)) >> bs; break; case Opcode.CNTR: // 10000000 01------ beamX = beamY = 0; curVertex = centerVertex; break; case Opcode.JSR: // 101-AAAA AAAAAAAA vaddr = code0 & 0x0fff; // spec says 12 bits, VECSIM uses 13 if (stackPtr == stack.Length) { mAppRef.ReportError("Stack overflow at +" + offset.ToString("x6")); return null; } stack[stackPtr++] = offset; if (!Branch(vaddr, baseAddr, ref offset)) { return null; } break; case Opcode.RTS: // 110----- -------- if (stackPtr == 0) { done = true; } else { offset = stack[--stackPtr]; } break; case Opcode.JMP: // 111-AAAA AAAAAAAA vaddr = code0 & 0x0fff; // spec says 12 bits, VECSIM uses 13 if (!Branch(vaddr, baseAddr, ref offset)) { return null; } break; default: mAppRef.ReportError("Unhandled code $" + code0.ToString("x4")); return null; } } } catch (IndexOutOfRangeException) { // assume it was our file data access that caused the failure mAppRef.ReportError("Ran off end of file"); return null; } string msg; if (!vw.Validate(out msg)) { mAppRef.ReportError("Data error: " + msg); return null; } return vw; } private static Opcode GetOpcode(ushort code) { switch (code & 0xe000) { case 0x0000: return Opcode.VCTR; case 0x2000: return Opcode.HALT; case 0x4000: return Opcode.SVEC; case 0x6000: return ((code & 0xf000) == 0x6000) ? Opcode.STAT : Opcode.SCAL; case 0x8000: return Opcode.CNTR; case 0xa000: return Opcode.JSR; case 0xc000: return Opcode.RTS; case 0xe000: return Opcode.JMP; default: return Opcode.Unknown; // shouldn't be possible } } // Sign-extend a signed 5-bit value. private static int sign5(int val) { byte val5 = (byte)(val << 3); return (sbyte)val5 >> 3; } // Sign-extend a signed 13-bit value. private static int sign13(int val) { ushort val13 = (ushort)(val << 3); return (short)val13 >> 3; } /// /// Converts a JSR/JMP operand to a file offset. /// /// AVG address operand. /// Base address of vector memory. /// File offset of instruction. /// False if the target address is outside the file bounds. private bool Branch(int vaddr, int baseAddr, ref int offset) { int fileAddr = baseAddr + vaddr * 2; int fileOffset = mAddrTrans.AddressToOffset(offset, fileAddr); if (fileOffset < 0) { mAppRef.ReportError("JMP/JSR to " + vaddr.ToString("x4") + " invalid"); return false; } offset = fileOffset; return true; } } }