The Allegro Wiki is migrating to github at https://github.com/liballeg/allegro_wiki/wiki

Difference between revisions of "Gemini's DirectInput Wrapper"

From Allegro Wiki
Jump to: navigation, search
m (Reverted edits by Ducro seb (Talk) to last revision by Kris Asick)
 
(No difference)

Latest revision as of 00:19, February 9, 2012

Gemini's DirectInput Wrapper (a.k.a. GDInput) is an alternative input wrapper for the Windows side of Allegro, or with some tweaking can also be used without Allegro. It was designed by Kris Asick so he could learn the intricacies of I/O programming using DirectX, to learn how Allegro uses DirectInput and to get around various bugs in the input system in Allegro. It is less advanced than the Allegro input system and is not compatible with any libraries which utilize Allegro's input handler.


Features

Basic Improvements

All input in GDInput is triple buffered. The first buffer is where DirectInput sends its raw data to every cycle, which is then merged in various ways with the second buffer, thus allowing for data to be processed before the programmer ever sees it. The third buffer is the polling simulator, so that data is only updated on demand, even though internally it's being processed constantly. The programmer only ever reads the data in the third buffer. Normally, the polling commands are called once every time an application loop repeats.

All I/O, including the keyboard, mouse and joysticks, copy keystroke/button presses with a binary-OR operation from the first to second buffer. This way, if a button is pressed and released extremely fast, GDInput is still able to catch it.

Keyboard Features

  • The keyboard has a full list of 256 pre-mapped key names, each one small enough to fit into the tight space required in some control configuration screens.
  • A typing system is in place which utilizes the Windows character conversion and keyboard mapping process so that character shifts are correctly interpreted. Almost every key on the keyboard, even if it doesn't convert to an ASCII value, is stuffed into a 64-character keyboard buffer, similarly to Allegro. Key repetition is handled exactly like without DirectInput, but with hardcoded timing values. (Could easily be converted to variables.)
  • Normally when using the keyboard in any DirectInput application, a return value of 0x80 indicates a key is held down. GDInput checks to see if a key state is 0x00 in the second buffer while 0x80 in the first. In this event, 0x08 is added to the key state, thus providing a method of checking for the first press of a key, without continuing to read presses as the key is held down.
  • Special Windows keys are overridden when the application is running full-screen, but key combinations like alt+tab are not.

Mouse Features

  • Under Allegro, the mouse roller is a cumulative axis. The more you roll the roller, the larger the value gets. Under GDInput, the roller functions like mouse mickeys, registering only the amount of roll since the last request for data from the mouse.

Joystick Features

  • GDInput can detect up to 16 joystick devices and can utilize all of them at once. For each joystick, up to 8 axises may be detected, 4 POV hats and 32 buttons. (This is standard for any application that is not utilizing the force feedback joystick data template provided by DirectInput.)
  • The joystick I/O can be paused without actually being removed.
  • Ranges for each axis in Allegro are either -128 to 128, or 0 to 256. Ranges in GDInput are -10,000 to 10,000, no matter what.
  • In rare cases when programming for DirectInput, joystick axis ranges may be read-only and thus any attempt to set the range will not function. For any axis which does not conform to the -10,000 to 10,000 range desired by GDInput, the values are manually expanded to that range.
  • Point-of-View (POV) hats can be read in three different ways.
    • By X and Y axis. The values range from -10,000 to 10,000 just like any regular axis, except diagonal values will be representative of a circle with a radius of 10,000. (Thus, pushing the hat up and right will usually be X 7071, Y -7071.)
    • By clockwise angle, in hundredths of a degree, with 0xFFFF (65,535) representing neutral and 0 representing north.
    • By button, with one button for each direction, stored in a separate array from the regular joystick buttons.

Missing Features

  • GDInput does not provide support for Unicode or MBCS.
  • There is no special key combination that will force-quit the application if running full-screen. (Ctrl+Alt+Del will still function when running windowed.)
  • Callback hooks cannot be installed.
  • Keyboard LED states cannot be manually changed.
  • The hardware mouse cursor is always disabled while the cursor is in the window client rectangle and cannot be switched on. (The mouse cursor must be drawn by the programmer.)
  • The mouse gets its movement from the screen position, even when running full-screen, so the sensitivity cannot be changed from within a GDInput using program.
  • If DirectInput cannot be initialized, GDInput will not function. (There is no straight API support for I/O devices.)
  • Windows treats the Pause/Break key differently in that, once read, it returns to an off state, even if it is still held down. GDInput does not compensate for this.


