Jade Virtual File System (VFS)

The 13 September 2006 I posted in this blog:

"Nothing more I think, probably the next post will be about the new virtual file system of Jade."

And then the nexts posts were not about the VFS :p They were more reports about how the project is going. I'll do a quickly one and move on: 1.1 is really close, most features are there (collada is there, animation is there, new input is there, scripting is there, VFS is there,...). But polishing takes a lot of time :( But you can download the latest check-ins and play with all the new things ;)

And now about the VFS. Back to when I started thinking about this subject (severals months ago), I asked in Stratos if someone had already done it (just to avoid common mistakes and to get some guidelines). I got several very useful answers, and a link to a very good article in old Flipcode (here). I also got several good answers from Jade forums.

Most of my objectives for the VFS were pretty similar to those of the article:

- it should be able to handle archives (a big file with lots of files inside, like a .zip file)
- it should allow plugins to read or write in archives (encryption, compression,...)
- accessing normal files and archives should be transparent to the user
- it should allow several file paths

The idea was to write the new VFS and replace the old class of the engine that managed that (JPath). I had a more or less idea of what JPath was doing and I though switching from one thing to the other wouldn't be very hard (BIG mistake, more on this later) .

So, with that in mind, I started to code. I took the decision to leave the plugins for the end (not a great idea, but not too bad either) because I wanted to have first a bare minimum functionality. The main code was written pretty fast (it was quite easy), and were I spent most time was writing the tool to create the "archives" (a small winforms application). I was lucky that Fernando (delahermosa) was not too busy during that time and he helped me a lot in several parts of the tool.

The structure for the VFS at the start was like this:

- there was a JVFS class that represented the file system. The engine or the user would ask for files to this class. This class would hold internally a dictionary of FileSources (places where you can find files) and pass the request to them. One of the features from JPath that the engine used a lot was that you could ask for a file without knowing where it is exactly and then JPath would search for it.

- I wrote to FileSources implementations: HardDiskSources and StorageSources. HardDiskSources is normal IO, and StorageSources are the so called archives. A StorageSource holds virtual directories and files inside it.

- The engine used to read files the .NET methods called FromFile(string path,...), but now it would have to use the FromStream(Stream stream,...), because there“s not a real file when reading from an archive file. To allow this a new type of stream class is created: a VirtualStream. A virtual stream works over a file stream (the stream for the archive file) and controls that the reading is done the right way (you read the file you want to, and not more or less than you should). This way, the archive is only open once really and close at the end (so you avoid lot of costly IO operations).

And that was it more or less. It sounded great, but when I started merging the VFS with the engine, things weren't so great ;)

First, I decided to do the plugins support. And well, it was hell to write that. Plugins are small classes that modify how a file is written or read, and you can apply several of them to a file. This is their final interface:

Stream ApplyForRead(Stream input);
Stream ApplyForWrite(Stream input);

In theory it is pretty easy: most times filters will be streams wrapping streams. In practice it was quite complicated. Why? Things go more or less like this: we are writing our new archive (remember, lots of files together like a .zip), so we have one stream open and a binary writer writing on it. But now, when writing files with filters applied, we have to modify the stream the writer uses to write (filters modify the stream). So:

- we cache the position where we are in the stream
- we close our stream
- we reopen it and apply the filters to write
- we write the file
- we get the new position in the stream. Now, we can calculate the number of bytes we wrote :) We can't use the Length property of a file stream to know the number of bytes we wrote because we can be reading a compressed file or writing a compressed file, and compressed streams don't support the Length property (System.IO.Compression).
- we close the stream
- we reopen it again to continue doing work with it (file header, and other things)

Sure you loved it :p But then things get better. After finishing that part (I can tell you it took me a while to write those 40 lines of code), now I needed to give support for the filters in the visual tool. It involved writting a custom attribute so filters can tell the type that edits them in a visual environment (like VS visual designers) and a lot of strange errors with reflection and assembly loading.

And the last headache I got was when testing the encryption filters, because for some strange reason, when using AES, DES or TDES symmetric cyphers with PaddingMode.ISO10126 the output ended truncated. It took me a while to realize it was the algorithm padding, so well, I set it to PaddingMode.Zeros and at last, filters were working :)

And things got worse :p

Now I started replacing JPath for JVFS. I deprecated all the JPath code and started searching the warnings around to replace it. I had to change also some configuration stuff, but nothing too serious.

