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;