Graphics Backend Abstraction

theagentd and I were discussing abstraction of graphics backends and we both couldn’t think of the best solution to the problem.

The issue, for me, is having my software renderer and OpenGL (and possibly other) backends work under the same level of abstraction and theagentd’s issue is the same but with OpenGL and Vulkan.

The main issue is with the symbolic constants. Vulkan, OpenGL and the software renderer have different symbolic constants and we dont know the best way to abstract these symbolic constants.

theagentd currently uses enums like


public enum PrimitiveTopology{
  
  PointList(GL_POINTS, VK_PRIMITIVE_TOPOLOGY_POINT_LIST),
  LineList(GL_LINES, VK_PRIMITIVE_TOPOLOGY_LINE_LIST),
  TriangleList(GL_TRIANGLES, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST),

  private int gl, vk;
  
  private PrimitiveTopology(int gl, int vk){
   this.gl = gl;
   this.vk = vk;
  }
  
  public int gl() {
   return gl;
  }
  
  public int vk() {
   return vk;
  }
 }

Where the implementation simply gets the appropriate symbolic constant that it needs from the enum by doing .gl() for the OpenGL implementation and .vk() for the Vulkan implementation.

This sounds great but then we realized that in order to add a new implementation, we would have to add its symbolic constants to this “master enum” which kinda ruins the idea of having a contained implementation.

theagentd said that the enum could hold array indices which index into an array of symbolic constants defined by the implementation, but this can be slower.

Does anyone have any ideas?

The problem with this system is as mentioned that it requires modifying the enum whenever you want to add a new backend, which makes it hard for people to add their own backend if someone would be crazy enough to want to do that.

My proposed solution would be to have the enum—>constant mapping in the backends instead. A hashmap is gonna be way too slow, but a simple array indexed by ordinal() could work.


private static final int[] PRIMITIVE_TOPOLOGY_MAP = {GL_POINTS, GL_LINES, GL_TRIANGLES};

public void render(PrimitiveTopology param){
    int constant = PRIMITIVE_TOPOLOGY_MAP[param.ordinal()];
    ... //use constant
}

I think this would have more overhead than having the data in the enum as a field though…

Ideas, anyone?

er … EnumMap!

OK, so I need to write more text ::slight_smile: Why would you use an array indexed by ordinal() over an EnumMap?

Question aside if this level of abstraction is what you should aim for, this would be a pretty simple implementation:


public enum PrimitiveTopology {

	PointList,
	LineList,
	TriangleList;

	private int value;

	public int value() {
		return value;
	}

	static void initialize(int... constants) {
		for (PrimitiveTopology v : values()) {
			v.value = constants[v.ordinal()];
		}
	}
}

class BackendGL {

	static void initialize() {
		// call once at backend initialization time
		PrimitiveTopology.initialize(GL_POINTS, GL_LINES, GL_TRIANGLES);
	}

}

@CoDi^R

That would be an issue when someone reordered the enum contents.

A non-issue in my book, but you could harden the initialize() function as much as you want, so that it fails hard if someone messes up.

You could use reflection and have a system like:

public enum Constant {
      POINTS,
      LINES,
      TRIANGLES
}
public interface Backend {
     // Returns {Class, Constant Name}
      public String[] getConstant(Constant c);
}
public class Values {
       private HashMap<Constant, Object> constants = new HashMap<>();
       public void load(Backend b) {
             for (Constant c : Constant.values()) {
                    String[] arr = b.getConstant(c);
                     // Use reflection to get the constant value arr[1] from class arr[0]
              }
       }
       public Object lookup(Constant c) {
              return constants.get(c);
       }
}

This system also allows for storing Functional interfaces as well as integer constants.

EDIT: You can also store the lookup results as integer/Object constants in another class to reduce lookup overhead

Because evidently an EnumMap is just a wrapper around an array indexed with ordinal() anyway, and using EnumMap has too much overhead, bad cache coherency and will need autoboxing of the integer constants to fit them in.

    public V put(K key, V value) {
        typeCheck(key);
        int index = ((Enum)key).ordinal();
        Object oldValue = vals[index];
        vals[index] = maskNull(value);
        if (oldValue == null)
            size++;
        return unmaskNull(oldValue);
    }

@CoDi^R: I actually really like this approach. It’d actually even be “faster” than storing all values in the enum, as the enum would only need to store one value. Hmm. You would only be able to use one backend at a time, but that’s fine. You could even live swap the backend by just calling initialize() again to switch out the values. I really like this approach. =P

@SHC: That could be solved by forcing the user to supply a mapping instead, so you’d pass in [icode]initialize(new PrimitiveTopology[]{PointList, LineList, TriangleList}, new int[]{GL_POINTS, GL_LINES, GL_TRIANGLES});[/icode], which in turn would simply loop over the two arrays doing [icode]constants[enumArray[i].ordinal()] = constantArray[i];[/icode]. That’d be resistant to order changes, and you could make it cleanly error during runtime if you’ve added more enum values that the backend doesn’t cover yet.

@CopyableCougar4: That’s way too slow. A hashmap has a huge amount of overhead, and I’ll be needing to look up 1-5 values per function call to the API. We’re talking thousands of lookups per frame or even more. This is where holding the int constants in the enum really shines, because it means that the getter of that int can be completely inlined and the enum just becomes a wrapper around the int. That’s essentially as fast as you can possibly get it (without having the user get the int themselves I guess, but I want the type safety of enums as well).

That was my point, that you were reinventing it without knowing whether it’s actually a bottleneck. True, if you’re mapping to int you’ve got the unboxing. Where’s the bad cache coherency come from in there though? From the Integer? I guess you need an EnumIntMap then! :slight_smile: Few around.

The bad cache coherency comes from having the additional EnumMap object lying around. By keeping the raw int[] for conversion instead of an EnumMap, you end up getting a cache miss when looking for the EnumMap instance, then another one for the actual array the EnumMap object references. The impact is probably not that severe, but it’s there.

I will run some benchmarks.

Benchmark for mapping enum to int: http://www.java-gaming.org/?action=pastebin&id=1525


HashMap: 5.521456 ms    hashMap.get(enum)
EnumMap: 1.240702 ms    enumMap.get(enum)
Array:   0.560504 ms    constants[enum.ordinal()]
Field:   0.338337 ms    enum.constant

You would just use the hashmap when you first load the program. Then you could store that values in static variables and lookup in that class when you need to.

How do you mean? That doesn’t make sense to me.

I tweaked my code a little and posted it here: http://pastebin.java-gaming.org/3e0306828521b

Once you wrote the Backend implementations, you would call this on load:

Graphics.load(new OpenGLBackend());
// now GL11.GL_POINTS is found at Graphics.points, etc.

You could also use this to store functional interfaces to call backend-specific methods.

@CopyableCougar4: The problem is that I want the user to be able to pass in an enum (say PrimitiveTopology.Points) and the backend then converts that enum to the symbolic constant of the API it uses (either GL_POINTS or VK_PRIMITIVE_TOPOLOGY_POINT_LIST) on the fly. It’s the problem of doing this mapping quickly that I’m worried about. I don’t really see your example code solving that, as extracting the int constants to variables at initialization time doesn’t really help when the user later passes in enums.

@CoDi^R


public enum PrimitiveTopology {

   PointList,
   LineList,
   TriangleList;

   private int value;

   public int value() {
      return value;
   }

   static void initialize(int points, int lines, int triangles) {
     PointList.value = points;
     LineList.value = lines;
     TriangleList.value = triangles;
   }
}

class BackendGL {

   static void initialize() {
      // call once at backend initialization time
      PrimitiveTopology.initialize(GL_POINTS, GL_LINES, GL_TRIANGLES);
   }

}

If you’re going to have the variables inside the enums for speed, I’d use ServiceLoader to lookup the required implementation at runtime and request the underlying values from it, rather than have the implementation have to configure the enums itself - that’s … yuck! :persecutioncomplex:

From my point of view, an abstraction layer shouldn’t contain backend-specific data in the first place.

The abstract model should have its own, backend-independent way to differentiate between
geometry types (POINTS, LINES, TRIANGLES, QUADS, SPLINES, SPHERES, something completely different) and the backends
have to map it to their own model at some point.

The mapping actually has to occur just once, for example when the geometry is read from a file.

What is a ServiceLoader? How would that work with my enums? What kind of advantages are there here?

I agree with this, which is why I want to go with enums for the abstraction to get compile time checking of arguments. My initial idea had the OpenGL and Vulkan constants in the enum, which was bad as it meant having backend specific data in the abstraction. However, I think putting an int field in the enum that the backend can use for mapping the enum to a constant isn’t the same thing and shouldn’t be bad design as it has the best performance and arguable the lowest complexity and maintenance requirements.

[quote=“homac,post:18,topic:58391”]
Not in my abstraction. It’s just a thin layer over OpenGL and Vulkan, so data in a buffer for example isn’t tied to a specific geometry topology like triangles or points. The mapping has to be done every time you submit a draw call, and there are a lot of other cases where I’ll be needing to map lots of enums to int constants for OGL and VK.

What if the rendering is abstracted so much that symbolic constants are not used at all from the user’s end