Java-Gaming.org Hi !
Featured games (90)
games approved by the League of Dukes
Games in Showcase (736)
Games in Android Showcase (223)
games submitted by our members
Games in WIP (813)
games currently in development
News: Read the Java Gaming Resources, or peek at the official Java tutorials
 
    Home     Help   Search   Login   Register   
Pages: [1] 2
  ignore  |  Print  
  Graphics Backend Abstraction  (Read 9134 times)
0 Members and 1 Guest are viewing this topic.
Offline Archive
« Posted 2017-03-24 00:48:04 »

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
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
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?

Offline theagentd
« Reply #1 - Posted 2017-03-24 11:08:42 »

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.

1  
2  
3  
4  
5  
6  
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?

Myomyomyo.
Offline nsigma
« Reply #2 - Posted 2017-03-24 11:34:18 »

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.

er ... EnumMap!

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

Praxis LIVE - hybrid visual IDE for (live) creative coding
Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline CoDi^R
« Reply #3 - Posted 2017-03-24 11:40:25 »

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

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
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);
   }

}

Robotality - steamworks4j - @code_disaster - codi^r @ #java-gaming
Offline SHC
« Reply #4 - Posted 2017-03-24 11:48:33 »

@CoDi^R

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

Offline CoDi^R
« Reply #5 - Posted 2017-03-24 11:51:14 »

@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.

Robotality - steamworks4j - @code_disaster - codi^r @ #java-gaming
Offline CopyableCougar4
« Reply #6 - Posted 2017-03-24 14:07:46 »

You could use reflection and have a system like:
1  
2  
3  
4  
5  
public enum Constant {
      POINTS,
      LINES,
      TRIANGLES
}

1  
2  
3  
4  
public interface Backend {
     // Returns {Class, Constant Name}
      public String[] getConstant(Constant c);
}

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
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

Either wandering the forum or programming. Most likely the latter Smiley

Github: http://github.com/CopyableCougar4
Offline theagentd
« Reply #7 - Posted 2017-03-24 14:56:49 »

OK, so I need to write more text  Roll Eyes  Why would you use an array indexed by ordinal() over an EnumMap?
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.
1  
2  
3  
4  
5  
6  
7  
8  
9  
    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
initialize(new PrimitiveTopology[]{PointList, LineList, TriangleList}, new int[]{GL_POINTS, GL_LINES, GL_TRIANGLES});
, which in turn would simply loop over the two arrays doing
constants[enumArray[i].ordinal()] = constantArray[i];
. 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).

Myomyomyo.
Offline nsigma
« Reply #8 - Posted 2017-03-24 15:06:44 »

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.

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!   Smiley  Few around.

Praxis LIVE - hybrid visual IDE for (live) creative coding
Offline theagentd
« Reply #9 - Posted 2017-03-24 15:13:21 »

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.

Myomyomyo.
Games published by our own members! Check 'em out!
Legends of Yore - The Casual Retro Roguelike
Offline theagentd
« Reply #10 - Posted 2017-03-24 15:32:23 »

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

1  
2  
3  
4  
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

Myomyomyo.
Offline CopyableCougar4
« Reply #11 - Posted 2017-03-24 15:33:34 »

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.

Either wandering the forum or programming. Most likely the latter Smiley

Github: http://github.com/CopyableCougar4
Offline theagentd
« Reply #12 - Posted 2017-03-24 15:34:14 »

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.

Myomyomyo.
Offline CopyableCougar4
« Reply #13 - Posted 2017-03-24 15:42:09 »

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:
1  
2  
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.

Either wandering the forum or programming. Most likely the latter Smiley

Github: http://github.com/CopyableCougar4
Offline theagentd
« Reply #14 - Posted 2017-03-24 17:51:34 »

@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.

Myomyomyo.
Offline Archive
« Reply #15 - Posted 2017-03-24 18:31:10 »

@CoDi^R

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
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);
   }

}

Offline nsigma
« Reply #16 - Posted 2017-03-24 18:37:35 »

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

Praxis LIVE - hybrid visual IDE for (live) creative coding
Offline homac

Senior Devvie


Medals: 18


life is cake.


« Reply #17 - Posted 2017-03-24 18:54:29 »

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.

