Software 3D Rendering in MAME ============================= .. contents:: :local: Background ---------- Beginning in the late 1980s, many arcade games began incorporating hardware-rendered 3D graphics into their video. These 3D graphics are typically rendered from low-level primitives into a frame buffer (usually double- or triple-buffered), then perhaps combined with traditional tilemaps or sprites, before being presented to the player. When it comes to emulating 3D games, there are two general approaches. The first approach is to leverage modern 3D hardware by mapping the low-level primitives onto modern equivalents. For a cross-platform emulator like MAME, this requires having an API that is flexible enough to describe the primitives and all their associated behaviors with high accuracy. It also requires the emulator to be able to read back from the rendered frame buffer (since many games do this) and combine it with other elements, in a way that is properly synchronized with background rendering. The alternative approach is to render the low-level primitives directly in software. This has the advantage of being able to achieve pretty much any behavior exhibited by the original hardware, but at the cost of speed. In MAME, since all emulation happens on one thread, this is particularly painful. However, just as with the 3D hardware approach, in theory a software-based approach could be spun off to other threads to handle the work, as long as mechanisms were present to synchronize when necessary, for example, when reading/writing directly to/from the frame buffer. For the time being, MAME has opted for the second approach, leveraging a templated helper class called **poly_manager** to handle common situations. Concepts -------- At its core, **poly_manager** is a mechanism to support multi-threaded rendering of low-level 3D primitives. Callers provide **poly_manager** with a set of *vertices* for a primitive plus a *render callback*. **poly_manager** breaks the primitive into clipped scanline *extents* and distributes the work among a pool of *worker threads*. The render callback is then called on the worker thread for each extent, where game-specific logic can do whatever needs to happen to render the data. One key responsibility that **poly_manager** takes care of is ensuring order. Given a pool of threads and a number of work items to complete, it is important that—at least within a given scanline—all work is performed serially in order. The basic approach is to assign each extent to a *bucket* based on the Y coordinate. **poly_manager** then ensures that only one worker thread at a time is responsible for processing work in a given bucket. Vertices in **poly_manager** consist of simple 2D X and Y *coordinates*, plus zero or more additional *iterated parameters*. These iterated parameters can be anything: intensity values for lighting; RGB(A) colors for Gouraud shading; normalized U, V coordinates for texture mapping; 1/Z values for Z buffering; etc. Iterated parameters, regardless of what they represent, are interpolated linearly across the primitive in screen space and provided as part of the extent to the render callback. ObjectType ~~~~~~~~~~ When creating a **poly_manager** class, you must provide it a special type that you define, known as **ObjectType**. Because rendering happens asynchronously on worker threads, the idea is that the **ObjectType** class will hold a snapshot of all the relevant data needed for rendering. This allows the main thread to proceed—potentially modifying some of the relevant state—while rendering happens elsewhere. In theory, we could allocate a new **ObjectType** class for each primitive rendered; however, that would be rather inefficient. It is quite common to set up the rendering state and then render several primitives using the same state. For this reason, **poly_manager** maintains an internal array of **ObjectType** objects and keeps a copy of the last **ObjectType** used. Before submitting a new primitive, callers can see if the rendering state has changed. If it has, it can ask **poly_manager** to allocate a new **ObjectType** class and fill it in. When the primitive is submitted for rendering, the most recently allocated **ObjectType** instance is implicitly captured and provided to the render callbacks. For more complex scenarios, where data might change even more infrequently, there is a **poly_array** template, which can be used to manage data in a similar way. In fact, internally **poly_manager** uses the **poly_array** class to manage its **ObjectType** allocations. More information on the **poly_array** class is provided later. Primitives ~~~~~~~~~~ **poly_manager** supports several different types of primitives: * The most commonly-used primitive in **poly_manager** is the *triangle*, which has the nice property that iterated parameters have constant deltas across the full surface. Arbitrary-length *triangle fans* and *triangle strips* are also supported. * In addition to triangles, **poly_manager** also supports *polygons* with an arbitrary number of vertices. The list of vertices is expected to be in either clockwise or anticlockwise order. **poly_manager** will walk the edges to compute deltas across each extent. * As a special case, **poly_manager** supports a *tile* primitive, which is a simple quad defined by two vertices, a top-left vertex and a bottom-right vertex. Like triangles, tiles have constant iterated parameter deltas across their surface. * Finally, **poly_manager** supports a fully custom mechanism where the caller provides a list of extents that are more or less fed directly to the worker threads. This is useful if emulating a system that has unusual primitives or requires highly specific behaviors for its edges. Synchronization ~~~~~~~~~~~~~~~ One of the key requirements of providing an asynchronous rendering mechanism is synchronization. Synchronization in **poly_manager** is super simple: just call the ``wait()`` function. There are several common reasons for issuing a wait: * At display time, the pixel data must be copied to the screen. If any primitives were queued which touch the portion of the display that is going to be shown, you need to wait for rendering to be complete before copying. Note that this wait may not be strictly necessary in some situations (for example, a triple-buffered system). * If the emulated system has a mechanism to read back from the framebuffer after rendering, then a wait must be issued prior to the read in order to ensure that asynchronous rendering is complete. * If the emulated system modifies any state that is not cached in the **ObjectType** or elsewhere (for example, texture memory), then a wait must be issued to ensure that pending primitives which might consume that state have finished their work. * If the emulated system can use a previous render target as, say, the texture source for a new primitive, then submitting the second primitive must wait until the first completes. **poly_manager** provides no internal mechanism to help detect this, so it is on the caller to determine when or if this is necessary. Because the wait operation knows after it is done that all rendering is complete, **poly_manager** also takes this opportunity to reclaim all memory allocated for its internal structures, as well as memory allocated for **ObjectType** structures. Thus it is important that you don’t hang onto any **ObjectType** pointers after a wait is called. The poly_manager class ---------------------- In most applications, **poly_manager** is not used directly, but rather serves as the base class for a more complete rendering class. The **poly_manager** class itself is a template:: template class poly_manager; and the template parameters are: * **BaseType** is the type used internally for coordinates and iterated parameters, and should generally be either ``float`` or ``double``. In theory, a fixed-point integral type could also be used, though the math logic has not been designed for that, so you may encounter problems. * **ObjectType** is the user-defined per-object data structure described above. Internally, **poly_manager** will manage a **poly_array** of these, and a pointer to the most-recently allocated one at the time a primitive is submitted will be implicitly passed to the render callback for each corresponding extent. * **MaxParams** is the maximum number of iterated parameters that may be specified in a vertex. Iterated parameters are generic and treated identically, so the mapping of parameter indices is completely up to the contract between the caller and the render callback. It is permitted for **MaxParams** to be 0. * **Flags** is zero or more of the following flags: - POLY_FLAG_NO_WORK_QUEUE — specify this flag to disable asynchronous rendering; this can be useful for debugging. When this option is enabled, all primitives are queued and then processed in order on the calling thread when ``wait()`` is called on the **poly_manager** class. - POLY_FLAG_NO_CLIPPING — specify this if you want **poly_manager** to skip its internal clipping. Use this if your render callbacks do their own clipping, or if the caller always handles clipping prior to submitting primitives. Types & Constants ~~~~~~~~~~~~~~~~~ vertex_t ++++++++ Within the **poly_manager** class, you’ll find a **vertex_t** type that describes a single vertex. All primitive drawing methods accept 2 or more of these **vertex_t** objects. The **vertex_t** includes the X and Y coordinates along with an array of iterated parameter values at that vertex:: struct vertex_t { vertex_t() { } vertex_t(BaseType _x, BaseType _y) { x = _x; y = _y; } BaseType x, y; // X, Y coordinates std::array p; // iterated parameters }; Note that **vertex_t** itself is defined in terms of the **BaseType** and **MaxParams** template values of the owning **poly_manager** class. All of **poly_manager**’s primitives operate in screen space, where (0,0) represents the top-left corner of the top-left pixel, and (0.5,0.5) represents the center of that pixel. Left and top pixel values are inclusive, while right and bottom pixel values are exclusive. Thus, a *tile* rendered from (2,2)-(4,3) will completely cover 2 pixels: (2,2) and (3,2). When calling a primitive drawing method, the iterated parameter array **p** need not be completely filled out. The number of valid iterated parameter values is specified as a template parameter to the primitive drawing methods, so only that many parameters need to actually be populated in the **vertex_t** structures that are passed in. extent_t ++++++++ **poly_manager** breaks primitives into extents, which are contiguous horizontal spans contained within a single scanline. These extents are then distributed to worker threads, who will call the render callback with information on how to render each extent. The **extent_t** type describes one such extent, providing the bounding X coordinates along with an array of iterated parameter start values and deltas across the span:: struct extent_t { struct param_t { BaseType start; // parameter value at start BaseType dpdx; // dp/dx relative to start }; int16_t startx, stopx; // starting (inclusive)/ending (exclusive) endpoints std::array param; // array of parameter start/deltas void *userdata; // custom per-span data }; For each iterated parameter, the **start** value contains the value at the left side of the span. The **dpdx** value contains the change of the parameter’s value per X coordinate. There is also a **userdata** field in the **extent_t** structure, which is not normally used, except when performing custom rendering. render_delegate +++++++++++++++ When rendering a primitive, in addition to the vertices, you must also provide a **render_delegate** callback of the form:: void render(int32_t y, extent_t const &extent, ObjectType const &object, int threadid) This callback is responsible for the actual rendering. It will be called at a later time, likely on a different thread, for each extent. The parameters passed are: * **y** is the Y coordinate (scanline) of the current extent. * **extent** is a reference to a **extent_t** structure, described above, which specifies for this extent the start/stop X values along with the start/delta values for each iterated parameter. * **object** is a reference to the most recently allocated **ObjectType** at the time the primitive was submitted for rendering; in theory it should contain most of not all of the necessary data to perform rendering. * **threadid** is a unique ID indicating the index of the thread you’re running on; this value is useful if you are keeping any kind of statistics and don’t want to add contention over shared values. In this situation, you can allocate **WORK_MAX_THREADS** instances of your data and update the instance for the **threadid** you are passed. When you want to display the statistics, the main thread can accumulate and reset the data from all threads when it’s safe to do so (e.g., after a wait). Methods ~~~~~~~ poly_manager ++++++++++++ :: poly_manager(running_machine &machine); The **poly_manager** constructor takes just one parameter, a reference to the **running_machine**. This grants **poly_manager** access to the work queues needed for multithreaded running. wait ++++ :: void wait(char const *debug_reason = "general"); Calling ``wait()`` stalls the calling thread until all outstanding rendering is complete: * **debug_reason** is an optional parameter specifying the reason for the wait. It is useful if the compile-time constant **TRACK_POLY_WAITS** is enabled, as it will print a summary of wait times and reasons at the end of execution. **Return value:** none. object_data +++++++++++ :: objectdata_array &object_data(); This method just returns a reference to the internally-maintained **poly_array** of the **ObjectType** you specified when creating **poly_manager**. For most applications, the only interesting thing to do with this object is call the ``next()`` method to allocate a new object to fill out. **Return value:** reference to a **poly_array** of **ObjectType**. register_poly_array +++++++++++++++++++ :: void register_poly_array(poly_array_base &array); For advanced applications, you may choose to create your own **poly_array** objects to manage large chunks of infrequently-changed data, such a palettes. After each ``wait()``, **poly_manager** resets all the **poly_array** objects it knows about in order to reclaim all outstanding allocated memory. By registering your **poly_array** objects here, you can ensure that your arrays will also be reset after an ``wait()`` call. **Return value:** none. render_tile +++++++++++ :: template uint32_t render_tile(rectangle const &cliprect, render_delegate callback, vertex_t const &v1, vertex_t const &v2); This method enqueues a single *tile* primitive for rendering: * **ParamCount** is the number of live values in the iterated parameter array within each **vertex_t** provided; it must be no greater than the **MaxParams** value specified in the **poly_manager** template instantiation. * **cliprect** is a reference to a clipping rectangle. All pixels and parameter values are clipped to stay within these bounds before being added to the work queues for rendering, unless **POLY_FLAG_NO_CLIPPING** was specified as a flag parameter to **poly_manager**. * **callback** is the render callback delegate that will be called to render each extent. * **v1** contains the coordinates and iterated parameters for the top-left corner of the tile. * **v2** contains the coordinates and iterated parameters for the bottom-right corner of the tile. **Return value:** the total number of clipped pixels represented by the enqueued extents. render_triangle +++++++++++++++ :: template uint32_t render_triangle(rectangle const &cliprect, render_delegate callback, vertex_t const &v1, vertex_t const &v2, vertex_t const &v3); This method enqueues a single *triangle* primitive for rendering: * **ParamCount** is the number of live values in the iterated parameter array within each **vertex_t** provided; it must be no greater than the **MaxParams** value specified in the **poly_manager** template instantiation. * **cliprect** is a reference to a clipping rectangle. All pixels and parameter values are clipped to stay within these bounds before being added to the work queues for rendering, unless **POLY_FLAG_NO_CLIPPING** was specified as a flag parameter to **poly_manager**. * **callback** is the render callback delegate that will be called to render each extent. * **v1**, **v2**, **v3** contain the coordinates and iterated parameters for each vertex of the triangle. **Return value:** the total number of clipped pixels represented by the enqueued extents. render_triangle_fan +++++++++++++++++++ :: template uint32_t render_triangle_fan(rectangle const &cliprect, render_delegate callback, int numverts, vertex_t const *v); This method enqueues one or more *triangle* primitives for rendering, specified in fan order: * **ParamCount** is the number of live values in the iterated parameter array within each **vertex_t** provided; it must be no greater than the **MaxParams** value specified in the **poly_manager** template instantiation. * **cliprect** is a reference to a clipping rectangle. All pixels and parameter values are clipped to stay within these bounds before being added to the work queues for rendering, unless **POLY_FLAG_NO_CLIPPING** was specified as a flag parameter to **poly_manager**. * **callback** is the render callback delegate that will be called to render each extent. * **numverts** is the total number of vertices provided; it must be at least 3. * **v** is a pointer to an array of **vertex_t** objects containing the coordinates and iterated parameters for all the triangles, in fan order. This means that the first vertex is fixed. So if 5 vertices are provided, indicating 3 triangles, the vertices used will be: (0,1,2) (0,2,3) (0,3,4) **Return value:** the total number of clipped pixels represented by the enqueued extents. render_triangle_strip +++++++++++++++++++++ :: template uint32_t render_triangle_strip(rectangle const &cliprect, render_delegate callback, int numverts, vertex_t const *v); This method enqueues one or more *triangle* primitives for rendering, specified in strip order: * **ParamCount** is the number of live values in the iterated parameter array within each **vertex_t** provided; it must be no greater than the **MaxParams** value specified in the **poly_manager** template instantiation. * **cliprect** is a reference to a clipping rectangle. All pixels and parameter values are clipped to stay within these bounds before being added to the work queues for rendering, unless **POLY_FLAG_NO_CLIPPING** was specified as a flag parameter to **poly_manager**. * **callback** is the render callback delegate that will be called to render each extent. * **numverts** is the total number of vertices provided; it must be at least 3. * **v** is a pointer to an array of **vertex_t** objects containing the coordinates and iterated parameters for all the triangles, in strip order. So if 5 vertices are provided, indicating 3 triangles, the vertices used will be: (0,1,2) (1,2,3) (2,3,4) **Return value:** the total number of clipped pixels represented by the enqueued extents. render_polygon ++++++++++++++ :: template uint32_t render_polygon(rectangle const &cliprect, render_delegate callback, vertex_t const *v); This method enqueues a single *polygon* primitive for rendering: * **NumVerts** is the number of vertices in the polygon. * **ParamCount** is the number of live values in the iterated parameter array within each **vertex_t** provided; it must be no greater than the **MaxParams** value specified in the **poly_manager** template instantiation. * **cliprect** is a reference to a clipping rectangle. All pixels and parameter values are clipped to stay within these bounds before being added to the work queues for rendering, unless **POLY_FLAG_NO_CLIPPING** was specified as a flag parameter to **poly_manager**. * **callback** is the render callback delegate that will be called to render each extent. * **v** is a pointer to an array of **vertex_t** objects containing the coordinates and iterated parameters for the polygon. Vertices are assumed to be in either clockwise or anticlockwise order. **Return value:** the total number of clipped pixels represented by the enqueued extents. render_extents ++++++++++++++ :: template uint32_t render_extents(rectangle const &cliprect, render_delegate callback, int startscanline, int numscanlines, extent_t const *extents); This method enqueues custom extents directly: * **ParamCount** is the number of live values in the iterated parameter array within each **vertex_t** provided; it must be no greater than the **MaxParams** value specified in the **poly_manager** template instantiation. * **cliprect** is a reference to a clipping rectangle. All pixels and parameter values are clipped to stay within these bounds before being added to the work queues for rendering, unless **POLY_FLAG_NO_CLIPPING** was specified as a flag parameter to **poly_manager**. * **callback** is the render callback delegate that will be called to render each extent. * **startscanline** is the Y coordinate of the first extent provided. * **numscanlines** is the number of extents provided. * **extents** is a pointer to an array of **extent_t** objects containing the start/stop X coordinates and iterated parameters. The **userdata** field of the source extents is copied to the target as well (this field is otherwise unused for all other types of rendering). **Return value:** the total number of clipped pixels represented by the enqueued extents. zclip_if_less +++++++++++++ :: template int zclip_if_less(int numverts, vertex_t const *v, vertex_t *outv, BaseType clipval); This method is a helper method to clip a polygon against a provided Z value. It assumes that the first iterated parameter in **vertex_t** represents the Z coordinate. If any edge crosses the Z plane represented by **clipval** that edge is clipped. * **ParamCount** is the number of live values in the iterated parameter array within each **vertex_t** provided; it must be no greater than the **MaxParams** value specified in the **poly_manager** template instantiation. * **numverts** is the number of vertices in the input array. * **v** is a pointer to the input array of **vertex_t** objects. * **outv** is a pointer to the output array of **vertex_t** objects. **v** and **outv** cannot overlap or point to the same memory. * **clipval** is the value to compare parameter 0 against for clipping. **Return value:** the number of output vertices written to **outv**. Note that by design it is possible for this method to produce more vertices than the input array, so callers should ensure there is enough room in the output buffer to accommodate this. Example Renderer ---------------- Here is a complete example of how to create a software 3D renderer using **poly_manager**. Our example renderer will only handle flat and Gouraud-shaded triangles with depth (Z) buffering. Types ~~~~~ The first thing we need to define is our *externally-visible* vertex format, which is distinct from the internal **vertex_t** that **poly_manager** will define. In theory you could use **vertex_t** directly, but the generic nature of **poly_manager**’s iterated parameters make it awkward:: struct example_vertex { float x, y, z; // X,Y,Z coordinates rgb_t color; // color at this vertex }; Next we define the **ObjectType** needed by **poly_manager**. For our simple case, we define an **example_object_data** struct that consists of pointers to our rendering buffers, plus a couple of fixed values that are consumed in some cases. More complex renderers would typically have many more object-wide parameters defined here:: struct example_object_data { bitmap_rgb32 *dest; // pointer to the rendering bitmap bitmap_ind16 *depth; // pointer to the depth bitmap rgb_t color; // overall color (for clearing and flat shaded case) uint16_t depthval; // fixed depth v alue (for clearing) }; Now it’s time to define our renderer class, which we derive from **poly_manager**. As template parameters we specify ``float`` as the base type for our data, since that will be enough accuracy for this example, and we also provide our **example_object_data** as the **ObjectType** class, plus the maximum number of iterated parameters our renderer will ever need (4 in this case):: class example_renderer : public poly_manager { public: example_renderer(running_machine &machine, uint32_t width, uint32_t height); bitmap_rgb32 *swap_buffers(); void clear_buffers(rgb_t color, uint16_t depthval); void draw_triangle(example_vertex const *verts); private: static uint16_t ooz_to_depthval(float ooz); void draw_triangle_flat(example_vertex const *verts); void draw_triangle_gouraud(example_vertex const *verts); void render_clear(int32_t y, extent_t const &extent, example_object_data const &object, int threadid); void render_flat(int32_t y, extent_t const &extent, example_object_data const &object, int threadid); void render_gouraud(int32_t y, extent_t const &extent, example_object_data const &object, int threadid); int m_draw_buffer; bitmap_rgb32 m_display[2]; bitmap_ind16 m_depth; }; Constructor ~~~~~~~~~~~ The constructor for our example renderer just initializes **poly_manager** and allocates the rendering and depth buffers:: example_renderer::example_renderer(running_machine &machine, uint32_t width, uint32_t height) : poly_manager(machine), m_draw_buffer(0) { // allocate two display buffers and a depth buffer m_display[0].allocate(width, height); m_display[1].allocate(width, height); m_depth.allocate(width, height); } swap_buffers ~~~~~~~~~~~~ The first interesting method in our renderer is ``swap_buffers()``, which returns a pointer to the buffer we’ve been drawing to, and sets up the other buffer as the new drawing target. The idea is that the display update handler will call this method to get ahold of the bitmap to display to the user:: bitmap_rgb32 *example_renderer::swap_buffers() { // wait for any rendering to complete before returning the buffer wait("swap_buffers"); // return the current draw buffer and then switch to the other // for future drawing bitmap_rgb32 *result = &m_display[m_draw_buffer]; m_draw_buffer ^= 1; return result; } The most important thing here to note here is the call to **poly_manager**’s ``wait()``, which will block the current thread until all rendering is complete. This is important because otherwise the caller may receive a bitmap that is still being drawn to, leading to torn or corrupt visuals. clear_buffers ~~~~~~~~~~~~~ One of the most common operations to perform when doing 3D rendering is to initialize or clear the display and depth buffers to a known value. This method below leverages the *tile* primitive to render a rectangle over the screen by passing in (0,0) and (width,height) for the two vertices. Because the color and depth values to clear the buffer to are constant, they are stored in a freshly-allocated **example_object_data** object, along with a pointer to the buffers in question. The ``render_tile()`` call is made with a ``<0>`` suffix indicating that there are no iterated parameters to worry about:: void example_renderer::clear_buffers(rgb_t color, uint16_t depthval) { // allocate object data and populate it with information needed example_object_data &object = object_data().next(); object.dest = &m_display[m_draw_buffer]; object.depth = &m_depth; object.color = color; object.depthval = depthval; // top,left coordinate is always (0,0) vertex_t topleft; topleft.x = 0; topleft.y = 0; // bottom,right coordinate is (width,height) vertex_t botright; botright.x = m_display[0].width(); botright.y = m_display[0].height(); // render as a tile with 0 iterated parameters render_tile<0>(m_display[0].cliprect(), render_delegate(&example_renderer::render_clear, this), topleft, botright); } The render callback provided to ``render_tile()`` is also defined (privately) in our class, and handles a single span. Note how the rendering parameters are extracted from the **example_object_data** struct provided:: void example_renderer::render_clear(int32_t y, extent_t const &extent, example_object_data const &object, int threadid) { // get pointers to the start of the depth buffer and destination scanlines uint16_t *depth = &object.depth->pix(y); uint32_t *dest = &object.dest->pix(y); // loop over the full extent and just store the constant values from the object for (int x = extent.startx; x < extent.stopx; x++) { dest[x] = object.color; depth[x] = object.depthval; } } Another important point to make is that the X coordinates provided by extent struct are inclusive of startx but exclusive of stopx. Clipping is performed ahead of time so that the render callback can focus on laying down pixels as quickly as possible with minimal overhead. draw_triangle ~~~~~~~~~~~~~ Next up, we have our actual triangle rendering function, which will draw a single triangle given an array of three vertices provided in the external **example_vertex** format:: void example_renderer::draw_triangle(example_vertex const *verts) { // flat shaded case if (verts[0].color == verts[1].color && verts[0].color == verts[2].color) draw_triangle_flat(verts); else draw_triangle_gouraud(verts); } Because it is simpler and faster to render a flat shaded triangle, the code checks to see if the colors are the same on all three vertices. If they are, we call through to a special flat-shaded case, otherwise we process it as a full Gouraud-shaded triangle. This is a common technique to optimize rendering performance: identify special cases that reduce the per-pixel work, and route them to separate render callbacks that are optimized for that special case. draw_triangle_flat ~~~~~~~~~~~~~~~~~~ Here’s the setup code for rendering a flat-shaded triangle:: void example_renderer::draw_triangle_flat(example_vertex const *verts) { // allocate object data and populate it with information needed example_object_data &object = object_data().next(); object.dest = &m_display[m_draw_buffer]; object.depth = &m_depth; // in this case the color is constant and specified in the object data object.color = verts[0].color; // copy X, Y, and 1/Z into poly_manager vertices vertex_t v[3]; for (int vertnum = 0; vertnum < 3; vertnum++) { v[vertnum].x = verts[vertnum].x; v[vertnum].y = verts[vertnum].y; v[vertnum].p[0] = 1.0f / verts[vertnum].z; } // render the triangle with 1 iterated parameter (1/Z) render_triangle<1>(m_display[0].cliprect(), render_delegate(&example_renderer::render_flat, this), v[0], v[1], v[2]); } First, we put the fixed color into the **example_object_data** directly, and then fill out three **vertex_t** objects with the X and Y coordinates in the usual spot, and 1/Z as our one and only iterated parameter. (We use 1/Z here because iterated parameters are interpolated linearly in screen space. Z is not linear in screen space, but 1/Z is due to perspective correction.) Our flat-shaded case then calls ``render_trangle`` specifying ``<1>`` iterated parameter to interpolate, and pointing to a special-case flat render callback:: void example_renderer::render_flat(int32_t y, extent_t const &extent, example_object_data const &object, int threadid) { // get pointers to the start of the depth buffer and destination scanlines uint16_t *depth = &object.depth->pix(y); uint32_t *dest = &object.dest->pix(y); // get the starting 1/Z value and the delta per X float ooz = extent.param[0].start; float doozdx = extent.param[0].dpdx; // iterate over the extent for (int x = extent.startx; x < extent.stopx; x++) { // convert the 1/Z value into an integral depth value uint16_t depthval = ooz_to_depthval(ooz); // if closer than the current pixel, copy the color and depth value if (depthval < depth[x]) { dest[x] = object.color; depth[x] = depthval; } // regardless, update the 1/Z value for the next pixel ooz += doozdx; } } This render callback is a bit more involved than the clearing case. First, we have an iterated parameter (1/Z) to deal with, whose starting and X-delta values we extract from the extent before the start of the inner loop. Second, we perform depth buffer testing, using ``ooz_to_depthval()`` as a helper to transform the floating-point 1/Z value into a 16-bit integer. We compare this value against the current depth buffer value, and only store the pixel/depth value if it’s less. At the end of each iteration, we advance the 1/Z value by the X-delta in preparation for the next pixel. draw_triangle_gouraud ~~~~~~~~~~~~~~~~~~~~~ Finally we get to the code for the full-on Gouraud-shaded case:: void example_renderer::draw_triangle_gouraud(example_vertex const *verts) { // allocate object data and populate it with information needed example_object_data &object = object_data().next(); object.dest = &m_display[m_draw_buffer]; object.depth = &m_depth; // copy X, Y, 1/Z, and R,G,B into poly_manager vertices vertex_t v[3]; for (int vertnum = 0; vertnum < 3; vertnum++) { v[vertnum].x = verts[vertnum].x; v[vertnum].y = verts[vertnum].y; v[vertnum].p[0] = 1.0f / verts[vertnum].z; v[vertnum].p[1] = verts[vertnum].color.r(); v[vertnum].p[2] = verts[vertnum].color.g(); v[vertnum].p[3] = verts[vertnum].color.b(); } // render the triangle with 4 iterated parameters (1/Z, R, G, B) render_triangle<4>(m_display[0].cliprect(), render_delegate(&example_renderer::render_gouraud, this), v[0], v[1], v[2]); } Here we have 4 iterated parameters: the 1/Z depth value, plus red, green, and blue, stored as floating point values. We call ``render_triangle()`` with ``<4>`` as the number of iterated parameters to process, and point to the full Gouraud render callback:: void example_renderer::render_gouraud(int32_t y, extent_t const &extent, example_object_data const &object, int threadid) { // get pointers to the start of the depth buffer and destination scanlines uint16_t *depth = &object.depth->pix(y); uint32_t *dest = &object.dest->pix(y); // get the starting 1/Z value and the delta per X float ooz = extent.param[0].start; float doozdx = extent.param[0].dpdx; // get the starting R,G,B values and the delta per X as 8.24 fixed-point values uint32_t r = uint32_t(extent.param[1].start * float(1 << 24)); uint32_t drdx = uint32_t(extent.param[1].dpdx * float(1 << 24)); uint32_t g = uint32_t(extent.param[2].start * float(1 << 24)); uint32_t dgdx = uint32_t(extent.param[2].dpdx * float(1 << 24)); uint32_t b = uint32_t(extent.param[3].start * float(1 << 24)); uint32_t dbdx = uint32_t(extent.param[3].dpdx * float(1 << 24)); // iterate over the extent for (int x = extent.startx; x < extent.stopx; x++) { // convert the 1/Z value into an integral depth value uint16_t depthval = ooz_to_depthval(ooz); // if closer than the current pixel, assemble the color if (depthval < depth[x]) { dest[x] = rgb_t(r >> 24, g >> 24, b >> 24); depth[x] = depthval; } // regardless, update the 1/Z and R,G,B values for the next pixel ooz += doozdx; r += drdx; g += dgdx; b += dbdx; } } This follows the same pattern as the flat-shaded callback, except we have 4 iterated parameters to step through. Note that even though the iterated parameters are of ``float`` type, we convert the color values to fixed-point integers when iterating over them. This saves us doing 3 float-to-int conversions each pixel. The original RGB values were 0-255, so interpolation can only produce values in the 0-255 range. Thus we can use 24 bits of a 32-bit integer as the fraction, which is plenty accurate for this case. Advanced Topic: the poly_array class ------------------------------------ **poly_array** is a template class that is used to manage a dynamically-sized vector of objects whose lifetime starts at allocation and ends when ``reset()`` is called. The **poly_manager** class uses several **poly_array** objects internally, including one for allocated **ObjectType** data, one for each primitive rendered, and one for holding all allocated extents. **poly_array** has an additional property where after a reset it retains a copy of the most recently allocated object. This ensures that callers can always call ``last()`` and get a valid object, even immediately after a reset. The **poly_array** class requires two template parameters:: template class poly_array; These parameters are: * **ArrayType** is the type of object you wish to allocate and manage. * **TrackingCount** is the number of objects you wish to preserve after a reset. Typically this value is either 0 (you don’t care to track any objects) or 1 (you only need one object); however, if you are using **poly_array** to manage a shared collection of objects across several independent consumers, it can be higher. See below for an example where this might be handy. Note that objects allocated by **poly_array** are owned by **poly_array** and will be automatically freed upon exit. **poly_array** is optimized for use in high frequency multi-threaded systems. Therefore, one added feature of the class is that it rounds the allocation size of **ArrayType** to the nearest cache line boundary, on the assumption that neighboring entries could be accessed by different cores simultaneously. Keeping each **ArrayType** object in its own cache line ensures no false sharing performance impacts. Currently, **poly_array** has no mechanism to determine cache line size at runtime, so it presumes that 64 bytes is a typical cache line size, which is true for most x64 and ARM chips as of 2021. This value can be altered by changing the **CACHE_LINE_SHIFT** constant defined at the top of the class. Objects allocated by **poly_array** are created in 64k chunks. At construction time, one chunk’s worth of objects is allocated up front. The chunk size is controlled by the **CHUNK_GRANULARITY** constant defined at the top of the class. As more objects are allocated, if **poly_array** runs out of space, it will dynamically allocate more. This will produce discontiguous chunks of objects until the next ``reset()`` call, at which point **poly_array** will reallocate all the objects into a contiguous vector once again. For the case where **poly_array** is used to manage a shared pool of objects, it can be configured to retain multiple most recently allocated items by using a **TrackingCount** greater than 1. For example, if **poly_array** is managing objects for two texture units, then it can set **TrackingCount** equal to 2, and pass the index of the texture unit in calls to ``next()`` and ``last()``. After a reset, **poly_array** will remember the most recently allocated object for each of the units independently. Methods ~~~~~~~ poly_array ++++++++++ :: poly_array(); The **poly_array** contructor requires no parameters and simply pre-allocates one chunk of objects in preparation for future allocations. count +++++ :: u32 count() const; **Return value:** the number of objects currently allocated. max +++ :: u32 max() const; **Return value:** the maximum number of objects ever allocated at one time. itemsize ++++++++ :: size_t itemsize() const; **Return value:** the size of an object, rounded up to the nearest cache line boundary. allocated +++++++++ :: u32 allocated() const; **Return value:** the number of objects that fit within what’s currently been allocated. byindex +++++++ :: ArrayType &byindex(u32 index); Returns a reference to an object in the array by index. Equivalent to [**index**] on a normal array: * **index** is the index of the item you wish to reference. **Return value:** a reference to the object in question. Since a reference is returned, it is your responsibility to ensure that **index** is less than ``count()`` as there is no mechanism to return an invalid result. contiguous ++++++++++ :: ArrayType *contiguous(u32 index, u32 count, u32 &chunk); Returns a pointer to the base of a contiguous section of **count** items starting at **index**. Because **poly_array** dynamically resizes, it may not be possible to access all **count** objects contiguously, so the number of actually contiguous items is returned in **chunk**: * **index** is the index of the first item you wish to access contiguously. * **count** is the number of items you wish to access contiguously. * **chunk** is a reference to a variable that will be set to the actual number of contiguous items available starting at **index**. If **chunk** is less than **count**, then the caller should process the **chunk** items returned, then call ``countiguous()`` again at (**index** + **chunk**) to access the rest. **Return value:** a pointer to the first item in the contiguous chunk. No range checking is performed, so it is your responsibility to ensure that **index** + **count** is less than or equal to ``count()``. indexof +++++++ :: int indexof(ArrayType &item) const; Returns the index within the array of the given item: * **item** is a reference to an item in the array. **Return value:** the index of the item. It should always be the case that:: array.indexof(array.byindex(index)) == index reset +++++ :: void reset(); Resets the **poly_array** by semantically deallocating all objects. If previous allocations created a discontiguous array, a fresh vector is allocated at this time so that future allocations up to the same level will remain contiguous. Note that the **ArrayType** destructor is *not* called on objects as they are deallocated. **Return value:** none. next ++++ :: ArrayType &next(int tracking_index = 0); Allocates a new object and returns a reference to it. If there is not enough space for a new object in the current array, a new discontiguous array is created to hold it: * **tracking_index** is the tracking index you wish to assign the new item to. In the common case this is 0, but could be non-zero if using a **TrackingCount** greater than 1. **Return value:** a reference to the object. Note that the placement new operator is called on this object, so the default **ArrayType** constructor will be invoked here. last ++++ :: ArrayType &last(int tracking_index = 0) const; Returns a reference to the last object allocated: * **tracking_index** is the tracking index whose object you want. In the common case this is 0, but could be non-zero if using a **TrackingCount** greater than 1. **poly_array** remembers the most recently allocated object independently for each **tracking_index**. **Return value:** a reference to the last allocated object.