5
\$\begingroup\$

About a year ago I wrote a small library for drawing graphics on the windows command line. I mostly used it for small projects, like a raycaster and a raytracer. I just rediscovered it today, at the time of me writing this question, and decided that it needed a review.

The library is split into six different classes:

  • Interop - an internal class for dealing with command-line related interop functions and structures.
  • RenderingPixel - stores information (foreground/background color and character) about a "pixel" on the command line.
  • RenderingBuffer - stores and manages a large array of RenderingPixels, as well as providing drawing utilities.
  • RenderingShader - provides a means to manipulate the pixels of a RenderingBuffer after it's been drawn to.
  • RenderingTexture - stores an array of pixels which can be drawn to a RenderingBuffer.
  • RenderingWindow - responsible for the creation and management of the command line window.

I'm mostly wondering what I can improve in the areas of performance and design, but any other criticisms are welcome.

Interop.cs

using System;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

namespace CLIGL
{
    /// <summary>
    /// This class contains various external interop methods and structures 
    /// used to help speed up CLIGL rendering and provide more functionality.
    /// </summary>
    internal static class Interop
    {
        /// <summary>
        /// Create a file or open a device.
        /// </summary>
        /// <param name="fileName">The name of the file or device.</param>
        /// <param name="fileAccess">The access to the file or device.</param>
        /// <param name="fileShare">The sharing mode of the file or device.</param>
        /// <param name="securityAttributes">The security attributes of the file or device.</param>
        /// <param name="creationDisposition">The action to take on a file or device that does not exist.</param>
        /// <param name="flags">The file or device attributes.</param>
        /// <param name="template">A valid handle to a template file.</param>
        /// <returns>A handle to a file or device.</returns>
        [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern SafeFileHandle CreateFile(
            string fileName,
            [MarshalAs(UnmanagedType.U4)] uint fileAccess,
            [MarshalAs(UnmanagedType.U4)] uint fileShare,
            IntPtr securityAttributes,
            [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
            [MarshalAs(UnmanagedType.U4)] int flags,
            IntPtr template
        );

        /// <summary>
        /// Write character and color attribute data to a specified area of a
        /// console screen buffer.
        /// </summary>
        /// <param name="hConsoleOutput">A handle to the console screen buffer.</param>
        /// <param name="lpBuffer">The buffer to be written to the screen buffer.</param>
        /// <param name="dwBufferSize">The size of the buffer to be written.</param>
        /// <param name="dwBufferCoord">The upper-left coordinates of the buffer.</param>
        /// <param name="lpWriteRegion">The upper-left and lower-right coordinates of the screen buffer to write to.</param>
        /// <returns>Whether or not the operation was successful.</returns>
        [DllImport("Kernel32.dll", SetLastError = true)]
        public static extern bool WriteConsoleOutput(
            SafeFileHandle hConsoleOutput,
            CharInfo[] lpBuffer,
            Coord dwBufferSize,
            Coord dwBufferCoord,
            ref SmallRect lpWriteRegion
        );

        /// <summary>
        /// Represents a C++-esque character union, containing a Unicode character
        /// and an ASCII character.
        /// </summary>
        [StructLayout(LayoutKind.Explicit)]
        public struct CharUnion
        {
            [FieldOffset(0)] public char UnicodeChar;
            [FieldOffset(0)] public char AsciiChar;
        }

        /// <summary>
        /// Contains information about a character to be written to the console, like
        /// the character data and color data.
        /// </summary>
        [StructLayout(LayoutKind.Explicit)]
        public struct CharInfo
        {
            [FieldOffset(0)] public CharUnion Char;
            [FieldOffset(2)] public short Attributes;
        }

        /// <summary>
        /// Represents a 2D coordinate with components of the type "short".
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        public struct Coord
        {
            public short X;
            public short Y;
        }

        /// <summary>
        /// Represents a 2D coordinate with components of the type "int".
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        public struct Point
        {
            public int X;
            public int Y;
        }

        /// <summary>
        /// Represents a small rectangle, with components of the type "short".
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        public struct SmallRect
        {
            public short Left;
            public short Top;
            public short Right;
            public short Bottom;
        }

        /// <summary>
        /// Represents a normal rectangle, with components of the type "int".
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        public struct Rect
        {
            public int Left;
            public int Top;
            public int Right;
            public int Bottom;
        }
    }
}

RenderingPixel.cs

using System;

namespace CLIGL
{
    /// <summary>
    /// This class is intended to be a compact representation of a "pixel"
    /// in CLIGL, which can be used instead of passing around individual characters
    /// and colors.
    /// </summary>
    public struct RenderingPixel
    {
        public static RenderingPixel EmptyPixel = new RenderingPixel(' ', ConsoleColor.Black, ConsoleColor.Black);
        public static RenderingPixel FullPixel = new RenderingPixel(' ', ConsoleColor.White, ConsoleColor.White);

        public char Character { get; set; }
        public ConsoleColor ForegroundColor { get; set; }
        public ConsoleColor BackgroundColor { get; set; }

        /// <summary>
        /// Constructor for the RenderingPixel class.
        /// </summary>
        /// <param name="character">The character that represents the pixel.</param>
        /// <param name="foregroundColor">The foreground color of the pixel.</param>
        /// <param name="backgroundColor">The background color of the pixel.</param>
        public RenderingPixel(char character, ConsoleColor foregroundColor, ConsoleColor backgroundColor)
        {
            this.Character = character;
            this.ForegroundColor = foregroundColor;
            this.BackgroundColor = backgroundColor;
        }

        /// <summary>
        /// Check to see if two rendering pixels are equal.
        /// </summary>
        /// <param name="a">The first rendering pixel.</param>
        /// <param name="b">The second rendering pixel.</param>
        /// <returns>Whether or not the objects are equal.</returns>
        public static bool operator==(RenderingPixel a, RenderingPixel b)
        {
            if(
                a.Character == b.Character &&
                a.ForegroundColor == b.ForegroundColor &&
                a.BackgroundColor == b.BackgroundColor
            )
            {
                return true;
            }
            return false;
        }

        /// <summary>
        /// Check to see if two rendering pixels are not equal.
        /// </summary>
        /// <param name="a">The first rendering pixel.</param>
        /// <param name="b">The second rendering pixel.</param>
        /// <returns>Whether or not the objects are not equal.</returns>
        public static bool operator!=(RenderingPixel a, RenderingPixel b)
        {
            if(
                a.Character == b.Character && 
                a.ForegroundColor == b.ForegroundColor &&
                a.BackgroundColor == b.BackgroundColor
            )
            {
                return false;
            }
            return true;
        }
    }
}

RenderingBuffer.cs

using System;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using Microsoft.Win32.SafeHandles;

namespace CLIGL
{
    /// <summary>
    /// This class represents a CLIGL render buffer, which is responsible for 
    /// managing what will currently be drawn to the screen.
    /// </summary>
    public class RenderingBuffer
    {
        public int BufferWidth { get; set; }
        public int BufferHeight { get; set; }
        public int BufferSize { get; set; }
        public RenderingPixel[] PixelBuffer { get; set; }

        /// <summary>
        /// Constructor for the RenderingBuffer class.
        /// </summary>
        /// <param name="bufferWidth">The width of the text and color buffers.</param>
        /// <param name="bufferHeight">The height of the text and color buffers.</param>
        public RenderingBuffer(int bufferWidth, int bufferHeight)
        {
            this.BufferWidth = bufferWidth;
            this.BufferHeight = bufferHeight;
            this.BufferSize = this.BufferWidth * this.BufferHeight;

            this.PixelBuffer = new RenderingPixel[this.BufferSize];
            for(int i = 0; i < this.BufferSize - 1; i++)
            {
                this.PixelBuffer[i] = RenderingPixel.EmptyPixel;
            }
        }

