Porting NESticle for Fun and Profit: Part 1
Written by Paco Pascal, on 28 May 2023.
Tags:
#nes
#emulation
What's going on? Where are the posts?
Originally, I was planning on writing these posts so readers can follow along with the process. I intended to make the problem solving visible instead of just dumping the end result. In my opinion, there's value in showing the actual thought process and methodology. I've helped many friends tackle programming problems in the past. In most cases, they have the potential to solve the problems without help; however, the main skill they were lacking that snagged them was dealing with large amounts of ambiguity, especially in large existing code-bases and systems.
Unfortunately, communicating the hills and vallies of problem solving is time consuming and frankly unenjoyable. RTFM. For the sake of closure, I'm just going to dump end results.
All of this happened last year (in 2022).
Getting Started
Obviously, the first thing to get done was compile each source file
into an object file. (Turn the widdle .cpp
's into .o
's.) This will
only imply the code is syntactically correct. Once the code can
compile, I can begin testing and playing with specific areas of the
code-base.
For projects like this, I prefer using the Acme editor.
Makefile Skeleton
This endeavor is an iterative process. Therefore, I need something simple that I can append each file as I progress. So, I make a simple Makefile to work with.
CPPFLAGS = -ggdb -m32 -O0 -g -Werror -std=c++17 SRC = OBJECTS = $(SRC:.cpp=.o) all: $(OBJECTS) %.o: %.cpp $(CXX) $(CPPFLAGS) -c $< clean: rm -f $(OBJECTS)
Getting the First Files Compiled
I started with dasasm.cpp
. There's very little code in it and it's
straight forward C++ code with nothing unusual. From there, I decided
to work on file.cpp
. There's more going on in it but it's still
straight forward.
Even though the code in the top directory is intended to be platform
independent, it's not entirely. The most obvious thing that has to be
changed is the include header files. DOS provides a collection of
non-standardized functions and headers such as, conio.h
, io.h
, and
direct.h
. Also, header paths that are in a subdirectory such as
sys/types.h
are written as sys\types.h
which isn't valid on Linux.
This is an easy fix. As I'm progressing through each file, issues such
as old DOS macros, non-standard C++ flexible arrays, and implicit
casts from const char*
to char*
show up. Some of which were easily
forgiven in the 90s. Eventually, each file finds its spot in the
makefile.
SRC = portutils.cpp disasm.cpp file.cpp font.cpp \ mouse.cpp nes.cpp nesdlg.cpp nesvideo.cpp \ prof.cpp r2img.cpp rom.cpp slist.cpp \ snapshot.cpp stddlg.cpp config.cpp command.cpp \ mmc.cpp nessound.cpp cpu.cpp message.cpp \ input.cpp inputw.cpp main.cpp vol.cpp
Suddenly, I have each file compiling.
Figure 1: NESticle C++ files compiling.
Making Things Work
Just because things are compiling, doesn't mean they work. I decided
to get file IO working first; specifically, I wanted to be able to
read the gui.vol
file. This file contains various assets that can be
drawn. Being able to read and write a GUI volume file had almost no
dependencies. The code was pretty much independent from the rest of
the code base. That makes it an easy starting place. Additionally,
it'll give me real assets to use later on when I port the drawing
code.
So, I pulled GUI.VOL
from NESticle versions 040 and 042 into the
source directory. Then wrote a simple tool (I called volmain.cpp
)
that linked to file.o
and vol.o
which are the only dependencies
needed to read a GUI volume file. The tool would just read each asset
from the volume and write it out to it's own file.
This is where one of the first bugs rose up which would become a
repetitive one throughout the code. The volume files have a header for
each asset that is defined by the structure called struct header
.
// header for each entry in the volume file struct header { char key[4]; // must be "DSL" char type; // type of data unsigned int size; // size of data char name[9]; // name of data } __attribute__((packed));
However, it didn't look like this originally. The structure wasn't
packed using the __attribute__((packed))
attribute. NESticle assumes
all it's structures are packed because back when the code was written,
structs were packed. These days most compilers pad their structs by
default. So, whenever NESticle reads data into a structure, it assumes
the data matches it's memory layout. However, the GUI volumes were
created using old versions of NESticle that had packed structs by
default. It's not a bug in the code. It's just changes in time rusting it a bit.
The alternative to packing the structs is to convert the volume files to have padded struct data. But the idea right now is to maintain and port what was, to today. Changing the memory layout prematurely might cause complications down the line that I can't see at the moment.
./vol.h:17:} __attribute__((packed)); ./r2img.h:24:} __attribute__((packed)); ./r2img.h:48:} __attribute__((packed)); ./r2img.h:56:} __attribute__((packed)); ./r2img.h:66:} __attribute__((packed)); ./r2img.h:92:} __attribute__((packed)); ./r2img.h:108:} __attribute__((packed));
With packed structs, I'm now able to successfully use the file IO and volume segments of the code-base. And I have the original collection of NESticle assets ready to be put on the screen.
Figure 2: Exporting assets from a GUI volume. The error message is the early tool not handling the EOF correctly.
Conclusion
After accomplishing these first critical steps, porting the r2 drawing code was within sight. Half of the code is written in x86 assembly. And I intended on keeping that way.