Engine

From DaphneWiki

(Difference between revisions)
Jump to: navigation, search
m (Game Drivers)
m (Vblank)
Line 52: Line 52:
Vblank is currently driven by the generic game class, and a class called VblankTimer.  It may make more sense to drive vblank from the laserdisc player since that is how it is generated on real hardware; however, we are using cpu cycles to compute when to fire off vblank, which is fairly handy, so for now we'll probably leave it in the game class.
Vblank is currently driven by the generic game class, and a class called VblankTimer.  It may make more sense to drive vblank from the laserdisc player since that is how it is generated on real hardware; however, we are using cpu cycles to compute when to fire off vblank, which is fairly handy, so for now we'll probably leave it in the game class.
 +
 +
In order for video to get rendered, IVideoObjectUtil::OnNewVblank() must be called.  The MainLoop class will then cause the screen to be redrawn/rendered as part of the work that it does when its Go() method is called.
===Game Drivers===
===Game Drivers===

Revision as of 22:27, 12 August 2019

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. All game and ldp classes automatically register to receive these events (so if you write your own you will get these events without having to do any work).

Vblank

All game and laserdisc drivers will receive highly accurate vblank start and stop events. The vblank events are tied to the first emulated CPU's current elapsed cycle count and will have an accuracy of +/- 1 emulated CPU cycles. If the vblank event would come in the middle of a CPU instruction, it will be received after the CPU instruction has finished.

Vblank event calculation is self-correcting meaning that on average, the space between vsync pulses will constantly be approaching (1001 * 1000000) / (1000 * 60) microseconds (~16.6 milliseconds).

If the game driver has no emulated CPU's, a dummy CPU will be added to handle vblank. (TODO, this is not currently implemented)

Vblank is currently driven by the generic game class, and a class called VblankTimer. It may make more sense to drive vblank from the laserdisc player since that is how it is generated on real hardware; however, we are using cpu cycles to compute when to fire off vblank, which is fairly handy, so for now we'll probably leave it in the game class.

In order for video to get rendered, IVideoObjectUtil::OnNewVblank() must be called. The MainLoop class will then cause the screen to be redrawn/rendered as part of the work that it does when its Go() method is called.

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)

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 or shared_array

We never want to call delete/free. boost's (or c++ tr1) shared_ptr should be used with all dynamic objects so that they do not need to be explicitly freed.

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

Game drivers should no longer inherit from a generic game class, and ldp drivers should no longer inherit from a generic ldp class. Common game and ldp functions should be put inside common classes that are abstracted away by interfaces, and game/ldp-specific implementations should also be hidden behind interfaces. 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.

No linked lists

The linked lists used by the old cpu and sound code should be destroyed and replaced with STL's list.

Prefer interfaces over function pointers

C++ interfaces should always be used instead of 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)

Instantiation

All constructor methods should be private. 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.

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".

Personal tools