The guy who cooks Java .Blend and helps people to launch their Life in the Woods.
Offline theagentd
« Reply #18 - Posted 2017-03-25 00:11:04 »

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
What is a ServiceLoader? How would that work with my enums? What kind of advantages are there here?


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.
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.

The mapping actually has to occur just once, for example when the geometry is read from a file.
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.


Myomyomyo.
Offline Archive
« Reply #19 - Posted 2017-03-25 02:23:57 »

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

Offline CopyableCougar4
« Reply #20 - Posted 2017-03-25 06:00:06 »

@theagentd: But that conversion from enum constant to backend specific integer is the same as my implementation, just using a static variable as opposed to an enum (although I would be interested in RAM usage with an enum as opposed to static class fields).

Although writing such a thin abstraction layer doesn't really seem to serve a practical purpose. It just seems to introduce more problems than it solves.

Either wandering the forum or programming. Most likely the latter Smiley

Github: http://github.com/CopyableCougar4
Offline nsigma
« Reply #21 - Posted 2017-03-25 12:46:46 »

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

It's the standard method built into the JDK for an abstraction to find an implementation without having any dependency on it. eg https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html

What I mean is that you build an initialization step into the abstraction API that looks up implementations at runtime using ServiceLoader. This usually involves an implementation of a provider class.  That in itself isn't specific to your enums.  As part of that initialization the abstracted API (eg. code in the enum or same package) queries the provider implementation for each int value and caches it in the required enums.  The implementations do not have to initialize the enums themselves, or know anything about what mechanism you're using to cache the constant mapping.  You manage the mechanism, initialization order, etc. in one place rather than many.

The code @Archive posted where each backend has to call back into each enum, know all the possible values, do things in the right order, etc. becomes tiresome quickly the more the API expands and the more backends you have.

Praxis LIVE - hybrid visual IDE for (live) creative coding
Offline homac

Senior Devvie


Medals: 18


life is cake.


« Reply #22 - Posted 2017-03-25 17:31:31 »

... 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 ...

I agree with that. I had another use case in mind. If you go for just those two backends and the mapping is n:m (with n <= m) then I'd go for the approach of CoDi^R or just map it directly in the switch/case statement.

I tend to think too long about such things ending up with solutions which doesn't seem to improve it too much, but burned time away like hell.

The guy who cooks Java .Blend and helps people to launch their Life in the Woods.
Offline contomlon

Innocent Bystander





« Reply #23 - Posted 2017-04-12 09:37:19 »

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.

* super mario world 
* madalin stunt cars 2
* slope
Offline princec

« JGO Spiffy Duke »


Medals: 945
Projects: 3
Exp: 16 years


Eh? Who? What? ... Me?


« Reply #24 - Posted 2017-04-12 11:06:50 »

OpenGL and Vulkan are already abstractions... you probably don't want to abstract over them any more.

Cas Smiley

Offline homac

Senior Devvie


Medals: 18


life is cake.


« Reply #25 - Posted 2017-04-12 11:52:22 »

c.ontomions post is SPAM. He just copy/pasted theagents reply and added his URLs below.

The guy who cooks Java .Blend and helps people to launch their Life in the Woods.
Offline princec

« JGO Spiffy Duke »


Medals: 945
Projects: 3
Exp: 16 years


Eh? Who? What? ... Me?


« Reply #26 - Posted 2017-04-12 13:46:29 »

Given the hoops you have to jump through to register that's one determined spammer...

Cas Smiley

Offline theagentd
« Reply #27 - Posted 2017-04-12 15:51:56 »

OpenGL and Vulkan are already abstractions... you probably don't want to abstract over them any more.

Cas Smiley
I don't agree. This is common practice in the engine world. You write your own abstraction layer over the different APIs you want to support, mainly to support console specific APIs, and then write your engine on top of this abstraction. This makes your engine independent of the API you use.

The huge mistake that people make is that they see Vulkan and all its complexity and say "Damn, this is too complicated. I'm gonna write an abstraction that let's me use Vulkan as if it's OpenGL!". Essentially, what this means is that they'll be writing their own OpenGL driver on top of Vulkan, and trust me, nobody has more resources than Nvidia's driver team when it comes to OpenGL drivers. You're going to fail, it's going to suck, it's going to be a nightmare of complexity and it's going to be slow.

