Repository: rustzx/rustzx (language: rust)

Issue:
Automatic binary release preparation from CI on master
Add a new CI step to prepare binary releases on each push to master with the higher crate version.
- Also, maybe publish to crates.io also can be automized 


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index faad5e4..2573304 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,10 +11,25 @@ env:
 
 jobs:
   build:
+    name: "Build (${{ matrix.os }}-${{ matrix.arch }})"
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        os: [ubuntu-latest, windows-latest, macos-latest]
+        include:
+          - os: ubuntu-20.04
+            os_name: linux
+            executable_suffix: ""
+            arch: amd64
+          - os: windows-2019
+            os_name: windows
+            executable_suffix: ".exe"
+            arch: amd64
+          - os: macos-10.15
+            os_name: macos
+            executable_suffix: ""
+            arch: amd64
+    env:
+      RUSTFLAGS: -D warnings
     steps:
       - uses: actions/checkout@v2
       - uses: actions-rs/toolchain@v1
@@ -43,13 +58,15 @@ jobs:
         with:
           command: build
           args: --release -p rustzx-core --verbose
-      - name: Build(rustzx-core) - Without AY
-        uses: actions-rs/cargo@v1
-        with:
-          command: build
-          args: --release -p rustzx-core --features without-ay --verbose
       - name: Test
         uses: actions-rs/cargo@v1
         with:
           command: test
           args: --workspace --all-features --verbose
+      - name: Upload artifacts
+        uses: actions/upload-artifact@v2
+        with:
+          name: "rustzx-bin-${{ matrix.os_name }}-${{ matrix.arch }}"
+          path: |
+            target/release/rustzx${{ matrix.executable_suffix }}
+            target/release/vtx${{ matrix.executable_suffix }}
diff --git a/.gitignore b/.gitignore
index 2e42fae..39d0eb8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,10 +6,14 @@
 
 # Executables
 *.exe
-
-# Backups
-*.bak
+
+# Backups
+*.bak
 
 # Generated by Cargo
 /target/
-/target-install/
\ No newline at end of file
+/target-install/
+
+# RustZX-specific
+*.rustzx.last.sna
+*.rustzx.prev.sna
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a9c8c88..af9a158 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,12 @@
+### RustZX v0.13
+- **[Feature]** Support for ZX Spectrum 128K SNA variant
+- **[Feature]** Quick save/load (`F1`/`F2` keys) via SNA
+- **[Feature]** Sinclair joysticks support
+- **[Feature]** Separate joystick keyboard layout (press `F9` to activate)
+- **[Feature]** Kempston mouse support (`--mouse`)
+- **[Fix]** Fixed invalid kempston joystick port
+- **[Fix]** Fixed missing screen update after SNA loading
+
 ### RustZX v0.12
 - **[Feature]** Now using rust-only AY sound chip library (`aym`) instead of `libayumi` C library
 - **[Feature]** Added vtx sound format parsing/playback library `vtx`
diff --git a/Cargo.lock b/Cargo.lock
index 60a8783..e2c4e23 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -51,7 +51,7 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
 
 [[package]]
 name = "aym"
