Advertisement Jump to content
  • Advertisement
  • entries
    222
  • comments
    606
  • views
    591716

About this blog

Z80 and C#-related shenanigans - now with added electronics.

Entries in this blog

 

XNA DOOM3

This journal is starting to look a little drab, so here's a splash of colour.


Not really all that colourful, on second thoughts. [smile] I fancied a short break from BBC BASIC, and seeing that the current XNA CTP supports VS 2008 I thought I'd try a bit more work on hardware-accelerated 3D. I've never worked with shadows, bump mapping or visibility portals, and have a copy of DOOM 3, so thought that would provide a nice set of resources to experiment with.


The screenshots are generated by simply brute-force rendering of all surfaces of all models in a level (.proc) file. The odd colours and lighting are courtesy of the default lighting provided by the BasicEffect class. The level files are simple to parse (they're just text files breaking each surface down into vertex and index arrays, ready to be fed to the video hardware), so though I've not found much documentation on them it's been pretty easy to guess what's what so far.


The above textures are loaded by taking the material name and appending .tga, which seems to return a random mixture of specular, diffuse or normal maps. There is a materials directory that appears to contain definitions for which image file to use for each different type of texture map, so that looks like the next thing to investigate.

benryves

benryves

 

Clipped graphics and ellipses

qarnos -- author of the superb Aether 3D engine -- has been lending a hand with the BBC BASIC graphics API and contributed a large amount of very useful code.


First up is some code to clip 16-bit line coordinates down to 8-bit coordinates. This allows for lines to be partially (or completely) off the screen.


He's also written a fast ellipse drawing and filling routine. The ellipses are also clipped to the viewport and are filled with an 8x8 pixel pattern.


The graphics viewport can be redefined using the VDU 24,left;top;right;bottom; command as demonstrated in the above example.


GCOL can also be used to set a plotting mode; either plotting the specified colour directly, performing a logical operation (OR, AND, EOR) or inverting the existing colour.


All but the last of the above screenshots are the result of running BBC BASIC on a TI-83+ SE at 15MHz. The final screenshot is running at the regular 6MHz.

benryves

benryves

 

Gyrating cubes in BBC BASIC

Work has been keeping me busy recently, but I've tried to set aside a small amount of time each evening to reclaim some sanity and do a little work on BBC BASIC. Not much progress has been made, but there has been some at least.


On the left is the program running on an 83+ SE at 15MHz, on the right on the regular 83+ at 6MHz. If you really wanted to do 3D in BBC BASIC you could probably get away with writing some of the more expensive operations -- such as transforming/projecting vertices in batches -- in assembly, but that would sort of go against the whole point of trying to write a program to test the speed of BASIC. [smile]

Here's the rather naive code:
10 *REFRESH OFF
20 DIM p%(15)
30 fps%=0
40 lfps%=0
50 fpst%=TIME+100
60 REPEAT
70 rX=TIME/300
80 rY=TIME/400
90 SrX=SIN(rX)
100 CrX=COS(rX)
110 SrY=SIN(rY)
120 CrY=COS(rY)
130 pt%=0
140 FOR x=-1TO1STEP2
150 FOR y=-1TO1STEP2
160 FOR z=-1TO1STEP2
170 tX=y*CrX-x*SrX
180 tY=-x*CrX*SrY-y*SrX*SrY-z*CrY
190 tZ=3-x*CrX*CrY-y*SrX*CrY+z*SrY
200 p%(pt%)=tX*40/tZ+48
210 pt%=pt%+1
220 p%(pt%)=tY*40/tZ+32
230 pt%=pt%+1
240 NEXT
250 NEXT
260 NEXT
270 CLG
280 PRINTTAB(10,0)lfps%" FPS"
290 MOVE p%(0),p%(1)
300 DRAW p%(4),p%(5)
310 DRAW p%(12),p%(13)
320 DRAW p%(8),p%(9)
330 DRAW p%(0),p%(1)
340 DRAW p%(2),p%(3)
350 DRAW p%(6),p%(7)
360 DRAW p%(14),p%(15)
370 DRAW p%(10),p%(11)
380 DRAW p%(2),p%(3)
390 MOVE p%(4),p%(5)
400 DRAW p%(6),p%(7)
410 MOVE p%(12),p%(13)
420 DRAW p%(14),p%(15)
430 MOVE p%(8),p%(9)
440 DRAW p%(10),p%(11)
450 *REFRESH
460 fps%=fps%+1
470 IF TIME>fpst% THEN lfps%=fps%:fps%=0:fpst%=TIME+100
480 UNTIL INKEY(0)-1
490 *REFRESH ON
500 END


I have also added support for the COLOUR statement (for changing the text foreground and background colour) and copy key editing.


Copy key editing, as demonstrated in the screenshot on the right, lets you break the text input cursor into two parts - a write cursor (which is left behind on the line you were editing) and a read cursor, which can be positioned anywhere on the screen. Pressing the copy key (in this case, XT?n) reads a character under the read cursor and writes it to the write cursor, then increments both.

One feature that's a bit more fun is the support of device files. This is a way of accessing external devices as if they were files. For example, by opening the file AT.DEV you can read and write bytes using the AT protocol (used by AT and PS/2 keyboards and mice) using BBC BASIC's built-in file manipulation routines.


You could use this to do something useful, or could just use this to flash the LED on a keyboard back and forth.

10 keyb%=OPENOUT"AT.DEV"
20 DATA 2,4,1,4,-1 : REM LED flash pattern (-1 terminated).
30 REPEAT
40 READ l%
50 REPEAT
60 PROC_setled(l%)
70 PROC_pause(30)
80 READ l%
90 UNTIL l%=-1
100 RESTORE
110 UNTIL FALSE
120 END
130 :
140 DEF PROC_flushin
150 REPEAT
160 IF EXT#keyb% d%=BGET#keyb%
170 UNTIL NOT EXT#keyb%
180 ENDPROC
190 :
200 DEF PROC_setled(l%)
210 BPUT#keyb%,&ED
220 PROC_flushin
230 BPUT#keyb%,l%
240 PROC_flushin
250 ENDPROC
260 :
270 DEF PROC_pause(t%)
280 start%=TIME
290 REPEAT UNTIL TIME >= start%+t%
300 ENDPROC

benryves

benryves

 

BBC BASIC running as an application

Richard Russell has kindly supplied the project with the BBC BASIC relocatable modules -- compiled object files which can be relocated to any memory address by a linker -- which means that BBC BASIC can now be configured to run on the TI's hardware.

The tools to relocate the modules run under CP/M, which means that rather trying to integrate the relocation into the build process (which would be a little awkward) I'm going to relocate the modules to a fixed base address and inject the resulting binary file directly into the application.

BBC BASIC will reside from &4100. From &4000..&40FF is a jump table, which BASIC uses to interact with the host. As the addresses of the host interface entry points will change as the code changes, and I don't wish to keep on relinking BASIC in CP/M, a fixed jump table makes life a lot easier. BASIC jumps to a predetermined fixed address in the jump table, which redirects - via a second jump - to the real entry point.


I think I've implemented all of the main host interface entry points, though some -- notably those involved in file I/O -- need making more robust. I don't currently reserve any memory for BASIC's scratch area, which means that the TI-OS can (and does) decide to overwrite it at inconvenient moments. Even though TI provided us with at least three different 768-byte buffers (the exact size of BBC BASIC's scratch area), none of them are aligned to a 256 byte boundary. [sad]

benryves

benryves

 

Z80 BBC BASIC - Emulated on Windows

I've started working with the actual BBC BASIC interpreter. As it won't run in its current state on the TI calculator (it relies on a jump table at &FF80..&FFFF to interact with the host, which is protected) I'm using the Z80 emulator I wrote for Cogwheel to try and puzzle out what the host interface should be doing from the relative sanity of C# code (the jump table is populated with OUT (n), A instructions which are subsequently trapped and handled by the emulator).


One thing I hadn't realised is that the graphics operations that BBC BASIC offers are actually implemented via the OSWRCH handler (OS WRite CHaracter), which means that BBC BASIC's PLOT, MOVE and DRAW commands will also be available, as well as any commands that use them indirectly (such as CIRCLE).

benryves

benryves

 

BBC BASIC

This is a project I initially attempted to get off the ground about four years ago, but never did. Anyhow, I've started work on it, and thanks to help from Richard Russell (the original developer) and J.G.Harston (who comparatively recently developed the Sinclair ZX Spectrum port) it looks like it should be possible this time around. [smile]

BBC BASIC was the native programming language on Acorn's BBC Micro. It's a structured BASIC dialect and supports procedures and functions, permitting far nicer code than the line-numbered GOTO and GOSUB code on other contemporary machines. It also has a built-in assembler, for inline assembly.

There is no source available, which is where the problems start to come in. Fortunately, J.G.Harston has developed a utility that permits the platform-agnostic BBC BASIC interpreter to be relocated. However, it assumes that the system has a jump table in RAM from $FF80..$FFFF (this jump table would be used to call platform-specific code); this memory range is not executable on the TI-83+. Execution protection in the $C000..$FFFF range may also cause issues for inline assembly code (which is, naturally, executed from RAM).

The TI-OS does not offer an especially suitable environment for BBC BASIC either; it is mainly menu driven (a command-line driven environment is preferable), does not have a plain text editor and does not use ASCII. To resolve this issues, I've concentrated on developing a suitable environment for BBC BASIC to live in, including a command-line interface and text editor.



Text files are stored as AppVars with a TEXTFILE header, and I've developed a Windows-based notepad clone for editing them (it saves and loads directly to and from .8xv). The following commands are currently supported (see here for a reference from the Windows verion): BYE, COPY, DELETE, DIR, ERASE, EXEC, QUIT, RENAME, TYPE, |.

To enter the editor, EDIT can be used. This presents a full-screen editor a little like the TI-OS program editor, but edits plain text files.

The interface transparently supports AT keyboards (which are rather easier to type on than the TI's keypad). The character resolution is 24 columns in 10 rows (4x6 pixel characters), giving you quite a lot of room to see what you're working on.

I intend on the final program being a 2-page Flash application; one page for BBC BASIC, and one page for the environment and OS interface. This unfortunately makes this a TI-83+ only project.

Even if I don't manage to shoe-horn BBC BASIC onto the TI-83+, the interface code (which uses direct hardware access for everything but opening and editing AppVars) could be useful for other projects.

benryves

benryves

 

CSG, fisheye and spotlights.

One way of constructing solids is to use a method named constructive solid geometry (or CSG for short). I added two simple CSG operators - intersection and subtraction - that both take two surfaces and return the result.


In the above image, the surface that makes up the ceiling is created by subtracting a sphere from a plane. Of course, much more interesting examples can be created. For example, here is the surface created by taking a sphere and subtracting three cylinders from it (each cylinder points directly along an axis).


One problem with the camera implementation was that it couldn't be rotated. To try and aid this, I used a spherical to cartesian conversion to generate the rays - which has the side-effect of images with "fisheye" distortion.


The above left image also demonstrates a small amount of refraction - something that I've not got working properly - through the surface. The above right image is the result of the intersection of three cylinders aligned along the x,y and z axes.

To try and combat the fisheye distortion, I hacked together a simple matrix structure that could be used to rotate the vectors generated by the earlier linear projection. The result looks a little less distorted!


The final addition to the raytracer before calling it a day was a spotlight. The spotlight has an origin, like the existing point light - but it adds a direction (in which it points) and an angle to specify how wide the beam is. In fact, there are two angles - if you're inside the inner one, you're fully lit; if you're outside the outer one, you're completely in the dark; if you're between the two then the light's intensity is blended accordingly.


In the above screenshot, a spotlight is shining down and backwards away from the camera towards a grid of spheres.

If you're interested in an extremely slow, buggy, and generally badly written raytracer, I've uploaded the C# 3 source and project file. The majority of the maths code has been pinched and hacked about from various sites on the internet, and there are no performance optimisations in place. I do not plan on taking this project any further.


Building and running the project will result in the above image, though you may well wish to put the kettle on and make yourself a cup of tea whilst it's working. [smile]

benryves

benryves

 

Cylinders and translucent surfaces

The first addition to the raytracer was a cylindrical surface, represented by two end points and a radius. In the two screenshots above, the cylinder is infinitely long - not very useful. However, by calculating the point on the cylinder's axis that is closest to the struck point on its surface you can work out how far along its axis you are, and from that whether you are between either of the cylinder's ends.


The cylinder can have its ends optionally capped. To add the caps, you can create plane that has a normal that points in the the direction of the cylinder's axis. If you collide with the plane, you can then calculate the distance between the point you struck on it and the end coordinate of the cylinder. If this distance is smaller than the radius of the cylinder, you've hit one of the end caps.


Texturing the cylinders proved rather difficult. The v component of the texture component can be calculated by working how far along the cylinder's axis you are - easy. However, to wrap a texture around the rotational axis of the cylinder is a bit more complicated. In the first screenshot, I simply used Math.Atan2(z,x) to get the angle, and hence texture coordinate - but this only works if the cylinder points along the y axis. If I had another vector that lay perpendicular to the cylinder's axis I could use the dot product to work out its angle, but I don't. The cross product could generate one, but I'd need another vector to cross the axis with... In the end, this post came to my rescue, and I managed to get it working for cylinders pointing along any axis - producing the second screenshot.

An addition I've wanted to make for a while was support for translucent surfaces.


This required a few changes to the structure of the raytracer. Previously, all methods for calculating ray intersections had to return a single Collision object, which contained a boolean flag specifying whether the collision was succesful. A translucent sphere would need to return two collision points - one as the ray enters the front and one as it leaves the back of its surface. To this end, all collision detection methods now return an array (empty or null if no collisions were made), and each collision has a flag indicating whether the collision was made by the ray entering the solid or leaving the solid (this is required, for example, to invert surface normals for correct shading).

Once the nearest struck point to the camera has been handled (including recursive reflection-handling code) it is checked to see if it's on a translucent surface. If so, the raytracer continues raytracing away from the camera, and blends the new result with the existing result based on its opacity. By stopping and starting again, one can adjust the direction of the ray - for example, to add a refraction effect (which I have not yet got working [sad]).

One trick the above image misses is that it's still simply scaling down the intensity of the light by the opacity of the surface it passes through. It would look nicer if the light was coloured by the surface it passes through; so, in the above example, the white light shining through the blue water on the sphere should cast blue-tinted shadows.


Whilst it's far from being real-time, I can still make it dump out a sequence of frames to create a basic animation.


benryves

benryves

 

Fixed reflections and proper textures

Just over a minute, so not very good performance at all. I've made some changes since then (including multithreading) that drop it down to about 30 seconds.


There is definitely something wrong with the reflections.
I decided to rewrite the main raycasting code from scratch, after seeing results such as the above. I'm not sure where the speckles were coming from, nor why the reflections were being calculated incorrectly. The new code writes to regions of an array of integers (for 32-bit ARGB output), and is designed much more simply. By splitting the output buffer into two halves I can perform the raytracing in two threads, which makes much better use of modern dual-core CPUs.


Before and after rewrite.
A scene with lots of reflective spheres would seem like a good test. If you look at the reflections in the outer ring of spheres, they're quite different (and now appear to be correct) now, so whatever was wrong now seems to have been fixed.


A similar scene to the one at the top of this entry.
A scene with multiple reflective planes no longer appears to have the noise and reflection bugs that were clearly visible in the first screenshot in this entry.

Textures would certainly make the objects look a bit more interesting, but I couldn't think of a simple way of aligning a texture to a surface. I decided that textures should be treated as simple 2D rectangles, and each material can now have a diffuse texture applied to it (which provides a method Colour GetColour(Vector2 coordinate) to read it). To attach the texture to the surface of an object the surface needs to implement ITexturable, which exposes the method Vector2 GetTextureCoordinate(Vector3 surfacePoint).

In short; it's the job of the surface class (such as the Sphere or Plane classes) to map the struck point to a texture coordinate. This is most easily handled with the sphere, which simply converts the cartesian coordinates of the stuck point to polar coordinates.


The small foreground sphere has an Earth texture.
For planes, I thought that the easiest way of aligning the texture would be to declare two vectors - one that represents the texture's X axis and one that represents the texture's Y axis.

For example, take the white wall at the back of the room in the above screenshot. To align a texture parallel to its surface, one could set the texture's X axis vector to point right and its Y axis vector to point down. By changing the magnitude of these vectors the texture can be scaled.


The back wall and floor are textured planes.
For the floor in the above image, the texture's X axis points right, and its Y axis points into the screen.

As the texture merely has to provide a method that takes in a texture coordinate and outputs a colour, this lets us declare simple procedural textures.


The floor and ceiling textures are procedurally generated.
The rather garish ceiling is declared like this in code:
this.Tracer.Objects.Add(new WorldObject() {
Surface = new Plane(Vector3.Down, 10.0d) {
TextureXAxis = Vector3.Right,
TextureYAxis = Vector3.Forward,
},
Material = new Material() {
Colour = Colour.White,
Texture = new ProceduralTexture(
p => new Colour(
1.0d,
(Math.Sin(p.X) * Math.Cos(p.Y * 2)) / 2.0d + 0.5d,
(Math.Cos(p.X) * Math.Sin(p.Y * 3)) / 2.0d + 0.5d,
(Math.Sin(p.X * 5) * Math.Sin(p.Y / 0.3d)) / 2.0d + 0.5d
)
),
},
});

I think before I go any further I'm going to need to support a wider variety of surfaces than spheres and planes. Another limitation with the existing implementation is that only a single collision between a ray and a surface is reported, which limits what can be done with the renderer - for example, a glass sphere that refracts a ray that passes through it would need to report two collisions, one for the front of the sphere as the ray passes through and again one for the back as the ray leaves.

benryves

benryves

 

Raytraced shadows, reflections and chessboards

I thought that better lighting might help the scene look a bit nicer, so decided removed all the existing lighting code (and reflection code, to make life easier) and tried to add some basic shadowing.


Simple shadowing test.
When a ray's intersection with the world is found, a ray is cast back from that point towards the light source. If this ray collides with another object on its return trip to the light, it's assumed that it's in the shade. In the above test, points in shade simply had their diffuse colour divided by two. This still looks rather flat, though.

As we know the surface normal of the surface that has been struck and the direction of the ray that's going between the struck point and the light source, we can work out how much the surface point is facing the light by taking the dot product of the two vectors. When multiplied by the surface's diffuse colour, this results in much smoother lighting.


Each colour value is multiplied by the dot product of the light beam and the surface normal.
That's better, but would look better with multiple lights. I start with a running total colour (initially black), then iterate over a list of lights. If the struck point is in line with the light, I add the surface diffuse colour multiplied by the light colour to this running total. This results in a much more interesting-looking scene.


Multiple lighting test - two white and one red light, all in different positions.
The edges of the surfaces are rather ugly and noisy, probably due to rounding errors. They are helped if I offset the pixel coordinates by 0.5 (so the rays are shot through the centre of pixels, rather than the top-left corner), but proper supersampling would probably look better.


Rendering with 4x supersampling.
It does, thankfully, at the expense of making the rendering time four times longer!


Reintroduction of reflective surface support.
I reintroduced the reflective surfaces - those reflections don't look quite right to me, but I can't really tell. More detail in the world might make it easier, so I'd like to add some sort of texturing.


The obligatory chessboard texture.
I'm undecided how to handle mapping struck points to texture coordinates. For the moment I'm just XORing together the x, y and z components together - if the least significant bit of the result is zero, return half of the diffuse colour, otherwise return the full diffuse colour.

benryves

benryves

 

Raytracing - Beware of the coder colours

As much as I claim to be interested in software rendering (be it as part of a game engine or as an effect in a demo), I've never actually written a raytracer. Having written some basic vector and plane arithmetic code for physics in the XNA Quake project, I thought I'd give it a stab.


No apologies made for the coder colours.
Currently, the world is just a simple List, where each WorldObject has a Surface and Material property. The Surface has to implement IRayCollidable, which lets me call GetCollision(Ray) on it to find out where a ray strikes it (if at all), returning the point of collision and the normal of the surface that was hit. Currently, there are only two types that implement this interface - Plane and Sphere - but they'll do for testing.

For each ray, I iterate over the list of items in the world and grab the collision point. If a collision is made, I add the details to another list (including the total length of the ray at this point) and, if the surface's material is marked as reflective (ie, has a Reflectivity property greater than zero) I reflect the ray against the surface normal and cast again (recursively, so it's very easy to cause a StackOverflowException when two shiny surfaces are parallel to eachother).

Once I have a record of all the collisions, I sort them in back-to-front order based on the length of the ray, then iterate over them, blending the colours as I go (so a reflection in a green surface ends up being green tinted).


Marginally less garish.
To try and get a better sense of the 3D scene, I added a simple directional light. This simply takes the dot product of the hit surface normal and the light's direction, then multiplies it by the material's diffuse colour. The above screenshot has a light pointing directly away from the camera, hence the upper and left walls are completely black (however, the bottom and right walls, being reflective, are partially visible).

I've trying to do this without looking up the correct way of doing it, experimenting as I go - mainly in an attempt to try and patch up my rather poor handle on 3D maths and collision detection.

benryves

benryves

 

SC-3000 keyboard and a final release

The latest addition to Cogwheel is SC-3000 keyboard emulation.


The SC-3000 was a home computer with similar hardware to the SG-1000 console, with the main addition of a keyboard. Software cartridges could add, for example, BASIC programming capabilities.

Due to lack of time and motivation, and the fact that the emulator is pretty much as good as I'm going to get it at this moment in time, I've removed the beta label and uploaded the latest version to its website.

benryves

benryves

 

3D glasses and CPU cycle counting

I reintroduced joystick support to the emulator front-end over the weekend, using a much more sensible input manager. The control editor form uses the same interface design for keyboard and joysticks - you click the button you wish to edit, it appears pressed, you press the key (or joystick button) you wish to bind to it and it pops back out again. It'll also check to see if you've moved the joystick in a particular direction on any of its reported axes.

Another feature I added was better support for games that used the 3D glasses, using a simple red-cyan anaglyph output blender.




If the memory address range that the glasses respond to has been written to within the last three frames, it switches to the blending mode.

Irritatingly, with one glaring bug fix I managed to lower overall compatibility. Some instructions weren't being timed correctly (or at all) meaning that the emulator was executing too many instructions for the number of clock cycles it was asked to run. During the time each video scanline is run, 228 CPU cycles are executed. Upping this to 248 cycles (just for experimentation) fixes all known bugs - including the long-standing flickering pixels at the top of the screen in Cosmic Spacehead and display corruption in GP Rider. (However, digitised speech plays at a noticably higher pitch).

I'm not entirely sure why this is - it could be that some instructions are claiming to take too long, it could be yet another interrupt problem. To call this mildly frustrating is a bit of an understatement!

benryves

benryves

 

ColecoVision and TMS9918 Multicolor

One notable gap in my TMS9918 (video) emulation was its "Multicolor" mode. This mode broke the screen down into 4x4 pixel squares, resulting in a 64x48 grid. Each cell could be assigned a unique colour, giving you a crude bitmapped video mode.

