Jump to content

Rewerking the Asset Class -or- Back that Asset Up

Josh

2,637 views

Since you guys are my boss, in a way, I wanted to report to you what I spent the last few days doing.

 

Early on in the development of Leadwerks 3, I had to figure out a way to handle the management of assets that are shared across many objects. Materials, textures, shaders, and a few other things can be used by many different objects, but they consume resources, so its important for the engine to clean them up when they are no longer needed.

 

I decided to implement an "Asset" and "AssetReference" class. The AssetReference object contains the actual data, and the Asset object is just a handle to the AssetReference. ("AssetBase" probably would have been a more appropriate name). Each AssetReference can have multiple instances (Assets). When a new Asset is created, the AssetReference's instance count is incremented. When an Asset is deleted, its AssetReference's instance counter is decremented. When the AssetReference instance counter reaches zero, the AssetReference object itself is deleted.

 

Each different kind of Asset had a class that extended each of these classes. For example, there was the Texture and TextureReference classes. The real OpenGL texture handle was stored in the TextureReference class.

 

Normal usage would involve deleting extra instances of the object, as follows:

Material* material = Material::Create();
Texture* texture = Texture::Load("myimage.tex");
material->SetTexture(texture);// This will create a new instance of the texture
delete texture;

 

This isn't a bad setup, but it creates a lot of extra classes. Remember, each of the AssetReference classes actually get extended for the graphics module, so we have the following classes:

Asset
Texture
AssetReference
TextureReference
OpenGL2TextureReference

OpenGLES2TextureReference

 

The "Texture" class is the only class the programmer (you) needs to see or deal with, though.

 

Anyways, this struck me as a bad design. Several months ago I decided I would redo this before the final release. I got rid of all the weird "Reference" classes and instead used reference counting built into the base Object class, from which these are all derived.

 

The usage for you, the end user, isn't drastically different:

 

Material* material = Material::Create();
Texture* texture = Texture::Load("myimage.tex");
material->SetTexture(texture);
texture->Release();// Decrements the reference counter and deletes the object when it reaches zero

 

In the Material's destructor, it will actually decrement each of its texture objects, so they will automatically get cleaned up if they are no longer needed. In the example below, the texture will be deleted from memory at the end of the code:

Material* material = Material::Create();
Texture* texture = Texture::Load("myimage.tex");
material->SetTexture(texture);
texture->Release();
material->Release();// texture's ref count will be decremented to zero and it will be deleted

 

If you want to make an extra "instance" (not really) of the Asset, just increment the reference counter:

 

Material* material = Material::Create();
Texture* texture = Texture::Load("myimage.tex");
texture->IncRefCount();// increments ref count from 1 to 2
Texture* tex2 = texture;
texture->Release();// decrements ref count from 2 to 1

 

This makes the code smaller, gets rid of a lot of classes, and I think it will be easier to explain to game studios when I am trying to sell them a source code license.

 

Naturally any time you make systemic changes to the engine there will be some bugs here and there, but I have the engine and editor running, and mistakes are easy to find when I debug the static library.

 

I also learned that operation overloading doesn't work with pointers, which i did not know, but had never had a need to try before. Originally, I was going to use operation overloads for the inc/dec ref count commands:

Texture* tex = Texture::Create();//refcount=1
tex++;//refcount=2
tex--;//refcount=1
tex--;// texture would get deleted here

 

But that just messes with the pointer! So don't do things like that.



22 Comments


Recommended Comments

looks like COM programming. not a fan of that really. I'd prefer to call delete instead of ->Release() for the simple fact that that's what we do with pointers. Having to call IncRefCount() is even worse. That'll be forgotten by people a large % of the time I'm guessing and cause some nice bugs.

Share this comment


Link to comment

I prefer that every definition, type, function, class and method are tailor made for the engine. That's how professional libraries do it too, and rarely depend on C++'s native functions and methods. In Qt it's actually done a bit clumsy, and it's kinda annoying that you can't use std::string with Qt, but you have to use QString. So, keep your own classes and methods still compatible with STL classes.

Share this comment


Link to comment

I think having to implement basic functionality that should have been thought about when C++ was first designed is a huge weakness of the language, but every language since then has been managed code, so I guess it's the best thing we've got.

Share this comment


Link to comment

