CrossBrowdy - Examples

Advanced

CHIP-8 emulator

This is an example of a simple CHIP-8 emulator (using the Game engine module from the previous example).

You can use one or more gamepads (depending on the ROM loaded), the keyboard, the mouse or a touch screen to control the emulator and its software.

This emulator includes some fancy features as for example automatic ROM identification and personalized theme (different visual style) depending on the ROM being emulated. If desired, you can easily enhance these features for more ROMs.

The purpose of this example is to show different ways to use CrossBrowdy and some of its multiple features.

index.html:

<!DOCTYPE html>
<html>
	<head>
		<!-- This file belongs to a CrossBrowdy.com example, made by Joan Alba Maldonado. Creative Commons Attribution 4.0 International License. -->
		<meta http-equiv="content-type" content="text/html; charset=utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
		<link rel="canonical" href="https://crossbrowdy.com/examples/advanced/chip_8_emulator/try" />
		<title>Advanced: CHIP-8 emulator - Example</title>
		<!-- Loads the needed CSS files: -->
		<link rel="stylesheet" type="text/css" href="main.css" />
		<!-- Loads FlashCanvas (Flash emulation) before CrossBrowdy. Needed also to use ExplorerCanvas (VML emulation) without problems: -->
		<!-- Note: it is recommended to download CrossBrowdy instead of hotlinking the online version. This is just for the example! -->
		<script src="https://crossbrowdy.com/CrossBrowdy/CrossBase/audiovisual/image/canvas/FlashCanvas/pro/bin/flashcanvas.js" type="text/javascript" language="javascript"></script><!-- FlashCanvas/ExplorerCanvas do not support lazy load. -->
		<!-- Loads CrossBrowdy.js (main file): -->
		<!-- Note: it is recommended to download CrossBrowdy instead of hotlinking the online version. This is just for the example! -->
		<script src="https://crossbrowdy.com/CrossBrowdy/CrossBrowdy.js" type="text/javascript" language="javascript"></script><!-- "type" and "language" parameters for legacy clients. -->
		<!-- Loads the other needed script files: -->
		<script src="main.js" type="text/javascript" language="javascript"></script>
		<script src="roms_data.js" type="text/javascript" language="javascript"></script>
		<script src="rom_loader.js" type="text/javascript" language="javascript"></script>
		<script src="cpu.js" type="text/javascript" language="javascript"></script>
	</head>
	<body>
		<!-- Toolbar and its icons: -->
		<div id="toolbar">
			<button id="button_reset" class="toolbar_icon" title="Reset em[u]lator" accesskey="u" disabled>R</button>
			<button id="button_file_selector" class="toolbar_icon" title="[L]oad a ROM" accesskey="l" disabled>L</button>
			<button id="button_pause" class="toolbar_icon" title="[P]ause" accesskey="p" disabled>P</button>
			<button id="button_fullscreen" class="toolbar_icon" title="[T]oggle Fullscreen mode" accesskey="t" disabled>F</button>
			<input type="file" id="file_selector" name="file_selector" disabled />
			<select id="rom_selector" disabled>
				<option id="none" value="none">-- Select a ROM --</option>
			</select>
			<input type="text" value="2" id="cpl_input" name="cpl_input" size="3" maxlength="5" placeholder="CPL" title="Cycles Per Loop (CPL)" disabled />
		</div>
		<!-- Screen controls: -->
		<div id="controls_toggler" onClick="screenControlsToggle();">T</div>
		<div id="controls">
			<center>
				<span id="screen_button_1" class="screen_button disabled">1 <sup>1</sup></span>
				<span id="screen_button_2" class="screen_button disabled">2 <sup>2</sup></span>
				<span id="screen_button_3" class="screen_button disabled">3 <sup>3</sup></span>
				<span id="screen_button_12" class="screen_button disabled">C <sup>4</sup></span>
				<br />
				<span id="screen_button_4" class="screen_button disabled">4 <sup>Q</sup></span>
				<span id="screen_button_5" class="screen_button disabled">5 <sup>W</sup></span>
				<span id="screen_button_6" class="screen_button disabled">6 <sup>E</sup></span>
				<span id="screen_button_13" class="screen_button disabled">D <sup>R</sup></span>
				<br />
				<span id="screen_button_7" class="screen_button disabled">7 <sup>A</sup></span>
				<span id="screen_button_8" class="screen_button disabled">8 <sup>S</sup></span>
				<span id="screen_button_9" class="screen_button disabled">9 <sup>D</sup></span>
				<span id="screen_button_14" class="screen_button disabled">E <sup>F</sup></span>
				<br />
				<span id="screen_button_10" class="screen_button disabled">A <sup>Z</sup></span>
				<span id="screen_button_0" class="screen_button disabled">0 <sup>X</sup></span>
				<span id="screen_button_11" class="screen_button disabled">B <sup>C</sup></span>
				<span id="screen_button_15" class="screen_button disabled">F <sup>V</sup></span>
			</center>
		</div>
		<div id="loading">Loading...</div>
		<div id="debug_switch"><label for="debug_checkbox"><input type="checkbox" id="debug_checkbox" name="debug_checkbox" />Debug</label></div>
		<div id="loading_rom">Loading ROM...</div>
		<div id="waiting_for_any_key">Waiting for any key!</div>
		<div id="error">Error</div>
		<canvas id="my_canvas">if you read this, canvas is not working</canvas><!-- Some emulation methods will require the canvas element created in HTML (not dynamically by JavaScript). -->
		<canvas id="my_canvas_buffer">if you read this, canvas is not working</canvas><!-- Some emulation methods will require the canvas element created in HTML (not dynamically by JavaScript). -->
		<button id="start_button" onClick="emulatorStart();">
			<span style="display:block; margin-bottom:10px; color:#cc0088;">This example belongs to CrossBrowdy.com, made by Joan Alba Maldonado. Creative Commons Attribution 4.0 International License.</span>
			<span style="display:block; margin-bottom:10px; color:#ff0000;">
				You can use one or more gamepads (depending on the ROM loaded), the keyboard, the mouse or a touch screen to control the emulator and its software.
			</span>
			<span style="display:block; margin-top:10px;">Start emulator!</span>
		</button>
		<br />
		<!-- The "CB_console" element will be used automatically in the case that the client does not support console: -->
		<div id="CB_console" style="display:none; visibility:hidden; overflow:scroll;">
			<span style="font-weight:bold;">Console:</span><br />
		</div>
		<div id="crossbrowdy_info"><a href="https://crossbrowdy.com/examples/advanced/chip_8_emulator" target="_blank">CrossBrowdy.com example</a></div>
	</body>
</html>

main.css:

/* This file belongs to a CrossBrowdy.com example, made by Joan Alba Maldonado. Creative Commons Attribution 4.0 International License. */

body { background-color:#555555; word-wrap:break-word; }
#crossbrowdy_info { position:fixed; bottom:2px; right:2px; z-index:5; }
#crossbrowdy_info a { color:#00aadd; }
#crossbrowdy_info a:hover { color:#0033aa; }
#CB_console { width:460px; height:100px; background-color:#aaaaaa; color:#ddddff; }
button { cursor:pointer; cursor:hand; }
span { color:#aa0000; }
#debug_switch { position:absolute; bottom:0px; left:6px; z-index:2; color:#ffaa00; }
#debug_switch, label, #debug_checkbox  { cursor:hand; cursor:pointer; }
#loading
{
	position:absolute;
	left:0px;
	top:0px;
	width:100%;
	height:100%;
	color:#ff0000;
	background-color:#ffffff;
	z-index:5;
}
#waiting_for_any_key { position:absolute; left:0px; top:0px; visibility:hidden; display:none; text-align:center; color:#ffff00; font-size:28px; line-height:28px; z-index:2; }
#loading_rom { position:absolute; left:0px; top:0px; visibility:hidden; display:none; text-align:center; color:#ff0000; font-size:28px; line-height:28px; z-index:2; }
#error { position:absolute; left:0px; top:0px; visibility:hidden; display:none; text-align:center; color:#ff0000; font-size:28px; line-height:28px; z-index:2; }
#waiting_for_any_key, #loading_rom, #error
{
	filter:alpha(opacity=70);
	opacity:0.7;
	-moz-opacity:0.7;
	-khtml-opacity:0.7;
	-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)";
	animation:blinking 1s ease-in-out infinite;
}
#my_canvas { position:absolute; left:0px; top:0px; z-index:1; }
#my_canvas_buffer { position:absolute; left:0px; top:0px; visibility:hidden; display:none; z-index:1; }
#start_button
{
	z-index:4;
	visibility: hidden;
	display: none;
	position:absolute;
	left:10%;
	top:10%;
	width:80%;
	height:80%;
	color:#ff0000;
	font-size:12px !important;
	font-size:0.58em !important;
	font-size:58% !important;
	font-size:2.8vmin !important;
	font-weight:bold;
	filter:alpha(opacity=90);
	opacity:0.9;
	-moz-opacity:0.9;
	-khtml-opacity:0.9;
	-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=90)";
}
#start_button:hover
{
	color:#ffaa00;
	filter:alpha(opacity=100);
	opacity:1;
	-moz-opacity:1;
	-khtml-opacity:1;
	-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
}
#toolbar { z-index:3; position:absolute; left:5px; top:0px; }
.toolbar_icon { cursor:pointer; cursor:hand; }
.toolbar_icon:disabled { cursor:default; }
#file_selector { visibility:hidden; display:none; }
#rom_selector { margin-top:2px; }
#cpl_input { margin:0px; padding:0px; border:1px solid #555555; }
#controls_toggler
{
	z-index:3;
	position:absolute;
	bottom:50px;
	right:0px;

	text-align:center;
	font-weight:bold;
	line-height:50px;
	color:#ffffff;
	border:1px dashed #ffffff;
	background-color:#5555aa;
	cursor:pointer;
	cursor:hand;
	width:50px;
	height:50px;
	margin:2px;
	
	border-radius:12px;
	-moz-border-radius:12px;
	-webkit-border-radius:12px;
	-khtml-border-radius:12px;

	filter:alpha(opacity=50);
	opacity:0.5;
	-moz-opacity:0.5;
	-khtml-opacity:0.5;
	-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
}
#controls_toggler.controls_hidden
{
	background-color:#0000ff;
}
#controls
{
	z-index:3;
	position:absolute;
	bottom:50px;
	right:50px;
	filter:alpha(opacity=50);
	opacity:0.5;
	-moz-opacity:0.5;
	-khtml-opacity:0.5;
	-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
}
.screen_button
{
	display:inline-block;
	text-align:center;
	font-weight:bold;
	line-height:100px;
	color:#ffffff;
	border:1px dotted #ff0000;
	background-color:#0000aa;
	cursor:pointer;
	cursor:hand;
	width:100px;
	height:100px;
	margin:2px;
	border-radius:12px;
	-moz-border-radius:12px;
	-webkit-border-radius:12px;
	-khtml-border-radius:12px;
}
.screen_button.used
{
	background-color:#aa0000;
}
.screen_button.pressed
{
	background-color:#aa00aa;
}
.screen_button.disabled
{
	background-color:#0000aa;
	cursor:default;
}
sup
{
	color:#aaaa00;
	font-size:70%;
}
@keyframes blinking
{
	0%
	{
		filter:alpha(opacity=70);
		opacity:0.7;
		-moz-opacity:0.7;
		-khtml-opacity:0.7;
		-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)";
	}

	50%
	{
		filter:alpha(opacity=0);
		opacity:0;
		-moz-opacity:0;
		-khtml-opacity:0;
		-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
	}

	100%
	{
		filter:alpha(opacity=70);
		opacity:0.7;
		-moz-opacity:0.7;
		-khtml-opacity:0.7;
		-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)";
	}
}

cpu.js:

/* This file belongs to a CrossBrowdy.com example, made by Joan Alba Maldonado. Creative Commons Attribution 4.0 International License. */
/* Documentation source: http://www.multigesture.net/articles/how-to-write-an-emulator-chip-8-interpreter/ */
/* Some code by Rodrigo Navarro taken (modified) from: https://github.com/reu/chip8.js/blob/master/src/cpu.js */

var EMULATOR_DEBUG = false;

var EMULATOR_CYCLES_PER_LOOP_DEFAULT = 2; //Number of CPU cycles per emulator loop by default. The higher the number, the faster it will be.
var emulatorCyclesPerLoop = EMULATOR_CYCLES_PER_LOOP_DEFAULT; //Number of CPU cycles per emulator loop, currently used. The higher the number, the faster it will be.

var lastROMIdLoaded = null; //It will keep the last ROM identifier loaded (if any).
var lastROMContentLoaded = null; //It will keep the last ROM content loaded.

var screenBitMap = null; //Array that will point to the bitmap used by the graphic rendering engine.

var emulatorPaused = true; //Defines whether the emulator is paused or not.

var emulatorError = false; //Defines whether any error has been detected or not (to stop emulation).

var emulatorInitialized = false; //Defines whether the emulator has been initialized (with all necessary values) or not.

var opcode = null; //It will store the current opcode being executed.
var mem = null; //Memory content. It will be a 'Uint8Array' typed array of 4096 bytes (initialized later as typed arrays might need to be polyfilled by CrossBrowdy first).
var v = null; //V registers (V0, V1, V2... to VE). It will be a 'Uint8Array' typed array of 16 bytes (initialized later as typed arrays might need to be polyfilled by CrossBrowdy first)
var i = null; //Index register.
var pc = null; //Program counter.
var stack = new Array(16); //Stack content.
var sp = null; //Stack pointer.

var timerDelay = null; //Countdown 60Hz timer for delays.
var timerDelayMsToDecrease = 1; //Milliseconds to decrease the timer delay (1 by default). It will updated internally automatically depending on the ROM loaded.
var timerSound = null; //Countdown 60Hz timer for sounds.

var drawFlag = false; //Flag to define whether to draw the screen or not.
var screenRendered = true; //Flag to check whether the screen has been rendered by the graphic rendering engine or not.

var waitingForKey = false;

var CPS = 0; //Last cycles per second performed.
var cyclesCounter = 0; //Number of cycles performed. It will be cleared automatically each second.

var KEYS_MAP_DEFAULT = null; //It will keep the keys mapping (for keyboard and controllers) by default. Filled automatically later.
var keysMap = KEYS_MAP_DEFAULT; //It will keep the keys mapping (for keyboard and controllers) for the current ROM.