No Master System or SG-1000 software used this mode to my knowledge, which reduced the likelihood of it being supported at all (if I could't test it, how could I emulate it?) I was tipped off that some ColecoVision software made use of it, so set about emulating the ColecoVision.


The ColecoVision hardware is very similar to the SG-1000 in terms of what is inside the case - a Z80 CPU, TMS9918 video and SN76489 sound. The memory map (as in, which address ranges map to which memory devices) is different, as is the I/O map (as in, the I/O ports that the various hardware components are connected to). To handle this case, the emulator now has a Family field, which can be set to Sega or ColecoVision. This controls which of the different mappings it uses for memory and hardware I/O.

Another difference is the presence of a BIOS ROM. The Sega Master System and Sega Game Gear consoles had the option of BIOS ROMs, but all these did was very basic initialisation and header/checksum checking. The ColecoVision, however, has an 8KB BIOS ROM that offers a lot of functionality to the programmer, so this must be present to run most ColecoVision games.


The controllers are also quite different. As well as the typical eight-direction joystick and two fire buttons, it added a 12-key keypad (0-9, * and #). This many keys is making my InputManager class look thoroughly idiotic, so that will certainly need a rewrite.

Apart from that, it's pretty simple. RAM is 1KB instead of 8KB; the sound generator uses the standard 15-bit wide shift register (instead of the 16-bit wide one used in the SMS); the video display processor interrupt output is connected to NMI rather than INT.

With those differences applied, it's easy to add the few lines of code to emulate the Multicolor video mode.


Smurf Paint 'n' Play Workshop
ColecoVision emulation has not been tested at all thoroughly, so chances are it doesn't work very well; Multicolor emulation has been tested with precisely one game!

You can download the latest build of Cogwheel from its website, featuring the new ColecoVision emulation.

benryves

benryves

 

SaveStates and Key Names

It's a good feeling when issue 1 is finally marked as Fixed - in this case it was another interrupt-related bug. The IFF1 flag was being used to mask non-maskable interrupts; I don't think it should and it hasn't seem to have broken anything just yet by making non-maskable interrupts truly non-maskable.


Go! Dizzy Go!

I have also changed the savestate format to something that will be a little more backwards compatible rather than a plain BinaryFormatter dump of the entire emulator state. The data is now saved in what is basically an INI file with C#-style attributes and CSS-style url() syntax for binary data.

[Type(BeeDevelopment.Sega8Bit.Hardware.VideoDisplayProcessor)]
VideoRam=Dump(Video\VideoRam.bin)
Address=49165
WaitingForSecond=False
AccessMode=ColourRamWrite
ReadBuffer=32
Registers=Dump(Video\Registers.bin)
System=Ntsc
SupportsMode4=True
; ... snip ...

Other changes include a faked fullscreen mode (ie, a maximised borderless window) and an option to retain the aspect ratio of the game, so games no longer appear stretched on widescreen monitors. The sound emulation is a bit better, but still a little noisy in certain games with spurious beeps or buzzes.

One minor problem was on the key configuration control panel. Converting members of the Keys enumeration into displayable strings can be done via .ToString(), but this results in names that do not match the user's keyboard layout (such as OemQuestion for /, OemTilde for # and even Oemcomma for , - note the lowercase c, all on a UK keyboard).

With a prod in the right direction from jpetrie, here's a snippet that can be used to convert Keys into friendly key name strings:
#region Converting Keys into human-readable strings.

///
/// Converts a value into a human-readable string describing the key.
///
/// The to convert.
/// A human-readable string describing the key.
public static string GetKeyName(Keys key) {

// Convert the virtual key code into a scancode (as required by GetKeyNameText).
int Scancode = MapVirtualKey((int)key, MapVirtualKeyMode.MAPVK_VK_TO_VSC);

// If that returned 0 (failure) just use the value returned by Keys.ToString().
if (Scancode == 0) return key.ToString();

// Certain keys end up being mapped to the number pad by the above function,
// as their virtual key can be generated by the number pad too.
// If it's one of the known number-pad duplicates, set the extended bit:
switch (key) {
case Keys.Insert:
case Keys.Delete:
case Keys.Home:
case Keys.End:
case Keys.PageUp:
case Keys.PageDown:
case Keys.Left:
case Keys.Right:
case Keys.Up:
case Keys.Down:
case Keys.NumLock:
Scancode |= 0x100;
break;
}

// Perform the conversion:
StringBuilder KeyName = new StringBuilder("".PadRight(32));
if (GetKeyNameText((Scancode 16), KeyName, KeyName.Length) != 0) {
return KeyName.ToString();
} else {
return key.ToString();
}
}

///
/// Retrieves a string that represents the name of a key.
///
/// Specifies the second parameter of the keyboard message (such as WM_KEYDOWN) to be processed.
/// Pointer to a buffer that will receive the key name.
/// Specifies the maximum length, in TCHAR, of the key name, including the terminating null character. (This parameter should be equal to the size of the buffer pointed to by the lpString parameter).
/// The length of the returned string.
[DllImport("user32.dll")]
static extern int GetKeyNameText(int lParam, StringBuilder lpString, int size);

///
/// Translates (maps) a virtual-key code into a scan code or character value, or translates a scan code into a virtual-key code.
///
/// Specifies the virtual-key code or scan code for a key. How this value is interpreted depends on the value of the parameter.
/// Specifies the translation to perform. The value of this parameter depends on the value of the parameter.
/// Either a scan code, a virtual-key code, or a character value, depending on the value of and . If there is no translation, the return value is zero.
[DllImport("user32.dll")]
static extern int MapVirtualKey(int uCode, MapVirtualKeyMode uMapType);

enum MapVirtualKeyMode {
/// uCode is a virtual-key code and is translated into a scan code. If it is a virtual-key code that does not distinguish between left- and right-hand keys, the left-hand scan code is returned. If there is no translation, the function returns 0.
MAPVK_VK_TO_VSC = 0,
/// uCode is a scan code and is translated into a virtual-key code that does not distinguish between left- and right-hand keys. If there is no translation, the function returns 0.
MAPVK_VSC_TO_VK = 1,
/// uCode is a virtual-key code and is translated into an unshifted character value in the low-order word of the return value. Dead keys (diacritics) are indicated by setting the top bit of the return value. If there is no translation, the function returns 0.
MAPVK_VK_TO_CHAR = 2,
/// uCode is a scan code and is translated into a virtual-key code that distinguishes between left- and right-hand keys. If there is no translation, the function returns 0.
MAPVK_VSC_TO_VK_EX = 3,
MAPVK_VK_TO_VSC_EX = 4,
}

#endregion

It uses P/Invoke and the Win32 API so is only suitable for use on Windows.


Out Run

benryves

benryves

 

Fun with IThumbnailProvider

Note: I have been informed that the code below no longer works in Windows 7 due to changes in the way IThumbnailProvider operates. It is recommended that you use unmanaged code instead of the managed solution presented below.


I have started releasing Cogwheel binaries on its project page, so if you'd like a look at the project but can't be bothered to check out and build the source yourself you can now give it a whirl.

One of the newer additions is a savestate mechanism; this is a very lazy bit of code on my behalf as all it does currently is serialise the entire emulator to a file using the BinaryFormatter. This resulted in savestates weighing in at about 6MB; by marking certain private fields (such as look-up tables in the Z80 emulator) as [NonSerialized] it was down to 2MB. To squash it down to the current ~250KB size the savestate is compressed using the zip file classes I've written to handle loading ROMs from zips.

Whilst this is going to change soon (I'm currently working this on an simple INI file serialiser, so the savestate files will be compatible with later releases of the software) I decided to experiment with the idea of dumping extra data into the savestate - namely, a screenshot.


The screenshot is simply saved as Screenshot.png in the root of the savestate's zip archive. Creating a thumbnailer is extremely easy under Vista, and as the thumbnailer runs out-of-process you can use .NET code! Here's a quick and dirty run-down of how to make them if you decide to write one yourself.


Setting up the project

Create a new class library project in Visual Studio, then go switch to its project properties editor. On the Application tab, set Target Framework to something sensible (I currently try and keep everything at .NET 2.0 level), then click on the Assembly Information button and tick the Make assembly COM-Visible box.

Finally, move to the Signing tab, and tick the box marked Sign the assembly. From the drop-down box, pick New, which will create a new key file and add it to the project (this is required later for COM registration).

Add the COM interface wrappers

This is a simple copy and paste job! Just bung this in a source file somewhere:

using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;

namespace Thumbnailer {

///
/// Defines the format of a bitmap returned by an .
///
public enum WTS_ALPHATYPE {
///
/// The bitmap is an unknown format. The Shell tries nonetheless to detect whether the image has an alpha channel.
///
WTSAT_UNKNOWN = 0,
///
/// The bitmap is an RGB image without alpha. The alpha channel is invalid and the Shell ignores it.
///
WTSAT_RGB = 1,
///
/// The bitmap is an ARGB image with a valid alpha channel.
///
WTSAT_ARGB = 2,
}

///
/// Exposes a method for getting a thumbnail image.
///
[ComVisible(true), Guid("e357fccd-a995-4576-b01f-234630154e96"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IThumbnailProvider {
///
/// Retrieves a thumbnail image and alpha type.
///
/// The maximum thumbnail size, in pixels. The Shell draws the returned bitmap at this size or smaller. The returned bitmap should fit into a square of width and height , though it does not need to be a square image. The Shell scales the bitmap to render at lower sizes. For example, if the image has a 6:4 aspect ratio, then the returned bitmap should also have a 6:4 aspect ratio.
/// When this method returns, contains a pointer to the thumbnail image handle. The image must be a device-independent bitmap (DIB) section and 32 bits per pixel. The Shell scales down the bitmap if its width or height is larger than the size specified by cx. The Shell always respects the aspect ratio and never scales a bitmap larger than its original size.
/// Specifies the format of the output bitmap.
void GetThumbnail(int cx, out IntPtr hBitmap, out WTS_ALPHATYPE bitmapType);
}

///
/// Provides a method used to initialize a handler, such as a property handler, thumbnail provider, or preview handler, with a file stream.
///
[ComVisible(true), Guid("b824b49d-22ac-4161-ac8a-9916e8fa3f7f"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IInitializeWithStream {
///
/// Initializes a handler with a file stream.
///
/// Pointer to an interface that represents the file stream source.
/// Indicates the access mode for .
void Initialize(IStream stream, int grfMode);
}

}

(You may wish to set the namespace to something more appropriate). As you can see, most of that source file is documentation.

Create your thumbnailer class

First thing you'll need to do here is to generate a GUID for your thumbnailer; this is so that when you register your thumbnailer Windows will know which COM object to create an instance of which it can then call to generate a thumbnail (the GUID of your thumbnailer is attached to the extension of the file via standard file associations - more on that later).

Your thumbnailer class should implement two interfaces; IThumbnailProvider (obviously!) and IInitializeWithStream. Here's a skeleton class for the thumbnailer:

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;

namespace Thumbnailer {

[ComVisible(true), ClassInterface(ClassInterfaceType.None)]
[ProgId("YourApp.ThumbnailProvider"), Guid("YOUR-GUID-IN-HERE")]
public class ThumbnailProvider : IThumbnailProvider, IInitializeWithStream {

#region IInitializeWithStream

private IStream BaseStream { get; set; }

public void Initialize(IStream stream, int grfMode) {
this.BaseStream = stream;
}

#endregion

#region IThumbnailProvider

public void GetThumbnail(int cx, out IntPtr hBitmap, out WTS_ALPHATYPE bitmapType) {

hBitmap = IntPtr.Zero;
bitmapType = WTS_ALPHATYPE.WTSAT_UNKNOWN;

try {

// Thumbnailer code in here...

} catch { } // A dirty cop-out.

}

#endregion
}
}

You will probably want to set the ProgId to something meaningful, and make sure you set the GUID to the one you just generated.

What will happen is that Windows will first initialise your object by calling IInitializeWithStream.Initialize(), passing in an IStream. The above implementation stores the IStream in a member property for future reference.

Windows will then call IThumbnailProvider.GetThumbnail(). cx is the maximum size of the thumbnail (width and height) you should return; Windows will scale your thumbnail down if you return one that is too large. Do not scale your thumbnail up to match this value; it is perfectly valid to return one that is smaller than the requested value. Also; do not scale your thumbnail up to a square: you should return it at the same aspect ratio of the source image.

For the moment, and for the sake of testing, here's a snippet that will create a bright red thumbnail using GDI+:

using (var Thumbnail = new Bitmap(cx, cx)) {
using (var G = Graphics.FromImage(Thumbnail)) {
G.Clear(Color.Red);
}
hBitmap = Thumbnail.GetHbitmap();
}


Registration

If you compile your class library at this point you should end up with a single DLL. You need to register this DLL using the command-line tool RegAsm.exe that comes with the .NET framework.

Open an elevated command prompt (you need admin rights for this bit) and set the working directory to the output directory of your DLL. Now, invoke the following command:
%windir%\Microsoft.NET\Framework\v2.0.50727\RegAsm /codebase YourThumbnailer.dllThat's half of the battle; the last bit boils down to conventional file associations.

Run the registry editor, and open the HKEY_CLASSES_ROOT key. You will see a list of keys representing file extensions; find one (or create a new one) to match the extension that you wish to attach your thumbnailer to. Under that create a new key named shellex, and under that create another key named {e357fccd-a995-4576-b01f-234630154e96}. Set its (Default) value to {YOUR-GUID-IN-HERE} - yes, the GUID you created earlier. That should look something like this:
HKEY_CLASSES_ROOT.yourextensionshellex{e357fccd-a995-4576-b01f-234630154e96} = {YOUR-GUID-IN-HERE}
That's it! [smile] You may need to log out then in again (and/or reboot and/or just kill all Explorer instances and restart them) for Explorer to catch on if nothing seems to be working.

A final note: IStream to Stream

The only final hitch is that IStream is not the same as our beloved .NET Stream. I use the following snippet to dump all of the contents of an IStream into an array of bytes (which can then be converted to a stream using new MemoryStream(byte[]) if need be).

private byte[] GetStreamContents() {

if (this.BaseStream == null) return null;

System.Runtime.InteropServices.ComTypes.STATSTG statData;
this.BaseStream.Stat(out statData, 1);

byte[] Result = new byte[statData.cbSize];

IntPtr P = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(UInt64)));
try {
this.BaseStream.Read(Result, Result.Length, P);
} finally {
Marshal.FreeCoTaskMem(P);
}
return Result;
}

This, naturally, is not a good idea if you're thumbnailing very large files, as it dumps the entire thing into memory!

For more information, take a look at MSDN: Thumbnail Providers, which includes useful information (including how to change the overlay icon in the bottom-right of your thumbnails or the adornments).

The COM wrappers and GetStreamContents() snippet are based on this uberdemo article.




Finally, another screenshot; you can now load IPS patch files directly into Cogwheel using the advanced ROM load dialog - which can be useful for translations.

benryves

benryves

 

Sound, at long last.

I have finally got around to adding sound to Cogwheel using Ianier Munoz's waveOut API wrapper.



The technique used is fairly simple. I start with a sound buffer that is a multiple a number of video frames in length (1/60th of a second is one frame) - four seems a good number. This buffer needs to be periodically topped up with sound samples (every four frames in the above example).

I run the emulator for one frame, then generate a frame's worth of audio. I add these samples to a queue. The sound callback then periodically dequeues these samples and appends them to its buffer.

// This is called once every video frame.
// 735 samples at 44100Hz = 1/60th second.
// (Multiplied by two for stereo).
this.Emulator.RunFrame();
short[] Buffer = new short[735 * 2];
this.Emulator.Sound.CreateSamples(Buffer);
this.GeneratedSoundSamples.Enqueue(Buffer);


The important thing is that the sound is always generated after the video frame (and thus after any hardware writes). I log writes to the sound hardware over the period of a frame (along with the number of CPU cycles that have elapsed), then space them out when generating the sound samples so that they play in synch. My previous problems were caused by the sound emulation trying to "look ahead" past what had already been generated.

However, there is a potential problem with this - as the video and sound emulation are not locked in synch with eachother, there are two cases that could crop up:
The emulator runs faster than 60Hz, generating too many sound samples. The emulator runs slower than 60Hz, not generating enough sound samples. The first is the easiest to deal with. In most instances you'd want a couple of extra frames of sound data left in the queue after topping up the sound buffer, in case in the next period not enough are generated. However, if I notice that the queue is longer than entire sound buffer after topping it up, I clear it completely. This would make the sound a little choppy, but so far this hasn't happened in my tests.

The latter is a little more complex. If I just left it the sound buffer would have gaps in it, causing noticable pops (this I have noticed in some of the more processor-intensive games). To cover up the gaps, I generate enough extra frames of sound data to fill the gap. As no sound hardware writes are made, this has the effect of extending any tones that were currently playing, so the sound will play back slightly out of time. However, slightly out of time by a few 60ths of a second is a better solution than a pop.

// This is called when the sound buffer needs topping up.
// That's about once every four frames.
private void SoundBufferFiller(IntPtr data, int size) {

// Temporary buffer to store the generated samples.
short[] Generated = new short[size / 2];

for (int i = 0; i 735 * 2) {
if (this.GeneratedSoundSamples.Count > 0) {
// We've already queued up some sound samples.
Array.Copy(this.GeneratedSoundSamples.Dequeue(), 0, Generated, i, 735 * 2);
} else {
// Erk, we're out of samples... force generate some more and use those instead.
// (This avoids popping).
short[] Temp = new short[735 * 2];
this.Emulator.Sound.CreateSamples(Temp);
Array.Copy(Temp, 0, Generated, i, 735 * 2);
}
}

// Copy to the sound buffer.
Marshal.Copy(Generated, 0, data, size / 2);

// If too many samples are being generated (FPS > 60Hz) then make sure it doesn't go out of control.
while (this.GeneratedSoundSamples.Count > this.SoundBufferSizeInFrames) this.GeneratedSoundSamples.Dequeue();

}

benryves

benryves

 

Joysticks and Game Genie codes

The biggest update to the emulator relates to input.



I've added a new control panel that lets you customise key bindings. For the keyboard, you simply click on the button you wish to customise, then press the key on your keyboard you wish to bind. As you can probably tell from the screenshot, I've also added joystick support (via the Multimedia Joystick Functions with a bit of P/Invoke for ease) which means that with a simple adapter and a free driver you can use original SMS pads with the emulator.

I haven't added support for the POV hat, which needs doing as the PPJoy PlayStation controller driver exposes the d-pad as a POV hat.


I've also added an interface for adding Game Genie codes. The Game Genie was a cheating device that could be used to patch memory.

I have also started reintroducing (albeit in a slightly buggy fashion) multiple memory devices. Previously I've just been emulating a cartridge ROM and work RAM; the real consoles had multiple memory devices including a card slot, ROM BIOS and expansion slots. By emulating this system one can boot with the original BIOS, and (for example) not insert a cartridge to watch the BIOS animation or play its integrated game.

For ease, there's a quick-load ROM menu option that will load anything into the cartridge slot and disable all others for quick game playing. For those who wish to play with the hardware more, there's an "advanced load" dialog that will let you pick what goes in which slot, and also force various options (such as locale, hardware model, video system and so on) instead of the automatically guessed options. I'd also like it to be able to pick patch files (such as translations) so you don't need to use an external tool. Currently this dialog only supports cartridge ROM and BIOS ROM loading, though.

benryves

benryves

 

Cogwheel on Google Code

On bakery2k1's suggestion, I took a look at my sprite collision flag code. It only checked for collisions between sprites appearing in the foreground - sprites that were hidden behind a background tile weren't checked, which caused the Fantastic Dizzy bug.


I have decided to give Google's free project hosting a whirl. As usual, they can't get it working in Opera [flaming] (no big surprises there) and it's horrifically slow, but I can't really complain given that it's free, and the issue tracking is pretty handy.


Another game that has been fixed is the conversion of that arcade classic, Gauntlet. This was partially due to buggy controller port emulation, but a long-standing bug has been the lack of the y-scrolling inhibition flag. Games could set a flag on the VDP that would prevent the rightmost 64 pixels from scrolling vertically - this is useful to create a locked status bar on the right for a vertical scrolling game, for example.


I'm slowly getting there with FluBBa's VDP tester...

benryves

benryves

 

/INTerminable Interrupt Problems

World Class Leaderboard Golf

Well, I've just about got it the way it was before with the new interrupt code; most games work again, but the few that have always given problems (such as Desert Speedtrap) still don't work properly. [sad] I think this is the stage where I have to start trawling through disassemblies to try and work out why they're not working.


Cosmic Spacehead - one of the few games to use the 256x224 mode.

One problem I still haven't got to the bottom of is with the Dizzy games. They either reset when starting a new game or lock up. I'm hoping this is a problem with my implementation of the Codemasters mapper. I guess that The Excellent Dizzy Collection has a simple front-end game selection screen that switches to the requisite ROM page for the selected game, then jumps to the start of that page - in my case this jumps back to the initial Codemasters screen.


Fantastic Dizzy

I'm not sure where Fantastic Dizzy's problems originate. It looks like an interrupt problem (maybe the palette should be switched when the VDP has finished with the status bar at the top?), but could also be related to the other Codemasters problems.

Interestingly, Micro Machines (1 and 2) and Cosmic Spacehead - both using the Codemasters ROM mapper - seem to work fine.

benryves

benryves

 

Interrupts: A Fresh Start

I gave in and rewrote all of the Z80's interrupt emulation from scratch, finding some rather horrible bugs in the existing implementation as I went.

Some of the highlights included non-maskable interrupts ignoring the state of the IFF1 flag (this flag is automatically cleared when an interrupt is serviced, and is used to prevent the interrupt handler from being called again before it has finished) and the RETN instruction not copying the state of the IFF2 flag back to IFF1. When non-maskable interrupts are serviced, the state of IFF1 is copied to IFF2 before it gets cleared, the idea being that if you use RETN interrupts are automatically re-enabled on exit of the NMI ISR. (Contrast this with maskable interrupts, where both flags are cleared, and you need an explicit EI to re-enable them).

The HALT instruction (executes NOPs until an interrupt is requested or the CPU is reset) was also completely incorrectly (and bizarrely) implemented. The rewrite just sets a Halted property, which prevents the CPU from fetching or executing any instructions. The interrupt-triggering code simply resets this property.

This has fixed numerous bugs (I'm not sure when they were introduced, as it was all working a while back). It's gone from "not working at all" to "just about working", but some games or demos that rely on precise interrupt timing don't work properly.


Game Gear Hicolor Demo and Desert Speedtrap
Both problems in the above screenshots relate to line-based interrupts from the VDP (Video Display Processor). Some other games simply hang at startup. [sad]

benryves

benryves

 

Loading ROMs

OutRun Europa
I've been adding a series of new features to Cogwheel. I wrote a basic zip file class a while back (only supporting store and deflate compression - the majority of zip files use deflate), and have added a Utility namespace with methods for help with loading ROMs.

For example, there is a method that is passed a filename by reference, and it'll return an array of bytes of the loaded file. If the passed filename was a zip file, by any chance, it'll search for ROMs inside the zip, and modify the filename (so it might end up as C:\Path\SomeFile.zip\Game.sms).

Once this is done, the CRC-32 checksum of the file is calculated then looked up against a database of known ROM dumps. I'm currently using the SMS Checker .romdata files as the database source. These contain information about known bad dumps, and can be used to strip headers and footers, patch bytes and remove redundant overdump data to end up with a valid ROM image.

Finally, the mapper is detected. Currently, I have three mappers - RAM (which is just 64KB of RAM), Standard (the standard Sega Master System ROM mapper) and Codemasters. I use the RAM mapper for all SG-1000 and SC-3000 games (I'm not sure what they're meant to use, here, but some games - such as The Castle - don't work with the standard SMS mapper).

The Codemasters mapper uses a different method to swap pages, which means that the conventional SMS BIOS checksumming code fails to swap in pages correctly and thus doesn't check the entire cartridge. The games therefore have their own checksumming routines and store their own checksum in a Codemasters-specific header; this makes detecting Codemasters games fairly easy, as all you need to do is check for the extra Codemasters checksum.

As the BIOS doesn't check the checksum, the cartridge provides code to do so itself. You can run it by holding 1 and 2 down as the game boots.


The Excellent Dizzy Collection


Micro Machines

I seem to have broken interrupts somewhere along the line; most noticable are non-maskable interrupts (as generated by the Pause button) which crash the emulator (it appears that once they fire, interrupts are never re-enabled). I'm not sure what's causing this, but hopefully it won't be too unpleasant to fix!

benryves

benryves

 

Game Gear LCD Scaling

The Game Gear's hardware is very similar indeed to the Master System's - so similar that you can play Master System games on a Game Gear via a special adapter. Some Game Gear games were just the Master System ROMs in a Game Gear cartridge. [smile]

That said, the Game Gear's LCD is only 160x144, and the Master System has a resolution of 256x192 (or 256x224 or 256x240, but those modes were very rarely used). In Game Gear mode, this resolution is simply cropped. In Master System mode something more interesting has to be done to scale this down to fit the LCD.

I won't bore you with the details here, but will refer you to a post here on the subject. Using the research, I added a mode to the VDP emulator that would mimic this scaling.





This is old news, of course. I have nothing especially new to report, but I have pretty much entirely rewritten the emulator now (apart from the Z80 emulator, which was fairly well designed). The code was absolutely horrible, and so I've redesigned it to be a lot more flexible and intuitive to use as a library. I've rewritten the standard memory mapper, I/O mapping, and VDP (though I did copy the rasterisation and timing stuff from the old VDP code and cleaned it a little); there's a lot more to do (so far I haven't even reintroduced joypad input) but at least it'll be nicer for me to work with. [smile]

In the meantime, here are some ugly screenshots of what Master System games look like on the Game Gear LCD.








benryves

benryves

 

Necromancy

After seeing Scet and Drilian's work on their respective emulator projects I decided I needed to do something with the stagnating Cogwheel source on my hard disk drive.

The only ROM I have tested where I can't find an explanation for a bug is the Game Gear Garfield: Caught in the Act game. Like many games, when left at the title screen it'll run a demo loop of the game in action. At one point Garfield would walk to the left of the screen, jump over a totem pole, shunt it to the right and use it as a way to jump out of a pit. However, in Cogwheel he would not jump far enough to the left, and not clearing the totem pole he'd just walk back to the right and not have anything to jump out of the pit on.

I remembered a post on MaxCoderz discussing a long-standing tradition of thinking that when a JP , failed the instruction took a single clock cycle. You can see this misreported here, for example. This document, on the other hand, claims it always takes 10 clock cycles - and most importantly of all, the official user manual backs this up.



So, Garfield can now get out of his pit. The user interface has changed (again) - I'm now using SlimDX to dump pixels to a Panel, which seems to be the least hassle distribution-wise and doesn't throw LoaderLockExceptions.

benryves

benryves

 

PlayStation Controllers

PlayStation controllers are relatively comfortable, and I have a number of them knocking about.


From top to bottom - PS2 IR remote control and receiver; Guitar Hero wireless Kramer and receiver; black PS2 DualShock 2 analogue joypad; a pair of grey standard PS1 digital joypads.

As I've learned in the past, a decent gamepad can help with certain games. Of course, what's much more fun than playing the games is trying to work out how these controllers work.

The byte-level protocol is very simple; the PlayStation pulls a select line low (used to grab the attention of the controller) then pulses the clock eight times, writing a bit at a time onto one line and reading another bit at a time from another. This means that the controller and PlayStation end up sending and receiving a byte simultaneously. Finally, the PlayStation checks to see if controller pulls the acknowledge line low to indicate that it received the data; if no acknowledgement is received it assumes that there is no controller on the port it is currently accessing.

All electrical connections are unidirectional, and so a controller can be easily connected to a standard PC's parallel port. There are a number of diagrams floating around the internet using similar pin connections, so I followed one of those.



I cut up a pound-shop parallel cable for the PC end and a controller extension cable for the PlayStation end. PlayStation controllers require power; a lot of diagrams I've seen refer to a 9V and 5V supply, some 7.6V and 3.3V. A voltmeter informs me that it's the latter option. Rather than try and draw power from the parallel port, I'm using a generic power supply set to 7.5V. To derive the 3.3V I'm using a 5V regulator followed by two 1A rectifier diodes in series - the diodes provide a voltage drop of 0.7V across each, resulting 3.6V.

I wrote an application in C# that attempted to swap bytes back and forth between the PC and the controller, and was getting good results. I was not, however, having any luck polling the acknowledgement line. It didn't appear to ever go low - my guess was that the program simply couldn't poll the parallel port rapidly enough. Not that this is a slur on C#, of course, but to access the parallel port in the first place I need to use an unmanaged library.

The solution was therefore to write an unmanaged library myself that would handle the PlayStation protocol side of things, which I could then wrap up and add nice functionality to via a C# managed library.

#include "Windows.h"

// inpout32.dll function declarations.
short Inp32(short portAddress);
void Out32(short portAddress, short data);

/// Gets the state of the data line.
/// Base address of the parallel port.
/// The state of the data line.
extern "C" __declspec(dllexport) bool GetData(short portAddress) {
return (Inp32(portAddress + 1) & (1 6)) != 0;
}

/// Gets the state of the acknowledge line.
/// Base address of the parallel port.
/// The state of the data line.
extern "C" __declspec(dllexport) bool GetAcknowledge(short portAddress) {
return (Inp32(portAddress + 1) & (1 5)) != 0;
}

/// Sets the state of the command line.
/// Base address of the parallel port.
/// The state to set the command line to.
extern "C" __declspec(dllexport) void SetCommand(short portAddress, bool state) {
Out32(portAddress, (Inp32(portAddress) & ~0x01) | (state ? 0x01 : 0x00));
}

/// Sets the state of the select line.
/// Base address of the parallel port.
/// The state to set the select line to.
extern "C" __declspec(dllexport) void SetSelect(short portAddress, bool state) {
Out32(portAddress, (Inp32(portAddress) & ~0x02) | (state ? 0x02 : 0x00));
}

/// Sets the state of the clock line.
/// Base address of the parallel port.
/// The state to set the clock line to.
extern "C" __declspec(dllexport) void SetClock(short portAddress, bool state) {
Out32(portAddress, (Inp32(portAddress) & ~0x04) | (state ? 0x04 : 0x00));
}

/// Begins a data transfer by pulling select low.
/// Base address of the parallel port.
extern "C" __declspec(dllexport) void BeginTransfer(short portAddress) {
SetSelect(portAddress, false);
}

/// Ends a data transfer by releasing select high.
/// Base address of the parallel port.
extern "C" __declspec(dllexport) void EndTransfer(short portAddress) {
SetSelect(portAddress, true);
}


/// Exchanges a byte between the PlayStation controller and the PC.
/// Base address of the parallel port.
/// The data to exchange.
/// True if the transmission was acknowledged, false if it timed out.
extern "C" __declspec(dllexport) bool ExchangeByte(short portAddress, unsigned char* data) {

DWORD TimeoutStart = GetTickCount();

for (int i = 0; i 8; ++i) {
SetClock(portAddress, false);
SetCommand(portAddress, (*data & (1 0);
SetClock(portAddress, true);
if (GetData(portAddress)) {
*data |= (1 } else {
*data &= ~(1 }
}

while (GetAcknowledge(portAddress)) {
if ((GetTickCount() - TimeoutStart) > 10) return false;
}

while (!GetAcknowledge(portAddress)) {
if ((GetTickCount() - TimeoutStart) > 10) return false;
}

return true;

}

/// Exchanges a block of data between the PlayStation controller and the PC.
/// Base address of the parallel port.
/// The command byte to send.
/// The data to exchange (input and output).
/// The size of data in bytes.
/// The size of the received packet.
extern "C" __declspec(dllexport) int SendPacket(short portAddress, unsigned char* command, unsigned char* data, int numberOfElements) {

// Start by sending 0x01.
unsigned char ToExchange = 0x01;
if (!ExchangeByte(portAddress, &ToExchange)) return 0;

// Next send the command byte.
// Controller will respond with packet size and mode.
if (!ExchangeByte(portAddress, command)) return 0;

// Check for end-of-header.
ToExchange = 0x00;
if (!ExchangeByte(portAddress, &ToExchange) || ToExchange != 0x5A) return 0;

// Fix the "numberOfElements" to only try and fetch the number of bytes that are in the packet.
numberOfElements = min(numberOfElements, (*command & 0xF) * 2);

for (int i = 0; i if (!ExchangeByte(portAddress, &data)) return i + 1;
}

return numberOfElements;
}

I'm not much of a C++ programmer, so I hope the above isn't too offensive.

Polling a standard digital joypad or a dual analogue is pretty straightforwards - send 0x42 to the device, and it'll return the status of each button as a bitfield 2 bytes in length. If the controller is in analogue mode, it'll then go on to return a further four bytes; one byte per axis, two axes per joystick.


Download library and demo.

Standard disclaimer applies; if you're going to hook anything up to your PC, I cannot be held responsible for any damages incurred. Be careful!

I'm still having some problems with data transfer. The controller doesn't always send back enough data (least reliable with a DualShock 2); this could be because I'm running its clock too fast. Introducing delays doesn't seem to help. This is most noticable in the demo program when a DualShock 2 is used in analogue mode; the analogue light flickers on and off.

I also haven't successfully managed to get the DualShock 2 to enter escape mode - this mode is used to access some of the more exotic commands, including commands to control the force feedback motors or to append extra data to the packet sent back when the controller is polled, such as the status of the analogue buttons.

benryves

benryves

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!