Reputation: 33
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
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