diff --git a/CHANGES b/CHANGES
index 68638bb67..d7b13c1aa 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,11 +1,18 @@
 0.11.0: (Future)
 Features:
+ - Forwarder support for 3DS and Vita
+ - Custom border support
  - New option to lock the maximum frame size
+ - Memory access and information logging
+ - 3DS: Add faster "loose" sync mode, default enabled
  - Scripting: New `input` API for getting raw keyboard/mouse/controller state
  - Scripting: New `storage` API for saving data for a script, e.g. settings
+ - Scripting: New `image` and `canvas` APIs for drawing images and displaying on-screen
  - Scripting: Debugger integration to allow for breakpoints and watchpoints
- - New unlicensed GB mappers: NT (older types 1 and 2), Li Cheng, GGB-81
+ - New unlicensed GB mappers: NT (older types 1 and 2), Li Cheng, GGB-81, Sintax
+ - Initial support for bootleg GBA multicarts
  - Debugger: Add range watchpoints
+ - "Headless" frontend for running tests, automation, etc.
 Emulation fixes:
  - ARM: Add framework for coprocessor support
  - GB Serialize: Add missing Pocket Cam state to savestates
@@ -16,6 +23,7 @@ Emulation fixes:
  - GBA Video: Disable BG target 1 blending when OBJ blending (fixes mgba.io/i/2722)
  - GBA Video: Improve emulation of window start/end conditions (fixes mgba.io/i/1945)
 Other fixes:
+ - ARM Debugger: Fix disassembly of ror r0 barrel shift (fixes mgba.io/i/3412)
  - Core: Fix inconsistencies with setting game-specific overrides (fixes mgba.io/i/2963)
  - Debugger: Fix writing to specific segment in command-line debugger
  - FFmpeg: Fix failing to record videos with CRF video (fixes mgba.io/i/3368)
@@ -26,10 +34,12 @@ Other fixes:
  - mGUI: Load parent directory if last used directory is missing (fixes mgba.io/i/3379)
  - Qt: Fix savestate preview sizes with different scales (fixes mgba.io/i/2560)
  - Qt: Fix potential crash when configuring shortcuts
+ - Qt: Fix regression where loading BIOS creates a save file (fixes mgba.io/i/3359)
 Misc:
  - Core: Handle relative paths for saves, screenshots, etc consistently (fixes mgba.io/i/2826)
  - Core: Improve rumble emulation by averaging state over entire frame (fixes mgba.io/i/3232)
  - Core: Add MD5 hashing for ROMs
+ - Core: Add support for specifying an arbitrary portable directory
  - GB: Prevent incompatible BIOSes from being used on differing models
  - GB Serialize: Add missing savestate support for MBC6 and NT (newer)
  - GBA: Improve detection of valid ELF ROMs
@@ -48,7 +58,12 @@ Misc:
  - Qt: Show maker code and game version in ROM info
  - Qt: Show a dummy shader settings tab if shaders aren't supported
  - Qt: Allow passing multiple games on command line for multiplayer (closes mgba.io/i/3061)
+ - Qt: Support building against Qt 6
+ - Qt: Add shortcuts to increment fast forward speed (mgba.io/i/2903)
+ - Qt: Enable ROM preloading by default
+ - Res: Port hq2x and OmniScale shaders from SameBoy
  - Res: Port NSO-gba-colors shader (closes mgba.io/i/2834)
+ - Res: Update gba-colors shader (closes mgba.io/i/2976)
  - Scripting: Add `callbacks:oneshot` for single-call callbacks
  - Updater: Fix rewriting folders and files on Windows (fixes mgba.io/i/3384)
 
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 42712e91c..ad464bab0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -241,6 +241,10 @@ endif()
 
 if(APPLE)
 	execute_process(COMMAND xcrun --show-sdk-version OUTPUT_VARIABLE MACOSX_SDK)
+	if(NOT MACOSX_SDK)
+		message(WARNING "Could not detect SDK version; defaulting to system version. Is SDKROOT set?")
+		set(MACOSX_SDK ${CMAKE_SYSTEM_VERSION})
+	endif()
 	add_definitions(-D_DARWIN_C_SOURCE)
 	list(APPEND OS_LIB "-framework Foundation")
 
diff --git a/README.md b/README.md
index d937fa0c9..5bb847e9f 100644
--- a/README.md
+++ b/README.md
@@ -73,6 +73,7 @@ The following mappers are partially supported:
 - Hitek (missing logo switching)
 - GGB-81 (missing logo switching)
 - Li Cheng (missing logo switching)
+- Sintax (missing logo switching)
 
 ### Planned features
 
diff --git a/README_JP.md b/README_JP.md
new file mode 100644
index 000000000..41bca336b
--- /dev/null
+++ b/README_JP.md
@@ -0,0 +1,268 @@
+mGBA
+====
+
+mGBAは、ゲームボーイアドバンスのゲームを実行するためのエミュレーターです。mGBAの目標は、既存の多くのゲームボーイアドバンスエミュレーターよりも高速かつ正確であり、他のエミュレーターにはない機能を追加することです。また、ゲームボーイおよびゲームボーイカラーのゲームもサポートしています。
+
+最新のニュースとダウンロードは、[mgba.io](https://mgba.io/)で見つけることができます。
+
+[![Build status](https://buildbot.mgba.io/badges/build-win32.svg)](https://buildbot.mgba.io)
+[![Translation status](https://hosted.weblate.org/widgets/mgba/-/svg-badge.svg)](https://hosted.weblate.org/engage/mgba)
+
+特徴
+--------
+
+- 高精度なゲームボーイアドバンスハードウェアのサポート[<sup>[1]</sup>](#missing)。
+- ゲームボーイ/ゲームボーイカラーのハードウェアサポート。
+- 高速なエミュレーション。ネットブックなどの低スペックハードウェアでもフルスピードで動作することが知られています。
+- 重量級と軽量級のフロントエンドのためのQtおよびSDLポート。
+- ローカル(同じコンピュータ)リンクケーブルのサポート。
+- フラッシュメモリサイズを含む保存タイプの検出[<sup>[2]</sup>](#flashdetect)。
+- モーションセンサーと振動機能を備えたカートリッジのサポート(ゲームコントローラーでのみ使用可能)。
+- 設定なしでもリアルタイムクロックのサポート。
+- ボクタイゲームのためのソーラーセンサーのサポート。
+- ゲームボーイカメラとゲームボーイプリンターのサポート。
+- 内蔵BIOS実装と外部BIOSファイルの読み込み機能。
+- Luaを使用したスクリプトサポート。
+- Tabキーを押し続けることでターボ/早送りサポート。
+- バッククォートを押し続けることで巻き戻し。
+- 最大10まで設定可能なフレームスキップ。
+- スクリーンショットのサポート。
+- チートコードのサポート。
+- 9つのセーブステートスロット。セーブステートはスクリーンショットとしても表示可能。
+- ビデオ、GIF、WebP、およびAPNGの録画。
+- e-Readerのサポート。
+- キーボードとゲームパッドのリマップ可能なコントロール。
+- ZIPおよび7zファイルからの読み込み。
+- IPS、UPS、およびBPSパッチのサポート。
+- コマンドラインインターフェースとGDBリモートサポートを介したゲームデバッグ、GhidraおよびIDA Proと互換性あり。
+- 設定可能なエミュレーションの巻き戻し。
+- GameSharkおよびAction Replayスナップショットの読み込みおよびエクスポートのサポート。
+- RetroArch/LibretroおよびOpenEmu用のコア。
+- [Weblate](https://hosted.weblate.org/engage/mgba)を介した複数の言語のコミュニティ提供の翻訳。
+- その他、多くの小さな機能。
+
+#### ゲームボーイマッパー
+
+以下のマッパーが完全にサポートされています:
+
+- MBC1
+- MBC1M
+- MBC2
+- MBC3
+- MBC3+RTC
+- MBC30
+- MBC5
+- MBC5+Rumble
+- MBC7
+- Wisdom Tree(非公式)
+- NT "old type" 1 and 2(非公式マルチカート)
+- NT "new type"(非公式MBC5類似)
+- Pokémon Jade/Diamond(非公式)
+- Sachen MMC1(非公式)
+
+以下のマッパーが部分的にサポートされています:
+
+- MBC6(フラッシュメモリ書き込みサポートなし)
+- MMM01
+- Pocket Cam
+- TAMA5(RTCサポート不完全)
+- HuC-1(IRサポートなし)
+- HuC-3(IRサポートなし)
+- Sachen MMC2(代替配線サポートなし)
+- BBD(ロゴ切り替えなし)
+- Hitek(ロゴ切り替えなし)
+- GGB-81(ロゴ切り替えなし)
+- Li Cheng(ロゴ切り替えなし)
+
+### 計画されている機能
+
+- ネットワーク対応のマルチプレイヤーリンクケーブルサポート。
+- Dolphin/JOYバスリンクケーブルサポート。
+- MP2kオーディオミキシング、ハードウェアより高品質のサウンド。
+- ツールアシストランのための再録サポート。
+- 包括的なデバッグスイート。
+- ワイヤレスアダプターのサポート。
+
+サポートされているプラットフォーム
+-------------------
+
+- Windows 7以降
+- OS X 10.9(Mavericks)[<sup>[3]</sup>](#osxver)以降
+- Linux
+- FreeBSD
+- Nintendo 3DS
+- Nintendo Switch
+- Wii
+- PlayStation Vita
+
+他のUnix系プラットフォーム(OpenBSDなど)も動作することが知られていますが、テストされておらず、完全にはサポートされていません。
+
+### システム要件
+
+要件は最小限です。Windows Vista以降を実行できるコンピュータであれば、エミュレーションを処理できるはずです。OpenGL 1.1以降のサポートも必要であり、シェーダーや高度な機能にはOpenGL 3.2以降が必要です。
+
+ダウンロード
+---------
+
+ダウンロードは公式ウェブサイトの[ダウンロード][downloads]セクションで見つけることができます。ソースコードは[GitHub][source]で見つけることができます。
+
+コントロール
+--------
+
+コントロールは設定メニューで設定可能です。多くのゲームコントローラーはデフォルトで自動的にマッピングされるはずです。デフォルトのキーボードコントロールは次のとおりです:
+
+- **A**:X
+- **B**:Z
+- **L**:A
+- **R**:S
+- **Start**:Enter
+- **Select**:Backspace
+
+コンパイル
+---------
+
+コンパイルにはCMake 3.1以降の使用が必要です。GCC、Clang、およびVisual Studio 2019はmGBAのコンパイルに使用できることが知られています。
+
+#### Dockerビルド
+
+ほとんどのプラットフォームでのビルドにはDockerを使用することをお勧めします。いくつかのプラットフォームでmGBAをビルドするために必要なツールチェーンと依存関係を含むいくつかのDockerイメージが提供されています。
+
+注意:Windows 10以前の古いWindowsシステムを使用している場合、DockerがVirtualBox共有フォルダーを使用して現在の`mgba`チェックアウトディレクトリをDockerイメージの作業ディレクトリに正しくマッピングするように構成する必要がある場合があります。(詳細については、issue [#1985](https://mgba.io/i/1985)を参照してください。)
+
+Dockerイメージを使用してmGBAをビルドするには、mGBAのチェックアウトのルートで次のコマンドを実行します:
+
+	docker run --rm -it -v ${PWD}:/home/mgba/src mgba/windows:w32
+
+Dockerコンテナを起動した後、ビルド成果物を含む`build-win32`ディレクトリが生成されます。他のプラットフォーム用のDockerイメージに置き換えると、対応する他のディレクトリが生成されます。Docker Hubで利用可能なDockerイメージは次のとおりです:
+
+- mgba/3ds
+- mgba/switch
+- mgba/ubuntu:xenial
+- mgba/ubuntu:bionic
+- mgba/ubuntu:focal
+- mgba/ubuntu:groovy
+- mgba/vita
+- mgba/wii
+- mgba/windows:w32
+- mgba/windows:w64
+
+ビルドプロセスを高速化したい場合は、`-e MAKEFLAGS=-jN`フラグを追加して、`N`個のCPUコアでmGBAの並列ビルドを行うことを検討してください。
+
+#### *nixビルド
+
+UnixベースのシステムでCMakeを使用してビルドするには、次のコマンドを実行することをお勧めします:
+
+	mkdir build
+	cd build
+	cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr ..
+	make
+	sudo make install
+
+これにより、mGBAがビルドされ、`/usr/bin`および`/usr/lib`にインストールされます。インストールされている依存関係は自動的に検出され、依存関係が見つからない場合に無効になる機能は、`cmake`コマンドを実行した後に警告として表示されます。
+
+macOSを使用している場合、手順は少し異なります。homebrewパッケージマネージャーを使用していると仮定すると、依存関係を取得してビルドするための推奨コマンドは次のとおりです:
+
+	brew install cmake ffmpeg libzip qt5 sdl2 libedit lua pkg-config
+	mkdir build
+	cd build
+	cmake -DCMAKE_PREFIX_PATH=`brew --prefix qt5` ..
+	make
+
+macOSでは`make install`を実行しないでください。正しく動作しないためです。
+
+#### Windows開発者ビルド
+
+##### MSYS2
+
+Windowsでの開発用ビルドにはMSYS2を使用することをお勧めします。MSYS2の[ウェブサイト](https://msys2.github.io)に記載されているインストール手順に従ってください。32ビットバージョン(「MSYS2 MinGW 32-bit」)を実行していることを確認してください(x86_64用にビルドする場合は64ビットバージョン「MSYS2 MinGW 64-bit」を実行してください)。必要な依存関係をインストールするために次の追加コマンド(中括弧を含む)を実行します(このコマンドは1100MiB以上のパッケージをダウンロードするため、長時間かかることに注意してください):
+
+	pacman -Sy --needed base-devel git ${MINGW_PACKAGE_PREFIX}-{cmake,ffmpeg,gcc,gdb,libelf,libepoxy,libzip,lua,pkgconf,qt5,SDL2,ntldd-git}
+
+次のコマンドを実行してソースコードをチェックアウトします:
+
+	git clone https://github.com/mgba-emu/mgba.git
+
+最後に、次のコマンドを実行してビルドします:
+
+	mkdir -p mgba/build
+	cd mgba/build
+	cmake .. -G "MSYS Makefiles"
+	make -j$(nproc --ignore=1)
+
+このWindows用mGBAビルドは、実行に必要なDLLが分散しているため、配布には適していないことに注意してください。ただし、開発には最適です。ただし、そのようなビルドを配布する必要がある場合(たとえば、MSYS2環境がインストールされていないマシンでのテスト用)、`cpack -G ZIP`を実行すると、必要なDLLをすべて含むzipファイルが準備されます。
+
+##### Visual Studio
+
+Visual Studioを使用してビルドするには、同様に複雑なセットアップが必要です。まず、[vcpkg](https://github.com/Microsoft/vcpkg)をインストールする必要があります。vcpkgをインストールした後、いくつかの追加パッケージをインストールする必要があります:
+
+    vcpkg install ffmpeg[vpx,x264] libepoxy libpng libzip lua sdl2 sqlite3
+
+このインストールでは、Nvidiaハードウェアでのハードウェアアクセラレーションビデオエンコーディングはサポートされません。これが重要な場合は、事前にCUDAをインストールし、前のコマンドに`ffmpeg[vpx,x264,nvcodec]`を置き換えます。
+
+Qtもインストールする必要があります。ただし、Qtは合理的な組織ではなく、困窮している会社によって所有および運営されているため、最新バージョンのオフラインオープンソースエディションインストーラーは存在しないため、[旧バージョンのインストーラー](https://download.qt.io/official_releases/qt/5.12/5.12.9/qt-opensource-windows-x86-5.12.9.exe)に戻る必要があります(これには無用なアカウントの作成が必要ですが、一時的に無効なプロキシを設定するか、ネットワークを無効にすることで回避できます)、オンラインインストーラーを使用する(いずれにしてもアカウントが必要です)、またはvcpkgを使用してビルドする(遅い)。これらはすべて良い選択肢ではありません。インストーラーを使用する場合は、適用可能なMSVCバージョンをインストールする必要があります。オフラインインストーラーはMSVC 2019をサポートしていないことに注意してください。vcpkgを使用する場合、次のようにインストールする必要があります。特にクアッドコア以下のコンピュータではかなりの時間がかかります:
+
+    vcpkg install qt5-base qt5-multimedia
+
+次に、Visual Studioを開き、「リポジトリのクローンを作成」を選択し、`https://github.com/mgba-emu/mgba.git`を入力します。Visual Studioがクローンを完了したら、「ファイル」>「CMake」に移動し、チェックアウトされたリポジトリのルートにあるCMakeLists.txtファイルを開きます。そこから、他のVisual Studio CMakeプロジェクトと同様にVisual StudioでmGBAを開発できます。
+
+#### ツールチェーンビルド
+
+devkitARM(3DS用)、devkitPPC(Wii用)、devkitA64(Switch用)、またはvitasdk(PS Vita用)を持っている場合は、次のコマンドを使用してビルドできます:
+
+	mkdir build
+	cd build
+	cmake -DCMAKE_TOOLCHAIN_FILE=../src/platform/3ds/CMakeToolchain.txt ..
+	make
+
+次のプラットフォーム用に`-DCMAKE_TOOLCHAIN_FILE`パラメータを置き換えます:
+
+- 3DS:`../src/platform/3ds/CMakeToolchain.txt`
+- Switch:`../src/platform/switch/CMakeToolchain.txt`
+- Vita:`../src/platform/psp2/CMakeToolchain.vitasdk`
+- Wii:`../src/platform/wii/CMakeToolchain.txt`
+
+### 依存関係
+
+mGBAには厳密な依存関係はありませんが、特定の機能には次のオプションの依存関係が必要です。依存関係が見つからない場合、これらの機能は無効になります。
+
+- Qt 5:GUIフロントエンド用。オーディオにはQt MultimediaまたはSDLが必要です。
+- SDL:より基本的なフロントエンドおよびQtフロントエンドでのゲームパッドサポート用。SDL 2が推奨されますが、1.2もサポートされています。
+- zlibおよびlibpng:スクリーンショットサポートおよびPNG内セーブステートサポート用。
+- libedit:コマンドラインデバッガーサポート用。
+- ffmpegまたはlibav:ビデオ、GIF、WebP、およびAPNGの録画用。
+- libzipまたはzlib:zipファイルに保存されたROMの読み込み用。
+- SQLite3:ゲームデータベース用。
+- libelf:ELF読み込み用。
+- Lua:スクリプト用。
+- json-c:スクリプトの`storage` API用。
+
+SQLite3、libpng、およびzlibはエミュレーターに含まれているため、最初に外部でコンパイルする必要はありません。
+
+脚注
+---------
+
+<a name="missing">[1]</a> 現在欠けている機能は次のとおりです
+
+- モード3、4、および5のOBJウィンドウ([バグ#5](http://mgba.io/b/5))
+
+<a name="flashdetect">[2]</a> フラッシュメモリサイズの検出は一部のケースで機能しません。これらは実行時に構成できますが、そのようなケースに遭遇した場合はバグを報告することをお勧めします。
+
+<a name="osxver">[3]</a> 10.9はQtポートにのみ必要です。10.7またはそれ以前のバージョンでQtポートをビルドまたは実行することは可能かもしれませんが、公式にはサポートされていません。SDLポートは10.5で動作することが知られており、古いバージョンでも動作する可能性があります。
+
+[downloads]: http://mgba.io/downloads.html
+[source]: https://github.com/mgba-emu/mgba/
+
+著作権
+---------
+
+mGBAの著作権は© 2013 – 2023 Jeffrey Pfauに帰属します。これは[Mozilla Public License version 2.0](https://www.mozilla.org/MPL/2.0/)の下で配布されています。配布されたLICENSEファイルにライセンスのコピーが含まれています。
+
+mGBAには次のサードパーティライブラリが含まれています:
+
+- [inih](https://github.com/benhoyt/inih)、著作権© 2009 – 2020 Ben Hoyt、BSD 3-clauseライセンスの下で使用。
+- [LZMA SDK](http://www.7-zip.org/sdk.html)、パブリックドメイン。
+- [MurmurHash3](https://github.com/aappleby/smhasher)、Austin Applebyによる実装、パブリックドメイン。
+- [getopt for MSVC](https://github.com/skandhurkat/Getopt-for-Visual-Studio/)、パブリックドメイン。
+- [SQLite3](https://www.sqlite.org)、パブリックドメイン。
+
+ゲームパブリッシャーであり、商業利用のためにmGBAのライセンスを取得したい場合は、[licensing@mgba.io](mailto:licensing@mgba.io)までメールでお問い合わせください。
diff --git a/include/mgba/core/cpu.h b/include/mgba/core/cpu.h
index 71f3398c2..e538f4a59 100644
--- a/include/mgba/core/cpu.h
+++ b/include/mgba/core/cpu.h
@@ -20,6 +20,15 @@ enum mCPUComponentType {
 	CPU_COMPONENT_MAX
 };
 
+enum mMemoryAccessSource {
+	mACCESS_UNKNOWN = 0,
+	mACCESS_PROGRAM,
+	mACCESS_DMA,
+	mACCESS_SYSTEM,
+	mACCESS_DECOMPRESS,
+	mACCESS_COPY,
+};
+
 struct mCPUComponent {
 	uint32_t id;
 	void (*init)(void* cpu, struct mCPUComponent* component);
diff --git a/include/mgba/debugger/debugger.h b/include/mgba/debugger/debugger.h
index eb69faaf4..230a95564 100644
--- a/include/mgba/debugger/debugger.h
+++ b/include/mgba/debugger/debugger.h
@@ -111,6 +111,7 @@ struct mDebuggerEntryInfo {
 			uint32_t newValue;
 			enum mWatchpointType watchType;
 			enum mWatchpointType accessType;
+			enum mMemoryAccessSource accessSource;
 		} wp;
 
 		struct {
@@ -256,6 +257,7 @@ bool mDebuggerIsShutdown(const struct mDebugger*);
 
 struct mDebuggerModule* mDebuggerCreateModule(enum mDebuggerType type, struct mCore*);
 void mDebuggerModuleSetNeedsCallback(struct mDebuggerModule*);
+void mDebuggerModuleClearNeedsCallback(struct mDebuggerModule*);
 
 bool mDebuggerLookupIdentifier(struct mDebugger* debugger, const char* name, int32_t* value, int* segment);
 
diff --git a/include/mgba/gb/interface.h b/include/mgba/gb/interface.h
index de26ef731..e9bec3044 100644
--- a/include/mgba/gb/interface.h
+++ b/include/mgba/gb/interface.h
@@ -50,6 +50,7 @@ enum GBMemoryBankControllerType {
 	GB_UNL_GGB81 = 0x223,
 	GB_UNL_SACHEN_MMC1 = 0x230,
 	GB_UNL_SACHEN_MMC2 = 0x231,
+	GB_UNL_SINTAX = 0x240,
 };
 
 enum GBVideoLayer {
diff --git a/include/mgba/internal/arm/arm.h b/include/mgba/internal/arm/arm.h
index 5f25acf90..5ef2da965 100644
--- a/include/mgba/internal/arm/arm.h
+++ b/include/mgba/internal/arm/arm.h
@@ -132,6 +132,8 @@ struct ARMMemory {
 	uint32_t activeNonseqCycles16;
 	int32_t (*stall)(struct ARMCore*, int32_t wait);
 	void (*setActiveRegion)(struct ARMCore*, uint32_t address);
+
+	enum mMemoryAccessSource accessSource;
 };
 
 struct ARMCoprocessor {
diff --git a/include/mgba/internal/debugger/access-logger.h b/include/mgba/internal/debugger/access-logger.h
index 26186ac20..180168f3a 100644
--- a/include/mgba/internal/debugger/access-logger.h
+++ b/include/mgba/internal/debugger/access-logger.h
@@ -23,6 +23,7 @@ struct mDebuggerAccessLogRegion {
 	uint32_t segmentStart;
 	mDebuggerAccessLogFlags* block;
 	mDebuggerAccessLogFlagsEx* blockEx;
+	ssize_t watchpoint;
 };
 
 DECLARE_VECTOR(mDebuggerAccessLogRegionList, struct mDebuggerAccessLogRegion);
@@ -41,11 +42,16 @@ void mDebuggerAccessLoggerDeinit(struct mDebuggerAccessLogger*);
 bool mDebuggerAccessLoggerOpen(struct mDebuggerAccessLogger*, struct VFile*, int mode);
 bool mDebuggerAccessLoggerClose(struct mDebuggerAccessLogger*);
 
+void mDebuggerAccessLoggerStart(struct mDebuggerAccessLogger*);
+void mDebuggerAccessLoggerStop(struct mDebuggerAccessLogger*);
+
 int mDebuggerAccessLoggerWatchMemoryBlockId(struct mDebuggerAccessLogger*, size_t id, mDebuggerAccessLogRegionFlags);
 int mDebuggerAccessLoggerWatchMemoryBlockName(struct mDebuggerAccessLogger*, const char* internalName, mDebuggerAccessLogRegionFlags);
 
 bool mDebuggerAccessLoggerCreateShadowFile(struct mDebuggerAccessLogger*, int region, struct VFile*, uint8_t fill);
 
+struct mDebuggerAccessLogRegion* mDebuggerAccessLoggerGetRegion(struct mDebuggerAccessLogger*, uint32_t address, int segment, size_t* offset);
+
 CXX_GUARD_END
 
 #endif
diff --git a/include/mgba/internal/gb/memory.h b/include/mgba/internal/gb/memory.h
index e4b34088b..2d1278feb 100644
--- a/include/mgba/internal/gb/memory.h
+++ b/include/mgba/internal/gb/memory.h
@@ -261,6 +261,13 @@ struct GBSachenState {
 	uint8_t baseBank;
 };
 
+struct GBSintaxState {
+	uint8_t mode;
+	uint8_t xorValues[4];
+	uint8_t bankNo;
+	uint8_t romBankXor;
+};
+
 union GBMBCState {
 	struct GBMBC1State mbc1;
 	struct GBMBC6State mbc6;
@@ -274,6 +281,7 @@ union GBMBCState {
 	struct GBPKJDState pkjd;
 	struct GBBBDState bbd;
 	struct GBSachenState sachen;
+	struct GBSintaxState sintax;
 };
 
 struct mRotationSource;
diff --git a/include/mgba/internal/gb/serialize.h b/include/mgba/internal/gb/serialize.h
index e76d0accb..4963340e5 100644
--- a/include/mgba/internal/gb/serialize.h
+++ b/include/mgba/internal/gb/serialize.h
@@ -451,6 +451,12 @@ struct GBSerializedState {
 				uint8_t unmaskedBank;
 				uint8_t baseBank;
 			} sachen;
+			struct {
+				uint8_t mode;
+				uint8_t xorValues[4];
+				uint8_t bankNo;
+				uint8_t romBankXor;
+			} sintax;
 			struct {
 				uint8_t reserved[16];
 			} padding;
diff --git a/include/mgba/internal/gba/io.h b/include/mgba/internal/gba/io.h
index 197e19d62..2891c650c 100644
--- a/include/mgba/internal/gba/io.h
+++ b/include/mgba/internal/gba/io.h
@@ -17,7 +17,7 @@ CXX_GUARD_START
 enum GBAIORegisters {
 	// Video
 	GBA_REG_DISPCNT = 0x000,
-	GBA_REG_GREENSWP = 0x002,
+	GBA_REG_STEREOCNT = 0x002,
 	GBA_REG_DISPSTAT = 0x004,
 	GBA_REG_VCOUNT = 0x006,
 	GBA_REG_BG0CNT = 0x008,
diff --git a/include/mgba/internal/gba/renderers/proxy.h b/include/mgba/internal/gba/renderers/proxy.h
index 6cb58d092..3d70d5666 100644
--- a/include/mgba/internal/gba/renderers/proxy.h
+++ b/include/mgba/internal/gba/renderers/proxy.h
@@ -17,6 +17,7 @@ struct GBAVideoProxyRenderer {
 	struct GBAVideoRenderer d;
 	struct GBAVideoRenderer* backend;
 	struct mVideoLogger* logger;
+	int flushScanline;
 };
 
 void GBAVideoProxyRendererCreate(struct GBAVideoProxyRenderer* renderer, struct GBAVideoRenderer* backend, struct mVideoLogger* logger);
diff --git a/include/mgba/internal/gba/renderers/video-software.h b/include/mgba/internal/gba/renderers/video-software.h
index d9a012fa8..ce918775a 100644
--- a/include/mgba/internal/gba/renderers/video-software.h
+++ b/include/mgba/internal/gba/renderers/video-software.h
@@ -110,7 +110,7 @@ struct GBAVideoSoftwareRenderer {
 	uint16_t bldy;
 
 	GBAMosaicControl mosaic;
-	bool greenswap;
+	bool stereo;
 
 	struct WindowN {
 		struct GBAVideoWindowRegion h;
diff --git a/include/mgba/internal/gba/savedata.h b/include/mgba/internal/gba/savedata.h
index e69d07834..7fec242c4 100644
--- a/include/mgba/internal/gba/savedata.h
+++ b/include/mgba/internal/gba/savedata.h
@@ -45,9 +45,13 @@ enum FlashStateMachine {
 	FLASH_STATE_CONTINUE = 2,
 };
 
-enum FlashManufacturer {
-	FLASH_MFG_PANASONIC = 0x1B32,
-	FLASH_MFG_SANYO = 0x1362
+enum FlashId {
+	FLASH_ATMEL_AT29LV512 = 0x3D1F, // 512k
+	FLASH_MACRONIX_MX29L512 = 0x1CC2, // 512k, unused
+	FLASH_MACRONIX_MX29L010 = 0x09C2, // 1M
+	FLASH_PANASONIC_MN63F805MNP = 0x1B32, // 512k, unused
+	FLASH_SANYO_LE26FV10N1TS = 0x1362, // 1M
+	FLASH_SST_39LVF512 = 0xD4BF, // 512k
 };
 
 enum {
diff --git a/include/mgba/internal/gba/video.h b/include/mgba/internal/gba/video.h
index 5fc68eb59..5ee2bc36f 100644
--- a/include/mgba/internal/gba/video.h
+++ b/include/mgba/internal/gba/video.h
@@ -42,7 +42,8 @@ enum {
 enum GBAVideoObjMode {
 	OBJ_MODE_NORMAL = 0,
 	OBJ_MODE_SEMITRANSPARENT = 1,
-	OBJ_MODE_OBJWIN = 2
+	OBJ_MODE_OBJWIN = 2,
+	OBJ_MODE_STEREO = 3,
 };
 
 enum GBAVideoObjShape {
@@ -142,6 +143,7 @@ DECL_BITS(GBARegisterDISPSTAT, VcountSetting, 8, 8);
 DECL_BITFIELD(GBARegisterBGCNT, uint16_t);
 DECL_BITS(GBARegisterBGCNT, Priority, 0, 2);
 DECL_BITS(GBARegisterBGCNT, CharBase, 2, 2);
+DECL_BITS(GBARegisterBGCNT, StereoMode, 4, 2);
 DECL_BIT(GBARegisterBGCNT, Mosaic, 6);
 DECL_BIT(GBARegisterBGCNT, 256Color, 7);
 DECL_BITS(GBARegisterBGCNT, ScreenBase, 8, 5);
diff --git a/include/mgba/internal/sm83/sm83.h b/include/mgba/internal/sm83/sm83.h
index 86b4d374b..7f0fabd7a 100644
--- a/include/mgba/internal/sm83/sm83.h
+++ b/include/mgba/internal/sm83/sm83.h
@@ -61,6 +61,8 @@ struct SM83Memory {
 	uint16_t activeMask;
 	uint16_t activeRegionEnd;
 	void (*setActiveRegion)(struct SM83Core*, uint16_t address);
+
+	enum mMemoryAccessSource accessSource;
 };
 
 struct SM83InterruptHandler {
diff --git a/src/arm/debugger/memory-debugger.c b/src/arm/debugger/memory-debugger.c
index 7dce87795..b0b48423b 100644
--- a/src/arm/debugger/memory-debugger.c
+++ b/src/arm/debugger/memory-debugger.c
@@ -121,6 +121,7 @@ static void _checkWatchpoints(struct ARMDebugger* debugger, uint32_t address, en
 			info.type.wp.newValue = newValue;
 			info.type.wp.watchType = watchpoint->type;
 			info.type.wp.accessType = type;
+			info.type.wp.accessSource = debugger->cpu->memory.accessSource;
 			info.address = address;
 			info.segment = 0;
 			info.width = width;
diff --git a/src/arm/decoder-arm.c b/src/arm/decoder-arm.c
index 73d1717e9..074c04314 100644
--- a/src/arm/decoder-arm.c
+++ b/src/arm/decoder-arm.c
@@ -36,7 +36,7 @@
 #define ADDR_MODE_1_ASR ADDR_MODE_1_SHIFT(ASR)
 #define ADDR_MODE_1_ROR \
 	ADDR_MODE_1_SHIFT(ROR) \
-	if (!info->op3.shifterImm) { \
+	if ((info->operandFormat & ARM_OPERAND_SHIFT_IMMEDIATE_3) && !info->op3.shifterImm) { \
 		info->op3.shifterOp = ARM_SHIFT_RRX; \
 	}
 
diff --git a/src/core/thread.c b/src/core/thread.c
index 988ce0848..92d134120 100644
--- a/src/core/thread.c
+++ b/src/core/thread.c
@@ -507,6 +507,10 @@ void mCoreThreadClearCrashed(struct mCoreThread* threadContext) {
 
 void mCoreThreadEnd(struct mCoreThread* threadContext) {
 	MutexLock(&threadContext->impl->stateMutex);
+	if (threadContext->impl->state == mTHREAD_SHUTDOWN) {
+		MutexUnlock(&threadContext->impl->stateMutex);
+		return;
+	}
 	_waitOnInterrupt(threadContext->impl);
 	threadContext->impl->state = mTHREAD_EXITING;
 	ConditionWake(&threadContext->impl->stateOnThreadCond);
diff --git a/src/debugger/access-logger.c b/src/debugger/access-logger.c
index 41c780c4e..b9e8222d1 100644
--- a/src/debugger/access-logger.c
+++ b/src/debugger/access-logger.c
@@ -62,74 +62,102 @@ static void _mDebuggerAccessLoggerEntered(struct mDebuggerModule* debugger, enum
 		break;
 	}
 
-	size_t i;
-	for (i = 0; i < mDebuggerAccessLogRegionListSize(&logger->regions); ++i) {
-		struct mDebuggerAccessLogRegion* region = mDebuggerAccessLogRegionListGetPointer(&logger->regions, i);
-		if (info->address < region->start || info->address >= region->end) {
-			continue;
-		}
-		size_t offset = info->address - region->start;
-		if (info->segment > 0) {
-			uint32_t segmentSize = region->end - region->segmentStart;
-			offset %= segmentSize;
-			offset += segmentSize * info->segment;
-		}
+	size_t offset;
+	struct mDebuggerAccessLogRegion* region = mDebuggerAccessLoggerGetRegion(logger, info->address, info->segment, &offset);
+	if (!region) {
+		return;
+	}
+	offset &= -info->width;
 
-		if (offset >= region->size) {
-			continue;
-		}
-
-		offset &= -info->width;
-
-		int j;
-		switch (reason) {
-		case DEBUGGER_ENTER_WATCHPOINT:
-			for (j = 0; j < info->width; ++j) {
-				if (info->type.wp.accessType & WATCHPOINT_WRITE) {
-					region->block[offset + j] = mDebuggerAccessLogFlagsFillWrite(region->block[offset + j]);
-				}
-				if (info->type.wp.accessType & WATCHPOINT_READ) {
-					region->block[offset + j] = mDebuggerAccessLogFlagsFillRead(region->block[offset + j]);
-				}
-			}
-			switch (info->width) {
-			case 1:
-				region->block[offset] = mDebuggerAccessLogFlagsFillAccess8(region->block[offset]);
-				break;
-			case 2:
-				region->block[offset] = mDebuggerAccessLogFlagsFillAccess16(region->block[offset]);
-				region->block[offset + 1] = mDebuggerAccessLogFlagsFillAccess16(region->block[offset + 1]);
-				break;
-			case 4:
-				region->block[offset] = mDebuggerAccessLogFlagsFillAccess32(region->block[offset]);
-				region->block[offset + 1] = mDebuggerAccessLogFlagsFillAccess32(region->block[offset + 1]);
-				region->block[offset + 2] = mDebuggerAccessLogFlagsFillAccess32(region->block[offset + 2]);
-				region->block[offset + 3] = mDebuggerAccessLogFlagsFillAccess32(region->block[offset + 3]);
-				break;
-			case 8:
-				region->block[offset] = mDebuggerAccessLogFlagsFillAccess64(region->block[offset]);
-				region->block[offset + 1] = mDebuggerAccessLogFlagsFillAccess64(region->block[offset + 1]);
-				region->block[offset + 2] = mDebuggerAccessLogFlagsFillAccess64(region->block[offset + 2]);
-				region->block[offset + 3] = mDebuggerAccessLogFlagsFillAccess64(region->block[offset + 3]);
-				region->block[offset + 4] = mDebuggerAccessLogFlagsFillAccess64(region->block[offset + 4]);
-				region->block[offset + 5] = mDebuggerAccessLogFlagsFillAccess64(region->block[offset + 5]);
-				region->block[offset + 6] = mDebuggerAccessLogFlagsFillAccess64(region->block[offset + 6]);
-				region->block[offset + 7] = mDebuggerAccessLogFlagsFillAccess64(region->block[offset + 7]);
-				break;
-			}
+	mDebuggerAccessLogFlags flags = 0;
+	mDebuggerAccessLogFlagsEx flagsEx = 0;
+	switch (reason) {
+	case DEBUGGER_ENTER_WATCHPOINT:
+		switch (info->type.wp.accessSource) {
+		case mACCESS_PROGRAM:
+			flagsEx = mDebuggerAccessLogFlagsExFillAccessProgram(flagsEx);
 			break;
-		case DEBUGGER_ENTER_ILLEGAL_OP:
-			region->block[offset] = mDebuggerAccessLogFlagsFillExecute(region->block[offset]);
+		case mACCESS_DMA:
+			flagsEx = mDebuggerAccessLogFlagsExFillAccessDMA(flagsEx);
+			break;
+		case mACCESS_SYSTEM:
+			flagsEx = mDebuggerAccessLogFlagsExFillAccessSystem(flagsEx);
+			break;
+		case mACCESS_DECOMPRESS:
+			flagsEx = mDebuggerAccessLogFlagsExFillAccessDecompress(flagsEx);
+			break;
+		case mACCESS_COPY:
+			flagsEx = mDebuggerAccessLogFlagsExFillAccessCopy(flagsEx);
+			break;
+		case mACCESS_UNKNOWN:
+			break;
+		}
+		if (info->type.wp.accessType & WATCHPOINT_WRITE) {
+			flags = mDebuggerAccessLogFlagsFillWrite(flags);
+		}
+		if (info->type.wp.accessType & WATCHPOINT_READ) {
+			flags = mDebuggerAccessLogFlagsFillRead(flags);
+		}
+		switch (info->width) {
+		case 1:
+			region->block[offset] = flags | mDebuggerAccessLogFlagsFillAccess8(region->block[offset]);
 			if (region->blockEx) {
-				uint16_t ex;
-				LOAD_16LE(ex, 0, &region->blockEx[offset]);
-				ex = mDebuggerAccessLogFlagsExFillErrorIllegalOpcode(ex);
-				STORE_16LE(ex, 0, &region->blockEx[offset]);
+				region->blockEx[offset] |= flagsEx;
 			}
 			break;
-		default:
+		case 2:
+			region->block[offset] = flags | mDebuggerAccessLogFlagsFillAccess16(region->block[offset]);
+			region->block[offset + 1] = flags | mDebuggerAccessLogFlagsFillAccess16(region->block[offset + 1]);
+			if (region->blockEx) {
+				region->blockEx[offset] |= flagsEx;
+				region->blockEx[offset + 1] |= flagsEx;
+			}
+			break;
+		case 4:
+			region->block[offset] = flags | mDebuggerAccessLogFlagsFillAccess32(region->block[offset]);
+			region->block[offset + 1] = flags | mDebuggerAccessLogFlagsFillAccess32(region->block[offset + 1]);
+			region->block[offset + 2] = flags | mDebuggerAccessLogFlagsFillAccess32(region->block[offset + 2]);
+			region->block[offset + 3] = flags | mDebuggerAccessLogFlagsFillAccess32(region->block[offset + 3]);
+			if (region->blockEx) {
+				region->blockEx[offset] |= flagsEx;
+				region->blockEx[offset + 1] |= flagsEx;
+				region->blockEx[offset + 2] |= flagsEx;
+				region->blockEx[offset + 3] |= flagsEx;
+			}
+			break;
+		case 8:
+			region->block[offset] = flags | mDebuggerAccessLogFlagsFillAccess64(region->block[offset]);
+			region->block[offset + 1] = flags | mDebuggerAccessLogFlagsFillAccess64(region->block[offset + 1]);
+			region->block[offset + 2] = flags | mDebuggerAccessLogFlagsFillAccess64(region->block[offset + 2]);
+			region->block[offset + 3] = flags | mDebuggerAccessLogFlagsFillAccess64(region->block[offset + 3]);
+			region->block[offset + 4] = flags | mDebuggerAccessLogFlagsFillAccess64(region->block[offset + 4]);
+			region->block[offset + 5] = flags | mDebuggerAccessLogFlagsFillAccess64(region->block[offset + 5]);
+			region->block[offset + 6] = flags | mDebuggerAccessLogFlagsFillAccess64(region->block[offset + 6]);
+			region->block[offset + 7] = flags | mDebuggerAccessLogFlagsFillAccess64(region->block[offset + 7]);
+			if (region->blockEx) {
+				region->blockEx[offset] |= flagsEx;
+				region->blockEx[offset + 1] |= flagsEx;
+				region->blockEx[offset + 2] |= flagsEx;
+				region->blockEx[offset + 3] |= flagsEx;
+				region->blockEx[offset + 4] |= flagsEx;
+				region->blockEx[offset + 5] |= flagsEx;
+				region->blockEx[offset + 6] |= flagsEx;
+				region->blockEx[offset + 7] |= flagsEx;
+			}
 			break;
 		}
+		break;
+	case DEBUGGER_ENTER_ILLEGAL_OP:
+		region->block[offset] = mDebuggerAccessLogFlagsFillExecute(region->block[offset]);
+		if (region->blockEx) {
+			uint16_t ex;
+			LOAD_16LE(ex, 0, &region->blockEx[offset]);
+			ex = mDebuggerAccessLogFlagsExFillErrorIllegalOpcode(ex);
+			STORE_16LE(ex, 0, &region->blockEx[offset]);
+		}
+		break;
+	default:
+		break;
 	}
 }
 
@@ -139,34 +167,22 @@ static void _mDebuggerAccessLoggerCallback(struct mDebuggerModule* debugger) {
 	struct mDebuggerInstructionInfo info;
 	logger->d.p->platform->nextInstructionInfo(logger->d.p->platform, &info);
 
+	size_t offset;
+	struct mDebuggerAccessLogRegion* region = mDebuggerAccessLoggerGetRegion(logger, info.address, info.segment, &offset);
+	if (!region) {
+		return;
+	}
+
 	size_t i;
-	for (i = 0; i < mDebuggerAccessLogRegionListSize(&logger->regions); ++i) {
-		struct mDebuggerAccessLogRegion* region = mDebuggerAccessLogRegionListGetPointer(&logger->regions, i);
-		if (info.address < region->start || info.address >= region->end) {
-			continue;
-		}
-		size_t offset = info.address - region->start;
-		if (info.segment > 0) {
-			uint32_t segmentSize = region->end - region->segmentStart;
-			offset %= segmentSize;
-			offset += segmentSize * info.segment;
-		}
+	for (i = 0; i < info.width; ++i) {
+		uint16_t ex = 0;
+		region->block[offset + i] = mDebuggerAccessLogFlagsFillExecute(region->block[offset + i]);
+		region->block[offset + i] |= info.flags[i];
 
-		if (offset >= region->size) {
-			continue;
-		}
-
-		size_t j;
-		for (j = 0; j < info.width; ++j) {
-			uint16_t ex = 0;
-			region->block[offset + j] = mDebuggerAccessLogFlagsFillExecute(region->block[offset + j]);
-			region->block[offset + j] |= info.flags[j];
-
-			if (region->blockEx) {
-				LOAD_16LE(ex, 0, &region->blockEx[offset + j]);
-				ex |= info.flagsEx[j];
-				STORE_16LE(ex, 0, &region->blockEx[offset + j]);
-			}
+		if (region->blockEx) {
+			LOAD_16LE(ex, 0, &region->blockEx[offset + i]);
+			ex |= info.flagsEx[i];
+			STORE_16LE(ex, 0, &region->blockEx[offset + i]);
 		}
 	}
 }
@@ -240,13 +256,18 @@ static bool _setupRegion(struct mDebuggerAccessLogger* logger, struct mDebuggerA
 		return false;
 	}
 
-	struct mWatchpoint wp = {
-		.segment = -1,
-		.minAddress = region->start,
-		.maxAddress = region->end,
-		.type = WATCHPOINT_RW,
-	};
-	logger->d.p->platform->setWatchpoint(logger->d.p->platform, &logger->d, &wp);
+	if (region->watchpoint < 0) {
+		struct mWatchpoint wp = {
+			.segment = -1,
+			.minAddress = region->start,
+			.maxAddress = region->end,
+			.type = WATCHPOINT_RW,
+		};
+		region->watchpoint = logger->d.p->platform->setWatchpoint(logger->d.p->platform, &logger->d, &wp);
+	}
+	if (region->watchpoint < 0) {
+		return false;
+	}
 	mDebuggerModuleSetNeedsCallback(&logger->d);
 	return true;
 }
@@ -290,7 +311,7 @@ static bool mDebuggerAccessLoggerLoad(struct mDebuggerAccessLogger* logger) {
 		LOAD_32LE(region->end, 0, &info->end);
 		LOAD_32LE(region->size, 0, &info->size);
 		LOAD_32LE(region->segmentStart, 0, &info->segmentStart);
-		if (!_setupRegion(logger, region, info)) {
+		if (!_mapRegion(logger, region, info)) {
 			mDebuggerAccessLogRegionListClear(&logger->regions);
 			return false;
 		}
@@ -335,6 +356,30 @@ bool mDebuggerAccessLoggerOpen(struct mDebuggerAccessLogger* logger, struct VFil
 	return loaded;
 }
 
+void mDebuggerAccessLoggerStart(struct mDebuggerAccessLogger* logger) {
+	size_t i;
+	for (i = 0; i < logger->mapped->header.nRegions; ++i) {
+		struct mDebuggerAccessLogRegionInfo* info = &logger->mapped->regionInfo[i];
+		struct mDebuggerAccessLogRegion* region = mDebuggerAccessLogRegionListGetPointer(&logger->regions, i);
+		if (!_setupRegion(logger, region, info)) {
+			return;
+		}
+	}
+}
+
+void mDebuggerAccessLoggerStop(struct mDebuggerAccessLogger* logger) {
+	size_t i;
+	for (i = 0; i < logger->mapped->header.nRegions; ++i) {
+		struct mDebuggerAccessLogRegion* region = mDebuggerAccessLogRegionListGetPointer(&logger->regions, i);
+		if (region->watchpoint < 0) {
+			continue;
+		}
+		logger->d.p->platform->clearBreakpoint(logger->d.p->platform, region->watchpoint);
+		region->watchpoint = -1;
+	}
+	mDebuggerModuleClearNeedsCallback(&logger->d);
+}
+
 static int _mDebuggerAccessLoggerWatchMemoryBlock(struct mDebuggerAccessLogger* logger, const struct mCoreMemoryBlock* block, mDebuggerAccessLogRegionFlags flags) {
 	if (mDebuggerAccessLogRegionListSize(&logger->regions) >= logger->mapped->header.regionCapacity) {
 		return -1;
@@ -406,6 +451,7 @@ static int _mDebuggerAccessLoggerWatchMemoryBlock(struct mDebuggerAccessLogger*
 	region->size = block->size;
 	region->segmentStart = block->segmentStart;
 	region->block = (mDebuggerAccessLogFlags*) ((uintptr_t) logger->backing + fileEnd);
+	region->watchpoint = -1;
 
 	struct mDebuggerAccessLogRegionInfo* info = &logger->mapped->regionInfo[id];
 	STORE_32LE(region->start, 0, &info->start);
@@ -434,6 +480,7 @@ bool mDebuggerAccessLoggerClose(struct mDebuggerAccessLogger* logger) {
 	if (!logger->backing) {
 		return true;
 	}
+	mDebuggerAccessLoggerStop(logger);
 	mDebuggerAccessLogRegionListClear(&logger->regions);
 	logger->backing->unmap(logger->backing, logger->mapped, logger->backing->size(logger->backing));
 	logger->mapped = NULL;
@@ -518,3 +565,28 @@ bool mDebuggerAccessLoggerCreateShadowFile(struct mDebuggerAccessLogger* logger,
 	}
 	return true;
 }
+
+struct mDebuggerAccessLogRegion* mDebuggerAccessLoggerGetRegion(struct mDebuggerAccessLogger* logger, uint32_t address, int segment, size_t* offsetOut) {
+	size_t i;
+	for (i = 0; i < mDebuggerAccessLogRegionListSize(&logger->regions); ++i) {
+		struct mDebuggerAccessLogRegion* region = mDebuggerAccessLogRegionListGetPointer(&logger->regions, i);
+		if (address < region->start || address >= region->end) {
+			continue;
+		}
+		size_t offset = address - region->start;
+		if (segment > 0) {
+			uint32_t segmentSize = region->end - region->segmentStart;
+			offset %= segmentSize;
+			offset += segmentSize * segment;
+		}
+
+		if (offset >= region->size) {
+			continue;
+		}
+		if (offsetOut) {
+			*offsetOut = offset;
+		}
+		return region;
+	}
+	return NULL;
+}
diff --git a/src/debugger/debugger.c b/src/debugger/debugger.c
index a03f106d9..995a28099 100644
--- a/src/debugger/debugger.c
+++ b/src/debugger/debugger.c
@@ -320,3 +320,8 @@ void mDebuggerModuleSetNeedsCallback(struct mDebuggerModule* debugger) {
 	debugger->needsCallback = true;
 	mDebuggerUpdatePaused(debugger->p);
 }
+
+void mDebuggerModuleClearNeedsCallback(struct mDebuggerModule* debugger) {
+	debugger->needsCallback = false;
+	mDebuggerUpdatePaused(debugger->p);
+}
diff --git a/src/feature/gui/gui-config.c b/src/feature/gui/gui-config.c
index 6214abc15..228a3f647 100644
--- a/src/feature/gui/gui-config.c
+++ b/src/feature/gui/gui-config.c
@@ -109,7 +109,7 @@ void mGUIShowConfig(struct mGUIRunner* runner, struct GUIMenuItem* extra, size_t
 		.nStates = 2
 	};
 	*GUIMenuItemListAppend(&menu.items) = (struct GUIMenuItem) {
-		.title = "Fast forward mute",
+		.title = "Mute while fast forwarding",
 		.data = GUI_V_S("fastForwardMute"),
 		.submenu = 0,
 		.state = false,
@@ -341,7 +341,7 @@ void mGUIShowConfig(struct mGUIRunner* runner, struct GUIMenuItem* extra, size_t
 						mCoreConfigSetUIntValue(&runner->config, item->data.v.s, v->v.u);
 						break;
 					case GUI_VARIANT_INT:
-						mCoreConfigSetUIntValue(&runner->config, item->data.v.s, v->v.i);
+						mCoreConfigSetIntValue(&runner->config, item->data.v.s, v->v.i);
 						break;
 					case GUI_VARIANT_FLOAT:
 						mCoreConfigSetFloatValue(&runner->config, item->data.v.s, v->v.f);
diff --git a/src/feature/gui/gui-runner.c b/src/feature/gui/gui-runner.c
index 76e15cdad..407a59781 100644
--- a/src/feature/gui/gui-runner.c
+++ b/src/feature/gui/gui-runner.c
@@ -702,6 +702,7 @@ void mGUIRun(struct mGUIRunner* runner, const char* path) {
 		mCoreConfigGetIntValue(&runner->config, "showOSD", &showOSD);
 		mCoreConfigGetIntValue(&runner->config, "mute", &mute);
 		mCoreConfigGetIntValue(&runner->config, "fastForwardMute", &fastForwardMute);
+		runner->core->reloadConfigOption(runner->core, "threadedVideo.flushScanline", &runner->config);
 #ifdef M_CORE_GB
 		if (runner->core->platform(runner->core) == mPLATFORM_GB) {
 			runner->core->reloadConfigOption(runner->core, "gb.pal", &runner->config);
diff --git a/src/gb/mbc.c b/src/gb/mbc.c
index 397b50088..2a19575a8 100644
--- a/src/gb/mbc.c
+++ b/src/gb/mbc.c
@@ -113,7 +113,7 @@ static struct {
 	{"M161", GB_MBC_AUTODETECT}, // TODO
 	{"BBD", GB_UNL_BBD},
 	{"HITK", GB_UNL_HITEK},
-	{"SNTX", GB_MBC_AUTODETECT}, // TODO
+	{"SNTX", GB_UNL_SINTAX},
 	{"NTO1", GB_UNL_NT_OLD_1},
 	{"NTO2", GB_UNL_NT_OLD_2},
 	{"NTN", GB_UNL_NT_NEW},
@@ -128,6 +128,8 @@ static struct {
 	{"NGHK", GB_MBC_AUTODETECT}, // TODO
 	{"GB81", GB_UNL_GGB81},
 	{"TPP1", GB_MBC_AUTODETECT}, // TODO
+	{"VF01", GB_MBC_AUTODETECT}, // TODO
+	{"SKL8", GB_MBC_AUTODETECT}, // TODO
 
 	{NULL, GB_MBC_AUTODETECT},
 };
@@ -223,6 +225,12 @@ static enum GBMemoryBankControllerType _detectUnlMBC(const uint8_t* mem, size_t
 			return GB_UNL_LI_CHENG;
 		}
 		break;
+	case 0x6c1dcf2d:
+	case 0x99e3449d:
+		if (mem[0x7FFF] != 0x01) { // Make sure we're not using a "fixed" version
+			return GB_UNL_SINTAX;
+		}
+		break;
 	}
 
 	if (mem[0x104] == 0xCE && mem[0x144] == 0xED && mem[0x114] == 0x66) {
@@ -504,6 +512,14 @@ void GBMBCInit(struct GB* gb) {
 			gb->memory.sramAccess = true;
 		}
 		break;
+	case GB_UNL_SINTAX:
+		gb->memory.mbcWrite = _GBSintax;
+		gb->memory.mbcRead = _GBSintaxRead;
+		gb->memory.mbcReadBank1 = true;
+		if (gb->sramSize) {
+			gb->memory.sramAccess = true;
+		}
+		break;
 	}
 
 	gb->memory.currentBank = 1;
@@ -559,6 +575,9 @@ void GBMBCReset(struct GB* gb) {
 		GBMBCSwitchBank0(gb, gb->memory.romSize / GB_SIZE_CART_BANK0 - 2);
 		GBMBCSwitchBank(gb, gb->memory.romSize / GB_SIZE_CART_BANK0 - 1);
 		break;
+	case GB_UNL_SINTAX:
+		gb->memory.mbcState.sintax.mode = 0xF;
+		break;
 	default:
 		break;
 	}
diff --git a/src/gb/mbc/mbc-private.h b/src/gb/mbc/mbc-private.h
index 4a07ab716..b6f7671a5 100644
--- a/src/gb/mbc/mbc-private.h
+++ b/src/gb/mbc/mbc-private.h
@@ -38,6 +38,7 @@ void _GBHitek(struct GB* gb, uint16_t address, uint8_t value);
 void _GBLiCheng(struct GB* gb, uint16_t address, uint8_t value);
 void _GBGGB81(struct GB* gb, uint16_t address, uint8_t value);
 void _GBSachen(struct GB* gb, uint16_t address, uint8_t value);
+void _GBSintax(struct GB* gb, uint16_t address, uint8_t value);
 
 uint8_t _GBMBC2Read(struct GBMemory*, uint16_t address);
 uint8_t _GBMBC6Read(struct GBMemory*, uint16_t address);
@@ -54,6 +55,7 @@ uint8_t _GBHitekRead(struct GBMemory*, uint16_t address);
 uint8_t _GBGGB81Read(struct GBMemory*, uint16_t address);
 uint8_t _GBSachenMMC1Read(struct GBMemory*, uint16_t address);
 uint8_t _GBSachenMMC2Read(struct GBMemory*, uint16_t address);
+uint8_t _GBSintaxRead(struct GBMemory*, uint16_t address);
 
 void _GBMBCLatchRTC(struct mRTCSource* rtc, uint8_t* rtcRegs, time_t* rtcLastLatch);
 void _GBMBCAppendSaveSuffix(struct GB* gb, const void* buffer, size_t size);
diff --git a/src/gb/mbc/unlicensed.c b/src/gb/mbc/unlicensed.c
index 844c0d0cd..00b0ef303 100644
--- a/src/gb/mbc/unlicensed.c
+++ b/src/gb/mbc/unlicensed.c
@@ -500,3 +500,102 @@ uint8_t _GBSachenMMC2Read(struct GBMemory* memory, uint16_t address) {
 		return 0xFF;
 	}
 }
+
+static const uint8_t _sintaxReordering[16][8] = {
+	{ 2, 1, 4, 3, 6, 5, 0, 7 },
+	{ 3, 2, 5, 4, 7, 6, 1, 0 },
+	{ 0, 1, 2, 3, 4, 5, 6, 7 }, // unknown
+	{ 0, 1, 2, 3, 4, 5, 6, 7 }, // unknown
+	{ 0, 1, 2, 3, 4, 5, 6, 7 }, // unknown
+	{ 4, 5, 2, 3, 0, 1, 6, 7 },
+	{ 0, 1, 2, 3, 4, 5, 6, 7 }, // unknown
+	{ 6, 7, 4, 5, 1, 3, 0, 2 },
+	{ 0, 1, 2, 3, 4, 5, 6, 7 }, // unknown
+	{ 7, 6, 1, 0, 3, 2, 5, 4 },
+	{ 0, 1, 2, 3, 4, 5, 6, 7 }, // unknown
+	{ 5, 4, 7, 6, 1, 0, 3, 2 },
+	{ 0, 1, 2, 3, 4, 5, 6, 7 }, // unknown
+	{ 2, 3, 4, 5, 6, 7, 0, 1 },
+	{ 0, 1, 2, 3, 4, 5, 6, 7 }, // unknown
+	{ 0, 1, 2, 3, 4, 5, 6, 7 },
+};
+
+void  _GBSintax(struct GB* gb, uint16_t address, uint8_t value) {
+	struct GBSintaxState* state = &gb->memory.mbcState.sintax;
+
+	if (address >= 0x2000 && address < 0x3000) {
+		state->bankNo = value;
+		value = _reorderBits(value, _sintaxReordering[state->mode]);
+		state->romBankXor = state->xorValues[state->bankNo & 0x3];
+	}
+
+	if ((address & 0xF0F0) == 0x5010) {
+		// contrary to previous belief it IS possible to change the mode after setting it initially
+		// The reason Metal Max was breaking is because it only recognises writes to 5x1x
+		// and that game writes to a bunch of other 5xxx addresses before battles
+		state->mode = value & 0xF;
+
+		mLOG(GB_MBC, DEBUG, "Sintax bank reorder mode: %X", state->mode);
+
+		switch (state->mode) {
+		// Supported modes
+		case 0x00: // Lion King, Golden Sun
+		case 0x01: // Langrisser
+		case 0x05: // Maple Story, Pokemon Platinum
+		case 0x07: // Bynasty Warriors 5
+		case 0x09: // ???
+		case 0x0B: // Shaolin Legend
+		case 0x0D: // Older games
+		case 0x0F: // Default mode, no reordering
+			break;
+		default:
+			mLOG(GB_MBC, DEBUG, "Bank reorder mode unsupported - %X", state->mode);
+			break;
+		}
+
+		_GBSintax(gb, 0x2000, state->bankNo); // fake a bank switch to select the correct bank
+		return;
+	}
+
+	if (address >= 0x7000 && address < 0x8000) {
+		int xorNo = (address & 0x00F0) >> 4;
+		switch (xorNo) {
+		case 2:
+			state->xorValues[0] = value;
+			mLOG(GB_MBC, DEBUG, "Sintax XOR 0: %X", value);
+			break;
+		case 3:
+			state->xorValues[1] = value;
+			mLOG(GB_MBC, DEBUG, "Sintax XOR 1: %X", value);
+			break;
+		case 4:
+			state->xorValues[2] = value;
+			mLOG(GB_MBC, DEBUG, "Sintax XOR 2: %X", value);
+			break;
+		case 5:
+			state->xorValues[3] = value;
+			mLOG(GB_MBC, DEBUG, "Sintax XOR 3: %X", value);
+			break;
+		}
+
+		// xor is applied immediately to the current bank
+		state->romBankXor = state->xorValues[state->bankNo & 0x3];
+	}
+	_GBMBC5(gb, address, value);
+}
+
+uint8_t _GBSintaxRead(struct GBMemory* memory, uint16_t address) {
+	struct GBSintaxState* state = &memory->mbcState.sintax;
+	switch (address >> 13) {
+	case 0x2:
+	case 0x3:
+		return memory->romBank[address & (GB_SIZE_CART_BANK0 - 1)] ^ state->romBankXor;
+	case 0x5:
+		if (memory->sramAccess && memory->sram) {
+			return memory->sramBank[address & (GB_SIZE_EXTERNAL_RAM - 1)];
+		}
+		return 0xFF;
+	default:
+		return 0xFF;
+	}
+}
diff --git a/src/gb/memory.c b/src/gb/memory.c
index dccd91e08..05d4a9669 100644
--- a/src/gb/memory.c
+++ b/src/gb/memory.c
@@ -148,6 +148,7 @@ void GBMemoryInit(struct GB* gb) {
 	cpu->memory.store8 = GBStore8;
 	cpu->memory.currentSegment = GBCurrentSegment;
 	cpu->memory.setActiveRegion = GBSetActiveRegion;
+	cpu->memory.accessSource = mACCESS_UNKNOWN;
 
 	gb->memory.wram = 0;
 	gb->memory.wramBank = 0;
@@ -205,6 +206,7 @@ void GBMemoryReset(struct GB* gb) {
 	gb->memory.hdmaDest = 0;
 	gb->memory.isHdma = false;
 
+	gb->cpu->memory.accessSource = mACCESS_UNKNOWN;
 
 	gb->memory.dmaEvent.context = gb;
 	gb->memory.dmaEvent.name = "GB DMA";
@@ -576,10 +578,13 @@ void _GBMemoryDMAService(struct mTiming* timing, void* context, uint32_t cyclesL
 	struct GB* gb = context;
 	int dmaRemaining = gb->memory.dmaRemaining;
 	gb->memory.dmaRemaining = 0;
+	enum mMemoryAccessSource oldAccess = gb->cpu->memory.accessSource;
+	gb->cpu->memory.accessSource = mACCESS_DMA;
 	uint8_t b = GBLoad8(gb->cpu, gb->memory.dmaSource);
 	// TODO: Can DMA write OAM during modes 2-3?
 	gb->video.oam.raw[gb->memory.dmaDest] = b;
 	gb->video.renderer->writeOAM(gb->video.renderer, gb->memory.dmaDest);
+	gb->cpu->memory.accessSource = oldAccess;
 	++gb->memory.dmaSource;
 	++gb->memory.dmaDest;
 	gb->memory.dmaRemaining = dmaRemaining - 1;
@@ -591,8 +596,11 @@ void _GBMemoryDMAService(struct mTiming* timing, void* context, uint32_t cyclesL
 void _GBMemoryHDMAService(struct mTiming* timing, void* context, uint32_t cyclesLate) {
 	struct GB* gb = context;
 	gb->cpuBlocked = true;
+	enum mMemoryAccessSource oldAccess = gb->cpu->memory.accessSource;
+	gb->cpu->memory.accessSource = mACCESS_DMA;
 	uint8_t b = gb->cpu->memory.load8(gb->cpu, gb->memory.hdmaSource);
 	gb->cpu->memory.store8(gb->cpu, gb->memory.hdmaDest, b);
+	gb->cpu->memory.accessSource = oldAccess;
 	++gb->memory.hdmaSource;
 	++gb->memory.hdmaDest;
 	--gb->memory.hdmaRemaining;
@@ -832,6 +840,12 @@ void GBMemorySerialize(const struct GB* gb, struct GBSerializedState* state) {
 		state->memory.sachen.unmaskedBank = memory->mbcState.sachen.unmaskedBank;
 		state->memory.sachen.baseBank = memory->mbcState.sachen.baseBank;
 		break;
+	case GB_UNL_SINTAX:
+		state->memory.sintax.mode = memory->mbcState.sintax.mode;
+		memcpy(state->memory.sintax.xorValues, memory->mbcState.sintax.xorValues, sizeof(state->memory.sintax.xorValues));
+		state->memory.sintax.bankNo = memory->mbcState.sintax.bankNo;
+		state->memory.sintax.romBankXor = memory->mbcState.sintax.romBankXor;
+		break;
 	default:
 		break;
 	}
@@ -1000,6 +1014,12 @@ void GBMemoryDeserialize(struct GB* gb, const struct GBSerializedState* state) {
 		memory->mbcState.sachen.baseBank = state->memory.sachen.baseBank;
 		GBMBCSwitchBank0(gb, memory->mbcState.sachen.baseBank & memory->mbcState.sachen.mask);
 		break;
+	case GB_UNL_SINTAX:
+		memory->mbcState.sintax.mode = state->memory.sintax.mode;
+		memcpy(memory->mbcState.sintax.xorValues, state->memory.sintax.xorValues, sizeof(memory->mbcState.sintax.xorValues));
+		memory->mbcState.sintax.bankNo = state->memory.sintax.bankNo;
+		memory->mbcState.sintax.romBankXor = state->memory.sintax.romBankXor;
+		break;
 	default:
 		break;
 	}
diff --git a/src/gba/bios.c b/src/gba/bios.c
index 1ff5004d3..c7b963a40 100644
--- a/src/gba/bios.c
+++ b/src/gba/bios.c
@@ -174,6 +174,8 @@ static void _BgAffineSet(struct GBA* gba) {
 	int destination = cpu->gprs[1];
 	float a, b, c, d;
 	float rx, ry;
+	enum mMemoryAccessSource oldAccess = cpu->memory.accessSource;
+	cpu->memory.accessSource = mACCESS_SYSTEM;
 	while (i--) {
 		// [ sx   0  0 ]   [ cos(theta)  -sin(theta)  0 ]   [ 1  0  cx - ox ]   [ A B rx ]
 		// [  0  sy  0 ] * [ sin(theta)   cos(theta)  0 ] * [ 0  1  cy - oy ] = [ C D ry ]
@@ -205,6 +207,7 @@ static void _BgAffineSet(struct GBA* gba) {
 		cpu->memory.store32(cpu, destination + 12, ry * 256, 0);
 		destination += 16;
 	}
+	cpu->memory.accessSource = oldAccess;
 }
 
 static void _ObjAffineSet(struct GBA* gba) {
@@ -216,6 +219,8 @@ static void _ObjAffineSet(struct GBA* gba) {
 	int destination = cpu->gprs[1];
 	int diff = cpu->gprs[3];
 	float a, b, c, d;
+	enum mMemoryAccessSource oldAccess = cpu->memory.accessSource;
+	cpu->memory.accessSource = mACCESS_SYSTEM;
 	while (i--) {
 		// [ sx   0 ]   [ cos(theta)  -sin(theta) ]   [ A B ]
 		// [  0  sy ] * [ sin(theta)   cos(theta) ] = [ C D ]
@@ -237,6 +242,7 @@ static void _ObjAffineSet(struct GBA* gba) {
 		cpu->memory.store16(cpu, destination + diff * 3, d * 256, 0);
 		destination += diff * 4;
 	}
+	cpu->memory.accessSource = oldAccess;
 }
 
 static void _MidiKey2Freq(struct GBA* gba) {
@@ -244,7 +250,10 @@ static void _MidiKey2Freq(struct GBA* gba) {
 
 	int oldRegion = gba->memory.activeRegion;
 	gba->memory.activeRegion = GBA_REGION_BIOS;
+	enum mMemoryAccessSource oldAccess = cpu->memory.accessSource;
+	cpu->memory.accessSource = mACCESS_SYSTEM;
 	uint32_t key = cpu->memory.load32(cpu, cpu->gprs[0] + 4, 0);
+	cpu->memory.accessSource = oldAccess;
 	gba->memory.activeRegion = oldRegion;
 
 	cpu->gprs[0] = key / exp2f((180.f - cpu->gprs[1] - cpu->gprs[2] / 256.f) / 12.f);
@@ -624,6 +633,8 @@ static void _unLz77(struct GBA* gba, int width) {
 	uint32_t source = cpu->gprs[0];
 	uint32_t dest = cpu->gprs[1];
 	int cycles = 20;
+	enum mMemoryAccessSource oldAccess = cpu->memory.accessSource;
+	cpu->memory.accessSource = mACCESS_DECOMPRESS;
 	int remaining = (cpu->memory.load32(cpu, source, &cycles) & 0xFFFFFF00) >> 8;
 	// We assume the signature byte (0x10) is correct
 	int blockheader = 0; // Some compilers warn if this isn't set, even though it's trivially provably always set
@@ -698,6 +709,7 @@ static void _unLz77(struct GBA* gba, int width) {
 			blocksRemaining = 8;
 		}
 	}
+	cpu->memory.accessSource = oldAccess;
 	cpu->gprs[0] = source;
 	cpu->gprs[1] = dest;
 	cpu->gprs[3] = 0;
@@ -713,6 +725,8 @@ static void _unHuffman(struct GBA* gba) {
 	struct ARMCore* cpu = gba->cpu;
 	uint32_t source = cpu->gprs[0] & 0xFFFFFFFC;
 	uint32_t dest = cpu->gprs[1];
+	enum mMemoryAccessSource oldAccess = cpu->memory.accessSource;
+	cpu->memory.accessSource = mACCESS_DECOMPRESS;
 	uint32_t header = cpu->memory.load32(cpu, source, 0);
 	int remaining = header >> 8;
 	unsigned bits = header & 0xF;
@@ -722,6 +736,7 @@ static void _unHuffman(struct GBA* gba) {
 	}
 	if (32 % bits || bits == 1) {
 		mLOG(GBA_BIOS, STUB, "Unimplemented unaligned Huffman");
+		cpu->memory.accessSource = oldAccess;
 		return;
 	}
 	// We assume the signature byte (0x20) is correct
@@ -773,6 +788,7 @@ static void _unHuffman(struct GBA* gba) {
 			}
 		}
 	}
+	cpu->memory.accessSource = oldAccess;
 	cpu->gprs[0] = source;
 	cpu->gprs[1] = dest;
 }
@@ -780,6 +796,8 @@ static void _unHuffman(struct GBA* gba) {
 static void _unRl(struct GBA* gba, int width) {
 	struct ARMCore* cpu = gba->cpu;
 	uint32_t source = cpu->gprs[0];
+	enum mMemoryAccessSource oldAccess = cpu->memory.accessSource;
+	cpu->memory.accessSource = mACCESS_DECOMPRESS;
 	int remaining = (cpu->memory.load32(cpu, source & 0xFFFFFFFC, 0) & 0xFFFFFF00) >> 8;
 	int padding = (4 - remaining) & 0x3;
 	// We assume the signature byte (0x30) is correct
@@ -846,6 +864,7 @@ static void _unRl(struct GBA* gba, int width) {
 			++dest;
 		}
 	}
+	cpu->memory.accessSource = oldAccess;
 	cpu->gprs[0] = source;
 	cpu->gprs[1] = dest;
 }
@@ -854,6 +873,8 @@ static void _unFilter(struct GBA* gba, int inwidth, int outwidth) {
 	struct ARMCore* cpu = gba->cpu;
 	uint32_t source = cpu->gprs[0] & 0xFFFFFFFC;
 	uint32_t dest = cpu->gprs[1];
+	enum mMemoryAccessSource oldAccess = cpu->memory.accessSource;
+	cpu->memory.accessSource = mACCESS_DECOMPRESS;
 	uint32_t header = cpu->memory.load32(cpu, source, 0);
 	int remaining = header >> 8;
 	// We assume the signature nybble (0x8) is correct
@@ -888,6 +909,7 @@ static void _unFilter(struct GBA* gba, int inwidth, int outwidth) {
 		old = new;
 		source += inwidth;
 	}
+	cpu->memory.accessSource = oldAccess;
 	cpu->gprs[0] = source;
 	cpu->gprs[1] = dest;
 }
@@ -897,6 +919,8 @@ static void _unBitPack(struct GBA* gba) {
 	uint32_t source = cpu->gprs[0];
 	uint32_t dest = cpu->gprs[1];
 	uint32_t info = cpu->gprs[2];
+	enum mMemoryAccessSource oldAccess = cpu->memory.accessSource;
+	cpu->memory.accessSource = mACCESS_DECOMPRESS;
 	unsigned sourceLen = cpu->memory.load16(cpu, info, 0);
 	unsigned sourceWidth = cpu->memory.load8(cpu, info + 2, 0);
 	unsigned destWidth = cpu->memory.load8(cpu, info + 3, 0);
@@ -908,6 +932,7 @@ static void _unBitPack(struct GBA* gba) {
 		break;
 	default:
 		mLOG(GBA_BIOS, GAME_ERROR, "Bad BitUnPack source width: %u", sourceWidth);
+		cpu->memory.accessSource = oldAccess;
 		return;
 	}
 	switch (destWidth) {
@@ -920,6 +945,7 @@ static void _unBitPack(struct GBA* gba) {
 		break;
 	default:
 		mLOG(GBA_BIOS, GAME_ERROR, "Bad BitUnPack destination width: %u", destWidth);
+		cpu->memory.accessSource = oldAccess;
 		return;
 	}
 	uint32_t bias = cpu->memory.load32(cpu, info + 4, 0);
@@ -949,6 +975,7 @@ static void _unBitPack(struct GBA* gba) {
 			dest += 4;
 		}
 	}
+	cpu->memory.accessSource = oldAccess;
 	cpu->gprs[0] = source;
 	cpu->gprs[1] = dest;
 }
diff --git a/src/gba/core.c b/src/gba/core.c
index da782d06c..b369c92df 100644
--- a/src/gba/core.c
+++ b/src/gba/core.c
@@ -479,6 +479,14 @@ static void _GBACoreReloadConfigOption(struct mCore* core, const char* option, c
 			GBAVideoAssociateRenderer(&gba->video, renderer);
 		}
 	}
+
+#ifndef MINIMAL_CORE
+	if (strcmp("threadedVideo.flushScanline", option) == 0) {
+		int flushScanline = -1;
+		mCoreConfigGetIntValue(config, "threadedVideo.flushScanline", &flushScanline);
+		gbacore->proxyRenderer.flushScanline = flushScanline;
+	}
+#endif
 }
 
 static void _GBACoreSetOverride(struct mCore* core, const void* override) {
@@ -730,6 +738,10 @@ static void _GBACoreReset(struct mCore* core) {
 		if (renderer && core->videoLogger) {
 			GBAVideoProxyRendererCreate(&gbacore->proxyRenderer, renderer, core->videoLogger);
 			renderer = &gbacore->proxyRenderer.d;
+
+			int flushScanline = -1;
+			mCoreConfigGetIntValue(&core->config, "threadedVideo.flushScanline", &flushScanline);
+			gbacore->proxyRenderer.flushScanline = flushScanline;
 		}
 #endif
 		if (renderer) {
diff --git a/src/gba/dma.c b/src/gba/dma.c
index e9044d26a..3e3e8222d 100644
--- a/src/gba/dma.c
+++ b/src/gba/dma.c
@@ -248,10 +248,12 @@ void GBADMAService(struct GBA* gba, int number, struct GBADMA* info) {
 	uint32_t dest = info->nextDest;
 	uint32_t sourceRegion = source >> BASE_OFFSET;
 	uint32_t destRegion = dest >> BASE_OFFSET;
+	enum mMemoryAccessSource oldAccess = cpu->memory.accessSource;
 	int32_t cycles = 2;
 
 	gba->cpuBlocked = true;
 	gba->performingDMA = 1 | (number << 1);
+	cpu->memory.accessSource = mACCESS_DMA;
 
 	if (info->count == info->nextCount) {
 		if (width == 4) {
@@ -315,6 +317,7 @@ void GBADMAService(struct GBA* gba, int number, struct GBADMA* info) {
 	--info->nextCount;
 
 	gba->performingDMA = 0;
+	cpu->memory.accessSource = oldAccess;
 
 	int i;
 	for (i = 0; i < 4; ++i) {
diff --git a/src/gba/extra/proxy.c b/src/gba/extra/proxy.c
index e33c7b568..bcf91f5f4 100644
--- a/src/gba/extra/proxy.c
+++ b/src/gba/extra/proxy.c
@@ -75,6 +75,7 @@ void GBAVideoProxyRendererCreate(struct GBAVideoProxyRenderer* renderer, struct
 	logger->vramSize = GBA_SIZE_VRAM;
 	logger->oamSize = GBA_SIZE_OAM;
 
+	renderer->flushScanline = -1;
 	renderer->backend = backend;
 }
 
@@ -359,6 +360,9 @@ void GBAVideoProxyRendererWriteOAM(struct GBAVideoRenderer* renderer, uint32_t o
 
 void GBAVideoProxyRendererDrawScanline(struct GBAVideoRenderer* renderer, int y) {
 	struct GBAVideoProxyRenderer* proxyRenderer = (struct GBAVideoProxyRenderer*) renderer;
+	if (proxyRenderer->flushScanline == y) {
+		mVideoLoggerRendererFlush(proxyRenderer->logger);
+	}
 	if (!proxyRenderer->logger->block) {
 		_copyExtraState(proxyRenderer);
 		proxyRenderer->backend->drawScanline(proxyRenderer->backend, y);
@@ -375,7 +379,9 @@ void GBAVideoProxyRendererFinishFrame(struct GBAVideoRenderer* renderer) {
 		proxyRenderer->backend->finishFrame(proxyRenderer->backend);
 	}
 	mVideoLoggerRendererFinishFrame(proxyRenderer->logger);
-	mVideoLoggerRendererFlush(proxyRenderer->logger);
+	if (proxyRenderer->flushScanline < 0) {
+		mVideoLoggerRendererFlush(proxyRenderer->logger);
+	}
 }
 
 static void GBAVideoProxyRendererGetPixels(struct GBAVideoRenderer* renderer, size_t* stride, const void** pixels) {
diff --git a/src/gba/io.c b/src/gba/io.c
index cb6058f7f..0747986f0 100644
--- a/src/gba/io.c
+++ b/src/gba/io.c
@@ -934,7 +934,7 @@ uint16_t GBAIORead(struct GBA* gba, uint32_t address) {
 		}
 		// Fall through
 	case GBA_REG_DISPCNT:
-	case GBA_REG_GREENSWP:
+	case GBA_REG_STEREOCNT:
 	case GBA_REG_DISPSTAT:
 	case GBA_REG_VCOUNT:
 	case GBA_REG_BG0CNT:
diff --git a/src/gba/memory.c b/src/gba/memory.c
index 4f074657a..27e289c8e 100644
--- a/src/gba/memory.c
+++ b/src/gba/memory.c
@@ -82,6 +82,7 @@ void GBAMemoryInit(struct GBA* gba) {
 	cpu->memory.activeSeqCycles16 = 0;
 	cpu->memory.activeNonseqCycles32 = 0;
 	cpu->memory.activeNonseqCycles16 = 0;
+	cpu->memory.accessSource = mACCESS_UNKNOWN;
 	gba->memory.biosPrefetch = 0;
 
 	gba->memory.agbPrintProtect = 0;
@@ -132,6 +133,7 @@ void GBAMemoryReset(struct GBA* gba) {
 
 	gba->memory.prefetch = false;
 	gba->memory.lastPrefetchedPc = 0;
+	gba->cpu->memory.accessSource = mACCESS_UNKNOWN;
 
 	if (!gba->memory.wram || !gba->memory.iwram) {
 		GBAMemoryDeinit(gba);
@@ -299,22 +301,27 @@ static void GBASetActiveRegion(struct ARMCore* cpu, uint32_t address) {
 	memory->activeRegion = newRegion;
 	switch (newRegion) {
 	case GBA_REGION_BIOS:
+		cpu->memory.accessSource = mACCESS_SYSTEM;
 		cpu->memory.activeRegion = memory->bios;
 		cpu->memory.activeMask = GBA_SIZE_BIOS - 1;
 		break;
 	case GBA_REGION_EWRAM:
+		cpu->memory.accessSource = mACCESS_PROGRAM;
 		cpu->memory.activeRegion = memory->wram;
 		cpu->memory.activeMask = GBA_SIZE_EWRAM - 1;
 		break;
 	case GBA_REGION_IWRAM:
+		cpu->memory.accessSource = mACCESS_PROGRAM;
 		cpu->memory.activeRegion = memory->iwram;
 		cpu->memory.activeMask = GBA_SIZE_IWRAM - 1;
 		break;
 	case GBA_REGION_PALETTE_RAM:
+		cpu->memory.accessSource = mACCESS_PROGRAM;
 		cpu->memory.activeRegion = (uint32_t*) gba->video.palette;
 		cpu->memory.activeMask = GBA_SIZE_PALETTE_RAM - 1;
 		break;
 	case GBA_REGION_VRAM:
+		cpu->memory.accessSource = mACCESS_PROGRAM;
 		if (address & 0x10000) {
 			cpu->memory.activeRegion = (uint32_t*) &gba->video.vram[0x8000];
 			cpu->memory.activeMask = 0x00007FFF;
@@ -324,6 +331,7 @@ static void GBASetActiveRegion(struct ARMCore* cpu, uint32_t address) {
 		}
 		break;
 	case GBA_REGION_OAM:
+		cpu->memory.accessSource = mACCESS_PROGRAM;
 		cpu->memory.activeRegion = (uint32_t*) gba->video.oam.raw;
 		cpu->memory.activeMask = GBA_SIZE_OAM - 1;
 		break;
@@ -333,6 +341,7 @@ static void GBASetActiveRegion(struct ARMCore* cpu, uint32_t address) {
 	case GBA_REGION_ROM1_EX:
 	case GBA_REGION_ROM2:
 	case GBA_REGION_ROM2_EX:
+		cpu->memory.accessSource = mACCESS_PROGRAM;
 		cpu->memory.activeRegion = memory->rom;
 		cpu->memory.activeMask = memory->romMask;
 		if ((address & (GBA_SIZE_ROM0 - 1)) < memory->romSize) {
@@ -345,6 +354,7 @@ static void GBASetActiveRegion(struct ARMCore* cpu, uint32_t address) {
 		}
 	// Fall through
 	default:
+		cpu->memory.accessSource = mACCESS_UNKNOWN;
 		memory->activeRegion = -1;
 		cpu->memory.activeRegion = (uint32_t*) _deadbeef;
 		cpu->memory.activeMask = 0;
diff --git a/src/gba/renderers/video-software.c b/src/gba/renderers/video-software.c
index 83550bcfa..e2dab9033 100644
--- a/src/gba/renderers/video-software.c
+++ b/src/gba/renderers/video-software.c
@@ -136,7 +136,7 @@ static void GBAVideoSoftwareRendererReset(struct GBAVideoRenderer* renderer) {
 	softwareRenderer->oamMax = 0;
 
 	softwareRenderer->mosaic = 0;
-	softwareRenderer->greenswap = false;
+	softwareRenderer->stereo = false;
 	softwareRenderer->nextY = 0;
 
 	softwareRenderer->objOffsetX = 0;
@@ -195,8 +195,8 @@ static uint16_t GBAVideoSoftwareRendererWriteVideoRegister(struct GBAVideoRender
 		softwareRenderer->dispcnt = value;
 		GBAVideoSoftwareRendererUpdateDISPCNT(softwareRenderer);
 		break;
-	case GBA_REG_GREENSWP:
-		softwareRenderer->greenswap = value & 1;
+	case GBA_REG_STEREOCNT:
+		softwareRenderer->stereo = value & 1;
 		break;
 	case GBA_REG_BG0CNT:
 		value &= 0xDFFF;
@@ -705,7 +705,7 @@ static void GBAVideoSoftwareRendererDrawScanline(struct GBAVideoRenderer* render
 	}
 
 	int x;
-	if (softwareRenderer->greenswap) {
+	if (softwareRenderer->stereo) {
 		for (x = 0; x < GBA_VIDEO_HORIZONTAL_PIXELS; x += 4) {
 			row[x] = softwareRenderer->row[x] & (M_COLOR_RED | M_COLOR_BLUE);
 			row[x] |= softwareRenderer->row[x + 1] & M_COLOR_GREEN;
diff --git a/src/gba/savedata.c b/src/gba/savedata.c
index 50d2be381..13204cbb3 100644
--- a/src/gba/savedata.c
+++ b/src/gba/savedata.c
@@ -372,11 +372,11 @@ uint8_t GBASavedataReadFlash(struct GBASavedata* savedata, uint16_t address) {
 	if (savedata->command == FLASH_COMMAND_ID) {
 		if (savedata->type == GBA_SAVEDATA_FLASH512) {
 			if (address < 2) {
-				return FLASH_MFG_PANASONIC >> (address * 8);
+				return FLASH_PANASONIC_MN63F805MNP >> (address * 8);
 			}
 		} else if (savedata->type == GBA_SAVEDATA_FLASH1M) {
 			if (address < 2) {
-				return FLASH_MFG_SANYO >> (address * 8);
+				return FLASH_SANYO_LE26FV10N1TS >> (address * 8);
 			}
 		}
 	}
diff --git a/src/gba/video.c b/src/gba/video.c
index 749a29be0..a86413251 100644
--- a/src/gba/video.c
+++ b/src/gba/video.c
@@ -128,7 +128,7 @@ void GBAVideoAssociateRenderer(struct GBAVideo* video, struct GBAVideoRenderer*
 	video->renderer->init(video->renderer);
 	video->renderer->reset(video->renderer);
 	renderer->writeVideoRegister(renderer, GBA_REG_DISPCNT, video->p->memory.io[GBA_REG(DISPCNT)]);
-	renderer->writeVideoRegister(renderer, GBA_REG_GREENSWP, video->p->memory.io[GBA_REG(GREENSWP)]);
+	renderer->writeVideoRegister(renderer, GBA_REG_STEREOCNT, video->p->memory.io[GBA_REG(STEREOCNT)]);
 	int address;
 	for (address = GBA_REG_BG0CNT; address < 0x56; address += 2) {
 		if (address == 0x4E) {
diff --git a/src/platform/3ds/main.c b/src/platform/3ds/main.c
index 2d7f80d15..7bac5f61e 100644
--- a/src/platform/3ds/main.c
+++ b/src/platform/3ds/main.c
@@ -273,6 +273,7 @@ static void _resetCamera(struct m3DSImageSource* imageSource) {
 static void _setup(struct mGUIRunner* runner) {
 	if (core2) {
 		mCoreConfigSetDefaultIntValue(&runner->config, "threadedVideo", 1);
+		mCoreConfigSetDefaultIntValue(&runner->config, "threadedVideo.flushScanline", 0);
 		mCoreLoadForeignConfig(runner->core, &runner->config);
 	}
 
@@ -968,6 +969,22 @@ int main(int argc, char* argv[]) {
 			{ .id = 0 }
 		},
 		.configExtra = (struct GUIMenuItem[]) {
+#ifdef M_CORE_GBA
+			{
+				.title = "Sync",
+				.data = GUI_V_S("threadedVideo.flushScanline"),
+				.state = 0,
+				.validStates = (const char*[]) {
+					"Loose (faster, can tear)", "Strict (slower, less input lag)"
+				},
+				.stateMappings = (const struct GUIVariant[]) {
+					GUI_V_I(0),
+					GUI_V_I(-1),
+				},
+				.nStates = 2
+			},
+#endif
+
 			{
 				.title = "Screen mode",
 				.data = GUI_V_S("screenMode"),
diff --git a/src/platform/python/mgba/gba.py b/src/platform/python/mgba/gba.py
index 0c249cec5..91ac3254e 100644
--- a/src/platform/python/mgba/gba.py
+++ b/src/platform/python/mgba/gba.py
@@ -54,7 +54,7 @@ class GBA(Core):
 
 
 class GBAMemory(Memory):
-    def __init__(self, core, romSize=lib.SIZE_CART0):
+    def __init__(self, core, romSize=lib.GBA_SIZE_ROM0):
         super(GBAMemory, self).__init__(core, 0x100000000)
 
         self.bios = Memory(core, lib.GBA_SIZE_BIOS, lib.GBA_BASE_BIOS)
@@ -64,11 +64,10 @@ class GBAMemory(Memory):
         self.palette = Memory(core, lib.GBA_SIZE_PALETTE_RAM, lib.GBA_BASE_PALETTE_RAM)
         self.vram = Memory(core, lib.GBA_SIZE_VRAM, lib.GBA_BASE_VRAM)
         self.oam = Memory(core, lib.GBA_SIZE_OAM, lib.GBA_BASE_OAM)
-        self.cart0 = Memory(core, romSize, lib.BASE_CART0)
-        self.cart1 = Memory(core, romSize, lib.BASE_CART1)
-        self.cart2 = Memory(core, romSize, lib.BASE_CART2)
-        self.cart = self.cart0
-        self.rom = self.cart0
+        self.rom0 = Memory(core, romSize, lib.GBA_BASE_ROM0)
+        self.rom1 = Memory(core, romSize, lib.GBA_BASE_ROM1)
+        self.rom2 = Memory(core, romSize, lib.GBA_BASE_ROM2)
+        self.rom = self.rom0
         self.sram = Memory(core, lib.GBA_SIZE_SRAM, lib.GBA_BASE_SRAM)
 
 
diff --git a/src/platform/qt/CMakeLists.txt b/src/platform/qt/CMakeLists.txt
index 910ab9b57..9858fddb0 100644
--- a/src/platform/qt/CMakeLists.txt
+++ b/src/platform/qt/CMakeLists.txt
@@ -262,6 +262,7 @@ if(ENABLE_DEBUGGERS)
 		DebuggerConsole.cpp
 		DebuggerConsoleController.cpp
 		MemoryAccessLogController.cpp
+		MemoryAccessLogModel.cpp
 		MemoryAccessLogView.cpp)
 endif()
 
diff --git a/src/platform/qt/ConfigController.cpp b/src/platform/qt/ConfigController.cpp
index a12768709..eae75fea4 100644
--- a/src/platform/qt/ConfigController.cpp
+++ b/src/platform/qt/ConfigController.cpp
@@ -149,6 +149,7 @@ ConfigController::ConfigController(QObject* parent)
 	mCoreConfigSetDefaultIntValue(&m_config, "sgb.borders", 1);
 	mCoreConfigSetDefaultIntValue(&m_config, "gb.colors", GB_COLORS_CGB);
 #endif
+	mCoreConfigSetDefaultIntValue(&m_config, "preload", true);
 	mCoreConfigMap(&m_config, &m_opts);
 
 	mSubParserGraphicsInit(&m_subparsers[0], &m_graphicsOpts);
diff --git a/src/platform/qt/CoreController.cpp b/src/platform/qt/CoreController.cpp
index 152efc222..7cd6c58d5 100644
--- a/src/platform/qt/CoreController.cpp
+++ b/src/platform/qt/CoreController.cpp
@@ -305,7 +305,7 @@ void CoreController::loadConfig(ConfigController* config) {
 	m_fastForwardMute = config->getOption("fastForwardMute", -1).toInt();
 	mCoreConfigCopyValue(&m_threadContext.core->config, config->config(), "volume");
 	mCoreConfigCopyValue(&m_threadContext.core->config, config->config(), "mute");
-	m_preload = config->getOption("preload").toInt();
+	m_preload = config->getOption("preload", true).toInt();
 
 	QSize sizeBefore = screenDimensions();
 	m_activeBuffer.resize(256 * 224 * sizeof(mColor));
@@ -1267,6 +1267,9 @@ void CoreController::finishFrame() {
 }
 
 void CoreController::updatePlayerSave() {
+	if (m_saveBlocked) {
+		return;
+	}
 	int savePlayerId = m_multiplayer->saveId(this);
 
 	QString saveSuffix;
diff --git a/src/platform/qt/CoreController.h b/src/platform/qt/CoreController.h
index a2ebe410e..a6b6a03ca 100644
--- a/src/platform/qt/CoreController.h
+++ b/src/platform/qt/CoreController.h
@@ -176,6 +176,7 @@ public slots:
 	void scanCards(const QStringList&);
 	void replaceGame(const QString&);
 	void yankPak();
+	void blockSave() { m_saveBlocked = true; }
 
 	void addKey(int key);
 	void clearKey(int key);
@@ -262,7 +263,8 @@ private:
 	QString m_savePath;
 
 	bool m_patched = false;
-	bool m_preload = false;
+	bool m_preload = true;
+	bool m_saveBlocked = false;
 
 	uint32_t m_crc32;
 	QString m_internalTitle;
diff --git a/src/platform/qt/CoreManager.cpp b/src/platform/qt/CoreManager.cpp
index b2f7cabf0..385b3440b 100644
--- a/src/platform/qt/CoreManager.cpp
+++ b/src/platform/qt/CoreManager.cpp
@@ -169,6 +169,7 @@ CoreController* CoreManager::loadBIOS(int platform, const QString& path) {
 	mDirectorySetAttachBase(&core->dirs, VDirOpen(bytes.constData()));
 
 	CoreController* cc = new CoreController(core);
+	cc->blockSave();
 	if (m_multiplayer) {
 		cc->setMultiplayerController(m_multiplayer);
 	}
diff --git a/src/platform/qt/CoreManager.h b/src/platform/qt/CoreManager.h
index da7460f22..e910886f6 100644
--- a/src/platform/qt/CoreManager.h
+++ b/src/platform/qt/CoreManager.h
@@ -36,7 +36,7 @@ signals:
 private:
 	const mCoreConfig* m_config = nullptr;
 	MultiplayerController* m_multiplayer = nullptr;
-	bool m_preload = false;
+	bool m_preload = true;
 };
 
 }
diff --git a/src/platform/qt/GameBoy.cpp b/src/platform/qt/GameBoy.cpp
index b07982721..9f64ae3b1 100644
--- a/src/platform/qt/GameBoy.cpp
+++ b/src/platform/qt/GameBoy.cpp
@@ -44,6 +44,7 @@ static const QList<GBMemoryBankControllerType> s_mbcList{
 	GB_UNL_LI_CHENG,
 	GB_UNL_SACHEN_MMC1,
 	GB_UNL_SACHEN_MMC2,
+	GB_UNL_SINTAX,
 };
 
 static QMap<GBModel, QString> s_gbModelNames;
@@ -102,6 +103,7 @@ QString GameBoy::mbcName(GBMemoryBankControllerType mbc) {
 		s_mbcNames[GB_UNL_LI_CHENG] = tr("Li Cheng");
 		s_mbcNames[GB_UNL_SACHEN_MMC1] = tr("Sachen (MMC1)");
 		s_mbcNames[GB_UNL_SACHEN_MMC2] = tr("Sachen (MMC2)");
+		s_mbcNames[GB_UNL_SINTAX] = tr("Sintax");
 	}
 
 	return s_mbcNames[mbc];
diff --git a/src/platform/qt/IOViewer.cpp b/src/platform/qt/IOViewer.cpp
index 9b44af04f..8ef921b7c 100644
--- a/src/platform/qt/IOViewer.cpp
+++ b/src/platform/qt/IOViewer.cpp
@@ -60,7 +60,7 @@ const QList<IOViewer::RegisterDescription>& IOViewer::registerDescriptions(mPlat
 		{ tr("Enable Window 1"), 14 },
 		{ tr("Enable OBJ Window"), 15 },
 	});
-	// 0x04000002: Green swap
+	// 0x04000002: STEREOCNT
 	regGBA.append({
 		{ tr("Swap green components"), 0 },
 	});
diff --git a/src/platform/qt/MemoryAccessLogController.cpp b/src/platform/qt/MemoryAccessLogController.cpp
index 95200e53e..d62b2f8a5 100644
--- a/src/platform/qt/MemoryAccessLogController.cpp
+++ b/src/platform/qt/MemoryAccessLogController.cpp
@@ -10,8 +10,14 @@
 #include "utils.h"
 #include "VFileDevice.h"
 
+#include <mgba-util/math.h>
+
 using namespace QGBA;
 
+int MemoryAccessLogController::Flags::count() const {
+	return popcount32(flags) + popcount32(flagsEx);
+}
+
 MemoryAccessLogController::MemoryAccessLogController(CoreController* controller, QObject* parent)
 	: QObject(parent)
 	, m_controller(controller)
@@ -39,6 +45,17 @@ bool MemoryAccessLogController::canExport() const {
 	return m_regionMapping.contains("cart0");
 }
 
+MemoryAccessLogController::Flags MemoryAccessLogController::flagsForAddress(uint32_t addresss, int segment) {
+	uint32_t offset = cacheRegion(addresss, segment);
+	if (!m_cachedRegion) {
+		return { 0, 0 };
+	}
+	return {
+		m_cachedRegion->blockEx ? m_cachedRegion->blockEx[offset] : mDebuggerAccessLogFlagsEx{},
+		m_cachedRegion->block ? m_cachedRegion->block[offset] : mDebuggerAccessLogFlags{},
+	};
+}
+
 void MemoryAccessLogController::updateRegion(const QString& internalName, bool checked) {
 	if (checked) {
 		m_watchedRegions += internalName;
@@ -48,7 +65,9 @@ void MemoryAccessLogController::updateRegion(const QString& internalName, bool c
 	if (!m_active) {
 		return;
 	}
-	m_regionMapping[internalName] = mDebuggerAccessLoggerWatchMemoryBlockName(&m_logger, internalName.toUtf8().constData(), activeFlags());
+	if (checked && !m_regionMapping.contains(internalName)) {
+		m_regionMapping[internalName] = mDebuggerAccessLoggerWatchMemoryBlockName(&m_logger, internalName.toUtf8().constData(), activeFlags());
+	}
 	emit regionMappingChanged(internalName, checked);
 }
 
@@ -57,6 +76,38 @@ void MemoryAccessLogController::setFile(const QString& path) {
 }
 
 void MemoryAccessLogController::start(bool loadExisting, bool logExtra) {
+	if (!m_loaded) {
+		load(loadExisting);
+	}
+	if (!m_loaded) {
+		return;
+	}
+	CoreController::Interrupter interrupter(m_controller);
+	mDebuggerAccessLoggerStart(&m_logger);
+	m_logExtra = logExtra;
+
+	m_active = true;
+	for (const auto& region : m_watchedRegions) {
+		m_regionMapping[region] = mDebuggerAccessLoggerWatchMemoryBlockName(&m_logger, region.toUtf8().constData(), activeFlags());
+	}
+	emit loggingChanged(true);
+}
+
+void MemoryAccessLogController::stop() {
+	if (!m_active) {
+		return;
+	}
+	CoreController::Interrupter interrupter(m_controller);
+	mDebuggerAccessLoggerStop(&m_logger);
+	emit loggingChanged(false);
+	interrupter.resume();
+	m_active = false;
+}
+
+void MemoryAccessLogController::load(bool loadExisting) {
+	if (m_loaded) {
+		return;
+	}
 	int flags = O_CREAT | O_RDWR;
 	if (!loadExisting) {
 		flags |= O_TRUNC;
@@ -66,7 +117,6 @@ void MemoryAccessLogController::start(bool loadExisting, bool logExtra) {
 		LOG(QT, ERROR) << tr("Failed to open memory log file");
 		return;
 	}
-	m_logExtra = logExtra;
 
 	mDebuggerAccessLoggerInit(&m_logger);
 	CoreController::Interrupter interrupter(m_controller);
@@ -76,25 +126,22 @@ void MemoryAccessLogController::start(bool loadExisting, bool logExtra) {
 		LOG(QT, ERROR) << tr("Failed to open memory log file");
 		return;
 	}
-
-	m_active = true;
-	emit loggingChanged(true);
-	for (const auto& region : m_watchedRegions) {
-		m_regionMapping[region] = mDebuggerAccessLoggerWatchMemoryBlockName(&m_logger, region.toUtf8().constData(), activeFlags());
-	}
-	interrupter.resume();
+	emit loaded(true);
+	m_loaded = true;
 }
 
-void MemoryAccessLogController::stop() {
-	if (!m_active) {
+void MemoryAccessLogController::unload() {
+	if (m_active) {
+		stop();
+	}
+	if (m_active) {
 		return;
 	}
 	CoreController::Interrupter interrupter(m_controller);
 	m_controller->detachDebuggerModule(&m_logger.d);
 	mDebuggerAccessLoggerDeinit(&m_logger);
-	emit loggingChanged(false);
-	interrupter.resume();
-	m_active = false;
+	emit loaded(false);
+	m_loaded = false;
 }
 
 mDebuggerAccessLogRegionFlags MemoryAccessLogController::activeFlags() const {
@@ -116,3 +163,27 @@ void MemoryAccessLogController::exportFile(const QString& filename) {
 	mDebuggerAccessLoggerCreateShadowFile(&m_logger, m_regionMapping[QString("cart0")], vf, 0);
 	vf->close(vf);
 }
+
+uint32_t MemoryAccessLogController::cacheRegion(uint32_t address, int segment) {
+	if (m_cachedRegion && (address < m_cachedRegion->start || address >= m_cachedRegion->end)) {
+		m_cachedRegion = nullptr;
+	}
+	if (!m_cachedRegion) {
+		m_cachedRegion = mDebuggerAccessLoggerGetRegion(&m_logger, address, segment, nullptr);
+	}
+	if (!m_cachedRegion) {
+		return 0;
+	}
+
+	size_t offset = address - m_cachedRegion->start;
+	if (segment > 0) {
+		uint32_t segmentSize = m_cachedRegion->end - m_cachedRegion->segmentStart;
+		offset %= segmentSize;
+		offset += segmentSize * segment;
+	}
+	if (offset >= m_cachedRegion->size) {
+		m_cachedRegion = nullptr;
+		return cacheRegion(address, segment);
+	}
+	return offset;
+}
diff --git a/src/platform/qt/MemoryAccessLogController.h b/src/platform/qt/MemoryAccessLogController.h
index 42ef655c3..b2df55214 100644
--- a/src/platform/qt/MemoryAccessLogController.h
+++ b/src/platform/qt/MemoryAccessLogController.h
@@ -12,6 +12,7 @@
 
 #include "CoreController.h"
 
+#include <mgba/debugger/debugger.h>
 #include <mgba/internal/debugger/access-logger.h>
 
 namespace QGBA {
@@ -25,6 +26,16 @@ public:
 		QString internalName;
 	};
 
+	struct Flags {
+		mDebuggerAccessLogFlagsEx flagsEx;
+		mDebuggerAccessLogFlags flags;
+
+		int count() const;
+		bool operator==(const Flags& other) const { return flags == other.flags && flagsEx == other.flagsEx; }
+		bool operator!=(const Flags& other) const { return flags != other.flags || flagsEx != other.flagsEx; }
+		operator bool() const { return flags || flagsEx; }
+	};
+
 	MemoryAccessLogController(CoreController* controller, QObject* parent = nullptr);
 	~MemoryAccessLogController();
 
@@ -34,8 +45,11 @@ public:
 	bool canExport() const;
 	mPlatform platform() const { return m_controller->platform(); }
 
+	Flags flagsForAddress(uint32_t address, int segment = -1);
+
 	QString file() const { return m_path; }
 	bool active() const { return m_active; }
+	bool isLoaded() const { return m_loaded; }
 
 public slots:
 	void updateRegion(const QString& internalName, bool enable);
@@ -44,9 +58,13 @@ public slots:
 	void start(bool loadExisting, bool logExtra);
 	void stop();
 
+	void load(bool loadExisting);
+	void unload();
+
 	void exportFile(const QString& filename);
 
 signals:
+	void loaded(bool loaded);
 	void loggingChanged(bool active);
 	void regionMappingChanged(const QString& internalName, bool active);
 
@@ -58,9 +76,12 @@ private:
 	QHash<QString, int> m_regionMapping;
 	QVector<Region> m_regions;
 	struct mDebuggerAccessLogger m_logger{};
+	bool m_loaded = false;
 	bool m_active = false;
+	mDebuggerAccessLogRegion* m_cachedRegion = nullptr;
 
 	mDebuggerAccessLogRegionFlags activeFlags() const;
+	uint32_t cacheRegion(uint32_t address, int segment);
 };
 
 }
diff --git a/src/platform/qt/MemoryAccessLogModel.cpp b/src/platform/qt/MemoryAccessLogModel.cpp
new file mode 100644
index 000000000..6fa8592d0
--- /dev/null
+++ b/src/platform/qt/MemoryAccessLogModel.cpp
@@ -0,0 +1,301 @@
+/* Copyright (c) 2013-2025 Jeffrey Pfau
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "MemoryAccessLogModel.h"
+
+#include <limits>
+
+using namespace QGBA;
+
+MemoryAccessLogModel::MemoryAccessLogModel(std::weak_ptr<MemoryAccessLogController> controller, mPlatform platform)
+	: m_controller(controller)
+	, m_platform(platform)
+{
+}
+
+QVariant MemoryAccessLogModel::data(const QModelIndex& index, int role) const {
+	if (role != Qt::DisplayRole) {
+		return {};
+	}
+	if (index.column() != 0) {
+		return {};
+	}
+	int blockIndex = -1;
+	int flagIndex = -1;
+	QModelIndex parent = index.parent();
+	if (!parent.isValid()) {
+		blockIndex = index.row();
+	} else {
+		blockIndex = parent.row();
+		flagIndex = index.row();
+	}
+
+	if (blockIndex < 0 || blockIndex >= m_cachedBlocks.count()) {
+		return {};
+	}
+
+	const Block& block = m_cachedBlocks[blockIndex];
+
+	if (flagIndex < 0) {
+		if (m_platform == mPLATFORM_GB) {
+			if (m_segment < 0) {
+				return QString("$%1 – $%2")
+					.arg(QString("%0").arg(block.region.first, 4, 16, QChar('0')).toUpper())
+					.arg(QString("%0").arg(block.region.second, 4, 16, QChar('0')).toUpper());
+			} else {
+				return QString("$%1:%3 – $%2:%4")
+					.arg(QString("%0").arg(m_segment, 2, 16, QChar('0')).toUpper())
+					.arg(QString("%0").arg(m_segment, 2, 16, QChar('0')).toUpper())
+					.arg(QString("%0").arg(block.region.first, 4, 16, QChar('0')).toUpper())
+					.arg(QString("%0").arg(block.region.second, 4, 16, QChar('0')).toUpper());
+			}
+		} else {
+			return QString("0x%1 – 0x%2")
+				.arg(QString("%0").arg(block.region.first, 8, 16, QChar('0')).toUpper())
+				.arg(QString("%0").arg(block.region.second, 8, 16, QChar('0')).toUpper());			
+		}
+	}
+	for (int i = 0; i < 8; ++i) {
+		if (!(block.flags.flags & (1 << i))) {
+			continue;
+		}
+		if (flagIndex == 0) {
+			switch (i) {
+			case 0:
+				return tr("Data read");
+			case 1:
+				return tr("Data written");
+			case 2:
+				return tr("Code executed");
+			case 3:
+				return tr("Code aborted");
+			case 4:
+				return tr("8-bit access");
+			case 5:
+				return tr("16-bit access");
+			case 6:
+				return tr("32-bit access");
+			case 7:
+				return tr("64-bit access");
+			default:
+				Q_UNREACHABLE();
+			}
+		}
+		--flagIndex;
+	}
+	for (int i = 0; i < 16; ++i) {
+		if (!(block.flags.flagsEx & (1 << i))) {
+			continue;
+		}
+		if (flagIndex == 0) {
+			switch (i) {
+			case 0:
+				return tr("Accessed by instruction");
+			case 1:
+				return tr("Accessed by DMA");
+			case 2:
+				return tr("Accessed by BIOS");
+			case 3:
+				return tr("Compressed data");
+			case 4:
+				return tr("Accessed by memory copy");
+			case 5:
+				return tr("(Unknown extra bit 5)");
+			case 6:
+				return tr("(Unknown extra bit 6)");
+			case 7:
+				return tr("(Unknown extra bit 7)");
+			case 8:
+				return tr("Invalid instruction");
+			case 9:
+				return tr("Invalid read");
+			case 10:
+				return tr("Invalid write");
+			case 11:
+				return tr("Invalid executable address");
+			case 12:
+				return tr("(Private bit 0)");
+			case 13:
+				return tr("(Private bit 1)");
+			case 14:
+				switch (m_platform) {
+				case mPLATFORM_GBA:
+					return tr("ARM code");
+				case mPLATFORM_GB:
+					return tr("Instruction opcode");
+				default:
+					return tr("(Private bit 2)");
+				}
+			case 15:
+				switch (m_platform) {
+				case mPLATFORM_GBA:
+					return tr("Thumb code");
+				case mPLATFORM_GB:
+					return tr("Instruction operand");
+				default:
+					return tr("(Private bit 3)");
+				}
+			default:
+				Q_UNREACHABLE();
+			}
+		}
+		--flagIndex;
+	}
+	return tr("(Unknown)");
+}
+
+QModelIndex MemoryAccessLogModel::index(int row, int column, const QModelIndex& parent) const {
+	if (column != 0) {
+		return {};
+	}
+	if (parent.isValid()) {
+		return createIndex(row, 0, parent.row());
+	}
+	return createIndex(row, 0, std::numeric_limits<quintptr>::max());
+}
+
+QModelIndex MemoryAccessLogModel::parent(const QModelIndex& index) const {
+	if (!index.isValid()) {
+		return {};
+	}
+	quintptr row = index.internalId();
+	if (row >= std::numeric_limits<uint32_t>::max()) {
+		return {};
+	}
+	return createIndex(row, 0, std::numeric_limits<quintptr>::max());
+}
+
+int MemoryAccessLogModel::rowCount(const QModelIndex& parent) const {
+	int blockIndex = -1;
+	if (!parent.isValid()) {
+		return m_cachedBlocks.count();
+	} else if (parent.column() != 0) {
+		return 0;
+	} else if (parent.parent().isValid()) {
+		return 0;
+	} else {
+		blockIndex = parent.row();
+	}
+
+	if (blockIndex < 0 || blockIndex >= m_cachedBlocks.count()) {
+		return 0;
+	}
+
+	const Block& block = m_cachedBlocks[blockIndex];
+	return block.flags.count();
+}
+
+void MemoryAccessLogModel::updateSelection(uint32_t start, uint32_t end) {
+	std::shared_ptr<MemoryAccessLogController> controller = m_controller.lock();
+	if (!controller) {
+		return;
+	}
+	QVector<Block> newBlocks;
+	uint32_t lastStart = start;
+	auto lastFlags = controller->flagsForAddress(m_base + start, m_segment);
+
+	for (uint32_t address = start; address < end; ++address) {
+		auto flags = controller->flagsForAddress(m_base + address, m_segment);
+		if (flags == lastFlags) {
+			continue;
+		}
+		if (lastFlags) {
+			newBlocks.append({ lastFlags, qMakePair(lastStart, address) });
+		}
+		lastFlags = flags;
+		lastStart = address;
+	}
+	if (lastFlags) {
+		newBlocks.append({ lastFlags, qMakePair(lastStart, end) });
+	}
+
+	if (m_cachedBlocks.count() == 0 || newBlocks.count() == 0) {
+		beginResetModel();
+		m_cachedBlocks = newBlocks;
+		endResetModel();
+		return;
+	}
+
+	QPair<int, int> changed{ -1, -1 };
+	for (int i = 0; i < m_cachedBlocks.count() && i < newBlocks.count(); ++i) {
+		const Block& oldBlock = m_cachedBlocks.at(i);
+		const Block& newBlock = newBlocks.at(i);
+
+		if (oldBlock != newBlock) {
+			changed = qMakePair(i, m_cachedBlocks.count());
+			break;
+		}
+	}
+
+	if (m_cachedBlocks.count() > newBlocks.count()) {
+		beginRemoveRows({}, newBlocks.count(), m_cachedBlocks.count());
+		m_cachedBlocks.resize(newBlocks.count());
+		endRemoveRows();
+		changed.second = newBlocks.count();
+	}
+
+	if (m_cachedBlocks.count() < newBlocks.count()) {
+		beginInsertRows({}, m_cachedBlocks.count(), newBlocks.count());
+		if (changed.first < 0) {
+			// Only new rows
+			m_cachedBlocks = newBlocks;
+			endInsertRows();
+			return;
+		}
+	}
+
+	if (changed.first < 0) {
+		// No changed rows, though some might have been removed
+		return;
+	}
+
+	for (int i = 0; i < m_cachedBlocks.count() && i < newBlocks.count(); ++i) {
+		const Block& oldBlock = m_cachedBlocks.at(i);
+		const Block& newBlock = newBlocks.at(i);
+		if (oldBlock.flags != newBlock.flags) {
+			int oldFlags = oldBlock.flags.count();
+			int newFlags = newBlock.flags.count();
+			if (oldFlags > newFlags) {
+				beginRemoveRows(createIndex(i, 0, std::numeric_limits<quintptr>::max()), newFlags, oldFlags);
+			} else if (oldFlags < newFlags) {
+				beginInsertRows(createIndex(i, 0, std::numeric_limits<quintptr>::max()), oldFlags, newFlags);
+			}
+			m_cachedBlocks[i] = newBlock;
+			emit dataChanged(createIndex(0, 0, i), createIndex(std::min(oldFlags, newFlags), 0, i));
+			if (oldFlags > newFlags) {
+				endRemoveRows();
+			} else if (oldFlags < newFlags) {
+				endInsertRows();
+			}
+		}
+	}
+	emit dataChanged(createIndex(changed.first, 0, std::numeric_limits<quintptr>::max()),
+	                 createIndex(changed.second, 0, std::numeric_limits<quintptr>::max()));
+
+	if (m_cachedBlocks.count() < newBlocks.count()) {
+		m_cachedBlocks = newBlocks;
+		endInsertRows();
+	}
+}
+
+void MemoryAccessLogModel::setSegment(int segment) {
+	if (m_segment == segment) {
+		return;
+	}
+	beginResetModel();
+	m_segment = segment;
+	m_cachedBlocks.clear();
+	endResetModel();
+}
+
+void MemoryAccessLogModel::setRegion(uint32_t base, uint32_t, bool useSegments) {
+	if (m_base == base) {
+		return;
+	}
+	beginResetModel();
+	m_segment = useSegments ? 0 : -1;
+	m_cachedBlocks.clear();
+	endResetModel();
+}
diff --git a/src/platform/qt/MemoryAccessLogModel.h b/src/platform/qt/MemoryAccessLogModel.h
new file mode 100644
index 000000000..b18f0dee5
--- /dev/null
+++ b/src/platform/qt/MemoryAccessLogModel.h
@@ -0,0 +1,55 @@
+/* Copyright (c) 2013-2025 Jeffrey Pfau
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#pragma once
+
+#include <QAbstractItemModel>
+#include <QVector>
+
+#include "MemoryAccessLogController.h"
+
+struct mCheatDevice;
+struct mCheatSet;
+
+namespace QGBA {
+
+class MemoryAccessLogModel : public QAbstractItemModel {
+Q_OBJECT
+
+public:
+	MemoryAccessLogModel(std::weak_ptr<MemoryAccessLogController> controller, mPlatform platform);
+
+	virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+
+	virtual QModelIndex index(int row, int column, const QModelIndex& parent) const override;
+	virtual QModelIndex parent(const QModelIndex& index) const override;
+
+	virtual int columnCount(const QModelIndex& = QModelIndex()) const override { return 1; }
+	virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+
+public slots:
+	void updateSelection(uint32_t start, uint32_t end);
+	void setSegment(int segment);
+	void setRegion(uint32_t base, uint32_t segmentSize, bool useSegments);
+
+private:
+	struct Block {
+		MemoryAccessLogController::Flags flags;
+		QPair<uint32_t, uint32_t> region;
+
+		bool operator==(const Block& other) const { return flags == other.flags && region == other.region; }
+		bool operator!=(const Block& other) const { return flags != other.flags || region != other.region; }
+	};
+
+	int flagCount(int index) const;
+
+	std::weak_ptr<MemoryAccessLogController> m_controller;
+	mPlatform m_platform;
+	uint32_t m_base = 0;
+	int m_segment = -1;
+	QVector<Block> m_cachedBlocks;
+};
+
+}
diff --git a/src/platform/qt/MemoryAccessLogView.cpp b/src/platform/qt/MemoryAccessLogView.cpp
index 97a0c932e..a06c649ae 100644
--- a/src/platform/qt/MemoryAccessLogView.cpp
+++ b/src/platform/qt/MemoryAccessLogView.cpp
@@ -26,8 +26,10 @@ MemoryAccessLogView::MemoryAccessLogView(std::weak_ptr<MemoryAccessLogController
 	connect(m_ui.exportButton, &QAbstractButton::clicked, this, &MemoryAccessLogView::exportFile);
 	connect(controllerPtr.get(), &MemoryAccessLogController::regionMappingChanged, this, &MemoryAccessLogView::updateRegion);
 	connect(controllerPtr.get(), &MemoryAccessLogController::loggingChanged, this, &MemoryAccessLogView::handleStartStop);
+	connect(controllerPtr.get(), &MemoryAccessLogController::loaded, this, &MemoryAccessLogView::handleLoadUnload);
 
 	bool active = controllerPtr->active();
+	bool loaded = controllerPtr->isLoaded();
 	auto watchedRegions = controllerPtr->watchedRegions();
 
 	QVBoxLayout* regionBox = static_cast<QVBoxLayout*>(m_ui.regionBox->layout());
@@ -46,11 +48,14 @@ MemoryAccessLogView::MemoryAccessLogView(std::weak_ptr<MemoryAccessLogController
 		});
 	}
 
+	handleLoadUnload(loaded);
 	handleStartStop(active);
 }
 
-void MemoryAccessLogView::updateRegion(const QString& internalName, bool) {
-	m_regionBoxes[internalName]->setEnabled(false);
+void MemoryAccessLogView::updateRegion(const QString& internalName, bool checked) {
+	if (checked) {
+		m_regionBoxes[internalName]->setEnabled(false);
+	}
 }
 
 void MemoryAccessLogView::start() {
@@ -68,10 +73,23 @@ void MemoryAccessLogView::stop() {
 		return;
 	}
 	controllerPtr->stop();
-	for (const auto& region : controllerPtr->watchedRegions()) {
-		m_regionBoxes[region]->setEnabled(true);
+}
+
+void MemoryAccessLogView::load() {
+	std::shared_ptr<MemoryAccessLogController> controllerPtr = m_controller.lock();
+	if (!controllerPtr) {
+		return;
 	}
-	m_ui.exportButton->setEnabled(false);
+	controllerPtr->setFile(m_ui.filename->text());
+	controllerPtr->load(m_ui.loadExisting->isChecked());
+}
+
+void MemoryAccessLogView::unload() {
+	std::shared_ptr<MemoryAccessLogController> controllerPtr = m_controller.lock();
+	if (!controllerPtr) {
+		return;
+	}
+	controllerPtr->unload();
 }
 
 void MemoryAccessLogView::selectFile() {
@@ -110,12 +128,26 @@ void MemoryAccessLogView::handleStartStop(bool start) {
 		m_regionBoxes[region]->setChecked(true);
 	}
 
-	if (watchedRegions.contains(QString("cart0"))) {
-		m_ui.exportButton->setEnabled(start);
-	}
-
 	m_ui.start->setDisabled(start);
 	m_ui.stop->setEnabled(start);
-	m_ui.filename->setDisabled(start);
-	m_ui.browse->setDisabled(start);
+	m_ui.unload->setDisabled(start || !controllerPtr->isLoaded());
+}
+
+void MemoryAccessLogView::handleLoadUnload(bool load) {
+	std::shared_ptr<MemoryAccessLogController> controllerPtr = m_controller.lock();
+	if (!controllerPtr) {
+		return;
+	}
+	m_ui.filename->setText(controllerPtr->file());
+
+	if (load && controllerPtr->canExport()) {
+		m_ui.exportButton->setEnabled(true);
+	} else if (!load) {
+		m_ui.exportButton->setEnabled(false);
+	}
+
+	m_ui.load->setDisabled(load);
+	m_ui.unload->setEnabled(load);
+	m_ui.filename->setDisabled(load);
+	m_ui.browse->setDisabled(load);
 }
diff --git a/src/platform/qt/MemoryAccessLogView.h b/src/platform/qt/MemoryAccessLogView.h
index e221a5d9d..f1f7c0388 100644
--- a/src/platform/qt/MemoryAccessLogView.h
+++ b/src/platform/qt/MemoryAccessLogView.h
@@ -34,9 +34,13 @@ private slots:
 	void start();
 	void stop();
 
+	void load();
+	void unload();
+
 	void exportFile();
 
 	void handleStartStop(bool start);
+	void handleLoadUnload(bool load);
 
 private:
 	Ui::MemoryAccessLogView m_ui;
diff --git a/src/platform/qt/MemoryAccessLogView.ui b/src/platform/qt/MemoryAccessLogView.ui
index fe9450f53..9fadd0870 100644
--- a/src/platform/qt/MemoryAccessLogView.ui
+++ b/src/platform/qt/MemoryAccessLogView.ui
@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>385</width>
-    <height>380</height>
+    <width>311</width>
+    <height>387</height>
    </rect>
   </property>
   <property name="windowTitle">
@@ -22,25 +22,25 @@
      <property name="title">
       <string>Log file</string>
      </property>
-     <layout class="QGridLayout" name="gridLayout_2">
-      <item row="0" column="0">
+     <layout class="QGridLayout" name="gridLayout_2" columnstretch="3,1,2">
+      <item row="0" column="0" colspan="2">
        <widget class="QLineEdit" name="filename"/>
       </item>
-      <item row="0" column="1">
+      <item row="0" column="2">
        <widget class="QPushButton" name="browse">
         <property name="text">
          <string>Browse</string>
         </property>
        </widget>
       </item>
-      <item row="1" column="0" colspan="2">
+      <item row="1" column="0" colspan="3">
        <widget class="QCheckBox" name="logExtra">
         <property name="text">
          <string>Log additional information (uses 3× space)</string>
         </property>
        </widget>
       </item>
-      <item row="2" column="0" colspan="2">
+      <item row="2" column="0" colspan="3">
        <widget class="QCheckBox" name="loadExisting">
         <property name="text">
          <string>Load existing file if present</string>
@@ -50,6 +50,23 @@
         </property>
        </widget>
       </item>
+      <item row="3" column="0">
+       <widget class="QPushButton" name="load">
+        <property name="text">
+         <string>Load</string>
+        </property>
+       </widget>
+      </item>
+      <item row="3" column="1" colspan="2">
+       <widget class="QPushButton" name="unload">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+        <property name="text">
+         <string>Unload</string>
+        </property>
+       </widget>
+      </item>
      </layout>
     </widget>
    </item>
@@ -124,9 +141,43 @@
     </hint>
    </hints>
   </connection>
+  <connection>
+   <sender>load</sender>
+   <signal>clicked()</signal>
+   <receiver>QGBA::MemoryAccessLogView</receiver>
+   <slot>load()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>81</x>
+     <y>152</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>192</x>
+     <y>189</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>unload</sender>
+   <signal>clicked()</signal>
+   <receiver>QGBA::MemoryAccessLogView</receiver>
+   <slot>unload()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>226</x>
+     <y>152</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>192</x>
+     <y>189</y>
+    </hint>
+   </hints>
+  </connection>
  </connections>
  <slots>
   <slot>start()</slot>
   <slot>stop()</slot>
+  <slot>load()</slot>
+  <slot>unload()</slot>
  </slots>
 </ui>
diff --git a/src/platform/qt/MemoryView.cpp b/src/platform/qt/MemoryView.cpp
index 4383867e0..dd0bc18a3 100644
--- a/src/platform/qt/MemoryView.cpp
+++ b/src/platform/qt/MemoryView.cpp
@@ -7,6 +7,7 @@
 #include "MemoryView.h"
 
 #include "CoreController.h"
+#include "MemoryAccessLogView.h"
 #include "MemoryDump.h"
 
 #include <mgba/core/core.h>
@@ -107,6 +108,9 @@ QValidator::State IntValidator::validate(QString& input, int&) const {
 MemoryView::MemoryView(std::shared_ptr<CoreController> controller, QWidget* parent)
 	: QWidget(parent)
 	, m_controller(controller)
+#ifdef ENABLE_DEBUGGERS
+	, m_malModel(controller->memoryAccessLogController(), controller->platform())
+#endif
 {
 	m_ui.setupUi(this);
 
@@ -133,20 +137,10 @@ MemoryView::MemoryView(std::shared_ptr<CoreController> controller, QWidget* pare
 		}
 	}
 
-	connect(m_ui.width8, &QAbstractButton::clicked, [this]() {
-		m_ui.hexfield->setAlignment(1);
-		m_sintValidator.setWidth(1);
-		m_uintValidator.setWidth(1);
-	});
-	connect(m_ui.width16, &QAbstractButton::clicked, [this]() {
-		m_ui.hexfield->setAlignment(2);
-		m_sintValidator.setWidth(2);
-		m_uintValidator.setWidth(2);
-	});
-	connect(m_ui.width32, &QAbstractButton::clicked, [this]() {
-		m_ui.hexfield->setAlignment(4);
-		m_sintValidator.setWidth(4);
-		m_uintValidator.setWidth(4);
+	connect(m_ui.width, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), [this](int index) {
+		m_ui.hexfield->setAlignment(1 << index);
+		m_sintValidator.setWidth(1 << index);
+		m_uintValidator.setWidth(1 << index);
 	});
 	connect(m_ui.setAddress, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
 	        this, static_cast<void (MemoryView::*)(uint32_t)>(&MemoryView::jumpToAddress));
@@ -199,6 +193,22 @@ MemoryView::MemoryView(std::shared_ptr<CoreController> controller, QWidget* pare
 		}
 		update();
 	});
+
+#ifdef ENABLE_DEBUGGERS
+	connect(m_ui.hexfield, &MemoryModel::selectionChanged, &m_malModel, &MemoryAccessLogModel::updateSelection);
+	connect(m_ui.segments, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
+	        &m_malModel, &MemoryAccessLogModel::setSegment);
+	connect(m_ui.accessLoggerButton, &QAbstractButton::clicked, this, [this]() {
+		std::weak_ptr<MemoryAccessLogController> controller = m_controller->memoryAccessLogController();
+		MemoryAccessLogView* view = new MemoryAccessLogView(controller);
+		connect(m_controller.get(), &CoreController::stopping, view, &QWidget::close);
+		view->setAttribute(Qt::WA_DeleteOnClose);
+		view->show();
+	});
+	m_ui.accessLog->setModel(&m_malModel);
+#else
+	m_ui.accessLog->hide();
+#endif
 }
 
 void MemoryView::setIndex(int index) {
@@ -216,6 +226,10 @@ void MemoryView::setIndex(int index) {
 	m_ui.segmentColon->setVisible(info.maxSegment > 0);
 	m_ui.segments->setMaximum(info.maxSegment);
 	m_ui.hexfield->setRegion(info.start, info.end - info.start, info.shortName);
+
+#ifdef ENABLE_DEBUGGERS
+	m_malModel.setRegion(info.start, info.segmentStart - info.start, info.maxSegment > 0);
+#endif
 }
 
 void MemoryView::setSegment(int segment) {
@@ -258,7 +272,7 @@ void MemoryView::updateStatus() {
 	mCore* core = m_controller->thread()->core;
 	QByteArray selection(m_ui.hexfield->serialize());
 	QString text(m_ui.hexfield->decodeText(selection));
-	m_ui.stringVal->setText(text);
+	m_ui.stringVal->setPlainText(text);
 
 	if (m_selection.first & (align - 1) || m_selection.second - m_selection.first != align) {
 		m_ui.sintVal->clear();
diff --git a/src/platform/qt/MemoryView.h b/src/platform/qt/MemoryView.h
index f68052a06..d6a5c6f93 100644
--- a/src/platform/qt/MemoryView.h
+++ b/src/platform/qt/MemoryView.h
@@ -8,6 +8,7 @@
 #include <QValidator>
 
 #include "MemoryModel.h"
+#include "MemoryAccessLogModel.h"
 
 #include "ui_MemoryView.h"
 
@@ -54,6 +55,10 @@ private:
 	std::shared_ptr<CoreController> m_controller;
 	QPair<uint32_t, uint32_t> m_region;
 	QPair<uint32_t, uint32_t> m_selection;
+
+#ifdef ENABLE_DEBUGGERS
+	MemoryAccessLogModel m_malModel;
+#endif
 };
 
 }
diff --git a/src/platform/qt/MemoryView.ui b/src/platform/qt/MemoryView.ui
index 45879db88..73407fdd1 100644
--- a/src/platform/qt/MemoryView.ui
+++ b/src/platform/qt/MemoryView.ui
@@ -6,167 +6,14 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>874</width>
-    <height>900</height>
+    <width>708</width>
+    <height>549</height>
    </rect>
   </property>
   <property name="windowTitle">
    <string>Memory</string>
   </property>
-  <layout class="QVBoxLayout" name="verticalLayout">
-   <item>
-    <layout class="QHBoxLayout" name="horizontalLayout">
-     <item>
-      <widget class="QComboBox" name="regions"/>
-     </item>
-     <item>
-      <spacer name="horizontalSpacer">
-       <property name="orientation">
-        <enum>Qt::Horizontal</enum>
-       </property>
-       <property name="sizeHint" stdset="0">
-        <size>
-         <width>0</width>
-         <height>0</height>
-        </size>
-       </property>
-      </spacer>
-     </item>
-     <item>
-      <widget class="QLabel" name="label">
-       <property name="text">
-        <string>Inspect Address:</string>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <widget class="QSpinBox" name="segments">
-       <property name="minimum">
-        <number>-1</number>
-       </property>
-       <property name="maximum">
-        <number>0</number>
-       </property>
-       <property name="displayIntegerBase">
-        <number>16</number>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <widget class="QLabel" name="segmentColon">
-       <property name="text">
-        <string notr="true">:</string>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <widget class="QSpinBox" name="setAddress">
-       <property name="accelerated">
-        <bool>true</bool>
-       </property>
-       <property name="prefix">
-        <string notr="true">0x</string>
-       </property>
-       <property name="maximum">
-        <number>268435455</number>
-       </property>
-       <property name="singleStep">
-        <number>16</number>
-       </property>
-       <property name="displayIntegerBase">
-        <number>16</number>
-       </property>
-      </widget>
-     </item>
-    </layout>
-   </item>
-   <item>
-    <layout class="QHBoxLayout" name="horizontalLayout_3">
-     <item>
-      <widget class="QLabel" name="label_2">
-       <property name="text">
-        <string>Set Alignment:</string>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <spacer name="horizontalSpacer_2">
-       <property name="orientation">
-        <enum>Qt::Horizontal</enum>
-       </property>
-       <property name="sizeHint" stdset="0">
-        <size>
-         <width>0</width>
-         <height>0</height>
-        </size>
-       </property>
-      </spacer>
-     </item>
-     <item>
-      <widget class="QRadioButton" name="width8">
-       <property name="text">
-        <string>&amp;1 Byte</string>
-       </property>
-       <property name="checked">
-        <bool>true</bool>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <spacer name="horizontalSpacer_4">
-       <property name="orientation">
-        <enum>Qt::Horizontal</enum>
-       </property>
-       <property name="sizeHint" stdset="0">
-        <size>
-         <width>0</width>
-         <height>0</height>
-        </size>
-       </property>
-      </spacer>
-     </item>
-     <item>
-      <widget class="QRadioButton" name="width16">
-       <property name="text">
-        <string>&amp;2 Bytes</string>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <spacer name="horizontalSpacer_5">
-       <property name="orientation">
-        <enum>Qt::Horizontal</enum>
-       </property>
-       <property name="sizeHint" stdset="0">
-        <size>
-         <width>0</width>
-         <height>0</height>
-        </size>
-       </property>
-      </spacer>
-     </item>
-     <item>
-      <widget class="QRadioButton" name="width32">
-       <property name="text">
-        <string>&amp;4 Bytes</string>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <spacer name="horizontalSpacer_3">
-       <property name="orientation">
-        <enum>Qt::Horizontal</enum>
-       </property>
-       <property name="sizeHint" stdset="0">
-        <size>
-         <width>0</width>
-         <height>0</height>
-        </size>
-       </property>
-      </spacer>
-     </item>
-    </layout>
-   </item>
+  <layout class="QHBoxLayout" name="horizontalLayout_7" stretch="1,0">
    <item>
     <widget class="QGBA::MemoryModel" name="hexfield" native="true">
      <property name="sizePolicy">
@@ -175,127 +22,296 @@
        <verstretch>0</verstretch>
       </sizepolicy>
      </property>
+     <property name="minimumSize">
+      <size>
+       <width>200</width>
+       <height>0</height>
+      </size>
+     </property>
     </widget>
    </item>
    <item>
-    <layout class="QGridLayout" name="gridLayout">
-     <item row="0" column="1">
-      <layout class="QHBoxLayout" name="horizontalLayout_6">
+    <layout class="QVBoxLayout" name="sidebarLayout" stretch="0,0,0,1,0,1">
+     <item>
+      <widget class="QComboBox" name="regions"/>
+     </item>
+     <item>
+      <layout class="QHBoxLayout" name="addressLayout">
        <item>
-        <widget class="QLabel" name="label_4">
+        <widget class="QLabel" name="address">
          <property name="text">
-          <string>Unsigned Integer:</string>
+          <string>Address:</string>
          </property>
         </widget>
        </item>
        <item>
-        <widget class="QLineEdit" name="uintVal">
-         <property name="maxLength">
-          <number>10</number>
+        <spacer name="horizontalSpacer">
+         <property name="orientation">
+          <enum>Qt::Horizontal</enum>
          </property>
-         <property name="readOnly">
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>0</width>
+           <height>0</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+       <item>
+        <widget class="QSpinBox" name="segments">
+         <property name="minimum">
+          <number>-1</number>
+         </property>
+         <property name="maximum">
+          <number>0</number>
+         </property>
+         <property name="displayIntegerBase">
+          <number>16</number>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLabel" name="segmentColon">
+         <property name="text">
+          <string notr="true">:</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QSpinBox" name="setAddress">
+         <property name="minimumSize">
+          <size>
+           <width>100</width>
+           <height>0</height>
+          </size>
+         </property>
+         <property name="accelerated">
           <bool>true</bool>
          </property>
+         <property name="prefix">
+          <string notr="true">0x</string>
+         </property>
+         <property name="maximum">
+          <number>268435455</number>
+         </property>
+         <property name="singleStep">
+          <number>16</number>
+         </property>
+         <property name="displayIntegerBase">
+          <number>16</number>
+         </property>
         </widget>
        </item>
       </layout>
      </item>
-     <item row="0" column="0">
-      <layout class="QHBoxLayout" name="horizontalLayout_2">
+     <item>
+      <layout class="QHBoxLayout" name="alignmentLayout">
        <item>
-        <widget class="QLabel" name="label_3">
+        <widget class="QLabel" name="alignmentLabel">
          <property name="text">
-          <string>Signed Integer:</string>
+          <string>Alignment:</string>
          </property>
         </widget>
        </item>
        <item>
-        <widget class="QLineEdit" name="sintVal">
-         <property name="maxLength">
-          <number>11</number>
+        <widget class="QComboBox" name="width">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
          </property>
-         <property name="readOnly">
-          <bool>true</bool>
+         <item>
+          <property name="text">
+           <string>1 Byte</string>
+          </property>
+         </item>
+         <item>
+          <property name="text">
+           <string>2 Bytes</string>
+          </property>
+         </item>
+         <item>
+          <property name="text">
+           <string>4 Bytes</string>
+          </property>
+         </item>
+        </widget>
+       </item>
+      </layout>
+     </item>
+     <item>
+      <widget class="QGroupBox" name="data">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Ignored" vsizetype="Preferred">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="title">
+        <string/>
+       </property>
+       <layout class="QGridLayout" name="gridLayout_9">
+        <item row="0" column="0">
+         <widget class="QLabel" name="sintLabel">
+          <property name="text">
+           <string>Signed:</string>
+          </property>
+         </widget>
+        </item>
+        <item row="0" column="1">
+         <widget class="QLineEdit" name="sintVal">
+          <property name="maxLength">
+           <number>11</number>
+          </property>
+          <property name="readOnly">
+           <bool>true</bool>
+          </property>
+          <property name="placeholderText">
+           <string notr="true"/>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="0">
+         <widget class="QLabel" name="uintLabel">
+          <property name="text">
+           <string>Unsigned:</string>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="1">
+         <widget class="QLineEdit" name="uintVal">
+          <property name="maxLength">
+           <number>10</number>
+          </property>
+          <property name="readOnly">
+           <bool>true</bool>
+          </property>
+          <property name="placeholderText">
+           <string notr="true"/>
+          </property>
+         </widget>
+        </item>
+        <item row="2" column="0">
+         <widget class="QLabel" name="stringLabel">
+          <property name="text">
+           <string>String:</string>
+          </property>
+         </widget>
+        </item>
+        <item row="2" column="1" alignment="Qt::AlignRight">
+         <widget class="QPushButton" name="loadTBL">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="text">
+           <string>Load TBL</string>
+          </property>
+         </widget>
+        </item>
+        <item row="3" column="0" colspan="2">
+         <widget class="QPlainTextEdit" name="stringVal">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="plainText">
+           <string notr="true"/>
+          </property>
+          <property name="placeholderText">
+           <string notr="true"/>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+     </item>
+     <item>
+      <layout class="QGridLayout" name="buttons">
+       <item row="0" column="0">
+        <widget class="QPushButton" name="copy">
+         <property name="text">
+          <string>Copy Selection</string>
+         </property>
+        </widget>
+       </item>
+       <item row="0" column="1">
+        <widget class="QPushButton" name="save">
+         <property name="text">
+          <string>Save Selection</string>
+         </property>
+        </widget>
+       </item>
+       <item row="1" column="0">
+        <widget class="QPushButton" name="paste">
+         <property name="text">
+          <string>Paste</string>
+         </property>
+        </widget>
+       </item>
+       <item row="1" column="1">
+        <widget class="QPushButton" name="saveRange">
+         <property name="text">
+          <string>Save Range</string>
+         </property>
+        </widget>
+       </item>
+       <item row="2" column="0" colspan="2">
+        <widget class="QPushButton" name="load">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="text">
+          <string>Load</string>
          </property>
         </widget>
        </item>
       </layout>
      </item>
-     <item row="1" column="0" colspan="2">
-      <layout class="QHBoxLayout" name="horizontalLayout_4">
-       <item>
-        <widget class="QLabel" name="label_5">
-         <property name="text">
-          <string>String:</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QLineEdit" name="stringVal">
-         <property name="readOnly">
-          <bool>true</bool>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QPushButton" name="loadTBL">
-         <property name="text">
-          <string>Load TBL</string>
-         </property>
-        </widget>
-       </item>
-      </layout>
-     </item>
-    </layout>
-   </item>
-   <item>
-    <layout class="QHBoxLayout" name="horizontalLayout_5">
      <item>
-      <widget class="QPushButton" name="copy">
-       <property name="text">
-        <string>Copy Selection</string>
+      <widget class="QGroupBox" name="verticalGroupBox_2">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
        </property>
-      </widget>
-     </item>
-     <item>
-      <widget class="QPushButton" name="paste">
-       <property name="text">
-        <string>Paste</string>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <spacer name="horizontalSpacer_7">
-       <property name="orientation">
-        <enum>Qt::Horizontal</enum>
-       </property>
-       <property name="sizeHint" stdset="0">
-        <size>
-         <width>40</width>
-         <height>20</height>
-        </size>
-       </property>
-      </spacer>
-     </item>
-     <item>
-      <widget class="QPushButton" name="save">
-       <property name="text">
-        <string>Save Selection</string>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <widget class="QPushButton" name="saveRange">
-       <property name="text">
-        <string>Save Range</string>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <widget class="QPushButton" name="load">
-       <property name="text">
-        <string>Load</string>
+       <property name="title">
+        <string>Selected address accesses</string>
        </property>
+       <layout class="QVBoxLayout" name="verticalLayout">
+        <item>
+         <widget class="QTreeView" name="accessLog">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="textElideMode">
+           <enum>Qt::ElideNone</enum>
+          </property>
+          <attribute name="headerVisible">
+           <bool>false</bool>
+          </attribute>
+         </widget>
+        </item>
+        <item>
+         <widget class="QPushButton" name="accessLoggerButton">
+          <property name="text">
+           <string>Logging configuration</string>
+          </property>
+         </widget>
+        </item>
+       </layout>
       </widget>
      </item>
     </layout>
@@ -310,23 +326,6 @@
    <container>1</container>
   </customwidget>
  </customwidgets>
- <tabstops>
-  <tabstop>regions</tabstop>
-  <tabstop>segments</tabstop>
-  <tabstop>setAddress</tabstop>
-  <tabstop>width8</tabstop>
-  <tabstop>width16</tabstop>
-  <tabstop>width32</tabstop>
-  <tabstop>sintVal</tabstop>
-  <tabstop>uintVal</tabstop>
-  <tabstop>stringVal</tabstop>
-  <tabstop>loadTBL</tabstop>
-  <tabstop>copy</tabstop>
-  <tabstop>paste</tabstop>
-  <tabstop>save</tabstop>
-  <tabstop>saveRange</tabstop>
-  <tabstop>load</tabstop>
- </tabstops>
  <resources/>
  <connections/>
 </ui>
diff --git a/src/platform/qt/SettingsView.cpp b/src/platform/qt/SettingsView.cpp
index f4eea01db..967cf1a4d 100644
--- a/src/platform/qt/SettingsView.cpp
+++ b/src/platform/qt/SettingsView.cpp
@@ -437,9 +437,9 @@ void SettingsView::setShaderSelector(ShaderSelector* shaderSelector) {
 		QObject::disconnect(m_shader, nullptr, this, nullptr);
 	}
 	m_shader = shaderSelector;
-	QObject::connect(this, &SettingsView::saveSettingsRequested, m_shader, &ShaderSelector::saveSettings);
-	QObject::connect(m_ui.buttonBox, &QDialogButtonBox::rejected, m_shader, &ShaderSelector::revert);
 	if (shaderSelector) {
+		QObject::connect(this, &SettingsView::saveSettingsRequested, m_shader, &ShaderSelector::saveSettings);
+		QObject::connect(m_ui.buttonBox, &QDialogButtonBox::rejected, m_shader, &ShaderSelector::revert);
 		addPage(tr("Shaders"), m_shader, Page::SHADERS);
 	} else {
 		addPage(tr("Shaders"), m_dummyShader, Page::SHADERS);
@@ -747,7 +747,7 @@ void SettingsView::reloadConfig() {
 	loadSetting("patchPath", m_ui.patchPath);
 	loadSetting("cheatsPath", m_ui.cheatsPath);
 	loadSetting("showLibrary", m_ui.showLibrary);
-	loadSetting("preload", m_ui.preload);
+	loadSetting("preload", m_ui.preload, true);
 	loadSetting("showFps", m_ui.showFps, true);
 	loadSetting("cheatAutoload", m_ui.cheatAutoload, true);
 	loadSetting("cheatAutosave", m_ui.cheatAutosave, true);
diff --git a/src/platform/qt/SettingsView.h b/src/platform/qt/SettingsView.h
index 1e45ec87a..710456958 100644
--- a/src/platform/qt/SettingsView.h
+++ b/src/platform/qt/SettingsView.h
@@ -7,10 +7,12 @@
 
 #include <QDialog>
 #include <QMap>
+#include <QPointer>
 #include <QTimer>
 
 #include "ColorPicker.h"
 #include "LogConfigModel.h"
+#include "ShaderSelector.h"
 
 #include <mgba/core/core.h>
 
@@ -25,7 +27,6 @@ namespace QGBA {
 class ConfigController;
 class InputController;
 class ShortcutController;
-class ShaderSelector;
 
 class SettingsView : public QDialog {
 Q_OBJECT
@@ -80,7 +81,7 @@ private:
 
 	ConfigController* m_controller;
 	InputController* m_input;
-	ShaderSelector* m_shader = nullptr;
+	QPointer<ShaderSelector> m_shader;
 	QLabel* m_dummyShader;
 	LogConfigModel m_logModel;
 	QTimer m_checkTimer;
diff --git a/src/platform/qt/SettingsView.ui b/src/platform/qt/SettingsView.ui
index 8539e6bdb..bce56f730 100644
--- a/src/platform/qt/SettingsView.ui
+++ b/src/platform/qt/SettingsView.ui
@@ -1263,6 +1263,9 @@
          <property name="text">
           <string>Preload entire ROM into memory</string>
          </property>
+         <property name="checked">
+          <bool>true</bool>
+         </property>
         </widget>
        </item>
        <item row="10" column="1">
diff --git a/src/platform/qt/Window.cpp b/src/platform/qt/Window.cpp
index 1be4a998c..a224c3ba4 100644
--- a/src/platform/qt/Window.cpp
+++ b/src/platform/qt/Window.cpp
@@ -972,7 +972,12 @@ void Window::gameStopped() {
 #endif
 	}
 
-	m_controller.reset();
+	std::shared_ptr<CoreController> controller;
+	m_controller.swap(controller);
+	QTimer::singleShot(0, this, [controller]() {
+		// Destroy the controller after everything else has cleaned up
+		Q_UNUSED(controller);
+	});
 	detachWidget();
 	updateTitle();
 
@@ -2155,11 +2160,6 @@ void Window::setController(CoreController* controller, const QString& fname) {
 	connect(m_controller.get(), &CoreController::started, this, &Window::gameStarted);
 	connect(m_controller.get(), &CoreController::started, GBAApp::app(), &GBAApp::suspendScreensaver);
 	connect(m_controller.get(), &CoreController::stopping, this, &Window::gameStopped);
-	{
-		connect(m_controller.get(), &CoreController::stopping, [this]() {
-			m_controller.reset();
-		});
-	}
 	connect(m_controller.get(), &CoreController::stopping, GBAApp::app(), &GBAApp::resumeScreensaver);
 	connect(m_controller.get(), &CoreController::paused, this, &Window::updateFrame);
 
diff --git a/src/platform/test/fuzz-main.c b/src/platform/test/fuzz-main.c
index 143edaba1..5fdfff8a3 100644
--- a/src/platform/test/fuzz-main.c
+++ b/src/platform/test/fuzz-main.c
@@ -130,6 +130,7 @@ int main(int argc, char** argv) {
 		mDebuggerAccessLoggerInit(&accessLog);
 		mDebuggerAttachModule(&debugger, &accessLog.d);
 		mDebuggerAccessLoggerOpen(&accessLog, vf, O_RDWR);
+		mDebuggerAccessLoggerStart(&accessLog);
 		hasDebugger = true;
 	}
 
diff --git a/src/sm83/debugger/memory-debugger.c b/src/sm83/debugger/memory-debugger.c
index a35c2afeb..345dc50bf 100644
--- a/src/sm83/debugger/memory-debugger.c
+++ b/src/sm83/debugger/memory-debugger.c
@@ -61,6 +61,7 @@ static void _checkWatchpoints(struct SM83Debugger* debugger, uint16_t address, e
 			info.type.wp.newValue = newValue;
 			info.type.wp.watchType = watchpoint->type;
 			info.type.wp.accessType = type;
+			info.type.wp.accessSource = debugger->cpu->memory.accessSource;
 			info.address = address;
 			info.segment = debugger->originalMemory.currentSegment(debugger->cpu, address);
 			info.width = 1;