ID:1544634
 
(See the best response by Ter13.)
I'm already starting to regret looking into this, but does anyone know how appearance objects work?
I don't mean the basics, I can add/remove/manipulate overlays just fine.
I mean the internals. Here's a proc I used to get some info on them:

/mob/verb/analyze_appearance()
var/T = null
T = pick(usr.overlays)
if(!T)
world << "pick(usr.overlays) didn't get anything"
return
world << "\blue Basic info on appearance:"
world << "Parent type: [T:parent_type]"
world << "Type: [T:type]"
world << "Tag: [T:tag]"
world << "Vars: [T:vars.len]"
world << "viz:"
for(var/V in T:vars)
world << "[V]"
world << "-----------------------------------------"
return


It gives the following results:

Basic info on overlay thing
Parent type: /datum
Type: /image
Tag:

runtime error: undefined variable /var/vars
proc name: analyze overlay thing (/mob/verb/analyze_overlay_thing)
source file: test.dm,125
usr: Jose Riker (/mob/living/carbon/human)
src: Jose Riker (/mob/living/carbon/human)
call stack:
Jose Riker (/mob/living/carbon/human): analyze overlay thing()

Feeding an /image, /obj, etc (even an /icon) to that proc gets sane results and no runtimes.

I'm mainly interested in this because I'd like to understand BYOND's internal graphics handling better.
Right now, my best guess is that /images and /"appearances" exist because /atom/movable has too much overhead to function as a lightweight graphics object.
/"appearances" are the more extreme class, since apparently they're stripped down to the point where they don't even have a vars list.

Then there's another problem.

To quote the DM reference:
In general, people who do not like reference counting garbage collection should be happy that DM provides a del instruction, allowing you to take charge and delete things whether they are referenced or not.

This is what happens upon attempting to del() an /"appearance":

runtime error: bad del

It seems appearances are an exception to the above principle.
Why aren't they bound by the same rules that govern most other classes?
It's natural that graphic objects should be as minimal as possible, to increase performance and allow freer manipulation.
Is that the goal here, or is there something i'm missing?


Best response
Well, appearances are internal types. They don't function like atoms.

Calling DarkCampaigner, SuperSaiyanX, KaioChao, and Stephen001! These guys probably understand these internal types the best, but I have a pretty solid grasp on them, and their use.

Alright, so basically, BYOND, as a server-side driven online game client, needs a way to communicate visual data to clients.

It works through a sort of appearance registry. Whenever you change these variables of any atom:

alpha
blend_mode
color
icon
icon_state
text
dir
layer
transform

You start manipulating information regarding their appearance.

Essentially, these variables act as a sort of unique hash that identifies each visual appearance generated by the world. These appearances are then communicated upon need to the client for reference by a 3 or 4-byte index.

In other words, whenever I change the visual appearance of any atom in the world, the server hunts for an exact match within the global appearance dictionary. If no exact match is found, it creates a new appearance and provides it with either an abandoned, or new reference number. (When objects are deleted or garbage collected, their reference number is vacated, and each time a new object is created, it has to search the dictionary for the first empty reference.)

Anyway, once this appearance is registered by an internal identifer, it can be communicated to the client.

So the server needs to send a message to the client that is lexically something like this:

ID: APPEARANCE_REGISTER
0: [icon]
1: [icon_state]
2: [text]
3: [alpha]
4: [color]
5: etc

The client receives this information, digs that relevant data out of the rsc if it's not already loaded (/icon objects are actually serialized references to RSC data), then it gets the information ready for rendering in a texture within the graphics context.

Appearances are internally referenced by their ID number by every type of atom. Atoms are defined by what you can see. The client doesn't actually care about any non-visual information regarding atoms, so what the client sees, when it sees something like a turf look like this:

turf
var
alpha
blend_mode
color
contents //a list of client-side movable atoms
dir
icon //an icon reference id
icon_state
underlays //a list of appearances
overlays //a list of appearances
loc //a reference of a /area
layer
name
opacity //not sure about this one
luminosity //not sure about this one
text
transform
mouse_drag_pointer
mouse_drop_pointer
mouse_drop_zone
mouse_opacity
mouse_over_pointer
mouse_pointer_icon


The rest of the variables are pretty much all server-side, stuff the client doesn't really care about.

Anyway, the point is, that appearances reference a unique visual state, and as such, only store information to specific virtual data. They aren't intended to be accessible, but you can abuse casting and the look-up operator to manipulate them.

They also can't be forcibly deleted, because ostensibly, if you are using BYOND from the front-end, you really shouldn't be manipulating back-end stuff like that.

Although, it would be nice to be able to grab an appearance object by id and globally forcibly update appearance data, rather than having to loop through every object using that appearance.
This makes me wonder, how difficult do you think it'd be for Tom/Lummox to allow us to work with appearances as bitmaps (assuming they're stored that way now)?

