Skip to content

IntroToClassAccessors

David Jewsbury edited this page Aug 19, 2015 · 7 revisions

Intro to XLE classes: Class Accessors!

Let's look at some of the reusable classes in XLE. Any good engine needs a lot of core reusable C++ classes. Engines are more than just rendering technologies -- half of the work is moving and organizing data!

This a quick introduction to the "Class Accessors" system.

Class Accessors for serialization in XLE.

Many rendering technologies in XLE have a "settings" or "config" structure. These define the adjustable properties for things like lights, environment settings, water settings (etc). These objects need 2 critical types of "serialization" operations:

  • the editor must be able to change the value of an individual property referenced by string name
  • for example, the editor makes changes like: set "Radius" to 5.f
  • we also need to be able to serialize and deserialize the entire object
  • ideally we want to have a text-based serialization path for most objects
  • and a binary-based serialization path for heavy objects

Now, these serialization operations are similar, but also different. Typical C++ serialization patterns can't handle both cases easily.

We want a single pattern that can handle all of the serialization paths. In higher level languages, we would use a reflection system to find the contents of an object. So, we will follow the same principle -- but we will do it in an interesting way.

Defining class accessors

The class accessors system allows us to define "accessor" functions for classes (which are basically just get and set functions). We do this by creating a GetAccessors specialization:

template<> const ClassAccessors& GetAccessors<ShadowSettings>()
{
    static ClassAccessors props(typeid(ShadowSettings).hash_code());
    static bool init = false;
    if (!init) {
        using Obj = ShadowSettings;
        props.Add(u("FocusDistance"),           DefaultGet(Obj, _focusDistance),            DefaultSet(Obj, _focusDistance));
        props.Add(u("WorldSpaceResolveBias"),   DefaultGet(Obj, _worldSpaceResolveBias),    DefaultSet(Obj, _worldSpaceResolveBias));
        props.Add(u("MinBlurSearch"),           DefaultGet(Obj, _minBlurSearch),            DefaultSet(Obj, _minBlurSearch));
        props.Add(u("MaxBlurSearch"),           DefaultGet(Obj, _maxBlurSearch),            DefaultSet(Obj, _maxBlurSearch));

        props.Add(u("FrustumCount"),
            DefaultGet(Obj, _frustumCount),
            [](Obj& obj, unsigned value) { obj._frustumCount = Clamp(value, 1u, SceneEngine::MaxShadowTexturesPerLight); });

        props.Add(u("TextureSize"),
            DefaultGet(Obj, _textureSize),
            [](Obj& obj, unsigned value) { obj._textureSize = 1<<(IntegerLog2(value-1)+1); });  // ceil to a power of two

        props.Add(u("BlurAngleDegrees"),
            [](const Obj& obj)          { return Rad2Deg(XlATan(obj._tanBlurAngle)); },
            [](Obj& obj, float value)   { obj._tanBlurAngle = XlTan(Deg2Rad(value)); } );

        init = true;
    }
    return props;
}

"Accessors" are similar to "properties" in C#. They are get and set functions that are designed for serialization and data binding operations.

Using the above class accessors function, we can now write:

ShadowSettings DeserializeShadowSettings(
    InputStreamFormatter<utf8>& formatter)
{
    ShadowSettings result;
    AccessorDeserialize(formatter, *this);
    return result;
}

Here, AccessorDeserialize is a general function that works for any object with a GetAccessors implementation.

But the interesting thing is; we can do with this a minimum of macros and template code. There are many other similar systems written for C++; but they all tend to have the same drawbacks:

  • they involve complex templates and strange macros scattered about
  • or they require learning a new script language, with some preprocessing steps before compile
  • or they require a lot of special case changes within the class definition
  • or they require storing member as non-standard data types

The advantage of the class accessors system, is we're focusing on just attaching get/set functions to string names. This gets the functionality we need, but avoids the big disadvantages

  • there's limited templates and macro code
  • no changes to class definition
  • no special preprocessing scripts or anything weird
  • GetAccessors is implemented in the .cpp file (not the .h file)
  • everything is type-safe!

Accessor definition

Most accessors simply use the default get/set implementations:

props.Add(
    u("FocusDistance"),
    DefaultGet(Obj, _focusDistance),
    DefaultSet(Obj, _focusDistance));

DefaultGet and DefaultSet use a pointer-to-member-variable, and will just get and set the variable in a type-safe way.

Some accessors need a special definition:

props.Add(
    u("BlurAngleDegrees"),
    [](const Obj& obj)          { return Rad2Deg(XlATan(obj._tanBlurAngle)); },
    [](Obj& obj, float value)   { obj._tanBlurAngle = XlTan(Deg2Rad(value)); } );

This is an example of a property that does a some basic math on get & set. So, this property will be serialized out as an angle in degrees (but stored in memory as the tangent of that angle).

For read-only properties, use nullptr for the set accessor.

Related functions

We can use AccessorDeserialize and AccessorSerialize for any type that has a GetAccessors implementation. Here is an example for AccessorSerialize:

void TerrainMaterialConfig::Write(OutputStreamFormatter& formatter) const
{
    AccessorSerialize(formatter, *this);
}

Note that the system deserializes from text. So the correct parsing and conversation operations are automatically performed.

We can use CreateFromParameters to create an object from a ParameterBox. (A ParameterBox is a set of variants with string names).

ParameterBox box({
    std::make_pair(u("FocusDistance"), ".5f"),
    std::make_pair(u("BlurAngleDegrees"), "12.f")});
auto settings = CreateFromParameters<ShadowSettings>(box);

We can query a value of a specific type:

ShadowSettings settings;
float focusDistance;
bool success = GetAccessors<ShadowSettings>().TryGet(
    focusDistance, settings, Hash64(u("FocusDistance")));
if (success)
    ...

We can set safely:

ShadowSettings& settings = ...;
float newFocusDistance = 100.f;
bool success = GetAccessors<ShadowSettings>().TryCastFrom(
    settings, Hash64(u("FocusDistance")),
    &newFocusDistance, ImpliedTyping::TypeOf<decltype(newFocusDistance)>());

Complex accessors

There is also support for arrays and "child lists". These work in similar ways to XML.

template<> const ClassAccessors& GetAccessors<Material>()
{
    static ClassAccessors props(typeid(Material).hash_code());
    static bool init = false;
    if (!init) {
        using Obj = Material;
        props.Add(
            u("Texture"),
            DefaultGetArray(Obj, _texture),
            DefaultSetArray(Obj, _texture),
            dimof(std::declval<Obj>()._texture));
        props.AddChildList<Obj::ProcTextureSetting>(
            u("ProcTextureSetting"),
            DefaultCreate(Obj, _procTextures),
            DefaultGetCount(Obj, _procTextures),
            DefaultGetChildByIndex(Obj, _procTextures),
            DefaultGetChildByKey(Obj, _procTextures));

        init = true;
    }
    return props;
}

More details for another day.

Future improvements

In addition to serialization, class accessors also provide a perfect interface for scripting. Currently XLE has some integration with both lua and IronPython for scripting tasks. These can be expanded to interface with class accessors easily.

In particular, by tieing in accessors to the .net DLR, we can allow IronPython (or any .net language) to use class accessors as if they were just .net properties. Given the similarity in concepts, this should be quite robust.

Another possible extension would be to add "annotations" to accessors. Annotations are used in .net to enable or disable serialization, and to help with data binding (for example, to a DataGrid). Some similar behavior could be added using the same pattern.

Performance

Deserialization performance is significantly slower than a hand-written deserialization method. This is ok for many purposes (and fine for graphics configuration objects in the editor). But for objects that are very large, or are deserialized frequently, it is recommended to write custom deserialization methods (potentially using the BlockSerializer or InputStreamFormatter depending on the type of object).

Implementation

Class accessors make use of several other reusable XLE class.

Type Link Usage
VariantFunctions Docs This is a collection of functions with variant signatures, accessible by string name & type safe. We can store any type of function or functor object within a VariantFunctions. They work like normal C++ functions, except that the caller references the function they want with a string name (ie, determined at run-time, not link-time).
ImpliedTyping Parsing and conversion functions for value types.
ParameterBox Docs A set of variants with string names (ie, VariantFunctions is for procedures as ParameterBox is for data)
InputStreamFormatter and XmlInputStreamFormatter Docs Reads strongly structured text based serialized data
OutputStreamFormatter Docs Writing equivalent of InputStreamFormatter

Click the links to see more information from doxygen.

Example

Here is a full example of a complex class using class accessors for serialization.

(Header file)

#pragma once

#include "../Assets/Assets.h"
#include "../Assets/AssetUtils.h"
#include "../Math/Vector.h"
#include "../Core/Types.h"
#include "../Utility/UTFUtils.h"
#include <vector>

namespace Utility
{
    template<typename CharType> class InputStreamFormatter;
    class OutputStreamFormatter;
}

namespace SceneEngine
{
    class TerrainMaterialConfig
    {
    public:
        class StrataMaterial
        {
        public:
            class Strata
            {
            public:
                ::Assets::rstring _texture[3];
                float _mappingConstant[3];
                float _endHeight;

                Strata();
            };
            std::vector<Strata> _strata;
            unsigned _id;

            StrataMaterial();
        };

        class GradFlagMaterial
        {
        public:
            ::Assets::rstring _texture[5];
            float _mappingConstant[5];
            unsigned _id;

            GradFlagMaterial();
        };

        class ProcTextureSetting
        {
        public:
            ::Assets::rstring _name;
            ::Assets::rstring _texture[2];
            float _hgrid, _gain;

            ProcTextureSetting();
        };

        UInt2 _diffuseDims;
        UInt2 _normalDims;
        UInt2 _paramDims;
        std::vector<StrataMaterial>     _strataMaterials;
        std::vector<GradFlagMaterial>   _gradFlagMaterials;
        std::vector<ProcTextureSetting> _procTextures;

        ::Assets::DirectorySearchRules  _searchRules;

        void Write(Utility::OutputStreamFormatter& formatter) const;

        TerrainMaterialConfig();
        TerrainMaterialConfig(
            InputStreamFormatter<utf8>& formatter,
            const ::Assets::DirectorySearchRules& searchRules);
        ~TerrainMaterialConfig();

            // the following constructor is intended for performance comparisons only
        TerrainMaterialConfig(
            InputStreamFormatter<utf8>& formatter,
            const ::Assets::DirectorySearchRules& searchRules,
            bool);
    };
}

(cpp file)

#include "TerrainMaterial.h"
#include "../Utility/Meta/ClassAccessorsImpl.h"
#include "../Utility/Meta/AccessorSerialize.h"

template<> const ClassAccessors& GetAccessors<SceneEngine::TerrainMaterialConfig>()
{
    using Obj = SceneEngine::TerrainMaterialConfig;
    static ClassAccessors props(typeid(Obj).hash_code());
    static bool init = false;
    if (!init) {
        props.Add(u("DiffuseDims"), DefaultGet(Obj, _diffuseDims),  DefaultSet(Obj, _diffuseDims));
        props.Add(u("NormalDims"),  DefaultGet(Obj, _normalDims),   DefaultSet(Obj, _normalDims));
        props.Add(u("ParamDims"),   DefaultGet(Obj, _paramDims),    DefaultSet(Obj, _paramDims));

        props.AddChildList<Obj::GradFlagMaterial>(
            u("GradFlagMaterial"),
            DefaultCreate(Obj, _gradFlagMaterials),
            DefaultGetCount(Obj, _gradFlagMaterials),
            DefaultGetChildByIndex(Obj, _gradFlagMaterials),
            DefaultGetChildByKey(Obj, _gradFlagMaterials));

        props.AddChildList<Obj::ProcTextureSetting>(
            u("ProcTextureSetting"),
            DefaultCreate(Obj, _procTextures),
            DefaultGetCount(Obj, _procTextures),
            DefaultGetChildByIndex(Obj, _procTextures),
            DefaultGetChildByKey(Obj, _procTextures));

        init = true;
    }
    return props;
}

template<> const ClassAccessors& GetAccessors<SceneEngine::TerrainMaterialConfig::GradFlagMaterial>()
{
    using Obj = SceneEngine::TerrainMaterialConfig::GradFlagMaterial;
    static ClassAccessors props(typeid(Obj).hash_code());
    static bool init = false;
    if (!init) {
        props.Add(u("MaterialId"),  DefaultGet(Obj, _id),   DefaultSet(Obj, _id));
        props.Add(
            u("Texture"),
            DefaultGetArray(Obj, _texture),
            DefaultSetArray(Obj, _texture),
            dimof(std::declval<Obj>()._texture));
        props.Add(
            u("Mapping"),
            DefaultGetArray(Obj, _mappingConstant),
            DefaultSetArray(Obj, _mappingConstant),
            dimof(std::declval<Obj>()._texture));
        props.Add(
            u("Key"),
            [](const Obj& mat) { return mat._id; },
            nullptr);

        init = true;
    }
    return props;
}

template<> const ClassAccessors& GetAccessors<SceneEngine::TerrainMaterialConfig::ProcTextureSetting>()
{
    using Obj = SceneEngine::TerrainMaterialConfig::ProcTextureSetting;
    static ClassAccessors props(typeid(Obj).hash_code());
    static bool init = false;
    if (!init) {
        props.Add(u("Name"), DefaultGet(Obj, _name), DefaultSet(Obj, _name));
        props.Add(
            u("Texture"),
            DefaultGetArray(Obj, _texture),
            DefaultSetArray(Obj, _texture),
            dimof(std::declval<Obj>()._texture));
        props.Add(u("HGrid"), DefaultGet(Obj, _hgrid), DefaultSet(Obj, _hgrid));
        props.Add(u("Gain"), DefaultGet(Obj, _gain), DefaultSet(Obj, _gain));
        init = true;
    }
    return props;
}

namespace SceneEngine
{

///////////////////////////////////////////////////////////////////////////////////////////////////

    void TerrainMaterialConfig::Write(OutputStreamFormatter& formatter) const
    {
        AccessorSerialize(formatter, *this);
    }

    TerrainMaterialConfig::TerrainMaterialConfig()
    {
        _diffuseDims = _normalDims = _paramDims = UInt2(32, 32);
    }

    TerrainMaterialConfig::TerrainMaterialConfig(
        InputStreamFormatter<utf8>& formatter,
        const ::Assets::DirectorySearchRules& searchRules)
    : TerrainMaterialConfig()
    {
        AccessorDeserialize(formatter, *this);
    }

    TerrainMaterialConfig::~TerrainMaterialConfig() {}

    TerrainMaterialConfig::GradFlagMaterial::GradFlagMaterial()
    {
        _id = 0;
        for (auto&m:_mappingConstant) m = 0.f;
    }

    TerrainMaterialConfig::StrataMaterial::StrataMaterial()
    {
        _id = 0;
    }

    TerrainMaterialConfig::StrataMaterial::Strata::Strata()
    {
        for (auto&m:_mappingConstant) m = 1.f;
        _endHeight = 0.f;
    }

    TerrainMaterialConfig::ProcTextureSetting::ProcTextureSetting()
        : _hgrid(100.f), _gain(.5f) {}
}