//Font set used for the memory:
var fontSet =
[ 
	0xF0, 0x90, 0x90, 0x90, 0xF0, //0.
	0x20, 0x60, 0x20, 0x20, 0x70, //1.
	0xF0, 0x10, 0xF0, 0x80, 0xF0, //2.
	0xF0, 0x10, 0xF0, 0x10, 0xF0, //3.
	0x90, 0x90, 0xF0, 0x10, 0x10, //4.
	0xF0, 0x80, 0xF0, 0x10, 0xF0, //5.
	0xF0, 0x80, 0xF0, 0x90, 0xF0, //6.
	0xF0, 0x10, 0x20, 0x40, 0x40, //7.
	0xF0, 0x90, 0xF0, 0x90, 0xF0, //8.
	0xF0, 0x90, 0xF0, 0x10, 0xF0, //9.
	0xF0, 0x90, 0xF0, 0x90, 0x90, //A.
	0xE0, 0x90, 0xE0, 0x90, 0xE0, //B.
	0xF0, 0x80, 0x80, 0x80, 0xF0, //C.
	0xE0, 0x90, 0x90, 0x90, 0xE0, //D.
	0xF0, 0x80, 0xF0, 0x80, 0xF0, //E.
	0xF0, 0x80, 0xF0, 0x80, 0x80  //F.
];



//Resets the status of the memory, registers, etc.:
function resetStatus(ROMContent)
{
	emulatorPaused = true; //Pauses the emulator.
	
	CB_console("Resetting status...");
	
	emulatorError = false;
	
	//Hides the "Waiting for any key" and the error message (just in case they were showing):
	waitingForKey = false;
	CB_Elements.hideById("waiting_for_any_key");
	CB_Elements.hideById("error");
	
	//If it was not created before, creates the typed arrays:
	mem = mem || new Uint8Array(4096);
	v = v || new Uint8Array(16)
	
	for (var x = 0; x < 16; x++) { v[x] = stack[x] = 0; mem[x] = fontSet[x]; } //Clears the V registers and the stack and loads the first part of the the font set into the memory.
	for (; x < 80; x++) { mem[x] = fontSet[x]; } //Loads the font set into the memory.
	
	//If there is ROM content to load, loads it:
	if (ROMContent && ROMContent.length)
	{
		for (; x < 512; x++) { mem[x] = 0; } //Clear the memory until 512 (0x200), without including it.
		CB_console("Filling the memory with the ROM...");
		var lastPositionROM = 512 + ROMContent.length;
		var ROMContentPointer = 0;
		for (; x < lastPositionROM; x++) { mem[x] = ROMContent[ROMContentPointer++]; }
		lastROMContentLoaded = ROMContent;
	}
	for (; x < 4096; x++) { mem[x] = 0; } //Clears the rest of the memory (if any).
	
	opcode = 0;
	i = 0;
	pc = 0x200; //Program counter starts at 0x200 (512).
	_pcPrevious = null;
	sp = 0;
	timerDelay = 0;
	timerSound = 0;
	
	//Initialize/clears the screen (its bitmap):
	clearScreen();
	
	//Defines the keys mapping by default:
	if (KEYS_MAP_DEFAULT === null)
	{
		KEYS_MAP_DEFAULT =
		{
			0x1:
			{
				keys: [ CB_Keyboard.keys._1 ],
				controllers: { gamepadIndex: "", axis: { index: 0, min: -1, max: -0.5 }	}
			},
			0x2:
			{
				keys: [ CB_Keyboard.keys._2 ],
				controllers: { gamepadIndex: "", axis: { index: 0, min: 0.5, max: 1 } }
			},
			0x3:
			{
				keys: [ CB_Keyboard.keys._3 ],
				controllers: { gamepadIndex: "", axis: { index: 1, min: -1, max: -0.5 }	}
			},
			0xC:
			{
				keys: [ CB_Keyboard.keys._4 ],
				controllers: { gamepadIndex: "", axis: { index: 1, min: 0.5, max: 1 } }
			},
			0x4:
			{
				keys: [ CB_Keyboard.keys.Q ],
				controllers: { gamepadIndex: "", buttons: [ 0 ]	}
			},
			0x5:
			{
				keys: [ CB_Keyboard.keys.W ],
				controllers: { gamepadIndex: "", buttons: [ 1 ]	}
			},
			0x6:
			{
				keys: [ CB_Keyboard.keys.E ],
				controllers: { gamepadIndex: "", buttons: [ 2 ]	}
			},
			0xD:
			{
				keys: [ CB_Keyboard.keys.R ],
				controllers: { gamepadIndex: "", buttons: [ 3 ]	}
			},
			0x7:
			{
				keys: [ CB_Keyboard.keys.A ],
				controllers: { gamepadIndex: "", buttons: [ 4 ] }
			},
			0x8:
			{
				keys: [ CB_Keyboard.keys.S ],
				controllers: { gamepadIndex: "", buttons: [ 5 ] }
			},
			0x9:
			{
				keys: [ CB_Keyboard.keys.D ],
				controllers: { gamepadIndex: "", buttons: [ 6 ]	}
			},
			0xE:
			{
				keys: [ CB_Keyboard.keys.F ],
				controllers: { gamepadIndex: "", buttons: [ 7 ]	}
			},
			0xA:
			{
				keys: [ CB_Keyboard.keys.Z ],
				controllers: { gamepadIndex: "", buttons: [ 8 ]	}
			},
			0x0:
			{
				keys: [ CB_Keyboard.keys.X ],
				controllers: { gamepadIndex: "", buttons: [ 9 ] }
			},
			0xB:
			{
				keys: [ CB_Keyboard.keys.C ],
				controllers: { gamepadIndex: "", buttons: [ 10 ] }
			},
			0xF:
			{
				keys: [ CB_Keyboard.keys.V ],
				controllers: { gamepadIndex: "", buttons: [ 11 ] }
			}
		};
	}
	
	emulatorInitialized = true;
}


//Loads the given file content which belongs to a ROM:
var loadingROMHidden = false; //Tells whether the loading ROM message has been hidden or not.
function loadROMContent(ROMContent, cyclesPerLoop, keysMapROM, ROMId)
{
	emulatorPaused = true; //Pauses the emulator.
	
	ROMContent = ROMContent || lastROMContentLoaded;
	if (!ROMContent)
	{
		loadingROMHidden = true;
		CB_Elements.hideById("loading_rom");
		CB_Elements.showById("error");
		CB_console("Cannot load/reload ROM content. It is empty!");
		return;
	}

	timerDelayMsToDecrease = 1; //Default value.

	//If not given a ROM id, tries to identify the ROM through its data:
	if (!CB_isString(ROMId) || !ROMId)
	{
		CB_console("ROM identifier not given. Trying to identify the ROM...")
		ROMId = getROMIdFromROMContent(ROMContent);
		if (ROMId !== null)
		{
			CB_console("* ROM identified with ID: " + ROMId);
			
			//If possible, select the ROM in the ROM selector:
			if (ROMs[ROMId])
			{
				var ROMSelectorElement = CB_Elements.id("rom_selector");
				if (ROMSelectorElement !== null)
				{
					ROMSelectorElement.value = ROMSelectorElement.selectedIndex = ROMId;
					CB_console("ROM selector set to: " + (ROMSelectorElement.value || ROMSelectorElement.selectedIndex) + " (index: " + ROMSelectorElement.selectedIndex + ")");
				}
			}
		}
		else { CB_console("* ROM could not be identified!"); }
	}

	//If a ROM identifier is set, fills the non-given data (if possible):
	lastROMIdLoaded = null;
	if (CB_isString(ROMId) && ROMId !== "")
	{
		if (ROMs[ROMId])
		{
			CB_console("ROM ID given (" + ROMId + ") found. Loading needed data...");
			CB_console
			(
				"Title / ROM ID: " + ROMId + "\n" +
				"Author: " + (ROMs[ROMId].author || "Unknown") + "\n" +
				"Year: " + (ROMs[ROMId].year || "Unknown") + "\n" +
				"File: " + ROMs[ROMId].file + "\n" +
				"Cycles per loop: " + (ROMs[ROMId].cyclesPerLoop ? ROMs[ROMId].cyclesPerLoop : EMULATOR_CYCLES_PER_LOOP_DEFAULT + " (default)") + "\n" +
				"Keys mapping: " + (ROMs[ROMId].keysMapping ? JSON.stringify(ROMs[ROMId].keysMapping) : "no")
			);
			if (!cyclesPerLoop) { cyclesPerLoop = ROMs[ROMId].cyclesPerLoop; CB_console("Cycles per loop (" + cyclesPerLoop + ") loaded from the data of the given ROM ID (" + ROMId + ")!"); }
			if (!keysMapROM) { keysMapROM = ROMs[ROMId].keysMapping; CB_console("Keys map loaded from the data of the given ROM ID (" + ROMId + ")!"); }
			lastROMIdLoaded = ROMId;
		}
		else { CB_console("ROM with ID " + ROMId + " not found!"); }
		
		if (typeof(ROMs[ROMId].timerDelayMsToDecrease) !== "undefined" && ROMs[ROMId].timerDelayMsToDecrease !== null && !isNaN(ROMs[ROMId].timerDelayMsToDecrease))
		{
			timerDelayMsToDecrease = ROMs[ROMId].timerDelayMsToDecrease;
		}
	}

	CB_console("Timer will be decreased each " + timerDelayMsToDecrease + " millisecond(s).");

	loadingROMHidden = false; //Marks the loading message as showing.
	CB_Elements.showById("loading_rom"); //Loading message will be showing already when loading the ROM for the first time but not when resetting.

	CB_console("Loading the following ROM:");
	CB_console(JSON.stringify(ROMContent));
	
	//Resets the status of the memory, registers, etc. and loads the ROM content into the memory:
	resetStatus(ROMContent);
	
	CB_console("Memory:");
	CB_console(JSON.stringify(mem));
	
	//If given (and it is greater than zero), sets the desired cycles per loop:
	emulatorCyclesPerLoop = cyclesPerLoop || EMULATOR_CYCLES_PER_LOOP_DEFAULT;
	CB_console("Cycles per loop set to: " + emulatorCyclesPerLoop);

	var CPLInputElement = CB_Elements.id("cpl_input");
	if (CPLInputElement !== null) { CPLInputElement.value = emulatorCyclesPerLoop; }

	//Enables toolbar icons (including the reset button):
	enableElements([ "button_reset", "button_pause", "button_fullscreen", "button_file_selector", "file_selector", "rom_selector", "cpl_input" ], true);
	
	//If given, sets the keys mapping desired:
	keysMap = CB_copyObject(KEYS_MAP_DEFAULT);
	var keyMapped = false;
	for (var keyCode in keysMap)
	{
		keyMapped = false;
		if (keysMapROM && keysMapROM[keyCode])
		{
			CB_console(keyCode + " key specially mapped for this ROM...");
			if (keysMapROM[keyCode].keys || keysMapROM[keyCode].keys === null)
			{
				keysMap[keyCode].keys = keysMapROM[keyCode].keys;
				keyMapped = !(keysMapROM[keyCode].keys === null);
				CB_console("* Keys: ");
				CB_console(keysMap[keyCode].keys);
			}
			if (keysMapROM[keyCode].controllers || keysMapROM[keyCode].controllers === null)
			{
				keysMap[keyCode].controllers = keysMapROM[keyCode].controllers;
				keyMapped = !(keysMapROM[keyCode].controllers === null);
				CB_console("* Controller(s): ");
				CB_console(JSON.stringify(keysMap[keyCode].controllers));
			}
		}
		if (keyMapped)
		{
			CB_Elements.addClassById("screen_button_" + keyCode, "used", true); //Only adds the class in the case it is not being used already.
			CB_console(keyCode + " screen button highlighted.");
		}
		else
		{
			CB_Elements.removeClassById("screen_button_" + keyCode, "used");
			CB_console(keyCode + " screen button NOT highlighted.");
		}
	}
	
	emulatorPaused = false; //Starts the emulator.
}


//Performs the emulation loop:
var emulationLoopTimeout = null;
function emulationLoop()
{
	var performed = true;
	if (!emulatorError && emulatorInitialized)
	{
		for (var j = 0; j < emulatorCyclesPerLoop && performed; j++)
		{
			performed = performCycle();
			cyclesCounter++;
		}
		if (!performed)
		{
			//If not hidden, hides the loading ROM message:
			if (!loadingROMHidden)
			{
				loadingROMHidden = true; //Marks the loading message as hidden.
				CB_Elements.hideById("loading_rom"); //Hides the loading message.
			}
		
			CB_console("Emulator cannot process more cycles due to an error.");
			emulatorError = true;
			CB_Elements.showById("error");
		}
	}

	emulationLoopTimeout = CB_symmetricCall(emulationLoop, 1, "emulationLoopTimeout");
};


//Performs a CPU cycle:
var a, b; //It will execute the opcode's argument being executed.
var lastTiming = 0;
function performCycle()
{
	//Decodes and performs the current opcode:
	opcode = mem[pc] << 8 | mem[pc + 1];
	a = (opcode & 0x0F00) >> 8;
    b = (opcode & 0x00F0) >> 4;
	
	if (!emulatorPaused)
	{
		pc += 2;
		var opcodeFound = performOpcode(opcode, a, b);
		if (!opcodeFound)
		{
			emulatorPaused = true;
			CB_console("Opcode could not be found! Emulator paused.");
			return false;
		}
	}
	
	//If desired, draws/updates the screen:
	if (drawFlag)
	{
		if (EMULATOR_DEBUG) { CB_console("Drawing/updating screen..."); }
		drawScreen();
		screenRendered = false;
		drawFlag = false;
	}

	//Update timers:
	if (!emulatorPaused)
	{
		if (timerDelay > 0 && CB_Device.getTiming() - lastTiming > timerDelayMsToDecrease) { timerDelay--; lastTiming = CB_Device.getTiming(); }
		if (timerSound > 0)
		{
			if (timerSound === 1) { playSoundFx("beep"); } //Plays a "beep" sound.
			timerSound--; //if (CB_Device.getTiming() - lastTiming > 1) { timerSound--; }
		}
		//lastTiming = CB_Device.getTiming();
	}
	else if (waitingForKey)
	{
		//If not hidden, hides the loading ROM message:
		if (!loadingROMHidden)
		{
			loadingROMHidden = true; //Marks the loading message as hidden.
			CB_Elements.hideById("loading_rom"); //Hides the loading message.
		}
		
		CB_console("Waiting for any key...");
		CB_Elements.showById("waiting_for_any_key");
		for (var keyCode in keysMap)
		{
			if (isKeyPressed(keyCode))
			{
				v[vRegisterToStoreKey] = keyCode;
				emulatorPaused = false;
				waitingForKey = false;
				CB_Elements.hideById("waiting_for_any_key");
				break;
			}
		}
	}

	return true;
}