        /// <summary>
        /// Convert a 1-dimensional index to a 2-dimensional index in the form 
        /// of a int array with two values.
        /// </summary>
        /// <param name="index">The index to convert/</param>
        /// <returns>A 2-dimensional index.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int[] Convert1DIndexTo2DIndex(int index)
        {
            int x = index % this.BufferWidth;
            int y = index / this.BufferWidth;
            return new int[] { x, y };
        }

        /// <summary>
        /// Convert a 2-dimensional index to a 1-dimensional index.
        /// </summary>
        /// <param name="x">The x coordinate.</param>
        /// <param name="y">The y coordinate.</param>
        /// <returns>A 1-dimensional index.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int Convert2DIndexTo1DIndex(int x, int y)
        {
            return x + (y * this.BufferWidth);
        }

        /// <summary>
        /// Apply a shader to the current rendering buffer, which may change the contents
        /// of the buffer, changing how the buffer looks when it is rendered.
        /// </summary>
        /// <param name="renderingShader">The shader to apply.</param>
        public void ApplyShader(RenderingShader renderingShader)
        {
            for(int i = 0; i < this.BufferSize - 1; i++)
            {
                int[] pixelPosition = this.Convert1DIndexTo2DIndex(i);
                this.PixelBuffer[i] = renderingShader.Execute(pixelPosition[0], pixelPosition[1], this.PixelBuffer[i]);
            }
        }

        /// <summary>
        /// Clear the current active pixel buffer with a clear character, a
        /// foreground clear color and a background clear color.
        /// </summary>
        /// <param name="clearPixel">The pixel to clear the pixel buffer with.</param>
        public void ClearPixelBuffer(RenderingPixel clearPixel)
        {
            for(int i = 0; i < this.BufferSize - 1; i++)
            {
                this.PixelBuffer[i] = clearPixel;
            }
        }

        /// <summary>
        /// Set a pixel into the current pixel buffer using a 1D index.
        /// </summary>
        /// <param name="index">The index of the pixel.</param>
        /// <param name="fillPixel">The pixel to fill the index with.</param>
        public void SetPixel(int index, RenderingPixel fillPixel)
        {
            if(index < this.BufferSize)
            {
                this.PixelBuffer[index] = fillPixel;
            }
        }

        /// <summary>
        /// Set a pixel into the current pixel buffer using a 2D index.
        /// </summary>
        /// <param name="x">The x coordinate of the pixel.</param>
        /// <param name="y">The y coordinate of the pixel.</param>
        /// <param name="fillPixel">The pixel to fill the coordinates with.</param>
        public void SetPixel(int x, int y, RenderingPixel fillPixel)
        {
            if(x >= 0 && y >= 0 && x < this.BufferWidth && y < this.BufferHeight)
            {
                int pixelIndex = this.Convert2DIndexTo1DIndex(x, y);
                this.PixelBuffer[pixelIndex] = fillPixel;
            }
        }

        /// <summary>
        /// Set a rectangle of pixels into the current pixel buffer.
        /// </summary>
        /// <param name="x">The x coordinate of the rectangle.</param>
        /// <param name="y">The y coordinate of the rectangle.</param>
        /// <param name="width">The width of the rectangle.</param>
        /// <param name="height">The height of the rectangle.</param>
        /// <param name="fillPixel">The pixel to fill the rectangle with.</param>
        public void SetRectangle(int x, int y, int width, int height, RenderingPixel fillPixel)
        {
            for(int xi = x; xi < x + width; xi++)
            {
                for(int yi = y; yi < y + height; yi++)
                {
                    this.SetPixel(xi, yi, fillPixel);
                }
            }
        }

        /// <summary>
        /// Set a texture into the current pixel buffer.
        /// </summary>
        /// <param name="x">The x coordinate of the texture.</param>
        /// <param name="y">The y coordinate of the texture.</param>
        /// <param name="texture">The texture to set.</param>
        public void SetTexture(int x, int y, RenderingTexture texture)
        {
            for(int xi = 0; xi < texture.Width; xi++)
            {
                for(int yi = 0; yi < texture.Height; yi++)
                {
                    this.SetPixel(x + xi, y + yi, texture.RenderingPixels[xi, yi]);
                }
            }
        }

