diff --git a/.cirrus.yml b/.cirrus.yml
index d3ca995ed..b7532256a 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -1,33 +1,9 @@
-linux-x86_64-binaries_task:
-    container:
-        image: ubuntu:latest
-
-    setup_script:
-        - apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y install build-essential libgtk2.0-dev libpulse-dev mesa-common-dev libcairo2-dev libsdl2-dev libxv-dev libao-dev libopenal-dev libudev-dev zip
-
-    compile_script:
-        - make -C bsnes local=false
-
-    package_script:
-        - mkdir bsnes-nightly
-        - mkdir bsnes-nightly/Database
-        - mkdir bsnes-nightly/Firmware
-        - cp -a bsnes/out/bsnes bsnes-nightly/bsnes
-        - cp -a bsnes/Database/* bsnes-nightly/Database
-        - cp -a shaders bsnes-nightly/Shaders
-        - cp -a GPLv3.txt bsnes-nightly
-        - cp -a extras/* bsnes-nightly
-        - zip -r bsnes-nightly.zip bsnes-nightly
-
-    bsnes-nightly_artifacts:
-        path: "bsnes-nightly.zip"
-
 freebsd-x86_64-binaries_task:
     freebsd_instance:
         image_family: freebsd-12-2
 
     setup_script:
-        - pkg install --yes gmake gdb gcc8 pkgconf sdl2 openal-soft gtk2 libXv zip
+        - pkg install --yes gmake gdb gcc8 pkgconf sdl2 openal-soft gtk3 gtksourceview3 libXv zip
 
     compile_script:
         - gmake -C bsnes local=false
@@ -45,43 +21,3 @@ freebsd-x86_64-binaries_task:
 
     bsnes-nightly_artifacts:
         path: "bsnes-nightly.zip"
-
-windows-x86_64-binaries_task:
-    container:
-        image: ubuntu:latest
-
-    setup_script:
-        - apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y install build-essential mingw-w64 zip
-
-    compile_script:
-        - make -C bsnes local=false platform=windows compiler="x86_64-w64-mingw32-g++" windres="x86_64-w64-mingw32-windres"
-
-    package_script:
-        - mkdir bsnes-nightly
-        - mkdir bsnes-nightly/Database
-        - mkdir bsnes-nightly/Firmware
-        - cp -a bsnes/out/bsnes.exe bsnes-nightly/bsnes.exe
-        - cp -a bsnes/Database/* bsnes-nightly/Database
-        - cp -a shaders bsnes-nightly/Shaders
-        - cp -a GPLv3.txt bsnes-nightly
-        - cp -a extras/* bsnes-nightly
-        - zip -r bsnes-nightly.zip bsnes-nightly
-
-    bsnes-nightly_artifacts:
-        path: "bsnes-nightly.zip"
-
-macOS-aarch64-binaries_task:
-    macos_instance:
-        image: ghcr.io/cirruslabs/macos-ventura-base:latest
-    compile_script:
-        - make -C bsnes local=false
-
-    package_script:
-        - mkdir bsnes-nightly
-        - cp -a bsnes/out/bsnes.app bsnes-nightly
-        - cp -a GPLv3.txt bsnes-nightly
-        - cp -a extras/* bsnes-nightly
-        - zip -r bsnes-nightly.zip bsnes-nightly
-
-    bsnes-nightly_artifacts:
-        path: "bsnes-nightly.zip"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 08fb83c43..d5f1772ef 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -5,6 +5,8 @@ on:
     tags: [ 'v*' ]
   pull_request:
     branches: [ master ]
+permissions:
+  contents: write
 jobs:
   build:
     strategy:
@@ -18,33 +20,33 @@ jobs:
           version: latest
     runs-on: ${{ matrix.os.name }}-${{ matrix.os.version }}
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v4
     - name: Install Dependencies
       if: matrix.os.name == 'ubuntu'
       run: |
         sudo apt-get update -y -qq
-        sudo apt-get install \
-          libgtk2.0-dev libpulse-dev mesa-common-dev libcairo2-dev \
-          libsdl2-dev libxv-dev libao-dev libopenal-dev libudev-dev
+        sudo apt-get install libsdl2-dev libgtk-3-dev gtksourceview-3.0 libao-dev libopenal-dev
     - name: Make
       run: make -j4 -C bsnes local=false
     - name: Upload
-      uses: actions/upload-artifact@v2
+      uses: actions/upload-artifact@v3
       with:
         name: bsnes-${{ matrix.os.name }}
         path: bsnes/out/bsnes*
 
   release:
     if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')
+    # Prevent multiple conflicting release jobs from running at once.
+    concurrency: release-${{ github.ref == 'refs/heads/master' && 'nightly' || github.ref }}
     runs-on: ubuntu-latest
     needs:
     - build
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v4
       with:
         path: 'src'
     - name: Download Artifacts
-      uses: actions/download-artifact@v2
+      uses: actions/download-artifact@v3
       with:
         path: 'bin'
     - name: Package Artifacts
@@ -85,103 +87,53 @@ jobs:
 
           cd -
         done
-    - name: Create Release
-      id: release
-      env:
-        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+    - name: Calculate release info
+      id: relinfo
       run: |
-        set -eu
-        github_rest()
-        {
-          local method="${1}"
-          local url="https://api.github.com${2}"
-          shift 2
-          >&2 echo "${method} ${url}"
-          curl \
-            --fail \
-            -H "Accept: application/vnd.github.v3+json" \
-            -H "Authorization: token ${GITHUB_TOKEN}" \
-            -X "${method}" \
-            "${url}" \
-            "$@"
-        }
-        github_get_release_id_for_tag()
-        {
-          payload=$(github_rest GET "/repos/${GITHUB_REPOSITORY}/releases/tags/${1}") || return
-          echo "${payload}" | jq .id
-        }
-        github_delete_release_by_id()
-        {
-          github_rest DELETE "/repos/${GITHUB_REPOSITORY}/releases/${1}"
-        }
-        github_delete_tag()
-        {
-          github_rest DELETE "/repos/${GITHUB_REPOSITORY}/git/refs/tags/${1}"
-        }
-        github_create_release()
-        {
-          local payload="{
-            \"tag_name\": \"${1}\",
-            \"target_commitish\": \"${2}\",
-            \"name\": \"${3}\",
-            \"body\": \"${4}\",
-            \"draft\": ${5},
-            \"prerelease\": ${6}
-          }"
-          github_rest POST "/repos/${GITHUB_REPOSITORY}/releases" -d "${payload}"
-        }
-        make_nightly_release()
-        {
-          github_create_release \
-            nightly \
-            "${GITHUB_SHA}" \
-            "bsnes nightly $(date +"%Y-%m-%d")" \
-            "Auto-generated nightly release on $(date -u +"%Y-%m-%d %T %Z")" \
-            false \
-            true
-        }
-        make_version_release()
-        {
-          github_create_release \
-            "${1}" \
-            "${GITHUB_SHA}" \
-            "bsnes ${1}" \
-            "This is bsnes ${1}, released on $(date +"%Y-%m-%d")." \
-            false \
-            false
-        }
-        case ${GITHUB_REF} in
-          refs/tags/*)
-            # Create a new version release using the current revision.
-            echo "UPLOAD_URL=$(make_version_release ${GITHUB_REF#refs/tags/} | jq -r .upload_url)" >> $GITHUB_ENV
-            ;;
-          refs/heads/master)
-            # Check for an existing nightly release.
-            { release_id=$(github_get_release_id_for_tag nightly); status=$?; } || true
-            # Delete existing nightly release if it exists.
-            case ${status} in
-              0)
-                  github_delete_release_by_id "${release_id}"
-                  # Deleting the 'nightly' release doesn't delete
-                  # the 'nightly' tag, so let's do it manually.
-                  github_delete_tag nightly
-                  ;;
-              22) >&2 echo "No current nightly release; skipping tag deletion." ;;
-              *) >&2 echo "API call failed unexpectedly." && exit 1 ;;
-            esac
-            # Create a new nightly release using the current revision.
-            echo "UPLOAD_URL=$(make_nightly_release | jq -r .upload_url)" >> $GITHUB_ENV
-            ;;
-        esac
-    - name: Upload bsnes-ubuntu
-      uses: actions/upload-release-asset@v1
-      env: { GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' }
-      with: { upload_url: '${{ env.UPLOAD_URL }}', asset_path: 'bsnes-ubuntu.zip', asset_name: 'bsnes-ubuntu.zip', asset_content_type: 'application/zip' }
-    - name: Upload bsnes-windows
-      uses: actions/upload-release-asset@v1
-      env: { GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' }
-      with: { upload_url: '${{ env.UPLOAD_URL }}', asset_path: 'bsnes-windows.zip', asset_name: 'bsnes-windows.zip', asset_content_type: 'application/zip' }
-    - name: Upload bsnes-macos
-      uses: actions/upload-release-asset@v1
-      env: { GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' }
-      with: { upload_url: '${{ env.UPLOAD_URL }}', asset_path: 'bsnes-macos.zip', asset_name: 'bsnes-macos.zip', asset_content_type: 'application/zip' }
+        echo "datetime=$(date -u +"%Y-%m-%d %T %Z")" >> $GITHUB_OUTPUT
+        echo "date=$(date +"%Y-%m-%d")" >> $GITHUB_OUTPUT
+    - name: Delete old nightly release
+      uses: actions/github-script@v7
+      id: release
+      if: ${{!startsWith(github.ref, 'refs/tags/')}}
+      with:
+        retries: 3
+        script: |
+          const {owner, repo} = context.repo;
+          try {
+            const release = await github.rest.repos.getReleaseByTag({owner, repo, tag: "nightly"});
+            if (release && release.status === 200) {
+              await github.rest.repos.deleteRelease({owner, repo, release_id: release.data.id});
+            }
+          } catch (e) {
+            console.log(`error deleting old release: ${e}`);
+          }
+          try {
+            await github.rest.git.deleteRef({owner, repo, ref: "tags/nightly"});
+          } catch (e) {
+            console.log(`error trying to delete ref: ${e}`);
+          }
+          await github.rest.git.createTag({
+            owner,
+            repo,
+            tag: "nightly",
+            message: "nightly release",
+            object: context.sha,
+            type: "commit",
+          })
+    - name: Create nightly release
+      uses: softprops/action-gh-release@v1
+      if: ${{!startsWith(github.ref, 'refs/tags/')}}
+      with:
+        tag_name: nightly
+        name: bsnes nightly ${{steps.relinfo.outputs.date}}
+        body: Auto-generated nightly release on ${{steps.relinfo.outputs.datetime}}
+        files: bsnes*.zip
+        prerelease: true
+    - name: Create version release
+      uses: softprops/action-gh-release@v1
+      if: ${{startsWith(github.ref, 'refs/tags/')}}
+      with:
+        name: bsnes ${{github.ref_name}}
+        body: This is bsnes ${{github.ref_name}}, released on ${{steps.relinfo.outputs.date}}.
+        files: bsnes*.zip
diff --git a/README.md b/README.md
index a121c4574..1f2aca717 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ bsnes
 ![bsnes logo](bsnes/target-bsnes/resource/logo.png)
 
 bsnes is a multi-platform Super Nintendo (Super Famicom) emulator, originally
-developed by [Near](https://near.sh), which focuses on performance,
+developed by Near, which focuses on performance,
 features, and ease of use.
 
 Unique Features
@@ -62,11 +62,10 @@ Links
 Nightly Builds
 --------------
 
-  - [Download](https://github.com/bsnes-emu/bsnes/releases/tag/nightly)
-  - ![Build status](https://api.cirrus-ci.com/github/bsnes-emu/bsnes.svg?task=windows-x86_64-binaries)
-  - ![Build status](https://api.cirrus-ci.com/github/bsnes-emu/bsnes.svg?task=macOS-x86_64-binaries)
-  - ![Build status](https://api.cirrus-ci.com/github/bsnes-emu/bsnes.svg?task=linux-x86_64-binaries)
-  - ![Build status](https://api.cirrus-ci.com/github/bsnes-emu/bsnes.svg?task=freebsd-x86_64-binaries)
+  - [Windows](https://github.com/bsnes-emu/bsnes/releases/download/nightly/bsnes-windows.zip)
+  - [macOS](https://github.com/bsnes-emu/bsnes/releases/download/nightly/bsnes-macos.zip)
+  - [Linux](https://github.com/bsnes-emu/bsnes/releases/download/nightly/bsnes-ubuntu.zip)
+  - [FreeBSD](https://api.cirrus-ci.com/v1/artifact/github/bsnes-emu/bsnes/freebsd-x86_64-binaries/bsnes-nightly/bsnes-nightly.zip)
 
 Preview
 -------
diff --git a/bsnes/processor/wdc65816/instructions-read.cpp b/bsnes/processor/wdc65816/instructions-read.cpp
index e364725b7..957223d24 100755
--- a/bsnes/processor/wdc65816/instructions-read.cpp
+++ b/bsnes/processor/wdc65816/instructions-read.cpp
@@ -113,8 +113,8 @@ auto WDC65816::instructionIndexedIndirectRead8(alu8 op) -> void {
   U.l = fetch();
   idle2();
   idle();
-  V.l = readDirect(U.l + X.w + 0);
-  V.h = readDirect(U.l + X.w + 1);
+  V.l = readDirectX(U.l + X.w, 0);
+  V.h = readDirectX(U.l + X.w, 1);
 L W.l = readBank(V.w + 0);
   alu(W.l);
 }
@@ -123,8 +123,8 @@ auto WDC65816::instructionIndexedIndirectRead16(alu16 op) -> void {
   U.l = fetch();
   idle2();
   idle();
-  V.l = readDirect(U.l + X.w + 0);
-  V.h = readDirect(U.l + X.w + 1);
+  V.l = readDirectX(U.l + X.w, 0);
+  V.h = readDirectX(U.l + X.w, 1);
   W.l = readBank(V.w + 0);
 L W.h = readBank(V.w + 1);
   alu(W.w);
diff --git a/bsnes/processor/wdc65816/instructions-write.cpp b/bsnes/processor/wdc65816/instructions-write.cpp
index f2851ed68..a3042d862 100755
--- a/bsnes/processor/wdc65816/instructions-write.cpp
+++ b/bsnes/processor/wdc65816/instructions-write.cpp
@@ -90,8 +90,8 @@ auto WDC65816::instructionIndexedIndirectWrite8() -> void {
   U.l = fetch();
   idle2();
   idle();
-  V.l = readDirect(U.l + X.w + 0);
-  V.h = readDirect(U.l + X.w + 1);
+  V.l = readDirectX(U.l + X.w, 0);
+  V.h = readDirectX(U.l + X.w, 1);
 L writeBank(V.w + 0, A.l);
 }
 
@@ -99,8 +99,8 @@ auto WDC65816::instructionIndexedIndirectWrite16() -> void {
   U.l = fetch();
   idle2();
   idle();
-  V.l = readDirect(U.l + X.w + 0);
-  V.h = readDirect(U.l + X.w + 1);
+  V.l = readDirectX(U.l + X.w, 0);
+  V.h = readDirectX(U.l + X.w, 1);
   writeBank(V.w + 0, A.l);
 L writeBank(V.w + 1, A.h);
 }
diff --git a/bsnes/processor/wdc65816/memory.cpp b/bsnes/processor/wdc65816/memory.cpp
index 4f6037407..7cfb2cc05 100644
--- a/bsnes/processor/wdc65816/memory.cpp
+++ b/bsnes/processor/wdc65816/memory.cpp
@@ -59,6 +59,13 @@ auto WDC65816::writeDirect(uint address, uint8 data) -> void {
   write(D.w + address & 0xffff, data);
 }
 
+auto WDC65816::readDirectX(uint address, uint offset) -> uint8 {
+  // The (direct,X) addressing mode has a bug in which the high byte is
+  // wrapped within the page if E = 1 and D&0xFF != 0.
+  if(EF && D.l) return read(((D.w + address) & 0xffff00) | ((D.w + address + offset) & 0xff));
+  else return readDirect(address + offset);
+}
+
 auto WDC65816::readDirectN(uint address) -> uint8 {
   return read(D.w + address & 0xffff);
 }
diff --git a/bsnes/processor/wdc65816/wdc65816.hpp b/bsnes/processor/wdc65816/wdc65816.hpp
index 3821794d0..102bd512b 100755
--- a/bsnes/processor/wdc65816/wdc65816.hpp
+++ b/bsnes/processor/wdc65816/wdc65816.hpp
@@ -58,6 +58,7 @@ struct WDC65816 {
   alwaysinline auto pushN(uint8 data) -> void;
   alwaysinline auto readDirect(uint address) -> uint8;
   alwaysinline auto writeDirect(uint address, uint8 data) -> void;
+  alwaysinline auto readDirectX(uint address, uint offset) -> uint8;
   alwaysinline auto readDirectN(uint address) -> uint8;
   alwaysinline auto readBank(uint address) -> uint8;
   alwaysinline auto writeBank(uint address, uint8 data) -> void;
diff --git a/bsnes/sfc/coprocessor/sa1/bwram.cpp b/bsnes/sfc/coprocessor/sa1/bwram.cpp
index 7d6059c7e..967fdccc7 100644
--- a/bsnes/sfc/coprocessor/sa1/bwram.cpp
+++ b/bsnes/sfc/coprocessor/sa1/bwram.cpp
@@ -39,6 +39,8 @@ auto SA1::BWRAM::writeCPU(uint address, uint8 data) -> void {
     address = sa1.mmio.sbm * 0x2000 + (address & 0x1fff);
   }
 
+  //note: BW-RAM protection works only when both SWEN and CWEN are disabled.
+  if(!sa1.mmio.swen && !sa1.mmio.cwen && (address & 0x3ffff) < 0x100 << sa1.mmio.bwp) return;
   return write(address, data);
 }
 
@@ -71,6 +73,15 @@ auto SA1::BWRAM::readLinear(uint address, uint8 data) -> uint8 {
 }
 
 auto SA1::BWRAM::writeLinear(uint address, uint8 data) -> void {
+  //note: BW-RAM protection works only when both SWEN and CWEN are disabled.
+  //this is required for Kirby's Dream Land 3 to work:
+  //* BWPA = 02 (protect 400000-4003ff)
+  //* SWEN = 80 (writes enabled)
+  //* CWEN = 00 (writes disabled)
+  //KDL3 proceeds to write to 4001ax and 40032x which must succeed.
+  //note: BWPA also affects SA-1 protection
+  if(!sa1.mmio.swen && !sa1.mmio.cwen && (address & 0x3ffff) < 0x100 << sa1.mmio.bwp) return;
+
   return write(address, data);
 }
 
diff --git a/bsnes/sfc/coprocessor/sa1/io.cpp b/bsnes/sfc/coprocessor/sa1/io.cpp
index 12e5e8f77..ae03007a2 100644
--- a/bsnes/sfc/coprocessor/sa1/io.cpp
+++ b/bsnes/sfc/coprocessor/sa1/io.cpp
@@ -108,8 +108,16 @@ auto SA1::writeIOCPU(uint address, uint8 data) -> void {
   //(CCNT) SA-1 control
   case 0x2200: {
     if(mmio.sa1_resb && !(data & 0x20)) {
-      //reset SA-1 CPU (PC bank set to 0x00)
+      //reset SA-1 CPU (PC bank and data bank set to 0x00, clear STP status)
       r.pc.d = mmio.crv;
+      r.b    = 0x00;
+      r.stp  = false;
+      //todo: probably needs a SA-1 CPU reset
+      //reset r.s, r.e, r.wai ...
+
+      //reset io status
+      //todo: reset timing is unknown, CIWP is set to 0 at reset
+      mmio.ciwp = 0x00;
     }
 
     mmio.sa1_irq  = (data & 0x80);
diff --git a/bsnes/sfc/coprocessor/sa1/memory.cpp b/bsnes/sfc/coprocessor/sa1/memory.cpp
index d82d974b8..f37d3b4ab 100644
--- a/bsnes/sfc/coprocessor/sa1/memory.cpp
+++ b/bsnes/sfc/coprocessor/sa1/memory.cpp
@@ -37,7 +37,7 @@ auto SA1::read(uint address) -> uint8 {
   }
 
   if((address & 0x40e000) == 0x006000  //00-3f,80-bf:6000-7fff
-  || (address & 0xf00000) == 0x400000  //40-4f:0000-ffff
+  || (address & 0xe00000) == 0x400000  //40-5f:0000-ffff
   || (address & 0xf00000) == 0x600000  //60-6f:0000-ffff
   ) {
     step();
@@ -81,7 +81,7 @@ auto SA1::write(uint address, uint8 data) -> void {
   }
 
   if((address & 0x40e000) == 0x006000  //00-3f,80-bf:6000-7fff
-  || (address & 0xf00000) == 0x400000  //40-4f:0000-ffff
+  || (address & 0xe00000) == 0x400000  //40-5f:0000-ffff
   || (address & 0xf00000) == 0x600000  //60-6f:0000-ffff
   ) {
     step();
diff --git a/hiro/GNUmakefile b/hiro/GNUmakefile
index 94736db51..b86a42451 100755
--- a/hiro/GNUmakefile
+++ b/hiro/GNUmakefile
@@ -32,7 +32,7 @@ endif
 
 ifneq ($(filter $(platform),linux bsd),)
   ifeq ($(hiro),)
-    hiro := gtk2
+    hiro := gtk3
   endif
 
   ifeq ($(hiro),gtk2)
diff --git a/hiro/gtk/application.cpp b/hiro/gtk/application.cpp
index fe44a3830..58628ea81 100755
--- a/hiro/gtk/application.cpp
+++ b/hiro/gtk/application.cpp
@@ -82,6 +82,8 @@ auto pApplication::state() -> State& {
 
 auto pApplication::initialize() -> void {
   #if defined(DISPLAY_XORG)
+  // If running on Wayland, force usage of XWayland
+  setenv("GDK_BACKEND", "x11", 1);
   state().display = XOpenDisplay(nullptr);
   state().screenSaverXDG = (bool)execute("xdg-screensaver", "--version").output.find("xdg-screensaver");