Как я могу оптимизировать производительность холста для эмулятора 60fps? - PullRequest
0 голосов
/ 30 октября 2018

Я хочу написать эмулятор gameboy на JavaScript. Разрешение экрана 160 x 144. Он имеет четыре оттенка цвета: черный, темно-серый, светло-серый и белый. Gameboy имеет частоту обновления экрана 60 кадров в секунду.

У меня есть массив screen размером 160*144=23040, где каждый индекс хранит цветовой оттенок 0-3.

Чтобы отобразить экран пользователю, я хочу использовать холст HTML5. Насколько я понимаю, самый простой способ сделать это - создать объект ImageData с растровым изображением Uint8ClampedArray RGBA и передать его на холст с помощью putImageData.

Для этого в каждом кадре мне нужно преобразовать свой экран в Uint8ClampedArray и преобразовать каждый оттенок в соответствующие им 4-байтовые числа RGBA. Это новое растровое изображение RGBA будет иметь размер (160 * 144 * 4 bytes per pixel = 92160:

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

Затем этот буфер передается пользовательской функции обратного вызова, где они отображают его на холст следующим образом:

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

Теоретически это прекрасно работает, но это наивный подход, и я обнаружил, что он вообще неэффективен. При использовании профилировщика только функция screenToBuffer занимает ~ 25 мс. Если экран будет обновляться со скоростью 60 кадров в секунду, он будет рендериться каждые 16,6 мс!

Как лучше всего подойти к этой проблеме? Создание совершенно нового 92kb Uint8ClampedArray и отображение 23 000 цветовых оттенков на кадр слишком дорого.

Я хочу сохранить массив screen, потому что он меньше и намного проще рассуждать в моем коде, чем Uint8ClampedArray. Я полагаю, что было бы лучше инициализировать Uint8ClampedArray один раз и обновлять его всякий раз, когда происходит изменение в массиве screen. Таким образом, я могу просто вернуть буфер RGBA в каждом кадре, и он уже должен быть синхронизирован с моим экраном.

Мне также интересно, если создание нового объекта размером 92 КБ ImageData каждый кадр также требует значительных ресурсов, и может ли быть лучший способ справиться с этим.

Есть предложения?

1 Ответ

0 голосов
/ 30 октября 2018

Объедините вызовы отрисовки и заранее сохраните цветовую палитру, и у вас не должно быть особых проблем с этим.

Посмотрите на мой фрагмент ниже:

//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);

Я последовательно получаю около 2 мс на кадр.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...