1
0
mirror of https://github.com/fadden/6502bench.git synced 2024-12-03 05:49:48 +00:00
6502bench/SourceGen/RuntimeData/Apple/VisHiRes.cs
Andy McFadden df04de61e6 Improve visualization
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.)
2019-12-04 15:59:37 -08:00

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
}
}
}