A curious non-OOP virtual inheritance for a GPU

A new technology I’m trying has a curious domain-specific extension for OpenGL graphics. It introduces a block concept allowing for modular shader code. From a language standpoint I was uncertain what to make of this feature. Is it new? Is it a tweak on an existing concept? From a theoretical view it most resembles inheritance, but retains a few distinct features.

The language is called Uno, from Outracks. The examples I use here are made up for this article. What I use in my actual code has grown quite complex in comparison. I’ve also significantly simplified the properties to avoid talking too deeply about the rendering aspect.

Basic example

 1 2 3 4 5 6 7 8 910111213
block Rectangle {    float2[] CoordData: [ [0,0], [1,0], [1,1], [0,1] ];    float2 VertexCoord: CoordData;    float3 FillColor: [ 1, 1, 1 ];    float Light: 1;    float3 PixelColor: FillColor * Light;}block BlueWideBox : Rectangle {    float2 Scale: [ 20, 1 ];    VertexCoords: prev * Scale;    FillColor: [ 0, 0, 1 ];}

Rectangle defines a block capable of drawing a rectangle on the screen. The CoordData are the corners of a rectangle in a normalized space. FillColor is the RGB white value. PixelColor is used by the renderer and says what color to use. VertexCoord says where to draw, and it is defined per-vertex. The compiler picks up that it is based on an array takes care of that for us. (Let’s assume the renderer can draw only a 4-vertex object so we simplify the indexing.)

BlueWideBox is an extension of the Rectangle. VertexCoords uses a prev keyword. The expression uses whatever was previously defined and multiplies it by Scale. FillColor ignores the previous value and simply sets a new one.

I’ll show this same code in a pseudo-OOP form to show it behaves the same as standard inheritance. Note that all methods are virtual in this code.

 1 2 3 4 5 6 7 8 91011121314151617181920212223242526272829
class Rectangle {    protected float2[] CoordData() {        return [ [0,0], [1,0], [1,1], [0,1] ]    }    public float2 VertexCoord(int ndx) {        return CoordData[ndx]    }    protected float3 FillColor() {        return [ 1, 1, 1 ]    }    protected float Light() {        return 1    }    public float3 PixelColor(int ndx) {        return FillColor() * Light()    }}class BlueWideBox : Rectangle {    protected float2 Scale() {        return [ 20, 1 ]    }    public override float2 VertexCoord(int ndx) {        return super(ndx) * Scale()    }    protected float3 FillColor() {        return [ 0, 0, 1 ]    }}

The VertexCoord and PixelColor are declared public to emphasize these properties are expected by the renderer. All the other properties are protected since the renderer doesn’t care about; it doesn’t use them directly.

The indexing concept is made explicit here by an integer parameter to the VertexCoord function. It’s also clear now that PixelColor is also an indexed field. It defines the color to use at each vertex.

The call of super(ndx) in VertexCoord shows that the prev keyword is just calling the same function on the base block.

FillColor is given a new definition here. All methods are virtual. When Rectangle.PixelColor calls FillColor it will be calling this new definition instead. The purpose of this is to allow us to define a new color but still have the Light applied by the base definition.

The indexing concept is actually more complex than this in the real language. These indexed variables are actually interpolated between the indexes. They definitely could be modelled in our OOP pseudo-code, but it’s not a nuance relevant to this discussion. It is however a key feature for the OpenGL rendering aspect of the language.

Abstract classes

Those special properties, VertexCoord and Pixel are essentially part of an implicit interface that all blocks define. This is not made explicit in the language, but since undefined values are allowed it can look like this.

1234
block Base {    public float2 VertexCoord: undefined;    public float3 PixelColor: undefined;}

This translates simply to an interface in most OOP languages.

1234
interface Base {    public float2 VertexCoord();    public float3 PixelColor();}

Abstract properties can be defined within any block. It is the compact nature of this syntax which makes it interesting. For example, it seems kind of silly to make the Rectangle white by default. Instead we can require the using block to define the color.

123456
block Rectangle {    ...    float Light: 1;    float3 PixelColor: req(FillColor as float3)        FillColor * Light;}

