Repository: rustzx/rustzx (language: rust)

Issue:
Eliminate loading a whole file to vector in tap loader
Currently we already have file-like `libcore`-friendly abstraction over currently loading tap asset but still perform read to `Vec` instead of `seek` + `read` on demand; This will allow to port `rustzx-core` to more resource-restricted hosts.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5f78e0b..93aaa56 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,7 +14,7 @@ jobs:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        os: [ubuntu-latest, windows-latest]
+        os: [ubuntu-latest, windows-latest, macos-latest]
     steps:
       - uses: actions/checkout@v2
       - uses: actions-rs/toolchain@v1
@@ -38,6 +38,16 @@ jobs:
         with:
           command: build
           args: --release --all --all-features --verbose
+      - name: Lib build - Minimal
+        uses: actions-rs/cargo@v1
+        with:
+          command: build
+          args: --release -p rustzx-core --verbose
+      - name: Lib build - 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:
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..5a0e921
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,78 @@
+### RustZX v0.11
+Authors:
+- Vladyslav Nikonov (@pacmancoder)
+
+Changes:
+- **[Feature]** Separated project to `rustzx` application and `no_std`-capable `rustzx-core` crate
+- **[Feature]** Updated CLI
+    - More features now enabled by default
+    - 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)
+- **[Infrastructure]** Moved CI to _Github Actions_
+    - Added `rustfmt` step to CI
+    - Added `clippy` step to CI
+- **[Refactroing]** Performed deep global refactoring of the project
+    - Updated to Rust 2018 edition
+    - Updated dependencies
+    - Fixed all `clippy` errors
+    - Minimized `rustzx-core` public interface
+    - Added feature gates for resource-hungry `rustzx-core` features
+    - Eliminated a lot of not redundant code
+    - Made groundwork for a future emulator features
+
+### RustZX v0.9
+Authors:
+- Konstantin Mochalov (@kolen)
+
+Changes:
+- **[Feature]** Drag-n-drop support for TAP and SNA files
+- **[Refactoring]** Multiple small refactoring changes
+- **[Dependencies]** Updated `sdl` crate
+
+### Pre-pelease
+Authors:
+- Vladyslav Nikonov (@pacmancoder)
+
+Changes:
+- **[19.08.2016]** RustzxApp and RustzxSettings refactoring
+- **[18.08.2016]** Moved sound, video, event to sdl lib.
+- **[15.08.2016]** Moving from **portaudio** to **cpal**
+- **[14.08.2016]** Kempston Joystick
+- **[14.08.2016]** AY implementation finished
+- **[12.08.2016]** AY implementation start
+- **[09.08.2016]** Refactoring
+- **[08.08.2016]** Aspect ratio correction, custom 128K rom loading
+- **[08.08.2016]** Window scale selection with `--scale` option
+- **[07.07.2016]** ZXScreen rewrite
+- **[06.07.2016]** Base 128K features implemented
+- **[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]** SNA files loading
+- **[11.06.2016]** Command line arguments using **clap** crate
+- **[11.06.2016]** Tap files fast loading implemented, finnaly!
+- **[07.06.2016]** Speed improvements (maybe :smile:) in flag setting [z80]
+- **[04.06.2016]** Border FX implemented
+- **[28.05.2016]** Some architecture rewrite, working on border
+- **[21.05.2016]** OVERSCAN and SHOCK demo's passed! :sparkles:
+- **[21.05.2016]** Screen reorganization and OpenGL rendering part fix
+- **[19.05.2016]** Documentation, Rustfmt
+- **[15.05.2016]** Fixed bug in INC/DEC (IX/IY + dd). After 2 weeks :smile:
+- **[12.05.2016]** Fixed CALL timings
+- **[06.05.2016]** Floating bus fix
+- **[28.04.2016]** All contentions implemented!
+- **[24.04.2016]** Almost all contentions working perfectly (IO still broken)
+- **[24.04.2016]** IM2 bug fixed, finally I found it! :smile:
+- **[24.04.2016]** new Z80Bus interface, serious z80 emulation part rewrite
+- **[22.04.2016]** work on implementing correct timings started.
+- **[18.04.2016]** fixed many instruction bugs (IO section still not finished)
+- **[12.04.2016]** fixed shader bug causing bad performance - palette was declared as non-const
+- **[12.04.2016]** log file added
+- **[29.03.2016]** Screen emulation, keyboard, test run of ROMs
+- **[14.03.2016]** All features of CPU have been implemented :sunglasses:
+- **[11.03.2016]** Serious code reorganization
+- **[06.03.2016]** All Z80 instruction groups have been implemented! :tada:
+- **[02.02.2016]** First commit
diff --git a/Cargo.lock b/Cargo.lock
index ed18f98..0077fcc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,6 +17,15 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "ansi_term"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "anyhow"
 version = "1.0.40"
@@ -74,36 +83,19 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "clap"
-version = "3.0.0-beta.2"
+version = "2.33.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
 dependencies = [
+ "ansi_term",
  "atty",
  "bitflags",
- "clap_derive",
- "indexmap",
- "lazy_static",
- "os_str_bytes",
- "strsim 0.10.0",
- "termcolor",
+ "strsim 0.8.0",
  "textwrap",
  "unicode-width",
  "vec_map",
 ]
 
-[[package]]
-name = "clap_derive"
-version = "3.0.0-beta.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1"
-dependencies = [
- "heck",
- "proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
-]
-
 [[package]]
 name = "cmake"
 version = "0.1.45"
@@ -259,12 +251,6 @@ version = "0.3.55"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
 
-[[package]]
-name = "hashbrown"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
-
 [[package]]
 name = "heck"
 version = "0.3.2"
@@ -295,16 +281,6 @@ version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
 
-[[package]]
-name = "indexmap"
-version = "1.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
-dependencies = [
- "autocfg",
- "hashbrown",
-]
-
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
@@ -348,12 +324,6 @@ version = "1.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
 
-[[package]]
-name = "os_str_bytes"
-version = "2.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85"
-
 [[package]]
 name = "proc-macro-error"
 version = "1.0.4"
@@ -424,21 +394,22 @@ checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548"
 
 [[package]]
 name = "rustzx"
-version = "0.10.0"
+version = "0.11.0"
 dependencies = [
  "anyhow",
- "clap",
  "env_logger",
  "log",
  "rustzx-core",
  "sdl2",
+ "structopt",
 ]
 
 [[package]]
 name = "rustzx-core"
