Content Reader Type

Trying to understand how GetRuntimeType() and GetRuntimeReader() are really used led me down a rabbit hole. If I understand correctly, it looks like content readers are chosen in 2 different ways:

  1. In the majority of cases, the reader is chosen by GetRuntimeReader().
  2. However in some cases it’s chosen by GetRuntimeType().

So I guess the question is, why? Why can’t we always select the reader based on the target type? Or, contrarily, why can’t the ArrayWriter write the inner reader type as the ArrayReader’s generic parameter instead of the target data type?

Having both of these methods seems a little redundant to me.

1 Like

I worked on my own XNB reader a while ago. And what I remember of it is that the XNB format starts with a list of readers that will be called later based on an index.

Value types like Vectors and Integers are written directly. And reference types are encoded as a reader index. This makes polymorphism possible.

If you’re interested in my implementation

Thanks for the response! I’ll check out your implementation, but I already spent awhile looking at the source code. What I’m really looking for is some insight into these questions about why they’re sometimes chosen by index and sometimes (in the case of generic readers) chosen by target data types.

We’ve been having a discussion on discord, so I’ll paste some of the important bits here.

Current Behavior

  • If you write a MyObj, then MyWriter tells it to read with MyReader.
  • If you write a MyObj[], then ArrayWriter tells it to read with a ArrayReader<MyObj>, which is mapped MyReader.

Question

Why do we have these two cases that require both GetRuntimeType() and GetRuntimeReader()?

Solutions?

(changes from current behavior are bolded)
  1. Why can’t you always resolve based on target data type? (always use GetRuntimeType())
    • If you write a MyObj, then MyWriter tells it to read a MyObj, which is mapped to MyReader.
    • If you write a MyObj[], then ArrayWriter tells it to read with an ArrayReader<MyObj>, which is mapped to MyReader.

or alternatively

  1. Why can’t you always resolve based on reader type? (always use GetRuntimeReader())
    • If you write a MyObj, then MyWriter tells it to read with MyReader.
    • If you write a MyObj[], then ArrayWriter tells it to read with an ArrayReader<MyReader>.

I have a new theory.

Regarding #2
I think it can’t always write GenericReader`1[[MyReader]] because there are some generic readers that don’t actually hand off to MyReader. For example EnumReader reads the enum value on its own, but still needs to know which type its reading.

Regarding #1
I think it could always write the data type and map it to the reader type, rather than writing the reader type directly. But if it doesn’t write the reader type, then the only way to get them is iterate over all types and look for an attribute or a base class, which is slow. If it writes the reader types to a list, then it can simply load all of those types into the map.

It still feels redundant and like there should be a simpler unified way though.

nkast had a great point about how #2 would require lots of reflection to work:

(in my own words in the way that made sense to me) It’s very easy for a ListReader<T> to create a List<T>. But if the ListReader instead had TReader as its generic type param, then it would be much harder for a ListReader<TReader> to get T through reflection and dynamically construct a List<T>.

And I still think the primary issue (at least in the original XNA design) with #1 is perf. I’m not sure how much it would slow down, but if you can ever increase your game size by a few extra bytes to avoid using reflection, even just once at startup, then it seems worth it. nkast is saying you could search assemblies in parallel and it might be very fast, but he’s concerned about being able to find and generate all the right reader types in an assembly.


Conclusion

So I guess we’re stuck with both GetRuntimeType() and GetRuntimeReader() then. But they don’t have to be ugly hardcoded strings. If your pipeline extensions project (with the writers) references your shared library (with the readers), you can do the following:

        public override string GetRuntimeType(TargetPlatform targetPlatform) => typeof(SampleData).AssemblyQualifiedName!;
        public override string GetRuntimeReader(TargetPlatform targetPlatform) => typeof(SampleDataReader).AssemblyQualifiedName!;

And if this is still too much boiler plate for you, you could also use some reflection in the pipeline extensions project to search the assembly for any ContentTypeReader<> of the type that your writer uses and get that data type and reader type dynamically. (Note that slow reflection isn’t a concern in the pipeline extension, because that only affects how long the game takes to build, not how long it takes to start up.)

Hopefully this was as informative for you as it was for me :smiley:

1 Like