Skip to main content
GameDev.net gamedev.net
🔒 Locked

Dealing with dependency

Started by Ariste Apr 9, 2009 at 2:21 AM 2 replies 1.1k views
Original Post
Ariste
Ariste
What, do you guys find, is the best way to do it? By way of background: I've been mucking my way through some DirectX tutorials for the last week or so, and, in the interest of learning and simplifying things for later use, I'm writing what I guess you would call a 'wrapper' for it (though I'm not 100% clear on the meaning of the term). It's the biggest project I've worked on so far, and the first one where design choices really mean something. My goal here is to set up a framework in which the inner workings of DX and Windows are abstracted away and their functionality is compartmentalized. Clients interface with DX through the framework; setting up a vertex buffer, for example, looks something like graphics.buffers.addVertexBuffer(). I like the idea of this kind of compartmentalization, but, being self-taught and still pretty new at this, I'm uncertain how to deal with some of the intricacies of such a project. The biggest questions I have deal with how to handle dependency between nominally independent modules. To give an example: I have a Graphics class which (gasp) handles my graphics. It has nodes, like BufferNode, VertexNode, EffectNode, etc. My impulse is to keep these as independent as possible. In reality, my vertex node has to feed information to my buffer node, my effect node has to interface with its child nodes (TransformNode, for example), and my Graphics class occasionally has to grab information from one or more of these to do its thing. These connections are probably necessary, but I know there are bad and less bad ways to handle them. What are some of your strategies for dealing with such interconnections? For example, should each node hold a pointer to each of its dependencies? If so, when should these pointers be given out, and who should be responsible for initializing them? Or should each node access its dependencies by worming through the Graphics class, to which each already holds a pointer? Or maybe access should be granted only by passing nodes around as function parameters. This is just the beginning, and I'm sure that as the project grows, it'll only get more complex. So what are some of your favorite strategies for dealing with dependencies? Does the idea for the framework I've laid out sound reasonable, or am I going about it totally the wrong way? Any input y'all have would be much appreciated. Looking forward to your responses!
haegarr
haegarr
This kind of thing isn't answered easily, since it has many aspects. The following is a coarse (and sometimes simplified) description of how it is done by me.

So-called "sub-systems" give the top level of abstraction. There is the Video, Audio, Input, Graphics, and Sound sub-systems as the primarily interesting ones. Video and Audio are abstractions of hardware, if one wants to say so. E.g. screens and/or windows belong to Video. Concrete Video sub-classes are e.g. QuartzVideo (for Mac OS X) or X11Video (for linux and the like). Graphics and Sound are sub-systems for rendering, well, graphics and sound, resp. You've guessed that, right? ;) Concrete sub-classes are e.g. OpenGL or SVG for Graphics.

Now, here is a typical example of your problem. How should the Application itself get access to the correct Video instance? Each head of each graphics card in the system is represented by an own concrete Video instance. The Application normally accesses the "main" or "standard" Video instance, or else it iterates the list of avaiable Video instances and look whether it finds one matching its requirements. An Application that is aware of running on e.g. linux can access directly X11Video instances, while system agnostic Applications simply use the interface already defined by the basic Video class although actually getting X11Video instances. In other words, agnostic Applications simply take advantage of polymorphism by inheritance.

Moreover, not each concrete Video supports each concrete Graphics. E.g. There is no D3D9Graphics available for QuartzVideo. This problem is solved similarly. A concrete Video instance is responsible to tell about all concrete Graphics instances that are supported by just that Video instance. Here something like the factory design pattern comes into play. A Video instance is a factory for Graphics instances. It can be asked to get a specific standard instance, e.g.
myVideo->newGraphics("OpenGL");
which will either return nil or a new specified instance, or it can be asked in a more general way
myVideo->newBest3DGraphics();

Now, the fact that the Graphics instance itself will usually depend on the concrete Video instance is hidden in the factory method: The factory method is implemented by the _concrete_ Video instance, and hence has implicit _knowledge_ of the concrete type of self, and also has implicit knowledge of the concrete type of OpenGL it requires. E.g. for X11Video its is an OpenGL backed by GXL, for Windows it would be backed by WGL, and so on.

And how should the system agnostic Application deal with this? Well, a concrete Graphics instance is itself a factory, but now its a factory of GfxMedia instances. It is invoked like "give me a full-screen media with 24 bit color and 24 bit depth support and ..." and returns a concrete GfxMedia. Such a media has e.g. the support to swap front and back rendering buffers. An OpenGL for Windows will return a concrete GfxMedia that uses WGL routines for this job, while OpenGL for X11 returns one that uses GLX, and so on.

For short: I'm defining general interfaces in the basic classes so that system agnostic Applications can deal with them. Concrete sub-classes do both, implementing the interface and specializing it, so that system aware Applications can access the concrete type. An important part of the interface are factories for dependent, concrete parts.

Besides this sub-system stuff, the scene description is a big issue w.r.t. the OP's problem. Matrix, vector, quaternion, transformation, ... are all written by my own. E.g. all rendering will expect those own classes for input and return them for output. If they require another representation internally, then they have to do the conversion as necessary. However, the matrix and vector classes are explicitely written so that compatibility with OpenGL and D3D is given.

So in such cases the "abstraction" is done by defining an own type used by all and everything.

A 3rd example are textures. One could go the factory way again. In my case, the special kind of resource system doesn't work well with such a way. It is so that my project is both an editor and a engine, and it allows to run several Graphics sub-systems at a time (e.g. OpenGL for on-screen and SVG for file output). Using textures as an example, a OpenGL texture would not work with another Graphics than OpenGL. So I've implemented another kind of abstraction. E.g. Texture is the base class of graphics sub-system agnostic textures. Concrete sub-classes are ImageTexture, ProceduralTexture, and so on, and hence does _not_ implement any graphics sub-system dependency. Instead, as soon as an ImageTexture is registered as another game resource with the resource library, it gets a numeric index assigned (which is unique w.r.t. all ImageTextures currently in the resource library).

The library can be observed (you may know it as the Listener design pattern), so that an observer is notified that a new resource (an ImageTexture in this case) is added or removed. Now, e.g. a concrete Graphics sub-system can observe the resource library of ImageTextures. Each time a new ImageTexture is added it does nothing at default. But when rendering requests a texture by passing an ImageTexture, the OpenGL renderer extracts the belonging index and looks into an internal table whether a belonging OpenGL specific texture object is stored there. If not, then a new one is created and stored.

The above is a lazy way of creation. Principally one could create the OpenGL texture object already when the resource library is extended. However, observing is used to delete the texture object when an ImageTexture is removed from the library. (BTW: Keys and maps can be used instead of indices and arrays, of course.)

A 4th example is given by the so-called "game object". It is a trend nowadays to assemble game objects instead of baking all together. There are several threads about this topic here in GDnet, and several sources are available on the internet, so I don't go into details myself here.

Well, the above show some possibilities/mechanisms. Other solutions exist, too. How to do things does heavily depend on what you want to yield in, and how the circumstances are. But I hope I have given some things to think about. And don't forget that there are libraries out there that already do some abtraction. Maybe the one or other is of interest for you.
ApochPiQ
ApochPiQ
These dependencies sound like they could be completely eliminated if you design the code well. For example, why does Graphics need to know anything about the internals of its nodes? Why not just have your nodes know how to render themselves? This is a standard OO approach to design: the point of an object is that it knows how to handle its own data. Ideally, an object should be the only thing that knows what to do with its data.

Another thing to remember is that introducing new classes is not necessarily a bad thing. So instead of having your VertexNodes talk directly to your BufferNodes, create a class that represents the shared data that needs to be passed around; then use composition to hold that data in each node class. This eliminates tight coupling from your node classes and isolates the exact nature of the data dependency, making it much easier to work with in the future.
Ariste
Ariste
Quote:
Original post by ApochPiQ
These dependencies sound like they could be completely eliminated if you design the code well. For example, why does Graphics need to know anything about the internals of its nodes?

Maybe it doesn't; I'm just not sure how to make it work yet. Here's an example: DX requires you to set an input layout before drawing to the screen (IASetInputLayout()). This call requires information about vertex formats and also information about technique passes, which are part of an effect. Right now I just have my Graphics class dipping its hands into the vertex node and the effect node to get the required information, with a call that looks like this:

void Graphics::setInputFormat(std::string vDescID, std::string fxID, LPCSTR techID, int passIndex){	// Get an appropriate pass description	D3D10_PASS_DESC passDesc = effects.getPassDesc(fxID, techID, passIndex);	// Create a layout	ID3D10InputLayout* inputLayout;	d3dDevice->CreateInputLayout(vertices.getVertexDesc(vDescID)->descs,                                     vertices.getVertexDesc(vDescID)->numDescs, 				     passDesc.pIAInputSignature,                                     passDesc.IAInputSignatureSize                                      &inputLayout);	// Set the layout	d3dDevice->IASetInputLayout(inputLayout);	// Set active effects/vertices	effects.setActiveEffect(fxID);	vertices.setActiveVertList(vDescID);}

In another place, my buffer node needs to know where vertex data is located so it can properly create a vertex buffer. It gets this information by asking my vertex node to provide it.
Quote:
Why not just have your nodes know how to render themselves? This is a standard OO approach to design: the point of an object is that it knows how to handle its own data. Ideally, an object should be the only thing that knows what to do with its data.

Yeah, I'm down with that notion completely. I'm trying to learn how to actually do it :)
Quote:
Another thing to remember is that introducing new classes is not necessarily a bad thing. So instead of having your VertexNodes talk directly to your BufferNodes, create a class that represents the shared data that needs to be passed around; then use composition to hold that data in each node class. This eliminates tight coupling from your node classes and isolates the exact nature of the data dependency, making it much easier to work with in the future.

Okay, so are you saying that I should, in the example above, create a class like InputLayoutData that keeps itself updated by reading in data from the buffer/vertex nodes? Or should each node contain an instance of an InputLayoutData class? If so, how do these instances stay synchronized when the client switches input layouts? In fact, how does the client switch input layouts? Does he call this new InputLayoutData object, like graphics.inputLayout.set()? Or does this happen 'behind the scenes,' and the client still just calls graphics.setInputLayout()? I guess the bottom line is: who should 'own' this inherently distributed data?

And, as always, thanks for your input.

[Edited by - Ariste on April 9, 2009 11:08:41 AM]

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.