Repository: rustzx/rustzx (language: rust)

Issue:
raxoft's `z80test` undocumented opcodes/flags tests fail
There are a lot of failures in z80 tests by @raxoft https://github.com/raxoft/z80test
(Found via https://viva-games.ru/game/zilog-z80-cpu-test-suite)

Passing test tapes: `z80doc.tap` `z80docflags.tap`
Failing test tapes: `z80full.tap`, `z80ccf.tap`, `z80flags.tap`, `z80memptr.tap`

Most likely `ccf` and `memptr` tests fail because lack of `Q` and `Memptr` hidden undocumented registers emulation.


Discussion / hints:
Marked as enhancement at this issue is not really world-breaking for now.
Memptr was implemented via #132, however the following still be done in scope of this task:
- Memptr integration tests
- Q register implementation
- Q register integration tests

Fix patch:
diff --git a/.cargo/config.toml b/.cargo/config.toml
index 2591cb8..17297ec 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -15,4 +15,4 @@ rustflags = [
 rustflags = [
   "-C", "link-arg=-undefined",
   "-C", "link-arg=dynamic_lookup",
-]
\ No newline at end of file
+]
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 60ae7b8..ef43a88 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -95,3 +95,10 @@ jobs:
         with:
           command: test
           args: -p rustzx-test
+  typos:
+    name: "Typos check"
+    runs-on: ubuntu-20.04
+    steps:
+      - uses: actions/checkout@v2
+      - name: Typos
+        uses: crate-ci/typos@master
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d3c8af1..530f860 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,7 +37,7 @@ Changes:
     - File autodetect from CLI
     - Added configurable sound sample rate
     - Removed redundant args
-- **[Dependencies]** Switched to bundled `sdl` crate mode, making build almost dependecy-free (CMake and C compiller are still requird)
+- **[Dependencies]** Switched to bundled `sdl` crate mode, making build almost dependency-free (CMake and C compiller are still requird)
 - **[Infrastructure]** Moved CI to _Github Actions_
     - Added `rustfmt` step to CI
     - Added `clippy` step to CI
@@ -78,7 +78,7 @@ Changes:
 - **[05.07.2016]** v0.8 development started in branch `develop`
 - **[27.06.2016]** Release v0.7.1
 - **[26.06.2016]** Beeper sound implemented :notes:, release planed to July 1 :rocket:
-- **[12.06.2016]** Some Comand line arguments fixes/enchantments
+- **[12.06.2016]** Some Command line arguments fixes/enchantments
 - **[12.06.2016]** SNA files loading
 - **[11.06.2016]** Command line arguments using **clap** crate
 - **[11.06.2016]** Tap files fast loading implemented, finnaly!
diff --git a/Cargo.lock b/Cargo.lock
index 5d58a05..5cc1ac9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -16,9 +16,9 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
 
 [[package]]
 name = "alsa"
-version = "0.6.0"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5915f52fe2cf65e83924d037b6c5290b7cee097c6b5c8700746e6168a343fd6b"
+checksum = "8512c9117059663fb5606788fbca3619e2a91dac0e3fe516242eab1fa6be5e44"
 dependencies = [
  "alsa-sys",
  "bitflags",
@@ -64,13 +64,13 @@ dependencies = [
 
 [[package]]
 name = "autocfg"
-version = "1.0.1"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
 [[package]]
 name = "aym"
-version = "0.15.1"
+version = "0.16.0"
 dependencies = [
  "libm",
  "num-traits",
@@ -233,6 +233,12 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "core-foundation-sys"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
+
 [[package]]
 name = "core-foundation-sys"
 version = "0.8.2"
@@ -241,11 +247,12 @@ checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
 
 [[package]]
 name = "coreaudio-rs"
-version = "0.10.0"
+version = "0.11.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88"
+checksum = "cb17e2d1795b1996419648915df94bc7103c28f7b48062d7acf4652fc371b2ff"
 dependencies = [
  "bitflags",
+ "core-foundation-sys 0.6.2",
  "coreaudio-sys",
 ]
 
@@ -260,27 +267,27 @@ dependencies = [
 
 [[package]]
 name = "cpal"
-version = "0.13.5"
+version = "0.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74117836a5124f3629e4b474eed03e479abaf98988b4bb317e29f08cfe0e4116"
+checksum = "f1241019dec4a73f874bdf6fe3467a8478b47cecaf79435f7acb01f107ab41b1"
 dependencies = [
  "alsa",
- "core-foundation-sys",
+ "core-foundation-sys 0.8.2",
  "coreaudio-rs",
- "jni",
+ "dasp_sample",
+ "jni 0.19.0",
  "js-sys",
- "lazy_static",
  "libc",
- "mach",
- "ndk 0.6.0",
- "ndk-glue 0.6.2",
- "nix",
+ "mach2",
+ "ndk",
+ "ndk-context",
  "oboe",
+ "once_cell",
  "parking_lot",
- "stdweb",
- "thiserror",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
  "web-sys",
- "winapi",
+ "windows",
 ]
 
 [[package]]
@@ -302,23 +309,22 @@ dependencies = [
 ]
 
 [[package]]
-name = "darling"
-version = "0.10.2"
+name = "crossbeam-utils"
+version = "0.8.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
+checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
 dependencies = [
- "darling_core 0.10.2",
- "darling_macro 0.10.2",
+ "cfg-if",
 ]
 
 [[package]]
 name = "darling"
-version = "0.13.4"
+version = "0.10.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
+checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
 dependencies = [
- "darling_core 0.13.4",
- "darling_macro 0.13.4",
+ "darling_core",
+ "darling_macro",
 ]
 
 [[package]]
@@ -335,41 +341,22 @@ dependencies = [
  "syn",
 ]
 
-[[package]]
-name = "darling_core"
-version = "0.13.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
-dependencies = [
- "fnv",
- "ident_case",
- "proc-macro2",
- "quote",
- "strsim 0.10.0",
- "syn",
-]
-
 [[package]]
 name = "darling_macro"
 version = "0.10.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
 dependencies = [
- "darling_core 0.10.2",
+ "darling_core",
  "quote",
  "syn",
 ]
 
 [[package]]
-name = "darling_macro"
-version = "0.13.4"
+name = "dasp_sample"
+version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
-dependencies = [
- "darling_core 0.13.4",
- "quote",
- "syn",
-]
+checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
 
 [[package]]
 name = "deflate"
@@ -484,7 +471,7 @@ version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7e08079fa3c89edec9160ceaa9e7172785468c26c053d12924cce0d5a55c241a"
 dependencies = [
- "darling 0.10.2",
+ "darling",
  "proc-macro2",
  "quote",
  "syn",
@@ -541,15 +528,6 @@ version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
 
-[[package]]
-name = "instant"
-version = "0.1.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd"
-dependencies = [
- "cfg-if",
-]
-
 [[package]]
 name = "itoa"
 version = "1.0.2"
@@ -570,6 +548,20 @@ dependencies = [
  "walkdir",
 ]
 
+[[package]]
+name = "jni"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c"
+dependencies = [
+ "cesu8",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+]
+
 [[package]]
 name = "jni-sys"
 version = "0.3.0"
@@ -587,9 +579,9 @@ dependencies = [
 
 [[package]]
 name = "js-sys"
-version = "0.3.55"
+version = "0.3.61"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
+checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -633,10 +625,11 @@ checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a"
 
 [[package]]
 name = "lock_api"
-version = "0.4.5"
+version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
+checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
 dependencies = [
+ "autocfg",
  "scopeguard",
 ]
 
@@ -650,10 +643,10 @@ dependencies = [
 ]
 
 [[package]]
-name = "mach"
-version = "0.3.2"
+name = "mach2"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
+checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8"
 dependencies = [
  "libc",
 ]
@@ -664,15 +657,6 @@ version = "2.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
 
-[[package]]
-name = "memoffset"
-version = "0.6.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
-dependencies = [
- "autocfg",
-]
-
 [[package]]
 name = "miniz_oxide"
 version = "0.3.7"
@@ -703,27 +687,15 @@ dependencies = [
 
 [[package]]
 name = "ndk"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d64d6af06fde0e527b1ba5c7b79a6cc89cfc46325b0b2887dffe8f70197e0c3c"
-dependencies = [
- "bitflags",
- "jni-sys",
- "ndk-sys 0.2.1",
- "num_enum",
- "thiserror",
-]
-
-[[package]]
-name = "ndk"
-version = "0.6.0"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4"
+checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0"
 dependencies = [
  "bitflags",
  "jni-sys",
- "ndk-sys 0.3.0",
+ "ndk-sys",
  "num_enum",
+ "raw-window-handle",
  "thiserror",
 ]
 
@@ -733,87 +705,24 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
 
-[[package]]
-name = "ndk-glue"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3e9e94628f24e7a3cb5b96a2dc5683acd9230bf11991c2a1677b87695138420"
-dependencies = [
- "lazy_static",
- "libc",
- "log",
- "ndk 0.4.0",
- "ndk-macro 0.2.0",
- "ndk-sys 0.2.1",
-]
-
-[[package]]
-name = "ndk-glue"
-version = "0.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d0c4a7b83860226e6b4183edac21851f05d5a51756e97a1144b7f5a6b63e65f"
-dependencies = [
- "lazy_static",
- "libc",
- "log",
- "ndk 0.6.0",
- "ndk-context",
- "ndk-macro 0.3.0",
- "ndk-sys 0.3.0",
-]
-
-[[package]]
-name = "ndk-macro"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d"
-dependencies = [
- "darling 0.10.2",
- "proc-macro-crate 0.1.5",
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "ndk-macro"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c"
-dependencies = [
- "darling 0.13.4",
- "proc-macro-crate 1.1.0",
- "proc-macro2",
- "quote",
- "syn",
-]
-
 [[package]]
 name = "ndk-sys"
-version = "0.2.1"
+version = "0.4.1+23.1.7779620"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d"
-
-[[package]]
-name = "ndk-sys"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97"
+checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3"
 dependencies = [
  "jni-sys",
 ]
 
 [[package]]
 name = "nix"
-version = "0.23.1"
+version = "0.24.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6"
+checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069"
 dependencies = [
  "bitflags",
- "cc",
  "cfg-if",
  "libc",
- "memoffset",
 ]
 
 [[package]]
@@ -882,7 +791,7 @@ version = "0.5.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9"
 dependencies = [
- "proc-macro-crate 1.1.0",
+ "proc-macro-crate",
  "proc-macro2",
  "quote",
  "syn",
@@ -899,13 +808,13 @@ dependencies = [
 
 [[package]]
 name = "oboe"
-version = "0.4.4"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e15e22bc67e047fe342a32ecba55f555e3be6166b04dd157cd0f803dfa9f48e1"
+checksum = "8868cc237ee02e2d9618539a23a8d228b9bb3fc2e7a5b11eed3831de77c395d0"
 dependencies = [
- "jni",
- "ndk 0.4.0",
- "ndk-glue 0.4.0",
+ "jni 0.20.0",
+ "ndk",
+ "ndk-context",
  "num-derive",
  "num-traits",
  "oboe-sys",
@@ -913,18 +822,18 @@ dependencies = [
 
 [[package]]
 name = "oboe-sys"
-version = "0.4.4"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "338142ae5ab0aaedc8275aa8f67f460e43ae0fca76a695a742d56da0a269eadc"
+checksum = "7f44155e7fb718d3cfddcf70690b2b51ac4412f347cd9e4fbe511abe9cd7b5f2"
 dependencies = [
  "cc",
 ]
 
 [[package]]
 name = "once_cell"
-version = "1.8.0"
+version = "1.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
+checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
 
 [[package]]
 name = "opaque-debug"
@@ -934,27 +843,25 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
 
 [[package]]
 name = "parking_lot"
-version = "0.11.2"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
 dependencies = [
- "instant",
  "lock_api",
  "parking_lot_core",
 ]
 
 [[package]]
 name = "parking_lot_core"
-version = "0.8.5"
+version = "0.9.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
+checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
 dependencies = [
  "cfg-if",
- "instant",
  "libc",
  "redox_syscall",
  "smallvec",
- "winapi",
+ "windows-sys",
 ]
 
 [[package]]
@@ -993,15 +900,6 @@ version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
 
-[[package]]
-name = "proc-macro-crate"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785"
-dependencies = [
- "toml",
-]
-
 [[package]]
 name = "proc-macro-crate"
 version = "1.1.0"
@@ -1038,11 +936,11 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.26"
+version = "1.0.53"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
+checksum = "ba466839c78239c09faf015484e5cc04860f88242cff4d03eb038f04b4699b73"
 dependencies = [
- "unicode-xid",
+ "unicode-ident",
 ]
 
 [[package]]
@@ -1094,6 +992,12 @@ dependencies = [
  "rand_core",
 ]
 
+[[package]]
+name = "raw-window-handle"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f851a03551ceefd30132e447f07f96cb7011d6b658374f3aed847333adb5559"
+
 [[package]]
 name = "redox_syscall"
 version = "0.2.10"
@@ -1124,6 +1028,15 @@ version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b9b1a3d5f46d53f4a3478e2be4a5a5ce5108ea58b100dcd139830eae7f79a3a1"
 
+[[package]]
+name = "ringbuf"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93ca10b9c9e53ac855a2d6953bce34cef6edbac32c4b13047a4d59d67299420a"
+dependencies = [
+ "crossbeam-utils",
+]
+
 [[package]]
 name = "rustc-hash"
 version = "1.1.0"
@@ -1132,11 +1045,12 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
 
 [[package]]
 name = "rustzx"
-version = "0.15.0"
+version = "0.16.0"
 dependencies = [
  "anyhow",
  "cpal",
  "log",
+ "ringbuf",
  "rustzx-core",
  "rustzx-utils",
  "sdl2",
@@ -1147,7 +1061,7 @@ dependencies = [
 
 [[package]]
 name = "rustzx-core"
-version = "0.15.0"
+version = "0.16.0"
 dependencies = [
  "aym",
  "bitflags",
@@ -1161,7 +1075,7 @@ dependencies = [
 
 [[package]]
 name = "rustzx-test"
-version = "0.15.0"
+version = "0.16.0"
 dependencies = [
  "anyhow",
  "base64",
@@ -1178,7 +1092,7 @@ dependencies = [
 
 [[package]]
 name = "rustzx-utils"
-version = "0.15.0"
+version = "0.16.0"
 dependencies = [
  "flate2",
  "log",
@@ -1187,7 +1101,7 @@ dependencies = [
 
 [[package]]
 name = "rustzx-z80"
-version = "0.15.0"
+version = "0.16.0"
 dependencies = [
  "paste",
 ]
@@ -1281,12 +1195,6 @@ version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
 
-[[package]]
-name = "stdweb"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e"
-
 [[package]]
 name = "strsim"
 version = "0.8.0"
@@ -1299,12 +1207,6 @@ version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
 
-[[package]]
-name = "strsim"
-version = "0.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
-
 [[package]]
 name = "structopt"
 version = "0.3.21"
@@ -1352,13 +1254,13 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "1.0.70"
+version = "1.0.109"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9505f307c872bab8eb46f77ae357c8eba1fdacead58ee5a850116b1d7f82883"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
 dependencies = [
  "proc-macro2",
  "quote",
- "unicode-xid",
+ "unicode-ident",
 ]
 
 [[package]]
@@ -1443,6 +1345,12 @@ version = "1.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
 
+[[package]]
+name = "unicode-ident"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
+
 [[package]]
 name = "unicode-segmentation"
 version = "1.7.1"
@@ -1455,12 +1363,6 @@ version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
 
-[[package]]
-name = "unicode-xid"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
-
 [[package]]
 name = "vec_map"
 version = "0.8.2"
@@ -1481,7 +1383,7 @@ checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
 
 [[package]]
 name = "vtx"
-version = "0.15.0"
+version = "0.16.0"
 dependencies = [
  "aym",
  "byteorder",
@@ -1495,7 +1397,7 @@ dependencies = [
 
 [[package]]
 name = "vtx-bin"
-version = "0.15.0"
+version = "0.16.0"
 dependencies = [
  "anyhow",
  "structopt",
@@ -1522,9 +1424,9 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.78"
+version = "0.2.84"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
+checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
 dependencies = [
  "cfg-if",
  "wasm-bindgen-macro",
@@ -1532,24 +1434,36 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.78"
+version = "0.2.84"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
+checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
 dependencies = [
  "bumpalo",
- "lazy_static",
  "log",
+ "once_cell",
  "proc-macro2",
  "quote",
  "syn",
  "wasm-bindgen-shared",
 ]
 
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.78"
+version = "0.2.84"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
+checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -1557,9 +1471,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.78"
+version = "0.2.84"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
+checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1570,9 +1484,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.78"
+version = "0.2.84"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
+checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
 
 [[package]]
 name = "wav"
@@ -1623,3 +1537,78 @@ name = "winapi-x86_64-pc-windows-gnu"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
diff --git a/Cargo.toml b/Cargo.toml
index 42589d1..0abb881 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,3 +9,20 @@ members = [
     "vtx",
     "vtx/vtx-bin",
 ]
+
+[workspace.package]
+version = "0.16.0"
+license = "MIT"
+edition = "2021"
+authors = ["The RustZX contributors", "Vladyslav Nikonov <mail@pacmancoder.xyz>"]
+repository = "https://github.com/rustzx/rustzx"
+
+
+
+
+[workspace.dependencies]
+aym = { path = "aym" }
+rustzx-core = { path = "rustzx-core" }
+rustzx-utils = { path = "rustzx-utils" }
+rustzx-z80 = { path = "rustzx-z80" }
+vtx = { path = "vtx" }
diff --git a/README.md b/README.md
index 529a689..411e0db 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,10 @@ ZX Spectrum emulator written in Rust
     - Global allocator is still needed, but all dynamic
        allocations were minimized
     - All resource-heavy features are configurable via cargo `features`
+- Obscure Z80 features emulation:
+    - `WZ/memptr` register (`F3/F5` flags obscure behavior in `BIT n, (HL)`)
+    - `Q` register (`F3/F5` flags obscure behavior in `SCF` and `CCF`)
+    - Block instruction flags [oddities](https://github.com/MrKWatkins/ZXSpectrumNextTests/tree/develop/Tests/ZX48_ZX128/Z80BlockInstructionFlags) (`LDxR`/`CPxR`/`INxR`/`OTxR`)
 
 ## Install
 1. Sure that you have C compiller and CMake to
@@ -118,6 +122,7 @@ defails about ZX Spectrum.
 - [FUSE](http://fuse-emulator.sourceforge.net/) emulator source for finding out correct timings
 - [YAZE test suite](https://www.mathematik.uni-ulm.de/users/ag/yaze-ag/)
 - [z80test test suite](https://github.com/raxoft/z80test)
+- [ROM routines](https://skoolkid.github.io/rom/maps/routines.html)
 
 ## ROM's
 Emulator contains ROMs, created by by Sinclair Research Ltd (now owned by Amstrad plc),
diff --git a/aym/Cargo.toml b/aym/Cargo.toml
index 8102448..b4f9780 100644
--- a/aym/Cargo.toml
+++ b/aym/Cargo.toml
@@ -1,12 +1,14 @@
 [package]
 name = "aym"
-version = "0.15.1"
 description = "AY/YM sound chip emulation library"
-authors = ["The RustZX contributors", "Vladyslav Nikonov <mail@pacmancoder.xyz>"]
-license = "MIT"
-edition = "2018"
 keywords = ["ay", "sound", "emulator", "no_std"]
-repository = "https://github.com/rustzx/rustzx"
+
+version.workspace = true
+license.workspace = true
+edition.workspace = true
+authors.workspace = true
+repository.workspace = true
+
 
 [dependencies]
 num-traits = { version = "0.2", default-features = false }
diff --git a/aym/src/lib.rs b/aym/src/lib.rs
index e6f4daf..617bb66 100644
--- a/aym/src/lib.rs
+++ b/aym/src/lib.rs
@@ -9,7 +9,7 @@ use num_traits::Num;
 /// AY/YM Sound chip register count
 pub const AY_REGISTER_COUNT: usize = 14;
 
-/// Samples, returned by `aym` implemnt this trait; It allows to correctly convert
+/// Samples, returned by `aym` implement this trait; It allows to correctly convert
 /// the sound sample between different types. e.g.
 /// [to_f32](AySample::to_f32)/[to_f64](AySample::to_f64) will always return sample in the
 /// correct range `[-1.0; 1.0]`, while for example [to_i8](AySample::to_i8)
diff --git a/rustzx-core/Cargo.toml b/rustzx-core/Cargo.toml
index 209a286..15a470f 100644
--- a/rustzx-core/Cargo.toml
+++ b/rustzx-core/Cargo.toml
@@ -1,12 +1,13 @@
 [package]
 name = "rustzx-core"
-version = "0.15.0"
-authors = ["The RustZX contributors", "Vladyslav Nikonov <mail@pacmancoder.xyz>"]
-description = "ZX Spectum emulator core library"
-repository = "https://github.com/rustzx/rustzx"
+description = "ZX Spectrum emulator core library"
 keywords = ["emulator", "game", "z80", "no_std"]
-license = "MIT"
-edition = "2018"
+
+version.workspace = true
+license.workspace = true
+edition.workspace = true
+authors.workspace = true
+repository.workspace = true
 
 [features]
 default = []
@@ -23,6 +24,6 @@ lazy_static = { version = "1.1", features = ["spin_no_std"] }
 displaydoc = { version = "0.2", default-features = false }
 from_variants = "0.6"
 enum_dispatch = "0.3"
-aym = { version = "0.15", path = "../aym", optional = true }
-rustzx-z80 = { version = "0.15", path = "../rustzx-z80" }
+aym = { workspace = true, optional = true }
+rustzx-z80 = { workspace = true }
 strum = { version = "0.22", default-features = false, features = ["derive"], optional = true }
diff --git a/rustzx-core/src/emulator/fastload/tap.rs b/rustzx-core/src/emulator/fastload/tap.rs
index 72a39aa..28e99f6 100644
--- a/rustzx-core/src/emulator/fastload/tap.rs
+++ b/rustzx-core/src/emulator/fastload/tap.rs
@@ -4,7 +4,7 @@ use rustzx_z80::{RegName16, Z80Bus, FLAG_CARRY, FLAG_ZERO};
 
 pub fn fast_load_tap<H: Host>(emulator: &mut Emulator<H>) -> Result<()> {
     // So, at current moment we at 0x056C in 48K Rom.
-    // AF contains some garbage. so we need to swap if wtih A'F'
+    // AF contains some garbage. so we need to swap if with A'F'
     emulator.cpu.regs.swap_af_alt();
     // now we have type of block at A and flags before LD-BYTES at F
     let mut f = emulator.cpu.regs.get_flags();
diff --git a/rustzx-core/src/emulator/mod.rs b/rustzx-core/src/emulator/mod.rs
index 54efe48..248bd06 100644
--- a/rustzx-core/src/emulator/mod.rs
+++ b/rustzx-core/src/emulator/mod.rs
@@ -1,5 +1,6 @@
 //! Platform-independent high-level Emulator interaction module
 mod fastload;
+pub mod poke;
 mod screenshot;
 mod snapshot;
 
@@ -33,6 +34,25 @@ use crate::zx::sound::sample::SoundSample;
 #[cfg(feature = "autoload")]
 use crate::{host::BufferCursor, zx::machine::ZXMachine};
 
+/// Represents emulator stop reason
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum EmulationStopReason {
+    /// Requested frames count have been emulated successfully
+    Completed,
+    /// Emulation time limit has been reached
+    Timeout,
+    /// Emulator has reached breakpoint address
+    Breakpoint,
+}
+
+/// Represents emulator emulation result
+pub struct EmulationInfo {
+    /// Emulation duration in emulated time (not real time)
+    pub duration: Duration,
+    /// Emulation stop reason, see [EmulationStopReason]
+    pub stop_reason: EmulationStopReason,
+}
+
 /// Represents main Emulator structure
 pub struct Emulator<H: Host> {
     settings: RustzxSettings,
@@ -167,7 +187,7 @@ impl<H: Host> Emulator<H> {
     }
 
     /// Rewinds tape. May return error if underlying tape asset failed to
-    /// perform seek operaion to go back to the the beginning of the tape
+    /// perform seek operation to go back to the the beginning of the tape
     pub fn rewind_tape(&mut self) -> Result<()> {
         self.controller.tape.rewind()
     }
@@ -189,6 +209,21 @@ impl<H: Host> Emulator<H> {
         self.controller.io_extender.as_mut()
     }
 
+    /// Sets [Host::DebugInterface] for the emulator instance
+    pub fn set_debug_interface(&mut self, debug_interface: H::DebugInterface) {
+        self.controller.debug_interface = Some(debug_interface);
+    }
+
+    /// Returns current [Host::DebugInterface] instance
+    pub fn debug_interface(&mut self) -> Option<&mut H::DebugInterface> {
+        self.controller.debug_interface.as_mut()
+    }
+
+    /// Reads byte from memory
+    pub fn peek(&self, addr: u16) -> u8 {
+        self.controller.memory.read(addr)
+    }
+
     pub fn border_color(&self) -> ZXColor {
         self.controller.border_color
     }
@@ -228,18 +263,26 @@ impl<H: Host> Emulator<H> {
         self.controller.mixer.pop()
     }
 
-    fn process_events(&mut self, event: EmulationEvents) -> Result<()> {
-        if event.contains(EmulationEvents::TAPE_FAST_LOAD_TRIGGER_DETECTED)
-            && self.controller.tape.can_fast_load()
-            && self.fast_load
-        {
+    fn process_fast_load_event(&mut self) -> Result<()> {
+        if self.controller.tape.can_fast_load() && self.fast_load {
             fastload::tap::fast_load_tap(self)?;
         }
         Ok(())
     }
 
-    /// Perform emulatio up to `emulation_limit` duration, returns actuall elapsed duration
-    pub fn emulate_frames(&mut self, emulation_limit: Duration) -> Result<Duration> {
+    /// Execute `poke::Poke` action on the emulator
+    pub fn execute_poke(&mut self, poke: impl poke::Poke) {
+        for action in poke.actions().iter().copied() {
+            match action {
+                poke::PokeAction::Mem { addr, value } => {
+                    self.controller.memory.force_write(addr, value);
+                }
+            }
+        }
+    }
+
+    /// Perform emulatio up to `emulation_limit` duration, returns actual elapsed duration
+    pub fn emulate_frames(&mut self, emulation_limit: Duration) -> Result<EmulationInfo> {
         let stopwatch = H::EmulationStopwatch::new();
         // frame loop
         loop {
@@ -251,15 +294,27 @@ impl<H: Host> Emulator<H> {
                 if let Some(e) = self.controller.take_last_emulation_error() {
                     return Err(e);
                 }
-                if !self.controller.events().is_empty() {
-                    self.process_events(self.controller.events())?;
-                    self.controller.clear_events();
+
+                let events = self.controller.take_events();
+                if !events.is_empty() {
+                    if events.contains(EmulationEvents::TAPE_FAST_LOAD_TRIGGER_DETECTED) {
+                        self.process_fast_load_event()?;
+                    }
+                    if events.contains(EmulationEvents::PC_BREAKPOINT) {
+                        return Ok(EmulationInfo {
+                            duration: stopwatch.measure(),
+                            stop_reason: EmulationStopReason::Breakpoint,
+                        });
+                    }
                 }
 
                 match self.mode {
                     EmulationMode::FrameCount(frames) => {
                         if self.controller.frames_count() >= frames {
-                            return Ok(stopwatch.measure());
+                            return Ok(EmulationInfo {
+                                duration: stopwatch.measure(),
+                                stop_reason: EmulationStopReason::Completed,
+                            });
                         };
                     }
                     EmulationMode::Max => {
@@ -271,7 +326,10 @@ impl<H: Host> Emulator<H> {
             }
             // if time is bigger than `max_time` then stop emulation cycle
             if stopwatch.measure() > emulation_limit {
-                return Ok(stopwatch.measure());
+                return Ok(EmulationInfo {
+                    duration: stopwatch.measure(),
+                    stop_reason: EmulationStopReason::Timeout,
+                });
             }
         }
     }
diff --git a/rustzx-core/src/emulator/poke.rs b/rustzx-core/src/emulator/poke.rs
new file mode 100644
index 0000000..b154d45
--- /dev/null
+++ b/rustzx-core/src/emulator/poke.rs
@@ -0,0 +1,35 @@
+//! Pokes are used to modify internal emulator state such as memory, registers, etc.
+
+/// Action to perform on emulator state
+#[derive(Clone, Copy)]
+pub enum PokeAction {
+    Mem { addr: u16, value: u8 },
+}
+
+impl PokeAction {
+    /// Creates new memory poke action
+    pub const fn mem(addr: u16, value: u8) -> Self {
+        Self::Mem { addr, value }
+    }
+}
+
+pub trait Poke {
+    /// Returns list of actions to perform on emulator state
+    fn actions(&self) -> &[PokeAction];
+}
+
+/// Poke which disables message and enter key prompt in 48K ROM when scrolling screen in BASIC mode
+pub struct DisableScrollMessageRom48;
+impl Poke for DisableScrollMessageRom48 {
+    fn actions(&self) -> &[PokeAction] {
+        // Injects `JP 0x0CD2` at 0x0C88
+        // https://skoolkid.github.io/rom/asm/0C55.html
+        const ACTIONS: &[PokeAction] = &[
+            PokeAction::mem(0x0C88, 0xC3),
+            PokeAction::mem(0x0C89, 0xD2),
+            PokeAction::mem(0x0C8A, 0x0C),
+        ];
+
+        ACTIONS
+    }
+}
diff --git a/rustzx-core/src/host/io.rs b/rustzx-core/src/host/io.rs
index ce33612..ce6a917 100644
--- a/rustzx-core/src/host/io.rs
+++ b/rustzx-core/src/host/io.rs
@@ -96,7 +96,7 @@ pub trait DataRecorder {
     /// destination asset was reached (e.g. buffer filled)
     fn write(&mut self, buf: &[u8]) -> Result<usize>;
 
-    /// Writes all bytes to the destiantion or returns
+    /// Writes all bytes to the destination or returns
     /// [IoError::WriteZero] if destination refused to accept
     /// more bytes
     fn write_all(&mut self, mut buf: &[u8]) -> Result<()> {
diff --git a/rustzx-core/src/host/mod.rs b/rustzx-core/src/host/mod.rs
index 4768904..b544a68 100644
--- a/rustzx-core/src/host/mod.rs
+++ b/rustzx-core/src/host/mod.rs
@@ -57,13 +57,13 @@ pub trait IoExtender {
     /// Read byte value from io extender
     fn read(&mut self, port: u16) -> u8;
     /// Return true if io externder can process
-    /// incomming read/write operation for a
+    /// incoming read/write operation for a
     /// given port
     fn extends_port(&self, port: u16) -> bool;
 }
 
-/// Does not extend any IO
-pub struct StubIoExtender {}
+/// IO externder which does nothing
+pub struct StubIoExtender;
 
 impl IoExtender for StubIoExtender {
     fn write(&mut self, _: u16, _: u8) {}
@@ -77,6 +77,21 @@ impl IoExtender for StubIoExtender {
     }
 }
 
+/// Allows to externd RustZX emulator with custom debug logic
+pub trait DebugInterface {
+    /// Returns true if breakpoint at given address is set and emulation should be stopped
+    fn check_pc_breakpoint(&mut self, addr: u16) -> bool;
+}
+
+/// Debug interface which does nothing
+pub struct StubDebugInterface;
+
+impl DebugInterface for StubDebugInterface {
+    fn check_pc_breakpoint(&mut self, _addr: u16) -> bool {
+        false
+    }
+}
+
 /// Represents set of required types for emulator implementation
 /// based on `rustzx-core`.
 pub trait Host {
@@ -91,4 +106,6 @@ pub trait Host {
     type EmulationStopwatch: Stopwatch;
     /// RustZX debug port implementation
     type IoExtender: IoExtender;
+    /// Debug interface logic (e.g. breakpoints)
+    type DebugInterface: DebugInterface;
 }
diff --git a/rustzx-core/src/lib.rs b/rustzx-core/src/lib.rs
index 5a571ee..408c8f7 100644
--- a/rustzx-core/src/lib.rs
+++ b/rustzx-core/src/lib.rs
@@ -8,7 +8,7 @@ pub mod error;
 pub mod host;
 pub mod zx;
 
-pub use emulator::Emulator;
+pub use emulator::{poke, EmulationInfo, EmulationStopReason, Emulator};
 pub use settings::RustzxSettings;
 pub use utils::EmulationMode;
 
diff --git a/rustzx-core/src/zx/constants.rs b/rustzx-core/src/zx/constants.rs
index 32abbd1..b3bd13f 100644
--- a/rustzx-core/src/zx/constants.rs
+++ b/rustzx-core/src/zx/constants.rs
@@ -15,7 +15,7 @@ pub const FPS: usize = 50;
 pub(crate) const BITMAP_MAX_REL: u16 = 0x17FF;
 pub(crate) const ATTR_BASE_REL: u16 = 0x1800;
 pub(crate) const ATTR_MAX_REL: u16 = 0x1AFF;
-/// on all spectrums theese values are fixed
+/// on all spectrums these values are fixed
 pub(crate) const CLOCKS_PER_COL: usize = 4;
 #[cfg(feature = "precise-border")]
 pub(crate) const PIXELS_PER_CLOCK: usize = 2;
diff --git a/rustzx-core/src/zx/controller.rs b/rustzx-core/src/zx/controller.rs
index 2245083..fd9c3fe 100644
--- a/rustzx-core/src/zx/controller.rs
+++ b/rustzx-core/src/zx/controller.rs
@@ -1,7 +1,7 @@
-//! Contains ZX Spectrum System contrller (like ula or so) of emulator
+//! Contains ZX Spectrum System controller (like ula or so) of emulator
 use crate::{
     error::Error,
-    host::{Host, HostContext, IoExtender},
+    host::{DebugInterface, Host, HostContext, IoExtender},
     settings::RustzxSettings,
     utils::screen::bitmap_line_addr,
     zx::{
@@ -30,7 +30,7 @@ use crate::zx::video::border::ZXBorder;
 
 /// ZX System controller
 pub(crate) struct ZXController<H: Host> {
-    // parts of ZX Spectum.
+    // parts of ZX Spectrum.
     pub machine: ZXMachine,
     pub memory: ZXMemory,
     pub screen: ZXScreen<H::FrameBuffer>,
@@ -40,6 +40,7 @@ pub(crate) struct ZXController<H: Host> {
     pub kempston: Option<KempstonJoy>,
     pub mouse: Option<KempstonMouse>,
     pub io_extender: Option<H::IoExtender>,
+    pub debug_interface: Option<H::DebugInterface>,
     #[cfg(feature = "sound")]
     pub mixer: ZXMixer,
     pub keyboard: [u8; 8],
@@ -50,7 +51,7 @@ pub(crate) struct ZXController<H: Host> {
     pub border_color: ZXColor,
     // clocls count from frame start
     frame_clocks: usize,
-    // frames count, which passed during emulation invokation
+    // frames count, which passed during emulation invocation
     passed_frames: usize,
     events: EmulationEvents,
     paging_enabled: bool,
@@ -108,6 +109,7 @@ impl<H: Host> ZXController<H> {
             kempston,
             mouse,
             io_extender: None,
+            debug_interface: None,
             #[cfg(feature = "sound")]
             mixer,
             keyboard: [0xFF; 8],
@@ -312,14 +314,9 @@ impl<H: Host> ZXController<H> {
         self.mixer.new_frame();
     }
 
-    /// Clears all detected
-    pub fn clear_events(&mut self) {
-        self.events.clear();
-    }
-
-    /// Returns last events
-    pub fn events(&self) -> EmulationEvents {
-        self.events
+    /// Collects all events from the last emulation step
+    pub fn take_events(&mut self) -> EmulationEvents {
+        self.events.take()
     }
 
     /// Returns true if all frame clocks has been passed
@@ -433,6 +430,11 @@ impl<H: Host> Z80Bus for ZXController<H> {
                 self.events |= EmulationEvents::TAPE_FAST_LOAD_TRIGGER_DETECTED;
             }
         }
+        if let Some(debug) = &mut self.debug_interface {
+            if debug.check_pc_breakpoint(addr) {
+                self.events |= EmulationEvents::PC_BREAKPOINT;
+            }
+        }
     }
 
     /// read data without taking onto account contention
@@ -450,7 +452,7 @@ impl<H: Host> Z80Bus for ZXController<H> {
         }
     }
 
-    /// Cahnges internal state on clocks count change (emualtion processing)
+    /// Changes internal state on clocks count change (emulation processing)
     fn wait_internal(&mut self, clk: usize) {
         self.frame_clocks += clk;
         if let Err(e) = self.tape.process_clocks(clk) {
diff --git a/rustzx-core/src/zx/events.rs b/rustzx-core/src/zx/events.rs
index 7a5951b..b869fb4 100644
--- a/rustzx-core/src/zx/events.rs
+++ b/rustzx-core/src/zx/events.rs
@@ -1,14 +1,20 @@
 use bitflags::bitflags;
 
 bitflags! {
+    /// Emulation events
     #[derive(Default)]
     pub struct EmulationEvents: u8 {
+        /// Set when tape fast load trigger is detected
         const TAPE_FAST_LOAD_TRIGGER_DETECTED = 0b00000001;
+        /// Set when PC breakpoint is reached
+        const PC_BREAKPOINT = 0b00000010;
     }
 }
 
 impl EmulationEvents {
-    pub fn clear(&mut self) {
+    pub fn take(&mut self) -> Self {
+        let events = *self;
         self.bits = 0;
+        events
     }
 }
diff --git a/rustzx-core/src/zx/memory.rs b/rustzx-core/src/zx/memory.rs
index a8604a1..124ecc7 100644
--- a/rustzx-core/src/zx/memory.rs
+++ b/rustzx-core/src/zx/memory.rs
@@ -42,7 +42,7 @@ pub struct ZXMemory {
 }
 
 impl ZXMemory {
-    /// Returns new Memory with coresponding rom and ram types
+    /// Returns new Memory with corresponding rom and ram types
     pub fn new(rom_type: RomType, ram_type: RamType) -> ZXMemory {
         let ram_size;
         let mem_map;
@@ -70,20 +70,27 @@ impl ZXMemory {
 
     /// Returns value form memory
     pub fn read(&self, addr: u16) -> u8 {
-        let page = self.map[(addr as usize) / PAGE_SIZE];
-        let addr_rel = addr as usize % PAGE_SIZE;
+        let (page, offset) = self.paged_address(addr);
         match page {
-            Page::Rom(page) => self.rom[(page as usize) * PAGE_SIZE + addr_rel],
-            Page::Ram(page) => self.ram[(page as usize) * PAGE_SIZE + addr_rel],
+            Page::Rom(page) => self.rom[(page as usize) * PAGE_SIZE + offset],
+            Page::Ram(page) => self.ram[(page as usize) * PAGE_SIZE + offset],
         }
     }
 
-    /// Writes value to memory
+    /// Writes value to writable memory
     pub fn write(&mut self, addr: u16, value: u8) {
-        let page = self.map[(addr as usize) / PAGE_SIZE];
-        let addr_rel = addr as usize % PAGE_SIZE;
+        let (page, offset) = self.paged_address(addr);
         if let Page::Ram(page) = page {
-            self.ram[(page as usize) * PAGE_SIZE + addr_rel] = value;
+            self.ram[(page as usize) * PAGE_SIZE + offset] = value;
+        }
+    }
+
+    /// Writes to memory space, overriding even ROM routines. Useful for testing and ROM poke's
+    pub(crate) fn force_write(&mut self, addr: u16, value: u8) {
+        let (page, offset) = self.paged_address(addr);
+        match page {
+            Page::Ram(page) => self.ram[(page as usize) * PAGE_SIZE + offset] = value,
+            Page::Rom(page) => self.rom[(page as usize) * PAGE_SIZE + offset] = value,
         }
     }
 
@@ -142,4 +149,11 @@ impl ZXMemory {
         let shift = page as usize * PAGE_SIZE;
         &self.ram[shift..shift + PAGE_SIZE]
     }
+
+    /// Calculates [Page] and local offset from memory address
+    fn paged_address(&self, addr: u16) -> (Page, usize) {
+        let page = self.map[(addr as usize) / PAGE_SIZE];
+        let offset = addr as usize % PAGE_SIZE;
+        (page, offset)
+    }
 }
diff --git a/rustzx-core/src/zx/mouse/kempston.rs b/rustzx-core/src/zx/mouse/kempston.rs
index 4f1eda1..db0e1b3 100644
--- a/rustzx-core/src/zx/mouse/kempston.rs
+++ b/rustzx-core/src/zx/mouse/kempston.rs
@@ -1,7 +1,7 @@
 const WHEEL_MASK: u8 = 0xF0;
 const WHEEL_SHIFT: usize = 4;
 
-// non_exhaustive allows to restrict struct instantiation only to `Default::deafault`
+// non_exhaustive allows to restrict struct instantiation only to `Default::defaault`
 #[non_exhaustive]
 pub(crate) struct KempstonMouse {
     pub buttons_port: u8,
diff --git a/rustzx-core/src/zx/sound/beeper.rs b/rustzx-core/src/zx/sound/beeper.rs
index d4679fd..ede8d4f 100644
--- a/rustzx-core/src/zx/sound/beeper.rs
+++ b/rustzx-core/src/zx/sound/beeper.rs
@@ -20,7 +20,7 @@ impl SampleGenerator<f64> for ZXBeeper {
         // - Beeper intentionally made produce only positive half-wave 0..0.5
         // range instead of -0.25..0.25) because of current emulator lack of
         // dc filtering.
-        // - Beeper only produces a quater of available sample
+        // - Beeper only produces a quarter of available sample
         // range because relatively to AY chip, square wave of a beeper is
         // too loud
 
diff --git a/rustzx-core/src/zx/sound/mixer.rs b/rustzx-core/src/zx/sound/mixer.rs
index fce6a40..ffb7732 100644
--- a/rustzx-core/src/zx/sound/mixer.rs
+++ b/rustzx-core/src/zx/sound/mixer.rs
@@ -1,4 +1,4 @@
-//! Module implemets zx spectrum audio devices mixer
+//! Module implements zx spectrum audio devices mixer
 use crate::zx::{
     constants::FPS,
     sound::{
diff --git a/rustzx-core/src/zx/sound/sample.rs b/rustzx-core/src/zx/sound/sample.rs
index fb2a9d1..49a883f 100644
--- a/rustzx-core/src/zx/sound/sample.rs
+++ b/rustzx-core/src/zx/sound/sample.rs
@@ -40,7 +40,7 @@ where
         self
     }
 
-    /// multiplies self channels separatly
+    /// multiplies self channels separately
     pub fn mul(&mut self, val_left: T, val_right: T) -> &mut Self {
         self.left *= val_left;
         self.right *= val_right;
diff --git a/rustzx-core/src/zx/tape/mod.rs b/rustzx-core/src/zx/tape/mod.rs
index e17cc57..8386b66 100644
--- a/rustzx-core/src/zx/tape/mod.rs
+++ b/rustzx-core/src/zx/tape/mod.rs
@@ -29,7 +29,7 @@ pub trait TapeImpl {
     fn can_fast_load(&self) -> bool;
     /// Returns byte of current block or `None` if block has ended
     fn next_block_byte(&mut self) -> Result<Option<u8>>;
-    /// Loads next block. Retruns false if end of the tape is reached
+    /// Loads next block. Returns false if end of the tape is reached
     fn next_block(&mut self) -> Result<bool>;
     /// Returns current tape (`ear`) bit
     fn current_bit(&self) -> bool;
diff --git a/rustzx-core/src/zx/tape/tap.rs b/rustzx-core/src/zx/tape/tap.rs
index 65a3e7e..482c75e 100644
--- a/rustzx-core/src/zx/tape/tap.rs
+++ b/rustzx-core/src/zx/tape/tap.rs
@@ -32,7 +32,7 @@ pub struct Tap<A: LoadableAsset + SeekableAsset> {
     state: TapeState,
     prev_state: TapeState,
     buffer: [u8; BUFFER_SIZE],
-    bufer_offset: usize,
+    buffer_offset: usize,
     block_bytes_read: usize,
     current_block_size: Option<usize>,
     tape_ended: bool,
@@ -50,7 +50,7 @@ impl<A: LoadableAsset + SeekableAsset> Tap<A> {
             curr_bit: false,
             curr_byte: 0x00,
             buffer: [0u8; BUFFER_SIZE],
-            bufer_offset: 0,
+            buffer_offset: 0,
             block_bytes_read: 0,
             current_block_size: None,
             delay: 0,
@@ -76,13 +76,14 @@ impl<A: LoadableAsset + SeekableAsset> TapeImpl for Tap<A> {
                 return Ok(None);
             }
 
-            let mut buffer_read_pos = self.block_bytes_read - self.bufer_offset;
+            let mut buffer_read_pos = self.block_bytes_read - self.buffer_offset;
 
             // Read new buffer if required
             if buffer_read_pos >= BUFFER_SIZE {
-                let bytes_to_read = (block_size - self.bufer_offset - BUFFER_SIZE).min(BUFFER_SIZE);
+                let bytes_to_read =
+                    (block_size - self.buffer_offset - BUFFER_SIZE).min(BUFFER_SIZE);
                 self.asset.read_exact(&mut self.buffer[0..bytes_to_read])?;
-                self.bufer_offset += BUFFER_SIZE;
+                self.buffer_offset += BUFFER_SIZE;
                 buffer_read_pos = 0;
             }
 
@@ -120,7 +121,7 @@ impl<A: LoadableAsset + SeekableAsset> TapeImpl for Tap<A> {
         self.asset
             .read_exact(&mut self.buffer[0..block_bytes_to_read])?;
 
-        self.bufer_offset = 0;
+        self.buffer_offset = 0;
         self.block_bytes_read = 0;
         self.current_block_size = Some(block_size);
 
@@ -264,7 +265,7 @@ impl<A: LoadableAsset + SeekableAsset> TapeImpl for Tap<A> {
         self.curr_bit = false;
         self.curr_byte = 0x00;
         self.block_bytes_read = 0;
-        self.bufer_offset = 0;
+        self.buffer_offset = 0;
         self.current_block_size = None;
         self.delay = 0;
         self.asset.seek(SeekFrom::Start(0))?;
diff --git a/rustzx-core/src/zx/video/border.rs b/rustzx-core/src/zx/video/border.rs
index 0b9b7e2..dd42f18 100644
--- a/rustzx-core/src/zx/video/border.rs
+++ b/rustzx-core/src/zx/video/border.rs
@@ -66,7 +66,7 @@ impl<FB: FrameBuffer> ZXBorder<FB> {
     /// and bool value, which signals end of frame
     fn next_border_pixel(&self, clocks: usize) -> (usize, usize, bool) {
         let specs = self.machine.specs();
-        // begining of the first line (first pixel timing minus border lines
+        // beginning of the first line (first pixel timing minus border lines
         // minus left border columns)
         let clocks_origin = specs.clocks_first_pixel
             - 8 * BORDER_ROWS * specs.clocks_line
@@ -82,7 +82,7 @@ impl<FB: FrameBuffer> ZXBorder<FB> {
         // so, next pixel will be current + 2
         let mut pixel = ((clocks % specs.clocks_line) + 1) * PIXELS_PER_CLOCK;
         // if beam out of screen on horizontal pos.
-        // pixel - 2 bacause we added 2 on prev line
+        // pixel - 2 because we added 2 on prev line
         if pixel - PIXELS_PER_CLOCK >= SCREEN_WIDTH {
             // first pixel of next line
             pixel = 0;
diff --git a/rustzx-core/src/zx/video/screen.rs b/rustzx-core/src/zx/video/screen.rs
index 80c74a0..6c50b07 100644
--- a/rustzx-core/src/zx/video/screen.rs
+++ b/rustzx-core/src/zx/video/screen.rs
@@ -178,7 +178,7 @@ impl<FB: FrameBuffer> ZXScreen<FB> {
                     );
                 }
             }
-            // cahnge last block to current
+            // change last block to current
             self.last_blocks = blocks;
         }
     }
diff --git a/rustzx-utils/Cargo.toml b/rustzx-utils/Cargo.toml
index 3bba001..62f756d 100644
--- a/rustzx-utils/Cargo.toml
+++ b/rustzx-utils/Cargo.toml
@@ -1,15 +1,16 @@
 [package]
 name = "rustzx-utils"
-version = "0.15.0"
-authors = ["The RustZX contributors", "Vladyslav Nikonov <mail@pacmancoder.xyz>"]
 description = "Additional helpers for rustzx-core based emulators"
-repository = "https://github.com/rustzx/rustzx"
 keywords = ["rustzx", "emulator", "no_std"]
-license = "MIT"
-edition = "2018"
+
+version.workspace = true
+license.workspace = true
+edition.workspace = true
+authors.workspace = true
+repository.workspace = true
 
 [dependencies]
-rustzx-core = { version = "0.15", path = "../rustzx-core" }
+rustzx-core = { workspace = true }
 log = { version = "0.4", optional = true }
 flate2 = { version = "1.0", optional = true }
 
diff --git a/rustzx-z80/Cargo.toml b/rustzx-z80/Cargo.toml
index 74e239f..c4906bf 100644
--- a/rustzx-z80/Cargo.toml
+++ b/rustzx-z80/Cargo.toml
@@ -1,12 +1,13 @@
 [package]
 name = "rustzx-z80"
-version = "0.15.0"
-authors = ["The RustZX contributors", "Vladyslav Nikonov <mail@pacmancoder.xyz>"]
 description = "Z80 CPU emulator library"
-repository = "https://github.com/rustzx/rustzx"
 keywords = ["emulator", "z80", "no_std"]
-license = "MIT"
-edition = "2018"
+
+version.workspace = true
+license.workspace = true
+edition.workspace = true
+authors.workspace = true
+repository.workspace = true
 
 [dev-dependencies]
 paste = "1.0"
diff --git a/rustzx-z80/src/bus.rs b/rustzx-z80/src/bus.rs
index 826127f..31b354b 100644
--- a/rustzx-z80/src/bus.rs
+++ b/rustzx-z80/src/bus.rs
@@ -40,7 +40,7 @@ pub trait Z80Bus {
     fn read_io(&mut self, port: u16) -> u8;
     // Method for writing to io port.
     fn write_io(&mut self, port: u16, data: u8);
-    /// Provided metod to write word, LSB first (clk - clocks per byte)
+    /// Provided method to write word, LSB first (clk - clocks per byte)
     fn write_word(&mut self, addr: u16, data: u16, clk: usize) {
         let [l, h] = data.to_le_bytes();
         self.write(addr, l, clk);
@@ -53,7 +53,7 @@ pub trait Z80Bus {
         u16::from_le_bytes([l, h])
     }
     /// Reads value from bus during interrupt.
-    /// mutable bacause on interrupt read some internal system attributes may be changed
+    /// mutable because on interrupt read some internal system attributes may be changed
     fn read_interrupt(&mut self) -> u8;
     /// Method, invoked by Z80 in case of RETI instruction. Default implementation is empty
     fn reti(&mut self);
diff --git a/rustzx-z80/src/cpu.rs b/rustzx-z80/src/cpu.rs
index 2985c66..00c49c5 100644
--- a/rustzx-z80/src/cpu.rs
+++ b/rustzx-z80/src/cpu.rs
@@ -107,6 +107,8 @@ impl Z80 {
 
     fn handle_interrupt(&mut self, bus: &mut impl Z80Bus) {
         if bus.nmi_active() {
+            // q resets during interrupt
+            self.regs.clear_q();
             // Release halt line on the bus
             if self.halted {
                 bus.halt(false);
@@ -126,6 +128,8 @@ impl Z80 {
             self.regs.inc_r();
             // 5 + 3 + 3 = 11 clocks
         } else if bus.int_active() && self.regs.get_iff1() {
+            // q resets during interrupt
+            self.regs.clear_q();
             // Release halt line on the bus
             if self.halted {
                 bus.halt(false);
@@ -143,9 +147,6 @@ impl Z80 {
 
                     // 3 + 3 + 7 = 13 clocks
                     bus.wait_internal(7);
-
-                    // mem_ptr is set to PC
-                    self.regs.set_mem_ptr(self.regs.get_pc());
                 }
                 // jump using interrupt vector
                 IntMode::Im2 => {
@@ -157,11 +158,10 @@ impl Z80 {
                     self.regs.set_pc(addr);
                     bus.wait_internal(7);
                     // 3 + 3 + 3 + 3 + 7 = 19 clocks
-
-                    // mem_ptr is set to PC
-                    self.regs.set_mem_ptr(self.regs.get_pc());
                 }
             }
+            // mem_ptr is set to PC
+            self.regs.set_mem_ptr(self.regs.get_pc());
         }
     }
 
@@ -174,6 +174,14 @@ impl Z80 {
             // allow interrupts again
             self.skip_interrupt = false;
         };
+
+        // Actions to be performed before any opcode execution
+        let before_execute_opcode = |cpu: &mut Self| {
+            // Save Q register value from previous emulation step, which is later used to
+            // properly calculate flags in some instructions
+            cpu.regs.step_q();
+        };
+
         let byte1 = if self.active_prefix != Prefix::None {
             let tmp = self.active_prefix.to_byte().unwrap();
             self.active_prefix = Prefix::None;
@@ -195,28 +203,33 @@ impl Z80 {
                             self.skip_interrupt = true;
                         }
                         Prefix::CB => {
+                            before_execute_opcode(self);
                             execute_bits(self, bus, prefix_single);
                         }
                         Prefix::None => {
                             let opcode = Opcode::from_byte(byte2);
+                            before_execute_opcode(self);
                             execute_normal(self, bus, opcode, prefix_single);
                         }
                     };
                 }
                 Prefix::CB => {
                     // opcode will be read in the called
+                    before_execute_opcode(self);
                     execute_bits(self, bus, Prefix::None);
                 }
                 Prefix::ED => {
                     let byte2 = self.fetch_byte(bus, 4);
                     self.regs.inc_r();
                     let opcode = Opcode::from_byte(byte2);
+                    before_execute_opcode(self);
                     execute_extended(self, bus, opcode);
                 }
                 _ => unreachable!(),
             };
         } else {
             let opcode = Opcode::from_byte(byte1);
+            before_execute_opcode(self);
             execute_normal(self, bus, opcode, Prefix::None);
         };
         // Allow bus implementation to process pc-based events
diff --git a/rustzx-z80/src/lib.rs b/rustzx-z80/src/lib.rs
index bcce89c..7358768 100644
--- a/rustzx-z80/src/lib.rs
+++ b/rustzx-z80/src/lib.rs
@@ -15,6 +15,6 @@ pub use codegen::{CodeGenerator, CodegenMemorySpace};
 pub use cpu::{IntMode, Z80};
 pub use opcode::{Opcode, Prefix};
 pub use registers::{
-    RegName16, RegName8, Regs, FLAG_CARRY, FLAG_F3, FLAG_F5, FLAG_HALF_CARRY, FLAG_PV, FLAG_SIGN,
-    FLAG_SUB, FLAG_ZERO,
+    flag_pos, RegName16, RegName8, Regs, FLAG_CARRY, FLAG_F3, FLAG_F5, FLAG_HALF_CARRY, FLAG_PV,
+    FLAG_SIGN, FLAG_SUB, FLAG_ZERO,
 };
diff --git a/rustzx-z80/src/opcode/group_bits.rs b/rustzx-z80/src/opcode/group_bits.rs
index 031d2e6..516cd9e 100644
--- a/rustzx-z80/src/opcode/group_bits.rs
+++ b/rustzx-z80/src/opcode/group_bits.rs
@@ -24,9 +24,7 @@ pub fn execute_bits(cpu: &mut Z80, bus: &mut impl Z80Bus, prefix: Prefix) {
         let displacement = cpu.fetch_byte(bus, 3) as i8;
         let addr = cpu
             .regs
-            .get_reg_16_with_displacement(RegName16::HL.with_prefix(prefix), displacement);
-
-        cpu.regs.set_mem_ptr(addr);
+            .build_addr_with_offset(RegName16::HL.with_prefix(prefix), displacement);
         let opcode = Opcode::from_byte(bus.read(cpu.regs.get_pc(), 3));
         bus.wait_loop(cpu.regs.get_pc(), 2);
         cpu.regs.inc_pc();
diff --git a/rustzx-z80/src/opcode/group_extended.rs b/rustzx-z80/src/opcode/group_extended.rs
index 62688d4..eb154b4 100644
--- a/rustzx-z80/src/opcode/group_extended.rs
+++ b/rustzx-z80/src/opcode/group_extended.rs
@@ -3,6 +3,7 @@ use crate::{
         execute_cpi_cpd, execute_ini_ind, execute_ldi_ldd, execute_outi_outd, execute_pop_16,
         BlockDir, Opcode, Prefix,
     },
+    registers::BlockIoOpcode,
     smallnum::{U1, U2, U3},
     tables::{
         lookup16_r12, lookup8_r12, HALF_CARRY_ADD_TABLE, HALF_CARRY_SUB_TABLE, OVERFLOW_ADD_TABLE,
@@ -95,15 +96,14 @@ pub fn execute_extended(cpu: &mut Z80, bus: &mut impl Z80Bus, opcode: Opcode) {
                         // LD (nn), rp[p]
                         U1::N0 => {
                             bus.write_word(addr, cpu.regs.get_reg_16(reg), 3);
-                            cpu.regs.set_mem_ptr(addr.wrapping_add(1));
                         }
                         // LD rp[p], (nn)
                         U1::N1 => {
                             let val = bus.read_word(addr, 3);
                             cpu.regs.set_reg_16(reg, val);
-                            cpu.regs.set_mem_ptr(addr.wrapping_add(1));
                         }
                     }
+                    cpu.regs.set_mem_ptr(addr.wrapping_add(1));
                 }
                 // NEG (A = 0 - A)
                 U3::N4 => {
@@ -243,6 +243,7 @@ pub fn execute_extended(cpu: &mut Z80, bus: &mut impl Z80Bus, opcode: Opcode) {
                                 bus.wait_loop(cpu.regs.get_de().wrapping_sub(1), 5);
                                 cpu.regs.dec_pc();
                                 cpu.regs.dec_pc();
+                                cpu.regs.update_flags_block_mem_cycle();
                             };
                         }
                         // LDDR
@@ -254,6 +255,7 @@ pub fn execute_extended(cpu: &mut Z80, bus: &mut impl Z80Bus, opcode: Opcode) {
                                 bus.wait_loop(cpu.regs.get_de().wrapping_add(1), 5);
                                 cpu.regs.dec_pc();
                                 cpu.regs.dec_pc();
+                                cpu.regs.update_flags_block_mem_cycle();
                             };
                         }
                         // NOP
@@ -282,6 +284,7 @@ pub fn execute_extended(cpu: &mut Z80, bus: &mut impl Z80Bus, opcode: Opcode) {
                                 bus.wait_loop(cpu.regs.get_hl().wrapping_sub(1), 5);
                                 cpu.regs.dec_pc();
                                 cpu.regs.dec_pc();
+                                cpu.regs.update_flags_block_mem_cycle();
                             };
                         }
                         // CPDR
@@ -292,6 +295,7 @@ pub fn execute_extended(cpu: &mut Z80, bus: &mut impl Z80Bus, opcode: Opcode) {
                                 bus.wait_loop(cpu.regs.get_hl().wrapping_add(1), 5);
                                 cpu.regs.dec_pc();
                                 cpu.regs.dec_pc();
+                                cpu.regs.update_flags_block_mem_cycle();
                             };
                         }
                         // NOP
@@ -304,25 +308,31 @@ pub fn execute_extended(cpu: &mut Z80, bus: &mut impl Z80Bus, opcode: Opcode) {
                 U3::N2 => {
                     match opcode.y {
                         // INI
-                        U3::N4 => execute_ini_ind(cpu, bus, BlockDir::Inc),
+                        U3::N4 => {
+                            execute_ini_ind(cpu, bus, BlockDir::Inc);
+                        }
                         // IND
-                        U3::N5 => execute_ini_ind(cpu, bus, BlockDir::Dec),
+                        U3::N5 => {
+                            execute_ini_ind(cpu, bus, BlockDir::Dec);
+                        }
                         // INIR
                         U3::N6 => {
-                            execute_ini_ind(cpu, bus, BlockDir::Inc);
+                            let m = execute_ini_ind(cpu, bus, BlockDir::Inc);
                             if cpu.regs.get_reg_8(RegName8::B) != 0 {
                                 bus.wait_loop(cpu.regs.get_hl().wrapping_sub(1), 5);
                                 cpu.regs.dec_pc();
                                 cpu.regs.dec_pc();
+                                cpu.regs.update_flags_block_io_cycle(BlockIoOpcode::Inir, m);
                             };
                         }
                         // INDR
                         U3::N7 => {
-                            execute_ini_ind(cpu, bus, BlockDir::Dec);
+                            let m = execute_ini_ind(cpu, bus, BlockDir::Dec);
                             if cpu.regs.get_reg_8(RegName8::B) != 0 {
                                 bus.wait_loop(cpu.regs.get_hl().wrapping_add(1), 5);
                                 cpu.regs.dec_pc();
                                 cpu.regs.dec_pc();
+                                cpu.regs.update_flags_block_io_cycle(BlockIoOpcode::Indr, m);
                             };
                         }
                         // NOP
@@ -335,25 +345,31 @@ pub fn execute_extended(cpu: &mut Z80, bus: &mut impl Z80Bus, opcode: Opcode) {
                 U3::N3 => {
                     match opcode.y {
                         // OUTI
-                        U3::N4 => execute_outi_outd(cpu, bus, BlockDir::Inc),
+                        U3::N4 => {
+                            execute_outi_outd(cpu, bus, BlockDir::Inc);
+                        }
                         // OUTD
-                        U3::N5 => execute_outi_outd(cpu, bus, BlockDir::Dec),
+                        U3::N5 => {
+                            execute_outi_outd(cpu, bus, BlockDir::Dec);
+                        }
                         // OTIR
                         U3::N6 => {
-                            execute_outi_outd(cpu, bus, BlockDir::Inc);
+                            let m = execute_outi_outd(cpu, bus, BlockDir::Inc);
                             if cpu.regs.get_reg_8(RegName8::B) != 0 {
                                 bus.wait_loop(cpu.regs.get_bc(), 5);
                                 cpu.regs.dec_pc();
                                 cpu.regs.dec_pc();
+                                cpu.regs.update_flags_block_io_cycle(BlockIoOpcode::Otir, m);
                             };
                         }
                         // OTDR
                         U3::N7 => {
-                            execute_outi_outd(cpu, bus, BlockDir::Dec);
+                            let m = execute_outi_outd(cpu, bus, BlockDir::Dec);
                             if cpu.regs.get_reg_8(RegName8::B) != 0 {
                                 bus.wait_loop(cpu.regs.get_bc(), 5);
                                 cpu.regs.dec_pc();
                                 cpu.regs.dec_pc();
+                                cpu.regs.update_flags_block_io_cycle(BlockIoOpcode::Otdr, m);
                             };
                         }
                         // NOP
diff --git a/rustzx-z80/src/opcode/group_nonprefixed.rs b/rustzx-z80/src/opcode/group_nonprefixed.rs
index 3688234..c1b36ae 100644
--- a/rustzx-z80/src/opcode/group_nonprefixed.rs
+++ b/rustzx-z80/src/opcode/group_nonprefixed.rs
@@ -263,7 +263,7 @@ pub fn execute_normal(cpu: &mut Z80, bus: &mut impl Z80Bus, opcode: Opcode, pref
                     bus.wait_loop(cpu.regs.get_pc(), 5);
                     cpu.regs.inc_pc();
                     cpu.regs
-                        .get_reg_16_with_displacement(RegName16::HL.with_prefix(prefix), d)
+                        .build_addr_with_offset(RegName16::HL.with_prefix(prefix), d)
                 };
                 // read data
                 data = bus.read(addr, 3);
@@ -322,7 +322,7 @@ pub fn execute_normal(cpu: &mut Z80, bus: &mut impl Z80Bus, opcode: Opcode, pref
                     // LD (IX+d/ IY+d)
                     let d = cpu.fetch_byte(bus, 3) as i8;
                     cpu.regs
-                        .get_reg_16_with_displacement(RegName16::HL.with_prefix(prefix), d)
+                        .build_addr_with_offset(RegName16::HL.with_prefix(prefix), d)
                 };
                 LoadOperand8::Indirect(addr)
             };
@@ -462,7 +462,9 @@ pub fn execute_normal(cpu: &mut Z80, bus: &mut impl Z80Bus, opcode: Opcode, pref
                 U3::N6 => {
                     let data = cpu.regs.get_acc();
                     let mut flags = cpu.regs.get_flags() & (FLAG_ZERO | FLAG_PV | FLAG_SIGN);
-                    flags |= F3F5_TABLE[data as usize] | FLAG_CARRY;
+                    flags |= ((cpu.regs.get_last_q() ^ cpu.regs.get_flags()) | data)
+                        & (FLAG_F3 | FLAG_F5);
+                    flags |= FLAG_CARRY;
                     cpu.regs.set_flags(flags);
                 }
                 // CCF Invert carry flag
@@ -471,7 +473,8 @@ pub fn execute_normal(cpu: &mut Z80, bus: &mut impl Z80Bus, opcode: Opcode, pref
                     let data = cpu.regs.get_acc();
                     let old_carry = (cpu.regs.get_flags() & FLAG_CARRY) != 0;
                     let mut flags = cpu.regs.get_flags() & (FLAG_SIGN | FLAG_PV | FLAG_ZERO);
-                    flags |= F3F5_TABLE[data as usize];
+                    flags |= ((cpu.regs.get_last_q() ^ cpu.regs.get_flags()) | data)
+                        & (FLAG_F3 | FLAG_F5);
                     flags |= old_carry as u8 * FLAG_HALF_CARRY;
                     flags |= (!old_carry) as u8 * FLAG_CARRY;
                     cpu.regs.set_flags(flags);
@@ -497,8 +500,9 @@ pub fn execute_normal(cpu: &mut Z80, bus: &mut impl Z80Bus, opcode: Opcode, pref
                 let d = bus.read(cpu.regs.get_pc(), 3) as i8;
                 bus.wait_loop(cpu.regs.get_pc(), 5);
                 cpu.regs.inc_pc();
+
                 cpu.regs
-                    .get_reg_16_with_displacement(RegName16::HL.with_prefix(prefix), d)
+                    .build_addr_with_offset(RegName16::HL.with_prefix(prefix), d)
             };
             cpu.regs
                 .set_reg_8(RegName8::from_u3(opcode.y).unwrap(), bus.read(src_addr, 3));
@@ -515,7 +519,7 @@ pub fn execute_normal(cpu: &mut Z80, bus: &mut impl Z80Bus, opcode: Opcode, pref
                 bus.wait_loop(cpu.regs.get_pc(), 5);
                 cpu.regs.inc_pc();
                 cpu.regs
-                    .get_reg_16_with_displacement(RegName16::HL.with_prefix(prefix), d)
+                    .build_addr_with_offset(RegName16::HL.with_prefix(prefix), d)
             };
             bus.write(
                 dst_addr,
@@ -532,8 +536,6 @@ pub fn execute_normal(cpu: &mut Z80, bus: &mut impl Z80Bus, opcode: Opcode, pref
             let to = RegName8::from_u3(opcode.y).unwrap().with_prefix(prefix);
             let tmp = cpu.regs.get_reg_8(from);
             cpu.regs.set_reg_8(to, tmp);
-            cpu.regs
-                .set_mem_ptr((cpu.regs.get_reg_8(to).wrapping_add(1)) as u16 | ((tmp as u16) << 8));
         }
         // ---------------------------------
         // [0b10yyyzzz] instruction section
@@ -553,7 +555,7 @@ pub fn execute_normal(cpu: &mut Z80, bus: &mut impl Z80Bus, opcode: Opcode, pref
                     cpu.regs.inc_pc();
                     let addr = cpu
                         .regs
-                        .get_reg_16_with_displacement(RegName16::HL.with_prefix(prefix), d);
+                        .build_addr_with_offset(RegName16::HL.with_prefix(prefix), d);
                     bus.read(addr, 3)
                 }
             };
@@ -596,10 +598,11 @@ pub fn execute_normal(cpu: &mut Z80, bus: &mut impl Z80Bus, opcode: Opcode, pref
                 // [0b11pp1001] instruction group (assorted)
                 U1::N1 => {
                     match opcode.p {
-                        // RET ; return
+                        // RET
                         // [0b11001001] : C9;
                         U2::N0 => {
                             execute_pop_16(cpu, bus, RegName16::PC, 3);
+                            cpu.regs.set_mem_ptr(cpu.regs.get_pc());
                             // Clocks: 10
                         }
                         // EXX
@@ -755,6 +758,7 @@ pub fn execute_normal(cpu: &mut Z80, bus: &mut impl Z80Bus, opcode: Opcode, pref
                             cpu.regs.inc_pc();
                             execute_push_16(cpu, bus, RegName16::PC, 3);
                             cpu.regs.set_pc(addr);
+                            cpu.regs.set_mem_ptr(cpu.regs.get_pc());
                         }
                         // [0b11011101] : DD
                         U2::N1 => {
@@ -786,8 +790,8 @@ pub fn execute_normal(cpu: &mut Z80, bus: &mut impl Z80Bus, opcode: Opcode, pref
             // CALL y*8
             cpu.regs
                 .set_reg_16(RegName16::PC, (opcode.y.as_byte() as u16) * 8);
-            // 4 + 1 + 3 + 3 = 11
             cpu.regs.set_mem_ptr(cpu.regs.get_pc());
+            // Clocks: 4 + 1 + 3 + 3 = 11
         }
     };
 }
diff --git a/rustzx-z80/src/opcode/internal_block.rs b/rustzx-z80/src/opcode/internal_block.rs
index d34ba69..8317dce 100644
--- a/rustzx-z80/src/opcode/internal_block.rs
+++ b/rustzx-z80/src/opcode/internal_block.rs
@@ -68,8 +68,10 @@ pub fn execute_cpi_cpd(cpu: &mut Z80, bus: &mut impl Z80Bus, dir: BlockDir) -> b
     tmp == 0
 }
 
-/// ini/ind instruction group
-pub fn execute_ini_ind(cpu: &mut Z80, bus: &mut impl Z80Bus, dir: BlockDir) {
+/// INI/IND instruction group
+///
+/// Returns last written value at (HL)
+pub fn execute_ini_ind(cpu: &mut Z80, bus: &mut impl Z80Bus, dir: BlockDir) -> u8 {
     bus.wait_no_mreq(cpu.regs.get_ir(), 1);
     let src = bus.read_io(cpu.regs.get_bc());
     bus.write(cpu.regs.get_hl(), src, 3);
@@ -97,10 +99,13 @@ pub fn execute_ini_ind(cpu: &mut Z80, bus: &mut impl Z80Bus, dir: BlockDir) {
     // Parity of (k & 7) xor B is PV flag
     flags |= PARITY_TABLE[((k & 0x07) ^ b) as usize];
     cpu.regs.set_flags(flags);
+
+    src
 }
 
-/// outi/outd instruction group
-pub fn execute_outi_outd(cpu: &mut Z80, bus: &mut impl Z80Bus, dir: BlockDir) {
+/// OUTI/OUTD instruction group
+/// Returns last read value at (HL)
+pub fn execute_outi_outd(cpu: &mut Z80, bus: &mut impl Z80Bus, dir: BlockDir) -> u8 {
     bus.wait_no_mreq(cpu.regs.get_ir(), 1);
     let src = bus.read(cpu.regs.get_hl(), 3);
     let b = cpu.regs.dec_reg_8(RegName8::B);
@@ -128,4 +133,6 @@ pub fn execute_outi_outd(cpu: &mut Z80, bus: &mut impl Z80Bus, dir: BlockDir) {
     // Parity of (k & 7) xor B is PV flag
     flags |= PARITY_TABLE[((k & 0x07) ^ b) as usize];
     cpu.regs.set_flags(flags);
+
+    src
 }
diff --git a/rustzx-z80/src/opcode/types.rs b/rustzx-z80/src/opcode/types.rs
index abb2b77..97af0d7 100644
--- a/rustzx-z80/src/opcode/types.rs
+++ b/rustzx-z80/src/opcode/types.rs
@@ -50,13 +50,13 @@ pub enum BitOperand8 {
     Reg(RegName8),
 }
 
-/// Direction of address cahange in block functions
+/// Direction of address change in block functions
 pub enum BlockDir {
     Inc,
     Dec,
 }
 
-/// Opcode, devided in parts
+/// Opcode, divided in parts
 /// ```text
 /// xxyyyzzz
 /// xxppqzzz
diff --git a/rustzx-z80/src/registers.rs b/rustzx-z80/src/registers.rs
index f0650eb..9e4d4f2 100644
--- a/rustzx-z80/src/registers.rs
+++ b/rustzx-z80/src/registers.rs
@@ -2,6 +2,7 @@
 use crate::{
     opcode::Prefix,
     smallnum::{U2, U3},
+    tables::PARITY_TABLE,
 };
 
 pub const FLAG_CARRY: u8 = 0b00000001;
@@ -13,6 +14,11 @@ pub const FLAG_F5: u8 = 0b00100000;
 pub const FLAG_ZERO: u8 = 0b01000000;
 pub const FLAG_SIGN: u8 = 0b10000000;
 
+// Returns flag position in binary. Useful for making binary shifts in code more obvious.
+pub const fn flag_pos(flag: u8) -> u8 {
+    flag.trailing_zeros() as u8
+}
+
 /// 8-bit register names
 #[derive(Clone,Copy)]
 #[rustfmt::skip]
@@ -115,7 +121,10 @@ impl RegName16 {
 pub struct Regs {
     pc: u16,
     sp: u16,
+    // Obscure Z80 feature which affects how F3/F5 flags are set in `BIT n, (HL)` instruction
     mem_ptr: u16,
+    // Obscure Z80 feature which affects how F3/F5 flags are set in `CCF` and `SCF` instructions
+    q: u8, last_q: u8,
     ixh: u8, ixl: u8,
     iyh: u8, iyl: u8,
     r: u8,
@@ -245,9 +254,11 @@ impl Regs {
         self.set_reg_16(reg, data)
     }
 
-    pub(crate) fn get_reg_16_with_displacement(&self, reg: RegName16, displacement: i8) -> u16 {
-        let word = self.get_reg_16(reg);
-        (word as i32).wrapping_add(displacement as i32) as u16
+    pub(crate) fn build_addr_with_offset(&mut self, reg: RegName16, displacement: i8) -> u16 {
+        let word = (self.get_reg_16(reg) as i32).wrapping_add(displacement as i32) as u16;
+        // Any (REG + d) access changes MEMPTR to calculated value
+        self.set_mem_ptr(word);
+        word
     }
 
     pub fn get_pc(&self) -> u16 {
@@ -273,6 +284,19 @@ impl Regs {
         self.mem_ptr
     }
 
+    pub fn step_q(&mut self) {
+        self.last_q = self.q;
+        self.q = 0;
+    }
+
+    pub fn clear_q(&mut self) {
+        self.q = 0;
+    }
+
+    pub fn get_last_q(&self) -> u8 {
+        self.last_q
+    }
+
     pub fn dec_pc(&mut self) -> u16 {
         self.pc = self.pc.wrapping_sub(1);
         self.pc
@@ -280,7 +304,7 @@ impl Regs {
 
     /// Displaces program counter with signed value
     pub fn shift_pc(&mut self, displacement: i8) -> u16 {
-        self.pc = self.get_reg_16_with_displacement(RegName16::PC, displacement);
+        self.pc = (self.pc as i32).wrapping_add(displacement as i32) as u16;
         self.pc
     }
 
@@ -388,6 +412,7 @@ impl Regs {
 
     pub fn set_flags(&mut self, value: u8) -> u8 {
         self.f = value;
+        self.q = value;
         self.f
     }
 
@@ -503,4 +528,79 @@ impl Regs {
         core::mem::swap(&mut self.h, &mut self.h_alt);
         core::mem::swap(&mut self.l, &mut self.l_alt);
     }
+
+    /// Sets F3 and F5 flags to 11th and and 15th bit of PC respectively. This obscurity is used by
+    /// block memory operations when performing repeated execution cycle
+    pub fn update_flags_block_mem_cycle(&mut self) {
+        self.f &= !(FLAG_F3 | FLAG_F5);
+        self.f |= (self.pc >> 8) as u8 & (FLAG_F3 | FLAG_F5);
+    }
+
+    /// Obscure logic for changing flags after block io opcode iteration,
+    /// which changes F3, F5, HF and PV.
+    ///
+    /// Implementation was derived based on the following
+    /// research: https://github.com/MrKWatkins/ZXSpectrumNextTests/tree/develop/Tests/ZX48_ZX128/Z80BlockInstructionFlags
+    ///
+    /// ### Original calculation algorithm:
+    /// M is the value written to or read from the I/O port == (HL), Co/Lo/Bo are "output" values of
+    /// registers C/L/B
+    /// ```text
+    /// if instruction == INIR
+    ///     T = M + ((Co + 1) & 0xFF)
+    /// else if instruction == INDR
+    ///     T = M + ((Co - 1) & 0xFF)
+    ///     WARNING: to verify the "& 0xFF" part (ie. bit-width of (C-1) intermediate) one would
+    ///     need to read value 00 from port 00, which is not possible on regular ZX machine with
+    ///     common peripherals -> giving up.
+    /// else if (instruction == OTIR) || (instruction == OTDR)
+    ///     T = M + Lo
+    ///
+    /// TMP = Bo + (NF ? -CF : CF);
+    /// HF = (TMP ^ Bo).4;
+    /// PV = ((T & 7) ^ Bo ^ (TMP & 7)).parity
+    /// ```
+    /// ### RustZX notes
+    /// RustZX code improves further, removing need for ternary operator during
+    /// TMP calculation
+    pub fn update_flags_block_io_cycle(&mut self, opcode: BlockIoOpcode, m: u8) {
+        let t = match opcode {
+            BlockIoOpcode::Inir => m.wrapping_add(self.c.wrapping_add(1)),
+            BlockIoOpcode::Indr => m.wrapping_add(self.c.wrapping_sub(1)),
+            BlockIoOpcode::Otir | BlockIoOpcode::Otdr => m.wrapping_add(self.l),
+        };
+
+        let cf = self.f & FLAG_CARRY;
+        let b = self.b;
+        let f = self.f;
+        // Explanation of faster "TMP = Bo + (NF ? -CF : CF)" calculation logic without branch:
+        //
+        // Basically, when CF is 1, it either stays 1 if NF is 0, or subtracted by 2 if NF
+        // is 1 (Which makes -CF part) - NF and CF shofts will produce bit at position 1 which will
+        // result in either 2 or 0.
+        //
+        // When CF is 0, no mater NF result will be 0
+        let tmp = b.wrapping_add(cf.wrapping_sub(
+            (f >> flag_pos(FLAG_SIGN.wrapping_sub(1)))
+                & (cf << flag_pos(FLAG_CARRY.wrapping_add(1))),
+        ));
+        // HF = (TMP ^ Bo).4;
+        let hf = (tmp ^ b) & FLAG_HALF_CARRY;
+        // PV = ((T & 7) ^ Bo ^ (TMP & 7)).parity
+        let pv = PARITY_TABLE[((t & 0x07) ^ b ^ (tmp & 0x07)) as usize];
+
+        // Set flags
+        self.f &= !(FLAG_HALF_CARRY | FLAG_PV | FLAG_F3 | FLAG_F5);
+        let f3f5 = (self.pc >> 8) as u8 & (FLAG_F3 | FLAG_F5);
+        self.f |= f3f5 | hf | pv;
+        // Instruction changes F, therefore update Q
+        self.q = self.f;
+    }
+}
+
+pub enum BlockIoOpcode {
+    Inir,
+    Indr,
+    Otir,
+    Otdr,
 }
diff --git a/rustzx/Cargo.toml b/rustzx/Cargo.toml
index 3cc0309..7488be4 100644
--- a/rustzx/Cargo.toml
+++ b/rustzx/Cargo.toml
@@ -1,32 +1,34 @@
 [package]
 name = "rustzx"
-version = "0.15.0"
-authors = ["The RustZX contributors", "Vladyslav Nikonov <mail@pacmancoder.xyz>"]
 description = "ZX Spectrum emulator"
-repository = "https://github.com/rustzx/rustzx"
 readme = "../README.md"
 keywords = ["emulator", "game", "z80"]
-license = "MIT"
-edition = "2018"
+
+version.workspace = true
+license.workspace = true
+edition.workspace = true
+authors.workspace = true
+repository.workspace = true
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
 sdl2 = { version = "0.35", features = ["unsafe_textures", "bundled", "static-link"] }
-rustzx-core = { version = "0.15", path =  "../rustzx-core", features = ["full"] }
-rustzx-utils = { version = "0.15", path =  "../rustzx-utils", features = ["std"] }
+rustzx-core = { workspace = true, features = ["full"] }
+rustzx-utils = { workspace = true, features = ["std"] }
 log = "0.4"
 anyhow = "1.0"
 structopt = "0.3"
 strum = { version = "0.22", default-features = false, features = ["derive", "std"] }
 simple_logger = "2"
-cpal = { version = "0.13", default-features = false, optional = true }
+cpal = { version = "0.15", default-features = false, optional = true }
+ringbuf = { version = "0.3", optional = true }
 
 [features]
 default = ["sound-cpal"]
-# cpal requries ALSA development headers on Linux, if this feature
+# cpal requires ALSA development headers on Linux, if this feature
 # is disabled, RustZX will be built only with SDL audio backend support
-sound-cpal = ["cpal"]
+sound-cpal = ["cpal", "ringbuf"]
 
 
 #[dev-dependencies]
diff --git a/rustzx/src/app/events/events_sdl.rs b/rustzx/src/app/events/events_sdl.rs
index 4848074..becb44f 100644
--- a/rustzx/src/app/events/events_sdl.rs
+++ b/rustzx/src/app/events/events_sdl.rs
@@ -19,7 +19,7 @@ use sdl2::{
     EventPump,
 };
 
-/// Represents SDL Envets bakend
+/// Represents SDL Envets backend
 pub struct EventsSdl {
     event_pump: EventPump,
     mouse: MouseUtil,
@@ -77,7 +77,7 @@ impl EventsSdl {
         }
     }
 
-    /// returns ZX Spectum key form scancode of None if not found
+    /// returns ZX Spectrum key form scancode of None if not found
     fn scancode_to_zxkey_event(&self, scancode: Option<Scancode>, pressed: bool) -> Option<Event> {
         let zxkey_event = match scancode? {
             // FEFE
@@ -261,7 +261,7 @@ impl EventDevice for EventsSdl {
                         .or_else(|| self.scancode_to_compound_key_event(scancode, pressed))
                 }
                 SdlEvent::MouseMotion { xrel, yrel, .. } => {
-                    // Change of direction  requires counter reset to elimiate lag
+                    // Change of direction  requires counter reset to eliminate lag
                     if self.mouse_x_counter.signum() != xrel.signum() {
                         self.mouse_x_counter = xrel;
                     } else {
@@ -273,7 +273,7 @@ impl EventDevice for EventsSdl {
                         self.mouse_y_counter += yrel;
                     }
 
-                    // Depending on sensitivity, diffent distance is required to move
+                    // Depending on sensitivity, different distance is required to move
                     // kempston mouse
                     let ticks_to_move =
                         sensitivity_to_mouse_counter_ticks(self.mouse_sensitivity) as i32;
diff --git a/rustzx/src/app/rustzx.rs b/rustzx/src/app/rustzx.rs
index 0a9b064..3617fd7 100644
--- a/rustzx/src/app/rustzx.rs
+++ b/rustzx/src/app/rustzx.rs
@@ -146,7 +146,8 @@ impl RustzxApp {
             let emulator_dt = self
                 .emulator
                 .emulate_frames(MAX_FRAME_TIME)
-                .map_err(|e| anyhow!("Emulation step failed: {:#?}", e))?;
+                .map_err(|e| anyhow!("Emulation step failed: {:#?}", e))?
+                .duration;
             // if sound enabled sound ganeration allowed then move samples to sound thread
             if let Some(ref mut snd) = self.snd {
                 // if can be turned off even on speed change, so check it everytime
@@ -214,8 +215,8 @@ impl RustzxApp {
                     Event::MouseMove { x, y } => {
                         self.emulator.send_mouse_pos_diff(x, y);
                     }
-                    Event::MouseButton(buton, pressed) => {
-                        self.emulator.send_mouse_button(buton, pressed);
+                    Event::MouseButton(button, pressed) => {
+                        self.emulator.send_mouse_button(button, pressed);
                     }
                     Event::MouseWheel(direction) => {
                         self.emulator.send_mouse_wheel(direction);
@@ -231,7 +232,7 @@ impl RustzxApp {
             let emulation_dt = frame_start.elapsed();
             if emulation_dt < frame_target_dt {
                 let wait_koef = if self.emulator.have_sound() { 9 } else { 10 };
-                // sleep untill frame sync
+                // sleep until frame sync
                 thread::sleep((frame_target_dt - emulation_dt) * wait_koef / 10);
             };
             // get exceed clocks and use them on next iteration
diff --git a/rustzx/src/app/settings.rs b/rustzx/src/app/settings.rs
index 9f9fe9b..3a3b02c 100644
--- a/rustzx/src/app/settings.rs
+++ b/rustzx/src/app/settings.rs
@@ -32,7 +32,7 @@ pub struct Settings {
     pub machine: ZXMachine,
     /// Set emulation speed at emualtor start-up. Can be specified as deciamal non-zero
     /// value or as a special value `MAX` to run emulator as fast as possible
-    #[structopt(long, default_value = "1", parse(try_from_str = emualtion_speed_from_str))]
+    #[structopt(long, default_value = "1", parse(try_from_str = emulation_speed_from_str))]
     pub speed: EmulationMode,
     /// Disable fast tape loading
     #[structopt(long = "nofastload")]
@@ -110,7 +110,7 @@ fn machine_from_str(s: &str) -> Result<ZXMachine, anyhow::Error> {
     }
 }
 
-fn emualtion_speed_from_str(s: &str) -> Result<EmulationMode, anyhow::Error> {
+fn emulation_speed_from_str(s: &str) -> Result<EmulationMode, anyhow::Error> {
     match s.to_lowercase().as_str() {
         "max" => Ok(EmulationMode::Max),
         s => {
diff --git a/rustzx/src/app/sound/mod.rs b/rustzx/src/app/sound/mod.rs
index 5ae6898..42cb21b 100644
--- a/rustzx/src/app/sound/mod.rs
+++ b/rustzx/src/app/sound/mod.rs
@@ -19,3 +19,9 @@ pub trait SoundDevice {
     /// Return selected device sample rate
     fn sample_rate(&self) -> usize;
 }
+
+pub fn ringbuf_size_from_sample_rate(sample_rate: usize) -> usize {
+    // Around 5 frames of buffering is available
+    const RINGBUF_LENGTH_MS: usize = 100;
+    (sample_rate as f32 * (RINGBUF_LENGTH_MS as f32 / 1000f32)) as usize
+}
diff --git a/rustzx/src/app/sound/sound_cpal.rs b/rustzx/src/app/sound/sound_cpal.rs
index a8eb87e..dc5e30d 100644
--- a/rustzx/src/app/sound/sound_cpal.rs
+++ b/rustzx/src/app/sound/sound_cpal.rs
@@ -1,18 +1,17 @@
 //! Real Audio SDL backend
 use crate::app::{
     settings::Settings,
-    sound::{SoundDevice, ZXSample, CHANNEL_COUNT},
+    sound::{ringbuf_size_from_sample_rate, SoundDevice, ZXSample, CHANNEL_COUNT},
 };
 use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
-use std::sync::mpsc;
+use std::sync::Arc;
 
 pub struct SoundCpal {
-    tx: mpsc::Sender<ZXSample>,
+    tx: ringbuf::Producer<ZXSample, Arc<ringbuf::HeapRb<ZXSample>>>,
     sample_rate: usize,
     // Keep stream alive until Drop
     _stream: cpal::Stream,
 }
-
 impl SoundCpal {
     /// Constructs sound backend from settings
     pub fn new(settings: &Settings) -> anyhow::Result<SoundCpal> {
@@ -32,7 +31,8 @@ impl SoundCpal {
                     }
                 }
 
-                c.channels() == CHANNEL_COUNT as u16
+                // Find any stereo config
+                (c.channels() as usize % CHANNEL_COUNT == 0) && c.channels() != 0
             })
             .ok_or_else(|| {
                 anyhow::anyhow!("Sound device does not support required configuration")
@@ -46,12 +46,24 @@ impl SoundCpal {
 
         let sample_rate = config.sample_rate().0 as usize;
 
-        let (tx, rx) = mpsc::channel();
+        let ringbuf_size = ringbuf_size_from_sample_rate(sample_rate);
+        let ringbuf = ringbuf::HeapRb::<ZXSample>::new(ringbuf_size);
+        let (tx, rx) = ringbuf.split();
 
         let stream = match config.sample_format() {
             cpal::SampleFormat::I16 => create_stream::<i16>(&device, &config.into(), rx)?,
             cpal::SampleFormat::U16 => create_stream::<u16>(&device, &config.into(), rx)?,
             cpal::SampleFormat::F32 => create_stream::<f32>(&device, &config.into(), rx)?,
+            cpal::SampleFormat::I8 => create_stream::<i8>(&device, &config.into(), rx)?,
+            cpal::SampleFormat::I32 => create_stream::<i32>(&device, &config.into(), rx)?,
+            cpal::SampleFormat::I64 => create_stream::<i64>(&device, &config.into(), rx)?,
+            cpal::SampleFormat::U8 => create_stream::<u8>(&device, &config.into(), rx)?,
+            cpal::SampleFormat::U32 => create_stream::<u32>(&device, &config.into(), rx)?,
+            cpal::SampleFormat::U64 => create_stream::<u64>(&device, &config.into(), rx)?,
+            cpal::SampleFormat::F64 => create_stream::<f64>(&device, &config.into(), rx)?,
+            _ => {
+                anyhow::bail!("Device has unsupported audio sample format")
+            }
         };
 
         Ok(SoundCpal {
@@ -64,7 +76,8 @@ impl SoundCpal {
 
 impl SoundDevice for SoundCpal {
     fn send_sample(&mut self, sample: ZXSample) {
-        self.tx.send(sample).unwrap();
+        // Ignore buffer overflows
+        let _ = self.tx.push(sample);
     }
 
     fn sample_rate(&self) -> usize {
@@ -75,10 +88,10 @@ impl SoundDevice for SoundCpal {
 fn create_stream<T>(
     device: &cpal::Device,
     config: &cpal::StreamConfig,
-    samples_rx: mpsc::Receiver<ZXSample>,
+    mut samples_rx: ringbuf::Consumer<ZXSample, Arc<ringbuf::HeapRb<ZXSample>>>,
 ) -> anyhow::Result<cpal::Stream>
 where
-    T: cpal::Sample,
+    T: cpal::FromSample<f32> + cpal::SizedSample,
 {
     let channels = config.channels as usize;
 
@@ -86,21 +99,25 @@ where
         config,
         move |out: &mut [T], _: &cpal::OutputCallbackInfo| {
             for frame in out.chunks_mut(channels) {
-                match samples_rx.try_recv().ok() {
+                match samples_rx.pop() {
                     Some(zx_sample) => {
-                        let left: T = cpal::Sample::from(&zx_sample.left);
-                        let right: T = cpal::Sample::from(&zx_sample.right);
+                        let left: T = cpal::Sample::from_sample(zx_sample.left);
+                        let right: T = cpal::Sample::from_sample(zx_sample.right);
                         frame[0] = left;
                         frame[1] = right;
                     }
                     None => {
-                        frame[0] = cpal::Sample::from(&0f32);
-                        frame[1] = cpal::Sample::from(&0f32);
+                        frame[0] = cpal::Sample::EQUILIBRIUM;
+                        frame[1] = cpal::Sample::EQUILIBRIUM;
                     }
                 }
+
+                // We use only first stereo channels, other channels should be silent
+                frame[2..channels].fill(cpal::Sample::EQUILIBRIUM);
             }
         },
         |_| {},
+        None,
     )?;
     stream.play()?;
     Ok(stream)
diff --git a/rustzx/src/app/sound/sound_sdl.rs b/rustzx/src/app/sound/sound_sdl.rs
index be26e57..e74be04 100644
--- a/rustzx/src/app/sound/sound_sdl.rs
+++ b/rustzx/src/app/sound/sound_sdl.rs
@@ -1,16 +1,19 @@
 use crate::{
     app::{
         settings::Settings,
-        sound::{SoundDevice, ZXSample, CHANNEL_COUNT, DEFAULT_LATENCY, DEFAULT_SAMPLE_RATE},
+        sound::{
+            ringbuf_size_from_sample_rate, SoundDevice, ZXSample, CHANNEL_COUNT, DEFAULT_LATENCY,
+            DEFAULT_SAMPLE_RATE,
+        },
     },
     backends::SDL_CONTEXT,
 };
 use sdl2::audio::{AudioCallback, AudioDevice, AudioSpecDesired};
-use std::sync::mpsc;
+use std::sync::Arc;
 
 /// Struct which used in SDL audio callback
 struct SdlCallback {
-    samples: mpsc::Receiver<ZXSample>,
+    samples: ringbuf::Consumer<ZXSample, Arc<ringbuf::HeapRb<ZXSample>>>,
 }
 
 impl AudioCallback for SdlCallback {
@@ -19,8 +22,8 @@ impl AudioCallback for SdlCallback {
     /// main callback function
     fn callback(&mut self, out: &mut [f32]) {
         for chunk in out.chunks_mut(CHANNEL_COUNT) {
-            // recieve samples from channel
-            if let Ok(sample) = self.samples.try_recv() {
+            // receive samples from channel
+            if let Some(sample) = self.samples.pop() {
                 chunk[0] = sample.left;
                 chunk[1] = sample.right;
             } else {
@@ -33,7 +36,7 @@ impl AudioCallback for SdlCallback {
 
 /// Represents SDL audio backend
 pub struct SoundSdl {
-    sender: mpsc::Sender<ZXSample>,
+    sender: ringbuf::Producer<ZXSample, Arc<ringbuf::HeapRb<ZXSample>>>,
     sample_rate: usize,
     _device: AudioDevice<SdlCallback>, // Should be alive until Drop invocation
 }
@@ -59,7 +62,10 @@ impl SoundSdl {
             channels: Some(CHANNEL_COUNT as u8),
             samples: Some(latency as u16),
         };
-        let (tx, rx) = mpsc::channel();
+        let ringbuf_size = ringbuf_size_from_sample_rate(sample_rate);
+        let ringbuf = ringbuf::HeapRb::<ZXSample>::new(ringbuf_size);
+        let (tx, rx) = ringbuf.split();
+
         let device_handle = audio
             .open_playback(None, &desired_spec, |_| SdlCallback { samples: rx })
             .map_err(|e| anyhow::anyhow!("Failed to start SDL sound stream: {}", e))?;
@@ -75,7 +81,8 @@ impl SoundSdl {
 
 impl SoundDevice for SoundSdl {
     fn send_sample(&mut self, sample: ZXSample) {
-        self.sender.send(sample).unwrap();
+        // Ignore sample push errors
+        let _ = self.sender.push(sample);
     }
 
     fn sample_rate(&self) -> usize {
diff --git a/rustzx/src/app/video/mod.rs b/rustzx/src/app/video/mod.rs
index 81acc48..8a05419 100644
--- a/rustzx/src/app/video/mod.rs
+++ b/rustzx/src/app/video/mod.rs
@@ -29,13 +29,13 @@ impl Rect {
     }
 }
 
-/// provides video functionality trough rela backend to emulator
+/// provides video functionality through real backend to emulator
 pub trait VideoDevice {
     /// generates and returns texture handle
     fn gen_texture(&mut self, width: u32, height: u32) -> TextureInfo;
     /// changes window title
     fn set_title(&mut self, title: &str);
-    /// udpates texture data
+    /// updates texture data
     fn update_texture(&mut self, tex: TextureInfo, buffer: &[u8]);
     /// starts render block
     fn begin(&mut self);
diff --git a/rustzx/src/app/video/video_sdl.rs b/rustzx/src/app/video/video_sdl.rs
index 2010794..8443681 100644
--- a/rustzx/src/app/video/video_sdl.rs
+++ b/rustzx/src/app/video/video_sdl.rs
@@ -36,7 +36,7 @@ impl VideoSdl {
                 .position_centered()
                 .opengl()
                 .build()
-                .expect("[ERROR] Sdl window buil fail");
+                .expect("[ERROR] Sdl window build fail");
             let renderer = window
                 .into_canvas()
                 .present_vsync()
diff --git a/rustzx/src/host/mod.rs b/rustzx/src/host/mod.rs
index 3bd936a..2ad0bdf 100644
--- a/rustzx/src/host/mod.rs
+++ b/rustzx/src/host/mod.rs
@@ -4,7 +4,8 @@ use anyhow::{anyhow, bail, Context};
 use frame_buffer::{FrameBufferContext, RgbaFrameBuffer};
 use rustzx_core::{
     host::{
-        FrameBuffer, Host, HostContext, RomFormat, RomSet, Screen, Snapshot, StubIoExtender, Tape,
+        FrameBuffer, Host, HostContext, RomFormat, RomSet, Screen, Snapshot, StubDebugInterface,
+        StubIoExtender, Tape,
     },
     zx::machine::ZXMachine,
 };
@@ -22,6 +23,7 @@ pub struct AppHost;
 
 impl Host for AppHost {
     type Context = AppHostContext;
+    type DebugInterface = StubDebugInterface;
     type EmulationStopwatch = InstantStopwatch;
     type FrameBuffer = RgbaFrameBuffer;
     type IoExtender = StubIoExtender;
diff --git a/typos.toml b/typos.toml
new file mode 100644
index 0000000..61c3649
--- /dev/null
+++ b/typos.toml
@@ -0,0 +1,9 @@
+[files]
+extend-exclude = [
+    # Ignore third-party software comment typos
+    "**/assets/*.z80",
+]
+
+[default.extend-words]
+# DAA is a valid Z80 instruction word
+DAA = "DAA"
diff --git a/vtx/Cargo.toml b/vtx/Cargo.toml
index fd323bf..c2a7cdd 100644
--- a/vtx/Cargo.toml
+++ b/vtx/Cargo.toml
@@ -1,12 +1,13 @@
 [package]
 name = "vtx"
-version = "0.15.0"
 description = "VTX sound format parsing and playback library"
-authors = ["The RustZX contributors", "Vladyslav Nikonov <mail@pacmancoder.xyz>"]
-edition = "2018"
 keywords = ["ay", "sound", "vtx", "parser"]
-license = "MIT"
-repository = "https://github.com/rustzx/rustzx"
+
+version.workspace = true
+license.workspace = true
+edition.workspace = true
+authors.workspace = true
+repository.workspace = true
 
 [dependencies]
 delharc = { version = "0.3", default-features = false }
@@ -14,7 +15,7 @@ thiserror = "1.0"
 byteorder = "1.4"
 num-traits = { version = "0.2", default-features = false }
 num-derive = "0.3"
-aym = { version = "0.15", path = "../aym" }
+aym = { workspace = true }
 
 [dev-dependencies]
 expect-test = "1.1"
diff --git a/vtx/src/lib.rs b/vtx/src/lib.rs
index ad939f0..0f68fee 100644
--- a/vtx/src/lib.rs
+++ b/vtx/src/lib.rs
@@ -91,7 +91,7 @@ pub enum VtxError {
     Io(#[from] std::io::Error),
     #[error("Invalid VTX header: {}", message)]
     InvalidHeader { message: &'static str },
-    #[error("Faield to decode lh5 compressed data")]
+    #[error("Failed to decode lh5 compressed data")]
     DecompressFailure,
 }
 
diff --git a/vtx/src/player.rs b/vtx/src/player.rs
index fca0573..11fe3cc 100644
--- a/vtx/src/player.rs
+++ b/vtx/src/player.rs
@@ -200,7 +200,7 @@ mod tests {
     }
 
     #[test]
-    fn precise_player_wroks_normally() {
+    fn precise_player_works_normally() {
         // This test also doubles as test for `aym` crate with AymPrecise backend selected
         expect![[r#"65127ad0e493b23b43e838b6281d7f8d07a47549efd0c9d1eda3d3ccaf0b7e5c"#]].assert_eq(
             &generate_samples_hash_for_vtx::<aym::AymPrecise>(include_bytes!("test/csoon.vtx")),
diff --git a/vtx/vtx-bin/Cargo.toml b/vtx/vtx-bin/Cargo.toml
index ebc4b91..1e9f904 100644
--- a/vtx/vtx-bin/Cargo.toml
+++ b/vtx/vtx-bin/Cargo.toml
@@ -1,19 +1,20 @@
 [package]
 name = "vtx-bin"
 description = "VTX sound format toolbox"
-version = "0.15.0"
-authors = ["The RustZX contributors", "Vladyslav Nikonov <mail@pacmancoder.xyz>"]
-edition = "2018"
-license = "MIT"
-repository = "https://github.com/rustzx/rustzx"
 keywords = ["emulator", "ay", "sound", "vtx"]
 
+version.workspace = true
+license.workspace = true
+edition.workspace = true
+authors.workspace = true
+repository.workspace = true
+
 [[bin]]
 name = "vtx"
 path = "src/main.rs"
 
 [dependencies]
-vtx = { version = "0.15", path = ".." }
+vtx = { workspace = true }
 structopt = "0.3"
 wav = "1.0"
 anyhow = "1.0"


Test patch:
diff --git a/.github/workflows/test-rustzx-z80.yml b/.github/workflows/test-rustzx-z80.yml
index a691683..d62576d 100644
--- a/.github/workflows/test-rustzx-z80.yml
+++ b/.github/workflows/test-rustzx-z80.yml
@@ -63,3 +63,18 @@ jobs:
         with:
           command: test
           args: -p rustzx-z80 -- --ignored --skip zexall_alu8r zexall
+  z80test:
+    name: "z80test-based tests"
+    runs-on: ubuntu-20.04
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: stable
+          override: true
+      - name: Test
+        uses: actions-rs/cargo@v1
+        with:
+          command: test
+          args: -p rustzx-test -- --nocapture --ignored z80full z80ccf z80memptr
diff --git a/rustzx-test/Cargo.toml b/rustzx-test/Cargo.toml
index 9833638..c3ef486 100644
--- a/rustzx-test/Cargo.toml
+++ b/rustzx-test/Cargo.toml
@@ -1,10 +1,14 @@
 [package]
 name = "rustzx-test"
-version = "0.15.0"
-edition = "2018"
 publish = false
 description = "RustZX emulator integration tests"
 
+version.workspace = true
+license.workspace = true
+edition.workspace = true
+authors.workspace = true
+repository.workspace = true
+
 [[bench]]
 name = "z80test"
 harness = false
@@ -15,8 +19,8 @@ base64 = "0.13"
 expect-test = "1.1"
 nanoid = "0.4"
 png = "0.16"
-rustzx-core = { version = "0.15", path = "../rustzx-core", features = ["full"] }
-rustzx-utils = { version = "0.15", path =  "../rustzx-utils", features = ["std"] }
+rustzx-core = { workspace = true, features = ["full"] }
+rustzx-utils = { workspace = true, features = ["std"] }
 sha2 = "0.9"
 wav = "1.0"
 
diff --git a/rustzx-test/benches/z80test.rs b/rustzx-test/benches/z80test.rs
index 8f94c5e..7341e4f 100644
--- a/rustzx-test/benches/z80test.rs
+++ b/rustzx-test/benches/z80test.rs
@@ -7,7 +7,7 @@ fn z80test_setup() -> RustZXTester {
     let settings = presets::settings_48k();
     let mut tester = RustZXTester::new("z80full", settings);
     tester.load_tap("z80full.tap.gz");
-    tester.send_keypress(ZXKey::Enter);
+    tester.disable_scroll_message();
 
     tester
 }
diff --git a/rustzx-test/src/framework.rs b/rustzx-test/src/framework.rs
index f071d30..aa5ef01 100644
--- a/rustzx-test/src/framework.rs
+++ b/rustzx-test/src/framework.rs
@@ -1,16 +1,17 @@
 use expect_test::Expect;
 use rustzx_core::{
     host::{
-        BufferCursor, FrameBuffer, FrameBufferSource, Host, HostContext, IoExtender, RomFormat,
-        RomSet, Snapshot, Tape,
+        BufferCursor, DebugInterface, FrameBuffer, FrameBufferSource, Host, HostContext,
+        IoExtender, RomFormat, RomSet, Snapshot, Tape,
     },
+    poke,
     zx::{
         keys::ZXKey,
         machine::ZXMachine,
         sound::ay::ZXAYMode,
         video::colors::{ZXBrightness, ZXColor},
     },
-    EmulationMode, Emulator, RustzxSettings,
+    EmulationMode, EmulationStopReason, Emulator, RustzxSettings,
 };
 use rustzx_utils::{
     io::{DynamicAsset, GzipAsset},
@@ -145,7 +146,7 @@ impl DebugPort {
         String::from_utf8(s).expect("Invalid debug port stdout")
     }
 
-    pub fn take_bufer(&mut self) -> Vec<u8> {
+    pub fn take_buffer(&mut self) -> Vec<u8> {
         Vec::from(std::mem::take(&mut self.stdout))
     }
 
@@ -155,10 +156,43 @@ impl DebugPort {
     }
 }
 
+/// A simple debug interface that allows to set breakpoints and check if they were hit.
+#[derive(Default)]
+struct TestDebugInterface {
+    breakpoints: std::collections::HashSet<u16>,
+    last_hit: Option<u16>,
+}
+
+impl TestDebugInterface {
+    pub fn add_breakpoint(&mut self, address: u16) {
+        self.breakpoints.insert(address);
+    }
+
+    pub fn clear_breakpoints(&mut self) {
+        self.breakpoints.clear();
+        self.last_hit = None;
+    }
+
+    pub fn last_breakpoint_hit(&self) -> Option<u16> {
+        self.last_hit
+    }
+}
+
+impl DebugInterface for TestDebugInterface {
+    fn check_pc_breakpoint(&mut self, addr: u16) -> bool {
+        if self.breakpoints.contains(&addr) {
+            self.last_hit = Some(addr);
+            return true;
+        }
+        false
+    }
+}
+
 struct TesterHost;
 
 impl Host for TesterHost {
     type Context = TesterContext;
+    type DebugInterface = TestDebugInterface;
     type EmulationStopwatch = InstantStopwatch;
     type FrameBuffer = FrameContent;
     type IoExtender = DebugPort;
@@ -316,21 +350,63 @@ impl RustZXTester {
         }
     }
 
-    pub fn emulate_for(&mut self, duration: Duration) {
+    /// Sets the breakpoint and emulates until it is hit or the timeout is reached
+    pub fn emulate_until_breakpoint(&mut self, breakpoint_addr: u16, timeout: Duration) {
+        self.clear_breakpoints();
+        self.add_breakpoint(breakpoint_addr);
+
+        let result = self.emulate_for(timeout);
+        if let EmulationStopReason::Breakpoint = result {
+            // Check if last breakpoint is the one we requested
+            let last_breakpoint = self.last_breakpoint();
+            assert_eq!(
+                last_breakpoint, breakpoint_addr,
+                "Emulator stopped at breakpoint {:04X}, expected {:04X}",
+                last_breakpoint, breakpoint_addr
+            );
+
+            return;
+        }
+        panic!("Emulator failed to hit breakpoint before reaching timeout");
+    }
+
+    ///  Emulates for the given duration or until the breakpoint is hit
+    pub fn emulate_for(&mut self, duration: Duration) -> EmulationStopReason {
         let mut emulated_duration = Duration::from_secs(0);
         while emulated_duration < duration {
-            self.emulator
+            let result = self
+                .emulator
                 .emulate_frames(FRAME_HOST_DURATION_LIMIT)
                 .expect("Emulation failed");
+
             self.update_sound();
             emulated_duration += FRAME_EMULATED_DURATION;
+
+            if result.stop_reason == EmulationStopReason::Breakpoint {
+                eprintln!(
+                    "Requested {} duration, emulated for {}",
+                    duration.as_millis(),
+                    emulated_duration.as_millis()
+                );
+
+                return result.stop_reason;
+            }
         }
+        EmulationStopReason::Completed
     }
 
     pub fn emulate_frame(&mut self) {
         self.emulate_for(FRAME_EMULATED_DURATION);
     }
 
+    pub fn last_breakpoint(&mut self) -> u16 {
+        self.emulator
+            .debug_interface()
+            .expect("no breakpoints were set")
+            .last_breakpoint_hit()
+            .expect("No breapoints were triggered")
+    }
+
     pub fn compare_buffer_with_file(
         &self,
         actual: Vec<u8>,
@@ -450,7 +526,7 @@ impl RustZXTester {
                 panic!("Timeout reached when trying to sync host with target");
             }
 
-            // Try to consume incomming signal and finish sync
+            // Try to consume incoming signal and finish sync
             if self.debug_port().take_byte().is_some() {
                 break;
             }
@@ -460,6 +536,31 @@ impl RustZXTester {
     pub fn set_sync_timeout(&mut self, timeout: Duration) {
         self.sync_timeout = timeout;
     }
+
+    /// Disables the message and key press prompt after a few lines of scroll in BASIC
+    pub fn disable_scroll_message(&mut self) {
+        self.emulator.execute_poke(poke::DisableScrollMessageRom48);
+    }
+
+    pub fn add_breakpoint(&mut self, address: u16) {
+        if let Some(interface) = self.emulator.debug_interface() {
+            interface.add_breakpoint(address);
+        } else {
+            let mut interface = TestDebugInterface::default();
+            interface.add_breakpoint(address);
+            self.emulator.set_debug_interface(interface);
+        }
+    }
+
+    pub fn clear_breakpoints(&mut self) {
+        if let Some(interface) = self.emulator.debug_interface() {
+            interface.clear_breakpoints();
+        }
+    }
+
+    pub fn peek(&mut self, addr: u16) -> u8 {
+        self.emulator.peek(addr)
+    }
 }
 
 struct TestEnv;
diff --git a/rustzx-test/test_data/README.md b/rustzx-test/test_data/README.md
new file mode 100644
index 0000000..5d851c7
--- /dev/null
+++ b/rustzx-test/test_data/README.md
@@ -0,0 +1,10 @@
+# Used third-party software list
+
+All rights to the third-party binaries and sources belong to their authors.
+
+| Name | Version | Source |
+| --- | --- | --- |
+| z80test | 1.2 | https://github.com/raxoft/z80test |
+| zmakebas | 1.3 | https://github.com/z00m128/zmakebas |
+| diag_rom | 56 | http://www.retroleum.co.uk/diagrom/?stub=diagrom |
+| Z80 Block Flags Test | e05310a | [https://github.com/MrKWatkins/ZXSpectrumNextTests](https://github.com/MrKWatkins/ZXSpectrumNextTests/tree/develop/Tests/ZX48_ZX128/Z80BlockInstructionFlags) |
diff --git a/rustzx-test/test_data/z80bltst.tap.gz b/rustzx-test/test_data/z80bltst.tap.gz
new file mode 100644
index 0000000..e6462e0
Binary files /dev/null and b/rustzx-test/test_data/z80bltst.tap.gz differ
diff --git a/rustzx-test/test_data/z80ccf.tap.gz b/rustzx-test/test_data/z80ccf.tap.gz
new file mode 100644
index 0000000..bd6b3d2
Binary files /dev/null and b/rustzx-test/test_data/z80ccf.tap.gz differ
diff --git a/rustzx-test/test_data/z80full.tap.gz b/rustzx-test/test_data/z80full.tap.gz
index d065b65..c655de5 100644
Binary files a/rustzx-test/test_data/z80full.tap.gz and b/rustzx-test/test_data/z80full.tap.gz differ
diff --git a/rustzx-test/test_data/z80memptr.tap.gz b/rustzx-test/test_data/z80memptr.tap.gz
new file mode 100644
index 0000000..b196f38
Binary files /dev/null and b/rustzx-test/test_data/z80memptr.tap.gz differ
diff --git a/rustzx-test/tests/z80test.rs b/rustzx-test/tests/z80test.rs
new file mode 100644
index 0000000..860fed4
--- /dev/null
+++ b/rustzx-test/tests/z80test.rs
@@ -0,0 +1,133 @@
+use expect_test::expect;
+use rustzx_test::framework::{presets, RustZXTester};
+use std::time::Duration;
+
+/// Address where emulator loads program from tape
+const PROG_START: u16 = 0x8000;
+
+/// Finds the address of pattern in emulator memory via peek method.
+fn search_pattern(
+    t: &mut RustZXTester,
+    pattern: &[u8],
+    min_search_range: u16,
+    max_search_range: u16,
+) -> u16 {
+    assert!(
+        min_search_range <= max_search_range,
+        "min_search_range must be less or equal to max_search_range"
+    );
+
+    assert!(
+        pattern.len() <= (max_search_range - min_search_range) as usize,
+        "Pattern is too big"
+    );
+
+    for mem_offset in min_search_range..max_search_range {
+        let mut found = true;
+        for (pattern_offset, expected) in pattern.into_iter().copied().enumerate() {
+            if t.peek(mem_offset + pattern_offset as u16) != expected {
+                found = false;
+                break;
+            }
+        }
+        if found {
+            return mem_offset;
+        }
+    }
+
+    panic!("Failed to found pattern in loaded program");
+}
+
+/// We are trying to find call opcode of the following `z80test` code (main.asm):
+/// ```text
+/// 65 | .ok    call    print                       ; print success message
+/// 66 |        db      "all tests passed.",13,0
+/// ```
+fn search_success_print_call(t: &mut RustZXTester) -> u16 {
+    // Search in first 1k of program
+    const MAX_SEARCH_BYTES: u16 = 1024;
+    const SEARCH_PATTERN: &[u8] = b"all tests passed";
+    const CALL_OPCODE_LEN: u16 = 3;
+
+    let pattern_offset =
+        search_pattern(t, SEARCH_PATTERN, PROG_START, PROG_START + MAX_SEARCH_BYTES);
+
+    assert!(
+        pattern_offset >= CALL_OPCODE_LEN,
+        "Pattern offset is too small"
+    );
+
+    pattern_offset - CALL_OPCODE_LEN
+}
+
+/// Runs tests based on z80test project tape files.
+fn run_z80_test(name: &str) {
+    const TIMEOUT_INIT: Duration = Duration::from_secs(1);
+    const TIMEOUT_TEST: Duration = Duration::from_secs(350);
+
+    let mut t = RustZXTester::new(name, presets::settings_48k_nosound());
+    // Disable scroll message to eliminate need for enter key press emulation during test run
+    t.disable_scroll_message();
+    t.load_tap(format!("{}.tap.gz", name));
+
+    // Wait until emulator loads program and jumps to PROG_START
+    t.emulate_until_breakpoint(PROG_START, TIMEOUT_INIT);
+    let success_breakpoint_addr = search_success_print_call(&mut t);
+
+    // Wait test to succeed
+    t.emulate_until_breakpoint(success_breakpoint_addr, TIMEOUT_TEST);
+}
+
+macro_rules! z80test {
+    ($($name:ident),*) => {
+        $(
+            #[test]
+            #[ignore]
+            fn $name() {
+                run_z80_test(stringify!($name));
+            }
+        )*
+    };
+}
+
+z80test!(z80full, z80ccf, z80memptr);
+
+/// This code finds the following opcodes addresses
+/// ```text
+/// 294 | .exit: di             ; F3
+/// 295 | .sp+1: ld    sp,0     ; 0x31 0x00  0x00
+/// ```
+fn search_exit_address(t: &mut RustZXTester) -> u16 {
+    // Opcodes should be around 0x200 offset, lets search for 0x400 bytes
+    const SEARCH_BYTES: u16 = 1024;
+    const SEARCH_PATTERN: &[u8] = &[0xF3, 0x31, 0x00, 0x00];
+    search_pattern(t, SEARCH_PATTERN, PROG_START, PROG_START + SEARCH_BYTES)
+}
+
+/// Tests z80 block instructions flags via z80bltst.tap from ZXSpectrumNextTests project
+#[test]
+fn z80_block_instruction_flags() {
+    const TIMEOUT_INIT: Duration = Duration::from_secs(1);
+    const TIMEOUT_TEST: Duration = Duration::from_secs(30);
+
+    let mut t = RustZXTester::new(
+        "z80_block_instruction_flags",
+        presets::settings_48k_nosound(),
+    );
+    t.load_tap("z80bltst.tap.gz");
+
+    // Wait until emulator loads program and jumps to PROG_START
+    t.emulate_until_breakpoint(PROG_START, TIMEOUT_INIT);
+    let exit_breakpoint_addr = search_exit_address(&mut t);
+
+    // Wait test to succeed
+    t.emulate_until_breakpoint(exit_breakpoint_addr, TIMEOUT_TEST);
+
+    // Prepare screen to be compared against expected frame hash
+    t.emulate_frame();
+
+    t.expect_screen(
+        "frame1",
+        expect![[r#"KnD/3IYvk72FtwHyxhia4wNWGwcJKgmnxZfTmaebTlo="#]],
+    );
+}
diff --git a/rustzx-z80/tests/integration/assets/zexall.z80 b/rustzx-z80/tests/integration/assets/zexall.z80
index 740ce76..a537aa1 100644
--- a/rustzx-z80/tests/integration/assets/zexall.z80
+++ b/rustzx-z80/tests/integration/assets/zexall.z80
@@ -91,7 +91,7 @@ loop:	ld	a,(hl)		; end of list ?
 	dec	hl
 	call	stt
 	jp	loop
-	
+
 done:	ld	de,msg2
 	ld	c,9
 	call	bdos
@@ -462,7 +462,7 @@ ld162:	db	0ffh		; flag mask
 	tstr	0,-1,0,0,0,0,0,0,0,0			; (16 cycles)
 	db	05fh,097h,024h,087h			; expected crc
 	tmsg	'ld hl,(nnnn)'
-	
+
 ; ld sp,(nnnn) (16 cycles)
 ld163:	db	0ffh		; flag mask
 	tstr	<0edh,07bh,low msbt,high msbt>,08dfch,057d7h,02161h,0ca18h,0c185h,027dah,083h,01eh,0f460h
@@ -478,7 +478,7 @@ ld164:	db	0ffh		; flag mask
 	tstr	0,-1,0,0,0,0,0,0,0,0			; (16 cycles)
 	db	085h,08bh,0f1h,06dh			; expected crc
 	tmsg	'ld <ix,iy>,(nnnn)'
-	
+
 ; ld (nnnn),<bc,de> (64 cycles)
 ld165:	db	0ffh		; flag mask
 	tstr	<0edh,043h,low msbt,high msbt>,01f98h,0844dh,0e8ach,0c9edh,0c95dh,08f61h,080h,03fh,0c7bfh
@@ -898,7 +898,7 @@ ncb1:	ld	a,b
 	ret	z
 	ld	a,1
 	ret
-	
+
 ; get next shifter bit in low bit of a
 shfbit:	ds	1
 shfbyt:	ds	2
@@ -924,7 +924,7 @@ nsb1:	ld	a,b
 	ret	z
 	ld	a,1
 	ret
-	
+
 
 ; clear memory at hl, bc bytes
 clrmem:	push	af
@@ -1017,7 +1017,7 @@ cntend:	pop	bc
 cntlp1:	inc	hl
 	inc	de
 	jp	cntlp
-	
+
 
 ; multi-byte shifter
 shift:	push	bc
@@ -1073,7 +1073,7 @@ test:	push	af
 	ld	b,16
 	ld	hl,msbt
 	call	hexstr
-      endif	
+      endif
 	di			; disable interrupts
 	ld	(spsav),sp	; save stack pointer
 	ld	sp,msbt+2	; point to test-case machine state
@@ -1170,7 +1170,7 @@ phex2:	push	af
 	rrca
 	call	phex1
 	pop	af
-; fall through	
+; fall through
 
 ; display low nibble in a
 phex1:	push	af
@@ -1543,4 +1543,3 @@ crctab:	db	000h,000h,000h,000h
 	db	0c3h,00ch,08eh,0a1h
 	db	05ah,005h,0dfh,01bh
 	db	02dh,002h,0efh,08dh
-