After looking how JPath was working I found the harsh reality: my JVFS interface wasn't prepared to manage it. JPath allowed the user to get all files from a directory, write a file (I didn't allow to write files! You can't save your game, you have to finish it at once :p), set special directories (like for example, Shaders directories where all shaders would be), ignore extensions,... Well, a lot of stuff I haven't thought. So I had to stop the merge and rework the VFS.

This rework involved mostly the FileSources interface. Before this change, the VFS had methods to ask for a file to a specific FileSource (all of them have an unique name) or to all of them at the same time. But now, the VFS class was reworked to be also a FileSource. So, I took out the methods to ask for a specific file or to a generic file. Now the method was something like this:

public abstract Stream GetFile(string path, string fileName, bool recurse, System.IO.FileAccess access);

So, if you do VFS.GetFile, the VFS implementation would search in all its FileSources, but if you do VFS["FileSourceName"].GetFile, it will be use whatever that specific FileSource implementation is. Everything is much clearer like that. FileSources also got some new methods (all files from one directory and minor things needed).

Next step was to allow for defined directories. The old way of putting directories for Jade was something like this:

X:\Whatever\InHouse
X:\Whatever\Base

InHouse is for internal engine files. Base is for your game files. But the engine assumes some directories always exist. For example, it loads all shaders in the Shaders defined directory, that is that the engine searches for InHouse\Shaders and Base\Shaders. So I had to allow a way to get all files from a specific path from all the FileSources. And the defined directories where born.

A defined directory is a pair of key (name, for example Shaders) and value (path, for example MyWork\Shaders). What the engine does when it finds a request to a defined directory is to ask every FileSource if the defined directory key exists, and if it does exist, it performs a normal search using the path associated to that key. So every user can put the Shaders wherever he likes the most as long as the key is Shaders.

All of this is defined in the configuration file, like this:

[VFS]
[FilesSource Type="HardDisk" Name="Base" Path="../../../../Base"]
[DefinedPath Name="Materials" Path="Materials"/]
[DefinedPath Name="Models" Path="Models"/]
[DefinedPath Name="Particles" Path="Particles "/]
[DefinedPath Name="Scenes" Path="Scenes"/]
[DefinedPath Name="Textures" Path="Textures"/]
[/FilesSource]
[FilesSource Type="HardDisk" Name="InHouse" Path="../../../../InHouse"]
[DefinedPath Name="Effects" Path="Effects"/]
[DefinedPath Name="PostProcess" Path="Effects/PostProcess"/]
[DefinedPath Name="Shaders" Path="Shaders"/]
[DefinedPath Name="Geometry" Path="Shaders/Geometry"/]
[DefinedPath Name="Internal" Path="Shaders/Internal"/]
[DefinedPath Name="Lighting" Path="Shaders/Lighting"/]
[DefinedPath Name="Materials" Path="Materials"/]
[DefinedPath Name="Textures" Path="Textures"/]
[DefinedPath Name="Gizmos" Path="Textures/Gizmos"/]
[/FilesSource]
[/VFS]

(Note: it's a normal XML File, I just replaced it with brackets [] to avoid some formatting problems here in the blog)

Quite easy: you define a FilesSource, with its type (HardDisk is normal files, Storage is an archive), its name and its path (relative to the configuration.xml file or absolute, as you wish). And then inside it you can add defined paths with their name and their real path.

With everything done, I went back to replace JPath for the VFS again: now things were fitting much better as there weren't strange things around that I hadn't thought. But, when I finished, the engine didn't work at all :( It would load and start, but it would not render correctly, probably related to a shaders loading problem, but I was unable to find it, so in the end I had to go back and undo all the changes in the engine (I backed up everything first). See changesets 14554, 14572 and 14574 comments to see what I mean (I broke the build compilation on the first check-in).

And so, there I'm at the moment, trying (for the 3rd time) to replace JPath for JVFS, but this time, instead of changing all at the same time, I'm going to have both systems living together and then I'll remove JPath little by little.

Let's hope this time it works ;)

Edit: I forgot to say (and I shouldn't have) that all of this work was possible thanks to the support, advice and comments from Jader, Reed, Quimbo and delahermosa. They helped me a lot to catch bugs, write some parts of the code, test, ideas,... Thks team! ;)

Comments

No Comments

Leave a Comment

(required) 
(required) 
(optional)
(required) 

Enter the numbers above: