ID:2081779
 
Welcome back again folks for another rambling post about how not to use DM.

That being said, this is one I'm actually really excited to write. Why? Because Lummox, our glorious dictator has finally decided to squash the need for me to write this installment with a new feature in the much hyped BYOND 511.

What are appearances?

If you've been around a while, you might think that appearances are a new thing to BYOND since almost exactly a year ago.

They aren't though. Appearances have been with us since the beginning. Very few people have ever really figured out what they are and why they do what they do. BYOND is a server-directed engine. This means that the client program has zero actual information about what's going on in a particular game that the server doesn't tell it about.

Now, this is a stark contrast from most game development platforms, where the client makes world pretty and drives the majority of what happens, while the server simply maintains the total state of the simulation without bothering itself with how things actually look.

When you change visual data on an atom, the server actually doesn't like to send the data that you have changed to any watching clients. The server attempts to reduce the total volume of information stored in an appearance to a simple numeric ID. In order for this ID to be of any value, the server has to figure out when the visual properties of one object to another are identical. That way, two objects that have been made to look identical will actually use the same internal appearance object in memory despite the fact that these two objects may not even know about one another at all.

The client is informed about what appearances themselves look like, and which appearances have been gotten rid of or have been newly created within the area of the gameworld it actually is concerned with.

This allows you to make your games pretty without having to worry about resource management for the most part.


The problem

This also has some consequences. It means that appearances are immutable. You can't change an appearance directly because multiple objects may depend on that appearance.

This means that every time you change an appearance-related value, you are actually telling the engine to search every appearance currently in existence to find a match, or if none exists, to create one.

Obviously, you can see the problem here. Big games can have LOTS of appearances in play. This means that what you would think of as some simple code can actually be much less than simple:

icon = 'herp.dmi'
icon_state = "derp"


Internally, this code is going to look something like this:

new_appearance.icon = 'herp.dmi'
var/appearance/match
for(var/appearance/a in appearances)
if(a==new_appearance)
match = a
break
if(!match)
appearance = new_appearance
new_appearance.icon_state = "derp"
for(var/appearance/a in appearances)
if(a==new_appearance)
match = a
break
if(!match)
appearance = new_appearance


If you have any programming experience at all, you'll realize that this is not good-looking, high performance code. Luckily, the engine does do some optimization for us and avoids appearance lookups if an assignment wouldn't actually change the appearance, and also possibly if the appearance only has one reference (I'm not sure about this last part, but it seems to be true.)


How to mitigate this lookup:

This section is reserved for after the release of 511's new appearance mutation feature. This new feature will let us reduce the number of searches on appearance change to exactly 1 no matter how many appearance related variables we change in a row so long as we adopt a new pattern in our code.


Overlays and Underlays

Overlays and underlays are an area where this can currently be mitigated quite a lot. Overlays and Underlays lists are actually internally not lists, but id arrays. Overlays are a list of appearance ids. If you make a change to an atom's overlays/underlays list, every overlay/underlay list in the world is checked for consistency in the same manner as appearances. This reduces the amount of data that BYOND needs to store in active memory a lot, but the sacrifice is CPU.

Recently, while working on Epoch, I spent a lot of time chasing down some inefficiency that was causing the game to take 30 or more seconds to load even on high end machines. The cause was not using more efficient means of changing overlays and underlays.

Every time you add or remove something from id arrays, a similar search happens. So the trick to working with overlays efficiently to avoid more than a single search is to copy the list if you need to work with it multiple times and then make your changes.

//4 churns
overlays += 'herp.dmi'
overlays += 'derp.dmi'
overlays += 'herpy.dmi'
overlays += 'derpy.dmi'


//1 churn
overlays = overlays.Copy()+'herp.dmi'+'derp.dmi'+'herpy.dmi'+'derpy.dmi'


//4 churns
overlays += 'herp.dmi'
overlays -= 'derp.dmi'
overlays -= 'herpy.dmi'
overlays += 'derpy.dmi'


//1 churn
overlays = overlays.Copy()+'herp.dmi'-'derp.dmi'-'herpy.dmi'+'derpy.dmi'



Also, a few common patterns that you can avoid:

//lots of churn
turf
New()
overlays += 'someoverlay.dmi'


//no churn
turf
overlays = list('someoverlay.dmi')


Overlay churn was costing Epoch almost 30 seconds on launch. I'm sure you'll find minimizing churn pretty useful yourself once you understand WHY something as simple as adding things to the overlays list can take a while.
If you have any programming experience at all, you'll realize that this is not good-looking, high performance code.

<--- has some programming experience and thinks the code looks fine :P. Can you explain why its not good looking/ high performance for me please :D? Or link me to where you already explained :D?
new_appearance.icon = 'herp.dmi'
var/appearance/match
for(var/appearance/a in appearances)
if(a==new_appearance)
match = a
break
if(!match)
appearance = new_appearance
new_appearance.icon_state = "derp"
for(var/appearance/a in appearances)
if(a==new_appearance)
match = a
break
if(!match)
appearance = new_appearance


Alright, let's think about this for a minute.

In the above code example, we're trying to get from the current appearance to one with:

{icon='herp.dmi';icon_state="derp;}

The above example searches every appearance in the world for the appearance:

{icon='herp.dmi';}

But we don't care about that appearance, because we're not trying to get there.

We're trying to get to:

{icon='herp.dmi';icon_state="derp;}

So let's improve performance under the assumption we have to search every appearance in the world to find a match in order to get to the appearance we want to become:

new_appearance.icon = 'herp.dmi'
new_appearance.icon_state = "derp"
var/appearance/match
for(var/appearance/a in appearances)
if(a==new_appearance)
match = a
break
if(!match)
appearance = new_appearance


See the difference? We're just not searching for data we don't care about. We're passing over it instead.

That's the reason for this feature request:

http://www.byond.com/forum/?post=2080201

Which Lummox has greenlit some form of it for 511. I'm uncertain on the details of what he's going to be doing for his solution to the problem, but this is essentially one of the major speed limits in BYOND for larger games.

One of the reasons that BYOND can't automatically reduce these searches, is that the engine has no inbuilt method of knowing when the appearance is finalized and should be searched for. BYOND sends as much information to the client as possible the minute it's finalized in the attempt to reduce networking delays. Waiting until the end of the frame would cause delays that don't always have to happen.

So the only solution that we've managed to come up with to the appearance updating problem is:

1) Naively generate a new appearance after each change.

2) Create a syntactic method of indicating that an appearance is ready to be updated.

The feature request I linked to is a proposal for approach number two.
tl;dr do things in batches, which the 511 feature should be wonderful for.
In response to Ter13
Ohhhh okay xD. Only got it after several hours of reading it and trying to make sense of it e.e. xD.
overlays.Add(
'icon1.dmi',
'icon2.dmi',
'icon3.dmi',
'icon4.dmi',
)


I typically use the above method when working with multiple items. This would be a single churn, right?

This topic explains a lot about problems I've had with game loading times in the distant past. I haven't been following the 511 talk very closely, but I'm interested in seeing what comes of the appearance handling solution.
This would be a single churn, right?

I'm not entirely sure. The elements may be added one at a time on the backend causing four churns.
So for shit like this
overlays -= image.appearance

if(image.icon != normalMap) image.icon = normalMap
if(image.color != nColor) image.color = nColor

overlays += image.appearance

You're saying CPU performance or network performance or both would be significantly improved if I had changed it to...
var/tempAppearance = image.appearance
var/appearanceChanged = 0

if(I.icon != normalMap)
image.icon = normalMap
appearanceChanged = 1

if(I.color != nColor)
image.color = nColor
appearanceChanged = 1

if(appearanceChanged)
overlays = overlays.Copy() - tempAppearance + image.appearance

To reduce it from 2 churns to 1
CPU yes, network possibly.
Hm, subtraction from the copy doesn't seem to work for me, even when the appearance is the same. It never did. Which is unfortunate, because I have to do my removals in a separate churn than my additions.
You need to subtract the appearance, not the object.

Copying the overlays list does not restore the text, icons, or objects. The overlays list is always stored as appearances.

So if you want to remove it with single churn, you need to store atom.appearance or access the overlays list after adding the element if using a string or an icon:

overlays += object
stored_appearance = overlays[overlays.len]


ID_ARRAY only stores appearance references, not objects, strings, and icons. ID_ARRAY.Copy() creates a /list. Lists do not obey the same conversion rule as an ID_ARRAY on add/subtract.
Yeah, that's what I meant. I subtract the appearance, yet nothing happens. Though instead, I used image(icon) and then took image.appearance. I can deal with 2 churns, though. I guess I could store the appearance like that in a member variable, if I need some extra CPU time. Ex:
overlays += weapon.cleanIcon //cleanIcon == 'knife.dmi'
weapon.cleanIconAppearance = overlays[overlays.len]
//Then later...
overlays = overlays.Copy()-weapon.cleanIconAppearance //Plus others, of course.

Are you sure you are subtracting the appearance you added? This could be easily tested by outputting "\ref[object.appearance]" as a test case to ensure that the appearance is in fact not changing.

There will never be a case where when stored the appearance will mutate.

So what you are describing as happening simply can't happen. Unless you aren't either storing the appearance or you aren't actually subtracting an appearance.
See my edited post. Also, does KEEP_TOGETHER make a difference? I'm using that.
Did you insert debug messages?

"\ref[weapon.cleanIconAppearance] added"

"\ref[weapon.cleanIconAppearance] removed"

That will tell you if it changed or not.

KEEP_TOGETHER should not make a difference. It affects rendering on the client, not actual ID_ARRAY storage.
Yeah, I fixed it using the code I posted previously. Thanks for your time.
What was wrong, out of curiosity?

I'm actually interested to know why it wasn't subtracting the appearance.
I have no clue, actually.
It fixed it when I started taking the appearance from the overlays list itself and storing that instead.
It fixed it when I started taking the appearance from the overlays list itself and storing that instead.

What were you doing before the fixed code? (I'd like to see the old code and the debug messages if you would.) Because understanding where you went wrong is the best way to avoid going wrong again in the future. Fixing something and having no clue why it works is a common thing in programming, but it's a bad thing for the programmer.
Well, I know why it works (I always make sure I do before I go on), but there's no easily accessible way for me to tell why the other way was not working because I don't have access to what's going on under the hood. The appearances were different (in the overlays list vs in the image), but I did nothing to make them different, so that's the basis of it.
Page: 1 2