diff --git a/.gitignore b/.gitignore
index 4a77473d4e2bfae5eb94e3479d68b23fb65466ec..be3d44f6d5fe02e8517f34a02305161496d55de9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,5 +5,5 @@ Cargo.lock
 /.idea
 
 /target
-/res/roms
+/res/roms.prop
 /examples/*/target
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..61d9dd401ebff7dd541326508d9f257699fdb64b
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,112 @@
+image: hivesolutions/ubuntu_dev
+
+variables:
+  NETLIFY_SITE_ID: boytacean
+  NETLIFY_AUTH_TOKEN: $NETLIFY_AUTH_TOKEN
+  CLOUDFLARE_API_TOKEN: $CLOUDFLARE_API_TOKEN
+  CRATES_TOKEN: $CRATES_TOKEN
+  NPM_TOKEN: $NPM_TOKEN
+
+stages:
+  - build
+  - deploy
+
+before_script:
+  - apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y -q pkg-config
+  - curl -sf -L https://static.rust-lang.org/rustup.sh | sh -s -- -y
+  - export PATH=$PATH:$HOME/.cargo/bin
+  - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
+  - export NVM_DIR="$HOME/.nvm"
+  - \[ -s "$NVM_DIR/nvm.sh" \] && \. "$NVM_DIR/nvm.sh"
+  - \[ -s "$NVM_DIR/bash_completion" \] && \. "$NVM_DIR/bash_completion"
+  - nvm install stable
+
+build-rust:
+  stage: build
+  parallel:
+    matrix:
+      - RUST_VERSION: ["1.56.1", "1.60.0", "stable", "nightly"]
+  script:
+    - rustup toolchain install $RUST_VERSION
+    - rustup override set $RUST_VERSION
+    - rustc --version
+    - cargo build
+    - cargo build --release
+
+build-wasm:
+  stage: build
+  parallel:
+    matrix:
+      - RUST_VERSION: ["1.60.0"]
+  script:
+    - rustup toolchain install $RUST_VERSION
+    - rustup override set $RUST_VERSION
+    - rustc --version
+    - cargo install wasm-pack
+    - wasm-pack build --release --target=web --out-dir=examples/web/lib -- --features wasm
+    - cd examples/web && npm install && npm run build
+  artifacts:
+    paths:
+      - examples/web/dist
+      - examples/web/lib
+    expire_in: 1 day
+
+deploy-netlify-preview:
+  stage: deploy
+  script:
+    - cd examples/web/dist
+    - npm_config_yes=true npx --package=netlify-cli netlify deploy --dir=.
+  dependencies:
+    - build-wasm
+  only:
+    - master
+
+deploy-netlify-prod:
+  stage: deploy
+  script:
+    - cd examples/web/dist
+    - npm_config_yes=true npx --package=netlify-cli netlify deploy --dir=. --prod
+  dependencies:
+    - build-wasm
+  only:
+    - tags
+
+deploy-cloudfare-preview:
+  stage: deploy
+  script:
+    - cd examples/web/dist
+    - npm_config_yes=true npx wrangler pages publish . --project-name=boytacean --branch master
+  dependencies:
+    - build-wasm
+  only:
+    - master
+
+deploy-cloudfare-prod:
+  stage: deploy
+  script:
+    - cd examples/web/dist
+    - npm_config_yes=true npx wrangler pages publish . --project-name=boytacean --branch stable
+  dependencies:
+    - build-wasm
+  only:
+    - tags
+
+deploy-crates:
+  stage: deploy
+  script:
+    - cargo login $CRATES_TOKEN
+    - cargo publish --no-verify
+  dependencies:
+    - build-rust
+  only:
+    - tags
+
+deploy-npm:
+  stage: deploy
+  script:
+    - echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
+    - cd examples/web/lib && npm publish
+  dependencies:
+    - build-wasm
+  only:
+    - tags
diff --git a/examples/sdl/src/main.rs b/examples/sdl/src/main.rs
index 73969689f8a5d8e9089bf14fd57cdf4c02ef6853..50af56f892bf24ea07c23a6463d9d155910c058e 100644
--- a/examples/sdl/src/main.rs
+++ b/examples/sdl/src/main.rs
@@ -79,9 +79,9 @@ fn main() {
         .unwrap();
 
     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_boot_static();
+    game_boy.load_rom_file("../../res/roms/firstwhite.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..1017d972ae95a264db5e230a4dc04e5cee3fc6ed
--- /dev/null
+++ b/examples/web/.parcelrc
@@ -0,0 +1,7 @@
+{
+    "extends": "@parcel/config-default",
+    "transformers": {
+        "*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"],
+        "*.gb": ["@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..decfc4d4079424e0ee6838a3700cf630f7e1fce7
--- /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", "Roboto", "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: 320px;
+    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..faa20ad4ca8c3c2eafbcfd30e29afb08fb62382b
--- /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="320" height="288"></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..68cd9aaa81b17b2aca0436a3fd2d004a8dc778ef
--- /dev/null
+++ b/examples/web/index.ts
@@ -0,0 +1,896 @@
+import { default as wasm, GameBoy } from "./lib/boytacean.js";
+import info from "./package.json";
+
+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 = 160;
+const DISPLAY_HEIGHT = 144;
+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/firstwhite.gb");
+
+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.2
+                });
+
+                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;
+
+    // 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);
+
+    let counterTicks = 0;
+
+    while (true) {
+        // limits the number of ticks to the typical number
+        // of ticks required to do a complete PPU draw
+        if (counterTicks >= 70224) {
+            break;
+        }
+
+        // runs the Game Boy clock, this operations should
+        // include the advance of both the CPU and the PPU
+        counterTicks += state.gameBoy.clock();
+    }
+
+    // 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_static();
+    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) => {
+    let offset = 0;
+    for (let index = 0; index < pixels.length; index += 3) {
+        const color =
+            (pixels[index] << 24) |
+            (pixels[index + 1] << 16) |
+            (pixels[index + 2] << 8) |
+            0xff;
+        state.videoBuff.setUint32(offset, color);
+        offset += 4;
+    }
+    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 * window.devicePixelRatio,
+            img.height * imageScale * window.devicePixelRatio
+        ];
+        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];
+};
+
+(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/res/bike.svg b/examples/web/res/bike.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ee11c9aef5bf3aec5fa590cfdedad74ed781ab68
--- /dev/null
+++ b/examples/web/res/bike.svg
@@ -0,0 +1 @@
+<svg width="48px" height="48px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-labelledby="bikeIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" color="#ffffff"> <title id="bikeIconTitle">Bike</title> <circle cx="14" cy="6" r="1"/> <path d="M12 18V14L9 12L12 9L14 11L16 12"/> <circle cx="6" cy="17" r="3"/> <circle cx="18" cy="17" r="3"/> </svg>
\ No newline at end of file
diff --git a/examples/web/res/bolt.svg b/examples/web/res/bolt.svg
new file mode 100644
index 0000000000000000000000000000000000000000..9bbaca340c017181bb2542e13eccf234c9937ae4
--- /dev/null
+++ b/examples/web/res/bolt.svg
@@ -0,0 +1 @@
+<svg role="img" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" viewBox="0 0 24 24" aria-labelledby="boltIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="boltIconTitle">Bolt</title> <path d="M5 14l8-11v7h5l-8 11v-7z"/> </svg>
\ No newline at end of file
diff --git a/examples/web/res/bug.svg b/examples/web/res/bug.svg
new file mode 100644
index 0000000000000000000000000000000000000000..db1713a9528be8cf8ae1e94d92ebf55c80d53f4e
--- /dev/null
+++ b/examples/web/res/bug.svg
@@ -0,0 +1 @@
+<svg role="img" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" viewBox="0 0 24 24" aria-labelledby="bugIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="bugIconTitle">Bug</title> <path d="M15 6.99989086C16.1045695 6.99989086 17 7.89532136 17 8.99989086L17 16.458686C17 17.1113133 16.6815784 17.722892 16.1469254 18.0971494L12 21 7.85307456 18.0971494C7.31842164 17.722892 7 17.1113133 7 16.458686L7 8.99989086C7 7.89532136 7.8954305 6.99989086 9 6.99989086 9.00005899 5.34308677 10.3431821 4 12 4 13.6568179 4 14.999941 5.34308677 15 6.99989086zM4 13L7 13"/> <polyline points="3 7 5 9 7 9"/> <polyline points="21 7 19 9 17 9"/> <polyline points="3 19 5 17 7 17"/> <polyline points="17 17 19 17 21 19 21 19"/> <path d="M17,13 L20,13"/> </svg>
\ No newline at end of file
diff --git a/examples/web/res/close.svg b/examples/web/res/close.svg
new file mode 100644
index 0000000000000000000000000000000000000000..aeac9982d889f0194b3b55905d166541704b5bac
--- /dev/null
+++ b/examples/web/res/close.svg
@@ -0,0 +1 @@
+<svg role="img" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" viewBox="0 0 24 24" aria-labelledby="closeIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="closeIconTitle">Close</title> <path d="M6.34314575 6.34314575L17.6568542 17.6568542M6.34314575 17.6568542L17.6568542 6.34314575"/> </svg>
\ No newline at end of file
diff --git a/examples/web/res/dialpad.svg b/examples/web/res/dialpad.svg
new file mode 100644
index 0000000000000000000000000000000000000000..bc3a286d40081aefb228a3225fc36c8f8457443e
--- /dev/null
+++ b/examples/web/res/dialpad.svg
@@ -0,0 +1 @@
+<svg width="48px" height="48px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-labelledby="dialpadIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="dialpadIconTitle">Dialpad</title> <circle cx="7" cy="5" r="1"/> <circle cx="12" cy="5" r="1"/> <circle cx="17" cy="5" r="1"/> <circle cx="7" cy="10" r="1"/> <circle cx="12" cy="10" r="1"/> <circle cx="17" cy="10" r="1"/> <circle cx="7" cy="15" r="1"/> <circle cx="12" cy="15" r="1"/> <circle cx="12" cy="20" r="1"/> <circle cx="17" cy="15" r="1"/> </svg>
\ No newline at end of file
diff --git a/examples/web/res/icon.png b/examples/web/res/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..e0d2d61e8466ac6c165cfcd72cd169ff341e190d
Binary files /dev/null and b/examples/web/res/icon.png differ
diff --git a/examples/web/res/info.svg b/examples/web/res/info.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f9b1fc185b43d86e783a349d07e7e2bd3bd2156f
--- /dev/null
+++ b/examples/web/res/info.svg
@@ -0,0 +1 @@
+<svg role="img" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" viewBox="0 0 24 24" aria-labelledby="infoIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="infoIconTitle">Information</title> <path d="M12,12 L12,15"/> <line x1="12" y1="9" x2="12" y2="9"/> <circle cx="12" cy="12" r="10"/> </svg>
\ No newline at end of file
diff --git a/examples/web/res/marker.svg b/examples/web/res/marker.svg
new file mode 100644
index 0000000000000000000000000000000000000000..461dcb53619534e4ab0d6e5ab417e0eacba74559
--- /dev/null
+++ b/examples/web/res/marker.svg
@@ -0,0 +1 @@
+<svg width="48px" height="48px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-labelledby="markerIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" color="#ffffff"> <title id="markerIconTitle">Marker</title> <path fill-rule="evenodd" clip-rule="evenodd" d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"/> <path d="M6 20L8 13H16L18 20"/> <path d="M9.5 13L11.0299 6.88057C11.2823 5.87062 12.7177 5.87062 12.9701 6.88057L14.5 13"/> </svg>
\ No newline at end of file
diff --git a/examples/web/res/maximise.svg b/examples/web/res/maximise.svg
new file mode 100644
index 0000000000000000000000000000000000000000..0ce8e6ad748612c05f28d736ee7ee31d4b706f54
--- /dev/null
+++ b/examples/web/res/maximise.svg
@@ -0,0 +1 @@
+<svg role="img" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" viewBox="0 0 24 24" aria-labelledby="maximiseIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="maximiseIconTitle">Maximise View</title> <polyline points="21 16 21 21 16 21"/> <polyline points="8 21 3 21 3 16"/> <polyline points="16 3 21 3 21 8"/> <polyline points="3 8 3 3 8 3"/> </svg>
\ No newline at end of file
diff --git a/examples/web/res/minimise.svg b/examples/web/res/minimise.svg
new file mode 100644
index 0000000000000000000000000000000000000000..3d41ee133bf7a84a220f8649695c5fdc0a9fdb31
--- /dev/null
+++ b/examples/web/res/minimise.svg
@@ -0,0 +1 @@
+<svg role="img" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" viewBox="0 0 24 24" aria-labelledby="minimiseIconTitle" stroke="#ffffff" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="minimiseIconTitle">Minimise View</title> <polyline points="8 3 8 8 3 8"/> <polyline points="21 8 16 8 16 3"/> <polyline points="3 16 8 16 8 21"/> <polyline points="16 21 16 16 21 16"/> </svg>
\ No newline at end of file
diff --git a/examples/web/res/pause.svg b/examples/web/res/pause.svg
new file mode 100644
index 0000000000000000000000000000000000000000..768f80a6eef920d2decafd4b64e3ee22fcda0a9f
--- /dev/null
+++ b/examples/web/res/pause.svg
@@ -0,0 +1 @@
+<svg role="img" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" viewBox="0 0 24 24" aria-labelledby="pauseIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="pauseIconTitle">Pause</title> <rect width="4" height="16" x="5" y="4"/> <rect width="4" height="16" x="15" y="4"/> </svg>
\ No newline at end of file
diff --git a/examples/web/res/play.svg b/examples/web/res/play.svg
new file mode 100644
index 0000000000000000000000000000000000000000..647e1f5af90b09975ab0feaf300c87ed355307bf
--- /dev/null
+++ b/examples/web/res/play.svg
@@ -0,0 +1 @@
+<svg role="img" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" viewBox="0 0 24 24" aria-labelledby="playIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="playIconTitle">Play</title> <path d="M20 12L5 21V3z"/> </svg>
\ No newline at end of file
diff --git a/examples/web/res/reset.svg b/examples/web/res/reset.svg
new file mode 100644
index 0000000000000000000000000000000000000000..31c7867465102246e497a50ed9d3e7dc0fc24581
--- /dev/null
+++ b/examples/web/res/reset.svg
@@ -0,0 +1 @@
+<svg role="img" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" viewBox="0 0 24 24" aria-labelledby="refreshIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="refreshIconTitle">Refresh</title> <polyline points="22 12 19 15 16 12"/> <path d="M11,20 C6.581722,20 3,16.418278 3,12 C3,7.581722 6.581722,4 11,4 C15.418278,4 19,7.581722 19,12 L19,14"/> </svg>
\ No newline at end of file
diff --git a/examples/web/res/storm.png b/examples/web/res/storm.png
new file mode 100644
index 0000000000000000000000000000000000000000..afcff7338dad180df01cfab7bffc39a85ffc50e4
Binary files /dev/null and b/examples/web/res/storm.png differ
diff --git a/examples/web/res/sunglasses.png b/examples/web/res/sunglasses.png
new file mode 100644
index 0000000000000000000000000000000000000000..98b0bba649db0699a170016e18bc4b7a9ce3b70c
Binary files /dev/null and b/examples/web/res/sunglasses.png differ
diff --git a/examples/web/res/thunder.png b/examples/web/res/thunder.png
new file mode 100644
index 0000000000000000000000000000000000000000..409773908ec7d835778393e7de7c2559fd366672
Binary files /dev/null and b/examples/web/res/thunder.png differ
diff --git a/examples/web/res/upload.svg b/examples/web/res/upload.svg
new file mode 100644
index 0000000000000000000000000000000000000000..99911c5c39937def74fb39f855cce6ddd50e876e
--- /dev/null
+++ b/examples/web/res/upload.svg
@@ -0,0 +1 @@
+<svg role="img" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" viewBox="0 0 24 24" aria-labelledby="uploadIconTitle" stroke="#ffffff" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" fill="none" color="#ffffff"> <title id="uploadIconTitle">Upload</title> <path d="M12,4 L12,17"/> <polyline points="7 8 12 3 17 8"/> <path d="M20,21 L4,21"/> </svg>
\ No newline at end of file
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/res/roms/firstwhite.gb b/res/roms/firstwhite.gb
new file mode 100644
index 0000000000000000000000000000000000000000..6d35132175512a3f349dcb4378cfebad2694af6d
Binary files /dev/null and b/res/roms/firstwhite.gb differ
diff --git a/res/roms/ld_r_r.gb b/res/roms/ld_r_r.gb
new file mode 100644
index 0000000000000000000000000000000000000000..d497bfd1275361bc847fa94dc87b43729f180b5d
Binary files /dev/null and b/res/roms/ld_r_r.gb differ
diff --git a/res/roms/opus5.gb b/res/roms/opus5.gb
new file mode 100644
index 0000000000000000000000000000000000000000..a3b8028a31c98b753f0b03d8b8faa2da4232ef14
Binary files /dev/null and b/res/roms/opus5.gb differ
diff --git a/res/roms/rtc3test.gb b/res/roms/rtc3test.gb
new file mode 100644
index 0000000000000000000000000000000000000000..06836967eb2927b31c99de201a44cfd709380268
Binary files /dev/null and b/res/roms/rtc3test.gb differ
diff --git a/res/roms/special.gb b/res/roms/special.gb
new file mode 100644
index 0000000000000000000000000000000000000000..ad3e9984f967b77b7ffdf768842ce3c04517d059
Binary files /dev/null and b/res/roms/special.gb differ
diff --git a/src/gb.rs b/src/gb.rs
index e918ba68414c40cf8a7aba37695b63136b5a7f81..a70e2cfe869cd57ad7774973a559ac63b890ea6e 100644
--- a/src/gb.rs
+++ b/src/gb.rs
@@ -8,6 +8,23 @@ use crate::{
 #[cfg(feature = "wasm")]
 use wasm_bindgen::prelude::*;
 
+/// Static data corresponding to the DMG boot ROM
+/// allows freely using the emulator without external dependency.
+pub const BOOT_DATA: [u8; 256] = [
+    49, 254, 255, 175, 33, 255, 159, 50, 203, 124, 32, 251, 33, 38, 255, 14, 17, 62, 128, 50, 226,
+    12, 62, 243, 226, 50, 62, 119, 119, 62, 252, 224, 71, 17, 4, 1, 33, 16, 128, 26, 205, 149, 0,
+    205, 150, 0, 19, 123, 254, 52, 32, 243, 17, 216, 0, 6, 8, 26, 19, 34, 35, 5, 32, 249, 62, 25,
+    234, 16, 153, 33, 47, 153, 14, 12, 61, 40, 8, 50, 13, 32, 249, 46, 15, 24, 243, 103, 62, 100,
+    87, 224, 66, 62, 145, 224, 64, 4, 30, 2, 14, 12, 240, 68, 254, 144, 32, 250, 13, 32, 247, 29,
+    32, 242, 14, 19, 36, 124, 30, 131, 254, 98, 40, 6, 30, 193, 254, 100, 32, 6, 123, 226, 12, 62,
+    135, 226, 240, 66, 144, 224, 66, 21, 32, 210, 5, 32, 79, 22, 32, 24, 203, 79, 6, 4, 197, 203,
+    17, 23, 193, 203, 17, 23, 5, 32, 245, 34, 35, 34, 35, 201, 206, 237, 102, 102, 204, 13, 0, 11,
+    3, 115, 0, 131, 0, 12, 0, 13, 0, 8, 17, 31, 136, 137, 0, 14, 220, 204, 110, 230, 221, 221, 217,
+    153, 187, 187, 103, 99, 110, 14, 236, 204, 221, 220, 153, 159, 187, 185, 51, 62, 60, 66, 185,
+    165, 185, 165, 66, 60, 33, 4, 1, 17, 168, 0, 26, 19, 190, 32, 254, 35, 125, 254, 52, 32, 245,
+    6, 25, 120, 134, 35, 5, 32, 251, 134, 32, 254, 62, 1, 224, 80,
+];
+
 #[cfg_attr(feature = "wasm", wasm_bindgen)]
 pub struct GameBoy {
     cpu: Cpu,
@@ -23,6 +40,10 @@ impl GameBoy {
         GameBoy { cpu: cpu }
     }
 
+    pub fn pc(&self) -> u16 {
+        self.cpu.pc()
+    }
+
     pub fn clock(&mut self) -> u8 {
         let cycles = self.cpu_clock();
         self.ppu_clock(cycles);
@@ -37,18 +58,34 @@ 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, path: &str) {
+    pub fn load_boot(&mut self, data: &[u8]) {
+        self.cpu.mmu().write_boot(0x0000, data);
+    }
+
+    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 load_boot_static(&mut self) {
+        self.load_boot(&BOOT_DATA);
+    }
+
+    pub fn frame_buffer_eager(&mut self) -> Vec<u8> {
+        self.frame_buffer().to_vec()
     }
 }
 
diff --git a/src/mmu.rs b/src/mmu.rs
index 4eab0ec249be6eca30538348832f7c7b81275e05..0a1be916b6c2d5246d80ae0383075345308913f8 100644
--- a/src/mmu.rs
+++ b/src/mmu.rs
@@ -94,15 +94,16 @@ impl Mmu {
         match addr & 0xf000 {
             // BOOT (256 B) + ROM0 (4 KB/16 KB)
             0x0000 => {
-                println!("Writing to BOOT")
+                self.rom[addr as usize] = value;
+                println!("Writing to BOOT at 0x{:04x}", addr)
             }
             // ROM0 (12 KB/16 KB)
             0x1000 | 0x2000 | 0x3000 => {
-                println!("Writing to ROM 0");
+                println!("Writing to ROM 0 at 0x{:04x}", addr);
             }
             // ROM1 (Unbanked) (16 KB)
             0x4000 | 0x5000 | 0x6000 | 0x7000 => {
-                println!("Writing to ROM 1");
+                println!("Writing to ROM 1 at 0x{:04x}", addr);
             }
             // Graphics: VRAM (8 KB)
             0x8000 | 0x9000 => {