Skip to main content
GameDev.net gamedev.net
🔒 Locked

A Modern Approach to Software Rasterization

Started by ZachBethel Dec 14, 2011 at 4:37 PM 17 replies 4.2k views
Original Post
ZachBethel
ZachBethel
As a part of two semesters research of undergraduate research at Taylor University, I development a multi-threaded tile-based software rasterizer. The pipeline rasterizes and shades four fragments in parallel using SSE instructions, and utilizes and extensive custom written SIMD optimized math library for all vertex transformations. Features supported include custom vertex and fragment shaders written purely in C++ (thanks to some operator overloading tricks), perspective correct texture mapping, clipping and backface culling, the flexibility to select different render targets, and early Z rejection. In addition, rendering order is preserved. On a Hyper-Threaded Quad-Core Intel I7 mobile processor, the rasterizer performs a full four times faster with eight threads than with one. It utilizes SDL for frame buffer, thread, and bitmap management.

Although software rendering is still considered inferior to hardware hards-down, I found it interesting how companies like Intel have been trying to revive it once again. With the direction that mainstream processor hardware is headed, it seems believable to me that we may see software rendering return to mainstream in the future. I wonder if Intel hasn't thought about replacing their IGP's in the future with dedicated cores with some additional hardware for texture filtering. That's still a few years off though, at least.

At any rate, I wrote a paper detailing the architecture of the rasterizer. You can find it here. Building a software renderer is a fun and highly educational endeavor and I recommend that anyone interested in computer graphics try it.

Click here to view the iotd
clb
clb
Nice work! You certainly do have focused into your topic.

The only thing I noticed from a cursory read is that you systematically misspelled 'heirarchy'.
Ravyne
Ravyne
Cool indeed. Did you research any of the papers/info regarding Intel's Larabee, or Nicolas Capens' work (which was bought up by Caldega).
throw table_exception("(? ???)? ? ???");
ZachBethel
ZachBethel
@clb

Oh my, so I did! Thank you for pointing that out. I'll fix it right away.

@Ravyne

Yes, heavily. Nick was very helpful in answering a lot of my questions, and I implemented a variation of his half-space rasterization approach detailed in his article on DevMaster. Michael Abrash's article on Larrabee rasterization was very helpful as well. The rasterizer uses a hierarchy (spelled it right this time!) of tiles and blocks that can trivially accept or reject sets of pixels.
HappyCoder
HappyCoder
That looks great. I once wrote a simple rasterizer for the pocket pc. It definitely wasn't as feature rich or as polished looking as yours is but was exciting for me nonetheless. Maybe I will take another go on it sometime.

Good work.
My current game project Platform RPG
Krohm
Krohm
I have indeed been against sw rasterization for a while.
However, I had to admit it has some advantages, especially when it comes to low-latency occlusion queries.
It appears we are going to see sw rasterization back into action sooner or later, for some cases, albeit perhaps not as fully complete rasterizers.
Previously "Krohm"
Vilem Otte
Vilem Otte
Good work indeed! I've been using software rasterization for occlusion culling, where it performs a lot better than GPU occlusion culling.
Hodgman
Hodgman
Nice work, thanks for sharing the paper with us!
N.B. I believe there's a niche market for reasonably-priced, well-performing software rasterization middleware these days wink.gif
it has some advantages, especially when it comes to low-latency occlusion queries.
Indeed, it's been making quite a comeback as a "GPU assistant" for the past few years to get around latency problems, and on the PS3 where there's arguably more CPU power than GPU power, it's been making headway for longer than that.
ZachBethel
ZachBethel
I don't have my laptop with me at the moment, but if I remember correctly, it runs just under 60fps at 1920x1080.
ZachBethel
ZachBethel
I just dug up the data that I used for building the graphs, and it turns out I was incorrect, the demo performed at 50fps at 1920x1080--not 60. Sorry about the misinformation. The statue and floor model is roughly 33,000 triangles, and it was rendered in two passes--with the shadow pass rendering to a 1024x1024 texture.
Matias Goldberg
Matias Goldberg
Hi, nice work. I'm sure your work will be very interesting for those using occlusion culling in the CPU (i.e. Battlefield)

Basically such renderers just focus on rendering Depth-Z (no colour or lighting) and an index value to know which objects to send to the GPU.

I don't personally think a sw renderer will be a trend to "the future"; however interesting applications like occlusion culling are probably spot on (I'm sure there may be other applications).