        /// <summary>
        /// Set a string into the current pixel buffer.
        /// </summary>
        /// <param name="x">The x position of the string.</param>
        /// <param name="y">The y position of the string.</param>
        /// <param name="text">The text to set.</param>
        /// <param name="foregroundColor">The foreground color of the string.</param>
        /// <param name="backgroundColor">The background color of the string.</param>
        public void SetString(int x, int y, string text, ConsoleColor foregroundColor, ConsoleColor backgroundColor)
        {
            for(int i = 0; i < text.Length; i++)
            {
                this.SetPixel(x + i, y, new RenderingPixel(text[i], foregroundColor, backgroundColor));
            }
        }

        /// <summary>
        /// Write the pixel buffer to the C# command-line interface, which will
        /// produce specific output based on the contents of the pixel buffer.
        /// </summary>
        /// <param name="consoleHandle">The handle to the current console window.</param>
        public void Render(SafeFileHandle consoleHandle)
        {
            Interop.CharInfo[] characterBuffer = new Interop.CharInfo[this.BufferWidth * this.BufferHeight];
            Interop.SmallRect windowRectangle = new Interop.SmallRect()
            {
                Left = 0,
                Top = 0,
                Right = (short)this.BufferWidth,
                Bottom = (short)this.BufferHeight
            };

            for(int i = 0; i < characterBuffer.Length; i++)
            {
                characterBuffer[i].Attributes = (byte)((byte)this.PixelBuffer[i].ForegroundColor | ((byte)this.PixelBuffer[i].BackgroundColor << 4));
                characterBuffer[i].Char.AsciiChar = this.PixelBuffer[i].Character;
            }

            Interop.WriteConsoleOutput(
                consoleHandle,
                characterBuffer,
                new Interop.Coord() { X = (short)this.BufferWidth, Y = (short)this.BufferHeight },
                new Interop.Coord() { X = 0, Y = 0 },
                ref windowRectangle
            );
        }
    }
}

RenderingShader.cs

using System;

namespace CLIGL
{
    /// <summary>
    /// This class represents a CLIGL shader. A shader in CLIGL is essentially
    /// an anonymous function that is applied to every pixel in a rendering buffer,
    /// or every pixel in a rendering buffer that is inside the specified region.
    /// </summary>
    public class RenderingShader
    {
        public int RegionX { get; set; }
        public int RegionY { get; set; }
        public int RegionWidth { get; set; }
        public int RegionHeight { get; set; }
        public Func<int, int, RenderingPixel, RenderingPixel> Function { get; set; }

        /// <summary>
        /// Constructor for the RenderingShaderClass.
        /// </summary>
        /// <param name="regionX">The x coordinate of the region the shader affects.</param>
        /// <param name="regionY">The y coordinate of the region the shader affects.</param>
        /// <param name="regionWidth">The width of the region the shader affects.</param>
        /// <param name="regionHeight">The height of the region the shader affects.</param>
        /// <param name="function">The shader's function.</param>
        public RenderingShader(int regionX, int regionY, int regionWidth, int regionHeight, Func<int, int, RenderingPixel, RenderingPixel> function)
        {
            this.RegionX = regionX;
            this.RegionY = regionY;
            this.RegionWidth = regionWidth;
            this.RegionHeight = regionHeight;
            this.Function = function;
        }