Yeah.. this is the way COM works (also see DirectX). Works well. You can also build in some automatically ref-counting in assignment operators, copy constructors and in destructors.

Share this comment


Link to comment

Yeah.. this is the way COM works (also see DirectX). Works well. You can also build in some automatically ref-counting in assignment operators, copy constructors and in destructors.

But not for pointers, right?

Share this comment


Link to comment

I think Ken mentioned in the other post that doing (*var)++ should work. Haven't tried it but seems like it would. Even if that does work I personally think that's messy. If I had the choice I'd rather call the function so it's clear and obvious what's happening.

Share this comment


Link to comment

I agree with Rick. It seems counter-intuitive to me to have to call IncRefCount. After all, why would I? I have a texture object, I want to use it, why should I do anything to it first? Similarly, using Release() instead of delete seems just as counter intuitive. Normal C++ would do exactly as in your first example.

 

I understand you're removing internal classes this way, but for the end user, it's not prettier, not more intuitive, and will actually be a bother in the long run if we have to type this for every object.

 

In the "source purity" aspect, this has the adverse effect of making me think this wasn't designed for C++, but just patched to work with it.

Share this comment


Link to comment

material->SetTexture(texture);// This will create a new instance of the texture

Why would the above line create a new instance? Wouldn't it be

Texture* texture = Texture::Load("myimage.tex");

that actually creates the instance? I can see the SetTexture() line adding to the ref count but not creating a new instance.

 

 

Also, why do we have the static Load() and Create() methods again vs using new? I know there was a reason but can't seem to remember.

Share this comment


Link to comment

The alternative to having a memory management system like this is either memory leaks or invalid pointer deletion crashes.

 

Or I could just make every call to the asset load functions uniquely load the data, and your video memory usage would increase 100 times.

Share this comment


Link to comment

The alternative to having a memory management system like this is either memory leaks or invalid pointer deletion crashes.

That's not a fatality. Yes, with people who do not understand how memory management works and do not call delete, this will happen. If you code properly, it won't. In the same spirit, using your newly proposed system will lead to invalid reference count crashes or objects that stay in memory indefinitely when they shouldn't, because people forget to call "IncRefCount" or "Release" or call them too often.

 

 

Also, why do we have the static Load() and Create() methods again vs using new? I know there was a reason but can't seem to remember.

 

