diff --git a/examples/sdl/src/main.rs b/examples/sdl/src/main.rs
index 73969689f8a5d8e9089bf14fd57cdf4c02ef6853..531febeac65b21655f45fca6bd10c655d3a346a9 100644
--- a/examples/sdl/src/main.rs
+++ b/examples/sdl/src/main.rs
@@ -80,8 +80,8 @@ fn main() {
 
     let mut game_boy = GameBoy::new();
     game_boy.load_boot_default();
-    game_boy.load_rom("../../res/roms/ld_r_r.gb");
-    //game_boy.load_rom("../../res/roms/opus5.gb");
+    game_boy.load_rom_file("../../res/roms/ld_r_r.gb");
+    //game_boy.load_rom_file("../../res/roms/opus5.gb");
 
     let mut counter = 0;
 
diff --git a/examples/web/.gitignore b/examples/web/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..5263f1b298966afe55b0447f940353959f789c55
--- /dev/null
+++ b/examples/web/.gitignore
@@ -0,0 +1,7 @@
+yarn.lock
+package-lock.json
+
+/.parcel-cache
+
+/dist
+/node_modules
diff --git a/examples/web/.parcelrc b/examples/web/.parcelrc
new file mode 100644
index 0000000000000000000000000000000000000000..50fbdcdb8d58cfcf093cd82645bea9d2e088c0c2
--- /dev/null
+++ b/examples/web/.parcelrc
@@ -0,0 +1,7 @@
+{
+    "extends": "@parcel/config-default",
+    "transformers": {
+        "*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"],
+        "*.ch8": ["@parcel/transformer-raw"]
+    }
+}
diff --git a/examples/web/.prettierrc b/examples/web/.prettierrc
new file mode 100644
index 0000000000000000000000000000000000000000..6afb03785f6c8c93401f7b35905b6d8f8065e499
--- /dev/null
+++ b/examples/web/.prettierrc
@@ -0,0 +1,7 @@
+{
+    "semi": true,
+    "trailingComma": "none",
+    "singleQuote": false,
+    "tabWidth": 4,
+    "endOfLine": "crlf"
+}
diff --git a/examples/web/index.css b/examples/web/index.css
new file mode 100644
index 0000000000000000000000000000000000000000..31f6533f372fd16b64435dfc0c076ff4d6a0a3c5
--- /dev/null
+++ b/examples/web/index.css
@@ -0,0 +1,632 @@
+@import url("https://fonts.googleapis.com/css2?family=VT323&display=swap");
+
+* {
+    box-sizing: border-box;
+    -o-box-sizing: border-box;
+    -ms-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    -khtml-box-sizing: border-box;
+    -webkit-box-sizing: border-box;
+}
+
+a {
+    border-bottom: 2px dotted #ffffff;
+    color: #ffffff;
+    text-decoration: none;
+}
+
+a:hover {
+    border-bottom-style: solid;
+}
+
+html {
+    margin: 0px 0px 0px 0px;
+    padding: 0px 0px 0px 0px;
+}
+
+body {
+    color: #ffffff;
+    font-family: "VT323", "Robot", "Open Sans", Arial, Helvetica, sans-serif;
+    margin: 0px 0px 0px 0px;
+    padding: 12px 12px 52px 12px;
+}
+
+p {
+    font-size: 18px;
+    line-height: 24px;
+    margin: 12px 0px 12px 0px;
+}
+
+.main {
+    display: flex;
+}
+
+@media only screen and (max-width: 1120px) {
+    .main {
+        flex-direction: column;
+    }
+}
+
+.main > .side-left {
+    display: flex;
+    flex: 1 0;
+    justify-content: center;
+    text-align: center;
+}
+
+.main > .side-left .canvas-container {
+    max-width: 100%;
+}
+
+.main > .side-left .canvas-container.fullscreen {
+    align-items: center;
+    background-color: #2d2d2d;
+    display: flex;
+    height: 100%;
+    justify-content: center;
+    left: 0px;
+    position: fixed;
+    top: 0px;
+    width: 100%;
+    z-index: 6;
+}
+
+.main > .side-left .canvas-container > .canvas-close {
+    bottom: 22px;
+    display: none;
+    position: absolute;
+    right: 22px;
+}
+
+.main > .side-left .canvas-container > .canvas-close > img {
+    height: 32px;
+    width: 32px;
+}
+
+.main > .side-left .canvas-container.fullscreen > .canvas-close {
+    display: block;
+}
+
+.main > .side-left .canvas-container > .canvas-frame {
+    background-color: #1b1a17;
+    border: 2px solid #50cb93;
+    font-size: 0px;
+    margin-top: 78px;
+    max-width: 660px;
+    padding: 8px 8px 8px 8px;
+}
+
+@media only screen and (max-width: 1120px) {
+    .main > .side-left .canvas-container > .canvas-frame {
+        margin-top: 12px;
+    }
+}
+
+.main > .side-left .canvas-container.fullscreen > .canvas-frame {
+    background-color: transparent;
+    border: none;
+    box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.24);
+    -o-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.24);
+    -ms-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.24);
+    -moz-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.24);
+    -khtml-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.24);
+    -webkit-box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.24);
+    margin: 0px 0px 0px 0px;
+    max-width: unset;
+    padding: 0px 0px 0px 0px;
+}
+
+.main > .side-left .canvas-container > .canvas-frame > .canvas {
+    width: 100%;
+}
+
+.main > .side-right {
+    flex: 0 1;
+    max-width: 100%;
+    min-width: 580px;
+    padding: 0px 24px 0px 24px;
+}
+
+@media only screen and (max-width: 1120px) {
+    .main > .side-right {
+        min-width: unset;
+        padding: 0px 0px 0px 0px;
+    }
+}
+
+.main > .side-right .logo-image {
+    vertical-align: middle;
+    width: 32px;
+}
+
+.main > .side-right .separator {
+    background: #ffffff;
+    height: 2px;
+    margin: 22px 0px 22px 0px;
+}
+
+.main > .side-right .diag {
+    font-size: 24px;
+    vertical-align: top;
+}
+
+.main > .side-right .diag > dt {
+    clear: both;
+    float: left;
+    margin-top: 12px;
+}
+
+.main > .side-right .diag > dt:first-of-type {
+    margin-top: 0px;
+}
+
+.main > .side-right .diag > dd {
+    float: right;
+    margin-top: 12px;
+}
+
+.main > .side-right .diag > dd:first-of-type {
+    margin-top: 0px;
+}
+
+.main > .side-right .diag::after {
+    clear: both;
+    content: '';
+    display: block;
+}
+
+.footer {
+    bottom: 0px;
+    height: 40px;
+    left: 0px;
+    line-height: 40px;
+    padding: 0px 0px 0px 0px;
+    position: fixed;
+    text-align: center;
+    width: 100%;
+}
+
+.footer-background {
+    bottom: 0px;
+    filter: blur(1.0rem);
+    -o-filter: blur(1.0rem);
+    -ms-filter: blur(1.0rem);
+    -moz-filter: blur(1.0rem);
+    -khtml-filter: blur(1.0rem);
+    -webkit-filter: blur(1.0rem);
+    height: 40px;
+    left: 0px;
+    position: fixed;
+    width: 100%;
+}
+
+.toast-container {
+    background-color: black;
+    height: 0px;
+    left: 0px;
+    padding: 0px 24px 0px 24px;
+    pointer-events: none;
+    position: fixed;
+    text-align: center;
+    top: 0px;
+    width: 100%;
+    z-index: 8;
+}
+
+.toast-container > .toast {
+    background-color: #2a9d8f;
+    border-radius: 4px 4px 4px 4px;
+    -o-border-radius: 4px 4px 4px 4px;
+    -ms-border-radius: 4px 4px 4px 4px;
+    -moz-border-radius: 4px 4px 4px 4px;
+    -khtml-border-radius: 4px 4px 4px 4px;
+    -webkit-border-radius: 4px 4px 4px 4px;
+    cursor: pointer;
+    display: inline-block;
+    font-size: 20px;
+    line-height: 22px;
+    opacity: 0.0;
+    -o-opacity: 0.0;
+    -ms-opacity: 0.0;
+    -moz-opacity: 0.0;
+    -khtml-opacity: 0.0;
+    -webkit-opacity: 0.0;
+    padding: 12px 18px 12px 18px;
+    position: relative;
+    top: -46px;
+    transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -o-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -ms-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -moz-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -khtml-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -webkit-transition: top 0.5s cubic-bezier(0.075, 0.82, 0.165, 1), opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    width: fit-content;
+}
+
+.toast-container > .toast.error {
+    background-color: #e63946;
+}
+
+.toast-container > .toast.visible {
+    opacity: 1.0;
+    -o-opacity: 1.0;
+    -ms-opacity: 1.0;
+    -moz-opacity: 1.0;
+    -khtml-opacity: 1.0;
+    -webkit-opacity: 1.0;
+    pointer-events: all;
+    top: 24px;
+}
+
+.button-area {
+    user-select: none;
+    -o-user-select: none;
+    -ms-user-select: none;
+    -moz-user-select: none;
+    -khtml-user-select: none;
+    -webkit-user-select: none;
+}
+
+.button-area > * {
+    margin-bottom: 12px;
+}
+
+.magnify-button {
+    cursor: pointer;
+    display: inline-block;
+    transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -o-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -ms-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -moz-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -khtml-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -webkit-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+}
+
+.magnify-button:hover {
+    transform: scale(1.3, 1.3);
+    -o-transform: scale(1.3, 1.3);
+    -ms-transform: scale(1.3, 1.3);
+    -moz-transform: scale(1.3, 1.3);
+    -khtml-transform: scale(1.3, 1.3);
+    -webkit-transform: scale(1.3, 1.3);
+}
+
+.magnify-button:active {
+    transform: scale(1.0, 1.0);
+    -o-transform: scale(1.0, 1.0);
+    -ms-transform: scale(1.0, 1.0);
+    -moz-transform: scale(1.0, 1.0);
+    -khtml-transform: scale(1.0, 1.0);
+    -webkit-transform: scale(1.0, 1.0);
+}
+
+.tiny-button {
+    border-radius: 96px 96px 96px 96px;
+    -o-border-radius: 96px 96px 96px 96px;
+    -ms-border-radius: 96px 96px 96px 96px;
+    -moz-border-radius: 96px 96px 96px 96px;
+    -khtml-border-radius: 96px 96px 96px 96px;
+    -webkit-border-radius: 96px 96px 96px 96px;
+    cursor: pointer;
+    display: inline-block;
+    padding: 0px 8px 0px 8px;
+    user-select: none;
+    -o-user-select: none;
+    -ms-user-select: none;
+    -moz-user-select: none;
+    -khtml-user-select: none;
+    -webkit-user-select: none;
+}
+
+.tiny-button.border {
+    border: 1px solid #ffffff;
+}
+
+.tiny-button.padded {
+    padding: 4px 10px 4px 10px;
+}
+
+.tiny-button.padded-large {
+    padding: 4px 14px 4px 14px;
+}
+
+.tiny-button.rounded {
+    padding: 6px 6px 6px 6px;
+}
+
+.tiny-button.enabled {
+    background-color: #50cb93;
+}
+
+.tiny-button.file {
+    position: relative;
+}
+
+.tiny-button:hover {
+    background-color: #50cb93;
+}
+
+.tiny-button.red:hover {
+    background-color: #e63946;
+}
+
+.tiny-button:active {
+    background-color: #2a9d8f;
+}
+
+.tiny-button.red:active {
+    background-color: #bf2a37;
+}
+
+.tiny-button > img {
+    margin-right: 6px;
+    margin-top: 2px;
+    vertical-align: top;
+    width: 13px;
+}
+
+.tiny-button > img.medium {
+    width: 20px;
+}
+
+.tiny-button > img.large {
+    width: 28px;
+}
+
+.tiny-button > img.very-large {
+    width: 38px;
+}
+
+.tiny-button.no-text > img {
+    margin-right: 0px;
+    margin-top: 0px;
+}
+
+.tiny-button.file > input[type="file"] {
+    cursor: pointer;
+    height: 100%;
+    left: 0px;
+    opacity: 0;
+    -o-opacity: 0;
+    -ms-opacity: 0;
+    -moz-opacity: 0;
+    -khtml-opacity: 0;
+    -webkit-opacity: 0;
+    position: absolute;
+    top: 0px;
+    vertical-align: top;
+    width: 100%;
+}
+
+.tiny-button.file > input[type="file"]::-webkit-file-upload-button {
+    cursor: pointer;
+}
+
+.overlay {
+    align-items: center;
+    background-color: rgba(80, 203, 147, 0.95);
+    display: flex;
+    font-size: 48px;
+    height: 100%;
+    justify-content: center;
+    left: 0px;
+    opacity: 0.0;
+    -o-opacity: 0.0;
+    -ms-opacity: 0.0;
+    -moz-opacity: 0.0;
+    -khtml-opacity: 0.0;
+    -webkit-opacity: 0.0;
+    pointer-events: none;
+    position: fixed;
+    text-align: center;
+    top: 0px;
+    transition: opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -o-transition: opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -ms-transition: opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -moz-transition: opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -khtml-transition: opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -webkit-transition: opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    width: 100%;
+    z-index: 10;
+}
+
+.overlay.visible {
+    opacity: 1.0;
+    -o-opacity: 1.0;
+    -ms-opacity: 1.0;
+    -moz-opacity: 1.0;
+    -khtml-opacity: 1.0;
+    -webkit-opacity: 1.0;
+}
+
+.overlay .overlay-image {
+    margin-top: 16px;
+}
+
+.overlay .overlay-image > img {
+    width: 64px;
+}
+
+.modal-container {
+    align-items: center;
+    background-color: rgba(20, 20, 20, 0.95);
+    display: flex;
+    height: 100%;
+    justify-content: center;
+    left: 0px;
+    opacity: 0;
+    -o-opacity: 0;
+    -ms-opacity: 0;
+    -moz-opacity: 0;
+    -khtml-opacity: 0;
+    -webkit-opacity: 0;
+    padding: 0px 12px 0px 12px;
+    pointer-events: none;
+    position: fixed;
+    text-align: center;
+    top: 0px;
+    transition: opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -o-transition: opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -ms-transition: opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -moz-transition: opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -khtml-transition: opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -webkit-transition: opacity 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    width: 100%;
+    z-index: 10;
+}
+
+.modal-container.visible {
+    opacity: 1.0;
+    -o-opacity: 1.0;
+    -ms-opacity: 1.0;
+    -moz-opacity: 1.0;
+    -khtml-opacity: 1.0;
+    -webkit-opacity: 1.0;
+    transition: opacity 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -o-transition: opacity 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -ms-transition: opacity 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -moz-transition: opacity 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -khtml-transition: opacity 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -webkit-transition: opacity 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+}
+
+.modal-container > .modal {
+    background-color: #264653;
+    border-radius: 6px 6px 6px 6px;
+    -o-border-radius: 6px 6px 6px 6px;
+    -ms-border-radius: 6px 6px 6px 6px;
+    -moz-border-radius: 6px 6px 6px 6px;
+    -khtml-border-radius: 6px 6px 6px 6px;
+    -webkit-border-radius: 6px 6px 6px 6px;
+    box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.24);
+    -o-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.24);
+    -ms-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.24);
+    -moz-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.24);
+    -khtml-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.24);
+    -webkit-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.24);
+    max-width: 100%;
+    padding: 24px 24px 24px 24px;
+    text-align: left;
+    transform: scale(0.96);
+    -o-transform: scale(0.96);
+    -ms-transform: scale(0.96);
+    -moz-transform: scale(0.96);
+    -khtml-transform: scale(0.96);
+    -webkit-transform: scale(0.96);
+    transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -o-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -ms-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -moz-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -khtml-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -webkit-transition: transform 0.35s cubic-bezier(0.075, 0.82, 0.165, 1);
+    width: 480px;
+}
+
+.modal-container.visible > .modal {
+    pointer-events: all;
+    transform: scale(1);
+    -o-transform: scale(1);
+    -ms-transform: scale(1);
+    -moz-transform: scale(1);
+    -khtml-transform: scale(1);
+    -webkit-transform: scale(1);
+    transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -o-transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -ms-transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -moz-transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -khtml-transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+    -webkit-transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
+}
+
+.modal-container > .modal .modal-top-buttons {
+    float: right;
+    margin-right: -10px;
+    margin-top: -10px;
+}
+
+.modal-container > .modal .modal-title {
+    font-size: 32px;
+    margin-top: 0px;
+    text-align: left;
+}
+
+.modal-container > .modal .modal-text {
+    font-size: 20px;
+    line-height: 22px;
+}
+
+.modal-container > .modal .modal-buttons {
+    font-size: 22px;
+    margin-top: 24px;
+    text-align: center;
+    user-select: none;
+    -o-user-select: none;
+    -ms-user-select: none;
+    -moz-user-select: none;
+    -khtml-user-select: none;
+    -webkit-user-select: none;
+}
+
+.modal-container > .modal .modal-buttons > .tiny-button {
+    margin-right: 12px;
+    min-width: 120px;
+}
+
+.modal-container > .modal .modal-buttons > .tiny-button:last-child {
+    margin-right: 0px;
+}
+
+.keyboard {
+    font-size: 0px;
+    text-align: center;
+    touch-callout: none;
+    -o-touch-callout: none;
+    -ms-touch-callout: none;
+    -moz-touch-callout: none;
+    -khtml-touch-callout: none;
+    -webkit-touch-callout: none;
+    user-select: none;
+    -o-user-select: none;
+    -ms-user-select: none;
+    -moz-user-select: none;
+    -khtml-user-select: none;
+    -webkit-user-select: none;
+}
+
+.keyboard > .keyboard-line {
+    margin-bottom: 12px;
+}
+
+.keyboard > .keyboard-line:last-child {
+    margin-bottom: 0px;
+}
+
+.keyboard .key {
+    border: 2px solid #ffffff;
+    border-radius: 5px 5px 5px 5px;
+    -o-border-radius: 5px 5px 5px 5px;
+    -ms-border-radius: 5px 5px 5px 5px;
+    -moz-border-radius: 5px 5px 5px 5px;
+    -khtml-border-radius: 5px 5px 5px 5px;
+    -webkit-border-radius: 5px 5px 5px 5px;
+    cursor: pointer;
+    display: inline-block;
+    font-size: 38px;
+    height: 48px;
+    line-height: 46px;
+    margin-right: 14px;
+    text-align: center;
+    width: 48px;
+}
+
+.keyboard .key:last-child {
+    margin-right: 0px;
+}
+
+.keyboard .key:hover {
+    background-color: #50cb93;
+}
+
+.keyboard .key:active {
+    background-color: #2a9d8f;
+}
diff --git a/examples/web/index.html b/examples/web/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..c66238ada7f1a561e7c6a3809b98c21e99d4866c
--- /dev/null
+++ b/examples/web/index.html
@@ -0,0 +1,158 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <title>Boytacean</title>
+    <meta charset="utf-8">
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <meta name="description" content="Game Boy emulator written in Rust 🦀." />
+    <meta name="viewport"
+        content="width=device-width, user-scalable=yes, initial-scale=1, minimum-scale=1, maximum-scale=5" />
+    <link rel="icon" href="res/icon.png" />
+    <link rel="stylesheet" href="index.css" />
+</head>
+
+<body>
+    <div class="main">
+        <div class="side-left">
+            <div id="canvas-container" class="canvas-container">
+                <span id="canvas-close" class="magnify-button canvas-close">
+                    <img class="large" src="res/minimise.svg" alt="minimise" />
+                </span>
+                <div class="canvas-frame">
+                    <canvas id="chip-canvas" class="canvas" width="640" height="320"></canvas>
+                </div>
+            </div>
+        </div>
+        <div class="side-right">
+            <h1>Boytacean <a id="version" href="https://gitlab.stage.hive.pt/joamag/boytacean/-/blob/master/CHANGELOG.md" target="_blank"></a> <img class="logo-image" src="res/thunder.png" alt="thunder" />
+            </h1>
+            <div class="separator"></div>
+            <div id="section-narrative" class="section">
+                <p>This is a <a href="https://en.wikipedia.org/wiki/Game_Boy" target="_blank">Game Boy</a> emulator built using
+                    the <a href="https://www.rust-lang.org" target="_blank">Rust Programming Language</a> and is running
+                    inside this browser with the help of <a href="https://webassembly.org/" target="_blank">WebAssembly</a>.
+                </p>
+                <p>You can check the source code of it at <a href="https://gitlab.stage.hive.pt/joamag/boytacean"
+                        target="_blank">GitLab</a>.</p>
+                <p>TIP: Drag and Drop ROM files to the Browser to load the ROM.</p>
+            </div>
+            <div id="separator-narrative"  class="separator"></div>
+            <div id="section-keyboard" class="section" style="display: none;">
+                <div id="keyboard" class="keyboard">
+                    <div class="keyboard-line">
+                        <span class="key">1</span>
+                        <span class="key">2</span>
+                        <span class="key">3</span>
+                        <span class="key">4</span>
+                    </div>
+                    <div class="keyboard-line">
+                        <span class="key">Q</span>
+                        <span class="key">W</span>
+                        <span class="key">E</span>
+                        <span class="key">R</span>
+                    </div>
+                    <div class="keyboard-line">
+                        <span class="key">A</span>
+                        <span class="key">S</span>
+                        <span class="key">D</span>
+                        <span class="key">F</span>
+                    </div>
+                    <div class="keyboard-line">
+                        <span class="key">Z</span>
+                        <span class="key">X</span>
+                        <span class="key">C</span>
+                        <span class="key">V</span>
+                    </div>
+                </div>
+            </div>
+            <div id="separator-keyboard" class="separator" style="display: none;"></div>
+            <div id="section-diag" class="section">
+                <dl class="diag">
+                    <dt>Engine</dt>
+                    <dd id="engine" class="tiny-button">-</dd>
+                    <dt>ROM</dt>
+                    <dd id="rom-name">-</dd>
+                    <dt>ROM Size</dt>
+                    <dd><span id="rom-size">-</span> bytes</dd>
+                    <dt>CPU Frequency</dt>
+                    <dd>
+                        <span id="logic-frequency-minus" class="tiny-button">-</span>
+                        <span id="logic-frequency">-</span> Hz
+                        <span id="logic-frequency-plus" class="tiny-button">+</span></dd>
+                    <dt>Framerate</dt>
+                    <dd><span id="fps-count">-</span> fps</dd>
+                </dl>
+            </div>
+            <div id="separator-diag" class="separator"></div>
+            <div class="section">
+                <div class="button-area">
+                    <span id="button-pause" class="tiny-button border padded">
+                        <img src="res/pause.svg" alt="pause" /><span>Pause</span>
+                    </span>
+                    <span id="button-reset" class="tiny-button border padded">
+                        <img src="res/reset.svg" alt="reset" /><span>Reset</span>
+                    </span>
+                    <span id="button-benchmark" class="tiny-button border padded">
+                        <img src="res/bolt.svg" alt="bolt" /><span>Benchmark</span>
+                    </span>
+                    <span id="button-fullscreen" class="tiny-button border padded">
+                        <img src="res/maximise.svg" alt="maximise" /><span>Fullscreen</span>
+                    </span>
+                    <span id="button-keyboard" class="tiny-button border padded">
+                        <img src="res/dialpad.svg" alt="info" /><span>Keyboard</span>
+                    </span>
+                    <span id="button-information" class="tiny-button border padded enabled">
+                        <img src="res/info.svg" alt="info" /><span>Information</span>
+                    </span>
+                    <span id="button-debug" class="tiny-button border padded">
+                        <img src="res/bug.svg" alt="bug" /><span>Debug</span>
+                    </span>
+                    <span id="button-theme" class="tiny-button border padded">
+                        <img src="res/marker.svg" alt="marker" /><span>Theme</span>
+                    </span>
+                    <span id="button-upload" class="tiny-button border padded file">
+                        <img src="res/upload.svg" alt="upload" /><span>Upload ROM</span>
+                        <input type="file" id="button-upload-file" name="button-upload-file" accept=".ch8">
+                    </span>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="toast-container">
+        <div id="toast" class="toast"></div>
+    </div>
+</body>
+<div id="modal-container" class="modal-container">
+    <div id="modal" class="modal">
+        <div class="modal-top-buttons">
+            <span id="modal-close" class="tiny-button rounded no-text">
+                <img class="medium" src="res/close.svg" alt="close" />
+            </span>
+        </div>
+        <h2 id="modal-title" class="modal-title"></h2>
+        <p id="modal-text" class="modal-text"></p>
+        <div class="modal-buttons">
+            <span id="modal-cancel" class="tiny-button red border padded-large">Cancel</span>
+            <span id="modal-confirm" class="tiny-button border padded-large">Confirm</span>
+        </div>
+    </div>
+</div>
+<div id="overlay" class="overlay">
+    <div class="overlay-container">
+        <div class="overlay-text">
+            Drag to load ROM <span id="rom-name"></span>
+        </div>
+        <div class="overlay-image">
+            <img src="res/sunglasses.png" alt="sunglasses" />
+        </div>
+    </div>
+</div>
+<div id="footer-background" class="footer-background"></div>
+<div id="footer" class="footer">
+    Built with ❤️ by <a href="https://joao.me" target="_blank">João Magalhães</a>
+</div>
+<script type="module" src="index.ts"></script>
+</body>
+
+</html>
diff --git a/examples/web/index.ts b/examples/web/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fda080e3d4e3ae6b6bf39bae74210b153e23f37c
--- /dev/null
+++ b/examples/web/index.ts
@@ -0,0 +1,897 @@
+import { default as wasm, GameBoy } from "./lib/boytacean.js";
+import info from "./package.json";
+
+const PIXEL_SET_COLOR = 0x50cb93ff;
+const PIXEL_UNSET_COLOR = 0x1b1a17ff;
+
+const LOGIC_HZ = 600;
+const VISUAL_HZ = 60;
+const TIMER_HZ = 60;
+const IDLE_HZ = 10;
+
+const FREQUENCY_DELTA = 60;
+
+const DISPLAY_WIDTH = 64;
+const DISPLAY_HEIGHT = 32;
+const DISPLAY_RATIO = DISPLAY_WIDTH / DISPLAY_HEIGHT;
+
+const SAMPLE_RATE = 2;
+
+const SOUND_DATA =
+    "data:audio/mpeg;base64,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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/7ksD/gAAAASwAAAAAAAAlgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+5LA/4AAAAEsAAAAAAAAJYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//uSwP+AAAABLAAAAAAAACWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/7ksD/gAAAASwAAAAAAAAlgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+5LA/4AAAAEsAAAAAAAAJYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//uSwP+AAAABLAAAAAAAACWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/7ksD/gAAAASwAAAAAAAAlgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAD/+5LA/4AAAAEsAAAAAAAAJYAAAAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAA";
+
+const BACKGROUNDS = [
+    "264653",
+    "1b1a17",
+    "023047",
+    "bc6c25",
+    "283618",
+    "2a9d8f",
+    "3a5a40"
+];
+
+const KEYS: Record<string, number> = {
+    "1": 0x01,
+    "2": 0x02,
+    "3": 0x03,
+    "4": 0x0c,
+    q: 0x04,
+    w: 0x05,
+    e: 0x06,
+    r: 0x0d,
+    a: 0x07,
+    s: 0x08,
+    d: 0x09,
+    f: 0x0e,
+    z: 0x0a,
+    x: 0x00,
+    c: 0x0b,
+    v: 0x0f
+};
+
+// @ts-ignore: ts(2580)
+const ROM_PATH = require("../../res/roms/pong.ch8");
+
+type State = {
+    gameBoy: GameBoy;
+    engine: string;
+    logicFrequency: number;
+    visualFrequency: number;
+    timerFrequency: number;
+    idleFrequency: number;
+    canvas: HTMLCanvasElement;
+    canvasScaled: HTMLCanvasElement;
+    canvasCtx: CanvasRenderingContext2D;
+    canvasScaledCtx: CanvasRenderingContext2D;
+    image: ImageData;
+    videoBuff: DataView;
+    toastTimeout: number;
+    paused: boolean;
+    background_index: number;
+    nextTickTime: number;
+    fps: number;
+    frameStart: number;
+    frameCount: number;
+    romName: string;
+    romData: Uint8Array;
+    romSize: number;
+};
+
+type Global = {
+    modalCallback: Function;
+};
+
+const state: State = {
+    gameBoy: null,
+    engine: null,
+    logicFrequency: LOGIC_HZ,
+    visualFrequency: VISUAL_HZ,
+    timerFrequency: TIMER_HZ,
+    idleFrequency: IDLE_HZ,
+    canvas: null,
+    canvasScaled: null,
+    canvasCtx: null,
+    canvasScaledCtx: null,
+    image: null,
+    videoBuff: null,
+    toastTimeout: null,
+    paused: false,
+    background_index: 0,
+    nextTickTime: 0,
+    fps: 0,
+    frameStart: new Date().getTime(),
+    frameCount: 0,
+    romName: null,
+    romData: null,
+    romSize: 0
+};
+
+const global: Global = {
+    modalCallback: null
+};
+
+const sound = ((data = SOUND_DATA, volume = 0.2) => {
+    const sound = new Audio(data);
+    sound.volume = volume;
+    sound.muted = true;
+    return sound;
+})();
+
+const main = async () => {
+    // initializes the WASM module, this is required
+    // so that the global symbols become available
+    await wasm();
+
+    // initializes the complete set of sub-systems
+    // and registers the event handlers
+    await init();
+    await register();
+
+    // start the emulator subsystem with the initial
+    // ROM retrieved from a remote data source
+    await start({ loadRom: true });
+
+    // runs the sequence as an infinite loop, running
+    // the associated CPU cycles accordingly
+    while (true) {
+        // in case the machin is paused we must delay the execution
+        // a little bit until the paused state is recovered
+        if (state.paused) {
+            await new Promise((resolve) => {
+                setTimeout(resolve, 1000 / state.idleFrequency);
+            });
+            continue;
+        }
+
+        // obtains the current time, this value is going
+        // to be used to compute the need for tick computation
+        let currentTime = new Date().getTime();
+
+        try {
+            tick(currentTime);
+        } catch (err) {
+            // sets the default error message to be displayed
+            // to the user
+            let message = String(err);
+
+            // verifies if the current issue is a panic one
+            // and updates the message value if that's the case
+            const messageNormalized = (err as Error).message.toLowerCase();
+            const isPanic =
+                messageNormalized.startsWith("unreachable") ||
+                messageNormalized.startsWith("recursive use of an object");
+            if (isPanic) {
+                message = "Unrecoverable error, restarting Game Boy";
+            }
+
+            // displays the error information to both the end-user
+            // and the developer (for dianostics)
+            showToast(message, true, 5000);
+            console.error(err);
+
+            // pauses the machine, allowing the end-user to act
+            // on the error in a proper fashion
+            pause();
+
+            // if we're talking about a panic proper action must be taken
+            // which in this case it means restarting both the WASM sub
+            // system and the machine state (to be able to recover)
+            // also sets the default color on screen to indicate the issue
+            if (isPanic) {
+                await clearCanvas(undefined, {
+                    // @ts-ignore: ts(2580)
+                    image: require("./res/storm.png"),
+                    imageScale: 0.4
+                });
+
+                await wasm();
+                await start({ restore: false });
+            }
+        }
+
+        // calculates the amount of time until the next draw operation
+        // this is the amount of time that is going to be pending
+        currentTime = new Date().getTime();
+        const pendingTime = Math.max(state.nextTickTime - currentTime, 0);
+
+        // waits a little bit for the next frame to be draw,
+        // this should control the flow of render
+        await new Promise((resolve) => {
+            setTimeout(resolve, pendingTime);
+        });
+    }
+};
+
+const tick = (currentTime: number) => {
+    // in case the time to draw the next frame has not been
+    // reached the flush of the "tick" logic is skiped
+    if (currentTime < state.nextTickTime) return;
+
+    // initializes the flag that is going to control is a beep
+    // is going to be issued
+    let beepFlag = false;
+
+    // calculates the number of ticks that have elapsed since the
+    // last draw operation, this is critical to be able to properly
+    // operate the clock of the CPU in frame drop situations
+    if (state.nextTickTime === 0) state.nextTickTime = currentTime;
+    let ticks = Math.ceil(
+        (currentTime - state.nextTickTime) /
+            ((1 / state.visualFrequency) * 1000)
+    );
+    ticks = Math.max(ticks, 1);
+
+    const ratioLogic = (state.logicFrequency / state.visualFrequency) * ticks;
+    for (let i = 0; i < ratioLogic; i++) {
+        state.gameBoy.clock();
+    }
+
+    // in case the beep flag is active issue a sound during a bried
+    // period, to notify the user about a certain event
+    if (beepFlag) beep();
+
+    // updates the canvas object with the new
+    // visual information coming in
+    updateCanvas(state.gameBoy.frame_buffer_eager());
+
+    // increments the number of frames rendered in the current
+    // section, this value is going to be used to calculate FPS
+    state.frameCount += 1;
+
+    // in case the target number of frames for FPS control
+    // has been reached calculates the number of FPS and
+    // flushes the value to the screen
+    if (state.frameCount === state.visualFrequency * SAMPLE_RATE) {
+        const currentTime = new Date().getTime();
+        const deltaTime = (currentTime - state.frameStart) / 1000;
+        const fps = Math.round(state.frameCount / deltaTime);
+        setFps(fps);
+        state.frameCount = 0;
+        state.frameStart = currentTime;
+    }
+
+    // updates the next update time reference to the, so that it
+    // can be used to control the game loop
+    state.nextTickTime += (1000 / state.visualFrequency) * ticks;
+};
+
+const start = async ({
+    engine = "neo",
+    restore = true,
+    loadRom = false,
+    romPath = ROM_PATH,
+    romName = null as string,
+    romData = null as Uint8Array
+} = {}) => {
+    // in case a remote ROM loading operation has been
+    // requested then loads it from the remote origin
+    if (loadRom) {
+        [romName, romData] = await fetchRom(romPath);
+    } else if (romName === null || romData === null) {
+        [romName, romData] = [state.romName, state.romData];
+    }
+
+    // selects the proper engine for execution
+    // and builds a new instance of it
+    switch (engine) {
+        case "neo":
+            state.gameBoy = new GameBoy();
+            break;
+
+        default:
+            if (!state.gameBoy) {
+                throw new Error("No engine requested");
+            }
+            break;
+    }
+
+    // resets the Game Boy engine to restore it into
+    // a valid state ready to be used
+    //state.gameBoy.reset_hard(); @todo
+    state.gameBoy.load_boot_default();
+    state.gameBoy.load_rom(romData);
+
+    // updates the name of the currently selected engine
+    // to the one that has been provided (logic change)
+    if (engine) state.engine = engine;
+
+    // updates the complete set of global information that
+    // is going to be displayed
+    setEngine(state.engine);
+    setRom(romName, romData);
+    setLogicFrequency(state.logicFrequency);
+    setFps(state.fps);
+
+    // in case the restore (state) flag is set
+    // then resumes the machine execution
+    if (restore) resume();
+};
+
+const register = async () => {
+    await Promise.all([
+        registerDrop(),
+        registerKeys(),
+        registerButtons(),
+        registerKeyboard(),
+        registerCanvas(),
+        registerToast(),
+        registerModal()
+    ]);
+};
+
+const init = async () => {
+    await Promise.all([initBase(), initCanvas()]);
+};
+
+const registerDrop = () => {
+    document.addEventListener("drop", async (event) => {
+        if (
+            !event.dataTransfer.files ||
+            event.dataTransfer.files.length === 0
+        ) {
+            return;
+        }
+
+        event.preventDefault();
+        event.stopPropagation();
+
+        const overlay = document.getElementById("overlay");
+        overlay.classList.remove("visible");
+
+        const file = event.dataTransfer.files[0];
+
+        if (!file.name.endsWith(".gb")) {
+            showToast("This is probably not a Game Boy ROM file!", true);
+            return;
+        }
+
+        const arrayBuffer = await file.arrayBuffer();
+        const romData = new Uint8Array(arrayBuffer);
+
+        start({ engine: null, romName: file.name, romData: romData });
+
+        showToast(`Loaded ${file.name} ROM successfully!`);
+    });
+    document.addEventListener("dragover", async (event) => {
+        if (!event.dataTransfer.items || event.dataTransfer.items[0].type)
+            return;
+
+        event.preventDefault();
+
+        const overlay = document.getElementById("overlay");
+        overlay.classList.add("visible");
+    });
+    document.addEventListener("dragenter", async (event) => {
+        if (!event.dataTransfer.items || event.dataTransfer.items[0].type)
+            return;
+        const overlay = document.getElementById("overlay");
+        overlay.classList.add("visible");
+    });
+    document.addEventListener("dragleave", async (event) => {
+        if (!event.dataTransfer.items || event.dataTransfer.items[0].type)
+            return;
+        const overlay = document.getElementById("overlay");
+        overlay.classList.remove("visible");
+    });
+};
+
+const registerKeys = () => {
+    document.addEventListener("keydown", (event) => {
+        const keyCode = KEYS[event.key];
+        if (keyCode !== undefined) {
+            //state.gameBoy.key_press_ws(keyCode); @todo
+            return;
+        }
+
+        switch (event.key) {
+            case "+":
+                setLogicFrequency(state.logicFrequency + FREQUENCY_DELTA);
+                break;
+
+            case "-":
+                setLogicFrequency(state.logicFrequency - FREQUENCY_DELTA);
+                break;
+
+            case "Escape":
+                minimize();
+                break;
+        }
+    });
+
+    document.addEventListener("keyup", (event) => {
+        const keyCode = KEYS[event.key];
+        if (keyCode !== undefined) {
+            //state.gameBoy.key_lift_ws(keyCode); @todo
+            return;
+        }
+    });
+};
+
+const registerButtons = () => {
+    const engine = document.getElementById("engine");
+    engine.addEventListener("click", () => {
+        const name = state.engine == "neo" ? "classic" : "neo";
+        start({ engine: name });
+        showToast(
+            `Game Boy running in engine "${name.toUpperCase()}" from now on!`
+        );
+    });
+
+    const logicFrequencyPlus = document.getElementById("logic-frequency-plus");
+    logicFrequencyPlus.addEventListener("click", () => {
+        setLogicFrequency(state.logicFrequency + FREQUENCY_DELTA);
+    });
+
+    const logicFrequencyMinus = document.getElementById(
+        "logic-frequency-minus"
+    );
+    logicFrequencyMinus.addEventListener("click", () => {
+        setLogicFrequency(state.logicFrequency - FREQUENCY_DELTA);
+    });
+
+    const buttonPause = document.getElementById("button-pause");
+    buttonPause.addEventListener("click", () => {
+        toggleRunning();
+    });
+
+    const buttonReset = document.getElementById("button-reset");
+    buttonReset.addEventListener("click", () => {
+        reset();
+    });
+
+    const buttonBenchmark = document.getElementById("button-benchmark");
+    buttonBenchmark.addEventListener("click", async () => {
+        const result = await showModal(
+            "Are you sure you want to start a benchmark?\nThe benchmark is considered an expensive operation!",
+            "Confirm"
+        );
+        if (!result) return;
+        buttonBenchmark.classList.add("enabled");
+        pause();
+        try {
+            const initial = Date.now();
+            const count = 500000000;
+            for (let i = 0; i < count; i++) {
+                state.gameBoy.clock();
+            }
+            const delta = (Date.now() - initial) / 1000;
+            const frequency_mhz = count / delta / 1000 / 1000;
+            showToast(
+                `Took ${delta.toFixed(
+                    2
+                )} seconds to run ${count} ticks (${frequency_mhz.toFixed(
+                    2
+                )} Mhz)!`,
+                undefined,
+                7500
+            );
+        } finally {
+            resume();
+            buttonBenchmark.classList.remove("enabled");
+        }
+    });
+
+    const buttonFullscreen = document.getElementById("button-fullscreen");
+    buttonFullscreen.addEventListener("click", () => {
+        maximize();
+    });
+
+    const buttonKeyboard = document.getElementById("button-keyboard");
+    buttonKeyboard.addEventListener("click", () => {
+        const sectionKeyboard = document.getElementById("section-keyboard");
+        const separatorKeyboard = document.getElementById("separator-keyboard");
+        const sectionNarrative = document.getElementById("section-narrative");
+        const separatorNarrative = document.getElementById(
+            "separator-narrative"
+        );
+        if (buttonKeyboard.classList.contains("enabled")) {
+            sectionKeyboard.style.display = "none";
+            separatorKeyboard.style.display = "none";
+            sectionNarrative.style.display = "block";
+            separatorNarrative.style.display = "block";
+            buttonKeyboard.classList.remove("enabled");
+        } else {
+            sectionKeyboard.style.display = "block";
+            separatorKeyboard.style.display = "block";
+            sectionNarrative.style.display = "none";
+            separatorNarrative.style.display = "none";
+            buttonKeyboard.classList.add("enabled");
+        }
+    });
+
+    const buttonInformation = document.getElementById("button-information");
+    buttonInformation.addEventListener("click", () => {
+        const sectionDiag = document.getElementById("section-diag");
+        const separatorDiag = document.getElementById("separator-diag");
+        if (buttonInformation.classList.contains("enabled")) {
+            sectionDiag.style.display = "none";
+            separatorDiag.style.display = "none";
+            buttonInformation.classList.remove("enabled");
+        } else {
+            sectionDiag.style.display = "block";
+            separatorDiag.style.display = "block";
+            buttonInformation.classList.add("enabled");
+        }
+    });
+
+    const buttonTheme = document.getElementById("button-theme");
+    buttonTheme.addEventListener("click", () => {
+        state.background_index =
+            (state.background_index + 1) % BACKGROUNDS.length;
+        const background = BACKGROUNDS[state.background_index];
+        setBackground(background);
+    });
+
+    const buttonUploadFile = document.getElementById(
+        "button-upload-file"
+    ) as HTMLInputElement;
+    buttonUploadFile.addEventListener("change", async () => {
+        if (!buttonUploadFile.files || buttonUploadFile.files.length === 0) {
+            return;
+        }
+
+        const file = buttonUploadFile.files[0];
+
+        const arrayBuffer = await file.arrayBuffer();
+        const romData = new Uint8Array(arrayBuffer);
+
+        buttonUploadFile.value = "";
+
+        start({ engine: null, romName: file.name, romData: romData });
+
+        showToast(`Loaded ${file.name} ROM successfully!`);
+    });
+};
+
+const registerKeyboard = () => {
+    const keyboard = document.getElementById("keyboard");
+    const keys = keyboard.getElementsByClassName("key");
+
+    keyboard.addEventListener("touchstart", function (event) {
+        event.preventDefault();
+        event.stopPropagation();
+    });
+
+    keyboard.addEventListener("touchend", function (event) {
+        event.preventDefault();
+        event.stopPropagation();
+    });
+
+    Array.prototype.forEach.call(keys, (k: Element) => {
+        k.addEventListener("mousedown", function (event) {
+            const keyCode = KEYS[this.textContent.toLowerCase()];
+            //state.gameBoy.key_press_ws(keyCode); @todo
+            event.preventDefault();
+            event.stopPropagation();
+        });
+
+        k.addEventListener("touchstart", function (event) {
+            const keyCode = KEYS[this.textContent.toLowerCase()];
+            //state.gameBoy.key_press_ws(keyCode); @todo
+            event.preventDefault();
+            event.stopPropagation();
+        });
+
+        k.addEventListener("mouseup", function (event) {
+            const keyCode = KEYS[this.textContent.toLowerCase()];
+            //state.gameBoy.key_lift_ws(keyCode); @todo
+            event.preventDefault();
+            event.stopPropagation();
+        });
+
+        k.addEventListener("touchend", function (event) {
+            const keyCode = KEYS[this.textContent.toLowerCase()];
+            //state.gameBoy.key_lift_ws(keyCode); @todo
+            event.preventDefault();
+            event.stopPropagation();
+        });
+    });
+};
+
+const registerCanvas = () => {
+    const canvasClose = document.getElementById("canvas-close");
+    canvasClose.addEventListener("click", () => {
+        minimize();
+    });
+};
+
+const registerToast = () => {
+    const toast = document.getElementById("toast");
+    toast.addEventListener("click", () => {
+        toast.classList.remove("visible");
+    });
+};
+
+const registerModal = () => {
+    const modalClose = document.getElementById("modal-close");
+    modalClose.addEventListener("click", () => {
+        hideModal(false);
+    });
+
+    const modalCancel = document.getElementById("modal-cancel");
+    modalCancel.addEventListener("click", () => {
+        hideModal(false);
+    });
+
+    const modalConfirm = document.getElementById("modal-confirm");
+    modalConfirm.addEventListener("click", () => {
+        hideModal(true);
+    });
+
+    document.addEventListener("keydown", (event) => {
+        if (event.key === "Escape") {
+            hideModal(false);
+        }
+    });
+};
+
+const initBase = async () => {
+    const background = BACKGROUNDS[state.background_index];
+    setBackground(background);
+    setVersion(info.version);
+};
+
+const initCanvas = async () => {
+    // initializes the off-screen canvas that is going to be
+    // used in the drawing process
+    state.canvas = document.createElement("canvas");
+    state.canvas.width = DISPLAY_WIDTH;
+    state.canvas.height = DISPLAY_HEIGHT;
+    state.canvasCtx = state.canvas.getContext("2d");
+
+    state.canvasScaled = document.getElementById(
+        "chip-canvas"
+    ) as HTMLCanvasElement;
+    state.canvasScaled.width =
+        state.canvasScaled.width * window.devicePixelRatio;
+    state.canvasScaled.height =
+        state.canvasScaled.height * window.devicePixelRatio;
+    state.canvasScaledCtx = state.canvasScaled.getContext("2d");
+
+    state.canvasScaledCtx.scale(
+        state.canvasScaled.width / state.canvas.width,
+        state.canvasScaled.height / state.canvas.height
+    );
+    state.canvasScaledCtx.imageSmoothingEnabled = false;
+
+    state.image = state.canvasCtx.createImageData(
+        state.canvas.width,
+        state.canvas.height
+    );
+    state.videoBuff = new DataView(state.image.data.buffer);
+};
+
+const updateCanvas = (pixels: Uint8Array) => {
+    for (let i = 0; i < pixels.length; i++) {
+        state.videoBuff.setUint32(
+            i * 4,
+            pixels[i] ? PIXEL_SET_COLOR : PIXEL_UNSET_COLOR
+        ); //@todo must take into consideration that these are RGB pixels
+    }
+    state.canvasCtx.putImageData(state.image, 0, 0);
+    state.canvasScaledCtx.drawImage(state.canvas, 0, 0);
+};
+
+const clearCanvas = async (
+    color = PIXEL_UNSET_COLOR,
+    { image = null as string, imageScale = 1 } = {}
+) => {
+    state.canvasScaledCtx.fillStyle = `#${color.toString(16).toUpperCase()}`;
+    state.canvasScaledCtx.fillRect(
+        0,
+        0,
+        state.canvasScaled.width,
+        state.canvasScaled.height
+    );
+
+    // in case an image was requested then uses that to load
+    // an image at the center of the screen
+    if (image) {
+        const img = await new Promise<HTMLImageElement>((resolve) => {
+            const img = new Image();
+            img.onload = () => {
+                resolve(img);
+            };
+            img.src = image;
+        });
+        const [imgWidth, imgHeight] = [
+            img.width * imageScale,
+            img.height * imageScale
+        ];
+        const [x0, y0] = [
+            state.canvasScaled.width / 2 - imgWidth / 2,
+            state.canvasScaled.height / 2 - imgHeight / 2
+        ];
+        state.canvasScaledCtx.setTransform(1, 0, 0, 1, 0, 0);
+        try {
+            state.canvasScaledCtx.drawImage(img, x0, y0, imgWidth, imgHeight);
+        } finally {
+            state.canvasScaledCtx.scale(
+                state.canvasScaled.width / state.canvas.width,
+                state.canvasScaled.height / state.canvas.height
+            );
+        }
+    }
+};
+
+const showToast = async (message: string, error = false, timeout = 3500) => {
+    const toast = document.getElementById("toast");
+    toast.classList.remove("error");
+    if (error) toast.classList.add("error");
+    toast.classList.add("visible");
+    toast.textContent = message;
+    if (state.toastTimeout) clearTimeout(state.toastTimeout);
+    state.toastTimeout = setTimeout(() => {
+        toast.classList.remove("visible");
+        state.toastTimeout = null;
+    }, timeout);
+};
+
+const showModal = async (
+    message: string,
+    title = "Alert"
+): Promise<boolean> => {
+    const modalContainer = document.getElementById("modal-container");
+    const modalTitle = document.getElementById("modal-title");
+    const modalText = document.getElementById("modal-text");
+    modalContainer.classList.add("visible");
+    modalTitle.textContent = title;
+    modalText.innerHTML = message.replace(/\n/g, "<br/>");
+    const result = (await new Promise((resolve) => {
+        global.modalCallback = resolve;
+    })) as boolean;
+    return result;
+};
+
+const hideModal = async (result = true) => {
+    const modalContainer = document.getElementById("modal-container");
+    modalContainer.classList.remove("visible");
+    if (global.modalCallback) global.modalCallback(result);
+    global.modalCallback = null;
+};
+
+const setVersion = (value: string) => {
+    document.getElementById("version").textContent = value;
+};
+
+const setEngine = (name: string, upper = true) => {
+    name = upper ? name.toUpperCase() : name;
+    document.getElementById("engine").textContent = name;
+};
+
+const setRom = (name: string, data: Uint8Array) => {
+    state.romName = name;
+    state.romData = data;
+    state.romSize = data.length;
+    document.getElementById("rom-name").textContent = name;
+    document.getElementById("rom-size").textContent = String(data.length);
+};
+
+const setLogicFrequency = (value: number) => {
+    if (value < 0) showToast("Invalid frequency value!", true);
+    value = Math.max(value, 0);
+    state.logicFrequency = value;
+    document.getElementById("logic-frequency").textContent = String(value);
+};
+
+const setFps = (value: number) => {
+    if (value < 0) showToast("Invalid FPS value!", true);
+    value = Math.max(value, 0);
+    state.fps = value;
+    document.getElementById("fps-count").textContent = String(value);
+};
+
+const setBackground = (value: string) => {
+    document.body.style.backgroundColor = `#${value}`;
+    document.getElementById(
+        "footer-background"
+    ).style.backgroundColor = `#${value}f2`;
+};
+
+const toggleRunning = () => {
+    if (state.paused) {
+        resume();
+    } else {
+        pause();
+    }
+};
+
+const pause = () => {
+    state.paused = true;
+    const buttonPause = document.getElementById("button-pause");
+    const img = buttonPause.getElementsByTagName("img")[0];
+    const span = buttonPause.getElementsByTagName("span")[0];
+    buttonPause.classList.add("enabled");
+    // @ts-ignore: ts(2580)
+    img.src = require("./res/play.svg");
+    span.textContent = "Resume";
+};
+
+const resume = () => {
+    state.paused = false;
+    state.nextTickTime = new Date().getTime();
+    const buttonPause = document.getElementById("button-pause");
+    const img = buttonPause.getElementsByTagName("img")[0];
+    const span = buttonPause.getElementsByTagName("span")[0];
+    buttonPause.classList.remove("enabled");
+    // @ts-ignore: ts(2580)
+    img.src = require("./res/pause.svg");
+    span.textContent = "Pause";
+};
+
+const toggleWindow = () => {
+    maximize();
+};
+
+const maximize = () => {
+    const canvasContainer = document.getElementById("canvas-container");
+    canvasContainer.classList.add("fullscreen");
+
+    window.addEventListener("resize", crop);
+
+    crop();
+};
+
+const minimize = () => {
+    const canvasContainer = document.getElementById("canvas-container");
+    const chipCanvas = document.getElementById("chip-canvas");
+    canvasContainer.classList.remove("fullscreen");
+    chipCanvas.style.width = null;
+    chipCanvas.style.height = null;
+    window.removeEventListener("resize", crop);
+};
+
+const crop = () => {
+    const chipCanvas = document.getElementById("chip-canvas");
+
+    // calculates the window ratio as this is fundamental to
+    // determine the proper way to crop the fulscreen
+    const windowRatio = window.innerWidth / window.innerHeight;
+
+    // in case the window is wider (more horizontal than the base ratio)
+    // this means that we must crop horizontaly
+    if (windowRatio > DISPLAY_RATIO) {
+        chipCanvas.style.width = `${
+            window.innerWidth * (DISPLAY_RATIO / windowRatio)
+        }px`;
+        chipCanvas.style.height = `${window.innerHeight}px`;
+    } else {
+        chipCanvas.style.width = `${window.innerWidth}px`;
+        chipCanvas.style.height = `${
+            window.innerHeight * (windowRatio / DISPLAY_RATIO)
+        }px`;
+    }
+};
+
+const reset = () => {
+    start({ engine: null });
+};
+
+const fetchRom = async (romPath: string): Promise<[string, Uint8Array]> => {
+    // extracts the name of the ROM from the provided
+    // path by splitting its structure
+    const romPathS = romPath.split(/\//g);
+    let romName = romPathS[romPathS.length - 1].split("?")[0];
+    const romNameS = romName.split(/\./g);
+    romName = `${romNameS[0]}.${romNameS[romNameS.length - 1]}`;
+
+    // loads the ROM data and converts it into the
+    // target byte array buffer (to be used by WASM)
+    const response = await fetch(ROM_PATH);
+    const blob = await response.blob();
+    const arrayBuffer = await blob.arrayBuffer();
+    const romData = new Uint8Array(arrayBuffer);
+
+    // returns both the name of the ROM and the data
+    // contents as a byte array
+    return [romName, romData];
+};
+
+const beep = async () => {
+    sound.muted = false;
+    await sound.play();
+};
+
+(async () => {
+    await main();
+})();
diff --git a/examples/web/package.json b/examples/web/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..6130b967919cbfd162bb51f0a20e95f588e63216
--- /dev/null
+++ b/examples/web/package.json
@@ -0,0 +1,24 @@
+{
+    "name": "boytacean-web",
+    "version": "0.1.0",
+    "description": "The web version of Boytacean",
+    "repository": {
+        "type": "git",
+        "url": "git+https://gitlab.stage.hive.pt/joamag/boytacean.git"
+    },
+    "license": "Apache-2.0",
+    "scripts": {
+        "build": "parcel build index.html",
+        "dev": "parcel index.html",
+        "pretty": "prettier --config .prettierrc \"./**/*.{ts,json}\" --write",
+        "start": "npm run build",
+        "watch": "parcel watch index.html"
+    },
+    "source": "index.ts",
+    "devDependencies": {
+        "@parcel/transformer-typescript-tsc": "^2.6.1",
+        "parcel": "^2.6.1",
+        "prettier": "^2.7.1",
+        "typescript": "^4.5.5"
+    }
+}
diff --git a/examples/web/tsconfig.json b/examples/web/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..0bc8cec0b5a82f53019127e9adb2821060aaa7dd
--- /dev/null
+++ b/examples/web/tsconfig.json
@@ -0,0 +1,19 @@
+{
+    "compilerOptions": {
+        "module": "es2015",
+        "moduleResolution": "node",
+        "resolveJsonModule": true,
+        "allowSyntheticDefaultImports": true,
+        "target": "es6",
+        "noImplicitAny": true,
+        "sourceMap": true,
+        "outDir": ".",
+        "baseUrl": ".",
+        "lib": ["es2015", "dom"],
+        "paths": {
+            "*": ["node_modules/*", "src/types/*"]
+        }
+    },
+    "include": ["**/*"],
+    "exclude": []
+}
diff --git a/src/gb.rs b/src/gb.rs
index e918ba68414c40cf8a7aba37695b63136b5a7f81..59bd189810236babc92ccc7d25ee20f8355671ef 100644
--- a/src/gb.rs
+++ b/src/gb.rs
@@ -37,18 +37,30 @@ impl GameBoy {
         self.ppu().clock(cycles)
     }
 
-    pub fn load_rom(&mut self, path: &str) {
+    pub fn load_rom(&mut self, data: &[u8]) {
+        self.cpu.mmu().write_rom(0x0000, data);
+    }
+
+    pub fn load_rom_file(&mut self, path: &str) {
         let data = read_file(path);
-        self.cpu.mmu().write_rom(0x0000, &data);
+        self.load_rom(&data);
+    }
+
+    pub fn load_boot(&mut self, data: &[u8]) {
+        self.cpu.mmu().write_boot(0x0000, data);
     }
 
-    pub fn load_boot(&mut self, path: &str) {
+    pub fn load_boot_file(&mut self, path: &str) {
         let data = read_file(path);
-        self.cpu.mmu().write_boot(0x0000, &data);
+        self.load_boot(&data);
     }
 
     pub fn load_boot_default(&mut self) {
-        self.load_boot("./res/dmg_rom.bin");
+        self.load_boot_file("./res/dmg_rom.bin");
+    }
+
+    pub fn frame_buffer_eager(&mut self) -> Vec<u8> {
+        self.frame_buffer().to_vec()
     }
 }