The correct approach which I'm taking is to do the opposite thing: Write a Vulkan emulator on top of OpenGL. Basically, emulate command buffers and descriptor sets, discard all the extra information that Vulkan requires, etc. In fact, OpenGL either trivially maps to Vulkan in most cases, or is simpler than Vulkan. Vulkan render passes just contain a mapping from a list of render targets to a set of draw buffers, while Vulkan framebuffers (together with render passes) trivially map to an array of OpenGL FBOs. Emulating Vulkan on OpenGL is trivial.

The result however is not at all a simplification of the underlying libraries. Rather, it is the most explicit kind of abstraction you can imagine. You essentially have to provide all information that any of the underlying libraries would require for everything, with the unneeded information being silently discarded by each implementation. In practice, this means essentially coding for Vulkan and getting OpenGL/DirectX/Metal/WebGL/OpenGL ES for free.

That being said, you still get a lot of things for free with my abstraction. The most complicated parts of Vulkan is arguably the swapchain management, synchronization and queue management. My abstraction will completely hide the swapchain management, simplify and abstract the synchronization and automate the queue construction. In the end you only need to worry about the "fun" parts of Vulkan where you actually gain a shitload of performance and flexibility.

Myomyomyo.
Offline princec

« JGO Spiffy Duke »


Medals: 945
Projects: 3
Exp: 16 years


Eh? Who? What? ... Me?


« Reply #28 - Posted 2017-04-12 16:08:16 »

What I'm getting at is, why abstract this, when your "engine" API is already the abstraction? The underlying rendering API - Vulkan or OpenGL (or even DX12!) - is what your engine will be using to turn its own ideas about what it has to render into rendering commands for the specified low-level API. You write a specific plug-in to render what your "engine" contains using the specific API. There's no real point in abstracting the rendering APIs and then using that abstraction to feed to your engine... do it directly. I'm probably not explaining this very well though.

Cas Smiley

Offline KaiHH

JGO Kernel


Medals: 442



« Reply #29 - Posted 2017-04-12 16:17:14 »

It looks like @theagentd is developing a "middleware" for game engine developers.
In my opinion, it is probably of little use to actual "game developers", who require a much higher level of abstraction from a game engine, to be able to say: "Here is my model file, please render that with a nice sky, a sun and beautiful reflecting water." and possibly just modeling and scripting everything in a nice editor, like the CryEngine Sandbox editor. Those people hardly want to get down to such low-level details as "command buffers" and "swap chains".
But for game engine developers, this could be a thing.
Pages: [1] 2
  ignore  |  Print  
 
 

 
cybrmynd (124 views)
2017-08-02 12:28:51

cybrmynd (147 views)
2017-08-02 12:19:43

cybrmynd (142 views)
2017-08-02 12:18:09

Sralse (158 views)
2017-07-25 17:13:48

Archive (629 views)
2017-04-27 17:45:51

buddyBro (737 views)
2017-04-05 03:38:00

CopyableCougar4 (1266 views)
2017-03-24 15:39:42

theagentd (1246 views)
2017-03-24 15:32:08

Rule (1221 views)
2017-03-19 12:43:22

Rule (1274 views)
2017-03-19 12:42:17
List of Learning Resources
by elect
2017-03-13 14:05:44

List of Learning Resources
by elect
2017-03-13 14:04:45

SF/X Libraries
by philfrei
2017-03-02 08:45:19

SF/X Libraries
by philfrei
2017-03-02 08:44:05

SF/X Libraries
by SkyAphid
2017-03-02 06:38:56

SF/X Libraries
by SkyAphid
2017-03-02 06:38:32

SF/X Libraries
by SkyAphid
2017-03-02 06:38:05

SF/X Libraries
by SkyAphid
2017-03-02 06:37:51
java-gaming.org is not responsible for the content posted by its members, including references to external websites, and other references that may or may not have a relation with our primarily gaming and game production oriented community. inquiries and complaints can be sent via email to the info‑account of the company managing the website of java‑gaming.org
Powered by MySQL Powered by PHP Powered by SMF 1.1.18 | SMF © 2013, Simple Machines | Managed by Enhanced Four Valid XHTML 1.0! Valid CSS!