From d48ab7582f68b199930895d1f7c36718dc5bf8c5 Mon Sep 17 00:00:00 2001 From: Andy McFadden Date: Mon, 1 Nov 2021 15:07:29 -0700 Subject: [PATCH] Add Atari DVG visualizer The DVG format, used for vector games like Asteroids, is the predecessor to the AVG graphics used in games like Battlezone. Also, added some extended error checks on wireframe vertices. Also, minor edits to the README and daily tips. --- PluginCommon/VisWireframe.cs | 10 + README.md | 9 +- SourceGen/RuntimeData/Atari/VisAVG.cs | 12 +- SourceGen/RuntimeData/Atari/VisDVG.cs | 355 +++++++++++++++++++++ SourceGen/RuntimeData/Tips/daily-tips.json | 2 +- SourceGen/WireframeObject.cs | 13 + 6 files changed, 391 insertions(+), 10 deletions(-) create mode 100644 SourceGen/RuntimeData/Atari/VisDVG.cs diff --git a/PluginCommon/VisWireframe.cs b/PluginCommon/VisWireframe.cs index 6b2d259..b573b43 100644 --- a/PluginCommon/VisWireframe.cs +++ b/PluginCommon/VisWireframe.cs @@ -75,6 +75,9 @@ namespace PluginCommon { /// Z coordinate. /// Vertex index. Indices start at zero and count up. public int AddVertex(float x, float y, float z) { + if (float.IsNaN(x) || float.IsNaN(y) || float.IsNaN(z)) { + throw new Exception("Invalid vertex x=" + x + " y=" + y + " z=" + z); + } mVerticesX.Add(x); mVerticesY.Add(y); mVerticesZ.Add(z); @@ -113,6 +116,9 @@ namespace PluginCommon { /// Z coordinate. /// Face index. Indices start at zero and count up. public int AddFaceNormal(float x, float y, float z) { + if (float.IsNaN(x) || float.IsNaN(y) || float.IsNaN(z)) { + throw new Exception("Invalid normal x=" + x + " y=" + y + " z=" + z); + } Debug.Assert(x != 0.0f || y != 0.0f || z != 0.0f); // no zero-length normals mNormalsX.Add(x); mNormalsY.Add(y); @@ -128,6 +134,10 @@ namespace PluginCommon { /// Y coordinate. /// Z coordinate. public void ReplaceFaceNormal(int index, float x, float y, float z) { + if (float.IsNaN(x) || float.IsNaN(y) || float.IsNaN(z)) { + throw new Exception("Invalid normal x=" + x + " y=" + y + " z=" + z); + } + Debug.Assert(x != 0.0f || y != 0.0f || z != 0.0f); // no zero-length normals mNormalsX[index] = x; mNormalsY[index] = y; mNormalsZ[index] = z; diff --git a/README.md b/README.md index f929c7b..fe3a430 100644 --- a/README.md +++ b/README.md @@ -146,10 +146,11 @@ that .NET apps don't work under WINE, so it can only be run on a full Windows system emulation. (SourceGen versions 1.0 and 1.1 used the WinForms API, which has been -implemented for Mono, but after encountering significant bugs that I wasn't -able to work around I abandoned the approach and switched to WPF. Besides -working better under Windows, WPF uses a more modern approach (XAML) that -may ease the transition to a cross-platform GUI API like Avalonia.) +implemented for [Mono](https://www.mono-project.com/), but after +encountering significant bugs that I wasn't able to work around I +abandoned the approach and switched to WPF. Besides working better +under Windows, WPF uses a more modern approach (XAML) that may ease +the transition to a cross-platform GUI API like Avalonia or MAUI.) ## Getting Started ## diff --git a/SourceGen/RuntimeData/Atari/VisAVG.cs b/SourceGen/RuntimeData/Atari/VisAVG.cs index 541a657..e87d0df 100644 --- a/SourceGen/RuntimeData/Atari/VisAVG.cs +++ b/SourceGen/RuntimeData/Atari/VisAVG.cs @@ -20,7 +20,9 @@ using PluginCommon; namespace RuntimeData.Atari { /// - /// Visualizer for Atari Analog Vector Generator commands. + /// 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 @@ -174,7 +176,7 @@ namespace RuntimeData.Atari { ii *= 2; } - // note dx/dy==0 is not supported for SVEC + // 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) { @@ -248,7 +250,7 @@ namespace RuntimeData.Atari { return vw; } - private Opcode GetOpcode(ushort code) { + private static Opcode GetOpcode(ushort code) { switch (code & 0xe000) { case 0x0000: return Opcode.VCTR; case 0x2000: return Opcode.HALT; @@ -263,13 +265,13 @@ namespace RuntimeData.Atari { } // Sign-extend a signed 5-bit value. - int sign5(int val) { + private static int sign5(int val) { byte val5 = (byte)(val << 3); return (sbyte)val5 >> 3; } // Sign-extend a signed 13-bit value. - int sign13(int val) { + private static int sign13(int val) { ushort val13 = (ushort)(val << 3); return (short)val13 >> 3; } diff --git a/SourceGen/RuntimeData/Atari/VisDVG.cs b/SourceGen/RuntimeData/Atari/VisDVG.cs new file mode 100644 index 0000000..94867f3 --- /dev/null +++ b/SourceGen/RuntimeData/Atari/VisDVG.cs @@ -0,0 +1,355 @@ +/* + * Copyright 2021 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 Digital Vector Generator commands (Asteroids, etc). + /// + /// Currently ignores beam brightness, except as on/off. + /// + /// References: + /// http://computerarcheology.com/Arcade/Asteroids/DVG.html + /// https://www.nicholasmikstas.com/asteroids-vector-rom + /// + /// 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 VisDVG : MarshalByRefObject, IPlugin, IPlugin_Visualizer_v2 { + private readonly bool VERBOSE = false; + + // IPlugin + public string Identifier { + get { return "Atari DVG 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_DVG = "atari-dvg"; + + private const string P_OFFSET = "offset"; + private const string P_BASE_ADDR = "baseAddr"; + private const string P_IGNORE_CUR = "ignoreCur"; + + // Visualization descriptors. + private VisDescr[] mDescriptors = new VisDescr[] { + new VisDescr(VIS_GEN_DVG, "Atari DVG 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, 0x4000), + new VisParamDescr("Ignore CUR movement", + P_IGNORE_CUR, typeof(bool), false, true, 0, false), + + 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_DVG: + return GenerateWireframe(parms); + default: + mAppRef.ReportError("Unknown ident " + descr.Ident); + return null; + } + } + + private enum Opcode { + Unknown = 0, VEC, CUR, HALT, JSR, RTS, JMP, SVEC + } + + private IVisualizationWireframe GenerateWireframe(ReadOnlyDictionary parms) { + int offset = Util.GetFromObjDict(parms, P_OFFSET, 0); + int baseAddr = Util.GetFromObjDict(parms, P_BASE_ADDR, 0); + bool ignoreCur = Util.GetFromObjDict(parms, P_IGNORE_CUR, false); + + 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; + + double beamX = 0; + double beamY = 0; + int scaleFactor = 0; // tiny + 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); + + switch (opc) { + case Opcode.VEC: { // SSSS -mYY YYYY YYYY | BBBB -mXX XXXX XXXX + ushort code1 = (ushort)Util.GetWord(mFileData, offset, 2, false); + offset += 2; + + int yval = sign11(code0 & 0x07ff); + int xval = sign11(code1 & 0x07ff); + int localsc = code0 >> 12; // local scale + int bb = code1 >> 12; // brightness + + double scale = CalcScaleVEC(scaleFactor + localsc); + double dx = xval * scale; + double dy = yval * scale; + + beamX += dx; + beamY += dy; + if (bb == 0) { + // move only + curVertex = vw.AddVertex((float)beamX, (float)beamY, 0); + } else if (xval == 0 && yval == 0) { + // plot point + vw.AddPoint(curVertex); + //mAppRef.DebugLog("PLOT v" + curVertex + ": " + // + beamX + "," + beamY); + } else { + // draw line from previous vertex + int newVertex = vw.AddVertex((float)beamX, (float)beamY, 0); + vw.AddEdge(curVertex, newVertex); + curVertex = newVertex; + } + + if (VERBOSE) { + mAppRef.DebugLog("VEC scale=" + localsc + " x=" + dx + + " y=" + dy + " b=" + bb + " --> dx=" + dx + + " dy=" + dy); + } + } + break; + case Opcode.CUR: { // 1010 00yy yyyy yyyy | SSSS 00xx xxxx xxxx + ushort code1 = (ushort)Util.GetWord(mFileData, offset, 2, false); + offset += 2; + + int yc = code0 & 0x07ff; + int xc = code1 & 0x07ff; + int scale = code1 >> 12; + + if (!ignoreCur) { + // Some things do a big screen movement before they start + // drawing, which throws off the auto-scaling. The output + // looks better if we ignore the initial movement. + beamX = xc; + beamY = yc; + } + // Sign-extend the scale factor. (It's usually 0 or 1 in ROM.) + byte left = (byte)(scale << 4); + scaleFactor = (sbyte)left >> 4; + + if (VERBOSE) { + mAppRef.DebugLog("CUR scale=" + scale + " x=" + xc + + " y=" + yc); + } + } + break; + case Opcode.HALT: // 1011 0000 0000 0000 + if (stackPtr != 0) { + mAppRef.DebugLog("NOTE: encountered HALT with nonzero stack"); + } + done = true; + break; + case Opcode.JSR: { // 1100 aaaa aaaa aaaa + int vaddr = code0 & 0x0fff; + 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.JMP: { // 1110 aaaa aaaa aaaa + int vaddr = code0 & 0x0fff; + if (!Branch(vaddr, baseAddr, ref offset)) { + return null; + } + } + break; + case Opcode.RTS: // 1101 0000 0000 0000 + if (stackPtr == 0) { + done = true; + } else { + offset = stack[--stackPtr]; + } + break; + case Opcode.SVEC: { // 1111 smYY BBBB SmXX + int yval = sign3((code0 >> 8) & 0x07); + int xval = sign3(code0 & 0x07); + int localsc = ((code0 >> 11) & 0x01) | ((code0 >> 2) & 0x02); + // SVEC scale is VEC scale + 2 + double scale = CalcScaleVEC(scaleFactor + localsc + 2); + int bb = (code0 >> 4) & 0x0f; + + // The dx/dy values need to be * 256 to make them work right. + double dx = (xval << 8) * scale; + double dy = (yval << 8) * scale; + beamX += dx; + beamY += dy; + + if (bb == 0) { + // move only + curVertex = vw.AddVertex((float)beamX, (float)beamY, 0); + } else if (xval == 0 && yval == 0) { + // plot point + vw.AddPoint(curVertex); + //mAppRef.DebugLog("SPLOT v" + curVertex + ": " + // + beamX + "," + beamY); + } else { + // draw line from previous vertex + int newVertex = vw.AddVertex((float)beamX, (float)beamY, 0); + vw.AddEdge(curVertex, newVertex); + curVertex = newVertex; + } + if (VERBOSE) { + mAppRef.DebugLog("SVEC scale=" + localsc + " x=" + dx + + " y=" + dy + " b=" + bb + " --> dx=" + dx + + " dy=" + dy); + } + } + 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 & 0xf000) { + case 0xa000: return Opcode.CUR; + case 0xb000: return Opcode.HALT; + case 0xc000: return Opcode.JSR; + case 0xd000: return Opcode.RTS; + case 0xe000: return Opcode.JMP; + case 0xf000: return Opcode.SVEC; + default: return Opcode.VEC; // 0x0nnn - 0x9nnn + } + } + + // Convert scale factor (0-9) to a multiplier. + private static double CalcScaleVEC(int scaleFactor) { + // 0 is N/512, 9 is N/1. + if (scaleFactor < 0) { + scaleFactor = 0; + } else if (scaleFactor > 9) { + scaleFactor = 9; + } + return 1.0 / (1 << (9 - scaleFactor)); + } + + // Set the sign for a 2-bit value with a sign flag in the 3rd bit. + private static int sign3(int val) { + if ((val & 0x0004) == 0) { + return val; + } else { + return -(val & 0x03); + } + } + + // Set the sign for a 10-bit value with a sign flag in the 11th bit. + private static int sign11(int val) { + if ((val & 0x0400) == 0) { + return val; + } else { + return -(val & 0x03ff); + } + } + + /// + /// Converts a JSR/JMP operand to a file offset. + /// + /// DVG 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; + } + } +} diff --git a/SourceGen/RuntimeData/Tips/daily-tips.json b/SourceGen/RuntimeData/Tips/daily-tips.json index 69510e6..1b0410d 100644 --- a/SourceGen/RuntimeData/Tips/daily-tips.json +++ b/SourceGen/RuntimeData/Tips/daily-tips.json @@ -43,7 +43,7 @@ "Text" : "2D bitmap images and 3D wireframe meshes can be converted to images that are displayed inline. This can make it much easier to figure out what a piece of code is drawing." }, { - "Text" : "Large address tables can be formatted with a single operation. Various arrangements of address bytes are supported." + "Text" : "Large tables of pointers to code and data can be formatted with a single operation. Various arrangements of address bytes are supported, including low/high parts split into separate tables." }, { "Text" : "Source code can be generated for several cross-assemblers, or exported to HTML with embedded graphics. Animations can be exported as animated GIFs." diff --git a/SourceGen/WireframeObject.cs b/SourceGen/WireframeObject.cs index a30ccaf..4c3d8c3 100644 --- a/SourceGen/WireframeObject.cs +++ b/SourceGen/WireframeObject.cs @@ -34,6 +34,10 @@ namespace SourceGen { public double Y1 { get; private set; } public LineSeg(double x0, double y0, double x1, double y1) { + if (double.IsNaN(x0) || double.IsNaN(y0) || double.IsNaN(x1) || double.IsNaN(y1)) { + throw new Exception("Invalid LineSeg: x0=" + x0 + " y0=" + y0 + + " x1=" + x1 + " y1=" + y1); + } X0 = x0; Y0 = y0; X1 = x1; @@ -243,6 +247,15 @@ namespace SourceGen { bigMagRc = mag; } } + + // Avoid divide-by-zero. + if (bigMag == 0) { + Debug.WriteLine("NOTE: wireframe magnitude was zero"); + bigMag = 1; + } + if (bigMagRc == 0) { + bigMagRc = 1; + } wireObj.mBigMag = bigMag; wireObj.mBigMagRc = bigMagRc;