-version = "0.10.0"
+version = "0.11.0"
 dependencies = [
  "ayumi",
+ "bitflags",
  "displaydoc",
  "enum_dispatch",
  "from_variants",
@@ -472,6 +443,12 @@ dependencies = [
  "version-compare",
 ]
 
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
 [[package]]
 name = "strsim"
 version = "0.9.3"
@@ -479,10 +456,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
 
 [[package]]
-name = "strsim"
-version = "0.10.0"
+name = "structopt"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c"
+dependencies = [
+ "clap",
+ "lazy_static",
+ "structopt-derive",
+]
+
+[[package]]
+name = "structopt-derive"
+version = "0.4.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
 
 [[package]]
 name = "syn"
@@ -517,9 +512,9 @@ dependencies = [
 
 [[package]]
 name = "textwrap"
-version = "0.12.1"
+version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
 dependencies = [
  "unicode-width",
 ]
diff --git a/LOG.md b/LOG.md
deleted file mode 100644
index 9ba1d08..0000000
--- a/LOG.md
+++ /dev/null
@@ -1,40 +0,0 @@
-**[19.08.2016]** RustzxApp and RustzxSettings refactoring  
-**[18.08.2016]** Moved sound, video, event to sdl lib.  
-**[15.08.2016]** Moving from **portaudio** to **cpal**  
-**[14.08.2016]** Kempston Joystick  
-**[14.08.2016]** AY implementation finished  
-**[12.08.2016]** AY implementation start  
-**[09.08.2016]** Refactoring  
-**[08.08.2016]** Aspect ratio correction, custom 128K rom loading  
-**[08.08.2016]** Window scale selection with `--scale` option  
-**[07.07.2016]** ZXScreen rewrite  
-**[06.07.2016]** Base 128K features implemented  
-**[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]** SNA files loading  
-**[11.06.2016]** Command line arguments using **clap** crate  
-**[11.06.2016]** Tap files fast loading implemented, finnaly!  
-**[07.06.2016]** Speed improvements (maybe :smile:) in flag setting [z80]    
-**[04.06.2016]** Border FX implemented  
-**[28.05.2016]** Some architecture rewrite, working on border    
-**[21.05.2016]** OVERSCAN and SHOCK demo's passed! :sparkles:     
-**[21.05.2016]** Screen reorganization and OpenGL rendering part fix  
-**[19.05.2016]** Documentation, Rustfmt  
-**[15.05.2016]** Fixed bug in INC/DEC (IX/IY + dd). After 2 weeks :smile:  
-**[12.05.2016]** Fixed CALL timings  
-**[06.05.2016]** Floating bus fix  
-**[28.04.2016]** All contentions implemented!  
-**[24.04.2016]** Almost all contentions working perfectly (IO still broken)  
-**[24.04.2016]** IM2 bug fixed, finally I found it! :smile:  
-**[24.04.2016]** new Z80Bus interface, serious z80 emulation part rewrite  
-**[22.04.2016]** work on implementing correct timings started.  
-**[18.04.2016]** fixed many instruction bugs (IO section still not finished)  
-**[12.04.2016]** fixed shader bug causing bad performance - palette was declared as non-const  
-**[12.04.2016]** log file added  
-**[29.03.2016]** Screen emulation, keyboard, test run of ROMs    
-**[14.03.2016]** All features of CPU have been implemented :sunglasses:  
-**[11.03.2016]** Serious code reorganization  
-**[06.03.2016]** All Z80 instruction groups have been implemented! :tada:  
-**[02.02.2016]** First commit  
diff --git a/README.md b/README.md
index ed9b3bb..d54c4b5 100644
--- a/README.md
+++ b/README.md
@@ -1,60 +1,54 @@
-# rustzx
-![logo](assets/logo_small.png)
+RustZX
+===============================
+
+[![RustZX CI](https://github.com/pacmancoder/rustzx/actions/workflows/ci.yml/badge.svg)](https://github.com/pacmancoder/rustzx/actions/workflows/ci.yml)
 
-ZX Spectrum emulator which I writing in rust.
-I develop this project just for fun and for learning the basics of computer
-architecture.
-Licensed under MIT License.
+![logo](assets/logo_small.png)
 
-**Watch [this](https://youtu.be/Xho3GWFyP2I) video showcase!**
+ZX Spectrum emulator written in Rust
+- Watch [this](https://youtu.be/Xho3GWFyP2I) video showcase (`v0.9.x`)
+- Read [CHANGELOG.md](CHANGELOG.md) for info on the latest version (`v0.11.x`) changes
 
-[![Build Status](https://travis-ci.org/pacmancoder/rustzx.svg?branch=master)](https://travis-ci.org/pacmancoder/rustzx)
 ## Features
 - Written in pure rust
 - Cross-platform
-- Documented source
 - Full ZX Spectrum 48K and 128K emulation
 - Perfect emulation of Z80 core
 - Highly precise AY chip emulation with Ayumi library
 - Beeper sound emulation
-- Can handle tap, sna files
+- Supported formats: TAP, SNA
 - Fast loading of tap files with standard loader
-- Emulates border
-- Kempston joystick emulation
-- Correct contentons
-
-## Download [v0.9.2]
-Packages are available in github releases.
-**Note:** On linux platforms sdl2 library must be installed
+- Precise timings
+- Full border emulation
+- Joystick emulation: Kempston
+- Separate `no_std` core library which can be used to port emulator
+  almost anywhere.
+    - Global allocator is still needed, but all dynamic
+       allocations were minimized
+    - All resource-heavy features are configurable via cargo `features`
 
-## Compiling
-Before compiling make shure that you have C compiller and CMate to
+## Compiling from the latest master
+1. Sure that you have C compiller and CMake to
 build bundled `sdl2`
-
-Then just install it with cargo
-
+2. Install it with cargo
 ```bash
-cargo install
+cargo install --path ./rustzx
 ```
-For advanced info use `--help` flag
 
 ## How to use
-Here some examples of usage:
 ```bash
-rustzx --help
-rustzx --fastload --tap test.tap
-rustzx -f --128k --AY abc --tap test128.tap
-rustzx --rom tester.rom --scale 3 --volume 50
+rustzx --help # Show help
+rustzx test.tap # Autodetect file type and run in 48K mode
+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
 ```
 For loading tape in 48K mode, press `j` then `Ctrl+p` twice, as on real Spectrum.
-You must see `LOAD ""` on emulator's screen. And then press `Enter`.
-If you `--fastload` option before launching, game will be launched, in other
-case press `Insert` to insert tape. `Delete` can be used for ejecting tape from
-tape reader. `--128k` flag launches emulator in 128K mode. For loading tape just
-press `Enter`.
+You should see `LOAD ""` on emulator's screen, then press `Enter` (in 128K mode just press enter).
+In `--nofastload` mode, press `Insert` to play the tape and `Delete` to stop
 
-If you have some audio troubles - use `--latency` flag with bigger samples
-count.
+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.
@@ -64,13 +58,11 @@ Use `F6` to display FPS in window title.
 ![](screenshots/q.png)
 ![](screenshots/arkanoid.png)
 ![](screenshots/sentinel.png)
-## Log
-Watch [LOG](LOG.md) for details and github issues
-for current plans and help requests.
+
 ## References
-Of course, I used many resources to find out, how to build my first
-emulator in life. So there is a list of useful references, from where I dig most
-information about Z80, ULA and other ZX Spectrum hardware parts:
+Many resources were used to find out, how to buildthis emulator.
+Huge thanks to the following resources which helped to figure out a lot of
+defails about ZX Spectrum.
 - Of course [z80.info](http://www.z80.info/)
     - [Decoding Z80 opcodes](http://www.z80.info/decoding.htm)
     - [Opcodes list](http://www.z80.info/z80code.txt)
@@ -93,6 +85,8 @@ information about Z80, ULA and other ZX Spectrum hardware parts:
 - [FUSE](http://fuse-emulator.sourceforge.net/) emulator source for finding out correct timings
 
 ## ROM's
-Emulator contains ROM's, created by by Sinclair Research Ltd (now owned by Amstrad plc),
-Amstrad was given permissions for distributing their ROM's with emulators, so they are
-included in source of emulator (mod zx::roms). More about this read [here](https://groups.google.com/forum/?hl=en#!msg/comp.sys.amstrad.8bit/HtpBU2Bzv_U/HhNDSU3MksAJ)
+Emulator contains ROMs, created by by Sinclair Research Ltd (now owned by Amstrad plc),
+Amstrad has [given](https://groups.google.com/forum/?hl=en#!msg/comp.sys.amstrad.8bit/HtpBU2Bzv_U/HhNDSU3MksAJ)
+permission to distribute their ROM's in conjunction with emulators.
+In RustZX these ROMs included in source of the core emulator library `mod rustzx_core::zx::roms`. Embedded roms
+can be opted-out from the core library by disabling feature `embedded-roms`.
diff --git a/rustfmt.toml b/rustfmt.toml
index 4301c1c..c5d444e 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -1,11 +1,13 @@
 use_field_init_shorthand = true
 use_try_shorthand = true
+# Available in nightly, but does not affect success of
+# stable rustfmt
+report_fixme = "Unnumbered"
+report_todo = "Unnumbered"
+imports_granularity = "Crate"
+normalize_comments = true
+condense_wildcard_suffixes = true
+format_strings = true
+reorder_impl_items = true
 # Uncomment following if available in stable rustfmt
-# imports_granularity = "Crate"
-# normalize_comments = true
-# condense_wildcard_suffixes = true
-# format_strings = true
 # imports_layout = "HorizontalVertical"
-# reorder_impl_items = true
-# report_fixme = "Unnumbered"
-# report_todo = "Unnumbered"
diff --git a/rustzx-core/Cargo.toml b/rustzx-core/Cargo.toml
index 7a8bcc6..497f36b 100644
--- a/rustzx-core/Cargo.toml
+++ b/rustzx-core/Cargo.toml
@@ -1,17 +1,26 @@
 [package]
 name = "rustzx-core"
-version = "0.10.0"
+version = "0.11.0"
 authors = ["Vladyslav Nikonov <mail@pacmancoder.xyz>"]
-publish = false
 description = "ZX Spectum emulator core library"
 repository = "https://github.com/pacmancoder/rustzx"
 keywords = ["emulator", "game", "z80", "no_std"]
 license = "MIT"
 edition = "2018"
 
+[features]
+default = []
+full = ["ay", "precise-border", "embedded-roms"]
+without-ay = ["sound", "precise-border", "embedded-roms"]
+precise-border = []
+embedded-roms = []
+sound = []
+ay = ["ayumi", "sound"]
+
 [dependencies]
-lazy_static = "1.4"
+bitflags = "1"
+lazy_static = "1"
 displaydoc = { version = "0.2", default-features = false }
 from_variants = "0.6"
 enum_dispatch = "0.3"
-ayumi = { git = "https://github.com/pacmancoder/rust-ayumi.git", rev = "9e57902" }
+ayumi = { git = "https://github.com/pacmancoder/rust-ayumi.git", rev = "9e57902", optional = true }
diff --git a/rustzx-core/src/emulator/loaders/sna.rs b/rustzx-core/src/emulator/loaders/sna.rs
index 6297782..8464cb1 100644
--- a/rustzx-core/src/emulator/loaders/sna.rs
+++ b/rustzx-core/src/emulator/loaders/sna.rs
@@ -3,58 +3,53 @@ use crate::{
     host::{Host, LoadableAsset},
     utils::{make_word, Clocks},
     z80::{opcodes::execute_pop_16, RegName16},
-    zx::colors::ZXColor,
+    zx::video::colors::ZXColor,
     Result,
 };
-use alloc::vec::Vec;
 
 /// SNA snapshot loading function
 pub fn load_sna<H: Host>(emulator: &mut Emulator<H>, mut asset: H::SnapshotAsset) -> Result<()> {
-    // TODO(#54): Eliminate loading a whole file to vector in sna loader
-    let mut data = Vec::new();
-    asset.read_to_end(&mut data)?;
-    assert!(data.len() == 49179);
+    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(data[0]);
+    emulator.cpu.regs.set_i(header[0]);
     // alt-regs
-    emulator.cpu.regs.set_hl(make_word(data[2], data[1]));
-    emulator.cpu.regs.set_de(make_word(data[4], data[3]));
-    emulator.cpu.regs.set_bc(make_word(data[6], data[5]));
+    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(data[8], data[7]));
+    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(data[10], data[9]));
-    emulator.cpu.regs.set_de(make_word(data[12], data[11]));
-    emulator.cpu.regs.set_bc(make_word(data[14], data[13]));
+    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(data[16], data[15]));
-    emulator.cpu.regs.set_ix(make_word(data[18], data[17]));
+    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 = (data[19] & 0x04) != 0;
+    let iff = (header[19] & 0x04) != 0;
     emulator.cpu.regs.set_iff1(iff);
     emulator.cpu.regs.set_iff2(iff);
     // r
-    emulator.cpu.regs.set_r(data[20]);
+    emulator.cpu.regs.set_r(header[20]);
     // af
-    emulator.cpu.regs.set_af(make_word(data[22], data[21]));
+    emulator.cpu.regs.set_af(make_word(header[22], header[21]));
     // sp
-    emulator.cpu.regs.set_sp(make_word(data[24], data[23]));
+    emulator.cpu.regs.set_sp(make_word(header[24], header[23]));
     // interrupt mode
-    emulator.cpu.set_im(data[25] & 0x03);
-    // set border
+    emulator.cpu.set_im(header[25] & 0x03);
     emulator
         .controller
-        .border
-        .set_border(Clocks(0), ZXColor::from_bits(data[26] & 0x07));
+        .set_border_color(Clocks(0), ZXColor::from_bits(header[26] & 0x07));
     // ram pages
-    let page = emulator.controller.memory.ram_page_data_mut(0);
-    page.copy_from_slice(&data[27..16411]);
-    let page = emulator.controller.memory.ram_page_data_mut(1);
-    page.copy_from_slice(&data[16411..32795]);
-    let page = emulator.controller.memory.ram_page_data_mut(2);
-    page.copy_from_slice(&data[32795..49179]);
+    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(
diff --git a/rustzx-core/src/emulator/loaders/tap.rs b/rustzx-core/src/emulator/loaders/tap.rs
index 9b62c13..934d579 100644
--- a/rustzx-core/src/emulator/loaders/tap.rs
+++ b/rustzx-core/src/emulator/loaders/tap.rs
@@ -3,13 +3,12 @@ use crate::{
     emulator::Emulator,
     host::Host,
     utils::{make_word, Clocks},
-    z80::*,
+    z80::{opcodes, RegName16, Z80Bus, FLAG_CARRY, FLAG_ZERO},
     zx::tape::TapeImpl,
+    Result,
 };
 
-pub fn fast_load_tap<H: Host>(emulator: &mut Emulator<H>) {
-    // resetting tape pos to beginning.
-    emulator.controller.tape.reset_pos_in_block();
+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'
     emulator.cpu.regs.swap_af_alt();
@@ -18,20 +17,22 @@ pub fn fast_load_tap<H: Host>(emulator: &mut Emulator<H>) {
     let mut acc = emulator.cpu.regs.get_acc();
     // variable to store resulting flags
     let mut result_flags;
-    // pos relative to block start
-    let mut pos = 0;
     // destination address in RAM
     let mut dest = emulator.cpu.regs.get_reg_16(RegName16::IX);
     // remaining length
     let mut length = emulator.cpu.regs.get_reg_16(RegName16::DE);
     // parity accumulator and current byte (h, l) regs
     let (mut parity_acc, mut current_byte) = (0, 0);
+    // move to next block
+    if !emulator.controller.tape.next_block()? {
+        return Ok(());
+    }
+
     'loader: loop {
         // if we still on block
-        if let Some(byte) = emulator.controller.tape.block_byte(pos) {
+        if let Some(byte) = emulator.controller.tape.next_block_byte()? {
             // set current byte, shift position and do parity check iteration
             current_byte = byte;
-            pos += 1;
             parity_acc ^= current_byte;
             // no bytes left, set A to parity accumulator (works as in ROM)
             // and check parity last time
@@ -98,6 +99,5 @@ pub fn fast_load_tap<H: Host>(emulator: &mut Emulator<H>) {
         );
     }
     emulator.cpu.regs.set_flags(f);
-    // move to next block
-    emulator.controller.tape.next_block();
+    Ok(())
 }
diff --git a/rustzx-core/src/emulator/mod.rs b/rustzx-core/src/emulator/mod.rs
index a83b22a..f112991 100644
--- a/rustzx-core/src/emulator/mod.rs
+++ b/rustzx-core/src/emulator/mod.rs
@@ -5,27 +5,32 @@ use crate::{
     error::RomLoadError,
     host::{Host, LoadableAsset, RomFormat, RomSet, Snapshot, Tape},
     settings::RustzxSettings,
-    utils::*,
-    z80::*,
+    utils::EmulationSpeed,
+    z80::Z80,
     zx::{
+        controller::ZXController,
+        events::EmulationEvents,
         joy::kempston::KempstonKey,
-        sound::sample::SoundSample,
+        keys::ZXKey,
         tape::{Tap, TapeImpl},
-        ZXController, ZXKey,
+        video::colors::ZXColor,
     },
     Result,
 };
 
+#[cfg(feature = "sound")]
+use crate::zx::sound::sample::SoundSample;
+
 use core::time::Duration;
 
 /// Represents main Emulator structure
 pub struct Emulator<H: Host> {
     settings: RustzxSettings,
     cpu: Z80,
-    // TODO(#52): eliminate direct access to the controller
-    pub controller: ZXController<H>,
+    controller: ZXController<H>,
     speed: EmulationSpeed,
     fast_load: bool,
+    #[cfg(feature = "sound")]
     sound_enabled: bool,
 }
 
@@ -38,13 +43,14 @@ impl<H: Host> Emulator<H> {
     /// Constructs new emulator
     /// # Arguments
     /// `settings` - emulator settings
-    pub fn new(settings: RustzxSettings) -> Result<Self> {
+    pub fn new(settings: RustzxSettings, context: H::Context) -> Result<Self> {
         let speed = settings.emulation_speed;
         let fast_load = settings.tape_fastload;
+        #[cfg(feature = "sound")]
         let sound_enabled = settings.sound_enabled;
 
         let cpu = Z80::default();
-        let controller = ZXController::<H>::new(&settings);
+        let controller = ZXController::<H>::new(&settings, context);
 
         let this = Self {
             settings,
@@ -52,6 +58,7 @@ impl<H: Host> Emulator<H> {
             controller,
             speed,
             fast_load,
+            #[cfg(feature = "sound")]
             sound_enabled,
         };
 
@@ -69,11 +76,13 @@ impl<H: Host> Emulator<H> {
     }
 
     /// changes sound playback flag
+    #[cfg(feature = "sound")]
     pub fn set_sound(&mut self, value: bool) {
         self.sound_enabled = value;
     }
 
     /// function for sound generation request check
+    #[cfg(feature = "sound")]
     pub fn have_sound(&self) -> bool {
         // enable sound only if speed is normal
         if let EmulationSpeed::Definite(1) = self.speed {
@@ -125,6 +134,19 @@ impl<H: Host> Emulator<H> {
         self.controller.tape.stop();
     }
 
+    pub fn screen_buffer(&self) -> &H::FrameBuffer {
+        self.controller.screen.frame_buffer()
+    }
+
+    #[cfg(feature = "precise-border")]
+    pub fn border_buffer(&self) -> &H::FrameBuffer {
+        self.controller.border.frame_buffer()
+    }
+
+    pub fn border_color(&self) -> ZXColor {
+        self.controller.border_color
+    }
+
     pub fn send_key(&mut self, key: ZXKey, pressed: bool) {
         self.controller.send_key(key, pressed);
     }
@@ -135,32 +157,25 @@ impl<H: Host> Emulator<H> {
         }
     }
 
+    #[cfg(feature = "sound")]
     pub fn next_audio_sample(&mut self) -> Option<SoundSample<f32>> {
         self.controller.mixer.pop()
     }
 
-    fn process_event(&mut self, event: Event) {
-        let Event { kind: e, time: _ } = event;
-        match e {
-            // Fast tape loading found, use it
-            EventKind::FastTapeLoad if self.controller.tape.can_fast_load() && self.fast_load => {
-                loaders::tap::fast_load_tap(self);
-            }
-            _ => {}
-        }
-    }
-
-    // processes all events, happened at frame emulation cycle
-    fn process_all_events(&mut self) {
-        while let Some(event) = self.controller.pop_event() {
-            self.process_event(event);
+    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
+        {
+            loaders::tap::fast_load_tap(self)?;
         }
+        Ok(())
     }
 
     /// Emulate frames, maximum in `max_time` time, returns emulation time in nanoseconds
     /// in most cases time is max 1/50 of second, even when using
     /// loader acceleration
-    pub fn emulate_frames<S>(&mut self, max_time: Duration, stopwatch: &mut S) -> Duration
+    pub fn emulate_frames<S>(&mut self, max_time: Duration, stopwatch: &mut S) -> Result<Duration>
     where
         S: Stopwatch,
     {
@@ -172,17 +187,19 @@ impl<H: Host> Emulator<H> {
             self.controller.reset_frame_counter();
             'cpu: loop {
                 // Emulation step. if instant event happened then accept in and execute
-                if !self.cpu.emulate(&mut self.controller) {
-                    if let Some(event) = self.controller.pop_event() {
-                        self.process_event(event);
-                    }
+                self.cpu.emulate(&mut self.controller);
+                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();
                 }
                 // If speed is defined
                 if let EmulationSpeed::Definite(multiplier) = self.speed {
                     if self.controller.frames_count() >= multiplier {
                         // no more frames
-                        self.controller.clear_events();
-                        return stopwatch.measure();
+                        return Ok(stopwatch.measure());
                     };
                 // if speed is maximal.
                 } else {
@@ -198,7 +215,6 @@ impl<H: Host> Emulator<H> {
                 break 'frame;
             }
         }
-        self.controller.clear_events();
-        time
+        Ok(time)
     }
 }
diff --git a/rustzx-core/src/error.rs b/rustzx-core/src/error.rs
index 8dbd531..5c96cf6 100644
--- a/rustzx-core/src/error.rs
+++ b/rustzx-core/src/error.rs
@@ -7,6 +7,8 @@ pub enum Error {
     AssetRead(AssetReadError),
     /// Failed to load rom
     RomLoad(RomLoadError),
+    /// Failed to load rom
+    TapeLoad(TapeLoadError),
 }
 
 #[derive(Debug, Display)]
@@ -22,3 +24,9 @@ pub enum RomLoadError {
     /// More assets required to load rom
     MoreAssetsRequired,
 }
+
+#[derive(Debug, Display)]
+pub enum TapeLoadError {
+    /// Provided tap file is invalid
+    InvalidTapFile,
+}
diff --git a/rustzx-core/src/host/frame_buffer.rs b/rustzx-core/src/host/frame_buffer.rs
new file mode 100644
index 0000000..56620a4
--- /dev/null
+++ b/rustzx-core/src/host/frame_buffer.rs
@@ -0,0 +1,14 @@
+use crate::zx::video::colors::{ZXBrightness, ZXColor};
+
+pub enum FrameBufferSource {
+    Screen,
+    Border,
+}
+
+pub trait FrameBuffer {
+    type Context: Clone;
+    /// Creates canvas size with required dimensions (`width`, `height`)
+    fn new(width: usize, height: usize, source: FrameBufferSource, context: Self::Context) -> Self;
+    /// Set `color` with `brightness` for pixel on canvas at (`x`, `y`)
+    fn set_color(&mut self, x: usize, y: usize, color: ZXColor, brightness: ZXBrightness);
+}
diff --git a/rustzx-core/src/host/io.rs b/rustzx-core/src/host/io.rs
index 504e648..9ba902a 100644
--- a/rustzx-core/src/host/io.rs
+++ b/rustzx-core/src/host/io.rs
@@ -36,14 +36,4 @@ pub trait LoadableAsset {
 
         Ok(())
     }
-
-    fn read_to_end(&mut self, buf: &mut alloc::vec::Vec<u8>) -> Result<()> {
-        let mut buffer = [0u8; 1024];
-        let mut read_bytes = self.read(&mut buffer)?;
-        while read_bytes != 0 {
-            buf.extend_from_slice(&buffer[0..read_bytes]);
-            read_bytes = self.read(&mut buffer)?;
-        }
-        Ok(())
-    }
 }
diff --git a/rustzx-core/src/host/mod.rs b/rustzx-core/src/host/mod.rs
index 1f9ca8e..d1c97c2 100644
--- a/rustzx-core/src/host/mod.rs
+++ b/rustzx-core/src/host/mod.rs
@@ -1,5 +1,7 @@
+mod frame_buffer;
 mod io;
 
+pub use frame_buffer::{FrameBuffer, FrameBufferSource};
 pub use io::{LoadableAsset, SeekFrom};
 
 pub enum Snapshot<LoadableAssetImpl: LoadableAsset> {
@@ -23,13 +25,22 @@ pub trait RomSet {
     fn next_asset(&mut self) -> Option<Self::Asset>;
 }
 
+pub trait HostContext<H: Host + ?Sized>: Sized {
+    fn frame_buffer_context(&self) -> <H::FrameBuffer as FrameBuffer>::Context;
+}
+
 /// Represents set of required types for emulator implementation
 /// based on `rustzx-core`.
 pub trait Host {
+    /// Immutable `Context` implementation which is used to obtain host-specific
+    /// 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;
     /// File-like type implementation for rom loading
     type RomSet: RomSet;
+    /// Frame buffer implementation
+    type FrameBuffer: FrameBuffer;
 }
diff --git a/rustzx-core/src/lib.rs b/rustzx-core/src/lib.rs
index 6c0c18b..bbd415c 100644
--- a/rustzx-core/src/lib.rs
+++ b/rustzx-core/src/lib.rs
@@ -1,15 +1,19 @@
 #![no_std]
 #![allow(dead_code)]
 
-pub mod emulator;
+pub(crate) mod emulator;
+pub(crate) mod settings;
+pub(crate) mod utils;
+pub(crate) mod z80;
+
 pub mod error;
 pub mod host;
-pub mod settings;
-pub mod utils;
-pub mod z80;
 pub mod zx;
 
+pub use emulator::{Emulator, Stopwatch};
+pub use settings::RustzxSettings;
+pub use utils::EmulationSpeed;
+
 extern crate alloc;
 
-pub use crate::error::Error;
-pub type Result<T> = core::result::Result<T, Error>;
+pub type Result<T> = core::result::Result<T, error::Error>;
diff --git a/rustzx-core/src/settings.rs b/rustzx-core/src/settings.rs
index 637ace7..c60d79a 100644
--- a/rustzx-core/src/settings.rs
+++ b/rustzx-core/src/settings.rs
@@ -1,17 +1,25 @@
-use crate::{
-    utils::EmulationSpeed,
-    zx::{machine::ZXMachine, sound::ay::ZXAYMode},
-};
+use crate::{utils::EmulationSpeed, zx::machine::ZXMachine};
+
+#[cfg(all(feature = "sound", feature = "ay"))]
+use crate::zx::sound::ay::ZXAYMode;
 
 pub struct RustzxSettings {
     pub machine: ZXMachine,
     pub emulation_speed: EmulationSpeed,
     pub tape_fastload: bool,
     pub enable_kempston: bool,
+    #[cfg(all(feature = "sound", feature = "ay"))]
     pub ay_mode: ZXAYMode,
+    #[cfg(all(feature = "sound", feature = "ay"))]
     pub ay_enabled: bool,
+    #[cfg(feature = "sound")]
     pub beeper_enabled: bool,
+    #[cfg(feature = "sound")]
     pub sound_enabled: bool,
+    #[cfg(feature = "sound")]
     pub sound_volume: u8,
+    #[cfg(feature = "sound")]
+    pub sound_sample_rate: usize,
+    #[cfg(feature = "embedded-roms")]
     pub load_default_rom: bool,
 }
diff --git a/rustzx-core/src/utils/events.rs b/rustzx-core/src/utils/events.rs
deleted file mode 100644
index a8eed0c..0000000
--- a/rustzx-core/src/utils/events.rs
+++ /dev/null
@@ -1,50 +0,0 @@
-use crate::utils::{Clocks, EmulationSpeed};
-use alloc::collections::VecDeque;
-
-/// Type of happened event
-pub enum EventKind {
-    Accelerate(EmulationSpeed),
-    Deaccelerate,
-    FastTapeLoad,
-}
-
-/// event, have information about kind and time of event
-pub struct Event {
-    pub kind: EventKind,
-    pub time: Clocks,
-}
-
-impl Event {
-    /// constructs new event
-    pub fn new(kind: EventKind, time: Clocks) -> Event {
-        Event { kind, time }
-    }
-}
-
-/// Queue-based event container
-#[derive(Default)]
-pub struct EventQueue {
-    deque: VecDeque<Event>,
-}
-
-impl EventQueue {
-    /// addd new event
-    pub fn send_event(&mut self, e: Event) {
-        self.deque.push_back(e);
-    }
-
-    /// pops last event from deque
-    pub fn receive_event(&mut self) -> Option<Event> {
-        self.deque.pop_front()
-    }
-
-    /// returns true if container is empty
-    pub fn is_empty(&self) -> bool {
-        self.deque.is_empty()
-    }
-
-    /// removes all events
-    pub fn clear(&mut self) {
-        self.deque.clear();
-    }
-}
diff --git a/rustzx-core/src/utils/instantflag.rs b/rustzx-core/src/utils/instantflag.rs
deleted file mode 100644
index 7a7faa3..0000000
--- a/rustzx-core/src/utils/instantflag.rs
+++ /dev/null
@@ -1,32 +0,0 @@
-use core::cell::Cell;
-
-/// Instant flag - type, which resets self on immutable read
-pub struct InstantFlag {
-    f: Cell<bool>,
-}
-
-impl InstantFlag {
-    /// constructs self from initial value
-    pub fn new(value: bool) -> InstantFlag {
-        InstantFlag {
-            f: Cell::new(value),
-        }
-    }
-
-    /// immutable read with reset
-    pub fn pick(&self) -> bool {
-        let value = self.f.get();
-        self.f.set(false);
-        value
-    }
-
-    /// read, but not reset
-    pub fn get_direct(&self) -> bool {
-        self.f.get()
-    }
-
-    /// set flag
-    pub fn set(&mut self) {
-        self.f.set(true);
-    }
-}
diff --git a/rustzx-core/src/utils/mod.rs b/rustzx-core/src/utils/mod.rs
index 9245209..1ed947e 100644
--- a/rustzx-core/src/utils/mod.rs
+++ b/rustzx-core/src/utils/mod.rs
@@ -1,11 +1,10 @@
 //! Some emulator-related utils
 
 pub mod clocks;
-pub mod events;
-pub mod instantflag;
 pub mod screen;
 pub mod smallnum;
-pub use self::{clocks::*, events::*, instantflag::*, smallnum::*};
+
+pub use self::{clocks::*, smallnum::*};
 
 #[derive(Copy, Clone)]
 pub enum EmulationSpeed {
diff --git a/rustzx-core/src/utils/screen.rs b/rustzx-core/src/utils/screen.rs
index 400585c..f19e097 100644
--- a/rustzx-core/src/utils/screen.rs
+++ b/rustzx-core/src/utils/screen.rs
@@ -1,5 +1,5 @@
 use super::split_word;
-use crate::zx::constants::*;
+use crate::zx::constants::{ATTR_BASE_REL, ATTR_COLS, ATTR_MAX_REL, CANVAS_HEIGHT};
 
 /// Encode line number to read memory address
 pub fn bitmap_line_addr(line: usize) -> u16 {
diff --git a/rustzx-core/src/utils/smallnum.rs b/rustzx-core/src/utils/smallnum.rs
index 539327b..7c7be30 100644
--- a/rustzx-core/src/utils/smallnum.rs
+++ b/rustzx-core/src/utils/smallnum.rs
@@ -4,6 +4,7 @@
 pub enum U1 {
     N0, N1,
 }
+
 impl U1 {
     /// Constructs self from byte
     /// # Panics
@@ -24,12 +25,14 @@ impl U1 {
         }
     }
 }
+
 #[derive(Clone, Copy, PartialEq, Eq)]
 #[rustfmt::skip]
 /// 2 bit unsigned
 pub enum U2 {
     N0, N1, N2, N3,
 }
+
 impl U2 {
     /// Constructs self from byte
     /// # Panics
@@ -54,12 +57,14 @@ impl U2 {
         }
     }
 }
+
 #[derive(Clone, Copy, PartialEq, Eq)]
 #[rustfmt::skip]
 /// 3 bit unsigned
 pub enum U3 {
     N0, N1, N2, N3, N4, N5, N6, N7,
 }
+
 impl U3 {
     /// Constructs self from byte
     /// # Panics
diff --git a/rustzx-core/src/z80/bus.rs b/rustzx-core/src/z80/bus.rs
index eaaec45..309dd2c 100644
--- a/rustzx-core/src/z80/bus.rs
+++ b/rustzx-core/src/z80/bus.rs
@@ -1,4 +1,4 @@
-use crate::utils::*;
+use crate::utils::{make_word, split_word, Clocks};
 
 /// Z80 processor System bus
 /// Implement it for communication with CPU.
@@ -61,8 +61,6 @@ pub trait Z80Bus {
     fn int_active(&self) -> bool;
     /// Checks nmi signal
     fn nmi_active(&self) -> bool;
-    /// Must return true if devices on bus invoked events
-    fn instant_event(&self) -> bool;
     /// invokes breakpoints check on bus device
     fn pc_callback(&mut self, addr: u16);
 }
diff --git a/rustzx-core/src/z80/cpu.rs b/rustzx-core/src/z80/cpu.rs
index ef8a4e6..2fa1f81 100644
--- a/rustzx-core/src/z80/cpu.rs
+++ b/rustzx-core/src/z80/cpu.rs
@@ -1,8 +1,11 @@
 //! Z80 CPU module
 
 use crate::{
-    utils::*,
-    z80::{opcodes::*, *},
+    utils::{make_word, Clocks},
+    z80::{
+        opcodes::{execute_bits, execute_extended, execute_normal, execute_push_16, Opcode},
+        IntMode, Prefix, RegName16, Regs, Z80Bus,
+    },
 };
 
 /// Z80 Processor struct
@@ -75,7 +78,7 @@ impl Z80 {
     /// Main emulation step function
     /// return `false` if execution can be continued or true if last event must be executed
     /// instantly
-    pub fn emulate(&mut self, bus: &mut dyn Z80Bus) -> bool {
+    pub fn emulate(&mut self, bus: &mut dyn Z80Bus) {
         // check interrupts
         if !self.skip_interrupt {
             // at first check nmi
@@ -194,11 +197,7 @@ impl Z80 {
             let opcode = Opcode::from_byte(byte1);
             execute_normal(self, bus, opcode, Prefix::None);
         };
-        // check events on bus
-        // if some events found, then signal that emulator must process events before
-        // next cpu step
+        // Allow bus implementation to process pc-based events
         bus.pc_callback(self.regs.get_pc());
-        // return true if events happened
-        bus.instant_event()
     }
 }
diff --git a/rustzx-core/src/z80/opcodes/group_bits.rs b/rustzx-core/src/z80/opcodes/group_bits.rs
index 944fe56..3775928 100644
--- a/rustzx-core/src/z80/opcodes/group_bits.rs
+++ b/rustzx-core/src/z80/opcodes/group_bits.rs
@@ -1,7 +1,11 @@
-use super::*;
 use crate::{
-    utils::*,
-    z80::{tables::*, *},
+    utils::{bool_to_u8, word_displacement, Clocks, U2},
+    z80::{
+        opcodes::{execute_rot, BitOperand8, Opcode},
+        tables::F3F5_TABLE,
+        Prefix, RegName16, RegName8, Z80Bus, FLAG_CARRY, FLAG_HALF_CARRY, FLAG_PV, FLAG_SIGN,
+        FLAG_ZERO, Z80,
+    },
 };
 
 /// Instruction group which operatis with bits
diff --git a/rustzx-core/src/z80/opcodes/group_extended.rs b/rustzx-core/src/z80/opcodes/group_extended.rs
index dcac441..4048406 100644
--- a/rustzx-core/src/z80/opcodes/group_extended.rs
+++ b/rustzx-core/src/z80/opcodes/group_extended.rs
@@ -1,7 +1,16 @@
-use super::*;
 use crate::{
-    utils::*,
-    z80::{tables::*, *},
+    utils::{bool_to_u8, Clocks, U1, U2, U3},
+    z80::{
+        opcodes::{
+            execute_cpi_cpd, execute_ini_ind, execute_ldi_ldd, execute_outi_outd, execute_pop_16,
+            BlockDir, Opcode,
+        },
+        tables::{
+            lookup16_r12, lookup8_r12, HALF_CARRY_ADD_TABLE, HALF_CARRY_SUB_TABLE,
+            OVERFLOW_ADD_TABLE, OVERFLOW_SUB_TABLE, SZF3F5_TABLE, SZPF3F5_TABLE,
+        },
+        Flag, IntMode, RegName16, RegName8, Z80Bus, FLAG_CARRY, FLAG_PV, FLAG_SUB, FLAG_ZERO, Z80,
+    },
 };
 
 /// Extended instruction group (ED-prefixed)
diff --git a/rustzx-core/src/z80/opcodes/group_nonprefixed.rs b/rustzx-core/src/z80/opcodes/group_nonprefixed.rs
index 40db9aa..88d7088 100644
--- a/rustzx-core/src/z80/opcodes/group_nonprefixed.rs
+++ b/rustzx-core/src/z80/opcodes/group_nonprefixed.rs
@@ -1,7 +1,14 @@
-use super::*;
 use crate::{
-    utils::*,
-    z80::{tables::*, *},
+    utils::{bool_to_u8, make_word, split_word, word_displacement, Clocks, U1, U2, U3},
+    z80::{
+        opcodes::{execute_alu_8, execute_pop_16, execute_push_16, LoadOperand8, Opcode},
+        tables::{
+            lookup16_r12, lookup8_r12, F3F5_TABLE, HALF_CARRY_ADD_TABLE, HALF_CARRY_SUB_TABLE,
+            SZF3F5_TABLE, SZPF3F5_TABLE,
+        },
+        Condition, Flag, Prefix, RegName16, RegName8, Z80Bus, FLAG_CARRY, FLAG_F3, FLAG_F5,
+        FLAG_HALF_CARRY, FLAG_PV, FLAG_SIGN, FLAG_SUB, FLAG_ZERO, Z80,
+    },
 };
 
 /// normal execution group, can be modified with prefixes DD, FD, providing
diff --git a/rustzx-core/src/z80/opcodes/internal_alu.rs b/rustzx-core/src/z80/opcodes/internal_alu.rs
index b914b24..29f301d 100644
--- a/rustzx-core/src/z80/opcodes/internal_alu.rs
+++ b/rustzx-core/src/z80/opcodes/internal_alu.rs
@@ -1,6 +1,12 @@
 use crate::{
-    utils::*,
-    z80::{tables::*, *},
+    utils::{bool_to_u8, U3},
+    z80::{
+        tables::{
+            lookup8_r12, F3F5_TABLE, HALF_CARRY_ADD_TABLE, HALF_CARRY_SUB_TABLE,
+            OVERFLOW_ADD_TABLE, OVERFLOW_SUB_TABLE, PARITY_TABLE,
+        },
+        Flag, FLAG_CARRY, FLAG_HALF_CARRY, FLAG_SIGN, FLAG_SUB, FLAG_ZERO, Z80,
+    },
 };
 
 /// 8-bit ALU operations
diff --git a/rustzx-core/src/z80/opcodes/internal_block.rs b/rustzx-core/src/z80/opcodes/internal_block.rs
index 250e68f..10e73b5 100644
--- a/rustzx-core/src/z80/opcodes/internal_block.rs
+++ b/rustzx-core/src/z80/opcodes/internal_block.rs
@@ -1,7 +1,11 @@
-use super::*;
 use crate::{
-    utils::*,
-    z80::{tables::*, *},
+    utils::{bool_to_u8, Clocks},
+    z80::{
+        opcodes::BlockDir,
+        tables::{lookup8_r12, HALF_CARRY_SUB_TABLE, PARITY_TABLE, SZF3F5_TABLE},
+        RegName16, RegName8, Z80Bus, FLAG_CARRY, FLAG_F3, FLAG_F5, FLAG_HALF_CARRY, FLAG_PV,
+        FLAG_SIGN, FLAG_SUB, FLAG_ZERO, Z80,
+    },
 };
 
 /// ldi or ldd instruction
diff --git a/rustzx-core/src/z80/opcodes/internal_rot.rs b/rustzx-core/src/z80/opcodes/internal_rot.rs
index dfb8b52..592c2d3 100644
--- a/rustzx-core/src/z80/opcodes/internal_rot.rs
+++ b/rustzx-core/src/z80/opcodes/internal_rot.rs
@@ -1,7 +1,6 @@
-use super::*;
 use crate::{
-    utils::*,
-    z80::{tables::*, *},
+    utils::{bool_to_u8, Clocks, U3},
+    z80::{opcodes::BitOperand8, tables::SZPF3F5_TABLE, Flag, Z80Bus, FLAG_CARRY, Z80},
 };
 
 /// Rotate operations (RLC, RRC, RL, RR, SLA, SRA, SLL, SRL)
diff --git a/rustzx-core/src/z80/opcodes/internal_stack.rs b/rustzx-core/src/z80/opcodes/internal_stack.rs
index dbe63aa..ef924f7 100644
--- a/rustzx-core/src/z80/opcodes/internal_stack.rs
+++ b/rustzx-core/src/z80/opcodes/internal_stack.rs
@@ -1,6 +1,6 @@
 use crate::{
     utils::{make_word, split_word, Clocks},
-    z80::*,
+    z80::{RegName16, Z80Bus, Z80},
 };
 
 /// Pushes 16 bit value to the stack. Clocks count using for each byte write
diff --git a/rustzx-core/src/z80/opcodes/mod.rs b/rustzx-core/src/z80/opcodes/mod.rs
index 63eb7bf..6641900 100644
--- a/rustzx-core/src/z80/opcodes/mod.rs
+++ b/rustzx-core/src/z80/opcodes/mod.rs
@@ -9,7 +9,7 @@ mod internal_rot;
 mod internal_stack;
 mod types;
 
-// re-export all functions
+// re-export all public functions
 pub use self::{
     group_bits::*, group_extended::*, group_nonprefixed::*, internal_alu::*, internal_block::*,
     internal_rot::*, internal_stack::*, types::*,
diff --git a/rustzx-core/src/z80/opcodes/types.rs b/rustzx-core/src/z80/opcodes/types.rs
index 721f762..ccbc1cf 100644
--- a/rustzx-core/src/z80/opcodes/types.rs
+++ b/rustzx-core/src/z80/opcodes/types.rs
@@ -1,4 +1,7 @@
-use crate::{utils::*, z80::*};
+use crate::{
+    utils::{U1, U2, U3},
+    z80::RegName8,
+};
 
 /// Operand for 8-bit LD instructions
 pub enum LoadOperand8 {
diff --git a/rustzx-core/src/z80/registers.rs b/rustzx-core/src/z80/registers.rs
index 89f433d..efcdb6c 100644
--- a/rustzx-core/src/z80/registers.rs
+++ b/rustzx-core/src/z80/registers.rs
@@ -1,5 +1,8 @@
 //! Module which contains Z80 registers implementation
-use crate::{utils::*, z80::Prefix};
+use crate::{
+    utils::{make_word, split_word, word_displacement, U2, U3},
+    z80::Prefix,
+};
 use core::fmt;
 
 // Flag register bits
diff --git a/rustzx-core/src/z80/tables/mod.rs b/rustzx-core/src/z80/tables/mod.rs
index 7cc515d..626f207 100644
--- a/rustzx-core/src/z80/tables/mod.rs
+++ b/rustzx-core/src/z80/tables/mod.rs
@@ -1,6 +1,5 @@
 //! Contains internal emulator tables
-
-use crate::z80::registers::*;
+use crate::z80::{FLAG_F3, FLAG_F5, FLAG_HALF_CARRY, FLAG_PV, FLAG_SIGN, FLAG_ZERO};
 use lazy_static::lazy_static;
 
 // TABLES FORM FUSE ZX SPECTRUM EMULATOR:
diff --git a/rustzx-core/src/zx/constants.rs b/rustzx-core/src/zx/constants.rs
index 3418e2b..1c3c510 100644
--- a/rustzx-core/src/zx/constants.rs
+++ b/rustzx-core/src/zx/constants.rs
@@ -1,31 +1,31 @@
 //! Module contains constants
-/// addresses
-pub const BITMAP_BASE_ADDR: u16 = 0x4000;
-pub const ATTR_BASE_ADDR: u16 = 0x5800;
-pub const ATTR_MAX_ADDR: u16 = 0x5AFF;
-/// relative addresses
-pub const BITMAP_MAX_REL: u16 = 0x17FF;
-pub const ATTR_BASE_REL: u16 = 0x1800;
-pub const ATTR_MAX_REL: u16 = 0x1AFF;
+
 /// main spectrum screen (canvas) constants
 pub const CANVAS_WIDTH: usize = 256;
 pub const CANVAS_HEIGHT: usize = 192;
 pub const CANVAS_X: usize = 32;
 pub const CANVAS_Y: usize = 24;
-/// on all spectrums theese values are fixed
-pub const CLOCKS_PER_COL: usize = 4;
-pub const PIXELS_PER_CLOCK: usize = 2;
-/// size of screen in rows, cols
-pub const ATTR_COLS: usize = CANVAS_WIDTH / 8;
-pub const ATTR_ROWS: usize = CANVAS_HEIGHT / 8;
-pub const BORDER_COLS: usize = 4;
-pub const BORDER_ROWS: usize = 3;
 /// canvas (emulated screen) constants
 pub const SCREEN_WIDTH: usize = CANVAS_WIDTH + BORDER_COLS * 8 * 2;
 pub const SCREEN_HEIGHT: usize = CANVAS_HEIGHT + BORDER_ROWS * 8 * 2;
-pub const PIXEL_COUNT: usize = SCREEN_HEIGHT * SCREEN_WIDTH;
-pub const BYTES_PER_PIXEL: usize = 4;
 /// Frames per second
 pub const FPS: usize = 50;
+
+/// addresses
+pub(crate) const BITMAP_BASE_ADDR: u16 = 0x4000;
+pub(crate) const ATTR_BASE_ADDR: u16 = 0x5800;
+pub(crate) const ATTR_MAX_ADDR: u16 = 0x5AFF;
+/// relative addresses
+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
+pub(crate) const CLOCKS_PER_COL: usize = 4;
+pub(crate) const PIXELS_PER_CLOCK: usize = 2;
+/// size of screen in rows, cols
+pub(crate) const ATTR_COLS: usize = CANVAS_WIDTH / 8;
+pub(crate) const ATTR_ROWS: usize = CANVAS_HEIGHT / 8;
+pub(crate) const BORDER_COLS: usize = 4;
+pub(crate) const BORDER_ROWS: usize = 3;
 /// Tape loading trap at LD-BREAK routine in ROM
-pub const ADDR_LD_BREAK: u16 = 0x056B;
+pub(crate) const ADDR_LD_BREAK: u16 = 0x056B;
diff --git a/rustzx-core/src/zx/controller.rs b/rustzx-core/src/zx/controller.rs
index 218a6fe..793fb83 100644
--- a/rustzx-core/src/zx/controller.rs
+++ b/rustzx-core/src/zx/controller.rs
@@ -1,63 +1,64 @@
 //! Contains ZX Spectrum System contrller (like ula or so) of emulator
 use crate::{
-    host::Host,
+    error::Error,
+    host::{Host, HostContext},
     settings::RustzxSettings,
-    utils::{events::*, screen::*, split_word, Clocks, InstantFlag},
+    utils::{screen::bitmap_line_addr, split_word, Clocks},
     z80::Z80Bus,
     zx::{
-        constants::*,
-        joy::kempston::*,
+        constants::{ADDR_LD_BREAK, CANVAS_HEIGHT, CLOCKS_PER_COL},
+        events::EmulationEvents,
+        joy::kempston::KempstonJoy,
+        keys::ZXKey,
         machine::ZXMachine,
-        memory::{Page, PAGE_SIZE},
-        roms::*,
-        screen::{
-            border::ZXBorder,
-            canvas::ZXCanvas,
-            colors::{ZXColor, ZXPalette},
-        },
-        sound::mixer::ZXMixer,
-        tape::{Tap, TapeImpl, ZXTape},
-        RamType, RomType, ZXKey, ZXMemory,
+        memory::{Page, RamType, RomType, ZXMemory, PAGE_SIZE},
+        tape::{TapeImpl, ZXTape},
+        video::{colors::ZXColor, screen::ZXScreen},
     },
 };
 
-// TODO(#57): Feature gates for resource-heavy features
+#[cfg(feature = "embedded-roms")]
+use crate::zx::roms;
+#[cfg(feature = "sound")]
+use crate::zx::sound::mixer::ZXMixer;
+#[cfg(feature = "precise-border")]
+use crate::zx::video::border::ZXBorder;
 
 /// ZX System controller
-pub struct ZXController<H: Host> {
+pub(crate) struct ZXController<H: Host> {
     // parts of ZX Spectum.
     pub machine: ZXMachine,
     pub memory: ZXMemory,
-    pub canvas: ZXCanvas,
-    pub tape: ZXTape,
-    pub border: ZXBorder,
+    pub screen: ZXScreen<H::FrameBuffer>,
+    pub tape: ZXTape<H::TapeAsset>,
+    #[cfg(feature = "precise-border")]
+    pub border: ZXBorder<H::FrameBuffer>,
     pub kempston: Option<KempstonJoy>,
-    // pub beeper: ZXBeeper,
+    #[cfg(feature = "sound")]
     pub mixer: ZXMixer,
     pub keyboard: [u8; 8],
     // current border color
-    border_color: u8,
+    pub border_color: ZXColor,
     // clocls count from frame start
     frame_clocks: Clocks,
     // frames count, which passed during emulation invokation
     passed_frames: usize,
-    // main event queue
-    events: EventQueue,
-    // flag, which signals emulator to break emulation and process last event immediately
-    instant_event: InstantFlag,
+    events: EmulationEvents,
     // audio in
     mic: bool,
     // audio out
     ear: bool,
     paging_enabled: bool,
     screen_bank: u8,
-    // `H` will be used when components e.g. Tap type will depend on host impl
-    _phantom: core::marker::PhantomData<H>,
+    // 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
+    last_emulation_error: Option<Error>,
 }
 
 impl<H: Host> ZXController<H> {
     /// Returns new ZXController from settings
-    pub fn new(settings: &RustzxSettings) -> Self {
+    pub fn new(settings: &RustzxSettings, host_context: H::Context) -> Self {
         let (memory, paging, screen_bank);
         match settings.machine {
             ZXMachine::Sinclair48K => {
@@ -76,36 +77,60 @@ impl<H: Host> ZXController<H> {
         } else {
             None
         };
-        let mut out = ZXController {
+
+        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());
+
+        #[cfg(feature = "sound")]
+        let mixer = Self::create_mixer(settings);
+
+        let out = ZXController {
             machine: settings.machine,
             memory,
-            canvas: ZXCanvas::new(settings.machine),
-            border: ZXBorder::new(settings.machine, ZXPalette::default()),
+            screen,
+            #[cfg(feature = "precise-border")]
+            border,
             kempston,
-            mixer: ZXMixer::new(settings.beeper_enabled, settings.ay_enabled),
+            #[cfg(feature = "sound")]
+            mixer,
             keyboard: [0xFF; 8],
-            border_color: 0x00,
+            border_color: ZXColor::Black,
             frame_clocks: Clocks(0),
             passed_frames: 0,
-            tape: Tap::default().into(),
-            events: EventQueue::default(),
-            instant_event: InstantFlag::new(false),
+            tape: Default::default(),
+            events: Default::default(),
             mic: false,
             ear: false,
             paging_enabled: paging,
             screen_bank,
-            _phantom: core::marker::PhantomData::default(),
+            last_emulation_error: None,
         };
-        out.mixer.ay.mode(settings.ay_mode);
-        out.mixer.volume(settings.sound_volume as f64 / 200.0);
 
+        #[cfg(feature = "embedded-roms")]
         if settings.load_default_rom {
+            let mut out = out;
             out.load_default_rom();
+            return out;
         }
 
         out
     }
 
+    #[cfg(feature = "sound")]
+    fn create_mixer(settings: &RustzxSettings) -> ZXMixer {
+        let mut mixer = ZXMixer::new(
+            settings.beeper_enabled,
+            #[cfg(feature = "ay")]
+            settings.ay_enabled,
+            settings.sound_sample_rate,
+        );
+        #[cfg(feature = "ay")]
+        mixer.ay.mode(settings.ay_mode);
+        mixer.volume(settings.sound_volume as f64 / 200.0);
+        mixer
+    }
+
     /// returns current frame emulation pos in percents
     fn frame_pos(&self) -> f64 {
         let val = self.frame_clocks.count() as f64 / self.machine.specs().clocks_frame as f64;
@@ -117,40 +142,28 @@ impl<H: Host> ZXController<H> {
     }
 
     /// loads builted-in ROM
+    #[cfg(feature = "embedded-roms")]
     fn load_default_rom(&mut self) {
         match self.machine {
             ZXMachine::Sinclair48K => {
                 let page = self.memory.rom_page_data_mut(0);
-                page.copy_from_slice(ROM_48K);
+                page.copy_from_slice(roms::ROM_48K);
             }
             ZXMachine::Sinclair128K => {
                 let page = self.memory.rom_page_data_mut(0);
-                page.copy_from_slice(ROM_128K_0);
+                page.copy_from_slice(roms::ROM_128K_0);
                 let page = self.memory.rom_page_data_mut(1);
-                page.copy_from_slice(ROM_128K_1);
+                page.copy_from_slice(roms::ROM_128K_1);
             }
         }
     }
 
     /// Changes key state in controller
     pub fn send_key(&mut self, key: ZXKey, pressed: bool) {
-        // TODO(#49): Move row detection to ZXKey type
-        let rownum = match key.half_port {
-            0xFE => Some(0),
-            0xFD => Some(1),
-            0xFB => Some(2),
-            0xF7 => Some(3),
-            0xEF => Some(4),
-            0xDF => Some(5),
-            0xBF => Some(6),
-            0x7F => Some(7),
-            _ => None,
-        };
-        if let Some(rownum) = rownum {
-            self.keyboard[rownum] &= !key.mask;
-            if !pressed {
-                self.keyboard[rownum] |= key.mask;
-            }
+        let row_id = key.row_id();
+        self.keyboard[row_id] &= !key.mask();
+        if !pressed {
+            self.keyboard[row_id] |= key.mask();
         }
     }
 
@@ -224,24 +237,21 @@ impl<H: Host> ZXController<H> {
     /// Starts a new frame
     fn new_frame(&mut self) {
         self.frame_clocks -= self.machine.specs().clocks_frame;
-        self.canvas.new_frame();
+        self.screen.new_frame();
+        #[cfg(feature = "precise-border")]
         self.border.new_frame();
+        #[cfg(feature = "sound")]
         self.mixer.new_frame();
     }
 
-    /// force clears all events
+    /// Clears all detected
     pub fn clear_events(&mut self) {
         self.events.clear();
     }
 
-    /// check events count
-    pub fn no_events(&self) -> bool {
-        self.events.is_empty()
-    }
-
-    /// Returns last event
-    pub fn pop_event(&mut self) -> Option<Event> {
-        self.events.receive_event()
+    /// Returns last events
+    pub fn events(&self) -> EmulationEvents {
+        self.events
     }
 
     /// Returns true if all frame clocks has been passed
@@ -267,7 +277,7 @@ impl<H: Host> ZXController<H> {
         // third block is not pageable
         // second block is screen buffer, not pageable. but we need to change active buffer
         let new_screen_bank = if val & 0x08 == 0 { 5 } else { 7 };
-        self.canvas.switch_bank(new_screen_bank as usize);
+        self.screen.switch_bank(new_screen_bank as usize);
         self.screen_bank = new_screen_bank;
         // remap ROM
         self.memory.remap(0, Page::Rom((val >> 4) & 0x01));
@@ -276,6 +286,46 @@ impl<H: Host> ZXController<H> {
             self.paging_enabled = false;
         }
     }
+
+    #[cfg(all(feature = "sound", feature = "ay"))]
+    fn read_ay_port(&mut self) -> u8 {
+        self.mixer.ay.read()
+    }
+
+    #[cfg(not(all(feature = "sound", feature = "ay")))]
+    fn read_ay_port(&mut self) -> u8 {
+        self.floating_bus_value()
+    }
+
+    #[cfg(all(feature = "sound", feature = "ay"))]
+    fn write_ay_port(&mut self, value: u8) {
+        self.mixer.ay.write(value);
+    }
+
+    #[cfg(not(all(feature = "sound", feature = "ay")))]
+    fn write_ay_port(&mut self, _: u8) {}
+
+    #[cfg(all(feature = "sound", feature = "ay"))]
+    fn select_ay_reg(&mut self, value: u8) {
+        self.mixer.ay.select_reg(value)
+    }
+
+    #[cfg(not(all(feature = "sound", feature = "ay")))]
+    fn select_ay_reg(&mut self, _: u8) {}
+
+    pub(crate) fn set_border_color(
+        &mut self,
+        #[allow(unused_variables)] clocks: Clocks,
+        color: ZXColor,
+    ) {
+        self.border_color = color;
+        #[cfg(feature = "precise-border")]
+        self.border.set_border(clocks, color);
+    }
+
+    pub(crate) fn take_last_emulation_error(&mut self) -> Option<Error> {
+        self.last_emulation_error.take()
+    }
 }
 
 impl<H: Host> Z80Bus for ZXController<H> {
@@ -293,9 +343,7 @@ impl<H: Host> Z80Bus for ZXController<H> {
             if addr == ADDR_LD_BREAK {
                 // Add event (Fast tape loading request) it must be executed
                 // by emulator immediately
-                self.events
-                    .send_event(Event::new(EventKind::FastTapeLoad, self.frame_clocks));
-                self.instant_event.set();
+                self.events |= EmulationEvents::TAPE_FAST_LOAD_TRIGGER_DETECTED;
             }
         }
     }
@@ -310,7 +358,7 @@ impl<H: Host> Z80Bus for ZXController<H> {
         self.memory.write(addr, data);
         // if ram then compare bank to screen bank
         if let Page::Ram(bank) = self.memory.get_page(addr) {
-            self.canvas
+            self.screen
                 .update(addr % PAGE_SIZE as u16, bank as usize, data);
         }
     }
@@ -318,13 +366,18 @@ impl<H: Host> Z80Bus for ZXController<H> {
     /// Cahnges internal state on clocks count change (emualtion processing)
     fn wait_internal(&mut self, clk: Clocks) {
         self.frame_clocks += clk;
-        self.tape.process_clocks(clk);
+        if let Err(e) = self.tape.process_clocks(clk) {
+            self.last_emulation_error = Some(e);
+        }
         let mic = self.tape.current_bit();
         self.mic = mic;
-        let pos = self.frame_pos();
-        self.mixer.beeper.change_bit(self.mic | self.ear);
-        self.mixer.process(pos);
-        self.canvas.process_clocks(self.frame_clocks);
+        #[cfg(feature = "sound")]
+        {
+            let pos = self.frame_pos();
+            self.mixer.beeper.change_bit(self.mic | self.ear);
+            self.mixer.process(pos);
+        }
+        self.screen.process_clocks(self.frame_clocks);
         if self.frame_clocks.count() >= self.machine.specs().clocks_frame {
             self.new_frame();
             self.passed_frames += 1;
@@ -373,8 +426,7 @@ impl<H: Host> Z80Bus for ZXController<H> {
             // 5 and 7 unused
             tmp
         } else if port & 0xC002 == 0xC000 {
-            // AY regs
-            self.mixer.ay.read()
+            self.read_ay_port()
         } else if self.kempston.is_some() && (port & 0x0020 == 0) {
             if let Some(ref joy) = self.kempston {
                 joy.read()
@@ -395,15 +447,14 @@ impl<H: Host> Z80Bus for ZXController<H> {
         self.io_contention_first(port);
         // find active port
         if port & 0xC002 == 0xC000 {
-            self.mixer.ay.select_reg(data);
+            self.select_ay_reg(data);
         } else if port & 0xC002 == 0x8000 {
-            self.mixer.ay.write(data);
+            self.write_ay_port(data);
         } else if port & 0x0001 == 0 {
-            self.border_color = data & 0x07;
-            self.border
-                .set_border(self.frame_clocks, ZXColor::from_bits(data & 0x07));
+            self.set_border_color(self.frame_clocks, ZXColor::from_bits(data & 0x07));
             self.mic = data & 0x08 != 0;
             self.ear = data & 0x10 != 0;
+            #[cfg(feature = "sound")]
             self.mixer.beeper.change_bit(self.mic | self.ear);
         } else if (port & 0x8002 == 0) && (self.machine == ZXMachine::Sinclair128K) {
             self.write_7ffd(data);
@@ -435,9 +486,4 @@ impl<H: Host> Z80Bus for ZXController<H> {
 
     /// CPU calls when was being halted
     fn halt(&mut self, _: bool) {}
-
-    /// checks instant events
-    fn instant_event(&self) -> bool {
-        self.instant_event.pick()
-    }
 }
diff --git a/rustzx-core/src/zx/events.rs b/rustzx-core/src/zx/events.rs
new file mode 100644
index 0000000..7a5951b
--- /dev/null
+++ b/rustzx-core/src/zx/events.rs
@@ -0,0 +1,14 @@
+use bitflags::bitflags;
+
+bitflags! {
+    #[derive(Default)]
+    pub struct EmulationEvents: u8 {
+        const TAPE_FAST_LOAD_TRIGGER_DETECTED = 0b00000001;
+    }
+}
+
+impl EmulationEvents {
+    pub fn clear(&mut self) {
+        self.bits = 0;
+    }
+}
diff --git a/rustzx-core/src/zx/joy/kempston.rs b/rustzx-core/src/zx/joy/kempston.rs
index 27a025a..585bb50 100644
--- a/rustzx-core/src/zx/joy/kempston.rs
+++ b/rustzx-core/src/zx/joy/kempston.rs
@@ -9,7 +9,7 @@ pub enum KempstonKey {
 
 /// Kempston Joystick
 #[derive(Default)]
-pub struct KempstonJoy {
+pub(crate) struct KempstonJoy {
     state: u8,
 }
 
diff --git a/rustzx-core/src/zx/keys.rs b/rustzx-core/src/zx/keys.rs
index 4e3950b..1919c6d 100644
--- a/rustzx-core/src/zx/keys.rs
+++ b/rustzx-core/src/zx/keys.rs
@@ -1,55 +1,64 @@
 //! Module with hardware key port\masks
 
 /// Struct, which contains mast and port of key
-pub struct ZXKey {
-    pub half_port: u8,
-    pub mask: u8,
+#[rustfmt::skip]
+#[derive(Clone, Copy)]
+pub enum ZXKey {
+    // Port 0xFEFE
+    Shift, Z, X, C, V,
+    // Port 0xFDFE
+    A, S, D, F, G,
+    // Port 0xFBFE
+    Q, W, E, R, T,
+    // Port 0xF7FE
+    N1, N2, N3, N4, N5,
+    // Port 0xEFFE
+    N0, N9, N8, N7, N6,
+    // Port 0xDFFE
+    P, O, I, U, Y,
+    // Port 0xBFFE
+    Enter, L, K, J, H,
+    // Port 0x7FFE
+    Space, SymShift, M, N, B,
+}
+
+impl ZXKey {
+    pub(crate) fn row_id(self) -> usize {
+        match self.half_port() {
+            0xFE => 0,
+            0xFD => 1,
+            0xFB => 2,
+            0xF7 => 3,
+            0xEF => 4,
+            0xDF => 5,
+            0xBF => 6,
+            0x7F => 7,
+            _ => unreachable!(),
+        }
+    }
+
+    pub(crate) fn mask(&self) -> u8 {
+        use ZXKey::*;
+        match self {
+            Shift | A | Q | N1 | N0 | P | Enter | Space => 0x01,
+            Z | S | W | N2 | N9 | O | L | SymShift => 0x02,
+            X | D | E | N3 | N8 | I | K | M => 0x04,
+            C | F | R | N4 | N7 | U | J | N => 0x08,
+            V | G | T | N5 | N6 | Y | H | B => 0x10,
+        }
+    }
+
+    fn half_port(self) -> u8 {
+        use ZXKey::*;
+        match self {
+            Shift | Z | X | C | V => 0xFE,
+            A | S | D | F | G => 0xFD,
+            Q | W | E | R | T => 0xFB,
+            N1 | N2 | N3 | N4 | N5 => 0xF7,
+            N0 | N9 | N8 | N7 | N6 => 0xEF,
+            P | O | I | U | Y => 0xDF,
+            Enter | L | K | J | H => 0xBF,
+            Space | SymShift | M | N | B => 0x7F,
+        }
+    }
 }
-// 0xFEFE
-pub const ZX_KEY_SHIFT: ZXKey = ZXKey { half_port: 0xFE, mask: 0x01 };
-pub const ZX_KEY_Z: ZXKey = ZXKey { half_port: 0xFE, mask: 0x02 };
-pub const ZX_KEY_X: ZXKey = ZXKey { half_port: 0xFE, mask: 0x04 };
-pub const ZX_KEY_C: ZXKey = ZXKey { half_port: 0xFE, mask: 0x08 };
-pub const ZX_KEY_V: ZXKey = ZXKey { half_port: 0xFE, mask: 0x10 };
-// FDFE
-pub const ZX_KEY_A: ZXKey = ZXKey { half_port: 0xFD, mask: 0x01 };
-pub const ZX_KEY_S: ZXKey = ZXKey { half_port: 0xFD, mask: 0x02 };
-pub const ZX_KEY_D: ZXKey = ZXKey { half_port: 0xFD, mask: 0x04 };
-pub const ZX_KEY_F: ZXKey = ZXKey { half_port: 0xFD, mask: 0x08 };
-pub const ZX_KEY_G: ZXKey = ZXKey { half_port: 0xFD, mask: 0x10 };
-// FBFE
-pub const ZX_KEY_Q: ZXKey = ZXKey { half_port: 0xFB, mask: 0x01 };
-pub const ZX_KEY_W: ZXKey = ZXKey { half_port: 0xFB, mask: 0x02 };
-pub const ZX_KEY_E: ZXKey = ZXKey { half_port: 0xFB, mask: 0x04 };
-pub const ZX_KEY_R: ZXKey = ZXKey { half_port: 0xFB, mask: 0x08 };
-pub const ZX_KEY_T: ZXKey = ZXKey { half_port: 0xFB, mask: 0x10 };
-// F7FE
-pub const ZX_KEY_1: ZXKey = ZXKey { half_port: 0xF7, mask: 0x01 };
-pub const ZX_KEY_2: ZXKey = ZXKey { half_port: 0xF7, mask: 0x02 };
-pub const ZX_KEY_3: ZXKey = ZXKey { half_port: 0xF7, mask: 0x04 };
-pub const ZX_KEY_4: ZXKey = ZXKey { half_port: 0xF7, mask: 0x08 };
-pub const ZX_KEY_5: ZXKey = ZXKey { half_port: 0xF7, mask: 0x10 };
-// EFFE
-pub const ZX_KEY_0: ZXKey = ZXKey { half_port: 0xEF, mask: 0x01 };
-pub const ZX_KEY_9: ZXKey = ZXKey { half_port: 0xEF, mask: 0x02 };
-pub const ZX_KEY_8: ZXKey = ZXKey { half_port: 0xEF, mask: 0x04 };
-pub const ZX_KEY_7: ZXKey = ZXKey { half_port: 0xEF, mask: 0x08 };
-pub const ZX_KEY_6: ZXKey = ZXKey { half_port: 0xEF, mask: 0x10 };
-//DFFE
-pub const ZX_KEY_P: ZXKey = ZXKey { half_port: 0xDF, mask: 0x01 };
-pub const ZX_KEY_O: ZXKey = ZXKey { half_port: 0xDF, mask: 0x02 };
-pub const ZX_KEY_I: ZXKey = ZXKey { half_port: 0xDF, mask: 0x04 };
-pub const ZX_KEY_U: ZXKey = ZXKey { half_port: 0xDF, mask: 0x08 };
-pub const ZX_KEY_Y: ZXKey = ZXKey { half_port: 0xDF, mask: 0x10 };
-// BFFE
-pub const ZX_KEY_ENTER: ZXKey = ZXKey { half_port: 0xBF, mask: 0x01 };
-pub const ZX_KEY_L: ZXKey = ZXKey { half_port: 0xBF, mask: 0x02 };
-pub const ZX_KEY_K: ZXKey = ZXKey { half_port: 0xBF, mask: 0x04 };
-pub const ZX_KEY_J: ZXKey = ZXKey { half_port: 0xBF, mask: 0x08 };
-pub const ZX_KEY_H: ZXKey = ZXKey { half_port: 0xBF, mask: 0x10 };
-// 7FFE
-pub const ZX_KEY_SPACE: ZXKey = ZXKey { half_port: 0x7F, mask: 0x01 };
-pub const ZX_KEY_SYM_SHIFT: ZXKey = ZXKey { half_port: 0x7F, mask: 0x02 };
-pub const ZX_KEY_M: ZXKey = ZXKey { half_port: 0x7F, mask: 0x04 };
-pub const ZX_KEY_N: ZXKey = ZXKey { half_port: 0x7F, mask: 0x08 };
-pub const ZX_KEY_B: ZXKey = ZXKey { half_port: 0x7F, mask: 0x10 };
diff --git a/rustzx-core/src/zx/machine/mod.rs b/rustzx-core/src/zx/machine/mod.rs
index 16c666c..2043525 100644
--- a/rustzx-core/src/zx/machine/mod.rs
+++ b/rustzx-core/src/zx/machine/mod.rs
@@ -2,14 +2,16 @@
 
 // Allow outer modules to use ZXSpecs struct, but not construct
 mod specs;
-pub use self::specs::ZXSpecs;
-use self::specs::ZXSpecsBuilder;
+
 use crate::utils::Clocks;
 use lazy_static::lazy_static;
+use specs::ZXSpecsBuilder;
+
+pub(crate) use specs::ZXSpecs;
 
 lazy_static! {
     /// ZX Spectrum 48K Specs
-    pub static ref SPECS_48K: ZXSpecs = {
+    pub(crate) static ref SPECS_48K: ZXSpecs = {
         ZXSpecsBuilder::new()
             .freq_cpu(3_500_000)
             .clocks_first_pixel(14336)
diff --git a/rustzx-core/src/zx/machine/specs.rs b/rustzx-core/src/zx/machine/specs.rs
index 350bb77..6ff7561 100644
--- a/rustzx-core/src/zx/machine/specs.rs
+++ b/rustzx-core/src/zx/machine/specs.rs
@@ -1,7 +1,3 @@
-use crate::zx::constants::BORDER_COLS;
-use alloc::{vec, vec::Vec};
-
-/// Immutable type (Builder is not public in outer module)
 pub struct ZXSpecs {
     // frequencies
     pub freq_cpu: usize,
@@ -13,7 +9,6 @@ pub struct ZXSpecs {
     pub clocks_right_border: usize,
     pub clocks_retrace: usize,
     pub clocks_line: usize,
-    pub clocks_line_base: Vec<usize>,
     // some ula clocks
     pub clocks_ula_read_shift: usize,
     pub clocks_ula_read_origin: usize,
@@ -54,7 +49,6 @@ impl ZXSpecsBuilder {
                 clocks_right_border: 0,
                 clocks_retrace: 0,
                 clocks_line: 0,
-                clocks_line_base: vec![],
                 // some ula clocks
                 clocks_ula_read_shift: 0,
                 clocks_ula_read_origin: 0,
@@ -83,19 +77,6 @@ impl ZXSpecsBuilder {
     pub fn build(mut self) -> ZXSpecs {
         self.specs.clocks_frame =
             (self.specs.lines_all + self.specs.lines_vsync) * self.specs.clocks_line;
-        // 4*4 is 4 border columns * 4 clocks per column
-        self.specs.clocks_line_base.push(
-            self.specs.clocks_first_pixel
-                - self.specs.lines_top_border * self.specs.clocks_line
-                - BORDER_COLS * 4,
-        );
-        // + 1 because TStates in calculations may be > frame length (CHECK)
-        let lines_count = self.specs.lines_all + 1;
-        for _ in 1..lines_count {
-            let last = *self.specs.clocks_line_base.last().unwrap();
-            let line_clocks = self.specs.clocks_line;
-            self.specs.clocks_line_base.push(last + line_clocks);
-        }
         self.specs.clocks_ula_read_origin =
             self.specs.clocks_first_pixel + self.specs.clocks_ula_read_shift;
         self.specs.clocks_ula_contention_origin =
diff --git a/rustzx-core/src/zx/mod.rs b/rustzx-core/src/zx/mod.rs
index b3de060..f591012 100644
--- a/rustzx-core/src/zx/mod.rs
+++ b/rustzx-core/src/zx/mod.rs
@@ -1,24 +1,16 @@
 //! Module with ZX Spectrum related things
 //! One of core platform-independent modules
+pub(crate) mod controller;
+pub(crate) mod events;
+pub(crate) mod memory;
+#[cfg(feature = "embedded-roms")]
+pub(crate) mod roms;
+pub(crate) mod tape;
+
 pub mod constants;
-pub mod controller;
 pub mod joy;
-#[rustfmt::skip]
 pub mod keys;
 pub mod machine;
-pub mod memory;
-pub mod roms;
-pub mod screen;
+#[cfg(feature = "sound")]
 pub mod sound;
-pub mod tape;
-
-// re-export most of things
-// TODO(#48): Reorganize imports
-pub use self::{
-    controller::ZXController,
-    keys::*,
-    machine::{ZXMachine, ZXSpecs},
-    memory::{RamType, RomType, ZXMemory},
-    screen::*,
-    tape::ZXTape,
-};
+pub mod video;
diff --git a/rustzx-core/src/zx/screen/colors.rs b/rustzx-core/src/zx/screen/colors.rs
deleted file mode 100644
index b40deca..0000000
--- a/rustzx-core/src/zx/screen/colors.rs
+++ /dev/null
@@ -1,163 +0,0 @@
-//! Contains Color generation related types
-use crate::zx::constants::BYTES_PER_PIXEL;
-
-/// struct represents single pixel color as array of bytes
-pub type ColorArray = [u8; BYTES_PER_PIXEL];
-
-/// splits usize value to 4 bytes
-#[rustfmt::skip]
-fn split_in_bytes(val: usize) -> ColorArray {
-    [
-        ((val >> 24) & 0xFF) as u8,
-        ((val >> 16) & 0xFF) as u8,
-        ((val >>  8) & 0xFF) as u8,
-        (val & 0xFF) as u8
-    ]
-}
-
-/// Represents color brightness
-#[derive(Clone, Copy)]
-pub enum ZXBrightness {
-    Normal,
-    Bright,
-}
-
-/// ZX Spectrum color enum
-/// Constructs self from 3-bit value
-#[derive(Clone, Copy)]
-pub enum ZXColor {
-    Black,
-    Blue,
-    Red,
-    Purple,
-    Green,
-    Cyan,
-    Yellow,
-    White,
-}
-
-impl ZXColor {
-    /// Returns ZXColor from 3 bits
-    /// # Panics
-    /// Panics when input color is bigger than 7
-    pub fn from_bits(bits: u8) -> ZXColor {
-        assert!(bits <= 7);
-        match bits {
-            0 => ZXColor::Black,
-            1 => ZXColor::Blue,
-            2 => ZXColor::Red,
-            3 => ZXColor::Purple,
-            4 => ZXColor::Green,
-            5 => ZXColor::Cyan,
-            6 => ZXColor::Yellow,
-            7 => ZXColor::White,
-            _ => unreachable!(),
-        }
-    }
-}
-
-/// ZX Spectrum attribute structure
-/// It contains information about ink, paper color,
-/// flash attribute and brightness
-#[derive(Clone, Copy)]
-pub struct ZXAttribute {
-    pub ink: ZXColor,
-    pub paper: ZXColor,
-    pub brightness: ZXBrightness,
-    pub flash: bool,
-}
-
-impl ZXAttribute {
-    /// Constructs self from byte
-    pub fn from_byte(data: u8) -> ZXAttribute {
-        ZXAttribute {
-            ink: ZXColor::from_bits(data & 0x07),
-            paper: ZXColor::from_bits((data >> 3) & 0x07),
-            flash: (data & 0x80) != 0,
-            brightness: if (data & 0x40) != 0 {
-                ZXBrightness::Bright
-            } else {
-                ZXBrightness::Normal
-            },
-        }
-    }
-
-    /// Returns active color of pixel in current attribute
-    pub fn active_color(&self, state: bool, enable_flash: bool) -> ZXColor {
-        if state ^ (self.flash && enable_flash) {
-            self.ink
-        } else {
-            self.paper
-        }
-    }
-}
-
-/// represents set of colors
-struct ColorSet {
-    black: ColorArray,
-    blue: ColorArray,
-    red: ColorArray,
-    purple: ColorArray,
-    green: ColorArray,
-    cyan: ColorArray,
-    yellow: ColorArray,
-    white: ColorArray,
-}
-/// Structure, that holds palette information.
-/// It have method to transform ZX Spectrum screen data
-/// to 4-byte rgba bixel
-pub struct ZXPalette {
-    transparent: ColorArray,
-    // 2 color sets
-    bright: ColorSet,
-    normal: ColorSet,
-}
-
-impl ZXPalette {
-    /// Returns default palette
-    #[rustfmt::skip]
-    pub fn default() -> ZXPalette {
-        ZXPalette {
-            transparent: split_in_bytes(0x00000000),
-            normal: ColorSet {
-                black:   split_in_bytes(0x000000FF),
-                blue:    split_in_bytes(0x0000CDFF),
-                red:     split_in_bytes(0xCD0000FF),
-                purple:  split_in_bytes(0xCD00CDFF),
-                green:   split_in_bytes(0x00CD00FF),
-                cyan:    split_in_bytes(0x00CDCDFF),
-                yellow:  split_in_bytes(0xCDCD00FF),
-                white:   split_in_bytes(0xCDCDCDFF),
-            },
-            bright: ColorSet {
-                black:   split_in_bytes(0x000000FF),
-                blue:    split_in_bytes(0x0000FFFF),
-                red:     split_in_bytes(0xFF0000FF),
-                purple:  split_in_bytes(0xFF00FFFF),
-                green:   split_in_bytes(0x00FF00FF),
-                cyan:    split_in_bytes(0x00FFFFFF),
-                yellow:  split_in_bytes(0xFFFF00FF),
-                white:   split_in_bytes(0xFFFFFFFF),
-            }
-        }
-    }
-
-    /// Returns rgba pixel from screen data
-    pub fn get_rgba(&self, color: ZXColor, brightness: ZXBrightness) -> &ColorArray {
-        // select palette
-        let set = match brightness {
-            ZXBrightness::Normal => &self.normal,
-            ZXBrightness::Bright => &self.bright,
-        };
-        match color {
-            ZXColor::Black => &set.black,
-            ZXColor::Blue => &set.blue,
-            ZXColor::Red => &set.red,
-            ZXColor::Purple => &set.purple,
-            ZXColor::Green => &set.green,
-            ZXColor::Cyan => &set.cyan,
-            ZXColor::Yellow => &set.yellow,
-            ZXColor::White => &set.white,
-        }
-    }
-}
diff --git a/rustzx-core/src/zx/sound/ay.rs b/rustzx-core/src/zx/sound/ay.rs
index b2ed3ec..3dc166d 100644
--- a/rustzx-core/src/zx/sound/ay.rs
+++ b/rustzx-core/src/zx/sound/ay.rs
@@ -1,9 +1,6 @@
 use crate::{
     utils::make_word,
-    zx::sound::{
-        sample::{SampleGenerator, SoundSample},
-        SAMPLE_RATE,
-    },
+    zx::sound::sample::{SampleGenerator, SoundSample},
 };
 use ayumi::{Ayumi, ChipType, ToneChannel};
 
@@ -20,7 +17,7 @@ pub enum ZXAYMode {
 }
 
 /// AY Chip implementation using Ayumi lib
-pub struct ZXAyChip {
+pub(crate) struct ZXAyChip {
     ay: Ayumi,
     current_reg: usize,
     regs: [u8; 16],
@@ -28,10 +25,10 @@ pub struct ZXAyChip {
 
 impl ZXAyChip {
     /// Constructs new AY Chip
-    pub fn new(mode: ZXAYMode) -> ZXAyChip {
+    pub fn new(sample_rate: usize, mode: ZXAYMode) -> ZXAyChip {
         // configure ayumi
         let mut out = ZXAyChip {
-            ay: Ayumi::new(ChipType::AY, AY_FREQ, SAMPLE_RATE as i32),
+            ay: Ayumi::new(ChipType::AY, AY_FREQ, sample_rate as i32),
             current_reg: 0,
             regs: [0; 16],
         };
diff --git a/rustzx-core/src/zx/sound/beeper.rs b/rustzx-core/src/zx/sound/beeper.rs
index b2419ff..6b25ab4 100644
--- a/rustzx-core/src/zx/sound/beeper.rs
+++ b/rustzx-core/src/zx/sound/beeper.rs
@@ -2,7 +2,7 @@ use crate::zx::sound::sample::{SampleGenerator, SoundSample};
 
 /// Simple beeper implementation
 #[derive(Default)]
-pub struct ZXBeeper {
+pub(crate) struct ZXBeeper {
     curr_bit: bool,
     next_bit: bool,
 }
diff --git a/rustzx-core/src/zx/sound/mixer.rs b/rustzx-core/src/zx/sound/mixer.rs
index 51df281..4cd384b 100644
--- a/rustzx-core/src/zx/sound/mixer.rs
+++ b/rustzx-core/src/zx/sound/mixer.rs
@@ -1,26 +1,35 @@
 //! Module implemets zx spectrum audio devices mixer
-use crate::zx::sound::{
-    ay::{ZXAYMode, ZXAyChip},
-    beeper::ZXBeeper,
-    sample::{SampleGenerator, SoundSample},
-    samples_from_time, SAMPLES,
+use crate::zx::{
+    constants::FPS,
+    sound::{
+        beeper::ZXBeeper,
+        sample::{SampleGenerator, SoundSample},
+    },
 };
+
+#[cfg(feature = "ay")]
+use crate::zx::sound::ay::{ZXAYMode, ZXAyChip};
+
 use alloc::collections::VecDeque;
 
 /// Main sound mixer.
-pub struct ZXMixer {
+pub(crate) struct ZXMixer {
     /// direct access to beeper device
     pub beeper: ZXBeeper,
     /// direct access to AY device
+    #[cfg(feature = "ay")]
     pub ay: ZXAyChip,
     ring_buffer: VecDeque<SoundSample<f32>>,
     last_pos: usize,
     last_sample: SoundSample<f32>,
     master_volume: f64,
     beeper_volume: f64,
+    #[cfg(feature = "ay")]
     ay_volume: f64,
+    #[cfg(feature = "ay")]
     use_ay: bool,
     use_beeper: bool,
+    sample_rate: usize,
 }
 
 impl ZXMixer {
@@ -28,18 +37,26 @@ impl ZXMixer {
     /// # Arguments
     /// - `use_beeper` - process beeper or not
     /// - `use_ay` - process ay chip or not
-    pub fn new(use_beeper: bool, use_ay: bool) -> ZXMixer {
+    pub fn new(
+        use_beeper: bool,
+        #[cfg(feature = "ay")] use_ay: bool,
+        sample_rate: usize,
+    ) -> ZXMixer {
         ZXMixer {
             beeper: ZXBeeper::default(),
-            ay: ZXAyChip::new(ZXAYMode::Mono),
-            ring_buffer: VecDeque::with_capacity(SAMPLES),
+            #[cfg(feature = "ay")]
+            ay: ZXAyChip::new(sample_rate, ZXAYMode::Mono),
+            ring_buffer: VecDeque::with_capacity(sample_rate),
             last_pos: 0,
             last_sample: SoundSample::new(0.0, 0.0),
             master_volume: 0.5,
             beeper_volume: 1.0,
+            #[cfg(feature = "ay")]
             ay_volume: 1.0,
+            #[cfg(feature = "ay")]
             use_ay,
             use_beeper,
+            sample_rate,
         }
     }
 
@@ -53,11 +70,11 @@ impl ZXMixer {
     /// Updates internal buffer of mixer and fills it with new samples
     pub fn process(&mut self, current_time: f64) {
         // buffer overflow
-        if self.ring_buffer.len() >= SAMPLES {
+        if self.ring_buffer.len() >= self.samples_per_frame() {
             return;
         }
         // so at this moment we need to get new samples from devices
-        let curr_pos = samples_from_time(current_time);
+        let curr_pos = self.sample_count_for_frame_fraction(current_time);
         // if we on same pos or frame passed then no new samples
         if curr_pos <= self.last_pos {
             return;
@@ -73,8 +90,8 @@ impl ZXMixer {
 
     /// fills buffer to eng on new frame
     pub fn new_frame(&mut self) {
-        if self.ring_buffer.len() < SAMPLES {
-            for _ in self.ring_buffer.len()..SAMPLES {
+        if self.ring_buffer.len() < self.samples_per_frame() {
+            for _ in self.ring_buffer.len()..self.samples_per_frame() {
                 self.ring_buffer.push_back(self.last_sample);
             }
         }
@@ -92,6 +109,7 @@ impl ZXMixer {
             SoundSample::new(0.0, 0.0)
         };
         // prevent AY sound generation if disabled [it is pretty long process]
+        #[cfg(feature = "ay")]
         if self.use_ay {
             master_float.mix(&self.ay.gen_sample());
         }
@@ -99,4 +117,15 @@ impl ZXMixer {
         self.last_sample = master;
         master
     }
+
+    fn samples_per_frame(&self) -> usize {
+        self.sample_rate / FPS
+    }
+
+    fn sample_count_for_frame_fraction(&self, fraction: f64) -> usize {
+        if fraction >= 1f64 {
+            return self.samples_per_frame();
+        }
+        (self.samples_per_frame() as f64 * fraction) as usize
+    }
 }
diff --git a/rustzx-core/src/zx/sound/mod.rs b/rustzx-core/src/zx/sound/mod.rs
index dbd6782..acd95bc 100644
--- a/rustzx-core/src/zx/sound/mod.rs
+++ b/rustzx-core/src/zx/sound/mod.rs
@@ -1,23 +1,7 @@
 //! Module implements emulation of sound chip AY, Spectrum Beeper and Mixer
+#[cfg(feature = "ay")]
 pub mod ay;
-pub mod beeper;
-pub mod mixer;
 pub mod sample;
 
-use crate::zx::constants::FPS;
-
-pub const SAMPLE_RATE: usize = 44100;
-/// samples per frame
-pub const SAMPLES: usize = SAMPLE_RATE / FPS;
-pub const CHANNELS: usize = 2;
-
-/// Returns, which must be already processed at this time
-pub fn samples_from_time(time: f64) -> usize {
-    if time >= 1.0 {
-        SAMPLES
-    } else if time <= 0.0 {
-        0
-    } else {
-        (SAMPLES as f64 * time) as usize
-    }
-}
+pub(crate) mod beeper;
+pub(crate) mod mixer;
diff --git a/rustzx-core/src/zx/sound/sample.rs b/rustzx-core/src/zx/sound/sample.rs
index c83e339..8a3d141 100644
--- a/rustzx-core/src/zx/sound/sample.rs
+++ b/rustzx-core/src/zx/sound/sample.rs
@@ -2,6 +2,7 @@ use core::{
     i16,
     ops::{Add, Mul, MulAssign, Sub},
 };
+
 /// Raw Sample can be only f64 or i16
 pub trait RawSample: Clone + Copy + MulAssign + Mul + Add + Sub {}
 impl RawSample for f64 {}
@@ -58,30 +59,6 @@ impl SoundSample<f64> {
         self
     }
 
-    /// Transforms normalized float sample to i16 sample
-    pub fn into_i16(self) -> SoundSample<i16> {
-        // here is some thick hack :D
-        // we have value in 0.0..1.0. We need value in min_i16...max_i16 (signed)
-        // so we unly need one float multiplication, one floor and one XOR with highest bit
-        // multiplication + floor => 0...max_u16
-        // XOR =>  4 bit example {
-        //      0b0000 [0] => 0b1000 [-8]
-        //      0b0001 [1] => 0b1000 [-7]
-        //      ...
-        //      0b0111 [7] => 0b1111 [-1]
-        //      0b1000 [8] => 0b0000 [0]
-        //      0b1000 [9] => 0b0001 [1]
-        //      ...
-        //      0b1111 [15] => 0b0111 [7]
-        // }
-        // So we can easily get range expansion only with XOR operation in MSb
-        // NOTE: `i16 as u16` and `u16 as i16` have no cost
-        SoundSample {
-            left: ((self.left * (u16::max_value() - ERROR_SIZE) as f64) as u16 ^ 0x8000) as i16,
-            right: ((self.right * (u16::max_value() - ERROR_SIZE) as f64) as u16 ^ 0x8000) as i16,
-        }
-    }
-
     /// transform into f32
     pub fn into_f32(self) -> SoundSample<f32> {
         SoundSample {
@@ -105,7 +82,7 @@ impl SoundSample<f64> {
 }
 
 /// Trait which signals that structure can generate SoundSamples
-pub trait SampleGenerator<T>
+pub(crate) trait SampleGenerator<T>
 where
     T: RawSample,
 {
diff --git a/rustzx-core/src/zx/tape/empty.rs b/rustzx-core/src/zx/tape/empty.rs
new file mode 100644
index 0000000..205d94e
--- /dev/null
+++ b/rustzx-core/src/zx/tape/empty.rs
@@ -0,0 +1,33 @@
+use crate::{utils::Clocks, zx::tape::TapeImpl, Result};
+
+pub struct Empty;
+
+impl TapeImpl for Empty {
+    fn can_fast_load(&self) -> bool {
+        false
+    }
+
+    fn next_block_byte(&mut self) -> Result<Option<u8>> {
+        Ok(None)
+    }
+
+    fn next_block(&mut self) -> Result<bool> {
+        Ok(false)
+    }
+
+    fn current_bit(&self) -> bool {
+        false
+    }
+
+    fn process_clocks(&mut self, _clocks: Clocks) -> Result<()> {
+        Ok(())
+    }
+
+    fn stop(&mut self) {}
+
+    fn play(&mut self) {}
+
+    fn rewind(&mut self) -> Result<()> {
+        Ok(())
+    }
+}
diff --git a/rustzx-core/src/zx/tape/mod.rs b/rustzx-core/src/zx/tape/mod.rs
index 847022b..1188910 100644
--- a/rustzx-core/src/zx/tape/mod.rs
+++ b/rustzx-core/src/zx/tape/mod.rs
@@ -1,42 +1,37 @@
-//! Contains Tape handling type and functions
-
+mod empty;
 mod tap;
-// reexport Tap Tape player
-pub use self::tap::Tap;
 
-use crate::utils::Clocks;
+pub use self::{empty::Empty, tap::Tap};
+
+use crate::{host::LoadableAsset, utils::Clocks, Result};
 
 use enum_dispatch::enum_dispatch;
 
 #[enum_dispatch(TapeImpl)]
-pub enum ZXTape {
-    Tap(Tap),
+pub enum ZXTape<A: LoadableAsset> {
+    Tap(Tap<A>),
+    Empty(Empty),
+}
+
+impl<A: LoadableAsset> Default for ZXTape<A> {
+    fn default() -> Self {
+        Self::Empty(Empty)
+    }
 }
 
 #[enum_dispatch]
 pub trait TapeImpl {
-    // -----------------
-    // FAST LOAD SECTION
-    // -----------------
-    /// is this type of tape is allowed to fast load blocks?
     fn can_fast_load(&self) -> bool;
-    /// Returns byte of current block or `None` if offset exceeds block Size
-    fn block_byte(&self, offset: usize) -> Option<u8>;
-    /// Moves tape pointer to next block
-    fn next_block(&mut self);
-    /// Resets relative position in block to zero
-    fn reset_pos_in_block(&mut self);
-    // -----------------
-    //  GENERAL SECTION
-    // -----------------
-    /// Returns current ear bit
+    /// 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
+    fn next_block(&mut self) -> Result<bool>;
+    /// Returns current tape (`ear`) bit
     fn current_bit(&self) -> bool;
-    /// Makes procession of type in definite time
-    fn process_clocks(&mut self, clocks: Clocks);
-    /// stops tape
+    /// Perform tape processing emulation within `clocks` time limit
+    fn process_clocks(&mut self, clocks: Clocks) -> Result<()>;
     fn stop(&mut self);
-    /// plays tape
     fn play(&mut self);
-    /// rewinds tape
-    fn rewind(&mut self);
+    /// Rewinds tape content to the beginning
+    fn rewind(&mut self) -> Result<()>;
 }
diff --git a/rustzx-core/src/zx/tape/tap.rs b/rustzx-core/src/zx/tape/tap.rs
index feaa5e7..ca66c9e 100644
--- a/rustzx-core/src/zx/tape/tap.rs
+++ b/rustzx-core/src/zx/tape/tap.rs
@@ -1,9 +1,11 @@
-//! TAP file tape player
+use crate::{
+    error::TapeLoadError,
+    host::{LoadableAsset, SeekFrom},
+    utils::{make_word, Clocks},
+    zx::tape::TapeImpl,
+    Result,
+};
 
-use crate::{host::LoadableAsset, utils::Clocks, zx::tape::TapeImpl, Result};
-use alloc::vec::Vec;
-
-// main constants
 const PILOT_LENGTH: usize = 2168;
 const PILOT_PULSES_HEADER: usize = 8063;
 const PILOT_PULSES_DATA: usize = 3223;
@@ -12,335 +14,263 @@ const SYNC2_LENGTH: usize = 735;
 const BIT_ONE_LENGTH: usize = 1710;
 const BIT_ZERO_LENGTH: usize = 855;
 const PAUSE_LENGTH: usize = 3_500_000;
+const BUFFER_SIZE: usize = 128;
 
-/// state of tape player
 #[derive(PartialEq, Eq, Clone, Copy)]
 enum TapeState {
     Stop,
     Play,
-    Pilot,
+    Pilot { pulses_left: usize },
     Sync,
     NextByte,
-    NextBit,
-    BitHalf(usize),
+    NextBit { mask: u8 },
+    BitHalf { half_bit_delay: usize, mask: u8 },
     Pause,
 }
 
-/// information about block of tape
-#[derive(Clone, Copy)]
-struct BlockInfo {
-    length: usize,
-    pos: usize,
-    end: usize,
-}
-
-// TODO(#53): Eliminate loading a whole file to vector in tap loader
-
-pub struct Tap {
-    /// state of tape
+pub struct Tap<A: LoadableAsset> {
+    asset: A,
     state: TapeState,
-    /// previous state
     prev_state: TapeState,
-    /// data of tape
-    data: Vec<u8>,
-    /// fields for pulse making from byte
+    buffer: [u8; BUFFER_SIZE],
+    bufer_offset: usize,
+    block_bytes_read: usize,
+    current_block_size: Option<usize>,
+    tape_ended: bool,
+    // Non-fastload related fields
     curr_bit: bool,
     curr_byte: u8,
-    curr_mask: u8,
-    // pulses left to next state
-    pulse_counter: usize,
-    /// block info
-    block_info: Vec<BlockInfo>,
-    block: usize,
-    pos_in_block: usize,
-    /// between-state timings
     delay: Clocks,
-    acc_clocks: Clocks,
 }
 
-impl Default for Tap {
-    fn default() -> Self {
-        Self {
+impl<A: LoadableAsset> Tap<A> {
+    pub fn from_asset(asset: A) -> Result<Self> {
+        let tap = Self {
             prev_state: TapeState::Stop,
             state: TapeState::Stop,
-            data: Vec::new(),
             curr_bit: false,
             curr_byte: 0x00,
-            curr_mask: 0x80,
-            pulse_counter: 0,
-            block_info: Vec::new(),
-            block: 0,
-            pos_in_block: 0,
+            buffer: [0u8; BUFFER_SIZE],
+            bufer_offset: 0,
+            block_bytes_read: 0,
+            current_block_size: None,
             delay: Clocks(0),
-            acc_clocks: Clocks(0),
-        }
+            asset,
+            tape_ended: false,
+        };
+        Ok(tap)
     }
 }
 
-impl Tap {
-    /// updates internal structure according new tape file
-    pub fn from_asset(mut asset: impl LoadableAsset) -> Result<Self> {
-        use crate::utils::make_word;
-
-        let mut tap = Self::default();
+impl<A: LoadableAsset> TapeImpl for Tap<A> {
+    fn can_fast_load(&self) -> bool {
+        self.state == TapeState::Stop
+    }
 
-        let mut buffer = [0u8; 1024];
-        let mut read_bytes = asset.read(&mut buffer)?;
-        while read_bytes != 0 {
-            tap.data.extend_from_slice(&buffer[0..read_bytes]);
-            read_bytes = asset.read(&mut buffer)?;
+    fn next_block_byte(&mut self) -> Result<Option<u8>> {
+        if self.tape_ended {
+            return Ok(None);
         }
 
-        tap.block_info.clear();
-        // get all blocks data
-        let mut p = 0;
-        'blocks: loop {
-            // get length of the block
-            let len = make_word(tap.data[p + 1], tap.data[p]) as usize;
-            // push to vector of blocks
-            tap.block_info.push(BlockInfo {
-                length: len,
-                pos: p + 2,
-                end: p + 2 + len - 1,
-            });
-            // shift pos
-            p += 2 + len;
-            // check bounds
-            if p >= tap.data.len() {
-                break 'blocks;
+        if let Some(block_size) = self.current_block_size {
+            if self.block_bytes_read >= block_size {
+                return Ok(None);
             }
-        }
-        tap.reset_state();
 
-        Ok(tap)
-    }
+            let mut buffer_read_pos = self.block_bytes_read - self.bufer_offset;
 
-    /// resets internal tape state
-    fn reset_state(&mut self) {
-        self.state = TapeState::Stop;
-        self.curr_bit = false;
-        self.curr_byte = 0x00;
-        self.curr_mask = 0x80;
-        self.block = 0;
-        self.pos_in_block = 0;
-        self.delay = Clocks(0);
-        self.acc_clocks = Clocks(0);
-    }
-}
+            // 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);
+                self.asset.read_exact(&mut self.buffer[0..bytes_to_read])?;
+                self.bufer_offset += BUFFER_SIZE;
+                buffer_read_pos = 0;
+            }
 
-impl TapeImpl for Tap {
-    /// can autoload only if tape stopped
-    fn can_fast_load(&self) -> bool {
-        self.state == TapeState::Stop
-    }
+            // Check last byte in block
+            if self.block_bytes_read >= block_size {
+                self.current_block_size = None;
+                self.block_bytes_read = 0;
+                return Ok(None);
+            }
 
-    /// returns byte of block
-    fn block_byte(&self, offset: usize) -> Option<u8> {
-        if self.block_info.is_empty() {
-            return None;
-        };
-        let block = self.block_info[self.block];
-        if offset < block.length {
-            Some(self.data[block.pos + offset])
-        } else {
-            None
+            // Perform actual read and advance position
+            let result = self.buffer[buffer_read_pos];
+            self.block_bytes_read += 1;
+            return Ok(Some(result));
         }
+
+        Ok(None)
     }
 
-    /// switches tape player to next block
-    fn next_block(&mut self) {
-        self.block += 1;
-        // make loop
-        if self.block >= self.block_info.len() {
-            self.block = 0;
+    fn next_block(&mut self) -> Result<bool> {
+        if self.tape_ended {
+            return Ok(false);
         }
-        self.state = TapeState::Stop;
-    }
 
-    /// resets position in block to 0
-    fn reset_pos_in_block(&mut self) {
-        self.pos_in_block = 0;
+        // Skip leftovers from the previous block
+        while self.next_block_byte()?.is_some() {}
+
+        let mut block_size_buffer = [0u8; 2];
+        if self.asset.read_exact(&mut block_size_buffer).is_err() {
+            self.tape_ended = true;
+            return Ok(false);
+        }
+        let block_size = make_word(block_size_buffer[1], block_size_buffer[0]) as usize;
+        let block_bytes_to_read = block_size.min(BUFFER_SIZE);
+        self.asset
+            .read_exact(&mut self.buffer[0..block_bytes_to_read])?;
+
+        self.bufer_offset = 0;
+        self.block_bytes_read = 0;
+        self.current_block_size = Some(block_size);
+
+        Ok(true)
     }
 
-    /// returns current bit
     fn current_bit(&self) -> bool {
         self.curr_bit
     }
 
-    /// makes internal state change based on clocks count
-    fn process_clocks(&mut self, clocks: Clocks) {
-        // if there are no blocks
-        if self.block_info.is_empty() {
-            return;
-        }
-        // get block info, check bunds
-        let block = if self.block >= self.block_info.len() {
-            self.block_info[0]
-        } else {
-            self.block_info[self.block]
-        };
-        // clocks
-        let clocks = clocks.count();
+    fn process_clocks(&mut self, clocks: Clocks) -> Result<()> {
         if self.state == TapeState::Stop {
-            return;
+            return Ok(());
         }
-        // check delay
+
         if self.delay.count() > 0 {
-            // accumulate clocks for delay
-            self.acc_clocks += clocks;
-            // if enough accumulated clocks then clear delay and drop some accumulated clocks
-            if self.acc_clocks.count() >= self.delay.count() {
-                self.acc_clocks -= self.delay;
+            if clocks > self.delay {
                 self.delay = Clocks(0);
+            } else {
+                self.delay -= clocks;
             }
-            // return anyway, it is delay!
-            return;
-        } else {
-            // clear accumulated clocks
-            self.acc_clocks = Clocks(0);
+            return Ok(());
         }
-        // state machine. Wrapped into the loop for sequental non-clock-consuming state execution
+
         'state_machine: loop {
             match self.state {
-                // Stop state.
                 TapeState::Stop => {
-                    // Tape stopped, return HI bit, set current block pos to zero
-                    self.curr_bit = false;
-                    self.pos_in_block = 0;
-                    // action maked, break state machine
+                    // Reset tape but leave in Stopped state
+                    self.rewind()?;
+                    self.state = TapeState::Stop;
                     break 'state_machine;
                 }
-                // Play state. Starts the tape
                 TapeState::Play => {
-                    // out of range play
-                    if self.block >= self.block_info.len() {
-                        // if play state happened when position is out of range,
-                        // loop will be breaked on next iteration and next block will be with
-                        // number zero
-                        self.block = 0;
+                    if !self.next_block()? {
                         self.state = TapeState::Stop;
                     } else {
-                        // select appropriate pulse count for Pilot sequence
-                        self.pulse_counter = if self.data[block.pos] < 128 {
+                        let first_byte = self
+                            .next_block_byte()?
+                            .ok_or(TapeLoadError::InvalidTapFile)?;
+
+                        // Select appropriate pulse count for Pilot sequence
+                        let pulses_left = if first_byte == 0x00 {
                             PILOT_PULSES_HEADER
                         } else {
                             PILOT_PULSES_DATA
                         };
-                        // so, ok seems to be ok, we can make output bit low
+                        self.curr_byte = first_byte;
                         self.curr_bit = true;
-                        // set delay before next state to one pilot pulse
                         self.delay = Clocks(PILOT_LENGTH);
-                        self.state = TapeState::Pilot;
-                        // break state machine, delay must be emulated
+                        self.state = TapeState::Pilot { pulses_left };
                         break 'state_machine;
                     }
                 }
-                // Pilot pulses
-                TapeState::Pilot => {
-                    // invert bit;
+                TapeState::Pilot { mut pulses_left } => {
                     self.curr_bit = !self.curr_bit;
-                    // one pulse passed
-                    self.pulse_counter -= 1;
-                    if self.pulse_counter > 0 {
-                        // add new delay and break
-                        self.delay = Clocks(PILOT_LENGTH);
-                    } else {
-                        // change state to first sync
-                        self.state = TapeState::Sync;
+                    pulses_left -= 1;
+                    if pulses_left == 0 {
                         self.delay = Clocks(SYNC1_LENGTH);
+                        self.state = TapeState::Sync;
+                    } else {
+                        self.delay = Clocks(PILOT_LENGTH);
+                        self.state = TapeState::Pilot { pulses_left };
                     }
-                    // break anyway for delay
                     break 'state_machine;
                 }
-                // sync pulse
                 TapeState::Sync => {
                     self.curr_bit = !self.curr_bit;
                     self.delay = Clocks(SYNC2_LENGTH);
-                    self.state = TapeState::NextByte;
+                    self.state = TapeState::NextBit { mask: 0x80 };
                     break 'state_machine;
                 }
-                // read next byte
                 TapeState::NextByte => {
-                    // read from most singificant bit
-                    self.curr_mask = 0x80;
-                    self.curr_byte = 0x00;
-                    // break not needed, state doesn't require any time
-                    self.state = TapeState::NextBit;
+                    self.state = if let Some(byte) = self.next_block_byte()? {
+                        self.curr_byte = byte;
+                        TapeState::NextBit { mask: 0x80 }
+                    } else {
+                        TapeState::Pause
+                    }
                 }
-                // next bit
-                TapeState::NextBit => {
-                    // invert bit
+                TapeState::NextBit { mask } => {
                     self.curr_bit = !self.curr_bit;
-                    // depending on bit state select timing and switch to new state
-                    if (self.data[block.pos + self.pos_in_block] & self.curr_mask) == 0 {
+                    if (self.curr_byte & mask) == 0 {
                         self.delay = Clocks(BIT_ZERO_LENGTH);
-                        self.state = TapeState::BitHalf(BIT_ZERO_LENGTH);
+                        self.state = TapeState::BitHalf {
+                            half_bit_delay: BIT_ZERO_LENGTH,
+                            mask,
+                        };
                     } else {
                         self.delay = Clocks(BIT_ONE_LENGTH);
-                        self.state = TapeState::BitHalf(BIT_ONE_LENGTH);
-                        self.curr_byte |= self.curr_mask;
+                        self.state = TapeState::BitHalf {
+                            half_bit_delay: BIT_ONE_LENGTH,
+                            mask,
+                        };
                     };
                     break 'state_machine;
                 }
-                // half of a bit
-                TapeState::BitHalf(pulse_length) => {
-                    // invert bit
+                TapeState::BitHalf {
+                    half_bit_delay,
+                    mut mask,
+                } => {
                     self.curr_bit = !self.curr_bit;
-                    // set timeout same as before
-                    self.delay = Clocks(pulse_length);
-                    // shift right, to the next bit
-                    self.curr_mask >>= 1;
-                    if self.curr_mask == 0 {
-                        self.pos_in_block += 1;
-                        // check if we heve next byte in block
-                        self.state = if self.pos_in_block < block.length {
-                            TapeState::NextByte
-                        } else {
-                            TapeState::Pause
-                        };
+                    self.delay = Clocks(half_bit_delay);
+                    mask >>= 1;
+                    self.state = if mask == 0 {
+                        TapeState::NextByte
                     } else {
-                        // fetch next bit
-                        self.state = TapeState::NextBit;
-                    }
+                        TapeState::NextBit { mask }
+                    };
                     break 'state_machine;
                 }
-                // pause after block
                 TapeState::Pause => {
                     self.curr_bit = !self.curr_bit;
-                    // make delay and go to another block. `Play` state can datermine
-                    // the end of tape
                     self.delay = Clocks(PAUSE_LENGTH);
-                    self.block += 1;
-                    self.pos_in_block = 0;
+                    // Next block or end of the tape
                     self.state = TapeState::Play;
-                    // break directly for delay
                     break 'state_machine;
                 }
             }
         }
+
+        Ok(())
     }
 
-    /// stops tape playback, sets `Stop` state
     fn stop(&mut self) {
         let state = self.state;
         self.prev_state = state;
         self.state = TapeState::Stop;
     }
 
-    /// starts playback
     fn play(&mut self) {
         if self.state == TapeState::Stop {
             if self.prev_state == TapeState::Stop {
                 self.state = TapeState::Play;
             } else {
-                let prev_state = self.prev_state;
-                self.state = prev_state;
+                self.state = self.prev_state;
             }
         }
     }
 
-    /// rewinds tape to start
-    fn rewind(&mut self) {
-        self.reset_state();
+    fn rewind(&mut self) -> Result<()> {
+        self.state = TapeState::Stop;
+        self.curr_bit = false;
+        self.curr_byte = 0x00;
+        self.block_bytes_read = 0;
+        self.bufer_offset = 0;
+        self.current_block_size = None;
+        self.delay = Clocks(0);
+        self.asset.seek(SeekFrom::Start(0))?;
+        self.tape_ended = false;
+        Ok(())
     }
 }
diff --git a/rustzx-core/src/zx/screen/border.rs b/rustzx-core/src/zx/video/border.rs
similarity index 82%
rename from rustzx-core/src/zx/screen/border.rs
rename to rustzx-core/src/zx/video/border.rs
index 28828a1..94bc9d0 100644
--- a/rustzx-core/src/zx/screen/border.rs
+++ b/rustzx-core/src/zx/video/border.rs
@@ -1,10 +1,15 @@
 //! Contains ZXSpectrum border implementation
-use super::colors::*;
+use super::colors::{ZXBrightness, ZXColor};
 use crate::{
+    host::{FrameBuffer, FrameBufferSource},
     utils::Clocks,
-    zx::{constants::*, machine::*},
+    zx::{
+        constants::{
+            BORDER_COLS, BORDER_ROWS, CLOCKS_PER_COL, PIXELS_PER_CLOCK, SCREEN_HEIGHT, SCREEN_WIDTH,
+        },
+        machine::ZXMachine,
+    },
 };
-use alloc::boxed::Box;
 
 /// Internal struct, which contains information about beam position and color
 #[derive(Clone, Copy)]
@@ -37,21 +42,24 @@ impl BeamInfo {
 }
 
 /// ZX Spectrum Border Device
-pub struct ZXBorder {
+pub struct ZXBorder<FB: FrameBuffer> {
     machine: ZXMachine,
-    palette: ZXPalette,
-    buffer: Box<[u8; PIXEL_COUNT * BYTES_PER_PIXEL]>,
+    buffer: FB,
     beam_last: BeamInfo,
     border_changed: bool,
     beam_block: bool,
 }
-impl ZXBorder {
+impl<FB: FrameBuffer> ZXBorder<FB> {
     /// Returns new instance of border device
-    pub fn new(machine: ZXMachine, palette: ZXPalette) -> ZXBorder {
+    pub fn new(machine: ZXMachine, context: FB::Context) -> Self {
         ZXBorder {
             machine,
-            palette,
-            buffer: Box::new([0; PIXEL_COUNT * BYTES_PER_PIXEL]),
+            buffer: FB::new(
+                SCREEN_WIDTH,
+                SCREEN_HEIGHT,
+                FrameBufferSource::Border,
+                context,
+            ),
             beam_last: BeamInfo::first_pixel(ZXColor::White),
             border_changed: true,
             beam_block: false,
@@ -96,17 +104,13 @@ impl ZXBorder {
     /// fills pixels from last pos to passed by arguments with
     fn fill_to(&mut self, line: usize, pixel: usize) {
         let last = self.beam_last;
-        let color_array = self.palette.get_rgba(last.color, ZXBrightness::Normal);
-        // fill pixels
         for p in (last.line * SCREEN_WIDTH + last.pixel)..(line * SCREEN_WIDTH + pixel) {
-            for (b, color_value) in color_array
-                .iter()
-                .copied()
-                .enumerate()
-                .take(BYTES_PER_PIXEL)
-            {
-                self.buffer[p * BYTES_PER_PIXEL + b] = color_value;
-            }
+            self.buffer.set_color(
+                p % SCREEN_WIDTH,
+                p / SCREEN_WIDTH,
+                last.color,
+                ZXBrightness::Normal,
+            );
         }
     }
 
@@ -143,7 +147,7 @@ impl ZXBorder {
     }
 
     /// Returns reference to texture
-    pub fn texture(&self) -> &[u8] {
-        &(*self.buffer)
+    pub fn frame_buffer(&self) -> &FB {
+        &self.buffer
     }
 }
diff --git a/rustzx-core/src/zx/video/colors.rs b/rustzx-core/src/zx/video/colors.rs
new file mode 100644
index 0000000..2248852
--- /dev/null
+++ b/rustzx-core/src/zx/video/colors.rs
@@ -0,0 +1,76 @@
+/// Represents color brightness
+#[derive(Clone, Copy)]
+pub enum ZXBrightness {
+    Normal,
+    Bright,
+}
+
+/// ZX Spectrum color enum
+/// Constructs self from 3-bit value
+#[derive(Clone, Copy)]
+pub enum ZXColor {
+    Black,
+    Blue,
+    Red,
+    Purple,
+    Green,
+    Cyan,
+    Yellow,
+    White,
+}
+
+impl ZXColor {
+    /// Returns ZXColor from 3 bits
+    /// # Panics
+    /// Panics when input color is bigger than 7
+    pub fn from_bits(bits: u8) -> ZXColor {
+        assert!(bits <= 7);
+        match bits {
+            0 => ZXColor::Black,
+            1 => ZXColor::Blue,
+            2 => ZXColor::Red,
+            3 => ZXColor::Purple,
+            4 => ZXColor::Green,
+            5 => ZXColor::Cyan,
+            6 => ZXColor::Yellow,
+            7 => ZXColor::White,
+            _ => unreachable!(),
+        }
+    }
+}
+
+/// ZX Spectrum attribute structure
+/// It contains information about ink, paper color,
+/// flash attribute and brightness
+#[derive(Clone, Copy)]
+pub(crate) struct ZXAttribute {
+    pub ink: ZXColor,
+    pub paper: ZXColor,
+    pub brightness: ZXBrightness,
+    pub flash: bool,
+}
+
+impl ZXAttribute {
+    /// Constructs self from byte
+    pub fn from_byte(data: u8) -> ZXAttribute {
+        ZXAttribute {
+            ink: ZXColor::from_bits(data & 0x07),
+            paper: ZXColor::from_bits((data >> 3) & 0x07),
+            flash: (data & 0x80) != 0,
+            brightness: if (data & 0x40) != 0 {
+                ZXBrightness::Bright
+            } else {
+                ZXBrightness::Normal
+            },
+        }
+    }
+
+    /// Returns active color of pixel in current attribute
+    pub fn active_color(&self, state: bool, enable_flash: bool) -> ZXColor {
+        if state ^ (self.flash && enable_flash) {
+            self.ink
+        } else {
+            self.paper
+        }
+    }
+}
diff --git a/rustzx-core/src/zx/screen/mod.rs b/rustzx-core/src/zx/video/mod.rs
similarity index 55%
rename from rustzx-core/src/zx/screen/mod.rs
rename to rustzx-core/src/zx/video/mod.rs
index 9ed21ec..36a3026 100644
--- a/rustzx-core/src/zx/screen/mod.rs
+++ b/rustzx-core/src/zx/video/mod.rs
@@ -1,5 +1,7 @@
 //! Module contains all screen-rendering platform-independent
 //! types and functions
-pub mod border;
-pub mod canvas;
+#[cfg(feature = "precise-border")]
+pub(crate) mod border;
+pub(crate) mod screen;
+
 pub mod colors;
diff --git a/rustzx-core/src/zx/screen/canvas.rs b/rustzx-core/src/zx/video/screen.rs
similarity index 80%
rename from rustzx-core/src/zx/screen/canvas.rs
rename to rustzx-core/src/zx/video/screen.rs
index 75e976f..30081e0 100644
--- a/rustzx-core/src/zx/screen/canvas.rs
+++ b/rustzx-core/src/zx/video/screen.rs
@@ -1,15 +1,23 @@
 //! Module describes ZX Spectrum screen
 //! *block* - is 8x1 pxels stripe.
 use crate::{
-    utils::{screen::*, *},
-    zx::{constants::*, machine::ZXMachine, screen::colors::*},
+    host::{FrameBuffer, FrameBufferSource},
+    utils::{
+        screen::{attr_col_rel, attr_row_rel, bitmap_col_rel, bitmap_line_rel},
+        Clocks,
+    },
+    zx::{
+        constants::{
+            ATTR_BASE_REL, ATTR_COLS, ATTR_MAX_REL, ATTR_ROWS, BITMAP_MAX_REL, CANVAS_HEIGHT,
+            CANVAS_WIDTH, CLOCKS_PER_COL,
+        },
+        machine::ZXMachine,
+        video::colors::ZXAttribute,
+    },
 };
 use alloc::boxed::Box;
 
-// size of screen buffer in bytes
-const BUFFER_LENGTH: usize = CANVAS_HEIGHT * CANVAS_WIDTH * BYTES_PER_PIXEL;
-
-/// Represents how much 8x1 have been **passed**.
+/// Represents how much 8x1 have been already **rendered**.
 #[derive(PartialEq, Eq, Debug)]
 pub struct BlocksCount {
     pub lines: usize,
@@ -76,32 +84,38 @@ struct ScreenBank {
 }
 
 /// Represents ZXSpectrum emulated mid part of screen (canvas)
-pub struct ZXCanvas {
+pub struct ZXScreen<FB: FrameBuffer> {
     machine: ZXMachine,
-    palette: ZXPalette,
     last_blocks: BlocksCount,
     flash: bool,
     frame_counter: usize,
-    // bitmap buffers
-    buffer: Box<[u8; BUFFER_LENGTH]>,
-    back_buffer: Box<[u8; BUFFER_LENGTH]>,
-    // memory
+    buffer: FB,
+    back_buffer: FB,
     banks: [ScreenBank; 2],
     active_bank: usize,
     next_bank: usize,
 }
 
-impl ZXCanvas {
+impl<FB: FrameBuffer> ZXScreen<FB> {
     /// Constructs new canvas of `machine`
-    pub fn new(machine: ZXMachine) -> ZXCanvas {
-        ZXCanvas {
+    pub fn new(machine: ZXMachine, context: FB::Context) -> Self {
+        Self {
             machine,
-            palette: ZXPalette::default(),
             last_blocks: BlocksCount::new(0, 0),
             flash: false,
             frame_counter: 0,
-            buffer: Box::new([0; BUFFER_LENGTH]),
-            back_buffer: Box::new([0; BUFFER_LENGTH]),
+            buffer: FB::new(
+                CANVAS_WIDTH,
+                CANVAS_HEIGHT,
+                FrameBufferSource::Screen,
+                context.clone(),
+            ),
+            back_buffer: FB::new(
+                CANVAS_WIDTH,
+                CANVAS_HEIGHT,
+                FrameBufferSource::Screen,
+                context,
+            ),
             banks: [
                 ScreenBank {
                     attributes: Box::new([ZXAttribute::from_byte(0); ATTR_COLS * ATTR_ROWS]),
@@ -161,11 +175,12 @@ impl ZXCanvas {
                 for pixel in 0..8 {
                     // from most significant bit
                     let state = ((bitmap << pixel) & 0x80) != 0;
-                    let color = self
-                        .palette
-                        .get_rgba(attr.active_color(state, self.flash), attr.brightness);
-                    let index = (block * 8 + pixel) * BYTES_PER_PIXEL;
-                    self.back_buffer[index..index + BYTES_PER_PIXEL].clone_from_slice(color);
+                    self.back_buffer.set_color(
+                        (block % ATTR_COLS) * 8 + pixel,
+                        block / ATTR_COLS,
+                        attr.active_color(state, self.flash),
+                        attr.brightness,
+                    );
                 }
             }
             // cahnge last block to current
@@ -176,7 +191,14 @@ impl ZXCanvas {
     /// starts new frame
     pub fn new_frame(&mut self) {
         // post finished bitmap to second buffer (all not-rendered part will be updated)
-        self.buffer.clone_from_slice(&(*self.back_buffer));
+        {
+            let Self {
+                buffer,
+                back_buffer,
+                ..
+            } = self;
+            core::mem::swap(buffer, back_buffer);
+        }
         self.last_blocks = BlocksCount::new(0, 0);
         if self.frame_counter % 16 == 0 {
             self.switch_flash();
@@ -207,9 +229,7 @@ impl ZXCanvas {
         }
     }
 
-    /// Returns reference to canvas main texture
-    pub fn texture(&self) -> &[u8] {
-        // TODO(#51): Perform texture generation and color mapping on the host implementation side
-        &(*self.buffer)
+    pub fn frame_buffer(&self) -> &FB {
+        &self.buffer
     }
 }
diff --git a/rustzx/Cargo.toml b/rustzx/Cargo.toml
index 6986387..5ea006e 100644
--- a/rustzx/Cargo.toml
+++ b/rustzx/Cargo.toml
@@ -1,9 +1,8 @@
 [package]
 name = "rustzx"
-version = "0.10.0"
+version = "0.11.0"
 authors = ["Vladyslav Nikonov <mail@pacmancoder.xyz>"]
-publish = false
-description = "ZX Spectum emulator application"
+description = "ZX Spectum emulator"
 repository = "https://github.com/pacmancoder/rustzx"
 readme = "../README.md"
 keywords = ["emulator", "game", "z80"]
@@ -14,8 +13,8 @@ edition = "2018"
 
 [dependencies]
 sdl2 = { version = "0.34", features = ["unsafe_textures", "bundled", "static-link"] }
-rustzx-core = { path =  "../rustzx-core" }
+rustzx-core = { path =  "../rustzx-core", features = ["full"] }
 log = "0.4"
 anyhow = "1"
-clap = "3.0.0-beta.2"
+structopt = "0.3"
 env_logger = "0.8"
diff --git a/rustzx/src/app/events/events_sdl.rs b/rustzx/src/app/events/events_sdl.rs
index fc1f261..84f4a2c 100644
--- a/rustzx/src/app/events/events_sdl.rs
+++ b/rustzx/src/app/events/events_sdl.rs
@@ -2,8 +2,8 @@
 use super::{Event, EventDevice};
 use crate::{app::settings::Settings, backends::SDL_CONTEXT};
 use rustzx_core::{
-    utils::EmulationSpeed,
-    zx::{joy::kempston::KempstonKey, keys::*},
+    zx::{joy::kempston::KempstonKey, keys::ZXKey},
+    EmulationSpeed,
 };
 use sdl2::{event::Event as SdlEvent, keyboard::Scancode, EventPump};
 
@@ -32,53 +32,53 @@ impl EventsSdl {
     fn scancode_to_zxkey(&self, scancode: Option<Scancode>) -> Option<ZXKey> {
         match scancode? {
             // FEFE
-            Scancode::LShift | Scancode::RShift => Some(ZX_KEY_SHIFT),
-            Scancode::Z => Some(ZX_KEY_Z),
-            Scancode::X => Some(ZX_KEY_X),
-            Scancode::C => Some(ZX_KEY_C),
-            Scancode::V => Some(ZX_KEY_V),
+            Scancode::LShift | Scancode::RShift => Some(ZXKey::Shift),
+            Scancode::Z => Some(ZXKey::Z),
+            Scancode::X => Some(ZXKey::X),
+            Scancode::C => Some(ZXKey::C),
+            Scancode::V => Some(ZXKey::V),
             // FDDE
-            Scancode::A => Some(ZX_KEY_A),
-            Scancode::S => Some(ZX_KEY_S),
-            Scancode::D => Some(ZX_KEY_D),
-            Scancode::F => Some(ZX_KEY_F),
-            Scancode::G => Some(ZX_KEY_G),
+            Scancode::A => Some(ZXKey::A),
+            Scancode::S => Some(ZXKey::S),
+            Scancode::D => Some(ZXKey::D),
+            Scancode::F => Some(ZXKey::F),
+            Scancode::G => Some(ZXKey::G),
             // FBFE
-            Scancode::Q => Some(ZX_KEY_Q),
-            Scancode::W => Some(ZX_KEY_W),
-            Scancode::E => Some(ZX_KEY_E),
-            Scancode::R => Some(ZX_KEY_R),
-            Scancode::T => Some(ZX_KEY_T),
+            Scancode::Q => Some(ZXKey::Q),
+            Scancode::W => Some(ZXKey::W),
+            Scancode::E => Some(ZXKey::E),
+            Scancode::R => Some(ZXKey::R),
+            Scancode::T => Some(ZXKey::T),
             // F7FE
-            Scancode::Num1 => Some(ZX_KEY_1),
-            Scancode::Num2 => Some(ZX_KEY_2),
-            Scancode::Num3 => Some(ZX_KEY_3),
-            Scancode::Num4 => Some(ZX_KEY_4),
-            Scancode::Num5 => Some(ZX_KEY_5),
+            Scancode::Num1 => Some(ZXKey::N1),
+            Scancode::Num2 => Some(ZXKey::N2),
+            Scancode::Num3 => Some(ZXKey::N3),
+            Scancode::Num4 => Some(ZXKey::N4),
+            Scancode::Num5 => Some(ZXKey::N5),
             // EFFE
-            Scancode::Num0 => Some(ZX_KEY_0),
-            Scancode::Num9 => Some(ZX_KEY_9),
-            Scancode::Num8 => Some(ZX_KEY_8),
-            Scancode::Num7 => Some(ZX_KEY_7),
-            Scancode::Num6 => Some(ZX_KEY_6),
+            Scancode::Num0 => Some(ZXKey::N0),
+            Scancode::Num9 => Some(ZXKey::N9),
+            Scancode::Num8 => Some(ZXKey::N8),
+            Scancode::Num7 => Some(ZXKey::N7),
+            Scancode::Num6 => Some(ZXKey::N6),
             // DFFE
-            Scancode::P => Some(ZX_KEY_P),
-            Scancode::O => Some(ZX_KEY_O),
-            Scancode::I => Some(ZX_KEY_I),
-            Scancode::U => Some(ZX_KEY_U),
-            Scancode::Y => Some(ZX_KEY_Y),
+            Scancode::P => Some(ZXKey::P),
+            Scancode::O => Some(ZXKey::O),
+            Scancode::I => Some(ZXKey::I),
+            Scancode::U => Some(ZXKey::U),
+            Scancode::Y => Some(ZXKey::Y),
             // BFFE
-            Scancode::Return => Some(ZX_KEY_ENTER),
-            Scancode::L => Some(ZX_KEY_L),
-            Scancode::K => Some(ZX_KEY_K),
-            Scancode::J => Some(ZX_KEY_J),
-            Scancode::H => Some(ZX_KEY_H),
+            Scancode::Return => Some(ZXKey::Enter),
+            Scancode::L => Some(ZXKey::L),
+            Scancode::K => Some(ZXKey::K),
+            Scancode::J => Some(ZXKey::J),
+            Scancode::H => Some(ZXKey::H),
             // 7FFE
-            Scancode::Space => Some(ZX_KEY_SPACE),
-            Scancode::LCtrl | Scancode::RCtrl => Some(ZX_KEY_SYM_SHIFT),
-            Scancode::M => Some(ZX_KEY_M),
-            Scancode::N => Some(ZX_KEY_N),
-            Scancode::B => Some(ZX_KEY_B),
+            Scancode::Space => Some(ZXKey::Space),
+            Scancode::LCtrl | Scancode::RCtrl => Some(ZXKey::SymShift),
+            Scancode::M => Some(ZXKey::M),
+            Scancode::N => Some(ZXKey::N),
+            Scancode::B => Some(ZXKey::B),
             _ => None,
         }
     }
diff --git a/rustzx/src/app/events/mod.rs b/rustzx/src/app/events/mod.rs
index 9230527..75b1194 100644
--- a/rustzx/src/app/events/mod.rs
+++ b/rustzx/src/app/events/mod.rs
@@ -3,8 +3,8 @@
 mod events_sdl;
 
 use rustzx_core::{
-    utils::EmulationSpeed,
-    zx::{joy::kempston::KempstonKey, keys::*},
+    zx::{joy::kempston::KempstonKey, keys::ZXKey},
+    EmulationSpeed,
 };
 use std::path::PathBuf;
 
diff --git a/rustzx/src/app/mod.rs b/rustzx/src/app/mod.rs
index 25f1095..48612e7 100644
--- a/rustzx/src/app/mod.rs
+++ b/rustzx/src/app/mod.rs
@@ -3,7 +3,7 @@ mod events;
 mod rustzx;
 mod settings;
 mod sound;
-mod video;
+pub(crate) mod video;
 
 // main re-export
 pub use self::{rustzx::RustzxApp, settings::Settings};
diff --git a/rustzx/src/app/rustzx.rs b/rustzx/src/app/rustzx.rs
index cf05c3e..dadb2db 100644
--- a/rustzx/src/app/rustzx.rs
+++ b/rustzx/src/app/rustzx.rs
@@ -3,12 +3,23 @@
 //! and command-line interface
 
 use crate::{
-    app::{events::*, settings::Settings, sound::*, video::*},
-    host::{self, DetectedFileKind, GuiHost},
+    app::{
+        events::{Event, EventDevice, EventsSdl},
+        settings::Settings,
+        sound::{SoundDevice, SoundSdl},
+        video::{Rect, TextureInfo, VideoDevice, VideoSdl},
+    },
+    host::{self, AppHost, AppHostContext, DetectedFileKind},
 };
 use anyhow::anyhow;
-use rustzx_core::{emulator::*, zx::constants::*};
+use rustzx_core::{
+    zx::constants::{
+        CANVAS_HEIGHT, CANVAS_WIDTH, CANVAS_X, CANVAS_Y, FPS, SCREEN_HEIGHT, SCREEN_WIDTH,
+    },
+    Emulator, Stopwatch,
+};
 use std::{
+    path::Path,
     thread,
     time::{Duration, Instant},
 };
@@ -56,7 +67,7 @@ fn frame_length(fps: usize) -> Duration {
 /// Application instance type
 pub struct RustzxApp {
     /// main emulator object
-    emulator: Emulator<GuiHost>,
+    emulator: Emulator<AppHost>,
     /// Sound rendering in a separate thread
     snd: Option<Box<dyn SoundDevice>>,
     video: Box<dyn VideoDevice>,
@@ -69,7 +80,7 @@ pub struct RustzxApp {
 impl RustzxApp {
     /// Starts application itself
     pub fn from_config(settings: Settings) -> anyhow::Result<RustzxApp> {
-        let snd: Option<Box<dyn SoundDevice>> = if settings.sound_enabled {
+        let snd: Option<Box<dyn SoundDevice>> = if !settings.disable_sound {
             Some(Box::new(SoundSdl::new(&settings)))
         } else {
             None
@@ -80,7 +91,7 @@ impl RustzxApp {
         let scale = settings.scale as u32;
         let events = Box::new(EventsSdl::new(&settings));
 
-        let mut emulator = Emulator::new(settings.to_rustzx_settings())
+        let mut emulator = Emulator::new(settings.to_rustzx_settings(), AppHostContext)
             .map_err(|e| anyhow!("Failed to construct emulator: {}", e))?;
 
         if let Some(rom) = settings.rom.as_ref() {
@@ -88,18 +99,18 @@ impl RustzxApp {
                 .load_rom(host::load_rom(rom, settings.machine)?)
                 .map_err(|e| anyhow!("Emulator failed to load rom: {}", e))?;
         }
-        if let Some(snapshot) = settings.sna.as_ref() {
+        if let Some(snapshot) = settings.snap.as_ref() {
             emulator
                 .load_snapshot(host::load_snapshot(snapshot)?)
                 .map_err(|e| anyhow!("Emulator failed to load snapshot: {}", e))?;
         }
-        if let Some(tape) = settings.tap.as_ref() {
+        if let Some(tape) = settings.tape.as_ref() {
             emulator
                 .load_tape(host::load_tape(tape)?)
                 .map_err(|e| anyhow!("Emulator failed to load tape: {}", e))?;
         }
 
-        let app = RustzxApp {
+        let mut app = RustzxApp {
             emulator,
             snd,
             video,
@@ -109,6 +120,10 @@ impl RustzxApp {
             scale,
         };
 
+        if let Some(file) = settings.file_autodetect.as_ref() {
+            app.load_file_autodetect(file)?;
+        }
+
         Ok(app)
     }
 
@@ -121,7 +136,10 @@ impl RustzxApp {
             // absolute start time
             let frame_start = Instant::now();
             // Emulate all requested frames
-            let cpu_dt = self.emulator.emulate_frames(MAX_FRAME_TIME, &mut stopwatch);
+            let cpu_dt = self
+                .emulator
+                .emulate_frames(MAX_FRAME_TIME, &mut stopwatch)
+                .map_err(|e| anyhow!("Emulation step failed: {}", e))?;
             // 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
@@ -131,12 +149,12 @@ impl RustzxApp {
                     }
                 }
             }
-            // load new textures to sdl
+
             self.video
-                .update_texture(self.tex_border, self.emulator.controller.border.texture());
+                .update_texture(self.tex_border, self.emulator.border_buffer().rgba_data());
             self.video
-                .update_texture(self.tex_canvas, self.emulator.controller.canvas.texture());
-            // rendering block
+                .update_texture(self.tex_canvas, self.emulator.screen_buffer().rgba_data());
+
             self.video.begin();
             self.video.draw_texture_2d(
                 self.tex_border,
@@ -181,22 +199,7 @@ impl RustzxApp {
                     }
                     Event::InsertTape => self.emulator.play_tape(),
                     Event::StopTape => self.emulator.stop_tape(),
-                    Event::OpenFile(path) => match host::detect_file_type(&path)? {
-                        DetectedFileKind::Snapshot => {
-                            self.emulator
-                                .load_snapshot(host::load_snapshot(&path)?)
-                                .map_err(|e| {
-                                    anyhow!("Emulator failed to drag-n-drop load snapshot: {}", e)
-                                })?;
-                        }
-                        DetectedFileKind::Tape => {
-                            self.emulator
-                                .load_tape(host::load_tape(&path)?)
-                                .map_err(|e| {
-                                    anyhow!("Emulator failed to drag-n-drop load tape: {}", e)
-                                })?;
-                        }
-                    },
+                    Event::OpenFile(path) => self.load_file_autodetect(&path)?,
                 }
             }
             // how long emulation iteration was
@@ -219,4 +222,22 @@ impl RustzxApp {
         }
         Ok(())
     }
+
+    fn load_file_autodetect(&mut self, path: &Path) -> anyhow::Result<()> {
+        match host::detect_file_type(&path)? {
+            DetectedFileKind::Snapshot => {
+                self.emulator
+                    .load_snapshot(host::load_snapshot(&path)?)
+                    .map_err(|e| {
+                        anyhow!("Emulator failed to load auto-detected snapshot: {}", e)
+                    })?;
+            }
+            DetectedFileKind::Tape => {
+                self.emulator
+                    .load_tape(host::load_tape(&path)?)
+                    .map_err(|e| anyhow!("Emulator failed to load auto-detected tape: {}", e))?;
+            }
+        }
+        Ok(())
+    }
 }
diff --git a/rustzx/src/app/settings.rs b/rustzx/src/app/settings.rs
index 398776a..a32cb2e 100644
--- a/rustzx/src/app/settings.rs
+++ b/rustzx/src/app/settings.rs
@@ -1,346 +1,159 @@
 use rustzx_core::{
-    settings::RustzxSettings,
-    utils::EmulationSpeed,
-    zx::{
-        constants::{SCREEN_HEIGHT, SCREEN_WIDTH},
-        machine::ZXMachine,
-        sound::ay::ZXAYMode,
-    },
+    zx::{machine::ZXMachine, sound::ay::ZXAYMode},
+    EmulationSpeed, RustzxSettings,
 };
 
-use clap::{App, AppSettings, Arg};
-use std::path::{Path, PathBuf};
+use std::path::PathBuf;
+
+use structopt::StructOpt;
 
 /// Structure to handle all emulator runtime settings
+#[derive(StructOpt)]
 pub struct Settings {
+    /// Specify machine type for launch. Possible values:
+    ///   [`48k`, `48`] - Sinclair ZX Spectrum 48K
+    ///   [`128k`, `128`] - Sinclair ZX Spectrum 128K
+    #[structopt(verbatim_doc_comment, short, long, default_value = "48k", parse(try_from_str = machine_from_str))]
     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))]
     pub speed: EmulationSpeed,
-    pub fastload: bool,
+    /// Disable fast tape loading
+    #[structopt(long = "nofastload")]
+    pub disable_fastload: 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,
-    pub screen_size: (usize, usize),
-    pub kempston: bool,
+    /// Disable kempston joy support. In 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).
+    /// Defaults to `abc`
+    #[structopt(long, default_value = "abc", parse(try_from_str = ay_mode_from_str))]
+    /// Disable AY-3-8910 chip support
     pub ay_mode: ZXAYMode,
-    pub ay_enabled: bool,
-    pub beeper_enabled: bool,
-    pub sound_enabled: bool,
-    pub volume: usize,
-    pub latency: usize,
+    /// Force enable AY-3-8910 chip on unsupported machines
+    #[structopt(long = "ay", conflicts_with = "force-disable-ay")]
+    pub force_enable_ay: bool,
+    /// Force disable AY-3-8910 chip on supported systems
+    #[structopt(long = "noay", conflicts_with = "force-enable-ay")]
+    pub force_disable_ay: bool,
+    /// Disable beeper
+    #[structopt(long = "nobeeper")]
+    pub disable_beeper: bool,
+    /// Disable sound
+    #[structopt(long = "nosound")]
+    pub disable_sound: bool,
+    /// Set custom sound latency. Defaults to 1024 samples
+    #[structopt(long, default_value = "1024", parse(try_from_str = sound_latency_from_str))]
+    pub sound_latency: usize,
+    /// Set custom sound sample rate. Defaults to 44100 samples per second
+    #[structopt(long, default_value = "44100", parse(try_from_str = sound_sample_rate_from_str))]
+    pub sound_sample_rate: usize,
+
+    /// Set path to custom rom file. in case of multipart ROMs for 128k, the first part file,
+    /// extension of which should end with `.0`
+    #[structopt(long, conflicts_with = "file-autodetect")]
     pub rom: Option<PathBuf>,
-    pub tap: Option<PathBuf>,
-    pub sna: Option<PathBuf>,
+    /// Set tape file path. Only `.tap` files are supported currently
+    #[structopt(long, conflicts_with = "file-autodetect")]
+    pub tape: Option<PathBuf>,
+    /// Set snapshot file path. Only `.sna` files are supported currently
+    #[structopt(long, conflicts_with = "file-autodetect")]
+    pub snap: Option<PathBuf>,
+
+    /// Load provided file to emulator. Emulator will perform autodetect of format if possible
+    pub file_autodetect: Option<PathBuf>,
 }
 
-impl Settings {
-    /// constructs new Settings
-    pub fn new() -> Self {
-        Self {
-            machine: ZXMachine::Sinclair48K,
-            speed: EmulationSpeed::Definite(1),
-            fastload: false,
-            scale: 2,
-            screen_size: (SCREEN_WIDTH * 2, SCREEN_HEIGHT * 2),
-            kempston: false,
-            ay_mode: ZXAYMode::Mono,
-            ay_enabled: false,
-            beeper_enabled: true,
-            sound_enabled: true,
-            volume: 100,
-            latency: 1024,
-            rom: None,
-            tap: None,
-            sna: None,
-        }
-    }
-
-    pub fn from_clap() -> Self {
-        // get defaults
-        let mut out = Self::new();
-        // parse cli
-        let cmd = App::new("rustzx")
-            .setting(AppSettings::ColoredHelp)
-            .version(env!("CARGO_PKG_VERSION"))
-            .author("Vladislav Nikonov <pacmancoder@gmail.com>")
-            .about("ZX Spectrum emulator written in pure Rust")
-            // machine settings
-            .arg(
-                Arg::new("128K")
-                    .long("128k")
-                    .about("Enables ZX Spectrum 128K mode"),
-            )
-            .arg(
-                Arg::new("FASTLOAD")
-                    .short('f')
-                    .long("fastload")
-                    .about("Accelerates standard tape loaders"),
-            )
-            // media files
-            .arg(
-                Arg::new("ROM")
-                    .long("rom")
-                    .value_name("ROM_PATH")
-                    .about("Selects path to rom, otherwise default will be used"),
-            )
-            .arg(
-                Arg::new("TAP")
-                    .long("tap")
-                    .value_name("TAP_PATH")
-                    .about("Selects path to *.tap file"),
-            )
-            .arg(
-                Arg::new("SNA")
-                    .long("sna")
-                    .value_name("SNA_PATH")
-                    .about("Selects path to *.sna snapshot file"),
-            )
-            // devices
-            .arg(Arg::new("KEMPSTON").short('k').long("kempston").about(
-                "Enables Kempston joystick. Controlls via arrow keys and \
-                 Alt buttons",
-            ))
-            // emulator settings
-            .arg(
-                Arg::new("SPEED")
-                    .long("speed")
-                    .value_name("SPEED_VALUE")
-                    .about("Selects speed for emulator in integer multiplier form"),
-            )
-            .arg(
-                Arg::new("SCALE")
-                    .long("scale")
-                    .value_name("SCALE_VALUE")
-                    .about(
-                        "Selects default screen size. possible values are positive \
-                         integers. Default value is 2",
-                    ),
-            )
-            // sound
-            .arg(Arg::new("NOSOUND").long("nosound").about(
-                "Disables sound. Use it when you have problems with audio \
-                 playback",
-            ))
-            .arg(
-                Arg::new("NOBEEPER")
-                    .long("nobeeper")
-                    .about("Disables beeper"),
-            )
-            .arg(
-                Arg::new("AY")
-                    .long("ay")
-                    .value_name("AY_TYPE")
-                    .possible_values(&["none", "mono", "abc", "acb"])
-                    .about(
-                        "Selects AY mode. Use none to disable. \
-                         For stereo features use abc or acb, default is mono for \
-                         128k and none for 48k.",
-                    ),
-            )
-            .arg(
-                Arg::new("VOLUME")
-                    .long("volume")
-                    .value_name("VOLUME_VALUE")
-                    .about(
-                        "Selects volume - value in range 0..200. Volume over 100 \
-                         can cause sound artifacts",
-                    ),
-            )
-            .arg(
-                Arg::new("LATENCY")
-                    .long("latency")
-                    .short('l')
-                    .value_name("SAMPLES")
-                    .about(
-                        "Selects audio latency. Default is 1024 samples. Set higher \
-                         latency if emulator have sound glitches. Or if your \
-                         machine can handle this - try to set it lower. Must be \
-                         power of two.",
-                    ),
-            )
-            .get_matches();
-        // machine type
-        if cmd.is_present("128K") {
-            out.machine(ZXMachine::Sinclair128K);
-        }
-        if let Some(speed_str) = cmd.value_of("SPEED") {
-            if let Ok(speed) = speed_str.parse::<usize>() {
-                out.speed(EmulationSpeed::Definite(speed));
-            }
-        };
-        if let Some(scale_str) = cmd.value_of("SCALE") {
-            if let Ok(scale) = scale_str.parse::<usize>() {
-                out.scale(scale);
-            } else {
-                println!("[Warning] Invalid scale factor");
-            };
-        }
-        out.fastload(cmd.is_present("FASTLOAD"))
-            .beeper(!cmd.is_present("NOBEEPER"))
-            .sound(!cmd.is_present("NOSOUND"))
-            .kempston(cmd.is_present("KEMPSTON"));
-        if let Some(path) = cmd.value_of_os("ROM") {
-            if Path::new(path).is_file() {
-                out.rom(path);
-            } else {
-                println!(
-                    "[Warning] ROM file \"{}\" not found",
-                    path.to_string_lossy()
-                );
-            }
-        }
-        if let Some(path) = cmd.value_of_os("TAP") {
-            if Path::new(path).is_file() {
-                out.tap(path);
-            } else {
-                println!(
-                    "[Warning] Tape file \"{}\" not found",
-                    path.to_string_lossy()
-                );
-            }
-        }
-        if let Some(path) = cmd.value_of_os("SNA") {
-            if out.machine == ZXMachine::Sinclair48K {
-                if Path::new(path).is_file() {
-                    out.sna(path);
-                } else {
-                    println!(
-                        "[Warning] Snapshot file \"{}\" not found",
-                        path.to_string_lossy()
-                    );
-                }
-            } else {
-                println!("[Warning] 128K SNA is not supported!");
-            }
-        }
-        if let Some(value) = cmd.value_of("AY") {
-            match value {
-                "none" => out.ay(false),
-                "mono" => out.ay_mode(ZXAYMode::Mono),
-                "abc" => out.ay_mode(ZXAYMode::ABC),
-                "acb" => out.ay_mode(ZXAYMode::ACB),
-                _ => unreachable!(),
-            };
-        };
-        if let Some(value) = cmd.value_of("VOLUME") {
-            if let Ok(value) = value.parse::<usize>() {
-                out.volume(value);
-            } else {
-                println!("[Warning] Volume value is incorrect, setting volume to 100");
-            }
-        };
-        if let Some(latency_str) = cmd.value_of("LATENCY") {
-            if let Ok(latency) = latency_str.parse::<usize>() {
-                out.latency(latency);
-            }
-        };
-        out
+fn machine_from_str(s: &str) -> Result<ZXMachine, anyhow::Error> {
+    match s.to_lowercase().as_str() {
+        "48k" | "48" => Ok(ZXMachine::Sinclair48K),
+        "128k" | "128" => Ok(ZXMachine::Sinclair128K),
+        s => Err(anyhow::anyhow!("Invalid machine type `{}`", s)),
     }
+}
 
-    /// Changes machine type
-    pub fn machine(&mut self, machine: ZXMachine) -> &mut Self {
-        self.machine = machine;
-        match machine {
-            ZXMachine::Sinclair48K => self.ay_enabled = false,
-            ZXMachine::Sinclair128K => self.ay_enabled = true,
-        }
-        self
-    }
+fn emualtion_speed_from_str(s: &str) -> Result<EmulationSpeed, anyhow::Error> {
+    match s.to_lowercase().as_str() {
+        "max" => Ok(EmulationSpeed::Max),
+        s => {
+            let speed: std::num::NonZeroUsize = s
+                .parse()
+                .map_err(|_| anyhow::anyhow!("Invalid emulation speed `{}`", s))?;
 
-    /// changes screen scale
-    pub fn scale(&mut self, scale: usize) -> &mut Self {
-        // place into bounds
-        if scale > 5 {
-            self.scale = 2;
-        } else {
-            self.scale = scale;
+            Ok(EmulationSpeed::Definite(speed.into()))
         }
-        self.screen_size = (SCREEN_WIDTH * self.scale, SCREEN_HEIGHT * self.scale);
-        self
-    }
-
-    /// changes fastload flag
-    pub fn fastload(&mut self, value: bool) -> &mut Self {
-        self.fastload = value;
-        self
     }
+}
 
-    /// changes lound latency
-    pub fn latency(&mut self, latency: usize) -> &mut Self {
-        self.latency = latency;
-        self
-    }
+fn scale_from_str(s: &str) -> Result<usize, anyhow::Error> {
+    let scale: std::num::NonZeroUsize = s
+        .parse()
+        .map_err(|_| anyhow::anyhow!("Invalid screen scale `{}`", s))?;
 
-    /// Changes AY chip mode
-    pub fn ay_mode(&mut self, mode: ZXAYMode) -> &mut Self {
-        self.ay_enabled = true;
-        self.ay_mode = mode;
-        self
-    }
+    Ok(scale.into())
+}
 
-    /// Changes ay state (on/off)
-    pub fn ay(&mut self, state: bool) -> &mut Self {
-        self.ay_enabled = state;
-        self
+fn ay_mode_from_str(s: &str) -> Result<ZXAYMode, anyhow::Error> {
+    match s.to_lowercase().as_str() {
+        "mono" => Ok(ZXAYMode::Mono),
+        "abc" => Ok(ZXAYMode::ABC),
+        "acb" => Ok(ZXAYMode::ACB),
+        s => Err(anyhow::anyhow!("Invalid AY chip mode `{}`", s)),
     }
+}
 
-    /// Changes beeper state (on/off)
-    pub fn beeper(&mut self, state: bool) -> &mut Self {
-        self.beeper_enabled = state;
-        self
-    }
+fn sound_latency_from_str(s: &str) -> Result<usize, anyhow::Error> {
+    let latency = s
+        .parse::<usize>()
+        .map_err(|_| anyhow::anyhow!("Invalid sound latency `{}`", s))?;
 
-    /// changes sound flag
-    pub fn sound(&mut self, state: bool) -> &mut Self {
-        self.sound_enabled = state;
-        self
+    if latency < 64 {
+        anyhow::bail!("Setting sound latency lower than 64 is bad for your health");
     }
-
-    /// Changes volume
-    pub fn volume(&mut self, val: usize) -> &mut Self {
-        self.volume = if val > 200 { 200 } else { val };
-        self
+    if latency > 1024 * 64 {
+        anyhow::bail!("This sound latency is HUGE. Please don't try this at home!");
     }
 
-    /// cahnges kempston joy connection
-    pub fn kempston(&mut self, value: bool) -> &mut Self {
-        self.kempston = value;
-        self
-    }
+    Ok(latency)
+}
 
-    /// changes TAP path
-    pub fn tap(&mut self, value: impl AsRef<Path>) -> &mut Self {
-        self.tap = Some(value.as_ref().into());
-        self
-    }
+fn sound_sample_rate_from_str(s: &str) -> Result<usize, anyhow::Error> {
+    let sample_rate = s
+        .parse::<usize>()
+        .map_err(|_| anyhow::anyhow!("Invalid sound sample rate {}", s))?;
 
-    /// changes SNA path
-    pub fn sna(&mut self, value: impl AsRef<Path>) -> &mut Self {
-        self.sna = Some(value.as_ref().into());
-        self
+    // Sample rate range derived from https://github.com/audiojs/sample-rate
+    if sample_rate < 8000 {
+        anyhow::bail!("Provided sound sample rate `{}` is too low", sample_rate);
     }
-
-    /// changes ROM path
-    pub fn rom(&mut self, value: impl AsRef<Path>) -> &mut Self {
-        self.rom = Some(value.as_ref().into());
-        self
+    if sample_rate > 384000 {
+        anyhow::bail!("Provided sound sample rate `{}` is too high", sample_rate);
     }
 
-    /// changes emulation speed
-    pub fn speed(&mut self, value: EmulationSpeed) -> &mut Self {
-        self.speed = value;
-        self
-    }
+    Ok(sample_rate)
+}
 
+impl Settings {
     pub fn to_rustzx_settings(&self) -> RustzxSettings {
+        let ay_enabled = (matches!(self.machine, ZXMachine::Sinclair128K) || self.force_enable_ay)
+            && (!self.force_disable_ay);
+
         RustzxSettings {
             machine: self.machine,
             emulation_speed: self.speed,
-            tape_fastload: self.fastload,
-            enable_kempston: self.kempston,
+            tape_fastload: !self.disable_fastload,
+            enable_kempston: !self.disable_kempston,
             ay_mode: self.ay_mode,
-            ay_enabled: self.ay_enabled,
-            beeper_enabled: self.beeper_enabled,
-            sound_enabled: self.sound_enabled,
-            sound_volume: self.volume as u8,
+            ay_enabled,
+            beeper_enabled: !self.disable_beeper,
+            sound_enabled: !self.disable_sound,
+            sound_volume: 100,
             load_default_rom: self.rom.is_none(),
+            sound_sample_rate: self.sound_sample_rate,
         }
     }
 }
diff --git a/rustzx/src/app/sound/sound_sdl.rs b/rustzx/src/app/sound/sound_sdl.rs
index 6f028c6..a7a6117 100644
--- a/rustzx/src/app/sound/sound_sdl.rs
+++ b/rustzx/src/app/sound/sound_sdl.rs
@@ -1,10 +1,11 @@
 //! Real Audio SDL backend
 use super::{SoundDevice, ZXSample};
 use crate::{app::settings::Settings, backends::SDL_CONTEXT};
-use rustzx_core::zx::sound::{CHANNELS, SAMPLE_RATE};
 use sdl2::audio::{AudioCallback, AudioDevice, AudioSpecDesired};
 use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
 
+const CHANNEL_COUNT: usize = 2;
+
 /// Struct which used in SDL audio callback
 struct SdlCallback {
     samples: Receiver<ZXSample>,
@@ -15,7 +16,7 @@ impl AudioCallback for SdlCallback {
 
     /// main callback function
     fn callback(&mut self, out: &mut [f32]) {
-        for chunk in out.chunks_mut(CHANNELS) {
+        for chunk in out.chunks_mut(CHANNEL_COUNT) {
             // recieve samples from channel
             if let Ok(sample) = self.samples.recv() {
                 chunk[0] = sample.left;
@@ -42,11 +43,11 @@ impl SoundSdl {
         if let Some(audio) = audio_subsystem {
             // prepare specs
             let desired_spec = AudioSpecDesired {
-                freq: Some(SAMPLE_RATE as i32),
-                channels: Some(CHANNELS as u8),
-                samples: Some(settings.latency as u16),
+                freq: Some(settings.sound_sample_rate as i32),
+                channels: Some(CHANNEL_COUNT as u8),
+                samples: Some(settings.sound_latency as u16),
             };
-            let (tx, rx) = sync_channel(settings.latency as usize);
+            let (tx, rx) = sync_channel(settings.sound_latency as usize);
             let device_handle = audio
                 .open_playback(None, &desired_spec, |_| SdlCallback { samples: rx })
                 .expect("[ERROR Sdl audio device error, try --nosound]");
diff --git a/rustzx/src/app/video/mod.rs b/rustzx/src/app/video/mod.rs
index b976ecb..81acc48 100644
--- a/rustzx/src/app/video/mod.rs
+++ b/rustzx/src/app/video/mod.rs
@@ -1,7 +1,9 @@
 //! platform-independent traits. Submodules with backends will be selectable
 //! via cargo features in future
+mod palette;
 mod video_sdl;
 
+pub use palette::Palette;
 pub use video_sdl::VideoSdl;
 
 /// Texture id binging
diff --git a/rustzx/src/app/video/palette.rs b/rustzx/src/app/video/palette.rs
new file mode 100644
index 0000000..d2768a5
--- /dev/null
+++ b/rustzx/src/app/video/palette.rs
@@ -0,0 +1,65 @@
+use rustzx_core::zx::video::colors::{ZXBrightness, ZXColor};
+
+type ColorRgba = [u8; 4];
+
+/// represents set of colors
+struct ColorSet {
+    black: ColorRgba,
+    blue: ColorRgba,
+    red: ColorRgba,
+    purple: ColorRgba,
+    green: ColorRgba,
+    cyan: ColorRgba,
+    yellow: ColorRgba,
+    white: ColorRgba,
+}
+pub struct Palette {
+    bright: ColorSet,
+    normal: ColorSet,
+}
+
+impl Default for Palette {
+    fn default() -> Self {
+        Palette {
+            normal: ColorSet {
+                black: 0x000000FF_u32.to_be_bytes(),
+                blue: 0x0000CDFF_u32.to_be_bytes(),
+                red: 0xCD0000FF_u32.to_be_bytes(),
+                purple: 0xCD00CDFF_u32.to_be_bytes(),
+                green: 0x00CD00FF_u32.to_be_bytes(),
+                cyan: 0x00CDCDFF_u32.to_be_bytes(),
+                yellow: 0xCDCD00FF_u32.to_be_bytes(),
+                white: 0xCDCDCDFF_u32.to_be_bytes(),
+            },
+            bright: ColorSet {
+                black: 0x000000FF_u32.to_be_bytes(),
+                blue: 0x0000FFFF_u32.to_be_bytes(),
+                red: 0xFF0000FF_u32.to_be_bytes(),
+                purple: 0xFF00FFFF_u32.to_be_bytes(),
+                green: 0x00FF00FF_u32.to_be_bytes(),
+                cyan: 0x00FFFFFF_u32.to_be_bytes(),
+                yellow: 0xFFFF00FF_u32.to_be_bytes(),
+                white: 0xFFFFFFFF_u32.to_be_bytes(),
+            },
+        }
+    }
+}
+
+impl Palette {
+    pub fn get_rgba(&self, color: ZXColor, brightness: ZXBrightness) -> ColorRgba {
+        let set = match brightness {
+            ZXBrightness::Normal => &self.normal,
+            ZXBrightness::Bright => &self.bright,
+        };
+        match color {
+            ZXColor::Black => set.black,
+            ZXColor::Blue => set.blue,
+            ZXColor::Red => set.red,
+            ZXColor::Purple => set.purple,
+            ZXColor::Green => set.green,
+            ZXColor::Cyan => set.cyan,
+            ZXColor::Yellow => set.yellow,
+            ZXColor::White => set.white,
+        }
+    }
+}
diff --git a/rustzx/src/app/video/video_sdl.rs b/rustzx/src/app/video/video_sdl.rs
index b71166e..d421f01 100644
--- a/rustzx/src/app/video/video_sdl.rs
+++ b/rustzx/src/app/video/video_sdl.rs
@@ -1,5 +1,6 @@
 use super::{Rect, TextureInfo, VideoDevice};
 use crate::{app::settings::Settings, backends::SDL_CONTEXT};
+use rustzx_core::zx::constants::{SCREEN_HEIGHT, SCREEN_WIDTH};
 use sdl2::{
     pixels::PixelFormatEnum as PixelFormat,
     rect::Rect as SdlRect,
@@ -26,7 +27,10 @@ impl VideoSdl {
         });
         if let Some(video) = video_subsystem {
             // construct window and renderer form it
-            let (width, height) = settings.screen_size;
+            let (width, height) = (
+                SCREEN_WIDTH * settings.scale,
+                SCREEN_HEIGHT * settings.scale,
+            );
             let window = video
                 .window(
                     &format!("RustZX v{}", env!("CARGO_PKG_VERSION")),
diff --git a/rustzx/src/host/frame_buffer.rs b/rustzx/src/host/frame_buffer.rs
new file mode 100644
index 0000000..0c1136e
--- /dev/null
+++ b/rustzx/src/host/frame_buffer.rs
@@ -0,0 +1,50 @@
+use crate::app::video::Palette;
+use rustzx_core::{
+    host::{FrameBuffer, FrameBufferSource},
+    zx::video::colors::{ZXBrightness, ZXColor},
+};
+
+const RGBA_PIXEL_SIZE: usize = 4;
+
+#[derive(Clone)]
+pub struct FrameBufferContext;
+
+pub struct RgbaFrameBuffer {
+    buffer: Vec<u8>,
+    palette: Palette,
+    buffer_row_size: usize,
+}
+
+impl FrameBuffer for RgbaFrameBuffer {
+    type Context = FrameBufferContext;
+
+    fn new(
+        width: usize,
+        height: usize,
+        _source: FrameBufferSource,
+        _context: Self::Context,
+    ) -> Self {
+        Self {
+            buffer: vec![0u8; width * height * RGBA_PIXEL_SIZE],
+            palette: Palette::default(),
+            buffer_row_size: width * RGBA_PIXEL_SIZE,
+        }
+    }
+
+    fn set_color(&mut self, x: usize, y: usize, color: ZXColor, brightness: ZXBrightness) {
+        let buffer_pos = y * self.buffer_row_size + x * RGBA_PIXEL_SIZE;
+
+        self.palette
+            .get_rgba(color, brightness)
+            .iter()
+            .copied()
+            .zip(&mut self.buffer[buffer_pos..buffer_pos + RGBA_PIXEL_SIZE])
+            .for_each(|(source, dest)| *dest = source);
+    }
+}
+
+impl RgbaFrameBuffer {
+    pub fn rgba_data(&self) -> &[u8] {
+        &self.buffer
+    }
+}
diff --git a/rustzx/src/host/mod.rs b/rustzx/src/host/mod.rs
index 4f77144..190a571 100644
--- a/rustzx/src/host/mod.rs
+++ b/rustzx/src/host/mod.rs
@@ -1,24 +1,36 @@
+mod frame_buffer;
 mod io;
 
 use anyhow::{anyhow, bail, Context};
+use frame_buffer::{FrameBufferContext, RgbaFrameBuffer};
 use io::FileAsset;
 use rustzx_core::{
-    host::{Host, RomFormat, RomSet, Snapshot, Tape},
-    zx::ZXMachine,
+    host::{FrameBuffer, Host, HostContext, RomFormat, RomSet, Snapshot, Tape},
+    zx::machine::ZXMachine,
 };
 use std::{collections::VecDeque, fs::File, path::Path};
 
 const SUPPORTED_SNAPSHOT_FORMATS: [&str; 1] = ["sna"];
 const SUPPORTED_TAPE_FORMATS: [&str; 1] = ["tap"];
 
-pub struct GuiHost;
+pub struct AppHost;
 
-impl Host for GuiHost {
+impl Host for AppHost {
+    type Context = AppHostContext;
+    type FrameBuffer = RgbaFrameBuffer;
     type RomSet = FileRomSet;
     type SnapshotAsset = FileAsset;
     type TapeAsset = FileAsset;
 }
 
+pub struct AppHostContext;
+
+impl HostContext<AppHost> for AppHostContext {
+    fn frame_buffer_context(&self) -> <<AppHost as Host>::FrameBuffer as FrameBuffer>::Context {
+        FrameBufferContext
+    }
+}
+
 pub struct FileRomSet {
     pages: VecDeque<FileAsset>,
 }
diff --git a/rustzx/src/main.rs b/rustzx/src/main.rs
index d899b36..45d98da 100644
--- a/rustzx/src/main.rs
+++ b/rustzx/src/main.rs
@@ -5,11 +5,12 @@ mod backends;
 mod host;
 
 use app::{RustzxApp, Settings};
+use structopt::StructOpt;
 
 fn main() {
     env_logger::init();
 
-    let settings = Settings::from_clap();
+    let settings = Settings::from_args();
     let result = RustzxApp::from_config(settings)
         .and_then(|mut emulator| emulator.start())
         .map_err(|e| log::error!("ERROR: {}", e));


Test patch: