PowerShell Cookbook

Twitter Updates

    follow me on Twitter

    Search

    Categories

     

    On this page

    burn-console: Optimized, and Ready For Marshmallows

    Archive

    Blogroll

    Disclaimer
    I work for Microsoft.

    The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

    RSS 2.0 | Atom 1.0 | CDF

    Send mail to the author(s) E-mail

    Total Posts: 235
    This Year: 12
    This Month: 0
    This Week: 0
    Comments: 634

    Sign In

     Sunday, December 18, 2005
    Sunday, December 18, 2005 8:57:13 PM (Pacific Standard Time, UTC-08:00) ( )

    In the last article, I introduced a library function to help you invoke inline C# from your scripts.  Since we carefully profiled the burn-console script’s performance, we know exactly where to apply these inline performance optimizations: to the updateBuffer function, and the updateScreen function.

    The completed script is included below. The inline optimizations bring us to about 30 - 40 frames per second, easily fast enough to provide smooth animation.

     
    ###############################################################################
    ## burn-console.ps1
    ##
    ## Create a fire effect in PowerShell, using the Console text buffer as the 
    ## rendering surface.
    ##
    ## Great overview of the fire effect algorithm here: 
    ## http://freespace.virgin.net/hugo.elias/models/m_fire.htm
    ##
    ###############################################################################

    function main
    {
        write-debug "ENTER main"
        ## Rather than a simple red fire, we'll introduce oranges and yellows
        ## by including Yellow as one of the base colours
        $colours = "Yellow","Red","DarkRed","Black"
        
        ## The four characters that we use to dither with, along with the 
        ## percentage of the foreground colour that they show
        $dithering = "█","▓","▒","░"
        $ditherFactor = 1,0.75,0.5,0.25
        
        ## Hold the palette.  We actually store each entry as a BufferCell,
        ## since we need to retain a foreground colour, background colour,
        ## and dithering character.
        [System.Management.Automation.Host.BufferCell[]] $palette = `
            new-object System.Management.Automation.Host.BufferCell[] 256
        
        ## Resize the console to 70, 61 so we have a consistent buffer
        ## size for performance comparison.
        # $bufferSize = new-object System.Management.Automation.Host.Size 70,61
        # $host.UI.RawUI.WindowSize = $bufferSize

        ## Retrieve some commonly used dimensions
        $windowWidth = $host.UI.RawUI.WindowSize.Width
        $windowHeight = $host.UI.RawUI.WindowSize.Height
        $origin = `
            new-object System.Management.Automation.Host.Coordinates 0,0
        $dimensions = `
            new-object System.Management.Automation.Host.Rectangle `
                0,0,$windowWidth,$windowHeight
        
        ## Create our random number generator
        $random = new-object Random
        $workingBuffer = new-object System.Int32[] ($windowHeight * $windowWidth)
        $screenBuffer = new-object System.Int32[] ($windowHeight * $windowWidth)

        ## Store a reference to the Invoke-Inline script to save lookup time
        ## since we run it so often.
        $inline = Get-Command Invoke-Inline.ps1
        
        clear-host
        
        ## Generate the palette
        generatePalette
        # displayPalette
        # return;

        ## Update the buffer, then update the screen until the user presses a key.  
        ## Keep track of the total time and frames generated to let us display
        ## performance statistics.
        $frameCount = 0
        $totalTime = measure-command {
            while(! $host.UI.RawUI.KeyAvailable)
            {
                updateBuffer
                updateScreen
                $frameCount++
            }
        }
        
        ## Clean up and exit
        $host.UI.RawUI.ForegroundColor = "Gray"
        $host.UI.RawUI.BackgroundColor = "Black"
        
        write-host
        write-host "$($frameCount / $totalTime.TotalSeconds) frames per second."
        write-debug "EXIT"
    }

    ## Update a back-buffer to hold all of the information we want to display on
    ## the screen.  To do this, we first re-generate the fire pixels on the bottom 
    ## row.  With that done, we visit every pixel in the screen buffer, and figure
    ## out the average heat of its neighbors.  Once we have that average, we move
    ## that average heat one pixel up.
    function updateBuffer
    {
        ## This function takes the most of our time, so we'll do it inline.
        ## Inputs:
        ##  Window Height
        ##  Window Width
        ##  Screen Buffer
        ##  Random Number Generator
        ## Output:
        ##  Working Buffer
        
        [System.Collections.ArrayList] $inputs = `
            new-object System.Collections.ArrayList
        [void] $inputs.Add([int] $windowHeight)
        [void] $inputs.Add([int] $windowWidth)
        [void] $inputs.Add([int[]] $screenBuffer)
        [void] $inputs.Add([System.Random] $random)
        
        $code = @"
        // Unpack the inputs from our input object
        int windowHeight = (int) ((System.Collections.ArrayList) arg)[0];
        int windowWidth = (int) ((System.Collections.ArrayList) arg)[1];
        int[] screenBuffer = (int[]) ((System.Collections.ArrayList) arg)[2];
        Random random = (Random) ((System.Collections.ArrayList) arg)[3];
        
        // Start fire on the last row of the screen buffer
        for(int column = 0; column < windowWidth; column++)
        {
            // There is an 80% chance that a pixel on the bottom row will
            // start new fire.
            if(random.NextDouble() >= 0.20)
            {
                // The chosen pixel gets a random amount of heat.  This gives
                // us a lot of nice colour variation.
                screenBuffer[(windowHeight - 2) * (windowWidth) + column] = 
                    (int) (random.NextDouble() * 255);
            }
        }
        
        int[] tempWorkingBuffer = (int[]) screenBuffer.Clone();
        
        // Propigate the fire
        int baseOffset = windowWidth + 1;
        for(int row = 1; row < (windowHeight - 1); row++)
        {
            for(int column = 1; column < (windowWidth - 1); column++)
            {
                // Get the average colour from the four pixels surrounding
                // the current pixel
                double colour = 
                    (
                        screenBuffer[baseOffset] + 
                        screenBuffer[baseOffset - 1] + 
                        screenBuffer[baseOffset + 1] + 
                        screenBuffer[baseOffset + windowWidth]
                     ) / 4.0;

                // Cool it off a little.  We apply uneven cooling, otherwise
                // the cool dark red tends to stretch up for too long.
                if(colour > 0)
                {
                    if(colour > 70)
                    { 
                        colour -= 1
                    }
                    else
                    {
                        colour -= 3;
                        
                        if(colour < 1)
                        {
                            colour = 0;
                        }
                        else if(colour < 20)
                        {
                            colour -= 1;
                        }
                    }
                }

                // Store the result into the previous row -- that is, one buffer 
                // cell up.
                tempWorkingBuffer[baseOffset - windowWidth] = (int) colour;
                baseOffset ++;
            }
            
            baseOffset += 2;
        }

        returnValue = tempWorkingBuffer;
    "@

        $returned = & $inline $code $inputs
        $SCRIPT:workingBuffer = $returned
    }

    ## Take the contents of our working buffer and blit it to the screen
    ## We do this in one highly-efficent step (the SetBufferContents) so that
    ## users don't see each individial pixel get updated.
    function updateScreen
    {
        write-debug "ENTER updateScreen"
        
        ## This function takes up a lot of time, so we'll do it inline.
        ## Inputs:
        ##  host.UI.RawUI
        ##  palette
        ##  workingBuffer
        ##  origin
        ##  dimensions
        ##  windowHeight
        ##  windowWidth
        ## Output:
        ##  None
        
        [System.Collections.ArrayList] $inputs = `
            new-object System.Collections.ArrayList
        [void] $inputs.Add([System.Management.Automation.Host.PSHostRawUserInterface] $host.UI.RawUI)
        [void] $inputs.Add([System.Management.Automation.Host.BufferCell[]] $palette)
        [void] $inputs.Add([int[]] $workingBuffer)
        [void] $inputs.Add([System.Management.Automation.Host.Coordinates] $origin)
        [void] $inputs.Add([System.Management.Automation.Host.Rectangle] $dimensions)
        [void] $inputs.Add([int] $windowHeight)
        [void] $inputs.Add([int] $windowWidth)
        
        $code = @"

        System.Management.Automation.Host.PSHostRawUserInterface rawUI = 
            (System.Management.Automation.Host.PSHostRawUserInterface)
                ((System.Collections.ArrayList) arg)[0];
        System.Management.Automation.Host.BufferCell[] palette =
            (System.Management.Automation.Host.BufferCell[]) 
                ((System.Collections.ArrayList) arg)[1];
        int[] workingBuffer = 
            (int[]) ((System.Collections.ArrayList) arg)[2];
        System.Management.Automation.Host.Coordinates origin = 
            (System.Management.Automation.Host.Coordinates)
                ((System.Collections.ArrayList) arg)[3];
        System.Management.Automation.Host.Rectangle dimensions = 
            (System.Management.Automation.Host.Rectangle)
                ((System.Collections.ArrayList) arg)[4];
        int windowHeight = (int) ((System.Collections.ArrayList) arg)[5];
        int windowWidth = (int) ((System.Collections.ArrayList) arg)[6];

        // Create a working buffer to hold the next screen that we want to
        // create.
        System.Management.Automation.Host.BufferCell[,] nextScreen = 
            rawUI.GetBufferContents(dimensions);
        
        // Go through our working buffer (that holds our next animation frame)
        // and place its contents into the buffer that we will soon blast into
        // the real RawUI
        for(int row = 0; row < windowHeight; row++)
        {
            for(int column = 0; column < windowWidth; column++)
            {
                nextScreen[row, column] = palette[workingBuffer[(row * windowWidth) + column]];
            }
        }
        
        // Bulk update the RawUI's buffer with the contents of our next screen
        rawUI.SetBufferContents(origin, nextScreen);
    "@
        & $inline $code $inputs
        
        ## And finally update our representation of the screen buffer to hold
        ## what actually is on the screen
        $SCRIPT:screenBuffer = $workingBuffer.Clone()

        write-debug "EXIT"
    }

    ## Generates a palette of 256 colours.  We create every combination of 
    ## foreground colour, background colour, and dithering character, and then
    ## order them by their visual intensity.
    ##
    ## The visual intensity of a colour can be expressed by the NTSC luminance 
    ## formula.  That formula depicts the apparent brightness of a colour based on 
    ## our eyes' sensitivity to different wavelengths that compose that colour.
    ## http://en.wikipedia.org/wiki/Luminance_%28video%29
    function generatePalette
    {
        ## The apparent intensities of our four primary colours.
        ## However, the formula under-represents the intensity of our straight
        ## red colour, so we artificially inflate it.
        $luminances = 225.93,106.245,38.272,0
        $apparentBrightnesses = @{}

        ## Cycle through each foreground, background, and dither character
        ## combination.  For each combination, find the apparent intensity of the 
        ## foreground, and the apparent intensity of the background.  Finally,
        ## weight the contribution of each based on how much of each colour the
        ## dithering character shows.
        ## This provides an intensity range between zero and some maximum.
        ## For each apparent intensity, we store the colours and characters
        ## that create that intensity.
        $maxBrightness = 0
        for($fgColour = 0; $fgColour -lt $colours.Count; $fgColour++)
        {
            for($bgColour = 0; $bgColour -lt $colours.Count; $bgColour++)
            {
                for($ditherCharacter = 0
                    $ditherCharacter -lt $dithering.Count; 
                    $ditherCharacter++)
                {
                    $apparentBrightness = `
                        $luminances[$fgColour] * $ditherFactor[$ditherCharacter] +
                        $luminances[$bgColour] *
                            (1 - $ditherFactor[$ditherCharacter])
                        
                    if($apparentBrightness -gt $maxBrightness) 
                    { 
                        $maxBrightness = $apparentBrightness 
                    }
                        
                    $apparentBrightnesses[$apparentBrightness] = `
                        "$fgColour$bgColour$ditherCharacter"
                }
           }
        }

        ## Finally, we normalize our computed intesities into a pallete of
        ## 0 to 255.  If a given intensity is 30% towards our maximum intensity,
        ## then it should be in the palette at 30% of index 255.
        $paletteIndex = 0
        foreach($key in ($apparentBrightnesses.Keys | sort))
        {
            $keyValue = $apparentBrightnesses[$key]
            do
            {
                $character = $dithering[[Int32]::Parse($keyValue[2])]
                $fgColour = $colours[[Int32]::Parse($keyValue[0])]
                $bgColour = $colours[[Int32]::Parse($keyValue[1])]
                
                $bufferCell = `
                    new-object System.Management.Automation.Host.BufferCell `
                        $character,
                        $fgColour,
                        $bgColour,
                        "Complete"
                        
                $palette[$paletteIndex] = $bufferCell
                $paletteIndex++
            } while(($paletteIndex / 256) -lt ($key / $maxBrightness))
        }
    }

    ## Dump the palette to the screen.
    function displayPalette
    {
        for($paletteIndex = 254; $paletteIndex -ge 0; $paletteIndex--)
        {
            $bufferCell = $palette[$paletteIndex]
            $fgColor = $bufferCell.ForegroundColor
            $bgColor = $bufferCell.BackgroundColor
            $character = $bufferCell.Character

            $host.UI.RawUI.ForegroundColor = $fgColor
            $host.UI.RawUI.BackgroundColor = $bgColor
            write-host -noNewLine $character
        }
        
        write-host
    }

    . main


     

    [Edit: Monad has now been renamed to Windows PowerShell. This script or discussion may require slight adjustments before it applies directly to newer builds.]
    [Edit: Updated to work with PowerShell RTM and updated Invoke-Inline.ps1 script]

    Comments [2] | | #