This translates to abstract functions in our pseudo-code, or pure virtual functions in C++.

1234567
class Rectangle : Base {    ...    protected abstract float3 FillColor();    public float3 PixelColor(int ndx) {        return FillColor() * Light();    }}

The definition of PixelColor doesn’t change here, but the compiler will now produce an error if a derived class does not provide FillColor.

Fallback definitions

Not all abstract properties have to actually be given values. In the block language it is possible to give a fallback definition in the case of a missing property.

123
    float3 PixelColor:        req(FillColor as float3) FillColor * Light,        [1,1,1] * Light;

If FillColor is never defined it will fallback to the alternate definition that doesn’t use it. The , operator does this. The entire expression is considered. Any transitive property in the left-side could be missing and it will then consider the right side.

There is no simple translation of this feature to classic OOP. One option is to make a parallel declaration for all members, prefix them with Has and return true or false. That would be excessively bulky.

A dynamic language however can achieve this relatively painlessly. I’ll just extend our pseudo language for this.

123456
    public float3 PixelColor(int ndx) {        if( HasField( "FillColor" ) ) {            return FillColor() * Light();        }        return [1,1,1] * Light();    }

It’s also possible to achieve this in C++ if you use template types and the type traits functionality. It will however not look pretty.

None of those are as satisfying as the block comma operator though. It is succinct and clear. I consider it worthwhile to consider adding this feature to a language. It does however require one clarification first…

It’s not a type hierarchy

A divergence from typical OOP is that Uno blocks are not creating a type hierarchy. The finally derived type is flat. Though BlueWideBox derives from Rectangle, it cannot be used as a Rectangle. It also isn’t merely private inheritance. Even within the block it can’t refer to itself as a Rectangle. BlueWideBox is a distinct flat type.

This is more akin to class composition. The trick of course being that all the bits are sharing the same virtual functions and are directly exposed. The only other language I can think of with this feature builtin is Ruby with mixins. I’ve also seen an example with Python decorators that might accomplish the same thing.

A major point to this system is the ability for the compiler to flatten the type. Though it looks like inheritance with virtual functions, the resulting type is fully known. The virtual calls can be resolved statically. Unused functions and fields can be eliminated. The optimizer gets to inline, modify, and reorder at will.

For Uno this is probably the key point. The code must become a GL shader, and that requires static code. Plus it really should be highly optimized code. It also splits the code between CPU/GPU, but that’s not of so much interest to us in this article.

This is definitely a feature more languages should have. There is a clear distinction between inheritance for the purpose of creating derived types, and composition for creating unique flat types.

Anonymous types

These blocks are used with the draw keyword. It instructs the system to actually use them to render something on the GPU.

This syntax introduces an additional feature. Multiple blocks can be chained in sequence and a final customization can be provided.

123
draw Rectangle, Texture {    Light: prev * 2;}

The final block is merely an anonymous block that is then composed with the two other blocks in sequence. In our pseudo-code this may look like:

12345678
class Anon1 : Rectangle, Texture {}class Anon2 : Anon1 {    public float Light() {        return super() * 2    }}

Java is the one other language I can think of that offers anonymous classes with inheritance. I suppose both Ruby and JavaScript can technically create anonymous classes at runtime. Many languages offer implicit tuple types that don’t allow inheritance.

Anonymous and implicit types are something I find very useful in a language. They encourage structure coding with increased code reuse.

Summary

Overall Uno’s block mechanism can be conceptually equated to inheritance. All the properties and methods are virtual. An undefined keyword as well as required properties allow creation of abstract blocks. The fallback definition feature can be done in other languages, but the syntax here is more satisfying. A few uncommon features are flattened types and anonymous blocks.

It’s curious that other languages have such a varying level support of similar features. Are they part of a nascent family of features, or are the use-cases more limited than it seems? Perhaps there’s something special about shader code. In any case, I see a lot of potential and would welcome them in any object oriented language. I will certainly look to integrating them cleanly into Leaf.

A curious non-OOP virtual inheritance for a GPU

相关文章:

你感兴趣的文章:

标签云: