diff --git a/src/core/arm/arm_interface.h b/src/core/arm/arm_interface.h
index b3d8ceaf8f..70098c5266 100644
--- a/src/core/arm/arm_interface.h
+++ b/src/core/arm/arm_interface.h
@@ -70,12 +70,19 @@ public:
     /// Clear all instruction cache
     virtual void ClearInstructionCache() = 0;
 
-    /// Notifies CPU emulation that the current page table has changed.
-    ///
-    /// @param new_page_table                 The new page table.
-    /// @param new_address_space_size_in_bits The new usable size of the address space in bits.
-    ///                                       This can be either 32, 36, or 39 on official software.
-    ///
+    /**
+     * Clear instruction cache range
+     * @param addr Start address of the cache range to clear
+     * @param size Size of the cache range to clear, starting at addr
+     */
+    virtual void InvalidateCacheRange(VAddr addr, std::size_t size) = 0;
+
+    /**
+     * Notifies CPU emulation that the current page table has changed.
+     *  @param new_page_table                 The new page table.
+     *  @param new_address_space_size_in_bits The new usable size of the address space in bits.
+     *                                        This can be either 32, 36, or 39 on official software.
+     */
     virtual void PageTableChanged(Common::PageTable& new_page_table,
                                   std::size_t new_address_space_size_in_bits) = 0;
 
diff --git a/src/core/arm/dynarmic/arm_dynarmic_32.cpp b/src/core/arm/dynarmic/arm_dynarmic_32.cpp
index af23206f59..193fd7d62a 100644
--- a/src/core/arm/dynarmic/arm_dynarmic_32.cpp
+++ b/src/core/arm/dynarmic/arm_dynarmic_32.cpp
@@ -286,6 +286,13 @@ void ARM_Dynarmic_32::ClearInstructionCache() {
     jit->ClearCache();
 }
 
+void ARM_Dynarmic_32::InvalidateCacheRange(VAddr addr, std::size_t size) {
+    if (!jit) {
+        return;
+    }
+    jit->InvalidateCacheRange(static_cast<u32>(addr), size);
+}
+
 void ARM_Dynarmic_32::ClearExclusiveState() {
     jit->ClearExclusiveState();
 }
diff --git a/src/core/arm/dynarmic/arm_dynarmic_32.h b/src/core/arm/dynarmic/arm_dynarmic_32.h
index e16b689c85..35e9ced484 100644
--- a/src/core/arm/dynarmic/arm_dynarmic_32.h
+++ b/src/core/arm/dynarmic/arm_dynarmic_32.h
@@ -59,6 +59,7 @@ public:
     void ClearExclusiveState() override;
 
     void ClearInstructionCache() override;
+    void InvalidateCacheRange(VAddr addr, std::size_t size) override;
     void PageTableChanged(Common::PageTable& new_page_table,
                           std::size_t new_address_space_size_in_bits) override;
 
diff --git a/src/core/arm/dynarmic/arm_dynarmic_64.cpp b/src/core/arm/dynarmic/arm_dynarmic_64.cpp
index 1c9fd18b5f..0f0585d0fa 100644
--- a/src/core/arm/dynarmic/arm_dynarmic_64.cpp
+++ b/src/core/arm/dynarmic/arm_dynarmic_64.cpp
@@ -322,6 +322,13 @@ void ARM_Dynarmic_64::ClearInstructionCache() {
     jit->ClearCache();
 }
 
+void ARM_Dynarmic_64::InvalidateCacheRange(VAddr addr, std::size_t size) {
+    if (!jit) {
+        return;
+    }
+    jit->InvalidateCacheRange(addr, size);
+}
+
 void ARM_Dynarmic_64::ClearExclusiveState() {
     jit->ClearExclusiveState();
 }
diff --git a/src/core/arm/dynarmic/arm_dynarmic_64.h b/src/core/arm/dynarmic/arm_dynarmic_64.h
index aa0a5c424c..329b59a32a 100644
--- a/src/core/arm/dynarmic/arm_dynarmic_64.h
+++ b/src/core/arm/dynarmic/arm_dynarmic_64.h
@@ -56,6 +56,7 @@ public:
     void ClearExclusiveState() override;
 
     void ClearInstructionCache() override;
+    void InvalidateCacheRange(VAddr addr, std::size_t size) override;
     void PageTableChanged(Common::PageTable& new_page_table,
                           std::size_t new_address_space_size_in_bits) override;
 
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 58368fe3cc..01e4faac8f 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -457,6 +457,10 @@ void System::InvalidateCpuInstructionCaches() {
     impl->kernel.InvalidateAllInstructionCaches();
 }
 
+void System::InvalidateCpuInstructionCacheRange(VAddr addr, std::size_t size) {
+    impl->kernel.InvalidateCpuInstructionCacheRange(addr, size);
+}
+
 void System::Shutdown() {
     impl->Shutdown();
 }
diff --git a/src/core/core.h b/src/core/core.h
index f642befc02..29b8fb92a1 100644
--- a/src/core/core.h
+++ b/src/core/core.h
@@ -166,6 +166,8 @@ public:
      */
     void InvalidateCpuInstructionCaches();
 
+    void InvalidateCpuInstructionCacheRange(VAddr addr, std::size_t size);
+
     /// Shutdown the emulated system.
     void Shutdown();
 
diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp
index c426b63786..929db696d3 100644
--- a/src/core/hle/kernel/kernel.cpp
+++ b/src/core/hle/kernel/kernel.cpp
@@ -497,12 +497,17 @@ const Core::ExclusiveMonitor& KernelCore::GetExclusiveMonitor() const {
 }
 
 void KernelCore::InvalidateAllInstructionCaches() {
-    if (!IsMulticore()) {
-        for (auto& physical_core : impl->cores) {
-            physical_core.ArmInterface().ClearInstructionCache();
+    for (auto& physical_core : impl->cores) {
+        physical_core.ArmInterface().ClearInstructionCache();
+    }
+}
+
+void KernelCore::InvalidateCpuInstructionCacheRange(VAddr addr, std::size_t size) {
+    for (auto& physical_core : impl->cores) {
+        if (!physical_core.IsInitialized()) {
+            continue;
         }
-    } else {
-        UNIMPLEMENTED();
+        physical_core.ArmInterface().InvalidateCacheRange(addr, size);
     }
 }
 
diff --git a/src/core/hle/kernel/kernel.h b/src/core/hle/kernel/kernel.h
index a9fdc58600..a73a930396 100644
--- a/src/core/hle/kernel/kernel.h
+++ b/src/core/hle/kernel/kernel.h
@@ -156,6 +156,8 @@ public:
 
     void InvalidateAllInstructionCaches();
 
+    void InvalidateCpuInstructionCacheRange(VAddr addr, std::size_t size);
+
     /// Adds a port to the named port table
     void AddNamedPort(std::string name, std::shared_ptr<ClientPort> port);
 
diff --git a/src/core/hle/kernel/memory/page_table.cpp b/src/core/hle/kernel/memory/page_table.cpp
index a3fadb5330..f53a7be820 100644
--- a/src/core/hle/kernel/memory/page_table.cpp
+++ b/src/core/hle/kernel/memory/page_table.cpp
@@ -670,6 +670,11 @@ ResultCode PageTable::SetCodeMemoryPermission(VAddr addr, std::size_t size, Memo
         return RESULT_SUCCESS;
     }
 
+    if ((prev_perm & MemoryPermission::Execute) != (perm & MemoryPermission::Execute)) {
+        // Memory execution state is changing, invalidate CPU cache range
+        system.InvalidateCpuInstructionCacheRange(addr, size);
+    }
+
     const std::size_t num_pages{size / PageSize};
     const OperationType operation{(perm & MemoryPermission::Execute) != MemoryPermission::None
                                       ? OperationType::ChangePermissionsAndRefresh
diff --git a/src/core/hle/kernel/physical_core.h b/src/core/hle/kernel/physical_core.h
index ace058a5a8..37513130a8 100644
--- a/src/core/hle/kernel/physical_core.h
+++ b/src/core/hle/kernel/physical_core.h
@@ -58,6 +58,10 @@ public:
     // Shutdown this physical core.
     void Shutdown();
 
+    bool IsInitialized() const {
+        return arm_interface != nullptr;
+    }
+
     Core::ARM_Interface& ArmInterface() {
         return *arm_interface;
     }
diff --git a/src/core/hle/service/ldr/ldr.cpp b/src/core/hle/service/ldr/ldr.cpp
index fff68326ba..9da786b4ef 100644
--- a/src/core/hle/service/ldr/ldr.cpp
+++ b/src/core/hle/service/ldr/ldr.cpp
@@ -527,9 +527,6 @@ public:
                                      header.segment_headers[RO_INDEX].memory_size,
                                      header.segment_headers[DATA_INDEX].memory_size, nro_address});
 
-        // Invalidate JIT caches for the newly mapped process code
-        system.InvalidateCpuInstructionCaches();
-
         IPC::ResponseBuilder rb{ctx, 4};
         rb.Push(RESULT_SUCCESS);
         rb.Push(*map_result);
@@ -590,8 +587,6 @@ public:
 
         const auto result{UnmapNro(iter->second)};
 
-        system.InvalidateCpuInstructionCaches();
-
         nro.erase(iter);
 
         IPC::ResponseBuilder rb{ctx, 2};