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.
  • +
+