Googles appar
Huvudmeny

Post a Comment On: cbloom rants

"02-16-13 - The Reflection Visitor Pattern"

13 Comments -

1 – 13 of 13
Blogger PeterM said...

Hello,

Ron Pieket (Insomniac) covered a few game data loading approaches with respect to versioning amongst other things in his GDC 2012 talk "Developing Imperfect Software", and came up with/described what I think is a pretty good solution. It's probably worth a watch if you haven't already:

http://www.itshouldjustworktm.com/?p=652

Cheers,
Peter

February 17, 2013 at 1:38 PM

Blogger Fabian 'ryg' Giesen said...

That task focuses on serialization, and unless I misunderstood what he's saying, what he describes is basically the serialization system that RAD's Granny has been shipping since 2002 or so. :)

February 17, 2013 at 2:04 PM

Blogger cbloom said...

Yeah, I just had a quick scan (god damn don't put information in video form. text text text) and it certainly sounds just like Granny.

The granny-style struct markup system is okay. I think the main advantage of it is that you can find the values that need to be endian-fixed and do an efficient pass of endian fixing and then just point at the struct blocks.

As for the Insomniac argument, I don't really see the win. To be clear, the alternative to the Insomniac way is :

You have some loose "source" data files (certainly not XML, wtf, but some kind of simple always-readable format).

You build fast-load binary variants from the source data. If the binary is up to date you "load and go" it, otherwise you load the source and parse it and generate a new binary (so that your next load will be fast).

The Insomniac way adds the ability to load out of date binaries without going all the way back to a source parse.

That sounds nice, but it also adds a whole extra pathway that you have to maintain, and you can introduce weird heisen-bugs due to the old-binary loading in a weird way (that you don't get when you bake new binaries from source data).

I dunno, maybe I could be convinced, but it certainly doesn't seem like a compelling win for so much added complexity.

February 17, 2013 at 2:10 PM

Blogger Unknown said...

There was a proposed boost library (that sadly didn't make it very far) that was auto implementing this pattern, including taking care of inheritance. It's called boost.reflect (it did many other things as well). It can be found here https://github.com/bytemaster/boost_reflect.

All you had to do was call a macro in the form:

BOOST_REFLECT(Class, (base1)(base2)(...), (member1)(member2)(...))

And it would generate the visitor for the class. You could call it this way:

boost::reflect::visitor::visit( myVisitor ) ;


I used it in a full size project (~60k lines) and it worked like a charm. I guessed it did impact compilation times but I have never bothered measuring. I even wrote a small pre compiler to auto generate the macros in the simple cases to make sure I wouldn't forget adding new members and relieving the pain of having to write all those macros.

My favorite usage was to dumps my objects to and from CSV files, and I had a lot of success with a visitor that converted endianness.


A last thing worth noting is that the C++ standard committee has appointed a study group on reflection (SG7). I can't wait to see what will come out of it.

February 17, 2013 at 5:28 PM

Blogger NeARAZ said...

This is very much how Unity's "serialization" system works (we call them "Transfer" instead of reflect, but same idea). And indeed that is used for both saving/loading and automatically building UI for editing objects (the UI recognizes some built-in types, e.g. a color member will automatically get a color picker in the UI etc.).

Couple points:

"1. Everything in headers" - yeah that's annoying. We kind of work around it by routing through virtual calls, which kind of sucks but we accept the hit.

So we have several macros, in header you do DECLARE_OBJECT_SERIALIZE() which does virtual void VirtualRedirectTransfer(BinaryRead&); virtual void VirtualRedirectTransfer(BinaryWrite&); etc.

And then in the .cpp file you do IMPLEMENT_OBJECT_SERIALIZE which does implementation of each of these functions that calls into the template one. And that single template one is defined in .cpp file, plus the macro explicitly instantiates the template with the transfer functors.


"3. Versioning" - what we have is, we have a "safe binary read" serializer as well as "binary read" one. The "safe" one can handle missing or added fields, and is always used while developing (in the editor). So if you just want to add a new field to a class, you just add it and call "transfer" on it. The other reader function is much simpler, can't handle any missing/added fields and is used when reading final baked game data.

In addition to that, the classes can also have a version number, for things where you really change something significantly.

February 18, 2013 at 12:29 AM

Blogger NeARAZ said...

...oh, and exactly same system is also used for things like "gather dependencies" - e.g. when building a game data file, we only include the assets that are actually referenced by something. That's just one more "transfer functor" that adds any "persistent pointer" members to the set of objects.

February 18, 2013 at 12:33 AM

Blogger MH said...

Oh hey. I realized you could add an additional parameter to the macro that represent metadata.

So, you could have:
REFLECT( , AttVersion(3).AttNetwork() );

This allows lots of cool things that I had thought of as weaknesses of the C++ approach.

February 18, 2013 at 2:25 AM

Blogger MH said...

Blogger ate part of my comment

REFLECT( {thing}, AttVersion( 3 ), AttNetwork() );

February 18, 2013 at 2:26 AM

Blogger cbloom said...

@MH - yeah, I always see that discussed as an advantage of the macro-markup way (that you can add various bits of extra metadata), but of course they can just be more args on the reflection visitor that are just ignored when they don't apply.

February 18, 2013 at 9:31 AM

Blogger cbloom said...

@Nearaz - good to know, that's how I imagined it would be used.

It sounds like you don't even have a "flat load" path, that you either load with slow/checked transfer or with fast/unchecked transfer?

For posterity, in case this wasn't clear :

In the main post I was assuming that IO through Reflection was "tagged"; that is you write something like [var name][var value] , so that rearranging variables and such doesn't break the file format. (of course var name can be a 4-byte hash or whatever, you don't have to do slow text IO).

But you can also do faster untagged/unchecked IO when you know the binary matches the code perfectly, just read/write the data without checking tags.

February 18, 2013 at 9:34 AM

Blogger Per Vognsen said...

"And yet almost nobody uses it."

Except every game that has used any version of the Unreal Engine? They use it for everything from serialization to garbage collection.

February 18, 2013 at 11:19 AM

Blogger NeARAZ said...

@cbloom: oh no, for checked vs. non-checked we do it differently.

In the checked/slow part, a data file also includes the "type tree" - kind of RTTI info for each type of object that is serialized in the file (basically name & type pairs). Then the data itself is just values (advantage: names&types only stored once per class, instead of in each data instance).

In the unchecked/fast part (used when you know that code that built the data is exactly the same version as code that's reading the data), this "type tree" information does not exist, and the data is just data. Reading the data, transfer(&myint,"myint") just ignores the name and fetches 4 bytes at the current read offset.

All the above is used for object-like data that has references to other objects etc. Large blob-like data (texture pixels, mesh data, audio etc.) go through another system that operates on just blobs of bytes.

February 19, 2013 at 10:54 AM

Blogger Mark Lee said...

"The Insomniac way adds the ability to load out of date binaries without going all the way back to a source parse."

That's huge though. The promise of forward and backward compatibility between different versions of data and executables takes on more value as teams grow big and many people are in the code, potentially tweaking file formats. I'm not sure if insomniac implemented this system in the end, but the promise of not having to wait around for new builds of levels just because you sync'ed code does sound very appealing in theory. Granted, in practice may be another matter.

@NeARAZ: The description of the unity system is starting to sound quite granny like - development only schemas which are dumped for final builds.

February 20, 2013 at 10:15 PM

You can use some HTML tags, such as <b>, <i>, <a>

This blog does not allow anonymous comments.

Comment moderation has been enabled. All comments must be approved by the blog author.

You will be asked to sign in after submitting your comment.