December 23, 2011

"Another World" Code Review

I spent two weeks reading and reverse engineering further the source code of Another World ("Out Of This World" in North America). I based my work on Gregory Montoir's "binary to C++" initial reverse engineering from the DOS executable.

I was amazed to discover an elegant system based on a virtual machine interpreting bytecode in realtime and generating fullscreen vectorial cinematic in order to produce one of the best game of all time.

All this shipping on a 1.44MB floppy disk and running within 600KB of RAM: Not bad for 1991 ! As usual I cleaned up my notes, it may save a few hours to someone.


But...What source code ?!

The source code of "Another World' was never officially released nor leaked. Some people were so passionate about this groundbreaking game that they reverse engineered the DOS executable.

This was possible partly because the binary was small (20KB). Why so small ? Because ANOTHER.EXE was not the game itself but just a virtual machine:

The bytecode performs all the game logic with its own opcodes but uses syscalls for "heavy" stuff like drawing, playing music, sound and managing assets.

To implement only the virtual machine for the target OS reduced the effort and the game was broadly ported to more than a dozen platforms:

Every time only the virtual machine had to be compiled to the target OS: The bytecode remained the same !


Architecture

The executable is 20KB. It can be summarized as:



We can see four modules:

Trivia : The palette memory segment actually contains several palettes, used for nice fading effects.

Upon startup, the executable sets the virtual machine's thread 0 program counter with 0x00 and start interpreting. Everything is commanded by the bytecode after that.


Rendition explained

In the previous drawing we see three framebuffers. Two because Another World implements Double Buffering" in software....and a third one as a clever optimization:

The third framebuffer is used to compose the background of a scene only once and then reuse it frame after frame with a simple memcpy:



In this video the legendary first level screen of Another World has been slowed down so we can actually see things being drawn. Everything is drawn with polygons and pixigons. Overdraw is very substantial but since this is only generated once it is not so bad.

Trivia : This famous background is made of 981 polygons.

In order to visualize the big picture I have slowed down and rendered the three framebuffer + what is seen on screen:


We can see very clearly:



If you want to analyze more : Full video.


Another World Virtual Machine

Eric Chahi's webpage explains a lot about how the machine is structured.

In the code on github you can see how every opcode have been implemented. All of them are pretty easy to understand except for the renditions ones. The trick is that the polygon segment source where the vertices should be read is embedded with the opcode id.

Finally a few screenshots from the vm bytecode editor (Called "script editor" by Eric Chahi):




You can see how the label have been lost: setvec 21 nag1 sets the thread 21 instruction counter at "nag1" label offset. In the bytecode we can only see a hardcoded offset.



Opcode cases


In the following drawings we can see the virtual machine calling an opcode that is actually a system call to the resource manager in order to load the four memory segments. This happens typically at the beginning of a game part (The entire game is made of 10 game parts):



In the next drawing the opcode is also a systemcall to the renderer asking to draw and fetch vertices. The rendition opcode are a bit more complex because they contains where to read the vertices from. To set the target framebuffer is an independent opcode altogether:


Note: Whether the render should read vertices from the cinematic polygon segment or the animation segment is encoded with the opcodeId.


Resource Management

Resources are identified by an unique integer id. Upon startup the resource manager opens MEMLIST.BIN and get records as follow:



           
            typedef struct memEntry_s
            {

            	int bankId;
            	int offset;
            	int size;
            	int unpackedSize;

            } memEntry_t;



			

When the vm requests a resourceId, the resource manager:


A few stats about the compression:

			
			
  Total # resources: 146
  Compressed       : 120
  Uncompressed     :  28
  Note: 82% of resources are compressed.


  Total size (uncompressed) : 1820901 bytes.
  Total size (compressed)   : 1236519 bytes.
  Note: Overall compression gain is : 32%.


  Total RT_SOUND          unpacked size:  699868 (38% of total unpacked size) packedSize  585052 (47% of floppy space) gain:(16%)
  Total RT_MUSIC          unpacked size:   33344 ( 2% of total unpacked size) packedSize    3540 ( 0% of floppy space) gain:(89%)
  Total RT_POLY_ANIM      unpacked size:  384000 (21% of total unpacked size) packedSize  106676 ( 9% of floppy space) gain:(72%)
  Total RT_PALETTE        unpacked size:   18432 ( 1% of total unpacked size) packedSize   11032 ( 1% of floppy space) gain:(40%)
  Total RT_BYTECODE       unpacked size:  203546 (11% of total unpacked size) packedSize  135948 (11% of floppy space) gain:(33%)
  Total RT_POLY_CINEMATIC unpacked size:  365960 (20% of total unpacked size) packedSize  291008 (24% of floppy space) gain:(20%)
  Note: Damn you sound compression rate!

  Total bank files:              148
  Total RT_SOUND          files: 103
  Total RT_MUSIC          files:   3
  Total RT_POLY_ANIM      files:  12
  Total RT_PALETTE        files:   9
  Total RT_BYTECODE       files:   9
  Total RT_POLY_CINEMATIC files:   9


			

I did not spent time reverse engineering the compression algorithm...the fact that sound doesn't compress very well leads me to believe it is entropy sensitive...so maybe a variation of huffman ?


Out of 146 resources: 120 are compressed:


Trivia : The introduction (resource 0x1C), 3 minutes long weights only 57,510 bytes once compressed.


Memory Management

Like all games from the 90s no memory is allocated during gameplay. Upon startup the game engine grabs 600KB of memory ( anybody remember DOS 640KB conventional memory here ?). Those 600KB are used as a stack allocator:


Free memory: The memory manager has the capability to unallocate one step back OR free the entire memory. In practice the entire memory is freed at the end of each 10 game parts.

Trivia : Originally the entire 600KB was storing bytecode and vertices. But after two years of generating the backgrounds with polygons/pixigons the game was still far from being done. In order to speed up the development speed Eric Chahi decided to integrate a hack in his beautiful architecture (at a performance cost): The resource manager can load background bitmap from the floppy disk to the background buffer (void copyToBackgroundBuffer(const uint8 *src);). Hence 32KB (320x200/2) are reserved at the end of the conventional memory.

Trivia : This hack was exploited for the release of Another World for Windows XP in 2005. All background were hand drawn and loaded directly from hard-drive without using the renderer and its pixigons:



Purist corner

If you are a purist and really want to play the original way, Another World works like a charm in DosBOX:



Or you can run the windows XP version. I recommend to get the Collector's edition since it feature a lot of additional informations, among them the techical notes from Eric Chahi:



One more thing

I worked on the code a lot, making it simpler to understand. You can see an example of how much clearer it is now.

Before:



   void Logic::runScripts() {                                                                                                
      for (int i = 0; i < 0x40; ++i) {                                                                                  
        if (_scriptPaused[0][i] == 0) {                                                                           
	     uint16 n = _scriptSlotsPos[0][i];                                                                 
	     if (n != 0xFFFF) {                                                                                
	         _scriptPtr.pc = _res->_segCode + n;                                                       
	         _stackPtr = 0;                                                                            
	         _scriptHalted = false;                                                                    
	         debug(DBG_LOGIC, "Logic::runScripts() i=0x%02X n=0x%02X *p=0x%02X", i, n, *_scriptPtr.pc);
	         executeScript();                                                                          
	         _scriptSlotsPos[0][i] = _scriptPtr.pc - _res->_segCode;                                   
	         debug(DBG_LOGIC, "Logic::runScripts() i=0x%02X pos=0x%X", i, _scriptSlotsPos[0][i]);      
	         if (_stub->_pi.quit) {                                                                    
	            break;                                                                                					
	         }                                                                                       
	     }                                                                                                 					
	    }                                                                                                         					                                                                                                             
	  }                                                                                                                 						                                                                                                              
	}                                                                                                                         			
			
			   

After:



  void VirtualMachine::hostFrame() {                                                                       
                                                                                                         
	// Run the Virtual Machine for every active threads (one vm frame).                                     
	// Inactive threads are marked with a thread instruction pointer set to 0xFFFF (VM_INACTIVE_THREAD).    
	// A thread must feature a break opcode so the interpreter can move to the next thread.                 
                                                                                                         
	for (int threadId = 0; threadId < VM_NUM_THREADS; threadId++) {                                         
                                                                                                         
		if (!vmIsChannelActive[CURR_STATE][threadId])                                                           
			continue;                                                                                             
		                                                                                                       
		uint16 pcOffset = threadsData[PC_OFFSET][threadId];                                                    
                                                                                                         
		if (pcOffset != VM_INACTIVE_THREAD) {                                                                  
                                                                                                         
			// Set the script pointer to the right location.                                                      
			// script pc is used in executeThread in order                                                        
			// to get the next opcode.                                                                            
			_scriptPtr.pc = res->segBytecode + pcOffset;                                                          
			_stackPtr = 0;                                                                                        
                                                                                                         
			gotoNextThread = false;                                                                               
			debug(DBG_VM, "VirtualMachine::hostFrame() i=0x%02X n=0x%02X *p=0x%02X", threadId, n, *_scriptPtr.pc);
			executeThread();                                                                                      
                                                                                                         
			//Since .pc is going to be modified by this next loop iteration, we need to save it.                  
			threadsData[PC_OFFSET][threadId] = _scriptPtr.pc - res->segBytecode;                                  
                                                                                                         
			debug(DBG_VM, "VirtualMachine::hostFrame() i=0x%02X pos=0x%X", threadId, threadsData[0][threadId]);
			   
			if (sys->input.quit) {                                                                                
				break;                                                                                               
			}                                                                                                     
		}                                                                                                      
		                                                                                                       
	}                                                                                                       
  }    
  
                                                                                                      