Testing Program

Explanation of Values

Screenshot of the GDInput test program.

The GDInput testing program shows a screen with a large quantity of values on it. The most prominent is the keyboard state grid, which shows the state of every key in the 256 key array. (However, practically all keyboards use less than half of these keys.) When a key is pressed the short name given to it is displayed and if the key translates to a character it is added to a string. Below the keyboard state grid is a list of all detected joysticks. Joysticks which are detected but could not be accessed for whatever reason will show up as "Unavailable". At the bottom is the mouse state variables, including the cursor coordinates, roller state, mickeys and button flags. The left side of the screen has several counters in the corner and below that are joystick state variables.

The counters in the corner represent:

Main Loop - This is actually the second thread created by the program, but is considered the main thread since this is where all the program logic happens. Each loop of the program logic increments the counter by 1. This thread loop ends with a double-buffered blit to the screen and Sleep(0).

Thread Loop - This is the third thread created by the program when GDInput is initialized. The thread loop counter is only incremented as long as the program has control over any I/O interfaces. Each loop is ended with either Sleep(0) or Sleep(1) depending on how much faster it's going than the main loop.

Message Loop - This is a Windows message pump entered immediately after starting the main loop, and thus represents the actual initial program thread. The main loop thread must return and exit before this loop will terminate. (As its termination would end the program.) This loop calls Sleep(1) since there's no necessity for it to run at top speed.

Message Count - The number of messages received by the message loop.

WndProc Count - The number of messages actually passed on from Allegro to the message handler.

Thread Resets - The number of times the input thread has reset. A reset will occur every time the application is switched in from the background so that any keys or buttons held down when the application regains the focus don't stick down.

Special Keys

The following keys have special meaning in the GDInput testing program:

Delete - Clears the typing string.

F1 thru F4 - Implements "simulated logic", designed to kill the framerate for testing the input under the worst of conditions. F1, once pressed, will cause 250,000 pixels to be drawn in random colours in the bottom right corner every frame. Each pixel draw consists of one call to putpixel(), one call to makecol(), eleven calls to rand() and eleven modulo operations. F2 is twice this, F3 three times, and F4 four times.

F5 - Turns off the simulated logic, restoring the framerate to normal.

F8 - Turns vsyncing on and off. (On by default since it looks better. The lack of timing in the program causes jumpy behaviour on some systems with vsyncing off.)

F9 - Switches between full-screen and windowed.

Escape - Quits the program.

Bugs

  • The typing string has no overflow detection. If it grows longer than the screen then the user should press delete to clear it before it gets too big.
  • The program will not automatically go into full-screen if windowed mode fails and will crash under these circumstances. (Illegal page fault before anything can really happen.)


Comparison

Differences in Methodology

GDInput, unlike Allegro's input system, relies on having the programmer manually request each update to the input state variables, as would happen in a polling situation. The purpose behind this, instead of using an interrupt driven method, is so that the programmer knows exactly when I/O states will change and can take advantage of this while programming. Because the values are being updated in the background through buffering, nothing is ever missed.

Bugs

Stuttering

This bug was first introduced into Allegro in 4.1.0, when the three separate input threads were all merged into a single input thread. (If you run a program made with Allegro prior to 4.1.0, you will note five threads, and any program made with 4.1.0 or later will have three.) This merging process introduced an annoying problem, mainly on powerful systems running older versions of Windows, whereby after every random period of time, the mouse, keyboard and joystick I/O would lock up for a split second, while gameplay would continue to process. After that split second, the input that occurred during that split second would then be processed as if it had happened normally.

This elusive bug showed up early in the initial design stages of GDInput, and was nailed down once the loop counters were added. The input thread loop, after every stutter, was showing massive increases of several tens of thousands of loops, over several hundred times what it should normally have incremented by, despite Sleep(0) being called at the end of every input thread loop.

Using Allegro alone, this problem could be worked around by calling rest(1) or Sleep(1) in a game loop instead of rest(0) or Sleep(0), however, this has the expected side effect of giving up CPU time, in the amount of 1 millisecond, all the way up to 25 milliseconds. On systems where rest(1) or Sleep(1) take that long routinely, framerates suffer with this workaround.

GDInput actually uses this same workaround, but in a special way. Normally, GDInput calls Sleep(0) at the end of its thread loop, but if the thread loop has repeated a certain number of times without the main loop requesting any of the polling buffers to be updated, it will call Sleep(1) instead.

It's not exactly certain what changed from 4.0.2 to 4.1.0 during the merge of the input threads to cause this behaviour, but GDInput gets around it while allowing the main loop to call Sleep(0) instead of Sleep(1), thus allowing the application or game logic to use as much CPU time as it is permitted.

Caps Lock

Many Allegro programmers have noted that pressing Caps Lock just once will make the shift key toggle the caps lock LED state. The reason why this happens is because of the way the Windows keyboard shift translator handles Caps Lock.

Windows processes the keyboard through scancodes and virtual keys to determine what characters are generated by what keys on the keyboard. The routines that do the translation (and is also where the problem is located) are ToAscii() and ToAsciiEx(), both of which handle the shift states of each key.

Caps Lock is special because on some keyboards, Caps Lock is a hold key, not a toggle, and the shift keys can sometimes change that state. Windows has to deal with this and the method Windows uses is to check and change shift states when ToAscii() is called.

However, ToAscii() was meant to be used with the API and an event handler, not DirectInput. The API sends events only when a key is first pressed, when it repeats and when it's released, whereas DirectInput is merely reporting which keys are held down. What ends up happening is that when the Caps Lock or shift keys are held down, ToAscii() reads them as freshly pushed, thus toggling the Caps Lock LED state, even though the virtual keyboard state says otherwise. (Because ToAscii(), not MapVirtualKey() is handling them!)

GDInput gets around this by having a table of keys which, under no circumstances, can generate typing keypresses. (Since ToAscii() is only needed for handling character conversions.) Shift, Alt, Ctrl, all three locks and certain other keys are in this table.

Task Switching

When you task switch out of an Allegro application running full-screen and switch back in using a keyboard combination, the keys you used to make the switch will be stuck down upon returning and in some cases will not reset properly. This happened with initial versions of GDInput as well. The solution was to simply reset GDInput's thread whenever a switch in occurred.

This solution was initially hindered by a bug with Allegro 4.2.1 and task switching, which caused the switch in from a minimized state to occur twice. This in turn caused the thread to start once then wait in an infinite loop to start again after being disabled, causing a crash when a GDInput using program was quit. The solution here was to simply ignore any additional requests to reset the GDInput thread.

Page Flipping

It is not known yet if this problem is solved by GDInput, but one of the problems with Allegro and input is on the implementation of page flipping, triple buffering, or manual locking and unlocking of video bitmaps such as the screen. Occasionally, under these conditions, given very specific hardware setups, all input can be halted. In Allegro 4.0.x, this problem was limited to Caps Lock and Shift. In Allegro 4.1.0 and later, the entire keyboard is affected. It may have something to do with the way shift states are read by Allegro.

Implementation Difficulties

Because of the nature of I/O programming, Allegro takes over a lot of the handling whether you initialize any of its input systems or not. For example, the mouse cursor will always be an arrow over an Allegro window no matter what you set it to if you don't call install_mouse(). (Which is made more curious since querying Windows for the Allegro window cursor gives you the resource address of IDC_WAIT as the return value, which is the hourglass.)

So in order to write an input handler that works with Allegro, the default Allegro window must be overridden with win_set_window(). This means writing a WinMain function, creating and handling a window manually, handling Windows messages with a message pump and doing multi-threaded programming.

Because GDInput requires a custom window to function, Allegro must always be set to run in a background processing mode, either SWITCH_BACKGROUND when windowed or SWITCH_BACKAMNESIA when full-screen, otherwise problems can occur during task switching situations.

Three threads are not just optimal, but required. One for the main application, one for the application message handling and one for the input handling. If the input is handled solely by the main application thread, many of its advantages wouldn't function, such as catching keystrokes when the framerate is all but gone. If the message handler is integrated into the main application loop the virtual key states in the input thread don't update properly since they need to update on demand. These threads rely on the AttachThreadInput() API command to function, otherwise virtual key states and mouse positions would go unnoticed by any of the additional threads created.

Properly sizing the window is more complicated than it may seem at first, since Allegro won't automatically do it when a custom window is utilized. Allegro has its own complex way of doing this as accurately as possible, but the simplest method is to call SystemParametersInfo() to get the size of the screen, AdjustWindowRect() to take a RECT object the size of Allegro's screen BITMAP and resize it correctly, then MoveWindow() to both set the size of the window and to centre it on the screen.


How to Obtain/Use

Basic Usage

First, download the GDInput source code. It also comes with the source to the testing program, a framework to build a game on top of and pre-compiled binaries of the Allegro 4.2.1 DLL and the GDInput testing program. The framework provided doesn't have to be used, but it's very hard to correctly implement GDInput otherwise.

Allegro and GDInput will automatically be initialized by the framework. Afterwards, Keyboard, Mouse and Joystick input must be requested by the programmer and then updated once per game loop through the appropriate commands. GDInput will automatically be shutdown along with the main thread before Allegro.

Note that it is possible to use GDInput without Allegro, but GDInput relies on the Allegro displaying switching system to correctly acquire and unacquire DirectInput devices. This handling must be moved into the message pump in order to avoid using Allegro entirely.

To properly exit the game loop and program, a call must be made to quit_mainthread() from the game and regular checks must be made to see if the _mainthread variable is any value besides 1. If it is, quit_mainthread() should be called immediately.

GDInput utilizes some C++ conventions. (Specifically in the declaration and usage of the gmouse and gjoy objects.) It is necessary to use a Windows compiler that can handle C++. It is also necessary to have the dinput.h header file and the dinput.lib library files somewhere on the system and to include the dinput.lib file in the project settings. The project's runtime settings must also be set to allow for multi-threading. (In MSVC 6 for example, the option to change is called "Use Run-Time Library" and is located in the C/C++ Code Generation settings.)

It is very important to call rest() or Sleep() with a value of 0 at the beginning or end of your game loops while using GDInput, otherwise the input thread may become starved and will not update properly. This same behaviour may also occur in Allegro's input handler.

Using the Keyboard

First, call GDINPUT_Init_Keyboard(). On success, the return value is 0. On failure, the return value will be a positive integer which can then be compared with the source code to find where the error is occurring.

In every loop that requires input from the keyboard, call GDINPUT_Poll_Keyboard() regularly to update the gkey[] state variables. The list of GKEY constants to use with the gkey[] array can be found in "g-dinput.h".

Text input however is handled immediately. Any time a key is pressed its scancode and ASCII value are added into a buffer much like the Allegro system. The commands which can read and manipulate this buffer are:

int GDINPUT_ReadKey (void) - Returns the next key in the buffer, or 0 if the buffer is empty. Do the following to separate the scancode and character code from the return result:

<highlightSyntax language="c">GDINPUT_ReadKey() >> 8 == Scancode GDINPUT_ReadKey() & 0xff == ASCII Character</highlightSyntax>

int GDINPUT_KeyPressed (void) - Returns 1 if there's a keypress waiting in the key buffer, or 0 if not.

void GDINPUT_ResetKeyBuffer (void) - Resets the contents of the keyboard buffer. Useful to call just before entering text, especially from the middle of gameplay when the contents of the buffer may not be constantly read.

void GDINPUT_AddKey (int keyval) - Allows the programmer to manually stuff a value into the keyboard buffer. Is in the same format as the return result of GDINPUT_ReadKey().

The short names of the various keyboard keys can also be retrieved by accessing the gkey_name_16[] array. Each name is a null-terminated string, most of which are no longer than 9 characters.

Using the Mouse

First, call GDINPUT_Init_Mouse(). On success, the return value is 0. On failure, the return value will be a positive integer which can then be compared with the source code to find where the error is occurring.

In every loop that requires input from the mouse, call GDINPUT_Poll_Mouse() regularly to update the gmouse object. This object stores the following information:

Variable Description
gmouse.x X coordinate of mouse cursor
gmouse.y Y coordinate of mouse cursor
gmouse.px Previous X coordinate of mouse cursor
gmouse.py Previous Y coordinate of mouse cursor
gmouse.cx X coordinate mouse cursor was at last time any button was clicked down.
gmouse.cy Y coordinate mouse cursor was at last time any button was clicked down.
gmouse.mx X mickeys (movement) since last poll.
gmouse.my Y mickeys (movement) since last poll.
gmouse.b Button state, expressed as the first four bits being the first four buttons of the mouse. (Only the first four buttons are detected.)
gmouse.pb Previous button state of the mouse.
gmouse.z Amount of mouse rolling done since last poll. Rolling towards you (down) gives a negative value, away (up) a positive value.


There are two other useful mouse handling functions:

void GDINPUT_SetMouseBounds (int bound_x1, int bound_y1, int bound_x2, int bound_y2) - Sets the boundaries of the mouse cursor. The X and Y values of the cursor will never go beyond these, even if the actual hardware cursor does. (Even though the hardware cursor is invisible when GDInput is active, it's still being processed.)

void GDINPUT_SetMousePos (int x, int y) - Sets the position of the hardware cursor and gmouse object to the coordinates provided.

Using Joysticks

First, call GDINPUT_Init_Joystick(). On success, the return value is 0. On failure, the return value will be a positive integer which can then be compared with the source code to find where the error is occurring. Once joysticks are assigned the gjoy_numsticks variable can be accessed to find out how many joysticks were detected. (Not necessarily which joysticks are responding however.) Up to 16 joysticks can be detected and allocated at a time.

In every loop that requires input from a joystick, call GDINPUT_Poll_Joystick() regularly to update the gjoy[] object array. Any part of this array can be accessed at any time as it is statically allocated, so even after any attempt to access a joystick, axis, button, etc. that doesn't exist the application will still function properly.

Each gjoy object has the following structure:

Variable Description
gjoy[n].name An array of characters with the descriptive name of the joystick.
gjoy[n].axis[0..15] Array shortcuts to the following axis values, in order.
gjoy[n].x X axis of the joystick. Every axis has a range of -10,000 to 10,000.
gjoy[n].y Y axis of the joystick.
gjoy[n].z Z axis of the joystick. (Sometimes the throttle.)
gjoy[n].rx Rx axis of the joystick.
gjoy[n].ry Ry axis of the joystick.
gjoy[n].rz Rz axis of the joystick. (Usually found on twist sticks as rudders.)
gjoy[n].s0 Slider 0 axis of the joystick. (Not always the throttle or rudder.)
gjoy[n].s1 Slider 1 axis of the joystick. (Not always the throttle or rudder.)
gjoy[n].pov0_x POV Hat #0 X axis.
gjoy[n].pov0_y POV Hat #0 Y axis.
gjoy[n].pov1_x POV Hat #1 X axis.
gjoy[n].pov1_y POV Hat #1 Y axis.
gjoy[n].pov2_x POV Hat #2 X axis.
gjoy[n].pov2_y POV Hat #2 Y axis.
gjoy[n].pov3_x POV Hat #3 X axis.
gjoy[n].pov3_y POV Hat #3 Y axis.
gjoy[n].axis_present[0..15] Set to 1 when an axis is available and 0 when it isn't.
gjoy[n].pov_angle[0..3] The angle of a POV hat, clockwise from north, in hundredths of a degree. Returns 0xffff when the hat is centred.
gjoy[n].pov_b[0..3][0..3] Conversions of the POV hats to button responses. [0] is north, [1] is east, [2] is south and [3] is west. Returns 0x80 when pressed, 0 when not.
gjoy[n].b[0..31] The first 32 buttons found on the joystick and their states.
gjoy[n]._numbuttons The number of buttons found on the joystick. (Up to a maximum of 32.)
gjoy[n]._numpovs The number of POV hats found on the joystick. (Up to a maximum of 4.)


There's only one other useful function for dealing with joysticks:

void GDINPUT_JoystickPause (int pausemode) - When set to any non-zero value, the joysticks will not be updated or polled, but their existence is left in memory so that the programmer can still determine how many joysticks are available, even if they're not being accessed. (Is basically an alternative to having to remove the joystick I/O interface from memory and bring it back every time the programmer wants to enable or disable joysticks.)


External Links