Constructors are not passable through a DLL for other languages, I think this was the reason. Also, in languages that require class wrapping (Java, C#, etc.), using static classes allowed to have 'Entity::Create' instead of 'Engine::CreateEntity', which is a nice syntactic sugar.

Share this comment


Link to comment

You do not need to call IncRefCount(). It's just used internally.

 

Yes, with people who do not understand how memory management works and do not call delete, this will happen.

You would effectively be writing your own engine at that level. If I load a model, for example, and then delete the model, it's reasonable to expect the model's materials, textures, and shaders to also get deleted if they are no longer in use.

 

I looked up COM and it's identical to what I am doing. Maybe I will even make the syntax match it exactly.

Share this comment


Link to comment

Since there is no "new" needed, there should be also no "delete" needed. The worst would be if you mix "new" and "delete" with ".Create()" and ".Release()". But since ".Create()" and ".Release()" can have much better error checks, and it looks also much more intuitive than "new" and "delete".

 

I don't want to make a "new" texture, but just load my old texture from disk. Also I don't want to "delete" my texture from disk, but just unload/release it from memory smile.png

 

After all LE3 is an 3D Engine, not a C++ programming language, and it should have context related commands, not C++ commands. And by using engine commands only, all the C++/C#/Java/Lua/etc... game source code would be indentical. You can use whatever language you like, and it's exactly the same source code! That's what I call cross-language compatibility :)

Share this comment


Link to comment

You do not need to call IncRefCount(). It's just used internally.

 

In you example where you say pass the texture to some other user object, then that makes another reference and we would have to call InRefCount() to be correct wouldn't we?

 

I looked up COM and it's identical to what I am doing. Maybe I will even make the syntax match it exactly.

 

People like the idea of what COM gives them, but I think in general people hate programming in it. Which is why they've created wrappers to it for these other languages like VBA, etc.

 

If I load a model, for example, and then delete the model, it's reasonable to expect the model's materials, textures, and shaders to also get deleted if they are no longer in use.

 

I agree with the above for sure, but it sounded like you had that setup already, but it added some classes to your source and you were trying to keep it "clean" for potential buyers of the source. I would assume you'll have more users than buyers so if it was easier for the user before but added classes to the source, then I would think that's the way you want to go.

 

 

I don't know how the internals of LE3 are setup, but some sort of singleton AssetManagement class that manages the instancing of LE objects seems like a good idea at first glance. A map where the key is the filename+path and multiple calls to Load() or new would check if the object is already loaded and handle the instance count for us. If we pass these around to other objects then it's on us to manage it correctly. Just tossing around ideas. It's tough without seeing the entire picture.

 

On a side note, I think Orge has a good model for C++ engine. I don't like the engine itself but it's not because of how it's coded in C++. Something like the below would make more "sense" from a programmer perspective I think:

 

Model* model = gfxDevice->LoadModel("");

 

Ogre uses the scene object to load/create objects but from what I remember the GFX device is really what needs to control this for LE. Either this way or you could pass in the gfxDevice to the ctors to get the functionality. The nice thing about this is that the management of the objects can be handled in the gfxDevice object and they wouldn't be stored globally. Again, not knowing how LE3 is currently setup, I'm guessing the engine objects are stored globally internally which I don't think they need to be.

 

One might think the downside to this is that you have to keep and pass around this gfxDevice object, but I don't see that as a bad thing. Someone can make it global if they like, or pass it around to their game states.

Share this comment


Link to comment
A map where the key is the filename+path and multiple calls to Load() or new would check if the object is already loaded and handle the instance count for us.

That's exactly what it does.

 

If we pass these around to other objects then it's on us to manage it correctly.
So you'll implement your own ref counting system, and then instead of a standard one built into the engine, we'll have a dozen non-standard approaches, all trying to make up for lacking functionality. :\

Share this comment


Link to comment

So you'll implement your own ref counting system, and then instead of a standard one built into the engine, we'll have a dozen non-standard approaches, all trying to make up for lacking functionality. :\

 

When we pass things to LE methods, LE needs to handle it, like it seems it was before you wanted to make the change. If we pass these to our own methods then it's on us. It's then no different than any other pointer to a class we make and pass it around for actual storage inside other classes.

 

How often do we really pass LE objects to be stored in classes vs having the LE objects directly part of a class or just used directly without storage in a method.

 

The one scenario I ran into was passing an Enemy pointer to my Wizard class and storing it there as it's active target. This proved to be very bad since that Enemy could die, and be deleted, outside of that Wizard class (another wizard might have had the same target and killed it). My design was poor. Passing these pointers to be stored in multiple places seems like a poor design on the developers side if they choose to do that. I think this is just inherant to working with C++ and pointers.

 

A similar issue can happen in LE2 can it not? I can create a TTexture, pass it along to 3 different classes to be stored, then delete it in 1 class, and screw the other 2 classes that were using it.

Share this comment


Link to comment

This makes the problems you listed go away. For example this code will only load the texture once, and at the end it will delete the texture:

 

Texture* texture = Texture::Load("myimage.tex");

material->SetTexture(texture);

material2->SetTexture(texture);

texture->Release();

texture = Texture::Load("myimage.tex");

material3->SetTexture(texture);

texture->Release();

material->Release();

material2->Release();

material3->Release();

 

In your entity pointer target example you could have incremented the reference count, used Release() instead of delete, and you would have been safe. (I renamed the IncRefCount() function to AddRef() to match the COM syntax, which uses an identical system).

 

Your wizard update function might have looked like this:

if (activetarget->GetKey("health")<=0)

{

activetarget->Release();

activetarget = NULL;

}

 

Setting the target might be like this:

void Wizard::SetActiveTarget(Entity* entity)

{

if (activetarget) activetarget->Release();// decrement the ref counter of the old one, if it exists

activetarget = entity;

if (activetarget) activetarget->AddRef();// increment the ref counter of the new one so we can hold onto it

}

Share this comment


Link to comment

The Asset class destructor actually looked like this before my changes:

Asset::~Asset()

{

this->assetreference->instancecount--;

if (this->assetreference->instancecount==0) delete this->assetreference;

}

 

So it was really the same thing, just with a lot of extra classes in an attempt to continue using "delete" when it really isn't appropriate.

Share this comment


Link to comment

I guess my point, and the point of why some people hate COM, is that your first example is a mess. It's a nightmare of following/matching AddRef() and Release() function calls, and that's all in one place. As soon as you spread that out into many different classes and functions it gets even harder. The better option for me was to not pass and store the Texture/Wizard pointer in another class at all, but to use an asset management class and store int ID's instead to avoid pointer nightmare.

 

I would just say adding this design will touch every piece of the way to program in C++ (what about Lua and other languages?) and is a pretty big decision. A good number of people dislike having to manually maintain the ref count. From COM wiki:

 

"To facilitate and promote COM development, Microsoft introduced ATL (Active Template Library) for C++ developers. ATL provides for a higher-level COM development paradigm. It also shields COM client application developers from the need to directly maintain reference counting, by providing smart pointer objects."

 

"Certain languages (e.g. Visual Basic) provide automatic reference counting so that COM object developers need not explicitly maintain any internal reference counter in their source codes."

 

There are reasons why MS created the above 2 things. I think they realized that people hated working with aspects of COM, the ref count being one of them. Having to maintain and match up AddRef() with Release() can be a pain.

Share this comment


Link to comment

Smart pointers really do seem interesting. I wouldn't want to have to manage my ref count either.

 

Think of the Lua scripters, who use this language to avoid the complexity of C++. If they don't want to deal with pointers, they certainly wouldn't want to deal with reference count either.

 

They'd want it to "Just work".

Share this comment


Link to comment

All those solutions like smart pointers and garbage collection usually lead to much worse problems:

http://www.codingwis...or-retards.html

 

If you are afraid of managing your memory manually, you should be terrified of debugging memory leaks when memory management is automated. I've done it, and it's a nightmare.

Share this comment


Link to comment

About that article:

 

1) That guy sounds like a real gem.

 