//Processes the given opcode:
//* Some code by Rodrigo Navarro taken (modified) from: https://github.com/reu/chip8.js/blob/master/src/cpu.js
var _pcPrevious = null; //Keeps the previous PC (Program Counter), to avoid infinite loops.
function performOpcode(opcode, a, b)
{
	var error = false;
	var emptyOpcode = false;
	switch (opcode & 0xF000)
	{

		case 0x000:
			switch(opcode)
			{
				case 0x00E0:
					if (EMULATOR_DEBUG) { CB_console("Executing opcode = " + opcode + " = 0x00E0"); }
					clearScreen();
					break;
					
				case 0x00EE:
					if (EMULATOR_DEBUG) { CB_console("Executing opcode = " + opcode + " = 0x00EE"); }
					pc = stack[--sp];
					break;
					
				default:
					error = true;
					emptyOpcode = true;
					break;
			}
			break;

			case 0x1000:
				if (EMULATOR_DEBUG) { CB_console("Executing opcode = " + opcode + " = 0x1000"); }
				_pcPrevious = pc - 2;
				pc = opcode & 0x0FFF;
				//If the previous PC is the same as the new one, restarts the ROM to avoid infinite loop:
				if (_pcPrevious === pc)
				{
					CB_console("Infinite loop detected! Reloading ROM...");
					loadROMContent(undefined, undefined, undefined, lastROMIdLoaded);
				}
				break;

			case 0x2000:
				if (EMULATOR_DEBUG) { CB_console("Executing opcode = " + opcode + " = 0x2000"); }
				stack[sp++] = pc;
				pc = opcode & 0x0FFF;
				break;

			case 0x3000:
				if (EMULATOR_DEBUG) { CB_console("Executing opcode = " + opcode + " = 0x3000"); }
				if (v[a] == (opcode & 0x00FF)) { pc += 2; }
				break;

			case 0x4000:
				if (EMULATOR_DEBUG) { CB_console("Executing opcode = " + opcode + " = 0x4000"); }
				if (v[a] != (opcode & 0x00FF)) { pc += 2; }
				break;

			case 0x5000:
				if (EMULATOR_DEBUG) { CB_console("Executing opcode = " + opcode + " = 0x5000"); }
				if (v[a] == v[b]) { pc += 2; }
				break;

			case 0x6000:
				if (EMULATOR_DEBUG) { CB_console("Executing opcode = " + opcode + " = 0x6000"); }
				v[a] = opcode & 0x00FF;
				break;

			case 0x7000:
				if (EMULATOR_DEBUG) { CB_console("Executing opcode = " + opcode + " = 0x7000"); }
				v[a] += opcode & 0x00FF;
				break;

			case 0x8000:
				switch (opcode & 0x000F)
				{
					case 0x0000:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x000F = " + opcode + " & 0x000F = 0x0000"); }
						v[a] = v[b];
						break;

					case 0x0001:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x000F = " + opcode + " & 0x000F = 0x0001"); }
						v[a] = v[a] | v[b];
						break;

					case 0x0002:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x000F = " + opcode + " & 0x000F = 0x0002"); }
						v[a] = v[a] & v[b];
						break;

					case 0x0003:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x000F = " + opcode + " & 0x000F = 0x0003"); }
						v[a] = v[a] ^ v[b];
						break;

					case 0x0004:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x000F = " + opcode + " & 0x000F = 0x0004"); }
						var sum = v[a] + v[b];
						if (sum > 0xFF) { v[0xF] = 1; }
						else { v[0xF] = 0;	}
						v[a] = sum;
						break;

					case 0x0005:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x000F = " + opcode + " & 0x000F = 0x0005"); }
						if (v[a] > v[b]) { v[0xF] = 1; }
						else { v[0xF] = 0; }
						v[a] = v[a] - v[b];
						break;

					case 0x0006:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x000F = " + opcode + " & 0x000F = 0x0006"); }
						v[0xF] = v[a] & 0x01;
						v[a] = v[a] >> 1;
						break;

					case 0x0007:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x000F = " + opcode + " & 0x000F = 0x0007"); }
						if (v[a] > v[b]) { v[0xF] = 0; }
						else { v[0xF] = 1; }
						v[a] = v[b] - v[a];
						break;

					case 0x000E:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x000F = " + opcode + " & 0x000F = 0x000E"); }
						v[0xF] = v[a] & 0x80;
						v[a] = v[a] << 1;
						break;
						
					default:
						error = true;
						break;
				}
				break;

			case 0x9000:
				if (EMULATOR_DEBUG) { CB_console("Executing opcode = " + opcode + " = 0x9000"); }
				if (v[a] != v[b]) { pc += 2; }
				break;

			case 0xA000:
				if (EMULATOR_DEBUG) { CB_console("Executing opcode = " + opcode + " = 0xA000"); }
				i = opcode & 0x0FFF;
				break;

			case 0xB000:
				if (EMULATOR_DEBUG) { CB_console("Executing opcode = " + opcode + " = 0xB000"); }
				pc = (opcode & 0x0FFF) + v[0];
				break;

			case 0xC000:
				if (EMULATOR_DEBUG) { CB_console("Executing opcode = " + opcode + " = 0xC000"); }
				v[a] = Math.floor(Math.random() * 0xFF) & (opcode & 0x00FF);
				break;

			case 0xD000:
				if (EMULATOR_DEBUG) { CB_console("Executing opcode = " + opcode + " = 0xD000"); }
				var row, col, sprite, height = opcode & 0x000F;
				v[0xF] = 0;
				for (row = 0; row < height; row++)
				{
					sprite = mem[i + row];
					for (col = 0; col < 8; col++)
					{
						if ((sprite & 0x80) > 0)
						{
							if (drawPixel(v[a] + col, v[b] + row)) { v[0xF] = 1; } //Collision.
							
							//If not hidden, hides the loading ROM message (since at least one pixel has been drawn):
							if (!loadingROMHidden)
							{
								loadingROMHidden = true; //Marks the loading message as hidden.
								CB_Elements.hideById("loading_rom"); //Hides the loading message.
							}
						}
						sprite <<= 1;
					}
				}
				drawFlag = true;
				break;

			case 0xE000:
				switch (opcode & 0x00FF)
				{
					case 0x009E:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x00FF = " + opcode + " & 0x00FF = 0x009E"); }
						if (isKeyPressed(v[a])) { pc += 2; }
						break;

					case 0x00A1:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x00FF = " + opcode + " & 0x00FF = 0x00A1"); }
						if (!isKeyPressed(v[a])) { pc += 2; }
						break;
						
					default:
						error = true;
						break;
				}
				break;

			case 0xF000:
				switch (opcode & 0x00FF)
				{
					case 0x0007:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x00FF = " + opcode + " & 0x00FF = 0x0007"); }
						v[a] = timerDelay;
						break;

					case 0x000A:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x00FF = " + opcode + " & 0x00FF = 0x000A"); }
						emulatorPaused = true;
						waitingForKey = true;
						vRegisterToStoreKey = a;
						break;

					case 0x0015:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x00FF = " + opcode + " & 0x00FF = 0x0015"); }
						timerDelay = v[a];
						break;

					case 0x0018:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x00FF = " + opcode + " & 0x00FF = 0x0018"); }
						timerSound = v[a];
						break;

					case 0x0029:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x00FF = " + opcode + " & 0x00FF = 0x0029"); }
						i = v[a] * 5;
						break;

					case 0x0033:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x00FF = " + opcode + " & 0x00FF = 0x0033"); }
						mem[i]     = parseInt(v[a] / 100);
						mem[i + 1] = parseInt(v[a] % 100 / 10);
						mem[i + 2] = v[a] % 10;
						break;

					case 0x0055:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x00FF = " + opcode + " & 0x00FF = 0x0055"); }
						for (var x = 0; x <= a; x++)
						{
							mem[i + x] = v[x];
						}
						break;

					case 0x0065:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x00FF = " + opcode + " & 0x00FF = 0x0065"); }
						for (var x = 0; x <= a; x++)
						{
							v[x] = mem[i + x];
						}
						break;

					case 0x001E:
						if (EMULATOR_DEBUG) { CB_console("Executing opcode & 0x00FF = " + opcode + " & 0x00FF = 0x001E"); }
						i += v[a];
						break;
						
					default:
						error = true;
						break;
				}
				break;
		default:
			error = true;
			break;
	}
	
	if (error)
	{
		CB_console("[ERROR] Unknown opcode: " + opcode + " (" + CB_intToBase(opcode, 16) + "), a: " + a + " (" + CB_intToBase(a, 16) + "), b: " + b + " (" + CB_intToBase(b, 16) + ") [pc: " + pc + " (" + CB_intToBase(pc, 16) + ")]");
	}
	else if (EMULATOR_DEBUG)
	{
		CB_console("[OK] Processed opcode: " + opcode + " (" + CB_intToBase(opcode, 16) + "), a: " + a + " (" + CB_intToBase(a, 16) + "), b: " + b + " (" + CB_intToBase(b, 16) + ") [pc: " + pc + " (" + CB_intToBase(pc, 16) + ")]");
	}
	
	return !emptyOpcode;
}


//Tells whether a given key code is pressed or not:
function isKeyPressed(keyCode)
{
	return keysMap[keyCode] &&
	(
		CB_Keyboard.isKeyDown(keysMap[keyCode].keys) ||
		isScreenButtonDown(keyCode) ||
		typeof(keysMap[keyCode].controllers) !== "undefined" && keysMap[keyCode].controllers !== null &&
		(
			CB_isArray(keysMap[keyCode].controllers.buttons) && CB_Controllers.isButtonDown(keysMap[keyCode].controllers.buttons, keysMap[keyCode].controllers.gamepadIndex) ||
			keysMap[keyCode].controllers.axis && CB_Controllers.isAxisDown(keysMap[keyCode].controllers.axis.index, keysMap[keyCode].controllers.gamepadIndex, keysMap[keyCode].controllers.axis.min, keysMap[keyCode].controllers.axis.max)
		)
	);
}


//Initialize/clears the screen (its bitmap):
function clearScreen()
{
	CB_console("Clearing screen...");
	for (var y = screenBitMap.length - 1; y >= 0; y--)
	{
		for (var x = screenBitMap[y].length - 1; x >= 0; x--)
		{
			screenBitMap[y][x] = 0;
		}
	}
	drawFlag = true;
}


//Draws the screen with the rendering engine:
function drawScreen()
{
	var bitmapRenderingEngine = CB_GEM.graphicSpritesSceneObject.getById("bitmap_group").getById("bitmap_current").src;
	for (var y = screenBitMap.length - 1; y >= 0; y--)
	{
		for (var x = screenBitMap[y].length - 1; x >= 0; x--)
		{
			bitmapRenderingEngine[y][x] = !!screenBitMap[y][x];
		}
	}
}


//Draws a pixel on the screen:
function drawPixel(x, y)
{
	x %= 64;
	y %= 32;
	screenBitMap[y][x] ^= 1;
	return !screenBitMap[y][x];
}


//Returns a multidimensional array from another given one, changing the boolean values to numbers (0 and 1). Useful for drawing the screen in the graphic rendering engine:
function arrayBooleansToNumbers(array, typedArray)
{
	var newArray = typedArray ? new Uint8Array(64) : [];
	if (CB_isArray(array))
	{
		CB_Arrays.forEach
		(
			array,
			function(value, index, array)
			{
				if (CB_isArray(array[index])) { newArray[index] = arrayBooleansToNumbers(array[index], true); }
				else { newArray[index] = value ? 1 : 0; }
			}
		);
	}
	return newArray;
}


//Returns whether a ROM instruction (opcode in the memory) is being pointed or not:
function emulatingROM()
{
	return (pc && mem[pc] || waitingForKey);
}

main.js:

/* This file belongs to a CrossBrowdy.com example, made by Joan Alba Maldonado. Creative Commons Attribution 4.0 International License. */

//Path to the graphic rendering engine module:
var CB_GEM_PATH = CB_GEM_PATH || "../simple_game_engine_files/";

//Defines whether to shows debug messages or not:
var CB_GEM_DEBUG_MESSAGES = false;

//Adds the game engine module to CrossBrowdy:
var CB_GEM_MODULE_NEEDED_MODULES = {};
CB_GEM_MODULE_NEEDED_MODULES[CB_GEM_PATH + "game_engine_module.js"] = { load: true, mandatory: true, absolutePath: true };
CB_Modules.addNeededModule(CB_NAME, "GAME_ENGINE_MODULE", CB_GEM_MODULE_NEEDED_MODULES);


CB_init(main); //It will call the "main" function when ready.


//This function will be called when CrossBrowdy is ready:
function main()
{
	CB_console("CrossBrowdy and all needed modules loaded. Starting emulator...");
	
	//Defines needed data:
	//NOTE: most of the data will be calculated automatically and dynamically.
	CB_GEM.data = //Data stored in the game engine module (can be exported to save the status):
	{
		//General data:
		soundEnabled: true, //Set to false to disable sound.
		PIXELS_USE_GRADIENT: true,
		PIXELS_COLOR_FIRST: "#aaddaa", //First colour for the gradient. If PIXELS_USE_GRADIENT is set to false, this will be the unique colour of the pixels.
		PIXELS_COLOR_LAST: "#118811", //Last colour for the gradient. Ignored if PIXELS_USE_GRADIENT is set to false.
		SCREEN_BORDER_COLOR: "#000000"
	};

	//Sets the desired sprites scene data (can be modified dynamically):
	CB_GEM.spritesGroupsData =
	{
		//'my_sprites_groups_1' ('CB_GraphicSpritesScene.SPRITES_GROUPS_OBJECT' object). Some missing or non-valid properties will get a default value:
		id: "my_sprites_groups_1",
		srcWidth: 40,
		srcHeight: 40,
		data: { loop: true, onlyUseInMap: false },
		//Numeric array containing 'CB_GraphicSprites.SPRITES_OBJECT' objects with all the sprites groups that will be used (their "parent" property will be set to point the current 'CB_GraphicSpritesScene' object which contains them):
		spritesGroups:
		[
			{
				id: "info",
				srcType: CB_GraphicSprites.SRC_TYPES.TEXT,
				top: 15,
				zIndex: 3,
				data:
				{
					fontSize: "12px",
					fontFamily: "courier",
					style: "#ffffaa",
					fontWeight: "bold"
				},
				sprites: [ { id: "info_sprite" } ]
			},
			//'border_sprites' ('CB_GraphicSprites.SPRITES_OBJECT' object). Some missing or non-valid properties will will be inherited from the parent ('CB_GraphicSpritesScene.SPRITES_GROUPS_OBJECT' object):
			{
				id: "border_sprites",
				srcType: CB_GraphicSprites.SRC_TYPES.RECTANGLE,
				data: { style: CB_GEM.data.SCREEN_BORDER_COLOR, stroke: true },
				//Numeric array containing 'CB_GraphicSprites.SPRITE_OBJECT' objects with all the sprites that will be used:
				sprites:
				[
					//'border_sprite' ('CB_GraphicSprites.SPRITE_OBJECT' object). Some missing or non-valid properties will be inherited from the sprites group:
					{
						id: "border_sprite"
					}
				]
			},
			//'bitmap_group' ('CB_GraphicSprites.SPRITES_OBJECT' object). Some missing or non-valid properties will will be inherited from the parent ('CB_GraphicSpritesScene.SPRITES_GROUPS_OBJECT' object):
			{
				id: "bitmap_group",
				srcType: CB_GraphicSprites.SRC_TYPES.BITMAP,
				width: 10, //It will be updated automatically according to the screen size.
				height: 10, //It will be updated automatically according to the screen size.
				data:
				{
					clearPreviousFirst: true,
					style: CB_GEM.data.PIXELS_COLOR_FIRST,
					gradientLeftOffset: 0, //Used to create a "blink" effect.
					beforeDrawingElement:
						function(element, canvasContext, canvasBufferContext, useBuffer, CB_GraphicSpritesSceneObject, drawingMap, x, y, mapElement) //Called before drawing the element. The 'element' and 'mapElement' will be the same.
						{
							//If desired and possible, it will draw the pixel with a personalized style (for the current ROM):
							CB_GEM.data._pixelsStyleTemp = CB_GEM.data._pixelsStyleTemp || {};
							CB_GEM.data._pixelsStyleTemp.stroke = null;
							CB_GEM.data._pixelsStyleTemp.first = null;
							CB_GEM.data._pixelsStyleTemp.last = null;
							if (lastROMIdLoaded && ROMs[lastROMIdLoaded] && typeof(ROMs[lastROMIdLoaded].pixelsColorizer) === "function")
							{
								CB_GEM.data._pixelsStyleTemp =
									ROMs[lastROMIdLoaded].pixelsColorizer.call(this, element, canvasContext, canvasBufferContext, useBuffer, CB_GraphicSpritesSceneObject, drawingMap, x, y, mapElement) ||
									CB_GEM.data._pixelsStyleTemp; //If the 'pixelsColorizer' returns null, keeps the pixels unaltered (null for 'first' and 'last').
							}
							
							//If we want to use gradient, it will add a "blink" effect:
							if (CB_GEM.data.PIXELS_USE_GRADIENT)
							{
								var gradient = canvasContext.createLinearGradient(element.data.gradientLeftOffset + x, y, element.width * 64, element.height * 32);
								element.data.gradientLeftOffset++;
								if (element.data.gradientLeftOffset > 100) { element.data.gradientLeftOffset = 0; }
								gradient.addColorStop(0, CB_GEM.data._pixelsStyleTemp.first || CB_GEM.data.PIXELS_COLOR_LAST);
								gradient.addColorStop(1, CB_GEM.data._pixelsStyleTemp.last || CB_GEM.data.PIXELS_COLOR_FIRST);
								this.data.style = gradient;
							}
							else { this.data.style = CB_GEM.data._pixelsStyleTemp.first || CB_GEM.data.PIXELS_COLOR_FIRST; }
							this.data.stroke = !!CB_GEM.data._pixelsStyleTemp.stroke;
							return this; //Same as 'element'. Must return the element to draw.
						}
				},
				sprites:
				[
					//Maps with string aliases:
					//'bitmap_current' ('CB_GraphicSprites.SPRITE_OBJECT' object). Some missing or non-valid properties will be inherited from the sprites group:
					{
						id: "bitmap_current", //Current map which will be displayed (it will be modified according to the position of the player and the other elements).
						src:
						[
							//NOTE: using 'CB_Arrays.forEach' to replace spaces with 'false' and other symbols with 'true' (an two-dimensional array of booleans is needed by source type 'CB_GraphicSprites.SRC_TYPES.BITMAP'):
							CB_Arrays.forEach("****************************************************************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("*****      ***      *** *********      ***       ***       *****".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("***** ******** ******** ********* ******** ************ ********".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("***** ******** ******** ********* ******** ************ ********".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("*****      ***      *** *********      *** ************ ********".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("********** *** ******** ********* ******** ************ ********".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("********** *** ******** ********* ******** ************ ********".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("*****      ***      ***       ***      ***       ****** ********".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("****************************************************************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("****************************************************************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("****************************************************************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("****************************************************************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("****************************************************************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("******************************  ********************************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("***************************** ** *******************************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("**************************** **** ******************************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("*************************** ****** *****************************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("***************************        *****************************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("************************** ******** ****************************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("************************** ******** ****************************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("****************************************************************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("****************************************************************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("****************************************************************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("****************************************************************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("*********************    ****    *** *** ***********************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("********************* *** *** ** ***  *  ***********************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("********************* *** *** ** *** * * ***********************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("*********************    **** ** *** *** ***********************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("*********************  ****** ** *** *** ***********************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("********************* * ***** ** *** *** ***********************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("********************* ** ****    *** *** ***********************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; }),
							CB_Arrays.forEach("****************************************************************".split(""), function(v, i, a) { a[i] = v === " " ? false : true; })
						]
					}
				]
			}
		]
	};
	
	//Defines the callbacks for the loop:
	CB_GEM.onLoopStart = function(graphicSpritesSceneObject, CB_REM_dataObject, expectedCallingTime) //When the loop starts (before rendering the graphics):
	{
		manageInput(); //Manages any possible input (for the UI, not for the emulator).
		
		if (!CB_GEM.data.emulatorStarted) { return; }
		else if (emulatorPaused) { return; }
		else if (screenRendered) { CB_GEM.REM.FPS++; return false; } //Returns false to avoid rendering the screen (the FPS counter needs to be increased manually in we want to count this loop as a frame).
	};
	
	CB_GEM.onLoopEnd = function(graphicSpritesSceneObject, CB_REM_dataObject, expectedCallingTime) //When the loop ends (after rendering the graphics):
	{
		screenRendered = true; //When the loop ends, the screen must have been rendered.
	};
	
	//Modifies the default refresh rate for the loop and the number cycles per loop (it will affect the FPS but not the emulation cycle as it will be processed separately):
	CB_GEM.options.LOOP_REFRESH_RATE = 1;
	CB_GEM.options.RENDERING_CYCLES_PER_LOOP = 2;
	
	//Starts the engine module:
	CB_GEM.begin
	(
		//onStart:
		function(graphicSpritesSceneObject, CB_CanvasObject, CB_CanvasObjectBuffer, FPSSprites) //'FPSSprites' contains the 'CB_GraphicSprites.SPRITES_OBJECT' object used to display the FPS counter.
		{
			//Sets the events to the toolbar and its icons:
			var toolbarIconEvent = function()
			{
				if (!CB_GEM.data.emulatorStarted) { return; }
				if (this.id === "button_reset") { loadROMContent(undefined, undefined, undefined, lastROMIdLoaded); }
				else if (this.id === "button_file_selector")
				{
					if (!supportedFileAPI)
					{
						alert("Cannot load local files because File API is not supported!");
						return;
					}
					CB_Elements.id("file_selector").click();
				}
				else if (this.id === "button_fullscreen") { fullScreenToggle(); }
				else if (this.id === "button_pause") { emulatorEnd("Emulator paused"); }
			};
			var toolbarIconsIDs = [ "button_reset", "button_pause", "button_fullscreen", "button_file_selector", "cpl_input" ]; //Identifiers of the toolbar icons.
			var toolbarIconElement = null;
			for (var x = toolbarIconsIDs.length - 1; x >= 0; x--)
			{
				toolbarIconElement = CB_Elements.id(toolbarIconsIDs[x]);
				if (toolbarIconElement !== null)
				{
					makeElementSolid(toolbarIconElement);
					CB_Events.on(toolbarIconElement, "click", toolbarIconEvent);
					if (toolbarIconsIDs[x] === "cpl_input")
					{
						var CPLInputOnChange = function()
						{
							var CPL = parseInt(this.value);
							if (isNaN(CPL)) { CPL = emulatorCyclesPerLoop; }
							else if (CPL < 1) { CPL = 1; } //Minimum is 1 CPL.
							else if (CPL > 10000) { CPL = 10000; } //Maximum are 10000 CPL.
							if (CPL !== emulatorCyclesPerLoop) { emulatorCyclesPerLoop = CPL; CB_console("CPL (Cycles Per Loop) set to: " + emulatorCyclesPerLoop); }
							this.value = CPL;
						};
						CB_Events.on(toolbarIconElement, "change", CPLInputOnChange);
					}
				}
			}
			
			//Prepares to allow using the file selector and the ROM selector (and fills it):
			prepareFileSelector();
			prepareROMSelector();
			
			//Sets the events to the screen controls:
			var screenControlsButtonPressEvent = function() { if (!CB_GEM.data.emulatorStarted || !emulatingROM() || !this.id) { return; } screenButtonPress(parseInt(this.id.replace("screen_button_", "")), true, this); };
			var screenControlsButtonReleaseEvent = function() { if (this.id) { screenButtonPress(parseInt(this.id.replace("screen_button_", "")), false, this); } };
			var screenControlsToggler = CB_Elements.id("controls_toggler");
			if (screenControlsToggler !== null)
			{
				makeElementSolid(screenControlsToggler);
			}
			var screenControls = CB_Elements.id("controls");
			if (screenControls !== null)
			{
				makeElementSolid(screenControls);
			}
			var buttonElement = null;
			for (var x = 0; x < 16; x++)
			{
				buttonElement = CB_Elements.id("screen_button_" + x);
				if (buttonElement !== null)
				{
					makeElementSolid(buttonElement);
					CB_Events.on(buttonElement, "touchstart", screenControlsButtonPressEvent);
					CB_Events.on(buttonElement, "mousedown", screenControlsButtonPressEvent);
					CB_Events.on(buttonElement, "touchend", screenControlsButtonReleaseEvent);
					CB_Events.on(buttonElement, "touchcancel", screenControlsButtonReleaseEvent);
					CB_Events.on(buttonElement, "mouseup", screenControlsButtonReleaseEvent);
				}
			}
			
			//Prevents selection for the message elements:
			CB_Elements.preventSelection(CB_Elements.id("loading"));
			CB_Elements.preventSelection(CB_Elements.id("loading_rom"));
			CB_Elements.preventSelection(CB_Elements.id("waiting_for_any_key"));
			CB_Elements.preventSelection(CB_Elements.id("error"));
			
			FPSSprites.setDisabled(false); //Set to true to hide FPS counter.
			FPSSprites.getCurrent().data.fontSize = "18px"; //Sets the font size for the FPS counter.
			FPSSprites.getCurrent().data.style = "#dddddd"; //Sets the font colour for the FPS counter.
	
			resizeElements(graphicSpritesSceneObject); //Updates all visual elements according to the screen size.

			//Starts showing the information constantly:
			setInterval(updateInfo, 1);
	
			//Updates all when the screen is resized or changes its orientation:
			CB_GEM.onResize = function(graphicSpritesSceneObject, CB_REM_dataObject, CB_CanvasObject, CB_CanvasObjectBuffer)
			{
				resizeElements(graphicSpritesSceneObject);
			};

			//Sets the screen array:
			screenBitMap = arrayBooleansToNumbers(graphicSpritesSceneObject.getById("bitmap_group").getById("bitmap_current").src);

			CB_Elements.hideById("loading"); //Hides the loading message.
			
			CB_Elements.showById("start_button"); //Shows the start button.
			
			//Sets the event for the debug checkbox:
			var debugCheckbox = CB_Elements.id("debug_checkbox");
			if (debugCheckbox !== null)
			{
				debugCheckbox.checked = !!CB_GEM_DEBUG_MESSAGES;
				CB_Events.on(debugCheckbox, "change", function() { CB_GEM_DEBUG_MESSAGES = !!debugCheckbox.checked; updateInfo(); });
			}
		},
		
		//onError:
		function(error) { CB_console("Error: " + error); }
	);
}


//Starts the emulator:
var CPSEraserInterval = null;
function emulatorStart(graphicSpritesSceneObject)
{
	if (CB_GEM.data.emulatorStarted) { return; }

	graphicSpritesSceneObject = graphicSpritesSceneObject || CB_GEM.graphicSpritesSceneObject;
	if (!graphicSpritesSceneObject) { return; }
	
	//Hides the start button:
	CB_Elements.hideById("start_button");
	
	//Enables toolbar icons:
	enableElements([ "button_pause", "button_fullscreen", "button_file_selector", "file_selector", "rom_selector", emulatingROM() || emulatorError && !XHRError ? "button_reset" : undefined, emulatingROM() ? "cpl_input" : undefined ], true); //Reset button and CPL are only enabled if a ROM is being emulated.
	
	//Shows hand (pointer) mouse cursor for screen buttons (as they will be enabled):
	var screenButtonElements = CB_Elements.classes("screen_button");
	if (screenButtonElements !== null)
	{
		for (var x = 0; x < screenButtonElements.length; x++)
		{
			CB_Elements.removeClass(screenButtonElements[x], "disabled");
		}
	}
	
	//Prepares the sound effects and plays one of them (recommended to do this through a user-driven event):
	try
	{
		prepareSoundFx(); //Prepares sound effects to be used later.
		playSoundFx("start");
	}
	catch(E)
	{
		CB_console("Error preparing sounds or playing sound with 'start' ID: " + E);
		CB_GEM.data.soundEnabled = false; //If it fails, disables the sound.
	}

	//Sets the emulator as started:
	CB_GEM.data.emulatorStarted = true;
	
	//Starts the interval to clear the CPS counter:
	clearInterval(CPSEraserInterval);
	CPSEraserInterval = setInterval(function() { CPS = cyclesCounter; cyclesCounter = 0; updateInfo(CPS); }, 1000);
	
	//Starts the emulation loop (independent from the game rendering engine loop):
	emulationLoopTimeout = CB_symmetricCall(emulationLoop, 1, "emulationLoopTimeout");
}


//Ends the emulator:
function emulatorEnd(message)
{
	if (!CB_GEM.data.emulatorStarted) { return; }

	//Clears the emulation loop timeout and the CPS (Cycles Per Second) counter:
	CPS = 0;
	clearTimeout(emulationLoopTimeout);
	CB_symmetricCallClear("emulationLoopTimeout"); //Clears the stored last time used by 'CB_symmetricCall' for the given "emulationLoopTimeout" symmetric interval identifier.

	//Disables toolbar icons:
	enableElements([ "button_pause", "button_reset", "button_fullscreen", "button_file_selector", "file_selector", "rom_selector", "cpl_input" ], false);

	//Shows normal (default) mouse cursor for screen buttons (as they will be disabled):
	var screenButtonElements = CB_Elements.classes("screen_button");
	if (screenButtonElements !== null)
	{
		for (var x = 0; x < screenButtonElements.length; x++)
		{
			CB_Elements.addClass(screenButtonElements[x], "disabled", true); //Only adds the class in the case it is not being used already.
		}
	}

	message = CB_trim(message);
	CB_GEM.data.emulatorStarted = false;
	CB_Elements.insertContentById("start_button", (message !== "" ? message + "<br />" : "") + "Continue emulation!")
	CB_Elements.showById("start_button"); //Shows the start button again.
}


//Updates the information shown:
var _updateInfoLastCPS = 0;
function updateInfo(lastCPS)
{
	CB_GEM.graphicSpritesSceneObject.getById("info").get(0).src =
		"Cycles per second: " + (lastCPS || _updateInfoLastCPS) + "\n" +
		"Cycles per loop: " + emulatorCyclesPerLoop +
		(!CB_GEM.data.emulatorStarted || emulatorPaused ? "\nEmulator paused!" : "") +
		(waitingForKey ? "\nWAITING FOR ANY KEY!" : "") +
		(
			CB_GEM_DEBUG_MESSAGES ?
				"\nLast opcode: " + opcode +
				"\n* First argument (a): " + a +
				"\n* Second argument (b): " + b +
				"\nProgram Counter (pc): " + pc +
				"\nIndex register (i): " + i +
				"\nV flags:\n " + v +
				"\nStack:\n " + stack +
				"\nStack Pointer (sp): " + sp +
				"\nTimer delay: " + timerDelay +
				"\nTimer sound: " + timerSound +
				"\nLast ROM ID: " + lastROMIdLoaded
			: ""
		);
	
	_updateInfoLastCPS = lastCPS || _updateInfoLastCPS || 0;
}


//Resizes all visual elements according to the screen size:
function resizeElements(graphicSpritesSceneObject)
{
	if (graphicSpritesSceneObject instanceof CB_GraphicSpritesScene)
	{
		//Resizes the current map which is being displayed according to the new screen size:
		var ELEMENTS_WIDTH = CB_Screen.getWindowWidth() * 0.80 / 64;
		var ELEMENTS_HEIGHT = CB_Screen.getWindowHeight() * 0.80 / 32;
		ELEMENTS_WIDTH = ELEMENTS_HEIGHT = Math.min(ELEMENTS_WIDTH, ELEMENTS_HEIGHT);
		graphicSpritesSceneObject.getById("bitmap_group").getById("bitmap_current").left = (CB_Screen.getWindowWidth() - ELEMENTS_WIDTH * 64) / 2;
		graphicSpritesSceneObject.getById("bitmap_group").getById("bitmap_current").top = (CB_Screen.getWindowHeight() - ELEMENTS_HEIGHT * 32) / 2;
	
		graphicSpritesSceneObject.getById("bitmap_group").getById("bitmap_current").width = ELEMENTS_WIDTH;
		graphicSpritesSceneObject.getById("bitmap_group").getById("bitmap_current").height = ELEMENTS_HEIGHT;
	
		//Resizes the border:
		graphicSpritesSceneObject.getById("border_sprites").getById("border_sprite").left = graphicSpritesSceneObject.getById("bitmap_group").getById("bitmap_current").left;
		graphicSpritesSceneObject.getById("border_sprites").getById("border_sprite").top = graphicSpritesSceneObject.getById("bitmap_group").getById("bitmap_current").top;
		graphicSpritesSceneObject.getById("border_sprites").getById("border_sprite").width = ELEMENTS_WIDTH * 64;
		graphicSpritesSceneObject.getById("border_sprites").getById("border_sprite").height = ELEMENTS_HEIGHT * 32;
		
		//Locates the toolbar:
		var toolbar = CB_Elements.id("toolbar");
		if (toolbar !== null)
		{
			toolbar.style.left = graphicSpritesSceneObject.getById("bitmap_group").getById("bitmap_current").left + "px";
		}
	}

	//Resizes the toolbar and its icons:
	var toolbarIconMargin = parseInt(Math.min(CB_Screen.getWindowWidth(), CB_Screen.getWindowHeight()) * 0.08) + "px";
	var toolbarIconWidthAndHeight = parseInt(Math.min(CB_Screen.getWindowWidth(), CB_Screen.getWindowHeight()) * 0.07) + "px";
	var toolbarIconsIDs = [ "button_reset", "button_pause", "button_fullscreen", "button_file_selector", "rom_selector", "cpl_input" ]; //Identifiers of the toolbar icons.
	var toolbarIconElement = null;
	for (var x = toolbarIconsIDs.length - 1; x >= 0; x--)
	{
		toolbarIconElement = CB_Elements.id(toolbarIconsIDs[x]);
		if (toolbarIconElement !== null)
		{
			toolbarIconElement.style.height = toolbarIconWidthAndHeight;
			toolbarIconElement.style.fontSize = toolbarIconElement.style.lineHeight = parseInt(toolbarIconWidthAndHeight) / 2 + "px";
			if (toolbarIconElement.id !== "rom_selector" && toolbarIconElement.id !== "cpl_input")
			{
				toolbarIconElement.style.width = toolbarIconWidthAndHeight;
			}
		}
	}
	
	//Resizes the screen controls:
	var screenControlsToggler = CB_Elements.id("controls_toggler");
	if (screenControlsToggler !== null)
	{
		screenControlsToggler.style.right = "0px";
		screenControlsToggler.style.bottom = toolbarIconMargin;
		screenControlsToggler.style.width = (parseInt(toolbarIconWidthAndHeight) / 2) + "px";
		screenControlsToggler.style.height = screenControlsToggler.style.lineHeight = (parseInt(toolbarIconWidthAndHeight) / 2) + "px";
		screenControlsToggler.style.fontSize = parseInt(toolbarIconWidthAndHeight) / 4 + "px";
	}
	var screenControls = CB_Elements.id("controls");
	if (screenControls !== null)
	{
		screenControls.style.right = toolbarIconMargin;
		screenControls.style.bottom = toolbarIconMargin;
	}
	var buttonElement = null;
	for (var x = 0; x < 16; x++)
	{
		buttonElement = CB_Elements.id("screen_button_" + x);
		if (buttonElement !== null)
		{
			buttonElement.style.width = toolbarIconWidthAndHeight;
			buttonElement.style.height = buttonElement.style.lineHeight = toolbarIconWidthAndHeight;
			buttonElement.style.fontSize = buttonElement.style.lineHeight = parseInt(toolbarIconWidthAndHeight) / 2 + "px";
		}
	}

	//Resizes the message when loading a ROM:
	var loadingROMElement = CB_Elements.id("loading_rom");
	if (loadingROMElement !== null)
	{
		 loadingROMElement.style.fontSize = loadingROMElement.style.lineHeight = parseInt(toolbarIconWidthAndHeight) * 2 + "px";
		 loadingROMElement.style.left = (CB_Screen.getWindowWidth() / 2 - parseInt(loadingROMElement.style.fontSize) * 6 / 2) + "px";
		 loadingROMElement.style.top = (CB_Screen.getWindowHeight() / 2 - parseInt(loadingROMElement.style.fontSize) / 2) + "px";
	}
	
	//Resizes the message when waiting for any key:
	var waitingForAnyKeyElement = CB_Elements.id("waiting_for_any_key");
	if (waitingForAnyKeyElement !== null)
	{
		waitingForAnyKeyElement.style.fontSize = waitingForAnyKeyElement.style.lineHeight = parseInt(toolbarIconWidthAndHeight) * 1.8 + "px";
		waitingForAnyKeyElement.style.left = (CB_Screen.getWindowWidth() / 2 - parseInt(waitingForAnyKeyElement.style.fontSize) * 8 / 2) + "px";
		waitingForAnyKeyElement.style.top = (CB_Screen.getWindowHeight() / 2 - parseInt(waitingForAnyKeyElement.style.fontSize) / 2) + "px";
	}

	//Resizes the message when a error happened:
	var errorElement = CB_Elements.id("error");
	if (errorElement !== null)
	{
		 errorElement.style.fontSize = errorElement.style.lineHeight = parseInt(toolbarIconWidthAndHeight) * 2 + "px";
		 errorElement.style.left = (CB_Screen.getWindowWidth() / 2 - parseInt(errorElement.style.fontSize) * 2.5 / 2) + "px";
		 errorElement.style.top = (CB_Screen.getWindowHeight() / 2 - parseInt(errorElement.style.fontSize) / 2) + "px";
	}
	
	//Resizes the FPS and the information text:
	CB_GEM.graphicSpritesSceneObject.getById("fps_group").getById("fps").data.fontSize = parseInt(toolbarIconWidthAndHeight) / 2.5 + "px";
	CB_GEM.graphicSpritesSceneObject.getById("info").get(0).data.fontSize = parseInt(toolbarIconWidthAndHeight) / 3 + "px";
	if (CB_Screen.isLandscape())
	{
		CB_GEM.graphicSpritesSceneObject.getById("fps_group").getById("fps").top = 1;
		CB_GEM.graphicSpritesSceneObject.getById("info").get(0).top = parseInt(toolbarIconWidthAndHeight) / 1.4;
	}
	else
	{
		CB_GEM.graphicSpritesSceneObject.getById("fps_group").getById("fps").top = parseInt(toolbarIconWidthAndHeight) + 2;
		CB_GEM.graphicSpritesSceneObject.getById("info").get(0).top = parseInt(toolbarIconWidthAndHeight) * 1.5;
	}
	
	//Resizes the font of the start button:
	var startButton = CB_Elements.id("start_button");
	if (startButton !== null) { startButton.style.fontSize = parseInt(toolbarIconWidthAndHeight) / 3 + "px"; }
	
	//Forces screen rendering again (just in case):
	screenRendered = false;
}


//Input management (some controllers can also fire keyboard events) for the UI (not for the emulator):
var _inputProcessedLastTime = 0;
var _ignoreInputMs = 150; //Number of milliseconds that the input will be ignored after an input has been processed (to avoid processing the input too fast).
function manageInput(action)
{
	//If not enough time has been elapsed since the last movement, exits (to avoid moving or processing the input too fast):
	if (CB_Device.getTiming() < _inputProcessedLastTime + _ignoreInputMs) { return; }

	var actionPerformed = false;

	//If the emulator has not started:
	if (!CB_GEM.data.emulatorStarted)
	{
		//If return, space or a button (button 1, 2 or 3) or axis from any gamepad is pressed, starts the emulator:
		if (CB_Keyboard.isKeyDown(CB_Keyboard.keys.ENTER) || CB_Keyboard.isKeyDown(CB_Keyboard.keys.SPACEBAR) || CB_Controllers.isButtonDown([1, 2, 3]) || CB_Controllers.getAxesDown().length > 0 || CB_Controllers.getAxesDown("", -1).length > 0)
		{
			actionPerformed = true;
			emulatorStart();
		}
	}
	else if (CB_Keyboard.isKeyDown(CB_Keyboard.keys.ESC))
	{
		actionPerformed = true;
		emulatorEnd("Emulator paused");
		
	}
	else if (CB_Keyboard.isKeyDown(CB_Keyboard.keys.T))
	{
		actionPerformed = true;
		screenControlsToggle();
	}
	
	if (actionPerformed)
	{
		_inputProcessedLastTime = CB_Device.getTiming(); //As we have processed the input, updates the time with the new one (to avoid processing the input too fast).		
	}
}


//Sets a screen button as pressed:
function screenButtonPress(buttonCode, pressed, screenButtonElement) //Button codes go from 0 to 15.
{
	pressed = !!pressed;
	screenButtonsPressed[buttonCode] = pressed;
	screenButtonElement = screenButtonElement || CB_Elements.id("screen_button_" + buttonCode);
	if (screenButtonElement !== null)
	{
		if (pressed) { CB_Elements.addClass(screenButtonElement, "pressed", true); } //Only adds the class in the case it is not being used already.
		else { CB_Elements.removeClass(screenButtonElement, "pressed"); }
	}
}


//Returns whether a given screen button is being pressed or not:
var screenButtonsPressed = new Array(15); //It will keep the screen buttons status.
for (var x = 0; x < 16; x++) { screenButtonsPressed[x] = false; }
function isScreenButtonDown(keyCode) //'keyCode' is an hexadecimal code used by CHIP-8.
{
	return screenButtonsPressed[keyCode];
}


//Prepares sound effects:
var sfx = null; //Global object to play the sounds.
var prepareSoundFxExecuted = false;
function prepareSoundFx()
{
	if (prepareSoundFxExecuted) { return; }

	prepareSoundFxExecuted = true;
	
	var jsfxObject = CB_Speaker.getJsfxObject(); //Gets the 'jsfx' object.
	if (jsfxObject !== null)
	{
		//Defines the sound effects:
		var library =
		{
			"start":
				jsfx.Preset.Select,
			"beep":
				{ "Volume": { "Sustain": 0.05, "Decay": 0.2, "Punch": 1, "Master": 0.25 } }
		};

		//Loads the sound effects:
		sfx = CB_AudioDetector.isAPISupported("WAAPI") ? jsfxObject.Live(library) : jsfxObject.Sounds(library); //Uses AudioContext (Web Audio API) if available.
	}
}


//Plays the desired sound effect (by its identifier):
function playSoundFx(id)
{
	if (!sfx || typeof(sfx[id]) !== "function") { return; }
	else if (!CB_GEM.data.soundEnabled) { return; }

	//Note: at least the first time, it is recommended to do it through a user-driven event (as "onClick", "onTouchStart", etc.) in order to maximize compatibility (as some clients could block sounds otherwise).
	sfx[id]();
}


//Toggles full screen mode:
//NOTE: some browsers will fail to enable full screen mode if it is not requested through a user-driven event (as "onClick", "onTouchStart", etc.).
function fullScreenToggle()
{
	CB_console("Toggling full screen mode...");
	//If it is using full screen mode already, disables it:
	if (CB_Screen.isFullScreen())
	{
		CB_console("Full screen mode detected. Trying to restore normal mode...");
		CB_Screen.setFullScreen(false); //Uses the Fullscreen API and fallbacks to other methods internally, including NW.js, Electron ones, when not available.
	}
	//...otherwise, requests full screen mode:
	else
	{
		CB_console("Normal mode detected. Trying to enable full screen mode...");
		CB_Screen.setFullScreen(true, undefined, true); //Allows reloading into another (bigger) window (for legacy clients).
	}
}


//Enables or disables desired elements:
function enableElements(elementsIDsArray, enable)
{
	if (CB_isArray(elementsIDsArray))
	{
		var elementLoop = null;
		for (var x = elementsIDsArray.length - 1; x >= 0; x--)
		{
			elementLoop = CB_Elements.id(elementsIDsArray[x]);
			if (elementLoop !== null)
			{
				elementLoop.disabled = !enable;
			}
		}
	}
}


//Toggles screen controls (make them show or hide):
function screenControlsToggle()
{
	CB_console("Toggling screen controls...");
	CB_Elements.showHideById
	(
		"controls", //element.
		undefined, //displayValue.
		true, //checkValues.
		false, //computed.
		undefined, //onToggleDisplay. Calls the function after showing/hiding the element.
		function(element, displayValue) //onShow. Calls the function after showing the element.
		{
			CB_console("Controls shown!");
			CB_Elements.setClassById("controls_toggler", "");
		},
		function(element, displayValue) //onHide. Calls the function after hiding the element.
		{
			CB_console("Controls hidden!");
			CB_Elements.setClassById("controls_toggler", "controls_hidden");
		}
	);
}


//Makes a DOM element non-draggable, non-selectable, etc.:
function makeElementSolid(element)
{
	if (element !== null)
	{
		element.style.draggable = false;
		element.style.touchAction = "none";
		CB_Elements.contextMenuDisable(element);
		CB_Elements.preventSelection(element);
	}
	return element;
}

rom_loader.js:

/* This file belongs to a CrossBrowdy.com example, made by Joan Alba Maldonado. Creative Commons Attribution 4.0 International License. */

var supportedFileAPI = (window.File && window.FileReader && window.FileList && window.Blob); //Used to check whether the client supports the File API or not.


//Sets the events for the file selector:
function prepareFileSelector()
{
	var fileSelector = CB_Elements.id("file_selector");
	if (fileSelector !== null)
	{
		CB_Events.on(fileSelector, "change", function(e) { e = CB_Events.normalize(e); loadROMLocally(e, fileSelector.value || fileSelector.selectedIndex); });
	}
}


//Sets the events for the ROM selector and fills it:
function prepareROMSelector()
{
	setROMsData(); //Sets the ROMs data.
	
	var ROMSelector = CB_Elements.id("rom_selector");
	if (ROMSelector !== null)
	{
		var optionLoop = null;
		for (var ROMId in ROMs)
		{
			optionLoop = document.createElement("option");
			optionLoop.id = optionLoop.name = optionLoop.value = optionLoop.text = optionLoop.textContent = optionLoop.innerText = ROMId;
			ROMSelector.appendChild(optionLoop);
		}
		var ROMSelectorLastValue = null;
		CB_Events.on
		(
			ROMSelector,
			"change",
			function(e)
			{
				//If a ROM is selected, tries to load it:
				ROMSelector.value = ROMSelector.value || ROMSelector.selectedIndex; //Cross-browser fix.
				if (ROMSelector.value && ROMSelector.value !== "none" && ROMs[ROMSelector.value])
				{
					ROMSelectorLastValue = ROMSelector.value;
					loadROMXHR(ROMSelector.value, ROMs[ROMSelector.value].cyclesPerLoop, ROMs[ROMSelector.value].keysMapping);
				}
				//...otherwise, if no ROM is selected, selects the current ROM again (if any):
				else if (ROMSelectorLastValue !== null) { ROMSelector.value = ROMSelector.selectedIndex = ROMSelectorLastValue; }
				//...otherwise, it does not select any ROM:
				else { ROMSelector.value = ROMSelector.selectedIndex = 0; }
				
				//Blurs the ROM selector (just in case it was selected):
				if (typeof(ROMSelector.blur) === "function")
				{
					CB_console("Blurring level selector...");
					ROMSelector.blur();
				}
			}
		);
	}
}


//Loads a desired ROM through XHR (AJAX):
var XHR = null;
var XHRError = false;
var XHRAllowedSuccessStatuses = null; //Array that will filled later with the status codes which will be used to consider an XHR request as successful.
function loadROMXHR(ROMId, cyclesPerLoop, keysMapping)
{
	if (!CB_GEM.data.emulatorStarted) { return; }

	if (typeof(ROMs[ROMId]) === "undefined" || !CB_isString(ROMs[ROMId].file)) { CB_console("Cannot find the file for the ROM whose ID is '" + ROMId + "'"); return; }

	XHRError = false;

	emulatorPaused = true; //Pauses the emulator.
	
	//Hides the "Waiting for any key" and the error message (just in case they were showing):
	waitingForKey = false;
	CB_Elements.hideById("waiting_for_any_key");
	CB_Elements.hideById("error");

	CB_Elements.showById("loading_rom"); //Shows the loading message.

	//Disables the toolbar icons:
	enableElements([ "button_reset", "button_pause", "button_fullscreen", "button_file_selector", "file_selector", "rom_selector", "cpl_input" ], false);
	
	CB_console("Trying to request the '" + ROMsPath + ROMs[ROMId].file + "' file through XHR (AJAX)...");
	
	var getFileSuccessFunction = function(XHR, getFileErrorFunction)
	{
		if (!XHR.response)
		{
			//NOTE: for older clients which do not support binary requests, we would have to convert the AJAX response into a typed array. Not done to make the example simpler.
			CB_console("Response is empty! Failed to get the binary data.");
			getFileErrorFunction.call(this, this, getFileSuccessFunction);
			return;
		}

		CB_console("The '" + ROMsPath + ROMs[ROMId].file + "' file has been loaded successfully! Response: ");
		CB_console(XHR.response);
		
		loadROMContent(new Uint8Array(XHR.response), cyclesPerLoop, keysMapping, ROMId); //CrossBrowdy will polyfill 'Uint8Array' when needed, automatically.
	};
	
	var getFileErrorFunction = function(XHR, getFileSuccessFunction)
	{
		CB_console("ERROR getting the '" + ROMsPath + ROMs[ROMId].file + "' file!");
		CB_console(XHR.response);
		CB_Elements.hideById("loading_rom");
		XHRError = emulatorError = true;
		CB_Elements.showById("error");
		var ROMSelector = CB_Elements.id("rom_selector");
		if (ROMSelector !== null) { ROMSelector.value = ROMSelector.selectedIndex = "none"; }
		
		//Enables the toolbar icons:
		enableElements([ "button_pause", "button_fullscreen", "button_file_selector", "file_selector", "rom_selector", "cpl_input" ], true);
	};
	
	XHRAllowedSuccessStatuses = XHRAllowedSuccessStatuses || (CB_Client.isRunningOnElectron() || CB_Client.isRunningOnNWjs() || CB_Client.isRunningLocally() ? [0, 200, 201] : [200, 201]); //Allows status 0 for file protocol as well as for NW.js and Electron.
	XHR = CB_Net.XHR.callBinary
	(
		//Parameters ("null" or "undefined" ones will get their default value, if needed, automatically):
		ROMsPath + ROMs[ROMId].file, //URL. Unique mandatory parameter.
		null, //data. It can be either a string (like URL parameters) or a JSON object. Default: undefined.
		null, //headers. Default: { "Content-Type" : "text/plain; charset=x-user-defined", "Cache-Control" : "no-cache", "Pragma" : "no-cache" }.
		null, //blobOrArrayBuffer. Default: 'arraybuffer'.
		null, //callbackFunction. Default: undefined. If provided, it will ignore both "callbackFunctionOK" and "callbackFunctionError".
		getFileSuccessFunction, //callbackFunctionOK. Default: undefined. Ignored if "callbackFunction" is provided.
		getFileErrorFunction, //callbackFunctionError. Default: undefined. Ignored if "callbackFunction" is provided.
		XHRAllowedSuccessStatuses, //allowedSuccessStatuses. Default: 200.
		XHR //XHR. Default: undefined. When not provided, it will try to create a new XHR object internally and it will be returned.
	);
}


//Loads a desired ROM which is placed locally:
function loadROMLocally(e, filePath)
{
	if (!CB_GEM.data.emulatorStarted) { return; }
	else if (!supportedFileAPI)
	{
		alert("Cannot load local files because File API is not supported!");
		return;
	}
	else if (!e || !e.target || !e.target.files || !e.target.files.length)
	{
		CB_console("Cannot load the '" + filePath + "' local file from the event");
		return;
	}
	
	XHRError = false;
	
	var ROMSelector = CB_Elements.id("rom_selector");
	if (ROMSelector !== null) { ROMSelector.value = ROMSelector.selectedIndex = "none"; }
	
	//Hides the "Waiting for any key" and the error message (just in case they were showing):
	waitingForKey = false;
	CB_Elements.hideById("waiting_for_any_key");
	CB_Elements.hideById("error");

	CB_Elements.showById("loading_rom"); //Shows the loading message.

	//Disables the toolbar icons:
	enableElements([ "button_reset", "button_pause", "button_fullscreen", "button_file_selector", "file_selector", "rom_selector", "cpl_input" ], false);
	
	emulatorPaused = true; //Pauses the emulator.
	
	CB_console("Trying to request '" + filePath + "' locally...");

	var file = e.target.files[0];

    CB_console("Name: " + file.name);
	CB_console("Type: " + file.type);
	CB_console("Size: " + file.size);

	var fileReader = new FileReader();

	fileReader.onload = function(e)
	{
		CB_console("The '" + file.name + "' file has been loaded successfully! Result: ");
		CB_console(fileReader.result);
		loadROMContent(new Uint8Array(fileReader.result)); //CrossBrowdy will polyfill 'Uint8Array' when needed, automatically.
	};
	
	fileReader.onerror = function(e)
	{
		CB_console("ERROR getting the '" + file.name + "' file!");
		CB_console(fileReader.error);
		CB_Elements.hideById("loading_rom");
		emulatorError = true;
		CB_Elements.showById("error");
	};

	fileReader.readAsArrayBuffer(file);    
}

roms_data.js:

/* This file belongs to a CrossBrowdy.com example, made by Joan Alba Maldonado. Creative Commons Attribution 4.0 International License. */

//ROMs list and their data:
var ROMsPath = "ROMs/"; //Path where the ROMs are stored.
var ROMs = null; //It will keep the ROM information.


//Sets the ROM data (needed for listing ROMs as well as useful for ROM identification, key mapping, etc.):
function setROMsData()
{
	ROMs = ROMs ||
	{
		"Blinky":
		{
			author: "Hans Christian Egeberg",
			year: 1991,
			file: "Blinky [Hans Christian Egeberg, 1991].ch8",
			cyclesPerLoop: 10,
			contentMatches: function(ROMContent) //Returns true if the given ROM content matches this ROM. Used to identify ROMs.
			{
				return ROMContentToString(ROMContent).indexOf("2.00 C. Egeberg 18/8-'91") !== -1;
			},
			keysMapping:
			{
				0x1: { keys: null, controllers: null },
				0x2: { keys: null, controllers: null },
				0x4: { keys: null, controllers: null },
				0x3:
				{
					keys: [ CB_Keyboard.keys._3, CB_Keyboard.keys.UP ],
					controllers: { gamepadIndex: "", axis: { index: 1, min: -1, max: -0.5 }	}
				},
				0x6:
				{
					keys: [ CB_Keyboard.keys.E, CB_Keyboard.keys.DOWN ],
					controllers: { gamepadIndex: "", axis: { index: 1, min: 0.5, max: 1 } }
				},
				0x7:
				{
					keys: [ CB_Keyboard.keys.A, CB_Keyboard.keys.LEFT ],
					controllers: { gamepadIndex: "", axis: { index: 0, min: -1, max: -0.5 }	}
				},
				0x8:
				{
					keys: [ CB_Keyboard.keys.S, CB_Keyboard.keys.RIGHT ],
					controllers: { gamepadIndex: "", axis: { index: 0, min: 0.5, max: 1 } }
				},
				0xF:
				{
					keys: [ CB_Keyboard.keys.V, CB_Keyboard.keys.SPACE, CB_Keyboard.keys.ENTER ],
					controllers: { gamepadIndex: "", buttons: [ 0, 1, 2, 3 ] }
				}
			},
			//Called for each pixel before drawing it, to colourize the screen according to the ROM (to use personalized theme):
			pixelsColorizer: function(element, canvasContext, canvasBufferContext, useBuffer, CB_GraphicSpritesSceneObject, drawingMap, x, y, mapElement)
			{
				//NOTE: theme not personalized for this game to keep the example simpler.
				return null; //It will use default style (set on 'CB_GEM.data.PIXELS_COLOR_FIRST' and 'CB_GEM.data.PIXELS_COLOR_LAST').
			}
		},
		"Brix":
		{
			author: "Andreas Gustafsson",
			year: 1990,
			file: "Brix [Andreas Gustafsson, 1990].ch8",
			cyclesPerLoop: 3,
			contentMatches: function(ROMContent) //Returns true if the given ROM content matches this ROM. Used to identify ROMs.
			{
				var ROMContentString = ROMContentToString(ROMContent);
				return ROMContentString.indexOf("ÞxÿHþhÿ") !== -1 && ROMContentString.indexOf("õ3òeñ)c7d") !== -1;
			},
			keysMapping:
			{
				0x4:
				{
					keys: [ CB_Keyboard.keys.Q, CB_Keyboard.keys.LEFT ],
					controllers: { gamepadIndex: "", axis: { index: 0, min: -1, max: -0.5 }	}
				},
				0x6:
				{
					keys: [ CB_Keyboard.keys.E, CB_Keyboard.keys.RIGHT ],
					controllers: { gamepadIndex: "", axis: { index: 0, min: 0.5, max: 1 } }
				}
			},
			//Called for each pixel before drawing it, to colourize the screen according to the ROM (to use personalized theme):
			pixelsColorizer: function(element, canvasContext, canvasBufferContext, useBuffer, CB_GraphicSpritesSceneObject, drawingMap, x, y, mapElement)
			{
				//Lives and score:
				if (y >= 0 && y < 5)
				{
					CB_GEM.data._pixelsStyleTemp.first = "#00aaaa";
					CB_GEM.data._pixelsStyleTemp.last = "#aa00aa";
				}
				//Paddle (player):
				else if (y === 31)
				{
					CB_GEM.data._pixelsStyleTemp.first = "#aa0000";
					CB_GEM.data._pixelsStyleTemp.last = "#aa0000";
				}
				//Others (bricks and ball):
				else { return { stroke: true }; } //It will use default style (set on 'CB_GEM.data.PIXELS_COLOR_FIRST' and 'CB_GEM.data.PIXELS_COLOR_LAST') but using stroke.
				
				return CB_GEM.data._pixelsStyleTemp;
			}
		},
		"Pong":
		{
			author: "Paul Vervalin",
			year: 1990,
			file: "Pong [Paul Vervalin, 1990].ch8",
			cyclesPerLoop: 2,
			contentMatches: function(ROMContent) //Returns true if the given ROM content matches this ROM. Used to identify ROMs.
			{
				return ROMContentToString(ROMContent).indexOf("iÿ¢ðÖq¢êÚ¶ÜÖ") !== -1;
			},
			keysMapping:
			{
				0x1:
				{
					keys: [ CB_Keyboard.keys._1, CB_Keyboard.keys.UP ],
					controllers: { gamepadIndex: 1, axis: { index: 1, min: -1, max: -0.5 }	}
				},
				0x4:
				{
					keys: [ CB_Keyboard.keys.Q, CB_Keyboard.keys.DOWN ],
					controllers: { gamepadIndex: 1, axis: { index: 1, min: 0.5, max: 1 } }
				},
				0xC:
				{
					keys: [ CB_Keyboard.keys._4, CB_Keyboard.keys.LEFT ],
					controllers: { gamepadIndex: 0, axis: { index: 1, min: -1, max: -0.5 }	}
				},
				0xD:
				{
					keys: [ CB_Keyboard.keys.R, CB_Keyboard.keys.RIGHT ],
					controllers: { gamepadIndex: 0, axis: { index: 1, min: 0.5, max: 1 } }
				}
			},
			//Called for each pixel before drawing it, to colourize the screen according to the ROM (to use personalized theme):
			pixelsColorizer: function(element, canvasContext, canvasBufferContext, useBuffer, CB_GraphicSpritesSceneObject, drawingMap, x, y, mapElement)
			{
				//Player 1's paddle:
				if (x === 2)
				{
					CB_GEM.data._pixelsStyleTemp.first = "#aa0000";
					CB_GEM.data._pixelsStyleTemp.last = "#0000aa";
				}
				//Player 2's paddle:
				else if (x === 63)
				{
					CB_GEM.data._pixelsStyleTemp.first = "#aa0000";
					CB_GEM.data._pixelsStyleTemp.last = "#aa0000";
				}
				//Others (scored goals and ball):
				else { return { stroke: true }; } //It will use default style (set on 'CB_GEM.data.PIXELS_COLOR_FIRST' and 'CB_GEM.data.PIXELS_COLOR_LAST') but using stroke.
				
				return CB_GEM.data._pixelsStyleTemp;
			}
		},
		"Space Intercept":
		{
			author: "Joseph Weisbecker",
			year: 1978,
			file: "Space Intercept [Joseph Weisbecker, 1978].ch8",
			cyclesPerLoop: 4,
			contentMatches: function(ROMContent) //Returns true if the given ROM content matches this ROM. Used to identify ROMs.
			{
				return ROMContentToString(ROMContent).indexOf("¢´õe¢ÀóU¢Àf") !== -1;
			},
			keysMapping:
			{
				0x4:
				{
					keys: [ CB_Keyboard.keys.Q, CB_Keyboard.keys.LEFT ],
					controllers: { gamepadIndex: "", axis: { index: 0, min: -1, max: -0.5 }	}
				},
				0x5:
				{
					keys: [ CB_Keyboard.keys.W, CB_Keyboard.keys.UP ],
					controllers: { gamepadIndex: "", axis: { index: 1, min: -1, max: -0.5 }	}
				},
				0x6:
				{
					keys: [ CB_Keyboard.keys.E, CB_Keyboard.keys.RIGHT ],
					controllers: { gamepadIndex: "", axis: { index: 0, min: 0.5, max: 1 } }
				}
			},
			//Called for each pixel before drawing it, to colourize the screen according to the ROM (to use personalized theme):
			pixelsColorizer: function(element, canvasContext, canvasBufferContext, useBuffer, CB_GraphicSpritesSceneObject, drawingMap, x, y, mapElement)
			{
				if (y > 26)
				{
					//Left numbers:
					if (x >= 0 && x < 14)
					{
						CB_GEM.data._pixelsStyleTemp.first = "#aa0000";
						CB_GEM.data._pixelsStyleTemp.last = "#0000aa";
					}
					//Right numbers:
					else if (x > 49)
					{
						CB_GEM.data._pixelsStyleTemp.first = "#aa0000";
						CB_GEM.data._pixelsStyleTemp.last = "#cc0000";
					}
					//Player:
					else if (y === 31)
					{
						CB_GEM.data._pixelsStyleTemp.first = "#aaaa00";
						CB_GEM.data._pixelsStyleTemp.last = "#00cccc";
						CB_GEM.data._pixelsStyleTemp.stroke = true;
					}
					//Others:
					else { return null; } //It will use default style (set on 'CB_GEM.data.PIXELS_COLOR_FIRST' and 'CB_GEM.data.PIXELS_COLOR_LAST').
				}
				else { return null; } //It will use default style (set on 'CB_GEM.data.PIXELS_COLOR_FIRST' and 'CB_GEM.data.PIXELS_COLOR_LAST').
				
				return CB_GEM.data._pixelsStyleTemp;
			}
		},
		"Space Invaders":
		{
			author: "David Winter",
			year: null,
			file: "Space Invaders [David Winter].ch8",
			cyclesPerLoop: 10,
			contentMatches: function(ROMContent) //Returns true if the given ROM content matches this ROM. Used to identify ROMs.
			{
				return ROMContentToString(ROMContent).indexOf("SPACE INVADERS 0.91 By David WINTER") !== -1;
			},
			keysMapping:
			{
				0x4:
				{
					keys: [ CB_Keyboard.keys.Q, CB_Keyboard.keys.LEFT ],
					controllers: { gamepadIndex: "", axis: { index: 0, min: -1, max: -0.5 }	}
				},
				0x5:
				{
					keys: [ CB_Keyboard.keys.W, CB_Keyboard.keys.UP, CB_Keyboard.keys.SPACEBAR, CB_Keyboard.keys.ENTER ],
					controllers: { gamepadIndex: "", axis: { index: 1, min: -1, max: -0.5 }, buttons: [ 0, 1, 2, 3 ] }
				},
				0x6:
				{
					keys: [ CB_Keyboard.keys.E, CB_Keyboard.keys.RIGHT ],
					controllers: { gamepadIndex: "", axis: { index: 0, min: 0.5, max: 1 } }
				}
			},
			//Called for each pixel before drawing it, to colourize the screen according to the ROM (to use personalized theme):
			pixelsColorizer: function(element, canvasContext, canvasBufferContext, useBuffer, CB_GraphicSpritesSceneObject, drawingMap, x, y, mapElement)
			{
				//NOTE: theme not personalized for this game to keep the example simpler.
				return null; //It will use default style (set on 'CB_GEM.data.PIXELS_COLOR_FIRST' and 'CB_GEM.data.PIXELS_COLOR_LAST').
			}
		},
		"Syzygy":
		{
			author: "Roy Trevino",
			year: 1990,
			file: "Syzygy [Roy Trevino, 1990].ch8",
			cyclesPerLoop: 3,
			timerDelayMsToDecrease: 30,
			contentMatches: function(ROMContent) //Returns true if the given ROM content matches this ROM. Used to identify ROMs.
			{
				var ROMContentString = ROMContentToString(ROMContent);
				return ROMContentString.indexOf(" ©1990 RTT") !== -1 && ROMContentString.indexOf("vÿ¥LÑ!Öqdði") !== -1;
			},
			keysMapping:
			{
				0x3:
				{
					keys: [ CB_Keyboard.keys.W, CB_Keyboard.keys.UP ],
					controllers: { gamepadIndex: "", axis: { index: 1, min: -1, max: -0.5 } }
				},
				0x6:
				{
					keys: [ CB_Keyboard.keys.E, CB_Keyboard.keys.DOWN ],
					controllers: { gamepadIndex: "", axis: { index: 1, min: 0.5, max: 1 } }
				},
				0x7:
				{
					keys: [ CB_Keyboard.keys.Q, CB_Keyboard.keys.LEFT ],
					controllers: { gamepadIndex: "", axis: { index: 0, min: -1, max: -0.5 }	}
				},
				0x8:
				{
					keys: [ CB_Keyboard.keys.S, CB_Keyboard.keys.RIGHT ],
					controllers: { gamepadIndex: "", axis: { index: 0, min: 0.5, max: 1 } }
				},
				0xB:
				{
					keys: [ CB_Keyboard.keys.F, CB_Keyboard.keys.ENTER, CB_Keyboard.keys.SPACEBAR ],
					controllers: { gamepadIndex: "", buttons: [ 0, 1, 2, 3 ] }
				},
				0xE:
				{
					keys: [ CB_Keyboard.keys.F, CB_Keyboard.keys.ENTER ],
					controllers: { gamepadIndex: "", buttons: [ 0, 1 ] }
				},
				0xF:
				{
					keys: [ CB_Keyboard.keys.V, CB_Keyboard.keys.SPACEBAR ],
					controllers: { gamepadIndex: "", buttons: [ 2, 3 ] }
				}
			},
			//Called for each pixel before drawing it, to colourize the screen according to the ROM (to use personalized theme):
			pixelsColorizer: function(element, canvasContext, canvasBufferContext, useBuffer, CB_GraphicSpritesSceneObject, drawingMap, x, y, mapElement)
			{
				//NOTE: theme not personalized for this game to keep the example simpler.
				return { stroke: true }; //It will use default style (set on 'CB_GEM.data.PIXELS_COLOR_FIRST' and 'CB_GEM.data.PIXELS_COLOR_LAST') but using stroke.
			}
		},
		"Tetris":
		{
			author: "Fran Dachille",
			year: 1991,
			file: "Tetris [Fran Dachille, 1991].ch8",
			cyclesPerLoop: 2,
			contentMatches: function(ROMContent) //Returns true if the given ROM content matches this ROM. Used to identify ROMs.
			{
				var ROMContentString = ROMContentToString(ROMContent);
				return ROMContentString.indexOf("¢´#æ") !== -1 && ROMContentString.indexOf("ò)Ýå§") !== -1;
			},
			keysMapping:
			{
				0x4:
				{
					keys: [ CB_Keyboard.keys.Q, CB_Keyboard.keys.UP, CB_Keyboard.keys.ENTER, CB_Keyboard.keys.SPACEBAR ],
					controllers: { gamepadIndex: "", axis: { index: 1, min: -1, max: -0.5 }, buttons: [ 0, 1, 2, 3 ] }
				},
				0x5:
				{
					keys: [ CB_Keyboard.keys.W, CB_Keyboard.keys.LEFT ],
					controllers: { gamepadIndex: "", axis: { index: 0, min: -1, max: -0.5 }	}
				},
				0x6:
				{
					keys: [ CB_Keyboard.keys.E, CB_Keyboard.keys.RIGHT ],
					controllers: { gamepadIndex: "", axis: { index: 0, min: 0.5, max: 1 } }
				}
			},
			//Called for each pixel before drawing it, to colourize the screen according to the ROM (to use personalized theme):
			pixelsColorizer: function(element, canvasContext, canvasBufferContext, useBuffer, CB_GraphicSpritesSceneObject, drawingMap, x, y, mapElement)
			{
				//Field border:
				if (y === 31 || x === 26 || x === 37)
				{
					CB_GEM.data._pixelsStyleTemp.first = "#aa0000";
					CB_GEM.data._pixelsStyleTemp.last = "#0000aa";
				}
				//Others:
				else { return { stroke: true }; } //It will use default style (set on 'CB_GEM.data.PIXELS_COLOR_FIRST' and 'CB_GEM.data.PIXELS_COLOR_LAST') but using stroke.
				
				return CB_GEM.data._pixelsStyleTemp;
			}
		},
		"Tic-Tac-Toe":
		{
			author: "David Winter",
			year: null,
			file: "Tic-Tac-Toe [David Winter].ch8",
			cyclesPerLoop: 5,
			contentMatches: function(ROMContent) //Returns true if the given ROM content matches this ROM. Used to identify ROMs.
			{
				return ROMContentToString(ROMContent).indexOf("TICTAC by David WINTERk") !== -1;
			},
			keysMapping:
			{
				0x0: { keys: null, controllers: null },
				0x1:
				{
					keys: [ CB_Keyboard.keys.Q, CB_Keyboard.keys._7, CB_Keyboard.keys.UP ],
					controllers: { gamepadIndex: "", axis: { index: 1, min: -1, max: -0.5 } }
				},
				0x2:
				{
					keys: [ CB_Keyboard.keys.W, CB_Keyboard.keys._8, CB_Keyboard.keys.DOWN ],
					controllers: { gamepadIndex: "", axis: { index: 1, min: 0.5, max: 1 } }
				},
				0x3:
				{
					keys: [ CB_Keyboard.keys.E, CB_Keyboard.keys._9, CB_Keyboard.keys.LEFT ],
					controllers: { gamepadIndex: "", axis: { index: 0, min: -1, max: -0.5 }	}
				},
				0x4:
				{
					keys: [ CB_Keyboard.keys.A, CB_Keyboard.keys._4, CB_Keyboard.keys.RIGHT ],
					controllers: { gamepadIndex: "", axis: { index: 0, min: 0.5, max: 1 } }
				},
				0x5:
				{
					keys: [ CB_Keyboard.keys.S, CB_Keyboard.keys._5, CB_Keyboard.keys.ENTER ],
					controllers: { gamepadIndex: "", buttons: [ 0 ] }
				},
				0x6:
				{
					keys: [ CB_Keyboard.keys.D, CB_Keyboard.keys._6, CB_Keyboard.keys.SPACEBAR ],
					controllers: { gamepadIndex: "", buttons: [ 1 ] }
				},
				0x7:
				{
					keys: [ CB_Keyboard.keys.Z, CB_Keyboard.keys._1, CB_Keyboard.keys.SHIFT ],
					controllers: { gamepadIndex: "", buttons: [ 2 ] }
				},
				0x8:
				{
					keys: [ CB_Keyboard.keys.X, CB_Keyboard.keys._2, CB_Keyboard.keys.CTRL ],
					controllers: { gamepadIndex: "", buttons: [ 3 ] }
				},
				0x9:
				{
					keys: [ CB_Keyboard.keys.C, CB_Keyboard.keys._3, CB_Keyboard.keys.TAB ],
					controllers: { gamepadIndex: "", buttons: [ 4 ] }
				}
			},
			//Called for each pixel before drawing it, to colourize the screen according to the ROM (to use personalized theme):
			pixelsColorizer: function(element, canvasContext, canvasBufferContext, useBuffer, CB_GraphicSpritesSceneObject, drawingMap, x, y, mapElement)
			{
				//Left side:
				if (x > 1 && x < 16)
				{
					//Left cross ('X'):
					if (y > 9 && y < 15)
					{
						CB_GEM.data._pixelsStyleTemp.first = "#aa0000";
						CB_GEM.data._pixelsStyleTemp.last = "#0000aa";
					}
					//Left score:
					else
					{
						CB_GEM.data._pixelsStyleTemp.first = "#00aaaa";
						CB_GEM.data._pixelsStyleTemp.last = "#00dddd";
						CB_GEM.data._pixelsStyleTemp.stroke = true;
					}
				}
				//Right side:
				else if (x > 46 && x < 61)
				{
					//Left circle ('O'):
					if (y > 9 && y < 15)
					{
						CB_GEM.data._pixelsStyleTemp.first = "#0000aa";
						CB_GEM.data._pixelsStyleTemp.last = "#aa0000";
					}
					//Right score:
					else
					{
						CB_GEM.data._pixelsStyleTemp.first = "#00aaaa";
						CB_GEM.data._pixelsStyleTemp.last = "#00dddd";
						CB_GEM.data._pixelsStyleTemp.stroke = true;
					}
				}
				//Board:
				else if (y === 3 || y === 11 || y === 19 || y === 27 || x === 19 || x === 27 || x === 35 || x === 43)
				{
					CB_GEM.data._pixelsStyleTemp.first = "#aa00aa";
					CB_GEM.data._pixelsStyleTemp.last = "#dd00dd";
				}
				//Others:
				else { return null; } //It will use default style (set on 'CB_GEM.data.PIXELS_COLOR_FIRST' and 'CB_GEM.data.PIXELS_COLOR_LAST').
				
				return CB_GEM.data._pixelsStyleTemp;
			}
		},
		"Vertical Brix":
		{
			author: "Paul Robson",
			year: 1996,
			file: "Vertical Brix [Paul Robson, 1996].ch8",
			cyclesPerLoop: 5,
			contentMatches: function(ROMContent) //Returns true if the given ROM content matches this ROM. Used to identify ROMs.
			{
				var ROMContentString = ROMContentToString(ROMContent);
				return ROMContentString.indexOf("Ô£dK") !== -1 && ROMContentString.indexOf("°pÞqÿbÿcÿd") !== -1;
			},
			keysMapping:
			{
				0x1:
				{
					keys: [ CB_Keyboard.keys._1, CB_Keyboard.keys.UP ],
					controllers: { gamepadIndex: "", axis: { index: 1, min: -1, max: -0.5 }	}
				},
				0x4:
				{
					keys: [ CB_Keyboard.keys.Q, CB_Keyboard.keys.DOWN ],
					controllers: { gamepadIndex: "", axis: { index: 1, min: 0.5, max: 1 } }
				},
				0x7:
				{
					keys: [ CB_Keyboard.keys.A, CB_Keyboard.keys.UP, CB_Keyboard.keys.DOWN, CB_Keyboard.keys.SPACEBAR, CB_Keyboard.keys.ENTER ],
					controllers: { gamepadIndex: "", buttons: [ 0, 1, 2, 3 ] }
				}
			},
			//Called for each pixel before drawing it, to colourize the screen according to the ROM (to use personalized theme):
			pixelsColorizer: function(element, canvasContext, canvasBufferContext, useBuffer, CB_GraphicSpritesSceneObject, drawingMap, x, y, mapElement)
			{
				//Border:
				if (y === 0 || y === 31 || x === 63)
				{
					CB_GEM.data._pixelsStyleTemp.first = "#00aaaa";
					CB_GEM.data._pixelsStyleTemp.last = "#00dddd";
				}
				//Paddle (player):
				else if (x === 2)
				{
					CB_GEM.data._pixelsStyleTemp.first = "#aa0000";
					CB_GEM.data._pixelsStyleTemp.last = "#dd0000";
				}
				//Others:
				else { return null; } //It will use default style (set on 'CB_GEM.data.PIXELS_COLOR_FIRST' and 'CB_GEM.data.PIXELS_COLOR_LAST').
				
				return CB_GEM.data._pixelsStyleTemp;
			}
		},
		"Wipe Off":
		{
			author: "Joseph Weisbecker",
			year: null,
			file: "Wipe Off [Joseph Weisbecker].ch8",
			cyclesPerLoop: 5,
			contentMatches: function(ROMContent) //Returns true if the given ROM content matches this ROM. Used to identify ROMs.
			{
				var ROMContentString = ROMContentToString(ROMContent);
				return ROMContentString.indexOf("¢ËÒ1eÿÄ") !== -1 && ROMContentString.indexOf("¢ËÒ1sÿ") !== -1;
			},
			keysMapping:
			{
				0x4:
				{
					keys: [ CB_Keyboard.keys.Q, CB_Keyboard.keys.LEFT ],
					controllers: { gamepadIndex: "", axis: { index: 0, min: -1, max: -0.5 }	}
				},
				0x6:
				{
					keys: [ CB_Keyboard.keys.E, CB_Keyboard.keys.RIGHT ],
					controllers: { gamepadIndex: "", axis: { index: 0, min: 0.5, max: 1 } }
				}
			},
			//Called for each pixel before drawing it, to colourize the screen according to the ROM (to use personalized theme):
			pixelsColorizer: function(element, canvasContext, canvasBufferContext, useBuffer, CB_GraphicSpritesSceneObject, drawingMap, x, y, mapElement)
			{
				//Paddle (player):
				//NOTE: we could check 'screenBitMap' to prevent detecting the ball as the paddle when it is in the same horizontal line. Not done to keep the example simpler.
				if (y === 30)
				{
					CB_GEM.data._pixelsStyleTemp.first = "#aa0000";
					CB_GEM.data._pixelsStyleTemp.last = "#dd0000";
				}
				//Others:
				else { return null; } //It will use default style (set on 'CB_GEM.data.PIXELS_COLOR_FIRST' and 'CB_GEM.data.PIXELS_COLOR_LAST').
				
				return CB_GEM.data._pixelsStyleTemp;
			}
		},
		"Worm V4":
		{
			author: "RB-Revival Studios",
			year: 2007,
			file: "Worm V4 [RB-Revival Studios, 2007].ch8",
			cyclesPerLoop: 2,
			contentMatches: function(ROMContent) //Returns true if the given ROM content matches this ROM. Used to identify ROMs.
			{
				return ROMContentToString(ROMContent).indexOf("Worm v.4, by: RB, Chip-8 version by: Martijn Wenting / Revival Studios") !== -1;
			},
			keysMapping:
			{
				0x2:
				{
					keys: [ CB_Keyboard.keys._2, CB_Keyboard.keys.UP ],
					controllers: { gamepadIndex: "", axis: { index: 1, min: -1, max: -0.5 }	}
				},
				0x4:
				{
					keys: [ CB_Keyboard.keys.Q, CB_Keyboard.keys.LEFT ],
					controllers: { gamepadIndex: "", axis: { index: 0, min: -1, max: -0.5 }	}
				},
				0x6:
				{
					keys: [ CB_Keyboard.keys.E, CB_Keyboard.keys.RIGHT ],
					controllers: { gamepadIndex: "", axis: { index: 0, min: 0.5, max: 1 } }
				},
				0x8:
				{
					keys: [ CB_Keyboard.keys.S, CB_Keyboard.keys.DOWN ],
					controllers: { gamepadIndex: "", axis: { index: 1, min: 0.5, max: 1 } }
				}
			},
			//Called for each pixel before drawing it, to colourize the screen according to the ROM (to use personalized theme):
			pixelsColorizer: function(element, canvasContext, canvasBufferContext, useBuffer, CB_GraphicSpritesSceneObject, drawingMap, x, y, mapElement)
			{
				//Score:
				if (x > 59)
				{
					CB_GEM.data._pixelsStyleTemp.first = "#00aaaa";
					CB_GEM.data._pixelsStyleTemp.last = "#00dddd";
				}
				//Border:
				else if (x === 0 || x === 58 || y === 0 || y === 31)
				{
					CB_GEM.data._pixelsStyleTemp.first = "#aa0000";
					CB_GEM.data._pixelsStyleTemp.last = "#dd0000";
				}
				//Others:
				else { return null; } //It will use default style (set on 'CB_GEM.data.PIXELS_COLOR_FIRST' and 'CB_GEM.data.PIXELS_COLOR_LAST').
				
				return CB_GEM.data._pixelsStyleTemp;
			}
		},
		"ZeroPong":
		{
			author: "zeroZshadow",
			year: 2007,
			file: "ZeroPong [zeroZshadow, 2007].ch8",
			cyclesPerLoop: 5,
			contentMatches: function(ROMContent) //Returns true if the given ROM content matches this ROM. Used to identify ROMs.
			{
				var ROMContentString = ROMContentToString(ROMContent);
				return ROMContentString.indexOf("<Ù¦zÿÙ¦¢") !== -1 && ROMContentString.indexOf("eÿÓA`È") !== -1;
			},
			keysMapping:
			{
				0x2:
				{
					keys: [ CB_Keyboard.keys._2, CB_Keyboard.keys.LEFT ],
					controllers: { gamepadIndex: 0, axis: { index: 1, min: -1, max: -0.5 }	}
				},
				0x8:
				{
					keys: [ CB_Keyboard.keys.S, CB_Keyboard.keys.RIGHT ],
					controllers: { gamepadIndex: 0, axis: { index: 1, min: 0.5, max: 1 } }
				},
				0xB:
				{
					keys: [ CB_Keyboard.keys.C, CB_Keyboard.keys.UP ],
					controllers: { gamepadIndex: 1, axis: { index: 1, min: -1, max: -0.5 }	}
				},
				0xA:
				{
					keys: [ CB_Keyboard.keys.Z, CB_Keyboard.keys.DOWN ],
					controllers: { gamepadIndex: 1, axis: { index: 1, min: 0.5, max: 1 } }
				},
				0xF:
				{
					keys: [ CB_Keyboard.keys.V, CB_Keyboard.keys.SPACE, CB_Keyboard.keys.ENTER ],
					controllers: { gamepadIndex: "", buttons: [ 0, 1, 2, 3 ] }
				}
			},
			//Called for each pixel before drawing it, to colourize the screen according to the ROM (to use personalized theme):
			pixelsColorizer: function(element, canvasContext, canvasBufferContext, useBuffer, CB_GraphicSpritesSceneObject, drawingMap, x, y, mapElement)
			{
				//NOTE: we could check 'screenBitMap' to prevent detecting the ball as the paddles when it is in the same vertical line. Not done to keep the example simpler.
				
				//Player 1's paddle:
				if (x === 2)
				{
					CB_GEM.data._pixelsStyleTemp.first = "#aa0000";
					CB_GEM.data._pixelsStyleTemp.last = "#0000aa";
				}
				//Player 2's paddle:
				else if (x === 62)
				{
					CB_GEM.data._pixelsStyleTemp.first = "#aa0000";
					CB_GEM.data._pixelsStyleTemp.last = "#aa0000";
				}
				//Others:
				else { return null; } //It will use default style (set on 'CB_GEM.data.PIXELS_COLOR_FIRST' and 'CB_GEM.data.PIXELS_COLOR_LAST').
				
				return CB_GEM.data._pixelsStyleTemp;
			}
		}
	};
}


//Identifies and returns the ROM identifier through the given ROM content (if possible):
function getROMIdFromROMContent(ROMContent)
{
	if (ROMs)
	{
		for (var ROMId in ROMs)
		{
			if (ROMs[ROMId] && typeof(ROMs[ROMId].contentMatches) === "function" && ROMs[ROMId].contentMatches(ROMContent))
			{
				return ROMId;
			}
		}
	}
	return null; //Returns null when it could not be identified.
}


//Returns the given ROM content as a string:
function ROMContentToString(ROMContent)
{
	var ROMContentString = "";
	for (var x = 0; x < ROMContent.length; x++)
	{
		ROMContentString += String.fromCharCode(ROMContent[x]);
	}
	return ROMContentString;
}

Additional files used (inside the "ROMs" folder) which belong to the different ROMs (CHIP-8 software) downloaded from here: Blinky [Hans Christian Egeberg, 1991].ch8, Brix [Andreas Gustafsson, 1990].ch8, Pong [Paul Vervalin, 1990].ch8, Space Intercept [Joseph Weisbecker, 1978].ch8, Space Invaders [David Winter].ch8, Syzygy [Roy Trevino, 1990].ch8, Tetris [Fran Dachille, 1991].ch8, Tic-Tac-Toe [David Winter].ch8, Vertical Brix [Paul Robson, 1996].ch8, Wipe Off [Joseph Weisbecker].ch8, Worm V4 [RB-Revival Studios, 2007].ch8 and ZeroPong [zeroZshadow, 2007].ch8.

Try this example

You can check the Guides & Tutorials category as well as the API documentation in the case you need more information.

All the examples together can be downloaded here.

Go back to Guides & Tutorials

Try this example












Share