Я хочу написать эмулятор gameboy в JavaScript. Разрешение экрана составляет 160 x 144
. Он имеет четыре оттенка цветов: черный, темно-серый, светло-серый и белый. Частота обновления экрана составляет 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
функция screenToBuffer
принимает ~ 25 мс. Если экран будет обновлен со скоростью 60 кадров в секунду, он должен будет отображать каждые 16,6 мс!
Каков наилучший способ решения этой проблемы? Создание совершенно нового 92kb Uint8ClampedArray
и отображение 23 000 цветовых оттенков на кадр являются слишком дорогостоящими.
Я хочу сохранить screen
массив, потому что в моем коде меньше и гораздо проще рассуждать, чем в Uint8ClampedArray
. Я предполагаю, что было бы лучше инициализировать Uint8ClampedArray
один раз и обновлять его всякий раз, когда есть изменение в screen
массиве. Таким образом, я могу просто вернуть буфер RGBA на каждый кадр, и он должен быть уже синхронизирован с моим экраном.
Я также задаюсь вопросом, является ли создание нового 92- ImageData
объекта ImageData
каждого фрейма ресурсоемким, и если может быть лучший способ справиться с этим.
Какие-либо предложения?
Свяжите призывы рисования и сохраните цветовую палитру раньше времени, и вы не должны слишком беспокоиться об этом.
Взгляните на мой фрагмент ниже:
//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 мс за кадр.