How To Use the 'New' Renderer

From MAMEDEV Wiki
Revision as of 15:35, 25 June 2008 by Madskunk (talk | contribs) (New page: Since I've had a few questions asked recently about how the new renderer works from an OSD developer's point of view, I figured this would be a good chance to kickstart things again. Ther...)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Since I've had a few questions asked recently about how the new renderer works from an OSD developer's point of view, I figured this would be a good chance to kickstart things again.

There are really two ways to use the new renderer. (Well, there are really an infinite number, but these two ways were what motivated the feature set as it exists today.) Both ways will produce similar results, and both have their uses.

The first thing to understand is the concept of a render_target. A render_target is really a rendering context. It is an object that holds all the information about how you would like the renderer to assemble the primitives that make up the final result. From the OSD perspective, this is really the only object you need to talk to. You can allocate as many render_targets as you like; in the Windows OSD code, I allocate one per window. The MAME core also allocates one internally for taking snapshots.

There are a number of knobs you can twiddle on each render_target. You can enumerate all of the views available for each target, find out which game screens are present on each view, and select which one to use. You can specify the orientation of the target, which rotates all of the primitives in the appropriate direction. You can specify flags that control which types of artwork are visible. You can tell the renderer what the maximum texture size is. You can ask it to compute the smallest size that will ensure each game screen pixel maps to at least one output pixel. And most importantly, you can specify the size or bounds of the target.

How you specify the bounds of a render_target fundamentally determines how you are using the renderer. And here is where the two approaches differ. The first approach is to tell the render_target the absolute truth about what it is drawing to. This entails calling render_target_set_bounds with the actual width, height, and pixel aspect ratio of the screen you are drawing to. Thus, if the output is a 1600x1200 monitor with square pixels, you would call render_target_set_bounds(1600, 1200, 1.0). This works well if your final result will be hardware accelerated. Under the covers, the renderer will assemble all the pieces to be placed within the 1600x1200 area you specified, and -- here's the key -- it will perform a high-quality resampling of any artwork to the final output resolution. This is much better quality and much more efficient than simply uploading the entire artwork as a giant texture and letting your video card filter it. Because it's not expected that you will change this size too often, the expensive overhead of resampling the artwork is usually just taken once. In contrast, the game screen textures are not resampled in this way; rather, they are always uploaded at their native resolution and your video card is expected to do the resampling.

This first approach is what you get when you run the Windows build with -video d3d or -video gdi. This is also why -video gdi is incredibly slow unless it's in a tiny window: all of that pixel pushing at full resolution is being done in software, and it ain't cheap.

The second approach is to lie to the render_target about what it is drawing to. The usual way to do this is to compute the minimum size via the render_target_get_minimum_size function, and then pass that width and height (or some integral multiple of such) into render_target_set_bounds as the target size. What happens in this case is that, as far as the renderer knows, you are drawing to a low resolution output device. It will still do high quality artwork resampling, but just to a much lower resolution. Why would you do this? The main motivation is to support limited or non-existant hardware acceleration cases.

Consider a situation where you can use your graphics hardware to accelerate only a final bitmap scaling operation, but none of the more advanced blending features required to assemble a full image. In this case, you could fall back to drawing everything in software at full resolution, but it is going to be very slow. The alternative is to lie about the actual resolution. Instead, allocate a buffer at the render_target's minimum size, draw to that buffer, and then use the graphics hardware to scale that buffer to full screen.

This second approach is what you get when you run with Windows build with -video ddraw. And in fact, the second approach produces output that is almost identical to what MAME produced before the new renderer went in, with the addition of more flexible features.

One thing you'll notice if you look at the Windows code is that it ends up calling render_target_set_bounds each frame, immediately before calling render_target_get_primitives. The main reason for this is that the Windows system lives in a dynamic environment. We support live resizing of windows in windowed mode, which means the output resolution can change at any time when using the first method. Furthermore, games in MAME can dynamically change their resolution, and users are free to change the currently selected view, meaning that the minimum size can change from frame to frame within a game when we use the second method. For these reasons, it is just simpler to update the output size each frame before requesting the primitives so that we ensure we're in sync.