Monday, January 24, 2011

XNA and bsnes

Download the code for this example here.

I miss my Super Nintendo.  My favorite console of all time also plays host to some of the best games ever made (at least, in the opinion of a guy who spent his adolescence playing video games in the mid-1990s).  A fuse near the power input of my SNES burned out years ago, and it hasn't worked since.  I should buy a replacement from eBay, but why, when there are so many excellent emulators available?

Excellent emulators, such as bsnes, developed by byuu.  As a developer, I greatly respect what byuu has accomplished: beautiful code which models the architecture of an SNES in a simple, logical fashion and doesn't circumvent quality in favor of slapdash development.  As a gamer, I appreciate the picture-perfect emulation that bsnes brings to any computer on which I care to play a "quick" game of Seiken Densetsu 3.

byuu has created a C-style interface to the bsnes library, called libsnes.  People have developed cool frontends to the bsnes backend in languages such as Python and C# using SlimDX.  I thought it would be fun to try my hand at an XNA-powered bsnes frontend.

Instead of using explicit P/Invoke to interop calls to libsnes, I opted to use C++/CLI.  It's a personal opinion, but C++/CLI helps me better understand where a problem lies when things go wrong with the tricky world of C++/C# interop.  That being said, Microsoft's support for C++/CLI in Visual Studio 2010 is pretty dismal.

I accomplished importation from the libsnes DLL by attaching the DLLImport attribute to each libsnes function call.  Since the libsnes assembly is compiled using MinGW GCC, it was necessary to specify the cdecl calling convention, seen below.

[DllImport("snes.dll", CallingConvention=CallingConvention::Cdecl)]
void snes_run(void);

The libsnes library uses function pointers to callbacks for various frame completion events.  However, the static ref class I use to expose libsnes to my XNA game is all managed C++, so its functions cannot be passed directly to native code.  To pass a function from the ref class to native code, it was necessary to define delegate types which included the cdecl calling convention on the delegates via the UnmanagedFunctionPointer attribute:

delegate static void SnesVideoRefresh(const unsigned short* data, unsigned int width, unsigned int height);

I used GetFunctionPointerForDelegate to pass managed functions which matched the delegate signatures to native libsnes:

template<typename functionPointerType, typename delegateType> static functionPointerType NativeFunctionPointer(delegateType managedFunction)
 auto pointer = System::Runtime::InteropServices::Marshal::GetFunctionPointerForDelegate(managedFunction);
 return static_cast<functionPointerType>(pointer.ToPointer());

_videoRefreshDelegate = gcnew SnesVideoRefresh(&LibSnes::OnVideoRefresh);

Additionally, I adapted the interface to be more C#-like, rather than a C-style translation.  This included the use of managed enums, properties, and events, for which I wrote custom event handler arguments that could send bsnes data to any event listeners in managed code.  The result was a concise interface for use with an XNA game that doesn't require unsafe codeblocks to accommodate C++ syntax.

public static class LibSnes
    public static Region CartridgeRegion { get; }
    public static uint LibraryRevisionMajor { get; }
    public static uint LibraryRevisionMinor { get; }
    public static uint SerializeSize { get; }

    public static event EventHandler<AudioRefreshEventArgs> AudioRefresh;
    public static event EventHandler InputPoll;
    public static event EventHandler<InputStateEventArgs> InputState;
    public static event EventHandler<VideoRefreshEventArgs> VideoRefresh;

    public static void CheatReset();
    public static void CheatSet(uint index, bool enabled, string code);
    public static byte[] GetMemoryData(Memory id);
    public static uint GetMemorySize(Memory id);
    public static void Init();
    public static void LoadCartridgeNormal(string xml_utf8, byte[] data, uint dataLength);
    public static void Power();
    public static void Reset();
    public static void Run();
    public static bool Serialize(byte[] data, uint size);
    public static void SetCartridgeBasename(string basename);
    public static void SetControllerPortDevice(Port port, Device device);
    public static void Term();
    public static void UnloadCartridge();
    public static bool Unserialize(byte[] data, uint size);

To translate bsnes audio and video data for use in an XNA game, I took a look at the existing code for other frontends.

Video was as simple as creating an array of Color data, copying it to a Texture2D, then drawing it with a SpriteBatch draw operation.  The XNA Creators Club has a sample for using effects with SpriteBatch drawing, which I adapted to draw the video using the HQ2X filter.

Audio data is sent in arrays of samples and played for each frame in bsnes.  This made it seem that it was going to be difficult to handle audio in XNA before I discovered a cool class, new to XNA 4.0: DynamicSoundEffectInstance.  Creating an instance of this class and calling Play allows you to submit buffers as you receive them, perfect for an application such as this.

Audio, Video, and Input were all handled using XNA GameComponent and DrawableGameComponent classes.  Each game Update calls Run on LibSnes, which advances the game one frame.  The game components receive raised events from LibSnes, and the video component draws updated bsnes video texture data using a SpriteBatch.

The XNA game accepts command line arguments for cartridge ROMs and runs at 60 FPS on my laptop.  I also tried out a very simple WPF UI based on my earlier post.  I implemented a view which has a load button, a pause button, and a combo box to switch filters.  The frame rate appears to be fill bound, but I haven't looked into the issue extensively.

I started with a view model for the game, which has an instance of the game class, a pause property, a filter property, and a property for the load command.  The view model implements INotifyPropertyChanged, used to communicate changes in the game to controls bound to the view model.  The load command launches a browse dialog and creates a new bsnes game instance.

I made some changes to my GamePanel this time.  The control was given Game, Pause, and Filter dependency properties, which have PropertyChangedCallback functions specified in their UIPropertyMetadata.  Rather than handle the game creation in the control's OnLoaded, the game is created whenever the DP changes.  Changes to the pause and filter state are communicated to the bound game instance.  I believe these changes make the code easier to understand.

No comments:

Post a Comment