diff --git a/PluginCommon/VisBitmap8.cs b/PluginCommon/VisBitmap8.cs
index 0058a39..f2e8a00 100644
--- a/PluginCommon/VisBitmap8.cs
+++ b/PluginCommon/VisBitmap8.cs
@@ -43,8 +43,9 @@ namespace PluginCommon {
/// Bitmap width, in pixels.
/// Bitmap height, in pixels.
public VisBitmap8(int width, int height) {
- Debug.Assert(width > 0 && width <= MAX_DIMENSION);
- Debug.Assert(height > 0 && height <= MAX_DIMENSION);
+ if (width <= 0 || width > MAX_DIMENSION || height <= 0 || height > MAX_DIMENSION) {
+ throw new ArgumentException("Bad bitmap width/height " + width + "," + height);
+ }
Width = width;
Height = height;
diff --git a/SourceGen/Res/Strings.xaml b/SourceGen/Res/Strings.xaml
index ff33f3a..886ca7a 100644
--- a/SourceGen/Res/Strings.xaml
+++ b/SourceGen/Res/Strings.xaml
@@ -175,6 +175,6 @@ limitations under the License.
[new project]
*READ-ONLY*
[unset]
- {0}: {1} (+{2} more)
- {0}: {1}
+ {1} (+{2} more)
+ {1}
\ No newline at end of file
diff --git a/SourceGen/RuntimeData/Atari/VisAtari2600.cs b/SourceGen/RuntimeData/Atari/VisAtari2600.cs
new file mode 100644
index 0000000..214df79
--- /dev/null
+++ b/SourceGen/RuntimeData/Atari/VisAtari2600.cs
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2019 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.Generic;
+using System.Collections.ObjectModel;
+using System.Text;
+
+using PluginCommon;
+
+namespace RuntimeData.Atari {
+ public class VisAtari2600 : MarshalByRefObject, IPlugin, IPlugin_Visualizer {
+ // IPlugin
+ public string Identifier {
+ get { return "Atari 2600 Graphic 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_SPRITE = "atari2600-sprite";
+ private const string VIS_GEN_PLAYFIELD = "atari2600-playfield";
+
+ private const string P_OFFSET = "offset";
+ private const string P_HEIGHT = "height";
+ private const string P_ROW_DUP = "rowDup";
+ private const string P_REFLECTED = "reflected";
+
+ private const int MAX_HEIGHT = 192;
+ private const int HALF_WIDTH = 20;
+
+ // Visualization descriptors.
+ private VisDescr[] mDescriptors = new VisDescr[] {
+ new VisDescr(VIS_GEN_SPRITE, "Atari 2600 Sprite", VisDescr.VisType.Bitmap,
+ new VisParamDescr[] {
+ new VisParamDescr("File offset (hex)",
+ P_OFFSET, typeof(int), 0, 0x00ffffff, VisParamDescr.SpecialMode.Offset, 0),
+ new VisParamDescr("Height",
+ P_HEIGHT, typeof(int), 1, 192, 0, 1),
+ }),
+ new VisDescr(VIS_GEN_PLAYFIELD, "Atari 2600 Playfield", VisDescr.VisType.Bitmap,
+ new VisParamDescr[] {
+ new VisParamDescr("File offset (hex)",
+ P_OFFSET, typeof(int), 0, 0x00ffffff, VisParamDescr.SpecialMode.Offset, 0),
+ new VisParamDescr("Height",
+ P_HEIGHT, typeof(int), 1, 192, 0, 1),
+ new VisParamDescr("Row duplication",
+ P_ROW_DUP, typeof(int), 0, 10, 0, 3),
+ new VisParamDescr("Reflected",
+ P_REFLECTED, typeof(bool), 0, 0, 0, false),
+ }),
+ };
+
+
+ // 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() {
+ // We're using a static set, but it could be generated based on file contents.
+ // Confirm that we're prepared.
+ if (mFileData == null) {
+ return null;
+ }
+ return mDescriptors;
+ }
+
+ // IPlugin_Visualizer
+ public IVisualization2d Generate2d(VisDescr descr,
+ ReadOnlyDictionary parms) {
+ switch (descr.Ident) {
+ case VIS_GEN_SPRITE:
+ return GenerateSprite(parms);
+ case VIS_GEN_PLAYFIELD:
+ return GeneratePlayfield(parms);
+ default:
+ mAppRef.ReportError("Unknown ident " + descr.Ident);
+ return null;
+ }
+ }
+
+ private IVisualization2d GenerateSprite(ReadOnlyDictionary parms) {
+ int offset = Util.GetFromObjDict(parms, P_OFFSET, 0);
+ int height = Util.GetFromObjDict(parms, P_HEIGHT, 1);
+
+ if (offset < 0 || offset >= mFileData.Length ||
+ height <= 0 || height > MAX_HEIGHT) {
+ // the UI should flag these based on range (and ideally wouldn't have called us)
+ mAppRef.ReportError("Invalid parameter");
+ return null;
+ }
+
+ int lastOffset = offset + height - 1;
+ if (lastOffset >= mFileData.Length) {
+ mAppRef.ReportError("Sprite runs off end of file (last offset +" +
+ lastOffset.ToString("x6") + ")");
+ return null;
+ }
+
+ VisBitmap8 vb = new VisBitmap8(8, height);
+ SetPalette(vb);
+
+ for (int row = 0; row < height; row++) {
+ byte val = mFileData[offset + row];
+ for (int col = 0; col < 8; col++) {
+ if ((val & 0x80) != 0) {
+ vb.SetPixelIndex(col, row, (byte)Color.White);
+ } else {
+ vb.SetPixelIndex(col, row, (byte)Color.Black);
+ }
+ val <<= 1;
+ }
+ }
+ return vb;
+ }
+
+ private IVisualization2d GeneratePlayfield(ReadOnlyDictionary parms) {
+ const int BYTE_WIDTH = 3;
+
+ int offset = Util.GetFromObjDict(parms, P_OFFSET, 0);
+ int height = Util.GetFromObjDict(parms, P_HEIGHT, 1);
+ int rowDup = Util.GetFromObjDict(parms, P_ROW_DUP, 3);
+ bool isReflected = Util.GetFromObjDict(parms, P_REFLECTED, false);
+
+ if (offset < 0 || offset >= mFileData.Length ||
+ height <= 0 || height > MAX_HEIGHT) {
+ // the UI should flag these based on range (and ideally wouldn't have called us)
+ mAppRef.ReportError("Invalid parameter");
+ return null;
+ }
+
+ int lastOffset = offset + BYTE_WIDTH * height - 1;
+ if (lastOffset >= mFileData.Length) {
+ mAppRef.ReportError("Playfield runs off end of file (last offset +" +
+ lastOffset.ToString("x6") + ")");
+ return null;
+ }
+
+ int rowHeight = rowDup + 1;
+
+ // Each half of the playfield is 20 bits wide.
+ VisBitmap8 vb = new VisBitmap8(40, height * rowHeight);
+ SetPalette(vb);
+
+ for (int row = 0; row < height; row++) {
+ // Assume data is stored as PF0,PF1,PF2. PF0/PF2 are in reverse order, so
+ // start by assembling them as a reversed 20-bit word.
+ int srcOff = offset + row * BYTE_WIDTH;
+ int rev = (mFileData[srcOff] >> 4) | (RevBits(mFileData[srcOff + 1], 8) << 4) |
+ (mFileData[srcOff + 2] << 12);
+
+ // Now generate the forward order.
+ int fwd = RevBits(rev, 20);
+
+ // Render the first part of the line forward.
+ RenderHalfField(vb, row * rowHeight, rowHeight, 0,
+ fwd, Color.White);
+ // Render the second half forward or reversed, in grey.
+ RenderHalfField(vb, row * rowHeight, rowHeight, HALF_WIDTH,
+ isReflected ? rev : fwd, Color.Grey);
+ }
+ return vb;
+ }
+
+ private int RevBits(int val, int count) {
+ int result = 0;
+ for (int i = 0; i < count; i++) {
+ result <<= 1;
+ result |= val & 0x01;
+ val >>= 1;
+ }
+ return result;
+ }
+
+ private void RenderHalfField(VisBitmap8 vb, int row, int rowDup, int startCol, int val,
+ Color setColor) {
+ for (int col = startCol; col < startCol + HALF_WIDTH; col++) {
+ val <<= 1;
+ byte colorIdx;
+ if ((val & (1 << HALF_WIDTH)) != 0) {
+ colorIdx = (byte)setColor;
+ } else {
+ colorIdx = (byte)Color.Black;
+ }
+
+ for (int r = row; r < row + rowDup; r++) {
+ vb.SetPixelIndex(col, r, colorIdx);
+ }
+ }
+ }
+
+ private enum Color : byte {
+ Transparent = 0,
+ Black = 1,
+ White = 2,
+ Grey = 3
+ }
+
+ private void SetPalette(VisBitmap8 vb) {
+ vb.AddColor(0, 0, 0, 0); // 0=transparent
+ vb.AddColor(0xff, 0x00, 0x00, 0x00); // 1=black
+ vb.AddColor(0xff, 0xff, 0xff, 0xff); // 2=white
+ vb.AddColor(0xff, 0xd0, 0xd0, 0xd0); // 3=grey
+ }
+ }
+}
diff --git a/SourceGen/RuntimeData/Help/visualization.html b/SourceGen/RuntimeData/Help/visualization.html
index 541e62b..1521598 100644
--- a/SourceGen/RuntimeData/Help/visualization.html
+++ b/SourceGen/RuntimeData/Help/visualization.html
@@ -108,7 +108,7 @@ Some less-common parameters include:
visualizer will default to no interleave (stride == 1).
-Apple II - VisHiRes
+Apple II - Apple/VisHiRes
There is no standard format for small hi-res bitmaps, but certain
arrangements are common. The script defines three generators:
@@ -139,6 +139,23 @@ but has no effect on black or white.
The converter generates one output pixel for every source pixel, so
half-pixel shifts are not rendered.
+Atari 2600 - Atari/VisAtari2600
+
+The Atari 2600 graphics system has registers that determine the
+appearance of a sprite or playfield on a single row. The visualization
+generator works for data stored in a straightforward fashion.
+
+
+ - Sprite - basic 1xN sprite, converted to an image 8 pixels
+ wide.
+ - Playfield - assumes PF0,PF1,PF2 are stored in that order,
+ multiple entries following each other. Specify the number of
+ 3-byte entries as the height.
+ Since most playfields aren't the full height of the screen,
+ it will tend to look squashed. Use the "row duplication" feature
+ to repeat each row N times to make it look more like it should.
+
+