diff --git a/src/citra/config.cpp b/src/citra/config.cpp
index 5b460466e..aaa5f5ea4 100644
--- a/src/citra/config.cpp
+++ b/src/citra/config.cpp
@@ -164,6 +164,7 @@ void Config::ReadValues() {
 
     // Utility
     Settings::values.dump_textures = sdl2_config->GetBoolean("Utility", "dump_textures", false);
+    Settings::values.custom_textures = sdl2_config->GetBoolean("Utility", "custom_textures", false);
 
     // Audio
     Settings::values.enable_dsp_lle = sdl2_config->GetBoolean("Audio", "enable_dsp_lle", false);
diff --git a/src/citra/default_ini.h b/src/citra/default_ini.h
index 4a6194bc4..04e7a14b8 100644
--- a/src/citra/default_ini.h
+++ b/src/citra/default_ini.h
@@ -182,6 +182,10 @@ swap_screen =
 # 0 (default): Off, 1: On
 dump_textures =
 
+# Reads PNG files from load/textures/[Title ID]/ and replaces textures.
+# 0 (default): Off, 1: On
+custom_textures =
+
 [Audio]
 # Whether or not to enable DSP LLE
 # 0 (default): No, 1: Yes
diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp
index 362ac8b19..1e1727165 100644
--- a/src/citra_qt/configuration/config.cpp
+++ b/src/citra_qt/configuration/config.cpp
@@ -231,9 +231,11 @@ void Config::ReadControlValues() {
 }
 
 void Config::ReadUtilityValues() {
-
     qt_config->beginGroup("Utility");
+
     Settings::values.dump_textures = ReadSetting("dump_textures", false).toBool();
+    Settings::values.custom_textures = ReadSetting("custom_textures", false).toBool();
+
     qt_config->endGroup();
 }
 
@@ -704,6 +706,7 @@ void Config::SaveUtilityValues() {
     qt_config->beginGroup("Utility");
 
     WriteSetting("dump_textures", Settings::values.dump_textures, false);
+    WriteSetting("custom_textures", Settings::values.custom_textures, false);
 
     qt_config->endGroup();
 }
diff --git a/src/citra_qt/configuration/configure_graphics.cpp b/src/citra_qt/configuration/configure_graphics.cpp
index f74b9a211..5bfe2f993 100644
--- a/src/citra_qt/configuration/configure_graphics.cpp
+++ b/src/citra_qt/configuration/configure_graphics.cpp
@@ -88,6 +88,7 @@ void ConfigureGraphics::ApplyConfiguration() {
         static_cast<Settings::LayoutOption>(ui->layout_combobox->currentIndex());
     Settings::values.swap_screen = ui->swap_screen->isChecked();
     Settings::values.dump_textures = ui->toggle_dump_textures->isChecked();
+    Settings::values.custom_textures = ui->toggle_custom_textures->isChecked();
     Settings::values.bg_red = static_cast<float>(bg_color.redF());
     Settings::values.bg_green = static_cast<float>(bg_color.greenF());
     Settings::values.bg_blue = static_cast<float>(bg_color.blueF());
diff --git a/src/citra_qt/configuration/configure_graphics.ui b/src/citra_qt/configuration/configure_graphics.ui
index 9cac5f185..c95bbb800 100644
--- a/src/citra_qt/configuration/configure_graphics.ui
+++ b/src/citra_qt/configuration/configure_graphics.ui
@@ -334,6 +334,16 @@
       <string>Utility</string>
      </property>
      <layout class="QVBoxLayout" name="verticalLayout_4">
+      <item>
+       <widget class="QCheckBox" name="toggle_custom_textures">
+        <property name="toolTip">
+         <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Replace textures with PNG files.&lt;/p&gt;&lt;p&gt;Textures are loaded from load/textures/[Title ID]/.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+        </property>
+        <property name="text">
+         <string>Use Custom Textures (Hardware Renderer only)</string>
+        </property>
+       </widget>
+      </item>
       <item>
        <widget class="QCheckBox" name="toggle_dump_textures">
         <property name="toolTip">
@@ -347,19 +357,6 @@
      </layout>
     </widget>
    </item>
-   <item>
-    <spacer name="verticalSpacer">
-     <property name="orientation">
-      <enum>Qt::Vertical</enum>
-     </property>
-     <property name="sizeHint" stdset="0">
-      <size>
-       <width>20</width>
-       <height>40</height>
-      </size>
-     </property>
-    </spacer>
-   </item>
   </layout>
  </widget>
  <resources/>
diff --git a/src/common/common_paths.h b/src/common/common_paths.h
index b302c2b25..42f952b2c 100644
--- a/src/common/common_paths.h
+++ b/src/common/common_paths.h
@@ -45,6 +45,7 @@
 #define DLL_DIR "external_dlls"
 #define SHADER_DIR "shaders"
 #define DUMP_DIR "dump"
+#define LOAD_DIR "load"
 
 // Filenames
 // Files in the directory returned by GetUserPath(UserPath::LogDir)
diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp
index 1f7360fa6..656050fd5 100644
--- a/src/common/file_util.cpp
+++ b/src/common/file_util.cpp
@@ -713,6 +713,7 @@ void SetUserPath(const std::string& path) {
     g_paths.emplace(UserPath::DLLDir, user_path + DLL_DIR DIR_SEP);
     g_paths.emplace(UserPath::ShaderDir, user_path + SHADER_DIR DIR_SEP);
     g_paths.emplace(UserPath::DumpDir, user_path + DUMP_DIR DIR_SEP);
+    g_paths.emplace(UserPath::LoadDir, user_path + LOAD_DIR DIR_SEP);
 }
 
 const std::string& GetUserPath(UserPath path) {
diff --git a/src/common/file_util.h b/src/common/file_util.h
index 62c9f36a9..6fc7e4e11 100644
--- a/src/common/file_util.h
+++ b/src/common/file_util.h
@@ -27,6 +27,7 @@ enum class UserPath {
     ConfigDir,
     DLLDir,
     DumpDir,
+    LoadDir,
     LogDir,
     NANDDir,
     RootDir,
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 5ec409f1e..f6ce91f76 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -36,6 +36,8 @@ add_library(core STATIC
     core.h
     core_timing.cpp
     core_timing.h
+    custom_tex_cache.cpp
+    custom_tex_cache.h
     dumping/backend.cpp
     dumping/backend.h
     file_sys/archive_backend.cpp
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 03dfc59c9..4a1ac0b9a 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -16,10 +16,14 @@
 #include "core/cheats/cheats.h"
 #include "core/core.h"
 #include "core/core_timing.h"
+<<<<<<< HEAD
 #include "core/dumping/backend.h"
 #ifdef ENABLE_FFMPEG_VIDEO_DUMPER
 #include "core/dumping/ffmpeg_backend.h"
 #endif
+=======
+#include "core/custom_tex_cache.h"
+>>>>>>> 387a49d7... fix crashes, add custom texture cache, load textures from load directory
 #include "core/gdbstub/gdbstub.h"
 #include "core/hle/kernel/client_port.h"
 #include "core/hle/kernel/kernel.h"
@@ -146,12 +150,16 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st
         }
     }
     cheat_engine = std::make_unique<Cheats::CheatEngine>(*this);
+<<<<<<< HEAD
     u64 title_id{0};
     if (app_loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) {
         LOG_ERROR(Core, "Failed to find title id for ROM (Error {})",
                   static_cast<u32>(load_result));
     }
     perf_stats = std::make_unique<PerfStats>(title_id);
+=======
+    custom_tex_cache = std::make_unique<Core::CustomTexCache>();
+>>>>>>> 387a49d7... fix crashes, add custom texture cache, load textures from load directory
     status = ResultStatus::Success;
     m_emu_window = &emu_window;
     m_filepath = filepath;
@@ -290,12 +298,21 @@ const Cheats::CheatEngine& System::CheatEngine() const {
     return *cheat_engine;
 }
 
+<<<<<<< HEAD
 VideoDumper::Backend& System::VideoDumper() {
     return *video_dumper;
 }
 
 const VideoDumper::Backend& System::VideoDumper() const {
     return *video_dumper;
+=======
+Core::CustomTexCache& System::CustomTexCache() {
+    return *custom_tex_cache;
+}
+
+const Core::CustomTexCache& System::CustomTexCache() const {
+    return *custom_tex_cache;
+>>>>>>> 387a49d7... fix crashes, add custom texture cache, load textures from load directory
 }
 
 void System::RegisterMiiSelector(std::shared_ptr<Frontend::MiiSelector> mii_selector) {
diff --git a/src/core/core.h b/src/core/core.h
index d4747ae36..5cd9c5ea6 100644
--- a/src/core/core.h
+++ b/src/core/core.h
@@ -7,6 +7,7 @@
 #include <memory>
 #include <string>
 #include "common/common_types.h"
+#include "core/custom_tex_cache.h"
 #include "core/frontend/applets/mii_selector.h"
 #include "core/frontend/applets/swkbd.h"
 #include "core/loader/loader.h"
@@ -216,7 +217,12 @@ public:
     /// Gets a const reference to the video dumper backend
     const VideoDumper::Backend& VideoDumper() const;
 
-    std::unique_ptr<PerfStats> perf_stats;
+    /// Gets a reference to the custom texture cache system
+    Core::CustomTexCache& CustomTexCache();
+
+    /// Gets a const reference to the custom texture cache system
+    const Core::CustomTexCache& CustomTexCache() const;
+
     FrameLimiter frame_limiter;
 
     void SetStatus(ResultStatus new_status, const char* details = nullptr) {
@@ -289,6 +295,9 @@ private:
     /// Video dumper backend
     std::unique_ptr<VideoDumper::Backend> video_dumper;
 
+    /// Custom texture cache system
+    std::unique_ptr<Core::CustomTexCache> custom_tex_cache;
+
     /// RPC Server for scripting support
     std::unique_ptr<RPC::RPCServer> rpc_server;
 
diff --git a/src/core/custom_tex_cache.cpp b/src/core/custom_tex_cache.cpp
new file mode 100644
index 000000000..ed8eabafd
--- /dev/null
+++ b/src/core/custom_tex_cache.cpp
@@ -0,0 +1,27 @@
+#include <stdexcept>
+#include <vector>
+#include "common/common_types.h"
+#include "custom_tex_cache.h"
+
+namespace Core {
+const bool CustomTexCache::IsTextureDumped(const u64 hash) {
+    return dumped_textures.find(hash) != dumped_textures.end();
+}
+
+void CustomTexCache::SetTextureDumped(const u64 hash) {
+    dumped_textures[hash] = true;
+}
+
+const bool CustomTexCache::IsTextureCached(const u64 hash) {
+    return custom_textures.find(hash) != custom_textures.end();
+}
+
+const CustomTexInfo& CustomTexCache::LookupTexture(const u64 hash) {
+    return custom_textures.at(hash);
+}
+
+void CustomTexCache::CacheTexture(const u64 hash, const std::vector<u8>& tex, u32 width,
+                                  u32 height) {
+    custom_textures[hash] = {width, height, tex};
+}
+} // namespace Core
\ No newline at end of file
diff --git a/src/core/custom_tex_cache.h b/src/core/custom_tex_cache.h
new file mode 100644
index 000000000..a6c226fe6
--- /dev/null
+++ b/src/core/custom_tex_cache.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <unordered_map>
+#include <vector>
+#include "common/common_types.h"
+
+namespace Core {
+struct CustomTexInfo {
+    u32 width;
+    u32 height;
+    std::vector<u8> tex;
+};
+
+// TODO: think of a better name for this class...
+class CustomTexCache {
+public:
+    const bool IsTextureDumped(const u64 hash);
+    void SetTextureDumped(const u64 hash);
+
+    const bool IsTextureCached(const u64 hash);
+    const CustomTexInfo& LookupTexture(const u64 hash);
+    void CacheTexture(const u64 hash, const std::vector<u8>& tex, u32 width, u32 height);
+
+private:
+    std::unordered_map<u64, bool> dumped_textures;
+    std::unordered_map<u64, CustomTexInfo> custom_textures;
+};
+} // namespace Core
\ No newline at end of file
diff --git a/src/core/settings.cpp b/src/core/settings.cpp
index 6b28de6fb..079d276d0 100644
--- a/src/core/settings.cpp
+++ b/src/core/settings.cpp
@@ -88,6 +88,7 @@ void LogSettings() {
     LogSetting("Layout_LayoutOption", static_cast<int>(Settings::values.layout_option));
     LogSetting("Layout_SwapScreen", Settings::values.swap_screen);
     LogSetting("Utility_DumpTextures", Settings::values.dump_textures);
+    LogSetting("Utility_CustomTextures", Settings::values.custom_textures);
     LogSetting("Audio_EnableDspLle", Settings::values.enable_dsp_lle);
     LogSetting("Audio_EnableDspLleMultithread", Settings::values.enable_dsp_lle_multithread);
     LogSetting("Audio_OutputEngine", Settings::values.sink_id);
diff --git a/src/core/settings.h b/src/core/settings.h
index 5bbf98edd..1d00a71a6 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -171,6 +171,7 @@ struct Values {
     std::string pp_shader_name;
 
     bool dump_textures;
+    bool custom_textures;
 
     // Audio
     bool enable_dsp_lle;
diff --git a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp
index 878f5fd16..bf6359eb5 100644
--- a/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp
+++ b/src/video_core/renderer_opengl/gl_rasterizer_cache.cpp
@@ -24,6 +24,7 @@
 #include "common/scope_exit.h"
 #include "common/vector_math.h"
 #include "core/core.h"
+#include "core/custom_tex_cache.h"
 #include "core/frontend/emu_window.h"
 #include "core/hle/kernel/process.h"
 #include "core/memory.h"
@@ -856,7 +857,7 @@ void CachedSurface::FlushGLBuffer(PAddr flush_start, PAddr flush_end) {
 
 // TODO: move this function to a better place
 void FlipRGBA8Texture(std::vector<u8>& tex, u64 width, u64 height) {
-    assert(tex.size() = width * height * 4);
+    ASSERT(tex.size() == width * height * 4);
     const u64 line_size = width * 4;
     // Thanks MSVC for not being able to make variable length arrays
     u8* temp_row = new u8[line_size];
@@ -883,38 +884,60 @@ void CachedSurface::UploadGLTexture(const Common::Rectangle<u32>& rect, GLuint r
 
     ASSERT(gl_buffer_size == width * height * GetGLBytesPerPixel(pixel_format));
 
-    // Decode and dump texture if texture dumping is enabled
-    // or read texture and replace
-    bool should_dump = false;
-    bool should_use_custom_tex = false;
-    std::string dump_path;
+    // Read custom texture
+    auto& custom_tex_cache = Core::System::GetInstance().CustomTexCache();
+    bool dump_tex = false;
+    bool use_custom_tex = false;
+    std::string dump_path; // Has to be declared here for logging later
     std::vector<u8> decoded_png;
     u32 png_width;
     u32 png_height;
-    if (Settings::values.dump_textures) {
-        dump_path = fmt::format("{}/textures", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir));
-        if (!FileUtil::IsDirectory(dump_path))
-            FileUtil::CreateDir(dump_path);
-        dump_path += fmt::format(
-            "/{:016X}",
-            Core::System::GetInstance().Kernel().GetCurrentProcess()->codeset->program_id);
-        if (!FileUtil::IsDirectory(dump_path))
-            FileUtil::CreateDir(dump_path);
-        // Hash the encoded texture
-        const u64 tex_hash = Common::ComputeHash64(gl_buffer.get(), gl_buffer_size);
-        dump_path += fmt::format("/tex1_{}x{}_{:016X}_{}.png", width, height, tex_hash,
-                                 static_cast<u32>(pixel_format));
-        if (!FileUtil::Exists(dump_path))
-            should_dump = true;
-        else {
-            u32 lodepng_ret = lodepng::decode(decoded_png, png_width, png_height, dump_path);
-            if (lodepng_ret)
-                LOG_CRITICAL(Render_OpenGL, "Failed to load custom texture: {}",
-                             lodepng_error_text(lodepng_ret));
-            else {
-                FlipRGBA8Texture(decoded_png, png_width, png_height);
-                should_use_custom_tex = true;
+    u64 tex_hash = 0;
+
+    if (Settings::values.dump_textures || Settings::values.custom_textures)
+        tex_hash = Common::ComputeHash64(gl_buffer.get(), gl_buffer_size);
+
+    if (Settings::values.custom_textures) {
+        const std::string load_path = fmt::format(
+            "{}textures/{:016X}/tex1_{}x{}_{:016X}_{}.png",
+            FileUtil::GetUserPath(FileUtil::UserPath::LoadDir),
+            Core::System::GetInstance().Kernel().GetCurrentProcess()->codeset->program_id,
+            width, height, tex_hash, static_cast<u32>(pixel_format));
+
+        if (!custom_tex_cache.IsTextureCached(tex_hash)) {
+            if (FileUtil::Exists(load_path)) {
+                u32 lodepng_ret = lodepng::decode(decoded_png, png_width, png_height, load_path);
+                if (lodepng_ret)
+                    LOG_CRITICAL(Render_OpenGL, "Failed to load custom texture: {}",
+                                 lodepng_error_text(lodepng_ret));
+                else {
+                    LOG_INFO(Render_OpenGL, "Loaded custom texture from {}", load_path);
+                    FlipRGBA8Texture(decoded_png, png_width, png_height);
+                    custom_tex_cache.CacheTexture(tex_hash, decoded_png, png_width, png_height);
+                    use_custom_tex = true;
+                }
             }
+        } else {
+            const auto custom_tex_info = custom_tex_cache.LookupTexture(tex_hash);
+            decoded_png = custom_tex_info.tex;
+            png_width = custom_tex_info.width;
+            png_height = custom_tex_info.height;
+            use_custom_tex = true;
+        }
+    }
+
+    if (Settings::values.dump_textures) {
+        dump_path = fmt::format(
+            "{}textures/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir),
+            Core::System::GetInstance().Kernel().GetCurrentProcess()->codeset->program_id);
+        if (!FileUtil::CreateFullPath(dump_path))
+            LOG_ERROR(Render, "Unable to create {}", dump_path);
+
+        dump_path += fmt::format("tex1_{}x{}_{:016X}_{}.png", width, height, tex_hash,
+                                 static_cast<u32>(pixel_format));
+        if (!custom_tex_cache.IsTextureDumped(tex_hash) && !FileUtil::Exists(dump_path)) {
+            custom_tex_cache.SetTextureDumped(tex_hash);
+            dump_tex = true;
         }
     }
 
@@ -929,7 +952,7 @@ void CachedSurface::UploadGLTexture(const Common::Rectangle<u32>& rect, GLuint r
     // If not 1x scale, create 1x texture that we will blit from to replace texture subrect in
     // surface
     OGLTexture unscaled_tex;
-    if (res_scale != 1 && !should_use_custom_tex) {
+    if (res_scale != 1 && !use_custom_tex) {
         x0 = 0;
         y0 = 0;
 
@@ -946,7 +969,7 @@ void CachedSurface::UploadGLTexture(const Common::Rectangle<u32>& rect, GLuint r
 
     // Ensure no bad interactions with GL_UNPACK_ALIGNMENT
     ASSERT(stride * GetGLBytesPerPixel(pixel_format) % 4 == 0);
-    if (!should_use_custom_tex) {
+    if (!use_custom_tex) {
         glPixelStorei(GL_UNPACK_ROW_LENGTH, static_cast<GLint>(stride));
 
         glActiveTexture(GL_TEXTURE0);
@@ -968,11 +991,11 @@ void CachedSurface::UploadGLTexture(const Common::Rectangle<u32>& rect, GLuint r
     }
 
     glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
-    if (should_dump) {
+    if (dump_tex) {
         // Dump texture to RGBA8 and encode as PNG
         LOG_INFO(Render_OpenGL, "Dumping texture to {}", dump_path);
         std::vector<u8> decoded_texture;
-        decoded_texture.resize(rect.GetWidth() * rect.GetHeight() * 4);
+        decoded_texture.resize(width * height * 4);
         glBindTexture(GL_TEXTURE_2D, target_tex);
         glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, &decoded_texture[0]);
         glBindTexture(GL_TEXTURE_2D, 0);
@@ -982,12 +1005,13 @@ void CachedSurface::UploadGLTexture(const Common::Rectangle<u32>& rect, GLuint r
             LOG_CRITICAL(Render_OpenGL, "Failed to save decoded texture! {}",
                          lodepng_error_text(png_error));
         }
+        custom_tex_cache.SetTextureDumped(tex_hash);
     }
 
     cur_state.texture_units[0].texture_2d = old_tex;
     cur_state.Apply();
 
-    if (res_scale != 1 && !should_use_custom_tex) {
+    if (res_scale != 1 && !use_custom_tex) {
         auto scaled_rect = rect;
         scaled_rect.left *= res_scale;
         scaled_rect.top *= res_scale;