Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Haxe & NME: fastest method for per pixel bitmap manipulation

Tags:

haxe

This is a small project for testing pixel level manipulation performance of NME for different builds (Windows c++, Flash).

It uses BitmapData.setPixel to modify the pixels one by one (320x240 for every frame). The C++ build runs at 22 FPS, and the flash build around ~100 FPS. Whats the reason for the huge performance drop for the C++ build compared to flash? How could I improve the code to get higher FPS using the C++ build?

Mandelbrot.hx

import nme.display.Sprite;
import nme.display.Bitmap;
import nme.display.BitmapData;
import nme.text.TextField;
import nme.events.Event;
import nme.events.TimerEvent;
import nme.utils.Timer;
import nme.geom.Matrix;
import nme.geom.Rectangle;
import nme.utils.ByteArray;

class Mandelbrot
{
    public static function main() : Void
    {
        new Mandelbrot();
    }

    public var pixels:Array<Array<Int>>;

    public var colorModifier:Int;
    private var bitmapData:BitmapData;
    private var bigBitmapData:BitmapData;

    private var fps:TextField;

    private var width:Int;
    private var height:Int;
    private var matrix:Matrix;

    public function new() 
    {
        width = 320; //Std.int(flash.Lib.current.stage.stageWidth/2);
        height = 240; //Std.int(flash.Lib.current.stage.stageHeight/2);

        var scale:Float = 2;//flash.Lib.current.stage.stageWidth/width;
        matrix = new Matrix();
        matrix.scale(scale, scale);

        var setBitmap:Bitmap = new Bitmap();
        bitmapData = new BitmapData( width , height , false , 0x000000 );
        bigBitmapData = new BitmapData( nme.Lib.current.stage.stageWidth , nme.Lib.current.stage.stageHeight , false , 0x000000 );

        setBitmap.bitmapData = bigBitmapData;

        nme.Lib.current.addChild( setBitmap );

        var maxIterations:Int = 128;

        pixels = new Array();


        var beforeTime = nme.Lib.getTimer();

        var xtemp;
        var iteration;
        var x0:Float = 0;
        var y0:Float = 0;
        for(ix in 0...width) {
            pixels[ix] = new Array();
            for(iy in 0...height) {
                    x0 = 0;
                    y0 = 0;
                    iteration = 128;

                    while ( x0*x0 + y0*y0 <= 4  &&  iteration > 0 ) 
                    {
                        xtemp = x0*x0 - y0*y0 + (ix-14*5000)/50000;
                        y0 = 2*x0*y0 + (iy-(height/0.6))/50000;
                        x0 = xtemp;

                        iteration--;
                    }

                    pixels[ix][iy] = iteration;
            }
        }

        var afterTime = nme.Lib.getTimer();

        var tf = new TextField();
        tf.width = 400;
        tf.text = "Generating fractal took "+(afterTime-beforeTime)+" ms";
        nme.Lib.current.addChild(tf);

        fps = new TextField();
        fps.width = 400;
        fps.y = 10;
        fps.text = "FPS: ";
        nme.Lib.current.addChild(fps);

        colorModifier = 2;
        var timer:haxe.Timer = new haxe.Timer(10);

        runLoop();
        timer.run = runLoop;
    }

    public function runLoop() {
        var r:Int=0, b:Int=0, g:Int=0;
        var pixel:Int = 0;

        var beforeTime = nme.Lib.getTimer();

        for(iy in 0...height) {
            for(ix in 0...width) {
                pixel = pixels[ix][iy];
                r = pixel + colorModifier;
                g = pixel + colorModifier + r;
                b = pixel + colorModifier + g;
                bitmapData.setPixel(ix, iy, (r<<16 | g<<8 | b));
            }
        }

        bigBitmapData.draw(bitmapData, matrix, null, null, null, false);
        var afterTime = nme.Lib.getTimer();
        fps.text = "FPS: "+Math.round(1000/(afterTime-beforeTime));

        colorModifier += 2;
        if(colorModifier > 65530)
                colorModifier = 0;


    }
}

Mandelbrot.nmml

<?xml version="1.0" encoding="utf-8"?>
<project>
  <app
     file="Mandelbrot.hx"
     title="Mandelbrot sample"
     package="org.haxe.nme.mandelbrot"
     version="1.0.0"
     company="nme" 
     main="Mandelbrot"
  />
  <window
        width="640"
        height="480"
        orientation="landscape"
        fps="60"
        background="0xffffff"
        resizeable="true"
        hardware="true"
    />
  <classpath name="." />
  <haxelib name="nme" />
  <ndll name="std" />
  <ndll name="regexp" />
  <ndll name="zlib" />
  <ndll name="nme" haxelib="nme" />
  <setenv name="SHOW_CONSOLE"/>
</project>
like image 463
Kalevi Avatar asked Apr 14 '12 22:04

Kalevi


People also ask

What is Haxe used for?

1.1 What is Haxe? Haxe consists of a high-level, open source programming language and a compiler. It allows compilation of programs, written using an ECMAScript-oriented syntax, to multiple target languages. Employing proper abstraction, it is possible to maintain a single code-base which compiles to multiple targets.

What is a Haxe file?

A haxelib for consistent cross-platform filesystem operations and proper Windows-/Unix-style path handling. Supports wildcard file searches (globbing).

Is Haxe good for game development?

Write high performance games for every major platform using world-class libraries. Haxe has a wealth of game frameworks and libraries available to help you create your masterpiece. Being able to achieve native speeds across multiple platforms with a single code base gives Haxe developers an edge.


2 Answers

Look into the nme.Memory API. The idea is to create a ByteArray with the correct size (or get it from a BitmapData), select it as the current virtual memory space and manipulate its bytes directly.

You'll get an approximately 10x speed boost with Flash and it should be way faster with the CPP target too. Don't forget to compile in Release mode or method inlining will be disabled and performances will suffer a lot.

Basic usage example (untested code) :

var rect:Rectangle = bitmapData.rect;

// 32bits integer = 4 bytes
var size:Int = bitmapData.width * bitmapData.height * 4;

// The virtual memory space we'll use
var pixels:ByteArray = new ByteArray();

// CPP does not support setting the length property directly
#if (cpp) pixels.setLength(size);
#else pixels.length = size; #end

// Select the memory space (call it once, not every frame)
Memory.select(pixels);

// And in your loop set your color
// Color is in BGRA mode, nme.Memory can only be used in little endian mode.
Memory.setI32((y * width + x) * 4, color);

// When you're done, render the BitmapData
// (don't forget to reset the ByteArray position)
pixels.position = 0;
bitmapData.setPixels(rect, pixels);

Keep in mind this is a very basic code example. In your case, you'd need to adapt it and actually use a double sized ByteArray because you need to store the iteration count too. Nested loops can be optimized in your main loop and you can avoid a lot of extra index/address computations :

// Note the size * 2 !
// First part of the ByteArray will be used to store the iteration count,
// the second part to draw the pixels.
#if (cpp) pixels.setLength(size * 2);
#else pixels.length = size * 2; #end

Memory.select(pixels);

// First loop storing iteration count
for (iy in 0...height)
{
    for (ix in 0...width)
    {
        // ... do some stuff ...
        Memory.setI32((iy * width + ix) << 2, iteration);
    }
}

// In your runLoop :
for (i in 0...(height * width))
{
    // Get the iteration count
    var pixel:Int = Memory.getI32(i << 2);

    r = pixel + colorModifier;
    g = pixel + colorModifier + r;
    b = pixel + colorModifier + g;

    // Note that we're writing the pixel in the second part of our ByteArray
    Memory.setI32(size + (i << 2), r | g << 8 | b << 16);
}

// Sets the position to the second part of our ByteArray
pixels.position = size;
bitmapData.setPixels(rect, pixels);

And this is it. If you really don't want to use Alchemy Opcodes on the Flash target, the next fastest way to blit pixels is to use getVector() / setVector() from the BitmapData class. But it's really not as fast.

like image 62
Chman Avatar answered Oct 23 '22 04:10

Chman


Array itself is not true liner array in flash, more like a map. For the per-pixel manipulation I can recommend to use getVector/setVector api of the BitmapData class, which can retrieve (and assign) a rectangular area of the image as flat pixel data. In which case you can access individual pixels in the vector as:

pixels[ix + image_width*iy] = <argb32>

Also, instead of constructing an intermediate Array of Arrays it would be faster to assign pixels directly.

like image 37
sigman Avatar answered Oct 23 '22 04:10

sigman