sepul
sepul
very nice work
It would be awesome if you could make a software occlusion library from this. you have all the stuff you need to make a kick-ass software occlusion system
the_edd
the_edd
Very nice! I'm doing a similar project for fun right now. Out of interest, how customisable is your pixel shading? What does a shader look like?

Currently, I'm doing deferred shading by storing barycentric coordinates in my G-buffer, which allows huge flexibility when it comes to the shading pass and prevents costly overdraw for expensive shading, but I'm still not sure it's a good idea overall

I'm assuming the screen-space tiling scheme hits quite hard when the scene complexity is predominantly localised to a subset of the bins? How is the number of tiles related to the number of threads?
ZachBethel
ZachBethel
The pixel shader is essentially a function pointer that takes as input a structure of globals and interpolated attributes--each one one is four values packed for SSE. You could write the pixel shader however you want, either in pure SSE assembly, SSE intrinsics, or using a set of classes that I wrote which encapsulate an __m128 value. The overloaded operators perform intrinsic functions on the data.

Here's an example of a shader:

void ColorFragment(MSR_FShaderParameters *params) { MSR_SSEColor3 &out = params->output; const MSR_ShaderGlobals *globals = params->globals; // Assemble the light direction const MSR_Vec4 &ld = params->globals->lights[0].direction; MSR_SSEVec3 LightDir( MSR_SSEFloat(ld.x), MSR_SSEFloat(ld.y), MSR_SSEFloat(ld.z) ); params->varyings[10] = _mm_rcp_ps( *params->varyings[10] ); params->varyings[8] *= params->varyings[10]; params->varyings[9] = 1.0f - (params->varyings[9] * params->varyings[10]); // Get the texture MSR_SSEColor3 tex = MSR_Tex2D_Wrap(params->globals->tex0, params->varyings[0], params->varyings[1]); // Get the eye vector MSR_SSEVec3 *Eye = (MSR_SSEVec3*)&params->varyings[2]; Eye->Normalize(); // Get the normal vector MSR_SSEVec3 *Normal = (MSR_SSEVec3*)&params->varyings[5]; Normal->Normalize(); // Diffuse MSR_SSEFloat diff = MSR_Clamp(Normal->Dot(LightDir), 0, 1); // Specular MSR_SSEVec3 Reflect = ((diff * MSR_SSEFloat(2.0f)) * *Normal) - LightDir; Reflect.Normalize(); MSR_SSEFloat spec = MSR_Clamp(Reflect.Dot(*Eye), 0, 1); spec = spec * spec * spec * spec * spec * spec * spec;

MSR_SSEFloat clamped_ao = MSR_Clamp( params->varyings[11], 0, 1 ); out.r = tex.r * clamped_ao; out.g = tex.g * clamped_ao; out.b = tex.b * clamped_ao; out.r *= (diff * globals->ml_diffuse[0].r) + (spec * globals->ml_specular[0].r) + globals->ml_ambient[0].r; out.g *= (diff * globals->ml_diffuse[0].g) + (spec * globals->ml_specular[0].g) + globals->ml_ambient[0].g; out.b *= (diff * globals->ml_diffuse[0].b) + (spec * globals->ml_specular[0].b) + globals->ml_ambient[0].b; }


It could definitely be cleaned up to look better, but my focus was to get it to work, not to make it look pretty.

The nice thing about this function is that I defined it in the driver program, so it's completely independent of the software rasterizer library. To change it, I just call a function to set the function pointer to something else.

The current tile size is 64 x 64. I found this to be the best tradeoff. At lower resolutions (like 800 x 600), 32 x 32 pixel tiles is better. I haven't found a size other than those two that perform better. Performance is actually quite good even when polygons bunch up in a single tile (or two), because the pixel workload is small enough at that resolution that it doesn't really become a bottleneck. The tile size doesn't really relate to the number of threads, but threads pull them off of a job queue and process them one by one. The job queue is pre-optimized to only include tiles with data in them.

Good luck on your deferred rasterizer!
the_edd
the_edd
Thanks for the shading info. I'm attempting something along similar lines right now. Good to know it's not completely crazy

Do you have a some clever scheme for texel lookup in texture mapping, or is it really just a case of doing the following (ignoring bilinear filtering etc for now):

* calculating 4 raster indices at a time
* unpack those indices (__m128i -> uint32_t[4])
* look up four texels, one at a time
* finally pack the resulting rgb(a) values back into __m128s

?
ZachBethel
ZachBethel
Yep. Since SSE doesn't have scatter/gather support, you have to serialize it and repack. There's no way around that for now, unfortunately.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.