Engine

From DaphneWiki

Jump to: navigation, search

Contents

DAPHNE Engine

This information applies mostly to the unreleased Daphne v2.0 code base. I am making notes as I develop it. This info is subject to change.

Program Entry

Program entry must be implemented on a platform-specific basis. It can be something like a main() function which loops and calls Daphne functions or whatever you need it to be. In short, these daphne-specific functions must be called regardless of the platform:

  • Daphne::CreateInstance (returns shared pointer to instance of Daphne object)
  • Init method inside of created Daphne object (parses command line and returns an instance of the IGenericLoop interface)
  • Method inside of created Daphne object to monitor the quit flag (naming subject to change so not included here)
  • IGenericLoop->Go() (call this over and over again until the getting the Quit Flag returns true; this value returns the number of milliseconds before calling Go() again; this value must be respected in order for Daphne to run at the proper speed)
  • Daphne shared_ptr should go out of scope (or be explicitly reset) before the platform shutdown occurs. This allows generic Daphne shutdown to occur first while platform functionality is still available.

Platform-specific

Platform-specific code inherits from the IPlatform interface and lives in the src/platform folder. It is responsible for providing the following functions:

  • Platform input event handling (keyboard, mouse, joystick, any other abstract means of input)
  • Sound (providing a mechanism to stream PCM audio to audio device)
  • Video (can be as simple as providing platform initialization to OpenGL code to as complex as providing a complete video object)
  • Timing (millisecond precision)

Video

All video functionality is inherits from the IVideoObject interface. The IPlatform interface returns an instance of IVideoObject. So you can extend pre-written IVideoObject classes or write your own from scratch. Very flexible.

TODO: The IVideoObject interface needs to be split up into smaller interfaces because it currently tries to do too many things.

Sound

The IPlatform interface has a SoundInit method, one argument of which is AudioStreamCallback. Your platform specific code must call this callback regularly (when audio buffer needs to be filled) to grab PCM audio generated by Daphne's sound code. Daphne's sound code currently is hard-coded to generate audio at 44.1khz, 16-bit stereo.

Input

The platform-specific code is responsible for returning an implementation of the IGenericInput interface. As of right now, it appears it is safe for all platforms to just return an instance of GenericInput.

The platform-specific code must implement some event handler to call the IGenericInput methods input_enable, input_disable to indicate that input has come in. The platform-specific code may optionally also support mouse motion by capturing mouse events and then calling IGenericInput->OnMouseMotion. The platform-specific code may optionally also support keyboard events (to handle Thayer's Quest and SINGE games) by calling IGenericInput->key_enable and IGenericInput->key_disable.

Input Events

The IGenericInput interface provides the option for any code in Daphne to register to receive input events. This means that, for example, a game class could receive events when the user takes a screenshot even though game classes normally have no knowledge of this. The reason for this design is so that the IGenericInput interface doesn't know what it is sending events to (to reduce coupling). The events that are available for notification are: game input (joystick, button), keyboard input, mouse movement, screenshot/pausing of Daphne, when enabling/disabling the debug console.

Timers

Daphne has an IGenericLoop interface which is essentially the outer loop that runs over and over again until the program exits. The loop generates an event every 1 millisecond called a "Think" event which basically means that anything that receives this event can check every 1 ms to see if it has any work to do. Any part of the Daphne code can register to receive think events by calling IGenericLoop->RegisterThinkObserver. If an IGame class wants to receive Think events, it needs to implement the IGameThink interface and indicate it has done so via the GetFeatures method.

Clocks and Events

A global TimeManager keeps track of clocks and timed events.

Within an IGame class, to create a clock or insert timed events, implement the IGameTimers interface and indicate such via the GetFeatures method. Created clocks will call OnClockEvent method to cause the clock to 'tick'. Timed events will call the OnTimeEvent method when an event occurs. Clocks should be created for constant predictable activities like crystals/oscillators on PCBs, while timed events should be for things like VBlank (which is different depending on whether it's an odd or even field), or LD-V1000 events like the status/command strobe.

Game Drivers

All game drivers must implement IGame interface which includes Init and GetFeatures method(s).

Game Manager

IGameManager is a common layer of abstraction separating IGame away from the rest of the code. IGame methods should generally be called by IGameManager and the rest of the code should call methods inside of IGameManager.

These methods include: Init, Start, and Reset.

IGameManager will provide a layer of abstraction so that methods like Start/Reset can always be called, even if IGame doesn't support them. This relieves each game of the burden of having to implement functionality that it doesn't need, and it relives callers the burden of checking to see whether certain functionality is supported.

Game General

General interfaces you may also want to implement include IGameCmdLine, IGameReset, and IGameStart.

Game Video

If using video overlay Implement IGameVideoOverlay interface which includes GetVideoOverlayFeatures, IsRepaintNeeded and Repaint method(s).

If using 8-bit video overlay with a color palette Implement IGameVideoOverlayPalette which includes PaletteCalculate method(s).

Game Input

If using switch input (buttons, digital joystick) Implement IInputSwitch which includes InputEnable and InputDisable methods(s).

If using trackball input (mouse) Implement IInputMouse which includes OnMouseMotion method(s).

If using analog joystick Implement IInputJoystickAnalog which includes OnJoystickMotion method(s).

If using keyboard Implement IInputKeyboard

OBSOLETE INFO FOLLOWS, HERE FOR REFERENCE

All game drivers must inherit from the 'game' base class.

CPUs should be set up in the game's init() method (see star rider driver for preferred method). Many of the game drivers currently set up CPUs and sound chips inside the game's constructor but this method is deprecated and discouraged. ROM paths may still be set up inside the constructor.

Most methods have defaults in the 'game' base class and do not need to be explicitly defined, but some of the methods you probably will need to define if you want the game driver to do anything useful:

  • init
  • cpu_mem_read
  • cpu_mem_write
  • input_enable
  • input_disable
  • OnVblankChanged

if using video overlay

  • palette_calculate
  • video_repaint

Video Overlay

If using video overlay, you must set the following variables:

  • m_game_uses_video_overlay = true;
  • m_video_overlay_width = [your game's video overlay width]
  • m_video_overlay_height = [your game's video overlay height]
  • m_palette_color_count = [your game's colors per palette, not to exceed 256]

video_blit() is currently called automatically at the end of every vblank. It used to be mandatory for the game driver to call it manually and most of them still do, which results in a harmless NO-OP aside from the redundant function call.

Laserdisc Player Drivers

All laserdisc drivers must implement ILdp interface which includes Init, Think, and GetFeatures method(s).

Laserdisc Player General

General interfaces you may also want to implement include [TODO].

Laserdisc Player Manager

ILdpManager is a common layer of abstraction separating ILdp away from the rest of the code, including the game drivers. ILdp methods should generally be called by ILdpManager and the rest of the code should call methods inside of ILdpManager.

These methods include: Init, BeginSearch, Play, Pause, Stop, Skip, Step, GetFieldVBI.

ILdpManager will throw an exception if ILdp doesn't support the required functionality (for example, if Skip is called and the ILdp object doesn't support skippng). This simplifies laserdisc player emulation so that each emulator is freed of the burden of having to check to see whether a desired function is supported (ie skipping). If this becomes problematic (ie player types appearing to be supported, only to fail with exceptions once they have started), a method could be added to ILdpManager to enumerate features.

Coding Standards

The current Daphne code was started in 1999 and has evolved as the developers have evolved. Unfortunately, a lot of legacy code exists, where the coding practices and style is no longer ideal. Here are some suggestions for how to improve the code base:

Interfaces

All concrete classes should derive from interfaces. Concrete classes should only be created when the program first runs or by abstract factories. (ie the Dragon's Lair driver should not create a concrete instance of the ldv1000 class, for example)

Exception: Simple classes that only contain data or simple getter/setter methods do not need to inherit from an interface. These classes are used to avoid methods with large number of parameters.

Interface segregation

The 'I' from SOLID. Classes shouldn't be forced to implement interfaces that they don't use or depend on methods that they don't use. This means that, for example, if only some games use emulated sound, then the base game interface should not have any methods relating to sound.

See the GetFeatures section.

The GetFeatures pattern

In order to be as cross-platform as possible, we do not require the compiler to support RTTI (run-time type information) which means that interfaces cannot necessarily be dynamically cast to other interfaces. To work around this, base interfaces (such as the IGame interface) must implement a GetFeatures method which allows a client to discover what other interfaces (ie other features) are available. The Features response classes are constructed using the Builder pattern to make extension relatively painless.

No globals

Global variables (and methods) still exist in the code base. These must be eradicated. This includes the cpu and sound code, as well as the g_ldp and g_game pointers. Global methods should be replaced with static class methods at the very least. The daphne globals should be put inside of a proper daphne class so that stuff like shutting down gets automatically handled.

No more than a few arguments per method

If a method is going to take in more than 2-3 arguments, pass in a class instead, using the Builder pattern. https://en.wikipedia.org/wiki/Builder_pattern

Use dependency injection model (aka Inversion of Control)

Class constructors (via instantiator methods, see Instantiation section) should take in interfaces, similar to how Unity Application Block works in C# or Spring works in Java. This can be faked by the beginning of the program entry (ie "main" or shortly thereafter) without using a specific dependency injection for C++ (if one even exists!). If more than a few interfaces are required, a class should be passed in instead, which uses the Builder pattern for instantiation. (see https://en.wikipedia.org/wiki/Builder_pattern )

All dynamic objects wrapped in shared_ptr

Never call new/delete on classes. std::shared_ptr should be used with all dynamic objects so that they do not need to be explicitly freed.

All constructor methods should be private for classes that are to be instantiated dynamically. A public static method is used to instantiate which will return a shared_ptr. GetInstance() will return a 'null' shared pointer on error (ie result.get() is NULL), CreateInstance() will throw a std::exception on instantiation error. CreateInstance() is preferred unless performance is a concern. All destructors must be private. The 'MpoDeleter' pattern using "void DeleteInstance() { delete this; }" should be part of all concrete classes that are to be instantiated dynamically.

GOAL: Make memory leaks impossible.

Throwing exceptions is preferred

A function should not return true/false on success or failure but should return on success or throw an exception on failure. Thrown exceptions must always derive from std::exception. The 'exception' to this rule is if the code needs to be optimized for performance reasons (ie cpu emulation) then 'assert' may be used instead to check for errors.

Avoid inheritance

Prefer composition over inheritance: https://en.wikipedia.org/wiki/Composition_over_inheritance . Dynamic utility methods do not belong in base classes. The only exception to this rule is if inheritance is used to reduce boilerplate code.

Prefer interfaces/lambdas over function pointers

C++ interfaces/lambdas should always be used instead of C function pointers. The only exception is if the code is written in C (ie the LDP interpreters, which are shared by Dexter) in which case function pointers may still be used.

Init/Shutdown functions

Shutdown methods should never need to be explicitly called (and should be made private if it makes sense). Shutdown functions should be automatically called by destructors. Calling init methods twice in a row should always be supported (ie if an init method is called twice, it should silently do any shutdown needed behind the scenes).

Avoid coupling

Game common code should not be coupled to LD-V1000 events (as it is today). Game common code also should probably not be coupled to cpu emulation (as it is today), because it is valid for a game driver to not have any cpu emulation (ie seektest).

Classes preferred over 'out' parameters

A method that needs to return more than one result should return a class rather than returning a single type and having one or more 'out' parameters as arguments.

"Out" parameters should use pointers, not be passed in by reference

void ChangeThis(int *pDstParm);

instead of

void ChangeThis(int &dstParm);

NOTE : 'out' parameters are discouraged unless using them helps performance.

Copy constructors discouraged

If a function needs a read-only list,

void DoStuff(const list<int> &lstWhatever)

instead of

void DoStuff(list<int> lstWhatever) or void DoStuff(list<int> *pLstWhatever)

If a function is going to modify a list,

void ChangeThisList(list<int> &lstWhatever)

instead of

list<int> ChangeThisList(list<int> lstWhatever)

Naming convention

For C++, "Camel Case" is preferred for class names and function names. (For example: "MyClass::MyFunction".) Acronyms should be treated like words. (For example, GetPlayerId, not GetPlayerID.) Local variables or class variables should begin with a lowercase letter and be camel case thereafter, and should have some indication of what the variable type is. (For example, list<string> might be called lstResult). Interfaces should begin with a capital 'I' (for example IMyStuff).

For C, all letters should be lower-case separated by '_' characters. For example, "my_result".

Build Environment

We are standardizing on CMake for build Daphne.

To build/install in a custom folder (so that you can easily experiment and clean-up), add the following arguments to CMake:

mpojson example:

-DCMAKE_SYSTEM_PREFIX_PATH=c:/temp/cmake_install -DCMAKE_INSTALL_PREFIX=c:/temp/cmake_install/mpojson

Ogg example:

-DCMAKE_SYSTEM_PREFIX_PATH=c:/temp/cmake_install -DCMAKE_INSTALL_PREFIX=c:/temp/cmake_install/Ogg

GLEW example:

-DCMAKE_SYSTEM_PREFIX_PATH=c:/temp/cmake_install -DCMAKE_INSTALL_PREFIX=c:/temp/cmake_install/GLEW


Command line to add to vorbis (for example):

-DOGG_ROOT:STRING=c:/temp/cmake_install/Ogg

Note that the vorbis cmake environment expects the 'O' to be capitalized.

Command line to add to import zlib (for example):

-DZLIB_ROOT:STRING=c:/temp/cmake_install/zlib

Command line to add to libjpeg-turbo (for example):

-DCMAKE_SYSTEM_PREFIX_PATH=c:/temp/cmake_install -DCMAKE_INSTALL_PREFIX=c:/temp/cmake_install/TurboJPEG -DASM_NASM:STRING="C:\Program Files\nasm\nasm.exe" -DENABLE_SHARED=OFF
Add -DWITH_CRT_DLL=ON if you are building with MSVC and the rest of your projects use the C runtime (common).

Command line to add to properly import turbo libjpeg (Daphne's cmake script will look for this variable):

-DTurboJPEG_ROOT:STRING=c:/temp/cmake_install/TurboJPEG

Example CMake command line to build Daphne (windows):

-DCMAKE_SYSTEM_PREFIX_PATH=c:/temp/cmake_install -DCMAKE_INSTALL_PREFIX=c:/temp/cmake_install/daphne -DTurboJPEG_ROOT:STRING=c:/temp/cmake_install/TurboJPEG -DZLIB_ROOT=c:/temp/cmake_install/zlib

Another example CMake command line (raspberry pi, linux):

cmake -DCMAKE_FIND_ROOT_PATH=~/dev/daphne/install -DCMAKE_INSTALL_PREFIX=~/dev/daphne/install/daphne -DTurboJPEG_ROOT:STRING=~/dev/daphne/install/TurboJPEG -DZLIB_ROOT=~/dev/daphne/install/zlib -DCMAKE_BUILD_TYPE=Release -DIS_RPI=true ..
Personal tools