-version = "0.1.0"
+version = "0.1.1"
 dependencies = [
  "libm",
  "num-traits",
@@ -519,7 +519,7 @@ checksum = "b9b1a3d5f46d53f4a3478e2be4a5a5ce5108ea58b100dcd139830eae7f79a3a1"
 
 [[package]]
 name = "rustzx"
-version = "0.12.0"
+version = "0.13.0"
 dependencies = [
  "anyhow",
  "env_logger",
@@ -531,7 +531,7 @@ dependencies = [
 
 [[package]]
 name = "rustzx-core"
-version = "0.12.0"
+version = "0.13.0"
 dependencies = [
  "aym",
  "bitflags",
@@ -743,7 +743,7 @@ checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
 
 [[package]]
 name = "vtx"
-version = "0.1.0"
+version = "0.1.1"
 dependencies = [
  "aym",
  "byteorder",
@@ -757,7 +757,7 @@ dependencies = [
 
 [[package]]
 name = "vtx-bin"
-version = "0.1.0"
+version = "0.1.1"
 dependencies = [
  "anyhow",
  "structopt",
diff --git a/README.md b/README.md
index 9f9b300..0a84556 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,9 @@ RustZX
 ===============================
 
 [![RustZX CI](https://github.com/pacmancoder/rustzx/actions/workflows/ci.yml/badge.svg)](https://github.com/pacmancoder/rustzx/actions/workflows/ci.yml)
+[![Crates.io](https://img.shields.io/crates/v/rustzx)](https://crates.io/crates/rustzx)
+[![License](https://img.shields.io/crates/l/rustzx)](https://github.com/pacmancoder/rustzx/blob/master/LICENSE.md)
+[![Discord](https://img.shields.io/discord/844696167794475009?logo=discord&logoColor=white)](https://discord.gg/aSHJh8UJre)
 
 ![logo](assets/logo_small.png)
 
@@ -16,11 +19,14 @@ ZX Spectrum emulator written in Rust
 - Perfect emulation of Z80 core
 - Highly precise AY chip emulation
 - Beeper sound emulation
-- Supported formats: TAP, SNA
+- Supported formats: TAP, SNA (both 48K and 128K version)
 - Fast loading of tap files with standard loader
 - Precise timings
 - Full border emulation
-- Joystick emulation: Kempston
+- Joystick emulation: Kempston, Sinclair
+- Kempston mouse emulation
+- Extended 128K keys emulation (arrows, backspace, caps lock)
+- Quick save/load
 - Separate `no_std` core library which can be used to port emulator
   almost anywhere.
     - Global allocator is still needed, but all dynamic
@@ -43,6 +49,7 @@ rustzx --ay test.tap # Run in 48K mode with AY sound chip
 rustzx -m128 --tape test128.tap # Run in 128K mode with tape
 rustzx --rom tester.rom -s3 # Run with custom rom and 3x screen scaling
 rustzx --nofastload test.tap # Run without fast tape loading
+rustzx --mouse test.tap # Run with Kempston mouse support
 ```
 For loading tape in 48K mode, press `j` then `Ctrl+p` twice, as on real Spectrum.
 You should see `LOAD ""` on emulator's screen, then press `Enter` (in 128K mode just press enter).
@@ -50,8 +57,29 @@ In `--nofastload` mode, press `Insert` to play the tape and `Delete` to stop
 
 If you have choppy audio, try `--sound-latency` option with bigger values.
 
-Use keys `F3 - F5` to set speed of emulation - this can be usefull when skipping some boring stuff.
-Use `F6` to display FPS in window title.
+## Default key bindings:
+- `F1` - quick save
+- `F2` - quick load
+- `F3` - set normal emulation speed
+- `F4` - set 2x emulation speed
+- `F5` - max possible emulation speed
+- `F6` - enable frame trace info
+- `F9` - enable kempston/sinclair joy keyboard layer
+- `Insert` - start tape
+- `Delete`- stop tape
+- `End` - break command
+- `Caps Lock` - caps lock command
+- `Backspace` - delete
+- `<Arrows>` - 128K arrow keys
+- `Esc` - unlock mouse (if `--mouse` is used)
+
+## In joy keyboard layer mode (F9)
+- `<Arrows>` - Kempston joy *arrows*
+- `Alt` - Kempston *fire*
+- `WASD`- Siclair Joy 1 *arrows*
+- `Caps Lock` - Sinclair Joy 1 *fire*
+- `IJKL`- Siclair Joy 2 *arrows*
+- `Enter` - Sinclair Joy 2 *fire*
 
 ## Screenshots
 ![](screenshots/rain.png)
diff --git a/aym/Cargo.toml b/aym/Cargo.toml
index 839476b..2dcdb10 100644
--- a/aym/Cargo.toml
+++ b/aym/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "aym"
-version = "0.1.0"
+version = "0.1.1"
 description = "AY/YM sound chip emulation library"
 authors = ["The RustZX contributors", "Vladyslav Nikonov <mail@pacmancoder.xyz>"]
 license = "MIT"
diff --git a/aym/src/backends/precise.rs b/aym/src/backends/precise.rs
index b7959fc..5972bc3 100644
--- a/aym/src/backends/precise.rs
+++ b/aym/src/backends/precise.rs
@@ -7,12 +7,12 @@ const DC_FILTER_SIZE: usize = 1024;
 
 #[derive(Default)]
 struct ToneChannel {
-    tone_period: usize,
-    tone_counter: usize,
+    tone_period: u16,
+    tone_counter: u16,
     tone: usize,
-    t_off: usize,
-    n_off: usize,
-    e_on: usize,
+    tone_off_bit: usize,
+    noise_off_bit: usize,
+    envelope_enabled: bool,
     volume: usize,
     pan_left: f64,
     pan_right: f64,
@@ -46,27 +46,34 @@ impl Default for DcFilter {
 /// Uses f64 for computations.
 pub struct AymPrecise {
     channels: [ToneChannel; TONE_CHANNELS],
-    noise_period: usize,
-    noise_counter: usize,
+
+    noise_period: u16,
+    noise_counter: u16,
     noise: usize,
-    envelope_counter: usize,
-    envelope_period: usize,
+
+    envelope_counter: u16,
+    envelope_period: u16,
     envelope_shape: usize,
     envelope_segment: usize,
     envelope: usize,
+
     dac_table: &'static [f64; 32],
     step: f64,
     x: f64,
     interpolator_left: Interpolator,
     interpolator_right: Interpolator,
+
     fir_left: [f64; FIR_SIZE * 2],
     fir_right: [f64; FIR_SIZE * 2],
     fir_index: usize,
+
     dc_left: DcFilter,
     dc_right: DcFilter,
     dc_index: usize,
+
     left: f64,
     right: f64,
+
     registers: [u8; AY_REGISTER_COUNT],
     dc_filter: bool,
 }
@@ -200,28 +207,33 @@ impl AymPrecise {
         }
     }
 
-    fn set_tone(&mut self, index: usize, period: usize) {
+    fn set_tone(&mut self, index: usize, period: u16) {
         let period = period & 0xFFF;
-        self.channels[index].tone_period = (period == 0) as usize | period;
+        self.channels[index].tone_period = (period == 0) as u16 | period;
     }
 
-    fn set_noise(&mut self, period: usize) {
+    fn set_noise(&mut self, period: u16) {
         self.noise_period = period & 0x1F;
     }
 
-    fn set_mixer(&mut self, index: usize, t_off: usize, n_off: usize, e_on: usize) {
-        self.channels[index].t_off = t_off & 1;
-        self.channels[index].n_off = n_off & 1;
-        self.channels[index].e_on = e_on;
+    fn set_mixer(
+        &mut self,
+        index: usize,
+        tone_enable: bool,
+        noise_enable: bool,
+        envelope_enabled: bool,
+    ) {
+        self.channels[index].tone_off_bit = (!tone_enable) as usize;
+        self.channels[index].noise_off_bit = (!noise_enable) as usize;
+        self.channels[index].envelope_enabled = envelope_enabled;
     }
 
     fn set_volume(&mut self, index: usize, volume: usize) {
         self.channels[index].volume = volume & 0x0F;
     }
 
-    fn set_envelope(&mut self, period: usize) {
-        let period = period & 0xFFFF;
-        self.envelope_period = (period == 0) as usize | period;
+    fn set_envelope(&mut self, period: u16) {
+        self.envelope_period = (period == 0) as u16 | period;
     }
 
     fn set_envelope_shape(&mut self, shape: usize) {
@@ -343,13 +355,14 @@ impl AymPrecise {
         self.left = 0.0;
         self.right = 0.0;
         for i in 0..TONE_CHANNELS {
-            let mut out =
-                (self.update_tone(i) | self.channels[i].t_off) & (noise | self.channels[i].n_off);
-            out *= if self.channels[i].e_on != 0 {
+            let mut out = (self.update_tone(i) | self.channels[i].tone_off_bit)
+                & (noise | self.channels[i].noise_off_bit);
+            out *= if self.channels[i].envelope_enabled {
                 envelope
             } else {
                 self.channels[i].volume * 2 + 1
             };
+            assert!(out < 32);
             self.left += self.dac_table[out] * self.channels[i].pan_left;
             self.right += self.dac_table[out] * self.channels[i].pan_right;
         }
@@ -358,6 +371,8 @@ impl AymPrecise {
 
 #[allow(clippy::excessive_precision)]
 fn decimate(x: &mut [f64]) -> f64 {
+    assert!(x.len() >= FIR_SIZE);
+
     let y = -0.0000046183113992051936 * (x[1] + x[191])
         + -0.00001117761640887225 * (x[2] + x[190])
         + -0.000018610264502005432 * (x[3] + x[189])
@@ -494,58 +509,58 @@ impl AymBackend for AymPrecise {
         let r = self.registers;
 
         match address {
-            0 | 1 => self.set_tone(0, u16::from_le_bytes([r[0], r[1] & 0x0f]) as usize),
-            2 | 3 => self.set_tone(1, u16::from_le_bytes([r[2], r[3] & 0x0f]) as usize),
-            4 | 5 => self.set_tone(2, u16::from_le_bytes([r[4], r[5] & 0x0f]) as usize),
-            6 => self.set_noise((r[6] & 0x1f) as usize),
+            0 | 1 => self.set_tone(0, u16::from_le_bytes([r[0], r[1] & 0x0f])),
+            2 | 3 => self.set_tone(1, u16::from_le_bytes([r[2], r[3] & 0x0f])),
+            4 | 5 => self.set_tone(2, u16::from_le_bytes([r[4], r[5] & 0x0f])),
+            6 => self.set_noise((r[6] & 0x1f) as u16),
             7 => {
                 self.set_mixer(
                     0,
-                    ((r[7] & 0x01) != 0) as usize,
-                    ((r[7] & 0x08) != 0) as usize,
-                    ((r[8] & 0x10) != 0) as usize,
+                    (r[7] & 0x01) == 0,
+                    (r[7] & 0x08) == 0,
+                    (r[8] & 0x10) != 0,
                 );
                 self.set_mixer(
                     1,
-                    ((r[7] & 0x02) != 0) as usize,
-                    ((r[7] & 0x10) != 0) as usize,
-                    ((r[9] & 0x10) != 0) as usize,
+                    (r[7] & 0x02) == 0,
+                    (r[7] & 0x10) == 0,
+                    (r[9] & 0x10) != 0,
                 );
                 self.set_mixer(
                     2,
-                    ((r[7] & 0x04) != 0) as usize,
-                    ((r[7] & 0x20) != 0) as usize,
-                    ((r[10] & 0x10) != 0) as usize,
+                    (r[7] & 0x04) == 0,
+                    (r[7] & 0x20) == 0,
+                    (r[10] & 0x10) != 0,
                 );
             }
             8 => {
                 self.set_mixer(
                     0,
-                    ((r[7] & 0x01) != 0) as usize,
-                    ((r[7] & 0x08) != 0) as usize,
-                    ((r[8] & 0x10) != 0) as usize,
+                    (r[7] & 0x01) == 0,
+                    (r[7] & 0x08) == 0,
+                    (r[8] & 0x10) != 0,
                 );
                 self.set_volume(0, (r[8] & 0x0F) as usize);
             }
             9 => {
                 self.set_mixer(
                     1,
-                    ((r[7] & 0x02) != 0) as usize,
-                    ((r[7] & 0x10) != 0) as usize,
-                    ((r[9] & 0x10) != 0) as usize,
+                    (r[7] & 0x02) == 0,
+                    (r[7] & 0x10) == 0,
+                    (r[9] & 0x10) != 0,
                 );
                 self.set_volume(1, (r[9] & 0x0F) as usize);
             }
             10 => {
                 self.set_mixer(
                     2,
-                    ((r[7] & 0x04) != 0) as usize,
-                    ((r[7] & 0x20) != 0) as usize,
-                    ((r[10] & 0x10) != 0) as usize,
+                    (r[7] & 0x04) == 0,
+                    (r[7] & 0x20) == 0,
+                    (r[10] & 0x10) != 0,
                 );
                 self.set_volume(2, (r[10] & 0x0F) as usize);
             }
-            11 | 12 => self.set_envelope(u16::from_le_bytes([r[11], r[12]]) as usize),
+            11 | 12 => self.set_envelope(u16::from_le_bytes([r[11], r[12]])),
             13 => self.set_envelope_shape((r[13] & 0x0F) as usize),
             _ => unreachable!(),
         }
diff --git a/rustzx-core/Cargo.toml b/rustzx-core/Cargo.toml
index df9654b..5c2f23c 100644
--- a/rustzx-core/Cargo.toml
+++ b/rustzx-core/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "rustzx-core"
-version = "0.12.0"
+version = "0.13.0"
 authors = ["The RustZX contributors", "Vladyslav Nikonov <mail@pacmancoder.xyz>"]
 description = "ZX Spectum emulator core library"
 repository = "https://github.com/pacmancoder/rustzx"
@@ -10,12 +10,12 @@ edition = "2018"
 
 [features]
 default = []
-full = ["ay", "precise-border", "embedded-roms"]
-without-ay = ["sound", "precise-border", "embedded-roms"]
+full = ["ay", "precise-border", "embedded-roms", "autoload"]
 precise-border = []
 embedded-roms = []
 sound = []
 ay = ["aym", "sound"]
+autoload = []
 
 [dependencies]
 bitflags = "1"
@@ -23,4 +23,4 @@ lazy_static = "1"
 displaydoc = { version = "0.2", default-features = false }
 from_variants = "0.6"
 enum_dispatch = "0.3"
-aym = { version = "0.1", path = "../aym", optional = true}
+aym = { version = "0.1.1", path = "../aym", optional = true}
diff --git a/rustzx-core/src/emulator/loaders/mod.rs b/rustzx-core/src/emulator/loaders/mod.rs
index 5a1edb6..44ad516 100644
--- a/rustzx-core/src/emulator/loaders/mod.rs
+++ b/rustzx-core/src/emulator/loaders/mod.rs
@@ -1,2 +1,1 @@
-pub mod sna;
 pub mod tap;
diff --git a/rustzx-core/src/emulator/loaders/sna.rs b/rustzx-core/src/emulator/loaders/sna.rs
deleted file mode 100644
index 8464cb1..0000000
--- a/rustzx-core/src/emulator/loaders/sna.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-use crate::{
-    emulator::Emulator,
-    host::{Host, LoadableAsset},
-    utils::{make_word, Clocks},
-    z80::{opcodes::execute_pop_16, RegName16},
-    zx::video::colors::ZXColor,
-    Result,
-};
-
-/// SNA snapshot loading function
-pub fn load_sna<H: Host>(emulator: &mut Emulator<H>, mut asset: H::SnapshotAsset) -> Result<()> {
-    const SNA_HEADER_SIZE: usize = 27;
-    let mut header = [0u8; SNA_HEADER_SIZE];
-    asset.read_exact(&mut header)?;
-
-    // i-reg
-    emulator.cpu.regs.set_i(header[0]);
-    // alt-regs
-    emulator.cpu.regs.set_hl(make_word(header[2], header[1]));
-    emulator.cpu.regs.set_de(make_word(header[4], header[3]));
-    emulator.cpu.regs.set_bc(make_word(header[6], header[5]));
-    emulator.cpu.regs.exx();
-    // af'
-    emulator.cpu.regs.set_af(make_word(header[8], header[7]));
-    emulator.cpu.regs.swap_af_alt();
-    // regs
-    emulator.cpu.regs.set_hl(make_word(header[10], header[9]));
-    emulator.cpu.regs.set_de(make_word(header[12], header[11]));
-    emulator.cpu.regs.set_bc(make_word(header[14], header[13]));
-    // index regs
-    emulator.cpu.regs.set_iy(make_word(header[16], header[15]));
-    emulator.cpu.regs.set_ix(make_word(header[18], header[17]));
-    // iff1, iff2
-    let iff = (header[19] & 0x04) != 0;
-    emulator.cpu.regs.set_iff1(iff);
-    emulator.cpu.regs.set_iff2(iff);
-    // r
-    emulator.cpu.regs.set_r(header[20]);
-    // af
-    emulator.cpu.regs.set_af(make_word(header[22], header[21]));
-    // sp
-    emulator.cpu.regs.set_sp(make_word(header[24], header[23]));
-    // interrupt mode
-    emulator.cpu.set_im(header[25] & 0x03);
-    emulator
-        .controller
-        .set_border_color(Clocks(0), ZXColor::from_bits(header[26] & 0x07));
-    // ram pages
-    for page_index in 0..3 {
-        let page = emulator.controller.memory.ram_page_data_mut(page_index);
-        asset.read_exact(page)?;
-    }
-
-    // RET
-    execute_pop_16(
-        &mut emulator.cpu,
-        &mut emulator.controller,
-        RegName16::PC,
-        Clocks(0),
-    );
-    Ok(())
-}
diff --git a/rustzx-core/src/emulator/mod.rs b/rustzx-core/src/emulator/mod.rs
index f112991..08c4850 100644
--- a/rustzx-core/src/emulator/mod.rs
+++ b/rustzx-core/src/emulator/mod.rs
@@ -1,17 +1,25 @@
 //! Platform-independent high-level Emulator interaction module
 mod loaders;
+mod snapshot;
 
 use crate::{
     error::RomLoadError,
-    host::{Host, LoadableAsset, RomFormat, RomSet, Snapshot, Tape},
+    host::{
+        DataRecorder, Host, LoadableAsset, RomFormat, RomSet, SeekableAsset, Snapshot,
+        SnapshotRecorder, Tape,
+    },
     settings::RustzxSettings,
     utils::EmulationSpeed,
     z80::Z80,
     zx::{
         controller::ZXController,
         events::EmulationEvents,
-        joy::kempston::KempstonKey,
-        keys::ZXKey,
+        joy::{
+            kempston::KempstonKey,
+            sinclair::{SinclairJoyNum, SinclairKey},
+        },
+        keys::{CompoundKey, ZXKey},
+        mouse::kempston::{KempstonMouseButton, KempstonMouseWheelDirection},
         tape::{Tap, TapeImpl},
         video::colors::ZXColor,
     },
@@ -20,6 +28,8 @@ use crate::{
 
 #[cfg(feature = "sound")]
 use crate::zx::sound::sample::SoundSample;
+#[cfg(feature = "autoload")]
+use crate::{host::BufferCursor, zx::machine::ZXMachine};
 
 use core::time::Duration;
 
@@ -45,7 +55,7 @@ impl<H: Host> Emulator<H> {
     /// `settings` - emulator settings
     pub fn new(settings: RustzxSettings, context: H::Context) -> Result<Self> {
         let speed = settings.emulation_speed;
-        let fast_load = settings.tape_fastload;
+        let fast_load = settings.tape_fastload_enabled;
         #[cfg(feature = "sound")]
         let sound_enabled = settings.sound_enabled;
 
@@ -92,9 +102,21 @@ impl<H: Host> Emulator<H> {
         }
     }
 
-    pub fn load_snapshot(&mut self, snapshot: Snapshot<H::SnapshotAsset>) -> Result<()> {
+    pub fn load_snapshot<A>(&mut self, snapshot: Snapshot<A>) -> Result<()>
+    where
+        A: LoadableAsset + SeekableAsset,
+    {
         match snapshot {
-            Snapshot::Sna(asset) => loaders::sna::load_sna(self, asset),
+            Snapshot::Sna(asset) => snapshot::sna::load(self, asset),
+        }
+    }
+
+    pub fn save_snapshot<R>(&mut self, recorder: SnapshotRecorder<R>) -> Result<()>
+    where
+        R: DataRecorder,
+    {
+        match recorder {
+            SnapshotRecorder::Sna(recorder) => snapshot::sna::save(self, recorder),
         }
     }
 
@@ -105,6 +127,16 @@ impl<H: Host> Emulator<H> {
             }
         }
 
+        #[cfg(feature = "autoload")]
+        if self.settings.autoload_enabled {
+            let snapshot = match self.settings.machine {
+                ZXMachine::Sinclair48K => &snapshot::autoload::tape::SNAPSHOT_SNA_48K,
+                ZXMachine::Sinclair128K => &snapshot::autoload::tape::SNAPSHOT_SNA_128K,
+            };
+
+            self.load_snapshot(Snapshot::Sna(BufferCursor::new(snapshot)))?;
+        }
+
         Ok(())
     }
 
@@ -151,12 +183,32 @@ impl<H: Host> Emulator<H> {
         self.controller.send_key(key, pressed);
     }
 
-    pub fn send_kempston_key(&mut self, key: KempstonKey, state: bool) {
+    pub fn send_compound_key(&mut self, key: CompoundKey, pressed: bool) {
+        self.controller.send_compound_key(key, pressed);
+    }
+
+    pub fn send_kempston_key(&mut self, key: KempstonKey, pressed: bool) {
         if let Some(joy) = &mut self.controller.kempston {
-            joy.key(key, state);
+            joy.key(key, pressed);
         }
     }
 
+    pub fn send_sinclair_key(&mut self, num: SinclairJoyNum, key: SinclairKey, pressed: bool) {
+        self.controller.send_sinclair_key(num, key, pressed);
+    }
+
+    pub fn send_mouse_button(&mut self, button: KempstonMouseButton, pressed: bool) {
+        self.controller.send_mouse_button(button, pressed);
+    }
+
+    pub fn send_mouse_wheel(&mut self, dir: KempstonMouseWheelDirection) {
+        self.controller.send_mouse_wheel(dir);
+    }
+
+    pub fn send_mouse_pos(&mut self, x: i8, y: i8) {
+        self.controller.send_mouse_pos_diff(x, y);
+    }
+
     #[cfg(feature = "sound")]
     pub fn next_audio_sample(&mut self) -> Option<SoundSample<f32>> {
         self.controller.mixer.pop()
diff --git a/rustzx-core/src/emulator/snapshot/autoload/mod.rs b/rustzx-core/src/emulator/snapshot/autoload/mod.rs
new file mode 100644
index 0000000..54f2a52
--- /dev/null
+++ b/rustzx-core/src/emulator/snapshot/autoload/mod.rs
@@ -0,0 +1,4 @@
+pub mod tape {
+    pub const SNAPSHOT_SNA_48K: &[u8] = include_bytes!("tape_48k.sna");
+    pub const SNAPSHOT_SNA_128K: &[u8] = include_bytes!("tape_128k.sna");
+}
diff --git a/rustzx-core/src/emulator/snapshot/autoload/tape_128k.sna b/rustzx-core/src/emulator/snapshot/autoload/tape_128k.sna
new file mode 100644
index 0000000..2849c1d
Binary files /dev/null and b/rustzx-core/src/emulator/snapshot/autoload/tape_128k.sna differ
diff --git a/rustzx-core/src/emulator/snapshot/autoload/tape_48k.sna b/rustzx-core/src/emulator/snapshot/autoload/tape_48k.sna
new file mode 100644
index 0000000..9532555
Binary files /dev/null and b/rustzx-core/src/emulator/snapshot/autoload/tape_48k.sna differ
diff --git a/rustzx-core/src/emulator/snapshot/mod.rs b/rustzx-core/src/emulator/snapshot/mod.rs
new file mode 100644
index 0000000..fdb6b77
--- /dev/null
+++ b/rustzx-core/src/emulator/snapshot/mod.rs
@@ -0,0 +1,3 @@
+#[cfg(feature = "autoload")]
+pub mod autoload;
+pub mod sna;
diff --git a/rustzx-core/src/emulator/snapshot/sna.rs b/rustzx-core/src/emulator/snapshot/sna.rs
new file mode 100644
index 0000000..f136be6
--- /dev/null
+++ b/rustzx-core/src/emulator/snapshot/sna.rs
@@ -0,0 +1,273 @@
+use crate::{
+    emulator::Emulator,
+    error::IoError,
+    host::{DataRecorder, Host, LoadableAsset, SeekFrom, SeekableAsset},
+    utils::{make_word, split_word, Clocks},
+    z80::{
+        opcodes::{execute_pop_16, execute_push_16},
+        RegName16,
+    },
+    zx::{machine::ZXMachine, video::colors::ZXColor},
+    Result,
+};
+
+const SNA_HEADER_SIZE: usize = 27;
+const SNA_128K_SECONDARY_HEADER_SIZE: usize = 4;
+const SNA_48K_SIZE: usize = 49179;
+const SNA_128K_SECONDARY_HEADER_OFFSET: usize = SNA_48K_SIZE;
+const SNA_128K_SECONDARY_TAIL_BANKS_OFFSET: usize = 49183;
+const SNA_128K_PERSISTENT_BANK_0: u8 = 5;
+const SNA_128K_PERSISTENT_BANK_1: u8 = 2;
+const SNA_IFF2_BIT_MASK: u8 = 0x04;
+const SNA_INTERRUPT_MODE_MASK: u8 = 0x03;
+const SNA_BORDER_COLOR_MASK: u8 = 0x07;
+const SNA_128K_TAIL_BANKS: &[u8] = &[0, 1, 3, 4, 6, 7];
+const SNA_PAGINATED_PAGED_BANK_ADDRESS: u16 = 0xFFFF;
+const SNA_48K_RAM_PAGES_COUNT: u8 = 3;
+
+/// SNA snapshot loading function
+pub fn load<H, A>(emulator: &mut Emulator<H>, mut asset: A) -> Result<()>
+where
+    H: Host,
+    A: LoadableAsset + SeekableAsset,
+{
+    let size = asset.seek(SeekFrom::End(0))?;
+    asset.seek(SeekFrom::Start(0))?;
+
+    let is_128k = size > SNA_48K_SIZE;
+
+    if !is_128k && size < SNA_48K_SIZE {
+        return Err(IoError::UnexpectedEof.into());
+    }
+
+    let mut header = [0u8; SNA_HEADER_SIZE];
+    asset.read_exact(&mut header)?;
+
+    // i-reg
+    emulator.cpu.regs.set_i(header[0]);
+    // alt-regs
+    emulator.cpu.regs.set_hl(make_word(header[2], header[1]));
+    emulator.cpu.regs.set_de(make_word(header[4], header[3]));
+    emulator.cpu.regs.set_bc(make_word(header[6], header[5]));
+    emulator.cpu.regs.exx();
+    // af'
+    emulator.cpu.regs.set_af(make_word(header[8], header[7]));
+    emulator.cpu.regs.swap_af_alt();
+    // regs
+    emulator.cpu.regs.set_hl(make_word(header[10], header[9]));
+    emulator.cpu.regs.set_de(make_word(header[12], header[11]));
+    emulator.cpu.regs.set_bc(make_word(header[14], header[13]));
+    // index regs
+    emulator.cpu.regs.set_iy(make_word(header[16], header[15]));
+    emulator.cpu.regs.set_ix(make_word(header[18], header[17]));
+    // iff2, iff1
+    let iff = (header[19] & SNA_IFF2_BIT_MASK) != 0;
+    emulator.cpu.regs.set_iff1(iff);
+    emulator.cpu.regs.set_iff2(iff);
+    // r
+    emulator.cpu.regs.set_r(header[20]);
+    // af
+    emulator.cpu.regs.set_af(make_word(header[22], header[21]));
+    // sp
+    emulator.cpu.regs.set_sp(make_word(header[24], header[23]));
+    // interrupt mode
+    emulator.cpu.set_im(header[25] & SNA_INTERRUPT_MODE_MASK);
+    // Border color
+    emulator.controller.set_border_color(
+        Clocks(0),
+        ZXColor::from_bits(header[26] & SNA_BORDER_COLOR_MASK),
+    );
+    if is_128k {
+        // PC, 7ffd port, trdos pagination status
+        let mut tmp = [0u8; SNA_128K_SECONDARY_HEADER_SIZE];
+        asset.seek(SeekFrom::Start(SNA_128K_SECONDARY_HEADER_OFFSET))?;
+        asset.read_exact(&mut tmp)?;
+        emulator.cpu.regs.set_pc(make_word(tmp[1], tmp[0]));
+        let port_7ffd = tmp[2];
+        let _trdos_paged = tmp[3];
+        // This will alsto setup required memory map before banks restore
+        emulator.controller.write_7ffd(port_7ffd);
+
+        // Go to the previous position
+        asset.seek(SeekFrom::Start(SNA_HEADER_SIZE))?;
+        let paginated_bank = match emulator
+            .controller
+            .memory
+            .get_page(SNA_PAGINATED_PAGED_BANK_ADDRESS)
+        {
+            crate::zx::memory::Page::Ram(bank) => bank,
+            crate::zx::memory::Page::Rom(_) => 0,
+        };
+
+        // write 3 head banks
+        let head_banks = &[
+            SNA_128K_PERSISTENT_BANK_0,
+            SNA_128K_PERSISTENT_BANK_1,
+            paginated_bank,
+        ];
+        for bank in head_banks {
+            let page = emulator.controller.memory.ram_page_data_mut(*bank);
+            asset.read_exact(page)?;
+        }
+
+        // tail banks
+        asset.seek(SeekFrom::Start(SNA_128K_SECONDARY_TAIL_BANKS_OFFSET))?;
+        for bank in SNA_128K_TAIL_BANKS {
+            if *bank == paginated_bank {
+                continue;
+            }
+            let page = emulator.controller.memory.ram_page_data_mut(*bank);
+            asset.read_exact(page)?;
+        }
+    } else {
+        for page_index in 0..SNA_48K_RAM_PAGES_COUNT {
+            let page = emulator.controller.memory.ram_page_data_mut(page_index);
+            asset.read_exact(page)?;
+        }
+
+        // Perform RET as 48K sna snapshot stores it in the machine stack
+        execute_pop_16(
+            &mut emulator.cpu,
+            &mut emulator.controller,
+            RegName16::PC,
+            Clocks(0),
+        );
+    }
+
+    // Refresh screen and other memory-dependent peripheral
+    emulator.controller.refresh_memory_dependent_devices();
+
+    Ok(())
+}
+
+/// Helper class to place emulator in the state required for
+/// snapshoting and return to normal state afterwards
+struct ScopedSnapshotState<'a, H: Host> {
+    pub emulator: &'a mut Emulator<H>,
+    pub is_48k: bool,
+}
+
+impl<'a, H: Host> ScopedSnapshotState<'a, H> {
+    fn enter(emulator: &'a mut Emulator<H>) -> Self {
+        let is_48k = emulator.settings.machine == ZXMachine::Sinclair48K;
+        if is_48k {
+            execute_push_16(
+                &mut emulator.cpu,
+                &mut emulator.controller,
+                RegName16::PC,
+                Clocks(0),
+            );
+        }
+
+        Self { emulator, is_48k }
+    }
+}
+
+impl<'a, H: Host> Drop for ScopedSnapshotState<'a, H> {
+    fn drop(&mut self) {
+        if self.is_48k {
+            execute_pop_16(
+                &mut self.emulator.cpu,
+                &mut self.emulator.controller,
+                RegName16::PC,
+                Clocks(0),
+            );
+        }
+    }
+}
+
+pub fn save<H, R>(emulator: &mut Emulator<H>, mut recorder: R) -> Result<()>
+where
+    H: Host,
+    R: DataRecorder,
+{
+    let state = ScopedSnapshotState::enter(emulator);
+    let ScopedSnapshotState { emulator, is_48k } = &state;
+
+    let mut header = [0u8; SNA_HEADER_SIZE];
+    // interrupt register
+    header[0] = emulator.cpu.regs.get_i();
+    // alt register pairs
+    header[1] = emulator.cpu.regs.get_l_alt();
+    header[2] = emulator.cpu.regs.get_h_alt();
+    header[3] = emulator.cpu.regs.get_e_alt();
+    header[4] = emulator.cpu.regs.get_d_alt();
+    header[5] = emulator.cpu.regs.get_c_alt();
+    header[6] = emulator.cpu.regs.get_b_alt();
+    header[7] = emulator.cpu.regs.get_flags_alt();
+    header[8] = emulator.cpu.regs.get_acc_alt();
+    // hl, de, bc, iy, ix
+    header[9] = emulator.cpu.regs.get_l();
+    header[10] = emulator.cpu.regs.get_h();
+    header[11] = emulator.cpu.regs.get_e();
+    header[12] = emulator.cpu.regs.get_d();
+    header[13] = emulator.cpu.regs.get_c();
+    header[14] = emulator.cpu.regs.get_b();
+    let (iyh, iyl) = split_word(emulator.cpu.regs.get_iy());
+    header[15] = iyl;
+    header[16] = iyh;
+    let (ixh, ixl) = split_word(emulator.cpu.regs.get_ix());
+    header[17] = ixl;
+    header[18] = ixh;
+    // iff2
+    if emulator.cpu.regs.get_iff2() {
+        header[19] = SNA_IFF2_BIT_MASK;
+    }
+    // r
+    header[20] = emulator.cpu.regs.get_r();
+    // AF
+    header[21] = emulator.cpu.regs.get_flags();
+    header[22] = emulator.cpu.regs.get_acc();
+    // SP
+    let (sph, spl) = split_word(emulator.cpu.regs.get_sp());
+    header[23] = spl;
+    header[24] = sph;
+    // Interrupt mode
+    header[25] = emulator.cpu.get_im().into();
+    // Border color
+    header[26] = emulator.controller.border_color.into();
+
+    recorder.write_all(&header)?;
+
+    if *is_48k {
+        for page_index in 0..SNA_48K_RAM_PAGES_COUNT {
+            let page = emulator.controller.memory.ram_page_data(page_index);
+            recorder.write_all(page)?;
+        }
+    } else {
+        let paginated_bank = match emulator
+            .controller
+            .memory
+            .get_page(SNA_PAGINATED_PAGED_BANK_ADDRESS)
+        {
+            crate::zx::memory::Page::Ram(bank) => bank,
+            crate::zx::memory::Page::Rom(_) => 0,
+        };
+        let head_banks = &[
+            SNA_128K_PERSISTENT_BANK_0,
+            SNA_128K_PERSISTENT_BANK_1,
+            paginated_bank,
+        ];
+        for bank in head_banks {
+            let page = emulator.controller.memory.ram_page_data(*bank);
+            recorder.write_all(page)?;
+        }
+
+        // PC, 7ffd, trdos
+        let (pch, pcl) = split_word(emulator.cpu.regs.get_pc());
+        let port_7ffd = emulator.controller.read_7ffd();
+        let trdos_paged = 0x00;
+        recorder.write_all(&[pcl, pch, port_7ffd, trdos_paged])?;
+
+        // remaining banks
+        for bank in SNA_128K_TAIL_BANKS {
+            if *bank == paginated_bank {
+                continue;
+            }
+            let page = emulator.controller.memory.ram_page_data(*bank);
+            recorder.write_all(page)?;
+        }
+    }
+
+    Ok(())
+}
diff --git a/rustzx-core/src/error.rs b/rustzx-core/src/error.rs
index 5c96cf6..0ade567 100644
--- a/rustzx-core/src/error.rs
+++ b/rustzx-core/src/error.rs
@@ -4,7 +4,7 @@ use from_variants::FromVariants;
 #[derive(Debug, Display, FromVariants)]
 pub enum Error {
     /// Failed to read asset
-    AssetRead(AssetReadError),
+    AssetRead(IoError),
     /// Failed to load rom
     RomLoad(RomLoadError),
     /// Failed to load rom
@@ -12,9 +12,13 @@ pub enum Error {
 }
 
 #[derive(Debug, Display)]
-pub enum AssetReadError {
+pub enum IoError {
     /// Unexpected end of file
     UnexpectedEof,
+    /// Sink unexpectedly refused to write more bytes
+    WriteZero,
+    /// Seek operation was performed with offset before beginning of the asset
+    SeekBeforeStart,
     /// Host-provided asset implementation failed
     HostAssetImplFailed,
 }
diff --git a/rustzx-core/src/host/io.rs b/rustzx-core/src/host/io.rs
index 9ba902a..9cc0f51 100644
--- a/rustzx-core/src/host/io.rs
+++ b/rustzx-core/src/host/io.rs
@@ -1,6 +1,7 @@
-use crate::error;
+use crate::error::IoError;
+use core::usize;
 
-type Result<T> = core::result::Result<T, error::AssetReadError>;
+type Result<T> = core::result::Result<T, IoError>;
 
 #[derive(Clone, Copy)]
 pub enum SeekFrom {
@@ -9,14 +10,59 @@ pub enum SeekFrom {
     Current(isize),
 }
 
+/// Implementation of loadable asset for buffer-like type
+pub struct BufferCursor<T: AsRef<[u8]>> {
+    data: T,
+    pos: usize,
+}
+
+impl<T: AsRef<[u8]>> BufferCursor<T> {
+    pub fn new(data: T) -> Self {
+        Self { data, pos: 0 }
+    }
+}
+
+impl<T: AsRef<[u8]>> SeekableAsset for BufferCursor<T> {
+    fn seek(&mut self, pos: SeekFrom) -> Result<usize> {
+        let new_pos = match pos {
+            SeekFrom::Start(pos) => pos as isize,
+            SeekFrom::End(pos) => self.data.as_ref().len() as isize + pos,
+            SeekFrom::Current(pos) => self.pos as isize + pos,
+        };
+        if new_pos < 0 {
+            return Err(IoError::SeekBeforeStart);
+        }
+        self.pos = new_pos as usize;
+
+        Ok(self.pos)
+    }
+}
+
+impl<T: AsRef<[u8]>> LoadableAsset for BufferCursor<T> {
+    fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
+        let data = self.data.as_ref();
+
+        if self.pos >= data.len() {
+            return Err(IoError::UnexpectedEof);
+        }
+        let bytes_to_read = buf.len().min(data.len() - self.pos);
+        buf[0..bytes_to_read].copy_from_slice(&data[self.pos..self.pos + bytes_to_read]);
+        self.pos += bytes_to_read;
+        Ok(bytes_to_read)
+    }
+}
+
+pub trait SeekableAsset {
+    /// Seek position in the asset. Returns current position in the asset
+    fn seek(&mut self, pos: SeekFrom) -> Result<usize>;
+}
+
 // While no_std environment does not provide Read
 // trait, we are forced to use substitution for it
 pub trait LoadableAsset {
     /// Read data from asset to `buf`
-    /// Return count of read bytes. Should return 0 read bytes when EOF was reached
+    /// Returns count of read bytes. Should return 0 read bytes when EOF was reached
     fn read(&mut self, buf: &mut [u8]) -> Result<usize>;
-    /// Seek position in the asset. Returns current position in the asset
-    fn seek(&mut self, pos: SeekFrom) -> Result<usize>;
 
     fn read_exact(&mut self, mut buf: &mut [u8]) -> Result<()> {
         while !buf.is_empty() {
@@ -31,9 +77,59 @@ pub trait LoadableAsset {
             }
         }
         if !buf.is_empty() {
-            return Err(error::AssetReadError::UnexpectedEof);
+            return Err(IoError::UnexpectedEof);
         }
 
         Ok(())
     }
 }
+
+pub trait DataRecorder {
+    /// Writes given buffer to recorder.
+    /// Returns count of written bytes or 0 if end of the
+    /// destination asset was reached (e.g. buffer filled)
+    fn write(&mut self, buf: &[u8]) -> Result<usize>;
+
+    /// Writes all bytes to the destiantion or returns
+    /// [IoError::WriteZero] if destination refused to accept
+    /// more bytes
+    fn write_all(&mut self, mut buf: &[u8]) -> Result<()> {
+        while !buf.is_empty() {
+            match self.write(buf)? {
+                0 => return Err(IoError::WriteZero),
+                n => buf = &buf[n..],
+            }
+        }
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn buffer_cursor_seek_works() {
+        const BUFFER: &[u8] = &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+        let mut cursor = BufferCursor::new(BUFFER);
+
+        let mut tmp = [0u8; 1];
+        cursor.read_exact(&mut tmp).unwrap();
+        assert_eq!(tmp[0], 1);
+
+        cursor.seek(SeekFrom::Current(1)).unwrap();
+        cursor.read_exact(&mut tmp).unwrap();
+        assert_eq!(tmp[0], 3);
+
+        cursor.seek(SeekFrom::Start(2)).unwrap();
+        cursor.read_exact(&mut tmp).unwrap();
+        assert_eq!(tmp[0], 3);
+
+        let mut tmp = [0u8; 3];
+        cursor.seek(SeekFrom::End(-2)).unwrap();
+        let read_bytes = cursor.read(&mut tmp).unwrap();
+        assert_eq!(read_bytes, 2);
+        assert_eq!(tmp[0], 9);
+        assert_eq!(tmp[1], 10);
+    }
+}
diff --git a/rustzx-core/src/host/mod.rs b/rustzx-core/src/host/mod.rs
index d1c97c2..d4b46b5 100644
--- a/rustzx-core/src/host/mod.rs
+++ b/rustzx-core/src/host/mod.rs
@@ -2,13 +2,19 @@ mod frame_buffer;
 mod io;
 
 pub use frame_buffer::{FrameBuffer, FrameBufferSource};
-pub use io::{LoadableAsset, SeekFrom};
+pub use io::{DataRecorder, LoadableAsset, SeekFrom, SeekableAsset};
+
+pub use io::BufferCursor;
 
 pub enum Snapshot<LoadableAssetImpl: LoadableAsset> {
     Sna(LoadableAssetImpl),
     // TODO(#55): Implement SLT snapshot format support
 }
 
+pub enum SnapshotRecorder<DataRecorderImpl: DataRecorder> {
+    Sna(DataRecorderImpl),
+}
+
 pub enum Tape<LoadableAssetImpl: LoadableAsset> {
     Tap(LoadableAssetImpl),
     // TODO(#56): Implement TZX tape format support
@@ -36,9 +42,7 @@ pub trait Host {
     /// context objects for host-defined emulator parts construction (e.g. FrameBuffers)
     type Context: HostContext<Self>;
     /// File-like type implementation for tape loading
-    type TapeAsset: LoadableAsset;
-    /// File-like type implementation for snapshot loading
-    type SnapshotAsset: LoadableAsset;
+    type TapeAsset: LoadableAsset + SeekableAsset;
     /// File-like type implementation for rom loading
     type RomSet: RomSet;
     /// Frame buffer implementation
diff --git a/rustzx-core/src/settings.rs b/rustzx-core/src/settings.rs
index c60d79a..71c335a 100644
--- a/rustzx-core/src/settings.rs
+++ b/rustzx-core/src/settings.rs
@@ -6,8 +6,9 @@ use crate::zx::sound::ay::ZXAYMode;
 pub struct RustzxSettings {
     pub machine: ZXMachine,
     pub emulation_speed: EmulationSpeed,
-    pub tape_fastload: bool,
-    pub enable_kempston: bool,
+    pub tape_fastload_enabled: bool,
+    pub kempston_enabled: bool,
+    pub mouse_enabled: bool,
     #[cfg(all(feature = "sound", feature = "ay"))]
     pub ay_mode: ZXAYMode,
     #[cfg(all(feature = "sound", feature = "ay"))]
@@ -22,4 +23,6 @@ pub struct RustzxSettings {
     pub sound_sample_rate: usize,
     #[cfg(feature = "embedded-roms")]
     pub load_default_rom: bool,
+    #[cfg(feature = "autoload")]
+    pub autoload_enabled: bool,
 }
diff --git a/rustzx-core/src/z80/common_types.rs b/rustzx-core/src/z80/common_types.rs
index e908c18..bbd46dc 100644
--- a/rustzx-core/src/z80/common_types.rs
+++ b/rustzx-core/src/z80/common_types.rs
@@ -40,3 +40,13 @@ pub enum IntMode {
     IM1,
     IM2,
 }
+
+impl From<IntMode> for u8 {
+    fn from(mode: IntMode) -> Self {
+        match mode {
+            IntMode::IM0 => 0,
+            IntMode::IM1 => 1,
+            IntMode::IM2 => 2,
+        }
+    }
+}
diff --git a/rustzx-core/src/z80/registers.rs b/rustzx-core/src/z80/registers.rs
index efcdb6c..6d8f8f4 100644
--- a/rustzx-core/src/z80/registers.rs
+++ b/rustzx-core/src/z80/registers.rs
@@ -464,6 +464,11 @@ impl Regs {
         self.a
     }
 
+    /// Returns alt accumulator
+    pub fn get_acc_alt(&self) -> u8 {
+        self.a_alt
+    }
+
     /// Changes accumulator
     pub fn set_acc(&mut self, value: u8) -> u8 {
         self.a = value;
@@ -481,6 +486,11 @@ impl Regs {
         self.f
     }
 
+    /// Returns alt F
+    pub fn get_flags_alt(&self) -> u8 {
+        self.f_alt
+    }
+
     /// Returns I
     pub fn get_i(&self) -> u8 {
         self.i
@@ -520,6 +530,16 @@ impl Regs {
         self.c
     }
 
+    /// Returns D
+    pub fn get_d(&self) -> u8 {
+        self.d
+    }
+
+    /// Returns E
+    pub fn get_e(&self) -> u8 {
+        self.e
+    }
+
     /// Returns H
     pub fn get_h(&self) -> u8 {
         self.h
@@ -530,6 +550,36 @@ impl Regs {
         self.l
     }
 
+    /// Returns alt B
+    pub fn get_b_alt(&self) -> u8 {
+        self.b_alt
+    }
+
+    /// Returns alt C
+    pub fn get_c_alt(&self) -> u8 {
+        self.c_alt
+    }
+
+    /// Returns alt D
+    pub fn get_d_alt(&self) -> u8 {
+        self.d_alt
+    }
+
+    /// Returns alt E
+    pub fn get_e_alt(&self) -> u8 {
+        self.e_alt
+    }
+
+    /// Returns alt H
+    pub fn get_h_alt(&self) -> u8 {
+        self.h
+    }
+
+    /// Returns alt L
+    pub fn get_l_alt(&self) -> u8 {
+        self.l
+    }
+
     // flip-flops --------------------------------------------------------------------------------
 
     /// Returns iff1
diff --git a/rustzx-core/src/zx/controller.rs b/rustzx-core/src/zx/controller.rs
index e654eb9..673b9b3 100644
--- a/rustzx-core/src/zx/controller.rs
+++ b/rustzx-core/src/zx/controller.rs
@@ -8,10 +8,14 @@ use crate::{
     zx::{
         constants::{ADDR_LD_BREAK, CANVAS_HEIGHT, CLOCKS_PER_COL},
         events::EmulationEvents,
-        joy::kempston::KempstonJoy,
-        keys::ZXKey,
+        joy::{
+            kempston::KempstonJoy,
+            sinclair::{self, SinclairJoyNum, SinclairKey},
+        },
+        keys::{CompoundKey, ZXKey},
         machine::ZXMachine,
         memory::{Page, RamType, RomType, ZXMemory, PAGE_SIZE},
+        mouse::kempston::{KempstonMouse, KempstonMouseButton, KempstonMouseWheelDirection},
         tape::{TapeImpl, ZXTape},
         video::{colors::ZXColor, screen::ZXScreen},
     },
@@ -34,9 +38,13 @@ pub(crate) struct ZXController<H: Host> {
     #[cfg(feature = "precise-border")]
     pub border: ZXBorder<H::FrameBuffer>,
     pub kempston: Option<KempstonJoy>,
+    pub mouse: Option<KempstonMouse>,
     #[cfg(feature = "sound")]
     pub mixer: ZXMixer,
     pub keyboard: [u8; 8],
+    pub keyboard_extended: [u8; 8],
+    pub keyboard_sinclair: [u8; 8],
+    pub caps_shift_modifier_mask: u32,
     // current border color
     pub border_color: ZXColor,
     // clocls count from frame start
@@ -50,6 +58,7 @@ pub(crate) struct ZXController<H: Host> {
     ear: bool,
     paging_enabled: bool,
     screen_bank: u8,
+    current_port_7ffd: u8,
     // Z80 module expected controller implementation without errors,
     // so we need to store the internal errors manually. For sake of simplicity,
     // Only last error is saved
@@ -72,12 +81,19 @@ impl<H: Host> ZXController<H> {
                 screen_bank = 5;
             }
         };
-        let kempston = if settings.enable_kempston {
+
+        let kempston = if settings.kempston_enabled {
             Some(KempstonJoy::default())
         } else {
             None
         };
 
+        let mouse = if settings.mouse_enabled {
+            Some(KempstonMouse::default())
+        } else {
+            None
+        };
+
         let screen = ZXScreen::new(settings.machine, host_context.frame_buffer_context());
         #[cfg(feature = "precise-border")]
         let border = ZXBorder::new(settings.machine, host_context.frame_buffer_context());
@@ -92,9 +108,13 @@ impl<H: Host> ZXController<H> {
             #[cfg(feature = "precise-border")]
             border,
             kempston,
+            mouse,
             #[cfg(feature = "sound")]
             mixer,
             keyboard: [0xFF; 8],
+            keyboard_extended: [0xFF; 8],
+            keyboard_sinclair: [0xFF; 8],
+            caps_shift_modifier_mask: 0,
             border_color: ZXColor::Black,
             frame_clocks: Clocks(0),
             passed_frames: 0,
@@ -104,6 +124,7 @@ impl<H: Host> ZXController<H> {
             ear: false,
             paging_enabled: paging,
             screen_bank,
+            current_port_7ffd: 0,
             last_emulation_error: None,
         };
 
@@ -160,10 +181,59 @@ impl<H: Host> ZXController<H> {
 
     /// Changes key state in controller
     pub fn send_key(&mut self, key: ZXKey, pressed: bool) {
-        let row_id = key.row_id();
-        self.keyboard[row_id] &= !key.mask();
-        if !pressed {
-            self.keyboard[row_id] |= key.mask();
+        if pressed {
+            self.keyboard[key.row_id()] &= !key.mask();
+            return;
+        }
+        self.keyboard[key.row_id()] |= key.mask();
+    }
+
+    pub fn send_sinclair_key(&mut self, num: SinclairJoyNum, key: SinclairKey, pressed: bool) {
+        let key = sinclair::sinclair_event_to_zx_key(key, num);
+        if pressed {
+            self.keyboard_sinclair[key.row_id()] &= !key.mask();
+            return;
+        }
+        self.keyboard_sinclair[key.row_id()] |= key.mask();
+    }
+
+    pub fn send_compound_key(&mut self, key: CompoundKey, pressed: bool) {
+        let mut dummy_modifier_mask = 0;
+        let modifier_mask = match key.modifier_key() {
+            ZXKey::Shift => &mut self.caps_shift_modifier_mask,
+            _ => &mut dummy_modifier_mask,
+        };
+        let primary_key = key.primary_key();
+        let modifier_key = key.modifier_key();
+
+        if pressed {
+            *modifier_mask |= key.modifier_mask();
+            self.keyboard_extended[primary_key.row_id()] &= !primary_key.mask();
+            self.keyboard_extended[modifier_key.row_id()] &= !modifier_key.mask();
+        } else {
+            *modifier_mask &= !key.modifier_mask();
+            if *modifier_mask == 0 {
+                self.keyboard_extended[modifier_key.row_id()] |= modifier_key.mask();
+            }
+            self.keyboard_extended[primary_key.row_id()] |= primary_key.mask();
+        }
+    }
+
+    pub fn send_mouse_button(&mut self, button: KempstonMouseButton, pressed: bool) {
+        if let Some(mouse) = &mut self.mouse {
+            mouse.send_button(button, pressed);
+        }
+    }
+
+    pub fn send_mouse_wheel(&mut self, dir: KempstonMouseWheelDirection) {
+        if let Some(mouse) = &mut self.mouse {
+            mouse.send_wheel(dir);
+        }
+    }
+
+    pub fn send_mouse_pos_diff(&mut self, x: i8, y: i8) {
+        if let Some(mouse) = &mut self.mouse {
+            mouse.send_pos_diff(x, y);
         }
     }
 
@@ -268,10 +338,11 @@ impl<H: Host> ZXController<H> {
         self.frame_clocks
     }
 
-    fn write_7ffd(&mut self, val: u8) {
+    pub fn write_7ffd(&mut self, val: u8) {
         if !self.paging_enabled {
             return;
         }
+        self.current_port_7ffd = val;
         // remap top 16K of the ram
         self.memory.remap(3, Page::Ram(val & 0x07));
         // third block is not pageable
@@ -287,6 +358,10 @@ impl<H: Host> ZXController<H> {
         }
     }
 
+    pub fn read_7ffd(&self) -> u8 {
+        self.current_port_7ffd
+    }
+
     #[cfg(all(feature = "sound", feature = "ay"))]
     fn read_ay_port(&mut self) -> u8 {
         self.mixer.ay.read()
@@ -326,6 +401,24 @@ impl<H: Host> ZXController<H> {
     pub(crate) fn take_last_emulation_error(&mut self) -> Option<Error> {
         self.last_emulation_error.take()
     }
+
+    pub(crate) fn refresh_memory_dependent_devices(&mut self) {
+        match self.machine {
+            ZXMachine::Sinclair48K => {
+                for (idx, data) in self.memory.ram_page_data(0).iter().enumerate() {
+                    self.screen.update(idx as u16, 0, *data);
+                }
+            }
+            ZXMachine::Sinclair128K => {
+                for (idx, data) in self.memory.ram_page_data(5).iter().enumerate() {
+                    self.screen.update(idx as u16, 5, *data);
+                }
+                for (idx, data) in self.memory.ram_page_data(7).iter().enumerate() {
+                    self.screen.update(idx as u16, 7, *data);
+                }
+            }
+        }
+    }
 }
 
 impl<H: Host> Z80Bus for ZXController<H> {
@@ -416,7 +509,9 @@ impl<H: Host> Z80Bus for ZXController<H> {
             for n in 0..8 {
                 // if bit of row reset
                 if ((h >> n) & 0x01) == 0 {
-                    tmp &= self.keyboard[n];
+                    let keyboard_byte =
+                        self.keyboard[n] & self.keyboard_extended[n] & self.keyboard_sinclair[n];
+                    tmp &= keyboard_byte;
                 }
             }
             // invert bit 6 if mic_hw active;
@@ -425,14 +520,16 @@ impl<H: Host> Z80Bus for ZXController<H> {
             }
             // 5 and 7 unused
             tmp
+        } else if self.mouse.is_some() && (port & 0x0121 == 0x0001) {
+            self.mouse.as_ref().unwrap().buttons_port
+        } else if self.mouse.is_some() && (port & 0x0521 == 0x0101) {
+            self.mouse.as_ref().unwrap().x_pos_port
+        } else if self.mouse.is_some() && (port & 0x0521 == 0x0501) {
+            self.mouse.as_ref().unwrap().y_pos_port
         } else if port & 0xC002 == 0xC000 {
             self.read_ay_port()
-        } else if self.kempston.is_some() && (port & 0x0020 == 0) {
-            if let Some(ref joy) = self.kempston {
-                joy.read()
-            } else {
-                unreachable!()
-            }
+        } else if self.kempston.is_some() && (port & 0x00E0 == 0) {
+            self.kempston.as_ref().unwrap().read()
         } else {
             self.floating_bus_value()
         };
diff --git a/rustzx-core/src/zx/joy/mod.rs b/rustzx-core/src/zx/joy/mod.rs
index f84329b..859ed62 100644
--- a/rustzx-core/src/zx/joy/mod.rs
+++ b/rustzx-core/src/zx/joy/mod.rs
@@ -1,1 +1,2 @@
 pub mod kempston;
+pub mod sinclair;
diff --git a/rustzx-core/src/zx/joy/sinclair.rs b/rustzx-core/src/zx/joy/sinclair.rs
new file mode 100644
index 0000000..2b0851a
--- /dev/null
+++ b/rustzx-core/src/zx/joy/sinclair.rs
@@ -0,0 +1,29 @@
+use crate::zx::keys::ZXKey;
+
+pub enum SinclairKey {
+    Left,
+    Right,
+    Up,
+    Down,
+    Fire,
+}
+
+pub enum SinclairJoyNum {
+    Fist,
+    Second,
+}
+
+pub(crate) fn sinclair_event_to_zx_key(key: SinclairKey, num: SinclairJoyNum) -> ZXKey {
+    match (num, key) {
+        (SinclairJoyNum::Fist, SinclairKey::Left) => ZXKey::N6,
+        (SinclairJoyNum::Fist, SinclairKey::Right) => ZXKey::N7,
+        (SinclairJoyNum::Fist, SinclairKey::Up) => ZXKey::N9,
+        (SinclairJoyNum::Fist, SinclairKey::Down) => ZXKey::N8,
+        (SinclairJoyNum::Fist, SinclairKey::Fire) => ZXKey::N0,
+        (SinclairJoyNum::Second, SinclairKey::Left) => ZXKey::N1,
+        (SinclairJoyNum::Second, SinclairKey::Right) => ZXKey::N2,
+        (SinclairJoyNum::Second, SinclairKey::Up) => ZXKey::N4,
+        (SinclairJoyNum::Second, SinclairKey::Down) => ZXKey::N2,
+        (SinclairJoyNum::Second, SinclairKey::Fire) => ZXKey::N5,
+    }
+}
diff --git a/rustzx-core/src/zx/keys.rs b/rustzx-core/src/zx/keys.rs
index 1919c6d..9fbbeec 100644
--- a/rustzx-core/src/zx/keys.rs
+++ b/rustzx-core/src/zx/keys.rs
@@ -22,6 +22,51 @@ pub enum ZXKey {
     Space, SymShift, M, N, B,
 }
 
+#[derive(Clone, Copy)]
+pub enum CompoundKey {
+    ArrowLeft,
+    ArrowRight,
+    ArrowUp,
+    ArrowDown,
+    CapsLock,
+    Delete,
+    Break,
+}
+
+impl CompoundKey {
+    /// This mask is required to implement logic to keep
+    /// modifier key pressed while one of the compound
+    /// keys were released, but one of them is still
+    /// pressed, therefore modifier should be unchanged
+    pub(crate) fn modifier_mask(self) -> u32 {
+        match self {
+            CompoundKey::ArrowLeft => 0x00000001,
+            CompoundKey::ArrowRight => 0x00000002,
+            CompoundKey::ArrowUp => 0x00000004,
+            CompoundKey::ArrowDown => 0x00000008,
+            CompoundKey::CapsLock => 0x00000010,
+            CompoundKey::Delete => 0x00000020,
+            CompoundKey::Break => 0x00000040,
+        }
+    }
+
+    pub(crate) fn modifier_key(self) -> ZXKey {
+        ZXKey::Shift
+    }
+
+    pub(crate) fn primary_key(self) -> ZXKey {
+        match self {
+            CompoundKey::ArrowLeft => ZXKey::N5,
+            CompoundKey::ArrowRight => ZXKey::N8,
+            CompoundKey::ArrowUp => ZXKey::N7,
+            CompoundKey::ArrowDown => ZXKey::N6,
+            CompoundKey::CapsLock => ZXKey::N2,
+            CompoundKey::Delete => ZXKey::N0,
+            CompoundKey::Break => ZXKey::Space,
+        }
+    }
+}
+
 impl ZXKey {
     pub(crate) fn row_id(self) -> usize {
         match self.half_port() {
diff --git a/rustzx-core/src/zx/memory.rs b/rustzx-core/src/zx/memory.rs
index 0110e7e..a01cc09 100644
--- a/rustzx-core/src/zx/memory.rs
+++ b/rustzx-core/src/zx/memory.rs
@@ -147,6 +147,15 @@ impl ZXMemory {
         &mut self.ram[shift..shift + PAGE_SIZE]
     }
 
+    /// Returns slice to ram page
+    pub fn ram_page_data(&self, page: u8) -> &[u8] {
+        if (page as usize + 1) * PAGE_SIZE > self.ram.len() {
+            panic!("[ERROR] Ram page {} does not exists!", page);
+        }
+        let shift = page as usize * PAGE_SIZE;
+        &self.ram[shift..shift + PAGE_SIZE]
+    }
+
     /// Dumps current address space
     pub fn dump(&self) -> Vec<u8> {
         let mut out = Vec::new();
diff --git a/rustzx-core/src/zx/mod.rs b/rustzx-core/src/zx/mod.rs
index f591012..44dd3e1 100644
--- a/rustzx-core/src/zx/mod.rs
+++ b/rustzx-core/src/zx/mod.rs
@@ -11,6 +11,8 @@ pub mod constants;
 pub mod joy;
 pub mod keys;
 pub mod machine;
+pub mod mouse;
+
 #[cfg(feature = "sound")]
 pub mod sound;
 pub mod video;
diff --git a/rustzx-core/src/zx/mouse/kempston.rs b/rustzx-core/src/zx/mouse/kempston.rs
new file mode 100644
index 0000000..44dad98
--- /dev/null
+++ b/rustzx-core/src/zx/mouse/kempston.rs
@@ -0,0 +1,58 @@
+const WHEEL_MASK: u8 = 0xF0;
+const WHEEL_SHIFT: usize = 4;
+
+// non_exhaustive allows to restrict struct instantiation only to `Default::deafault`
+#[non_exhaustive]
+pub(crate) struct KempstonMouse {
+    pub buttons_port: u8,
+    pub x_pos_port: u8,
+    pub y_pos_port: u8,
+}
+
+impl Default for KempstonMouse {
+    fn default() -> Self {
+        Self {
+            buttons_port: 0xFF,
+            x_pos_port: 0xFF,
+            y_pos_port: 0xFF,
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug)]
+pub enum KempstonMouseButton {
+    Left = 0x01,
+    Right = 0x02,
+    Middle = 0x04,
+    Additional = 0x08,
+}
+
+#[derive(Clone, Copy, Debug)]
+#[repr(i8)]
+pub enum KempstonMouseWheelDirection {
+    Up = 1,
+    Down = -1,
+}
+
+impl KempstonMouse {
+    pub fn send_button(&mut self, button: KempstonMouseButton, pressed: bool) {
+        if pressed {
+            self.buttons_port &= !(button as u8);
+            return;
+        }
+
+        self.buttons_port |= button as u8
+    }
+
+    pub fn send_wheel(&mut self, dir: KempstonMouseWheelDirection) {
+        let mut current = (self.buttons_port & WHEEL_MASK) >> WHEEL_SHIFT;
+        current = ((current as i8) + (dir as i8)) as u8;
+        self.buttons_port =
+            (self.buttons_port & (!WHEEL_MASK)) | ((current << WHEEL_SHIFT) & WHEEL_MASK);
+    }
+
+    pub fn send_pos_diff(&mut self, x: i8, y: i8) {
+        self.x_pos_port = ((self.x_pos_port as i16) + x as i16) as u8;
+        self.y_pos_port = ((self.y_pos_port as i16) - y as i16) as u8;
+    }
+}
diff --git a/rustzx-core/src/zx/mouse/mod.rs b/rustzx-core/src/zx/mouse/mod.rs
new file mode 100644
index 0000000..f84329b
--- /dev/null
+++ b/rustzx-core/src/zx/mouse/mod.rs
@@ -0,0 +1,1 @@
+pub mod kempston;
diff --git a/rustzx-core/src/zx/tape/mod.rs b/rustzx-core/src/zx/tape/mod.rs
index 1188910..1880a9b 100644
--- a/rustzx-core/src/zx/tape/mod.rs
+++ b/rustzx-core/src/zx/tape/mod.rs
@@ -3,17 +3,21 @@ mod tap;
 
 pub use self::{empty::Empty, tap::Tap};
 
-use crate::{host::LoadableAsset, utils::Clocks, Result};
+use crate::{
+    host::{LoadableAsset, SeekableAsset},
+    utils::Clocks,
+    Result,
+};
 
 use enum_dispatch::enum_dispatch;
 
 #[enum_dispatch(TapeImpl)]
-pub enum ZXTape<A: LoadableAsset> {
+pub enum ZXTape<A: LoadableAsset + SeekableAsset> {
     Tap(Tap<A>),
     Empty(Empty),
 }
 
-impl<A: LoadableAsset> Default for ZXTape<A> {
+impl<A: LoadableAsset + SeekableAsset> Default for ZXTape<A> {
     fn default() -> Self {
         Self::Empty(Empty)
     }
diff --git a/rustzx-core/src/zx/tape/tap.rs b/rustzx-core/src/zx/tape/tap.rs
index ca66c9e..183c716 100644
--- a/rustzx-core/src/zx/tape/tap.rs
+++ b/rustzx-core/src/zx/tape/tap.rs
@@ -1,6 +1,6 @@
 use crate::{
     error::TapeLoadError,
-    host::{LoadableAsset, SeekFrom},
+    host::{LoadableAsset, SeekFrom, SeekableAsset},
     utils::{make_word, Clocks},
     zx::tape::TapeImpl,
     Result,
@@ -28,7 +28,7 @@ enum TapeState {
     Pause,
 }
 
-pub struct Tap<A: LoadableAsset> {
+pub struct Tap<A: LoadableAsset + SeekableAsset> {
     asset: A,
     state: TapeState,
     prev_state: TapeState,
@@ -43,7 +43,7 @@ pub struct Tap<A: LoadableAsset> {
     delay: Clocks,
 }
 
-impl<A: LoadableAsset> Tap<A> {
+impl<A: LoadableAsset + SeekableAsset> Tap<A> {
     pub fn from_asset(asset: A) -> Result<Self> {
         let tap = Self {
             prev_state: TapeState::Stop,
@@ -62,7 +62,7 @@ impl<A: LoadableAsset> Tap<A> {
     }
 }
 
-impl<A: LoadableAsset> TapeImpl for Tap<A> {
+impl<A: LoadableAsset + SeekableAsset> TapeImpl for Tap<A> {
     fn can_fast_load(&self) -> bool {
         self.state == TapeState::Stop
     }
diff --git a/rustzx-core/src/zx/video/colors.rs b/rustzx-core/src/zx/video/colors.rs
index 2248852..3009151 100644
--- a/rustzx-core/src/zx/video/colors.rs
+++ b/rustzx-core/src/zx/video/colors.rs
@@ -39,6 +39,21 @@ impl ZXColor {
     }
 }
 
+impl From<ZXColor> for u8 {
+    fn from(color: ZXColor) -> Self {
+        match color {
+            ZXColor::Black => 0,
+            ZXColor::Blue => 1,
+            ZXColor::Red => 2,
+            ZXColor::Purple => 3,
+            ZXColor::Green => 4,
+            ZXColor::Cyan => 5,
+            ZXColor::Yellow => 6,
+            ZXColor::White => 7,
+        }
+    }
+}
+
 /// ZX Spectrum attribute structure
 /// It contains information about ink, paper color,
 /// flash attribute and brightness
diff --git a/rustzx/Cargo.toml b/rustzx/Cargo.toml
index 884b6ea..607d735 100644
--- a/rustzx/Cargo.toml
+++ b/rustzx/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "rustzx"
-version = "0.12.0"
+version = "0.13.0"
 authors = ["The RustZX contributors", "Vladyslav Nikonov <mail@pacmancoder.xyz>"]
 description = "ZX Spectum emulator"
 repository = "https://github.com/pacmancoder/rustzx"
@@ -13,7 +13,7 @@ edition = "2018"
 
 [dependencies]
 sdl2 = { version = "0.34", features = ["unsafe_textures", "bundled", "static-link"] }
-rustzx-core = { version = "0.12", path =  "../rustzx-core", features = ["full"] }
+rustzx-core = { version = "0.13", path =  "../rustzx-core", features = ["full"] }
 log = "0.4"
 anyhow = "1"
 structopt = "0.3"
diff --git a/rustzx/src/app/events/events_sdl.rs b/rustzx/src/app/events/events_sdl.rs
index 84f4a2c..444bda5 100644
--- a/rustzx/src/app/events/events_sdl.rs
+++ b/rustzx/src/app/events/events_sdl.rs
@@ -2,35 +2,86 @@
 use super::{Event, EventDevice};
 use crate::{app::settings::Settings, backends::SDL_CONTEXT};
 use rustzx_core::{
-    zx::{joy::kempston::KempstonKey, keys::ZXKey},
+    zx::{
+        joy::{
+            kempston::KempstonKey,
+            sinclair::{SinclairJoyNum, SinclairKey},
+        },
+        keys::{CompoundKey, ZXKey},
+        mouse::kempston::{KempstonMouseButton, KempstonMouseWheelDirection},
+    },
     EmulationSpeed,
 };
-use sdl2::{event::Event as SdlEvent, keyboard::Scancode, EventPump};
+use sdl2::{
+    event::Event as SdlEvent,
+    keyboard::Scancode,
+    mouse::{MouseButton, MouseUtil},
+    EventPump,
+};
 
 /// Represents SDL Envets bakend
 pub struct EventsSdl {
     event_pump: EventPump,
+    mouse: MouseUtil,
+    kempston_enabled: bool,
+    mouse_enabled: bool,
+    mouse_locked: bool,
+    screen_scale: usize,
+    mouse_sensitivity: usize,
+    enable_joy_keyaboard_layer: bool,
+    mouse_x_counter: i32,
+    mouse_y_counter: i32,
 }
 
 impl EventsSdl {
     /// constructs new event backend from setttigs/
     /// Settings will be used in future for key bindings sittings
-    pub fn new(_settings: &Settings) -> EventsSdl {
+    pub fn new(settings: &Settings) -> EventsSdl {
         // init event system
-        let mut pump = None;
-        SDL_CONTEXT.with(|sdl| {
-            pump = sdl.borrow_mut().event_pump().ok();
+        let (event_pump, mouse) = SDL_CONTEXT.with(|sdl| {
+            let context = sdl.borrow_mut();
+            let pump = context
+                .event_pump()
+                .expect("[ERROR] Sdl event pump init error");
+
+            let mouse = context.mouse();
+
+            (pump, mouse)
         });
-        if let Some(pump) = pump {
-            EventsSdl { event_pump: pump }
-        } else {
-            panic!("[ERROR] Sdl event pump init error");
+
+        EventsSdl {
+            event_pump,
+            mouse,
+            mouse_enabled: settings.enable_mouse,
+            mouse_locked: false,
+            kempston_enabled: !settings.disable_kempston,
+            screen_scale: settings.scale,
+            enable_joy_keyaboard_layer: false,
+            mouse_sensitivity: settings.mouse_sensitivity,
+            mouse_x_counter: 0,
+            mouse_y_counter: 0,
+        }
+    }
+
+    fn lock_mouse(&mut self) {
+        if self.mouse_enabled {
+            self.mouse.set_relative_mouse_mode(true);
+            self.mouse.show_cursor(false);
+            self.mouse_locked = true;
+        }
+    }
+
+    fn unlock_mouse(&mut self) {
+        if self.mouse_enabled {
+            self.mouse.set_relative_mouse_mode(false);
+            self.mouse.show_cursor(true);
+            self.mouse_locked = false;
         }
     }
 
     /// returns ZX Spectum key form scancode of None if not found
-    fn scancode_to_zxkey(&self, scancode: Option<Scancode>) -> Option<ZXKey> {
-        match scancode? {
+    fn scancode_to_zxkey_event(&self, scancode: Option<Scancode>, pressed: bool) -> Option<Event> {
+        let zxkey_event = match scancode? {
             // FEFE
             Scancode::LShift | Scancode::RShift => Some(ZXKey::Shift),
             Scancode::Z => Some(ZXKey::Z),
@@ -80,18 +131,109 @@ impl EventsSdl {
             Scancode::N => Some(ZXKey::N),
             Scancode::B => Some(ZXKey::B),
             _ => None,
-        }
+        };
+
+        zxkey_event.map(|k| Event::ZXKey(k, pressed))
+    }
+
+    fn scancode_to_compound_key_event(
+        &self,
+        scancode: Option<Scancode>,
+        pressed: bool,
+    ) -> Option<Event> {
+        let compound_key_event = match scancode? {
+            Scancode::Up => Some(CompoundKey::ArrowUp),
+            Scancode::Down => Some(CompoundKey::ArrowDown),
+            Scancode::Left => Some(CompoundKey::ArrowLeft),
+            Scancode::Right => Some(CompoundKey::ArrowDown),
+            Scancode::CapsLock => Some(CompoundKey::CapsLock),
+            Scancode::Backspace => Some(CompoundKey::Delete),
+            Scancode::End => Some(CompoundKey::Break),
+            _ => None,
+        };
+
+        compound_key_event.map(|k| Event::CompoundKey(k, pressed))
     }
 
     /// returns kempston key form scancode of None if not found
-    fn scancode_to_joy(&self, scancode: Option<Scancode>) -> Option<KempstonKey> {
-        match scancode? {
+    fn scancode_to_kempston_event(
+        &self,
+        scancode: Option<Scancode>,
+        pressed: bool,
+    ) -> Option<Event> {
+        if !(self.kempston_enabled && self.enable_joy_keyaboard_layer) {
+            return None;
+        }
+
+        let kempston_event = match scancode? {
             Scancode::LAlt | Scancode::RAlt => Some(KempstonKey::Fire),
             Scancode::Up => Some(KempstonKey::Up),
             Scancode::Down => Some(KempstonKey::Down),
             Scancode::Left => Some(KempstonKey::Left),
             Scancode::Right => Some(KempstonKey::Right),
             _ => None,
+        };
+
+        kempston_event.map(|k| Event::Kempston(k, pressed))
+    }
+
+    fn scancode_to_sinclair_event(
+        &self,
+        scancode: Option<Scancode>,
+        pressed: bool,
+    ) -> Option<Event> {
+        if !self.enable_joy_keyaboard_layer {
+            return None;
+        }
+
+        let sinclair_event = match scancode? {
+            // Joy 1
+            Scancode::A => Some((SinclairJoyNum::Fist, SinclairKey::Left)),
+            Scancode::W => Some((SinclairJoyNum::Fist, SinclairKey::Up)),
+            Scancode::S => Some((SinclairJoyNum::Fist, SinclairKey::Down)),
+            Scancode::D => Some((SinclairJoyNum::Fist, SinclairKey::Right)),
+            Scancode::CapsLock => Some((SinclairJoyNum::Fist, SinclairKey::Fire)),
+            // Joy 2
+            Scancode::J => Some((SinclairJoyNum::Second, SinclairKey::Left)),
+            Scancode::I => Some((SinclairJoyNum::Second, SinclairKey::Up)),
+            Scancode::K => Some((SinclairJoyNum::Second, SinclairKey::Down)),
+            Scancode::L => Some((SinclairJoyNum::Second, SinclairKey::Right)),
+            Scancode::Return => Some((SinclairJoyNum::Second, SinclairKey::Fire)),
+            _ => None,
+        };
+
+        sinclair_event.map(|(n, k)| Event::Sinclair(n, k, pressed))
+    }
+
+    fn scancode_to_emulator_event(
+        &mut self,
+        scancode: Option<Scancode>,
+        pressed: bool,
+    ) -> Option<Event> {
+        if let (Some(code), true) = (scancode, pressed) {
+            match code {
+                Scancode::F1 => Some(Event::QuickSave),
+                Scancode::F2 => Some(Event::QuickLoad),
+                Scancode::F3 => Some(Event::ChangeSpeed(EmulationSpeed::Definite(1))),
+                Scancode::F4 => Some(Event::ChangeSpeed(EmulationSpeed::Definite(2))),
+                Scancode::F5 => Some(Event::ChangeSpeed(EmulationSpeed::Max)),
+                Scancode::F6 => Some(Event::SwitchFrameTrace),
+                Scancode::F9 => {
+                    self.enable_joy_keyaboard_layer = !self.enable_joy_keyaboard_layer;
+                    Some(Event::ChangeJoyKeyboardLayer(
+                        self.enable_joy_keyaboard_layer,
+                    ))
+                }
+                Scancode::Insert => Some(Event::InsertTape),
+                Scancode::Delete => Some(Event::StopTape),
+                Scancode::Escape => {
+                    self.unlock_mouse();
+                    None
+                }
+                _ => None,
+            }
+        } else {
+            None
         }
     }
 }
@@ -107,43 +249,81 @@ impl EventDevice for EventsSdl {
                 // if any key pressed
                 action @ SdlEvent::KeyDown { .. } | action @ SdlEvent::KeyUp { .. } => {
                     // assemble tuple from scancode and its state
-                    let (scancode, state) = match action {
+                    let (scancode, pressed) = match action {
                         SdlEvent::KeyDown { scancode: code, .. } => (code, true),
                         SdlEvent::KeyUp { scancode: code, .. } => (code, false),
                         _ => unreachable!(),
                     };
-                    if let Some(key) = self.scancode_to_zxkey(scancode) {
-                        // if zx spectrum key found
-                        Some(Event::GameKey(key, state))
-                    } else if let Some(key) = self.scancode_to_joy(scancode) {
-                        // of kempston key found
-                        Some(Event::Kempston(key, state))
+
+                    // Form highest priority event to lowest
+                    self.scancode_to_emulator_event(scancode, pressed)
+                        .or_else(|| self.scancode_to_kempston_event(scancode, pressed))
+                        .or_else(|| self.scancode_to_sinclair_event(scancode, pressed))
+                        .or_else(|| self.scancode_to_zxkey_event(scancode, pressed))
+                        .or_else(|| self.scancode_to_compound_key_event(scancode, pressed))
+                }
+                SdlEvent::MouseMotion { xrel, yrel, .. } => {
+                    // Change of direction  requires counter reset to elimiate lag
+                    if self.mouse_x_counter.signum() != xrel.signum() {
+                        self.mouse_x_counter = xrel;
+                    } else {
+                        self.mouse_x_counter += xrel;
+                    }
+                    if self.mouse_y_counter.signum() != yrel.signum() {
+                        self.mouse_y_counter = yrel;
                     } else {
-                        // if speial keys are used
-                        if state {
-                            if let Some(code) = scancode {
-                                match code {
-                                    // speed control
-                                    Scancode::F3 => {
-                                        Some(Event::ChangeSpeed(EmulationSpeed::Definite(1)))
-                                    }
-                                    Scancode::F4 => {
-                                        Some(Event::ChangeSpeed(EmulationSpeed::Definite(2)))
-                                    }
-                                    Scancode::F5 => Some(Event::ChangeSpeed(EmulationSpeed::Max)),
-                                    // debug info control
-                                    Scancode::F6 => Some(Event::SwitchDebug),
-                                    // tape control
-                                    Scancode::Insert => Some(Event::InsertTape),
-                                    Scancode::Delete => Some(Event::StopTape),
-                                    _ => None,
-                                }
-                            } else {
-                                None
-                            }
+                        self.mouse_y_counter += yrel;
+                    }
+
+                    // Depending on sensitivity, diffent distance is required to move
+                    // kempston mouse
+                    let ticks_to_move =
+                        sensitivity_to_mouse_counter_ticks(self.mouse_sensitivity) as i32;
+                    let xshift = self.mouse_x_counter / ticks_to_move;
+                    let yshift = self.mouse_y_counter / ticks_to_move;
+                    let xrem = self.mouse_x_counter % ticks_to_move;
+                    let yrem = self.mouse_y_counter % ticks_to_move;
+                    if xshift != 0 {
+                        self.mouse_x_counter = xrem;
+                    }
+                    if yshift != 0 {
+                        self.mouse_y_counter = yrem;
+                    }
+
+                    if self.mouse_locked {
+                        let x = xshift.clamp(i8::MIN as i32, i8::MAX as i32) as i8;
+                        let y = yshift.clamp(i8::MIN as i32, i8::MAX as i32) as i8;
+                        Some(Event::MouseMove { x, y })
+                    } else {
+                        None
+                    }
+                }
+                SdlEvent::MouseButtonDown { mouse_btn, .. } => {
+                    self.lock_mouse();
+                    if self.mouse_locked {
+                        sdl_mouse_button_to_kempston(mouse_btn)
+                            .map(|button| Event::MouseButton(button, true))
+                    } else {
+                        None
+                    }
+                }
+                SdlEvent::MouseButtonUp { mouse_btn, .. } => {
+                    if self.mouse_locked {
+                        sdl_mouse_button_to_kempston(mouse_btn)
+                            .map(|button| Event::MouseButton(button, false))
+                    } else {
+                        None
+                    }
+                }
+                SdlEvent::MouseWheel { y, .. } => {
+                    if self.mouse_locked {
+                        if y > 0 {
+                            Some(Event::MouseWheel(KempstonMouseWheelDirection::Up))
                         } else {
-                            None
+                            Some(Event::MouseWheel(KempstonMouseWheelDirection::Down))
                         }
+                    } else {
+                        None
                     }
                 }
                 SdlEvent::DropFile { filename, .. } => Some(Event::OpenFile(filename.into())),
@@ -154,3 +334,20 @@ impl EventDevice for EventsSdl {
         }
     }
 }
+
+fn sdl_mouse_button_to_kempston(button: MouseButton) -> Option<KempstonMouseButton> {
+    match button {
+        MouseButton::Left => Some(KempstonMouseButton::Left),
+        MouseButton::Right => Some(KempstonMouseButton::Right),
+        MouseButton::Middle => Some(KempstonMouseButton::Middle),
+        MouseButton::X1 => Some(KempstonMouseButton::Additional),
+        _ => None,
+    }
+}
+
+fn sensitivity_to_mouse_counter_ticks(sensitivity: usize) -> usize {
+    const MIN_MOUSE_SENSITIVITY: usize = 1;
+    const MAX_MOUSE_SENSITIVITY: usize = 100;
+
+    MAX_MOUSE_SENSITIVITY / sensitivity.clamp(MIN_MOUSE_SENSITIVITY, MAX_MOUSE_SENSITIVITY)
+}
diff --git a/rustzx/src/app/events/mod.rs b/rustzx/src/app/events/mod.rs
index 75b1194..d413839 100644
--- a/rustzx/src/app/events/mod.rs
+++ b/rustzx/src/app/events/mod.rs
@@ -3,7 +3,14 @@
 mod events_sdl;
 
 use rustzx_core::{
-    zx::{joy::kempston::KempstonKey, keys::ZXKey},
+    zx::{
+        joy::{
+            kempston::KempstonKey,
+            sinclair::{SinclairJoyNum, SinclairKey},
+        },
+        keys::{CompoundKey, ZXKey},
+        mouse::kempston::{KempstonMouseButton, KempstonMouseWheelDirection},
+    },
     EmulationSpeed,
 };
 use std::path::PathBuf;
@@ -12,15 +19,20 @@ pub use events_sdl::EventsSdl;
 
 // Event type
 pub enum Event {
-    GameKey(ZXKey, bool),
+    ZXKey(ZXKey, bool),
+    CompoundKey(CompoundKey, bool),
     Kempston(KempstonKey, bool),
-    SwitchDebug,
+    Sinclair(SinclairJoyNum, SinclairKey, bool),
+    MouseMove { x: i8, y: i8 },
+    MouseButton(KempstonMouseButton, bool),
+    MouseWheel(KempstonMouseWheelDirection),
+    SwitchFrameTrace,
+    ChangeJoyKeyboardLayer(bool),
     ChangeSpeed(EmulationSpeed),
     InsertTape,
     StopTape,
-    // QuickSave,
-    // QuickLoad,
-    // Pause,
+    QuickSave,
+    QuickLoad,
     OpenFile(PathBuf),
     Exit,
 }
diff --git a/rustzx/src/app/rustzx.rs b/rustzx/src/app/rustzx.rs
index dadb2db..1d9e1d5 100644
--- a/rustzx/src/app/rustzx.rs
+++ b/rustzx/src/app/rustzx.rs
@@ -9,17 +9,19 @@ use crate::{
         sound::{SoundDevice, SoundSdl},
         video::{Rect, TextureInfo, VideoDevice, VideoSdl},
     },
-    host::{self, AppHost, AppHostContext, DetectedFileKind},
+    host::{self, AppHost, AppHostContext, DetectedFileKind, FileAsset},
 };
 use anyhow::anyhow;
 use rustzx_core::{
+    host::SnapshotRecorder,
     zx::constants::{
         CANVAS_HEIGHT, CANVAS_WIDTH, CANVAS_X, CANVAS_Y, FPS, SCREEN_HEIGHT, SCREEN_WIDTH,
     },
     Emulator, Stopwatch,
 };
 use std::{
-    path::Path,
+    fs::{self, File},
+    path::{Path, PathBuf},
     thread,
     time::{Duration, Instant},
 };
@@ -75,6 +77,10 @@ pub struct RustzxApp {
     tex_border: TextureInfo,
     tex_canvas: TextureInfo,
     scale: u32,
+    settings: Settings,
+
+    enable_frame_trace: bool,
+    enable_joy_keyaboard_layer: bool,
 }
 
 impl RustzxApp {
@@ -110,6 +116,8 @@ impl RustzxApp {
                 .map_err(|e| anyhow!("Emulator failed to load tape: {}", e))?;
         }
 
+        let file_autodetect = settings.file_autodetect.clone();
+
         let mut app = RustzxApp {
             emulator,
             snd,
@@ -118,17 +126,35 @@ impl RustzxApp {
             tex_border,
             tex_canvas,
             scale,
+            settings,
+            enable_frame_trace: cfg!(debug_assertions),
+            enable_joy_keyaboard_layer: false,
         };
 
-        if let Some(file) = settings.file_autodetect.as_ref() {
+        if let Some(file) = file_autodetect.as_ref() {
             app.load_file_autodetect(file)?;
         }
 
+        app.update_window_title();
+
         Ok(app)
     }
 
+    fn update_window_title(&mut self) {
+        let mut title = format!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
+
+        if self.enable_joy_keyaboard_layer {
+            title.push_str(" [JOY]");
+        }
+
+        if self.enable_frame_trace {
+            title.push_str(" [FRAME_TRACE]");
+        }
+
+        self.video.set_title(&title);
+    }
+
     pub fn start(&mut self) -> anyhow::Result<()> {
-        let mut debug = false;
         let scale = self.scale;
         let mut stopwatch = InstantStopwatch::default();
         'emulator: loop {
@@ -176,20 +202,21 @@ impl RustzxApp {
             );
             self.video.end();
             // check all events
-            if let Some(event) = self.events.pop_event() {
+            while let Some(event) = self.events.pop_event() {
                 match event {
                     Event::Exit => {
                         break 'emulator;
                     }
-                    Event::GameKey(key, state) => {
+                    Event::ZXKey(key, state) => {
                         self.emulator.send_key(key, state);
                     }
-                    Event::SwitchDebug => {
-                        debug = !debug;
-                        if !debug {
-                            self.video
-                                .set_title(&format!("RustZX v{}", env!("CARGO_PKG_VERSION")));
-                        }
+                    Event::SwitchFrameTrace => {
+                        self.enable_frame_trace = !self.enable_frame_trace;
+                        self.update_window_title();
+                    }
+                    Event::ChangeJoyKeyboardLayer(value) => {
+                        self.enable_joy_keyaboard_layer = value;
+                        self.update_window_title();
                     }
                     Event::ChangeSpeed(speed) => {
                         self.emulator.set_speed(speed);
@@ -197,9 +224,26 @@ impl RustzxApp {
                     Event::Kempston(key, state) => {
                         self.emulator.send_kempston_key(key, state);
                     }
+                    Event::Sinclair(num, key, state) => {
+                        self.emulator.send_sinclair_key(num, key, state);
+                    }
+                    Event::CompoundKey(key, state) => {
+                        self.emulator.send_compound_key(key, state);
+                    }
+                    Event::MouseMove { x, y } => {
+                        self.emulator.send_mouse_pos(x, y);
+                    }
+                    Event::MouseButton(buton, pressed) => {
+                        self.emulator.send_mouse_button(buton, pressed);
+                    }
+                    Event::MouseWheel(direction) => {
+                        self.emulator.send_mouse_wheel(direction);
+                    }
                     Event::InsertTape => self.emulator.play_tape(),
                     Event::StopTape => self.emulator.stop_tape(),
                     Event::OpenFile(path) => self.load_file_autodetect(&path)?,
+                    Event::QuickSave => self.quick_save()?,
+                    Event::QuickLoad => self.quick_load()?,
                 }
             }
             // how long emulation iteration was
@@ -212,12 +256,12 @@ impl RustzxApp {
             // get exceed clocks and use them on next iteration
             let frame_dt = frame_start.elapsed();
             // change window header
-            if debug {
-                self.video.set_title(&format!(
+            if self.enable_frame_trace {
+                log::trace!(
                     "CPU: {:7.3}ms; FRAME:{:7.3}ms",
                     cpu_dt.as_millis(),
                     frame_dt.as_millis()
-                ));
+                );
             }
         }
         Ok(())
@@ -240,4 +284,48 @@ impl RustzxApp {
         }
         Ok(())
     }
+
+    fn quick_save(&mut self) -> anyhow::Result<()> {
+        let new_path = self.last_quick_snapshot_path();
+        let prev_path = self.prev_quick_snapshot_path();
+
+        if new_path.exists() {
+            if prev_path.exists() {
+                fs::remove_file(&prev_path)?;
+            }
+            fs::rename(&new_path, &prev_path)?;
+        }
+
+        let recorder = SnapshotRecorder::Sna(FileAsset::from(File::create(new_path)?));
+        self.emulator
+            .save_snapshot(recorder)
+            .map_err(|e| anyhow!("Failed to save qick snapshot: {}", e))?;
+        Ok(())
+    }
+
+    fn quick_load(&mut self) -> anyhow::Result<()> {
+        let last_snapshot_path = self.last_quick_snapshot_path();
+        if !last_snapshot_path.exists() {
+            log::warn!("Quick snapshot was not found");
+            return Ok(());
+        }
+        self.emulator
+            .load_snapshot(host::load_snapshot(&last_snapshot_path)?)
+            .map_err(|e| anyhow!("Emulator failed to load quick snapshot: {}", e))?;
+        Ok(())
+    }
+
+    fn last_quick_snapshot_path(&self) -> PathBuf {
+        if let Some(path) = self.settings.file_autodetect.as_ref() {
+            return path.with_extension(".rustzx.last.sna");
+        }
+        Path::new("default.rustzx.last.sna").to_owned()
+    }
+
+    fn prev_quick_snapshot_path(&self) -> PathBuf {
+        if let Some(path) = self.settings.file_autodetect.as_ref() {
+            return path.with_extension(".rustzx.prev.sna");
+        }
+        Path::new("default.rustzx.prev.sna").to_owned()
+    }
 }
diff --git a/rustzx/src/app/settings.rs b/rustzx/src/app/settings.rs
index a32cb2e..9eb25e8 100644
--- a/rustzx/src/app/settings.rs
+++ b/rustzx/src/app/settings.rs
@@ -22,14 +22,24 @@ pub struct Settings {
     /// Disable fast tape loading
     #[structopt(long = "nofastload")]
     pub disable_fastload: bool,
+    /// Disable automatic tape loading via placing emulator to tape load state immediately
+    /// after launch
+    #[structopt(long = "noautoload")]
+    pub disable_autoload: bool,
     /// Set windows scale for emulator. Can be set as decimal non-zero value. Defaults to 2
     #[structopt(short, long, default_value = "2", parse(try_from_str = scale_from_str))]
     pub scale: usize,
-    /// Disable kempston joy support. In enabled, arrow and `Alt` keys are bound by default
-    /// to the kempston joy.
+    /// Disable kempston joy support. If enabled, arrow and `Alt` keys are bound by default
+    /// to the kempston joy
     #[structopt(long = "nokempston")]
     pub disable_kempston: bool,
-    /// Set AY-3-8910 sound chip mode. Can be set to `mono`, `abc`(stereo) or `acb`(stereo).
+    /// Enables kempston mouse support. If enabled, locks mouse in application
+    #[structopt(long = "mouse")]
+    pub enable_mouse: bool,
+    /// Sets mouse sensitivity [1..=100]. Defaults to 20
+    #[structopt(long = "mouse-sensitivity", default_value = "20")]
+    pub mouse_sensitivity: usize,
+    /// Set AY-3-8910 sound chip mode. Can be set to `mono`, `abc`(stereo) or `acb`(stereo)
     /// Defaults to `abc`
     #[structopt(long, default_value = "abc", parse(try_from_str = ay_mode_from_str))]
     /// Disable AY-3-8910 chip support
@@ -145,8 +155,9 @@ impl Settings {
         RustzxSettings {
             machine: self.machine,
             emulation_speed: self.speed,
-            tape_fastload: !self.disable_fastload,
-            enable_kempston: !self.disable_kempston,
+            tape_fastload_enabled: !self.disable_fastload,
+            kempston_enabled: !self.disable_kempston,
+            mouse_enabled: self.enable_mouse,
             ay_mode: self.ay_mode,
             ay_enabled,
             beeper_enabled: !self.disable_beeper,
@@ -154,6 +165,7 @@ impl Settings {
             sound_volume: 100,
             load_default_rom: self.rom.is_none(),
             sound_sample_rate: self.sound_sample_rate,
+            autoload_enabled: !self.disable_autoload,
         }
     }
 }
diff --git a/rustzx/src/app/video/video_sdl.rs b/rustzx/src/app/video/video_sdl.rs
index d421f01..2010794 100644
--- a/rustzx/src/app/video/video_sdl.rs
+++ b/rustzx/src/app/video/video_sdl.rs
@@ -32,11 +32,7 @@ impl VideoSdl {
                 SCREEN_HEIGHT * settings.scale,
             );
             let window = video
-                .window(
-                    &format!("RustZX v{}", env!("CARGO_PKG_VERSION")),
-                    width as u32,
-                    height as u32,
-                )
+                .window("RustZX", width as u32, height as u32)
                 .position_centered()
                 .opengl()
                 .build()
diff --git a/rustzx/src/host/io.rs b/rustzx/src/host/io.rs
index 93b5e6a..66b94c0 100644
--- a/rustzx/src/host/io.rs
+++ b/rustzx/src/host/io.rs
@@ -1,11 +1,11 @@
 use rustzx_core::{
-    error::AssetReadError,
-    host::{LoadableAsset, SeekFrom},
+    error::IoError,
+    host::{DataRecorder, LoadableAsset, SeekFrom, SeekableAsset},
 };
 
 use std::{
     fs::File,
-    io::{Read, Seek},
+    io::{Read, Seek, Write},
 };
 
 pub struct FileAsset {
@@ -18,25 +18,36 @@ impl From<File> for FileAsset {
     }
 }
 
-impl LoadableAsset for FileAsset {
-    fn read(&mut self, buf: &mut [u8]) -> Result<usize, AssetReadError> {
-        self.file.read(buf).map_err(|e| {
-            log::error!("Failed to read asset: {}", e);
-            AssetReadError::HostAssetImplFailed
-        })
-    }
-
-    fn seek(&mut self, pos: SeekFrom) -> Result<usize, AssetReadError> {
+impl SeekableAsset for FileAsset {
+    fn seek(&mut self, pos: SeekFrom) -> Result<usize, IoError> {
         self.file
             .seek(into_std_seek_pos(pos))
             .map_err(|e| {
                 log::error!("Failed to seeek asset: {}", e);
-                AssetReadError::HostAssetImplFailed
+                IoError::HostAssetImplFailed
             })
             .map(|count| count as usize)
     }
 }
 
+impl LoadableAsset for FileAsset {
+    fn read(&mut self, buf: &mut [u8]) -> Result<usize, IoError> {
+        self.file.read(buf).map_err(|e| {
+            log::error!("Failed to read asset: {}", e);
+            IoError::HostAssetImplFailed
+        })
+    }
+}
+
+impl DataRecorder for FileAsset {
+    fn write(&mut self, buf: &[u8]) -> Result<usize, IoError> {
+        self.file.write(buf).map_err(|e| {
+            log::error!("Failed to write data to file: {}", e);
+            IoError::HostAssetImplFailed
+        })
+    }
+}
+
 fn into_std_seek_pos(pos: SeekFrom) -> std::io::SeekFrom {
     match pos {
         SeekFrom::Start(offset) => std::io::SeekFrom::Start(offset as u64),
diff --git a/rustzx/src/host/mod.rs b/rustzx/src/host/mod.rs
index 190a571..5fbc868 100644
--- a/rustzx/src/host/mod.rs
+++ b/rustzx/src/host/mod.rs
@@ -3,7 +3,7 @@ mod io;
 
 use anyhow::{anyhow, bail, Context};
 use frame_buffer::{FrameBufferContext, RgbaFrameBuffer};
-use io::FileAsset;
+pub use io::FileAsset;
 use rustzx_core::{
     host::{FrameBuffer, Host, HostContext, RomFormat, RomSet, Snapshot, Tape},
     zx::machine::ZXMachine,
@@ -19,7 +19,6 @@ impl Host for AppHost {
     type Context = AppHostContext;
     type FrameBuffer = RgbaFrameBuffer;
     type RomSet = FileRomSet;
-    type SnapshotAsset = FileAsset;
     type TapeAsset = FileAsset;
 }
 
diff --git a/vtx/Cargo.toml b/vtx/Cargo.toml
index 418d181..2a4c89a 100644
--- a/vtx/Cargo.toml
+++ b/vtx/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "vtx"
-version = "0.1.0"
+version = "0.1.1"
 description = "VTX sound format parsing and playback library"
 authors = ["The RustZX contributors", "Vladyslav Nikonov <mail@pacmancoder.xyz>"]
 edition = "2018"
@@ -14,7 +14,7 @@ thiserror = "1"
 byteorder = "1"
 num-traits = { version = "0.2", default-features = false }
 num-derive = "0.3"
-aym = { version = "0.1", path = "../aym" }
+aym = { version = "0.1.1", path = "../aym" }
 
 [dev-dependencies]
 expect-test = "1"
diff --git a/vtx/src/lib.rs b/vtx/src/lib.rs
index 465952b..985acfa 100644
--- a/vtx/src/lib.rs
+++ b/vtx/src/lib.rs
@@ -1,8 +1,6 @@
 //! VTX sound format parsing library
 //!
 //! See sources of `vtx-bin` crate for usage example
-#![allow(dead_code)]
-
 pub mod player;
 
 use byteorder::{LittleEndian, ReadBytesExt};
diff --git a/vtx/vtx-bin/Cargo.toml b/vtx/vtx-bin/Cargo.toml
index 50d2eba..7524d13 100644
--- a/vtx/vtx-bin/Cargo.toml
+++ b/vtx/vtx-bin/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "vtx-bin"
 description = "VTX sound format toolbox"
-version = "0.1.0"
+version = "0.1.1"
 authors = ["The RustZX contributors", "Vladyslav Nikonov <mail@pacmancoder.xyz>"]
 edition = "2018"
 license = "MIT"


Test patch: