1
0
mirror of https://github.com/fadden/6502bench.git synced 2024-11-25 14:34:27 +00:00

Add NES visualization generator

Added a visualizer for the CHR ROM pattern tables, and a semi-useful
visualizer for tile grids.

Also added a few chars in an 8x8 font that visualizers can use to
label things.
This commit is contained in:
Andy McFadden 2020-05-14 15:34:05 -07:00
parent 63d7a48705
commit 100d2ffc13
4 changed files with 606 additions and 2 deletions

244
CommonUtil/Font8x8.cs Normal file
View File

@ -0,0 +1,244 @@
/*
* Copyright 2020 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.Diagnostics;
namespace CommonUtil {
public static class Font8x8 {
public static int[] GetBitData(char ch) {
if (sBitData == null) {
InitBitData();
}
int index = MapChar(ch);
return sBitData[index];
}
private static int MapChar(char ch) {
if (ch == ' ') {
return 1;
} else if (ch >= '0' && ch <= '9') {
return ch - '0' + 2;
} else if (ch >= 'A' && ch <= 'F') {
return ch - 'A' + 12;
} else {
return 0;
}
}
private static List<int[]> sBitData;
/// <summary>
/// Converts the easy-to-edit string data into easy-to-process bitmaps.
/// </summary>
private static void InitBitData() {
Debug.Assert(sBitData == null);
sBitData = new List<int[]>(sFontData.Length);
for (int i = 0; i < sFontData.Length; i++) {
int[] bits = new int[8];
string str = sFontData[i];
for (int row = 0; row < 8; row++) {
byte data = 0;
for (int col = 0; col < 8; col++) {
data <<= 1;
char ch = str[row * 8 + col];
if (ch == '#') {
data |= 1;
} else if (ch != '.') {
Debug.WriteLine("Unknown char '" + ch + "' in Font8x8 data " + i);
}
}
bits[row] = data;
}
sBitData.Add(bits);
}
}
private static string[] sFontData = {
// unknown value (U+FFFD)
"..###..." +
".#####.." +
"###.###." +
"##.#.##." +
"###.###." +
".#####.." +
"..##...." +
"........",
// ' '
"........" +
"........" +
"........" +
"........" +
"........" +
"........" +
"........" +
"........",
// '0'
".#####.." +
"#.....#." +
"#...#.#." +
"#..#..#." +
"#.#...#." +
"#.....#." +
".#####.." +
"........",
// '1'
"...#...." +
"..##...." +
".#.#...." +
"...#...." +
"...#...." +
"...#...." +
".#####.." +
"........",
// '2'
".#####.." +
"#.....#." +
"......#." +
".#####.." +
"#......." +
"#......." +
"#######." +
"........",
// '3'
"######.." +
"......#." +
"......#." +
".#####.." +
"......#." +
"......#." +
"######.." +
"........",
// '4'
".....#.." +
"....##.." +
"...#.#.." +
"..#..#.." +
".#...#.." +
"#######." +
".....#.." +
"........",
// '5'
"#######." +
"#......." +
"#......." +
".#####.." +
"......#." +
"#.....#." +
".#####.." +
"........",
// '6'
".#####.." +
"#.....#." +
"#......." +
"######.." +
"#.....#." +
"#.....#." +
".#####.." +
"........",
// ' '
"#######." +
"......#." +
".....#.." +
"....#..." +
"...#...." +
"..#....." +
".#......" +
"........",
// ' '
".#####.." +
"#.....#." +
"#.....#." +
".#####.." +
"#.....#." +
"#.....#." +
".#####.." +
"........",
// '9'
".#####.." +
"#.....#." +
"#.....#." +
".######." +
"......#." +
"......#." +
".#####.." +
"........",
// 'A'
"...#...." +
"..#.#..." +
".#...#.." +
"#######." +
"#.....#." +
"#.....#." +
"#.....#." +
"........",
// 'B'
"######.." +
"#.....#." +
"#.....#." +
"######.." +
"#.....#." +
"#.....#." +
"######.." +
"........",
// 'C'
".#####.." +
"#.....#." +
"#......." +
"#......." +
"#......." +
"#.....#." +
".#####.." +
"........",
// 'D'
"######.." +
"#.....#." +
"#.....#." +
"#.....#." +
"#.....#." +
"#.....#." +
"######.." +
"........",
// 'E'
"#######." +
"#......." +
"#......." +
"######.." +
"#......." +
"#......." +
"#######." +
"........",
// 'F'
"#######." +
"#......." +
"#......." +
"######.." +
"#......." +
"#......." +
"#......." +
"........",
};
}
}

View File

@ -16,6 +16,8 @@
using System;
using System.Diagnostics;
using CommonUtil;
namespace PluginCommon {
/// <summary>
/// Bitmap with 8-bit palette indices, for use with visualization generators.
@ -137,5 +139,34 @@ namespace PluginCommon {
public void AddColor(byte a, byte r, byte g, byte b) {
AddColor(Util.MakeARGB(a, r, g, b));
}
/// <summary>
/// Draws an 8x8 character on the bitmap.
/// </summary>
/// <param name="vb">Bitma to draw on.</param>
/// <param name="ch">Character to draw.</param>
/// <param name="xc">X coord of upper-left pixel.</param>
/// <param name="yc">Y coord of upper-left pixel.</param>
/// <param name="foreColor">Foreground color index.</param>
/// <param name="backColor">Background color index.</param>
public static void DrawChar(VisBitmap8 vb, char ch, int xc, int yc,
byte foreColor, byte backColor) {
int origXc = xc;
int[] charBits = Font8x8.GetBitData(ch);
for (int row = 0; row < 8; row++) {
int rowBits = charBits[row];
for (int col = 7; col >= 0; col--) {
if ((rowBits & (1 << col)) != 0) {
vb.SetPixelIndex(xc, yc, foreColor);
} else {
vb.SetPixelIndex(xc, yc, backColor);
}
xc++;
}
xc = origXc;
yc++;
}
}
}
}

View File

@ -229,8 +229,9 @@ is the shape number.</p>
<p>The Atari 2600 graphics system has registers that determine the
appearance of a sprite or playfield on a single row. The register
values are typically changed as the screen is drawn to get different
data on successive rows. The visualization generator works for data
stored in a straightforward fashion.</p>
data on successive rows. The visualization generator doesn't attempt
to emulate this behavior, but works well for data stored in a
straightforward fashion.</p>
<ul>
<li><b>Sprite</b> - basic 1xN sprite, converted to an image 8 pixels
@ -245,6 +246,17 @@ stored in a straightforward fashion.</p>
repeated as-is or flipped.</li>
</ul>
<h3>Atari Arcade - Atari/VisAVG </h3>
<p>Different versions of Atari's Analog Vector Graphics were used in
several games, notably Battlezone, Tempest, and Star Wars. The commands
drove a vector display monitor. SourceGen visualizes them as 2D
wireframes, which isn't a perfect fit since they can describe points as
well as lines, but works fine for annotating a disassembly.</p>
<p>The visualizer takes two arguments: the offset of the start of
the commands to visualize, and the base address of vector RAM. The latter
is necessary to convert AVG JMP/JSR commands into offsets.</p>
<h3>Commodore 64 - Commodore/VisC64</h3>
<p>The Commodore 64 has a 64-bit sprite format defined by the hardware.
@ -280,6 +292,16 @@ It comes in two basic varieties:</p>
a sprite that is doubled in both width and height will look exactly like
a sprite that is not doubled at all.</p>
<h3>Nintendo Entertainment System - Nintendo/VisNES</h3>
<p>NES PPU pattern tables hold 8x8 tiles with 2 bits of color per pixel.
Converting the full collection to a reference bitmap is straightforward.
A few color palette options are offered.</p>
<p>Sprites and backgrounds are formed from collections of tiles. In
some cases this is straightfoward, in others it's not. A visualization
generator that renders a "tile grid" is available for simpler cases.</p>
</div>
<div id="footer">

View File

@ -0,0 +1,307 @@
/*
* Copyright 2020 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 PluginCommon;
namespace RuntimeData.Nintendo {
/// <summary>
/// Visualization generators for Nintendo Entertainment System graphics.
///
/// The full PPU pattern table grid is pretty straightforward. The way the tiles are
/// combined into sprites and background is not. This presents a "tile grid" that
/// shows a simple MxN grid of tiles in row-major order, but reality seems to be
/// more complex than that and may be game-specific.
///
/// To simplify things, the CHR ROM section must be labeled "CHR_ROM", and should have
/// a unique address.
/// </summary>
public class VisNES : MarshalByRefObject, IPlugin, IPlugin_SymbolList, IPlugin_Visualizer {
// IPlugin
public string Identifier {
get { return "Nintendo Entertainment System Graphic Visualizer"; }
}
private IApplication mAppRef;
private byte[] mFileData;
private AddressTranslate mAddrTrans;
private const string CHR_ROM = "CHR_ROM";
private int mChrRomOffset = -1;
// Visualization identifiers; DO NOT change or projects that use them will break.
private const string VIS_CHR_ROM = "nes-chr-rom";
private const string VIS_TILE_GRID = "nes-tile-grid";
private const string P_OFFSET = "offset";
private const string P_WIDTH = "width";
private const string P_HEIGHT = "height";
private const string P_COLOR_PALETTE = "colorPalette";
private const string P_SHOW_LABELS = "showLabels";
private const string P_FLIP_RIGHT = "flipRight";
private const string P_RIGHT_TABLE = "useRightTable";
private const int TileWidth = 8;
private const int TileHeight = 8;
private const int BytesPerTile = 16;
// Visualization descriptors.
private VisDescr[] mDescriptors = new VisDescr[] {
new VisDescr(VIS_CHR_ROM, "NES CHR ROM Pattern Tables", VisDescr.VisType.Bitmap,
new VisParamDescr[] {
//new VisParamDescr("File offset (hex)",
// P_OFFSET, typeof(int), 0, 0x00ffffff, VisParamDescr.SpecialMode.Offset, 0),
// TODO: either make this an enum, or just provide 4 slots that take a color
// (add a SpecialMode.Color and accept six-digit #123456 inputs;
// no need to restrict to NES limitations)
new VisParamDescr("Color palette",
P_COLOR_PALETTE, typeof(int), 0, 2, 0, 0),
new VisParamDescr("Show labels",
P_SHOW_LABELS, typeof(bool), 0, 0, 0, true),
}),
new VisDescr(VIS_TILE_GRID, "NES Tile Grid", VisDescr.VisType.Bitmap,
new VisParamDescr[] {
new VisParamDescr("File offset (hex)",
P_OFFSET, typeof(int), 0, 0x00ffffff, VisParamDescr.SpecialMode.Offset, 0),
new VisParamDescr("Color palette",
P_COLOR_PALETTE, typeof(int), 0, 2, 0, 0),
new VisParamDescr("Width (in tiles)",
P_WIDTH, typeof(int), 1, 256, 0, 1),
new VisParamDescr("Height (in tiles)",
P_HEIGHT, typeof(int), 1, 256, 0, 1),
// Flips the pixels of the tiles on the right side. This handles a common
// case, but in practice a sprite can be an arbitrary mix of flipped and
// normal tiles.
new VisParamDescr("Horiz-flip right side",
P_FLIP_RIGHT, typeof(bool), 0, 0, 0, false),
new VisParamDescr("Use right table",
P_RIGHT_TABLE, 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_SymbolList
public void UpdateSymbolList(List<PlSymbol> plSyms) {
// reset this every time, in case they remove the symbol
mChrRomOffset = -1;
foreach (PlSymbol sym in plSyms) {
if (sym.Label == CHR_ROM) {
int addr = sym.Value;
mChrRomOffset = mAddrTrans.AddressToOffset(0, addr);
break;
}
}
mAppRef.DebugLog(CHR_ROM + " @ +" + mChrRomOffset.ToString("x6"));
}
// IPlugin_SymbolList
public bool IsLabelSignificant(string beforeLabel, string afterLabel) {
return beforeLabel == CHR_ROM || afterLabel == CHR_ROM;
}
// 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<string, object> parms) {
switch (descr.Ident) {
case VIS_CHR_ROM:
return GenerateRomChart(parms);
case VIS_TILE_GRID:
return GenerateTileGrid(parms);
default:
mAppRef.ReportError("Unknown ident " + descr.Ident);
return null;
}
}
private IVisualization2d GenerateRomChart(ReadOnlyDictionary<string, object> parms) {
int paletteNum = Util.GetFromObjDict(parms, P_COLOR_PALETTE, 0);
bool showLabels = Util.GetFromObjDict(parms, P_SHOW_LABELS, true);
if (mChrRomOffset < 0) {
mAppRef.ReportError("CHR_ROM symbol not found");
return null;
}
if (mChrRomOffset + 8192 > mFileData.Length) {
mAppRef.ReportError("8KB CHR ROM runs off end of file");
return null;
}
const int spacing = 1;
const int tWidth = TileWidth + spacing;
const int tHeight = TileHeight + spacing;
const int gap = 4 + spacing * 2;
int labelSpacing = showLabels ? 9 : 0;
VisBitmap8 vb = new VisBitmap8(tWidth * 16 * 2 + gap + labelSpacing * 2 + 1,
tHeight * 16 + labelSpacing + 1);
SetPalette(vb, (Palette)paletteNum);
if (showLabels) {
for (int i = 0; i < 16; i++) {
char ch = (i < 10) ? (char)('0' + i) : (char)('A' + i - 10);
VisBitmap8.DrawChar(vb, ch, (i + 1) * tWidth + 1, 1,
(byte)Color.Black, (byte)Color.White);
VisBitmap8.DrawChar(vb, ch, (i + 16 + 1) * tWidth + gap + 1, 1,
(byte)Color.Black, (byte)Color.White);
VisBitmap8.DrawChar(vb, ch, 1, (i + 1) * tHeight + 1,
(byte)Color.Black, (byte)Color.White);
VisBitmap8.DrawChar(vb, ch, (1 + 16 + 16) * tWidth + gap + 1,
(i + 1) * tHeight + 1, (byte)Color.Black, (byte)Color.White);
}
}
for (int idx = 0; idx < 512; idx++) {
int xshift = idx < 256 ? 0 : tWidth * 16 + gap;
int xc = (idx & 0x0f) * tWidth + xshift + labelSpacing + 1;
int yc = ((idx & 0xff) >> 4) * tHeight + labelSpacing + 1;
RenderTile(idx, vb, xc, yc, false);
}
return vb;
}
private IVisualization2d GenerateTileGrid(ReadOnlyDictionary<string, object> parms) {
int offset = Util.GetFromObjDict(parms, P_OFFSET, 0);
int paletteNum = Util.GetFromObjDict(parms, P_COLOR_PALETTE, 0);
int width = Util.GetFromObjDict(parms, P_WIDTH, 1);
int height = Util.GetFromObjDict(parms, P_HEIGHT, 1);
bool flipRight = Util.GetFromObjDict(parms, P_FLIP_RIGHT, false);
bool useRightTable = Util.GetFromObjDict(parms, P_RIGHT_TABLE, false);
if (mChrRomOffset < 0) {
mAppRef.ReportError("CHR_ROM symbol not found");
return null;
}
if (offset < 0 || offset >= mFileData.Length) {
mAppRef.ReportError("Invalid parameter");
return null;
}
if (offset + width * height > mFileData.Length) {
mAppRef.ReportError("Data runs off end of file");
return null;
}
VisBitmap8 vb = new VisBitmap8(TileWidth * width, TileHeight * height);
SetPalette(vb, (Palette)paletteNum);
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
int tileNum = mFileData[offset + row * width + col] +
(useRightTable ? 256 : 0);
RenderTile(tileNum, vb, TileWidth * col, TileHeight * row,
flipRight && col >= (width + 1) / 2);
}
}
return vb;
}
/// <summary>
/// Renders a tile from the PPU pattern table.
/// </summary>
/// <param name="tileNum">Tile number (0-511).</param>
/// <param name="vb">Bitmap to render to.</param>
/// <param name="xc">X coordinate for upper-left coordinate.</param>
/// <param name="yc">Y coordinate for upper-left coordinate.</param>
/// <param name="flipHoriz">Flip pixels horizontally</param>
private void RenderTile(int tileNum, VisBitmap8 vb, int xc, int yc, bool flipHoriz) {
int tileOff = mChrRomOffset + tileNum * BytesPerTile;
for (int row = 0; row < 8; row++) {
byte part0 = mFileData[tileOff];
byte part1 = mFileData[tileOff + 8];
for (int bit = 7; bit >= 0; bit--) {
int val = ((part0 >> bit) & 0x01) | (((part1 >> bit) & 0x01) << 1);
vb.SetPixelIndex(xc + (flipHoriz ? bit : 7 - bit), yc,
(byte)((byte)Color.Color0 + val));
}
tileOff++;
yc++;
}
}
private enum Color : byte {
Transparent = 0,
Black = 1,
White = 2,
Color0 = 3,
Color1 = 4,
Color2 = 5,
Color3 = 6
}
private enum Palette : int {
Greyscale = 0,
Pinkish = 1,
Greenish = 2,
}
private void SetPalette(VisBitmap8 vb, Palette pal) {
vb.AddColor(0, 0, 0, 0); // 0=transparent
vb.AddColor(0xff, 0x01, 0x01, 0x01); // 1=near black (so VB doesn't uniquify)
vb.AddColor(0xff, 0xfe, 0xfe, 0xfe); // 2=near white
switch (pal) {
case Palette.Greyscale:
default:
vb.AddColor(0xff, 0x00, 0x00, 0x00); // black
vb.AddColor(0xff, 0x80, 0x80, 0x80); // dark grey
vb.AddColor(0xff, 0xb0, 0xb0, 0xb0); // medium grey
vb.AddColor(0xff, 0xe0, 0xe0, 0xe0); // light grey
break;
case Palette.Pinkish:
vb.AddColor(0xff, 0x49, 0x99, 0xfe); // sky blue
vb.AddColor(0xff, 0xff, 0xbd, 0xaf); // pinkish
vb.AddColor(0xff, 0xcd, 0x50, 0x00); // dark orange
vb.AddColor(0xff, 0x00, 0x00, 0x00); // black
break;
case Palette.Greenish:
vb.AddColor(0xff, 0x49, 0x99, 0xfe); // sky blue
vb.AddColor(0xff, 0x00, 0xa4, 0x00); // medium green
vb.AddColor(0xff, 0xfc, 0xfc, 0xfc); // near white
vb.AddColor(0xff, 0xff, 0x99, 0x2b); // orange
break;
}
}
}
}