Matthew
Matthew

Reputation: 33

How can I optimize canvas performance for a 60fps emulator?

I want to write a gameboy emulator in JavaScript. The screen resolution is 160 x 144. It has four shades of colors: black, dark gray, light gray, and white. The gameboy has a screen refresh rate of 60fps.

I have a screen array which has a size of 160*144=23040, where each index stores a color shade from 0-3.

To render the screen to the user, I want to use the HTML5 canvas. From what I understand, the most straightforward way to do this is to create an ImageData object with a Uint8ClampedArray RGBA bitmap and pass it to the canvas via putImageData.

In order to do this, every frame I have to transform my screen to a Uint8ClampedArray, and transform each shade into their respective 4 byte RGBA numbers. This new RGBA bitmap would be (160 * 144 * 4 bytes per pixel = 92160 in size:

function screenToBuffer(screen) {
  return Uint8ClampedArray.from(screen.map(shade => shadeToRGBA(shade)).flat());
}

Then, this buffer is passed to a user-supplied callback function where they would display it to the canvas like so:

onFrame: function(frameBuffer) {
  var image = new ImageData(frameBuffer, 160, 144)
  ctx.putImageData(image, 0, 0);
}

This works fine in theory, but it is a naive approach and I have discovered that it's not efficient at all. By using the profiler, the screenToBuffer function alone takes ~25ms. If the screen is going to be updated at 60fps, it will need to render every 16.6ms!

What is the best way to approach this problem? Creating an entirely new 92kb Uint8ClampedArray and mapping 23,000 color shades per frame is way too costly.

I want to keep the screen array because it is smaller and much easier to reason about in my code than a Uint8ClampedArray. I imagine that it would be better to initialize a Uint8ClampedArray once, and update it whenever there is a change to the screen array. That way I can simply return the RGBA buffer on each frame and it should be already be synced with my screen.

I also wonder if creating a new 92kb ImageData object every frame is also resource intensive and if there might be a better way to handle that.

Any suggestions?

Upvotes: 2

Views: 1116

Answers (1)

Emil S. Jørgensen
Emil S. Jørgensen

Reputation: 6366

Bundle up draw calls and save a color palette ahead of time and you shouldn't have too much of an issue with this.

Take a look at my snippet below:

//Generate output node for time
var output = document.body.appendChild(document.createElement("p"));
//Generate canvas
var canvas = document.body.appendChild(document.createElement("canvas"));
var ctx = canvas.getContext("2d");
canvas.style.width = "500px";
//Setup canvas
canvas.width = 160;
canvas.height = 144;
//Generate color pallette
var colors = ["black", "#444", "#ccc", "white"]
    .map(function (b) {
    ctx.fillStyle = b;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    return ctx.getImageData(0, 0, 1, 1).data;
});
//Generate framedata (referencing back to one of our 4 base colors)
var data = [];
while (data.length < canvas.width * canvas.height) {
    data.push(Math.floor(Math.random() * 4));
}
//draw function
function drawCanvas() {
    //Start time
    var t = Date.now();
    //Fill with basic color
    ctx.fillStyle = "black";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    //Prepare ImageData
    var frame = new ImageData(canvas.width, canvas.height);
    //Loop through buffer
    for (var i = 0; i < data.length; i++) {
        var dataPoint = data[i];
        //Skip base color
        if (dataPoint == 0) {
            continue;
        }
        //Set color from palette
        frame.data.set(colors[dataPoint], i * 4);
    }
    //Put ImageData on canvas
    ctx.putImageData(frame, 0, 0);
    //Output time
    output.textContent = (Date.now() - t).toFixed(2);
    //Schedule next frame
    requestAnimationFrame(drawCanvas);
}
//Start drawing
drawCanvas();
//Start simulation at 60 fps
setInterval(function () {
    data = data.map(function () { return Math.floor(Math.random() * 4); });
}, 16);

I consistently get about 2ms per frame.

Upvotes: 2

Related Questions