2) That article basically shows and explains why you shouldn't be trying to do what you are Josh.

 

 

His watcher approach is something I've tried also. In my example, right before an enemy pointer was deleted (delete enemy), it would fire an event that the enemy had. When I originally assigned this enemy as a target to a wizard, that wizard would register to this delete event. So now every object that needed to know when this enemy was killed and got deleted would be notified and can then do what it needed to do with it's pointer to the object it had.

 

In the end I didn't go this route though. I decided to make a singleton class that held all the enemies and each enemy had an int ID > 0. When I assigned the target I just passed the int ID to the wizard. Every time in the wizard class I wanted to use the target I would pass the in ID to the singleton GetObject(id) function to get the pointer to the object IF it wasn't deleted. If it was I just got back NULL and then knew that my target was deleted.

 

I actually want to explore the event method more because I think it's more elegant and doesn't require sort of global objects in the game like a singleton is basically doing.

 

The even way however won't work in your situation because that's more setup to tell the object that shares the pointer that the pointer is no longer valid, meaning it has already been deleted and there isn't anything it can do about it. In your case you don't want to actually delete the pointer until everything is finished referencing it.

Share this comment


Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Blog Entries

    • By Josh in Josh's Dev Blog 4
      Here are some screenshots showing more complex interface items scaled at different resolutions. First, here is the interface at 100% scaling:

      And here is the same interface at the same screen resolution, with the DPI scaling turned up to 150%:

      The code to control this is sort of complex, and I don't care. GUI resolution independence is a complicated thing, so the goal should be to create a system that does what it is supposed to do reliably, not to make complicated things simpler at the expense of functionality.
      function widget:Draw(x,y,width,height) local scale = self.gui:GetScale() self.primitives[1].size = iVec2(self.size.x, self.size.y - self.tabsize.y * scale) self.primitives[2].size = iVec2(self.size.x, self.size.y - self.tabsize.y * scale) --Tabs local n local tabpos = 0 for n = 1, #self.items do local tw = self:TabWidth(n) * scale if n * 3 > #self.primitives - 2 then self:AddRect(iVec2(tabpos,0), iVec2(tw, self.tabsize.y * scale), self.bordercolor, false, self.itemcornerradius * scale) self:AddRect(iVec2(tabpos+1,1), iVec2(tw, self.tabsize.y * scale) - iVec2(2 * scale,-1 * scale), self.backgroundcolor, false, self.itemcornerradius * scale) self:AddTextRect(self.items[n].text, iVec2(tabpos,0), iVec2(tw, self.tabsize.y*scale), self.textcolor, TEXT_CENTER + TEXT_MIDDLE) end if self:SelectedItem() == n then self.primitives[2 + (n - 1) * 3 + 1].position = iVec2(tabpos, 0) self.primitives[2 + (n - 1) * 3 + 1].size = iVec2(tw, self.tabsize.y * scale) + iVec2(0,2) self.primitives[2 + (n - 1) * 3 + 2].position = iVec2(tabpos + 1, 1) self.primitives[2 + (n - 1) * 3 + 2].color = self.selectedtabcolor self.primitives[2 + (n - 1) * 3 + 2].size = iVec2(tw, self.tabsize.y * scale) - iVec2(2,-1) self.primitives[2 + (n - 1) * 3 + 3].color = self.hoveredtextcolor self.primitives[2 + (n - 1) * 3 + 1].position = iVec2(tabpos,0) self.primitives[2 + (n - 1) * 3 + 2].position = iVec2(tabpos + 1, 1) self.primitives[2 + (n - 1) * 3 + 3].position = iVec2(tabpos,0) else self.primitives[2 + (n - 1) * 3 + 1].size = iVec2(tw, self.tabsize.y * scale) self.primitives[2 + (n - 1) * 3 + 2].color = self.tabcolor self.primitives[2 + (n - 1) * 3 + 2].size = iVec2(tw, self.tabsize.y * scale) - iVec2(2,2) if n == self.hovereditem then self.primitives[2 + (n - 1) * 3 + 3].color = self.hoveredtextcolor else self.primitives[2 + (n - 1) * 3 + 3].color = self.textcolor end self.primitives[2 + (n - 1) * 3 + 1].position = iVec2(tabpos,2) self.primitives[2 + (n - 1) * 3 + 2].position = iVec2(tabpos + 1, 3) self.primitives[2 + (n - 1) * 3 + 3].position = iVec2(tabpos,2) end self.primitives[2 + (n - 1) * 3 + 3].text = self.items[n].text tabpos = tabpos + tw - 2 end end  
    • By 💎Yue💎 in Dev Log 5
      The prototype of a four-wheeled vehicle is completed, where the third person player can get on and off the vehicle by pressing the E key.  To move the vehicle either forward or backward, is done with the keys W, and the key S, to brake with the space key.  And the principle is the same as when driving the character, a third person camera goes behind the car orbiting 360 degrees.

      I don't think the vehicle is that bad, but I'm absolutely sure it can be improved.  The idea is that this explorer works with batteries, which eventually run out during the night when there is no sunlight.
      Translated with www.DeepL.com/Translator
       
      Mechanics of the game.
      I'm going to focus on the mechanics of the game, establish starting point (Landing area), after the orbiter accident on Mars where all your companions died, now, to survive, you will have to repair your suit, oxygen runs out, good luck.  This involves replacing the oxygen condenser that is failing and the suit is stuck.

      On the ground and performance.
      The rocks, the terrain and the vehicle kill the SPF, but there is a solution, and everything is related to the chassis of the vehicle. That is to say that if I put a simple collision bucket for the vehicle, the yield recovers, something that does not happen if I put a collider of precise calculation for the car. This has the advantage of better performance but is not very accurate, especially when the car crashes with an object in front, because the horn of the car has no collision. And the solution to this, is to put a sliding joint, as was done with the area in which the player climbs the car and descends from it.


       
      On the rocks, I am trying to make them with the slightest polygons and the most distant from each other. 
      Obviously on Mars I can not create canyons, high mountains, is because the terrain does not produce shadows on itself, that's why the terrain tries to be as flat as possible, simulating a desert with dunes. 

      That's all for now.
       
    • By 💎Yue💎 in Dev Log 9
      The prototype is finished, and the mechanics of the game can be given way.  It has established a desert terrain in the form of dunes, this implies that there are no cannons or anything similar, because Leadwerks does not allow a terrain to cast shadows on that same terrain and this looks visually rare.
      So the terrain is like low-slope dunes. On the other hand, I think the texture of the terrain is already the definitive one, with the possibility of changes and suggestions on the part of those involved in this project.
      On the other hand we have taken the model of a habitat of the nasa, which certainly looks very nice. 
      The next steps, are to establish the starting point of the player, this must start near the capsule return to Mars somewhere on the map of 2024 x 2.
      And think about the first thing you should do, repair your suit? Seek a shelter? things like that.  


×
×
  • Create New...