        /// <summary>
        /// Execute the shader, and return an output pixel based on what the shader
        /// does to, and with the inputs.
        /// </summary>
        /// <param name="x">The x coordinate the shader is executing at.</param>
        /// <param name="y">The y coordinate the shader is executing at.</param>
        /// <param name="inputPixel">The input pixel at the x and y coordinates.</param>
        /// <returns>An output pixel, or the input pixel, if the input pixel is out of bounds.</returns>
        public RenderingPixel Execute(int x, int y, RenderingPixel inputPixel)
        {
            if(x >= this.RegionX && x <= this.RegionX + this.RegionWidth && y >= this.RegionY && y <= this.RegionY + this.RegionHeight)
            {
                return this.Function(x, y, inputPixel);
            }
            else
            {
                return inputPixel;
            }
        }
    }
}

RenderingTexture.cs

using System;
using System.Runtime.CompilerServices;

namespace CLIGL
{
    /// <summary>
    /// This class is intended to represented a rendering texture, which is a
    /// 2-dimensional array of Pixel objects.
    /// </summary>
    public class RenderingTexture
    {
        public int Width { get; set; }
        public int Height { get; set; }
        public RenderingPixel[,] RenderingPixels { get; set; }

        /// <summary>
        /// Constructor for the RenderingTexture class.
        /// </summary>
        /// <param name="width">The width of the rendering texture.</param>
        /// <param name="height">The height of the rendering texture.</param>
        public RenderingTexture(int width, int height)
        {
            this.Width = width;
            this.Height = height;
            this.RenderingPixels = new RenderingPixel[this.Width, this.Height];

            for(int x = 0; x < this.Width; x++)
            {
                for(int y = 0; y < this.Height; y++)
                {
                    this.RenderingPixels[x, y] = RenderingPixel.EmptyPixel;
                }
            }
        }

        /// <summary>
        /// Apply a shader to the current rendering texture, which may change the contents
        /// of the texture, changing how the texture looks when it is rendered.
        /// </summary>
        /// <param name="renderingShader">The shader to apply.</param>
        public void ApplyShader(RenderingShader renderingShader)
        {
            for(int x = 0; x < this.Width; x++)
            {
                for(int y = 0; y < this.Height; y++)
                {
                    this.RenderingPixels[x, y] = renderingShader.Execute(x, y, this.RenderingPixels[x, y]);
                }
            }
        }

        /// <summary>
        /// Set a pixel into the texture at the specified coordinates.
        /// </summary>
        /// <param name="x">The x coordinate of the pixel.</param>
        /// <param name="y">The y coordinate of the pixel.</param>
        /// <param name="pixel">The pixel to set.</param>
        public void SetPixel(int x, int y, RenderingPixel pixel)
        {
            if(x >= 0 && x < this.Width && y >= 0 && y < this.Height)
            {
                this.RenderingPixels[x, y] = pixel;
            }
            else
            {
                throw new Exception(string.Format("The provided coordinates '({0}, {1})' are out of range.", x, y));
            }
        }
    }
}

RenderingWindow.cs

using System;
using System.IO;
using System.Text;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

namespace CLIGL
{
    /// <summary>
    /// This class represents a CLIGL "window", which contains the various components
    /// required for window rendering and is responsible for managing and updating them.
    /// </summary>
    public class RenderingWindow
    {
        public string Title { get; set; }
        public int Width { get; set; }
        public int Height { get; set; }
        public SafeFileHandle ConsoleHandle { get; set; }

        /// <summary>
        /// Constructor for the Window class.
        /// </summary>
        /// <param name="width">The width of the window.</param>
        /// <param name="height">The height of the window.</param>
        public RenderingWindow(string title, int width, int height)
        {
            this.Title = title;
            this.Width = width;
            this.Height = height;

            Console.Title = this.Title;
            Console.CursorVisible = false;
            Console.OutputEncoding = Encoding.Unicode;
            Console.TreatControlCAsInput = true;

            Console.SetWindowSize(this.Width, this.Height);
            Console.SetBufferSize(this.Width, this.Height);

            this.ConsoleHandle = Interop.CreateFile("CONOUT$", 0x40000000, 2, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero);
            if(this.ConsoleHandle.IsInvalid)
            {
                throw new Exception("Creating the console window handle failed.");
            }
        }

