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;