mirror of
https://github.com/fadden/6502bench.git
synced 2024-12-03 05:49:48 +00:00
df04de61e6
Various improvements: - Switched to ReadOnlyDictionary in Visualization to make it clear that the parameter dictionary should not be modified. - Added a warning to the Visualization Set editor that appears when there are no plugins that implement a visualizer. - Make sure an item is selected in the set editor after edit/remove. - Replaced the checkerboard background with one that's a little bit more grey, so it's more distinct from white pixel data. - Added a new Apple II hi-res color converter whose output more closely matches KEGS and AppleWin RGB. - Added VisHiRes.cs to some Apple II system definitions. - Added some test bitmaps for Apple II hi-res to the test directory. (These are not part of an automated test.)
450 lines
22 KiB
C#
450 lines
22 KiB
C#
/*
|
|
* 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.Apple {
|
|
public class VisHiRes : MarshalByRefObject, IPlugin, IPlugin_Visualizer {
|
|
// IPlugin
|
|
public string Identifier {
|
|
get { return "Apple II Hi-Res 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_BITMAP = "apple2-hi-res-bitmap";
|
|
private const string VIS_GEN_BITMAP_FONT = "apple2-hi-res-bitmap-font";
|
|
|
|
private const string P_OFFSET = "offset";
|
|
private const string P_BYTE_WIDTH = "byteWidth";
|
|
private const string P_HEIGHT = "height";
|
|
private const string P_COL_STRIDE = "colStride";
|
|
private const string P_ROW_STRIDE = "rowStride";
|
|
private const string P_IS_COLOR = "isColor";
|
|
private const string P_IS_FIRST_ODD = "isFirstOdd";
|
|
|
|
private const string P_ITEM_BYTE_WIDTH = "itemByteWidth";
|
|
private const string P_ITEM_HEIGHT = "itemHeight";
|
|
private const string P_COUNT = "count";
|
|
|
|
private const int MAX_DIM = 4096;
|
|
|
|
// Visualization descriptors.
|
|
private VisDescr[] mDescriptors = new VisDescr[] {
|
|
new VisDescr(VIS_GEN_BITMAP, "Apple II Hi-Res Bitmap", VisDescr.VisType.Bitmap,
|
|
new VisParamDescr[] {
|
|
new VisParamDescr("File offset (hex)",
|
|
P_OFFSET, typeof(int), 0, 0x00ffffff, VisParamDescr.SpecialMode.Offset, 0),
|
|
new VisParamDescr("Width (in bytes)",
|
|
P_BYTE_WIDTH, typeof(int), 1, 40, 0, 1),
|
|
new VisParamDescr("Height",
|
|
P_HEIGHT, typeof(int), 1, 192, 0, 1),
|
|
new VisParamDescr("Column stride (bytes)",
|
|
P_COL_STRIDE, typeof(int), 0, 256, 0, 0),
|
|
new VisParamDescr("Row stride (bytes)",
|
|
P_ROW_STRIDE, typeof(int), 0, 256, 0, 0),
|
|
new VisParamDescr("Color",
|
|
P_IS_COLOR, typeof(bool), 0, 0, 0, true),
|
|
new VisParamDescr("First col odd",
|
|
P_IS_FIRST_ODD, typeof(bool), 0, 0, 0, false),
|
|
//new VisParamDescr("Test Float",
|
|
// "floaty", typeof(float), -5.0f, 5.0f, 0, 0.1f),
|
|
}),
|
|
new VisDescr(VIS_GEN_BITMAP_FONT, "Apple II Hi-Res Bitmap Font", VisDescr.VisType.Bitmap,
|
|
new VisParamDescr[] {
|
|
new VisParamDescr("File offset (hex)",
|
|
P_OFFSET, typeof(int), 0, 0x00ffffff, VisParamDescr.SpecialMode.Offset, 0),
|
|
new VisParamDescr("Item width (in bytes)",
|
|
P_ITEM_BYTE_WIDTH, typeof(int), 1, 40, 0, 1),
|
|
new VisParamDescr("Item height",
|
|
P_ITEM_HEIGHT, typeof(int), 1, 192, 0, 8),
|
|
new VisParamDescr("Number of items",
|
|
P_COUNT, typeof(int), 1, 256, 0, 1),
|
|
}),
|
|
};
|
|
|
|
|
|
// 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() {
|
|
return mDescriptors;
|
|
}
|
|
|
|
// IPlugin_Visualizer
|
|
public IVisualization2d Generate2d(VisDescr descr,
|
|
ReadOnlyDictionary<string, object> parms) {
|
|
switch (descr.Ident) {
|
|
case VIS_GEN_BITMAP:
|
|
return GenerateBitmap(parms);
|
|
case VIS_GEN_BITMAP_FONT:
|
|
// TODO (xyzzy)
|
|
return null;
|
|
default:
|
|
mAppRef.DebugLog("Unknown ident " + descr.Ident);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private IVisualization2d GenerateBitmap(ReadOnlyDictionary<string, object> parms) {
|
|
int offset, byteWidth, height, colStride, rowStride;
|
|
bool isColor, isFirstOdd;
|
|
|
|
offset = Util.GetFromObjDict(parms, P_OFFSET, 0);
|
|
byteWidth = Util.GetFromObjDict(parms, P_BYTE_WIDTH, 1); // width ignoring colStride
|
|
height = Util.GetFromObjDict(parms, P_HEIGHT, 1);
|
|
colStride = Util.GetFromObjDict(parms, P_COL_STRIDE, 0);
|
|
rowStride = Util.GetFromObjDict(parms, P_ROW_STRIDE, 0);
|
|
isColor = Util.GetFromObjDict(parms, P_IS_COLOR, true);
|
|
isFirstOdd = Util.GetFromObjDict(parms, P_IS_FIRST_ODD, false);
|
|
|
|
// We allow the stride entries to be zero to indicate a "dense" bitmap.
|
|
if (colStride == 0) {
|
|
colStride = 1;
|
|
}
|
|
if (rowStride == 0) {
|
|
rowStride = byteWidth * colStride;
|
|
}
|
|
|
|
if (offset < 0 || offset >= mFileData.Length ||
|
|
byteWidth <= 0 || byteWidth > MAX_DIM ||
|
|
height <= 0 || height > MAX_DIM) {
|
|
// the UI should flag these based on range (and ideally wouldn't have called us)
|
|
mAppRef.DebugLog("Invalid parameter");
|
|
return null;
|
|
}
|
|
if (colStride <= 0 || colStride > MAX_DIM) {
|
|
mAppRef.DebugLog("Invalid column stride");
|
|
return null;
|
|
}
|
|
if (rowStride < byteWidth * colStride - (colStride - 1) || rowStride > MAX_DIM) {
|
|
mAppRef.DebugLog("Invalid row stride");
|
|
return null;
|
|
}
|
|
|
|
int lastOffset = offset + rowStride * height - (colStride - 1) - 1;
|
|
if (lastOffset >= mFileData.Length) {
|
|
mAppRef.DebugLog("Bitmap runs off end of file (last offset +" +
|
|
lastOffset.ToString("x6") + ")");
|
|
return null;
|
|
}
|
|
|
|
|
|
VisBitmap8 vb = new VisBitmap8(byteWidth * 7, height);
|
|
SetHiResPalette(vb);
|
|
|
|
RenderBitmap(offset, byteWidth, height, colStride, rowStride,
|
|
isColor ? ColorMode.SimpleColor : ColorMode.Mono, isFirstOdd, vb);
|
|
return vb;
|
|
}
|
|
|
|
private enum ColorMode { Mono, ClunkyColor, SimpleColor, IIgsRGB };
|
|
|
|
private void RenderBitmap(int offset, int byteWidth, int height, int colStride,
|
|
int rowStride, ColorMode colorMode, bool isFirstOdd, VisBitmap8 vb) {
|
|
int bx = 0;
|
|
int by = 0;
|
|
switch (colorMode) {
|
|
case ColorMode.Mono: {
|
|
// Since we're not displaying this we don't need to worry about
|
|
// half-pixel shifts, and can just convert 7 bits to pixels.
|
|
for (int row = 0; row < height; row++) {
|
|
int colIdx = 0;
|
|
for (int col = 0; col < byteWidth; col++) {
|
|
byte val = mFileData[offset + colIdx];
|
|
for (int bit = 0; bit < 7; bit++) {
|
|
if ((val & 0x01) == 0) {
|
|
vb.SetPixelIndex(bx, by, (int)HiResColors.Black0);
|
|
} else {
|
|
vb.SetPixelIndex(bx, by, (int)HiResColors.White0);
|
|
}
|
|
val >>= 1;
|
|
bx++;
|
|
}
|
|
colIdx += colStride;
|
|
}
|
|
bx = 0;
|
|
by++;
|
|
offset += rowStride;
|
|
}
|
|
}
|
|
break;
|
|
case ColorMode.ClunkyColor: {
|
|
// We treat the data as a strictly 140-mode bitmap, which doesn't match
|
|
// up well with how the pixels will be displayed, but does allow a
|
|
// straightforward conversion between file formats. Color fringing is
|
|
// severe.
|
|
for (int row = 0; row < height; row++) {
|
|
int lastBit;
|
|
if (isFirstOdd) {
|
|
lastBit = 0; // pretend we already have one bit
|
|
} else {
|
|
lastBit = -1;
|
|
}
|
|
|
|
for (int colByte = 0; colByte < byteWidth; colByte += colStride) {
|
|
byte val = mFileData[offset + colByte];
|
|
bool hiBitSet = (val & 0x80) != 0;
|
|
|
|
// Grab 3 or 4 pairs of bits.
|
|
int pairCount = (lastBit < 0) ? 3 : 4;
|
|
while (pairCount-- > 0) {
|
|
int twoBits;
|
|
if (lastBit >= 0) {
|
|
// merge with bit from previous byte
|
|
twoBits = (lastBit << 1) | (val & 0x01);
|
|
val >>= 1;
|
|
lastBit = -1;
|
|
} else {
|
|
// grab two bits
|
|
twoBits = (val & 0x03);
|
|
val >>= 2;
|
|
}
|
|
|
|
if (hiBitSet) {
|
|
twoBits += 4;
|
|
}
|
|
|
|
// We're in 140 mode, so set two adjacent pixels.
|
|
vb.SetPixelIndex(bx++, by, sHiResColorMap[twoBits]);
|
|
vb.SetPixelIndex(bx++, by, sHiResColorMap[twoBits]);
|
|
}
|
|
|
|
bool thisEven = ((colByte & 0x01) == 0) ^ isFirstOdd;
|
|
if (thisEven) {
|
|
// started in even column we have one bit left over
|
|
lastBit = val & 0x01;
|
|
} else {
|
|
// started in odd column, all bits consumed
|
|
lastBit = -1;
|
|
}
|
|
}
|
|
bx = 0;
|
|
by++;
|
|
offset += rowStride;
|
|
}
|
|
}
|
|
break;
|
|
case ColorMode.SimpleColor: {
|
|
// Straightforward conversion, with no funky border effects. This
|
|
// represents an idealized version of the hardware.
|
|
|
|
// Bits for every byte, plus a couple of "fake" bits on the ends so
|
|
// we don't have to throw range-checks everywhere.
|
|
const int OVER = 2;
|
|
bool[] lineBits = new bool[OVER + byteWidth * 7 + OVER];
|
|
bool[] hiFlags = new bool[OVER + byteWidth * 7 + OVER];
|
|
for (int row = 0; row < height; row++) {
|
|
// Unravel the bits. Note we do each byte "backwards", i.e. the
|
|
// low bit (which is generally considered to be on the right) is
|
|
// the leftmost pixel.
|
|
int idx = OVER; // start past "fake" bits
|
|
int colIdx = 0;
|
|
for (int col = 0; col < byteWidth; col++) {
|
|
byte val = mFileData[offset + colIdx];
|
|
bool hiBitSet = (val & 0x80) != 0;
|
|
|
|
for (int bit = 0; bit < 7; bit++) {
|
|
hiFlags[idx] = hiBitSet;
|
|
lineBits[idx] = (val & 0x01) != 0;
|
|
idx++;
|
|
val >>= 1;
|
|
}
|
|
colIdx += colStride;
|
|
}
|
|
|
|
// Convert to color.
|
|
int lastBit = byteWidth * 7;
|
|
for (idx = OVER; idx < lastBit + OVER; idx++) {
|
|
int colorShift = hiFlags[idx] ? 2 : 0;
|
|
if (lineBits[idx] && (lineBits[idx - 1] || lineBits[idx + 1])) {
|
|
// [X]11 or [1]1X; two 1s in a row is always white
|
|
vb.SetPixelIndex(bx++, by, (byte)HiResColors.White0);
|
|
} else if (lineBits[idx]) {
|
|
// [0]10, color pixel
|
|
bool isOdd = ((idx & 0x01) != 0) ^ isFirstOdd;
|
|
if (isOdd) {
|
|
vb.SetPixelIndex(bx++, by,
|
|
(byte)((int)HiResColors.Green + colorShift));
|
|
} else {
|
|
vb.SetPixelIndex(bx++, by,
|
|
(byte)((int)HiResColors.Purple + colorShift));
|
|
}
|
|
} else if (lineBits[idx - 1] && lineBits[idx + 1]) {
|
|
// [1]01, keep color going
|
|
bool isOdd = ((idx & 0x01) != 0) ^ isFirstOdd;
|
|
if (isOdd) {
|
|
vb.SetPixelIndex(bx++, by,
|
|
(byte)((int)HiResColors.Purple + colorShift));
|
|
} else {
|
|
vb.SetPixelIndex(bx++, by,
|
|
(byte)((int)HiResColors.Green + colorShift));
|
|
}
|
|
} else {
|
|
// [0]0X or [X]01
|
|
vb.SetPixelIndex(bx++, by, (byte)HiResColors.Black0);
|
|
}
|
|
}
|
|
|
|
// move to next row
|
|
bx = 0;
|
|
by++;
|
|
offset += rowStride;
|
|
}
|
|
}
|
|
break;
|
|
case ColorMode.IIgsRGB: {
|
|
// Color conversion similar to what CiderPress does, but without the
|
|
// half-pixel shift (we're trying to create a 1:1 bitmap, not 1:2).
|
|
//
|
|
// This replicates some of the oddness in Apple IIgs RGB monitor output,
|
|
// but it's not quite right though. For example:
|
|
//
|
|
// observed generated
|
|
// d5 2a: blue [dk blue] purple ... black ...
|
|
// aa 55: orange [yellow] green ... white ...
|
|
// 55 aa: purple [lt blue] blue ... black ...
|
|
// 2a d5: green [brown] orange ... black ...
|
|
//
|
|
// KEGS doesn't seem to try to model this; it shows solid colors with no
|
|
// wackiness. AppleWin in "Color TV" mode shows similar effects, but is
|
|
// much blurrier (by design).
|
|
bool[] lineBits = new bool[byteWidth * 7];
|
|
bool[] hiFlags = new bool[byteWidth * 7]; // overkill, but simpler
|
|
int[] colorBuf = new int[byteWidth * 7];
|
|
for (int row = 0; row < height; row++) {
|
|
// Unravel the bits.
|
|
int idx = 0;
|
|
int colIdx = 0;
|
|
for (int col = 0; col < byteWidth; col++) {
|
|
byte val = mFileData[offset + colIdx];
|
|
bool hiBitSet = (val & 0x80) != 0;
|
|
|
|
for (int bit = 0; bit < 7; bit++) {
|
|
hiFlags[idx] = hiBitSet;
|
|
lineBits[idx] = (val & 0x01) != 0;
|
|
idx++;
|
|
val >>= 1;
|
|
}
|
|
colIdx += colStride;
|
|
}
|
|
|
|
// Convert to color.
|
|
int lastBit = byteWidth * 7;
|
|
for (idx = 0; idx < lastBit; idx++) {
|
|
int colorShift = hiFlags[idx] ? 2 : 0;
|
|
if (!lineBits[idx]) {
|
|
// Bit not set, set pixel to black.
|
|
colorBuf[idx] = (int)HiResColors.Black0;
|
|
} else {
|
|
// Bit set, set pixel to white or color.
|
|
if (idx > 0 && colorBuf[idx - 1] != (int)HiResColors.Black0) {
|
|
// previous bit was also set, this is white
|
|
colorBuf[idx] = (int)HiResColors.White0;
|
|
|
|
// the previous pixel is part of a run of white
|
|
colorBuf[idx - 1] = (int)HiResColors.White0;
|
|
} else {
|
|
// previous bit not set *or* was first pixel in line;
|
|
// set color based on whether this is even or odd pixel col
|
|
bool isOdd = ((idx & 0x01) != 0) ^ isFirstOdd;
|
|
if (isOdd) {
|
|
colorBuf[idx] = (int)HiResColors.Green + colorShift;
|
|
} else {
|
|
colorBuf[idx] = (int)HiResColors.Purple + colorShift;
|
|
}
|
|
}
|
|
|
|
// Do we have a run of the same color? If so, smooth the
|
|
// color out. Note that white blends smoothly with everything.
|
|
if (idx > 1 && (colorBuf[idx - 2] == colorBuf[idx] ||
|
|
colorBuf[idx - 2] == (int)HiResColors.White0)) {
|
|
colorBuf[idx - 1] = colorBuf[idx];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write to bitmap.
|
|
for (idx = 0; idx < lastBit; idx++) {
|
|
vb.SetPixelIndex(bx++, by, (byte)colorBuf[idx]);
|
|
}
|
|
|
|
// move to next row
|
|
bx = 0;
|
|
by++;
|
|
offset += rowStride;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
// just leave the bitmap empty
|
|
mAppRef.DebugLog("Unknown ColorMode " + colorMode);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Map hi-res colors to palette entries.
|
|
/// </summary>
|
|
private enum HiResColors : byte {
|
|
Black0 = 1,
|
|
Green = 3,
|
|
Purple = 4,
|
|
White0 = 2,
|
|
Black1 = 1,
|
|
Orange = 5,
|
|
Blue = 6,
|
|
White1 = 2
|
|
}
|
|
|
|
// Maps HiResColors to the palette entries. Used for mapping 2-bit values to colors.
|
|
private static readonly byte[] sHiResColorMap = new byte[8] {
|
|
1, 3, 4, 2, 1, 5, 6, 2
|
|
};
|
|
|
|
private void SetHiResPalette(VisBitmap8 vb) {
|
|
// These don't match directly to hi-res color numbers because we want to
|
|
// avoid adding black/white twice. The colors correspond to Apple IIgs RGB
|
|
// monitor output.
|
|
vb.AddColor(0, 0, 0, 0); // 0=transparent
|
|
vb.AddColor(0xff, 0x00, 0x00, 0x00); // 1=black0/black1
|
|
vb.AddColor(0xff, 0xff, 0xff, 0xff); // 2=white0/white1
|
|
vb.AddColor(0xff, 0x11, 0xdd, 0x00); // 3=green
|
|
vb.AddColor(0xff, 0xdd, 0x22, 0xdd); // 4=purple
|
|
vb.AddColor(0xff, 0xff, 0x66, 0x00); // 5=orange
|
|
vb.AddColor(0xff, 0x22, 0x22, 0xff); // 6=blue
|
|
}
|
|
}
|
|
}
|