        /// <summary>
        /// Render the window. This function will render the provided rendering buffer
        /// to the console window output. If the dimensions of the provided rendering
        /// buffer do not match the dimensions of the window, then an error is thrown.
        /// </summary>
        /// <param name="renderingBuffer">The rendering buffer to render.</param>
        public void Render(RenderingBuffer renderingBuffer)
        {
            try
            {
                Console.SetWindowSize(this.Width, this.Height);
                Console.SetBufferSize(this.Width, this.Height);

                if(renderingBuffer.BufferWidth == this.Width && renderingBuffer.BufferHeight == this.Height)
                {
                    renderingBuffer.Render(this.ConsoleHandle);
                }
                else
                {
                    throw new Exception("The dimensions of the provided rendering buffer do not match the dimensions of the window.");
                }
            }
            catch(IOException) { }
            catch(ArgumentOutOfRangeException) { }
        }
    }
}

Examples

The following two videos are very simple, small examples of what one can do with CLIGL. However, they are only the bare minimum. Anything from a 2D game to a full-blown 3D renderer is theoretically possible.

enter image description here

enter image description here

Finally, for those who are interested, the GitHub repository for CLIGL can be found here. For those who would like to see example usage, see here.

\$\endgroup\$
  • \$\begingroup\$ These GIFs are killing my browsers. The CPU usage skyrockets to 100% \$\endgroup\$ – t3chb0t Jun 28 '17 at 17:41
  • 1
    \$\begingroup\$ Good to know but this is interesting because the console uses system calls too (I just looked a the source code) although it does a lot of additional stuff that probably adds some overhead. \$\endgroup\$ – t3chb0t Jun 28 '17 at 17:55
  • 1
    \$\begingroup\$ @t3chb0t Definitely! I can't remember where I placed the code for those two gifs, but I can certainly make a 'tutorial' of sorts. Give me a bit and I should have it up. :) \$\endgroup\$ – Ethan Bierlein Jun 28 '17 at 18:51
  • 1
    \$\begingroup\$ Wow, this is really good explained and it's running! ;-D \$\endgroup\$ – t3chb0t Jun 28 '17 at 19:41
  • 1
    \$\begingroup\$ @t3chb0t Awesome! If you have any other questions, I'd be glad to help. :D \$\endgroup\$ – Ethan Bierlein Jun 28 '17 at 19:42
4
\$\begingroup\$

RenderingPixel

Well, a struct should be immutable if possible. For the RenderingPixel struct this is possible by making the fields EmptyPixel and FullPixel readonly and the properties Character, ForegroundColor and BackgroundColor private settable. Not doing so will asking for the sense of the constructor at all.

The == operator can be sanitized to

public static bool operator ==(RenderingPixel a, RenderingPixel b)
{
    return a.Character == b.Character &&
           a.ForegroundColor == b.ForegroundColor &&
           a.BackgroundColor == b.BackgroundColor;
}  

which is clearer.

The != operator could just look like so

public static bool operator !=(RenderingPixel a, RenderingPixel b)
{
    return !(a == b);
} 

RenderingBuffer

The lines

for(int i = 0; i < this.BufferSize - 1; i++)
{
    this.PixelBuffer[i] = RenderingPixel.EmptyPixel;
}  

inside the constrcutor should be changed to

ClearPixelBuffer(RenderingPixel.EmptyPixel);  

to remove the small code duplication.

Calling SetPixel for index < 0 will blow in your face.


General

  • If you don't need to change a property from the outside, make it private set.
  • Always validate method parameters of public methods if they likely can throw an exception. Better throw the exception yourself so you won't expose implementation details.

  • If validating method parameters like you did in the RenderingTexture.SetPixel() method you should throw early instead of at the end of the method.

\$\endgroup\$
  • 1
    \$\begingroup\$ CA1060: just a small (maybe pedantic) addition: I'd also split Interop into usual class NativeMethods (UnsafeNativeMethods and SafeNativeMethods? I guess we don't need it) \$\endgroup\$ – Adriano Repetti Jun 27 '17 at 12:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.