diff --git a/src/common/types.h b/src/common/types.h
index 6295006b4..600a06c8c 100644
--- a/src/common/types.h
+++ b/src/common/types.h
@@ -224,3 +224,6 @@ static constexpr u32 HOST_PAGE_SHIFT = 12;
                              static_cast<std::underlying_type<type_>::type>(rhs));                                     \
     return lhs;                                                                                                        \
   }
+
+// Compute the address of a base type given a field offset.
+#define BASE_FROM_RECORD_FIELD(ptr, base_type, field) ((base_type*)(((char*)ptr) - offsetof(base_type, field)))
diff --git a/src/util/image.cpp b/src/util/image.cpp
index e5af1007d..8cc05cf1c 100644
--- a/src/util/image.cpp
+++ b/src/util/image.cpp
@@ -518,7 +518,62 @@ bool JPEGBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size)
 
 bool JPEGFileLoader(RGBA8Image* image, const char* filename, std::FILE* fp)
 {
-  return WrapJPEGDecompress(image, [fp](jpeg_decompress_struct& info) { jpeg_stdio_src(&info, fp); });
+  static constexpr u32 BUFFER_SIZE = 16384;
+
+  struct FileCallback
+  {
+    jpeg_source_mgr mgr;
+
+    std::FILE* fp;
+    std::unique_ptr<u8[]> buffer;
+    bool end_of_file;
+  };
+
+  FileCallback cb = {
+    .mgr = {
+      .init_source = [](j_decompress_ptr cinfo) {},
+      .fill_input_buffer = [](j_decompress_ptr cinfo) -> boolean {
+        FileCallback* cb = BASE_FROM_RECORD_FIELD(cinfo->src, FileCallback, mgr);
+        cb->mgr.next_input_byte = cb->buffer.get();
+        if (cb->end_of_file)
+        {
+          cb->buffer[0] = 0xFF;
+          cb->buffer[1] = JPEG_EOI;
+          cb->mgr.bytes_in_buffer = 2;
+          return TRUE;
+        }
+
+        const size_t r = std::fread(cb->buffer.get(), 1, BUFFER_SIZE, cb->fp);
+        cb->end_of_file |= (std::feof(cb->fp) != 0);
+        cb->mgr.bytes_in_buffer = r;
+        return TRUE;
+      },
+      .skip_input_data =
+        [](j_decompress_ptr cinfo, long num_bytes) {
+          FileCallback* cb = BASE_FROM_RECORD_FIELD(cinfo->src, FileCallback, mgr);
+          const size_t skip_in_buffer = std::min<size_t>(cb->mgr.bytes_in_buffer, static_cast<size_t>(num_bytes));
+          cb->mgr.next_input_byte += skip_in_buffer;
+          cb->mgr.bytes_in_buffer -= skip_in_buffer;
+
+          const size_t seek_cur = static_cast<size_t>(num_bytes) - skip_in_buffer;
+          if (seek_cur > 0)
+          {
+            if (FileSystem::FSeek64(cb->fp, static_cast<size_t>(seek_cur), SEEK_CUR) != 0)
+            {
+              cb->end_of_file = true;
+              return;
+            }
+          }
+        },
+      .resync_to_restart = jpeg_resync_to_restart,
+      .term_source = [](j_decompress_ptr cinfo) {},
+    },
+    .fp = fp,
+    .buffer = std::make_unique<u8[]>(BUFFER_SIZE),
+    .end_of_file = false,
+  };
+
+  return WrapJPEGDecompress(image, [&cb](jpeg_decompress_struct& info) { info.src = &cb.mgr; });
 }
 
 template<typename T>
@@ -613,7 +668,49 @@ bool JPEGBufferSaver(const RGBA8Image& image, std::vector<u8>* buffer, u8 qualit
 
 bool JPEGFileSaver(const RGBA8Image& image, const char* filename, std::FILE* fp, u8 quality)
 {
-  return WrapJPEGCompress(image, quality, [fp](jpeg_compress_struct& info) { jpeg_stdio_dest(&info, fp); });
+  static constexpr u32 BUFFER_SIZE = 16384;
+
+  struct FileCallback
+  {
+    jpeg_destination_mgr mgr;
+
+    std::FILE* fp;
+    std::unique_ptr<u8[]> buffer;
+    bool write_error;
+  };
+
+  FileCallback cb = {
+    .mgr = {
+      .init_destination =
+        [](j_compress_ptr cinfo) {
+          FileCallback* cb = BASE_FROM_RECORD_FIELD(cinfo->dest, FileCallback, mgr);
+          cb->mgr.next_output_byte = cb->buffer.get();
+          cb->mgr.free_in_buffer = BUFFER_SIZE;
+        },
+      .empty_output_buffer = [](j_compress_ptr cinfo) -> boolean {
+        FileCallback* cb = BASE_FROM_RECORD_FIELD(cinfo->dest, FileCallback, mgr);
+        if (!cb->write_error)
+          cb->write_error |= (std::fwrite(cb->buffer.get(), 1, BUFFER_SIZE, cb->fp) != BUFFER_SIZE);
+
+        cb->mgr.next_output_byte = cb->buffer.get();
+        cb->mgr.free_in_buffer = BUFFER_SIZE;
+        return TRUE;
+      },
+      .term_destination =
+        [](j_compress_ptr cinfo) {
+          FileCallback* cb = BASE_FROM_RECORD_FIELD(cinfo->dest, FileCallback, mgr);
+          const size_t left = BUFFER_SIZE - cb->mgr.free_in_buffer;
+          if (left > 0 && !cb->write_error)
+            cb->write_error |= (std::fwrite(cb->buffer.get(), 1, left, cb->fp) != left);
+        },
+    },
+    .fp = fp,
+    .buffer = std::make_unique<u8[]>(BUFFER_SIZE),
+    .write_error = false,
+  };
+
+  return (WrapJPEGCompress(image, quality, [&cb](jpeg_compress_struct& info) { info.dest = &cb.mgr; }) &&
+          !cb.write_error);
 }
 
 bool WebPBufferLoader(RGBA8Image* image, const void* buffer, size_t buffer_size)
diff --git a/src/util/imgui_fullscreen.cpp b/src/util/imgui_fullscreen.cpp
index 6efa48df6..ebe9a4fbb 100644
--- a/src/util/imgui_fullscreen.cpp
+++ b/src/util/imgui_fullscreen.cpp
@@ -1,4 +1,4 @@
-// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
+// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
 
 #define IMGUI_DEFINE_MATH_OPERATORS
@@ -10,6 +10,7 @@
 
 #include "common/assert.h"
 #include "common/easing.h"
+#include "common/error.h"
 #include "common/file_system.h"
 #include "common/log.h"
 #include "common/lru_cache.h"
@@ -271,24 +272,37 @@ const std::shared_ptr<GPUTexture>& ImGuiFullscreen::GetPlaceholderTexture()
 std::optional<RGBA8Image> ImGuiFullscreen::LoadTextureImage(const char* path)
 {
   std::optional<RGBA8Image> image;
-
-  std::optional<std::vector<u8>> data;
   if (Path::IsAbsolute(path))
-    data = FileSystem::ReadBinaryFile(path);
-  else
-    data = Host::ReadResourceFile(path, true);
-  if (data.has_value())
   {
-    image = RGBA8Image();
-    if (!image->LoadFromBuffer(path, data->data(), data->size()))
+    Error error;
+    auto fp = FileSystem::OpenManagedCFile(path, "rb", &error);
+    if (fp)
     {
-      Log_ErrorPrintf("Failed to read texture resource '%s'", path);
-      image.reset();
+      image = RGBA8Image();
+      if (!image->LoadFromFile(path, fp.get()))
+        Log_ErrorFmt("Failed to read texture file '{}'", path);
+    }
+    else
+    {
+      Log_ErrorFmt("Failed to open texture file '{}': {}", path, error.GetDescription());
     }
   }
   else
   {
-    Log_ErrorPrintf("Failed to open texture resource '%s'", path);
+    std::optional<std::vector<u8>> data = Host::ReadResourceFile(path, true);
+    if (data.has_value())
+    {
+      image = RGBA8Image();
+      if (!image->LoadFromBuffer(path, data->data(), data->size()))
+      {
+        Log_ErrorFmt("Failed to read texture resource '{}'", path);
+        image.reset();
+      }
+    }
+    else
+    {
+      Log_ErrorFmt("Failed to open texture resource '{}'", path);
+    }
   }
 
   return image;