This would cut out the middlemen (like /icon's, /image's, and many /overlay's) and allow manipulating an atom's graphics directly.
Much faster and more flexible.

Less abstraction inversion, too.
Right now, BYOND provides the advanced tools we want(color, transform, animate(), etc) via its built-in graphics library, but doesn't give us access to the basic ones we need.
It has to be abstracted, though. Without some means of providing code to the client-side to perform bitmap updates, we can't actually manipulate the graphics any better than we currently can using /icon objects.

While yes, it would be nice to be able to specify an appearance-wide override, such as the ability to specify all objects that use appearance 0x00009a11, for instance, to actually display as 0x00009a42. That'd be a neat little rendering quirk. But beyond that, manipulating appearances is at best, quirky, and at worst, straight up dangerous.
Actually, you're probably right about that.
In practice, there's only 2 things missing that I actually need.

One is merging/flattening 2 or more appearances.
The other is bitwise pixel manipulation, e.g. bitmap[y][x] &= 8.

Both can be hacked with /icons and overlays, but the existing methods are so slow and painful they just won't work for any decent graphics system.


In practice, if you need something that permits direct manipulation of the screen buffer in real time... You probably shouldn't be working with BYOND.

There *ARE* ways around it, but none of them are pretty. For instance, you can inject a SWF and send it to the client via a browser, but of course, it can't overlay the screen in any way shape or form, because the browser just plain doesn't support transparency, and probably never will.

BYOND's actually pretty quick processing-wise, but its weakness when it comes to graphical fidelity is simply that its niche is a server-controlled networked game solution with absolutely no option for client-side scripting except for limited Javascript support.

There's a lot of misunderstanding of what BYOND "should" be able to do, and how hard it is to do a lot of things people think that we should be able to do.

It's absolutely true that the Atari ST could do things in the 80s that BYOND simply cannot do in the year 2014. However, in terms of grunt processing power, BYOND could have actually run circles around a PS2 on a modern PC. BYOND, simply put, is intended to do server-driven applications. My feeling is, that BYOND taps out at 200 users on a relatively well written game, no matter what modern hardware you are going to be running. In the end, yes, there's ways around even this, but you will never get past the limitation of being server-driven unless you basically write your own networking protocol using some form of socket communication.

Even then, if you can do that, why bother with BYOND at all? The only thing BYOND provides that's actually worthwhile to the programmer, is a simplistic, one-click networking solution.
True, direct bitmap manipulation is probably not feasible.
Flattening/merging overlays would be very useful, though.
Probably going to make a feature request for it, unless there's a specific reason it wouldn't be doable.
How so, merging overlays, I might ask? There's some quirks you can use depending on what you want to do with it.
Merging as in turning 2+ overlays into one.
There's some situations where an object can get 10+ slapped on, and I figure flattening them might help performance.

Plus it would be nice for manual pixel drawing (using an icon with 1 white pixel).

For example, drawing a scatterplot. My guess is that making and positioning 50 1x1 overlays would be much faster than using the DrawBox() proc. Since they're not likely to change, flattening them down into one would be nice.

Basically, I'm trying to avoid using /icon procs, since they're substantially slower than any other graphics operations, and could create pretty noticeable bottlenecks.

Actually, that's a fair observation.

You actually can merge graphics like this, and then treat them as one object without using icon procs. That's exactly what overlays do.

var/obj/A = new()
var/obj/B = new()
B.icon = 'overlay.dmi'
B.layer = FLOAT_LAYER
for(var/count=0;count<100;count++)
B.pixel_x = count % 10 * ICON_WIDTH
B.pixel_y = round(count / 10) * ICON_HEIGHT
B.icon_state = "[rand(1,5)]"
A.overlays += B
A.screen_loc = "1,1"
usr.client.screen += A


You only need two objects to pull this off, and since every object you have in the client's screen, the client's view, and the client's images list creates a certain amount of overhead, this will actually save you 99 objects per-player-per frame that you would normally have to loop through and update.

There is a slight performance hit when you need to update these objects, though, so you will have to be careful about how many of these objects you batch up at once, because regenerating too large of a batch too often can be pretty costly in both CPU and bandwidth. It's all about finding the right batch sizes, just like in any other game engine that features any kind of static batching approach.

In addition, you can add the A object to another object's overlays list, in effect adding all the overlays of A to that object.
Ter13 wrote:
In addition, you can add the A object to another object's overlays list, in effect adding all the overlays of A to that object.

This is the part i'm interested in.
It's simple to add 50 overlays to something then just move that thing around, but i'm concerned about inefficiency there, since all 50 are managed separately.

Sometimes this is useful: for example, I plan to rely on overlays, Add()/Remove() and a categorized list of references to implement things like mixed piles of poker chips.
For other things (such as the scatterplot), it's unnecessary.
Given the following:

var/obj/A
var/obj/B
var/obj/C

/proc/P1
for(var/x=1 to 100)
B.overlays += A
C.overlays += B.overlays

/proc/P2
for(var/x=1 to 100)
B.overlays += A
C.overlays += B

How many overlays would each proc add to C?
I'm hoping P1 gives it all 100, and P2 gives it only 1.
If that's true, then problem solved.


I'm not sure you can add an overlays list to another overlays list like that.

P1 should be invalid, AFAIK, while P2 should only add a single object to the list. Give P1 a test, though, because I'm not sure exactly how that will come out due to the nature of the overlays lists not actually being normal lists. They are sort of mutable objects, and not lists really.