<< Newer | Article #176 | Older >> |
A Peek Inside the New Renderer (part 2)
In Part 1 of this article, I explained all about the three core objects in the new renderer: the render_target, the render_container, and the layout_view. In this part, I'll tackle how the final collection of things to render is arrived at, based on all of this information.
As I mentioned before, the layout_view is essentially a description of how to position the various render_containers amongst external artwork elements within the confines of the render_target. In order to do this, we need to introduce the concept of an object_transform. In a fully 3D world, a transformation is usually a matrix, and can describe rotation, translation, and scaling. Although the new renderer handles all three parts of a 3D transformation, it is still strictly a 2D engine. So to keep things simple, an object_transform describes a simple 2D translation (2 parameters: X and Y offset), a simple 2D scaling (2 parameters: X and Y scaling), and basic rotation (orientation described via X flip, Y flip, and X/Y swap). In addition, an object_transform also contains a color factor (4 parameters: red, green, blue, and alpha).
In order to assemble the list of items to render, the OSD layer calls render_target_get_primitive_list().
This is different compared to the old way of operating.
In the old model, when MAME had something that needed to be
drawn, it called to the OS-dependent code (OSD layer) and handed it everything
it needed to update the video. In the new system, it is the OSD layer who
is responsible for calling back to the renderer for the list of items to
render for a target. Usually this is done in response to the more generic
osd_update() call which is used as a signal to indicate that something
interesting has happened.
Anyway, render_target_get_primitive_list() returns a list of render_primitive objects, which constitute a description of what to render. Each render_primitive object specifies a type, a set of coordinates, a color, and a set of flags, along with some additional type-specific data. At this time there are only two types of primitives that will ever be returned: lines and quads.
A line primitive is simply two points that should be rendered with a line drawn between them, using the color, alpha, and width specified. Points can be drawn by specifying a line with equal start and end points. Lines are used mostly for drawing the user interface and for vector games (though that may change in the future).
A quad primitive is also specified by two points: a top, left coordinate and a bottom, right coordinate. This limits MAME to specifying rectilinear quads, so there is currently no way to do anything fancy or 3D with the system. (Believe it or not, this is intentional.) Quads can be either textured or non-textured. Non-textured quads are rendered as a solid color using the specified color and alpha values. Textured quads are rendered using the specified texture, with the color and alpha values providing modulation of the texture data.
Common to both primitives is a set of rendering flags. These flags control how the primitive is to be blended against previously-rendered graphics on the screen, among other details. The important thing to keep in mind is that the primitives are provided in back-to-front rendering order. This means that the system that does the final rendering should turn off all Z buffering and simply render each primitive in order. Another interesting tidbit is that the screen does not need to be explicitly cleared before drawing because the primitive list contains non-textured quads that erase any relevant areas of the screen before drawing them.
The easiest way to understand how it all comes together is to walk through an example. Let's say you're running Space Invaders, and you have selected a layout_view that includes a backdrop, an overlay, and a bezel, as well as the screen. Let's also say that you've configured MAME to draw a special effect overlay on top of the screen graphics, just to throw everything in together. Now what?
Well, internally, MAME has examined the layout_view and grouped all of its elements into one of four categories: backdrop, overlay, screen, and bezel. Once that is done, it then starts with an empty list of primitives, and goes through each layer one at a time, adding primitives to the end of the list. The trick is that ordering is very important to get all of the effects to mesh together.
The first thing that gets drawn is the screen layer. I know you're asking, what? Why aren't you drawing the backdrop layer first? It's in back! Well, the screen layer needs to be rendered first in order to apply overlay effects before the screens are added to the backdrop; otherwise, you get ugly results as the overlay effects are applied to the backdrop graphics as well.
In order to draw the screen layer, the renderer loops over all the screen elements that were specified in the layout, and copies the render_container contents for that screen to the end of the primitive list. A couple of notes about this. First, what does a render_container actually contain? Well, it contains objects that are called container_items. These items are kind of like primitives, but they are less primitive than the render_primitives that we've been looking at so far. Container_items can be lines, quads, or characters. Another important thing to understand about render_containers is that they have an internal coordinate system that goes from 0.0-1.0 along both axes. This is where the object_transforms that I mentioned above come in. Since a screen element in a layout_view can position the actual contents of the screen anywhere within the view, we need to know how to transform the container_items from their basic coordinate system to the final screen coordinates. To do this, we build an object_transform describing that mapping, and apply the object_transform to each line, quad, or character we encounter in the screen's render_container. Note that during this process, characters get translated into quad render_pritimives.
As container_items within a render_container are being converted into render_primitives, they are added to the end of current working list of primitives. Along the way, they are set to render with an additive blending mode. This means that the RGB values of the lines/quads/characters are added to the RGB values of what is already there. In most cases, what is already there is just black, so it doesn't make much of a difference.
Once each screen's render_container has been added to the working list, the special effect overlay is applied. This is simply a replicated copy of a texture that is rendered on top of the screen area using a multiplicative RGB blend. This means that the RGB values of the overlay texture are multiplied by the RGB values of what has already been rendered. This allows the overlay texture to control how much red, green, or blue from the final screen is actually shown.
Having added the screens' containers to the render list, the next step is to add the graphical overlays that are specified by the layout_view. These are simply bitmaps that are described by the layout XML, which I covered in a previous article. Like the effect overlays, these are rendered with multiplicative RGB blending. The easiest way to think about it is with vector games. Black and white vector games only drew white lines. White is R=G=B=1.0, while black is R=G=B=0.0. When you multiply the pixels of an overlay by these values, you will get black in the areas where nothing is drawn, and the overlay color in the areas where what was drawn.
Now that we have the screens and overlays assembled, it is time to add the backdrops. Backdrops and screen data are combined using additive RGB blending again. This is why you can draw the backdrops after the screens. If you remember in school, addition is commutative (a+b = b+a), so it doesn't matter which you draw first, and which you add on top of it. The main issue to be aware of is if there are overlapping backdrop elements, they will be added to each other. Right now, Golly Ghost uses overlapping backdrop elements, so as a workaround, if we find multiple backdrop elements, we draw the backdrop first and add the screens second.
Finally, we add the bezels. Bezels are drawn using a standard alpha blend, which means each pixel has an alpha component that controls how much of the previously rendered stuff shows through.
Now we have an ordered list of everything to draw. The OSD layer runs through this list and draws things in order, displaying the final result. And I think that's enough for one article. Thanks for hanging in there!