1
0
mirror of https://github.com/fadden/6502bench.git synced 2024-06-11 17:29:29 +00:00

Add Atari 2600 sprite/playfield visualizer

First swing at a visualizer for Atari 2600 sprites and playfields.
Won't necessarily present an accurate view of what is displayed on
screen, but should provide a reasonable shape for data stored in
the obvious way.

The Adventure playfields looked squashed, so I added a simple row
duplication value.

Also, minor improvements to visualizers generally:
- Throw an exception, rather than an Assert, in VisBitmap8 when the
  arguments are bad.
- Show the exception in the Visualization Edit dialog.
- If generation fails and we don't have an error message, show a
  generic "stuff be broke" string.
- Set focus on OK button in Visualization Set Edit after editing,
  so you can hit Enter twice after renaming a tag.
This commit is contained in:
Andy McFadden 2019-12-06 17:19:27 -08:00
parent 1cdb31de32
commit 5635a1e33a
12 changed files with 438 additions and 6 deletions

View File

@ -43,8 +43,9 @@ namespace PluginCommon {
/// <param name="width">Bitmap width, in pixels.</param>
/// <param name="height">Bitmap height, in pixels.</param>
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;

View File

@ -175,6 +175,6 @@ limitations under the License.
<system:String x:Key="str_TitleNewProject">[new project]</system:String>
<system:String x:Key="str_TitleReadOnly">*READ-ONLY*</system:String>
<system:String x:Key="str_Unset">[unset]</system:String>
<system:String x:Key="str_VisSetMultipleFmt">{0}: {1} (+{2} more)</system:String>
<system:String x:Key="str_VisSetSingleFmt">{0}: {1}</system:String>
<system:String x:Key="str_VisSetMultipleFmt">{1} (+{2} more)</system:String>
<system:String x:Key="str_VisSetSingleFmt">{1}</system:String>
</ResourceDictionary>

View File

@ -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<string, object> 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<string, object> 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<string, object> 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
}
}
}

View File

@ -108,7 +108,7 @@ Some less-common parameters include:</p>
visualizer will default to no interleave (stride == 1).</li>
</ul>
<h3>Apple II - VisHiRes</h3>
<h3>Apple II - Apple/VisHiRes</h3>
<p>There is no standard format for small hi-res bitmaps, but certain
arrangements are common. The script defines three generators:</p>
@ -139,6 +139,23 @@ but has no effect on black or white.</p>
<p>The converter generates one output pixel for every source pixel, so
half-pixel shifts are not rendered.</p>
<h3>Atari 2600 - Atari/VisAtari2600</h3>
<p>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.</p>
<ul>
<li><b>Sprite</b> - basic 1xN sprite, converted to an image 8 pixels
wide.</li>
<li><b>Playfield</b> - 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.</li>
</ul>
</div>
<div id="footer">

View File

@ -137,6 +137,7 @@
"RT:Atari/2600.sym65"
],
"ExtensionScripts" : [
"RT:Atari/VisAtari2600.cs"
],
"Parameters" : {
"load-address":"0xf000"

View File

@ -0,0 +1,103 @@
; Copyright 2019 faddenSoft. All Rights Reserved.
; See the LICENSE.txt file for distribution terms (Apache 2.0).
;
; Assembler: Merlin 32
org $f000
bit sprite1
bit sprite2
bit sprite3
nop
bit playfield1
bit playfield2
bit playfield3
rts
; Atari Adventure: GfxKey (1x3)
sprite1
dfb $07
dfb $fd
dfb $a7
dfb $00
; Atari Adventure: GfxDrag0 (1x20)
sprite2
dfb $06
dfb $0f
dfb $f3
dfb $fe
dfb $0e
dfb $04
dfb $04
dfb $1e
dfb $3f
dfb $7f
dfb $e3
dfb $c3
dfb $c3
dfb $c7
dfb $ff
dfb $3c
dfb $08
dfb $8f
dfb $e1
dfb $3f
dfb $00
; Atari Adventure: GfxDrag1 (1x22)
sprite3
dfb $80
dfb $40
dfb $26
dfb $1f
dfb $0b
dfb $0e
dfb $1e
dfb $24
dfb $44
dfb $8e
dfb $1e
dfb $3f
dfb $7f
dfb $7f
dfb $7f
dfb $7f
dfb $3e
dfb $1c
dfb $08
dfb $f8
dfb $80
dfb $e0
dfb $00
; Atari Adventure: BlackMaze3 -- x7 not reflected
playfield1
dfb $f0,$f0,$ff
dfb $30,$00,$00
dfb $30,$3f,$ff
dfb $00,$30,$00
dfb $f0,$f0,$ff
dfb $30,$00,$03
dfb $f0,$f0,$ff
; Atari Adventure: RedMazeBottom -- x7 reflected
playfield2
dfb $f0,$33,$cf
dfb $f0,$30,$00
dfb $f0,$33,$ff
dfb $00,$33,$00
dfb $f0,$ff,$00
dfb $00,$00,$00
dfb $f0,$ff,$0f
; Atari Adventure: CastleDef -- x7 reflected
playfield3
dfb $f0,$fe,$15
dfb $30,$03,$1f
dfb $30,$03,$ff
dfb $30,$00,$ff
dfb $30,$00,$3f
dfb $30,$00,$00
dfb $f0,$ff,$0f

View File

@ -0,0 +1,71 @@
### 6502bench SourceGen dis65 v1.0 ###
{
"_ContentVersion":3,"FileDataLength":131,"FileDataCrc32":-1864286961,"ProjectProps":{
"CpuName":"6502","IncludeUndocumentedInstr":false,"TwoByteBrk":false,"EntryFlags":32702671,"AutoLabelStyle":"Simple","AnalysisParams":{
"AnalyzeUncategorizedData":true,"DefaultTextScanMode":"LowHighAscii","MinCharsForString":4,"SeekNearbyTargets":true,"SmartPlpHandling":true},
"PlatformSymbolFileIdentifiers":["RT:Atari/2600.sym65"],"ExtensionScriptFileIdentifiers":["RT:Atari/VisAtari2600.cs"],"ProjectSyms":{
}},
"AddressMap":[{
"Offset":0,"Addr":61440}],"TypeHints":[{
"Low":0,"High":0,"Hint":"Code"}],"StatusFlagOverrides":{
},
"Comments":{
},
"LongComments":{
"-2147483647":{
"Text":"A few sprites and playfield graphics from Atari Adventure.\r\n","BoxMode":false,"MaxWidth":80,"BackgroundColor":0}},
"Notes":{
},
"UserLabels":{
"110":{
"Label":"CastleDef","Value":61550,"Source":"User","Type":"GlobalAddr","LabelAnno":"None"},
"89":{
"Label":"RedMazeBottom","Value":61529,"Source":"User","Type":"GlobalAddr","LabelAnno":"None"},
"68":{
"Label":"BlackMaze3","Value":61508,"Source":"User","Type":"GlobalAddr","LabelAnno":"None"},
"45":{
"Label":"GfxDrag1","Value":61485,"Source":"User","Type":"GlobalAddr","LabelAnno":"None"},
"24":{
"Label":"GfxDrag0","Value":61464,"Source":"User","Type":"GlobalAddr","LabelAnno":"None"},
"20":{
"Label":"GfxKey","Value":61460,"Source":"User","Type":"GlobalAddr","LabelAnno":"None"}},
"OperandFormats":{
"20":{
"Length":4,"Format":"Dense","SubFormat":"None","SymbolRef":null},
"24":{
"Length":21,"Format":"Dense","SubFormat":"None","SymbolRef":null},
"45":{
"Length":23,"Format":"Dense","SubFormat":"None","SymbolRef":null},
"68":{
"Length":21,"Format":"Dense","SubFormat":"None","SymbolRef":null},
"89":{
"Length":21,"Format":"Dense","SubFormat":"None","SymbolRef":null},
"110":{
"Length":21,"Format":"Dense","SubFormat":"None","SymbolRef":null}},
"LvTables":{
},
"VisualizationSets":{
"20":{
"Items":[{
"Tag":"Key sprite","VisGenIdent":"atari2600-sprite","VisGenParams":{
"offset":20,"height":3}}]},
"24":{
"Items":[{
"Tag":"Dragon 0","VisGenIdent":"atari2600-sprite","VisGenParams":{
"offset":24,"height":20}}]},
"45":{
"Items":[{
"Tag":"Dragon 1","VisGenIdent":"atari2600-sprite","VisGenParams":{
"offset":45,"height":22}}]},
"68":{
"Items":[{
"Tag":"Black Maze 3","VisGenIdent":"atari2600-playfield","VisGenParams":{
"offset":68,"height":7,"rowDup":3,"reflected":false}}]},
"89":{
"Items":[{
"Tag":"Red Maze (bottom)","VisGenIdent":"atari2600-playfield","VisGenParams":{
"offset":89,"height":7,"rowDup":3,"reflected":true}}]},
"110":{
"Items":[{
"Tag":"Castle Def","VisGenIdent":"atari2600-playfield","VisGenParams":{
"offset":110,"height":7,"rowDup":3,"reflected":true}}]}}}

View File

@ -29,6 +29,8 @@ limitations under the License.
Closed="Window_Closed">
<Window.Resources>
<system:String x:Key="str_VisGenFailed">Visualization generation failed</system:String>
<!-- big thanks: http://drwpf.com/blog/2008/01/03/itemscontrol-d-is-for-datatemplate/ -->
<DataTemplate x:Key="BoolTemplate">
<Grid>

View File

@ -433,12 +433,17 @@ namespace SourceGen.WpfGui {
} catch (Exception ex) {
Debug.WriteLine("Vis generation failed: " + ex);
vis2d = null;
if (string.IsNullOrEmpty(LastPluginMessage)) {
LastPluginMessage = ex.Message;
}
}
if (vis2d == null) {
previewImage.Source = sBadParamsImage;
if (!string.IsNullOrEmpty(LastPluginMessage)) {
// Report the last message we got as an error.
PluginErrMessage = LastPluginMessage;
} else {
PluginErrMessage = (string)FindResource("str_VisGenFailed");
}
IsValid = false;
} else {

View File

@ -103,7 +103,7 @@ limitations under the License.
<DockPanel Grid.Column="0" Grid.Row="2" Grid.ColumnSpan="2" Margin="0,8,0,0" LastChildFill="False">
<Button DockPanel.Dock="Right" Content="Cancel" Width="70" Margin="8,0,0,0" IsCancel="True"/>
<Button DockPanel.Dock="Right" Grid.Column="1" Content="OK" Width="70"
<Button DockPanel.Dock="Right" Name="okButton" Grid.Column="1" Content="OK" Width="70"
IsDefault="True" Click="OkButton_Click"/>
</DockPanel>
</Grid>

View File

@ -195,6 +195,8 @@ namespace SourceGen.WpfGui {
VisualizationList.Remove(item);
VisualizationList.Insert(index, dlg.NewVis);
visualizationGrid.SelectedIndex = index;
okButton.Focus();
}
private void RemoveButton_Click(object sender, RoutedEventArgs e) {