CONTENTS

Home
Updates
Software
Electronics
Music
Resume
Contact


YouTube
Twitter
GitHub
LinkedIn


Playstation 2 Java

Make Believe In Java

Posted: August 6, 2018

Introduction

Here's a project to answer the question: Is it possible to write a Java API for Playstation 2 and write a graphics demo with it. I don't want to spoil it or anything, but the answer is yes.

A few years ago I started this Java Grinder project that takes javac compiled Java .class files and basically acts as a disassembler. But instead of disassembling into Java assembly code it disassembles into assembly source code for real CPUs. If that class file needs other class files, those are read in and processed also. Any API method calls are written to the output as either inline assembly or calls to prewritten functions to take care of what they are supposed to do.

Since Java Grinder was written in a C++, object oriented, abstracted, polymorphic, or any other buzzword that hiring managers like to hear way, extending it mostly required creating a Playstation2 class that extended a new R5900 class that extended the main Generator class.

This ended up being a bigger project than I thought it would. The system itself is actually pretty simple, but there was still a lot to learn and not a lot of easy places to find good information. This is actually the first time I've done any real 3D programming. I posted some information on another page about things that I learned (along with some deeper info here) on my Playstation 2 Programming page.

Below are videos and a detailed explanation of the development process.

Videos

I recorded the demo on a PS2 slim with the audio / video cables plugged into a DVD recorder. I was a little worried the PS2 would have some macrovision protection that would have scrambled the video but it either didn't have that enabled or the DVD recorder ignored it. The video starts by showing the actual Playstation 2 that ran the demo hooked up to a composite to VGA converter connected to an LCD display proving it's running on a real machine. I then cut to the actual video that was recorded directly from the PS2 into the DVD recorder.

YouTube: https://youtu.be/AjM069oKUGs

Related Projects @mikekohn.net

Java Grinder: Playstation 2 Java, Nintendo 64 Java, Sega Genesis Java, Amiga Java, Apple IIgs Java, TI99/4A Java, C64 Java, dsPIC Mandelbrots, Atari 2600 Java, Intellivision Java, chipKIT Java, Java Grinder, naken_asm

The Demo

Looking back at the Sega Genesis Java demo, I kind of regretted not making the demo more interesting. I was more just trying to show off the possibilities of the Java API. When I started this project, I decided I was going to do something bigger. Unfortunately, I once again got so burnt out working on learning the system and creating the API, that I didn't make such a big demo.

The demo consists of the following subroutines:

  • 3 Billion Devices Logo: This is the low resolution 3 billion devices run Java logo that Joe Davisson made for his Commodore 64 Java demo.
  • Logos: I drew these with a Sharpie and scanned them in (except the Java logo).
  • Stars: I actually copied the code from the Sega Genesis Java demo and modified it to work with the Playstation 2 API. The text here is also drawn with a Sharpie and scanned in.
  • Mandelbrots: This is demonstrating using vector unit 0 for calculating Mandelbrots while vector unit 1 is doing 3D calculations. The MIPS is controlling what both vector units are doing.
  • Cubes: I drew these cubes with Wings3d and wrote some C code to convert STL files into arrays that Java Grinder can use. I added the colors by hand.
  • SquareRing: Just trying to draw a lot of stuff on the screen moving around. I probably should have put more things until the system slowed down.

The Music

I wrote and recorded 3 songs for the demo, and only ended up using 2. The first song was actually a song I wrote for another project I posted on my website (the coin acceptor project) about a year ago... actually it was originally just the wah-wah part. After I finished that project I thought it might be fun to try a guitar solo over it, and after recording something really quick I just imagined it playing while the demo's stars were flying. Had to wait a few months to use it. The guitar for this song is a Fender Strat I scalloped.

The second song I recorded just yesterday. The guitar solo sounds kind of .. drunk.. because it's being played on a guitar that I turned into a fretless guitar. I'm really not good at playing that thing and the higher notes mute out really fast, but the slides sound pretty cool. The rhythm part is being played with my Yngwie wanna-be kit (cheapo scalloped Squier Strat, DOD YJM308 overdrive, and a 9v battery driven Mini-Marshall).

I programmed the drums for both songs using a program I wrote a long time ago called Drums++. It takes text files written in a language I came up with and turns them into .mid files that I imported into Apple's Garage Band and could then record the bass and guitar tracks. The fretless.dpp and shoebox.dpp source files are in the assets directory of the demo's repository.

The music is played by the SPU2 of the Playstation 2 leaving the R5900 chip free to do other work. Due to the lack of good documentation, I almost finished the demo without music at all. More on that below.

Here are the two songs in MP3 format:

Development

This project was done over a pretty long period time. I started out adding R5900 Emotion Engine instructions to the MIPS assembler in naken_asm and then floating point instructions, and macro / micro mode vector unit instructions. Taking large breaks in between to work on other projects, I learned all the other things needed for this demo and started adding support for them in Java Grinder. If anyone is interested in the low level details, I put up a page to try to document what I learned: Playstation 2 programming.

I did most of the programming by running the code on the PCXS2 emulator, which was pretty nice since I could examine registers and such on the screen. Definitely not as flexable or easy to use as MAME was for doing Sega Genesis development, for example, MAME made it easier to examine memory, both RAM and video / sound registers, to confirm that Java Grinder was doing the correct thing.

One mistake I made with the Sega code was I didn't test on the machine until after the demo was written. There were at least 3 quirks in the Sega code that the emulator ignored but the real machine barfed on. This time after getting pieces of code working, I tested on the real machine to make sure when the demo is done, it just works on both the real hardware and emulated. Again there have been things that worked in the emulator that didn't work on the real PS2. I also found something that worked on the real Playstation 2, but did not work correctly in the emulator.

API Features

  • Vector Unit 0 has methods to upload/run code and upload/download data.
  • Vector Unit 1 does 3D rotations and projections.
  • Textures using 16 or 24 bit formats (with black transparency).
  • Textures using 16 bit can be RLE encoded.
  • Code to draw points, lines, triangles with and without textures.
  • Fogging and gouraud shading.
  • Methods to access random number generator.
  • Use of 2 contexts (page flipping)
  • Insert larger binaries into compiled assembly code.
  • Play music.

The API

Most of the API is defined in the Playstation2 class. I was originally going to give a lot of freedom for setting up video modes and such but then thought it might be better just to hide all the complexities of that. It basically just sets up a 640x448 interlaced display, Just like the other Java Grinder projects, I pretty much just added methods / features as I needed them.

There is another set of classes that I gave a cheesy, boring name of Draw3D. These basically defined all the different types of primitives that the Graphics Synthesizer can draw along with 16, 24, and 32 bit texture support. I debated adding 8 bit textures, but decided to leave that out for now. Draw3D takes care of 3D rotations, projection, hardware DMA transfers, textures, etc. I'll probably get asked why I didn't base this off OpenGL, I've actually never done OpenGL before. I did a little ps2dev programming a long time ago, nothing big and I barely remember it, so again this is kind of the first time I've done something significant in 3D.

There are examples of using all these things, not only in the demo itself, but also in the samples directory.

Most of the complexity is hidden in the API. The developer doesn't have to worry about cache flushing, 3D computations, etc. The trade off is losing some flexibility. So if a texture is modified by the CPU but only the first 64 pixels are changed, really just a single 64 byte cache line needs to be flushed, but Java Grinder will flush the whole image. It does mark objects so they only get flushed if they need to, but it will flush the whole chunk of memory. Most likely though if 64 bytes changed, the whole image did too.

Vector Unit 0 (VU0)

VU0 is free for the Java Grinder user to use. In the demo I used it in the "Two Vector Units, One MIPS" part of the demo to draw Mandelbrots. The code could be optimized better, for example most of the floating point vector unit instructions have a throughput of 1 and a latency of 4, which I believe would mean that if a register is in the destination of an instruction it can execute in 1 cycle but it takes 4 cycles for the result to be available. If that register is used, the vector unit will stall until it's ready. So I think the idea is if I can space out the instructions I can execute every instruction with 1 cycle without stalling. When I did Playstation 3 Mandelbrots last year I optimized that code by computing 8 pixels (2 SIMD registers) at one time and saw a significant increase in speed. In this case I wanted the code to be easier to read so I didn't bother to optimize it.

VU0 only has 4k of data memory which can't hold the entire Mandelbrot image, so the MIPS will send it coordinates for just 1/8 of the image at a time.

A quirk I ran across while working with VU0 is that I was originally starting the VU0 code using the vcallms instruction and using VIF0_STAT to check if it's done executing. VIF0_STAT didn't seem to work unless I started VU0 with a VIF packet. That fixed it in the emulator, but it was still broken on real hardware. It ended up that vcallms and using a cfc2 on register 29 worked on both.

One thing that I felt was kind of missing from the vector unit instruction sets is a parallel compare instruction that is found in the Intel x86_64, Playstation 3, and even in the Emotion Engine's MIPS R5900 instruction set. Mandelbrots are supposed to keep iterating a formula until the result passes 4.0 so on other instruction sets I could just run a parallel compare which creates a mask of either all binary 1's or 0's. The mask can be used to stop the color counters from incrementing. For Playstation 2 I came up with a really goofy formula that seems to accurately estimate it... which I kind of documented in the mandelbrot_vu0.asm source code in some commented out Python.

One thing I thought was cool about Playstation 2 vector units that I haven't seen other SIMD instruction sets is all the FPU instructions can have a .xyzw attribute which tells the instruction which of the 4 floating point numbers are actually affected. So if I only wanted the result of an instruction to affect the "x" part of the vector, I would just add a .x to the end of the instruction. The other neat thing is it's a VLIW instruction set, so every tick executes two instructions at once. Actually, module felt a lot more like a DSP than a general purpose processor.

Vector Unit 1 (VU1)

VU1 is reserved by Java Grinder for doing 3D rotations, translations, and projections. Draw3D objects are sent to VU1 using the draw() method which uses vector unit assembly to transform the points and kick them to the Graphics Synthesizer. The assembly code in VU1 could be optimized a lot better for speed, but it works for what I need so I'd rather leave the code easier to read (unoptimized).

To learn the formulas for the projections and rotations, I found the formation on Wikipedia: projections and rotations.

The 3D transformation code is also in the naken_asm repository as a plain .asm file: rotation_vu1.asm.

MIPS R5900

I wasn't so fond of the MIPS instruction set until working on this project. It's actually a quite easy CPU to deal with. The Emotion Engine version of this CPU has some pretty neat features. The most noticable is the registers are 128 bits wide, although this is really just used for load / store and SIMD. So really it's 128 bit registers, 64 bit ALU, and 32 bit pointers.

There are code optimzations I could have put in the main MIPS code too that I either left out for code readability or time. For example, the MIPS CPU will stall for a cycle if the destination register of an instruction is used directly after it was set. I could have done a better job dealing with that.

Java Hacks

Java Grinder itself has some.. quirks and some things just missing, kind of mostly due to the fact that I was originally targetting the MSP430 as more of an experimental thing. One thing that was missing was the ability to do memory allocation of objects. I added the ability to instantiate some objects on Playstation 2, mostly with the Draw3D API. I didn't write any kind of memory allocator or garbage collector, so all calls to "new" are being done on the stack. I thought about doing some kind of heap memory allocator thing, but decided in the end to just keep it simple. I also added more "float" support for Playstation 2 (there was already some support in the Epiphany / Parallella code). Some other things such as "long" and "double" types are still not supported.

Probably the nastiest thing I did was... well... there is an awkward limitation in Java class files. A Java method can't be bigger than.. I think 64k if I remember right. Which is probably okay, but the problem is if there is a static array in a class file, it doesn't get dumped into the class file as binary data. It gets put in the class file as Java assembly instructions in a static initializer to build the array. I was trying to save images in class files as static byte[] arrays, but some of them didn't fit, so in Java Grinder's Memory class file I added a method: byte[] Memory.preloadByteArray(String filename);

This won't load that file at runtime, it loads it at assembly time using naken_asm's .binfile directive. The images copied into the output binary at assembly time.

With all that said, I really hope James Gosling never finds this project :(.

Images

The Draw3D API has the ability to use 16, 24, and 32 bit textures. Pixels in the texture can be set pixel by pixel or by loading them with byte[] arrays. I also added the ability to RLE compress the images using the format { length, lo16, hi16 } where lo16 and hi16 are the are 16 bit color in little endian format and is copied into the texture "length" times.

Tools

While working on Sega I used Google Go to make tools for creating images and music and such just to learn a new language. This time I tried out Rust. The first tool converts binary files into Java source code and the second converts BMP's into a binary format that can be loaded into textures, including the RLE format. I ended up writing them in Python also in case anyone wanted to join me in making the demo.

Sound

After learning how the graphics and vector units worked, the last step was to get sound working. I figured this would be the easiest part, especially after looking at Sony's SPU2 PDF. Wow was I wrong. This part of the system is so undocumented.

So the first thing I figured out is the SPU2 (sound processing unit) is connected to the IOP (the I/O processor.. aka a Playstation 1). The Playstation 2 CPU is connected to the IOP through something called the SIF. Sony's PDF's only mention the SIF DMA, but nothing on how to use it.

I ended up giving up on the SIF, but decided to add a linker to naken_asm so I could use functions from kernel.a from the PS2DEV SDK. I got the linker working, but that didn't work.

At this point I actually decided I just wasn't going to get sound to work and decided to just finish the demo without it. But it was bugging me so I decided to look into the source code of some different Playstation 2 emulators to try to figure out how the SIF works. I finally ended up figuring out how to DMA some MIPS R3000 code to the IOP and run it (there's an example in the samples directory of naken_asm). I got sound working in the emulator.

In the end I found out that the IOP's memory (including the SPU2) was mapped into EE space so with quite a lot of work (since that was extremely undocumented.. none of the emulators got it completely correct, but for their use-case it doesn't matter) I got it working.

Getting sound working was *not* fun.

Emulator vs. Hardware

I found some differences between running on the real machine and running in the emulator.

  • If a GIF packet sets the PRIM register with both the IIP (shading method) and FIX bits set to 1, the emulator will honor the IIP bit and do gouraud shading while the real hardware will do flat shading.
  • If a GIF packet is sent via PATH3 (EE direct to GS) and the EOP (end of packet) flag is not set, then if the VU1 tries to send a GIF packet via PATH 1 (VU1 to GS) it will cause a deadlock in the real hardware but work in the emulator.
  • Omitting cache CPU cache flushing before a DMA transfer isn't needed on the emulator but causes odd things on the real machine.
  • When mapping the SPU2 to EE space, sound data could just be written to the SPU2 FIFO with the emulator. With the real Playstation 2 after writing 32 half-words a register has to be written to to tell the FIFO to flush. Also in the real hardware the transfer mode must be 0 when setting the transfer / start address of the SPU2. The emulators didn't care if the mode was 0.
  • Writing to the IOP memory mapped registers from the EE crashes on a real machine unless it's in kernel mode. The emulator let it work without caring what mode the CPU is in.
  • Using the SIF DMA channels worked on the emulator, but I never got it working on real hardware. I got memory access violations on the SIF DMA registers, even in kernel mode.
  • The emulator is too slow to run the demo with vector unit VU0 computing Mandelbrots, so the sound gets out of sync.

Conclusion

I had been wanting to write some program for the Playstation 2 pretty much since I got it. I actually got the PS2 Linux kit long ago (I think that was the reason I even got a Playstation 2) and I also did try the PS2DEV C library, but it's kind of a much different experience than actually programming in assembly language directly for the hardware.

Have to say thanks to Lukasz for saving old PS2 assembly source code and documents. I'm not sure I could have even gotten started without Duke's 3 Star demo as a guide to initializing the hardware. And also to the PCSX2 emulator.. made debugging a lot faster, plus when I couldn't figure out sound I could look at the source code to get some clues to what's wrong.

And thank you to Sony for this beautiful little computer. If anyone from Sony actually reads this, how about shrinking it down to the size of a Rapsberry Pi and selling it as a hobbyist board? :).

Building The Demo

git clone https://github.com/mikeakohn/playstation2_demo.git git clone https://github.com/mikeakohn/java_grinder.git git clone https://github.com/mikeakohn/naken_asm.git cd naken_asm ./configure make cd .. cd java_grinder make java make cd .. cd playstation2_demo make

Copyright 1997-2024 - Michael Kohn