246 lines
8.0 KiB
React
246 lines
8.0 KiB
React
|
import React from 'react';
|
||
|
|
||
|
import CenterElement from '/components/center-element';
|
||
|
import FormSelectFiles from '/components/form-select-files';
|
||
|
import CanvasGBAEmulator from '/components/canvas-gba-emulator';
|
||
|
|
||
|
import Endian from '/endian';
|
||
|
|
||
|
import {MIN_WIDTH, MIN_HEIGHT, PACKET_ID_HELLO, PACKET_ID_SEND_FRAME} from '/constants';
|
||
|
|
||
|
function handleClickStartEmulationButton({e, inputRom, inputSaveState, setHiddenFormSelectFiles, canvas}) {
|
||
|
const ctx = canvas.getContext('2d')
|
||
|
e.preventDefault();
|
||
|
if (inputRom.files.length == 0) {
|
||
|
alert('There is no rom still');
|
||
|
return;
|
||
|
}
|
||
|
if (inputSaveState.files.length == 0) {
|
||
|
alert('There is no savestate still');
|
||
|
return;
|
||
|
}
|
||
|
const rom_file = inputRom.files[0];
|
||
|
const savestate_file = inputSaveState.files[0];
|
||
|
rom_file.arrayBuffer().then((rom_buffer) => {
|
||
|
savestate_file.arrayBuffer().then((savestate_buffer) => {
|
||
|
const rom_array = new Uint8Array(rom_buffer);
|
||
|
const savestate_array = new Uint8Array(savestate_buffer);
|
||
|
const websocket = new WebSocket(`ws://localhost:3000/ws`);
|
||
|
websocket.binaryType = 'arraybuffer';
|
||
|
websocket.onclose = (message) => console.log('CLOSE', message);
|
||
|
websocket.onopen = () => {
|
||
|
setHiddenFormSelectFiles(c => false);
|
||
|
console.log('Opened websocket.');
|
||
|
sendHello(websocket, rom_array, savestate_array);
|
||
|
};
|
||
|
websocket.addEventListener('message', (event) => onWebSocketPacket(event, canvas, ctx));
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function concatU8Array(array1, array2) {
|
||
|
const final_array = new Uint8Array(array1.length + array2.length);
|
||
|
final_array.set(array1);
|
||
|
final_array.set(array2, array1.length);
|
||
|
return final_array;
|
||
|
}
|
||
|
|
||
|
function u64ToByteArrayBigEndian(input_number) {
|
||
|
const buffer = new ArrayBuffer(8);
|
||
|
const buffer8 = new Uint8Array(buffer);
|
||
|
const buffer64 = new BigUint64Array(buffer);
|
||
|
buffer64[0] = input_number;
|
||
|
if (Endian.isLittleEndian()) {
|
||
|
buffer8.reverse();
|
||
|
}
|
||
|
return buffer8;
|
||
|
}
|
||
|
|
||
|
function sendPacket(websocket, id, raw_data) {
|
||
|
const packet_u8 = concatU8Array(
|
||
|
concatU8Array(u64ToByteArrayBigEndian(id), u64ToByteArrayBigEndian(BigInt(raw_data.length))),
|
||
|
raw_data
|
||
|
);
|
||
|
const packet_buffer = packet_u8.buffer;
|
||
|
console.log('Sending packet');
|
||
|
websocket.send(packet_buffer);
|
||
|
}
|
||
|
|
||
|
function sendHello(websocket, rom_array, savestate_array) {
|
||
|
console.log('Sending hello.');
|
||
|
const length_rom = BigInt(rom_array.length);
|
||
|
const length_savestate = BigInt(savestate_array.length);
|
||
|
const raw_data =
|
||
|
concatU8Array(
|
||
|
concatU8Array(
|
||
|
concatU8Array(u64ToByteArrayBigEndian(length_rom), rom_array),
|
||
|
u64ToByteArrayBigEndian(length_savestate)
|
||
|
),
|
||
|
savestate_array
|
||
|
);
|
||
|
sendPacket(websocket, PACKET_ID_HELLO, raw_data);
|
||
|
}
|
||
|
|
||
|
function onWebSocketPacket(event, canvas, ctx) {
|
||
|
const buffer = event.data;
|
||
|
let packet_u8 = new Uint8Array(buffer);
|
||
|
const id = byteArrayToU64BigEndian(packet_u8.slice(0, 8));
|
||
|
packet_u8 = packet_u8.slice(8, packet_u8.length);
|
||
|
const size = byteArrayToU64BigEndian(packet_u8.slice(0, 8));
|
||
|
const raw_data = packet_u8.slice(8, packet_u8.length);
|
||
|
packet_u8 = null;
|
||
|
switch (id) {
|
||
|
case PACKET_ID_SEND_FRAME:
|
||
|
handleSendFrame(raw_data, canvas, ctx);
|
||
|
break;
|
||
|
default:
|
||
|
console.log(`Received unknown packet ${id}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let printing_frame = false;
|
||
|
function handleSendFrame(raw_data, canvas, ctx) {
|
||
|
if (printing_frame) {
|
||
|
return;
|
||
|
}
|
||
|
printing_frame = true;
|
||
|
let data = raw_data;
|
||
|
const stride = byteArrayToU32BigEndian(data.slice(0, 4));
|
||
|
data = data.slice(4, data.length);
|
||
|
const output_buffer_size = byteArrayToU32BigEndian(data.slice(0, 8));
|
||
|
data = data.slice(8, data.length);
|
||
|
console.log(data.length / 4 / MIN_WIDTH);
|
||
|
const img_data = ctx.createImageData(MIN_WIDTH, MIN_HEIGHT);
|
||
|
const img_data_u8 = new Uint8Array(img_data.data.buffer);
|
||
|
for (let i = 0; i<data.length; i++) {
|
||
|
if (i % 4 == 3) {
|
||
|
img_data_u8[i] = 255;
|
||
|
continue;
|
||
|
}
|
||
|
img_data_u8[i] = data[i];
|
||
|
}
|
||
|
data = null;
|
||
|
createImageBitmap(img_data).then((bitmap) => drawBitmap(bitmap, canvas, ctx));
|
||
|
}
|
||
|
|
||
|
function byteArrayToU32BigEndian(input_array) {
|
||
|
if (Endian.isLittleEndian()) {
|
||
|
input_array = input_array.reverse();
|
||
|
}
|
||
|
const buffer = input_array.buffer;
|
||
|
const output_u32_array = new Uint32Array(buffer);
|
||
|
return output_u32_array[0];
|
||
|
}
|
||
|
|
||
|
function byteArrayToU64BigEndian(input_array) {
|
||
|
if (Endian.isLittleEndian()) {
|
||
|
input_array = input_array.reverse();
|
||
|
}
|
||
|
const buffer = input_array.buffer;
|
||
|
const output_u64_array = new BigUint64Array(buffer);
|
||
|
return output_u64_array[0];
|
||
|
}
|
||
|
|
||
|
function drawBitmap(bitmap, canvas, ctx) {
|
||
|
ctx.drawImage(bitmap, 0, 0, canvas1.width, canvas1.height);
|
||
|
printing_frame = false;
|
||
|
}
|
||
|
|
||
|
export default function Page() {
|
||
|
const screenDimensions = useScreenDimensions();
|
||
|
const emulatorDimensions = calculateSizeEmulator(screenDimensions);
|
||
|
const canvasRef = React.useRef(null);
|
||
|
function resizeCanvas(node) {
|
||
|
const canvas = canvasRef.current;
|
||
|
if (canvas) {
|
||
|
canvas.width = emulatorDimensions.width;
|
||
|
canvas.height = emulatorDimensions.height;
|
||
|
const ctx = canvas.getContext('2d')
|
||
|
fillBlack(canvas, ctx);
|
||
|
}
|
||
|
};
|
||
|
const [hiddenFormSelectFiles, setHiddenFormSelectFiles] = React.useState(false);
|
||
|
React.useEffect(resizeCanvas, [emulatorDimensions]);
|
||
|
const refInputRom = React.useRef(null);
|
||
|
const refInputSaveState = React.useRef(null);
|
||
|
const onStartEmulation = (e) => {
|
||
|
handleClickStartEmulationButton({
|
||
|
e: e,
|
||
|
setHiddenFormSelectFiles: setHiddenFormSelectFiles,
|
||
|
inputRom: refInputRom.current,
|
||
|
inputSaveState: refInputSaveState.current,
|
||
|
canvas: canvasRef.current,
|
||
|
});
|
||
|
};
|
||
|
return (
|
||
|
<div>
|
||
|
<CenterElement>
|
||
|
<h2>msGBA Emulator Online for GBA.</h2>
|
||
|
</CenterElement>
|
||
|
<CenterElement>
|
||
|
<CanvasGBAEmulator canvasRef={canvasRef}/>
|
||
|
</CenterElement>
|
||
|
<CenterElement hidden={hiddenFormSelectFiles}>
|
||
|
<FormSelectFiles refInputRom={refInputRom}
|
||
|
refInputSaveState={refInputSaveState}
|
||
|
onStartEmulation={onStartEmulation}/>
|
||
|
</CenterElement>
|
||
|
|
||
|
</div>
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function getScreenDimensions() {
|
||
|
return {
|
||
|
width: document.body.clientWidth,
|
||
|
height: document.body.clientHeight
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function useScreenDimensions() {
|
||
|
const [screenDimensions, setScreenDimensions] = React.useState(getScreenDimensions());
|
||
|
|
||
|
React.useEffect(() => {
|
||
|
function onResize() {
|
||
|
setScreenDimensions(getScreenDimensions());
|
||
|
}
|
||
|
|
||
|
window.addEventListener("resize", onResize);
|
||
|
|
||
|
return () => {
|
||
|
window.removeEventListener("resize", onResize);
|
||
|
}
|
||
|
}, []);
|
||
|
|
||
|
return screenDimensions;
|
||
|
}
|
||
|
|
||
|
function fillBlack(canvas, ctx) {
|
||
|
ctx.beginPath();
|
||
|
ctx.rect(0, 0, canvas.width, canvas.height);
|
||
|
ctx.fillStyle = 'black';
|
||
|
ctx.fill();
|
||
|
}
|
||
|
|
||
|
function calculateSizeEmulator(screenDimensions) {
|
||
|
const width = screenDimensions.width;
|
||
|
const height = screenDimensions.height * 0.75;
|
||
|
const emulatorDimensions = {};
|
||
|
if (width < MIN_WIDTH || height < MIN_HEIGHT) {
|
||
|
return {
|
||
|
width: MIN_WIDTH,
|
||
|
height: MIN_HEIGHT,
|
||
|
};
|
||
|
}
|
||
|
const ratioWidth = Math.floor(width / MIN_WIDTH);
|
||
|
const ratioHeight = Math.floor(height / MIN_HEIGHT);
|
||
|
if (ratioWidth < ratioHeight) {
|
||
|
emulatorDimensions.width = MIN_WIDTH * ratioWidth;
|
||
|
emulatorDimensions.height = MIN_HEIGHT * ratioWidth;
|
||
|
} else {
|
||
|
emulatorDimensions.height = MIN_HEIGHT * ratioHeight;
|
||
|
emulatorDimensions.width = MIN_WIDTH * ratioHeight;
|
||
|
}
|
||
|
return emulatorDimensions;
|
||
|
}
|