Windows Forms framework uses for its rendering a library called GDI+ [12, 13] (Graphics Device Interface). It is a managed wrapper library over the standard C/C++ based API called GDI which interacts with device drivers on behalf of applications. GDI+ provides a simple graphical functionality from drawing simple shapes such as triangles, circles or squares to drawing bitmaps, setting filter options and more. The only functionality I chose to use from this GDI+ library was setting and drawing bitmap onto the screen with given resolution. This is enough functionality since the bitmap itself is being set from Render3D class which I have created from scratch.
Drawing strategy I’ve chosen to implement consists of following steps:
- Let the Render3D class fill entire frame buffer of pixel colors
- Initialize new variable of type Bitmap
- Set the bitmap data to frame buffer data by looping over each element of frame buffer array and set bitmap data by calling Bitmap.SetPixel(x, y, color) method which will set each pixel of given bitmap to certain color
- Let the GDI+ draw the bitmap onto the screen
There was a performance problem with Bitmap.SetPixel(x, y, color) method however. Since the set pixel operation is called each time the loop iterates, set pixel method must lock and unlock bits in memory multiple times in one frame which is time-expensive operation [15].
So instead of using built-in set pixel function I had to implement my own function for writing bytes into bitmap memory which I called "RenderIntoMemory".
The purpose of this method is to overcome the limitation of set pixel operation [11] and to create more efficient way to read and write the memory. RenderIntoMemory method executes following steps in sequence:
Table above show performance data of five different resolutions with aspect ratio of 4:3. This and all following performance tests have been measured on a cacomputer with following parameters:
From the table above, one can see the higher the resolution the greater the difference between individual rendering methods. This is caused by the lock / unlock bits operation which is executed for each pixel in SetPixel method while RenderIntoMemory locks pixels once in the beginning and unlocks them in the end which improves performance of the application.
Part 3: Modern Rendering Pipelines <<< | >>> Part 5: Level loader & Design
So instead of using built-in set pixel function I had to implement my own function for writing bytes into bitmap memory which I called "RenderIntoMemory".
4.1 "RenderIntoMemory" function
- Use build-in Bitmap.LockBits(RECT, LOCK_MODE, PIXEL_FORMAT) function which locks the bitmap into system’s memory and thus gaining access to the BitmapData structure it returns.
- Initialize RGB byte[] array which size is determined by the BitmapData.Stride multiplied by screen height. BitmapData.Stride is usually defined as a screen width multiplied by the number of color channels given pixel format uses. In standard pixel format I use, stride can be calculated as s = W * 4 where W represents screen width and value 4 represents four color channels (blue, green, red, alpha).
- Each R, G and B component of RGB byte array is initialized by decomposing Render3D frame buffer hex color while the alpha channel A is not used, hence it is set to 255.
- Invoke build-in Marshal.Copy(RGB[], OFFSET, SCAN_0, LENGTH) function which copies the content of initialized RGB byte array in step 3 into the BitmapData structure itself.
- Unlock bits by calling Bitmap.UnlockBits(BITMAP_DATA) function. This final call is going to set the bitmap contents and will unlock it from memory making it ready for final drawing by GDI.
4.2 GDI+ Bitmap Drawing
After the "RenderIntoMemory" function has finished execution, the final step is to draw the bitmap itself onto the screen. Before this operation can be executed an InterpolationMode has to be set to "NearestNeighbour" so that no additional image interpolation will be applied to the bitmap to prevent blurriness. Then Graphics.DrawImage(BITMAP, RECT) function is called to send the given bitmap onto the graphics device for final rendering.
Following benchmark table shows the average performance difference between using the set pixel operation and implementing my own RenderIntoMemory function while iterating an entire image. Data were measured in first second of execution for defined pixel areas:
![]() |
Table of measured time based on resolution |
Table above show performance data of five different resolutions with aspect ratio of 4:3. This and all following performance tests have been measured on a cacomputer with following parameters:
- OS: Windows 10 Home of type x64
- CPU: Intel Core i7-7700 CPU @ 3.60GHz – 4.02GHz
- GPU: Nvidia GeForce GTX 1080
- RAM: 16.0 GB
One can see that choosing CPU as the main rendering-based calculations unit instead of GPU can be very inefficient since CPU is already busy with managing other applications as well. This was one of the many reasons why GPUs have been implemented later inside each computer hardware to accelerate rendering.
Following graph shows the time intervals measured in ticks against the resolution of both methods:
It is clear how the performance of "SetPixel" function scales linearly while the resolution doubles. The same applies to the "RenderIntoMemor" function except for the fact that RenderIntoMemory is approximately 12 to 21 times faster than the SetPixel method.
From the table above, one can see the higher the resolution the greater the difference between individual rendering methods. This is caused by the lock / unlock bits operation which is executed for each pixel in SetPixel method while RenderIntoMemory locks pixels once in the beginning and unlocks them in the end which improves performance of the application.
Part 3: Modern Rendering Pipelines <<< | >>> Part 5: Level loader & Design
No comments:
Post a Comment