I used:


Here is the "human readable" source code :) ! Happy hacking.


Edit (video presentation)

Jeff Somers submitted a link to a fantastic video from GDC Vault in which Eric Chahi talks about the Genesis of Another World. Thanks a lot Jeff :) !


Add a comment



Name Homepage
E-mail
(Will not appear online)
Comment



Comments (35)


#1 - stef666 - 12/23/2011 - 04:30
Amazing work... Thanks !!
#2 - Maarten - 12/23/2011 - 05:52
Ah, nostalgia.

This brings back some old pain. I never finished that game... and got very frustrated being eaten all the time.
#3 - no apple user - 12/23/2011 - 07:11
can't see - apple format video - heard about youtube ?
#4 - Diego Guidi - 12/23/2011 - 08:02
simply, thanks
#5 - gbatcisco - 12/23/2011 - 08:46
Really cool
#6 - Fabien Sanglard - 12/23/2011 - 09:02
@"no apple user" I used youtube a few times. The great thing with Quicktime videos is that you can move frame by frame with the left and right arrow key. That is absolutely fantastic and far superior to a youtube video when you want to analyze things in details.
#7 - Jeff Somers - 12/23/2011 - 09:19
Eric Chahi's talk on the making of Another World was the highlight of GDC for me. He has amazing passion, but it sounds like making the game damn near killed him.

http://www.gdcvault.com/play/1014630/Classic-Game-Postmortem-OUT-OF
#8 - xyproto - 12/23/2011 - 09:24
Thanks for a great read! The explanations and illustrations are suitable, and it must have taken you some time and effort to create.

Is there a typo in the title in anotherWorldArchitecture.png, or is it supposed to be "ANOTHER WORD"?

Thanks for the reformatted source code too, looking forward to reading through it.
#9 - Fabien Sanglard - 12/23/2011 - 10:05
@Jeff: Wow Wow Wow Wow, GDC link is pure gold !!! I updated the webpage and credited your contribution. Thanks a lot !
#10 - Gil Megidish - 12/23/2011 - 10:11
Hey

Awesome post! I actually reverse-engineered the 3DO version as well as the PC/HIRES version. I made a few ports of it all, most important was the AWJS (http://www.megidish.net/awjs/) which also appeared in Google Chrome Experiments.

I have been obsessed with the games since day one, and I've played every port it ever made. I was once working on such paper myself, to explain how the graphics were made (parallelograms and actual sprites on the 3DO). I never finished it.

As a side note, I also reverse-engineered and released a perfectly working Heart of The Alien port. (HOTA Redux) at http://hota.sourceforge.net . HOTA is based in the same world, but its engine is far far more complicated (over 100 opcodes and special animation file format.) Unfortunately it has only been released on the Sega CD, -- fortunately, I rewrote it and GPL'd it ;)

Awesome post!
#11 - Pablo - 12/23/2011 - 10:38
I used to play this game. I didn't have any idea of how sophisticated it was. Simply amazing.
#12 - Meir - 12/23/2011 - 11:12
Amazing !
#13 - Terry A. Davis - 12/23/2011 - 12:29
Cool! Port it to LoseThos!
#14 - Fabien Sanglard - 12/23/2011 - 13:10
@Gil Megidish

Haha, I did browse your website while I was gathering informations: I am honoured you liked this article, I was very impressed with Another World javascript: I found the source code very clear and easy to read (you start from newraw if I am correct ?).

I'll try to get Heart of The Alien code and compile it on Windows and Mac :) !

Fab
#15 - Gil Megidish - 12/23/2011 - 14:08
@Fabien

Actually, Gregory and I worked on it at the same time. I was about to release my 3DO remake code when he released his PC remake. Something that was awesome with the 3DO version is that Burger Bill Heineman left the debug information in the production binary. A few years later (2006) I flew to France for a sushi dinner with Eric (awesome!) and asked him about some of the function names :)

3DO version was different than the PC version. It used 18 colors (16 colors plus two extra bits), used parallelograms and some of the graphics were actually sprites. That was to speed up rendering, and only consisted of running Lester.

The way the graphics are constructed is incredible. Elements and array of elements. The tasks (vectors) is also interesting, every element on screen had its own task, and one task could control another. The 3DO version had a secret screen http://www.flickr.com/photos/gawd0r/4542324599/ and a secret game, a shmup left by Burger Bill. You could access these via the password screen. Aside of that, the 3DO had 16bit graphics, much better than the HIRES version in my opinion, and 44khz audio tracks superior to the HIRES version. All in all, it's my favorite version.
#16 - haqu - 12/24/2011 - 21:00
Yeah, I feel the hacker's spirit here. Awesome research Fabien. Thanks a lot!
#17 - Raimu - 12/25/2011 - 07:36
Awesome work done on a magnificent game. :) I wonder, does the original release of the game ever use that bitmap-loading background backdoor?
#18 - Fabien Sanglard - 12/25/2011 - 08:46
@Raimu:

Q: "does the original release of the game ever use that bitmap-loading background backdoor?".
A: Yes it does. 6-7 screens in Another World on Amiga/Atari are using it.
#19 - gabe - 12/26/2011 - 01:01
@noappleuser

stop giving credit to that awful company. they have no say in H.264/AVC more then they control html.
#20 - Ulrich VACHON - 12/26/2011 - 05:19
Un beau cadeau de noël :)

thx
#21 - mmoroca - 12/27/2011 - 03:13
I loved every implementation of this game and I was sure it has to be that special! Good job, it is an amazing reading, you make my day! ;-)
#22 - EOL - 12/27/2011 - 05:36
A small correction: this game was also released for Mac OS! I remember seeing it on a Mac (maybe back in 1987?); Mac OS is also listed on the Wikipedia page.
#23 - marcus - 12/27/2011 - 12:49
The game wasn't actually ported to the Amiga; it was ported to all the other platforms. Another World was originally developed for the Amiga.
#24 - LuigiBlood - 12/27/2011 - 12:56
I swear... You got me interesting in modding the game. But i didn't get the compression of the DOS version.
Oh well, i still changed the MEMLIST.BIN, and replaced the DOS Intro files with the Sega CD intro files. It worked.
And also: I didn't know Another World DOS had an unused song, when i found it, i made this page: http://tcrf.net/Out_of_This_World_(PC)
#25 - aaa111 - 12/27/2011 - 15:54
Great Review.I like your site.
#26 - BeFr4ctal - 12/27/2011 - 17:52
Wonderful job, Fabien!
You might be interrested in the "Agence tous geeks" podcast. Eric Chai is invited in Episode #4. (I assume you are french speaking?)

-- Twitter: BeFr4ctal --
#27 - JonT - 12/29/2011 - 18:00
This is a heroic effort! (And another "thanks" to Jeff for the GDC video link! fascinating!)
#28 - Bruce - 01/14/2012 - 05:53
Wow - another great review... always a pleasure!
#29 - Dawid - 03/01/2012 - 18:07
This is the best game ever made! looking at the screens beings back memories, I played it on Amiga 1200 and was never able to finish I got up to the caves with water. I have bought the new release for PC as I love it so much and finished it couple times now :)

Good work here Fabien!
#30 - Java Programming - 10/03/2012 - 04:33
This article is one of the gem. only one word to describe it , Fantastic
#31 - Xeno - 11/07/2012 - 02:20
I salute you man. This is really dedication and hardwork and also eric chahi is an obsessive genius who make the game alone. thanks for the effort.
#32 - Joeled - 01/11/2013 - 14:05
Now its back on AmigaOS4 :-) http://os4depot.net/?function=showfile&file=game/action/anotherworld.lha

Thanks!
#33 - carlmartus - 02/18/2013 - 08:11
Amazing analysis.

I had no idea they would build their game in this way.

Keep up these code reviews! They are gold!
#34 - Francisco - 02/19/2013 - 09:33
Great job! I can't wait to get myself some to time to go around it!

Cheers,
FB
#35 - Crayoz - 02/27/2013 - 20:42
Awesome, this was one of the first I ever played. Great!. I am able to compile using VS 2010 and it runs great from console, but when I try to debug I cannot. Is there any way to do this? Resource::setupPart(uint16_t partId) is trying to load a partId that doesn't exist it seems like the threads are messed up. I was wondering if debugging is possible. Thanks!

 

@2011